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,101 @@
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_entry_statuses(concept, fname)
56
+ end
57
+
58
+ def validate_id(concept, fname, seen_ids)
59
+ id = concept.data&.id
60
+ unless id
61
+ @errors << "#{fname}: missing concept id"
62
+ return
63
+ end
64
+
65
+ id_str = id.to_s
66
+ if seen_ids[id_str]
67
+ @errors << "#{fname}: duplicate id '#{id_str}' (first seen in #{seen_ids[id_str]})"
68
+ else
69
+ seen_ids[id_str] = fname
70
+ end
71
+ end
72
+
73
+ def validate_localizations(concept, fname)
74
+ l10ns = concept.localizations&.values || []
75
+ if l10ns.empty?
76
+ @errors << "#{fname}: no localizations found"
77
+ return
78
+ end
79
+
80
+ l10ns.each do |l10n|
81
+ lang = l10n.language_code || "unknown"
82
+ terms = l10n.data&.terms
83
+ unless terms.is_a?(Array) && terms.any?
84
+ @errors << "#{fname}/#{lang}: must have at least 1 term"
85
+ end
86
+ end
87
+ end
88
+
89
+ def validate_entry_statuses(concept, fname)
90
+ (concept.localizations&.values || []).each do |l10n|
91
+ lang = l10n.language_code || "unknown"
92
+ status = l10n.data&.entry_status
93
+ next unless status
94
+
95
+ unless VALID_ENTRY_STATUSES.include?(status)
96
+ @errors << "#{fname}/#{lang}: invalid entry_status '#{status}' (expected one of: #{VALID_ENTRY_STATUSES.join(', ')})"
97
+ end
98
+ end
99
+ end
100
+ end
101
+ 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
@@ -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
@@ -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
@@ -0,0 +1,223 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "zip"
4
+ require "fileutils"
5
+
6
+ module Glossarist
7
+ class GcrPackage
8
+ attr_reader :zip_path, :metadata, :concepts
9
+
10
+ def initialize(zip_path)
11
+ @zip_path = zip_path
12
+ @metadata = nil
13
+ @concepts = []
14
+ end
15
+
16
+ def self.create(concepts:, metadata:, output_path:, register_data: nil)
17
+ FileUtils.mkdir_p(File.dirname(output_path))
18
+ package = new(output_path)
19
+ package.send(:write, concepts, metadata, register_data)
20
+ package
21
+ end
22
+
23
+ def self.load(zip_path)
24
+ package = new(zip_path)
25
+ package.send(:read)
26
+ package
27
+ end
28
+
29
+ def self.create_from_directory(dir, output:, shortname:, version:, # rubocop:disable Metrics/MethodLength, Metrics/ParameterLists
30
+ title: nil, description: nil, owner: nil,
31
+ tags: [], register_yaml: nil,
32
+ uri_prefix: nil, concept_uri_template: nil,
33
+ streaming: false)
34
+ dir = File.expand_path(dir)
35
+
36
+ if streaming
37
+ create_streaming(dir, output: output, shortname: shortname, version: version,
38
+ title: title, description: description, owner: owner,
39
+ tags: tags, register_yaml: register_yaml,
40
+ uri_prefix: uri_prefix,
41
+ concept_uri_template: concept_uri_template)
42
+ else
43
+ create_batch(dir, output: output, shortname: shortname, version: version,
44
+ title: title, description: description, owner: owner,
45
+ tags: tags, register_yaml: register_yaml,
46
+ uri_prefix: uri_prefix,
47
+ concept_uri_template: concept_uri_template)
48
+ end
49
+ end
50
+
51
+ def validate
52
+ GcrValidator.new.validate(@zip_path)
53
+ end
54
+
55
+ private
56
+
57
+ def write(concepts, metadata, register_data)
58
+ Zip::File.open(@zip_path, create: true) do |zf|
59
+ zf.get_output_stream("metadata.yaml") do |f|
60
+ f.write(metadata.to_yaml)
61
+ end
62
+
63
+ if register_data
64
+ zf.get_output_stream("register.yaml") do |f|
65
+ f.write(register_data.to_yaml)
66
+ end
67
+ end
68
+
69
+ concepts.each do |mc|
70
+ write_concept(zf, mc)
71
+ end
72
+ end
73
+ end
74
+
75
+ def write_concept(zip_file, concept)
76
+ termid = concept.data.id.to_s
77
+ doc = ConceptDocument.from_managed_concept(concept)
78
+ zip_file.get_output_stream("concepts/#{termid}.yaml") do |f|
79
+ f.write(doc.to_yamls)
80
+ end
81
+ end
82
+
83
+ def read
84
+ @concepts = []
85
+
86
+ Zip::File.open(@zip_path) do |zf|
87
+ if (entry = zf.find_entry("metadata.yaml"))
88
+ @metadata = GcrMetadata.from_yaml(entry.get_input_stream.read)
89
+ end
90
+
91
+ zf.entries.each do |entry|
92
+ next unless entry.name.start_with?("concepts/") && entry.name.end_with?(".yaml")
93
+
94
+ raw = entry.get_input_stream.read
95
+ doc = ConceptDocument.from_yamls(raw)
96
+ @concepts << doc.to_managed_concept
97
+ end
98
+ end
99
+ end
100
+
101
+ class << self
102
+ private
103
+
104
+ def create_batch(dir, output:, shortname:, version:, **opts)
105
+ concepts = ConceptCollector.collect(dir)
106
+ if concepts.empty?
107
+ raise ArgumentError,
108
+ "No concept files found in #{dir}"
109
+ end
110
+
111
+ enricher = ConceptEnricher.new
112
+ enricher.inject_references(concepts)
113
+ if opts[:concept_uri_template]
114
+ enricher.apply_uri_template(concepts,
115
+ opts[:concept_uri_template])
116
+ end
117
+
118
+ register_data = load_register_data(opts[:register_yaml], dir)
119
+ metadata = build_metadata(concepts, shortname: shortname, version: version,
120
+ register_data: register_data, **opts)
121
+
122
+ create(
123
+ concepts: concepts,
124
+ metadata: metadata,
125
+ register_data: register_data,
126
+ output_path: File.expand_path(output),
127
+ )
128
+ end
129
+
130
+ def create_streaming(dir, output:, shortname:, version:, **opts) # rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/BlockLength
131
+ enricher = ConceptEnricher.new
132
+ output_path = File.expand_path(output)
133
+ FileUtils.mkdir_p(File.dirname(output_path))
134
+
135
+ register_data = load_register_data(opts[:register_yaml], dir)
136
+ concept_count = 0
137
+ languages = Set.new
138
+
139
+ Zip::OutputStream.open(output_path) do |zos|
140
+ if register_data
141
+ zos.put_next_entry("register.yaml")
142
+ zos.write(register_data.to_yaml)
143
+ end
144
+
145
+ ConceptCollector.each_concept(dir) do |mc|
146
+ enricher.inject_references([mc])
147
+ if opts[:concept_uri_template]
148
+ enricher.apply_uri_template([mc],
149
+ opts[:concept_uri_template])
150
+ end
151
+
152
+ mc.localizations.each do |l10n|
153
+ languages << l10n.language_code if l10n.language_code
154
+ end
155
+ concept_count += 1
156
+
157
+ termid = mc.data.id.to_s
158
+ doc = ConceptDocument.from_managed_concept(mc)
159
+ zos.put_next_entry("concepts/#{termid}.yaml")
160
+ zos.write(doc.to_yamls)
161
+ end
162
+
163
+ if concept_count.zero?
164
+ raise ArgumentError,
165
+ "No concept files found in #{dir}"
166
+ end
167
+
168
+ metadata = build_streaming_metadata(concept_count, languages,
169
+ shortname: shortname, version: version,
170
+ register_data: register_data, **opts)
171
+ zos.put_next_entry("metadata.yaml")
172
+ zos.write(metadata.to_yaml)
173
+ end
174
+
175
+ new(output_path)
176
+ end
177
+
178
+ def build_streaming_metadata(concept_count, languages, shortname:, version:, # rubocop:disable Metrics/ParameterLists
179
+ register_data: nil, **opts)
180
+ GcrMetadata.new(
181
+ shortname: shortname,
182
+ version: version,
183
+ title: opts[:title],
184
+ description: opts[:description],
185
+ owner: opts[:owner],
186
+ tags: opts[:tags] || [],
187
+ concept_count: concept_count,
188
+ languages: languages.sort,
189
+ created_at: Time.now.utc.iso8601,
190
+ glossarist_version: Glossarist::VERSION,
191
+ schema_version: register_data&.dig("schema_version") || SchemaMigration::CURRENT_SCHEMA_VERSION,
192
+ uri_prefix: opts[:uri_prefix],
193
+ concept_uri_template: opts[:concept_uri_template],
194
+ )
195
+ end
196
+
197
+ def build_metadata(concepts, shortname:, version:, register_data: nil,
198
+ **opts)
199
+ GcrMetadata.from_concepts(
200
+ concepts,
201
+ register_data: register_data,
202
+ options: {
203
+ shortname: shortname,
204
+ version: version,
205
+ title: opts[:title],
206
+ description: opts[:description],
207
+ owner: opts[:owner],
208
+ tags: opts[:tags],
209
+ uri_prefix: opts[:uri_prefix],
210
+ concept_uri_template: opts[:concept_uri_template],
211
+ },
212
+ )
213
+ end
214
+
215
+ def load_register_data(register_yaml_path, dir)
216
+ path = register_yaml_path || File.join(dir, "register.yaml")
217
+ return nil unless File.exist?(path)
218
+
219
+ RegisterData.from_file(path)
220
+ end
221
+ end
222
+ end
223
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Glossarist
4
+ class GcrStatistics < Lutaml::Model::Serializable
5
+ attribute :total_concepts, :integer
6
+ attribute :languages, :string, collection: true
7
+ attribute :concepts_by_status, :hash
8
+ attribute :concepts_with_definitions, :integer
9
+ attribute :concepts_with_sources, :integer
10
+
11
+ key_value do
12
+ map :total_concepts, to: :total_concepts
13
+ map :languages, to: :languages
14
+ map :concepts_by_status, to: :concepts_by_status
15
+ map :concepts_with_definitions, to: :concepts_with_definitions
16
+ map :concepts_with_sources, to: :concepts_with_sources
17
+ end
18
+
19
+ def self.from_concepts(concepts)
20
+ l10ns = concepts.flat_map { |c| c.localizations.to_a }
21
+
22
+ new(
23
+ total_concepts: concepts.length,
24
+ languages: l10ns.map(&:language_code).compact.sort.uniq,
25
+ concepts_by_status: l10ns.map(&:entry_status).compact.tally,
26
+ concepts_with_definitions: count_with(l10ns, :definition),
27
+ concepts_with_sources: count_with(l10ns, :sources),
28
+ )
29
+ end
30
+
31
+ def self.count_with(l10ns, attr)
32
+ l10ns.count { |l| l.data.send(attr)&.any? }
33
+ end
34
+ end
35
+ end