laminar 0.3.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.
@@ -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: []