simple-service 0.1.5

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 (68) 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 +173 -0
  47. data/lib/simple/service/action.rb +190 -0
  48. data/lib/simple/service/action/comment.rb +57 -0
  49. data/lib/simple/service/action/method_reflection.rb +70 -0
  50. data/lib/simple/service/action/parameter.rb +42 -0
  51. data/lib/simple/service/context.rb +94 -0
  52. data/lib/simple/service/errors.rb +54 -0
  53. data/lib/simple/service/version.rb +29 -0
  54. data/log/.gitkeep +0 -0
  55. data/scripts/release +2 -0
  56. data/scripts/release.rb +91 -0
  57. data/scripts/stats +5 -0
  58. data/scripts/watch +2 -0
  59. data/simple-service.gemspec +25 -0
  60. data/spec/simple/service/action_invoke3_spec.rb +266 -0
  61. data/spec/simple/service/action_invoke_spec.rb +166 -0
  62. data/spec/simple/service/action_spec.rb +51 -0
  63. data/spec/simple/service/context_spec.rb +69 -0
  64. data/spec/simple/service/service_spec.rb +156 -0
  65. data/spec/simple/service/version_spec.rb +7 -0
  66. data/spec/spec_helper.rb +38 -0
  67. data/spec/support/spec_services.rb +58 -0
  68. metadata +129 -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,173 @@
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.invoke3</tt> or <tt>Simple::Service.invoke</tt>. You must set a context first.
40
+ #
41
+ # Simple::Service.with_context do
42
+ # Simple::Service.invoke3(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 +args+ and +flags+.
83
+ #
84
+ # This is a helper method which one can use to easily call an action from
85
+ # ruby source code.
86
+ #
87
+ # As the main purpose of this module is to call services with outside data,
88
+ # the +.invoke+ action is usually preferred.
89
+ #
90
+ # *Note:* You cannot call this method if the context is not set.
91
+ def self.invoke3(service, name, *args, **flags)
92
+ flags = flags.each_with_object({}) { |(k, v), hsh| hsh[k.to_s] = v }
93
+ invoke service, name, args: args, flags: flags
94
+ end
95
+
96
+ # invokes an action with a given +name+.
97
+ #
98
+ # This is the general form of invoking a service. It accepts the following
99
+ # arguments:
100
+ #
101
+ # - args: an Array of positional arguments OR a Hash of named arguments.
102
+ # - flags: a Hash of flags.
103
+ #
104
+ # Note that the keys in both the +flags+ and the +args+ Hash must be strings.
105
+ #
106
+ # The service is being called with a parameters built out of those like this:
107
+ #
108
+ # - The service's positional arguments are being built from the +args+ array
109
+ # parameter or from the +named_args+ hash parameter.
110
+ # - The service's keyword arguments are being built from the +named_args+
111
+ # and +flags+ arguments.
112
+ #
113
+ # In other words:
114
+ #
115
+ # 1. You cannot set both +args+ and +named_args+ at the same time.
116
+ # 2. The +flags+ arguments are only being used to determine the
117
+ # service's keyword parameters.
118
+ #
119
+ # So, if the service X implements an action "def foo(bar, baz:)", the following would
120
+ # all invoke that service:
121
+ #
122
+ # - +Service.invoke3(X, :foo, "bar-value", baz: "baz-value")+, or
123
+ # - +Service.invoke3(X, :foo, bar: "bar-value", baz: "baz-value")+, or
124
+ # - +Service.invoke(X, :foo, args: ["bar-value"], flags: { "baz" => "baz-value" })+, or
125
+ # - +Service.invoke(X, :foo, args: { "bar" => "bar-value", "baz" => "baz-value" })+.
126
+ #
127
+ # (see spec/service_spec.rb)
128
+ #
129
+ # When there are not enough positional arguments to match the number of required
130
+ # positional arguments of the method we raise an ArgumentError.
131
+ #
132
+ # When there are more positional arguments provided than the number accepted
133
+ # by the method we raise an ArgumentError.
134
+ #
135
+ # Entries in the +named_args+ Hash that are not defined in the action itself are ignored.
136
+
137
+ # <b>Note:</b> You cannot call this method if the context is not set.
138
+ def self.invoke(service, name, args: {}, flags: {})
139
+ raise ContextMissingError, "Need to set context before calling ::Simple::Service.invoke3" unless context
140
+
141
+ expect! args => [Hash, Array], flags: Hash
142
+ args.keys.each { |key| expect! key => String } if args.is_a?(Hash)
143
+ flags.keys.each { |key| expect! key => String }
144
+
145
+ action(service, name).invoke(args: args, flags: flags)
146
+ end
147
+
148
+ module ClassMethods # @private
149
+ # returns a Hash of actions provided by the service module.
150
+ def __simple_service_actions__
151
+ @__simple_service_actions__ ||= Action.enumerate(service: self)
152
+ end
153
+ end
154
+
155
+ # # Resolves a service by name. Returns nil if the name does not refer to a service,
156
+ # # or the service module otherwise.
157
+ # def self.resolve(str)
158
+ # return unless str =~ /^[A-Z][A-Za-z0-9_]*(::[A-Z][A-Za-z0-9_]*)*$/
159
+ #
160
+ # service = resolve_constant(str)
161
+ #
162
+ # return unless service.is_a?(Module)
163
+ # return unless service.include?(::Simple::Service)
164
+ #
165
+ # service
166
+ # end
167
+ #
168
+ # def self.resolve_constant(str)
169
+ # const_get(str)
170
+ # rescue NameError
171
+ # nil
172
+ # end
173
+ end
@@ -0,0 +1,190 @@
1
+ module Simple::Service
2
+ class Action
3
+ end
4
+ end
5
+
6
+ require_relative "./action/comment"
7
+ require_relative "./action/parameter"
8
+
9
+ module Simple::Service
10
+ # rubocop:disable Metrics/AbcSize
11
+ # rubocop:disable Metrics/PerceivedComplexity
12
+ # rubocop:disable Metrics/CyclomaticComplexity
13
+ # rubocop:disable Metrics/ClassLength
14
+
15
+ class Action
16
+ IDENTIFIER_PATTERN = "[a-z][a-z0-9_]*" # @private
17
+ IDENTIFIER_REGEXP = Regexp.compile("\\A#{IDENTIFIER_PATTERN}\\z") # @private
18
+
19
+ # determines all services provided by the +service+ service module.
20
+ def self.enumerate(service:) # @private
21
+ service.public_instance_methods(false)
22
+ .grep(IDENTIFIER_REGEXP)
23
+ .each_with_object({}) { |name, hsh| hsh[name] = Action.new(service, name) }
24
+ end
25
+
26
+ attr_reader :service
27
+ attr_reader :name
28
+
29
+ def full_name
30
+ "#{service.name}##{name}"
31
+ end
32
+
33
+ def to_s # @private
34
+ full_name
35
+ end
36
+
37
+ # returns an Array of Parameter structures.
38
+ def parameters
39
+ @parameters ||= Parameter.reflect_on_method(service: service, name: name)
40
+ end
41
+
42
+ def initialize(service, name) # @private
43
+ @service = service
44
+ @name = name
45
+
46
+ parameters
47
+ end
48
+
49
+ def short_description
50
+ comment.short
51
+ end
52
+
53
+ def full_description
54
+ comment.full
55
+ end
56
+
57
+ private
58
+
59
+ # returns a Comment object
60
+ #
61
+ # The comment object is extracted on demand on the first call.
62
+ def comment
63
+ @comment ||= Comment.extract(action: self)
64
+ end
65
+
66
+ public
67
+
68
+ def source_location
69
+ @service.instance_method(name).source_location
70
+ end
71
+
72
+ # invokes an action with a given +name+ in a service with a Hash of arguments.
73
+ #
74
+ # You cannot call this method if the context is not set.
75
+ def invoke(args:, flags:)
76
+ args = convert_argument_array_to_hash(args) if args.is_a?(Array)
77
+
78
+ verify_required_args!(args, flags)
79
+
80
+ positionals = build_positional_arguments(args, flags)
81
+ keywords = build_keyword_arguments(args.merge(flags))
82
+
83
+ service_instance = Object.new
84
+ service_instance.extend service
85
+
86
+ if keywords.empty?
87
+ service_instance.public_send(@name, *positionals)
88
+ else
89
+ # calling this with an empty keywords Hash still raises an ArgumentError
90
+ # if the target method does not accept arguments.
91
+ service_instance.public_send(@name, *positionals, **keywords)
92
+ end
93
+ end
94
+
95
+ private
96
+
97
+ # returns an error if the keywords hash does not define all required keyword arguments.
98
+ def verify_required_args!(args, flags) # @private
99
+ @required_names ||= parameters.select(&:required?).map(&:name).map(&:to_s)
100
+
101
+ missing_parameters = @required_names - args.keys - flags.keys
102
+ return if missing_parameters.empty?
103
+
104
+ raise ::Simple::Service::MissingArguments.new(self, missing_parameters)
105
+ end
106
+
107
+ # Enumerating all parameters it puts all named parameters into a Hash
108
+ # of keyword arguments.
109
+ def build_keyword_arguments(args)
110
+ @keyword_names ||= parameters.select(&:keyword?).map(&:name).map(&:to_s)
111
+
112
+ keys = @keyword_names & args.keys
113
+ values = args.fetch_values(*keys)
114
+
115
+ # Note that +keys+ now only contains names of keyword arguments that actually exist.
116
+ # This is therefore not a way to DOS this process.
117
+ Hash[keys.map(&:to_sym).zip(values)]
118
+ end
119
+
120
+ def variadic_parameter
121
+ return @variadic_parameter if defined? @variadic_parameter
122
+
123
+ @variadic_parameter = parameters.detect(&:variadic?)
124
+ end
125
+
126
+ def positional_names
127
+ @positional_names ||= parameters.select(&:positional?).map(&:name).map(&:to_s)
128
+ end
129
+
130
+ # Enumerating all parameters it collects all positional parameters into
131
+ # an Array.
132
+ def build_positional_arguments(args, flags)
133
+ positionals = positional_names.each_with_object([]) do |parameter_name, ary|
134
+ if args.key?(parameter_name)
135
+ ary << args[parameter_name]
136
+ elsif flags.key?(parameter_name)
137
+ ary << flags[parameter_name]
138
+ end
139
+ end
140
+
141
+ # A variadic parameter is appended to the positionals array.
142
+ # It is always optional - but if it exists it must be an Array.
143
+ if variadic_parameter
144
+ value = if args.key?(variadic_parameter.name)
145
+ args[variadic_parameter.name]
146
+ elsif flags.key?(variadic_parameter.name)
147
+ flags[variadic_parameter.name]
148
+ end
149
+
150
+ positionals.concat(value) if value
151
+ end
152
+
153
+ positionals
154
+ end
155
+
156
+ def convert_argument_array_to_hash(ary)
157
+ expect! ary => Array
158
+
159
+ # +ary* might contain more, less, or the exact number of positional
160
+ # arguments. If the number is less, we return a hash with only whart
161
+ # exists in ary - the action might define default values after all.
162
+ #
163
+ # If it contains more the action better supports a variadic parameter;
164
+ # we otherwise raise a ExtraArguments exception.
165
+ case ary.length <=> positional_names.length
166
+ when 1 # i.e. ary.length > positional_names.length
167
+ extra_arguments = ary[positional_names.length..-1]
168
+ ary = ary[0..positional_names.length]
169
+
170
+ if !extra_arguments.empty? && !variadic_parameter
171
+ raise ::Simple::Service::ExtraArguments.new(self, extra_arguments)
172
+ end
173
+
174
+ existing_positional_names = positional_names
175
+ when 0 # i.e. ary.length == positional_names.length
176
+ existing_positional_names = positional_names
177
+ when -1 # i.e. ary.length < positional_names.length
178
+ existing_positional_names = positional_names[0, ary.length]
179
+ end
180
+
181
+ # Build a hash with the existing_positional_names and the values from the array.
182
+ hsh = Hash[existing_positional_names.zip(ary)]
183
+
184
+ # Add the variadic_parameter, if any.
185
+ hsh[variadic_parameter.name] = extra_arguments if variadic_parameter
186
+
187
+ hsh
188
+ end
189
+ end
190
+ end