diff --git a/CHANGELOG.md b/CHANGELOG.md index 7549a31..2e91c2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `getOperationsToImplementOrDeclareFromInterfacesWithoutParents`. - `getOperationsToImplementOrDeclare`. +TypeScript: +- Method Observers + - `connector.subscribe('export', observer: Observer)` + - `connector.subscribe('import', observer: Observer)` +- Released versions 1.0.0-alpha.10, 1.0.0-alpha-11 + ### Changed TypeScript: - Ensure the entry point, `src/index.ts`, is always regenerated on fresh builds. diff --git a/src/org/datafoodconsortium/connector/codegen/typescript/static/CHANGELOG.md b/src/org/datafoodconsortium/connector/codegen/typescript/static/CHANGELOG.md index 50eb440..7b4fd5a 100644 --- a/src/org/datafoodconsortium/connector/codegen/typescript/static/CHANGELOG.md +++ b/src/org/datafoodconsortium/connector/codegen/typescript/static/CHANGELOG.md @@ -12,6 +12,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - None +## [1.0.0-alpha.11] - 2025-05-01 + +### Added +- Method Observers + - `connector.subscribe('export', observer: Observer)` + - `connector.subscribe('import', observer: Observer)` + ## [1.0.0-alpha.10] - 2025-04-28 ### Changed diff --git a/src/org/datafoodconsortium/connector/codegen/typescript/static/README.md b/src/org/datafoodconsortium/connector/codegen/typescript/static/README.md index 8001b94..7752a21 100644 --- a/src/org/datafoodconsortium/connector/codegen/typescript/static/README.md +++ b/src/org/datafoodconsortium/connector/codegen/typescript/static/README.md @@ -292,6 +292,110 @@ const order: string = "http://www.datafoodconsortium.org/ontologies/DFC_Business const IOrder | undefined = await connector.importOneTyped(jsonAsAString, { only: order }); ``` +## Method Observers +Certain methods permit observers to listen events and perform side effects +whenever those methods are called. For instance, you can subscribe to all +`"export"` events by registering an object with three methods: a `.next()` +method, which fires each time `connector.export()` is invoked and receives +as its single argument the same JSON payload returned by the export method; an +optional `.error()` method, in case the export fails; and an optional +`.complete()` method, which is called without any arguments whenever the +observer unsubscribes or in the case that the Connector cancels it for some +reason. + +```js +const exportedJson = new Map(); +const exportObserver = { + next(json) { + exportedJson.set(Date.now(), json); + }, + error(e) { console.error(e.message); }, + complete() { + console.log('Total exports:', exportedJson.size); + }, +} +const subscription = connector.subscribe('export', exportObserver); + +connector.export(someDataSet); // exportedJson.size => 1 +connector.export(anotherDataSet); // exportedJson.size => 2 +subscription.unsubscribe(); // CONSOLE: 'Total exports: 2' +``` + +The following string values can be passed as the first argument to +`Connector.subscribe()`, supporting the corresponding methods/events: + +- `"export"` +- `"import"` + +An observer can also implement the `Observer` interface as a class for more +nuanced, stateful behavior, and so thatmultiple instantiations can each +subscribe and unsubscribe to events separately: + +```ts +class ImportObserver implements Observer { + type: string; + baseUrl: URL; + noisy = false; + #imported: QuadExt[] = []; + + constructor(semType: string|Semanticable, baseUrl: string, noisy?: boolean) { + if (typeof semType === 'string') this.type = semType; + else this.type = semType.getSemanticType(); + this.baseUrl = new URL(baseUrl); + if (typeof noisy === "boolean") this.noisy = noisy; + } + + next(datasets: DatasetExt[]) { + const count = datasets.reduce((i, dSet) => { + return i + dSet.reduce((j, quad) => { + if (quad.subject.termType !== this.type) return j; + this.#imported.push(quad); + return j + 1; + }, 0); + }, 0); + if (this.noisy && count > 0) { + console.log(`Imported ${this.type} ${count}x.`); + } + } + + error(e: Error) { + if (this.noisy) console.error('Whoops!', e.message); + } + + complete() { + this.#imported.forEach((quad) => { + const opts = { method: 'POST', body: JSON.stringify(quad)}; + fetch(`${this.baseUrl}/${this.type}`, opts); + }); + if (this.noisy) { + const total = this.#imported.length; + const msg = this.noisy && total > 0 + ? `Imported ${this.type} a total of ${total} times! 🙌` + : `Didn't import a single ${this.type}! 😭`; + console.log(msg); + } + } +} +``` + +Multiple observers can also subscribe to the same method/event simultaneously to +to control their behaviors and attributes separately: + +```js +// Use the ImportObserver above to watch for different data types. +const api = 'https://api.example.net/v1/'; +const addrObs = new ImportObserver('dfc-b:Address', api, true); +const catalogObs = new ImportObserver('"dfc-b:CatalogItem"', api); +const catalogSub = connector.subscribe('import', catalogObs); +const addrSubAPIv1 = connector.subscribe('import', addrObs); + +// Add new observers with different behaviors or modify others. +const api2 = 'https://api.example.net/v2/'; +const addrObsAPIv2 = new ImportObserver('dfc-b:Address', api2); +const addrSubAPIv2 = connector.subscribe('import', addrObsAPIv2); +addrObsAPIv1.noisy = false; +``` + ## Configure You can adapt different components of the connector to your needs with the following connector methods: diff --git a/src/org/datafoodconsortium/connector/codegen/typescript/static/package-lock.json b/src/org/datafoodconsortium/connector/codegen/typescript/static/package-lock.json index 69d1e54..23f2ec5 100644 --- a/src/org/datafoodconsortium/connector/codegen/typescript/static/package-lock.json +++ b/src/org/datafoodconsortium/connector/codegen/typescript/static/package-lock.json @@ -1,12 +1,12 @@ { "name": "@datafoodconsortium/connector", - "version": "1.0.0-alpha.10", + "version": "1.0.0-alpha.11", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@datafoodconsortium/connector", - "version": "1.0.0-alpha.10", + "version": "1.0.0-alpha.11", "license": "MIT", "dependencies": { "@rdfjs/serializer-jsonld-ext": "^4.0.0", @@ -1180,9 +1180,10 @@ } }, "node_modules/undici": { - "version": "5.28.5", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.5.tgz", - "integrity": "sha512-zICwjrDrcrUE0pyyJc1I2QzBkLM8FINsgOrt6WjA+BgajVq9Nxu2PbFFXUrAggLfDXlZGZBVZYw7WNV5KiBiBA==", + "version": "5.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz", + "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==", + "license": "MIT", "dependencies": { "@fastify/busboy": "^2.0.0" }, @@ -1190,11 +1191,6 @@ "node": ">=14.0" } }, - "node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==" - }, "node_modules/validate-iri": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/validate-iri/-/validate-iri-1.0.1.tgz", diff --git a/src/org/datafoodconsortium/connector/codegen/typescript/static/package.json b/src/org/datafoodconsortium/connector/codegen/typescript/static/package.json index 9bf6dfa..b0c1036 100644 --- a/src/org/datafoodconsortium/connector/codegen/typescript/static/package.json +++ b/src/org/datafoodconsortium/connector/codegen/typescript/static/package.json @@ -4,10 +4,10 @@ "author": "Maxime Lecoq", "license": "MIT", "type": "module", - "version": "1.0.0-alpha.10", + "version": "1.0.0-alpha.11", "repository": { "type": "git", - "url": "https://github.com/datafoodconsortium/connector-typescript.git", + "url": "git+https://github.com/datafoodconsortium/connector-typescript.git", "directory": "/" }, "scripts": { diff --git a/src/org/datafoodconsortium/connector/codegen/typescript/static/src/Connector.ts b/src/org/datafoodconsortium/connector/codegen/typescript/static/src/Connector.ts index 7a3d659..d4a41cf 100644 --- a/src/org/datafoodconsortium/connector/codegen/typescript/static/src/Connector.ts +++ b/src/org/datafoodconsortium/connector/codegen/typescript/static/src/Connector.ts @@ -16,6 +16,7 @@ import IConnectorImporter from "./IConnectorImporter"; import IConnectorImportOptions from "./IConnectorImportOptions.js"; import IConnectorStore from "./IConnectorStore"; import IGetterOptions from "./IGetterOptions.js"; +import type { Observer } from "./observer.js"; // Generated Code import IAddress from "./IAddress.js"; @@ -41,12 +42,24 @@ import IPlannedConsumptionFlow from "./IPlannedConsumptionFlow.js"; import IPlannedProductionFlow from "./IPlannedProductionFlow.js"; import IDefinedProduct from "./IDefinedProduct.js"; +// The keys map to the name of the method to the private member where the +// corresponding observable object is actually stored. +const ConnectorObservables = { + export: "exporter", + import: "importer", +} as const; +type ConnectorObservables = typeof ConnectorObservables; +type ConnectorObservableKeys = keyof ConnectorObservables; +type ConnectorObservableMethods = ConnectorObservables[ConnectorObservableKeys]; +type ConnectorObservableStrings = ConnectorObservableKeys | ConnectorObservableMethods; + export default class Connector implements IConnector { public FACETS?: ISKOSConcept; public MEASURES?: ISKOSConcept; public PRODUCT_TYPES?: ISKOSConcept; public VOCABULARY?: ISKOSConcept; + public OBSERVABLES = ConnectorObservables; private semantizer: ISemantizer; private fetchFunction: (semanticId: string) => Promise; @@ -195,6 +208,13 @@ export default class Connector implements IConnector { return this.factory; } + public subscribe(event: ConnectorObservableStrings, observer: Observer) { + const observable = event in this.OBSERVABLES + ? this.OBSERVABLES[event as ConnectorObservableKeys] + : event as ConnectorObservableMethods; + return this[observable].subscribe(observer); + } + public async import(data: string, options?: IConnectorImportOptions): Promise> { return new Promise(async (resolve, reject) => { try { diff --git a/src/org/datafoodconsortium/connector/codegen/typescript/static/src/ConnectorExporterJsonldStream.ts b/src/org/datafoodconsortium/connector/codegen/typescript/static/src/ConnectorExporterJsonldStream.ts index 0e61eb8..62c1336 100644 --- a/src/org/datafoodconsortium/connector/codegen/typescript/static/src/ConnectorExporterJsonldStream.ts +++ b/src/org/datafoodconsortium/connector/codegen/typescript/static/src/ConnectorExporterJsonldStream.ts @@ -6,13 +6,16 @@ import { Readable } from 'readable-stream'; import IConnectorExporter from "./IConnectorExporter"; import IConnectorExporterOptions from "./IConnectorExporterOptions"; +import IConnectorExportObserver from "./IConnectorExportObserver"; +import { Observable } from "./observer.js"; -export default class ConnectorExporterJsonldStream implements IConnectorExporter { +export default class ConnectorExporterJsonldStream extends Observable implements IConnectorExporter { private context?: ContextDefinition; private outputContext?: any; public constructor(context?: ContextDefinition, outputContext?: any) { + super(); this.context = context; this.outputContext = outputContext; } @@ -38,7 +41,11 @@ export default class ConnectorExporterJsonldStream implements IConnectorExporter if (outputContext) { json["@context"] = outputContext; } - resolve(JSON.stringify(json)); + const serialized = JSON.stringify(json); + this.subscribers.forEach((observer: IConnectorExportObserver) => { + observer.next(serialized); + }); + resolve(serialized); }); }); } diff --git a/src/org/datafoodconsortium/connector/codegen/typescript/static/src/ConnectorImporterJsonldStream.ts b/src/org/datafoodconsortium/connector/codegen/typescript/static/src/ConnectorImporterJsonldStream.ts index 5acdfa5..16de1ed 100644 --- a/src/org/datafoodconsortium/connector/codegen/typescript/static/src/ConnectorImporterJsonldStream.ts +++ b/src/org/datafoodconsortium/connector/codegen/typescript/static/src/ConnectorImporterJsonldStream.ts @@ -6,14 +6,17 @@ import QuadExt from "rdf-ext/lib/Quad"; import IConnectorImporter from "./IConnectorImporter"; import IConnectorImporterOptions from "./IConnectorImporterOptions"; +import IConnectorImportObserver from "./IConnectorImportObserver"; +import { Observable } from "./observer.js"; -export default class ConnectorImporterJsonldStream implements IConnectorImporter { +export default class ConnectorImporterJsonldStream extends Observable implements IConnectorImporter { private context: string | undefined; private documentLoader: any; // TODO: add the optional parameters of the JsonLdParser class. public constructor(parameters?: { context?: any, documentLoader?: any }) { + super(); this.context = parameters?.context; this.documentLoader = parameters?.documentLoader; } @@ -112,6 +115,9 @@ export default class ConnectorImporterJsonldStream implements IConnectorImporter datasets.push(blankNodes[0]); } + this.subscribers.forEach((observer: IConnectorImportObserver) => { + observer.next(datasets); + }); resolve(datasets); }); }); diff --git a/src/org/datafoodconsortium/connector/codegen/typescript/static/src/IConnectorExportObserver.ts b/src/org/datafoodconsortium/connector/codegen/typescript/static/src/IConnectorExportObserver.ts new file mode 100644 index 0000000..ec784ad --- /dev/null +++ b/src/org/datafoodconsortium/connector/codegen/typescript/static/src/IConnectorExportObserver.ts @@ -0,0 +1,5 @@ +import type { Observer } from './observer'; + +export default interface IConnectorExportObserver extends Observer { + next(json: string): void; +} diff --git a/src/org/datafoodconsortium/connector/codegen/typescript/static/src/IConnectorExporter.ts b/src/org/datafoodconsortium/connector/codegen/typescript/static/src/IConnectorExporter.ts index f26b10b..831d58b 100644 --- a/src/org/datafoodconsortium/connector/codegen/typescript/static/src/IConnectorExporter.ts +++ b/src/org/datafoodconsortium/connector/codegen/typescript/static/src/IConnectorExporter.ts @@ -1,8 +1,11 @@ import { Semanticable } from "@virtual-assembly/semantizer"; import IConnectorExporterOptions from "./IConnectorExporterOptions"; +import { Observable, Subscription } from "./observer"; +import IConnectorExportObserver from "./IConnectorExportObserver"; -export default interface IConnectorExporter { +export default interface IConnectorExporter extends Observable { export(semanticObjets: Array, options?: IConnectorExporterOptions): Promise; - + + subscribe(observer: IConnectorExportObserver): Subscription; } \ No newline at end of file diff --git a/src/org/datafoodconsortium/connector/codegen/typescript/static/src/IConnectorImportObserver.ts b/src/org/datafoodconsortium/connector/codegen/typescript/static/src/IConnectorImportObserver.ts new file mode 100644 index 0000000..d278dac --- /dev/null +++ b/src/org/datafoodconsortium/connector/codegen/typescript/static/src/IConnectorImportObserver.ts @@ -0,0 +1,6 @@ +import type DatasetExt from "rdf-ext/lib/Dataset"; +import type { Observer } from './observer'; + +export default interface IConnectorImportObserver extends Observer { + next(datasets: DatasetExt[]): void; +} diff --git a/src/org/datafoodconsortium/connector/codegen/typescript/static/src/IConnectorImporter.ts b/src/org/datafoodconsortium/connector/codegen/typescript/static/src/IConnectorImporter.ts index 3918af2..8af9105 100644 --- a/src/org/datafoodconsortium/connector/codegen/typescript/static/src/IConnectorImporter.ts +++ b/src/org/datafoodconsortium/connector/codegen/typescript/static/src/IConnectorImporter.ts @@ -1,8 +1,11 @@ import DatasetExt from "rdf-ext/lib/Dataset"; import IConnectorImporterOptions from "./IConnectorImporterOptions"; +import { Observable, Subscription } from "./observer"; +import IConnectorImportObserver from "./IConnectorImportObserver"; -export default interface IConnectorImporter { +export default interface IConnectorImporter extends Observable { import(data: string, options?: IConnectorImporterOptions): Promise>; - + + subscribe(observer: IConnectorImportObserver): Subscription; } \ No newline at end of file diff --git a/src/org/datafoodconsortium/connector/codegen/typescript/static/src/observer.ts b/src/org/datafoodconsortium/connector/codegen/typescript/static/src/observer.ts new file mode 100644 index 0000000..4170faa --- /dev/null +++ b/src/org/datafoodconsortium/connector/codegen/typescript/static/src/observer.ts @@ -0,0 +1,125 @@ +/** + * OBSERVERS & OBSERVABLES + * For adding behaviors and side effects to the Connectors core methods. Unlike + * the PHP version, which uses an OOP-style Observer Pattern, this hews more + * to the reactive programming model, which is more prevalent in TypeScript. + * Roughly based on the RxJS v8 implementation, but greatly simplified: + * {@link https://rxjs.dev/guide/overview} + * {@link https://github.com/ReactiveX/rxjs/blob/8.0.0-alpha.14/packages/rxjs/src/internal/types.ts} + */ + +/** + * OBSERVER + * {@link https://rxjs.dev/api/index/interface/Observer} + */ +export interface Observer { + next(value: T): void; + error(error: any): void; + complete(): void; +} + +/** + * UNSUBSCRIBABLE + * {@link https://rxjs.dev/api/index/interface/Unsubscribable} + */ +export interface Unsubscribable { + closed: boolean; + unsubscribe(): void; +} + +/** + * SUBSCRIBABLE + * {@link https://rxjs.dev/api/index/interface/Subscribable} + */ +export interface Subscribable { + subscribe(observer: Partial>): Unsubscribable +} + +/** + * TEARDOWN + * {@link https://rxjs.dev/api/index/type-alias/TeardownLogic} + */ +export type Teardown = Subscription | Unsubscribable | (() => void) | void; + +/** + * SUBSCRIPTION-LIKE + */ +export interface SubscriptionLike extends Unsubscribable { + unsubscribe(): void; + readonly closed: boolean; +} + +/** + * SUBSCRIPTION + * This is mainly for internal use by Observable, based on RxJS but simplified: + * {@link https://rxjs.dev/api/index/class/Subscription} + * For the complete implementation: + * {@link https://github.com/ReactiveX/rxjs/blob/8.0.0-alpha.14/packages/rxjs/src/internal/Observable.ts#L49-L179} + */ +export class Subscription implements SubscriptionLike { + public closed = false; + private finalizers: Set | null = null; + + constructor(private initialTeardown?: () => void) {} + + public unsubscribe(): void { + if (!this.closed) { + this.closed = true; + const { finalizers, initialTeardown } = this; + this.finalizers = new Set(); + if (typeof initialTeardown === 'function') initialTeardown(); + if (finalizers) finalizers.forEach((teardown: Teardown) => { + if (typeof teardown === 'function') teardown(); + else if (teardown) teardown.unsubscribe(); + }) + } + } +} + +/** + * OBSERVABLE + * For adding behaviors and side effects to the Connectors core methods, using + * the observer pattern. This is roughly based on the RxJS v8 implementation, + * but greatly simplified: + * {@link https://rxjs.dev/api/index/class/Observable} + * For the full implementation: + * {@link https://github.com/ReactiveX/rxjs/blob/8.0.0-alpha.14/packages/rxjs/src/internal/Observable.ts#L551-L1008} + */ +export class Observable implements Subscribable { + protected subscribers: Set> = new Set(); + protected finalizers: WeakMap, Teardown> = new WeakMap(); + private _subscribe(_subscriber: Observer): Teardown|void { + return; + } + + constructor( + subscribe?: (this: Observable, subscriber: Observer) => Teardown|void, + ) { + if (subscribe) this._subscribe = subscribe; + } + + public subscribe( + observerOrNext?: Partial> | ((value: T) => void), + error?: (error: any) => void, + complete?: () => void, + ): Subscription { + let observer: Observer; + if (typeof observerOrNext === 'function') { + observer = { + next: observerOrNext, + error: typeof error === 'function' ? error : (_: any) => {}, + complete: typeof complete === 'function' ? complete : () => {}, + }; + } else { + observer = observerOrNext as Observer; + } + this.subscribers.add(observer); + const finalizer: Teardown | void = this._subscribe(observer); + if (typeof finalizer === 'function') { + const subscription = new Subscription(finalizer); + this.finalizers.set(observer, finalizer); + return subscription; + } + return new Subscription(); + } +} diff --git a/src/org/datafoodconsortium/connector/codegen/typescript/static/test/Address.test.js b/src/org/datafoodconsortium/connector/codegen/typescript/static/test/Address.test.js index ae47a30..cca3271 100644 --- a/src/org/datafoodconsortium/connector/codegen/typescript/static/test/Address.test.js +++ b/src/org/datafoodconsortium/connector/codegen/typescript/static/test/Address.test.js @@ -1,6 +1,7 @@ import expect from 'node:assert'; import { test } from 'node:test'; import Connector from "../lib/Connector.js"; +import { assertSemanticEqual, TestObserver } from './utils.js'; const connector = new Connector(); @@ -15,15 +16,27 @@ const address = connector.createAddress({ const json = '{"@context":"https://www.datafoodconsortium.org","@id":"http://myplatform.com/address/address1","@type":"dfc-b:Address","dfc-b:hasCity":"Brussels","dfc-b:hasCountry":"Belgium","dfc-b:hasPostalCode":"00001","dfc-b:hasStreet":"1, place or Europe"}'; test('Address:import', async () => { + const testObs = new TestObserver(address, assertSemanticEqual); + const testSub = connector.subscribe('import', testObs); const imported = await connector.import(json); const expected = imported[0]; expect.strictEqual(imported.length, 1); expect.strictEqual(expected.equals(address), true); + expect.doesNotThrow(() => { + testObs.complete(); + testSub.unsubscribe(); + }, '#unsubscribe'); }); test('Address:export', async () => { + const testObs = new TestObserver(json, expect.strictEqual); + const testSub = connector.subscribe('export', testObs); const serialized = await connector.export([address]); expect.strictEqual(serialized, json); + expect.doesNotThrow(() => { + testObs.complete(); + testSub.unsubscribe(); + }, '#unsubscribe'); }); test('Address:getSemanticId', () => { diff --git a/src/org/datafoodconsortium/connector/codegen/typescript/static/test/AllergenCharacteristic.test.js b/src/org/datafoodconsortium/connector/codegen/typescript/static/test/AllergenCharacteristic.test.js index 82095f3..19f683b 100644 --- a/src/org/datafoodconsortium/connector/codegen/typescript/static/test/AllergenCharacteristic.test.js +++ b/src/org/datafoodconsortium/connector/codegen/typescript/static/test/AllergenCharacteristic.test.js @@ -2,6 +2,8 @@ import * as fs from 'fs'; import expect from 'node:assert'; import { test } from 'node:test'; import Connector from "../lib/Connector.js"; +import { assertSemanticEqual, TestObserver } from './utils.js'; + const measures = JSON.parse(fs.readFileSync('./test/thesaurus/measures.json')); const connector = new Connector(); @@ -19,10 +21,16 @@ const allergenCharacteristic = connector.createAllergenCharacteristic({ const json = `{"@context":"https://www.datafoodconsortium.org","@id":"_:b1","@type":"dfc-b:AllergenCharacteristic","dfc-b:hasAllergenDimension":"dfc-m:Peanuts","dfc-b:hasUnit":"dfc-m:Kilogram","dfc-b:value":"1"}`; test('AllergenCharacteristic:import', async () => { + const testObs = new TestObserver(allergenCharacteristic, assertSemanticEqual); + const testSub = connector.subscribe('import', testObs); const imported = await connector.import(json); const importedAllergenCharacteristic = imported[0]; expect.strictEqual(imported.length, 1); expect.strictEqual(importedAllergenCharacteristic.equals(allergenCharacteristic), true); + expect.doesNotThrow(() => { + testObs.complete(); + testSub.unsubscribe(); + }, '#unsubscribe'); }); test('AllergenCharacteristic:export', async () => { diff --git a/src/org/datafoodconsortium/connector/codegen/typescript/static/test/Catalog.test.js b/src/org/datafoodconsortium/connector/codegen/typescript/static/test/Catalog.test.js index afea2f4..b60fb06 100644 --- a/src/org/datafoodconsortium/connector/codegen/typescript/static/test/Catalog.test.js +++ b/src/org/datafoodconsortium/connector/codegen/typescript/static/test/Catalog.test.js @@ -1,6 +1,7 @@ import expect from 'node:assert'; import { test } from 'node:test'; import Connector from "../lib/Connector.js"; +import { assertSemanticEqual, TestObserver } from './utils.js'; const connector = new Connector(); @@ -29,10 +30,16 @@ const catalog = connector.createCatalog({ }); test('Catalog:import', async () => { + const testObs = new TestObserver(catalog, assertSemanticEqual); + const testSub = connector.subscribe('import', testObs); const imported = await connector.import(json); const importedCatalog = imported[0]; expect.strictEqual(imported.length, 1); expect.strictEqual(importedCatalog.equals(catalog), true); + expect.doesNotThrow(() => { + testObs.complete(); + testSub.unsubscribe(); + }, '#unsubscribe'); }); test('Catalog:export', async () => { diff --git a/src/org/datafoodconsortium/connector/codegen/typescript/static/test/CatalogItem.test.js b/src/org/datafoodconsortium/connector/codegen/typescript/static/test/CatalogItem.test.js index 35095aa..efd4589 100644 --- a/src/org/datafoodconsortium/connector/codegen/typescript/static/test/CatalogItem.test.js +++ b/src/org/datafoodconsortium/connector/codegen/typescript/static/test/CatalogItem.test.js @@ -1,6 +1,7 @@ import expect from 'node:assert'; import { test } from 'node:test'; import Connector from "../lib/Connector.js"; +import { assertSemanticEqual, TestObserver } from './utils.js'; const connector = new Connector(); @@ -40,15 +41,27 @@ const catalogItem = connector.createCatalogItem({ }); test('CatalogItem:import', async () => { + const testObs = new TestObserver(catalogItem, assertSemanticEqual); + const testSub = connector.subscribe('import', testObs); const imported = await connector.import(json); const importedCatalogItem = imported[0]; expect.strictEqual(imported.length, 1); expect.strictEqual(importedCatalogItem.equals(catalogItem), true); + expect.doesNotThrow(() => { + testObs.complete(); + testSub.unsubscribe(); + }, '#unsubscribe'); }); test('CatalogItem:export', async () => { + const testObs = new TestObserver(json, expect.strictEqual); + const testSub = connector.subscribe('export', testObs); const serialized = await connector.export([catalogItem]); expect.strictEqual(serialized, json); + expect.doesNotThrow(() => { + testObs.complete(); + testSub.unsubscribe(); + }, '#unsubscribe'); }); test('CatalogItem:getSemanticId', () => { diff --git a/src/org/datafoodconsortium/connector/codegen/typescript/static/test/CustomerCategory.test.js b/src/org/datafoodconsortium/connector/codegen/typescript/static/test/CustomerCategory.test.js index d23cf31..707c294 100644 --- a/src/org/datafoodconsortium/connector/codegen/typescript/static/test/CustomerCategory.test.js +++ b/src/org/datafoodconsortium/connector/codegen/typescript/static/test/CustomerCategory.test.js @@ -1,6 +1,7 @@ import expect from 'node:assert'; import { test } from 'node:test'; import Connector from "../lib/Connector.js"; +import { assertSemanticEqual, TestObserver } from './utils.js'; const connector = new Connector(); @@ -12,10 +13,16 @@ const customerCategory = connector.createCustomerCategory({ const json = `{"@context":"https://www.datafoodconsortium.org","@id":"http://myplatform.com/customerCategory1","@type":"dfc-b:CustomerCategory","dfc-b:description":"description"}`; test('CustomerCategory:import', async () => { + const testObs = new TestObserver(customerCategory, assertSemanticEqual); + const testSub = connector.subscribe('import', testObs); const imported = await connector.import(json); const importedCustomerCategory = imported[0]; expect.strictEqual(imported.length, 1); expect.strictEqual(importedCustomerCategory.equals(customerCategory), true); + expect.doesNotThrow(() => { + testObs.complete(); + testSub.unsubscribe(); + }, '#unsubscribe'); }); test('CustomerCategory:export', async () => { diff --git a/src/org/datafoodconsortium/connector/codegen/typescript/static/test/Enterprise.test.js b/src/org/datafoodconsortium/connector/codegen/typescript/static/test/Enterprise.test.js index c1dc398..26304bb 100644 --- a/src/org/datafoodconsortium/connector/codegen/typescript/static/test/Enterprise.test.js +++ b/src/org/datafoodconsortium/connector/codegen/typescript/static/test/Enterprise.test.js @@ -1,6 +1,7 @@ import expect from 'node:assert'; import { test } from 'node:test'; import Connector from "../lib/Connector.js"; +import { assertSemanticEqual, TestObserver } from './utils.js'; const connector = new Connector(); @@ -60,15 +61,27 @@ const enterprise = connector.createEnterprise({ const json = `{"@context":"https://www.datafoodconsortium.org","@id":"http://myplatform.com/enterprise1","@type":"dfc-b:Enterprise","dfc-b:VATnumber":"vatNumber","dfc-b:defines":"http://myplatform.com/customerCategory1","dfc-b:hasAddress":{"@id":"http://myplatform.com/address1"},"dfc-b:hasDescription":"description","dfc-b:maintains":{"@id":"http://myplatform.com/catalog1"},"dfc-b:manages":"http://myplatform.com/catalogItem1","dfc-b:supplies":"http://myplatform.com/suppliedProduct1"}`; test('Enterprise:import', async () => { + const testObs = new TestObserver(enterprise, assertSemanticEqual); + const testSub = connector.subscribe('import', testObs); const imported = await connector.import(json); const importedEnterprise = imported[0]; expect.strictEqual(imported.length, 1); expect.strictEqual(importedEnterprise.equals(enterprise), true); + expect.doesNotThrow(() => { + testObs.complete(); + testSub.unsubscribe(); + }, '#unsubscribe'); }); test('Enterprise:export', async () => { + const testObs = new TestObserver(json, expect.strictEqual); + const testSub = connector.subscribe('export', testObs); const serialized = await connector.export([enterprise]); expect.strictEqual(serialized, json); + expect.doesNotThrow(() => { + testObs.complete(); + testSub.unsubscribe(); + }, '#unsubscribe'); }); test('Enterprise:getSemanticId', () => { diff --git a/src/org/datafoodconsortium/connector/codegen/typescript/static/test/NutrientCharacteristic.test.js b/src/org/datafoodconsortium/connector/codegen/typescript/static/test/NutrientCharacteristic.test.js index ac92fc3..28918b1 100644 --- a/src/org/datafoodconsortium/connector/codegen/typescript/static/test/NutrientCharacteristic.test.js +++ b/src/org/datafoodconsortium/connector/codegen/typescript/static/test/NutrientCharacteristic.test.js @@ -2,6 +2,8 @@ import * as fs from 'fs'; import expect from 'node:assert'; import { test } from 'node:test'; import Connector from "../lib/Connector.js"; +import { assertSemanticEqual, TestObserver } from './utils.js'; + const facets = JSON.parse(fs.readFileSync('./test/thesaurus/facets.json')); const measures = JSON.parse(fs.readFileSync('./test/thesaurus/measures.json')); @@ -23,10 +25,16 @@ const nutrientCharacteristic = connector.createNutrientCharacteristic({ const json = `{"@context":"https://www.datafoodconsortium.org","@id":"_:b1","@type":"dfc-b:NutrientCharacteristic","dfc-b:hasNutrientDimension":"dfc-m:Calcium","dfc-b:hasUnit":"dfc-m:Kilogram","dfc-b:value":"10"}`; test('NutrientCharacteristic:import', async () => { + const testObs = new TestObserver(nutrientCharacteristic, assertSemanticEqual); + const testSub = connector.subscribe('import', testObs); const imported = await connector.import(json); const importedNutrientCharacteristic = imported[0]; expect.strictEqual(imported.length, 1); expect.strictEqual(importedNutrientCharacteristic.equals(nutrientCharacteristic), true); + expect.doesNotThrow(() => { + testObs.complete(); + testSub.unsubscribe(); + }, '#unsubscribe'); }); test('NutrientCharacteristic:export', async () => { diff --git a/src/org/datafoodconsortium/connector/codegen/typescript/static/test/Offer.test.js b/src/org/datafoodconsortium/connector/codegen/typescript/static/test/Offer.test.js index 8d1fd28..c8705b7 100644 --- a/src/org/datafoodconsortium/connector/codegen/typescript/static/test/Offer.test.js +++ b/src/org/datafoodconsortium/connector/codegen/typescript/static/test/Offer.test.js @@ -2,6 +2,8 @@ import * as fs from 'fs'; import expect from 'node:assert'; import { test } from 'node:test'; import Connector from "../lib/Connector.js"; +import { assertSemanticEqual, TestObserver } from './utils.js'; + const measures = JSON.parse(fs.readFileSync('./test/thesaurus/measures.json')); const connector = new Connector(); @@ -32,10 +34,16 @@ const offer = connector.createOffer({ const json = `{"@context":"https://www.datafoodconsortium.org","@graph":[{"@id":"_:b1","@type":"dfc-b:Price","dfc-b:VATrate":"8","dfc-b:hasUnit":"dfc-m:Euro","dfc-b:value":"2.54"},{"@id":"http://myplatform.com/offer1","@type":"dfc-b:Offer","dfc-b:hasPrice":{"@id":"_:b1"},"dfc-b:offeredItem":{"@id":"http://myplatform.com/suppliedProduct1"},"dfc-b:offeredTo":{"@id":"http://myplatform.com/customerCategory1"},"dfc-b:stockLimitation":"4.21"}]}`; test('Offer:import', async () => { + const testObs = new TestObserver(offer, assertSemanticEqual); + const testSub = connector.subscribe('import', testObs); const imported = await connector.import(json); const importedOffer = imported[0]; expect.strictEqual(imported.length, 1); expect.strictEqual(importedOffer.equals(offer), true); + expect.doesNotThrow(() => { + testObs.complete(); + testSub.unsubscribe(); + }, '#unsubscribe'); }); test('Offer:export', async () => { diff --git a/src/org/datafoodconsortium/connector/codegen/typescript/static/test/Order.test.js b/src/org/datafoodconsortium/connector/codegen/typescript/static/test/Order.test.js index 98d9cef..085b56f 100644 --- a/src/org/datafoodconsortium/connector/codegen/typescript/static/test/Order.test.js +++ b/src/org/datafoodconsortium/connector/codegen/typescript/static/test/Order.test.js @@ -1,6 +1,7 @@ import expect from 'node:assert'; import { test } from 'node:test'; import Connector from "../lib/Connector.js"; +import { assertSemanticEqual, TestObserver } from './utils.js'; const connector = new Connector(); @@ -40,10 +41,16 @@ const order = connector.createOrder({ const json = `{"@context":"https://www.datafoodconsortium.org","@id":"http://myplatform.com/order1","@type":"dfc-b:Order","dfc-b:belongsTo":{"@id":"http://myplatform.com/saleSession1"},"dfc-b:date":"date","dfc-b:hasPart":{"@id":"http://myplatform.com/orderLine1"},"dfc-b:orderNumber":"0001","dfc-b:orderedBy":{"@id":"http://myplatform.com/person1"}}`; test('Order:import', async () => { + const testObs = new TestObserver(order, assertSemanticEqual); + const testSub = connector.subscribe('import', testObs); const imported = await connector.import(json); const importedOrder = imported[0]; expect.strictEqual(imported.length, 1); expect.strictEqual(importedOrder.equals(order), true); + expect.doesNotThrow(() => { + testObs.complete(); + testSub.unsubscribe(); + }, '#unsubscribe'); }); test('Order:export', async () => { diff --git a/src/org/datafoodconsortium/connector/codegen/typescript/static/test/OrderLine.test.js b/src/org/datafoodconsortium/connector/codegen/typescript/static/test/OrderLine.test.js index 070752c..27a39d2 100644 --- a/src/org/datafoodconsortium/connector/codegen/typescript/static/test/OrderLine.test.js +++ b/src/org/datafoodconsortium/connector/codegen/typescript/static/test/OrderLine.test.js @@ -2,6 +2,8 @@ import * as fs from 'fs'; import expect from 'node:assert'; import { test } from 'node:test'; import Connector from "../lib/Connector.js"; +import { assertSemanticEqual, TestObserver } from './utils.js'; + const measures = JSON.parse(fs.readFileSync('./test/thesaurus/measures.json')); const connector = new Connector(); @@ -32,10 +34,16 @@ const orderLine = connector.createOrderLine({ const json = `{"@context":"https://www.datafoodconsortium.org","@graph":[{"@id":"_:b1","@type":"dfc-b:Price","dfc-b:VATrate":"19.9","dfc-b:hasUnit":"dfc-m:Euro","dfc-b:value":"5.42"},{"@id":"http://myplatform.com/orderLine1","@type":"dfc-b:OrderLine","dfc-b:concerns":{"@id":"http://myplatform.com/offer1"},"dfc-b:hasPrice":{"@id":"_:b1"},"dfc-b:partOf":{"@id":"http://myplatform.com/order1"},"dfc-b:quantity":"2"}]}`; test('OrderLine:import', async () => { + const testObs = new TestObserver(orderLine, assertSemanticEqual); + const testSub = connector.subscribe('import', testObs); const imported = await connector.import(json); const importedOrderLine = imported[0]; expect.strictEqual(imported.length, 1); expect.strictEqual(importedOrderLine.equals(orderLine), true); + expect.doesNotThrow(() => { + testObs.complete(); + testSub.unsubscribe(); + }, '#unsubscribe'); }); test('OrderLine:export', async () => { diff --git a/src/org/datafoodconsortium/connector/codegen/typescript/static/test/Person.test.js b/src/org/datafoodconsortium/connector/codegen/typescript/static/test/Person.test.js index a5dda42..7e6c7ef 100644 --- a/src/org/datafoodconsortium/connector/codegen/typescript/static/test/Person.test.js +++ b/src/org/datafoodconsortium/connector/codegen/typescript/static/test/Person.test.js @@ -1,6 +1,7 @@ import expect from 'node:assert'; import { test } from 'node:test'; import Connector from "../lib/Connector.js"; +import { assertSemanticEqual, TestObserver } from './utils.js'; const connector = new Connector(); @@ -31,10 +32,16 @@ const person = connector.createPerson({ const json = `{"@context":"https://www.datafoodconsortium.org","@id":"http://myplatform.com/person1","@type":"dfc-b:Person","dfc-b:affiliates":"http://myplatform.com/address/enterprise1","dfc-b:familyName":"Smith","dfc-b:firstName":"John","dfc-b:hasAddress":{"@id":"http://myplatform.com/address/address1"}}`; test('Person:import', async () => { + const testObs = new TestObserver(person, assertSemanticEqual); + const testSub = connector.subscribe('import', testObs); const imported = await connector.import(json); const importedPerson = imported[0]; expect.strictEqual(imported.length, 1); expect.strictEqual(importedPerson.equals(person), true); + expect.doesNotThrow(() => { + testObs.complete(); + testSub.unsubscribe(); + }, '#unsubscribe'); }); test('Person:export', async () => { diff --git a/src/org/datafoodconsortium/connector/codegen/typescript/static/test/PhysicalCharacteristic.test.js b/src/org/datafoodconsortium/connector/codegen/typescript/static/test/PhysicalCharacteristic.test.js index 56ba9fa..ef7cdf3 100644 --- a/src/org/datafoodconsortium/connector/codegen/typescript/static/test/PhysicalCharacteristic.test.js +++ b/src/org/datafoodconsortium/connector/codegen/typescript/static/test/PhysicalCharacteristic.test.js @@ -2,6 +2,8 @@ import * as fs from 'fs'; import expect from 'node:assert'; import { test } from 'node:test'; import Connector from "../lib/Connector.js"; +import { assertSemanticEqual, TestObserver } from './utils.js'; + const facets = JSON.parse(fs.readFileSync('./test/thesaurus/facets.json')); const measures = JSON.parse(fs.readFileSync('./test/thesaurus/measures.json')); @@ -23,10 +25,16 @@ const physicalCharacteristic = connector.createPhysicalCharacteristic({ const json = `{"@context":"https://www.datafoodconsortium.org","@id":"_:b1","@type":"dfc-b:PhysicalCharacteristic","dfc-b:hasPhysicalDimension":"dfc-m:Weight","dfc-b:hasUnit":"dfc-m:Kilogram","dfc-b:value":"100"}`; test('PhysicalCharacteristic:import', async () => { + const testObs = new TestObserver(physicalCharacteristic, assertSemanticEqual); + const testSub = connector.subscribe('import', testObs); const imported = await connector.import(json); const importedPhysicalCharacteristic = imported[0]; expect.strictEqual(imported.length, 1); expect.strictEqual(importedPhysicalCharacteristic.equals(physicalCharacteristic), true); + expect.doesNotThrow(() => { + testObs.complete(); + testSub.unsubscribe(); + }, '#unsubscribe'); }); test('PhysicalCharacteristic:export', async () => { diff --git a/src/org/datafoodconsortium/connector/codegen/typescript/static/test/PlannedTransformationLoop.test.js b/src/org/datafoodconsortium/connector/codegen/typescript/static/test/PlannedTransformationLoop.test.js index 3aee9e8..3bcddf9 100644 --- a/src/org/datafoodconsortium/connector/codegen/typescript/static/test/PlannedTransformationLoop.test.js +++ b/src/org/datafoodconsortium/connector/codegen/typescript/static/test/PlannedTransformationLoop.test.js @@ -2,6 +2,8 @@ import * as fs from 'fs'; import expect from 'node:assert'; import { test } from 'node:test'; import Connector from "../lib/Connector.js"; +import { assertSemanticEqual, TestObserver } from './utils.js'; + const measures = JSON.parse(fs.readFileSync('./test/thesaurus/measures.json')); const vocabulary = JSON.parse(fs.readFileSync('./test/thesaurus/vocabulary.json')); @@ -53,6 +55,8 @@ const plannedTransformation = connector.createPlannedTransformation({ const json = '{"@context":"https://www.datafoodconsortium.org","@graph":[{"@id":"_:b1","@type":"dfc-b:QuantitativeValue","dfc-b:hasUnit":"dfc-m:Kilogram","dfc-b:value":"1.2"},{"@id":"_:b2","@type":"dfc-b:QuantitativeValue","dfc-b:hasUnit":"dfc-m:Kilogram","dfc-b:value":"1"},{"@id":"http://myplatform.com/plannedConsumptionFlow","@type":"dfc-b:AsPlannedConsumptionFlow","dfc-b:consumes":"http://myplatform.com/inputProduct","dfc-b:hasQuantity":"_:b1"},{"@id":"http://myplatform.com/plannedProductionFlow","@type":"dfc-b:AsPlannedProductionFlow","dfc-b:hasQuantity":"_:b2","dfc-b:produces":"http://myplatform.com/outputProduct"},{"@id":"http://myplatform.com/transformation","@type":"dfc-b:AsPlannedTransformation","dfc-b:hasIncome":"http://myplatform.com/plannedConsumptionFlow","dfc-b:hasOutcome":"http://myplatform.com/plannedProductionFlow","dfc-b:hasTransformationType":"dfc-v:modify"}]}'; test('PlannedTransformationLoop:import', async () => { + const testObs = new TestObserver(plannedConsumptionFlow, assertSemanticEqual); + const testSub = connector.subscribe('import', testObs); const imported = await connector.import(json); const expectedPlannedConsumptionFlow = imported[0]; const expectedPlannedProductionFlow = imported[1]; @@ -61,6 +65,10 @@ test('PlannedTransformationLoop:import', async () => { expect.strictEqual(expectedPlannedTransformation.equals(plannedTransformation), true); expect.strictEqual(expectedPlannedConsumptionFlow.equals(plannedConsumptionFlow), true); expect.strictEqual(expectedPlannedProductionFlow.equals(plannedProductionFlow), true); + expect.doesNotThrow(() => { + testObs.complete(); + testSub.unsubscribe(); + }, '#unsubscribe'); }); test('PlannedTransformationLoop:export', async () => { diff --git a/src/org/datafoodconsortium/connector/codegen/typescript/static/test/Price.test.js b/src/org/datafoodconsortium/connector/codegen/typescript/static/test/Price.test.js index e43051c..e5bce00 100644 --- a/src/org/datafoodconsortium/connector/codegen/typescript/static/test/Price.test.js +++ b/src/org/datafoodconsortium/connector/codegen/typescript/static/test/Price.test.js @@ -2,6 +2,8 @@ import * as fs from 'fs'; import expect from 'node:assert'; import { test } from 'node:test'; import Connector from "../lib/Connector.js"; +import { assertSemanticEqual, TestObserver } from './utils.js'; + const measures = JSON.parse(fs.readFileSync('./test/thesaurus/measures.json')); const connector = new Connector(); @@ -18,10 +20,16 @@ const price = connector.createPrice({ const json = `{"@context":"https://www.datafoodconsortium.org","@id":"_:b1","@type":"dfc-b:Price","dfc-b:VATrate":"8","dfc-b:hasUnit":"dfc-m:Euro","dfc-b:value":"2.54"}`; test('Price:import', async () => { + const testObs = new TestObserver(price, assertSemanticEqual); + const testSub = connector.subscribe('import', testObs); const imported = await connector.import(json); const importedPrice = imported[0]; expect.strictEqual(imported.length, 1); expect.strictEqual(importedPrice.equals(price), true); + expect.doesNotThrow(() => { + testObs.complete(); + testSub.unsubscribe(); + }, '#unsubscribe'); }); test('Price:export', async () => { diff --git a/src/org/datafoodconsortium/connector/codegen/typescript/static/test/QuantitativeValue.test.js b/src/org/datafoodconsortium/connector/codegen/typescript/static/test/QuantitativeValue.test.js index 4c41735..e89ffd2 100644 --- a/src/org/datafoodconsortium/connector/codegen/typescript/static/test/QuantitativeValue.test.js +++ b/src/org/datafoodconsortium/connector/codegen/typescript/static/test/QuantitativeValue.test.js @@ -2,6 +2,8 @@ import * as fs from 'fs'; import expect from 'node:assert'; import { test } from 'node:test'; import Connector from "../lib/Connector.js"; +import { assertSemanticEqual, TestObserver } from './utils.js'; + const measures = JSON.parse(fs.readFileSync('./test/thesaurus/measures.json')); const connector = new Connector(); @@ -17,10 +19,16 @@ const quantitativeValue = connector.createQuantity({ const json = `{"@context":"https://www.datafoodconsortium.org","@id":"_:b1","@type":"dfc-b:QuantitativeValue","dfc-b:hasUnit":"dfc-m:Kilogram","dfc-b:value":"1"}`; test('QuantitativeValue:import', async () => { + const testObs = new TestObserver(quantitativeValue, assertSemanticEqual); + const testSub = connector.subscribe('import', testObs); const importedAll = await connector.import(json); const imported = importedAll[0]; expect.strictEqual(importedAll.length, 1); expect.strictEqual(imported.equals(quantitativeValue), true); + expect.doesNotThrow(() => { + testObs.complete(); + testSub.unsubscribe(); + }, '#unsubscribe'); }); test('QuantitativeValue:export', async () => { diff --git a/src/org/datafoodconsortium/connector/codegen/typescript/static/test/SaleSession.test.js b/src/org/datafoodconsortium/connector/codegen/typescript/static/test/SaleSession.test.js index f2692e4..fb66aef 100644 --- a/src/org/datafoodconsortium/connector/codegen/typescript/static/test/SaleSession.test.js +++ b/src/org/datafoodconsortium/connector/codegen/typescript/static/test/SaleSession.test.js @@ -1,6 +1,7 @@ import expect from 'node:assert'; import { test } from 'node:test'; import Connector from "../lib/Connector.js"; +import { assertSemanticEqual, TestObserver } from './utils.js'; const connector = new Connector(); @@ -23,10 +24,16 @@ const saleSession = connector.createSaleSession({ const json = `{"@context":"https://www.datafoodconsortium.org","@id":"http://myplatform.com/saleSession1","@type":"dfc-b:SaleSession","dfc-b:beginDate":"beginDate","dfc-b:endDate":"endDate","dfc-b:lists":{"@id":"http://myplatform.com/offer1"},"dfc-b:quantity":"5"}`; test('SaleSession:import', async () => { + const testObs = new TestObserver(saleSession, assertSemanticEqual); + const testSub = connector.subscribe('import', testObs); const importedAll = await connector.import(json); const imported = importedAll[0]; expect.strictEqual(importedAll.length, 1); expect.strictEqual(imported.equals(saleSession), true); + expect.doesNotThrow(() => { + testObs.complete(); + testSub.unsubscribe(); + }, '#unsubscribe'); }); test('SaleSession:export', async () => { diff --git a/src/org/datafoodconsortium/connector/codegen/typescript/static/test/SuppliedProduct.test.js b/src/org/datafoodconsortium/connector/codegen/typescript/static/test/SuppliedProduct.test.js index a56bdd7..212192f 100644 --- a/src/org/datafoodconsortium/connector/codegen/typescript/static/test/SuppliedProduct.test.js +++ b/src/org/datafoodconsortium/connector/codegen/typescript/static/test/SuppliedProduct.test.js @@ -2,6 +2,8 @@ import * as fs from 'fs'; import expect from 'node:assert'; import { test } from 'node:test'; import Connector from "../lib/Connector.js"; +import { assertSemanticEqual, TestObserver } from './utils.js'; + const facets = JSON.parse(fs.readFileSync('./test/thesaurus/facets.json')); const measures = JSON.parse(fs.readFileSync('./test/thesaurus/measures.json')); const productTypes = JSON.parse(fs.readFileSync('./test/thesaurus/productTypes.json')); @@ -88,10 +90,16 @@ let suppliedProduct = connector.createSuppliedProduct({ const json = `{"@context":"https://www.datafoodconsortium.org","@graph":[{"@id":"_:b1","@type":"dfc-b:QuantitativeValue","dfc-b:hasUnit":"dfc-m:Kilogram","dfc-b:value":"1.2"},{"@id":"_:b2","@type":"dfc-b:AllergenCharacteristic","dfc-b:hasAllergenDimension":"dfc-m:Peanuts","dfc-b:hasUnit":"dfc-m:Kilogram","dfc-b:value":"1"},{"@id":"_:b4","@type":"dfc-b:NutrientCharacteristic","dfc-b:hasNutrientDimension":"dfc-m:Calcium","dfc-b:hasUnit":"dfc-m:Gram","dfc-b:value":"10"},{"@id":"_:b6","@type":"dfc-b:PhysicalCharacteristic","dfc-b:hasPhysicalDimension":"dfc-m:Weight","dfc-b:hasUnit":"dfc-m:Gram","dfc-b:value":"100"},{"@id":"http://myplatform.com/tomato","@type":"dfc-b:SuppliedProduct","dfc-b:alcoholPercentage":"0","dfc-b:description":"Awesome tomato","dfc-b:hasAllergenCharacteristic":{"@id":"_:b2"},"dfc-b:hasCertification":[{"@id":"dfc-f:Organic-AB"},{"@id":"dfc-f:Organic-EU"}],"dfc-b:hasClaim":"dfc-f:NoAddedSugars","dfc-b:hasGeographicalOrigin":"dfc-f:CentreValLoire","dfc-b:hasNatureOrigin":{"@id":"dfc-f:PlantOrigin"},"dfc-b:hasNutrientCharacteristic":{"@id":"_:b4"},"dfc-b:hasPartOrigin":{"@id":"dfc-f:Fruit"},"dfc-b:hasPhysicalCharacteristic":{"@id":"_:b6"},"dfc-b:hasQuantity":"_:b1","dfc-b:hasType":"dfc-pt:round-tomato","dfc-b:image":["http://myplatform.com/image1","http://myplatform.com/image2"],"dfc-b:lifetime":"a week","dfc-b:referencedBy":"http://myplatform.com/catalogItem","dfc-b:totalTheoreticalStock":"2.23","dfc-b:usageOrStorageCondition":"free text"}]}`; test('SuppliedProduct:import', async () => { + const testObs = new TestObserver(suppliedProduct, assertSemanticEqual); + const testSub = connector.subscribe('import', testObs); const importedAll = await connector.import(json); const imported = importedAll[0]; expect.strictEqual(importedAll.length, 1); expect.strictEqual(imported.equals(suppliedProduct), true); + expect.doesNotThrow(() => { + testObs.complete(); + testSub.unsubscribe(); + }, '#unsubscribe'); }); test('SuppliedProduct:export', async () => { diff --git a/src/org/datafoodconsortium/connector/codegen/typescript/static/test/utils.js b/src/org/datafoodconsortium/connector/codegen/typescript/static/test/utils.js index a286397..d3d0d31 100644 --- a/src/org/datafoodconsortium/connector/codegen/typescript/static/test/utils.js +++ b/src/org/datafoodconsortium/connector/codegen/typescript/static/test/utils.js @@ -1,19 +1,111 @@ -import { ok, AssertionError } from 'node:assert'; +import { equal, ok, AssertionError } from 'node:assert'; +import Dataset from "@rdfjs/dataset/DatasetCore.js"; import { SemanticObject } from '@virtual-assembly/semantizer'; /** - * assertSemanticEqual - * @param {*} actual Actual test value + * Asserts that the actual semanticable object or RDF dataset is equivalent to + * the expected semantic object. + * @param {SemanticObject|Dataset[]|Dataset[]|unknown} actual Actual test value * @param {SemanticObject} expected Expected test value */ export function assertSemanticEqual(actual, expected) { + if (Array.isArray(actual)) { + assertDatasetEqualsSemanticObject(actual, expected); + } else if (actual && actual instanceof Dataset) { + assertDatasetEqualsSemanticObject([actual], expected); + } else { + assertSemanticStrictEqual(actual, expected); + } +} + +/** + * Asserts a strict equivalence between actual and expected semantic objects via + * SemanticObject.prototype.equals(). + * @param {SemanticObject|unknown} actual Actual test value + * @param {SemanticObject} expected Expected test value + */ +export function assertSemanticStrictEqual(actual, expected) { const operator = 'SemanticObject.equals'; const errOpts = { actual, expected, operator }; - if (!(actual instanceof SemanticObject)) { + if (!(actual && actual instanceof SemanticObject)) { errOpts.message = 'Expected an instance of SemanticObject:'; - } else if (!actual.equals(expected)) { + } else if (!expected.equals(actual)) { errOpts.message = 'Expected semantic objects to have equal properties:'; } if (errOpts.message) throw new AssertionError(errOpts); ok(actual); } + +/** + * Asserts that the first element in an expected array of semantic objects is + * equivalent to a single expected semantic object. + * @param {Dataset[]} actual An array of actual test values + * @param {SemanticObject} expected Expected first value + */ +export function assertDatasetEqualsSemanticObject(actual, expected) { + ok(Array.isArray(actual) && actual.length > 0); + const [dataset] = actual; + let maybeSemObj = dataset; + if (dataset && typeof dataset === 'object' && dataset instanceof Dataset) { + const semantizer = expected.getSemantizer(); + maybeSemObj = SemanticObject.createFromRdfDataset(semantizer, dataset); + } + const isSemanticable = maybeSemObj + && typeof maybeSemObj === 'object' + && maybeSemObj instanceof SemanticObject; + if (isSemanticable) { + assertSemanticStrictEqual(maybeSemObj, expected); + } else { + const message = 'Expected an instance of SemanticObject or RDF dataset.'; + throw new AssertionError({ actual, expected, message }); + } +} + +/** + * @import { TestContext } from 'node:test + * @import { Observer } from "../src/observer" + * @template T + * @implements { Observer } + */ +export class TestObserver { + /** @public {T} */ + expected; + /** @public {(actual: unknown, expected: T) => void} */ + assert = equal; + /** @public {(result?: any) => void} */ + done; + + /** + * @constructor + * @param {T} expected + * @param {(actual: unknown, expected: T) => void} [assert=equal] + * @param {(result?: any) => void} [done] + */ + constructor(expected, assert = equal, done = undefined) { + this.expected = expected; + if (typeof assert === 'function') this.assert = assert; + if (typeof done === 'function') this.done = done; + } + + next(actual) { + if (typeof this.assert !== 'function' || this.assert.length < 2) { + const message = + `${this.assert} [TestObserver:assert] is not a valid assertion function.`; + throw new AssertionError({ + actual, expected: this.expected, message, operator: 'TestObserver:next', + }); + } + else this.assert(actual, this.expected); + } + + error({ message }) { + const operator = 'TestObserver:error'; + throw new AssertionError({ message, operator, expected: this.expected }); + } + + complete() { + ok(true, 'TestObserver:complete'); + if (typeof this.done === 'function') this.done(); + } + +}