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 +4 -4
- data/.rubocop.yml +4 -0
- data/.tm_properties +1 -1
- data/Makefile +12 -0
- data/README.md +2 -0
- data/VERSION +1 -1
- data/lib/simple/service.rb +66 -51
- data/lib/simple/service/action.rb +95 -94
- data/lib/simple/service/action/comment.rb +1 -1
- data/lib/simple/service/action/method_reflection.rb +2 -2
- data/lib/simple/service/action/parameter.rb +2 -6
- data/lib/simple/service/context.rb +69 -27
- data/lib/simple/service/errors.rb +54 -0
- data/lib/simple/service/version.rb +7 -2
- data/scripts/stats +2 -1
- data/spec/simple/service/action_invoke2_spec.rb +166 -0
- data/spec/simple/service/action_invoke_spec.rb +266 -0
- data/spec/simple/service/action_spec.rb +51 -0
- data/spec/simple/service/context_spec.rb +39 -7
- data/spec/simple/service/service_spec.rb +105 -0
- data/spec/simple/service/version_spec.rb +7 -0
- data/spec/spec_helper.rb +15 -2
- data/spec/support/spec_services.rb +50 -0
- metadata +15 -5
- data/lib/simple.rb +0 -2
- data/spec/support/004_simplecov.rb +0 -13
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ddfef08de84da78d9ffbc767ed827300fd1c17e354b3227dc896c0271870eece
|
4
|
+
data.tar.gz: 71682a1f87227f246a4ab7d62c5884e2a7babb5052c175e16c902a39572b21d1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 760810f36df807705637d39079501aebc79a80be8423a25bc41371c5d4c2f5ee165a2dff4ae55d518e04dd0e07e58f5b306a6566e51dd4a7d8e75cc7fe9bd12b
|
7
|
+
data.tar.gz: 315b4b8ff7d70687e8cfb5cc1a8206d3bdc1bd3200663c1c26ec41c599884e83f96a2be41435400d42f6c763f03aa2e0fd91e1a7d42627b3df415bc8bf4fcdef
|
data/.rubocop.yml
CHANGED
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
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.1.
|
1
|
+
0.1.2
|
data/lib/simple/service.rb
CHANGED
@@ -1,10 +1,12 @@
|
|
1
|
-
module Simple
|
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
|
-
#
|
22
|
-
def self.
|
23
|
-
|
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
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
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
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
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
|
-
|
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
|
-
|
49
|
-
|
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
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
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
|
-
|
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__
|
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
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
end
|
83
|
-
|
84
|
-
def self.resolve_constant(str)
|
85
|
-
|
86
|
-
rescue NameError
|
87
|
-
|
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
|
-
|
16
|
-
|
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
|
-
|
19
|
-
|
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,
|
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
|
-
|
79
|
-
|
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
|
-
|
83
|
-
|
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
|
-
|
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
|
-
|
93
|
-
|
94
|
-
|
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
|
-
|
97
|
-
|
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
|
-
|
104
|
-
|
118
|
+
raise ::Simple::Service::MissingArguments.new(self, missing_parameters)
|
119
|
+
end
|
105
120
|
|
106
|
-
|
107
|
-
|
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
|
-
|
110
|
-
|
111
|
-
|
126
|
+
keys = @keyword_names & args.keys
|
127
|
+
values = args.fetch_values(*keys)
|
128
|
+
|
129
|
+
Hash[keys.zip(values)]
|
112
130
|
end
|
113
131
|
|
114
|
-
|
132
|
+
def variadic_parameter
|
133
|
+
return @variadic_parameter if defined? @variadic_parameter
|
134
|
+
|
135
|
+
@variadic_parameter = parameters.detect(&:variadic?)
|
136
|
+
end
|
115
137
|
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
keyword_args = {}
|
138
|
+
def positional_names
|
139
|
+
@positional_names ||= parameters.select(&:positional?).map(&:name)
|
140
|
+
end
|
120
141
|
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
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
|
-
|
140
|
-
|
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
|
-
|
165
|
+
positionals
|
144
166
|
end
|
145
167
|
|
146
168
|
def convert_argument_array_to_hash(ary)
|
147
|
-
|
148
|
-
|
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
|
-
|
156
|
-
|
157
|
-
|
173
|
+
if variadic_parameter
|
174
|
+
hsh[variadic_parameter.name] = []
|
175
|
+
end
|
158
176
|
|
159
|
-
|
160
|
-
|
161
|
-
next
|
162
|
-
end
|
177
|
+
if ary.length > positional_names.length
|
178
|
+
extra_arguments = ary[positional_names.length..-1]
|
163
179
|
|
164
|
-
if
|
165
|
-
|
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
|
-
|
174
|
-
|
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
|