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.
Files changed (72) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.rubocop_todo.yml +50 -146
  4. data/CLAUDE.md +85 -0
  5. data/Gemfile +26 -5
  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 +3 -2
  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/concept_source_collection.rb +9 -0
  18. data/lib/glossarist/collections/detailed_definition_collection.rb +18 -0
  19. data/lib/glossarist/collections/localization_collection.rb +37 -0
  20. data/lib/glossarist/collections/typed_collection.rb +26 -0
  21. data/lib/glossarist/collections.rb +21 -4
  22. data/lib/glossarist/concept.rb +1 -1
  23. data/lib/glossarist/concept_collector.rb +153 -0
  24. data/lib/glossarist/concept_data.rb +15 -8
  25. data/lib/glossarist/concept_date.rb +1 -1
  26. data/lib/glossarist/concept_document.rb +29 -0
  27. data/lib/glossarist/concept_enricher.rb +34 -0
  28. data/lib/glossarist/concept_manager.rb +31 -49
  29. data/lib/glossarist/concept_reference.rb +45 -0
  30. data/lib/glossarist/concept_source.rb +1 -1
  31. data/lib/glossarist/concept_validator.rb +114 -0
  32. data/lib/glossarist/custom_locality.rb +1 -1
  33. data/lib/glossarist/dataset_validator.rb +69 -0
  34. data/lib/glossarist/designation/abbreviation.rb +1 -1
  35. data/lib/glossarist/designation/base.rb +11 -4
  36. data/lib/glossarist/designation/expression.rb +1 -1
  37. data/lib/glossarist/designation/grammar_info.rb +1 -1
  38. data/lib/glossarist/designation/graphical_symbol.rb +1 -1
  39. data/lib/glossarist/designation/letter_symbol.rb +1 -1
  40. data/lib/glossarist/designation/symbol.rb +2 -2
  41. data/lib/glossarist/designation.rb +8 -11
  42. data/lib/glossarist/detailed_definition.rb +1 -1
  43. data/lib/glossarist/error.rb +2 -5
  44. data/lib/glossarist/gcr_metadata.rb +87 -0
  45. data/lib/glossarist/gcr_package.rb +223 -0
  46. data/lib/glossarist/gcr_statistics.rb +35 -0
  47. data/lib/glossarist/gcr_validator.rb +98 -0
  48. data/lib/glossarist/locality.rb +1 -1
  49. data/lib/glossarist/localized_concept.rb +12 -1
  50. data/lib/glossarist/managed_concept.rb +1 -1
  51. data/lib/glossarist/managed_concept_data.rb +8 -5
  52. data/lib/glossarist/non_verb_rep.rb +1 -1
  53. data/lib/glossarist/reference_extractor.rb +227 -0
  54. data/lib/glossarist/reference_resolver.rb +169 -0
  55. data/lib/glossarist/register_data.rb +39 -0
  56. data/lib/glossarist/related_concept.rb +1 -1
  57. data/lib/glossarist/resolution_adapter/local.rb +73 -0
  58. data/lib/glossarist/resolution_adapter/package.rb +22 -0
  59. data/lib/glossarist/resolution_adapter/remote.rb +60 -0
  60. data/lib/glossarist/resolution_adapter/route.rb +34 -0
  61. data/lib/glossarist/resolution_adapter.rb +14 -0
  62. data/lib/glossarist/schema_migration.rb +334 -0
  63. data/lib/glossarist/urn_resolver.rb +71 -0
  64. data/lib/glossarist/utilities.rb +6 -2
  65. data/lib/glossarist/v1/concept.rb +81 -0
  66. data/lib/glossarist/v1/cross_references.rb +41 -0
  67. data/lib/glossarist/v1/register.rb +50 -0
  68. data/lib/glossarist/v1.rb +9 -0
  69. data/lib/glossarist/validation_result.rb +38 -0
  70. data/lib/glossarist/version.rb +1 -1
  71. data/lib/glossarist.rb +54 -24
  72. metadata +62 -6
@@ -3,7 +3,7 @@ module Glossarist
3
3
  attribute :path, :string
4
4
  attribute :localized_concepts_path, :string
5
5
 
6
- yaml do
6
+ key_value do
7
7
  map :path, to: :path
8
8
  map %i[localized_concepts_path localizedConceptsPath],
9
9
  to: :localized_concepts_path
@@ -33,62 +33,43 @@ module Glossarist
33
33
  end
34
34
  end
35
35
 
36
- def group_concept_hashes(mixed_hashes)
37
- concept_hashes = mixed_hashes.select do |concept_hash|
38
- !concept_hash["data"]["localized_concepts"].nil? ||
39
- !concept_hash["data"]["localizedConcepts"].nil?
36
+ def load_concept_from_file(filename) # rubocop:disable Metrics/CyclomaticComplexity
37
+ raw = File.read(filename, encoding: "utf-8")
38
+ doc = ConceptDocument.from_yamls(raw)
39
+ concept = doc.concept
40
+ unless concept
41
+ raise Glossarist::ParseError.new(filename: filename)
40
42
  end
41
43
 
42
- localized_concept_hashes = mixed_hashes.select do |concept_hash|
43
- concept_hash["data"]["localized_concepts"].nil? &&
44
- concept_hash["data"]["localizedConcepts"].nil?
45
- end
46
-
47
- [concept_hashes, localized_concept_hashes]
48
- end
49
-
50
- def load_concept_from_file(filename)
51
- mixed_hashes = YAML.load_stream(File.read(filename, encoding: "utf-8"))
52
- concepts = []
53
-
54
- concept_hashes, localized_concept_hashes =
55
- group_concept_hashes(mixed_hashes)
56
-
57
- concept_hashes.each do |concept_hash|
58
- concept_hash["uuid"] = concept_hash["id"] ||
59
- File.basename(filename, ".*")
60
- concept = Config.class_for(:managed_concept).of_yaml(concept_hash)
44
+ concept_uuid = concept.identifier || concept.data&.id || File.basename(
45
+ filename, ".*"
46
+ )
47
+ concept.instance_variable_set(:@uuid, concept_uuid)
61
48
 
62
- concept.data.localized_concepts.each_value do |id|
63
- localized_concept =
64
- load_localized_concept(id, localized_concept_hashes)
65
- concept.add_l10n(localized_concept)
66
- end
67
-
68
- concepts << concept
49
+ concept.data.localized_concepts.each_value do |id|
50
+ localized_concept = load_localized_concept(id, doc.localizations)
51
+ concept.add_l10n(localized_concept)
69
52
  end
70
53
 
71
- concepts
54
+ [concept]
72
55
  rescue Psych::SyntaxError => e
73
56
  raise Glossarist::ParseError.new(filename: filename, line: e.line)
74
57
  end
75
58
 
76
- def load_localized_concept(id, localized_concept_hashes = [])
77
- {}
78
-
79
- concept_hash = if localized_concept_hashes.empty?
80
- Psych.safe_load(
81
- File.read(localized_concept_path(id), encoding: "utf-8"),
82
- permitted_classes: [Date, Time],
83
- )
84
- else
85
- localized_concept_hashes.find do |hash|
86
- hash["id"] == id
87
- end
88
- end
89
-
90
- concept_hash["uuid"] = id
91
- Config.class_for(:localized_concept).of_yaml(concept_hash)
59
+ def load_localized_concept(id, inline_localizations = nil)
60
+ if inline_localizations
61
+ l10n = inline_localizations.find { |l| l.id == id }
62
+ if l10n
63
+ l10n.instance_variable_set(:@uuid, id)
64
+ return l10n
65
+ end
66
+ end
67
+
68
+ l10n = LocalizedConcept.from_yaml(
69
+ File.read(localized_concept_path(id), encoding: "utf-8"),
70
+ )
71
+ l10n.instance_variable_set(:@uuid, id)
72
+ l10n
92
73
  rescue Psych::SyntaxError => e
93
74
  raise Glossarist::ParseError.new(filename: filename, line: e.line)
94
75
  end
@@ -107,7 +88,8 @@ module Glossarist
107
88
 
108
89
  concept.localized_concepts.each do |lang, uuid|
109
90
  filename = File.join(localized_concept_dir, "#{uuid}.yaml")
110
- File.write(filename, concept.localization(lang).to_yaml, encoding: "utf-8")
91
+ File.write(filename, concept.localization(lang).to_yaml,
92
+ encoding: "utf-8")
111
93
  end
112
94
  end
113
95
 
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Glossarist
4
+ class ConceptReference < Lutaml::Model::Serializable
5
+ attribute :term, :string
6
+ attribute :concept_id, :string
7
+ attribute :source, :string
8
+ attribute :ref_type, :string
9
+
10
+ key_value do
11
+ map :term, to: :term
12
+ map :concept_id, to: :concept_id
13
+ map :source, to: :source
14
+ map :ref_type, to: :ref_type
15
+ end
16
+
17
+ def local?
18
+ %w[local designation].include?(ref_type) ||
19
+ (ref_type.nil? && (source.nil? || source.empty?))
20
+ end
21
+
22
+ def external?
23
+ !local?
24
+ end
25
+
26
+ def to_urn
27
+ return nil unless external?
28
+ return nil unless source && concept_id
29
+
30
+ case source
31
+ when /\Aurn:iec/ then "#{source}-#{concept_id}"
32
+ when /\Aurn:iso/ then "#{source}:term:#{concept_id}"
33
+ else "#{source}/#{concept_id}"
34
+ end
35
+ end
36
+
37
+ def to_gcr_hash
38
+ h = { "term" => term }
39
+ h["concept_id"] = concept_id if concept_id
40
+ h["source"] = source if source
41
+ h["ref_type"] = ref_type if ref_type
42
+ h.compact
43
+ end
44
+ end
45
+ end
@@ -7,7 +7,7 @@ module Glossarist
7
7
  attribute :origin, Citation
8
8
  attribute :modification, :string
9
9
 
10
- yaml do
10
+ key_value do
11
11
  # TODO: change to `map [:ref, :origin], to: :origin
12
12
  # when multiple key mapping is supported in lutaml-model
13
13
  map :origin, to: :origin
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Glossarist
4
+ class ConceptValidator
5
+ LANG_CODES = Glossarist::LANG_CODES
6
+ VALID_ENTRY_STATUSES = %w[valid superseded withdrawn draft].freeze
7
+
8
+ attr_reader :path, :errors, :warnings
9
+
10
+ def initialize(path)
11
+ @path = path
12
+ @errors = []
13
+ @warnings = []
14
+ end
15
+
16
+ def validate_all
17
+ seen_ids = {}
18
+ file_idx = 0
19
+
20
+ ConceptCollector.each_concept(@path) do |concept|
21
+ fname = concept_file_name(concept, file_idx)
22
+ validate_concept(concept, fname, seen_ids)
23
+ file_idx += 1
24
+ end
25
+
26
+ if file_idx.zero?
27
+ yaml_files = find_yaml_files
28
+ if yaml_files.any?
29
+ @errors << "YAML files found but no parseable concepts"
30
+ end
31
+ end
32
+
33
+ ValidationResult.new(errors: @errors, warnings: @warnings)
34
+ end
35
+
36
+ private
37
+
38
+ def find_yaml_files
39
+ concepts_dir = if File.directory?(File.join(@path, "concepts"))
40
+ File.join(@path, "concepts")
41
+ else
42
+ @path
43
+ end
44
+ Dir.glob(File.join(concepts_dir, "*.yaml"))
45
+ end
46
+
47
+ def concept_file_name(concept, idx)
48
+ id = concept.data&.id
49
+ id ? "concept-#{id}.yaml" : "concept-#{idx}.yaml"
50
+ end
51
+
52
+ def validate_concept(concept, fname, seen_ids)
53
+ validate_id(concept, fname, seen_ids)
54
+ validate_localizations(concept, fname)
55
+ validate_definitions(concept, fname)
56
+ validate_entry_statuses(concept, fname)
57
+ end
58
+
59
+ def validate_id(concept, fname, seen_ids)
60
+ id = concept.data&.id
61
+ unless id
62
+ @errors << "#{fname}: missing concept id"
63
+ return
64
+ end
65
+
66
+ id_str = id.to_s
67
+ if seen_ids[id_str]
68
+ @errors << "#{fname}: duplicate id '#{id_str}' (first seen in #{seen_ids[id_str]})"
69
+ else
70
+ seen_ids[id_str] = fname
71
+ end
72
+ end
73
+
74
+ def validate_localizations(concept, fname)
75
+ l10ns = concept.localizations&.values || []
76
+ if l10ns.empty?
77
+ @errors << "#{fname}: no localizations found"
78
+ return
79
+ end
80
+
81
+ l10ns.each do |l10n|
82
+ lang = l10n.language_code || "unknown"
83
+ terms = l10n.data&.terms
84
+ unless terms.is_a?(Array) && terms.any?
85
+ @errors << "#{fname}/#{lang}: must have at least 1 term"
86
+ end
87
+ end
88
+ end
89
+
90
+ def validate_definitions(concept, fname)
91
+ (concept.localizations&.values || []).each do |l10n|
92
+ lang = l10n.language_code || "unknown"
93
+ next unless l10n.data&.definition
94
+
95
+ defs = l10n.data.definition
96
+ if defs.empty?
97
+ @errors << "#{fname}/#{lang}: definition is empty"
98
+ end
99
+ end
100
+ end
101
+
102
+ def validate_entry_statuses(concept, fname)
103
+ (concept.localizations&.values || []).each do |l10n|
104
+ lang = l10n.language_code || "unknown"
105
+ status = l10n.data&.entry_status
106
+ next unless status
107
+
108
+ unless VALID_ENTRY_STATUSES.include?(status)
109
+ @errors << "#{fname}/#{lang}: invalid entry_status '#{status}' (expected one of: #{VALID_ENTRY_STATUSES.join(', ')})"
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
@@ -8,7 +8,7 @@ module Glossarist
8
8
  # @return [String]
9
9
  attribute :value, :string
10
10
 
11
- yaml do
11
+ key_value do
12
12
  map :name, to: :name
13
13
  map :value, to: :value
14
14
  end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Glossarist
4
+ class DatasetValidator
5
+ def validate(path, strict: false, reference_path: nil)
6
+ result = validate_path(path)
7
+
8
+ if reference_path
9
+ ref_result = validate_cross_references(path, reference_path)
10
+ result.merge(ref_result)
11
+ end
12
+
13
+ result
14
+ end
15
+
16
+ private
17
+
18
+ def validate_path(path)
19
+ if File.extname(path).downcase == ".gcr"
20
+ validate_gcr(path)
21
+ else
22
+ validate_directory(path)
23
+ end
24
+ end
25
+
26
+ def validate_gcr(path)
27
+ GcrValidator.new.validate(path)
28
+ end
29
+
30
+ def validate_directory(path)
31
+ ConceptValidator.new(path).validate_all
32
+ end
33
+
34
+ def validate_cross_references(path, reference_path)
35
+ extractor = ReferenceExtractor.new
36
+ resolver = build_resolver(reference_path)
37
+
38
+ if File.extname(path).downcase == ".gcr"
39
+ validate_gcr_refs(resolver, path, extractor)
40
+ else
41
+ validate_directory_refs(resolver, path, extractor)
42
+ end
43
+ end
44
+
45
+ def build_resolver(reference_path)
46
+ resolver = ReferenceResolver.new
47
+ Dir.glob(File.join(reference_path, "*.gcr")).each do |gcr_path|
48
+ pkg = GcrPackage.load(gcr_path)
49
+ uri_prefix = pkg.metadata&.dig("uri_prefix") || pkg.metadata&.dig("shortname")
50
+ resolver.register_package(pkg, uri_prefix: uri_prefix)
51
+ end
52
+ resolver
53
+ end
54
+
55
+ def validate_gcr_refs(resolver, path, extractor)
56
+ pkg = GcrPackage.load(path)
57
+ uri_prefix = pkg.metadata&.dig("uri_prefix") || pkg.metadata&.dig("shortname")
58
+ resolver.register_self(pkg.concepts)
59
+ resolver.register_package(pkg, uri_prefix: uri_prefix)
60
+ resolver.validate_all(pkg, extractor: extractor)
61
+ end
62
+
63
+ def validate_directory_refs(resolver, path, extractor)
64
+ concepts = ConceptCollector.collect(path)
65
+ resolver.register_self(concepts)
66
+ resolver.validate_all(concepts, extractor: extractor)
67
+ end
68
+ end
69
+ end
@@ -8,7 +8,7 @@ module Glossarist
8
8
  attribute name.to_sym, :boolean
9
9
  end
10
10
 
11
- yaml do
11
+ key_value do
12
12
  map :international, to: :international
13
13
  map :type, to: :type, render_default: true
14
14
  Glossarist::GlossaryDefinition::ABBREVIATION_TYPES.each do |name|
@@ -7,7 +7,7 @@ module Glossarist
7
7
  values: Glossarist::GlossaryDefinition::DESIGNATION_BASE_NORMATIVE_STATUSES
8
8
  attribute :type, :string
9
9
 
10
- yaml do
10
+ key_value do
11
11
  map :type, to: :type
12
12
  map %i[normative_status normativeStatus], to: :normative_status
13
13
  map %i[geographical_area geographicalArea], to: :geographical_area
@@ -18,14 +18,13 @@ module Glossarist
18
18
  type = hash["type"]
19
19
 
20
20
  if type.nil? || /\w/ !~ type
21
- raise ArgumentError, "designation type is missing"
21
+ type = infer_designation_type(hash)
22
+ hash["type"] = type
22
23
  end
23
24
 
24
25
  if self == Base
25
- # called on Base class, delegate it to proper subclass
26
26
  SERIALIZED_TYPES[type].of_yaml(hash)
27
27
  else
28
- # called on subclass, instantiate object
29
28
  unless SERIALIZED_TYPES[self] == type
30
29
  raise ArgumentError, "unexpected designation type: #{type}"
31
30
  end
@@ -33,6 +32,14 @@ module Glossarist
33
32
  super
34
33
  end
35
34
  end
35
+
36
+ def self.infer_designation_type(hash)
37
+ if hash["international"] || hash["abbreviation_type"]
38
+ "symbol"
39
+ else
40
+ "expression"
41
+ end
42
+ end
36
43
  end
37
44
  end
38
45
  end
@@ -13,7 +13,7 @@ module Glossarist
13
13
  attribute :grammar_info, GrammarInfo, collection: true
14
14
  attribute :type, :string, default: -> { "expression" }
15
15
 
16
- yaml do
16
+ key_value do
17
17
  map :type, to: :type, render_default: true
18
18
  map :prefix, to: :prefix
19
19
  map %i[usage_info usageInfo], to: :usage_info
@@ -8,7 +8,7 @@ module Glossarist
8
8
  attribute :part_of_speech, :string,
9
9
  values: Glossarist::GlossaryDefinition::GRAMMAR_INFO_BOOLEAN_ATTRIBUTES
10
10
 
11
- yaml do
11
+ key_value do
12
12
  map :gender, to: :gender
13
13
  map :number, to: :number
14
14
 
@@ -6,7 +6,7 @@ module Glossarist
6
6
  attribute :text, :string
7
7
  attribute :image, :string
8
8
 
9
- yaml do
9
+ key_value do
10
10
  map :text, to: :text
11
11
  map :image, to: :image
12
12
  end
@@ -5,7 +5,7 @@ module Glossarist
5
5
  attribute :language, :string
6
6
  attribute :script, :string
7
7
 
8
- yaml do
8
+ key_value do
9
9
  map :text, to: :text
10
10
  map :language, to: :language
11
11
  map :script, to: :script
@@ -2,9 +2,9 @@ module Glossarist
2
2
  module Designation
3
3
  class Symbol < Base
4
4
  attribute :international, :boolean
5
- attribute :type, :string
5
+ attribute :type, :string, default: -> { "symbol" }
6
6
 
7
- yaml do
7
+ key_value do
8
8
  map :international, to: :international
9
9
  map :type, to: :type, render_default: true
10
10
  end
@@ -1,18 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # (c) Copyright 2021 Ribose Inc.
4
- #
5
-
6
- require_relative "designation/base"
7
- require_relative "designation/expression"
8
- require_relative "designation/abbreviation"
9
- require_relative "designation/grammar_info"
10
- require_relative "designation/symbol"
11
- require_relative "designation/graphical_symbol"
12
- require_relative "designation/letter_symbol"
13
-
14
3
  module Glossarist
15
4
  module Designation
5
+ autoload :Base, "glossarist/designation/base"
6
+ autoload :Expression, "glossarist/designation/expression"
7
+ autoload :Abbreviation, "glossarist/designation/abbreviation"
8
+ autoload :GrammarInfo, "glossarist/designation/grammar_info"
9
+ autoload :Symbol, "glossarist/designation/symbol"
10
+ autoload :GraphicalSymbol, "glossarist/designation/graphical_symbol"
11
+ autoload :LetterSymbol, "glossarist/designation/letter_symbol"
12
+
16
13
  # Bi-directional class-to-string mapping for STI-like serialization.
17
14
  SERIALIZED_TYPES = {
18
15
  Expression => "expression",
@@ -5,7 +5,7 @@ module Glossarist
5
5
  attribute :content, :string
6
6
  attribute :sources, ConceptSource, collection: true
7
7
 
8
- yaml do
8
+ key_value do
9
9
  map :content, to: :content
10
10
  map :sources, to: :sources
11
11
  end
@@ -1,9 +1,6 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Glossarist
2
4
  class Error < StandardError
3
5
  end
4
6
  end
5
-
6
- require_relative "error/invalid_type_error"
7
- require_relative "error/invalid_language_code_error"
8
- require_relative "error/parse_error"
9
- require_relative "error/cache_version_mismatch_error"
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Glossarist
4
+ class GcrMetadata < Lutaml::Model::Serializable
5
+ attribute :shortname, :string
6
+ attribute :version, :string
7
+ attribute :title, :string
8
+ attribute :description, :string
9
+ attribute :owner, :string
10
+ attribute :tags, :string, collection: true
11
+ attribute :concept_count, :integer
12
+ attribute :languages, :string, collection: true
13
+ attribute :created_at, :string
14
+ attribute :glossarist_version, :string
15
+ attribute :schema_version, :string, default: -> { "1.0.0" }
16
+ attribute :statistics, GcrStatistics
17
+ attribute :homepage, :string
18
+ attribute :repository, :string
19
+ attribute :license, :string
20
+ attribute :uri_prefix, :string
21
+ attribute :concept_uri_template, :string
22
+ attribute :external_references, :hash, collection: true
23
+
24
+ key_value do
25
+ map :shortname, to: :shortname
26
+ map :version, to: :version
27
+ map :title, to: :title
28
+ map :description, to: :description
29
+ map :owner, to: :owner
30
+ map :tags, to: :tags
31
+ map :concept_count, to: :concept_count
32
+ map :languages, to: :languages
33
+ map :created_at, to: :created_at
34
+ map :glossarist_version, to: :glossarist_version
35
+ map :schema_version, to: :schema_version
36
+ map :statistics, to: :statistics
37
+ map :homepage, to: :homepage
38
+ map :repository, to: :repository
39
+ map :license, to: :license
40
+ map :uri_prefix, to: :uri_prefix
41
+ map :concept_uri_template, to: :concept_uri_template
42
+ map :external_references, to: :external_references
43
+ end
44
+
45
+ def self.from_concepts(concepts, register_data: nil, options: {})
46
+ stats = GcrStatistics.from_concepts(concepts)
47
+ new(
48
+ shortname: options[:shortname] || register_data&.dig("shortname") || register_data&.dig("id"),
49
+ version: options[:version] || register_data&.dig("version"),
50
+ title: options[:title] || register_data&.dig("name"),
51
+ description: options[:description] || register_data&.dig("description"),
52
+ owner: options[:owner],
53
+ tags: options[:tags] || [],
54
+ concept_count: concepts.length,
55
+ languages: stats.languages,
56
+ created_at: Time.now.utc.iso8601,
57
+ glossarist_version: Glossarist::VERSION,
58
+ schema_version: register_data&.dig("schema_version") || SchemaMigration::CURRENT_SCHEMA_VERSION,
59
+ statistics: stats,
60
+ uri_prefix: options[:uri_prefix],
61
+ concept_uri_template: options[:concept_uri_template],
62
+ external_references: derive_external_references(concepts),
63
+ )
64
+ end
65
+
66
+ def self.derive_external_references(concepts)
67
+ sources = Set.new
68
+ concepts.each do |concept|
69
+ concept.localizations.each do |l10n|
70
+ l10n.data.references&.each do |ref|
71
+ src = ref.source
72
+ sources.add(src) if src && !src.empty?
73
+ end
74
+ end
75
+ end
76
+ sources.map { |uri| { "uri" => uri } }
77
+ end
78
+
79
+ def [](key)
80
+ to_yaml_hash[key]
81
+ end
82
+
83
+ def dig(*keys)
84
+ to_yaml_hash.dig(*keys)
85
+ end
86
+ end
87
+ end