glossarist 2.6.6 → 2.6.7

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 (139) hide show
  1. checksums.yaml +4 -4
  2. data/README.adoc +90 -29
  3. data/glossarist.gemspec +2 -0
  4. data/lib/glossarist/citation.rb +26 -123
  5. data/lib/glossarist/cli/compare_command.rb +106 -0
  6. data/lib/glossarist/cli/export_command.rb +11 -14
  7. data/lib/glossarist/cli/validate_command.rb +111 -20
  8. data/lib/glossarist/cli.rb +18 -0
  9. data/lib/glossarist/collections/bibliography_collection.rb +4 -2
  10. data/lib/glossarist/collections/localization_collection.rb +2 -0
  11. data/lib/glossarist/comparison_result.rb +35 -0
  12. data/lib/glossarist/concept_collector.rb +44 -0
  13. data/lib/glossarist/concept_comparator.rb +72 -0
  14. data/lib/glossarist/concept_data.rb +16 -0
  15. data/lib/glossarist/concept_diff.rb +15 -0
  16. data/lib/glossarist/concept_document.rb +11 -0
  17. data/lib/glossarist/concept_manager.rb +19 -5
  18. data/lib/glossarist/concept_ref.rb +13 -0
  19. data/lib/glossarist/concept_validator.rb +6 -1
  20. data/lib/glossarist/context_configuration.rb +90 -0
  21. data/lib/glossarist/dataset_validator.rb +8 -4
  22. data/lib/glossarist/designation/prefix.rb +17 -0
  23. data/lib/glossarist/designation/suffix.rb +17 -0
  24. data/lib/glossarist/gcr_metadata.rb +7 -14
  25. data/lib/glossarist/gcr_package.rb +35 -23
  26. data/lib/glossarist/gcr_validator.rb +38 -17
  27. data/lib/glossarist/localized_concept.rb +8 -0
  28. data/lib/glossarist/managed_concept.rb +39 -6
  29. data/lib/glossarist/managed_concept_data.rb +2 -1
  30. data/lib/glossarist/rdf/ext/jsonld_transform_ext.rb +208 -0
  31. data/lib/glossarist/rdf/ext/mapping_ext.rb +37 -0
  32. data/lib/glossarist/rdf/ext/mapping_rule_ext.rb +27 -0
  33. data/lib/glossarist/rdf/ext/member_rule_ext.rb +34 -0
  34. data/lib/glossarist/rdf/ext/turtle_transform_ext.rb +222 -0
  35. data/lib/glossarist/rdf/ext.rb +39 -0
  36. data/lib/glossarist/rdf/gloss_citation.rb +36 -0
  37. data/lib/glossarist/rdf/gloss_concept.rb +58 -0
  38. data/lib/glossarist/rdf/gloss_concept_date.rb +24 -0
  39. data/lib/glossarist/rdf/gloss_concept_reference.rb +29 -0
  40. data/lib/glossarist/rdf/gloss_concept_source.rb +37 -0
  41. data/lib/glossarist/rdf/gloss_designation.rb +146 -0
  42. data/lib/glossarist/rdf/gloss_detailed_definition.rb +24 -0
  43. data/lib/glossarist/rdf/gloss_grammar_info.rb +57 -0
  44. data/lib/glossarist/rdf/gloss_locality.rb +25 -0
  45. data/lib/glossarist/rdf/gloss_localized_concept.rb +67 -0
  46. data/lib/glossarist/rdf/gloss_non_verbal_rep.rb +31 -0
  47. data/lib/glossarist/rdf/gloss_pronunciation.rb +32 -0
  48. data/lib/glossarist/rdf/gloss_reference.rb +55 -0
  49. data/lib/glossarist/rdf/namespaces/glossarist_namespace.rb +12 -0
  50. data/lib/glossarist/rdf/namespaces/iso_thes_namespace.rb +12 -0
  51. data/lib/glossarist/rdf/namespaces/owl_namespace.rb +12 -0
  52. data/lib/glossarist/rdf/namespaces/prov_namespace.rb +12 -0
  53. data/lib/glossarist/rdf/namespaces/rdf_namespace.rb +12 -0
  54. data/lib/glossarist/rdf/namespaces/skosxl_namespace.rb +12 -0
  55. data/lib/glossarist/rdf/namespaces.rb +8 -2
  56. data/lib/glossarist/rdf/relationships.rb +19 -0
  57. data/lib/glossarist/rdf/v3/configuration.rb +15 -0
  58. data/lib/glossarist/rdf/v3.rb +79 -0
  59. data/lib/glossarist/rdf.rb +22 -2
  60. data/lib/glossarist/reference_extractor.rb +12 -19
  61. data/lib/glossarist/reference_resolver.rb +3 -3
  62. data/lib/glossarist/related_concept.rb +2 -10
  63. data/lib/glossarist/schema_migration.rb +39 -0
  64. data/lib/glossarist/sts/term_mapper.rb +2 -2
  65. data/lib/glossarist/transforms/concept_to_gloss_transform.rb +355 -0
  66. data/lib/glossarist/transforms.rb +2 -2
  67. data/lib/glossarist/v1/concept.rb +17 -17
  68. data/lib/glossarist/v2/citation.rb +36 -0
  69. data/lib/glossarist/v2/concept_data.rb +46 -0
  70. data/lib/glossarist/v2/concept_document.rb +18 -0
  71. data/lib/glossarist/v2/concept_ref.rb +8 -0
  72. data/lib/glossarist/v2/concept_source.rb +16 -0
  73. data/lib/glossarist/v2/configuration.rb +13 -0
  74. data/lib/glossarist/v2/detailed_definition.rb +14 -0
  75. data/lib/glossarist/v2/localized_concept.rb +9 -0
  76. data/lib/glossarist/v2/managed_concept.rb +25 -0
  77. data/lib/glossarist/v2/managed_concept_data.rb +49 -0
  78. data/lib/glossarist/v2/related_concept.rb +15 -0
  79. data/lib/glossarist/v2.rb +28 -0
  80. data/lib/glossarist/v3/bibliography_entry.rb +19 -0
  81. data/lib/glossarist/v3/bibliography_file.rb +27 -0
  82. data/lib/glossarist/v3/citation.rb +30 -0
  83. data/lib/glossarist/v3/concept_data.rb +46 -0
  84. data/lib/glossarist/v3/concept_document.rb +18 -0
  85. data/lib/glossarist/v3/concept_ref.rb +8 -0
  86. data/lib/glossarist/v3/concept_source.rb +16 -0
  87. data/lib/glossarist/v3/configuration.rb +13 -0
  88. data/lib/glossarist/v3/detailed_definition.rb +14 -0
  89. data/lib/glossarist/v3/image_entry.rb +21 -0
  90. data/lib/glossarist/v3/image_file.rb +31 -0
  91. data/lib/glossarist/v3/localized_concept.rb +9 -0
  92. data/lib/glossarist/v3/managed_concept.rb +26 -0
  93. data/lib/glossarist/v3/managed_concept_data.rb +34 -0
  94. data/lib/glossarist/v3/related_concept.rb +15 -0
  95. data/lib/glossarist/v3.rb +36 -0
  96. data/lib/glossarist/validation/bibliography_index.rb +61 -30
  97. data/lib/glossarist/validation/rules/asciidoc_xref_rule.rb +2 -15
  98. data/lib/glossarist/validation/rules/authoritative_source_rule.rb +2 -15
  99. data/lib/glossarist/validation/rules/base.rb +5 -0
  100. data/lib/glossarist/validation/rules/bibliography_yaml_rule.rb +2 -3
  101. data/lib/glossarist/validation/rules/citation_completeness_rule.rb +5 -27
  102. data/lib/glossarist/validation/rules/dataset_context.rb +8 -3
  103. data/lib/glossarist/validation/rules/date_validity_rule.rb +1 -1
  104. data/lib/glossarist/validation/rules/designation_status_rule.rb +0 -1
  105. data/lib/glossarist/validation/rules/designation_type_rule.rb +1 -5
  106. data/lib/glossarist/validation/rules/domain_ref_rule.rb +37 -0
  107. data/lib/glossarist/validation/rules/domain_target_rule.rb +56 -0
  108. data/lib/glossarist/validation/rules/gcr_context.rb +12 -13
  109. data/lib/glossarist/validation/rules/image_reference_rule.rb +2 -17
  110. data/lib/glossarist/validation/rules/locality_completeness_rule.rb +58 -0
  111. data/lib/glossarist/validation/rules/localization_consistency_rule.rb +72 -0
  112. data/lib/glossarist/validation/rules/localization_presence_rule.rb +1 -1
  113. data/lib/glossarist/validation/rules/model_validity_rule.rb +71 -0
  114. data/lib/glossarist/validation/rules/orphaned_bibliography_rule.rb +1 -13
  115. data/lib/glossarist/validation/rules/orphaned_images_rule.rb +16 -11
  116. data/lib/glossarist/validation/rules/ref_shape_rule.rb +68 -0
  117. data/lib/glossarist/validation/rules/related_concept_cycle_rule.rb +1 -3
  118. data/lib/glossarist/validation/rules/related_concept_symmetry_rule.rb +1 -3
  119. data/lib/glossarist/validation/rules/related_concept_target_rule.rb +64 -0
  120. data/lib/glossarist/validation/rules/schema_version_rule.rb +41 -0
  121. data/lib/glossarist/validation/rules/source_type_rule.rb +1 -15
  122. data/lib/glossarist/validation/rules/source_urn_format_rule.rb +65 -0
  123. data/lib/glossarist/validation/rules/uuid_format_rule.rb +33 -0
  124. data/lib/glossarist/validation/rules.rb +10 -43
  125. data/lib/glossarist/validation/validation_issue.rb +14 -11
  126. data/lib/glossarist/validation_result.rb +12 -22
  127. data/lib/glossarist/version.rb +1 -1
  128. data/lib/glossarist.rb +9 -0
  129. data/memory/project-status.md +43 -0
  130. data/scripts/migrate_dataset.rb +180 -0
  131. data/scripts/migrate_isotc204_to_v3.rb +134 -0
  132. data/scripts/migrate_isotc211_to_v3.rb +153 -0
  133. data/scripts/migrate_osgeo_to_v3.rb +155 -0
  134. data/scripts/upgrade_dataset_to_v3.rb +47 -0
  135. metadata +111 -6
  136. data/TODO.integration/01-gcr-package-cli.md +0 -180
  137. data/lib/glossarist/rdf/skos_concept.rb +0 -43
  138. data/lib/glossarist/rdf/skos_vocabulary.rb +0 -25
  139. data/lib/glossarist/transforms/concept_to_skos_transform.rb +0 -131
@@ -2,6 +2,10 @@
2
2
 
3
3
  module Glossarist
4
4
  class DatasetValidator
5
+ def initialize(on_progress: nil)
6
+ @on_progress = on_progress
7
+ end
8
+
5
9
  def validate(path, strict: false, reference_path: nil)
6
10
  if File.extname(path).downcase == ".gcr"
7
11
  validate_gcr(path, reference_path: reference_path)
@@ -13,7 +17,7 @@ module Glossarist
13
17
  private
14
18
 
15
19
  def validate_gcr(path, reference_path: nil)
16
- result = GcrValidator.new.validate(path)
20
+ result = GcrValidator.new(on_progress: @on_progress).validate(path)
17
21
 
18
22
  if reference_path
19
23
  ref_result = validate_gcr_cross_references(path, reference_path)
@@ -24,7 +28,7 @@ module Glossarist
24
28
  end
25
29
 
26
30
  def validate_directory(path, reference_path: nil)
27
- result = ConceptValidator.new(path).validate_all
31
+ result = ConceptValidator.new(path, on_progress: @on_progress).validate_all
28
32
 
29
33
  if reference_path
30
34
  ref_result = validate_directory_cross_references(path, reference_path)
@@ -38,7 +42,7 @@ module Glossarist
38
42
  extractor = ReferenceExtractor.new
39
43
  resolver = build_resolver(reference_path)
40
44
  pkg = GcrPackage.load(path)
41
- uri_prefix = pkg.metadata&.dig("uri_prefix") || pkg.metadata&.dig("shortname")
45
+ uri_prefix = pkg.metadata&.uri_prefix || pkg.metadata&.shortname
42
46
  resolver.register_self(pkg.concepts)
43
47
  resolver.register_package(pkg, uri_prefix: uri_prefix)
44
48
  resolver.validate_all(pkg, extractor: extractor)
@@ -56,7 +60,7 @@ module Glossarist
56
60
  resolver = ReferenceResolver.new
57
61
  Dir.glob(File.join(reference_path, "*.gcr")).each do |gcr_path|
58
62
  pkg = GcrPackage.load(gcr_path)
59
- uri_prefix = pkg.metadata&.dig("uri_prefix") || pkg.metadata&.dig("shortname")
63
+ uri_prefix = pkg.metadata&.uri_prefix || pkg.metadata&.shortname
60
64
  resolver.register_package(pkg, uri_prefix: uri_prefix)
61
65
  end
62
66
  resolver
@@ -0,0 +1,17 @@
1
+ module Glossarist
2
+ module Designation
3
+ class Prefix < Base
4
+ attribute :type, :string, default: -> { "prefix" }
5
+
6
+ key_value do
7
+ map :type, to: :type, render_default: true
8
+ end
9
+
10
+ def self.of_yaml(hash, options = {})
11
+ hash["type"] = "prefix" unless hash["type"]
12
+
13
+ super
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ module Glossarist
2
+ module Designation
3
+ class Suffix < Base
4
+ attribute :type, :string, default: -> { "suffix" }
5
+
6
+ key_value do
7
+ map :type, to: :type, render_default: true
8
+ end
9
+
10
+ def self.of_yaml(hash, options = {})
11
+ hash["type"] = "suffix" unless hash["type"]
12
+
13
+ super
14
+ end
15
+ end
16
+ end
17
+ end
@@ -12,7 +12,7 @@ module Glossarist
12
12
  attribute :languages, :string, collection: true
13
13
  attribute :created_at, :string
14
14
  attribute :glossarist_version, :string
15
- attribute :schema_version, :string, default: -> { "1.0.0" }
15
+ attribute :schema_version, :string, default: -> { Glossarist::SCHEMA_VERSION }
16
16
  attribute :statistics, GcrStatistics
17
17
  attribute :homepage, :string
18
18
  attribute :repository, :string
@@ -46,18 +46,19 @@ module Glossarist
46
46
 
47
47
  def self.from_concepts(concepts, register_data: nil, options: {})
48
48
  stats = GcrStatistics.from_concepts(concepts)
49
+ rd = register_data
49
50
  new(
50
- shortname: options[:shortname] || register_data&.dig("shortname") || register_data&.dig("id"),
51
- version: options[:version] || register_data&.dig("version"),
52
- title: options[:title] || register_data&.dig("name"),
53
- description: options[:description] || register_data&.dig("description"),
51
+ shortname: options[:shortname] || rd&.[]("shortname") || rd&.[]("id"),
52
+ version: options[:version] || rd&.[]("version"),
53
+ title: options[:title] || rd&.[]("name"),
54
+ description: options[:description] || rd&.[]("description"),
54
55
  owner: options[:owner],
55
56
  tags: options[:tags] || [],
56
57
  concept_count: concepts.length,
57
58
  languages: stats.languages,
58
59
  created_at: Time.now.utc.iso8601,
59
60
  glossarist_version: Glossarist::VERSION,
60
- schema_version: register_data&.dig("schema_version") || SchemaMigration::CURRENT_SCHEMA_VERSION,
61
+ schema_version: rd&.[]("schema_version") || SchemaMigration::CURRENT_SCHEMA_VERSION,
61
62
  statistics: stats,
62
63
  uri_prefix: options[:uri_prefix],
63
64
  concept_uri_template: options[:concept_uri_template],
@@ -78,13 +79,5 @@ module Glossarist
78
79
  end
79
80
  sources.map { |uri| { "uri" => uri } }
80
81
  end
81
-
82
- def [](key)
83
- to_yaml_hash[key]
84
- end
85
-
86
- def dig(*keys)
87
- to_yaml_hash.dig(*keys)
88
- end
89
82
  end
90
83
  end
@@ -156,31 +156,41 @@ module Glossarist
156
156
  end
157
157
 
158
158
  def read_file_assets(zip_file)
159
- DATASET_ASSETS.each do |asset|
160
- next unless asset[:type] == :file && asset[:attr]
161
-
162
- entry = zip_file.find_entry(asset[:path])
163
- next unless entry
164
-
165
- instance_variable_set("@#{asset[:attr]}", entry.get_input_stream.read)
166
- end
159
+ entry = zip_file.find_entry("bibliography.yaml")
160
+ @bibliography = entry.get_input_stream.read if entry
167
161
  end
168
162
 
169
163
  def read_concepts(zip_file)
164
+ doc_class = concept_document_class_for_read
170
165
  zip_file.entries.each do |entry|
171
166
  next unless entry.name.start_with?("concepts/") && entry.name.end_with?(".yaml")
172
167
 
173
168
  raw = entry.get_input_stream.read
174
- doc = ConceptDocument.from_yamls(raw)
169
+ doc = doc_class.from_yamls(raw)
175
170
  @concepts << doc.to_managed_concept
176
171
  end
177
172
  end
178
173
 
179
174
  private
180
175
 
176
+ def concept_document_class_for_read
177
+ version = @metadata&.schema_version.to_s
178
+ ConceptDocument.for_version(version)
179
+ end
180
+
181
+ def concept_document_class_for_write(concept)
182
+ version = if concept.schema_version.to_s == "2"
183
+ "2"
184
+ else
185
+ "3"
186
+ end
187
+ ConceptDocument.for_version(version)
188
+ end
189
+
181
190
  def write_concept(zip_file, concept)
182
191
  termid = concept.data.id.to_s
183
- doc = ConceptDocument.from_managed_concept(concept)
192
+ doc_class = concept_document_class_for_write(concept)
193
+ doc = doc_class.from_managed_concept(concept)
184
194
  zip_file.get_output_stream("concepts/#{termid}.yaml") do |f|
185
195
  f.write(doc.to_yamls)
186
196
  end
@@ -215,19 +225,21 @@ module Glossarist
215
225
  end
216
226
 
217
227
  def write_compiled_skos(zip_file, concepts, formats, opts, name) # rubocop:disable Metrics/MethodLength
218
- require "glossarist/transforms/concept_to_skos_transform"
219
- vocab = Transforms::ConceptToSkosTransform.transform_document(concepts,
220
- opts)
228
+ require "glossarist/transforms/concept_to_gloss_transform"
229
+
230
+ if formats.include?("jsonld") || formats.include?("turtle")
231
+ transform = Transforms::ConceptToGlossTransform.new(nil, opts)
221
232
 
222
- if formats.include?("jsonld")
223
- zip_file.get_output_stream("compiled/#{name}.jsonld") do |f|
224
- f.write(vocab.to_jsonld)
233
+ if formats.include?("jsonld")
234
+ zip_file.get_output_stream("compiled/#{name}.jsonld") do |f|
235
+ f.write(transform.to_jsonld(concepts))
236
+ end
225
237
  end
226
- end
227
238
 
228
- if formats.include?("turtle")
229
- zip_file.get_output_stream("compiled/#{name}.ttl") do |f|
230
- f.write(vocab.to_turtle)
239
+ if formats.include?("turtle")
240
+ zip_file.get_output_stream("compiled/#{name}.ttl") do |f|
241
+ f.write(transform.to_turtle(concepts))
242
+ end
231
243
  end
232
244
  end
233
245
 
@@ -235,8 +247,8 @@ module Glossarist
235
247
 
236
248
  zip_file.get_output_stream("compiled/#{name}.jsonl") do |f|
237
249
  concepts.each do |concept|
238
- skos = Transforms::ConceptToSkosTransform.transform(concept, opts)
239
- f.write(skos.to_jsonld)
250
+ transform = Transforms::ConceptToGlossTransform.new(concept, opts)
251
+ f.write(transform.to_jsonl_line)
240
252
  f.write("\n")
241
253
  end
242
254
  end
@@ -343,7 +355,7 @@ compiled_formats: [], **opts)
343
355
  languages: languages.sort,
344
356
  created_at: Time.now.utc.iso8601,
345
357
  glossarist_version: Glossarist::VERSION,
346
- schema_version: register_data&.dig("schema_version") || SchemaMigration::CURRENT_SCHEMA_VERSION,
358
+ schema_version: register_data&.[]("schema_version") || SchemaMigration::CURRENT_SCHEMA_VERSION,
347
359
  uri_prefix: opts[:uri_prefix],
348
360
  concept_uri_template: opts[:concept_uri_template],
349
361
  )
@@ -4,6 +4,10 @@ require "zip"
4
4
 
5
5
  module Glossarist
6
6
  class GcrValidator
7
+ def initialize(on_progress: nil)
8
+ @on_progress = on_progress
9
+ end
10
+
7
11
  def validate(zip_path)
8
12
  result = ValidationResult.new
9
13
 
@@ -24,27 +28,36 @@ module Glossarist
24
28
  return result
25
29
  end
26
30
 
27
- begin
28
- context = Validation::Rules::GcrContext.new(zip_path)
29
- rescue StandardError => e
30
- result.add_error("Failed to load GCR: #{e.message}")
31
- return result
32
- end
31
+ context, all_concepts = load_gcr_context(zip_path, result)
32
+ return result if all_concepts.nil?
33
33
 
34
- # Collection-level rules (metadata, structure, integrity)
35
- collection_rules = Validation::Rules::Registry.for_scope(:collection)
36
- collection_rules.each do |rule|
37
- next unless rule.applicable?(context)
34
+ validate_concepts(context, all_concepts, result)
35
+ validate_collection(context, result)
38
36
 
39
- rule.check(context).each { |i| result.add_issue(i) }
40
- end
37
+ result
38
+ end
41
39
 
42
- # Per-concept rules
40
+ private
41
+
42
+ def load_gcr_context(zip_path, result)
43
+ context = Validation::Rules::GcrContext.new(zip_path)
44
+ pkg = GcrPackage.load(zip_path)
45
+ [context, pkg.concepts]
46
+ rescue StandardError => e
47
+ result.add_error("Failed to load GCR: #{e.message}")
48
+ [nil, nil]
49
+ end
50
+
51
+ def validate_concepts(context, all_concepts, result)
43
52
  concept_rules = Validation::Rules::Registry.for_scope(:concept)
44
- context.concepts.each_with_index do |concept, idx|
45
- fname = concept.data&.id ? "concepts/#{concept.data.id}.yaml" : "concepts/concept-#{idx}.yaml"
53
+ total = all_concepts.length
54
+
55
+ all_concepts.each_with_index do |concept, idx|
56
+ context.add_concept(concept)
46
57
  concept_context = Validation::Rules::ConceptContext.new(
47
- concept, file_name: fname, collection_context: context
58
+ concept,
59
+ file_name: concept.data&.id ? "concepts/#{concept.data.id}.yaml" : "concepts/concept-#{idx}.yaml",
60
+ collection_context: context,
48
61
  )
49
62
 
50
63
  concept_rules.each do |rule|
@@ -52,9 +65,17 @@ module Glossarist
52
65
 
53
66
  rule.check(concept_context).each { |i| result.add_issue(i) }
54
67
  end
68
+
69
+ @on_progress&.call(idx + 1, total)
55
70
  end
71
+ end
56
72
 
57
- result
73
+ def validate_collection(context, result)
74
+ Validation::Rules::Registry.for_scope(:collection).each do |rule|
75
+ next unless rule.applicable?(context)
76
+
77
+ rule.check(context).each { |i| result.add_issue(i) }
78
+ end
58
79
  end
59
80
  end
60
81
  end
@@ -37,5 +37,13 @@ module Glossarist
37
37
  def entry_status=(value)
38
38
  data.entry_status = value
39
39
  end
40
+
41
+ def all_sources
42
+ data.all_sources
43
+ end
44
+
45
+ def text_content
46
+ data.text_content
47
+ end
40
48
  end
41
49
  end
@@ -8,7 +8,7 @@ module Glossarist
8
8
 
9
9
  attribute :related, RelatedConcept, collection: true
10
10
  attribute :dates, ConceptDate, collection: true
11
- attribute :sources, ConceptSource
11
+ attribute :sources, ConceptSource, collection: true
12
12
  attribute :date_accepted, ConceptDate
13
13
  attribute :status, :string,
14
14
  values: Glossarist::GlossaryDefinition::CONCEPT_STATUSES
@@ -19,6 +19,8 @@ module Glossarist
19
19
 
20
20
  attribute :uuid, :string
21
21
 
22
+ attribute :schema_version, :string
23
+
22
24
  key_value do
23
25
  map :data, to: :data
24
26
  map :id, with: { to: :identifier_to_yaml, from: :identifier_from_yaml }
@@ -26,11 +28,13 @@ module Glossarist
26
28
  with: { to: :identifier_to_yaml, from: :identifier_from_yaml }
27
29
  map :related, to: :related
28
30
  map :dates, to: :dates
31
+ map :sources, to: :sources
29
32
  map %i[date_accepted dateAccepted],
30
33
  with: { from: :date_accepted_from_yaml, to: :date_accepted_to_yaml }
31
34
  map :status, to: :status
32
35
 
33
36
  map :uuid, to: :uuid, with: { from: :uuid_from_yaml, to: :uuid_to_yaml }
37
+ map :schema_version, to: :schema_version
34
38
  end
35
39
 
36
40
  def localizations
@@ -65,7 +69,7 @@ module Glossarist
65
69
  def uuid
66
70
  @uuid ||= Glossarist::Utilities::UUID.uuid_v5(
67
71
  Glossarist::Utilities::UUID::OID_NAMESPACE,
68
- to_yaml(except: [:uuid]),
72
+ to_yaml(except: %i[uuid schema_version]),
69
73
  )
70
74
  end
71
75
 
@@ -109,18 +113,19 @@ module Glossarist
109
113
  data.localized_concepts ||= {}
110
114
  data.localized_concepts[lang] =
111
115
  data.localized_concepts[lang] || localized_concept.uuid
116
+ localized_concept.uuid = data.localized_concepts[lang]
112
117
  localizations.store(lang, localized_concept)
113
118
  end
114
119
  alias :add_l10n :add_localization
115
120
 
116
121
  def to_jsonld
117
- require "glossarist/transforms/concept_to_skos_transform"
118
- Transforms::ConceptToSkosTransform.transform(self).to_jsonld
122
+ require "glossarist/transforms/concept_to_gloss_transform"
123
+ Transforms::ConceptToGlossTransform.transform(self).to_jsonld
119
124
  end
120
125
 
121
126
  def to_turtle
122
- require "glossarist/transforms/concept_to_skos_transform"
123
- Transforms::ConceptToSkosTransform.transform(self).to_turtle
127
+ require "glossarist/transforms/concept_to_gloss_transform"
128
+ Transforms::ConceptToGlossTransform.transform(self).to_turtle
124
129
  end
125
130
 
126
131
  def default_designation
@@ -138,6 +143,34 @@ module Glossarist
138
143
  localization("eng") || localizations.values.first
139
144
  end
140
145
 
146
+ def schema_version
147
+ @schema_version
148
+ end
149
+
150
+ def assign_uuid(new_uuid)
151
+ @uuid = new_uuid
152
+ end
153
+
154
+ def self.detect_schema_version(concept) # rubocop:disable Metrics/PerceivedComplexity
155
+ raw = concept.schema_version
156
+ if raw && !%w[legacy nil].include?(raw.to_s)
157
+ return raw.to_s
158
+ end
159
+
160
+ return "3" if concept.related&.any?
161
+ return "3" if concept.sources&.any?
162
+ return "3" if concept.data&.domains&.any?
163
+ return "3" if localization_has_references?(concept)
164
+
165
+ "2"
166
+ end
167
+
168
+ def self.localization_has_references?(concept)
169
+ concept.localizations&.any? do |l10n|
170
+ l10n.is_a?(LocalizedConcept) && l10n.data&.references&.any?
171
+ end
172
+ end
173
+
141
174
  Glossarist::GlossaryDefinition::RELATED_CONCEPT_TYPES.each do |type|
142
175
  # List of related concepts of the specified type.
143
176
  # @return [Array<RelatedConcept>]
@@ -10,6 +10,7 @@ module Glossarist
10
10
  attribute :localizations, LocalizedConcept,
11
11
  collection: Collections::LocalizationCollection,
12
12
  initialize_empty: true
13
+ attribute :related, RelatedConcept, collection: true
13
14
 
14
15
  key_value do
15
16
  map %i[id identifier], to: :id,
@@ -17,7 +18,7 @@ module Glossarist
17
18
  map :uri, to: :uri
18
19
  map %i[localized_concepts localizedConcepts], to: :localized_concepts
19
20
  map %i[domains groups], to: :domains,
20
- with: { from: :domains_from_yaml, to: :domains_to_yaml }
21
+ with: { from: :domains_from_yaml, to: :domains_to_yaml }
21
22
  map :sources, to: :sources
22
23
  map :localizations, to: :localizations,
23
24
  with: { from: :localizations_from_yaml, to: :localizations_to_yaml }
@@ -0,0 +1,208 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ # Extends Lutaml::JsonLd::Transform to handle:
6
+ # - Multiple rdf:type values
7
+ # - URI-valued predicates (as: :uri)
8
+ # - Linking predicates for members (link option)
9
+ # - Recursive context/namespace collection
10
+ # - Recursive graph document building
11
+ module Lutaml
12
+ module JsonLd
13
+ class Transform < Lutaml::Rdf::Transform
14
+ def model_to_data(instance, _format, options = {})
15
+ mapping = extract_mapping(options)
16
+ return {} unless mapping
17
+
18
+ if mapping.rdf_members.any?
19
+ build_graph_document(mapping, instance)
20
+ else
21
+ build_resource_object(mapping, instance)
22
+ end
23
+ end
24
+
25
+ def data_to_model(data, _format, options = {})
26
+ mapping = extract_mapping(options)
27
+ return model_class.new unless mapping
28
+
29
+ hash = data.is_a?(String) ? JSON.parse(data) : data
30
+
31
+ if hash.key?("@graph") && hash["@graph"].is_a?(Array) && !hash["@graph"].empty?
32
+ graph_data = hash["@graph"]
33
+ first = graph_data.first
34
+ hash = first.is_a?(Hash) ? first : {}
35
+ end
36
+
37
+ hash = strip_jsonld_keywords(hash)
38
+
39
+ attrs = {}
40
+ mapping.rdf_predicates.each do |rule|
41
+ value = hash[rule.predicate_name]
42
+ next if value.nil?
43
+
44
+ attrs[rule.to] = if rule.lang_tagged && value.is_a?(Hash)
45
+ flatten_language_map(value)
46
+ else
47
+ value
48
+ end
49
+ end
50
+
51
+ build_instance(attrs, options)
52
+ end
53
+
54
+ private
55
+
56
+ def extract_mapping(options)
57
+ options[:mappings] || mappings_for(:jsonld, lutaml_register)
58
+ end
59
+
60
+ def build_graph_document(mapping, instance)
61
+ context = build_merged_context_recursive(mapping, instance)
62
+ graph = collect_resources(mapping, instance)
63
+
64
+ { "@context" => context, "@graph" => graph }
65
+ end
66
+
67
+ def collect_resources(mapping, instance)
68
+ graph = []
69
+
70
+ if mapping.rdf_subject
71
+ resource = build_resource_data(mapping, instance)
72
+ graph << resource unless resource.empty?
73
+ end
74
+
75
+ mapping.rdf_members.each do |member_rule|
76
+ collection = Array(instance.public_send(member_rule.attr_name))
77
+ collection.each do |member|
78
+ member_mapping = member.class.mappings[:jsonld]
79
+ next unless member_mapping
80
+
81
+ resource = build_resource_data(member_mapping, member)
82
+ graph << resource unless resource.empty?
83
+
84
+ # Recurse into child members
85
+ child_resources = collect_resources(member_mapping, member)
86
+ # Skip the first entry (already added above) and any empty resources
87
+ child_resources[1..-1].each { |r| graph << r }
88
+ end
89
+ end
90
+
91
+ graph
92
+ end
93
+
94
+ def build_merged_context_recursive(mapping, instance)
95
+ context_hash = build_context_from_mapping(mapping).to_hash
96
+
97
+ mapping.rdf_members.each do |member_rule|
98
+ collection = Array(instance.public_send(member_rule.attr_name))
99
+ next if collection.empty?
100
+
101
+ collection.map(&:class).uniq.each do |klass|
102
+ member_mapping = klass.mappings[:jsonld]
103
+ next unless member_mapping
104
+
105
+ context_hash.merge!(build_context_from_mapping(member_mapping).to_hash)
106
+
107
+ # Recurse into child members
108
+ child_ctx = build_merged_context_recursive(member_mapping, collection.first)
109
+ context_hash.merge!(child_ctx)
110
+ end
111
+ end
112
+
113
+ context_hash
114
+ end
115
+
116
+ def build_context_from_mapping(mapping)
117
+ context = Context.new
118
+ mapping.namespace_set.each { |ns| context.prefix(ns) }
119
+ mapping.rdf_predicates.each do |pred|
120
+ if pred.lang_tagged
121
+ context.term(pred.predicate_name,
122
+ id: pred.uri,
123
+ container: :language)
124
+ else
125
+ context.term(pred.predicate_name, id: pred.uri)
126
+ end
127
+ end
128
+ context
129
+ end
130
+
131
+ def build_resource_object(mapping, instance)
132
+ context = build_context_from_mapping(mapping).to_hash
133
+ data = build_resource_data(mapping, instance)
134
+ { "@context" => context }.merge(data)
135
+ end
136
+
137
+ def build_resource_data(mapping, instance)
138
+ result = {}
139
+
140
+ if mapping.rdf_types.any?
141
+ types = mapping.rdf_types.map do |t|
142
+ mapping.namespace_set.resolve_compact_iri(t)
143
+ end
144
+ result["@type"] = types.length == 1 ? types.first : types
145
+ end
146
+
147
+ if mapping.rdf_subject
148
+ result["@id"] = resolve_subject_uri(mapping, instance)
149
+ end
150
+
151
+ mapping.rdf_predicates.each do |rule|
152
+ value = instance.public_send(rule.to)
153
+ next if value.nil?
154
+ next if value.is_a?(String) && value.empty?
155
+
156
+ result[rule.predicate_name] = if rule.lang_tagged
157
+ build_language_map(value)
158
+ else
159
+ serialize_rdf_value(value, rule, mapping)
160
+ end
161
+ end
162
+
163
+ # Emit variable-predicate relationship triples
164
+ if instance.is_a?(Glossarist::Rdf::Relationships)
165
+ Array(instance.relationship_triples).each do |pred_uri, obj_uri|
166
+ key = pred_uri.split("#").last.split("/").last
167
+ result[key] = { "@id" => obj_uri }
168
+ end
169
+ end
170
+
171
+ result
172
+ end
173
+
174
+ def build_language_map(values)
175
+ case values
176
+ when Array
177
+ map = {}
178
+ values.each do |v|
179
+ lang = extract_language(v)
180
+ map[lang] = v.to_s if lang
181
+ end
182
+ map.empty? ? nil : map
183
+ else
184
+ lang = extract_language(values)
185
+ lang ? { lang => values.to_s } : values.to_s
186
+ end
187
+ end
188
+
189
+ def flatten_language_map(lang_map)
190
+ lang_map.values
191
+ end
192
+
193
+ def serialize_rdf_value(value, rule = nil, mapping = nil)
194
+ case value
195
+ when Array then value.map { |v| serialize_rdf_value(v, rule, mapping) }
196
+ when Integer, Float, TrueClass, FalseClass then value
197
+ else value.to_s
198
+ end
199
+ end
200
+
201
+ def strip_jsonld_keywords(data)
202
+ return data unless data.is_a?(Hash)
203
+
204
+ data.reject { |key, _| key.start_with?("@") }
205
+ end
206
+ end
207
+ end
208
+ end