simple-service 0.1.6 → 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 903d43626ea1853a44478fd58d6aef0a4e86374745e9a5732e127ab85fcd4437
4
- data.tar.gz: ed5f2caa57468945b72bcf805c0ff3e2afd7df654ea03387a59fa16ea136ad19
3
+ metadata.gz: 6532ece44cb9ef6df3dab9073552ef9f35df38910395f76d9c0d67ec62bb56bc
4
+ data.tar.gz: a9d32157d67e9d97884ae46928183cba08c88b85157bc742e4632ea78d536efa
5
5
  SHA512:
6
- metadata.gz: 0ab49e773b69b41e844a73da66f7ab06ca90f0d4edc097a4d44bc8457272040acd428fc37e3128a6e662f18bab30fb6d0e8a396c40eed6eccb2dd316171c5b1a
7
- data.tar.gz: b06385441ec3d0ce87aba8095e30cb7ab07f871c6507ae133b611eab0f370458c378d5f1ffd27111221b3ca75096ca0ff41bc1c6b61ab1f40cfebe45b1a5ac81
6
+ metadata.gz: c70f870e322d3927d4cc4e23f7a71913e8a455ed0ddf8e157ab37d3b29017c3727f967e9457172daf282fd91d683585db23a1c515a203e60620a11cb0d0990a9
7
+ data.tar.gz: d73c6f4771642ff1c4a7237c5fa9528562894c20da8100f7097da33caee9a183683afa2b001fa0d9cd3becb09335de41054039ac1b37f273b5b130c33bd94403
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.2
@@ -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
@@ -80,6 +81,12 @@ module Simple::Service
80
81
  positionals = build_positional_arguments(args, flags)
81
82
  keywords = build_keyword_arguments(args.merge(flags))
82
83
 
84
+ # check for extra flags
85
+ extra_flags = (flags.keys - keywords.keys.map(&:to_s)).map { |flag| "--#{flag}" }
86
+ unless extra_flags.empty?
87
+ raise Simple::Service::ArgumentError, "Unknown flag(s): #{extra_flags.join(", ")}."
88
+ end
89
+
83
90
  service_instance = Object.new
84
91
  service_instance.extend service
85
92
 
@@ -114,7 +121,7 @@ module Simple::Service
114
121
 
115
122
  # Note that +keys+ now only contains names of keyword arguments that actually exist.
116
123
  # This is therefore not a way to DOS this process.
117
- Hash[keys.map(&:to_sym).zip(values)]
124
+ keys.map(&:to_sym).zip(values).to_h
118
125
  end
119
126
 
120
127
  def variadic_parameter
@@ -164,7 +171,7 @@ module Simple::Service
164
171
  # we otherwise raise a ExtraArguments exception.
165
172
  case ary.length <=> positional_names.length
166
173
  when 1 # i.e. ary.length > positional_names.length
167
- extra_arguments = ary[positional_names.length..-1]
174
+ extra_arguments = ary[positional_names.length..]
168
175
  ary = ary[0..positional_names.length]
169
176
 
170
177
  if !extra_arguments.empty? && !variadic_parameter
@@ -179,7 +186,7 @@ module Simple::Service
179
186
  end
180
187
 
181
188
  # Build a hash with the existing_positional_names and the values from the array.
182
- hsh = Hash[existing_positional_names.zip(ary)]
189
+ hsh = existing_positional_names.zip(ary).to_h
183
190
 
184
191
  # Add the variadic_parameter, if any.
185
192
  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 "$@"
@@ -19,7 +19,8 @@ Gem::Specification.new do |gem|
19
19
  # executables are used for development purposes only
20
20
  gem.executables = []
21
21
 
22
- gem.required_ruby_version = '~> 2.5'
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.2
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: 2022-07-24 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
@@ -106,7 +136,7 @@ require_paths:
106
136
  - lib
107
137
  required_ruby_version: !ruby/object:Gem::Requirement
108
138
  requirements:
109
- - - "~>"
139
+ - - ">="
110
140
  - !ruby/object:Gem::Version
111
141
  version: '2.5'
112
142
  required_rubygems_version: !ruby/object:Gem::Requirement
@@ -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