spec_forge 0.5.0 → 0.7.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 +3 -3
- data/CHANGELOG.md +217 -2
- data/README.md +162 -25
- data/flake.lock +3 -3
- data/flake.nix +11 -5
- data/lib/spec_forge/attribute/chainable.rb +208 -20
- data/lib/spec_forge/attribute/factory.rb +92 -15
- data/lib/spec_forge/attribute/faker.rb +62 -13
- 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 +186 -11
- data/lib/spec_forge/attribute/parameterized.rb +45 -12
- 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 +166 -66
- data/lib/spec_forge/backtrace_formatter.rb +26 -3
- data/lib/spec_forge/callbacks.rb +88 -0
- data/lib/spec_forge/cli/actions.rb +27 -0
- data/lib/spec_forge/cli/command.rb +78 -24
- data/lib/spec_forge/cli/docs/generate.rb +72 -0
- data/lib/spec_forge/cli/docs.rb +92 -0
- data/lib/spec_forge/cli/init.rb +51 -9
- data/lib/spec_forge/cli/new.rb +67 -6
- data/lib/spec_forge/cli/run.rb +32 -4
- data/lib/spec_forge/cli/serve.rb +155 -0
- data/lib/spec_forge/cli.rb +26 -7
- data/lib/spec_forge/configuration.rb +96 -24
- 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 +131 -0
- data/lib/spec_forge/context/variables.rb +91 -0
- data/lib/spec_forge/context.rb +36 -0
- data/lib/spec_forge/core_ext/array.rb +27 -0
- data/lib/spec_forge/core_ext/rspec.rb +22 -4
- data/lib/spec_forge/documentation/builder.rb +383 -0
- data/lib/spec_forge/documentation/document/operation.rb +47 -0
- data/lib/spec_forge/documentation/document/parameter.rb +22 -0
- data/lib/spec_forge/documentation/document/request_body.rb +24 -0
- data/lib/spec_forge/documentation/document/response.rb +39 -0
- data/lib/spec_forge/documentation/document/response_body.rb +27 -0
- data/lib/spec_forge/documentation/document.rb +48 -0
- data/lib/spec_forge/documentation/generators/base.rb +81 -0
- data/lib/spec_forge/documentation/generators/openapi/base.rb +100 -0
- data/lib/spec_forge/documentation/generators/openapi/error_formatter.rb +149 -0
- data/lib/spec_forge/documentation/generators/openapi/v3_0.rb +65 -0
- data/lib/spec_forge/documentation/generators/openapi.rb +59 -0
- data/lib/spec_forge/documentation/generators.rb +17 -0
- data/lib/spec_forge/documentation/loader/cache.rb +138 -0
- data/lib/spec_forge/documentation/loader.rb +159 -0
- data/lib/spec_forge/documentation/openapi/base.rb +33 -0
- data/lib/spec_forge/documentation/openapi/v3_0/example.rb +44 -0
- data/lib/spec_forge/documentation/openapi/v3_0/media_type.rb +42 -0
- data/lib/spec_forge/documentation/openapi/v3_0/operation.rb +175 -0
- data/lib/spec_forge/documentation/openapi/v3_0/response.rb +65 -0
- data/lib/spec_forge/documentation/openapi/v3_0/schema.rb +80 -0
- data/lib/spec_forge/documentation/openapi/v3_0/tag.rb +71 -0
- data/lib/spec_forge/documentation/openapi.rb +23 -0
- data/lib/spec_forge/documentation.rb +27 -0
- data/lib/spec_forge/error.rb +284 -113
- data/lib/spec_forge/factory.rb +35 -16
- data/lib/spec_forge/filter.rb +86 -0
- data/lib/spec_forge/forge.rb +171 -0
- data/lib/spec_forge/http/backend.rb +101 -29
- data/lib/spec_forge/http/client.rb +23 -13
- data/lib/spec_forge/http/request.rb +85 -62
- data/lib/spec_forge/http/verb.rb +79 -0
- data/lib/spec_forge/http.rb +105 -0
- data/lib/spec_forge/loader.rb +244 -0
- data/lib/spec_forge/matchers.rb +130 -0
- data/lib/spec_forge/normalizer/default.rb +51 -0
- data/lib/spec_forge/normalizer/definition.rb +248 -0
- data/lib/spec_forge/normalizer/validators.rb +99 -0
- data/lib/spec_forge/normalizer.rb +486 -115
- data/lib/spec_forge/normalizers/_shared.yml +74 -0
- data/lib/spec_forge/normalizers/configuration.yml +23 -0
- data/lib/spec_forge/normalizers/constraint.yml +8 -0
- data/lib/spec_forge/normalizers/expectation.yml +47 -0
- data/lib/spec_forge/normalizers/factory.yml +12 -0
- data/lib/spec_forge/normalizers/factory_reference.yml +15 -0
- data/lib/spec_forge/normalizers/global_context.yml +28 -0
- data/lib/spec_forge/normalizers/spec.yml +50 -0
- data/lib/spec_forge/runner/adapter.rb +183 -0
- 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 +98 -0
- data/lib/spec_forge/runner.rb +50 -125
- data/lib/spec_forge/spec/expectation/constraint.rb +100 -21
- data/lib/spec_forge/spec/expectation.rb +47 -51
- data/lib/spec_forge/spec.rb +50 -108
- data/lib/spec_forge/type.rb +36 -4
- data/lib/spec_forge/version.rb +4 -1
- data/lib/spec_forge.rb +168 -76
- data/lib/templates/openapi.yml.tt +22 -0
- data/lib/templates/redoc.html.tt +28 -0
- data/lib/templates/swagger.html.tt +59 -0
- metadata +109 -16
- data/lib/spec_forge/normalizer/configuration.rb +0 -77
- data/lib/spec_forge/normalizer/constraint.rb +0 -47
- data/lib/spec_forge/normalizer/expectation.rb +0 -86
- data/lib/spec_forge/normalizer/factory.rb +0 -65
- data/lib/spec_forge/normalizer/factory_reference.rb +0 -71
- data/lib/spec_forge/normalizer/spec.rb +0 -74
- data/spec_forge/factories/user.yml +0 -4
- data/spec_forge/forge_helper.rb +0 -48
- data/spec_forge/specs/users.yml +0 -65
- /data/lib/templates/{forge_helper.tt → forge_helper.rb.tt} +0 -0
- /data/lib/templates/{new_factory.tt → new_factory.yml.tt} +0 -0
- /data/lib/templates/{new_spec.tt → new_spec.yml.tt} +0 -0
data/lib/spec_forge/factory.rb
CHANGED
@@ -1,9 +1,14 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module SpecForge
|
4
|
+
#
|
5
|
+
# Manages factory definitions and registration with FactoryBot
|
6
|
+
# Provides methods for loading factories from YAML files
|
7
|
+
#
|
4
8
|
class Factory
|
5
9
|
#
|
6
|
-
# Loads
|
10
|
+
# Loads factories from files and registers them with FactoryBot
|
11
|
+
# Sets up paths and loads definitions based on configuration
|
7
12
|
#
|
8
13
|
def self.load_and_register
|
9
14
|
if SpecForge.configuration.factories.paths?
|
@@ -17,19 +22,18 @@ module SpecForge
|
|
17
22
|
end
|
18
23
|
|
19
24
|
#
|
20
|
-
# Loads
|
25
|
+
# Loads factory definitions from YAML files
|
26
|
+
# Creates Factory instances but doesn't register them with FactoryBot
|
21
27
|
#
|
22
|
-
# @return [Array<Factory>]
|
23
|
-
# Note: This factories have not been registered with FactoryBot.
|
24
|
-
# See #register
|
28
|
+
# @return [Array<Factory>] Array of loaded factory instances
|
25
29
|
#
|
26
30
|
def self.load_from_files
|
27
|
-
path = SpecForge.
|
31
|
+
path = SpecForge.forge_path.join("factories", "**/*.yml")
|
28
32
|
|
29
33
|
factories = []
|
30
34
|
|
31
35
|
Dir[path].each do |file_path|
|
32
|
-
hash = YAML.load_file(file_path)
|
36
|
+
hash = YAML.load_file(file_path, symbolize_names: true)
|
33
37
|
|
34
38
|
hash.each do |factory_name, factory_hash|
|
35
39
|
factory_hash[:name] = factory_name
|
@@ -43,17 +47,32 @@ module SpecForge
|
|
43
47
|
|
44
48
|
############################################################################
|
45
49
|
|
46
|
-
|
50
|
+
# @return [Symbol, String] The name of the factory
|
51
|
+
attr_reader :name
|
47
52
|
|
53
|
+
# @return [Hash] The raw input that defined this factory
|
54
|
+
attr_reader :input
|
55
|
+
|
56
|
+
# @return [String, nil] The model class name this factory represents, if specified
|
57
|
+
attr_reader :model_class
|
58
|
+
|
59
|
+
# @return [Hash<Symbol, Attribute>] Variables defined for this factory
|
60
|
+
attr_reader :variables
|
61
|
+
|
62
|
+
# @return [Hash<Symbol, Attribute>] The attributes that define this factory
|
63
|
+
attr_reader :attributes
|
64
|
+
|
65
|
+
#
|
66
|
+
# Creates a new Factory instance
|
48
67
|
#
|
49
|
-
#
|
68
|
+
# @param name [String, Symbol] The name of the factory
|
69
|
+
# @param input [Hash] The attributes defining the factory
|
50
70
|
#
|
51
|
-
# @
|
52
|
-
# @param **input [Hash] Attributes to define the factory. See Normalizer::Factory
|
71
|
+
# @return [Factory] A new factory instance
|
53
72
|
#
|
54
73
|
def initialize(name:, **input)
|
55
74
|
@name = name
|
56
|
-
input = Normalizer.
|
75
|
+
input = Normalizer.normalize!(input, using: :factory)
|
57
76
|
|
58
77
|
@input = input
|
59
78
|
@model_class = input[:model_class]
|
@@ -63,10 +82,10 @@ module SpecForge
|
|
63
82
|
end
|
64
83
|
|
65
84
|
#
|
66
|
-
# Registers this factory with FactoryBot
|
67
|
-
#
|
85
|
+
# Registers this factory with FactoryBot
|
86
|
+
# Makes the factory available for use in specs
|
68
87
|
#
|
69
|
-
# @return [
|
88
|
+
# @return [self] Returns self for method chaining
|
70
89
|
#
|
71
90
|
def register
|
72
91
|
dsl = FactoryBot::Syntax::Default::DSL.new
|
@@ -78,7 +97,7 @@ module SpecForge
|
|
78
97
|
factory_forge = self
|
79
98
|
dsl.factory(name, options) do
|
80
99
|
factory_forge.attributes.each do |name, attribute|
|
81
|
-
add_attribute(name) { attribute.
|
100
|
+
add_attribute(name) { attribute.resolve }
|
82
101
|
end
|
83
102
|
end
|
84
103
|
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SpecForge
|
4
|
+
#
|
5
|
+
# Provides filtering capabilities for test suites based on different criteria
|
6
|
+
#
|
7
|
+
# The Filter class allows running specific tests by filtering forges, specs,
|
8
|
+
# and expectations based on file name, spec name, and expectation name.
|
9
|
+
#
|
10
|
+
# @example Filtering specs by name
|
11
|
+
# forges = Loader.load_from_files
|
12
|
+
# filtered = Filter.apply(forges, file_name: "users", spec_name: "create_user")
|
13
|
+
#
|
14
|
+
class Filter
|
15
|
+
class << self
|
16
|
+
#
|
17
|
+
# Prints out a message if any of the filters were used
|
18
|
+
#
|
19
|
+
# @param forges [Array<Forge>] The collection of forges that was filtered
|
20
|
+
# @param file_name [String, nil] Optional file name that was used by the filter
|
21
|
+
# @param spec_name [String, nil] Optional spec name that was used by the filter
|
22
|
+
# @param expectation_name [String, nil] Optional expectation name that was used by the filter
|
23
|
+
#
|
24
|
+
def announce(forges, file_name:, spec_name:, expectation_name:)
|
25
|
+
filters = {file_name:, spec_name:, expectation_name:}.compact_blank
|
26
|
+
return if filters.size == 0
|
27
|
+
|
28
|
+
filters_display = filters.join_map(", ") { |k, v| "#{k.in_quotes} => #{v.in_quotes}" }
|
29
|
+
|
30
|
+
expectation_count = forges.sum do |forge|
|
31
|
+
forge.specs.sum { |spec| spec.expectations.size }
|
32
|
+
end
|
33
|
+
|
34
|
+
puts "Applied filter #{filters_display}"
|
35
|
+
puts "Found #{expectation_count} #{"expectation".pluralize(expectation_count)}"
|
36
|
+
end
|
37
|
+
|
38
|
+
#
|
39
|
+
# Filters a collection of forges based on specified criteria
|
40
|
+
#
|
41
|
+
# This method allows running specific tests by filtering forges, specs,
|
42
|
+
# and expectations based on file name, spec name, and expectation name.
|
43
|
+
# It returns only the forges, specs, and expectations that match the criteria.
|
44
|
+
#
|
45
|
+
# @param forges [Array<Forge>] The collection of forges to filter
|
46
|
+
# @param file_name [String, nil] Optional file name to filter by
|
47
|
+
# @param spec_name [String, nil] Optional spec name to filter by
|
48
|
+
# @param expectation_name [String, nil] Optional expectation name to filter by
|
49
|
+
#
|
50
|
+
# @return [Array<Forge>] The filtered collection of forges
|
51
|
+
#
|
52
|
+
# @raise [ArgumentError] If filtering parameters are provided in an invalid combination
|
53
|
+
#
|
54
|
+
def apply(forges, file_name: nil, spec_name: nil, expectation_name: nil)
|
55
|
+
# Guard against invalid partial filters
|
56
|
+
if expectation_name && spec_name.blank?
|
57
|
+
raise ArgumentError, "The spec's name is required when filtering by an expectation's name"
|
58
|
+
end
|
59
|
+
|
60
|
+
if spec_name && file_name.blank?
|
61
|
+
raise ArgumentError, "The spec's filename is required when filtering by a spec's name"
|
62
|
+
end
|
63
|
+
|
64
|
+
forges.filter_map do |forge|
|
65
|
+
specs = forge.specs.filter_map do |spec|
|
66
|
+
next if file_name && spec.file_name != file_name # File filter
|
67
|
+
next if spec_name && spec.name != spec_name # Name filter
|
68
|
+
|
69
|
+
# Expectation filter
|
70
|
+
next spec unless expectation_name
|
71
|
+
|
72
|
+
spec.expectations.select! { |e| e.name == expectation_name }
|
73
|
+
next if spec.expectations.empty?
|
74
|
+
|
75
|
+
spec
|
76
|
+
end
|
77
|
+
|
78
|
+
next if specs.empty?
|
79
|
+
|
80
|
+
forge.specs = specs
|
81
|
+
forge
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,171 @@
|
|
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)] }.compact_blank
|
121
|
+
|
122
|
+
hash[spec[:id]] = {base: spec.delete(:variables), overlay:}
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
#
|
127
|
+
# Extracts request configuration from specs and organizes them into base and overlay configs
|
128
|
+
#
|
129
|
+
# @param specs [Array<Hash>] Array of spec definitions
|
130
|
+
#
|
131
|
+
# @return [Hash] A hash mapping spec IDs to their request configurations
|
132
|
+
#
|
133
|
+
# @private
|
134
|
+
#
|
135
|
+
def extract_request!(specs)
|
136
|
+
#
|
137
|
+
# Creates a hash that looks like this:
|
138
|
+
#
|
139
|
+
# {
|
140
|
+
# spec_1: {
|
141
|
+
# base: {base_url: "https://foo.bar", url: "", ...},
|
142
|
+
# overlay: {
|
143
|
+
# expectation: {base_url: "https://bar.baz", ...}
|
144
|
+
# }
|
145
|
+
# },
|
146
|
+
# spec_2: ...
|
147
|
+
# }
|
148
|
+
#
|
149
|
+
config = SpecForge.configuration.to_h.slice(:base_url, :headers, :query)
|
150
|
+
|
151
|
+
specs.each_with_object({}) do |spec, hash|
|
152
|
+
overlay = spec[:expectations].to_h do |expectation|
|
153
|
+
[
|
154
|
+
expectation[:id],
|
155
|
+
expectation.extract!(*HTTP::REQUEST_ATTRIBUTES).compact_blank
|
156
|
+
]
|
157
|
+
end
|
158
|
+
|
159
|
+
overlay.compact_blank!
|
160
|
+
|
161
|
+
base = spec.extract!(*HTTP::REQUEST_ATTRIBUTES)
|
162
|
+
base.compact_blank!
|
163
|
+
|
164
|
+
base = config.deep_merge(base)
|
165
|
+
base[:http_verb] ||= "GET"
|
166
|
+
|
167
|
+
hash[spec[:id]] = {base:, overlay:}
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
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,112 @@ 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)
|
147
|
+
request.headers.transform_values!(&:to_s)
|
148
|
+
|
103
149
|
request.params.merge!(query)
|
104
150
|
request.body = body.to_json
|
105
151
|
end
|
106
152
|
|
153
|
+
#
|
154
|
+
# Normalizes a URL by replacing path parameters with their values
|
155
|
+
#
|
156
|
+
# Handles both curly brace style {param} and colon style :param
|
157
|
+
# Parameters are extracted from the query hash and removed after substitution
|
158
|
+
#
|
159
|
+
# @param url [String] The URL pattern with potential placeholders
|
160
|
+
# @param query [Hash] Query parameters that may contain values for placeholders
|
161
|
+
#
|
162
|
+
# @return [String] The URL with placeholders replaced by actual values
|
163
|
+
#
|
164
|
+
# @raise [URI::InvalidURIError] If the resulting URL is invalid
|
165
|
+
#
|
166
|
+
# @private
|
167
|
+
#
|
107
168
|
def normalize_url(url, query)
|
108
169
|
# /users/<user_id>
|
109
170
|
url = replace_url_placeholder(url, query, CURLY_PLACEHOLDER)
|
@@ -122,6 +183,17 @@ module SpecForge
|
|
122
183
|
url
|
123
184
|
end
|
124
185
|
|
186
|
+
#
|
187
|
+
# Replaces URL placeholders with values from the query hash
|
188
|
+
#
|
189
|
+
# @param url [String] The URL with placeholders
|
190
|
+
# @param query [Hash] The query parameters containing values
|
191
|
+
# @param regex [Regexp] The pattern to match (curly or colon style)
|
192
|
+
#
|
193
|
+
# @return [String] The URL with placeholders replaced
|
194
|
+
#
|
195
|
+
# @private
|
196
|
+
#
|
125
197
|
def replace_url_placeholder(url, query, regex)
|
126
198
|
match = url.match(regex)
|
127
199
|
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
|