Skip to content

Commit

Permalink
Add hasEntity and addTermDefinition methods
Browse files Browse the repository at this point in the history
  • Loading branch information
alvinsw committed May 21, 2024
1 parent 93f8f59 commit 2fd932b
Show file tree
Hide file tree
Showing 3 changed files with 137 additions and 88 deletions.
187 changes: 102 additions & 85 deletions lib/rocrate.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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; }

Expand All @@ -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; }
Expand Down Expand Up @@ -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);
}

/**
Expand Down Expand Up @@ -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);
}

/**
Expand All @@ -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.
Expand Down Expand Up @@ -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"];
Expand All @@ -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);
}
};
}
Expand All @@ -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]}`;
}
}

/**
Expand All @@ -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) };
}

/**
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
}


Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand All @@ -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",
Expand Down
34 changes: 33 additions & 1 deletion test/rocrate.new.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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');
Expand All @@ -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 () {
Expand Down

0 comments on commit 2fd932b

Please sign in to comment.