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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: '074681d0fda0240e14f6a8157716f872c6af54a8'
4
- data.tar.gz: 3405a3f3c338bda484efa7b9221601d24c2dd35e
3
+ metadata.gz: 04ddcc9225aeeabdd7e65aecad90097af5fd702f
4
+ data.tar.gz: 6814518d323632b5d65c0991ae84e286fd02678d
5
5
  SHA512:
6
- metadata.gz: d0f049e84677b12d58d4e53d7781ae80bba9e6859c9d25f46f10e4e7c25c3eea0f13cd1c6ca436a20e3ad910f3b9d31b3c21c8a6e47e07a8382fab0268524cd1
7
- data.tar.gz: 9df6468ce430ea9c640ad088f8b0e1e37ff69379b30d9002bca705bd5c3b989648a221b2d8d95939e50b94168d8217adbfb8b15b845546829ea0abeb68af68e6
6
+ metadata.gz: 9ee6f6f6254eb3c615e473c3ff6ba000df3d5538a8d31849d5d9161718fec3d79c9ba47ff9620658dbd3a7726d604fd858eb76f58f6f697640b35e9b1db246b2
7
+ data.tar.gz: 359b11969cd277940be18a94c7709acf514808b8816aa3172a2474932ad5f03c0ffcdb26e613e85be1acca685bc5a540e3e3db2a0e5948bb7ada94a519c09fcd
data/CHANGELOG.md CHANGED
@@ -1,3 +1,7 @@
1
+ ## [0.5.0] - 2017-11-6
2
+ ### Changed
3
+ - Change base class for `Pathway::Error` from `StandardError` to `Object`
4
+
1
5
  ## [0.4.0] - 2017-10-31
2
6
  ### Changed
3
7
  - Renamed `:authorization` plugin to `:simple_auth`
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 allows you to encapsulate your app's business logic into operation objects (also known as application services on the DDD lingo).
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
- ## Introduction
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 in the following sections.
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
- ### Core API and concepts
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 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).
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.
@@ -1,3 +1,3 @@
1
1
  module Pathway
2
- VERSION = '0.4.0'
2
+ VERSION = '0.5.0'
3
3
  end
data/lib/pathway.rb CHANGED
@@ -27,7 +27,7 @@ module Pathway
27
27
  end
28
28
  end
29
29
 
30
- class Error < StandardError
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
- def result_key
99
- self.class.result_key
100
- end
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(with_seq, &bl)
155
+ def sequence(steps_wrapper, &steps)
159
156
  @result.then do |state|
160
- seq = -> { @result = dup.run(&bl) }
161
- _callable(with_seq).call(seq, state)
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, &bl)
162
+ def guard(cond, &steps)
166
163
  cond = _callable(cond)
167
164
  sequence(-> seq, state {
168
165
  seq.call if cond.call(state)
169
- }, &bl)
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 bussines logic in simple steps.}
13
- spec.description = %q{Define your bussines logic in simple steps.}
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.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-10-31 00:00:00.000000000 Z
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 bussines logic in simple steps.
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 bussines logic in simple steps.
213
+ summary: Define your business logic in simple steps.
214
214
  test_files: []