diff --git a/Gemfile.lock b/Gemfile.lock index 707123d..7e7544c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,19 +1,19 @@ PATH remote: . specs: - sapi-client-ruby (1.4.0) + sapi-client-ruby (1.4.1) faraday_middleware (~> 1.0.0) i18n (~> 1.5) GEM remote: https://rubygems.org/ specs: - ansi (1.5.0) + ansi (1.6.0) ast (2.4.3) builder (3.3.0) - byebug (11.1.3) + byebug (13.0.0) + reline (>= 0.6.0) concurrent-ruby (1.3.7) - docile (1.4.1) faraday (1.10.6) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) @@ -39,12 +39,13 @@ GEM faraday-retry (1.0.4) faraday_middleware (1.0.0) faraday (~> 1.0) - i18n (1.14.8) + i18n (1.15.2) concurrent-ruby (~> 1.0) - json (2.20.0) + io-console (0.8.2) + json (2.21.1) language_server-protocol (3.17.0.6) lint_roller (1.1.0) - minitest (5.26.1) + minitest (5.27.0) minitest-reporters (1.8.0) ansi builder @@ -53,7 +54,7 @@ GEM mocha (2.8.2) ruby2_keywords (>= 0.0.5) multipart-post (2.4.1) - parallel (1.28.0) + parallel (2.1.0) parser (3.3.11.1) ast (~> 2.4.1) racc @@ -62,6 +63,8 @@ GEM rainbow (3.1.1) rake (13.4.2) regexp_parser (2.12.0) + reline (0.6.3) + io-console (~> 0.5) rubocop (1.88.2) json (~> 2.3) language_server-protocol (~> 3.17.0.2) @@ -78,20 +81,15 @@ GEM prism (~> 1.7) ruby-progressbar (1.13.0) ruby2_keywords (0.0.5) - simplecov (0.22.0) - docile (~> 1.1) - simplecov-html (~> 0.11) - simplecov_json_formatter (~> 0.1) - simplecov-html (0.13.2) - simplecov_json_formatter (0.1.4) + simplecov (1.0.1) unicode-display_width (3.2.0) unicode-emoji (~> 4.1) unicode-emoji (4.2.0) vcr (6.4.0) PLATFORMS + ruby x86_64-darwin-24 - x86_64-linux DEPENDENCIES bundler @@ -105,5 +103,50 @@ DEPENDENCIES simplecov vcr (~> 6.0) +CHECKSUMS + ansi (1.6.0) sha256=ac9ea0c0ea8d32fb4e271348e609963ac78882f34b73836c2a02b3622e666658 + ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383 + builder (3.3.0) sha256=497918d2f9dca528fdca4b88d84e4ef4387256d984b8154e9d5d3fe5a9c8835f + byebug (13.0.0) sha256=d2263efe751941ca520fa29744b71972d39cbc41839496706f5d9b22e92ae05d + concurrent-ruby (1.3.7) sha256=4412caec3a5ea2e5fdc52076724c071a81f2c0593d83b2ac8cbb8ca63b3151b0 + faraday (1.10.6) sha256=7ff4802a6b312876a2241b3e641ce0d5045e168dd871b422c35b505e5261ad4d + faraday-em_http (1.0.0) sha256=7a3d4c7079789121054f57e08cd4ef7e40ad1549b63101f38c7093a9d6c59689 + faraday-em_synchrony (1.0.1) sha256=bf3ce45dcf543088d319ab051f80985ea6d294930635b7a0b966563179f81750 + faraday-excon (1.1.0) sha256=b055c842376734d7f74350fe8611542ae2000c5387348d9ba9708109d6e40940 + faraday-httpclient (1.0.1) sha256=4c8ff1f0973ff835be8d043ef16aaf54f47f25b7578f6d916deee8399a04d33b + faraday-multipart (1.2.0) sha256=7d89a949693714176f612323ca13746a2ded204031a6ba528adee788694ef757 + faraday-net_http (1.0.2) sha256=63992efea42c925a20818cf3c0830947948541fdcf345842755510d266e4c682 + faraday-net_http_persistent (1.2.0) sha256=0b0cbc8f03dab943c3e1cc58d8b7beb142d9df068b39c718cd83e39260348335 + faraday-patron (1.0.0) sha256=dc2cd7b340bb3cc8e36bcb9e6e7eff43d134b6d526d5f3429c7a7680ddd38fa7 + faraday-rack (1.0.0) sha256=ef60ec969a2bb95b8dbf24400155aee64a00fc8ba6c6a4d3968562bcc92328c0 + faraday-retry (1.0.4) sha256=dc659233777fabf96c69c2ffe56c0a5d2c102af90321a42cc6c90157bcd716aa + faraday_middleware (1.0.0) sha256=19e808539681bbf2e65df30dfbe27bb402bde916a1dceb4c7496dbe8de14334a + i18n (1.15.2) sha256=00f9eb62412fe593b2a65a97daa75300d37abb8f7202ec748e94b6d46a9dd1b5 + io-console (0.8.2) sha256=d6e3ae7a7cc7574f4b8893b4fca2162e57a825b223a177b7afa236c5ef9814cc + json (2.21.1) sha256=13a43df75d95641443f5702dff350f237164a9d811ff0f2c2800d4d980220583 + language_server-protocol (3.17.0.6) sha256=5ef2c0c138f8267e1bc631d3328347d354f96724b0af22f2c79516120443b7f0 + lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87 + minitest (5.27.0) sha256=2d3b17f8a36fe7801c1adcffdbc38233b938eb0b4966e97a6739055a45fa77d5 + minitest-reporters (1.8.0) sha256=8ce5280fb73ad3178ae525454df169b6f28c1b38b1d088ea91815d3a370ba384 + mocha (2.8.2) sha256=1f77e729db47e72b4ef776461ce20caeec2572ffdf23365b0a03608fee8f4eee + multipart-post (2.4.1) sha256=9872d03a8e552020ca096adadbf5e3cb1cd1cdd6acd3c161136b8a5737cdb4a8 + parallel (2.1.0) sha256=b35258865c2e31134c5ecb708beaaf6772adf9d5efae28e93e99260877b09356 + parser (3.3.11.1) sha256=d17ace7aabe3e72c3cc94043714be27cc6f852f104d81aa284c2281aecc65d54 + prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85 + racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f + rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a + rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701 + regexp_parser (2.12.0) sha256=35a916a1d63190ab5c9009457136ae5f3c0c7512d60291d0d1378ba18ce08ebb + reline (0.6.3) sha256=1198b04973565b36ec0f11542ab3f5cfeeec34823f4e54cebde90968092b1835 + rubocop (1.88.2) sha256=8def251c90cd955feb4daa3edc0ab56893250c4ce90ef81e6c80c03f9a939bbf + rubocop-ast (1.50.0) sha256=b9ca88300da0803ee222ad20cdb30494c0a784eed06fdc35d254b06d662788db + ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33 + ruby2_keywords (0.0.5) sha256=ffd13740c573b7301cf7a2e61fc857b2a8e3d3aff32545d6f8300d8bae10e3ef + sapi-client-ruby (1.4.1) + simplecov (1.0.1) sha256=419d93c40484159d20b40e26e9a83cdd4c3afa2f3fd443bb8946c301cd658d75 + unicode-display_width (3.2.0) sha256=0cdd96b5681a5949cdbc2c55e7b420facae74c4aaf9a9815eee1087cb1853c42 + unicode-emoji (4.2.0) sha256=519e69150f75652e40bf736106cfbc8f0f73aa3fb6a65afe62fefa7f80b0f80f + vcr (6.4.0) sha256=077ac92cc16efc5904eb90492a18153b5e6ca5398046d8a249a7c96a9ea24ae6 + BUNDLED WITH - 2.4.22 + 4.0.5 diff --git a/lib/sapi_client/application.rb b/lib/sapi_client/application.rb index 38346a0..784851d 100644 --- a/lib/sapi_client/application.rb +++ b/lib/sapi_client/application.rb @@ -4,8 +4,8 @@ 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 - def initialize(base_url, application_or_endpoints) + class Application # rubocop:disable Metrics/ClassLength + def initialize(base_url, application_or_endpoints) # rubocop:disable Metrics/MethodLength unless File.exist?(application_or_endpoints) raise(SapiError, "Could not find spec file/directory #{application_or_endpoints}") end @@ -16,6 +16,10 @@ def initialize(base_url, application_or_endpoints) @specification = (@application_spec_file && YAML.load_file(application_or_endpoints)) || { 'sapi-nt' => { 'config' => { 'loadSpecPath' => 'classpath:endpointSpecs' } } } + + # Initialize parsed_model_spec and call parse_model_spec before returning + @parsed_model_spec = {} + parse_model_spec end attr_reader :base_url, :specification @@ -36,14 +40,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) } @@ -56,7 +64,7 @@ def endpoints # and endpoint `e` will have a methdod `e()` to get the JSON items for # that endpoint, and a method `e_spec()` to get the endpoint specification def instance # rubocop:disable Metrics/MethodLength - inst = SapiClient::Instance.new(base_url) + inst = SapiClient::Instance.new(base_url, @parsed_model_spec) endpoints.each do |endpoint| inst.define_singleton_method(:"#{endpoint.name}", get_items_proc(endpoint, inst)) @@ -97,5 +105,105 @@ def get_hierarchy_proc(endpoint, inst) inst.get_hierarchy(endpoint_url, options, scheme) 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 with the results + 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 + rescue StandardError => e + puts(SapiError, "Error parsing model spec file #{model_spec}: #{e.message}") + 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) + 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('::', '/') + .gsub(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2') + .gsub(/([a-z\d])([A-Z])/, '\1_\2') + .tr('-', '_') + .downcase + end end end diff --git a/lib/sapi_client/instance.rb b/lib/sapi_client/instance.rb index 88f11c8..b980285 100644 --- a/lib/sapi_client/instance.rb +++ b/lib/sapi_client/instance.rb @@ -12,8 +12,9 @@ class Instance # rubocop:disable Metrics/ClassLength attr_accessor :request_logger attr_reader :base_url, :logger, :instrumenter - def initialize(base_url, config = {}) + def initialize(base_url, parsed_model_spec = {}, config = {}) @base_url = base_url + SapiClient::SapiResource.parsed_model_spec = parsed_model_spec @logger = config[:logger] || rails_logger @instrumenter = config[:instrumenter] || rails_active_support_notifications end diff --git a/lib/sapi_client/sapi_resource.rb b/lib/sapi_client/sapi_resource.rb index 8198f64..53aa229 100644 --- a/lib/sapi_client/sapi_resource.rb +++ b/lib/sapi_client/sapi_resource.rb @@ -9,6 +9,10 @@ module SapiClient # Commonly, this class will be the base class for creating domain-specific # model classes to encapsulate particular API values. class SapiResource # rubocop:disable Metrics/ClassLength + class << self + attr_accessor :parsed_model_spec + end + # Create a new Sapi Resource, wrapping an existing value. The possible values # for `resource` are: # - a hash of values; hash keys will be transformed to symbols @@ -92,7 +96,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 @@ -134,15 +138,22 @@ 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) return self[property] if resource.key?(property) + # If not found, try looking for a camelCase version of the property cc_property = as_camel_case_method_name(property) return self[cc_property] if resource.key?(cc_property) + # If still not found, check if it's in the model spec for this resource's type(s) + return nil if property_in_model_spec?(property) || property_in_model_spec?(cc_property) + super end @@ -193,9 +204,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 } @@ -240,5 +252,20 @@ 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 + + # Helper method to find if property is in parsed_model_spec for the corresponding type + def property_in_model_spec?(property) + return false unless (spec = self.class.parsed_model_spec) + + types&.any? do |typ| + full_type = type_to_string(typ) + spec.key?(full_type) && spec[full_type].key?(property.to_s) + end + end end end 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 diff --git a/test/sapi_client/application_test.rb b/test/sapi_client/application_test.rb index 31e9f75..00e2a2a 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.instance_variable_get(:@parsed_model_spec).size).must_be :>, 0 + end + end + end end end end diff --git a/test/sapi_client/sapi_resource_test.rb b/test/sapi_client/sapi_resource_test.rb index 1c6bb93..de4eef7 100644 --- a/test/sapi_client/sapi_resource_test.rb +++ b/test/sapi_client/sapi_resource_test.rb @@ -344,6 +344,17 @@ 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 + original_spec = SapiClient::SapiResource.parsed_model_spec + SapiClient::SapiResource.parsed_model_spec = { 'http://wimbledon.org/Womble' => { 'home' => 'String' } } + begin + fixture = SapiClient::SapiResource.new(name: 'Tobermory', type: { '@id' => 'http://wimbledon.org/Womble' }) + _(fixture.home).must_be_nil + ensure + SapiClient::SapiResource.parsed_model_spec = original_spec + end + end end describe 'assignment' do