simple-service 0.1.5

Sign up to get free protection for your applications and to get access to all the features.
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