blob: 9efbf64f92b4c83ab884d6cb8a25b2f81ab0d878 [file] [log] [blame]
// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
library protocol;
import 'dart:convert' show JsonDecoder;
/**
* An abstract enumeration.
*/
abstract class Enum2<E extends Enum2> implements Comparable<E> {
/**
* The name of this enum constant, as declared in the enum declaration.
*/
final String name;
/**
* The position in the enum declaration.
*/
final int ordinal;
const Enum2(this.name, this.ordinal);
@override
int get hashCode => ordinal;
@override
String toString() => name;
int compareTo(E other) => ordinal - other.ordinal;
/**
* Returns the enum constant with the given [name], `null` if not found.
*/
static Enum2 valueOf(List<Enum2> values, String name) {
for (int i = 0; i < values.length; i++) {
Enum2 value = values[i];
if (value.name == name) {
return value;
}
}
return null;
}
}
/**
* Instances of the class [Request] represent a request that was received.
*/
class Request {
/**
* The name of the JSON attribute containing the id of the request.
*/
static const String ID = 'id';
/**
* The name of the JSON attribute containing the name of the request.
*/
static const String METHOD = 'method';
/**
* The name of the JSON attribute containing the request parameters.
*/
static const String PARAMS = 'params';
/**
* The unique identifier used to identify this request.
*/
final String id;
/**
* The method being requested.
*/
final String method;
/**
* A table mapping the names of request parameters to their values.
*/
final Map<String, Object> params = new Map<String, Object>();
/**
* A decoder that can be used to decode strings into JSON objects.
*/
static const JsonDecoder DECODER = const JsonDecoder(null);
/**
* Initialize a newly created [Request] to have the given [id] and [method]
* name.
*/
Request(this.id, this.method);
/**
* Return a request parsed from the given [data], or `null` if the [data] is
* not a valid json representation of a request. The [data] is expected to
* have the following format:
*
* {
* 'id': String,
* 'method': methodName,
* 'params': {
* paramter_name: value
* }
* }
*
* where the parameters are optional and can contain any number of name/value
* pairs.
*/
factory Request.fromString(String data) {
try {
var result = DECODER.convert(data);
if (result is! Map) {
return null;
}
var id = result[Request.ID];
var method = result[Request.METHOD];
if (id is! String || method is! String) {
return null;
}
var params = result[Request.PARAMS];
Request request = new Request(id, method);
if (params is Map) {
params.forEach((String key, Object value) {
request.setParameter(key, value);
});
} else if (params != null) {
return null;
}
return request;
} catch (exception) {
return null;
}
}
/**
* Return the value of the parameter with the given [name], or [defaultValue]
* if there is no such parameter associated with this request.
*/
RequestDatum getParameter(String name, defaultValue) {
Object value = params[name];
if (value == null) {
return new RequestDatum(this, "default for $name", defaultValue);
}
return new RequestDatum(this, name, params[name]);
}
/**
* Return the value of the parameter with the given [name], or throw a
* [RequestFailure] exception with an appropriate error message if there is no
* such parameter associated with this request.
*/
RequestDatum getRequiredParameter(String name) {
Object value = params[name];
if (value == null) {
throw new RequestFailure(new Response.missingRequiredParameter(this, name));
}
return new RequestDatum(this, name, value);
}
/**
* Set the value of the parameter with the given [name] to the given [value].
*/
void setParameter(String name, Object value) {
params[name] = value;
}
/**
* Return a table representing the structure of the Json object that will be
* sent to the client to represent this response.
*/
Map<String, Object> toJson() {
Map<String, Object> jsonObject = new Map<String, Object>();
jsonObject[ID] = id;
jsonObject[METHOD] = method;
if (params.isNotEmpty) {
jsonObject[PARAMS] = params;
}
return jsonObject;
}
}
/**
* Instances of the class [RequestDatum] wrap a piece of data from a
* request parameter, and contain accessor methods which automatically validate
* and convert the data into the appropriate form.
*/
class RequestDatum {
/**
* Request object that should be referred to in any errors that are
* generated.
*/
final Request request;
/**
* String description of how [datum] was obtained from the request.
*/
final String path;
/**
* Value to be decoded and validated.
*/
final dynamic datum;
/**
* Create a RequestDatum for decoding and validating [datum], which refers to
* [request] in any errors it reports.
*/
RequestDatum(this.request, this.path, this.datum);
/**
* Validate that the datum is a Map containing the given [key], and return
* a [RequestDatum] containing the corresponding value.
*/
RequestDatum operator [](String key) {
if (datum is! Map) {
throw new RequestFailure(new Response.invalidParameter(request, path,
"be a map"));
}
if (!datum.containsKey(key)) {
throw new RequestFailure(new Response.invalidParameter(request, path,
"contain key '$key'"));
}
return new RequestDatum(request, "$path.$key", datum[key]);
}
/**
* Return `true` if the datum is a Map containing the given [key].
*/
bool hasKey(String key) {
if (datum is! Map) {
throw new RequestFailure(new Response.invalidParameter(request, path,
"be a map"));
}
return datum.containsKey(key);
}
/**
* Validate that the datum is a Map whose keys are strings, and call [f] on
* each key/value pair in the map.
*/
void forEachMap(void f(String key, RequestDatum value)) {
if (datum is! Map) {
throw new RequestFailure(new Response.invalidParameter(request, path,
"be a map"));
}
datum.forEach((String key, value) {
f(key, new RequestDatum(request, "$path.$key", value));
});
}
/**
* Validate that the datum is an integer (or a string that can be parsed
* as an integer), and return the int.
*/
int asInt() {
if (datum is int) {
return datum;
} else if (datum is String) {
return int.parse(datum, onError: (String value) {
throw new RequestFailure(new Response.invalidParameter(request, path,
"be an integer"));
});
}
throw new RequestFailure(new Response.invalidParameter(request, path,
"be an integer"));
}
/**
* Validate that the datum is a boolean (or a string that can be parsed
* as a boolean), and return the bool.
*
* The value is typically the result of invoking either [getParameter] or
* [getRequiredParameter].
*/
bool asBool() {
if (datum is bool) {
return datum;
} else if (datum == 'true') {
return true;
} else if (datum == 'false') {
return false;
}
throw new RequestFailure(new Response.invalidParameter(request, datum,
"be a boolean"));
}
/**
* Validate that the datum is a string, and return it.
*/
String asString() {
if (datum is! String) {
throw new RequestFailure(new Response.invalidParameter(request, path,
"be a string"));
}
return datum;
}
/**
* Determine if the datum is a list of strings.
*/
bool get isStringList {
if (datum is! List) {
return false;
}
for (var element in datum) {
if (element is! String) {
return false;
}
}
return true;
}
/**
* Validate that the datum is a list of strings, and return it.
*/
List<String> asStringList() {
if (!isStringList) {
throw new RequestFailure(new Response.invalidParameter(request, path,
"be a list of strings"));
}
return datum;
}
/**
* Validate that the datum is a list of strings, and convert it into [Enum]s.
*/
Set<Enum2> asEnumSet(List<Enum2> allValues) {
Set values = new Set();
for (String name in asStringList()) {
Enum2 value = Enum2.valueOf(allValues, name);
if (value == null) {
throw new RequestFailure(new Response.invalidParameter(request, path,
"be a list of names from the list $allValues"));
}
values.add(value);
}
return values;
}
/**
* Determine if the datum is a map whose values are all strings.
*
* Note: we can safely assume that the keys are all strings, since JSON maps
* cannot have any other key type.
*/
bool get isStringMap {
if (datum is! Map) {
return false;
}
for (var value in datum.values) {
if (value is! String) {
return false;
}
}
return true;
}
/**
* Validate that the datum is a map from strings to strings, and return it.
*/
Map<String, String> asStringMap() {
if (!isStringMap) {
throw new RequestFailure(new Response.invalidParameter(request, path,
"be a string map"));
}
return datum;
}
/**
* Determine if the datum is a map whose values are all string lists.
*
* Note: we can safely assume that the keys are all strings, since JSON maps
* cannot have any other key type.
*/
bool isStringListMap() {
if (datum is! Map) {
return false;
}
for (var value in datum.values) {
if (value is! List) {
return false;
}
for (var listItem in value) {
if (listItem is! String) {
return false;
}
}
}
return true;
}
/**
* Validate that the datum is a map from strings to string listss, and return
* it.
*/
Map<String, List<String>> asStringListMap() {
if (!isStringListMap()) {
throw new RequestFailure(new Response.invalidParameter(request, path,
"be a string list map"));
}
return datum;
}
}
/**
* Instances of the class [Response] represent a response to a request.
*/
class Response {
/**
* The name of the JSON attribute containing the id of the request for which
* this is a response.
*/
static const String ID = 'id';
/**
* The name of the JSON attribute containing the error message.
*/
static const String ERROR = 'error';
/**
* The name of the JSON attribute containing the result values.
*/
static const String RESULT = 'result';
/**
* The unique identifier used to identify the request that this response is
* associated with.
*/
final String id;
/**
* The error that was caused by attempting to handle the request, or `null` if
* there was no error.
*/
final RequestError error;
/**
* A table mapping the names of result fields to their values. The table
* should be empty if there was an error.
*/
final Map<String, Object> result = new Map<String, Object>();
/**
* Initialize a newly created instance to represent a response to a request
* with the given [id]. If an [error] is provided then the response will
* represent an error condition.
*/
Response(this.id, [this.error]);
/**
* Initialize a newly created instance to represent an error condition caused
* by a [request] referencing a context that does not exist.
*/
Response.contextDoesNotExist(Request request)
: this(request.id, new RequestError(-1, 'Context does not exist'));
/**
* Initialize a newly created instance to represent an error condition caused
* by a [request] that had invalid parameter. [path] is the path to the
* invalid parameter, in Javascript notation (e.g. "foo.bar" means that the
* parameter "foo" contained a key "bar" whose value was the wrong type).
* [expectation] is a description of the type of data that was expected.
*/
Response.invalidParameter(Request request, String path, String expectation)
: this(request.id, new RequestError(-2,
"Expected parameter $path to $expectation"));
/**
* Initialize a newly created instance to represent an error condition caused
* by a malformed request.
*/
Response.invalidRequestFormat()
: this('', new RequestError(-4, 'Invalid request'));
/**
* Initialize a newly created instance to represent an error condition caused
* by a [request] that does not have a required parameter.
*/
Response.missingRequiredParameter(Request request, String parameterName)
: this(request.id, new RequestError(-5, 'Missing required parameter: $parameterName'));
/**
* Initialize a newly created instance to represent an error condition caused
* by a [request] that takes a set of analysis options but for which an
* unknown analysis option was provided.
*/
Response.unknownAnalysisOption(Request request, String optionName)
: this(request.id, new RequestError(-6, 'Unknown analysis option: "$optionName"'));
/**
* Initialize a newly created instance to represent an error condition caused
* by a [request] that cannot be handled by any known handlers.
*/
Response.unknownRequest(Request request)
: this(request.id, new RequestError(-7, 'Unknown request'));
Response.contextAlreadyExists(Request request)
: this(request.id, new RequestError(-8, 'Context already exists'));
Response.unsupportedFeature(String requestId, String message)
: this(requestId, new RequestError(-9, message));
/**
* Initialize a newly created instance to represent an error condition caused
* by a `analysis.setSubscriptions` [request] that includes an unknown
* analysis service name.
*/
Response.unknownAnalysisService(Request request, String name)
: this(request.id, new RequestError(-10, 'Unknown analysis service: "$name"'));
/**
* Initialize a newly created instance based upon the given JSON data
*/
factory Response.fromJson(Map<String, Object> json) {
try {
Object id = json[Response.ID];
if (id is! String) {
return null;
}
Object error = json[Response.ERROR];
Object result = json[Response.RESULT];
Response response;
if (error is Map) {
response = new Response(id, new RequestError.fromJson(error));
} else {
response = new Response(id);
}
if (result is Map) {
result.forEach((String key, Object value) {
response.setResult(key, value);
});
}
return response;
} catch (exception) {
return null;
}
}
/**
* Return the value of the result field with the given [name].
*/
Object getResult(String name) {
return result[name];
}
/**
* Set the value of the result field with the given [name] to the given [value].
*/
void setResult(String name, Object value) {
result[name] = value;
}
/**
* Return a table representing the structure of the Json object that will be
* sent to the client to represent this response.
*/
Map<String, Object> toJson() {
Map<String, Object> jsonObject = new Map<String, Object>();
jsonObject[ID] = id;
if (error != null) {
jsonObject[ERROR] = error.toJson();
}
if (!result.isEmpty) {
jsonObject[RESULT] = result;
}
return jsonObject;
}
}
/**
* Instances of the class [RequestError] represent information about an error
* that occurred while attempting to respond to a [Request].
*/
class RequestError {
/**
* The name of the JSON attribute containing the code that uniquely identifies
* the error that occurred.
*/
static const String CODE = 'code';
/**
* The name of the JSON attribute containing an object with additional data
* related to the error.
*/
static const String DATA = 'data';
/**
* The name of the JSON attribute containing a short description of the error.
*/
static const String MESSAGE = 'message';
/**
* An error code indicating a parse error. Invalid JSON was received by the
* server. An error occurred on the server while parsing the JSON text.
*/
static const int CODE_PARSE_ERROR = -32700;
/**
* An error code indicating that the analysis server has already been
* started (and hence won't accept new connections).
*/
static const int CODE_SERVER_ALREADY_STARTED = -32701;
/**
* An error code indicating an invalid request. The JSON sent is not a valid
* [Request] object.
*/
static const int CODE_INVALID_REQUEST = -32600;
/**
* An error code indicating a method not found. The method does not exist or
* is not currently available.
*/
static const int CODE_METHOD_NOT_FOUND = -32601;
/**
* An error code indicating one or more invalid parameters.
*/
static const int CODE_INVALID_PARAMS = -32602;
/**
* An error code indicating an internal error.
*/
static const int CODE_INTERNAL_ERROR = -32603;
/**
* An error code indicating a problem using the specified Dart SDK.
*/
static const int CODE_SDK_ERROR = -32603;
/*
* In addition, codes -32000 to -32099 indicate a server error. They are
* reserved for implementation-defined server-errors.
*/
/**
* The code that uniquely identifies the error that occurred.
*/
final int code;
/**
* A short description of the error.
*/
final String message;
/**
* A table mapping the names of notification parameters to their values.
*/
final Map<String, Object> data = new Map<String, Object>();
/**
* Initialize a newly created [Error] to have the given [code] and [message].
*/
RequestError(this.code, this.message);
/**
* Initialize a newly created [Error] to indicate a parse error. Invalid JSON
* was received by the server. An error occurred on the server while parsing
* the JSON text.
*/
RequestError.parseError() : this(CODE_PARSE_ERROR, "Parse error");
/**
* Initialize a newly created [Error] to indicate that the analysis server
* has already been started (and hence won't accept new connections).
*/
RequestError.serverAlreadyStarted()
: this(CODE_SERVER_ALREADY_STARTED, "Server already started");
/**
* Initialize a newly created [Error] to indicate an invalid request. The
* JSON sent is not a valid [Request] object.
*/
RequestError.invalidRequest() : this(CODE_INVALID_REQUEST, "Invalid request");
/**
* Initialize a newly created [Error] to indicate that a method was not found.
* Either the method does not exist or is not currently available.
*/
RequestError.methodNotFound() : this(CODE_METHOD_NOT_FOUND, "Method not found");
/**
* Initialize a newly created [Error] to indicate one or more invalid
* parameters.
*/
RequestError.invalidParameters() : this(CODE_INVALID_PARAMS, "Invalid parameters");
/**
* Initialize a newly created [Error] to indicate an internal error.
*/
RequestError.internalError() : this(CODE_INTERNAL_ERROR, "Internal error");
/**
* Initialize a newly created [Error] from the given JSON.
*/
factory RequestError.fromJson(Map<String, Object> json) {
try {
int code = json[RequestError.CODE];
String message = json[RequestError.MESSAGE];
Map<String, Object> data = json[RequestError.DATA];
RequestError requestError = new RequestError(code, message);
if (data != null) {
data.forEach((String key, Object value) {
requestError.setData(key, value);
});
}
return requestError;
} catch (exception) {
return null;
}
}
/**
* Return the value of the data with the given [name], or `null` if there is
* no such data associated with this error.
*/
Object getData(String name) => data[name];
/**
* Set the value of the data with the given [name] to the given [value].
*/
void setData(String name, Object value) {
data[name] = value;
}
/**
* Return a table representing the structure of the Json object that will be
* sent to the client to represent this response.
*/
Map<String, Object> toJson() {
Map<String, Object> jsonObject = new Map<String, Object>();
jsonObject[CODE] = code;
jsonObject[MESSAGE] = message;
if (!data.isEmpty) {
jsonObject[DATA] = data;
}
return jsonObject;
}
@override
String toString() => toJson().toString();
}
/**
* Instances of the class [Notification] represent a notification from the
* server about an event that occurred.
*/
class Notification {
/**
* The name of the JSON attribute containing the name of the event that
* triggered the notification.
*/
static const String EVENT = 'event';
/**
* The name of the JSON attribute containing the result values.
*/
static const String PARAMS = 'params';
/**
* The name of the event that triggered the notification.
*/
final String event;
/**
* A table mapping the names of notification parameters to their values.
*/
final Map<String, Object> params = new Map<String, Object>();
/**
* Initialize a newly created [Notification] to have the given [event] name.
*/
Notification(this.event);
/**
* Initialize a newly created instance based upon the given JSON data
*/
factory Notification.fromJson(Map<String, Object> json) {
try {
String event = json[Notification.EVENT];
Object params = json[Notification.PARAMS];
Notification notification = new Notification(event);
if (params is Map) {
params.forEach((String key, Object value) {
notification.setParameter(key, value);
});
}
return notification;
} catch (exception) {
return null;
}
}
/**
* Return the value of the parameter with the given [name], or `null` if there
* is no such parameter associated with this notification.
*/
Object getParameter(String name) => params[name];
/**
* Set the value of the parameter with the given [name] to the given [value].
*/
void setParameter(String name, Object value) {
params[name] = value;
}
/**
* Return a table representing the structure of the Json object that will be
* sent to the client to represent this response.
*/
Map<String, Object> toJson() {
Map<String, Object> jsonObject = new Map<String, Object>();
jsonObject[EVENT] = event;
if (!params.isEmpty) {
jsonObject[PARAMS] = params;
}
return jsonObject;
}
}
/**
* Instances of the class [RequestHandler] implement a handler that can handle
* requests and produce responses for them.
*/
abstract class RequestHandler {
/**
* Attempt to handle the given [request]. If the request is not recognized by
* this handler, return `null` so that other handlers will be given a chance
* to handle it. Otherwise, return the response that should be passed back to
* the client.
*/
Response handleRequest(Request request);
}
/**
* Instances of the class [RequestFailure] represent an exception that occurred
* during the handling of a request that requires that an error be returned to
* the client.
*/
class RequestFailure implements Exception {
/**
* The response to be returned as a result of the failure.
*/
final Response response;
/**
* Initialize a newly created exception to return the given reponse.
*/
RequestFailure(this.response);
}