blob: 19c03e31f4f207b12b651d8ebd878230ecd26e42 [file] [log] [blame]
// Copyright (c) 2017, 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.
/**
* Data structures representing an API definition, and visitor base classes
* for visiting those data structures.
*/
import 'dart:collection';
import 'package:html/dom.dart' as dom;
/**
* Toplevel container for the API.
*/
class Api extends ApiNode {
final String version;
final List<Domain> domains;
final Types types;
final Refactorings refactorings;
Api(this.version, this.domains, this.types, this.refactorings,
dom.Element html,
{bool experimental})
: super(html, experimental, false);
}
/**
* Base class for objects in the API model.
*/
class ApiNode {
/**
* A flag to indicate if this API is experimental.
*/
final bool experimental;
/**
* A flag to indicate if this API is deprecated.
*/
final bool deprecated;
/**
* Html element representing this part of the API.
*/
final dom.Element html;
ApiNode(this.html, bool experimental, bool deprecated)
: this.experimental = experimental ?? false,
this.deprecated = deprecated ?? false;
}
/**
* Base class for visiting the API definition.
*/
abstract class ApiVisitor<T> {
/**
* Dispatch the given [type] to the visitor.
*/
T visitTypeDecl(TypeDecl type) => type.accept(this) as T;
T visitTypeEnum(TypeEnum typeEnum);
T visitTypeList(TypeList typeList);
T visitTypeMap(TypeMap typeMap);
T visitTypeObject(TypeObject typeObject);
T visitTypeReference(TypeReference typeReference);
T visitTypeUnion(TypeUnion typeUnion);
}
/**
* Definition of a single domain.
*/
class Domain extends ApiNode {
final String name;
final List<Request> requests;
final List<Notification> notifications;
Domain(this.name, this.requests, this.notifications, dom.Element html,
{bool experimental, bool deprecated})
: super(html, experimental, deprecated);
}
/**
* API visitor that visits the entire API hierarchically by default.
*/
class HierarchicalApiVisitor extends ApiVisitor {
/**
* The API to visit.
*/
final Api api;
HierarchicalApiVisitor(this.api);
/**
* If [type] is a [TypeReference] that is defined in the API, follow the
* chain until a non-[TypeReference] is found, if possible.
*
* If it is not possible (because the chain ends with a [TypeReference] that
* is not defined in the API), then that final [TypeReference] is returned.
*/
TypeDecl resolveTypeReferenceChain(TypeDecl type) {
while (type is TypeReference && api.types.containsKey(type.typeName)) {
type = api.types[(type as TypeReference).typeName].type;
}
return type;
}
void visitApi() {
api.domains.forEach(visitDomain);
visitTypes(api.types);
visitRefactorings(api.refactorings);
}
void visitDomain(Domain domain) {
domain.requests.forEach(visitRequest);
domain.notifications.forEach(visitNotification);
}
void visitNotification(Notification notification) {
if (notification.params != null) {
visitTypeDecl(notification.params);
}
}
void visitRefactoring(Refactoring refactoring) {
if (refactoring.feedback != null) {
visitTypeDecl(refactoring.feedback);
}
if (refactoring.options != null) {
visitTypeDecl(refactoring.options);
}
}
void visitRefactorings(Refactorings refactorings) {
refactorings?.forEach(visitRefactoring);
}
void visitRequest(Request request) {
if (request.params != null) {
visitTypeDecl(request.params);
}
if (request.result != null) {
visitTypeDecl(request.result);
}
}
void visitTypeDefinition(TypeDefinition typeDefinition) {
visitTypeDecl(typeDefinition.type);
}
@override
void visitTypeEnum(TypeEnum typeEnum) {
typeEnum.values.forEach(visitTypeEnumValue);
}
void visitTypeEnumValue(TypeEnumValue typeEnumValue) {}
@override
void visitTypeList(TypeList typeList) {
visitTypeDecl(typeList.itemType);
}
@override
void visitTypeMap(TypeMap typeMap) {
visitTypeDecl(typeMap.keyType);
visitTypeDecl(typeMap.valueType);
}
@override
void visitTypeObject(TypeObject typeObject) {
typeObject.fields.forEach(visitTypeObjectField);
}
void visitTypeObjectField(TypeObjectField typeObjectField) {
visitTypeDecl(typeObjectField.type);
}
@override
void visitTypeReference(TypeReference typeReference) {}
void visitTypes(Types types) {
types.forEach(visitTypeDefinition);
}
@override
void visitTypeUnion(TypeUnion typeUnion) {
typeUnion.choices.forEach(visitTypeDecl);
}
}
/**
* Description of a notification method.
*/
class Notification extends ApiNode {
/**
* Name of the domain enclosing this request.
*/
final String domainName;
/**
* Name of the notification, without the domain prefix.
*/
final String event;
/**
* Type of the object associated with the "params" key in the notification
* object, or null if the notification has no parameters.
*/
final TypeObject params;
Notification(this.domainName, this.event, this.params, dom.Element html,
{bool experimental})
: super(html, experimental, false);
/**
* Get the name of the notification, including the domain prefix.
*/
String get longEvent => '$domainName.$event';
/**
* Get the full type of the notification object, including the common "id"
* and "error" fields.
*/
TypeDecl get notificationType {
List<TypeObjectField> fields = [
new TypeObjectField('event', new TypeReference('String', null), null,
value: '$domainName.$event')
];
if (params != null) {
fields.add(new TypeObjectField('params', params, null));
}
return new TypeObject(fields, null);
}
}
/**
* Description of a single refactoring.
*/
class Refactoring extends ApiNode {
/**
* Name of the refactoring. This should match one of the values allowed for
* RefactoringKind.
*/
final String kind;
/**
* Type of the refactoring feedback, or null if the refactoring has no
* feedback.
*/
final TypeObject feedback;
/**
* Type of the refactoring options, or null if the refactoring has no options.
*/
final TypeObject options;
Refactoring(this.kind, this.feedback, this.options, dom.Element html,
{bool experimental})
: super(html, experimental, false);
}
/**
* A collection of refactoring definitions.
*/
class Refactorings extends ApiNode with IterableMixin<Refactoring> {
final List<Refactoring> refactorings;
Refactorings(this.refactorings, dom.Element html, {bool experimental})
: super(html, experimental, false);
@override
Iterator<Refactoring> get iterator => refactorings.iterator;
}
/**
* Description of a request method.
*/
class Request extends ApiNode {
/**
* Name of the domain enclosing this request.
*/
final String domainName;
/**
* Name of the request, without the domain prefix.
*/
final String method;
/**
* Type of the object associated with the "params" key in the request object,
* or null if the request has no parameters.
*/
final TypeObject params;
/**
* Type of the object associated with the "result" key in the response object,
* or null if the response has no results.
*/
final TypeObject result;
Request(
this.domainName, this.method, this.params, this.result, dom.Element html,
{bool experimental, bool deprecated})
: super(html, experimental, deprecated);
/**
* Get the name of the request, including the domain prefix.
*/
String get longMethod => '$domainName.$method';
/**
* Get the full type of the request object, including the common "id" and
* "method" fields.
*/
TypeDecl get requestType {
List<TypeObjectField> fields = [
new TypeObjectField('id', new TypeReference('String', null), null),
new TypeObjectField('method', new TypeReference('String', null), null,
value: '$domainName.$method')
];
if (params != null) {
fields.add(new TypeObjectField('params', params, null));
}
return new TypeObject(fields, null);
}
/**
* Get the full type of the response object, including the common "id" and
* "error" fields.
*/
TypeDecl get responseType {
List<TypeObjectField> fields = [
new TypeObjectField('id', new TypeReference('String', null), null),
new TypeObjectField(
'error', new TypeReference('RequestError', null), null,
optional: true)
];
if (result != null) {
fields.add(new TypeObjectField('result', result, null));
}
return new TypeObject(fields, null);
}
}
/**
* Base class for all possible types.
*/
abstract class TypeDecl extends ApiNode {
TypeDecl(dom.Element html, bool experimental, bool deprecated)
: super(html, experimental, deprecated);
accept(ApiVisitor visitor);
}
/**
* Description of a named type definition.
*/
class TypeDefinition extends ApiNode {
final String name;
final TypeDecl type;
TypeDefinition(this.name, this.type, dom.Element html,
{bool experimental, bool deprecated})
: super(html, experimental, deprecated);
}
/**
* Type of an enum. We represent enums in JSON as strings, so this type
* declaration simply lists the allowed values.
*/
class TypeEnum extends TypeDecl {
final List<TypeEnumValue> values;
TypeEnum(this.values, dom.Element html, {bool experimental, bool deprecated})
: super(html, experimental, deprecated);
@override
accept(ApiVisitor visitor) => visitor.visitTypeEnum(this);
}
/**
* Description of a single allowed value for an enum.
*/
class TypeEnumValue extends ApiNode {
final String value;
TypeEnumValue(this.value, dom.Element html,
{bool experimental, bool deprecated})
: super(html, experimental, deprecated);
}
/**
* Type of a JSON list.
*/
class TypeList extends TypeDecl {
final TypeDecl itemType;
TypeList(this.itemType, dom.Element html, {bool experimental})
: super(html, experimental, false);
@override
accept(ApiVisitor visitor) => visitor.visitTypeList(this);
}
/**
* Type of a JSON map.
*/
class TypeMap extends TypeDecl {
/**
* Type of map keys. Note that since JSON map keys must always be strings,
* this must either be a [TypeReference] for [String], or a [TypeReference]
* to a type which is defined in the API as an enum or a synonym for [String].
*/
final TypeReference keyType;
/**
* Type of map values.
*/
final TypeDecl valueType;
TypeMap(this.keyType, this.valueType, dom.Element html, {bool experimental})
: super(html, experimental, false);
@override
accept(ApiVisitor visitor) => visitor.visitTypeMap(this);
}
/**
* Type of a JSON object with specified fields, some of which may be optional.
*/
class TypeObject extends TypeDecl {
final List<TypeObjectField> fields;
TypeObject(this.fields, dom.Element html,
{bool experimental, bool deprecated})
: super(html, experimental, deprecated);
@override
accept(ApiVisitor visitor) => visitor.visitTypeObject(this);
/**
* Return the field with the given [name], or null if there is no such field.
*/
TypeObjectField getField(String name) {
for (TypeObjectField field in fields) {
if (field.name == name) {
return field;
}
}
return null;
}
}
/**
* Description of a single field in a [TypeObject].
*/
class TypeObjectField extends ApiNode {
final String name;
final TypeDecl type;
final bool optional;
/**
* Value that the field is required to contain, or null if it may vary.
*/
final Object value;
TypeObjectField(this.name, this.type, dom.Element html,
{this.optional: false, this.value, bool experimental, bool deprecated})
: super(html, experimental, deprecated);
}
/**
* A reference to a type which is either defined elsewhere in the API or which
* is built-in ([String], [bool], or [int]).
*/
class TypeReference extends TypeDecl {
final String typeName;
TypeReference(this.typeName, dom.Element html, {bool experimental})
: super(html, experimental, false) {
if (typeName.isEmpty) {
throw new Exception('Empty type name');
}
}
@override
accept(ApiVisitor visitor) => visitor.visitTypeReference(this);
}
/**
* A collection of type definitions.
*/
class Types extends ApiNode with IterableMixin<TypeDefinition> {
final Map<String, TypeDefinition> types;
Types(this.types, dom.Element html, {bool experimental})
: super(html, experimental, false);
@override
Iterator<TypeDefinition> get iterator => types.values.iterator;
Iterable<String> get keys => types.keys;
TypeDefinition operator [](String typeName) => types[typeName];
bool containsKey(String typeName) => types.containsKey(typeName);
}
/**
* Type which represents a union among multiple choices.
*/
class TypeUnion extends TypeDecl {
final List<TypeDecl> choices;
/**
* The field that is used to disambiguate this union
*/
final String field;
TypeUnion(this.choices, this.field, dom.Element html, {bool experimental})
: super(html, experimental, false);
@override
accept(ApiVisitor visitor) => visitor.visitTypeUnion(this);
}