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,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "addressable/uri"
4
+ require "public_suffix"
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
+ # Module ArgumentValidation provides validation of method arguments
15
+ #
16
+ module ArgumentValidation
17
+ extend self
18
+
19
+ #
20
+ # Require the value to be any of the types past in
21
+ #
22
+ #
23
+ # @param [Object] value the value to validate
24
+ # @param [Array<Class>, Array<Module>, Class, Module] is_a
25
+ #
26
+ # @raise [InvalidType] when the value is disallowed
27
+ #
28
+ # @return [true] when the value is allowed
29
+ #
30
+ def validate!(value, is_a:)
31
+ expected_types = Array(is_a)
32
+ return true if expected_types.any? { |type| value.is_a?(type) }
33
+
34
+ raise StubRequests::InvalidType,
35
+ actual: value.class,
36
+ expected: expected_types.join(", ")
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "stub_requests/core_ext/object/blank"
@@ -0,0 +1,171 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "concurrent/map"
4
+
5
+ # Copied from https://raw.githubusercontent.com/rails/rails/d66e7835bea9505f7003e5038aa19b6ea95ceea1/activesupport/lib/active_support/core_ext/object/blank.rb
6
+
7
+ # :nodoc:
8
+ # :nocov:
9
+
10
+ # @see Object
11
+ class Object # :nodoc: # :nocov:
12
+ # An object is blank if it's false, empty, or a whitespace string.
13
+ # For example, +nil+, '', ' ', [], {}, and +false+ are all blank.
14
+ #
15
+ # This simplifies
16
+ #
17
+ # !address || address.empty?
18
+ #
19
+ # to
20
+ #
21
+ # address.blank?
22
+ #
23
+ # @return [true, false]
24
+ def blank?
25
+ respond_to?(:empty?) ? !!empty? : !self # rubocop:disable Style/DoubleNegation
26
+ end unless respond_to?(:blank?)
27
+
28
+ # An object is present if it's not blank.
29
+ #
30
+ # @return [true, false]
31
+ def present?
32
+ !blank?
33
+ end unless respond_to?(:present?)
34
+
35
+ # Returns the receiver if it's present otherwise returns +nil+.
36
+ # <tt>object.presence</tt> is equivalent to
37
+ #
38
+ # object.present? ? object : nil
39
+ #
40
+ # For example, something like
41
+ #
42
+ # state = params[:state] if params[:state].present?
43
+ # country = params[:country] if params[:country].present?
44
+ # region = state || country || 'US'
45
+ #
46
+ # becomes
47
+ #
48
+ # region = params[:state].presence || params[:country].presence || 'US'
49
+ #
50
+ # @return [Object]
51
+ def presence
52
+ self if present?
53
+ end unless respond_to?(:presence)
54
+ end
55
+
56
+ # @see NilClass
57
+ class NilClass
58
+ # +nil+ is blank:
59
+ #
60
+ # nil.blank? # => true
61
+ #
62
+ # @return [true]
63
+ def blank?
64
+ true
65
+ end unless respond_to?(:blank?)
66
+ end
67
+
68
+ # @see FalseClass
69
+ class FalseClass
70
+ # +false+ is blank:
71
+ #
72
+ # false.blank? # => true
73
+ #
74
+ # @return [true]
75
+ def blank?
76
+ true
77
+ end unless respond_to?(:blank?)
78
+ end
79
+
80
+ # @see TrueClass
81
+ class TrueClass
82
+ # +true+ is not blank:
83
+ #
84
+ # true.blank? # => false
85
+ #
86
+ # @return [false]
87
+ def blank?
88
+ false
89
+ end unless respond_to?(:blank?)
90
+ end
91
+
92
+ # @see Array
93
+ class Array
94
+ # An array is blank if it's empty:
95
+ #
96
+ # [].blank? # => true
97
+ # [1,2,3].blank? # => false
98
+ #
99
+ # @return [true, false]
100
+ alias blank? empty?
101
+ end
102
+
103
+ # @see Hash
104
+ class Hash
105
+ # A hash is blank if it's empty:
106
+ #
107
+ # {}.blank? # => true
108
+ # { key: 'value' }.blank? # => false
109
+ #
110
+ # @return [true, false]
111
+ alias blank? empty?
112
+ end
113
+
114
+ # @see String
115
+ class String
116
+ # :nodoc:
117
+ BLANK_RE = /\A[[:space:]]*\z/.freeze
118
+ # :nodoc:
119
+ ENCODED_BLANKS = Concurrent::Map.new do |map, enc|
120
+ map[enc] = Regexp.new(BLANK_RE.source.encode(enc), BLANK_RE.options | Regexp::FIXEDENCODING)
121
+ end
122
+
123
+ # A string is blank if it's empty or contains whitespaces only:
124
+ #
125
+ # ''.blank? # => true
126
+ # ' '.blank? # => true
127
+ # "\t\n\r".blank? # => true
128
+ # ' blah '.blank? # => false
129
+ #
130
+ # Unicode whitespace is supported:
131
+ #
132
+ # "\u00a0".blank? # => true
133
+ #
134
+ # @return [true, false]
135
+ def blank?
136
+ # The regexp that matches blank strings is expensive. For the case of empty
137
+ # strings we can speed up this method (~3.5x) with an empty? call. The
138
+ # penalty for the rest of strings is marginal.
139
+ empty? ||
140
+ begin
141
+ BLANK_RE.match?(self)
142
+ rescue Encoding::CompatibilityError
143
+ ENCODED_BLANKS[encoding].match?(self)
144
+ end
145
+ end unless respond_to?(:blank?)
146
+ end
147
+
148
+ # @see Numeric
149
+ class Numeric
150
+ # No number is blank:
151
+ #
152
+ # 1.blank? # => false
153
+ # 0.blank? # => false
154
+ #
155
+ # @return [false]
156
+ def blank?
157
+ false
158
+ end unless respond_to?(:blank?)
159
+ end
160
+
161
+ # @see Time
162
+ class Time
163
+ # No Time is blank:
164
+ #
165
+ # Time.now.blank? # => false
166
+ #
167
+ # @return [false]
168
+ def blank?
169
+ false
170
+ end unless respond_to?(:blank?)
171
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "stub_requests/core_ext/all"
@@ -0,0 +1,100 @@
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
+ # Class Endpoint provides registration of stubbed endpoints
12
+ #
13
+ class Endpoint
14
+ include ArgumentValidation
15
+ include Comparable
16
+
17
+ #
18
+ # @!attribute [rw] id
19
+ # @return [Symbol] the id of the endpoint
20
+ attr_reader :id
21
+
22
+ #
23
+ # @!attribute [rw] verb
24
+ # @return [Symbol] a HTTP verb
25
+ attr_reader :verb
26
+
27
+ #
28
+ # @!attribute [rw] uri_template
29
+ # @return [String] a string template for the endpoint
30
+ attr_reader :uri_template
31
+
32
+ #
33
+ # @!attribute [rw] default_options
34
+ # @see
35
+ # @return [Hash<Symbol>] a string template for the endpoint
36
+ attr_reader :default_options
37
+
38
+ #
39
+ # An endpoint for a specific {Service}
40
+ #
41
+ # @param [Symbol] endpoint_id a descriptive id for the endpoint
42
+ # @param [Symbol] verb a HTTP verb
43
+ # @param [String] uri_template how to reach the endpoint
44
+ # @param [optional, Hash<Symbol>] default_options
45
+ # @option default_options [optional, Hash<Symbol>] :request for request_stub.with
46
+ # @option default_options [optional, Hash<Symbol>] :response for request_stub.to_return
47
+ # @option default_options [optional, Array, Exception, StandardError, String] :error for request_stub.to_raise
48
+ # @option default_options [optional, TrueClass] :timeout for request_stub.to_timeout
49
+ #
50
+ def initialize(endpoint_id, verb, uri_template, default_options = {})
51
+ validate! endpoint_id, is_a: Symbol
52
+ validate! verb, is_a: Symbol
53
+ validate! uri_template, is_a: String
54
+
55
+ @id = endpoint_id
56
+ @verb = verb
57
+ @uri_template = uri_template
58
+ @default_options = default_options
59
+ end
60
+
61
+ #
62
+ # Updates this endpoint
63
+ #
64
+ # @param [Symbol] verb a HTTP verb
65
+ # @param [String] uri_template how to reach the endpoint
66
+ # @param [optional, Hash<Symbol>] default_options
67
+ # @option default_options [optional, Hash<Symbol>] :request for request_stub.with
68
+ # @option default_options [optional, Hash<Symbol>] :response for request_stub.to_return
69
+ # @option default_options [optional, Array, Exception, StandardError, String] :error for request_stub.to_raise
70
+ # @option default_options [optional, TrueClass] :timeout for request_stub.to_timeout
71
+ #
72
+ # @return [Endpoint] returns the updated endpoint
73
+ #
74
+ def update(verb, uri_template, default_options)
75
+ @verb = verb
76
+ @uri_template = uri_template
77
+ @default_options = default_options
78
+ self
79
+ end
80
+
81
+ def <=>(other)
82
+ id <=> other.id
83
+ end
84
+
85
+ def hash
86
+ [id, self.class].hash
87
+ end
88
+
89
+ alias eql? ==
90
+
91
+ #
92
+ # Returns a descriptive string of this endpoint
93
+ #
94
+ # @return [String]
95
+ #
96
+ def to_s
97
+ "#<#{self.class} id=:#{id} verb=:#{verb} uri_template='#{uri_template}'>"
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,157 @@
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
+ # Class EndpointRegistry holds a collection of {Endpoint}
12
+ #
13
+ class EndpointRegistry
14
+ include Enumerable
15
+
16
+ #
17
+ # @!attribute [rw] endpoints
18
+ # @return [Concurrent::Map<Symbol, Endpoint>] a map with endpoints
19
+ attr_reader :endpoints
20
+
21
+ def initialize
22
+ @endpoints = Concurrent::Map.new
23
+ end
24
+
25
+ #
26
+ # Required by Enumerable
27
+ #
28
+ # @return [Concurrent::Map<Symbol, Service>] a map with endpoints
29
+ #
30
+ # @yield used by Enumerable
31
+ #
32
+ def each(&block)
33
+ endpoints.each(&block)
34
+ end
35
+
36
+ #
37
+ # Registers an endpoint in the collection
38
+ #
39
+ # @param [Symbol] endpoint_id the id of this Endpoint
40
+ # @param [Symbol] verb a HTTP verb
41
+ # @param [String] uri_template the URI to reach the endpoint
42
+ # @param [optional, Hash<Symbol>] default_options default options
43
+ #
44
+ # @return [Endpoint]
45
+ #
46
+ # :reek:LongParameterList { max_params: 4 }
47
+ def register(endpoint_id, verb, uri_template, default_options = {})
48
+ endpoint =
49
+ if (endpoint = get(endpoint_id))
50
+ StubRequests.logger.warn("Endpoint already registered: #{endpoint}")
51
+ endpoint.update(verb, uri_template, default_options)
52
+ else
53
+ Endpoint.new(endpoint_id, verb, uri_template, default_options)
54
+ end
55
+
56
+ endpoints[endpoint.id] = endpoint
57
+ endpoint
58
+ end
59
+
60
+ #
61
+ # Check if an endpoint is registered
62
+ #
63
+ # @param [Symbol] endpoint_id the id of the endpoint
64
+ #
65
+ # @return [true, false]
66
+ #
67
+ def registered?(endpoint_id)
68
+ endpoints[endpoint_id].present?
69
+ end
70
+
71
+ #
72
+ # Updates an endpoint
73
+ #
74
+ # @param [Symbol] endpoint_id the id of the endpoint
75
+ # @param [Symbol] verb a HTTP verb
76
+ # @param [String] uri_template how to reach the endpoint
77
+ # @param [optional, Hash<Symbol>] default_options
78
+ # @option default_options [optional, Hash<Symbol>] :request request options
79
+ # @option default_options [optional, Hash<Symbol>] :response options
80
+ # @option default_options [optional, Array, Exception, StandardError, String] :error to raise
81
+ # @option default_options [optional, TrueClass] :timeout raise a timeout error?
82
+ #
83
+ # @raise [EndpointNotFound] when the endpoint couldn't be found
84
+ #
85
+ # @return [Endpoint] returns the updated endpoint
86
+ #
87
+ # :reek:LongParameterList { max_params: 4 }
88
+ def update(endpoint_id, verb, uri_template, default_options)
89
+ endpoint = get!(endpoint_id)
90
+ endpoint.update(verb, uri_template, default_options)
91
+ end
92
+
93
+ #
94
+ # Removes an endpoint from the collection
95
+ #
96
+ # @param [Symbol] endpoint_id the id of the endpoint, `:file_service`
97
+ #
98
+ # @return [Endpoint] the endpoint that was removed
99
+ #
100
+ def remove(endpoint_id)
101
+ endpoints.delete(endpoint_id)
102
+ end
103
+
104
+ #
105
+ # Fetches an endpoint from the collection
106
+ #
107
+ # @param [<type>] endpoint_id <description>
108
+ #
109
+ # @return [Endpoint]
110
+ #
111
+ def get(endpoint_id)
112
+ endpoints[endpoint_id]
113
+ end
114
+
115
+ #
116
+ # Fetches an endpoint from the collection or raises an error
117
+ #
118
+ # @param [Symbol] endpoint_id the id of the endpoint
119
+ #
120
+ # @raise [EndpointNotFound] when an endpoint couldn't be found
121
+ #
122
+ # @return [Endpoint, nil]
123
+ #
124
+ def get!(endpoint_id)
125
+ get(endpoint_id) || raise(EndpointNotFound, "Couldn't find an endpoint with id=:#{endpoint_id}")
126
+ end
127
+
128
+ #
129
+ # Returns a descriptive string with all endpoints in the collection
130
+ #
131
+ # @return [String]
132
+ #
133
+ def to_s
134
+ [
135
+ +"#<#{self.class} endpoints=",
136
+ +endpoints_string,
137
+ +">",
138
+ ].join("")
139
+ end
140
+
141
+ #
142
+ # Returns a nicely formatted string with an array of endpoints
143
+ #
144
+ #
145
+ # @return [<type>] <description>
146
+ #
147
+ def endpoints_string
148
+ "[#{endpoints_as_string}]"
149
+ end
150
+
151
+ private
152
+
153
+ def endpoints_as_string
154
+ endpoints.values.map(&:to_s).join(",") if endpoints.size.positive?
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,30 @@
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 hashes
12
+ #
13
+ module HashUtil
14
+ #
15
+ # Removes all entries with nil values (first level only)
16
+ #
17
+ # @param [Hash] options the hash to compact
18
+ #
19
+ # @return [Hash, nil] Returns
20
+ #
21
+ # @yieldparam [Hash] compacted the hash without nils
22
+ # @yieldreturn [void]
23
+ def self.compact(options)
24
+ return if options.blank?
25
+
26
+ compacted = options.delete_if { |_, val| val.blank? }
27
+ yield compacted if compacted.present?
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,118 @@
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
+ # Class Service provides details for a registered service
12
+ #
13
+ class Service
14
+ include ArgumentValidation
15
+ include Comparable
16
+
17
+ # @!attribute [rw] id
18
+ # @return [EndpointRegistry] the id of the service
19
+ attr_reader :id
20
+
21
+ # @!attribute [rw] uri
22
+ # @return [EndpointRegistry] the base uri to the service
23
+ attr_reader :uri
24
+
25
+ # @!attribute [rw] endpoint_registry
26
+ # @return [EndpointRegistry] a list with defined endpoints
27
+ attr_reader :endpoint_registry
28
+
29
+ #
30
+ # Initializes a new instance of a Service
31
+ #
32
+ # @param [Symbol] service_id the id of this service
33
+ # @param [String] service_uri the base uri to reach the service
34
+ #
35
+ def initialize(service_id, service_uri)
36
+ validate! service_id, is_a: Symbol
37
+ validate! service_uri, is_a: String
38
+
39
+ @id = service_id
40
+ @uri = service_uri
41
+ @endpoint_registry = EndpointRegistry.new
42
+ end
43
+
44
+ #
45
+ # Registers a new endpoint or updates an existing one
46
+ #
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>] default_options default options
52
+ #
53
+ # @return [Endpoint] either the new endpoint or the updated one
54
+ #
55
+ # :reek:LongParameterList { max_params: 5 }
56
+ def register_endpoint(endpoint_id, verb, uri_template, default_options = {})
57
+ endpoint_registry.register(endpoint_id, verb, uri_template, default_options)
58
+ end
59
+
60
+ #
61
+ # Check if the endpoint registry has endpoints
62
+ #
63
+ # @return [true,false]
64
+ #
65
+ def endpoints?
66
+ endpoint_registry.any?
67
+ end
68
+
69
+ #
70
+ # Gets an endpoint from the {#endpoint_registry} collection
71
+ #
72
+ # @param [Symbol] endpoint_id the id of the endpoint
73
+ #
74
+ # @raise [EndpointNotFound] when the endpoint couldn't be found
75
+ #
76
+ # @return [Endpoint]
77
+ #
78
+ def get_endpoint!(endpoint_id)
79
+ endpoint_registry.get!(endpoint_id)
80
+ end
81
+
82
+ #
83
+ # Gets an endpoint from the {#endpoint_registry} collection
84
+ #
85
+ # @param [Symbol] endpoint_id the id of the endpoint
86
+ #
87
+ # @return [Endpoint, nil]
88
+ #
89
+ def get_endpoint(endpoint_id)
90
+ endpoint_registry.get(endpoint_id)
91
+ end
92
+
93
+ #
94
+ # Returns a nicely formatted string with this service
95
+ #
96
+ # @return [String]
97
+ #
98
+ def to_s
99
+ [
100
+ +"#<#{self.class}",
101
+ +" id=#{id}",
102
+ +" uri=#{uri}",
103
+ +" endpoints=#{endpoint_registry.endpoints_string}",
104
+ +">",
105
+ ].join("")
106
+ end
107
+
108
+ def <=>(other)
109
+ id <=> other.id
110
+ end
111
+
112
+ def hash
113
+ [id, self.class].hash
114
+ end
115
+
116
+ alias eql? ==
117
+ end
118
+ end