UNPKG

@api-components/api-forms

Version:

A library containing helper classes to compute API data from the AMF web API model.

1,395 lines (1,331 loc) 46 kB
/* eslint-disable no-plusplus */ /* eslint-disable no-param-reassign */ /* eslint-disable class-methods-use-this */ /** * @license * Copyright 2016 The Advanced REST client authors <arc@mulesoft.com> * 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. */ import { AmfHelperMixin } from '@api-components/amf-helper-mixin/amf-helper-mixin.js'; import { ExampleGenerator } from '@api-components/api-example-generator'; /** @typedef {import('@advanced-rest-client/arc-types').FormTypes.AmfFormItem} AmfFormItem */ /** @typedef {import('@advanced-rest-client/arc-types').FormTypes.AmfFormItemSchema} AmfFormItemSchema */ /** @typedef {import('@advanced-rest-client/arc-types').FormTypes.Example} Example */ /** @typedef {import('./types').ConstructorOptions} ConstructorOptions */ /** @typedef {import('./types').ProcessOptions} ProcessOptions */ const GLOBAL_PATH_PARAMS = []; const GLOBAL_QUERY_PARAMS = []; const GLOBAL_OTHER_PARAMS = []; /** * Generates a common key from data model item. * * @param {AmfFormItem} data AMF type object model. * @return {String} Generated key to search for the item. */ function getKey(data) { let key = `${data.name}-${data.schema.apiType}`; if (data.schema.enum) { key += '-enum'; } if (data.schema.required) { key += '-required'; } return key; } /** * Searches for a model value in cache store. * * @param {string} binding * @param {AmfFormItem} data AMF model item. * @return {AmfFormItem|undefined} Model item or undefined if not found. */ function getGlobalValue(binding, data) { let store; switch (binding) { case 'query': store = GLOBAL_QUERY_PARAMS; break; case 'path': store = GLOBAL_PATH_PARAMS; break; default: store = GLOBAL_OTHER_PARAMS; break; } if (!store.length) { return undefined; } const key = getKey(data); const item = store.find((storeItem) => storeItem.key === key); if (item) { return item.value; } return undefined; } /** * Appends a value item to the global params. * * @param {string} binding * @param {AmfFormItem} data Model item to be added to the list. */ function appendGlobalValue(binding, data) { const item = getGlobalValue(binding, data); if (item) { return; } const key = getKey(data); const model = { key, value: data }; switch (binding) { case 'query': GLOBAL_QUERY_PARAMS.push(model); break; case 'path': GLOBAL_PATH_PARAMS.push(model); break; default: GLOBAL_OTHER_PARAMS.push(model); break; } } const NUMBER_INPUT_TYPES = ['number', 'integer', 'float', 'double']; /** * An element to transform AMF LD model into a form view model. * * Note, this element does not include polyfills for `Promise` and `Array.from`. * * The model should be used to build a form view for request parameters * like header, query parameters, uri parameters or the body. * * ## Example * * ```html * <api-view-model-transformer on-view-model-changed="_updateView"></api-view-model-transformer> * <script> * const amfModel = getAmfFromRamlOrOas(); * const processor = document.querySelector('api-view-model-transformer'); * processor.amf = amfModel; * processor.shape = extractHeadersForMethod(amfModel); * processor.addEventListener('view-model-changed', (e) => { * console.log(e.detail.value); * }); * </script> * ``` * * This example uses `getAmfFromRamlOrOas()` function where you implement * the logic of getting AMF json/ld data. It can be stored in file or parsed * using AMF parsers. The `extractHeadersForMethod()` represents a logic to * extract properties that you want to transform. It can be headers, query * parameters or body type. */ export class ApiViewModel extends AmfHelperMixin(Object) { /** * @param {ConstructorOptions=} [opts={}] */ constructor(opts = {}) { super(); /** * An array of properties for which view model is to be generated. * It accepts model for headers, query parameters, uri parameters and * body. * If `manualModel` is not set, assigning a value to this property will * trigger model computation. Otherwise call `computeViewModel()` * function manually to generate the model. */ this.amf = opts.amf; /** * Does not compute documentation for the processed property */ this.noDocs = opts.noDocs; } /** * Clears cache for computed models. * All computed models are kept in in-memory cache to another call for computation * of the same model will result with reference to already computed value. * This function clears all cached objects. * * Note, the memory won't be freed for objects that are in use. */ clearCache() { GLOBAL_PATH_PARAMS.splice(0, GLOBAL_PATH_PARAMS.length); GLOBAL_QUERY_PARAMS.splice(0, GLOBAL_QUERY_PARAMS.length); GLOBAL_OTHER_PARAMS.splice(0, GLOBAL_OTHER_PARAMS.length); } /** * Computes view model from AMF data model. This should not be called if * `manualModel` is not set. Use `shape` property instead. * * @param {any} shape AMF type model. If not set it uses `shape` property of the element. * @return {AmfFormItem[]|undefined} A promise resolved to generated model. */ computeViewModel(shape) { this.viewModel = undefined; if (!shape) { return undefined; } if (Array.isArray(shape)) { // creates a shallow copy so it's not altered later. shape = Array.from(shape); } const result = this._computeViewModel(shape); this.viewModel = result; return result; } /** * Computes model for each item recursively. It allows browser to return * the event loop and prohibit ANR to show. * * @param {any} items List of remanding AMF model items. * This should be copy of the model since this function removes items from * the list. * @return {AmfFormItem[]} The view model. */ _computeViewModel(items) { let result = []; if (!items) { return result; } const isArray = Array.isArray(items); if (isArray && !items.length) { return result; } if (isArray) { for (let i = 0, len = items.length; i < len; i++) { const item = items[i]; const model = this.uiModelForAmfItem(item); if (model) { result.push(model); } } } else if (this._hasType(items, this.ns.raml.vocabularies.data.Object)) { const data = this.modelForRawObject(items); if (data) { result = data; } } else if (this._hasType(items, this.ns.w3.shacl.NodeShape)) { result = this._processNodeShape(items); } else if (this._hasType(items, this.ns.aml.vocabularies.shapes.ScalarShape)) { const data = this._uiModelForPropertyShape(items); if (data) { result[result.length] = data; } } else if (this._hasType(items, this.ns.aml.vocabularies.shapes.UnionShape)) { result = this._modelForUnion(items); } return result; } /** * Creates a UI model item from AMF json/ld model. * @param {any} amfItem AMF model with schema for * `http://raml.org/vocabularies/http#Parameter` * @return {AmfFormItem|undefined} UI data model. */ uiModelForAmfItem(amfItem) { if (this._hasType(amfItem, this.ns.aml.vocabularies.apiContract.Parameter)) { return this._uiModelForParameter(amfItem); } if (this._hasType(amfItem, this.ns.w3.shacl.PropertyShape)) { return this._uiModelForPropertyShape(amfItem); } return undefined; } /** * Creates a model for a shacl's PropertyShape. It can be found, for example, * in `queryString` of security scheme settings. * * @param {any} shape The shape to process * @return {AmfFormItem[]} Generated view model for an item. */ _processNodeShape(shape) { this._resolve(shape); const key = this._getAmfKey(this.ns.w3.shacl.property); const items = this._ensureArray(shape[key]); const result = []; if (!items) { return result; } for (let i = 0, len = items.length; i < len; i++) { const item = items[i]; const model = this.uiModelForAmfItem(item); if (model) { result[result.length] = model; } } return result; } _toBoolean(value) { if (typeof value === 'boolean') { return value } return value === 'true' } /** * Creates a UI model item from AMF json/ld model for a parameter. * @param {any} amfItem AMF model with schema for * `http://raml.org/vocabularies/http#Parameter` * @return {AmfFormItem} UI data model. */ _uiModelForParameter(amfItem) { amfItem = this._resolve(amfItem); const binding = /** @type string */ (this._getValue(amfItem, this.ns.aml.vocabularies.apiContract.binding)) const name = this._computeFormName(amfItem); const required = this._toBoolean(this._getValue(amfItem, this.ns.aml.vocabularies.apiContract.required)) const schemaItem = /** @type AmfFormItemSchema */ ({ required, }); const result = /** @type AmfFormItem */ ({ name, value: undefined, schema: schemaItem, enabled: true, }); schemaItem.isFile = false; // I am not sure why is this not computed here schemaItem.isUnion = false; // or this. schemaItem.readOnly = false; const sKey = this._getAmfKey(this.ns.aml.vocabularies.shapes.schema); let schema = amfItem[sKey]; if (schema) { if (Array.isArray(schema)) { [schema] = schema; } const def = this._resolve(schema); schemaItem.apiType = this._computeModelType(def); // Now check if there's cached model for this property // So far now it took only required steps to compute cache key of the // property. const cachedModel = getGlobalValue(binding, result); if (cachedModel) { // Safe to return it. return cachedModel; } const isEnum = this._hasProperty(def, this.ns.w3.shacl.in); schemaItem.inputLabel = this._computeInputLabel(def, schemaItem.required, result.name); schemaItem.pattern = this._computeShaclProperty(def, 'pattern'); schemaItem.minLength = this._computeShaclProperty(def, 'minLength'); schemaItem.maxLength = this._computeShaclProperty(def, 'maxLength'); schemaItem.defaultValue = this._computeDefaultValue(def); schemaItem.multipleOf = this._computeVocabularyShapeProperty(def, 'multipleOf'); schemaItem.minimum = this._computeShaclProperty(def, 'minInclusive'); schemaItem.maximum = this._computeShaclProperty(def, 'maxInclusive'); schemaItem.enum = isEnum ? this._computeModelEnum(def) : undefined; schemaItem.isArray = schemaItem.apiType === 'array'; schemaItem.isBool = schemaItem.apiType === 'boolean'; schemaItem.isObject = schemaItem.apiType === 'object'; schemaItem.examples = this._computeModelExamples(def); schemaItem.items = schemaItem.isArray ? this._computeModelItems(def) : undefined; schemaItem.inputType = this._computeModelInputType(schemaItem.apiType, schemaItem.items); schemaItem.format = this._computeVocabularyShapeProperty(schema, 'format'); schemaItem.pattern = this._computeModelPattern(schemaItem.apiType, schemaItem.pattern, schemaItem.format); schemaItem.isNillable = schemaItem.apiType === 'union' ? this._computeIsNillable(def) : false; schemaItem.noAutoEncode = this._hasNoAutoEncodeProperty(schema); } if (!this.noDocs) { schemaItem.description = this._computeDescription(amfItem); } if (schemaItem.isBool) { result.value = schemaItem.defaultValue } const valueDelimiter = this._computeValueDelimiter(binding); const decodeValues = this._computeDecodeValues(binding); const processOptions = { name: result.name, required: schemaItem.required, valueDelimiter, decodeValues, }; this._processAfterItemCreated(result, binding, processOptions); // store cache appendGlobalValue(binding, result); return result; } /** * Creates a UI model item from AMF json/ld model for a parameter. * @param {Object} amfItem AMF model with schema for * `http://raml.org/vocabularies/http#Parameter` * @return {AmfFormItem} UI data model. */ _uiModelForPropertyShape(amfItem) { amfItem = this._resolve(amfItem); const binding = 'type'; const name = this._computeShaclProperty(amfItem, 'name'); const schemaItem = /** @type AmfFormItemSchema */ ({ required: false, }); const result = /** @type AmfFormItem */ ({ name, value: undefined, schema: schemaItem, enabled: true, }); let def; if (this._hasType(amfItem, this.ns.aml.vocabularies.shapes.ScalarShape)) { def = amfItem; } else { const rangeKey = this._getAmfKey(this.ns.aml.vocabularies.shapes.range); def = amfItem[rangeKey]; if (!def) { return result; } if (Array.isArray(def)) { [def] = def; } def = this._resolve(def); } schemaItem.required = this._computeRequiredPropertyShape(amfItem); result.enabled = true; schemaItem.apiType = this._computeModelType(def); // Now check if there's cached model for this property // So far now it took only required steps to compute cache key of the // property. const cachedModel = getGlobalValue(binding, result); if (cachedModel) { // Safe to return it. return cachedModel; } schemaItem.inputLabel = this._computeInputLabel(def, schemaItem.required, result.name); schemaItem.pattern = this._computeShaclProperty(def, 'pattern'); schemaItem.minLength = this._computeShaclProperty(def, 'minLength'); schemaItem.maxLength = this._computeShaclProperty(def, 'maxLength'); schemaItem.defaultValue = this._computeDefaultValue(def); schemaItem.multipleOf = this._computeVocabularyShapeProperty(def, 'multipleOf'); schemaItem.minimum = this._computeShaclProperty(def, 'minInclusive'); schemaItem.maximum = this._computeShaclProperty(def, 'maxInclusive'); schemaItem.enum = this._computeModelEnum(def); schemaItem.isArray = schemaItem.apiType === 'array'; schemaItem.isObject = schemaItem.apiType === 'object'; schemaItem.isBool = schemaItem.apiType === 'boolean'; schemaItem.examples = this._computeModelExamples(def); schemaItem.inputType = this._computeModelInputType(schemaItem.apiType, schemaItem.items); schemaItem.format = this._computeVocabularyShapeProperty(def, 'format'); schemaItem.pattern = this._computeModelPattern( schemaItem.apiType, schemaItem.pattern, schemaItem.format); schemaItem.isNillable = schemaItem.apiType === 'union' ? this._computeIsNillable(result) : false; if (!this.noDocs) { schemaItem.description = this._computeDescription(def); } if (schemaItem.apiType === 'file') { schemaItem.isFile = true; schemaItem.fileTypes = /** @type string[] */ (this._getValueArray(def, this.ns.aml.vocabularies.shapes.fileType)); } else { schemaItem.isFile = false; } if (schemaItem.isObject) { const props = []; const pKey = this._getAmfKey(this.ns.w3.shacl.property); const items = this._ensureArray(def[pKey]); if (items) { items.forEach((item) => { if (Array.isArray(item)) { [item] = item; } props.push(this.uiModelForAmfItem(item)); }); } result.properties = props; } this._processAfterItemCreated(result, binding, {}); // Store cache appendGlobalValue(binding, result); return result; } /** * Creates a view model for an object definition. Object definition can be * part of trait or annotation properties description. * * @param {Object} model Model to extract data from. * @param {ProcessOptions=} processOptions * @return {AmfFormItem[]} View model for items. */ modelForRawObject(model, processOptions = {}) { const result = []; const keys = Object.keys(model); const dataKey = this._getAmfKey(this.ns.raml.vocabularies.data.toString()); keys.forEach((key) => { if (key.indexOf(dataKey) === -1) { return; } let item = model[key]; if (Array.isArray(item)) { [item] = item; } item = this._uiModelForRawObject(key, item); if (item) { item = this._processAfterItemCreated(item, 'type', processOptions); result.push(item); } }); return result; } /** * Creates a view model for union definition. * * @param {any} model Model to extract data from. * @return {AmfFormItem[]} View model for items. */ _modelForUnion(model) { let result; const resolvedModel = this._resolve(model); const key = this._getAmfKey(this.ns.aml.vocabularies.shapes.anyOf); const values = this._ensureArray(resolvedModel[key]); if (values) { result = []; values.forEach(v => { const pKey = this._getAmfKey(this.ns.w3.shacl.property); const items = this._ensureArray(v[pKey]); if (items) { items.forEach((item) => { if (Array.isArray(item)) { [item] = item; } result.push(this.uiModelForAmfItem(item)); }); } }) } return result; } /** * Creates a view model from "raw" item (model before resolving). * * @param {string} key Key of the item in the model. * @param {string} model Item model * @return {AmfFormItem} View model */ _uiModelForRawObject(key, model) { let index = key.indexOf('#'); if (index === -1) { index = key.indexOf(':'); } let name; if (index === -1) { name = key; } else { name = key.substr(index + 1); } // const binding = 'type'; const schemaItem = /** @type AmfFormItemSchema */ ({ required: false, }); const result = /** @type AmfFormItem */ ({ name, value: undefined, schema: schemaItem, enabled: true, }); const typeKey = this._getAmfKey(this.ns.raml.vocabularies.data.type); let type = /** @type String */ (this._computeRawModelValue(model[typeKey])); if (!type) { type = 'string'; } const bracesIndex = type.indexOf('[]'); let items; if (bracesIndex !== -1) { items = type.substr(0, bracesIndex); type = 'array'; } if (!this.noDocs) { const descKey = this._getAmfKey(this.ns.raml.vocabularies.data.description); schemaItem.description = /** @type String */ (this._computeRawModelValue(model[descKey])); } const requiredValue = model[this._getAmfKey(this.ns.raml.vocabularies.data.required)]; schemaItem.required = /** @type boolean */ (this._computeRawModelValue(requiredValue)); result.enabled = true; schemaItem.apiType = type || 'string'; const displayNameValue = model[this._getAmfKey(this.ns.raml.vocabularies.data.displayName)]; const displayName = /** @type string */ (this._computeRawModelValue(displayNameValue)); schemaItem.inputLabel = this._completeInputLabel(displayName, name, schemaItem.required); const mlKey = this._getAmfKey(this.ns.raml.vocabularies.data.minLength); schemaItem.minLength = /** @type number */ (this._computeRawModelValue(model[mlKey])); const mxKey = this._getAmfKey(this.ns.raml.vocabularies.data.maxLength); schemaItem.maxLength = /** @type number */ (this._computeRawModelValue(model[mxKey])); const dfKey = this._getAmfKey(this.ns.raml.vocabularies.data.default); schemaItem.defaultValue = this._computeRawModelValue(model[dfKey]); const mpKey = this._getAmfKey(this.ns.raml.vocabularies.data.multipleOf); schemaItem.multipleOf = /** @type number */ (this._computeRawModelValue(model[mpKey])); schemaItem.minimum = /** @type number */ (this._computeRawModelValue(model[this._getAmfKey(this.ns.raml.vocabularies.data.minimum)])); schemaItem.maximum = /** @type number */ (this._computeRawModelValue(model[this._getAmfKey(this.ns.raml.vocabularies.data.maximum)])); schemaItem.enum = /** @type any[] */ (this._computeRawModelValue(model[this._getAmfKey(this.ns.raml.vocabularies.data.enum)])); let pattern = this._computeRawModelValue(model[this._getAmfKey(this.ns.raml.vocabularies.data.pattern)]); if (Array.isArray(pattern)) { pattern = `[${pattern[0]}]`; } schemaItem.pattern = /** @type string */ (pattern); schemaItem.isArray = schemaItem.apiType === 'array'; schemaItem.isBool = schemaItem.apiType === 'boolean'; // schemaItem.examples = this._computeModelExamples(def); if (schemaItem.isArray) { schemaItem.items = items || /** @type string[] */ (this._computeRawModelValue(model[this._getAmfKey(this.ns.raml.vocabularies.data.items)])); } schemaItem.inputType = this._computeModelInputType(schemaItem.apiType, schemaItem.items); schemaItem.format = /** @type string */ (this._computeRawModelValue(model[this._getAmfKey(this.ns.raml.vocabularies.data.format)])); schemaItem.pattern = this._computeModelPattern(schemaItem.apiType, schemaItem.pattern, schemaItem.format); const example = /** @type any */ (this._computeRawModelValue(model[this._getAmfKey(this.ns.raml.vocabularies.data.example)])); if (example) { schemaItem.examples = [example]; } const examples = this._computeRawExamples(model[this._getAmfKey(this.ns.raml.vocabularies.data.examples)]); if (examples) { const existing = schemaItem.examples || []; schemaItem.examples = existing.concat(examples); } return result; } /** * Sets up additional properties like `value` or placeholder from * values read from the AMF model. * * @param {AmfFormItem} item Computed UI model. * @param {string} binding * @param {ProcessOptions} processOptions Model creation options * @return {AmfFormItem} */ _processAfterItemCreated(item, binding, processOptions) { if (item.schema.apiType === 'null') { switch (binding) { case 'header': case 'query': case 'path': item.value = 'nil'; break; default: item.value = 'null'; } item.schema.readOnly = true; } if (item.schema.examples && item.schema.examples.length && item.schema.examples[0].value) { item.schema.inputPlaceholder = `Example: ${this._exampleAsValue(item.schema.examples[0].value, processOptions)}`; } if (!item.schema.inputPlaceholder) { item.schema.inputPlaceholder = this._computeTypePlaceholder(item.schema.apiType, item.schema.format); } if (item.schema.required && typeof item.schema.defaultValue !== 'undefined') { item.value = item.schema.isArray ? this._parseArrayExample(/** @type {string} */(item.schema.defaultValue), processOptions) : this._exampleAsValue(/** @type {string} */(item.schema.defaultValue), processOptions); } if (typeof item.value === 'undefined' && item.schema.required) { const { examples } = item.schema; if (examples && examples.length) { item.value = this._exampleAsValue(examples[0].value, processOptions); } if ((typeof item.value === 'undefined' || item.value === '') && Array.isArray(item.schema.enum)) { item.value = this._exampleAsValue(item.schema.enum[0], processOptions); } } if (Array.isArray(item.schema.enum) && item.schema.examples && item.schema.examples.length === 1 && !item.schema.examples[0].value) { delete item.schema.examples; } if (item.value && item.schema.isArray && typeof item.value === 'string') { const _v = this._parseArrayExample(item.value, processOptions); item.value = _v instanceof Array ? _v : [_v]; } if (item.schema.isArray && !item.value) { item.value = ['']; } if (item.schema.isBool && typeof item.value === 'boolean') { item.value = String(item.value); } item.schema.extendedDescription = this._computeExtendedDocumentation(item); return item; } /** * Completes computation of input label. * * @param {string} displayName Value of the `displayName` property * @param {string} name Property name * @param {boolean} required Is item required * @return {string} Common input label construction. */ _completeInputLabel(displayName, name, required) { if (!displayName) { displayName = name || 'Input value'; } if (required) { displayName += '*'; } return displayName; } /** * Computes list of examples from the Raw data model. * @param {any} model * @return {Example[]|undefined} */ _computeRawExamples(model) { if (!model || !model[0]) { return undefined; } const result = []; [model] = model; const keys = Object.keys(model); keys.forEach((key) => { const dKey = this._getAmfKey(this.ns.raml.vocabularies.data.toString()); if (key.indexOf(dKey) === -1) { return; } const symbol = key.indexOf('#') !== -1 ? '#' : ':'; const name = key.split(symbol)[1]; const value = this._computeRawModelValue(model[key]); if (value) { result.push({ name, value, hasTitle: !!name }); } }); return result; } /** * Computes form (parameter) name from AMF model. * @param {any} model AMF item model * @return {string|undefined} Name property or undefined if not found. */ _computeFormName(model) { const pNameKey = this.ns.aml.vocabularies.apiContract.paramName; let name = this._getValue(model, pNameKey); if (!name) { const key = this.ns.aml.vocabularies.core.name; name = this._getValue(model, key); } return /** @type string */ (name); } /** * Computes `minCount` property from AMF model for PropertyShape object. * * @param {any} model AMF item model * @return {boolean} True if `minCount` equals `1` */ _computeRequiredPropertyShape(model) { const key = this.ns.w3.shacl.minCount; const result = this._getValue(model, key); return Number(result) === 1; } /** * Computes type of the model. It's RAML data type property. * @param {any} shape Property schema. * @return {String} Type of the property. */ _computeModelType(shape) { if (!shape) { return undefined; } const vsh = this.ns.aml.vocabularies.shapes; const sa = this.ns.w3.shacl; if (this._hasType(shape, vsh.UnionShape)) { return 'union'; } if (this._hasType(shape, vsh.ArrayShape)) { return 'array'; } if (this._hasType(shape, sa.NodeShape)) { return 'object'; } if (this._hasType(shape, sa.PropertyShape)) { return 'object'; } if (this._hasType(shape, vsh.FileShape)) { return 'file'; } if (this._hasType(shape, vsh.NilShape)) { return 'null'; } // Apparently version 2 of the model has AnyShape type with ScalarShape. // if (this._hasType(shape, vsh.AnyShape)) { // return 'string'; // } if (this._hasType(shape, vsh.MatrixShape)) { return 'array'; } if (this._hasType(shape, vsh.TupleShape)) { return 'object'; } if (this._hasType(shape, vsh.ScalarShape)) { let dt = shape[this._getAmfKey(sa.datatype)]; if (Array.isArray(dt)) { [dt] = dt; } let id = dt; if (typeof id !== 'string') { id = id['@id']; } const x = this.ns.w3.xmlSchema; switch (id) { case x.string: case this._getAmfKey(x.string): return 'string'; case x.integer: case this._getAmfKey(x.integer): return 'integer'; case x.long: case this._getAmfKey(x.long): return 'long'; case x.float: case this._getAmfKey(x.float): return 'float'; case x.double: case this._getAmfKey(x.double): return 'double'; case vsh.number: case this._getAmfKey(vsh.number): return 'number'; case x.boolean: case this._getAmfKey(x.boolean): return 'boolean'; case x.dateTime: case this._getAmfKey(x.dateTime): return 'datetime'; case vsh.dateTimeOnly: case this._getAmfKey(vsh.dateTimeOnly): return 'datetime-only'; case x.time: case this._getAmfKey(x.time): return 'time'; case x.date: case this._getAmfKey(x.date): return 'date'; case x.base64Binary: case this._getAmfKey(x.base64Binary): return 'string'; case vsh.password: case this._getAmfKey(vsh.password): return 'password'; default: } } return 'string'; } /** * Computes type of the raw model. * * @param {any} model Property schema. * @return {string|number|boolean|object[]|null|undefined} Type of the property. */ _computeRawModelValue(model) { if (!model) { return undefined; } if (Array.isArray(model)) { [model] = model; } let dataType = model['@type']; if (Array.isArray(dataType)) { [dataType] = dataType; } switch (dataType) { case this._getAmfKey(this.ns.raml.vocabularies.data.Scalar): return this._computeRawScalarValue(model); case this._getAmfKey(this.ns.raml.vocabularies.data.Array): return this._computeRawArrayValue(model); case this._getAmfKey(this.ns.aml.vocabularies.shapes.FileShape): return this._getValueArray(model, this.ns.aml.vocabularies.shapes.fileType); default: return undefined; } } /** * Computes scalar value that has proper type. * @param {any} item Shape to test for a value. * @return {string|number|boolean} */ _computeRawScalarValue(item) { const value = this._getValue(item, this.ns.raml.vocabularies.data.value); const dtKey = this._getAmfKey(this.ns.w3.shacl.datatype); let type = item[dtKey]; if (Array.isArray(type)) { [type] = type; } type = type['@id']; const s = this.ns.w3.xmlSchema; switch (type) { case this._getAmfKey(s.number): case this._getAmfKey(s.long): case this._getAmfKey(s.integer): case this._getAmfKey(s.float): case this._getAmfKey(s.double): return Number(value); case this._getAmfKey(s.boolean): return value !== 'false'; default: return value; } } /** * @param {any} item Array property schema * @return {any[]|undefined} Array values. */ _computeRawArrayValue(item) { const key = this._getAmfKey(this.ns.w3.rdfSchema.member); const values = this._ensureArray(item[key]); if (!values) { return undefined; } const result = []; values.forEach((value) => { const res = this._computeRawScalarValue(value); if (res) { result.push(res); } }); return result; } /** * Computes form input label value. * * @param {any} def Property definition * @param {boolean=} required True if the property is required * @param {string=} name Property name * @return {string} Input display name. */ _computeInputLabel(def, required, name) { const result = /** @type string */ (this._getValue(def, this.ns.aml.vocabularies.core.name)); return this._completeInputLabel(result, name, required); } /** * Computes the value of a property that namespace starts with * `http://www.w3.org/ns/shacl`. * * @param {any} shape Property AMF definition * @param {string} property Name of the schema. * @return {any|undefined} Value of the property or undefined if not set. */ _computeShaclProperty(shape, property) { const key = this.ns.w3.shacl.key + String(property); return this._getValue(shape, key); } /** * Computes the value of a property that namespace starts with * `http://raml.org/vocabularies/shapes`. * * @param {any} shape Property AMF definition * @param {string} property Name of the schema. * @return {any|undefined} Value of the property or undefined if not set. */ _computeVocabularyShapeProperty(shape, property) { const key = this.ns.aml.vocabularies.shapes + String(property); return this._getValue(shape, key); } /** * Computes default value for a shape. * @param {any} shape Amf shape * @return {any|undefined} Default value for the model or undefined. */ _computeDefaultValue(shape) { const valueKey = this._getAmfKey(this.ns.w3.shacl.defaultValueStr); let value = shape[valueKey]; if (!value) { return undefined; } if (Array.isArray(value)) { [value] = value; } if (value['@value']) { value = value['@value']; } if (this._hasType(shape, this.ns.aml.vocabularies.shapes.ScalarShape)) { const dtKey = this._getAmfKey(this.ns.w3.shacl.datatype); let type = shape[dtKey]; if (Array.isArray(type)) { [type] = type; } type = type['@id']; const s = this.ns.w3.xmlSchema; switch (type) { case s.number: case s.long: case s.integer: case s.float: case s.double: case this._getAmfKey(s.number): case this._getAmfKey(s.long): case this._getAmfKey(s.integer): case this._getAmfKey(s.float): case this._getAmfKey(s.double): return Number(value); case s.boolean: case this._getAmfKey(s.boolean): return value !== 'false'; default: return value; } } else if (this._hasType(shape, this.ns.aml.vocabularies.shapes.ArrayShape)) { const vKey = this._getAmfKey(this.ns.w3.shacl.defaultValue); let value2 = shape[vKey]; if (!value2) { return value; } if (Array.isArray(value2)) { [value2] = value2; } return this._computeRawArrayValue(value2); } return value; } /** * Computes enum values for the view model. * @param {any} def Model definition. * @return {any[]} List of values. */ _computeModelEnum(def) { def = this._resolve(def); const inKey = this._getAmfKey(this.ns.w3.shacl.in); let model = def[inKey]; if (!model) { return undefined; } if (Array.isArray(model)) { [model] = model; } const result = []; const rdfKey = this._getAmfKey(this.ns.w3.rdfSchema.key); Object.keys(model).forEach((key) => { if (key.indexOf(rdfKey) === -1) { return; } let shape = model[key]; if (Array.isArray(shape)) { [shape] = shape; } const vKey = this._getAmfKey(this.ns.raml.vocabularies.data.value); const value = this._getValue(shape, vKey); if (value) { result.push(value); } }); return result; } /** * Computes list of examples for the Property model. * * @param {any} model AMF property model * @return {Example[]|undefined} List of examples or `undefined` if not defined. */ _computeModelExamples(model) { const gen = new ExampleGenerator(this.amf); return gen.computeExamples(model, 'application/json', {}); } /** * Computes `items` property for AMF array property * * @param {any} model AMF property model * @return {string} Type of an item */ _computeModelItems(model) { if (!this._hasType(model, this.ns.aml.vocabularies.shapes.ArrayShape)) { return undefined; } model = this._resolve(model); const itKeys = this._getAmfKey(this.ns.aml.vocabularies.shapes.items); let item = model[itKeys]; if (!item) { return undefined; } if (Array.isArray(item)) { [item] = item; } const type = this._computeModelType(item); // _computeModelType() always returns a value // TODO: add support for objects and unions. return type; } /** * Computes value delimiter for processing options. * @param {string} binding Property's binding * @return {string} A `:` for headers, `=` for query param, and empty for other. */ _computeValueDelimiter(binding) { switch (binding) { case 'header': return ':'; case 'query': return '='; default: return ''; } } /** * Computes value for decodeValues of ProcessingOptions. * @param {string} binding Property's binding * @return {boolean} True whe values should be encoded. */ _computeDecodeValues(binding) { switch (binding) { case 'query': return true; default: return false; } } /** * Parses a string from example or enum value to be used as a default value. * @param {string | number | boolean | object | any[]} example Example value to process as a value * @param {ProcessOptions} opts * * @return {string} */ _exampleAsValue(example, opts) { if (!example || typeof example !== 'string') { return example; } example = example.trim(); if (opts.valueDelimiter && example.indexOf(opts.name + opts.valueDelimiter) === 0) { example = example.substr(opts.name.length + 1).trim(); } if (opts.decodeValues) { try { example = decodeURIComponent(example.replace(/\+/g, ' ')); } catch (e) { // ... } } return example; } /** * Parses example in an array type. * * @param {string} example An array example * @param {ProcessOptions} processOptions * @return {string[]|string} Array of examples or string if cannot parse */ _parseArrayExample(example, processOptions) { try { const arr = JSON.parse(example); if (arr instanceof Array) { const result = []; arr.forEach((item) => { const ex = this._exampleAsValue(item, processOptions); if (ex) { result[result.length] = ex; } }); return result.length ? result : undefined; } } catch (e) { // ... } return this._exampleAsValue(example, processOptions); } /** * Computes rendered item input field type based on RAML definition. * * It will be either numeric or text. Type will be determined from * item's type or, in case of array, item's items property. * * @param {string} type Property data type. * @param {AmfFormItem|Object|string} items Array items if any * @return {"number" | "boolean" | "date" | "text"} Input field type. */ _computeModelInputType(type, items) { if (type === 'array') { if (typeof items === 'string') { return this._computeInputType(items); } if (!items) { return 'text'; } return this._computeInputType(items.schema ? items.schema.apiType : items.apiType); } return this._computeInputType(type); } /** * Computes the type attribute value for a text input for given type. * * @param {string} type One of the schema types * @return {"number" | "boolean" | "date" | "text"} Value for the text input type. */ _computeInputType(type) { if (type && NUMBER_INPUT_TYPES.indexOf(type) !== -1) { return 'number'; } if (type === 'boolean') { return 'boolean'; } if (type === 'date-only' || type === 'date') { return 'date'; } /* else if (type === 'time-only' || type === 'time') { Time input does not work well in the console. It's better to use regular input. Unless someone create custom time input. return 'time'; } */ return 'text'; } /** * Computes pattern for the input. * * @param {string} modelType Type of the property item. * @param {string=} pattern Pattern declared on the property * @param {string=} format For `datetime` type additional format value. * `rfc3339` is assumed by default * @return {string|undefined} Pattern or undefined if does not exists. */ _computeModelPattern(modelType, pattern, format) { if (!pattern) { switch (modelType) { case 'time': pattern = '^[0-9]{2}:[0-9]{2}:[0-9]{2}\\.?[0-9]{0,3}$'; break; case 'date': pattern = '^[0-9]{4}-[0-9]{2}-[0-9]{2}$'; break; case 'datetime-only': pattern = '^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\\.?[0-9]{0,3}$'; break; case 'datetime': if (format === 'rfc2616') { pattern = ''; } else { pattern = '^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}.*$'; } break; default: } } return pattern; } /** * Computes a placeholder value for data and time inputs. * * @param {string} type Model type. * @param {string=} format For `datetime` type additional format value.`rfc3339` is assumed by default * @return {string|undefined} Placeholder value. */ _computeTypePlaceholder(type, format) { let value; switch (type) { case 'time': value = '00:00:00.000'; break; case 'date': value = '0000-00-00'; break; case 'datetime-only': value = '0000-00-00T00:00:00.000'; break; case 'datetime': if (format === 'rfc2616') { value = 'Sun, 01 Jan 2000 00:00:00 GMT'; } else { value = '0000-00-00T00:00:00Z+01:00'; } break; default: } return value; } /** * Builds an empty view model without traversing AMF model. * * @param {AmfFormItem=} defaults View model with default values. This values won't be set. * @param {string=} binding * @return {AmfFormItem} Generated basic view model. */ buildProperty(defaults = { name: undefined, value: undefined, enabled: true, }, binding) { defaults.schema = defaults.schema || {}; defaults.schema.apiType = defaults.schema.apiType || 'string'; defaults.schema.isArray = defaults.schema.isArray || false; defaults.schema.isBool = defaults.schema.isBool || false; defaults.schema.isFile = defaults.schema.apiType === 'file'; defaults.schema.inputType = defaults.schema.inputType || 'text'; defaults.schema.inputType = this._computeModelInputType(defaults.schema.apiType, defaults.schema.items); defaults.schema.pattern = this._computeModelPattern(defaults.schema.apiType, defaults.schema.pattern); if (!defaults.schema.inputLabel) { defaults.schema.inputLabel = defaults.name || 'Parameter value'; } this._processAfterItemCreated(defaults, binding, {}); return defaults; } /** * Computes documentation as a markdown to be placed in the `marked-element` * @param {AmfFormItem} item View model * @returns {string} Generated documentation */ _computeExtendedDocumentation(item) { let docs = ''; if (item.schema.description) { docs += item.schema.description; } const { schema } = item; const items = []; if (schema.pattern) { items[items.length] = `- Pattern: \`${schema.pattern}\``; } if (schema.examples && schema.examples.length) { schema.examples.forEach((example) => { if (example.value === undefined || example.value === '') { return; } let result = '- Example'; if (example.title) { result += ` ${example.title}`; } let value; if (Array.isArray(example.value)) { value = example.value.join(', '); } else { value = example.value; } result += `: \`${value}\``; items[items.length] = result; }); } if (docs && items.length) { docs += '\n\n\n'; } return docs + items.join('\n'); } /** * Returns `true` only when passed shape has `shapes#anyOf` array and * one of the union properties is of a type od NilShape. * @param {Object} shape Shape test for a nillable union. * @return {boolean} */ _computeIsNillable(shape) { if (!shape) { return false; } const key = this._getAmfKey(this.ns.aml.vocabularies.shapes.anyOf); const values = this._ensureArray(shape[key]); if (!values) { return false; } for (let i = 0, len = values.length; i < len; i++) { if (this._hasType(values[i], this.ns.aml.vocabularies.shapes.NilShape)) { return true; } } return false; } /** * Checks if given property has `no-auto-encoding` annotation. * * @param {object} shape An object to test for the annotation. * @return {boolean} True if the annotation is set. */ _hasNoAutoEncodeProperty(shape) { if (!shape) { return false; } const key = this._getAmfKey(this.ns.aml.vocabularies.document.customDomainProperties); const values = this._ensureArray(shape[key]); if (!values) { return false; } for (let i = 0, len = values.length; i < len; i++) { const id = this._ensureAmfPrefix(/** @type string */(this._getValue(values[i], '@id'))); const node = shape[id]; const extensionNameKey = this._getAmfKey(this.ns.aml.vocabularies.core.extensionName); if (this._getValue(node, extensionNameKey) === 'no-auto-encoding') { return true; } } return false; } /** * * @param {string} id * @return {string} */ _ensureAmfPrefix(id) { if (!id.startsWith('amf://id')) { return `amf://id${id}`; } return id; } }