simple-service 0.1.4

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.
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