glossarist 2.8.2 → 2.8.4

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 (93) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +413 -63
  3. data/Gemfile +1 -0
  4. data/glossarist.gemspec +1 -1
  5. data/lib/glossarist/bibliography_data.rb +41 -0
  6. data/lib/glossarist/bibliography_entry.rb +13 -0
  7. data/lib/glossarist/citation.rb +8 -2
  8. data/lib/glossarist/cli/export_command.rb +10 -5
  9. data/lib/glossarist/cli/validate_command.rb +21 -5
  10. data/lib/glossarist/collection.rb +2 -2
  11. data/lib/glossarist/collections/bibliography_collection.rb +2 -1
  12. data/lib/glossarist/collections/collection.rb +2 -2
  13. data/lib/glossarist/collections/localization_collection.rb +4 -4
  14. data/lib/glossarist/concept_collector.rb +6 -6
  15. data/lib/glossarist/concept_document.rb +2 -1
  16. data/lib/glossarist/concept_manager.rb +6 -7
  17. data/lib/glossarist/concept_set.rb +4 -4
  18. data/lib/glossarist/concept_store.rb +38 -48
  19. data/lib/glossarist/dataset_validator.rb +2 -1
  20. data/lib/glossarist/gcr_package_definition.rb +37 -0
  21. data/lib/glossarist/gcr_statistics.rb +2 -2
  22. data/lib/glossarist/glossary_definition.rb +1 -1
  23. data/lib/glossarist/glossary_store.rb +201 -0
  24. data/lib/glossarist/managed_concept_collection.rb +2 -2
  25. data/lib/glossarist/rdf/gloss_citation.rb +8 -4
  26. data/lib/glossarist/rdf/gloss_concept.rb +6 -3
  27. data/lib/glossarist/rdf/gloss_concept_date.rb +4 -2
  28. data/lib/glossarist/rdf/gloss_concept_reference.rb +6 -3
  29. data/lib/glossarist/rdf/gloss_concept_source.rb +6 -3
  30. data/lib/glossarist/rdf/gloss_designation.rb +56 -25
  31. data/lib/glossarist/rdf/gloss_grammar_info.rb +19 -9
  32. data/lib/glossarist/rdf/gloss_locality.rb +6 -3
  33. data/lib/glossarist/rdf/gloss_localized_concept.rb +14 -7
  34. data/lib/glossarist/rdf/gloss_non_verbal_rep.rb +9 -4
  35. data/lib/glossarist/rdf/gloss_pronunciation.rb +13 -6
  36. data/lib/glossarist/rdf/gloss_reference.rb +8 -4
  37. data/lib/glossarist/rdf/namespaces.rb +3 -2
  38. data/lib/glossarist/rdf/relationship_predicates.rb +14 -7
  39. data/lib/glossarist/rdf.rb +2 -1
  40. data/lib/glossarist/register_data.rb +68 -18
  41. data/lib/glossarist/schema_migration.rb +8 -5
  42. data/lib/glossarist/transforms/concept_to_gloss_transform.rb +23 -10
  43. data/lib/glossarist/v2/concept_data.rb +2 -1
  44. data/lib/glossarist/v2/concept_document.rb +1 -1
  45. data/lib/glossarist/v3/concept_data.rb +2 -1
  46. data/lib/glossarist/v3/concept_document.rb +1 -1
  47. data/lib/glossarist/validation/asset_index.rb +2 -2
  48. data/lib/glossarist/validation/bibliography_index.rb +2 -1
  49. data/lib/glossarist/validation/rules/asciidoc_xref_rule.rb +2 -1
  50. data/lib/glossarist/validation/rules/authoritative_source_rule.rb +1 -1
  51. data/lib/glossarist/validation/rules/bibliography_yaml_rule.rb +1 -1
  52. data/lib/glossarist/validation/rules/citation_completeness_rule.rb +1 -1
  53. data/lib/glossarist/validation/rules/concept_count_rule.rb +2 -3
  54. data/lib/glossarist/validation/rules/concept_id_rule.rb +0 -1
  55. data/lib/glossarist/validation/rules/concept_id_uniqueness_rule.rb +0 -1
  56. data/lib/glossarist/validation/rules/concept_mention_rule.rb +1 -2
  57. data/lib/glossarist/validation/rules/concept_status_rule.rb +1 -2
  58. data/lib/glossarist/validation/rules/concept_uri_rule.rb +1 -1
  59. data/lib/glossarist/validation/rules/date_type_rule.rb +5 -3
  60. data/lib/glossarist/validation/rules/date_validity_rule.rb +1 -1
  61. data/lib/glossarist/validation/rules/definition_content_rule.rb +1 -2
  62. data/lib/glossarist/validation/rules/domain_target_rule.rb +1 -1
  63. data/lib/glossarist/validation/rules/duplicate_term_rule.rb +1 -2
  64. data/lib/glossarist/validation/rules/entry_status_rule.rb +1 -2
  65. data/lib/glossarist/validation/rules/filename_id_rule.rb +2 -3
  66. data/lib/glossarist/validation/rules/image_reference_rule.rb +3 -2
  67. data/lib/glossarist/validation/rules/l10n_uuid_integrity_rule.rb +1 -2
  68. data/lib/glossarist/validation/rules/language_coverage_rule.rb +1 -2
  69. data/lib/glossarist/validation/rules/language_list_rule.rb +2 -3
  70. data/lib/glossarist/validation/rules/locality_completeness_rule.rb +3 -1
  71. data/lib/glossarist/validation/rules/localization_consistency_rule.rb +1 -1
  72. data/lib/glossarist/validation/rules/localization_presence_rule.rb +0 -1
  73. data/lib/glossarist/validation/rules/model_validity_rule.rb +1 -1
  74. data/lib/glossarist/validation/rules/orphaned_bibliography_rule.rb +2 -1
  75. data/lib/glossarist/validation/rules/orphaned_images_rule.rb +6 -4
  76. data/lib/glossarist/validation/rules/orphaned_l10n_files_rule.rb +1 -2
  77. data/lib/glossarist/validation/rules/preferred_term_rule.rb +1 -2
  78. data/lib/glossarist/validation/rules/related_concept_cycle_rule.rb +2 -2
  79. data/lib/glossarist/validation/rules/related_concept_rule.rb +1 -2
  80. data/lib/glossarist/validation/rules/related_concept_symmetry_rule.rb +1 -1
  81. data/lib/glossarist/validation/rules/related_concept_target_rule.rb +1 -1
  82. data/lib/glossarist/validation/rules/source_type_rule.rb +2 -2
  83. data/lib/glossarist/validation/rules/source_urn_format_rule.rb +2 -2
  84. data/lib/glossarist/validation/rules/terms_presence_rule.rb +1 -1
  85. data/lib/glossarist/validation/rules/uuid_format_rule.rb +1 -1
  86. data/lib/glossarist/version.rb +1 -1
  87. data/lib/glossarist.rb +4 -0
  88. data/scripts/migrate_dataset.rb +14 -8
  89. data/scripts/migrate_isotc204_to_v3.rb +3 -1
  90. data/scripts/migrate_isotc211_to_v3.rb +5 -3
  91. data/scripts/migrate_osgeo_to_v3.rb +4 -2
  92. data/scripts/upgrade_dataset_to_v3.rb +0 -0
  93. metadata +13 -5
@@ -56,8 +56,14 @@ module Glossarist
56
56
 
57
57
  doc["locality"] = {}
58
58
  doc["locality"]["type"] = model.locality.type
59
- doc["locality"]["reference_from"] = model.locality.reference_from if model.locality.reference_from
60
- doc["locality"]["reference_to"] = model.locality.reference_to if model.locality.reference_to
59
+ if model.locality.reference_from
60
+ doc["locality"]["reference_from"] =
61
+ model.locality.reference_from
62
+ end
63
+ if model.locality.reference_to
64
+ doc["locality"]["reference_to"] =
65
+ model.locality.reference_to
66
+ end
61
67
  end
62
68
  end
63
69
  end
@@ -74,14 +74,18 @@ module Glossarist
74
74
 
75
75
  def export_jsonld(concepts, name, output_dir)
76
76
  require "glossarist/transforms/concept_to_gloss_transform"
77
- transform = Transforms::ConceptToGlossTransform.new(nil, transform_options)
78
- File.write(File.join(output_dir, "#{name}.jsonld"), transform.to_jsonld(concepts))
77
+ transform = Transforms::ConceptToGlossTransform.new(nil,
78
+ transform_options)
79
+ File.write(File.join(output_dir, "#{name}.jsonld"),
80
+ transform.to_jsonld(concepts))
79
81
  end
80
82
 
81
83
  def export_turtle(concepts, name, output_dir)
82
84
  require "glossarist/transforms/concept_to_gloss_transform"
83
- transform = Transforms::ConceptToGlossTransform.new(nil, transform_options)
84
- File.write(File.join(output_dir, "#{name}.ttl"), transform.to_turtle(concepts))
85
+ transform = Transforms::ConceptToGlossTransform.new(nil,
86
+ transform_options)
87
+ File.write(File.join(output_dir, "#{name}.ttl"),
88
+ transform.to_turtle(concepts))
85
89
  end
86
90
 
87
91
  def export_tbx(concepts, name, output_dir)
@@ -95,7 +99,8 @@ module Glossarist
95
99
  require "glossarist/transforms/concept_to_gloss_transform"
96
100
  File.open(File.join(output_dir, "#{name}.jsonl"), "w") do |f|
97
101
  concepts.each do |concept|
98
- transform = Transforms::ConceptToGlossTransform.new(concept, transform_options)
102
+ transform = Transforms::ConceptToGlossTransform.new(concept,
103
+ transform_options)
99
104
  f.write(transform.to_jsonl_line)
100
105
  f.write("\n")
101
106
  end
@@ -37,7 +37,8 @@ module Glossarist
37
37
  filled = (current.to_f / total * bar_width).round
38
38
  bar = "#{'█' * filled}#{'░' * (bar_width - filled)}"
39
39
 
40
- $stderr.print "\r #{Paint['Validating', :bold]} #{bar} #{current}/#{total} (#{pct}%)"
40
+ $stderr.print "\r #{Paint['Validating',
41
+ :bold]} #{bar} #{current}/#{total} (#{pct}%)"
41
42
  $stderr.flush
42
43
  end
43
44
 
@@ -96,18 +97,33 @@ module Glossarist
96
97
  msg_col = 21
97
98
 
98
99
  puts " #{label} #{code} #{issue.message}"
99
- puts "#{' ' * msg_col}#{Paint[issue.suggestion, :green]}" if issue.suggestion
100
+ if issue.suggestion
101
+ puts "#{' ' * msg_col}#{Paint[issue.suggestion,
102
+ :green]}"
103
+ end
100
104
  end
101
105
 
102
106
  def print_summary_line(result)
103
107
  error_count = result.issues.count(&:error?)
104
108
  warning_count = result.issues.count(&:warning?)
105
109
 
106
- status = error_count.positive? ? Paint["INVALID", :red, :bold] : Paint["VALID", :green, :bold]
110
+ status = if error_count.positive?
111
+ Paint["INVALID", :red,
112
+ :bold]
113
+ else
114
+ Paint["VALID", :green,
115
+ :bold]
116
+ end
107
117
 
108
118
  details = []
109
- details << Paint["#{error_count} error(s)", :red] if error_count.positive?
110
- details << Paint["#{warning_count} warning(s)", :yellow] if warning_count.positive?
119
+ if error_count.positive?
120
+ details << Paint["#{error_count} error(s)",
121
+ :red]
122
+ end
123
+ if warning_count.positive?
124
+ details << Paint["#{warning_count} warning(s)",
125
+ :yellow]
126
+ end
111
127
 
112
128
  puts " #{status} #{details.join(', ')}"
113
129
  end
@@ -15,8 +15,8 @@ module Glossarist
15
15
  @index = {}
16
16
  end
17
17
 
18
- def each(&block)
19
- @index.each_value(&block)
18
+ def each(&)
19
+ @index.each_value(&)
20
20
  end
21
21
 
22
22
  # Returns concept with given ID, if it is present in collection, or +nil+
@@ -7,7 +7,7 @@ module Glossarist
7
7
  class BibliographyCollection < Relaton::Db
8
8
  def initialize(_concepts, global_cache, local_cache)
9
9
  @version_mismatch = check_cache_version(local_cache) ||
10
- check_cache_version(global_cache)
10
+ check_cache_version(global_cache)
11
11
  super(global_cache, local_cache)
12
12
  end
13
13
 
@@ -40,6 +40,7 @@ module Glossarist
40
40
  concepts.each do |concept|
41
41
  concept.default_lang.sources.each do |source|
42
42
  next if source.origin.ref.nil?
43
+
43
44
  ref_text = source.origin.ref.source
44
45
  next if ref_text.nil?
45
46
 
@@ -18,8 +18,8 @@ module Glossarist
18
18
  @collection << @klass.new(object)
19
19
  end
20
20
 
21
- def each(&block)
22
- @collection.each(&block)
21
+ def each(&)
22
+ @collection.each(&)
23
23
  end
24
24
 
25
25
  def empty?
@@ -27,12 +27,12 @@ module Glossarist
27
27
  to_a
28
28
  end
29
29
 
30
- def each_key(&block)
31
- keys.each(&block)
30
+ def each_key(&)
31
+ keys.each(&)
32
32
  end
33
33
 
34
- def each_value(&block)
35
- values.each(&block)
34
+ def each_value(&)
35
+ values.each(&)
36
36
  end
37
37
  end
38
38
  end
@@ -136,25 +136,25 @@ module Glossarist
136
136
  end
137
137
  end
138
138
 
139
- def each_v2_concept(dir, &block)
139
+ def each_v2_concept(dir, &)
140
140
  if v2_flat_concepts?(dir)
141
- each_grouped_v2_concepts(File.join(dir, "concepts"), &block)
141
+ each_grouped_v2_concepts(File.join(dir, "concepts"), &)
142
142
  else
143
143
  v2_dir = File.join(dir, "geolexica-v2")
144
144
  if File.directory?(File.join(v2_dir, "concepts"))
145
- each_managed_concept(v2_dir, &block)
145
+ each_managed_concept(v2_dir, &)
146
146
  else
147
- each_grouped_v2_concepts(v2_dir, &block)
147
+ each_grouped_v2_concepts(v2_dir, &)
148
148
  end
149
149
  end
150
150
  end
151
151
 
152
- def each_grouped_v2_concepts(v2_dir, &block)
152
+ def each_grouped_v2_concepts(v2_dir, &)
153
153
  collection = ManagedConceptCollection.new
154
154
  manager = ConceptManager.new(path: v2_dir)
155
155
  manager.version = detect_schema_version(v2_dir)
156
156
  manager.load_from_files(collection: collection)
157
- collection.each(&block)
157
+ collection.each(&)
158
158
  end
159
159
 
160
160
  def collect_grouped_v2_concepts(v2_dir)
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Glossarist
4
4
  class ConceptDocument < Lutaml::Model::Serializable
5
+ attribute :id, :string
5
6
  attribute :concept, ManagedConcept
6
7
  attribute :localizations, LocalizedConcept, collection: true
7
8
 
@@ -9,7 +10,7 @@ module Glossarist
9
10
  sequence do
10
11
  map_document 0, to: :concept, type: ManagedConcept
11
12
  map_document 1.., to: :localizations, type: LocalizedConcept,
12
- collection: true
13
+ collection: true
13
14
  end
14
15
  end
15
16
 
@@ -132,13 +132,12 @@ module Glossarist
132
132
  if v1_collection?
133
133
  File.join(path, "concept-*.{yaml,yml}")
134
134
  else
135
- # normal v2 collection
136
- concepts_glob = File.join(path, "concept", "*.{yaml,yml}")
137
- if Dir.glob(concepts_glob).empty?
138
- # multiple content YAML files
139
- concepts_glob = File.join(path, "*.{yaml,yml}")
140
- end
141
- concepts_glob
135
+ candidates = [
136
+ File.join(path, "concept", "*.{yaml,yml}"),
137
+ File.join(path, "concepts", "*.{yaml,yml}"),
138
+ File.join(path, "*.{yaml,yml}"),
139
+ ]
140
+ candidates.find { |g| !Dir.glob(g).empty? }
142
141
  end
143
142
  end
144
143
 
@@ -36,7 +36,7 @@ module Glossarist
36
36
  end
37
37
 
38
38
  def to_latex_from_file(entries_file)
39
- File.readlines(entries_file).map do |concept_name|
39
+ File.readlines(entries_file).filter_map do |concept_name|
40
40
  concept = concept_map[concept_name.strip.downcase]
41
41
 
42
42
  if concept.nil?
@@ -44,7 +44,7 @@ module Glossarist
44
44
  else
45
45
  latex_template(concept)
46
46
  end
47
- end.compact.join("\n")
47
+ end.join("\n")
48
48
  end
49
49
 
50
50
  def read_concepts(concepts)
@@ -72,9 +72,9 @@ module Glossarist
72
72
  end
73
73
 
74
74
  def concept_map
75
- @concept_map ||= concepts.managed_concepts.map do |concept|
75
+ @concept_map ||= concepts.managed_concepts.to_h do |concept|
76
76
  [concept.default_designation.downcase, concept]
77
- end.to_h
77
+ end
78
78
  end
79
79
  end
80
80
  end
@@ -4,26 +4,23 @@ require "lutaml/store"
4
4
 
5
5
  module Glossarist
6
6
  class ConceptStore
7
- # Custom serializer that preserves both uuid and identifier through
8
- # YAML string storage. ManagedConcept's key_value mapping writes both
9
- # uuid and identifier to the same "id" key, so a naive to_hash/from_hash
10
- # round-trip loses one of them. Storing the YAML string preserves the
11
- # model exactly, and explicit metadata fields let the store index and
12
- # query by uuid/identifier without deserializing.
13
- class Serializer
14
- def serialize(model)
7
+ # Serializes ConceptDocument for storage in lutaml-store.
8
+ # Stores the YAML stream string to preserve the full concept + localizations.
9
+ class ConceptDocumentSerializer
10
+ def serialize(concept_document)
15
11
  {
16
- "_yaml" => model.to_yaml,
17
- "_uuid" => model.uuid,
18
- "_identifier" => model.identifier,
12
+ "_yamls" => concept_document.to_yamls,
13
+ "_id" => concept_document.id,
19
14
  }
20
15
  end
21
16
 
22
17
  def deserialize(data, model_class)
23
- model = model_class.from_yaml(data["_yaml"])
24
- model.assign_uuid(data["_uuid"]) if data["_uuid"]
25
- model.identifier = data["_identifier"] if data["_identifier"]
26
- model
18
+ doc = model_class.from_yamls(data["_yamls"])
19
+ doc.id = data["_id"]
20
+ concept = doc.concept
21
+ concept.uuid = doc.id if doc.id && concept
22
+ doc.localizations.each { |l10n| concept&.add_localization(l10n) }
23
+ doc
27
24
  end
28
25
  end
29
26
 
@@ -32,62 +29,55 @@ module Glossarist
32
29
  def initialize(adapter: :memory)
33
30
  @db = Lutaml::Store::DatabaseStore.new(
34
31
  adapter: adapter,
35
- models: [managed_concept_registration],
32
+ models: [concept_document_registration],
36
33
  )
37
34
  end
38
35
 
39
- def save(concept)
40
- db.save(concept)
41
- end
42
-
43
- def fetch(uuid)
44
- db.fetch(model: ManagedConcept, uuid: uuid)
45
- end
36
+ def load_glossary(path)
37
+ documents = db.load_all(
38
+ V3::ConceptDocument, path: path, format: :yamls, layout: :grouped
39
+ )
46
40
 
47
- def fetch_by_id(identifier)
48
- db.where(model: ManagedConcept, identifier: identifier).first
49
- end
41
+ documents.each do |doc|
42
+ concept = doc.concept
43
+ concept.uuid = doc.id
44
+ db.save(doc)
45
+ end
50
46
 
51
- def update(uuid, **attributes)
52
- db.update(model: ManagedConcept, uuid: uuid, attributes: attributes)
47
+ documents
53
48
  end
54
49
 
55
- def delete(uuid)
56
- db.destroy(model: ManagedConcept, uuid: uuid)
50
+ def fetch(uuid)
51
+ doc = db.fetch(model: V3::ConceptDocument, id: uuid)
52
+ doc&.concept
57
53
  end
58
54
 
59
- def all
60
- db.all(model: ManagedConcept)
55
+ def concepts
56
+ db.all(model: V3::ConceptDocument).map(&:concept)
61
57
  end
62
58
 
63
59
  def count
64
- db.count(model: ManagedConcept)
60
+ db.count(model: V3::ConceptDocument)
65
61
  end
66
62
 
67
63
  def exists?(uuid)
68
- db.exists?(model: ManagedConcept, uuid: uuid)
64
+ db.exists?(model: V3::ConceptDocument, id: uuid)
69
65
  end
70
66
 
71
67
  def clear
72
- all.each { |concept| delete(concept.uuid) }
73
- end
74
-
75
- def load_from_directory(path, format: :yaml, layout: :separate)
76
- db.import_all(ManagedConcept, path: path, format: format, layout: layout)
77
- end
78
-
79
- def save_to_directory(path, format: :yaml, layout: :separate)
80
- db.save_all(all, path: path, format: format, layout: layout)
68
+ db.all(model: V3::ConceptDocument).each do |doc|
69
+ db.destroy(model: V3::ConceptDocument, id: doc.id)
70
+ end
81
71
  end
82
72
 
83
73
  private
84
74
 
85
- def managed_concept_registration
75
+ def concept_document_registration
86
76
  {
87
- model: ManagedConcept,
88
- key: :uuid,
89
- dir: "concept",
90
- serializer: Serializer.new,
77
+ model: V3::ConceptDocument,
78
+ key: :id,
79
+ dir: "concepts",
80
+ serializer: ConceptDocumentSerializer.new,
91
81
  }
92
82
  end
93
83
  end
@@ -28,7 +28,8 @@ module Glossarist
28
28
  end
29
29
 
30
30
  def validate_directory(path, reference_path: nil)
31
- result = ConceptValidator.new(path, on_progress: @on_progress).validate_all
31
+ result = ConceptValidator.new(path,
32
+ on_progress: @on_progress).validate_all
32
33
 
33
34
  if reference_path
34
35
  ref_result = validate_directory_cross_references(path, reference_path)
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/store"
4
+
5
+ module Glossarist
6
+ module GcrPackageDefinition
7
+ def self.definition(concept_document_class: V3::ConceptDocument)
8
+ Lutaml::Store::PackageDefinition.new(
9
+ name: :gcr,
10
+ metadata_model: GcrMetadata,
11
+ metadata_file: "metadata.yaml",
12
+ ) do |pkg|
13
+ pkg.model(
14
+ model: concept_document_class,
15
+ dir: "concepts",
16
+ layout: :grouped,
17
+ key: :id,
18
+ default_format: :yamls,
19
+ serializer: ConceptStore::ConceptDocumentSerializer.new,
20
+ )
21
+ pkg.model(
22
+ model: RegisterData,
23
+ file: "register.yaml",
24
+ key: :key,
25
+ default_format: :yaml,
26
+ )
27
+ pkg.model(
28
+ model: BibliographyData,
29
+ file: "bibliography.yaml",
30
+ key: :shortname,
31
+ default_format: :yaml,
32
+ )
33
+ pkg.asset("images", type: :directory)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -21,8 +21,8 @@ module Glossarist
21
21
 
22
22
  new(
23
23
  total_concepts: concepts.length,
24
- languages: l10ns.map(&:language_code).compact.sort.uniq,
25
- concepts_by_status: l10ns.map(&:entry_status).compact.tally,
24
+ languages: l10ns.filter_map(&:language_code).sort.uniq,
25
+ concepts_by_status: l10ns.filter_map(&:entry_status).tally,
26
26
  concepts_with_definitions: count_with(l10ns, :definition),
27
27
  concepts_with_sources: count_with(l10ns, :sources),
28
28
  )
@@ -30,7 +30,7 @@ module Glossarist
30
30
  CONCEPT_STATUSES = config.dig("concept", "status").freeze
31
31
 
32
32
  DESIGNATION_RELATIONSHIP_TYPES = config.dig("designation",
33
- "relationship_type")&.freeze
33
+ "relationship_type")&.freeze
34
34
 
35
35
  ISO12620_TERM_TYPES = config.dig("iso12620", "term_type").freeze
36
36
  end
@@ -0,0 +1,201 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/store"
4
+ require "zip"
5
+
6
+ module Glossarist
7
+ class GlossaryStore
8
+ attr_reader :package
9
+
10
+ def initialize
11
+ @package = nil
12
+ @concept_document_class = V3::ConceptDocument
13
+ end
14
+
15
+ # ── Load ──
16
+
17
+ def load_directory(path, format: nil)
18
+ metadata = load_metadata_from_directory(path)
19
+ @concept_document_class = resolve_concept_document_class(metadata)
20
+
21
+ definition = GcrPackageDefinition.definition(
22
+ concept_document_class: @concept_document_class,
23
+ )
24
+ @package = Lutaml::Store::PackageStore.load(
25
+ definition, path, transport: :directory, format: format
26
+ )
27
+
28
+ apply_metadata(metadata)
29
+ self
30
+ end
31
+
32
+ def load_zip(path, format: nil)
33
+ metadata = load_metadata_from_zip(path)
34
+ @concept_document_class = resolve_concept_document_class(metadata)
35
+
36
+ definition = GcrPackageDefinition.definition(
37
+ concept_document_class: @concept_document_class,
38
+ )
39
+ @package = Lutaml::Store::PackageStore.load(
40
+ definition, path, transport: :zip, format: format
41
+ )
42
+
43
+ apply_metadata(metadata)
44
+ self
45
+ end
46
+
47
+ def load(path, format: nil)
48
+ ext = File.extname(path).downcase
49
+ if [".gcr", ".zip"].include?(ext)
50
+ load_zip(path, format: format)
51
+ else
52
+ load_directory(path, format: format)
53
+ end
54
+ end
55
+
56
+ # ── Save ──
57
+
58
+ def save_directory(path, format: nil, formats: {})
59
+ @package.save(path, transport: :directory, format: format,
60
+ formats: formats)
61
+ end
62
+
63
+ def save_zip(path, format: nil, formats: {})
64
+ @package.save(path, transport: :zip, format: format, formats: formats)
65
+ end
66
+
67
+ # ── Concepts ──
68
+
69
+ def concepts
70
+ @package.models_for(@concept_document_class).map(&:to_managed_concept)
71
+ end
72
+
73
+ def concept(uuid)
74
+ doc = @package.fetch_model(@concept_document_class, uuid)
75
+ doc&.to_managed_concept
76
+ end
77
+
78
+ def add_concept(managed_concept)
79
+ ensure_package
80
+ doc = @concept_document_class.from_managed_concept(managed_concept)
81
+ doc.id = managed_concept.uuid
82
+ @package.add_model(doc)
83
+ end
84
+
85
+ def remove_concept(uuid)
86
+ @package.remove_model(@concept_document_class, uuid)
87
+ end
88
+
89
+ def concept_count
90
+ @package.model_count(@concept_document_class)
91
+ end
92
+
93
+ def concept_exists?(uuid)
94
+ @package.model_exists?(@concept_document_class, uuid)
95
+ end
96
+
97
+ # ── Metadata ──
98
+
99
+ def metadata
100
+ @package&.metadata
101
+ end
102
+
103
+ def metadata=(value)
104
+ ensure_package
105
+ @package.metadata = value
106
+ end
107
+
108
+ # ── Register Data ──
109
+
110
+ def register_data
111
+ @package.models_for(RegisterData).first
112
+ end
113
+
114
+ def register_data=(value)
115
+ ensure_package
116
+ existing = register_data
117
+ @package.remove_model(RegisterData, existing.key) if existing
118
+ @package.add_model(value)
119
+ end
120
+
121
+ # ── Bibliography ──
122
+
123
+ def bibliography
124
+ @package.models_for(BibliographyData).first
125
+ end
126
+
127
+ def bibliography=(value)
128
+ ensure_package
129
+ existing = bibliography
130
+ @package.remove_model(BibliographyData, existing.shortname) if existing
131
+ @package.add_model(value)
132
+ end
133
+
134
+ # ── Images ──
135
+
136
+ def image(path)
137
+ @package.asset(path)
138
+ end
139
+
140
+ def add_image(path, content)
141
+ ensure_package
142
+ @package.add_asset(path, content)
143
+ end
144
+
145
+ def image_paths
146
+ @package.asset_paths.select { |p| p.start_with?("images/") }
147
+ end
148
+
149
+ # ── Stats ──
150
+
151
+ def stats
152
+ @package&.stats
153
+ end
154
+
155
+ # ── Convenience ──
156
+
157
+ def build_metadata(shortname:, version:, **opts)
158
+ GcrMetadata.from_concepts(concepts, register_data: register_data, options: {
159
+ shortname: shortname,
160
+ version: version,
161
+ **opts,
162
+ })
163
+ end
164
+
165
+ private
166
+
167
+ def ensure_package
168
+ return if @package
169
+
170
+ definition = GcrPackageDefinition.definition(
171
+ concept_document_class: @concept_document_class,
172
+ )
173
+ @package = Lutaml::Store::PackageStore.new(definition)
174
+ end
175
+
176
+ def resolve_concept_document_class(metadata)
177
+ version = metadata&.schema_version.to_s
178
+ ConceptDocument.for_version(version)
179
+ end
180
+
181
+ def load_metadata_from_directory(path)
182
+ file_path = File.join(path, "metadata.yaml")
183
+ return nil unless File.exist?(file_path)
184
+
185
+ GcrMetadata.from_yaml(File.read(file_path, encoding: "utf-8"))
186
+ end
187
+
188
+ def load_metadata_from_zip(path)
189
+ Zip::File.open(path) do |zf|
190
+ entry = zf.find_entry("metadata.yaml")
191
+ return nil unless entry
192
+
193
+ GcrMetadata.from_yaml(entry.get_input_stream.read)
194
+ end
195
+ end
196
+
197
+ def apply_metadata(metadata)
198
+ @package.metadata = metadata if metadata && @package
199
+ end
200
+ end
201
+ end
@@ -16,8 +16,8 @@ module Glossarist
16
16
  }.compact
17
17
  end
18
18
 
19
- def each(&block)
20
- @managed_concepts.each(&block)
19
+ def each(&)
20
+ @managed_concepts.each(&)
21
21
  end
22
22
 
23
23
  # Returns concept with given ID, if it is present in collection, or +nil+