diff --git a/lib/loading/ComponentRegistry.ts b/lib/loading/ComponentRegistry.ts index 385fbe5..9a88f33 100644 --- a/lib/loading/ComponentRegistry.ts +++ b/lib/loading/ComponentRegistry.ts @@ -1,5 +1,6 @@ -import type { Readable } from 'stream'; +import type { Readable } from 'node:stream'; import type * as RDF from '@rdfjs/types'; +import type { ContextParser } from 'jsonld-context-parser'; import type { Resource, RdfObjectLoader } from 'rdf-object'; import type { Logger } from 'winston'; import { RdfParser } from '../rdf/RdfParser'; @@ -20,6 +21,7 @@ export class ComponentRegistry { private readonly componentResources: Record; private readonly skipContextValidation: boolean; private readonly remoteContextLookups: boolean; + private readonly contextParser?: ContextParser; public constructor(options: IComponentLoaderRegistryOptions) { this.moduleState = options.moduleState; @@ -28,6 +30,7 @@ export class ComponentRegistry { this.componentResources = options.componentResources; this.skipContextValidation = options.skipContextValidation; this.remoteContextLookups = options.remoteContextLookups; + this.contextParser = options.contextParser; } /** @@ -37,7 +40,7 @@ export class ComponentRegistry { */ public async registerAvailableModules(): Promise { await Promise.all(Object.values(this.moduleState.componentModules) - .flatMap(Object.values) + .flatMap(x => Object.values(x)) .map((moduleResourceUrl: string) => this.registerModule(moduleResourceUrl))); } @@ -54,6 +57,7 @@ export class ComponentRegistry { logger: this.logger, skipContextValidation: this.skipContextValidation, remoteContextLookups: this.remoteContextLookups, + contextParser: this.contextParser, })); } @@ -123,4 +127,8 @@ export interface IComponentLoaderRegistryOptions { componentResources: Record; skipContextValidation: boolean; remoteContextLookups: boolean; + /** + * An optional shared, cache-bearing JSON-LD context parser reused across all module files. + */ + contextParser?: ContextParser; } diff --git a/lib/loading/ComponentsManagerBuilder.ts b/lib/loading/ComponentsManagerBuilder.ts index ed3407f..6f85c0c 100644 --- a/lib/loading/ComponentsManagerBuilder.ts +++ b/lib/loading/ComponentsManagerBuilder.ts @@ -2,6 +2,9 @@ import type { Resource } from 'rdf-object'; import { RdfObjectLoader } from 'rdf-object'; import type { Logger } from 'winston'; import { createLogger, format, transports } from 'winston'; + +// eslint-disable-next-line import/extensions +import contextJson from '../../components/context.json'; import { ComponentsManager } from '../ComponentsManager'; import { ConfigConstructorPool } from '../construction/ConfigConstructorPool'; import type { IConfigConstructorPool } from '../construction/IConfigConstructorPool'; @@ -11,6 +14,7 @@ import { ConfigPreprocessorComponent } from '../preprocess/ConfigPreprocessorCom import { ConfigPreprocessorComponentMapped } from '../preprocess/ConfigPreprocessorComponentMapped'; import { ConfigPreprocessorOverride } from '../preprocess/ConfigPreprocessorOverride'; import { ParameterHandler } from '../preprocess/ParameterHandler'; +import { RdfParser } from '../rdf/RdfParser'; import type { LogLevel } from '../util/LogLevel'; import { ComponentRegistry } from './ComponentRegistry'; import { ComponentRegistryFinalizer } from './ComponentRegistryFinalizer'; @@ -21,11 +25,11 @@ import type { IModuleState } from './ModuleStateBuilder'; /** * Builds {@link ComponentsManager}'s based on given options. */ -export class ComponentsManagerBuilder { +export class ComponentsManagerBuilder { private readonly mainModulePath: string; private readonly componentLoader: (registry: ComponentRegistry) => Promise; private readonly configLoader: (registry: ConfigRegistry) => Promise; - private readonly constructionStrategy: IConstructionStrategy; + private readonly constructionStrategy: IConstructionStrategy; private readonly dumpErrorState: boolean; private readonly logger: Logger; private readonly moduleState?: IModuleState; @@ -33,13 +37,13 @@ export class ComponentsManagerBuilder { private readonly typeChecking: boolean; private readonly remoteContextLookups: boolean; - public constructor(options: IComponentsManagerBuilderOptions) { + public constructor(options: IComponentsManagerBuilderOptions) { this.mainModulePath = options.mainModulePath; - this.componentLoader = options.moduleLoader || (async registry => registry.registerAvailableModules()); - this.configLoader = options.configLoader || (async() => { + this.componentLoader = options.moduleLoader ?? (async registry => registry.registerAvailableModules()); + this.configLoader = options.configLoader ?? (async() => { // Do nothing }); - this.constructionStrategy = options.constructionStrategy || new ConstructionStrategyCommonJs({ req: require }); + this.constructionStrategy = options.constructionStrategy ?? new ConstructionStrategyCommonJs({ req: require }); this.dumpErrorState = options.dumpErrorState === undefined ? true : Boolean(options.dumpErrorState); this.logger = ComponentsManagerBuilder.createLogger(options.logLevel); this.moduleState = options.moduleState; @@ -73,14 +77,14 @@ export class ComponentsManagerBuilder { public static createObjectLoader(): RdfObjectLoader { return new RdfObjectLoader({ uniqueLiterals: true, - context: require('../../components/context.json'), + context: contextJson, }); } /** * @return A new instance of {@link ComponentsManager}. */ - public async build(): Promise> { + public async build(): Promise> { // Initialize module state let moduleState: IModuleState; if (this.moduleState) { @@ -95,6 +99,16 @@ export class ComponentsManagerBuilder { // Initialize object loader with built-in context const objectLoader: RdfObjectLoader = ComponentsManagerBuilder.createObjectLoader(); + // Create a single, cache-bearing JSON-LD context parser (with one shared prefetched document + // loader) that is reused across every component and config file. This avoids re-loading and + // re-normalizing the shared well-known @contexts once per file. + const contextParser = RdfParser.createSharedContextParser({ + contexts: moduleState.contexts, + logger: this.logger, + remoteContextLookups: this.remoteContextLookups, + skipContextValidation: this.skipContextValidation, + }); + // Load modules this.logger.info(`Initiating component loading`); const componentResources: Record = {}; @@ -105,6 +119,7 @@ export class ComponentsManagerBuilder { componentResources, skipContextValidation: this.skipContextValidation, remoteContextLookups: this.remoteContextLookups, + contextParser, }); await this.componentLoader(componentRegistry); const componentFinalizer = new ComponentRegistryFinalizer({ @@ -122,6 +137,7 @@ export class ComponentsManagerBuilder { logger: this.logger, skipContextValidation: this.skipContextValidation, remoteContextLookups: this.remoteContextLookups, + contextParser, }); await this.configLoader(configRegistry); this.logger.info(`Loaded configs`); @@ -129,7 +145,7 @@ export class ComponentsManagerBuilder { // Build constructor pool const runTypeConfigs = {}; const parameterHandler = new ParameterHandler({ objectLoader, typeChecking: this.typeChecking }); - const configConstructorPool: IConfigConstructorPool = new ConfigConstructorPool({ + const configConstructorPool: IConfigConstructorPool = new ConfigConstructorPool({ objectLoader, configPreprocessors: [ new ConfigPreprocessorOverride({ @@ -156,7 +172,7 @@ export class ComponentsManagerBuilder { moduleState, }); - return new ComponentsManager({ + return new ComponentsManager({ moduleState, objectLoader, componentResources, @@ -168,7 +184,7 @@ export class ComponentsManagerBuilder { } } -export interface IComponentsManagerBuilderOptions { +export interface IComponentsManagerBuilderOptions { /* ----- REQUIRED FIELDS ----- */ /** * Absolute path to the package root from which module resolution should start. @@ -192,7 +208,7 @@ export interface IComponentsManagerBuilderOptions { * A strategy for constructing instances. * Defaults to {@link ConstructionStrategyCommonJs}. */ - constructionStrategy?: IConstructionStrategy; + constructionStrategy?: IConstructionStrategy; /** * If the error state should be dumped into `componentsjs-error-state.json` * after failed instantiations. diff --git a/lib/loading/ConfigRegistry.ts b/lib/loading/ConfigRegistry.ts index 27cef6f..f239c76 100644 --- a/lib/loading/ConfigRegistry.ts +++ b/lib/loading/ConfigRegistry.ts @@ -1,5 +1,6 @@ -import type { Readable } from 'stream'; +import type { Readable } from 'node:stream'; import type * as RDF from '@rdfjs/types'; +import type { ContextParser } from 'jsonld-context-parser'; import type { RdfObjectLoader, Resource } from 'rdf-object'; import { termToString } from 'rdf-string'; import type { Logger } from 'winston'; @@ -15,6 +16,7 @@ export class ConfigRegistry { private readonly logger: Logger; private readonly skipContextValidation: boolean; private readonly remoteContextLookups: boolean; + private readonly contextParser?: ContextParser; public constructor(options: IConfigLoaderRegistryOptions) { this.moduleState = options.moduleState; @@ -22,6 +24,7 @@ export class ConfigRegistry { this.logger = options.logger; this.skipContextValidation = options.skipContextValidation; this.remoteContextLookups = options.remoteContextLookups; + this.contextParser = options.contextParser; } /** @@ -38,6 +41,7 @@ export class ConfigRegistry { logger: this.logger, skipContextValidation: this.skipContextValidation, remoteContextLookups: this.remoteContextLookups, + contextParser: this.contextParser, })); } @@ -62,6 +66,7 @@ export class ConfigRegistry { ): Promise { // Create ad-hoc resource const configResource = this.objectLoader.createCompactedResource({ + // eslint-disable-next-line ts/naming-convention '@id': configId, types: componentTypeIri, }); @@ -85,4 +90,8 @@ export interface IConfigLoaderRegistryOptions { logger: Logger; skipContextValidation: boolean; remoteContextLookups: boolean; + /** + * An optional shared, cache-bearing JSON-LD context parser reused across all config files. + */ + contextParser?: ContextParser; } diff --git a/lib/rdf/RdfParser.ts b/lib/rdf/RdfParser.ts index 7edc3af..bd952bf 100644 --- a/lib/rdf/RdfParser.ts +++ b/lib/rdf/RdfParser.ts @@ -1,18 +1,53 @@ -import { createReadStream } from 'fs'; -import type { Readable } from 'stream'; +import { createReadStream, promises as fs } from 'node:fs'; +import type { Readable } from 'node:stream'; import type * as RDF from '@rdfjs/types'; +import * as JsonLdContextParser from 'jsonld-context-parser'; +import { ContextParser } from 'jsonld-context-parser'; import type { ParseOptions } from 'rdf-parse'; -import rdfParser from 'rdf-parse'; +import { rdfParser } from 'rdf-parse'; import type { Logger } from 'winston'; import { PrefetchedDocumentLoader } from './PrefetchedDocumentLoader'; import { RdfStreamIncluder } from './RdfStreamIncluder'; -// Import syntax only works in Node > 12 -const fs = require('fs').promises; /** * Parses a data stream to a triple stream. */ export class RdfParser { + /** + * Create a single, cache-bearing JSON-LD context parser that can be reused across all files + * that are parsed during a component/config load. + * + * By default, Components.js constructs a fresh {@link PrefetchedDocumentLoader} and (indirectly, + * via jsonld-streaming-parser) a fresh `ContextParser` for every `.jsonld` file. As a result the + * well-known `@context`s (e.g. the shared `componentsjs` and `@solid/community-server` contexts) + * are re-loaded and re-normalized once per file - during a Community Solid Server boot the + * `componentsjs` context alone is normalized ~979 times. + * + * Passing the returned parser to every parse (see {@link RdfParserOptions#contextParser}) shares a + * single {@link PrefetchedDocumentLoader}, a single raw-document cache, and a single normalized + * {@link ContextCache} across all files, so the well-known contexts are loaded once and (once the + * shared cache can be hit) normalized once instead of once per file. + * + * @param options Options describing the prefetched contexts and validation behaviour. + */ + public static createSharedContextParser(options: ISharedContextParserOptions): ContextParser { + const documentLoader = new PrefetchedDocumentLoader({ + contexts: options.contexts ?? {}, + logger: options.logger, + remoteContextLookups: options.remoteContextLookups, + }); + const parserOptions: Record = { + documentLoader, + skipValidation: options.skipContextValidation, + }; + // Attach a shared normalized-context cache when the installed jsonld-context-parser exposes one + // (feature-detected so this remains compatible with versions that predate the cache). + if (( JsonLdContextParser).ContextCache) { + parserOptions.contextCache = new ( JsonLdContextParser).ContextCache(); + } + return new ContextParser(parserOptions); + } + /** * Parses the given stream into RDF quads. * @param textStream A text stream. @@ -20,7 +55,7 @@ export class RdfParser { */ public parse(textStream: NodeJS.ReadableStream, options: RdfParserOptions): RDF.Stream & Readable { // Parsing libraries don't work as expected if path contains backslashes - options.path = options.path.replace(/\\+/gu, '/'); + options.path = options.path.replaceAll(/\\+/gu, '/'); if (!options.baseIRI) { // Try converting path to URL using defined import paths @@ -44,10 +79,12 @@ export class RdfParser { } // Set JSON-LD parser options - ( options)['@comunica/actor-rdf-parse-jsonld:parserOptions'] = { - // Override the JSON-LD document loader + const jsonLdParserOptions: Record = { + // Override the JSON-LD document loader. + // This is always provided so that a jsonld-streaming-parser that does not (yet) support an + // injected context parser still uses the prefetched contexts and remote-lookup protection. documentLoader: new PrefetchedDocumentLoader({ - contexts: options.contexts || {}, + contexts: options.contexts ?? {}, logger: options.logger, path: options.path, remoteContextLookups: options.remoteContextLookups, @@ -57,6 +94,15 @@ export class RdfParser { // If JSON-LD context validation should be skipped skipContextValidation: options.skipContextValidation, }; + if (options.contextParser) { + // Reuse one shared, cache-bearing context parser (which carries its own single prefetched + // document loader) across all files, so the shared @contexts are only loaded/normalized once + // instead of once per file. This is honoured by a jsonld-streaming-parser that supports the + // `contextParser` option; on older versions the option is ignored and the per-file + // `documentLoader` above is used, preserving the previous behaviour. + jsonLdParserOptions.contextParser = options.contextParser; + } + ( options)['@comunica/actor-rdf-parse-jsonld:parserOptions'] = jsonLdParserOptions; // Execute parsing const quadStream = rdfParser.parse(textStream, options); @@ -131,4 +177,30 @@ export type RdfParserOptions = ParseOptions & { * If allowed, only a warning is emitted. */ remoteContextLookups?: boolean; + /** + * An optional shared JSON-LD context parser to reuse across all files. + * When provided, this (cache-bearing) parser and its prefetched document loader are used for all + * context resolution instead of constructing a new document loader (and context parser) per file. + * Create one with {@link RdfParser#createSharedContextParser}. + */ + contextParser?: ContextParser; }; + +export interface ISharedContextParserOptions { + /** + * The cached JSON-LD contexts (id -> context document). + */ + contexts?: Record; + /** + * An optional logger, used to warn on remote context lookups. + */ + logger?: Logger; + /** + * If remote context lookups are allowed. + */ + remoteContextLookups?: boolean; + /** + * If JSON-LD context validation should be skipped. + */ + skipContextValidation?: boolean; +} diff --git a/test/unit/rdf/RdfParser-createSharedContextParser-test.ts b/test/unit/rdf/RdfParser-createSharedContextParser-test.ts new file mode 100644 index 0000000..17b0caf --- /dev/null +++ b/test/unit/rdf/RdfParser-createSharedContextParser-test.ts @@ -0,0 +1,55 @@ +/** + * Isolated tests for {@link RdfParser#createSharedContextParser} that exercise the optional, + * feature-detected normalized-context cache. Only `ContextParser` (to capture its constructor + * arguments) and `ContextCache` are stubbed; all other jsonld-context-parser exports are kept real + * so the transitive rdf-parse/jsonld-streaming-parser imports still resolve. This lets both the + * cache-available and cache-unavailable code paths be covered regardless of the installed version. + */ +describe('RdfParser.createSharedContextParser', () => { + const contexts = { 'http://example.org/c.jsonld': { '@context': {}}}; + + afterEach(() => { + jest.resetModules(); + jest.dontMock('jsonld-context-parser'); + }); + + it('creates a context parser with a prefetched loader and no cache when unavailable', () => { + jest.isolateModules(() => { + const contextParserArgs: any[] = []; + jest.doMock('jsonld-context-parser', (): any => ({ + ...jest.requireActual('jsonld-context-parser'), + ContextParser: jest.fn((options: any) => contextParserArgs.push(options)), + ContextCache: undefined, + })); + const { RdfParser } = require('../../../lib/rdf/RdfParser'); + + RdfParser.createSharedContextParser({ contexts, skipContextValidation: true }); + + expect(contextParserArgs).toHaveLength(1); + expect(contextParserArgs[0].documentLoader).toBeDefined(); + expect(contextParserArgs[0].skipValidation).toBe(true); + expect(contextParserArgs[0].contextCache).toBeUndefined(); + }); + }); + + it('attaches a shared normalized-context cache when the parser exposes one', () => { + jest.isolateModules(() => { + const contextParserArgs: any[] = []; + const cacheInstances: any[] = []; + jest.doMock('jsonld-context-parser', (): any => ({ + ...jest.requireActual('jsonld-context-parser'), + ContextParser: jest.fn((options: any) => contextParserArgs.push(options)), + ContextCache: jest.fn(function fakeCache(this: any) { + cacheInstances.push(this); + }), + })); + const { RdfParser } = require('../../../lib/rdf/RdfParser'); + + RdfParser.createSharedContextParser({ contexts }); + + expect(contextParserArgs).toHaveLength(1); + expect(cacheInstances).toHaveLength(1); + expect(contextParserArgs[0].contextCache).toBe(cacheInstances[0]); + }); + }); +}); diff --git a/test/unit/rdf/RdfParser-test.ts b/test/unit/rdf/RdfParser-test.ts index ac6ae3d..782cdad 100644 --- a/test/unit/rdf/RdfParser-test.ts +++ b/test/unit/rdf/RdfParser-test.ts @@ -1,14 +1,15 @@ -import * as Path from 'path'; +import * as Path from 'node:path'; import type { RdfParserOptions } from '../../../lib/rdf/RdfParser'; import { RdfParser } from '../../../lib/rdf/RdfParser'; import 'jest-rdf'; + // Import syntax only works in Node > 12 const arrayifyStream = require('arrayify-stream'); const quad = require('rdf-quad'); const stringifyStream = require('stream-to-string'); const streamifyString = require('streamify-string'); -global.fetch = jest.fn(async(url: string) => { +globalThis.fetch = jest.fn(async(url: string) => { if (url === 'http://example.org/myfile1.ttl') { return { body: streamifyString(` .`), @@ -71,7 +72,7 @@ describe('RdfParser', () => { const options: RdfParserOptions = { path: 'path/to/file.ttl', }; - expect(await arrayifyStream(parser.parse(streamifyString(``), options))) + await expect(arrayifyStream(parser.parse(streamifyString(``), options))).resolves .toEqual([]); }); @@ -79,7 +80,7 @@ describe('RdfParser', () => { const options: RdfParserOptions = { path: 'path/to/file.ttl', }; - expect(await arrayifyStream(parser.parse(streamifyString(` .`), options))) + await expect(arrayifyStream(parser.parse(streamifyString(` .`), options))).resolves .toBeRdfIsomorphic([ quad('ex:s', 'ex:p', 'ex:o'), ]); @@ -89,7 +90,7 @@ describe('RdfParser', () => { const options: RdfParserOptions = { path: 'path/to/file.ttl', }; - expect(await arrayifyStream(parser.parse(streamifyString(` .`), options))) + await expect(arrayifyStream(parser.parse(streamifyString(` .`), options))).resolves .toBeRdfIsomorphic([ quad('file://path/to/s', 'ex:p', 'ex:o'), ]); @@ -99,7 +100,7 @@ describe('RdfParser', () => { const options: RdfParserOptions = { path: 'path/./to/file.ttl', }; - expect(await arrayifyStream(parser.parse(streamifyString(` .`), options))) + await expect(arrayifyStream(parser.parse(streamifyString(` .`), options))).resolves .toBeRdfIsomorphic([ quad('file://path/to/s', 'ex:p', 'ex:o'), ]); @@ -109,7 +110,7 @@ describe('RdfParser', () => { const options: RdfParserOptions = { path: '/path/to/file.ttl', }; - expect(await arrayifyStream(parser.parse(streamifyString(` .`), options))) + await expect(arrayifyStream(parser.parse(streamifyString(` .`), options))).resolves .toBeRdfIsomorphic([ quad('file:///path/to/s', 'ex:p', 'ex:o'), ]); @@ -119,7 +120,7 @@ describe('RdfParser', () => { const options: RdfParserOptions = { path: 'C:/path/to/file.ttl', }; - expect(await arrayifyStream(parser.parse(streamifyString(` .`), options))) + await expect(arrayifyStream(parser.parse(streamifyString(` .`), options))).resolves .toBeRdfIsomorphic([ quad('file://C:/path/to/s', 'ex:p', 'ex:o'), ]); @@ -129,7 +130,7 @@ describe('RdfParser', () => { const options: RdfParserOptions = { path: 'http://example.org/file.ttl', }; - expect(await arrayifyStream(parser.parse(streamifyString(` .`), options))) + await expect(arrayifyStream(parser.parse(streamifyString(` .`), options))).resolves .toBeRdfIsomorphic([ quad('http://example.org/s', 'ex:p', 'ex:o'), ]); @@ -140,7 +141,7 @@ describe('RdfParser', () => { path: 'path/to/file.ttl', baseIRI: 'http://base.org/', }; - expect(await arrayifyStream(parser.parse(streamifyString(` .`), options))) + await expect(arrayifyStream(parser.parse(streamifyString(` .`), options))).resolves .toBeRdfIsomorphic([ quad('http://base.org/s', 'ex:p', 'ex:o'), ]); @@ -154,7 +155,7 @@ describe('RdfParser', () => { "@id": "ex:s", "ex:p": { "@id": "ex:o" } }`; - expect(await arrayifyStream(parser.parse(streamifyString(doc), options))) + await expect(arrayifyStream(parser.parse(streamifyString(doc), options))).resolves .toBeRdfIsomorphic([ quad('ex:s', 'ex:p', 'ex:o'), ]); @@ -176,7 +177,7 @@ describe('RdfParser', () => { "@id": "ex:s", "p": { "@id": "ex:o" } }`; - expect(await arrayifyStream(parser.parse(streamifyString(doc), options))) + await expect(arrayifyStream(parser.parse(streamifyString(doc), options))).resolves .toBeRdfIsomorphic([ quad('ex:s', 'http://vocab.org/p', 'ex:o'), ]); @@ -194,6 +195,29 @@ describe('RdfParser', () => { .toThrow(new Error(`Error while parsing file "path/to/file.jsonld": Invalid predicate IRI: p`)); }); + it('for JSON-LD streams reusing a shared context parser', async() => { + const contexts = { + 'http://example.org/context.jsonld': { + '@context': { + '@vocab': 'http://vocab.org/', + }, + }, + }; + const contextParser = RdfParser.createSharedContextParser({ contexts, skipContextValidation: true }); + const doc = `{ + "@context": "http://example.org/context.jsonld", + "@id": "ex:s", + "p": { "@id": "ex:o" } +}`; + // Parse two separate files reusing the same context parser (as the loaders do per-boot). + for (const path of [ 'path/to/file1.jsonld', 'path/to/file2.jsonld' ]) { + await expect(arrayifyStream(new RdfParser().parse(streamifyString(doc), { path, contexts, contextParser }))) + .resolves.toBeRdfIsomorphic([ + quad('ex:s', 'http://vocab.org/p', 'ex:o'), + ]); + } + }); + it('for a Turtle stream with imports', async() => { const options: RdfParserOptions = { path: 'path/to/file.ttl', @@ -202,7 +226,7 @@ describe('RdfParser', () => { . , . `; - expect(await arrayifyStream(parser.parse(streamifyString(doc), options))) + await expect(arrayifyStream(parser.parse(streamifyString(doc), options))).resolves .toBeRdfIsomorphic([ quad('ex:s', 'ex:p', 'ex:o'), quad('ex:s', 'http://www.w3.org/2000/01/rdf-schema#seeAlso', 'http://example.org/myfile1.ttl'), @@ -220,7 +244,7 @@ describe('RdfParser', () => { . , . `; - expect(await arrayifyStream(parser.parse(streamifyString(doc), options))) + await expect(arrayifyStream(parser.parse(streamifyString(doc), options))).resolves .toBeRdfIsomorphic([ quad('ex:s', 'ex:p', 'ex:o'), quad('ex:s', 'http://www.w3.org/2000/01/rdf-schema#seeAlso', 'http://example.org/myfile1%2Ettl'), @@ -286,7 +310,7 @@ describe('RdfParser', () => { . . `; - expect(await arrayifyStream(parser.parse(streamifyString(doc), options))) + await expect(arrayifyStream(parser.parse(streamifyString(doc), options))).resolves .toBeRdfIsomorphic([ quad('ex:s', 'ex:p', 'ex:o'), quad('ex:s', 'http://www.w3.org/2000/01/rdf-schema#seeAlso', 'http://example.org/myfilenest.ttl'), @@ -304,7 +328,7 @@ describe('RdfParser', () => { . , . `; - expect(await arrayifyStream(parser.parse(streamifyString(doc), options))) + await expect(arrayifyStream(parser.parse(streamifyString(doc), options))).resolves .toBeRdfIsomorphic([ quad('ex:s', 'ex:p', 'ex:o'), quad('ex:s', 'http://www.w3.org/2000/01/rdf-schema#seeAlso', 'http://example.org/myfile1.ttl'), @@ -323,7 +347,7 @@ describe('RdfParser', () => { . , . `; - expect(await arrayifyStream(parser.parse(streamifyString(doc), options))) + await expect(arrayifyStream(parser.parse(streamifyString(doc), options))).resolves .toBeRdfIsomorphic([ quad('ex:s', 'ex:p', 'ex:o'), quad('ex:s', 'http://www.w3.org/2000/01/rdf-schema#seeAlso', 'http://example.org/myfile1.ttl'), @@ -344,7 +368,7 @@ describe('RdfParser', () => { . , . `; - expect(await arrayifyStream(parser.parse(streamifyString(doc), options))) + await expect(arrayifyStream(parser.parse(streamifyString(doc), options))).resolves .toBeRdfIsomorphic([ quad('ex:s', 'ex:p', 'ex:o'), quad('ex:s', 'http://www.w3.org/2000/01/rdf-schema#seeAlso', 'http://example.org/a/myfile1.ttl'), @@ -366,7 +390,7 @@ describe('RdfParser', () => { . `; await expect(arrayifyStream(parser.parse(streamifyString(doc), options))) - .rejects.toThrowError(/^Error while parsing file/u); + .rejects.toThrow(/^Error while parsing file/u); }); it('for a Turtle stream with imports, with import path to erroring file', async() => { @@ -381,7 +405,7 @@ describe('RdfParser', () => { . `; await expect(arrayifyStream(parser.parse(streamifyString(doc), options))) - .rejects.toThrowError(/^Error while parsing file/u); + .rejects.toThrow(/^Error while parsing file/u); }); it('for a Turtle stream with invalid IRI should produce logger warnings', async() => { @@ -392,7 +416,7 @@ describe('RdfParser', () => { path: 'path/to/file.ttl', logger, }; - expect(await arrayifyStream(parser.parse(streamifyString(` .`), options))) + await expect(arrayifyStream(parser.parse(streamifyString(` .`), options))).resolves .toBeRdfIsomorphic([ quad('ex:s', 'ex:p', 'ex:o'), ]); @@ -410,7 +434,7 @@ describe('RdfParser', () => { path: 'path/to/file.ttl', logger, }; - expect(await arrayifyStream(parser.parse(streamifyString(`

.`), options))) + await expect(arrayifyStream(parser.parse(streamifyString(`

.`), options))).resolves .toBeRdfIsomorphic([ quad('file://path/to/s', 'file://path/to/p', 'file://path/to/o'), ]); @@ -428,7 +452,7 @@ describe('RdfParser', () => { }, logger, }; - expect(await arrayifyStream(parser.parse(streamifyString(`

.`), options))) + await expect(arrayifyStream(parser.parse(streamifyString(`

.`), options))).resolves .toBeRdfIsomorphic([ quad('http://example.org/s', 'http://example.org/p', 'http://example.org/o'), ]); @@ -438,30 +462,31 @@ describe('RdfParser', () => { describe('fetchFileOrUrl', () => { it('for a URL', async() => { - expect(await stringifyStream(await RdfParser.fetchFileOrUrl('http://example.org/myfile1.ttl'))) - .toEqual(` .`); + await expect(stringifyStream(await RdfParser.fetchFileOrUrl('http://example.org/myfile1.ttl'))).resolves + .toBe(` .`); }); it('for a file without protocol', async() => { - expect(await stringifyStream(await RdfParser.fetchFileOrUrl(Path.join(__dirname, '../assets/rdf/a/myfile1.ttl')))) - .toEqual(` . + const filePath = Path.join(__dirname, '../assets/rdf/a/myfile1.ttl'); + await expect(stringifyStream(await RdfParser.fetchFileOrUrl(filePath))).resolves + .toBe(` . `); }); it('for a file with protocol', async() => { - expect(await stringifyStream(await RdfParser.fetchFileOrUrl(`file://${Path.join(__dirname, '../assets/rdf/a/myfile1.ttl')}`))) - .toEqual(` . + await expect(stringifyStream(await RdfParser.fetchFileOrUrl(`file://${Path.join(__dirname, '../assets/rdf/a/myfile1.ttl')}`))).resolves + .toBe(` . `); }); it('for a non-existing file without protocol', async() => { await expect(RdfParser.fetchFileOrUrl(Path.join(__dirname, '../assets/rdf/a/myfileunknown.ttl'))) - .rejects.toThrowError(/^ENOENT/u); + .rejects.toThrow(/^ENOENT/u); }); it('for a folder without protocol', async() => { await expect(RdfParser.fetchFileOrUrl(Path.join(__dirname, '../assets/rdf/a/'))) - .rejects.toThrowError(/^Path does not refer to a valid file/u); + .rejects.toThrow(/^Path does not refer to a valid file/u); }); }); });