diff --git a/src/cli.mjs b/src/cli.mjs index f2a5e113..714e7a37 100755 --- a/src/cli.mjs +++ b/src/cli.mjs @@ -22,6 +22,7 @@ const knownOpts = { 'language': [path], 'examples': [path, Array], 'as-path': [Boolean], + 'bidirectional': [Boolean], 'pass-throughs': [Boolean] } diff --git a/src/openrpc/index.mjs b/src/openrpc/index.mjs index 806f6198..a007aca4 100644 --- a/src/openrpc/index.mjs +++ b/src/openrpc/index.mjs @@ -134,7 +134,7 @@ const run = async ({ // Add schemas mergedOpenRpc.components && Object.assign(platformApiOpenRpc.components.schemas, mergedOpenRpc.components.schemas) - appApiOpenRpc.components && Object.assign(appApiOpenRpc.components.schemas, mergedOpenRpc.components.schemas) + appApiOpenRpc?.components && Object.assign(appApiOpenRpc.components.schemas, mergedOpenRpc.components.schemas) // Add externally referenced schemas that are in our shared schemas path platformApiOpenRpc = addExternalSchemas(platformApiOpenRpc, sharedSchemas) diff --git a/src/shared/json-schema.mjs b/src/shared/json-schema.mjs index 0a3843a5..5293cfbe 100644 --- a/src/shared/json-schema.mjs +++ b/src/shared/json-schema.mjs @@ -24,7 +24,29 @@ const isNull = schema => { return (schema.type === 'null' || schema.const === null) } -const isSchema = element => element.$ref || element.type || element.const || element.oneOf || element.anyOf || element.allOf +const isSchema = element => element.$ref || element.type || element.const || element.oneOf || element.anyOf || element.allOf || element.$id + +const pathToArray = (ref, json) => { + //let path = ref.split('#').pop().substr(1).split('/') + + const ids = [] + if (json) { + ids.push(...getAllValuesForName("$id", json)) // add all $ids but the first one + } + + const subschema = ids.find(id => ref.indexOf(id) >= 0) + + let path = ref.split('#').pop().substring(1) + + if (subschema) { + path = [].concat(...path.split('/'+subschema+'/').map(n => [n.split('/'), subschema])).slice(0, -1).flat() + } + else { + path = path.split('/') + } + + return path.map(x => x.match(/^[0-9]+$/) ? parseInt(x) : x) +} const refToPath = ref => { let path = ref.split('#').pop().substr(1).split('/') @@ -36,6 +58,9 @@ const objectPaths = obj => { const addDelimiter = (a, b) => a ? `${a}/${b}` : b; const paths = (obj = {}, head = '#') => { + if (obj && isObject(obj) && obj.$id && head !== '#') { + head = obj.$id + } return obj ? Object.entries(obj) .reduce((product, [key, value]) => { let fullPath = addDelimiter(head, key) @@ -47,17 +72,37 @@ const objectPaths = obj => { return paths(obj); } +const getAllValuesForName = (name, obj) => { + const isObject = val => typeof val === 'object' + + const values = (name, obj = {}) => { + return obj ? Object.entries(obj) + .reduce((product, [key, value]) => { + if (isObject(value)) { + return product.concat(values(name, value)) + } + else if (key === name) { + return product.concat(value) + } + else { + return product + } + }, []) : [] + } + return [...new Set(values(name, obj))]; +} + const getExternalSchemaPaths = obj => { return objectPaths(obj) .filter(x => /\/\$ref$/.test(x)) - .map(refToPath) + .map(x => pathToArray(x, obj)) .filter(x => !/^#/.test(getPathOr(null, x, obj))) } const getLocalSchemaPaths = obj => { return objectPaths(obj) .filter(x => /\/\$ref$/.test(x)) - .map(refToPath) + .map(x => pathToArray(x, obj)) .filter(x => /^#.+/.test(getPathOr(null, x, obj))) } @@ -456,11 +501,16 @@ const getLocalSchemas = (json = {}) => { } const isDefinitionReferencedBySchema = (name = '', moduleJson = {}) => { + let subSchema = false + if (name.indexOf("/https://") >= 0) { + name = name.substring(name.indexOf('/https://')+1) + subSchema = true + } const refs = objectPaths(moduleJson) .filter(x => /\/\$ref$/.test(x)) - .map(refToPath) + .map(x => pathToArray(x, moduleJson)) .map(x => getPathOr(null, x, moduleJson)) - .filter(x => x === name) + .filter(x => subSchema ? x.startsWith(name) : x === name) return (refs.length > 0) } @@ -622,4 +672,5 @@ export { dereferenceAndMergeAllOfs, flattenMultipleOfs, namespaceRefs, + getAllValuesForName, } diff --git a/src/shared/methods.mjs b/src/shared/methods.mjs new file mode 100644 index 00000000..d99d6c47 --- /dev/null +++ b/src/shared/methods.mjs @@ -0,0 +1,39 @@ +/* + * Copyright 2021 Comcast Cable Communications Management, LLC + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +const tag = (method, name) => method.tags.find(tag => tag.name === name) +export const extension = (method, name) => (method.tags.find(t => t[name]) || {})[name] + +export const capabilities = method => tag(method, 'capabilities') +export const isProvider = method => capabilities(method)['x-provides'] +export const isPusher = method => capabilities(method)['x-push'] +export const isNotifier = method => method.tags.find(t => t.name === 'notifier') +export const isEvent = method => tag(method, 'event') +export const isRegistration = method => tag(method, 'registration') +export const isProviderInterface = method => isProvider(method) && !isRegistration(method) && !isPusher(method) + +export const name = method => method.name.split('.').pop() +export const rename = (method, renamer) => method.name.split('.').map((x, i, arr) => i === (arr.length-1) ? renamer(x) : x).join('.') + +export const getNotifier = (method, client) => client.methods.find(m => m.name === method.tags.find(t => t.name === "event")['x-notifier']) +export const getEvent = (method, server) => server.methods.find(m => m.name === method.tags.find(t => t.name === "notifier")['x-event']) +export const getCapability = (method) => Object.values(capabilities(method)).map(v => Array.isArray(v) ? v.shift() : v).filter(v => typeof v === 'string').find(x => (x.startsWith('xrn:firebolt:capability'))) +export const getRole = (method) => Object.keys(capabilities(method)).find(x => ['x-uses', 'x-provides', 'x-manages'].includes(x)) + +export const provides = (method) => extension(method, 'x-provides') + diff --git a/src/shared/modules.mjs b/src/shared/modules.mjs index 7418b346..b3abf7e5 100644 --- a/src/shared/modules.mjs +++ b/src/shared/modules.mjs @@ -24,12 +24,11 @@ import getPath from 'crocks/Maybe/getPath.js' import pointfree from 'crocks/pointfree/index.js' const { chain, filter, option, map } = pointfree import logic from 'crocks/logic/index.js' -import isEmpty from 'crocks/core/isEmpty.js' const { and, not } = logic import isString from 'crocks/core/isString.js' import predicates from 'crocks/predicates/index.js' -import { getExternalSchemaPaths, isDefinitionReferencedBySchema, isNull, localizeDependencies, isSchema, getLocalSchemaPaths, replaceRef, getPropertySchema, dereferenceAndMergeAllOfs } from './json-schema.mjs' -import { getPath as getRefDefinition } from './json-schema.mjs' +import { getExternalSchemaPaths, isDefinitionReferencedBySchema, isNull, localizeDependencies, isSchema, getLocalSchemaPaths, replaceRef, getPropertySchema, dereferenceAndMergeAllOfs, getPath as getRefDefinition, getAllValuesForName, replaceUri } from './json-schema.mjs' +import { extension, getNotifier, isEvent, isNotifier, isPusher, isRegistration, name as methodName, rename as methodRename, provides } from './methods.mjs' const { isObject, isArray, propEq, pathSatisfies, hasProp, propSatisfies } = predicates // TODO remove these when major/rpc branch is merged @@ -59,26 +58,14 @@ const getMethods = compose( getPath(['methods']) ) -const isProviderInterfaceMethod = compose( - and( - compose( - propSatisfies('name', name => name.startsWith('onRequest')) - ), - compose( - option(false), - map(_ => true), - chain( - find( - and( - propEq('name', 'capabilities'), - propSatisfies('x-provides', not(isEmpty)) - ) - ) - ), - getPath(['tags']) - ) - ) - ) +const isProviderInterfaceMethod = method => { + let tag = method.tags.find(t => t.name === 'capabilities') + const isProvider = tag['x-provides'] && !tag['x-allow-focus-for'] && !tag['x-response-for'] && !tag['x-error-for'] && !tag['x-push'] && !method.tags.find(t => t.name === 'registration') + + tag = method.tags.find(t => t.name.startsWith('polymorphic-pull')) + const isPuller = !!tag + return isProvider && !isPuller //(!method.tags.find(t => t.name.startsWith('polymorphic-pull'))) +} const getProvidedCapabilities = (json) => { return Array.from(new Set([...getMethods(json).filter(isProviderInterfaceMethod).map(method => method.tags.find(tag => tag['x-provides'])['x-provides'])])) @@ -88,7 +75,16 @@ const getProviderInterfaceMethods = (capability, json) => { return getMethods(json).filter(method => method.name.startsWith("onRequest") && method.tags && method.tags.find(tag => tag['x-provides'] === capability)) } - +const getInterfaces = (json) => { + const list = Array.from(new Set((json.methods || []).filter(m => m.tags.find(t => t['x-provides'])) + .filter(m => !m.tags.find(t => t.name.startsWith('registration'))) + .filter(m => !m.tags.find(t => t.name.startsWith('polymorphic-pull'))) + .filter(m => !extension(m, 'x-push')) + .map(m => m.name.split('.')[0]))) + + return list +} + function getProviderInterface(capability, module, extractProviderSchema = false) { module = JSON.parse(JSON.stringify(module)) const iface = getProviderInterfaceMethods(capability, module).map(method => dereferenceAndMergeAllOfs(method, module)) @@ -381,43 +377,43 @@ const eventDefaults = event => { event.tags = [ { - 'name': 'event' + 'name': 'notifier' } ] return event } -const createEventResultSchemaFromProperty = (property, type='') => { - const subscriberType = property.tags.map(t => t['x-subscriber-type']).find(t => typeof t === 'string') || 'context' +const createEventResultSchemaFromProperty = (property, type='Changed') => { + const subscriberType = property.tags.map(t => t['x-subscriber-type']).find(t => typeof t === 'string') || 'context' - const caps = property.tags.find(t => t.name === 'capabilities') - let name = caps['x-provided-by'] ? caps['x-provided-by'].split('.').pop().replace('onRequest', '') : property.name - name = name.charAt(0).toUpperCase() + name.substring(1) + const caps = property.tags.find(t => t.name === 'capabilities') + let name = caps['x-provided-by'] ? caps['x-provided-by'].split('.').pop().replace('onRequest', '') : property.name + name = name.charAt(0).toUpperCase() + name.substring(1) - if ( subscriberType === 'global') { - // wrap the existing result and the params in a new result object - const schema = { - title: name + type + 'Info', - type: "object", - properties: { + if ( subscriberType === 'global') { + // wrap the existing result and the params in a new result object + const schema = { + title: methodRename(property, name => name.charAt(0).toUpperCase() + name.substring(1) + type + 'Info').split('.').pop(), + type: "object", + properties: { - }, - required: [] - } + }, + required: [] + } - // add all of the params - property.params.filter(p => p.name !== 'listen').forEach(p => { - schema.properties[p.name] = p.schema - schema.required.push(p.name) - }) + // add all of the params + property.params.filter(p => p.name !== 'listen').forEach(p => { + schema.properties[p.name] = p.schema + schema.required.push(p.name) + }) - // add the result (which might override a param of the same name) - schema.properties[property.result.name] = property.result.schema - !schema.required.includes(property.result.name) && schema.required.push(property.result.name) + // add the result (which might override a param of the same name) + schema.properties[property.result.name] = property.result.schema + !schema.required.includes(property.result.name) && schema.required.push(property.result.name) - return schema - } + return schema + } } const createEventFromProperty = (property, type='', alternative, json) => { @@ -480,6 +476,55 @@ const createEventFromProperty = (property, type='', alternative, json) => { return event } +const createNotifierFromProperty = (property, type='Changed') => { + const subscriberType = property.tags.map(t => t['x-subscriber-type']).find(t => typeof t === 'string') || 'context' + + const notifier = JSON.parse(JSON.stringify(property)) + notifier.name = methodRename(notifier, name => name + type) + + Object.assign(notifier.tags.find(t => t.name.startsWith('property')), { + name: 'notifier', + 'x-notifier-for': property.name, + 'x-event': methodRename(notifier, name => 'on' + name.charAt(0).toUpperCase() + name.substring(1)) + }) + + if (subscriberType === 'global') { + notifier.params = [ + { + name: "info", + schema: { + "$ref": "#/components/schemas/" + methodRename(notifier, name => name.charAt(0).toUpperCase() + name.substr(1) + 'Info') + } + } + ] + } + else { + notifier.params.push(notifier.result) + } + + delete notifier.result + + if (subscriberType === 'global') { + notifier.examples = property.examples.map(example => ({ + name: example.name, + params: [ + { + name: "info", + value: Object.assign(Object.fromEntries(example.params.map(p => [p.name, p.value])), Object.fromEntries([[example.result.name, example.result.value]])) + } + ] + })) + } + else { + notifier.examples.forEach(example => { + example.params.push(example.result) + delete example.result + }) + } + + return notifier +} + // create foo() notifier from onFoo() event const createNotifierFromEvent = (event, json) => { const push = JSON.parse(JSON.stringify(event)) @@ -518,103 +563,68 @@ const createPushEvent = (requestor, json) => { } const createPullEventFromPush = (pusher, json) => { - const event = eventDefaults(JSON.parse(JSON.stringify(pusher))) - event.params = [] - event.name = 'onPull' + event.name.charAt(0).toUpperCase() + event.name.substr(1) - const old_tags = JSON.parse(JSON.stringify(pusher.tags)) - - event.tags[0]['x-pulls-for'] = pusher.name - event.tags.unshift({ - name: 'polymorphic-pull-event' - }) + const event = JSON.parse(JSON.stringify(pusher)) + event.params = [] + event.name = methodRename(event, name => 'pull' + name.charAt(0).toUpperCase() + name.substr(1)) + const old_tags = pusher.tags.concat() + event.tags = [ + { + name: "notifier", + 'x-event': methodRename(pusher, name => 'onPull' + name.charAt(0).toUpperCase() + name.substr(1)) + } + ] + + event.tags[0]['x-pulls-for'] = pusher.name + event.tags.unshift({ + name: 'polymorphic-pull-event' + }) + + const requestType = methodRename(pusher, name => name.charAt(0).toUpperCase() + name.substr(1) + "FederatedRequest") + event.params.push({ + name: "request", + summary: "A " + requestType + " object.", + schema: { + "$ref": "#/components/schemas/" + requestType + } + }) - const requestType = (pusher.name.charAt(0).toUpperCase() + pusher.name.substr(1)) + "FederatedRequest" - event.result.name = "request" - event.result.summary = "A " + requestType + " object." + delete event.result - event.result.schema = { - "$ref": "#/components/schemas/" + requestType - } - - const exampleResult = { - name: "result", - value: JSON.parse(JSON.stringify(getPathOr(null, ['components', 'schemas', requestType, 'examples', 0], json))) - } + const exampleResult = { + name: "request", + value: JSON.parse(JSON.stringify(getPathOr(null, ['components', 'schemas', requestType, 'examples', 0], json))) + } - event.examples && event.examples.forEach(example => { - example.result = exampleResult - example.params = [] - }) + event.examples && event.examples.forEach(example => { + delete example.result + example.params = [ + exampleResult + ] + }) - old_tags.forEach(t => { - if (t.name !== 'polymorphic-pull' && t.name) - { - event.tags.push(t) - } - }) + old_tags.forEach(t => { + if (t.name !== 'polymorphic-pull' && t.name) + { + event.tags.push(t) + } + }) - return event + return event } -const createPullProvider = (requestor, params) => { - const event = eventDefaults(JSON.parse(JSON.stringify(requestor))) - event.name = requestor.tags.find(t => t['x-provided-by'])['x-provided-by'] - const old_tags = JSON.parse(JSON.stringify(requestor.tags)) - - const value = event.result - - event.tags[0]['x-response'] = value.schema - event.tags[0]['x-response'].examples = event.examples.map(e => e.result.value) - - event.result = { - "name": "request", - "schema": { - "type": "object", - "required": ["correlationId", "parameters"], - "properties":{ - "correlationId": { - "type": "string", - }, - "parameters": { - "$ref": "#/components/schemas/" + params - } - }, - "additionalProperties": false - } - } - - event.params = [] - - event.examples = event.examples.map(example => { - example.result = { - "name": "request", - "value": { - "correlationId": "xyz", - "parameters": {} - } - } - example.params.forEach(p => { - example.result.value.parameters[p.name] = p.value - }) - example.params = [] - return example - }) - - old_tags.forEach(t => { - if (t.name !== 'push-pull') - { - event.tags.push(t) - } - }) +const createPullProvider = (requestor) => { + const provider = JSON.parse(JSON.stringify(requestor)) + provider.name = requestor.tags.find(t => t['x-provided-by'])['x-provided-by'] + const old_tags = JSON.parse(JSON.stringify(requestor.tags)) - const caps = event.tags.find(t => t.name === 'capabilities') - caps['x-provides'] = caps['x-uses'].pop() || caps['x-manages'].pop() - caps['x-requestor'] = requestor.name - delete caps['x-uses'] - delete caps['x-manages'] - delete caps['x-provided-by'] + const caps = provider.tags.find(t => t.name === 'capabilities') + caps['x-provides'] = caps['x-uses'].pop() || caps['x-manages'].pop() + caps['x-requestor'] = requestor.name + delete caps['x-uses'] + delete caps['x-manages'] + delete caps['x-provided-by'] - return event + return provider } const createPullProviderParams = (requestor) => { @@ -697,285 +707,277 @@ const createTemporalEventMethod = (method, json, name) => { } const createEventFromMethod = (method, json, name, correlationExtension, tagsToRemove = []) => { - const event = eventDefaults(JSON.parse(JSON.stringify(method))) - event.name = 'on' + name - const old_tags = JSON.parse(JSON.stringify(method.tags)) - - event.tags[0][correlationExtension] = method.name - event.tags.unshift({ - name: 'rpc-only' - }) - - old_tags.forEach(t => { - if (!tagsToRemove.find(t => tagsToRemove.includes(t.name))) - { - event.tags.push(t) - } - }) + const event = eventDefaults(JSON.parse(JSON.stringify(method))) + event.name = methodRename(event, _ => 'on' + name) + const old_tags = JSON.parse(JSON.stringify(method.tags)) + + event.tags[0][correlationExtension] = method.name + event.tags.unshift({ + name: 'rpc-only' + }) + + old_tags.forEach(t => { + if (!tagsToRemove.find(t => tagsToRemove.includes(t.name))) + { + event.tags.push(t) + } + }) - return event + return event } const createTemporalStopMethod = (method, jsoname) => { - const stop = JSON.parse(JSON.stringify(method)) + const stop = JSON.parse(JSON.stringify(method)) - stop.name = 'stop' + method.name.charAt(0).toUpperCase() + method.name.substr(1) + stop.name = methodRename(stop, name => 'stop' + name.charAt(0).toUpperCase() + name.substr(1)) - stop.tags = stop.tags.filter(tag => tag.name !== 'temporal-set') - stop.tags.unshift({ - name: "rpc-only" - }) + stop.tags = stop.tags.filter(tag => tag.name !== 'temporal-set') + stop.tags.unshift({ + name: "rpc-only" + }) - // copy the array items schema to the main result for individual events - stop.result.name = "result" - stop.result.schema = { - type: "null" - } + // copy the array items schema to the main result for individual events + stop.result.name = "result" + stop.result.schema = { + type: "null" + } - stop.params = [{ - name: "correlationId", - required: true, - schema: { - type: "string" - } - }] + stop.params = [{ + name: "correlationId", + required: true, + schema: { + type: "string" + } + }] - stop.examples && stop.examples.forEach(example => { - example.params = [{ - name: "correlationId", - value: "xyz" - }] + stop.examples && stop.examples.forEach(example => { + example.params = [{ + name: "correlationId", + value: "xyz" + }] - example.result = { - name: "result", - value: null - } - }) + example.result = { + name: "result", + value: null + } + }) - return stop + return stop } const createSetterFromProperty = property => { - const setter = JSON.parse(JSON.stringify(property)) - setter.name = 'set' + setter.name.charAt(0).toUpperCase() + setter.name.substr(1) - const old_tags = setter.tags - setter.tags = [ - { - 'name': 'setter', - 'x-setter-for': property.name - } - ] - - const param = setter.result - param.name = 'value' - param.required = true - setter.params.push(param) - - setter.result = { - name: 'result', - schema: { - type: "null" - } - } + const setter = JSON.parse(JSON.stringify(property)) + setter.name = methodRename(setter, name => 'set' + name.charAt(0).toUpperCase() + name.substr(1)) + const old_tags = setter.tags + setter.tags = [ + { + 'name': 'setter', + 'x-setter-for': property.name + } + ] - setter.examples && setter.examples.forEach(example => { - example.params.push({ - name: 'value', - value: example.result.value - }) + const param = setter.result + param.name = 'value' + param.required = true + setter.params.push(param) + + setter.result = { + name: 'result', + schema: { + type: "null" + } + } - example.result.value = null - }) + setter.examples && setter.examples.forEach(example => { + example.params.push({ + name: 'value', + value: example.result.value + }) - old_tags.forEach(t => { - if (t.name !== 'property' && !t.name.startsWith('property:')) - { - if (t.name === 'capabilities') { - setter.tags.push({ - name: 'capabilities', - 'x-manages': t['x-uses'] || t['x-manages'] - }) - } else { - setter.tags.push(t) - } - } - }) + example.result.value = null + }) + + old_tags.forEach(t => { + if (t.name !== 'property' && !t.name.startsWith('property:')) + { + if (t.name === 'capabilities') { + setter.tags.push({ + name: 'capabilities', + 'x-manages': t['x-uses'] || t['x-manages'] + }) + } else { + setter.tags.push(t) + } + } + }) - return setter + return setter } const createFocusFromProvider = provider => { + + const ready = JSON.parse(JSON.stringify(provider)) + ready.name = methodRename(ready, name => name.charAt(9).toLowerCase() + name.substr(10) + 'Focus') + ready.summary = `Internal API for ${methodName(provider).substr(9)} Provider to request focus for UX purposes.` + ready.tags = ready.tags.filter(t => t.name !== 'event') + ready.tags.find(t => t.name === 'capabilities')['x-allow-focus-for'] = provider.name + + ready.params = [] + ready.result = { + name: 'result', + schema: { + type: "null" + } + } - if (!name(provider).startsWith('onRequest')) { - throw "Methods with the `x-provider` tag extension MUST start with 'onRequest'." - } - - const ready = JSON.parse(JSON.stringify(provider)) - ready.name = rename(ready, n => n.charAt(9).toLowerCase() + n.substr(10) + 'Focus') - ready.summary = `Internal API for ${name(provider).substr(9)} Provider to request focus for UX purposes.` - ready.tags = ready.tags.filter(t => t.name !== 'event') - ready.tags.find(t => t.name === 'capabilities')['x-allow-focus-for'] = provider.name - - ready.params = [] - ready.result = { - name: 'result', - schema: { - type: "null" - } - } - - ready.examples = [ - { - name: "Example", - params: [], - result: { - name: "result", - value: null - } - } - ] + ready.examples = [ + { + name: "Example", + params: [], + result: { + name: "result", + value: null + } + } + ] - return ready + return ready } // type = Response | Error const createResponseFromProvider = (provider, type, json) => { - if (!name(provider).startsWith('onRequest')) { - throw "Methods with the `x-provider` tag extension MUST start with 'onRequest'." - } + const response = JSON.parse(JSON.stringify(provider)) + response.name = methodRename(response, name => name.charAt(9).toLowerCase() + name.substr(10) + type) + response.summary = `Internal API for ${methodName(provider).substr(9)} Provider to send back ${type.toLowerCase()}.` - const response = JSON.parse(JSON.stringify(provider)) - response.name = rename(response, n => n.charAt(9).toLowerCase() + n.substr(10) + type) - response.summary = `Internal API for ${provider.name.substr(9)} Provider to send back ${type.toLowerCase()}.` + response.tags = response.tags.filter(t => t.name !== 'event') + response.tags.find(t => t.name === 'capabilities')[`x-${type.toLowerCase()}-for`] = provider.name - response.tags = response.tags.filter(t => t.name !== 'event') - response.tags.find(t => t.name === 'capabilities')[`x-${type.toLowerCase()}-for`] = provider.name + const paramExamples = [] - const paramExamples = [] + if (provider.tags.find(t => t[`x-${type.toLowerCase()}`])) { + response.params = [ + { + name: "correlationId", + schema: { + type: "string" + }, + required: true + }, + { + name: type === 'Error' ? 'error' : "result", + schema: provider.tags.find(t => t[`x-${type.toLowerCase()}`])[`x-${type.toLowerCase()}`], + required: true + } + ] - if (provider.tags.find(t => t[`x-${type.toLowerCase()}`])) { - response.params = [ - { - name: "correlationId", - schema: { - type: "string" + if (!provider.tags.find(t => t['x-error'])) { + provider.tags.find(t => t.name === 'event')['x-error'] = { + //"$ref": "https://meta.open-rpc.org/#definitions/errorObject" + // TODO: replace this with ref above (requires merge of `fix/rpc.discover`) + "type": "object", + "additionalProperties": false, + "required": [ + "code", + "message" + ], + "properties": { + "code": { + "title": "errorObjectCode", + "description": "A Number that indicates the error type that occurred. This MUST be an integer. The error codes from and including -32768 to -32000 are reserved for pre-defined errors. These pre-defined errors SHOULD be assumed to be returned from any JSON-RPC api.", + "type": "integer" }, - required: true - }, - { - name: type === 'Error' ? 'error' : "result", - schema: provider.tags.find(t => t[`x-${type.toLowerCase()}`])[`x-${type.toLowerCase()}`], - required: true - } - ] - - if (!provider.tags.find(t => t['x-error'])) { - provider.tags.find(t => t.name === 'event')['x-error'] = { - //"$ref": "https://meta.open-rpc.org/#definitions/errorObject" - // TODO: replace this with ref above (requires merge of `fix/rpc.discover`) - "type": "object", - "additionalProperties": false, - "required": [ - "code", - "message" - ], - "properties": { - "code": { - "title": "errorObjectCode", - "description": "A Number that indicates the error type that occurred. This MUST be an integer. The error codes from and including -32768 to -32000 are reserved for pre-defined errors. These pre-defined errors SHOULD be assumed to be returned from any JSON-RPC api.", - "type": "integer" + "message": { + "title": "errorObjectMessage", + "description": "A String providing a short description of the error. The message SHOULD be limited to a concise single sentence.", + "type": "string" + }, + "data": { + "title": "errorObjectData", + "description": "A Primitive or Structured value that contains additional information about the error. This may be omitted. The value of this member is defined by the Server (e.g. detailed error information, nested errors etc.)." + } + } + } + } + + const schema = localizeDependencies(provider.tags.find(t => t[`x-${type.toLowerCase()}`])[`x-${type.toLowerCase()}`], json) + + let n = 1 + if (schema.examples && schema.examples.length) { + paramExamples.push(... (schema.examples.map( param => ({ + name: schema.examples.length === 1 ? "Example" : `Example #${n++}`, + params: [ + { + name: 'correlationId', + value: '123' }, - "message": { - "title": "errorObjectMessage", - "description": "A String providing a short description of the error. The message SHOULD be limited to a concise single sentence.", - "type": "string" + { + name: 'result', + value: param + } + ], + result: { + name: 'result', + value: null + } + })) || [])) + delete schema.examples + } + else if (schema['$ref']) { + paramExamples.push({ + name: 'Generated Example', + params: [ + { + name: `${type.toLowerCase()}`, + value: { + correlationId: "123", + result: { + '$ref': schema['$ref'] + '/examples/0' + } + } + } + ], + result: { + name: 'result', + value: null + } + }) + } + } + + if (paramExamples.length === 0) { + const value = type === 'Error' ? { code: 1, message: 'Error' } : {} + paramExamples.push( + { + name: 'Example 1', + params: [ + { + name: 'correlationId', + value: '123' }, - "data": { - "title": "errorObjectData", - "description": "A Primitive or Structured value that contains additional information about the error. This may be omitted. The value of this member is defined by the Server (e.g. detailed error information, nested errors etc.)." + { + name: type === 'Error' ? 'error' : 'result', + value } - } - } - } - - const schema = localizeDependencies(provider.tags.find(t => t[`x-${type.toLowerCase()}`])[`x-${type.toLowerCase()}`], json) - - let n = 1 - if (schema.examples && schema.examples.length) { - paramExamples.push(... (schema.examples.map( param => ({ - name: schema.examples.length === 1 ? "Example" : `Example #${n++}`, - params: [ - { - name: 'correlationId', - value: '123' - }, - { - name: 'result', - value: param - } - ], - result: { - name: 'result', - value: null - } - })) || [])) - delete schema.examples - } - else if (schema['$ref']) { - paramExamples.push({ - name: 'Generated Example', - params: [ - { - name: `${type.toLowerCase()}`, - value: { - correlationId: "123", - result: { - '$ref': schema['$ref'] + '/examples/0' - } - } - } - ], - result: { - name: 'result', - value: null - } - }) - } - } - - if (paramExamples.length === 0) { - const value = type === 'Error' ? { code: 1, message: 'Error' } : {} - paramExamples.push( - { - name: 'Example 1', - params: [ - { - name: 'correlationId', - value: '123' - }, - { - name: type === 'Error' ? 'error' : 'result', - value - } - ], - result: { - name: 'result', - value: null - } - }) - } + ], + result: { + name: 'result', + value: null + } + }) + } - response.result = { - name: 'result', - schema: { - type: 'null' - } - } + response.result = { + name: 'result', + schema: { + type: 'null' + } + } - response.examples = paramExamples + response.examples = paramExamples - return response + return response } const copyAllowFocusTags = (json) => { @@ -991,25 +993,25 @@ const copyAllowFocusTags = (json) => { } const generatePropertyEvents = json => { - const properties = json.methods.filter( m => m.tags && m.tags.find( t => t.name == 'property')) || [] - const readonlies = json.methods.filter( m => m.tags && m.tags.find( t => t.name == 'property:readonly')) || [] - - properties.forEach(property => { - json.methods.push(createEventFromProperty(property, 'Changed', property.name, json)) - const schema = createEventResultSchemaFromProperty(property, 'Changed') - if (schema) { - json.components.schemas[schema.title] = schema - } - }) - readonlies.forEach(property => { - json.methods.push(createEventFromProperty(property, 'Changed', property.name, json)) - const schema = createEventResultSchemaFromProperty(property, 'Changed') - if (schema) { - json.components.schemas[schema.title] = schema - } - }) + const properties = json.methods.filter( m => m.tags && m.tags.find( t => t.name == 'property')) || [] + const readonlies = json.methods.filter( m => m.tags && m.tags.find( t => t.name == 'property:readonly')) || [] + + properties.forEach(property => { + json.methods.push(createNotifierFromProperty(property)) + const schema = createEventResultSchemaFromProperty(property) + if (schema) { + json.components.schemas[property.name.split('.').shift() + '.' + schema.title] = schema + } + }) + readonlies.forEach(property => { + json.methods.push(createNotifierFromProperty(property)) + const schema = createEventResultSchemaFromProperty(property) + if (schema) { + json.components.schemas[property.name.split('.').shift() + '.' + schema.title] = schema + } + }) - return json + return json } const generatePropertySetters = json => { @@ -1045,24 +1047,16 @@ const generatePushPullMethods = json => { } const generateProvidedByMethods = json => { - const requestors = json.methods.filter(m => !m.tags.find(t => t.name === 'event')).filter( m => m.tags && m.tags.find( t => t['x-provided-by'])) || [] - const events = json.methods .filter(m => m.tags.find(t => t.name === 'event')) - .filter( m => m.tags && m.tags.find( t => t['x-provided-by'])) - .filter(e => !json.methods.find(m => m.name === e.tags.find(t => t['x-provided-by'])['x-provided-by'])) - - const pushers = events.map(m => createNotifierFromEvent(m, json)) - pushers.forEach(m => json.methods.push(m)) - - requestors.forEach(requestor => { - const schema = createPullProviderParams(requestor) - json.methods.push(createPullProvider(requestor, schema.title)) + const requestors = json.methods.filter(m => !m.tags.find(t => t.name === 'notifier')).filter( m => m.tags && m.tags.find( t => t['x-provided-by'])) || [] - json.components = json.components || {} - json.components.schemas = json.components.schemas || {} - json.components.schemas[schema.title] = schema - }) + requestors.forEach(requestor => { + const provider = json.methods.find(m => (m.name === extension(requestor, 'x-provided-by')) && provides(m) && !isEvent(m) && !isPusher(m) && !isNotifier(m)) + if (!provider) { + json.methods.push(createPullProvider(requestor)) + } + }) - return json + return json } const generateTemporalSetMethods = json => { @@ -1099,6 +1093,259 @@ const generateProviderMethods = json => { return json } +const generateUnidirectionalProviderMethods = json => { + const providers = json.methods.filter(isProviderInterfaceMethod)// m => m.tags && m.tags.find( t => t.name == 'capabilities' && t['x-provides'] && !t['x-push'])) || [] + + // Transform providers to legacy events + providers.forEach(p => { + const name = methodRename(p, name => 'onRequest' + name.charAt(0).toUpperCase() + name.substring(1)) + const prefix = name.split('.').pop().substring(9) + + json.methods.filter(m => m.tags && m.tags.find( t=> t.name === 'capabilities')['x-provided-by'] === p.name && !m.tags.find(t => t.name === 'notifier')).forEach(m => { + m.tags.find(t => t.name === 'capabilities')['x-provided-by'] = name + }) + p.name = name + p.tags.push({ + name: 'event', + 'x-response-name': p.result.name, + 'x-response': p.result.schema, + // todo: add examples + }) + + // Need to calculate if the module name ends with the same word as the method starts with, and dedupe + // This is here because we're generating names that used to be editorial. These don't match exactly, + // but they're good enough and "PinChallengeRequest" is way better than "PinChallengeChallengeRequest" + let overlap = 0 + const _interface = p.name.split('.')[0] + const method = methodName(p).substring(9) + const capability = extension(p, 'x-provides') + + for (let i=0; i { + parameters.properties[param.name] = param.schema + if (param.required) { + parameters.required.push(param.name) + } + }) + + // remove them from the method + p.params = [] + + // build the request wrapper + const request = { + title: prefix + 'Request', + type: "object", + required: [ + "parameters", + "correlationId" + ], + properties: { + parameters: { + $ref: `#/components/schemas/${_interface}.${parameters.title}` + }, + correlationId: { + type: "string" + } + }, + additionalProperties: false + } + + json.components.schemas[_interface + '.' + request.title] = request + json.components.schemas[_interface + '.' + parameters.title] = parameters + + // Put the request into the new event's result + p.result = { + name: 'result', + schema: { + $ref: `#/components/schemas/${_interface}.${request.title}` + } + } + + const eventTag = p.tags.find(t => t.name === 'event') + eventTag['x-response'].examples = [] + p.examples.forEach(example => { + // transform examples + eventTag['x-response'].examples.push(example.result.value) + example.result = { + name: 'result', + value: { + correlationId: '1', + parameters: Object.fromEntries(example.params.map(p => [p.name, p.value])) + } + } + example.params = [ + { + name: 'listen', + value: true + } + ] + }) + }) + + return json +} + +const generateEventSubscribers = json => { + const notifiers = json.methods.filter( m => m.tags && m.tags.find(t => t.name == 'notifier')) || [] + + notifiers.forEach(notifier => { + const tag = notifier.tags.find(tag => tag.name === 'notifier') + // if there's an x-event extension, this denotes an editorially created subscriber + if (!tag['x-event']) { + tag['x-event'] = methodRename(notifier, name => 'on' + name.charAt(0).toUpperCase() + name.substring(1)) + } + const subscriber = json.methods.find(method => method.name === tag['x-event']) + + if (!subscriber) { + const subscriber = JSON.parse(JSON.stringify(notifier)) + subscriber.name = methodRename(subscriber, name => 'on' + name.charAt(0).toUpperCase() + name.substring(1)) + subscriber.params.pop() + subscriber.params.push({ + name: 'listen', + schema: { + type: 'boolean' + } + }) + + subscriber.result = { + name: "result", + schema: { + type: "null" + } + } + + subscriber.examples.forEach(example => { + example.params.pop() + example.params.push({ + name: "listen", + value: true + }) + example.result = { + name: "result", + value: null + } + }) + + const tag = subscriber.tags.find(tag => tag.name === 'notifier') + + tag['x-notifier'] = notifier.name + tag['x-subscriber-for'] = tag['x-notifier-for'] + tag.name = 'event' + delete tag['x-notifier-for'] + delete tag['x-event'] + + subscriber.result = { + name: "result", + schema: { + "type": "null" + } + } + json.methods.push(subscriber) + } + }) + + return json +} + +const generateProviderRegistrars = json => { + const interfaces = getInterfaces(json) + + interfaces.forEach(name => { + const registration = json.methods.find(m => m.tags.find(t => t.name === 'registration') && extension(m, 'x-interface') === name) + + if (!registration) { + json.methods.push({ + name: `${name}.provide`, + tags: [ + { + "name": "registration", + "x-interface": name + }, + { + "name": "capabilities", + "x-provides": json.methods.find(m => m.name.startsWith(name) && m.tags.find(t => t.name === 'capabilities')['x-provides']).tags.find(t => t.name === 'capabilities')['x-provides'] + } + + ], + params: [ + { + name: "enabled", + schema: { + type: "boolean" + } + } + ], + result: { + name: "result", + schema: { + type: "null" + } + }, + examples: [ + { + name: "Default example", + params: [ + { + name: "enabled", + value: true + } + ], + result: { + name: "result", + value: null + } + } + ] + }) + } + }) + + return json +} + +const removeProviderRegistrars = (json) => { + json.methods && (json.methods = json.methods.filter(m => !isRegistration(m))) + return json +} + +const generateUnidirectionalEventMethods = json => { + const events = json.methods.filter( m => m.tags && m.tags.find(t => t.name == 'notifier')) || [] + + events.forEach(event => { + const tag = event.tags.find(t => t.name === 'notifier') + event.name = tag['x-event'] || methodRename(event, n => 'on' + n.charAt(0).toUpperCase() + n.substr(1)) + delete tag['x-event'] + tag['x-subscriber-for'] = tag['x-notifier-for'] + delete tag['x-notifier-for'] + + tag.name = 'event' + event.result = event.params.pop() + event.examples.forEach(example => { + example.result = example.params.pop() + }) + }) + + return json +} + const generateEventListenerParameters = json => { const events = json.methods.filter( m => m.tags && m.tags.find(t => t.name == 'event')) || [] @@ -1350,22 +1597,29 @@ const getPathFromModule = (module, path) => { return item } -const fireboltize = (json) => { +const fireboltize = (json, bidirectional) => { json = generatePropertyEvents(json) json = generatePropertySetters(json) - // TODO: we don't use this yet... consider removing? - // json = generatePushPullMethods(json) - // json = generateProvidedByMethods(json) + json = generateProvidedByMethods(json) json = generatePolymorphicPullEvents(json) - json = generateProviderMethods(json) - json = generateTemporalSetMethods(json) - json = generateEventListenerParameters(json) - json = generateEventListenResponse(json) - - return json -} -const fireboltizeMerged = (json) => { + if (bidirectional) { + console.log('Creating bidirectional APIs') + json = generateEventSubscribers(json) + json = generateProviderRegistrars(json) + + } else { + console.log('Creating unidirectional APIs') + json = generateUnidirectionalProviderMethods(json) + json = generateUnidirectionalEventMethods(json) + json = generateProviderMethods(json) + json = generateEventListenerParameters(json) + json = generateEventListenResponse(json) + json = removeProviderRegistrars(json) + + } + + json = generateTemporalSetMethods(json) json = copyAllowFocusTags(json) return json @@ -1451,78 +1705,100 @@ const getExternalSchemas = (json = {}, schemas = {}) => { } const addExternalSchemas = (json, sharedSchemas) => { - json = JSON.parse(JSON.stringify(json)) - - let searching = true - - while (searching) { - searching = false - const externalSchemas = getExternalSchemas(json, sharedSchemas) - Object.entries(externalSchemas).forEach( ([name, schema]) => { - const group = sharedSchemas[name.split('#')[0]].title - const id = sharedSchemas[name.split('#')[0]].$id - const refs = getLocalSchemaPaths(schema) - refs.forEach(ref => { - ref.pop() // drop the actual '$ref' so we can modify it - getPathOr(null, ref, schema).$ref = id + getPathOr(null, ref, schema).$ref - }) - // if this schema is a child of some other schema that will be copied in this batch, then skip it - if (Object.keys(externalSchemas).find(s => name.startsWith(s+'/') && s.length < name.length)) { - console.log('Skipping: ' + name) - console.log('Because of: ' + Object.keys(externalSchemas).find(s => name.startsWith(s) && s.length < name.length)) - throw "Skipping sub schema" - return - } - searching = true - json['x-schemas'] = json['x-schemas'] || {} - json['x-schemas'][group] = json['x-schemas'][group] || { uri: name.split("#")[0]} - json['x-schemas'][group][name.split("/").pop()] = schema - }) - - //update references to external schemas to be local - Object.keys(externalSchemas).forEach(ref => { - const group = sharedSchemas[ref.split('#')[0]].title - replaceRef(ref, `#/x-schemas/${group}/${ref.split("#").pop().substring('/definitions/'.length)}`, json) - }) - } + json = JSON.parse(JSON.stringify(json)) + json.components = json.components || {} + json.components.schemas = json.components.schemas || {} + + let found = true + const added = [] + while (found) { + const ids = getAllValuesForName('$ref', json) + found = false + Object.entries(sharedSchemas).forEach( ([key, schema], i) => { + if (!added.includes(key)) { + if (ids.find(id => id.startsWith(key))) { + const bundle = JSON.parse(JSON.stringify(schema)) + replaceUri('', bundle.$id, bundle) + json.components.schemas[key] = bundle + added.push(key) + found = true + } + } + }) + } - return json +// json = removeUnusedSchemas(json) + return json } // TODO: make this recursive, and check for group vs schema const removeUnusedSchemas = (json) => { - const schema = JSON.parse(JSON.stringify(json)) - - const recurse = (schema, path) => { - let deleted = false - Object.keys(schema).forEach(name => { - if (isSchema(schema[name])) { - const used = isDefinitionReferencedBySchema(path + '/' + name, json) + const schema = JSON.parse(JSON.stringify(json)) + const components = schema.components + schema.components = { schemas: {} } + + const refs = getAllValuesForName('$ref', schema) + + const addSchemas = (schema, refs) => { + let added = false + refs.forEach(ref => { + if (ref.startsWith("https://")) { + const [uri, fragment] = ref.split("#") + if (!schema.components.schemas[uri]) { + schema.components.schemas[uri] = components.schemas[uri] + console.log(`Adding ${uri}`) + added = true + } + } + else { + const key = ref.split("/").pop() + if (!schema.components.schemas[key]) { + schema.components.schemas[key] = components.schemas[key] + console.log(`Adding ${key}`) + added = true + } + } + }) + return added + } - if (!used) { - delete schema[name] - deleted = true - } - else { - } - } - else if (typeof schema[name] === 'object') { - deleted = deleted || recurse(schema[name], path + '/' + name) - } - }) - return deleted + if (schema.components.schemas) { + while(addSchemas(schema, refs)) { + refs.length = 0 + refs.push(...getAllValuesForName('$ref', schema)) } + } - if (schema.components.schemas) { - while(recurse(schema.components.schemas, '#/components/schemas')) {} - } + return schema +} - if (schema['x-schemas']) { - while(recurse(schema['x-schemas'], '#/x-schemas')) {} +const removeUnusedBundles = (json) => { + json = JSON.parse(JSON.stringify(json)) + // remove all the shared schemas + const sharedSchemas = {} + Object.keys(json.components.schemas).forEach (key => { + if (key.startsWith('https://')) { + sharedSchemas[key] = json.components.schemas[key] + delete json.components.schemas[key] } + }) + + // and only add back in the ones that are still referenced + let found = true + while(found) { + found = false + const ids = [ ...new Set(getAllValuesForName('$ref', json).map(ref => ref.split('#').shift()))] + Object.keys(sharedSchemas).forEach(key => { + if (ids.includes(key)) { + json.components.schemas[key] = sharedSchemas[key] + delete sharedSchemas[key] + found = true + } + }) + } - return schema -} + return json +} const getModule = (name, json, copySchemas, extractSubSchemas) => { let openrpc = JSON.parse(JSON.stringify(json)) @@ -1664,7 +1940,6 @@ export { getSchemas, getParamsFromMethod, fireboltize, - fireboltizeMerged, getPayloadFromEvent, getPathFromModule, providerHasNoParameters,