laminar 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Laminar
4
+ module Flow
5
+ # Specification for a flow (chained sequence of particles).
6
+ class Specification
7
+ attr_accessor :steps, :first_step
8
+
9
+ def initialize(_args = {}, &spec)
10
+ @steps = {}
11
+ instance_eval(&spec) if spec
12
+ end
13
+
14
+ def step(name, options = {}, &gotos)
15
+ step = add_step(name, options, &gotos)
16
+
17
+ # backport a default next step onto the previous step to point to
18
+ # the current one, unless this is the first step. Allows for simple
19
+ # case where execution just falls through to the next step where they
20
+ # haven't specified any explicit branching or none of the branch
21
+ # conditions get met.
22
+ @prev_step&.branch(step.name)
23
+ @prev_step = step
24
+ end
25
+
26
+ def before_each(*args, &block)
27
+ before_each_callbacks.concat(args)
28
+ before_each_callbacks << block if block
29
+ end
30
+
31
+ def after_each(*args, &block)
32
+ after_each_callbacks.concat(args)
33
+ after_each_callbacks << block if block
34
+ end
35
+
36
+ def before_each_callbacks
37
+ @before_each_callbacks ||= []
38
+ end
39
+
40
+ def after_each_callbacks
41
+ @after_each_callbacks ||= []
42
+ end
43
+
44
+ private
45
+
46
+ def add_step(name, options = {}, &gotos)
47
+ raise ArgumentError, "Step #{name} defined twice" if @steps.key?(name)
48
+
49
+ step = Step.new(name, options, &gotos)
50
+ @first_step ||= step.name
51
+ @steps[step.name] = step
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'laminar/flow/options_validator'
4
+ require 'active_support'
5
+ require 'active_support/core_ext/string/inflections'
6
+
7
+ module Laminar
8
+ module Flow
9
+ # Specification for an executable step in a Flow.
10
+ class Step
11
+ include OptionsValidator
12
+ attr_reader :name, :branches, :class_name
13
+
14
+ valid_options %i[class].freeze
15
+
16
+ def initialize(name, options = {}, &block)
17
+ unless name.class.method_defined?(:to_sym)
18
+ raise ArgumentError, 'invalid name'
19
+ end
20
+
21
+ validate_options(options)
22
+ @class_name = (options[:class] || name).to_s.camelize
23
+ @name = name.to_sym
24
+ @branches = []
25
+
26
+ instance_eval(&block) if block
27
+ end
28
+
29
+ # Return class instance of the associated particle.
30
+ def particle
31
+ class_name.constantize
32
+ end
33
+
34
+ # Add a branch specification. This is typically called as
35
+ # part of a flow specification:
36
+ #
37
+ # flow do
38
+ # step :step1
39
+ # end
40
+ #
41
+ def branch(target, options = {})
42
+ branches << Branch.new(target, options)
43
+ end
44
+ alias goto branch
45
+
46
+ # Find the next rule in the flow. Examines the branches associated
47
+ # with the current rule and returns the name of the first branch
48
+ # that satisfies its condition.
49
+ def next_step_name(impl_context)
50
+ branch = first_applicable_branch(impl_context)
51
+ return if branch.nil?
52
+
53
+ branch.name
54
+ end
55
+
56
+ # Defines a callback to run before the flow executes the step.
57
+ def before(*args, &block)
58
+ before_callbacks.concat(args)
59
+ before_callbacks << block if block
60
+ end
61
+
62
+ # Defines a callback to run after the flow executes the step.
63
+ def after(*args, &block)
64
+ after_callbacks.concat(args)
65
+ after_callbacks << block if block
66
+ end
67
+
68
+ # Return the list of before callbacks.
69
+ def before_callbacks
70
+ @before_callbacks ||= []
71
+ end
72
+
73
+ # Return the list of after callbacks.
74
+ def after_callbacks
75
+ @after_callbacks ||= []
76
+ end
77
+
78
+ # Return the first branch that satisfies its condition.
79
+ def first_applicable_branch(target)
80
+ branches.each do |branch|
81
+ return branch if branch.meets_condition?(target)
82
+ end
83
+ nil
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Laminar
4
+ # Base methods for a logic particle. Particles can be invoked
5
+ # by themselves or as part of a Flow. Classes should include
6
+ # this module rather than inherit.
7
+ module Particle
8
+ def self.included(klass)
9
+ klass.class_eval do
10
+ extend ClassMethods
11
+ include Callbacks
12
+ include InstanceMethods
13
+ end
14
+
15
+ attr_reader :context
16
+ end
17
+
18
+ # Laminar::Particle class methods and attributes.
19
+ module ClassMethods
20
+ def call(context = {})
21
+ new(context).invoke
22
+ end
23
+
24
+ def call!(context = {})
25
+ new(context).invoke!
26
+ end
27
+ end
28
+
29
+ module InstanceMethods
30
+ def initialize(context = {})
31
+ @context = Context.build(context)
32
+ end
33
+
34
+ def invoke
35
+ invoke!
36
+ rescue ParticleStopped
37
+ context
38
+ end
39
+
40
+ def invoke!
41
+ run_before_callbacks
42
+ return context if context.halted?
43
+
44
+ param_list = context_slice
45
+ param_list.empty? ? call : call(context_slice)
46
+ run_after_callbacks
47
+ context
48
+ end
49
+
50
+ def call; end
51
+
52
+ private
53
+
54
+ def context_slice
55
+ context.select { |k, _v| introspect_params.include?(k) }
56
+ end
57
+
58
+ # Returns an array of keyword parameters that the instance expects
59
+ # or accepts. If the signature includes a 'splat' (:keyrest) to catch
60
+ # a variable set of arguments, returns the current context keys.
61
+ def introspect_params
62
+ params = self.class.instance_method(:call).parameters
63
+ return context.keys if params.map(&:first).include?(:keyrest)
64
+
65
+ params.map(&:last)
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Laminar
4
+ # Raised when someone calls fail!() on a Laminar::Context.
5
+ class ParticleStopped < StandardError
6
+ attr_reader :context
7
+
8
+ def initialize(context = nil)
9
+ @context = context
10
+ super
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Laminar
4
+ VERSION = '0.3.0'
5
+ end
metadata ADDED
@@ -0,0 +1,140 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: laminar
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.3.0
5
+ platform: ruby
6
+ authors:
7
+ - Robert Lockerd
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2018-10-01 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '4.2'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '4.2'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.16'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.16'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '10.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '10.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: simplecov
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '0.16'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '0.16'
83
+ description:
84
+ email:
85
+ - rmlockerd@gmail.com
86
+ executables: []
87
+ extensions: []
88
+ extra_rdoc_files: []
89
+ files:
90
+ - ".github/ISSUE_TEMPLATE/bug_report.md"
91
+ - ".github/ISSUE_TEMPLATE/feature_request.md"
92
+ - ".gitignore"
93
+ - ".rspec"
94
+ - ".travis.yml"
95
+ - CHANGELOG.md
96
+ - CODE_OF_CONDUCT.md
97
+ - Gemfile
98
+ - LICENSE.txt
99
+ - README.md
100
+ - Rakefile
101
+ - bin/console
102
+ - bin/setup
103
+ - laminar.gemspec
104
+ - lib/laminar.rb
105
+ - lib/laminar/callbacks.rb
106
+ - lib/laminar/context.rb
107
+ - lib/laminar/flow.rb
108
+ - lib/laminar/flow/branch.rb
109
+ - lib/laminar/flow/flow_error.rb
110
+ - lib/laminar/flow/options_validator.rb
111
+ - lib/laminar/flow/specification.rb
112
+ - lib/laminar/flow/step.rb
113
+ - lib/laminar/particle.rb
114
+ - lib/laminar/particle_stopped.rb
115
+ - lib/laminar/version.rb
116
+ homepage: https://github.com/rmlockerd/laminar
117
+ licenses:
118
+ - MIT
119
+ metadata: {}
120
+ post_install_message:
121
+ rdoc_options: []
122
+ require_paths:
123
+ - lib
124
+ required_ruby_version: !ruby/object:Gem::Requirement
125
+ requirements:
126
+ - - ">="
127
+ - !ruby/object:Gem::Version
128
+ version: '0'
129
+ required_rubygems_version: !ruby/object:Gem::Requirement
130
+ requirements:
131
+ - - ">="
132
+ - !ruby/object:Gem::Version
133
+ version: '0'
134
+ requirements: []
135
+ rubyforge_project:
136
+ rubygems_version: 2.6.11
137
+ signing_key:
138
+ specification_version: 4
139
+ summary: Simple, composable business objects & workflow
140
+ test_files: []