stub_requests 0.1.2 → 0.1.3

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 (35) hide show
  1. checksums.yaml +4 -4
  2. data/.simplecov +1 -0
  3. data/CHANGELOG.md +8 -0
  4. data/README.md +27 -0
  5. data/gemfiles/webmock_2.3.gemfile.lock +1 -1
  6. data/gemfiles/webmock_3.5.gemfile.lock +1 -1
  7. data/gemfiles/webmock_develop.gemfile.lock +1 -1
  8. data/lib/stub_requests.rb +19 -14
  9. data/lib/stub_requests/api.rb +30 -16
  10. data/lib/stub_requests/argument_validation.rb +12 -12
  11. data/lib/stub_requests/endpoint.rb +12 -10
  12. data/lib/stub_requests/exceptions.rb +2 -6
  13. data/lib/stub_requests/metrics.rb +5 -4
  14. data/lib/stub_requests/metrics/{endpoint_stat.rb → endpoint.rb} +23 -22
  15. data/lib/stub_requests/metrics/registry.rb +25 -25
  16. data/lib/stub_requests/metrics/{stub_stat.rb → request.rb} +22 -13
  17. data/lib/stub_requests/observable.rb +62 -0
  18. data/lib/stub_requests/observable/registry.rb +152 -0
  19. data/lib/stub_requests/observable/subscription.rb +58 -0
  20. data/lib/stub_requests/property.rb +9 -3
  21. data/lib/stub_requests/property/validator.rb +4 -4
  22. data/lib/stub_requests/registration.rb +87 -0
  23. data/lib/stub_requests/registration/endpoint.rb +107 -0
  24. data/lib/stub_requests/registration/endpoints.rb +156 -0
  25. data/lib/stub_requests/registration/registry.rb +112 -0
  26. data/lib/stub_requests/registration/service.rb +85 -0
  27. data/lib/stub_requests/uri.rb +1 -1
  28. data/lib/stub_requests/version.rb +1 -1
  29. data/lib/tasks/changelog.rake +11 -2
  30. data/update_docs.sh +33 -0
  31. metadata +13 -8
  32. data/bin/update_docs.sh +0 -16
  33. data/lib/stub_requests/endpoint_registry.rb +0 -159
  34. data/lib/stub_requests/service.rb +0 -77
  35. data/lib/stub_requests/service_registry.rb +0 -104
@@ -39,8 +39,8 @@ module StubRequests
39
39
  # Define property methods for the name
40
40
  #
41
41
  # @param [Symbol] name the name of the property
42
- # @param [Object] type: the expected type of the property
43
- # @param [Hash<Symbol>] **options a hash with options
42
+ # @param [Object] type the expected type of the property
43
+ # @param [Hash<Symbol>] options a hash with options
44
44
  # @option options [Object] :default a default value for the property
45
45
  #
46
46
  # @return [Object] the whatever
@@ -55,6 +55,7 @@ module StubRequests
55
55
  end
56
56
  end
57
57
 
58
+ # @api private
58
59
  def normalize_type(type, **options)
59
60
  type_array = Array(type)
60
61
  return type_array unless (default = options[:default])
@@ -62,6 +63,7 @@ module StubRequests
62
63
  type_array.concat([default.class]).flatten.uniq
63
64
  end
64
65
 
66
+ # @api private
65
67
  def define_property(name, type, default)
66
68
  property_reader(name)
67
69
  property_predicate(name)
@@ -70,6 +72,7 @@ module StubRequests
70
72
  set_property_defined(name, type, default)
71
73
  end
72
74
 
75
+ # @api private
73
76
  def property_reader(name)
74
77
  silence_redefinition_of_method(name.to_s)
75
78
  redefine_method(name) do
@@ -77,6 +80,7 @@ module StubRequests
77
80
  end
78
81
  end
79
82
 
83
+ # @api private
80
84
  def property_predicate(name)
81
85
  silence_redefinition_of_method("#{name}?")
82
86
  redefine_method("#{name}?") do
@@ -84,13 +88,15 @@ module StubRequests
84
88
  end
85
89
  end
86
90
 
91
+ # @api private
87
92
  def property_writer(name, type)
88
93
  redefine_method("#{name}=") do |obj|
89
- validate! name, obj, is_a: type
94
+ validate! name: name, value: obj, type: type
90
95
  instance_variable_set("@#{name}", obj)
91
96
  end
92
97
  end
93
98
 
99
+ # @api private
94
100
  def set_property_defined(name, type, default)
95
101
  properties[name] = { type: type, default: default }
96
102
  end
@@ -68,10 +68,10 @@ module StubRequests
68
68
  # @param [Hash] properties the list of currently defined properties
69
69
  #
70
70
  # :reek:LongParameterList
71
- def initialize(name, type, default, properties)
72
- @name = name
71
+ def initialize(name, type, default = nil, properties = {})
73
72
  @type = Array(type).flatten
74
73
  @default = default
74
+ @name = name
75
75
  @properties = properties
76
76
  end
77
77
 
@@ -101,7 +101,7 @@ module StubRequests
101
101
  # @return [void]
102
102
  #
103
103
  def validate_name
104
- validate! :name, name, is_a: Symbol
104
+ validate! name: :name, value: name, type: Symbol
105
105
  end
106
106
 
107
107
  #
@@ -115,7 +115,7 @@ module StubRequests
115
115
  def validate_default
116
116
  return unless default || default.is_a?(FalseClass)
117
117
 
118
- validate! :default, default, is_a: type
118
+ validate! name: :default, value: default, type: type
119
119
  end
120
120
 
121
121
  #
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StubRequests
4
+ #
5
+ # Module Registration handles registration of stubbed endpoints and services
6
+ #
7
+ # @author Mikael Henriksson <mikael@zoolutions.se>
8
+ # @since 0.1.3
9
+ #
10
+ module Registration
11
+ # Register a service in the service registry
12
+ #
13
+ #
14
+ # @param [Symbol] service_id a descriptive id for the service
15
+ # @param [Symbol] service_uri the uri used to call the service
16
+ #
17
+ # @example Register a service with endpoints
18
+ # register_service(:documents, "https://company.com/api/v1") do
19
+ # register(:show, :get, "documents/:id")
20
+ # register(:index, :get, "documents")
21
+ # register(:create, :post, "documents")
22
+ # register(:update, :patch, "documents/:id")
23
+ # register(:destroy, :delete, "documents/:id")
24
+ # end
25
+ #
26
+ # @return [Service] a new service or a previously registered service
27
+ #
28
+ def self.register_service(service_id, service_uri, &block)
29
+ service = Registry.instance.register(service_id, service_uri)
30
+ Docile.dsl_eval(service.endpoints, &block) if block.present?
31
+ service
32
+ end
33
+
34
+ #
35
+ # Stub a request to a registered service endpoint
36
+ #
37
+ #
38
+ # @param [Symbol] service_id the id of a registered service
39
+ # @param [Symbol] endpoint_id the id of a registered endpoint
40
+ # @param [Hash<Symbol>] uri_replacements a list of URI replacements
41
+ # @param [Hash<Symbol>] options
42
+ # @option options [optional, Hash<Symbol>] :request webmock request options
43
+ # @option options [optional, Hash<Symbol>] :response webmock response options
44
+ # @option options [optional, Array, Exception, StandardError, String] :error webmock error to raise
45
+ # @option options [optional, TrueClass] :timeout set to true to raise some kind of timeout error
46
+ #
47
+ # @note the kind of timeout error raised by webmock is depending on the HTTP client used
48
+ #
49
+ # @example Stub a request to a registered service endpoint
50
+ # register_stub(
51
+ # :google_api,
52
+ # :get_map_location,
53
+ # {}, # No URI replacements needed for this endpoint
54
+ # { request: { headers: { "Accept" => "application/json" }}},
55
+ # { response: { body: { id: "abyasdjasd", status: "successful" }}}
56
+ # )
57
+ #
58
+ # @example Stub a request to a registered service endpoint using block version
59
+ # register_stub(:documents, :index) do
60
+ # with(headers: { "Accept" => "application/json" }}})
61
+ # to_return(body: "No content", status: 204)
62
+ # end
63
+ #
64
+ # @see #stub_http_request
65
+ # @return [WebMock::RequestStub] a mocked request
66
+ #
67
+ # :reek:UtilityFunction
68
+ # :reek:LongParameterList { max_params: 5 }
69
+ def self.stub_endpoint(service_id, endpoint_id, uri_replacements = {}, options = {}, &callback)
70
+ service, endpoint, uri = StubRequests::URI.for_service_endpoint(service_id, endpoint_id, uri_replacements)
71
+ endpoint_stub = WebMock::Builder.build(endpoint.verb, uri, options, &callback)
72
+
73
+ Metrics.record(service, endpoint, endpoint_stub)
74
+ ::WebMock::StubRegistry.instance.register_request_stub(endpoint_stub)
75
+ end
76
+
77
+ # @api private
78
+ # Used only for testing purposes
79
+ # :reek:LongParameterList { max_params: 4 }
80
+ def self.__stub_endpoint(service_id, endpoint_id, uri_replacements = {}, options = {})
81
+ _service, endpoint, uri = StubRequests::URI.for_service_endpoint(service_id, endpoint_id, uri_replacements)
82
+ endpoint_stub = WebMock::Builder.build(endpoint.verb, uri, options)
83
+
84
+ ::WebMock::StubRegistry.instance.register_request_stub(endpoint_stub)
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Abstraction over WebMock to reduce duplication
5
+ #
6
+ # @author Mikael Henriksson <mikael@zoolutions.se>
7
+ # @since 0.1.0
8
+ #
9
+ module StubRequests
10
+ #
11
+ # Module Registration provides registration of stubbed endpoints and services
12
+ #
13
+ # @author Mikael Henriksson <mikael@zoolutions.se>
14
+ # @since 0.1.3
15
+ #
16
+ module Registration
17
+ #
18
+ # Class Endpoint provides registration of stubbed endpoints
19
+ #
20
+ # @author Mikael Henriksson <mikael@zoolutions.se>
21
+ #
22
+ class Endpoint
23
+ extend Forwardable
24
+
25
+ include Comparable
26
+ include Property
27
+
28
+ # Delegate id, uri and endpoints to service
29
+ delegate [:id, :uri, :endpoints] => :service
30
+ #
31
+ # @!attribute [rw] id
32
+ # @return [Symbol] the id of the endpoint
33
+ property :id, type: Symbol
34
+ #
35
+ # @!attribute [rw] verb
36
+ # @return [Symbol] a HTTP verb
37
+ property :verb, type: Symbol
38
+ #
39
+ # @!attribute [rw] uri_template
40
+ # @return [String] a string template for the endpoint
41
+ property :uri_template, type: String
42
+ #
43
+ # @!attribute [rw] options
44
+ # @see
45
+ # @return [Hash<Symbol>] a Hash with default request/response options
46
+ property :options, type: Hash, default: {}
47
+
48
+ #
49
+ # An endpoint for a specific {StubRequests::Registration::Service}
50
+ #
51
+ # @param [Symbol] endpoint_id a descriptive id for the endpoint
52
+ # @param [Symbol] verb a HTTP verb
53
+ # @param [String] uri_template how to reach the endpoint
54
+ # @param [optional, Hash<Symbol>] options
55
+ # @option options [optional, Hash<Symbol>] :request for request_stub.with
56
+ # @option options [optional, Hash<Symbol>] :response for request_stub.to_return
57
+ # @option options [optional, Array, Exception, StandardError, String] :error for request_stub.to_raise
58
+ # @option options [optional, TrueClass] :timeout for request_stub.to_timeout
59
+ #
60
+ def initialize(endpoint_id, verb, uri_template, options = {})
61
+ self.id = endpoint_id
62
+ self.verb = verb
63
+ self.uri_template = uri_template
64
+ self.options = options
65
+ end
66
+
67
+ #
68
+ # Updates this endpoint
69
+ #
70
+ # @param [Symbol] verb a HTTP verb
71
+ # @param [String] uri_template how to reach the endpoint
72
+ # @param [optional, Hash<Symbol>] options
73
+ # @option options [optional, Hash<Symbol>] :request for request_stub.with
74
+ # @option options [optional, Hash<Symbol>] :response for request_stub.to_return
75
+ # @option options [optional, Array, Exception, StandardError, String] :error for request_stub.to_raise
76
+ # @option options [optional, TrueClass] :timeout for request_stub.to_timeout
77
+ #
78
+ # @return [Registration::Endpoint] returns the updated endpoint
79
+ #
80
+ def update(verb, uri_template, options)
81
+ self.verb = verb
82
+ self.uri_template = uri_template
83
+ self.options = options
84
+ self
85
+ end
86
+
87
+ def <=>(other)
88
+ id <=> other.id
89
+ end
90
+
91
+ def hash
92
+ [id, self.class].hash
93
+ end
94
+
95
+ alias eql? ==
96
+
97
+ #
98
+ # Returns a descriptive string of this endpoint
99
+ #
100
+ # @return [String]
101
+ #
102
+ def to_s
103
+ "#<#{self.class} id=:#{id} verb=:#{verb} uri_template='#{uri_template}'>"
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,156 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Abstraction over WebMock to reduce duplication
5
+ #
6
+ # @author Mikael Henriksson <mikael@zoolutions.se>
7
+ # @since 0.1.0
8
+ #
9
+ module StubRequests
10
+ #
11
+ # Module Registration provides registration of stubbed endpoints and services
12
+ #
13
+ # @author Mikael Henriksson <mikael@zoolutions.se>
14
+ # @since 0.1.3
15
+ #
16
+ module Registration
17
+ #
18
+ # Class Endpoints manages a collection of endpoints
19
+ #
20
+ # @author Mikael Henriksson <mikael@zoolutions.se>
21
+ #
22
+ class Endpoints
23
+ include Enumerable
24
+
25
+ #
26
+ # @!attribute [rw] endpoints
27
+ # @return [Concurrent::Map<Symbol, Endpoint>] a map with endpoints
28
+ attr_reader :endpoints
29
+
30
+ def initialize
31
+ @endpoints = Concurrent::Map.new
32
+ end
33
+
34
+ #
35
+ # Required by Enumerable
36
+ #
37
+ # @return [Concurrent::Map<Symbol, Service>] a map with endpoints
38
+ #
39
+ # @yield used by Enumerable
40
+ #
41
+ def each(&block)
42
+ endpoints.each(&block)
43
+ end
44
+
45
+ #
46
+ # Registers an endpoint in the collection
47
+ #
48
+ # @param [Symbol] endpoint_id the id of this Endpoint
49
+ # @param [Symbol] verb a HTTP verb
50
+ # @param [String] uri_template the URI to reach the endpoint
51
+ # @param [optional, Hash<Symbol>] options default options
52
+ #
53
+ # @return [Endpoint]
54
+ #
55
+ # :reek:LongParameterList { max_params: 4 }
56
+ def register(endpoint_id, verb, uri_template, options = {})
57
+ endpoint =
58
+ if (endpoint = find(endpoint_id))
59
+ StubRequests.logger.warn("Endpoint already registered: #{endpoint}")
60
+ endpoint.update(verb, uri_template, options)
61
+ else
62
+ Endpoint.new(endpoint_id, verb, uri_template, options)
63
+ end
64
+
65
+ endpoints[endpoint.id] = endpoint
66
+ endpoint
67
+ end
68
+
69
+ #
70
+ # Updates an endpoint
71
+ #
72
+ # @param [Symbol] endpoint_id the id of the endpoint
73
+ # @param [Symbol] verb a HTTP verb
74
+ # @param [String] uri_template how to reach the endpoint
75
+ # @param [optional, Hash<Symbol>] options
76
+ # @option options [optional, Hash<Symbol>] :request request options
77
+ # @option options [optional, Hash<Symbol>] :response options
78
+ # @option options [optional, Array, Exception, StandardError, String] :error to raise
79
+ # @option options [optional, TrueClass] :timeout raise a timeout error?
80
+ #
81
+ # @raise [EndpointNotFound] when the endpoint couldn't be found
82
+ #
83
+ # @return [Endpoint] returns the updated endpoint
84
+ #
85
+ # :reek:LongParameterList { max_params: 4 }
86
+ def update(endpoint_id, verb, uri_template, options)
87
+ endpoint = find!(endpoint_id)
88
+ endpoint.update(verb, uri_template, options)
89
+ end
90
+
91
+ #
92
+ # Removes an endpoint from the collection
93
+ #
94
+ # @param [Symbol] endpoint_id the id of the endpoint, `:file_service`
95
+ #
96
+ # @return [Endpoint] the endpoint that was removed
97
+ #
98
+ def remove(endpoint_id)
99
+ endpoints.delete(endpoint_id)
100
+ end
101
+
102
+ #
103
+ # Fetches an endpoint from the collection
104
+ #
105
+ # @param [<type>] endpoint_id <description>
106
+ #
107
+ # @return [Endpoint]
108
+ #
109
+ def find(endpoint_id)
110
+ endpoints[endpoint_id]
111
+ end
112
+
113
+ #
114
+ # Fetches an endpoint from the collection or raises an error
115
+ #
116
+ # @param [Symbol] endpoint_id the id of the endpoint
117
+ #
118
+ # @raise [EndpointNotFound] when an endpoint couldn't be found
119
+ #
120
+ # @return [Endpoint, nil]
121
+ #
122
+ def find!(endpoint_id)
123
+ find(endpoint_id) || raise(EndpointNotFound, "Couldn't find an endpoint with id=:#{endpoint_id}")
124
+ end
125
+
126
+ #
127
+ # Returns a descriptive string with all endpoints in the collection
128
+ #
129
+ # @return [String]
130
+ #
131
+ def to_s
132
+ [
133
+ +"#<#{self.class} endpoints=",
134
+ +endpoints_string,
135
+ +">",
136
+ ].join("")
137
+ end
138
+
139
+ #
140
+ # Returns a nicely formatted string with an array of endpoints
141
+ #
142
+ #
143
+ # @return [<type>] <description>
144
+ #
145
+ def endpoints_string
146
+ "[#{endpoints_as_string}]"
147
+ end
148
+
149
+ private
150
+
151
+ def endpoints_as_string
152
+ endpoints.values.map(&:to_s).join(",") if endpoints.size.positive?
153
+ end
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Abstraction over WebMock to reduce duplication
5
+ #
6
+ # @author Mikael Henriksson <mikael@zoolutions.se>
7
+ # @since 0.1.0
8
+ #
9
+ module StubRequests
10
+ #
11
+ # Module Registration provides registration of stubbed endpoints and services
12
+ #
13
+ # @author Mikael Henriksson <mikael@zoolutions.se>
14
+ # @since 0.1.3
15
+ #
16
+ module Registration
17
+ #
18
+ # Class Registry provides registration of services
19
+ #
20
+ # @author Mikael Henriksson <mikael@zoolutions.se>
21
+ #
22
+ class Registry
23
+ include Singleton
24
+ include Enumerable
25
+
26
+ #
27
+ # @!attribute [rw] services
28
+ # @return [Concurrent::Map<Symbol, Service>] a map with services
29
+ attr_reader :services
30
+
31
+ def initialize
32
+ @services = Concurrent::Map.new
33
+ end
34
+
35
+ #
36
+ # Resets the map with registered services
37
+ #
38
+ #
39
+ # @api private
40
+ def reset
41
+ services.clear
42
+ end
43
+
44
+ #
45
+ # Required by Enumerable
46
+ #
47
+ #
48
+ # @return [Concurrent::Map<Symbol, Service>] an map with services
49
+ #
50
+ # @yield used by Enumerable
51
+ #
52
+ def each(&block)
53
+ services.each(&block)
54
+ end
55
+
56
+ #
57
+ # Registers a service in the registry
58
+ #
59
+ #
60
+ # @param [Symbol] service_id a symbolic id of the service
61
+ # @param [String] service_uri a string with a base_uri to the service
62
+ #
63
+ # @return [Registration::Service] the service that was just registered
64
+ #
65
+ def register(service_id, service_uri)
66
+ if (service = find(service_id))
67
+ StubRequests.logger.warn("Service already registered #{service}")
68
+ raise ServiceHaveEndpoints, service if service.endpoints?
69
+ end
70
+ services[service_id] = Service.new(service_id, service_uri)
71
+ end
72
+
73
+ #
74
+ # Removes a service from the registry
75
+ #
76
+ #
77
+ # @param [Symbol] service_id the service_id to remove
78
+ #
79
+ # @raise [ServiceNotFound] when the service was not removed
80
+ #
81
+ def remove(service_id)
82
+ services.delete(service_id) || raise(ServiceNotFound, service_id)
83
+ end
84
+
85
+ #
86
+ # Fetches a service from the registry
87
+ #
88
+ #
89
+ # @param [Symbol] service_id id of the service to remove
90
+ #
91
+ # @return [Registration::Service] the found service
92
+ #
93
+ def find(service_id)
94
+ services[service_id]
95
+ end
96
+
97
+ #
98
+ # Fetches a service from the registry or raises {ServiceNotFound}
99
+ #
100
+ #
101
+ # @param [Symbol] service_id the id of a service
102
+ #
103
+ # @raise [ServiceNotFound] when an endpoint couldn't be found
104
+ #
105
+ # @return [Registration::Service]
106
+ #
107
+ def find!(service_id)
108
+ find(service_id) || raise(ServiceNotFound, service_id)
109
+ end
110
+ end
111
+ end
112
+ end