composable-core 0.0.4 → 0.0.5

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: 7a9bcadd807c77480fa834c65abcccdb7ec5cf1ea2808575c6c2137ca331d07c
4
- data.tar.gz: 4ebebe834f4807f3c7b641724a7199bc0f6c19adcbde352a49b6520215249f41
3
+ metadata.gz: a79063aeae6d1075137b90dd1f4979cd3ce45a217e0060ccdf3dea2653fb5cfe
4
+ data.tar.gz: 48941772dda21b2f50a0951be88764fe2084eac74447a91fab07c3c42c7e9ed9
5
5
  SHA512:
6
- metadata.gz: 815641fddf905a9348f40afc64d0ed62c2bc286fa34e0be0cf0d0785c52bc57e2d39a8655897d75a84a17121dfa3c8548cb1967a30857837e6dc93268097e350
7
- data.tar.gz: 8374f9fdd9724d97e1e7eef2ab21fbb39e33ef36b6ce0e77970371fc5b16552ac95542173fbe17aadd5da36852cdb223087a8709a178eb8913bff640b12ea9b4
6
+ metadata.gz: a4b2074df7a43304080cdbff56f64b5bcf2bde0fcd6c8f7231ae52f3645ac3f94779b3e651c349c7d572010bbcf459be64b7f7a4b61af08c90d60651c61d24ee
7
+ data.tar.gz: 752c88d42e650202e19e65a6709dc65dda68b3806d3d52787a08c80f2ffecf0efc93d21dc8c5c7fdb2e13d5908c6233861e48b2153c5815b6cd93a8a2fd6cb77
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Composable
4
+ module Core
5
+ module AttributeDSL
6
+ extend ActiveSupport::Concern
7
+ include InheritableAttributes
8
+
9
+ included do
10
+ inheritable_attributes :attributes, default: Set.new
11
+ inheritable_attributes :_attribute_options, default: {}
12
+ end
13
+
14
+ def initialize(options = {})
15
+ unless options.respond_to?(:has_key?)
16
+ raise ArgumentError, "When assigning attributes, you must pass a hash" \
17
+ " as an argument, #{options.class} passed."
18
+ end
19
+
20
+ attributes.each do |attribute|
21
+ # Try a `string` key if `symbol` key does not exist
22
+ value = options.fetch(attribute.to_sym, options[attribute.to_s])
23
+ send("#{attribute}=", value) unless value.nil?
24
+ end
25
+ end
26
+
27
+ def attributes
28
+ self.class.attributes
29
+ end
30
+
31
+ def params
32
+ attributes.each_with_object({}) do |attribute, hash|
33
+ hash[attribute] = send(attribute)
34
+ end
35
+ end
36
+
37
+ module ClassMethods
38
+ def attribute(*attrs)
39
+ options = attrs.extract_options!
40
+
41
+ return attributes if attrs.empty?
42
+
43
+ attrs.each do |attribute|
44
+ _save_options_for(attribute, **options)
45
+
46
+ next if attributes.include?(attribute.to_sym)
47
+
48
+ _define_getter(attribute)
49
+ _define_setter(attribute)
50
+ _define_question_mark_method(attribute)
51
+ attributes << attribute.to_sym
52
+ end
53
+ end
54
+
55
+ private
56
+
57
+ def _define_getter(attribute)
58
+ define_method(attribute) do
59
+ options = self.class._attribute_options.fetch(attribute.to_sym, {})
60
+ value = if instance_variable_defined?("@#{attribute}")
61
+ instance_variable_get("@#{attribute}")
62
+ else
63
+ instance_variable_set("@#{attribute}", options[:default].deep_dup)
64
+ end
65
+
66
+ options[:type] ? options[:type].cast(value) : value
67
+ end
68
+ end
69
+
70
+ def _define_setter(attribute)
71
+ define_method("#{attribute}=") do |value|
72
+ options = self.class._attribute_options.fetch(attribute.to_sym, {})
73
+ instance_variable_set("@#{attribute}", options[:type] ? options[:type].cast(value) : value)
74
+ end
75
+ end
76
+
77
+ def _define_question_mark_method(attribute)
78
+ define_method("#{attribute}?") do
79
+ send(attribute).present?
80
+ end
81
+ end
82
+
83
+ def _save_options_for(attribute, type: nil, default: nil, **typed_options)
84
+ options = _attribute_options[attribute.to_sym] ||= {}
85
+ options[:default] = default unless default.nil?
86
+ options[:type] = ActiveModel::Type.lookup(type, **typed_options) unless type.nil?
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Composable
4
+ module Core
5
+ module Callbacks
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ extend ActiveModel::Callbacks
10
+
11
+ define_model_callbacks :save
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Composable
4
+ module Core
5
+ module Command
6
+ class NotImplementedError < ::StandardError; end
7
+
8
+ attr_reader :result
9
+
10
+ module ClassMethods
11
+ def call(*args, **kwargs)
12
+ new(*args, **kwargs).call
13
+ end
14
+
15
+ def call!(*args, **kwargs)
16
+ new(*args, **kwargs).call(raise_exception: true)
17
+ end
18
+ end
19
+
20
+ def self.prepended(base)
21
+ base.extend ClassMethods
22
+
23
+ base.include ActiveModel::Validations
24
+ base.include ActiveSupport::Rescuable
25
+ end
26
+
27
+ def call(raise_exception: false)
28
+ fail NotImplementedError unless defined?(super)
29
+
30
+ @called = true
31
+ @result = super()
32
+
33
+ raise Error, errors.full_messages.to_sentence if raise_exception && failure?
34
+
35
+ self
36
+ rescue Exception => exception
37
+ raise if raise_exception || !rescue_with_handler(exception)
38
+
39
+ self
40
+ end
41
+
42
+ def success?
43
+ called? && !failure?
44
+ end
45
+
46
+ def failure?
47
+ called? && errors.any?
48
+ end
49
+
50
+ private
51
+
52
+ def called?
53
+ @called ||= false
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,221 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Composable
4
+ module Core
5
+ module ComposableDSL
6
+ extend ActiveSupport::Concern
7
+ include InheritableAttributes
8
+
9
+ included do
10
+ inheritable_attributes :composables, default: {}
11
+ end
12
+
13
+ module ClassMethods
14
+ def composable(attribute, **options, &block)
15
+ composables[attribute.to_sym] ||= Composable.new(attribute)
16
+ composables[attribute.to_sym].evaluate(**options, &block)
17
+
18
+ return if attributes.include?(attribute.to_sym)
19
+
20
+ attribute attribute
21
+
22
+ alias_method "#{attribute}_original_setter=", "#{attribute}="
23
+
24
+ define_method("#{attribute}=") do |value|
25
+ send("#{attribute}_original_setter=", value)
26
+ composables[attribute.to_sym].sync_attributes(
27
+ self, composable_record_for(attribute), reverse: true
28
+ )
29
+ end
30
+ end
31
+ end
32
+
33
+ # In order to prevent attribute values passed in at initialize from being
34
+ # overridden by composable_record values, composable_record mapping methods
35
+ # have to be synced first.
36
+ def initialize(options = {})
37
+ composable_keys.each do |attribute|
38
+ value = options.delete(attribute) || options.delete(attribute.to_s)
39
+ send("#{attribute}=", value) if value
40
+ end
41
+
42
+ super
43
+ end
44
+
45
+ def composables
46
+ self.class.composables
47
+ end
48
+
49
+ def save_composables
50
+ multi_db_transaction do
51
+ sync_composable_records
52
+ save_composable_records
53
+
54
+ yield if block_given?
55
+ end
56
+ end
57
+
58
+ private
59
+
60
+ def sync_composable_records
61
+ composables.each do |attribute, composable|
62
+ record = composable_record_for(attribute)
63
+ composable.sync_attributes(self, record) if composable.valid?(self, record)
64
+ end
65
+ end
66
+
67
+ def save_composable_records
68
+ composables.each do |attribute, composable|
69
+ record = composable_record_for(attribute)
70
+ record.save! if composable.valid?(self, record)
71
+ end
72
+ end
73
+
74
+ # Ensures `save!` queries are wrapped within multiple db transactions.
75
+ # It might be the case that our service is working with several composable
76
+ # objects that are gonna be saved into diffent databases, if there is an
77
+ # error, all transactions are gonna be rollback regardless of the DB.
78
+ def multi_db_transaction(attribute: composable_keys.first, &block)
79
+ return yield if attribute.nil?
80
+
81
+ index = composable_keys.index(attribute)
82
+ record = composable_record_for(attribute)
83
+
84
+ multi_db_transaction(attribute: composable_keys[index + 1]) do
85
+ record.respond_to?(:transaction) ? record.transaction(&block) : yield
86
+ end
87
+ end
88
+
89
+ def composable_keys
90
+ @composable_keys ||= composables.keys.freeze
91
+ end
92
+
93
+ def composable_record_for(attribute)
94
+ send(attribute) or
95
+ raise ArgumentError, "required composable argument: #{attribute}"
96
+ end
97
+
98
+ class Composable
99
+ attr_reader :attribute, :mapping, :conditions
100
+
101
+ def initialize(attribute)
102
+ @attribute = attribute
103
+ @mapping = {}
104
+ @conditions = Conditions.new
105
+ end
106
+
107
+ def initialize_copy(original_object)
108
+ @mapping = original_object.mapping.deep_dup
109
+ @conditions = original_object.conditions.deep_dup
110
+ end
111
+
112
+ def evaluate(**options, &block)
113
+ conditions.merge(**options)
114
+
115
+ instance_eval(&block) if block
116
+ end
117
+
118
+ def sync_attributes(form, record, reverse: false)
119
+ mapping.each_value do |instance|
120
+ instance.sync(form, record, reverse: reverse)
121
+ end
122
+ end
123
+
124
+ def valid?(...)
125
+ conditions.valid?(...)
126
+ end
127
+
128
+ private
129
+
130
+ # DSL method
131
+ def sync(*args, to: nil, **options)
132
+ args.extract_options!
133
+
134
+ args.compact.uniq.each do |name|
135
+ mapping[name.to_sym] ||= Sync.new(from: name)
136
+ mapping[name.to_sym].merge(to: (to || name), **options)
137
+ end
138
+ end
139
+
140
+ class Sync
141
+ attr_reader :from, :to, :conditions
142
+
143
+ def initialize(from:)
144
+ @from = from.to_sym
145
+ @conditions = Conditions.new
146
+ end
147
+
148
+ def initialize_copy(original_object)
149
+ @conditions = original_object.conditions.deep_dup
150
+ end
151
+
152
+ def merge(to:, **options)
153
+ @to = to.to_sym
154
+
155
+ conditions.merge(**options)
156
+ end
157
+
158
+ def sync(form, record, reverse: false)
159
+ if reverse
160
+ form.send("#{from}=", record.send(to))
161
+ else
162
+ value = form.send(from)
163
+ record.send("#{to}=", value) if conditions.valid?(form, record)
164
+ end
165
+ end
166
+ end
167
+
168
+ class Conditions
169
+ attr_reader :conditions
170
+
171
+ def initialize
172
+ @conditions = {}
173
+ end
174
+
175
+ def initialize_copy(original_object)
176
+ @conditions = original_object.conditions.deep_dup
177
+ end
178
+
179
+ def merge(**options)
180
+ options.symbolize_keys.slice(:if, :unless).each do |statement, condition|
181
+ conditions[statement] = Condition.new(statement, condition)
182
+ end
183
+ end
184
+
185
+ def valid?(form, record)
186
+ conditions.values.all? { |condition| condition.call(form, record) }
187
+ end
188
+ end
189
+
190
+ class Condition
191
+ attr_reader :condition, :statement, :callable
192
+
193
+ def initialize(statement, condition)
194
+ @statement = statement.to_sym
195
+ @condition = condition
196
+ @callable = make_lambda
197
+ end
198
+
199
+ def call(form, record)
200
+ callable.call(form, record).eql?(statement == :if)
201
+ end
202
+
203
+ private
204
+
205
+ def make_lambda
206
+ case condition
207
+ when Symbol
208
+ ->(form, _record) { form.send(condition) }
209
+ when ::Proc
210
+ lambda { |form, record|
211
+ form.instance_exec(*[record][0...condition.arity], &condition)
212
+ }
213
+ else
214
+ ->(*) { true }
215
+ end
216
+ end
217
+ end
218
+ end
219
+ end
220
+ end
221
+ end
@@ -10,7 +10,7 @@ module Composable
10
10
  module VERSION
11
11
  MAJOR = 0
12
12
  MINOR = 0
13
- TINY = 4
13
+ TINY = 5
14
14
  PRE = nil
15
15
 
16
16
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Composable
4
+ module Core
5
+ module InheritableAttributes
6
+ extend ActiveSupport::Concern
7
+
8
+ module ClassMethods
9
+ def inheritable_attributes(*attributes)
10
+ options = attributes.extract_options!
11
+
12
+ @inheritable_attributes ||= [{attribute: :inheritable_attributes}]
13
+ attributes.each do |attribute|
14
+ @inheritable_attributes << {attribute: attribute, default: options[:default]}
15
+
16
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
17
+ class << self; attr_accessor :#{attribute} end
18
+ RUBY
19
+ send("#{attribute}=", options[:default]) unless options[:default].nil?
20
+ end
21
+ @inheritable_attributes
22
+ end
23
+
24
+ def uninheritable_attributes(*attributes)
25
+ options = attributes.extract_options!
26
+
27
+ @uninheritable_attributes ||= [{attribute: :uninheritable_attributes}]
28
+
29
+ attributes.each do |attribute|
30
+ @uninheritable_attributes << {attribute: attribute, default: options[:default]}
31
+
32
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
33
+ class << self; attr_accessor :#{attribute} end
34
+ RUBY
35
+ send("#{attribute}=", options[:default]) unless options[:default].nil?
36
+ end
37
+
38
+ @uninheritable_attributes
39
+ end
40
+
41
+ def inherited(subclass)
42
+ super
43
+
44
+ Array(@inheritable_attributes).each do |hash|
45
+ inheritable_attribute = hash[:attribute]
46
+ instance_name = "@#{inheritable_attribute}"
47
+ instance_value = instance_variable_get(instance_name).deep_dup
48
+ subclass.instance_variable_set(instance_name, instance_value || hash[:default].deep_dup)
49
+ end
50
+
51
+ Array(@uninheritable_attributes).each do |hash|
52
+ uninheritable_attribute = hash[:attribute]
53
+ instance_name = "@#{uninheritable_attribute}"
54
+
55
+ if instance_name == "@uninheritable_attributes"
56
+ instance_value = instance_variable_get(instance_name).deep_dup
57
+ subclass.instance_variable_set(instance_name, instance_value)
58
+ else
59
+ subclass.instance_variable_set(instance_name, hash[:default].deep_dup)
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Composable
4
+ module Core
5
+ module RecordInvalid
6
+ extend ActiveSupport::Concern
7
+ include ActiveSupport::Rescuable
8
+
9
+ class ErrorWithRecord < Exception
10
+ attr_reader :record
11
+
12
+ def initialize(record)
13
+ @record = record
14
+ end
15
+ end
16
+
17
+ included do
18
+ prepend ComposableDSLExt
19
+
20
+ rescue_from(ErrorWithRecord) do |exception|
21
+ exception.record.errors.delete(:base)&.each { |m| errors.add(:base, m) }
22
+ exception.record.errors.each do |error|
23
+ if error.attribute.to_sym.in?(attributes)
24
+ errors.add(error.attribute, error.message)
25
+ else
26
+ errors.add(:base, exception.record.errors.full_message(error.attribute, error.message))
27
+ end
28
+ end
29
+ end
30
+ end
31
+
32
+ module ComposableDSLExt
33
+ def save_composable_records
34
+ super
35
+ rescue ActiveRecord::RecordInvalid => exception
36
+ raise ErrorWithRecord.new(exception.record)
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+ Composable::Core::ComposableDSL.include Composable::Core::RecordInvalid
@@ -1,9 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "core/version"
4
+ require "active_support"
5
+ require "active_support/core_ext/object/deep_dup"
6
+ require "active_model"
4
7
 
5
8
  module Composable
6
9
  module Core
7
10
  class Error < StandardError; end
11
+
12
+ autoload :AttributeDSL, "composable/core/attribute_dsl"
13
+ autoload :Callbacks, "composable/core/callbacks"
14
+ autoload :Command, "composable/core/command"
15
+ autoload :ComposableDSL, "composable/core/composable_dsl"
16
+ autoload :InheritableAttributes, "composable/core/inheritable_attributes"
8
17
  end
9
18
  end
19
+
20
+ require "composable/core/record_invalid" if defined?(ActiveRecord)
metadata CHANGED
@@ -1,15 +1,43 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: composable-core
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.4
4
+ version: 0.0.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jairo Vazquez
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-01-25 00:00:00.000000000 Z
12
- dependencies: []
11
+ date: 2022-04-06 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '6.1'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '6.1'
27
+ - !ruby/object:Gem::Dependency
28
+ name: activemodel
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '6.1'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '6.1'
13
41
  description: Core composable object to work with ruby services and forms
14
42
  email:
15
43
  - jairovm20@gmail.com
@@ -21,7 +49,13 @@ files:
21
49
  - LICENSE.txt
22
50
  - README.md
23
51
  - lib/composable/core.rb
52
+ - lib/composable/core/attribute_dsl.rb
53
+ - lib/composable/core/callbacks.rb
54
+ - lib/composable/core/command.rb
55
+ - lib/composable/core/composable_dsl.rb
24
56
  - lib/composable/core/gem_version.rb
57
+ - lib/composable/core/inheritable_attributes.rb
58
+ - lib/composable/core/record_invalid.rb
25
59
  - lib/composable/core/version.rb
26
60
  homepage: https://github.com/jairovm/composables
27
61
  licenses:
@@ -45,7 +79,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
45
79
  - !ruby/object:Gem::Version
46
80
  version: '0'
47
81
  requirements: []
48
- rubygems_version: 3.2.17
82
+ rubygems_version: 3.3.3
49
83
  signing_key:
50
84
  specification_version: 4
51
85
  summary: Core Composable Object