Skip to content

Instantly share code, notes, and snippets.

@sebilasse
Last active April 20, 2023 09:04
Show Gist options
  • Save sebilasse/98aa5e5fa6718e5443cbe0d02e2e27eb to your computer and use it in GitHub Desktop.
Save sebilasse/98aa5e5fa6718e5443cbe0d02e2e27eb to your computer and use it in GitHub Desktop.
@Jsonld (decorators)
import { Reflect } from "https://deno.land/x/reflect_metadata@v0.1.12/mod.ts";
import { crypto } from "https://deno.land/std@0.182.0/crypto/mod.ts";
import * as log from "https://deno.land/std@0.182.0/log/mod.ts";
import defaultContext, { transportInsecure } from "/protocol/default.ts";
import prefixes from "/redaktor/known/prefixes.ts";
import equalsPrimitives from "objectIsEqualPrimitivesShallow";
import normal from "objectNormal";
import jsonld from "jsonld";
import { toArray } from "to";
export async function compact(doc: string | {[k: string]: any}, ctx: string | {[k: string]: any}) {
if (typeof doc === "string" && doc.trim().charAt(0) === "{") doc = JSON.parse(doc);
if (typeof ctx === "string" && ctx.trim().charAt(0) === "{") ctx = JSON.parse(ctx);
return jsonld.compact(doc, ctx);
}
export async function expand(doc: string | {[k: string]: any}) {
if (typeof doc === "string" && doc.trim().charAt(0) === "{") doc = JSON.parse(doc);
return jsonld.expand(doc);
}
/* JSON LD Decorators */
const CONTEXT$ = Symbol.for('redaktor@context');
const PREFIX$ = Symbol.for('redaktor@prefix');
const SCHEMA$ = Symbol.for('redaktor@schema');
const FUNCTIONAL = "owl:FunctionalProperty";
const DATATYPE = "owl:DatatypeProperty";
const SHORTCUTS = {
"@id": "@id",
"@index": "@index",
"@graph": "@graph",
"@language": "@language",
"@list": "@list",
"@set": "@set",
"@type": "@type"
}
const C = {
id: "@id",
index: "@index",
graph: "@graph",
language: "@language",
list: "@list",
set: "@set",
type: "@type"
}
const D = {
string: "xsd:string",
id: "@id",
json: "@json",
uri: "xsd:anyURI",
token: "xsd:token",
set: "@set",
boolean: "xsd:boolean",
decimal: "xsd:decimal",
int: "xsd:int",
intPositive: "xsd:nonNegativeInteger",
/* TODO xsd:dateTime also DtoSchema
https://www.w3.org/TR/activitystreams-core/#dates
as2-partial-time = time-hour ":" time-minute [":" time-second]
[time-secfrac]
as2-full-time = as2-partial-time time-offset
as2-date-time = full-date "T" as2-full-time
TODO "title" and "description" from TSDOC or wdt: ?
https://json-schema.org/draft/2020-12/json-schema-validation.html#name-title-and-description
+ "default", "deprecated", "readOnly" and "writeOnly", "examples"
TODO custom err message via decorator
*/
}
const P = {
/** Activity Vocabulary - W3C Rec. */
as: prefixes.as,
/** The Schema.org vocabulary */
schema: prefixes.schema,
/** wikidata */
wd: prefixes.wd,
wdt: prefixes.wdt,
/** XML Schema Datatypes, XML Schema Part 2: Datatypes Second Edition - W3C Rec. */
xsd: prefixes.xsd
}
const DtoSchema = {
"xsd:string": { type: "string" },
"@id": { type: "string", format: "iri" },
"@json": { type: "string", format: "redaktor-json" },
"xsd:anyURI": { type: "string", format: "uri" },
"xsd:token": { type: "string" },
"@set": { type: "array" },
"xsd:boolean": { type: "boolean" },
"xsd:decimal": { type: "number" },
"xsd:int": { type: "integer" },
"xsd:nonNegativeInteger": { type: "integer", exclusiveMinimum: 0 }
}
type ContainerType = string & Function;
type ContainerTypes = {
set: ContainerType, list: ContainerType, graph: ContainerType,
id: ContainerType, index: ContainerType, language: ContainerType,
idMap: ContainerType, indexMap: ContainerType, languageMap: ContainerType, typeMap: ContainerType
};
const CACHE = {};
const pipe = (...fns) => (x) => fns.reduce((v, f) => f(v), x);
const targetIds = (fn: Function|FunctionConstructor, key?: string) => fn.toString().split("{")[0]
.replace(/(^class )|( extends )/g, " ").trim().split(" ")
.map((s, i) => `redaktor.${s.trim()}`);
const targetType = (tstrType: any) => {
if (tstrType === String) {
return D.string
} else if (tstrType === Number) {
return D.decimal
} else if (tstrType === Boolean) {
return D.boolean
} else if (tstrType === Object) {
return "owl:ObjectProperty"
}
}
let curSchema: {format?: string[]} & {[k:string]: number|string|(number|string)[]} = {};
const setCacheGetId = (constructor: any, key: string) => {
const tId = targetIds(constructor)[0];
if (!CACHE[tId]) curSchema = {};
if (!CACHE[tId]) Object.assign(CACHE, {[tId]: { [CONTEXT$]: {"@version": 1.1}, [PREFIX$]: P }});
if (!CACHE[tId][CONTEXT$][key]) {
Object.assign(CACHE[tId][CONTEXT$], {
[key]: { id: "", container: [], protected: false, type: { ts: [], ld: [] }, schema: {} }
});
}
return tId;
}
const checkPrefixOrUrl = (s: string, key: string, ctxPrefixes: {[k: string]: string} = {}): boolean => {
if (SHORTCUTS[s] || key.indexOf(":@") > 0) return true;
if (s.indexOf(":") > -1) {
const [prefix, vId] = s.split(":");
if (!transportInsecure[prefix] && !P[prefix] && !ctxPrefixes[prefix]) {
// prefix not found
return false;
}
} else {
log.warning(`${key}: No protocol or prefix`);
log.warning(`This is not an URI and could not be expanded to an URI.
It will be treated as plain JSON ...`);
return false;
}
return true;
}
const jsonschemaFromContext = (ctx: any) => {
}
export function protectFn(target: any, key: string) {
const t = Reflect.getMetadata("design:type", target, key);
const tId = setCacheGetId(target.constructor, key);
CACHE[tId][CONTEXT$][key].protected = true;
}
export const protect: Function = protectFn;
export function type(ld: string | string[] = D.id, cType: string | string[] = [], jsonSchema?: any): Function {
return function (target: any, key: string) {
const tId = setCacheGetId(target.constructor, key);
CACHE[tId][CONTEXT$][key].type.ld = ld;
if (jsonSchema) CACHE[tId][CONTEXT$][key].schema = jsonSchema;
const cTypes = toArray(cType);
if (cTypes.length) {
CACHE[tId][CONTEXT$][key].container = CACHE[tId][CONTEXT$][key].container
.concat(cTypes);
Reflect.defineMetadata("type-decorator", cType, target, key);
}
// console.log("2 @type", ld, tId, ":", key);
Reflect.defineMetadata("type-decorator", ld, target, key);
}
}
export function container(cType: string | string[] | false = C.set, idStr?: string): Function {
return function (target: any, key: string) {
const cTypes = toArray(cType);
const tId = setCacheGetId(target.constructor, key);
if (!cType) {
CACHE[tId][CONTEXT$][key].container = false;
Reflect.defineMetadata("container-decorator", false, target, key);
return;
}
CACHE[tId][CONTEXT$][key].container = CACHE[tId][CONTEXT$][key].container
.concat(cTypes);
// console.log("2 @type", ld, tId, ":", key);
Reflect.defineMetadata("container-decorator", cTypes, target, key);
if (idStr) id(idStr)(target, key);
};
}
/* TODO
// ["@graph", "@index"]
// ["@graph", "@id"]
*/
container.set = container(C.set);
container.list = container(C.list);
container.graph = container(C.graph);
container.id = container(C.id);
container.index = container(C.index);
container.language = container(C.language);
container.idMap = container([C.id, C.set]);
container.indexMap = container([C.index, C.set]);
container.languageMap = container([C.language, C.set]);
container.typeMap = container([C.type, C.set]);
export function id(id: string): Function & ContainerTypes {
console.log(id);
const idFn = function (target: any, key: string) {
console.log("1 @id", key);
const t = Reflect.getMetadata("design:type", target, key);
const tId = setCacheGetId(target.constructor, key);
const [prefix, vId] = id.split(":");
if (prefix && vId && !CACHE[tId][PREFIX$][prefix]) {
CACHE[tId][PREFIX$][prefix] = prefixes[prefix] ? prefixes[prefix] : "";
}
if (CACHE[tId][CONTEXT$][key].id && CACHE[tId][CONTEXT$][key].id !== id) {
log.warning(`${tId}: JSON-LD context: Key already defined`);
log.warning(`The key ${key} was already defined as ${CACHE[tId][CONTEXT$][key].id}.
It will be overwritten as ${id} ...`);
}
CACHE[tId][CONTEXT$][key].id = id;
CACHE[tId][CONTEXT$][key].type.ts = targetType(t);
console.log("2 @id", id, target, ":", key);
Reflect.defineMetadata("id-decorator", id, target, key);
};
idFn.set = Object.assign(container(C.set, id), idFn);
idFn.list = Object.assign(container(C.list, id), idFn);
idFn.graph = Object.assign(container(C.graph, id), idFn);
idFn.id = Object.assign(container(C.id, id), idFn);
idFn.index = Object.assign(container(C.index, id), idFn);
idFn.language = Object.assign(container(C.language, id), idFn);
idFn.idMap = Object.assign(container([C.id, C.set], id), idFn);
idFn.indexMap = Object.assign(container([C.index, C.set], id), idFn);
idFn.languageMap = Object.assign(container([C.language, C.set], id), idFn);
idFn.typeMap = Object.assign(container([C.type, C.set], id), idFn);
return idFn as any
}
export type Constructor = {
new (...args: any[]): {}
prototype: any;
name: string;
}
export function context(contextPrefix: string, customPrefixes = {}): Function {
return function <T extends { new (...args: any[]): {} }>(constructor: T): T | void {
const targets = targetIds(constructor);
const tId = targets[0];
const ctxPrefixes = {};
CACHE[tId][PREFIX$][contextPrefix] = "";
for (const k in JSON.parse(JSON.stringify(CACHE[tId][PREFIX$]))) {
if (CACHE[tId][PREFIX$][k]) {
ctxPrefixes[k] = CACHE[tId][PREFIX$][k];
continue;
}
if (customPrefixes[k]) {
ctxPrefixes[k] = customPrefixes[k];
continue;
}
if (prefixes[k]) {
ctxPrefixes[k] = prefixes[k];
continue;
}
}
for (const k in CACHE[tId][CONTEXT$]) {
if (k.charAt(0) === "@" || !CACHE[tId][CONTEXT$][k] || !CACHE[tId][CONTEXT$][k].type) continue;
const _type = CACHE[tId][CONTEXT$][k].type.ld.length
? CACHE[tId][CONTEXT$][k].type.ld
: CACHE[tId][CONTEXT$][k].type.ts;
// TODO CACHE[tId][CONTEXT$][k].schema console.log("SET",jsonSchema,tId,k,CACHE[tId][CONTEXT$][k])
if (!CACHE[tId][CONTEXT$][k].schema) CACHE[tId][CONTEXT$][k].schema = {};
if (!CACHE[tId][CONTEXT$][k].schema.type && DtoSchema[_type]) {
CACHE[tId][CONTEXT$][k].schema = {...CACHE[tId][CONTEXT$][k].schema, ...DtoSchema[_type]};
}
console.log(k, CACHE[tId][CONTEXT$][k].schema);
const isProtected = CACHE[tId][CONTEXT$][k].protected;
CACHE[tId][CONTEXT$][k] = {
"@id": CACHE[tId][CONTEXT$][k].id || k,
"@type": _type,
"@container": (CACHE[tId][CONTEXT$][k].container.length
? CACHE[tId][CONTEXT$][k].container
: [])
}
if (isProtected) CACHE[tId][CONTEXT$][k]["@protected"] = true;
if (targets.length === 2 && CACHE[targets[1]][CONTEXT$][k]) {
if (CACHE[targets[1]][CONTEXT$][k] && !equalsPrimitives(CACHE[tId][CONTEXT$][k], CACHE[targets[1]][CONTEXT$][k])) {
const wasProtected = CACHE[targets[1]][CONTEXT$][k]["@protected"];
if (wasProtected) CACHE[tId][CONTEXT$][k] = CACHE[targets[1]][CONTEXT$][k];
log[wasProtected ? "critical" : "warning"](`${tId}: JSON-LD context: Key has different definition in base class`);
log.warning(`The key ${k} was already defined as ${CACHE[targets[1]][CONTEXT$][k]["@id"]} in ${targets[1]}.
${wasProtected ? `It was protected, can't overwrite it.`: `Overwriting it as ${CACHE[targets[0]][CONTEXT$][k]}`}`)
}
}
checkPrefixOrUrl(CACHE[tId][CONTEXT$][k]["@id"], `${tId}:@id`, ctxPrefixes);
checkPrefixOrUrl(CACHE[tId][CONTEXT$][k]["@type"], `${tId}:@type`, ctxPrefixes);
if (CACHE[tId][CONTEXT$][k]["@container"].length) {
CACHE[tId][CONTEXT$][k]["@container"].forEach((container) => {
checkPrefixOrUrl(container, `${tId}:@container`, ctxPrefixes);
})
}
}
const extended = targets.length > 1 ? CACHE[targets[1]][CONTEXT$] : {};
CACHE[tId][CONTEXT$] = {
"@version": 1.1,
"@id": "",
...ctxPrefixes,
...extended,
...CACHE[tId][CONTEXT$]
};
// Check if we have URLs for all prefixes
for (const k in CACHE[tId][CONTEXT$]) {
if (k.charAt(0) === "@") continue;
const prefix = k.split(":")[0];
if (typeof CACHE[tId][CONTEXT$][k] === "string" && !!CACHE[tId][CONTEXT$][prefix]) continue;
if (typeof CACHE[tId][CONTEXT$][k] === "object" && !!CACHE[tId][CONTEXT$][prefix]) continue;
log.critical(`${tId}: JSON-LD context: Prefix not found`);
log.warning(`The prefix "${prefix}" could not be found. It will be ignored.
Keys beginning with "${prefix}:" in ${tId} like ${k} will be treated as plain JSON.
You can supply custom prefixes in the context-decorator as second property like
@context( "name", {[prefix]: IRI} )`);
}
CACHE[tId][CONTEXT$]["@id"] = tId.replace('redaktor.', `${contextPrefix}:`);
delete CACHE[tId][PREFIX$];
const newClass = (class extends constructor {
constructor(...args: any[]) {
super(args[0]);
for (const k in args[0]) { this[k] = args[0][k]; }
}
toJSON() {
return {
"@context": [
defaultContext,
CACHE[tId][CONTEXT$]
],
...this
}
}
async toHash() {
// if (this["toHash"]) return this["toHash"]();
const encoder = new TextEncoder();
const json = JSON.stringify(this);
const data = encoder.encode(JSON.stringify(normal(json, {isJSON: true})));
const buf = await crypto.subtle.digest("BLAKE3", data);
const hashArray = Array.from(new Uint8Array(buf));
const hashHex = hashArray.map((b) => b.toString(16).padStart(2, "0"))
.join(""); // convert bytes to hex string
return hashHex;
}
} as T)
Object.freeze(newClass.prototype);
return newClass
}
}
/*
const schemaFns = {
"any": new Set(["type", "enum", "const", "maxItems", "minItems", "uniqueItems", "maxContains", "minContains"]),
"number": new Set(["multipleOf", "maximum", "exclusiveMaximum", "minimum", "exclusiveMinimum"]),
"string": new Set(["maxLength", "minLength", "pattern"]),
"array":
}
// "null", "boolean", "object", "array", "integer", "number", or "string"
const typeProxy = (ret: any, schemaType: string) => ({
get: (object, property) => {
return (v: any) => {
console.log(property, v)
if (v) {
return new Proxy(object, typeProxy(ret, schemaType));
}
return ret;
}
}
});
type.decimal = new Proxy(type(D.decimal), typeProxy(type(D.decimal), "number"));
*/
type SchemaStringFormat = "date-time"|"date"|"time"|"duration"|"email"|"idn-email"|"hostname"|"idn-hostname"|
"uri"|"uri-reference"|"iri"|"iri-reference"|"uuid"|"uri-template"|"json-pointer"|"relative-json-pointer"|"regex"|
// custom:
"json"|"booleanString"|"numberString"|"uppercase"|"lowercase"|"alpha"|"alphaNumeric"|"phoneNumber"|"phoneMobile"|
"locale"|"country"|"calendar"|"collation"|"currency"|"numberingSystem"|"timeZone"|"unit";
/*
https://github.com/typestack/class-validator
Contains
NotContains
NotEquals
IsNotEmpty
MinDate
MaxDate
*/
const everyRuntime = (schemaType: string, dType: string, ret: any) => ({
enum: (v: any[]) => {
v = toArray(v);
curSchema = {type: schemaType, ...curSchema, enum: Array.from(new Set(v))};
return Object.assign(type(D[dType], [], curSchema), ret);
},
const: (v: any) => {
curSchema = {type: schemaType, ...curSchema, const: v};
return Object.assign(type(D[dType], [], curSchema), ret);
}
});
const Dnr = {decimal: "number", int: "integer"};
const nrRuntime = (nrType: "decimal"|"int" = "decimal", fixedSchema = {}) => ({
maximum: (v: number) => {
curSchema = {type: Dnr[nrType], ...curSchema, ...fixedSchema, maximum: v};
return Object.assign(type(D[nrType], [], curSchema), nrRuntime(nrType, fixedSchema));
},
exclusiveMaximum: (v: number) => {
curSchema = {type: Dnr[nrType], ...curSchema, ...fixedSchema, exclusiveMaximum: v};
return Object.assign(type(D[nrType], [], curSchema), nrRuntime(nrType, fixedSchema));
},
minimum: (v: number) => {
curSchema = {type: Dnr[nrType], ...curSchema, ...fixedSchema, minimum: v};
return Object.assign(type(D[nrType], [], curSchema), nrRuntime(nrType, fixedSchema));
},
exclusiveMinimum: (v: number) => {
curSchema = {type: Dnr[nrType], ...curSchema, ...fixedSchema, exclusiveMinimum: v};
return Object.assign(type(D[nrType], [], curSchema), nrRuntime(nrType, fixedSchema));
},
multipleOf: (v: number) => {
curSchema = {type: Dnr[nrType], ...curSchema, ...fixedSchema, multipleOf: v};
return Object.assign(type(D[nrType], [], curSchema), nrRuntime(nrType, fixedSchema));
}
});
const numberRuntime = (nrType: "decimal"|"int" = "decimal", fixedSchema = {}) => ({
...everyRuntime(Dnr[nrType], nrType, nrRuntime(nrType, fixedSchema)),
...nrRuntime(nrType, fixedSchema)
});
const strRuntime = (strType: "string"|"token"|"uri"|"id"|"json" = "string", fixedSchema = {}) => ({
format: (v: SchemaStringFormat) => {
if (!curSchema.format) curSchema.format = [];
curSchema = {type: "string", ...curSchema, format: curSchema.format.concat(toArray(v))};
return Object.assign(type(D[strType], [], curSchema), strRuntime(strType, fixedSchema));
},
maxLength: (v: number) => {
curSchema = {type: "string", ...curSchema, ...fixedSchema, maxLength: v};
return Object.assign(type(D[strType], [], curSchema), strRuntime(strType, fixedSchema));
},
minLength: (v: number) => {
curSchema = {type: "string", ...curSchema, ...fixedSchema, minLength: v};
return Object.assign(type(D[strType], [], curSchema), strRuntime(strType, fixedSchema));
},
pattern: (v: string) => {
curSchema = {type: "string", ...curSchema, ...fixedSchema, pattern: v};
return Object.assign(type(D[strType], [], curSchema), strRuntime(strType, fixedSchema));
}
});
const stringRuntime = (strType: "string"|"token"|"uri"|"id"|"json" = "string", fixedSchema = {}) => ({
...everyRuntime("string", strType, strRuntime(strType, fixedSchema)),
...strRuntime(strType, fixedSchema)
});
type.boolean = type(D.boolean);
type.decimal = Object.assign(type(D.decimal), numberRuntime());
type.integer = Object.assign(type(D.int), numberRuntime("int"));
type.integerPositive = Object.assign(type(D.int), numberRuntime("int"), { exclusiveMinimum: 0 });
type.string = Object.assign(type(D.string), stringRuntime());
type.token = Object.assign(type(D.token), stringRuntime("token"));
type.uri = Object.assign(type(D.uri), stringRuntime("uri"), { format: "uri" });
type.id = Object.assign(type(D.id), stringRuntime("id"), { format: "iri" });
type.idOrObject = Object.assign(type(D.id, ["@set"]), stringRuntime("id"), { format: "iri" });
type.idOnly = Object.assign(type(D.id, ["@set", "@id"]), stringRuntime("id"), { format: "iri" });
type.json = Object.assign(type(D.json), stringRuntime("json"), { format: "redaktor-json" });
// TODO set, list, array, allRuntime :
type.set = type(D.set);
// TODO object
/*
export const functional = {
id: type([FUNCTIONAL, "owl:ObjectProperty", "@id"]),
string: type([FUNCTIONAL, DATATYPE, "xsd:string"]),
uri: type([FUNCTIONAL, DATATYPE, "xsd:anyURI"]),
token: type([FUNCTIONAL, DATATYPE, "xsd:token"]),
decimal: type([FUNCTIONAL, DATATYPE, "xsd:decimal"]),
int: type([FUNCTIONAL, DATATYPE, "xsd:int"]),
integerPositive: type([FUNCTIONAL, DATATYPE, "xsd:nonNegativeInteger"]),
};
*/
import { Reflect } from "https://deno.land/x/reflect_metadata@v0.1.12/mod.ts";
import { context, id, type, container, protect } from "@jsonld";
@context("as")
class AS {
constructor(args: AS) {}
@id("as:name") @container.set @type.string @protect
protected name: string | string[];
xy: any
}
@context("redaktor")
class MyClass extends AS {
constructor(args: MyClass) { super(args) }
@id("wdt:P2048") @type.integerPositive
population: number;
@id("redaktor:test") @type.idOrObject @protect
protected anotherProperty: string;
// SHOULD error cause protected:
/*
@id("as:name") @container.set @type.string
name: string | string[];
*/
}
const x = new MyClass({population: 2, name: "Ed", xy:() => { return ""}});
const JSONRES = JSON.stringify(x, null, 2); // <- THE MAGIC
console.log(x, '___', JSONRES, '___');
// Retrieve the metadata for the decorator
const metadata = Reflect.getMetadata("id-decorator", MyClass.prototype, "population");
console.log(3, metadata);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment