src/rdf/InternalRDFSerializer.ts
import {
ArrayTypeDescriptor,
MapTypeDescriptor,
ObjectMemberMetadata,
ObjectMetadata,
Serializer,
SerializerFn,
TypeDescriptor,
Serializable,
DataSerializerUtils,
} from '@openhps/core';
import { IriString, Thing, XmlSchemaTypeIri, xsd } from './types';
import { DataFactory, Literal, NamedNode, Quad_Object } from 'n3';
import { RDFIdentifierOptions, RDFLiteralOptions } from '../decorators/';
import { mergeDeep } from './utils';
import { MemberSerializerOptionsParent } from '../decorators/options';
export class InternalRDFSerializer extends Serializer {
serializationStrategy = new Map<Serializable<any>, SerializerFn<any, TypeDescriptor, any>>([
[Number, this.serializeLiteral.bind(this)],
[String, this.serializeLiteral.bind(this)],
[Boolean, this.serializeLiteral.bind(this)],
[Date, this.serializeDate.bind(this)],
[Array, this.serializeArray.bind(this)],
[Map, this.serializeMap.bind(this)],
[Set, this.serializeSet.bind(this)],
]);
convertSingleValue(
sourceObject: any,
typeDescriptor: TypeDescriptor,
memberName: string,
memberOptions?: ObjectMemberMetadata,
serializerOptions: InternalSerializerOptions = {},
): Thing | Literal {
if (this.retrievePreserveNull(memberOptions) && sourceObject === null) {
return null;
}
if (sourceObject === undefined || sourceObject === null) {
return;
}
// Custom serializer
if (
memberOptions &&
memberOptions.options &&
memberOptions.options.rdf &&
memberOptions.options.rdf.serializer
) {
const output = memberOptions.options.rdf.serializer(sourceObject, serializerOptions.sourceObject, {
thing: serializerOptions.current,
baseUri: serializerOptions.rdf.baseUri ?? ('' as IriString),
dataType: typeDescriptor.ctor,
parent: serializerOptions.parent,
}) as Thing | Quad_Object;
if (output === undefined) {
return undefined;
} else if (output.termType === 'Literal') {
return output as Literal;
} else if (output.termType === 'NamedNode') {
return output as NamedNode;
} else {
return mergeDeep(
{
termType: 'BlankNode',
value: DataFactory.blankNode().value,
predicates: {},
},
output,
);
}
}
// Existing serialization strategy
const serializer = this.serializationStrategy.get(typeDescriptor.ctor);
if (serializer !== undefined) {
return serializer(sourceObject, typeDescriptor, memberName, this, memberOptions, serializerOptions);
}
// if not present in the strategy do property by property serialization
if (typeof sourceObject === 'object') {
return this.serializeObject(
sourceObject,
typeDescriptor,
memberName,
this,
memberOptions,
serializerOptions,
);
}
let error = `Could not serialize '${memberName}'; don't know how to serialize type`;
if (typeDescriptor.hasFriendlyName()) {
error += ` '${typeDescriptor.ctor.name}'`;
}
this.errorHandler(new TypeError(`${error}.`));
}
serializeObject<T, TD extends TypeDescriptor>(
sourceObject: T,
typeDescriptor: TD,
_: string,
serializer: Serializer,
__: ObjectMemberMetadata,
serializerOptions: InternalSerializerOptions,
): Thing {
let metadata: ObjectMetadata | undefined;
const rootMetadata = DataSerializerUtils.getRootMetadata(sourceObject.constructor);
if (sourceObject.constructor !== typeDescriptor.ctor) {
metadata = DataSerializerUtils.getOwnMetadata(sourceObject.constructor) ?? rootMetadata;
} else {
metadata = DataSerializerUtils.getOwnMetadata(typeDescriptor.ctor) ?? rootMetadata;
}
const options =
metadata && metadata.options && metadata.options.rdf
? metadata.options.rdf
: rootMetadata && rootMetadata.options && rootMetadata.options.rdf
? rootMetadata.options.rdf
: undefined;
if (!options) {
return undefined;
}
// Get the URI if available
const identifierMember = Array.from(metadata.dataMembers.values()).filter((member) => {
return (
member &&
member.options &&
member.options.rdf &&
(member.options.rdf as RDFIdentifierOptions).identifier
);
})[0];
let uri: string = undefined;
if ((sourceObject as any).rdf && (sourceObject as any).rdf.uri) {
uri = (sourceObject as any).rdf.uri;
} else if ((sourceObject as any).rdf && (sourceObject as any).rdf.path) {
uri = (sourceObject as any).rdf.path;
} else if (identifierMember) {
const rdfOptions = identifierMember.options.rdf as RDFIdentifierOptions;
uri = rdfOptions.serializer
? rdfOptions.serializer((sourceObject as any)[identifierMember.key] as string, sourceObject.constructor)
: ((sourceObject as any)[identifierMember.key] as string);
}
uri =
uri && !uri.startsWith('http') && serializerOptions.rdf.baseUri
? serializerOptions.rdf.baseUri.endsWith('/') && uri.startsWith('/')
? serializerOptions.rdf.baseUri + uri.substring(1)
: serializerOptions.rdf.baseUri + uri
: DataFactory.blankNode(uri).value;
if (!(sourceObject as any).rdf) {
(sourceObject as any).rdf = { uri };
}
let thing: Thing = {
value: uri,
predicates: options.predicates
? Object.entries(options.predicates)
.map(([k, v]) => {
return { [k]: v.map(DataFactory.namedNode) };
})
.reduce((a, b) => {
return { ...a, ...b };
})
: {},
};
if (options.serializer) {
thing = mergeDeep(thing, options.serializer(sourceObject, serializerOptions.rdf.baseUri));
}
thing.termType = thing.value.startsWith('http') ? 'NamedNode' : 'BlankNode';
// Current thing
if (serializerOptions.current) {
serializerOptions.parent = {
thing: serializerOptions.current,
parent: serializerOptions.parent,
};
}
serializerOptions.current = thing;
// Check for circular serialization
if (serializerOptions.root === undefined) {
serializerOptions.root = thing;
} else if (serializerOptions.root.value === thing.value) {
return undefined;
}
serializerOptions.sourceObject = sourceObject;
metadata.dataMembers.forEach((member) => {
const rootMember = rootMetadata.dataMembers.get(member.key);
const memberOptions =
member.options && member.options.rdf
? member
: rootMember && rootMember.options && rootMember.options.rdf
? rootMember
: undefined;
if (
!memberOptions ||
(!(memberOptions.options.rdf as RDFLiteralOptions).predicate &&
!memberOptions.options.rdf.serializer) ||
memberOptions.options.rdf.identifier
) {
return;
}
const object = serializer.convertSingleValue(
(sourceObject as any)[memberOptions.key],
memberOptions.type(),
`${memberOptions.name}`,
memberOptions,
{ ...serializerOptions },
);
if (object) {
const predicates = (memberOptions.options.rdf as RDFLiteralOptions).predicate;
[...(Array.isArray(predicates) ? predicates : [predicates])].forEach((predicateIri: IriString) => {
const predicate = thing.predicates[predicateIri] || [];
predicate.push(...(Array.isArray(object) ? object : [object]).filter((s) => s));
thing.predicates[predicateIri] = predicate;
});
}
});
return thing;
}
protected serializeArray(
sourceObject: Array<any>,
typeDescriptor?: ArrayTypeDescriptor,
memberName?: string,
_?: Serializer,
memberOptions?: ObjectMemberMetadata,
serializerOptions?: InternalSerializerOptions,
): (Literal | Thing)[] {
return sourceObject.map((obj) => {
return this.convertSingleValue(
obj,
typeDescriptor.elementType,
memberName,
memberOptions,
serializerOptions,
);
});
}
protected serializeMap(
sourceObject: Map<any, any>,
typeDescriptor?: MapTypeDescriptor,
memberName?: string,
_?: Serializer,
memberOptions?: ObjectMemberMetadata,
serializerOptions?: InternalSerializerOptions,
): (Literal | Thing)[] {
return Array.from(sourceObject.values()).map((obj) => {
return this.convertSingleValue(obj, typeDescriptor.valueType, memberName, memberOptions, serializerOptions);
});
}
protected serializeSet(
sourceObject: Map<any, any>,
typeDescriptor?: MapTypeDescriptor,
memberName?: string,
_?: Serializer,
memberOptions?: ObjectMemberMetadata,
serializerOptions?: any,
): (Literal | Thing)[] {
return Array.from(sourceObject.values()).map((obj) => {
return this.convertSingleValue(obj, typeDescriptor.valueType, memberName, memberOptions, serializerOptions);
});
}
protected serializeLiteral(
sourceObject: any,
typeDescriptor?: TypeDescriptor,
memberName?: string,
serializer?: Serializer,
memberOptions?: ObjectMemberMetadata,
): Literal {
let xsdDatatype: XmlSchemaTypeIri = undefined;
const rdfOptions = memberOptions.options.rdf as RDFLiteralOptions;
if (rdfOptions && rdfOptions.datatype) {
// Data type provided
xsdDatatype = rdfOptions.datatype;
switch (rdfOptions.datatype) {
case xsd.date:
case xsd.dateTime:
return this.serializeDate(sourceObject, typeDescriptor, memberName, serializer, memberOptions);
}
} else {
// Data type is not provided or not detected
switch (typeof sourceObject) {
case 'bigint':
case 'number':
xsdDatatype = xsd.double;
break;
case 'boolean':
xsdDatatype = xsd.boolean;
break;
default:
xsdDatatype = xsd.string;
break;
}
}
const dataTypeNode = this.iriToNode(xsdDatatype);
return DataFactory.literal(sourceObject, rdfOptions ? rdfOptions.language ?? dataTypeNode : dataTypeNode);
}
protected serializeDate(
sourceObject: any,
_?: TypeDescriptor,
__?: string,
___?: Serializer,
memberOptions?: ObjectMemberMetadata,
): Literal {
const rdfOptions = memberOptions.options.rdf as RDFLiteralOptions;
const xsdDatatype: XmlSchemaTypeIri = xsd.dateTime;
const dateString = new Date(sourceObject).toISOString();
const dataTypeNode = this.iriToNode(xsdDatatype);
return DataFactory.literal(dateString, rdfOptions ? rdfOptions.language ?? dataTypeNode : dataTypeNode);
}
protected iriToNode(iri: IriString): NamedNode {
return DataFactory.namedNode(iri);
}
}
interface InternalSerializerOptions {
sourceObject?: any;
root?: Thing;
current?: Thing;
parent?: MemberSerializerOptionsParent;
rdf?: {
baseUri: IriString;
};
}