dexkit 0.1.0 → 0.2.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.
@@ -2,20 +2,11 @@
2
2
 
3
3
  module Dex
4
4
  module LockWrapper
5
- def self.included(base)
6
- base.extend(ClassMethods)
7
- end
8
-
9
- LOCK_KNOWN_OPTIONS = %i[timeout].freeze
5
+ extend Dex::Concern
10
6
 
11
7
  module ClassMethods
12
8
  def advisory_lock(key = nil, **options, &block)
13
- unknown = options.keys - LockWrapper::LOCK_KNOWN_OPTIONS
14
- if unknown.any?
15
- raise ArgumentError,
16
- "unknown advisory_lock option(s): #{unknown.map(&:inspect).join(", ")}. " \
17
- "Known: #{LockWrapper::LOCK_KNOWN_OPTIONS.map(&:inspect).join(", ")}"
18
- end
9
+ validate_options!(options, %i[timeout], :advisory_lock)
19
10
 
20
11
  lock_key = block || key
21
12
 
@@ -18,13 +18,13 @@ module Dex
18
18
  end
19
19
 
20
20
  def add(name, method: :"_#{name}_wrap", before: nil, after: nil, at: nil)
21
- _validate_positioning!(before, after, at)
21
+ validate_positioning!(before, after, at)
22
22
  step = Step.new(name: name, method: method)
23
23
 
24
24
  if at == :outer then @steps.unshift(step)
25
25
  elsif at == :inner then @steps.push(step)
26
- elsif before then @steps.insert(_find_index!(before), step)
27
- elsif after then @steps.insert(_find_index!(after) + 1, step)
26
+ elsif before then @steps.insert(find_index!(before), step)
27
+ elsif after then @steps.insert(find_index!(after) + 1, step)
28
28
  else @steps.push(step)
29
29
  end
30
30
  self
@@ -44,13 +44,13 @@ module Dex
44
44
 
45
45
  private
46
46
 
47
- def _validate_positioning!(before, after, at)
47
+ def validate_positioning!(before, after, at)
48
48
  count = [before, after, at].count { |v| !v.nil? }
49
49
  raise ArgumentError, "specify only one of before:, after:, at:" if count > 1
50
50
  raise ArgumentError, "at: must be :outer or :inner" if at && !%i[outer inner].include?(at)
51
51
  end
52
52
 
53
- def _find_index!(name)
53
+ def find_index!(name)
54
54
  idx = @steps.index { |s| s.name == name }
55
55
  raise ArgumentError, "pipeline step :#{name} not found" unless idx
56
56
  idx
@@ -2,41 +2,26 @@
2
2
 
3
3
  module Dex
4
4
  module RecordWrapper
5
- def self.included(base)
6
- base.extend(ClassMethods)
7
- end
5
+ extend Dex::Concern
8
6
 
9
7
  def _record_wrap
10
- halted = nil
11
- result = catch(:_dex_halt) { yield }
12
-
13
- if result.is_a?(Operation::Halt)
14
- halted = result
15
- result = halted.success? ? halted.value : nil
16
- end
8
+ interceptor = Operation::HaltInterceptor.new { yield }
17
9
 
18
- if halted.nil? || halted.success?
10
+ if interceptor.success?
19
11
  if _record_has_pending_record?
20
- _record_update_done!(result)
12
+ _record_update_done!(interceptor.result)
21
13
  elsif _record_enabled?
22
- _record_save!(result)
14
+ _record_save!(interceptor.result)
23
15
  end
24
16
  end
25
17
 
26
- throw(:_dex_halt, halted) if halted
27
- result
18
+ interceptor.rethrow!
19
+ interceptor.result
28
20
  end
29
21
 
30
- RECORD_KNOWN_OPTIONS = %i[params response].freeze
31
-
32
22
  module ClassMethods
33
23
  def record(enabled = nil, **options)
34
- unknown = options.keys - RecordWrapper::RECORD_KNOWN_OPTIONS
35
- if unknown.any?
36
- raise ArgumentError,
37
- "unknown record option(s): #{unknown.map(&:inspect).join(", ")}. " \
38
- "Known: #{RecordWrapper::RECORD_KNOWN_OPTIONS.map(&:inspect).join(", ")}"
39
- end
24
+ validate_options!(options, %i[params response], :record)
40
25
 
41
26
  if enabled == false
42
27
  set :record, enabled: false
@@ -101,7 +86,7 @@ module Dex
101
86
  success_type = self.class.respond_to?(:_success_type) && self.class._success_type
102
87
 
103
88
  if success_type
104
- _record_serialize_typed_result(result, success_type)
89
+ result.nil? ? nil : self.class.send(:_serialize_value, success_type, result)
105
90
  else
106
91
  case result
107
92
  when nil then nil
@@ -111,25 +96,12 @@ module Dex
111
96
  end
112
97
  end
113
98
 
114
- def _record_serialize_typed_result(result, type)
115
- return nil if result.nil?
116
-
117
- ref_type = self.class.send(:_dex_find_ref_type, type)
118
- if ref_type && result.respond_to?(:id)
119
- result.id
120
- else
121
- result.respond_to?(:as_json) ? result.as_json : result
122
- end
123
- end
124
-
125
99
  def _record_current_time
126
100
  Time.respond_to?(:current) ? Time.current : Time.now
127
101
  end
128
102
 
129
103
  def _record_handle_error(error)
130
- if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
131
- Rails.logger.warn "[Dex] Failed to record operation: #{error.message}"
132
- end
104
+ Dex.warn("Failed to record operation: #{error.message}")
133
105
  end
134
106
  end
135
107
  end
@@ -2,9 +2,7 @@
2
2
 
3
3
  module Dex
4
4
  module RescueWrapper
5
- def self.included(base)
6
- base.extend(ClassMethods)
7
- end
5
+ extend Dex::Concern
8
6
 
9
7
  module ClassMethods
10
8
  def rescue_from(*exception_classes, as:, message: nil)
@@ -32,23 +32,16 @@ module Dex
32
32
  end
33
33
  end
34
34
 
35
- def self.included(base)
36
- base.extend(ClassMethods)
37
- end
35
+ extend Dex::Concern
38
36
 
39
37
  def _result_wrap
40
- halted = catch(:_dex_halt) { yield }
41
- if halted.is_a?(Operation::Halt)
42
- if halted.success?
43
- _result_validate_success_type!(halted.value)
44
- halted.value
45
- else
46
- raise Dex::Error.new(halted.error_code, halted.error_message, details: halted.error_details)
47
- end
48
- else
49
- _result_validate_success_type!(halted)
50
- halted
38
+ interceptor = Operation::HaltInterceptor.new { yield }
39
+ if interceptor.error?
40
+ h = interceptor.halt
41
+ raise Dex::Error.new(h.error_code, h.error_message, details: h.error_details)
51
42
  end
43
+ _result_validate_success_type!(interceptor.result)
44
+ interceptor.result
52
45
  end
53
46
 
54
47
  def error!(code, message = nil, details: nil)
@@ -8,6 +8,15 @@ module Dex
8
8
  @_settings[key] = (@_settings[key] || {}).merge(options)
9
9
  end
10
10
 
11
+ def validate_options!(options, known, dsl_name)
12
+ unknown = options.keys - known
13
+ return if unknown.empty?
14
+
15
+ raise ArgumentError,
16
+ "unknown #{dsl_name} option(s): #{unknown.map(&:inspect).join(", ")}. " \
17
+ "Known: #{known.map(&:inspect).join(", ")}"
18
+ end
19
+
11
20
  def settings_for(key)
12
21
  parent_settings = if superclass.respond_to?(:settings_for)
13
22
  superclass.settings_for(key) || {}
@@ -19,8 +28,6 @@ module Dex
19
28
  end
20
29
  end
21
30
 
22
- def self.included(base)
23
- base.extend(ClassMethods)
24
- end
31
+ extend Dex::Concern
25
32
  end
26
33
  end
@@ -2,40 +2,27 @@
2
2
 
3
3
  module Dex
4
4
  module TransactionWrapper
5
- def self.included(base)
6
- base.extend(ClassMethods)
7
- end
5
+ extend Dex::Concern
8
6
 
9
7
  def _transaction_wrap
10
8
  return yield unless _transaction_enabled?
11
9
 
12
- halted = nil
10
+ interceptor = nil
13
11
  result = _transaction_execute do
14
- halted_value = catch(:_dex_halt) { yield }
15
- if halted_value.is_a?(Operation::Halt)
16
- halted = halted_value
17
- raise _transaction_adapter.rollback_exception_class if halted.error?
18
- halted.value
19
- else
20
- halted_value
21
- end
12
+ interceptor = Operation::HaltInterceptor.new { yield }
13
+ raise _transaction_adapter.rollback_exception_class if interceptor.error?
14
+ interceptor.result
22
15
  end
23
16
 
24
- throw(:_dex_halt, halted) if halted
17
+ interceptor&.rethrow!
25
18
  result
26
19
  end
27
20
 
28
21
  TRANSACTION_KNOWN_ADAPTERS = %i[active_record mongoid].freeze
29
- TRANSACTION_KNOWN_OPTIONS = %i[adapter].freeze
30
22
 
31
23
  module ClassMethods
32
24
  def transaction(enabled_or_options = nil, **options)
33
- unknown = options.keys - TransactionWrapper::TRANSACTION_KNOWN_OPTIONS
34
- if unknown.any?
35
- raise ArgumentError,
36
- "unknown transaction option(s): #{unknown.map(&:inspect).join(", ")}. " \
37
- "Known: #{TransactionWrapper::TRANSACTION_KNOWN_OPTIONS.map(&:inspect).join(", ")}"
38
- end
25
+ validate_options!(options, %i[adapter], :transaction)
39
26
 
40
27
  case enabled_or_options
41
28
  when false
data/lib/dex/operation.rb CHANGED
@@ -2,7 +2,6 @@
2
2
 
3
3
  # Wrapper modules (loaded before class body so `include`/`use` can find them)
4
4
  require_relative "operation/settings"
5
- require_relative "operation/props_setup"
6
5
  require_relative "operation/result_wrapper"
7
6
  require_relative "operation/record_wrapper"
8
7
  require_relative "operation/transaction_wrapper"
@@ -22,112 +21,81 @@ module Dex
22
21
  def error? = type == :error
23
22
  end
24
23
 
25
- def self._serialized_coercions
26
- @_serialized_coercions ||= {
27
- Time => ->(v) { v.is_a?(String) ? Time.parse(v) : v },
28
- Symbol => ->(v) { v.is_a?(String) ? v.to_sym : v }
29
- }.tap do |h|
30
- h[Date] = ->(v) { v.is_a?(String) ? Date.parse(v) : v } if defined?(Date)
31
- h[DateTime] = ->(v) { v.is_a?(String) ? DateTime.parse(v) : v } if defined?(DateTime)
32
- h[BigDecimal] = ->(v) { v.is_a?(String) ? BigDecimal(v) : v } if defined?(BigDecimal)
33
- end.freeze
34
- end
35
-
36
- Contract = Data.define(:params, :success, :errors)
37
-
38
- def self.contract
39
- Contract.new(
40
- params: _contract_params,
41
- success: _success_type,
42
- errors: _declared_errors
43
- )
44
- end
45
-
46
- def self._contract_params
47
- return {} unless respond_to?(:literal_properties)
48
-
49
- literal_properties.each_with_object({}) do |prop, hash|
50
- hash[prop.name] = prop.type
51
- end
52
- end
24
+ class HaltInterceptor
25
+ attr_reader :result, :halt
53
26
 
54
- private_class_method :_contract_params
55
-
56
- def self._dex_find_ref_type(type)
57
- return type if type.is_a?(Dex::RefType)
58
-
59
- if type.respond_to?(:type)
60
- inner = type.type
61
- return inner if inner.is_a?(Dex::RefType)
27
+ def initialize
28
+ raw = catch(:_dex_halt) { yield }
29
+ if raw.is_a?(Halt)
30
+ @halt = raw
31
+ @result = raw.success? ? raw.value : nil
32
+ else
33
+ @halt = nil
34
+ @result = raw
35
+ end
62
36
  end
63
- nil
64
- end
65
37
 
66
- def self._dex_coerce_serialized_hash(hash)
67
- return hash.transform_keys(&:to_sym) unless respond_to?(:literal_properties)
38
+ def halted? = !@halt.nil?
39
+ def success? = !error?
40
+ def error? = @halt&.error? || false
68
41
 
69
- result = {}
70
- literal_properties.each do |prop|
71
- name = prop.name
72
- raw = hash.key?(name) ? hash[name] : hash[name.to_s]
73
- result[name] = _dex_coerce_value(prop.type, raw)
42
+ def rethrow!
43
+ throw(:_dex_halt, @halt) if halted?
74
44
  end
75
- result
76
45
  end
77
46
 
78
- def self._dex_resolve_base_class(type)
79
- return type.model_class if type.is_a?(Dex::RefType)
80
- return _dex_resolve_base_class(type.type) if type.respond_to?(:type)
47
+ RESERVED_PROP_NAMES = %i[call perform async safe initialize].to_set.freeze
81
48
 
82
- type if type.is_a?(Class)
83
- end
49
+ include PropsSetup
50
+ include TypeCoercion
84
51
 
85
- def self._dex_coerce_value(type, value)
86
- return value unless value
87
- return value if _dex_find_ref_type(type)
52
+ Contract = Data.define(:params, :success, :errors)
88
53
 
89
- if type.respond_to?(:type) && type.is_a?(Literal::Types::ArrayType)
90
- return value.map { |v| _dex_coerce_value(type.type, v) } if value.is_a?(Array)
91
- return value
54
+ class << self
55
+ def contract
56
+ Contract.new(
57
+ params: _contract_params,
58
+ success: _success_type,
59
+ errors: _declared_errors
60
+ )
92
61
  end
93
62
 
94
- if type.respond_to?(:type) && type.is_a?(Literal::Types::NilableType)
95
- return _dex_coerce_value(type.type, value)
63
+ def inherited(subclass)
64
+ subclass.instance_variable_set(:@_pipeline, pipeline.dup)
65
+ super
96
66
  end
97
67
 
98
- base = _dex_resolve_base_class(type)
99
- coercion = _serialized_coercions[base]
100
- coercion ? coercion.call(value) : value
101
- end
68
+ def pipeline
69
+ @_pipeline ||= Pipeline.new
70
+ end
102
71
 
103
- private_class_method :_dex_resolve_base_class, :_dex_coerce_value, :_dex_find_ref_type, :_dex_coerce_serialized_hash
72
+ def use(mod, as: nil, wrap: nil, before: nil, after: nil, at: nil)
73
+ step_name = as || _derive_step_name(mod)
74
+ wrap_method = wrap || :"_#{step_name}_wrap"
75
+ pipeline.add(step_name, method: wrap_method, before: before, after: after, at: at)
76
+ include mod
77
+ end
104
78
 
105
- def self.inherited(subclass)
106
- subclass.instance_variable_set(:@_pipeline, pipeline.dup)
107
- super
108
- end
79
+ private
109
80
 
110
- def self.pipeline
111
- @_pipeline ||= Pipeline.new
112
- end
81
+ def _contract_params
82
+ return {} unless respond_to?(:literal_properties)
113
83
 
114
- def self.use(mod, as: nil, wrap: nil, before: nil, after: nil, at: nil)
115
- step_name = as || _derive_step_name(mod)
116
- wrap_method = wrap || :"_#{step_name}_wrap"
117
- pipeline.add(step_name, method: wrap_method, before: before, after: after, at: at)
118
- include mod
119
- end
84
+ literal_properties.each_with_object({}) do |prop, hash|
85
+ hash[prop.name] = prop.type
86
+ end
87
+ end
120
88
 
121
- def self._derive_step_name(mod)
122
- base = mod.name&.split("::")&.last
123
- raise ArgumentError, "anonymous modules require explicit as: parameter" unless base
89
+ def _derive_step_name(mod)
90
+ base = mod.name&.split("::")&.last
91
+ raise ArgumentError, "anonymous modules require explicit as: parameter" unless base
124
92
 
125
- base.sub(/Wrapper\z/, "")
126
- .gsub(/([a-z])([A-Z])/, '\1_\2')
127
- .downcase
128
- .to_sym
93
+ base.sub(/Wrapper\z/, "")
94
+ .gsub(/([a-z])([A-Z])/, '\1_\2')
95
+ .downcase
96
+ .to_sym
97
+ end
129
98
  end
130
- private_class_method :_derive_step_name
131
99
 
132
100
  def perform(*, **)
133
101
  end
@@ -149,28 +117,9 @@ module Dex
149
117
  new(**kwargs).call
150
118
  end
151
119
 
152
- # Serialization helpers
153
-
154
- def _props_as_json
155
- return {} unless self.class.respond_to?(:literal_properties)
156
-
157
- result = {}
158
- self.class.literal_properties.each do |prop|
159
- value = public_send(prop.name)
160
- ref = self.class.send(:_dex_find_ref_type, prop.type)
161
- result[prop.name.to_s] = if ref && value
162
- value.id
163
- else
164
- value.respond_to?(:as_json) ? value.as_json : value
165
- end
166
- end
167
- result
168
- end
169
-
170
120
  include Settings
171
121
  include AsyncWrapper
172
122
  include SafeWrapper
173
- include PropsSetup
174
123
 
175
124
  use ResultWrapper
176
125
  use LockWrapper
@@ -1,20 +1,26 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "set"
4
-
5
3
  module Dex
4
+ # Shared prop DSL for Operation and Event.
5
+ #
6
+ # Wraps Literal::Properties' prop/prop? with three Dex-specific behaviors:
7
+ # 1. Reserved name validation — each class defines RESERVED_PROP_NAMES
8
+ # 2. reader: :public by default (Literal defaults to private)
9
+ # 3. Automatic RefType coercion — _Ref(Model) props auto-coerce IDs to records
6
10
  module PropsSetup
11
+ extend Dex::Concern
12
+
7
13
  def self.included(base)
8
14
  base.extend(Literal::Properties)
9
15
  base.extend(Literal::Types)
10
- base.extend(ClassMethods)
16
+ super
11
17
  end
12
18
 
13
19
  module ClassMethods
14
- RESERVED_PROP_NAMES = %i[call perform async safe initialize].to_set.freeze
15
-
16
20
  def prop(name, type, kind = :keyword, **options, &block)
17
- _props_validate_name!(name)
21
+ if const_defined?(:RESERVED_PROP_NAMES) && self::RESERVED_PROP_NAMES.include?(name)
22
+ raise ArgumentError, "Property :#{name} is reserved."
23
+ end
18
24
  options[:reader] = :public unless options.key?(:reader)
19
25
  if type.is_a?(Dex::RefType) && !block
20
26
  ref = type
@@ -36,15 +42,6 @@ module Dex
36
42
  def _Ref(model_class, lock: false) # rubocop:disable Naming/MethodName
37
43
  Dex::RefType.new(model_class, lock: lock)
38
44
  end
39
-
40
- private
41
-
42
- def _props_validate_name!(name)
43
- return unless RESERVED_PROP_NAMES.include?(name)
44
-
45
- raise ArgumentError,
46
- "Property :#{name} conflicts with core Operation methods."
47
- end
48
45
  end
49
46
  end
50
47
  end
@@ -110,9 +110,11 @@ module Dex
110
110
  end
111
111
 
112
112
  module TestHelpers
113
+ extend Dex::Concern
114
+
113
115
  def self.included(base)
114
116
  Dex::TestWrapper.install!
115
- base.extend(ClassMethods)
117
+ super
116
118
  end
117
119
 
118
120
  def setup
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dex
4
+ module TypeCoercion
5
+ extend Dex::Concern
6
+
7
+ module ClassMethods
8
+ def _serialized_coercions
9
+ @_serialized_coercions ||= {
10
+ Time => ->(v) { v.is_a?(String) ? Time.parse(v) : v },
11
+ Symbol => ->(v) { v.is_a?(String) ? v.to_sym : v }
12
+ }.tap do |h|
13
+ h[Date] = ->(v) { v.is_a?(String) ? Date.parse(v) : v } if defined?(Date)
14
+ h[DateTime] = ->(v) { v.is_a?(String) ? DateTime.parse(v) : v } if defined?(DateTime)
15
+ h[BigDecimal] = ->(v) { v.is_a?(String) ? BigDecimal(v) : v } if defined?(BigDecimal)
16
+ end.freeze
17
+ end
18
+
19
+ private
20
+
21
+ def _find_ref_type(type)
22
+ return type if type.is_a?(Dex::RefType)
23
+
24
+ if type.respond_to?(:type)
25
+ inner = type.type
26
+ return inner if inner.is_a?(Dex::RefType)
27
+ end
28
+ nil
29
+ end
30
+
31
+ def _resolve_base_class(type)
32
+ return type.model_class if type.is_a?(Dex::RefType)
33
+ return _resolve_base_class(type.type) if type.respond_to?(:type)
34
+
35
+ type if type.is_a?(Class)
36
+ end
37
+
38
+ def _coerce_value(type, value)
39
+ return value unless value
40
+
41
+ if type.is_a?(Literal::Types::ArrayType)
42
+ return value.map { |v| _coerce_value(type.type, v) } if value.is_a?(Array)
43
+
44
+ return value
45
+ end
46
+
47
+ return _coerce_value(type.type, value) if type.is_a?(Literal::Types::NilableType)
48
+
49
+ ref = _find_ref_type(type)
50
+ return ref.coerce(value) if ref
51
+
52
+ base = _resolve_base_class(type)
53
+ coercion = _serialized_coercions[base]
54
+ coercion ? coercion.call(value) : value
55
+ end
56
+
57
+ def _serialize_value(type, value)
58
+ return value unless value
59
+
60
+ if type.is_a?(Literal::Types::ArrayType) && value.is_a?(Array)
61
+ return value.map { |v| _serialize_value(type.type, v) }
62
+ end
63
+
64
+ return _serialize_value(type.type, value) if type.is_a?(Literal::Types::NilableType)
65
+
66
+ ref = _find_ref_type(type)
67
+ return value.id if ref
68
+
69
+ value.respond_to?(:as_json) ? value.as_json : value
70
+ end
71
+
72
+ def _coerce_serialized_hash(hash)
73
+ return hash.transform_keys(&:to_sym) unless respond_to?(:literal_properties)
74
+
75
+ result = {}
76
+ literal_properties.each do |prop|
77
+ name = prop.name
78
+ raw = hash.key?(name) ? hash[name] : hash[name.to_s]
79
+ result[name] = _coerce_value(prop.type, raw)
80
+ end
81
+ result
82
+ end
83
+ end
84
+
85
+ def _props_as_json
86
+ return {} unless self.class.respond_to?(:literal_properties)
87
+
88
+ result = {}
89
+ self.class.literal_properties.each do |prop|
90
+ value = public_send(prop.name)
91
+ result[prop.name.to_s] = self.class.send(:_serialize_value, prop.type, value)
92
+ end
93
+ result
94
+ end
95
+ end
96
+ end
data/lib/dex/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Dex
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/dexkit.rb CHANGED
@@ -10,21 +10,34 @@ loader.ignore("#{__dir__}/dex")
10
10
  loader.setup
11
11
 
12
12
  require_relative "dex/version"
13
+ require_relative "dex/concern"
13
14
  require_relative "dex/ref_type"
15
+ require_relative "dex/type_coercion"
16
+ require_relative "dex/props_setup"
14
17
  require_relative "dex/error"
15
18
  require_relative "dex/operation"
19
+ require_relative "dex/event"
16
20
 
17
21
  module Dex
18
22
  class Configuration
19
- attr_accessor :record_class, :transaction_adapter
23
+ attr_accessor :record_class, :transaction_adapter, :event_store, :event_context, :restore_event_context
20
24
 
21
25
  def initialize
22
26
  @record_class = nil
23
27
  @transaction_adapter = nil
28
+ @event_store = nil
29
+ @event_context = nil
30
+ @restore_event_context = nil
24
31
  end
25
32
  end
26
33
 
27
34
  class << self
35
+ def warn(message)
36
+ return unless defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
37
+
38
+ Rails.logger.warn("[Dex] #{message}")
39
+ end
40
+
28
41
  def configuration
29
42
  @configuration ||= Configuration.new
30
43
  end