stub_requests 0.1.0

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