simple-service 0.1.1 → 0.1.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: 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