dfc-linkml-connector 2.0.6

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 (104) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +78 -0
  4. data/contexts/context_2.0.0.json +544 -0
  5. data/lib/core/connector.rb +670 -0
  6. data/lib/core/json_ld_serializer.rb +92 -0
  7. data/lib/core/semantic_object.rb +103 -0
  8. data/lib/core/vocabulary_loader.rb +125 -0
  9. data/lib/dfc_linkml_connector.rb +102 -0
  10. data/lib/models/address.rb +68 -0
  11. data/lib/models/agent.rb +83 -0
  12. data/lib/models/allergen_characteristic.rb +57 -0
  13. data/lib/models/as_planned_consumption_flow.rb +25 -0
  14. data/lib/models/as_planned_local_consumption_flow.rb +25 -0
  15. data/lib/models/as_planned_local_production_flow.rb +25 -0
  16. data/lib/models/as_planned_local_transformation.rb +53 -0
  17. data/lib/models/as_planned_production_flow.rb +25 -0
  18. data/lib/models/as_planned_transformation.rb +38 -0
  19. data/lib/models/as_realized_consumption_flow.rb +25 -0
  20. data/lib/models/as_realized_production_flow.rb +25 -0
  21. data/lib/models/as_realized_transformation.rb +48 -0
  22. data/lib/models/brand.rb +33 -0
  23. data/lib/models/catalog.rb +43 -0
  24. data/lib/models/catalog_item.rb +93 -0
  25. data/lib/models/certfication.rb +43 -0
  26. data/lib/models/collection.rb +47 -0
  27. data/lib/models/concept.rb +82 -0
  28. data/lib/models/concept_scheme.rb +47 -0
  29. data/lib/models/consumption_flow.rb +67 -0
  30. data/lib/models/coordination.rb +62 -0
  31. data/lib/models/customer_category.rb +38 -0
  32. data/lib/models/defined_product.rb +178 -0
  33. data/lib/models/delivery_option.rb +48 -0
  34. data/lib/models/delivery_step.rb +25 -0
  35. data/lib/models/ditributed_representation.rb +47 -0
  36. data/lib/models/enterprise.rb +29 -0
  37. data/lib/models/feature.rb +57 -0
  38. data/lib/models/functional_product.rb +33 -0
  39. data/lib/models/geometry.rb +52 -0
  40. data/lib/models/how_subject.rb +47 -0
  41. data/lib/models/individual.rb +47 -0
  42. data/lib/models/ingredient.rb +38 -0
  43. data/lib/models/labelling_characteristic.rb +57 -0
  44. data/lib/models/length.rb +25 -0
  45. data/lib/models/localized_product.rb +68 -0
  46. data/lib/models/nutrient_characteristic.rb +57 -0
  47. data/lib/models/offer.rb +82 -0
  48. data/lib/models/opening_hours_specification.rb +62 -0
  49. data/lib/models/order.rb +115 -0
  50. data/lib/models/order_line.rb +86 -0
  51. data/lib/models/organization.rb +92 -0
  52. data/lib/models/payment_method.rb +43 -0
  53. data/lib/models/person.rb +38 -0
  54. data/lib/models/phone_number.rb +38 -0
  55. data/lib/models/physical_characteristic.rb +57 -0
  56. data/lib/models/physical_place.rb +58 -0
  57. data/lib/models/physical_product.rb +78 -0
  58. data/lib/models/pick_up_step.rb +25 -0
  59. data/lib/models/pickup_option.rb +33 -0
  60. data/lib/models/place.rb +28 -0
  61. data/lib/models/platform.rb +25 -0
  62. data/lib/models/point.rb +25 -0
  63. data/lib/models/polygon.rb +25 -0
  64. data/lib/models/price.rb +37 -0
  65. data/lib/models/product_batch.rb +53 -0
  66. data/lib/models/product_option.rb +28 -0
  67. data/lib/models/product_option_value.rb +25 -0
  68. data/lib/models/production_flow.rb +67 -0
  69. data/lib/models/properties.rb +47 -0
  70. data/lib/models/quantitative_value.rb +33 -0
  71. data/lib/models/real_stock.rb +38 -0
  72. data/lib/models/representation_pivot.rb +25 -0
  73. data/lib/models/represented_thing.rb +25 -0
  74. data/lib/models/route.rb +38 -0
  75. data/lib/models/sale_session.rb +92 -0
  76. data/lib/models/shipment.rb +77 -0
  77. data/lib/models/shipping_option.rb +58 -0
  78. data/lib/models/social_media.rb +33 -0
  79. data/lib/models/step.rb +48 -0
  80. data/lib/models/stock.rb +67 -0
  81. data/lib/models/supplied_product.rb +73 -0
  82. data/lib/models/technical_product.rb +38 -0
  83. data/lib/models/temperature.rb +28 -0
  84. data/lib/models/template_sale_session.rb +57 -0
  85. data/lib/models/theoritical_stock.rb +33 -0
  86. data/lib/models/transaction.rb +62 -0
  87. data/lib/models/transformation.rb +25 -0
  88. data/lib/models/value_r_e_c_u_r.rb +67 -0
  89. data/lib/models/variant.rb +33 -0
  90. data/lib/models/variant_caracteristic.rb +33 -0
  91. data/lib/models/vehicle.rb +58 -0
  92. data/lib/models/vevent.rb +62 -0
  93. data/lib/models/virtual_place.rb +33 -0
  94. data/lib/models/volume.rb +25 -0
  95. data/lib/models/weight.rb +25 -0
  96. data/lib/models/what_subject.rb +47 -0
  97. data/lib/models/where_subject.rb +47 -0
  98. data/lib/models/who_subject.rb +47 -0
  99. data/vocabularies/facet.jsonld +4130 -0
  100. data/vocabularies/measure.jsonld +2586 -0
  101. data/vocabularies/product_type.jsonld +10116 -0
  102. data/vocabularies/scope.jsonld +190 -0
  103. data/vocabularies/vocabulary_term.jsonld +502 -0
  104. metadata +199 -0
@@ -0,0 +1,670 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'net/http'
5
+ require 'uri'
6
+ require_relative 'vocabulary_loader'
7
+ require_relative 'json_ld_serializer'
8
+ require_relative 'semantic_object'
9
+
10
+ module DfcLinkmlConnector
11
+ module Core
12
+ # Main connector for DFC data interchange.
13
+ # Instantiate with specific ontology/taxonomy versions.
14
+ #
15
+ # connector = Connector.new(ontology_version: "2.0.0", taxonomy_version: "2.0.0")
16
+ # connector.import(json_ld_string)
17
+ # connector.export(some_object)
18
+ class Connector
19
+ ONTOLOGY_BASE_URL = "https://w3id.org/dfc/ontology".freeze
20
+ TAXONOMY_BASE_URL = "https://w3id.org/dfc/taxonomies".freeze
21
+ BUNDLED_CONTEXT_DIR = File.expand_path("../../contexts", __dir__).freeze
22
+
23
+ PREDICATE_MAP = {
24
+ "http://www.w3.org/2002/12/cal/icaltzd#byday" => "byday",
25
+ "http://www.w3.org/2002/12/cal/icaltzd#bymonth" => "bymonth",
26
+ "http://www.w3.org/2002/12/cal/icaltzd#dtend" => "dtend",
27
+ "http://www.w3.org/2002/12/cal/icaltzd#dtstart" => "dtstart",
28
+ "http://www.w3.org/2002/12/cal/icaltzd#freq" => "freq",
29
+ "http://www.w3.org/2002/12/cal/icaltzd#interval" => "interval",
30
+ "https://purl.org/geojson/vocab#coordinates" => "coordinates",
31
+ "https://schema.org/dayOfWeek" => "day_of_week",
32
+ "https://schema.org/opens" => "opens",
33
+ "dfc-b:Image" => "image",
34
+ "dfc-b:URL" => "url",
35
+ "dfc-b:VATnumber" => "vat_number",
36
+ "dfc-b:VATrate" => "vat_rate",
37
+ "dfc-b:VATstatus" => "vat_status",
38
+ "dfc-b:accessibilityInfo" => "accessibility_info",
39
+ "dfc-b:arrivalDate" => "arrival_date",
40
+ "dfc-b:availabilityDate" => "availability_date",
41
+ "dfc-b:availabilityTime" => "availability_time",
42
+ "dfc-b:batchNumber" => "batch_number",
43
+ "dfc-b:bestBeforeDate" => "best_before_date",
44
+ "dfc-b:brand" => "brand",
45
+ "dfc-b:certiferReference" => "certifer_reference",
46
+ "dfc-b:certificationScore" => "certification_score",
47
+ "dfc-b:city" => "city",
48
+ "dfc-b:claim" => "claim",
49
+ "dfc-b:closes" => "closes",
50
+ "dfc-b:cost" => "cost",
51
+ "dfc-b:country" => "country",
52
+ "dfc-b:countryCode" => "country_code",
53
+ "dfc-b:date" => "date",
54
+ "dfc-b:deliveryCondition" => "delivery_condition",
55
+ "dfc-b:deliveryConstraint" => "delivery_constraint",
56
+ "dfc-b:description" => "description",
57
+ "dfc-b:discount" => "discount",
58
+ "dfc-b:duration" => "duration",
59
+ "dfc-b:email" => "email",
60
+ "dfc-b:endDate" => "end_date",
61
+ "dfc-b:enterpriseID" => "enterprise_id",
62
+ "dfc-b:expiryDate" => "expiry_date",
63
+ "dfc-b:extraAvailabilityTime" => "extra_availability_time",
64
+ "dfc-b:extraDeliveryCondition" => "extra_delivery_condition",
65
+ "dfc-b:familyName" => "family_name",
66
+ "dfc-b:fee" => "fee",
67
+ "dfc-b:firstName" => "first_name",
68
+ "dfc-b:frozen" => "frozen",
69
+ "dfc-b:hasPercentageOfAlcoholByVolume" => "percentage_of_alcohol_by_volume",
70
+ "dfc-b:invoiceNumber" => "invoice_number",
71
+ "dfc-b:latitude" => "latitude",
72
+ "dfc-b:lifetime" => "lifetime",
73
+ "dfc-b:logo" => "logo",
74
+ "dfc-b:longitude" => "longitude",
75
+ "dfc-b:marginPercent" => "margin_percent",
76
+ "dfc-b:maxValue" => "max_value",
77
+ "dfc-b:minValue" => "min_value",
78
+ "dfc-b:name" => "name",
79
+ "dfc-b:operatorId" => "operator_id",
80
+ "dfc-b:orderNumber" => "order_number",
81
+ "dfc-b:paymentMethodProvider" => "payment_method_provider",
82
+ "dfc-b:paymentMethodType" => "payment_method_type",
83
+ "dfc-b:phoneNumber" => "phone_number",
84
+ "dfc-b:physicalCharacteristics" => "physical_characteristics",
85
+ "dfc-b:postcode" => "postcode",
86
+ "dfc-b:productionDate" => "production_date",
87
+ "dfc-b:quantity" => "quantity",
88
+ "dfc-b:refrigerated" => "refrigerated",
89
+ "dfc-b:region" => "region",
90
+ "dfc-b:sku" => "sku",
91
+ "dfc-b:specificCondition" => "specific_condition",
92
+ "dfc-b:startDate" => "start_date",
93
+ "dfc-b:stockLimitation" => "stock_limitation",
94
+ "dfc-b:street" => "street",
95
+ "dfc-b:totalTheoriticalStock" => "total_theoritical_stock",
96
+ "dfc-b:value" => "value",
97
+ "dfc-b:websitePage" => "website_page",
98
+ "http://www.w3.org/2002/12/cal/icaltzd#rrule" => "rrule",
99
+ "skos:broader" => "broader",
100
+ "skos:inScheme" => "in_scheme",
101
+ "skos:narrower" => "narrower",
102
+ "https://purl.org/geojson/vocab#geometry" => "geometry",
103
+ "https://purl.org/geojson/vocab#properties" => "properties",
104
+ "dfc-b:DFC_BusinessOntology_ObjectProperty" => "d_f_c__business_ontology__object_property",
105
+ "dfc-b:DFC_Interface_Property" => "d_f_c__interface__property",
106
+ "dfc-b:addressOf" => "address_of",
107
+ "dfc-b:affiliatedTo" => "affiliated_to",
108
+ "dfc-b:affiliates" => "affiliates",
109
+ "dfc-b:allergenCharacteristicOf" => "allergen_characteristic_of",
110
+ "dfc-b:basedAt" => "based_at",
111
+ "dfc-b:belongsTo" => "belongs_to",
112
+ "dfc-b:brandOf" => "brand_of",
113
+ "dfc-b:certificateOf" => "certificate_of",
114
+ "dfc-b:certifies" => "certifies",
115
+ "dfc-b:characteristicOf" => "characteristic_of",
116
+ "dfc-b:claimOf" => "claim_of",
117
+ "dfc-b:composedOf" => "composed_of",
118
+ "dfc-b:composes" => "composes",
119
+ "dfc-b:concernedBy" => "concerned_by",
120
+ "dfc-b:concerns" => "concerns",
121
+ "dfc-b:constituedBy" => "constitued_by",
122
+ "dfc-b:constitutes" => "constitutes",
123
+ "dfc-b:consumedBy" => "consumed_by",
124
+ "dfc-b:consumes" => "consumes",
125
+ "dfc-b:containerInformationOf" => "container_information_of",
126
+ "dfc-b:coordinatedBy" => "coordinated_by",
127
+ "dfc-b:definedBy" => "defined_by",
128
+ "dfc-b:defines" => "defines",
129
+ "dfc-b:deliveredAt" => "delivered_at",
130
+ "dfc-b:delivery" => "delivery",
131
+ "dfc-b:endsAt" => "ends_at",
132
+ "dfc-b:facetOf" => "facet_of",
133
+ "dfc-b:from" => "from",
134
+ "dfc-b:fulfills" => "fulfills",
135
+ "dfc-b:geographicalOriginOf" => "geographical_origin_of",
136
+ "dfc-b:hasAddress" => "address",
137
+ "dfc-b:hasAllergenCharacteristic" => "allergen_characteristic",
138
+ "dfc-b:hasAllergenDimension" => "allergen_dimension",
139
+ "dfc-b:hasBrand" => "has_brand",
140
+ "dfc-b:hasCertification" => "certification",
141
+ "dfc-b:hasCharacteristic" => "characteristic",
142
+ "dfc-b:hasClaim" => "has_claim",
143
+ "dfc-b:hasContainerInformation" => "container_information",
144
+ "dfc-b:hasCountry" => "has_country",
145
+ "dfc-b:hasDimension" => "dimension",
146
+ "dfc-b:hasFacet" => "facet",
147
+ "dfc-b:hasFulfilmentStatus" => "fulfilment_status",
148
+ "dfc-b:hasGeoJsonFeature" => "geo_json_feature",
149
+ "dfc-b:hasGeographicalOrigin" => "geographical_origin",
150
+ "dfc-b:hasIngredient" => "ingredient",
151
+ "dfc-b:hasInput" => "input",
152
+ "dfc-b:hasLabellingCharacteristic" => "labelling_characteristic",
153
+ "dfc-b:hasLabellingDimension" => "labelling_dimension",
154
+ "dfc-b:hasMainContact" => "main_contact",
155
+ "dfc-b:hasMember" => "member",
156
+ "dfc-b:hasNatureOrigin" => "nature_origin",
157
+ "dfc-b:hasNutrientCharacteristic" => "nutrient_characteristic",
158
+ "dfc-b:hasNutrientDimension" => "nutrient_dimension",
159
+ "dfc-b:hasObject" => "object",
160
+ "dfc-b:hasOffer" => "offer",
161
+ "dfc-b:hasOption" => "option",
162
+ "dfc-b:hasOrderStatus" => "order_status",
163
+ "dfc-b:hasOutput" => "output",
164
+ "dfc-b:hasPart" => "part",
165
+ "dfc-b:hasPartOrigin" => "part_origin",
166
+ "dfc-b:hasPaymentMethod" => "payment_method",
167
+ "dfc-b:hasPaymentStatus" => "payment_status",
168
+ "dfc-b:hasPhoneNumber" => "has_phone_number",
169
+ "dfc-b:hasPhysicalCharacteristic" => "physical_characteristic",
170
+ "dfc-b:hasPhysicalDimension" => "physical_dimension",
171
+ "dfc-b:hasPrice" => "price",
172
+ "dfc-b:hasProductOption" => "product_option",
173
+ "dfc-b:hasProductOptionValue" => "product_option_value",
174
+ "dfc-b:hasQuantity" => "has_quantity",
175
+ "dfc-b:hasReference" => "reference",
176
+ "dfc-b:hasReferenceProductOption" => "reference_product_option",
177
+ "dfc-b:hasReferenceProductOptionValue" => "reference_product_option_value",
178
+ "dfc-b:hasSocialMedia" => "social_media",
179
+ "dfc-b:hasStatus" => "status",
180
+ "dfc-b:hasStep" => "step",
181
+ "dfc-b:hasTemperature" => "temperature",
182
+ "dfc-b:hasTemplateSaleSession" => "template_sale_session",
183
+ "dfc-b:hasTransformationType" => "transformation_type",
184
+ "dfc-b:hasType" => "type",
185
+ "dfc-b:hasUnit" => "unit",
186
+ "dfc-b:hasVariant" => "variant",
187
+ "dfc-b:hasVariantCaracteristic" => "variant_caracteristic",
188
+ "dfc-b:holds" => "holds",
189
+ "dfc-b:hostedAt" => "hosted_at",
190
+ "dfc-b:hosts" => "hosts",
191
+ "dfc-b:identifiedBy" => "identified_by",
192
+ "dfc-b:identifies" => "identifies",
193
+ "dfc-b:industrializedBy" => "industrialized_by",
194
+ "dfc-b:industrializes" => "industrializes",
195
+ "dfc-b:inputOf" => "input_of",
196
+ "dfc-b:isAvailableDuring" => "is_available_during",
197
+ "dfc-b:isCertifiedBy" => "is_certified_by",
198
+ "dfc-b:isFulfilledBy" => "is_fulfilled_by",
199
+ "dfc-b:isIngredientOf" => "is_ingredient_of",
200
+ "dfc-b:isMemberOf" => "is_member_of",
201
+ "dfc-b:isOpenDuring" => "is_open_during",
202
+ "dfc-b:isPriceOf" => "is_price_of",
203
+ "dfc-b:isShippedIn" => "is_shipped_in",
204
+ "dfc-b:isStepOf" => "is_step_of",
205
+ "dfc-b:isTemperatureOf" => "is_temperature_of",
206
+ "dfc-b:isTemplateSaleSessionOf" => "is_template_sale_session_of",
207
+ "dfc-b:isVariantOf" => "is_variant_of",
208
+ "dfc-b:labellingCharacteristicOf" => "labelling_characteristic_of",
209
+ "dfc-b:listedIn" => "listed_in",
210
+ "dfc-b:lists" => "lists",
211
+ "dfc-b:localizedBy" => "localized_by",
212
+ "dfc-b:localizes" => "localizes",
213
+ "dfc-b:mainContactOf" => "main_contact_of",
214
+ "dfc-b:maintainedBy" => "maintained_by",
215
+ "dfc-b:maintains" => "maintains",
216
+ "dfc-b:managedBy" => "managed_by",
217
+ "dfc-b:manages" => "manages",
218
+ "dfc-b:natureOriginOf" => "nature_origin_of",
219
+ "dfc-b:nutrientCharacteristicOf" => "nutrient_characteristic_of",
220
+ "dfc-b:objectOf" => "object_of",
221
+ "dfc-b:offeredThrough" => "offered_through",
222
+ "dfc-b:offers" => "offers",
223
+ "dfc-b:offersTo" => "offers_to",
224
+ "dfc-b:optionOf" => "option_of",
225
+ "dfc-b:orderedBy" => "ordered_by",
226
+ "dfc-b:orders" => "orders",
227
+ "dfc-b:outputOf" => "output_of",
228
+ "dfc-b:ownedBy" => "owned_by",
229
+ "dfc-b:owns" => "owns",
230
+ "dfc-b:paidWith" => "paid_with",
231
+ "dfc-b:partOf" => "part_of",
232
+ "dfc-b:partOriginOf" => "part_origin_of",
233
+ "dfc-b:phoneNumberOf" => "phone_number_of",
234
+ "dfc-b:physicalCharacteristicOf" => "physical_characteristic_of",
235
+ "dfc-b:pickUp" => "pick_up",
236
+ "dfc-b:pickedUpAt" => "picked_up_at",
237
+ "dfc-b:processOf" => "process_of",
238
+ "dfc-b:producedBy" => "produced_by",
239
+ "dfc-b:produces" => "produces",
240
+ "dfc-b:proposedBy" => "proposed_by",
241
+ "dfc-b:proposes" => "proposes",
242
+ "dfc-b:referenceOf" => "reference_of",
243
+ "dfc-b:referencedBy" => "referenced_by",
244
+ "dfc-b:references" => "references",
245
+ "dfc-b:refersTo" => "refers_to",
246
+ "dfc-b:representedBy" => "represented_by",
247
+ "dfc-b:represents" => "represents",
248
+ "dfc-b:requestedBy" => "requested_by",
249
+ "dfc-b:requests" => "requests",
250
+ "dfc-b:satisfiedBy" => "satisfied_by",
251
+ "dfc-b:satisfies" => "satisfies",
252
+ "dfc-b:selectedBy" => "selected_by",
253
+ "dfc-b:selects" => "selects",
254
+ "dfc-b:sells" => "sells",
255
+ "dfc-b:ships" => "ships",
256
+ "dfc-b:socialMediaOf" => "social_media_of",
257
+ "dfc-b:soldBy" => "sold_by",
258
+ "dfc-b:startsAt" => "starts_at",
259
+ "dfc-b:storedIn" => "stored_in",
260
+ "dfc-b:stores" => "stores",
261
+ "dfc-b:suppliedBy" => "supplied_by",
262
+ "dfc-b:supplies" => "supplies",
263
+ "dfc-b:suppliesTo" => "supplies_to",
264
+ "dfc-b:to" => "to",
265
+ "dfc-b:tracedBy" => "traced_by",
266
+ "dfc-b:traces" => "traces",
267
+ "dfc-b:transformedBy" => "transformed_by",
268
+ "dfc-b:transforms" => "transforms",
269
+ "dfc-b:transportedBy" => "transported_by",
270
+ "dfc-b:transports" => "transports",
271
+ "dfc-b:typeOf" => "type_of",
272
+ "dfc-b:useVehicle" => "use_vehicle",
273
+ "dfc-b:usedInRoute" => "used_in_route",
274
+ "dfc-b:uses" => "uses",
275
+ "dfc-b:DFC_TechnicalOntology_ObjectProperty" => "d_f_c__technical_ontology__object_property",
276
+ "dfc-t:hasPivot" => "pivot",
277
+ "dfc-t:hostedBy" => "hosted_by",
278
+ "dfc-t:represent" => "represent",
279
+ }.freeze
280
+
281
+ # Slots the ontology requires, per semantic type, with the predicate each
282
+ # one serialises to. Every entry comes from an `rdfs:subClassOf` restriction
283
+ # with `minimum_cardinality 1` -- all 42 DFC restrictions are singletons.
284
+ # Consulted by #validate, which is opt-in: constructors stay permissive
285
+ # because these would reject ordinary partial documents.
286
+ REQUIRED_SLOTS = {
287
+ "dfc-b:AsPlannedConsumptionFlow" => [["consumes", "consumes", "dfc-b:consumes"], ["input_of", "input_of", "dfc-b:inputOf"]],
288
+ "dfc-b:AsPlannedLocalConsumptionFlow" => [["consumes", "consumes", "dfc-b:consumes"], ["input_of", "input_of", "dfc-b:inputOf"]],
289
+ "dfc-b:AsPlannedLocalProductionFlow" => [["output_of", "output_of", "dfc-b:outputOf"], ["produces", "produces", "dfc-b:produces"]],
290
+ "dfc-b:AsPlannedLocalTransformation" => [["transformed_by", "transformed_by", "dfc-b:transformedBy"]],
291
+ "dfc-b:AsPlannedProductionFlow" => [["output_of", "output_of", "dfc-b:outputOf"], ["produces", "produces", "dfc-b:produces"]],
292
+ "dfc-b:AsRealizedConsumptionFlow" => [["consumes", "consumes", "dfc-b:consumes"], ["input_of", "input_of", "dfc-b:inputOf"]],
293
+ "dfc-b:AsRealizedProductionFlow" => [["output_of", "output_of", "dfc-b:outputOf"], ["produces", "produces", "dfc-b:produces"]],
294
+ "dfc-b:Catalog" => [["maintained_by", "maintained_by", "dfc-b:maintainedBy"]],
295
+ "dfc-b:CatalogItem" => [["listed_in", "listed_in", "dfc-b:listedIn"], ["managed_by", "managed_by", "dfc-b:managedBy"], ["references", "references", "dfc-b:references"]],
296
+ "dfc-b:ConsumptionFlow" => [["consumes", "consumes", "dfc-b:consumes"], ["input_of", "input_of", "dfc-b:inputOf"]],
297
+ "dfc-b:Coordination" => [["coordinated_by", "coordinated_by", "dfc-b:coordinatedBy"], ["has_object", "object", "dfc-b:hasObject"]],
298
+ "dfc-b:CustomerCategory" => [["defined_by", "defined_by", "dfc-b:definedBy"]],
299
+ "dfc-b:DefinedProduct" => [["lifetime", "lifetime", "dfc-b:lifetime"]],
300
+ "dfc-b:DeliveryOption" => [["refers_to", "refers_to", "dfc-b:refersTo"]],
301
+ "dfc-b:FunctionalProduct" => [["lifetime", "lifetime", "dfc-b:lifetime"], ["requested_by", "requested_by", "dfc-b:requestedBy"]],
302
+ "dfc-b:Length" => [["value", "value", "dfc-b:value"]],
303
+ "dfc-b:Offer" => [["offers", "offers", "dfc-b:offers"], ["offers_to", "offers_to", "dfc-b:offersTo"]],
304
+ "dfc-b:Order" => [["belongs_to", "belongs_to", "dfc-b:belongsTo"], ["ordered_by", "ordered_by", "dfc-b:orderedBy"], ["selects", "selects", "dfc-b:selects"], ["uses", "uses", "dfc-b:uses"]],
305
+ "dfc-b:OrderLine" => [["concerns", "concerns", "dfc-b:concerns"], ["part_of", "part_of", "dfc-b:partOf"]],
306
+ "dfc-b:Organization" => [["has_main_contact", "main_contact", "dfc-b:hasMainContact"]],
307
+ "dfc-b:PaymentMethod" => [["payment_method_provider", "payment_method_provider", "dfc-b:paymentMethodProvider"], ["payment_method_type", "payment_method_type", "dfc-b:paymentMethodType"]],
308
+ "dfc-b:PhysicalPlace" => [["has_address", "address", "dfc-b:hasAddress"]],
309
+ "dfc-b:PickupOption" => [["uses", "uses", "dfc-b:uses"]],
310
+ "dfc-b:Price" => [["value", "value", "dfc-b:value"]],
311
+ "dfc-b:ProductionFlow" => [["output_of", "output_of", "dfc-b:outputOf"], ["produces", "produces", "dfc-b:produces"]],
312
+ "dfc-b:QuantitativeValue" => [["value", "value", "dfc-b:value"]],
313
+ "dfc-b:RealStock" => [["availability_date", "availability_date", "dfc-b:availabilityDate"], ["constitutes", "constitutes", "dfc-b:constitutes"], ["identified_by", "identified_by", "dfc-b:identifiedBy"], ["stored_in", "stored_in", "dfc-b:storedIn"]],
314
+ "dfc-b:Stock" => [["availability_date", "availability_date", "dfc-b:availabilityDate"]],
315
+ "dfc-b:SuppliedProduct" => [["lifetime", "lifetime", "dfc-b:lifetime"], ["supplied_by", "supplied_by", "dfc-b:suppliedBy"], ["total_theoritical_stock", "total_theoritical_stock", "dfc-b:totalTheoriticalStock"]],
316
+ "dfc-b:TechnicalProduct" => [["lifetime", "lifetime", "dfc-b:lifetime"], ["proposed_by", "proposed_by", "dfc-b:proposedBy"]],
317
+ "dfc-b:Temperature" => [["value", "value", "dfc-b:value"]],
318
+ "dfc-b:TheoriticalStock" => [["availability_date", "availability_date", "dfc-b:availabilityDate"], ["constitutes", "constitutes", "dfc-b:constitutes"], ["localized_by", "localized_by", "dfc-b:localizedBy"]],
319
+ "dfc-b:Transaction" => [["from", "from", "dfc-b:from"], ["to", "to", "dfc-b:to"]],
320
+ "dfc-b:Variant" => [["lifetime", "lifetime", "dfc-b:lifetime"]],
321
+ "dfc-b:VariantCaracteristic" => [["has_product_option", "product_option", "dfc-b:hasProductOption"], ["has_product_option_value", "product_option_value", "dfc-b:hasProductOptionValue"]],
322
+ "dfc-b:Volume" => [["value", "value", "dfc-b:value"]],
323
+ "dfc-b:Weight" => [["value", "value", "dfc-b:value"]],
324
+ }.freeze
325
+
326
+ TYPE_ALIASES = {
327
+ "dfc-b:Enterprise" => "dfc-b:Organization",
328
+ }.freeze
329
+
330
+ # Predicate local names whose has_ prefix is preserved (e.g. "hasBrand"
331
+ # vs "brand"). Mirrors ruby_property_name's _HAS_PREFIX_KEEP handling
332
+ # so the _predicate_to_prop_name fallback agrees with PREDICATE_MAP.
333
+ HAS_PREFIX_KEEP = [
334
+ "hasBrand",
335
+ "hasClaim",
336
+ "hasCountry",
337
+ "hasPhoneNumber",
338
+ "hasQuantity",
339
+ "has_brand",
340
+ "has_claim",
341
+ "has_country",
342
+ "has_phone_number",
343
+ "has_quantity",
344
+ ].freeze
345
+
346
+ class << self
347
+ def default_context_url
348
+ @default_context_url ||= "https://w3id.org/dfc/ontology/v2.0.0/context/context_2.0.0.json"
349
+ end
350
+
351
+ def default_context_url=(url)
352
+ @default_context_url = url
353
+ end
354
+
355
+ # Original-connector migration aid: the original gem exposes a
356
+ # singleton; ours is instantiable, this default instance covers
357
+ # `Connector.instance` call sites.
358
+ def instance
359
+ @instance ||= new
360
+ end
361
+ end
362
+
363
+ attr_reader :ontology_version, :taxonomy_version, :vocab_loader
364
+
365
+ # Bundled v2.0.0 vocabularies are loaded unconditionally by design — the gem
366
+ # ships only that version offline. Callers requesting a different
367
+ # taxonomy_version must override via load_* or load_from_url.
368
+ def initialize(ontology_version: "2.0.0", taxonomy_version: "2.0.0")
369
+ @ontology_version = ontology_version
370
+ @taxonomy_version = taxonomy_version
371
+ @vocab_loader = VocabularyLoader.new(taxonomy_version: taxonomy_version, ontology_version: ontology_version)
372
+ @context = nil
373
+ @facets = {}
374
+ @measures = {}
375
+ @product_types = {}
376
+ @other_vocabularies = {}
377
+ load_bundled_taxonomies
378
+ end
379
+
380
+ # Loads the taxonomies shipped with the gem (ruby-gem/vocabularies),
381
+ # falling back to network fetches only when the bundled files are absent.
382
+ def load_bundled_taxonomies
383
+ facets = _bundled_json("Facet")
384
+ measures = _bundled_json("Measure")
385
+ product_types = _bundled_json("ProductType")
386
+ scopes = _bundled_json("Scope")
387
+ vocabulary_terms = _bundled_json("VocabularyTerm")
388
+ load_facets(facets) if facets
389
+ load_measures(measures) if measures
390
+ load_product_types(product_types) if product_types
391
+ load_vocabulary("Scope", scopes) if scopes
392
+ load_vocabulary("VocabularyTerm", vocabulary_terms) if vocabulary_terms
393
+ self
394
+ end
395
+
396
+ def context_url
397
+ "#{ONTOLOGY_BASE_URL}/v#{@ontology_version}/context/context_#{@ontology_version}.json"
398
+ end
399
+
400
+ def context
401
+ @context ||= _bundled_context || _fetch_context
402
+ end
403
+
404
+ # Loads the JSON-LD context shipped with the gem (ruby-gem/contexts).
405
+ # Returns nil if no bundled context matches the requested version, so the
406
+ # caller falls back to fetching it from the network.
407
+ def bundled_context
408
+ _bundled_context
409
+ end
410
+
411
+ def load_facets(json_data)
412
+ @vocab_loader.load("Facet", json_data)
413
+ @facets = _build_nested_hash(@vocab_loader.vocabulary("Facet"))
414
+ @other_vocabularies["Facet"] = @facets
415
+ self
416
+ end
417
+
418
+ def load_measures(json_data)
419
+ @vocab_loader.load("Measure", json_data)
420
+ @measures = _build_nested_hash(@vocab_loader.vocabulary("Measure"))
421
+ @other_vocabularies["Measure"] = @measures
422
+ self
423
+ end
424
+
425
+ def load_product_types(json_data)
426
+ @vocab_loader.load("ProductType", json_data)
427
+ @product_types = _build_nested_hash(@vocab_loader.vocabulary("ProductType"))
428
+ @other_vocabularies["ProductType"] = @product_types
429
+ self
430
+ end
431
+
432
+ def load_vocabulary(name, json_data)
433
+ @vocab_loader.load(name, json_data)
434
+ @other_vocabularies[name] = _build_nested_hash(@vocab_loader.vocabulary(name))
435
+ self
436
+ end
437
+
438
+ def load_facets_from_url
439
+ load_facets(_fetch_taxonomy_json("facets"))
440
+ end
441
+
442
+ def load_measures_from_url
443
+ load_measures(_fetch_taxonomy_json("measures"))
444
+ end
445
+
446
+ def load_product_types_from_url
447
+ load_product_types(_fetch_taxonomy_json("productTypes"))
448
+ end
449
+
450
+ def export(*objects)
451
+ serializer = JsonLdSerializer.new(_safe_context, context_url)
452
+ serializer.to_json(*objects)
453
+ end
454
+ #
455
+ # Opt-in by design. A data-plane connector has to accept partially built
456
+ # objects, and these restrictions are heavy enough that enforcing them in
457
+ # the constructor would reject ordinary documents -- every Organization
458
+ # would need a hasMainContact. See docs/concepts/cardinality.md for where
459
+ # the constraint data comes from.
460
+ #
461
+ # @param objects [Array<SemanticObject>] objects to check
462
+ # @return [Array<Hash>] one entry per missing required property
463
+ # Reports properties the ontology requires and the object does not carry.
464
+ #
465
+ # Opt-in by design. A data-plane connector has to accept partially built
466
+ # objects, and these restrictions are heavy enough that enforcing them in
467
+ # the constructor would reject ordinary documents -- every Organization
468
+ # would need a hasMainContact. See docs/concepts/cardinality.md for where
469
+ # the constraint data comes from.
470
+ #
471
+ # @param objects [Array<SemanticObject>] objects to check
472
+ # @return [Array<Hash>] one entry per missing required property
473
+ def validate(*objects)
474
+ issues = []
475
+ objects.flatten.each do |object|
476
+ next unless object.respond_to?(:semanticType)
477
+
478
+ REQUIRED_SLOTS.fetch(object.semanticType, []).each do |slot, property, predicate|
479
+ value = object.respond_to?(property) ? object.public_send(property) : nil
480
+ next unless value.nil?
481
+
482
+ issues << {
483
+ semanticId: object.semanticId,
484
+ semanticType: object.semanticType,
485
+ slot: slot,
486
+ predicate: predicate,
487
+ }
488
+ end
489
+ end
490
+ issues
491
+ end
492
+
493
+ # Import JSON-LD data and return SemanticObject instances.
494
+ # Resolves @id references within the same document (shallow).
495
+ def import(json_ld_data)
496
+ data = json_ld_data.is_a?(String) ? JSON.parse(json_ld_data) : json_ld_data
497
+
498
+ entries = data.is_a?(Array) ? data : (data["@graph"] || [data])
499
+
500
+ objects_by_id = {}
501
+ instances = []
502
+
503
+ entries.each do |entry|
504
+ semantic_id = entry["@id"]
505
+ raw_type = entry["@type"]
506
+ semantic_type = if raw_type.is_a?(Array)
507
+ raw_type.find { |t| t.is_a?(String) && !t.start_with?("@") }
508
+ else
509
+ raw_type
510
+ end
511
+ next unless semantic_id && semantic_type
512
+
513
+ semantic_type = TYPE_ALIASES[semantic_type] || semantic_type
514
+ klass = SemanticObject.type_registry[semantic_type]
515
+ next unless klass
516
+
517
+ obj = klass.new(semantic_id)
518
+ objects_by_id[semantic_id] = obj
519
+ instances << obj
520
+ end
521
+
522
+ entries.each do |entry|
523
+ semantic_id = entry["@id"]
524
+ obj = objects_by_id[semantic_id]
525
+ next unless obj
526
+
527
+ entry.each do |key, value|
528
+ next if key.start_with?("@")
529
+ prop_name = _predicate_to_prop_name(key)
530
+ next unless obj.respond_to?(:"#{prop_name}=")
531
+
532
+ if value.is_a?(Array)
533
+ resolved = value.map do |v|
534
+ if v.is_a?(String) && (v.start_with?("http", "/") || v.start_with?("_:"))
535
+ objects_by_id[v] || v
536
+ elsif v.is_a?(Hash) && v["@id"]
537
+ objects_by_id[v["@id"]] || v
538
+ else
539
+ v
540
+ end
541
+ end
542
+ obj.send(:"#{prop_name}=", resolved)
543
+ elsif value.is_a?(String) && (value.start_with?("http") || value.start_with?("/") || value.start_with?("_:"))
544
+ obj.send(:"#{prop_name}=", objects_by_id[value] || value)
545
+ elsif value.is_a?(Hash) && value["@id"]
546
+ obj.send(:"#{prop_name}=", objects_by_id[value["@id"]] || value)
547
+ else
548
+ obj.send(:"#{prop_name}=", value)
549
+ end
550
+ end
551
+ end
552
+
553
+ instances
554
+ end
555
+
556
+ def facet
557
+ @other_vocabularies["Facet"] || @vocab_loader.vocabulary("Facet")
558
+ end
559
+
560
+ def measure
561
+ @other_vocabularies["Measure"] || @vocab_loader.vocabulary("Measure")
562
+ end
563
+
564
+ def product_type
565
+ @other_vocabularies["ProductType"] || @vocab_loader.vocabulary("ProductType")
566
+ end
567
+
568
+ def scope
569
+ @other_vocabularies["Scope"] || @vocab_loader.vocabulary("Scope")
570
+ end
571
+
572
+ def vocabulary_term
573
+ @other_vocabularies["VocabularyTerm"] || @vocab_loader.vocabulary("VocabularyTerm")
574
+ end
575
+ private
576
+
577
+ def _bundled_context
578
+ file = "context_#{@ontology_version}.json"
579
+ path = File.join(BUNDLED_CONTEXT_DIR, file)
580
+ return nil unless File.exist?(path)
581
+ JSON.parse(File.read(path))
582
+ end
583
+
584
+ def _bundled_json(name)
585
+ file = VocabularyLoader::BUNDLED_FILES[name]
586
+ return nil unless file
587
+ path = File.join(VocabularyLoader::BUNDLED_DIR, file)
588
+ File.exist?(path) ? JSON.parse(File.read(path)) : nil
589
+ end
590
+
591
+ def _safe_context
592
+ context
593
+ rescue => e
594
+ warn "Warning: could not load JSON-LD context (#{e.message}); exporting without compaction."
595
+ nil
596
+ end
597
+
598
+ def _fetch_context
599
+ uri = URI(context_url)
600
+ response = _http_get_follow_redirects(uri)
601
+ raise "Failed to fetch context from #{context_url}: #{response.code}" unless response.is_a?(Net::HTTPSuccess)
602
+ JSON.parse(response.body)
603
+ rescue => e
604
+ raise "Failed to load JSON-LD context: #{e.message}"
605
+ end
606
+
607
+ def _http_get_follow_redirects(uri, limit = 5)
608
+ raise "Too many redirects fetching #{uri}" if limit.zero?
609
+ request = Net::HTTP::Get.new(uri)
610
+ request["dfc-version"] = @ontology_version
611
+ response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https") { |http| http.request(request) }
612
+ if response.is_a?(Net::HTTPRedirection) && response["location"]
613
+ redirect_uri = URI.join(uri.to_s, response["location"])
614
+ return _http_get_follow_redirects(redirect_uri, limit - 1)
615
+ end
616
+ response
617
+ end
618
+
619
+ def _fetch_taxonomy_json(name)
620
+ url = "#{TAXONOMY_BASE_URL}/v#{@taxonomy_version}/#{name}.json"
621
+ uri = URI(url)
622
+ request = Net::HTTP::Get.new(uri)
623
+ request["dfc-version"] = @ontology_version
624
+ response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https") { |http| http.request(request) }
625
+ raise "Failed to fetch taxonomy from #{url}: #{response.code}" unless response.is_a?(Net::HTTPSuccess)
626
+ JSON.parse(response.body)
627
+ end
628
+
629
+ def _build_nested_hash(concepts)
630
+ result = {}
631
+ concepts.each do |key, concept|
632
+ parts = key.split(/[_\s]+/)
633
+ current = result
634
+ parts.each_with_index do |part, i|
635
+ normalized = part.downcase.gsub(/[^a-z0-9]/, "_")
636
+ if i == parts.length - 1
637
+ current[normalized] = concept
638
+ else
639
+ current[normalized] ||= {}
640
+ current = current[normalized]
641
+ end
642
+ end
643
+ end
644
+ result
645
+ end
646
+
647
+ def _predicate_to_prop_name(predicate)
648
+ return PREDICATE_MAP[predicate] if PREDICATE_MAP.key?(predicate)
649
+
650
+ name = predicate.dup
651
+ if name.include?("#")
652
+ name = name[(name.rindex("#") + 1)..-1]
653
+ else
654
+ colon_index = name.rindex(":")
655
+ name = name[(colon_index + 1)..-1] if colon_index
656
+ end
657
+ # Keep the has_ prefix for colliding slots (e.g. hasBrand vs brand);
658
+ # otherwise strip it to match ruby_property_name.
659
+ unless HAS_PREFIX_KEEP.include?(name)
660
+ if name.start_with?("has")
661
+ name = name[3..-1]
662
+ end
663
+ end
664
+ name = name.gsub(/([A-Z])/, "_\\1").downcase
665
+ name.sub!(/^_/, "")
666
+ name
667
+ end
668
+ end
669
+ end
670
+ end