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.
- checksums.yaml +4 -4
- data/.standard.yml +3 -3
- data/CHANGELOG.md +217 -2
- data/README.md +162 -25
- data/flake.lock +3 -3
- data/flake.nix +11 -5
- data/lib/spec_forge/attribute/chainable.rb +208 -20
- data/lib/spec_forge/attribute/factory.rb +92 -15
- data/lib/spec_forge/attribute/faker.rb +62 -13
- data/lib/spec_forge/attribute/global.rb +96 -0
- data/lib/spec_forge/attribute/literal.rb +15 -2
- data/lib/spec_forge/attribute/matcher.rb +186 -11
- data/lib/spec_forge/attribute/parameterized.rb +45 -12
- data/lib/spec_forge/attribute/regex.rb +55 -5
- data/lib/spec_forge/attribute/resolvable.rb +48 -5
- data/lib/spec_forge/attribute/resolvable_array.rb +62 -4
- data/lib/spec_forge/attribute/resolvable_hash.rb +62 -4
- data/lib/spec_forge/attribute/store.rb +65 -0
- data/lib/spec_forge/attribute/transform.rb +33 -5
- data/lib/spec_forge/attribute/variable.rb +37 -6
- data/lib/spec_forge/attribute.rb +166 -66
- data/lib/spec_forge/backtrace_formatter.rb +26 -3
- data/lib/spec_forge/callbacks.rb +88 -0
- data/lib/spec_forge/cli/actions.rb +27 -0
- data/lib/spec_forge/cli/command.rb +78 -24
- data/lib/spec_forge/cli/docs/generate.rb +72 -0
- data/lib/spec_forge/cli/docs.rb +92 -0
- data/lib/spec_forge/cli/init.rb +51 -9
- data/lib/spec_forge/cli/new.rb +67 -6
- data/lib/spec_forge/cli/run.rb +32 -4
- data/lib/spec_forge/cli/serve.rb +155 -0
- data/lib/spec_forge/cli.rb +26 -7
- data/lib/spec_forge/configuration.rb +96 -24
- data/lib/spec_forge/context/callbacks.rb +91 -0
- data/lib/spec_forge/context/global.rb +72 -0
- data/lib/spec_forge/context/store.rb +131 -0
- data/lib/spec_forge/context/variables.rb +91 -0
- data/lib/spec_forge/context.rb +36 -0
- data/lib/spec_forge/core_ext/array.rb +27 -0
- data/lib/spec_forge/core_ext/rspec.rb +22 -4
- data/lib/spec_forge/documentation/builder.rb +383 -0
- data/lib/spec_forge/documentation/document/operation.rb +47 -0
- data/lib/spec_forge/documentation/document/parameter.rb +22 -0
- data/lib/spec_forge/documentation/document/request_body.rb +24 -0
- data/lib/spec_forge/documentation/document/response.rb +39 -0
- data/lib/spec_forge/documentation/document/response_body.rb +27 -0
- data/lib/spec_forge/documentation/document.rb +48 -0
- data/lib/spec_forge/documentation/generators/base.rb +81 -0
- data/lib/spec_forge/documentation/generators/openapi/base.rb +100 -0
- data/lib/spec_forge/documentation/generators/openapi/error_formatter.rb +149 -0
- data/lib/spec_forge/documentation/generators/openapi/v3_0.rb +65 -0
- data/lib/spec_forge/documentation/generators/openapi.rb +59 -0
- data/lib/spec_forge/documentation/generators.rb +17 -0
- data/lib/spec_forge/documentation/loader/cache.rb +138 -0
- data/lib/spec_forge/documentation/loader.rb +159 -0
- data/lib/spec_forge/documentation/openapi/base.rb +33 -0
- data/lib/spec_forge/documentation/openapi/v3_0/example.rb +44 -0
- data/lib/spec_forge/documentation/openapi/v3_0/media_type.rb +42 -0
- data/lib/spec_forge/documentation/openapi/v3_0/operation.rb +175 -0
- data/lib/spec_forge/documentation/openapi/v3_0/response.rb +65 -0
- data/lib/spec_forge/documentation/openapi/v3_0/schema.rb +80 -0
- data/lib/spec_forge/documentation/openapi/v3_0/tag.rb +71 -0
- data/lib/spec_forge/documentation/openapi.rb +23 -0
- data/lib/spec_forge/documentation.rb +27 -0
- data/lib/spec_forge/error.rb +284 -113
- data/lib/spec_forge/factory.rb +35 -16
- data/lib/spec_forge/filter.rb +86 -0
- data/lib/spec_forge/forge.rb +171 -0
- data/lib/spec_forge/http/backend.rb +101 -29
- data/lib/spec_forge/http/client.rb +23 -13
- data/lib/spec_forge/http/request.rb +85 -62
- data/lib/spec_forge/http/verb.rb +79 -0
- data/lib/spec_forge/http.rb +105 -0
- data/lib/spec_forge/loader.rb +244 -0
- data/lib/spec_forge/matchers.rb +130 -0
- data/lib/spec_forge/normalizer/default.rb +51 -0
- data/lib/spec_forge/normalizer/definition.rb +248 -0
- data/lib/spec_forge/normalizer/validators.rb +99 -0
- data/lib/spec_forge/normalizer.rb +486 -115
- data/lib/spec_forge/normalizers/_shared.yml +74 -0
- data/lib/spec_forge/normalizers/configuration.yml +23 -0
- data/lib/spec_forge/normalizers/constraint.yml +8 -0
- data/lib/spec_forge/normalizers/expectation.yml +47 -0
- data/lib/spec_forge/normalizers/factory.yml +12 -0
- data/lib/spec_forge/normalizers/factory_reference.yml +15 -0
- data/lib/spec_forge/normalizers/global_context.yml +28 -0
- data/lib/spec_forge/normalizers/spec.yml +50 -0
- data/lib/spec_forge/runner/adapter.rb +183 -0
- data/lib/spec_forge/runner/callbacks.rb +246 -0
- data/lib/spec_forge/runner/debug_proxy.rb +213 -0
- data/lib/spec_forge/runner/listener.rb +54 -0
- data/lib/spec_forge/runner/metadata.rb +58 -0
- data/lib/spec_forge/runner/state.rb +98 -0
- data/lib/spec_forge/runner.rb +50 -125
- data/lib/spec_forge/spec/expectation/constraint.rb +100 -21
- data/lib/spec_forge/spec/expectation.rb +47 -51
- data/lib/spec_forge/spec.rb +50 -108
- data/lib/spec_forge/type.rb +36 -4
- data/lib/spec_forge/version.rb +4 -1
- data/lib/spec_forge.rb +168 -76
- data/lib/templates/openapi.yml.tt +22 -0
- data/lib/templates/redoc.html.tt +28 -0
- data/lib/templates/swagger.html.tt +59 -0
- metadata +109 -16
- data/lib/spec_forge/normalizer/configuration.rb +0 -77
- data/lib/spec_forge/normalizer/constraint.rb +0 -47
- data/lib/spec_forge/normalizer/expectation.rb +0 -86
- data/lib/spec_forge/normalizer/factory.rb +0 -65
- data/lib/spec_forge/normalizer/factory_reference.rb +0 -71
- data/lib/spec_forge/normalizer/spec.rb +0 -74
- data/spec_forge/factories/user.yml +0 -4
- data/spec_forge/forge_helper.rb +0 -48
- data/spec_forge/specs/users.yml +0 -65
- /data/lib/templates/{forge_helper.tt → forge_helper.rb.tt} +0 -0
- /data/lib/templates/{new_factory.tt → new_factory.yml.tt} +0 -0
- /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
|