// 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. /// Data structures representing an API definition, and visitor base classes /// for visiting those data structures. library; import 'dart:collection'; import 'package:analyzer_utilities/html_dom.dart' as dom; /// Toplevel container for the API. class Api extends ApiNode { final String version; final List domains; final Types types; final Refactorings refactorings; Api( this.version, this.domains, this.types, this.refactorings, dom.Element html, { bool experimental = false, }) : super(html, experimental: experimental, deprecated: 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, `null` if built-in. final dom.Element? html; ApiNode(this.html, {this.experimental = false, this.deprecated = false}); } /// Base class for visiting the API definition. abstract class ApiVisitor { /// Dispatch the given [type] to the visitor. T visitTypeDecl(TypeDecl type) => type.accept(this); 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 requests; final List notifications; Domain( this.name, this.requests, this.notifications, dom.Element html, { bool experimental = false, bool deprecated = false, }) : super(html, experimental: experimental, deprecated: deprecated); @override dom.Element get html => super.html!; } /// 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) { var newTypeRef = api.types[type.typeName]; if (newTypeRef == null) { break; } type = newTypeRef.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) { var params = notification.params; if (params != null) { visitTypeDecl(params); } } void visitRefactoring(Refactoring refactoring) { var feedback = refactoring.feedback; if (feedback != null) { visitTypeDecl(feedback); } var options = refactoring.options; if (options != null) { visitTypeDecl(options); } } void visitRefactorings(Refactorings refactorings) { refactorings.forEach(visitRefactoring); } void visitRequest(Request request) { var params = request.params; if (params != null) { visitTypeDecl(params); } var result = request.result; if (result != null) { visitTypeDecl(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 = false, }) : super(html, experimental: experimental, deprecated: 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 { var fields = [ TypeObjectField( 'event', TypeReference('String', null), null, value: '$domainName.$event', ), ]; var params = this.params; if (params != null) { fields.add(TypeObjectField('params', params, null)); } return 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 = false, }) : super(html, experimental: experimental, deprecated: false); } /// A collection of refactoring definitions. class Refactorings extends ApiNode with IterableMixin { final List refactorings; Refactorings( this.refactorings, dom.Element? html, { bool experimental = false, }) : super(html, experimental: experimental, deprecated: false); @override Iterator 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 = false, bool deprecated = false, }) : super(html, experimental: experimental, deprecated: 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 { var fields = [ TypeObjectField('id', TypeReference('String', null), null), TypeObjectField( 'method', TypeReference('String', null), null, value: '$domainName.$method', ), ]; var params = this.params; if (params != null) { fields.add(TypeObjectField('params', params, null)); } return TypeObject(fields, null); } /// Get the full type of the response object, including the common "id" and /// "error" fields. TypeDecl get responseType { var fields = [ TypeObjectField('id', TypeReference('String', null), null), TypeObjectField( 'error', TypeReference('RequestError', null), null, optional: true, ), ]; var result = this.result; if (result != null) { fields.add(TypeObjectField('result', result, null)); } return TypeObject(fields, null); } } /// Base class for all possible types. sealed class TypeDecl extends ApiNode { TypeDecl(super.html, {super.experimental, super.deprecated}); T accept(ApiVisitor visitor); } /// Description of a named type definition. class TypeDefinition extends ApiNode { final String name; final TypeDecl type; bool isExternal = false; TypeDefinition( this.name, this.type, dom.Element html, { bool experimental = false, bool deprecated = false, }) : super(html, experimental: experimental, deprecated: 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 values; TypeEnum( this.values, dom.Element html, { bool experimental = false, bool deprecated = false, }) : super(html, experimental: experimental, deprecated: deprecated); @override T 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 = false, bool deprecated = false, }) : super(html, experimental: experimental, deprecated: deprecated); @override dom.Element get html => super.html!; } /// Type of a JSON list. class TypeList extends TypeDecl { final TypeDecl itemType; TypeList(this.itemType, dom.Element html, {bool experimental = false}) : super(html, experimental: experimental, deprecated: false); @override T 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 = false, }) : super(html, experimental: experimental, deprecated: false); @override T 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 fields; TypeObject( this.fields, dom.Element? html, { bool experimental = false, bool deprecated = false, }) : super(html, experimental: experimental, deprecated: deprecated); @override T 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 (var 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 = false, bool deprecated = false, }) : super(html, experimental: experimental, deprecated: deprecated); @override String toString() => name; } /// 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 = false}) : super(html, experimental: experimental, deprecated: false) { if (typeName.isEmpty) { throw Exception('Empty type name'); } } @override T accept(ApiVisitor visitor) => visitor.visitTypeReference(this); } /// A collection of type definitions. class Types extends ApiNode with IterableMixin { final Map types; List importUris = []; Types(this.types, dom.Element? html, {bool experimental = false}) : super(html, experimental: experimental, deprecated: false); @override Iterator get iterator => types.values.iterator; Iterable 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 choices; /// The field that is used to disambiguate this union final String field; TypeUnion( this.choices, this.field, dom.Element html, { bool experimental = false, }) : super(html, experimental: experimental, deprecated: false); @override T accept(ApiVisitor visitor) => visitor.visitTypeUnion(this); }