ledger_sync-domains 1.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 5ee91277b3f0600c5553ce5f2183da338ef451b649bbf452a9871d9aa4048f01
4
+ data.tar.gz: 3723fb3b32fcdffdc87cc4355b4b898f070cac3793f9278186f55137ff123c04
5
+ SHA512:
6
+ metadata.gz: 477cffd0916f91dfffcc6099758d597e8df1cb3f4b11aa0464e3e791b254bdccb2f6b0a3f53a2186298d17c8d41714925e6676f028d093af6b7430b172e414fc
7
+ data.tar.gz: 7b002212bff9beb39ee7b3c5fe87d6112ef56ab5f6392e7c9b2ed16e5630f3f3b5ba89efa0deba17ea912b5f289e661bf8f00e49d6d3073d781c1d3d61de4606
@@ -0,0 +1,18 @@
1
+ name: Ruby
2
+
3
+ on: [push,pull_request]
4
+
5
+ jobs:
6
+ build:
7
+ runs-on: ubuntu-latest
8
+ steps:
9
+ - uses: actions/checkout@v2
10
+ - name: Set up Ruby
11
+ uses: ruby/setup-ruby@v1
12
+ with:
13
+ ruby-version: 3.0.0
14
+ - name: Run the default task
15
+ run: |
16
+ gem install bundler -v 2.2.15
17
+ bundle install
18
+ bundle exec rake
data/.gitignore ADDED
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,16 @@
1
+ AllCops:
2
+ TargetRubyVersion: 3.0
3
+
4
+ Style/StringLiterals:
5
+ Enabled: true
6
+ EnforcedStyle: single_quotes
7
+
8
+ Style/StringLiteralsInInterpolation:
9
+ Enabled: true
10
+ EnforcedStyle: single_quotes
11
+
12
+ Layout/LineLength:
13
+ Max: 100
14
+
15
+ Style/Documentation:
16
+ Enabled: false
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2021-08-31
4
+
5
+ - Initial release
data/Gemfile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in ledger_sync-domains.gemspec
6
+ gemspec
7
+
8
+ gem "rake", "~> 13.0"
9
+
10
+ gem "rspec", "~> 3.0"
11
+
12
+ gem "rubocop", "~> 1.7"
data/Gemfile.lock ADDED
@@ -0,0 +1,161 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ ledger_sync-domains (1.0.0.rc1)
5
+ ledger_sync (~> 2.2.0)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ activemodel (6.1.4.1)
11
+ activesupport (= 6.1.4.1)
12
+ activesupport (6.1.4.1)
13
+ concurrent-ruby (~> 1.0, >= 1.0.2)
14
+ i18n (>= 1.6, < 2)
15
+ minitest (>= 5.1)
16
+ tzinfo (~> 2.0)
17
+ zeitwerk (~> 2.3)
18
+ ast (2.4.2)
19
+ colorize (0.8.1)
20
+ concurrent-ruby (1.1.9)
21
+ diff-lcs (1.4.4)
22
+ dotenv (2.7.6)
23
+ dry-configurable (0.12.1)
24
+ concurrent-ruby (~> 1.0)
25
+ dry-core (~> 0.5, >= 0.5.0)
26
+ dry-container (0.8.0)
27
+ concurrent-ruby (~> 1.0)
28
+ dry-configurable (~> 0.1, >= 0.1.3)
29
+ dry-core (0.7.1)
30
+ concurrent-ruby (~> 1.0)
31
+ dry-equalizer (0.3.0)
32
+ dry-inflector (0.2.1)
33
+ dry-initializer (3.0.4)
34
+ dry-logic (1.2.0)
35
+ concurrent-ruby (~> 1.0)
36
+ dry-core (~> 0.5, >= 0.5)
37
+ dry-schema (1.5.6)
38
+ concurrent-ruby (~> 1.0)
39
+ dry-configurable (~> 0.8, >= 0.8.3)
40
+ dry-core (~> 0.4)
41
+ dry-equalizer (~> 0.2)
42
+ dry-initializer (~> 3.0)
43
+ dry-logic (~> 1.0)
44
+ dry-types (~> 1.4)
45
+ dry-types (1.5.1)
46
+ concurrent-ruby (~> 1.0)
47
+ dry-container (~> 0.3)
48
+ dry-core (~> 0.5, >= 0.5)
49
+ dry-inflector (~> 0.1, >= 0.1.2)
50
+ dry-logic (~> 1.0, >= 1.0.2)
51
+ dry-validation (1.5.6)
52
+ concurrent-ruby (~> 1.0)
53
+ dry-container (~> 0.7, >= 0.7.1)
54
+ dry-core (~> 0.4)
55
+ dry-equalizer (~> 0.2)
56
+ dry-initializer (~> 3.0)
57
+ dry-schema (~> 1.5, >= 1.5.2)
58
+ faraday (1.7.1)
59
+ faraday-em_http (~> 1.0)
60
+ faraday-em_synchrony (~> 1.0)
61
+ faraday-excon (~> 1.1)
62
+ faraday-httpclient (~> 1.0.1)
63
+ faraday-net_http (~> 1.0)
64
+ faraday-net_http_persistent (~> 1.1)
65
+ faraday-patron (~> 1.0)
66
+ faraday-rack (~> 1.0)
67
+ multipart-post (>= 1.2, < 3)
68
+ ruby2_keywords (>= 0.0.4)
69
+ faraday-detailed_logger (2.3.0)
70
+ faraday (>= 0.8, < 2)
71
+ faraday-em_http (1.0.0)
72
+ faraday-em_synchrony (1.0.0)
73
+ faraday-excon (1.1.0)
74
+ faraday-httpclient (1.0.1)
75
+ faraday-net_http (1.0.1)
76
+ faraday-net_http_persistent (1.2.0)
77
+ faraday-patron (1.0.0)
78
+ faraday-rack (1.0.0)
79
+ faraday_middleware (1.1.0)
80
+ faraday (~> 1.0)
81
+ fingerprintable (1.2.1)
82
+ colorize
83
+ i18n (1.8.10)
84
+ concurrent-ruby (~> 1.0)
85
+ ledger_sync (2.2.0)
86
+ activemodel
87
+ colorize
88
+ dry-schema (~> 1.5.4)
89
+ dry-validation (~> 1.5.6)
90
+ faraday
91
+ faraday-detailed_logger
92
+ faraday_middleware
93
+ fingerprintable (>= 1.2.1)
94
+ nokogiri
95
+ openssl (~> 2.2.0)
96
+ pd_ruby
97
+ rack (~> 2.2.3)
98
+ resonad
99
+ simply_serializable (>= 1.5.1)
100
+ minitest (5.14.4)
101
+ multipart-post (2.1.1)
102
+ nokogiri (1.12.4-x86_64-linux)
103
+ racc (~> 1.4)
104
+ openssl (2.2.0)
105
+ parallel (1.20.1)
106
+ parser (3.0.2.0)
107
+ ast (~> 2.4.1)
108
+ pd_ruby (0.2.3)
109
+ colorize
110
+ racc (1.5.2)
111
+ rack (2.2.3)
112
+ rainbow (3.0.0)
113
+ rake (13.0.6)
114
+ regexp_parser (2.1.1)
115
+ resonad (1.4.0)
116
+ rexml (3.2.5)
117
+ rspec (3.10.0)
118
+ rspec-core (~> 3.10.0)
119
+ rspec-expectations (~> 3.10.0)
120
+ rspec-mocks (~> 3.10.0)
121
+ rspec-core (3.10.1)
122
+ rspec-support (~> 3.10.0)
123
+ rspec-expectations (3.10.1)
124
+ diff-lcs (>= 1.2.0, < 2.0)
125
+ rspec-support (~> 3.10.0)
126
+ rspec-mocks (3.10.2)
127
+ diff-lcs (>= 1.2.0, < 2.0)
128
+ rspec-support (~> 3.10.0)
129
+ rspec-support (3.10.2)
130
+ rubocop (1.20.0)
131
+ parallel (~> 1.10)
132
+ parser (>= 3.0.0.0)
133
+ rainbow (>= 2.2.2, < 4.0)
134
+ regexp_parser (>= 1.8, < 3.0)
135
+ rexml
136
+ rubocop-ast (>= 1.9.1, < 2.0)
137
+ ruby-progressbar (~> 1.7)
138
+ unicode-display_width (>= 1.4.0, < 3.0)
139
+ rubocop-ast (1.11.0)
140
+ parser (>= 3.0.1.1)
141
+ ruby-progressbar (1.11.0)
142
+ ruby2_keywords (0.0.5)
143
+ simply_serializable (1.5.1)
144
+ fingerprintable (>= 1.2.1)
145
+ tzinfo (2.0.4)
146
+ concurrent-ruby (~> 1.0)
147
+ unicode-display_width (2.0.0)
148
+ zeitwerk (2.4.2)
149
+
150
+ PLATFORMS
151
+ x86_64-linux
152
+
153
+ DEPENDENCIES
154
+ dotenv
155
+ ledger_sync-domains!
156
+ rake (~> 13.0)
157
+ rspec (~> 3.0)
158
+ rubocop (~> 1.7)
159
+
160
+ BUNDLED WITH
161
+ 2.2.15
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2021 Jozef Vaclavik
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,254 @@
1
+ # LedgerSync::Domains
2
+
3
+ Achieving Domain Driven Design (DDD) inside of a rails application is not a simple task. Rails engines by themself provide minimal value for code isolation. There are several gems that allows you to handle cross-engine communication through service/operation/inflection classes. Unfortunately these only touches execution, but they won't help you with passing around serialized objects.
4
+
5
+ LedgerSync has been developed to handle cross-service (API) communication in elegant way. Same aproach can be used for cross-domain communication.
6
+
7
+ Operations are great to ensure there is single point to perform specific action. If the return object is regular ActiveRecord Model object, there is nothing that stops developer from accessing cross-domain relationship, updating it or calling another action on it. You can have rubocop scanning through the code and screaming every time it finds something fishy, or you can just stop passing ActiveRecord objects around. And that is where `LedgerSync::Serializer` comes handy. It gives you simple way to define how your object should look towards specific domain. Instead of passing ruby hash(es), you work with `OpenStruct` objects that supports relationships.
8
+
9
+ Use LedgerSync Operations to trigger actions from other domains and LedgerSync Serializers to pass around serialized objects instead of ActiveRecord Models. ActiveRecord Models are compatible with LedgerSync Resources and can be serialized to OpenStruct objects automatically.
10
+
11
+ ## Installation
12
+
13
+ Add this line to your application's Gemfile:
14
+
15
+ ```ruby
16
+ gem 'ledger_sync-domains'
17
+ ```
18
+
19
+ And then execute:
20
+
21
+ $ bundle install
22
+
23
+ Or install it yourself as:
24
+
25
+ $ gem install ledger_sync-domains
26
+
27
+ ## Usage
28
+
29
+ LedgerSync comes with 3 components.
30
+ 1. Resource
31
+ 2. Serializers
32
+ 3. Operations
33
+
34
+ ### Resource/Model
35
+
36
+ While `LedgerSync::Resources` represent object from external service, in case of domains we replace this with build in ActiveRecord Models. If you wish to use `LedgerSync::Domains` outside of a rails app (there is nothing that stops you doing that), just use LedgerSync::Resource to prepare your data for Serializers.
37
+
38
+ Use of ActiveRecord Models allows us to easily work with current rails data structure. Define your models as you would do in any rails application.
39
+
40
+ ```ruby
41
+ class Customer < ActiveRecord::Base
42
+ has_many :addresses
43
+ belongs_to :user, class_name: 'Auth::User'
44
+ end
45
+
46
+ class Address < ActiveRecord::Base
47
+ belongs_to :customer
48
+ end
49
+
50
+ module Auth
51
+ class User < ActiveRecord::Base
52
+ has_many :customers
53
+ end
54
+ end
55
+ ```
56
+
57
+ Alternatively you can use LedgerSync::Resource class to wrap your data into Model-like objects with relationships.
58
+ ```ruby
59
+ class Customer < LedgerSync::Resource
60
+ attribute :name, type: LedgerSync::Type::String
61
+
62
+ references_many :addresses, to: Address
63
+ references_one :user, to: Auth::User
64
+ end
65
+
66
+ class Address < LedgerSync::Resource
67
+ attribute :address, type: LedgerSync::Type::String
68
+ attribute :city, type: LedgerSync::Type::String
69
+ attribute :country, type: LedgerSync::Type::String
70
+
71
+ references_one :customer, to: Customer
72
+ end
73
+
74
+ module Auth
75
+ class User < LedgerSync::Resource
76
+ attribute :email, type: LedgerSync::Type::String
77
+
78
+ references_many :customers, to: Customer
79
+ end
80
+ end
81
+ ```
82
+
83
+ In above example(s) you can see two models `Customer` and `Address` that are part of `Main` domain, and `User` model being part of `Auth` domain. For the sake of examples, we will have also `Client` domain that consumes both resources from `Main` as well as `Auth` domain.
84
+
85
+ ### Serializers
86
+
87
+ Next step is to serialize records from these Models/Resources. One of the concepts of DDD is to be able to present your object differently to each domain. For example `User` can exposes different set of attributes towards `Client` domain as well as `Main` domain. `LedgerSync::Serializer` allows you to define serializer for each domain and specify which attributes will be exposed towards each domain.
88
+
89
+ ```ruby
90
+ module Auth
91
+ module Users
92
+ class AuthSerializer < LedgerSync::Domains::Serializer
93
+ attribute :email
94
+ end
95
+
96
+ class ClientSerializer < LedgerSync::Domains::Serializer
97
+ attribute :email
98
+ end
99
+ end
100
+ end
101
+
102
+ module Addresses
103
+ class ClientSerializer < LedgerSync::Domains::Serializer
104
+ attribute :address
105
+ attribute :city
106
+ attribute :state
107
+ end
108
+ end
109
+
110
+ module Customers
111
+ class AuthSerializer < LedgerSync::Domains::Serializer
112
+ attribute :id, resource_attribute: :ledger_id
113
+ attribute :name
114
+ references_one :user, resource_attribute: :user,
115
+ serializer: Auth::Users::AuthSerializer
116
+ end
117
+
118
+ class ClientSerializer < LedgerSync::Domains::Serializer
119
+ attribute :id, resource_attribute: :ledger_id
120
+ attribute :name
121
+ references_many :addresses, resource_attribute: :addresses,
122
+ serializer: Addresses::ClientSerializer
123
+ end
124
+ end
125
+ ```
126
+
127
+ In above example we represent `Customer` with two serializers aimed towards two domains. `Customer::AuthSerializer` exposes customer and `user` relationship, while `Customer::ClientSerializer` exposes customer and `addresses` relationship.
128
+
129
+ `LedgerSync::Domains::Serializer` serializes into `OpenStruct` object. Original resource is part of it and used to lazily load and serialize relationships. This way relationships are queried and serialized only once required. Here is quick example below.
130
+
131
+
132
+ Here is quick example how that looks in above example.
133
+ ```ruby
134
+ # load all above classes - watch for dependencies
135
+
136
+ irb(main):001:0> user = Auth::User.new(email: 'test@ledger_sync.dev')
137
+ => #<Auth::User:0x00005624204f1da8 @resource_attributes=#<LedgerSync::ResourceAttributeSet:0x00005624204f1790 @attributes={:external_id=>#<L...
138
+ irb(main):002:0> customer = Customer.new(ledger_id: '123', name: 'LedgerSync', user: user)
139
+ => #<Customer:0x0000562420792300 @resource_attributes=#<LedgerSync::ResourceAttributeSet:0x0000562420791dd8 @attributes={:external_id=>#<Led...
140
+
141
+ irb(main):003:0> auth_customer = Customers::AuthSerializer.new.serialize(resource: customer)
142
+ => #<Customers::AuthStruct id="123", name="LedgerSync">
143
+ irb(main):004:0> auth_customer.user
144
+ => #<Auth::Users::AuthStruct email="test@ledger_sync.dev">
145
+
146
+ irb(main):005:0> client_customer = Customers::ClientSerializer.new.serialize(resource: customer)
147
+ => #<Customers::ClientStruct id="123", name="LedgerSync">
148
+ irb(main):006:0> client_customer.user
149
+ Traceback (most recent call last):
150
+ 3: from ./bin/console:15:in `<main>'
151
+ 2: from (irb):06:in `<main>'
152
+ 1: from /root/.asdf/installs/ruby/3.0.0/lib/ruby/3.0.0/delegate.rb:91:in `method_missing'
153
+ NoMethodError (undefined method `user' for #<OpenStruct id="123", name="LedgerSync">)
154
+ irb(main):007:0> client_customer.addresses
155
+ => []
156
+ ```
157
+ Above you can see that `client_customer` can't access `user` relationship, while `auth_customer` can.
158
+
159
+ ### Operations
160
+
161
+ Operations allows you to expose actions towards other domains. Validate input based on specified contract and return result object that you can use to retrieve data or error details. Here is sample operation to fetch object by specific ID and one to deactivate an user.
162
+
163
+ ```ruby
164
+ module Auth
165
+ module Users
166
+ class FindOperation < LedgerSync::Domains::Operation::Find
167
+ # Ha, that was easy!
168
+ end
169
+ end
170
+ end
171
+
172
+ module Auth
173
+ module Users
174
+ class DeactivateOperation
175
+ include LedgerSync::Domains::Operation::Mixin
176
+
177
+ class Contract < LedgerSync::Ledgers::Contract
178
+ params do
179
+ required(:id).value(:integer)
180
+ end
181
+ end
182
+
183
+ private
184
+
185
+ def operate
186
+ return failure('Not found') if resource.nil?
187
+
188
+ if resource.update(active: false)
189
+ success(serialize(resource: resource))
190
+ else
191
+ failure('Unable to deactivate')
192
+ end
193
+ end
194
+
195
+ def resource
196
+ @resource ||= User.find_by(id: params[:id])
197
+ end
198
+
199
+ def failure(message)
200
+ super(
201
+ LedgerSync::Error::OperationError.new(
202
+ operation: self,
203
+ message: message
204
+ )
205
+ )
206
+ end
207
+ end
208
+ end
209
+ end
210
+ ```
211
+ Successful result of an operation should be serialized resource(s). Operation by itself doesn't know what domain triggered it, and therefore you need to pass in serializer you want to use to serialize the resource. Here is how that looks for the example above.
212
+
213
+ ```ruby
214
+ irb(main):001:0> op = Auth::Users::FindOperation.new(id: 1, limit: {}, serializer: Auth::Users::ClientSerializer.new)
215
+ => #<Auth::Users::FindOperation:0x0000555937876cf8 @serializer=#<Auth::Users::ClientSerializer:0x0000555937876dc0>, @deserializer=nil...
216
+ irb(main):002:0> op.perform
217
+ => #<LedgerSync::Domains::Operation::OperationResult::Success:0x00005559372f9d70 @meta=nil, @value=#<OpenStruct email="test@ledger_sync.dev">>
218
+ irb(main):003:0> op.result.value
219
+ => #<Auth::Users::ClientStruct email="test@ledger_sync.dev">
220
+ ```
221
+
222
+ And thats it. Now you can use `LedgerSync` to define operations and have their results serialized against specific domain you are requesting it from.
223
+
224
+ ### Cross-domain relationships
225
+
226
+ One important note about relationships. Splitting your app into multiple engines is eventually gonna lead to your ActiveRecord Models to have relationships that reference Models from other engines. There are two ways how to look at this issue.
227
+
228
+ #### Use of cross-domain relationships is bad
229
+
230
+ In this case, you don't define them. Or if you do, you override reader method to raise exception. If `user` references `customer`, you don't access it through `user.customer`, but you retrieve it through operation `Engine::Customers::FindOperation.new(id: user.customer_id)`. This is a clean solution, but it will lead to N+1 queries.
231
+
232
+ #### Use of cross-domain relationships is fine
233
+
234
+ If you double down on getting into root of an issue, you will realize that resources reference each other is not really the source of it. The problem is that if you work with ActiveRecord objects, there is nothing stopping you from accessing related records and modifying them.
235
+
236
+ That cannot happen when accessing objects from other domains. In that case you retrieve serialized object through operation. Accessing relationships through serialized object will always return serialized relationship.
237
+
238
+ The problem is when working with records within current engine where fetching data from ActiveRecord is accepted practice. There is nothing that stops you from crossing domain boundary.
239
+
240
+ The obvious solution is to use Operations to work with Models within current engine as well. Serialized `OpenStruct` objects are (or try to be) compatible with ActiveRecord objects. They require almost zero changes in your templates. So think of them as drop-in replacement.
241
+
242
+ ## Development
243
+
244
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
245
+
246
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
247
+
248
+ ## Contributing
249
+
250
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/ledger_sync-domains.
251
+
252
+ ## License
253
+
254
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
data/bin/console ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "ledger_sync/domains"
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require "irb"
15
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/ledger_sync/domains/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'ledger_sync-domains'
7
+ spec.version = LedgerSync::Domains::VERSION
8
+ spec.authors = ['Jozef Vaclavik']
9
+ spec.email = ['jozef@dropbot.sh']
10
+
11
+ spec.summary = 'LedgerSync for Domains/Engines'
12
+ spec.description = 'Use LedgerSync Operations and Serializers for cross-domain communication.'
13
+ spec.homepage = 'https://engineering.dropbot.sh'
14
+ spec.license = 'MIT'
15
+ spec.required_ruby_version = Gem::Requirement.new('>= 3.0.0')
16
+
17
+ # spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'"
18
+
19
+ spec.metadata['homepage_uri'] = spec.homepage
20
+ spec.metadata['source_code_uri'] = 'https://github.com/LedgerSync/ledger_sync-domains'
21
+ spec.metadata['changelog_uri'] = 'https://github.com/LedgerSync/ledger_sync-domains/blob/main/CHANGELOG.md'
22
+
23
+ # Specify which files should be added to the gem when it is released.
24
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
25
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
26
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
27
+ end
28
+ spec.bindir = 'exe'
29
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
30
+ spec.require_paths = ['lib']
31
+
32
+ spec.add_dependency 'ledger_sync', '~> 2.2.0'
33
+ spec.add_development_dependency 'dotenv'
34
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../operation'
4
+
5
+ module LedgerSync
6
+ module Domains
7
+ class Operation
8
+ class Add
9
+ include LedgerSync::Domains::Operation::Mixin
10
+
11
+ private
12
+
13
+ def operate
14
+ if resource.save
15
+ success
16
+ else
17
+ failure(
18
+ 'Please review the problems below:',
19
+ data: serialize(resource: resource)
20
+ )
21
+ end
22
+ end
23
+
24
+ def resource
25
+ @resource ||= resource_class.new(params)
26
+ end
27
+
28
+ def success
29
+ super(
30
+ serialize(resource: resource)
31
+ )
32
+ end
33
+
34
+ def failure(message, data: nil)
35
+ super(
36
+ LedgerSync::Error::OperationError.new(
37
+ operation: self,
38
+ message: message,
39
+ response: data
40
+ )
41
+ )
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../operation'
4
+
5
+ module LedgerSync
6
+ module Domains
7
+ class Operation
8
+ class Find
9
+ include LedgerSync::Domains::Operation::Mixin
10
+
11
+ class Contract < LedgerSync::Ledgers::Contract
12
+ params do
13
+ required(:id).filled(:integer)
14
+ required(:limit).value(:hash)
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ def operate
21
+ if resource
22
+ success
23
+ else
24
+ failure('Not found')
25
+ end
26
+ end
27
+
28
+ def resource
29
+ @resource ||= resource_class.find_by(id: params[:id])
30
+ end
31
+
32
+ def success
33
+ super(
34
+ serialize(resource: resource)
35
+ )
36
+ end
37
+
38
+ def failure(message)
39
+ super(
40
+ LedgerSync::Error::OperationError.new(
41
+ operation: self,
42
+ message: message
43
+ )
44
+ )
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../operation'
4
+
5
+ module LedgerSync
6
+ module Domains
7
+ class Operation
8
+ class Remove
9
+ include LedgerSync::Domains::Operation::Mixin
10
+
11
+ private
12
+
13
+ def operate
14
+ return failure('Resource not found') unless resource
15
+
16
+ if resource.destroy
17
+ success
18
+ else
19
+ failure(
20
+ 'Please review the problems below:',
21
+ data: serialize(resource: resource)
22
+ )
23
+ end
24
+ end
25
+
26
+ def resource
27
+ @resource ||= resource_class.find_by(id: params[:id])
28
+ end
29
+
30
+ def success
31
+ super(
32
+ true
33
+ )
34
+ end
35
+
36
+ def failure(message, data: nil)
37
+ super(
38
+ LedgerSync::Error::OperationError.new(
39
+ operation: self,
40
+ message: message,
41
+ response: data
42
+ )
43
+ )
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../operation'
4
+
5
+ module LedgerSync
6
+ module Domains
7
+ class Operation
8
+ class Search
9
+ include LedgerSync::Domains::Operation::Mixin
10
+
11
+ # make kaminari work with serialized results
12
+ class PaginatedResult < SimpleDelegator
13
+ attr_accessor :next_page, :last_page, :current_page, :total_pages,
14
+ :limit_value, :total_count
15
+
16
+ def self.from_result(result, items:)
17
+ search = new(items)
18
+ search.next_page = result.next_page
19
+ search.last_page = result.last_page?
20
+ search.current_page = result.current_page
21
+ search.total_pages = result.total_pages
22
+ search.total_count = result.total_count
23
+ search.limit_value = result.limit_value
24
+ search
25
+ end
26
+
27
+ def last_page?
28
+ last_page == true
29
+ end
30
+ end
31
+
32
+ class Contract < LedgerSync::Ledgers::Contract
33
+ params do
34
+ required(:query).value(:hash)
35
+ required(:limit).value(:hash)
36
+ required(:includes).value(:array)
37
+ required(:order).value(:string)
38
+ required(:page).value(:integer)
39
+ required(:per).value(:integer)
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ def operate
46
+ if resources
47
+ success
48
+ else
49
+ failure('Not found')
50
+ end
51
+ end
52
+
53
+ def resources # rubocop:disable Metrics/AbcSize
54
+ @resources ||= resource_class.where(params[:limit])
55
+ .where(params[:query])
56
+ .includes(params[:includes])
57
+ .order(params[:order])
58
+ .page(params[:page])
59
+ .per(params[:per])
60
+ end
61
+
62
+ def success
63
+ super(
64
+ PaginatedResult.from_result(
65
+ resources,
66
+ items: resources.map { |resource| serialize(resource: resource) }
67
+ )
68
+ )
69
+ end
70
+
71
+ def failure(message)
72
+ super(
73
+ LedgerSync::Error::OperationError.new(
74
+ operation: self,
75
+ message: message
76
+ )
77
+ )
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../operation'
4
+
5
+ module LedgerSync
6
+ module Domains
7
+ class Resource
8
+ class Transition
9
+ include LedgerSync::Domains::Operation::Mixin
10
+
11
+ class Contract < LedgerSync::Ledgers::Contract
12
+ params do
13
+ required(:model_name).filled(:string)
14
+ required(:id).filled(:integer)
15
+ required(:event).value(:string)
16
+ required(:attrs).maybe(:hash)
17
+ required(:attrs).maybe(:array)
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def operate
24
+ if resource.present?
25
+ if resource.send(guard_method, params[:attrs]) &&
26
+ resource.send(event_method, params[:attrs])
27
+ success
28
+ else
29
+ failure('Unable to transition')
30
+ end
31
+ else
32
+ failure('Not found')
33
+ end
34
+ end
35
+
36
+ def guard_method
37
+ "may_#{params[:event]}?"
38
+ end
39
+
40
+ def event_method
41
+ "#{params[:event]}!"
42
+ end
43
+
44
+ def resource
45
+ @resource ||= resource_class.find_by(id: params[:id])
46
+ end
47
+
48
+ def resource_class
49
+ @resource_class ||= Object.const_get(params[:model_name])
50
+ end
51
+
52
+ def success
53
+ super(
54
+ serialize(resource: resource)
55
+ )
56
+ end
57
+
58
+ def failure(message)
59
+ super(
60
+ LedgerSync::Error::OperationError.new(
61
+ operation: self,
62
+ message: message
63
+ )
64
+ )
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../operation'
4
+
5
+ module LedgerSync
6
+ module Domains
7
+ class Operation
8
+ class Update
9
+ include LedgerSync::Domains::Operation::Mixin
10
+
11
+ private
12
+
13
+ def operate
14
+ return failure('Resource not found') unless resource
15
+
16
+ if resource.update(params.except(:id))
17
+ success
18
+ else
19
+ failure(
20
+ 'Please review the problems below:',
21
+ data: serialize(resource: resource)
22
+ )
23
+ end
24
+ end
25
+
26
+ def resource
27
+ @resource ||= resource_class.find_by(id: params[:id])
28
+ end
29
+
30
+ def success
31
+ super(
32
+ serialize(resource: resource)
33
+ )
34
+ end
35
+
36
+ def failure(message, data: nil)
37
+ super(
38
+ LedgerSync::Error::OperationError.new(
39
+ operation: self,
40
+ message: message,
41
+ response: data
42
+ )
43
+ )
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,174 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LedgerSync
4
+ module Domains
5
+ class Operation
6
+ class OperationResult
7
+ module ResultTypeBase
8
+ attr_reader :meta
9
+
10
+ def self.included(base)
11
+ base.class_eval do
12
+ simply_serialize only: %i[meta]
13
+ end
14
+ end
15
+
16
+ def initialize(*args, meta: nil)
17
+ @meta = meta
18
+ super(*args)
19
+ end
20
+ end
21
+
22
+ include LedgerSync::ResultBase
23
+ end
24
+
25
+ module Mixin
26
+ module ClassMethods
27
+ def inferred_resource_class
28
+ name = to_s.split('::')
29
+ name.pop # remove serializer/operation class from name
30
+ resource = name.pop.singularize # pluralized resource module name
31
+
32
+ const_get((name + [resource]).join('::'))
33
+ end
34
+
35
+ def inferred_serializer_class
36
+ const_get("#{inferred_resource_class}Serializer")
37
+ end
38
+
39
+ def inferred_deserializer_class
40
+ const_get("#{inferred_resource_class}Deserializer")
41
+ end
42
+
43
+ def inferred_validation_contract_class
44
+ const_get("#{self}::Contract")
45
+ end
46
+ end
47
+
48
+ def self.included(base)
49
+ base.include SimplySerializable::Mixin
50
+ base.include Fingerprintable::Mixin
51
+ base.include LedgerSync::Error::HelpersMixin
52
+ base.extend ClassMethods
53
+
54
+ base.class_eval do
55
+ simply_serialize only: %i[
56
+ params
57
+ result
58
+ ]
59
+ end
60
+ end
61
+
62
+ attr_reader :params, :result
63
+
64
+ def initialize(serializer: nil, deserializer: nil, **params)
65
+ @serializer = serializer
66
+ @deserializer = deserializer
67
+ @params = params
68
+ @result = nil
69
+ end
70
+
71
+ def perform # rubocop:disable Metrics/MethodLength
72
+ if performed?
73
+ return failure(
74
+ LedgerSync::Error::OperationError::PerformedOperationError.new(
75
+ operation: self
76
+ )
77
+ )
78
+ end
79
+ return errors unless valid?
80
+
81
+ @result = begin
82
+ operate
83
+ rescue LedgerSync::Error => e
84
+ failure(e)
85
+ rescue StandardError => e
86
+ failure(e)
87
+ ensure
88
+ @performed = true
89
+ end
90
+ end
91
+
92
+ def performed?
93
+ @performed == true
94
+ end
95
+
96
+ def serialize(resource:)
97
+ serializer.serialize(resource: resource)
98
+ end
99
+
100
+ def deserializer
101
+ @deserializer ||= deserializer_class.new
102
+ end
103
+
104
+ def deserializer_class
105
+ @deserializer_class ||= self.class.inferred_deserializer_class
106
+ end
107
+
108
+ def serializer
109
+ @serializer ||= serializer_class.new
110
+ end
111
+
112
+ def serializer_class
113
+ @serializer_class ||= self.class.inferred_serializer_class
114
+ end
115
+
116
+ def resource_class
117
+ @resource_class ||= self.class.inferred_resource_class
118
+ end
119
+
120
+ # Results
121
+
122
+ def failure(error)
123
+ @result = OperationResult.Failure(error)
124
+ end
125
+
126
+ def failure?
127
+ result.failure?
128
+ end
129
+
130
+ def success(value, meta: nil)
131
+ @result = OperationResult.Success(value, meta: meta)
132
+ end
133
+
134
+ def success?
135
+ result.success?
136
+ end
137
+
138
+ def valid?
139
+ validate.success?
140
+ end
141
+
142
+ def validate
143
+ LedgerSync::Util::Validator.new(
144
+ contract: validation_contract,
145
+ data: params
146
+ ).validate
147
+ end
148
+
149
+ def validation_contract
150
+ @validation_contract ||= self.class.inferred_validation_contract_class
151
+ end
152
+
153
+ def errors
154
+ validate.validator.errors
155
+ end
156
+
157
+ # Comparison
158
+
159
+ def ==(other)
160
+ return false unless self.class == other.class
161
+ return false unless params == other.params
162
+
163
+ true
164
+ end
165
+
166
+ private
167
+
168
+ def operate
169
+ raise NotImplementedError, self.class.name
170
+ end
171
+ end
172
+ end
173
+ end
174
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LedgerSync
4
+ module Domains
5
+ class Serializer < LedgerSync::Serializer
6
+ def create_record_from(hash, resource:, references:) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
7
+ # This is the most ruby hackery I ever did
8
+ # Defining Record class that inherits from SimpleDelegator is not
9
+ # enough. Adding methods through define_method was adding these methods
10
+ # to all objects created through this main class. Specifically address
11
+ # record has an attribute called address, which returned serialized
12
+ # address. This is a same approach, but with dynamic class definition
13
+ # to avoid defining methods in unrelated classes. We are using
14
+ # SimpleDelegator to delegate these methods into OpenStruct passed in.
15
+ # Pure hackery.
16
+ klass = Class.new(SimpleDelegator) do
17
+ def self.with_lazy_references(struct, resource:, references:) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
18
+ define_method('valid?') { resource.valid? }
19
+ define_method('errors') { resource.errors }
20
+ references.each do |args|
21
+ define_method(args.hash_attribute) do
22
+ if args.references_many?
23
+ resource.try(args.resource_attribute).each do |item|
24
+ args.type.serializer.new.serialize(resource: item)
25
+ end
26
+ else
27
+ item = resource.try(args.resource_attribute)
28
+ next if item.nil?
29
+
30
+ args.type.serializer.new.serialize(resource: item)
31
+ end
32
+ end
33
+ end
34
+
35
+ new(struct)
36
+ end
37
+
38
+ def to_param
39
+ id.to_s
40
+ end
41
+
42
+ def persisted?
43
+ id.present?
44
+ end
45
+ end
46
+
47
+ name = self.class.to_s.split('::')
48
+ struct_name = name.pop.sub(/.*\KSerializer/, 'Struct')
49
+ module_name = name.empty? ? Object : Object.const_get(name.join('::'))
50
+ module_name.const_set(struct_name, Class.new(OpenStruct))
51
+
52
+ klass.with_lazy_references(
53
+ module_name.const_get(struct_name).new(hash),
54
+ resource: resource, references: references
55
+ )
56
+ end
57
+
58
+ def self.references
59
+ @references ||= []
60
+ end
61
+
62
+ def self.split_attributes
63
+ regular = []
64
+ references = []
65
+
66
+ attributes.each_value do |attr|
67
+ if attr.references_many? || attr.references_one?
68
+ references.push(attr)
69
+ else
70
+ regular.push(attr)
71
+ end
72
+ end
73
+ [regular, references]
74
+ end
75
+
76
+ def serialize(args = {}) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
77
+ only_changes = args.fetch(:only_changes, false)
78
+ resource = args.fetch(:resource)
79
+
80
+ ret = {}
81
+
82
+ regular, references = self.class.split_attributes
83
+ regular.each do |serializer_attribute|
84
+ if (only_changes && !resource.attribute_changed?(serializer_attribute.resource_attribute)) || # rubocop:disable Layout/LineLength
85
+ (serializer_attribute.if_method.present? && !send(serializer_attribute.if_method, resource: resource)) # rubocop:disable Layout/LineLength
86
+ next
87
+ end
88
+
89
+ ret = LedgerSync::Util::HashHelpers.deep_merge(
90
+ hash_to_merge_into: ret,
91
+ other_hash: serializer_attribute.hash_attribute_hash_for(resource: resource)
92
+ )
93
+ end
94
+ create_record_from(ret, resource: resource, references: references)
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LedgerSync
4
+ module Domains
5
+ VERSION = '1.0.0.rc1'
6
+ end
7
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ledger_sync'
4
+ require_relative 'domains/version'
5
+ require_relative 'domains/serializer'
6
+ require_relative 'domains/operation'
7
+ require_relative 'domains/operation/add'
8
+ require_relative 'domains/operation/find'
9
+ require_relative 'domains/operation/remove'
10
+ require_relative 'domains/operation/search'
11
+ require_relative 'domains/operation/transition'
12
+ require_relative 'domains/operation/update'
13
+
14
+ module LedgerSync
15
+ module Domains; end
16
+ end
metadata ADDED
@@ -0,0 +1,97 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ledger_sync-domains
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0.rc1
5
+ platform: ruby
6
+ authors:
7
+ - Jozef Vaclavik
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2021-09-03 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: ledger_sync
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 2.2.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 2.2.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: dotenv
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ description: Use LedgerSync Operations and Serializers for cross-domain communication.
42
+ email:
43
+ - jozef@dropbot.sh
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - ".github/workflows/main.yml"
49
+ - ".gitignore"
50
+ - ".rspec"
51
+ - ".rubocop.yml"
52
+ - CHANGELOG.md
53
+ - Gemfile
54
+ - Gemfile.lock
55
+ - LICENSE.txt
56
+ - README.md
57
+ - Rakefile
58
+ - bin/console
59
+ - bin/setup
60
+ - ledger_sync-domains.gemspec
61
+ - lib/ledger_sync/domains.rb
62
+ - lib/ledger_sync/domains/operation.rb
63
+ - lib/ledger_sync/domains/operation/add.rb
64
+ - lib/ledger_sync/domains/operation/find.rb
65
+ - lib/ledger_sync/domains/operation/remove.rb
66
+ - lib/ledger_sync/domains/operation/search.rb
67
+ - lib/ledger_sync/domains/operation/transition.rb
68
+ - lib/ledger_sync/domains/operation/update.rb
69
+ - lib/ledger_sync/domains/serializer.rb
70
+ - lib/ledger_sync/domains/version.rb
71
+ homepage: https://engineering.dropbot.sh
72
+ licenses:
73
+ - MIT
74
+ metadata:
75
+ homepage_uri: https://engineering.dropbot.sh
76
+ source_code_uri: https://github.com/LedgerSync/ledger_sync-domains
77
+ changelog_uri: https://github.com/LedgerSync/ledger_sync-domains/blob/main/CHANGELOG.md
78
+ post_install_message:
79
+ rdoc_options: []
80
+ require_paths:
81
+ - lib
82
+ required_ruby_version: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: 3.0.0
87
+ required_rubygems_version: !ruby/object:Gem::Requirement
88
+ requirements:
89
+ - - ">"
90
+ - !ruby/object:Gem::Version
91
+ version: 1.3.1
92
+ requirements: []
93
+ rubygems_version: 3.2.3
94
+ signing_key:
95
+ specification_version: 4
96
+ summary: LedgerSync for Domains/Engines
97
+ test_files: []