From 2fd932b3664d699933952ec38941a9148092940e Mon Sep 17 00:00:00 2001 From: Alvin Sebastian Date: Tue, 21 May 2024 11:46:48 +0000 Subject: [PATCH] Add hasEntity and addTermDefinition methods --- lib/rocrate.js | 187 +++++++++++++++++++++------------------ package.json | 4 +- test/rocrate.new.spec.js | 34 ++++++- 3 files changed, 137 insertions(+), 88 deletions(-) diff --git a/lib/rocrate.js b/lib/rocrate.js index ec7a664..b9f844a 100644 --- a/lib/rocrate.js +++ b/lib/rocrate.js @@ -54,10 +54,11 @@ class ROCrate { __handlerReverse; /** Internal representation of the context */ - __context = {}; + __context = new Set(); /** Index of all context contents or terms */ - __contextIndex; + __contextTermIndex = new Map(); + __contextDefinitionIndex = new Map(); /** @deprecated Import {@link Utils} class directly*/ utils = Utils; @@ -94,8 +95,10 @@ class ROCrate { //this.defaultMetadataIds = new Set(defaults.roCrateMetadataIDs); // init graph - this.__context = Utils.clone(json["@context"] || defaults.context); - this.__contextIndex = resolveLocalContext(this.__context); + for (const c of Utils.asArrayRef(Utils.clone(json["@context"] || defaults.context))) { + this.__context.add(c); + } + resolveLocalContext(this.__context, this.__contextTermIndex, this.__contextDefinitionIndex); let g = json["@graph"]; if (!Array.isArray(g) || !g.length) g = [ defaults.datasetTemplate, @@ -283,47 +286,6 @@ class ROCrate { //return !utils.isEqual(oldVals, entity[prop]); } - /** - * Find the context term definition from the contextIndex. - * It will also search for term defined locally in the graph. - */ - __getDefinition(contextIndex, term) { - var def = { '@id': this.__resolveTerm(contextIndex, term) }; - let localDef; - if (def["@id"] && (localDef = this.getEntity(def["@id"]))) { - let id; - if ((id = localDef.sameAs?.["@id"])) { - // There's a same-as - so use its ID - def["@id"] = id; - localDef = this.getEntity(id); - } - if (localDef && (this.hasType(localDef, "rdfs:Class") || this.hasType(localDef, "rdf:Property"))) { - def = localDef; - } - } - return def; - } - - __resolveTerm(contextIndex, term) { - if (term.match(/^http(s?):\/\//i)) { - return term; - } - term = term.replace(/^schema:/, ""); //schema is the default namespace - const val = contextIndex[term]; - var parts, url; - if (val && val.match(/^http(s?):\/\//i)) { - return val; - } else if (val && (parts = val.match(/(.*?):(.*)/))) { - url = contextIndex[parts[1]]; - } else if ((parts = term.match(/(.*?):(.*)/))) { - // eg txc:Somthing - url = contextIndex[parts[1]]; - } - if (url && url.match(/^http(s?):\/\//i)) { - return `${url}${parts[2]}`; - } - } - get ['@context']() { return this.context; } @@ -332,7 +294,9 @@ class ROCrate { * This returns the original context information. */ get context() { - return Utils.asArray(this.__context); + const arr = Array.from(this.__context); + if (arr.length <= 1 && !this.config.array) return arr[0]; + return arr; } get ['@graph']() { return this.graph; } @@ -380,13 +344,24 @@ class ROCrate { //////// Public mutator methods /** * Append the specified string or object directly as an entry into the RO-Crate JSON-LD Context array. - * It does not check for duplicates or overlapping content. - * @param {String|Object} context - A URL or an Object that contains the context mapping + * It does not check for duplicates or overlapping content if the context is an object. + * @param {string|object|string[]|object[]} context - A URL or an Object that contains the context mapping */ addContext(context) { - this.__context = Utils.asArrayRef(this.__context); - this.__context.push(context); - indexContext(this.__contextIndex, context); + for (let c of Utils.asArrayRef(context)) { + if (!this.__context.has(c)) { + this.__context.add(c); + indexContext(context, this.__contextTermIndex, this.__contextDefinitionIndex); + } + } + } + + addTermDefinition(term, definition) { + var context = Array.from(this.__context).find(c => typeof c === 'object'); + context[term] = definition; + const id = typeof definition === 'string' ? definition : definition["@id"] || term; + this.__contextTermIndex.set(term, definition); + this.__contextDefinitionIndex.set(id, term); } /** @@ -666,18 +641,39 @@ class ROCrate { * @param {string} term */ getDefinition(term) { - if (!this.__contextIndex) return; - return this.__getDefinition(this.__contextIndex, term); + // Find the context term definition from the contextIndex. + // It will also search for term defined locally in the graph. + var def = this.__contextTermIndex.get(term); + if (!def || !def['@id']) { + const id = this.resolveTerm(term); + if (id) { + def = { '@id': id }; + } + } + let localDef; + if (def && def["@id"] && (localDef = this.getEntity(def["@id"]))) { + let id; + if ((id = localDef.sameAs?.["@id"])) { + // There's a same-as - so use its ID + def["@id"] = id; + localDef = this.getEntity(id); + } + if (localDef && (this.hasType(localDef, "rdfs:Class") || this.hasType(localDef, "rdf:Property"))) { + def = localDef; + } + } + return def; + } /** * Get the context term name from it's definition id. * Make sure `resolveContext()` has been called prior calling this method. - * @param {string} definitionId + * @param {string|object} definition */ - getTerm(definitionId) { - if (!this.__contextIndex) return; - return this.__contextIndex[definitionId]; + getTerm(definition) { + var id = typeof definition === 'string' ? definition : definition['@id']; + return this.__contextDefinitionIndex.get(id); } /** @@ -692,6 +688,15 @@ class ROCrate { if (n && n[$size]) return this.__getNodeProxy(n); } + /** + * Check if entity exists in the graph + * @param {string} id An entity identifier + */ + hasEntity(id) { + let n = this.__nodeById.get(id); + return n?.[$size] > 0; + } + /** * Get an array of all nodes in the graph. Each node in the array is an Entity instance. * If config.link is true, any link to other node will be made into nested object. @@ -888,8 +893,7 @@ class ROCrate { */ async resolveContext() { let t = this; - this.__contextIndex = {}; - let results = Utils.asArray(this.__context).map(async (contextUrl) => { + let results = Array.from(this.__context, async (contextUrl) => { if (typeof contextUrl === 'string') { if (defaults.standardContexts[contextUrl]) { return defaults.standardContexts[contextUrl]["@context"]; @@ -906,13 +910,13 @@ class ROCrate { return contextUrl; } }); - results = await Promise.all(results); - - this.__contextIndex = results.reduce(indexContext, this.__contextIndex); + (await Promise.all(results)).forEach(c => indexContext(c, this.__contextTermIndex, this.__contextDefinitionIndex)); return { - _indexer: this.__contextIndex, getDefinition(term) { - return t.__getDefinition(this._indexer, term); + return t.getDefinition(term); + }, + getTerm(definition) { + return t.getTerm(definition); } }; } @@ -937,8 +941,25 @@ class ROCrate { * @return {string} */ resolveTerm(term) { - if (!this.__contextIndex) return; - return this.__resolveTerm(this.__contextIndex, term); + if (term.match(/^http(s?):\/\//i)) { + return term; + } + term = term.replace(/^schema:/, ""); //schema is the default namespace + var contextIndex = this.__contextTermIndex; + var val = contextIndex.get(term); + if (val && val['@id']) val = val['@id']; + var parts, url; + if (val && val.match(/^http(s?):\/\//i)) { + return val; + } else if (val && (parts = val.match(/(.*?):(.*)/))) { + url = contextIndex.get(parts[1]); + } else if ((parts = term.match(/(.*?):(.*)/))) { + // eg txc:Somthing + url = contextIndex.get(parts[1]); + } + if (url && url.match(/^http(s?):\/\//i)) { + return `${url}${parts[2]}`; + } } /** @@ -947,7 +968,7 @@ class ROCrate { * @return plain JSON object */ toJSON() { - return { '@context': this.__context, '@graph': this.getGraph(true) }; + return { '@context': Array.from(this.__context), '@graph': this.getGraph(true) }; } /** @@ -1162,17 +1183,18 @@ function mapProp(entity, fn, results = {}) { return results; } -function indexContext(indexer, c) { +function indexContext(c, termIndex, definitionIndex) { // Put all the keys into a flat lookup TODO: handle indirection - for (let name in c) { - const v = c[name]; - if (v) { - const id = typeof v === 'string' ? v : v["@id"] || name; - indexer[name] = id - indexer[id] = name; + if (typeof c === 'object') { + for (let name in c) { + const v = c[name]; + if (v) { + const id = typeof v === 'string' ? v : v["@id"] || name; + termIndex.set(name, v); + definitionIndex.set(id, name); + } } } - return indexer; } function addReverse(parentRef, prop, childRef) { @@ -1213,18 +1235,13 @@ function removeAllReverse(targetNode, props, filterFn) { } } -function resolveLocalContext(context) { - var indexer; - for (let c of Utils.asArray(context)) { - if (typeof c === "string") { - c = defaults.standardContexts[c]?.["@context"]; - } - if (typeof c === "object") { - if (!indexer) indexer = {}; - indexContext(indexer, c); +function resolveLocalContext(context, termIndex, definitionIndex) { + for (let c of context) { + if (typeof c === 'string') { + c = defaults.standardContexts[c]?.['@context']; } + indexContext(c, termIndex, definitionIndex); } - return indexer; } diff --git a/package.json b/package.json index ee499eb..5b26dd7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ro-crate", - "version": "3.3.6", + "version": "3.3.7", "description": "Research Object Crate (RO-Crate) utilities for making and consuming crates", "main": "index.js", "scripts": { @@ -15,7 +15,7 @@ }, "repository": { "type": "git", - "url": "https://github.com/Arkisto-Platform/ro-crate-js.git" + "url": "github:Language-Research-Technology/ro-crate-js" }, "keywords": [ "RO-Crate", diff --git a/test/rocrate.new.spec.js b/test/rocrate.new.spec.js index 9856ebd..0fa2824 100644 --- a/test/rocrate.new.spec.js +++ b/test/rocrate.new.spec.js @@ -54,6 +54,16 @@ describe("ROCrate Create new graph", function () { }); }); +describe("hasEntity", function () { + it("can find existing entity", function () { + let crate = new ROCrate(testData); + let e = crate.hasEntity('https://orcid.org/0000'); + assert.ok(e); + e = crate.hasEntity('https://orcid.org/non-existant'); + assert.ok(!e); + }); +}); + describe("getEntity", function () { it("can get a raw entity", function () { let crate = new ROCrate(testData); @@ -640,6 +650,7 @@ describe("deleteValues", function () { describe("getContext", function () { it("can return locally defined properties and classes", function () { const crate = new ROCrate(); + console.log(crate.context); assert.ok(Utils.asArray(crate.context).indexOf(defaults.context[0]) >= 0); //assert.equal(crate.context?.name, "http://schema.org/name"); assert.equal(crate.getDefinition('name')?.['@id'], 'http://schema.org/name'); @@ -653,10 +664,31 @@ describe("addContext", function () { crate.addContext({ "new_term": "http://example.com/new_term" }); assert.equal(crate.getDefinition("new_term")?.['@id'], "http://example.com/new_term"); assert.equal(crate.resolveTerm("new_term"), "http://example.com/new_term"); - console.log(crate.getDefinition('new_term')); + //console.log(crate.getDefinition('new_term')); const newCrate = new ROCrate(crate.toJSON()); assert.equal(newCrate.resolveTerm("new_term"), "http://example.com/new_term"); }); + it('can add URL entry to context', function(){ + const crate = new ROCrate({array: true}); + let l = crate.context.length; + crate.addContext('http://example.com/context'); + let c = crate.context; + assert.equal(c.length, l + 1); + assert.equal(c.pop(), 'http://example.com/context'); + l = crate.context.length; + crate.addContext('http://example.com/context'); + assert.equal(crate.context.length, l); + }); +}); +describe("addTermDefinition", function () { + it("can add a new term to existing context", async function () { + const crate = new ROCrate({array: true}); + await crate.resolveContext(); + assert.ok(!crate.getDefinition('Geometry')); + crate.addTermDefinition('Geometry', 'http://www.opengis.net/ont/geosparql#Geometry'); + assert.ok(crate.getDefinition('Geometry')); + + }); }); describe("getTerm", function () { it("can get a term from default context", async function () {