linearly 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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