Skip to content

Commit de52ab2

Browse files
committed
Provide defaults for attributes
1 parent 59b2417 commit de52ab2

File tree

3 files changed

+135
-17
lines changed

3 files changed

+135
-17
lines changed

lib/active_shopify_graphql/base.rb

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,9 @@ def graphql_type_for_loader(loader_class)
3838
# @param path [String] The GraphQL field path (auto-inferred if not provided)
3939
# @param type [Symbol] The type for coercion (:string, :integer, :float, :boolean, :datetime). Arrays are preserved automatically.
4040
# @param null [Boolean] Whether the attribute can be null (default: true)
41+
# @param default [Object] Default value to use when the GraphQL response is nil
4142
# @param transform [Proc] Custom transform block for the value
42-
def attribute(name, path: nil, type: :string, null: true, transform: nil)
43+
def attribute(name, path: nil, type: :string, null: true, default: nil, transform: nil)
4344
@base_attributes ||= {}
4445

4546
# Auto-infer GraphQL path for simple cases: display_name -> displayName
@@ -49,6 +50,7 @@ def attribute(name, path: nil, type: :string, null: true, transform: nil)
4950
path: path,
5051
type: type,
5152
null: null,
53+
default: default,
5254
transform: transform
5355
}
5456

@@ -62,8 +64,9 @@ def attribute(name, path: nil, type: :string, null: true, transform: nil)
6264
# @param key [String] The metafield key
6365
# @param type [Symbol] The type for coercion (:string, :integer, :float, :boolean, :datetime, :json). Arrays are preserved automatically.
6466
# @param null [Boolean] Whether the attribute can be null (default: true)
67+
# @param default [Object] Default value to use when the GraphQL response is nil
6568
# @param transform [Proc] Custom transform block for the value
66-
def metafield_attribute(name, namespace:, key:, type: :string, null: true, transform: nil)
69+
def metafield_attribute(name, namespace:, key:, type: :string, null: true, default: nil, transform: nil)
6770
@base_attributes ||= {}
6871
@metafields ||= {}
6972

@@ -83,6 +86,7 @@ def metafield_attribute(name, namespace:, key:, type: :string, null: true, trans
8386
path: path,
8487
type: type,
8588
null: null,
89+
default: default,
8690
transform: transform,
8791
is_metafield: true,
8892
metafield_alias: alias_name,
@@ -135,13 +139,13 @@ def metafields
135139
private
136140

137141
# Override attribute method to handle loader context
138-
def attribute_with_context(name, path: nil, type: :string, null: true, transform: nil)
142+
def attribute_with_context(name, path: nil, type: :string, null: true, default: nil, transform: nil)
139143
if @current_loader_context
140144
# Auto-infer path if not provided
141145
path ||= infer_path(name)
142-
@loader_contexts[@current_loader_context][name] = { path: path, type: type, null: null, transform: transform }
146+
@loader_contexts[@current_loader_context][name] = { path: path, type: type, null: null, default: default, transform: transform }
143147
else
144-
attribute_without_context(name, path: path, type: type, null: null, transform: transform)
148+
attribute_without_context(name, path: path, type: type, null: null, default: default, transform: transform)
145149
end
146150

147151
# Always create attr_accessor for the attribute on base model
@@ -174,6 +178,7 @@ def metafield_attribute_with_context(name, **options)
174178
path: path,
175179
type: type,
176180
null: options[:null] || true,
181+
default: options[:default],
177182
transform: options[:transform],
178183
is_metafield: true,
179184
metafield_alias: alias_name,

lib/active_shopify_graphql/loader.rb

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ def model_class
3131
attr_writer :model_class
3232

3333
# For backward compatibility - loaders can still define attributes directly
34-
def attribute(name, path: nil, type: :string, null: true, transform: nil)
34+
def attribute(name, path: nil, type: :string, null: true, default: nil, transform: nil)
3535
@attributes ||= {}
3636

3737
# Auto-infer GraphQL path for simple cases: display_name -> displayName
@@ -41,12 +41,13 @@ def attribute(name, path: nil, type: :string, null: true, transform: nil)
4141
path: path,
4242
type: type,
4343
null: null,
44+
default: default,
4445
transform: transform
4546
}
4647
end
4748

4849
# For backward compatibility - loaders can still define metafield attributes directly
49-
def metafield_attribute(name, namespace:, key:, type: :string, null: true, transform: nil)
50+
def metafield_attribute(name, namespace:, key:, type: :string, null: true, default: nil, transform: nil)
5051
@attributes ||= {}
5152
@metafields ||= {}
5253

@@ -66,6 +67,7 @@ def metafield_attribute(name, namespace:, key:, type: :string, null: true, trans
6667
path: path,
6768
type: type,
6869
null: null,
70+
default: default,
6971
transform: transform,
7072
is_metafield: true,
7173
metafield_alias: alias_name,
@@ -436,18 +438,25 @@ def map_response_from_attributes(response_data)
436438
# Use dig to safely extract the value
437439
value = root_data.dig(*path_parts)
438440

439-
# Validate null constraint
440-
raise ArgumentError, "Attribute '#{attr_name}' (GraphQL path: '#{path}') cannot be null but received nil" if !config[:null] && value.nil?
441-
442-
# Apply type coercion if value is not nil
441+
# Handle nil values with defaults or transforms
443442
if value.nil?
444-
result[attr_name] = nil
445-
else
446-
# Apply custom transform first, then type coercion
447-
value = config[:transform].call(value) if config[:transform]
448-
449-
result[attr_name] = coerce_value(value, config[:type], attr_name, path)
443+
# Use default value if provided (more efficient than transform for simple defaults)
444+
if !config[:default].nil?
445+
value = config[:default]
446+
elsif config[:transform]
447+
# Only call transform if no default is provided
448+
value = config[:transform].call(value)
449+
end
450+
elsif config[:transform]
451+
# Apply transform to non-nil values
452+
value = config[:transform].call(value)
450453
end
454+
455+
# Validate null constraint after applying defaults/transforms
456+
raise ArgumentError, "Attribute '#{attr_name}' (GraphQL path: '#{path}') cannot be null but received nil" if !config[:null] && value.nil?
457+
458+
# Apply type coercion
459+
result[attr_name] = value.nil? ? nil : coerce_value(value, config[:type], attr_name, path)
451460
end
452461

453462
result

spec/active_shopify_graphql/metafield_attributes_spec.rb

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,110 @@ def execute_graphql_query(_query, **_variables)
199199
expect(attributes[:tags]).to eq(%w[TAG1 TAG2])
200200
end
201201

202+
it "handles nil metafields with default values" do
203+
default_loader = Class.new(ActiveShopifyGraphQL::Loader) do
204+
graphql_type "Product"
205+
206+
metafield_attribute :missing_string, namespace: 'custom', key: 'missing_str', type: :string,
207+
default: "default_value"
208+
209+
metafield_attribute :missing_json, namespace: 'custom', key: 'missing_json', type: :json,
210+
default: { "default" => true }
211+
212+
metafield_attribute :missing_integer, namespace: 'custom', key: 'missing_int', type: :integer,
213+
default: 42
214+
215+
def execute_graphql_query(_query, **_variables)
216+
{
217+
"data" => {
218+
"product" => {
219+
"missing_stringMetafield" => nil,
220+
"missing_jsonMetafield" => nil,
221+
"missing_integerMetafield" => nil
222+
}
223+
}
224+
}
225+
end
226+
end
227+
228+
loader = default_loader.new
229+
attributes = loader.load_attributes("test-id")
230+
231+
expect(attributes[:missing_string]).to eq("default_value")
232+
expect(attributes[:missing_json]).to eq({ "default" => true })
233+
expect(attributes[:missing_integer]).to eq(42)
234+
end
235+
236+
it "handles nil metafields with transform blocks providing defaults" do
237+
transform_loader = Class.new(ActiveShopifyGraphQL::Loader) do
238+
graphql_type "Product"
239+
240+
metafield_attribute :missing_string, namespace: 'custom', key: 'missing', type: :string,
241+
transform: ->(value) { value.nil? ? "transform_default" : value }
242+
243+
metafield_attribute :missing_json, namespace: 'custom', key: 'json', type: :json,
244+
transform: ->(value) { value.nil? ? { "transform" => true } : value }
245+
246+
def execute_graphql_query(_query, **_variables)
247+
{
248+
"data" => {
249+
"product" => {
250+
"missing_stringMetafield" => nil,
251+
"missing_jsonMetafield" => nil
252+
}
253+
}
254+
}
255+
end
256+
end
257+
258+
loader = transform_loader.new
259+
attributes = loader.load_attributes("test-id")
260+
261+
expect(attributes[:missing_string]).to eq("transform_default")
262+
expect(attributes[:missing_json]).to eq({ "transform" => true })
263+
end
264+
265+
it "prefers default over transform for nil values (optimization)" do
266+
call_count = 0
267+
268+
mixed_loader = Class.new(ActiveShopifyGraphQL::Loader) do
269+
graphql_type "Product"
270+
271+
# This should use default and NOT call transform
272+
metafield_attribute :with_default, namespace: 'custom', key: 'def', type: :string,
273+
default: "default_used",
274+
transform: lambda { |value|
275+
call_count += 1
276+
"transform_should_not_be_called"
277+
}
278+
279+
# This should call transform since no default
280+
metafield_attribute :with_transform, namespace: 'custom', key: 'trans', type: :string,
281+
transform: lambda { |value|
282+
call_count += 1
283+
"transform_called"
284+
}
285+
286+
def execute_graphql_query(_query, **_variables)
287+
{
288+
"data" => {
289+
"product" => {
290+
"with_defaultMetafield" => nil,
291+
"with_transformMetafield" => nil
292+
}
293+
}
294+
}
295+
end
296+
end
297+
298+
loader = mixed_loader.new
299+
attributes = loader.load_attributes("test-id")
300+
301+
expect(attributes[:with_default]).to eq("default_used")
302+
expect(attributes[:with_transform]).to eq("transform_called")
303+
expect(call_count).to eq(1) # Only transform should be called once
304+
end
305+
202306
it "generates unique aliases for metafields with same namespace/key but different names" do
203307
multi_loader = Class.new(ActiveShopifyGraphQL::Loader) do
204308
graphql_type "Product"

0 commit comments

Comments
 (0)