zero_ruby 0.1.0.alpha4 → 0.1.0.alpha5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8d78f7995f5a44093ed0202e53dd345587f73a2d2a675e31c158fe01ed392e6e
4
- data.tar.gz: 9fbbb4cdce370fca8c035e0fad0cd0dbbeaa9ace5cf92061cdab6274223987ae
3
+ metadata.gz: 68451ec0c319aab33578ee4ac8aa28f61426ade64f4185047528387c5c95f3e5
4
+ data.tar.gz: fc5b90718c5c0aa5ee11020b02e01d422cee0108bb1095c9aac8eb184dd6c050
5
5
  SHA512:
6
- metadata.gz: 6b4e963190e7911e4b9b137933165bd20eebabf2e9a183f9ae7404e0f3ab6b00c2f321098fdf58aaf2ab84fbca7490e1e82b47bb5ed2e3616aa10151173ba2ba
7
- data.tar.gz: 35b53c46c18655067e7e393893539fa451baf956a3f3a827cd9edfd340e33b29eb55ef9327297ae279a3604717244f3d04cf5ddb2bba5f28308b200c6f724b19
6
+ metadata.gz: b60c2a285735db948bf33ebd52f78fc09329ed3c0a012cc000dab2eda668bf37920058dd4444317ff8bde80fcf70713014ed24186037fdf6b9acf35f32f3dc6b
7
+ data.tar.gz: bb7c53048a48d3c65c0303be1d67bf59368e3a2eec7ae8ce482c50e94304a8295be05ffb062e4591162a5d2cfb53f66d322b807e9c9c26c0039ec5ac3967e5d9
data/README.md CHANGED
@@ -27,7 +27,7 @@ By default, the entire `execute` method runs inside a transaction with LMID (Las
27
27
  # app/zero/mutations/post_update.rb
28
28
  module Mutations
29
29
  class PostUpdate < ApplicationMutation
30
- argument :id, ID
30
+ argument :id, Types::ID
31
31
  argument :post_input, Types::PostInput
32
32
 
33
33
  def execute(id:, post_input:)
@@ -101,14 +101,14 @@ module Types
101
101
  class PostInput < Types::BaseInputObject
102
102
  argument :title, Types::String.constrained(min_size: 1, max_size: 200)
103
103
  argument :body, Types::String.optional
104
- argument :published, Boolean.default(false)
104
+ argument :published, Types::Boolean.default(false)
105
105
  end
106
106
  end
107
107
  ```
108
108
 
109
109
  ## Configuration
110
110
 
111
- Create an initializer to customize settings (all options have sensible defaults):
111
+ Create an initializer to customize settings:
112
112
 
113
113
  ```ruby
114
114
  # config/initializers/zero_ruby.rb
@@ -168,29 +168,24 @@ export type Mutators = typeof mutators
168
168
 
169
169
  ## Types
170
170
 
171
- ZeroRuby provides types built on [dry-types](https://dry-rb.org/gems/dry-types/). When you inherit from `ZeroRuby::Mutation` or `ZeroRuby::InputObject`, the following are available:
172
-
173
- - **Direct constants**: `ID`, `Boolean`, `ISO8601Date`, `ISO8601DateTime`
174
- - **Via Types module**: `Types::String`, `Types::Integer`, `Types::Float`
175
-
176
- (Note: `String`, `Integer`, `Float` can't be direct constants because they conflict with Ruby's built-in classes)
171
+ ZeroRuby provides types built on [dry-types](https://dry-rb.org/gems/dry-types/). When you inherit from `ZeroRuby::Mutation` or `ZeroRuby::InputObject`, types are available via the `Types` module:
177
172
 
178
173
  ```ruby
179
174
  # Basic types
180
175
  argument :name, Types::String
181
176
  argument :count, Types::Integer
182
177
  argument :price, Types::Float
183
- argument :active, Boolean
184
- argument :id, ID # Non-empty string
185
- argument :date, ISO8601Date
186
- argument :timestamp, ISO8601DateTime
178
+ argument :active, Types::Boolean
179
+ argument :id, Types::ID # Non-empty string
180
+ argument :date, Types::ISO8601Date
181
+ argument :timestamp, Types::ISO8601DateTime
187
182
 
188
183
  # Optional types (accepts nil)
189
184
  argument :nickname, Types::String.optional
190
185
 
191
186
  # Default values
192
187
  argument :status, Types::String.default("draft")
193
- argument :enabled, Boolean.default(false)
188
+ argument :enabled, Types::Boolean.default(false)
194
189
  ```
195
190
 
196
191
  ## Validation with Constraints
@@ -260,7 +255,7 @@ By default, the entire `execute` method runs inside a transaction in order to at
260
255
  class PostUpdate < ApplicationMutation
261
256
  skip_auto_transaction
262
257
 
263
- argument :id, ID
258
+ argument :id, Types::ID
264
259
  argument :post_input, Types::PostInput
265
260
 
266
261
  def execute(id:, post_input:)
@@ -21,7 +21,9 @@ module ZeroRuby
21
21
  field = match ? match[1] : "field"
22
22
  ["#{field} is required"]
23
23
  elsif message.include?("has invalid type")
24
- match = message.match(/:(\w+) has invalid type/)
24
+ # Matches both ":title has invalid type" and "has invalid type for :title"
25
+ match = message.match(/:(\w+) has invalid type/) ||
26
+ message.match(/has invalid type for :(\w+)/)
25
27
  field = match ? match[1] : "field"
26
28
  ["#{field}: invalid type"]
27
29
  elsif message.include?("violates constraints")
@@ -81,7 +83,7 @@ module ZeroRuby
81
83
  begin
82
84
  result = MESSAGES.call(key, path: [:base], tokens: build_tokens(key, args))
83
85
  result&.text
84
- rescue StandardError
86
+ rescue
85
87
  nil
86
88
  end
87
89
  end
@@ -91,7 +91,7 @@ module ZeroRuby
91
91
  # Raised for database transaction errors.
92
92
  # Triggers top-level PushFailed with reason: "database"
93
93
  # Wraps unexpected errors during transaction execution (matching TS behavior).
94
- class DatabaseTransactionError < Error
94
+ class TransactionError < Error
95
95
  end
96
96
 
97
97
  # Raised when push data is malformed or missing required fields.
@@ -8,21 +8,20 @@ module ZeroRuby
8
8
  # Base class for input objects (nested argument types).
9
9
  # Uses Dry::Struct for type coercion and validation.
10
10
  #
11
- # Includes ZeroRuby::TypeNames for convenient type access:
12
- # - ID, Boolean, ISO8601Date, ISO8601DateTime (direct constants)
13
- # - Types::String, Types::Integer, Types::Float (via Types module)
11
+ # Includes ZeroRuby::TypeNames for convenient type access via the Types module
12
+ # (e.g., Types::String, Types::ID, Types::Boolean).
14
13
  #
15
14
  # @example
16
15
  # class Types::PostInput < ZeroRuby::InputObject
17
- # argument :id, ID
16
+ # argument :id, Types::ID
18
17
  # argument :title, Types::String.constrained(min_size: 1, max_size: 200)
19
18
  # argument :body, Types::String.optional
20
- # argument :published, Boolean.default(false)
19
+ # argument :published, Types::Boolean.default(false)
21
20
  # end
22
21
  #
23
22
  # class PostCreate < ZeroRuby::Mutation
24
23
  # argument :post_input, Types::PostInput
25
- # argument :notify, Boolean.default(false)
24
+ # argument :notify, Types::Boolean.default(false)
26
25
  #
27
26
  # def execute
28
27
  # # args[:post_input].title, args[:post_input].body, etc.
@@ -3,21 +3,21 @@
3
3
  require_relative "types"
4
4
  require_relative "type_names"
5
5
  require_relative "errors"
6
+ require_relative "error_formatter"
6
7
 
7
8
  module ZeroRuby
8
9
  # Base class for Zero mutations.
9
10
  # Provides argument DSL with dry-types validation.
10
11
  #
11
- # Includes ZeroRuby::TypeNames for convenient type access:
12
- # - ID, Boolean, ISO8601Date, ISO8601DateTime (direct constants)
13
- # - Types::String, Types::Integer, Types::Float (via Types module)
12
+ # Includes ZeroRuby::TypeNames for convenient type access via the Types module
13
+ # (e.g., Types::String, Types::ID, Types::Boolean).
14
14
  #
15
15
  # By default (auto_transact: true), the entire execute method runs inside
16
16
  # a transaction with LMID tracking. For 3-phase control, set auto_transact false.
17
17
  #
18
18
  # @example Simple mutation (auto_transact: true, default)
19
19
  # class WorkCreate < ZeroRuby::Mutation
20
- # argument :id, ID
20
+ # argument :id, Types::ID
21
21
  # argument :title, Types::String.constrained(max_size: 200)
22
22
  #
23
23
  # def execute(id:, title:)
@@ -30,7 +30,7 @@ module ZeroRuby
30
30
  # class WorkUpdate < ZeroRuby::Mutation
31
31
  # skip_auto_transaction
32
32
  #
33
- # argument :id, ID
33
+ # argument :id, Types::ID
34
34
  # argument :title, Types::String
35
35
  #
36
36
  # def execute(id:, title:)
@@ -92,16 +92,21 @@ module ZeroRuby
92
92
  end
93
93
 
94
94
  # Coerce and validate raw arguments.
95
- # Collects "field is required" errors for all missing required fields,
96
- # but lets Dry exceptions bubble up for type/constraint errors.
95
+ # Collects ALL validation errors (missing fields, type coercion, constraints)
96
+ # and raises a single ValidationError with all issues.
97
97
  #
98
- # @param raw_args [Hash] Raw input arguments
99
- # @return [Hash] Validated and coerced arguments (may contain InputObject instances)
100
- # @raise [ZeroRuby::ValidationError] If required fields are missing
101
- # @raise [Dry::Types::CoercionError] If type coercion fails
102
- # @raise [Dry::Types::ConstraintError] If constraint validation fails
103
- # @raise [Dry::Struct::Error] If InputObject validation fails
98
+ # Uses type.try(value) which returns a Result instead of raising, allowing
99
+ # us to collect all errors in one pass rather than failing on the first one.
100
+ # Works for both Dry::Types (scalars) and Dry::Struct (InputObjects).
101
+ #
102
+ # @param raw_args [Hash] Raw input arguments (string keys from JSON)
103
+ # @return [Hash] Validated and coerced arguments (symbol keys, may contain InputObject instances)
104
+ # @raise [ZeroRuby::ValidationError] If any validation fails
104
105
  def coerce_and_validate!(raw_args)
106
+ # Result hash: symbol keys → coerced values (strings, integers, InputObject instances, etc.)
107
+ # eg:
108
+ # raw_args: {"name" => "test", "count" => "5"} # string keys, raw values
109
+ # validated: {name: "test", count: 5} # symbol keys, coerced values
105
110
  validated = {}
106
111
  errors = []
107
112
 
@@ -110,47 +115,38 @@ module ZeroRuby
110
115
  str_key = name.to_s
111
116
  key_present = raw_args.key?(str_key)
112
117
  value = raw_args[str_key]
118
+ is_input_object = input_object_type?(type)
113
119
 
114
- # Handle InputObject types (subclasses of InputObject)
115
- if input_object_type?(type)
116
- if value.nil? && key_present
117
- # Explicit nil
118
- validated[name] = nil
119
- elsif value.nil? && !key_present
120
- # Missing key - check if required
121
- if required_type?(type)
122
- errors << "#{name} is required"
123
- end
124
- # Skip if optional and not provided
125
- else
126
- # Let Dry::Struct errors bubble up
127
- validated[name] = type.new(value)
128
- end
129
- next
130
- end
131
-
132
- # Handle missing key
133
- if !key_present
120
+ # Missing key: use default if available, otherwise error if required
121
+ unless key_present
134
122
  if has_default?(type)
135
123
  validated[name] = get_default(type)
136
124
  elsif required_type?(type)
137
125
  errors << "#{name} is required"
138
126
  end
127
+ # Optional fields without defaults are simply omitted from result
139
128
  next
140
129
  end
141
130
 
142
- # Handle explicit null
131
+ # Explicit null: InputObjects always allow nil (they handle optionality internally),
132
+ # scalars only allow nil if the type is optional
143
133
  if value.nil?
144
- if required_type?(type)
145
- errors << "#{name} is required"
146
- else
134
+ if is_input_object || !required_type?(type)
147
135
  validated[name] = nil
136
+ else
137
+ errors << "#{name} is required"
148
138
  end
149
139
  next
150
140
  end
151
141
 
152
- # Coerce and validate value - let Dry exceptions bubble up
153
- validated[name] = type[value]
142
+ # Coerce value: type.try returns Result instead of raising, so we can
143
+ # collect all errors. Works for both Dry::Types and Dry::Struct.
144
+ result = type.try(value)
145
+ if result.failure?
146
+ errors << format_type_error(name, result.error, is_input_object)
147
+ else
148
+ validated[name] = result.input
149
+ end
154
150
  end
155
151
 
156
152
  raise ValidationError.new(errors) if errors.any?
@@ -180,6 +176,28 @@ module ZeroRuby
180
176
  return true unless type.respond_to?(:optional?)
181
177
  !type.optional? && !has_default?(type)
182
178
  end
179
+
180
+ # Format a type error (coercion or constraint failure)
181
+ # @param name [Symbol] The field name
182
+ # @param error [Exception] The error from try.failure
183
+ # @param is_input_object [Boolean] Whether this is an InputObject type
184
+ # @return [String] Formatted error message
185
+ def format_type_error(name, error, is_input_object = false)
186
+ if is_input_object && error.is_a?(Dry::Struct::Error)
187
+ # InputObject errors get prefixed with field name
188
+ ErrorFormatter.format_struct_error(error).map { |m| "#{name}.#{m}" }.first
189
+ else
190
+ message = case error
191
+ when Dry::Types::CoercionError
192
+ ErrorFormatter.format_coercion_error(error)
193
+ when Dry::Types::ConstraintError
194
+ ErrorFormatter.format_constraint_error(error)
195
+ else
196
+ error.message
197
+ end
198
+ "#{name}: #{message}"
199
+ end
200
+ end
183
201
  end
184
202
 
185
203
  # Initialize a mutation with raw arguments and context
@@ -194,9 +212,20 @@ module ZeroRuby
194
212
  # @param transact_proc [Proc] Block that wraps transactional work (internal use)
195
213
  # @return [Hash] Empty hash on success, or {data: ...} if execute returns a Hash
196
214
  # @raise [ZeroRuby::Error] On failure (formatted at boundary)
215
+ # @raise [ZeroRuby::TransactNotCalledError] If skip_auto_transaction and transact not called
197
216
  def call(&transact_proc)
198
217
  @transact_proc = transact_proc
199
- data = execute(**@args)
218
+ @transact_called = false
219
+
220
+ if self.class.skip_auto_transaction?
221
+ # Manual mode: Use defined mutation calls transact {}
222
+ data = execute(**@args)
223
+ raise TransactNotCalledError.new unless @transact_called
224
+ else
225
+ # Auto mode: wrap entire execute in transaction
226
+ data = transact_proc.call { execute(**@args) }
227
+ end
228
+
200
229
  result = {}
201
230
  result[:data] = data if data.is_a?(Hash) && !data.empty?
202
231
  result
@@ -205,6 +234,7 @@ module ZeroRuby
205
234
  private
206
235
 
207
236
  # Wrap database operations in a transaction with LMID tracking.
237
+ # Used by user defined mutation.
208
238
  #
209
239
  # Behavior depends on skip_auto_transaction:
210
240
  # - Default (no skip) - Just executes the block (already in transaction)
@@ -217,12 +247,13 @@ module ZeroRuby
217
247
  # @return [Object] Result of the block
218
248
  def transact(&block)
219
249
  raise "transact requires a block" unless block_given?
250
+ @transact_called = true
220
251
 
221
- if @transact_proc
222
- # Manual transact mode (auto_transact: false) - use the provided proc
252
+ if self.class.skip_auto_transaction?
253
+ # Manual mode - actually call the transact_proc to start transaction
223
254
  @transact_proc.call(&block)
224
255
  else
225
- # Auto transact mode - already in transaction, just execute the block
256
+ # Auto mode - already in transaction, just execute the block
226
257
  block.call
227
258
  end
228
259
  end
@@ -37,10 +37,6 @@ module ZeroRuby
37
37
  mutations.each_with_index do |mutation_data, index|
38
38
  result = process_mutation_with_lmid(mutation_data, client_group_id, context)
39
39
  results << result
40
- rescue MutationNotFoundError => e
41
- # Unknown mutation - return error response and continue batch
42
- mutation_id_obj = {id: mutation_data["id"], clientID: mutation_data["clientID"]}
43
- results << {id: mutation_id_obj, result: format_error_response(e)}
44
40
  rescue OutOfOrderMutationError => e
45
41
  # Return top-level PushFailedBody with all unprocessed mutation IDs
46
42
  unprocessed_ids = mutations[index..].map { |m| {id: m["id"], clientID: m["clientID"]} }
@@ -51,7 +47,7 @@ module ZeroRuby
51
47
  message: e.message,
52
48
  mutationIDs: unprocessed_ids
53
49
  }
54
- rescue DatabaseTransactionError => e
50
+ rescue TransactionError => e
55
51
  # Database errors trigger top-level PushFailed per Zero protocol
56
52
  unprocessed_ids = mutations[index..].map { |m| {id: m["id"], clientID: m["clientID"]} }
57
53
  return {
@@ -70,21 +66,10 @@ module ZeroRuby
70
66
 
71
67
  # Process a single mutation with LMID validation, transaction support, and phase tracking.
72
68
  #
73
- # Supports two execution modes:
69
+ # The Mutation#call method decides whether to auto-wrap execute in a transaction
70
+ # (default behavior) or pass control to user code (skip_auto_transaction mode).
74
71
  #
75
- # **Default (auto-transaction)**
76
- # - Entire execute method is wrapped in a transaction with LMID tracking
77
- # - User does NOT need to call transact {} - it's automatic
78
- # - Simple mutations benefit from less boilerplate
79
- #
80
- # **skip_auto_transaction**
81
- # - Uses three-phase model matching Zero's TypeScript implementation:
82
- # 1. Pre-transaction: User code runs before calling transact (auth, validation)
83
- # 2. Transaction: User code inside transact { } block (database operations, LMID tracking)
84
- # 3. Post-commit: User code after transact returns (side effects)
85
- # - User MUST call transact {} or TransactNotCalledError is raised
86
- #
87
- # LMID semantics by phase:
72
+ # Phase tracking enables correct LMID semantics:
88
73
  # - Pre-transaction error: LMID advanced in separate transaction
89
74
  # - Transaction error: LMID advanced in separate transaction (original tx rolled back)
90
75
  # - Post-commit error: LMID already committed with transaction
@@ -97,46 +82,9 @@ module ZeroRuby
97
82
  handler_class = schema.handler_for(mutation_name)
98
83
  raise MutationNotFoundError.new(mutation_name) unless handler_class
99
84
 
100
- if handler_class.skip_auto_transaction?
101
- process_manual_transact(mutation_data, client_group_id, client_id, mutation_id, mutation_id_obj, context)
102
- else
103
- process_auto_transact(mutation_data, client_group_id, client_id, mutation_id, mutation_id_obj, context)
104
- end
105
- end
106
-
107
- # Process mutation with auto-transact (entire execute wrapped in transaction)
108
- def process_auto_transact(mutation_data, client_group_id, client_id, mutation_id, mutation_id_obj, context)
109
- result = lmid_store.transaction do
110
- last_mutation_id = lmid_store.fetch_and_increment(client_group_id, client_id)
111
- check_lmid!(client_id, mutation_id, last_mutation_id)
112
- schema.execute_mutation(mutation_data, context)
113
- end
114
- {id: mutation_id_obj, result: result}
115
- rescue MutationAlreadyProcessedError => e
116
- {id: mutation_id_obj, result: format_error_response(e)}
117
- rescue OutOfOrderMutationError, DatabaseTransactionError
118
- raise
119
- rescue ActiveRecord::StatementInvalid => e
120
- raise DatabaseTransactionError.new("Transaction failed: #{e.message}")
121
- rescue ZeroRuby::Error, FloatDomainError => e
122
- # No retry - matches TypeScript behavior (TS does NOT retry user code)
123
- # Transaction rolled back - advance LMID separately to prevent replay
124
- persist_lmid_on_application_error(client_group_id, client_id)
125
- error = e.is_a?(FloatDomainError) ?
126
- ValidationError.new(["Invalid numeric value: #{e.message}"]) : e
127
- {id: mutation_id_obj, result: format_error_response(error)}
128
- rescue => e
129
- raise DatabaseTransactionError.new("Transaction failed: #{e.message}")
130
- end
131
-
132
- # Process mutation with manual transact (3-phase model)
133
- # No retry - matches TypeScript behavior (TS does NOT retry user code)
134
- def process_manual_transact(mutation_data, client_group_id, client_id, mutation_id, mutation_id_obj, context)
135
85
  phase = :pre_transaction
136
- transact_called = false
137
86
 
138
87
  transact_proc = proc { |&user_block|
139
- transact_called = true
140
88
  phase = :transaction
141
89
  result = lmid_store.transaction do
142
90
  last_mutation_id = lmid_store.fetch_and_increment(client_group_id, client_id)
@@ -148,27 +96,24 @@ module ZeroRuby
148
96
  }
149
97
 
150
98
  result = schema.execute_mutation(mutation_data, context, &transact_proc)
151
- raise TransactNotCalledError.new unless transact_called
152
99
  {id: mutation_id_obj, result: result}
153
- rescue MutationAlreadyProcessedError => e
100
+ rescue MutationNotFoundError, MutationAlreadyProcessedError => e
101
+ # Known skip conditions - return error response, batch continues
154
102
  {id: mutation_id_obj, result: format_error_response(e)}
155
- rescue OutOfOrderMutationError, DatabaseTransactionError
103
+ rescue OutOfOrderMutationError, TransactionError
104
+ # Batch-terminating errors - bubble up to process() for PushFailed response
156
105
  raise
157
- rescue ActiveRecord::StatementInvalid => e
158
- raise DatabaseTransactionError.new("Transaction failed: #{e.message}")
159
- rescue ZeroRuby::Error, FloatDomainError => e
160
- # LMID semantics by phase (no retry - matches TS):
161
- # - Pre-transaction: LMID advanced separately
162
- # - Transaction (rolled back): LMID advanced separately
163
- # - Post-commit: LMID already committed with transaction
106
+ rescue ZeroRuby::Error => e
107
+ # Application errors - advance LMID based on phase, return error response
108
+ # Pre-transaction/transaction: LMID advanced separately
109
+ # Post-commit: LMID already committed with transaction
164
110
  if phase != :post_commit
165
111
  persist_lmid_on_application_error(client_group_id, client_id)
166
112
  end
167
- error = e.is_a?(FloatDomainError) ?
168
- ValidationError.new(["Invalid numeric value: #{e.message}"]) : e
169
- {id: mutation_id_obj, result: format_error_response(error)}
113
+ {id: mutation_id_obj, result: format_error_response(e)}
170
114
  rescue => e
171
- raise DatabaseTransactionError.new("Transaction failed: #{e.message}")
115
+ # Unexpected errors - wrap and bubble up as batch-terminating
116
+ raise TransactionError.new("Transaction failed: #{e.message}")
172
117
  end
173
118
 
174
119
  # Persist LMID advancement after an application error.
@@ -1,29 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "types"
4
-
5
3
  module ZeroRuby
6
- # Provides shorthand constants for ZeroRuby types.
7
- # Include this module to use ID, Boolean, etc. without the ZeroRuby::Types:: prefix.
4
+ # Provides access to ZeroRuby::Types via the Types constant.
5
+ # Automatically included in Mutation and InputObject classes.
8
6
  #
9
- # @example Including in your own classes
7
+ # @example Usage in mutations
10
8
  # class PostCreate < ZeroRuby::Mutation
11
- # include ZeroRuby::TypeNames
12
- #
13
- # argument :id, ID
9
+ # argument :id, Types::ID
14
10
  # argument :title, Types::String
15
- # argument :active, Boolean
11
+ # argument :active, Types::Boolean
16
12
  # end
17
- #
18
- # Note: Ruby's built-in String, Integer, and Float cannot be aliased since
19
- # they are actual classes. Use ZeroRuby::Types::String, etc. instead.
20
13
  module TypeNames
21
- ID = ZeroRuby::Types::ID
22
- Boolean = ZeroRuby::Types::Boolean
23
- ISO8601Date = ZeroRuby::Types::ISO8601Date
24
- ISO8601DateTime = ZeroRuby::Types::ISO8601DateTime
25
-
26
- # Provide Types module access without prefix
27
14
  Types = ZeroRuby::Types
28
15
  end
29
16
  end
@@ -6,16 +6,14 @@ module ZeroRuby
6
6
  # Type definitions using dry-types.
7
7
  #
8
8
  # When inheriting from ZeroRuby::Mutation or ZeroRuby::InputObject, types are
9
- # available via ZeroRuby::TypeNames which is automatically included:
10
- # - Direct: ID, Boolean, ISO8601Date, ISO8601DateTime
11
- # - Via Types: Types::String, Types::Integer, Types::Float
9
+ # available via the Types module (e.g., Types::String, Types::ID).
12
10
  #
13
11
  # @example Basic usage
14
12
  # class MyMutation < ZeroRuby::Mutation
15
- # argument :id, ID
13
+ # argument :id, Types::ID
16
14
  # argument :name, Types::String
17
15
  # argument :count, Types::Integer.optional
18
- # argument :active, Boolean.default(false)
16
+ # argument :active, Types::Boolean.default(false)
19
17
  # end
20
18
  #
21
19
  # @example With constraints
@@ -58,6 +58,18 @@ module ZeroRuby
58
58
  def collect_input_objects_from_arguments(arguments)
59
59
  arguments.each do |_name, config|
60
60
  type = config[:type]
61
+
62
+ # Unwrap optional types first (Sum type: NilClass | actual_type)
63
+ if sum_type?(type)
64
+ inner = extract_non_nil_type(type)
65
+ type = inner if inner
66
+ end
67
+
68
+ # Then unwrap array types to check element type for InputObjects
69
+ if array_type?(type)
70
+ type = type.member
71
+ end
72
+
61
73
  if input_object_type?(type)
62
74
  type_name = extract_type_name(type)
63
75
  unless @input_objects.key?(type_name)
@@ -65,7 +77,7 @@ module ZeroRuby
65
77
  # Recursively collect nested InputObjects from Dry::Struct schema
66
78
  if type.respond_to?(:schema)
67
79
  nested_args = type.schema.keys.each_with_object({}) do |key, hash|
68
- hash[key.name] = { type: key.type, name: key.name }
80
+ hash[key.name] = {type: key.type, name: key.name}
69
81
  end
70
82
  collect_input_objects_from_arguments(nested_args)
71
83
  end
@@ -169,6 +181,19 @@ module ZeroRuby
169
181
  return extract_type_name(type)
170
182
  end
171
183
 
184
+ # Handle Array types
185
+ if array_type?(type)
186
+ element_type = type.member
187
+ return "#{resolve_type(element_type)}[]"
188
+ end
189
+
190
+ # Handle optional types (Sum of NilClass and another type)
191
+ # This must come before unwrap_primitive to handle optional arrays
192
+ if sum_type?(type)
193
+ inner = extract_non_nil_type(type)
194
+ return resolve_type(inner) if inner
195
+ end
196
+
172
197
  # Unwrap the dry-type to get the primitive
173
198
  primitive = unwrap_primitive(type)
174
199
 
@@ -181,7 +206,7 @@ module ZeroRuby
181
206
  PRIMITIVE_MAP[primitive] || raise(
182
207
  ArgumentError,
183
208
  "Cannot map type #{type.inspect} (primitive: #{primitive.inspect}) to TypeScript. " \
184
- "Supported primitives: #{PRIMITIVE_MAP.keys.map(&:name).join(', ')}"
209
+ "Supported primitives: #{PRIMITIVE_MAP.keys.map(&:name).join(", ")}"
185
210
  )
186
211
  end
187
212
 
@@ -231,6 +256,23 @@ module ZeroRuby
231
256
  type.is_a?(Class) && type < ZeroRuby::InputObject
232
257
  end
233
258
 
259
+ def array_type?(type)
260
+ type.respond_to?(:primitive) && type.primitive == Array
261
+ end
262
+
263
+ def sum_type?(type)
264
+ type.respond_to?(:left) && type.respond_to?(:right)
265
+ end
266
+
267
+ def extract_non_nil_type(type)
268
+ return nil unless sum_type?(type)
269
+ if type.left.respond_to?(:primitive) && type.left.primitive == NilClass
270
+ type.right
271
+ elsif type.right.respond_to?(:primitive) && type.right.primitive == NilClass
272
+ type.left
273
+ end
274
+ end
275
+
234
276
  def extract_type_name(type)
235
277
  # Get just the class name without module prefixes
236
278
  type.name.split("::").last
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ZeroRuby
4
- VERSION = "0.1.0.alpha4"
4
+ VERSION = "0.1.0.alpha5"
5
5
  end
data/lib/zero_ruby.rb CHANGED
@@ -7,7 +7,7 @@ require_relative "zero_ruby/errors"
7
7
  # Types module (dry-types based)
8
8
  require_relative "zero_ruby/types"
9
9
 
10
- # Type name shortcuts (ID, Boolean, etc. without ZeroRuby::Types:: prefix)
10
+ # Provides Types constant for accessing ZeroRuby::Types
11
11
  require_relative "zero_ruby/type_names"
12
12
 
13
13
  # Core classes
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: zero_ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0.alpha4
4
+ version: 0.1.0.alpha5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alex Serban
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-12-22 00:00:00.000000000 Z
10
+ date: 2026-01-01 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: dry-struct