shikibu 0.1.0 → 0.3.0

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: 6d241ad341f0963dd1fe64cb61dc2a25a95fd8d0f01d12aef81c0372075dbcba
4
- data.tar.gz: d287f1ee1f8f58a6a4b6f44565e54ad848792ec3005931fe77a55a187c7c7a6c
3
+ metadata.gz: 3bf06e3a63cc523afe19105e1ed5debd74a949dc3faec06a35ee9d3853d9d4ae
4
+ data.tar.gz: 9877fbfe7dec08f504d067999bd6e83ffd483c050192e7057f29b6d50e2317c1
5
5
  SHA512:
6
- metadata.gz: 39e4b6bbabb954eb6fd11ec715e91afff3b61ba2737c836332ed5161b6f5f9cbef2d30b72f3b08b96d76b96e05018e86127c88fffb8952a0aece2724e643a979
7
- data.tar.gz: bc4cacdb7f83dc73a80468d0daa92af3bc23f69315b07836e12d195b48d46c4bbe5ed18374609cefe7fc8ba03cd3fde8fff9316b147646b85ff57bad99d637df
6
+ metadata.gz: a9adb5bfbda37f44168434adccde9e22cc65a1c101f1cf2bf2350a003ebd848f2778493b40ade87a4c7e17efa293f949c2c2ac18ce9645bd53fa9c3a42e12a56
7
+ data.tar.gz: 16102137dc003ab1d8b787edd5d78770e84228529f55318d3d0a0b3a7a7cf1e7050acc3826431f48cc96483b30e300df34877b8cbb6891a869b367f4f2a2d88f
data/README.md CHANGED
@@ -198,6 +198,45 @@ end
198
198
 
199
199
  **Activity IDs**: Activities are automatically identified with IDs like `"create_user:1"` for deterministic replay.
200
200
 
201
+ ### Typed Workflows (dry-struct)
202
+
203
+ Shikibu supports typed input/output with [dry-struct](https://dry-rb.org/gems/dry-struct/) for validation and type coercion:
204
+
205
+ ```ruby
206
+ require 'dry-struct'
207
+
208
+ module Types
209
+ include Dry.Types()
210
+ end
211
+
212
+ class OrderInput < Dry::Struct
213
+ attribute :order_id, Types::String
214
+ attribute :amount, Types::Coercible::Decimal
215
+ end
216
+
217
+ class OrderResult < Dry::Struct
218
+ attribute :order_id, Types::String
219
+ attribute :status, Types::String
220
+ end
221
+
222
+ class TypedOrderSaga < Shikibu::Workflow
223
+ workflow_name 'typed_order'
224
+ input OrderInput
225
+ output OrderResult
226
+
227
+ def execute(order)
228
+ # order is an OrderInput instance with validated/coerced types
229
+ OrderResult.new(order_id: order.order_id, status: 'completed')
230
+ end
231
+ end
232
+
233
+ # Run with automatic type coercion
234
+ instance_id = app.start_workflow(TypedOrderSaga, order_id: 'ORD-123', amount: '99.99')
235
+ result = app.get_typed_result(instance_id, TypedOrderSaga) # Returns OrderResult
236
+ ```
237
+
238
+ **Note**: `dry-struct` is an optional dependency. Add it to your Gemfile if you want typed workflows.
239
+
201
240
  ### Durable Execution
202
241
 
203
242
  Shikibu ensures workflow progress is never lost through **deterministic replay**:
data/lib/shikibu/app.rb CHANGED
@@ -162,6 +162,18 @@ module Shikibu
162
162
  }
163
163
  end
164
164
 
165
+ # Get workflow result with typed output deserialization
166
+ # @param instance_id [String] Instance ID
167
+ # @param workflow_class [Class] Workflow class with output type definition
168
+ # @return [Object, nil] Typed output or nil if no output
169
+ # @raise [WorkflowNotFoundError] If instance not found
170
+ def get_typed_result(instance_id, workflow_class)
171
+ result = get_result(instance_id)
172
+ return nil unless result[:output]
173
+
174
+ workflow_class.deserialize_output(result[:output])
175
+ end
176
+
165
177
  # Get workflow status
166
178
  # @param instance_id [String] Instance ID
167
179
  # @return [String] Status
@@ -14,9 +14,12 @@ module Shikibu
14
14
  # Start a new workflow instance
15
15
  # @param workflow_class [Class] Workflow class
16
16
  # @param instance_id [String] Instance ID
17
- # @param input [Hash] Input parameters
17
+ # @param input [Hash, Object] Input parameters (Hash for untyped, typed object for typed workflows)
18
18
  # @return [Object, nil] Workflow result or nil if suspended
19
19
  def start_workflow(workflow_class, instance_id:, **input)
20
+ # Serialize input for storage (handles typed inputs)
21
+ serialized_input = workflow_class.serialize_input(input)
22
+
20
23
  # Save workflow definition
21
24
  storage.save_workflow_definition(
22
25
  workflow_name: workflow_class.workflow_name,
@@ -30,12 +33,12 @@ module Shikibu
30
33
  workflow_name: workflow_class.workflow_name,
31
34
  source_hash: workflow_class.source_hash,
32
35
  owner_service: 'default',
33
- input_data: input,
36
+ input_data: serialized_input,
34
37
  status: Status::RUNNING
35
38
  )
36
39
 
37
40
  # Execute the workflow
38
- execute_workflow(instance_id, workflow_class, input, replaying: false)
41
+ execute_workflow(instance_id, workflow_class, serialized_input, replaying: false)
39
42
  end
40
43
 
41
44
  # Resume a workflow from its current state
@@ -197,12 +200,22 @@ module Shikibu
197
200
  workflow.instance_variable_set(:@pending_compensations, [])
198
201
  workflow.context = ctx
199
202
 
200
- # Symbolize input keys
201
- symbolized_input = symbolize_keys(input)
202
- result = workflow.execute(**symbolized_input)
203
+ # Handle typed vs untyped input
204
+ result = if workflow_class.typed_input?
205
+ # Typed workflow: deserialize and pass single object
206
+ typed_input = workflow_class.deserialize_input(input)
207
+ workflow.execute(typed_input)
208
+ else
209
+ # Legacy: symbolize and spread as keyword args
210
+ symbolized_input = symbolize_keys(input)
211
+ workflow.execute(**symbolized_input)
212
+ end
213
+
214
+ # Serialize output
215
+ serialized_output = workflow_class.serialize_output(result)
203
216
 
204
217
  # Mark completed
205
- storage.update_instance_status(instance_id, Status::COMPLETED, output_data: result)
218
+ storage.update_instance_status(instance_id, Status::COMPLETED, output_data: serialized_output)
206
219
  storage.clear_compensations(instance_id)
207
220
 
208
221
  # Cleanup direct subscriptions
@@ -33,8 +33,43 @@ module Shikibu
33
33
  # Transaction Management
34
34
  # ============================================
35
35
 
36
+ # Thread-local storage for transaction state
37
+ # Tracks nesting depth and post-commit callbacks
38
+ def transaction_state
39
+ Thread.current[:shikibu_tx_state] ||= { depth: 0, callbacks: [] }
40
+ end
41
+
42
+ # Register a callback to be executed after successful commit of the outermost transaction
43
+ # @param block [Proc] The callback to execute after commit
44
+ # @raise [RuntimeError] If called outside of a transaction
45
+ def register_post_commit_callback(&block)
46
+ raise 'Not in transaction' unless in_transaction?
47
+
48
+ transaction_state[:callbacks] << block
49
+ end
50
+
36
51
  def transaction(&)
37
- @db.transaction(&)
52
+ state = transaction_state
53
+ state[:depth] += 1
54
+ committed = false
55
+
56
+ begin
57
+ result = @db.transaction(&)
58
+ committed = true
59
+
60
+ # Execute callbacks only after successful commit of outermost transaction
61
+ if state[:depth] == 1
62
+ callbacks = state[:callbacks].dup
63
+ state[:callbacks].clear
64
+ callbacks.each(&:call)
65
+ end
66
+
67
+ result
68
+ ensure
69
+ state[:depth] -= 1
70
+ # Clear callbacks on rollback (when not committed) at outermost level
71
+ state[:callbacks].clear if state[:depth].zero? && !committed
72
+ end
38
73
  end
39
74
 
40
75
  def in_transaction?
@@ -405,8 +440,15 @@ module Shikibu
405
440
  )
406
441
  end
407
442
 
408
- # Send NOTIFY for new message
409
- send_notify(Notify::Channel::CHANNEL_MESSAGE, { ch: channel, msg_id: message_id })
443
+ # Send NOTIFY for new message (deferred if in transaction)
444
+ notify_payload = { ch: channel, msg_id: message_id }
445
+ if in_transaction?
446
+ register_post_commit_callback do
447
+ send_notify(Notify::Channel::CHANNEL_MESSAGE, notify_payload)
448
+ end
449
+ else
450
+ send_notify(Notify::Channel::CHANNEL_MESSAGE, notify_payload)
451
+ end
410
452
 
411
453
  message_id
412
454
  end
@@ -588,8 +630,15 @@ module Shikibu
588
630
  )
589
631
  end
590
632
 
591
- # Send NOTIFY for new outbox event
592
- send_notify(Notify::Channel::OUTBOX_PENDING, { evt_id: event_id, evt_type: event_type })
633
+ # Send NOTIFY for new outbox event (deferred if in transaction)
634
+ notify_payload = { evt_id: event_id, evt_type: event_type }
635
+ if in_transaction?
636
+ register_post_commit_callback do
637
+ send_notify(Notify::Channel::OUTBOX_PENDING, notify_payload)
638
+ end
639
+ else
640
+ send_notify(Notify::Channel::OUTBOX_PENDING, notify_payload)
641
+ end
593
642
  end
594
643
 
595
644
  def get_pending_outbox_events(limit: 100)
@@ -0,0 +1,189 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shikibu
4
+ # Module to detect and handle typed payload classes
5
+ # Supports dry-struct, Ruby 3.2+ Data classes, and duck-typed objects
6
+ #
7
+ # @example with dry-struct
8
+ # require 'dry-struct'
9
+ # class OrderInput < Dry::Struct
10
+ # attribute :order_id, Types::String
11
+ # attribute :amount, Types::Coercible::Decimal
12
+ # end
13
+ #
14
+ # TypedPayload.typed_class?(OrderInput) # => true
15
+ # TypedPayload.to_h(OrderInput.new(order_id: '123', amount: 99.99))
16
+ # # => { order_id: '123', amount: 99.99 }
17
+ #
18
+ # @example with Data class
19
+ # OrderInput = Data.define(:order_id, :amount)
20
+ # TypedPayload.typed_class?(OrderInput) # => true
21
+ #
22
+ module TypedPayload
23
+ class << self
24
+ # Check if dry-struct is available
25
+ # @return [Boolean]
26
+ def dry_struct_available?
27
+ return @dry_struct_available if defined?(@dry_struct_available)
28
+
29
+ @dry_struct_available = begin
30
+ require 'dry-struct'
31
+ true
32
+ rescue LoadError
33
+ false
34
+ end
35
+ end
36
+
37
+ # Check if an object is a typed payload class (not instance)
38
+ # @param obj [Object] Object to check
39
+ # @return [Boolean]
40
+ def typed_class?(obj)
41
+ return false unless obj.is_a?(Class)
42
+
43
+ # Check for dry-struct
44
+ return true if dry_struct_class?(obj)
45
+
46
+ # Check for Ruby 3.2+ Data class
47
+ return true if data_class?(obj)
48
+
49
+ # Check for duck-typed class with required methods
50
+ duck_typed_class?(obj)
51
+ end
52
+
53
+ # Check if an object is a typed payload instance
54
+ # @param obj [Object] Object to check
55
+ # @return [Boolean]
56
+ def typed_instance?(obj)
57
+ return true if dry_struct_instance?(obj)
58
+ return true if data_instance?(obj)
59
+
60
+ duck_typed_instance?(obj)
61
+ end
62
+
63
+ # Serialize a typed instance to a hash
64
+ # @param obj [Object] Typed instance or any object
65
+ # @return [Hash, Object] Serialized hash or original object
66
+ def to_h(obj)
67
+ deep_serialize(obj)
68
+ end
69
+
70
+ # Deserialize a hash to a typed instance
71
+ # @param hash [Hash] Hash to deserialize
72
+ # @param type_class [Class] Target type class
73
+ # @return [Object] Typed instance or original hash
74
+ def from_h(hash, type_class)
75
+ return hash unless typed_class?(type_class)
76
+ return hash unless hash.is_a?(Hash)
77
+
78
+ # Symbolize keys for compatibility
79
+ symbolized = deep_symbolize_keys(hash)
80
+
81
+ # All typed classes support .new(**hash) interface
82
+ type_class.new(**symbolized)
83
+ end
84
+
85
+ # Deserialize an array of typed objects
86
+ # @param array [Array<Hash>] Array of hashes
87
+ # @param element_type [Class] Element type class
88
+ # @return [Array] Array of typed instances
89
+ def from_array(array, element_type)
90
+ return array unless array.is_a?(Array)
91
+ return array unless typed_class?(element_type)
92
+
93
+ array.map do |item|
94
+ item.is_a?(Hash) ? from_h(item, element_type) : item
95
+ end
96
+ end
97
+
98
+ private
99
+
100
+ def dry_struct_class?(obj)
101
+ return false unless dry_struct_available?
102
+
103
+ obj.is_a?(Class) && obj < Dry::Struct
104
+ end
105
+
106
+ def dry_struct_instance?(obj)
107
+ return false unless dry_struct_available?
108
+
109
+ obj.is_a?(Dry::Struct)
110
+ end
111
+
112
+ def data_class?(obj)
113
+ return false unless defined?(Data)
114
+
115
+ obj.is_a?(Class) && obj < Data
116
+ end
117
+
118
+ def data_instance?(obj)
119
+ return false unless defined?(Data)
120
+
121
+ obj.is_a?(Data)
122
+ end
123
+
124
+ def duck_typed_class?(obj)
125
+ # A class is considered typed if it has new and instances have to_h
126
+ # but exclude Hash itself
127
+ return false if obj == Hash
128
+
129
+ obj.respond_to?(:new) &&
130
+ obj.method_defined?(:to_h)
131
+ end
132
+
133
+ def duck_typed_instance?(obj)
134
+ # An instance is typed if it has to_h but is not a Hash/Array
135
+ return false if obj.is_a?(Hash)
136
+ return false if obj.is_a?(Array)
137
+ return false unless obj.respond_to?(:to_h)
138
+
139
+ # Additional check: should have proper to_h implementation
140
+ # (not just Object#to_h which doesn't exist or returns weird things)
141
+ begin
142
+ result = obj.to_h
143
+ result.is_a?(Hash)
144
+ rescue StandardError
145
+ false
146
+ end
147
+ end
148
+
149
+ # Recursively serialize typed objects to hashes
150
+ # Note: Symbols are preserved here. They are converted to strings
151
+ # only during JSON serialization in the storage layer.
152
+ def deep_serialize(obj)
153
+ case obj
154
+ when Hash
155
+ obj.transform_values { |v| deep_serialize(v) }
156
+ when Array
157
+ obj.map { |v| deep_serialize(v) }
158
+ when ->(o) { dry_struct_instance?(o) || data_instance?(o) }
159
+ deep_serialize(obj.to_h)
160
+ when Time, DateTime
161
+ obj.iso8601
162
+ when Date
163
+ obj.to_s
164
+ when BigDecimal
165
+ obj.to_s('F')
166
+ else
167
+ # Symbols, Integers, Strings, etc. are preserved as-is
168
+ obj
169
+ end
170
+ end
171
+
172
+ # Recursively symbolize hash keys
173
+ def deep_symbolize_keys(hash)
174
+ return hash unless hash.is_a?(Hash)
175
+
176
+ symbolized = hash.transform_keys do |key|
177
+ key.is_a?(String) ? key.to_sym : key
178
+ end
179
+ symbolized.transform_values do |value|
180
+ case value
181
+ when Hash then deep_symbolize_keys(value)
182
+ when Array then value.map { |v| v.is_a?(Hash) ? deep_symbolize_keys(v) : v }
183
+ else value
184
+ end
185
+ end
186
+ end
187
+ end
188
+ end
189
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Shikibu
4
- VERSION = '0.1.0'
4
+ VERSION = '0.3.0'
5
5
  end
@@ -59,6 +59,85 @@ module Shikibu
59
59
  end
60
60
  end
61
61
 
62
+ # Set the input type for this workflow
63
+ # @param type_class [Class, nil] A Dry::Struct, Data, or duck-typed class
64
+ # @example
65
+ # class OrderSaga < Shikibu::Workflow
66
+ # input OrderInput
67
+ # output OrderResult
68
+ # end
69
+ def input(type_class = nil)
70
+ if type_class
71
+ unless TypedPayload.typed_class?(type_class)
72
+ raise ArgumentError, "#{type_class} must be a Dry::Struct, Data, or respond to .new and #to_h"
73
+ end
74
+
75
+ @input_type = type_class
76
+ else
77
+ @input_type
78
+ end
79
+ end
80
+
81
+ # Set the output type for this workflow (optional)
82
+ # @param type_class [Class, nil] A Dry::Struct, Data, or duck-typed class
83
+ def output(type_class = nil)
84
+ if type_class
85
+ unless TypedPayload.typed_class?(type_class)
86
+ raise ArgumentError, "#{type_class} must be a Dry::Struct, Data, or respond to .new and #to_h"
87
+ end
88
+
89
+ @output_type = type_class
90
+ else
91
+ @output_type
92
+ end
93
+ end
94
+
95
+ # Check if this workflow has typed input
96
+ # @return [Boolean]
97
+ def typed_input?
98
+ !@input_type.nil?
99
+ end
100
+
101
+ # Check if this workflow has typed output
102
+ # @return [Boolean]
103
+ def typed_output?
104
+ !@output_type.nil?
105
+ end
106
+
107
+ # Serialize input for storage
108
+ # @param input_value [Object] Input to serialize
109
+ # @return [Hash] Serialized hash
110
+ def serialize_input(input_value)
111
+ TypedPayload.to_h(input_value)
112
+ end
113
+
114
+ # Deserialize input from storage
115
+ # @param data [Hash] Stored data
116
+ # @return [Object] Typed input or hash
117
+ def deserialize_input(data)
118
+ return data unless typed_input?
119
+ return data unless data.is_a?(Hash)
120
+
121
+ TypedPayload.from_h(data, @input_type)
122
+ end
123
+
124
+ # Serialize output for storage
125
+ # @param output_value [Object] Output to serialize
126
+ # @return [Hash, Object] Serialized output
127
+ def serialize_output(output_value)
128
+ TypedPayload.to_h(output_value)
129
+ end
130
+
131
+ # Deserialize output from storage
132
+ # @param data [Object] Stored data
133
+ # @return [Object] Typed output or original data
134
+ def deserialize_output(data)
135
+ return data unless typed_output?
136
+ return data unless data.is_a?(Hash)
137
+
138
+ TypedPayload.from_h(data, @output_type)
139
+ end
140
+
62
141
  # Get the source hash for this workflow
63
142
  def source_hash
64
143
  @source_hash ||= begin
@@ -130,9 +209,15 @@ module Shikibu
130
209
  # Execute an activity with automatic retry and history tracking
131
210
  # @param name [Symbol, String] Activity name
132
211
  # @param retry_policy [RetryPolicy] Retry policy
212
+ # @param returns [Class, nil] Return type class for type restoration during replay
133
213
  # @param block [Proc] Activity logic
134
214
  # @return [Object] Activity result
135
- def activity(name, retry_policy: nil, &)
215
+ # @raise [ArgumentError] If returns is not a valid typed class
216
+ def activity(name, retry_policy: nil, returns: nil, &)
217
+ if returns && !TypedPayload.typed_class?(returns)
218
+ raise ArgumentError, "returns: must be a typed class (Dry::Struct, Data, or duck-typed), got #{returns.inspect}"
219
+ end
220
+
136
221
  activity_id = ctx.generate_activity_id(name.to_s)
137
222
  ctx.current_activity_id = activity_id
138
223
 
@@ -141,7 +226,17 @@ module Shikibu
141
226
  cached = ctx.get_cached_result(activity_id)
142
227
  handle_cached_result(activity_id, cached)
143
228
  ctx.record_last_activity_id(activity_id)
144
- return cached[:result] if cached[:event_type] == EventType::ACTIVITY_COMPLETED
229
+
230
+ if cached[:event_type] == EventType::ACTIVITY_COMPLETED
231
+ result = cached[:result]
232
+
233
+ # Restore type if specified
234
+ if returns && TypedPayload.typed_class?(returns) && result.is_a?(Hash)
235
+ result = TypedPayload.from_h(result, returns)
236
+ end
237
+
238
+ return result
239
+ end
145
240
 
146
241
  # Re-raise cached error
147
242
  raise reconstruct_error(cached)
@@ -279,21 +374,24 @@ module Shikibu
279
374
  end
280
375
 
281
376
  def execute_single_compensation(comp, compensation_id)
282
- comp[:block].call
283
-
284
- # Record successful compensation (Romancy/Edda compatible format)
285
- ctx.storage.append_history(
286
- instance_id: ctx.instance_id,
287
- activity_id: "compensation:#{compensation_id || comp[:activity_id]}",
288
- event_type: EventType::COMPENSATION_EXECUTED,
289
- event_data: {
290
- compensation_id: compensation_id,
291
- activity_id: comp[:activity_id],
292
- activity_name: comp[:compensation_name]
293
- }
294
- )
377
+ # Execute compensation and record result atomically in a transaction
378
+ ctx.storage.transaction do
379
+ comp[:block].call
380
+
381
+ # Record successful compensation (Romancy/Edda compatible format)
382
+ ctx.storage.append_history(
383
+ instance_id: ctx.instance_id,
384
+ activity_id: "compensation:#{compensation_id || comp[:activity_id]}",
385
+ event_type: EventType::COMPENSATION_EXECUTED,
386
+ event_data: {
387
+ compensation_id: compensation_id,
388
+ activity_id: comp[:activity_id],
389
+ activity_name: comp[:compensation_name]
390
+ }
391
+ )
392
+ end
295
393
  rescue StandardError => e
296
- # Record failed compensation but continue with others
394
+ # Record failed compensation but continue with others (outside transaction)
297
395
  ctx.storage.append_history(
298
396
  instance_id: ctx.instance_id,
299
397
  activity_id: "compensation:#{compensation_id || comp[:activity_id]}",
@@ -320,23 +418,28 @@ module Shikibu
320
418
  # Call hooks
321
419
  ctx.hooks&.on_activity_start&.call(ctx.instance_id, activity_id, name, attempt)
322
420
 
323
- result = block.call
421
+ # Execute activity and record result atomically in a transaction
422
+ result = ctx.storage.transaction do
423
+ res = block.call
324
424
 
325
- # Record successful result
326
- ctx.storage.append_history(
327
- instance_id: ctx.instance_id,
328
- activity_id: activity_id,
329
- event_type: EventType::ACTIVITY_COMPLETED,
330
- event_data: { result: result }
331
- )
425
+ # Record successful result (atomic with activity execution)
426
+ ctx.storage.append_history(
427
+ instance_id: ctx.instance_id,
428
+ activity_id: activity_id,
429
+ event_type: EventType::ACTIVITY_COMPLETED,
430
+ event_data: { result: res }
431
+ )
332
432
 
333
- # Cache for replay
433
+ res
434
+ end
435
+
436
+ # Cache for replay (outside transaction)
334
437
  ctx.cache_result(activity_id, {
335
438
  event_type: EventType::ACTIVITY_COMPLETED,
336
439
  result: result
337
440
  })
338
441
 
339
- # Call hooks
442
+ # Call hooks (outside transaction)
340
443
  ctx.hooks&.on_activity_complete&.call(ctx.instance_id, activity_id, name, result, false)
341
444
 
342
445
  return result
data/lib/shikibu.rb CHANGED
@@ -132,6 +132,7 @@ require_relative 'shikibu/notify/wake_event'
132
132
  require_relative 'shikibu/locking'
133
133
  require_relative 'shikibu/channels'
134
134
  require_relative 'shikibu/context'
135
+ require_relative 'shikibu/typed_payload'
135
136
  require_relative 'shikibu/workflow'
136
137
  require_relative 'shikibu/activity'
137
138
  require_relative 'shikibu/storage/migrations'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: shikibu
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yasushi Itoh
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-12-21 00:00:00.000000000 Z
11
+ date: 2025-12-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: concurrent-ruby
@@ -80,6 +80,20 @@ dependencies:
80
80
  - - "~>"
81
81
  - !ruby/object:Gem::Version
82
82
  version: '2.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: dry-struct
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '1.6'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '1.6'
83
97
  - !ruby/object:Gem::Dependency
84
98
  name: mysql2
85
99
  requirement: !ruby/object:Gem::Requirement
@@ -192,6 +206,7 @@ files:
192
206
  - lib/shikibu/retry_policy.rb
193
207
  - lib/shikibu/storage/migrations.rb
194
208
  - lib/shikibu/storage/sequel_storage.rb
209
+ - lib/shikibu/typed_payload.rb
195
210
  - lib/shikibu/version.rb
196
211
  - lib/shikibu/worker.rb
197
212
  - lib/shikibu/workflow.rb