-
Notifications
You must be signed in to change notification settings - Fork 474
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Adds attributes comparator and test case #1282
Changes from 6 commits
a83923b
9004239
8ad3986
0971d72
bfbb2d4
910e33a
c0621c7
327a819
930e582
a09db36
221f06e
938aece
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
# typed: strict | ||
# frozen_string_literal: true | ||
|
||
require "hash_diff" | ||
|
||
module ShopifyAPI | ||
module Utils | ||
module AttributesComparator | ||
class << self | ||
extend T::Sig | ||
|
||
sig do | ||
params( | ||
original_attributes: T::Hash[String, T.untyped], | ||
updated_attributes: T::Hash[String, T.untyped], | ||
).returns(T::Hash[String, T.untyped]) | ||
end | ||
def compare(original_attributes, updated_attributes) | ||
attributes_diff = HashDiff::Comparison.new( | ||
original_attributes, | ||
updated_attributes, | ||
).left_diff | ||
|
||
update_value = build_update_value( | ||
attributes_diff, | ||
reference_values: updated_attributes, | ||
) | ||
|
||
update_value | ||
end | ||
|
||
sig do | ||
params( | ||
diff: T::Hash[String, T.untyped], | ||
path: T::Array[String], | ||
reference_values: T::Hash[String, T.untyped], | ||
).returns(T::Hash[String, T.untyped]) | ||
end | ||
def build_update_value(diff, path: [], reference_values: {}) | ||
new_hash = {} | ||
|
||
diff.each do |key, value| | ||
current_path = path + [key.to_s] | ||
|
||
if value.is_a?(Hash) | ||
has_numbered_key = value.keys.any? { |k| k.is_a?(Integer) } | ||
ref_value = T.unsafe(reference_values).dig(*current_path) | ||
|
||
if has_numbered_key && ref_value.is_a?(Array) | ||
new_hash[key] = ref_value | ||
Comment on lines
+49
to
+50
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. array diff will have the following format original = { "a" => [] }
updated = { "a" => ["foo"] }
diff = { "a" => { 0 => "foo" } } However, we don't want to submit the diff values, we want to submit the array so these 2 lines just take the updated value has_numbered_key = true # because of `0` key
ref_value = ["foo"] # which is_a Array
resulting_hash = { "a" => ["foo"] } There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i don't dig deeper into this because when assigning a resource's array, we either pass in a completely new array, adding to it, modify an existing item in the array or removing an existing item. Then it's better to just use the entire updated array to send to Shopify. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed! |
||
else | ||
new_value = build_update_value(value, path: current_path, reference_values: reference_values) | ||
|
||
new_hash[key] = new_value unless new_value.empty? && !ref_value.empty? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is for cases where an object is explicitly emptied. For example draft_order = ShopifyAPI::DraftOrder.find(session: @test_session, id: 1143974756657)
draft_order.shipping_address = {}
draft_order.save! if there was a shipping address in the draft order, this should remove that shipping address from the draft order. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. in an opposite case, where we only update the address partially but accidentally use the same value for the same attribute, this helps clear out the entire diff because there's nothing to update. It's an edge case. For example original = {
shipping_address: {
address1: "foo",
address2: "bar"
}
}
updated = {
shipping_address: {
address2: "bar"
}
}
diff = {
shipping_address: {
address1: HashDiff::NO_VALUE # hash_diff thinks we want to remove `address1`
} # address2 doesn't show up because it's not different
}
update_value = {} # should be nothing to update
sle-c marked this conversation as resolved.
Show resolved
Hide resolved
|
||
end | ||
elsif value != HashDiff::NO_VALUE | ||
new_hash[key] = value | ||
end | ||
end | ||
|
||
new_hash | ||
end | ||
end | ||
end | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -237,7 +237,7 @@ def test_saves_removing_children | |
body = draft_order_data.dup | ||
body["line_items"] = [] | ||
stubbed_request = stub_request(:put, "#{@prefix}/draft_orders/#{draft_order_data.dig("id")}.json") | ||
.with(body: hash_including("draft_order": { line_items: [] })) | ||
.with(body: hash_including("draft_order": { line_items: [], id: 1124601987358 })) | ||
.to_return(status: 200) | ||
|
||
draft_order.line_items = [] | ||
|
@@ -416,15 +416,17 @@ def test_put_requests_only_modify_changed_attributes | |
headers: { "X-Shopify-Access-Token" => "this_is_a_test_token", "Accept" => "application/json", | ||
"Content-Type" => "application/json", }, | ||
body: { "product" => hash_including({ "metafields" => [{ "key" => "new", "value" => "newvalue", | ||
"type" => "single_line_text_field", "namespace" => "global", }] }) }, | ||
"type" => "single_line_text_field", "namespace" => "global", }], | ||
"id" => 632910392, }) }, | ||
) | ||
.to_return(status: 200, body: JSON.generate({ "product" => { "id" => 632910392, "title" => "IPod Nano - 8GB", | ||
"body_html" => "<p>It's the small iPod with one very big idea: Video. Now the world's most popular music player, available in 4GB and 8GB models, lets you enjoy TV shows, movies, video podcasts, and more. The larger, brighter display means amazing picture quality. In six eye-catching colors, iPod nano is stunning all around. And with models starting at just $149, little speaks volumes.</p>", "vendor" => "Apple", "product_type" => "Cult Products", "created_at" => "2023-02-02T09:09:49-05:00", "handle" => "ipod-nano", "updated_at" => "2023-02-02T09:28:31-05:00", "published_at" => "2007-12-31T19:00:00-05:00", "template_suffix" => nil, "status" => "active", "published_scope" => "web", "tags" => "Emotive, Flash Memory, MP3, Music", "admin_graphql_api_id" => "gid://shopify/Product/632910392", "variants" => [{ "id" => 808950810, "product_id" => 632910392, "title" => "Pink", "price" => "199.00", "sku" => "IPOD2008PINK", "position" => 1, "inventory_policy" => "continue", "compare_at_price" => nil, "fulfillment_service" => "manual", "inventory_management" => "shopify", "option1" => "Pink", "option2" => nil, "option3" => nil, "created_at" => "2023-02-02T09:09:49-05:00", "updated_at" => "2023-02-02T09:09:49-05:00", "taxable" => true, "barcode" => "1234_pink", "grams" => 567, "image_id" => 562641783, "weight" => 1.25, "weight_unit" => "lb", "inventory_item_id" => 808950810, "inventory_quantity" => 10, "old_inventory_quantity" => 10, "presentment_prices" => [{ "price" => { "amount" => "199.00", "currency_code" => "USD" }, "compare_at_price" => nil }], "requires_shipping" => true, "admin_graphql_api_id" => "gid://shopify/ProductVariant/808950810" }, { "id" => 49148385, "product_id" => 632910392, "title" => "Red", "price" => "199.00", "sku" => "IPOD2008RED", "position" => 2, "inventory_policy" => "continue", "compare_at_price" => nil, "fulfillment_service" => "manual", "inventory_management" => "shopify", "option1" => "Red", "option2" => nil, "option3" => nil, "created_at" => "2023-02-02T09:09:49-05:00", "updated_at" => "2023-02-02T09:09:49-05:00", "taxable" => true, "barcode" => "1234_red", "grams" => 567, "image_id" => nil, "weight" => 1.25, "weight_unit" => "lb", "inventory_item_id" => 49148385, "inventory_quantity" => 20, "old_inventory_quantity" => 20, "presentment_prices" => [{ "price" => { "amount" => "199.00", "currency_code" => "USD" }, "compare_at_price" => nil }], "requires_shipping" => true, "admin_graphql_api_id" => "gid://shopify/ProductVariant/49148385" }, { "id" => 39072856, "product_id" => 632910392, "title" => "Green", "price" => "199.00", "sku" => "IPOD2008GREEN", "position" => 3, "inventory_policy" => "continue", "compare_at_price" => nil, "fulfillment_service" => "manual", "inventory_management" => "shopify", "option1" => "Green", "option2" => nil, "option3" => nil, "created_at" => "2023-02-02T09:09:49-05:00", "updated_at" => "2023-02-02T09:09:49-05:00", "taxable" => true, "barcode" => "1234_green", "grams" => 567, "image_id" => nil, "weight" => 1.25, "weight_unit" => "lb", "inventory_item_id" => 39072856, "inventory_quantity" => 30, "old_inventory_quantity" => 30, "presentment_prices" => [{ "price" => { "amount" => "199.00", "currency_code" => "USD" }, "compare_at_price" => nil }], "requires_shipping" => true, "admin_graphql_api_id" => "gid://shopify/ProductVariant/39072856" }, { "id" => 457924702, "product_id" => 632910392, "title" => "Black", "price" => "199.00", "sku" => "IPOD2008BLACK", "position" => 4, "inventory_policy" => "continue", "compare_at_price" => nil, "fulfillment_service" => "manual", "inventory_management" => "shopify", "option1" => "Black", "option2" => nil, "option3" => nil, "created_at" => "2023-02-02T09:09:49-05:00", "updated_at" => "2023-02-02T09:09:49-05:00", "taxable" => true, "barcode" => "1234_black", "grams" => 567, "image_id" => nil, "weight" => 1.25, "weight_unit" => "lb", "inventory_item_id" => 457924702, "inventory_quantity" => 40, "old_inventory_quantity" => 40, "presentment_prices" => [{ "price" => { "amount" => "199.00", "currency_code" => "USD" }, "compare_at_price" => nil }], "requires_shipping" => true, "admin_graphql_api_id" => "gid://shopify/ProductVariant/457924702" }], "options" => [{ "id" => 594680422, "product_id" => 632910392, "name" => "Color", "position" => 1, "values" => ["Pink", "Red", "Green", "Black"] }], "images" => [{ "id" => 850703190, "product_id" => 632910392, "position" => 1, "created_at" => "2023-02-02T09:09:49-05:00", "updated_at" => "2023-02-02T09:09:49-05:00", "alt" => nil, "width" => 123, "height" => 456, "src" => "https://cdn.shopify.com/s/files/1/0005/4838/0009/products/ipod-nano.png?v=1675346989", "variant_ids" => [], "admin_graphql_api_id" => "gid://shopify/ProductImage/850703190" }, { "id" => 562641783, "product_id" => 632910392, "position" => 2, "created_at" => "2023-02-02T09:09:49-05:00", "updated_at" => "2023-02-02T09:09:49-05:00", "alt" => nil, "width" => 123, "height" => 456, "src" => "https://cdn.shopify.com/s/files/1/0005/4838/0009/products/ipod-nano-2.png?v=1675346989", "variant_ids" => [808950810], "admin_graphql_api_id" => "gid://shopify/ProductImage/562641783" }, { "id" => 378407906, "product_id" => 632910392, "position" => 3, "created_at" => "2023-02-02T09:09:49-05:00", "updated_at" => "2023-02-02T09:09:49-05:00", "alt" => nil, "width" => 123, "height" => 456, "src" => "https://cdn.shopify.com/s/files/1/0005/4838/0009/products/ipod-nano.png?v=1675346989", "variant_ids" => [], "admin_graphql_api_id" => "gid://shopify/ProductImage/378407906" }], "image" => { "id" => 850703190, "product_id" => 632910392, "position" => 1, "created_at" => "2023-02-02T09:09:49-05:00", "updated_at" => "2023-02-02T09:09:49-05:00", "alt" => nil, "width" => 123, "height" => 456, "src" => "https://cdn.shopify.com/s/files/1/0005/4838/0009/products/ipod-nano.png?v=1675346989", "variant_ids" => [], "admin_graphql_api_id" => "gid://shopify/ProductImage/850703190" }, } }), headers: {}) | ||
|
||
product = ShopifyAPI::Product.find(id: 632910392, session: @session) | ||
|
||
product.client.expects(:put).with( | ||
body: { "product" => { "metafields" => [{ "key" => "new", "value" => "newvalue", "type" => "single_line_text_field", | ||
"namespace" => "global", }] } }, | ||
"namespace" => "global", }], "id" => 632910392, } }, | ||
path: "products/632910392.json", | ||
) | ||
product.metafields = [ | ||
|
@@ -438,7 +440,7 @@ def test_put_requests_only_modify_changed_attributes | |
product.save | ||
end | ||
|
||
def test_put_request_for_has_one_associaiton_works | ||
def test_put_request_for_has_one_association_works | ||
stub_request(:get, "https://test-shop.myshopify.com/admin/api/#{ShopifyAPI::Context.api_version}/customers/207119551.json") | ||
.to_return(status: 200, body: JSON.generate({ "customer" => { "id" => 207119551, | ||
"email" => "[email protected]", "accepts_marketing" => false, "created_at" => "2023-02-02T09:42:27-05:00", "updated_at" => "2023-02-02T09:42:27-05:00", "first_name" => "Bob", "last_name" => "Norman", "orders_count" => 1, "state" => "disabled", "total_spent" => "199.65", "last_order_id" => 450789469, "note" => nil, "verified_email" => true, "multipass_identifier" => nil, "tax_exempt" => false, "tags" => "L\u00E9on, No\u00EBl", "last_order_name" => "#1001", "currency" => "USD", "phone" => "+16136120707", "addresses" => [{ "id" => 207119551, "customer_id" => 207119551, "first_name" => nil, "last_name" => nil, "company" => nil, "address1" => "Chestnut Street 92", "address2" => "", "city" => "Louisville", "province" => "Kentucky", "country" => "United States", "zip" => "40202", "phone" => "555-625-1199", "name" => "", "province_code" => "KY", "country_code" => "US", "country_name" => "United States", "default" => true }], "accepts_marketing_updated_at" => "2005-06-12T11:57:11-04:00", "marketing_opt_in_level" => nil, "tax_exemptions" => [], "email_marketing_consent" => { "state" => "not_subscribed", "opt_in_level" => nil, "consent_updated_at" => "2004-06-13T11:57:11-04:00" }, "sms_marketing_consent" => { "state" => "not_subscribed", "opt_in_level" => "single_opt_in", "consent_updated_at" => "2023-02-02T09:42:27-05:00", "consent_collected_from" => "OTHER" }, "admin_graphql_api_id" => "gid://shopify/Customer/207119551", "default_address" => { "id" => 207119551, "customer_id" => 207119551, "first_name" => nil, "last_name" => nil, "company" => nil, "address1" => "Chestnut Street 92", "address2" => "", "city" => "Louisville", "province" => "Kentucky", "country" => "United States", "zip" => "40202", "phone" => "555-625-1199", "name" => "", "province_code" => "KY", "country_code" => "US", "country_name" => "United States", "default" => true }, } }), headers: {}) | ||
|
@@ -448,7 +450,7 @@ def test_put_request_for_has_one_associaiton_works | |
session: @session, | ||
) | ||
customer.client.expects(:put).with( | ||
body: { "customer" => { "tags" => "New Customer, Repeat Customer" } }, | ||
body: { "customer" => { "tags" => "New Customer, Repeat Customer", "id" => 207119551 } }, | ||
path: "customers/207119551.json", | ||
) | ||
customer.tags = "New Customer, Repeat Customer" | ||
|
@@ -496,7 +498,7 @@ def test_put_requests_for_resource_with_read_only_attributes | |
|
||
variant = ShopifyAPI::Variant.find(id: 169, session: @session) | ||
variant.client.expects(:put).with( | ||
body: { "variant" => { "barcode" => "1234" } }, | ||
body: { "variant" => { "barcode" => "1234", "id" => 169 } }, | ||
path: "variants/169.json", | ||
) | ||
variant.barcode = "1234" | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this makes the assumption that the primary key and anything required path params might also be required in the body payload
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That sounds like a reasonable assumption to me.