@pubid/pubid 0.1.3 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/artifact/corpus.json.gz +0 -0
- package/dist/conformance/cli.js +5 -1
- package/dist/flavors/amca/grammar.d.ts +4 -0
- package/dist/flavors/amca/grammar.js +87 -0
- package/dist/flavors/amca/index.d.ts +4 -0
- package/dist/flavors/amca/index.js +76 -0
- package/dist/flavors/amca/model.d.ts +29 -0
- package/dist/flavors/amca/model.js +146 -0
- package/dist/flavors/ansi/index.d.ts +24 -0
- package/dist/flavors/ansi/index.js +147 -0
- package/dist/flavors/api/grammar.d.ts +4 -0
- package/dist/flavors/api/grammar.js +79 -0
- package/dist/flavors/api/index.d.ts +4 -0
- package/dist/flavors/api/index.js +71 -0
- package/dist/flavors/api/model.d.ts +27 -0
- package/dist/flavors/api/model.js +114 -0
- package/dist/flavors/ashrae/grammar.d.ts +2 -0
- package/dist/flavors/ashrae/grammar.js +667 -0
- package/dist/flavors/ashrae/index.d.ts +4 -0
- package/dist/flavors/ashrae/index.js +306 -0
- package/dist/flavors/ashrae/model.d.ts +87 -0
- package/dist/flavors/ashrae/model.js +335 -0
- package/dist/flavors/ashrae/preprocessor.d.ts +7 -0
- package/dist/flavors/ashrae/preprocessor.js +44 -0
- package/dist/flavors/asme/grammar.d.ts +2 -0
- package/dist/flavors/asme/grammar.js +200 -0
- package/dist/flavors/asme/index.d.ts +4 -0
- package/dist/flavors/asme/index.js +138 -0
- package/dist/flavors/asme/model.d.ts +34 -0
- package/dist/flavors/asme/model.js +123 -0
- package/dist/flavors/astm/grammar.d.ts +2 -0
- package/dist/flavors/astm/grammar.js +110 -0
- package/dist/flavors/astm/index.d.ts +2 -0
- package/dist/flavors/astm/index.js +127 -0
- package/dist/flavors/astm/model.d.ts +42 -0
- package/dist/flavors/astm/model.js +263 -0
- package/dist/flavors/bipm/index.d.ts +30 -0
- package/dist/flavors/bipm/index.js +434 -0
- package/dist/flavors/bsi/grammar.d.ts +4 -0
- package/dist/flavors/bsi/grammar.js +535 -0
- package/dist/flavors/bsi/index.d.ts +4 -0
- package/dist/flavors/bsi/index.js +941 -0
- package/dist/flavors/bsi/model.d.ts +127 -0
- package/dist/flavors/bsi/model.js +863 -0
- package/dist/flavors/calconnect/index.d.ts +21 -0
- package/dist/flavors/calconnect/index.js +113 -0
- package/dist/flavors/cen_cenelec/grammar.d.ts +4 -0
- package/dist/flavors/cen_cenelec/grammar.js +107 -0
- package/dist/flavors/cen_cenelec/index.d.ts +5 -0
- package/dist/flavors/cen_cenelec/index.js +263 -0
- package/dist/flavors/cen_cenelec/model.d.ts +45 -0
- package/dist/flavors/cen_cenelec/model.js +490 -0
- package/dist/flavors/cie/grammar.d.ts +5 -0
- package/dist/flavors/cie/grammar.js +127 -0
- package/dist/flavors/cie/index.d.ts +2 -0
- package/dist/flavors/cie/index.js +311 -0
- package/dist/flavors/cie/model.d.ts +122 -0
- package/dist/flavors/cie/model.js +464 -0
- package/dist/flavors/csa/grammar.d.ts +6 -0
- package/dist/flavors/csa/grammar.js +239 -0
- package/dist/flavors/csa/index.d.ts +2 -0
- package/dist/flavors/csa/index.js +634 -0
- package/dist/flavors/csa/model.d.ts +33 -0
- package/dist/flavors/csa/model.js +505 -0
- package/dist/flavors/doi/index.d.ts +13 -0
- package/dist/flavors/doi/index.js +65 -0
- package/dist/flavors/easc/index.d.ts +4 -0
- package/dist/flavors/easc/index.js +107 -0
- package/dist/flavors/ecma/index.d.ts +4 -0
- package/dist/flavors/ecma/index.js +117 -0
- package/dist/flavors/etsi/index.d.ts +14 -0
- package/dist/flavors/etsi/index.js +307 -0
- package/dist/flavors/evs/grammar.d.ts +2 -0
- package/dist/flavors/evs/grammar.js +22 -0
- package/dist/flavors/evs/index.d.ts +2 -0
- package/dist/flavors/evs/index.js +24 -0
- package/dist/flavors/evs/model.d.ts +19 -0
- package/dist/flavors/evs/model.js +36 -0
- package/dist/flavors/gb/index.d.ts +31 -0
- package/dist/flavors/gb/index.js +159 -0
- package/dist/flavors/gost/grammar.d.ts +2 -0
- package/dist/flavors/gost/grammar.js +46 -0
- package/dist/flavors/gost/index.d.ts +2 -0
- package/dist/flavors/gost/index.js +165 -0
- package/dist/flavors/gost/model.d.ts +61 -0
- package/dist/flavors/gost/model.js +147 -0
- package/dist/flavors/gost/prefix_routes.d.ts +15 -0
- package/dist/flavors/gost/prefix_routes.js +192 -0
- package/dist/flavors/iala/index.d.ts +32 -0
- package/dist/flavors/iala/index.js +300 -0
- package/dist/flavors/iana/index.d.ts +19 -0
- package/dist/flavors/iana/index.js +71 -0
- package/dist/flavors/idf/grammar.d.ts +2 -0
- package/dist/flavors/idf/grammar.js +71 -0
- package/dist/flavors/idf/index.d.ts +2 -0
- package/dist/flavors/idf/index.js +100 -0
- package/dist/flavors/idf/model.d.ts +54 -0
- package/dist/flavors/idf/model.js +359 -0
- package/dist/flavors/iec/grammar.d.ts +3 -0
- package/dist/flavors/iec/grammar.js +187 -0
- package/dist/flavors/iec/index.d.ts +4 -0
- package/dist/flavors/iec/index.js +507 -0
- package/dist/flavors/iec/model.d.ts +54 -0
- package/dist/flavors/iec/model.js +437 -0
- package/dist/flavors/ieee/grammar.d.ts +2 -0
- package/dist/flavors/ieee/grammar.js +697 -0
- package/dist/flavors/ieee/index.d.ts +3 -0
- package/dist/flavors/ieee/index.js +1258 -0
- package/dist/flavors/ieee/model.d.ts +144 -0
- package/dist/flavors/ieee/model.js +1237 -0
- package/dist/flavors/ieee/preprocessor.d.ts +8 -0
- package/dist/flavors/ieee/preprocessor.js +410 -0
- package/dist/flavors/ieee/update_codes.d.ts +8 -0
- package/dist/flavors/ieee/update_codes.js +197 -0
- package/dist/flavors/ietf/index.d.ts +30 -0
- package/dist/flavors/ietf/index.js +151 -0
- package/dist/flavors/iho/index.d.ts +4 -0
- package/dist/flavors/iho/index.js +126 -0
- package/dist/flavors/index.d.ts +9 -0
- package/dist/flavors/index.js +136 -0
- package/dist/flavors/isbn/index.d.ts +13 -0
- package/dist/flavors/isbn/index.js +97 -0
- package/dist/flavors/iso/grammar.d.ts +2 -0
- package/dist/flavors/iso/grammar.js +207 -0
- package/dist/flavors/iso/index.d.ts +2 -0
- package/dist/flavors/iso/index.js +343 -0
- package/dist/flavors/iso/model.d.ts +159 -0
- package/dist/flavors/iso/model.js +802 -0
- package/dist/flavors/iso/stages.generated.d.ts +13 -0
- package/dist/flavors/iso/stages.generated.js +391 -0
- package/dist/flavors/itu/grammar.d.ts +4 -0
- package/dist/flavors/itu/grammar.js +205 -0
- package/dist/flavors/itu/index.d.ts +2 -0
- package/dist/flavors/itu/index.js +224 -0
- package/dist/flavors/itu/model.d.ts +184 -0
- package/dist/flavors/itu/model.js +599 -0
- package/dist/flavors/jcgm/index.d.ts +7 -0
- package/dist/flavors/jcgm/index.js +300 -0
- package/dist/flavors/jis/index.d.ts +4 -0
- package/dist/flavors/jis/index.js +267 -0
- package/dist/flavors/nist/grammar.d.ts +2 -0
- package/dist/flavors/nist/grammar.js +287 -0
- package/dist/flavors/nist/index.d.ts +2 -0
- package/dist/flavors/nist/index.js +1244 -0
- package/dist/flavors/nist/model.d.ts +155 -0
- package/dist/flavors/nist/model.js +823 -0
- package/dist/flavors/nist/preprocessor.d.ts +11 -0
- package/dist/flavors/nist/preprocessor.js +281 -0
- package/dist/flavors/oasis/index.d.ts +23 -0
- package/dist/flavors/oasis/index.js +110 -0
- package/dist/flavors/ogc/index.d.ts +20 -0
- package/dist/flavors/ogc/index.js +75 -0
- package/dist/flavors/oiml/grammar.d.ts +2 -0
- package/dist/flavors/oiml/grammar.js +206 -0
- package/dist/flavors/oiml/implementation.d.ts +9 -0
- package/dist/flavors/oiml/implementation.js +45 -0
- package/dist/flavors/oiml/model.d.ts +23 -0
- package/dist/flavors/oiml/model.js +592 -0
- package/dist/flavors/omg/index.d.ts +14 -0
- package/dist/flavors/omg/index.js +73 -0
- package/dist/flavors/plateau/index.d.ts +4 -0
- package/dist/flavors/plateau/index.js +97 -0
- package/dist/flavors/tgpp/index.d.ts +4 -0
- package/dist/flavors/tgpp/index.js +122 -0
- package/dist/flavors/un/index.d.ts +13 -0
- package/dist/flavors/un/index.js +90 -0
- package/dist/flavors/w3c/index.d.ts +14 -0
- package/dist/flavors/w3c/index.js +148 -0
- package/dist/flavors/xsf/index.d.ts +18 -0
- package/dist/flavors/xsf/index.js +65 -0
- package/dist/grammar/engine.d.ts +78 -0
- package/dist/grammar/engine.js +351 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3 -0
- package/dist/model/attribute.d.ts +54 -0
- package/dist/model/attribute.js +53 -0
- package/dist/model/builder.d.ts +39 -0
- package/dist/model/builder.js +124 -0
- package/dist/model/component.d.ts +70 -0
- package/dist/model/component.js +170 -0
- package/dist/model/identifier.d.ts +84 -0
- package/dist/model/identifier.js +270 -0
- package/dist/model/urn-generator.d.ts +27 -0
- package/dist/model/urn-generator.js +91 -0
- package/package.json +1 -1
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The unified model's Components — the TS mirror of lib/pubid/components/.
|
|
3
|
+
* Semantics pinned in TODO.unified/01-study-pubid-core.md §6-7.
|
|
4
|
+
*
|
|
5
|
+
* Every component carries: plain fields, `render(context)` (the
|
|
6
|
+
* RenderingContext seam: "human" default, "urn" lowercases / year-only),
|
|
7
|
+
* `toWire()` (its serialized hash form), and degenerate-scalar support
|
|
8
|
+
* (a component holding only its single significant field collapses to
|
|
9
|
+
* that scalar in the canonical hash).
|
|
10
|
+
*/
|
|
11
|
+
export type RenderContext = "human" | "urn";
|
|
12
|
+
/** Render a value that may be a component or a bare scalar (Renderers::Base#render_component). */
|
|
13
|
+
export declare function renderComponent(value: unknown, context?: RenderContext): string | undefined;
|
|
14
|
+
export declare abstract class Component {
|
|
15
|
+
/** The serialized hash form (lutaml to_hash of the component). */
|
|
16
|
+
abstract toWire(): Record<string, unknown>;
|
|
17
|
+
/** The render seam (context: human default, urn lowercases etc.). */
|
|
18
|
+
abstract render(context?: RenderContext): string | undefined;
|
|
19
|
+
/**
|
|
20
|
+
* The scalar this component degenerates to when `field` is its only
|
|
21
|
+
* significant value, else undefined (Pubid::Identifier#degenerate_scalar).
|
|
22
|
+
* A field holding another component never degenerates.
|
|
23
|
+
*/
|
|
24
|
+
degenerateScalar(field: string): string | undefined;
|
|
25
|
+
/** True when `value` equals the field's declared default. */
|
|
26
|
+
protected fieldIsDefaulted(_name: string, _value: unknown): boolean;
|
|
27
|
+
}
|
|
28
|
+
/** Publication date (Components::Date). Human "YYYY[-MM[-DD]]", URN year only, "--" undated. */
|
|
29
|
+
export declare class PubidDate extends Component {
|
|
30
|
+
readonly year?: string | undefined;
|
|
31
|
+
readonly month?: string | undefined;
|
|
32
|
+
readonly day?: string | undefined;
|
|
33
|
+
readonly undated: boolean;
|
|
34
|
+
constructor(attrs: Record<string, unknown>);
|
|
35
|
+
present(): boolean;
|
|
36
|
+
render(context?: RenderContext): string | undefined;
|
|
37
|
+
toWire(): Record<string, unknown>;
|
|
38
|
+
protected fieldIsDefaulted(name: string, value: unknown): boolean;
|
|
39
|
+
}
|
|
40
|
+
/** Publisher (Components::Publisher): body; URN render lowercases. */
|
|
41
|
+
export declare class Publisher extends Component {
|
|
42
|
+
readonly body: string;
|
|
43
|
+
constructor(attrs: Record<string, unknown>);
|
|
44
|
+
render(context?: RenderContext): string;
|
|
45
|
+
toWire(): Record<string, unknown>;
|
|
46
|
+
}
|
|
47
|
+
/** Language (Components::Language): single-char CHAR_MAP codes; URN lowercases. */
|
|
48
|
+
export declare class Language extends Component {
|
|
49
|
+
static readonly CHAR_MAP: Record<string, string>;
|
|
50
|
+
readonly code: string;
|
|
51
|
+
readonly originalCode?: string | undefined;
|
|
52
|
+
constructor(attrs: Record<string, unknown>);
|
|
53
|
+
render(context?: RenderContext): string;
|
|
54
|
+
toWire(): Record<string, unknown>;
|
|
55
|
+
}
|
|
56
|
+
/** Edition (Components::Edition): degenerates to its number. */
|
|
57
|
+
export declare class Edition extends Component {
|
|
58
|
+
readonly number: string;
|
|
59
|
+
readonly phase?: string | undefined;
|
|
60
|
+
constructor(attrs: Record<string, unknown>);
|
|
61
|
+
render(context?: RenderContext): string | undefined;
|
|
62
|
+
toWire(): Record<string, unknown>;
|
|
63
|
+
}
|
|
64
|
+
/** Iteration (Components::Iteration): degenerate by construction. */
|
|
65
|
+
export declare class Iteration extends Component {
|
|
66
|
+
readonly string: string;
|
|
67
|
+
constructor(attrs: Record<string, unknown>);
|
|
68
|
+
render(_context?: RenderContext): string | undefined;
|
|
69
|
+
toWire(): Record<string, unknown>;
|
|
70
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The unified model's Components — the TS mirror of lib/pubid/components/.
|
|
3
|
+
* Semantics pinned in TODO.unified/01-study-pubid-core.md §6-7.
|
|
4
|
+
*
|
|
5
|
+
* Every component carries: plain fields, `render(context)` (the
|
|
6
|
+
* RenderingContext seam: "human" default, "urn" lowercases / year-only),
|
|
7
|
+
* `toWire()` (its serialized hash form), and degenerate-scalar support
|
|
8
|
+
* (a component holding only its single significant field collapses to
|
|
9
|
+
* that scalar in the canonical hash).
|
|
10
|
+
*/
|
|
11
|
+
/** Render a value that may be a component or a bare scalar (Renderers::Base#render_component). */
|
|
12
|
+
export function renderComponent(value, context) {
|
|
13
|
+
if (value === undefined || value === null)
|
|
14
|
+
return undefined;
|
|
15
|
+
if (value instanceof Component)
|
|
16
|
+
return value.render(context);
|
|
17
|
+
return String(value);
|
|
18
|
+
}
|
|
19
|
+
export class Component {
|
|
20
|
+
/**
|
|
21
|
+
* The scalar this component degenerates to when `field` is its only
|
|
22
|
+
* significant value, else undefined (Pubid::Identifier#degenerate_scalar).
|
|
23
|
+
* A field holding another component never degenerates.
|
|
24
|
+
*/
|
|
25
|
+
degenerateScalar(field) {
|
|
26
|
+
let found;
|
|
27
|
+
for (const [name, value] of Object.entries(this)) {
|
|
28
|
+
if (value === undefined || value === null || value === "")
|
|
29
|
+
continue;
|
|
30
|
+
if (this.fieldIsDefaulted(name, value))
|
|
31
|
+
continue;
|
|
32
|
+
if (name === field) {
|
|
33
|
+
if (value instanceof Component)
|
|
34
|
+
return undefined;
|
|
35
|
+
found = String(value);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return found;
|
|
42
|
+
}
|
|
43
|
+
/** True when `value` equals the field's declared default. */
|
|
44
|
+
fieldIsDefaulted(_name, _value) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const pad2 = (value) => value.padStart(2, "0");
|
|
49
|
+
/** Publication date (Components::Date). Human "YYYY[-MM[-DD]]", URN year only, "--" undated. */
|
|
50
|
+
export class PubidDate extends Component {
|
|
51
|
+
year;
|
|
52
|
+
month;
|
|
53
|
+
day;
|
|
54
|
+
undated;
|
|
55
|
+
constructor(attrs) {
|
|
56
|
+
super();
|
|
57
|
+
this.year = attrs["year"];
|
|
58
|
+
this.month = attrs["month"];
|
|
59
|
+
this.day = attrs["day"];
|
|
60
|
+
this.undated = attrs["undated"] ?? false;
|
|
61
|
+
}
|
|
62
|
+
present() {
|
|
63
|
+
if (this.undated)
|
|
64
|
+
return true;
|
|
65
|
+
return this.year !== undefined && this.year !== "";
|
|
66
|
+
}
|
|
67
|
+
render(context) {
|
|
68
|
+
if (this.undated && (this.year === undefined || this.year === ""))
|
|
69
|
+
return "--";
|
|
70
|
+
if (!this.present())
|
|
71
|
+
return undefined;
|
|
72
|
+
if (context === "urn")
|
|
73
|
+
return this.year;
|
|
74
|
+
if (this.month === undefined)
|
|
75
|
+
return this.year;
|
|
76
|
+
let result = `${this.year}-${pad2(this.month)}`;
|
|
77
|
+
if (this.day !== undefined)
|
|
78
|
+
result += `-${pad2(this.day)}`;
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
toWire() {
|
|
82
|
+
const wire = {};
|
|
83
|
+
if (this.year !== undefined)
|
|
84
|
+
wire["year"] = this.year;
|
|
85
|
+
if (this.month !== undefined)
|
|
86
|
+
wire["month"] = this.month;
|
|
87
|
+
if (this.day !== undefined)
|
|
88
|
+
wire["day"] = this.day;
|
|
89
|
+
if (this.undated)
|
|
90
|
+
wire["undated"] = true;
|
|
91
|
+
return wire;
|
|
92
|
+
}
|
|
93
|
+
fieldIsDefaulted(name, value) {
|
|
94
|
+
return name === "undated" && value === false;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/** Publisher (Components::Publisher): body; URN render lowercases. */
|
|
98
|
+
export class Publisher extends Component {
|
|
99
|
+
body;
|
|
100
|
+
constructor(attrs) {
|
|
101
|
+
super();
|
|
102
|
+
this.body = attrs["body"];
|
|
103
|
+
}
|
|
104
|
+
render(context) {
|
|
105
|
+
return context === "urn" ? this.body.toLowerCase() : this.body;
|
|
106
|
+
}
|
|
107
|
+
toWire() {
|
|
108
|
+
return { body: this.body };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/** Language (Components::Language): single-char CHAR_MAP codes; URN lowercases. */
|
|
112
|
+
export class Language extends Component {
|
|
113
|
+
static CHAR_MAP = {
|
|
114
|
+
R: "ru",
|
|
115
|
+
F: "fr",
|
|
116
|
+
E: "en",
|
|
117
|
+
A: "ar",
|
|
118
|
+
S: "es",
|
|
119
|
+
D: "de",
|
|
120
|
+
};
|
|
121
|
+
code;
|
|
122
|
+
originalCode;
|
|
123
|
+
constructor(attrs) {
|
|
124
|
+
super();
|
|
125
|
+
this.code = attrs["code"];
|
|
126
|
+
this.originalCode = (attrs["originalCode"] ?? attrs["original_code"]);
|
|
127
|
+
}
|
|
128
|
+
render(context) {
|
|
129
|
+
if (context === "urn")
|
|
130
|
+
return this.code.toLowerCase();
|
|
131
|
+
if (this.originalCode !== undefined) {
|
|
132
|
+
return this.originalCode.length === 1 ? this.code : this.originalCode;
|
|
133
|
+
}
|
|
134
|
+
return this.code;
|
|
135
|
+
}
|
|
136
|
+
toWire() {
|
|
137
|
+
return this.originalCode === undefined ? { code: this.code } : { code: this.code, original_code: this.originalCode };
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/** Edition (Components::Edition): degenerates to its number. */
|
|
141
|
+
export class Edition extends Component {
|
|
142
|
+
number;
|
|
143
|
+
phase;
|
|
144
|
+
constructor(attrs) {
|
|
145
|
+
super();
|
|
146
|
+
this.number = attrs["number"];
|
|
147
|
+
this.phase = attrs["phase"];
|
|
148
|
+
}
|
|
149
|
+
render(context) {
|
|
150
|
+
void context;
|
|
151
|
+
return this.phase === undefined ? this.number : `${this.number}${this.phase}`;
|
|
152
|
+
}
|
|
153
|
+
toWire() {
|
|
154
|
+
return this.phase === undefined ? { number: this.number } : { number: this.number, phase: this.phase };
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/** Iteration (Components::Iteration): degenerate by construction. */
|
|
158
|
+
export class Iteration extends Component {
|
|
159
|
+
string;
|
|
160
|
+
constructor(attrs) {
|
|
161
|
+
super();
|
|
162
|
+
this.string = attrs["string"];
|
|
163
|
+
}
|
|
164
|
+
render(_context) {
|
|
165
|
+
return this.string;
|
|
166
|
+
}
|
|
167
|
+
toWire() {
|
|
168
|
+
return { string: this.string };
|
|
169
|
+
}
|
|
170
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BaseIdentifier — the TS mirror of Pubid::Identifier's declaration and
|
|
3
|
+
* canonical-serialization core. Semantics pinned in
|
|
4
|
+
* TODO.unified/01-study-pubid-core.md §2-3:
|
|
5
|
+
*
|
|
6
|
+
* toHash: `_type` first, mapped attributes, then canonicalization
|
|
7
|
+
* (drop empty / default-valued attributes, recursively), then
|
|
8
|
+
* degenerate-component flattening (`date: {year: "2024"}` → `year:
|
|
9
|
+
* "2024"`, `edition: {number: "2"}` → `edition: "2"`), then the class's
|
|
10
|
+
* compactHash hook.
|
|
11
|
+
*
|
|
12
|
+
* fromHash: dispatch on `_type` through the registered type map,
|
|
13
|
+
* re-inflate flat scalars into component hashes (unless the flavor has a
|
|
14
|
+
* converter for the key or declares the flat name as its own attribute),
|
|
15
|
+
* construct with type coercion.
|
|
16
|
+
*
|
|
17
|
+
* Flavors NEVER hand-roll a hash here — they declare attributes and
|
|
18
|
+
* mappings; the only imperative wire code is a converter pair.
|
|
19
|
+
*/
|
|
20
|
+
import type { AttributeTable, FieldMapping } from "./attribute.js";
|
|
21
|
+
export interface IdentifierStatic {
|
|
22
|
+
new (attrs?: Record<string, unknown>): BaseIdentifier;
|
|
23
|
+
polymorphicName: string;
|
|
24
|
+
attributes: AttributeTable;
|
|
25
|
+
/** Optional mapping overrides; default = every attribute under its own name. */
|
|
26
|
+
mappings?: FieldMapping[];
|
|
27
|
+
/** Per-class shorter-wire hook (Ruby compact_hash); must invert in inflate. */
|
|
28
|
+
compactHash?: (model: BaseIdentifier, hash: Record<string, unknown>) => void;
|
|
29
|
+
/** Additional flat-scalar entries for this class. */
|
|
30
|
+
flatScalarComponents?: Record<string, string>;
|
|
31
|
+
/** The field each additional flat-scalar component degenerates to. */
|
|
32
|
+
flatScalarFields?: Record<string, string>;
|
|
33
|
+
fromHash(hash: Record<string, unknown>): BaseIdentifier;
|
|
34
|
+
/** The flavor's URN generator; undefined = the base template (Ruby resolve_urn_generator fallback). */
|
|
35
|
+
urnGenerator?: UrnGeneratorCtor;
|
|
36
|
+
}
|
|
37
|
+
export type UrnGeneratorCtor = new (identifier: BaseIdentifier) => {
|
|
38
|
+
generate(): string;
|
|
39
|
+
};
|
|
40
|
+
/** Register a concrete identifier class under its polymorphic _type (the *_TYPE_MAP role). */
|
|
41
|
+
export declare function registerType(klass: IdentifierStatic): void;
|
|
42
|
+
export declare function resolveType(type: string): IdentifierStatic | undefined;
|
|
43
|
+
export declare abstract class BaseIdentifier {
|
|
44
|
+
static polymorphicName: string;
|
|
45
|
+
/** The human renderer body — the one method every flavor writes. */
|
|
46
|
+
abstract render(): string;
|
|
47
|
+
/** The root table; subclasses compose via extendAttributes(BaseIdentifier, …). */
|
|
48
|
+
static attributes: AttributeTable;
|
|
49
|
+
static mappings: FieldMapping[] | undefined;
|
|
50
|
+
static compactHash: ((model: BaseIdentifier, hash: Record<string, unknown>) => void) | undefined;
|
|
51
|
+
static flatScalarComponents: Record<string, string> | undefined;
|
|
52
|
+
["constructor"]: IdentifierStatic;
|
|
53
|
+
constructor(attrs?: Record<string, unknown>);
|
|
54
|
+
/** The human form (Ruby render(format: :human) → the flavor renderer). */
|
|
55
|
+
toHuman(): string;
|
|
56
|
+
/** Ruby to_urn: the flavor's UrnGenerator, else the base template. */
|
|
57
|
+
toUrn(): string;
|
|
58
|
+
fromHash(hash: Record<string, unknown>): BaseIdentifier;
|
|
59
|
+
protected classAttributes(): AttributeTable;
|
|
60
|
+
/** Wire key for an attribute: custom mapping or the attribute name. */
|
|
61
|
+
protected wireKeyFor(name: string): {
|
|
62
|
+
wire: string;
|
|
63
|
+
toWire?: FieldMapping["toWire"];
|
|
64
|
+
};
|
|
65
|
+
protected attrValue(name: string): unknown;
|
|
66
|
+
/** Serialize one attribute's value (component → toWire, nested identifier → toHash, scalars as-is). */
|
|
67
|
+
protected serializeValue(value: unknown): unknown;
|
|
68
|
+
/** Nested serialization: same as toHash but with the unfiltered mappings. */
|
|
69
|
+
toHashNested(): Record<string, unknown>;
|
|
70
|
+
toHash(): Record<string, unknown>;
|
|
71
|
+
private toHashWith;
|
|
72
|
+
/**
|
|
73
|
+
* Degenerate single-field components collapse to their scalar
|
|
74
|
+
* (identifier.rb flatten_scalar_components): `date` RENAMES to `year`,
|
|
75
|
+
* `edition` keeps its name; guards: never overwrite an emitted wire
|
|
76
|
+
* key, never rename onto a declared attribute name.
|
|
77
|
+
*/
|
|
78
|
+
protected flattenScalars(hash: Record<string, unknown>): void;
|
|
79
|
+
static fromHash(hash: Record<string, unknown>): BaseIdentifier;
|
|
80
|
+
/** Re-nest flat scalars into component hashes (identifier.rb inflate_scalar_components). */
|
|
81
|
+
protected static inflateScalarComponents(data: Record<string, unknown>): Record<string, unknown>;
|
|
82
|
+
/** Apply custom `fromWire` converters to the inflated hash. */
|
|
83
|
+
protected static applyMappings(data: Record<string, unknown>): Record<string, unknown>;
|
|
84
|
+
}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BaseIdentifier — the TS mirror of Pubid::Identifier's declaration and
|
|
3
|
+
* canonical-serialization core. Semantics pinned in
|
|
4
|
+
* TODO.unified/01-study-pubid-core.md §2-3:
|
|
5
|
+
*
|
|
6
|
+
* toHash: `_type` first, mapped attributes, then canonicalization
|
|
7
|
+
* (drop empty / default-valued attributes, recursively), then
|
|
8
|
+
* degenerate-component flattening (`date: {year: "2024"}` → `year:
|
|
9
|
+
* "2024"`, `edition: {number: "2"}` → `edition: "2"`), then the class's
|
|
10
|
+
* compactHash hook.
|
|
11
|
+
*
|
|
12
|
+
* fromHash: dispatch on `_type` through the registered type map,
|
|
13
|
+
* re-inflate flat scalars into component hashes (unless the flavor has a
|
|
14
|
+
* converter for the key or declares the flat name as its own attribute),
|
|
15
|
+
* construct with type coercion.
|
|
16
|
+
*
|
|
17
|
+
* Flavors NEVER hand-roll a hash here — they declare attributes and
|
|
18
|
+
* mappings; the only imperative wire code is a converter pair.
|
|
19
|
+
*/
|
|
20
|
+
import { FLAT_SCALAR_COMPONENTS, FLAT_SCALAR_FIELDS, BASE_ATTRIBUTES } from "./attribute.js";
|
|
21
|
+
import { Component } from "./component.js";
|
|
22
|
+
import { BaseUrnGenerator } from "./urn-generator.js";
|
|
23
|
+
const TYPE_REGISTRY = new Map();
|
|
24
|
+
/** Register a concrete identifier class under its polymorphic _type (the *_TYPE_MAP role). */
|
|
25
|
+
export function registerType(klass) {
|
|
26
|
+
TYPE_REGISTRY.set(klass.polymorphicName, klass);
|
|
27
|
+
}
|
|
28
|
+
export function resolveType(type) {
|
|
29
|
+
return TYPE_REGISTRY.get(type);
|
|
30
|
+
}
|
|
31
|
+
function isScalarType(t) {
|
|
32
|
+
return typeof t === "string";
|
|
33
|
+
}
|
|
34
|
+
function coerce(value, spec) {
|
|
35
|
+
if (value === undefined || value === null)
|
|
36
|
+
return value;
|
|
37
|
+
if (spec.collection) {
|
|
38
|
+
const list = Array.isArray(value) ? value : [value];
|
|
39
|
+
return list.map((v) => coerceOne(v, spec));
|
|
40
|
+
}
|
|
41
|
+
return coerceOne(value, spec);
|
|
42
|
+
}
|
|
43
|
+
function coerceOne(value, spec) {
|
|
44
|
+
if (isScalarType(spec.type)) {
|
|
45
|
+
if (spec.type === "integer")
|
|
46
|
+
return Number(value);
|
|
47
|
+
if (spec.type === "boolean")
|
|
48
|
+
return Boolean(value);
|
|
49
|
+
return typeof value === "object" && value !== null ? value : String(value);
|
|
50
|
+
}
|
|
51
|
+
if (value instanceof spec.type)
|
|
52
|
+
return value;
|
|
53
|
+
// A cross-flavor identifier instance (e.g. an IEC id nested in an IEEE
|
|
54
|
+
// adoption) is complete already; coercing through the declared flavor's
|
|
55
|
+
// fromHash would rebuild it as the wrong (abstract) class.
|
|
56
|
+
if (value instanceof BaseIdentifier)
|
|
57
|
+
return value;
|
|
58
|
+
if (typeof value === "object" && value !== null) {
|
|
59
|
+
// Identifier-typed attributes (oiml supplement `base`) dispatch
|
|
60
|
+
// polymorphically through the registered type map.
|
|
61
|
+
if (typeof spec.type === "function" && spec.type.prototype instanceof BaseIdentifier) {
|
|
62
|
+
const idCtor = spec.type;
|
|
63
|
+
const v = value;
|
|
64
|
+
return "_type" in v ? idCtor.fromHash(v) : idCtor.fromHash({ ...v, _type: idCtor.polymorphicName });
|
|
65
|
+
}
|
|
66
|
+
return new spec.type(value);
|
|
67
|
+
}
|
|
68
|
+
return value;
|
|
69
|
+
}
|
|
70
|
+
/** Utils.empty?: nil is NOT empty here (nil is dropped earlier); "" and [] are. */
|
|
71
|
+
function isEmptyValue(value) {
|
|
72
|
+
if (value === "")
|
|
73
|
+
return true;
|
|
74
|
+
if (Array.isArray(value) && value.length === 0)
|
|
75
|
+
return true;
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
function resolveDefault(spec) {
|
|
79
|
+
return typeof spec.default === "function" ? spec.default() : spec.default;
|
|
80
|
+
}
|
|
81
|
+
export class BaseIdentifier {
|
|
82
|
+
/** The root table; subclasses compose via extendAttributes(BaseIdentifier, …). */
|
|
83
|
+
static attributes = BASE_ATTRIBUTES;
|
|
84
|
+
constructor(attrs = {}) {
|
|
85
|
+
for (const [name, spec] of Object.entries(this.classAttributes())) {
|
|
86
|
+
const value = attrs[name];
|
|
87
|
+
if (value !== undefined) {
|
|
88
|
+
this[name] = coerce(value, spec);
|
|
89
|
+
}
|
|
90
|
+
else if (spec.initializeEmpty && spec.collection) {
|
|
91
|
+
this[name] = [];
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/** The human form (Ruby render(format: :human) → the flavor renderer). */
|
|
96
|
+
toHuman() {
|
|
97
|
+
return this.render();
|
|
98
|
+
}
|
|
99
|
+
/** Ruby to_urn: the flavor's UrnGenerator, else the base template. */
|
|
100
|
+
toUrn() {
|
|
101
|
+
const Generator = this.constructor.urnGenerator ?? BaseUrnGenerator;
|
|
102
|
+
return new Generator(this).generate();
|
|
103
|
+
}
|
|
104
|
+
fromHash(hash) {
|
|
105
|
+
return this.constructor.fromHash(hash);
|
|
106
|
+
}
|
|
107
|
+
classAttributes() {
|
|
108
|
+
return this.constructor.attributes;
|
|
109
|
+
}
|
|
110
|
+
/** Wire key for an attribute: custom mapping or the attribute name. */
|
|
111
|
+
wireKeyFor(name) {
|
|
112
|
+
const mappings = this.constructor.mappings;
|
|
113
|
+
const found = mappings?.find((m) => m.to === name);
|
|
114
|
+
return found ?? { wire: name };
|
|
115
|
+
}
|
|
116
|
+
attrValue(name) {
|
|
117
|
+
return this[name];
|
|
118
|
+
}
|
|
119
|
+
/** Serialize one attribute's value (component → toWire, nested identifier → toHash, scalars as-is). */
|
|
120
|
+
serializeValue(value) {
|
|
121
|
+
// A nested identifier serializes through the NESTED mapping list: a
|
|
122
|
+
// class whose top-level wire deletes derived keys (the Ruby reference
|
|
123
|
+
// drops JointDevelopment's derived publisher/copublisher only on the
|
|
124
|
+
// public to_hash — lutaml's nested transform reads the accessors and
|
|
125
|
+
// keeps them) must keep them here.
|
|
126
|
+
if (value instanceof BaseIdentifier)
|
|
127
|
+
return value.toHashNested();
|
|
128
|
+
if (value instanceof Component)
|
|
129
|
+
return value.toWire();
|
|
130
|
+
if (Array.isArray(value))
|
|
131
|
+
return value.map((v) => this.serializeValue(v));
|
|
132
|
+
return value;
|
|
133
|
+
}
|
|
134
|
+
/** Nested serialization: same as toHash but with the unfiltered mappings. */
|
|
135
|
+
toHashNested() {
|
|
136
|
+
const ctor = this.constructor;
|
|
137
|
+
if (ctor.mappingsNested === undefined)
|
|
138
|
+
return this.toHash();
|
|
139
|
+
return this.toHashWith(ctor.mappingsNested);
|
|
140
|
+
}
|
|
141
|
+
toHash() {
|
|
142
|
+
return this.toHashWith(this.constructor.mappings);
|
|
143
|
+
}
|
|
144
|
+
toHashWith(mappings) {
|
|
145
|
+
const hash = { _type: this.constructor.polymorphicName };
|
|
146
|
+
// A flavor's explicit mapping list is a WHITELIST (lutaml key_value):
|
|
147
|
+
// unmapped attributes never serialize — that is how un keeps its
|
|
148
|
+
// runtime `date` out of the hash while the URN still reads it.
|
|
149
|
+
const emitted = mappings
|
|
150
|
+
? mappings.map((m) => [m.to, m.wire, m.toWire])
|
|
151
|
+
: Object.keys(this.classAttributes()).map((name) => [name, name, undefined]);
|
|
152
|
+
for (const [name, wire, toWire] of emitted) {
|
|
153
|
+
const value = this.attrValue(name);
|
|
154
|
+
// nil never serializes; empty and default-valued attributes are
|
|
155
|
+
// dropped by canonicalization (TODO.unified/01 §2.2).
|
|
156
|
+
if (value === undefined || value === null)
|
|
157
|
+
continue;
|
|
158
|
+
const spec = this.classAttributes()[name];
|
|
159
|
+
if (spec && isEmptyValue(value))
|
|
160
|
+
continue;
|
|
161
|
+
if (spec?.default !== undefined && deepEqual(value, resolveDefault(spec)))
|
|
162
|
+
continue;
|
|
163
|
+
const serialized = toWire ? toWire(this) : this.serializeValue(value);
|
|
164
|
+
if (serialized === undefined || serialized === null)
|
|
165
|
+
continue;
|
|
166
|
+
hash[wire] = serialized;
|
|
167
|
+
}
|
|
168
|
+
this.flattenScalars(hash);
|
|
169
|
+
this.constructor.compactHash?.(this, hash);
|
|
170
|
+
return hash;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Degenerate single-field components collapse to their scalar
|
|
174
|
+
* (identifier.rb flatten_scalar_components): `date` RENAMES to `year`,
|
|
175
|
+
* `edition` keeps its name; guards: never overwrite an emitted wire
|
|
176
|
+
* key, never rename onto a declared attribute name.
|
|
177
|
+
*/
|
|
178
|
+
flattenScalars(hash) {
|
|
179
|
+
const table = { ...FLAT_SCALAR_COMPONENTS, ...this.constructor.flatScalarComponents };
|
|
180
|
+
for (const [attrName, flatKey] of Object.entries(table)) {
|
|
181
|
+
const key = attrName in hash ? attrName : undefined;
|
|
182
|
+
if (key === undefined)
|
|
183
|
+
continue;
|
|
184
|
+
const value = hash[key];
|
|
185
|
+
const field = FLAT_SCALAR_FIELDS[attrName] ?? this.constructor.flatScalarFields?.[attrName];
|
|
186
|
+
if (field === undefined)
|
|
187
|
+
continue;
|
|
188
|
+
const model = this.attrValue(attrName);
|
|
189
|
+
if (Array.isArray(value) && Array.isArray(model)) {
|
|
190
|
+
if (model.every((c) => c instanceof Component) && model.length === value.length) {
|
|
191
|
+
const scalars = model.map((c) => c.degenerateScalar(field));
|
|
192
|
+
if (scalars.every((s) => s !== undefined))
|
|
193
|
+
hash[key] = scalars;
|
|
194
|
+
}
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
if (!(model instanceof Component))
|
|
198
|
+
continue;
|
|
199
|
+
const scalar = model.degenerateScalar(field);
|
|
200
|
+
if (scalar === undefined)
|
|
201
|
+
continue;
|
|
202
|
+
if (flatKey !== key) {
|
|
203
|
+
if (flatKey in hash || flatKey in this.classAttributes())
|
|
204
|
+
continue;
|
|
205
|
+
delete hash[key];
|
|
206
|
+
hash[flatKey] = scalar;
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
hash[key] = scalar;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
static fromHash(hash) {
|
|
214
|
+
const klass = typeof hash["_type"] === "string" ? resolveType(hash["_type"]) : undefined;
|
|
215
|
+
if (klass && klass !== this) {
|
|
216
|
+
return klass.fromHash(hash);
|
|
217
|
+
}
|
|
218
|
+
const inflated = this.inflateScalarComponents(hash);
|
|
219
|
+
return new this(this.applyMappings(inflated));
|
|
220
|
+
}
|
|
221
|
+
/** Re-nest flat scalars into component hashes (identifier.rb inflate_scalar_components). */
|
|
222
|
+
static inflateScalarComponents(data) {
|
|
223
|
+
const klass = this;
|
|
224
|
+
if (!klass.attributes)
|
|
225
|
+
return data;
|
|
226
|
+
const convertedKeys = new Set((klass.mappings ?? []).map((m) => m.wire));
|
|
227
|
+
const out = { ...data };
|
|
228
|
+
for (const [attrName, flatKey] of Object.entries({ ...FLAT_SCALAR_COMPONENTS, ...klass.flatScalarComponents })) {
|
|
229
|
+
const spec = klass.attributes[attrName];
|
|
230
|
+
if (!spec || typeof spec.type === "string")
|
|
231
|
+
continue; // component attributes only
|
|
232
|
+
if (convertedKeys.has(flatKey))
|
|
233
|
+
continue;
|
|
234
|
+
if (flatKey !== attrName && flatKey in klass.attributes)
|
|
235
|
+
continue;
|
|
236
|
+
const value = out[flatKey];
|
|
237
|
+
if (value === undefined || value === null || typeof value === "object")
|
|
238
|
+
continue;
|
|
239
|
+
const field = FLAT_SCALAR_FIELDS[attrName];
|
|
240
|
+
if (Array.isArray(value))
|
|
241
|
+
continue; // scalar-list collections keep their name
|
|
242
|
+
delete out[flatKey];
|
|
243
|
+
out[attrName] = { [field]: String(value) };
|
|
244
|
+
}
|
|
245
|
+
return out;
|
|
246
|
+
}
|
|
247
|
+
/** Apply custom `fromWire` converters to the inflated hash. */
|
|
248
|
+
static applyMappings(data) {
|
|
249
|
+
const klass = this;
|
|
250
|
+
const mappings = klass.mappings;
|
|
251
|
+
if (!mappings)
|
|
252
|
+
return data;
|
|
253
|
+
const out = { ...data };
|
|
254
|
+
for (const m of mappings) {
|
|
255
|
+
if (m.fromWire && m.wire in out) {
|
|
256
|
+
const value = m.fromWire(out);
|
|
257
|
+
if (value !== undefined && value !== null)
|
|
258
|
+
out[m.to] = value;
|
|
259
|
+
}
|
|
260
|
+
else if (m.wire !== m.to && m.wire in out) {
|
|
261
|
+
out[m.to] = out[m.wire];
|
|
262
|
+
delete out[m.wire];
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return out;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
function deepEqual(a, b) {
|
|
269
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
270
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BaseUrnGenerator — the TS mirror of Pubid::UrnGenerator::Base
|
|
3
|
+
* (TODO.unified/01 §4). Flavors with a Ruby `urn_generator.rb` subclass
|
|
4
|
+
* and override hooks; flavors without one (doi, isbn, omg, un, gb) use
|
|
5
|
+
* this template directly, and the parts are derived from their declared
|
|
6
|
+
* attributes — never hand-assembled per flavor.
|
|
7
|
+
*/
|
|
8
|
+
import type { BaseIdentifier } from "./identifier.js";
|
|
9
|
+
type UrnableIdentifier = BaseIdentifier;
|
|
10
|
+
export declare class BaseUrnGenerator<T extends UrnableIdentifier = UrnableIdentifier> {
|
|
11
|
+
protected readonly identifier: T;
|
|
12
|
+
constructor(identifier: T);
|
|
13
|
+
generate(): string;
|
|
14
|
+
/** Template methods — override in subclasses (Ruby precedent). */
|
|
15
|
+
protected urnNamespace(): string;
|
|
16
|
+
/** Reads a DECLARED attribute only (Base#maybe) — constants never appear. */
|
|
17
|
+
protected maybe(name: string): unknown;
|
|
18
|
+
protected urnPublisher(): string | undefined;
|
|
19
|
+
protected urnType(): string | undefined;
|
|
20
|
+
protected urnNumber(): string | undefined;
|
|
21
|
+
protected urnPart(): string | undefined;
|
|
22
|
+
protected urnSubpart(): string | undefined;
|
|
23
|
+
protected urnYear(): string | undefined;
|
|
24
|
+
protected urnEdition(): string | undefined;
|
|
25
|
+
protected urnLanguage(): string | undefined;
|
|
26
|
+
}
|
|
27
|
+
export {};
|