lipdjs
Version:
A JavaScript library for reading and writing LiPD (Linked Paleo Data) files
855 lines (762 loc) • 28.3 kB
text/typescript
import fs from 'fs';
import path from 'path';
import { SCHEMA } from '../globals/schema';
import { ONTONS } from '../globals/urls';
import { NSURL } from '../globals/urls';
import { REVERSE_BLACKLIST } from '../globals/blacklist';
import { SYNONYMS } from '../globals/synonyms';
// Get script directory and class directory
const SCRIPTDIR = __dirname;
const CLASSDIR = path.resolve(`${SCRIPTDIR}/../classes`);
// Create all necessary directories
fs.mkdirSync(path.dirname(CLASSDIR), { recursive: true });
fs.mkdirSync(CLASSDIR, { recursive: true });
console.log(`Generating TypeScript classes in: ${CLASSDIR}`);
function getToJsonItem(ptype: string, pname: string, tsType: string | null): string {
let tojsonitem = "";
if (ptype === "literal") {
tojsonitem += `
const obj = valueObj`;
} else if (ptype === "object") {
tojsonitem += `
const obj = valueObj.toJson()`;
}
return tojsonitem;
}
function getFromJsonItem(ptype: string, tsType: string | null, isEnum: boolean): string {
let fromjsonitem = "";
if (ptype === "object" && tsType !== null) {
if (isEnum) {
fromjsonitem += `
obj = ${tsType}.fromSynonym(value.replace(/^.*?#/, ""))`;
} else {
fromjsonitem += `
obj = ${tsType}.fromJson(value)`;
}
} else if (tsType) {
fromjsonitem += `
obj = value`;
} else {
fromjsonitem += `
obj = value`;
}
return fromjsonitem;
}
function getFromDictionaryItem(varid: string, ptype: string, tsType: string | null, isEnum: boolean): string {
if (ptype === "object" && tsType !== null) {
if (isEnum) {
return `new ${tsType}(${varid}.id, ${varid}.label)`;
} else {
return `${tsType}.fromDictionary(${varid})`;
}
} else {
return `${varid}`;
}
}
function getToDataItem(ptype: string, range: string | null): string {
if (ptype === "literal" && range) {
return `
const obj = {
"@value": valueObj,
"@type": "literal",
"@datatype": "http://www.w3.org/2001/XMLSchema#${range}"
}`;
} else if (ptype === "literal") {
return `
const obj = {
"@id": valueObj,
"@type": "uri"
}`;
} else if (ptype === "object") {
return `
let obj: any = null;
if (typeof valueObj === "string") {
obj = {
"@value": valueObj,
"@type": "literal",
"@datatype": "http://www.w3.org/2001/XMLSchema#string"
}
} else {
obj = {
"@id": valueObj.getId(),
"@type": "uri"
}
data = valueObj.toData(data);
}`;
}
return "";
}
function getFromDataItem(ptype: string, range: string | null, isEnum: boolean): string {
let fromdataitem = `
for (const val of value as any[]) {
let obj: any = null;`;
if (ptype === "object" && range !== null) {
if (isEnum) {
fromdataitem += `
obj = ${range}.fromSynonym(val["@id"].replace(/^.*?#/, ""));`;
} else {
fromdataitem += `
if ("@id" in val) {
obj = ${range}.fromData(val["@id"], data);
} else {
obj = val["@value"];
}`;
}
} else if (range) {
fromdataitem += `
if ("@value" in val) {
obj = val["@value"];
}`;
} else {
fromdataitem += `
obj = val["@id"];`;
}
return fromdataitem;
}
function fetchExtraTemplateFunctions(clsid: string): [string[], string[], string[]] {
const imports: string[] = [];
const structures: string[] = [];
const fns: string[] = [];
const tplfilename = `${CLASSDIR}/templates/${clsid.toLowerCase()}.ts`;
if (fs.existsSync(tplfilename)) {
console.log(`Fetching extra template functions from: ${tplfilename}`);
const fileContent = fs.readFileSync(tplfilename, 'utf8');
const lines = fileContent.split('\n');
let curfn = "";
let fnOngoing = false;
let structureOngoing = true;
for (const line of lines) {
if (/^import /.test(line.trim()) || /^from .+ import /.test(line.trim())) {
imports.push(line);
} else if (/^export class/.test(line.trim())) {
structureOngoing = false;
} else if (/^\/\/.*START TEMPLATE FUNCTION/.test(line.trim())) {
fnOngoing = true;
} else if (/^\/\/.*END TEMPLATE FUNCTION/.test(line.trim())) {
fnOngoing = false;
if (curfn) {
fns.push("\n" + curfn);
}
curfn = "";
} else if (/^\/\/.*START TYPES/.test(line.trim())) {
structureOngoing = true;
} else if (/^\/\/.*END TYPES/.test(line.trim())) {
structureOngoing = false;
} else if (fnOngoing) {
curfn += line + "\n";
} else if (structureOngoing) {
structures.push(line);
}
}
}
return [imports, structures, fns];
}
function generateClassFile(
clsid: string,
importSnippets: string[],
defvarSnippets: string[],
initvarSnippets: string[],
todataSnippets: string[],
fromdataSnippets: string[],
tojsonSnippets: string[],
fromjsonSnippets: string[],
fromdictionarySnippets: string[],
fnSnippets: string[]
): void {
const [ximports, xstructures, xfnSnippets] = fetchExtraTemplateFunctions(clsid);
fnSnippets = [...fnSnippets, ...xfnSnippets];
const filename = `${CLASSDIR}/${clsid.toLowerCase()}.ts`;
let tsCode = `
// Auto-generated. Do not edit.
import { uniqid } from "../utils/utils";
import { parseVariableValues } from "../utils/utils";
`;
// Write the imports
// for (const im of ximports) {
// tsCode += `${im}\n`;
// }
for (const im of importSnippets) {
tsCode += `import { ${im} } from "./${im.toLowerCase()}";\n`;
}
tsCode += "\n";
// Write the structures
for (const st of xstructures) {
tsCode += `${st}\n`;
}
// Write the class header
tsCode += `\n\nexport class ${clsid} {\n`;
// Write the class variables
for (const snippet of defvarSnippets) {
tsCode += `
${snippet}`;
}
// Write the init function
tsCode += `
protected _id: string;
protected _type: string;
protected _misc: Record<string, any>;
protected _ontns: string;
protected _ns: string;
constructor() {`;
for (const snippet of initvarSnippets) {
tsCode += `
${snippet}`;
}
tsCode += `
this._misc = {};
this._ontns = "${ONTONS}";
this._ns = "${NSURL}";
this._type = "${ONTONS}${clsid}";
this._id = this._ns + "/" + uniqid("${clsid}");
}
public getId(): string {
return this._id;
}
public getType(): string {
return this._type;
}
public getMisc(): Record<string, any> {
return this._misc;
}
`;
// Write the fromDictionary function
tsCode += `
public static fromDictionary(data: Record<string, any>): ${clsid} {
const thisObj = new ${clsid}();
thisObj._id = data._id;
thisObj._type = data._type;
thisObj._misc = data._misc;
thisObj._ontns = data._ontns;
thisObj._ns = data._ns;`;
for (const snippet of fromdictionarySnippets) {
tsCode += `${snippet}`;
}
tsCode += `
return thisObj;
}\n`;
// Write the fromData function
tsCode += `
public static fromData(id: string, data: Record<string, any>): ${clsid} {
const thisObj = new ${clsid}();
thisObj._id = id;
const mydata = data[id] as any;
for (const [key, value] of Object.entries(mydata)) {
if (key === "type") {
for (const val of value as any[]) {
thisObj._type = val["@id"];
}
continue;
}`;
for (const snippet of fromdataSnippets) {
tsCode += `
${snippet}`;
}
tsCode += `
else {
// Store unknown properties in misc
for (const val of value as any[]) {
let obj: any;
if ("@id" in val) {
obj = data[val["@id"]];
} else if ("@value" in val) {
obj = val["@value"];
}
thisObj._misc[key] = obj;
}
}
}
return thisObj;
}\n`;
// Write the toData function
tsCode += `
public toData(data: Record<string, any> = {}): Record<string, any> {
data[this._id] = {};
data[this._id]["type"] = [
{
"@id": this._type,
"@type": "uri"
}
]`;
for (const snippet of todataSnippets) {
tsCode += `${snippet}`;
}
tsCode += `
// Add misc properties
for (const [key, value] of Object.entries(this._misc)) {
data[this._id][key] = [];
let ptype: string | null = null;
const tp = typeof value;
if (tp === "number") {
if (Number.isInteger(value)) {
ptype = "http://www.w3.org/2001/XMLSchema#integer";
} else {
ptype = "http://www.w3.org/2001/XMLSchema#float";
}
} else if (tp === "string") {
if (/\\d{4}-\\d{2}-\\d{2}( |T)\\d{2}:\\d{2}:\\d{2}/.test(value as string)) {
ptype = "http://www.w3.org/2001/XMLSchema#datetime";
} else if (/\\d{4}-\\d{2}-\\d{2}/.test(value as string)) {
ptype = "http://www.w3.org/2001/XMLSchema#date";
} else {
ptype = "http://www.w3.org/2001/XMLSchema#string";
}
} else if (tp === "boolean") {
ptype = "http://www.w3.org/2001/XMLSchema#boolean";
}
data[this._id][key].push({
"@value": value,
"@type": "literal",
"@datatype": ptype
});
}
return data;
}\n`;
// Write the toJson function
tsCode += `
public toJson(): Record<string, any> {
const data: Record<string, any> = {
"@id": this._id
}`;
for (const snippet of tojsonSnippets) {
tsCode += `${snippet}`;
}
tsCode += `
// Add misc properties
for (const [key, value] of Object.entries(this._misc)) {
data[key] = value;
}
return data;
}\n`;
// Write the fromJson function
tsCode += `
public static fromJson(data: Record<string, any>): ${clsid} {
const thisObj = new ${clsid}();
for (const [key, pvalue] of Object.entries(data)) {
if (key === "@id") {
thisObj._id = pvalue as string;
continue;
}`;
for (const snippet of fromjsonSnippets) {
tsCode += `${snippet}`;
}
tsCode += `
// Store unknown properties in misc
thisObj._misc[key] = pvalue;
}
return thisObj;
}\n`;
// Write the functions to handle non standard properties
tsCode += `
public setNonStandardProperty(key: string, value: unknown): void {
this._misc[key] = value;
}
public getNonStandardProperty(key: string): unknown {
return this._misc[key];
}
public getAllNonStandardProperties(): Record<string, unknown> {
return this._misc;
}
public addNonStandardProperty(key: string, value: unknown): void {
if (!(key in this._misc)) {
this._misc[key] = [];
}
(this._misc[key] as unknown[]).push(value);
}
`;
// Write the getter/setter functions
for (const fn of fnSnippets) {
tsCode += fn;
}
tsCode += `\n}\n`;
// Write to file
fs.writeFileSync(filename, tsCode);
}
function getMultiValuePropertySnippets(
clsid: string,
pid: string,
propid: string,
pname: string,
ptype: string,
ontRange: string | null,
tsType: string | null,
getter: string,
setter: string,
adder: string | null,
isEnum: boolean
): [string, string, string, string, string, string, string, string] {
// Create the TypeScript snippet for initializing property variables
const defvar = `public ${pname}: ${tsType}[];`;
const initvar = `this.${pname} = [];`;
// Create the TypeScript function snippet for this property to convert the class to a dictionary (todata)
const todataitem = getToDataItem(ptype, ontRange);
const todata = `
if (this.${pname}.length > 0) {
data[this._id]["${propid}"] = [];
for (const valueObj of this.${pname}) {${todataitem}
data[this._id]["${propid}"].push(obj);
}
}`;
// Create the TypeScript function snippet for this property to convert from data dictionary to a class (fromdata)
const fromdataitem = getFromDataItem(ptype, tsType, isEnum);
const fromdata = `
else if (key === "${propid}") {
thisObj.${pname} = [];${fromdataitem}
thisObj.${pname}.push(obj);
}
}`;
// Create the TypeScript function snippet for this property to convert the class to json (tojson)
let tojson = "";
if (!(pid in REVERSE_BLACKLIST)) {
const tojsonitem = getToJsonItem(ptype, pname, tsType);
tojson = `
if (this.${pname}.length > 0) {
data["${pid}"] = [];
for (const valueObj of this.${pname}) {${tojsonitem}
data["${pid}"].push(obj);
}
}`;
}
// Create the TypeScript function snippet for this property to convert from json dictionary to a class (fromjson)
const fromjsonitem = getFromJsonItem(ptype, tsType, isEnum);
const fromjson = `
if (key === "${pid}") {
let obj: any = null;
thisObj.${pname} = [];
for (const value of pvalue as any[]) {${fromjsonitem}
thisObj.${pname}.push(obj);
}
continue;
}`;
// Create the TypeScript function snippet for this property to convert from dictionary to a class (fromdictionary)
const fromdictionaryitem = getFromDictionaryItem(`value`, ptype, tsType, isEnum);
const fromdictionary = `
thisObj.${pname} = [];
for (const value of (data.${pname} || []) as any[]) {
thisObj.${pname}.push(${fromdictionaryitem});
}`;
// Create error message for type checking
let errorMsg = `Error: '\${${pname}}' is not of type ${tsType}`;
if (isEnum) {
errorMsg += `\\nYou can create a new ${tsType} object from a string using the following syntax:\\n`;
errorMsg += `- Fetch existing ${tsType} by synonym: ${tsType}.fromSynonym("\${${pname}}")\\n`;
errorMsg += `- Create a new custom ${tsType}: new ${tsType}("\${${pname}}")`;
}
// Create the TypeScript snippet for getter/setter/adders
const fns = `
${getter}(): ${tsType}[] {
return this.${pname};
}
${setter}(${pname}: ${tsType}[]): void {
// if (!Array.isArray(${pname})) {
// throw new Error("Error: ${pname} is not an array");
// }
// if (!${pname}.every(x => x instanceof ${tsType})) {
// throw new Error(\`${errorMsg}\`);
// }
this.${pname} = ${pname};
}
${adder}(${pname}: ${tsType}): void {
// if (!(${pname} instanceof ${tsType})) {
// throw new Error(\`${errorMsg}\`);
// }
this.${pname}.push(${pname});
}`;
return [defvar, initvar, todata, fromdata, tojson, fromjson, fromdictionary, fns];
}
function getPropertySnippets(
clsid: string,
pid: string,
propid: string,
pname: string,
ptype: string,
ontRange: string | null,
tsType: string | null,
getter: string,
setter: string,
isEnum: boolean
): [string, string, string, string, string, string, string, string] {
// Create the TypeScript snippet for initializing property variables
const defvar = `public ${pname}: ${tsType} | null;`;
const initvar = `this.${pname} = null;`;
// Create the TypeScript function snippet for this property to convert the class to a dictionary (todata)
const todataitem = getToDataItem(ptype, ontRange);
const todata = `
if (this.${pname} !== null) {
const valueObj = this.${pname};${todataitem}
data[this._id]["${propid}"] = [obj];
}`;
// Create the TypeScript function snippet for this property to convert from data dictionary to a class (fromdata)
const fromdataitem = getFromDataItem(ptype, tsType, isEnum);
const fromdata = `
else if (key === "${propid}") {${fromdataitem}
thisObj.${pname} = obj;
}
}`;
// Create the TypeScript function snippet for this property to convert the class to json (tojson)
let tojson = "";
if (!(pid in REVERSE_BLACKLIST)) {
const tojsonitem = getToJsonItem(ptype, pname, tsType);
tojson = `
if (this.${pname} !== null) {
const valueObj = this.${pname};${tojsonitem}
data["${pid}"] = obj;
}`;
}
// Create the TypeScript function snippet for this property to convert from json dictionary to a class (fromjson)
const fromjsonitem = getFromJsonItem(ptype, tsType, isEnum);
const fromjson = `
if (key === "${pid}") {
let obj: any = null;
let value: any = pvalue;${fromjsonitem}
thisObj.${pname} = obj;
continue;
}`;
// Create the TypeScript function snippet for this property to convert from json dictionary to a class (fromjson)
const fromdictionaryitem = getFromDictionaryItem(`data.${pname}`, ptype, tsType, isEnum);
const fromdictionary = `
if (data.${pname} !== null) {
thisObj.${pname} = ${fromdictionaryitem};
}`;
// Create error message for type checking
let errorMsg = `Error: '\${${pname}}' is not of type ${tsType}`;
if (isEnum) {
errorMsg += `\\nYou can create a new ${tsType} object from a string using the following syntax:\\n`;
errorMsg += `- Fetch existing ${tsType} by synonym: ${tsType}.fromSynonym("\${${pname}}")\\n`;
errorMsg += `- Create a new custom ${tsType}: new ${tsType}("\${${pname}}")`;
}
// Create the TypeScript snippet for getter/setter
let fns = `
${getter}(): ${tsType} | null {
return this.${pname};
}
${setter}(${pname}: ${tsType}): void {
// if (!(${pname} instanceof ${tsType})) {
// throw new Error(\`${errorMsg}\`);
// }
this.${pname} = ${pname};`
// Special case for Dataset
if (clsid === "Dataset" && pname === "name") {
fns += `
this._id = this._ns + '/' + ${pname}; // This is a hack to set the id of the dataset based on the name`;
}
fns += `
}`;
return [defvar, initvar, todata, fromdata, tojson, fromjson, fromdictionary,fns];
}
function generateEnumClasses(): void {
for (const sectionid in SYNONYMS) {
for (const clsid in SYNONYMS[sectionid as keyof typeof SYNONYMS]) {
const done: Record<string, boolean> = {};
const synonyms = (SYNONYMS[sectionid as keyof typeof SYNONYMS] as any)[clsid];
const fileid = clsid.toLowerCase();
const filename = `${CLASSDIR}/${fileid}.ts`;
let tsCode = `
// Auto-generated. Do not edit.
import { SYNONYMS } from "../globals/synonyms";
export class ${clsid} {
private id: string;
private label: string;
static synonyms: any = SYNONYMS.${sectionid}?.${clsid};
constructor(id: string, label: string) {
this.id = id;
this.label = label;
}
equals(value: ${clsid}): boolean {
return this.id === value.id;
}
getLabel(): string {
return this.label;
}
getId(): string {
return this.id;
}
toData(data: Record<string, any> = {}): Record<string, any> {
data[this.id] = {
"label": [
{
"@datatype": null,
"@type": "literal",
"@value": this.label
}
]
};
return data;
}
toJson(): string {
return this.label;
}
static fromSynonym(synonym: string): ${clsid} | null {
const lowerSynonym = synonym.toLowerCase();
if (lowerSynonym in ${clsid}.synonyms) {
const synobj = ${clsid}.synonyms[lowerSynonym];
return new ${clsid}(synobj.id, synobj.label);
}
return null;
}
}
export class ${clsid}Constants {`;
for (const synonym in synonyms) {
const synobj = synonyms[synonym];
const synid = synobj.id.replace(/.*?#/, "").replace(/[^a-zA-Z0-9]/g, "_");
if (synid in done) {
continue;
}
done[synid] = true;
tsCode += `
static ${synid} = new ${clsid}("${synobj.id}", "${synobj.label}");`;
}
tsCode += `
}`;
fs.writeFileSync(filename, tsCode);
}
}
}
function generateLipdClasses(): void {
// Check all classes in Schema
for (const clsid in SCHEMA) {
const props = SCHEMA[clsid as keyof typeof SCHEMA];
const importSnippets = new Set<string>();
const defvarSnippets = new Set<string>();
const initvarSnippets = new Set<string>();
const fromdataSnippets = new Set<string>();
const todataSnippets = new Set<string>();
const tojsonSnippets = new Set<string>();
const fromjsonSnippets = new Set<string>();
const fromdictionarySnippets = new Set<string>();
const fnSnippets = new Set<string>();
// Check all properties
for (const pid in props) {
if (pid[0] === "@") {
continue;
}
if (pid == "fromJson" || pid == "toJson") {
continue;
}
const prop = props[pid as keyof typeof props];
let propid = pid;
if ("name" in prop) {
propid = prop["name"];
}
// Create the TypeScript property name (pname) from ontology property id (propid)
let pname = propid;
if (propid.startsWith("has")) {
pname = propid.substring(3);
} else if (propid.startsWith("is")) {
pname = propid.substring(2);
}
// Fix if property name is "type". Gets mixed up
if (pname.toLowerCase() === "type") {
pname = `${clsid}Type`;
}
pname = pname[0].toLowerCase() + pname.substring(1);
// Check if the property is supposed to have multiple values. Rename accordingly
const multiple = (prop as any).multiple || false;
let mpname = pname;
if (multiple && !pname.match(/.*(data|by|s)$/i)) {
mpname += "s";
}
const isEnum = "synonyms" in prop;
// Create getter/setter/adder function names
let adder: string | null = null;
const suffix = pname[0].toUpperCase() + pname.substring(1);
let setter = "set" + suffix;
let getter: string;
if (propid.startsWith("is")) {
getter = "is" + suffix;
} else {
getter = "get" + suffix;
}
if (multiple) {
adder = "add" + suffix;
if (!pname.match(/.*(data|by|s)$/i)) {
setter += "s";
getter += "s";
}
}
// Get the range of the property
let ontRange: string | null = "string";
let ptype = "literal";
if ("class_type" in prop) {
ontRange = prop["class_type"];
} else if ("type" in prop) {
ontRange = prop["type"];
if (ontRange === "Individual") {
ontRange = null;
}
}
if ("class_range" in prop) {
ptype = "object";
ontRange = prop["class_range"];
} else if ("schema" in prop) {
ptype = "object";
ontRange = prop["schema"];
}
let tsType = ontRange || "object";
// Rewrite the property range to TypeScript types
if (ontRange === "integer") {
tsType = "number";
} else if (ontRange === "float") {
tsType = "number";
} else if (ontRange === "string") {
tsType = "string";
} else if (ontRange === "boolean") {
tsType = "boolean";
} else if (ontRange === "datetime") {
tsType = "Date";
} else if (ontRange === "date") {
tsType = "Date";
}
// Get TypeScript snippets for initialization function, todata function, fromdata from, and the setter/getter functions
let defvar: string;
let initvar: string;
let todata: string;
let fromdata: string;
let tojson: string;
let fromjson: string;
let fromdictionary: string;
let fns: string;
if (multiple) {
[defvar, initvar, todata, fromdata, tojson, fromjson, fromdictionary, fns] = getMultiValuePropertySnippets(
clsid, pid, propid, mpname, ptype,
ontRange, tsType,
getter, setter, adder, isEnum
);
} else {
[defvar, initvar, todata, fromdata, tojson, fromjson, fromdictionary, fns] = getPropertySnippets(
clsid, pid, propid, mpname, ptype,
ontRange, tsType,
getter, setter, isEnum
);
}
// Collect all snippets
// Import the range class (in case the range is a class)
if (ptype === "object") {
importSnippets.add(tsType ?? "");
}
defvarSnippets.add(defvar);
initvarSnippets.add(initvar);
todataSnippets.add(todata);
fromdataSnippets.add(fromdata);
tojsonSnippets.add(tojson);
fromjsonSnippets.add(fromjson);
fromdictionarySnippets.add(fromdictionary);
fnSnippets.add(fns);
}
// Write outputs
generateClassFile(
clsid,
Array.from(importSnippets).sort(),
Array.from(defvarSnippets).sort(),
Array.from(initvarSnippets).sort(),
Array.from(todataSnippets).sort(),
Array.from(fromdataSnippets).sort(),
Array.from(tojsonSnippets).sort(),
Array.from(fromjsonSnippets).sort(),
Array.from(fromdictionarySnippets).sort(),
Array.from(fnSnippets).sort()
);
}
}
// Main execution
generateLipdClasses();
generateEnumClasses();