README.md in ledger_sync-domains-1.0.0.rc4 vs README.md in ledger_sync-domains-1.0.0.rc6
- old
+ new
@@ -4,11 +4,11 @@
LedgerSync has been developed to handle cross-service (API) communication in elegant way. Same aproach can be used for cross-domain communication.
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.
-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.
+Use `LedgerSync::Operation` to trigger actions from other domains and `LedgerSync::Serializer` to pass around serialized objects instead of ActiveRecord Models. ActiveRecord Models are compatible with LedgerSync Resources and can be serialized to OpenStruct objects automatically.
## Installation
Add this line to your application's Gemfile:
@@ -25,14 +25,46 @@
$ gem install ledger_sync-domains
## Usage
LedgerSync comes with 3 components.
-1. Resource
-2. Serializers
-3. Operations
+1. Registration
+2. Resource
+3. Serializers
+4. Operations
+### Register your engines
+
+`LedgerSync::Domains` requires little configuration so it can properly pick up domains and their namespace. While `LedgerSync::Domains` is not required to be used with Rails, it is the most expected usecase. THe problem with Rails (and possibly other codebases) is that the `MainApp` does not have a module namespace and referencing it from other domain operations is not as simple. There are two methods to help you register.
+
+#### Register your Main App
+
+`register_main_domain` creates a domain with name `:main` without any `base_module`. This way you can reference `MainApp` as `domain: :main` and serializers/operations will be picked up from there.
+
+```ruby
+# config/application.rb
+module DropBot
+ class Application < Rails::Application
+ LedgerSync::Domains.register_main_domain
+ # ...
+ end
+end
+```
+
+#### Register your Engine
+
+`register_domain` creates a domain with your own `name` and your own `base_module`. Lets say you place your engines under `engines` folder and you just created `billing` engine. To register it as a domain, update your `engine.rb` file. After that use it in your operations as `domain: :engine`. You can pass in string or symbol.
+```ruby
+# engines/billing/lib/billing/engine.rb
+module Billing
+ class Engine < ::Rails::Engine
+ isolate_namespace Billing
+ LedgerSync::Domains.register_domain(:billing, base_module: Billing)
+ end
+end
+```
+
### Resource/Model
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.
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.
@@ -184,11 +216,11 @@
def operate
return failure('Not found') if resource.nil?
if resource.update(active: false)
- success(serialize(resource: resource))
+ success(resource)
else
failure('Unable to deactivate')
end
end
@@ -206,33 +238,51 @@
end
end
end
end
```
-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.
+Successful result of an operation should be serialized resource(s). Operations automatically perform success result serialization for you. Any `ActiveRecord::Base` or `LedgerSync::Resource` object will be automatically serialized through matching serializer for the target domain. Any other value will remain untouched. Hashes and arrays are deep-serialized, so you can return hash or array including multiple ActiveRecord objects. Operation by itself doesn't know what domain triggered it, and therefore it requires target domain to be passed either as a module, string or symbol. Here is how that looks for the example above.
+
+
```ruby
-irb(main):001:0> op = Auth::Users::FindOperation.new(id: 1, limit: {}, serializer: Auth::Users::ClientSerializer.new)
+irb(main):001:0> op = Auth::Users::FindOperation.new(id: 1, limit: {}, domain: Client)
=> #<Auth::Users::FindOperation:0x0000555937876cf8 @serializer=#<Auth::Users::ClientSerializer:0x0000555937876dc0>, @deserializer=nil...
irb(main):002:0> op.perform
=> #<LedgerSync::Domains::Operation::OperationResult::Success:0x00005559372f9d70 @meta=nil, @value=#<OpenStruct email="test@ledger_sync.dev">>
irb(main):003:0> op.result.value
=> #<Auth::Users::ClientStruct email="test@ledger_sync.dev">
```
And thats it. Now you can use `LedgerSync` to define operations and have their results serialized against specific domain you are requesting it from.
+#### Internal operations
+
+Sometimes you want to create operation, that is not accessible from the rest of the app. The nature of ruby allows all defined classes be accessible from everywhere. To prevent execution of an operation from different domain, use `internal` flag when defining class.
+
+```ruby
+module Auth
+ module Users
+ class FindOperation < LedgerSync::Domains::Operation::Find
+ internal
+ end
+ end
+end
+```
+
+When performing an operation there are series of guards. First one validates if operation is allowed to be executed. That means either it is not flagged as internal operation, or target domain is same domain as module operation is defined in. If operation is not allowed to be executed, failure is returned with `LedgerSync::Domains::InternalOperationError` error.
+
### Cross-domain relationships
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.
#### Use of cross-domain relationships is bad
-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.
+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, domain: 'OtherEngine')`. This is a clean solution, but it will lead to N+1 queries.
#### Use of cross-domain relationships is fine
-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.
+If you try to get into the 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.
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.
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.