Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,17 @@ If the value is an array then first element must be API format and second is API
`ApiDefinition.type` ⇒ `String`. API type to process. Can be `RAML 0.8`, `RAML 1.0`, `OAS 2.0`, `OAS 3.0`, `ASYNC 2.0`, or `GRPC`.
`ApiDefinition.mime` ⇒ `String`. API media type. Default to `application/yaml`. For GRPC use `application/x-protobuf`.
`ApiDefinition.resolution` ⇒ `String`. AMF resolution pipeline. Default to `editing` which is the original resolution pipeline for API Console. Future releases of AMF can support different options.
`ApiDefinition.flattened` ⇒ `Boolean`. When `true`, generates a compact JSON-LD model using `@graph` instead of an expanded array. Recommended for large APIs. Default to `false`.
`ApiDefinition.sourceMaps` ⇒ `Boolean`. Controls source maps in the **full** model (`<name>.json`) only. When `false`, source map nodes are omitted — useful when no editing tooling will consume this API. The compact model (`<name>-compact.json`) never includes source maps. Default to `true`.

### Output files

Each API produces two files:

| File | URIs | Source maps | Intended consumer |
|------|------|-------------|-------------------|
| `<name>.json` | Expanded (`http://a.ml/...`) | Yes (controlled by `sourceMaps`) | Editing tools, validators |
| `<name>-compact.json` | Compact (`apiContract:WebAPI`) | Never | API Console, display/browsing |


### Example apis.json
Expand All @@ -44,6 +55,12 @@ If the value is an array then first element must be API format and second is API
"type": "RAML 1.0",
"mime": "application/raml",
"resolution": "editing"
},
"grpc/service.proto": {
"type": "GRPC",
"mime": "application/protobuf",
"flattened": true,
"sourceMaps": false
}
}
```
Expand Down
117 changes: 95 additions & 22 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,50 @@ const {
PipelineId,
} = amf;

/**
* Strips AMF source-map nodes and inline source-map properties from a
* serialised JSON-LD @graph string.
*
* AMF v5 only includes declared types (doc:declares) in the @graph when
* withSourceMaps() is used. For the compact model we want declares without
* the source-map noise, so we generate with source maps and then strip them.
*
* @param {string} data Raw JSON-LD string
* @returns {string} Stripped JSON-LD string
*/
function stripSourceMaps(data) {
const obj = JSON.parse(data);
const graph = obj['@graph'];
if (!Array.isArray(graph)) return data;

const filtered = graph.filter((node) => {
const rawTypes = node['@type'];
const joined = Array.isArray(rawTypes) ? rawTypes.join(',') : String(rawTypes || '');
if (joined.includes('SourceMap') || joined.includes('source-maps')) return false;
const id = node['@id'];
if (typeof id === 'string' && id.includes('source-map')) return false;
return true;
});

const sourceMapProps = [
'http://a.ml/vocabularies/document-source-maps#sources',
'http://a.ml/vocabularies/document-source-maps#lexical',
'sourcemaps:sources',
'sourcemaps:lexical',
'sourcemaps:synthesized-field',
'sourcemaps:grpc-raw-proto',
'sourcemaps:element',
'sourcemaps:value',
];
for (const node of filtered) {
for (const prop of sourceMapProps) {
delete node[prop];
}
}

return JSON.stringify({ ...obj, '@graph': filtered });
}

/** @typedef {import('./types').ApiConfiguration} ApiConfiguration */
/** @typedef {import('./types').FilePrepareResult} FilePrepareResult */
/** @typedef {import('./types').ApiGenerationOptions} ApiGenerationOptions */
Expand Down Expand Up @@ -40,56 +84,85 @@ function getConfiguration(type) {
/**
* Generates json/ld file from parsed document using AMF v5 API.
*
* Produces two output files:
* - `<name>.json` — full model, expanded URIs, source maps controlled by `sourceMaps`
* - `<name>-compact.json` — compact URIs, never includes source maps (optimized for display)
*
* @param {string} sourceFile
* @param {string} file
* @param {string} type
* @param {string} destPath
* @param {string} resolution
* @param {boolean} flattened
* @param {boolean} sourceMaps
* @return {Promise<void>}
*/
async function processFile(sourceFile, file, type, destPath, resolution, flattened) {
async function processFile(sourceFile, file, type, destPath, resolution, flattened, sourceMaps) {
let dest = `${file.substr(0, file.lastIndexOf('.')) }.json`;
if (dest.indexOf('/') !== -1) {
dest = dest.substr(dest.lastIndexOf('/'));
}

// Setup render options
let renderOpts = new RenderOptions().withSourceMaps().withCompactUris();
if (flattened) {
renderOpts = renderOpts.withCompactedEmission();
}

// Get configuration for API type
const apiConfiguration = getConfiguration(type).withRenderOptions(renderOpts);
const client = apiConfiguration.baseUnitClient();

// Parse the file
const parseResult = await client.parse(sourceFile);
// Parse using a base client (render options don't affect parsing).
// AMF v5 mutates the baseUnit during transform/render so we must parse twice —
// once for the full model and once for the compact model — to get independent
// base units.
const parseClient = getConfiguration(type).baseUnitClient();
const parseResult = await parseClient.parse(sourceFile);

if (!parseResult.conforms) {
/* eslint-disable-next-line no-console */
console.log('Validation warnings/errors:');
console.log(parseResult.toString());
}

// Transform using resolution pipeline
const pipelineId = resolution === 'editing' ? PipelineId.Editing : PipelineId.Default;
const transformed = client.transform(parseResult.baseUnit, pipelineId);
const transformed = parseClient.transform(parseResult.baseUnit, pipelineId);

// Fresh parse for compact model (avoids base-unit mutation by the full render above)
const parseClientForCompact = getConfiguration(type).baseUnitClient();
const parseResultForCompact = await parseClientForCompact.parse(sourceFile);
const transformedForCompact = parseClientForCompact.transform(parseResultForCompact.baseUnit, pipelineId);

// Render to JSON-LD
const fullFile = path.join(destPath, dest);
const compactDest = dest.replace('.json', '-compact.json');
const compactFile = path.join(destPath, compactDest);

// Generate full model (same as compact in v5 with withCompactUris)
const modelData = await client.render(transformed.baseUnit, 'application/ld+json');
// Full model: expanded URIs, source maps controlled by option (for editing tooling)
// withoutCompactedEmission() is required: AMF v5 defaults to @graph (compacted emission),
// but consumers like amf-loader.ts expect the old array format [{"@id": "amf://id", ...}].
// Only flattened models intentionally use @graph.
let fullRenderOpts = new RenderOptions().withoutCompactedEmission();
if (sourceMaps) {
fullRenderOpts = fullRenderOpts.withSourceMaps();
}
if (flattened) {
fullRenderOpts = fullRenderOpts.withCompactedEmission();
}
const fullData = await getConfiguration(type)
.withRenderOptions(fullRenderOpts)
.baseUnitClient()
.render(transformed.baseUnit, 'application/ld+json');

await fs.ensureFile(fullFile);
await fs.writeFile(fullFile, modelData, 'utf8');
await fs.writeFile(fullFile, fullData, 'utf8');

// Compact model: same as full model but with source maps stripped after rendering.
// AMF v5 only includes doc:declares (type definitions) when withSourceMaps() is
// active, so we must generate with source maps and remove them in post-processing.
// withCompactUris() is omitted: it also drops declared types from the @graph.
let compactRenderOpts = new RenderOptions().withSourceMaps().withoutCompactedEmission();
if (flattened) {
compactRenderOpts = compactRenderOpts.withCompactedEmission();
}
const compactRaw = await getConfiguration(type)
.withRenderOptions(compactRenderOpts)
.baseUnitClient()
.render(transformedForCompact.baseUnit, 'application/ld+json');
const compactData = flattened ? compactRaw : stripSourceMaps(compactRaw);

await fs.ensureFile(compactFile);
await fs.writeFile(compactFile, modelData, 'utf8');
await fs.writeFile(compactFile, compactData, 'utf8');
}

/**
Expand Down Expand Up @@ -127,9 +200,9 @@ async function parseFile(file, cnf, opts) {
if (!dest.endsWith('/')) {
dest += '/';
}
const { type, mime='application/yaml', resolution='editing', flattened = false } = normalizeOptions(cnf);
const { type, mime='application/yaml', resolution='editing', flattened = false, sourceMaps = true } = normalizeOptions(cnf);
const sourceFile = `file://${src}${file}`;
return processFile(sourceFile, file, type, dest, resolution, flattened);
return processFile(sourceFile, file, type, dest, resolution, flattened, sourceMaps);
}

/**
Expand Down
Loading
Loading