simple-service 0.1.1 → 0.1.2

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: 9641586855a538c0b450d4731d39d28f23c4ee19abec379f5ac9047888fe9106
4
- data.tar.gz: a395f0a7815935557163cc81532ef7e3fc2b97dfc2bf7347e32460481589dd1d
3
+ metadata.gz: ddfef08de84da78d9ffbc767ed827300fd1c17e354b3227dc896c0271870eece
4
+ data.tar.gz: 71682a1f87227f246a4ab7d62c5884e2a7babb5052c175e16c902a39572b21d1
5
5
  SHA512:
6
- metadata.gz: 43a8d60d9c7b1f14913f431b24ebd50db6a41c2b951a41bd8b1c136d466b2d198b0582e7adcbc34b0df0c81206abe79fda05ea25f8e9c74c4f52a5722e23146a
7
- data.tar.gz: a0376b41661076f53a4ffe6a20650efc6dda513f762ddd0e96d7c7da0f568b6dc2e687c4fbfb67da759f7ac076ce115468a0fd2ba63a686f7ab15504a1ee5cd8
6
+ metadata.gz: 760810f36df807705637d39079501aebc79a80be8423a25bc41371c5d4c2f5ee165a2dff4ae55d518e04dd0e07e58f5b306a6566e51dd4a7d8e75cc7fe9bd12b
7
+ data.tar.gz: 315b4b8ff7d70687e8cfb5cc1a8206d3bdc1bd3200663c1c26ec41c599884e83f96a2be41435400d42f6c763f03aa2e0fd91e1a7d42627b3df415bc8bf4fcdef
data/.rubocop.yml CHANGED
@@ -10,6 +10,10 @@ AllCops:
10
10
  - 'Rakefile'
11
11
  - 'scripts/*.rb'
12
12
 
13
+ Metrics/BlockLength:
14
+ Exclude:
15
+ - 'spec/**/*'
16
+
13
17
  Metrics/LineLength:
14
18
  Max: 140
15
19
 
data/.tm_properties CHANGED
@@ -1 +1 @@
1
- excludeDirectories = "{_build,coverage,assets/node_modules,node_modules,deps,db,cover,priv/static,storage,github,vendor,arena,}"
1
+ excludeDirectories = "{_build,coverage,rdoc,assets/node_modules,node_modules,deps,db,cover,priv/static,storage,github,vendor,arena,}"
data/Makefile CHANGED
@@ -2,3 +2,15 @@
2
2
 
3
3
  test:
4
4
  rspec
5
+
6
+ .PHONY: doc doc/rdoc
7
+ doc: doc/rdoc
8
+
9
+ doc/rdoc:
10
+ rm -rf doc/rdoc
11
+ rdoc -o doc/rdoc README.md \
12
+ lib/simple/service.rb \
13
+ lib/simple/service/action.rb \
14
+ lib/simple/service/context.rb \
15
+ lib/simple/service/errors.rb \
16
+ lib/simple/service/version.rb
data/README.md CHANGED
@@ -1 +1,3 @@
1
1
  # simple-service – a pretty simple and somewhat abstract service description
2
+
3
+ Yea, that's it.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.1
1
+ 0.1.2
@@ -1,10 +1,12 @@
1
- module Simple::Service
2
- class ArgumentError < ::ArgumentError
3
- end
1
+ module Simple # :nodoc:
4
2
  end
5
3
 
4
+ require "expectation"
5
+
6
+ require_relative "service/errors"
6
7
  require_relative "service/action"
7
8
  require_relative "service/context"
9
+ require_relative "service/version"
8
10
 
9
11
  # The Simple::Service module.
10
12
  #
@@ -14,76 +16,89 @@ require_relative "service/context"
14
16
  # This serves as a marker that this module is actually intended
15
17
  # to be used as a service.
16
18
  module Simple::Service
17
- def self.included(klass)
19
+ def self.included(klass) # :nodoc:
18
20
  klass.extend ClassMethods
19
21
  end
20
22
 
21
- # Returns the current context.
22
- def self.context
23
- Thread.current[:"Simple::Service.context"]
23
+ # returns true if the passed in object is a service module.
24
+ def self.service?(service)
25
+ service.is_a?(Module) && service.include?(self)
24
26
  end
25
27
 
26
- # yields a block with a given context, and restores the previous context
27
- # object afterwards.
28
- def self.with_context(ctx, &block)
29
- expect! ctx => [Simple::Service::Context, nil]
30
- _ = block
28
+ def self.verify_service!(service) # :nodoc:
29
+ raise ::ArgumentError, "#{service.inspect} must be a Simple::Service, but is not even a Module" unless service.is_a?(Module)
30
+ raise ::ArgumentError, "#{service.inspect} must be a Simple::Service, did you 'include Simple::Service'" unless service?(service)
31
+ end
31
32
 
32
- old_ctx = Thread.current[:"Simple::Service.context"]
33
- Thread.current[:"Simple::Service.context"] = ctx
34
- yield
35
- ensure
36
- Thread.current[:"Simple::Service.context"] = old_ctx
33
+ # returns a Hash with all actions in the +service+ module
34
+ def self.actions(service)
35
+ verify_service!(service)
36
+
37
+ service.__simple_service_actions__
37
38
  end
38
39
 
40
+ # returns the action with the given name.
39
41
  def self.action(service, name)
40
42
  actions = self.actions(service)
41
43
  actions[name] || begin
42
- action_names = actions.keys.sort
43
- informal = "service #{service} has these actions: #{action_names.map(&:inspect).join(", ")}"
44
- raise "No such action #{name.inspect}; #{informal}"
44
+ raise ::Simple::Service::NoSuchAction.new(service, name)
45
45
  end
46
46
  end
47
47
 
48
- def self.service?(service)
49
- service.is_a?(Module) && service.include?(self)
48
+ # invokes an action with a given +name+ in a service with +arguments+ and +params+.
49
+ #
50
+ # You cannot call this method if the context is not set.
51
+ #
52
+ # When calling #invoke using positional arguments they will be matched against
53
+ # positional arguments of the invoked method - but they will not be matched
54
+ # against named arguments.
55
+ #
56
+ # When there are not enough positional arguments to match the number of required
57
+ # positional arguments of the method we raise an ArgumentError.
58
+ #
59
+ # When there are more positional arguments provided than the number accepted
60
+ # by the method we raise an ArgumentError.
61
+ #
62
+ # Entries in the named_args Hash that are not defined in the action itself are ignored.
63
+ def self.invoke(service, name, *args, **named_args)
64
+ raise ContextMissingError, "Need to set context before calling ::Simple::Service.invoke" unless context
65
+
66
+ action(service, name).invoke(*args, **named_args)
50
67
  end
51
68
 
52
- def self.actions(service)
53
- raise ArgumentError, "service must be a #{self}" unless service?(service)
54
-
55
- service.__simple_service_actions__
56
- end
69
+ # invokes an action with a given +name+ in a service with a Hash of arguments.
70
+ #
71
+ # You cannot call this method if the context is not set.
72
+ def self.invoke2(service, name, args: {}, flags: {})
73
+ args.each { |key, _| expect! key => Symbol }
74
+ raise ContextMissingError, "Need to set context before calling ::Simple::Service.invoke" unless context
57
75
 
58
- def self.invoke(service, name, arguments, params, context: nil)
59
- with_context(context) do
60
- action(service, name).invoke(arguments, params)
61
- end
76
+ action(service, name).invoke2(args: args, flags: flags)
62
77
  end
63
78
 
64
- module ClassMethods
79
+ module ClassMethods # :nodoc:
65
80
  # returns a Hash of actions provided by the service module.
66
- def __simple_service_actions__ # :nodoc:
81
+ def __simple_service_actions__
67
82
  @__simple_service_actions__ ||= Action.enumerate(service: self)
68
83
  end
69
84
  end
70
85
 
71
- # Resolves a service by name. Returns nil if the name does not refer to a service,
72
- # or the service module otherwise.
73
- def self.resolve(str)
74
- return unless str =~ /^[A-Z][A-Za-z0-9_]*(::[A-Z][A-Za-z0-9_]*)*$/
75
-
76
- service = resolve_constant(str)
77
-
78
- return unless service.is_a?(Module)
79
- return unless service.include?(::Simple::Service)
80
-
81
- service
82
- end
83
-
84
- def self.resolve_constant(str)
85
- const_get(str)
86
- rescue NameError
87
- nil
88
- end
86
+ # # Resolves a service by name. Returns nil if the name does not refer to a service,
87
+ # # or the service module otherwise.
88
+ # def self.resolve(str)
89
+ # return unless str =~ /^[A-Z][A-Za-z0-9_]*(::[A-Z][A-Za-z0-9_]*)*$/
90
+ #
91
+ # service = resolve_constant(str)
92
+ #
93
+ # return unless service.is_a?(Module)
94
+ # return unless service.include?(::Simple::Service)
95
+ #
96
+ # service
97
+ # end
98
+ #
99
+ # def self.resolve_constant(str)
100
+ # const_get(str)
101
+ # rescue NameError
102
+ # nil
103
+ # end
89
104
  end
@@ -1,8 +1,3 @@
1
- # rubocop:disable Metrics/CyclomaticComplexity
2
- # rubocop:disable Metrics/AbcSize
3
- # rubocop:disable Metrics/MethodLength
4
- # rubocop:disable Metrics/PerceivedComplexity
5
-
6
1
  module Simple::Service
7
2
  class Action
8
3
  end
@@ -12,11 +7,15 @@ require_relative "./action/comment"
12
7
  require_relative "./action/parameter"
13
8
 
14
9
  module Simple::Service
15
- class Action
16
- ArgumentError = ::Simple::Service::ArgumentError
10
+ # rubocop:disable Metrics/AbcSize
11
+ # rubocop:disable Metrics/PerceivedComplexity
12
+ # rubocop:disable Metrics/CyclomaticComplexity
13
+ # rubocop:disable Style/GuardClause
14
+ # rubocop:disable Metrics/ClassLength
17
15
 
18
- IDENTIFIER_PATTERN = "[a-z][a-z0-9_]*"
19
- IDENTIFIER_REGEXP = Regexp.compile("\\A#{IDENTIFIER_PATTERN}\\z")
16
+ class Action
17
+ IDENTIFIER_PATTERN = "[a-z][a-z0-9_]*" # :nodoc:
18
+ IDENTIFIER_REGEXP = Regexp.compile("\\A#{IDENTIFIER_PATTERN}\\z") # :nodoc:
20
19
 
21
20
  # determines all services provided by the +service+ service module.
22
21
  def self.enumerate(service:) # :nodoc:
@@ -28,12 +27,20 @@ module Simple::Service
28
27
  attr_reader :service
29
28
  attr_reader :name
30
29
 
30
+ def full_name
31
+ "#{service.name}##{name}"
32
+ end
33
+
34
+ def to_s # :nodoc:
35
+ full_name
36
+ end
37
+
31
38
  # returns an Array of Parameter structures.
32
39
  def parameters
33
40
  @parameters ||= Parameter.reflect_on_method(service: service, name: name)
34
41
  end
35
42
 
36
- def initialize(service, name)
43
+ def initialize(service, name) # :nodoc:
37
44
  @service = service
38
45
  @name = name
39
46
 
@@ -65,129 +72,123 @@ module Simple::Service
65
72
 
66
73
  # build a service_instance and run the action, with arguments constructed from
67
74
  # args_hsh and params_hsh.
68
- def invoke(args, options)
69
- args ||= {}
70
- options ||= {}
71
-
75
+ def invoke(*args, **named_args)
72
76
  # convert Array arguments into a Hash of named arguments. This is strictly
73
77
  # necessary to be able to apply default value-based type conversions. (On
74
78
  # the downside this also means we convert an array to a hash and then back
75
79
  # into an array. This, however, should only be an issue for CLI based action
76
80
  # invocations, because any other use case (that I can think of) should allow
77
- # us to provide arguments as a Hash.
78
- if args.is_a?(Array)
79
- args = convert_argument_array_to_hash(args)
80
- end
81
+ # us to provide arguments as a Hash.
82
+ args = convert_argument_array_to_hash(args)
83
+ named_args = named_args.merge(args)
81
84
 
82
- # [TODO] Type conversion according to default values.
83
- args_ary = build_method_arguments(args, options)
85
+ invoke2(args: named_args, flags: {})
86
+ end
87
+
88
+ # invokes an action with a given +name+ in a service with a Hash of arguments.
89
+ #
90
+ # You cannot call this method if the context is not set.
91
+ def invoke2(args:, flags:)
92
+ verify_required_args!(args, flags)
93
+
94
+ positionals = build_positional_arguments(args, flags)
95
+ keywords = build_keyword_arguments(args.merge(flags))
84
96
 
85
97
  service_instance = Object.new
86
98
  service_instance.extend service
87
- service_instance.public_send(@name, *args_ary)
99
+
100
+ if keywords.empty?
101
+ service_instance.public_send(@name, *positionals)
102
+ else
103
+ # calling this with an empty keywords Hash still raises an ArgumentError
104
+ # if the target method does not accept arguments.
105
+ service_instance.public_send(@name, *positionals, **keywords)
106
+ end
88
107
  end
89
108
 
90
109
  private
91
110
 
92
- module IndifferentHashEx
93
- def self.fetch(hsh, name)
94
- missing_key!(name) unless hsh
111
+ # returns an error if the keywords hash does not define all required keyword arguments.
112
+ def verify_required_args!(args, flags) # :nodoc:
113
+ @required_names ||= parameters.select(&:required?).map(&:name)
95
114
 
96
- hsh.fetch(name.to_sym) do
97
- hsh.fetch(name.to_s) do
98
- missing_key!(name)
99
- end
100
- end
101
- end
115
+ missing_parameters = @required_names - args.keys - flags.keys
116
+ return if missing_parameters.empty?
102
117
 
103
- def self.key?(hsh, name)
104
- return false unless hsh
118
+ raise ::Simple::Service::MissingArguments.new(self, missing_parameters)
119
+ end
105
120
 
106
- hsh.key?(name.to_sym) || hsh.key?(name.to_s)
107
- end
121
+ # Enumerating all parameters it puts all named parameters into a Hash
122
+ # of keyword arguments.
123
+ def build_keyword_arguments(args)
124
+ @keyword_names ||= parameters.select(&:keyword?).map(&:name)
108
125
 
109
- def self.missing_key!(name)
110
- raise ArgumentError, "Missing argument in arguments hash: #{name}"
111
- end
126
+ keys = @keyword_names & args.keys
127
+ values = args.fetch_values(*keys)
128
+
129
+ Hash[keys.zip(values)]
112
130
  end
113
131
 
114
- I = IndifferentHashEx
132
+ def variadic_parameter
133
+ return @variadic_parameter if defined? @variadic_parameter
134
+
135
+ @variadic_parameter = parameters.detect(&:variadic?)
136
+ end
115
137
 
116
- # returns an array of arguments suitable to be sent to the action method.
117
- def build_method_arguments(args_hsh, params_hsh)
118
- args = []
119
- keyword_args = {}
138
+ def positional_names
139
+ @positional_names ||= parameters.select(&:positional?).map(&:name)
140
+ end
120
141
 
121
- parameters.each do |parameter|
122
- if parameter.keyword?
123
- if I.key?(params_hsh, parameter.name)
124
- keyword_args[parameter.name] = I.fetch(params_hsh, parameter.name)
125
- end
126
- else
127
- if parameter.variadic?
128
- if I.key?(args_hsh, parameter.name)
129
- args.concat(Array(I.fetch(args_hsh, parameter.name)))
130
- end
131
- else
132
- if !parameter.optional? || I.key?(args_hsh, parameter.name)
133
- args << I.fetch(args_hsh, parameter.name)
134
- end
135
- end
142
+ # Enumerating all parameters it collects all positional parameters into
143
+ # an Array.
144
+ def build_positional_arguments(args, flags)
145
+ positionals = positional_names.each_with_object([]) do |parameter_name, ary|
146
+ if args.key?(parameter_name)
147
+ ary << args[parameter_name]
148
+ elsif flags.key?(parameter_name)
149
+ ary << flags[parameter_name]
136
150
  end
137
151
  end
138
152
 
139
- unless keyword_args.empty?
140
- args << keyword_args
153
+ # A variadic parameter is appended to the positionals array.
154
+ # It is always optional - but if it exists it must be an Array.
155
+ if variadic_parameter
156
+ value = if args.key?(variadic_parameter.name)
157
+ args[variadic_parameter.name]
158
+ elsif flags.key?(variadic_parameter.name)
159
+ flags[variadic_parameter.name]
160
+ end
161
+
162
+ positionals.concat(value) if value
141
163
  end
142
164
 
143
- args
165
+ positionals
144
166
  end
145
167
 
146
168
  def convert_argument_array_to_hash(ary)
147
- # enumerate all of the action's anonymous arguments, trying to match them
148
- # against the values in +ary+. If afterwards any arguments are still left
149
- # in +ary+ they will be assigned to the variadic arguments array, which
150
- # - if a variadic parameter is defined in this action - will be added to
151
- # the hash as well.
169
+ expect! ary => Array
170
+
152
171
  hsh = {}
153
- variadic_parameter_name = nil
154
172
 
155
- parameters.each do |parameter|
156
- next if parameter.keyword?
157
- parameter_name = parameter.name
173
+ if variadic_parameter
174
+ hsh[variadic_parameter.name] = []
175
+ end
158
176
 
159
- if parameter.variadic?
160
- variadic_parameter_name = parameter_name
161
- next
162
- end
177
+ if ary.length > positional_names.length
178
+ extra_arguments = ary[positional_names.length..-1]
163
179
 
164
- if ary.empty? && !parameter.optional?
165
- raise ::Simple::Service::ArgumentError, "Missing #{parameter_name} parameter"
180
+ if variadic_parameter
181
+ hsh[variadic_parameter.name] = extra_arguments
182
+ else
183
+ raise ::Simple::Service::ExtraArguments.new(self, extra_arguments)
166
184
  end
167
-
168
- next if ary.empty?
169
-
170
- hsh[parameter_name] = ary.shift
171
185
  end
172
186
 
173
- # Any arguments are left? Set variadic parameter, if defined, raise an error otherwise.
174
- unless ary.empty?
175
- unless variadic_parameter_name
176
- raise ::Simple::Service::ArgumentError, "Extra parameters: #{ary.map(&:inspect).join(", ")}"
177
- end
178
-
179
- hsh[variadic_parameter_name] = ary
187
+ ary.zip(positional_names).each do |value, parameter_name|
188
+ hsh[parameter_name] = value
180
189
  end
181
190
 
182
191
  hsh
183
192
  end
184
-
185
- def full_name
186
- "#{service}##{name}"
187
- end
188
-
189
- def to_s
190
- full_name
191
- end
192
193
  end
193
194
  end