glossarist 0.1.1 → 0.1.2

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/src/uuid.js ADDED
@@ -0,0 +1,28 @@
1
+ import { createHash } from 'crypto';
2
+
3
+ const NAMESPACE_UUID = '6ba7b810-9dad-11d1-80b4-00c04fd430c8';
4
+
5
+ export function uuidV5(namespace, name) {
6
+ const ns = _parseUuid(namespace);
7
+ const hash = createHash('sha1').update(ns).update(name, 'utf8').digest();
8
+ hash[6] = (hash[6] & 0x0f) | 0x50;
9
+ hash[8] = (hash[8] & 0x3f) | 0x80;
10
+ return _formatUuid(hash);
11
+ }
12
+
13
+ export function conceptUuid(conceptId, namespace = NAMESPACE_UUID) {
14
+ return uuidV5(namespace, conceptId);
15
+ }
16
+
17
+ export function localizedConceptUuid(conceptId, languageCode, namespace = NAMESPACE_UUID) {
18
+ return uuidV5(namespace, `${conceptId}:${languageCode}`);
19
+ }
20
+
21
+ function _parseUuid(str) {
22
+ return Buffer.from(str.replace(/-/g, ''), 'hex');
23
+ }
24
+
25
+ function _formatUuid(hash) {
26
+ const h = hash.toString('hex');
27
+ return [h.slice(0, 8), h.slice(8, 12), h.slice(12, 16), h.slice(16, 20), h.slice(20, 32)].join('-');
28
+ }
@@ -0,0 +1,63 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import yaml from 'js-yaml';
4
+ import { Concept } from './models/concept.js';
5
+ import { InvalidInputError } from './errors.js';
6
+
7
+ export class V1Reader {
8
+ static isV1Directory(dir) {
9
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
10
+ return entries.some(e =>
11
+ e.isDirectory() && fs.existsSync(path.join(dir, e.name, 'concept.yaml'))
12
+ );
13
+ }
14
+
15
+ static readConcept(conceptDir) {
16
+ const conceptFile = path.join(conceptDir, 'concept.yaml');
17
+ if (!fs.existsSync(conceptFile)) {
18
+ throw new InvalidInputError(`No concept.yaml found in ${conceptDir}`);
19
+ }
20
+
21
+ const data = yaml.load(fs.readFileSync(conceptFile, 'utf8'));
22
+ const localizations = {};
23
+
24
+ for (const file of fs.readdirSync(conceptDir)) {
25
+ if (file === 'concept.yaml' || !file.endsWith('.yaml')) continue;
26
+ const lang = file.slice(0, -'.yaml'.length);
27
+ localizations[lang] = yaml.load(fs.readFileSync(path.join(conceptDir, file), 'utf8'));
28
+ }
29
+
30
+ return new Concept({
31
+ id: String(data.termid ?? data.data?.identifier ?? path.basename(conceptDir)),
32
+ term: data.term ?? null,
33
+ localizations,
34
+ raw: data,
35
+ });
36
+ }
37
+
38
+ static readAll(dir) {
39
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
40
+ const concepts = [];
41
+
42
+ for (const entry of entries) {
43
+ if (!entry.isDirectory()) continue;
44
+ const conceptDir = path.join(dir, entry.name);
45
+ if (!fs.existsSync(path.join(conceptDir, 'concept.yaml'))) continue;
46
+ try {
47
+ concepts.push(V1Reader.readConcept(conceptDir));
48
+ } catch { /* skip unreadable concepts */ }
49
+ }
50
+
51
+ return concepts;
52
+ }
53
+ }
54
+
55
+ export async function migrateV1ToV2(v1Dir, v2Dir) {
56
+ const concepts = V1Reader.readAll(v1Dir);
57
+ const { writeConcept } = await import('./concept-writer.js');
58
+ fs.mkdirSync(v2Dir, { recursive: true });
59
+
60
+ for (const concept of concepts) {
61
+ writeConcept(v2Dir, concept, 'canonical');
62
+ }
63
+ }
@@ -0,0 +1,119 @@
1
+ import { ValidationRule } from './validation-rule.js';
2
+ import { ValidationError } from './validation-error.js';
3
+
4
+ const VALID_DESIGNATION_TYPES = new Set([
5
+ 'expression', 'abbreviation', 'symbol', 'graphical symbol', 'graphical_symbol',
6
+ ]);
7
+
8
+ const VALID_ENTRY_STATUSES = new Set([
9
+ 'valid', 'draft', 'retired', 'notValid', 'superseded', 'withdrawn',
10
+ ]);
11
+
12
+ export class LanguageCodeRule extends ValidationRule {
13
+ constructor() { super('language-code'); }
14
+ validate(value, path) {
15
+ if (!value.localizations) return [];
16
+ const errors = [];
17
+ for (const lang of Object.keys(value.localizations)) {
18
+ if (!/^[a-z]{3}$/.test(lang)) {
19
+ errors.push(...this.error(`${path}localizations.${lang}`,
20
+ `Invalid language code '${lang}': expected ISO 639-3 (3 lowercase letters)`));
21
+ }
22
+ }
23
+ return errors;
24
+ }
25
+ }
26
+
27
+ export class DesignationTypeRule extends ValidationRule {
28
+ constructor() { super('designation-type'); }
29
+ validate(value, path) {
30
+ if (!value.localizations) return [];
31
+ const errors = [];
32
+ for (const [lang, lc] of Object.entries(value.localizations)) {
33
+ for (let i = 0; i < (lc.terms?.length ?? 0); i++) {
34
+ const t = lc.terms[i];
35
+ if (t.type && !VALID_DESIGNATION_TYPES.has(t.type)) {
36
+ errors.push(...this.error(`${path}localizations.${lang}.terms[${i}].type`,
37
+ `Unknown designation type '${t.type}'`));
38
+ }
39
+ }
40
+ }
41
+ return errors;
42
+ }
43
+ }
44
+
45
+ export class EntryStatusRule extends ValidationRule {
46
+ constructor() { super('entry-status'); }
47
+ validate(value, path) {
48
+ if (!value.localizations) return [];
49
+ const errors = [];
50
+ for (const [lang, lc] of Object.entries(value.localizations)) {
51
+ if (lc.entry_status && !VALID_ENTRY_STATUSES.has(lc.entry_status)) {
52
+ errors.push(...this.error(`${path}localizations.${lang}.entry_status`,
53
+ `Unknown entry status '${lc.entry_status}'`));
54
+ }
55
+ }
56
+ return errors;
57
+ }
58
+ }
59
+
60
+ export class ConceptValidator {
61
+ _rules = [];
62
+
63
+ addRule(rule) {
64
+ this._rules.push(rule);
65
+ return this;
66
+ }
67
+
68
+ validate(concept) {
69
+ const errors = [];
70
+ const json = typeof concept.toJSON === 'function' ? concept.toJSON() : concept;
71
+
72
+ if (!json.id) {
73
+ errors.push(new ValidationError('id', 'Concept must have an id'));
74
+ }
75
+ if (!json.localizations || Object.keys(json.localizations).length === 0) {
76
+ errors.push(new ValidationError('localizations',
77
+ 'Concept must have at least one localization', 'warning'));
78
+ } else {
79
+ for (const [lang, lc] of Object.entries(json.localizations)) {
80
+ if (!lc.terms || lc.terms.length === 0) {
81
+ errors.push(new ValidationError(
82
+ `localizations.${lang}.terms`,
83
+ `Localization '${lang}' must have at least one term`, 'warning'));
84
+ }
85
+ }
86
+ }
87
+
88
+ for (const rule of this._rules) {
89
+ errors.push(...rule.validate(json, ''));
90
+ }
91
+
92
+ return {
93
+ valid: errors.filter(e => e.severity === 'error').length === 0,
94
+ errors: errors.filter(e => e.severity === 'error'),
95
+ warnings: errors.filter(e => e.severity === 'warning'),
96
+ };
97
+ }
98
+ }
99
+
100
+ export class RegisterValidator {
101
+ validate(register) {
102
+ const errors = [];
103
+ if (!register || typeof register !== 'object') {
104
+ errors.push(new ValidationError('', 'Register must be a non-null object'));
105
+ return { valid: false, errors, warnings: [] };
106
+ }
107
+ if (!register.schema_version) {
108
+ errors.push(new ValidationError('schema_version', 'Register must have a schema_version', 'warning'));
109
+ }
110
+ if (!register.shortname) {
111
+ errors.push(new ValidationError('shortname', 'Register should have a shortname', 'warning'));
112
+ }
113
+ return {
114
+ valid: errors.filter(e => e.severity === 'error').length === 0,
115
+ errors: errors.filter(e => e.severity === 'error'),
116
+ warnings: errors.filter(e => e.severity === 'warning'),
117
+ };
118
+ }
119
+ }
@@ -0,0 +1,26 @@
1
+ export class ValidationError {
2
+ readonly path: string;
3
+ readonly message: string;
4
+ readonly severity: 'error' | 'warning';
5
+ toString(): string;
6
+ }
7
+
8
+ export class ValidationRule {
9
+ readonly name: string;
10
+ readonly severity: 'error' | 'warning';
11
+ validate(value: any, path: string): ValidationError[];
12
+ }
13
+
14
+ export function validateConcept(concept: any): {
15
+ valid: boolean;
16
+ errors: ValidationError[];
17
+ warnings: ValidationError[];
18
+ };
19
+
20
+ export function validateRegister(register: any): {
21
+ valid: boolean;
22
+ errors: ValidationError[];
23
+ warnings: ValidationError[];
24
+ };
25
+
26
+ export function createConceptValidator(): import('./concept-validator').ConceptValidator;
@@ -0,0 +1,23 @@
1
+ export { ValidationError } from './validation-error.js';
2
+ export { ValidationRule } from './validation-rule.js';
3
+ export { ConceptValidator, RegisterValidator, LanguageCodeRule, DesignationTypeRule, EntryStatusRule } from './concept-validator.js';
4
+
5
+ import { ConceptValidator, LanguageCodeRule, DesignationTypeRule, EntryStatusRule } from './concept-validator.js';
6
+ import { RegisterValidator } from './concept-validator.js';
7
+
8
+ const _default = new ConceptValidator()
9
+ .addRule(new LanguageCodeRule())
10
+ .addRule(new DesignationTypeRule())
11
+ .addRule(new EntryStatusRule());
12
+
13
+ export function validateConcept(concept) {
14
+ return _default.validate(concept);
15
+ }
16
+
17
+ export function createConceptValidator() {
18
+ return new ConceptValidator();
19
+ }
20
+
21
+ export function validateRegister(register) {
22
+ return new RegisterValidator().validate(register);
23
+ }
@@ -0,0 +1,11 @@
1
+ export class ValidationError {
2
+ constructor(path, message, severity = 'error') {
3
+ this.path = path;
4
+ this.message = message;
5
+ this.severity = severity;
6
+ }
7
+
8
+ toString() {
9
+ return `[${this.severity.toUpperCase()}] ${this.path}: ${this.message}`;
10
+ }
11
+ }
@@ -0,0 +1,16 @@
1
+ import { ValidationError } from './validation-error.js';
2
+
3
+ export class ValidationRule {
4
+ constructor(name, severity = 'error') {
5
+ this.name = name;
6
+ this.severity = severity;
7
+ }
8
+
9
+ validate(_value, _path) {
10
+ throw new Error(`${this.constructor.name} must implement validate()`);
11
+ }
12
+
13
+ error(path, message) {
14
+ return [new ValidationError(path, message, this.severity)];
15
+ }
16
+ }