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.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.rubocop_todo.yml +50 -146
- data/CLAUDE.md +33 -7
- data/Gemfile +20 -19
- data/README.adoc +383 -7
- data/TODO.integration/01-gcr-package-cli.md +180 -0
- data/exe/glossarist +1 -53
- data/glossarist.gemspec +1 -0
- 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.rb +15 -8
- data/lib/glossarist/concept.rb +1 -1
- data/lib/glossarist/concept_collector.rb +153 -0
- data/lib/glossarist/concept_data.rb +3 -1
- 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 +101 -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/detailed_definition.rb +1 -1
- 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 +5 -2
- 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/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 +29 -4
- data/relaton-bib-2.0.0.gem +0 -0
- data/relaton-bib-2.1.0.gem +0 -0
- data/relaton-cen-2.0.0.gem +0 -0
- data/relaton-iec-2.0.0.gem +0 -0
- data/relaton-iso-2.0.0.gem +0 -0
- data/relaton-itu-2.0.0.gem +0 -0
- 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
|