spec_forge 0.7.1 → 1.0.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 +75 -1
- data/README.md +124 -202
- data/bin/spec_forge +1 -1
- data/flake.lock +76 -4
- data/flake.nix +5 -4
- data/lib/spec_forge/attribute/chainable.rb +6 -6
- data/lib/spec_forge/attribute/environment.rb +45 -0
- data/lib/spec_forge/attribute/factory.rb +26 -17
- data/lib/spec_forge/attribute/faker.rb +6 -1
- data/lib/spec_forge/attribute/generate.rb +114 -0
- data/lib/spec_forge/attribute/literal.rb +1 -14
- data/lib/spec_forge/attribute/matcher.rb +6 -2
- data/lib/spec_forge/attribute/parameterized.rb +20 -22
- data/lib/spec_forge/attribute/resolvable_array.rb +16 -16
- data/lib/spec_forge/attribute/resolvable_hash.rb +17 -16
- data/lib/spec_forge/attribute/resolvable_struct.rb +67 -0
- data/lib/spec_forge/attribute/template.rb +118 -0
- data/lib/spec_forge/attribute/transform.rb +14 -19
- data/lib/spec_forge/attribute/variable.rb +31 -31
- data/lib/spec_forge/attribute.rb +54 -100
- data/lib/spec_forge/blueprint.rb +27 -0
- data/lib/spec_forge/cli/docs/generate.rb +28 -8
- data/lib/spec_forge/cli/docs.rb +5 -2
- data/lib/spec_forge/cli/init.rb +4 -4
- data/lib/spec_forge/cli/new.rb +78 -27
- data/lib/spec_forge/cli/run.rb +84 -52
- data/lib/spec_forge/cli/serve.rb +5 -0
- data/lib/spec_forge/cli.rb +6 -14
- data/lib/spec_forge/configuration.rb +209 -79
- data/lib/spec_forge/documentation/{loader → builder}/cache.rb +26 -23
- data/lib/spec_forge/documentation/builder/compiler.rb +373 -0
- data/lib/spec_forge/documentation/builder/extractor.rb +75 -0
- data/lib/spec_forge/documentation/builder.rb +77 -329
- data/lib/spec_forge/documentation/document/operation.rb +4 -4
- data/lib/spec_forge/documentation/document.rb +0 -6
- data/lib/spec_forge/documentation/generator.rb +88 -0
- data/lib/spec_forge/documentation/{generators/openapi → openapi/v3_0}/error_formatter.rb +2 -2
- data/lib/spec_forge/documentation/openapi/v3_0/example.rb +1 -1
- data/lib/spec_forge/documentation/openapi/v3_0/media_type.rb +1 -1
- data/lib/spec_forge/documentation/openapi/v3_0/operation.rb +21 -5
- data/lib/spec_forge/documentation/openapi/v3_0/response.rb +28 -6
- data/lib/spec_forge/documentation/openapi/v3_0/schema.rb +20 -2
- data/lib/spec_forge/documentation/openapi/v3_0/tag.rb +1 -1
- data/lib/spec_forge/documentation/openapi/v3_0.rb +116 -0
- data/lib/spec_forge/documentation/openapi.rb +40 -12
- data/lib/spec_forge/documentation.rb +1 -7
- data/lib/spec_forge/error.rb +215 -41
- data/lib/spec_forge/factory.rb +38 -18
- data/lib/spec_forge/forge/action.rb +41 -0
- data/lib/spec_forge/forge/actions/call.rb +33 -0
- data/lib/spec_forge/forge/actions/debug.rb +47 -0
- data/lib/spec_forge/forge/actions/expect.rb +44 -0
- data/lib/spec_forge/forge/actions/request.rb +65 -0
- data/lib/spec_forge/forge/actions/store.rb +31 -0
- data/lib/spec_forge/forge/callbacks.rb +80 -0
- data/lib/spec_forge/forge/context.rb +41 -0
- data/lib/spec_forge/forge/display.rb +503 -0
- data/lib/spec_forge/forge/hooks.rb +131 -0
- data/lib/spec_forge/forge/runner/array_io.rb +81 -0
- data/lib/spec_forge/forge/runner/content_validator.rb +92 -0
- data/lib/spec_forge/forge/runner/header_validator.rb +66 -0
- data/lib/spec_forge/forge/runner/reporter.rb +56 -0
- data/lib/spec_forge/forge/runner/schema_validator.rb +113 -0
- data/lib/spec_forge/forge/runner.rb +118 -0
- data/lib/spec_forge/forge/timer.rb +94 -0
- data/lib/spec_forge/forge/variables.rb +38 -0
- data/lib/spec_forge/forge.rb +207 -133
- data/lib/spec_forge/http/backend.rb +49 -146
- data/lib/spec_forge/http/client.rb +14 -17
- data/lib/spec_forge/http/request.rb +37 -84
- data/lib/spec_forge/http/verb.rb +4 -0
- data/lib/spec_forge/http.rb +0 -5
- data/lib/spec_forge/loader/filter.rb +85 -0
- data/lib/spec_forge/loader/step_processor.rb +282 -0
- data/lib/spec_forge/loader.rb +105 -220
- data/lib/spec_forge/normalizer/default.rb +1 -1
- data/lib/spec_forge/normalizer/structure.rb +140 -0
- data/lib/spec_forge/normalizer/transformers.rb +168 -0
- data/lib/spec_forge/normalizer/validators.rb +50 -8
- data/lib/spec_forge/normalizer.rb +76 -119
- data/lib/spec_forge/normalizers/callback.yml +38 -0
- data/lib/spec_forge/normalizers/configuration.yml +59 -9
- data/lib/spec_forge/normalizers/factory.yml +53 -2
- data/lib/spec_forge/normalizers/factory_reference.yml +63 -2
- data/lib/spec_forge/normalizers/json_schema.yml +79 -0
- data/lib/spec_forge/normalizers/step.yml +506 -0
- data/lib/spec_forge/step/call.rb +36 -0
- data/lib/spec_forge/step/expect.rb +110 -0
- data/lib/spec_forge/step/source.rb +22 -0
- data/lib/spec_forge/step.rb +129 -0
- data/lib/spec_forge/type.rb +115 -66
- data/lib/spec_forge/version.rb +1 -1
- data/lib/spec_forge.rb +44 -106
- data/lib/templates/forge_helper.rb.tt +43 -22
- data/lib/templates/new_blueprint.yml.tt +54 -0
- metadata +75 -44
- data/lib/spec_forge/attribute/global.rb +0 -96
- data/lib/spec_forge/attribute/store.rb +0 -65
- data/lib/spec_forge/backtrace_formatter.rb +0 -50
- data/lib/spec_forge/callbacks.rb +0 -88
- data/lib/spec_forge/context/callbacks.rb +0 -91
- data/lib/spec_forge/context/global.rb +0 -72
- data/lib/spec_forge/context/store.rb +0 -131
- data/lib/spec_forge/context/variables.rb +0 -91
- data/lib/spec_forge/context.rb +0 -36
- data/lib/spec_forge/core_ext/rspec.rb +0 -55
- data/lib/spec_forge/core_ext.rb +0 -5
- data/lib/spec_forge/documentation/generators/base.rb +0 -81
- data/lib/spec_forge/documentation/generators/openapi/base.rb +0 -100
- data/lib/spec_forge/documentation/generators/openapi/v3_0.rb +0 -65
- data/lib/spec_forge/documentation/generators/openapi.rb +0 -59
- data/lib/spec_forge/documentation/generators.rb +0 -17
- data/lib/spec_forge/documentation/loader.rb +0 -159
- data/lib/spec_forge/documentation/openapi/base.rb +0 -33
- data/lib/spec_forge/filter.rb +0 -86
- data/lib/spec_forge/normalizer/definition.rb +0 -248
- data/lib/spec_forge/normalizers/_shared.yml +0 -76
- data/lib/spec_forge/normalizers/constraint.yml +0 -8
- data/lib/spec_forge/normalizers/expectation.yml +0 -47
- data/lib/spec_forge/normalizers/global_context.yml +0 -28
- data/lib/spec_forge/normalizers/spec.yml +0 -50
- data/lib/spec_forge/runner/adapter.rb +0 -181
- data/lib/spec_forge/runner/callbacks.rb +0 -246
- data/lib/spec_forge/runner/debug_proxy.rb +0 -215
- data/lib/spec_forge/runner/listener.rb +0 -54
- data/lib/spec_forge/runner/metadata.rb +0 -58
- data/lib/spec_forge/runner/state.rb +0 -98
- data/lib/spec_forge/runner.rb +0 -75
- data/lib/spec_forge/spec/expectation/constraint.rb +0 -127
- data/lib/spec_forge/spec/expectation.rb +0 -68
- data/lib/spec_forge/spec.rb +0 -68
- data/lib/templates/new_spec.yml.tt +0 -43
data/lib/spec_forge/forge.rb
CHANGED
|
@@ -2,170 +2,244 @@
|
|
|
2
2
|
|
|
3
3
|
module SpecForge
|
|
4
4
|
#
|
|
5
|
-
#
|
|
5
|
+
# The main execution engine for running blueprints
|
|
6
6
|
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
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)
|
|
7
|
+
# Forge orchestrates the execution of blueprints by managing the execution
|
|
8
|
+
# context, HTTP client, variable storage, and display output. It processes
|
|
9
|
+
# each step sequentially and tracks statistics across the run.
|
|
16
10
|
#
|
|
17
11
|
class Forge
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
12
|
+
class << self
|
|
13
|
+
#
|
|
14
|
+
# Initializes SpecForge by loading the forge_helper and factories
|
|
15
|
+
#
|
|
16
|
+
# @return [Class] self for chaining
|
|
17
|
+
#
|
|
18
|
+
def ignite
|
|
19
|
+
load_forge_helper
|
|
20
|
+
Factory.load_and_register
|
|
24
21
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
# @return [Hash] The global variables and configuration
|
|
29
|
-
#
|
|
30
|
-
attr_reader :global
|
|
22
|
+
# Return for chaining
|
|
23
|
+
self
|
|
24
|
+
end
|
|
31
25
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
26
|
+
#
|
|
27
|
+
# Creates and runs a new Forge instance with the given blueprints
|
|
28
|
+
#
|
|
29
|
+
# @param blueprints [Array<Blueprint>] The blueprints to execute
|
|
30
|
+
# @option verbosity_level [Integer] Output verbosity (0-3)
|
|
31
|
+
# @option hooks [Hash] Forge-level event hooks
|
|
32
|
+
#
|
|
33
|
+
# @return [void]
|
|
34
|
+
#
|
|
35
|
+
def run(blueprints, **)
|
|
36
|
+
new(blueprints, **).run
|
|
37
|
+
end
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
39
|
+
#
|
|
40
|
+
# Returns the current execution context for the current thread
|
|
41
|
+
#
|
|
42
|
+
# @return [Context, nil] The current context or nil if not executing
|
|
43
|
+
#
|
|
44
|
+
def context
|
|
45
|
+
Thread.current[:spec_forge_context]
|
|
46
|
+
end
|
|
45
47
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
#
|
|
49
|
+
# Executes a block with a given context
|
|
50
|
+
#
|
|
51
|
+
# @param context [Context] The context to use during execution
|
|
52
|
+
#
|
|
53
|
+
# @yield Block to execute with the context
|
|
54
|
+
#
|
|
55
|
+
# @return [Object] The result of the block
|
|
56
|
+
#
|
|
57
|
+
def with_context(context)
|
|
58
|
+
old_context = Thread.current[:spec_forge_context]
|
|
59
|
+
Thread.current[:spec_forge_context] = context
|
|
60
|
+
yield
|
|
61
|
+
ensure
|
|
62
|
+
Thread.current[:spec_forge_context] = old_context
|
|
63
|
+
end
|
|
52
64
|
|
|
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
|
|
65
|
+
private
|
|
59
66
|
|
|
60
|
-
|
|
61
|
-
|
|
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]
|
|
67
|
+
def load_forge_helper
|
|
68
|
+
forge_helper = SpecForge.forge_path.join("forge_helper.rb")
|
|
69
|
+
return unless File.exist?(forge_helper)
|
|
71
70
|
|
|
72
|
-
|
|
73
|
-
@metadata = metadata
|
|
71
|
+
require_relative forge_helper
|
|
74
72
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
73
|
+
# Revalidate in case anything was changed
|
|
74
|
+
SpecForge.configuration.validate
|
|
75
|
+
end
|
|
78
76
|
end
|
|
79
77
|
|
|
80
|
-
#
|
|
81
|
-
|
|
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
|
|
78
|
+
# @return [Array<Blueprint>] The blueprints being executed
|
|
79
|
+
attr_reader :blueprints
|
|
93
80
|
|
|
94
|
-
|
|
81
|
+
# @return [Callbacks] Callback registry for this forge run
|
|
82
|
+
attr_reader :callbacks
|
|
83
|
+
|
|
84
|
+
# @return [Display] Display handler for output formatting
|
|
85
|
+
attr_reader :display
|
|
86
|
+
|
|
87
|
+
# @return [Array<Hash>] List of failed expectations
|
|
88
|
+
attr_reader :failures
|
|
89
|
+
|
|
90
|
+
# @return [Hash{Symbol => Array<Step::Call>}] Forge-level before and after hooks
|
|
91
|
+
attr_reader :hooks
|
|
92
|
+
|
|
93
|
+
# @return [HTTP::Client] HTTP client for making requests
|
|
94
|
+
attr_reader :http_client
|
|
95
|
+
|
|
96
|
+
# @return [Runner] RSpec runner for executing expectations
|
|
97
|
+
attr_reader :runner
|
|
98
|
+
|
|
99
|
+
# @return [Hash] Statistics about the current run
|
|
100
|
+
attr_reader :stats
|
|
101
|
+
|
|
102
|
+
# @return [Timer] Timer for tracking execution duration
|
|
103
|
+
attr_reader :timer
|
|
104
|
+
|
|
105
|
+
# @return [Variables] Variable storage for the current run
|
|
106
|
+
attr_reader :variables
|
|
95
107
|
|
|
96
108
|
#
|
|
97
|
-
#
|
|
98
|
-
#
|
|
99
|
-
# @param specs [Array<Hash>] Array of spec definitions
|
|
109
|
+
# Creates a new Forge instance with the specified blueprints
|
|
100
110
|
#
|
|
101
|
-
# @
|
|
111
|
+
# @param blueprints [Array<Blueprint>] The blueprints to execute
|
|
112
|
+
# @param verbosity_level [Integer] Output verbosity (0-3)
|
|
113
|
+
# @param hooks [Hash] Forge-level event hooks
|
|
102
114
|
#
|
|
103
|
-
# @
|
|
115
|
+
# @return [Forge] A new forge instance
|
|
104
116
|
#
|
|
105
|
-
def
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
|
117
|
+
def initialize(blueprints, verbosity_level: 0, hooks: {})
|
|
118
|
+
@blueprints = blueprints
|
|
119
|
+
@callbacks = Callbacks.new
|
|
120
|
+
@display = Display.new(verbosity_level:)
|
|
121
|
+
@failures = []
|
|
122
|
+
@hooks = Step::Call.wrap_hooks(hooks)
|
|
123
|
+
@http_client = HTTP::Client.new
|
|
124
|
+
@runner = Runner.new
|
|
125
|
+
@stats = {}
|
|
126
|
+
@timer = Timer.new
|
|
127
|
+
@variables = Variables.new(static: SpecForge.configuration.global_variables)
|
|
121
128
|
|
|
122
|
-
|
|
123
|
-
end
|
|
129
|
+
reset_stats
|
|
124
130
|
end
|
|
125
131
|
|
|
126
132
|
#
|
|
127
|
-
#
|
|
133
|
+
# Executes all blueprints and their steps
|
|
128
134
|
#
|
|
129
|
-
# @
|
|
135
|
+
# @return [void]
|
|
130
136
|
#
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
]
|
|
137
|
+
def run
|
|
138
|
+
context = Context.new(variables:)
|
|
139
|
+
|
|
140
|
+
Forge.with_context(context) do
|
|
141
|
+
forge_start
|
|
142
|
+
|
|
143
|
+
@blueprints.each do |blueprint|
|
|
144
|
+
blueprint_start(blueprint)
|
|
145
|
+
|
|
146
|
+
blueprint.steps.each do |step|
|
|
147
|
+
step_start(blueprint, step)
|
|
148
|
+
step_action(blueprint, step)
|
|
149
|
+
step_end(blueprint, step)
|
|
150
|
+
rescue => e
|
|
151
|
+
step_end(blueprint, step, error: e)
|
|
152
|
+
break
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
blueprint_end(blueprint)
|
|
157
156
|
end
|
|
157
|
+
ensure
|
|
158
|
+
forge_end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
158
161
|
|
|
159
|
-
|
|
162
|
+
private
|
|
160
163
|
|
|
161
|
-
|
|
162
|
-
|
|
164
|
+
def reset_stats
|
|
165
|
+
@stats = {
|
|
166
|
+
blueprints: 0,
|
|
167
|
+
steps: 0,
|
|
168
|
+
passed: 0,
|
|
169
|
+
failed: 0
|
|
170
|
+
}
|
|
171
|
+
end
|
|
163
172
|
|
|
164
|
-
|
|
165
|
-
|
|
173
|
+
def forge_start
|
|
174
|
+
reset_stats
|
|
166
175
|
|
|
167
|
-
|
|
176
|
+
# Load the callbacks from the configuration
|
|
177
|
+
SpecForge.configuration.callbacks.each { |name, block| @callbacks.register(name, &block) }
|
|
178
|
+
|
|
179
|
+
@display.forge_start(self)
|
|
180
|
+
@timer.start
|
|
181
|
+
|
|
182
|
+
Hooks.before_forge(self)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def blueprint_start(blueprint)
|
|
186
|
+
@variables.clear
|
|
187
|
+
@failures.clear
|
|
188
|
+
|
|
189
|
+
@display.blueprint_start(blueprint)
|
|
190
|
+
|
|
191
|
+
Hooks.before_blueprint(self, blueprint)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def step_start(blueprint, step)
|
|
195
|
+
@display.step_start(step)
|
|
196
|
+
|
|
197
|
+
Hooks.before_step(self, blueprint, step)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def step_action(blueprint, step)
|
|
201
|
+
# HEY! LISTEN: These read and write to the forge's state
|
|
202
|
+
Call.new(step).run(self) if step.calls?
|
|
203
|
+
Request.new(step).run(self) if step.request?
|
|
204
|
+
Debug.new(step).run(self, blueprint) if step.debug?
|
|
205
|
+
Expect.new(step).run(self) if step.expects?
|
|
206
|
+
Store.new(step).run(self) if step.store?
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def step_end(blueprint, step, error: nil)
|
|
210
|
+
@stats[:steps] += 1
|
|
211
|
+
|
|
212
|
+
if error.is_a?(Error::ExpectationFailure)
|
|
213
|
+
@failures += error.failed_examples.map { |example| {step:, example:} }
|
|
168
214
|
end
|
|
215
|
+
|
|
216
|
+
Hooks.after_step(self, blueprint, step, error:)
|
|
217
|
+
|
|
218
|
+
@display.step_end(self, step, error:)
|
|
219
|
+
|
|
220
|
+
# Bubble up only AFTER display has been updated
|
|
221
|
+
raise error if error && !error.is_a?(Error::ExpectationFailure)
|
|
222
|
+
ensure
|
|
223
|
+
# Drop the request/response data from scope
|
|
224
|
+
# Do this after everything is done so variables can be printed out if needed
|
|
225
|
+
@variables.except!(:request, :response)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def blueprint_end(blueprint)
|
|
229
|
+
@stats[:blueprints] += 1
|
|
230
|
+
|
|
231
|
+
@display.blueprint_end(blueprint, success: @failures.empty?)
|
|
232
|
+
|
|
233
|
+
Hooks.after_blueprint(self, blueprint)
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def forge_end
|
|
237
|
+
@timer.stop
|
|
238
|
+
|
|
239
|
+
@display.forge_end(self)
|
|
240
|
+
Hooks.after_forge(self)
|
|
241
|
+
|
|
242
|
+
@display.stats(self)
|
|
169
243
|
end
|
|
170
244
|
end
|
|
171
245
|
end
|
|
@@ -3,30 +3,12 @@
|
|
|
3
3
|
module SpecForge
|
|
4
4
|
module HTTP
|
|
5
5
|
#
|
|
6
|
-
#
|
|
6
|
+
# Low-level HTTP client wrapper around Faraday
|
|
7
7
|
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
# @example Basic usage
|
|
12
|
-
# backend = Backend.new(request)
|
|
13
|
-
# response = backend.get("/users")
|
|
8
|
+
# Backend provides methods for each HTTP verb and handles the actual
|
|
9
|
+
# communication with the server. It's used internally by HTTP::Client.
|
|
14
10
|
#
|
|
15
11
|
class Backend
|
|
16
|
-
#
|
|
17
|
-
# Regular expression to match { placeholder } style URL parameters
|
|
18
|
-
#
|
|
19
|
-
# @return [Regexp]
|
|
20
|
-
#
|
|
21
|
-
CURLY_PLACEHOLDER = /\{(\w+)\}/
|
|
22
|
-
|
|
23
|
-
#
|
|
24
|
-
# Regular expression to match :placeholder style URL parameters
|
|
25
|
-
#
|
|
26
|
-
# @return [Regexp]
|
|
27
|
-
#
|
|
28
|
-
COLON_PLACEHOLDER = /:(\w+)/
|
|
29
|
-
|
|
30
12
|
#
|
|
31
13
|
# The configured Faraday connection
|
|
32
14
|
#
|
|
@@ -35,180 +17,101 @@ module SpecForge
|
|
|
35
17
|
attr_reader :connection
|
|
36
18
|
|
|
37
19
|
#
|
|
38
|
-
#
|
|
20
|
+
# Creates a new HTTP backend with a Faraday connection
|
|
39
21
|
#
|
|
40
|
-
# @
|
|
22
|
+
# @return [Backend] A new backend instance
|
|
41
23
|
#
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def initialize(request)
|
|
45
|
-
@connection =
|
|
46
|
-
Faraday.new(url: request.base_url) do |builder|
|
|
47
|
-
# Content-Type
|
|
48
|
-
if !request.headers.key?("Content-Type")
|
|
49
|
-
builder.request :json
|
|
50
|
-
builder.response :json
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
# Headers
|
|
54
|
-
builder.headers.merge!(request.headers.resolved)
|
|
55
|
-
end
|
|
24
|
+
def initialize
|
|
25
|
+
@connection = Faraday.new
|
|
56
26
|
end
|
|
57
27
|
|
|
58
28
|
#
|
|
59
29
|
# Executes a DELETE request to <base_url>/<provided_url>
|
|
60
30
|
#
|
|
61
|
-
# @
|
|
62
|
-
# @
|
|
63
|
-
# @
|
|
64
|
-
# @
|
|
31
|
+
# @option options [String] :url The URL path to DELETE
|
|
32
|
+
# @option options [String] :base_url The base URL to use for the request
|
|
33
|
+
# @option options [Hash] :headers HTTP headers to add
|
|
34
|
+
# @option options [Hash] :query Any query parameters to send
|
|
35
|
+
# @option options [Hash] :body Any body data to send
|
|
65
36
|
#
|
|
66
37
|
# @return [Faraday::Response] The HTTP response
|
|
67
38
|
#
|
|
68
|
-
def delete(
|
|
69
|
-
|
|
70
|
-
connection.delete(url) { |request| update_request(request, headers, query, body) }
|
|
39
|
+
def delete(**)
|
|
40
|
+
run_http_method(:delete, **)
|
|
71
41
|
end
|
|
72
42
|
|
|
73
43
|
#
|
|
74
44
|
# Executes a GET request to <base_url>/<provided_url>
|
|
75
45
|
#
|
|
76
|
-
# @
|
|
77
|
-
# @
|
|
78
|
-
# @
|
|
79
|
-
# @
|
|
46
|
+
# @option options [String] :url The URL path to GET
|
|
47
|
+
# @option options [String] :base_url The base URL to use for the request
|
|
48
|
+
# @option options [Hash] :headers HTTP headers to add
|
|
49
|
+
# @option options [Hash] :query Any query parameters to send
|
|
50
|
+
# @option options [Hash] :body Any body data to send
|
|
80
51
|
#
|
|
81
52
|
# @return [Faraday::Response] The HTTP response
|
|
82
53
|
#
|
|
83
|
-
def get(
|
|
84
|
-
|
|
85
|
-
connection.get(url) { |request| update_request(request, headers, query, body) }
|
|
54
|
+
def get(**)
|
|
55
|
+
run_http_method(:get, **)
|
|
86
56
|
end
|
|
87
57
|
|
|
88
58
|
#
|
|
89
59
|
# Executes a PATCH request to <base_url>/<provided_url>
|
|
90
60
|
#
|
|
91
|
-
# @
|
|
92
|
-
# @
|
|
93
|
-
# @
|
|
94
|
-
# @
|
|
61
|
+
# @option options [String] :url The URL path to PATCH
|
|
62
|
+
# @option options [String] :base_url The base URL to use for the request
|
|
63
|
+
# @option options [Hash] :headers HTTP headers to add
|
|
64
|
+
# @option options [Hash] :query Any query parameters to send
|
|
65
|
+
# @option options [Hash] :body Any body data to send
|
|
95
66
|
#
|
|
96
67
|
# @return [Faraday::Response] The HTTP response
|
|
97
68
|
#
|
|
98
|
-
def patch(
|
|
99
|
-
|
|
100
|
-
connection.patch(url) { |request| update_request(request, headers, query, body) }
|
|
69
|
+
def patch(**)
|
|
70
|
+
run_http_method(:patch, **)
|
|
101
71
|
end
|
|
102
72
|
|
|
103
73
|
#
|
|
104
74
|
# Executes a POST request to <base_url>/<provided_url>
|
|
105
75
|
#
|
|
106
|
-
# @
|
|
107
|
-
# @
|
|
108
|
-
# @
|
|
109
|
-
# @
|
|
76
|
+
# @option options [String] :url The URL path to POST
|
|
77
|
+
# @option options [String] :base_url The base URL to use for the request
|
|
78
|
+
# @option options [Hash] :headers HTTP headers to add
|
|
79
|
+
# @option options [Hash] :query Any query parameters to send
|
|
80
|
+
# @option options [Hash] :body Any body data to send
|
|
110
81
|
#
|
|
111
82
|
# @return [Faraday::Response] The HTTP response
|
|
112
83
|
#
|
|
113
|
-
def post(
|
|
114
|
-
|
|
115
|
-
connection.post(url) { |request| update_request(request, headers, query, body) }
|
|
84
|
+
def post(**)
|
|
85
|
+
run_http_method(:post, **)
|
|
116
86
|
end
|
|
117
87
|
|
|
118
88
|
#
|
|
119
89
|
# Executes a PUT request to <base_url>/<provided_url>
|
|
120
90
|
#
|
|
121
|
-
# @
|
|
122
|
-
# @
|
|
123
|
-
# @
|
|
124
|
-
# @
|
|
91
|
+
# @option options [String] :url The URL path to PUT
|
|
92
|
+
# @option options [String] :base_url The base URL to use for the request
|
|
93
|
+
# @option options [Hash] :headers HTTP headers to add
|
|
94
|
+
# @option options [Hash] :query Any query parameters to send
|
|
95
|
+
# @option options [Hash] :body Any body data to send
|
|
125
96
|
#
|
|
126
97
|
# @return [Faraday::Response] The HTTP response
|
|
127
98
|
#
|
|
128
|
-
def put(
|
|
129
|
-
|
|
130
|
-
connection.put(url) { |request| update_request(request, headers, query, body) }
|
|
99
|
+
def put(**)
|
|
100
|
+
run_http_method(:put, **)
|
|
131
101
|
end
|
|
132
102
|
|
|
133
103
|
private
|
|
134
104
|
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
149
|
-
request.params.merge!(query)
|
|
150
|
-
request.body = body.to_json
|
|
151
|
-
end
|
|
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
|
-
#
|
|
168
|
-
def normalize_url(url, query)
|
|
169
|
-
# Strip leading slash so paths properly append to base URL
|
|
170
|
-
url = url.delete_prefix("/")
|
|
105
|
+
def run_http_method(method, url:, base_url:, headers: {}, query: {}, body: {})
|
|
106
|
+
connection.url_prefix = base_url
|
|
171
107
|
|
|
172
|
-
|
|
173
|
-
|
|
108
|
+
connection.public_send(method, url) do |request|
|
|
109
|
+
request.headers.merge!(headers)
|
|
110
|
+
request.headers.transform_values!(&:to_s)
|
|
174
111
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
# Attempt to validate (the colon style is considered valid apparently)
|
|
179
|
-
begin
|
|
180
|
-
URI.parse(url)
|
|
181
|
-
rescue URI::InvalidURIError
|
|
182
|
-
raise URI::InvalidURIError,
|
|
183
|
-
"#{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."
|
|
112
|
+
request.params.merge!(query)
|
|
113
|
+
request.body = body
|
|
184
114
|
end
|
|
185
|
-
|
|
186
|
-
url
|
|
187
|
-
end
|
|
188
|
-
|
|
189
|
-
#
|
|
190
|
-
# Replaces URL placeholders with values from the query hash
|
|
191
|
-
#
|
|
192
|
-
# @param url [String] The URL with placeholders
|
|
193
|
-
# @param query [Hash] The query parameters containing values
|
|
194
|
-
# @param regex [Regexp] The pattern to match (curly or colon style)
|
|
195
|
-
#
|
|
196
|
-
# @return [String] The URL with placeholders replaced
|
|
197
|
-
#
|
|
198
|
-
# @private
|
|
199
|
-
#
|
|
200
|
-
def replace_url_placeholder(url, query, regex)
|
|
201
|
-
match = url.match(regex)
|
|
202
|
-
return url if match.nil?
|
|
203
|
-
|
|
204
|
-
key = match[1].to_sym
|
|
205
|
-
return url unless query.key?(key)
|
|
206
|
-
|
|
207
|
-
value = query.delete(key)
|
|
208
|
-
url.gsub(
|
|
209
|
-
match[0],
|
|
210
|
-
URI.encode_uri_component(value.to_s)
|
|
211
|
-
)
|
|
212
115
|
end
|
|
213
116
|
end
|
|
214
117
|
end
|