[mod] code structure [add] support for templating

This commit is contained in:
Christian Fraß 2023-08-08 14:39:21 +02:00
parent e0daa85c88
commit 40f6550246
13 changed files with 778 additions and 324 deletions

127
source/base.ts Normal file
View file

@ -0,0 +1,127 @@
declare var __dirname;
namespace _sindri
{
/**
*/
export type type_input = {
domains : Array<
{
name : string;
description : (null | string);
key_field : (
null
|
{
name : string;
description ?: (null | string);
}
);
data_fields : Array<
{
name : string;
description : (null | string);
type : ("boolean" | "integer" | "float" | "string_short" | "string_medium" | "string_long");
nullable : boolean;
default : (null | boolean | int | float | string);
}
>;
constraints ?: Array<
{
kind : ("unique" | "foreign_key");
parameters : Record<string, any>;
}
>;
}
>;
};
/**
*/
export type type_output = {
render : ((input_data : type_input) => Promise<string>);
};
/**
*/
export enum enum_realm {
database = "database",
backend = "backend",
frontend = "frontend",
other = "other",
}
/**
*/
var _outputs : Record<enum_realm, Record<string, _sindri.type_output>> = {
[enum_realm.database]: {},
[enum_realm.backend]: {},
[enum_realm.frontend]: {},
[enum_realm.other]: {},
};
/**
*/
export function add_output(
realm : enum_realm,
implementation : string,
output : _sindri.type_output
) : void
{
_outputs[realm][implementation] = output;
}
/**
*/
export function get_output(
realm : enum_realm,
implementation : string
) : _sindri.type_output
{
return _outputs[realm][implementation];
}
/**
*/
export function list_outputs(
) : Array<{realm : enum_realm; implementation : string;}>
{
return (
Object.entries(_outputs)
.map(
([realm, group]) => (
Object.keys(group)
.map(
implementation => ({"realm": (realm as enum_realm), "implementation": implementation})
)
)
)
.reduce(
(x, y) => x.concat(y),
[]
)
);
}
/**
*/
export function get_template(
realm : enum_realm,
implementation : string,
name : string
) : Promise<string>
{
return lib_plankton.file.read(
[__dirname, "templates", realm, implementation, name].join("/")
);
}
}

View file

@ -1,10 +1,12 @@
namespace _sindri
{
/** /**
* @todo generate generic * @todo generate generic
*/ */
function input_schema( export function input_schema(
) : any ) : any
{ {
return { return {
"type": "object", "type": "object",
"additionalProperties": false, "additionalProperties": false,
@ -118,15 +120,15 @@ function input_schema(
"domains" "domains"
] ]
} }
} }
/** /**
*/ */
function input_normalize( export function input_normalize(
input_raw : any input_raw : any
) : type_input ) : type_input
{ {
// validate // validate
if (! input_raw.hasOwnProperty("domains")) { if (! input_raw.hasOwnProperty("domains")) {
throw (new Error("input node is missing mandatory field 'domains'")); throw (new Error("input node is missing mandatory field 'domains'"));
@ -182,4 +184,6 @@ function input_normalize(
), ),
}; };
} }
}
} }

View file

@ -1,17 +1,12 @@
/** namespace _sindri
*/
async function main(
args_raw : Array<string>
) : Promise<void>
{ {
const outputs : Record<string, type_output> = {
"jsonschema": output_jsonschema,
"database-sqlite": output_database_sqlite,
"database-mysql": output_database_mysql,
"backend-typescript": output_backend_typescript,
"frontend-typescript": output_frontend_typescript,
};
/**
*/
export async function main(
args_raw : Array<string>
) : Promise<void>
{
const arg_handler = new lib_plankton.args.class_handler( const arg_handler = new lib_plankton.args.class_handler(
{ {
"format": new lib_plankton.args.class_argument({ "format": new lib_plankton.args.class_argument({
@ -19,13 +14,25 @@ async function main(
"type": lib_plankton.args.enum_type.string, "type": lib_plankton.args.enum_type.string,
"kind": lib_plankton.args.enum_kind.volatile, "kind": lib_plankton.args.enum_kind.volatile,
"mode": lib_plankton.args.enum_mode.replace, "mode": lib_plankton.args.enum_mode.replace,
"default": "database-sqlite", "default": "database:sqlite",
"parameters": { "parameters": {
"indicators_long": ["format"], "indicators_long": ["format"],
"indicators_short": ["f"], "indicators_short": ["f"],
}, },
"info": "output format", "info": "output format",
}), }),
"list": new lib_plankton.args.class_argument({
"name": "list",
"type": lib_plankton.args.enum_type.boolean,
"kind": lib_plankton.args.enum_kind.volatile,
"mode": lib_plankton.args.enum_mode.replace,
"default": false,
"parameters": {
"indicators_long": ["list"],
"indicators_short": ["l"],
},
"info": "list available output formats",
}),
"schema": new lib_plankton.args.class_argument({ "schema": new lib_plankton.args.class_argument({
"name": "schema", "name": "schema",
"type": lib_plankton.args.enum_type.boolean, "type": lib_plankton.args.enum_type.boolean,
@ -69,23 +76,60 @@ async function main(
else { else {
if (args["schema"]) { if (args["schema"]) {
process.stdout.write( process.stdout.write(
JSON.stringify(input_schema(), undefined, "\t") JSON.stringify(_sindri.input_schema(), undefined, "\t")
);
}
else {
if (args["list"]) {
process.stdout.write(
_sindri.list_outputs()
.map(
entry => lib_plankton.string.coin(
"{{realm}}:{{implementation}}\n",
{
"realm": entry.realm,
"implementation": entry.implementation,
}
)
)
.join("")
); );
} }
else { else {
const input_content : string = await lib_plankton.file.read_stdin(); const input_content : string = await lib_plankton.file.read_stdin();
const input_data_raw : any = lib_plankton.json.decode(input_content); const input_data_raw : any = lib_plankton.json.decode(input_content);
const input_data : type_input = input_normalize(input_data_raw); const input_data : type_input = _sindri.input_normalize(input_data_raw);
if (! outputs.hasOwnProperty(args["format"])) { const format_parts : Array<string> = args["format"].split(":");
const realm_encoded : string = format_parts[0];
const realm : _sindri.enum_realm = {
"database": _sindri.enum_realm.database,
"backend": _sindri.enum_realm.backend,
"frontend": _sindri.enum_realm.frontend,
"other": _sindri.enum_realm.other,
}[realm_encoded];
const name : string = format_parts.slice(1).join(":");
let output : (null | _sindri.type_output);
try {
output = _sindri.get_output(realm, name);
}
catch (error) {
output = null;
}
if (output === null) {
throw (new Error("unhandled output format: " + args["format"])); throw (new Error("unhandled output format: " + args["format"]));
} }
else { else {
const output_content : string = outputs[args["format"]].render(input_data); const output_content : string = await output.render(input_data);
process.stdout.write(output_content); process.stdout.write(output_content);
} }
} }
} }
}
}
} }
main(process.argv.slice(2));
_sindri.main(process.argv.slice(2));

View file

@ -1437,6 +1437,10 @@ namespace _sindri.outputs.backend.typescript
} }
const output_backend_typescript : type_output = { _sindri.add_output(
"render": _sindri.outputs.backend.typescript.render, _sindri.enum_realm.backend,
}; "typescript",
{
"render": (x) => Promise.resolve<string>(_sindri.outputs.backend.typescript.render(x)),
}
);

View file

@ -212,6 +212,10 @@ namespace _sindri.outputs.database.mysql
/** /**
*/ */
const output_database_mysql : type_output = { _sindri.add_output(
"render": _sindri.outputs.database.mysql.render, _sindri.enum_realm.database,
}; "mysql",
{
"render": (x) => Promise.resolve<string>(_sindri.outputs.backend.typescript.render(x)),
}
);

View file

@ -174,6 +174,10 @@ namespace _sindri.outputs.database.sqlite
} }
const output_database_sqlite : type_output = { _sindri.add_output(
"render": _sindri.outputs.database.sqlite.render, _sindri.enum_realm.database,
}; "sqlite",
{
"render": (x) => Promise.resolve<string>(_sindri.outputs.backend.typescript.render(x)),
}
);

View file

@ -0,0 +1,36 @@
namespace _sindri.outputs.frontend.typescript
{
/**
*/
async function get_template(
name : string
) : Promise<string>
{
return _sindri.get_template(_sindri.enum_realm.frontend, "typescript", name);
}
/**
*/
export async function render(
input_data
) : Promise<string>
{
return lib_plankton.string.coin(
await get_template("core.ts.tpl"),
{
"value": JSON.stringify(input_data, undefined, "\t"),
}
);
}
}
_sindri.add_output(
_sindri.enum_realm.frontend,
"typescript",
{
"render": _sindri.outputs.frontend.typescript.render,
}
);

View file

@ -0,0 +1,251 @@
namespace _sindri
{
/**
*/
const abstract = {{value}};
/**
* @todo headers
* @todo query
*/
export function api_call(
method : string,
path : string,
input : any = null
) : Promise<any>
{
return (
fetch(
(conf.backend.scheme + "://" + conf.backend.host + ":" + conf.backend.port.toFixed() + path),
{
"method": method,
"headers": {
"Content-Type": "application/json",
},
"body": (
(input === null)
? undefined
: /*Buffer.from*/(JSON.stringify(input))
),
}
)
.then(
x => x.json()
)
);
}
/**
*/
export function editor(
domain_description : any,
hook_switch : (null | ((state : lib_plankton.zoo_editor.type_state<int, any>) => void))
) : lib_plankton.zoo_editor.type_editor<int, any>
{
return lib_plankton.zoo_editor.make<int, any>(
// store
{
"setup": () => Promise.resolve<void>(undefined),
"search": (term) => (
api_call(
"GET",
lib_plankton.string.coin(
"/{{name}}",
{
"name": domain_description.name,
}
),
null
)
.then(
(entries) => Promise.resolve<Array<{key : int; preview : any}>>(
entries
.filter(
entry => (
entry.key.toFixed().includes(term.toLowerCase())
||
(
/*
(term.length >= 3)
&&
*/
domain_description.data_fields
.some(
data_field => JSON.stringify(entry.value[data_field.name]).toLowerCase().includes(term.toLowerCase())
)
)
)
)
.map(
entry => ({
"key": entry.key,
"preview": entry.value,
})
)
)
)
),
"read": (id) => api_call(
"GET",
lib_plankton.string.coin(
"/{{name}}/{{id}}",
{
"name": domain_description.name,
"id": id.toFixed(0),
}
),
null
),
"create": (object) => api_call(
"POST",
lib_plankton.string.coin(
"/{{name}}",
{
"name": domain_description.name,
}
),
object
),
"update": (id, object) => api_call(
"PATCH",
lib_plankton.string.coin(
"/{{name}}/{{id}}",
{
"name": domain_description.name,
"id": id.toFixed(0),
}
),
object
),
"delete": (id) => api_call(
"DELETE",
lib_plankton.string.coin(
"/{{name}}/{{id}}",
{
"name": domain_description.name,
"id": id.toFixed(0),
}
),
null
),
},
// form
lib_plankton.zoo_form.make<any>(
// method
"GET",
// fields
(
domain_description.data_fields.map(
data_field => ({
"name": data_field.name,
"type": {
"string_short": "text",
"string_medium": "text",
"string_long": "text",
"integer": "number",
"float": "number",
}[data_field.type],
})
)
),
// encode
(object) => object,
// decode
(object_encoded) => object_encoded,
),
// options
{
"hook_switch": hook_switch,
}
);
}
/**
*/
export function edit_location_name(
domain_description
) : string
{
return lib_plankton.string.coin(
"edit_{{name}}",
{
"name": domain_description.name,
}
);
}
/**
*/
export function register_editor_page_for_domain(
domain_description
) : void
{
const location_name : string = edit_location_name(domain_description);
lib_plankton.zoo_page.register(
location_name,
async (parameters, element_main) => {
const id : (null | int) = ((! ("id" in parameters)) ? null : parseInt(parameters["id"]));
const mode : lib_plankton.zoo_editor.enum_mode = editor_decode_mode(parameters["mode"], id);
const search_term : (null | string) = (parameters["search"] ?? null);
lib_plankton.zoo_editor.render<int, any>(
editor(
domain_description,
(state) => {
lib_plankton.zoo_page.set(
{
"name": location_name,
"parameters": {
"mode": state.mode,
"id": ((state.key === null) ? null : state.key.toFixed(0)),
"search": state.search_state.term,
},
}
);
}
),
element_main,
{
"state": {
"mode": mode,
"key": id,
"search_state": {"term": search_term},
}
}
);
}
);
lib_plankton.zoo_page.add_nav_entry(
{
"name": location_name,
"parameters": {
"mode": "find",
"key": null,
"search": ""
}
},
{
"label": (domain_description.name + "s"),
}
);
}
/**
*/
export function init(
) : Promise<void>
{
abstract.domains.forEach(
domain_description => {
register_editor_page_for_domain(domain_description);
}
);
return Promise.resolve<void>(undefined);
}
}

View file

@ -1,22 +0,0 @@
namespace _sindri.outputs.frontend.typescript
{
/**
*/
export function render(
input_data
) : string
{
return lib_plankton.string.coin(
"const sindri_abstract = {{value}};",
{
"value": JSON.stringify(input_data, undefined, "\t"),
}
);
}
}
const output_frontend_typescript : type_output = {
"render": _sindri.outputs.frontend.typescript.render,
};

View file

@ -1,5 +1,12 @@
const output_jsonschema : type_output = { namespace _sindri.outputs.other.jsonschema
"render": function (input_data) { {
/**
*/
export function render(
input_data
) : string
{
return lib_plankton.json.encode( return lib_plankton.json.encode(
Object.fromEntries( Object.fromEntries(
input_data.domains.map( input_data.domains.map(
@ -78,5 +85,17 @@ const output_jsonschema : type_output = {
), ),
true true
); );
}, }
};
}
/**
*/
_sindri.add_output(
_sindri.enum_realm.other,
"jsonschema",
{
"render": (x) => Promise.resolve<string>(_sindri.outputs.backend.typescript.render(x)),
}
);

View file

@ -1,40 +0,0 @@
/**
*/
type type_input = {
domains : Array<
{
name : string;
description : (null | string);
key_field : (
null
|
{
name : string;
description ?: (null | string);
}
);
data_fields : Array<
{
name : string;
description : (null | string);
type : ("boolean" | "integer" | "float" | "string_short" | "string_medium" | "string_long");
nullable : boolean;
default : (null | boolean | int | float | string);
}
>;
constraints ?: Array<
{
kind : ("unique" | "foreign_key");
parameters : Record<string, any>;
}
>;
}
>;
};
/**
*/
type type_output = {
render : ((input_data : type_input) => string);
};

View file

@ -13,16 +13,16 @@ cmd_copy := cp -r -u -v
## rules ## rules
.PHONY: all .PHONY: all
all: build/sindri all: build/sindri templates
temp/sindri-unlinked.js: \ temp/sindri-unlinked.js: \
lib/plankton/plankton.d.ts \ lib/plankton/plankton.d.ts \
source/types.ts \ source/base.ts \
source/outputs/jsonschema.ts \ source/outputs/other/jsonschema/logic.ts \
source/outputs/database_sqlite.ts \ source/outputs/database/sqlite/logic.ts \
source/outputs/database_mysql.ts \ source/outputs/database/mysql/logic.ts \
source/outputs/backend_typescript.ts \ source/outputs/backend/typescript/logic.ts \
source/outputs/frontend_typescript.ts \ source/outputs/frontend/typescript/logic.ts \
source/conf.ts \ source/conf.ts \
source/main.ts source/main.ts
@ ${cmd_log} "compiling …" @ ${cmd_log} "compiling …"
@ -35,3 +35,9 @@ build/sindri: lib/plankton/plankton.js temp/sindri-unlinked.js
@ ${cmd_echox} "#!/usr/bin/env node" > temp/head.js @ ${cmd_echox} "#!/usr/bin/env node" > temp/head.js
@ ${cmd_concatenate} temp/head.js $^ > $@ @ ${cmd_concatenate} temp/head.js $^ > $@
@ ${cmd_chmod} +x $@ @ ${cmd_chmod} +x $@
.PHONY: templates
templates: \
source/outputs/frontend/typescript/templates/core.ts.tpl
@ ${cmd_log} "placing templates …"
@ tools/place-templates

17
tools/place-templates Executable file
View file

@ -0,0 +1,17 @@
#!/usr/bin/env bash
## consts
dir_from=source/outputs
dir_to=build/templates
## exec
paths=$(find ${dir_from} -type d -name templates)
for path in ${paths}
do
type=$(echo ${path} | cut --delimiter='/' --fields='3-' | cut --delimiter='/' --fields='-2')
mkdir --parents ${dir_to}/${type}
cp --update --verbose ${path}/* ${dir_to}/${type}/
done