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
3 changes: 0 additions & 3 deletions packages/dataset-registry-client/tsconfig.lib.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,6 @@
},
"include": ["src/**/*.ts"],
"references": [
{
"path": "../local-sparql-endpoint/tsconfig.lib.json"
},
{
"path": "../dataset/tsconfig.lib.json"
}
Expand Down
20 changes: 16 additions & 4 deletions packages/docgen/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ npx @lde/docgen@latest from-shacl <shacl-file> <template-file> [options]

### Options

| Option | Description | Default |
| -------------------- | ---------------------------- | ------------------------------------ |
| `-f, --frame <file>` | Path to a JSON-LD Frame file | Built-in `frames/shacl.frame.jsonld` |
| Option | Description | Default |
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ |
| `-f, --frame <file>` | Path to a JSON-LD Frame file. Deep-merged on top of the built-in default frame, so it only needs to contain your additions (e.g. extra `@context` entries). | Built-in `frames/shacl.frame.jsonld` |

### Example

Expand Down Expand Up @@ -108,8 +108,20 @@ Property shapes with the same `sh:path` are common in SHACL (e.g. one for cardin

## Custom frames

The default frame selects all `sh:NodeShape` resources. To customise which shapes are selected or how they are nested, pass a custom [JSON-LD Frame](https://www.w3.org/TR/json-ld11-framing/):
The default frame selects all `sh:NodeShape` resources and provides type coercions for common SHACL terms (`targetClass`, `path`, `severity`, etc.). To extend it, pass a partial [JSON-LD Frame](https://www.w3.org/TR/json-ld11-framing/) – it is **deep-merged** on top of the default, so you only need to specify your additions:

```json
{
"@context": {
"nde": "https://def.nde.nl#",
"nde:futureChange": {},
"nde:version": {}
}
}
```

```sh
npx @lde/docgen@latest from-shacl shapes.ttl template.liquid -f my-frame.jsonld
```

Plain objects are merged key-by-key, with user values winning; arrays and primitives in your frame replace the default. To override a built-in coercion (e.g. change `severity` from `@vocab` to `@id`), redefine the same key in your `@context`.
10 changes: 2 additions & 8 deletions packages/docgen/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,6 @@
import { Command } from 'commander';
import { generateDocumentation } from './index.js';
import packageJson from '../package.json' with { type: 'json' };
import { dirname } from 'path';
import { fileURLToPath } from 'url';

const __dirname = dirname(fileURLToPath(import.meta.url));

const program = new Command();

Expand All @@ -27,9 +23,8 @@ program
'Path to Liquid template file'
)
.option(
'-f --frame <json-ld-frame-file>',
'Path to a JSON-LD Frame file',
__dirname + '/../frames/shacl.frame.jsonld'
'-f --frame <json-ld-frame-file>',
'Path to a JSON-LD Frame file. Deep-merged on top of the built-in default frame, so it only needs to contain your additions.'
)
.addHelpText(
'after',
Expand All @@ -49,4 +44,3 @@ Example:
});

program.parse();

58 changes: 50 additions & 8 deletions packages/docgen/src/frame.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,56 @@
import type { JsonLdArray } from 'jsonld/jsonld-spec.js';
import type { Frame, JsonLdArray } from 'jsonld/jsonld-spec.js';
import type { NodeObject } from 'jsonld';
import jsonld from 'jsonld';
import { readFile } from 'node:fs/promises';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';

export async function frame(document: JsonLdArray, frame: string) {
return await jsonld.frame(
document,
JSON.parse(await readFile(frame, 'utf8')),
{
omitGraph: false,
embed: '@always',
const __dirname = dirname(fileURLToPath(import.meta.url));

const defaultFramePath = join(__dirname, '../frames/shacl.frame.jsonld');

export async function frame(
document: JsonLdArray,
userFramePath?: string
): Promise<NodeObject> {
const defaultFrame = await readFrame(defaultFramePath);
const mergedFrame = userFramePath
? deepMerge(defaultFrame, await readFrame(userFramePath))
: defaultFrame;

return await jsonld.frame(document, mergedFrame, {
omitGraph: false,
embed: '@always',
});
}

async function readFrame(path: string): Promise<Frame> {
return JSON.parse(await readFile(path, 'utf8')) as Frame;
}

/**
* Recursively merges `source` into `target`, returning a new object. Plain
* objects are merged key-by-key; arrays and primitives in `source` replace
* those in `target`. Used to compose a user-supplied JSON-LD frame on top of
* docgen’s built-in default so consumers only need to specify their additions.
*/
function deepMerge(target: Frame, source: Frame): Frame {
const result = { ...(target as Record<string, unknown>) };
for (const [key, sourceValue] of Object.entries(
source as Record<string, unknown>
)) {
const targetValue = result[key];
if (isPlainObject(targetValue) && isPlainObject(sourceValue)) {
result[key] = deepMerge(targetValue as Frame, sourceValue as Frame);
} else {
result[key] = sourceValue;
}
}
return result as Frame;
}

function isPlainObject(value: unknown): value is Record<string, unknown> {
return (
typeof value === 'object' && value !== null && !Array.isArray(value)
);
}
11 changes: 10 additions & 1 deletion packages/docgen/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,19 @@ import { parseRdfToJsonLd } from './parse.js';
import { frame } from './frame.js';
import { render } from './render.js';

/**
* Generate documentation from a SHACL shapes file using a Liquid template.
*
* @param rdfPath Path to a SHACL shapes file in any RDF serialization.
* @param templatePath Path to a Liquid template.
* @param framePath Optional path to a JSON-LD frame. When provided, it is
* deep-merged on top of docgen’s built-in default frame, so consumers only
* need to specify their additions (e.g. extra `@context` entries).
*/
export async function generateDocumentation(
rdfPath: string,
templatePath: string,
framePath: string
framePath?: string
): Promise<string> {
const jsonld = await parseRdfToJsonLd(rdfPath);
const framed = await frame(jsonld, framePath);
Expand Down
6 changes: 6 additions & 0 deletions packages/docgen/test/fixtures/partial.frame.jsonld
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"@context": {
"schema": "https://schema.org/",
"schema:version": {}
}
}
63 changes: 63 additions & 0 deletions packages/docgen/test/frame.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { describe, expect, it } from 'vitest';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { frame } from '../src/frame.js';
import { parseRdfToJsonLd } from '../src/parse.js';

const __dirname = dirname(fileURLToPath(import.meta.url));
const SHACL_PATH = join(__dirname, 'fixtures', 'shacl.ttl');
const PARTIAL_FRAME_PATH = join(__dirname, 'fixtures', 'partial.frame.jsonld');

describe('frame', () => {
it('uses the built-in default frame when no user frame is given', async () => {
const document = await parseRdfToJsonLd(SHACL_PATH);
const framed = (await frame(document)) as { '@graph': unknown[] };

expect(framed['@graph']).toBeDefined();
expect(Array.isArray(framed['@graph'])).toBe(true);
expect(framed['@graph'].length).toBeGreaterThan(0);
});

it('deep-merges a user-supplied frame with the default', async () => {
const document = await parseRdfToJsonLd(SHACL_PATH);
const framed = (await frame(document, PARTIAL_FRAME_PATH)) as {
'@context': Record<string, unknown>;
'@graph': unknown[];
};

// User addition is present.
expect(framed['@context'].schema).toBe('https://schema.org/');

// Default coercions are still applied (path is rendered as IRI string,
// not a `{ "@id": "..." }` object), proving the default frame’s
// `"path": { "@type": "@id" }` was preserved.
const firstShape = framed['@graph'][0] as { property: { path: unknown }[] };
expect(typeof firstShape.property[0].path).toBe('string');
});

it('lets a user frame override a default coercion', async () => {
const overridePath = join(tmpdir(), `docgen-frame-override-${Date.now()}.jsonld`);
await writeFile(
overridePath,
JSON.stringify({
'@context': {
severity: { '@type': '@id' },
},
})
);

const document = await parseRdfToJsonLd(SHACL_PATH);
const framed = (await frame(document, overridePath)) as {
'@graph': { property: { severity?: unknown }[] }[];
};

const properties = framed['@graph'].flatMap((shape) => shape.property);
const withSeverity = properties.find((p) => p.severity !== undefined);

// Default coerces severity to @vocab → "Info"; the override coerces to
// @id → full IRI "http://www.w3.org/ns/shacl#Info".
expect(withSeverity?.severity).toBe('http://www.w3.org/ns/shacl#Info');
});
});
9 changes: 2 additions & 7 deletions packages/docgen/test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,10 @@ import { generateDocumentation } from '../src/index.js';
const __dirname = dirname(fileURLToPath(import.meta.url));
const SHACL_PATH = join(__dirname, 'fixtures', 'shacl.ttl');
const TEMPLATE_PATH = join(__dirname, 'fixtures', 'template.liquid');
const FRAME_PATH = join(__dirname, '../frames', 'shacl.frame.jsonld');

describe('Integration tests', () => {
it('should render template', async () => {
const output = await generateDocumentation(
SHACL_PATH,
TEMPLATE_PATH,
FRAME_PATH
);
it('should render template using the built-in default frame', async () => {
const output = await generateDocumentation(SHACL_PATH, TEMPLATE_PATH);

expect(output.trim().replace(/ +$/gm, ''))
.toBe(`targetClass: http://www.w3.org/ns/dcat#Dataset
Expand Down
3 changes: 0 additions & 3 deletions packages/pipeline/tsconfig.lib.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,6 @@
},
"include": ["src/**/*.ts"],
"references": [
{
"path": "../local-sparql-endpoint/tsconfig.lib.json"
},
{
"path": "../sparql-server/tsconfig.lib.json"
},
Expand Down