glossarist 2.5.0 → 2.5.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.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.rubocop_todo.yml +50 -146
  4. data/CLAUDE.md +33 -7
  5. data/Gemfile +20 -19
  6. data/README.adoc +383 -7
  7. data/TODO.integration/01-gcr-package-cli.md +180 -0
  8. data/exe/glossarist +1 -53
  9. data/glossarist.gemspec +1 -0
  10. data/lib/glossarist/asset.rb +1 -1
  11. data/lib/glossarist/citation.rb +1 -1
  12. data/lib/glossarist/cli/package_command.rb +32 -0
  13. data/lib/glossarist/cli/upgrade_command.rb +34 -0
  14. data/lib/glossarist/cli/validate_command.rb +56 -0
  15. data/lib/glossarist/cli.rb +105 -0
  16. data/lib/glossarist/collection_config.rb +23 -0
  17. data/lib/glossarist/collections.rb +15 -8
  18. data/lib/glossarist/concept.rb +1 -1
  19. data/lib/glossarist/concept_collector.rb +153 -0
  20. data/lib/glossarist/concept_data.rb +3 -1
  21. data/lib/glossarist/concept_date.rb +1 -1
  22. data/lib/glossarist/concept_document.rb +29 -0
  23. data/lib/glossarist/concept_enricher.rb +34 -0
  24. data/lib/glossarist/concept_manager.rb +31 -49
  25. data/lib/glossarist/concept_reference.rb +45 -0
  26. data/lib/glossarist/concept_source.rb +1 -1
  27. data/lib/glossarist/concept_validator.rb +101 -0
  28. data/lib/glossarist/custom_locality.rb +1 -1
  29. data/lib/glossarist/dataset_validator.rb +69 -0
  30. data/lib/glossarist/designation/abbreviation.rb +1 -1
  31. data/lib/glossarist/designation/base.rb +11 -4
  32. data/lib/glossarist/designation/expression.rb +1 -1
  33. data/lib/glossarist/designation/grammar_info.rb +1 -1
  34. data/lib/glossarist/designation/graphical_symbol.rb +1 -1
  35. data/lib/glossarist/designation/letter_symbol.rb +1 -1
  36. data/lib/glossarist/designation/symbol.rb +2 -2
  37. data/lib/glossarist/detailed_definition.rb +1 -1
  38. data/lib/glossarist/gcr_metadata.rb +87 -0
  39. data/lib/glossarist/gcr_package.rb +223 -0
  40. data/lib/glossarist/gcr_statistics.rb +35 -0
  41. data/lib/glossarist/gcr_validator.rb +98 -0
  42. data/lib/glossarist/locality.rb +1 -1
  43. data/lib/glossarist/localized_concept.rb +12 -1
  44. data/lib/glossarist/managed_concept.rb +1 -1
  45. data/lib/glossarist/managed_concept_data.rb +5 -2
  46. data/lib/glossarist/non_verb_rep.rb +1 -1
  47. data/lib/glossarist/reference_extractor.rb +227 -0
  48. data/lib/glossarist/reference_resolver.rb +169 -0
  49. data/lib/glossarist/register_data.rb +39 -0
  50. data/lib/glossarist/related_concept.rb +1 -1
  51. data/lib/glossarist/resolution_adapter/local.rb +73 -0
  52. data/lib/glossarist/resolution_adapter/package.rb +22 -0
  53. data/lib/glossarist/resolution_adapter/remote.rb +60 -0
  54. data/lib/glossarist/resolution_adapter/route.rb +34 -0
  55. data/lib/glossarist/resolution_adapter.rb +14 -0
  56. data/lib/glossarist/schema_migration.rb +334 -0
  57. data/lib/glossarist/urn_resolver.rb +71 -0
  58. data/lib/glossarist/v1/concept.rb +81 -0
  59. data/lib/glossarist/v1/cross_references.rb +41 -0
  60. data/lib/glossarist/v1/register.rb +50 -0
  61. data/lib/glossarist/v1.rb +9 -0
  62. data/lib/glossarist/validation_result.rb +38 -0
  63. data/lib/glossarist/version.rb +1 -1
  64. data/lib/glossarist.rb +29 -4
  65. data/relaton-bib-2.0.0.gem +0 -0
  66. data/relaton-bib-2.1.0.gem +0 -0
  67. data/relaton-cen-2.0.0.gem +0 -0
  68. data/relaton-iec-2.0.0.gem +0 -0
  69. data/relaton-iso-2.0.0.gem +0 -0
  70. data/relaton-itu-2.0.0.gem +0 -0
  71. metadata +60 -7
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Glossarist
4
+ class ResolutionAdapter
5
+ class Package < ResolutionAdapter
6
+ attr_reader :uri_prefix, :local_adapter
7
+
8
+ def initialize(concepts, uri_prefix:)
9
+ super()
10
+ @uri_prefix = uri_prefix
11
+ @local_adapter = Local.new(concepts)
12
+ end
13
+
14
+ def resolve(reference)
15
+ return nil unless reference.ref_type == "urn"
16
+ return nil unless reference.source == uri_prefix
17
+
18
+ @local_adapter.resolve_by_id(reference.concept_id)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "json"
5
+
6
+ module Glossarist
7
+ class ResolutionAdapter
8
+ class Remote < ResolutionAdapter
9
+ attr_reader :uri_prefix, :endpoint, :cache
10
+
11
+ def initialize(uri_prefix:, endpoint:)
12
+ super()
13
+ @uri_prefix = uri_prefix
14
+ @endpoint = endpoint.chomp("/")
15
+ @cache = {}
16
+ end
17
+
18
+ def resolve(reference)
19
+ return nil unless reference.ref_type == "urn"
20
+ return nil unless reference.source == uri_prefix
21
+
22
+ key = cache_key(reference)
23
+ return @cache[key] if @cache.key?(key)
24
+
25
+ @cache[key] = fetch(reference)
26
+ end
27
+
28
+ private
29
+
30
+ def build_uri(reference)
31
+ URI.parse("#{endpoint}/#{URI.encode_www_form_component(reference.source)}/#{URI.encode_www_form_component(reference.concept_id)}")
32
+ end
33
+
34
+ def parse_response(response)
35
+ content_type = response["content-type"].to_s
36
+ if content_type.include?("json")
37
+ JSON.parse(response.body)
38
+ elsif content_type.include?("yaml")
39
+ ConceptDocument.from_yamls(response.body).to_managed_concept
40
+ else
41
+ ManagedConcept.from_yaml(response.body)
42
+ end
43
+ end
44
+
45
+ def cache_key(reference)
46
+ "#{reference.source}/#{reference.concept_id}"
47
+ end
48
+
49
+ def fetch(reference)
50
+ uri = build_uri(reference)
51
+ response = Net::HTTP.get_response(uri)
52
+ return nil unless response.is_a?(Net::HTTPSuccess)
53
+
54
+ parse_response(response)
55
+ rescue StandardError
56
+ nil
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Glossarist
4
+ class ResolutionAdapter
5
+ class Route < ResolutionAdapter
6
+ attr_reader :routes
7
+
8
+ def initialize(routes = {})
9
+ super()
10
+ @routes = routes
11
+ end
12
+
13
+ def add(from:, to:)
14
+ @routes[from] = to
15
+ end
16
+
17
+ def resolve(reference)
18
+ return nil unless reference.ref_type == "urn"
19
+ return nil unless routes.key?(reference.source)
20
+
21
+ ConceptReference.new(
22
+ term: reference.term,
23
+ concept_id: reference.concept_id,
24
+ source: routes[reference.source],
25
+ ref_type: reference.ref_type,
26
+ )
27
+ end
28
+
29
+ def remap(source)
30
+ routes[source] || source
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Glossarist
4
+ class ResolutionAdapter
5
+ autoload :Local, "glossarist/resolution_adapter/local"
6
+ autoload :Package, "glossarist/resolution_adapter/package"
7
+ autoload :Route, "glossarist/resolution_adapter/route"
8
+ autoload :Remote, "glossarist/resolution_adapter/remote"
9
+
10
+ def resolve(_reference)
11
+ raise NotImplementedError, "#{self.class}#resolve must be implemented"
12
+ end
13
+ end
14
+ end
@@ -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
@@ -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