forked from openfoodfoundation/openfoodnetwork
-
Notifications
You must be signed in to change notification settings - Fork 0
/
importer.rb
161 lines (129 loc) · 4.7 KB
/
importer.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
# frozen_string_literal: true
require_relative "skos_parser"
module DataFoodConsortium
module Connector
class Importer # rubocop:disable Metrics/ClassLength
TYPES = [
DataFoodConsortium::Connector::CatalogItem,
DataFoodConsortium::Connector::Enterprise,
DataFoodConsortium::Connector::Offer,
DataFoodConsortium::Connector::Person,
DataFoodConsortium::Connector::QuantitativeValue,
DataFoodConsortium::Connector::SuppliedProduct,
].freeze
def self.type_map
unless @type_map
@type_map = {}
TYPES.each(&method(:register_type))
end
@type_map
end
def self.register_type(clazz)
# Methods with variable arguments have a negative arity of -n-1
# where n is the number of required arguments.
number_of_required_args = -1 * (clazz.instance_method(:initialize).arity + 1)
args = Array.new(number_of_required_args)
type_uri = clazz.new(*args).semanticType
type_map[type_uri] = clazz
end
def self.prefixed_name(uri)
# When we skip backwards compatibility, we can just do this:
#
# key = RDF::URI.new(uri).pname(prefixes: Context::VERSION_1_8)
#
# But for now we do it manually.
uri.gsub(
"https://github.com/datafoodconsortium/ontology/releases/latest/download/DFC_BusinessOntology.owl#",
"dfc-b:"
).gsub(
# legacy URI
"http://static.datafoodconsortium.org/ontologies/DFC_BusinessOntology.owl#",
"dfc-b:"
)
end
def import(json_string_or_io)
@subjects = {}
graph = parse_rdf(json_string_or_io)
build_subjects(graph)
apply_statements(graph)
if @subjects.size > 1
@subjects.values
else
@subjects.values.first
end
end
private
# The `io` parameter can be a String or an IO instance.
def parse_rdf(io)
io = StringIO.new(io) if io.is_a?(String)
RDF::Graph.new << JSON::LD::API.toRdf(io)
end
def build_subjects(graph)
graph.query({ predicate: RDF.type }).each do |statement|
@subjects[statement.subject] = build_subject(statement)
end
end
def build_subject(type_statement)
# Not all subjects have an id, some are anonymous.
id = type_statement.subject.try(:value)
type = type_statement.object.value
key = self.class.prefixed_name(type)
clazz = self.class.type_map[key]
clazz.new(*[id].compact)
end
def apply_statements(statements)
statements.each do |statement|
apply_statement(statement)
end
end
def apply_statement(statement)
subject = subject_of(statement)
property_uri = statement.predicate.value
value = resolve_object(statement.object)
property_id = self.class.prefixed_name(property_uri)
return unless subject.hasSemanticProperty?(property_id)
property = subject.semanticProperty(property_id)
if property.value.is_a?(Enumerable)
property.value << value
else
setter = guess_setter_name(statement)
subject.try(setter, value) if setter
end
end
def subject_of(statement)
@subjects[statement.subject]
end
def resolve_object(object)
@subjects[object] || skos_concept(object) || object.object
end
def skos_concept(object)
return unless object.uri?
id = object.value.sub(
"http://static.datafoodconsortium.org/data/measures.rdf#", "dfc-m:"
).sub(
"https://github.com/datafoodconsortium/taxonomies/releases/latest/download/measures.rdf#",
"dfc-m:"
)
SKOSParser.concepts[id]
end
def guess_setter_name(statement)
predicate = statement.predicate
# Ideally the product models would be consitent with the rule below and use "type"
# instead of "productType" but alast they are not so we need this exception
return "productType=" if predicate.fragment == "hasType" && product_type?(statement)
name =
# Some predicates are named like `hasQuantity`
# but the attribute name would be `quantity`.
predicate.fragment&.sub(/^has/, "")&.camelize(:lower) ||
# And sometimes the URI looks like `ofn:spree_product_id`.
predicate.to_s.split(":").last
"#{name}="
end
def product_type?(statement)
return true if statement.object.literal? && statement.object.value.match("dfc-pt")
return true if statement.object.path.match("productTypes")
false
end
end
end
end