diff --git a/languages/cpp/language.config.json b/languages/cpp/language.config.json index 724a1359..0f7cee05 100644 --- a/languages/cpp/language.config.json +++ b/languages/cpp/language.config.json @@ -4,9 +4,8 @@ "createModuleDirectories": false, "extractSubSchemas": true, "unwrapResultObjects": false, - "createPolymorphicMethods": true, + "enableUnionTypes": false, "excludeDeclarations": true, - "extractProviderSchema": true, "aggregateFiles": [ "/include/firebolt.h", "/src/firebolt.cpp" diff --git a/languages/cpp/templates/interfaces/default.cpp b/languages/cpp/templates/classes/default.cpp similarity index 100% rename from languages/cpp/templates/interfaces/default.cpp rename to languages/cpp/templates/classes/default.cpp diff --git a/languages/cpp/templates/interfaces/focusable.cpp b/languages/cpp/templates/classes/focusable.cpp similarity index 100% rename from languages/cpp/templates/interfaces/focusable.cpp rename to languages/cpp/templates/classes/focusable.cpp diff --git a/languages/cpp/templates/codeblocks/interface.cpp b/languages/cpp/templates/codeblocks/class.cpp similarity index 100% rename from languages/cpp/templates/codeblocks/interface.cpp rename to languages/cpp/templates/codeblocks/class.cpp diff --git a/languages/cpp/templates/codeblocks/provider.h b/languages/cpp/templates/codeblocks/provider.h deleted file mode 100644 index b2895fe0..00000000 --- a/languages/cpp/templates/codeblocks/provider.h +++ /dev/null @@ -1 +0,0 @@ -${interface} diff --git a/languages/cpp/templates/imports/calls-metrics.cpp b/languages/cpp/templates/imports/calls-metrics.cpp deleted file mode 100644 index 4ba289b3..00000000 --- a/languages/cpp/templates/imports/calls-metrics.cpp +++ /dev/null @@ -1 +0,0 @@ -#include "metrics.h" diff --git a/languages/cpp/templates/imports/calls-metrics.impl b/languages/cpp/templates/imports/calls-metrics.impl deleted file mode 100644 index 75fd87c6..00000000 --- a/languages/cpp/templates/imports/calls-metrics.impl +++ /dev/null @@ -1 +0,0 @@ -#include "metrics_impl.h" diff --git a/languages/cpp/templates/modules/include/module.h b/languages/cpp/templates/modules/include/module.h index ff3b1080..01554b06 100644 --- a/languages/cpp/templates/modules/include/module.h +++ b/languages/cpp/templates/modules/include/module.h @@ -19,7 +19,10 @@ #pragma once #include "error.h" -/* ${IMPORTS} */ +/* ${IMPORTS:h} */ +${if.callsmetrics}#include "metrics.h" +${end.if.callsmetrics} + ${if.declarations}namespace Firebolt { namespace ${info.Title} { @@ -30,7 +33,19 @@ namespace ${info.Title} { ${if.types} // Types /* ${TYPES} */${end.if.types} -${if.providers}/* ${PROVIDERS} */${end.if.providers}${if.xuses}/* ${XUSES} */${end.if.xuses} +${if.providers}// Provider Interfaces +struct IProviderSession { + virtual ~IProviderSession() = default; + + virtual std::string correlationId() const = 0; +}; + +struct IFocussableProviderSession : virtual public IProviderSession { + virtual ~IFocussableProviderSession() override = default; + + virtual void focus( Firebolt::Error *err = nullptr ) = 0; +}; +/* ${PROVIDER_INTERFACES} */${end.if.providers}${if.xuses}/* ${XUSES} */${end.if.xuses} ${if.methods}struct I${info.Title} { virtual ~I${info.Title}() = default; diff --git a/languages/cpp/templates/modules/src/module_impl.cpp b/languages/cpp/templates/modules/src/module_impl.cpp index abdbecd6..d774c396 100644 --- a/languages/cpp/templates/modules/src/module_impl.cpp +++ b/languages/cpp/templates/modules/src/module_impl.cpp @@ -22,7 +22,7 @@ namespace Firebolt { namespace ${info.Title} { ${if.providers} -/* ${PROVIDERS} */${end.if.providers} +/* ${PROVIDER_CLASES} */${end.if.providers} // Methods /* ${METHODS} */ @@ -35,5 +35,5 @@ namespace ${info.Title} { namespace WPEFramework { -/* ${ENUMS} */ +/* ${ENUM_IMPLEMENTATIONS} */ }${end.if.enums} diff --git a/languages/cpp/templates/modules/src/module_impl.h b/languages/cpp/templates/modules/src/module_impl.h index ce2837f4..3a6df1ea 100644 --- a/languages/cpp/templates/modules/src/module_impl.h +++ b/languages/cpp/templates/modules/src/module_impl.h @@ -20,7 +20,9 @@ #include "FireboltSDK.h" #include "IModule.h" -/* ${IMPORTS} */ +/* ${IMPORTS:impl} */ +${if.callsmetrics}#include "metrics_impl.h" +${end.if.callsmetrics} #include "${info.title.lowercase}.h" ${if.implementations} diff --git a/languages/cpp/templates/schemas/include/common/module.h b/languages/cpp/templates/schemas/include/common/module.h index 87ba7425..982cc396 100644 --- a/languages/cpp/templates/schemas/include/common/module.h +++ b/languages/cpp/templates/schemas/include/common/module.h @@ -19,7 +19,7 @@ #pragma once #include "error.h" -/* ${IMPORTS} */ +/* ${IMPORTS:h} */ ${if.declarations}namespace Firebolt { namespace ${info.Title} { diff --git a/languages/cpp/templates/schemas/src/jsondata_module.h b/languages/cpp/templates/schemas/src/jsondata_module.h index 916b8698..09c8dfb0 100644 --- a/languages/cpp/templates/schemas/src/jsondata_module.h +++ b/languages/cpp/templates/schemas/src/jsondata_module.h @@ -18,7 +18,7 @@ #pragma once -/* ${IMPORTS} */ +/* ${IMPORTS:jsondata} */ #include "common/${info.title.lowercase}.h" ${if.schemas}namespace Firebolt { diff --git a/languages/cpp/templates/schemas/src/module_common.cpp b/languages/cpp/templates/schemas/src/module_common.cpp index 7d9d13af..2551d0e3 100644 --- a/languages/cpp/templates/schemas/src/module_common.cpp +++ b/languages/cpp/templates/schemas/src/module_common.cpp @@ -17,11 +17,11 @@ */ #include "FireboltSDK.h" -/* ${IMPORTS} */ +/* ${IMPORTS:cpp} */ #include "jsondata_${info.title.lowercase}.h" ${if.enums} namespace WPEFramework { -/* ${ENUMS} */ +/* ${ENUM_IMPLEMENTATIONS} */ }${end.if.enums} \ No newline at end of file diff --git a/languages/cpp/templates/sdk/include/firebolt.h b/languages/cpp/templates/sdk/include/firebolt.h index fbd99809..d896e374 100644 --- a/languages/cpp/templates/sdk/include/firebolt.h +++ b/languages/cpp/templates/sdk/include/firebolt.h @@ -127,7 +127,7 @@ struct IFireboltAccessor { // Module Instance methods goes here. // Instances are owned by the FireboltAcccessor and linked with its lifecycle. -${module.init} +${module.init:h} }; } diff --git a/languages/cpp/templates/sdk/src/firebolt.cpp b/languages/cpp/templates/sdk/src/firebolt.cpp index a864c2c4..30466821 100644 --- a/languages/cpp/templates/sdk/src/firebolt.cpp +++ b/languages/cpp/templates/sdk/src/firebolt.cpp @@ -94,7 +94,7 @@ namespace Firebolt { { } -${module.init} +${module.init:cpp} private: FireboltSDK::Accessor* _accessor; static FireboltAccessorImpl* _singleton; diff --git a/languages/cpp/templates/sections/provider-interfaces.h b/languages/cpp/templates/sections/provider-interfaces.h deleted file mode 100644 index 88134258..00000000 --- a/languages/cpp/templates/sections/provider-interfaces.h +++ /dev/null @@ -1,14 +0,0 @@ -// Provider Interfaces -struct IProviderSession { - virtual ~IProviderSession() = default; - - virtual std::string correlationId() const = 0; -}; - -struct IFocussableProviderSession : virtual public IProviderSession { - virtual ~IFocussableProviderSession() override = default; - - virtual void focus( Firebolt::Error *err = nullptr ) = 0; -}; - -${providers.list} diff --git a/languages/cpp/templates/types/enum.cpp b/languages/cpp/templates/types/enum-implementation.cpp similarity index 100% rename from languages/cpp/templates/types/enum.cpp rename to languages/cpp/templates/types/enum-implementation.cpp diff --git a/languages/javascript/language.config.json b/languages/javascript/language.config.json index 19b4659d..360f0ec9 100644 --- a/languages/javascript/language.config.json +++ b/languages/javascript/language.config.json @@ -5,8 +5,13 @@ "/index.mjs", "/defaults.mjs" ], + "templatesPerSchema": [ + "/index.mjs" + ], "createModuleDirectories": true, - "copySchemasIntoModules": true, + "copySchemasIntoModules": false, + "enableUnionTypes": true, + "mergeOnTitle": true, "aggregateFiles": [ "/index.d.ts" ], diff --git a/languages/javascript/src/shared/Events/index.mjs b/languages/javascript/src/shared/Events/index.mjs index 18c2092a..0949785a 100644 --- a/languages/javascript/src/shared/Events/index.mjs +++ b/languages/javascript/src/shared/Events/index.mjs @@ -16,8 +16,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import Transport from '../Transport/index.mjs' -import { setMockListener } from '../Transport/MockTransport.mjs' +import Gateway from '../Gateway/index.mjs' let listenerId = 0 @@ -82,41 +81,30 @@ const listeners = { } } -// holds a map of RPC Ids => Context Key, e.g. the RPC id of an onEvent call mapped to the corresponding context parameters key for that RPC call -const keys = {} - // holds a map of ${module}.${event} => Transport.send calls (only called once per event) // note that the keys here MUST NOT contain wild cards const oncers = [] const validEvents = {} const validContext = {} -let transportInitialized = false - -export const emit = (id, value) => { - callCallbacks(listeners.internal[keys[id]], [value]) - callCallbacks(listeners.external[keys[id]], [value]) -} - export const registerEvents = (module, events) => { - validEvents[module.toLowerCase()] = events.concat() + validEvents[module] = events.concat() } export const registerEventContext = (module, event, context) => { - validContext[module.toLowerCase()] = validContext[module.toLowerCase()] || {} - validContext[module.toLowerCase()][event] = context.concat() + validContext[module] = validContext[module] || {} + validContext[module][event] = context.concat() } -const callCallbacks = (cbs, args) => { - cbs && - Object.keys(cbs).forEach(listenerId => { - let callback = cbs[listenerId] - if (oncers.indexOf(parseInt(listenerId)) >= 0) { - oncers.splice(oncers.indexOf(parseInt(listenerId)), 1) - delete cbs[listenerId] - } - callback.apply(null, args) - }) +const callCallbacks = (key, args) => { + const callbacks = Object.entries(listeners.internal[key] || {}).concat(Object.entries(listeners.external[key] || {})) + callbacks.forEach( ([listenerId, callback]) => { + if (oncers.indexOf(parseInt(listenerId)) >= 0) { + oncers.splice(oncers.indexOf(parseInt(listenerId)), 1) + delete listeners.external[key][listenerId] + } + callback.apply(null, [args]) + }) } const doListen = function(module, event, callback, context, once, internal=false) { @@ -146,8 +134,15 @@ const doListen = function(module, event, callback, context, once, internal=false if (Object.values(listeners.get(key)).length === 0) { const args = Object.assign({ listen: true }, context) - const { id, promise } = Transport.listen(module, 'on' + event[0].toUpperCase() + event.substring(1), args) - keys[id] = key + + // TODO: Is subscriber -> notifer required to be a simple transform (drop 'on'?) + const subscriber = module + '.on' + event[0].toUpperCase() + event.substring(1) + const notifier = module + '.' + event + + Gateway.subscribe(notifier, (params) => { + callCallbacks(key, params) + }) + const promise = Gateway.request(subscriber, args) promises.push(promise) } @@ -197,7 +192,7 @@ const getListenArgs = function(...args) { } const getClearArgs = function(...args) { - const module = (args.shift() || '*').toLowerCase() + const module = (args.shift() || '*') const event = args.shift() || '*' const context = {} @@ -240,7 +235,8 @@ export const prioritize = function(...args) { const unsubscribe = (key, context) => { const [module, event] = key.split('.').slice(0, 2) const args = Object.assign({ listen: false }, context) - Transport.send(module, 'on' + event[0].toUpperCase() + event.substr(1), args) + Gateway.request(module + '.on' + event[0].toUpperCase() + event.substr(1), args) + Gateway.unsubscribe(`${module}.${event}`) } @@ -270,7 +266,7 @@ const doClear = function (moduleOrId = false, event = false, context) { }) } else if (!event) { listeners.keys().forEach(key => { - if (key.indexOf(moduleOrId.toLowerCase()) === 0) { + if (key.indexOf(moduleOrId) === 0) { listeners.removeKey(key) unsubscribe(key) } @@ -287,18 +283,10 @@ const doClear = function (moduleOrId = false, event = false, context) { } const init = () => { - if (!transportInitialized) { - Transport.addEventEmitter(emit) - setMockListener(listen) - transportInitialized = true - } } export default { listen: listen, once: once, - clear: clear, - broadcast(event, value) { - emit(Object.entries(keys).find( ([k, v]) => v === 'app.'+event)[0], value) - }, + clear: clear } diff --git a/languages/javascript/src/shared/Gateway/Bidirectional.mjs b/languages/javascript/src/shared/Gateway/Bidirectional.mjs new file mode 100644 index 00000000..a0b44538 --- /dev/null +++ b/languages/javascript/src/shared/Gateway/Bidirectional.mjs @@ -0,0 +1,108 @@ +/* + * 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 + */ + +import Server from "./Server.mjs" +import Client from "./Client.mjs" +import Transport from "../Transport/index.mjs" +import Settings from "../Settings/index.mjs" + +Transport.receive(async (message) => { + const json = JSON.parse(message) + if (Array.isArray(json)) { + json.forEach(message => processMessage(message)) + } + else { + processMessage(json) + } +}) + +function processMessage(json) { + if (Settings.getLogLevel() === 'DEBUG') { + console.debug('Receiving message from transport: \n' + JSON.stringify(json, { indent: '\t'})) + } + + if (json.method !== undefined) { + if (json.id !== undefined) { + Server.request(json.id, json.method, json.params) + } + else { + Server.notify(json.method, json.params) + } + } + else if (json.id !== undefined) { + Client.response(json.id, json.result, json.error) + } +} + +export async function batch(requests) { + if (Array.isArray(requests)) { + return await Client.batch(requests) + } + else { + throw "Gateway.batch() requires an array of requests: { method: String, params: Object, id: Boolean }" + } +} + +export async function request(method, params) { + if (Array.isArray(method)) { + throw "Use Gateway.batch() for batch requests." + } + else { + return await Client.request(method, params) + } +} + +export async function notify(method, params) { + if (Array.isArray(method)) { + throw "Use Gateway.batch() for batch requests." + } + else { + return await Client.notify(method, params) + } +} + +export function subscribe(event, callback) { + Server.subscribe(event, callback) +} + +export function unsubscribe(event) { + Server.unsubscribe(event) +} + +export function simulate(event, value) { + Server.simulate(event, value) +} + +export function provide(interfaceName, provider) { + Server.provide(interfaceName, provider) +} + +export function deprecate(method, alternative) { + Client.deprecate(method, alternative) +} + +export default { + request, + notify, + batch, + subscribe, + unsubscribe, + simulate, + provide, + deprecate +} \ No newline at end of file diff --git a/languages/javascript/src/shared/Gateway/Client.mjs b/languages/javascript/src/shared/Gateway/Client.mjs new file mode 100644 index 00000000..a633bdd1 --- /dev/null +++ b/languages/javascript/src/shared/Gateway/Client.mjs @@ -0,0 +1,121 @@ +/* + * 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 + */ + +import Transport from "../Transport/index.mjs" + +const win = typeof window !== 'undefined' ? window : {} +win.__firebolt = win.__firebolt || {} + +// JSON RPC id generator, to be shared across all SDKs +class JsonRpcIdIterator { + constructor() { + this._id = 1 + } + getJsonRpcId() { + return this._id++ + } +} + +let idGenerator = win.__firebolt.idGenerator || new JsonRpcIdIterator() +win.__firebolt.idGenerator = idGenerator + +const promises = {} +const deprecated = {} + +// request = { method: string, params: object, id: boolean }[] +// request with no `id` property are assumed to NOT be notifications, i.e. id must be set to false explicitly +export async function batch(requests) { + if (Array.isArray(requests)) { + const processed = requests.map(req => processRequest(req.method, req.params, req.id, req.id === false)) + + // filter requests exclude notifications, as they don't need promises + const promises = processed.filter(req => req.id).map(request => addPromiseToQueue(request.id)) + + Transport.send(processed) + + // Using Promise.all get's us batch blocking for free + return Promise.all(promises) + } + throw `Bulk requests must be in an array` +} + +// Request that the server provide fulfillment of an method +export async function request(method, params) { + const json = processRequest(method, params) + const promise = addPromiseToQueue(json.id) + Transport.send(json) + return promise +} + +export function notify(method, params) { + Transport.send(processRequest(method, params, true)) +} + +export function response(id, result, error) { + const promise = promises[id] + + if (promise) { + if (result !== undefined) { + promises[id].resolve(result) + } + else if (error !== undefined) { + promises[id].reject(error) + } + + // TODO make sure this works + delete promises[id] + } + else { + throw `Received a response for an unidentified request ${id}` + } +} + +export function deprecate(method, alternative) { + deprecated[method] = alternative +} + +function addPromiseToQueue (id) { + return new Promise((resolve, reject) => { + promises[id] = {} + promises[id].promise = this + promises[id].resolve = resolve + promises[id].reject = reject + }) +} + +function processRequest(method, params, notification=false) { + if (deprecated[method]) { + console.warn(`WARNING: ${method}() is deprecated. ` + deprecated[method]) + } + + const id = !notification && idGenerator.getJsonRpcId() + const jsonrpc = '2.0' + const json = { jsonrpc, method, params } + + !notification && (json.id = id) + + return json +} + + +export default { + request, + batch, + response, + deprecate +} \ No newline at end of file diff --git a/languages/javascript/src/shared/Gateway/Server.mjs b/languages/javascript/src/shared/Gateway/Server.mjs new file mode 100644 index 00000000..ef832973 --- /dev/null +++ b/languages/javascript/src/shared/Gateway/Server.mjs @@ -0,0 +1,128 @@ +/* + * 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 + */ + +import Transport from "../Transport/index.mjs" + +const providers = {} +const interfaces = {} +const listeners = {} + +// TODO: add support for batch requests on server +// TODO: check JSON-RPC spec for batch behavior + +// Request that the app provide fulfillment of an method +export async function request(id, method, params, transforms) { + let result, error + try { + result = await getProviderResult(method, params) + } + catch (e) { + error = e + } + + const response = { + jsonrpc: '2.0', + id: id + } + + if (error) { + // todo: make sure this conforms to JSON-RPC schema for errors + response.error = error + } + else { + response.result = result + if (result === undefined) { + response.result = null + } + } + + Transport.send(response) +} + +// TODO: How do we know what order the params are in!? +// Need to implement this spec: +// https://github.com/rdkcentral/firebolt-apis/blob/feature/protocol/requirements/specifications/general/context-parameters.md +// Which ensures that we'll only have one (any name) or two (data & context) parameters. +export async function notify(method, params) { + if (listeners[method]) { + listeners[method](...Object.values(params)) + return + } + throw `Notification not implemented: ${method}` +} + +// Register a provider implementation with an interface name +export function provide(interfaceName, provider) { + providers[interfaceName] = provider +} + +// Register a notification listener with an event name +export function subscribe(event, callback) { + listeners[event] = callback +} + +export function unsubscribe(event) { + delete listeners[event] +} + +export function simulate(event, value) { + listeners[event](value) +} + +// TODO: consider renaming +export function registerProviderInterface(capability, _interface, method, parameters, response, focusable) { + interfaces[_interface] = interfaces[_interface] || { + capability, + name: _interface, + methods: [], + } + + interfaces[_interface].methods.push({ + name: method, + parameters, + response, + focusable + }) +} + + +async function getProviderResult(method, params={}) { + const split = method.split('.') + method = split.pop() + const interfaceName = split.join('.') + + if (providers[interfaceName]) { + if (providers[interfaceName][method]) { + // sort the params into an array based on the interface parameter order + const parameters = interfaces[interfaceName].methods.find(m => m.name === method).parameters.map(p => params[p]).filter(p => p !== undefined) + return await providers[interfaceName][method](...parameters) + } + throw `Method not implemented: ${method}` + } + throw `Interface not provided: ${interfaceName}` +} + + +export default { + request, + notify, + provide, + subscribe, + unsubscribe, + simulate +} diff --git a/languages/javascript/src/shared/Gateway/Unidirectional.mjs b/languages/javascript/src/shared/Gateway/Unidirectional.mjs new file mode 100644 index 00000000..c0bdb821 --- /dev/null +++ b/languages/javascript/src/shared/Gateway/Unidirectional.mjs @@ -0,0 +1,315 @@ +/* + * 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 + */ + +import mock from '../Transport/MockTransport.mjs' +import Queue from '../Transport/queue.mjs' +import Settings, { initSettings } from '../Settings/index.mjs' +import LegacyTransport from '../Transport/LegacyTransport.mjs' +import WebsocketTransport from '../Transport/WebsocketTransport.mjs' +import Results from '../Results/index.mjs' + +const LEGACY_TRANSPORT_SERVICE_NAME = 'com.comcast.BridgeObject_1' +let moduleInstance = null + +const isEventSuccess = (x) => + x && typeof x.event === 'string' && typeof x.listening === 'boolean' + +const win = typeof window !== 'undefined' ? window : {} + +export default class Transport { + constructor() { + this._promises = [] + this._transport = null + this._id = 1 + this._subscribers = [] + this._eventIds = {} + this._queue = new Queue() + this._deprecated = {} + this.isMock = false + } + + static registerDeprecatedMethod(module, method, alternative) { + Transport.get()._deprecated[ + module.toLowerCase() + '.' + method.toLowerCase() + ] = { + alternative: alternative || '', + } + } + + _endpoint() { + if (win.__firebolt && win.__firebolt.endpoint) { + return win.__firebolt.endpoint + } + return null + } + + constructTransportLayer() { + let transport + const endpoint = this._endpoint() + if ( + endpoint && + (endpoint.startsWith('ws://') || endpoint.startsWith('wss://')) + ) { + transport = new WebsocketTransport(endpoint) + transport.receive(this.receiveHandler.bind(this)) + } else if ( + typeof win.ServiceManager !== 'undefined' && + win.ServiceManager && + win.ServiceManager.version + ) { + // Wire up the queue + transport = this._queue + // get the default bridge service, and flush the queue + win.ServiceManager.getServiceForJavaScript( + LEGACY_TRANSPORT_SERVICE_NAME, + (service) => { + if (LegacyTransport.isLegacy(service)) { + transport = new LegacyTransport(service) + } else { + transport = service + } + this.setTransportLayer(transport) + }, + ) + } else { + this.isMock = true + transport = mock + transport.receive(this.receiveHandler.bind(this)) + } + return transport + } + + setTransportLayer(tl) { + this._transport = tl + this._queue.flush(tl) + } + + static request(method, params, transforms) { + /** Transport singleton across all SDKs to keep single id map */ + console.dir(method) + + const module = method.split('.')[0] + method = method.split('.')[1] + + if (method.match(/^on[A-Z]/) && params.listen !== undefined) { + const { id, promise } = Transport.get()._sendAndGetId(module, method, params, transforms) + const notifier = `${module}.${method.charAt(2).toLowerCase() + method.substring(3)}` + if (params.listen) { + Transport.get()._eventIds[id] = notifier + } + else { + delete Transport.get()._eventIds[id] + delete Transport.get()._subscribers[notifier] + } + return promise + } + else { + return Transport.get()._send(module, method, params, transforms) + } + + } + + static subscribe(method, listener) { + const module = method.split('.')[0] + method = method.split('.')[1] + const notifier = `${module}.${method}` + Transport.get()._subscribers[notifier] = Transport.get()._subscribers[notifier] || [] + Transport.get()._subscribers[notifier].push(listener) + return + } + + static unsubscribe(method) { + // TODO: implement + } + + static simulate(method, value) { + console.log(`simulate(${method})`) + const module = method.split('.')[0] + method = method.split('.')[1] + const notifier = `${module}.${method}` + Transport.get()._subscribers[notifier].forEach(callback => callback(value)) + } + + static deprecate() { + // TODO: implement + } + + _send(module, method, params, transforms) { + if (Array.isArray(module) && !method && !params) { + return this._batch(module) + } else { + return this._sendAndGetId(module, method, params, transforms).promise + } + } + + _sendAndGetId(module, method, params, transforms) { + const { promise, json, id } = this._processRequest( + module, + method, + params, + transforms, + ) + const msg = JSON.stringify(json) + if (Settings.getLogLevel() === 'DEBUG') { + console.debug('Sending message to transport: ' + msg) + } + this._transport.send(msg) + + return { id, promise } + } + + _batch(requests) { + const results = [] + const json = [] + + requests.forEach(({ module, method, params, transforms }) => { + const result = this._processRequest(module, method, params, transforms) + results.push({ + promise: result.promise, + id: result.id, + }) + json.push(result.json) + }) + + const msg = JSON.stringify(json) + if (Settings.getLogLevel() === 'DEBUG') { + console.debug('Sending message to transport: ' + msg) + } + this._transport.send(msg) + + return results + } + + _processRequest(module, method, params, transforms) { + const p = this._addPromiseToQueue(module, method, params, transforms) + const json = this._createRequestJSON(module, method, params) + + const result = { + promise: p, + json: json, + id: this._id, + } + + this._id++ + + return result + } + + _createRequestJSON(module, method, params) { + return { + jsonrpc: '2.0', + method: module.toLowerCase() + '.' + method, + params: params, + id: this._id, + } + } + + _addPromiseToQueue(module, method, params, transforms) { + return new Promise((resolve, reject) => { + this._promises[this._id] = {} + this._promises[this._id].promise = this + this._promises[this._id].resolve = resolve + this._promises[this._id].reject = reject + this._promises[this._id].transforms = transforms + + const deprecated = + this._deprecated[module.toLowerCase() + '.' + method.toLowerCase()] + if (deprecated) { + console.warn( + `WARNING: ${module}.${method}() is deprecated. ` + + deprecated.alternative, + ) + } + }) + } + + /** + * If we have a global transport, use that. Otherwise, use the module-scoped transport instance. + * @returns {Transport} + */ + static get() { + /** Set up singleton and initialize it */ + win.__firebolt = win.__firebolt || {} + if (win.__firebolt.transport == null && moduleInstance == null) { + const transport = new Transport() + transport.init() + if (transport.isMock) { + /** We should use the mock transport built with the SDK, not a global */ + moduleInstance = transport + } else { + win.__firebolt = win.__firebolt || {} + win.__firebolt.transport = transport + } + win.__firebolt.setTransportLayer = + transport.setTransportLayer.bind(transport) + } + return win.__firebolt.transport ? win.__firebolt.transport : moduleInstance + } + + receiveHandler(message) { + if (Settings.getLogLevel() === 'DEBUG') { + console.debug('Received message from transport: ' + message) + } + const json = JSON.parse(message) + const p = this._promises[json.id] + + if (p) { + if (json.error) p.reject(json.error) + else { + // Do any module-specific transforms on the result + let result = json.result + + if (p.transforms) { + if (Array.isArray(json.result)) { + result = result.map((x) => Results.transform(x, p.transforms)) + } else { + result = Results.transform(result, p.transforms) + } + } + + p.resolve(result) + } + delete this._promises[json.id] + } + + // event responses need to be emitted, even after the listen call is resolved + if (this._eventIds[json.id] && !isEventSuccess(json.result)) { + this._subscribers[this._eventIds[json.id]].forEach(callback => callback(json.result)) + } + } + + init() { + initSettings({}, { log: true }) + this._queue.receive(this.receiveHandler.bind(this)) + if (win.__firebolt) { + if (win.__firebolt.mockTransportLayer === true) { + this.isMock = true + this.setTransportLayer(mock) + } else if (win.__firebolt.getTransportLayer) { + this.setTransportLayer(win.__firebolt.getTransportLayer()) + } + } + if (this._transport == null) { + this._transport = this.constructTransportLayer() + } + } +} +win.__firebolt = win.__firebolt || {} +win.__firebolt.setTransportLayer = (transport) => { + Transport.get().setTransportLayer(transport) +} diff --git a/languages/javascript/src/shared/Gateway/index.mjs b/languages/javascript/src/shared/Gateway/index.mjs new file mode 100644 index 00000000..256e3017 --- /dev/null +++ b/languages/javascript/src/shared/Gateway/index.mjs @@ -0,0 +1,19 @@ +/* + * 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 + */ + +export {default} from './Bidirectional.mjs' \ No newline at end of file diff --git a/languages/javascript/src/shared/Prop/MockProps.mjs b/languages/javascript/src/shared/Prop/MockProps.mjs index fd14e918..4aaf6e76 100644 --- a/languages/javascript/src/shared/Prop/MockProps.mjs +++ b/languages/javascript/src/shared/Prop/MockProps.mjs @@ -1,4 +1,3 @@ -import Mock from "../Transport/MockTransport.mjs" import router from "./Router.mjs" const mocks = {} @@ -16,7 +15,6 @@ function mock(module, method, params, value, contextParameterCount, def) { } else if (type === "setter") { mocks[key] = value - Mock.event(module, `${method}Changed`, { value }) return null } } diff --git a/languages/javascript/src/shared/Prop/index.mjs b/languages/javascript/src/shared/Prop/index.mjs index 72f1e391..45195a33 100644 --- a/languages/javascript/src/shared/Prop/index.mjs +++ b/languages/javascript/src/shared/Prop/index.mjs @@ -1,4 +1,4 @@ -import Transport from "../Transport/index.mjs" +import Gateway from "../Gateway/index.mjs" import Events from "../Events/index.mjs" import router from "./Router.mjs" @@ -7,7 +7,7 @@ function prop(moduleName, key, params, callbackOrValue = undefined, immutable, r const type = router(params, callbackOrValue, contextParameterCount) if (type === "getter") { - return Transport.send(moduleName, key, params) + return Gateway.request(moduleName + '.' + key, params) } else if (type === "subscriber") { // subscriber @@ -24,7 +24,7 @@ function prop(moduleName, key, params, callbackOrValue = undefined, immutable, r if (readonly) { throw new Error('Cannot set a value to a readonly property') } - return Transport.send(moduleName, 'set' + key[0].toUpperCase() + key.substring(1), Object.assign({ + return Gateway.request(moduleName + '.set' + key[0].toUpperCase() + key.substring(1), Object.assign({ value: callbackOrValue }, params)) } diff --git a/languages/javascript/src/shared/ProvideManager/index.mjs b/languages/javascript/src/shared/ProvideManager/index.mjs index 68ce6e9c..8ab79cb4 100644 --- a/languages/javascript/src/shared/ProvideManager/index.mjs +++ b/languages/javascript/src/shared/ProvideManager/index.mjs @@ -16,18 +16,24 @@ * SPDX-License-Identifier: Apache-2.0 */ -import Transport from '../Transport/index.mjs' import Events from '../Events/index.mjs' +import Gateway from '../Gateway/index.mjs' + +// NOTE: this class only used by Unidirectional SDKs Gateway/index.mjs provides this capability to Bidirectional SDKs const providerInterfaces = {} -export const registerProviderInterface = (capability, module, methods) => { - if (providerInterfaces[capability]) { - throw `Capability ${capability} has multiple provider interfaces registered.` +export const registerProviderInterface = (capability, _interface, method, params, response, focusable) => { + if (!providerInterfaces[capability]) { + providerInterfaces[capability] = [] } - methods.forEach(m => m.name = `${module}.${m.name}`) - providerInterfaces[capability] = methods.concat() + providerInterfaces[capability].push({ + name: `${_interface}.${method}`, + parameters: params && params.length, + response, + focusable + }) } const provide = function(capability, provider) { @@ -42,7 +48,7 @@ const provide = function(capability, provider) { } if (!iface) { - throw "Ignoring unknown provider capability." + throw "Ignoring unknown provider interface." } // make sure every interfaced method exists in the providers methods list @@ -52,6 +58,8 @@ const provide = function(capability, provider) { throw `Provider that does not fully implement ${capability}:\n\t${iface.map(m=>m.name.split('.').pop()).join('\n\t')}` } +// Gateway.provide(iface[0].name.split('.')[0], provider) + iface.forEach(imethod => { const parts = imethod.name.split('.') const method = parts.pop(); @@ -64,7 +72,9 @@ const provide = function(capability, provider) { Events.listen(module, `request${method.charAt(0).toUpperCase() + method.substr(1)}`, function (request) { const providerCallArgs = [] - + + console.dir(request) + // only pass in parameters object if schema exists if (imethod.parameters) { providerCallArgs.push(request.parameters) @@ -82,7 +92,7 @@ const provide = function(capability, provider) { // only pass in the focus handshake if needed if (imethod.focus) { session.focus = () => { - Transport.send(module, `${method}Focus`, { + Gateway.request(`${module}.${method}Focus`, { correlationId: request.correlationId }) } @@ -103,7 +113,7 @@ const provide = function(capability, provider) { response.error.data = JSON.parse(JSON.stringify(error.data)) } - Transport.send(module, `${method}Error`, response) + Gateway.request(`${module}.${method}Error`, response) } try { @@ -118,7 +128,7 @@ const provide = function(capability, provider) { response.result = result } - Transport.send(module, `${method}Response`, response) + Gateway.request(`${module}.${method}Response`, response) }).catch(err => handleError(err)) } catch(error) { diff --git a/languages/javascript/src/shared/Results/index.mjs b/languages/javascript/src/shared/Results/index.mjs index 018381e5..4c9069e5 100644 --- a/languages/javascript/src/shared/Results/index.mjs +++ b/languages/javascript/src/shared/Results/index.mjs @@ -1,4 +1,4 @@ -import Transport from "../Transport/index.mjs" +import Gateway from "../Gateway/index.mjs" /* methods = Map { diff --git a/languages/javascript/src/shared/TemporalSet/index.mjs b/languages/javascript/src/shared/TemporalSet/index.mjs index 7fa9ee6e..1cbbed79 100644 --- a/languages/javascript/src/shared/TemporalSet/index.mjs +++ b/languages/javascript/src/shared/TemporalSet/index.mjs @@ -1,21 +1,7 @@ -import Transport from "../Transport/index.mjs" -import Events from "../Events/index.mjs" +import Gateway from "../Gateway/index.mjs" const sessions = {} -let eventEmitterInitialized = false -const eventHandler = (module, event, value) => { - const session = getSession(module, method) - if (session) { - if (event === session.addName && session.add) { - session.add(value) - } - else if (event === session.removeName && session.remove) { - session.remove(value) - } - } -} - function getSession(module, method) { return sessions[module.toLowerCase() + '.' + method] } @@ -29,14 +15,9 @@ function stopSession(module, method) { delete sessions[module.toLowerCase() + '.' + method] } -function start(module, method, addName, removeName, params, add, remove, timeout, transforms) { +async function start(module, method, addName, removeName, params, add, remove, timeout, transforms) { let session = getSession(module, method) - if (!eventEmitterInitialized) { - Transport.addEventEmitter(eventHandler) - eventEmitterInitialized = true - } - if (session) { throw `Error: only one ${module}.${method} operation may be in progress at a time. Call stop${method.charAt(0).toUpperCase() + method.substr(1)} on previous ${method} first.` } @@ -50,114 +31,87 @@ function start(module, method, addName, removeName, params, add, remove, timeout const requests = [ { - module: module, - method: method, + method: `${module}.${method}`, params: params, transforms: transforms } ] requests.push({ - module: module, - method: addName, + method: `${module}.${addName}`, params: { listen: true }, transforms: transforms }) + Gateway.subscribe(`${module}.${addName}`, (item) => { + session.add(item) + }) + if (remove) { requests.push({ - module: module, - method: removeName, + method: `${module}.${removeName}`, params: { listen: true }, transforms: transforms }) + Gateway.subscribe(`${module}.${removeName}`, (item) => { + session.remove(item) + }) } - const results = Transport.send(requests) + const results = await Gateway.batch(requests) - session.id = results[0].id - session.addRpcId = results[1].id session.add = add session.remove = remove session.addName = addName session.removeName = removeName - results[0].promise.then( items => { - add && items && items.forEach(item => add(item)) - }) - - results[1].promise.then( id => { - // clear it out if the session is already canceled - if (!session.id) { - Events.clear(id) - } - else { - session.addListenerId = id - } - }) - - if (remove) { - session.removeRpcId = results[2].id - results[2].promise.then( id => { - // clear it out if the session is already canceled - if (!session.id) { - Events.clear(id) - } - else { - session.removeListenerId = id - } - }) - } if (add) { + results[0] && results[0].forEach(item => add(item)) + return { stop: () => { const requests = [ { - module: module, - method: `stop${method.charAt(0).toUpperCase() + method.substr(1)}`, - params: { - correlationId: session.id - } + method: `${module}.stop${method.charAt(0).toUpperCase() + method.substr(1)}`, + params: {} }, { - module: module, - method: addName, + method: `${module}.${addName}`, params: { listen: false } } ] - + + Gateway.unsubscribe(`${module}.${addName}`) + if (remove) { requests.push({ - module: module, - method: removeName, + method: `${module}.${removeName}`, params: { listen: false } }) } - Transport.send(requests) + + Gateway.unsubscribe(`${module}.${removeName}`) + Gateway.batch(requests) stopSession(module, method) } } } else if (timeout) { - return results[0].promise.then(results => { - stopSession(module, method) - return results.shift() - }) + stopSession(module, method) + return results[0].shift() } else { - return results[0].promise.then(results => { - stopSession(module, method) - return results - }) + stopSession(module, method) + return Promise.resolve(results[0]) } } diff --git a/languages/javascript/src/shared/Transport/MockTransport.mjs b/languages/javascript/src/shared/Transport/MockTransport.mjs index ec55964b..1075a53f 100644 --- a/languages/javascript/src/shared/Transport/MockTransport.mjs +++ b/languages/javascript/src/shared/Transport/MockTransport.mjs @@ -16,6 +16,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +import Gateway from "../Gateway/index.mjs" const win = typeof window !== 'undefined' ? window : {} @@ -24,7 +25,6 @@ export const setMockListener = func => { listener = func } let mock const pending = [] -const eventMap = {} let callback let testHarness @@ -34,38 +34,35 @@ if (win.__firebolt && win.__firebolt.testHarness) { } function send(message) { - console.debug('Sending message to transport: ' + message) - let json = JSON.parse(message) - + const json = JSON.parse(message) // handle bulk sends if (Array.isArray(json)) { - json.forEach(j => send(JSON.stringify(j))) + json.forEach(json => send(JSON.stringify(json))) return } - let [module, method] = json.method.split('.') + if (json.method) { + let [module, method] = json.method.split('.') - if (testHarness && testHarness.onSend) { - testHarness.onSend(module, method, json.params, json.id) + if (testHarness && testHarness.onSend) { + testHarness.onSend(module, method, json.params, json.id) + } + + if (mock) + handle(json) + else + pending.push(json) } - - // store the ID of the first listen for each event - if (method.match(/^on[A-Z]/)) { - if (json.params.listen) { - eventMap[json.id] = module.toLowerCase() + '.' + method[2].toLowerCase() + method.substr(3) - } else { - Object.keys(eventMap).forEach(key => { - if (eventMap[key] === module.toLowerCase() + '.' + method[2].toLowerCase() + method.substr(3)) { - delete eventMap[key] - } - }) + else if (json.id !== undefined && requests[json.id]) { + const promise = requests[json.id] + if (json.result !== undefined) { + promise.resolve(json.result) + } + else { + promise.reject(json.error) } } - if (mock) - handle(json) - else - pending.push(json) } function handle(json) { @@ -96,22 +93,42 @@ function receive(_callback) { if (testHarness && (typeof testHarness.initialize === 'function')) { testHarness.initialize({ - emit: event, + emit: (module, method, value) => { + Gateway.simulate(`${module}.${method}`, value) + }, listen: function(...args) { listener(...args) }, }) } } function event(module, event, value) { - const listener = Object.entries(eventMap).find(([k, v]) => v.toLowerCase() === module.toLowerCase() + '.' + event.toLowerCase()) - if (listener) { - let message = JSON.stringify({ - jsonrpc: '2.0', - id: parseInt(listener[0]), - result: value - }) - callback(message) - } + callback(JSON.stringify({ + jsonrpc: '2.0', + method: `${module}.${event}`, + params: [ + { + name: 'value', + value: value + } + ] + })) +} + +let id = 0 +const requests = [] + +function request(method, params) { + const promise = new Promise( (resolve, reject) => { + requests[id] = { resolve, reject } + }) + callback(JSON.stringify({ + jsonrpc: '2.0', + id: id, + method: `${method}`, + params: params + })) + + return promise } function dotGrab(obj = {}, key) { @@ -134,7 +151,11 @@ function getResult(method, params) { } if (typeof api === 'function') { - return params == null ? api() : api(params) + let result = params == null ? api() : api(params) + if (result === undefined) { + result = null + } + return result } else return api } @@ -148,6 +169,7 @@ export function setMockResponses(m) { export default { send: send, receive: receive, - event: event + event: event, + request: request } diff --git a/languages/javascript/src/shared/Transport/WebsocketTransport.mjs b/languages/javascript/src/shared/Transport/WebsocketTransport.mjs index f41e4648..f1e0d7cd 100644 --- a/languages/javascript/src/shared/Transport/WebsocketTransport.mjs +++ b/languages/javascript/src/shared/Transport/WebsocketTransport.mjs @@ -9,14 +9,14 @@ export default class WebsocketTransport { this._callbacks = [] } - send (msg) { + send (message) { this._connect() if (this._connected) { - this._ws.send(msg) + this._ws.send(message) } else { if (this._queue.length < MAX_QUEUED_MESSAGES) { - this._queue.push(msg) + this._queue.push(message) } } } diff --git a/languages/javascript/src/shared/Transport/index.mjs b/languages/javascript/src/shared/Transport/index.mjs index e81255de..d400b4ac 100644 --- a/languages/javascript/src/shared/Transport/index.mjs +++ b/languages/javascript/src/shared/Transport/index.mjs @@ -16,254 +16,60 @@ * SPDX-License-Identifier: Apache-2.0 */ -import mock from './MockTransport.mjs' -import Queue from './queue.mjs' +import MockTransport from './MockTransport.mjs' import Settings, { initSettings } from '../Settings/index.mjs' -import LegacyTransport from './LegacyTransport.mjs' import WebsocketTransport from './WebsocketTransport.mjs' -import Results from '../Results/index.mjs' - -const LEGACY_TRANSPORT_SERVICE_NAME = 'com.comcast.BridgeObject_1' -let moduleInstance = null - -const isEventSuccess = x => x && (typeof x.event === 'string') && (typeof x.listening === 'boolean') const win = typeof window !== 'undefined' ? window : {} +win.__firebolt = win.__firebolt || {} -export default class Transport { - constructor () { - this._promises = [] - this._transport = null - this._id = 1 - this._eventEmitters = [] - this._eventIds = [] - this._queue = new Queue() - this._deprecated = {} - this.isMock = false - } - - static addEventEmitter (emitter) { - Transport.get()._eventEmitters.push(emitter) - } - - static registerDeprecatedMethod (module, method, alternative) { - Transport.get()._deprecated[module.toLowerCase() + '.' + method.toLowerCase()] = { - alternative: alternative || '' - } - } - - _endpoint () { - if (win.__firebolt && win.__firebolt.endpoint) { - return win.__firebolt.endpoint - } - return null - } - - constructTransportLayer () { - let transport - const endpoint = this._endpoint() - if (endpoint && (endpoint.startsWith('ws://') || endpoint.startsWith('wss://'))) { - transport = new WebsocketTransport(endpoint) - transport.receive(this.receiveHandler.bind(this)) - } else if ( - typeof win.ServiceManager !== 'undefined' && - win.ServiceManager && - win.ServiceManager.version - ) { - // Wire up the queue - transport = this._queue - // get the default bridge service, and flush the queue - win.ServiceManager.getServiceForJavaScript(LEGACY_TRANSPORT_SERVICE_NAME, service => { - if (LegacyTransport.isLegacy(service)) { - transport = new LegacyTransport(service) - } else { - transport = service - } - this.setTransportLayer(transport) - }) - } else { - this.isMock = true - transport = mock - transport.receive(this.receiveHandler.bind(this)) - } - return transport - } - - setTransportLayer (tl) { - this._transport = tl - this._queue.flush(tl) - } - - static send (module, method, params, transforms) { - /** Transport singleton across all SDKs to keep single id map */ - return Transport.get()._send(module, method, params, transforms) - } - - static listen(module, method, params, transforms) { - return Transport.get()._sendAndGetId(module, method, params, transforms) - } +initSettings({}, { log: true }) - _send (module, method, params, transforms) { - if (Array.isArray(module) && !method && !params) { - return this._batch(module) - } - else { - return this._sendAndGetId(module, method, params, transforms).promise - } - } +let implementation +let _callback - _sendAndGetId (module, method, params, transforms) { - const {promise, json, id } = this._processRequest(module, method, params, transforms) - const msg = JSON.stringify(json) - if (Settings.getLogLevel() === 'DEBUG') { - console.debug('Sending message to transport: ' + msg) - } - this._transport.send(msg) +export function send(json) { + implementation = getImplementation() - return { id, promise } + if (Settings.getLogLevel() === 'DEBUG') { + console.debug('Sending message to transport: \n' + JSON.stringify(json, { indent: '\t'})) } - _batch (requests) { - const results = [] - const json = [] - - requests.forEach( ({module, method, params, transforms}) => { - const result = this._processRequest(module, method, params, transforms) - results.push({ - promise: result.promise, - id: result.id - }) - json.push(result.json) - }) - - const msg = JSON.stringify(json) - if (Settings.getLogLevel() === 'DEBUG') { - console.debug('Sending message to transport: ' + msg) - } - this._transport.send(msg) + implementation.send(JSON.stringify(json)) +} - return results +export function receive(callback) { + if (implementation) { + implementation.receive(callback) } - - _processRequest (module, method, params, transforms) { - - const p = this._addPromiseToQueue(module, method, params, transforms) - const json = this._createRequestJSON(module, method, params) - - const result = { - promise: p, - json: json, - id: this._id - } - - this._id++ - - return result + else { + _callback = callback } +} - _createRequestJSON (module, method, params) { - return { jsonrpc: '2.0', method: module.toLowerCase() + '.' + method, params: params, id: this._id } +function getImplementation() { + if (implementation) { + return implementation } - - _addPromiseToQueue (module, method, params, transforms) { - return new Promise((resolve, reject) => { - this._promises[this._id] = {} - this._promises[this._id].promise = this - this._promises[this._id].resolve = resolve - this._promises[this._id].reject = reject - this._promises[this._id].transforms = transforms - - const deprecated = this._deprecated[module.toLowerCase() + '.' + method.toLowerCase()] - if (deprecated) { - console.warn(`WARNING: ${module}.${method}() is deprecated. ` + deprecated.alternative) - } - - // store the ID of the first listen for each event - // TODO: what about wild cards? - if (method.match(/^on[A-Z]/)) { - if (params.listen) { - this._eventIds.push(this._id) - } else { - this._eventIds = this._eventIds.filter(id => id !== this._id) - } - } - }) + + if (win.__firebolt.transport) { + implementation = win.__firebolt.transport } - - /** - * If we have a global transport, use that. Otherwise, use the module-scoped transport instance. - * @returns {Transport} - */ - static get () { - /** Set up singleton and initialize it */ - win.__firebolt = win.__firebolt || {} - if ((win.__firebolt.transport == null) && (moduleInstance == null)) { - const transport = new Transport() - transport.init() - if (transport.isMock) { - /** We should use the mock transport built with the SDK, not a global */ - moduleInstance = transport - } else { - win.__firebolt = win.__firebolt || {} - win.__firebolt.transport = transport - } - win.__firebolt.setTransportLayer = transport.setTransportLayer.bind(transport) - } - return win.__firebolt.transport ? win.__firebolt.transport : moduleInstance + else if (win.__firebolt.endpoint) { + implementation = new WebsocketTransport(win.__firebolt.endpoint) } - - receiveHandler (message) { - if (Settings.getLogLevel() === 'DEBUG') { - console.debug('Received message from transport: ' + message) - } - const json = JSON.parse(message) - const p = this._promises[json.id] - - if (p) { - if (json.error) p.reject(json.error) - else { - // Do any module-specific transforms on the result - let result = json.result - - if (p.transforms) { - if (Array.isArray(json.result)) { - result = result.map(x => Results.transform(x, p.transforms)) - } - else { - result = Results.transform(result, p.transforms) - } - } - - p.resolve(result) - } - delete this._promises[json.id] - } - - // event responses need to be emitted, even after the listen call is resolved - if (this._eventIds.includes(json.id) && !isEventSuccess(json.result)) { - this._eventEmitters.forEach(emit => { - emit(json.id, json.result) - }) - } + else { + implementation = MockTransport } + + win.__firebolt.transport = implementation + implementation.receive(_callback) + _callback = undefined - init () { - initSettings({}, { log: true }) - this._queue.receive(this.receiveHandler.bind(this)) - if (win.__firebolt) { - if (win.__firebolt.mockTransportLayer === true) { - this.isMock = true - this.setTransportLayer(mock) - } else if (win.__firebolt.getTransportLayer) { - this.setTransportLayer(win.__firebolt.getTransportLayer()) - } - } - if (this._transport == null) { - this._transport = this.constructTransportLayer() - } - } -} -win.__firebolt = win.__firebolt || {} -win.__firebolt.setTransportLayer = transport => { - Transport.get().setTransportLayer(transport) + return implementation } + +export default { + send, + receive +} \ No newline at end of file diff --git a/languages/javascript/src/shared/Transport/queue.mjs b/languages/javascript/src/shared/Transport/queue.mjs index 513b52e1..bdfe448d 100644 --- a/languages/javascript/src/shared/Transport/queue.mjs +++ b/languages/javascript/src/shared/Transport/queue.mjs @@ -22,8 +22,8 @@ export default class Queue { this._queue = [] } - send (json) { - this._queue.push(json) + send (message) { + this._queue.push(message) } receive (_callback) { diff --git a/languages/javascript/templates/codeblocks/provider.js b/languages/javascript/templates/codeblocks/provider.js index 08adac03..ed0e64c3 100644 --- a/languages/javascript/templates/codeblocks/provider.js +++ b/languages/javascript/templates/codeblocks/provider.js @@ -1,3 +1,5 @@ ${interface} +${if.unidirectional} function provide(capability: '${capability}', provider: ${provider} | object): Promise +${end.if.unidirectional} \ No newline at end of file diff --git a/languages/javascript/templates/codeblocks/provider.subscribe.mjs b/languages/javascript/templates/codeblocks/provider.subscribe.mjs new file mode 100644 index 00000000..2acad664 --- /dev/null +++ b/languages/javascript/templates/codeblocks/provider.subscribe.mjs @@ -0,0 +1,2 @@ + +function provide(capability: '${capability}', provider: ${provider} | object): Promise \ No newline at end of file diff --git a/languages/javascript/templates/codeblocks/subscriber.js b/languages/javascript/templates/codeblocks/subscriber.js index e8071187..ad4f8509 100644 --- a/languages/javascript/templates/codeblocks/subscriber.js +++ b/languages/javascript/templates/codeblocks/subscriber.js @@ -2,4 +2,4 @@ * Subscriber: ${method.summary} * */ -function ${method.alternative}(subscriber: (${method.result.name}: ${method.result.type}) => void): Promise +function ${method.alternative}(subscriber: (${event.result.name}: ${event.result.type}) => void): Promise diff --git a/languages/javascript/templates/codeblocks/transform.mjs b/languages/javascript/templates/codeblocks/transform.mjs new file mode 100644 index 00000000..e2248d46 --- /dev/null +++ b/languages/javascript/templates/codeblocks/transform.mjs @@ -0,0 +1,3 @@ +.then( result => { + return Results.transform(result, ${transforms}) +}) \ No newline at end of file diff --git a/languages/javascript/templates/declarations/event.js b/languages/javascript/templates/declarations/event.js index ca1ac977..f52fa2a9 100644 --- a/languages/javascript/templates/declarations/event.js +++ b/languages/javascript/templates/declarations/event.js @@ -5,7 +5,7 @@ * @param {Function} callback ${if.deprecated} * @deprecated ${method.deprecation} ${end.if.deprecated} */ - function listen(event: '${event.name}'${if.context}, ${event.signature.params}${end.if.context}, callback: (data: ${method.result.type}) => void): Promise + function listen(event: '${event.name}'${if.context}, ${event.signature.params}${end.if.context}, callback: (data: ${event.result.type}) => void): Promise /** * ${method.summary} @@ -15,4 +15,4 @@ ${end.if.deprecated} */ * @param {Function} callback ${if.deprecated} * @deprecated ${method.deprecation} ${end.if.deprecated} */ -function once(event: '${event.name}'${if.context}, ${event.signature.params}${end.if.context}, callback: (data: ${method.result.type}) => void): Promise +function once(event: '${event.name}'${if.context}, ${event.signature.params}${end.if.context}, callback: (data: ${event.result.type}) => void): Promise diff --git a/languages/javascript/templates/declarations/registration.js b/languages/javascript/templates/declarations/registration.js new file mode 100644 index 00000000..ec7cc00c --- /dev/null +++ b/languages/javascript/templates/declarations/registration.js @@ -0,0 +1,6 @@ + /** + * ${method.summary} + * +${method.params.annotations}${if.deprecated} * @deprecated ${method.deprecation} +${end.if.deprecated} */ +function ${method.name}(provider: ${method.interface}): Promise diff --git a/languages/javascript/templates/imports/provider.mjs b/languages/javascript/templates/imports/provider.mjs index b3ea7b4f..37be97f3 100644 --- a/languages/javascript/templates/imports/provider.mjs +++ b/languages/javascript/templates/imports/provider.mjs @@ -1,2 +1 @@ -import ProvideManager from '../ProvideManager/index.mjs' -import { registerProviderInterface } from '../ProvideManager/index.mjs' +import { registerProviderInterface } from '../Gateway/Server.mjs' diff --git a/languages/javascript/templates/imports/rpc.mjs b/languages/javascript/templates/imports/rpc.mjs index af459a1e..38c9fe2c 100644 --- a/languages/javascript/templates/imports/rpc.mjs +++ b/languages/javascript/templates/imports/rpc.mjs @@ -1 +1 @@ -import Transport from '../Transport/index.mjs' +import Gateway from '../Gateway/index.mjs' diff --git a/languages/javascript/templates/imports/unidirectional-provider.mjs b/languages/javascript/templates/imports/unidirectional-provider.mjs new file mode 100644 index 00000000..b3ea7b4f --- /dev/null +++ b/languages/javascript/templates/imports/unidirectional-provider.mjs @@ -0,0 +1,2 @@ +import ProvideManager from '../ProvideManager/index.mjs' +import { registerProviderInterface } from '../ProvideManager/index.mjs' diff --git a/languages/javascript/templates/imports/unidirectional-rpc.mjs b/languages/javascript/templates/imports/unidirectional-rpc.mjs new file mode 100644 index 00000000..05456bae --- /dev/null +++ b/languages/javascript/templates/imports/unidirectional-rpc.mjs @@ -0,0 +1 @@ +import Gateway from '../Gateway/Unidirectional.mjs' diff --git a/languages/javascript/templates/initializations/deprecated.mjs b/languages/javascript/templates/initializations/deprecated.mjs index 737d1c25..2d6b3298 100644 --- a/languages/javascript/templates/initializations/deprecated.mjs +++ b/languages/javascript/templates/initializations/deprecated.mjs @@ -1 +1 @@ -Transport.registerDeprecatedMethod('${info.title}', '${method.name}', 'Use ${method.alternative} instead.') +Gateway.deprecate('${info.title}.${method.name}', 'Use ${method.alternative} instead.') diff --git a/languages/javascript/templates/initializations/provider.mjs b/languages/javascript/templates/initializations/provider.mjs index b113d236..9416fb09 100644 --- a/languages/javascript/templates/initializations/provider.mjs +++ b/languages/javascript/templates/initializations/provider.mjs @@ -1 +1 @@ -registerProviderInterface('${capability}', '${info.title}', ${interface}) +registerProviderInterface('${capability}', '${interface}', '${method.name}', ${method.params.array}, ${method.response}, ${method.focusable}) diff --git a/languages/javascript/templates/interfaces/default.mjs b/languages/javascript/templates/interfaces/default.mjs index 63f63093..1d8a2365 100644 --- a/languages/javascript/templates/interfaces/default.mjs +++ b/languages/javascript/templates/interfaces/default.mjs @@ -1 +1 @@ - ${method.name}(${method.signature.params}, session: ProviderSession): Promise<${method.result.type}> + ${method.name}(${method.signature.params}${if.unidirectional}, session: ProviderSession${end.if.unidirectional}): Promise<${method.result.type}> diff --git a/languages/javascript/templates/interfaces/focusable.mjs b/languages/javascript/templates/interfaces/focusable.mjs index b81737b1..60a3b2c2 100644 --- a/languages/javascript/templates/interfaces/focusable.mjs +++ b/languages/javascript/templates/interfaces/focusable.mjs @@ -1 +1 @@ - ${method.name}(${method.signature.params}, session: FocusableProviderSession): Promise<${method.result.type}> + ${method.name}(${method.signature.params}${if.unidirectional}, session: FocusableProviderSession${end.if.unidirectional}): Promise<${method.result.type}> diff --git a/languages/javascript/templates/methods/calls-metrics.js b/languages/javascript/templates/methods/calls-metrics.js index 138240c2..2fa8a48b 100644 --- a/languages/javascript/templates/methods/calls-metrics.js +++ b/languages/javascript/templates/methods/calls-metrics.js @@ -2,9 +2,7 @@ import { ${method.name} as log${method.Name} } from '../Metrics/index.mjs' function ${method.name}(${method.params.list}) { - const transforms = ${method.transforms} - - const p = Transport.send('${info.title}', '${method.name}', { ${method.params.list} }, transforms) + const p = Gateway.request('${info.title}.${method.name}', { ${method.params.list} })${method.transform} p.then(_ => { setTimeout(_ => { diff --git a/languages/javascript/templates/methods/default.js b/languages/javascript/templates/methods/default.js index 0c7f799b..c6e49ede 100644 --- a/languages/javascript/templates/methods/default.js +++ b/languages/javascript/templates/methods/default.js @@ -1,7 +1,4 @@ function ${method.name}(${method.params.list}) { - - const transforms = ${method.transforms} - - return Transport.send('${info.title}', '${method.name}', { ${method.params.list} }, transforms) + return Gateway.request('${info.title}.${method.name}', { ${method.params.list} })${method.transform} } \ No newline at end of file diff --git a/languages/javascript/templates/methods/polymorphic-pull.js b/languages/javascript/templates/methods/polymorphic-pull.js index 8cbfa308..0bbe1493 100644 --- a/languages/javascript/templates/methods/polymorphic-pull.js +++ b/languages/javascript/templates/methods/polymorphic-pull.js @@ -18,7 +18,7 @@ function ${method.name} (data) { correlationId: request.correlationId, result: result } - Transport.send('${info.title}', '${method.name}', params).catch(error => { + Gateway.request('${info.title}.${method.name}', params).catch(error => { const msg = typeof error === 'string' ? error : error.message || 'Unknown Error' console.error(`Failed to send ${method.name} pull response through Transport Layer: ${msg}`) }) @@ -34,6 +34,6 @@ function ${method.name} (data) { }) } else { - return Transport.send('${info.title}', '${method.name}', { correlationId: null, result: data }) + return Gateway.request('${info.title}.${method.name}', { correlationId: null, result: data }) } } \ No newline at end of file diff --git a/languages/javascript/templates/methods/polymorphic-reducer.js b/languages/javascript/templates/methods/polymorphic-reducer.js index db07747f..4789f264 100644 --- a/languages/javascript/templates/methods/polymorphic-reducer.js +++ b/languages/javascript/templates/methods/polymorphic-reducer.js @@ -1,11 +1,9 @@ function ${method.name}(${method.params.list}) { - const transforms = ${method.transforms} - if (arguments.length === 1 && Array.isArray(arguments[0])) { - return Transport.send('${info.title}', '${method.name}', arguments[0], transforms) + return Gateway.request('${info.title}.${method.name}', arguments[0])${method.transform} } else { - return Transport.send('${info.title}', '${method.name}', { ${method.params.list} }, transforms) + return Gateway.request('${info.title}.${method.name}', { ${method.params.list} })${method.transform} } } \ No newline at end of file diff --git a/languages/javascript/templates/methods/registration.js b/languages/javascript/templates/methods/registration.js new file mode 100644 index 00000000..8359b68f --- /dev/null +++ b/languages/javascript/templates/methods/registration.js @@ -0,0 +1,5 @@ + +function ${method.name}(provider) { + Gateway.provide('${method.interface}', provider) + return Gateway.request('${method.rpc.name}', { enabled: true } ) + } \ No newline at end of file diff --git a/languages/javascript/templates/methods/temporal-set.js b/languages/javascript/templates/methods/temporal-set.js index ab456d4a..c17dc032 100644 --- a/languages/javascript/templates/methods/temporal-set.js +++ b/languages/javascript/templates/methods/temporal-set.js @@ -10,8 +10,6 @@ function ${method.name}(...args) { else if (typeof args[args.length-1] === 'number') { timeout = args.pop() } - - const transforms = ${method.transforms} - return TemporalSet.start('${info.title}', '${method.name}', '${method.temporalset.add}', '${method.temporalset.remove}', args, add, remove, timeout, transforms) + return TemporalSet.start('${info.title}', '${method.name}', '${method.temporalset.add}', '${method.temporalset.remove}', args, add, remove, timeout) } \ No newline at end of file diff --git a/languages/javascript/templates/modules/index.mjs b/languages/javascript/templates/modules/index.mjs index 826ed7ef..c01856e6 100644 --- a/languages/javascript/templates/modules/index.mjs +++ b/languages/javascript/templates/modules/index.mjs @@ -26,7 +26,7 @@ export default { /* ${EVENTS_ENUM} */ - /* ${ENUMS} */ + /* ${ENUM_IMPLEMENTATIONS} */ /* ${METHOD_LIST} */ } \ No newline at end of file diff --git a/languages/javascript/templates/schemas/index.mjs b/languages/javascript/templates/schemas/index.mjs new file mode 100644 index 00000000..8c2aed50 --- /dev/null +++ b/languages/javascript/templates/schemas/index.mjs @@ -0,0 +1,27 @@ +/* + * 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 + */ + +/* ${IMPORTS} */ + +/* ${INITIALIZATION} */ + +export default { + + /* ${ENUM_IMPLEMENTATIONS} */ + + } \ No newline at end of file diff --git a/languages/javascript/templates/sdk/Gateway/index.mjs b/languages/javascript/templates/sdk/Gateway/index.mjs new file mode 100644 index 00000000..2ee1a3ab --- /dev/null +++ b/languages/javascript/templates/sdk/Gateway/index.mjs @@ -0,0 +1,26 @@ +/* + * 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 + */ + + +${if.bidirectional} +export {default} from './Bidirectional.mjs' +${end.if.bidirectional} + +${if.unidirectional} +export {default} from './Unidirectional.mjs' +${end.if.unidirectional} diff --git a/languages/javascript/templates/sections/provider-interfaces.js b/languages/javascript/templates/sections/provider-interfaces.js index 0b77c87d..05fe0f7e 100644 --- a/languages/javascript/templates/sections/provider-interfaces.js +++ b/languages/javascript/templates/sections/provider-interfaces.js @@ -1,5 +1,4 @@ -// Provider Interfaces - +// Provider Interfaces ${if.unidirectional} interface ProviderSession { correlationId(): string // Returns the correlation id of the current provider session } @@ -7,5 +6,6 @@ interface ProviderSession { interface FocusableProviderSession extends ProviderSession { focus(): Promise // Requests that the provider app be moved into focus to prevent a user experience } +${end.if.unidirectional} ${providers.list} \ No newline at end of file diff --git a/languages/javascript/templates/types/enum.mjs b/languages/javascript/templates/types/enum-implementation.mjs similarity index 100% rename from languages/javascript/templates/types/enum.mjs rename to languages/javascript/templates/types/enum-implementation.mjs diff --git a/languages/javascript/templates/types/namespace.mjs b/languages/javascript/templates/types/namespace.mjs new file mode 100644 index 00000000..f5894dc9 --- /dev/null +++ b/languages/javascript/templates/types/namespace.mjs @@ -0,0 +1 @@ +${if.namespace.notsame}${parent.Title}.${end.if.namespace.notsame} \ No newline at end of file diff --git a/package.json b/package.json index 6ae1731a..49dca724 100644 --- a/package.json +++ b/package.json @@ -12,12 +12,11 @@ "prepare:setup": "npx mkdirp ./dist/docs ./build/docs/markdown ./build/docs/wiki ./build/sdk/javascript/src", "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js --config=jest.config.json --detectOpenHandles", "build": "npm run validate && npm run build:docs && npm run build:sdk", - "validate": "node ./src/cli.mjs validate --input ./test/openrpc --schemas test/schemas --transformations && npm run build:openrpc && node ./src/cli.mjs validate --input ./build/sdk-open-rpc.json", - "build:openrpc": "node ./src/cli.mjs openrpc --input ./test --template ./src/openrpc-template.json --output ./build/sdk-open-rpc.json --schemas test/schemas", - "build:sdk": "node ./src/cli.mjs sdk --input ./build/sdk-open-rpc.json --template ./test/sdk --output ./build/sdk/javascript/src --schemas test/schemas", - "build:d": "node ./src/cli.mjs declarations --input ./build/sdk-open-rpc.json --output ./dist/lib/sdk.d.ts --schemas src/schemas", - "build:docs": "node ./src/cli.mjs docs --input ./build/sdk-open-rpc.json --output ./build/docs/markdown --schemas test/schemas --as-path", - "build:wiki": "node ./src/cli.mjs docs --input ./build/sdk-open-rpc.json --output ./build/docs/wiki --schemas test/schemas", + "validate": "node ./src/cli.mjs validate --input ./test/openrpc --schemas test/schemas --transformations --bidirectional && npm run build:openrpc && node ./src/cli.mjs validate --input ./build/sdk-open-rpc.json", + "build:openrpc": "node ./src/cli.mjs openrpc --input ./test --template ./src/openrpc-template.json --server ./build/sdk-open-rpc.json --client ./build/sdk-app-open-rpc.json -schemas test/schemas", + "build:sdk": "node ./src/cli.mjs sdk --server ./build/sdk-open-rpc.json --client ./build/sdk-app-open-rpc.json --template ./test/sdk --output ./build/sdk/javascript/src --schemas test/schemas", + "build:docs": "node ./src/cli.mjs docs --server ./build/sdk-open-rpc.json --client ./build/sdk-app-open-rpc.json --output ./build/docs/markdown --schemas test/schemas --as-path", + "build:wiki": "node ./src/cli.mjs docs --server ./build/sdk-open-rpc.json --client ./build/sdk-app-open-rpc.json --output ./build/docs/wiki --schemas test/schemas", "dist": "npm run validate && npm run build:sdk && npm run build:docs && npm run test", "prepare": "husky install" }, diff --git a/src/cli.mjs b/src/cli.mjs index 9cb60368..b2a3aa38 100755 --- a/src/cli.mjs +++ b/src/cli.mjs @@ -5,6 +5,7 @@ import sdk from './sdk/index.mjs' import docs from './docs/index.mjs' import openrpc from './openrpc/index.mjs' import validate from './validate/index.mjs' +import update from './update/index.mjs' import nopt from 'nopt' import path from 'path' @@ -13,6 +14,8 @@ import url from 'url' const knownOpts = { 'input': [path], 'output': [path], + 'client': [path], + 'server': [path], 'sdk': [path], 'schemas': [path, Array], 'template': [path], @@ -20,6 +23,7 @@ const knownOpts = { 'language': [path], 'examples': [path, Array], 'as-path': [Boolean], + 'bidirectional': [Boolean], 'pass-throughs': [Boolean] } @@ -49,20 +53,30 @@ const parsedArgs = Object.assign({}, defaults, nopt(knownOpts, shortHands, proce const task = process.argv[2] const signOff = () => console.log('\nThis has been a presentation of \x1b[38;5;202mFirebolt\x1b[0m \u{1F525} \u{1F529}\n') -if (task === 'slice') { - slice(parsedArgs).then(signOff) +try { + if (task === 'slice') { + await slice(parsedArgs).then(signOff) + } + else if (task === 'sdk') { + await sdk(parsedArgs).then(signOff) + } + else if (task === 'docs') { + await docs(parsedArgs).then(signOff) + } + else if (task === 'validate') { + await validate(parsedArgs).then(signOff) + } + else if (task === 'openrpc') { + await openrpc(parsedArgs).then(signOff) + } + else if (task === 'update') { + await update(parsedArgs).then(signOff) + } + else { + console.log("Invalid task: " + task) + } } -else if (task === 'sdk') { - sdk(parsedArgs).then(signOff) -} -else if (task === 'docs') { - docs(parsedArgs).then(signOff) -} -else if (task === 'validate') { - validate(parsedArgs).then(signOff) -} -else if (task === 'openrpc') { - openrpc(parsedArgs).then(signOff) -} else { - console.log("Invalid build type") +catch (error) { + console.dir(error) + throw error } \ No newline at end of file diff --git a/src/docs/index.mjs b/src/docs/index.mjs index fdf71acd..be45cc19 100755 --- a/src/docs/index.mjs +++ b/src/docs/index.mjs @@ -27,7 +27,8 @@ import { readJson } from '../shared/filesystem.mjs' /************************************************************************************************/ // destructure well-known cli args and alias to variables expected by script const run = async ({ - input: input, + server: server, + client: client, template: template, output: output, examples: examples, @@ -39,7 +40,7 @@ const run = async ({ // Important file/directory locations try { // Important file/directory locations - const packageJsonFile = path.join(path.dirname(input), '..', 'package.json') + const packageJsonFile = path.join(path.dirname(server), '..', 'package.json') const packageJson = await readJson(packageJsonFile) libraryName = packageJson.name || libraryName } @@ -50,7 +51,7 @@ const run = async ({ const config = await readJson(path.join(language, 'language.config.json')) - return macrofy(input, template, output, { + return macrofy(server, client, template, output, { headline: "documentation", outputDirectory: 'content', sharedTemplates: path.join(language, 'templates'), diff --git a/src/firebolt-openrpc.json b/src/firebolt-openrpc.json index e2c2abc8..c1a12ffd 100644 --- a/src/firebolt-openrpc.json +++ b/src/firebolt-openrpc.json @@ -1,5 +1,5 @@ { - "$id": "https://meta.comcast.com/firebolt/openrpc", + "$id": "https://meta.rdkcentral.com/firebolt/schemas/openrpc", "title": "FireboltOpenRPC", "oneOf": [ { @@ -696,6 +696,7 @@ "enum": [ "name", "x-response", + "x-response-name", "x-alternative", "x-since", "x-pulls-for", @@ -1010,6 +1011,12 @@ }, "x-provided-by": { "type": "string" + }, + "x-requestor": { + "type": "string" + }, + "x-push": { + "type": "boolean" } }, "if": { diff --git a/src/macrofier/engine.mjs b/src/macrofier/engine.mjs index 9a7823fc..8029ee84 100644 --- a/src/macrofier/engine.mjs +++ b/src/macrofier/engine.mjs @@ -29,9 +29,12 @@ import isString from 'crocks/core/isString.js' import predicates from 'crocks/predicates/index.js' const { isObject, isArray, propEq, pathSatisfies, propSatisfies } = predicates -import { isRPCOnlyMethod, isProviderInterfaceMethod, getProviderInterface, getPayloadFromEvent, providerHasNoParameters, isTemporalSetMethod, hasMethodAttributes, getMethodAttributes, isEventMethodWithContext, getSemanticVersion, getSetterFor, getProvidedCapabilities, isPolymorphicPullMethod, hasPublicAPIs, isAllowFocusMethod, hasAllowFocusMethods, createPolymorphicMethods, isExcludedMethod, isCallsMetricsMethod } from '../shared/modules.mjs' +import { isRPCOnlyMethod, isProviderInterfaceMethod, getProviderInterface, getPayloadFromEvent, providerHasNoParameters, isTemporalSetMethod, hasMethodAttributes, getMethodAttributes, isEventMethodWithContext, getSemanticVersion, getSetterFor, getProvidedCapabilities, isPolymorphicPullMethod, hasPublicAPIs, isAllowFocusMethod, hasAllowFocusMethods, isExcludedMethod, isCallsMetricsMethod, getProvidedInterfaces, getUnidirectionalProviderInterfaceName } from '../shared/modules.mjs' +import { extension, getNotifier, name as methodName, name, provides } from '../shared/methods.mjs' import isEmpty from 'crocks/core/isEmpty.js' -import { getPath as getJsonPath, getLinkedSchemaPaths, getSchemaConstraints, isSchema, localizeDependencies, isDefinitionReferencedBySchema, mergeAnyOf, mergeOneOf, getSafeEnumKeyName } from '../shared/json-schema.mjs' +import { getReferencedSchema, getLinkedSchemaPaths, getSchemaConstraints, isSchema, localizeDependencies, isDefinitionReferencedBySchema, getSafeEnumKeyName, getAllValuesForName } from '../shared/json-schema.mjs' + +import Types from './types.mjs' // util for visually debugging crocks ADTs const _inspector = obj => { @@ -42,28 +45,15 @@ const _inspector = obj => { } } -// getSchemaType(schema, module, options = { destination: 'file.txt', title: true }) -// getSchemaShape(schema, module, options = { name: 'Foo', destination: 'file.txt' }) -// getJsonType(schema, module, options = { name: 'Foo', prefix: '', descriptions: false, level: 0 }) -// getSchemaInstantiation(schema, module, options = {type: 'params' | 'result' | 'callback.params'| 'callback.result' | 'callback.response'}) - -let types = { - getSchemaShape: () => null, - getSchemaType: () => null -} - let config = { copySchemasIntoModules: false, extractSubSchemas: false, unwrapResultObjects: false, excludeDeclarations: false, - extractProviderSchema: false, } const state = { - destination: undefined, - typeTemplateDir: 'types', - section: undefined + typeTemplateDir: 'types' } const capitalize = str => str[0].toUpperCase() + str.substr(1) @@ -95,10 +85,6 @@ const indent = (str, paddingStr, repeat = 1, endRepeat = 0) => { }).join('\n') } -const setTyper = (t) => { - types = t -} - const setConfig = (c) => { config = c } @@ -127,21 +113,23 @@ const getTemplateForExample = (method, templates) => { const getTemplateForExampleResult = (method, templates) => { const template = getTemplateTypeForMethod(method, 'examples/results', templates) - return template || JSON.stringify(method.examples[0].result.value, null, '\t') + const value = method.examples[0].result ? method.examples[0].result.value : method.examples[0].params.slice(-1)[0]?.value + return template || JSON.stringify(value) } const getLinkForSchema = (schema, json) => { const dirs = config.createModuleDirectories const copySchemasIntoModules = config.copySchemasIntoModules + const definitions = json.definitions || json.components.schemas - const type = types.getSchemaType(schema, json, { templateDir: state.typeTemplateDir, destination: state.destination, section: state.section }) + const type = Types.getSchemaType(schema, json, { templateDir: state.typeTemplateDir, namespace: !config.copySchemasIntoModules }) // local - insert a bogus link, that we'll update later based on final table-of-contents - if (json.components.schemas[type]) { + if (definitions && definitions[type]) { return `#\$\{LINK:schema:${type}\}` } else { - const [group, schema] = Object.entries(json['x-schemas']).find(([key, value]) => json['x-schemas'][key] && json['x-schemas'][key][type]) || [null, null] + const [group, schema] = Object.entries(definitions).find(([key, value]) => definitions[key] && definitions[key][type]) || [null, null] if (group && schema) { if (copySchemasIntoModules) { return `#\$\{LINK:schema:${type}\}` @@ -161,25 +149,6 @@ const getLinkForSchema = (schema, json) => { return '#' } -const getComponentExternalSchema = (json) => { - let refSchemas = [] - if (json.components && json.components.schemas) { - Object.entries(json.components.schemas).forEach(([name, schema]) => { - let refs = getLinkedSchemaPaths(schema).map(path => getPathOr(null, path, schema)) - refs.map(ref => { - let title = '' - if (ref.includes('x-schemas')) { - if (ref.split('/')[2] !== json.info.title) { - title = ref.split('/')[2] - } - } - title && !refSchemas.includes(title) ? refSchemas.push(title) : null - }) - }) - } - return (refSchemas) -} - // Maybe methods array of objects const getMethods = compose( map(filter(isObject)), @@ -288,6 +257,7 @@ const eventsOrEmptyArray = compose( map(map(e => { if (!e.name.match(/on[A-Z]/)) { console.error(`ERROR: ${e.name} method is tagged as an event, but does not match the pattern "on[A-Z]"`) + console.dir(e, { depth: 10 }) process.kill(process.pid) // Using process.kill so that other worspaces all exit (and don't bury this error w/ logs) } return e @@ -331,7 +301,8 @@ const providersOrEmptyArray = compose( // Maintain the side effect of process.exit here if someone is violating the rules map(map(e => { if (!e.name.match(/on[A-Z]/)) { - console.error(`ERROR: ${e.name} method is tagged as an event, but does not match the pattern "on[A-Z]"`) + console.error(`ERROR: ${e.name} method is tagged as a provider, but does not match the pattern "on[A-Z]"`) + console.dir(e, { depth: 10 }) process.exit(1) // Non-zero exit since we don't want to continue. Useful for CI/CD pipelines. } return e @@ -357,26 +328,29 @@ const getModuleName = json => { return json ? (json.title || (json.info ? json.info.title : 'Unknown')) : 'Unknown' } -const makeEventName = x => x.name[2].toLowerCase() + x.name.substr(3) // onFooBar becomes fooBar +const makeEventName = x => methodName(x)[2].toLowerCase() + methodName(x).substr(3) // onFooBar becomes fooBar const makeProviderMethod = x => x.name["onRequest".length].toLowerCase() + x.name.substr("onRequest".length + 1) // onRequestChallenge becomes challenge //import { default as platform } from '../Platform/defaults' -const generateAggregateMacros = (openrpc, modules, templates, library) => Object.values(modules) - .reduce((acc, module) => { + +const generateAggregateMacros = (server, client, additional, templates, library) => { + return additional.reduce((acc, module) => { + + const infoMacros = generateInfoMacros(module) let template = getTemplate('/codeblocks/export', templates) - if (template) { - acc.exports += insertMacros(template + '\n', generateMacros(module, templates)) + if (template) { + acc.exports += insertInfoMacros(template + '\n', infoMacros) } template = getTemplate('/codeblocks/mock-import', templates) - if (template) { - acc.mockImports += insertMacros(template + '\n', generateMacros(module, templates)) + if (template && module.info) { + acc.mockImports += insertInfoMacros(template + '\n', infoMacros) } template = getTemplate('/codeblocks/mock-parameter', templates) - if (template) { - acc.mockObjects += insertMacros(template + '\n', generateMacros(module, templates)) + if (template && module.info) { + acc.mockObjects += insertInfoMacros(template + '\n', infoMacros) } return acc @@ -384,9 +358,11 @@ const generateAggregateMacros = (openrpc, modules, templates, library) => Object exports: '', mockImports: '', mockObjects: '', - version: getSemanticVersion(openrpc), - library: library + version: getSemanticVersion(server), + library: library, + unidirectional: !client }) +} const addContentDescriptorSubSchema = (descriptor, prefix, obj) => { const title = getPromotionNameFromContentDescriptor(descriptor, prefix) @@ -399,7 +375,7 @@ const getPromotionNameFromContentDescriptor = (descriptor, prefix) => { } const promoteSchema = (location, property, title, document, destinationPath) => { - const destination = getJsonPath(destinationPath, document) + const destination = getReferencedSchema(destinationPath, document) destination[title] = location[property] destination[title].title = title location[property] = { @@ -413,42 +389,48 @@ const isSubSchema = (schema) => schema.type === 'object' || (schema.type === 'st // check schema is sub enum of array const isSubEnumOfArraySchema = (schema) => (schema.type === 'array' && schema.items.enum) -const promoteAndNameSubSchemas = (obj) => { +const promoteAndNameSubSchemas = (server, client) => { + const moduleTitle = server.info ? server.info.title : server.title + // make a copy so we don't polute our inputs - obj = JSON.parse(JSON.stringify(obj)) + server = JSON.parse(JSON.stringify(server)) // find anonymous method param or result schemas and name/promote them - obj.methods && obj.methods.forEach(method => { + server.methods && server.methods.forEach(method => { method.params && method.params.forEach(param => { if (isSubSchema(param.schema)) { - addContentDescriptorSubSchema(param, '', obj) + addContentDescriptorSubSchema(param, '', server) } }) - if (isSubSchema(method.result.schema)) { - addContentDescriptorSubSchema(method.result, '', obj) + if (method.result && isSubSchema(method.result.schema)) { + addContentDescriptorSubSchema(method.result, '', server) } - else if (isEventMethod(method) && isSubSchema(getPayloadFromEvent(method))) { + else if (!client && isEventMethod(method) && isSubSchema(getPayloadFromEvent(method))) { // TODO: the `1` below is brittle... should find the index of the non-ListenResponse schema - promoteSchema(method.result.schema.anyOf, 1, getPromotionNameFromContentDescriptor(method.result, ''), obj, '#/components/schemas') + promoteSchema(method.result.schema.anyOf, 1, getPromotionNameFromContentDescriptor(method.result, ''), server, '#/components/schemas') + } + else if (isEventMethod(method) && isSubSchema(getNotifier(method, client).params.slice(-1)[0])) { + const notifier = getNotifier(method, client) + promoteSchema(notifier.params[notifier.params.length-1], 'schema', getPromotionNameFromContentDescriptor(notifier.params[notifier.params.length-1], ''), server, '#/components/schemas') } if (method.tags.find(t => t['x-error'])) { method.tags.forEach(tag => { if (tag['x-error']) { const descriptor = { - name: obj.info.title + 'Error', + name: moduleTitle + 'Error', schema: tag['x-error'] } - addContentDescriptorSubSchema(descriptor, '', obj) + addContentDescriptorSubSchema(descriptor, '', server) } }) } }) // find non-primitive sub-schemas of components.schemas and name/promote them - if (obj.components && obj.components.schemas) { + if (server.components && server.components.schemas) { let more = true while (more) { more = false - Object.entries(obj.components.schemas).forEach(([key, schema]) => { + Object.entries(server.components.schemas).forEach(([key, schema]) => { let componentSchemaProperties = schema.allOf ? schema.allOf : [schema] componentSchemaProperties.forEach((componentSchema) => { if ((componentSchema.type === "object") && componentSchema.properties) { @@ -459,7 +441,7 @@ const promoteAndNameSubSchemas = (obj) => { name: name, schema: propSchema } - addContentDescriptorSubSchema(descriptor, key, obj) + addContentDescriptorSubSchema(descriptor, key, server) componentSchema.properties[name] = descriptor.schema } if (isSubEnumOfArraySchema(propSchema)) { @@ -467,7 +449,7 @@ const promoteAndNameSubSchemas = (obj) => { name: name, schema: propSchema.items } - addContentDescriptorSubSchema(descriptor, key, obj) + addContentDescriptorSubSchema(descriptor, key, server) componentSchema.properties[name].items = descriptor.schema } }) @@ -481,28 +463,16 @@ const promoteAndNameSubSchemas = (obj) => { } } - return obj + return server } -const generateMacros = (obj, templates, languages, options = {}) => { - if (options.createPolymorphicMethods) { - let methods = [] - obj.methods && obj.methods.forEach(method => { - let polymorphicMethods = createPolymorphicMethods(method, obj) - if (polymorphicMethods.length > 1) { - polymorphicMethods.forEach(polymorphicMethod => { - methods.push(polymorphicMethod) - }) - } - else { - methods.push(method) - } - }) - obj.methods = methods - } +const generateMacros = (server, client, templates, languages, options = {}) => { // for languages that don't support nested schemas, let's promote them to first-class schemas w/ titles if (config.extractSubSchemas) { - obj = promoteAndNameSubSchemas(obj) + server = promoteAndNameSubSchemas(server, client) + if (client) { + client = promoteAndNameSubSchemas(client) + } } // grab the options so we don't have to pass them from method to method @@ -512,27 +482,43 @@ const generateMacros = (obj, templates, languages, options = {}) => { schemas: {}, types: {}, enums: {}, + enum_implementations: {}, methods: {}, events: {}, methodList: '', - eventList: '' + eventList: '', + callsMetrics: false + } + + if (callsMetrics(server)) { + macros.callsMetrics = true } + let start = Date.now() + + const unique = list => list.map((item, i) => Object.assign(item, { index: i })).filter( (item, i, list) => !(list.find(x => x.name === item.name) && list.find(x => x.name === item.name).index < item.index)) + Array.from(new Set(['types'].concat(config.additionalSchemaTemplates))).filter(dir => dir).forEach(dir => { state.typeTemplateDir = dir - const schemasArray = generateSchemas(obj, templates, { baseUrl: '', section: 'schemas' }).filter(s => (options.copySchemasIntoModules || !s.uri)) + const schemasArray = unique(generateSchemas(server, templates, { baseUrl: '' }).concat(generateSchemas(client, templates, { baseUrl: '' }))) macros.schemas[dir] = getTemplate('/sections/schemas', templates).replace(/\$\{schema.list\}/g, schemasArray.map(s => s.body).filter(body => body).join('\n')) macros.types[dir] = getTemplate('/sections/types', templates).replace(/\$\{schema.list\}/g, schemasArray.filter(x => !x.enum).map(s => s.body).filter(body => body).join('\n')) macros.enums[dir] = getTemplate('/sections/enums', templates).replace(/\$\{schema.list\}/g, schemasArray.filter(x => x.enum).map(s => s.body).filter(body => body).join('\n')) + macros.enum_implementations[dir] = getTemplate('/sections/enums', templates).replace(/\$\{schema.list\}/g, schemasArray.filter(x => x.enum).map(s => s.impl).filter(body => body).join('\n')) }) + console.log(` - Generated types macros ${Date.now() - start}`) + start = Date.now() + state.typeTemplateDir = 'types' - const imports = generateImports(obj, templates, { destination: (options.destination ? options.destination : '') }) - const initialization = generateInitialization(obj, templates) - const eventsEnum = generateEvents(obj, templates) + const imports = Object.fromEntries(Array.from(new Set(Object.keys(templates).filter(key => key.startsWith('/imports/')).map(key => key.split('.').pop()))).map(key => [key, generateImports(server, client, templates, { destination: key })])) + const initialization = generateInitialization(server, client, templates) + const eventsEnum = generateEvents(server, templates) - const examples = generateExamples(obj, templates, languages) - const allMethodsArray = generateMethods(obj, examples, templates, languages, options.type) + console.log(` - Generated imports, etc macros ${Date.now() - start}`) + start = Date.now() + + const allMethodsArray = generateMethods(server, client, templates, languages, options.type) Array.from(new Set(['methods'].concat(config.additionalMethodTemplates))).filter(dir => dir).forEach(dir => { @@ -548,45 +534,60 @@ const generateMacros = (obj, templates, languages, options = {}) => { macros.events[dir] = eventsArray.length ? getTemplate('/sections/events', templates).replace(/\$\{event.list\}/g, eventsArray.map(m => m.body[dir]).join('\n')) : '' if (dir === 'methods') { - macros.methodList = methodsArray.filter(m => m.body).map(m => m.name) + macros.methodList = methodsArray.filter(m => m.body).map(m => methodName(m)) macros.eventList = eventsArray.map(m => makeEventName(m)) } } }) - const xusesInterfaces = generateXUsesInterfaces(obj, templates) - const providerSubscribe = generateProviderSubscribe(obj, templates) - const providerInterfaces = generateProviderInterfaces(obj, templates) - const defaults = generateDefaults(obj, templates) + console.log(` - Generated method macros ${Date.now() - start}`) + start = Date.now() + + const xusesInterfaces = generateXUsesInterfaces(server, templates) + const providerSubscribe = generateProviderSubscribe(server, client, templates, !!client) + const providerInterfaces = generateProviderInterfaces(server, client, templates, 'interface', 'interfaces', !!client) + const providerClasses = generateProviderInterfaces(server, client, templates, 'class', 'classes', !!client) + const defaults = generateDefaults(server, client, templates) + + console.log(` - Generated provider macros ${Date.now() - start}`) + start = Date.now() - const suffix = options.destination ? options.destination.split('.').pop().trim() : '' const module = getTemplate('/codeblocks/module', templates) - const moduleInclude = getTemplate(suffix ? `/codeblocks/module-include.${suffix}` : '/codeblocks/module-include', templates) - const moduleIncludePrivate = getTemplate(suffix ? `/codeblocks/module-include-private.${suffix}` : '/codeblocks/module-include-private', templates) - const moduleInit = getTemplate(suffix ? `/codeblocks/module-init.${suffix}` : '/codeblocks/module-init', templates) + const moduleInclude = getTemplate('/codeblocks/module-include', templates) + const moduleIncludePrivate = getTemplate('/codeblocks/module-include-private', templates) +// const moduleInit = getTemplate(suffix ? `/codeblocks/module-init.${suffix}` : '/codeblocks/module-init', templates) + const moduleInit = Object.fromEntries(Array.from(new Set(Object.keys(templates).filter(key => key.startsWith('/imports/')).map(key => key.split('.').pop()))).map(key => [key, getTemplate(`/codeblocks/module-init.${key}`, templates)])) Object.assign(macros, { imports, initialization, eventsEnum, defaults, - examples, xusesInterfaces, providerInterfaces, + providerClasses, providerSubscribe, - version: getSemanticVersion(obj), - title: obj.info.title, - description: obj.info.description, module: module, moduleInclude: moduleInclude, moduleIncludePrivate: moduleIncludePrivate, moduleInit: moduleInit, - public: hasPublicAPIs(obj) + public: hasPublicAPIs(server), + unidirectional: !client }) + Object.assign(macros, generateInfoMacros(server)) + return macros } +const generateInfoMacros = (document) => { + return { + version: getSemanticVersion(document), + title: document.title || document.info.title, + description: document.info ? document.info.description : document.description + } +} + const clearMacros = (fContents = '') => { fContents = fContents.replace(/\$\{module\.includes\}/g, "") fContents = fContents.replace(/\$\{module\.includes\.private\}/g, "") @@ -599,8 +600,10 @@ const insertAggregateMacros = (fContents = '', aggregateMacros = {}) => { fContents = fContents.replace(/[ \t]*\/\* \$\{EXPORTS\} \*\/[ \t]*\n/, aggregateMacros.exports) fContents = fContents.replace(/[ \t]*\/\* \$\{MOCK_IMPORTS\} \*\/[ \t]*\n/, aggregateMacros.mockImports) fContents = fContents.replace(/[ \t]*\/\* \$\{MOCK_OBJECTS\} \*\/[ \t]*\n/, aggregateMacros.mockObjects) - fContents = fContents.replace(/\$\{readable\}/g, aggregateMacros.version.readable) + fContents = fContents.replace(/\$\{readable\}/g, aggregateMacros.version ? aggregateMacros.version.readable : '') fContents = fContents.replace(/\$\{package.name\}/g, aggregateMacros.library) + fContents = fContents.replace(/\$\{if\.unidirectional\}(.*?)\$\{end\.if\.unidirectional\}/gms, aggregateMacros.unidirectional ? '$1' : '') + fContents = fContents.replace(/\$\{if\.bidirectional\}(.*?)\$\{end\.if\.bidirectional\}/gms, !aggregateMacros.unidirectional ? '$1' : '') return fContents } @@ -617,11 +620,18 @@ const insertMacros = (fContents = '', macros = {}) => { fContents = fContents.replace(/\$\{if\.schemas\}(.*?)\$\{end\.if\.schemas\}/gms, macros.schemas.types.trim() ? '$1' : '') fContents = fContents.replace(/\$\{if\.enums\}(.*?)\$\{end\.if\.enums\}/gms, macros.enums.types.trim() ? '$1' : '') fContents = fContents.replace(/\$\{if\.declarations\}(.*?)\$\{end\.if\.declarations\}/gms, (macros.methods.declarations && macros.methods.declarations.trim() || macros.enums.types.trim()) || macros.types.types.trim()? '$1' : '') - + fContents = fContents.replace(/\$\{if\.callsmetrics\}(.*?)\$\{end\.if\.callsmetrics\}/gms, macros.callsMetrics ? '$1' : '') + fContents = fContents.replace(/\$\{module\.list\}/g, macros.module) fContents = fContents.replace(/\$\{module\.includes\}/g, macros.moduleInclude) fContents = fContents.replace(/\$\{module\.includes\.private\}/g, macros.moduleIncludePrivate) - fContents = fContents.replace(/\$\{module\.init\}/g, macros.moduleInit) + fContents = fContents.replace(/\$\{module\.init\}/g, Object.values(macros.moduleInit)[0]) + + Object.keys(macros.moduleInit).forEach(key => { + const regex = new RegExp('\\$\\{module\.init\\:' + key + '\\}', 'gms') + fContents = fContents.replace(regex, macros.moduleInit[key]) + }) + let methods = '' Array.from(new Set(['methods'].concat(config.additionalMethodTemplates))).filter(dir => dir).every(dir => { @@ -657,6 +667,7 @@ const insertMacros = (fContents = '', macros = {}) => { fContents = fContents.replace(/[ \t]*\/\* \$\{SCHEMAS\} \*\/[ \t]*\n/, macros.schemas.types) fContents = fContents.replace(/[ \t]*\/\* \$\{TYPES\} \*\/[ \t]*\n/, macros.types.types) fContents = fContents.replace(/[ \t]*\/\* \$\{ENUMS\} \*\/[ \t]*\n/, macros.enums.types) + fContents = fContents.replace(/[ \t]*\/\* \$\{ENUM_IMPLEMENTATIONS\} \*\/[ \t]*\n/, macros.enum_implementations.types) // Output all schemas with all dynamically configured templates Array.from(new Set(['types'].concat(config.additionalSchemaTemplates))).filter(dir => dir).forEach(dir => { @@ -666,10 +677,19 @@ const insertMacros = (fContents = '', macros = {}) => { }) }) + // Output all imports with all dynamically configured templates + Object.keys(macros.imports).forEach(key => { + const regex = new RegExp('[ \\t]*\\/\\* \\$\\{IMPORTS\\:' + key + '\\} \\*\\/[ \\t]*\\n', 'g') + fContents = fContents.replace(regex, macros.imports[key]) + }) + + fContents = fContents.replace(/[ \t]*\/\* \$\{PROVIDERS\} \*\/[ \t]*\n/, macros.providerInterfaces) + fContents = fContents.replace(/[ \t]*\/\* \$\{PROVIDER_INTERFACES\} \*\/[ \t]*\n/, macros.providerInterfaces) + fContents = fContents.replace(/[ \t]*\/\* \$\{PROVIDER_CLASSES\} \*\/[ \t]*\n/, macros.providerClasses) fContents = fContents.replace(/[ \t]*\/\* \$\{PROVIDERS\} \*\/[ \t]*\n/, macros.providerInterfaces) fContents = fContents.replace(/[ \t]*\/\* \$\{XUSES\} \*\/[ \t]*\n/, macros.xusesInterfaces) fContents = fContents.replace(/[ \t]*\/\* \$\{PROVIDERS_SUBSCRIBE\} \*\/[ \t]*\n/, macros.providerSubscribe) - fContents = fContents.replace(/[ \t]*\/\* \$\{IMPORTS\} \*\/[ \t]*\n/, macros.imports) + fContents = fContents.replace(/[ \t]*\/\* \$\{IMPORTS\} \*\/[ \t]*\n/, Object.values(macros.imports)[0]) fContents = fContents.replace(/[ \t]*\/\* \$\{INITIALIZATION\} \*\/[ \t]*\n/, macros.initialization) fContents = fContents.replace(/[ \t]*\/\* \$\{DEFAULTS\} \*\/[ \t]*\n/, macros.defaults) fContents = fContents.replace(/\$\{events.array\}/g, JSON.stringify(macros.eventList)) @@ -677,12 +697,8 @@ const insertMacros = (fContents = '', macros = {}) => { fContents = fContents.replace(/\$\{major\}/g, macros.version.major) fContents = fContents.replace(/\$\{minor\}/g, macros.version.minor) fContents = fContents.replace(/\$\{patch\}/g, macros.version.patch) - fContents = fContents.replace(/\$\{info\.title\}/g, macros.title) - fContents = fContents.replace(/\$\{info\.title\.lowercase\}/g, macros.title.toLowerCase()) - fContents = fContents.replace(/\$\{info\.Title\}/g, capitalize(macros.title)) - fContents = fContents.replace(/\$\{info\.TITLE\}/g, macros.title.toUpperCase()) - fContents = fContents.replace(/\$\{info\.description\}/g, macros.description) - fContents = fContents.replace(/\$\{info\.version\}/g, macros.version.readable) + + fContents = insertInfoMacros(fContents, macros) if (macros.public) { fContents = fContents.replace(/\$\{if\.public\}(.*?)\$\{end\.if\.public\}/gms, '$1') @@ -698,17 +714,24 @@ const insertMacros = (fContents = '', macros = {}) => { fContents = fContents.replace(/\$\{if\.events\}.*?\$\{end\.if\.events\}/gms, '') } - const examples = [...fContents.matchAll(/0 \/\* \$\{EXAMPLE\:(.*?)\} \*\//g)] - - examples.forEach((match) => { - fContents = fContents.replace(match[0], JSON.stringify(macros.examples[match[1]][0].value)) - }) + fContents = fContents.replace(/\$\{if\.unidirectional\}(.*?)\$\{end\.if\.unidirectional\}/gms, macros.unidirectional ? '$1' : '') + fContents = fContents.replace(/\$\{if\.bidirectional\}(.*?)\$\{end\.if\.bidirectional\}/gms, !macros.unidirectional ? '$1' : '') fContents = insertTableofContents(fContents) return fContents } +function insertInfoMacros(fContents, macros) { + fContents = fContents.replace(/\$\{info\.title\}/g, macros.title) + fContents = fContents.replace(/\$\{info\.title\.lowercase\}/g, macros.title.toLowerCase()) + fContents = fContents.replace(/\$\{info\.Title\}/g, capitalize(macros.title)) + fContents = fContents.replace(/\$\{info\.TITLE\}/g, macros.title.toUpperCase()) + fContents = fContents.replace(/\$\{info\.description\}/g, macros.description) + fContents = fContents.replace(/\$\{info\.version\}/g, macros.version.readable) + return fContents +} + function insertTableofContents(content) { let toc = '' const count = {} @@ -781,23 +804,22 @@ const enumFinder = compose( filter(([_key, val]) => isObject(val)) ) -const generateEnums = (json, templates, options = { destination: '' }) => { - const suffix = options.destination.split('.').pop() +const generateEnums = (json, templates, template='enum') => { return compose( option(''), map(val => { - let template = val ? getTemplate(`/sections/enum.${suffix}`, templates) : val - return template ? template.replace(/\$\{schema.list\}/g, val.trimEnd()) : val + let output = val ? getTemplate(`/sections/enum`, templates) : val + return output ? output.replace(/\$\{schema.list\}/g, val.trimEnd()) : val }), map(reduce((acc, val) => acc.concat(val).concat('\n'), '')), - map(map((schema) => convertEnumTemplate(schema, suffix ? `/types/enum.${suffix}` : '/types/enum', templates))), + map(map((schema) => convertEnumTemplate(schema, `/types/${template}`, templates))), map(enumFinder), getSchemas )(json) } const generateEvents = (json, templates) => { - const eventNames = eventsOrEmptyArray(json).map(makeEventName) + const eventNames = eventsOrEmptyArray(json).map(x => makeEventName(x)) const obj = eventNames.reduce((acc, val, i, arr) => { if (!acc) { @@ -818,18 +840,18 @@ const generateEvents = (json, templates) => { return acc }, null) - return generateEnums(obj, templates) + return generateEnums(obj, templates, 'enum-implementation') } -function generateDefaults(json = {}, templates) { +function generateDefaults(server = {}, client, templates) { const reducer = compose( reduce((acc, val, i, arr) => { if (isPropertyMethod(val)) { - acc += insertMethodMacros(getTemplate('/defaults/property', templates), val, json, templates) + acc += insertMethodMacros(getTemplate('/defaults/property', templates), val, server, client, templates) } else if (val.tags.find(t => t.name === "setter")) { - acc += insertMethodMacros(getTemplate('/defaults/setter', templates), val, json, templates) + acc += insertMethodMacros(getTemplate('/defaults/setter', templates), val, server, client, templates) } else { - acc += insertMethodMacros(getTemplate('/defaults/default', templates), val, json, templates) + acc += insertMethodMacros(getTemplate('/defaults/default', templates), val, server, client, templates) } if (i < arr.length - 1) { acc = acc.concat(',\n') @@ -845,7 +867,7 @@ function generateDefaults(json = {}, templates) { ), ) - return reducer(json) + return reducer(server) } function sortSchemasByReference(schemas = []) { @@ -873,16 +895,25 @@ const isEnum = x => { return schema.type && schema.type === 'string' && Array.isArray(schema.enum) && x.title } -function generateSchemas(json, templates, options) { +function generateSchemas(server, templates, options) { let results = [] - const schemas = JSON.parse(JSON.stringify(json.definitions || (json.components && json.components.schemas) || {})) + if (!server) { + return results + } + + const schemas = JSON.parse(JSON.stringify(server.definitions || (server.components && server.components.schemas) || {})) const generate = (name, schema, uri, { prefix = '' } = {}) => { // these are internal schemas used by the fireboltize-openrpc tooling, and not meant to be used in code/doc generation if (['ListenResponse', 'ProviderRequest', 'ProviderResponse', 'FederatedResponse', 'FederatedRequest'].includes(name)) { return } + + if (!schema.title) { + return + } + let content = getTemplate('/schemas/default', templates) if (!schema.examples || schema.examples.length === 0) { @@ -898,18 +929,20 @@ function generateSchemas(json, templates, options) { else { content = content.replace(/\$\{if\.description\}(.*?)\{end\.if\.description\}/gms, '$1') } - const schemaShape = types.getSchemaShape(schema, json, { templateDir: state.typeTemplateDir, destination: state.destination, section: options.section, primitive: config.primitives ? Object.keys(config.primitives).length > 0 : false }) + + + const schemaShape = Types.getSchemaShape(schema, server, { templateDir: state.typeTemplateDir, primitive: config.primitives ? Object.keys(config.primitives).length > 0 : false, namespace: !config.copySchemasIntoModules }) + const schemaImpl = Types.getSchemaShape(schema, server, { templateDir: state.typeTemplateDir, enumImpl: true, primitive: config.primitives ? Object.keys(config.primitives).length > 0 : false, namespace: !config.copySchemasIntoModules }) content = content .replace(/\$\{schema.title\}/, (schema.title || name)) .replace(/\$\{schema.description\}/, schema.description || '') - .replace(/\$\{schema.shape\}/, schemaShape) if (schema.examples) { content = content.replace(/\$\{schema.example\}/, schema.examples.map(ex => JSON.stringify(ex, null, ' ')).join('\n\n')) } - let seeAlso = getRelatedSchemaLinks(schema, json, templates, options) + let seeAlso = getRelatedSchemaLinks(schema, server, templates, options) if (seeAlso) { content = content.replace(/\$\{schema.seeAlso\}/, '\n\n' + seeAlso) } @@ -918,6 +951,9 @@ function generateSchemas(json, templates, options) { } content = content.trim().length ? content : content.trim() + const impl = content.replace(/\$\{schema.shape\}/, schemaImpl) + content = content.replace(/\$\{schema.shape\}/, schemaShape) + const isEnum = x => x.type && Array.isArray(x.enum) && x.title && ((x.type === 'string') || (x.type[0] === 'string')) const result = uri ? { @@ -931,20 +967,40 @@ function generateSchemas(json, templates, options) { enum: isEnum(schema) } - results.push(result) + if (isEnum(schema)) { + result.impl = impl + } + + if (result.name) { + results.push(result) + } } let list = [] // schemas may be 1 or 2 levels deeps Object.entries(schemas).forEach(([name, schema]) => { - if (isSchema(schema)) { + if (isSchema(schema) && !schema.$id) { list.push([name, schema]) } + else if (server.info && isSchema(schema) && schema.$id && schema.definitions) { + if ( (config.mergeOnTitle && (schema.title === server.info.title)) || config.copySchemasIntoModules) { + Object.entries(schema.definitions).forEach( ([name, schema]) => { + list.push([name, schema]) + }) + } + } }) list = sortSchemasByReference(list) - list.forEach(item => generate(...item)) + list.forEach(item => { + try { + generate(...item) + } + catch (error) { + console.error(error) + } + }) return results } @@ -956,13 +1012,13 @@ function getRelatedSchemaLinks(schema = {}, json = {}, templates = {}, options = // - dedupe them // - convert them to the $ref value (which are paths to other schema files), instead of the path to the ref node itself // - convert those into markdown links of the form [Schema](Schema#/link/to/element) + let links = getLinkedSchemaPaths(schema) .map(path => getPathOr(null, path, schema)) .filter(path => seen.hasOwnProperty(path) ? false : (seen[path] = true)) - .map(path => path.substring(2).split('/')) - .map(path => getPathOr(null, path, json)) + .map(ref => getReferencedSchema(ref, json)) .filter(schema => schema.title) - .map(schema => '[' + types.getSchemaType(schema, json, { templateDir: state.typeTemplateDir, destination: state.destination, section: state.section }) + '](' + getLinkForSchema(schema, json) + ')') // need full module here, not just the schema + .map(schema => '[' + Types.getSchemaType(schema, json, { templateDir: state.typeTemplateDir, namespace: !config.copySchemasIntoModules }) + '](' + getLinkForSchema(schema, json) + ')') // need full module here, not just the schema .filter(link => link) .join('\n') @@ -983,58 +1039,71 @@ function getTemplateFromDestination(destination, templateName, templates) { return template } -const generateImports = (json, templates, options = { destination: '' }) => { +const generateImports = (server, client, templates, options = { destination: '' }) => { let imports = '' - if (rpcMethodsOrEmptyArray(json).length) { - imports += getTemplate('/imports/rpc', templates) + if (rpcMethodsOrEmptyArray(server).length) { + if (client) { + imports += getTemplate('/imports/rpc', templates) + } + else { + imports += getTemplate('/imports/unidirectional-rpc', templates) + } } - if (eventsOrEmptyArray(json).length) { + if (eventsOrEmptyArray(server).length) { imports += getTemplate('/imports/event', templates) } - if (eventsOrEmptyArray(json).find(m => m.params.length > 1)) { + if (eventsOrEmptyArray(server).find(m => m.params.length > 1)) { imports += getTemplate('/imports/context-event', templates) } - if (providersOrEmptyArray(json).length) { - imports += getTemplate('/imports/provider', templates) + if (getProvidedInterfaces(client || server).length) { + if (client) { + imports += getTemplate('/imports/provider', templates) + } + else { + imports += getTemplate('/imports/unidirectional-provider', templates) + } } - if (props(json).length) { + if (props(server).length) { imports += getTemplate('/imports/property', templates) } - if (temporalSets(json).length) { + if (temporalSets(server).length) { imports += getTemplate('/imports/temporal-set', templates) } - if (methodsWithXMethodsInResult(json).length) { + if (methodsWithXMethodsInResult(server).length) { imports += getTemplate('/imports/x-method', templates) } - if (callsMetrics(json).length) { + if (callsMetrics(server).length) { imports += getTemplateFromDestination(options.destination, '/imports/calls-metrics', templates) } let template = getTemplateFromDestination(options.destination, '/imports/default', templates) - if (json['x-schemas'] && Object.keys(json['x-schemas']).length > 0 && !json.info['x-uri-titles']) { - imports += Object.keys(json['x-schemas']).map(shared => template.replace(/\$\{info.title.lowercase\}/g, shared.toLowerCase())).join('') - } - - let componentExternalSchema = getComponentExternalSchema(json) - if (componentExternalSchema.length && json.info['x-uri-titles']) { - imports += componentExternalSchema.map(shared => template.replace(/\$\{info.title.lowercase\}/g, shared.toLowerCase())).join('') - } + const subschemas = getAllValuesForName("$id", server) + const subschemaLocation = server.definitions || server.components && server.components.schemas || {} + subschemas.shift() // remove main $id + if (subschemas.length) { + imports += subschemas.map(id => subschemaLocation[id].title).map(shared => template.replace(/\$\{info.title.lowercase\}/g, shared.toLowerCase())).join('') + } + // TODO: this does the same as above? am i missing something? + // let componentExternalSchema = getComponentExternalSchema(json) + // if (componentExternalSchema.length && json.info['x-uri-titles']) { + // imports += componentExternalSchema.map(shared => template.replace(/\$\{info.title.lowercase\}/g, shared.toLowerCase())).join('') + // } return imports } -const generateInitialization = (json, templates) => generateEventInitialization(json, templates) + '\n' + generateProviderInitialization(json, templates) + '\n' + generateDeprecatedInitialization(json, templates) +const generateInitialization = (server, client, templates) => generateEventInitialization(server, client, templates) + '\n' + generateProviderInitialization(client || server, templates) + '\n' + generateDeprecatedInitialization(server, client, templates) -const generateEventInitialization = (json, templates) => { - const events = eventsOrEmptyArray(json) +const generateEventInitialization = (server, client, templates) => { + const events = eventsOrEmptyArray(server) if (events.length > 0) { return getTemplate('/initializations/event', templates) @@ -1044,27 +1113,46 @@ const generateEventInitialization = (json, templates) => { } } -const getProviderInterfaceNameFromRPC = name => name.charAt(9).toLowerCase() + name.substr(10) // Drop onRequest prefix - // TODO: this passes a JSON object to the template... might be hard to get working in non JavaScript languages. -const generateProviderInitialization = (json, templates) => compose( - reduce((acc, capability, i, arr) => { - const methods = providersOrEmptyArray(json) - .filter(m => m.tags.find(t => t['x-provides'] === capability)) - .map(m => ({ - name: getProviderInterfaceNameFromRPC(m.name), - focus: ((m.tags.find(t => t['x-allow-focus']) || { 'x-allow-focus': false })['x-allow-focus']), - response: ((m.tags.find(t => t['x-response']) || { 'x-response': null })['x-response']) !== null, - parameters: !providerHasNoParameters(localizeDependencies(getPayloadFromEvent(m), json)) - })) - return acc + getTemplate('/initializations/provider', templates) - .replace(/\$\{capability\}/g, capability) - .replace(/\$\{interface\}/g, JSON.stringify(methods)) - }, ''), - providedCapabilitiesOrEmptyArray -)(json) - -const generateDeprecatedInitialization = (json, templates) => { +const generateProviderInitialization = (document, templates) => { + let result = '' + const interfaces = getProvidedInterfaces(document) + + interfaces.forEach(_interface => { + const methods = getProviderInterface(_interface, document) + const capability = provides(methods[0]) + methods.forEach(method => { + result += getTemplate('/initializations/provider', templates) + .replace(/\$\{capability\}/g, capability) + .replace(/\$\{interface\}/g, _interface) + .replace(/\$\{method\.name\}/g, name(method)) + .replace(/\$\{method\.params\.array\}/g, JSON.stringify(method.params.map(p => p.name))) + .replace(/\$\{method\.focusable\}/g, ((method.tags.find(t => t['x-allow-focus']) || { 'x-allow-focus': false })['x-allow-focus'])) + .replace(/\$\{method\.response\}/g, !!method.result) + }) + }) + + return result +} +// compose( +// reduce((acc, capability, i, arr) => { +// document = client || server +// const methods = providersOrEmptyArray(document) +// .filter(m => m.tags.find(t => t['x-provides'] === capability)) +// .map(m => ({ +// name: getProviderInterfaceNameFromRPC(m.name), +// focus: ((m.tags.find(t => t['x-allow-focus']) || { 'x-allow-focus': false })['x-allow-focus']), +// response: ((m.tags.find(t => t['x-response']) || { 'x-response': null })['x-response']) !== null, +// parameters: !providerHasNoParameters(localizeDependencies(getPayloadFromEvent(m), document)) +// })) +// return acc + getTemplate('/initializations/provider', templates) +// .replace(/\$\{capability\}/g, capability) +// .replace(/\$\{interface\}/g, JSON.stringify(methods)) +// }, ''), +// providedCapabilitiesOrEmptyArray +// )(server) + +const generateDeprecatedInitialization = (server, client, templates) => { return compose( reduce((acc, method, i, arr) => { if (i === 0) { @@ -1076,49 +1164,48 @@ const generateDeprecatedInitialization = (json, templates) => { alternative = `Use ${alternative} instead.` } - return acc + insertMethodMacros(getTemplate('/initializations/deprecated', templates), method, json, templates) + // TODO: we're just inserting basic method info here... probably worth slicing up insertMethodMacros... it doesa TON of work + return acc + insertMethodMacros(getTemplate('/initializations/deprecated', templates), method, server, client, templates) }, ''), deprecatedOrEmptyArray - )(json) + )(server) } -function generateExamples(json = {}, mainTemplates = {}, languages = {}) { - const examples = {} - - json && json.methods && json.methods.forEach(method => { - examples[method.name] = method.examples.map(example => ({ - json: example, - value: example.result.value, - languages: Object.fromEntries(Object.entries(languages).map(([lang, templates]) => ([lang, { - langcode: templates['__config'].langcode, - code: getTemplateForExample(method, templates) - .replace(/\$\{rpc\.example\.params\}/g, JSON.stringify(Object.fromEntries(example.params.map(param => [param.name, param.value])))), - result: getTemplateForExampleResult(method, templates) - .replace(/\$\{example\.result\}/g, JSON.stringify(example.result.value, null, '\t')) - .replace(/\$\{example\.result\.item\}/g, Array.isArray(example.result.value) ? JSON.stringify(example.result.value[0], null, '\t') : ''), - template: lang === 'JSON-RPC' ? getTemplate('/examples/jsonrpc', mainTemplates) : getTemplateForExample(method, mainTemplates) // getTemplate('/examples/default', mainTemplates) - }]))) +function generateExamples(method = {}, mainTemplates = {}, languages = {}) { + let value + + let examples = method.examples.map(example => ({ + json: example, + value: value = example.result ? example.result.value : example.params[example.params.length-1].value, + languages: Object.fromEntries(Object.entries(languages).map(([lang, templates]) => ([lang, { + langcode: templates['__config'].langcode, + code: getTemplateForExample(method, templates) + .replace(/\$\{rpc\.example\.params\}/g, JSON.stringify(Object.fromEntries(example.params.map(param => [param.name, param.value])))), + result: method.result && getTemplateForExampleResult(method, templates) + .replace(/\$\{example\.result\}/g, JSON.stringify(value, null, '\t')) + .replace(/\$\{example\.result\.item\}/g, Array.isArray(value) ? JSON.stringify(value[0], null, '\t') : ''), + template: lang === 'JSON-RPC' ? getTemplate('/examples/jsonrpc', mainTemplates) : getTemplateForExample(method, mainTemplates) // getTemplate('/examples/default', mainTemplates) + }]))) + })) + + // delete non RPC examples from rpc-only methods + if (isRPCOnlyMethod(method)) { + examples = examples.map(example => ({ + json: example.json, + value: example.value, + languages: Object.fromEntries(Object.entries(example.languages).filter(([k, v]) => k === 'JSON-RPC')) })) + } - // delete non RPC examples from rpc-only methods - if (isRPCOnlyMethod(method)) { - examples[method.name] = examples[method.name].map(example => ({ - json: example.json, - value: example.value, - languages: Object.fromEntries(Object.entries(example.languages).filter(([k, v]) => k === 'JSON-RPC')) - })) - } - - // clean up JSON-RPC indentation, because it's easy and we can. - examples[method.name].map(example => { - if (example.languages['JSON-RPC']) { - try { - example.languages['JSON-RPC'].code = JSON.stringify(JSON.parse(example.languages['JSON-RPC'].code), null, '\t') - example.languages['JSON-RPC'].result = JSON.stringify(JSON.parse(example.languages['JSON-RPC'].result), null, '\t') - } - catch (error) { } + // clean up JSON-RPC indentation, because it's easy and we can. + examples.forEach(example => { + if (example.languages['JSON-RPC']) { + try { + example.languages['JSON-RPC'].code = JSON.stringify(JSON.parse(example.languages['JSON-RPC'].code), null, '\t') + example.languages['JSON-RPC'].result = JSON.stringify(JSON.parse(example.languages['JSON-RPC'].result), null, '\t') } - }) + catch (error) { } + } }) return examples @@ -1145,11 +1232,11 @@ function generateMethodResult(type, templates) { return result } -function generateMethods(json = {}, examples = {}, templates = {}, languages = [], type = '') { +function generateMethods(server = {}, client = null, templates = {}, languages = [], type = '') { const methods = compose( option([]), getMethods - )(json) + )(server) // Code to generate methods const results = reduce((acc, methodObj, i, arr) => { @@ -1158,23 +1245,22 @@ function generateMethods(json = {}, examples = {}, templates = {}, languages = [ body: {}, declaration: {}, excluded: methodObj.tags.find(t => t.name === 'exclude-from-sdk'), - event: isEventMethod(methodObj) + event: isEventMethod(methodObj), + examples: generateExamples(methodObj, templates, languages) } - const suffix = state.destination && config.templateExtensionMap ? state.destination.split(state.destination.includes('_') ? '_' : '.').pop() : '' - // Generate implementation of methods/events for both dynamic and static configured templates Array.from(new Set(['methods'].concat(config.additionalMethodTemplates))).filter(dir => dir).forEach(dir => { - if (dir.includes('declarations') && (suffix && config.templateExtensionMap[dir] ? config.templateExtensionMap[dir].includes(suffix) : true)) { + if (dir.includes('declarations')) { const template = getTemplateForDeclaration(methodObj, templates, dir) if (template && template.length) { - result.declaration[dir] = insertMethodMacros(template, methodObj, json, templates, '', examples) + result.declaration[dir] = insertMethodMacros(template, methodObj, server, client, templates, '', result.examples) } } - else if (dir.includes('methods') && (suffix && config.templateExtensionMap[dir] ? config.templateExtensionMap[dir].includes(suffix) : true)) { + else if (dir.includes('methods')) { const template = getTemplateForMethod(methodObj, templates, dir) if (template && template.length) { - result.body[dir] = insertMethodMacros(template, methodObj, json, templates, type, examples, languages) + result.body[dir] = insertMethodMacros(template, methodObj, server, client, templates, type, result.examples, languages) } } }) @@ -1184,291 +1270,323 @@ function generateMethods(json = {}, examples = {}, templates = {}, languages = [ return acc }, [], methods) - // TODO: might be useful to pass in local macro for an array with all capability & provider interface names - if (json.methods && json.methods.find(isProviderInterfaceMethod)) { - results.push(generateMethodResult('provide', templates)) - } - // TODO: might be useful to pass in local macro for an array with all event names - if (json.methods && json.methods.find(isPublicEventMethod)) { + if (server.methods && server.methods.find(isPublicEventMethod)) { ['listen', 'once', 'clear'].forEach(type => { results.push(generateMethodResult(type, templates)) }) } + if (server.methods && server.methods.find(isProviderInterfaceMethod)) { + ['provide'].forEach(type => { + results.push(generateMethodResult(type, templates)) + }) + } + results.sort((a, b) => a.name.localeCompare(b.name)) return results } // TODO: this is called too many places... let's reduce that to just generateMethods -function insertMethodMacros(template, methodObj, json, templates, type = '', examples = {}, languages = {}) { - const moduleName = getModuleName(json) - - const info = { - title: moduleName - } - const method = { - name: methodObj.name, - params: methodObj.params.map(p => p.name).join(', '), - transforms: null, - alternative: null, - deprecated: isDeprecatedMethod(methodObj), - context: [] - } +function insertMethodMacros(template, methodObj, server, client, templates, type = 'method', examples = [], languages = {}) { + // try { + // need a guaranteed place to get client stuff from... + const document = client || server + const moduleName = getModuleName(server) + const info = { + title: moduleName + } + const method = { + name: methodObj.name.split('.').pop(), + params: methodObj.params.map(p => p.name).join(', '), + transforms: null, + transform: '', + alternative: null, + deprecated: isDeprecatedMethod(methodObj), + context: [] + } - if (isEventMethod(methodObj) && methodObj.params.length > 1) { - method.context = methodObj.params.filter(p => p.name !== 'listen').map(p => p.name) - } + if (isEventMethod(methodObj) && methodObj.params.length > 1) { + method.context = methodObj.params.filter(p => p.name !== 'listen').map(p => p.name) + } - if (getAlternativeMethod(methodObj)) { - method.alternative = getAlternativeMethod(methodObj) - } + if (getAlternativeMethod(methodObj)) { + method.alternative = getAlternativeMethod(methodObj) + } + else if (extension(methodObj, 'x-subscriber-for')) { + method.alternative = extension(methodObj, 'x-subscriber-for') + } - const flattenedMethod = localizeDependencies(methodObj, json) + const flattenedMethod = localizeDependencies(methodObj, server) - if (hasMethodAttributes(flattenedMethod)) { - method.transforms = { - methods: getMethodAttributes(flattenedMethod) + if (hasMethodAttributes(flattenedMethod)) { + method.transforms = { + methods: getMethodAttributes(flattenedMethod) + } + method.transform = getTemplate('/codeblocks/transform', templates).replace(/\$\{transforms\}/g, JSON.stringify(method.transforms)) } - } - const paramDelimiter = config.operators ? config.operators.paramDelimiter : '' + const paramDelimiter = config.operators ? config.operators.paramDelimiter : '' - const temporalItemName = isTemporalSetMethod(methodObj) ? methodObj.result.schema.items && methodObj.result.schema.items.title || 'Item' : '' - const temporalAddName = isTemporalSetMethod(methodObj) ? `on${temporalItemName}Available` : '' - const temporalRemoveName = isTemporalSetMethod(methodObj) ? `on${temporalItemName}Unvailable` : '' - const params = methodObj.params && methodObj.params.length ? getTemplate('/sections/parameters', templates) + methodObj.params.map(p => insertParameterMacros(getTemplate('/parameters/default', templates), p, methodObj, json)).join(paramDelimiter) : '' - const paramsRows = methodObj.params && methodObj.params.length ? methodObj.params.map(p => insertParameterMacros(getTemplate('/parameters/default', templates), p, methodObj, json)).join('') : '' - const paramsAnnotations = methodObj.params && methodObj.params.length ? methodObj.params.map(p => insertParameterMacros(getTemplate('/parameters/annotations', templates), p, methodObj, json)).join('') : '' - const paramsJson = methodObj.params && methodObj.params.length ? methodObj.params.map(p => insertParameterMacros(getTemplate('/parameters/json', templates), p, methodObj, json)).join('') : '' + const temporalItemName = isTemporalSetMethod(methodObj) ? methodObj.result.schema.items && methodObj.result.schema.items.title || 'Item' : '' + const temporalAddName = isTemporalSetMethod(methodObj) ? `on${temporalItemName}Available` : '' + const temporalRemoveName = isTemporalSetMethod(methodObj) ? `on${temporalItemName}Unvailable` : '' + const params = methodObj.params && methodObj.params.length ? getTemplate('/sections/parameters', templates) + methodObj.params.map(p => insertParameterMacros(getTemplate('/parameters/default', templates), p, methodObj, server)).join(paramDelimiter) : '' + const paramsRows = methodObj.params && methodObj.params.length ? methodObj.params.map(p => insertParameterMacros(getTemplate('/parameters/default', templates), p, methodObj, server)).join('') : '' + const paramsAnnotations = methodObj.params && methodObj.params.length ? methodObj.params.map(p => insertParameterMacros(getTemplate('/parameters/annotations', templates), p, methodObj, server)).join('') : '' + const paramsJson = methodObj.params && methodObj.params.length ? methodObj.params.map(p => insertParameterMacros(getTemplate('/parameters/json', templates), p, methodObj, server)).join('') : '' - const deprecated = methodObj.tags && methodObj.tags.find(t => t.name === 'deprecated') - const deprecation = deprecated ? deprecated['x-since'] ? `since version ${deprecated['x-since']}` : '' : '' + const deprecated = methodObj.tags && methodObj.tags.find(t => t.name === 'deprecated') + const deprecation = deprecated ? deprecated['x-since'] ? `since version ${deprecated['x-since']}` : '' : '' - const capabilities = getTemplate('/sections/capabilities', templates) + insertCapabilityMacros(getTemplate('/capabilities/default', templates), methodObj.tags.find(t => t.name === "capabilities"), methodObj, json) + const capabilities = getTemplate('/sections/capabilities', templates) + insertCapabilityMacros(getTemplate('/capabilities/default', templates), methodObj.tags.find(t => t.name === "capabilities"), methodObj, server) - const result = JSON.parse(JSON.stringify(methodObj.result)) - const event = isEventMethod(methodObj) ? JSON.parse(JSON.stringify(methodObj)) : '' + const result = methodObj.result && JSON.parse(JSON.stringify(methodObj.result)) + const event = isEventMethod(methodObj) ? JSON.parse(JSON.stringify(methodObj)) : '' - if (event) { - result.schema = JSON.parse(JSON.stringify(getPayloadFromEvent(methodObj))) - event.result.schema = getPayloadFromEvent(event) - event.params = event.params.filter(p => p.name !== 'listen') - } + if (event) { + // if this is unidirection, do some simplification + if (!client) { + result.schema = JSON.parse(JSON.stringify(getPayloadFromEvent(methodObj))) + event.result.schema = getPayloadFromEvent(event) + } + else { + const notifier = getNotifier(methodObj, client) + event.result = notifier.params.slice(-1)[0] + } + event.params = event.params.filter(p => p.name !== 'listen') + } - const eventParams = event.params && event.params.length ? getTemplate('/sections/parameters', templates) + event.params.map(p => insertParameterMacros(getTemplate('/parameters/default', templates), p, event, json)).join('') : '' - const eventParamsRows = event.params && event.params.length ? event.params.map(p => insertParameterMacros(getTemplate('/parameters/default', templates), p, event, json)).join('') : '' - - let itemName = '' - let itemType = '' - - // grab some related methdos in case they are output together in a single template file - const puller = json.methods.find(method => method.tags.find(tag => tag['x-pulls-for'] === methodObj.name)) - const pullsFor = methodObj.tags.find(t => t['x-pulls-for']) && json.methods.find(method => method.name === methodObj.tags.find(t => t['x-pulls-for'])['x-pulls-for']) - const pullerTemplate = (puller ? insertMethodMacros(getTemplate('/codeblocks/puller', templates), puller, json, templates, type, examples) : '') - const setter = getSetterFor(methodObj.name, json) - const setterTemplate = (setter ? insertMethodMacros(getTemplate('/codeblocks/setter', templates), setter, json, templates, type, examples) : '') - const subscriber = json.methods.find(method => method.tags.find(tag => tag['x-alternative'] === methodObj.name)) - const subscriberTemplate = (subscriber ? insertMethodMacros(getTemplate('/codeblocks/subscriber', templates), subscriber, json, templates, type, examples) : '') - const setterFor = methodObj.tags.find(t => t.name === 'setter') && methodObj.tags.find(t => t.name === 'setter')['x-setter-for'] || '' - const pullsResult = (puller || pullsFor) ? localizeDependencies(pullsFor || methodObj, json).params.findLast(x=>true).schema : null - const pullsParams = (puller || pullsFor) ? localizeDependencies(getPayloadFromEvent(puller || methodObj), json, null, { mergeAllOfs: true }).properties.parameters : null - - const pullsResultType = (pullsResult && (type === 'methods')) ? types.getSchemaShape(pullsResult, json, { destination: state.destination, templateDir: state.typeTemplateDir, section: state.section }) : '' - const pullsForType = pullsResult && types.getSchemaType(pullsResult, json, { destination: state.destination, templateDir: state.typeTemplateDir, section: state.section }) - const pullsParamsType = (pullsParams && (type === 'methods')) ? types.getSchemaShape(pullsParams, json, { destination: state.destination, templateDir: state.typeTemplateDir, section: state.section }) : '' - const pullsForParamTitle = pullsParams ? pullsParams.title.charAt(0).toLowerCase() + pullsParams.title.substring(1) : '' - const pullsForResultTitle = pullsResult ? pullsResult.title.charAt(0).toLowerCase() + pullsResult.title.substring(1) : '' - const pullsResponseInit = (pullsParams && (type === 'methods')) ? types.getSchemaShape(pullsParams, json, { templateDir: 'result-initialization', property: pullsForParamTitle, required: pullsParams.required, destination: state.destination, section: state.section, primitive: true, skipTitleOnce: true }) : '' - const pullsResponseInst = (pullsParams && (type === 'methods')) ? types.getSchemaShape(pullsParams, json, { templateDir: 'result-instantiation', property: pullsForParamTitle, required: pullsParams.required, destination: state.destination, section: state.section, primitive: true, skipTitleOnce: true }) : '' - const pullsResultSerialize = (pullsResult && (type === 'methods')) ? types.getSchemaShape(pullsResult, json, { templateDir: 'parameter-serialization/sub-property', property: pullsForResultTitle, required: pullsResult.required, destination: state.destination, section: state.section, primitive: true, skipTitleOnce: true }) : '' - - const serializedParams = (type === 'methods') ? flattenedMethod.params.map(param => types.getSchemaShape(param.schema, json, { templateDir: 'parameter-serialization', property: param.name, required: param.required, destination: state.destination, section: state.section, primitive: true, skipTitleOnce: true })).join('\n') : '' - const resultInst = (type === 'methods') ? types.getSchemaShape(flattenedMethod.result.schema, json, { templateDir: 'result-instantiation', property: flattenedMethod.result.name, required: flattenedMethod.result.required, destination: state.destination, section: state.section, primitive: true, skipTitleOnce: true }) : '' // w/out primitive: true, getSchemaShape skips anonymous types, like primitives - const resultInit = (type === 'methods') ? types.getSchemaShape(flattenedMethod.result.schema, json, { templateDir: 'result-initialization', property: flattenedMethod.result.name, destination: state.destination, section: state.section, primitive: true, skipTitleOnce: true }) : '' // w/out primitive: true, getSchemaShape skips anonymous types, like primitives - const serializedEventParams = event && (type === 'methods') ? flattenedMethod.params.filter(p => p.name !== 'listen').map(param => types.getSchemaShape(param.schema, json, {templateDir: 'parameter-serialization', property: param.name, required: param.required, destination: state.destination, section: state.section, primitive: true, skipTitleOnce: true })).join('\n') : '' - // this was wrong... check when we merge if it was fixed - const callbackSerializedList = event && (type === 'methods') ? types.getSchemaShape(event.result.schema, json, { templateDir: eventHasOptionalParam(event) && !event.tags.find(t => t.name === 'provider') ? 'callback-serialization' : 'callback-result-serialization', property: result.name, required: event.result.schema.required, destination: state.destination, section: state.section, primitive: true, skipTitleOnce: true }) : '' - const callbackInitialization = event && (type === 'methods') ? (eventHasOptionalParam(event) && !event.tags.find(t => t.name === 'provider') ? (event.params.map(param => isOptionalParam(param) ? types.getSchemaShape(param.schema, json, { templateDir: 'callback-initialization-optional', property: param.name, required: param.required, destination: state.destination, section: state.section, primitive: true, skipTitleOnce: true }) : '').filter(param => param).join('\n') + '\n') : '' ) + (types.getSchemaShape(event.result.schema, json, { templateDir: 'callback-initialization', property: result.name, destination: state.destination, section: state.section, primitive: true, skipTitleOnce: true })) : '' - let callbackInstantiation = '' - if (event) { - if (eventHasOptionalParam(event) && !event.tags.find(t => t.name === 'provider')) { - callbackInstantiation = (type === 'methods') ? types.getSchemaShape(event.result.schema, json, { templateDir: 'callback-instantiation', property: result.name, destination: state.destination, section: state.section, primitive: true, skipTitleOnce: true }) : '' - let paramInstantiation = (type === 'methods') ? event.params.map(param => isOptionalParam(param) ? types.getSchemaShape(param.schema, json, { templateDir: 'callback-context-instantiation', property: param.name, required: param.required, destination: state.destination, section: state.section, primitive: true, skipTitleOnce: true }) : '').filter(param => param).join('\n') : '' - let resultInitialization = (type === 'methods') ? types.getSchemaShape(event.result.schema, json, { templateDir: 'callback-value-initialization', property: result.name, destination: state.destination, section: state.section, primitive: true, skipTitleOnce: true }) : '' - let resultInstantiation = (type === 'methods') ? types.getSchemaShape(event.result.schema, json, { templateDir: 'callback-value-instantiation', property: result.name, destination: state.destination, section: state.section, primitive: true, skipTitleOnce: true }) : '' - callbackInstantiation = callbackInstantiation - .replace(/\$\{callback\.param\.instantiation\.with\.indent\}/g, indent(paramInstantiation, ' ', 3)) - .replace(/\$\{callback\.result\.initialization\.with\.indent\}/g, indent(resultInitialization, ' ', 1)) - .replace(/\$\{callback\.result\.instantiation\}/g, resultInstantiation) + const eventParams = event.params && event.params.length ? getTemplate('/sections/parameters', templates) + event.params.map(p => insertParameterMacros(getTemplate('/parameters/default', templates), p, event, document)).join('') : '' + const eventParamsRows = event.params && event.params.length ? event.params.map(p => insertParameterMacros(getTemplate('/parameters/default', templates), p, event, document)).join('') : '' + + let itemName = '' + let itemType = '' + + // grab some related methdos in case they are output together in a single template file + const puller = server.methods.find(method => method.tags.find(tag => tag.name === 'event' && tag['x-pulls-for'] === methodObj.name)) + const pullsFor = methodObj.tags.find(t => t['x-pulls-for']) && server.methods.find(method => method.name === methodObj.tags.find(t => t['x-pulls-for'])['x-pulls-for']) + const pullerTemplate = (puller ? insertMethodMacros(getTemplate('/codeblocks/puller', templates), puller, server, client, templates, type, generateExamples(puller, templates, languages)) : '') + const setter = getSetterFor(methodObj.name, server) + const setterTemplate = (setter ? insertMethodMacros(getTemplate('/codeblocks/setter', templates), setter, server, client, templates, type, generateExamples(setter, templates, languages)) : '') + const subscriber = server.methods.find(method => method.tags.find(tag => tag['x-subscriber-for'] === methodObj.name)) + const subscriberTemplate = (subscriber ? insertMethodMacros(getTemplate('/codeblocks/subscriber', templates), subscriber, server, client, templates, type, generateExamples(subscriber, templates, languages)) : '') + const setterFor = methodObj.tags.find(t => t.name === 'setter') && methodObj.tags.find(t => t.name === 'setter')['x-setter-for'] || '' + const pullsResult = (puller || pullsFor) ? localizeDependencies(pullsFor || methodObj, server).params.findLast(x=>true).schema : null + const pullsParams = (puller || pullsFor) ? localizeDependencies(getPayloadFromEvent(puller || methodObj, document), document, null, { mergeAllOfs: true }).properties.parameters : null + const pullsResultType = (pullsResult && (type === 'methods')) ? Types.getSchemaShape(pullsResult, server, { templateDir: state.typeTemplateDir, namespace: !config.copySchemasIntoModules }) : '' + const pullsForType = pullsResult && Types.getSchemaType(pullsResult, server, { templateDir: state.typeTemplateDir, namespace: !config.copySchemasIntoModules }) + const pullsParamsType = (pullsParams && (type === 'methods')) ? Types.getSchemaShape(pullsParams, server, { templateDir: state.typeTemplateDir, namespace: !config.copySchemasIntoModules }) : '' + const pullsForParamTitle = pullsParams ? pullsParams.title.charAt(0).toLowerCase() + pullsParams.title.substring(1) : '' + const pullsForResultTitle = (pullsResult && pullsResult.title) ? pullsResult.title.charAt(0).toLowerCase() + pullsResult.title.substring(1) : '' + const pullsResponseInit = (pullsParams && (type === 'methods')) ? Types.getSchemaShape(pullsParams, server, { templateDir: 'result-initialization', property: pullsForParamTitle, required: pullsParams.required, primitive: true, skipTitleOnce: true, namespace: !config.copySchemasIntoModules }) : '' + const pullsResponseInst = (pullsParams && (type === 'methods')) ? Types.getSchemaShape(pullsParams, server, { templateDir: 'result-instantiation', property: pullsForParamTitle, required: pullsParams.required, primitive: true, skipTitleOnce: true, namespace: !config.copySchemasIntoModules }) : '' + const pullsResultSerialize = (pullsResult && (type === 'methods')) ? Types.getSchemaShape(pullsResult, server, { templateDir: 'parameter-serialization/sub-property', property: pullsForResultTitle, required: pullsResult.required, primitive: true, skipTitleOnce: true, namespace: !config.copySchemasIntoModules }) : '' + + const serializedParams = (type === 'methods') ? flattenedMethod.params.map(param => Types.getSchemaShape(param.schema, server, { templateDir: 'parameter-serialization', property: param.name, required: param.required, primitive: true, skipTitleOnce: true, namespace: !config.copySchemasIntoModules })).join('\n') : '' + const resultInst = result && (type === 'methods') ? Types.getSchemaShape(flattenedMethod.result.schema, server, { templateDir: 'result-instantiation', property: flattenedMethod.result.name, required: flattenedMethod.result.required, primitive: true, skipTitleOnce: true, namespace: !config.copySchemasIntoModules }) : '' // w/out primitive: true, getSchemaShape skips anonymous types, like primitives + const resultInit = result && (type === 'methods') ? Types.getSchemaShape(flattenedMethod.result.schema, server, { templateDir: 'result-initialization', property: flattenedMethod.result.name, primitive: true, skipTitleOnce: true, namespace: !config.copySchemasIntoModules }) : '' // w/out primitive: true, getSchemaShape skips anonymous types, like primitives + const serializedEventParams = event && (type === 'methods') ? flattenedMethod.params.filter(p => p.name !== 'listen').map(param => Types.getSchemaShape(param.schema, document, {templateDir: 'parameter-serialization', property: param.name, required: param.required, primitive: true, skipTitleOnce: true, namespace: !config.copySchemasIntoModules })).join('\n') : '' + // this was wrong... check when we merge if it was fixed + const callbackSerializedList = event && (type === 'methods') ? Types.getSchemaShape(event.result.schema, document, { templateDir: eventHasOptionalParam(event) && !event.tags.find(t => t.name === 'provider') ? 'callback-serialization' : 'callback-result-serialization', property: result.name, primitive: true, skipTitleOnce: true, namespace: !config.copySchemasIntoModules }) : '' + const callbackInitialization = event && (type === 'methods') ? (eventHasOptionalParam(event) && !event.tags.find(t => t.name === 'provider') ? (event.params.map(param => isOptionalParam(param) ? Types.getSchemaShape(param.schema, document, { templateDir: 'callback-initialization-optional', property: param.name, required: param.required, primitive: true, skipTitleOnce: true }) : '').filter(param => param).join('\n') + '\n') : '' ) + (Types.getSchemaShape(event.result.schema, document, { templateDir: 'callback-initialization', property: result.name, primitive: true, skipTitleOnce: true, namespace: !config.copySchemasIntoModules })) : '' + let callbackInstantiation = '' + if (event) { + if (eventHasOptionalParam(event) && !event.tags.find(t => t.name === 'provider')) { + callbackInstantiation = (type === 'methods') ? Types.getSchemaShape(event.result.schema, document, { templateDir: 'callback-instantiation', property: result.name, primitive: true, skipTitleOnce: true, namespace: !config.copySchemasIntoModules }) : '' + let paramInstantiation = (type === 'methods') ? event.params.map(param => isOptionalParam(param) ? Types.getSchemaShape(param.schema, document, { templateDir: 'callback-context-instantiation', property: param.name, required: param.required, primitive: true, skipTitleOnce: true, namespace: !config.copySchemasIntoModules }) : '').filter(param => param).join('\n') : '' + let resultInitialization = (type === 'methods') ? Types.getSchemaShape(event.result.schema, document, { templateDir: 'callback-value-initialization', property: result.name, primitive: true, skipTitleOnce: true, namespace: !config.copySchemasIntoModules }) : '' + let resultInstantiation = (type === 'methods') ? Types.getSchemaShape(event.result.schema, document, { templateDir: 'callback-value-instantiation', property: result.name, primitive: true, skipTitleOnce: true, namespace: !config.copySchemasIntoModules }) : '' + callbackInstantiation = callbackInstantiation + .replace(/\$\{callback\.param\.instantiation\.with\.indent\}/g, indent(paramInstantiation, ' ', 3)) + .replace(/\$\{callback\.result\.initialization\.with\.indent\}/g, indent(resultInitialization, ' ', 1)) + .replace(/\$\{callback\.result\.instantiation\}/g, resultInstantiation) + } + else { + callbackInstantiation = (type === 'methods') ? Types.getSchemaShape(event.result.schema, document, { templateDir: 'callback-result-instantiation', property: result.name, primitive: true, skipTitleOnce: true, namespace: !config.copySchemasIntoModules }) : '' + } } - else { - callbackInstantiation = (type === 'methods') ? types.getSchemaShape(event.result.schema, json, { templateDir: 'callback-result-instantiation', property: result.name, destination: state.destination, section: state.section, primitive: true, skipTitleOnce: true }) : '' + // hmm... how is this different from callbackSerializedList? i guess they get merged? + const callbackResponseInst = event && (type === 'methods') ? (eventHasOptionalParam(event) ? (event.params.map(param => isOptionalParam(param) ? Types.getSchemaShape(param.schema, document, { templateDir: 'callback-response-instantiation', property: param.name, required: param.required, primitive: true, skipTitleOnce: true, namespace: !config.copySchemasIntoModules }) : '').filter(param => param).join(', ') + ', ') : '' ) + (Types.getSchemaShape(event.result.schema, document, { templateDir: 'callback-response-instantiation', property: result.name, primitive: true, skipTitleOnce: true, namespace: !config.copySchemasIntoModules })) : '' + const resultType = result && result.schema ? Types.getSchemaType(result.schema, server, { templateDir: state.typeTemplateDir, namespace: !config.copySchemasIntoModules }) : '' + const resultSchemaType = result && result.schema.type + const resultJsonType = result && result.schema ? Types.getSchemaType(result.schema, server, { templateDir: 'json-types', namespace: !config.copySchemasIntoModules }) : '' + + try { + generateResultParams(result.schema, server, templates, { name: result.name}) } - } - // hmm... how is this different from callbackSerializedList? i guess they get merged? - const callbackResponseInst = event && (type === 'methods') ? (eventHasOptionalParam(event) ? (event.params.map(param => isOptionalParam(param) ? types.getSchemaShape(param.schema, json, { templateDir: 'callback-response-instantiation', property: param.name, required: param.required, destination: state.destination, section: state.section, primitive: true, skipTitleOnce: true }) : '').filter(param => param).join(', ') + ', ') : '' ) + (types.getSchemaShape(event.result.schema, json, { templateDir: 'callback-response-instantiation', property: result.name, destination: state.destination, section: state.section, primitive: true, skipTitleOnce: true })) : '' - const resultType = result.schema ? types.getSchemaType(result.schema, json, { templateDir: state.typeTemplateDir }) : '' - const resultSchemaType = result.schema.type - const resultJsonType = result.schema ? types.getSchemaType(result.schema, json, { templateDir: 'json-types' }) : '' - const resultParams = generateResultParams(result.schema, json, templates, { name: result.name}) - - // todo: what does prefix do in Types.mjs? need to account for it somehow - const callbackResultJsonType = event && result.schema ? types.getSchemaType(result.schema, json, { templateDir: 'json-types' }) : '' - - const pullsForParamType = pullsParams ? types.getSchemaType(pullsParams, json, { destination: state.destination, section: state.section }) : '' - const pullsForJsonType = pullsResult ? types.getSchemaType(pullsResult, json, { templateDir: 'json-types' }) : '' - const pullsForParamJsonType = pullsParams ? types.getSchemaType(pullsParams, json, { templateDir: 'json-types' }) : '' - - const pullsEventParamName = event ? types.getSchemaInstantiation(event.result, json, event.name, { instantiationType: 'pull.param.name' }) : '' + catch (e) { + console.dir(methodObj) + } + const resultParams = result && generateResultParams(result.schema, server, templates, { name: result.name}) - let seeAlso = '' - if (isPolymorphicPullMethod(methodObj) && pullsForType) { - seeAlso = `See also: [${pullsForType}](#${pullsForType.toLowerCase()}-1)` // this assumes the schema will be after the method... - } - else if (methodObj.tags.find(t => t.name === 'polymorphic-pull')) { - const type = method.name[0].toUpperCase() + method.name.substring(1) - seeAlso = `See also: [${type}](#${type.toLowerCase()}-1)` // this assumes the schema will be after the method... - } + // todo: what does prefix do in Types.mjs? need to account for it somehow + const callbackResultJsonType = event && result.schema ? Types.getSchemaType(result.schema, document, { templateDir: 'json-types', namespace: !config.copySchemasIntoModules }) : '' - if (isTemporalSetMethod(methodObj)) { - itemName = result.schema.items.title || 'item' - itemName = itemName.charAt(0).toLowerCase() + itemName.substring(1) - itemType = types.getSchemaType(result.schema.items, json, { destination: state.destination, templateDir: state.typeTemplateDir, section: state.section }) - } + const pullsForParamType = pullsParams ? Types.getSchemaType(pullsParams, server, { namespace: !config.copySchemasIntoModules }) : '' + const pullsForJsonType = pullsResult ? Types.getSchemaType(pullsResult, server, { templateDir: 'json-types', namespace: !config.copySchemasIntoModules }) : '' + const pullsForParamJsonType = pullsParams ? Types.getSchemaType(pullsParams, server, { templateDir: 'json-types', namespace: !config.copySchemasIntoModules }) : '' + + const pullsEventParamName = event ? Types.getSchemaInstantiation(event.result, document, event.name, { instantiationType: 'pull.param.name', namespace: !config.copySchemasIntoModules }) : '' - let signature - - if (Object.keys(languages).length && template.indexOf('${method.signature}') >= 0) { - const lang = languages[Object.keys(languages)[0]] - signature = getTemplateForDeclaration(methodObj, templates, 'declarations') - types.setTemplates(lang) - const currentConfig = JSON.parse(JSON.stringify(config)) - config.operators = config.operators || {} - config.operators.paramDelimiter = ', ' - signature = insertMethodMacros(signature, methodObj, json, lang, type) - config = currentConfig - types.setTemplates(templates) - } - else { - signature = '' - } + let seeAlso = '' + if (isPolymorphicPullMethod(methodObj) && pullsForType) { + seeAlso = `See also: [${pullsForType}](#${pullsForType.toLowerCase()}-1)` // this assumes the schema will be after the method... + } + else if (methodObj.tags.find(t => t.name === 'polymorphic-pull')) { + const type = method.name[0].toUpperCase() + method.name.substring(1) + seeAlso = `See also: [${type}](#${type.toLowerCase()}-1)` // this assumes the schema will be after the method... + } - template = insertExampleMacros(template, examples[methodObj.name] || [], methodObj, json, templates) - template = template.replace(/\$\{method\.name\}/g, method.name) - .replace(/\$\{method\.rpc\.name\}/g, methodObj.rpc_name || methodObj.name) - .replace(/\$\{method\.summary\}/g, methodObj.summary) - .replace(/\$\{method\.description\}/g, methodObj.description - || methodObj.summary) - // Parameter stuff - .replace(/\$\{method\.params\}/g, params) - .replace(/\$\{method\.params\.table\.rows\}/g, paramsRows) - .replace(/\$\{method\.params\.annotations\}/g, paramsAnnotations) - .replace(/\$\{method\.params\.json\}/g, paramsJson) - .replace(/\$\{method\.params\.list\}/g, method.params) - .replace(/\$\{method\.params\.array\}/g, JSON.stringify(methodObj.params.map(p => p.name))) - .replace(/\$\{method\.params\.count}/g, methodObj.params ? methodObj.params.length : 0) - .replace(/\$\{if\.params\}(.*?)\$\{end\.if\.params\}/gms, method.params.length ? '$1' : '') - .replace(/\$\{if\.result\}(.*?)\$\{end\.if\.result\}/gms, resultType ? '$1' : '') - .replace(/\$\{if\.result.nonvoid\}(.*?)\$\{end\.if\.result.nonvoid\}/gms, resultType && resultType !== 'void' ? '$1' : '') - .replace(/\$\{if\.result.nonboolean\}(.*?)\$\{end\.if\.result.nonboolean\}/gms, resultSchemaType && resultSchemaType !== 'boolean' ? '$1' : '') - .replace(/\$\{if\.result\.properties\}(.*?)\$\{end\.if\.result\.properties\}/gms, resultParams ? '$1' : '') - .replace(/\$\{if\.params\.empty\}(.*?)\$\{end\.if\.params\.empty\}/gms, method.params.length === 0 ? '$1' : '') - .replace(/\$\{if\.signature\.empty\}(.*?)\$\{end\.if\.signature\.empty\}/gms, (method.params.length === 0 && resultType === '') ? '$1' : '') - .replace(/\$\{if\.context\}(.*?)\$\{end\.if\.context\}/gms, event && event.params.length ? '$1' : '') - .replace(/\$\{method\.params\.serialization\}/g, serializedParams) - .replace(/\$\{method\.params\.serialization\.with\.indent\}/g, indent(serializedParams, ' ')) - // Typed signature stuff - .replace(/\$\{method\.signature\}/g, signature) - .replace(/\$\{method\.signature\.params\}/g, types.getMethodSignatureParams(methodObj, json, { destination: state.destination, section: state.section })) - .replace(/\$\{method\.signature\.result\}/g, types.getMethodSignatureResult(methodObj, json, { destination: state.destination, section: state.section })) - .replace(/\$\{method\.context\}/g, method.context.join(', ')) - .replace(/\$\{method\.context\.array\}/g, JSON.stringify(method.context)) - .replace(/\$\{method\.context\.count}/g, method.context ? method.context.length : 0) - .replace(/\$\{method\.deprecation\}/g, deprecation) - .replace(/\$\{method\.Name\}/g, method.name[0].toUpperCase() + method.name.substr(1)) - .replace(/\$\{event\.name\}/g, method.name.toLowerCase()[2] + method.name.substr(3)) - .replace(/\$\{event\.params\}/g, eventParams) - .replace(/\$\{event\.params\.table\.rows\}/g, eventParamsRows) - .replace(/\$\{if\.event\.params\}(.*?)\$\{end\.if\.event\.params\}/gms, event && event.params.length ? '$1' : '') - .replace(/\$\{if\.event\.callback\.params\}(.*?)\$\{end\.if\.event\.callback\.params\}/gms, event && eventHasOptionalParam(event) ? '$1' : '') - .replace(/\$\{event\.signature\.params\}/g, event ? types.getMethodSignatureParams(event, json, { destination: state.destination, section: state.section }) : '') - .replace(/\$\{event\.signature\.callback\.params\}/g, event ? types.getMethodSignatureParams(event, json, { destination: state.destination, section: state.section, callback: true }) : '') - .replace(/\$\{event\.params\.serialization\}/g, serializedEventParams) - .replace(/\$\{event\.callback\.serialization\}/g, callbackSerializedList) - .replace(/\$\{event\.callback\.initialization\}/g, callbackInitialization) - .replace(/\$\{event\.callback\.instantiation\}/g, callbackInstantiation) - .replace(/\$\{event\.callback\.response\.instantiation\}/g, callbackResponseInst) - .replace(/\$\{info\.title\.lowercase\}/g, info.title.toLowerCase()) - .replace(/\$\{info\.title\}/g, info.title) - .replace(/\$\{info\.Title\}/g, capitalize(info.title)) - .replace(/\$\{info\.TITLE\}/g, info.title.toUpperCase()) - .replace(/\$\{method\.property\.immutable\}/g, hasTag(methodObj, 'property:immutable')) - .replace(/\$\{method\.property\.readonly\}/g, !getSetterFor(methodObj.name, json)) - .replace(/\$\{method\.temporalset\.add\}/g, temporalAddName) - .replace(/\$\{method\.temporalset\.remove\}/g, temporalRemoveName) - .replace(/\$\{method\.transforms}/g, JSON.stringify(method.transforms)) - .replace(/\$\{method\.seeAlso\}/g, seeAlso) - .replace(/\$\{method\.item\}/g, itemName) - .replace(/\$\{method\.item\.type\}/g, itemType) - .replace(/\$\{method\.capabilities\}/g, capabilities) - .replace(/\$\{method\.result\.name\}/g, result.name) - .replace(/\$\{method\.result\.summary\}/g, result.summary) - .replace(/\$\{method\.result\.link\}/g, getLinkForSchema(result.schema, json)) //, baseUrl: options.baseUrl - .replace(/\$\{method\.result\.type\}/g, types.getSchemaType(result.schema, json, { templateDir: state.typeTemplateDir, title: true, asPath: false, destination: state.destination, result: true })) //, baseUrl: options.baseUrl - .replace(/\$\{method\.result\.json\}/g, types.getSchemaType(result.schema, json, { templateDir: 'json-types', destination: state.destination, section: state.section, title: true, code: false, link: false, asPath: false, expandEnums: false, namespace: true })) - // todo: what does prefix do? - .replace(/\$\{event\.result\.type\}/g, isEventMethod(methodObj) ? types.getMethodSignatureResult(event, json, { destination: state.destination, section: state.section, callback: true }) : '') - .replace(/\$\{event\.result\.json\.type\}/g, resultJsonType) - .replace(/\$\{event\.result\.json\.type\}/g, callbackResultJsonType) - .replace(/\$\{event\.pulls\.param\.name\}/g, pullsEventParamName) - .replace(/\$\{method\.result\}/g, generateResult(result.schema, json, templates, { name: result.name })) - .replace(/\$\{method\.result\.json\.type\}/g, resultJsonType) - .replace(/\$\{method\.result\.instantiation\}/g, resultInst) - .replace(/\$\{method\.result\.initialization\}/g, resultInit) - .replace(/\$\{method\.result\.properties\}/g, resultParams) - .replace(/\$\{method\.result\.instantiation\.with\.indent\}/g, indent(resultInst, ' ')) - .replace(/\$\{method\.example\.value\}/g, JSON.stringify(methodObj.examples[0].result.value)) - .replace(/\$\{method\.alternative\}/g, method.alternative) - .replace(/\$\{method\.alternative.link\}/g, '#' + (method.alternative || "").toLowerCase()) - .replace(/\$\{method\.pulls\.for\}/g, pullsFor ? pullsFor.name : '') - .replace(/\$\{method\.pulls\.type\}/g, pullsForType) - .replace(/\$\{method\.pulls\.json\.type\}/g, pullsForJsonType) - .replace(/\$\{method\.pulls\.result\}/g, pullsResultType) - .replace(/\$\{method\.pulls\.result\.title\}/g, pullsForResultTitle) - .replace(/\$\{method\.pulls\.params.type\}/g, pullsParams ? pullsParams.title : '') - .replace(/\$\{method\.pulls\.params\}/g, pullsParamsType) - .replace(/\$\{method\.pulls\.param\.type\}/g, pullsForParamType) - .replace(/\$\{method\.pulls\.param\.title\}/g, pullsForParamTitle) - .replace(/\$\{method\.pulls\.param\.json\.type\}/g, pullsForParamJsonType) - .replace(/\$\{method\.pulls\.response\.initialization\}/g, pullsResponseInit) - .replace(/\$\{method\.pulls\.response\.instantiation}/g, pullsResponseInst) - .replace(/\$\{method\.pulls\.result\.serialization\.with\.indent\}/g, indent(pullsResultSerialize, ' ', 3, 2)) - .replace(/\$\{method\.setter\.for\}/g, setterFor) - .replace(/\$\{method\.puller\}/g, pullerTemplate) // must be last!! - .replace(/\$\{method\.setter\}/g, setterTemplate) // must be last!! - .replace(/\$\{method\.subscriber\}/g, subscriberTemplate) // must be last!! - - - if (method.deprecated) { - template = template.replace(/\$\{if\.deprecated\}(.*?)\$\{end\.if\.deprecated\}/gms, '$1') - } - else { - template = template.replace(/\$\{if\.deprecated\}(.*?)\$\{end\.if\.deprecated\}/gms, '') - } + if (isTemporalSetMethod(methodObj)) { + itemName = result.schema.items.title || 'item' + itemName = itemName.charAt(0).toLowerCase() + itemName.substring(1) + itemType = Types.getSchemaType(result.schema.items, server, { templateDir: state.typeTemplateDir, namespace: !config.copySchemasIntoModules }) + } - // method.params[n].xxx macros - const matches = [...template.matchAll(/\$\{method\.params\[([0-9]+)\]\.type\}/g)] - matches.forEach(match => { - const index = parseInt(match[1]) - template = template.replace(/\$\{method\.params\[([0-9]+)\]\.type\}/g, types.getSchemaType(methodObj.params[index].schema, json, { destination: state.destination, templateDir: state.typeTemplateDir })) - template = template.replace(/\$\{method\.params\[([0-9]+)\]\.name\}/g, methodObj.params[index].name) - }) + let signature + + if (Object.keys(languages).length && template.indexOf('${method.signature}') >= 0) { + const lang = languages[Object.keys(languages)[0]] + signature = getTemplateForDeclaration(methodObj, templates, 'declarations') + Types.setTemplates(lang) + const currentConfig = JSON.parse(JSON.stringify(config)) + config.operators = config.operators || {} + config.operators.paramDelimiter = ', ' + signature = insertMethodMacros(signature, methodObj, server, client, lang, type) + config = currentConfig + Types.setTemplates(templates) + } + else { + signature = '' + } + + template = insertExampleMacros(template, examples || [], methodObj, server, templates) + + template = template.replace(/\$\{method\.name\}/g, method.name) + .replace(/\$\{method\.rpc\.name\}/g, methodObj.rpc_name || methodObj.name) + .replace(/\$\{method\.summary\}/g, methodObj.summary) + .replace(/\$\{method\.description\}/g, methodObj.description + || methodObj.summary) + // Parameter stuff + .replace(/\$\{method\.params\}/g, params) + .replace(/\$\{method\.params\.table\.rows\}/g, paramsRows) + .replace(/\$\{method\.params\.annotations\}/g, paramsAnnotations) + .replace(/\$\{method\.params\.json\}/g, paramsJson) + .replace(/\$\{method\.params\.list\}/g, method.params) + .replace(/\$\{method\.params\.array\}/g, JSON.stringify(methodObj.params.map(p => p.name))) + .replace(/\$\{method\.params\.count}/g, methodObj.params ? methodObj.params.length : 0) + .replace(/\$\{if\.params\}(.*?)\$\{end\.if\.params\}/gms, method.params.length ? '$1' : '') + .replace(/\$\{if\.result\}(.*?)\$\{end\.if\.result\}/gms, resultType ? '$1' : '') + .replace(/\$\{if\.result.nonvoid\}(.*?)\$\{end\.if\.result.nonvoid\}/gms, resultType && resultType !== 'void' ? '$1' : '') + .replace(/\$\{if\.result.nonboolean\}(.*?)\$\{end\.if\.result.nonboolean\}/gms, resultSchemaType && resultSchemaType !== 'boolean' ? '$1' : '') + .replace(/\$\{if\.result\.properties\}(.*?)\$\{end\.if\.result\.properties\}/gms, resultParams ? '$1' : '') + .replace(/\$\{if\.params\.empty\}(.*?)\$\{end\.if\.params\.empty\}/gms, method.params.length === 0 ? '$1' : '') + .replace(/\$\{if\.signature\.empty\}(.*?)\$\{end\.if\.signature\.empty\}/gms, (method.params.length === 0 && resultType === '') ? '$1' : '') + .replace(/\$\{if\.context\}(.*?)\$\{end\.if\.context\}/gms, event && event.params.length ? '$1' : '') + .replace(/\$\{method\.params\.serialization\}/g, serializedParams) + .replace(/\$\{method\.params\.serialization\.with\.indent\}/g, indent(serializedParams, ' ')) + // Typed signature stuff + .replace(/\$\{method\.signature\}/g, signature) + .replace(/\$\{method\.signature\.params\}/g, Types.getMethodSignatureParams(methodObj, server, { namespace: !config.copySchemasIntoModules })) + .replace(/\$\{method\.signature\.result\}/g, Types.getMethodSignatureResult(methodObj, server, { namespace: !config.copySchemasIntoModules })) + .replace(/\$\{method\.context\}/g, method.context.join(', ')) + .replace(/\$\{method\.context\.array\}/g, JSON.stringify(method.context)) + .replace(/\$\{method\.context\.count}/g, method.context ? method.context.length : 0) + .replace(/\$\{method\.deprecation\}/g, deprecation) + .replace(/\$\{method\.Name\}/g, method.name[0].toUpperCase() + method.name.substr(1)) + .replace(/\$\{event\.name\}/g, method.name.toLowerCase()[2] + method.name.substr(3)) + .replace(/\$\{event\.params\}/g, eventParams) + .replace(/\$\{event\.params\.table\.rows\}/g, eventParamsRows) + .replace(/\$\{if\.event\.params\}(.*?)\$\{end\.if\.event\.params\}/gms, event && event.params.length ? '$1' : '') + .replace(/\$\{if\.event\.callback\.params\}(.*?)\$\{end\.if\.event\.callback\.params\}/gms, event && eventHasOptionalParam(event) ? '$1' : '') + .replace(/\$\{event\.signature\.params\}/g, event ? Types.getMethodSignatureParams(event, document, { namespace: !config.copySchemasIntoModules }) : '') + .replace(/\$\{event\.signature\.callback\.params\}/g, event ? Types.getMethodSignatureParams(event, document, { callback: true, namespace: !config.copySchemasIntoModules }) : '') + .replace(/\$\{event\.params\.serialization\}/g, serializedEventParams) + .replace(/\$\{event\.callback\.serialization\}/g, callbackSerializedList) + .replace(/\$\{event\.callback\.initialization\}/g, callbackInitialization) + .replace(/\$\{event\.callback\.instantiation\}/g, callbackInstantiation) + .replace(/\$\{event\.callback\.response\.instantiation\}/g, callbackResponseInst) + .replace(/\$\{info\.title\.lowercase\}/g, info.title.toLowerCase()) + .replace(/\$\{info\.title\}/g, info.title) + .replace(/\$\{info\.Title\}/g, capitalize(info.title)) + .replace(/\$\{info\.TITLE\}/g, info.title.toUpperCase()) + .replace(/\$\{method\.property\.immutable\}/g, hasTag(methodObj, 'property:immutable')) + .replace(/\$\{method\.property\.readonly\}/g, !getSetterFor(methodObj.name, server)) + .replace(/\$\{method\.temporalset\.add\}/g, temporalAddName) + .replace(/\$\{method\.temporalset\.remove\}/g, temporalRemoveName) + .replace(/\$\{method\.transform}/g, method.transform) + .replace(/\$\{method\.transforms}/g, JSON.stringify(method.transforms)) + .replace(/\$\{method\.seeAlso\}/g, seeAlso) + .replace(/\$\{method\.item\}/g, itemName) + .replace(/\$\{method\.item\.type\}/g, itemType) + .replace(/\$\{method\.capabilities\}/g, capabilities) + .replace(/\$\{method\.result\.name\}/g, result.name) + .replace(/\$\{method\.result\.summary\}/g, result.summary) + .replace(/\$\{method\.result\.link\}/g, getLinkForSchema(result.schema, server)) //, baseUrl: options.baseUrl + .replace(/\$\{method\.result\.type\}/g, Types.getSchemaType(result.schema, server, { templateDir: state.typeTemplateDir, title: true, asPath: false, result: true, namespace: !config.copySchemasIntoModules })) //, baseUrl: options.baseUrl + .replace(/\$\{method\.result\.json\}/g, Types.getSchemaType(result.schema, server, { templateDir: 'json-types', title: true, code: false, link: false, asPath: false, expandEnums: false, namespace: !config.copySchemasIntoModules })) + .replace(/\$\{event\.result\.type\}/g, isEventMethod(methodObj) ? Types.getSchemaType(event.result.schema, document, { templateDir: state.typeTemplateDir, title: true, asPath: false, result: true, namespace: !config.copySchemasIntoModules }) : '') + .replace(/\$\{event\.result\.name\}/g, isEventMethod(methodObj) ? event.result.name : '') + .replace(/\$\{event\.result\.json\.type\}/g, resultJsonType) + .replace(/\$\{event\.result\.json\.type\}/g, callbackResultJsonType) + .replace(/\$\{event\.pulls\.param\.name\}/g, pullsEventParamName) + .replace(/\$\{method\.result\}/g, generateResult(result.schema, server, templates, { name: result.name })) + .replace(/\$\{method\.result\.json\.type\}/g, resultJsonType) + .replace(/\$\{method\.result\.instantiation\}/g, resultInst) + .replace(/\$\{method\.result\.initialization\}/g, resultInit) + .replace(/\$\{method\.result\.properties\}/g, resultParams) + .replace(/\$\{method\.result\.instantiation\.with\.indent\}/g, indent(resultInst, ' ')) + .replace(/\$\{method\.example\.value\}/g, methodObj.examples[0].result ? JSON.stringify(methodObj.examples[0].result.value ) : '') + .replace(/\$\{method\.alternative\}/g, method.alternative ? method.alternative.split('.').pop() : '') + .replace(/\$\{method\.alternative.link\}/g, '#' + (method.alternative || "").toLowerCase()) + .replace(/\$\{method\.pulls\.for\}/g, pullsFor ? methodName(pullsFor) : '') + .replace(/\$\{method\.pulls\.type\}/g, pullsForType) + .replace(/\$\{method\.pulls\.json\.type\}/g, pullsForJsonType) + .replace(/\$\{method\.pulls\.result\}/g, pullsResultType) + .replace(/\$\{method\.pulls\.result\.title\}/g, pullsForResultTitle) + .replace(/\$\{method\.pulls\.params.type\}/g, pullsParams ? pullsParams.title : '') + .replace(/\$\{method\.pulls\.params\}/g, pullsParamsType) + .replace(/\$\{method\.pulls\.param\.type\}/g, pullsForParamType) + .replace(/\$\{method\.pulls\.param\.title\}/g, pullsForParamTitle) + .replace(/\$\{method\.pulls\.param\.json\.type\}/g, pullsForParamJsonType) + .replace(/\$\{method\.pulls\.response\.initialization\}/g, pullsResponseInit) + .replace(/\$\{method\.pulls\.response\.instantiation}/g, pullsResponseInst) + .replace(/\$\{method\.pulls\.result\.serialization\.with\.indent\}/g, indent(pullsResultSerialize, ' ', 3, 2)) + .replace(/\$\{method\.setter\.for\}/g, setterFor.split('.').pop()) + .replace(/\$\{method\.interface\}/g, extension(methodObj, 'x-interface')) + .replace(/\$\{method\.puller\}/g, pullerTemplate) // must be last!! + .replace(/\$\{method\.setter\}/g, setterTemplate) // must be last!! + .replace(/\$\{method\.subscriber\}/g, subscriberTemplate) // must be last!! + + + if (method.deprecated) { + template = template.replace(/\$\{if\.deprecated\}(.*?)\$\{end\.if\.deprecated\}/gms, '$1') + } + else { + template = template.replace(/\$\{if\.deprecated\}(.*?)\$\{end\.if\.deprecated\}/gms, '') + } + + // method.params[n].xxx macros + const matches = [...template.matchAll(/\$\{method\.params\[([0-9]+)\]\.type\}/g)] + matches.forEach(match => { + const index = parseInt(match[1]) + template = template.replace(/\$\{method\.params\[([0-9]+)\]\.type\}/g, Types.getSchemaType(methodObj.params[index].schema, server, { templateDir: state.typeTemplateDir, namespace: !config.copySchemasIntoModules })) + template = template.replace(/\$\{method\.params\[([0-9]+)\]\.name\}/g, methodObj.params[index].name) + }) - // Note that we do this twice to ensure all recursive macros are resolved - template = insertExampleMacros(template, examples[methodObj.name] || [], methodObj, json, templates) + // Note that we do this twice to ensure all recursive macros are resolved + template = insertExampleMacros(template, examples || [], methodObj, server, templates) - return template + return template + // } + // catch (error) { + // console.log(`Error processing method ${methodObj.name}`) + // console.dir(methodObj, { depth: 10 }) + // console.log() + // console.dir(error) + // process.exit(1) + // } } function insertExampleMacros(template, examples, method, json, templates) { @@ -1566,7 +1684,7 @@ function insertExampleMacros(template, examples, method, json, templates) { function generateResult(result, json, templates, { name = '' } = {}) { - const type = types.getSchemaType(result, json, { templateDir: state.typeTemplateDir, destination: state.destination, section: state.section }) + const type = Types.getSchemaType(result, json, { templateDir: state.typeTemplateDir, namespace: !config.copySchemasIntoModules }) if (result.type === 'object' && result.properties) { let content = getTemplate('/types/object', templates).split('\n') @@ -1587,13 +1705,13 @@ function generateResult(result, json, templates, { name = '' } = {}) { // if we get a real link use it if (link !== '#') { - return `[${types.getSchemaType(result, json, { destination: state.destination, templateDir: state.typeTemplateDir, section: state.section })}](${link})` + return `[${Types.getSchemaType(result, json, { templateDir: state.typeTemplateDir, namespace: !config.copySchemasIntoModules })}](${link})` } // otherwise this was a schema with no title, and we'll just copy it here else { const schema = localizeDependencies(result, json) return getTemplate('/types/default', templates) - .replace(/\$\{type\}/, types.getSchemaShape(schema, json, { templateDir: state.typeTemplateDir })) + .replace(/\$\{type\}/, Types.getSchemaShape(schema, json, { templateDir: state.typeTemplateDir, namespace: !config.copySchemasIntoModules })) } } else { @@ -1608,7 +1726,7 @@ function generateResultParams(result, json, templates, { name = '' } = {}) { if (result.$ref.includes("/x-schemas/")) { moduleTitle = result.$ref.split("/")[2] } - result = getJsonPath(result.$ref, json) + result = getReferencedSchema(result.$ref, json) } // const results are almost certainly `"const": "null"` so there's no need to include it in the method signature @@ -1620,7 +1738,7 @@ function generateResultParams(result, json, templates, { name = '' } = {}) { const template = getTemplate('/parameters/result', templates) return Object.entries(result.properties).map( ([name, type]) => template .replace(/\$\{method\.param\.name\}/g, name) - .replace(/\$\{method\.param\.type\}/g, types.getSchemaType(type, json, { moduleTitle: moduleTitle, result: true, namespace: true})) + .replace(/\$\{method\.param\.type\}/g, Types.getSchemaType(type, json, { moduleTitle: moduleTitle, result: true, namespace: !config.copySchemasIntoModules})) ).join(', ') // most languages separate params w/ a comma, so leaving this here for now } // tuples get unwrapped @@ -1629,14 +1747,14 @@ function generateResultParams(result, json, templates, { name = '' } = {}) { const template = getTemplate('/parameters/result', templates) return result.items.map( (type) => template .replace(/\$\{method\.param\.name\}/g, type['x-property']) - .replace(/\$\{method\.param\.type\}/g, types.getSchemaType(type, json, { moduleTitle: moduleTitle, result: true, namespace: true})) + .replace(/\$\{method\.param\.type\}/g, Types.getSchemaType(type, json, { moduleTitle: moduleTitle, result: true, namespace: !config.copySchemasIntoModules})) ).join(', ') } // everything else is just output as-is else { const template = getTemplate('/parameters/result', templates) - const type = types.getSchemaType(result, json, { moduleTitle: moduleTitle, result: true, namespace: true}) + const type = Types.getSchemaType(result, json, { moduleTitle: moduleTitle, result: true, namespace: !config.copySchemasIntoModules}) if (type === 'undefined') { console.log(`Warning: undefined type for ${name}`) } @@ -1647,40 +1765,51 @@ function generateResultParams(result, json, templates, { name = '' } = {}) { } } -function insertSchemaMacros(template, title, schema, module) { - return template.replace(/\$\{property\}/g, title) - .replace(/\$\{type\}/g, types.getSchemaType(schema, module, { templateDir: state.typeTemplateDir, destination: state.destination, section: state.section, code: false })) - .replace(/\$\{type.link\}/g, getLinkForSchema(schema, module)) +function insertSchemaMacros(template, title, schema, document) { + try { + return template.replace(/\$\{property\}/g, title) + .replace(/\$\{type\}/g, Types.getSchemaType(schema, document, { templateDir: state.typeTemplateDir, code: false, namespace: !config.copySchemasIntoModules })) + .replace(/\$\{type.link\}/g, getLinkForSchema(schema, document)) .replace(/\$\{description\}/g, schema.description || '') .replace(/\$\{name\}/g, title || '') + } + catch(error) { + console.log(`Error processing method ${schema.title}`) + console.dir(schema) + console.log() + console.dir(error) + process.exit(1) + } } -function insertParameterMacros(template, param, method, module) { +function insertParameterMacros(template, param, method, document) { //| `${method.param.name}` | ${method.param.type} | ${method.param.required} | ${method.param.summary} ${method.param.constraints} | - - let constraints = getSchemaConstraints(param, module) - let type = types.getSchemaType(param.schema, module, { templateDir: state.typeTemplateDir, destination: state.destination, section: state.section, code: false, link: false, asPath: false, expandEnums: false }) //baseUrl: options.baseUrl - let typeLink = getLinkForSchema(param.schema, module) - let jsonType = types.getSchemaType(param.schema, module, { templateDir: 'json-types', destination: state.destination, section: state.section, code: false, link: false, asPath: false, expandEnums: false }) - - if (constraints && type) { - constraints = '
' + constraints - } - - template = template - .replace(/\$\{method.param.name\}/g, param.name) - .replace(/\$\{method.param.Name\}/g, param.name[0].toUpperCase() + param.name.substring(1)) - .replace(/\$\{method.param.summary\}/g, param.summary || '') - .replace(/\$\{method.param.required\}/g, param.required || 'false') - .replace(/\$\{method.param.type\}/g, type) - .replace(/\$\{json.param.type\}/g, jsonType) - .replace(/\$\{method.param.link\}/g, getLinkForSchema(param.schema, module)) //getType(param)) - .replace(/\$\{method.param.constraints\}/g, constraints) //getType(param)) - + try { + let constraints = getSchemaConstraints(param, document) + let type = Types.getSchemaType(param.schema, document, { templateDir: state.typeTemplateDir, code: false, link: false, asPath: false, expandEnums: false, namespace: !config.copySchemasIntoModules }) //baseUrl: options.baseUrl + let typeLink = getLinkForSchema(param.schema, document) + let jsonType = Types.getSchemaType(param.schema, document, { templateDir: 'json-types', code: false, link: false, asPath: false, expandEnums: false, namespace: !config.copySchemasIntoModules }) + + if (constraints && type) { + constraints = '
' + constraints + } + return template + .replace(/\$\{method.param.name\}/g, param.name) + .replace(/\$\{method.param.Name\}/g, param.name[0].toUpperCase() + param.name.substring(1)) + .replace(/\$\{method.param.summary\}/g, param.summary || '') + .replace(/\$\{method.param.required\}/g, param.required || 'false') + .replace(/\$\{method.param.type\}/g, type) + .replace(/\$\{json.param.type\}/g, jsonType) + .replace(/\$\{method.param.link\}/g, getLinkForSchema(param.schema, document)) //getType(param)) + .replace(/\$\{method.param.constraints\}/g, constraints) //getType(param)) } + catch (e) { + console.dir(param, { depth: 100 }) + } +} function insertCapabilityMacros(template, capabilities, method, module) { const content = [] @@ -1704,62 +1833,37 @@ function insertCapabilityMacros(template, capabilities, method, module) { function generateXUsesInterfaces(json, templates) { let template = '' if (hasAllowFocusMethods(json)) { - const suffix = state.destination ? state.destination.split('.').pop() : '' - template = getTemplate(suffix ? `/sections/xuses-interfaces.${suffix}` : '/sections/xuses-interfaces', templates) - if (!template) { - template = getTemplate('/sections/xuses-interfaces', templates) - } + template = getTemplate('/sections/xuses-interfaces', templates) } return template } -function generateProviderSubscribe(json, templates) { - const interfaces = getProvidedCapabilities(json) - const suffix = state.destination ? state.destination.split('.').pop() : '' - let template = getTemplate(suffix ? `/sections/provider-subscribe.${suffix}` : '/sections/provider-subscribe', templates) +function generateProviderSubscribe(server, client, templates, bidirectional) { + const interfaces = getProvidedCapabilities(server) + let template = getTemplate(`/sections/provider-subscribe`, templates) const providers = reduce((acc, capability) => { - const template = insertProviderSubscribeMacros(getTemplate(suffix ? `/codeblocks/provider-subscribe.${suffix}` : '/codeblocks/provider-subscribe', templates), capability, json, templates) + const template = insertProviderSubscribeMacros(getTemplate('/codeblocks/provider-subscribe', templates), capability, server, client, templates, bidirectional) return acc + template }, '', interfaces) return interfaces.length ? template.replace(/\$\{providers\.list\}/g, providers) : '' } -function generateProviderInterfaces(json, templates) { - const interfaces = getProvidedCapabilities(json) - const suffix = state.destination ? state.destination.split('.').pop() : '' - - let template - if (suffix) { - template = getTemplate(`/sections/provider-interfaces.${suffix}`, templates) - } - if (!template) { - template = getTemplate('/sections/provider-interfaces', templates) - } +function generateProviderInterfaces(server, client, templates, codeblock, directory, bidirectional) { + const interfaces = getProvidedInterfaces(client || server) + + let template = getTemplate('/sections/provider-interfaces', templates) - const providers = reduce((acc, capability) => { - let providerTemplate = getTemplate(suffix ? `/codeblocks/provider.${suffix}` : '/codeblocks/provider', templates) - if (!providerTemplate) { - providerTemplate = getTemplate('/codeblocks/provider', templates) - } + const providers = reduce((acc, _interface) => { + let providerTemplate = getTemplate('/codeblocks/provider', templates) - const template = insertProviderInterfaceMacros(providerTemplate, capability, json, templates) + const template = insertProviderInterfaceMacros(providerTemplate, _interface, server, client, codeblock, directory, templates, bidirectional) return acc + template }, '', interfaces) return interfaces.length ? template.replace(/\$\{providers\.list\}/g, providers) : '' } -function getProviderInterfaceName(iface, capability, moduleJson = {}) { - const uglyName = capability.split(':').slice(-2).map(capitalize).map(x => x.split('-').map(capitalize).join('')).reverse().join('') + "Provider" - let name = iface.length === 1 ? iface[0].name.charAt(0).toUpperCase() + iface[0].name.substr(1) + "Provider" : uglyName - - if (moduleJson.info['x-interface-names']) { - name = moduleJson.info['x-interface-names'][capability] || name - } - return name -} - function getProviderXValues(method) { let xValues = [] if (method.tags.find(t => t['x-error']) || method.tags.find(t => t['x-response'])) { @@ -1775,60 +1879,53 @@ function getProviderXValues(method) { return xValues } -function insertProviderXValues(template, moduleJson, xValues) { +function insertProviderXValues(template, document, xValues) { if (xValues['x-response']) { - const xResponseInst = types.getSchemaShape(xValues['x-response'], moduleJson, { templateDir: 'parameter-serialization', property: 'result', required: true, destination: state.destination, section: state.section, primitive: true, skipTitleOnce: true }) + const xResponseInst = Types.getSchemaShape(xValues['x-response'], document, { templateDir: 'parameter-serialization', property: 'result', required: true, primitive: true, skipTitleOnce: true, namespace: !config.copySchemasIntoModules }) template = template.replace(/\$\{provider\.xresponse\.serialization\}/gms, xResponseInst) .replace(/\$\{provider\.xresponse\.name\}/gms, xValues['x-response'].title) } if (xValues['x-error']) { - const xErrorInst = types.getSchemaShape(xValues['x-error'], moduleJson, { templateDir: 'parameter-serialization', property: 'result', required: true, destination: state.destination, section: state.section, primitive: true, skipTitleOnce: true }) + const xErrorInst = Types.getSchemaShape(xValues['x-error'], document, { templateDir: 'parameter-serialization', property: 'result', required: true, primitive: true, skipTitleOnce: true, namespace: !config.copySchemasIntoModules }) template = template.replace(/\$\{provider\.xerror\.serialization\}/gms, xErrorInst) .replace(/\$\{provider\.xerror\.name\}/gms, xValues['x-error'].title) } return template } -function insertProviderSubscribeMacros(template, capability, moduleJson = {}, templates) { - const iface = getProviderInterface(capability, moduleJson, config.extractProviderSchema) - let name = getProviderInterfaceName(iface, capability, moduleJson) +function insertProviderSubscribeMacros(template, capability, server = {}, client, templates, bidirectional) { + const iface = getProviderInterface(capability, server, bidirectional) - const suffix = state.destination ? state.destination.split('.').pop() : '' template = template.replace(/\$\{subscribe\}/gms, iface.map(method => { - return insertMethodMacros(getTemplate(suffix ? `/codeblocks/subscribe.${suffix}` : '/codeblocks/subscribe', templates), method, moduleJson, templates) + return insertMethodMacros(getTemplate('/codeblocks/subscribe', templates), method, server, client, templates) }).join('\n') + '\n') return template } -function insertProviderInterfaceMacros(template, capability, moduleJson = {}, templates) { - const iface = getProviderInterface(capability, moduleJson, config.extractProviderSchema) - let name = getProviderInterfaceName(iface, capability, moduleJson) +// TODO: ideally this method should be configurable with tag-names/template-names +function insertProviderInterfaceMacros(template, _interface, server = {}, client = null, codeblock='interface', directory='interfaces', templates, bidirectional) { + const document = client || server + const iface = getProviderInterface(_interface, document, bidirectional) + const capability = extension(iface[0], 'x-provides') let xValues - const suffix = state.destination ? state.destination.split('.').pop() : '' - let interfaceShape = getTemplate(suffix ? `/codeblocks/interface.${suffix}` : '/codeblocks/interface', templates) - if (!interfaceShape) { - interfaceShape = getTemplate('/codeblocks/interface', templates) - } + let interfaceShape = getTemplate(`/codeblocks/${codeblock}`, templates) - interfaceShape = interfaceShape.replace(/\$\{name\}/g, name) + if (!client) { + _interface = getUnidirectionalProviderInterfaceName(_interface, capability, server) + } + interfaceShape = interfaceShape.replace(/\$\{name\}/g, _interface) .replace(/\$\{capability\}/g, capability) .replace(/[ \t]*\$\{methods\}[ \t]*\n/g, iface.map(method => { const focusable = method.tags.find(t => t['x-allow-focus']) - let interfaceDeclaration; - const interfaceTemplate = '/interfaces/' + (focusable ? 'focusable' : 'default') - if (suffix) { - interfaceDeclaration = getTemplate(`${interfaceTemplate}.${suffix}`, templates) - } - if (!interfaceDeclaration) { - interfaceDeclaration = getTemplate(interfaceTemplate, templates) - } + const interfaceTemplate = `/${directory}/` + (focusable ? 'focusable' : 'default') + const interfaceDeclaration = getTemplate(interfaceTemplate, templates) xValues = getProviderXValues(method) method.tags.unshift({ name: 'provider' }) - let type = config.templateExtensionMap && config.templateExtensionMap['methods'] && config.templateExtensionMap['methods'].includes(suffix) ? 'methods' : 'declarations' - return insertMethodMacros(interfaceDeclaration, method, moduleJson, templates, type) +// let type = config.templateExtensionMap && config.templateExtensionMap['methods'] && config.templateExtensionMap['methods'].includes(suffix) ? 'methods' : 'declarations' + return insertMethodMacros(interfaceDeclaration, method, server, client, templates) }).join('') + '\n') if (iface.length === 0) { @@ -1843,30 +1940,32 @@ function insertProviderInterfaceMacros(template, capability, moduleJson = {}, te // insert the standard method templates for each provider if (match) { iface.forEach(method => { - // add a tag to pick the correct template - method.tags.unshift({ - name: 'provider' - }) - const parametersSchema = method.params[0].schema - const parametersShape = types.getSchemaShape(parametersSchema, moduleJson, { destination: state.destination, templateDir: state.typeTemplateDir, section: state.section }) - let methodBlock = insertMethodMacros(getTemplateForMethod(method, templates), method, moduleJson, templates) - methodBlock = methodBlock.replace(/\${parameters\.shape\}/g, parametersShape) - const hasProviderParameters = parametersSchema && parametersSchema.properties && Object.keys(parametersSchema.properties).length > 0 - if (hasProviderParameters) { - const lines = methodBlock.split('\n') - for (let i = lines.length - 1; i >= 0; i--) { - if (lines[i].match(/\$\{provider\.param\.[a-zA-Z]+\}/)) { - let line = lines[i] - lines.splice(i, 1) - line = insertProviderParameterMacros(line, method.params[0].schema, moduleJson) - lines.splice(i++, 0, line) + let methodBlock = insertMethodMacros(getTemplateForMethod(method, templates), method, server, client, templates) + + // uni-directional providers have all params composed into an object, these macros output them + if (!client) { + const parametersSchema = method.params[0].schema + const parametersShape = Types.getSchemaShape(parametersSchema, document, { templateDir: state.typeTemplateDir, namespace: !config.copySchemasIntoModules }) + methodBlock = methodBlock.replace(/\${parameters\.shape\}/g, parametersShape) + + const hasProviderParameters = parametersSchema && parametersSchema.properties && Object.keys(parametersSchema.properties).length > 0 + if (hasProviderParameters) { + const lines = methodBlock.split('\n') + for (let i = lines.length - 1; i >= 0; i--) { + if (lines[i].match(/\$\{provider\.param\.[a-zA-Z]+\}/)) { + let line = lines[i] + lines.splice(i, 1) + line = insertProviderParameterMacros(line, method.params[0].schema, document) + lines.splice(i++, 0, line) + } } + methodBlock = lines.join('\n') + } + else { + methodBlock = methodBlock.replace(/\$\{if\.provider\.params\}.*?\$\{end\.if\.provider\.params\}/gms, '') } - methodBlock = lines.join('\n') - } - else { - methodBlock = methodBlock.replace(/\$\{if\.provider\.params\}.*?\$\{end\.if\.provider\.params\}/gms, '') } + methodsBlock += methodBlock }) @@ -1888,12 +1987,12 @@ function insertProviderInterfaceMacros(template, capability, moduleJson = {}, te // first check for indented lines, and do the fancy indented replacement .replace(/^([ \t]+)(.*?)\$\{provider\.interface\.example\.result\}/gm, '$1$2' + indent(JSON.stringify(method.examples[0].result.value, null, ' '), '$1')) - .replace(/^([ \t]+)(.*?)\$\{provider\.interface\.example\.parameters\}/gm, '$1$2' + indent(JSON.stringify(method.examples[0].params[0].value, null, ' '), '$1')) + .replace(/^([ \t]+)(.*?)\$\{provider\.interface\.example\.parameters\}/gm, '$1$2' + indent(JSON.stringify(method.examples[0].params[0]?.value || '', null, ' '), '$1')) // okay now just do the basic replacement (a single regex for both was not fun) .replace(/\$\{provider\.interface\.example\.result\}/g, JSON.stringify(method.examples[0].result.value)) - .replace(/\$\{provider\.interface\.example\.parameters\}/g, JSON.stringify(method.examples[0].params[0].value)) + .replace(/\$\{provider\.interface\.example\.parameters\}/g, JSON.stringify(method.examples[0].params[0]?.value || '')) - .replace(/\$\{provider\.interface\.example\.correlationId\}/g, JSON.stringify(method.examples[0].params[1].value.correlationId)) + .replace(/\$\{provider\.interface\.example\.correlationId\}/g, JSON.stringify(method.examples[0].params[1]?.value.correlationId || '')) // a set of up to three RPC "id" values for generating intersting examples with matching ids .replace(/\$\{provider\.interface\.i\}/g, i) @@ -1910,15 +2009,15 @@ function insertProviderInterfaceMacros(template, capability, moduleJson = {}, te // TODO: JSON-RPC examples need to use ${provider.interface} macros, but we're replacing them globally instead of each block // there's examples of this in methods, i think - template = template.replace(/\$\{provider\}/g, name) + template = template.replace(/\$\{provider\}/g, _interface) template = template.replace(/\$\{interface\}/g, interfaceShape) template = template.replace(/\$\{capability\}/g, capability) - template = insertProviderXValues(template, moduleJson, xValues) + template = insertProviderXValues(template, document, xValues) return template } -function insertProviderParameterMacros(data = '', parameters, module = {}, options = {}) { +function insertProviderParameterMacros(data = '', parameters, document = {}, options = {}) { if (!parameters || !parameters.properties) { return '' @@ -1927,8 +2026,8 @@ function insertProviderParameterMacros(data = '', parameters, module = {}, optio let result = '' Object.entries(parameters.properties).forEach(([name, param]) => { - let constraints = getSchemaConstraints(param, module) - let type = types.getSchemaType(param, module, { destination: state.destination, templateDir: state.typeTemplateDir, section: state.section, code: true, link: true, asPath: options.asPath, baseUrl: options.baseUrl }) + let constraints = getSchemaConstraints(param, document) + let type = Types.getSchemaType(param, document, { templateDir: state.typeTemplateDir, code: true, link: true, asPath: options.asPath, baseUrl: options.baseUrl, namespace: !config.copySchemasIntoModules }) if (constraints && type) { constraints = '
' + constraints @@ -1959,6 +2058,5 @@ export default { insertMacros, generateAggregateMacros, insertAggregateMacros, - setTyper, setConfig } diff --git a/src/macrofier/index.mjs b/src/macrofier/index.mjs index 298241f6..b40d7bb2 100644 --- a/src/macrofier/index.mjs +++ b/src/macrofier/index.mjs @@ -21,17 +21,19 @@ import { emptyDir, readDir, readFiles, readFilesPermissions, readJson, writeFiles, writeFilesPermissions, writeText } from '../shared/filesystem.mjs' import { getTemplate, getTemplateForModule } from '../shared/template.mjs' -import { getModule, hasPublicAPIs } from '../shared/modules.mjs' +import { getClientModule, getModule, hasPublicAPIs } from '../shared/modules.mjs' import { logHeader, logSuccess } from '../shared/io.mjs' +import Types from './types.mjs' import path from 'path' import engine from './engine.mjs' -import { getLocalSchemas, replaceRef } from '../shared/json-schema.mjs' +import { findAll, flattenMultipleOfs, getLocalSchemas, replaceRef, replaceUri } from '../shared/json-schema.mjs' /************************************************************************************************/ /******************************************** MAIN **********************************************/ /************************************************************************************************/ const macrofy = async ( - input, + server, + client, template, output, options @@ -45,8 +47,10 @@ const macrofy = async ( templatesPerSchema, persistPermission, createPolymorphicMethods, + enableUnionTypes, createModuleDirectories, copySchemasIntoModules, + mergeOnTitle, extractSubSchemas, unwrapResultObjects, allocatedPrimitiveProxies, @@ -55,7 +59,6 @@ const macrofy = async ( additionalMethodTemplates, templateExtensionMap, excludeDeclarations, - extractProviderSchema, aggregateFiles, operators, primitives, @@ -68,29 +71,33 @@ const macrofy = async ( libraryName, treeshakePattern = null, treeshakeEntry = null, - treeshakeTypes = [] + treeshakeTypes = [], + moduleWhitelist = [] } = options return new Promise( async (resolve, reject) => { - const openrpc = await readJson(input) + const serverRpc = await readJson(server) + const clientRpc = client && await readJson(client) || null - logHeader(`Generating ${headline} for version ${openrpc.info.title} ${openrpc.info.version}`) + // Combine all-ofs to make code-generation simplier + flattenMultipleOfs(serverRpc, 'allOf') + flattenMultipleOfs(clientRpc, 'allOf') - let typer + // Combine union types (anyOf / oneOf) for languages that don't have them + // NOTE: anyOf and oneOf are both treated as ORs, i.e. oneOf is not XOR + if (!enableUnionTypes) { + flattenMultipleOfs(serverRpc, 'anyOf') + flattenMultipleOfs(serverRpc, 'oneOf') - try { -// const typerModule = await import(path.join(sharedTemplates, '..', 'Types.mjs')) -// typer = typerModule.default + flattenMultipleOfs(clientRpc, 'anyOf') + flattenMultipleOfs(clientRpc, 'oneOf') } - catch (_) { -// typer = (await import('../shared/typescript.mjs')).default - } - - typer = (await import('./types.mjs')).default + + logHeader(`Generating ${headline} for version ${serverRpc.info.title} ${serverRpc.info.version}`) - engine.setTyper(typer) engine.setConfig({ copySchemasIntoModules, + mergeOnTitle, createModuleDirectories, extractSubSchemas, unwrapResultObjects, @@ -100,11 +107,10 @@ const macrofy = async ( additionalMethodTemplates, templateExtensionMap, excludeDeclarations, - extractProviderSchema, - operators + operators }) - const moduleList = [...(new Set(openrpc.methods.map(method => method.name.split('.').shift())))] + const moduleList = moduleWhitelist.length ? moduleWhitelist : [...(new Set(serverRpc.methods.map(method => method.name.split('.').shift())))] const sdkTemplateList = template ? await readDir(template, { recursive: true }) : [] const sharedTemplateList = await readDir(sharedTemplates, { recursive: true }) const templates = Object.assign(await readFiles(sharedTemplateList, sharedTemplates), @@ -130,32 +136,53 @@ const macrofy = async ( // check if this is a "real" language or just documentation broiler-plate, e.g. markdown if (Object.keys(templates).find(key => key.startsWith('/types/primitive'))) { - typer.setTemplates && typer.setTemplates(templates) - typer.setPrimitives(primitives) + Types.setTemplates && Types.setTemplates(templates) + Types.setPrimitives(primitives) } else { const lang = Object.entries(exampleTemplates)[0][1] const prims = Object.entries(exampleTemplates)[0][1]['__config'].primitives // add the templates from the first example language and the wrapper langauage - typer.setTemplates && typer.setTemplates(lang) - typer.setTemplates && typer.setTemplates(templates) - typer.setPrimitives(prims) + Types.setTemplates && Types.setTemplates(lang) + Types.setTemplates && Types.setTemplates(templates) + Types.setPrimitives(prims) } - typer.setAllocatedPrimitiveProxies(allocatedPrimitiveProxies) - typer.setConvertTuples(convertTuplesToArraysOrObjects) + Types.setAllocatedPrimitiveProxies(allocatedPrimitiveProxies) + Types.setConvertTuples(convertTuplesToArraysOrObjects) const staticCodeList = staticContent ? await readDir(staticContent, { recursive: true }) : [] const staticModules = staticModuleNames.map(name => ( { info: { title: name } } )) let modules + const time = Date.now() if (hidePrivate) { - modules = moduleList.map(name => getModule(name, openrpc, copySchemasIntoModules, extractSubSchemas)).filter(hasPublicAPIs) + modules = moduleList.map(name => getModule(name, serverRpc, copySchemasIntoModules, extractSubSchemas)).filter(hasPublicAPIs) } else { - modules = moduleList.map(name => getModule(name, openrpc, copySchemasIntoModules, extractSubSchemas)) + modules = moduleList.map(name => getModule(name, serverRpc, copySchemasIntoModules, extractSubSchemas)) } + logSuccess(`Separated modules (${Date.now() - time}ms)`) + + + // Grab all schema groups w/ a URI string. These came from some external json-schema that was bundled into the OpenRPC + const externalSchemas = {} + serverRpc.components && serverRpc.components.schemas + && Object.entries(serverRpc.components.schemas).filter(([_, schema]) => schema.$id).forEach(([name, schema]) => { + const id = schema.$id + externalSchemas[id] = JSON.parse(JSON.stringify(schema)) + replaceUri(id, '', externalSchemas[id]) + Object.values(serverRpc.components.schemas).forEach(schema => { + if (schema.$id && schema.$id !== id) { + externalSchemas[id].definitions[schema.$id] = schema + } + }) + }) - const aggregateMacros = engine.generateAggregateMacros(openrpc, modules.concat(staticModules), templates, libraryName) + const aggregatedExternalSchemas = mergeOnTitle ? Object.values(externalSchemas).filter(s => !modules.find(m => m.info.title === s.title)) : Object.values(externalSchemas) + + let start = Date.now() + const aggregateMacros = engine.generateAggregateMacros(serverRpc, clientRpc, modules.concat(staticModules).concat(copySchemasIntoModules ? [] : Object.values(aggregatedExternalSchemas)), templates, libraryName) + logSuccess(`Generated aggregate macros (${Date.now() - start}ms)`) const outputFiles = Object.fromEntries(Object.entries(await readFiles( staticCodeList, staticContent)) .map( ([n, v]) => [path.join(output, n), v])) @@ -163,6 +190,7 @@ const macrofy = async ( let primaryOutput = [] Object.keys(templates).forEach(file => { + start = Date.now() if (file.startsWith(path.sep + outputDirectory + path.sep) || outputDirectory === '') { // Note: '/foo/bar/file.js'.split('/') => ['', 'foo', 'bar', 'file.js'] so we need to drop one more that you might suspect, hence slice(2) below... const dirsToDrop = outputDirectory === '' ? 1 : 2 @@ -181,7 +209,7 @@ const macrofy = async ( if (persistPermission) { templatesPermission[path.join(output, outputFile)] = templatesPermission[file] } - logSuccess(`Generated macros for file ${path.relative(output, path.join(output, outputFile))}`) + logSuccess(`Inserted aggregate macros for file ${path.relative(output, path.join(output, outputFile))} (${Date.now() - start}ms)`) } if (persistPermission) { delete templatesPermission[file] @@ -191,10 +219,16 @@ const macrofy = async ( let append = false modules.forEach(module => { + start = Date.now() + const clientRpc2 = clientRpc && getClientModule(module.info.title, clientRpc, module) + logSuccess(` - gotClientModule ${Date.now() - start}ms`) + start = Date.now() + const macros = engine.generateMacros(module, clientRpc2, templates, exampleTemplates, {hideExcluded: hideExcluded, copySchemasIntoModules: copySchemasIntoModules, createPolymorphicMethods: createPolymorphicMethods, type: 'methods'}) + logSuccess(`Generated macros for module ${module.info.title} (${Date.now() - start}ms)`) // Pick the index and defaults templates for each module. templatesPerModule.forEach(t => { - const macros = engine.generateMacros(module, templates, exampleTemplates, {hideExcluded: hideExcluded, copySchemasIntoModules: copySchemasIntoModules, createPolymorphicMethods: createPolymorphicMethods, destination: t, type: 'methods'}) + start = Date.now() let content = getTemplateForModule(module.info.title, t, templates) // NOTE: whichever insert is called first also needs to be called again last, so each phase can insert recursive macros from the other @@ -205,13 +239,14 @@ const macrofy = async ( const location = createModuleDirectories ? path.join(output, module.info.title, t) : path.join(output, t.replace(/module/, module.info.title.toLowerCase()).replace(/index/, module.info.title)) outputFiles[location] = content - logSuccess(`Generated macros for module ${path.relative(output, location)}`) + logSuccess(` - Inserted ${module.info.title} macros for template ${path.relative(output, location)} (${Date.now() - start}ms)`) }) primaryOutput.forEach(output => { - const macros = engine.generateMacros(module, templates, exampleTemplates, {hideExcluded: hideExcluded, copySchemasIntoModules: copySchemasIntoModules, createPolymorphicMethods: createPolymorphicMethods, destination: output}) + start = Date.now() macros.append = append outputFiles[output] = engine.insertMacros(outputFiles[output], macros) + logSuccess(` - Inserted ${module.info.title} macros for template ${output} (${Date.now() - start}ms)`) }) append = true @@ -251,60 +286,32 @@ const macrofy = async ( } }) } - - // Grab all schema groups w/ a URI string. These came from some external json-schema that was bundled into the OpenRPC - const externalSchemas = {} - openrpc['x-schemas'] - && Object.entries(openrpc['x-schemas']).forEach(([name, schema]) => { - if (schema.uri) { - const id = schema.uri - externalSchemas[id] = externalSchemas[id] || { $id: id, info: {title: name }, methods: []} - externalSchemas[id].components = externalSchemas[id].components || {} - externalSchemas[id].components.schemas = externalSchemas[id].components.schemas || {} - externalSchemas[id]['x-schemas'] = JSON.parse(JSON.stringify(openrpc['x-schemas'])) - - const schemas = JSON.parse(JSON.stringify(schema)) - delete schemas.uri - Object.assign(externalSchemas[id].components.schemas, schemas) - } - }) - - // update the refs - Object.values(externalSchemas).forEach( document => { - getLocalSchemas(document).forEach((path) => { - const parts = path.split('/') - // Drop the grouping path element, since we've pulled this schema out into it's own document - if (parts.length === 4 && path.startsWith('#/x-schemas/' + document.info.title + '/')) { - replaceRef(path, ['#/components/schemas', parts[3]].join('/'), document) - } - // Add the fully qualified URI for any schema groups other than this one - else if (parts.length === 4 && path.startsWith('#/x-schemas/')) { - const uri = openrpc['x-schemas'][parts[2]].uri - // store the case-senstive group title for later use - document.info['x-uri-titles'] = document.info['x-uri-titles'] || {} - document.info['x-uri-titles'][uri] = document.info.title - openrpc.info['x-uri-titles'] = openrpc.info['x-uri-titles'] || {} - openrpc.info['x-uri-titles'][uri] = document.info.title - replaceRef(path, '#/x-schemas/' + parts[2] + '/' + parts[3], document) - } - }) - }) // Output any schema templates for each bundled external schema document - Object.values(externalSchemas).forEach( document => { - if (templatesPerSchema) { - templatesPerSchema.forEach( t => { - const macros = engine.generateMacros(document, templates, exampleTemplates, {hideExcluded: hideExcluded, createPolymorphicMethods: createPolymorphicMethods, destination: t}) + !copySchemasIntoModules && Object.values(externalSchemas).forEach( document => { + if (mergeOnTitle && modules.find(m => m.info.title === document.title)) { + return // skip this one, it was already merged into the module w/ the same name + } + + const macros = engine.generateMacros(document, null, templates, exampleTemplates, {hideExcluded: hideExcluded, copySchemasIntoModules: copySchemasIntoModules, createPolymorphicMethods: createPolymorphicMethods }) + + if (templatesPerSchema || primaryOutput.length) { + templatesPerSchema && templatesPerSchema.forEach( t => { let content = getTemplate('/schemas', t, templates) - - // NOTE: whichever insert is called first also needs to be called again last, so each phase can insert recursive macros from the other content = engine.insertMacros(content, macros) - const location = createModuleDirectories ? path.join(output, document.info.title, t) : path.join(output, t.replace(/module/, document.info.title.toLowerCase()).replace(/index/, document.info.title)) + const location = createModuleDirectories ? path.join(output, document.title, t) : path.join(output, t.replace(/module/, document.title.toLowerCase()).replace(/index/, document.title)) outputFiles[location] = content logSuccess(`Generated macros for schema ${path.relative(output, location)}`) }) + + primaryOutput && primaryOutput.forEach(output => { + macros.append = append + outputFiles[output] = engine.insertMacros(outputFiles[output], macros) + }) + + append = true } }) @@ -315,7 +322,7 @@ const macrofy = async ( await writeFiles(outputFiles) if (persistPermission) { - await writeFilesPermissions(templatesPermission) +// await writeFilesPermissions(templatesPermission) } logSuccess(`Wrote ${Object.keys(outputFiles).length} files.`) diff --git a/src/macrofier/types.mjs b/src/macrofier/types.mjs index 6e854d15..2901cb5b 100644 --- a/src/macrofier/types.mjs +++ b/src/macrofier/types.mjs @@ -17,12 +17,11 @@ */ import deepmerge from 'deepmerge' -import { getPath, localizeDependencies, getSafeEnumKeyName } from '../shared/json-schema.mjs' +import { getReferencedSchema, localizeDependencies, getSafeEnumKeyName, schemaReferencesItself } from '../shared/json-schema.mjs' import path from "path" let convertTuplesToArraysOrObjects = false const templates = {} -const state = {} let primitives = { "integer": "number", "number": "number", @@ -70,20 +69,20 @@ const indent = (str, padding) => { } // TODO: This is what's left of getMethodSignatureParams. We need to figure out / handle C's `FireboltTypes_StringHandle` -function getMethodSignatureParams(method, module, { destination, callback }) { +function getMethodSignatureParams(method, module, { callback, namespace }) { const paramOptional = getTemplate('/parameters/optional') let polymorphicPull = method.tags.find(t => t.name === 'polymorphic-pull') return method.params.map(param => { if (polymorphicPull && (param.name === 'correlationId')) { return } - let type = getSchemaType(param.schema, module, { destination, namespace : true }) + let type = getSchemaType(param.schema, module, { namespace }) if (callback && allocatedPrimitiveProxies[type]) { type = allocatedPrimitiveProxies[type] } let paramRequired = '' - let jsonType = getJsonType(param.schema, module, { destination }) + let jsonType = getJsonType(param.schema, module, { }) if (!isPrimitiveType(jsonType) && getTemplate('/parameters/nonprimitive')) { paramRequired = getTemplate('/parameters/nonprimitive') } @@ -103,12 +102,12 @@ function getMethodSignatureParams(method, module, { destination, callback }) { }).filter(param => param).join(', ') } -function getMethodSignatureResult(method, module, { destination, callback }) { - let type = getSchemaType(method.result.schema, module, { destination, namespace : true }) +function getMethodSignatureResult(method, module, { callback, namespace }) { + let type = getSchemaType(method.result.schema, module, { namespace }) let result = '' if (callback) { - let jsonType = getJsonType(method.result.schema, module, { destination }) + let jsonType = getJsonType(method.result.schema, module, { }) if (!isVoid(type) && !isPrimitiveType(jsonType) && getTemplate('/result-callback/nonprimitive')) { result = getTemplate('/result-callback/nonprimitive') @@ -149,22 +148,25 @@ const getXSchemaGroupFromProperties = (schema, title, properties, group) => { // TODO: this assumes the same title doesn't exist in multiple x-schema groups! const getXSchemaGroup = (schema, module) => { - let group = module.info.title - - if (schema.title && module['x-schemas']) { - Object.entries(module['x-schemas']).forEach(([title, module]) => { - Object.values(module).forEach(moduleSchema => { + let group = module.info ? module.info.title : module.title + let bundles = module.definitions || module.components.schemas + + if (schema.title && bundles) { + Object.entries(bundles).filter(([key, s]) => s.$id).forEach(([id, bundle]) => { + const title = bundle.title + Object.values(bundle.definitions).forEach(moduleSchema => { let schemas = moduleSchema.allOf ? moduleSchema.allOf : [moduleSchema] schemas.forEach((s) => { if (schema.title === s.title || schema.title === moduleSchema.title) { group = title } else { group = getXSchemaGroupFromProperties(schema, title, s.properties, group) - } + } }) }) }) } + return group } @@ -173,8 +175,8 @@ function getSchemaDescription(schema, module) { if (schema.type === 'array' && schema.items) { schema = schema.items } - if (schema['$ref'] && (schema['$ref'][0] === '#')) { - const refSchema = getPath(schema['$ref'], module) + if (schema['$ref']) { + const refSchema = getReferencedSchema(schema['$ref'], module) description = (refSchema && refSchema.description) || description } return description @@ -182,7 +184,8 @@ function getSchemaDescription(schema, module) { function insertSchemaMacros(content, schema, module, { name = '', parent = '', property = '', required = false, recursive = true, templateDir = 'types'}) { const title = name || schema.title || '' - const moduleTitle = getXSchemaGroup(schema, module) + const parentTitle = getXSchemaGroup(schema, module) + const moduleTitle = module.info ? module.info.title : module.title const description = getSchemaDescription(schema, module) content = content @@ -191,9 +194,9 @@ function insertSchemaMacros(content, schema, module, { name = '', parent = '', p .replace(/\$\{TITLE\}/g, title.toUpperCase()) .replace(/\$\{property\}/g, property) .replace(/\$\{Property\}/g, capitalize(property)) - .replace(/\$\{if\.namespace\.notsame}(.*?)\$\{end\.if\.namespace\.notsame\}/g, (module.info.title !== (parent || moduleTitle)) ? '$1' : '') - .replace(/\$\{parent\.title\}/g, parent || moduleTitle) - .replace(/\$\{parent\.Title\}/g, capitalize(parent || moduleTitle)) + .replace(/\$\{if\.namespace\.notsame}(.*?)\$\{end\.if\.namespace\.notsame\}/g, (moduleTitle !== (parent || parentTitle)) ? '$1' : '') + .replace(/\$\{parent\.title\}/g, parent || parentTitle) + .replace(/\$\{parent\.Title\}/g, capitalize(parent || parentTitle)) .replace(/\$\{description\}/g, description) .replace(/\$\{if\.optional\}(.*?)\$\{end\.if\.optional\}/gms, (Array.isArray(required) ? required.includes(property) : required) ? '' : '$1') .replace(/\$\{if\.non.optional\}(.*?)\$\{end\.if\.non.optional\}/gms, (Array.isArray(required) ? required.includes(property) : required) ? '$1' : '') @@ -201,12 +204,12 @@ function insertSchemaMacros(content, schema, module, { name = '', parent = '', p .replace(/\$\{summary\}/g, description ? description.split('\n')[0] : '') .replace(/\$\{name\}/g, title) .replace(/\$\{NAME\}/g, title.toUpperCase()) - .replace(/\$\{info.title\}/g, moduleTitle) - .replace(/\$\{info.Title\}/g, capitalize(moduleTitle)) - .replace(/\$\{info.TITLE\}/g, moduleTitle.toUpperCase()) + .replace(/\$\{info.title\}/g, parentTitle) + .replace(/\$\{info.Title\}/g, capitalize(parentTitle)) + .replace(/\$\{info.TITLE\}/g, parentTitle.toUpperCase()) if (recursive) { - content = content.replace(/\$\{type\}/g, getSchemaType(schema, module, { templateDir: templateDir, destination: state.destination, section: state.section, code: false, namespace: true })) + content = content.replace(/\$\{type\}/g, getSchemaType(schema, module, { templateDir: templateDir, code: false, namespace: true })) } return content } @@ -217,7 +220,7 @@ const insertConstMacros = (content, schema, module, name) => { return content } -const insertEnumMacros = (content, schema, module, name, suffix, templateDir = "types") => { +const insertEnumMacros = (content, schema, module, name, templateDir = "types") => { const template = content.split('\n') for (var i = 0; i < template.length; i++) { @@ -225,7 +228,7 @@ const insertEnumMacros = (content, schema, module, name, suffix, templateDir = " let values = [] schema.enum.map(value => { if (!value) { - value = getTemplate(path.join(templateDir, 'unset' + suffix)) + value = getTemplate(path.join(templateDir, 'unset')) } value ? values.push(template[i].replace(/\$\{key\}/g, getSafeEnumKeyName(value)) .replace(/\$\{value\}/g, value)) : '' @@ -253,6 +256,8 @@ const insertObjectAdditionalPropertiesMacros = (content, schema, module, title, jsonType = 'string' } + const moduleTitle = module.info ? module.info.title : module.title + const additionalType = getPrimitiveType(jsonType, 'additional-types') let namespace = '' @@ -262,8 +267,8 @@ const insertObjectAdditionalPropertiesMacros = (content, schema, module, title, let parent = getXSchemaGroup(propertyNames, module) key = propertyNames.title namespace = getTemplate(path.join(options.templateDir, 'namespace')) - .replace(/\$\{if\.namespace\.notsame}(.*?)\$\{end\.if\.namespace\.notsame\}/g, (module.info.title !== (parent || moduleTitle)) ? '$1' : '') - .replace(/\$\{parent\.Title\}/g, (parent && module.info.title !== parent) ? parent : '') + .replace(/\$\{if\.namespace\.notsame}(.*?)\$\{end\.if\.namespace\.notsame\}/g, (moduleTitle !== (parent || moduleTitle)) ? '$1' : '') + .replace(/\$\{parent\.Title\}/g, (parent && moduleTitle !== parent) ? parent : '') defaultKey = false } content = content @@ -298,7 +303,6 @@ const insertObjectPatternPropertiesMacros = (content, schema, module, title, opt if (patternSchema) { const shape = getSchemaShape(patternSchema, module, options2) let type = getSchemaType(patternSchema, module, options2).trimEnd() - const propertyNames = localizeDependencies(schema, module).propertyNames content = content .replace(/\$\{shape\}/g, shape) @@ -318,7 +322,7 @@ const getIndents = level => level ? ' ' : '' const insertObjectMacros = (content, schema, module, title, property, options) => { const options2 = options ? JSON.parse(JSON.stringify(options)) : {} options2.parent = title - options2.parentLevel = options.parentLevel + options2.parentLevel = options.level options2.level = options.level + 1 options2.templateDir = options.templateDir ;(['properties', 'properties.register', 'properties.assign']).forEach(macro => { @@ -332,15 +336,10 @@ const insertObjectMacros = (content, schema, module, title, property, options) = const subProperty = getTemplate(path.join(options2.templateDir, 'sub-property/object')) options2.templateDir += subProperty ? '/sub-property' : '' const objSeparator = getTemplate(path.join(options2.templateDir, 'object-separator')) - if (localizedProp.type === 'array' || localizedProp.anyOf || localizedProp.oneOf || (typeof localizedProp.const === 'string')) { - options2.property = name - options2.required = schema.required - } else { - options2.property = options.property - options2.required = schema.required && schema.required.includes(name) - } - const schemaShape = indent + getSchemaShape(localizedProp, module, options2).replace(/\n/gms, '\n' + indent) - const type = getSchemaType(localizedProp, module, options2) + options2.property = name + options2.required = schema.required && schema.required.includes(name) //schema.required + const schemaShape = indent + getSchemaShape(prop, module, options2).replace(/\n/gms, '\n' + indent) + const type = getSchemaType(prop, module, options2) // don't push properties w/ unsupported types if (type) { const description = getSchemaDescription(prop, module) @@ -355,12 +354,12 @@ const insertObjectMacros = (content, schema, module, title, property, options) = .replace(/\$\{if\.summary\}(.*?)\$\{end\.if\.summary\}/gms, description ? '$1' : '') .replace(/\$\{summary\}/g, description ? description.split('\n')[0] : '') .replace(/\$\{delimiter\}(.*?)\$\{end.delimiter\}/gms, i === schema.properties.length - 1 ? '' : '$1') - .replace(/\$\{if\.optional\}(.*?)\$\{end\.if\.optional\}/gms, ((schema.required && schema.required.includes(name)) || (localizedProp.required && localizedProp.required === true)) ? '' : '$1') - .replace(/\$\{if\.non.optional\}(.*?)\$\{end\.if\.non.optional\}/gms, ((schema.required && schema.required.includes(name)) || (localizedProp.required && localizedProp.required === true)) ? '$1' : '') + .replace(/\$\{if\.optional\}(.*?)\$\{end\.if\.optional\}/gms, ((schema.required && schema.required.includes(name))) ? '' : '$1') + .replace(/\$\{if\.non.optional\}(.*?)\$\{end\.if\.non.optional\}/gms, ((schema.required && schema.required.includes(name))) ? '$1' : '') .replace(/\$\{if\.base\.optional\}(.*?)\$\{end\.if\.base\.optional\}/gms, options.required ? '' : '$1') .replace(/\$\{if\.non\.object\}(.*?)\$\{end\.if\.non\.object\}/gms, isObject(localizedProp) ? '' : '$1') .replace(/\$\{if\.non\.array\}(.*?)\$\{end\.if\.non\.array\}/gms, (localizedProp.type === 'array') ? '' : '$1') - .replace(/\$\{if\.non\.anyOf\}(.*?)\$\{end\.if\.non\.anyOf\}/gms, (localizedProp.anyOf || localizedProp.anyOneOf) ? '' : '$1') + .replace(/\$\{if\.non\.anyOf\}(.*?)\$\{end\.if\.non\.anyOf\}/gms, (localizedProp.anyOf || localizedProp.oneOf) ? '' : '$1') .replace(/\$\{if\.non\.const\}(.*?)\$\{end\.if\.non\.const\}/gms, (typeof localizedProp.const === 'string') ? '' : '$1') let baseTitle = options.property @@ -449,7 +448,7 @@ const insertObjectMacros = (content, schema, module, title, property, options) = const insertArrayMacros = (content, schema, module, level = 0, items, required = false) => { content = content - .replace(/\$\{json\.type\}/g, getSchemaType(schema.items, module, { templateDir: 'json-types', destination: state.destination, section: state.section, code: false, namespace: true })) + .replace(/\$\{json\.type\}/g, getSchemaType(schema.items, module, { templateDir: 'json-types', code: false, namespace: true })) .replace(/\$\{items\}/g, items) .replace(/\$\{items\.with\.indent\}/g, required ? indent(items, ' ') : indent(items, ' ')) .replace(/\$\{if\.impl.array.optional\}(.*?)\$\{end\.if\.impl.array.optional\}/gms, required ? '' : '$1') @@ -488,7 +487,7 @@ const insertTupleMacros = (content, schema, module, title, options) => { content = content.replace(/\$\{properties\}/g, schema.items.map((prop, i) => doMacroWork(propTemplate, prop, i, propIndent)).join(tupleDelimiter)) content = content.replace(/\$\{items\}/g, schema.items.map((prop, i) => doMacroWork(itemsTemplate, prop, i, itemsIndent)).join(tupleDelimiter)) - content = content.replace(/\$\{json\.type\}/g, getSchemaType(schema.items[0], module, { templateDir: 'json-types', destination: state.destination, section: state.section, code: false, namespace: true })) + content = content.replace(/\$\{json\.type\}/g, getSchemaType(schema.items[0], module, { templateDir: 'json-types', code: false, namespace: true })) return content } @@ -504,11 +503,12 @@ const insertPrimitiveMacros = (content, schema, module, name, templateDir) => { return content } -const insertAnyOfMacros = (content, schema, module, name) => { +// +const insertAnyOfMacros = (content, schema, module, namespace) => { const itemTemplate = content if (content.split('\n').find(line => line.includes("${type}"))) { content = schema.anyOf.map((item, i) => itemTemplate - .replace(/\$\{type\}/g, getSchemaType(item, module)) + .replace(/\$\{type\}/g, getSchemaType(item, module, { namespace })) .replace(/\$\{delimiter\}(.*?)\$\{end.delimiter\}/g, i === schema.anyOf.length - 1 ? '' : '$1') ).join('') } @@ -532,42 +532,44 @@ const sanitize = (schema) => { return result } -function getSchemaShape(schema = {}, module = {}, { templateDir = 'types', parent = '', property = '', required = false, parentLevel = 0, level = 0, summary, descriptions = true, destination, section, enums = true, skipTitleOnce = false, array = false, primitive = false, type = false } = {}) { +function getSchemaShape(schema = {}, module = {}, { templateDir = 'types', parent = '', property = '', required = false, parentLevel = 0, level = 0, summary, descriptions = true, enums = true, enumImpl = false, skipTitleOnce = false, array = false, primitive = false, type = false, namespace=true } = {}) { schema = sanitize(schema) - state.destination = destination - state.section = section if (level === 0 && !schema.title && !primitive) { return '' } - const suffix = destination && ('.' + destination.split('.').pop()) || '' - const theTitle = insertSchemaMacros(getTemplate(path.join(templateDir, 'title' + suffix)), schema, module, { name: schema.title, parent, property, required, recursive: false }) + if (schema.anyOf && schema.anyOf.find(s => s.$ref?.indexOf('/ListenResponse') >= 0)) { + schema = schema.anyOf.find(s => !s.$ref || s.$ref.indexOf('/ListenResponse') === -1) + } + + const theTitle = insertSchemaMacros(getTemplate(path.join(templateDir, 'title')), schema, module, { name: schema.title, parent, property, required, recursive: false }) + const moduleTitle = module.info ? module.info.title : module.title - let result = getTemplate(path.join(templateDir, 'default' + suffix)) || '${shape}' + let result = getTemplate(path.join(templateDir, 'default')) || '${shape}' - let genericTemplate = getTemplate(path.join(templateDir, 'generic' + suffix)) + let genericTemplate = getTemplate(path.join(templateDir, 'generic')) if (enums && level === 0 && Array.isArray(schema.enum) && ((schema.type === "string") || (schema.type[0] === "string"))) { - result = getTemplate(path.join(templateDir, 'enum' + suffix)) || genericTemplate - return insertSchemaMacros(insertEnumMacros(result, schema, module, theTitle, suffix, templateDir), schema, module, { name: theTitle, parent, property, required }) + result = getTemplate(path.join(templateDir, enumImpl ? 'enum-implementation' : 'enum')) || genericTemplate + return insertSchemaMacros(insertEnumMacros(result, schema, module, theTitle, templateDir), schema, module, { name: theTitle, parent, property, required }) } if (schema['$ref']) { - const someJson = getPath(schema['$ref'], module) + const someJson = getReferencedSchema(schema['$ref'], module) if (someJson) { - return getSchemaShape(someJson, module, { templateDir, parent, property, required, parentLevel, level, summary, descriptions, destination, enums, array, primitive }) + return getSchemaShape(someJson, module, { templateDir, parent, property, required, parentLevel, level, summary, descriptions, enums, array, primitive, namespace }) } - throw "Unresolvable $ref: " + schema['ref'] + ", in " + module.info.title + throw "Unresolvable $ref: " + schema['$ref'] + ", in " + moduleTitle } else if (schema.hasOwnProperty('const')) { - const shape = insertConstMacros(getTemplate(path.join(templateDir, 'const' + suffix)) || genericTemplate, schema, module, theTitle) + const shape = insertConstMacros(getTemplate(path.join(templateDir, 'const')) || genericTemplate, schema, module, theTitle) return insertSchemaMacros(result.replace(/\$\{shape\}/g, shape), schema, module, { name: theTitle, parent, property, required }) } else if (!skipTitleOnce && (level > 0) && schema.title) { let enumType = (schema.type === 'string' && Array.isArray(schema.enum)) // TODO: allow the 'ref' template to actually insert the shape using getSchemaShape - const innerShape = getSchemaShape(schema, module, { skipTitleOnce: true, templateDir, parent, property, required, parentLevel, level, summary, descriptions, destination, enums: enumType, array, primitive }) + const innerShape = getSchemaShape(schema, module, { skipTitleOnce: true, templateDir, parent, property, required, parentLevel, level, summary, descriptions, enums: enumType, array, primitive, namespace }) - const shape = getTemplate(path.join(templateDir, 'ref' + suffix)) + const shape = getTemplate(path.join(templateDir, 'ref')) .replace(/\$\{shape\}/g, innerShape) result = result.replace(/\$\{shape\}/g, shape) @@ -577,15 +579,15 @@ function getSchemaShape(schema = {}, module = {}, { templateDir = 'types', paren let shape const additionalPropertiesTemplate = getTemplate(path.join(templateDir, 'additionalProperties')) if (additionalPropertiesTemplate && schema.additionalProperties && (typeof schema.additionalProperties === 'object')) { - shape = insertObjectAdditionalPropertiesMacros(additionalPropertiesTemplate, schema, module, theTitle, { level, parent, templateDir, namespace: true, required }) + shape = insertObjectAdditionalPropertiesMacros(additionalPropertiesTemplate, schema, module, theTitle, { level, parent, templateDir, namespace, required }) } else { const patternPropertiesTemplate = getTemplate(path.join(templateDir, 'patternProperties')) if (patternPropertiesTemplate && schema.patternProperties) { - shape = insertObjectPatternPropertiesMacros(patternPropertiesTemplate, schema, module, theTitle, { level, parent, templateDir, namespace: true, required }) + shape = insertObjectPatternPropertiesMacros(patternPropertiesTemplate, schema, module, theTitle, { level, parent, templateDir, namespace, required }) } else { let objectLevel = array ? 0 : level - shape = insertObjectMacros(getTemplate(path.join(templateDir, 'object' + (array ? '-array' : '') + suffix)) || genericTemplate, schema, module, theTitle, property, { parentLevel, level: objectLevel, parent, property, required, templateDir, descriptions, destination, section, enums, namespace: true, primitive }) + shape = insertObjectMacros(getTemplate(path.join(templateDir, 'object' + (array ? '-array' : ''))) || genericTemplate, schema, module, theTitle, property, { parentLevel, level: objectLevel, parent, property, required, templateDir, descriptions, enums, namespace, primitive }) } } result = result.replace(/\$\{shape\}/g, shape) @@ -596,25 +598,16 @@ function getSchemaShape(schema = {}, module = {}, { templateDir = 'types', paren return insertSchemaMacros(result, schema, module, { name: theTitle, parent, property, required, templateDir }) } else if (schema.anyOf || schema.oneOf) { - const template = getTemplate(path.join(templateDir, 'anyOfSchemaShape' + suffix)) - let shape - if (template) { - shape = insertAnyOfMacros(template, schema, module, theTitle) - } - else { - // borrow anyOf logic, note that schema is a copy, so we're not breaking it. - if (!schema.anyOf) { - schema.anyOf = schema.oneOf - } - shape = insertAnyOfMacros(getTemplate(path.join(templateDir, 'anyOf' + suffix)) || genericTemplate, schema, module, theTitle) - } - if (shape) { - result = result.replace(/\$\{shape\}/g, shape) - return insertSchemaMacros(result, schema, module, { name: theTitle, parent, property, required }) - } - else { - return '' + // borrow anyOf logic, note that schema is a copy, so we're not breaking it. + if (!schema.anyOf) { + schema.anyOf = schema.oneOf } + + let template = getTemplate(path.join(templateDir, 'anyOf')) || genericTemplate + template = insertAnyOfMacros(template, schema, module, namespace) + + result = result.replace(/\$\{shape\}/g, template) + return insertSchemaMacros(result, schema, module, { name: theTitle, parent, property, required }) } else if (schema.allOf) { const merger = (key) => function (a, b) { @@ -629,7 +622,7 @@ function getSchemaShape(schema = {}, module = {}, { templateDir = 'types', paren } } - let union = deepmerge.all([...schema.allOf.map(x => x['$ref'] ? getPath(x['$ref'], module) || x : x).reverse()], { + let union = deepmerge.all([...schema.allOf.map(x => x['$ref'] ? getReferencedSchema(x['$ref'], module) || x : x).reverse()], { customMerge: merger }) @@ -638,25 +631,25 @@ function getSchemaShape(schema = {}, module = {}, { templateDir = 'types', paren } delete union['$ref'] - return getSchemaShape(union, module, { templateDir, parent, property, required, parentLevel, level, summary, descriptions, destination, enums: false, array, primitive }) + return getSchemaShape(union, module, { templateDir, parent, property, required, parentLevel, level, summary, descriptions, enums: false, array, primitive, namespace }) } else if (schema.type === "array" && schema.items && isSupportedTuple(schema)) { // tuple - const shape = insertTupleMacros(getTemplate(path.join(templateDir, 'tuple' + suffix)) || genericTemplate, schema, module, theTitle, { level, templateDir, descriptions, destination, section, enums }) + const shape = insertTupleMacros(getTemplate(path.join(templateDir, 'tuple')) || genericTemplate, schema, module, theTitle, { level, templateDir, descriptions, enums }) result = result.replace(/\$\{shape\}/g, shape) return insertSchemaMacros(result, schema, module, { name: theTitle, parent, property, required, templateDir }) } else if (schema.type === "array" && schema.items && !Array.isArray(schema.items)) { // array - const items = getSchemaShape(schema.items, module, { templateDir, parent, property, required, parentLevel: parentLevel + 1, level, summary, descriptions, destination, enums: false, array: true, primitive }) - const shape = insertArrayMacros(getTemplate(path.join(templateDir, 'array' + suffix)) || genericTemplate, schema, module, level, items, Array.isArray(required) ? required.includes(property) : required) + const items = getSchemaShape(schema.items, module, { templateDir, parent, property, required, parentLevel: parentLevel + 1, level, summary, descriptions, enums: false, array: true, primitive, namespace }) + const shape = insertArrayMacros(getTemplate(path.join(templateDir, 'array')) || genericTemplate, schema, module, level, items, Array.isArray(required) ? required.includes(property) : required) result = result.replace(/\$\{shape\}/g, shape) .replace(/\$\{if\.object\}(.*?)\$\{end\.if\.object\}/gms, isObject(schema.items) ? '$1' : '') .replace(/\$\{if\.non\.object\}(.*?)\$\{end\.if\.non\.object\}/gms, (schema.items.type !== 'object') ? '$1' : '') return insertSchemaMacros(result, schema, module, { name: items, parent, property, required, templateDir }) } else if (schema.type) { - const shape = insertPrimitiveMacros(getTemplate(path.join(templateDir, 'primitive' + suffix)), schema, module, theTitle, templateDir) + const shape = insertPrimitiveMacros(getTemplate(path.join(templateDir, 'primitive')), schema, module, theTitle, templateDir) result = result.replace(/\$\{shape\}/g, shape) if (level > 0 || primitive) { return insertSchemaMacros(result, schema, module, { name: theTitle, parent, property, required, templateDir }) @@ -714,27 +707,25 @@ const isSupportedTuple = schema => { } } -function getSchemaType(schema, module, { destination, templateDir = 'types', link = false, code = false, asPath = false, event = false, result = false, expandEnums = true, baseUrl = '', namespace = false } = {}) { +function getSchemaType(schema, module, { templateDir = 'types', link = false, code = false, asPath = false, event = false, result = false, expandEnums = true, baseUrl = '', namespace = true } = {}) { const wrap = (str, wrapper) => wrapper + str + wrapper schema = sanitize(schema) - const suffix = destination && ('.' + destination.split('.').pop()) || '' - const namespaceStr = namespace ? getTemplate(path.join(templateDir, 'namespace' + suffix)) : '' - const theTitle = insertSchemaMacros(namespaceStr + getTemplate(path.join(templateDir, 'title' + suffix)), schema, module, { name: schema.title, parent: getXSchemaGroup(schema, module), recursive: false }) + const moduleTitle = module.info ? module.info.title : module.title + const namespaceStr = namespace ? getTemplate(path.join(templateDir, 'namespace')) : '' + const theTitle = insertSchemaMacros(namespaceStr + getTemplate(path.join(templateDir, 'title')), schema, module, { name: schema.title, parent: getXSchemaGroup(schema, module), recursive: false }) const allocatedProxy = event || result const title = schema.type === "object" || schema.anyOf || schema.oneOf || Array.isArray(schema.type) && schema.type.includes("object") || schema.enum ? true : false if (schema['$ref']) { - if (schema['$ref'][0] === '#') { - const refSchema = getPath(schema['$ref'], module) - const includeNamespace = (module.info.title !== getXSchemaGroup(refSchema, module)) - return getSchemaType(refSchema, module, {destination, templateDir, link, code, asPath, event, result, expandEnums, baseUrl, namespace:includeNamespace })// { link: link, code: code, destination }) + const refSchema = getReferencedSchema(schema['$ref'], module) + if (refSchema) { + return getSchemaType(refSchema, module, { templateDir, link, code, asPath, event, result, expandEnums, baseUrl, namespace })// { link: link, code: code }) } else { // TODO: This never happens... but might be worth keeping in case we link to an opaque external schema at some point? - if (link) { return '[' + wrap(theTitle, code ? '`' : '') + '](' + schema['$ref'] + ')' } @@ -752,10 +743,10 @@ function getSchemaType(schema, module, { destination, templateDir = 'types', lin } } else if (schema.const) { - return insertConstMacros(getTemplate(path.join(templateDir, 'const' + suffix)), schema, module) + return insertConstMacros(getTemplate(path.join(templateDir, 'const')), schema, module) } else if (schema['x-method']) { - const target = JSON.parse(JSON.stringify(module.methods.find(m => m.name === schema['x-method'].split('.').pop()))) + const target = JSON.parse(JSON.stringify(module.methods.find(m => m.name === schema['x-method']))) // transform the method copy params to be in the order of the x-additional-params array (and leave out any we don't want) if (schema['x-additional-params']) { @@ -769,7 +760,7 @@ function getSchemaType(schema, module, { destination, templateDir = 'types', lin target.params = [] } - const params = getMethodSignatureParams(target, module, { destination }) + const params = getMethodSignatureParams(target, module, { }) const template = getTemplate(path.join(templateDir, 'x-method')) return insertSchemaMacros(template.replace(/\$\{params\}/g, params), target.result.schema, module, { name: theTitle, recursive: false }) } @@ -786,7 +777,7 @@ function getSchemaType(schema, module, { destination, templateDir = 'types', lin else if ((schema.type === 'object' || (schema.type === 'array')) && schema.title) { const maybeGetPath = (path, json) => { try { - return getPath(path, json) + return getReferencedSchema(path, json) } catch (e) { return null @@ -806,25 +797,25 @@ function getSchemaType(schema, module, { destination, templateDir = 'types', lin let firstItem if (Array.isArray(schema.items)) { if (!isHomogenous(schema.items)) { - console.log(`Non-homogenous tuples not supported: ${schema.items} in ${module.info.title}, ${theTitle}`) + console.log(`Non-homogenous tuples not supported: ${schema.items} in ${moduleTitle}, ${theTitle}`) return '' } firstItem = schema.items[0] - // let type = '[' + schema.items.map(x => getSchemaType(x, module, { destination })).join(', ') + ']' // no links, no code + // let type = '[' + schema.items.map(x => getSchemaType(x, module, { })).join(', ') + ']' // no links, no code } let template // Tuple -> Array if (convertTuplesToArraysOrObjects && isTuple(schema) && isHomogenous(schema)) { template = insertArrayMacros(getTemplate(path.join(templateDir, 'array')), schema, module) - template = insertSchemaMacros(template, firstItem, module, { name: getSchemaType(firstItem, module, {destination, templateDir, link, title, code, asPath, event, result, expandEnums, baseUrl, namespace }), recursive: false }) + template = insertSchemaMacros(template, firstItem, module, { name: getSchemaType(firstItem, module, { templateDir, link, title, code, asPath, event, result, expandEnums, baseUrl, namespace }), recursive: false }) } // Normal Array else if (!isTuple(schema)) { const baseDir = (templateDir !== 'json-types' ? 'types': templateDir) template = insertArrayMacros(getTemplate(path.join(baseDir, 'array')), schema, module) - template = insertSchemaMacros(template, schema.items, module, { name: getSchemaType(schema.items, module, {destination, templateDir, link, title, code, asPath, event, result, expandEnums, baseUrl, namespace })}) + template = insertSchemaMacros(template, schema.items, module, { name: getSchemaType(schema.items, module, { templateDir, link, title, code, asPath, event, result, expandEnums, baseUrl, namespace })}) } else { template = insertTupleMacros(getTemplate(path.join(templateDir, 'tuple')), schema, module, '', { templateDir }) @@ -838,26 +829,26 @@ function getSchemaType(schema, module, { destination, templateDir = 'types', lin return template } else if (schema.allOf) { - let union = deepmerge.all([...schema.allOf.map(x => x['$ref'] ? getPath(x['$ref'], module) || x : x)]) + let union = deepmerge.all([...schema.allOf.map(x => x['$ref'] ? getReferencedSchema(x['$ref'], module) || x : x)]) if (schema.title) { union.title = schema.title } - return getSchemaType(union, module, { templateDir, destination, link, title, code, asPath, baseUrl, namespace }) + return getSchemaType(union, module, { templateDir, link, title, code, asPath, baseUrl, namespace }) } else if (schema.oneOf || schema.anyOf) { if (!schema.anyOf) { schema.anyOf = schema.oneOf } - // todo... we probably shouldn't allow untitled anyOfs, at least not w/out a feature flag - const shape = insertAnyOfMacros(getTemplate(path.join(templateDir, 'anyOf' + suffix)), schema, module, theTitle) + + const shape = insertAnyOfMacros(getTemplate(path.join(templateDir, 'anyOf')), schema, module, namespace) return insertSchemaMacros(shape, schema, module, { name: theTitle, recursive: false }) // if (event) { - // return getSchemaType((schema.oneOf || schema.anyOf)[0], module, { destination, link, title, code, asPath, baseUrl }) + // return getSchemaType((schema.oneOf || schema.anyOf)[0], module, { link, title, code, asPath, baseUrl }) // } // else { - // const newOptions = JSON.parse(JSON.stringify({ destination, link, title, code, asPath, baseUrl })) + // const newOptions = JSON.parse(JSON.stringify({ link, title, code, asPath, baseUrl })) // newOptions.code = false // const result = (schema.oneOf || schema.anyOf).map(s => getSchemaType(s, module, newOptions)).join(' | ') @@ -879,7 +870,7 @@ function getSchemaType(schema, module, { destination, templateDir = 'types', lin const schemaType = !Array.isArray(schema.type) ? schema.type : schema.type.find(t => t !== 'null') const baseDir = (templateDir !== 'json-types' ? 'types': templateDir) let primitive = getPrimitiveType(schemaType, baseDir, schema.title ? true: false) - primitive = primitive ? primitive.replace(/\$\{title\}/g, schema.title) : primitive + primitive = primitive ? primitive.replace(/\$\{title\}/g, theTitle) : primitive const type = allocatedProxy ? allocatedPrimitiveProxies[schemaType] || primitive : primitive return wrap(type, code ? '`' : '') @@ -898,7 +889,7 @@ function getSchemaType(schema, module, { destination, templateDir = 'types', lin } } -function getJsonType(schema, module, { destination, link = false, title = false, code = false, asPath = false, event = false, expandEnums = true, baseUrl = '' } = {}) { +function getJsonType(schema, module, { link = false, title = false, code = false, asPath = false, event = false, expandEnums = true, baseUrl = '' } = {}) { schema = sanitize(schema) let type @@ -906,8 +897,11 @@ function getJsonType(schema, module, { destination, link = false, title = false, if (schema['$ref'][0] === '#') { //Ref points to local schema //Get Path to ref in this module and getSchemaType - let definition = getPath(schema['$ref'], module) - type = getJsonType(definition, schema, {destination}) + let definition = getReferencedSchema(schema['$ref'], module) + if (!definition) { + throw `Unresolved schema ${schema.$ref} in ${module.title || module.info.title}` + } + type = getJsonType(definition, schema, {}) } } else { diff --git a/src/openrpc-template.json b/src/openrpc-template.json index 249a355b..9ca404e6 100644 --- a/src/openrpc-template.json +++ b/src/openrpc-template.json @@ -8,13 +8,13 @@ { "name": "rpc.discover", "summary": "The OpenRPC schema for this JSON-RPC API", + "params": [], "tags": [ { "name": "capabilities", "x-uses": [ "xrn:firebolt:capability:rpc:discover" ] } ], - "params": [], "result": { "name": "OpenRPC Schema", "schema": { diff --git a/src/openrpc/index.mjs b/src/openrpc/index.mjs index 2931d21c..e3d932df 100644 --- a/src/openrpc/index.mjs +++ b/src/openrpc/index.mjs @@ -17,30 +17,39 @@ */ import { readJson, readFiles, readDir, writeJson } from "../shared/filesystem.mjs" -import { addExternalMarkdown, addExternalSchemas, fireboltize, fireboltizeMerged } from "../shared/modules.mjs" +import { addExternalMarkdown, addExternalSchemas, fireboltize } from "../shared/modules.mjs" import path from "path" import { logHeader, logSuccess } from "../shared/io.mjs" +import { flattenMultipleOfs, namespaceRefs } from "../shared/json-schema.mjs" const run = async ({ input: input, - output: output, + client: client, + server: server, template: template, schemas: schemas, + argv: { + remain: moduleWhitelist + } }) => { - let openrpc = await readJson(template) + let serverOpenRPC = await readJson(template) + let clientOpenRPC = client && await readJson(template) + let mergedOpenRpc = await readJson(template) + const sharedSchemaList = schemas ? (await Promise.all(schemas.map(d => readDir(d, { recursive: true })))).flat() : [] const sharedSchemas = await readFiles(sharedSchemaList) try { const packageJson = await readJson(path.join(input, '..', 'package.json')) - openrpc.info.version = packageJson.version + serverOpenRPC.info.version = packageJson.version + clientOpenRPC && (clientOpenRPC.info.version = packageJson.version) } catch (error) { // fail silently } - logHeader(`Generating compiled ${openrpc.info.title} OpenRPC document version ${openrpc.info.version}`) + logHeader(`Generating compiled ${serverOpenRPC.info.title} OpenRPC document version ${serverOpenRPC.info.version}`) Object.entries(sharedSchemas).forEach(([path, schema]) => { const json = JSON.parse(schema) @@ -55,57 +64,60 @@ const run = async ({ const descriptionsList = input ? await readDir(path.join(input, 'descriptions'), { recursive: true }) : [] const markdown = await readFiles(descriptionsList, path.join(input, 'descriptions')) - Object.keys(modules).forEach(key => { - let json = JSON.parse(modules[key]) - - // Do the firebolt API magic - json = fireboltize(json) + const isNotifier = method => method.tags.find(t => t.name === 'notifier') + const isProvider = method => method.tags.find(t => t.name === 'capabilities')['x-provides'] && !method.tags.find(t => t.name === 'event') && !method.tags.find(t => t.name === 'polymorphic-pull') && !method.tags.find(t => t.name === 'registration') + const isClientAPI = method => client && (isNotifier(method) || isProvider(method)) + const isServerAPI = method => !isClientAPI(method) + Object.values(modules).map(JSON.parse).filter(m => moduleWhitelist.length ? moduleWhitelist.includes(m.info.title) : true).forEach(json => { // pull in external markdown files for descriptions json = addExternalMarkdown(json, markdown) // put module name in front of each method - json.methods.forEach(method => method.name = method.name.includes('\.') ? method.name : json.info.title + '.' + method.name) + json.methods.filter(method => method.name.indexOf('.') === -1).forEach(method => method.name = json.info.title + '.' + method.name) // merge any info['x-'] extension values (maps & arrays only..) Object.keys(json.info).filter(key => key.startsWith('x-')).forEach(extension => { if (Array.isArray(json.info[extension])) { - openrpc.info[extension] = openrpc.info[extension] || [] - openrpc.info[extension].push(...json.info[extension]) + mergedOpenRpc.info[extension] = mergedOpenRpc.info[extension] || [] + mergedOpenRpc.info[extension].push(...json.info[extension]) } else if (typeof json.info[extension] === 'object') { - openrpc.info[extension] = openrpc.info[extension] || {} + mergedOpenRpc.info[extension] = mergedOpenRpc.info[extension] || {} Object.keys(json.info[extension]).forEach(k => { - openrpc.info[extension][k] = json.info[extension][k] + mergedOpenRpc.info[extension][k] = json.info[extension][k] }) } }) if (json.info.description) { - openrpc.info['x-module-descriptions'] = openrpc.info['x-module-descriptions'] || {} - openrpc.info['x-module-descriptions'][json.info.title] = json.info.description + mergedOpenRpc.info['x-module-descriptions'] = mergedOpenRpc.info['x-module-descriptions'] || {} + mergedOpenRpc.info['x-module-descriptions'][json.info.title] = json.info.description } - // add methods from this module - openrpc.methods.push(...json.methods) + mergedOpenRpc.methods.push(...json.methods) // add schemas from this module - json.components && Object.assign(openrpc.components.schemas, json.components.schemas) +// json.components && Object.assign(mergedOpenRpc.components.schemas, json.components.schemas) - // add externally referenced schemas that are in our shared schemas path - openrpc = addExternalSchemas(openrpc, sharedSchemas) + json.components && json.components.schemas && Object.assign(mergedOpenRpc.components.schemas, Object.fromEntries(Object.entries(json.components.schemas).map( ([key, schema]) => ([json.info.title + '.' + key, schema]) ))) + namespaceRefs('', json.info.title, mergedOpenRpc) - modules[key] = JSON.stringify(json, null, '\t') + // add externally referenced schemas that are in our shared schemas path + mergedOpenRpc = addExternalSchemas(mergedOpenRpc, sharedSchemas) - logSuccess(`Generated the ${json.info.title} module.`) + logSuccess(`Merged the ${json.info.title} module.`) }) + // Fireboltize! + mergedOpenRpc = fireboltize(mergedOpenRpc, !!client) + // make sure all provided-by APIs point to a real provider method - const appProvided = openrpc.methods.filter(m => m.tags.find(t=>t['x-provided-by'])) || [] + const appProvided = mergedOpenRpc.methods.filter(m => m.tags.find(t=>t['x-provided-by'])) || [] appProvided.forEach(m => { const providedBy = m.tags.find(t=>t['x-provided-by'])['x-provided-by'] - const provider = openrpc.methods.find(m => m.name === providedBy) + const provider = mergedOpenRpc.methods.find(m => m.name === providedBy) if (!provider) { throw `Method ${m.name} is provided by an undefined method (${providedBy})` } @@ -114,14 +126,29 @@ const run = async ({ } }) - openrpc = fireboltizeMerged(openrpc) + Object.assign(serverOpenRPC.info, mergedOpenRpc.info) + + // split into client & server APIs + serverOpenRPC.methods.push(...mergedOpenRpc.methods.filter(isServerAPI)) + clientOpenRPC && clientOpenRPC.methods.push(...mergedOpenRpc.methods.filter(isClientAPI)) + + // add schemas - TODO: this just blindly copies them all + mergedOpenRpc.components && Object.assign(serverOpenRPC.components.schemas, mergedOpenRpc.components.schemas) + clientOpenRPC && mergedOpenRpc.components && Object.assign(clientOpenRPC.components.schemas, mergedOpenRpc.components.schemas) + + // add externally referenced schemas that are in our shared schemas path + serverOpenRPC = addExternalSchemas(serverOpenRPC, sharedSchemas) + + // add externally referenced schemas that are in our shared schemas path + clientOpenRPC && (clientOpenRPC = addExternalSchemas(clientOpenRPC, sharedSchemas)) - await writeJson(output, openrpc) + await writeJson(server, serverOpenRPC) + clientOpenRPC && await writeJson(client, clientOpenRPC) - console.log() - logSuccess(`Wrote file ${path.relative('.', output)}`) + logSuccess(`Wrote file ${path.relative('.', server)}`) + client && logSuccess(`Wrote file ${path.relative('.', client)}`) return Promise.resolve() } -export default run +export default run \ No newline at end of file diff --git a/src/sdk/index.mjs b/src/sdk/index.mjs index f6fd0f5e..91228433 100755 --- a/src/sdk/index.mjs +++ b/src/sdk/index.mjs @@ -27,57 +27,82 @@ import macrofy from '../macrofier/index.mjs' /************************************************************************************************/ // destructure well-known cli args and alias to variables expected by script const run = async ({ - input: input, + server: server, + client: client, template: template, output: output, language: language, - 'static-module': staticModuleNames + 'static-module': staticModuleNames, + argv: { + remain: moduleWhitelist + } }) => { let mainFilename let declarationsFilename + const config = { + language: null, + project: null + } + try { + const projectDir = process.env.npm_config_local_prefix + const workspaceDir = path.dirname(process.env.npm_package_json) + // Important file/directory locations - const packageJsonFile = path.join(path.dirname(input), '..', 'package.json') + const packageJsonFile = path.join(workspaceDir, 'package.json') const packageJson = await readJson(packageJsonFile) mainFilename = path.basename(packageJson.main) declarationsFilename = path.basename(packageJson.types) + + // Load project firebolt-openrpc.config.json, if it exists + config.project = await readJson(path.join(projectDir, 'firebolt-openrpc.config.json')) } catch (error) { + //console.dir(error) // fail silently } - - const config = await readJson(path.join(language, 'language.config.json')) - return macrofy(input, template, output, { + config.language = await readJson(path.join(language, 'language.config.json')) + + if (config.project && config.project.languages && config.project.languages[config.language.langcode]) { + console.log(`Applying project overrides to language config:`) + const overrides = config.project.languages[config.language.langcode] + console.log(Object.entries(overrides).map( ([key, value]) => ` - ${key} -> ${JSON.stringify(value)}`).join('\n')) + Object.assign(config.language, overrides) + } + + return macrofy(server, client, template, output, { headline: 'SDK code', outputDirectory: 'sdk', sharedTemplates: path.join(language, 'templates'), staticContent: path.join(language, 'src', 'shared'), - templatesPerModule: config.templatesPerModule, - templatesPerSchema: config.templatesPerSchema, - persistPermission: config.persistPermission, - createPolymorphicMethods: config.createPolymorphicMethods, - operators: config.operators, - primitives: config.primitives, - createModuleDirectories: config.createModuleDirectories, - copySchemasIntoModules: config.copySchemasIntoModules, - extractSubSchemas: config.extractSubSchemas, - convertTuplesToArraysOrObjects: config.convertTuplesToArraysOrObjects, - unwrapResultObjects: config.unwrapResultObjects, - allocatedPrimitiveProxies: config.allocatedPrimitiveProxies, - additionalSchemaTemplates: config.additionalSchemaTemplates, - additionalMethodTemplates: config.additionalMethodTemplates, - templateExtensionMap: config.templateExtensionMap, - excludeDeclarations: config.excludeDeclarations, - extractProviderSchema: config.extractProviderSchema, + templatesPerModule: config.language.templatesPerModule, + templatesPerSchema: config.language.templatesPerSchema, + persistPermission: config.language.persistPermission, + createPolymorphicMethods: config.language.createPolymorphicMethods || false, + enableUnionTypes: config.language.enableUnionTypes || false, + operators: config.language.operators, + primitives: config.language.primitives, + createModuleDirectories: config.language.createModuleDirectories, + copySchemasIntoModules: config.language.copySchemasIntoModules, + mergeOnTitle: config.language.mergeOnTitle, + extractSubSchemas: config.language.extractSubSchemas, + convertTuplesToArraysOrObjects: config.language.convertTuplesToArraysOrObjects, + unwrapResultObjects: config.language.unwrapResultObjects, + allocatedPrimitiveProxies: config.language.allocatedPrimitiveProxies, + additionalSchemaTemplates: config.language.additionalSchemaTemplates, + additionalMethodTemplates: config.language.additionalMethodTemplates, + templateExtensionMap: config.language.templateExtensionMap, + excludeDeclarations: config.language.excludeDeclarations, staticModuleNames: staticModuleNames, hideExcluded: true, - aggregateFiles: config.aggregateFiles, + moduleWhitelist: moduleWhitelist, + aggregateFiles: config.language.aggregateFiles, rename: mainFilename ? { '/index.mjs': mainFilename, '/index.d.ts': declarationsFilename } : {}, - treeshakePattern: config.treeshakePattern ? new RegExp(config.treeshakePattern, "g") : undefined, - treeshakeTypes: config.treeshakeTypes, + treeshakePattern: config.language.treeshakePattern ? new RegExp(config.language.treeshakePattern, "g") : undefined, + treeshakeTypes: config.language.treeshakeTypes, treeshakeEntry: mainFilename ? '/' + mainFilename : '/index.mjs' }) } diff --git a/src/shared/json-schema.mjs b/src/shared/json-schema.mjs index 98fdeaba..39837f32 100644 --- a/src/shared/json-schema.mjs +++ b/src/shared/json-schema.mjs @@ -24,10 +24,27 @@ 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('/') + } -const refToPath = ref => { - let path = ref.split('#').pop().substr(1).split('/') return path.map(x => x.match(/^[0-9]+$/) ? parseInt(x) : x) } @@ -36,6 +53,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,24 +67,49 @@ 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))) } const getLinkedSchemaPaths = obj => { return objectPaths(obj) .filter(x => /\/\$ref$/.test(x)) - .map(refToPath) + .map(x => pathToArray(x, obj)) +} + +const getLinkedSchemaUris = obj => { + return objectPaths(obj) + .filter(x => /\/\$ref$/.test(x)) } const updateRefUris = (schema, uri) => { @@ -118,23 +163,41 @@ const replaceRef = (existing, replacement, schema) => { } } -const getPath = (uri = '', moduleJson = {}) => { +const namespaceRefs = (uri, namespace, schema) => { + if (schema) { + if (schema.hasOwnProperty('$ref') && (typeof schema['$ref'] === 'string')) { + const parts = schema.$ref.split('#') + if (parts[0] === uri && parts[1].indexOf('.') === -1) { + const old = schema.$ref + schema['$ref'] = schema['$ref'].split('#').map( x => x === uri ? uri : x.split('/').map((y, i, arr) => i===arr.length-1 ? namespace + '.' + y : y).join('/')).join('#') + } + } + else if (typeof schema === 'object') { + Object.keys(schema).forEach(key => { + namespaceRefs(uri, namespace, schema[key]) + }) + } + } +} + +const getReferencedSchema = (uri = '', moduleJson = {}) => { const [mainPath, subPath] = (uri || '').split('#') let result if (!uri) { - throw "getPath requires a non-null uri parameter" + throw "getReferencedSchema requires a non-null uri parameter" } if (mainPath) { - throw `Cannot call getPath with a fully qualified URI: ${uri}` + // TODO... assuming that bundles are in one of these two places is dangerous, should write a quick method to "find" where they are + result = getPathOr(null, ['components', 'schemas', mainPath, ...subPath.slice(1).split('/')], moduleJson) + || getPathOr(null, ['definitions', mainPath, ...subPath.slice(1).split('/')], moduleJson) } - - if (subPath) { + else if (subPath) { result = getPathOr(null, subPath.slice(1).split('/'), moduleJson) } if (!result) { - //throw `getPath: Path '${uri}' not found in ${moduleJson ? (moduleJson.title || moduleJson.info.title) : moduleJson}.` + //throw `getReferencedSchema: Path '${uri}' not found in ${moduleJson ? (moduleJson.title || moduleJson.info.title) : moduleJson}.` return null } else { @@ -149,12 +212,12 @@ const getPropertySchema = (json, dotPath, document) => { for (var i=0; i j >= i ).join('.') - if (node.$ref) { - node = getPropertySchema(getPath(node.$ref, document), remainingPath, document) - } - else if (property === '') { + if (property === '') { return node } + else if (node.$ref) { + node = getPropertySchema(getReferencedSchema(node.$ref, document), remainingPath, document) + } else if (node.type === 'object' || (node.type && node.type.includes && node.type.includes('object'))) { if (node.properties && node.properties[property]) { node = node.properties[property] @@ -200,9 +263,10 @@ const getPropertiesInSchema = (json, document) => { props.push(...Object.keys(node.properties)) } - if (node.propertyNames) { - props.push(...node.propertyNames) - } + // TODO: this propertyNames requires either additionalProperties or patternProperties in order to use this method w/ getPropertySchema, as intended... + // if (node.propertyNames) { + // props.push(...node.propertyNames) + // } return props } @@ -218,7 +282,7 @@ function getSchemaConstraints(schema, module, options = { delimiter: '\n' }) { if (schema['$ref']) { if (schema['$ref'][0] === '#') { - return getSchemaConstraints(getPath(schema['$ref'], module), module, options) + return getSchemaConstraints(getReferencedSchema(schema['$ref'], module), module, options) } else { return '' @@ -299,9 +363,10 @@ const localizeDependencies = (json, document, schemas = {}, options = defaultLoc let path = refs[i] const ref = getPathOr(null, path, definition) path.pop() // drop ref - if (refToPath(ref).length > 1) { - let resolvedSchema = JSON.parse(JSON.stringify(getPathOr(null, refToPath(ref), document))) - if (schemaReferencesItself(resolvedSchema, refToPath(ref))) { + if (pathToArray(ref, document).length > 1) { + let resolvedSchema = JSON.parse(JSON.stringify(getPathOr(null, pathToArray(ref, document), document))) + + if (schemaReferencesItself(resolvedSchema, pathToArray(ref, document))) { resolvedSchema = null } @@ -314,7 +379,9 @@ const localizeDependencies = (json, document, schemas = {}, options = defaultLoc // don't loose examples from original object w/ $ref // todo: should we preserve other things, like title? const examples = getPathOr(null, [...path, 'examples'], definition) - resolvedSchema.examples = examples || resolvedSchema.examples + if (examples || resolvedSchema.examples) { + resolvedSchema.examples = examples || resolvedSchema.examples + } definition = setPath(path, resolvedSchema, definition) } else { @@ -391,13 +458,7 @@ const localizeDependencies = (json, document, schemas = {}, options = defaultLoc findAndMergeAllOfs(pointer[key]) } else if (key === 'allOf' && Array.isArray(pointer[key])) { - const union = deepmerge.all(pointer.allOf.reverse()) // reversing so lower `title` attributes will win - const title = pointer.title - Object.assign(pointer, union) - if (title) { - pointer.title = title - } - delete pointer.allOf + definition = mergeAllOf(pointer) } }) } @@ -408,92 +469,473 @@ const localizeDependencies = (json, document, schemas = {}, options = defaultLoc return definition } +const mergeAllOf = (schema) => { + if (schema.allOf) { + const union = deepmerge.all(schema.allOf.reverse()) // reversing so lower `title` attributes will win + const title = schema.title + Object.assign(schema, union) + if (title) { + schema.title = title + } + delete schema.allOf + } + return schema +} + const getLocalSchemas = (json = {}) => { return Array.from(new Set(getLocalSchemaPaths(json).map(path => getPathOr(null, path, 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) } -function union(schemas) { - - const result = {}; - for (const schema of schemas) { - for (const [key, value] of Object.entries(schema)) { - if (!result.hasOwnProperty(key)) { - // If the key does not already exist in the result schema, add it - if (value && value.anyOf) { - result[key] = union(value.anyOf) - } else if (key === 'title' || key === 'description' || key === 'required') { - //console.warn(`Ignoring "${key}"`) - } else { - result[key] = value; - } - } else if (key === '$ref') { - if (result[key].endsWith("/ListenResponse")) { - - } - // If the key is '$ref' make sure it's the same - else if(result[key] === value) { - //console.warn(`Ignoring "${key}" that is already present and same`) - } else { - console.warn(`ERROR "${key}" is not same -${JSON.stringify(result, null, 4)} ${key} ${result[key]} - ${value}`); - throw "ERROR: $ref is not same" - } - } else if (key === 'type') { - // If the key is 'type', merge the types of the two schemas - if(result[key] === value) { - //console.warn(`Ignoring "${key}" that is already present and same`) - } else { - console.warn(`ERROR "${key}" is not same -${JSON.stringify(result, null, 4)} ${key} ${result[key]} - ${value}`); - throw "ERROR: type is not same" - } - } else { - //If the Key is a const then merge them into an enum - if(value && value.const) { - if(result[key].enum) { - result[key].enum = Array.from(new Set([...result[key].enum, value.const])) +const findAll = (document, finder) => { + const results = [] + + if (document && finder(document)) { + results.push(document) + } + + if ((typeof document) !== 'object' || !document) { + return results + } + + Object.keys(document).forEach(key => { + + if (Array.isArray(document) && key === 'length') { + return results + } + else if (typeof document[key] === 'object') { + results.push(...findAll(document[key], finder)) + } + }) + + return results +} + +const flattenMultipleOfs = (document, type, pointer, path) => { + if (!pointer) { + pointer = document + path = '' + } + + if ((typeof pointer) !== 'object' || !pointer) { + return + } + + if (pointer !== document && schemaReferencesItself(pointer, path.split('.'))) { + console.warn(`Skipping recursive schema: ${pointer.title}`) + return + } + + Object.keys(pointer).forEach(key => { + + if (Array.isArray(pointer) && key === 'length') { + return + } + if ( (pointer.$id && pointer !== document) || ((key !== type) && (typeof pointer[key] === 'object') && (pointer[key] != null))) { + flattenMultipleOfs(document, type, pointer[key], path + '.' + key) + } + else if (key === type && Array.isArray(pointer[key])) { + + try { + const schemas = pointer[key] + if (schemas.find(schema => schema.$ref?.endsWith("/ListenResponse"))) { + // ignore the ListenResponse parent anyOf, but dive into it's sibling + const sibling = schemas.find(schema => !schema.$ref?.endsWith("/ListenResponse")) + const n = schemas.indexOf(sibling) + flattenMultipleOfs(document, type, schemas[n], path + '.' + key + '.' + n) + } + else { + const title = pointer.title + let debug = false + Object.assign(pointer, combineSchemas(pointer[key], document, path, type === 'allOf')) + if (title) { + pointer.title = title + } + delete pointer[key] + } + } + catch(error) { + console.warn(` - Unable to flatten ${type} in ${path}`) + console.log(error) + } + } + }) +} + +function combineProperty(result, schema, document, prop, path, all) { + if (result.properties === undefined || result.properties[prop] === undefined) { + if (result.additionalProperties === false || schema.additionalProperties === false) { + if (all) { + // leave it out + } + else { + result.properties = result.properties || {} + result.properties[prop] = getPropertySchema(schema, prop, document) + } + } + else if (typeof result.additionalProperties === 'object') { + result.properties = result.properties || {} + result.properties[prop] = combineSchemas([result.additionalProperties, getPropertySchema(schema, prop, document)], document, path + '.' + prop, all) + } + else { + result.properties = result.properties || {} + result.properties[prop] = getPropertySchema(schema, prop, document) + } + } + else if (schema.properties === undefined || schema.properties[prop] === undefined) { + if (result.additionalProperties === false || schema.additionalProperties === false) { + if (all) { + delete result.properties[prop] + } + else { + // leave it + } + } + else if (typeof schema.additionalProperties === 'object') { + result.properties = result.properties || {} + result.properties[prop] = combineSchemas([schema.additionalProperties, getPropertySchema(result, prop, document)], document, path + '.' + prop, all) + } + else { + // do nothing + } + } + else { + const a = getPropertySchema(result, prop, document) + const b = getPropertySchema(schema, prop, document) + + result.properties[prop] = combineSchemas([a, b], document, path + '.' + prop, all, true) + } + + result = JSON.parse(JSON.stringify(result)) +} + +// TODO: fix titles, drop if/then/else/not +function combineSchemas(schemas, document, path, all, createRefIfNeeded=false) { + schemas = JSON.parse(JSON.stringify(schemas)) + let createRefSchema = false + + if (createRefIfNeeded && schemas.find(s => s?.$ref) && !schemas.every(s => s.$ref === schemas.find(s => s?.$ref).$ref)) { + createRefSchema = true + } + + const reference = createRefSchema ? schemas.filter(schema => schema?.$ref).map(schema => schema.$ref).reduce( (prefix, ref, i, arr) => { + if (prefix === '') { + if (arr.length === 1) { + return ref.split('/').slice(0, -1).join('/') + '/' + } + else { + return ref + } + } + else { + let index = 0 + while ((index < Math.min(prefix.length, ref.length)) && (prefix.charAt(index) === ref.charAt(index))) { + index++ + } + return prefix.substring(0, index) + } + }, '') : '' + + const resolve = (schema) => { + while (schema.$ref) { + if (!getReferencedSchema(schema.$ref, document)) { + console.log(`getReferencedSChema returned null`) + console.dir(schema) + } + schema = getReferencedSchema(schema.$ref, document) + } + return schema + } + + let debug = false + + const merge = (schema) => { + if (schema.allOf) { + schema.allOf = schema.allOf.map(resolve) + Object.assign(schema, combineSchemas(schema.allOf, document, path, true)) + delete schema.allOf + } + if (schema.oneOf) { + schema.oneOf = schema.oneOf.map(resolve) + Object.assign(schema, combineSchemas(schema.oneOf, document, path, false)) + delete schema.oneOf + } + if (schema.anyOf) { + schema.anyOf = schema.anyOf.map(resolve) + Object.assign(schema, combineSchemas(schema.anyOf, document, path, false)) + delete schema.anyOf + } + return schema + } + + const flatten = (schema) => { + while (schema.$ref || schema.oneOf || schema.anyOf || schema.allOf) { + schema = resolve(schema) + schema = merge(schema) + } + return schema + } + + let result = schemas.shift() + + schemas.forEach(schema => { + + if (!schema) { + return // skip + } + + if (schema.$ref && (schema.$ref === result.$ref)) { + return + } + + result = JSON.parse(JSON.stringify(flatten(result))) + schema = JSON.parse(JSON.stringify(flatten(schema))) + + if (schema.examples && result.examples) { + result.examples.push(...schema.examples) + } + + if (schema.anyOf) { + throw "Cannot combine schemas that contain anyOf" + } + else if (schema.oneOf) { + throw "Cannot combine schemas that contain oneOf" + } + else if (schema.allOf) { + throw "Cannot combine schemas that contain allOf" + } + else if (Array.isArray(schema.type)) { + throw "Cannot combine schemas that have type set to an Array" + } + else { + if (result.const !== undefined && schema.const != undefined) { + if (result.const === schema.const) { + return + } + else if (all) { + throw `Combined allOf resulted in impossible schema: const ${schema.const} !== const ${result.const}` + } + else { + result.enum = [result.const, schema.const] + result.type = typeof result.const + delete result.const + } + } + else if (result.enum && schema.enum) { + if (all) { + result.enum = result.enum.filter(value => schema.enum.includes(value)) + if (result.enum.length === 0) { + throw `Combined allOf resulted in impossible schema: enum: []` + } + } + else { + result.enum = Array.from(new Set(result.enum.concat(schema.enum))) + } + } + else if ((result.const !== undefined || schema.const !== undefined) && (result.enum || schema.enum)) { + if (all) { + const c = result.const !== undefined ? result.const : schema.const + const e = result.enum || schema.enum + if (e.contains(c)) { + result.const = c + delete result.enum + delete result.type } else { - result[key].enum = Array.from(new Set([result[key].const, value.const])) - delete result[key].const + throw `Combined allOf resulted in impossible schema: enum: ${e} does not contain const: ${c}` } } - // If the key exists in both schemas and is not 'type', merge the values - else if (Array.isArray(result[key])) { - // If the value is an array, concatenate the arrays and remove duplicates - result[key] = Array.from(new Set([...result[key], ...value])) - } else if (result[key] && result[key].enum && value && value.enum) { - //If the value is an enum, merge the enums together and remove duplicates - result[key].enum = Array.from(new Set([...result[key].enum, ...value.enum])) - } else if (typeof result[key] === 'object' && typeof value === 'object') { - // If the value is an object, recursively merge the objects - result[key] = union([result[key], value]); - } else if (result[key] !== value) { - // If the value is a primitive and is not the same in both schemas, ignore it - //console.warn(`Ignoring conflicting value for key "${key}"`) + else { + result.enum = Array.from(new Set([].concat(result.enum || result.const).concat(schema.enum || schema.const))) + result.type = result.type || schema.type + delete result.const } } + else if ((result.const !== undefined || schema.const !== undefined) && (result.type || schema.type)) { + // TODO need to make sure the types match + if (all) { + result.const = result.const !== undefined ? result.const : schema.const + delete result.type + } + else { + result.type = result.type || schema.type + delete result.const + } + } + else if (schema.type !== result.type) { + throw `Cannot combine schemas with property type conflicts, '${path}': ${schema.type} != ${result.type} in ${schema.title} / ${result.title}` + } + else if ((result.enum || schema.enum) && (result.type || schema.type)) { + if (all) { + result.enum = result.enum || schema.enum + } + else { + result.type = result.type || schema.type + delete result.enum + } + } + else if (schema.type === "object") { + const propsInSchema = getPropertiesInSchema(schema, document) + const propsOnlyInResult = getPropertiesInSchema(result, document).filter(p => !propsInSchema.includes(p)) + + propsInSchema.forEach(prop => { + combineProperty(result, schema, document, prop, path, all) + delete result.title + }) + + propsOnlyInResult.forEach(prop => { + combineProperty(result, schema, document, prop, path, all) + delete result.title + }) + + if (result.additionalProperties === false || schema.additionalProperties === false) { + if (all) { + result.additionalProperties = false + } + else { + if (result.additionalProperties === true || schema.additionalProperties === true || result.additionalProperties === undefined || schema.additionalProperties === undefined) { + result.additionalProperties = true + } + else if (typeof result.additionalProperties === 'object' || typeof schema.additionalProperties === 'object') { + result.additionalProperties = result.additionalProperties || schema.additionalProperties + } + } + } + else if (typeof result.additionalProperties === 'object' || typeof schema.additionalProperties === 'object') { + result.additionalProperties = combineSchemas([result.additionalProperties, schema.additionalProperties], document, path, all) + } + + if (Array.isArray(result.propertyNames) && Array.isArray(schema.propertyNames)) { + if (all) { + result.propertyNames = Array.from(new Set(result.propertyNames.concat(schema.propertyNames))) + } + else { + result.propertyNames = result.propertyNames.filter(prop => schema.propertyNames.includes(prop)) + } + } + else if (Array.isArray(result.propertyNames) || Array.isArray(schema.propertyNames)) { + if (all) { + result.propertyNames = result.propertyNames || schema.propertyNames + } + else { + delete result.propertyNames + } + } + + if (result.patternProperties || schema.patternProperties) { + throw `Cannot combine object schemas that have patternProperties ${schema.title} / ${result.title}, ${path}` + } + + if (result.required && schema.required) { + if (all) { + result.required = Array.from(new Set(result.required.concat(schema.required))) + } + else { + result.required = result.required.filter(prop => schema.required.includes(prop)) + } + } + else if (result.required || schema.required) { + if (all) { + result.required = result.required || schema.required + } + else { + delete result.required + } + } + } + else if (schema.type === "array") { + if (Array.isArray(result.items) || Array.isArray(schema.items)) { + throw `Cannot combine tuple schemas, ${path}: ${schema.title} / ${result.title}` + } + result.items = combineSchemas([result.items, schema.items], document, path, all) + } + + if (result.title || schema.title) { + result.title = schema.title || result.title // prefer titles from lower in the any/all/oneOf list + } + + // combine all other stuff + const skip = ['title', 'type', '$ref', 'const', 'enum', 'properties', 'items', 'additionalProperties', 'patternProperties', 'anyOf', 'oneOf', 'allOf'] + const keysInSchema = Object.keys(schema) + const keysOnlyInResult = Object.keys(result).filter(k => !keysInSchema.includes(k)) + + keysInSchema.filter(key => !skip.includes(key)).forEach(key => { + if (result[key] === undefined) { + if (all) { + result[key] = schema[key] + } + } + else { + // not worth doing this for code-generation, e.g. minimum doesn't actually affect type defintions in most languages + } + }) + + keysOnlyInResult.filter(key => !skip.includes(key)).forEach(key => { + if (all) { + // do nothing + } + else { + delete result[key] + } + }) } - } - return result; -} + }) -function mergeAnyOf(schema) { - return union(schema.anyOf) -} + delete result.if + delete result.then + delete result.else + delete result.not + + if (reference && createRefSchema) { + const [fragment, uri] = reference.split('#').reverse() + const title = result.title || path.split('.').slice(-2).map(x => x.charAt(0).toUpperCase() + x.substring(1)).join('') -function mergeOneOf(schema) { - return union(schema.oneOf) + result.title = title + + let bundle + + if (uri) { + bundle = findAll(document, s => s.$id === uri)[0] + } + else { + bundle = document + } + + let pathArray = (fragment + title).split('/') + const name = pathArray.pop() + let key, i=1 + while (key = pathArray[i]) { + bundle = bundle[key] + i++ + } + + bundle[name] = result + + const refSchema = { + $ref: [uri ? uri : '', [...pathArray, name].join('/')].join('#') + } + + return refSchema + } + + return result } + const getSafeEnumKeyName = (value) => value.split(':').pop() // use last portion of urn:style:values .replace(/[\.\-]/g, '_') // replace dots and dashes .replace(/\+/g, '_plus') // change + to _plus @@ -508,7 +950,9 @@ export { getLocalSchemas, getLocalSchemaPaths, getLinkedSchemaPaths, - getPath, + getLinkedSchemaUris, + getAllValuesForName, + getReferencedSchema, getPropertySchema, getPropertiesInSchema, isDefinitionReferencedBySchema, @@ -517,7 +961,10 @@ export { localizeDependencies, replaceUri, replaceRef, + namespaceRefs, removeIgnoredAdditionalItems, - mergeAnyOf, - mergeOneOf + combineSchemas, + flattenMultipleOfs, + schemaReferencesItself, + findAll } diff --git a/src/shared/markdown.mjs b/src/shared/markdown.mjs index 07deb125..3a3b764b 100644 --- a/src/shared/markdown.mjs +++ b/src/shared/markdown.mjs @@ -1,3 +1,21 @@ +/* + * 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 getFilename = (json, asPath) => (json ? json.info ? json.info.title : (asPath ? json.title : json.title + 'Schema'): '') const getDirectory = (json, asPath) => asPath ? json.info ? '' : 'schemas' : '' 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 5fa64c6a..e4d1d804 100644 --- a/src/shared/modules.mjs +++ b/src/shared/modules.mjs @@ -28,13 +28,10 @@ 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 } from './json-schema.mjs' -import { getPath as getRefDefinition } from './json-schema.mjs' +import { getExternalSchemaPaths, isDefinitionReferencedBySchema, isNull, localizeDependencies, isSchema, getLocalSchemaPaths, replaceRef, getPropertySchema, getLinkedSchemaUris, getAllValuesForName, replaceUri } from './json-schema.mjs' +import { getReferencedSchema } from './json-schema.mjs' const { isObject, isArray, propEq, pathSatisfies, hasProp, propSatisfies } = predicates - -// TODO remove these when major/rpc branch is merged -const name = method => method.name.split('.').pop() -const rename = (method, renamer) => method.name.split('.').map((x, i, arr) => i === (arr.length-1) ? renamer(x) : x).join('.') +import { extension, getNotifier, isEvent, isNotifier, isPusher, isRegistration, name as methodName, rename as methodRename, provides } from './methods.mjs' // util for visually debugging crocks ADTs const inspector = obj => { @@ -59,114 +56,167 @@ 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 isProviderInterfaceMethod = compose( +// compose( +// option(false), +// map(_ => true), +// chain( +// find( +// and( +// propEq('name', 'capabilities'), +// and( +// propSatisfies('x-provides', not(isEmpty)), +// and( +// propSatisfies('x-allow-focus-for', isEmpty), +// and( +// propSatisfies('x-response-for', isEmpty), +// propSatisfies('x-error-for', isEmpty) +// ) +// ) +// ) +// ) +// ) +// ), +// getPath(['tags']) +// ) +// ) const getProvidedCapabilities = (json) => { return Array.from(new Set([...getMethods(json).filter(isProviderInterfaceMethod).map(method => method.tags.find(tag => tag['x-provides'])['x-provides'])])) } +const getProvidedInterfaces = (json) => { +// return json.methods?.filter(m => extension(m, 'x-interface')).map(m => extension(m, 'x-interface')) || [] + return getInterfaces(json) -const getProviderInterfaceMethods = (capability, json) => { - return getMethods(json).filter(method => method.name.startsWith("onRequest") && method.tags && method.tags.find(tag => tag['x-provides'] === capability)) + const list = Array.from(new Set((json.methods || []).filter(m => m.tags.find(t => t['x-provides'])) + .filter(m => !extension(m, 'x-push')) + .filter(m => !m.tags.find(t => t.name.startsWith('polymorphic-pull'))) + .map(m => m.name.split('.')[0]))) + + return list } - -function getProviderInterface(capability, module, extractProviderSchema = false) { +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 +} + +// TODO: this code is all based on capability, but we now support two interfaces in the same capability. need to refactor + +const getProviderInterfaceMethods = (_interface, json, prefix) => { + return json.methods.filter(method => method.name.split('.')[0] === _interface).filter(isProviderInterfaceMethod) + //return getMethods(json).filter(method => methodName(method).startsWith(prefix) && method.tags && method.tags.find(tag => tag['x-provides'] === _interface)) +} + +function getProviderInterface(_interface, module) { module = JSON.parse(JSON.stringify(module)) - const iface = getProviderInterfaceMethods(capability, module)//.map(method => localizeDependencies(method, module, null, { mergeAllOfs: true })) - - iface.forEach(method => { - const payload = getPayloadFromEvent(method) - const focusable = method.tags.find(t => t['x-allow-focus']) - - // remove `onRequest` - method.name = method.name.charAt(9).toLowerCase() + method.name.substr(10) - const schema = getPropertySchema(payload, 'properties.parameters', module) - - method.params = [ - { - "name": "parameters", - "required": true, - "schema": schema - } - ] - - if (!extractProviderSchema) { - let exampleResult = null + // TODO: localizeDependencies?? + const iface = getProviderInterfaceMethods(_interface, module).map(method => localizeDependencies(method, module, null, { mergeAllOfs: true })) + + if (iface.length && iface.every(method => methodName(method).startsWith('onRequest'))) { + console.log(`Transforming legacy provider interface ${_interface}`) + updateUnidirectionalProviderInterface(iface, module) + } + + return iface +} - if (method.tags.find(tag => tag['x-response'])) { - const result = method.tags.find(tag => tag['x-response'])['x-response'] - - method.result = { - "name": "result", - "schema": result +const capitalize = str => str.charAt(0).toUpperCase() + str.substr(1) + +// This is getting called before downgrading the provider interfaces AND after... it can't work for both cases. +function getUnidirectionalProviderInterfaceName(_interface, capability, document = {}) { + const iface = getProviderInterface(_interface, document) + const [ module, method ] = iface[0].name.split('.') + const uglyName = capability.split(":").slice(-2).map(capitalize).reverse().join('') + "Provider" + let name = iface.length === 1 ? method.charAt(0).toUpperCase() + method.substr(1) + "Provider" : uglyName + + if (document.info['x-interface-names']) { + name = document.info['x-interface-names'][capability] || name + } + return name + } + +function updateUnidirectionalProviderInterface(iface, module) { + iface.forEach(method => { + const payload = getPayloadFromEvent(method) + const focusable = method.tags.find(t => t['x-allow-focus']) + + // remove `onRequest` + method.name = methodRename(method, name => name.charAt(9).toLowerCase() + name.substr(10)) + + const schema = getPropertySchema(payload, 'properties.parameters', module) + + method.params = [ + { + "name": "parameters", + "required": true, + "schema": schema } + ] + + // TODO: we used to say !extractProviderSchema, which CPP sets to true and therefor skips this. not sure why... + if (true) { + let exampleResult = null - if (result.examples && result.examples[0]) { - exampleResult = result.examples[0] - } - } - else { - method.result = { - "name": "result", - "schema": { - "const": null + if (method.tags.find(tag => tag['x-response'])) { + const result = method.tags.find(tag => tag['x-response'])['x-response'] + + method.result = { + "name": "result", + "schema": result + } + + if (result.examples && result.examples[0]) { + exampleResult = result.examples[0] } } - } - - method.examples = method.examples.map( example => ( - { - params: [ - { - name: "parameters", - value: example.result.value.parameters - }, - { - name: "correlationId", - value: example.result.value.correlationId + else { + method.result = { + "name": "result", + "schema": { + "const": null } - ], - result: { - name: "result", - value: exampleResult } } - )) - - // remove event tag - method.tags = method.tags.filter(tag => tag.name !== 'event') - } - }) - - return iface - } - - -const addMissingTitles = ([k, v]) => { - if (v && !v.hasOwnProperty('title')) { - v.title = k - } - return v + + method.examples = method.examples.map( example => ( + { + params: [ + { + name: "parameters", + value: example.result.value.parameters + }, + { + name: "correlationId", + value: example.result.value.correlationId + } + ], + result: { + name: "result", + value: exampleResult + } + } + )) + + // remove event tag + method.tags = method.tags.filter(tag => tag.name !== 'event') + } + }) } // Maybe an array of from the schema @@ -322,10 +372,26 @@ const getParamsFromMethod = compose( getPath(['params']) ) -const getPayloadFromEvent = (event) => { - const choices = (event.result.schema.oneOf || event.result.schema.anyOf) - const choice = choices.find(schema => schema.title !== 'ListenResponse' && !(schema['$ref'] || '').endsWith('/ListenResponse')) - return choice +const getPayloadFromEvent = (event, client) => { + try { + if (event.result) { + const choices = (event.result.schema.oneOf || event.result.schema.anyOf) + if (choices) { + const choice = choices.find(schema => schema.title !== 'ListenResponse' && !(schema['$ref'] || '').endsWith('/ListenResponse')) + return choice + } + else if (client) { + const payload = getNotifier(event, client).params.slice(-1)[0].schema + return payload + } + else { + return event.result.schema + } + } + } + catch (error) { + throw error + } } const getSetterFor = (property, json) => json.methods && json.methods.find(m => m.tags && m.tags.find(t => t['x-setter-for'] === property)) @@ -336,18 +402,18 @@ const providerHasNoParameters = (schema) => { if (schema.allOf || schema.oneOf) { return !!(schema.allOf || schema.oneOf).find(schema => providerHasNoParameters(schema)) } - else if (schema.properties && schema.properties.parameters) { - return isNull(schema.properties.parameters) + else if (schema.properties && schema.properties.params) { + return isNull(schema.properties.params) } else { console.dir(schema, {depth: 10}) - throw "Invalid ProviderRequest" + console.log("Invalid ProviderRequest") } } const validEvent = and( pathSatisfies(['name'], isString), - pathSatisfies(['name'], x => x.match(/on[A-Z]/)) + pathSatisfies(['name'], x => x.split('.').pop().match(/on[A-Z]/)) ) // Pick events out of the methods array @@ -356,7 +422,7 @@ const getEvents = compose( map(filter(validEvent)), // Maintain the side effect of process.exit here if someone is violating the rules map(map(e => { - if (!e.name.match(/on[A-Z]/)) { + if (!methodName(e).match(/on[A-Z]/)) { console.error(`ERROR: ${e.name} method is tagged as an event, but does not match the pattern "on[A-Z]"`) process.exit(1) // Non-zero exit since we don't want to continue. Useful for CI/CD pipelines. } @@ -381,14 +447,14 @@ const eventDefaults = event => { event.tags = [ { - 'name': 'event' + 'name': 'notifier' } ] return event } -const createEventResultSchemaFromProperty = (property, type='') => { +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') @@ -398,7 +464,7 @@ const createEventResultSchemaFromProperty = (property, type='') => { if ( subscriberType === 'global') { // wrap the existing result and the params in a new result object const schema = { - title: name + type + 'Info', + title: methodRename(property, name => name.charAt(0).toUpperCase() + name.substring(1) + type + 'Info').split('.').pop(), type: "object", properties: { @@ -420,64 +486,53 @@ const createEventResultSchemaFromProperty = (property, type='') => { } } -const createEventFromProperty = (property, type='', alternative, json) => { - const provider = (property.tags.find(t => t['x-provided-by']) || {})['x-provided-by'] - const pusher = provider ? provider.replace('onRequest', '').split('.').map((x, i, arr) => (i === arr.length-1) ? x.charAt(0).toLowerCase() + x.substr(1) : x).join('.') : undefined - const event = eventDefaults(JSON.parse(JSON.stringify(property))) -// event.name = (module ? module + '.' : '') + 'on' + event.name.charAt(0).toUpperCase() + event.name.substr(1) + type - event.name = provider ? provider.split('.').pop().replace('onRequest', '') : event.name.charAt(0).toUpperCase() + event.name.substr(1) + type - event.name = event.name.split('.').map((x, i, arr) => (i === arr.length-1) ? 'on' + x.charAt(0).toUpperCase() + x.substr(1) : x).join('.') - const subscriberFor = pusher || (json.info.title + '.' + property.name) - - const old_tags = JSON.parse(JSON.stringify(property.tags)) +const createNotifierFromProperty = (property, type='Changed') => { + const subscriberType = property.tags.map(t => t['x-subscriber-type']).find(t => typeof t === 'string') || 'context' - alternative && (event.tags[0]['x-alternative'] = alternative) + const notifier = JSON.parse(JSON.stringify(property)) + notifier.name = methodRename(notifier, name => name + type) - !provider && event.tags.unshift({ - name: "subscriber", - 'x-subscriber-for': subscriberFor + 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)) }) - const subscriberType = property.tags.map(t => t['x-subscriber-type']).find(t => typeof t === 'string') || 'context' - - // if the subscriber type is global, zap all of the parameters and change the result type to the schema that includes them if (subscriberType === 'global') { - - // wrap the existing result and the params in a new result object - const result = { - name: "data", - schema: { - $ref: "#/components/schemas/" + event.name.substring(2) + 'Info' + notifier.params = [ + { + name: "info", + schema: { + "$ref": "#/components/schemas/" + methodRename(notifier, name => name.charAt(0).toUpperCase() + name.substr(1) + 'Info') + } } - } - - event.examples.map(example => { - const result = {} - example.params.filter(p => p.name !== 'listen').forEach(p => { - result[p.name] = p.value - }) - result[example.result.name] = example.result.value - example.params = example.params.filter(p => p.name === 'listen') - example.result.name = "data" - example.result.value = result - }) - - event.result = result - - // remove the params - event.params = event.params.filter(p => p.name === 'listen') + ] + } + else { + notifier.params.push(notifier.result) } - old_tags.forEach(t => { - if (t.name !== 'property' && !t.name.startsWith('property:') && t.name !== 'push-pull') - { - event.tags.push(t) - } - }) + delete notifier.result - provider && (event.tags.find(t => t.name === 'capabilities')['x-provided-by'] = subscriberFor) + 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 event + return notifier } // create foo() notifier from onFoo() event @@ -513,37 +568,44 @@ const createNotifierFromEvent = (event, json) => { return push } -const createPushEvent = (requestor, json) => { - return createEventFromProperty(requestor, '', undefined, json) -} - const createPullEventFromPush = (pusher, json) => { - const event = eventDefaults(JSON.parse(JSON.stringify(pusher))) + const event = 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.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 = (pusher.name.charAt(0).toUpperCase() + pusher.name.substr(1)) + "FederatedRequest" - event.result.name = "request" - event.result.summary = "A " + requestType + " object." + 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 + } + }) - event.result.schema = { - "$ref": "#/components/schemas/" + requestType - } + delete event.result const exampleResult = { - name: "result", + 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 = [] + delete example.result + example.params = [ + exampleResult + ] }) old_tags.forEach(t => { @@ -556,65 +618,19 @@ const createPullEventFromPush = (pusher, json) => { 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 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 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 caps = event.tags.find(t => t.name === 'capabilities') + 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) => { @@ -698,7 +714,7 @@ const createTemporalEventMethod = (method, json, name) => { const createEventFromMethod = (method, json, name, correlationExtension, tagsToRemove = []) => { const event = eventDefaults(JSON.parse(JSON.stringify(method))) - event.name = 'on' + name + event.name = methodRename(event, _ => 'on' + name) const old_tags = JSON.parse(JSON.stringify(method.tags)) event.tags[0][correlationExtension] = method.name @@ -719,7 +735,7 @@ const createEventFromMethod = (method, json, name, correlationExtension, tagsToR const createTemporalStopMethod = (method, jsoname) => { 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({ @@ -757,7 +773,7 @@ const createTemporalStopMethod = (method, jsoname) => { const createSetterFromProperty = property => { const setter = JSON.parse(JSON.stringify(property)) - setter.name = 'set' + setter.name.charAt(0).toUpperCase() + setter.name.substr(1) + setter.name = methodRename(setter, name => 'set' + name.charAt(0).toUpperCase() + name.substr(1)) const old_tags = setter.tags setter.tags = [ { @@ -805,14 +821,10 @@ const createSetterFromProperty = property => { } const createFocusFromProvider = provider => { - - 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.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 @@ -841,13 +853,9 @@ const createFocusFromProvider = provider => { // 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 = 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.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()}.` response.tags = response.tags.filter(t => t.name !== 'event') response.tags.find(t => t.name === 'capabilities')[`x-${type.toLowerCase()}-for`] = provider.name @@ -995,17 +1003,17 @@ const generatePropertyEvents = json => { 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') + json.methods.push(createNotifierFromProperty(property)) + const schema = createEventResultSchemaFromProperty(property) if (schema) { - json.components.schemas[schema.title] = schema + json.components.schemas[property.name.split('.').shift() + '.' + schema.title] = schema } }) readonlies.forEach(property => { - json.methods.push(createEventFromProperty(property, 'Changed', property.name, json)) - const schema = createEventResultSchemaFromProperty(property, 'Changed') + json.methods.push(createNotifierFromProperty(property)) + const schema = createEventResultSchemaFromProperty(property) if (schema) { - json.components.schemas[schema.title] = schema + json.components.schemas[property.name.split('.').shift() + '.' + schema.title] = schema } }) @@ -1028,38 +1036,14 @@ const generatePolymorphicPullEvents = json => { return json } -const generatePushPullMethods = json => { - const requestors = json.methods.filter( m => m.tags && m.tags.find( t => t.name == 'push-pull')) || [] - requestors.forEach(requestor => { - json.methods.push(createPushEvent(requestor, json)) - - const schema = createEventResultSchemaFromProperty(requestor) - if (schema) { - json.components = json.components || {} - json.components.schemas = json.components.schemas || {} - json.components.schemas[schema.title] = schema - } - }) - - return 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)) + 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'])) || [] requestors.forEach(requestor => { - const schema = createPullProviderParams(requestor) - json.methods.push(createPullProvider(requestor, schema.title)) - - json.components = json.components || {} - json.components.schemas = json.components.schemas || {} - json.components.schemas[schema.title] = schema + 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 @@ -1076,10 +1060,120 @@ const generateTemporalSetMethods = json => { } -const generateProviderMethods = json => { - const providers = json.methods.filter( m => name(m).startsWith('onRequest') && m.tags && m.tags.find( t => t.name == 'capabilities' && t['x-provides'])) || [] +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'])) || [] - providers.forEach(provider => { + // 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 generateProviderMethods = (json) => { + const providers = json.methods.filter(isProviderInterfaceMethod) || [] + + providers.filter(p => methodName(p).startsWith('onRequest') && p.tags.find(t => t.name === 'event')).forEach(provider => { if (! isRPCOnlyMethod(provider)) { provider.tags.unshift({ "name": "rpc-only" @@ -1099,6 +1193,182 @@ const generateProviderMethods = json => { 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 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.tags.find(t => t.name === 'notifier')['x-notifier'] = notifier.name + subscriber.tags.find(t => t.name === 'notifier').name = 'event' + subscriber.result = { + name: "result", + schema: { + "type": "null" + } + } + json.methods.push(subscriber) + } + }) + + 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')) || [] @@ -1133,7 +1403,7 @@ const generateEventListenResponse = json => { // only want or and xor here (might even remove xor) const anyOf = event.result.schema.oneOf || event.result.schema.anyOf const ref = { - "$ref": "https://meta.comcast.com/firebolt/types#/definitions/ListenResponse" + "$ref": "https://meta.rdkcentral.com/firebolt/schemas/types#/definitions/ListenResponse" } if (anyOf) { @@ -1152,138 +1422,6 @@ const generateEventListenResponse = json => { return json } -const getAnyOfSchema = (inType, json) => { - let anyOfTypes = [] - let outType = localizeDependencies(inType, json) - if (outType.schema.anyOf) { - let definition = '' - if (inType.schema['$ref'] && (inType.schema['$ref'][0] === '#')) { - definition = getRefDefinition(inType.schema['$ref'], json, json['x-schemas']) - } - else { - definition = outType.schema - } - definition.anyOf.forEach(anyOf => { - anyOfTypes.push(anyOf) - }) - outType.schema.anyOf = anyOfTypes - } - return outType -} - -const generateAnyOfSchema = (anyOf, name, summary) => { - let anyOfType = {} - anyOfType["name"] = name; - anyOfType["summary"] = summary - anyOfType["schema"] = anyOf - return anyOfType -} - -const generateParamsAnyOfSchema = (methodParams, anyOf, anyOfTypes, title, summary) => { - let params = [] - methodParams.forEach(p => { - if (p.schema.anyOf === anyOfTypes) { - let anyOfType = generateAnyOfSchema(anyOf, p.name, summary) - anyOfType.required = p.required - params.push(anyOfType) - } - else { - params.push(p) - } - }) - return params -} - -const generateResultAnyOfSchema = (method, methodResult, anyOf, anyOfTypes, title, summary) => { - let methodResultSchema = {} - if (methodResult.schema.anyOf === anyOfTypes) { - let anyOfType = generateAnyOfSchema(anyOf, title, summary) - let index = 0 - if (isEventMethod(method)) { - index = (method.result.schema.anyOf || method.result.schema.oneOf).indexOf(getPayloadFromEvent(method)) - } - else { - index = (method.result.schema.anyOf || method.result.schema.oneOf).indexOf(anyOfType) - } - if (method.result.schema.anyOf) { - methodResultSchema["anyOf"] = Object.assign([], method.result.schema.anyOf) - methodResultSchema.anyOf[index] = anyOfType.schema - } - else if (method.result.schema.oneOf) { - methodResultSchema["oneOf"] = Object.assign([], method.result.schema.oneOf) - methodResultSchema.oneOf[index] = anyOfType.schema - } - else { - methodResultSchema = anyOfType.schema - } - } - return methodResultSchema -} - -const createPolymorphicMethods = (method, json) => { - let anyOfTypes - let methodParams = [] - let methodResult = Object.assign({}, method.result) - - method.params.forEach(p => { - if (p.schema) { - let param = getAnyOfSchema(p, json) - if (param.schema.anyOf && anyOfTypes) { - //anyOf is allowed with only one param in the params list - throw `WARNING anyOf is repeated with param:${p}` - } - else if (param.schema.anyOf) { - anyOfTypes = param.schema.anyOf - } - methodParams.push(param) - } - }) - let foundAnyOfParams = anyOfTypes ? true : false - - if (isEventMethod(method)) { - methodResult.schema = getPayloadFromEvent(method) - } - methodResult = getAnyOfSchema(methodResult, json) - let foundAnyOfResult = methodResult.schema.anyOf ? true : false - if (foundAnyOfParams === true && foundAnyOfResult === true) { - throw `WARNING anyOf is already with param schema, it is repeated with ${method.name} result too` - } - else if (foundAnyOfResult === true) { - anyOfTypes = methodResult.schema.anyOf - } - let polymorphicMethodSchemas = [] - //anyOfTypes will be allowed either in any one of the params or in result - if (anyOfTypes) { - let polymorphicMethodSchema = { - name: {}, - tags: {}, - summary: `${method.summary}`, - params: {}, - result: {}, - examples: {} - } - anyOfTypes.forEach(anyOf => { - - let localized = localizeDependencies(anyOf, json) - let title = localized.title || localized.name || '' - let summary = localized.summary || localized.description || '' - polymorphicMethodSchema.rpc_name = method.name - polymorphicMethodSchema.name = foundAnyOfResult && isEventMethod(method) ? `${method.name}${title}` : method.name - polymorphicMethodSchema.tags = method.tags - polymorphicMethodSchema.params = foundAnyOfParams ? generateParamsAnyOfSchema(methodParams, anyOf, anyOfTypes, title, summary) : methodParams - polymorphicMethodSchema.result = Object.assign({}, method.result) - polymorphicMethodSchema.result.schema = foundAnyOfResult ? generateResultAnyOfSchema(method, methodResult, anyOf, anyOfTypes, title, summary) : methodResult.schema - polymorphicMethodSchema.examples = method.examples - polymorphicMethodSchemas.push(Object.assign({}, polymorphicMethodSchema)) - }) - } - else { - polymorphicMethodSchemas = method - } - - return polymorphicMethodSchemas -} - const isSubSchema = (schema) => schema.type === 'object' || (schema.type === 'string' && schema.enum) const isSubEnumOfArraySchema = (schema) => (schema.type === 'array' && schema.items.enum) @@ -1350,24 +1488,33 @@ 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) => { - json = copyAllowFocusTags(json) + if (bidirectional) { + console.log('Creating bidirectional APIs') + json = generateEventSubscribers(json) + json = generateProviderRegistrars(json) + // generateInterfaceProviders + } + else { + console.log('Creating uni-directional provider and event 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 } @@ -1452,88 +1599,120 @@ 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 + 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 + } } - 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 = 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) - - if (!used) { - delete schema[name] - deleted = true - } - else { + 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 if (typeof schema[name] === 'object') { - deleted = deleted || recurse(schema[name], path + '/' + name) + 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 deleted + return added } if (schema.components.schemas) { - while(recurse(schema.components.schemas, '#/components/schemas')) {} - } - - if (schema['x-schemas']) { - while(recurse(schema['x-schemas'], '#/x-schemas')) {} + while(addSchemas(schema, refs)) { + refs.length = 0 + refs.push(...getAllValuesForName('$ref', schema)) + } } return schema } +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 json +} + const getModule = (name, json, copySchemas, extractSubSchemas) => { + + // TODO: extractSubschemas was added by cpp branch, but that code is short-circuited out here... + let openrpc = JSON.parse(JSON.stringify(json)) openrpc.methods = openrpc.methods .filter(method => method.name.toLowerCase().startsWith(name.toLowerCase() + '.')) - .map(method => Object.assign(method, { name: method.name.split('.').pop() })) + .filter(method => method.name !== 'rpc.discover') +// .map(method => Object.assign(method, { name: method.name.split('.').pop() })) openrpc.info.title = name + openrpc.components.schemas = Object.fromEntries(Object.entries(openrpc.components.schemas).filter( ([key, schema]) => key.startsWith('http') || key.split('.')[0] === name)) if (json.info['x-module-descriptions'] && json.info['x-module-descriptions'][name]) { openrpc.info.description = json.info['x-module-descriptions'][name] } delete openrpc.info['x-module-descriptions'] + + openrpc = promoteAndNameXSchemas(openrpc) + return removeUnusedSchemas(openrpc) + return removeUnusedBundles(removeUnusedSchemas(openrpc)) + const copy = JSON.parse(JSON.stringify(openrpc)) // zap all of the schemas @@ -1594,6 +1773,27 @@ const getModule = (name, json, copySchemas, extractSubSchemas) => { return removeUnusedSchemas(openrpc) } +const getClientModule = (name, client, server) => { + + const notifierFor = m => (m.tags.find(t => t['x-event']) || {})['x-event'] + const interfaces = server.methods.filter(m => m.tags.find(t => t['x-interface'])) + .map(m => m.tags.find(t => t['x-interface'])['x-interface']) + + let openrpc = JSON.parse(JSON.stringify(client)) + + openrpc.methods = openrpc.methods + .filter(method => (notifierFor(method) && notifierFor(method).startsWith(name + '.') || interfaces.find(name => method.name.startsWith(name + '.')))) + openrpc.info.title = name + openrpc.components.schemas = Object.fromEntries(Object.entries(openrpc.components.schemas).filter( ([key, schema]) => key.startsWith('http') || key.split('.')[0] === name)) + if (client.info['x-module-descriptions'] && client.info['x-module-descriptions'][name]) { + openrpc.info.description = client.info['x-module-descriptions'][name] + } + delete openrpc.info['x-module-descriptions'] + + openrpc = promoteAndNameXSchemas(openrpc) + return removeUnusedBundles(removeUnusedSchemas(openrpc)) +} + const getSemanticVersion = json => { const str = json && json.info && json.info.version || '0.0.0-unknown.0' const version = { @@ -1655,6 +1855,8 @@ export { getMethods, getProviderInterface, getProvidedCapabilities, + getProvidedInterfaces, + getUnidirectionalProviderInterfaceName, getSetterFor, getSubscriberFor, getEnums, @@ -1664,15 +1866,15 @@ export { getSchemas, getParamsFromMethod, fireboltize, - fireboltizeMerged, getPayloadFromEvent, getPathFromModule, providerHasNoParameters, removeUnusedSchemas, + removeUnusedBundles, getModule, + getClientModule, getSemanticVersion, addExternalMarkdown, addExternalSchemas, - getExternalMarkdownPaths, - createPolymorphicMethods + getExternalMarkdownPaths } diff --git a/src/shared/typescript.mjs b/src/shared/typescript.mjs index 930549f1..7eb5a99a 100644 --- a/src/shared/typescript.mjs +++ b/src/shared/typescript.mjs @@ -17,7 +17,7 @@ */ import deepmerge from 'deepmerge' -import { getPath, getSafeEnumKeyName, localizeDependencies } from './json-schema.mjs' +import { getReferencedSchema, getSafeEnumKeyName, localizeDependencies } from './json-schema.mjs' const isSynchronous = m => !m.tags ? false : m.tags.map(t => t.name).find(s => s === 'synchronous') @@ -37,9 +37,8 @@ function getMethodSignatureParams(method, module, { destination }) { const safeName = prop => prop.match(/[.+]/) ? '"' + prop + '"' : prop function getSchemaShape(schema = {}, module = {}, { name = '', level = 0, title, summary, descriptions = true, destination, enums = true } = {}) { - schema = JSON.parse(JSON.stringify(schema)) + schema = JSON.parse(JSON.stringify(schema)) let structure = [] - let prefix = (level === 0 ? 'type ' : '') let operator = (level == 0 ? ' =' : ':') let theTitle = (level === 0 ? schema.title || name : name) @@ -58,7 +57,7 @@ function getSchemaShape(schema = {}, module = {}, { name = '', level = 0, title, return `${prefix}${theTitle};` } else { - const someJson = getPath(schema['$ref'], module) + const someJson = getReferencedSchema(schema['$ref'], module) if (someJson) { return getSchemaShape(someJson, module, { name, level, title, summary, descriptions, destination, enums: false }) } @@ -141,7 +140,7 @@ function getSchemaShape(schema = {}, module = {}, { name = '', level = 0, title, } } - let union = deepmerge.all([...schema.allOf.map(x => x['$ref'] ? getPath(x['$ref'], module) || x : x).reverse()], { + let union = deepmerge.all([...schema.allOf.map(x => x['$ref'] ? getReferencedSchema(x['$ref'], module) || x : x).reverse()], { customMerge: merger }) @@ -205,7 +204,7 @@ function getSchemaShape(schema = {}, module = {}, { name = '', level = 0, title, if (schema['$ref']) { if (schema['$ref'][0] === '#') { - return getSchemaType(getPath(schema['$ref'], module), module, {title: true, link: link, code: code, destination}) + return getSchemaType(getReferencedSchema(schema['$ref'], module), module, {title: true, link: link, code: code, destination}) } else { // TODO: This never happens... but might be worth keeping in case we link to an opaque external schema at some point? @@ -280,7 +279,7 @@ function getSchemaShape(schema = {}, module = {}, { name = '', level = 0, title, } } else if (schema.allOf) { - let union = deepmerge.all([...schema.allOf.map(x => x['$ref'] ? getPath(x['$ref'], module) || x : x)]) + let union = deepmerge.all([...schema.allOf.map(x => x['$ref'] ? getReferencedSchema(x['$ref'], module) || x : x)]) if (schema.title) { union.title = schema.title } @@ -301,7 +300,7 @@ function getSchemaShape(schema = {}, module = {}, { name = '', level = 0, title, else if (schema.type === 'object' && schema.title) { const maybeGetPath = (path, json) => { try { - return getPath(path, json) + return getReferencedSchema(path, json) } catch (e) { return null diff --git a/src/slice/index.mjs b/src/slice/index.mjs index f0a8b993..94a52d94 100644 --- a/src/slice/index.mjs +++ b/src/slice/index.mjs @@ -17,7 +17,8 @@ */ import { readJson, writeJson } from '../shared/filesystem.mjs' -import { removeUnusedSchemas } from '../shared/modules.mjs' +import { getAllValuesForName } from '../shared/json-schema.mjs' +import { addExternalSchemas, removeUnusedBundles, removeUnusedSchemas } from '../shared/modules.mjs' // destructure well-known cli args and alias to variables expected by script const run = ({ @@ -76,6 +77,7 @@ const run = ({ // Tree-shake unused schemas openrpc.components = removeUnusedSchemas(openrpc).components + openrpc.components.schemas = removeUnusedBundles(openrpc).components.schemas await writeJson(output, openrpc) resolve() diff --git a/src/update/index.mjs b/src/update/index.mjs new file mode 100644 index 00000000..d47a32dd --- /dev/null +++ b/src/update/index.mjs @@ -0,0 +1,174 @@ +/* + * 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 + */ + +import { readJson, readFiles, readDir, writeJson, writeFiles } from "../shared/filesystem.mjs" +import { addExternalMarkdown, addExternalSchemas, fireboltize } from "../shared/modules.mjs" +import path from "path" +import { logHeader, logSuccess } from "../shared/io.mjs" +import { isEvent, isProvider } from "../shared/methods.mjs" +import { getReferencedSchema } from '../shared/json-schema.mjs' + +const run = async ({ + input: input +}) => { + + logHeader(`Upgrading modules to latest Firebolt OpenRPC schema`) + + const moduleList = input ? await readDir(path.join(input, 'openrpc'), { recursive: true, base: path.resolve('.') }) : [] + const modules = await readFiles(moduleList, path.resolve('.') + '/.') + + console.log(input) + console.log(path.resolve(input)) + console.dir(moduleList) + + Object.keys(modules).forEach(key => { + let json = JSON.parse(modules[key]) + + // Do the firebolt API magic + update(json) + + modules[key] = JSON.stringify(json, null, '\t') + + logSuccess(`Updated the ${json.info.title} module.`) + }) + + await writeFiles(modules) + + console.log() + logSuccess(`Wrote files`) + + return Promise.resolve() +} + +function update(json) { + json.methods = json.methods.map(method => { + // update providers + if (isProvider(method)) { + // handle Provider Interfaces + if (method.name.startsWith('onRequest')) { + // simplify name + method.name = method.name.charAt(9).toLowerCase() + method.name.substr(10) + + // move params out of custom extension, and unwrap them into individual parameters + method.params.splice(0, method.params.length) + console.dir(method) + console.log(method.result.schema.$ref) + const request = method.result.schema.$ref ? getReferencedSchema(method.result.schema.$ref, json) : method.result.schema + console.dir(request, { depth: 10 }) + console.log((request.allOf ? request.allOf[1] : request).properties.parameters) + let params = (request.allOf ? request.allOf[1] : request).properties.parameters + if (params.$ref) { + params = getReferencedSchema(params.$ref, json) + } + + // add required params first, in order listed + params.required && params.required.forEach(p => { + method.params.push({ + name: p, + required: true, + schema: params.properties[p] + }) + delete params.properties[p] + }) + + // add unrequired params in arbitrary order... (there's currently no provider method method with more than one unrequired param) + if (params.type === "object" && params.properties) { + Object.keys(params.properties).forEach(p => { + method.params.push({ + name: p, + required: false, + schema: params.properties[p] + }) + delete params.properties[p] + }) + } + + // move result out of custom extension + method.result = { + name: 'result', + schema: isEvent(method)['x-response'] || { type: "null", examples: [ null ] } + } + + // fix example pairings + method.examples.forEach((example, i) => { + if (example.result.value.parameters) { + example.params = Object.entries(example.result.value.parameters).map(entry => ({ + name: entry[0], + value: entry[1] + })) + } + + const result = method.result.schema.examples ? method.result.schema.examples[Math.min(i, method.result.schema.examples.length-1)] : getReferencedSchema(method.result.schema.$ref, json).examples[0] + example.result = { + "name": "result", + "value": result + } + }) + + // delete examples, TODO: this needs to go into the method pairing examples... + delete method.result.schema.examples + + // TODO handle x-error + for (var i=method.tags.length-1; i>=0; i--) { + if (method.tags[i].name === "event" || method.tags[i].name === "rpc-only") { + method.tags.splice(i, 1) + } + } + + method.tags = method.tags.filter(tag => (tag.name !== "event" && tag.name !== "rpc-only")) + } + } + else if (isEvent(method)) { + // store the subscriber name in the x-event extension + isEvent(method)['x-event'] = json.info.title + '.' + method.name + + // simplify name + method.name = method.name.charAt(2).toLowerCase() + method.name.substr(3) + // put the notification playload at the end of the params + method.params.push(method.result) + + // rename the event tag to notifier + isEvent(method).name = "notifier" + + // remove the result, since this is a notification + delete method.result + + method.examples.forEach(example => { + example.params.push(example.result) + delete example.result + }) + } + return method + }) + + json.methods = json.methods.filter(m => !m.tags.find(t => t.name === 'polymorphic-push')) + + // look for pass-through providers w/ same name as use method + json.methods.filter(m => json.methods.filter(x => x.name === m.name).length > 1) + .filter(m => m.tags.find(t => t.name === 'capabilities')['x-provides']) + .reverse() + .forEach(provider => { + const i = json.methods.indexOf(provider) + json.methods.splice(i, 1) + json.methods.find(m => m.name === provider.name).tags.find(t => t.name === 'capabilities')['x-provided-by'] = json.info.title+'.'+provider.name + console.dir(json.methods.find(m => m.name === provider.name)) + }) + +} + +export default run \ No newline at end of file diff --git a/src/validate/index.mjs b/src/validate/index.mjs index ca7d0712..3880cf00 100644 --- a/src/validate/index.mjs +++ b/src/validate/index.mjs @@ -18,7 +18,7 @@ import { readJson, readFiles, readDir } from "../shared/filesystem.mjs" import { addExternalMarkdown, addExternalSchemas, fireboltize } from "../shared/modules.mjs" -import { removeIgnoredAdditionalItems, replaceUri } from "../shared/json-schema.mjs" +import { namespaceRefs, removeIgnoredAdditionalItems, replaceUri } from "../shared/json-schema.mjs" import { validate, displayError, validatePasshtroughs } from "./validator/index.mjs" import { logHeader, logSuccess, logError } from "../shared/io.mjs" @@ -27,17 +27,27 @@ import addFormats from 'ajv-formats' import url from "url" import path from "path" import fetch from "node-fetch" +import { getCapability } from "../shared/methods.mjs" const __dirname = url.fileURLToPath(new URL('.', import.meta.url)) const run = async ({ input: input, + server: server, + client: client, schemas: schemas, transformations = false, + bidirectional = false, 'pass-throughs': passThroughs }) => { - logHeader(`Validating ${path.relative('.', input)} with${transformations ? '' : 'out'} Firebolt transformations.`) + if (input) { + logHeader(`Validating ${path.relative('.', input)} with${transformations ? '' : 'out'} Firebolt transformations.`) + } + else { + logHeader(`Validating ${path.relative('.', server)} with${transformations ? '' : 'out'} Firebolt transformations.`) + + } let invalidResults = 0 @@ -66,11 +76,18 @@ const run = async ({ delete sharedSchemas[path] }) - const moduleList = input ? await readDir(path.join(input), { recursive: true }) : [] - const modules = await readFiles(moduleList, path.join(input)) + const moduleList = input ? await readDir(path.join(input), { recursive: true }) : [server] + const modules = await readFiles(moduleList, input ? path.join(input) : '.') - const descriptionsList = transformations ? await readDir(path.join(input, '..', 'descriptions'), { recursive: true }) : [] - const markdown = await readFiles(descriptionsList, path.join(input, '..', 'descriptions')) + let descriptionsList, markdown + + try { + descriptionsList = transformations ? await readDir(path.join(input, '..', 'descriptions'), { recursive: true }) : [] + markdown = await readFiles(descriptionsList, path.join(input, '..', 'descriptions')) + } + catch (error) { + markdown = {} + } const jsonSchemaSpec = await (await fetch('https://meta.json-schema.tools')).json() @@ -103,12 +120,14 @@ const run = async ({ schemas: [ jsonSchemaSpec, openRpcSpec, - fireboltOpenRpcSpec, - ...Object.values(sharedSchemas) + fireboltOpenRpcSpec ] }) +// ...( transformations ? Object.values(sharedSchemas) : []) + addFormats(ajv) + // explicitly add our custom extensions so we can keep strict mode on (TODO: put these in a JSON config?) ajv.addVocabulary(['x-method', 'x-this-param', 'x-additional-params', 'x-schemas', 'components', 'x-property']) @@ -116,6 +135,9 @@ const run = async ({ const jsonschema = ajv.compile(jsonSchemaSpec) const openrpc = ajv.compile(openRpcSpec) + // add the shared schemas so that each shared schema knows about the others + ajv.addSchema(Object.values(sharedSchemas)) + // Validate all shared schemas sharedSchemas && Object.keys(sharedSchemas).forEach(key => { const json = sharedSchemas[key] @@ -161,13 +183,21 @@ const run = async ({ printResult(exampleResult, "JSON Schema") }) - // Validate all modules + // Do Firebolt Transformations Object.keys(modules).forEach(key => { let json = JSON.parse(modules[key]) if (transformations) { + + // put module name in front of each method + json.methods.filter(method => method.name.indexOf('.') === -1).forEach(method => { + method.name = json.info.title + '.' + method.name + }) + json.components && json.components.schemas && (json.components.schemas = Object.fromEntries(Object.entries(json.components.schemas).map( ([key, schema]) => ([json.info.title + '.' + key, schema]) ))) + namespaceRefs('', json.info.title, json) + // Do the firebolt API magic - json = fireboltize(json) + json = fireboltize(json, bidirectional) // pull in external markdown files for descriptions json = addExternalMarkdown(json, markdown) @@ -177,123 +207,129 @@ const run = async ({ json.components.schemas = json.components.schemas || {} // add externally referenced schemas that are in our shared schemas path - json = addExternalSchemas(json, sharedSchemas) +// json = addExternalSchemas(json, sharedSchemas) } - const exampleSpec = { - "$id": "https://meta.rdkcentral.com/firebolt/dynamic/" + (json.info.title) +"/examples", - "title": "FireboltOpenRPCExamples", - "definitions": { - "Document": { - "type": "object", - "properties": { - "methods": { - "type": "array", - "items": { - "allOf": json.methods.filter(m => m.result).map(method => ({ - "if": { - "type": "object", - "properties": { - "name": { - "const": method.name + ajv.addSchema(Object.values(json.components.schemas).filter(s => s.$id)) + + modules[key] = json + }) + + // Validate all modules examples + Object.keys(modules).forEach(key => { + const json = modules[key] + json.methods.forEach((method, index) => { + const exampleSpec = { + "$id": `${json.info.title}.method.${index}.examples`, + "title": `${method.name} Examples`, + "oneOf": [], + "components": json.components + } + + exampleSpec.oneOf.push({ + type: "object", + required: ["examples"], + properties: { + examples: { + type: "array", + items: { + type: "object", + properties: { + params: method.params.length ? { + "type": "array", + "items": { + "allOf": method.params.map(param => ({ + "if": { + "type": "object", + "properties": { + "name": { + "const": param.name + } + } + }, + "then": { + "type": "object", + "properties": { + "value": param.schema + } } - } + })) }, - "then": { - "type": "object", - "properties": { - "examples": { - "type": "array", - "items": { - "type": "object", - "properties": { - "result": { - "type": "object", - "properties": { - "value": method.result.schema - } - }, - "params": method.params.length ? { - "type": "array", - "items": { - "allOf": method.params.map(param => ({ - "if": { - "type": "object", - "properties": { - "name": { - "const": param.name - } - } - }, - "then": { - "type": "object", - "properties": { - "value": param.schema - } - } - })) - }, - "if": { - "type": "array" // always true, but avoids an empty allOf below - }, - "then": method.params.filter(p => p.required).length ? { - "allOf": method.params.filter(p => p.required).map(param => ({ - "contains": { - "type": "object", - "properties": { - "name": { - "const": param.name - } - } - } - })) - } : {} - } : {} + "if": { + "type": "array" // always true, but avoids an empty allOf below + }, + "then": method.params.filter(p => p.required).length ? { + "allOf": method.params.filter(p => p.required).map(param => ({ + "contains": { + "type": "object", + "properties": { + "name": { + "const": param.name } } } - } + })) + } : {} + } : {}, + result: method.result?.schema ? { + type: "object", + required: ["value"], + properties: { + value: method.result.schema } - })) + } : {} } } } } - }, - "x-schemas": json['x-schemas'], - "components": json.components - } + }) + + const examples = ajv.compile(exampleSpec) - exampleSpec.oneOf = [ - { - "$ref": "#/definitions/Document" + try { + const exampleResult = validate(method, { title: json.info.title, path: `/methods/${method.name}` }, ajv, examples) + + if (exampleResult.valid) { +// printResult(exampleResult, "Firebolt Examples") + } + else { + printResult(exampleResult, "Firebolt Examples") + + // if (!exampleResult.valid) { + // console.dir(exampleSpec, { depth: 100 }) + // } + } } - ] + catch (error) { + throw error + } + }) + + }) - const examples = ajv.compile(exampleSpec) + // Remove the shared schemas, because they're bundled into the OpenRPC docs + Object.values(sharedSchemas).map(schema => schema.$id).map(x => ajv.removeSchema(x)) + + // Validate all modules + Object.keys(modules).forEach(key => { + let json = modules[key] try { const openrpcResult = validate(json, {}, ajv, openrpc) const fireboltResult = validate(json, {}, ajv, firebolt) - const exampleResult = validate(json, {}, ajv, examples) - if (openrpcResult.valid && fireboltResult.valid && exampleResult.valid) { + if (openrpcResult.valid && fireboltResult.valid) { printResult(openrpcResult, "OpenRPC & Firebolt") } else { printResult(openrpcResult, "OpenRPC") printResult(fireboltResult, "Firebolt") - printResult(exampleResult, "Firebolt Examples") - - if (!exampleResult.valid) { -// console.dir(exampleSpec, { depth: 100 }) - } } - if (passThroughs) { + if (false && passThroughs) { const passthroughResult = validatePasshtroughs(json) printResult(passthroughResult, "Firebolt App pass-through") - } + } } catch (error) { throw error diff --git a/src/validate/validator/index.mjs b/src/validate/validator/index.mjs index 687aaada..d9ddc1b0 100644 --- a/src/validate/validator/index.mjs +++ b/src/validate/validator/index.mjs @@ -19,10 +19,11 @@ import groupBy from 'array.prototype.groupby' import util from 'util' import { getPayloadFromEvent } from '../../shared/modules.mjs' import { getPropertiesInSchema, getPropertySchema } from '../../shared/json-schema.mjs' +import { getCapability, getRole } from '../../shared/methods.mjs' -const addPrettyPath = (error, json) => { +const addPrettyPath = (error, json, info) => { const path = [] - const root = json.title || json.info.title + const root = json.title || json.info?.title || info?.title || `Unknown` let pointer = json error.instancePath.substr(1).split('/').forEach(x => { @@ -35,7 +36,9 @@ const addPrettyPath = (error, json) => { pointer = pointer[x] } }) - error.prettyPath = '/' + path.join('/') + + error.instancePath = (info.path ? info.path : '') + error.instancePath + error.prettyPath = (info.path ? info.path : '') + '/' + path.join('/') error.document = root error.node = pointer return error @@ -127,6 +130,11 @@ export const displayError = (error) => { console.error(`\t\x1b[2m${pad('document:')}\x1b[0m\x1b[38;5;208m${error.document}\x1b[0m \x1b[2m(${errorFileType})\x1b[2m\x1b[0m`) console.error(`\t\x1b[2m${pad('source:')}\x1b[0m\x1b[38;5;208m${error.source}\x1b[0m`) + if (error.capability) { + console.error(`\t\x1b[2m${pad('capability:')}\x1b[0m\x1b[38;5;208m${error.capability}\x1b[0m`) + console.error(`\t\x1b[2m${pad('role:')}\x1b[0m\x1b[38;5;208m${error.role}\x1b[0m`) + } + if (error.value) { console.error(`\t\x1b[2m${pad('value:')}\x1b[0m\n`) console.dir(error.value, {depth: null, colors: true})// + JSON.stringify(example, null, ' ') + '\n') @@ -139,9 +147,8 @@ export const displayError = (error) => { console.error() } -export const validate = (json = {}, schemas = {}, ajv, validator, additionalPackages = []) => { +export const validate = (json = {}, info = {}, ajv, validator, additionalPackages = []) => { let valid = validator(json) - let root = json.title || json.info.title const errors = [] if (valid) { @@ -150,7 +157,7 @@ export const validate = (json = {}, schemas = {}, ajv, validator, additionalPack const additionalValid = addtnlValidator(json) if (!additionalValid) { valid = false - addtnlValidator.errors.forEach(error => addPrettyPath(error, json)) + addtnlValidator.errors.forEach(error => addPrettyPath(error, json, info)) addtnlValidator.errors.forEach(error => error.source = 'Firebolt OpenRPC') addtnlValidator.errors.forEach(error => addFailingMethodSchema(error, json, addtnlValidator.schema)) errors.push(...pruneErrors(addtnlValidator.errors)) @@ -159,13 +166,21 @@ export const validate = (json = {}, schemas = {}, ajv, validator, additionalPack } } else { - validator.errors.forEach(error => addPrettyPath(error, json)) + validator.errors.forEach(error => addPrettyPath(error, json, info)) validator.errors.forEach(error => error.source = 'OpenRPC') + json.methods && validator.errors.forEach(error => { + if (error.instancePath.startsWith('/methods/')) { + const method = json.methods[parseInt(error.instancePath.split('/')[2])] + error.capability = getCapability(method) + error.role = getRole(method) + } + }) + errors.push(...pruneErrors(validator.errors)) } - return { valid: valid, title: json.title || json.info.title, errors: errors } + return { valid: valid, title: json.title || info?.title || json.info?.title, errors: errors } } const schemasMatch = (a, b) => { @@ -219,12 +234,12 @@ export const validatePasshtroughs = (json) => { examples2 = provider.examples.map(e => e.params[e.params.length-1].value) } else { - destination = method.result.schema - examples1 = method.examples.map(e => e.result.value) - source = JSON.parse(JSON.stringify(provider.tags.find(t => t['x-response'])['x-response'])) - sourceName = provider.tags.find(t => t['x-response'])['x-response-name'] - examples2 = provider.tags.find(t => t['x-response'])['x-response'].examples - delete source.examples + // destination = method.result.schema + // examples1 = method.examples.map(e => e.result.value) + // source = JSON.parse(JSON.stringify(provider.tags.find(t => t['x-response'])['x-response'])) + // sourceName = provider.tags.find(t => t['x-response'])['x-response-name'] + // examples2 = provider.tags.find(t => t['x-response'])['x-response'].examples + // delete source.examples } if (!schemasMatch(source, destination)) { diff --git a/test/TransportHarness.js b/test/TransportHarness.js index 88ab891a..b82f2903 100644 --- a/test/TransportHarness.js +++ b/test/TransportHarness.js @@ -36,8 +36,7 @@ let receiver export const transport = { - send: function(message) { - const json = JSON.parse(message) + send: function(json) { sendListener && sendListener(json) }, receive: function(callback) { @@ -54,9 +53,12 @@ result: result } receiver && receiver(JSON.stringify(response)) + }, + request: function(json) { + receiver && receiver(JSON.stringify(json)) } } - win.__firebolt.setTransportLayer(transport) + win.__firebolt.transport = transport export default transport \ No newline at end of file diff --git a/test/openrpc/advanced.json b/test/openrpc/advanced.json index 74d1b12d..84275fd6 100644 --- a/test/openrpc/advanced.json +++ b/test/openrpc/advanced.json @@ -1,302 +1,309 @@ { - "openrpc": "1.2.4", - "info": { - "title": "Advanced", - "description": "Module for testing advanced firebolt-openrpc features", - "version": "0.0.0" - }, - "methods": [ - { - "name": "onEventWithContext", - "tags": [ - { - "name": "event" - }, - { - "name": "capabilities", - "x-uses": ["xrn:firebolt:capability:test:test"] - } - ], - "summary": "An event with one context parameter.", - "params": [ - { - "name": "appId", - "required": true, - "schema": { - "type": "string" - } - } - ], - "result": { - "name": "result", - "schema": { - "type": "object", - "properties": { - "foo": { - "type": "string" - } - } - } - }, - "examples": [ - { - "name": "Default Example", - "params": [ - { - "name": "appId", - "value": "hulu" - } - ], - "result": { - "name": "result", - "value": { - "foo": "bar" - } - } - } - ] - }, - { - "name": "onEventWithTwoContext", - "tags": [ - { - "name": "event" - }, - { - "name": "capabilities", - "x-uses": ["xrn:firebolt:capability:test:test"] - } - ], - "summary": "An event with two context parameters.", - "params": [ - { - "name": "appId", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "state", - "required": true, - "schema": { - "type": "string" - } - } - ], - "result": { - "name": "result", - "schema": { - "type": "object", - "properties": { - "foo": { - "type": "string" - } - } - } - }, - "examples": [ - { - "name": "Default Example", - "params": [ - { - "name": "appId", - "value": "hulu" - }, - { - "name": "state", - "value": "inactive" - } - ], - "result": { - "name": "result", - "value": { - "foo": "bar" - } - } - } - ] - }, - { - "name": "propertyWithContext", - "summary":"", - "tags": [ - { - "name": "property" - }, - { - "name": "capabilities", - "x-uses": [ - "xrn:firebolt:capability:test:test" - ] - } - ], - "params": [ - { - "name": "appId", - "required": true, - "schema": { - "type": "string" - } - } - ], - "result": { - "name": "value", - "schema": { - "type": "boolean" - } - }, - "examples": [ - { - "name": "Get the property for hulu", - "params": [ - { - "name": "appId", - "value": "hulu" - } - ], - "result": { - "name": "value", - "value": false - } - }, - { - "name": "Get the property for peacock", - "params": [ - { - "name": "appId", - "value": "peacock" - } - ], - "result": { - "name": "value", - "value": true - } - } - ] - }, - { - "name": "list", - "tags": [ - { - "name": "temporal-set" - }, - { - "name": "capabilities", - "x-uses": ["xrn:firebolt:capability:test:test"] - } - ], - "summary": "A temporal set method that lists Advanced objects.", - "params": [ - - ], - "result": { - "name": "items", - "schema": { - "type": "array", - "items": { - "title": "Item", - "$ref": "#/components/schemas/Advanced" - } - } - }, - "examples": [ - { - "name": "Default Example", - "params": [ - ], - "result": { - "name": "items", - "value": [ - { - "aString": "Here's a string", - "aNumber": 123 - } - ] - } - } - ] - }, - { - "name": "action", - "summary": "A method that takes an Advanced object.", - "tags": [ - { - "name": "capabilities", - "x-uses": ["xrn:firebolt:capability:test:test"] - } - ], - "description": "A method for testing advanced method generation.", - "params": [ - { - "name": "advanced", - "required": true, - "schema": { - "$ref": "#/components/schemas/Advanced" - }, - "summary": "A test parameter." - } - ], - "result": { - "name": "result", - "summary": "A result for testing basic method generation.", - "schema": { - "type": "object", - "required": [ - "foo" - ], - "properties": { - "foo": { - "type": "string", - "description": "A required field in the result." - }, - "bar": { - "type": "number", - "description": "An optional field in the result." - } - }, - "additionalProperties": false - } - }, - "examples": [ - { - "name": "Default Example", - "params": [ - { - "name": "advanced", - "value": { - "aString": "Here's a string", - "aNumber": 123 - } - } - ], - "result": { - "name": "Default Result", - "value": { - "foo": "here's foo", - "bar": 1 - } - } - } - ] - } - ], - "components": { - "schemas": { - "Advanced": { - "title": "Advanced", - "type": "object", - "properties": { - "aString": { - "type": "string" - }, - "aMethod": { - "type": "null", - "x-method": "Advanced.action", - "x-this-param": "advanced" - } - } - } - } - } + "openrpc": "1.2.4", + "info": { + "title": "Advanced", + "description": "Module for testing advanced firebolt-openrpc features", + "version": "0.0.0" + }, + "methods": [ + { + "name": "eventWithContext", + "tags": [ + { + "name": "notifier", + "x-event": "Advanced.onEventWithContext" + }, + { + "name": "capabilities", + "x-uses": [ + "xrn:firebolt:capability:test:test" + ] + } + ], + "summary": "An event with one context parameter.", + "params": [ + { + "name": "appId", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "result", + "schema": { + "type": "object", + "properties": { + "foo": { + "type": "string" + } + } + } + } + ], + "examples": [ + { + "name": "Default Example", + "params": [ + { + "name": "appId", + "value": "hulu" + }, + { + "name": "result", + "value": { + "foo": "bar" + } + } + ] + } + ] + }, + { + "name": "eventWithTwoContext", + "tags": [ + { + "name": "notifier", + "x-event": "Advanced.onEventWithTwoContext" + }, + { + "name": "capabilities", + "x-uses": [ + "xrn:firebolt:capability:test:test" + ] + } + ], + "summary": "An event with two context parameters.", + "params": [ + { + "name": "appId", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "state", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "result", + "schema": { + "type": "object", + "properties": { + "foo": { + "type": "string" + } + } + } + } + ], + "examples": [ + { + "name": "Default Example", + "params": [ + { + "name": "appId", + "value": "hulu" + }, + { + "name": "state", + "value": "inactive" + }, + { + "name": "result", + "value": { + "foo": "bar" + } + } + ] + } + ] + }, + { + "name": "propertyWithContext", + "summary": "", + "tags": [ + { + "name": "property" + }, + { + "name": "capabilities", + "x-uses": [ + "xrn:firebolt:capability:test:test" + ] + } + ], + "params": [ + { + "name": "appId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "result": { + "name": "value", + "schema": { + "type": "boolean" + } + }, + "examples": [ + { + "name": "Get the property for hulu", + "params": [ + { + "name": "appId", + "value": "hulu" + } + ], + "result": { + "name": "value", + "value": false + } + }, + { + "name": "Get the property for peacock", + "params": [ + { + "name": "appId", + "value": "peacock" + } + ], + "result": { + "name": "value", + "value": true + } + } + ] + }, + { + "name": "list", + "tags": [ + { + "name": "temporal-set" + }, + { + "name": "capabilities", + "x-uses": [ + "xrn:firebolt:capability:test:test" + ] + } + ], + "summary": "A temporal set method that lists Advanced objects.", + "params": [], + "result": { + "name": "items", + "schema": { + "type": "array", + "items": { + "title": "Item", + "$ref": "#/components/schemas/Advanced" + } + } + }, + "examples": [ + { + "name": "Default Example", + "params": [], + "result": { + "name": "items", + "value": [ + { + "aString": "Here's a string", + "aNumber": 123 + } + ] + } + } + ] + }, + { + "name": "action", + "summary": "A method that takes an Advanced object.", + "tags": [ + { + "name": "capabilities", + "x-uses": [ + "xrn:firebolt:capability:test:test" + ] + } + ], + "description": "A method for testing advanced method generation.", + "params": [ + { + "name": "advanced", + "required": true, + "schema": { + "$ref": "#/components/schemas/Advanced" + }, + "summary": "A test parameter." + } + ], + "result": { + "name": "result", + "summary": "A result for testing basic method generation.", + "schema": { + "type": "object", + "required": [ + "foo" + ], + "properties": { + "foo": { + "type": "string", + "description": "A required field in the result." + }, + "bar": { + "type": "number", + "description": "An optional field in the result." + } + }, + "additionalProperties": false + } + }, + "examples": [ + { + "name": "Default Example", + "params": [ + { + "name": "advanced", + "value": { + "aString": "Here's a string", + "aNumber": 123 + } + } + ], + "result": { + "name": "Default Result", + "value": { + "foo": "here's foo", + "bar": 1 + } + } + } + ] + } + ], + "components": { + "schemas": { + "Advanced": { + "title": "Advanced", + "type": "object", + "properties": { + "aString": { + "type": "string" + }, + "aMethod": { + "type": "null", + "x-method": "Advanced.action", + "x-this-param": "advanced" + } + } + } + } + } } \ No newline at end of file diff --git a/test/openrpc/provider.json b/test/openrpc/provider.json index d4851605..d27709ca 100644 --- a/test/openrpc/provider.json +++ b/test/openrpc/provider.json @@ -1,281 +1,341 @@ { - "openrpc": "1.2.4", - "info": { - "title": "Provider", - "description": "A module for testing Provider interfaces.", - "version": "0.0.0" - }, - "methods": [ - { - "name": "onRequestSimpleMethod", - "tags": [ - { - "name": "capabilities", - "x-provides": "xrn:firebolt:capability:test:simple", - "x-allow-focus": false - }, - { - "name": "event", - "x-response": { - "type": "string", - "examples": [ - "a value!" - ] - } - } - ], - "summary": "Dispatches a request for the simple method to the simple provider.", - "params": [ - ], - "result": { - "name": "result", - "schema": { - "$ref": "#/components/schemas/SimpleProviderRequest" - } - }, - "examples": [ - { - "name": "Get simple", - "params": [], - "result": { - "name": "result", - "value": { - "correlationId": "123", - "parameters": null - } - } - } - ] - }, - { - "name": "onRequestHandshakeMethod", - "tags": [ - { - "name": "capabilities", - "x-provides": "xrn:firebolt:capability:test:handshake", - "x-allow-focus": true - }, - { - "name": "event", - "x-response": { - "type": "string", - "examples": [ - "a value for handshake!" - ] - } - } - ], - "summary": "Dispatches a request for the simple method, with a handshake, to the simple provider.", - "params": [ - ], - "result": { - "name": "result", - "schema": { - "$ref": "#/components/schemas/SimpleProviderRequest" - } - }, - "examples": [ - { - "name": "Get handshake", - "params": [], - "result": { - "name": "result", - "value": { - "correlationId": "123", - "parameters": null - } - } - } - ] - }, - { - "name": "onRequestMultiMethodOne", - "tags": [ - { - "name": "capabilities", - "x-provides": "xrn:firebolt:capability:test:multi", - "x-allow-focus": false - }, - { - "name": "event", - "x-response": { - "type": "string", - "examples": [ - "a first value!" - ] - } - } - ], - "summary": "Dispatches a request for the simple method one to the simple provider.", - "params": [ - ], - "result": { - "name": "result", - "schema": { - "$ref": "#/components/schemas/SimpleProviderRequest" - } - }, - "examples": [ - { - "name": "Get simple", - "params": [], - "result": { - "name": "result", - "value": { - "correlationId": "123", - "parameters": null - } - } - } - ] - }, - { - "name": "onRequestMultiMethodTwo", - "tags": [ - { - "name": "capabilities", - "x-provides": "xrn:firebolt:capability:test:multi", - "x-allow-focus": false - }, - { - "name": "event", - "x-response": { - "type": "string", - "examples": [ - "a second value!" - ] - } - } - ], - "summary": "Dispatches a request for the simple method two to the simple provider.", - "params": [ - ], - "result": { - "name": "result", - "schema": { - "$ref": "#/components/schemas/SimpleProviderRequest" - } - }, - "examples": [ - { - "name": "Get simple", - "params": [], - "result": { - "name": "result", - "value": { - "correlationId": "456", - "parameters": null - } - } - } - ] - }, - { - "name": "onRequestNoResponseMethod", - "tags": [ - { - "name": "capabilities", - "x-provides": "xrn:firebolt:capability:test:noresponse", - "x-allow-focus": false - }, - { - "name": "event" - } - ], - "summary": "Dispatches a request for the simple method to the simple provider.", - "params": [ - ], - "result": { - "name": "result", - "schema": { - "$ref": "#/components/schemas/NoResponseProviderRequest" - } - }, - "examples": [ - { - "name": "Get simple", - "params": [], - "result": { - "name": "result", - "value": { - "correlationId": "123", - "parameters": { - "first": true, - "second": [ - 1 - ], - "last": "foo" - } - } - } - } - ] - } - ], - "components": { - "schemas": { - "SimpleProviderRequest": { - "title": "SimpleProviderRequest", - "allOf": [ - { - "$ref": "https://meta.comcast.com/firebolt/types#/definitions/ProviderRequest" - }, - { - "type": "object", - "properties": { - "parameters": { - "title": "SimpleProviderParameters", - "const": null - } - } - } - ], - "examples": [ - { - "correlationId": "abc" - } - ] - }, - "NoResponseProviderRequest": { - "title": "NoResponseProviderRequest", - "allOf": [ - { - "$ref": "https://meta.comcast.com/firebolt/types#/definitions/ProviderRequest" - }, - { - "type": "object", - "properties": { - "parameters": { - "title": "NoResponseParametes", - "type": "object", - "properties": { - "first": { - "type": "boolean" - }, - "second": { - "type": "array", - "items": { - "type": "integer" - } - }, - "last": { - "type": "string", - "enum": [ - "foo", - "bar" - ] - } - } - } - } - } - ], - "examples": [ - { - "correlationId": "abc" - } - ] - } - } - } + "openrpc": "1.2.4", + "info": { + "title": "Provider", + "description": "A module for testing Provider interfaces.", + "version": "0.0.0" + }, + "methods": [ + { + "name": "provideSimple", + "tags": [ + { + "name": "registration", + "x-interface": "Simple" + }, + { + "name": "capabilities", + "x-provides": "xrn:firebolt:capability:test:simple" + } + ], + "params": [ + { + "name": "enabled", + "schema": { + "type": "boolean" + } + } + ], + "result": { + "name": "result", + "schema": { + "const": null + } + }, + "examples": [ + { + "name": "Default example", + "params": [ + { + "name": "enabled", + "value": true + } + ], + "result": { + "name": "result", + "value": null + } + } + ] + }, + { + "name": "provideMultipleMethods", + "tags": [ + { + "name": "registration", + "x-interface": "MultipleMethods" + }, + { + "name": "capabilities", + "x-provides": "xrn:firebolt:capability:test:multi" + } + ], + "params": [ + { + "name": "enabled", + "schema": { + "type": "boolean" + } + } + ], + "result": { + "name": "result", + "schema": { + "const": null + } + }, + "examples": [ + { + "name": "Default example", + "params": [ + { + "name": "enabled", + "value": true + } + ], + "result": { + "name": "result", + "value": null + } + } + ] + }, + { + "name": "provideNoResponseMethod", + "tags": [ + { + "name": "registration", + "x-interface": "NoResponseMethod" + }, + { + "name": "capabilities", + "x-provides": "xrn:firebolt:capability:test:noresponse" + } + ], + "params": [ + { + "name": "enabled", + "schema": { + "type": "boolean" + } + } + ], + "result": { + "name": "result", + "schema": { + "const": null + } + }, + "examples": [ + { + "name": "Default example", + "params": [ + { + "name": "enabled", + "value": true + } + ], + "result": { + "name": "result", + "value": null + } + } + ] + }, + { + "name": "Simple.simpleMethod", + "tags": [ + { + "name": "capabilities", + "x-provides": "xrn:firebolt:capability:test:simple", + "x-allow-focus": false + } + ], + "summary": "Dispatches a request for the simple method to the simple provider.", + "params": [], + "result": { + "name": "result", + "schema": { + "type": "string" + } + }, + "examples": [ + { + "name": "Get simple", + "params": [], + "result": { + "name": "result", + "value": "a value!" + } + } + ] + }, + { + "name": "MultipleMethods.multiMethodOne", + "tags": [ + { + "name": "capabilities", + "x-provides": "xrn:firebolt:capability:test:multi", + "x-allow-focus": false + } + ], + "summary": "Dispatches a request for the simple method one to the simple provider.", + "params": [], + "result": { + "name": "result", + "schema": { + "type": "string" + } + }, + "examples": [ + { + "name": "Get simple", + "params": [], + "result": { + "name": "result", + "value": "a first value!" + } + } + ] + }, + { + "name": "MultipleMethods.multiMethodTwo", + "tags": [ + { + "name": "capabilities", + "x-provides": "xrn:firebolt:capability:test:multi", + "x-allow-focus": false + } + ], + "summary": "Dispatches a request for the simple method two to the simple provider.", + "params": [], + "result": { + "name": "result", + "schema": { + "type": "string" + } + }, + "examples": [ + { + "name": "Get simple", + "params": [], + "result": { + "name": "result", + "value": "a second value!" + } + } + ] + }, + { + "name": "NoResponseMethod.noResponseMethod", + "tags": [ + { + "name": "capabilities", + "x-provides": "xrn:firebolt:capability:test:noresponse", + "x-allow-focus": false + } + ], + "summary": "Dispatches a request for the simple method to the simple provider.", + "params": [ + { + "name": "first", + "required": false, + "schema": { + "type": "boolean" + } + }, + { + "name": "second", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "integer" + } + } + }, + { + "name": "last", + "required": false, + "schema": { + "type": "string", + "enum": [ + "foo", + "bar" + ] + } + } + ], + "result": { + "name": "result", + "schema": { + "type": "null" + } + }, + "examples": [ + { + "name": "Get simple", + "params": [ + { + "name": "first", + "value": true + }, + { + "name": "second", + "value": [ + 1 + ] + }, + { + "name": "last", + "value": "foo" + } + ], + "result": { + "name": "result", + "value": null + } + } + ] + } + ], + "components": { + "schemas": { + "SimpleProviderRequest": { + "title": "SimpleProviderRequest", + "allOf": [ + { + "$ref": "https://meta.rdkcentral.com/firebolt/schemas/types#/definitions/ProviderRequest" + }, + { + "type": "object", + "properties": { + "parameters": { + "title": "SimpleProviderParameters", + "const": null + } + } + } + ], + "examples": [ + { + "correlationId": "abc" + } + ] + }, + "NoResponseProviderRequest": { + "title": "NoResponseProviderRequest", + "allOf": [ + { + "$ref": "https://meta.rdkcentral.com/firebolt/schemas/types#/definitions/ProviderRequest" + }, + { + "type": "object", + "properties": { + "parameters": { + "title": "NoResponseParametes", + "type": "object", + "properties": {} + } + } + } + ], + "examples": [ + { + "correlationId": "abc" + } + ] + } + } + } } \ No newline at end of file diff --git a/test/openrpc/simple.json b/test/openrpc/simple.json index 7d009546..b595eaae 100644 --- a/test/openrpc/simple.json +++ b/test/openrpc/simple.json @@ -1,374 +1,386 @@ { - "openrpc": "1.2.4", - "info": { - "title": "Simple", - "description": "Simple module for testing firebolt-openrpc", - "version": "0.0.0" - }, - "methods": [ - { - "name": "method", - "summary": "A method.", - "tags": [ - { - "name": "capabilities", - "x-uses": ["xrn:firebolt:capability:test:test"] - } - ], - "description": "A method for testing basic method generation.", - "params": [ - { - "name": "parameter", - "required": true, - "schema": { - "type": "boolean" - }, - "summary": "A test parameter." - } - ], - "result": { - "name": "result", - "summary": "A result for testing basic method generation.", - "schema": { - "type": "object", - "required": [ - "foo" - ], - "properties": { - "foo": { - "type": "string", - "description": "A required field in the result." - }, - "bar": { - "type": "number", - "description": "An optional field in the result." - } - }, - "additionalProperties": false - } - }, - "examples": [ - { - "name": "Default Example", - "params": [ - { - "name": "parameter", - "value": true - } - ], - "result": { - "name": "Default Result", - "value": { - "foo": "here's foo" - } - } - } - ] - }, - { - "name": "property", - "summary": "A property.", - "tags": [ - { - "name": "property" - }, - { - "name": "capabilities", - "x-uses": [ - "xrn:firebolt:capability:test:test" - ] - } - ], - "description": "A property for testing basic property generation.", - "params": [ - ], - "result": { - "name": "result", - "summary": "A result for testing basic method generation.", - "schema": { - "type": "object", - "required": [ - "foo" - ], - "properties": { - "foo": { - "type": ["string", "null"], - "description": "A required field in the result." - }, - "bar": { - "type": "number", - "description": "An optional field in the result." - } - }, - "additionalProperties": false - } - }, - "examples": [ - { - "name": "Default Example", - "params": [ - { - "name": "parameter", - "value": true - } - ], - "result": { - "name": "Default Result", - "value": { - "foo": "here's foo" - } - } - }, - { - "name": "Another Example", - "params": [ - { - "name": "parameter", - "value": false - } - ], - "result": { - "name": "Another Result", - "value": { - "foo": "here's bar" - } - } - }, - { - "name": "Example to set null value", - "params": [ - { - "name": "parameter", - "value": null - } - ], - "result": { - "name": "Another Result", - "value": { - "foo": null - } - } - } - ] - }, - { - "name": "methodWithMarkdownDescription", - "summary": "A method that pulls it's description from an external markdown file.", - "description": { - "$ref": "file:../descriptions/modules/Simple/methodWithMarkdownDescription.md" - }, - "tags": [ - { - "name": "capabilities", - "x-uses": ["xrn:firebolt:capability:test:test"] - } - ], - "params": [ - { - "name": "parameter", - "required": true, - "schema": { - "type": "boolean" - }, - "summary": "A test parameter." - } - ], - "result": { - "name": "result", - "summary": "A result for testing basic method generation.", - "schema": { - "type": "object", - "required": [ - "foo" - ], - "properties": { - "foo": { - "type": "string", - "description": "A required field in the result." - }, - "bar": { - "type": "number", - "description": "An optional field in the result." - } - }, - "additionalProperties": false - } - }, - "examples": [ - { - "name": "Default Example", - "params": [ - { - "name": "parameter", - "value": true - } - ], - "result": { - "name": "Default Result", - "value": { - "foo": "here's foo" - } - } - } - ] - }, - { - "name": "methodWithSchema", - "summary": "A method using a schema.", - "description": "A method for testing schema-dependent method generation.", - "tags": [ - { - "name": "capabilities", - "x-uses": ["xrn:firebolt:capability:test:test"] - } - ], - "params": [ - { - "name": "title", - "required": true, - "schema": { - "$ref": "https://meta.comcast.com/firebolt/types#/definitions/LocalizedString" - }, - "summary": "A localized string test parameter." - } - ], - "result": { - "name": "result", - "summary": "A result for testing basic method generation.", - "schema": { - "$ref": "https://meta.comcast.com/firebolt/types#/definitions/LocalizedString" - } - }, - "examples": [ - { - "name": "Default Example", - "params": [ - { - "name": "title", - "value": "test" - } - ], - "result": { - "name": "Default Result", - "value": { - "foo": "here's foo" - } - } - } - ] - }, - { - "name": "methodWithMethodAttribute", - "summary": "A method using a method-attribute transform.", - "description": "A method for testing sub-method generation.", - "tags": [ - { - "name": "capabilities", - "x-uses": ["xrn:firebolt:capability:test:test"] - } - ], - "params": [ - { - "name": "title", - "required": true, - "schema": { - "$ref": "https://meta.comcast.com/firebolt/types#/definitions/LocalizedString" - }, - "summary": "A localized string test parameter." - } - ], - "result": { - "name": "result", - "summary": "A result for testing basic method generation.", - "schema": { - "type": "object", - "properties": { - "aString": { - "type": "string" - }, - "aMethod": { - "type": "string", - "x-method": "Simple.method", - "x-this-param": "accessory" - } - } - } - }, - "examples": [ - { - "name": "Default Example", - "params": [ - { - "name": "title", - "value": "test" - } - ], - "result": { - "name": "Default Result", - "value": { - "foo": "here's foo" - } - } - } - ] - }, - { - "name": "methodWithMultipleParams", - "summary": "A method that takes two parameters", - "description": "A method that takes two parameters", - "tags": [ - { - "name": "capabilities", - "x-uses": ["xrn:firebolt:capability:test:test"] - } - ], - "params": [ - { - "name": "id", - "required": true, - "schema": { - "type": "number" - }, - "summary": "A test number" - }, - { - "name": "title", - "required": true, - "schema": { - "type": "string" - }, - "summary": "A string test parameter." - } - ], - "result": { - "name": "result", - "summary": "A result for testing basic method generation.", - "schema": { - "type": "null" - } - }, - "examples": [ - { - "name": "Default Example", - "params": [ - { - "name": "id", - "value": 42 - }, - { - "name": "title", - "value": "test" - } - ], - "result": { - "name": "Default Result", - "value": null - } - } - ] - } - ], - "components": { - "schemas": {} - } + "openrpc": "1.2.4", + "info": { + "title": "Simple", + "description": "Simple module for testing firebolt-openrpc", + "version": "0.0.0" + }, + "methods": [ + { + "name": "method", + "summary": "A method.", + "tags": [ + { + "name": "capabilities", + "x-uses": [ + "xrn:firebolt:capability:test:test" + ] + } + ], + "description": "A method for testing basic method generation.", + "params": [ + { + "name": "parameter", + "required": true, + "schema": { + "type": "boolean" + }, + "summary": "A test parameter." + } + ], + "result": { + "name": "result", + "summary": "A result for testing basic method generation.", + "schema": { + "type": "object", + "required": [ + "foo" + ], + "properties": { + "foo": { + "type": "string", + "description": "A required field in the result." + }, + "bar": { + "type": "number", + "description": "An optional field in the result." + } + }, + "additionalProperties": false + } + }, + "examples": [ + { + "name": "Default Example", + "params": [ + { + "name": "parameter", + "value": true + } + ], + "result": { + "name": "Default Result", + "value": { + "foo": "here's foo" + } + } + } + ] + }, + { + "name": "property", + "summary": "A property.", + "tags": [ + { + "name": "property" + }, + { + "name": "capabilities", + "x-uses": [ + "xrn:firebolt:capability:test:test" + ] + } + ], + "description": "A property for testing basic property generation.", + "params": [], + "result": { + "name": "result", + "summary": "A result for testing basic method generation.", + "schema": { + "type": "object", + "required": [ + "foo" + ], + "properties": { + "foo": { + "type": [ + "string", + "null" + ], + "description": "A required field in the result." + }, + "bar": { + "type": "number", + "description": "An optional field in the result." + } + }, + "additionalProperties": false + } + }, + "examples": [ + { + "name": "Default Example", + "params": [ + { + "name": "parameter", + "value": true + } + ], + "result": { + "name": "Default Result", + "value": { + "foo": "here's foo" + } + } + }, + { + "name": "Another Example", + "params": [ + { + "name": "parameter", + "value": false + } + ], + "result": { + "name": "Another Result", + "value": { + "foo": "here's bar" + } + } + }, + { + "name": "Example to set null value", + "params": [ + { + "name": "parameter", + "value": null + } + ], + "result": { + "name": "Another Result", + "value": { + "foo": null + } + } + } + ] + }, + { + "name": "methodWithMarkdownDescription", + "summary": "A method that pulls it's description from an external markdown file.", + "description": { + "$ref": "file:../descriptions/modules/Simple/methodWithMarkdownDescription.md" + }, + "tags": [ + { + "name": "capabilities", + "x-uses": [ + "xrn:firebolt:capability:test:test" + ] + } + ], + "params": [ + { + "name": "parameter", + "required": true, + "schema": { + "type": "boolean" + }, + "summary": "A test parameter." + } + ], + "result": { + "name": "result", + "summary": "A result for testing basic method generation.", + "schema": { + "type": "object", + "required": [ + "foo" + ], + "properties": { + "foo": { + "type": "string", + "description": "A required field in the result." + }, + "bar": { + "type": "number", + "description": "An optional field in the result." + } + }, + "additionalProperties": false + } + }, + "examples": [ + { + "name": "Default Example", + "params": [ + { + "name": "parameter", + "value": true + } + ], + "result": { + "name": "Default Result", + "value": { + "foo": "here's foo" + } + } + } + ] + }, + { + "name": "methodWithSchema", + "summary": "A method using a schema.", + "description": "A method for testing schema-dependent method generation.", + "tags": [ + { + "name": "capabilities", + "x-uses": [ + "xrn:firebolt:capability:test:test" + ] + } + ], + "params": [ + { + "name": "title", + "required": true, + "schema": { + "$ref": "https://meta.rdkcentral.com/firebolt/schemas/types#/definitions/LocalizedString" + }, + "summary": "A localized string test parameter." + } + ], + "result": { + "name": "result", + "summary": "A result for testing basic method generation.", + "schema": { + "$ref": "https://meta.rdkcentral.com/firebolt/schemas/types#/definitions/LocalizedString" + } + }, + "examples": [ + { + "name": "Default Example", + "params": [ + { + "name": "title", + "value": "test" + } + ], + "result": { + "name": "Default Result", + "value": { + "foo": "here's foo" + } + } + } + ] + }, + { + "name": "methodWithMethodAttribute", + "summary": "A method using a method-attribute transform.", + "description": "A method for testing sub-method generation.", + "tags": [ + { + "name": "capabilities", + "x-uses": [ + "xrn:firebolt:capability:test:test" + ] + } + ], + "params": [ + { + "name": "title", + "required": true, + "schema": { + "$ref": "https://meta.rdkcentral.com/firebolt/schemas/types#/definitions/LocalizedString" + }, + "summary": "A localized string test parameter." + } + ], + "result": { + "name": "result", + "summary": "A result for testing basic method generation.", + "schema": { + "type": "object", + "properties": { + "aString": { + "type": "string" + }, + "aMethod": { + "type": "string", + "x-method": "Simple.method", + "x-this-param": "accessory" + } + } + } + }, + "examples": [ + { + "name": "Default Example", + "params": [ + { + "name": "title", + "value": "test" + } + ], + "result": { + "name": "Default Result", + "value": { + "foo": "here's foo" + } + } + } + ] + }, + { + "name": "methodWithMultipleParams", + "summary": "A method that takes two parameters", + "description": "A method that takes two parameters", + "tags": [ + { + "name": "capabilities", + "x-uses": [ + "xrn:firebolt:capability:test:test" + ] + } + ], + "params": [ + { + "name": "id", + "required": true, + "schema": { + "type": "number" + }, + "summary": "A test number" + }, + { + "name": "title", + "required": true, + "schema": { + "type": "string" + }, + "summary": "A string test parameter." + } + ], + "result": { + "name": "result", + "summary": "A result for testing basic method generation.", + "schema": { + "type": "null" + } + }, + "examples": [ + { + "name": "Default Example", + "params": [ + { + "name": "id", + "value": 42 + }, + { + "name": "title", + "value": "test" + } + ], + "result": { + "name": "Default Result", + "value": null + } + } + ] + } + ], + "components": { + "schemas": {} + } } \ No newline at end of file diff --git a/test/schemas/types.json b/test/schemas/types.json index c1ab4031..fdf80f5a 100644 --- a/test/schemas/types.json +++ b/test/schemas/types.json @@ -1,5 +1,5 @@ { - "$id": "https://meta.comcast.com/firebolt/types", + "$id": "https://meta.rdkcentral.com/firebolt/schemas/types", "title": "Types", "anyOf": [ { diff --git a/test/sharedSchemas/types.json b/test/sharedSchemas/types.json index 5399002d..1b52068b 100644 --- a/test/sharedSchemas/types.json +++ b/test/sharedSchemas/types.json @@ -1,5 +1,5 @@ { - "$id": "https://meta.comcast.com/firebolt/types", + "$id": "https://meta.rdkcentral.com/firebolt/schemas/types", "title": "Types", "anyOf": [ { diff --git a/test/suite/method-as-attribute.test.js b/test/suite/method-as-attribute.test.js index 2638180f..203a5d0d 100644 --- a/test/suite/method-as-attribute.test.js +++ b/test/suite/method-as-attribute.test.js @@ -16,10 +16,12 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Simple, Advanced } from '../../build/sdk/javascript/src/sdk.mjs' +import { Simple, Advanced, Settings } from '../../build/sdk/javascript/src/sdk.mjs' import Setup from '../Setup' import { expect } from '@jest/globals'; +Settings.setLogLevel('DEBUG') + test('Method as attribute', () => { return Simple.methodWithMethodAttribute('test').then( result => { expect(typeof result.aMethod).toBe('function') @@ -33,19 +35,8 @@ test('Method attribute returns promise', () => { }); test('Method attribute promise resolves', () => { - let resolver - const p = new Promise( (a, b) => { resolver = a; }) - - Advanced.list(item => { - expect(item.aString).toBe("Here's a string") - expect(item.aNumber).toBe(123) - expect(typeof item.aMethod).toBe('function') - item.aMethod().then(result => { - expect(result.foo).toBe("here's foo") - expect(result.bar).toBe(1) - resolver() - }) + return Simple.methodWithMethodAttribute('test').then( async result => { + const value = await result.aMethod() + expect(value).toBeDefined() }) - - return p }) diff --git a/test/suite/properties-context.test.js b/test/suite/properties-context.test.js index 77ba2c6f..48c27d41 100644 --- a/test/suite/properties-context.test.js +++ b/test/suite/properties-context.test.js @@ -31,15 +31,16 @@ let bothContextSentToEvent = false beforeAll( () => { - transport.onSend(json => { + transport.onSend(message => { + const json = JSON.parse(message) console.dir(json) - if (json.method === 'advanced.propertyWithContext') { + if (json.method === 'Advanced.propertyWithContext') { if (json.params.appId === 'some-app') { contextSentToGetter = true } transport.response(json.id, true) } - else if (json.method === 'advanced.onPropertyWithContextChanged') { + else if (json.method === 'Advanced.onPropertyWithContextChanged') { if (json.params.appId === 'some-app') { contextSentToSubscriber = true } @@ -55,7 +56,7 @@ beforeAll( () => { transport.response(json.id, false) }) } - else if (json.method === 'advanced.setPropertyWithContext') { + else if (json.method === 'Advanced.setPropertyWithContext') { if (json.params.appId === 'some-app') { contextSentToSetter = true } @@ -65,12 +66,12 @@ beforeAll( () => { propertySetterWasTriggeredWithValue = true } } - else if (json.method === "advanced.onEventWithContext") { + else if (json.method === "Advanced.onEventWithContext") { if (json.params.appId === 'some-app') { contextSentToEvent = true } } - else if (json.method === "advanced.onEventWithTwoContext") { + else if (json.method === "Advanced.onEventWithTwoContext") { if (json.params.appId === 'some-app' && json.params.state === 'inactive') { bothContextSentToEvent = true } diff --git a/test/suite/properties.test.js b/test/suite/properties.test.js index 27825d76..e56c0036 100644 --- a/test/suite/properties.test.js +++ b/test/suite/properties.test.js @@ -16,23 +16,26 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Simple } from '../../build/sdk/javascript/src/sdk.mjs' +import { Settings, Simple } from '../../build/sdk/javascript/src/sdk.mjs' import Setup from '../Setup' import { transport } from '../TransportHarness.js' import { expect } from '@jest/globals'; +Settings.setLogLevel('DEBUG') + let propertySetterWasTriggered = false let propertySetterWasTriggeredWithValue = false beforeAll( () => { - transport.onSend(json => { - if (json.method === 'simple.property') { + transport.onSend(message => { + const json = JSON.parse(message) + if (json.method === 'Simple.property') { transport.response(json.id, { foo: "here's foo" }) } - else if (json.method === 'simple.onPropertyChanged') { + else if (json.method === 'Simple.onPropertyChanged') { // Confirm the listener is on transport.response(json.id, { listening: true, @@ -46,7 +49,7 @@ beforeAll( () => { }) }) } - else if (json.method === 'simple.setProperty') { + else if (json.method === 'Simple.setProperty') { propertySetterWasTriggered = true if (json.params.value.foo === 'a new foo!' || json.params.value.foo === null) { propertySetterWasTriggeredWithValue = true diff --git a/test/suite/provider-errors.test.js b/test/suite/provider-errors.test.js index d8d2deb6..a03cbc19 100644 --- a/test/suite/provider-errors.test.js +++ b/test/suite/provider-errors.test.js @@ -16,7 +16,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Provider } from '../../build/sdk/javascript/src/sdk.mjs' +import { Settings, Provider } from '../../build/sdk/javascript/src/sdk.mjs' import Setup from '../Setup.js' import { transport } from '../TransportHarness.js' @@ -25,11 +25,11 @@ let providerMethodRequestDispatched = false let providerMethodErrorSent = false let methodSession let value -let responseCorrelationId - beforeAll( () => { + Settings.setLogLevel('DEBUG') + class SimpleProvider { simpleMethod(...args) { methodSession = args[1] @@ -44,32 +44,31 @@ beforeAll( () => { } } - transport.onSend(json => { - if (json.method === 'provider.onRequestSimpleMethod') { - providerMethodNotificationRegistered = true - - // Confirm the listener is on - transport.response(json.id, { - listening: true, - event: json.method - }) - - // send out a request event - setTimeout( _ => { - providerMethodRequestDispatched = true - transport.response(json.id, { - correlationId: 123 + transport.onSend(message => { + const json = JSON.parse(message) + if (json.method) { + if (json.method === 'Provider.provideSimple') { + providerMethodNotificationRegistered = true + + // send out a request event + setTimeout( _ => { + providerMethodRequestDispatched = true + transport.request({ + id: 1, + method: 'Simple.simpleMethod' + }) }) - }) + } } - else if (json.method === 'provider.simpleMethodError') { - providerMethodErrorSent = true - value = json.params.error - responseCorrelationId = json.params.correlationId + else { + if (json.id === 1 && json.error) { + providerMethodErrorSent = true + value = json.error + } } }) - Provider.provide('xrn:firebolt:capability:test:simple', new SimpleProvider()) + Provider.provideSimple(new SimpleProvider()) return new Promise( (resolve, reject) => { setTimeout(resolve, 100) @@ -89,18 +88,6 @@ test('Provider method request dispatched', () => { expect(providerMethodRequestDispatched).toBe(true) }) -test('Provide method session arg has correlationId', () => { - expect(methodSession.correlationId()).toBe(123) -}) - -test('Provide method session arg DOES NOT have focus', () => { - expect(methodSession.hasOwnProperty('focus')).toBe(false) -}) - -test('Provider response used correct correlationId', () => { - expect(responseCorrelationId).toBe(123) -}) - test('Provider method error is correct', () => { expect(value.code).toBe(50) expect(value.data.info).toBe('the_info') diff --git a/test/suite/provider.as-class.test.js b/test/suite/provider.as-class.test.js index 5c02c517..295a7b6c 100644 --- a/test/suite/provider.as-class.test.js +++ b/test/suite/provider.as-class.test.js @@ -24,49 +24,42 @@ let providerMethodNotificationRegistered = false let providerMethodRequestDispatched = false let providerMethodResultSent = false let numberOfArgs = -1 -let methodParameters -let methodSession let value -let responseCorrelationId - beforeAll( () => { class SimpleProvider { - simpleMethod(...args) { - methodParameters = args[0] - methodSession = args[1] - numberOfArgs = args.length + simpleMethod() { + numberOfArgs = arguments.length return Promise.resolve('a value!') } } - transport.onSend(json => { - if (json.method === 'provider.onRequestSimpleMethod') { - providerMethodNotificationRegistered = true - - // Confirm the listener is on - transport.response(json.id, { - listening: true, - event: json.method - }) - - // send out a request event - setTimeout( _ => { - providerMethodRequestDispatched = true - transport.response(json.id, { - correlationId: 123 + transport.onSend(message => { + const json = JSON.parse(message) + if (json.method) { + if (json.method === 'Provider.provideSimple') { + providerMethodNotificationRegistered = true + + // send out a request event + setTimeout( _ => { + providerMethodRequestDispatched = true + transport.request({ + id: 1, + method: 'Simple.simpleMethod' + }) }) - }) + } } - else if (json.method === 'provider.simpleMethodResponse') { - providerMethodResultSent = true - value = json.params.result - responseCorrelationId = json.params.correlationId + else { + if (json.id === 1 && json.result) { + providerMethodResultSent = true + value = json.result + } } }) - Provider.provide('xrn:firebolt:capability:test:simple', new SimpleProvider()) + Provider.provideSimple(new SimpleProvider()) return new Promise( (resolve, reject) => { setTimeout(resolve, 100) @@ -87,23 +80,7 @@ test('Provider method request dispatched', () => { }) test('Provide method called with two args', () => { - expect(numberOfArgs).toBe(2) -}) - -test('Provide method parameters arg is null', () => { - expect(methodParameters).toBe(null) -}) - -test('Provide method session arg has correlationId', () => { - expect(methodSession.correlationId()).toBe(123) -}) - -test('Provide method session arg DOES NOT have focus', () => { - expect(methodSession.hasOwnProperty('focus')).toBe(false) -}) - -test('Provider response used correct correlationId', () => { - expect(responseCorrelationId).toBe(123) + expect(numberOfArgs).toBe(0) }) test('Provider method result is correct', () => { diff --git a/test/suite/provider.as-object.test.js b/test/suite/provider.as-object.test.js index ff40030b..9bd8f5cd 100644 --- a/test/suite/provider.as-object.test.js +++ b/test/suite/provider.as-object.test.js @@ -24,54 +24,50 @@ let providerMethodNotificationRegistered = false let providerMethodRequestDispatched = false let providerMethodResultSent = false let numberOfArgs = -1 -let methodParameters -let methodSession let value -let responseCorrelationId - beforeAll( () => { - - transport.onSend(json => { - if (json.method === 'provider.onRequestSimpleMethod') { - providerMethodNotificationRegistered = true - // Confirm the listener is on - transport.response(json.id, { - listening: true, - event: json.method - }) + const myProvider = { + simpleMethod: (...args) => { + numberOfArgs = args.length + return Promise.resolve('a value!') + } + } + - // send out a request event - setTimeout( _ => { - providerMethodRequestDispatched = true - transport.response(json.id, { - correlationId: 123 + transport.onSend(message => { + const json = JSON.parse(message) + if (json.method) { + if (json.method === 'Provider.provideSimple') { + providerMethodNotificationRegistered = true + + // send out a request event + setTimeout( _ => { + providerMethodRequestDispatched = true + transport.request({ + id: 1, + method: 'Simple.simpleMethod' + }) }) - }) + } } - else if (json.method === 'provider.simpleMethodResponse') { - providerMethodResultSent = true - value = json.params.result - responseCorrelationId = json.params.correlationId + else { + if (json.id === 1 && json.result) { + providerMethodResultSent = true + value = json.result + } } }) - Provider.provide('xrn:firebolt:capability:test:simple', { - simpleMethod: (...args) => { - numberOfArgs = args.length - methodParameters = args[0] - methodSession = args[1] - return Promise.resolve('a value!') - } - }) - + Provider.provideSimple(myProvider) + return new Promise( (resolve, reject) => { setTimeout(resolve, 100) }) }) -test('Provider as Object registered', () => { +test('Provider as Class registered', () => { // this one is good as long as there's no errors yet expect(1).toBe(1) }); @@ -85,23 +81,7 @@ test('Provider method request dispatched', () => { }) test('Provide method called with two args', () => { - expect(numberOfArgs).toBe(2) -}) - -test('Provide method parameters arg is null', () => { - expect(methodParameters).toBe(null) -}) - -test('Provide method session arg has correlationId', () => { - expect(methodSession.correlationId()).toBe(123) -}) - -test('Provide method session arg DOES NOT have focus', () => { - expect(methodSession.hasOwnProperty('focus')).toBe(false) -}) - -test('Provider response used correct correlationId', () => { - expect(responseCorrelationId).toBe(123) + expect(numberOfArgs).toBe(0) }) test('Provider method result is correct', () => { diff --git a/test/suite/provider.with-handshake.test.js b/test/suite/provider.with-handshake.test.js deleted file mode 100644 index aba6a5a2..00000000 --- a/test/suite/provider.with-handshake.test.js +++ /dev/null @@ -1,112 +0,0 @@ -/* - * 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 - */ - -import { Provider } from '../../build/sdk/javascript/src/sdk.mjs' -import Setup from '../Setup.js' -import { transport } from '../TransportHarness.js' - -let providerMethodNotificationRegistered = false -let providerMethodRequestDispatched = false -let providerMethodResultSent = false -let providerMethodReadySent = false -let methodSession -let numberOfArgs = -1 -let value -let responseCorrelationId - - -beforeAll( () => { - - class SimpleProvider { - handshakeMethod(...args) { - numberOfArgs = args.length - methodSession = args[1] - // call 'focus' - methodSession.focus() - return Promise.resolve('a value!') - } - } - - transport.onSend(json => { - if (json.method === 'provider.onRequestHandshakeMethod') { - providerMethodNotificationRegistered = true - - // Confirm the listener is on - transport.response(json.id, { - listening: true, - event: json.method - }) - - // send out a request event - setTimeout( _ => { - providerMethodRequestDispatched = true - transport.response(json.id, { - correlationId: 123 - }) - }) - } - else if (json.method === 'provider.handshakeMethodFocus') { - providerMethodReadySent = true - } - else if (json.method === 'provider.handshakeMethodResponse') { - providerMethodResultSent = true - value = json.params.result - responseCorrelationId = json.params.correlationId - } - }) - - Provider.provide('xrn:firebolt:capability:test:handshake', new SimpleProvider()) - - return new Promise( (resolve, reject) => { - setTimeout(resolve, 100) - }) -}) - -test('Provider as Class registered', () => { - // this one is good as long as there's no errors yet - expect(1).toBe(1) -}); - -test('Provider method notification turned on', () => { - expect(providerMethodNotificationRegistered).toBe(true) -}) - -test('Provider method request dispatched', () => { - expect(providerMethodRequestDispatched).toBe(true) -}) - -test('Provider called ready method', () => { - expect(providerMethodReadySent).toBe(true) -}) - -test('Provide method called with two args (parameters, session)', () => { - expect(numberOfArgs).toBe(2) -}) - -test('Provide method session arg DOES have focus()', () => { - expect(methodSession.hasOwnProperty('focus')).toBe(true) - expect(typeof methodSession.focus === 'function') -}) - -test('Provider response used correct correlationId', () => { - expect(responseCorrelationId).toBe(123) -}) - -test('Provider method result is correct', () => { - expect(value).toBe('a value!') -}) diff --git a/test/suite/provider.with-multiple-methods.test.js b/test/suite/provider.with-multiple-methods.test.js index 49d952e2..f4d5a09f 100644 --- a/test/suite/provider.with-multiple-methods.test.js +++ b/test/suite/provider.with-multiple-methods.test.js @@ -16,7 +16,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Provider } from '../../build/sdk/javascript/src/sdk.mjs' +import { Settings, Provider } from '../../build/sdk/javascript/src/sdk.mjs' import Setup from '../Setup.js' import { transport } from '../TransportHarness.js' @@ -24,7 +24,6 @@ let providerMethodOneNotificationRegistered = false let providerMethodOneRequestDispatched = false let providerMethodOneResultSent = false let numberOfArgsMethodOne = -1 -let methodOneParameters let methodOneSession let valueOne let responseCorrelationIdOne @@ -41,70 +40,57 @@ let responseCorrelationIdTwo beforeAll( () => { + Settings.setLogLevel('DEBUG') + class MultiProvider { - multiMethodOne(...args) { - numberOfArgsMethodOne = args.length - methodOneParameters = args[0] - methodOneSession = args[1] + multiMethodOne() { + numberOfArgsMethodOne = arguments.length return Promise.resolve('a value!') } - multiMethodTwo(...args) { - numberOfArgsMethodTwo = args.length - methodTwoParameters = args[0] - methodTwoSession = args[1] + multiMethodTwo() { + numberOfArgsMethodTwo = arguments.length return Promise.resolve('another value!') } } - transport.onSend(json => { - if (json.method === 'provider.onRequestMultiMethodOne') { - providerMethodOneNotificationRegistered = true - - // Confirm the listener is on - transport.response(json.id, { - listening: true, - event: json.method - }) - - // send out a request event - setTimeout( _ => { - providerMethodOneRequestDispatched = true - transport.response(json.id, { - correlationId: 123 + transport.onSend(message => { + const json = JSON.parse(message) + if (json.method) { + if (json.method === 'Provider.provideMultipleMethods') { + providerMethodOneNotificationRegistered = true + providerMethodTwoNotificationRegistered = true + + // send out a request event + setTimeout( _ => { + providerMethodOneRequestDispatched = true + transport.request({ + id: 1, + method: 'MultipleMethods.multiMethodOne' + }) }) - }) - } - else if (json.method === 'provider.multiMethodOneResponse') { - providerMethodOneResultSent = true - valueOne = json.params.result - responseCorrelationIdOne = json.params.correlationId + setTimeout( _ => { + providerMethodTwoRequestDispatched = true + transport.request({ + id: 2, + method: "MultipleMethods.multiMethodTwo" + }) + }) + } } - if (json.method === 'provider.onRequestMultiMethodTwo') { - providerMethodTwoNotificationRegistered = true - - // Confirm the listener is on - transport.response(json.id, { - listening: true, - event: json.method - }) - - // send out a request event - setTimeout( _ => { - providerMethodTwoRequestDispatched = true - transport.response(json.id, { - correlationId: 456 - }) - }) - } - else if (json.method === 'provider.multiMethodTwoResponse') { - providerMethodTwoResultSent = true - valueTwo = json.params.result - responseCorrelationIdTwo = json.params.correlationId + else { + if (json.id === 1 && json.result) { + providerMethodOneResultSent = true + valueOne = json.result + } + else if (json.id === 2 && json.result) { + providerMethodTwoResultSent = true + valueTwo = json.result + } } }) - Provider.provide('xrn:firebolt:capability:test:multi', new MultiProvider()) + Provider.provideMultipleMethods(new MultiProvider()) return new Promise( (resolve, reject) => { setTimeout(resolve, 100) @@ -125,23 +111,7 @@ test('Provider method 1 request dispatched', () => { }) test('Provide method 1 called with two args', () => { - expect(numberOfArgsMethodOne).toBe(2) -}) - -test('Provide method 1 parameters arg is null', () => { - expect(methodOneParameters).toBe(null) -}) - -test('Provide method 1 session arg has correlationId', () => { - expect(methodOneSession.correlationId()).toBe(123) -}) - -test('Provide method 1 session arg DOES NOT have focus', () => { - expect(methodOneSession.hasOwnProperty('focus')).toBe(false) -}) - -test('Provider response 1 used correct correlationId', () => { - expect(responseCorrelationIdOne).toBe(123) + expect(numberOfArgsMethodOne).toBe(0) }) test('Provider method 1 result is correct', () => { @@ -157,23 +127,7 @@ test('Provider method 2 request dispatched', () => { }) test('Provide method 2 called with two args', () => { - expect(numberOfArgsMethodTwo).toBe(2) -}) - -test('Provide method 2 parameters arg is null', () => { - expect(methodTwoParameters).toBe(null) -}) - -test('Provide method 2 session arg has correlationId', () => { - expect(methodTwoSession.correlationId()).toBe(456) -}) - -test('Provide method 2 session arg DOES NOT have focus', () => { - expect(methodTwoSession.hasOwnProperty('focus')).toBe(false) -}) - -test('Provider response 2 used correct correlationId', () => { - expect(responseCorrelationIdTwo).toBe(456) + expect(numberOfArgsMethodTwo).toBe(0) }) test('Provider method 2 result is correct', () => { diff --git a/test/suite/provider.with-no-response.test.js b/test/suite/provider.with-no-response.test.js index cd2a6211..cd4d8f3e 100644 --- a/test/suite/provider.with-no-response.test.js +++ b/test/suite/provider.with-no-response.test.js @@ -16,7 +16,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Provider } from '../../build/sdk/javascript/src/sdk.mjs' +import { Settings, Provider } from '../../build/sdk/javascript/src/sdk.mjs' import Setup from '../Setup.js' import { transport } from '../TransportHarness.js' @@ -25,44 +25,44 @@ let providerMethodRequestDispatched = false let providerMethodResultSent = false let numberOfArgs = -1 let hasNoResponse -let responseCorrelationId beforeAll( () => { + Settings.setLogLevel('DEBUG') + class NoResponseProvider { noResponseMethod(...args) { numberOfArgs = args.length return Promise.resolve() } } - - transport.onSend(json => { - if (json.method === 'provider.onRequestNoResponseMethod') { - providerMethodNotificationRegistered = true - // Confirm the listener is on - transport.response(json.id, { - listening: true, - event: json.method - }) + transport.onSend(message => { + const json = JSON.parse(message) + if (json.method) { + if (json.method === 'Provider.provideNoResponseMethod') { + providerMethodNotificationRegistered = true - // send out a request event - setTimeout( _ => { - providerMethodRequestDispatched = true - transport.response(json.id, { - correlationId: 123 + // send out a request event + setTimeout( _ => { + providerMethodRequestDispatched = true + transport.request({ + id: 1, + method: 'NoResponseMethod.noResponseMethod' + }) }) - }) + } } - else if (json.method === 'provider.noResponseMethodResponse') { - providerMethodResultSent = true - hasNoResponse = json.params.hasOwnProperty('result') === false - responseCorrelationId = json.params.correlationId + else { + if (json.id === 1 && json.result !== undefined) { + providerMethodResultSent = true + hasNoResponse = json.result === null + } } }) - Provider.provide('xrn:firebolt:capability:test:noresponse', new NoResponseProvider()) + Provider.provideNoResponseMethod(new NoResponseProvider()) return new Promise( (resolve, reject) => { setTimeout(resolve, 100) @@ -82,14 +82,6 @@ test('Provider method request dispatched', () => { expect(providerMethodRequestDispatched).toBe(true) }) -test('Provide method called with two args', () => { - expect(numberOfArgs).toBe(2) -}) - -test('Provider response used correct correlationId', () => { - expect(responseCorrelationId).toBe(123) -}) - test('Provider method has no response', () => { expect(hasNoResponse).toBe(true) }) diff --git a/test/suite/simple.test.js b/test/suite/simple.test.js index c969afce..cd45a9e9 100644 --- a/test/suite/simple.test.js +++ b/test/suite/simple.test.js @@ -17,47 +17,24 @@ */ import { Simple } from '../../build/sdk/javascript/src/sdk.mjs' -import { expect } from '@jest/globals'; +import { expect, beforeAll } from '@jest/globals'; +beforeAll( () => { -class TransportSpy { - - constructor(spy) { - this.spy = spy - this.responder = null - } - - async send(msg) { - let parsed = JSON.parse(msg) - this.spy(parsed) - this.responder(JSON.stringify({ - jsonrpc: '2.0', - id: parsed.id, - result: {} - })) - } - - receive(callback) { - this.responder = callback - } -} +}) test('Basic', () => { return Simple.method(true).then(result => { + console.dir(result) expect(result.foo).toBe("here's foo") }) }); -test('Multiple Parameters', async () => { - let cb = null; - let promise = new Promise((resolve, reject) => { - cb = resolve - }) - window['__firebolt'].setTransportLayer(new TransportSpy(cb)) - await Simple.methodWithMultipleParams(5, 'foo') - let msg = await promise - expect(msg.method).toBe('simple.methodWithMultipleParams') - expect(msg.params.id).toBe(5) - expect(msg.params.title).toBe('foo') - console.log(JSON.stringify(msg)) -}); +// test('Multiple Parameters', async () => { +// await Simple.methodWithMultipleParams(5, 'foo') +// let msg = await promise +// expect(msg.method).toBe('simple.methodWithMultipleParams') +// expect(msg.params.id).toBe(5) +// expect(msg.params.title).toBe('foo') +// console.log(JSON.stringify(msg)) +// }); diff --git a/test/suite/temporal-set.test.js b/test/suite/temporal-set.test.js index c6a1fb37..b7c02d19 100644 --- a/test/suite/temporal-set.test.js +++ b/test/suite/temporal-set.test.js @@ -16,9 +16,12 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Advanced } from '../../build/sdk/javascript/src/sdk.mjs' -import Setup from '../Setup' -import { expect } from '@jest/globals'; +import { Settings, Advanced } from '../../build/sdk/javascript/src/sdk.mjs' +import { expect, beforeAll } from '@jest/globals'; + +beforeAll( () => { + Settings.setLogLevel('DEBUG') +}) test('Temporal Set - all known values', () => { return Advanced.list().then(items => { @@ -27,33 +30,33 @@ test('Temporal Set - all known values', () => { }) }) -test('Temporal Set - first match', () => { - return Advanced.list(10000).then(item => { - console.dir(item) - expect(Array.isArray(item)).toBe(false) - }) -}) +// test('Temporal Set - first match', () => { +// return Advanced.list(10000).then(item => { +// console.dir(item) +// expect(Array.isArray(item)).toBe(false) +// }) +// }) -test('Temporal Set - live list', () => { - - return new Promise( (resolve, reject) => { - const process = Advanced.list((item) => { - // add - console.dir(item) - expect(item.aString).toBe("Here's a string") - expect(item.aNumber).toBe(123) - expect(typeof item.aMethod).toBe('function') - item.aMethod().then(result => { - expect(result.foo).toBe("here's foo") - expect(result.bar).toBe(1) - resolve() - }) - }, - (item) => { - // remove - }) - - expect(typeof process.stop).toBe('function') - }) -}) +// test('Temporal Set - live list', () => { + +// return new Promise( (resolve, reject) => { +// const process = Advanced.list((item) => { +// // add +// console.dir(item) +// expect(item.aString).toBe("Here's a string") +// expect(item.aNumber).toBe(123) +// expect(typeof item.aMethod).toBe('function') +// item.aMethod().then(result => { +// expect(result.foo).toBe("here's foo") +// expect(result.bar).toBe(1) +// resolve() +// }) +// }, +// (item) => { +// // remove +// }) + +// expect(typeof process.stop).toBe('function') +// }) +// })