From 44e073a66211a61d90eaa4709d7dc14acedae6aa Mon Sep 17 00:00:00 2001 From: Stefan Blaginov Date: Fri, 20 Jan 2023 14:42:56 +0000 Subject: [PATCH] feat(tools): parse OpenAPI to Concerto Metamodel JSON --- .../concerto-tools/lib/codegen/codegen.js | 4 +- .../codegen/fromJsonSchema/cto/inferModel.js | 397 ---------- .../fromJsonSchema/cto/jsonSchemaClasses.js | 125 +++ .../fromJsonSchema/cto/jsonschemavisitor.js | 729 ++++++++++++++++++ .../codegen/fromopenapi/cto/openapivisitor.js | 617 +++++++++++++++ packages/concerto-tools/package-lock.json | 123 ++- packages/concerto-tools/package.json | 1 + .../concerto-tools/test/codegen/codegen.js | 2 +- .../cto/data/concertoJsonModel.json | 599 ++++++++++++++ .../fromJsonSchema/cto/data/concertoModel.cto | 115 +++ .../codegen/fromJsonSchema/cto/data/full.cto | 59 -- .../{schema.json => jsonSchemaModel.json} | 40 +- .../cto/{inferModel.js => inferModel.js.old} | 0 .../fromJsonSchema/cto/jsonschemavisitor.js | 117 +++ .../cto/data/openapidefinition.json | 107 +++ .../codegen/fromopenapi/cto/inferModel.js | 52 ++ 16 files changed, 2584 insertions(+), 503 deletions(-) delete mode 100644 packages/concerto-tools/lib/codegen/fromJsonSchema/cto/inferModel.js create mode 100644 packages/concerto-tools/lib/codegen/fromJsonSchema/cto/jsonSchemaClasses.js create mode 100644 packages/concerto-tools/lib/codegen/fromJsonSchema/cto/jsonschemavisitor.js create mode 100644 packages/concerto-tools/lib/codegen/fromopenapi/cto/openapivisitor.js create mode 100644 packages/concerto-tools/test/codegen/fromJsonSchema/cto/data/concertoJsonModel.json create mode 100644 packages/concerto-tools/test/codegen/fromJsonSchema/cto/data/concertoModel.cto delete mode 100644 packages/concerto-tools/test/codegen/fromJsonSchema/cto/data/full.cto rename packages/concerto-tools/test/codegen/fromJsonSchema/cto/data/{schema.json => jsonSchemaModel.json} (93%) rename packages/concerto-tools/test/codegen/fromJsonSchema/cto/{inferModel.js => inferModel.js.old} (100%) create mode 100644 packages/concerto-tools/test/codegen/fromJsonSchema/cto/jsonschemavisitor.js create mode 100644 packages/concerto-tools/test/codegen/fromopenapi/cto/data/openapidefinition.json create mode 100644 packages/concerto-tools/test/codegen/fromopenapi/cto/inferModel.js diff --git a/packages/concerto-tools/lib/codegen/codegen.js b/packages/concerto-tools/lib/codegen/codegen.js index d89699451b..359bd8a026 100644 --- a/packages/concerto-tools/lib/codegen/codegen.js +++ b/packages/concerto-tools/lib/codegen/codegen.js @@ -32,7 +32,7 @@ const OpenApiVisitor = require('./fromcto/openapi/openapivisitor'); const AvroVisitor = require('./fromcto/avro/avrovisitor'); -const InferFromJsonSchema = require('./fromJsonSchema/cto/inferModel'); +// const InferFromJsonSchema = require('./fromJsonSchema/cto/inferModel'); module.exports = { AbstractPlugin, @@ -50,7 +50,7 @@ module.exports = { ProtobufVisitor, OpenApiVisitor, AvroVisitor, - InferFromJsonSchema, + // InferFromJsonSchema, formats: { golang: GoLangVisitor, jsonschema: JSONSchemaVisitor, diff --git a/packages/concerto-tools/lib/codegen/fromJsonSchema/cto/inferModel.js b/packages/concerto-tools/lib/codegen/fromJsonSchema/cto/inferModel.js deleted file mode 100644 index 95582c4b7e..0000000000 --- a/packages/concerto-tools/lib/codegen/fromJsonSchema/cto/inferModel.js +++ /dev/null @@ -1,397 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -'use strict'; - -const Writer = require('@accordproject/concerto-util').Writer; -const Ajv2019 = require('ajv/dist/2019'); -const Ajv2020 = require('ajv/dist/2020'); -const draft6MetaSchema = require('ajv/dist/refs/json-schema-draft-06.json'); -const draft7MetaSchema = require('ajv/dist/refs/json-schema-draft-07.json'); -const addFormats = require('ajv-formats'); - -/** - * Capitalize the first letter of a string - * @param {string} string the input string - * @returns {string} input with first letter capitalized - * @private - */ -function capitalizeFirstLetter(string) { - return string.charAt(0).toUpperCase() + string.slice(1); -} - -const REGEX_ESCAPED_CHARS = /[\s\\.-]/g; - -/** - * Remove whitespace and periods from a Type identifier - * @param {string} type the input string - * @param {object} options processing options for inference - * @returns {string} the normalized type name - * @private - */ -function normalizeType(type, options) { - const typeName = type - // TODO This could cause naming collisions - // In CTO we only have one place to store definitions, so we flatten the storage structure from JSON Schema - .replace(/^#\/(definitions|\$defs|components\/schemas)\//, '') - // Replace delimiters with underscore - .replace(REGEX_ESCAPED_CHARS, '_'); - - if (options?.capitalizeFirstLetterOfTypeName){ - return capitalizeFirstLetter(typeName); - } - return typeName; -} - -/** - * Parse a $id URL to use it as a namespace and root type - * @param {string} id - the $id value from a JSON schema - * @param {object} options processing options for inference - * @returns {object} A namespace and type pair - * @private - */ -function parseIdUri(id, options) { - if (!id) { return; } - - // TODO (MCR) - support non-URL URI $id values - // https://datatracker.ietf.org/doc/html/draft-wright-json-schema-01#section-9.2 - const url = new URL(id); - let namespace = url.hostname.split('.').reverse().join('.'); - const path = url.pathname.split('/'); - const type = normalizeType(path.pop() - .replace(/\.json$/, '') // Convention is to add .schema.json to $id - .replace(/\.schema$/, ''), options); - - namespace += path.length > 0 ? path.join('.') : ''; - - return { namespace, type }; -} - -/** - * Infer a type name for a definition. Examines $id, title and parent declaration - * @param {object} definition - the input object - * @param {*} context - the processing context - * @param {boolean} [skipDictionary] - if true, this function will not use the dictionary help inference - * @returns {string} A name for the definition - * @private - */ -function inferTypeName(definition, context, skipDictionary) { - if (definition.$ref) { - return normalizeType(definition.$ref, context.options); - } - - const name = context.parents.slice(-1).pop(); - const { type } = parseIdUri(definition.$id, context.options) || { type: name }; - - if (skipDictionary || context.dictionary.has(normalizeType(type, context.options))){ - return normalizeType(type, context.options); - } - - // We've found an inline sub-schema - if (definition.properties || definition.enum){ - const subSchemaName = context.parents - .map(p => normalizeType(p, context.options)) - .join('_'); - - // Come back to this later - context.jobs.push({ name: subSchemaName, definition }); - return subSchemaName; - } - - // We fallback to a stringified object representation. This is "untyped". - return 'String'; -} - -/** - * Get the Concerto type for an JSON Schema definition - * @param {*} definition the input object - * @param {*} context the processing context - * @returns {string} the Concerto type - * @private - */ -function inferType(definition, context) { - const name = context.parents.slice(-1).pop(); - if (definition.$ref) { - // Recursive defintion - if (definition.$ref === '#') { - const top = context.parents.pop(); - const parent = context.parents.slice(-1).pop(); - context.parents.push(top); - return parent; - } - - return inferTypeName(definition, context); - } - - if (definition.enum) { - return inferTypeName(definition, context); - } - - if (definition.type) { - switch (definition.type) { - case 'string': - if (definition.format) { - if (definition.format === 'date-time' || definition.format === 'date') { - return 'DateTime'; - } else { - console.warn(`Format '${definition.format}' in '${name}' is not supported. It has been ignored.`); - return 'String'; - } - } - return 'String'; - case 'boolean': - return 'Boolean'; - case 'number': - return 'Double'; - case 'integer': - return 'Integer'; // Could also be Long? - case 'array': - return inferType(definition.items, context) + '[]'; - case 'object': - return inferTypeName(definition, context); - default: - throw new Error(`Type keyword '${definition.type}' in '${name}' is not supported`); - } - } - - // Hack until we support union types. - // https://github.com/accordproject/concerto/issues/292 - const alternative = definition.anyOf || definition.oneOf; - if (alternative){ - const keyword = definition.anyOf ? 'anyOf' : 'oneOf'; - console.warn( - `Keyword '${keyword}' in definition '${name}' is not fully supported. Defaulting to first alternative.` - ); - - // Just choose the first item - return inferType(alternative[0], context); - } - - throw new Error(`Unsupported definition: ${JSON.stringify(definition)}`); -} - -/** - * Convert JSON Schema enumeration to Concerto enum - * @param {*} definition the input object - * @param {*} context the processing context - * @private - */ -function inferEnum(definition, context) { - const { writer } = context; - - writer.writeLine(0, `enum ${inferTypeName(definition, context)} {`); - definition.enum.forEach((value) => { - let normalizedValue = value; - // Concerto does not allow enum values to start with numbers or values such as `true` - // If we can relax the parser rules, this branch could be removed - if (typeof normalizedValue !== 'string' || normalizedValue.match(/^\d/)){ - normalizedValue = `_${normalizedValue}`; - } - normalizedValue = normalizedValue.replace(REGEX_ESCAPED_CHARS, '_'); - writer.writeLine( - 1, - `o ${normalizedValue}` - ); - }); - writer.writeLine(0, '}'); - writer.writeLine(0, ''); -} - -/** - * Convert JSON Schema object definiton to Concerto concept - * @param {*} definition the input object - * @param {*} context the processing context - * @private - */ -function inferConcept(definition, context) { - const { writer } = context; - const type = inferType(definition, context); - - if (definition.additionalProperties) { - throw new Error('\'additionalProperties\' are not supported in Concerto'); - } - - const requiredFields = []; - if (definition.required) { - requiredFields.push(...definition.required); - } - - writer.writeLine(0, `concept ${type} {`); - Object.keys(definition.properties || []).forEach((field) => { - // Ignore reserved properties - if (['$identifier', '$class'].includes(field)) { - return; - } - - const optional = !requiredFields.includes(field) ? ' optional' : ''; - - const propertyDefinition = definition.properties[field]; - context.parents.push(field); - - const type = inferType(propertyDefinition, context); - - let validator = ''; - // Note: Concerto does not provide syntax for exclusive minimum or inclusive maximum - // https://json-schema.org/understanding-json-schema/reference/numeric.html#range - if (['Double', 'Long', 'Integer'].includes(type)) { - if (propertyDefinition.minimum || propertyDefinition.exclusiveMaximum) { - const min = propertyDefinition.minimum || ''; - const exclusiveMax = propertyDefinition.exclusiveMaximum || ''; - validator = ` range=[${min},${exclusiveMax}]`; - } - } else if (type === 'String' && propertyDefinition.pattern) { - validator = ` regex=/${propertyDefinition.pattern.replace(/\//g, '\\/')}/`; - } - - // Warning: The semantics of this default property differs between JSON Schema and Concerto - // JSON Schema does not fill in missing values during validation, whereas Concerto does - // https://json-schema.org/understanding-json-schema/reference/generic.html#id2 - let defaultValue = ''; - if (propertyDefinition.default) { - defaultValue = ` default=${JSON.stringify(propertyDefinition.default)}`; - } - - writer.writeLine( - 1, - `o ${type} ${field}${defaultValue}${validator}${optional}` - ); - context.parents.pop(); - }); - writer.writeLine(0, '}'); - writer.writeLine(0, ''); -} - -/** - * Infers a Concerto model from a JSON Schema. - * @param {*} definition the input object - * @param {*} context the processing context - * @private - */ -function inferDeclaration(definition, context) { - const name = context.parents.slice(-1).pop(); - - if (definition.enum) { - inferEnum(definition, context); - } else if (definition.type) { - if (definition.type === 'object') { - inferConcept(definition, context); - } else if (definition.type === 'array') { - console.warn( - `Type keyword 'array' in definition '${name}' is not supported. It has been ignored.` - ); - } else { - throw new Error( - `Type keyword '${definition.type}' in definition '${name}' is not supported.` - ); - } - } else { - // Find all keys that are not supported - const badKeys = Object.keys(definition).filter(key => - !['enum', 'type'].includes(key) && - !key.startsWith('x-') // Ignore custom extensions - ); - console.warn( - `Keyword(s) '${badKeys.join('\', \'')}' in definition '${name}' are not supported.` - ); - } -} - -/** - * Infers a Concerto model from a JSON Schema. - * @param {string} defaultNamespace a fallback namespace to use for the model if it can't be infered - * @param {string} defaultType a fallback name for the root concept if it can't be infered - * @param {object} schema the input json object - * @param {object} options processing options for inference - * @returns {string} the Concerto model - */ -function inferModelFile(defaultNamespace, defaultType, schema, options = {}) { - const schemaVersion = schema.$schema; - - let ajv = new Ajv2019({ strict: false }) - .addMetaSchema(draft6MetaSchema) - .addMetaSchema(draft7MetaSchema); - - if (schemaVersion && schemaVersion.startsWith('https://json-schema.org/draft/2020-12/schema')) { - ajv = new Ajv2020({ strict: false }); - } - - if (schemaVersion && schemaVersion.startsWith('https://json-schema.org/draft/2020-12/schema')) { - ajv = new Ajv2020({ strict: false }); - } - - const { namespace, type } = parseIdUri(schema.$id) || - { namespace: defaultNamespace, type: defaultType }; - - ajv.addSchema(schema, type); - addFormats(ajv); - - // Will throw an error for bad schemas - ajv.compile(schema); - - const context = { - parents: new Array(), // Track ancestors in the tree - writer: new Writer(), - dictionary: new Set(), // Track types that we've seen before - jobs: new Array(), // Queue of inline definitions to come-back to - options, - }; - - context.writer.writeLine(0, `namespace ${namespace}`); - context.writer.writeLine(0, ''); - - // Create definitions - const defs = schema.definitions || schema.$defs || schema?.components?.schemas ||[]; - - // Build a dictionary - context.dictionary.add(defaultType); - if (schema.$id) { - context.dictionary.add(normalizeType(parseIdUri(schema.$id).type, options)); - } - Object.keys(defs).forEach((key) => { - context.parents.push(key); - const definition = defs[key]; - const typeName = inferTypeName(definition, context, true); - if (context.dictionary.has(typeName)){ - throw new Error(`Duplicate definition found for type '${typeName}'.`); - } - context.dictionary.add(typeName); - context.parents.pop(); - }); - - // Visit each declaration - Object.keys(defs).forEach((key) => { - context.parents.push(key); - const definition = defs[key]; - inferDeclaration(definition, context); - context.parents.pop(); - }); - - // Create root type - context.parents.push(type); - inferDeclaration(schema, context); - context.parents.pop(); - - // Generate declarations for all inline sub-schemas - while(context.jobs.length > 0){ - const job = context.jobs.pop(); - context.parents.push(job.name); - context.dictionary.add(job.name); - inferDeclaration(job.definition, context); - context.parents.pop(); - } - - return context.writer.getBuffer(); -} - -module.exports = inferModelFile; diff --git a/packages/concerto-tools/lib/codegen/fromJsonSchema/cto/jsonSchemaClasses.js b/packages/concerto-tools/lib/codegen/fromJsonSchema/cto/jsonSchemaClasses.js new file mode 100644 index 0000000000..dd3bfc7302 --- /dev/null +++ b/packages/concerto-tools/lib/codegen/fromJsonSchema/cto/jsonSchemaClasses.js @@ -0,0 +1,125 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +'use strict'; + +/** + * A visitable class to be used by the JsonSchemaVisitor. + * + * @private + * @class + */ +class Visitable { + /** + * @param {Object} body - the body. + * @param {String[]} path - the location inside the JSON Schema model. + * + */ + constructor(body, path) { + this.body = body; + this.path = path; + } + /** + * @param {Object} visitor - the JSON Schema mode visitor. + * @param {Object} parameters - the visitor parameters. + * + * @return {Object} the result of visiting or undefined. + */ + accept(visitor, parameters) { + return visitor.visit(this, parameters); + } +} + +/** + * A local reference visitable class. + * + * @class + */ +class LocalReference extends Visitable { isLocalReference = true; } +/** + * A reference visitable class. + * + * @class + */ +class Reference extends Visitable { isReference = true; } +/** + * An array property visitable class. + * + * @class + */ +class ArrayProperty extends Visitable { isArrayProperty = true; } +/** + * A property visitable class. + * + * @class + */ +class Property extends Visitable { isProperty = true; } +/** + * A properties visitable class. + * + * @class + */ +class Properties extends Visitable { isProperties = true; } +/** + * A non-enum definition visitable class. + * + * @class + */ +class NonEnumDefinition extends Visitable { isNonEnumDefinition = true; } +/** + * An enum definition visitable class. + * + * @class + */ +class EnumDefinition extends Visitable { isEnumDefinition = true; } +/** + * An definition visitable class. + * + * @class + */ +class Definition extends Visitable { isDefinition = true; } +/** + * A definitions visitable class. + * + * @class + */ +class Definitions extends Visitable { isDefinitions = true; } +/** + * A JsonSchemaModel visitable class. + * + * @class + */ +class JsonSchemaModel extends Visitable { + /** + * @param {Object} body - the body. + * + */ + constructor(body) { + super(body, []); + } + + isJsonSchemaModel = true; +} + +module.exports = { + LocalReference, + Reference, + ArrayProperty, + Property, + Properties, + NonEnumDefinition, + EnumDefinition, + Definition, + Definitions, + JsonSchemaModel, +}; diff --git a/packages/concerto-tools/lib/codegen/fromJsonSchema/cto/jsonschemavisitor.js b/packages/concerto-tools/lib/codegen/fromJsonSchema/cto/jsonschemavisitor.js new file mode 100644 index 0000000000..376b8a157d --- /dev/null +++ b/packages/concerto-tools/lib/codegen/fromJsonSchema/cto/jsonschemavisitor.js @@ -0,0 +1,729 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +const { + LocalReference, + Reference, + ArrayProperty, + Property, + Properties, + NonEnumDefinition, + EnumDefinition, + Definition, + Definitions, +} = require('./jsonSchemaClasses.js'); + +/** + * Convert the contents of a JSON Schema file to a Concerto JSON model. + * Set the following parameters to use: + * - metaModelNamespace: the current metamodel namespace. + * - namespace: the desired namespace of the generated model. + * + * @private + * @class + */ +class JsonSchemaVisitor { + /** + * Returns true if the property maps to the Concerto DateTime type. + * @param {Object} property - a JSON Schema model property. + * + * @return {Boolean} "true" if the property maps to the Concerto DateTime + * type. + * @private + */ + isPropertyDateTime(property) { + return property.body.format && + ( + property.body.format === 'date-time' || + property.body.format === 'date' + ); + } + /** + * Returns true if the property contains an "anyOf" or a "oneOf" element. + * @param {Object} property - a JSON Schema model property. + * + * @return {Boolean} "true" if the property contains aan "anyOf" or a + * "oneOf" element. + * @private + */ + doesPropertyContainAnyOfOrOneOf(property) { + return !!(property.body.anyOf || property.body.oneOf); + } + /** + * Flatten a property containing an "anyOf" or a "oneOf" element. + * @param {Object} property - a JSON Schema model property. + * + * @return {Object} a JSON Schema model property with the "anyOf" or + * "oneOf" elements resolved. + * @private + */ + flattenAnyOfOrOneOfInProperty(property) { + // eslint-disable-next-line no-console + console.warn( + `Keyword '${ + property.body.anyOf ? 'anyOf' : 'oneOf' + }' in definition '${ + property.path[property.path.length - 1] + }' is not fully supported. Defaulting to first alternative.` + ); + + return { + ...Object.fromEntries( + Object.entries(property.body) + .filter( + property => ![ + 'anyOf', 'oneOf' + ].includes(property[0]) + ) + ), + ...( + property.body.anyOf || + property.body.oneOf + )[0] + }; + } + /** + * Returns true if the property contains a JSON Schema model reference. + * @param {Object} property - a JSON Schema model property. + * + * @return {Boolean} "true" if the property contains a JSON Schema model + * reference. + * @private + */ + doesPropertyContainAReference(property) { + return typeof property.body.$ref === 'string'; + } + /** + * Returns true if the string is a JSON Schema model local + * reference one. + * @param {String} potentialReferenceString - a JSON Schema model local + * reference string. + * + * @return {Boolean} "true" if the string is a JSON Schema model local + * reference one. + * @private + */ + isStringLocalReference(potentialReferenceString) { + return potentialReferenceString.charAt(0) === '#'; + } + /** + * Parses a local reference string. + * @param {Object} referenceString - a JSON Schema model local reference + * string. + * + * @return {String[]} the path to the reffered object. + * @private + */ + parseLocalReferenceString(referenceString) { + return referenceString + .slice(1, referenceString.length) + .split('/') + .filter(pathSegment => pathSegment.length > 0); + } + /** + * Infers a primitive Concerto type from a JSON Schema model property. + * @param {Object} property - a JSON Schema model property. + * @param {Object} parameters - the visitor parameters. + * + * @return {Object} the primitive Concerto type inferred from the JSON + * Schema model property. + * @private + */ + inferPrimitiveConcertoType(property, parameters) { + if (property.body.type) { + switch (property.body.type) { + case 'string': + if ( + this.isPropertyDateTime(property) + ) { + return `${parameters.metaModelNamespace}.DateTimeProperty`; + } + + if (property.body.format) { + // eslint-disable-next-line no-console + console.warn( + `Format '${property.body.format}' in '${ + property.path[property.path.length - 1] + }' is not supported. It has been ignored.` + ); + } + + return `${parameters.metaModelNamespace}.StringProperty`; + case 'boolean': + return `${parameters.metaModelNamespace}.BooleanProperty`; + case 'number': + return `${parameters.metaModelNamespace}.DoubleProperty`; + case 'integer': + // Could also be Long? + return `${parameters.metaModelNamespace}.IntegerProperty`; + default: + throw new Error( + `Type keyword '${property.body.type}' in '${ + property.path[property.path.length - 1] + }' is not supported.` + ); + } + } + } + /** + * Infers a Concerto concept name from a JSON Schema model inline property + * path. + * @param {Object} propertyPath - a JSON Schema model property path. + * @param {Object} options - the options: + * - removePropertiesSegment: removes any occurances of "properties" from + * the generated name. + * + * @return {Object} the Concerto concept name inferred from the JSON Schema + * model inline object property path. + * @private + */ + inferInlineObjectConceptName(propertyPath, options) { + return `${ + propertyPath + // Note: We're running the risk of removing objects legitimately named "properties". + .filter( + (segment) => options.removePropertiesSegment && + segment !== 'properties' + ) + .join('$_') + }`; + } + /** + * Infers a type-specific validator, appropriate to a Concerto primitive. + * @param {Object} property - a JSON Schema model property. + * @param {Object} metaModelNamespace - the Concerto meta model namespace. + * + * @return {Object} the Concerto field validator inferred from the JSON + * Schema model property. + * @private + */ + inferTypeSpecificValidator(property, metaModelNamespace) { + if ( + ( + property.body.type === 'integer' || + property.body.type === 'number' + ) && + (property.body.minimum || property.body.exclusiveMaximum) + ) { + return { + validator: { + $class: `${metaModelNamespace}.${ + property.body.type === 'number' + ? 'DoubleDomainValidator' + : 'IntegerDomainValidator' + }`, + // Note: Concerto does not provide syntax for exclusive minimum or inclusive maximum + // https://json-schema.org/understanding-json-schema/reference/numeric.html#range + ...( + property.body.minimum + ? { lower: property.body.minimum } + : {} + ), + ...( + property.body.exclusiveMaximum + ? { upper: property.body.exclusiveMaximum } + : {} + ), + } + }; + } + + if ( + property.body.type === 'string' && + !this.isPropertyDateTime(property) && + property.body.pattern + ) { + return { + validator: { + $class: `${metaModelNamespace}.StringRegexValidator`, + pattern: property.body.pattern, + flags: 'u', + } + }; + } + + return {}; + } + /** + * Infers a type-specific property, mapping to a Concerto primitive. + * @param {Object} property - a JSON Schema model property. + * @param {Object} metaModelNamespace - the Concerto meta model namespace. + * + * @return {Object} the Concerto field inferred from the JSON Schema model + * property. + * @private + */ + inferTypeSpecificProperties(property, metaModelNamespace) { + if ( + ['boolean', 'integer', 'number', 'string'] + .includes(property.body.type) + ) { + return { + // Warning: The semantics of this default property differs between JSON Schema and Concerto + // JSON Schema does not fill in missing values during validation, whereas Concerto does + // https://json-schema.org/understanding-json-schema/reference/generic.html#id2 + ...( + property.body.default + ? { defaultValue: property.body.default } + : {} + ), + ...this.inferTypeSpecificValidator(property, metaModelNamespace) + }; + } + } + /** + * Local reference property visitor. + * @param {Object} reference - a JSON Schema model local reference property. + * @param {Object} parameters - the visitor parameters. + * + * @return {Object} the Concerto fields and declarations inferred from + * the JSON Schema model local reference property. A property can spawn + * declarations as well, if it contains inline objects. + * @private + */ + visitLocalReference(reference, parameters) { + const pathToDefinition = this.parseLocalReferenceString(reference.body); + + if (pathToDefinition[0] === 'definitions') { + const definitionName = pathToDefinition[ + pathToDefinition.length - 1 + ]; + + return ( + new Definition( + parameters + .jsonSchemaModel + .definitions[definitionName], + pathToDefinition, + ) + ).accept(this, parameters); + } + } + /** + * Reference property visitor. + * @param {Object} reference - a JSON Schema model reference property. + * @param {Object} parameters - the visitor parameters. + * + * @return {Object} the Concerto fields and declarations inferred from + * the JSON Schema model reference property. A property can spawn + * declarations as well, if it contains inline objects. + * @private + */ + visitReference(reference, parameters) { + if (this.isStringLocalReference(reference.body)) { + return (new LocalReference(reference.body, reference.path)) + .accept(this, parameters); + } + // TODO: Handle remote reference. + // TODO: Handle URL reference. + } + /** + * Array property visitor. + * @param {Object} arrayProperty - a JSON Schema model array property. + * @param {Object} parameters - the visitor parameters. + * + * @return {Object} the Concerto fields and declarations inferred from + * the JSON Schema model array property. A property can spawn declarations + * as well, if it contains inline objects. + * @private + */ + visitArrayProperty(arrayProperty, parameters) { + parameters.assignableFields = { + ...parameters.assignableFields, + isArray: true, + }; + + return (new Property(arrayProperty.body.items, arrayProperty.path)) + .accept(this, parameters); + } + /** + * Property visitor. + * @param {Object} property - a JSON Schema model property. + * @param {Object} parameters - the visitor parameters. + * + * @return {Object} the Concerto fields and declarations inferred from + * the JSON Schema model property. A property can spawn declarations as + * well, if it contains inline objects. + * @private + */ + visitProperty(property, parameters) { + const propertyName = property.path[ + property.path.length - 1 + ]; + + const propertyProperties = { + ...{ + name: propertyName, + isArray: false, + isOptional: !parameters.required + ?.includes(propertyName), + }, + ...parameters.assignableFields + }; + delete parameters.assignableFields; + + // Handle reserved properties. + if (['$identifier', '$class'].includes(propertyName)) { + return; + } + + // Handle an array. + if ( + property.body.type === 'array' && + typeof property.body.items === 'object' + ) { + return (new ArrayProperty(property.body, property.path)) + .accept(this, parameters); + } + + // Handle anyOf or oneOf. + if (this.doesPropertyContainAnyOfOrOneOf(property)) { + return ( + new Property( + this.flattenAnyOfOrOneOfInProperty( + property + ), + property.path + ) + ).accept(this, parameters); + } + + // Handle a reference. + if (this.doesPropertyContainAReference(property)) { + const referenced = ( + new Reference(property.body.$ref, property.path) + ).accept(this, parameters); + + return { + $class: `${parameters.metaModelNamespace}.ObjectProperty`, + ...propertyProperties, + type: { + $class: `${parameters.metaModelNamespace}.TypeIdentifier`, + name: referenced.name + } + }; + } + + // Handle an undefined type. + if ( + property.body.type === 'object' && + // typeof property.body.properties === 'undefined' && + ( + typeof property.body.additionalProperties === 'object' || + typeof property.body.properties !== 'object' + ) + ) { + return { + $class: `${parameters.metaModelNamespace}.StringProperty`, + ...propertyProperties, + decorators: [ + { + $class: 'concerto.metamodel@1.0.0.Decorator', + name: 'StringifiedJson', + } + ] + }; + } + + // Handle an inline object. + if ( + property.body.type === 'object' && + typeof property.body.properties === 'object' + ) { + const inlineObjectDerivedConceptName = this.inferInlineObjectConceptName( + property.path, + { + removePropertiesSegment: true, + } + ); + + const inlineObjectDerivedConcept = ( + new Definition( + property.body, + [inlineObjectDerivedConceptName], + ) + ).accept(this, parameters); + + return [ + { + $class: `${parameters.metaModelNamespace}.ObjectProperty`, + ...propertyProperties, + type: { + $class: 'concerto.metamodel@1.0.0.TypeIdentifier', + name: inlineObjectDerivedConceptName, + } + }, + inlineObjectDerivedConcept + ]; + } + + // Handle primitive types. + return { + $class: this.inferPrimitiveConcertoType( + property, parameters + ), + ...propertyProperties, + ...this.inferTypeSpecificProperties( + property, parameters.metaModelNamespace + ) + }; + } + /** + * Property visitor. + * @param {Object} properties - the JSON Schema model properties. + * @param {Object} parameters - the visitor parameters. + * + * @return {Object} the Concerto fields and declarations inferred from + * the JSON Schema model properties. A property can spawn declarations as + * well, if it contains inline objects. + * @private + */ + visitProperties(properties, parameters) { + const propertyClasses = Object.entries(properties.body) + .map( + ([propertyName, propertyBody]) => new Property( + propertyBody, + [...properties.path, propertyName] + ) + ); + + + return propertyClasses + .map( + propertyClass => propertyClass.accept(this, parameters) + ) + .flat(Infinity) + .filter(field => field); + } + /** + * Non-enum definition visitor. + * @param {Object} nonEnumDefinition - a JSON Schema model non-enum + * definition. + * @param {Object} parameters - the visitor parameters. + * + * @return {Object} the Concerto declaration or declarations inferred from + * the JSON Schema model non-enum definition. A definition can spawn more + * than one Concerto declarations if it contains inline objects. + * @private + */ + visitNonEnumDefinition(nonEnumDefinition, parameters) { + const propertiesAndInlineObjectDerived = ( + new Properties( + nonEnumDefinition.body.properties, + [...nonEnumDefinition.path, 'properties'] + ) + ).accept(this, { + ...parameters, + required: nonEnumDefinition.body.required, + }); + + const inlineObjectDerived = [ + `${parameters.metaModelNamespace}.ConceptDeclaration`, + `${parameters.metaModelNamespace}.EnumDeclaration`, + ]; + + const properties = propertiesAndInlineObjectDerived + .filter( + propertyOrInlineDerivedConcept => + !inlineObjectDerived + .includes(propertyOrInlineDerivedConcept.$class) + ); + + const inlineObjectConcepts = propertiesAndInlineObjectDerived + .filter( + propertyOrInlineDerivedConcept => + inlineObjectDerived + .includes(propertyOrInlineDerivedConcept.$class) + ); + + const conceptDeclaration = { + $class: `${parameters.metaModelNamespace}.ConceptDeclaration`, + name: nonEnumDefinition.path[ + nonEnumDefinition.path.length - 1 + ], + isAbstract: false, + properties + }; + + if (inlineObjectConcepts.length > 0) { + return [conceptDeclaration, ...inlineObjectConcepts]; + } + + return conceptDeclaration; + } + /** + * Enum definition visitor. + * @param {Object} enumDefinition - a JSON Schema model enum definition. + * @param {Object} parameters - the visitor parameters. + * + * @return {Object} the Concerto enum declaration inferred from + * the JSON Schema model enum definition. + * @private + */ + visitEnumDefinition(enumDefinition, parameters) { + const properties = enumDefinition.body.enum.map( + enumName => ({ + $class: `${parameters.metaModelNamespace}.EnumProperty`, + name: enumName, + }) + ); + + const enumDeclaration = { + $class: `${parameters.metaModelNamespace}.EnumDeclaration`, + name: enumDefinition.path[enumDefinition.path.length - 1], + properties, + }; + + return enumDeclaration; + } + /** + * Definition visitor. + * @param {Object} definition - a JSON Schema model definition. + * @param {Object} parameters - the visitor parameters. + * + * @return {Object} the Concerto declaration or declarations inferred from + * the JSON Schema model definition. A definition can spawn more than one + * Concerto declarations if it contains inline objects. + * @private + */ + visitDefinition(definition, parameters) { + if (typeof definition.body.enum === 'object') { + return ( + new EnumDefinition(definition.body, definition.path) + ).accept(this, parameters); + } else { + return ( + new NonEnumDefinition(definition.body, definition.path) + ).accept(this, parameters); + } + } + /** + * Definitions visitor. + * @param {Object} definitions - the JSON Schema model definitions. + * @param {Object} parameters - the visitor parameters. + * + * @return {Object} the Concerto declarations inferred from the JSON Schema + * model definitions. + * @private + */ + visitDefinitions(definitions, parameters) { + const definitionClasses = Object.entries(definitions.body) + .map( + ([definitionName, definitionBody]) => new Definition( + definitionBody, + [...definitions.path, definitionName] + ) + ); + + return definitionClasses.map( + definitionClass => definitionClass.accept(this, parameters) + ).flat(); + } + /** + * JSON Schema model visitor. + * @param {Object} jsonSchemaModel - the JSON Schema model. + * @param {Object} parameters - the visitor parameters. + * + * @return {Object} the Concerto JSON model. + * @private + */ + visitJsonSchemaModel(jsonSchemaModel, parameters) { + const rootDefinitionClass = new Definition( + jsonSchemaModel.body, + [`${ jsonSchemaModel.title || 'Root' }`] + ); + + const definitionsClass = new Definitions( + jsonSchemaModel.body.definitions, + ['definitions'] + ); + + const parametersWithJsonSchemaModel = { + ...parameters, jsonSchemaModel: jsonSchemaModel.body + }; + + const declarations = [ + ...rootDefinitionClass.accept( + this, parametersWithJsonSchemaModel + ), + ...definitionsClass.accept( + this, parametersWithJsonSchemaModel + ), + ]; + + const convertedModel = { + $class: `${parameters.metaModelNamespace}.Model`, + decorators: [], + namespace: parameters.namespace, + imports: [], // TODO: Review if anything needs to be imptorted. + declarations, + }; + + const concertoJsonModel = { + $class: `${parameters.metaModelNamespace}.Models`, + models: [ + convertedModel + ], + }; + + return concertoJsonModel; + } + /** + * Visitor dispatch i.e. main entry point to this visitor. + * @param {Object} thing - the visited entity. + * @param {Object} parameters - the visitor parameters. + * Set the following parameters to use: + * - metaModelNamespace: the current metamodel namespace. + * - namespace: the desired namespace of the generated model. + * + * @return {Object} the result of visiting or undefined. + * @public + */ + visit(thing, parameters) { + if (thing.isLocalReference) { + return this.visitLocalReference(thing, parameters); + } + if (thing.isReference) { + return this.visitReference(thing, parameters); + } + if (thing.isArrayProperty) { + return this.visitArrayProperty(thing, parameters); + } + if (thing.isProperty) { + return this.visitProperty(thing, parameters); + } + if (thing.isProperties) { + return this.visitProperties(thing, parameters); + } + if (thing.isNonEnumDefinition) { + return this.visitNonEnumDefinition(thing, parameters); + } + if (thing.isEnumDefinition) { + return this.visitEnumDefinition(thing, parameters); + } + if (thing.isDefinition) { + return this.visitDefinition(thing, parameters); + } + if (thing.isDefinitions) { + return this.visitDefinitions(thing, parameters); + } + if (thing.isJsonSchemaModel) { + return this.visitJsonSchemaModel(thing, parameters); + } + } +} + +module.exports = JsonSchemaVisitor; diff --git a/packages/concerto-tools/lib/codegen/fromopenapi/cto/openapivisitor.js b/packages/concerto-tools/lib/codegen/fromopenapi/cto/openapivisitor.js new file mode 100644 index 0000000000..e074d92e86 --- /dev/null +++ b/packages/concerto-tools/lib/codegen/fromopenapi/cto/openapivisitor.js @@ -0,0 +1,617 @@ +/* eslint-disable no-unused-vars */ +/* eslint-disable require-jsdoc */ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +// const ResourceValidator = require("../../../../../concerto-core/lib/serializer/resourcevalidator"); + +function capitalizeFirstLetter(string) { + return string.charAt(0).toUpperCase() + string.slice(1); +} + +function isOaiPropertyDateTime(oaiPropertyBody) { + return oaiPropertyBody.format && + ( + oaiPropertyBody.format === 'date-time' || + oaiPropertyBody.format === 'date' + ); +} + +function oaiTypeToConertoBasic$class([oaiPropertyName, oaiPropertyBody]) { + if (oaiPropertyBody.type) { + switch (oaiPropertyBody.type) { + case 'string': + if ( + isOaiPropertyDateTime(oaiPropertyBody) + ) { + return `${parameters.metaModelNamespace}.DateTimeProperty`; + } + + if (oaiPropertyBody.format) { + console.warn( + `Format '${oaiPropertyBody.format}' in '${oaiPropertyName}' is not supported. It has been ignored.` + ); + } + + return `${parameters.metaModelNamespace}.StringProperty`; + case 'boolean': + return `${parameters.metaModelNamespace}.BooleanProperty`; + case 'number': + return `${parameters.metaModelNamespace}.DoubleProperty`; + case 'integer': + return `${parameters.metaModelNamespace}.IntegerProperty`; // Could also be Long? + default: + throw new Error( + `Type keyword '${oaiPropertyBody.type}' in '${oaiPropertyName}' is not supported` + ); + } + } +} + +function isOaiPropertyArray(oaiPropertyBody) { + return oaiPropertyBody.type === 'array'; +} + +function isOaiPropertyObject(oaiPropertyBody) { + return oaiPropertyBody.type === 'object'; +} + +// function isOaiPropertyReference() { + +// } + +function flattenOaiAnyOfOrOneOf([oaiPropertyName, oaiPropertyBody]) { + const alternative = oaiPropertyBody.anyOf || + oaiPropertyBody.oneOf; + + if (alternative) { + const keyword = oaiPropertyBody.anyOf ? 'anyOf' : 'oneOf'; + console.warn( + `Keyword '${keyword}' in definition '${oaiPropertyName}' is not fully supported. Defaulting to first alternative.` + ); + + delete oaiPropertyBody.anyOf; + delete oaiPropertyBody.oneOf; + + return [ + oaiPropertyName, { + ...oaiPropertyBody, + ...alternative + } + ]; + } + + return [oaiPropertyName, oaiPropertyBody]; +} + +function isOaiPropertyUnmodelledJson(oaiPropertyBody) { + return oaiPropertyBody.type === 'object' && + typeof oaiPropertyBody.additionalProperties === 'object'; +} + +function oaiTypeToConerto$class([oaiPropertyName, oaiPropertyBody]) { + if (isOaiPropertyArray(oaiPropertyBody)) { + return oaiTypeToConertoBasic$class([ + oaiPropertyName, + oaiPropertyBody.items + ]); + } + + if (isOaiPropertyObject(oaiPropertyBody)) { + return; + } + + return oaiTypeToConertoBasic$class([oaiPropertyName, oaiPropertyBody]); +} + +function oaiToConcertoProperty([oaiPropertyName, oaiPropertyBody]) { + // const oaiPropertyWithout + return { + $class: oaiTypeToConerto$class([oaiPropertyName, oaiPropertyBody]), + isArray: isOaiPropertyArray(oaiPropertyBody), + isOptional: false // TODO: + }; +} + +function oaiToConcertoProperties(oaiProperties) { + return Object.entries(oaiProperties) + .map(flattenOaiAnyOfOrOneOf) + // @ts-ignore + .map(oaiToConcertoProperty); +} + +function oaiObjectToDeclaration([oaiObjectK, oaiObjectV]) { + const properties = oaiToConcertoProperties(oaiObjectV.properties); + return { + $class: `${parameters.metaModelNamespace}.ConceptDeclaration`, + name: oaiObjectK, + isAbstract: false, + properties + }; +} + + +class Visitable { + accept(visitor, parameters) { + return visitor.visit(this, parameters); + } +} + +class BodyVisitable extends Visitable { + constructor(body, path) { + super(); + this.body = body; + this.path = path; + } +} + +class NameAndBodyVisitable extends Visitable { + constructor(name, body, path) { + super(); + this.name = name; + this.body = body; + this.path = path; + } +} + +class OaiLocalReference extends BodyVisitable { + isOaiLocalReference = true; +} + +class OaiReference extends BodyVisitable { + isOaiReference = true; +} + +class OaiProperty extends NameAndBodyVisitable { + isOaiProperty = true; +} + +class OaiProperties extends BodyVisitable { + isOaiProperties = true; +} + +class OaiSchema extends NameAndBodyVisitable { + isOaiSchema = true; +} + +class OaiSchemas extends BodyVisitable { + isOaiSchemas = true; +} + +class OpenApiVisitor { + // isOaiSchemas(thing) { + // // TODO: Expand criteria + // return thing[0] === 'schemas' && + // typeof thing[1].components === 'object'; + // } + // isOaiComponents(thing) { + // // TODO: Expand criteria + // return thing[0] === 'components' && + // typeof thing[1].components === 'object'; + // } + isOaiFile(thing) { + // TODO: Expand criteria + return typeof thing.components === 'object'; + } + doesPropertyContainAnyOfOrOneOf({ body: oaiPropertyBody }) { + return !!(oaiPropertyBody.anyOf || oaiPropertyBody.oneOf); + } + doesPropertyContain$ref({ body: oaiPropertyBody }) { + return !!oaiPropertyBody.$ref; + } + flattenAnyOfOrOneOfInProperty({ + name: oaiPropertyName, + body: oaiPropertyBody, + path: oaiPropertyPath, + }) { + console.warn( + `Keyword '${ + oaiPropertyBody.anyOf ? 'anyOf' : 'oneOf' + }' in definition '${oaiPropertyName}' is not fully supported. Defaulting to first alternative.` + ); + + return new OaiProperty( + oaiPropertyName, + { + ...Object.fromEntries( + Object.entries(oaiPropertyBody) + .filter( + property => ![ + 'anyOf', 'oneOf' + ].includes(property[0]) + ) + ), + ...( + oaiPropertyBody.anyOf || + oaiPropertyBody.oneOf + )[0] + }, + oaiPropertyPath, + ); + } + doesPropertyContainAReference({ body: oaiPropertyBody }) { + return typeof oaiPropertyBody.$ref === 'string'; + } + isStringLocalReference(oaiReference) { + return oaiReference.charAt(0) === '#'; + } + parseLocalReferenceString(oaiReference) { + return oaiReference + .slice(1, oaiReference.length) + .split('/') + .filter(str => str.length > 0); + } + inferPrimitiveConcertoType( + { + name: oaiPropertyName, body: oaiPropertyBody + }, + parameters + ) { + if (oaiPropertyBody.type) { + switch (oaiPropertyBody.type) { + case 'string': + if ( + isOaiPropertyDateTime(oaiPropertyBody) + ) { + return `${parameters.metaModelNamespace}.DateTimeProperty`; + } + + if (oaiPropertyBody.format) { + console.warn( + `Format '${oaiPropertyBody.format}' in '${oaiPropertyName}' is not supported. It has been ignored.` + ); + } + + return `${parameters.metaModelNamespace}.StringProperty`; + case 'boolean': + return `${parameters.metaModelNamespace}.BooleanProperty`; + case 'number': + return `${parameters.metaModelNamespace}.DoubleProperty`; + case 'integer': + return `${parameters.metaModelNamespace}.IntegerProperty`; // Could also be Long? + default: + throw new Error( + `Type keyword '${oaiPropertyBody.type}' in '${oaiPropertyName}' is not supported` + ); + } + } + } + normalizeTypeName(type, options) { + const REGEX_ESCAPED_CHARS = /[\s\\.-]/g; + const typeName = type + // TODO: This could cause naming collisions + // In CTO we only have one place to store definitions, so we flatten the storage structure from JSON Schema + .replace(/^#\/(definitions|\$defs|components\/schemas)\//, '') + // Replace delimiters with underscore + .replace(REGEX_ESCAPED_CHARS, '_'); + + if (options?.capitalizeFirstLetterOfTypeName){ + return capitalizeFirstLetter(typeName); + } + + return typeName; + } + parseIdUri(id, options) { + if (!id) { return; } + + // TODO (MCR) - support non-URL URI $id values + // https://datatracker.ietf.org/doc/html/draft-wright-json-schema-01#section-9.2 + const url = new URL(id); + let namespace = url.hostname.split('.').reverse().join('.'); + const path = url.pathname.split('/'); + const type = this.normalizeTypeName(path.pop() + .replace(/\.json$/, '') // Convention is to add .schema.json to $id + .replace(/\.schema$/, ''), options); + + namespace += path.length > 0 ? path.join('.') : ''; + + return { namespace, type }; + } + inferTypeName(definition, context, skipDictionary) { + if (definition.$ref) { + return normalizeType(definition.$ref, context.options); + } + + const name = context.parents.slice(-1).pop(); + const { type } = parseIdUri(definition.$id, context.options) || { type: name }; + + if (skipDictionary || context.dictionary.has(normalizeType(type, context.options))){ + return normalizeType(type, context.options); + } + + // We've found an inline sub-schema + if (definition.properties || definition.enum){ + const subSchemaName = context.parents + .map(p => normalizeType(p, context.options)) + .join('_'); + + // Come back to this later + context.jobs.push({ name: subSchemaName, definition }); + return subSchemaName; + } + + // The object would contain a map or dictionary. + // Currently we're treating those as stringified JSON. + if (definition.additionalProperties) { + return 'String'; + } + + // We fallback to a stringified object representation. This is "untyped". + return 'String'; + } + ccc(oaiPropertyPath, options) { + if (options.fullOaiObjectPathName) { + return `${oaiPropertyPath.join('_')}`; + } + + return `${ + oaiPropertyPath + .filter( + (segment, i) => !( + (segment === 'components' && i === 0) || + (segment === 'schemas' && i === 1) + ) + ) + .filter( + (segment, i) => !( + segment === 'properties' && i % 2 === 1 + ) + ) + .join('_') + }`; + } + visitOaiLocalReference({ body: oaiReference, path: oaiPath }, parameters) { + const pathToOaiObject = this.parseLocalReferenceString(oaiReference); + + if ( + pathToOaiObject[0] === 'components' && + pathToOaiObject[1] === 'schemas' + ) { + const schemaName = pathToOaiObject[2]; + return ( + new OaiSchema( + schemaName, + parameters + .openapidefinition + .components + .schemas[schemaName], + pathToOaiObject, + ) + ).accept(this, parameters); + } + } + visitOaiReference({ body: oaiReference, path: oaiPath }, parameters) { + if (this.isStringLocalReference(oaiReference)) { + return (new OaiLocalReference(oaiReference, oaiPath)) + .accept(this, parameters); + } + // TODO: Handle remote reference + // TODO: Handle URL reference + } + visitOaiProperty( + oaiProperty, parameters + ) { + const { + name: oaiPropertyName, + body: oaiPropertyBody, + path: oaiPropertyPath, + } = oaiProperty; + + const propertyProperties = { + name: oaiPropertyName, + // isArray: isOaiPropertyArray(oaiPropertyBody), + isArray: false, // TODO: + isOptional: false // TODO: + }; + + // Handle anyOf or oneOf. + if (this.doesPropertyContainAnyOfOrOneOf(oaiProperty)) { + return this.flattenAnyOfOrOneOfInProperty(oaiProperty) + .accept(this, parameters); + } + + // Handle undefined type. + if ( + oaiPropertyBody.type === 'object' && + typeof oaiPropertyBody.additionalProperties === 'object' + ) { + return { + $class: `${parameters.metaModelNamespace}.StringProperty`, + ...propertyProperties, + decorators: [ + { + $class: 'concerto.metamodel@1.0.0.Decorator', + name: 'StringifiedJson', + } + ] + }; + } + + // Handle a reference. + if (this.doesPropertyContainAReference(oaiProperty)) { + const referenced = (new OaiReference( + oaiPropertyBody.$ref, + oaiPropertyPath + )) + .accept(this, parameters); + return { + $class: `${parameters.metaModelNamespace}.ObjectProperty`, + ...propertyProperties, + type: { + $class: `${parameters.metaModelNamespace}.TypeIdentifier`, + name: referenced.name + } + }; + } + + // Handle inline object. + if ( + oaiPropertyBody.type === 'object' && + typeof oaiPropertyBody.properties === 'object' + ) { + const inlineObjectDerivedConceptName = this.ccc( + oaiPropertyPath, + { + fullOaiObjectPathName: false + } + ); + + const inlineObjectDerivedConcept = ( + new OaiSchema( + inlineObjectDerivedConceptName, + oaiPropertyBody, + oaiPropertyPath, + ) + ).accept(this, parameters); + + return [ + { + $class: `${parameters.metaModelNamespace}.ObjectProperty`, + ...propertyProperties, + name: inlineObjectDerivedConceptName + }, + inlineObjectDerivedConcept + ]; + } + + // Handle primitive types. + return { + $class: this.inferPrimitiveConcertoType( + oaiProperty, parameters + ), + ...propertyProperties, + }; + } + visitOaiProperties(properties, parameters) { + const oaiPropertyClasses = Object.entries(properties.body) + .map( + ([oaiPropertyName, oaiPropertyBody]) => new OaiProperty( + oaiPropertyName, + oaiPropertyBody, + [...properties.path, oaiPropertyName] + ) + ); + + return oaiPropertyClasses + .map( + oaiPropertyClass => oaiPropertyClass.accept( + this, parameters + ) + ).flat(Infinity); + } + visitOaiSchema(schema, parameters) { + const oaiPropertiesClass = new OaiProperties( + schema.body.properties, + [...schema.path, 'properties'] + ); + const propertiesAndInlineObjectConcepts = oaiPropertiesClass.accept(this, parameters); + + const properties = propertiesAndInlineObjectConcepts + .filter( + propertyOrInlinDerivedConcept => + propertyOrInlinDerivedConcept.$class !== + `${parameters.metaModelNamespace}.ConceptDeclaration` + ); + + const inlineObjectConcepts = propertiesAndInlineObjectConcepts + .filter( + propertyOrInlinDerivedConcept => + propertyOrInlinDerivedConcept.$class === + `${parameters.metaModelNamespace}.ConceptDeclaration` + ); + + const concept = { + $class: `${parameters.metaModelNamespace}.ConceptDeclaration`, + name: schema.name, + isAbstract: false, + properties + }; + + if (inlineObjectConcepts.length > 0) { + return [concept, ...inlineObjectConcepts]; + } + + return concept; + } + visitOaiSchemas(schemas, parameters) { + const oaiSchemaClasses = Object.entries(schemas.body) + .map( + ([schemaName, schemaBody]) => new OaiSchema( + schemaName, + schemaBody, + [...schemas.path, schemaName] + ) + ); + + return oaiSchemaClasses.map( + oaiSchemaClass => oaiSchemaClass.accept(this, parameters) + ).flat(); + } + visitOaiFile(file, parameters) { + const oaiSchemasClass = new OaiSchemas( + file.components.schemas, + ['components', 'schemas'] + ); + + const declarations = [ + ...oaiSchemasClass.accept(this, parameters), + // TODO: Inline path payloads. + ]; + + const convertedModel = { + $class: `${parameters.metaModelNamespace}.Model`, + decorators: [], + namespace: parameters.namespace, + imports: [], // TODO: Review if anything needs to be imptorted. + declarations, + }; + + const concertoMetamodel = { + $class: `${parameters.metaModelNamespace}.Models`, + models: [ + convertedModel + ], + }; + + return concertoMetamodel; + } + visit(thing, parameters) { + if (thing.isOaiLocalReference) { + return this.visitOaiLocalReference(thing, parameters); + } + if (thing.isOaiReference) { + return this.visitOaiReference(thing, parameters); + } + if (thing.isOaiProperty) { + return this.visitOaiProperty(thing, parameters); + } + if (thing.isOaiProperties) { + return this.visitOaiProperties(thing, parameters); + } + if (thing.isOaiSchema) { + return this.visitOaiSchema(thing, parameters); + } + if (thing.isOaiSchemas) { + return this.visitOaiSchemas(thing, parameters); + } + if (this.isOaiFile(thing)) { + return this.visitOaiFile(thing, parameters); + } + return {}; + } +} + +module.exports = OpenApiVisitor; diff --git a/packages/concerto-tools/package-lock.json b/packages/concerto-tools/package-lock.json index ef565df806..f993569490 100644 --- a/packages/concerto-tools/package-lock.json +++ b/packages/concerto-tools/package-lock.json @@ -23,6 +23,7 @@ "chai": "4.3.6", "chai-as-promised": "7.1.1", "chai-things": "0.2.0", + "deep-equal-in-any-order": "2.0.2", "eslint": "8.2.0", "expect": "29.1.0", "jsdoc": "^3.6.7", @@ -46,13 +47,13 @@ } }, "node_modules/@accordproject/concerto-core": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@accordproject/concerto-core/-/concerto-core-3.2.0.tgz", - "integrity": "sha512-rBxtsacY1fAKdplYvNf2bRdwqmsfoGOujEHBbpks5gZQP+kXxGmnndzXU+Zkjsxbs/AQKsD5LG5JeOZuYGFD2A==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@accordproject/concerto-core/-/concerto-core-3.3.0.tgz", + "integrity": "sha512-TEcJULuyttaiWyOWAmgDMZqTkPtFOWLe8PZP+12qiisGRdLwhGPwH5fM4yUtuQoF/z6+C8MpgAYXV7WEzviBXg==", "dependencies": { - "@accordproject/concerto-cto": "3.2.0", - "@accordproject/concerto-metamodel": "3.2.0", - "@accordproject/concerto-util": "3.2.0", + "@accordproject/concerto-cto": "3.3.0", + "@accordproject/concerto-metamodel": "3.3.0", + "@accordproject/concerto-util": "3.3.0", "dayjs": "1.10.8", "debug": "4.3.1", "lorem-ipsum": "2.0.3", @@ -68,12 +69,12 @@ } }, "node_modules/@accordproject/concerto-cto": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@accordproject/concerto-cto/-/concerto-cto-3.2.0.tgz", - "integrity": "sha512-rCEtJrG9njmMZXt1t+VCb7gWf9aj33EhS5eWU7bYUtS0PfwxtIxuP44BJieohdFAZ4k4patLOWDBFHy6xRwNlA==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@accordproject/concerto-cto/-/concerto-cto-3.3.0.tgz", + "integrity": "sha512-yD3R8XPIFIAUaCpLP6foaj7gQA21KxxVMJYvjpYG2775QDodIDKltLViorYRO6tXZTcsZhE4P8AvsWaWVPd1bQ==", "dependencies": { - "@accordproject/concerto-metamodel": "3.2.0", - "@accordproject/concerto-util": "3.2.0", + "@accordproject/concerto-metamodel": "3.3.0", + "@accordproject/concerto-util": "3.3.0", "path-browserify": "1.0.1" }, "engines": { @@ -82,11 +83,11 @@ } }, "node_modules/@accordproject/concerto-metamodel": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@accordproject/concerto-metamodel/-/concerto-metamodel-3.2.0.tgz", - "integrity": "sha512-tLOCVCwAlrIE9S5w3ycH0OpjDdjImT0lu3hmuW/4al9U9ZtKT7baGCRSrDl2jdiY7om46OOZdIFdN92tO0gNPA==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@accordproject/concerto-metamodel/-/concerto-metamodel-3.3.0.tgz", + "integrity": "sha512-cyFaX6S4htj4x3iRqoax9A1pXq3LpGpca461PBVQowJXoqq4YJH1LYqe5DTPPIpm1Bp/m+tmfqKxQ7rn7NBZ/g==", "dependencies": { - "@accordproject/concerto-util": "3.2.0" + "@accordproject/concerto-util": "3.3.0" }, "engines": { "node": ">=14", @@ -94,9 +95,9 @@ } }, "node_modules/@accordproject/concerto-util": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@accordproject/concerto-util/-/concerto-util-3.2.0.tgz", - "integrity": "sha512-evzBWY+BLPSVgB1OgGKhT2cIXz6kZAhPqG0yWjaHAyUJYpvyN0PmBzmc4bI5AiL5RUseyBVkArfl2IAE9jz49w==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@accordproject/concerto-util/-/concerto-util-3.3.0.tgz", + "integrity": "sha512-IaJ+jEnaX6jP9F2ZIoEoQVrcRoW9ynj/hwY2XERli8QdGLOu6J270Lb243HEa2h868Vpj5WxwaRYGwgQtCTgUA==", "dependencies": { "@supercharge/promise-pool": "1.7.0", "axios": "0.23.0", @@ -3766,6 +3767,16 @@ "node": ">=0.12" } }, + "node_modules/deep-equal-in-any-order": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/deep-equal-in-any-order/-/deep-equal-in-any-order-2.0.2.tgz", + "integrity": "sha512-w3AcXmiVQ7J/F1ySul/ydPgymvj5HH0WIyt986f7qsYvn6LiNJ+bsl4/pQgElVzw/w8sn/+/NlI791I+uUbitQ==", + "dev": true, + "dependencies": { + "lodash.mapvalues": "^4.6.0", + "sort-any": "^2.0.0" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -5599,6 +5610,12 @@ "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==" }, + "node_modules/lodash.mapvalues": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.mapvalues/-/lodash.mapvalues-4.6.0.tgz", + "integrity": "sha512-JPFqXFeZQ7BfS00H58kClY7SPVeHertPE0lNuCyZ26/XlN8TvakYD7b9bGyNmXbT/D3BbtPAAmq90gPWqLkxlQ==", + "dev": true + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -7588,6 +7605,15 @@ "node": ">=8" } }, + "node_modules/sort-any": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/sort-any/-/sort-any-2.0.0.tgz", + "integrity": "sha512-T9JoiDewQEmWcnmPn/s9h/PH9t3d/LSWi0RgVmXSuDYeZXTZOZ1/wrK2PHaptuR1VXe3clLLt0pD6sgVOwjNEA==", + "dev": true, + "dependencies": { + "lodash": "^4.17.21" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -8462,13 +8488,13 @@ }, "dependencies": { "@accordproject/concerto-core": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@accordproject/concerto-core/-/concerto-core-3.2.0.tgz", - "integrity": "sha512-rBxtsacY1fAKdplYvNf2bRdwqmsfoGOujEHBbpks5gZQP+kXxGmnndzXU+Zkjsxbs/AQKsD5LG5JeOZuYGFD2A==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@accordproject/concerto-core/-/concerto-core-3.3.0.tgz", + "integrity": "sha512-TEcJULuyttaiWyOWAmgDMZqTkPtFOWLe8PZP+12qiisGRdLwhGPwH5fM4yUtuQoF/z6+C8MpgAYXV7WEzviBXg==", "requires": { - "@accordproject/concerto-cto": "3.2.0", - "@accordproject/concerto-metamodel": "3.2.0", - "@accordproject/concerto-util": "3.2.0", + "@accordproject/concerto-cto": "3.3.0", + "@accordproject/concerto-metamodel": "3.3.0", + "@accordproject/concerto-util": "3.3.0", "dayjs": "1.10.8", "debug": "4.3.1", "lorem-ipsum": "2.0.3", @@ -8480,27 +8506,27 @@ } }, "@accordproject/concerto-cto": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@accordproject/concerto-cto/-/concerto-cto-3.2.0.tgz", - "integrity": "sha512-rCEtJrG9njmMZXt1t+VCb7gWf9aj33EhS5eWU7bYUtS0PfwxtIxuP44BJieohdFAZ4k4patLOWDBFHy6xRwNlA==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@accordproject/concerto-cto/-/concerto-cto-3.3.0.tgz", + "integrity": "sha512-yD3R8XPIFIAUaCpLP6foaj7gQA21KxxVMJYvjpYG2775QDodIDKltLViorYRO6tXZTcsZhE4P8AvsWaWVPd1bQ==", "requires": { - "@accordproject/concerto-metamodel": "3.2.0", - "@accordproject/concerto-util": "3.2.0", + "@accordproject/concerto-metamodel": "3.3.0", + "@accordproject/concerto-util": "3.3.0", "path-browserify": "1.0.1" } }, "@accordproject/concerto-metamodel": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@accordproject/concerto-metamodel/-/concerto-metamodel-3.2.0.tgz", - "integrity": "sha512-tLOCVCwAlrIE9S5w3ycH0OpjDdjImT0lu3hmuW/4al9U9ZtKT7baGCRSrDl2jdiY7om46OOZdIFdN92tO0gNPA==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@accordproject/concerto-metamodel/-/concerto-metamodel-3.3.0.tgz", + "integrity": "sha512-cyFaX6S4htj4x3iRqoax9A1pXq3LpGpca461PBVQowJXoqq4YJH1LYqe5DTPPIpm1Bp/m+tmfqKxQ7rn7NBZ/g==", "requires": { - "@accordproject/concerto-util": "3.2.0" + "@accordproject/concerto-util": "3.3.0" } }, "@accordproject/concerto-util": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@accordproject/concerto-util/-/concerto-util-3.2.0.tgz", - "integrity": "sha512-evzBWY+BLPSVgB1OgGKhT2cIXz6kZAhPqG0yWjaHAyUJYpvyN0PmBzmc4bI5AiL5RUseyBVkArfl2IAE9jz49w==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@accordproject/concerto-util/-/concerto-util-3.3.0.tgz", + "integrity": "sha512-IaJ+jEnaX6jP9F2ZIoEoQVrcRoW9ynj/hwY2XERli8QdGLOu6J270Lb243HEa2h868Vpj5WxwaRYGwgQtCTgUA==", "requires": { "@supercharge/promise-pool": "1.7.0", "axios": "0.23.0", @@ -11284,6 +11310,16 @@ "type-detect": "^4.0.0" } }, + "deep-equal-in-any-order": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/deep-equal-in-any-order/-/deep-equal-in-any-order-2.0.2.tgz", + "integrity": "sha512-w3AcXmiVQ7J/F1ySul/ydPgymvj5HH0WIyt986f7qsYvn6LiNJ+bsl4/pQgElVzw/w8sn/+/NlI791I+uUbitQ==", + "dev": true, + "requires": { + "lodash.mapvalues": "^4.6.0", + "sort-any": "^2.0.0" + } + }, "deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -12648,6 +12684,12 @@ "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==" }, + "lodash.mapvalues": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.mapvalues/-/lodash.mapvalues-4.6.0.tgz", + "integrity": "sha512-JPFqXFeZQ7BfS00H58kClY7SPVeHertPE0lNuCyZ26/XlN8TvakYD7b9bGyNmXbT/D3BbtPAAmq90gPWqLkxlQ==", + "dev": true + }, "lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -14144,6 +14186,15 @@ "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==" }, + "sort-any": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/sort-any/-/sort-any-2.0.0.tgz", + "integrity": "sha512-T9JoiDewQEmWcnmPn/s9h/PH9t3d/LSWi0RgVmXSuDYeZXTZOZ1/wrK2PHaptuR1VXe3clLLt0pD6sgVOwjNEA==", + "dev": true, + "requires": { + "lodash": "^4.17.21" + } + }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", diff --git a/packages/concerto-tools/package.json b/packages/concerto-tools/package.json index 52914d36aa..9650a5b086 100644 --- a/packages/concerto-tools/package.json +++ b/packages/concerto-tools/package.json @@ -49,6 +49,7 @@ "chai": "4.3.6", "chai-as-promised": "7.1.1", "chai-things": "0.2.0", + "deep-equal-in-any-order": "2.0.2", "eslint": "8.2.0", "expect": "29.1.0", "jsdoc": "^3.6.7", diff --git a/packages/concerto-tools/test/codegen/codegen.js b/packages/concerto-tools/test/codegen/codegen.js index f2d2fbad59..7488328510 100644 --- a/packages/concerto-tools/test/codegen/codegen.js +++ b/packages/concerto-tools/test/codegen/codegen.js @@ -75,4 +75,4 @@ describe('codegen', function () { }); }); }); -}); \ No newline at end of file +}); diff --git a/packages/concerto-tools/test/codegen/fromJsonSchema/cto/data/concertoJsonModel.json b/packages/concerto-tools/test/codegen/fromJsonSchema/cto/data/concertoJsonModel.json new file mode 100644 index 0000000000..1976f36e55 --- /dev/null +++ b/packages/concerto-tools/test/codegen/fromJsonSchema/cto/data/concertoJsonModel.json @@ -0,0 +1,599 @@ +{ + "$class": "concerto.metamodel@1.0.0.Models", + "models": [ + { + "$class": "concerto.metamodel@1.0.0.Model", + "decorators": [], + "namespace": "com.test@1.0.0", + "imports": [], + "declarations": [ + { + "$class": "concerto.metamodel@1.0.0.ConceptDeclaration", + "name": "Root", + "isAbstract": false, + "properties": [ + { + "$class": "concerto.metamodel@1.0.0.StringProperty", + "name": "firstName", + "isArray": false, + "isOptional": false + }, + { + "$class": "concerto.metamodel@1.0.0.StringProperty", + "name": "lastName", + "isArray": false, + "isOptional": false + }, + { + "$class": "concerto.metamodel@1.0.0.DateTimeProperty", + "name": "dob", + "isArray": false, + "isOptional": false + }, + { + "$class": "concerto.metamodel@1.0.0.DateTimeProperty", + "name": "graduationDate", + "isArray": false, + "isOptional": true + }, + { + "$class": "concerto.metamodel@1.0.0.IntegerProperty", + "name": "age", + "isArray": false, + "isOptional": false + }, + { + "$class": "concerto.metamodel@1.0.0.DoubleProperty", + "name": "height", + "isArray": false, + "isOptional": false, + "validator": { + "$class": "concerto.metamodel@1.0.0.DoubleDomainValidator", + "lower": 50 + } + }, + { + "$class": "concerto.metamodel@1.0.0.StringProperty", + "name": "favouriteFood", + "isArray": false, + "isOptional": true + }, + { + "$class": "concerto.metamodel@1.0.0.ObjectProperty", + "name": "children", + "isArray": true, + "isOptional": false, + "type": { + "$class": "concerto.metamodel@1.0.0.TypeIdentifier", + "name": "Root$_children" + } + }, + { + "$class": "concerto.metamodel@1.0.0.ObjectProperty", + "name": "company", + "isArray": false, + "isOptional": false, + "type": { + "$class": "concerto.metamodel@1.0.0.TypeIdentifier", + "name": "Root$_company" + } + }, + { + "$class": "concerto.metamodel@1.0.0.StringProperty", + "name": "alternative", + "isArray": false, + "isOptional": true + } + ] + }, + { + "$class": "concerto.metamodel@1.0.0.ConceptDeclaration", + "name": "Root$_children", + "isAbstract": false, + "properties": [ + { + "$class": "concerto.metamodel@1.0.0.StringProperty", + "name": "name", + "isArray": false, + "isOptional": false + }, + { + "$class": "concerto.metamodel@1.0.0.IntegerProperty", + "name": "age", + "isArray": false, + "isOptional": false + }, + { + "$class": "concerto.metamodel@1.0.0.StringProperty", + "name": "hairColor", + "isArray": false, + "isOptional": false + }, + { + "$class": "concerto.metamodel@1.0.0.BooleanProperty", + "name": "coolDude", + "isArray": false, + "isOptional": false + }, + { + "$class": "concerto.metamodel@1.0.0.StringProperty", + "name": "missing", + "isArray": false, + "isOptional": true + }, + { + "$class": "concerto.metamodel@1.0.0.ObjectProperty", + "name": "pet", + "isArray": false, + "isOptional": false, + "type": { + "$class": "concerto.metamodel@1.0.0.TypeIdentifier", + "name": "Root$_children$_pet" + } + }, + { + "$class": "concerto.metamodel@1.0.0.StringProperty", + "name": "favoriteColors", + "isArray": true, + "isOptional": false + }, + { + "$class": "concerto.metamodel@1.0.0.IntegerProperty", + "name": "favoriteNumbers", + "isArray": true, + "isOptional": false + }, + { + "$class": "concerto.metamodel@1.0.0.StringProperty", + "name": "mixed", + "isArray": true, + "isOptional": false + }, + { + "$class": "concerto.metamodel@1.0.0.StringProperty", + "name": "arrayOfNull", + "isArray": true, + "isOptional": false + }, + { + "$class": "concerto.metamodel@1.0.0.StringProperty", + "name": "emptyArray", + "isArray": true, + "isOptional": false + }, + { + "$class": "concerto.metamodel@1.0.0.ObjectProperty", + "name": "favoritePets", + "isArray": true, + "isOptional": false, + "type": { + "$class": "concerto.metamodel@1.0.0.TypeIdentifier", + "name": "Root$_children$_favoritePets" + } + }, + { + "$class": "concerto.metamodel@1.0.0.ObjectProperty", + "name": "stuff", + "isArray": true, + "isOptional": false, + "type": { + "$class": "concerto.metamodel@1.0.0.TypeIdentifier", + "name": "Root$_children$_stuff" + } + }, + { + "$class": "concerto.metamodel@1.0.0.StringProperty", + "name": "alternative", + "isArray": false, + "isOptional": true + } + ] + }, + { + "$class": "concerto.metamodel@1.0.0.ConceptDeclaration", + "name": "Root$_children$_pet", + "isAbstract": false, + "properties": [ + { + "$class": "concerto.metamodel@1.0.0.StringProperty", + "name": "name", + "isArray": false, + "isOptional": false + }, + { + "$class": "concerto.metamodel@1.0.0.StringProperty", + "name": "breed", + "isArray": false, + "isOptional": false + } + ] + }, + { + "$class": "concerto.metamodel@1.0.0.ConceptDeclaration", + "name": "Root$_children$_favoritePets", + "isAbstract": false, + "properties": [ + { + "$class": "concerto.metamodel@1.0.0.StringProperty", + "name": "name", + "isArray": false, + "isOptional": false + }, + { + "$class": "concerto.metamodel@1.0.0.StringProperty", + "name": "breed", + "isArray": false, + "isOptional": false + } + ] + }, + { + "$class": "concerto.metamodel@1.0.0.ConceptDeclaration", + "name": "Root$_children$_stuff", + "isAbstract": false, + "properties": [ + { + "$class": "concerto.metamodel@1.0.0.StringProperty", + "name": "sku", + "isArray": false, + "isOptional": false + }, + { + "$class": "concerto.metamodel@1.0.0.DoubleProperty", + "name": "price", + "isArray": false, + "isOptional": false + }, + { + "$class": "concerto.metamodel@1.0.0.ObjectProperty", + "name": "product", + "isArray": false, + "isOptional": false, + "type": { + "$class": "concerto.metamodel@1.0.0.TypeIdentifier", + "name": "Pet" + } + } + ] + }, + { + "$class": "concerto.metamodel@1.0.0.ConceptDeclaration", + "name": "Root$_company", + "isAbstract": false, + "properties": [ + { + "$class": "concerto.metamodel@1.0.0.StringProperty", + "name": "name", + "isArray": false, + "isOptional": false + }, + { + "$class": "concerto.metamodel@1.0.0.ObjectProperty", + "name": "employees", + "isArray": true, + "isOptional": false, + "type": { + "$class": "concerto.metamodel@1.0.0.TypeIdentifier", + "name": "Root$_company$_employees" + } + } + ] + }, + { + "$class": "concerto.metamodel@1.0.0.ConceptDeclaration", + "name": "Root$_company$_employees", + "isAbstract": false, + "properties": [ + { + "$class": "concerto.metamodel@1.0.0.StringProperty", + "name": "name", + "isArray": false, + "isOptional": false + } + ] + }, + { + "$class": "concerto.metamodel@1.0.0.ConceptDeclaration", + "name": "Children", + "isAbstract": false, + "properties": [ + { + "$class": "concerto.metamodel@1.0.0.StringProperty", + "name": "name", + "isArray": false, + "isOptional": false + }, + { + "$class": "concerto.metamodel@1.0.0.IntegerProperty", + "name": "age", + "isArray": false, + "isOptional": false, + "validator": { + "$class": "concerto.metamodel@1.0.0.IntegerDomainValidator", + "upper": 150 + } + }, + { + "$class": "concerto.metamodel@1.0.0.StringProperty", + "name": "hairColor", + "isArray": false, + "isOptional": false + }, + { + "$class": "concerto.metamodel@1.0.0.BooleanProperty", + "name": "coolDude", + "isArray": false, + "isOptional": false, + "defaultValue": "true" + }, + { + "$class": "concerto.metamodel@1.0.0.StringProperty", + "name": "missing", + "isArray": false, + "isOptional": true + }, + { + "$class": "concerto.metamodel@1.0.0.ObjectProperty", + "name": "pet", + "isArray": false, + "isOptional": false, + "type": { + "$class": "concerto.metamodel@1.0.0.TypeIdentifier", + "name": "Pet" + } + }, + { + "$class": "concerto.metamodel@1.0.0.ObjectProperty", + "name": "favoriteColors", + "isArray": true, + "isOptional": false, + "type": { + "$class": "concerto.metamodel@1.0.0.TypeIdentifier", + "name": "Color" + } + }, + { + "$class": "concerto.metamodel@1.0.0.IntegerProperty", + "name": "favoriteNumbers", + "isArray": true, + "isOptional": false, + "validator": { + "$class": "concerto.metamodel@1.0.0.IntegerDomainValidator", + "upper": 999999 + } + }, + { + "$class": "concerto.metamodel@1.0.0.StringProperty", + "name": "mixed", + "isArray": true, + "isOptional": false + }, + { + "$class": "concerto.metamodel@1.0.0.StringProperty", + "name": "arrayOfNull", + "isArray": true, + "isOptional": false + }, + { + "$class": "concerto.metamodel@1.0.0.StringProperty", + "name": "emptyArray", + "isArray": true, + "isOptional": false + }, + { + "$class": "concerto.metamodel@1.0.0.ObjectProperty", + "name": "favoritePets", + "isArray": true, + "isOptional": false, + "type": { + "$class": "concerto.metamodel@1.0.0.TypeIdentifier", + "name": "Pet" + } + }, + { + "$class": "concerto.metamodel@1.0.0.ObjectProperty", + "name": "stuff", + "isArray": true, + "isOptional": false, + "type": { + "$class": "concerto.metamodel@1.0.0.TypeIdentifier", + "name": "definitions$_Children$_stuff" + } + }, + { + "$class": "concerto.metamodel@1.0.0.StringProperty", + "name": "json", + "isArray": false, + "isOptional": true, + "decorators": [ + { + "$class": "concerto.metamodel@1.0.0.Decorator", + "name": "StringifiedJson" + } + ] + }, + { + "$class": "concerto.metamodel@1.0.0.StringProperty", + "name": "jsonWithAdditionalProperties", + "isArray": false, + "isOptional": true, + "decorators": [ + { + "$class": "concerto.metamodel@1.0.0.Decorator", + "name": "StringifiedJson" + } + ] + }, + { + "$class": "concerto.metamodel@1.0.0.DoubleProperty", + "name": "alternation", + "isArray": false, + "isOptional": true + } + ] + }, + { + "$class": "concerto.metamodel@1.0.0.ConceptDeclaration", + "name": "definitions$_Children$_stuff", + "isAbstract": false, + "properties": [ + { + "$class": "concerto.metamodel@1.0.0.StringProperty", + "name": "sku", + "isArray": false, + "isOptional": false + }, + { + "$class": "concerto.metamodel@1.0.0.DoubleProperty", + "name": "price", + "isArray": false, + "isOptional": false + }, + { + "$class": "concerto.metamodel@1.0.0.ObjectProperty", + "name": "product", + "isArray": false, + "isOptional": false, + "type": { + "$class": "concerto.metamodel@1.0.0.TypeIdentifier", + "name": "Pet" + } + } + ] + }, + { + "$class": "concerto.metamodel@1.0.0.EnumDeclaration", + "name": "Color", + "properties": [ + { + "$class": "concerto.metamodel@1.0.0.EnumProperty", + "name": "blue" + }, + { + "$class": "concerto.metamodel@1.0.0.EnumProperty", + "name": "green" + }, + { + "$class": "concerto.metamodel@1.0.0.EnumProperty", + "name": "red" + }, + { + "$class": "concerto.metamodel@1.0.0.EnumProperty", + "name": "yellow" + }, + { + "$class": "concerto.metamodel@1.0.0.EnumProperty", + "name": "orange" + } + ] + }, + { + "$class": "concerto.metamodel@1.0.0.ConceptDeclaration", + "name": "Pet", + "isAbstract": false, + "properties": [ + { + "$class": "concerto.metamodel@1.0.0.StringProperty", + "name": "name", + "isArray": false, + "isOptional": false + }, + { + "$class": "concerto.metamodel@1.0.0.StringProperty", + "name": "breed", + "isArray": false, + "isOptional": false, + "validator": { + "$class": "concerto.metamodel@1.0.0.StringRegexValidator", + "pattern": "^[a-zA-Z]*$", + "flags": "u" + } + } + ] + }, + { + "$class": "concerto.metamodel@1.0.0.ConceptDeclaration", + "name": "Stuff", + "isAbstract": false, + "properties": [ + { + "$class": "concerto.metamodel@1.0.0.StringProperty", + "name": "sku", + "isArray": false, + "isOptional": false + }, + { + "$class": "concerto.metamodel@1.0.0.DoubleProperty", + "name": "price", + "isArray": false, + "isOptional": false, + "validator": { + "$class": "concerto.metamodel@1.0.0.DoubleDomainValidator", + "upper": 99999999 + } + }, + { + "$class": "concerto.metamodel@1.0.0.ObjectProperty", + "name": "product", + "isArray": false, + "isOptional": false, + "type": { + "$class": "concerto.metamodel@1.0.0.TypeIdentifier", + "name": "Pet" + } + } + ] + }, + { + "$class": "concerto.metamodel@1.0.0.ConceptDeclaration", + "name": "Company", + "isAbstract": false, + "properties": [ + { + "$class": "concerto.metamodel@1.0.0.StringProperty", + "name": "name", + "isArray": false, + "isOptional": false + }, + { + "$class": "concerto.metamodel@1.0.0.ObjectProperty", + "name": "employees", + "isArray": true, + "isOptional": false, + "type": { + "$class": "concerto.metamodel@1.0.0.TypeIdentifier", + "name": "definitions$_Company$_employees" + } + } + ] + }, + { + "$class": "concerto.metamodel@1.0.0.ConceptDeclaration", + "name": "definitions$_Company$_employees", + "isAbstract": false, + "properties": [ + { + "$class": "concerto.metamodel@1.0.0.StringProperty", + "name": "name", + "isArray": false, + "isOptional": false + } + ] + }, + { + "$class": "concerto.metamodel@1.0.0.ConceptDeclaration", + "name": "Employees", + "isAbstract": false, + "properties": [ + { + "$class": "concerto.metamodel@1.0.0.StringProperty", + "name": "name", + "isArray": false, + "isOptional": false + } + ] + } + ] + } + ] +} diff --git a/packages/concerto-tools/test/codegen/fromJsonSchema/cto/data/concertoModel.cto b/packages/concerto-tools/test/codegen/fromJsonSchema/cto/data/concertoModel.cto new file mode 100644 index 0000000000..3a6049a7ae --- /dev/null +++ b/packages/concerto-tools/test/codegen/fromJsonSchema/cto/data/concertoModel.cto @@ -0,0 +1,115 @@ +namespace com.test@1.0.0 + +concept Root { + o String firstName + o String lastName + o DateTime dob + o DateTime graduationDate optional + o Integer age + o Double height range=[50,] + o String favouriteFood optional + o Root$_children[] children + o Root$_company company + o String alternative optional +} + +concept Root$_children { + o String name + o Integer age + o String hairColor + o Boolean coolDude + o String missing optional + o Root$_children$_pet pet + o String[] favoriteColors + o Integer[] favoriteNumbers + o String[] mixed + o String[] arrayOfNull + o String[] emptyArray + o Root$_children$_favoritePets[] favoritePets + o Root$_children$_stuff[] stuff + o String alternative optional +} + +concept Root$_children$_pet { + o String name + o String breed +} + +concept Root$_children$_favoritePets { + o String name + o String breed +} + +concept Root$_children$_stuff { + o String sku + o Double price + o Pet product +} + +concept Root$_company { + o String name + o Root$_company$_employees[] employees +} + +concept Root$_company$_employees { + o String name +} + +concept Children { + o String name + o Integer age range=[,150] + o String hairColor + o Boolean coolDude + o String missing optional + o Pet pet + o Color[] favoriteColors + o Integer[] favoriteNumbers range=[,999999] + o String[] mixed + o String[] arrayOfNull + o String[] emptyArray + o Pet[] favoritePets + o definitions$_Children$_stuff[] stuff + @StringifiedJson + o String json optional + @StringifiedJson + o String jsonWithAdditionalProperties optional + o Double alternation optional +} + +concept definitions$_Children$_stuff { + o String sku + o Double price + o Pet product +} + +enum Color { + o blue + o green + o red + o yellow + o orange +} + +concept Pet { + o String name + o String breed regex=/^[a-zA-Z]*$/u +} + +concept Stuff { + o String sku + o Double price range=[,99999999] + o Pet product +} + +concept Company { + o String name + o definitions$_Company$_employees[] employees +} + +concept definitions$_Company$_employees { + o String name +} + +concept Employees { + o String name +} diff --git a/packages/concerto-tools/test/codegen/fromJsonSchema/cto/data/full.cto b/packages/concerto-tools/test/codegen/fromJsonSchema/cto/data/full.cto deleted file mode 100644 index 3915974ffe..0000000000 --- a/packages/concerto-tools/test/codegen/fromJsonSchema/cto/data/full.cto +++ /dev/null @@ -1,59 +0,0 @@ -namespace org.acme - -concept Children { - o String name - o Integer age - o String hairColor - o Boolean coolDude - o String missing optional - o Pet pet - o Color[] favoriteColors - o Integer[] favoriteNumbers - o String[] mixed - o String[] arrayOfNull - o String[] emptyArray - o Pet[] favoritePets - o Stuff[] stuff - o String json optional - o Double alternation optional -} - -enum Color { - o blue - o green - o red - o yellow - o orange -} - -concept Pet { - o String name - o String breed -} - -concept Stuff { - o String sku - o Double price - o Pet product -} - -concept Company { - o String name - o Employees[] employees -} - -concept Employees { - o String name -} - -concept Root { - o String firstName - o String lastName - o DateTime dob - o Integer age - o Double height - o Children[] children - o Company company - o String alternative optional -} - diff --git a/packages/concerto-tools/test/codegen/fromJsonSchema/cto/data/schema.json b/packages/concerto-tools/test/codegen/fromJsonSchema/cto/data/jsonSchemaModel.json similarity index 93% rename from packages/concerto-tools/test/codegen/fromJsonSchema/cto/data/schema.json rename to packages/concerto-tools/test/codegen/fromJsonSchema/cto/data/jsonSchemaModel.json index 21e0f3303b..385f47c999 100644 --- a/packages/concerto-tools/test/codegen/fromJsonSchema/cto/data/schema.json +++ b/packages/concerto-tools/test/codegen/fromJsonSchema/cto/data/jsonSchemaModel.json @@ -16,13 +16,16 @@ "type": "string" }, "age": { - "type": "integer" + "type": "integer", + "minimum": 0, + "exclusiveMaximum": 150 }, "hairColor": { "type": "string" }, "coolDude": { - "type": "boolean" + "type": "boolean", + "default": "true" }, "missing": { "type": "string" @@ -42,7 +45,8 @@ "favoriteNumbers": { "type": "array", "items": { - "type": "integer" + "type": "integer", + "exclusiveMaximum": 999999 } }, "mixed": { @@ -104,6 +108,13 @@ "json": { "type": "object" }, + "jsonWithAdditionalProperties": { + "type": "object", + "additionalProperties": { + "maxLength": 500, + "type": "string" + } + }, "alternation": { "oneOf": [ { "type": "number" }, @@ -147,7 +158,8 @@ "type": "string" }, "breed": { - "type": "string" + "type": "string", + "pattern": "^[a-zA-Z]*$" } }, "required": ["$class", "name", "breed"] @@ -167,7 +179,8 @@ "type": "string" }, "price": { - "type": "number" + "type": "number", + "exclusiveMaximum": 99999999.0 }, "product": { "title": "Pet", @@ -253,11 +266,21 @@ "format": "date-time", "type": "string" }, + "graduationDate": { + "format": "date", + "type": "string" + }, "age": { - "type": "integer" + "type": "integer", + "minimum": 0 }, "height": { - "type": "number" + "type": "number", + "minimum": 50.0 + }, + "favouriteFood": { + "format": "food", + "type": "string" }, "children": { "type": "array", @@ -377,7 +400,8 @@ "type": "string" }, "price": { - "type": "number" + "type": "number", + "minimum": 0.0 }, "product": { "title": "Pet", diff --git a/packages/concerto-tools/test/codegen/fromJsonSchema/cto/inferModel.js b/packages/concerto-tools/test/codegen/fromJsonSchema/cto/inferModel.js.old similarity index 100% rename from packages/concerto-tools/test/codegen/fromJsonSchema/cto/inferModel.js rename to packages/concerto-tools/test/codegen/fromJsonSchema/cto/inferModel.js.old diff --git a/packages/concerto-tools/test/codegen/fromJsonSchema/cto/jsonschemavisitor.js b/packages/concerto-tools/test/codegen/fromJsonSchema/cto/jsonschemavisitor.js new file mode 100644 index 0000000000..b77abdd5fb --- /dev/null +++ b/packages/concerto-tools/test/codegen/fromJsonSchema/cto/jsonschemavisitor.js @@ -0,0 +1,117 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +const chai = require('chai'); +const deepEqualInAnyOrder = require('deep-equal-in-any-order'); +chai.should(); +chai.use(deepEqualInAnyOrder); +const { assert } = chai; +const fs = require('fs'); +const path = require('path'); +const Printer = require('@accordproject/concerto-cto').Printer; + +const JsonSchemaVisitor = require( + '../../../../lib/codegen/fromjsonschema/cto/jsonSchemaVisitor.js' +); +const { JsonSchemaModel } = require( + '../../../../lib/codegen/fromjsonschema/cto/jsonSchemaClasses.js' +); + +const jsonSchemaVisitor = new JsonSchemaVisitor(); +const jsonSchemaVisitorParameters = { + metaModelNamespace: 'concerto.metamodel@1.0.0', + namespace: 'com.test@1.0.0', +}; + +describe('JsonSchemaVisitor', function () { + it( + 'should generate a Concerto JSON and CTO from a JSON schema', + async () => { + const jsonSchemaModel = JSON.parse( + fs.readFileSync( + path.resolve( + __dirname, '../cto/data/jsonSchemaModel.json' + ), 'utf8' + ) + ); + const desiredConcertoJsonModelString = fs.readFileSync( + path.resolve( + __dirname, '../cto/data/concertoJsonModel.json' + ), 'utf8' + ); + const desiredConcertoJsonModel = JSON.parse( + desiredConcertoJsonModelString + ); + const desiredConcertoModel = fs.readFileSync( + path.resolve( + __dirname, '../cto/data/concertoModel.cto' + ), 'utf8' + ); + + const jsonSchemaModelClass = new JsonSchemaModel(jsonSchemaModel); + + const inferredConcertoJsonModel = jsonSchemaModelClass.accept( + jsonSchemaVisitor, jsonSchemaVisitorParameters + ); + + const inferredConcertoModel = Printer.toCTO( + inferredConcertoJsonModel.models[0] + ); + + // TODO: Remove before PR merge. + // fs.writeFileSync( + // '/Users/stefan.blaginov/Implementations/stefanblaginov-concerto/packages/concerto-tools/test/codegen/fromjsonschema/cto/data/out.json', + // JSON.stringify(inferredConcertoJsonModel, null, 4), + // 'utf8', + // ); + // fs.writeFileSync( + // '/Users/stefan.blaginov/Implementations/stefanblaginov-concerto/packages/concerto-tools/test/codegen/fromjsonschema/cto/data/out.cto', + // inferredConcertoModel, + // 'utf8', + // ); + + // @ts-ignore + assert.deepEqualInAnyOrder( + inferredConcertoJsonModel, desiredConcertoJsonModel + ); + + assert.equal( + JSON.stringify(inferredConcertoJsonModel, null, 4) + '\n', + desiredConcertoJsonModelString + ); + + assert.equal( + inferredConcertoModel + '\n', + desiredConcertoModel + ); + }); + + it('should not generate when unsupported type keywords are used', async () => { + (function () { + const jsonSchemaModelClass = new JsonSchemaModel({ + $schema: 'http://json-schema.org/draft-07/schema#', + type: 'object', + properties: { + Foo: { type: 'bar' } + } + }); + + jsonSchemaModelClass.accept( + jsonSchemaVisitor, jsonSchemaVisitorParameters + ); + }).should.throw('Type keyword \'bar\' in \'Foo\' is not supported.'); + }); +}); diff --git a/packages/concerto-tools/test/codegen/fromopenapi/cto/data/openapidefinition.json b/packages/concerto-tools/test/codegen/fromopenapi/cto/data/openapidefinition.json new file mode 100644 index 0000000000..2014fe6500 --- /dev/null +++ b/packages/concerto-tools/test/codegen/fromopenapi/cto/data/openapidefinition.json @@ -0,0 +1,107 @@ +{ + "components": { + "schemas": { + "address": { + "description": "", + "properties": { + "city": { + "description": "City, district, suburb, town, or village.", + "maxLength": 5000, + "nullable": true, + "type": "string" + }, + "country": { + "description": "Two-letter country code ([ISO 3166-1 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2)).", + "maxLength": 5000, + "nullable": true, + "type": "string" + }, + "line1": { + "description": "Address line 1 (e.g., street, PO Box, or company name).", + "maxLength": 5000, + "nullable": true, + "type": "string" + }, + "line2": { + "description": "Address line 2 (e.g., apartment, suite, unit, or building).", + "maxLength": 5000, + "nullable": true, + "type": "string" + }, + "postal_code": { + "description": "ZIP or postal code.", + "maxLength": 5000, + "nullable": true, + "type": "string" + }, + "state": { + "description": "State, county, province, or region.", + "maxLength": 5000, + "nullable": true, + "type": "string" + } + }, + "title": "Address", + "type": "object", + "x-expandableFields": [] + }, + "customer": { + "description": "This object represents a customer of your business. It lets you create recurring charges and track payments that belong to the same customer.\n\nRelated guide: [Save a card during payment](https://stripe.com/docs/payments/save-during-payment).", + "properties": { + "created": { + "description": "Time at which the object was created. Measured in seconds since the Unix epoch.", + "format": "unix-time", + "type": "integer" + }, + "id": { + "description": "Unique identifier for the object.", + "maxLength": 5000, + "type": "string" + }, + "address": { + "anyOf": [ + { + "$ref": "#/components/schemas/address" + } + ], + "description": "The customer's address.", + "nullable": true + }, + "metadata": { + "additionalProperties": { + "maxLength": 500, + "type": "string" + }, + "description": "Set of [key-value pairs](https://stripe.com/docs/api/metadata) that you can attach to an object. This can be useful for storing additional information about the object in a structured format.", + "type": "object" + }, + "foo": { + "description": "A foo.", + "type": "object", + "properties": { + "bar": { + "description": "", + "type": "object", + "properties": { + "har": { + "description": "", + "maxLength": 5000, + "nullable": true, + "type": "string" + } + } + } + } + } + }, + "required": [ + "created", + "id" + ], + "title": "Customer", + "type": "object", + "x-resourceId": "customer" + } + } + } +} diff --git a/packages/concerto-tools/test/codegen/fromopenapi/cto/inferModel.js b/packages/concerto-tools/test/codegen/fromopenapi/cto/inferModel.js new file mode 100644 index 0000000000..b8cc1de68b --- /dev/null +++ b/packages/concerto-tools/test/codegen/fromopenapi/cto/inferModel.js @@ -0,0 +1,52 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +const chai = require('chai'); +chai.should(); +const fs = require('fs'); +const path = require('path'); + +// const inferModel = require('../../../../lib/codegen/fromopenapi/cto/concertometamodelvisitor.js'); + +const ConcertoMetamodelVisitor = require('../../../../lib/codegen/fromopenapi/cto/openapivisitor.js'); + +// const options = { capitalizeFirstLetterOfTypeName: true }; + +describe('inferModel', function () { + it( + 'should generate a Concerto metamodel from an OpenAPI definition', async () => { + const openapidefinition = JSON.parse( + fs.readFileSync( + path.resolve( + __dirname, '../cto/data/openapidefinition.json' + ), 'utf8' + ) + ); + + const visitor = new ConcertoMetamodelVisitor(); + + const metamodel = visitor.visit( + // 'org.acme', + // 'Root', + openapidefinition, + { + namespace: 'com.test@1.0.0', + openapidefinition, + metaModelNamespace: 'concerto.metamodel@1.0.0' + } + ); + }); +});