pathway 0.4.0 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +4 -0
- data/README.md +177 -18
- data/lib/pathway/version.rb +1 -1
- data/lib/pathway.rb +10 -13
- data/pathway.gemspec +2 -2
- metadata +4 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 04ddcc9225aeeabdd7e65aecad90097af5fd702f
|
4
|
+
data.tar.gz: 6814518d323632b5d65c0991ae84e286fd02678d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9ee6f6f6254eb3c615e473c3ff6ba000df3d5538a8d31849d5d9161718fec3d79c9ba47ff9620658dbd3a7726d604fd858eb76f58f6f697640b35e9b1db246b2
|
7
|
+
data.tar.gz: 359b11969cd277940be18a94c7709acf514808b8816aa3172a2474932ad5f03c0ffcdb26e613e85be1acca685bc5a540e3e3db2a0e5948bb7ada94a519c09fcd
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -4,35 +4,22 @@
|
|
4
4
|
[![CircleCI](https://circleci.com/gh/pabloh/pathway/tree/master.svg?style=shield)](https://circleci.com/gh/pabloh/pathway/tree/master)
|
5
5
|
[![Coverage Status](https://coveralls.io/repos/github/pabloh/pathway/badge.svg?branch=master)](https://coveralls.io/github/pabloh/pathway?branch=master)
|
6
6
|
|
7
|
-
Pathway
|
7
|
+
Pathway encapsulates your business logic into simple operation objects (AKA application services on the [DDD](https://en.wikipedia.org/wiki/Domain-driven_design) lingo).
|
8
8
|
|
9
9
|
## Installation
|
10
10
|
|
11
|
-
Add this line to your application's Gemfile:
|
12
|
-
|
13
|
-
```ruby
|
14
|
-
gem 'pathway'
|
15
|
-
```
|
16
|
-
|
17
|
-
And then execute:
|
18
|
-
|
19
|
-
$ bundle
|
20
|
-
|
21
|
-
Or install it yourself as:
|
22
|
-
|
23
11
|
$ gem install pathway
|
24
12
|
|
25
|
-
##
|
13
|
+
## Description
|
26
14
|
|
27
15
|
Pathway helps you separate your business logic from the rest of your application; regardless if is an HTTP backend, a background processing daemon, etc.
|
28
|
-
The main concept Pathway relies upon to build domain logic modules is the operation, this important concept will be explained in detail
|
29
|
-
|
16
|
+
The main concept Pathway relies upon to build domain logic modules is the operation, this important concept will be explained in detail the following sections.
|
30
17
|
|
31
18
|
Pathway also aims to be easy to use, stay lightweight and extensible (by the use of plugins), avoid unnecessary dependencies, keep the core classes clean from monkey patching and help yielding an organized and uniform codebase.
|
32
19
|
|
33
20
|
## Usage
|
34
21
|
|
35
|
-
###
|
22
|
+
### Main concepts and API
|
36
23
|
|
37
24
|
As mentioned earlier the operation is a crucial concept Pathway leverages upon. Operations not only structure your code (using steps as will be explained later) but also express meaningful business actions. Operations can be thought as use cases too: they represent an activity -to be perform by an actor interacting with the system- which should be understandable by anyone familiar with the business regardless of their technical expertise.
|
38
25
|
|
@@ -492,7 +479,7 @@ end
|
|
492
479
|
```
|
493
480
|
|
494
481
|
As you can see above you can also customize the search field (`:search_by`) and indicate if you want to override the result key (`:set_result_key`) when calling to `model`.
|
495
|
-
These two options aren't mandatory, and by default
|
482
|
+
These two options aren't mandatory, and by default Pathway will set the search field to the class model primary key, and override the result key to a snake cased version of the model name (ignoring namespaces if contained inside a class or module).
|
496
483
|
|
497
484
|
Let's now take a look at the provided extensions:
|
498
485
|
|
@@ -583,10 +570,182 @@ As you can see is almost identical as the previous example only that this time y
|
|
583
570
|
|
584
571
|
### Plugin architecture
|
585
572
|
|
573
|
+
Going a bit deeper now, we'll explain how to implement your own plugins. As was mention before `pathway` follows a very similar approach to the [Roda](http://roda.jeremyevans.net/) or [Sequel](http://sequel.jeremyevans.net/) plugin systems, which is reflected on its implementation.
|
574
|
+
|
575
|
+
Each plugin must be defined in a file placed within the `pathway/plugins/` directory of your gem or application, so `pathway` can require the file; and must be implemented as a module inside the `Pathway::Plugins` namespace module. Inside your plugin module, three extra modules can be define to extend the operation API `ClassMethods`, `InstanceMethods` and `DSLMethods`; plus a class method `apply` for plugin initialization when needed.
|
576
|
+
|
577
|
+
If you are familiar with the aforementioned plugin mechanism (or other as well), the function of each module is probably starting to feel evident: `ClassMethods` will be used to extend the operation class, so any class methods should be defined here; `InstanceMethods` will be included on the operation so all the instance methods you need to add to the operation should be here, this include every custom step you need to add; and finally `DSLMethods` will be included on the `Operation::DSL` class, which holds all the DSL methods like `step` or `set`.
|
578
|
+
The `apply` method will simply be run whenever the plugin is included, taking the operation class on the first argument and all then arguments the call to `plugin` received (excluding the plugin name).
|
579
|
+
|
580
|
+
Lets explain with more detail using a complete example:
|
581
|
+
|
582
|
+
```ruby
|
583
|
+
# lib/pathway/plugins/active_record.rb
|
584
|
+
|
585
|
+
module Pathway
|
586
|
+
module Plugins
|
587
|
+
module ActiveRecord
|
588
|
+
module ClassMethods
|
589
|
+
attr_accessor :model, :pk
|
590
|
+
|
591
|
+
def inherited(subclass)
|
592
|
+
super
|
593
|
+
subclass.model = self.model
|
594
|
+
subclass.pk = self.pk
|
595
|
+
end
|
596
|
+
end
|
597
|
+
|
598
|
+
module InstanceMethods
|
599
|
+
delegate :model, :pk, to: :class
|
600
|
+
|
601
|
+
# This plugin will conflict will :sequel_models so you mustn't load them in the same operation
|
602
|
+
def fetch_model(state, column: pk)
|
603
|
+
current_pk = state[:input][column]
|
604
|
+
result = model.first(column => current_pk)
|
605
|
+
|
606
|
+
if result
|
607
|
+
state.update(result_key => result)
|
608
|
+
else
|
609
|
+
error(:not_found)
|
610
|
+
end
|
611
|
+
end
|
612
|
+
end
|
613
|
+
|
614
|
+
module DSLMethods
|
615
|
+
# This method also conflicts with :sequel_models, so don't use them as once.
|
616
|
+
def transaction(&steps)
|
617
|
+
transactional_seq = -> seq, _state do
|
618
|
+
ActiveRecord::Base.transaction do
|
619
|
+
seq.call
|
620
|
+
end
|
621
|
+
end
|
622
|
+
|
623
|
+
sequence(transactional_seq, &steps)
|
624
|
+
end
|
625
|
+
end
|
626
|
+
|
627
|
+
def self.apply(operation, model: nil, pk: nil)
|
628
|
+
operation.model = model
|
629
|
+
opertaion.pk = pk || model&.primary_key
|
630
|
+
end
|
631
|
+
end
|
632
|
+
end
|
633
|
+
end
|
634
|
+
```
|
635
|
+
|
636
|
+
The code above implements a plugin to provide basic interaction with the [ActiveRecord](http://guides.rubyonrails.org/active_record_basics.html) gem.
|
637
|
+
Even though is a very simply plugin, it shows all the essentials to develop more complex plugin.
|
638
|
+
|
639
|
+
First as is pointed out in the code, some of the methods implemented here (`fetch_model` and `transmission`) collide with methods defined for the `:sequel_models`, so as a consequence these two plugin's are not compatible with each other an cannot be activated at the same time on the same operation (although you can still do it for different operation within the same application).
|
640
|
+
You must be mindful about colliding method names when mixing plugins, since `Pathway` can't book keep compatibility among every plugin that exists of will ever exist.
|
641
|
+
Is a good practice to document known incompatibilities on the plugin definition itself when they are known.
|
642
|
+
|
643
|
+
The whole plugin is completely defined within the `ActiveRecord` module inside the `Pathway::Plugins` namespace, also the file is placed at the load path in `pathway/plugin/active_record.rb` (assuming `lib/` is listed in `$LOAD_PATH`). This will ensure, when calling `plugin :active_record` inside an operation, the correct file will be loaded and the correct plugin module will be applied to the current operation.
|
644
|
+
|
645
|
+
Moving on to the `ClassMethods` module, we can see the accessors `model` and `pk` are defined for the operation's class to allow configuration.
|
646
|
+
Also, the `inherited` hook is defined, this will simply be another class method at the operation and as such will be executed normally when the operation class is inherited. In our implementation we just call to `super` (which is extremely important since other modules or parent classes could be using this hook), and then copy the `model` and `pk` options from the parent to the subclass in order to propagate the configuration downwards.
|
647
|
+
|
648
|
+
At the end of the `ActiveRecord` module definition you can see the `apply` method. It will receive the operation class and the parameters passed when the `plugin` method is invoked. This method is usually used for loading dependencies of just setting up config parameters as we do in this particular example.
|
649
|
+
|
650
|
+
`InstanceMethods` first defines a few delegator methods to the class itself to use later.
|
651
|
+
Then the `fetch_model` step is defined (remember steps are but operation instance methods). Its first parameter is the state itself, as in the other steps we've seen before, and the remaining parameters are the options we can pass when calling `step :fetch_model` (mind you, this is also valid for steps defined in operations classes). Here we only take a single keyword argument: `column: pk`, with a default value; this will allow us to change the look up column when using the step, and is the only parameter we can use, passing other keyword arguments or extra positional parameters when invoking the step will raise errors.
|
652
|
+
|
653
|
+
Let's now examine the `fetch_model` step body, is not really that much different from other steps, here we extract the model primary key from `state[:input][column]` and use it to perform a search. If nothing is found an error is returned, otherwise the state is updated on the result key to hold the model we just fetched from the DB.
|
654
|
+
|
655
|
+
We finally see a `DSLMethods` module defined to extend the process DSL.
|
656
|
+
For this plugin we'll define a way to group steps within an `ActiveRecord` transaction, much in the same way the `:sequel_models` plugin already does for `Sequel`.
|
657
|
+
To this end we define a `transaction` method to expect a steps block and pass it down to the `sequence` helper below which expects a callable (like a `Proc`) and a step list block. As you can see the lambda we pass on the first parameter is the one that makes sure the steps are being run inside a transaction.
|
658
|
+
|
659
|
+
The `sequence` method is a low level tool available to help extending the process DSL and it may seem a bit daunting at first glance but it usage is quite simple, the block is just a step list like the ones we find inside the `process` call; and the parameter is a callable (usually a lambda), that will take 2 arguments, an object from which we can run the step list by invoking `call` (and is the only thing it can do), and the current state. From here we can examine the state and decide upon whether to run the steps, how many times (if any) or run some code before and/or after doing so, like what we need to do in our example to surround the steps within a DB transaction.
|
660
|
+
|
586
661
|
### Testing tools
|
662
|
+
|
663
|
+
As of right now only `rspec` is supported, that is, you can obviously test your operations with any framework you want, but all the provided matchers are designed for `rspec`.
|
664
|
+
|
587
665
|
#### Rspec config
|
666
|
+
|
667
|
+
In order to load Pathway's operation matchers you must add the following line to your `spec_helper.rb` file, after loading `rspec`:
|
668
|
+
|
669
|
+
```ruby
|
670
|
+
require 'pathway/rspec'
|
671
|
+
```
|
672
|
+
|
588
673
|
#### Rspec matchers
|
589
674
|
|
675
|
+
Pathway provide a few matchers in order to tests your operation easier.
|
676
|
+
Let's go through a full example and break it up in the following subsections:
|
677
|
+
|
678
|
+
```ruby
|
679
|
+
# create_nugget.rb
|
680
|
+
|
681
|
+
class CreateNugget < Pathway::Operation
|
682
|
+
plugin :dry_validation
|
683
|
+
|
684
|
+
form do
|
685
|
+
required(:owner).filled(:str?)
|
686
|
+
required(:price).filled(:int?)
|
687
|
+
optional(:disabled).maybe(:bool?)
|
688
|
+
end
|
689
|
+
|
690
|
+
process do
|
691
|
+
step :validate
|
692
|
+
set :create_nugget
|
693
|
+
end
|
694
|
+
|
695
|
+
def create_nugget(params:,**)
|
696
|
+
Nugget.create(params)
|
697
|
+
end
|
698
|
+
end
|
699
|
+
|
700
|
+
|
701
|
+
# create_nugget_spec.rb
|
702
|
+
|
703
|
+
describe CreateNugget do
|
704
|
+
describe '#call' do
|
705
|
+
subject(:operation) { CreateNugget.new }
|
706
|
+
|
707
|
+
context 'when the input is valid' do
|
708
|
+
let(:input) { owner: 'John Smith', value: '11230' }
|
709
|
+
|
710
|
+
it { is_expected.to succeed_on(input).returning(an_instace_of(Nugget)) }
|
711
|
+
end
|
712
|
+
|
713
|
+
context 'when the input is invalid' do
|
714
|
+
let(:input) { owner: '', value: '11230' }
|
715
|
+
|
716
|
+
it { is_expected.to fail_on(input).
|
717
|
+
with_type(:validation).
|
718
|
+
message('Is not valid').
|
719
|
+
and_details(owner: ['must be present']) }
|
720
|
+
end
|
721
|
+
end
|
722
|
+
|
723
|
+
describe '.form' do
|
724
|
+
subject(:form) { CreateNugget.form }
|
725
|
+
|
726
|
+
it { is_expected.to require_fields(:owner, :price) }
|
727
|
+
it { is_expected.to accept_optional_field(:disabled) }
|
728
|
+
end
|
729
|
+
end
|
730
|
+
```
|
731
|
+
|
732
|
+
##### `succeed_on` matcher
|
733
|
+
|
734
|
+
This first matcher works on the operation itself and that's why we could set `subject` with the operation instance and use `is_expected.to succeed_on(...)` on the example.
|
735
|
+
The assertion it performs is simply is that the operation was successful, also you can optionally chain `returning(...)` if you want to test the returning value, this method allows nesting matchers as is the case in the example.
|
736
|
+
|
737
|
+
##### `fail_on` matcher
|
738
|
+
|
739
|
+
This second matcher is analog to `succeed_on` but it asserts that operation execution was a failure instead. Also if you return an error object, and you need to, you can assert the error type using the `type` chain method (aliased as `and_type` and `with_type`); the error message (`and_message`, `with_message` or `message`); and the error details (`and_details`, `with_details` or `details`). Mind you, the chain methods for the message and details accept nested matchers while the `type` chain can only test by equality.
|
740
|
+
|
741
|
+
##### form matchers
|
742
|
+
|
743
|
+
Finally we can see that we are also testing the operation's form, implemented here with the `dry-validation` gem.
|
744
|
+
|
745
|
+
Two more matchers are provided when we use this gem: `require_fields` (aliased `require_field`) to test when form is expected to define a required set of fields, and `accept_optional_fields` (aliased `accept_optional_field`) to test when a form must define certain set of optional fields.
|
746
|
+
|
747
|
+
These matchers are only useful when using `dry-validation` and will very likely be extracted to its own gem in the future.
|
748
|
+
|
590
749
|
## Development
|
591
750
|
|
592
751
|
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.
|
data/lib/pathway/version.rb
CHANGED
data/lib/pathway.rb
CHANGED
@@ -27,7 +27,7 @@ module Pathway
|
|
27
27
|
end
|
28
28
|
end
|
29
29
|
|
30
|
-
class Error
|
30
|
+
class Error
|
31
31
|
attr_reader :type, :message, :details
|
32
32
|
singleton_class.send :attr_accessor, :default_messages
|
33
33
|
|
@@ -95,18 +95,15 @@ module Pathway
|
|
95
95
|
module InstanceMethods
|
96
96
|
extend Forwardable
|
97
97
|
|
98
|
-
|
99
|
-
|
100
|
-
|
98
|
+
delegate :result_key => 'self.class'
|
99
|
+
delegate %i[result success failure] => Result
|
100
|
+
|
101
|
+
alias :wrap :result
|
101
102
|
|
102
103
|
def call(*)
|
103
104
|
fail "must implement at subclass"
|
104
105
|
end
|
105
106
|
|
106
|
-
delegate %i[result success failure] => Result
|
107
|
-
|
108
|
-
alias :wrap :result
|
109
|
-
|
110
107
|
def error(type, message: nil, details: nil)
|
111
108
|
failure Error.new(type: type, message: message, details: details)
|
112
109
|
end
|
@@ -155,18 +152,18 @@ module Pathway
|
|
155
152
|
@result = @result.then(bl)
|
156
153
|
end
|
157
154
|
|
158
|
-
def sequence(
|
155
|
+
def sequence(steps_wrapper, &steps)
|
159
156
|
@result.then do |state|
|
160
|
-
seq = -> { @result = dup.run(&
|
161
|
-
_callable(
|
157
|
+
seq = -> { @result = dup.run(&steps) }
|
158
|
+
_callable(steps_wrapper).call(seq, state)
|
162
159
|
end
|
163
160
|
end
|
164
161
|
|
165
|
-
def guard(cond, &
|
162
|
+
def guard(cond, &steps)
|
166
163
|
cond = _callable(cond)
|
167
164
|
sequence(-> seq, state {
|
168
165
|
seq.call if cond.call(state)
|
169
|
-
}, &
|
166
|
+
}, &steps)
|
170
167
|
end
|
171
168
|
|
172
169
|
private
|
data/pathway.gemspec
CHANGED
@@ -9,8 +9,8 @@ Gem::Specification.new do |spec|
|
|
9
9
|
spec.authors = ["Pablo Herrero"]
|
10
10
|
spec.email = ["pablodherrero@gmail.com"]
|
11
11
|
|
12
|
-
spec.summary = %q{Define your
|
13
|
-
spec.description = %q{Define your
|
12
|
+
spec.summary = %q{Define your business logic in simple steps.}
|
13
|
+
spec.description = %q{Define your business logic in simple steps.}
|
14
14
|
#spec.homepage = "TODO: Put your gem's website or public repo URL here."
|
15
15
|
spec.license = "MIT"
|
16
16
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: pathway
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Pablo Herrero
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-
|
11
|
+
date: 2017-11-07 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: inflecto
|
@@ -150,7 +150,7 @@ dependencies:
|
|
150
150
|
- - ">="
|
151
151
|
- !ruby/object:Gem::Version
|
152
152
|
version: '0'
|
153
|
-
description: Define your
|
153
|
+
description: Define your business logic in simple steps.
|
154
154
|
email:
|
155
155
|
- pablodherrero@gmail.com
|
156
156
|
executables: []
|
@@ -210,5 +210,5 @@ rubyforge_project:
|
|
210
210
|
rubygems_version: 2.6.9
|
211
211
|
signing_key:
|
212
212
|
specification_version: 4
|
213
|
-
summary: Define your
|
213
|
+
summary: Define your business logic in simple steps.
|
214
214
|
test_files: []
|