spec_forge 0.6.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.
Files changed (83) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +112 -2
  3. data/README.md +133 -8
  4. data/flake.lock +3 -3
  5. data/flake.nix +3 -3
  6. data/lib/spec_forge/attribute/factory.rb +1 -1
  7. data/lib/spec_forge/callbacks.rb +9 -0
  8. data/lib/spec_forge/cli/docs/generate.rb +72 -0
  9. data/lib/spec_forge/cli/docs.rb +92 -0
  10. data/lib/spec_forge/cli/init.rb +39 -7
  11. data/lib/spec_forge/cli/new.rb +13 -3
  12. data/lib/spec_forge/cli/run.rb +12 -4
  13. data/lib/spec_forge/cli/serve.rb +155 -0
  14. data/lib/spec_forge/cli.rb +14 -6
  15. data/lib/spec_forge/configuration.rb +2 -2
  16. data/lib/spec_forge/context/store.rb +23 -40
  17. data/lib/spec_forge/core_ext/array.rb +27 -0
  18. data/lib/spec_forge/documentation/builder.rb +383 -0
  19. data/lib/spec_forge/documentation/document/operation.rb +47 -0
  20. data/lib/spec_forge/documentation/document/parameter.rb +22 -0
  21. data/lib/spec_forge/documentation/document/request_body.rb +24 -0
  22. data/lib/spec_forge/documentation/document/response.rb +39 -0
  23. data/lib/spec_forge/documentation/document/response_body.rb +27 -0
  24. data/lib/spec_forge/documentation/document.rb +48 -0
  25. data/lib/spec_forge/documentation/generators/base.rb +81 -0
  26. data/lib/spec_forge/documentation/generators/openapi/base.rb +100 -0
  27. data/lib/spec_forge/documentation/generators/openapi/error_formatter.rb +149 -0
  28. data/lib/spec_forge/documentation/generators/openapi/v3_0.rb +65 -0
  29. data/lib/spec_forge/documentation/generators/openapi.rb +59 -0
  30. data/lib/spec_forge/documentation/generators.rb +17 -0
  31. data/lib/spec_forge/documentation/loader/cache.rb +138 -0
  32. data/lib/spec_forge/documentation/loader.rb +159 -0
  33. data/lib/spec_forge/documentation/openapi/base.rb +33 -0
  34. data/lib/spec_forge/documentation/openapi/v3_0/example.rb +44 -0
  35. data/lib/spec_forge/documentation/openapi/v3_0/media_type.rb +42 -0
  36. data/lib/spec_forge/documentation/openapi/v3_0/operation.rb +175 -0
  37. data/lib/spec_forge/documentation/openapi/v3_0/response.rb +65 -0
  38. data/lib/spec_forge/documentation/openapi/v3_0/schema.rb +80 -0
  39. data/lib/spec_forge/documentation/openapi/v3_0/tag.rb +71 -0
  40. data/lib/spec_forge/documentation/openapi.rb +23 -0
  41. data/lib/spec_forge/documentation.rb +27 -0
  42. data/lib/spec_forge/error.rb +17 -0
  43. data/lib/spec_forge/factory.rb +2 -2
  44. data/lib/spec_forge/filter.rb +3 -4
  45. data/lib/spec_forge/forge.rb +5 -4
  46. data/lib/spec_forge/http/backend.rb +2 -0
  47. data/lib/spec_forge/http/request.rb +14 -3
  48. data/lib/spec_forge/loader.rb +14 -24
  49. data/lib/spec_forge/normalizer/default.rb +51 -0
  50. data/lib/spec_forge/normalizer/definition.rb +248 -0
  51. data/lib/spec_forge/normalizer/validators.rb +99 -0
  52. data/lib/spec_forge/normalizer.rb +356 -199
  53. data/lib/spec_forge/normalizers/_shared.yml +74 -0
  54. data/lib/spec_forge/normalizers/configuration.yml +23 -0
  55. data/lib/spec_forge/normalizers/constraint.yml +8 -0
  56. data/lib/spec_forge/normalizers/expectation.yml +47 -0
  57. data/lib/spec_forge/normalizers/factory.yml +12 -0
  58. data/lib/spec_forge/normalizers/factory_reference.yml +15 -0
  59. data/lib/spec_forge/normalizers/global_context.yml +28 -0
  60. data/lib/spec_forge/normalizers/spec.yml +50 -0
  61. data/lib/spec_forge/runner/adapter.rb +183 -0
  62. data/lib/spec_forge/runner/debug_proxy.rb +3 -3
  63. data/lib/spec_forge/runner/state.rb +4 -5
  64. data/lib/spec_forge/runner.rb +40 -124
  65. data/lib/spec_forge/spec/expectation/constraint.rb +13 -5
  66. data/lib/spec_forge/spec/expectation.rb +7 -3
  67. data/lib/spec_forge/spec.rb +13 -58
  68. data/lib/spec_forge/version.rb +1 -1
  69. data/lib/spec_forge.rb +30 -23
  70. data/lib/templates/openapi.yml.tt +22 -0
  71. data/lib/templates/redoc.html.tt +28 -0
  72. data/lib/templates/swagger.html.tt +59 -0
  73. metadata +92 -14
  74. data/lib/spec_forge/normalizer/configuration.rb +0 -90
  75. data/lib/spec_forge/normalizer/constraint.rb +0 -60
  76. data/lib/spec_forge/normalizer/expectation.rb +0 -105
  77. data/lib/spec_forge/normalizer/factory.rb +0 -78
  78. data/lib/spec_forge/normalizer/factory_reference.rb +0 -85
  79. data/lib/spec_forge/normalizer/global_context.rb +0 -88
  80. data/lib/spec_forge/normalizer/spec.rb +0 -97
  81. /data/lib/templates/{forge_helper.tt → forge_helper.rb.tt} +0 -0
  82. /data/lib/templates/{new_factory.tt → new_factory.yml.tt} +0 -0
  83. /data/lib/templates/{new_spec.tt → new_spec.yml.tt} +0 -0
@@ -0,0 +1,383 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SpecForge
4
+ module Documentation
5
+ #
6
+ # Transforms extracted test data into a structured document
7
+ #
8
+ # This class processes raw endpoint data from tests into a hierarchical document
9
+ # structure suitable for rendering as API documentation.
10
+ #
11
+ # @example Creating a document from test data
12
+ # document = Builder.document_from_endpoints(endpoints)
13
+ #
14
+ 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
+ #
31
+ # Matches whole numbers with optional negative sign, used for type detection
32
+ # when analyzing API response data.
33
+ #
34
+ # @api private
35
+ #
36
+ FLOAT_REGEX = /^-?\d+\.\d+$/
37
+
38
+ #
39
+ # Creates a document from endpoint data
40
+ #
41
+ # @param endpoints [Array<Hash>] Array of endpoint data extracted from tests
42
+ #
43
+ # @return [Document] A structured documentation document
44
+ #
45
+ def self.document_from_endpoints(endpoints = [])
46
+ new(endpoints).export_as_document
47
+ end
48
+
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
+ Document.new(endpoints:)
107
+ end
108
+
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
+ #
154
+ # @param endpoints [Array<Hash>] Array of endpoint data
155
+ #
156
+ # @return [Hash] Endpoints grouped by path and method
157
+ #
158
+ # @private
159
+ #
160
+ def group_endpoints(endpoints)
161
+ grouped = Hash.new_nested_hash(depth: 1)
162
+
163
+ # Convert the endpoints from a flat array of objects into a hash
164
+ endpoints.each do |input|
165
+ # "/users" => {}
166
+ endpoint_hash = grouped[input[:url]]
167
+
168
+ # "GET" => []
169
+ (endpoint_hash[input[:http_verb]] ||= []) << input
170
+ end
171
+
172
+ grouped
173
+ end
174
+
175
+ #
176
+ # Sanitizes operations that represent error responses
177
+ #
178
+ # Removes request details from operations with 4xx/5xx responses
179
+ # to prevent invalid data from appearing in documentation.
180
+ #
181
+ # @param operations [Array<Hash>] Array of operations
182
+ #
183
+ # @return [Array<Hash>] Sanitized operations
184
+ #
185
+ # @private
186
+ #
187
+ def sanitize_error_operations(operations)
188
+ operations.each do |operation|
189
+ next unless operation[:response_status] >= 400
190
+
191
+ # This keeps tests that handle errors from including their invalid attributes
192
+ # and such in the output.
193
+ operation[:request_query] = {}
194
+ operation[:request_headers] = {}
195
+ operation[:request_body] = {}
196
+ end
197
+ end
198
+
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
212
+ end
213
+
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
225
+
226
+ description = operations.key_map(:expectation_name)
227
+ .reject(&:blank?)
228
+ .first
229
+ &.split(" - ")
230
+ &.second || ""
231
+
232
+ parameters = normalize_parameters(operations)
233
+ requests = normalize_requests(operations)
234
+ responses = normalize_responses(operations)
235
+
236
+ {
237
+ id:,
238
+ description:,
239
+ parameters:,
240
+ requests:,
241
+ responses:
242
+ }
243
+ end
244
+
245
+ #
246
+ # Normalizes request parameters from operations
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 = {}
259
+
260
+ operations.each do |operation|
261
+ # Store the URL so it can be determined if the param is in the path or not
262
+ url = operation[:url]
263
+ params = operation[:request_query].transform_values { |value| {value:, url:} }
264
+
265
+ parameters.merge!(params)
266
+ end
267
+
268
+ parameters.transform_values!(with_key: true) do |data, key|
269
+ key_in_path = data[:url].include?("{#{key}}")
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
299
+
300
+ {
301
+ name: name || "Example #{index}",
302
+ content_type: operation[:content_type],
303
+ type: determine_type(content),
304
+ content:
305
+ }
306
+ end
307
+ end
308
+
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
330
+ end
331
+
332
+ #
333
+ # Normalizes response headers
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
345
+ end
346
+
347
+ #
348
+ # Normalizes response body structure
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
380
+ end
381
+ end
382
+ end
383
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SpecForge
4
+ module Documentation
5
+ class Document
6
+ #
7
+ # Represents an API operation (endpoint + HTTP method)
8
+ #
9
+ # An Operation contains all the information about a specific API endpoint
10
+ # with a specific HTTP method, including parameters, request bodies,
11
+ # and possible responses.
12
+ #
13
+ # @example Operation for creating a user
14
+ # operation = Operation.new(
15
+ # id: "create_user",
16
+ # description: "Creates a new user",
17
+ # parameters: {id: {name: "id", location: "path", type: "integer"}},
18
+ # requests: [{name: "example", content_type: "application/json", type: "object", content: {}}],
19
+ # responses: [{status: 201, content_type: "application/json", headers: {}, body: {}}]
20
+ # )
21
+ #
22
+ class Operation < Data.define(:id, :description, :parameters, :requests, :responses)
23
+ #
24
+ # Creates a new operation with normalized sub-components
25
+ #
26
+ # @param id [String] Unique identifier for the operation
27
+ # @param description [String] Human-readable description
28
+ # @param parameters [Hash] Parameters by name with their details
29
+ # @param requests [Array<Hash>] Request body examples
30
+ # @param responses [Array<Hash>] Possible responses
31
+ #
32
+ # @return [Operation] A new operation instance
33
+ #
34
+ def initialize(id:, description:, parameters:, requests:, responses:)
35
+ parameters = parameters.each_pair.map do |name, value|
36
+ [name, Parameter.new(name: name.to_s, **value)]
37
+ end.to_h
38
+
39
+ requests = requests.map { |r| RequestBody.new(**r) }
40
+ responses = responses.map { |r| Response.new(**r) }
41
+
42
+ super
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SpecForge
4
+ module Documentation
5
+ class Document
6
+ #
7
+ # Represents a parameter for an API operation
8
+ #
9
+ # Parameters can appear in various locations (path, query, header)
10
+ # and have different types and validation rules.
11
+ #
12
+ # @example Path parameter
13
+ # Parameter.new(name: "id", location: "path", type: "integer")
14
+ #
15
+ # @example Query parameter
16
+ # Parameter.new(name: "limit", location: "query", type: "integer")
17
+ #
18
+ class Parameter < Data.define(:name, :location, :type)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SpecForge
4
+ module Documentation
5
+ class Document
6
+ #
7
+ # Represents a request body example for an API operation
8
+ #
9
+ # Contains the content type, data structure, and example content
10
+ # for a request body.
11
+ #
12
+ # @example JSON request body
13
+ # RequestBody.new(
14
+ # name: "Create User",
15
+ # content_type: "application/json",
16
+ # type: "object",
17
+ # content: {name: "Example User", email: "user@example.com"}
18
+ # )
19
+ #
20
+ class RequestBody < Data.define(:name, :content_type, :type, :content)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SpecForge
4
+ module Documentation
5
+ class Document
6
+ #
7
+ # Represents a possible response from an API operation
8
+ #
9
+ # Contains the status code, headers, and body content
10
+ # with content type information.
11
+ #
12
+ # @example Success response
13
+ # Response.new(
14
+ # content_type: "application/json",
15
+ # status: 200,
16
+ # headers: {"Cache-Control" => {type: "string"}},
17
+ # body: {type: "object", content: {id: {type: "integer"}}}
18
+ # )
19
+ #
20
+ class Response < Data.define(:content_type, :status, :headers, :body)
21
+ #
22
+ # Creates a new response with a normalized body
23
+ #
24
+ # @param content_type [String] The content type (e.g., "application/json")
25
+ # @param status [Integer] The HTTP status code
26
+ # @param headers [Hash] Response headers with their types
27
+ # @param body [Hash] Response body description
28
+ #
29
+ # @return [Response] A new response instance
30
+ #
31
+ def initialize(content_type:, status:, headers:, body:)
32
+ body = ResponseBody.new(**body) if body.present?
33
+
34
+ super
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SpecForge
4
+ module Documentation
5
+ class Document
6
+ #
7
+ # Represents a response body structure
8
+ #
9
+ # Contains the type and content structure of a response body.
10
+ #
11
+ # @example Object response body
12
+ # ResponseBody.new(
13
+ # type: "object",
14
+ # content: {user: {type: "object", content: {id: {type: "integer"}}}}
15
+ # )
16
+ #
17
+ # @example Array response body
18
+ # ResponseBody.new(
19
+ # type: "array",
20
+ # content: [{type: "string"}]
21
+ # )
22
+ #
23
+ class ResponseBody < Data.define(:type, :content)
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SpecForge
4
+ module Documentation
5
+ #
6
+ # Represents the structured API documentation
7
+ #
8
+ # This class is the central data structure for API documentation,
9
+ # containing all endpoints organized by path and HTTP method.
10
+ # It serves as the bridge between extracted test data and generators.
11
+ #
12
+ # @example Creating a document
13
+ # document = Document.new(
14
+ # endpoints: {
15
+ # "/users" => {
16
+ # "get" => {id: "list_users", description: "List all users"...},
17
+ # "post" => {id: "create_user", description: "Create a user"...}
18
+ # }
19
+ # }
20
+ # )
21
+ #
22
+ class Document < Data.define(:endpoints)
23
+ #
24
+ # Creates a new document with normalized endpoints
25
+ #
26
+ # @param endpoints [Hash] A hash mapping paths to operations by HTTP method
27
+ #
28
+ # @return [Document] A new document instance
29
+ #
30
+ def initialize(endpoints: {})
31
+ endpoints = endpoints.transform_values do |operations|
32
+ operations.transform_keys(&:downcase)
33
+ .transform_values! { |op| Operation.new(**op) }
34
+ end
35
+
36
+ endpoints.deep_symbolize_keys!
37
+
38
+ super
39
+ end
40
+ end
41
+ end
42
+ end
43
+
44
+ require_relative "document/operation"
45
+ require_relative "document/parameter"
46
+ require_relative "document/request_body"
47
+ require_relative "document/response"
48
+ require_relative "document/response_body"
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SpecForge
4
+ module Documentation
5
+ module Generators
6
+ #
7
+ # Base class for all documentation generators
8
+ #
9
+ # Provides the common interface and shared functionality for generators
10
+ # that transform SpecForge documents into various output formats.
11
+ # Subclasses implement format-specific generation logic.
12
+ #
13
+ # @example Creating a custom generator
14
+ # class MyGenerator < Base
15
+ # def generate
16
+ # # Transform input document to custom format
17
+ # end
18
+ # end
19
+ #
20
+ class Base
21
+ #
22
+ # Generates documentation from test data with optional caching
23
+ #
24
+ # @param use_cache [Boolean] Whether to use cached test data if available
25
+ #
26
+ # @return [Object] The generated documentation in the target format
27
+ #
28
+ # @raise [RuntimeError] Must be implemented by subclasses
29
+ #
30
+ def self.generate(use_cache: false)
31
+ raise "not implemented"
32
+ end
33
+
34
+ #
35
+ # Validates the generated output according to format specifications
36
+ #
37
+ # @param input [Object] The generated documentation to validate
38
+ #
39
+ # @return [void]
40
+ #
41
+ # @raise [RuntimeError] Must be implemented by subclasses
42
+ #
43
+ def self.validate!(input)
44
+ raise "not implemented"
45
+ end
46
+
47
+ #
48
+ # The input document containing structured API data
49
+ #
50
+ # Contains all the endpoint information extracted from tests,
51
+ # organized and ready for transformation into the target format.
52
+ #
53
+ # @return [Document] The document to be processed by the generator
54
+ #
55
+ attr_reader :input
56
+
57
+ #
58
+ # Initializes a new generators
59
+ #
60
+ # @param input [Hash, Document] The document to generate
61
+ #
62
+ # @return [Base] A new generator instance
63
+ #
64
+ def initialize(input = {})
65
+ @input = input
66
+ end
67
+
68
+ #
69
+ # Generates the document into a specific format
70
+ #
71
+ # @raise [RuntimeError] Must be implemented by subclasses
72
+ #
73
+ # @return [Object] The generated document
74
+ #
75
+ def generate
76
+ raise "not implemented"
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end