From 476344e51168ee249c82972f9fddc4c6e3f1a5fb Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Fri, 27 Oct 2023 13:33:26 +0100 Subject: [PATCH 1/2] feat: allow custom context parser as a parameter to the parser & fix context mutations --- lib/JsonLdParser.ts | 6 ++++- lib/ParsingContext.ts | 6 ++--- .../keyword/EntryHandlerKeywordType.ts | 13 +++------- test/JsonLdParser-test.ts | 25 +++++++++++++++++-- 4 files changed, 35 insertions(+), 15 deletions(-) diff --git a/lib/JsonLdParser.ts b/lib/JsonLdParser.ts index a5a32a9..b379351 100644 --- a/lib/JsonLdParser.ts +++ b/lib/JsonLdParser.ts @@ -1,7 +1,7 @@ import * as RDF from "@rdfjs/types"; // tslint:disable-next-line:no-var-requires const Parser = require('@bergos/jsonparse'); -import {ERROR_CODES, ErrorCoded, IDocumentLoader, JsonLdContext, Util as ContextUtil} from "jsonld-context-parser"; +import {ERROR_CODES, ErrorCoded, IDocumentLoader, JsonLdContext, Util as ContextUtil, ContextParser} from "jsonld-context-parser"; import {PassThrough, Transform, Readable} from "readable-stream"; import {EntryHandlerArrayValue} from "./entryhandler/EntryHandlerArrayValue"; import {EntryHandlerContainer} from "./entryhandler/EntryHandlerContainer"; @@ -672,4 +672,8 @@ export interface IJsonLdParserOptions { * Defaults to false. */ rdfstarReverseInEmbedded?: boolean; + /** + * The the context parser to use. + */ + contextParser?: ContextParser; } diff --git a/lib/ParsingContext.ts b/lib/ParsingContext.ts index f7b6955..7dc0938 100644 --- a/lib/ParsingContext.ts +++ b/lib/ParsingContext.ts @@ -86,7 +86,7 @@ export class ParsingContext { constructor(options: IParsingContextOptions) { // Initialize settings - this.contextParser = new ContextParser({ documentLoader: options.documentLoader, skipValidation: options.skipContextValidation }); + this.contextParser = options.contextParser ?? new ContextParser({ documentLoader: options.documentLoader, skipValidation: options.skipContextValidation }); this.streamingProfile = !!options.streamingProfile; this.baseIRI = options.baseIRI; this.produceGeneralizedRdf = !!options.produceGeneralizedRdf; @@ -207,11 +207,11 @@ export class ParsingContext { || scopedContext[key]['@context']['@propagate']; // Propagation is true by default if (propagate !== false || i === keysOriginal.length - 1 - offset) { - contextRaw = scopedContext; + contextRaw = { ...scopedContext }; // Clean up final context delete contextRaw['@propagate']; - contextRaw[key] = { ...contextRaw[key] }; + contextRaw[key] = { ...contextRaw[key], }; if ('@id' in contextKeyEntry) { contextRaw[key]['@id'] = contextKeyEntry['@id']; } diff --git a/lib/entryhandler/keyword/EntryHandlerKeywordType.ts b/lib/entryhandler/keyword/EntryHandlerKeywordType.ts index ac292b7..08ee24e 100644 --- a/lib/entryhandler/keyword/EntryHandlerKeywordType.ts +++ b/lib/entryhandler/keyword/EntryHandlerKeywordType.ts @@ -69,18 +69,13 @@ export class EntryHandlerKeywordType extends EntryHandlerKeyword { if (hasTypedScopedContext) { // Do not propagate by default scopedContext = scopedContext.then((c) => { - if (!('@propagate' in c.getContextRaw())) { - c.getContextRaw()['@propagate'] = false; - } + let contextRaw = c.getContextRaw(); - // Set the original context at this depth as a fallback - // This is needed when a context was already defined at the given depth, - // and this context needs to remain accessible from child nodes when propagation is disabled. - if (c.getContextRaw()['@propagate'] === false) { - c.getContextRaw()['@__propagateFallback'] = context.getContextRaw(); + if (!('@propagate' in contextRaw) || contextRaw['@propagate'] === false) { + contextRaw = { ...contextRaw, '@propagate': false, '@__propagateFallback': context.getContextRaw() }; } - return c; + return new JsonLdContextNormalized(contextRaw); }); // Set the new context in the context tree diff --git a/test/JsonLdParser-test.ts b/test/JsonLdParser-test.ts index 24ca1ca..567f84c 100644 --- a/test/JsonLdParser-test.ts +++ b/test/JsonLdParser-test.ts @@ -6,7 +6,7 @@ import { EventEmitter } from 'events'; import {DataFactory} from "rdf-data-factory"; import each from 'jest-each'; import "jest-rdf"; -import {ERROR_CODES, ErrorCoded, FetchDocumentLoader, JsonLdContextNormalized} from "jsonld-context-parser"; +import {ContextParser, ERROR_CODES, ErrorCoded, FetchDocumentLoader, IParseOptions, JsonLdContext, JsonLdContextNormalized} from "jsonld-context-parser"; import {PassThrough} from "stream"; import {Util} from "../lib/Util"; import { ParsingContext } from '../lib/ParsingContext'; @@ -14,6 +14,23 @@ import contexts, { MockedDocumentLoader } from '../mocks/contexts'; const DF = new DataFactory(); +const deepFreeze = obj => { + Object.keys(obj).forEach(prop => { + if (typeof obj[prop] === 'object' && !Object.isFrozen(obj[prop])) deepFreeze(obj[prop]); + }); + return Object.freeze(obj); +}; + +class FrozenContextParser extends ContextParser { + constructor(options: ConstructorParameters[0]) { + super(options); + } + + public parse(context: JsonLdContext, options?: IParseOptions): Promise { + return super.parse(context, options)// .then(deepFreeze); + } +} + describe('JsonLdParser', () => { describe('Parsing a Verifiable Credential', () => { @@ -22,7 +39,11 @@ describe('JsonLdParser', () => { beforeEach(() => { parser = new JsonLdParser({ dataFactory: DF, - documentLoader: new MockedDocumentLoader(), + // Use the frozen context parser so we can detect if there are + // any attempts at mutations in the unit tests + contextParser: new FrozenContextParser({ + documentLoader: new MockedDocumentLoader() + }), }) }); From 99249d040fdf55c38307dc544d9a07bca6543708 Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Sat, 28 Oct 2023 16:51:09 +0100 Subject: [PATCH 2/2] chore: property scoped testing --- package.json | 2 +- spec/parser.js | 26 +++++++++++++ test/JsonLdParser-test.ts | 81 +++++++++++++++++++++++++++++++-------- yarn.lock | 14 +++---- 4 files changed, 99 insertions(+), 24 deletions(-) diff --git a/package.json b/package.json index 1e69542..a6a4744 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "buffer": "^6.0.3", "canonicalize": "^1.0.1", "http-link-header": "^1.0.2", - "jsonld-context-parser": "^2.3.3", + "jsonld-context-parser": "../jsonld-context-parser.js/", "rdf-data-factory": "^1.1.0", "readable-stream": "^4.0.0" }, diff --git a/spec/parser.js b/spec/parser.js index 120804d..4726ba0 100644 --- a/spec/parser.js +++ b/spec/parser.js @@ -1,6 +1,28 @@ const { JsonLdParser } = require(".."); +const { ContextParser, FetchDocumentLoader, ContextCache } = require("jsonld-context-parser"); const { ErrorSkipped } = require('rdf-test-suite'); + +const deepFreeze = obj => { + Object.keys(obj).forEach(prop => { + if (typeof obj[prop] === 'object' && !Object.isFrozen(obj[prop])) deepFreeze(obj[prop]); + }); + return Object.freeze(obj); +}; + +class FrozenContextParser extends ContextParser { + constructor(options) { + super(options); + } + + async parse(context, options) { + const parsed = await super.parse(context, options); + deepFreeze(parsed); + return parsed; + } +} + + module.exports = { parse: function (data, baseIRI, options) { if (options.processingMode && (options.processingMode !== '1.0' && options.processingMode !== '1.1')) { @@ -16,6 +38,10 @@ module.exports = { baseIRI, validateValueIndexes: true, normalizeLanguageTags: true, // To simplify testing + contextParser: new FrozenContextParser({ + documentLoader: new FetchDocumentLoader(), + contextCache: new ContextCache(), + }), }, options)))); }, }; diff --git a/test/JsonLdParser-test.ts b/test/JsonLdParser-test.ts index 567f84c..54e6388 100644 --- a/test/JsonLdParser-test.ts +++ b/test/JsonLdParser-test.ts @@ -1,16 +1,16 @@ -import {JsonLdParser} from "../index"; -import arrayifyStream from 'arrayify-stream'; -const streamifyString = require('streamify-string'); import * as RDF from "@rdfjs/types"; +import arrayifyStream from 'arrayify-stream'; import { EventEmitter } from 'events'; -import {DataFactory} from "rdf-data-factory"; import each from 'jest-each'; import "jest-rdf"; -import {ContextParser, ERROR_CODES, ErrorCoded, FetchDocumentLoader, IParseOptions, JsonLdContext, JsonLdContextNormalized} from "jsonld-context-parser"; -import {PassThrough} from "stream"; -import {Util} from "../lib/Util"; +import { ERROR_CODES, ErrorCoded, JsonLdContextNormalized, ContextParser, IParseOptions, JsonLdContext } from "jsonld-context-parser"; +import { DataFactory } from "rdf-data-factory"; +import { PassThrough } from "stream"; +import { JsonLdParser } from "../index"; import { ParsingContext } from '../lib/ParsingContext'; -import contexts, { MockedDocumentLoader } from '../mocks/contexts'; +import { Util } from "../lib/Util"; +import { MockedDocumentLoader } from '../mocks/contexts'; +const streamifyString = require('streamify-string'); const DF = new DataFactory(); @@ -21,13 +21,15 @@ const deepFreeze = obj => { return Object.freeze(obj); }; -class FrozenContextParser extends ContextParser { +export class FrozenContextParser extends ContextParser { constructor(options: ConstructorParameters[0]) { super(options); } - public parse(context: JsonLdContext, options?: IParseOptions): Promise { - return super.parse(context, options)// .then(deepFreeze); + public async parse(context: JsonLdContext, options?: IParseOptions): Promise { + const parsed = await super.parse(context, options); + deepFreeze(parsed); + return parsed; } } @@ -310,16 +312,20 @@ describe('JsonLdParser', () => { }); ( each ([ - [true], - [false], - ])).describe('when instantiated with a data factory and streamingProfile %s', (streamingProfile: boolean) => { + [true, true], + [false, true], + [true, false], + [false, false], + ])).describe('when instantiated with a data factory and streamingProfile %s and context caching %s', (streamingProfile: boolean, contextCache: boolean) => { // Enable the following instead if you want to run tests more conveniently with IDE integration /*describe('when instantiated with a data factory and streamingProfile %s', () => { const streamingProfile = true;*/ let parser: any; beforeEach(() => { - parser = new JsonLdParser({ dataFactory: DF, streamingProfile }); + parser = new JsonLdParser({ dataFactory: DF, streamingProfile, contextParser: new ContextParser({ + // contextCache: contextCache ? new ContextCache() : undefined, + }) }); }); describe('should parse', () => { @@ -11816,6 +11822,51 @@ describe('JsonLdParser', () => { ERROR_CODES.PROTECTED_TERM_REDEFINITION)); }); + it('should error on protected term overrides after a property scoped-context', async () => { + const stream = streamifyString(JSON.stringify({ + "@context": { + "@vocab": "http://example.com/", + "@version": 1.1, + "protected": { + "@protected": false + }, + "scope1": { + "@protected": false, + "@context": { + "protected": { + "@id": "http://example.com/something-else" + } + } + }, + "scope2": { + "@protected": true, + "@context": { + "protected": { + "@protected": true + } + } + } + }, + "protected": false, + "scope1": { + "@context": { + "protected": "http://example.com/another-thing" + }, + "protected": "property http://example.com/another-thing" + }, + "scope2": { + "@context": { + "protected": "http://example.com/another-thing" + }, + "protected": "error / property http://example.com/protected" + } + } + )); + return expect(arrayifyStream(stream.pipe(parser))).rejects.toThrow(new ErrorCoded( + 'Attempted to override the protected keyword protected from \"http://example.com/protected\" to \"http://example.com/another-thing\"', + ERROR_CODES.PROTECTED_TERM_REDEFINITION)); + }); + it('should not error on protected term, context null in a property scoped-context, and override', async () => { const stream = streamifyString(` { diff --git a/yarn.lock b/yarn.lock index 1fa95e5..b6f512c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2506,10 +2506,8 @@ jsonify@^0.0.1: resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.1.tgz#2aa3111dae3d34a0f151c63f3a45d995d9420978" integrity sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg== -jsonld-context-parser@^2.0.2, jsonld-context-parser@^2.1.3: - version "2.2.3" - resolved "https://registry.yarnpkg.com/jsonld-context-parser/-/jsonld-context-parser-2.2.3.tgz#51f1042dd1dbd75c9991ae70faf984048020ebe6" - integrity sha512-SMMS/CEKK9VSiKIpmaivVZVpHfC59qax6UM4hbWv5YRcyxx7kmzXWzM/LGxPAwxwx3nwbB644Xd+4443L7OW4g== +jsonld-context-parser@../jsonld-context-parser.js/: + version "2.3.3" dependencies: "@types/http-link-header" "^1.0.1" "@types/node" "^18.0.0" @@ -2518,10 +2516,10 @@ jsonld-context-parser@^2.0.2, jsonld-context-parser@^2.1.3: http-link-header "^1.0.2" relative-to-absolute-iri "^1.0.5" -jsonld-context-parser@^2.3.3: - version "2.3.3" - resolved "https://registry.yarnpkg.com/jsonld-context-parser/-/jsonld-context-parser-2.3.3.tgz#0bdab9eb5cb4b7e68aa7d6c38e58455363caaf9c" - integrity sha512-H+REInOx7XI2ciF8wJV31D20Bh+ofBmEjN2Tkds51vypqDJIiD341E5g+hYyrEInIKRnbW58TN/Ehz+ACT0l0w== +jsonld-context-parser@^2.0.2, jsonld-context-parser@^2.1.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/jsonld-context-parser/-/jsonld-context-parser-2.2.3.tgz#51f1042dd1dbd75c9991ae70faf984048020ebe6" + integrity sha512-SMMS/CEKK9VSiKIpmaivVZVpHfC59qax6UM4hbWv5YRcyxx7kmzXWzM/LGxPAwxwx3nwbB644Xd+4443L7OW4g== dependencies: "@types/http-link-header" "^1.0.1" "@types/node" "^18.0.0"