simple-service 0.1.6 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 903d43626ea1853a44478fd58d6aef0a4e86374745e9a5732e127ab85fcd4437
4
- data.tar.gz: ed5f2caa57468945b72bcf805c0ff3e2afd7df654ea03387a59fa16ea136ad19
3
+ metadata.gz: 706a23842071b121c2b6298b7bdaec0b43c4b110125c2c09011537541c4f5b9b
4
+ data.tar.gz: 9ad938ece973392233c554053aa1906da0ea799722d527cb15870913ef108070
5
5
  SHA512:
6
- metadata.gz: 0ab49e773b69b41e844a73da66f7ab06ca90f0d4edc097a4d44bc8457272040acd428fc37e3128a6e662f18bab30fb6d0e8a396c40eed6eccb2dd316171c5b1a
7
- data.tar.gz: b06385441ec3d0ce87aba8095e30cb7ab07f871c6507ae133b611eab0f370458c378d5f1ffd27111221b3ca75096ca0ff41bc1c6b61ab1f40cfebe45b1a5ac81
6
+ metadata.gz: 68563b2c8d9c955049814f90d31b765a30c077553d7199287c4d0117d7d1ed0eb2ce71674195820d482de52bd2f53c70dbb798549d2bf6a7c35ef2f149de3b56
7
+ data.tar.gz: ccb9fc69a58b3a9f7225e60e1b8120dced8ec58802483ee4151cb6a0fc71c20b5b3f9b4393b6b9c09de2ec18d3c09bfbdac16f341669ac055b3daa9b31b98ae0
data/.rubocop.yml CHANGED
@@ -1,5 +1,6 @@
1
1
  AllCops:
2
- TargetRubyVersion: 2.3
2
+ NewCops: enable
3
+ TargetRubyVersion: 2.7
3
4
  Exclude:
4
5
  - 'spec/**/*'
5
6
  - 'test/**/*'
@@ -14,7 +15,7 @@ Metrics/BlockLength:
14
15
  Exclude:
15
16
  - 'spec/**/*'
16
17
 
17
- Metrics/LineLength:
18
+ Layout/LineLength:
18
19
  Max: 140
19
20
 
20
21
  Metrics/MethodLength:
@@ -101,3 +102,7 @@ Style/ParallelAssignment:
101
102
 
102
103
  Style/CommentedKeyword:
103
104
  Enabled: false
105
+
106
+ Style/AccessorGrouping:
107
+ Enabled: false
108
+
data/Gemfile CHANGED
@@ -10,5 +10,7 @@ group :development, :test do
10
10
  gem 'rspec', '~> 3.7'
11
11
  # gem 'rubocop', '~> 0.61.1'
12
12
  gem 'simplecov', '~> 0'
13
- gem 'byebug'
13
+ gem 'pry-byebug'
14
14
  end
15
+
16
+ # gem "simple-immutable", "~> 1", path: "../simple-immutable"
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.6
1
+ 0.2.0
@@ -1,5 +1,3 @@
1
- # rubocop:disable Metrics/AbcSize
2
-
3
1
  # returns the comment for an action
4
2
  class ::Simple::Service::Action::Comment # @private
5
3
  attr_reader :short
@@ -8,7 +6,7 @@ class ::Simple::Service::Action::Comment # @private
8
6
  def self.extract(action:)
9
7
  file, line = action.source_location
10
8
  lines = Extractor.extract_comment_lines(file: file, before_line: line)
11
- full = lines[2..-1].join("\n") if lines.length >= 2
9
+ full = lines[2..].join("\n") if lines.length >= 2
12
10
  new short: lines[0], full: full
13
11
  end
14
12
 
@@ -1,4 +1,5 @@
1
1
  module Simple::Service
2
+ # rubocop:disable Lint/EmptyClass
2
3
  class Action
3
4
  end
4
5
  end
@@ -114,7 +115,7 @@ module Simple::Service
114
115
 
115
116
  # Note that +keys+ now only contains names of keyword arguments that actually exist.
116
117
  # This is therefore not a way to DOS this process.
117
- Hash[keys.map(&:to_sym).zip(values)]
118
+ keys.map(&:to_sym).zip(values).to_h
118
119
  end
119
120
 
120
121
  def variadic_parameter
@@ -164,7 +165,7 @@ module Simple::Service
164
165
  # we otherwise raise a ExtraArguments exception.
165
166
  case ary.length <=> positional_names.length
166
167
  when 1 # i.e. ary.length > positional_names.length
167
- extra_arguments = ary[positional_names.length..-1]
168
+ extra_arguments = ary[positional_names.length..]
168
169
  ary = ary[0..positional_names.length]
169
170
 
170
171
  if !extra_arguments.empty? && !variadic_parameter
@@ -179,7 +180,7 @@ module Simple::Service
179
180
  end
180
181
 
181
182
  # Build a hash with the existing_positional_names and the values from the array.
182
- hsh = Hash[existing_positional_names.zip(ary)]
183
+ hsh = existing_positional_names.zip(ary).to_h
183
184
 
184
185
  # Add the variadic_parameter, if any.
185
186
  hsh[variadic_parameter.name] = extra_arguments if variadic_parameter
@@ -2,8 +2,10 @@ module Simple::Service
2
2
  # Will be raised by ::Simple::Service.action.
3
3
  class NoSuchAction < ::ArgumentError
4
4
  attr_reader :service, :name
5
+
5
6
  def initialize(service, name)
6
7
  @service, @name = service, name
8
+ super()
7
9
  end
8
10
 
9
11
  def to_s
@@ -22,6 +24,7 @@ module Simple::Service
22
24
 
23
25
  def initialize(action, parameters)
24
26
  @action, @parameters = action, parameters
27
+ super()
25
28
  end
26
29
 
27
30
  def to_s
@@ -35,6 +38,7 @@ module Simple::Service
35
38
 
36
39
  def initialize(action, arguments)
37
40
  @action, @arguments = action, arguments
41
+ super()
38
42
  end
39
43
 
40
44
  def to_s
@@ -43,9 +47,6 @@ module Simple::Service
43
47
  end
44
48
  end
45
49
 
46
- class ContextMissingError < ::StandardError
47
- end
48
-
49
50
  class ContextReadOnlyError < ::StandardError
50
51
  def initialize(key)
51
52
  super "Cannot overwrite existing context setting #{key.inspect}"
@@ -1,12 +1,14 @@
1
1
  module Simple # @private
2
2
  end
3
3
 
4
+ module Simple::Service # @private
5
+ end
6
+
4
7
  require "expectation"
5
8
  require "logger"
6
9
 
7
10
  require_relative "service/errors"
8
11
  require_relative "service/action"
9
- require_relative "service/context"
10
12
  require_relative "service/version"
11
13
 
12
14
  # <b>The Simple::Service interface</b>
@@ -40,11 +42,9 @@ require_relative "service/version"
40
42
  # TODO: why a Hash? It feels much better if Simple::Service.actions returns an array of names.
41
43
  #
42
44
  #
43
- # 3. <em>Invoke a service:</em> run <tt>Simple::Service.invoke3</tt> or <tt>Simple::Service.invoke</tt>. You must set a context first.
45
+ # 3. <em>Invoke a service:</em> run <tt>Simple::Service.invoke3</tt> or <tt>Simple::Service.invoke</tt>.
44
46
  #
45
- # Simple::Service.with_context do
46
- # Simple::Service.invoke3(GodMode, :build_universe, "TestWorld", c: 1e9)
47
- # end
47
+ # Simple::Service.invoke3(GodMode, :build_universe, "TestWorld", c: 1e9)
48
48
  # => 42
49
49
  #
50
50
  module Simple::Service
@@ -61,28 +61,16 @@ module Simple::Service
61
61
  klass.include ServiceExpectations
62
62
  end
63
63
 
64
- def self.logger
65
- @logger ||= ::Logger.new(STDOUT)
66
- end
67
-
68
- def self.logger=(logger)
69
- @logger = logger
70
- end
71
-
72
- # returns true if the passed in object is a service module.
73
- #
74
- # A service must be a module, and it must include the Simple::Service module.
75
- def self.service?(service)
76
- verify_service! service
77
- true
78
- rescue ::ArgumentError
79
- false
80
- end
81
-
82
- # Raises an error if the passed in object is not a service
64
+ # Raises an error if the passed in object is not a Simple::Service
83
65
  def self.verify_service!(service) # @private
84
- raise ::ArgumentError, "#{service.inspect} must be a Simple::Service, but is not even a Module" unless service.is_a?(Module)
85
- raise ::ArgumentError, "#{service.inspect} must be a Simple::Service, did you 'include Simple::Service'" unless service.include?(self)
66
+ expect! service => Module
67
+
68
+ # rubocop:disable Style/GuardClause
69
+ unless service.include?(::Simple::Service)
70
+ raise ::ArgumentError,
71
+ "#{service.name} is not a Simple::Service, did you 'include Simple::Service'"
72
+ end
73
+ # rubocop:enable Style/GuardClause
86
74
  end
87
75
 
88
76
  # returns a Hash with all actions in the +service+ module
@@ -94,6 +82,8 @@ module Simple::Service
94
82
 
95
83
  # returns the action with the given name.
96
84
  def self.action(service, name)
85
+ expect! name => Symbol
86
+
97
87
  actions = self.actions(service)
98
88
  actions[name] || begin
99
89
  raise ::Simple::Service::NoSuchAction.new(service, name)
@@ -107,10 +97,8 @@ module Simple::Service
107
97
  #
108
98
  # As the main purpose of this module is to call services with outside data,
109
99
  # the +.invoke+ action is usually preferred.
110
- #
111
- # *Note:* You cannot call this method if the context is not set.
112
100
  def self.invoke3(service, name, *args, **flags)
113
- flags = flags.each_with_object({}) { |(k, v), hsh| hsh[k.to_s] = v }
101
+ flags = flags.transform_keys(&:to_s)
114
102
  invoke service, name, args: args, flags: flags
115
103
  end
116
104
 
@@ -154,14 +142,10 @@ module Simple::Service
154
142
  # by the method we raise an ArgumentError.
155
143
  #
156
144
  # Entries in the +named_args+ Hash that are not defined in the action itself are ignored.
157
-
158
- # <b>Note:</b> You cannot call this method if the context is not set.
159
145
  def self.invoke(service, name, args: {}, flags: {})
160
- raise ContextMissingError, "Need to set context before calling ::Simple::Service.invoke3" unless context
161
-
162
146
  expect! args => [Hash, Array], flags: Hash
163
- args.keys.each { |key| expect! key => String } if args.is_a?(Hash)
164
- flags.keys.each { |key| expect! key => String }
147
+ args.each_key { |key| expect! key => String } if args.is_a?(Hash)
148
+ flags.each_key { |key| expect! key => String }
165
149
 
166
150
  action(service, name).invoke(args: args, flags: flags)
167
151
  end
@@ -0,0 +1,105 @@
1
+ require "simple/immutable"
2
+
3
+ module Simple::Workflow
4
+ # A context object
5
+ #
6
+ # Each service executes with a current context. The system manages a stack of
7
+ # contexts; whenever a service execution is done the current context is reverted
8
+ # to its previous value.
9
+ #
10
+ # A context object can store a large number of values; the only way to set or
11
+ # access a value is via getters and setters. These are implemented via
12
+ # +method_missing(..)+.
13
+ #
14
+ # Also, once a value is set in the context it is not possible to change or
15
+ # unset it.
16
+ class Context < Simple::Immutable
17
+ SELF = self
18
+
19
+ # returns a new Context object.
20
+ #
21
+ # Parameters:
22
+ #
23
+ # - hsh (Hash or nil): sets values for this context
24
+ # - previous_context (Context or nil): if +previous_context+ is provided,
25
+ # values that are not defined in the \a +hsh+ argument are read from the
26
+ # +previous_context+ instead (or the previous context's +previous_context+,
27
+ # etc.)
28
+ def initialize(hsh, previous_context = nil)
29
+ expect! hsh => [Hash, nil]
30
+ expect! previous_context => [SELF, nil]
31
+
32
+ @previous_context = previous_context
33
+ super(hsh || {})
34
+ end
35
+
36
+ def reload!(a_module)
37
+ if @previous_context
38
+ @previous_context.reload!(a_module)
39
+ return a_module
40
+ end
41
+
42
+ @reloaded_modules ||= []
43
+ return if @reloaded_modules.include?(a_module)
44
+
45
+ ::Simple::Workflow::Reloader.reload(a_module)
46
+ @reloaded_modules << a_module
47
+ a_module
48
+ end
49
+
50
+ def fetch_attribute!(sym, raise_when_missing:)
51
+ unless @previous_context
52
+ return super(sym, raise_when_missing: raise_when_missing)
53
+ end
54
+
55
+ first_error = nil
56
+
57
+ # check this context first. We catch any NameError, to be able to look up
58
+ # the attribute also in the previous_context.
59
+ begin
60
+ return super(sym, raise_when_missing: true)
61
+ rescue NameError => e
62
+ first_error = e
63
+ end
64
+
65
+ # check previous_context
66
+ begin
67
+ return @previous_context.fetch_attribute!(sym, raise_when_missing: raise_when_missing)
68
+ rescue NameError
69
+ :nop
70
+ end
71
+
72
+ # Not in +self+, not in +previous_context+, and +raise_when_missing+ is true:
73
+ raise(first_error)
74
+ end
75
+
76
+ # def inspect
77
+ # if @previous_context
78
+ # "#{object_id} [" + @hsh.keys.map(&:inspect).join(", ") + "; #{@previous_context.inspect}]"
79
+ # else
80
+ # "#{object_id} [" + @hsh.keys.map(&:inspect).join(", ") + "]"
81
+ # end
82
+ # end
83
+
84
+ private
85
+
86
+ IDENTIFIER = "[a-z_][a-z0-9_]*" # @private
87
+
88
+ def method_missing(sym, *args, &block)
89
+ raise ArgumentError, "#{self.class.name}##{sym}: Block given" if block
90
+ raise ArgumentError, "#{self.class.name}##{sym}: Extra args #{args.inspect}" unless args.empty?
91
+
92
+ if sym !~ /\A(#{IDENTIFIER})(\?)?\z/
93
+ raise ArgumentError, "#{self.class.name}: Invalid context key '#{sym}'"
94
+ end
95
+
96
+ # rubocop:disable Lint/OutOfRangeRegexpRef
97
+ fetch_attribute!($1, raise_when_missing: $2.nil?)
98
+ # rubocop:enable Lint/OutOfRangeRegexpRef
99
+ end
100
+
101
+ def respond_to_missing?(sym, include_private = false)
102
+ super || @previous_context&.respond_to_missing?(sym, include_private)
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,33 @@
1
+ module Simple::Workflow
2
+ module CurrentContext
3
+ # Returns the current context.
4
+ #
5
+ # This method never returns nil - it raises a ContextMissingError exception if
6
+ # the context was not initialized (via <tt>Simple::Workflow.with_context</tt>).
7
+ def current_context
8
+ Thread.current[:"Simple::Workflow.current_context"] || raise(ContextMissingError)
9
+ end
10
+
11
+ # Returns a logger
12
+ #
13
+ # Returns a logger if a context is set and contains a logger.
14
+ def logger
15
+ current_context = Thread.current[:"Simple::Workflow.current_context"]
16
+ current_context&.logger?
17
+ end
18
+
19
+ # yields a block with a given context, and restores the previous context
20
+ # object afterwards.
21
+ def with_context(ctx = nil, &block)
22
+ old_ctx = Thread.current[:"Simple::Workflow.current_context"]
23
+
24
+ Thread.current[:"Simple::Workflow.current_context"] = Context.new(ctx, old_ctx)
25
+
26
+ block.call
27
+ ensure
28
+ Thread.current[:"Simple::Workflow.current_context"] = old_ctx
29
+ end
30
+ end
31
+
32
+ extend CurrentContext
33
+ end
@@ -0,0 +1,84 @@
1
+ # The Simple::Workflow::Reloader provides a way to locate and reload a module
2
+ module Simple::Workflow::Reloader
3
+ extend self
4
+
5
+ def reload(a_module)
6
+ source_paths = locate(a_module)
7
+ if source_paths.empty?
8
+ logger&.warn "#{a_module}: cannot reload module: cannot find sources"
9
+ return
10
+ end
11
+
12
+ source_paths.each do |source_path|
13
+ logger&.debug "#{a_module}: reload #{source_path}"
14
+ end
15
+
16
+ logger&.info "#{a_module}: reloaded module"
17
+ end
18
+
19
+ # This method tries to identify source files for a module's functions.
20
+ def locate(a_module)
21
+ expect! a_module => Module
22
+
23
+ @registered_source_paths ||= {}
24
+ @registered_source_paths[a_module.name] ||= locate_source_paths(a_module)
25
+ end
26
+
27
+ private
28
+
29
+ def logger
30
+ ::Simple::Workflow.logger
31
+ end
32
+
33
+ def locate_source_paths(a_module)
34
+ source_paths = []
35
+
36
+ source_paths.concat locate_by_instance_methods(a_module)
37
+ source_paths.concat locate_by_methods(a_module)
38
+ source_paths.concat locate_by_name(a_module)
39
+
40
+ source_paths.uniq
41
+ end
42
+
43
+ def locate_by_instance_methods(a_module)
44
+ method_names = a_module.instance_methods(false)
45
+ methods = method_names.map { |sym| a_module.instance_method(sym) }
46
+ methods.map(&:source_location).map(&:first)
47
+ end
48
+
49
+ def locate_by_methods(a_module)
50
+ method_names = a_module.methods(false)
51
+ methods = method_names.map { |sym| a_module.method(sym) }
52
+ methods.map(&:source_location).map(&:first)
53
+ end
54
+
55
+ def locate_by_name(a_module)
56
+ source_file = "#{underscore(a_module.name)}.rb"
57
+
58
+ $LOAD_PATH.each do |dir|
59
+ full_path = File.join(dir, source_file)
60
+ return [full_path] if File.exist?(full_path)
61
+ end
62
+
63
+ []
64
+ end
65
+
66
+ # Makes an underscored, lowercase form from the expression in the string.
67
+ #
68
+ # Changes '::' to '/' to convert namespaces to paths.
69
+ #
70
+ # This is copied and slightly changed (we don't support any custom
71
+ # inflections) from activesupport's lib/active_support/inflector/methods.rb
72
+ #
73
+ def underscore(camel_cased_word)
74
+ return camel_cased_word unless /[A-Z-]|::/.match?(camel_cased_word)
75
+
76
+ word = camel_cased_word.to_s.gsub("::", "/")
77
+
78
+ word.gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2')
79
+ word.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
80
+ word.tr!("-", "_")
81
+ word.downcase!
82
+ word
83
+ end
84
+ end
@@ -0,0 +1,15 @@
1
+ module ::Simple::Workflow::RSpecHelper
2
+ def self.included(base)
3
+ base.let(:current_context) { {} }
4
+
5
+ base.around do |example|
6
+ if (ctx = current_context)
7
+ ::Simple::Workflow.with_context(ctx) do
8
+ example.run
9
+ end
10
+ else
11
+ example.run
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,96 @@
1
+ require "simple/service"
2
+
3
+ require_relative "workflow/context"
4
+ require_relative "workflow/current_context"
5
+ require_relative "workflow/reloader"
6
+
7
+ if defined?(RSpec)
8
+ require_relative "workflow/rspec_helper"
9
+ end
10
+
11
+ module Simple::Workflow
12
+ class ContextMissingError < ::StandardError
13
+ def to_s
14
+ "Simple::Workflow.current_context not initialized; remember to call Simple::Workflow.with_context/1"
15
+ end
16
+ end
17
+
18
+ module HelperMethods
19
+ def invoke(*args, **kwargs)
20
+ Simple::Workflow.invoke(self, *args, **kwargs)
21
+ end
22
+ end
23
+
24
+ module InstanceHelperMethods
25
+ private
26
+
27
+ def current_context
28
+ Simple::Workflow.current_context
29
+ end
30
+ end
31
+
32
+ module ModuleMethods
33
+ def register_workflow(mod)
34
+ expect! mod => Module
35
+
36
+ mod.extend ::Simple::Workflow::HelperMethods
37
+ mod.include ::Simple::Workflow::InstanceHelperMethods
38
+ mod.extend mod
39
+ mod.include Simple::Service
40
+ end
41
+
42
+ def reload_on_invocation?
43
+ !!@reload_on_invocation
44
+ end
45
+
46
+ def reload_on_invocation!
47
+ @reload_on_invocation = true
48
+ end
49
+
50
+ def invoke(workflow, *args, **kwargs)
51
+ # This call to Simple::Workflow.current_context raises a ContextMissingError
52
+ # if the context is not set.
53
+ _ = ::Simple::Workflow.current_context
54
+
55
+ expect! workflow => [Module, String]
56
+
57
+ workflow_module = lookup_workflow!(workflow)
58
+
59
+ # We will reload workflow modules only once per invocation.
60
+ if Simple::Workflow.reload_on_invocation?
61
+ Simple::Workflow.current_context.reload!(workflow_module)
62
+ end
63
+
64
+ Simple::Service.invoke(workflow_module, :call, args: args, flags: kwargs.transform_keys(&:to_s))
65
+ end
66
+
67
+ private
68
+
69
+ def lookup_workflow!(workflow)
70
+ workflow_module = lookup_workflow(workflow)
71
+
72
+ verify_workflow! workflow_module
73
+
74
+ workflow_module
75
+ end
76
+
77
+ def lookup_workflow(workflow)
78
+ case workflow
79
+ when Module
80
+ workflow
81
+ when String
82
+ Object.const_get workflow
83
+ else
84
+ expect! workflow => [Module, String]
85
+ end
86
+ end
87
+
88
+ def verify_workflow!(workflow_module)
89
+ return if Simple::Service.actions(workflow_module).key?(:call)
90
+
91
+ raise ArgumentError, "#{workflow_module.name} is not a Simple::Workflow"
92
+ end
93
+ end
94
+
95
+ extend ModuleMethods
96
+ end
@@ -0,0 +1,3 @@
1
+ # rubocop:disable Naming/FileName
2
+
3
+ require "simple/workflow"
data/scripts/test ADDED
@@ -0,0 +1,2 @@
1
+ #!/bin/bash
2
+ rspec "$@"
@@ -22,4 +22,5 @@ Gem::Specification.new do |gem|
22
22
  gem.required_ruby_version = '~> 2.5'
23
23
 
24
24
  gem.add_dependency "expectation", "~> 1"
25
+ gem.add_dependency "simple-immutable", "~> 1", ">= 1.1"
25
26
  end
@@ -3,14 +3,6 @@
3
3
  require "spec_helper"
4
4
 
5
5
  describe "Simple::Service.invoke3" do
6
- # the context to use in the around hook below. By default this is nil -
7
- # which gives us an empty context.
8
- let(:context) { nil }
9
-
10
- around do |example|
11
- ::Simple::Service.with_context(context) { example.run }
12
- end
13
-
14
6
  let(:service) { InvokeTestService }
15
7
  let(:action) { nil }
16
8
 
@@ -1,16 +1,7 @@
1
1
  # rubocop:disable Style/WordArray
2
-
3
2
  require "spec_helper"
4
3
 
5
4
  describe "Simple::Service.invoke" do
6
- # the context to use in the around hook below. By default this is nil -
7
- # which gives us an empty context.
8
- let(:context) { nil }
9
-
10
- around do |example|
11
- ::Simple::Service.with_context(context) { example.run }
12
- end
13
-
14
5
  let(:service) { InvokeTestService }
15
6
  let(:action) { nil }
16
7
 
@@ -6,12 +6,6 @@ describe "Simple::Service" do
6
6
  context "when running against a NoService module" do
7
7
  let(:service) { NoServiceModule }
8
8
 
9
- describe ".service?" do
10
- it "returns false on a NoService module" do
11
- expect(Simple::Service.service?(service)).to eq(false)
12
- end
13
- end
14
-
15
9
  describe ".actions" do
16
10
  it "raises an argument error" do
17
11
  expect { Simple::Service.actions(service) }.to raise_error(ArgumentError)
@@ -26,9 +20,7 @@ describe "Simple::Service" do
26
20
 
27
21
  describe ".invoke3" do
28
22
  it "raises an argument error" do
29
- ::Simple::Service.with_context do
30
- expect { Simple::Service.invoke3(service, :service1, {}, {}, context: nil) }.to raise_error(::ArgumentError)
31
- end
23
+ expect { Simple::Service.invoke3(service, :service1, {}, {}, context: nil) }.to raise_error(::ArgumentError)
32
24
  end
33
25
  end
34
26
  end
@@ -36,12 +28,6 @@ describe "Simple::Service" do
36
28
  # running against a proper service module
37
29
  let(:service) { SpecService }
38
30
 
39
- describe ".service?" do
40
- it "returns true" do
41
- expect(Simple::Service.service?(service)).to eq(true)
42
- end
43
- end
44
-
45
31
  describe ".actions" do
46
32
  it "returns a Hash of actions on a Service module" do
47
33
  actions = Simple::Service.actions(service)
@@ -83,26 +69,11 @@ describe "Simple::Service" do
83
69
  Simple::Service.invoke3(service, :service1, "my_a", "my_b", d: "my_d")
84
70
  end
85
71
 
86
- context "when context is not set" do
87
- it "raises a ContextMissingError" do
88
- action = Simple::Service.actions(service)[:service1]
89
- expect(action).not_to receive(:invoke)
90
-
91
- expect do
92
- invoke3
93
- end.to raise_error(::Simple::Service::ContextMissingError)
94
- end
95
- end
96
-
97
- context "when context is set" do
98
- it "calls Action#invoke with the right arguments" do
99
- action = Simple::Service.actions(service)[:service1]
100
- expect(action).to receive(:invoke).with(args: ["my_a", "my_b"], flags: { "d" => "my_d" })
72
+ it "calls Action#invoke with the right arguments" do
73
+ action = Simple::Service.actions(service)[:service1]
74
+ expect(action).to receive(:invoke).with(args: ["my_a", "my_b"], flags: { "d" => "my_d" })
101
75
 
102
- ::Simple::Service.with_context do
103
- invoke3
104
- end
105
- end
76
+ invoke3
106
77
  end
107
78
  end
108
79
 
@@ -112,26 +83,11 @@ describe "Simple::Service" do
112
83
  Simple::Service.invoke(service, :service1, args: ["my_a", "my_b"], flags: { "d" => "my_d" })
113
84
  end
114
85
 
115
- context "when context is not set" do
116
- it "raises a ContextMissingError" do
117
- action = Simple::Service.actions(service)[:service1]
118
- expect(action).not_to receive(:invoke)
119
-
120
- expect do
121
- invoke
122
- end.to raise_error(::Simple::Service::ContextMissingError)
123
- end
124
- end
125
-
126
- context "when context is set" do
127
- it "calls Action#invoke with the right arguments" do
128
- action = Simple::Service.actions(service)[:service1]
129
- expect(action).to receive(:invoke).with(args: ["my_a", "my_b"], flags: { "d" => "my_d" }).and_call_original
86
+ it "calls Action#invoke with the right arguments" do
87
+ action = Simple::Service.actions(service)[:service1]
88
+ expect(action).to receive(:invoke).with(args: ["my_a", "my_b"], flags: { "d" => "my_d" }).and_call_original
130
89
 
131
- ::Simple::Service.with_context do
132
- invoke
133
- end
134
- end
90
+ invoke
135
91
  end
136
92
  end
137
93
  end
@@ -147,12 +103,11 @@ describe "Simple::Service" do
147
103
 
148
104
  it "calls Action#invoke with the right arguments" do
149
105
  expected = ["bar-value", "baz-value"]
150
- ::Simple::Service.with_context do
151
- expect(invoke3("bar-value", baz: "baz-value")).to eq(expected)
152
- expect(invoke3(bar: "bar-value", baz: "baz-value")).to eq(expected)
153
- expect(invoke(args: ["bar-value"], flags: { "baz" => "baz-value" })).to eq(expected)
154
- expect(invoke(args: { "bar" => "bar-value", "baz" => "baz-value" })).to eq(expected)
155
- end
106
+
107
+ expect(invoke3("bar-value", baz: "baz-value")).to eq(expected)
108
+ expect(invoke3(bar: "bar-value", baz: "baz-value")).to eq(expected)
109
+ expect(invoke(args: ["bar-value"], flags: { "baz" => "baz-value" })).to eq(expected)
110
+ expect(invoke(args: { "bar" => "bar-value", "baz" => "baz-value" })).to eq(expected)
156
111
  end
157
112
  end
158
113
  end
@@ -0,0 +1,90 @@
1
+ require "spec_helper"
2
+
3
+ describe Simple::Workflow::Context do
4
+ RSpec.shared_examples "context requesting" do
5
+ it "inherits from Simple::Immutable" do
6
+ expect(context).to be_a(Simple::Immutable)
7
+ end
8
+
9
+ describe "checking for identifier" do
10
+ it "raises an ArgumentError if the key is invalid" do
11
+ expect { context.oneTwoThree? }.to raise_error(ArgumentError)
12
+ end
13
+
14
+ it "returns nil if the key is not set" do
15
+ expect(context.two?).to be_nil
16
+ end
17
+
18
+ it "returns the value if the key is set" do
19
+ expect(context.one?).to eq 1
20
+ end
21
+ end
22
+
23
+ describe "fetching identifier" do
24
+ it "raises an ArgumentError if the identifier is invalid" do
25
+ expect { context.one! }.to raise_error(ArgumentError)
26
+ expect { context.oneTwoThree }.to raise_error(ArgumentError)
27
+ end
28
+
29
+ it "raises a NoMethodError if the key is not set" do
30
+ expect { context.two }.to raise_error(NameError)
31
+ end
32
+
33
+ it "returns the value if the key is set" do
34
+ expect(context.one).to eq 1
35
+ end
36
+ end
37
+
38
+ describe "#merge" do
39
+ context "with symbolized keys" do
40
+ it "sets a value if it does not exist yet" do
41
+ expect(context.two?).to eq(nil)
42
+ new_context = Simple::Workflow::Context.new({two: 2}, context)
43
+ expect(new_context.two).to eq(2)
44
+
45
+ # doesn't change the source context
46
+ expect(context.two?).to eq(nil)
47
+ end
48
+
49
+ it "sets a value if it does exist" do
50
+ new_context = Simple::Workflow::Context.new({one: 2}, context)
51
+ expect(new_context.one).to eq(2)
52
+
53
+ # doesn't change the source context
54
+ expect(context.one).to eq(1)
55
+ end
56
+ end
57
+
58
+ context "with stringified keys" do
59
+ it "sets a value if it does not exist yet" do
60
+ expect(context.two?).to eq(nil)
61
+ new_context = Simple::Workflow::Context.new({"two" => 2}, context)
62
+ expect(new_context.two).to eq(2)
63
+
64
+ # doesn't change the source context
65
+ expect(context.two?).to eq(nil)
66
+ end
67
+
68
+ it "sets a value if it does exist" do
69
+ new_context = Simple::Workflow::Context.new({"one" => 2}, context)
70
+ expect(new_context.one).to eq(2)
71
+
72
+ # doesn't change the source context
73
+ expect(context.one).to eq(1)
74
+ end
75
+ end
76
+ end
77
+ end
78
+
79
+ context "with a symbolized context" do
80
+ let(:context) { Simple::Workflow::Context.new(one: 1) }
81
+
82
+ it_behaves_like "context requesting"
83
+ end
84
+
85
+ context "with a stringified context" do
86
+ let(:context) { Simple::Workflow::Context.new("one" => 1) }
87
+
88
+ it_behaves_like "context requesting"
89
+ end
90
+ end
@@ -0,0 +1,41 @@
1
+ require "spec_helper"
2
+
3
+ describe Simple::Workflow::CurrentContext do
4
+ describe ".with_context" do
5
+ it "merges the current context for the duration of the block" do
6
+ block_called = false
7
+
8
+ Simple::Workflow.with_context(a: "a", foo: "bar") do
9
+ expect(Simple::Workflow.current_context.a).to eq("a")
10
+ expect(Simple::Workflow.current_context.foo).to eq("bar")
11
+
12
+ # layering
13
+ Simple::Workflow.with_context() do
14
+ expect(Simple::Workflow.current_context.foo).to eq("bar")
15
+
16
+ expect(Simple::Workflow.current_context.unknown?).to be_nil
17
+ expect {
18
+ Simple::Workflow.current_context.unknown
19
+ }.to raise_error(NameError)
20
+ end
21
+
22
+ # overwrite value
23
+ Simple::Workflow.with_context(a: "b") do
24
+ expect(Simple::Workflow.current_context.a).to eq("b")
25
+ block_called = true
26
+ end
27
+
28
+ # overwrite value w/nil
29
+ Simple::Workflow.with_context(a: nil) do
30
+ expect(Simple::Workflow.current_context.a).to be_nil
31
+ Simple::Workflow.with_context(a: "c") do
32
+ expect(Simple::Workflow.current_context.a).to eq("c")
33
+ end
34
+ end
35
+ expect(Simple::Workflow.current_context.a).to eq("a")
36
+ end
37
+
38
+ expect(block_called).to eq(true)
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,10 @@
1
+ module ReloaderSpecExample1
2
+ def foo; end
3
+ end
4
+
5
+ module ReloaderSpecExample2
6
+ end
7
+
8
+ module ReloaderSpecExample3
9
+ def foo; end
10
+ end
@@ -0,0 +1,7 @@
1
+ module ReloaderSpecExample1
2
+ def self.bar; end
3
+ end
4
+
5
+ module ReloaderSpecExample2
6
+ def self.bar; end
7
+ end
@@ -0,0 +1,48 @@
1
+ require "spec_helper"
2
+
3
+ require_relative "reloader_spec/example1"
4
+ require_relative "reloader_spec/example2"
5
+
6
+ describe "Simple::Service::Reloader" do
7
+ describe ".locate" do
8
+ def locate(mod)
9
+ Simple::Workflow::Reloader.locate(mod)
10
+ end
11
+
12
+ it "Returns all source files of a module" do
13
+ root = Dir.getwd
14
+
15
+ expected = [
16
+ "#{root}/lib/simple/workflow/reloader.rb"
17
+ ]
18
+ expect(locate(Simple::Workflow::Reloader)).to contain_exactly(*expected)
19
+
20
+ expected = [
21
+ "#{__dir__}/reloader_spec/example1.rb",
22
+ "#{__dir__}/reloader_spec/example2.rb"
23
+ ]
24
+ expect(locate(ReloaderSpecExample1)).to contain_exactly(*expected)
25
+
26
+ expected = [
27
+ "#{__dir__}/reloader_spec/example2.rb"
28
+ ]
29
+ expect(locate(ReloaderSpecExample2)).to contain_exactly(*expected)
30
+
31
+ expected = [
32
+ "#{__dir__}/reloader_spec/example1.rb"
33
+ ]
34
+ expect(locate(ReloaderSpecExample3)).to contain_exactly(*expected)
35
+ end
36
+ end
37
+
38
+ describe ".reload" do
39
+ def reload(mod)
40
+ Simple::Workflow::Reloader.reload(mod)
41
+ end
42
+
43
+ it "Reloads a module" do
44
+ # [TODO] this doesn't really check reloading...
45
+ reload Simple::Workflow::Reloader
46
+ end
47
+ end
48
+ end
data/spec/spec_helper.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  ENV["RACK_ENV"] = "test"
2
2
  ENV["RAILS_ENV"] = "test"
3
3
 
4
- require "byebug"
4
+ require "pry-byebug"
5
5
  require "rspec"
6
6
 
7
7
  require "simplecov"
@@ -18,6 +18,7 @@ SimpleCov.start do
18
18
  end
19
19
 
20
20
  require "simple/service"
21
+ require "simple/workflow"
21
22
 
22
23
  Dir.glob("./spec/support/**/*.rb").sort.each { |path| load path }
23
24
 
@@ -1,5 +1,3 @@
1
- # rubocop:disable Naming/UncommunicativeMethodParamName
2
-
3
1
  module NoServiceModule
4
2
  end
5
3
 
@@ -53,6 +51,6 @@ module SpecTestService
53
51
  include Simple::Service
54
52
 
55
53
  def foo(bar, baz:)
56
- [ bar, baz ]
54
+ [bar, baz]
57
55
  end
58
56
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: simple-service
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.6
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - radiospiel
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-12-25 00:00:00.000000000 Z
11
+ date: 2021-10-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: expectation
@@ -24,6 +24,26 @@ dependencies:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: '1'
27
+ - !ruby/object:Gem::Dependency
28
+ name: simple-immutable
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1'
34
+ - - ">="
35
+ - !ruby/object:Gem::Version
36
+ version: '1.1'
37
+ type: :runtime
38
+ prerelease: false
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - "~>"
42
+ - !ruby/object:Gem::Version
43
+ version: '1'
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '1.1'
27
47
  description: Pretty simple and somewhat abstract service description
28
48
  email: eno@radiospiel.org
29
49
  executables: []
@@ -75,26 +95,36 @@ files:
75
95
  - doc/method_list.html
76
96
  - doc/top-level-namespace.html
77
97
  - lib/simple-service.rb
98
+ - lib/simple-workflow.rb
78
99
  - lib/simple/service.rb
79
100
  - lib/simple/service/action.rb
80
101
  - lib/simple/service/action/comment.rb
81
102
  - lib/simple/service/action/method_reflection.rb
82
103
  - lib/simple/service/action/parameter.rb
83
- - lib/simple/service/context.rb
84
104
  - lib/simple/service/errors.rb
85
105
  - lib/simple/service/version.rb
106
+ - lib/simple/workflow.rb
107
+ - lib/simple/workflow/context.rb
108
+ - lib/simple/workflow/current_context.rb
109
+ - lib/simple/workflow/reloader.rb
110
+ - lib/simple/workflow/rspec_helper.rb
86
111
  - log/.gitkeep
87
112
  - scripts/release
88
113
  - scripts/release.rb
89
114
  - scripts/stats
115
+ - scripts/test
90
116
  - scripts/watch
91
117
  - simple-service.gemspec
92
118
  - spec/simple/service/action_invoke3_spec.rb
93
119
  - spec/simple/service/action_invoke_spec.rb
94
120
  - spec/simple/service/action_spec.rb
95
- - spec/simple/service/context_spec.rb
96
121
  - spec/simple/service/service_spec.rb
97
122
  - spec/simple/service/version_spec.rb
123
+ - spec/simple/workflow/context_spec.rb
124
+ - spec/simple/workflow/current_context_spec.rb
125
+ - spec/simple/workflow/reloader_spec.rb
126
+ - spec/simple/workflow/reloader_spec/example1.rb
127
+ - spec/simple/workflow/reloader_spec/example2.rb
98
128
  - spec/spec_helper.rb
99
129
  - spec/support/spec_services.rb
100
130
  homepage: http://github.com/radiospiel/simple-service
@@ -115,7 +145,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
115
145
  - !ruby/object:Gem::Version
116
146
  version: '0'
117
147
  requirements: []
118
- rubygems_version: 3.0.6
148
+ rubygems_version: 3.1.4
119
149
  signing_key:
120
150
  specification_version: 4
121
151
  summary: Pretty simple and somewhat abstract service description
@@ -123,8 +153,12 @@ test_files:
123
153
  - spec/simple/service/action_invoke3_spec.rb
124
154
  - spec/simple/service/action_invoke_spec.rb
125
155
  - spec/simple/service/action_spec.rb
126
- - spec/simple/service/context_spec.rb
127
156
  - spec/simple/service/service_spec.rb
128
157
  - spec/simple/service/version_spec.rb
158
+ - spec/simple/workflow/context_spec.rb
159
+ - spec/simple/workflow/current_context_spec.rb
160
+ - spec/simple/workflow/reloader_spec.rb
161
+ - spec/simple/workflow/reloader_spec/example1.rb
162
+ - spec/simple/workflow/reloader_spec/example2.rb
129
163
  - spec/spec_helper.rb
130
164
  - spec/support/spec_services.rb
@@ -1,94 +0,0 @@
1
- module Simple::Service
2
- # Returns the current context.
3
- def self.context
4
- Thread.current[:"Simple::Service.context"]
5
- end
6
-
7
- # yields a block with a given context, and restores the previous context
8
- # object afterwards.
9
- def self.with_context(ctx = nil, &block)
10
- old_ctx = Thread.current[:"Simple::Service.context"]
11
- new_ctx = old_ctx ? old_ctx.merge(ctx) : Context.new(ctx)
12
-
13
- Thread.current[:"Simple::Service.context"] = new_ctx
14
-
15
- block.call
16
- ensure
17
- Thread.current[:"Simple::Service.context"] = old_ctx
18
- end
19
- end
20
-
21
- module Simple::Service
22
- # A context object
23
- #
24
- # Each service executes with a current context. The system manages a stack of
25
- # contexts; whenever a service execution is done the current context is reverted
26
- # to its previous value.
27
- #
28
- # A context object can store a large number of values; the only way to set or
29
- # access a value is via getters and setters. These are implemented via
30
- # +method_missing(..)+.
31
- #
32
- # Also, once a value is set in the context it is not possible to change or
33
- # unset it.
34
- class Context
35
- def initialize(hsh = nil) # @private
36
- @hsh = hsh || {}
37
- end
38
-
39
- # returns a new Context object, which merges the values in the +overlay+
40
- # argument (which must be a Hash or nil) with the values in this context.
41
- #
42
- # The overlay is allowed to change values in the current context.
43
- #
44
- # It does not change this context.
45
- def merge(overlay)
46
- expect! overlay => [Hash, nil]
47
-
48
- overlay ||= {}
49
- new_context_hsh = @hsh.merge(overlay)
50
- ::Simple::Service::Context.new(new_context_hsh)
51
- end
52
-
53
- private
54
-
55
- IDENTIFIER_PATTERN = "[a-z][a-z0-9_]*" # @private
56
- IDENTIFIER_REGEXP = Regexp.compile("\\A#{IDENTIFIER_PATTERN}\\z") # @private
57
- ASSIGNMENT_REGEXP = Regexp.compile("\\A(#{IDENTIFIER_PATTERN})=\\z") # @private
58
-
59
- def method_missing(sym, *args, &block)
60
- raise ArgumentError, "Block given" if block
61
-
62
- if args.count == 0 && sym =~ IDENTIFIER_REGEXP
63
- self[sym]
64
- elsif args.count == 1 && sym =~ ASSIGNMENT_REGEXP
65
- self[$1.to_sym] = args.first
66
- else
67
- super
68
- end
69
- end
70
-
71
- def respond_to_missing?(sym, include_private = false)
72
- # :nocov:
73
- return true if IDENTIFIER_REGEXP.match?(sym)
74
- return true if ASSIGNMENT_REGEXP.match?(sym)
75
-
76
- super
77
- # :nocov:
78
- end
79
-
80
- def [](key)
81
- @hsh[key]
82
- end
83
-
84
- def []=(key, value)
85
- existing_value = @hsh[key]
86
-
87
- unless existing_value.nil? || existing_value == value
88
- raise ::Simple::Service::ContextReadOnlyError, key
89
- end
90
-
91
- @hsh[key] = value
92
- end
93
- end
94
- end
@@ -1,69 +0,0 @@
1
- require "spec_helper"
2
-
3
- describe Simple::Service do
4
- describe ".with_context" do
5
- it "merges the current context for the duration of the block" do
6
- block_called = false
7
-
8
- Simple::Service.with_context(a: "a") do
9
- expect(Simple::Service.context.a).to eq("a")
10
-
11
- # overwrite value
12
- Simple::Service.with_context(a: "b") do
13
- expect(Simple::Service.context.a).to eq("b")
14
- block_called = true
15
- end
16
-
17
- # overwrite value w/nil
18
- Simple::Service.with_context(a: nil) do
19
- expect(Simple::Service.context.a).to be_nil
20
- Simple::Service.context.a = "c"
21
- expect(Simple::Service.context.a).to eq("c")
22
- end
23
- expect(Simple::Service.context.a).to eq("a")
24
- end
25
-
26
- expect(block_called).to eq(true)
27
- end
28
- end
29
- end
30
-
31
- describe Simple::Service::Context do
32
- let(:context) { Simple::Service::Context.new }
33
-
34
- before do
35
- context.one = 1
36
- end
37
-
38
- describe "invalid identifier" do
39
- it "raises an error" do
40
- expect { context.one! }.to raise_error(NoMethodError)
41
- end
42
- end
43
-
44
- describe "context reading" do
45
- it "returns a value if set" do
46
- expect(context.one).to eq(1)
47
- end
48
-
49
- it "returns nil if not set" do
50
- expect(context.two).to be_nil
51
- end
52
- end
53
-
54
- describe "context writing" do
55
- it "sets a value if it does not exist yet" do
56
- context.two = 2
57
- expect(context.two).to eq(2)
58
- end
59
-
60
- it "raises a ReadOnly error if the value exists and is not equal" do
61
- expect { context.one = 2 }.to raise_error(::Simple::Service::ContextReadOnlyError)
62
- end
63
-
64
- it "sets the value if it exists and is equal" do
65
- context.one = 1
66
- expect(context.one).to eq(1)
67
- end
68
- end
69
- end