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
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SpecForge
|
|
4
|
+
module Documentation
|
|
5
|
+
class Builder
|
|
6
|
+
#
|
|
7
|
+
# Compiles raw endpoint data into structured documentation format
|
|
8
|
+
#
|
|
9
|
+
# The Compiler transforms flat endpoint data extracted from test runs
|
|
10
|
+
# into a hierarchical structure organized by URL path and HTTP method.
|
|
11
|
+
# It handles:
|
|
12
|
+
# - Grouping endpoints by path and HTTP verb
|
|
13
|
+
# - Sanitizing error responses to exclude invalid request data
|
|
14
|
+
# - Merging multiple operations with the same status code
|
|
15
|
+
# - Normalizing parameters, request bodies, and responses
|
|
16
|
+
# - Type detection for all values
|
|
17
|
+
#
|
|
18
|
+
# @example Compiling endpoints
|
|
19
|
+
# compiler = Compiler.new(endpoints)
|
|
20
|
+
# compiled = compiler.compile
|
|
21
|
+
# # => { "/users" => { "GET" => { id: "...", responses: [...] } } }
|
|
22
|
+
#
|
|
23
|
+
class Compiler
|
|
24
|
+
#
|
|
25
|
+
# Regular expression for matching UUID v4 strings
|
|
26
|
+
#
|
|
27
|
+
# @see https://gist.github.com/johnelliott/cf77003f72f889abbc3f32785fa3df8d
|
|
28
|
+
#
|
|
29
|
+
# @api private
|
|
30
|
+
#
|
|
31
|
+
UUID_REGEX = /^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i
|
|
32
|
+
|
|
33
|
+
#
|
|
34
|
+
# Regular expression for matching integer numbers in strings
|
|
35
|
+
#
|
|
36
|
+
# Matches whole numbers with optional negative sign, used for type detection
|
|
37
|
+
# when analyzing API response data.
|
|
38
|
+
#
|
|
39
|
+
# @api private
|
|
40
|
+
#
|
|
41
|
+
INTEGER_REGEX = /^-?\d+$/
|
|
42
|
+
|
|
43
|
+
#
|
|
44
|
+
# Regular expression for matching floating point numbers in strings
|
|
45
|
+
#
|
|
46
|
+
# Matches decimal numbers with optional negative sign, used for type detection
|
|
47
|
+
# when analyzing API response data.
|
|
48
|
+
#
|
|
49
|
+
# @api private
|
|
50
|
+
#
|
|
51
|
+
FLOAT_REGEX = /^-?\d+\.\d+$/
|
|
52
|
+
|
|
53
|
+
#
|
|
54
|
+
# Creates a new Compiler instance
|
|
55
|
+
#
|
|
56
|
+
# @param endpoints [Array<Hash>] Raw endpoint data from the Extractor
|
|
57
|
+
#
|
|
58
|
+
# @return [Compiler] A new compiler instance
|
|
59
|
+
#
|
|
60
|
+
def initialize(endpoints)
|
|
61
|
+
@endpoints = endpoints
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
#
|
|
65
|
+
# Compiles endpoints into a structured documentation format
|
|
66
|
+
#
|
|
67
|
+
# Processes all endpoints through grouping, sanitization, merging,
|
|
68
|
+
# and normalization steps to produce a hash structure suitable
|
|
69
|
+
# for documentation generation.
|
|
70
|
+
#
|
|
71
|
+
# @return [Hash] Compiled endpoints organized by path and HTTP method.
|
|
72
|
+
# Each operation contains :id, :description, :parameters, :requests,
|
|
73
|
+
# and :responses keys.
|
|
74
|
+
#
|
|
75
|
+
def compile
|
|
76
|
+
# Step one, group the endpoints by their paths and verb
|
|
77
|
+
# { path: {get: [], post: []}, path_2: {get: []}, ... }
|
|
78
|
+
grouped = group_endpoints(@endpoints)
|
|
79
|
+
|
|
80
|
+
grouped.each_value do |endpoint|
|
|
81
|
+
# Operations are those arrays
|
|
82
|
+
endpoint.transform_values! do |operations|
|
|
83
|
+
# Step two, clear data from any error (4xx, 5xx) operations
|
|
84
|
+
operations = sanitize_error_operations(operations)
|
|
85
|
+
|
|
86
|
+
# Step three, merge all of the operations into one single hash
|
|
87
|
+
operations = merge_operations(operations)
|
|
88
|
+
|
|
89
|
+
# Step four, flatten the operations into one
|
|
90
|
+
flatten_operations(operations)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
private
|
|
96
|
+
|
|
97
|
+
def determine_type(value)
|
|
98
|
+
case value
|
|
99
|
+
when true, false
|
|
100
|
+
"boolean"
|
|
101
|
+
when Float
|
|
102
|
+
# According to the docs: A Float object represents a sometimes-inexact real number
|
|
103
|
+
# using the native architecture’s double-precision floating point representation.
|
|
104
|
+
# So a double it is!
|
|
105
|
+
"double"
|
|
106
|
+
when Integer
|
|
107
|
+
"integer"
|
|
108
|
+
when Array
|
|
109
|
+
"array"
|
|
110
|
+
when NilClass
|
|
111
|
+
"null"
|
|
112
|
+
when DateTime, Time
|
|
113
|
+
"datetime"
|
|
114
|
+
when Date
|
|
115
|
+
"date"
|
|
116
|
+
when String, Symbol
|
|
117
|
+
if value.match?(UUID_REGEX)
|
|
118
|
+
"uuid"
|
|
119
|
+
elsif value.match?(INTEGER_REGEX)
|
|
120
|
+
"integer"
|
|
121
|
+
elsif value.match?(FLOAT_REGEX)
|
|
122
|
+
"double"
|
|
123
|
+
elsif value == "true" || value == "false"
|
|
124
|
+
"boolean"
|
|
125
|
+
else
|
|
126
|
+
"string"
|
|
127
|
+
end
|
|
128
|
+
when URI
|
|
129
|
+
"uri"
|
|
130
|
+
when Numeric
|
|
131
|
+
"number"
|
|
132
|
+
else
|
|
133
|
+
"object"
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
#
|
|
138
|
+
# Groups endpoints by path and HTTP method
|
|
139
|
+
#
|
|
140
|
+
# @param endpoints [Array<Hash>] Array of endpoint data
|
|
141
|
+
#
|
|
142
|
+
# @return [Hash] Endpoints grouped by path and method
|
|
143
|
+
#
|
|
144
|
+
# @private
|
|
145
|
+
#
|
|
146
|
+
def group_endpoints(endpoints)
|
|
147
|
+
grouped = Hash.new { |hash, key| hash[key] = {} }
|
|
148
|
+
|
|
149
|
+
# Convert the endpoints from a flat array of objects into a hash
|
|
150
|
+
endpoints.each do |input|
|
|
151
|
+
# "/users" => {}
|
|
152
|
+
endpoint_hash = grouped[input[:url]]
|
|
153
|
+
|
|
154
|
+
# "GET" => []
|
|
155
|
+
(endpoint_hash[input[:http_verb]] ||= []) << input
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
grouped
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
#
|
|
162
|
+
# Sanitizes operations that represent error responses
|
|
163
|
+
#
|
|
164
|
+
# Removes request details from operations with 4xx/5xx responses
|
|
165
|
+
# to prevent invalid data from appearing in documentation.
|
|
166
|
+
#
|
|
167
|
+
# @param operations [Array<Hash>] Array of operations
|
|
168
|
+
#
|
|
169
|
+
# @return [Array<Hash>] Sanitized operations
|
|
170
|
+
#
|
|
171
|
+
# @private
|
|
172
|
+
#
|
|
173
|
+
def sanitize_error_operations(operations)
|
|
174
|
+
operations.each do |operation|
|
|
175
|
+
next unless operation[:response_status] >= 400
|
|
176
|
+
|
|
177
|
+
# This keeps tests that handle errors from including their invalid attributes
|
|
178
|
+
# and such in the output.
|
|
179
|
+
operation[:request_query] = {}
|
|
180
|
+
operation[:request_headers] = {}
|
|
181
|
+
operation[:request_body] = {}
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
#
|
|
186
|
+
# Merges similar operations into a single operation
|
|
187
|
+
#
|
|
188
|
+
# @param operations [Array<Hash>] Array of operations
|
|
189
|
+
#
|
|
190
|
+
# @return [Array<Hash>] Merged operations
|
|
191
|
+
#
|
|
192
|
+
# @private
|
|
193
|
+
#
|
|
194
|
+
def merge_operations(operations)
|
|
195
|
+
operations.group_by { |o| o[:response_status] }
|
|
196
|
+
.transform_values { |o| o.to_merged_h }
|
|
197
|
+
.values
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
#
|
|
201
|
+
# Flattens multiple operations into a single operation structure
|
|
202
|
+
#
|
|
203
|
+
# @param operations [Array<Hash>] Array of operations
|
|
204
|
+
#
|
|
205
|
+
# @return [Hash] Flattened operation
|
|
206
|
+
#
|
|
207
|
+
# @private
|
|
208
|
+
#
|
|
209
|
+
def flatten_operations(operations)
|
|
210
|
+
# Get HTTP method and path from first operation (all operations in this array have same path/method)
|
|
211
|
+
first_op = operations.first
|
|
212
|
+
http_method = first_op[:http_verb]
|
|
213
|
+
path = first_op[:url]
|
|
214
|
+
|
|
215
|
+
# Create a sanitized ID for operationId (e.g., "getApiV10GuildsId")
|
|
216
|
+
id = "#{http_method}_#{path}".gsub(/[^a-zA-Z0-9]/, "_").to_camelcase(:lower)
|
|
217
|
+
|
|
218
|
+
# Use "METHOD /path" format for summary (e.g., "GET /api/v10/guilds/{id}")
|
|
219
|
+
summary = "#{http_method} #{path}"
|
|
220
|
+
|
|
221
|
+
parameters = normalize_parameters(operations)
|
|
222
|
+
requests = normalize_requests(operations)
|
|
223
|
+
responses = normalize_responses(operations)
|
|
224
|
+
|
|
225
|
+
{
|
|
226
|
+
id:,
|
|
227
|
+
summary:,
|
|
228
|
+
parameters:,
|
|
229
|
+
requests:,
|
|
230
|
+
responses:
|
|
231
|
+
}
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
#
|
|
235
|
+
# Normalizes request parameters from operations
|
|
236
|
+
#
|
|
237
|
+
# Extracts and categorizes parameters as path or query parameters
|
|
238
|
+
# and determines their data types.
|
|
239
|
+
#
|
|
240
|
+
# @param operations [Array<Hash>] Array of operations
|
|
241
|
+
#
|
|
242
|
+
# @return [Hash] Normalized parameters
|
|
243
|
+
#
|
|
244
|
+
# @private
|
|
245
|
+
#
|
|
246
|
+
def normalize_parameters(operations)
|
|
247
|
+
parameters = {}
|
|
248
|
+
|
|
249
|
+
operations.each do |operation|
|
|
250
|
+
# Store the URL so it can be determined if the param is in the path or not
|
|
251
|
+
url = operation[:url]
|
|
252
|
+
params = operation[:request_query].transform_values { |value| {value:, url:} }
|
|
253
|
+
|
|
254
|
+
parameters.merge!(params)
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
parameters.transform_values!(with_key: true) do |data, key|
|
|
258
|
+
key_in_path = data[:url].include?("{#{key}}")
|
|
259
|
+
|
|
260
|
+
{
|
|
261
|
+
location: key_in_path ? "path" : "query",
|
|
262
|
+
type: determine_type(data[:value])
|
|
263
|
+
}
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
#
|
|
268
|
+
# Normalizes request bodies from operations
|
|
269
|
+
#
|
|
270
|
+
# Extracts request bodies from successful operations and
|
|
271
|
+
# determines their data types.
|
|
272
|
+
#
|
|
273
|
+
# @param operations [Array<Hash>] Array of operations
|
|
274
|
+
#
|
|
275
|
+
# @return [Array<Hash>] Normalized request bodies
|
|
276
|
+
#
|
|
277
|
+
# @private
|
|
278
|
+
#
|
|
279
|
+
def normalize_requests(operations)
|
|
280
|
+
successful_operations = operations.select { |o| o[:response_status] < 400 }
|
|
281
|
+
return [] if successful_operations.blank?
|
|
282
|
+
|
|
283
|
+
successful_operations.filter_map.with_index do |operation, index|
|
|
284
|
+
content = operation[:request_body]
|
|
285
|
+
next if content.blank?
|
|
286
|
+
|
|
287
|
+
name = "name_#{SecureRandom.uuid}"
|
|
288
|
+
|
|
289
|
+
{
|
|
290
|
+
name: name || "Example #{index}",
|
|
291
|
+
content_type: operation[:content_type],
|
|
292
|
+
type: determine_type(content),
|
|
293
|
+
content:
|
|
294
|
+
}
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
#
|
|
299
|
+
# Normalizes responses from operations
|
|
300
|
+
#
|
|
301
|
+
# Extracts response details including status, headers, and body
|
|
302
|
+
# and determines their data types.
|
|
303
|
+
#
|
|
304
|
+
# @param operations [Array<Hash>] Array of operations
|
|
305
|
+
#
|
|
306
|
+
# @return [Array<Hash>] Normalized responses
|
|
307
|
+
#
|
|
308
|
+
# @private
|
|
309
|
+
#
|
|
310
|
+
def normalize_responses(operations)
|
|
311
|
+
operations.map do |operation|
|
|
312
|
+
{
|
|
313
|
+
content_type: operation[:content_type],
|
|
314
|
+
status: operation[:response_status],
|
|
315
|
+
headers: normalize_headers(operation[:response_headers]),
|
|
316
|
+
body: normalize_response_body(operation[:response_body])
|
|
317
|
+
}
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
#
|
|
322
|
+
# Normalizes response headers
|
|
323
|
+
#
|
|
324
|
+
# @param headers [Hash] Response headers
|
|
325
|
+
#
|
|
326
|
+
# @return [Hash] Normalized headers with types
|
|
327
|
+
#
|
|
328
|
+
# @private
|
|
329
|
+
#
|
|
330
|
+
def normalize_headers(headers)
|
|
331
|
+
headers.transform_values do |value|
|
|
332
|
+
{type: determine_type(value)}
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
#
|
|
337
|
+
# Normalizes response body structure
|
|
338
|
+
#
|
|
339
|
+
# @param body [Hash, Array, String] Response body
|
|
340
|
+
#
|
|
341
|
+
# @return [Hash] Normalized body structure with type information
|
|
342
|
+
#
|
|
343
|
+
# @private
|
|
344
|
+
#
|
|
345
|
+
def normalize_response_body(body)
|
|
346
|
+
proc = lambda do |value|
|
|
347
|
+
{type: determine_type(value)}
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
case body
|
|
351
|
+
when Hash
|
|
352
|
+
{
|
|
353
|
+
type: "object",
|
|
354
|
+
content: body.deep_transform_values(&proc)
|
|
355
|
+
}
|
|
356
|
+
when Array
|
|
357
|
+
{
|
|
358
|
+
type: "array",
|
|
359
|
+
content: body.map(&proc)
|
|
360
|
+
}
|
|
361
|
+
when String
|
|
362
|
+
{
|
|
363
|
+
type: "string",
|
|
364
|
+
content: body
|
|
365
|
+
}
|
|
366
|
+
else
|
|
367
|
+
raise "Unexpected body: #{body.inspect}"
|
|
368
|
+
end
|
|
369
|
+
end
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SpecForge
|
|
4
|
+
module Documentation
|
|
5
|
+
class Builder
|
|
6
|
+
#
|
|
7
|
+
# Extracts endpoint data from a test execution context
|
|
8
|
+
#
|
|
9
|
+
# The Extractor takes a Forge::Context from a completed test step
|
|
10
|
+
# and extracts all relevant request and response data needed for
|
|
11
|
+
# API documentation generation.
|
|
12
|
+
#
|
|
13
|
+
# @example Extracting endpoint data
|
|
14
|
+
# extractor = Extractor.new(context)
|
|
15
|
+
# endpoint = extractor.extract_endpoint
|
|
16
|
+
# # => { url: "/users", http_verb: "GET", response_status: 200, ... }
|
|
17
|
+
#
|
|
18
|
+
class Extractor
|
|
19
|
+
#
|
|
20
|
+
# Creates a new Extractor instance
|
|
21
|
+
#
|
|
22
|
+
# @param context [Forge::Context] The execution context from a test step
|
|
23
|
+
# containing request/response variables
|
|
24
|
+
#
|
|
25
|
+
# @return [Extractor] A new extractor instance
|
|
26
|
+
#
|
|
27
|
+
def initialize(context)
|
|
28
|
+
@context = context
|
|
29
|
+
@step = context.step
|
|
30
|
+
@variables = context.variables
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
#
|
|
34
|
+
# Extracts endpoint data from the context
|
|
35
|
+
#
|
|
36
|
+
# Pulls request and response information from the context variables
|
|
37
|
+
# and organizes it into a hash structure for documentation.
|
|
38
|
+
#
|
|
39
|
+
# @return [Hash] Endpoint data containing:
|
|
40
|
+
# - :base_url [String] The API base URL
|
|
41
|
+
# - :url [String] The endpoint path
|
|
42
|
+
# - :http_verb [String] The HTTP method (GET, POST, etc.)
|
|
43
|
+
# - :content_type [String, nil] The request content type
|
|
44
|
+
# - :request_body [Hash] The request body
|
|
45
|
+
# - :request_headers [Hash] Request headers (excluding content-type)
|
|
46
|
+
# - :request_query [Hash] Query parameters
|
|
47
|
+
# - :response_status [Integer] HTTP response status code
|
|
48
|
+
# - :response_body [Hash, Array, String] The response body
|
|
49
|
+
# - :response_headers [Hash] Response headers
|
|
50
|
+
#
|
|
51
|
+
def extract_endpoint
|
|
52
|
+
request = @variables[:request]
|
|
53
|
+
response = @variables[:response]
|
|
54
|
+
headers = request[:headers]
|
|
55
|
+
|
|
56
|
+
{
|
|
57
|
+
# Request data
|
|
58
|
+
base_url: request[:base_url],
|
|
59
|
+
url: request[:url],
|
|
60
|
+
http_verb: request[:http_verb],
|
|
61
|
+
content_type: headers["content-type"],
|
|
62
|
+
request_body: request[:body],
|
|
63
|
+
request_headers: headers.except("content-type"),
|
|
64
|
+
request_query: request[:query],
|
|
65
|
+
|
|
66
|
+
# Response data
|
|
67
|
+
response_status: response[:status],
|
|
68
|
+
response_body: response[:body],
|
|
69
|
+
response_headers: response[:headers]
|
|
70
|
+
}
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|