glossarist 2.4.0 → 2.5.1
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.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.rubocop_todo.yml +50 -146
- data/CLAUDE.md +85 -0
- data/Gemfile +26 -5
- data/README.adoc +383 -7
- data/TODO.integration/01-gcr-package-cli.md +180 -0
- data/exe/glossarist +1 -53
- data/glossarist.gemspec +3 -2
- data/lib/glossarist/asset.rb +1 -1
- data/lib/glossarist/citation.rb +1 -1
- data/lib/glossarist/cli/package_command.rb +32 -0
- data/lib/glossarist/cli/upgrade_command.rb +34 -0
- data/lib/glossarist/cli/validate_command.rb +56 -0
- data/lib/glossarist/cli.rb +105 -0
- data/lib/glossarist/collection_config.rb +23 -0
- data/lib/glossarist/collections/concept_source_collection.rb +9 -0
- data/lib/glossarist/collections/detailed_definition_collection.rb +18 -0
- data/lib/glossarist/collections/localization_collection.rb +37 -0
- data/lib/glossarist/collections/typed_collection.rb +26 -0
- data/lib/glossarist/collections.rb +21 -4
- data/lib/glossarist/concept.rb +1 -1
- data/lib/glossarist/concept_collector.rb +153 -0
- data/lib/glossarist/concept_data.rb +15 -8
- data/lib/glossarist/concept_date.rb +1 -1
- data/lib/glossarist/concept_document.rb +29 -0
- data/lib/glossarist/concept_enricher.rb +34 -0
- data/lib/glossarist/concept_manager.rb +31 -49
- data/lib/glossarist/concept_reference.rb +45 -0
- data/lib/glossarist/concept_source.rb +1 -1
- data/lib/glossarist/concept_validator.rb +114 -0
- data/lib/glossarist/custom_locality.rb +1 -1
- data/lib/glossarist/dataset_validator.rb +69 -0
- data/lib/glossarist/designation/abbreviation.rb +1 -1
- data/lib/glossarist/designation/base.rb +11 -4
- data/lib/glossarist/designation/expression.rb +1 -1
- data/lib/glossarist/designation/grammar_info.rb +1 -1
- data/lib/glossarist/designation/graphical_symbol.rb +1 -1
- data/lib/glossarist/designation/letter_symbol.rb +1 -1
- data/lib/glossarist/designation/symbol.rb +2 -2
- data/lib/glossarist/designation.rb +8 -11
- data/lib/glossarist/detailed_definition.rb +1 -1
- data/lib/glossarist/error.rb +2 -5
- data/lib/glossarist/gcr_metadata.rb +87 -0
- data/lib/glossarist/gcr_package.rb +223 -0
- data/lib/glossarist/gcr_statistics.rb +35 -0
- data/lib/glossarist/gcr_validator.rb +98 -0
- data/lib/glossarist/locality.rb +1 -1
- data/lib/glossarist/localized_concept.rb +12 -1
- data/lib/glossarist/managed_concept.rb +1 -1
- data/lib/glossarist/managed_concept_data.rb +8 -5
- data/lib/glossarist/non_verb_rep.rb +1 -1
- data/lib/glossarist/reference_extractor.rb +227 -0
- data/lib/glossarist/reference_resolver.rb +169 -0
- data/lib/glossarist/register_data.rb +39 -0
- data/lib/glossarist/related_concept.rb +1 -1
- data/lib/glossarist/resolution_adapter/local.rb +73 -0
- data/lib/glossarist/resolution_adapter/package.rb +22 -0
- data/lib/glossarist/resolution_adapter/remote.rb +60 -0
- data/lib/glossarist/resolution_adapter/route.rb +34 -0
- data/lib/glossarist/resolution_adapter.rb +14 -0
- data/lib/glossarist/schema_migration.rb +334 -0
- data/lib/glossarist/urn_resolver.rb +71 -0
- data/lib/glossarist/utilities.rb +6 -2
- data/lib/glossarist/v1/concept.rb +81 -0
- data/lib/glossarist/v1/cross_references.rb +41 -0
- data/lib/glossarist/v1/register.rb +50 -0
- data/lib/glossarist/v1.rb +9 -0
- data/lib/glossarist/validation_result.rb +38 -0
- data/lib/glossarist/version.rb +1 -1
- data/lib/glossarist.rb +54 -24
- metadata +62 -6
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module Glossarist
|
|
6
|
+
class SchemaMigration
|
|
7
|
+
CURRENT_SCHEMA_VERSION = "1"
|
|
8
|
+
|
|
9
|
+
ENTRY_STATUS_MAP = {
|
|
10
|
+
"Standard" => "valid",
|
|
11
|
+
"Confirmed" => "valid",
|
|
12
|
+
"Proposed" => "draft",
|
|
13
|
+
}.freeze
|
|
14
|
+
|
|
15
|
+
LANG_CODES = Glossarist::LANG_CODES
|
|
16
|
+
|
|
17
|
+
IEV_PATTERN = /\{\{([^,}]+),\s*IEV:([^}]+)\}\}/.freeze
|
|
18
|
+
URN_PATTERN = /\{urn:iso:std:iso:(\d+):([^,}]+),([^}]+)\}/.freeze
|
|
19
|
+
|
|
20
|
+
attr_reader :from_version, :to_version
|
|
21
|
+
|
|
22
|
+
def initialize(concept_hash, from_version: "0",
|
|
23
|
+
to_version: CURRENT_SCHEMA_VERSION, ref_maps: {})
|
|
24
|
+
@concept = concept_hash
|
|
25
|
+
@from_version = from_version
|
|
26
|
+
@to_version = to_version
|
|
27
|
+
@ref_maps = ref_maps
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def migrate
|
|
31
|
+
case [from_version, to_version]
|
|
32
|
+
when ["0", "1"] then migrate_v0_to_v1
|
|
33
|
+
else
|
|
34
|
+
raise Error, "Unsupported migration: #{from_version} -> #{to_version}"
|
|
35
|
+
end
|
|
36
|
+
@concept
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.upgrade_directory(source_dir, output:, # rubocop:disable Metrics/MethodLength, Metrics/ParameterLists
|
|
40
|
+
target_version: CURRENT_SCHEMA_VERSION,
|
|
41
|
+
cross_references: nil,
|
|
42
|
+
dry_run: false)
|
|
43
|
+
source_dir = File.expand_path(source_dir)
|
|
44
|
+
|
|
45
|
+
concepts_dir = find_concepts_dir(source_dir)
|
|
46
|
+
unless File.directory?(source_dir)
|
|
47
|
+
raise ArgumentError,
|
|
48
|
+
"#{source_dir} is not a directory"
|
|
49
|
+
end
|
|
50
|
+
unless concepts_dir
|
|
51
|
+
raise ArgumentError,
|
|
52
|
+
"No concept YAML files found in #{source_dir}"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
source_version = detect_schema_version(source_dir)
|
|
56
|
+
ref_maps = load_ref_maps(cross_references)
|
|
57
|
+
concepts = read_and_migrate_concepts(concepts_dir, source_version,
|
|
58
|
+
target_version, ref_maps)
|
|
59
|
+
register_data = read_register_yaml(source_dir, target_version)
|
|
60
|
+
|
|
61
|
+
write_output(concepts, register_data, output, dry_run)
|
|
62
|
+
|
|
63
|
+
{
|
|
64
|
+
concepts: concepts,
|
|
65
|
+
register_data: register_data,
|
|
66
|
+
source_version: source_version,
|
|
67
|
+
target_version: target_version,
|
|
68
|
+
output: File.expand_path(output),
|
|
69
|
+
count: concepts.length,
|
|
70
|
+
}
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def migrate_v0_to_v1
|
|
76
|
+
migrate_termid
|
|
77
|
+
LANG_CODES.each do |lang|
|
|
78
|
+
migrate_language_block(lang) if @concept[lang]
|
|
79
|
+
end
|
|
80
|
+
strip_revisions
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def migrate_termid
|
|
84
|
+
@concept["termid"] = String(@concept["termid"]) if @concept.key?("termid")
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def migrate_language_block(lang)
|
|
88
|
+
lc = @concept[lang]
|
|
89
|
+
return unless lc.is_a?(Hash)
|
|
90
|
+
|
|
91
|
+
migrate_definition(lc)
|
|
92
|
+
migrate_authoritative_source(lc)
|
|
93
|
+
migrate_dates(lc)
|
|
94
|
+
migrate_entry_status(lc)
|
|
95
|
+
migrate_terms_abbrev(lc)
|
|
96
|
+
extract_inline_refs(lc)
|
|
97
|
+
strip_revisions(lc)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def migrate_definition(lc)
|
|
101
|
+
return unless lc.key?("definition")
|
|
102
|
+
return unless lc["definition"].is_a?(String)
|
|
103
|
+
|
|
104
|
+
lc["definition"] = [{ "content" => lc["definition"] }]
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def migrate_authoritative_source(lc)
|
|
108
|
+
return unless lc.key?("authoritative_source")
|
|
109
|
+
|
|
110
|
+
src = lc.delete("authoritative_source")
|
|
111
|
+
return if lc.key?("sources")
|
|
112
|
+
|
|
113
|
+
sources = (src.is_a?(Array) ? src : [src]).map do |s|
|
|
114
|
+
next unless s.is_a?(Hash)
|
|
115
|
+
|
|
116
|
+
origin = {}
|
|
117
|
+
origin["ref"] = s["ref"] if s["ref"]
|
|
118
|
+
origin["clause"] = s["clause"] if s["clause"]
|
|
119
|
+
origin["link"] = s["link"] if s["link"]
|
|
120
|
+
|
|
121
|
+
entry = { "type" => "authoritative", "origin" => origin }
|
|
122
|
+
if s["relationship"]
|
|
123
|
+
entry["status"] = s["relationship"]["type"] || "identical"
|
|
124
|
+
if s["relationship"]["modification"]
|
|
125
|
+
entry["modification"] =
|
|
126
|
+
s["relationship"]["modification"]
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
entry
|
|
130
|
+
end.compact
|
|
131
|
+
|
|
132
|
+
lc["sources"] = sources if sources.any?
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def migrate_dates(lc)
|
|
136
|
+
return if lc.key?("dates")
|
|
137
|
+
|
|
138
|
+
dates = []
|
|
139
|
+
if lc["date_accepted"]
|
|
140
|
+
dates << { "type" => "accepted", "date" => lc["date_accepted"] }
|
|
141
|
+
end
|
|
142
|
+
if lc["date_amended"]
|
|
143
|
+
dates << { "type" => "amended", "date" => lc["date_amended"] }
|
|
144
|
+
end
|
|
145
|
+
lc["dates"] = dates if dates.any?
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def migrate_entry_status(lc)
|
|
149
|
+
return unless lc.key?("entry_status")
|
|
150
|
+
|
|
151
|
+
mapped = ENTRY_STATUS_MAP[lc["entry_status"]]
|
|
152
|
+
lc["entry_status"] = mapped if mapped
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def migrate_terms_abbrev(lc)
|
|
156
|
+
return unless lc["terms"].is_a?(Array)
|
|
157
|
+
|
|
158
|
+
lc["terms"].each do |term|
|
|
159
|
+
next unless term.is_a?(Hash)
|
|
160
|
+
next unless term["abbrev"] == true
|
|
161
|
+
|
|
162
|
+
term["type"] = "abbreviation"
|
|
163
|
+
term.delete("abbrev")
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def extract_inline_refs(lc)
|
|
168
|
+
texts = []
|
|
169
|
+
|
|
170
|
+
if lc["definition"].is_a?(Array)
|
|
171
|
+
lc["definition"].each do |d|
|
|
172
|
+
texts << (d.is_a?(Hash) ? d["content"].to_s : d.to_s)
|
|
173
|
+
end
|
|
174
|
+
elsif lc["definition"].is_a?(String)
|
|
175
|
+
texts << lc["definition"]
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
Array(lc["notes"]).each do |n|
|
|
179
|
+
texts << (n.is_a?(Hash) ? n["content"].to_s : n.to_s)
|
|
180
|
+
end
|
|
181
|
+
Array(lc["examples"]).each do |e|
|
|
182
|
+
texts << (e.is_a?(Hash) ? e["content"].to_s : e.to_s)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
full_text = texts.join(" ")
|
|
186
|
+
|
|
187
|
+
refs = []
|
|
188
|
+
|
|
189
|
+
full_text.scan(IEV_PATTERN) do |term, id|
|
|
190
|
+
refs << {
|
|
191
|
+
"term" => term.strip,
|
|
192
|
+
"concept_id" => id.strip,
|
|
193
|
+
"source" => "urn:iec:std:iec:60050",
|
|
194
|
+
"ref_type" => "urn",
|
|
195
|
+
}
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
full_text.scan(URN_PATTERN) do |std_num, id, term|
|
|
199
|
+
refs << {
|
|
200
|
+
"term" => term.strip,
|
|
201
|
+
"concept_id" => id.strip,
|
|
202
|
+
"source" => "urn:iso:std:iso:#{std_num}",
|
|
203
|
+
"ref_type" => "urn",
|
|
204
|
+
}
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
return if refs.empty?
|
|
208
|
+
|
|
209
|
+
existing = lc["references"] || []
|
|
210
|
+
seen_ids = existing.to_set { |r| r["concept_id"] || r["id"] }
|
|
211
|
+
refs.each do |ref|
|
|
212
|
+
key = ref["concept_id"] || ref["id"]
|
|
213
|
+
next if seen_ids.include?(key)
|
|
214
|
+
|
|
215
|
+
seen_ids.add(key)
|
|
216
|
+
existing << ref
|
|
217
|
+
end
|
|
218
|
+
lc["references"] = existing
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def strip_revisions(hash = @concept)
|
|
222
|
+
hash.delete("_revisions")
|
|
223
|
+
LANG_CODES.each do |lang|
|
|
224
|
+
next unless hash[lang].is_a?(Hash)
|
|
225
|
+
|
|
226
|
+
hash[lang].delete("_revisions")
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
class << self
|
|
231
|
+
private
|
|
232
|
+
|
|
233
|
+
def find_concepts_dir(source_dir)
|
|
234
|
+
candidates = [
|
|
235
|
+
File.join(source_dir, "concepts"),
|
|
236
|
+
source_dir,
|
|
237
|
+
]
|
|
238
|
+
candidates.find { |dir| Dir.glob(File.join(dir, "*.yaml")).any? }
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def detect_schema_version(source_dir)
|
|
242
|
+
register = V1::Register.from_file(File.join(source_dir,
|
|
243
|
+
"register.yaml"))
|
|
244
|
+
register&.schema_version || "0"
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def load_ref_maps(cross_references_path)
|
|
248
|
+
xref = V1::CrossReferences.from_file(cross_references_path)
|
|
249
|
+
xref ? xref.to_ref_maps : {}
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def read_and_migrate_concepts(concepts_dir, source_version, # rubocop:disable Metrics/MethodLength
|
|
253
|
+
target_version, ref_maps)
|
|
254
|
+
files = Dir.glob(File.join(concepts_dir, "*.yaml"))
|
|
255
|
+
concepts = []
|
|
256
|
+
errors = 0
|
|
257
|
+
|
|
258
|
+
files.each do |file|
|
|
259
|
+
v1 = V1::Concept.from_file(file)
|
|
260
|
+
next unless v1
|
|
261
|
+
|
|
262
|
+
migration = new(
|
|
263
|
+
v1.to_yaml_hash,
|
|
264
|
+
from_version: source_version,
|
|
265
|
+
to_version: target_version,
|
|
266
|
+
ref_maps: ref_maps,
|
|
267
|
+
)
|
|
268
|
+
concepts << migration.migrate
|
|
269
|
+
rescue StandardError => e
|
|
270
|
+
errors += 1
|
|
271
|
+
warn " Error migrating #{File.basename(file)}: #{e.message}" if errors <= 5
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
warn " ... #{errors - 5} more errors" if errors > 5
|
|
275
|
+
concepts
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def read_register_yaml(source_dir, target_version)
|
|
279
|
+
register = V1::Register.from_file(File.join(source_dir,
|
|
280
|
+
"register.yaml"))
|
|
281
|
+
return nil unless register
|
|
282
|
+
|
|
283
|
+
data = register.to_h
|
|
284
|
+
data["schema_version"] = target_version
|
|
285
|
+
data
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def write_output(concepts, register_data, output, dry_run) # rubocop:disable Metrics/MethodLength
|
|
289
|
+
output_path = File.expand_path(output)
|
|
290
|
+
|
|
291
|
+
if File.extname(output).downcase == ".gcr"
|
|
292
|
+
if dry_run
|
|
293
|
+
puts "Would package #{concepts.length} concepts into #{output_path}"
|
|
294
|
+
return
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
v1_concepts = concepts.map { |h| V1::Concept.of_yaml(h).to_managed_concept }
|
|
298
|
+
rd = register_data ? RegisterData.of_yaml(register_data) : nil
|
|
299
|
+
metadata = GcrMetadata.from_concepts(v1_concepts,
|
|
300
|
+
register_data: rd)
|
|
301
|
+
GcrPackage.create(
|
|
302
|
+
concepts: v1_concepts,
|
|
303
|
+
metadata: metadata,
|
|
304
|
+
register_data: rd,
|
|
305
|
+
output_path: output_path,
|
|
306
|
+
)
|
|
307
|
+
else
|
|
308
|
+
if dry_run
|
|
309
|
+
puts "Would write #{concepts.length} concepts to #{File.join(
|
|
310
|
+
output_path, 'concepts/'
|
|
311
|
+
)}"
|
|
312
|
+
return
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
concepts_out = File.join(output_path, "concepts")
|
|
316
|
+
FileUtils.mkdir_p(concepts_out)
|
|
317
|
+
|
|
318
|
+
concepts.each do |concept|
|
|
319
|
+
termid = concept["termid"]
|
|
320
|
+
mc = V1::Concept.of_yaml(concept).to_managed_concept
|
|
321
|
+
File.write(File.join(concepts_out, "#{termid}.yaml"),
|
|
322
|
+
mc.to_yaml)
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
if register_data
|
|
326
|
+
rd = RegisterData.of_yaml(register_data)
|
|
327
|
+
File.write(File.join(output_path, "register.yaml"),
|
|
328
|
+
rd.to_yaml)
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Glossarist
|
|
4
|
+
class UrnResolver
|
|
5
|
+
ELECTROPEDIA_BASE = "https://www.electropedia.org/iev/iev.nsf/display?openform&ievref="
|
|
6
|
+
ISO_OBP_BASE = "https://www.iso.org/obp/ui/#"
|
|
7
|
+
|
|
8
|
+
def initialize
|
|
9
|
+
@schemes = {}
|
|
10
|
+
register_default_schemes
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def resolve(urn_or_reference)
|
|
14
|
+
urn = to_urn(urn_or_reference)
|
|
15
|
+
return nil unless urn
|
|
16
|
+
|
|
17
|
+
_, resolver = @schemes.find { |prefix, _| urn.start_with?(prefix) }
|
|
18
|
+
resolver&.call(urn)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def register_scheme(prefix, &block)
|
|
22
|
+
@schemes[prefix] = block
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
class << self
|
|
26
|
+
def instance
|
|
27
|
+
@instance ||= new
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def resolve(urn_or_reference)
|
|
31
|
+
instance.resolve(urn_or_reference)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def register_default_schemes
|
|
38
|
+
register_scheme("urn:iec:std:iec:60050") do |urn|
|
|
39
|
+
resolve_iec(urn)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
register_scheme("urn:iso:") do |urn|
|
|
43
|
+
resolve_iso(urn)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def resolve_iec(urn)
|
|
48
|
+
code = extract_iec_code(urn)
|
|
49
|
+
return nil unless code
|
|
50
|
+
|
|
51
|
+
"#{ELECTROPEDIA_BASE}#{CGI.escape(code)}"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def extract_iec_code(urn)
|
|
55
|
+
m = urn.match(/#con-([\d-]+)/) || urn.match(/\Aurn:iec:std:iec:60050-([\d-]+)/)
|
|
56
|
+
m&.[](1)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def resolve_iso(urn)
|
|
60
|
+
path = urn.delete_prefix("urn:")
|
|
61
|
+
"#{ISO_OBP_BASE}#{path}"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def to_urn(urn_or_reference)
|
|
65
|
+
case urn_or_reference
|
|
66
|
+
when String then urn_or_reference
|
|
67
|
+
when ConceptReference then urn_or_reference.to_urn
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
data/lib/glossarist/utilities.rb
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
module Glossarist
|
|
4
|
+
module Utilities
|
|
5
|
+
autoload :CommonFunctions, "glossarist/utilities/common_functions"
|
|
6
|
+
autoload :UUID, "glossarist/utilities/uuid"
|
|
7
|
+
end
|
|
8
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Glossarist
|
|
4
|
+
module V1
|
|
5
|
+
class Concept < Lutaml::Model::Serializable
|
|
6
|
+
KNOWN_KEYS = %w[termid term groups references].freeze
|
|
7
|
+
|
|
8
|
+
attribute :termid, :string
|
|
9
|
+
attribute :term, :string
|
|
10
|
+
attribute :groups, :string, collection: true
|
|
11
|
+
attribute :references, :hash, collection: true
|
|
12
|
+
attribute :language_blocks, :hash, default: -> { {} }
|
|
13
|
+
|
|
14
|
+
key_value do
|
|
15
|
+
map :termid, to: :termid
|
|
16
|
+
map :term, to: :term
|
|
17
|
+
map :groups, to: :groups
|
|
18
|
+
map :references, to: :references, render_nil: false
|
|
19
|
+
map nil, to: :language_blocks,
|
|
20
|
+
with: { from: :lang_blocks_from, to: :lang_blocks_to }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.from_file(path)
|
|
24
|
+
return nil unless path && File.exist?(path)
|
|
25
|
+
|
|
26
|
+
concept = from_yaml(File.read(path))
|
|
27
|
+
return nil unless concept&.termid?
|
|
28
|
+
|
|
29
|
+
concept
|
|
30
|
+
rescue Psych::SyntaxError, Lutaml::Model::InvalidFormatError
|
|
31
|
+
nil
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def termid?
|
|
35
|
+
!!termid && !termid.empty?
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def to_managed_concept
|
|
39
|
+
mc = ManagedConcept.new(data: { id: termid })
|
|
40
|
+
|
|
41
|
+
language_blocks.each_value do |data|
|
|
42
|
+
mc.add_localization(LocalizedConcept.of_yaml({ "data" => data }))
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
assign_references(mc) if references.is_a?(Array) && references.any?
|
|
46
|
+
|
|
47
|
+
mc
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def assign_references(concept)
|
|
53
|
+
l10n = concept.localization("eng") || concept.localizations.values.first
|
|
54
|
+
return unless l10n
|
|
55
|
+
|
|
56
|
+
l10n.data.references = references.map do |r|
|
|
57
|
+
ConceptReference.new(r.transform_keys(&:to_sym))
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def lang_blocks_from(model, value)
|
|
62
|
+
blocks = {}
|
|
63
|
+
value.each do |key, v|
|
|
64
|
+
next if KNOWN_KEYS.include?(key)
|
|
65
|
+
next unless v.is_a?(Hash)
|
|
66
|
+
|
|
67
|
+
data = v.dup
|
|
68
|
+
data["language_code"] ||= key
|
|
69
|
+
blocks[key] = data
|
|
70
|
+
end
|
|
71
|
+
model.language_blocks = blocks
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def lang_blocks_to(model, doc)
|
|
75
|
+
model.language_blocks.each do |lang, data|
|
|
76
|
+
doc[lang] = data
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Glossarist
|
|
4
|
+
module V1
|
|
5
|
+
class CrossReferences < Lutaml::Model::Serializable
|
|
6
|
+
attribute :ref_prefix_map, :hash, default: -> { {} }
|
|
7
|
+
attribute :urn_standard_map, :hash, default: -> { {} }
|
|
8
|
+
|
|
9
|
+
yaml do
|
|
10
|
+
map :crossReferences, with: { from: :xref_from_yaml, to: :xref_to_yaml }
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.from_file(path)
|
|
14
|
+
return nil unless path && File.exist?(path)
|
|
15
|
+
|
|
16
|
+
from_yaml(File.read(path))
|
|
17
|
+
rescue Psych::SyntaxError, Lutaml::Model::InvalidFormatError
|
|
18
|
+
nil
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def xref_from_yaml(model, value)
|
|
22
|
+
model.ref_prefix_map = value&.dig("refPrefixMap") || {}
|
|
23
|
+
model.urn_standard_map = value&.dig("urnStandardMap") || {}
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def xref_to_yaml(model, doc)
|
|
27
|
+
doc["crossReferences"] = {
|
|
28
|
+
"refPrefixMap" => model.ref_prefix_map,
|
|
29
|
+
"urnStandardMap" => model.urn_standard_map,
|
|
30
|
+
}
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def to_ref_maps
|
|
34
|
+
{
|
|
35
|
+
ref_prefix_map: ref_prefix_map,
|
|
36
|
+
urn_standard_map: urn_standard_map,
|
|
37
|
+
}
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Glossarist
|
|
4
|
+
module V1
|
|
5
|
+
class Register < Lutaml::Model::Serializable
|
|
6
|
+
attribute :data, :hash, default: -> { {} }
|
|
7
|
+
|
|
8
|
+
key_value do
|
|
9
|
+
map nil, to: :data, with: { from: :data_from, to: :data_to }
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def self.from_file(path)
|
|
13
|
+
return nil unless File.exist?(path)
|
|
14
|
+
|
|
15
|
+
register = from_yaml(File.read(path))
|
|
16
|
+
return nil unless register.data.is_a?(Hash) && !register.data.empty?
|
|
17
|
+
|
|
18
|
+
register
|
|
19
|
+
rescue Psych::SyntaxError, Lutaml::Model::InvalidFormatError
|
|
20
|
+
nil
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def [](key)
|
|
24
|
+
data[key]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def dig(*keys)
|
|
28
|
+
data.dig(*keys)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def schema_version
|
|
32
|
+
data["schema_version"]&.to_s
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def to_h
|
|
36
|
+
data
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def data_from(model, value)
|
|
40
|
+
model.data = value
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def data_to(model, doc)
|
|
44
|
+
model.data.each do |key, value|
|
|
45
|
+
doc[key] = value
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Glossarist
|
|
4
|
+
class ValidationResult
|
|
5
|
+
attr_reader :errors, :warnings
|
|
6
|
+
|
|
7
|
+
def initialize(errors: [], warnings: [])
|
|
8
|
+
@errors = errors
|
|
9
|
+
@warnings = warnings
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def valid?
|
|
13
|
+
@errors.empty?
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def add_error(message)
|
|
17
|
+
@errors << message
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def add_warning(message)
|
|
21
|
+
@warnings << message
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def merge(other)
|
|
25
|
+
other.errors.each { |e| add_error(e) }
|
|
26
|
+
other.warnings.each { |w| add_warning(w) }
|
|
27
|
+
self
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def to_h
|
|
31
|
+
{
|
|
32
|
+
"valid" => valid?,
|
|
33
|
+
"errors" => errors,
|
|
34
|
+
"warnings" => warnings,
|
|
35
|
+
}
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
data/lib/glossarist/version.rb
CHANGED