composable-core 0.0.2 → 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d56d924c8b49a41a1dd25112961b582c628ac71f6f16d4d8bcc39e5a1c1dd657
4
- data.tar.gz: e7417b50a4acb0540ecb9f0b4f46556f0db36d74866081dc9451e34dce702f3e
3
+ metadata.gz: a79063aeae6d1075137b90dd1f4979cd3ce45a217e0060ccdf3dea2653fb5cfe
4
+ data.tar.gz: 48941772dda21b2f50a0951be88764fe2084eac74447a91fab07c3c42c7e9ed9
5
5
  SHA512:
6
- metadata.gz: e791a3a17e333488b297be1d3822a2fac9eebd9910ff0d3f4f3038b91087cc36cfe1505213c154b5efa8f7894b8aad7b4832ad2a64642ae4af91d47d85e64147
7
- data.tar.gz: c3fe7a0ee96143036adde71194bc38d17d47caea05d662b6672f2f36ebad2296cbc1fc67b78b3f0d962da439c5425a19e45afec317dc92406a69e7ffb649f9e1
6
+ metadata.gz: a4b2074df7a43304080cdbff56f64b5bcf2bde0fcd6c8f7231ae52f3645ac3f94779b3e651c349c7d572010bbcf459be64b7f7a4b61af08c90d60651c61d24ee
7
+ data.tar.gz: 752c88d42e650202e19e65a6709dc65dda68b3806d3d52787a08c80f2ffecf0efc93d21dc8c5c7fdb2e13d5908c6233861e48b2153c5815b6cd93a8a2fd6cb77
data/CHANGELOG.md CHANGED
@@ -1,5 +1,5 @@
1
1
  ## [Unreleased]
2
2
 
3
- ## [0.1.0] - 2022-01-20
3
+ ## [0.0.1] - 2022-01-20
4
4
 
5
5
  - Initial release
data/README.md CHANGED
@@ -9,7 +9,7 @@ TODO: Delete this and the text above, and describe your gem
9
9
  Add this line to your application's Gemfile:
10
10
 
11
11
  ```ruby
12
- gem 'core'
12
+ gem 'composable-core'
13
13
  ```
14
14
 
15
15
  And then execute:
@@ -18,7 +18,7 @@ And then execute:
18
18
 
19
19
  Or install it yourself as:
20
20
 
21
- $ gem install core
21
+ $ gem install composable-core
22
22
 
23
23
  ## Usage
24
24
 
@@ -32,7 +32,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
32
32
 
33
33
  ## Contributing
34
34
 
35
- Bug reports and pull requests are welcome on GitHub at https://github.com/jairovm/core. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/jairovm/composables/tree/main/core/CODE_OF_CONDUCT.md).
35
+ Bug reports and pull requests are welcome on GitHub at https://github.com/jairovm/composables. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/jairovm/composables/tree/main/CODE_OF_CONDUCT.md).
36
36
 
37
37
  ## License
38
38
 
@@ -40,4 +40,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
40
40
 
41
41
  ## Code of Conduct
42
42
 
43
- Everyone interacting in the Core project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/jairovm/composables/tree/main/core/CODE_OF_CONDUCT.md).
43
+ Everyone interacting in the Core project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/jairovm/composables/tree/main/CODE_OF_CONDUCT.md).
@@ -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 = 2
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.2
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-22 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,15 +49,21 @@ 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:
28
62
  - MIT
29
63
  metadata:
30
64
  homepage_uri: https://github.com/jairovm/composables
31
- source_code_uri: https://github.com/jairovm/composables/tree/main/core
32
- changelog_uri: https://github.com/jairovm/composables/tree/main/CHANGELOG.md
65
+ source_code_uri: https://github.com/jairovm/composables/tree/main/composable-core
66
+ changelog_uri: https://github.com/jairovm/composables/tree/main/composable-core/CHANGELOG.md
33
67
  post_install_message:
34
68
  rdoc_options: []
35
69
  require_paths:
@@ -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