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.
Files changed (133) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +139 -9
  3. data/README.md +125 -203
  4. data/bin/spec_forge +1 -1
  5. data/flake.lock +76 -4
  6. data/flake.nix +5 -4
  7. data/lib/spec_forge/attribute/chainable.rb +6 -6
  8. data/lib/spec_forge/attribute/environment.rb +45 -0
  9. data/lib/spec_forge/attribute/factory.rb +26 -17
  10. data/lib/spec_forge/attribute/faker.rb +6 -1
  11. data/lib/spec_forge/attribute/generate.rb +114 -0
  12. data/lib/spec_forge/attribute/literal.rb +1 -14
  13. data/lib/spec_forge/attribute/matcher.rb +6 -2
  14. data/lib/spec_forge/attribute/parameterized.rb +20 -22
  15. data/lib/spec_forge/attribute/resolvable_array.rb +16 -16
  16. data/lib/spec_forge/attribute/resolvable_hash.rb +17 -16
  17. data/lib/spec_forge/attribute/resolvable_struct.rb +67 -0
  18. data/lib/spec_forge/attribute/template.rb +118 -0
  19. data/lib/spec_forge/attribute/transform.rb +14 -19
  20. data/lib/spec_forge/attribute/variable.rb +31 -31
  21. data/lib/spec_forge/attribute.rb +54 -100
  22. data/lib/spec_forge/blueprint.rb +27 -0
  23. data/lib/spec_forge/cli/docs/generate.rb +28 -8
  24. data/lib/spec_forge/cli/docs.rb +5 -2
  25. data/lib/spec_forge/cli/init.rb +4 -4
  26. data/lib/spec_forge/cli/new.rb +78 -27
  27. data/lib/spec_forge/cli/run.rb +84 -52
  28. data/lib/spec_forge/cli/serve.rb +6 -0
  29. data/lib/spec_forge/cli.rb +6 -14
  30. data/lib/spec_forge/configuration.rb +212 -78
  31. data/lib/spec_forge/documentation/{loader → builder}/cache.rb +26 -23
  32. data/lib/spec_forge/documentation/builder/compiler.rb +373 -0
  33. data/lib/spec_forge/documentation/builder/extractor.rb +75 -0
  34. data/lib/spec_forge/documentation/builder.rb +77 -329
  35. data/lib/spec_forge/documentation/document/operation.rb +4 -4
  36. data/lib/spec_forge/documentation/document.rb +0 -6
  37. data/lib/spec_forge/documentation/generator.rb +88 -0
  38. data/lib/spec_forge/documentation/{generators/openapi → openapi/v3_0}/error_formatter.rb +2 -2
  39. data/lib/spec_forge/documentation/openapi/v3_0/example.rb +1 -1
  40. data/lib/spec_forge/documentation/openapi/v3_0/media_type.rb +1 -1
  41. data/lib/spec_forge/documentation/openapi/v3_0/operation.rb +22 -6
  42. data/lib/spec_forge/documentation/openapi/v3_0/response.rb +29 -7
  43. data/lib/spec_forge/documentation/openapi/v3_0/schema.rb +20 -2
  44. data/lib/spec_forge/documentation/openapi/v3_0/tag.rb +1 -1
  45. data/lib/spec_forge/documentation/openapi/v3_0.rb +116 -0
  46. data/lib/spec_forge/documentation/openapi.rb +40 -12
  47. data/lib/spec_forge/documentation.rb +1 -7
  48. data/lib/spec_forge/error.rb +215 -41
  49. data/lib/spec_forge/factory.rb +38 -18
  50. data/lib/spec_forge/forge/action.rb +41 -0
  51. data/lib/spec_forge/forge/actions/call.rb +33 -0
  52. data/lib/spec_forge/forge/actions/debug.rb +47 -0
  53. data/lib/spec_forge/forge/actions/expect.rb +44 -0
  54. data/lib/spec_forge/forge/actions/request.rb +65 -0
  55. data/lib/spec_forge/forge/actions/store.rb +31 -0
  56. data/lib/spec_forge/forge/callbacks.rb +80 -0
  57. data/lib/spec_forge/forge/context.rb +41 -0
  58. data/lib/spec_forge/forge/display.rb +503 -0
  59. data/lib/spec_forge/forge/hooks.rb +131 -0
  60. data/lib/spec_forge/forge/runner/array_io.rb +81 -0
  61. data/lib/spec_forge/forge/runner/content_validator.rb +92 -0
  62. data/lib/spec_forge/forge/runner/header_validator.rb +66 -0
  63. data/lib/spec_forge/forge/runner/reporter.rb +56 -0
  64. data/lib/spec_forge/forge/runner/schema_validator.rb +113 -0
  65. data/lib/spec_forge/forge/runner.rb +118 -0
  66. data/lib/spec_forge/forge/timer.rb +94 -0
  67. data/lib/spec_forge/forge/variables.rb +38 -0
  68. data/lib/spec_forge/forge.rb +207 -133
  69. data/lib/spec_forge/http/backend.rb +49 -143
  70. data/lib/spec_forge/http/client.rb +14 -17
  71. data/lib/spec_forge/http/request.rb +37 -84
  72. data/lib/spec_forge/http/verb.rb +4 -0
  73. data/lib/spec_forge/http.rb +0 -5
  74. data/lib/spec_forge/loader/filter.rb +85 -0
  75. data/lib/spec_forge/loader/step_processor.rb +282 -0
  76. data/lib/spec_forge/loader.rb +105 -220
  77. data/lib/spec_forge/normalizer/default.rb +1 -1
  78. data/lib/spec_forge/normalizer/structure.rb +140 -0
  79. data/lib/spec_forge/normalizer/transformers.rb +168 -0
  80. data/lib/spec_forge/normalizer/validators.rb +50 -8
  81. data/lib/spec_forge/normalizer.rb +76 -119
  82. data/lib/spec_forge/normalizers/callback.yml +38 -0
  83. data/lib/spec_forge/normalizers/configuration.yml +59 -9
  84. data/lib/spec_forge/normalizers/factory.yml +53 -2
  85. data/lib/spec_forge/normalizers/factory_reference.yml +63 -2
  86. data/lib/spec_forge/normalizers/json_schema.yml +79 -0
  87. data/lib/spec_forge/normalizers/step.yml +506 -0
  88. data/lib/spec_forge/step/call.rb +36 -0
  89. data/lib/spec_forge/step/expect.rb +110 -0
  90. data/lib/spec_forge/step/source.rb +22 -0
  91. data/lib/spec_forge/step.rb +129 -0
  92. data/lib/spec_forge/type.rb +115 -66
  93. data/lib/spec_forge/version.rb +1 -1
  94. data/lib/spec_forge.rb +44 -106
  95. data/lib/templates/forge_helper.rb.tt +43 -22
  96. data/lib/templates/new_blueprint.yml.tt +54 -0
  97. metadata +75 -44
  98. data/lib/spec_forge/attribute/global.rb +0 -96
  99. data/lib/spec_forge/attribute/store.rb +0 -65
  100. data/lib/spec_forge/backtrace_formatter.rb +0 -50
  101. data/lib/spec_forge/callbacks.rb +0 -88
  102. data/lib/spec_forge/context/callbacks.rb +0 -91
  103. data/lib/spec_forge/context/global.rb +0 -72
  104. data/lib/spec_forge/context/store.rb +0 -131
  105. data/lib/spec_forge/context/variables.rb +0 -91
  106. data/lib/spec_forge/context.rb +0 -36
  107. data/lib/spec_forge/core_ext/rspec.rb +0 -55
  108. data/lib/spec_forge/core_ext.rb +0 -5
  109. data/lib/spec_forge/documentation/generators/base.rb +0 -81
  110. data/lib/spec_forge/documentation/generators/openapi/base.rb +0 -100
  111. data/lib/spec_forge/documentation/generators/openapi/v3_0.rb +0 -65
  112. data/lib/spec_forge/documentation/generators/openapi.rb +0 -59
  113. data/lib/spec_forge/documentation/generators.rb +0 -17
  114. data/lib/spec_forge/documentation/loader.rb +0 -159
  115. data/lib/spec_forge/documentation/openapi/base.rb +0 -33
  116. data/lib/spec_forge/filter.rb +0 -86
  117. data/lib/spec_forge/normalizer/definition.rb +0 -248
  118. data/lib/spec_forge/normalizers/_shared.yml +0 -74
  119. data/lib/spec_forge/normalizers/constraint.yml +0 -8
  120. data/lib/spec_forge/normalizers/expectation.yml +0 -47
  121. data/lib/spec_forge/normalizers/global_context.yml +0 -28
  122. data/lib/spec_forge/normalizers/spec.yml +0 -50
  123. data/lib/spec_forge/runner/adapter.rb +0 -183
  124. data/lib/spec_forge/runner/callbacks.rb +0 -246
  125. data/lib/spec_forge/runner/debug_proxy.rb +0 -213
  126. data/lib/spec_forge/runner/listener.rb +0 -54
  127. data/lib/spec_forge/runner/metadata.rb +0 -58
  128. data/lib/spec_forge/runner/state.rb +0 -98
  129. data/lib/spec_forge/runner.rb +0 -75
  130. data/lib/spec_forge/spec/expectation/constraint.rb +0 -127
  131. data/lib/spec_forge/spec/expectation.rb +0 -68
  132. data/lib/spec_forge/spec.rb +0 -68
  133. data/lib/templates/new_spec.yml.tt +0 -43
@@ -3,380 +3,128 @@
3
3
  module SpecForge
4
4
  module Documentation
5
5
  #
6
- # Transforms extracted test data into a structured document
6
+ # Builds API documentation by running blueprints and extracting endpoint data
7
7
  #
8
- # This class processes raw endpoint data from tests into a hierarchical document
9
- # structure suitable for rendering as API documentation.
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
- # @example Creating a document from test data
12
- # document = Builder.document_from_endpoints(endpoints)
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
- # Matches whole numbers with optional negative sign, used for type detection
32
- # when analyzing API response data.
24
+ # Creates a complete Document from blueprint files
33
25
  #
34
- # @api private
35
- #
36
- FLOAT_REGEX = /^-?\d+\.\d+$/
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
- # Creates a document from endpoint data
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
- # @param endpoints [Array<Hash>] Array of endpoint data extracted from tests
35
+ # @return [Document] A structured document containing all API endpoints
42
36
  #
43
- # @return [Document] A structured documentation document
37
+ # @raise [Error::NoBlueprintsError] If no blueprints are found
44
38
  #
45
- def self.document_from_endpoints(endpoints = [])
46
- new(endpoints).export_as_document
47
- end
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
- # @param endpoints [Array<Hash>] Array of endpoint data
47
+ # Creates a new Builder instance
155
48
  #
156
- # @return [Hash] Endpoints grouped by path and method
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
- # @private
54
+ # @return [Builder] A new builder instance
159
55
  #
160
- def group_endpoints(endpoints)
161
- grouped = Hash.new_nested_hash(depth: 1)
56
+ def initialize(base_path: nil, paths: nil, verbosity_level: 0, use_cache: false)
57
+ @cache = Cache.new
162
58
 
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
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
- # Sanitizes operations that represent error responses
66
+ # Extracts endpoint data from blueprint test executions
177
67
  #
178
- # Removes request details from operations with 4xx/5xx responses
179
- # to prevent invalid data from appearing in documentation.
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
- # @param operations [Array<Hash>] Array of operations
72
+ # @return [Array<Hash>] Array of endpoint data hashes containing
73
+ # request and response information
182
74
  #
183
- # @return [Array<Hash>] Sanitized operations
75
+ # @raise [Error::NoBlueprintsError] If no blueprints are found
184
76
  #
185
- # @private
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
- # 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
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
- description = operations.key_map(:expectation_name)
227
- .reject(&:blank?)
228
- .first
229
- &.split(" - ")
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
- parameters = normalize_parameters(operations)
233
- requests = normalize_requests(operations)
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
- id:,
238
- description:,
239
- parameters:,
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
- # 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 = {}
102
+ def register_callback
103
+ contexts = []
259
104
 
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:} }
105
+ SpecForge.configure do |config|
106
+ config.register_callback(:documentation_builder) do |context|
107
+ next if context.failure?
264
108
 
265
- parameters.merge!(params)
266
- end
109
+ step = context.step
110
+ next if step.nil? || step.documentation == false
111
+ next unless step.request?
267
112
 
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
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
- # 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
122
+ def run_blueprints(blueprints, **)
123
+ SpecForge::Forge.ignite.run(blueprints, **)
345
124
  end
346
125
 
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
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
- # description: "Creates a new user",
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, :description, :parameters, :requests, :responses)
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 description [String] Human-readable description
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:, description:, parameters:, requests:, responses:)
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
@@ -40,9 +40,3 @@ module SpecForge
40
40
  end
41
41
  end
42
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,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
@@ -2,8 +2,8 @@
2
2
 
3
3
  module SpecForge
4
4
  module Documentation
5
- module Generators
6
- module OpenAPI
5
+ module OpenAPI
6
+ class V30
7
7
  #
8
8
  # Formats OpenAPI validation errors into human-readable messages
9
9
  #
@@ -3,7 +3,7 @@
3
3
  module SpecForge
4
4
  module Documentation
5
5
  module OpenAPI
6
- module V3_0 # standard:disable Naming/ClassAndModuleCamelCase
6
+ class V30
7
7
  #
8
8
  # Represents an OpenAPI 3.0 Example object
9
9
  #
@@ -3,7 +3,7 @@
3
3
  module SpecForge
4
4
  module Documentation
5
5
  module OpenAPI
6
- module V3_0 # standard:disable Naming/ClassAndModuleCamelCase
6
+ class V30
7
7
  #
8
8
  # Represents an OpenAPI 3.0 Media Type object
9
9
  #
@@ -3,7 +3,7 @@
3
3
  module SpecForge
4
4
  module Documentation
5
5
  module OpenAPI
6
- module V3_0 # standard:disable Naming/ClassAndModuleCamelCase
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 < OpenAPI::Base
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
- }.merge_compact(
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.to_camelcase(:lower) + object_id.to_s
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.id.humanize
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
- document.description
83
+ ""
68
84
  end
69
85
 
70
86
  #