linearly 0.1.0

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: eac2749b4aad79a7f0a3720eb59024d1b1761747
4
+ data.tar.gz: 920e5ac7f405ac7326d439a7815c18ce353b3795
5
+ SHA512:
6
+ metadata.gz: f9e00e9d2d3064c3cf2934c080a77c43643e2c4375ce92bb335c45895502ab938b3021f0285a9470be171195db4e8a7a916cbb3aa48309a40f2e27b5573c5538
7
+ data.tar.gz: 622bde992eacf7a3c30bf2a0cfaae44fc0462e380b21038a4b56f24da010bb62e7b8d10033640cb1fd0b8d192b1d0783c2af18eb8b7e002f7d9c8c312f0fb06a
@@ -0,0 +1,80 @@
1
+ require 'forwardable'
2
+
3
+ # rubocop:disable Metrics/LineLength
4
+
5
+ module Linearly
6
+ module Errors
7
+ # {BrokenContract} is what happens when inputs or outputs for a {Step} do
8
+ # not match expectations.
9
+ # @abstract
10
+ class BrokenContract < RuntimeError
11
+ extend Forwardable
12
+
13
+ # Input/output validation failures
14
+ #
15
+ # @return [Hash<Symbol, Validation::Failure>]
16
+ # @api public
17
+ # @example
18
+ # err = Linearly::Errors::Inputs.new(
19
+ # key: Linearly::Validation::Failure::Missing.instance,
20
+ # )
21
+ # err.failures
22
+ # => {:key => [missing]}
23
+ attr_reader :failures
24
+
25
+ # @!method keys
26
+ # @return [Array<Symbol>]
27
+ # @see https://docs.ruby-lang.org/en/2.0.0/Hash.html#method-i-keys Hash#keys
28
+ # @api public
29
+ # @example
30
+ # err = Linearly::Errors::BrokenContract::Inputs.new(
31
+ # key: Linearly::Validation::Failure::Missing.instance,
32
+ # )
33
+ # err.keys
34
+ # => [:key]
35
+ def_delegators :failures, :keys
36
+
37
+ # Constructor for a {BrokenContract} error
38
+ #
39
+ # @param failures [Hash<Symbol, Validation::Failure>]
40
+ #
41
+ # @api public
42
+ # @example
43
+ # Linearly::Errors::BrokenContract::Inputs.new(
44
+ # key: Linearly::Validation::Failure::Missing.instance,
45
+ # )
46
+ # => #<Linearly::Errors::BrokenContract::Inputs:
47
+ # failed input expectations: [key]>
48
+ def initialize(failures)
49
+ @failures = failures
50
+ super("#{copy}: [#{keys.join(', ')}]")
51
+ end
52
+
53
+ # {Inputs} means a {BrokenContract} on inputs.
54
+ class Inputs < BrokenContract
55
+ private
56
+
57
+ # Copy for the error message
58
+ #
59
+ # @return [String]
60
+ # @api private
61
+ def copy
62
+ 'failed input expectations'
63
+ end
64
+ end # class Inputs
65
+
66
+ # {Outputs} means a {BrokenContract} on outputs.
67
+ class Outputs < BrokenContract
68
+ private
69
+
70
+ # Copy for the error message
71
+ #
72
+ # @return [String]
73
+ # @api private
74
+ def copy
75
+ 'failed output expectations'
76
+ end
77
+ end # class Outputs
78
+ end # class BrokenContract
79
+ end # module Errors
80
+ end # module Linearly
@@ -0,0 +1,29 @@
1
+ module Linearly
2
+ module Errors
3
+ # {StateNotReturned} is an error that is getting thrown when one of {Step}s
4
+ # in the {Flow} does not return an instance of +Statefully::State+.
5
+ # @api public
6
+ class StateNotReturned < RuntimeError
7
+ # Value that caused the error
8
+ #
9
+ # @return [Object]
10
+ # @api public
11
+ # @example
12
+ # Linearly::Errors::StateNotReturned.new('surprise').value
13
+ # => "surprise"
14
+ attr_reader :value
15
+
16
+ # Constructor for the {StateNotReturned} class
17
+ #
18
+ # @param value [Object]
19
+ #
20
+ # @api public
21
+ # @example
22
+ # Linearly::Errors::StateNotReturned.new('surprise')
23
+ def initialize(value)
24
+ super("#{value.class.name} is not a Statefully::State")
25
+ @value = value
26
+ end
27
+ end # class StateNotReturned
28
+ end # module Errors
29
+ end # module Linearly
@@ -0,0 +1,144 @@
1
+ require 'forwardable'
2
+
3
+ module Linearly
4
+ class Flow
5
+ extend Forwardable
6
+ include Mixins::Reducer
7
+
8
+ # @!method call(state)
9
+ # Validate the input state and run steps as long as it's a +Success+
10
+ #
11
+ # @param [Statefully::State] state
12
+ #
13
+ # @return [Statefully::State]
14
+ # @api public
15
+ # @example
16
+ # flow = Linearly::Flow.new(
17
+ # Users::Find,
18
+ # Users::AddRole.new(:admin),
19
+ # Users::Save,
20
+ # )
21
+ # flow.call(Statefully::State.create(user_id: params[:id]))
22
+ #
23
+ # @!method inputs
24
+ # Inputs required for the {Flow}
25
+ #
26
+ # @return [Hash<Symbol, TrueClass>]
27
+ # @api public
28
+ # @example
29
+ # Linearly::Flow.new(Users::Find).inputs
30
+ # => {user_id: true}
31
+ #
32
+ # @!method outputs
33
+ # Outputs provided by the {Flow}
34
+ #
35
+ # @return [Hash<Symbol, TrueClass>]
36
+ # @api public
37
+ # @example
38
+ # Linearly::Flow.new(Users::Find).outputs
39
+ # => {user: true}
40
+ def_delegators :@contract, :inputs, :outputs
41
+
42
+ # Constructor for the {Flow}
43
+ #
44
+ # @param steps [Array<Step>] array of things that implement the +Step+
45
+ # interface (+call+, +inputs+ and +outputs+ methods).
46
+ #
47
+ # @api public
48
+ # @example
49
+ # flow = Linearly::Flow.new(
50
+ # Users::Find,
51
+ # Users::AddRole.new(:admin),
52
+ # Users::Save,
53
+ # )
54
+ def initialize(*steps)
55
+ @steps = steps
56
+ @contract = Contract.new(steps)
57
+ end
58
+
59
+ # Convenience method to join +Step+s into one {Flow}
60
+ #
61
+ # @param other [Step]
62
+ #
63
+ # @return [Flow]
64
+ # @api public
65
+ # @example
66
+ # flow =
67
+ # Users::Find
68
+ # .>> Users::Update
69
+ # .>> Users::Save
70
+ def >>(other)
71
+ Flow.new(other, *@steps)
72
+ end
73
+
74
+ private
75
+
76
+ # Steps to be ran by the {Flow}
77
+ #
78
+ # @return [Array<Step>]
79
+ # @api private
80
+ def steps
81
+ [
82
+ Validation::Inputs.new(inputs),
83
+ *@steps.map(&Runner.method(:new)),
84
+ Validation::Outputs.new(outputs),
85
+ ]
86
+ end
87
+
88
+ # {Contract} is a companion for the {Flow}, providing it with logic for
89
+ # properly determining required +inputs+ and expected +outputs+.
90
+ class Contract
91
+ extend Forwardable
92
+
93
+ # Inputs required for the {Flow}
94
+ #
95
+ # @return [Hash<Symbol, TrueClass>]
96
+ # @api private
97
+ attr_reader :inputs
98
+
99
+ # Outputs provided by the {Flow}
100
+ #
101
+ # @return [Hash<Symbol, TrueClass>]
102
+ # @api private
103
+ attr_reader :outputs
104
+
105
+ # Constructor for the {Contract}
106
+ #
107
+ # @param steps [Array<Step>] array of things that implement the +Step+
108
+ # interface (+call+, +inputs+ and +outputs+ methods).
109
+ #
110
+ # @api private
111
+ def initialize(steps)
112
+ @steps = steps
113
+ @inputs = {}
114
+ @outputs = {}
115
+ build
116
+ end
117
+
118
+ private
119
+
120
+ # Figure out inputs required and outputs provided by the {Flow}
121
+ #
122
+ # @return [Array] irrelevant
123
+ # @api private
124
+ def build
125
+ @steps.each(&method(:process))
126
+ [@inputs, @outputs].map(&:freeze)
127
+ end
128
+
129
+ # Process a single step
130
+ #
131
+ # @param step [Step]
132
+ #
133
+ # @return [Hash] irrelevant
134
+ # @api private
135
+ def process(step)
136
+ step.inputs.each do |key, val|
137
+ @inputs[key] = val unless @inputs.key?(key) || @outputs.key?(key)
138
+ end
139
+ @outputs.merge!(step.outputs)
140
+ end
141
+ end # class Contract
142
+ private_constant :Contract
143
+ end # class Flow
144
+ end # module Linearly
@@ -0,0 +1,22 @@
1
+ module Linearly
2
+ module Mixins
3
+ module FlowBuilder
4
+ # @!method self.>>(other)
5
+ # Convenience method to create a {Flow} from linked Steps
6
+ #
7
+ # @param other [Step] next step in the {Flow}
8
+ #
9
+ # @return [Flow]
10
+ # @api public
11
+ # @example
12
+ # flow =
13
+ # Users::Find
14
+ # .>> Users::Update
15
+ # .>> Users::Save
16
+ def >>(other)
17
+ Flow.new(self, other)
18
+ end
19
+ end # module FlowBuilder
20
+ end # module Mixins
21
+ private_constant :Mixins
22
+ end # module Linearly
@@ -0,0 +1,32 @@
1
+ require 'statefully'
2
+
3
+ module Linearly
4
+ module Mixins
5
+ # {Reducer} is a mixin to include in all classes which need to run more than
6
+ # one step.
7
+ # @api private
8
+ module Reducer
9
+ # Keep calling steps as long as the state is successful
10
+ #
11
+ # This method reeks of :reek:TooManyStatements and :reek:FeatureEnvy.
12
+ #
13
+ # @param state [Statefully::State]
14
+ #
15
+ # @return [Statefully::State]
16
+ # @api private
17
+ def call(state)
18
+ steps.reduce(state) do |current, step|
19
+ break current if current.failed? || current.finished?
20
+ begin
21
+ next_state = step.call(current)
22
+ rescue StandardError => err
23
+ break current.fail(err)
24
+ end
25
+ next next_state if next_state.is_a?(Statefully::State)
26
+ current.fail(Errors::StateNotReturned.new(step))
27
+ end
28
+ end
29
+ end # module Reducer
30
+ end # module Mixins
31
+ private_constant :Mixins
32
+ end # module Linearly
@@ -0,0 +1,38 @@
1
+ module Linearly
2
+ # {Runner} is a wrapper around a single step with inputs and outputs, which
3
+ # validates the inputs, runs the step, and validates the outputs.
4
+ # @api private
5
+ class Runner
6
+ include Mixins::Reducer
7
+
8
+ # Constructor for the {Runner} object
9
+ # @param step [Step] anything that implements the +Step+ interface
10
+ # (+call+, +inputs+ and +outputs+ methods).
11
+ #
12
+ # @api private
13
+ def initialize(step)
14
+ @step = step
15
+ end
16
+
17
+ private
18
+
19
+ # Return the wrapped {Step}
20
+ #
21
+ # @return [Step]
22
+ # @api private
23
+ attr_reader :step
24
+
25
+ # Wrap the provided {Step} with input and output validation
26
+ #
27
+ # @return [Array<Step>]
28
+ # @api private
29
+ def steps
30
+ [
31
+ Validation::Inputs.new(step.inputs),
32
+ step,
33
+ Validation::Outputs.new(step.outputs),
34
+ ]
35
+ end
36
+ end # class Runner
37
+ private_constant :Runner
38
+ end # module Linearly
@@ -0,0 +1,50 @@
1
+ module Linearly
2
+ module Step
3
+ module Dynamic
4
+ include Mixins::FlowBuilder
5
+
6
+ # Inputs for a step
7
+ #
8
+ # An invalid implementation is provided to ensure that a failure to
9
+ # override this method is not quietly caught as a StandardError.
10
+ #
11
+ # @return [Hash<Symbol, Expectation>]
12
+ # @api public
13
+ # @example
14
+ # FindUser.new.inputs
15
+ # => { user_id: Integer }
16
+ def inputs
17
+ raise NotImplementedError
18
+ end
19
+
20
+ # Outputs for a step
21
+ #
22
+ # An invalid implementation is provided to ensure that a failure to
23
+ # override this method is not quietly caught as a StandardError.
24
+ #
25
+ # @return [Hash<Symbol, Expectation>]
26
+ # @api public
27
+ # @example
28
+ # FindUser.new.outputs
29
+ # => { user: User }
30
+ def outputs
31
+ raise NotImplementedError
32
+ end
33
+
34
+ # User-defined logic for this +Step+
35
+ #
36
+ # An invalid implementation is provided to ensure that a failure to
37
+ # override this method is not quietly caught as a StandardError.
38
+ #
39
+ # @param _state [Statefully::State]
40
+ #
41
+ # @return [Statefully::State]
42
+ # @api public
43
+ # @example
44
+ # FindUser.new.call(Statefully::State.create(user_id: 7))
45
+ def call(_state)
46
+ raise NotImplementedError
47
+ end
48
+ end # module Dynamic
49
+ end # module Step
50
+ end # module Linearly
@@ -0,0 +1,120 @@
1
+ module Linearly
2
+ module Step
3
+ # {Static} is a type of +Step+ whose operation solely depends on the content
4
+ # of the +State+ passed to its {.call} method. It's the best and most
5
+ # deterministic type of a +Step+, hence we provided a helper. Inheriting
6
+ # from {Static} will still require you to implement three methods: class
7
+ # +inputs+ and +outputs+ methods, and an instance +call+ method. What you
8
+ # get though is that your +call+ method does not need to take any parameters
9
+ # and will have access to the private +state+ instance variable. What's
10
+ # more, any unknown messages will be forwarded to +state+, so that your code
11
+ # can be shorter and more expressive. Also, you don't have to explicitly
12
+ # rescue exceptions - the static {.call} method will catch those and fail
13
+ # the +state+ accordingly.
14
+ class Static
15
+ extend Mixins::FlowBuilder
16
+
17
+ # Main entry point to {Step::Static}
18
+ #
19
+ # @param state [Statefully::State]
20
+ #
21
+ # @return [Statefully::State]
22
+ # @api public
23
+ # @example
24
+ # class FindUser < Linearly::Step::Static
25
+ # def self.inputs
26
+ # { user_id: Integer }
27
+ # end
28
+ #
29
+ # def self.outputs
30
+ # { user: User }
31
+ # end
32
+ #
33
+ # def call
34
+ # succeed(user: User.find(user_id))
35
+ # end
36
+ # end # class FindUser
37
+ # FindUser.call(Statefully::State.create(user_id: params[:id]))
38
+ def self.call(state)
39
+ new(state).call
40
+ rescue StandardError => err
41
+ state.fail(err)
42
+ end
43
+
44
+ # User-defined logic for this +Step+
45
+ #
46
+ # @return [Statefully::State]
47
+ # @api private
48
+ def call
49
+ raise NotImplementedError
50
+ end
51
+
52
+ # Inputs for a step
53
+ #
54
+ # @return [Hash<Symbol, Expectation>]
55
+ # @api public
56
+ # @example
57
+ # FindUser.inputs
58
+ # => { user_id: Integer }
59
+ def self.inputs
60
+ raise NotImplementedError
61
+ end
62
+
63
+ # Outputs for a step
64
+ #
65
+ # @return [Hash<Symbol, Expectation>]
66
+ # @api public
67
+ # @example
68
+ # FindUser.outputs
69
+ # => { user: User }
70
+ def self.outputs
71
+ raise NotImplementedError
72
+ end
73
+
74
+ private
75
+
76
+ # {State} received through the constructor
77
+ #
78
+ # @return [Statefully::State]
79
+ # @api private
80
+ attr_reader :state
81
+
82
+ # Constructor for the {Step::Static}
83
+ #
84
+ # @param state [Statefully::State]
85
+ # @api private
86
+ def initialize(state)
87
+ @state = state
88
+ end
89
+ private_class_method :new
90
+
91
+ # Dynamically pass unknown messages to the wrapped +State+
92
+ #
93
+ # @param name [Symbol|String]
94
+ # @param args [Array<Object>]
95
+ # @param block [Proc]
96
+ #
97
+ # @return [Object]
98
+ # @raise [NoMethodError]
99
+ # @api private
100
+ def method_missing(name, *args, &block)
101
+ state.send(name, *args, &block)
102
+ rescue NoMethodError
103
+ super
104
+ end
105
+
106
+ # Companion to `method_missing`
107
+ #
108
+ # This method reeks of :reek:BooleanParameter.
109
+ #
110
+ # @param name [Symbol|String]
111
+ # @param include_private [Boolean]
112
+ #
113
+ # @return [Boolean]
114
+ # @api private
115
+ def respond_to_missing?(name, include_private = false)
116
+ state.send(:respond_to_missing?, name, include_private)
117
+ end
118
+ end # class Static
119
+ end # module Step
120
+ end # module Linearly