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,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