stub_requests 0.1.0

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.
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "singleton"
4
+ require "concurrent/map"
5
+
6
+ #
7
+ # Abstraction over WebMock to reduce duplication
8
+ #
9
+ # @author Mikael Henriksson <mikael@zoolutions.se>
10
+ # @since 0.1.0
11
+ #
12
+ module StubRequests
13
+ #
14
+ # Class ServiceRegistry provides registration of services
15
+ #
16
+ class ServiceRegistry
17
+ include Singleton
18
+ include Enumerable
19
+
20
+ #
21
+ # @!attribute [rw] services
22
+ # @return [Concurrent::Map<Symbol, Service>] a map with services
23
+ attr_reader :services
24
+
25
+ def initialize
26
+ @services = Concurrent::Map.new
27
+ end
28
+
29
+ #
30
+ # Resets the map with registered services
31
+ #
32
+ #
33
+ # @api private
34
+ def reset
35
+ services.clear
36
+ end
37
+
38
+ #
39
+ # Required by Enumerable
40
+ #
41
+ #
42
+ # @return [Concurrent::Map<Symbol, Service>] an map with services
43
+ #
44
+ # @yield used by Enumerable
45
+ #
46
+ def each(&block)
47
+ services.each(&block)
48
+ end
49
+
50
+ #
51
+ # Registers a service in the registry
52
+ #
53
+ #
54
+ # @param [Symbol] service_id a symbolic id of the service
55
+ # @param [String] service_uri a string with a base_uri to the service
56
+ #
57
+ # @return [Service] the service that was just registered
58
+ #
59
+ def register_service(service_id, service_uri)
60
+ if (service = get_service(service_id))
61
+ StubRequests.logger.warn("Service already registered #{service}")
62
+ raise ServiceHaveEndpoints, service if service.endpoints?
63
+ end
64
+ services[service_id] = Service.new(service_id, service_uri)
65
+ end
66
+
67
+ #
68
+ # Removes a service from the registry
69
+ #
70
+ #
71
+ # @param [Symbol] service_id the service_id to remove
72
+ #
73
+ # @raise [ServiceNotFound] when the service was not removed
74
+ #
75
+ def remove_service(service_id)
76
+ services.delete(service_id) || raise(ServiceNotFound, service_id)
77
+ end
78
+
79
+ #
80
+ # Fetches a service from the registry
81
+ #
82
+ #
83
+ # @param [Symbol] service_id id of the service to remove
84
+ #
85
+ # @return [Service] the found service
86
+ #
87
+ def get_service(service_id)
88
+ services[service_id]
89
+ end
90
+
91
+ #
92
+ # Fetches a service from the registry or raises {ServiceNotFound}
93
+ #
94
+ #
95
+ # @param [Symbol] service_id the id of a service
96
+ #
97
+ # @raise [ServiceNotFound] when an endpoint couldn't be found
98
+ #
99
+ # @return [Endpoint]
100
+ #
101
+ def get_service!(service_id)
102
+ get_service(service_id) || raise(ServiceNotFound, service_id)
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,90 @@
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
+ # Error is a base class for all gem errors
12
+ #
13
+ class Error < StandardError; end
14
+
15
+ #
16
+ # ServiceHaveEndpoints is raised to prevent overwriting a registered service's endpoints
17
+ #
18
+ class ServiceHaveEndpoints < StandardError
19
+ def initialize(service)
20
+ super("Service with id #{service.id} have already been registered. #{service}")
21
+ end
22
+ end
23
+
24
+ #
25
+ # InvalidType is raised when an argument is invalid
26
+ #
27
+ class InvalidType < Error
28
+ def initialize(actual:, expected:)
29
+ super("Expected `#{actual}` to be any of [#{expected}]")
30
+ end
31
+ end
32
+
33
+ #
34
+ # EndpointNotFound is raised when an endpoint cannot be found
35
+ #
36
+ class EndpointNotFound < Error; end
37
+
38
+ #
39
+ # ServiceNotFound is raised when a service cannot be found
40
+ #
41
+ class ServiceNotFound < Error
42
+ def initialize(service_id)
43
+ super("Couldn't find a service with id=:#{service_id}")
44
+ end
45
+ end
46
+
47
+ #
48
+ # UriSegmentMismatch is raised when a segment cannot be replaced
49
+ #
50
+ class UriSegmentMismatch < Error; end
51
+
52
+ #
53
+ # InvalidUri is raised when a URI is invalid
54
+ #
55
+ class InvalidUri < Error
56
+ def initialize(uri)
57
+ super("'#{uri}' is not a valid URI.")
58
+ end
59
+ end
60
+
61
+ # extends "self"
62
+ # @!parse extend self
63
+ extend self
64
+
65
+ # includes "UriFor" and extends "UriFor"
66
+ # using the API.included callback
67
+ # @!parse include UriFor
68
+ # @!parse extend UriFor
69
+
70
+ # includes "API" and extends "API"
71
+ # using the API.included callback
72
+ # @!parse include API
73
+ # @!parse extend API
74
+ include API
75
+
76
+ #
77
+ # @!attribute [rw] logger
78
+ # @return [Logger] the logger to use in the gem
79
+ attr_accessor :logger
80
+
81
+ #
82
+ # The current version of the gem
83
+ #
84
+ #
85
+ # @return [String] version string, `"1.0.0"`
86
+ #
87
+ def version
88
+ VERSION
89
+ end
90
+ end
@@ -0,0 +1,226 @@
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
+ # Builds a URI from a #host, #template and #replacements
12
+ #
13
+ module URI
14
+ #
15
+ # Builder constructs and validates URIs
16
+ #
17
+ # :reek:TooManyInstanceVariables { max_instance_variables: 6 }
18
+ class Builder
19
+ #
20
+ # @return [Regexp] A pattern for matching url segment keys
21
+ URL_SEGMENT_REGEX = /(:\w+)/.freeze
22
+
23
+ #
24
+ # Convenience method to avoid .new.build
25
+ #
26
+ #
27
+ # @raise [UriSegmentMismatch] when there are unused URI segments
28
+ # @raise [UriSegmentMismatch] when the template have unplaced URI segments
29
+ #
30
+ # @param [String] host the URI used to reach the service
31
+ # @param [String] template the endpoint template
32
+ # @param [Hash<Symbol>] replacements a list of uri_replacement keys
33
+ #
34
+ # @return [String] a validated URI string
35
+ #
36
+ def self.build(host, template, replacements = {})
37
+ new(host, template, replacements).build
38
+ end
39
+
40
+ #
41
+ # @!attribute [r] uri
42
+ # @return [String] the request host {Service#service_uri}
43
+ attr_reader :host
44
+ #
45
+ # @!attribute [r] template
46
+ # @return [String] a string template for the endpoint
47
+ attr_reader :template
48
+ #
49
+ # @!attribute [r] path
50
+ # @return [String] a valid URI path
51
+ attr_reader :path
52
+ #
53
+ # @!attribute [r] replacements
54
+ # @return [Hash<Symbol] a hash with keys matching the {#template}
55
+ attr_reader :replacements
56
+ #
57
+ # @!attribute [r] unused
58
+ # @return [Array<String>] a list with unused {#replacements}
59
+ attr_reader :unused
60
+ #
61
+ # @!attribute [r] unreplaced
62
+ # @return [Array<String>] a list of uri_segments that should have been replaced
63
+ attr_reader :unreplaced
64
+
65
+ #
66
+ # Initializes a new Builder
67
+ #
68
+ #
69
+ # @param [String] host the URI used to reach the service
70
+ # @param [String] template the endpoint template
71
+ # @param [Hash<Symbol>] replacements a list of uri_replacement keys
72
+ #
73
+ def initialize(host, template, replacements = {})
74
+ @host = +host
75
+ @template = +template
76
+ @path = +@template.dup
77
+ @replacements = replacements
78
+ end
79
+
80
+ #
81
+ # Builds a URI string
82
+ #
83
+ #
84
+ # @raise [UriSegmentMismatch] when there are unused URI segments
85
+ # @raise [UriSegmentMismatch] when the template have unplaced URI segments
86
+ #
87
+ # @return [String] a validated URI string
88
+ #
89
+ def build
90
+ build_uri
91
+ run_validations
92
+
93
+ uri
94
+ end
95
+
96
+ private
97
+
98
+ def build_uri
99
+ replace_segments
100
+ parse_unreplaced_segments
101
+ end
102
+
103
+ def uri
104
+ @uri ||= [host, path].join("/")
105
+ end
106
+
107
+ def run_validations
108
+ validate_replacements_used
109
+ validate_segments_replaced
110
+ validate_uri
111
+ end
112
+
113
+ #
114
+ # Replaces the URI segments with the arguments in replacements
115
+ #
116
+ #
117
+ # @return [Array] an list with unused replacements
118
+ #
119
+ def replace_segments
120
+ @unused = replacements.map do |key, value|
121
+ uri_segment = ":#{key}"
122
+ if path.include?(uri_segment)
123
+ path.gsub!(uri_segment.to_s, value.to_s)
124
+ next
125
+ else
126
+ uri_segment
127
+ end
128
+ end.compact
129
+ end
130
+
131
+ #
132
+ # Validates that all replacements have been used
133
+ #
134
+ #
135
+ # @raise [UriSegmentMismatch] when there are unused replacements
136
+ #
137
+ # @return [void]
138
+ #
139
+ def validate_replacements_used
140
+ return if replacents_used?
141
+
142
+ raise UriSegmentMismatch,
143
+ "The URI segment(s) [#{unused.join(',')}] are missing in template (#{path})"
144
+ end
145
+
146
+ #
147
+ # Checks that no replacements are left
148
+ #
149
+ #
150
+ # @return [true,false]
151
+ #
152
+ def replacents_used?
153
+ unused.none?
154
+ end
155
+
156
+ #
157
+ # Validates that all URI segments have been replaced in {#path}
158
+ #
159
+ #
160
+ # @raise [UriSegmentMismatch] when the path have unplaced URI segments
161
+ #
162
+ # @return [void]
163
+ #
164
+ def validate_segments_replaced
165
+ return if segments_replaced?
166
+
167
+ raise UriSegmentMismatch,
168
+ "The URI segment(s) [#{unreplaced.join(',')}]" \
169
+ " were not replaced in template (#{path})." \
170
+ " Given replacements=[#{segment_keys.join(',')}]"
171
+ end
172
+
173
+ def segment_keys
174
+ @segment_keys ||= replacements.keys.map { |segment_key| ":#{segment_key}" }
175
+ end
176
+
177
+ #
178
+ # Checks that all URI segments were replaced
179
+ #
180
+ #
181
+ # @return [true,false]
182
+ #
183
+ def segments_replaced?
184
+ unreplaced.none?
185
+ end
186
+
187
+ #
188
+ # Parses out all unused URI segments
189
+ #
190
+ #
191
+ # @return [Array<String>] a list of not replaced uri_segments
192
+ #
193
+ def parse_unreplaced_segments
194
+ @unreplaced = URL_SEGMENT_REGEX.match(path).to_a.uniq
195
+ end
196
+
197
+ #
198
+ # Validates {#uri} is valid
199
+ #
200
+ #
201
+ # @return [true, false]
202
+ #
203
+ def validate_uri
204
+ StubRequests::URI::Validator.valid?(uri)
205
+ rescue InvalidUri
206
+ StubRequests.logger.warn("URI (#{uri}) is not valid.")
207
+ false
208
+ end
209
+
210
+ #
211
+ # Raise exception when {#validate_uri} is false
212
+ #
213
+ # @see #validate_uri
214
+ #
215
+ # @raise [InvalidUri] when #{uri} is invalid
216
+ #
217
+ # @return [void]
218
+ #
219
+ # :nocov:
220
+ # :nodoc:
221
+ def validate_uri!
222
+ raise InvalidUri, uri unless validate_uri
223
+ end
224
+ end
225
+ end
226
+ end
@@ -0,0 +1,34 @@
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
+ # Provides convenience methods for URI
12
+ #
13
+ module URI
14
+ #
15
+ # Module Scheme handles validation of {URI} schemes
16
+ #
17
+ module Scheme
18
+ #
19
+ # @return [Array<String>] a list of valid HTTP schemes
20
+ SCHEMES = %w[http https].freeze
21
+
22
+ #
23
+ # Checks if the scheme is valid
24
+ #
25
+ # @param [String] scheme a string with the URI scheme to check
26
+ #
27
+ # @return [true,false]
28
+ #
29
+ def self.valid?(scheme)
30
+ SCHEMES.include?(scheme.to_s)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,31 @@
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
+ module URI
11
+ #
12
+ # Module Suffix deals with validating {URI} suffix
13
+ #
14
+ module Suffix
15
+ #
16
+ # @return [RegExp] a pattern used for matching HTTP(S) ports
17
+ PORT_REGEX = %r{:(\d+)/}.freeze
18
+
19
+ #
20
+ # Checks if the host has a valid suffix
21
+ #
22
+ # @param [String] host a string to check
23
+ #
24
+ # @return [true,false]
25
+ #
26
+ def self.valid?(host)
27
+ PublicSuffix.valid?(host, default_rule: nil)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,69 @@
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
+ # Provides convenience methods for URI
12
+ #
13
+ module URI
14
+ #
15
+ # Validator provides functionality for validating a {::URI}
16
+ #
17
+ class Validator
18
+ #
19
+ # Validates a URI
20
+ #
21
+ # @param [String] uri a full uri with path
22
+ #
23
+ # @return [true, false]
24
+ #
25
+ def self.valid?(uri)
26
+ new(uri).valid?
27
+ end
28
+
29
+ #
30
+ # @!attribute [r] uri
31
+ # @return [String] a complete URI
32
+ attr_reader :uri
33
+ #
34
+ # @!attribute [r] host
35
+ # @return [String] the URI host
36
+ attr_reader :host
37
+ #
38
+ # @!attribute [r] scheme
39
+ # @return [String] the URI scheme
40
+ attr_reader :scheme
41
+
42
+ #
43
+ # Initialize a new instance of {Validator}
44
+ #
45
+ # @raise [InvalidUri] when URI can't be parsed
46
+ #
47
+ # @param [String] uri the full URI
48
+ #
49
+ #
50
+ def initialize(uri)
51
+ @uri = ::URI.parse(uri)
52
+ @host = @uri.host
53
+ @scheme = @uri.scheme
54
+ rescue ::URI::InvalidURIError
55
+ raise InvalidUri, uri
56
+ end
57
+
58
+ #
59
+ # Checks if a URI is valid
60
+ #
61
+ #
62
+ # @return [true,false] <description>
63
+ #
64
+ def valid?
65
+ URI::Scheme.valid?(scheme) && URI::Suffix.valid?(host)
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,13 @@
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
+ # @return [String] a version string
12
+ VERSION = "0.1.0"
13
+ end
@@ -0,0 +1,108 @@
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 API abstraction to reduce the amount of WebMock.stub_request
12
+ #
13
+ # @note This module can either be used by its class methods
14
+ # or included in say RSpec
15
+ #
16
+ class WebMockBuilder
17
+ include HashUtil
18
+
19
+ #
20
+ # Builds and registers a WebMock::RequestStub
21
+ #
22
+ #
23
+ # @param [Symbol] verb a HTTP verb/method
24
+ # @param [String] uri a URI to call
25
+ # @param [Hash<Symbol>] options request/response options for Webmock::RequestStub
26
+ #
27
+ # @yield a callback to eventually yield to the caller
28
+ #
29
+ # @return [WebMock::RequestStub] the registered stub
30
+ #
31
+ def self.build(verb, uri, options = {}, &callback)
32
+ new(verb, uri, options, &callback).build
33
+ end
34
+
35
+ #
36
+ # @!attribute [r] request_stub
37
+ # @return [WebMock::RequestStub] a stubbed webmock request
38
+ attr_reader :request_stub
39
+ #
40
+ # @!attribute [r] options
41
+ # @return [Hash<Symbol>] options for the stub_request
42
+ attr_reader :options
43
+ #
44
+ # @!attribute [r] callback
45
+ # @return [Block] call back when given a block
46
+ attr_reader :callback
47
+
48
+ #
49
+ # Initializes a new instance of
50
+ #
51
+ #
52
+ # @param [Symbol] verb a HTTP verb/method
53
+ # @param [String] uri a URI to call
54
+ # @param [Hash<Symbol>] options request/response options for Webmock::RequestStub
55
+ #
56
+ # @yield a block to eventually yield to the caller
57
+ #
58
+ def initialize(verb, uri, options = {}, &callback)
59
+ @request_stub = WebMock::RequestStub.new(verb, uri)
60
+ @options = options
61
+ @callback = callback
62
+ end
63
+
64
+ #
65
+ # Prepares a WebMock::RequestStub and registers it in WebMock
66
+ #
67
+ #
68
+ # @return [WebMock::RequestStub] the registered stub
69
+ #
70
+ def build
71
+ if callback.present?
72
+ Docile.dsl_eval(request_stub, &callback)
73
+ else
74
+ prepare_mock_request
75
+ end
76
+
77
+ WebMock::StubRegistry.instance.register_request_stub(request_stub)
78
+ end
79
+
80
+ private
81
+
82
+ def prepare_mock_request
83
+ prepare_with
84
+ prepare_to_return
85
+ prepare_to_raise
86
+ request_stub.to_timeout if options[:timeout]
87
+ request_stub
88
+ end
89
+
90
+ def prepare_with
91
+ HashUtil.compact(options[:request]) do |request_options|
92
+ request_stub.with(request_options)
93
+ end
94
+ end
95
+
96
+ def prepare_to_return
97
+ HashUtil.compact(options[:response]) do |response_options|
98
+ request_stub.to_return(response_options)
99
+ end
100
+ end
101
+
102
+ def prepare_to_raise
103
+ return unless (error = options[:error])
104
+
105
+ request_stub.to_raise(*Array(error))
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "singleton"
4
+ require "docile"
5
+ require "webmock"
6
+ require "webmock/api"
7
+
8
+ require "stub_requests/version"
9
+
10
+ require "stub_requests/core_ext"
11
+ require "stub_requests/hash_util"
12
+ require "stub_requests/argument_validation"
13
+ require "stub_requests/endpoint"
14
+ require "stub_requests/endpoint_registry"
15
+ require "stub_requests/service"
16
+ require "stub_requests/service_registry"
17
+ require "stub_requests/uri/scheme"
18
+ require "stub_requests/uri/suffix"
19
+ require "stub_requests/uri/validator"
20
+ require "stub_requests/uri/builder"
21
+ require "stub_requests/webmock_builder"
22
+
23
+ require "stub_requests/api"
24
+
25
+ require "stub_requests/stub_requests"