Skip to content

Commit 120465a

Browse files
Enhance case conversion documentation and testing
- Add comprehensive README documentation with configuration and examples - Add tests for nested structures, arrays, and mixed data types - Add test fixture for complex nested GraphQL responses - Cover edge cases like empty arrays and nil values 🤖 Generated with [Claude Code](https://claude.ai/code) Co-authored-by: Kirill Platonov <kirillplatonov@users.noreply.github.com>
1 parent a3d5118 commit 120465a

File tree

3 files changed

+436
-0
lines changed

3 files changed

+436
-0
lines changed

README.md

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,119 @@ shop.with_shopify_session do
3636
end
3737
```
3838

39+
## Configuration
40+
41+
### Case Conversion
42+
43+
The gem supports automatic case conversion between Ruby's snake_case conventions and GraphQL's camelCase conventions. This feature is disabled by default to maintain backward compatibility.
44+
45+
To enable case conversion, configure it in your initializer:
46+
47+
```rb
48+
# config/initializers/shopify_graphql.rb
49+
ShopifyGraphql.configure do |config|
50+
config.convert_case = true
51+
end
52+
```
53+
54+
When enabled, the gem will:
55+
- Convert snake_case variables to camelCase when sending GraphQL requests
56+
- Convert camelCase response keys to snake_case for easier access in Ruby
57+
58+
<details><summary>Example: Request Variable Conversion</summary>
59+
60+
```rb
61+
# With convert_case = true
62+
class GetProduct
63+
include ShopifyGraphql::Query
64+
65+
QUERY = <<~GRAPHQL
66+
query($product_id: ID!, $include_variants: Boolean!) {
67+
product(id: $product_id) {
68+
title
69+
variants(first: 10) @include(if: $include_variants) {
70+
edges {
71+
node {
72+
displayName
73+
}
74+
}
75+
}
76+
}
77+
}
78+
GRAPHQL
79+
80+
def call(product_id:, include_variants: false)
81+
# Variables are automatically converted: product_id → productId, include_variants → includeVariants
82+
response = execute(QUERY, product_id: product_id, include_variants: include_variants)
83+
response.data.product
84+
end
85+
end
86+
87+
# Usage
88+
product = GetProduct.call(
89+
product_id: "gid://shopify/Product/12345",
90+
include_variants: true
91+
)
92+
```
93+
94+
</details>
95+
96+
<details><summary>Example: Response Key Conversion</summary>
97+
98+
```rb
99+
# With convert_case = true
100+
class GetProduct
101+
include ShopifyGraphql::Query
102+
103+
QUERY = <<~GRAPHQL
104+
query($id: ID!) {
105+
product(id: $id) {
106+
title
107+
featuredImage {
108+
originalSrc
109+
altText
110+
}
111+
variants(first: 5) {
112+
edges {
113+
node {
114+
displayName
115+
selectedOptions {
116+
name
117+
value
118+
}
119+
}
120+
}
121+
}
122+
}
123+
}
124+
GRAPHQL
125+
126+
def call(id:)
127+
response = execute(QUERY, id: id)
128+
response.data.product
129+
end
130+
end
131+
132+
# Usage - all keys are automatically converted to snake_case
133+
product = GetProduct.call(id: "gid://shopify/Product/12345")
134+
135+
puts product.title
136+
puts product.featured_image.original_src # originalSrc → original_src
137+
puts product.featured_image.alt_text # altText → alt_text
138+
139+
product.variants.edges.each do |edge|
140+
puts edge.node.display_name # displayName → display_name
141+
142+
edge.node.selected_options.each do |option|
143+
puts "#{option.name}: #{option.value}" # Keys converted automatically
144+
end
145+
end
146+
```
147+
148+
</details>
149+
150+
**Note:** Case conversion adds a small performance overhead due to key transformation. Enable it only if you prefer Ruby naming conventions over GraphQL's camelCase.
151+
39152
## Conventions
40153

41154
To better organize your Graphql code use the following conventions:

test/configuration_test.rb

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,4 +65,219 @@ class ConfigurationTest < ActiveSupport::TestCase
6565
assert_equal "Graphql Gem Test", response.data.shop.name
6666
assert_equal "graphql-gem-test.myshopify.com", response.data.shop.myshopifyDomain
6767
end
68+
69+
test "nested structures are converted to snake_case when enabled" do
70+
ShopifyGraphql.configure { |config| config.convert_case = true }
71+
72+
query = <<~GRAPHQL
73+
query($id: ID!) {
74+
product(id: $id) {
75+
title
76+
featuredImage {
77+
originalSrc
78+
altText
79+
transformedSrc
80+
}
81+
variants(first: 5) {
82+
edges {
83+
node {
84+
displayName
85+
selectedOptions {
86+
name
87+
value
88+
}
89+
price {
90+
amount
91+
currencyCode
92+
}
93+
}
94+
}
95+
}
96+
metafields(first: 10) {
97+
edges {
98+
node {
99+
namespace
100+
key
101+
value
102+
createdAt
103+
updatedAt
104+
}
105+
}
106+
}
107+
seo {
108+
title
109+
description
110+
}
111+
}
112+
}
113+
GRAPHQL
114+
115+
fake("queries/nested_product.json", query, id: "gid://shopify/Product/12345")
116+
117+
client = ShopifyGraphql::Client.new
118+
response = client.execute(query, id: "gid://shopify/Product/12345")
119+
product = response.data.product
120+
121+
# Test top-level object
122+
assert_equal "Complex Product", product.title
123+
124+
# Test nested object keys converted to snake_case
125+
assert_equal "https://example.com/image.jpg", product.featured_image.original_src
126+
assert_equal "Product image", product.featured_image.alt_text
127+
assert_equal "https://example.com/image_400x400.jpg", product.featured_image.transformed_src
128+
129+
# Test array of nested objects
130+
assert_equal 2, product.variants.edges.length
131+
132+
first_variant = product.variants.edges.first.node
133+
assert_equal "Small / Red", first_variant.display_name
134+
assert_equal "29.99", first_variant.price.amount
135+
assert_equal "USD", first_variant.price.currency_code
136+
137+
# Test deeply nested arrays
138+
assert_equal 2, first_variant.selected_options.length
139+
assert_equal "Size", first_variant.selected_options.first.name
140+
assert_equal "Small", first_variant.selected_options.first.value
141+
142+
# Test multiple levels of nesting
143+
second_variant = product.variants.edges.last.node
144+
assert_equal "Large / Blue", second_variant.display_name
145+
assert_equal "https://example.com/variant2.jpg", second_variant.image.original_src
146+
assert_equal "Blue variant", second_variant.image.alt_text
147+
148+
# Test metafields array conversion
149+
assert_equal 2, product.metafields.edges.length
150+
first_metafield = product.metafields.edges.first.node
151+
assert_equal "care_instructions", first_metafield.key
152+
assert_equal "Machine wash cold", first_metafield.value
153+
assert_equal "2024-01-01T00:00:00Z", first_metafield.created_at
154+
assert_equal "2024-01-02T00:00:00Z", product.metafields.edges.last.node.updated_at
155+
156+
# Test simple nested object
157+
assert_equal "SEO Title", product.seo.title
158+
assert_equal "SEO Description", product.seo.description
159+
end
160+
161+
test "nested structures maintain camelCase when convert_case is disabled" do
162+
ShopifyGraphql.configure { |config| config.convert_case = false }
163+
164+
query = <<~GRAPHQL
165+
query($id: ID!) {
166+
product(id: $id) {
167+
title
168+
featuredImage {
169+
originalSrc
170+
altText
171+
}
172+
variants(first: 5) {
173+
edges {
174+
node {
175+
displayName
176+
selectedOptions {
177+
name
178+
value
179+
}
180+
}
181+
}
182+
}
183+
}
184+
}
185+
GRAPHQL
186+
187+
fake("queries/nested_product.json", query, id: "gid://shopify/Product/12345")
188+
189+
client = ShopifyGraphql::Client.new
190+
response = client.execute(query, id: "gid://shopify/Product/12345")
191+
product = response.data.product
192+
193+
# Keys should maintain original camelCase
194+
assert_equal "https://example.com/image.jpg", product.featuredImage.originalSrc
195+
assert_equal "Product image", product.featuredImage.altText
196+
assert_equal "Small / Red", product.variants.edges.first.node.displayName
197+
assert_equal "Size", product.variants.edges.first.node.selectedOptions.first.name
198+
end
199+
200+
test "empty arrays and nil values handled correctly with case conversion" do
201+
ShopifyGraphql.configure { |config| config.convert_case = true }
202+
203+
# Create a fixture with empty/nil values
204+
empty_response = {
205+
data: {
206+
product: {
207+
title: "Empty Product",
208+
featuredImage: nil,
209+
variants: {
210+
edges: []
211+
},
212+
metafields: {
213+
edges: []
214+
}
215+
}
216+
}
217+
}
218+
219+
query = "{ product { title featuredImage variants { edges } metafields { edges } } }"
220+
stub_request(:post, "https://test-shop.myshopify.com/admin/api/2024-07/graphql.json")
221+
.with(body: { query: query, variables: {} })
222+
.to_return(body: empty_response.to_json)
223+
224+
client = ShopifyGraphql::Client.new
225+
response = client.execute(query)
226+
product = response.data.product
227+
228+
assert_equal "Empty Product", product.title
229+
assert_nil product.featured_image
230+
assert_equal [], product.variants.edges
231+
assert_equal [], product.metafields.edges
232+
end
233+
234+
test "mixed data types preserved with case conversion" do
235+
ShopifyGraphql.configure { |config| config.convert_case = true }
236+
237+
mixed_response = {
238+
data: {
239+
product: {
240+
title: "Mixed Product",
241+
isActive: true,
242+
priceRange: {
243+
minVariantPrice: {
244+
amount: "10.50",
245+
currencyCode: "USD"
246+
},
247+
maxVariantPrice: {
248+
amount: "25.99",
249+
currencyCode: "USD"
250+
}
251+
},
252+
tags: ["summer", "sale", "new-arrival"],
253+
productType: "Clothing",
254+
totalInventory: 100
255+
}
256+
}
257+
}
258+
259+
query = "{ product { title isActive priceRange { minVariantPrice { amount currencyCode } maxVariantPrice { amount currencyCode } } tags productType totalInventory } }"
260+
stub_request(:post, "https://test-shop.myshopify.com/admin/api/2024-07/graphql.json")
261+
.with(body: { query: query, variables: {} })
262+
.to_return(body: mixed_response.to_json)
263+
264+
client = ShopifyGraphql::Client.new
265+
response = client.execute(query)
266+
product = response.data.product
267+
268+
# Boolean preserved
269+
assert_equal true, product.is_active
270+
271+
# Nested objects converted
272+
assert_equal "10.50", product.price_range.min_variant_price.amount
273+
assert_equal "USD", product.price_range.min_variant_price.currency_code
274+
assert_equal "25.99", product.price_range.max_variant_price.amount
275+
276+
# Arrays preserved
277+
assert_equal ["summer", "sale", "new-arrival"], product.tags
278+
279+
# String and numbers preserved
280+
assert_equal "Clothing", product.product_type
281+
assert_equal 100, product.total_inventory
282+
end
68283
end

0 commit comments

Comments
 (0)