spec_forge 0.5.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 (116) hide show
  1. checksums.yaml +4 -4
  2. data/.standard.yml +3 -3
  3. data/CHANGELOG.md +217 -2
  4. data/README.md +162 -25
  5. data/flake.lock +3 -3
  6. data/flake.nix +11 -5
  7. data/lib/spec_forge/attribute/chainable.rb +208 -20
  8. data/lib/spec_forge/attribute/factory.rb +92 -15
  9. data/lib/spec_forge/attribute/faker.rb +62 -13
  10. data/lib/spec_forge/attribute/global.rb +96 -0
  11. data/lib/spec_forge/attribute/literal.rb +15 -2
  12. data/lib/spec_forge/attribute/matcher.rb +186 -11
  13. data/lib/spec_forge/attribute/parameterized.rb +45 -12
  14. data/lib/spec_forge/attribute/regex.rb +55 -5
  15. data/lib/spec_forge/attribute/resolvable.rb +48 -5
  16. data/lib/spec_forge/attribute/resolvable_array.rb +62 -4
  17. data/lib/spec_forge/attribute/resolvable_hash.rb +62 -4
  18. data/lib/spec_forge/attribute/store.rb +65 -0
  19. data/lib/spec_forge/attribute/transform.rb +33 -5
  20. data/lib/spec_forge/attribute/variable.rb +37 -6
  21. data/lib/spec_forge/attribute.rb +166 -66
  22. data/lib/spec_forge/backtrace_formatter.rb +26 -3
  23. data/lib/spec_forge/callbacks.rb +88 -0
  24. data/lib/spec_forge/cli/actions.rb +27 -0
  25. data/lib/spec_forge/cli/command.rb +78 -24
  26. data/lib/spec_forge/cli/docs/generate.rb +72 -0
  27. data/lib/spec_forge/cli/docs.rb +92 -0
  28. data/lib/spec_forge/cli/init.rb +51 -9
  29. data/lib/spec_forge/cli/new.rb +67 -6
  30. data/lib/spec_forge/cli/run.rb +32 -4
  31. data/lib/spec_forge/cli/serve.rb +155 -0
  32. data/lib/spec_forge/cli.rb +26 -7
  33. data/lib/spec_forge/configuration.rb +96 -24
  34. data/lib/spec_forge/context/callbacks.rb +91 -0
  35. data/lib/spec_forge/context/global.rb +72 -0
  36. data/lib/spec_forge/context/store.rb +131 -0
  37. data/lib/spec_forge/context/variables.rb +91 -0
  38. data/lib/spec_forge/context.rb +36 -0
  39. data/lib/spec_forge/core_ext/array.rb +27 -0
  40. data/lib/spec_forge/core_ext/rspec.rb +22 -4
  41. data/lib/spec_forge/documentation/builder.rb +383 -0
  42. data/lib/spec_forge/documentation/document/operation.rb +47 -0
  43. data/lib/spec_forge/documentation/document/parameter.rb +22 -0
  44. data/lib/spec_forge/documentation/document/request_body.rb +24 -0
  45. data/lib/spec_forge/documentation/document/response.rb +39 -0
  46. data/lib/spec_forge/documentation/document/response_body.rb +27 -0
  47. data/lib/spec_forge/documentation/document.rb +48 -0
  48. data/lib/spec_forge/documentation/generators/base.rb +81 -0
  49. data/lib/spec_forge/documentation/generators/openapi/base.rb +100 -0
  50. data/lib/spec_forge/documentation/generators/openapi/error_formatter.rb +149 -0
  51. data/lib/spec_forge/documentation/generators/openapi/v3_0.rb +65 -0
  52. data/lib/spec_forge/documentation/generators/openapi.rb +59 -0
  53. data/lib/spec_forge/documentation/generators.rb +17 -0
  54. data/lib/spec_forge/documentation/loader/cache.rb +138 -0
  55. data/lib/spec_forge/documentation/loader.rb +159 -0
  56. data/lib/spec_forge/documentation/openapi/base.rb +33 -0
  57. data/lib/spec_forge/documentation/openapi/v3_0/example.rb +44 -0
  58. data/lib/spec_forge/documentation/openapi/v3_0/media_type.rb +42 -0
  59. data/lib/spec_forge/documentation/openapi/v3_0/operation.rb +175 -0
  60. data/lib/spec_forge/documentation/openapi/v3_0/response.rb +65 -0
  61. data/lib/spec_forge/documentation/openapi/v3_0/schema.rb +80 -0
  62. data/lib/spec_forge/documentation/openapi/v3_0/tag.rb +71 -0
  63. data/lib/spec_forge/documentation/openapi.rb +23 -0
  64. data/lib/spec_forge/documentation.rb +27 -0
  65. data/lib/spec_forge/error.rb +284 -113
  66. data/lib/spec_forge/factory.rb +35 -16
  67. data/lib/spec_forge/filter.rb +86 -0
  68. data/lib/spec_forge/forge.rb +171 -0
  69. data/lib/spec_forge/http/backend.rb +101 -29
  70. data/lib/spec_forge/http/client.rb +23 -13
  71. data/lib/spec_forge/http/request.rb +85 -62
  72. data/lib/spec_forge/http/verb.rb +79 -0
  73. data/lib/spec_forge/http.rb +105 -0
  74. data/lib/spec_forge/loader.rb +244 -0
  75. data/lib/spec_forge/matchers.rb +130 -0
  76. data/lib/spec_forge/normalizer/default.rb +51 -0
  77. data/lib/spec_forge/normalizer/definition.rb +248 -0
  78. data/lib/spec_forge/normalizer/validators.rb +99 -0
  79. data/lib/spec_forge/normalizer.rb +486 -115
  80. data/lib/spec_forge/normalizers/_shared.yml +74 -0
  81. data/lib/spec_forge/normalizers/configuration.yml +23 -0
  82. data/lib/spec_forge/normalizers/constraint.yml +8 -0
  83. data/lib/spec_forge/normalizers/expectation.yml +47 -0
  84. data/lib/spec_forge/normalizers/factory.yml +12 -0
  85. data/lib/spec_forge/normalizers/factory_reference.yml +15 -0
  86. data/lib/spec_forge/normalizers/global_context.yml +28 -0
  87. data/lib/spec_forge/normalizers/spec.yml +50 -0
  88. data/lib/spec_forge/runner/adapter.rb +183 -0
  89. data/lib/spec_forge/runner/callbacks.rb +246 -0
  90. data/lib/spec_forge/runner/debug_proxy.rb +213 -0
  91. data/lib/spec_forge/runner/listener.rb +54 -0
  92. data/lib/spec_forge/runner/metadata.rb +58 -0
  93. data/lib/spec_forge/runner/state.rb +98 -0
  94. data/lib/spec_forge/runner.rb +50 -125
  95. data/lib/spec_forge/spec/expectation/constraint.rb +100 -21
  96. data/lib/spec_forge/spec/expectation.rb +47 -51
  97. data/lib/spec_forge/spec.rb +50 -108
  98. data/lib/spec_forge/type.rb +36 -4
  99. data/lib/spec_forge/version.rb +4 -1
  100. data/lib/spec_forge.rb +168 -76
  101. data/lib/templates/openapi.yml.tt +22 -0
  102. data/lib/templates/redoc.html.tt +28 -0
  103. data/lib/templates/swagger.html.tt +59 -0
  104. metadata +109 -16
  105. data/lib/spec_forge/normalizer/configuration.rb +0 -77
  106. data/lib/spec_forge/normalizer/constraint.rb +0 -47
  107. data/lib/spec_forge/normalizer/expectation.rb +0 -86
  108. data/lib/spec_forge/normalizer/factory.rb +0 -65
  109. data/lib/spec_forge/normalizer/factory_reference.rb +0 -71
  110. data/lib/spec_forge/normalizer/spec.rb +0 -74
  111. data/spec_forge/factories/user.yml +0 -4
  112. data/spec_forge/forge_helper.rb +0 -48
  113. data/spec_forge/specs/users.yml +0 -65
  114. /data/lib/templates/{forge_helper.tt → forge_helper.rb.tt} +0 -0
  115. /data/lib/templates/{new_factory.tt → new_factory.yml.tt} +0 -0
  116. /data/lib/templates/{new_spec.tt → new_spec.yml.tt} +0 -0
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SpecForge
4
+ module Documentation
5
+ class Loader
6
+ #
7
+ # Manages caching of test execution data for documentation generation
8
+ #
9
+ # Provides caching of endpoint data extracted from tests,
10
+ # checking file modification times to determine cache validity and
11
+ # avoiding unnecessary test re-execution when specs haven't changed.
12
+ #
13
+ # @example Using the cache
14
+ # cache = Cache.new
15
+ # if cache.valid?
16
+ # endpoints = cache.read
17
+ # else
18
+ # endpoints = run_tests_and_extract_data
19
+ # cache.create(endpoints)
20
+ # end
21
+ #
22
+ class Cache
23
+ #
24
+ # Creates a new cache manager
25
+ #
26
+ # Sets up file paths for endpoint and spec caches in the OpenAPI
27
+ # generated directory structure.
28
+ #
29
+ # @return [Cache] A new cache instance
30
+ #
31
+ def initialize
32
+ @endpoint_cache = SpecForge.openapi_path.join("generated", ".cache", "endpoints.yml")
33
+ @spec_cache = SpecForge.openapi_path.join("generated", ".cache", "specs.yml")
34
+ end
35
+
36
+ #
37
+ # Checks if the cache is valid and can be used
38
+ #
39
+ # Determines cache validity by checking if endpoint cache exists
40
+ # and whether any spec files have been modified since the cache
41
+ # was created.
42
+ #
43
+ # @return [Boolean] true if cache is valid and can be used
44
+ #
45
+ def valid?
46
+ endpoint_cache? && !specs_updated?
47
+ end
48
+
49
+ #
50
+ # Creates a cache entry with endpoint data and spec file metadata
51
+ #
52
+ # Writes both the endpoint data and current spec file modification times
53
+ # to enable cache invalidation when specs change.
54
+ #
55
+ # @param endpoints [Array<Hash>] Endpoint data to cache
56
+ #
57
+ # @return [void]
58
+ #
59
+ def create(endpoints)
60
+ write_spec_cache
61
+ write(endpoints)
62
+ end
63
+
64
+ #
65
+ # Writes endpoint data to the cache file
66
+ #
67
+ # @param endpoints [Array<Hash>] Endpoint data to write
68
+ #
69
+ # @return [void]
70
+ #
71
+ def write(endpoints)
72
+ write_to_file(endpoints, @endpoint_cache)
73
+ end
74
+
75
+ #
76
+ # Reads cached endpoint data from disk
77
+ #
78
+ # @return [Array<Hash>] Previously cached endpoint data
79
+ #
80
+ # @raise [StandardError] If cache file is missing or corrupted
81
+ #
82
+ def read
83
+ read_from_file(@endpoint_cache)
84
+ end
85
+
86
+ private
87
+
88
+ def write_to_file(data, path)
89
+ File.write(path, data.to_yaml(stringify_names: true))
90
+ end
91
+
92
+ def read_from_file(path)
93
+ YAML.safe_load_file(path, symbolize_names: true, permitted_classes: [Symbol, Time])
94
+ end
95
+
96
+ def specs_updated?
97
+ return true if !File.exist?(@spec_cache)
98
+
99
+ cache = read_from_file(@spec_cache)
100
+ new_cache = generate_spec_cache
101
+
102
+ different?(cache, new_cache)
103
+ end
104
+
105
+ def endpoint_cache?
106
+ File.exist?(@endpoint_cache)
107
+ end
108
+
109
+ def generate_spec_cache
110
+ paths = SpecForge.forge_path.join("specs", "**", "*.{yml,yaml}")
111
+
112
+ Dir[paths].each_with_object({}) do |path, hash|
113
+ hash[path.to_sym] = File.mtime(path)
114
+ end
115
+ end
116
+
117
+ def write_spec_cache
118
+ data = generate_spec_cache
119
+ write_to_file(data, @spec_cache)
120
+ end
121
+
122
+ def different?(cache_left, cache_right)
123
+ # The number of files changed
124
+ return true if cache_left.size != cache_right.size
125
+
126
+ default_time = Time.now
127
+
128
+ # Check if any of the files have changed since last time
129
+ cache_left.any? do |path, time_left|
130
+ time_right = cache_right[path] || default_time
131
+
132
+ time_left != time_right
133
+ end
134
+ end
135
+ end
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "loader/cache"
4
+
5
+ module SpecForge
6
+ module Documentation
7
+ #
8
+ # Extracts API documentation data from SpecForge tests
9
+ #
10
+ # This class runs all tests and captures successful test contexts
11
+ # to extract endpoint information, including request/response data.
12
+ #
13
+ # @example Extracting documentation data
14
+ # endpoints = Loader.extract_from_tests
15
+ #
16
+ class Loader
17
+ #
18
+ # Loads a documentation document with optional caching
19
+ #
20
+ # Extracts endpoint data from SpecForge tests, either from cache (if valid
21
+ # and requested) or by running fresh tests. Returns a structured document
22
+ # ready for generator consumption.
23
+ #
24
+ # @param use_cache [Boolean] Whether to use cached data if available
25
+ #
26
+ # @return [Document] Structured document containing endpoint data
27
+ #
28
+ def self.load_document(use_cache: false)
29
+ cache = Cache.new
30
+
31
+ endpoints =
32
+ if use_cache && cache.valid?
33
+ puts "Loading from cache..."
34
+ cache.read
35
+ else
36
+ puts "Cache invalid - Regenerating..." if use_cache
37
+
38
+ endpoints = extract_from_tests
39
+ cache.create(endpoints)
40
+
41
+ endpoints
42
+ end
43
+
44
+ Builder.document_from_endpoints(endpoints)
45
+ end
46
+
47
+ #
48
+ # Runs tests and extracts endpoint data
49
+ #
50
+ # @return [Array<Hash>] Extracted endpoint data from successful tests
51
+ #
52
+ def self.extract_from_tests
53
+ new
54
+ .run_tests
55
+ .extract_and_normalize_data
56
+ end
57
+
58
+ #
59
+ # Initializes a new loader
60
+ #
61
+ # Sets up a unique callback and prepares storage for successful test results
62
+ #
63
+ # @return [Loader] A new loader instance
64
+ #
65
+ def initialize
66
+ @callback_name = "__sf_docs_#{SpecForge.generate_id(self)}"
67
+ @successes = []
68
+ end
69
+
70
+ #
71
+ # Runs all tests and captures successful test results
72
+ #
73
+ # Registers a callback to capture test context and runs all tests
74
+ #
75
+ # @return [self] Returns self for method chaining
76
+ #
77
+ def run_tests
78
+ @successes.clear
79
+
80
+ Callbacks.register(@callback_name) do |context|
81
+ next if context.expectation.documentation == false || context.spec.documentation == false
82
+
83
+ @successes << context if context.example.execution_result.status == :passed
84
+ end
85
+
86
+ forges = prepare_forges
87
+
88
+ Runner.run(forges, exit_on_finish: false, exit_on_failure: true)
89
+
90
+ self
91
+ ensure
92
+ Callbacks.deregister(@callback_name)
93
+ end
94
+
95
+ #
96
+ # Prepares forge objects for test execution
97
+ #
98
+ # Adds the documentation callback to each forge
99
+ #
100
+ # @return [Array<Forge>] Array of prepared forge objects
101
+ #
102
+ def prepare_forges
103
+ forges = Runner.prepare
104
+
105
+ forges.each do |forge|
106
+ forge.global[:callbacks] << {after_each: @callback_name}
107
+ end
108
+
109
+ forges
110
+ end
111
+
112
+ #
113
+ # Extracts and normalizes endpoint data from test results
114
+ #
115
+ # @return [Array<Hash>] Normalized endpoint data
116
+ #
117
+ def extract_and_normalize_data
118
+ @successes.map { |d| extract_endpoint(d) }
119
+ end
120
+
121
+ private
122
+
123
+ def extract_endpoint(context)
124
+ request_hash = context.request.to_h
125
+ response_hash = context.response.to_hash
126
+
127
+ # Only pull the headers that the user explicitly checked for.
128
+ # This keeps the extra unrelated headers from being included
129
+ response_headers = context.expectation
130
+ .constraints
131
+ .headers
132
+ .keys
133
+ .map { |h| h.to_s.downcase }
134
+
135
+ response_headers = response_hash[:response_headers].slice(*response_headers)
136
+
137
+ {
138
+ # Metadata
139
+ spec_name: context.spec.name,
140
+ expectation_name: context.expectation.name,
141
+
142
+ # Request data
143
+ base_url: request_hash[:base_url],
144
+ url: request_hash[:url],
145
+ http_verb: request_hash[:http_verb],
146
+ content_type: request_hash[:content_type],
147
+ request_body: request_hash[:body],
148
+ request_headers: request_hash[:headers],
149
+ request_query: request_hash[:query],
150
+
151
+ # Response data
152
+ response_status: response_hash[:status],
153
+ response_body: response_hash[:body],
154
+ response_headers:
155
+ }
156
+ end
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SpecForge
4
+ module Documentation
5
+ module OpenAPI
6
+ #
7
+ # Base class for OpenAPI documentation objects
8
+ #
9
+ # Provides common functionality for OpenAPI specification objects
10
+ # like operations, responses, and schemas.
11
+ #
12
+ class Base
13
+ #
14
+ # The document object containing structured API data
15
+ #
16
+ # @return [Object] The document with endpoint information
17
+ #
18
+ attr_reader :document
19
+
20
+ #
21
+ # Creates a new OpenAPI base object
22
+ #
23
+ # @param document [Object] The document object containing API data
24
+ #
25
+ # @return [Base] A new base instance
26
+ #
27
+ def initialize(document)
28
+ @document = document
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SpecForge
4
+ module Documentation
5
+ module OpenAPI
6
+ module V3_0 # standard:disable Naming/ClassAndModuleCamelCase
7
+ #
8
+ # Represents an OpenAPI 3.0 Example object
9
+ #
10
+ # Creates example objects for request/response documentation with
11
+ # optional summary, description, and external reference support.
12
+ #
13
+ # @see https://spec.openapis.org/oas/v3.0.4.html#example-object
14
+ #
15
+ class Example < Data.define(:summary, :description, :value, :external_value)
16
+ #
17
+ # Creates a new OpenAPI example object
18
+ #
19
+ # @param summary [String, nil] Brief summary of the example's purpose
20
+ # @param description [String, nil] Detailed description of the example
21
+ # @param value [Object, nil] The actual example value
22
+ # @param external_value [String, nil] URL pointing to the example value
23
+ #
24
+ # @return [Example] A new example instance
25
+ #
26
+ def initialize(summary: nil, description: nil, value: nil, external_value: nil)
27
+ super
28
+ end
29
+
30
+ #
31
+ # Converts the example to an OpenAPI-compliant hash
32
+ #
33
+ # @return [Hash] OpenAPI-formatted example object
34
+ #
35
+ def to_h
36
+ super
37
+ .rename_key_unordered!(:external_value, :externalValue)
38
+ .compact_blank!
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SpecForge
4
+ module Documentation
5
+ module OpenAPI
6
+ module V3_0 # standard:disable Naming/ClassAndModuleCamelCase
7
+ #
8
+ # Represents an OpenAPI 3.0 Media Type object
9
+ #
10
+ # Handles media type definitions for request and response bodies,
11
+ # including schema definitions, examples, and encoding information.
12
+ #
13
+ # @see https://spec.openapis.org/oas/v3.0.4.html#media-type-object
14
+ #
15
+ class MediaType < Data.define(:schema, :example, :examples, :encoding)
16
+ #
17
+ # Creates a new OpenAPI media type object
18
+ #
19
+ # @param schema [Hash, nil] Schema definition for the media type
20
+ # @param example [Object, nil] Single example value
21
+ # @param examples [Hash, nil] Multiple named examples
22
+ # @param encoding [Hash, nil] Encoding information for the media type
23
+ #
24
+ # @return [MediaType] A new media type instance
25
+ #
26
+ def initialize(schema: nil, example: nil, examples: nil, encoding: nil)
27
+ super
28
+ end
29
+
30
+ #
31
+ # Converts the media type to an OpenAPI-compliant hash
32
+ #
33
+ # @return [Hash] OpenAPI-formatted media type object
34
+ #
35
+ def to_h
36
+ super.compact_blank!
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,175 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SpecForge
4
+ module Documentation
5
+ module OpenAPI
6
+ module V3_0 # standard:disable Naming/ClassAndModuleCamelCase
7
+ #
8
+ # Represents an OpenAPI 3.0 Operation object
9
+ #
10
+ # Handles the complete definition of API operations including parameters,
11
+ # request bodies, responses, and security requirements for OpenAPI specs.
12
+ #
13
+ # @see https://spec.openapis.org/oas/v3.0.4.html#operation-object
14
+ #
15
+ class Operation < OpenAPI::Base
16
+ #
17
+ # Converts the operation to an OpenAPI-compliant hash
18
+ #
19
+ # Builds the complete operation object with all required and optional
20
+ # fields properly formatted for OpenAPI specification.
21
+ #
22
+ # @return [Hash] OpenAPI-formatted operation object
23
+ #
24
+ def to_h
25
+ {
26
+ # Required
27
+ responses:,
28
+ security:
29
+ }.merge_compact(
30
+ # All optional
31
+ tags:,
32
+ summary:,
33
+ description:,
34
+ operationId:,
35
+ parameters:,
36
+ requestBody:
37
+ )
38
+ end
39
+
40
+ #
41
+ # Returns the operation's unique identifier
42
+ #
43
+ # @return [String] The operation ID
44
+ #
45
+ def id
46
+ # The object ID is added to make every ID unique
47
+ document.id.to_camelcase(:lower) + object_id.to_s
48
+ end
49
+
50
+ alias_method :operationId, :id
51
+
52
+ #
53
+ # Returns a human-readable summary of the operation
54
+ #
55
+ # @return [String, nil] Brief operation summary
56
+ #
57
+ def summary
58
+ document.id.humanize
59
+ end
60
+
61
+ #
62
+ # Returns detailed description of the operation
63
+ #
64
+ # @return [String] Detailed operation description
65
+ #
66
+ def description
67
+ document.description
68
+ end
69
+
70
+ #
71
+ # Returns security requirements for the operation
72
+ #
73
+ # @return [Array] Array of security requirement objects
74
+ #
75
+ def security
76
+ # User defined
77
+ []
78
+ end
79
+
80
+ #
81
+ # Returns tags for categorizing the operation
82
+ #
83
+ # @return [Array] Array of tag names
84
+ #
85
+ def tags
86
+ # User defined
87
+ []
88
+ end
89
+
90
+ #
91
+ # Returns parameter definitions for the operation
92
+ #
93
+ # Transforms document parameters into OpenAPI parameter objects
94
+ # with proper schema types and location information.
95
+ #
96
+ # @return [Array] Array of parameter objects
97
+ #
98
+ def parameters
99
+ document.parameters.values.map do |parameter|
100
+ schema = Schema.new(type: parameter.type).to_h
101
+
102
+ {
103
+ schema:,
104
+ name: parameter.name,
105
+ in: parameter.location,
106
+ required: parameter.location == "path" || false
107
+ }
108
+ end
109
+ end
110
+
111
+ #
112
+ # Returns request body definition for the operation
113
+ #
114
+ # Groups requests by content type and creates proper OpenAPI
115
+ # request body object with examples and schemas.
116
+ #
117
+ # @return [Hash, nil] Request body object
118
+ #
119
+ def request_body
120
+ requests = document.requests
121
+ return if requests.blank?
122
+
123
+ requests = requests.group_by(&:content_type)
124
+
125
+ content =
126
+ requests.transform_values do |grouped_requests|
127
+ media_type_from_requests(grouped_requests)
128
+ end
129
+
130
+ {
131
+ description: "",
132
+ content:
133
+ }
134
+ end
135
+
136
+ alias_method :requestBody, :request_body
137
+
138
+ #
139
+ # Returns response definitions for the operation
140
+ #
141
+ # Groups responses by status code and transforms them into
142
+ # OpenAPI response objects with proper formatting.
143
+ #
144
+ # @return [Hash] Hash mapping status codes to response objects
145
+ #
146
+ def responses
147
+ document.responses
148
+ .group_by(&:status)
149
+ .transform_values! do |responses|
150
+ response = responses.first
151
+ Response.new(response).to_h
152
+ end
153
+ end
154
+
155
+ private
156
+
157
+ def media_type_from_requests(requests)
158
+ request = requests.first
159
+ schema = Schema.new(type: request.type, content: request.content).to_h
160
+
161
+ examples =
162
+ requests.to_h do |request|
163
+ example_name = request.name.to_camelcase(:lower)
164
+ example = Example.new(summary: request.name, value: request.content).to_h
165
+
166
+ [example_name, example]
167
+ end
168
+
169
+ MediaType.new(schema:, examples:).to_h
170
+ end
171
+ end
172
+ end
173
+ end
174
+ end
175
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SpecForge
4
+ module Documentation
5
+ module OpenAPI
6
+ module V3_0 # standard:disable Naming/ClassAndModuleCamelCase
7
+ #
8
+ # Represents an OpenAPI 3.0 Response object
9
+ #
10
+ # Handles response definitions including status descriptions, content types,
11
+ # headers, and links for OpenAPI specifications.
12
+ #
13
+ # @see https://spec.openapis.org/oas/v3.0.4.html#response-object
14
+ #
15
+ class Response < OpenAPI::Base
16
+ #
17
+ # Converts the response to an OpenAPI-compliant hash
18
+ #
19
+ # Builds the complete response object with required description and
20
+ # optional content, headers, and links.
21
+ #
22
+ # @return [Hash] OpenAPI-formatted response object
23
+ #
24
+ def to_h
25
+ {
26
+ # Required
27
+ description: "",
28
+ content:
29
+ }.merge_compact(
30
+ # Optional
31
+ headers:
32
+ )
33
+ end
34
+
35
+ #
36
+ # Returns content definitions for the response
37
+ #
38
+ # Creates media type objects with schemas and merges with any
39
+ # documentation-provided content definitions.
40
+ #
41
+ # @return [Hash] Content definitions by media type
42
+ #
43
+ def content
44
+ schema = Schema.new(type: document.body.type).to_h
45
+
46
+ {
47
+ document.content_type => MediaType.new(schema:).to_h
48
+ }
49
+ end
50
+
51
+ #
52
+ # Returns header definitions for the response
53
+ #
54
+ # Merges document headers with documentation-provided headers.
55
+ #
56
+ # @return [Hash, nil] Header definitions
57
+ #
58
+ def headers
59
+ document.headers.presence
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end