Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions languages/javascript/language.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@
"/index.mjs",
"/defaults.mjs"
],
"templatesPerSchema": [
"/index.mjs"
],
"enableStringPropertyKeys": true,
"createModuleDirectories": true,
"copySchemasIntoModules": false,
Expand Down
18 changes: 18 additions & 0 deletions languages/javascript/templates/declarations/subscriber.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* ${method.summary}
*
* @param {'${event.name}'} event
* @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: ${event.result.type}) => void): Promise<number>

/**
* ${method.summary}
* When using `once` the callback method will only fire once, and then disconnect your listener
*
* @param {'${event.name}'} event
* @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: ${event.result.type}) => void): Promise<number>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
registerEvents('${info.title}', ${events.array})
3 changes: 3 additions & 0 deletions languages/javascript/templates/methods/subscriber.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// ${method.name} is accessed via listen('${event.name}, ...)
${if.context}
registerEventContext('${info.title}', '${event.name}', ${method.context.array})${end.if.context}
2 changes: 1 addition & 1 deletion languages/markdown/templates/codeblocks/subscriber.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
To subscribe to notifications when the value changes, call the method like this:

```typescript
function ${method.alternative}(${event.signature.params}${if.context}, ${end.if.context}callback: (value) => ${method.result.type}): Promise<number>
function ${method.alternative}(${event.signature.params}${if.context}, ${end.if.context}callback: (${method.result.name}) => ${method.result.type}): Promise<number>
```

${event.params}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@ Response:

```${example.langcode}
{"jsonrpc":"2.0","id":1,"result":null}
```
```
8 changes: 6 additions & 2 deletions languages/markdown/templates/examples/subscriber.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import { ${module} } from '${package.name}'
let listenerId = await ${method.alternative}(${method.result.name} => {
console.log(${method.result.name})
})
console.log(${method.result.name})

```
Value of `${method.result.name}`:

```javascript
${example.result}
```
Value of `${method.result.name}: ${example.result} `
1 change: 1 addition & 0 deletions languages/markdown/templates/initializations/subscriber.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
registerEvents('${info.title}', ${events.array})
2 changes: 1 addition & 1 deletion languages/markdown/templates/sections/parameters.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Parameters:

| Param | Type | Required | Description |
| Param | Type | Required | Description |
| ---------------------- | -------------------- | ------------------------ | ----------------------- |
12 changes: 6 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@
"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 --platformApi ./build/sdk-open-rpc.json --appApi ./build/sdk-app-open-rpc.json --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 --platformApi ./build/sdk-open-rpc.json --appApi ./build/sdk-app-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 --platformApi ./build/sdk-open-rpc.json --appApi ./build/sdk-app-open-rpc.json --schemas test/schemas",
"build:d": "node ./src/cli.mjs declarations --input ./build/sdk-open-rpc.json --output ./dist/lib/sdk.d.ts --platformApi ./build/sdk-open-rpc.json --appApi ./build/sdk-app-open-rpc.json --schemas test/schemas",
"build:docs": "node ./src/cli.mjs docs --input ./build/sdk-open-rpc.json --output ./build/docs/markdown --platformApi ./build/sdk-open-rpc.json --appApi ./build/sdk-app-open-rpc.json --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_sdk/openrpc --platformApi ./build/sdk-open-rpc.json --appApi ./build/sdk-app-open-rpc.json --schemas test_sdk/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_sdk --template ./src/openrpc-template.json --platformApi ./build/sdk-open-rpc.json --appApi ./build/sdk-app-open-rpc.json --schemas test_sdk/schemas",
"build:sdk": "node ./src/cli.mjs sdk --input ./build/sdk-open-rpc.json --template ./test_sdk/sdk --output ./build/sdk/javascript/src --platformApi ./build/sdk-open-rpc.json --appApi ./build/sdk-app-open-rpc.json --schemas test_sdk/schemas",
"build:d": "node ./src/cli.mjs declarations --input ./build/sdk-open-rpc.json --output ./dist/lib/sdk.d.ts --platformApi ./build/sdk-open-rpc.json --appApi ./build/sdk-app-open-rpc.json --schemas test_sdk/schemas",
"build:docs": "node ./src/cli.mjs docs --input ./build/sdk-open-rpc.json --output ./build/docs/markdown --platformApi ./build/sdk-open-rpc.json --appApi ./build/sdk-app-open-rpc.json --schemas test_sdk/schemas --as-path",
"build:wiki": "node ./src/cli.mjs docs --input ./build/sdk-open-rpc.json --output ./build/docs/wiki --schemas test_sdk/schemas",
"dist": "npm run validate && npm run build:sdk && npm run build:docs && npm run test",
"prepare": "husky install"
},
Expand Down
21 changes: 15 additions & 6 deletions src/macrofier/engine.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,10 @@ const getTemplate = (name, templates) => {
}

const getTemplateTypeForMethod = (method, type, templates) => {
const name = method.tags ? (isAllowFocusMethod(method) && Object.keys(templates).find(name => name.startsWith(`/${type}/allowsFocus.`))) ? 'allowsFocus' : (method.tags.map(tag => tag.name.split(":").shift()).find(tag => Object.keys(templates).find(name => name.startsWith(`/${type}/${tag}.`)))) || 'default' : 'default'
let name = method.tags ? (isAllowFocusMethod(method) && Object.keys(templates).find(name => name.startsWith(`/${type}/allowsFocus.`))) ? 'allowsFocus' : (method.tags.map(tag => tag.name.split(":").shift()).find(tag => Object.keys(templates).find(name => name.startsWith(`/${type}/${tag}.`)))) || 'default' : 'default'
if(isXSubscriberFor(method)) {
name = 'subscriber'
}
const path = `/${type}/${name}`
return getTemplate(path, templates)
}
Expand Down Expand Up @@ -614,7 +617,13 @@ const generateMacros = (platformApi, appApi, templates, languages, options = {})

Array.from(new Set(['types'].concat(config.additionalSchemaTemplates))).filter(dir => dir).forEach(dir => {
state.typeTemplateDir = dir
const schemasArray = unique(generateSchemas(platformApi, templates, { baseUrl: '' }).concat(generateSchemas(appApi, templates, { baseUrl: '' })))
let includeXSchemas = true;
if (isGeneratingDocs(languages)){
includeXSchemas = false;
}

const schemasArray = unique(generateSchemas(platformApi, templates, { baseUrl: '' }, includeXSchemas).concat(generateSchemas(appApi, templates, { baseUrl: '' }, includeXSchemas)))

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'))
Expand Down Expand Up @@ -1030,7 +1039,7 @@ const isEnum = x => {
return schema.type && schema.type === 'string' && Array.isArray(schema.enum) && x.title
}

function generateSchemas(platformApi, templates, options) {
function generateSchemas(platformApi, templates, options, includeXSchemas = true) {
let results = []

if (!platformApi) {
Expand All @@ -1039,7 +1048,7 @@ function generateSchemas(platformApi, templates, options) {

let schemas = JSON.parse(JSON.stringify(platformApi.definitions || (platformApi.components && platformApi.components.schemas) || {}))
// we need to add also the schemas in platformApi["x-schemas"]
if (platformApi["x-schemas"]) {
if (includeXSchemas && platformApi["x-schemas"]) {
//iterate over each key in platformApi["x-schemas"] and merge into schemas
Object.entries(platformApi["x-schemas"]).forEach(([key, value]) => {
schemas = {
Expand Down Expand Up @@ -1639,7 +1648,7 @@ function insertMethodMacros(template, methodObj, platformApi, appApi, templates,
const pullerTemplate = (puller ? insertMethodMacros(getTemplate('/codeblocks/puller', templates), puller, platformApi, appApi, templates, type, examples, languages) : '')
const setter = getSetterFor(methodObj.name, platformApi)
const setterTemplate = (setter ? insertMethodMacros(getTemplate('/codeblocks/setter', templates), setter, platformApi, appApi, templates, type, examples, languages) : '')
const subscriber = platformApi.methods.find(method => method.tags.find(tag => tag['x-subscriber-for'] === `${moduleName}.${methodObj.name}`) || method.tags.find(tag => tag['x-alternative'] === `${moduleName}.${methodObj.name}()`))
const subscriber = platformApi.methods.find(method => method.tags.find(tag => tag['x-subscriber-for'] === `${moduleName}.${methodObj.name}`) )
let subscriberTemplate = ''
if (subscriber) {
subscriberTemplate = getTemplate('/codeblocks/subscriber', templates)
Expand Down Expand Up @@ -1830,7 +1839,7 @@ function insertMethodMacros(template, methodObj, platformApi, appApi, templates,
.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, currentModuleApiForEvent, templates, { name: result.name }))
.replace(/\$\{method\.result\}/g, generateResult(result.schema, currentModuleApi, templates, { name: result.name }))
.replace(/\$\{method\.result\.json\.type\}/g, resultJsonType)
.replace(/\$\{method\.result\.instantiation\}/g, resultInst)
.replace(/\$\{method\.result\.initialization\}/g, resultInit)
Expand Down
50 changes: 40 additions & 10 deletions src/macrofier/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import Types from './types.mjs'
import path from 'path'
import engine from './engine.mjs'
import { replaceUri } from '../shared/json-schema.mjs'
import { getLocalSchemas, replaceRef } from '../shared/json-schema.mjs'
import { getConfig } from '../shared/configLoader.mjs'

/************************************************************************************************/
Expand Down Expand Up @@ -270,32 +271,61 @@ const macrofy = async (
}
})
}


platformApiOpenRpc['x-schemas']
&& Object.entries(platformApiOpenRpc['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(platformApiOpenRpc['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 + '/')) {
Copy link
Contributor

@tomasz-blasz tomasz-blasz Oct 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please, comment on why there is a 4 — or even better, extract it as a const. It’ll make our lives much happier when inspecting the code later 😉
applies to line 299 as well

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

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 = platformApiOpenRpc['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
platformApiOpenRpc.info['x-uri-titles'] = platformApiOpenRpc.info['x-uri-titles'] || {}
platformApiOpenRpc.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
!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, suffix: suffixes?.js })
const macrosPrimary = engine.generateMacros(document, null, templates, exampleTemplates, {hideExcluded: hideExcluded, copySchemasIntoModules: copySchemasIntoModules, createPolymorphicMethods: createPolymorphicMethods, suffix: suffixes?.ts })

if (templatesPerSchema || primaryOutput.length) {
templatesPerSchema && templatesPerSchema.forEach( t => {
let content = getTemplate('/schemas', t, templates)
content = engine.insertMacros(content, macros)
const location = createModuleDirectories ? path.join(output, document.title, t) : path.join(output, t.replace(/module/, document.title.toLowerCase()).replace(/index/, document.title))

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))

outputFiles[location] = content
logSuccess(`Generated macros for schema ${path.relative(output, location)}`)
})

primaryOutput && primaryOutput.forEach(output => {
macrosPrimary.append = append
outputFiles[output] = engine.insertMacros(outputFiles[output], macrosPrimary)
})

append = true
}
})
Expand Down
1 change: 1 addition & 0 deletions src/shared/json-schema.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -587,4 +587,5 @@ export {
dereferenceAndMergeAllOfs,
getReferencedSchema,
getAllValuesForName,
dereferenceSchema
}
129 changes: 129 additions & 0 deletions src/shared/module/remove-unused-schemas.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/*
* If not stated otherwise in this file or this component's LICENSE file the
* following copyright and licenses apply:
*
* Copyright 2025 Sky UK
*
* 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.
*/


// utilities for cleaning unused schemas

/**
* Recursively find all $ref values in an object.
* @param {object} obj
* @param {Set<string>} refs
* @returns {Set<string>}
*/
function findRefs(obj, refs = new Set()) {
if (!obj || typeof obj !== "object") return refs;
if (Array.isArray(obj)) {
obj.forEach(item => findRefs(item, refs));
} else {
for (const [key, value] of Object.entries(obj)) {
if (key === "$ref" && typeof value === "string") {
refs.add(value);
} else {
findRefs(value, refs);
}
}
}
return refs;
}

/**
* Resolve a $ref path like "#/components/schemas/EventObject"
* inside a given object.
* @param {string} ref
* @param {object} root
* @returns {object|null}
*/
function resolveRef(ref, root) {
if (!ref.startsWith("#/")) return null;
const pathParts = ref.slice(2).split("/");
let target = root;
for (const part of pathParts) {
if (target && typeof target === "object") target = target[part];
else return null;
}
return target;
}

/**
* Collect all transitive references from an initial set of refs.
* @param {Set<string>} refSet
* @param {object} root
* @param {Set<string>} referenced
*/
function collectRefs(refSet, root, referenced = new Set()) {
const queue = [...refSet];
while (queue.length) {
const ref = queue.pop();
if (referenced.has(ref)) continue;
referenced.add(ref);
const target = resolveRef(ref, root);
if (target) {
const newRefs = findRefs(target);
for (const newRef of newRefs) {
if (!referenced.has(newRef)) queue.push(newRef);
}
}
}
return referenced;
}

/**
* Clean unused schemas from an OpenRPC-like JSON definition.
* @param {object} spec
* @returns {object} cleaned deep-cloned JSON object
*/
function removeUnusedSchemas(spec) {
const clone =
typeof structuredClone === "function"
? structuredClone(spec)
: JSON.parse(JSON.stringify(spec));

const referenced = new Set();
const initialRefs = findRefs(clone.methods);
collectRefs(initialRefs, clone, referenced);

// Prune components/schemas
if (clone.components?.schemas) {
for (const key of Object.keys(clone.components.schemas)) {
const refPath = `#/components/schemas/${key}`;
if (!referenced.has(refPath)) delete clone.components.schemas[key];
}
}

// Prune x-schemas
if (clone["x-schemas"]) {
for (const nsKey of Object.keys(clone["x-schemas"])) {
const ns = clone["x-schemas"][nsKey];
for (const schemaKey of Object.keys(ns)) {
if (schemaKey === "uri") continue;
const refPath = `#/x-schemas/${nsKey}/${schemaKey}`;
if (!referenced.has(refPath)) delete ns[schemaKey];
}
}
}

return clone;
}

export {
findRefs,
collectRefs,
resolveRef,
removeUnusedSchemas
}
Loading
Loading