pathway 0.0.9

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
+ SHA1:
3
+ metadata.gz: f08df7daa90f1675a099551783c3bba0cb8371f8
4
+ data.tar.gz: 313773af0d379310bd55d5bbfc536ce51a91b70a
5
+ SHA512:
6
+ metadata.gz: 0c9c704a68d3f24756cf8b43a6049663bcecaf8055b9c6794c227fad096770a346a3d7d6b00b1da53013d8879fe3693ab469b95298ca167c5d550f46f56c7b4f
7
+ data.tar.gz: fbf219d58c759cadb6753317b828497e7cba6b133edda435b95c33a0d72fdfca9e3c3c61c52b4ddaa25dc3ab65d194181c1a70998602ed8414eb927c7fa6ed47
data/.gitignore ADDED
@@ -0,0 +1,12 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+
11
+ # rspec failure tracking
12
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ ruby '2.4.0'
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in pathway.gemspec
6
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2017 Pablo Herrero
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,42 @@
1
+ # Pathway
2
+
3
+ [![CircleCI](https://circleci.com/gh/pabloh/pathway/tree/master.svg?style=shield)](https://circleci.com/gh/pabloh/pathway/tree/master)
4
+ [![Coverage Status](https://coveralls.io/repos/github/pabloh/pathway/badge.svg?branch=master)](https://coveralls.io/github/pabloh/pathway?branch=master)
5
+
6
+ Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/pathway`. To experiment with that code, run `bin/console` for an interactive prompt.
7
+
8
+ TODO: Delete this and the text above, and describe your gem
9
+
10
+ ## Installation
11
+
12
+ Add this line to your application's Gemfile:
13
+
14
+ ```ruby
15
+ gem 'pathway'
16
+ ```
17
+
18
+ And then execute:
19
+
20
+ $ bundle
21
+
22
+ Or install it yourself as:
23
+
24
+ $ gem install pathway
25
+
26
+ ## Usage
27
+
28
+ TODO: Write usage instructions here
29
+
30
+ ## Development
31
+
32
+ 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.
33
+
34
+ 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 tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
35
+
36
+ ## Contributing
37
+
38
+ Bug reports and pull requests are welcome on GitHub at https://github.com/pabloh/pathway.
39
+
40
+ ## License
41
+
42
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/bin/byebug ADDED
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+ #
4
+ # This file was generated by Bundler.
5
+ #
6
+ # The application 'byebug' is installed as part of a gem, and
7
+ # this file is here to facilitate running it.
8
+ #
9
+
10
+ require "pathname"
11
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
12
+ Pathname.new(__FILE__).realpath)
13
+
14
+ require "rubygems"
15
+ require "bundler/setup"
16
+
17
+ load Gem.bin_path("byebug", "byebug")
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "pathway"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "pry"
14
+ Pry.start(Pathway)
data/bin/rake ADDED
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+ #
4
+ # This file was generated by Bundler.
5
+ #
6
+ # The application 'rake' is installed as part of a gem, and
7
+ # this file is here to facilitate running it.
8
+ #
9
+
10
+ require "pathname"
11
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
12
+ Pathname.new(__FILE__).realpath)
13
+
14
+ require "rubygems"
15
+ require "bundler/setup"
16
+
17
+ load Gem.bin_path("rake", "rake")
data/bin/rspec ADDED
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+ #
4
+ # This file was generated by Bundler.
5
+ #
6
+ # The application 'rspec' is installed as part of a gem, and
7
+ # this file is here to facilitate running it.
8
+ #
9
+
10
+ require "pathname"
11
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
12
+ Pathname.new(__FILE__).realpath)
13
+
14
+ require "rubygems"
15
+ require "bundler/setup"
16
+
17
+ load Gem.bin_path("rspec-core", "rspec")
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
data/lib/pathway.rb ADDED
@@ -0,0 +1,192 @@
1
+ require 'forwardable'
2
+ require 'inflecto'
3
+ require 'contextualizer'
4
+ require 'pathway/version'
5
+ require 'pathway/result'
6
+
7
+ module Pathway
8
+ class Operation
9
+ def self.plugin(name)
10
+ require "pathway/plugins/#{Inflecto.underscore(name)}" if name.is_a?(Symbol)
11
+
12
+ plugin = name.is_a?(Module) ? name : Plugins.const_get(Inflecto.camelize(name))
13
+
14
+ self.extend plugin::ClassMethods if plugin.const_defined? :ClassMethods
15
+ self.include plugin::InstanceMethods if plugin.const_defined? :InstanceMethods
16
+ # TODO: Separate DSL per operation hierarchy
17
+ DSL.include plugin::DSLMethods if plugin.const_defined? :DSLMethods
18
+ plugin.apply(self) if plugin.respond_to?(:apply)
19
+ end
20
+ end
21
+
22
+ class DSL
23
+ end
24
+
25
+ class Error < StandardError
26
+ attr_reader :type, :message, :details
27
+ singleton_class.send :attr_accessor, :default_messages
28
+
29
+ @default_messages = {}
30
+
31
+ alias :error_type :type
32
+ alias :error_message :message
33
+ alias :errors :details
34
+
35
+ def initialize(type:, message: nil, details: nil)
36
+ @type = type.to_sym
37
+ @message = message || default_message_for(type)
38
+ @details = details || {}
39
+ end
40
+
41
+ private
42
+
43
+ def default_message_for(type)
44
+ self.class.default_messages[type] || Inflecto.humanize(type)
45
+ end
46
+ end
47
+
48
+ class State
49
+ extend Forwardable
50
+
51
+ def initialize(operation, values = {})
52
+ @hash = operation.context.merge(values)
53
+ @result_key = operation.result_key
54
+ end
55
+
56
+ delegate %i([] []= fetch store include?) => :@hash
57
+
58
+ def update(kargs)
59
+ @hash.update(kargs)
60
+ self
61
+ end
62
+
63
+ def result
64
+ @hash[@result_key]
65
+ end
66
+
67
+ def to_hash
68
+ @hash
69
+ end
70
+
71
+ alias :to_h :to_hash
72
+ end
73
+
74
+ module Plugins
75
+ module Base
76
+ module ClassMethods
77
+ attr_accessor :result_key
78
+
79
+ def process(&bl)
80
+ define_method(:call) do |input|
81
+ DSL.new(self, input).run(&bl)
82
+ .then { |state| state[result_key] }
83
+ end
84
+ end
85
+
86
+ alias :result_at :result_key=
87
+
88
+ def inherited(subclass)
89
+ super
90
+ subclass.result_key = result_key
91
+ end
92
+ end
93
+
94
+ module InstanceMethods
95
+ extend Forwardable
96
+
97
+ def result_key
98
+ self.class.result_key
99
+ end
100
+
101
+ def call(*)
102
+ fail "must implement at subclass"
103
+ end
104
+
105
+ delegate %i[result success failure] => Result
106
+
107
+ alias :wrap :result
108
+
109
+ def error(type, message: nil, details: nil)
110
+ failure Error.new(type: type, message: message, details: details)
111
+ end
112
+
113
+ def wrap_if_present(value, type: :not_found, message: nil, details: [])
114
+ value.nil? ? error(type, message: message, details: details) : success(value)
115
+ end
116
+ end
117
+
118
+ def self.apply(klass)
119
+ klass.extend Contextualizer
120
+ klass.result_key = :value
121
+ end
122
+
123
+ module DSLMethods
124
+ def initialize(operation, input)
125
+ @result = wrap(State.new(operation, input: input))
126
+ @operation = operation
127
+ end
128
+
129
+ def run(&bl)
130
+ instance_eval(&bl)
131
+ @result
132
+ end
133
+
134
+ # Execute step and preserve the former state
135
+ def step(callable, *args)
136
+ bl = _callable(callable)
137
+
138
+ @result = @result.tee { |state| bl.call(state, *args) }
139
+ end
140
+
141
+ # Execute step and modify the former state setting the key
142
+ def set(callable, *args, to: @operation.result_key)
143
+ bl = _callable(callable)
144
+
145
+ @result = @result.then do |state|
146
+ wrap(bl.call(state, *args))
147
+ .then { |value| state.update(to => value) }
148
+ end
149
+ end
150
+
151
+ # Execute step and replace the current state completely
152
+ def map(callable)
153
+ bl = _callable(callable)
154
+ @result = @result.then(bl)
155
+ end
156
+
157
+ def sequence(with_seq, &bl)
158
+ @result.then do |state|
159
+ seq = -> { @result = dup.run(&bl) }
160
+ _callable(with_seq).call(seq, state)
161
+ end
162
+ end
163
+
164
+ def guard(cond, &bl)
165
+ cond = _callable(cond)
166
+ sequence(-> seq, state {
167
+ seq.call if cond.call(state)
168
+ }, &bl)
169
+ end
170
+
171
+ private
172
+
173
+ def wrap(obj)
174
+ Result.result(obj)
175
+ end
176
+
177
+ def _callable(callable)
178
+ case callable
179
+ when Proc
180
+ -> *args { @operation.instance_exec(*args, &callable) }
181
+ when Symbol
182
+ -> *args { @operation.send(callable, *args) }
183
+ else
184
+ callable
185
+ end
186
+ end
187
+ end
188
+ end
189
+ end
190
+
191
+ Operation.plugin Plugins::Base
192
+ end
@@ -0,0 +1,26 @@
1
+ module Pathway
2
+ module Plugins
3
+ module Authorization
4
+ module ClassMethods
5
+ def authorization(&block)
6
+ define_method(:authorized?, &block)
7
+ end
8
+ end
9
+
10
+ module InstanceMethods
11
+ def authorize(state, using: nil)
12
+ authorize_with(state[using || result_key]).then { state }
13
+ end
14
+
15
+ def authorize_with(*objs)
16
+ objs = objs.first if objs.size <= 1
17
+ authorized?(*objs) ? wrap(objs) : error(:forbidden)
18
+ end
19
+
20
+ def authorized?(*_)
21
+ true
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,73 @@
1
+ require 'dry/validation'
2
+
3
+ module Pathway
4
+ module Plugins
5
+ module DryValidation
6
+ module ClassMethods
7
+ attr_reader :form_class
8
+
9
+ def form(base = nil, **opts, &block)
10
+ if block_given?
11
+ base ||= _base_form
12
+ self.form_class = Dry::Validation.Form(_form_class(base), _form_opts(opts), &block)
13
+ elsif base
14
+ self.form_class = _form_class(base)
15
+ else
16
+ raise ArgumentError, 'Either a form class or a block must be provided'
17
+ end
18
+ end
19
+
20
+ def form_class= klass
21
+ @builded_form = klass.options.empty? ? klass.new : nil
22
+ @form_class = klass
23
+ end
24
+
25
+ def build_form(opts = {})
26
+ @builded_form || form_class.new(opts)
27
+ end
28
+
29
+ def inherited(subclass)
30
+ super
31
+ subclass.form_class = form_class
32
+ end
33
+
34
+ private
35
+
36
+ def _base_form
37
+ superclass.respond_to?(:form_class) ? superclass.form_class : Dry::Validation::Schema::Form
38
+ end
39
+
40
+ def _form_class(form)
41
+ form.is_a?(Class) ? form : form.class
42
+ end
43
+
44
+ def _form_opts(opts = {})
45
+ opts.merge(build: false)
46
+ end
47
+ end
48
+
49
+ module InstanceMethods
50
+ extend Forwardable
51
+
52
+ delegate :build_form => 'self.class'
53
+ alias :form :build_form
54
+
55
+ def validate(state, with: [])
56
+ opts = Array(with).map { |key| [key, state[key]] }.to_h
57
+ validate_with(state[:input], opts)
58
+ .then { |params| state.update(params: params) }
59
+ end
60
+
61
+ def validate_with(params, opts = {})
62
+ val = form(opts).call(params)
63
+
64
+ val.success? ? wrap(val.output) : error(:validation, details: val.messages)
65
+ end
66
+ end
67
+
68
+ def self.apply(operation)
69
+ operation.form_class = Dry::Validation::Schema::Form
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,14 @@
1
+ require 'pathway/responder'
2
+
3
+ module Pathway
4
+ module Plugins
5
+ module Responder
6
+ module ClassMethods
7
+ def call(ctx, *params, &bl)
8
+ result = new(ctx).call(*params)
9
+ block_given? ? Pathway::Responder.respond(result, &bl) : result
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,57 @@
1
+ require 'sequel/model'
2
+
3
+ module Pathway
4
+ module Plugins
5
+ module SequelModels
6
+ module DSLMethods
7
+ def transaction(&bl)
8
+ sequence(-> seq, _ {
9
+ db.transaction(savepoint: true) do
10
+ raise Sequel::Rollback if seq.call.failure?
11
+ end
12
+ }, &bl)
13
+ end
14
+ end
15
+
16
+ module InstanceMethods
17
+ module Finder
18
+ def self.[](model_class, by: :id)
19
+ Module.new do
20
+ include InstanceMethods
21
+
22
+ define_singleton_method :included do |klass|
23
+ klass.class_eval do
24
+ result_at Inflecto.underscore(model_class.name.split('::').last).to_sym
25
+
26
+ define_method(:model_class) { model_class }
27
+ define_method(:field) { by }
28
+ define_method(:db) { model_class.db }
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+
35
+ extend Forwardable
36
+ delegate %i[model_class db field] => 'self.class'
37
+
38
+ def fetch_model(state)
39
+ fetch_model_with(state[:input])
40
+ .then { |model| state.update(result_key => model) }
41
+ end
42
+
43
+ def fetch_model_with(params)
44
+ wrap_if_present(find_model_with(params[field]))
45
+ end
46
+
47
+ def build_model_with(params)
48
+ wrap(model_class.new(params))
49
+ end
50
+
51
+ def find_model_with(key)
52
+ model_class.first(field => key)
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,35 @@
1
+ module Pathway
2
+ class Responder
3
+ def self.respond(result, &bl)
4
+ r = new(result, &bl)
5
+ r.respond
6
+ end
7
+
8
+ def initialize(result, &bl)
9
+ @result, @context, @fails = result, bl.binding.receiver, {}
10
+ instance_eval(&bl)
11
+ end
12
+
13
+ def success(&bl)
14
+ @ok = bl
15
+ end
16
+
17
+ def failure(type = nil, &bl)
18
+ if type.nil?
19
+ @fail_default = bl
20
+ else
21
+ @fails[type] = bl
22
+ end
23
+ end
24
+
25
+ def respond
26
+ if @result.success?
27
+ @context.instance_exec(@result.value, &@ok)
28
+ elsif Error === @result.error && fail_block = @fails[@result.error.type]
29
+ @context.instance_exec(@result.error, &fail_block)
30
+ else
31
+ @context.instance_exec(@result.error, &@fail_default)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,61 @@
1
+ module Pathway
2
+ class Result
3
+ extend Forwardable
4
+ attr_reader :value, :error
5
+
6
+ class Success < Result
7
+ def initialize(value)
8
+ @value = value
9
+ end
10
+
11
+ def success?
12
+ true
13
+ end
14
+
15
+ def then(bl=nil)
16
+ result(block_given? ? yield(value): bl.call(value))
17
+ end
18
+
19
+ def tee(bl=nil, &block)
20
+ follow = self.then(bl, &block)
21
+ follow.failure? ? follow : self
22
+ end
23
+ end
24
+
25
+ class Failure < Result
26
+ def initialize(error)
27
+ @error = error
28
+ end
29
+
30
+ def success?
31
+ false
32
+ end
33
+
34
+ def then(_=nil)
35
+ self
36
+ end
37
+
38
+ def tee(_=nil)
39
+ self
40
+ end
41
+ end
42
+
43
+ def failure?
44
+ !success?
45
+ end
46
+
47
+ def self.success(value)
48
+ Success.new(value)
49
+ end
50
+
51
+ def self.failure(error)
52
+ Failure.new(error)
53
+ end
54
+
55
+ def self.result(object)
56
+ object.is_a?(Result) ? object : success(object)
57
+ end
58
+
59
+ delegate :result => 'self.class'
60
+ end
61
+ end
@@ -0,0 +1 @@
1
+ require 'pathway/rspec/matchers'
@@ -0,0 +1,4 @@
1
+ require 'pathway/rspec/matchers/succeed_on'
2
+ require 'pathway/rspec/matchers/fail_on'
3
+ require 'pathway/rspec/matchers/accept_optional_fields'
4
+ require 'pathway/rspec/matchers/require_fields'
@@ -0,0 +1,41 @@
1
+ require 'pathway/rspec/matchers/form_schema_helpers'
2
+
3
+ RSpec::Matchers.define :accept_optional_fields do |*fields|
4
+ match do |form|
5
+ @form, @fields = form, fields
6
+
7
+ not_defined.empty? && not_optional.empty?
8
+ end
9
+
10
+ match_when_negated do |form|
11
+ @form, @fields = form, fields
12
+
13
+ not_defined.empty? && optional.empty?
14
+ end
15
+
16
+ description do
17
+ "accept #{field_list} as optional #{pluralize_fields}"
18
+ end
19
+
20
+ failure_message do
21
+ "Expected to accept #{field_list} as optional #{pluralize_fields} but " +
22
+ [not_optional_list, not_defined_list].compact.join("; and ")
23
+ end
24
+
25
+ failure_message_when_negated do
26
+ "Did not expect to accept #{field_list} as optional #{pluralize_fields} but " +
27
+ [optional_list, not_defined_list].compact.join("; and ")
28
+ end
29
+
30
+ include Pathway::Rspec::FormSchemaHelpers
31
+
32
+ def optional_list
33
+ "#{as_list(optional)} #{were_was(optional)} optional" if optional.any?
34
+ end
35
+
36
+ def not_optional_list
37
+ "#{as_list(not_required)} #{were_was(not_required)} not optional" if not_optional.any?
38
+ end
39
+ end
40
+
41
+ RSpec::Matchers.alias_matcher :accept_optional_field, :accept_optional_fields
@@ -0,0 +1,98 @@
1
+ require 'pathway/rspec/matchers/list_helpers'
2
+
3
+ RSpec::Matchers.define :fail_on do |input|
4
+ match do |operation|
5
+ @operation, @input = operation, input
6
+
7
+ failure? && type_matches? && message_matches? && details_matches?
8
+ end
9
+
10
+ match_when_negated do |operation|
11
+ raise NotImplementedError, '`expect().not_to fail_on(input).with_type()` is not supported.' if @type
12
+ raise NotImplementedError, '`expect().not_to fail_on(input).with_message()` is not supported.' if @message
13
+ raise NotImplementedError, '`expect().not_to fail_on(input).with_details()` is not supported.' if @details
14
+ @operation, @input = operation, input
15
+
16
+ !failure?
17
+ end
18
+
19
+ chain :type do |type|
20
+ @type = type
21
+ end
22
+
23
+ alias :with_type :type
24
+ alias :and_type :type
25
+
26
+ chain :message do |message|
27
+ @message = message
28
+ end
29
+
30
+ alias :with_message :message
31
+ alias :and_message :message
32
+
33
+ chain :details do |details|
34
+ @details = details
35
+ end
36
+
37
+ alias :with_details :details
38
+ alias :and_details :details
39
+
40
+ description do
41
+ "fail" + (@type ? " with :#@type error" : '')
42
+ end
43
+
44
+ failure_message do
45
+ if !failure?
46
+ "Expected operation to fail but it didn't"
47
+ else
48
+ "Expected failed operation to " +
49
+ as_sentence(failure_descriptions, connector: '; ', last_connector: '; and ')
50
+ end
51
+ end
52
+
53
+ failure_message_when_negated do
54
+ "Did not to expected operation to fail but it did"
55
+ end
56
+
57
+ def failure?
58
+ result.failure?
59
+ end
60
+
61
+ def type_matches?
62
+ @type.nil? || @type == error.type
63
+ end
64
+
65
+ def message_matches?
66
+ @message.nil? || values_match?(@message, error.message)
67
+ end
68
+
69
+ def details_matches?
70
+ @details.nil? || values_match?(@details, error.details)
71
+ end
72
+
73
+ def result
74
+ @result ||= @operation.call(@input)
75
+ end
76
+
77
+ def error
78
+ result.error
79
+ end
80
+
81
+ def failure_descriptions
82
+ [type_failure_description, message_failure_description, details_failure_description].compact
83
+ end
84
+
85
+ def type_failure_description
86
+ type_matches? ? nil : "have type :#@type but instead was :#{error.type}"
87
+ end
88
+
89
+ def message_failure_description
90
+ message_matches? ? nil : "have message like #{description_of(@message)} but instead got #{description_of(error.message)}"
91
+ end
92
+
93
+ def details_failure_description
94
+ details_matches? ? nil : "have details like #{description_of(@details)} but instead got #{description_of(error.details)}"
95
+ end
96
+
97
+ include Pathway::Rspec::ListHelpers
98
+ end
@@ -0,0 +1,21 @@
1
+ require 'pathway/rspec/matchers/list_helpers'
2
+
3
+ module Pathway
4
+ module Rspec
5
+ module FieldListHelpers
6
+ include ListHelpers
7
+
8
+ def field_list
9
+ as_list(@fields)
10
+ end
11
+
12
+ def were_was(list)
13
+ list.size > 1 ? "were" : "was"
14
+ end
15
+
16
+ def pluralize_fields
17
+ @fields.size > 1 ? "fields" : "field"
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,57 @@
1
+ require 'pathway/rspec/matchers/field_list_helpers'
2
+
3
+ module Pathway
4
+ module Rspec
5
+ module FormSchemaHelpers
6
+ include FieldListHelpers
7
+
8
+ def rules
9
+ @form.rules
10
+ end
11
+
12
+ def not_defined_list
13
+ "#{as_list(not_defined)} #{were_was(not_defined)} not defined" if not_defined.any?
14
+ end
15
+
16
+ def required
17
+ @required ||= @fields.select { |field| required?(field) }
18
+ end
19
+
20
+ def optional
21
+ @optional ||= @fields.select { |field| optional?(field) }
22
+ end
23
+
24
+ def not_required
25
+ @not_required ||= defined - required
26
+ end
27
+
28
+ def not_optional
29
+ @not_required ||= defined - optional
30
+ end
31
+
32
+ def not_defined
33
+ @not_defined ||= @fields - defined
34
+ end
35
+
36
+ def defined
37
+ @defined ||= @fields & rules.keys
38
+ end
39
+
40
+ def optional?(field)
41
+ if rules[field]&.type == :implication
42
+ left = rules[field].left
43
+
44
+ left.type == :predicate && left.name == :key? && left.args.first == field
45
+ end
46
+ end
47
+
48
+ def required?(field)
49
+ if rules[field]&.type == :and
50
+ left = rules[field].left
51
+
52
+ left.type == :predicate && left.name == :key? && left.args.first == field
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,17 @@
1
+ module Pathway
2
+ module Rspec
3
+ module ListHelpers
4
+ def as_list(items)
5
+ as_sentence(items.map(&:inspect))
6
+ end
7
+
8
+ def as_sentence(items, connector: ", ", last_connector: " and ")
9
+ *rest, last = items
10
+
11
+ result = String.new
12
+ result << rest.join(connector) << last_connector if rest.any?
13
+ result << last
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,41 @@
1
+ require 'pathway/rspec/matchers/form_schema_helpers'
2
+
3
+ RSpec::Matchers.define :require_fields do |*fields|
4
+ match do |form|
5
+ @form, @fields = form, fields
6
+
7
+ not_defined.empty? && not_required.empty?
8
+ end
9
+
10
+ match_when_negated do |form|
11
+ @form, @fields = form, fields
12
+
13
+ not_defined.empty? && required.empty?
14
+ end
15
+
16
+ description do
17
+ "require #{field_list} as #{pluralize_fields}"
18
+ end
19
+
20
+ failure_message do
21
+ "Expected to require #{field_list} as #{pluralize_fields} but " +
22
+ [not_required_list, not_defined_list].compact.join("; and ")
23
+ end
24
+
25
+ failure_message_when_negated do
26
+ "Did not expect to require #{field_list} as #{pluralize_fields} but " +
27
+ [required_list, not_defined_list].compact.join("; and ")
28
+ end
29
+
30
+ include Pathway::Rspec::FormSchemaHelpers
31
+
32
+ def required_list
33
+ "#{as_list(required)} #{were_was(required)} required" if required.any?
34
+ end
35
+
36
+ def not_required_list
37
+ "#{as_list(not_required)} #{were_was(not_required)} not required" if not_required.any?
38
+ end
39
+ end
40
+
41
+ RSpec::Matchers.alias_matcher :require_field, :require_fields
@@ -0,0 +1,46 @@
1
+ RSpec::Matchers.define :succeed_on do |input|
2
+ match do |operation|
3
+ @operation, @input = operation, input
4
+
5
+ success? && return_value_matches?
6
+ end
7
+
8
+ match_when_negated do |operation|
9
+ raise NotImplementedError, '`expect().not_to succeed_on(input).returning()` is not supported.' if @value
10
+ @operation, @input = operation, input
11
+
12
+ !success?
13
+ end
14
+
15
+ chain :returning do |value|
16
+ @value = value
17
+ end
18
+
19
+ description do
20
+ "be successful"
21
+ end
22
+
23
+ failure_message do
24
+ if !success?
25
+ "Expected operation to be successful but failed with :#{result.error.type} error"
26
+ else
27
+ "Expected successful operation to return #{description_of(@value)} but instead got #{description_of(result.value)}"
28
+ end
29
+ end
30
+
31
+ failure_message_when_negated do
32
+ "Did not to expected operation to be successful but it was"
33
+ end
34
+
35
+ def success?
36
+ result.success?
37
+ end
38
+
39
+ def return_value_matches?
40
+ @value.nil? || values_match?(@value, result.value)
41
+ end
42
+
43
+ def result
44
+ @result ||= @operation.call(@input)
45
+ end
46
+ end
@@ -0,0 +1,3 @@
1
+ module Pathway
2
+ VERSION = '0.0.9'
3
+ end
data/pathway.gemspec ADDED
@@ -0,0 +1,37 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "pathway/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "pathway"
8
+ spec.version = Pathway::VERSION
9
+ spec.authors = ["Pablo Herrero"]
10
+ spec.email = ["pablodherrero@gmail.com"]
11
+
12
+ spec.summary = %q{Define your bussines logic in simple steps.}
13
+ spec.description = %q{Define your bussines logic in simple steps.}
14
+ #spec.homepage = "TODO: Put your gem's website or public repo URL here."
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
18
+ f.match(%r{^(test|spec|features)/})
19
+ end
20
+ spec.bindir = "exe"
21
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
22
+ spec.require_paths = ["lib"]
23
+
24
+ spec.required_ruby_version = ">= 2.4.0"
25
+
26
+ spec.add_dependency "inflecto"
27
+ spec.add_dependency "contextualizer", "~> 0.0.3"
28
+
29
+ spec.add_development_dependency "dry-validation", "~> 0.11"
30
+ spec.add_development_dependency "bundler", ">= 1.14.0"
31
+ spec.add_development_dependency "sequel", "~> 4.46.0"
32
+ spec.add_development_dependency "rake", "~> 10.0"
33
+ spec.add_development_dependency "rspec", "~> 3.0"
34
+ spec.add_development_dependency "coveralls"
35
+ spec.add_development_dependency "pry"
36
+ spec.add_development_dependency "pry-byebug"
37
+ end
metadata ADDED
@@ -0,0 +1,213 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pathway
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.9
5
+ platform: ruby
6
+ authors:
7
+ - Pablo Herrero
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2017-07-04 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: inflecto
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: contextualizer
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 0.0.3
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 0.0.3
41
+ - !ruby/object:Gem::Dependency
42
+ name: dry-validation
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0.11'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0.11'
55
+ - !ruby/object:Gem::Dependency
56
+ name: bundler
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: 1.14.0
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: 1.14.0
69
+ - !ruby/object:Gem::Dependency
70
+ name: sequel
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 4.46.0
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 4.46.0
83
+ - !ruby/object:Gem::Dependency
84
+ name: rake
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '10.0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '10.0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rspec
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '3.0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '3.0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: coveralls
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: pry
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: pry-byebug
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ description: Define your bussines logic in simple steps.
154
+ email:
155
+ - pablodherrero@gmail.com
156
+ executables: []
157
+ extensions: []
158
+ extra_rdoc_files: []
159
+ files:
160
+ - ".gitignore"
161
+ - ".rspec"
162
+ - Gemfile
163
+ - LICENSE.txt
164
+ - README.md
165
+ - Rakefile
166
+ - bin/byebug
167
+ - bin/console
168
+ - bin/rake
169
+ - bin/rspec
170
+ - bin/setup
171
+ - lib/pathway.rb
172
+ - lib/pathway/plugins/authorization.rb
173
+ - lib/pathway/plugins/dry_validation.rb
174
+ - lib/pathway/plugins/responder.rb
175
+ - lib/pathway/plugins/sequel_models.rb
176
+ - lib/pathway/responder.rb
177
+ - lib/pathway/result.rb
178
+ - lib/pathway/rspec.rb
179
+ - lib/pathway/rspec/matchers.rb
180
+ - lib/pathway/rspec/matchers/accept_optional_fields.rb
181
+ - lib/pathway/rspec/matchers/fail_on.rb
182
+ - lib/pathway/rspec/matchers/field_list_helpers.rb
183
+ - lib/pathway/rspec/matchers/form_schema_helpers.rb
184
+ - lib/pathway/rspec/matchers/list_helpers.rb
185
+ - lib/pathway/rspec/matchers/require_fields.rb
186
+ - lib/pathway/rspec/matchers/succeed_on.rb
187
+ - lib/pathway/version.rb
188
+ - pathway.gemspec
189
+ homepage:
190
+ licenses:
191
+ - MIT
192
+ metadata: {}
193
+ post_install_message:
194
+ rdoc_options: []
195
+ require_paths:
196
+ - lib
197
+ required_ruby_version: !ruby/object:Gem::Requirement
198
+ requirements:
199
+ - - ">="
200
+ - !ruby/object:Gem::Version
201
+ version: 2.4.0
202
+ required_rubygems_version: !ruby/object:Gem::Requirement
203
+ requirements:
204
+ - - ">="
205
+ - !ruby/object:Gem::Version
206
+ version: '0'
207
+ requirements: []
208
+ rubyforge_project:
209
+ rubygems_version: 2.6.9
210
+ signing_key:
211
+ specification_version: 4
212
+ summary: Define your bussines logic in simple steps.
213
+ test_files: []