spec_forge 0.7.0 → 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 +139 -9
- data/README.md +125 -203
- 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 +6 -0
- data/lib/spec_forge/cli.rb +6 -14
- data/lib/spec_forge/configuration.rb +212 -78
- 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 +22 -6
- data/lib/spec_forge/documentation/openapi/v3_0/response.rb +29 -7
- 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 -143
- 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 -74
- 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 -183
- data/lib/spec_forge/runner/callbacks.rb +0 -246
- data/lib/spec_forge/runner/debug_proxy.rb +0 -213
- 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
|
@@ -3,380 +3,128 @@
|
|
|
3
3
|
module SpecForge
|
|
4
4
|
module Documentation
|
|
5
5
|
#
|
|
6
|
-
#
|
|
6
|
+
# Builds API documentation by running blueprints and extracting endpoint data
|
|
7
7
|
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
8
|
+
# The Builder orchestrates the documentation generation process by:
|
|
9
|
+
# 1. Loading and running blueprint test files
|
|
10
|
+
# 2. Capturing request/response data from successful test executions
|
|
11
|
+
# 3. Compiling the raw data into a structured Document
|
|
10
12
|
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
+
# It supports caching to avoid re-running tests when blueprints haven't changed.
|
|
14
|
+
#
|
|
15
|
+
# @example Creating a document from blueprints
|
|
16
|
+
# document = Builder.create_document!(paths: "spec/blueprints/api.yml")
|
|
17
|
+
#
|
|
18
|
+
# @example Using the builder directly for raw endpoint data
|
|
19
|
+
# builder = Builder.new(paths: "spec/blueprints/api.yml")
|
|
20
|
+
# endpoints = builder.endpoints
|
|
13
21
|
#
|
|
14
22
|
class Builder
|
|
15
|
-
# Source: https://gist.github.com/johnelliott/cf77003f72f889abbc3f32785fa3df8d
|
|
16
|
-
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
|
|
17
|
-
|
|
18
|
-
#
|
|
19
|
-
# Regular expression for matching floating point numbers in strings
|
|
20
|
-
#
|
|
21
|
-
# Matches decimal numbers with optional negative sign, used for type detection
|
|
22
|
-
# when analyzing API response data.
|
|
23
|
-
#
|
|
24
|
-
# @api private
|
|
25
|
-
#
|
|
26
|
-
INTEGER_REGEX = /^-?\d+$/
|
|
27
|
-
|
|
28
|
-
#
|
|
29
|
-
# Regular expression for matching integer numbers in strings
|
|
30
23
|
#
|
|
31
|
-
#
|
|
32
|
-
# when analyzing API response data.
|
|
24
|
+
# Creates a complete Document from blueprint files
|
|
33
25
|
#
|
|
34
|
-
#
|
|
35
|
-
#
|
|
36
|
-
|
|
37
|
-
|
|
26
|
+
# This is the primary entry point for generating documentation.
|
|
27
|
+
# It instantiates a Builder, extracts endpoints, compiles them,
|
|
28
|
+
# and returns a structured Document object.
|
|
38
29
|
#
|
|
39
|
-
#
|
|
30
|
+
# @option base_path [String, Pathname, nil] Base directory for blueprint files
|
|
31
|
+
# @option paths [String, Pathname, nil] Specific blueprint file paths
|
|
32
|
+
# @option verbosity_level [Integer] Output verbosity (0 = silent)
|
|
33
|
+
# @option use_cache [Boolean] Whether to use cached endpoint data if available
|
|
40
34
|
#
|
|
41
|
-
# @
|
|
35
|
+
# @return [Document] A structured document containing all API endpoints
|
|
42
36
|
#
|
|
43
|
-
# @
|
|
37
|
+
# @raise [Error::NoBlueprintsError] If no blueprints are found
|
|
44
38
|
#
|
|
45
|
-
def self.
|
|
46
|
-
new(
|
|
47
|
-
|
|
39
|
+
def self.create_document!(**)
|
|
40
|
+
endpoints = new(**).endpoints
|
|
41
|
+
endpoints = Compiler.new(endpoints).compile
|
|
48
42
|
|
|
49
|
-
#
|
|
50
|
-
# The processed endpoints organized by path and HTTP method
|
|
51
|
-
#
|
|
52
|
-
# Contains all endpoint data after grouping, sanitizing, merging,
|
|
53
|
-
# and flattening operations for document generation.
|
|
54
|
-
#
|
|
55
|
-
# @return [Hash] Processed endpoints ready for document creation
|
|
56
|
-
#
|
|
57
|
-
attr_reader :endpoints
|
|
58
|
-
|
|
59
|
-
#
|
|
60
|
-
# Initializes a new builder with endpoint data
|
|
61
|
-
#
|
|
62
|
-
# @param endpoints [Array<Hash>] Array of endpoint data extracted from tests
|
|
63
|
-
#
|
|
64
|
-
# @return [Builder] A new builder instance
|
|
65
|
-
#
|
|
66
|
-
def initialize(endpoints)
|
|
67
|
-
@endpoints = prepare_endpoints(endpoints)
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
#
|
|
71
|
-
# Prepares endpoint data for document creation
|
|
72
|
-
#
|
|
73
|
-
# Groups endpoints by path and HTTP method, sanitizes error responses,
|
|
74
|
-
# merges similar operations, and flattens the result.
|
|
75
|
-
#
|
|
76
|
-
# @param endpoints [Array<Hash>] Raw endpoint data from tests
|
|
77
|
-
#
|
|
78
|
-
# @return [Hash] Processed endpoints organized by path and method
|
|
79
|
-
#
|
|
80
|
-
def prepare_endpoints(endpoints)
|
|
81
|
-
# Step one, group the endpoints by their paths and verb
|
|
82
|
-
# { path: {get: [], post: []}, path_2: {get: []}, ... }
|
|
83
|
-
grouped = group_endpoints(endpoints)
|
|
84
|
-
|
|
85
|
-
grouped.each_value do |endpoint|
|
|
86
|
-
# Operations are those arrays
|
|
87
|
-
endpoint.transform_values! do |operations|
|
|
88
|
-
# Step two, clear data from any error (4xx, 5xx) operations
|
|
89
|
-
operations = sanitize_error_operations(operations)
|
|
90
|
-
|
|
91
|
-
# Step three, merge all of the operations into one single hash
|
|
92
|
-
operations = merge_operations(operations)
|
|
93
|
-
|
|
94
|
-
# Step four, flatten the operations into one
|
|
95
|
-
flatten_operations(operations)
|
|
96
|
-
end
|
|
97
|
-
end
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
#
|
|
101
|
-
# Exports the processed endpoints as a document
|
|
102
|
-
#
|
|
103
|
-
# @return [Document] A document containing the processed endpoints
|
|
104
|
-
#
|
|
105
|
-
def export_as_document
|
|
106
43
|
Document.new(endpoints:)
|
|
107
44
|
end
|
|
108
45
|
|
|
109
|
-
private
|
|
110
|
-
|
|
111
|
-
def determine_type(value)
|
|
112
|
-
case value
|
|
113
|
-
when true, false
|
|
114
|
-
"boolean"
|
|
115
|
-
when Float
|
|
116
|
-
# According to the docs: A Float object represents a sometimes-inexact real number
|
|
117
|
-
# using the native architecture’s double-precision floating point representation.
|
|
118
|
-
# So a double it is!
|
|
119
|
-
"double"
|
|
120
|
-
when Integer
|
|
121
|
-
"integer"
|
|
122
|
-
when Array
|
|
123
|
-
"array"
|
|
124
|
-
when NilClass
|
|
125
|
-
"null"
|
|
126
|
-
when DateTime, Time
|
|
127
|
-
"datetime"
|
|
128
|
-
when Date
|
|
129
|
-
"date"
|
|
130
|
-
when String, Symbol
|
|
131
|
-
if value.match?(UUID_REGEX)
|
|
132
|
-
"uuid"
|
|
133
|
-
elsif value.match?(INTEGER_REGEX)
|
|
134
|
-
"integer"
|
|
135
|
-
elsif value.match?(FLOAT_REGEX)
|
|
136
|
-
"double"
|
|
137
|
-
elsif value == "true" || value == "false"
|
|
138
|
-
"boolean"
|
|
139
|
-
else
|
|
140
|
-
"string"
|
|
141
|
-
end
|
|
142
|
-
when URI
|
|
143
|
-
"uri"
|
|
144
|
-
when Numeric
|
|
145
|
-
"number"
|
|
146
|
-
else
|
|
147
|
-
"object"
|
|
148
|
-
end
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
#
|
|
152
|
-
# Groups endpoints by path and HTTP method
|
|
153
46
|
#
|
|
154
|
-
#
|
|
47
|
+
# Creates a new Builder instance
|
|
155
48
|
#
|
|
156
|
-
# @
|
|
49
|
+
# @param base_path [String, Pathname, nil] Base directory for blueprint files
|
|
50
|
+
# @param paths [String, Pathname, nil] Specific blueprint file paths
|
|
51
|
+
# @param verbosity_level [Integer] Output verbosity during test execution (0 = silent)
|
|
52
|
+
# @param use_cache [Boolean] Whether to use cached endpoint data if valid
|
|
157
53
|
#
|
|
158
|
-
# @
|
|
54
|
+
# @return [Builder] A new builder instance
|
|
159
55
|
#
|
|
160
|
-
def
|
|
161
|
-
|
|
56
|
+
def initialize(base_path: nil, paths: nil, verbosity_level: 0, use_cache: false)
|
|
57
|
+
@cache = Cache.new
|
|
162
58
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
# "GET" => []
|
|
169
|
-
(endpoint_hash[input[:http_verb]] ||= []) << input
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
grouped
|
|
59
|
+
@base_path = base_path
|
|
60
|
+
@paths = paths
|
|
61
|
+
@use_cache = use_cache
|
|
62
|
+
@verbosity_level = verbosity_level
|
|
173
63
|
end
|
|
174
64
|
|
|
175
65
|
#
|
|
176
|
-
#
|
|
66
|
+
# Extracts endpoint data from blueprint test executions
|
|
177
67
|
#
|
|
178
|
-
#
|
|
179
|
-
#
|
|
68
|
+
# Runs all blueprints and captures request/response data from each
|
|
69
|
+
# successful test step. Results are cached for subsequent calls
|
|
70
|
+
# when caching is enabled.
|
|
180
71
|
#
|
|
181
|
-
# @
|
|
72
|
+
# @return [Array<Hash>] Array of endpoint data hashes containing
|
|
73
|
+
# request and response information
|
|
182
74
|
#
|
|
183
|
-
# @
|
|
75
|
+
# @raise [Error::NoBlueprintsError] If no blueprints are found
|
|
184
76
|
#
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
def sanitize_error_operations(operations)
|
|
188
|
-
operations.each do |operation|
|
|
189
|
-
next unless operation[:response_status] >= 400
|
|
77
|
+
def endpoints
|
|
78
|
+
return @cache.read if @use_cache && @cache.valid?
|
|
190
79
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
operation[:request_query] = {}
|
|
194
|
-
operation[:request_headers] = {}
|
|
195
|
-
operation[:request_body] = {}
|
|
196
|
-
end
|
|
197
|
-
end
|
|
80
|
+
endpoints = capture_endpoint_data
|
|
81
|
+
@cache.create(endpoints)
|
|
198
82
|
|
|
199
|
-
|
|
200
|
-
# Merges similar operations into a single operation
|
|
201
|
-
#
|
|
202
|
-
# @param operations [Array<Hash>] Array of operations
|
|
203
|
-
#
|
|
204
|
-
# @return [Array<Hash>] Merged operations
|
|
205
|
-
#
|
|
206
|
-
# @private
|
|
207
|
-
#
|
|
208
|
-
def merge_operations(operations)
|
|
209
|
-
operations.group_by { |o| o[:response_status] }
|
|
210
|
-
.transform_values { |o| o.to_merged_h }
|
|
211
|
-
.values
|
|
83
|
+
endpoints
|
|
212
84
|
end
|
|
213
85
|
|
|
214
|
-
|
|
215
|
-
# Flattens multiple operations into a single operation structure
|
|
216
|
-
#
|
|
217
|
-
# @param operations [Array<Hash>] Array of operations
|
|
218
|
-
#
|
|
219
|
-
# @return [Hash] Flattened operation
|
|
220
|
-
#
|
|
221
|
-
# @private
|
|
222
|
-
#
|
|
223
|
-
def flatten_operations(operations)
|
|
224
|
-
id = operations.key_map(:spec_name).reject(&:blank?).first
|
|
86
|
+
private
|
|
225
87
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
&.second || ""
|
|
88
|
+
def capture_endpoint_data
|
|
89
|
+
# contexts will be empty until the blueprints have been ran
|
|
90
|
+
# Must be done before blueprints are loaded
|
|
91
|
+
contexts = register_callback
|
|
231
92
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
responses = normalize_responses(operations)
|
|
93
|
+
blueprints, forge_hooks = SpecForge::Loader.load_blueprints(base_path: @base_path, paths: @paths)
|
|
94
|
+
raise Error::NoBlueprintsError if blueprints.empty?
|
|
235
95
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
requests:,
|
|
241
|
-
responses:
|
|
242
|
-
}
|
|
96
|
+
run_blueprints(blueprints, verbosity_level: @verbosity_level, hooks: forge_hooks)
|
|
97
|
+
build_endpoints(contexts)
|
|
98
|
+
ensure
|
|
99
|
+
SpecForge.configuration.deregister_callback(:documentation_builder)
|
|
243
100
|
end
|
|
244
101
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
#
|
|
248
|
-
# Extracts and categorizes parameters as path or query parameters
|
|
249
|
-
# and determines their data types.
|
|
250
|
-
#
|
|
251
|
-
# @param operations [Array<Hash>] Array of operations
|
|
252
|
-
#
|
|
253
|
-
# @return [Hash] Normalized parameters
|
|
254
|
-
#
|
|
255
|
-
# @private
|
|
256
|
-
#
|
|
257
|
-
def normalize_parameters(operations)
|
|
258
|
-
parameters = {}
|
|
102
|
+
def register_callback
|
|
103
|
+
contexts = []
|
|
259
104
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
params = operation[:request_query].transform_values { |value| {value:, url:} }
|
|
105
|
+
SpecForge.configure do |config|
|
|
106
|
+
config.register_callback(:documentation_builder) do |context|
|
|
107
|
+
next if context.failure?
|
|
264
108
|
|
|
265
|
-
|
|
266
|
-
|
|
109
|
+
step = context.step
|
|
110
|
+
next if step.nil? || step.documentation == false
|
|
111
|
+
next unless step.request?
|
|
267
112
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
{
|
|
272
|
-
location: key_in_path ? "path" : "query",
|
|
273
|
-
type: determine_type(data[:value])
|
|
274
|
-
}
|
|
275
|
-
end
|
|
276
|
-
end
|
|
277
|
-
|
|
278
|
-
#
|
|
279
|
-
# Normalizes request bodies from operations
|
|
280
|
-
#
|
|
281
|
-
# Extracts request bodies from successful operations and
|
|
282
|
-
# determines their data types.
|
|
283
|
-
#
|
|
284
|
-
# @param operations [Array<Hash>] Array of operations
|
|
285
|
-
#
|
|
286
|
-
# @return [Array<Hash>] Normalized request bodies
|
|
287
|
-
#
|
|
288
|
-
# @private
|
|
289
|
-
#
|
|
290
|
-
def normalize_requests(operations)
|
|
291
|
-
successful_operations = operations.select { |o| o[:response_status] < 400 }
|
|
292
|
-
return [] if successful_operations.blank?
|
|
293
|
-
|
|
294
|
-
successful_operations.filter_map.with_index do |operation, index|
|
|
295
|
-
content = operation[:request_body]
|
|
296
|
-
next if content.blank?
|
|
297
|
-
|
|
298
|
-
name = operation[:expectation_name].split(" - ").second
|
|
113
|
+
contexts << context.with(variables: context.variables.dup)
|
|
114
|
+
end
|
|
299
115
|
|
|
300
|
-
|
|
301
|
-
name: name || "Example #{index}",
|
|
302
|
-
content_type: operation[:content_type],
|
|
303
|
-
type: determine_type(content),
|
|
304
|
-
content:
|
|
305
|
-
}
|
|
116
|
+
config.after(:step, :documentation_builder)
|
|
306
117
|
end
|
|
307
|
-
end
|
|
308
118
|
|
|
309
|
-
|
|
310
|
-
# Normalizes responses from operations
|
|
311
|
-
#
|
|
312
|
-
# Extracts response details including status, headers, and body
|
|
313
|
-
# and determines their data types.
|
|
314
|
-
#
|
|
315
|
-
# @param operations [Array<Hash>] Array of operations
|
|
316
|
-
#
|
|
317
|
-
# @return [Array<Hash>] Normalized responses
|
|
318
|
-
#
|
|
319
|
-
# @private
|
|
320
|
-
#
|
|
321
|
-
def normalize_responses(operations)
|
|
322
|
-
operations.map do |operation|
|
|
323
|
-
{
|
|
324
|
-
content_type: operation[:content_type],
|
|
325
|
-
status: operation[:response_status],
|
|
326
|
-
headers: normalize_headers(operation[:response_headers]),
|
|
327
|
-
body: normalize_response_body(operation[:response_body])
|
|
328
|
-
}
|
|
329
|
-
end
|
|
119
|
+
contexts
|
|
330
120
|
end
|
|
331
121
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
#
|
|
335
|
-
# @param headers [Hash] Response headers
|
|
336
|
-
#
|
|
337
|
-
# @return [Hash] Normalized headers with types
|
|
338
|
-
#
|
|
339
|
-
# @private
|
|
340
|
-
#
|
|
341
|
-
def normalize_headers(headers)
|
|
342
|
-
headers.transform_values do |value|
|
|
343
|
-
{type: determine_type(value)}
|
|
344
|
-
end
|
|
122
|
+
def run_blueprints(blueprints, **)
|
|
123
|
+
SpecForge::Forge.ignite.run(blueprints, **)
|
|
345
124
|
end
|
|
346
125
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
#
|
|
350
|
-
# @param body [Hash, Array, String] Response body
|
|
351
|
-
#
|
|
352
|
-
# @return [Hash] Normalized body structure with type information
|
|
353
|
-
#
|
|
354
|
-
# @private
|
|
355
|
-
#
|
|
356
|
-
def normalize_response_body(body)
|
|
357
|
-
proc = lambda do |value|
|
|
358
|
-
{type: determine_type(value)}
|
|
359
|
-
end
|
|
360
|
-
|
|
361
|
-
case body
|
|
362
|
-
when Hash
|
|
363
|
-
{
|
|
364
|
-
type: "object",
|
|
365
|
-
content: body.deep_transform_values(&proc)
|
|
366
|
-
}
|
|
367
|
-
when Array
|
|
368
|
-
{
|
|
369
|
-
type: "array",
|
|
370
|
-
content: body.map(&proc)
|
|
371
|
-
}
|
|
372
|
-
when String
|
|
373
|
-
{
|
|
374
|
-
type: "string",
|
|
375
|
-
content: body
|
|
376
|
-
}
|
|
377
|
-
else
|
|
378
|
-
raise "Unexpected body: #{body.inspect}"
|
|
379
|
-
end
|
|
126
|
+
def build_endpoints(contexts)
|
|
127
|
+
contexts.map { |context| Extractor.new(context).extract_endpoint }
|
|
380
128
|
end
|
|
381
129
|
end
|
|
382
130
|
end
|
|
@@ -13,25 +13,25 @@ module SpecForge
|
|
|
13
13
|
# @example Operation for creating a user
|
|
14
14
|
# operation = Operation.new(
|
|
15
15
|
# id: "create_user",
|
|
16
|
-
#
|
|
16
|
+
# summary: "Creates a new user",
|
|
17
17
|
# parameters: {id: {name: "id", location: "path", type: "integer"}},
|
|
18
18
|
# requests: [{name: "example", content_type: "application/json", type: "object", content: {}}],
|
|
19
19
|
# responses: [{status: 201, content_type: "application/json", headers: {}, body: {}}]
|
|
20
20
|
# )
|
|
21
21
|
#
|
|
22
|
-
class Operation < Data.define(:id, :
|
|
22
|
+
class Operation < Data.define(:id, :summary, :parameters, :requests, :responses)
|
|
23
23
|
#
|
|
24
24
|
# Creates a new operation with normalized sub-components
|
|
25
25
|
#
|
|
26
26
|
# @param id [String] Unique identifier for the operation
|
|
27
|
-
# @param
|
|
27
|
+
# @param summary [String] Human-readable summary
|
|
28
28
|
# @param parameters [Hash] Parameters by name with their details
|
|
29
29
|
# @param requests [Array<Hash>] Request body examples
|
|
30
30
|
# @param responses [Array<Hash>] Possible responses
|
|
31
31
|
#
|
|
32
32
|
# @return [Operation] A new operation instance
|
|
33
33
|
#
|
|
34
|
-
def initialize(id:,
|
|
34
|
+
def initialize(id:, summary:, parameters:, requests:, responses:)
|
|
35
35
|
parameters = parameters.each_pair.map do |name, value|
|
|
36
36
|
[name, Parameter.new(name: name.to_s, **value)]
|
|
37
37
|
end.to_h
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SpecForge
|
|
4
|
+
module Documentation
|
|
5
|
+
#
|
|
6
|
+
# Base class for all documentation generators
|
|
7
|
+
#
|
|
8
|
+
# Provides the common interface and shared functionality for generators
|
|
9
|
+
# that transform SpecForge documents into various output formats.
|
|
10
|
+
# Subclasses implement format-specific generation logic.
|
|
11
|
+
#
|
|
12
|
+
# @example Creating a custom generator
|
|
13
|
+
# class MyGenerator < Generator
|
|
14
|
+
# def generate
|
|
15
|
+
# # Transform input document to custom format
|
|
16
|
+
# end
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
class Generator
|
|
20
|
+
#
|
|
21
|
+
# Converts the generator's version to a semantic version object
|
|
22
|
+
#
|
|
23
|
+
# @return [SemVersion] The semantic version
|
|
24
|
+
#
|
|
25
|
+
def self.to_sem_version
|
|
26
|
+
SemVersion.new(const_get("CURRENT_VERSION"))
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
#
|
|
30
|
+
# Generates documentation from test data with optional caching
|
|
31
|
+
#
|
|
32
|
+
# @param use_cache [Boolean] Whether to use cached test data if available
|
|
33
|
+
#
|
|
34
|
+
# @return [Object] The generated documentation in the target format
|
|
35
|
+
#
|
|
36
|
+
# @raise [RuntimeError] Must be implemented by subclasses
|
|
37
|
+
#
|
|
38
|
+
def self.generate(use_cache: false)
|
|
39
|
+
raise "not implemented"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
#
|
|
43
|
+
# Validates the generated output according to format specifications
|
|
44
|
+
#
|
|
45
|
+
# @param input [Object] The generated documentation to validate
|
|
46
|
+
#
|
|
47
|
+
# @return [void]
|
|
48
|
+
#
|
|
49
|
+
# @raise [RuntimeError] Must be implemented by subclasses
|
|
50
|
+
#
|
|
51
|
+
def self.validate!(input)
|
|
52
|
+
raise "not implemented"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
#
|
|
56
|
+
# The input document containing structured API data
|
|
57
|
+
#
|
|
58
|
+
# Contains all the endpoint information extracted from tests,
|
|
59
|
+
# organized and ready for transformation into the target format.
|
|
60
|
+
#
|
|
61
|
+
# @return [Document] The document to be processed by the generator
|
|
62
|
+
#
|
|
63
|
+
attr_reader :input
|
|
64
|
+
|
|
65
|
+
#
|
|
66
|
+
# Initializes a new generators
|
|
67
|
+
#
|
|
68
|
+
# @param input [Hash, Document] The document to generate
|
|
69
|
+
#
|
|
70
|
+
# @return [Base] A new generator instance
|
|
71
|
+
#
|
|
72
|
+
def initialize(input = {})
|
|
73
|
+
@input = input
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
#
|
|
77
|
+
# Generates the document into a specific format
|
|
78
|
+
#
|
|
79
|
+
# @raise [RuntimeError] Must be implemented by subclasses
|
|
80
|
+
#
|
|
81
|
+
# @return [Object] The generated document
|
|
82
|
+
#
|
|
83
|
+
def generate
|
|
84
|
+
raise "not implemented"
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module SpecForge
|
|
4
4
|
module Documentation
|
|
5
5
|
module OpenAPI
|
|
6
|
-
|
|
6
|
+
class V30
|
|
7
7
|
#
|
|
8
8
|
# Represents an OpenAPI 3.0 Operation object
|
|
9
9
|
#
|
|
@@ -12,7 +12,23 @@ module SpecForge
|
|
|
12
12
|
#
|
|
13
13
|
# @see https://spec.openapis.org/oas/v3.0.4.html#operation-object
|
|
14
14
|
#
|
|
15
|
-
class Operation
|
|
15
|
+
class Operation
|
|
16
|
+
#
|
|
17
|
+
# The document object containing structured API data
|
|
18
|
+
#
|
|
19
|
+
# @return [Object] The document with endpoint information
|
|
20
|
+
#
|
|
21
|
+
attr_reader :document
|
|
22
|
+
|
|
23
|
+
#
|
|
24
|
+
# Creates a new Operation from a document
|
|
25
|
+
#
|
|
26
|
+
# @param document [Object] The document containing operation data
|
|
27
|
+
#
|
|
28
|
+
def initialize(document)
|
|
29
|
+
@document = document
|
|
30
|
+
end
|
|
31
|
+
|
|
16
32
|
#
|
|
17
33
|
# Converts the operation to an OpenAPI-compliant hash
|
|
18
34
|
#
|
|
@@ -26,7 +42,7 @@ module SpecForge
|
|
|
26
42
|
# Required
|
|
27
43
|
responses:,
|
|
28
44
|
security:
|
|
29
|
-
}.
|
|
45
|
+
}.compact_merge(
|
|
30
46
|
# All optional
|
|
31
47
|
tags:,
|
|
32
48
|
summary:,
|
|
@@ -44,7 +60,7 @@ module SpecForge
|
|
|
44
60
|
#
|
|
45
61
|
def id
|
|
46
62
|
# The object ID is added to make every ID unique
|
|
47
|
-
document.id
|
|
63
|
+
document.id + object_id.to_s
|
|
48
64
|
end
|
|
49
65
|
|
|
50
66
|
alias_method :operationId, :id
|
|
@@ -55,7 +71,7 @@ module SpecForge
|
|
|
55
71
|
# @return [String, nil] Brief operation summary
|
|
56
72
|
#
|
|
57
73
|
def summary
|
|
58
|
-
document.
|
|
74
|
+
document.summary
|
|
59
75
|
end
|
|
60
76
|
|
|
61
77
|
#
|
|
@@ -64,7 +80,7 @@ module SpecForge
|
|
|
64
80
|
# @return [String] Detailed operation description
|
|
65
81
|
#
|
|
66
82
|
def description
|
|
67
|
-
|
|
83
|
+
""
|
|
68
84
|
end
|
|
69
85
|
|
|
70
86
|
#
|