From 19d4240ee5da39063e1cf7c60bb776e1e72c5685 Mon Sep 17 00:00:00 2001 From: Ewen Le Bihan Date: Sun, 14 Apr 2024 18:25:04 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=96=20Release=20v1.2.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 5 + meta.go | 2 +- packages/python/ortfodb/exporter.py | 149 +++++++++++++++++ packages/python/pyproject.toml | 2 +- packages/rust/Cargo.toml | 2 +- packages/rust/src/exporter.rs | 54 +++++++ packages/typescript/package.json | 2 +- packages/typescript/src/exporter.ts | 238 ++++++++++++++++++++++++++++ schemas/configuration.schema.json | 2 +- schemas/database.schema.json | 2 +- schemas/exporter.schema.json | 2 +- schemas/tags.schema.json | 2 +- schemas/technologies.schema.json | 2 +- 13 files changed, 455 insertions(+), 9 deletions(-) create mode 100644 packages/python/ortfodb/exporter.py create mode 100644 packages/rust/src/exporter.rs create mode 100644 packages/typescript/src/exporter.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index f45fcab..7aa9136 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.2.0] - 2024-04-14 + ### Added - (S)FTP exporter @@ -67,6 +69,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Initial release [Unreleased]: https://github.com/ortfo/db/compare/v1.1.0...HEAD +[1.2.0]: https://github.com/ortfo/db/-/releases/tag/v1.2.0 [1.1.0]: https://github.com/ortfo/db/compare/v1.0.0...v1.1.0 [1.0.0]: https://github.com/ortfo/db/compare/v0.3.2...v1.0.0 [0.3.2]: https://github.com/ortfo/db/compare/v0.3.1...v0.3.2 @@ -75,3 +78,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [0.2.0]: https://github.com/ortfo/db/releases/tag/v0.2.0 [//]: # (C3-2-DKAC:GGH:Rortfo/db:Tv{t}) + +[unreleased]: https://github.com/ortfo/db/-/compare/v1.2.0...main diff --git a/meta.go b/meta.go index 83d1a79..91fa142 100644 --- a/meta.go +++ b/meta.go @@ -1,3 +1,3 @@ package ortfodb -const Version = "1.1.0" +const Version = "1.2.0" diff --git a/packages/python/ortfodb/exporter.py b/packages/python/ortfodb/exporter.py new file mode 100644 index 0000000..bbed1c1 --- /dev/null +++ b/packages/python/ortfodb/exporter.py @@ -0,0 +1,149 @@ +from typing import List, Optional, Any, Dict, TypeVar, Callable, Type, cast + + +T = TypeVar("T") + + +def from_list(f: Callable[[Any], T], x: Any) -> List[T]: + assert isinstance(x, list) + return [f(y) for y in x] + + +def from_str(x: Any) -> str: + assert isinstance(x, str) + return x + + +def from_none(x: Any) -> Any: + assert x is None + return x + + +def from_union(fs, x): + for f in fs: + try: + return f(x) + except: + pass + assert False + + +def from_dict(f: Callable[[Any], T], x: Any) -> Dict[str, T]: + assert isinstance(x, dict) + return { k: f(v) for (k, v) in x.items() } + + +def from_bool(x: Any) -> bool: + assert isinstance(x, bool) + return x + + +def to_class(c: Type[T], x: Any) -> dict: + assert isinstance(x, c) + return cast(Any, x).to_dict() + + +class ExporterSchema: + log: Optional[List[str]] + """Log a message. The first argument is the verb, the second is the color, the third is the + message. + """ + run: Optional[str] + """Run a command in a shell""" + + def __init__(self, log: Optional[List[str]], run: Optional[str]) -> None: + self.log = log + self.run = run + + @staticmethod + def from_dict(obj: Any) -> 'ExporterSchema': + assert isinstance(obj, dict) + log = from_union([lambda x: from_list(from_str, x), from_none], obj.get("log")) + run = from_union([from_str, from_none], obj.get("run")) + return ExporterSchema(log, run) + + def to_dict(self) -> dict: + result: dict = {} + if self.log is not None: + result["log"] = from_union([lambda x: from_list(from_str, x), from_none], self.log) + if self.run is not None: + result["run"] = from_union([from_str, from_none], self.run) + return result + + +class Exporter: + after: Optional[List[ExporterSchema]] + """Commands to run after the build finishes. Go text template that receives .Data and + .Database, the built database. + """ + before: Optional[List[ExporterSchema]] + """Commands to run before the build starts. Go text template that receives .Data""" + + data: Optional[Dict[str, Any]] + """Initial data""" + + description: str + """Some documentation about the exporter""" + + name: str + """The name of the exporter""" + + requires: Optional[List[str]] + """List of programs that are required to be available in the PATH for the exporter to run.""" + + verbose: Optional[bool] + """If true, will show every command that is run""" + + work: Optional[List[ExporterSchema]] + """Commands to run during the build, for each work. Go text template that receives .Data and + .Work, the current work. + """ + + def __init__(self, after: Optional[List[ExporterSchema]], before: Optional[List[ExporterSchema]], data: Optional[Dict[str, Any]], description: str, name: str, requires: Optional[List[str]], verbose: Optional[bool], work: Optional[List[ExporterSchema]]) -> None: + self.after = after + self.before = before + self.data = data + self.description = description + self.name = name + self.requires = requires + self.verbose = verbose + self.work = work + + @staticmethod + def from_dict(obj: Any) -> 'Exporter': + assert isinstance(obj, dict) + after = from_union([lambda x: from_list(ExporterSchema.from_dict, x), from_none], obj.get("after")) + before = from_union([lambda x: from_list(ExporterSchema.from_dict, x), from_none], obj.get("before")) + data = from_union([lambda x: from_dict(lambda x: x, x), from_none], obj.get("data")) + description = from_str(obj.get("description")) + name = from_str(obj.get("name")) + requires = from_union([lambda x: from_list(from_str, x), from_none], obj.get("requires")) + verbose = from_union([from_bool, from_none], obj.get("verbose")) + work = from_union([lambda x: from_list(ExporterSchema.from_dict, x), from_none], obj.get("work")) + return Exporter(after, before, data, description, name, requires, verbose, work) + + def to_dict(self) -> dict: + result: dict = {} + if self.after is not None: + result["after"] = from_union([lambda x: from_list(lambda x: to_class(ExporterSchema, x), x), from_none], self.after) + if self.before is not None: + result["before"] = from_union([lambda x: from_list(lambda x: to_class(ExporterSchema, x), x), from_none], self.before) + if self.data is not None: + result["data"] = from_union([lambda x: from_dict(lambda x: x, x), from_none], self.data) + result["description"] = from_str(self.description) + result["name"] = from_str(self.name) + if self.requires is not None: + result["requires"] = from_union([lambda x: from_list(from_str, x), from_none], self.requires) + if self.verbose is not None: + result["verbose"] = from_union([from_bool, from_none], self.verbose) + if self.work is not None: + result["work"] = from_union([lambda x: from_list(lambda x: to_class(ExporterSchema, x), x), from_none], self.work) + return result + + +def exporter_from_dict(s: Any) -> Exporter: + return Exporter.from_dict(s) + + +def exporter_to_dict(x: Exporter) -> Any: + return to_class(Exporter, x) diff --git a/packages/python/pyproject.toml b/packages/python/pyproject.toml index 26ec594..5f616a1 100644 --- a/packages/python/pyproject.toml +++ b/packages/python/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "ortfodb" -version = "1.1.0" +version = "1.2.0" description = "ortfodb client library" authors = ["Ewen Le Bihan "] license = "MIT" diff --git a/packages/rust/Cargo.toml b/packages/rust/Cargo.toml index b63f661..109853c 100644 --- a/packages/rust/Cargo.toml +++ b/packages/rust/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ortfodb" -version = "1.1.0" +version = "1.2.0" edition = "2021" description = "An ortfodb (https://github.com/ortfo/db) client library for Rust." license = "MIT" diff --git a/packages/rust/src/exporter.rs b/packages/rust/src/exporter.rs new file mode 100644 index 0000000..2212131 --- /dev/null +++ b/packages/rust/src/exporter.rs @@ -0,0 +1,54 @@ +// Example code that deserializes and serializes the model. +// extern crate serde; +// #[macro_use] +// extern crate serde_derive; +// extern crate serde_json; +// +// use ortfodb::exporter; +// +// fn main() { +// let json = r#"{"answer": 42}"#; +// let model: exporter = serde_json::from_str(&json).unwrap(); +// } + +use serde::{Serialize, Deserialize}; +use std::collections::HashMap; + +#[derive(Serialize, Deserialize)] +pub struct Exporter { + /// Commands to run after the build finishes. Go text template that receives .Data and + /// .Database, the built database. + pub after: Option>, + + /// Commands to run before the build starts. Go text template that receives .Data + pub before: Option>, + + /// Initial data + pub data: Option>>, + + /// Some documentation about the exporter + pub description: String, + + /// The name of the exporter + pub name: String, + + /// List of programs that are required to be available in the PATH for the exporter to run. + pub requires: Option>, + + /// If true, will show every command that is run + pub verbose: Option, + + /// Commands to run during the build, for each work. Go text template that receives .Data and + /// .Work, the current work. + pub work: Option>, +} + +#[derive(Serialize, Deserialize)] +pub struct ExporterSchema { + /// Log a message. The first argument is the verb, the second is the color, the third is the + /// message. + pub log: Option>, + + /// Run a command in a shell + pub run: Option, +} diff --git a/packages/typescript/package.json b/packages/typescript/package.json index 52722a3..1a6c9d7 100644 --- a/packages/typescript/package.json +++ b/packages/typescript/package.json @@ -1,6 +1,6 @@ { "name": "@ortfo/db", - "version": "1.1.0", + "version": "1.2.0", "description": "ortfodb client library", "scripts": { "build": "tsc -p tsconfig.json --declaration --outDir dist" diff --git a/packages/typescript/src/exporter.ts b/packages/typescript/src/exporter.ts new file mode 100644 index 0000000..685d201 --- /dev/null +++ b/packages/typescript/src/exporter.ts @@ -0,0 +1,238 @@ +// To parse this data: +// +// import { Convert, Exporter } from "./file"; +// +// const exporter = Convert.toExporter(json); +// +// These functions will throw an error if the JSON doesn't +// match the expected interface, even if the JSON is valid. + +export interface Exporter { + /** + * Commands to run after the build finishes. Go text template that receives .Data and + * .Database, the built database. + */ + after?: ExporterSchema[]; + /** + * Commands to run before the build starts. Go text template that receives .Data + */ + before?: ExporterSchema[]; + /** + * Initial data + */ + data?: { [key: string]: any }; + /** + * Some documentation about the exporter + */ + description: string; + /** + * The name of the exporter + */ + name: string; + /** + * List of programs that are required to be available in the PATH for the exporter to run. + */ + requires?: string[]; + /** + * If true, will show every command that is run + */ + verbose?: boolean; + /** + * Commands to run during the build, for each work. Go text template that receives .Data and + * .Work, the current work. + */ + work?: ExporterSchema[]; +} + +export interface ExporterSchema { + /** + * Log a message. The first argument is the verb, the second is the color, the third is the + * message. + */ + log?: string[]; + /** + * Run a command in a shell + */ + run?: string; +} + +// Converts JSON strings to/from your types +// and asserts the results of JSON.parse at runtime +export class Convert { + public static toExporter(json: string): Exporter { + return cast(JSON.parse(json), r("Exporter")); + } + + public static exporterToJson(value: Exporter): string { + return JSON.stringify(uncast(value, r("Exporter")), null, 2); + } +} + +function invalidValue(typ: any, val: any, key: any, parent: any = ''): never { + const prettyTyp = prettyTypeName(typ); + const parentText = parent ? ` on ${parent}` : ''; + const keyText = key ? ` for key "${key}"` : ''; + throw Error(`Invalid value${keyText}${parentText}. Expected ${prettyTyp} but got ${JSON.stringify(val)}`); +} + +function prettyTypeName(typ: any): string { + if (Array.isArray(typ)) { + if (typ.length === 2 && typ[0] === undefined) { + return `an optional ${prettyTypeName(typ[1])}`; + } else { + return `one of [${typ.map(a => { return prettyTypeName(a); }).join(", ")}]`; + } + } else if (typeof typ === "object" && typ.literal !== undefined) { + return typ.literal; + } else { + return typeof typ; + } +} + +function jsonToJSProps(typ: any): any { + if (typ.jsonToJS === undefined) { + const map: any = {}; + typ.props.forEach((p: any) => map[p.json] = { key: p.js, typ: p.typ }); + typ.jsonToJS = map; + } + return typ.jsonToJS; +} + +function jsToJSONProps(typ: any): any { + if (typ.jsToJSON === undefined) { + const map: any = {}; + typ.props.forEach((p: any) => map[p.js] = { key: p.json, typ: p.typ }); + typ.jsToJSON = map; + } + return typ.jsToJSON; +} + +function transform(val: any, typ: any, getProps: any, key: any = '', parent: any = ''): any { + function transformPrimitive(typ: string, val: any): any { + if (typeof typ === typeof val) return val; + return invalidValue(typ, val, key, parent); + } + + function transformUnion(typs: any[], val: any): any { + // val must validate against one typ in typs + const l = typs.length; + for (let i = 0; i < l; i++) { + const typ = typs[i]; + try { + return transform(val, typ, getProps); + } catch (_) {} + } + return invalidValue(typs, val, key, parent); + } + + function transformEnum(cases: string[], val: any): any { + if (cases.indexOf(val) !== -1) return val; + return invalidValue(cases.map(a => { return l(a); }), val, key, parent); + } + + function transformArray(typ: any, val: any): any { + // val must be an array with no invalid elements + if (!Array.isArray(val)) return invalidValue(l("array"), val, key, parent); + return val.map(el => transform(el, typ, getProps)); + } + + function transformDate(val: any): any { + if (val === null) { + return null; + } + const d = new Date(val); + if (isNaN(d.valueOf())) { + return invalidValue(l("Date"), val, key, parent); + } + return d; + } + + function transformObject(props: { [k: string]: any }, additional: any, val: any): any { + if (val === null || typeof val !== "object" || Array.isArray(val)) { + return invalidValue(l(ref || "object"), val, key, parent); + } + const result: any = {}; + Object.getOwnPropertyNames(props).forEach(key => { + const prop = props[key]; + const v = Object.prototype.hasOwnProperty.call(val, key) ? val[key] : undefined; + result[prop.key] = transform(v, prop.typ, getProps, key, ref); + }); + Object.getOwnPropertyNames(val).forEach(key => { + if (!Object.prototype.hasOwnProperty.call(props, key)) { + result[key] = transform(val[key], additional, getProps, key, ref); + } + }); + return result; + } + + if (typ === "any") return val; + if (typ === null) { + if (val === null) return val; + return invalidValue(typ, val, key, parent); + } + if (typ === false) return invalidValue(typ, val, key, parent); + let ref: any = undefined; + while (typeof typ === "object" && typ.ref !== undefined) { + ref = typ.ref; + typ = typeMap[typ.ref]; + } + if (Array.isArray(typ)) return transformEnum(typ, val); + if (typeof typ === "object") { + return typ.hasOwnProperty("unionMembers") ? transformUnion(typ.unionMembers, val) + : typ.hasOwnProperty("arrayItems") ? transformArray(typ.arrayItems, val) + : typ.hasOwnProperty("props") ? transformObject(getProps(typ), typ.additional, val) + : invalidValue(typ, val, key, parent); + } + // Numbers can be parsed by Date but shouldn't be. + if (typ === Date && typeof val !== "number") return transformDate(val); + return transformPrimitive(typ, val); +} + +function cast(val: any, typ: any): T { + return transform(val, typ, jsonToJSProps); +} + +function uncast(val: T, typ: any): any { + return transform(val, typ, jsToJSONProps); +} + +function l(typ: any) { + return { literal: typ }; +} + +function a(typ: any) { + return { arrayItems: typ }; +} + +function u(...typs: any[]) { + return { unionMembers: typs }; +} + +function o(props: any[], additional: any) { + return { props, additional }; +} + +function m(additional: any) { + return { props: [], additional }; +} + +function r(name: string) { + return { ref: name }; +} + +const typeMap: any = { + "Exporter": o([ + { json: "after", js: "after", typ: u(undefined, a(r("ExporterSchema"))) }, + { json: "before", js: "before", typ: u(undefined, a(r("ExporterSchema"))) }, + { json: "data", js: "data", typ: u(undefined, m("any")) }, + { json: "description", js: "description", typ: "" }, + { json: "name", js: "name", typ: "" }, + { json: "requires", js: "requires", typ: u(undefined, a("")) }, + { json: "verbose", js: "verbose", typ: u(undefined, true) }, + { json: "work", js: "work", typ: u(undefined, a(r("ExporterSchema"))) }, + ], false), + "ExporterSchema": o([ + { json: "log", js: "log", typ: u(undefined, a("")) }, + { json: "run", js: "run", typ: u(undefined, "") }, + ], false), +}; diff --git a/schemas/configuration.schema.json b/schemas/configuration.schema.json index 819c5fc..48959e6 100644 --- a/schemas/configuration.schema.json +++ b/schemas/configuration.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://raw.githubusercontent.com/ortfo/db/v1.1.0/schemas/configuration.schema.json", + "$id": "https://raw.githubusercontent.com/ortfo/db/v1.2.0/schemas/configuration.schema.json", "$ref": "#/$defs/Configuration", "$defs": { "Configuration": { diff --git a/schemas/database.schema.json b/schemas/database.schema.json index cf9eb20..2ffd1c4 100644 --- a/schemas/database.schema.json +++ b/schemas/database.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://raw.githubusercontent.com/ortfo/db/v1.1.0/schemas/database.schema.json", + "$id": "https://raw.githubusercontent.com/ortfo/db/v1.2.0/schemas/database.schema.json", "$ref": "#/$defs/Database", "$defs": { "AnalyzedWork": { diff --git a/schemas/exporter.schema.json b/schemas/exporter.schema.json index dbcf403..8f251a9 100644 --- a/schemas/exporter.schema.json +++ b/schemas/exporter.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://raw.githubusercontent.com/ortfo/db/v1.1.0/schemas/exporter.schema.json", + "$id": "https://raw.githubusercontent.com/ortfo/db/v1.2.0/schemas/exporter.schema.json", "$ref": "#/$defs/ExporterManifest", "$defs": { "ExporterCommand": { diff --git a/schemas/tags.schema.json b/schemas/tags.schema.json index 449bf7b..e161929 100644 --- a/schemas/tags.schema.json +++ b/schemas/tags.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://raw.githubusercontent.com/ortfo/db/v1.1.0/schemas/tags.schema.json", + "$id": "https://raw.githubusercontent.com/ortfo/db/v1.2.0/schemas/tags.schema.json", "$ref": "#/$defs/tags", "$defs": { "Tag": { diff --git a/schemas/technologies.schema.json b/schemas/technologies.schema.json index ba6195a..7f877f1 100644 --- a/schemas/technologies.schema.json +++ b/schemas/technologies.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://raw.githubusercontent.com/ortfo/db/v1.1.0/schemas/technologies.schema.json", + "$id": "https://raw.githubusercontent.com/ortfo/db/v1.2.0/schemas/technologies.schema.json", "$ref": "#/$defs/technologies", "$defs": { "Technology": {