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
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Glossarist
4
+ class CLI
5
+ class UpgradeCommand
6
+ def initialize(source_dir, options)
7
+ @source_dir = source_dir
8
+ @options = options
9
+ end
10
+
11
+ def run
12
+ result = SchemaMigration.upgrade_directory(
13
+ @source_dir,
14
+ output: @options[:output],
15
+ target_version: @options[:target_version],
16
+ cross_references: @options[:cross_references],
17
+ dry_run: @options[:dry_run],
18
+ )
19
+ report(result)
20
+ rescue ArgumentError => e
21
+ warn "Error: #{e.message}"
22
+ exit 1
23
+ end
24
+
25
+ private
26
+
27
+ def report(result)
28
+ puts "Upgraded #{result[:count]} concepts " \
29
+ "from schema v#{result[:source_version]} to v#{result[:target_version]}"
30
+ puts "Output: #{result[:output]}"
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Glossarist
4
+ class CLI
5
+ class ValidateCommand
6
+ def initialize(path, options)
7
+ @path = path
8
+ @options = options
9
+ end
10
+
11
+ def run
12
+ result = DatasetValidator.new.validate(
13
+ @path,
14
+ strict: @options[:strict],
15
+ reference_path: @options[:reference_path],
16
+ )
17
+ report(result)
18
+ exit_code = result.errors.any? || (@options[:strict] && result.warnings.any?) ? 1 : 0
19
+ exit(exit_code) unless exit_code.zero?
20
+ end
21
+
22
+ private
23
+
24
+ def report(result)
25
+ case @options[:format]
26
+ when "json"
27
+ require "json"
28
+ puts JSON.pretty_generate(result.to_h)
29
+ when "yaml"
30
+ require "yaml"
31
+ puts YAML.dump(result.to_h)
32
+ else
33
+ report_text(result)
34
+ end
35
+ end
36
+
37
+ def report_text(result)
38
+ if result.valid?
39
+ puts "Valid."
40
+ else
41
+ puts "Invalid."
42
+ result.errors.each { |e| puts " ERROR: #{e}" }
43
+ end
44
+
45
+ if result.warnings.any?
46
+ result.warnings.each do |w|
47
+ puts " WARNING: #{w}"
48
+ end
49
+ end
50
+
51
+ total = result.errors.length + result.warnings.length
52
+ puts "#{total} issue(s) found."
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor"
4
+
5
+ module Glossarist
6
+ class CLI < Thor
7
+ desc "generate_latex", "Convert Concepts to Latex format"
8
+
9
+ option :concepts_path, aliases: :p, required: true,
10
+ desc: "Path to yaml concepts directory"
11
+ option :latex_concepts, aliases: :l,
12
+ desc: "File path having list of concepts that should be converted to LATEX format. If not provided all the concepts will be converted to the latex format"
13
+ option :output_file, aliases: :o,
14
+ desc: "Output file path. By default the output will pe printed to the console"
15
+ option :extra_attributes, aliases: :e, type: :array,
16
+ desc: "List of extra attributes that are not in standard Glossarist Concept model"
17
+ def generate_latex
18
+ assets = []
19
+
20
+ if options[:extra_attributes]
21
+ Glossarist.configure do |config|
22
+ config.register_extension_attributes(options[:extra_attributes])
23
+ end
24
+ end
25
+
26
+ concept_set = Glossarist::ConceptSet.new(options[:concepts_path], assets)
27
+ latex_str = concept_set.to_latex(options[:latex_concepts])
28
+ output_latex(latex_str)
29
+ end
30
+
31
+ desc "upgrade SOURCE_DIR", "Upgrade dataset to current schema version"
32
+ option :output, aliases: :o, required: true,
33
+ desc: "Output directory or .gcr file path"
34
+ option :target_version, type: :string, default: Glossarist::SchemaMigration::CURRENT_SCHEMA_VERSION,
35
+ desc: "Target schema version (default: current)"
36
+ option :cross_references, type: :string,
37
+ desc: "Path to datasets.yml for cross-reference maps"
38
+ option :dry_run, type: :boolean, default: false,
39
+ desc: "Show what would change without writing"
40
+ def upgrade(source_dir)
41
+ require_relative "cli/upgrade_command"
42
+ Glossarist::CLI::UpgradeCommand.new(source_dir, options).run
43
+ end
44
+
45
+ desc "package DIR", "Create a .gcr ZIP archive from a schema v1 dataset"
46
+ option :output, aliases: :o, required: true,
47
+ desc: "Output .gcr file path"
48
+ option :shortname, type: :string, required: true,
49
+ desc: "Machine-readable dataset ID"
50
+ option :version, type: :string, required: true,
51
+ desc: "Semantic version (e.g. 1.0.0)"
52
+ option :uri_prefix, type: :string,
53
+ desc: "URI namespace prefix (e.g. urn:iec:std:iec:60050)"
54
+ option :title, type: :string, desc: "Dataset title"
55
+ option :description, type: :string, desc: "Dataset description"
56
+ option :owner, type: :string, desc: "Dataset owner"
57
+ option :register_yaml, type: :string,
58
+ desc: "Path to register.yaml to include in package"
59
+ option :tags, type: :array, desc: "Tags for the dataset"
60
+ def package(dir)
61
+ require_relative "cli/package_command"
62
+ Glossarist::CLI::PackageCommand.new(dir, options).run
63
+ end
64
+
65
+ desc "validate PATH",
66
+ "Validate dataset directory or .gcr for schema compliance"
67
+ option :strict, type: :boolean, default: false,
68
+ desc: "Treat warnings as errors"
69
+ option :format, type: :string, default: "text",
70
+ enum: %w[text json yaml],
71
+ desc: "Output format for validation results"
72
+ option :reference_path, type: :string,
73
+ desc: "Path to directory of .gcr files for cross-dataset reference validation"
74
+ def validate(path)
75
+ require_relative "cli/validate_command"
76
+ Glossarist::CLI::ValidateCommand.new(path, options).run
77
+ end
78
+
79
+ def method_missing(*args)
80
+ warn "No method found named: #{args[0]}"
81
+ warn "Run with `--help` or `-h` to see available options"
82
+ exit 1
83
+ end
84
+
85
+ def respond_to_missing?
86
+ true
87
+ end
88
+
89
+ def self.exit_on_failure?
90
+ true
91
+ end
92
+
93
+ private
94
+
95
+ def output_latex(latex_str)
96
+ output_file_path = options[:output_file]
97
+
98
+ if output_file_path
99
+ File.open(output_file_path, "w") { |file| file.puts latex_str }
100
+ else
101
+ puts latex_str
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Glossarist
4
+ class CollectionConfig < Lutaml::Model::Serializable
5
+ attribute :packages, :hash, collection: true, default: -> { [] }
6
+ attribute :routes, :hash, collection: true, default: -> { [] }
7
+ attribute :remotes, :hash, collection: true, default: -> { [] }
8
+
9
+ yaml do
10
+ map :packages, to: :packages
11
+ map :routes, to: :routes
12
+ map :remote, to: :remotes
13
+ end
14
+
15
+ def self.from_file(path)
16
+ return nil unless File.exist?(path)
17
+
18
+ from_yaml(File.read(path))
19
+ rescue Psych::SyntaxError, Lutaml::Model::InvalidFormatError
20
+ nil
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Glossarist
4
+ module Collections
5
+ class ConceptSourceCollection < TypedCollection
6
+ instances :sources, ConceptSource
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Glossarist
4
+ module Collections
5
+ class DetailedDefinitionCollection < TypedCollection
6
+ instances :definitions, DetailedDefinition
7
+
8
+ private
9
+
10
+ def coerce_other(item)
11
+ case item
12
+ when String then DetailedDefinition.new(content: item)
13
+ else item
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Glossarist
4
+ module Collections
5
+ class LocalizationCollection < Lutaml::Model::Collection
6
+ instances :localized_concepts, LocalizedConcept
7
+
8
+ index_by :language_code
9
+
10
+ def [](lang_code)
11
+ find_by(:language_code, lang_code.to_s)
12
+ end
13
+
14
+ def store(lang_code, localized_concept)
15
+ localized_concept.language_code ||= lang_code.to_s
16
+ push(localized_concept)
17
+ localized_concept
18
+ end
19
+
20
+ def keys
21
+ map(&:language_code)
22
+ end
23
+
24
+ def values
25
+ to_a
26
+ end
27
+
28
+ def each_key(&block)
29
+ keys.each(&block)
30
+ end
31
+
32
+ def each_value(&block)
33
+ values.each(&block)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Glossarist
4
+ module Collections
5
+ class TypedCollection < Lutaml::Model::Collection
6
+ def <<(item)
7
+ push(coerce(item))
8
+ end
9
+
10
+ private
11
+
12
+ def coerce(item)
13
+ return item if item.is_a?(self.class.instance_type)
14
+
15
+ case item
16
+ when Hash then self.class.instance_type.new(item)
17
+ else coerce_other(item)
18
+ end
19
+ end
20
+
21
+ def coerce_other(item)
22
+ item
23
+ end
24
+ end
25
+ end
26
+ end
@@ -1,4 +1,21 @@
1
- require_relative "collections/asset_collection"
2
- require_relative "collections/bibliography_collection"
3
- require_relative "collections/collection"
4
- require_relative "collections/designation_collection"
1
+ # frozen_string_literal: true
2
+
3
+ module Glossarist
4
+ module Collections
5
+ autoload :AssetCollection,
6
+ "glossarist/collections/asset_collection"
7
+ autoload :BibliographyCollection,
8
+ "glossarist/collections/bibliography_collection"
9
+ autoload :Collection, "glossarist/collections/collection"
10
+ autoload :ConceptSourceCollection,
11
+ "glossarist/collections/concept_source_collection"
12
+ autoload :DesignationCollection,
13
+ "glossarist/collections/designation_collection"
14
+ autoload :DetailedDefinitionCollection,
15
+ "glossarist/collections/detailed_definition_collection"
16
+ autoload :LocalizationCollection,
17
+ "glossarist/collections/localization_collection"
18
+ autoload :TypedCollection,
19
+ "glossarist/collections/typed_collection"
20
+ end
21
+ end
@@ -11,7 +11,7 @@ module Glossarist
11
11
  attribute :extension_attributes, :hash
12
12
  attribute :termid, :string
13
13
 
14
- yaml do
14
+ key_value do
15
15
  map :data, to: :data
16
16
  map :termid, to: :termid
17
17
  map :subject, to: :subject
@@ -0,0 +1,153 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Glossarist
4
+ class ConceptCollector
5
+ def self.collect(dir)
6
+ dir = File.expand_path(dir)
7
+ unless File.directory?(dir)
8
+ raise ArgumentError, "#{dir} is not a directory"
9
+ end
10
+
11
+ if v2_concepts?(dir)
12
+ collect_v2_concepts(dir)
13
+ elsif managed_concepts?(dir)
14
+ collect_managed_concepts(dir)
15
+ elsif v1_concepts?(dir)
16
+ collect_v1_concepts(dir)
17
+ else
18
+ []
19
+ end
20
+ end
21
+
22
+ def self.each_concept(dir, &block)
23
+ dir = File.expand_path(dir)
24
+ unless File.directory?(dir)
25
+ raise ArgumentError, "#{dir} is not a directory"
26
+ end
27
+ return enum_for(:each_concept, dir) unless block
28
+
29
+ if v2_concepts?(dir)
30
+ each_v2_concept(dir, &block)
31
+ elsif managed_concepts?(dir)
32
+ each_managed_concept(dir, &block)
33
+ elsif v1_concepts?(dir)
34
+ each_v1_concept(dir, &block)
35
+ end
36
+ end
37
+
38
+ class << self
39
+ private
40
+
41
+ def v1_concepts?(dir)
42
+ concepts_dir = File.join(dir, "concepts")
43
+ File.directory?(concepts_dir) &&
44
+ Dir.glob(File.join(concepts_dir, "*.yaml")).any? do |f|
45
+ V1::Concept.from_file(f)&.termid?
46
+ end
47
+ end
48
+
49
+ def v2_concepts?(dir)
50
+ File.directory?(File.join(dir, "geolexica-v2"))
51
+ end
52
+
53
+ def managed_concepts?(dir)
54
+ concept_dir = File.join(dir, "concepts", "concept")
55
+ File.directory?(concept_dir) &&
56
+ Dir.glob(File.join(concept_dir, "*.yaml")).any?
57
+ end
58
+
59
+ def collect_v1_concepts(dir)
60
+ concepts = []
61
+ each_v1_concept(dir) { |mc| concepts << mc }
62
+ concepts
63
+ end
64
+
65
+ def each_v1_concept(dir)
66
+ concepts_dir = File.join(dir, "concepts")
67
+ files = Dir.glob(File.join(concepts_dir, "*.yaml"))
68
+ files.each do |file|
69
+ v1 = V1::Concept.from_file(file)
70
+ next unless v1
71
+
72
+ yield v1.to_managed_concept
73
+ end
74
+ end
75
+
76
+ def collect_v2_concepts(dir)
77
+ v2_dir = File.join(dir, "geolexica-v2")
78
+ if File.directory?(File.join(v2_dir, "concepts"))
79
+ collect_managed_concepts(v2_dir)
80
+ else
81
+ collect_grouped_v2_concepts(v2_dir)
82
+ end
83
+ end
84
+
85
+ def each_v2_concept(dir, &block)
86
+ v2_dir = File.join(dir, "geolexica-v2")
87
+ if File.directory?(File.join(v2_dir, "concepts"))
88
+ each_managed_concept(v2_dir, &block)
89
+ else
90
+ each_grouped_v2_concept(v2_dir, &block)
91
+ end
92
+ end
93
+
94
+ def each_grouped_v2_concept(v2_dir, &block)
95
+ collection = ManagedConceptCollection.new
96
+ manager = ConceptManager.new(path: v2_dir)
97
+ manager.load_from_files(collection: collection)
98
+ collection.each(&block)
99
+ end
100
+
101
+ def collect_grouped_v2_concepts(v2_dir)
102
+ collection = ManagedConceptCollection.new
103
+ manager = ConceptManager.new(path: v2_dir)
104
+ manager.load_from_files(collection: collection)
105
+ collection.to_a
106
+ end
107
+
108
+ def collect_managed_concepts(dir) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
109
+ concepts = []
110
+ each_managed_concept(dir) { |mc| concepts << mc }
111
+ concepts
112
+ end
113
+
114
+ def each_managed_concept(dir) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
115
+ concepts_dir = File.join(dir, "concepts")
116
+ concept_files = Dir.glob(File.join(concepts_dir, "concept", "*.yaml"))
117
+ return if concept_files.empty?
118
+
119
+ lc_dir = find_localized_concepts_dir(concepts_dir)
120
+ lc_index = build_lc_index(lc_dir) if lc_dir
121
+
122
+ concept_files.each do |f|
123
+ mc = ManagedConcept.from_yaml(File.read(f))
124
+ next unless mc.data&.id
125
+
126
+ lc_map = mc.data.localized_concepts || {}
127
+ lc_map.each_value do |uuid|
128
+ lc_file = lc_index ? lc_index[uuid] : nil
129
+ next unless lc_file
130
+
131
+ l10n = LocalizedConcept.from_yaml(File.read(lc_file))
132
+ mc.add_localization(l10n)
133
+ end
134
+
135
+ yield mc
136
+ end
137
+ end
138
+
139
+ def build_lc_index(lc_dir)
140
+ Dir.glob(File.join(lc_dir, "*.{yaml,yml}"))
141
+ .to_h { |f| [File.basename(f, ".*"), f] }
142
+ end
143
+
144
+ def find_localized_concepts_dir(concepts_dir)
145
+ %w[localized_concept localized-concept].each do |name|
146
+ d = File.join(concepts_dir, name)
147
+ return d if File.directory?(d)
148
+ end
149
+ nil
150
+ end
151
+ end
152
+ end
153
+ end
@@ -3,18 +3,24 @@ module Glossarist
3
3
  include Glossarist::Utilities::CommonFunctions
4
4
 
5
5
  attribute :dates, ConceptDate, collection: true
6
- attribute :definition, DetailedDefinition, collection: true,
7
- initialize_empty: true
8
- attribute :examples, DetailedDefinition, collection: true,
9
- initialize_empty: true
6
+ attribute :definition, DetailedDefinition,
7
+ collection: Collections::DetailedDefinitionCollection,
8
+ initialize_empty: true
9
+ attribute :examples, DetailedDefinition,
10
+ collection: Collections::DetailedDefinitionCollection,
11
+ initialize_empty: true
10
12
  attribute :id, :string
11
13
  attribute :lineage_source_similarity, :integer
12
- attribute :notes, DetailedDefinition, collection: true,
13
- initialize_empty: true
14
+ attribute :notes, DetailedDefinition,
15
+ collection: Collections::DetailedDefinitionCollection,
16
+ initialize_empty: true
14
17
  attribute :release, :string
15
- attribute :sources, ConceptSource, collection: true
18
+ attribute :sources, ConceptSource,
19
+ collection: Collections::ConceptSourceCollection,
20
+ initialize_empty: true
16
21
  attribute :terms, Designation::Base, collection: true
17
22
  attribute :related, RelatedConcept, collection: true
23
+ attribute :references, ConceptReference, collection: true
18
24
  attribute :domain, :string
19
25
  attribute :review_date, :date_time
20
26
  attribute :review_decision_date, :date_time
@@ -26,7 +32,7 @@ module Glossarist
26
32
  attribute :language_code, :string, pattern: /^.{3}$/
27
33
  attribute :entry_status, :string
28
34
 
29
- yaml do
35
+ key_value do
30
36
  map :dates, to: :dates
31
37
  map :definition, to: :definition, value_map: { to: { empty: :empty } }
32
38
  map :examples, to: :examples, value_map: { to: { empty: :empty } }
@@ -39,6 +45,7 @@ module Glossarist
39
45
  map :terms, to: :terms,
40
46
  with: { from: :terms_from_yaml, to: :terms_to_yaml }
41
47
  map :related, to: :related
48
+ map :references, to: :references
42
49
  map :domain, to: :domain
43
50
  map %i[language_code languageCode], to: :language_code
44
51
  map %i[entry_status entryStatus], to: :entry_status
@@ -8,7 +8,7 @@ module Glossarist
8
8
  attribute :type, :string,
9
9
  values: Glossarist::GlossaryDefinition::CONCEPT_DATE_TYPES
10
10
 
11
- yaml do
11
+ key_value do
12
12
  map :date, to: :date
13
13
  map :type, to: :type
14
14
  end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Glossarist
4
+ class ConceptDocument < Lutaml::Model::Serializable
5
+ attribute :concept, ManagedConcept
6
+ attribute :localizations, LocalizedConcept, collection: true
7
+
8
+ yamls do
9
+ sequence do
10
+ map_document 0, to: :concept, type: ManagedConcept
11
+ map_document 1.., to: :localizations, type: LocalizedConcept,
12
+ collection: true
13
+ end
14
+ end
15
+
16
+ def self.from_managed_concept(managed_concept)
17
+ new(
18
+ concept: managed_concept,
19
+ localizations: managed_concept.localizations&.values || [],
20
+ )
21
+ end
22
+
23
+ def to_managed_concept
24
+ mc = concept
25
+ localizations.each { |l10n| mc.add_localization(l10n) }
26
+ mc
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Glossarist
4
+ class ConceptEnricher
5
+ def inject_references(concepts) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
6
+ extractor = ReferenceExtractor.new
7
+
8
+ concepts.each do |mc|
9
+ mc.localizations.each do |l10n|
10
+ refs = extractor.extract_from_localized_concept(l10n)
11
+ next if refs.empty?
12
+
13
+ existing = l10n.data.references || []
14
+ seen_keys = existing.to_set { |r| [r.source, r.concept_id] }
15
+
16
+ refs.each do |ref|
17
+ key = [ref.source, ref.concept_id]
18
+ next if seen_keys.include?(key)
19
+
20
+ seen_keys.add(key)
21
+ existing << ref
22
+ end
23
+ l10n.data.references = existing
24
+ end
25
+ end
26
+ end
27
+
28
+ def apply_uri_template(concepts, template)
29
+ concepts.each do |mc|
30
+ mc.data.uri = template.sub("{id}", mc.data.id.to_s)
31
+ end
32
+ end
33
+ end
34
+ end