import * as yaml from 'js-yaml'; import {Id, Layer} from '../shared'; import {RelationField, ResourceSchema} from '../system'; import {EndpointNode, EndpointNodeDef} from './node'; import { NamedInterface } from './interface'; import {Field, Model} from './model'; import {identifyObjectIdentifier, parseObjectIdentifier, ResultSet, ModelSet, FieldSet} from './util'; /** * Represents the model type: * - `model` - ordinary model, i.e. not a field and not a value * - `field` - field of a model * - `value` - special model representing a data value */ export declare type ModelType = 'model' | 'field' | 'value'; /** * The Universe class represents a VYZE modeling, including its models, description and dependencies. * * See also {@linkcode loadUniverse}, {@linkcode ServiceClient.loadUniverse}, {@linkcode loadUniverseFromFile} */ export class Universe { private readonly _id?: Id; private readonly _name: string; private readonly _description: string; private readonly _bases: string[]; private readonly _dependencies: string[]; private readonly _models: Map; private readonly _objects: Map; private readonly _endpoints: Map; private readonly _interfaces: Map; private readonly _layers: Map; private readonly _definition?: string; constructor(id: Id | undefined, name: string, description: string, bases: string[], dependencies: string[], definition?: string) { this._id = id; this._name = name; this._description = description; this._bases = bases; this._dependencies = dependencies; this._models = new Map(); this._endpoints = new Map(); this._interfaces = new Map(); this._layers = new Map(); this._objects = new Map(); this._definition = definition; } public get id(): Id | undefined { return this._id; } public get name(): string { return this._name; } public get description(): string { return this._description; } public get bases(): string[] { return this._bases; } public get dependencies(): string[] { return this._dependencies; } public get models(): ModelSet { return new ModelSet(this._models); } public get fields(): FieldSet { const fields = Array.from(this._models.entries()) .filter(m => m[1].type === 'field' && m[1].field) .map(m => [m[0], m[1].field] as [string, Field]); return new FieldSet(new Map(fields)); } public get objects(): ResultSet { return new ResultSet(this._objects); } public get endpoints(): ResultSet { return new ResultSet(this._endpoints); } public get interfaces(): ResultSet { return new ResultSet(this._interfaces); } public get layers(): ResultSet { return new ResultSet(this._layers); } public get definition(): string | undefined { return this._definition; } /** * Obtains the ID for a model inside this universe, unless it is already an ID. * @param name name of the model or an ID * @returns id of the object or `undefined` if not available */ public resolve(name: string): Id | undefined { let prefix = ''; if (name.startsWith('-')) { name = name.substring(1); prefix = '-'; } if (name.length === 32 && isHex(name)) { return prefix + name; } const model = this.getModel(name); return model ? prefix + model.object : undefined; } /** * Obtains {@linkcode Model} instance by name. * @param name name of the model * @returns {@linkcode Model} instance or `undefined` if not available */ public getModel(name: string): Model | undefined { const oi = parseObjectIdentifier(name, this.name); const oii = identifyObjectIdentifier(oi); return this.models.get(oii); } /** * Obtains {@linkcode EndpointNode} by name. * @param name name of the endpoint node * @returns {@linkcode EndpointNode} or `undefined` if not available */ public getEndpoint(name: string): EndpointNode | undefined { return this.endpoints.get(name); } /** * Obtains {@linkcode NamedInterface} by name. * @param name name of the interface * @returns {@linkcode NamedInterface} or `undefined` if not available */ public getInterface(name: string): NamedInterface | undefined { return this.interfaces.get(name); } /** * Searches for {@linkcode Model} instance by its corresponding object id. * @param id id of the model object * @returns {@linkcode Model} instance or `undefined` if not available */ public getObject(id: Id): Model | undefined { return this.objects.get(id); } /** @internal */ public addModelDefinition(def: string, object?: Id): void { const oi = parseObjectIdentifier(def, this.name); const oii = identifyObjectIdentifier(parseObjectIdentifier(def, this.name)); const model = new Model(oi.name, oi.base, oi.target); if (object) { model.object = object; this._objects.set(object, model); } this._models.set(oii, model); } /** @internal */ public addAbstractionDefinition(def: string) { const as = def.split(':'); const abstract = this.getModel(as[0]); const special = this.getModel(as[1]); if (!abstract) { throw new Error(`Abstract not found: ${as[0]}`); } if (!special) { throw new Error(`Special not found: ${as[1]}`); } abstract.specials.set(special.getIdentifier(), special); special.abstracts.set(abstract.getIdentifier(), abstract); } /** @internal */ public addRelationDefinition(def: string) { const rs = def.split(':'); const relation = this.getModel(rs[0]); const origin = this.getModel(rs[1]); const target = this.getModel(rs[2]); if (!relation) { throw new Error(`Relation not found: ${rs[0]}`); } if (!origin) { throw new Error(`Relation not found: ${rs[1]}`); } if (!target) { throw new Error(`Relation not found: ${rs[2]}`); } const field: Field = new Field(origin, relation, target); if (!relation.field) { relation.field = field; origin.targetFields.set(relation.getIdentifier(), field); target.originFields.set(relation.getIdentifier(), field); } } public addEndpointDefinition(def: EndpointNodeDef) { const entryNode = new EndpointNode(); this._endpoints.set(def.name, entryNode); if (def.context.environment.model) { const model = this.getModel(def.context.environment.model); if (model) { model.nodes.set(def.name, entryNode); } } } public addInterfaceDefinition(def: NamedInterface) { this._interfaces.set(def.name, def); } } interface InfoDef { mapping: string; description: string; type: ModelType; object?: Id; } interface UniverseDef { id?: Id; name: string; description: string; bases: string[]; dependencies: string[]; objects: string[]; info: InfoDef[]; abstractions: string[]; relations: string[]; endpoints: EndpointNodeDef[]; interfaces: NamedInterface[]; } export function loadUniverseYaml(universeYaml: string): Universe | undefined { const universeDef = yaml.load(universeYaml) as UniverseDef; return loadUniverse(universeDef, universeYaml); } function loadUniverse(universeDef: UniverseDef, universeYaml?: string): Universe | undefined { const universe = new Universe( universeDef.id, universeDef.name, universeDef.description, Array.from(universeDef.bases), Array.from(universeDef.dependencies), universeYaml); for (const ident of universeDef.objects) { universe.addModelDefinition(ident); } for (const info of universeDef.info) { const model = universe.getModel(info.mapping); if (!model) { throw new Error(`Model not found: ${info.mapping}`); } model.description = info.description; model.type = info.type; if (info.object) { model.object = info.object; universe.objects.map.set(info.object, model); } } for (const def of universeDef.abstractions) { universe.addAbstractionDefinition(def); } for (const def of universeDef.relations) { universe.addRelationDefinition(def); } if (universeDef.endpoints) { for (const def of universeDef.endpoints) { universe.addEndpointDefinition(def); } for (const def of universeDef.endpoints) { const ep = universe.endpoints.get(def.name); if (!ep) { continue; } ep.load(def, universe); } } if (universeDef.interfaces) { for (const def of universeDef.interfaces) { universe.addInterfaceDefinition(def); } } return universe; } export function resolve(universe: Universe, name: string): Id | undefined { return universe.resolve(name); } /** * Resolves model ids inside the provided schema. * @param universe modeling to resolve ids from * @param schema schema to resolve */ export function resolveSchema(universe: Universe, schema: ResourceSchema) { for (const field of schema.fields) { const relationField = field as RelationField; if (relationField.relation) { const relation = universe.getModel(relationField.relation); if (!relation) { throw new Error(`mapping not found: ${relationField.relation}`); } if (relation.type !== 'field') { throw new Error(`not a field: ${relationField.relation}`); } if (!relation.object) { throw new Error(`id not found: ${relationField.relation}`); } relationField.relation = relation.object; } } } function isHex(num: string): boolean { return Boolean(num.match(/^[0-9a-f]+$/i)) } export interface UniverseInfo { [_: string]: any; id: Id; name: string; owner: Id; root: Id; description: string; layer: Id; public: boolean; extensible: boolean; }