@tpluscode/rdfine
Version:
RDF/JS idiomatic, native, effective
209 lines (208 loc) • 8.36 kB
JavaScript
import { toEdgeTraversals } from '../../path.js';
import { enumerateList, isList } from '../../rdf-list.js';
import { onlyUnique } from '../../filter.js';
function getObjects(env, subjects, path) {
const nodes = path.reduce((subjects, edge) => {
const objects = [];
subjects.forEach(subject => {
objects.push(...edge(subject, env));
});
return objects;
}, subjects);
return nodes.reduce((contexts, node) => {
return contexts.concat(node.toArray());
}, []);
}
function getNodeFromEveryGraph(node, env) {
const graphs = node.datasets.reduce((set, dataset) => {
return [...dataset].reduce((set, quad) => {
return set.add(quad.graph);
}, set);
}, env.termSet());
const graphNodes = [...graphs.values()];
if (!graphNodes.length) {
return [node];
}
// TODO: when clownface gets graph feature
// return graphNodes.map(graph => node.fromGraph(graph))
return graphNodes.map(graph => env.clownface({
dataset: node.dataset,
term: node.term,
graph,
}));
}
function createProperty(proto, name, options) {
const { fromTerm, toTerm, assertSetValue, valueTypeName, initial, strict, compare, subjectFromAllGraphs, filter } = options;
let values = ['single'];
if (Array.isArray(options.values)) {
values = options.values;
}
else if (options.values) {
values = [options.values];
}
const getPath = (resource) => Array.isArray(options.path)
? toEdgeTraversals(proto.constructor.__ns, resource.env, options.path)
: toEdgeTraversals(proto.constructor.__ns, resource.env, [options.path || name]);
Object.defineProperty(proto, name, {
get() {
const rootNode = subjectFromAllGraphs ? getNodeFromEveryGraph(this.pointer, this.env) : [this.pointer];
const path = getPath(this);
let nodes = getObjects(this.env, rootNode, path);
const crossesBoundaries = path.some(edge => edge.crossesGraphBoundaries);
if (subjectFromAllGraphs || crossesBoundaries) {
values = ['array'];
}
if (filter) {
nodes = nodes.filter(node => filter(node.term));
}
const returnValues = nodes.map((obj, index) => {
if (isList(obj)) {
if (index > 0) {
throw new Error('Lists of lists are not supported');
}
return enumerateList(this, obj, fromTerm.bind(this));
}
return fromTerm.call(this, obj);
}).filter(onlyUnique(compare));
if (values.includes('array') && returnValues.length !== 1) {
return returnValues;
}
if (values.includes('list') && Array.isArray(returnValues[0])) {
return returnValues[0] || [];
}
if (returnValues.length > 1 && !values.includes('array')) {
throw new Error(`${name}: Multiple terms found where 0..1 was expected`);
}
if (Array.isArray(returnValues[0]) && !values.includes('list')) {
throw new Error(`${name}: RDF List found where 0..1 object was expected`);
}
if (this.__initialized && strict && returnValues.length === 0) {
throw new Error(`Object not found for property ${name}`);
}
return values.includes('single') ? returnValues[0] : returnValues;
},
set(value) {
if (!values.includes('array') && !values.includes('list') && Array.isArray(value)) {
throw new Error(`${name}: Cannot set array to a non-array property`);
}
const path = getPath(this);
const subjects = path.length === 1
? this.pointer.toArray()
: getObjects(this.env, [this.pointer], path.slice(0, path.length - 1));
if (subjects.length > 1) {
throw new Error('Cannot set value to multiple nodes at once');
}
const subject = subjects[0];
const lastPredicate = path[path.length - 1].predicate;
subject.out(lastPredicate).forEach(obj => {
if (isList(obj)) {
subject.deleteList(lastPredicate);
}
});
subject.deleteOut(lastPredicate);
if (value === null || typeof value === 'undefined') {
return;
}
let initializedArray = false;
let valueArray;
if (Array.isArray(value)) {
initializedArray = true;
valueArray = value;
}
else {
valueArray = [value];
}
const termsArray = valueArray.reduce((terms, valueOrFactory) => {
let term;
let value;
if (typeof valueOrFactory === 'function') {
value = valueOrFactory(this.pointer.any());
}
else {
value = valueOrFactory;
}
if (value && typeof value === 'object' && 'termType' in value) {
term = value;
}
else if (value && typeof value === 'object' && 'term' in value) {
term = value.term;
}
else if (value && typeof value === 'object' && 'pointer' in value) {
term = value.id;
}
else {
term = toTerm.call(this, value);
}
if (filter && !filter(term)) {
return terms;
}
if (!assertSetValue(value)) {
const pathStr = path.map(edge => `<${edge.predicate.value}>`).join('/');
throw new Error(`Unexpected value for path ${pathStr}. Expecting a ${valueTypeName} or RDF/JS term.`);
}
return [...terms, term];
}, []);
if (values.includes('list') && (values.length === 1 || initializedArray)) {
// only set RDF List when property only allows lists or explicitly initialized with array
if (termsArray.length === 0) {
subject.addOut(lastPredicate, this.env.ns.rdf.nil);
}
else {
subject.addList(lastPredicate, termsArray);
}
}
else {
subject.addOut(lastPredicate, termsArray);
}
},
});
if (!Object.hasOwnProperty.call(proto.constructor, '__properties')) {
proto.constructor.__properties = new Map();
}
if (!Object.hasOwnProperty.call(proto.constructor, '__initializers')) {
proto.constructor.__initializers = new Map();
}
proto.constructor.__properties.set(name, {
initial,
options: {
...options,
values,
},
});
if (initial) {
proto.constructor.__initializers.set(name, initial);
}
}
const legacyProperty = (options, proto, name) => {
createProperty(proto, name.toString(), options);
};
const standardProperty = (options, element) => {
return {
kind: 'field',
key: Symbol(),
placement: 'own',
descriptor: {},
// When @babel/plugin-proposal-decorators implements initializers,
// do this instead of the initializer below. See:
// https://github.com/babel/babel/issues/9260 extras: [
// {
// kind: 'initializer',
// placement: 'own',
// initializer: descriptor.initializer,
// }
// ],
initializer() {
if (typeof element.initializer === 'function') {
this[element.key] = element.initializer.call(this);
}
},
finisher(clazz) {
createProperty(clazz.prototype, element.key.toString(), options);
},
};
};
export function propertyDecorator(options) {
return (protoOrDescriptor, name) => (name !== undefined)
? legacyProperty(options, protoOrDescriptor, name)
: standardProperty(options, protoOrDescriptor);
}