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.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/.standard.yml +4 -0
  3. data/CHANGELOG.md +145 -1
  4. data/README.md +49 -638
  5. data/flake.lock +3 -3
  6. data/flake.nix +8 -2
  7. data/lib/spec_forge/attribute/chainable.rb +208 -20
  8. data/lib/spec_forge/attribute/factory.rb +141 -12
  9. data/lib/spec_forge/attribute/faker.rb +64 -15
  10. data/lib/spec_forge/attribute/global.rb +96 -0
  11. data/lib/spec_forge/attribute/literal.rb +15 -2
  12. data/lib/spec_forge/attribute/matcher.rb +188 -13
  13. data/lib/spec_forge/attribute/parameterized.rb +45 -20
  14. data/lib/spec_forge/attribute/regex.rb +55 -5
  15. data/lib/spec_forge/attribute/resolvable.rb +48 -5
  16. data/lib/spec_forge/attribute/resolvable_array.rb +62 -4
  17. data/lib/spec_forge/attribute/resolvable_hash.rb +62 -4
  18. data/lib/spec_forge/attribute/store.rb +65 -0
  19. data/lib/spec_forge/attribute/transform.rb +33 -5
  20. data/lib/spec_forge/attribute/variable.rb +37 -6
  21. data/lib/spec_forge/attribute.rb +168 -66
  22. data/lib/spec_forge/backtrace_formatter.rb +26 -3
  23. data/lib/spec_forge/callbacks.rb +79 -0
  24. data/lib/spec_forge/cli/actions.rb +27 -0
  25. data/lib/spec_forge/cli/command.rb +78 -24
  26. data/lib/spec_forge/cli/init.rb +11 -1
  27. data/lib/spec_forge/cli/new.rb +54 -3
  28. data/lib/spec_forge/cli/run.rb +20 -0
  29. data/lib/spec_forge/cli.rb +16 -5
  30. data/lib/spec_forge/configuration.rb +94 -25
  31. data/lib/spec_forge/context/callbacks.rb +91 -0
  32. data/lib/spec_forge/context/global.rb +72 -0
  33. data/lib/spec_forge/context/store.rb +148 -0
  34. data/lib/spec_forge/context/variables.rb +91 -0
  35. data/lib/spec_forge/context.rb +36 -0
  36. data/lib/spec_forge/core_ext/rspec.rb +24 -4
  37. data/lib/spec_forge/error.rb +267 -113
  38. data/lib/spec_forge/factory.rb +33 -14
  39. data/lib/spec_forge/filter.rb +87 -0
  40. data/lib/spec_forge/forge.rb +170 -0
  41. data/lib/spec_forge/http/backend.rb +99 -29
  42. data/lib/spec_forge/http/client.rb +23 -13
  43. data/lib/spec_forge/http/request.rb +74 -62
  44. data/lib/spec_forge/http/verb.rb +79 -0
  45. data/lib/spec_forge/http.rb +105 -0
  46. data/lib/spec_forge/loader.rb +254 -0
  47. data/lib/spec_forge/matchers.rb +130 -0
  48. data/lib/spec_forge/normalizer/configuration.rb +24 -11
  49. data/lib/spec_forge/normalizer/constraint.rb +22 -9
  50. data/lib/spec_forge/normalizer/expectation.rb +31 -12
  51. data/lib/spec_forge/normalizer/factory.rb +24 -11
  52. data/lib/spec_forge/normalizer/factory_reference.rb +32 -13
  53. data/lib/spec_forge/normalizer/global_context.rb +88 -0
  54. data/lib/spec_forge/normalizer/spec.rb +39 -16
  55. data/lib/spec_forge/normalizer.rb +255 -41
  56. data/lib/spec_forge/runner/callbacks.rb +246 -0
  57. data/lib/spec_forge/runner/debug_proxy.rb +213 -0
  58. data/lib/spec_forge/runner/listener.rb +54 -0
  59. data/lib/spec_forge/runner/metadata.rb +58 -0
  60. data/lib/spec_forge/runner/state.rb +99 -0
  61. data/lib/spec_forge/runner.rb +133 -119
  62. data/lib/spec_forge/spec/expectation/constraint.rb +95 -20
  63. data/lib/spec_forge/spec/expectation.rb +43 -51
  64. data/lib/spec_forge/spec.rb +83 -96
  65. data/lib/spec_forge/type.rb +36 -4
  66. data/lib/spec_forge/version.rb +4 -1
  67. data/lib/spec_forge.rb +161 -76
  68. metadata +20 -5
  69. data/spec_forge/factories/user.yml +0 -4
  70. data/spec_forge/forge_helper.rb +0 -37
  71. 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 with the config values
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
- # @param request [HTTP::Request]
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.resolve)
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 query [Hash] Any query attributes to send
35
- # @param body [Hash] Any body data to send
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 [Hash] The response
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 query [Hash] Any query attributes to send
49
- # @param body [Hash] Any body data to send
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 [Hash] The response
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 query [Hash] Any query attributes to send
63
- # @param body [Hash] Any body data to send
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 [Hash] The response
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 query [Hash] Any query attributes to send
77
- # @param body [Hash] Any body data to send
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 [Hash] The response
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 query [Hash] Any query attributes to send
91
- # @param body [Hash] Any body data to send
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 [Hash] The response
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
- def update_request(request, query, body)
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 to middleman between the tests and the backend
17
+ # Creates a new HTTP client with configured backend
10
18
  #
11
- # @param ** [Hash] Request attributes
19
+ # @return [Client] A new HTTP client instance
12
20
  #
13
21
  def initialize(**)
14
- @request = Request.new(**)
15
- @adapter = Backend.new(request)
22
+ @backend = Backend.new(HTTP::Request.new(**))
16
23
  end
17
24
 
18
25
  #
19
- # Triggers an HTTP request to the URL
26
+ # Executes an HTTP request and returns the response
27
+ #
28
+ # @param request [HTTP::Request] The request to execute
20
29
  #
21
- # @return [Hash] The response
30
+ # @return [Faraday::Response] The HTTP response
22
31
  #
23
- def call
24
- @adapter.public_send(
25
- request.http_verb,
32
+ def call(request)
33
+ @backend.public_send(
34
+ request.http_verb.to_s.downcase,
26
35
  request.url,
27
- query: request.query.resolve,
28
- body: request.body.resolve
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
- class Request < Data.define(:base_url, :url, :http_method, :headers, :query, :body)
6
- HEADER = /^[A-Z][A-Za-z0-9!-]*$/
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
- # Initializes a new Request instance with the given options
10
- #
11
- # @param [Hash] options The options to create the Request with
30
+ # Regex that attempts to match a valid header
12
31
  #
13
- # @option options [String] :url The request URL
32
+ # @return [Regexp]
14
33
  #
15
- # @option options [String, Verb] :http_method The HTTP method to use
34
+ HEADER = /^[A-Z][A-Za-z0-9!-]*$/
35
+
16
36
  #
17
- # @option options [Hash] :headers Any headers
37
+ # Creates a new Request with standardized headers and values
18
38
  #
19
- # @option options [Hash] :query The query parameters for the request (defaults to {})
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
- # @option options [Hash] :body The request body (defaults to {})
46
+ # @return [Request] A new immutable request object
22
47
  #
23
48
  def initialize(**options)
24
- url = extract_url(options)
25
- base_url = extract_base_url(options)
26
- http_method = normalize_http_method(options)
27
- headers = normalize_headers(options)
28
- query = normalize_query(options)
29
- body = normalize_body(options)
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:, http_method:, headers:, query:, body:)
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?(:resolve) ? v.resolve : v }
65
+ super.transform_values { |v| v.respond_to?(:resolved) ? v.resolved : v }
40
66
  end
41
67
 
42
68
  private
43
69
 
44
- def extract_base_url(options)
45
- options[:base_url].resolve
46
- end
47
-
48
- def extract_url(options)
49
- options[:url].resolve
50
- end
51
-
52
- def normalize_http_method(options)
53
- method = options[:http_method].resolve.presence || "GET"
54
-
55
- if method.is_a?(String)
56
- Verb.from(method)
57
- else
58
- method
59
- end
60
- end
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
- # If the key is already like a header, don't change it
67
- if key.match?(HEADER)
68
- key
69
- else
70
- # content_type => Content-Type
71
- key.downcase.titleize.gsub(/\s+/, "-")
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
- def normalize_body(options)
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