simple-service 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (69) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +12 -0
  3. data/.rubocop.yml +100 -0
  4. data/.tm_properties +1 -0
  5. data/Gemfile +14 -0
  6. data/Makefile +9 -0
  7. data/README.md +68 -0
  8. data/Rakefile +6 -0
  9. data/VERSION +1 -0
  10. data/bin/bundle +105 -0
  11. data/bin/console +15 -0
  12. data/bin/rake +29 -0
  13. data/bin/rspec +29 -0
  14. data/doc/Simple.html +117 -0
  15. data/doc/Simple/Service.html +863 -0
  16. data/doc/Simple/Service/Action.html +1014 -0
  17. data/doc/Simple/Service/Action/Comment.html +451 -0
  18. data/doc/Simple/Service/Action/Comment/Extractor.html +347 -0
  19. data/doc/Simple/Service/Action/IndieHash.html +506 -0
  20. data/doc/Simple/Service/Action/MethodReflection.html +285 -0
  21. data/doc/Simple/Service/Action/Parameter.html +816 -0
  22. data/doc/Simple/Service/ArgumentError.html +128 -0
  23. data/doc/Simple/Service/ClassMethods.html +187 -0
  24. data/doc/Simple/Service/Context.html +379 -0
  25. data/doc/Simple/Service/ContextMissingError.html +124 -0
  26. data/doc/Simple/Service/ContextReadOnlyError.html +206 -0
  27. data/doc/Simple/Service/ExtraArguments.html +428 -0
  28. data/doc/Simple/Service/GemHelper.html +190 -0
  29. data/doc/Simple/Service/MissingArguments.html +426 -0
  30. data/doc/Simple/Service/NoSuchAction.html +433 -0
  31. data/doc/_index.html +286 -0
  32. data/doc/class_list.html +51 -0
  33. data/doc/css/common.css +1 -0
  34. data/doc/css/full_list.css +58 -0
  35. data/doc/css/style.css +496 -0
  36. data/doc/file.README.html +146 -0
  37. data/doc/file_list.html +56 -0
  38. data/doc/frames.html +17 -0
  39. data/doc/index.html +146 -0
  40. data/doc/js/app.js +303 -0
  41. data/doc/js/full_list.js +216 -0
  42. data/doc/js/jquery.js +4 -0
  43. data/doc/method_list.html +539 -0
  44. data/doc/top-level-namespace.html +110 -0
  45. data/lib/simple-service.rb +3 -0
  46. data/lib/simple/service.rb +143 -0
  47. data/lib/simple/service/action.rb +203 -0
  48. data/lib/simple/service/action/comment.rb +57 -0
  49. data/lib/simple/service/action/indie_hash.rb +37 -0
  50. data/lib/simple/service/action/method_reflection.rb +70 -0
  51. data/lib/simple/service/action/parameter.rb +42 -0
  52. data/lib/simple/service/context.rb +94 -0
  53. data/lib/simple/service/errors.rb +54 -0
  54. data/lib/simple/service/version.rb +29 -0
  55. data/log/.gitkeep +0 -0
  56. data/scripts/release +2 -0
  57. data/scripts/release.rb +91 -0
  58. data/scripts/stats +5 -0
  59. data/scripts/watch +2 -0
  60. data/simple-service.gemspec +25 -0
  61. data/spec/simple/service/action_invoke2_spec.rb +166 -0
  62. data/spec/simple/service/action_invoke_spec.rb +266 -0
  63. data/spec/simple/service/action_spec.rb +51 -0
  64. data/spec/simple/service/context_spec.rb +69 -0
  65. data/spec/simple/service/service_spec.rb +105 -0
  66. data/spec/simple/service/version_spec.rb +7 -0
  67. data/spec/spec_helper.rb +38 -0
  68. data/spec/support/spec_services.rb +50 -0
  69. metadata +130 -0
@@ -0,0 +1,110 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>
7
+ Top Level Namespace
8
+
9
+ &mdash; Documentation by YARD 0.9.20
10
+
11
+ </title>
12
+
13
+ <link rel="stylesheet" href="css/style.css" type="text/css" charset="utf-8" />
14
+
15
+ <link rel="stylesheet" href="css/common.css" type="text/css" charset="utf-8" />
16
+
17
+ <script type="text/javascript" charset="utf-8">
18
+ pathId = "";
19
+ relpath = '';
20
+ </script>
21
+
22
+
23
+ <script type="text/javascript" charset="utf-8" src="js/jquery.js"></script>
24
+
25
+ <script type="text/javascript" charset="utf-8" src="js/app.js"></script>
26
+
27
+
28
+ </head>
29
+ <body>
30
+ <div class="nav_wrap">
31
+ <iframe id="nav" src="class_list.html?1"></iframe>
32
+ <div id="resizer"></div>
33
+ </div>
34
+
35
+ <div id="main" tabindex="-1">
36
+ <div id="header">
37
+ <div id="menu">
38
+
39
+ <a href="_index.html">Index</a> &raquo;
40
+
41
+
42
+ <span class="title">Top Level Namespace</span>
43
+
44
+ </div>
45
+
46
+ <div id="search">
47
+
48
+ <a class="full_list_link" id="class_list_link"
49
+ href="class_list.html">
50
+
51
+ <svg width="24" height="24">
52
+ <rect x="0" y="4" width="24" height="4" rx="1" ry="1"></rect>
53
+ <rect x="0" y="12" width="24" height="4" rx="1" ry="1"></rect>
54
+ <rect x="0" y="20" width="24" height="4" rx="1" ry="1"></rect>
55
+ </svg>
56
+ </a>
57
+
58
+ </div>
59
+ <div class="clear"></div>
60
+ </div>
61
+
62
+ <div id="content"><h1>Top Level Namespace
63
+
64
+
65
+
66
+ </h1>
67
+ <div class="box_info">
68
+
69
+
70
+
71
+
72
+
73
+
74
+
75
+
76
+
77
+
78
+
79
+ </div>
80
+
81
+ <h2>Defined Under Namespace</h2>
82
+ <p class="children">
83
+
84
+
85
+ <strong class="modules">Modules:</strong> <span class='object_link'><a href="Simple.html" title="Simple (module)">Simple</a></span>
86
+
87
+
88
+
89
+
90
+ </p>
91
+
92
+
93
+
94
+
95
+
96
+
97
+
98
+
99
+
100
+ </div>
101
+
102
+ <div id="footer">
103
+ Generated on Tue Dec 3 13:46:26 2019 by
104
+ <a href="http://yardoc.org" title="Yay! A Ruby Documentation Tool" target="_parent">yard</a>
105
+ 0.9.20 (ruby-2.5.1).
106
+ </div>
107
+
108
+ </div>
109
+ </body>
110
+ </html>
@@ -0,0 +1,3 @@
1
+ # rubocop:disable Naming/FileName
2
+
3
+ require "simple/service"
@@ -0,0 +1,143 @@
1
+ module Simple # @private
2
+ end
3
+
4
+ require "expectation"
5
+
6
+ require_relative "service/errors"
7
+ require_relative "service/action"
8
+ require_relative "service/context"
9
+ require_relative "service/version"
10
+
11
+ # <b>The Simple::Service interface</b>
12
+ #
13
+ # This module implements the main API of the Simple::Service ruby gem.
14
+ #
15
+ # 1. <em>Marking a service module:</em> To turn a target module as a service module one must include <tt>Simple::Service</tt>
16
+ # into the target. This serves as a marker that this module is actually intended
17
+ # to provide one or more services. Example:
18
+ #
19
+ # module GodMode
20
+ # include Simple::Service
21
+ #
22
+ # # Build a universe.
23
+ # #
24
+ # # This comment will become part of the full description of the
25
+ # # "build_universe" service
26
+ # def build_universe(name, c: , pi: 3.14, e: 2.781)
27
+ # # at this point I realize that *I* am not God.
28
+ #
29
+ # 42 # Best try approach
30
+ # end
31
+ # end
32
+ #
33
+ # 2. <em>Discover services:</em> To discover services in a service module use the #actions method. This returns a Hash
34
+ # of actions. [TODO] why a Hash?
35
+ #
36
+ # Simple::Service.actions(GodMode)
37
+ # => {:build_universe=>#<Simple::Service::Action...>, ...}
38
+ #
39
+ # 3. <em>Invoke a service:</em> run <tt>Simple::Service.invoke</tt> or <tt>Simple::Service.invoke2</tt>. You must set a context first.
40
+ #
41
+ # Simple::Service.with_context do
42
+ # Simple::Service.invoke(GodMode, :build_universe, "TestWorld", c: 1e9)
43
+ # end
44
+ # => 42
45
+ #
46
+ module Simple::Service
47
+ def self.included(klass) # @private
48
+ klass.extend ClassMethods
49
+ end
50
+
51
+ # returns true if the passed in object is a service module.
52
+ #
53
+ # A service must be a module, and it must include the Simple::Service module.
54
+ def self.service?(service)
55
+ verify_service! service
56
+ true
57
+ rescue ::ArgumentError
58
+ false
59
+ end
60
+
61
+ # Raises an error if the passed in object is not a service
62
+ def self.verify_service!(service) # @private
63
+ raise ::ArgumentError, "#{service.inspect} must be a Simple::Service, but is not even a Module" unless service.is_a?(Module)
64
+ raise ::ArgumentError, "#{service.inspect} must be a Simple::Service, did you 'include Simple::Service'" unless service.include?(self)
65
+ end
66
+
67
+ # returns a Hash with all actions in the +service+ module
68
+ def self.actions(service)
69
+ verify_service!(service)
70
+
71
+ service.__simple_service_actions__
72
+ end
73
+
74
+ # returns the action with the given name.
75
+ def self.action(service, name)
76
+ actions = self.actions(service)
77
+ actions[name] || begin
78
+ raise ::Simple::Service::NoSuchAction.new(service, name)
79
+ end
80
+ end
81
+
82
+ # invokes an action with a given +name+ in a service with +arguments+ and +params+.
83
+ #
84
+ # When calling +invoke+ using positional arguments (i.e. non-keyword arguments)
85
+ # they will be matched against positional arguments of the invoked method -
86
+ # but they will not be matched against named arguments.
87
+ #
88
+ # In other words: if the service implements an action "def foo(bar, baz:)", one can
89
+ # run it via
90
+ #
91
+ # - +Service.invoke("bar-value", baz: "baz-value")+, or via
92
+ # - +Service.invoke(bar: "bar-value", baz: "baz-value")+
93
+ #
94
+ # When there are not enough positional arguments to match the number of required
95
+ # positional arguments of the method we raise an ArgumentError.
96
+ #
97
+ # When there are more positional arguments provided than the number accepted
98
+ # by the method we raise an ArgumentError.
99
+ #
100
+ # Entries in the +named_args+ Hash that are not defined in the action itself are ignored.
101
+ #
102
+ # *Note:* You cannot call this method if the context is not set.
103
+ def self.invoke(service, name, *args, **named_args)
104
+ raise ContextMissingError, "Need to set context before calling ::Simple::Service.invoke" unless context
105
+
106
+ action(service, name).invoke(*args, **named_args)
107
+ end
108
+
109
+ # invokes an action with a given +name+ in a service with a Hash of arguments.
110
+ #
111
+ # You cannot call this method if the context is not set.
112
+ def self.invoke2(service, name, args: {}, flags: {})
113
+ raise ContextMissingError, "Need to set context before calling ::Simple::Service.invoke" unless context
114
+
115
+ action(service, name).invoke2(args: args, flags: flags)
116
+ end
117
+
118
+ module ClassMethods # @private
119
+ # returns a Hash of actions provided by the service module.
120
+ def __simple_service_actions__
121
+ @__simple_service_actions__ ||= Action.enumerate(service: self)
122
+ end
123
+ end
124
+
125
+ # # Resolves a service by name. Returns nil if the name does not refer to a service,
126
+ # # or the service module otherwise.
127
+ # def self.resolve(str)
128
+ # return unless str =~ /^[A-Z][A-Za-z0-9_]*(::[A-Z][A-Za-z0-9_]*)*$/
129
+ #
130
+ # service = resolve_constant(str)
131
+ #
132
+ # return unless service.is_a?(Module)
133
+ # return unless service.include?(::Simple::Service)
134
+ #
135
+ # service
136
+ # end
137
+ #
138
+ # def self.resolve_constant(str)
139
+ # const_get(str)
140
+ # rescue NameError
141
+ # nil
142
+ # end
143
+ end
@@ -0,0 +1,203 @@
1
+ module Simple::Service
2
+ class Action
3
+ end
4
+ end
5
+
6
+ require_relative "./action/comment"
7
+ require_relative "./action/parameter"
8
+ require_relative "./action/indie_hash"
9
+
10
+ module Simple::Service
11
+ # rubocop:disable Metrics/AbcSize
12
+ # rubocop:disable Metrics/PerceivedComplexity
13
+ # rubocop:disable Metrics/CyclomaticComplexity
14
+ # rubocop:disable Style/GuardClause
15
+ # rubocop:disable Metrics/ClassLength
16
+
17
+ class Action
18
+ IDENTIFIER_PATTERN = "[a-z][a-z0-9_]*" # @private
19
+ IDENTIFIER_REGEXP = Regexp.compile("\\A#{IDENTIFIER_PATTERN}\\z") # @private
20
+
21
+ # determines all services provided by the +service+ service module.
22
+ def self.enumerate(service:) # @private
23
+ service.public_instance_methods(false)
24
+ .grep(IDENTIFIER_REGEXP)
25
+ .each_with_object({}) { |name, hsh| hsh[name] = Action.new(service, name) }
26
+ end
27
+
28
+ attr_reader :service
29
+ attr_reader :name
30
+
31
+ def full_name
32
+ "#{service.name}##{name}"
33
+ end
34
+
35
+ def to_s # @private
36
+ full_name
37
+ end
38
+
39
+ # returns an Array of Parameter structures.
40
+ def parameters
41
+ @parameters ||= Parameter.reflect_on_method(service: service, name: name)
42
+ end
43
+
44
+ def initialize(service, name) # @private
45
+ @service = service
46
+ @name = name
47
+
48
+ parameters
49
+ end
50
+
51
+ def short_description
52
+ comment.short
53
+ end
54
+
55
+ def full_description
56
+ comment.full
57
+ end
58
+
59
+ private
60
+
61
+ # returns a Comment object
62
+ #
63
+ # The comment object is extracted on demand on the first call.
64
+ def comment
65
+ @comment ||= Comment.extract(action: self)
66
+ end
67
+
68
+ public
69
+
70
+ def source_location
71
+ @service.instance_method(name).source_location
72
+ end
73
+
74
+ # build a service_instance and run the action, with arguments constructed from
75
+ # args_hsh and params_hsh.
76
+ def invoke(*args, **named_args)
77
+ # convert Array arguments into a Hash of named arguments. This is strictly
78
+ # necessary to be able to apply default value-based type conversions. (On
79
+ # the downside this also means we convert an array to a hash and then back
80
+ # into an array. This, however, should only be an issue for CLI based action
81
+ # invocations, because any other use case (that I can think of) should allow
82
+ # us to provide arguments as a Hash.
83
+ args = convert_argument_array_to_hash(args)
84
+ named_args = named_args.merge(args)
85
+
86
+ invoke2(args: named_args, flags: {})
87
+ end
88
+
89
+ # invokes an action with a given +name+ in a service with a Hash of arguments.
90
+ #
91
+ # You cannot call this method if the context is not set.
92
+ def invoke2(args:, flags:)
93
+ # args and flags are being stringified. This is necessary to not allow any
94
+ # unchecked input to DOS this process by just providing always changing
95
+ # key values.
96
+ args = IndieHash.new(args)
97
+ flags = IndieHash.new(flags)
98
+
99
+ verify_required_args!(args, flags)
100
+
101
+ positionals = build_positional_arguments(args, flags)
102
+ keywords = build_keyword_arguments(args.merge(flags))
103
+
104
+ service_instance = Object.new
105
+ service_instance.extend service
106
+
107
+ if keywords.empty?
108
+ service_instance.public_send(@name, *positionals)
109
+ else
110
+ # calling this with an empty keywords Hash still raises an ArgumentError
111
+ # if the target method does not accept arguments.
112
+ service_instance.public_send(@name, *positionals, **keywords)
113
+ end
114
+ end
115
+
116
+ private
117
+
118
+ # returns an error if the keywords hash does not define all required keyword arguments.
119
+ def verify_required_args!(args, flags) # @private
120
+ @required_names ||= parameters.select(&:required?).map(&:name).map(&:to_s)
121
+
122
+ missing_parameters = @required_names - args.keys - flags.keys
123
+ return if missing_parameters.empty?
124
+
125
+ raise ::Simple::Service::MissingArguments.new(self, missing_parameters)
126
+ end
127
+
128
+ # Enumerating all parameters it puts all named parameters into a Hash
129
+ # of keyword arguments.
130
+ def build_keyword_arguments(args)
131
+ @keyword_names ||= parameters.select(&:keyword?).map(&:name).map(&:to_s)
132
+
133
+ keys = @keyword_names & args.keys
134
+ values = args.fetch_values(*keys)
135
+
136
+ # Note that +keys+ now only contains names of keyword arguments that actually exist.
137
+ # This is therefore not a way to DOS this process.
138
+ Hash[keys.map(&:to_sym).zip(values)]
139
+ end
140
+
141
+ def variadic_parameter
142
+ return @variadic_parameter if defined? @variadic_parameter
143
+
144
+ @variadic_parameter = parameters.detect(&:variadic?)
145
+ end
146
+
147
+ def positional_names
148
+ @positional_names ||= parameters.select(&:positional?).map(&:name)
149
+ end
150
+
151
+ # Enumerating all parameters it collects all positional parameters into
152
+ # an Array.
153
+ def build_positional_arguments(args, flags)
154
+ positionals = positional_names.each_with_object([]) do |parameter_name, ary|
155
+ if args.key?(parameter_name)
156
+ ary << args[parameter_name]
157
+ elsif flags.key?(parameter_name)
158
+ ary << flags[parameter_name]
159
+ end
160
+ end
161
+
162
+ # A variadic parameter is appended to the positionals array.
163
+ # It is always optional - but if it exists it must be an Array.
164
+ if variadic_parameter
165
+ value = if args.key?(variadic_parameter.name)
166
+ args[variadic_parameter.name]
167
+ elsif flags.key?(variadic_parameter.name)
168
+ flags[variadic_parameter.name]
169
+ end
170
+
171
+ positionals.concat(value) if value
172
+ end
173
+
174
+ positionals
175
+ end
176
+
177
+ def convert_argument_array_to_hash(ary)
178
+ expect! ary => Array
179
+
180
+ hsh = {}
181
+
182
+ if variadic_parameter
183
+ hsh[variadic_parameter.name] = []
184
+ end
185
+
186
+ if ary.length > positional_names.length
187
+ extra_arguments = ary[positional_names.length..-1]
188
+
189
+ if variadic_parameter
190
+ hsh[variadic_parameter.name] = extra_arguments
191
+ else
192
+ raise ::Simple::Service::ExtraArguments.new(self, extra_arguments)
193
+ end
194
+ end
195
+
196
+ ary.zip(positional_names).each do |value, parameter_name|
197
+ hsh[parameter_name] = value
198
+ end
199
+
200
+ hsh
201
+ end
202
+ end
203
+ end