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.
- checksums.yaml +4 -4
- data/.github/workflows/release.yml +1 -4
- data/.rubocop_todo.yml +53 -2
- data/CLAUDE.md +27 -2
- data/README.adoc +532 -56
- data/config.yml +68 -1
- data/glossarist.gemspec +2 -0
- data/lib/glossarist/citation.rb +26 -123
- data/lib/glossarist/cli/compare_command.rb +106 -0
- data/lib/glossarist/cli/export_command.rb +11 -14
- data/lib/glossarist/cli/validate_command.rb +111 -20
- data/lib/glossarist/cli.rb +18 -0
- data/lib/glossarist/collections/bibliography_collection.rb +4 -2
- data/lib/glossarist/collections/localization_collection.rb +2 -0
- data/lib/glossarist/comparison_result.rb +35 -0
- data/lib/glossarist/concept.rb +1 -1
- data/lib/glossarist/concept_collector.rb +44 -0
- data/lib/glossarist/concept_comparator.rb +72 -0
- data/lib/glossarist/concept_data.rb +20 -0
- data/lib/glossarist/concept_diff.rb +15 -0
- data/lib/glossarist/concept_document.rb +11 -0
- data/lib/glossarist/concept_manager.rb +19 -5
- data/lib/glossarist/concept_ref.rb +13 -0
- data/lib/glossarist/concept_reference.rb +12 -19
- data/lib/glossarist/concept_validator.rb +6 -1
- data/lib/glossarist/context_configuration.rb +90 -0
- data/lib/glossarist/dataset_validator.rb +8 -4
- data/lib/glossarist/designation/abbreviation.rb +0 -2
- data/lib/glossarist/designation/base.rb +21 -1
- data/lib/glossarist/designation/expression.rb +3 -0
- data/lib/glossarist/designation/letter_symbol.rb +0 -4
- data/lib/glossarist/designation/prefix.rb +17 -0
- data/lib/glossarist/designation/suffix.rb +17 -0
- data/lib/glossarist/designation/symbol.rb +0 -2
- data/lib/glossarist/gcr_metadata.rb +7 -14
- data/lib/glossarist/gcr_package.rb +35 -23
- data/lib/glossarist/gcr_validator.rb +38 -17
- data/lib/glossarist/glossary_definition.rb +5 -0
- data/lib/glossarist/localized_concept.rb +8 -0
- data/lib/glossarist/managed_concept.rb +39 -6
- data/lib/glossarist/managed_concept_data.rb +22 -2
- data/lib/glossarist/non_verb_rep.rb +21 -6
- data/lib/glossarist/pronunciation.rb +32 -0
- data/lib/glossarist/rdf/ext/jsonld_transform_ext.rb +208 -0
- data/lib/glossarist/rdf/ext/mapping_ext.rb +37 -0
- data/lib/glossarist/rdf/ext/mapping_rule_ext.rb +27 -0
- data/lib/glossarist/rdf/ext/member_rule_ext.rb +34 -0
- data/lib/glossarist/rdf/ext/turtle_transform_ext.rb +222 -0
- data/lib/glossarist/rdf/ext.rb +39 -0
- data/lib/glossarist/rdf/gloss_citation.rb +36 -0
- data/lib/glossarist/rdf/gloss_concept.rb +58 -0
- data/lib/glossarist/rdf/gloss_concept_date.rb +24 -0
- data/lib/glossarist/rdf/gloss_concept_reference.rb +29 -0
- data/lib/glossarist/rdf/gloss_concept_source.rb +37 -0
- data/lib/glossarist/rdf/gloss_designation.rb +146 -0
- data/lib/glossarist/rdf/gloss_detailed_definition.rb +24 -0
- data/lib/glossarist/rdf/gloss_grammar_info.rb +57 -0
- data/lib/glossarist/rdf/gloss_locality.rb +25 -0
- data/lib/glossarist/rdf/gloss_localized_concept.rb +67 -0
- data/lib/glossarist/rdf/gloss_non_verbal_rep.rb +31 -0
- data/lib/glossarist/rdf/gloss_pronunciation.rb +32 -0
- data/lib/glossarist/rdf/gloss_reference.rb +55 -0
- data/lib/glossarist/rdf/namespaces/glossarist_namespace.rb +12 -0
- data/lib/glossarist/rdf/namespaces/iso_thes_namespace.rb +12 -0
- data/lib/glossarist/rdf/namespaces/owl_namespace.rb +12 -0
- data/lib/glossarist/rdf/namespaces/prov_namespace.rb +12 -0
- data/lib/glossarist/rdf/namespaces/rdf_namespace.rb +12 -0
- data/lib/glossarist/rdf/namespaces/skosxl_namespace.rb +12 -0
- data/lib/glossarist/rdf/namespaces.rb +8 -2
- data/lib/glossarist/rdf/relationships.rb +19 -0
- data/lib/glossarist/rdf/v3/configuration.rb +15 -0
- data/lib/glossarist/rdf/v3.rb +79 -0
- data/lib/glossarist/rdf.rb +22 -2
- data/lib/glossarist/reference_extractor.rb +15 -24
- data/lib/glossarist/reference_resolver.rb +3 -3
- data/lib/glossarist/related_concept.rb +2 -10
- data/lib/glossarist/schema_migration.rb +39 -0
- data/lib/glossarist/sts/term_mapper.rb +2 -2
- data/lib/glossarist/transforms/concept_to_gloss_transform.rb +355 -0
- data/lib/glossarist/transforms.rb +2 -2
- data/lib/glossarist/urn_resolver.rb +13 -1
- data/lib/glossarist/v1/concept.rb +18 -11
- data/lib/glossarist/v2/citation.rb +36 -0
- data/lib/glossarist/v2/concept_data.rb +46 -0
- data/lib/glossarist/v2/concept_document.rb +18 -0
- data/lib/glossarist/v2/concept_ref.rb +8 -0
- data/lib/glossarist/v2/concept_source.rb +16 -0
- data/lib/glossarist/v2/configuration.rb +13 -0
- data/lib/glossarist/v2/detailed_definition.rb +14 -0
- data/lib/glossarist/v2/localized_concept.rb +9 -0
- data/lib/glossarist/v2/managed_concept.rb +25 -0
- data/lib/glossarist/v2/managed_concept_data.rb +49 -0
- data/lib/glossarist/v2/related_concept.rb +15 -0
- data/lib/glossarist/v2.rb +28 -0
- data/lib/glossarist/v3/bibliography_entry.rb +19 -0
- data/lib/glossarist/v3/bibliography_file.rb +27 -0
- data/lib/glossarist/v3/citation.rb +30 -0
- data/lib/glossarist/v3/concept_data.rb +46 -0
- data/lib/glossarist/v3/concept_document.rb +18 -0
- data/lib/glossarist/v3/concept_ref.rb +8 -0
- data/lib/glossarist/v3/concept_source.rb +16 -0
- data/lib/glossarist/v3/configuration.rb +13 -0
- data/lib/glossarist/v3/detailed_definition.rb +14 -0
- data/lib/glossarist/v3/image_entry.rb +21 -0
- data/lib/glossarist/v3/image_file.rb +31 -0
- data/lib/glossarist/v3/localized_concept.rb +9 -0
- data/lib/glossarist/v3/managed_concept.rb +26 -0
- data/lib/glossarist/v3/managed_concept_data.rb +34 -0
- data/lib/glossarist/v3/related_concept.rb +15 -0
- data/lib/glossarist/v3.rb +36 -0
- data/lib/glossarist/validation/asset_index.rb +4 -3
- data/lib/glossarist/validation/bibliography_index.rb +61 -30
- data/lib/glossarist/validation/rules/asciidoc_xref_rule.rb +2 -15
- data/lib/glossarist/validation/rules/authoritative_source_rule.rb +2 -15
- data/lib/glossarist/validation/rules/base.rb +5 -0
- data/lib/glossarist/validation/rules/bibliography_yaml_rule.rb +2 -3
- data/lib/glossarist/validation/rules/citation_completeness_rule.rb +5 -27
- data/lib/glossarist/validation/rules/dataset_context.rb +8 -3
- data/lib/glossarist/validation/rules/date_validity_rule.rb +1 -1
- data/lib/glossarist/validation/rules/designation_status_rule.rb +0 -1
- data/lib/glossarist/validation/rules/designation_type_rule.rb +1 -5
- data/lib/glossarist/validation/rules/domain_ref_rule.rb +37 -0
- data/lib/glossarist/validation/rules/domain_target_rule.rb +56 -0
- data/lib/glossarist/validation/rules/gcr_context.rb +12 -13
- data/lib/glossarist/validation/rules/image_reference_rule.rb +2 -17
- data/lib/glossarist/validation/rules/locality_completeness_rule.rb +58 -0
- data/lib/glossarist/validation/rules/localization_consistency_rule.rb +72 -0
- data/lib/glossarist/validation/rules/localization_presence_rule.rb +1 -1
- data/lib/glossarist/validation/rules/model_validity_rule.rb +71 -0
- data/lib/glossarist/validation/rules/orphaned_bibliography_rule.rb +1 -13
- data/lib/glossarist/validation/rules/orphaned_images_rule.rb +16 -11
- data/lib/glossarist/validation/rules/ref_shape_rule.rb +68 -0
- data/lib/glossarist/validation/rules/related_concept_cycle_rule.rb +1 -3
- data/lib/glossarist/validation/rules/related_concept_symmetry_rule.rb +1 -3
- data/lib/glossarist/validation/rules/related_concept_target_rule.rb +64 -0
- data/lib/glossarist/validation/rules/schema_version_rule.rb +41 -0
- data/lib/glossarist/validation/rules/source_type_rule.rb +1 -15
- data/lib/glossarist/validation/rules/source_urn_format_rule.rb +65 -0
- data/lib/glossarist/validation/rules/uuid_format_rule.rb +33 -0
- data/lib/glossarist/validation/rules.rb +10 -43
- data/lib/glossarist/validation/validation_issue.rb +14 -11
- data/lib/glossarist/validation_result.rb +12 -22
- data/lib/glossarist/version.rb +1 -1
- data/lib/glossarist.rb +10 -0
- data/memory/project-status.md +43 -0
- data/scripts/migrate_dataset.rb +180 -0
- data/scripts/migrate_isotc204_to_v3.rb +134 -0
- data/scripts/migrate_isotc211_to_v3.rb +153 -0
- data/scripts/migrate_osgeo_to_v3.rb +155 -0
- data/scripts/upgrade_dataset_to_v3.rb +47 -0
- metadata +112 -6
- data/TODO.integration/01-gcr-package-cli.md +0 -180
- data/lib/glossarist/rdf/skos_concept.rb +0 -43
- data/lib/glossarist/rdf/skos_vocabulary.rb +0 -25
- 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
|