spec_forge 0.4.0 → 0.6.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.
- checksums.yaml +4 -4
- data/.standard.yml +4 -0
- data/CHANGELOG.md +145 -1
- data/README.md +49 -638
- data/flake.lock +3 -3
- data/flake.nix +8 -2
- data/lib/spec_forge/attribute/chainable.rb +208 -20
- data/lib/spec_forge/attribute/factory.rb +141 -12
- data/lib/spec_forge/attribute/faker.rb +64 -15
- data/lib/spec_forge/attribute/global.rb +96 -0
- data/lib/spec_forge/attribute/literal.rb +15 -2
- data/lib/spec_forge/attribute/matcher.rb +188 -13
- data/lib/spec_forge/attribute/parameterized.rb +45 -20
- data/lib/spec_forge/attribute/regex.rb +55 -5
- data/lib/spec_forge/attribute/resolvable.rb +48 -5
- data/lib/spec_forge/attribute/resolvable_array.rb +62 -4
- data/lib/spec_forge/attribute/resolvable_hash.rb +62 -4
- data/lib/spec_forge/attribute/store.rb +65 -0
- data/lib/spec_forge/attribute/transform.rb +33 -5
- data/lib/spec_forge/attribute/variable.rb +37 -6
- data/lib/spec_forge/attribute.rb +168 -66
- data/lib/spec_forge/backtrace_formatter.rb +26 -3
- data/lib/spec_forge/callbacks.rb +79 -0
- data/lib/spec_forge/cli/actions.rb +27 -0
- data/lib/spec_forge/cli/command.rb +78 -24
- data/lib/spec_forge/cli/init.rb +11 -1
- data/lib/spec_forge/cli/new.rb +54 -3
- data/lib/spec_forge/cli/run.rb +20 -0
- data/lib/spec_forge/cli.rb +16 -5
- data/lib/spec_forge/configuration.rb +94 -25
- data/lib/spec_forge/context/callbacks.rb +91 -0
- data/lib/spec_forge/context/global.rb +72 -0
- data/lib/spec_forge/context/store.rb +148 -0
- data/lib/spec_forge/context/variables.rb +91 -0
- data/lib/spec_forge/context.rb +36 -0
- data/lib/spec_forge/core_ext/rspec.rb +24 -4
- data/lib/spec_forge/error.rb +267 -113
- data/lib/spec_forge/factory.rb +33 -14
- data/lib/spec_forge/filter.rb +87 -0
- data/lib/spec_forge/forge.rb +170 -0
- data/lib/spec_forge/http/backend.rb +99 -29
- data/lib/spec_forge/http/client.rb +23 -13
- data/lib/spec_forge/http/request.rb +74 -62
- data/lib/spec_forge/http/verb.rb +79 -0
- data/lib/spec_forge/http.rb +105 -0
- data/lib/spec_forge/loader.rb +254 -0
- data/lib/spec_forge/matchers.rb +130 -0
- data/lib/spec_forge/normalizer/configuration.rb +24 -11
- data/lib/spec_forge/normalizer/constraint.rb +22 -9
- data/lib/spec_forge/normalizer/expectation.rb +31 -12
- data/lib/spec_forge/normalizer/factory.rb +24 -11
- data/lib/spec_forge/normalizer/factory_reference.rb +32 -13
- data/lib/spec_forge/normalizer/global_context.rb +88 -0
- data/lib/spec_forge/normalizer/spec.rb +39 -16
- data/lib/spec_forge/normalizer.rb +255 -41
- data/lib/spec_forge/runner/callbacks.rb +246 -0
- data/lib/spec_forge/runner/debug_proxy.rb +213 -0
- data/lib/spec_forge/runner/listener.rb +54 -0
- data/lib/spec_forge/runner/metadata.rb +58 -0
- data/lib/spec_forge/runner/state.rb +99 -0
- data/lib/spec_forge/runner.rb +133 -119
- data/lib/spec_forge/spec/expectation/constraint.rb +95 -20
- data/lib/spec_forge/spec/expectation.rb +43 -51
- data/lib/spec_forge/spec.rb +83 -96
- data/lib/spec_forge/type.rb +36 -4
- data/lib/spec_forge/version.rb +4 -1
- data/lib/spec_forge.rb +161 -76
- metadata +20 -5
- data/spec_forge/factories/user.yml +0 -4
- data/spec_forge/forge_helper.rb +0 -37
- data/spec_forge/specs/users.yml +0 -65
@@ -0,0 +1,170 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SpecForge
|
4
|
+
#
|
5
|
+
# Represents a collection of related specs loaded from a single YAML file
|
6
|
+
#
|
7
|
+
# A Forge contains multiple specs with their expectations, global variables,
|
8
|
+
# and request configuration. It acts as the container for all tests defined
|
9
|
+
# in a single file and manages their shared context.
|
10
|
+
#
|
11
|
+
# @example Creating a forge
|
12
|
+
# global = {variables: {api_key: "123"}}
|
13
|
+
# metadata = {file_name: "users", file_path: "/path/to/users.yml"}
|
14
|
+
# specs = [{name: "list_users", url: "/users", expectations: [...]}]
|
15
|
+
# forge = Forge.new(global, metadata, specs)
|
16
|
+
#
|
17
|
+
class Forge
|
18
|
+
#
|
19
|
+
# The name of this forge from the relative path
|
20
|
+
#
|
21
|
+
# @return [String] The name derived from the file path
|
22
|
+
#
|
23
|
+
attr_reader :name
|
24
|
+
|
25
|
+
#
|
26
|
+
# Global variables and configuration shared across all specs
|
27
|
+
#
|
28
|
+
# @return [Hash] The global variables and configuration
|
29
|
+
#
|
30
|
+
attr_reader :global
|
31
|
+
|
32
|
+
#
|
33
|
+
# Metadata about the spec file
|
34
|
+
#
|
35
|
+
# @return [Hash] File information such as path and name
|
36
|
+
#
|
37
|
+
attr_reader :metadata
|
38
|
+
|
39
|
+
#
|
40
|
+
# Variables defined at the spec and expectation levels
|
41
|
+
#
|
42
|
+
# @return [Hash] Variable definitions organized by spec
|
43
|
+
#
|
44
|
+
attr_reader :variables
|
45
|
+
|
46
|
+
#
|
47
|
+
# Request configuration for the specs
|
48
|
+
#
|
49
|
+
# @return [Hash] HTTP request configuration by spec
|
50
|
+
#
|
51
|
+
attr_reader :request
|
52
|
+
|
53
|
+
#
|
54
|
+
# Collection of specs contained in this forge
|
55
|
+
#
|
56
|
+
# @return [Array<Spec>] The specs defined in this file
|
57
|
+
#
|
58
|
+
attr_accessor :specs
|
59
|
+
|
60
|
+
#
|
61
|
+
# Creates a new Forge instance containing specs from a YAML file
|
62
|
+
#
|
63
|
+
# @param global [Hash] Global variables shared across all specs in the file
|
64
|
+
# @param metadata [Hash] Information about the spec file
|
65
|
+
# @param specs [Array<Hash>] Array of spec definitions from the file
|
66
|
+
#
|
67
|
+
# @return [Forge] A new forge instance with the processed specs
|
68
|
+
#
|
69
|
+
def initialize(global, metadata, specs)
|
70
|
+
@name = metadata[:relative_path]
|
71
|
+
|
72
|
+
@global = global
|
73
|
+
@metadata = metadata
|
74
|
+
|
75
|
+
@variables = extract_variables!(specs)
|
76
|
+
@request = extract_request!(specs)
|
77
|
+
@specs = specs.map { |spec| Spec.new(**spec) }
|
78
|
+
end
|
79
|
+
|
80
|
+
#
|
81
|
+
# Retrieves variables for a specific spec
|
82
|
+
#
|
83
|
+
# Returns the variables defined for a specific spec, including
|
84
|
+
# both base variables and any overlay variables for its expectations.
|
85
|
+
#
|
86
|
+
# @param spec [Spec] The spec to get variables for
|
87
|
+
#
|
88
|
+
# @return [Hash] The variables for the spec
|
89
|
+
#
|
90
|
+
def variables_for_spec(spec)
|
91
|
+
@variables[spec.id]
|
92
|
+
end
|
93
|
+
|
94
|
+
private
|
95
|
+
|
96
|
+
#
|
97
|
+
# Extracts variables from specs and organizes them into base and overlay variables
|
98
|
+
#
|
99
|
+
# @param specs [Array<Hash>] Array of spec definitions
|
100
|
+
#
|
101
|
+
# @return [Hash] A hash mapping spec IDs to their variables
|
102
|
+
#
|
103
|
+
# @private
|
104
|
+
#
|
105
|
+
def extract_variables!(specs)
|
106
|
+
#
|
107
|
+
# Creates a hash that looks like this:
|
108
|
+
#
|
109
|
+
# {
|
110
|
+
# spec_1: {
|
111
|
+
# base: {var_1: true, var_2: false},
|
112
|
+
# overlay: {
|
113
|
+
# expectation: {var_1: false}
|
114
|
+
# }
|
115
|
+
# },
|
116
|
+
# spec_2: ...
|
117
|
+
# }
|
118
|
+
#
|
119
|
+
specs.each_with_object({}) do |spec, hash|
|
120
|
+
overlay = spec[:expectations].to_h { |e| [e[:id], e.delete(:variables)] }
|
121
|
+
.reject { |_k, v| v.blank? }
|
122
|
+
|
123
|
+
hash[spec[:id]] = {base: spec.delete(:variables), overlay:}
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
#
|
128
|
+
# Extracts request configuration from specs and organizes them into base and overlay configs
|
129
|
+
#
|
130
|
+
# @param specs [Array<Hash>] Array of spec definitions
|
131
|
+
#
|
132
|
+
# @return [Hash] A hash mapping spec IDs to their request configurations
|
133
|
+
#
|
134
|
+
# @private
|
135
|
+
#
|
136
|
+
def extract_request!(specs)
|
137
|
+
#
|
138
|
+
# Creates a hash that looks like this:
|
139
|
+
#
|
140
|
+
# {
|
141
|
+
# spec_1: {
|
142
|
+
# base: {base_url: "https://foo.bar", url: "", ...},
|
143
|
+
# overlay: {
|
144
|
+
# expectation: {base_url: "https://bar.baz", ...}
|
145
|
+
# }
|
146
|
+
# },
|
147
|
+
# spec_2: ...
|
148
|
+
# }
|
149
|
+
#
|
150
|
+
config = SpecForge.configuration.to_h.slice(:base_url, :headers, :query)
|
151
|
+
|
152
|
+
specs.each_with_object({}) do |spec, hash|
|
153
|
+
overlay = spec[:expectations].to_h do |expectation|
|
154
|
+
[
|
155
|
+
expectation[:id],
|
156
|
+
expectation.extract!(*HTTP::REQUEST_ATTRIBUTES).reject { |_k, v| v.blank? }
|
157
|
+
]
|
158
|
+
end
|
159
|
+
|
160
|
+
overlay.reject! { |_k, v| v.blank? }
|
161
|
+
|
162
|
+
base = spec.extract!(*HTTP::REQUEST_ATTRIBUTES)
|
163
|
+
base = config.deep_merge(base)
|
164
|
+
base[:http_verb] ||= "GET"
|
165
|
+
|
166
|
+
hash[spec[:id]] = {base:, overlay:}
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
@@ -2,16 +2,44 @@
|
|
2
2
|
|
3
3
|
module SpecForge
|
4
4
|
module HTTP
|
5
|
+
#
|
6
|
+
# Handles the low-level HTTP operations using Faraday
|
7
|
+
#
|
8
|
+
# This class is responsible for creating and configuring the Faraday connection,
|
9
|
+
# executing the actual HTTP requests, and handling URL path parameter substitution.
|
10
|
+
#
|
11
|
+
# @example Basic usage
|
12
|
+
# backend = Backend.new(request)
|
13
|
+
# response = backend.get("/users")
|
14
|
+
#
|
5
15
|
class Backend
|
16
|
+
#
|
17
|
+
# Regular expression to match { placeholder } style URL parameters
|
18
|
+
#
|
19
|
+
# @return [Regexp]
|
20
|
+
#
|
6
21
|
CURLY_PLACEHOLDER = /\{(\w+)\}/
|
22
|
+
|
23
|
+
#
|
24
|
+
# Regular expression to match :placeholder style URL parameters
|
25
|
+
#
|
26
|
+
# @return [Regexp]
|
27
|
+
#
|
7
28
|
COLON_PLACEHOLDER = /:(\w+)/
|
8
29
|
|
30
|
+
#
|
31
|
+
# The configured Faraday connection
|
32
|
+
#
|
33
|
+
# @return [Faraday::Connection]
|
34
|
+
#
|
9
35
|
attr_reader :connection
|
10
36
|
|
11
37
|
#
|
12
|
-
# Configures Faraday
|
38
|
+
# Configures a new Faraday connection based on the request configuration
|
39
|
+
#
|
40
|
+
# @param request [HTTP::Request] The request configuration to use
|
13
41
|
#
|
14
|
-
# @
|
42
|
+
# @return [Backend] A new backend instance with a configured connection
|
15
43
|
#
|
16
44
|
def initialize(request)
|
17
45
|
@connection =
|
@@ -23,7 +51,7 @@ module SpecForge
|
|
23
51
|
end
|
24
52
|
|
25
53
|
# Headers
|
26
|
-
builder.headers.merge!(request.headers.
|
54
|
+
builder.headers.merge!(request.headers.resolved)
|
27
55
|
end
|
28
56
|
end
|
29
57
|
|
@@ -31,79 +59,110 @@ module SpecForge
|
|
31
59
|
# Executes a DELETE request to <base_url>/<provided_url>
|
32
60
|
#
|
33
61
|
# @param url [String] The URL path to DELETE
|
34
|
-
# @param
|
35
|
-
# @param
|
62
|
+
# @param headers [Hash] HTTP headers to add
|
63
|
+
# @param query [Hash] Any query parameters to send
|
64
|
+
# @param body [Hash] Any body data to send
|
36
65
|
#
|
37
|
-
# @return [
|
66
|
+
# @return [Faraday::Response] The HTTP response
|
38
67
|
#
|
39
|
-
def delete(url, query: {}, body: {})
|
68
|
+
def delete(url, headers: {}, query: {}, body: {})
|
40
69
|
url = normalize_url(url, query)
|
41
|
-
connection.delete(url) { |request| update_request(request, query, body) }
|
70
|
+
connection.delete(url) { |request| update_request(request, headers, query, body) }
|
42
71
|
end
|
43
72
|
|
44
73
|
#
|
45
74
|
# Executes a GET request to <base_url>/<provided_url>
|
46
75
|
#
|
47
76
|
# @param url [String] The URL path to GET
|
48
|
-
# @param
|
49
|
-
# @param
|
77
|
+
# @param headers [Hash] HTTP headers to add
|
78
|
+
# @param query [Hash] Any query parameters to send
|
79
|
+
# @param body [Hash] Any body data to send
|
50
80
|
#
|
51
|
-
# @return [
|
81
|
+
# @return [Faraday::Response] The HTTP response
|
52
82
|
#
|
53
|
-
def get(url, query: {}, body: {})
|
83
|
+
def get(url, headers: {}, query: {}, body: {})
|
54
84
|
url = normalize_url(url, query)
|
55
|
-
connection.get(url) { |request| update_request(request, query, body) }
|
85
|
+
connection.get(url) { |request| update_request(request, headers, query, body) }
|
56
86
|
end
|
57
87
|
|
58
88
|
#
|
59
89
|
# Executes a PATCH request to <base_url>/<provided_url>
|
60
90
|
#
|
61
91
|
# @param url [String] The URL path to PATCH
|
62
|
-
# @param
|
63
|
-
# @param
|
92
|
+
# @param headers [Hash] HTTP headers to add
|
93
|
+
# @param query [Hash] Any query parameters to send
|
94
|
+
# @param body [Hash] Any body data to send
|
64
95
|
#
|
65
|
-
# @return [
|
96
|
+
# @return [Faraday::Response] The HTTP response
|
66
97
|
#
|
67
|
-
def patch(url, query: {}, body: {})
|
98
|
+
def patch(url, headers: {}, query: {}, body: {})
|
68
99
|
url = normalize_url(url, query)
|
69
|
-
connection.patch(url) { |request| update_request(request, query, body) }
|
100
|
+
connection.patch(url) { |request| update_request(request, headers, query, body) }
|
70
101
|
end
|
71
102
|
|
72
103
|
#
|
73
104
|
# Executes a POST request to <base_url>/<provided_url>
|
74
105
|
#
|
75
106
|
# @param url [String] The URL path to POST
|
76
|
-
# @param
|
77
|
-
# @param
|
107
|
+
# @param headers [Hash] HTTP headers to add
|
108
|
+
# @param query [Hash] Any query parameters to send
|
109
|
+
# @param body [Hash] Any body data to send
|
78
110
|
#
|
79
|
-
# @return [
|
111
|
+
# @return [Faraday::Response] The HTTP response
|
80
112
|
#
|
81
|
-
def post(url, query: {}, body: {})
|
113
|
+
def post(url, headers: {}, query: {}, body: {})
|
82
114
|
url = normalize_url(url, query)
|
83
|
-
connection.post(url) { |request| update_request(request, query, body) }
|
115
|
+
connection.post(url) { |request| update_request(request, headers, query, body) }
|
84
116
|
end
|
85
117
|
|
86
118
|
#
|
87
119
|
# Executes a PUT request to <base_url>/<provided_url>
|
88
120
|
#
|
89
121
|
# @param url [String] The URL path to PUT
|
90
|
-
# @param
|
91
|
-
# @param
|
122
|
+
# @param headers [Hash] HTTP headers to add
|
123
|
+
# @param query [Hash] Any query parameters to send
|
124
|
+
# @param body [Hash] Any body data to send
|
92
125
|
#
|
93
|
-
# @return [
|
126
|
+
# @return [Faraday::Response] The HTTP response
|
94
127
|
#
|
95
|
-
def put(url, query: {}, body: {})
|
128
|
+
def put(url, headers: {}, query: {}, body: {})
|
96
129
|
url = normalize_url(url, query)
|
97
|
-
connection.put(url) { |request| update_request(request, query, body) }
|
130
|
+
connection.put(url) { |request| update_request(request, headers, query, body) }
|
98
131
|
end
|
99
132
|
|
100
133
|
private
|
101
134
|
|
102
|
-
|
135
|
+
#
|
136
|
+
# Updates the request with query parameters and body
|
137
|
+
#
|
138
|
+
# @param request [Faraday::Request] The request to update
|
139
|
+
# @param headers [Hash] HTTP headers to add
|
140
|
+
# @param query [Hash] Query parameters to add
|
141
|
+
# @param body [Hash] Body data to add
|
142
|
+
#
|
143
|
+
# @private
|
144
|
+
#
|
145
|
+
def update_request(request, headers, query, body)
|
146
|
+
request.headers.merge!(headers)
|
103
147
|
request.params.merge!(query)
|
104
148
|
request.body = body.to_json
|
105
149
|
end
|
106
150
|
|
151
|
+
#
|
152
|
+
# Normalizes a URL by replacing path parameters with their values
|
153
|
+
#
|
154
|
+
# Handles both curly brace style {param} and colon style :param
|
155
|
+
# Parameters are extracted from the query hash and removed after substitution
|
156
|
+
#
|
157
|
+
# @param url [String] The URL pattern with potential placeholders
|
158
|
+
# @param query [Hash] Query parameters that may contain values for placeholders
|
159
|
+
#
|
160
|
+
# @return [String] The URL with placeholders replaced by actual values
|
161
|
+
#
|
162
|
+
# @raise [URI::InvalidURIError] If the resulting URL is invalid
|
163
|
+
#
|
164
|
+
# @private
|
165
|
+
#
|
107
166
|
def normalize_url(url, query)
|
108
167
|
# /users/<user_id>
|
109
168
|
url = replace_url_placeholder(url, query, CURLY_PLACEHOLDER)
|
@@ -122,6 +181,17 @@ module SpecForge
|
|
122
181
|
url
|
123
182
|
end
|
124
183
|
|
184
|
+
#
|
185
|
+
# Replaces URL placeholders with values from the query hash
|
186
|
+
#
|
187
|
+
# @param url [String] The URL with placeholders
|
188
|
+
# @param query [Hash] The query parameters containing values
|
189
|
+
# @param regex [Regexp] The pattern to match (curly or colon style)
|
190
|
+
#
|
191
|
+
# @return [String] The URL with placeholders replaced
|
192
|
+
#
|
193
|
+
# @private
|
194
|
+
#
|
125
195
|
def replace_url_placeholder(url, query, regex)
|
126
196
|
match = url.match(regex)
|
127
197
|
return url if match.nil?
|
@@ -2,30 +2,40 @@
|
|
2
2
|
|
3
3
|
module SpecForge
|
4
4
|
module HTTP
|
5
|
+
#
|
6
|
+
# HTTP client that executes requests and returns responses
|
7
|
+
#
|
8
|
+
# This class serves as a mediator between the test expectations
|
9
|
+
# and the actual HTTP backend implementation.
|
10
|
+
#
|
11
|
+
# @example Basic usage
|
12
|
+
# client = HTTP::Client.new(base_url: "https://api.example.com")
|
13
|
+
# response = client.call(request)
|
14
|
+
#
|
5
15
|
class Client
|
6
|
-
attr_reader :request
|
7
|
-
|
8
16
|
#
|
9
|
-
# Creates a new HTTP client
|
17
|
+
# Creates a new HTTP client with configured backend
|
10
18
|
#
|
11
|
-
# @
|
19
|
+
# @return [Client] A new HTTP client instance
|
12
20
|
#
|
13
21
|
def initialize(**)
|
14
|
-
@
|
15
|
-
@adapter = Backend.new(request)
|
22
|
+
@backend = Backend.new(HTTP::Request.new(**))
|
16
23
|
end
|
17
24
|
|
18
25
|
#
|
19
|
-
#
|
26
|
+
# Executes an HTTP request and returns the response
|
27
|
+
#
|
28
|
+
# @param request [HTTP::Request] The request to execute
|
20
29
|
#
|
21
|
-
# @return [
|
30
|
+
# @return [Faraday::Response] The HTTP response
|
22
31
|
#
|
23
|
-
def call
|
24
|
-
@
|
25
|
-
request.http_verb,
|
32
|
+
def call(request)
|
33
|
+
@backend.public_send(
|
34
|
+
request.http_verb.to_s.downcase,
|
26
35
|
request.url,
|
27
|
-
|
28
|
-
|
36
|
+
headers: request.headers.resolved,
|
37
|
+
query: request.query.resolved,
|
38
|
+
body: request.body.resolved
|
29
39
|
)
|
30
40
|
end
|
31
41
|
end
|
@@ -2,88 +2,100 @@
|
|
2
2
|
|
3
3
|
module SpecForge
|
4
4
|
module HTTP
|
5
|
-
|
6
|
-
|
5
|
+
#
|
6
|
+
# The attributes used to build a Request
|
7
|
+
#
|
8
|
+
# @return [Array<Symbol>]
|
9
|
+
#
|
10
|
+
REQUEST_ATTRIBUTES = [:base_url, :url, :http_verb, :headers, :query, :body].freeze
|
7
11
|
|
12
|
+
#
|
13
|
+
# Represents an HTTP request configuration
|
14
|
+
#
|
15
|
+
# This data object contains all the necessary information to construct
|
16
|
+
# an HTTP request, including URL, method, headers, query params, and body.
|
17
|
+
#
|
18
|
+
# @example Creating a request
|
19
|
+
# request = HTTP::Request.new(
|
20
|
+
# base_url: "https://api.example.com",
|
21
|
+
# url: "/users",
|
22
|
+
# http_verb: "GET",
|
23
|
+
# headers: {"Content-Type" => "application/json"},
|
24
|
+
# query: {page: 1},
|
25
|
+
# body: {}
|
26
|
+
# )
|
27
|
+
#
|
28
|
+
class Request < Data.define(*REQUEST_ATTRIBUTES)
|
8
29
|
#
|
9
|
-
#
|
10
|
-
#
|
11
|
-
# @param [Hash] options The options to create the Request with
|
30
|
+
# Regex that attempts to match a valid header
|
12
31
|
#
|
13
|
-
# @
|
32
|
+
# @return [Regexp]
|
14
33
|
#
|
15
|
-
|
34
|
+
HEADER = /^[A-Z][A-Za-z0-9!-]*$/
|
35
|
+
|
16
36
|
#
|
17
|
-
#
|
37
|
+
# Creates a new Request with standardized headers and values
|
18
38
|
#
|
19
|
-
# @
|
39
|
+
# @param base_url [String, nil] The base URL for the request
|
40
|
+
# @param url [String, nil] The path portion of the URL
|
41
|
+
# @param http_verb [String, Symbol, nil] The HTTP method (GET, POST, etc.)
|
42
|
+
# @param headers [Hash, nil] HTTP headers for the request
|
43
|
+
# @param query [Hash, nil] Query parameters to include
|
44
|
+
# @param body [Hash, nil] Request body data
|
20
45
|
#
|
21
|
-
# @
|
46
|
+
# @return [Request] A new immutable request object
|
22
47
|
#
|
23
48
|
def initialize(**options)
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
49
|
+
base_url = options[:base_url] || ""
|
50
|
+
url = options[:url] || ""
|
51
|
+
http_verb = Verb.from(options[:http_verb].presence || "GET")
|
52
|
+
query = Attribute.from(options[:query] || {})
|
53
|
+
body = Attribute.from(options[:body] || {})
|
54
|
+
headers = normalize_headers(options[:headers] || {})
|
30
55
|
|
31
|
-
super(base_url:, url:,
|
32
|
-
end
|
33
|
-
|
34
|
-
def http_verb
|
35
|
-
http_method.name.downcase
|
56
|
+
super(base_url:, url:, http_verb:, headers:, query:, body:)
|
36
57
|
end
|
37
58
|
|
59
|
+
#
|
60
|
+
# Returns a hash representation with all attributes fully resolved
|
61
|
+
#
|
62
|
+
# @return [Hash] The request data with all dynamic values resolved
|
63
|
+
#
|
38
64
|
def to_h
|
39
|
-
super.transform_values { |v| v.respond_to?(:
|
65
|
+
super.transform_values { |v| v.respond_to?(:resolved) ? v.resolved : v }
|
40
66
|
end
|
41
67
|
|
42
68
|
private
|
43
69
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
def normalize_headers(options)
|
63
|
-
headers = options[:headers].transform_keys do |key|
|
64
|
-
key = key.to_s
|
70
|
+
#
|
71
|
+
# Normalizes HTTP header keys to standard format
|
72
|
+
#
|
73
|
+
# Converts snake_case and other formats to HTTP Header-Case format
|
74
|
+
# Examples:
|
75
|
+
# content_type -> Content-Type
|
76
|
+
# api_key -> Api-Key
|
77
|
+
#
|
78
|
+
# @param headers [Hash] The headers to normalize
|
79
|
+
#
|
80
|
+
# @return [Attribute::ResolvableHash] Normalized headers as attributes
|
81
|
+
#
|
82
|
+
# @private
|
83
|
+
#
|
84
|
+
def normalize_headers(headers)
|
85
|
+
headers =
|
86
|
+
headers.transform_keys do |key|
|
87
|
+
key = key.to_s
|
65
88
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
89
|
+
# If the key is already like a header, don't change it
|
90
|
+
if key.match?(HEADER)
|
91
|
+
key
|
92
|
+
else
|
93
|
+
# content_type => Content-Type
|
94
|
+
key.downcase.titleize.gsub(/\s+/, "-")
|
95
|
+
end
|
72
96
|
end
|
73
|
-
end
|
74
|
-
|
75
|
-
headers = Attribute.bind_variables(headers, options[:variables])
|
76
|
-
Attribute::ResolvableHash.new(headers)
|
77
|
-
end
|
78
|
-
|
79
|
-
def normalize_query(options)
|
80
|
-
query = Attribute.bind_variables(options[:query], options[:variables])
|
81
|
-
Attribute::ResolvableHash.new(query)
|
82
|
-
end
|
83
97
|
|
84
|
-
|
85
|
-
body = Attribute.bind_variables(options[:body], options[:variables])
|
86
|
-
Attribute::ResolvableHash.new(body)
|
98
|
+
Attribute.from(headers)
|
87
99
|
end
|
88
100
|
end
|
89
101
|
end
|