spec_forge 0.1.0 → 0.2.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/CHANGELOG.md +51 -2
- data/README.md +366 -143
- data/lib/spec_forge/attribute/chainable.rb +19 -16
- data/lib/spec_forge/attribute/factory.rb +6 -18
- data/lib/spec_forge/attribute/faker.rb +56 -20
- data/lib/spec_forge/attribute/literal.rb +4 -0
- data/lib/spec_forge/attribute/resolvable.rb +4 -6
- data/lib/spec_forge/attribute/resolvable_array.rb +4 -0
- data/lib/spec_forge/attribute/resolvable_hash.rb +4 -0
- data/lib/spec_forge/attribute/variable.rb +6 -13
- data/lib/spec_forge/attribute.rb +3 -12
- data/lib/spec_forge/cli/init.rb +2 -9
- data/lib/spec_forge/configuration.rb +58 -0
- data/lib/spec_forge/factory.rb +4 -4
- data/lib/spec_forge/http/backend.rb +42 -8
- data/lib/spec_forge/http/client.rb +2 -2
- data/lib/spec_forge/http/request.rb +11 -14
- data/lib/spec_forge/normalizer/configuration.rb +77 -0
- data/lib/spec_forge/normalizer/expectation.rb +1 -0
- data/lib/spec_forge/normalizer/spec.rb +1 -0
- data/lib/spec_forge/normalizer.rb +14 -11
- data/lib/spec_forge/runner.rb +98 -72
- data/lib/spec_forge/spec/expectation/constraint.rb +2 -5
- data/lib/spec_forge/spec/expectation.rb +32 -13
- data/lib/spec_forge/spec.rb +20 -14
- data/lib/spec_forge/version.rb +1 -1
- data/lib/spec_forge.rb +20 -11
- data/lib/templates/forge_helper.tt +48 -0
- data/spec_forge/forge_helper.rb +37 -0
- data/spec_forge/specs/users.yml +6 -4
- metadata +6 -7
- data/lib/spec_forge/config.rb +0 -84
- data/lib/spec_forge/environment.rb +0 -71
- data/lib/spec_forge/normalizer/config.rb +0 -104
- data/lib/templates/config.tt +0 -19
- data/spec_forge/config.yml +0 -19
@@ -5,7 +5,7 @@ module SpecForge
|
|
5
5
|
module Chainable
|
6
6
|
NUMBER_REGEX = /^\d+$/i
|
7
7
|
|
8
|
-
attr_reader :invocation_chain, :base_object
|
8
|
+
attr_reader :header, :invocation_chain, :base_object
|
9
9
|
|
10
10
|
#
|
11
11
|
# Represents any attribute that is a series of chained invocations:
|
@@ -21,37 +21,40 @@ module SpecForge
|
|
21
21
|
# Drop the keyword
|
22
22
|
sections = input.split(".")[1..]
|
23
23
|
|
24
|
-
|
25
|
-
@invocation_chain = sections || []
|
24
|
+
@header = sections.first&.to_sym
|
25
|
+
@invocation_chain = sections[1..] || []
|
26
26
|
end
|
27
27
|
|
28
28
|
def value
|
29
29
|
invoke_chain
|
30
30
|
end
|
31
31
|
|
32
|
-
#
|
33
|
-
# Custom implementation to ensure the underlying values are resolved
|
34
|
-
# without breaking #value's functionality
|
35
|
-
#
|
36
32
|
def resolve
|
37
|
-
@resolved ||=
|
33
|
+
@resolved ||= resolve_chain
|
38
34
|
end
|
39
35
|
|
40
36
|
private
|
41
37
|
|
42
|
-
def invoke_chain
|
43
|
-
|
38
|
+
def invoke_chain
|
39
|
+
traverse_chain(resolve: false)
|
40
|
+
end
|
41
|
+
|
42
|
+
def resolve_chain
|
43
|
+
__resolve(traverse_chain(resolve: true))
|
44
|
+
end
|
45
|
+
|
46
|
+
def traverse_chain(resolve:)
|
47
|
+
result = invocation_chain.reduce(base_object) do |current_value, step|
|
48
|
+
next_value = retrieve_value(current_value, resolve:)
|
44
49
|
|
45
|
-
|
46
|
-
object = retrieve_value(current_value, resolve:)
|
47
|
-
current_value = invoke(step, object)
|
50
|
+
invoke(step, next_value)
|
48
51
|
end
|
49
52
|
|
50
|
-
retrieve_value(
|
53
|
+
retrieve_value(result, resolve:)
|
51
54
|
end
|
52
55
|
|
53
|
-
def retrieve_value(object, resolve:
|
54
|
-
return object
|
56
|
+
def retrieve_value(object, resolve:)
|
57
|
+
return object unless object.is_a?(Attribute)
|
55
58
|
|
56
59
|
resolve ? object.resolve : object.value
|
57
60
|
end
|
@@ -14,7 +14,7 @@ module SpecForge
|
|
14
14
|
build_stubbed
|
15
15
|
].freeze
|
16
16
|
|
17
|
-
|
17
|
+
alias_method :factory_name, :header
|
18
18
|
|
19
19
|
#
|
20
20
|
# Represents any attribute that is a factory reference
|
@@ -24,39 +24,27 @@ module SpecForge
|
|
24
24
|
def initialize(...)
|
25
25
|
super
|
26
26
|
|
27
|
-
@factory_name = invocation_chain.shift&.to_sym
|
28
|
-
|
29
27
|
# Check the arguments before preparing them
|
30
28
|
arguments[:keyword] = Normalizer.normalize_factory_reference!(arguments[:keyword])
|
31
29
|
|
32
30
|
prepare_arguments!
|
33
31
|
end
|
34
32
|
|
35
|
-
def value
|
36
|
-
@base_object = create_factory_object
|
37
|
-
super
|
38
|
-
end
|
39
|
-
|
40
|
-
def resolve
|
41
|
-
@base_object = create_factory_object
|
42
|
-
super
|
43
|
-
end
|
44
|
-
|
45
33
|
private
|
46
34
|
|
47
|
-
def
|
35
|
+
def base_object
|
48
36
|
attributes = arguments[:keyword]
|
49
|
-
return FactoryBot.create(
|
37
|
+
return FactoryBot.create(factory_name) if attributes.blank?
|
50
38
|
|
51
39
|
# Determine build strat
|
52
|
-
build_strategy = attributes[:build_strategy].
|
40
|
+
build_strategy = attributes[:build_strategy].resolve_value
|
53
41
|
|
54
42
|
# stubbed => build_stubbed
|
55
43
|
build_strategy.prepend("build_") if build_strategy == "stubbed"
|
56
44
|
raise InvalidBuildStrategy, build_strategy unless BUILD_STRATEGIES.include?(build_strategy)
|
57
45
|
|
58
|
-
attributes = attributes[:attributes].
|
59
|
-
FactoryBot.public_send(build_strategy,
|
46
|
+
attributes = attributes[:attributes].resolve_value
|
47
|
+
FactoryBot.public_send(build_strategy, factory_name, **attributes)
|
60
48
|
end
|
61
49
|
end
|
62
50
|
end
|
@@ -3,6 +3,8 @@
|
|
3
3
|
module SpecForge
|
4
4
|
class Attribute
|
5
5
|
class Faker < Parameterized
|
6
|
+
include Chainable
|
7
|
+
|
6
8
|
KEYWORD_REGEX = /^faker\./i
|
7
9
|
|
8
10
|
attr_reader :faker_class, :faker_method
|
@@ -15,39 +17,73 @@ module SpecForge
|
|
15
17
|
def initialize(...)
|
16
18
|
super
|
17
19
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
20
|
+
@faker_class, @faker_method = extract_faker_call
|
21
|
+
|
22
|
+
prepare_arguments!
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def base_object
|
28
|
+
if uses_positional_arguments?(faker_method)
|
29
|
+
faker_method.call(*arguments[:positional].resolve)
|
30
|
+
elsif uses_keyword_arguments?(faker_method)
|
31
|
+
faker_method.call(**arguments[:keyword].resolve)
|
32
|
+
else
|
33
|
+
faker_method.call
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def extract_faker_call
|
38
|
+
class_name = header.downcase.to_s
|
39
|
+
|
40
|
+
# Simple case: faker.<header>.<method>
|
41
|
+
if invocation_chain.size == 1
|
42
|
+
return resolve_faker_class_and_method(class_name, invocation_chain.shift)
|
43
|
+
end
|
44
|
+
|
45
|
+
# Try each part of the chain as a potential class name
|
46
|
+
# Example: faker.games.zelda.game.underscore
|
47
|
+
namespace = []
|
48
|
+
|
49
|
+
while invocation_chain.any?
|
50
|
+
part = invocation_chain.first.downcase
|
51
|
+
test_class_name = ([class_name] + namespace + [part]).map(&:camelize).join("::")
|
52
|
+
|
53
|
+
begin
|
54
|
+
"::Faker::#{test_class_name}".constantize
|
22
55
|
|
23
|
-
|
24
|
-
|
56
|
+
namespace << invocation_chain.shift
|
57
|
+
rescue NameError
|
58
|
+
# This part isn't a valid class, so it must be our method
|
59
|
+
method_name = invocation_chain.shift
|
60
|
+
class_name = ([class_name] + namespace).map(&:camelize).join("::")
|
61
|
+
|
62
|
+
return resolve_faker_class_and_method(class_name, method_name)
|
63
|
+
end
|
64
|
+
end
|
25
65
|
|
66
|
+
# If we get here, we consumed all parts as classes but found no method
|
67
|
+
class_name = ([class_name] + namespace).map(&:camelize).join("::")
|
68
|
+
raise InvalidFakerMethodError.new(nil, "::#{class_name}".constantize)
|
69
|
+
end
|
70
|
+
|
71
|
+
def resolve_faker_class_and_method(class_name, method_name)
|
26
72
|
# Load the class
|
27
|
-
|
28
|
-
"::#{class_name}".constantize
|
73
|
+
faker_class = begin
|
74
|
+
"::Faker::#{class_name.camelize}".constantize
|
29
75
|
rescue NameError
|
30
76
|
raise InvalidFakerClassError, class_name
|
31
77
|
end
|
32
78
|
|
33
79
|
# Load the method
|
34
|
-
|
80
|
+
faker_method = begin
|
35
81
|
faker_class.method(method_name)
|
36
82
|
rescue NameError
|
37
83
|
raise InvalidFakerMethodError.new(method_name, faker_class)
|
38
84
|
end
|
39
85
|
|
40
|
-
|
41
|
-
end
|
42
|
-
|
43
|
-
def value
|
44
|
-
if uses_positional_arguments?(faker_method)
|
45
|
-
faker_method.call(*arguments[:positional].resolve)
|
46
|
-
elsif uses_keyword_arguments?(faker_method)
|
47
|
-
faker_method.call(**arguments[:keyword].resolve)
|
48
|
-
else
|
49
|
-
faker_method.call
|
50
|
-
end
|
86
|
+
[faker_class, faker_method]
|
51
87
|
end
|
52
88
|
end
|
53
89
|
end
|
@@ -6,16 +6,14 @@ module SpecForge
|
|
6
6
|
# Helpers for ResolvableHash and ResolvableArray
|
7
7
|
#
|
8
8
|
module Resolvable
|
9
|
-
# @private
|
10
|
-
def to_proc
|
11
|
-
this = self
|
12
|
-
-> { this.resolve }
|
13
|
-
end
|
14
|
-
|
15
9
|
# @private
|
16
10
|
def resolvable_proc
|
17
11
|
->(v) { v.respond_to?(:resolve) ? v.resolve : v }
|
18
12
|
end
|
13
|
+
|
14
|
+
def resolvable_value_proc
|
15
|
+
->(v) { v.respond_to?(:resolve_value) ? v.resolve_value : v }
|
16
|
+
end
|
19
17
|
end
|
20
18
|
end
|
21
19
|
end
|
@@ -16,6 +16,10 @@ module SpecForge
|
|
16
16
|
value.transform_values(&resolvable_proc)
|
17
17
|
end
|
18
18
|
|
19
|
+
def resolve_value
|
20
|
+
value.transform_values(&resolvable_value_proc)
|
21
|
+
end
|
22
|
+
|
19
23
|
def bind_variables(variables)
|
20
24
|
value.each_value { |v| Attribute.bind_variables(v, variables) }
|
21
25
|
end
|
@@ -2,24 +2,17 @@
|
|
2
2
|
|
3
3
|
module SpecForge
|
4
4
|
class Attribute
|
5
|
+
#
|
6
|
+
# Represents any attribute that is a variable reference
|
7
|
+
#
|
8
|
+
# variables.<variable_name>
|
9
|
+
#
|
5
10
|
class Variable < Attribute
|
6
11
|
include Chainable
|
7
12
|
|
8
13
|
KEYWORD_REGEX = /^variables\./i
|
9
14
|
|
10
|
-
|
11
|
-
|
12
|
-
#
|
13
|
-
# Represents any attribute that is a variable reference
|
14
|
-
#
|
15
|
-
# variables.<variable_name>
|
16
|
-
#
|
17
|
-
def initialize(...)
|
18
|
-
super
|
19
|
-
|
20
|
-
# Remove the variable name from the chain
|
21
|
-
@variable_name = invocation_chain.shift&.to_sym
|
22
|
-
end
|
15
|
+
alias_method :variable_name, :header
|
23
16
|
|
24
17
|
def bind_variables(variables)
|
25
18
|
raise InvalidTypeError.new(variables, Hash, for: "'variables'") unless Type.hash?(variables)
|
data/lib/spec_forge/attribute.rb
CHANGED
@@ -138,8 +138,6 @@ module SpecForge
|
|
138
138
|
|
139
139
|
#
|
140
140
|
# Returns the fully evaluated result, recursively resolving any nested attributes
|
141
|
-
# Note: This method can only be called once to ensure data is correct across the board
|
142
|
-
# You can still call #value if you need a new value
|
143
141
|
#
|
144
142
|
# @return [Object] The resolved value
|
145
143
|
#
|
@@ -152,18 +150,11 @@ module SpecForge
|
|
152
150
|
# attr.resolve # => [42, ["Jane"]]
|
153
151
|
#
|
154
152
|
def resolve
|
155
|
-
|
156
|
-
@resolved ||= __resolve(value)
|
153
|
+
@resolved ||= resolve_value
|
157
154
|
end
|
158
155
|
|
159
|
-
|
160
|
-
|
161
|
-
#
|
162
|
-
# @return [Proc]
|
163
|
-
#
|
164
|
-
def to_proc
|
165
|
-
this = self # kek - what are we, javascript?
|
166
|
-
-> { this.resolve }
|
156
|
+
def resolve_value
|
157
|
+
__resolve(value)
|
167
158
|
end
|
168
159
|
|
169
160
|
#
|
data/lib/spec_forge/cli/init.rb
CHANGED
@@ -13,17 +13,10 @@ module SpecForge
|
|
13
13
|
actions.empty_directory "#{base_path}/specs"
|
14
14
|
|
15
15
|
actions.template(
|
16
|
-
"
|
17
|
-
SpecForge.root.join(base_path, "
|
18
|
-
context: binding
|
16
|
+
"forge_helper.tt",
|
17
|
+
SpecForge.root.join(base_path, "forge_helper.rb")
|
19
18
|
)
|
20
19
|
end
|
21
|
-
|
22
|
-
def default_authorization_value
|
23
|
-
<<~STRING.chomp
|
24
|
-
Bearer <%= ENV.fetch("API_TOKEN", "") %>
|
25
|
-
STRING
|
26
|
-
end
|
27
20
|
end
|
28
21
|
end
|
29
22
|
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SpecForge
|
4
|
+
class Configuration < Struct.new(:base_url, :headers, :query, :factories, :specs, :on_debug)
|
5
|
+
############################################################################
|
6
|
+
|
7
|
+
class Factories < Struct.new(:auto_discover, :paths)
|
8
|
+
attr_predicate :auto_discover, :paths
|
9
|
+
|
10
|
+
def initialize(auto_discover: true, paths: []) = super
|
11
|
+
end
|
12
|
+
|
13
|
+
############################################################################
|
14
|
+
|
15
|
+
def self.overlay_options(source, overlay)
|
16
|
+
source.deep_merge(overlay) do |key, source_value, overlay_value|
|
17
|
+
# If overlay has a populated value, use it
|
18
|
+
if overlay_value.present? || overlay_value == false
|
19
|
+
overlay_value
|
20
|
+
# If source is nil and overlay exists (but wasn't "present"), use overlay
|
21
|
+
elsif source_value.nil? && !overlay_value.nil?
|
22
|
+
overlay_value
|
23
|
+
# Otherwise keep source value
|
24
|
+
else
|
25
|
+
source_value
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def initialize
|
31
|
+
config = Normalizer.default_configuration
|
32
|
+
|
33
|
+
config[:base_url] = "http://localhost:3000"
|
34
|
+
config[:factories] = Factories.new
|
35
|
+
config[:specs] = RSpec.configuration
|
36
|
+
config[:on_debug] = Runner::DebugProxy.default
|
37
|
+
|
38
|
+
super(**config)
|
39
|
+
end
|
40
|
+
|
41
|
+
def validate
|
42
|
+
output = Normalizer.normalize_configuration!(to_h)
|
43
|
+
|
44
|
+
# In case any value was set to `nil`
|
45
|
+
self.base_url = output[:base_url] if base_url.blank?
|
46
|
+
self.query = output[:query] if query.blank?
|
47
|
+
self.headers = output[:headers] if headers.blank?
|
48
|
+
|
49
|
+
self
|
50
|
+
end
|
51
|
+
|
52
|
+
def to_h
|
53
|
+
hash = super.except(:specs)
|
54
|
+
hash[:factories] = hash[:factories].to_h
|
55
|
+
hash
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
data/lib/spec_forge/factory.rb
CHANGED
@@ -8,11 +8,11 @@ module SpecForge
|
|
8
8
|
# @param path [String, Path] The base path where the factories directory are located
|
9
9
|
#
|
10
10
|
def self.load_and_register(base_path)
|
11
|
-
if
|
12
|
-
FactoryBot.definition_file_paths = paths
|
11
|
+
if SpecForge.configuration.factories.paths?
|
12
|
+
FactoryBot.definition_file_paths = SpecForge.configuration.factories.paths
|
13
13
|
end
|
14
14
|
|
15
|
-
FactoryBot.find_definitions if SpecForge.
|
15
|
+
FactoryBot.find_definitions if SpecForge.configuration.factories.auto_discover?
|
16
16
|
|
17
17
|
factories = load_from_path(base_path.join("factories", "**/*.yml"))
|
18
18
|
factories.each(&:register)
|
@@ -80,7 +80,7 @@ module SpecForge
|
|
80
80
|
factory_forge = self
|
81
81
|
dsl.factory(name, options) do
|
82
82
|
factory_forge.attributes.each do |name, attribute|
|
83
|
-
add_attribute(name
|
83
|
+
add_attribute(name) { attribute.resolve_value }
|
84
84
|
end
|
85
85
|
end
|
86
86
|
|
@@ -3,6 +3,9 @@
|
|
3
3
|
module SpecForge
|
4
4
|
module HTTP
|
5
5
|
class Backend
|
6
|
+
CURLY_PLACEHOLDER = /\{(\w+)\}/
|
7
|
+
COLON_PLACEHOLDER = /:(\w+)/
|
8
|
+
|
6
9
|
attr_reader :connection
|
7
10
|
|
8
11
|
#
|
@@ -13,20 +16,14 @@ module SpecForge
|
|
13
16
|
def initialize(request)
|
14
17
|
@connection =
|
15
18
|
Faraday.new(url: request.base_url) do |builder|
|
16
|
-
# Authorization
|
17
|
-
builder.headers[request.authorization.header] = request.authorization.value
|
18
|
-
|
19
19
|
# Content-Type
|
20
20
|
if !request.headers.key?("Content-Type")
|
21
21
|
builder.request :json
|
22
22
|
builder.response :json
|
23
23
|
end
|
24
24
|
|
25
|
-
# Headers
|
26
|
-
builder.headers.merge!(request.headers)
|
27
|
-
|
28
|
-
# Params
|
29
|
-
builder.params.merge!(request.query.resolve)
|
25
|
+
# Headers
|
26
|
+
builder.headers.merge!(request.headers.resolve)
|
30
27
|
end
|
31
28
|
end
|
32
29
|
|
@@ -40,6 +37,7 @@ module SpecForge
|
|
40
37
|
# @return [Hash] The response
|
41
38
|
#
|
42
39
|
def delete(url, query: {}, body: {})
|
40
|
+
url = normalize_url(url, query)
|
43
41
|
connection.delete(url) { |request| update_request(request, query, body) }
|
44
42
|
end
|
45
43
|
|
@@ -53,6 +51,7 @@ module SpecForge
|
|
53
51
|
# @return [Hash] The response
|
54
52
|
#
|
55
53
|
def get(url, query: {}, body: {})
|
54
|
+
url = normalize_url(url, query)
|
56
55
|
connection.get(url) { |request| update_request(request, query, body) }
|
57
56
|
end
|
58
57
|
|
@@ -66,6 +65,7 @@ module SpecForge
|
|
66
65
|
# @return [Hash] The response
|
67
66
|
#
|
68
67
|
def patch(url, query: {}, body: {})
|
68
|
+
url = normalize_url(url, query)
|
69
69
|
connection.patch(url) { |request| update_request(request, query, body) }
|
70
70
|
end
|
71
71
|
|
@@ -79,6 +79,7 @@ module SpecForge
|
|
79
79
|
# @return [Hash] The response
|
80
80
|
#
|
81
81
|
def post(url, query: {}, body: {})
|
82
|
+
url = normalize_url(url, query)
|
82
83
|
connection.post(url) { |request| update_request(request, query, body) }
|
83
84
|
end
|
84
85
|
|
@@ -92,6 +93,7 @@ module SpecForge
|
|
92
93
|
# @return [Hash] The response
|
93
94
|
#
|
94
95
|
def put(url, query: {}, body: {})
|
96
|
+
url = normalize_url(url, query)
|
95
97
|
connection.put(url) { |request| update_request(request, query, body) }
|
96
98
|
end
|
97
99
|
|
@@ -101,6 +103,38 @@ module SpecForge
|
|
101
103
|
request.params.merge!(query)
|
102
104
|
request.body = body.to_json
|
103
105
|
end
|
106
|
+
|
107
|
+
def normalize_url(url, query)
|
108
|
+
# /users/<user_id>
|
109
|
+
url = replace_url_placeholder(url, query, CURLY_PLACEHOLDER)
|
110
|
+
|
111
|
+
# /users/:user_id
|
112
|
+
url = replace_url_placeholder(url, query, COLON_PLACEHOLDER)
|
113
|
+
|
114
|
+
# Attempt to validate (the colon style is considered valid apparently)
|
115
|
+
begin
|
116
|
+
URI.parse(url)
|
117
|
+
rescue URI::InvalidURIError
|
118
|
+
raise URI::InvalidURIError,
|
119
|
+
"#{url.inspect} is not a valid URI. If you're using path parameters (like ':id' or '{id}'), ensure they are defined in the 'query' section."
|
120
|
+
end
|
121
|
+
|
122
|
+
url
|
123
|
+
end
|
124
|
+
|
125
|
+
def replace_url_placeholder(url, query, regex)
|
126
|
+
match = url.match(regex)
|
127
|
+
return url if match.nil?
|
128
|
+
|
129
|
+
key = match[1].to_sym
|
130
|
+
return url unless query.key?(key)
|
131
|
+
|
132
|
+
value = query.delete(key)
|
133
|
+
url.gsub(
|
134
|
+
match[0],
|
135
|
+
URI.encode_uri_component(value.to_s)
|
136
|
+
)
|
137
|
+
end
|
104
138
|
end
|
105
139
|
end
|
106
140
|
end
|
@@ -24,8 +24,8 @@ module SpecForge
|
|
24
24
|
@adapter.public_send(
|
25
25
|
request.http_verb,
|
26
26
|
request.url,
|
27
|
-
query: request.query.
|
28
|
-
body: request.body.
|
27
|
+
query: request.query.resolve,
|
28
|
+
body: request.body.resolve
|
29
29
|
)
|
30
30
|
end
|
31
31
|
end
|
@@ -2,9 +2,7 @@
|
|
2
2
|
|
3
3
|
module SpecForge
|
4
4
|
module HTTP
|
5
|
-
|
6
|
-
|
7
|
-
class Request < Data.define(*attributes)
|
5
|
+
class Request < Data.define(:base_url, :url, :http_method, :headers, :query, :body)
|
8
6
|
HEADER = /^[A-Z][A-Za-z0-9!-]*$/
|
9
7
|
|
10
8
|
#
|
@@ -23,33 +21,36 @@ module SpecForge
|
|
23
21
|
# @option options [Hash] :body The request body (defaults to {})
|
24
22
|
#
|
25
23
|
def initialize(**options)
|
26
|
-
base_url = extract_base_url(options)
|
27
24
|
url = extract_url(options)
|
28
|
-
|
25
|
+
base_url = extract_base_url(options)
|
29
26
|
http_method = normalize_http_method(options)
|
27
|
+
headers = normalize_headers(options)
|
30
28
|
query = normalize_query(options)
|
31
29
|
body = normalize_body(options)
|
32
|
-
authorization = extract_authorization(options)
|
33
30
|
|
34
|
-
super(base_url:, url:, http_method:, headers:, query:, body
|
31
|
+
super(base_url:, url:, http_method:, headers:, query:, body:)
|
35
32
|
end
|
36
33
|
|
37
34
|
def http_verb
|
38
35
|
http_method.name.downcase
|
39
36
|
end
|
40
37
|
|
38
|
+
def to_h
|
39
|
+
super.transform_values { |v| v.respond_to?(:resolve) ? v.resolve : v }
|
40
|
+
end
|
41
|
+
|
41
42
|
private
|
42
43
|
|
43
44
|
def extract_base_url(options)
|
44
|
-
options[:base_url]
|
45
|
+
options[:base_url].resolve
|
45
46
|
end
|
46
47
|
|
47
48
|
def extract_url(options)
|
48
|
-
options[:url].
|
49
|
+
options[:url].resolve
|
49
50
|
end
|
50
51
|
|
51
52
|
def normalize_http_method(options)
|
52
|
-
method = options[:http_method].
|
53
|
+
method = options[:http_method].resolve.presence || "GET"
|
53
54
|
|
54
55
|
if method.is_a?(String)
|
55
56
|
Verb.from(method)
|
@@ -84,10 +85,6 @@ module SpecForge
|
|
84
85
|
body = Attribute.bind_variables(options[:body], options[:variables])
|
85
86
|
Attribute::ResolvableHash.new(body)
|
86
87
|
end
|
87
|
-
|
88
|
-
def extract_authorization(options)
|
89
|
-
SpecForge.config.authorization.default
|
90
|
-
end
|
91
88
|
end
|
92
89
|
end
|
93
90
|
end
|