From 80770fdd99f0b4bedb7c3ef7a38f0fa91a6636fe Mon Sep 17 00:00:00 2001 From: Bogdan Marc Date: Wed, 11 Mar 2026 16:00:47 +0000 Subject: [PATCH 01/20] Added parsed model spec Hash as constant --- lib/sapi_client/application.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/sapi_client/application.rb b/lib/sapi_client/application.rb index f9b62f1..bbc0c0b 100644 --- a/lib/sapi_client/application.rb +++ b/lib/sapi_client/application.rb @@ -1,10 +1,12 @@ -# frozen-string-literal: true +# frozen_string_literal: true module SapiClient # Wraps an entire Sapi-NT application, such that we can walk over all of the # enclosed endpoint specifications to perform various operations, such as creating # methods we can call class Application + PARSED_MODEL_SPEC = {} # rubocop:disable Style/MutableConstant + def initialize(base_url, application_or_endpoints) unless File.exist?(application_or_endpoints) raise(SapiError, "Could not find spec file/directory #{application_or_endpoints}") From abed4a1442261efe7e94e6dc077b00a150968011 Mon Sep 17 00:00:00 2001 From: Bogdan Marc Date: Wed, 11 Mar 2026 16:01:17 +0000 Subject: [PATCH 02/20] Refactored code into final path helper method --- lib/sapi_client/application.rb | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/sapi_client/application.rb b/lib/sapi_client/application.rb index bbc0c0b..8132ee4 100644 --- a/lib/sapi_client/application.rb +++ b/lib/sapi_client/application.rb @@ -38,14 +38,18 @@ def load_spec_path @endpoints_path || configuration['loadSpecPath'].sub(/^classpath:/, '') end - def endpoint_group_files + def final_path if @endpoints_path.nil? - Dir["#{application_spec_dir}/#{load_spec_path}/*.yaml"] + "#{application_spec_dir}/#{load_spec_path}/*.yaml" else - Dir["#{@endpoints_path}/*.yaml"] + "#{@endpoints_path}/*.yaml" end end + def endpoint_group_files + Dir[final_path] + end + def endpoints endpoint_group_files .map { |spec| SapiClient::EndpointGroup.new(base_url, spec) } From 6cee24651636f23ab9628418ecb46590bf041bbc Mon Sep 17 00:00:00 2001 From: Bogdan Marc Date: Wed, 11 Mar 2026 16:01:54 +0000 Subject: [PATCH 03/20] Added to_underscore method to prevent rails require --- lib/sapi_client/application.rb | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/sapi_client/application.rb b/lib/sapi_client/application.rb index 8132ee4..7920fb4 100644 --- a/lib/sapi_client/application.rb +++ b/lib/sapi_client/application.rb @@ -103,5 +103,13 @@ def get_hierarchy_proc(endpoint, inst) inst.get_hierarchy(endpoint_url, options, scheme) end end + # Helper method for parsing model spec file + def to_underscore(string) + string.gsub('::', '/') + .gsub(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2') + .gsub(/([a-z\d])([A-Z])/, '\1_\2') + .tr('-', '_') + .downcase + end end end From 60eeadea5d956160cea4d01f11d67bf6733ab307 Mon Sep 17 00:00:00 2001 From: Bogdan Marc Date: Wed, 11 Mar 2026 16:02:14 +0000 Subject: [PATCH 04/20] Added helper method for parsing model file --- lib/sapi_client/application.rb | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/lib/sapi_client/application.rb b/lib/sapi_client/application.rb index 7920fb4..df1d936 100644 --- a/lib/sapi_client/application.rb +++ b/lib/sapi_client/application.rb @@ -103,6 +103,31 @@ def get_hierarchy_proc(endpoint, inst) inst.get_hierarchy(endpoint_url, options, scheme) end end + # Helper method for parsing model spec file + def type2ruby(typ, prefix2uri, qname2local, builtins) + full_uri = type2fulltype(typ, prefix2uri) + if qname2local.include? typ + qname2local[typ] + elsif builtins.include? full_uri + builtins[full_uri] + else + 'String' + end + rescue StandardError + 'String' + end + + # Helper method for parsing model spec file + def returns(types) + if types.size > 1 + "( #{types.join(' | ')} )" + elsif types.size == 1 + types.first + else + 'untyped' + end + end + # Helper method for parsing model spec file def to_underscore(string) string.gsub('::', '/') From 54261b9f3e0f7749ab947c4aa60cef05f46be088 Mon Sep 17 00:00:00 2001 From: Bogdan Marc Date: Wed, 11 Mar 2026 16:02:35 +0000 Subject: [PATCH 05/20] Added type2fulltype helper method for parsing model file --- lib/sapi_client/application.rb | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lib/sapi_client/application.rb b/lib/sapi_client/application.rb index df1d936..5a61f3e 100644 --- a/lib/sapi_client/application.rb +++ b/lib/sapi_client/application.rb @@ -103,6 +103,16 @@ def get_hierarchy_proc(endpoint, inst) inst.get_hierarchy(endpoint_url, options, scheme) end end + + # Helper method for parsing model spec file + def type2fulltype(typ, prefix2uri) + spl = typ.split(':') + pref = prefix2uri[spl[0]] + pref + spl[1] + rescue StandardError + typ + end + # Helper method for parsing model spec file def type2ruby(typ, prefix2uri, qname2local, builtins) full_uri = type2fulltype(typ, prefix2uri) From 474ae6e65f5ed2ea929ac3547c2706af60fd3606 Mon Sep 17 00:00:00 2001 From: Bogdan Marc Date: Wed, 11 Mar 2026 16:03:24 +0000 Subject: [PATCH 06/20] Added method to parse model spec file --- lib/sapi_client/application.rb | 55 ++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/lib/sapi_client/application.rb b/lib/sapi_client/application.rb index 5a61f3e..6406039 100644 --- a/lib/sapi_client/application.rb +++ b/lib/sapi_client/application.rb @@ -104,6 +104,61 @@ def get_hierarchy_proc(endpoint, inst) end end + # Parses the API model spec file and populates Hash with resulting class names and properties + def parse_model_spec # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity + # Load model spec file + model_spec = final_path.gsub('/*.yaml', '/model.yaml') + m = YAML.load_file(model_spec) + + # Parse class names and prefixes + qname2local = {} + m['classes'].each do |cls| + qname2local[cls['class']] = cls['name'] + end + prefix2uri = m['prefixes'] + builtins = { + 'http://www.w3.org/1999/02/22-rdf-syntax-ns#langString' => 'String', + 'http://www.w3.org/2001/XMLSchema#string' => 'String', + 'http://www.w3.org/2000/01/rdf-schema#Literal' => 'String', + 'http://www.w3.org/2001/XMLSchema#boolean' => 'bool', + 'http://www.w3.org/2001/XMLSchema#date' => 'Date', + 'http://www.w3.org/2001/XMLSchema#dateTime' => 'DateTime', + 'http://www.w3.org/2001/XMLSchema#integer' => 'Integer', + 'http://www.w3.org/2001/XMLSchema#decimal' => 'BigDecimal', + 'http://www.w3.org/2001/XMLSchema#double' => 'Float' + } + + # Parse classes and properties and populate PARSED_MODEL_SPEC + m['classes'].each do |cls| + # Skip if class has already been parsed + next if PARSED_MODEL_SPEC.keys.include?(type2fulltype(cls['class'], prefix2uri)) + + # If not, parse class and properties + PARSED_MODEL_SPEC[type2fulltype(cls['class'], prefix2uri)] = {} + cls['properties'].each do |prop| + ts = Set.new + + if prop['type'].is_a?(Array) + prop['type'].each do |t| + ts << type2ruby(t, prefix2uri, qname2local, builtins) + end + else + ts << type2ruby(prop['type'], prefix2uri, qname2local, builtins) + end + ts << 'nil' if prop['optional'] + + PARSED_MODEL_SPEC[type2fulltype(cls['class'], prefix2uri)][prop['name']] = returns(ts) + snake_prop = to_underscore(prop['name']) + if snake_prop != prop['name'] + PARSED_MODEL_SPEC[type2fulltype(cls['class'], prefix2uri)][snake_prop] = + returns(ts) + end + end + end + + nil + end + # Helper method for parsing model spec file def type2fulltype(typ, prefix2uri) spl = typ.split(':') From 5c09502879332561aa163b82499c0ed50e439783 Mon Sep 17 00:00:00 2001 From: Bogdan Marc Date: Wed, 11 Mar 2026 16:03:40 +0000 Subject: [PATCH 07/20] Parse model spec as last init step --- lib/sapi_client/application.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/sapi_client/application.rb b/lib/sapi_client/application.rb index 6406039..fbcce48 100644 --- a/lib/sapi_client/application.rb +++ b/lib/sapi_client/application.rb @@ -18,6 +18,9 @@ def initialize(base_url, application_or_endpoints) @specification = (@application_spec_file && YAML.load_file(application_or_endpoints)) || { 'sapi-nt' => { 'config' => { 'loadSpecPath' => 'classpath:endpointSpecs' } } } + + # Call method to parse model spec before returning + parse_model_spec end attr_reader :base_url, :specification From df9238575a0d79790262eb02d1513fb1d855303f Mon Sep 17 00:00:00 2001 From: Bogdan Marc Date: Thu, 12 Mar 2026 11:54:14 +0000 Subject: [PATCH 08/20] Refactored code into helper method --- lib/sapi_client/sapi_resource.rb | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/sapi_client/sapi_resource.rb b/lib/sapi_client/sapi_resource.rb index 80ac44e..10cca26 100644 --- a/lib/sapi_client/sapi_resource.rb +++ b/lib/sapi_client/sapi_resource.rb @@ -1,4 +1,4 @@ -# frozen-string-literal: true +# frozen_string_literal: true module SapiClient # Encapsulates a JSON-LD -style resource that we get back from a Sapi-NT endpoint, @@ -92,7 +92,7 @@ def types # @return True if this resource has the given URI among its types def type?(uri) - type_uris = types&.map { |typ| typ.is_a?(String) ? typ : typ['@id'] } + type_uris = types&.map { |typ| type_to_string(typ) } type_uris&.include?(uri) end @@ -240,5 +240,11 @@ def as_camel_case_method_name(str) first_segment, *remaining_segments = str.to_s.split('_') [first_segment, *remaining_segments.map(&:capitalize)].join.to_sym end + + # Helper method to convert type to string + def type_to_string(typ) + typ.is_a?(String) ? typ : typ['@id'] + end + end end From ddeb4219597da4d9dfebfc4ac871212e402aafdf Mon Sep 17 00:00:00 2001 From: Bogdan Marc Date: Thu, 12 Mar 2026 11:54:25 +0000 Subject: [PATCH 09/20] Fixed small rubocop error --- lib/sapi_client/sapi_resource.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/sapi_client/sapi_resource.rb b/lib/sapi_client/sapi_resource.rb index 10cca26..a402b3a 100644 --- a/lib/sapi_client/sapi_resource.rb +++ b/lib/sapi_client/sapi_resource.rb @@ -193,7 +193,7 @@ def lang_select(values, preferred_lang) # Return the given value as an un-wrapped resource. A Hash given to this # method will have its keys transformed to symbols. def as_resource(res) - if res.is_a?(SapiResource) # rubocop:disable Style/CaseLikeIf + if res.is_a?(SapiResource) res.resource.clone elsif res.is_a?(Hash) hash_with_symbol_keys(res) From 755399e2c3883373740bc8e684335c4bba666df8 Mon Sep 17 00:00:00 2001 From: Bogdan Marc Date: Thu, 12 Mar 2026 11:54:37 +0000 Subject: [PATCH 10/20] Added method to check if property is in model spec --- lib/sapi_client/sapi_resource.rb | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/sapi_client/sapi_resource.rb b/lib/sapi_client/sapi_resource.rb index a402b3a..f8b2f77 100644 --- a/lib/sapi_client/sapi_resource.rb +++ b/lib/sapi_client/sapi_resource.rb @@ -246,5 +246,12 @@ def type_to_string(typ) typ.is_a?(String) ? typ : typ['@id'] end + # Helper method to find if property is in PARSED_MODEL_SPEC for the corresponding type + def property_in_model_spec?(property) + types&.any? do |typ| + full_type = type_to_string(typ) + Application::PARSED_MODEL_SPEC.key?(full_type) && Application::PARSED_MODEL_SPEC[full_type].key?(property.to_s) + end + end end end From a48ddeaaf87372ddde73cb2d65989d6e6bef154a Mon Sep 17 00:00:00 2001 From: Bogdan Marc Date: Thu, 12 Mar 2026 11:54:53 +0000 Subject: [PATCH 11/20] Respond to missing if property is in model spec as well --- lib/sapi_client/sapi_resource.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/sapi_client/sapi_resource.rb b/lib/sapi_client/sapi_resource.rb index f8b2f77..b729a07 100644 --- a/lib/sapi_client/sapi_resource.rb +++ b/lib/sapi_client/sapi_resource.rb @@ -134,7 +134,10 @@ def name(options = {}) end def respond_to_missing?(property, _include_private = false) - resource.key?(property) || resource.key?(as_camel_case_method_name(property)) + resource.key?(property) || + resource.key?(as_camel_case_method_name(property)) || + property_in_model_spec?(property) || + property_in_model_spec?(as_camel_case_method_name(property)) end def method_missing(property, *_args) From b1ab9b05eecca4106c8948283da522503f7e0869 Mon Sep 17 00:00:00 2001 From: Bogdan Marc Date: Thu, 12 Mar 2026 11:58:03 +0000 Subject: [PATCH 12/20] If property is in model spec return nil instead of error raise --- lib/sapi_client/sapi_resource.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/sapi_client/sapi_resource.rb b/lib/sapi_client/sapi_resource.rb index b729a07..692597a 100644 --- a/lib/sapi_client/sapi_resource.rb +++ b/lib/sapi_client/sapi_resource.rb @@ -142,9 +142,13 @@ def respond_to_missing?(property, _include_private = false) def method_missing(property, *_args) return self[property] if resource.key?(property) + # If the property is not found, check if it's in the model spec for this resource's type(s) + return nil if property_in_model_spec?(property) + # If still not found, try looking for a camelCase version of the property as well cc_property = as_camel_case_method_name(property) return self[cc_property] if resource.key?(cc_property) + return nil if property_in_model_spec?(cc_property) super end From 317fc6dbbef3894fbfd45d5529d06abfa94942f0 Mon Sep 17 00:00:00 2001 From: Bogdan Marc Date: Thu, 12 Mar 2026 16:35:25 +0000 Subject: [PATCH 13/20] Refactored method to satisfy rubocop --- lib/sapi_client/sapi_resource.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/sapi_client/sapi_resource.rb b/lib/sapi_client/sapi_resource.rb index 692597a..92545e7 100644 --- a/lib/sapi_client/sapi_resource.rb +++ b/lib/sapi_client/sapi_resource.rb @@ -200,9 +200,10 @@ def lang_select(values, preferred_lang) # Return the given value as an un-wrapped resource. A Hash given to this # method will have its keys transformed to symbols. def as_resource(res) - if res.is_a?(SapiResource) + case res + when SapiResource res.resource.clone - elsif res.is_a?(Hash) + when Hash hash_with_symbol_keys(res) else { '@id': res.to_s } From fa9f49492449abf1edf9569f5d574877c71e935a Mon Sep 17 00:00:00 2001 From: Bogdan Marc Date: Thu, 12 Mar 2026 16:35:35 +0000 Subject: [PATCH 14/20] Fixed more rubocop issues --- lib/sapi_client/application.rb | 2 +- lib/sapi_client/sapi_resource.rb | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/sapi_client/application.rb b/lib/sapi_client/application.rb index fbcce48..693709e 100644 --- a/lib/sapi_client/application.rb +++ b/lib/sapi_client/application.rb @@ -4,7 +4,7 @@ module SapiClient # Wraps an entire Sapi-NT application, such that we can walk over all of the # enclosed endpoint specifications to perform various operations, such as creating # methods we can call - class Application + class Application # rubocop:disable Metrics/ClassLength PARSED_MODEL_SPEC = {} # rubocop:disable Style/MutableConstant def initialize(base_url, application_or_endpoints) diff --git a/lib/sapi_client/sapi_resource.rb b/lib/sapi_client/sapi_resource.rb index 92545e7..66a6a14 100644 --- a/lib/sapi_client/sapi_resource.rb +++ b/lib/sapi_client/sapi_resource.rb @@ -258,7 +258,8 @@ def type_to_string(typ) def property_in_model_spec?(property) types&.any? do |typ| full_type = type_to_string(typ) - Application::PARSED_MODEL_SPEC.key?(full_type) && Application::PARSED_MODEL_SPEC[full_type].key?(property.to_s) + Application::PARSED_MODEL_SPEC.key?(full_type) && + Application::PARSED_MODEL_SPEC[full_type].key?(property.to_s) end end end From 851e5799908c52aaa5f49e0938182f6349ed6496 Mon Sep 17 00:00:00 2001 From: Bogdan Marc Date: Fri, 13 Mar 2026 19:23:01 +0000 Subject: [PATCH 15/20] Rescue and print error message if parsing the model failed --- lib/sapi_client/application.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/sapi_client/application.rb b/lib/sapi_client/application.rb index 693709e..969d513 100644 --- a/lib/sapi_client/application.rb +++ b/lib/sapi_client/application.rb @@ -160,6 +160,8 @@ def parse_model_spec # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplex end nil + rescue StandardError => e + puts(SapiError, "Error parsing model spec file #{model_spec}: #{e.message}") end # Helper method for parsing model spec file From 474d4132e15b316eeb83a9361412f90beaed2b1e Mon Sep 17 00:00:00 2001 From: Bogdan Marc Date: Fri, 13 Mar 2026 19:28:43 +0000 Subject: [PATCH 16/20] Added missing model spec for cbd api fixture --- .../fixtures/cbd_api/endpointSpecs/model.yaml | 216 ++++++++++++++++++ 1 file changed, 216 insertions(+) create mode 100644 test/fixtures/cbd_api/endpointSpecs/model.yaml diff --git a/test/fixtures/cbd_api/endpointSpecs/model.yaml b/test/fixtures/cbd_api/endpointSpecs/model.yaml new file mode 100644 index 0000000..1141e86 --- /dev/null +++ b/test/fixtures/cbd_api/endpointSpecs/model.yaml @@ -0,0 +1,216 @@ +type: model +name: model +prefixes: + core: http://data.food.gov.uk/cbd-products/def/core/ + dct: http://purl.org/dc/terms/ + owl: http://www.w3.org/2002/07/owl# + rdf: http://www.w3.org/1999/02/22-rdf-syntax-ns# + rdfs: http://www.w3.org/2000/01/rdf-schema# + skos: http://www.w3.org/2004/02/skos/core# + xsd: http://www.w3.org/2001/XMLSchema# + dcat: http://www.w3.org/ns/dcat# + foaf: http://xmlns.com/foaf/0.1/ + vcard: http://www.w3.org/2006/vcard/ns# + def-cbd: http://data.food.gov.uk/cbd-products/def/ + pl-base: http://data.food.gov.uk/cbd-products/id/ + cbd-base: http://data.food.gov.uk/cbd-products/id/ + codespace: http://data.food.gov.uk/cbd-products/id/codespace/ + cdt: http://w3id.org/lindt/custom_datatypes# + cbd-listing: http://data.food.gov.uk/cbd-products/id/listing/ + cbd-supplier: http://data.food.gov.uk/cbd-products/id/supplier/ + cbd-status: http://data.food.gov.uk/cbd-products/id/application-status/ + +properties: + - prop: "rdf:type" + name: "type" + kind: "object" + type: "rdfs:Resource" + optional: true + multi: true + unique: false + - prop: "core:searchText" + name: "searchText" + type: "xsd:string" + optional: true + - prop: "skos:notation" + name: "notation" + type: "xsd:string" + optional: true + - prop: "core:memberOf" + name: "memberOf" + type: "code:Collection" + multi: false + optional: true + - prop: "core:dateTimeStamp" + name: "dateTimeStamp" + type: "xsd:date" + optional: true + - prop: "core:lastModified" + name: "lastModified" + type: "xsd:date" + optional: true + - prop: "core:remark" + name: "remark" + type: "xsd:string" + optional: true + - prop: "core:status" + name: "status" + type: "core:Concept" + optional: true + +classes: + - class: "core:Thing" + name: "Thing" + properties: + - "rdf:type" + - "skos:notation" + - "core:searchText" + - "core:dateTimeStamp" + - "core:memberOf" + + - class: "core:Agent" + name: "Agent" + properties: + - "rdf:type" + - "skos:notation" + - "core:searchText" + - "core:dateTimeStamp" + - "core:memberOf" + - prop: "foaf:name" + name: "name" + kind: "datatype" + type: "rdf:langString" + optional: true + multi: true + unique: false + comment: "A name for some thing." + + - class: "core:Concept" + name: "Concept" + properties: + - "rdf:type" + - "skos:notation" + - "core:searchText" + - "core:dateTimeStamp" + - "core:memberOf" + - prop: "skos:inScheme" + name: "inScheme" + type: "core:Concept" + - prop: "skos:prefLabel" + name: "prefLabel" + type: "xsd:string" + multi: false + + - class: "core:ConceptScheme" + name: "ConceptScheme" + properties: + - "rdf:type" + - "skos:notation" + - "core:searchText" + - "core:dateTimeStamp" + - "core:memberOf" + - prop: "skos:hasTopConcept" + name: "hasTopConcept" + type: "core:Concept" + + - class: "core:List" + name: "List" + properties: + - "rdf:type" + - "skos:notation" + - "core:searchText" + - "core:dateTimeStamp" + - "core:memberOf" + - prop: "core:statusScheme" + name: "statusScheme" + type: "core:ConceptScheme" + - prop: "core:supplierGroup" + name: "supplierGroup" + type: "core:Collection" + - prop: "core:listEntryClass" + name: "listEntryClass" + type: "owl:Class" + + - class: "core:Listing" + name: "Listing" + properties: + - "rdf:type" + - "skos:notation" + - "core:searchText" + - "core:dateTimeStamp" + - prop: "core:memberOf" + name: "memberOf" + type: "core:List" + - "core:status" + - "core:remark" + + - class: "core:ProductListing" + name: "ProductListing" + properties: + - "rdf:type" + - "skos:notation" + - "core:searchText" + - "core:dateTimeStamp" + - "core:lastModified" + - prop: "core:memberOf" + name: "memberOf" + type: "core:List" + - "core:remark" + - prop: "core:applicationNumber" + name: "applicationNumber" + optional: true + multi: true + type: "xsd:string" + - prop: "core:status" + name: "status" + type: "core:Concept" + optional: true + valueBase: "cbd-status:" + - prop: "core:manufacturerSupplier" + name: "manufacturerSupplier" + type: "core:Agent" + optional: true + valueBase: "cbd-supplier:" + - prop: "core:productId" + name: "productId" + optional: true + type: "xsd:string" + - prop: "core:productLinkedToApplication" + name: "productLinkedToApplication" + optional: true + type: "xsd:string" + - prop: "core:productName" + name: "productName" + type: "xsd:string" + - prop: "core:productSizeVolumeQuantity" + name: "productSizeVolumeQuantity" + optional: true + type: "xsd:string" + - class: "core:List" + name: "List" + properties: + - "rdf:type" + - "skos:notation" + - "core:searchText" + - "core:lastModified" + - prop: "skos:prefLabel" + name: "prefLabel" + type: "xsd:string" + multi: false + - prop: "core:memberOf" + name: "memberOf" + type: "core:List" + - "core:status" + - "core:remark" + - prop: "core:listEntryClass" + name: "listEntryClass" + type: "owl:Class" + optional: true + - prop: "core:statusScheme" + name: "statusScheme" + type: "skos:ConceptScheme" + optional: true + - prop: "core:supplierGroup" + name: "supplierGroup" + type: "core:Collection" + optional: true From f9057c7ef00924cfaddf8a827be763cca85adc02 Mon Sep 17 00:00:00 2001 From: Bogdan Marc Date: Fri, 13 Mar 2026 19:29:59 +0000 Subject: [PATCH 17/20] Added unit test for parsing the model spec file --- test/sapi_client/application_test.rb | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/sapi_client/application_test.rb b/test/sapi_client/application_test.rb index 31e9f75..710dda4 100644 --- a/test/sapi_client/application_test.rb +++ b/test/sapi_client/application_test.rb @@ -97,6 +97,18 @@ def initialize(_json) _(hierarchy.roots.size).must_equal(5) end end + + describe '#parsed_model_spec' do + it 'should populate the parsed model spec on initialization' do + VCR.use_cassette('application.test_parsed_model_spec') do + app = SapiClient::Application.new( + 'http://fsa-rp-test.epimorphics.net', + 'test/fixtures/regulated-products/application.yaml' + ) + _(app.class.const_get(:PARSED_MODEL_SPEC).size).must_be :>, 0 + end + end + end end end end From a0b776a9244ab5f4d04d50cf2fd45d7058257b5b Mon Sep 17 00:00:00 2001 From: Bogdan Marc Date: Fri, 13 Mar 2026 19:30:41 +0000 Subject: [PATCH 18/20] Added minitest-stub-const gem to gemspec --- Gemfile.lock | 2 ++ sapi-client-ruby.gemspec | 1 + 2 files changed, 3 insertions(+) diff --git a/Gemfile.lock b/Gemfile.lock index b4c138e..2568fba 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -47,6 +47,7 @@ GEM builder minitest (>= 5.0) ruby-progressbar + minitest-stub-const (0.6) mocha (2.7.1) ruby2_keywords (>= 0.0.5) multipart-post (2.4.1) @@ -89,6 +90,7 @@ DEPENDENCIES byebug (~> 11.1.3) minitest (~> 5.25) minitest-reporters (~> 1.7) + minitest-stub-const (~> 0.6) mocha (~> 2.4) rake (~> 13.0.1) rubocop (~> 1.26.0) diff --git a/sapi-client-ruby.gemspec b/sapi-client-ruby.gemspec index 449e96b..1118d61 100644 --- a/sapi-client-ruby.gemspec +++ b/sapi-client-ruby.gemspec @@ -38,6 +38,7 @@ Gem::Specification.new do |spec| spec.add_development_dependency 'byebug', '~> 11.1.3' spec.add_development_dependency 'minitest', '~> 5.25' spec.add_development_dependency 'minitest-reporters', '~> 1.7' + spec.add_development_dependency 'minitest-stub-const', '~> 0.6' spec.add_development_dependency 'mocha', '~> 2.4' spec.add_development_dependency 'rake', '~> 13.0.1' spec.add_development_dependency 'rubocop', '~> 1.26.0' From 962e9bcbf47499d441806589fd2ff9287f062ccb Mon Sep 17 00:00:00 2001 From: Bogdan Marc Date: Fri, 13 Mar 2026 19:30:51 +0000 Subject: [PATCH 19/20] Require new gem in test helper --- test/test_helper.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/test/test_helper.rb b/test/test_helper.rb index 2b563e2..e72cc75 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -19,6 +19,7 @@ require 'minitest/autorun' require 'minitest/mock' require 'minitest/reporters' +require 'minitest/stub_const' require 'mocha/minitest' require 'vcr' From e4d8c3ff30deb2e234a362b92b4a637737a56a1f Mon Sep 17 00:00:00 2001 From: Bogdan Marc Date: Fri, 13 Mar 2026 19:31:37 +0000 Subject: [PATCH 20/20] Added test to make sure nil is returned if property is not in the data but is in the model spec --- test/sapi_client/sapi_resource_test.rb | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/test/sapi_client/sapi_resource_test.rb b/test/sapi_client/sapi_resource_test.rb index 9df921d..fbd76d9 100644 --- a/test/sapi_client/sapi_resource_test.rb +++ b/test/sapi_client/sapi_resource_test.rb @@ -1,4 +1,4 @@ -# frozen-string-literal: true +# frozen_string_literal: true require 'test_helper' require 'sapi_client' @@ -344,6 +344,13 @@ class SapiResourceTest < Minitest::Test fixture = SapiClient::SapiResource.new(prefLabel: 'I am Womble!') _(fixture.pref_label).must_equal('I am Womble!') end + + it 'should return nil for a property that is not present on the resource but is in the model spec' do + SapiClient::Application.stub_const(:PARSED_MODEL_SPEC, { 'http://wimbledon.org/Womble' => { 'home' => 'String' } }) do + fixture = SapiClient::SapiResource.new(name: 'Tobermory', type: { '@id' => 'http://wimbledon.org/Womble' }) + _(fixture.home).must_be_nil + end + end end describe 'assignment' do