glossarist 2.6.5 → 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 (155) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/release.yml +1 -4
  3. data/.rubocop_todo.yml +53 -2
  4. data/CLAUDE.md +27 -2
  5. data/README.adoc +532 -56
  6. data/config.yml +68 -1
  7. data/glossarist.gemspec +2 -0
  8. data/lib/glossarist/citation.rb +26 -123
  9. data/lib/glossarist/cli/compare_command.rb +106 -0
  10. data/lib/glossarist/cli/export_command.rb +11 -14
  11. data/lib/glossarist/cli/validate_command.rb +111 -20
  12. data/lib/glossarist/cli.rb +18 -0
  13. data/lib/glossarist/collections/bibliography_collection.rb +4 -2
  14. data/lib/glossarist/collections/localization_collection.rb +2 -0
  15. data/lib/glossarist/comparison_result.rb +35 -0
  16. data/lib/glossarist/concept.rb +1 -1
  17. data/lib/glossarist/concept_collector.rb +44 -0
  18. data/lib/glossarist/concept_comparator.rb +72 -0
  19. data/lib/glossarist/concept_data.rb +20 -0
  20. data/lib/glossarist/concept_diff.rb +15 -0
  21. data/lib/glossarist/concept_document.rb +11 -0
  22. data/lib/glossarist/concept_manager.rb +19 -5
  23. data/lib/glossarist/concept_ref.rb +13 -0
  24. data/lib/glossarist/concept_reference.rb +12 -19
  25. data/lib/glossarist/concept_validator.rb +6 -1
  26. data/lib/glossarist/context_configuration.rb +90 -0
  27. data/lib/glossarist/dataset_validator.rb +8 -4
  28. data/lib/glossarist/designation/abbreviation.rb +0 -2
  29. data/lib/glossarist/designation/base.rb +21 -1
  30. data/lib/glossarist/designation/expression.rb +3 -0
  31. data/lib/glossarist/designation/letter_symbol.rb +0 -4
  32. data/lib/glossarist/designation/prefix.rb +17 -0
  33. data/lib/glossarist/designation/suffix.rb +17 -0
  34. data/lib/glossarist/designation/symbol.rb +0 -2
  35. data/lib/glossarist/gcr_metadata.rb +7 -14
  36. data/lib/glossarist/gcr_package.rb +35 -23
  37. data/lib/glossarist/gcr_validator.rb +38 -17
  38. data/lib/glossarist/glossary_definition.rb +5 -0
  39. data/lib/glossarist/localized_concept.rb +8 -0
  40. data/lib/glossarist/managed_concept.rb +39 -6
  41. data/lib/glossarist/managed_concept_data.rb +22 -2
  42. data/lib/glossarist/non_verb_rep.rb +21 -6
  43. data/lib/glossarist/pronunciation.rb +32 -0
  44. data/lib/glossarist/rdf/ext/jsonld_transform_ext.rb +208 -0
  45. data/lib/glossarist/rdf/ext/mapping_ext.rb +37 -0
  46. data/lib/glossarist/rdf/ext/mapping_rule_ext.rb +27 -0
  47. data/lib/glossarist/rdf/ext/member_rule_ext.rb +34 -0
  48. data/lib/glossarist/rdf/ext/turtle_transform_ext.rb +222 -0
  49. data/lib/glossarist/rdf/ext.rb +39 -0
  50. data/lib/glossarist/rdf/gloss_citation.rb +36 -0
  51. data/lib/glossarist/rdf/gloss_concept.rb +58 -0
  52. data/lib/glossarist/rdf/gloss_concept_date.rb +24 -0
  53. data/lib/glossarist/rdf/gloss_concept_reference.rb +29 -0
  54. data/lib/glossarist/rdf/gloss_concept_source.rb +37 -0
  55. data/lib/glossarist/rdf/gloss_designation.rb +146 -0
  56. data/lib/glossarist/rdf/gloss_detailed_definition.rb +24 -0
  57. data/lib/glossarist/rdf/gloss_grammar_info.rb +57 -0
  58. data/lib/glossarist/rdf/gloss_locality.rb +25 -0
  59. data/lib/glossarist/rdf/gloss_localized_concept.rb +67 -0
  60. data/lib/glossarist/rdf/gloss_non_verbal_rep.rb +31 -0
  61. data/lib/glossarist/rdf/gloss_pronunciation.rb +32 -0
  62. data/lib/glossarist/rdf/gloss_reference.rb +55 -0
  63. data/lib/glossarist/rdf/namespaces/glossarist_namespace.rb +12 -0
  64. data/lib/glossarist/rdf/namespaces/iso_thes_namespace.rb +12 -0
  65. data/lib/glossarist/rdf/namespaces/owl_namespace.rb +12 -0
  66. data/lib/glossarist/rdf/namespaces/prov_namespace.rb +12 -0
  67. data/lib/glossarist/rdf/namespaces/rdf_namespace.rb +12 -0
  68. data/lib/glossarist/rdf/namespaces/skosxl_namespace.rb +12 -0
  69. data/lib/glossarist/rdf/namespaces.rb +8 -2
  70. data/lib/glossarist/rdf/relationships.rb +19 -0
  71. data/lib/glossarist/rdf/v3/configuration.rb +15 -0
  72. data/lib/glossarist/rdf/v3.rb +79 -0
  73. data/lib/glossarist/rdf.rb +22 -2
  74. data/lib/glossarist/reference_extractor.rb +15 -24
  75. data/lib/glossarist/reference_resolver.rb +3 -3
  76. data/lib/glossarist/related_concept.rb +2 -10
  77. data/lib/glossarist/schema_migration.rb +39 -0
  78. data/lib/glossarist/sts/term_mapper.rb +2 -2
  79. data/lib/glossarist/transforms/concept_to_gloss_transform.rb +355 -0
  80. data/lib/glossarist/transforms.rb +2 -2
  81. data/lib/glossarist/urn_resolver.rb +13 -1
  82. data/lib/glossarist/v1/concept.rb +18 -11
  83. data/lib/glossarist/v2/citation.rb +36 -0
  84. data/lib/glossarist/v2/concept_data.rb +46 -0
  85. data/lib/glossarist/v2/concept_document.rb +18 -0
  86. data/lib/glossarist/v2/concept_ref.rb +8 -0
  87. data/lib/glossarist/v2/concept_source.rb +16 -0
  88. data/lib/glossarist/v2/configuration.rb +13 -0
  89. data/lib/glossarist/v2/detailed_definition.rb +14 -0
  90. data/lib/glossarist/v2/localized_concept.rb +9 -0
  91. data/lib/glossarist/v2/managed_concept.rb +25 -0
  92. data/lib/glossarist/v2/managed_concept_data.rb +49 -0
  93. data/lib/glossarist/v2/related_concept.rb +15 -0
  94. data/lib/glossarist/v2.rb +28 -0
  95. data/lib/glossarist/v3/bibliography_entry.rb +19 -0
  96. data/lib/glossarist/v3/bibliography_file.rb +27 -0
  97. data/lib/glossarist/v3/citation.rb +30 -0
  98. data/lib/glossarist/v3/concept_data.rb +46 -0
  99. data/lib/glossarist/v3/concept_document.rb +18 -0
  100. data/lib/glossarist/v3/concept_ref.rb +8 -0
  101. data/lib/glossarist/v3/concept_source.rb +16 -0
  102. data/lib/glossarist/v3/configuration.rb +13 -0
  103. data/lib/glossarist/v3/detailed_definition.rb +14 -0
  104. data/lib/glossarist/v3/image_entry.rb +21 -0
  105. data/lib/glossarist/v3/image_file.rb +31 -0
  106. data/lib/glossarist/v3/localized_concept.rb +9 -0
  107. data/lib/glossarist/v3/managed_concept.rb +26 -0
  108. data/lib/glossarist/v3/managed_concept_data.rb +34 -0
  109. data/lib/glossarist/v3/related_concept.rb +15 -0
  110. data/lib/glossarist/v3.rb +36 -0
  111. data/lib/glossarist/validation/asset_index.rb +4 -3
  112. data/lib/glossarist/validation/bibliography_index.rb +61 -30
  113. data/lib/glossarist/validation/rules/asciidoc_xref_rule.rb +2 -15
  114. data/lib/glossarist/validation/rules/authoritative_source_rule.rb +2 -15
  115. data/lib/glossarist/validation/rules/base.rb +5 -0
  116. data/lib/glossarist/validation/rules/bibliography_yaml_rule.rb +2 -3
  117. data/lib/glossarist/validation/rules/citation_completeness_rule.rb +5 -27
  118. data/lib/glossarist/validation/rules/dataset_context.rb +8 -3
  119. data/lib/glossarist/validation/rules/date_validity_rule.rb +1 -1
  120. data/lib/glossarist/validation/rules/designation_status_rule.rb +0 -1
  121. data/lib/glossarist/validation/rules/designation_type_rule.rb +1 -5
  122. data/lib/glossarist/validation/rules/domain_ref_rule.rb +37 -0
  123. data/lib/glossarist/validation/rules/domain_target_rule.rb +56 -0
  124. data/lib/glossarist/validation/rules/gcr_context.rb +12 -13
  125. data/lib/glossarist/validation/rules/image_reference_rule.rb +2 -17
  126. data/lib/glossarist/validation/rules/locality_completeness_rule.rb +58 -0
  127. data/lib/glossarist/validation/rules/localization_consistency_rule.rb +72 -0
  128. data/lib/glossarist/validation/rules/localization_presence_rule.rb +1 -1
  129. data/lib/glossarist/validation/rules/model_validity_rule.rb +71 -0
  130. data/lib/glossarist/validation/rules/orphaned_bibliography_rule.rb +1 -13
  131. data/lib/glossarist/validation/rules/orphaned_images_rule.rb +16 -11
  132. data/lib/glossarist/validation/rules/ref_shape_rule.rb +68 -0
  133. data/lib/glossarist/validation/rules/related_concept_cycle_rule.rb +1 -3
  134. data/lib/glossarist/validation/rules/related_concept_symmetry_rule.rb +1 -3
  135. data/lib/glossarist/validation/rules/related_concept_target_rule.rb +64 -0
  136. data/lib/glossarist/validation/rules/schema_version_rule.rb +41 -0
  137. data/lib/glossarist/validation/rules/source_type_rule.rb +1 -15
  138. data/lib/glossarist/validation/rules/source_urn_format_rule.rb +65 -0
  139. data/lib/glossarist/validation/rules/uuid_format_rule.rb +33 -0
  140. data/lib/glossarist/validation/rules.rb +10 -43
  141. data/lib/glossarist/validation/validation_issue.rb +14 -11
  142. data/lib/glossarist/validation_result.rb +12 -22
  143. data/lib/glossarist/version.rb +1 -1
  144. data/lib/glossarist.rb +10 -0
  145. data/memory/project-status.md +43 -0
  146. data/scripts/migrate_dataset.rb +180 -0
  147. data/scripts/migrate_isotc204_to_v3.rb +134 -0
  148. data/scripts/migrate_isotc211_to_v3.rb +153 -0
  149. data/scripts/migrate_osgeo_to_v3.rb +155 -0
  150. data/scripts/upgrade_dataset_to_v3.rb +47 -0
  151. metadata +112 -6
  152. data/TODO.integration/01-gcr-package-cli.md +0 -180
  153. data/lib/glossarist/rdf/skos_concept.rb +0 -43
  154. data/lib/glossarist/rdf/skos_vocabulary.rb +0 -25
  155. data/lib/glossarist/transforms/concept_to_skos_transform.rb +0 -131
@@ -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
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Extends Lutaml::Rdf::Mapping with support for multiple rdf:type values.
4
+ #
5
+ # Usage in rdf do blocks:
6
+ # type "gloss:Concept" # single type (original API)
7
+ # types "gloss:Concept", "skos:Concept" # multiple types (new API)
8
+ module Lutaml
9
+ module Rdf
10
+ class Mapping
11
+ attr_writer :namespace_set, :rdf_subject, :rdf_type, :rdf_predicates,
12
+ :rdf_members
13
+
14
+ def types(*values)
15
+ @rdf_types = values.flatten.map(&:to_s)
16
+ end
17
+
18
+ def rdf_types
19
+ if defined?(@rdf_types) && @rdf_types
20
+ @rdf_types
21
+ elsif @rdf_type
22
+ [@rdf_type.to_s]
23
+ else
24
+ []
25
+ end
26
+ end
27
+
28
+ def rdf_types=(values)
29
+ @rdf_types = values
30
+ end
31
+
32
+ def has_types_or_predicates?
33
+ rdf_types.any? || rdf_predicates.any?
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Extends Lutaml::Rdf::MappingRule with `as:` option for URI-valued predicates.
4
+ #
5
+ # Usage:
6
+ # predicate :hasStatus, namespace: Ns, to: :status, as: :uri
7
+ # predicate :name, namespace: Ns, to: :name # default: :literal
8
+ module Lutaml
9
+ module Rdf
10
+ class MappingRule
11
+ attr_reader :as
12
+
13
+ def initialize(predicate_name, namespace:, to:, lang_tagged: false, as: :literal)
14
+ validate!(predicate_name, namespace, to)
15
+ @predicate_name = predicate_name.to_s.freeze
16
+ @namespace = namespace
17
+ @to = to
18
+ @lang_tagged = lang_tagged
19
+ @as = as
20
+ end
21
+
22
+ def uri_value?
23
+ @as == :uri
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Extends Lutaml::Rdf::MemberRule with `inline:` and `link:` options.
4
+ #
5
+ # Usage:
6
+ # members :localizations, link: "gloss:hasLocalization"
7
+ # members :designations, link: ->(d) { skosxl_predicate_for(d) }
8
+ # members :definitions, link: "gloss:hasDefinition"
9
+ # members :sources # no linking predicate
10
+ module Lutaml
11
+ module Rdf
12
+ class MemberRule
13
+ attr_reader :attr_name, :inline, :link
14
+
15
+ def initialize(attr_name, inline: true, link: nil)
16
+ @attr_name = attr_name.to_sym
17
+ @inline = inline
18
+ @link = link
19
+ end
20
+
21
+ def link_predicate_for(member, mapping)
22
+ return nil unless @link
23
+
24
+ case @link
25
+ when String
26
+ mapping.namespace_set.resolve_compact_iri(@link)
27
+ when Proc
28
+ uri = @link.call(member)
29
+ uri.include?(":") ? mapping.namespace_set.resolve_compact_iri(uri) : uri
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,222 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rdf/turtle"
4
+
5
+ # Extends Lutaml::Turtle::Transform to handle:
6
+ # - Multiple rdf:type values (types method)
7
+ # - URI-valued predicates (as: :uri)
8
+ # - Reference-only members (inline: false)
9
+ # - Linking predicates for members (link option)
10
+ # - Recursive prefix collection
11
+ # - Polymorphic member collections
12
+ module Lutaml
13
+ module Turtle
14
+ class Transform < Lutaml::Rdf::Transform
15
+ def model_to_data(instance, _format, options = {})
16
+ mapping = extract_turtle_mapping(options)
17
+ return "" unless mapping
18
+
19
+ if !mapping.rdf_subject && mapping.has_types_or_predicates? && mapping.rdf_members.empty?
20
+ raise MissingSubjectError,
21
+ "Turtle mapping requires a subject block"
22
+ end
23
+
24
+ graph = build_graph(mapping, instance)
25
+ return "" if graph.empty?
26
+
27
+ prefixes = collect_all_prefixes(mapping, instance)
28
+ RDF::Turtle::Writer.buffer(prefixes: prefixes) do |writer|
29
+ graph.each_statement { |stmt| writer << stmt }
30
+ end.strip
31
+ end
32
+
33
+ def data_to_model(data, _format, options = {})
34
+ mapping = extract_turtle_mapping(options)
35
+ unless mapping&.rdf_subject
36
+ raise MissingSubjectError,
37
+ "Turtle mapping requires a subject block"
38
+ end
39
+
40
+ graph = data.is_a?(RDF::Graph) ? data : Lutaml::Turtle::Adapter.parse(data)
41
+ attrs = extract_attributes(graph, mapping)
42
+ build_instance(attrs, options)
43
+ end
44
+
45
+ private
46
+
47
+ def extract_turtle_mapping(options)
48
+ options[:mappings] || mappings_for(:turtle, lutaml_register)
49
+ end
50
+
51
+ def build_graph(mapping, instance)
52
+ graph = RDF::Graph.new
53
+
54
+ if mapping.has_types_or_predicates?
55
+ subject_uri = if mapping.rdf_subject
56
+ RDF::URI(resolve_subject_uri(mapping, instance))
57
+ else
58
+ RDF::Node.new
59
+ end
60
+
61
+ emit_type_statements(graph, subject_uri, mapping)
62
+ emit_predicate_statements(graph, subject_uri, instance, mapping)
63
+ emit_member_link_statements(graph, subject_uri, instance, mapping)
64
+ emit_relationship_statements(graph, subject_uri, instance)
65
+ end
66
+
67
+ emit_child_resources(graph, instance, mapping)
68
+
69
+ graph
70
+ end
71
+
72
+ def emit_type_statements(graph, subject_uri, mapping)
73
+ mapping.rdf_types.each do |type_str|
74
+ type_uri = RDF::URI(mapping.namespace_set.resolve_compact_iri(type_str))
75
+ graph << RDF::Statement.new(subject_uri, RDF.type, type_uri)
76
+ end
77
+ end
78
+
79
+ def emit_predicate_statements(graph, subject_uri, instance, mapping)
80
+ mapping.rdf_predicates.each do |rule|
81
+ value = instance.public_send(rule.to)
82
+ next if value.nil?
83
+
84
+ Array(value).each do |v|
85
+ next if v.is_a?(String) && v.empty?
86
+
87
+ object = build_rdf_object(v, rule, mapping)
88
+ graph << RDF::Statement.new(subject_uri, RDF::URI(rule.uri), object)
89
+ end
90
+ end
91
+ end
92
+
93
+ def emit_member_link_statements(graph, subject_uri, instance, mapping)
94
+ mapping.rdf_members.each do |member_rule|
95
+ next unless member_rule.link
96
+
97
+ collection = Array(instance.public_send(member_rule.attr_name))
98
+ collection.each do |member|
99
+ child_mapping = member.class.mappings[:turtle]
100
+ next unless child_mapping&.rdf_subject
101
+
102
+ child_uri = RDF::URI(resolve_subject_uri(child_mapping, member))
103
+ link_uri = RDF::URI(member_rule.link_predicate_for(member, mapping))
104
+ next unless link_uri
105
+
106
+ graph << RDF::Statement.new(subject_uri, link_uri, child_uri)
107
+ end
108
+ end
109
+ end
110
+
111
+ def emit_relationship_statements(graph, subject_uri, instance)
112
+ return unless instance.is_a?(Glossarist::Rdf::Relationships)
113
+
114
+ Array(instance.relationship_triples).each do |pred_uri, obj_uri|
115
+ graph << RDF::Statement.new(subject_uri, RDF::URI(pred_uri), RDF::URI(obj_uri))
116
+ end
117
+ end
118
+
119
+ def emit_child_resources(graph, instance, mapping)
120
+ mapping.rdf_members.each do |member_rule|
121
+ collection = Array(instance.public_send(member_rule.attr_name))
122
+ collection.each do |member|
123
+ member_mapping = member.class.mappings[:turtle]
124
+ next unless member_mapping
125
+
126
+ graph << build_graph(member_mapping, member)
127
+ end
128
+ end
129
+ end
130
+
131
+ def build_rdf_object(value, rule, mapping = nil)
132
+ if rule.uri_value?
133
+ resolved = if mapping && value.include?(":")
134
+ mapping.namespace_set.resolve_compact_iri(value)
135
+ else
136
+ value
137
+ end
138
+ RDF::URI(resolved)
139
+ elsif rule.lang_tagged
140
+ lang = extract_language(value)
141
+ RDF::Literal.new(value.to_s, language: lang)
142
+ else
143
+ case value
144
+ when Integer then RDF::Literal.new(value, datatype: RDF::XSD.integer)
145
+ when Float then RDF::Literal.new(value, datatype: RDF::XSD.double)
146
+ when TrueClass, FalseClass then RDF::Literal.new(value, datatype: RDF::XSD.boolean)
147
+ else RDF::Literal.new(value.to_s)
148
+ end
149
+ end
150
+ end
151
+
152
+ def collect_all_prefixes(mapping, instance)
153
+ ns_set = collect_namespaces_recursive(mapping, instance)
154
+ ns_set.each.with_object({}) do |ns, h|
155
+ h[ns.prefix.to_sym] = ns.uri if ns.prefix && ns.uri
156
+ end
157
+ end
158
+
159
+ def collect_namespaces_recursive(mapping, instance)
160
+ ns_set = mapping.namespace_set
161
+
162
+ mapping.rdf_members.each do |member_rule|
163
+ collection = Array(instance.public_send(member_rule.attr_name))
164
+ next if collection.empty?
165
+
166
+ collection.map(&:class).uniq.each do |klass|
167
+ member_mapping = klass.mappings[:turtle]
168
+ next unless member_mapping
169
+
170
+ ns_set = ns_set.merge(member_mapping.namespace_set)
171
+ # Recurse into child members
172
+ child_ns = collect_namespaces_recursive(member_mapping, collection.first)
173
+ ns_set = ns_set.merge(child_ns)
174
+ end
175
+ end
176
+
177
+ ns_set
178
+ end
179
+
180
+ def extract_attributes(graph, mapping)
181
+ attrs = {}
182
+ first_type_uri = mapping.rdf_types.first
183
+ return attrs unless first_type_uri
184
+
185
+ type_uri = RDF::URI(mapping.namespace_set.resolve_compact_iri(first_type_uri))
186
+ matching_subjects = find_subjects_by_type(graph, type_uri)
187
+
188
+ matching_subjects.each do |subject|
189
+ mapping.rdf_predicates.each do |rule|
190
+ stmts = graph.query([subject, RDF::URI(rule.uri), nil])
191
+ next if stmts.empty?
192
+
193
+ values = stmts.map do |s|
194
+ rule.uri_value? ? s.object.to_s : literal_to_ruby(s.object)
195
+ end
196
+ attrs[rule.to] = values.length == 1 ? values.first : values
197
+ end
198
+ end
199
+
200
+ attrs
201
+ end
202
+
203
+ def find_subjects_by_type(graph, type_uri)
204
+ graph.query([nil, RDF.type, RDF::URI(type_uri)]).map(&:subject).uniq
205
+ end
206
+
207
+ def literal_to_ruby(rdf_object)
208
+ case rdf_object
209
+ when RDF::Literal
210
+ case rdf_object.datatype
211
+ when RDF::XSD.integer then rdf_object.value.to_i
212
+ when RDF::XSD.double, RDF::XSD.decimal, RDF::XSD.float then rdf_object.value.to_f
213
+ when RDF::XSD.boolean then rdf_object.value == "true"
214
+ else rdf_object.value
215
+ end
216
+ else
217
+ rdf_object.to_s
218
+ end
219
+ end
220
+ end
221
+ end
222
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/turtle"
4
+ require "lutaml/jsonld"
5
+
6
+ require_relative "ext/mapping_ext"
7
+ require_relative "ext/mapping_rule_ext"
8
+ require_relative "ext/member_rule_ext"
9
+ require_relative "ext/turtle_transform_ext"
10
+ require_relative "ext/jsonld_transform_ext"
11
+
12
+ # Update Lutaml::Rdf::Mapping#predicate to pass through `as` option.
13
+ # Update Lutaml::Rdf::Mapping#members to pass through `inline` and `link` options.
14
+ Lutaml::Rdf::Mapping.class_eval do
15
+ def predicate(name, namespace:, to:, lang_tagged: false, as: :literal)
16
+ @rdf_predicates << Lutaml::Rdf::MappingRule.new(
17
+ name,
18
+ namespace: namespace,
19
+ to: to,
20
+ lang_tagged: lang_tagged,
21
+ as: as,
22
+ )
23
+ end
24
+
25
+ def members(attr_name, inline: true, link: nil)
26
+ @rdf_members << Lutaml::Rdf::MemberRule.new(attr_name, inline: inline, link: link)
27
+ end
28
+
29
+ def deep_dup
30
+ self.class.new.tap do |new_mapping|
31
+ new_mapping.namespace_set = @namespace_set
32
+ new_mapping.rdf_subject = @rdf_subject
33
+ new_mapping.rdf_type = @rdf_type
34
+ new_mapping.rdf_types = @rdf_types&.dup
35
+ new_mapping.rdf_predicates = @rdf_predicates.dup
36
+ new_mapping.rdf_members = @rdf_members.dup
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/model"
4
+
5
+ module Glossarist
6
+ module Rdf
7
+ class GlossCitation < Lutaml::Model::Serializable
8
+ attribute :source, :string
9
+ attribute :id, :string
10
+ attribute :version, :string
11
+ attribute :link, :string
12
+ attribute :locality, GlossLocality
13
+
14
+ rdf do
15
+ namespace Namespaces::GlossaristNamespace
16
+
17
+ subject { |c| "citation/#{GlossCitation.slug(c)}" }
18
+
19
+ types "gloss:Citation"
20
+
21
+ predicate :citationSource, namespace: Namespaces::GlossaristNamespace, to: :source
22
+ predicate :citationId, namespace: Namespaces::GlossaristNamespace, to: :id
23
+ predicate :citationVersion, namespace: Namespaces::GlossaristNamespace, to: :version
24
+ predicate :citationLink, namespace: Namespaces::GlossaristNamespace, to: :link
25
+
26
+ members :locality, link: "gloss:hasLocality"
27
+ end
28
+
29
+ def self.slug(citation)
30
+ slug = [citation.source, citation.id].compact.join("/")
31
+ slug = Digest::MD5.hexdigest(citation.source || "")[0..11] if slug.empty?
32
+ slug
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/model"
4
+
5
+ module Glossarist
6
+ module Rdf
7
+ class GlossConcept < Lutaml::Model::Serializable
8
+ include Relationships
9
+
10
+ attribute :identifier, :string
11
+ attribute :status, :string
12
+ attribute :localizations, GlossLocalizedConcept, collection: true
13
+ attribute :sources, GlossConceptSource, collection: true
14
+ attribute :domains, GlossConceptReference, collection: true
15
+ attribute :dates, GlossConceptDate, collection: true
16
+
17
+ rdf do
18
+ namespace Namespaces::GlossaristNamespace,
19
+ Namespaces::SkosNamespace,
20
+ Namespaces::DctermsNamespace,
21
+ Namespaces::SkosxlNamespace,
22
+ Namespaces::IsoThesNamespace,
23
+ Namespaces::RdfNamespace
24
+
25
+ subject { |c| "concept/#{c.identifier}" }
26
+
27
+ types "gloss:Concept", "skos:Concept"
28
+
29
+ predicate :identifier, namespace: Namespaces::GlossaristNamespace, to: :identifier
30
+ predicate :hasStatus, namespace: Namespaces::GlossaristNamespace, to: :status, as: :uri
31
+
32
+ members :localizations,
33
+ link: "gloss:hasLocalization"
34
+ members :sources,
35
+ link: "gloss:hasSource"
36
+ members :domains,
37
+ link: "gloss:hasDomain"
38
+ members :dates,
39
+ link: "gloss:hasDate"
40
+ end
41
+ end
42
+
43
+ class GlossDocument < Lutaml::Model::Serializable
44
+ attribute :concepts, GlossConcept, collection: true
45
+
46
+ rdf do
47
+ namespace Namespaces::GlossaristNamespace,
48
+ Namespaces::SkosNamespace,
49
+ Namespaces::DctermsNamespace,
50
+ Namespaces::SkosxlNamespace,
51
+ Namespaces::IsoThesNamespace,
52
+ Namespaces::RdfNamespace
53
+
54
+ members :concepts
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/model"
4
+
5
+ module Glossarist
6
+ module Rdf
7
+ class GlossConceptDate < Lutaml::Model::Serializable
8
+ attribute :date_value, :string
9
+ attribute :date_type, :string
10
+ attribute :concept_id, :string
11
+
12
+ rdf do
13
+ namespace Namespaces::GlossaristNamespace
14
+
15
+ subject { |d| "concept/#{d.concept_id}/date/#{d.date_type}" }
16
+
17
+ types "gloss:ConceptDate"
18
+
19
+ predicate :dateValue, namespace: Namespaces::GlossaristNamespace, to: :date_value
20
+ predicate :dateType, namespace: Namespaces::GlossaristNamespace, to: :date_type, as: :uri
21
+ end
22
+ end
23
+ end
24
+ end