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,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
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SpecForge
4
+ module Documentation
5
+ module Generators
6
+ module OpenAPI
7
+ #
8
+ # Base class for OpenAPI generators
9
+ #
10
+ # Provides common functionality for OpenAPI generators of different versions.
11
+ #
12
+ class Base < Generators::Base
13
+ #
14
+ # Converts the generator's version to a semantic version object
15
+ #
16
+ # @return [SemVersion] The semantic version
17
+ #
18
+ def self.to_sem_version
19
+ SemVersion.new(CURRENT_VERSION)
20
+ end
21
+
22
+ #
23
+ # Generates OpenAPI documentation from test data with optional caching
24
+ #
25
+ # Loads endpoint data from tests (either fresh or cached), creates a document,
26
+ # and generates the OpenAPI specification using the appropriate version generator.
27
+ #
28
+ # @param use_cache [Boolean] Whether to use cached test data if available
29
+ #
30
+ # @return [Hash] The generated OpenAPI specification
31
+ #
32
+ def self.generate(use_cache: false)
33
+ document = Documentation::Loader.load_document(use_cache:)
34
+ new(document).generate
35
+ end
36
+
37
+ #
38
+ # Validates an OpenAPI specification against the standard
39
+ #
40
+ # Uses the openapi3_parser gem to validate the generated specification
41
+ # and provides detailed error reporting if validation fails.
42
+ #
43
+ # @param output [Hash] The OpenAPI specification to validate
44
+ #
45
+ # @return [void]
46
+ #
47
+ # @raise [Error::InvalidOASDocument] If the specification is invalid
48
+ #
49
+ def self.validate!(output)
50
+ document = Openapi3Parser.load(output)
51
+ if document.valid?
52
+ puts "✅ No validation errors found!"
53
+ return
54
+ end
55
+
56
+ puts ErrorFormatter.format(document.errors.errors)
57
+ raise Error::InvalidOASDocument
58
+ end
59
+
60
+ protected
61
+
62
+ #
63
+ # Loads OpenAPI configuration from YAML
64
+ #
65
+ # @return [Hash] The normalized OpenAPI configuration
66
+ #
67
+ # @api private
68
+ #
69
+ def config
70
+ @config ||= begin
71
+ file_extension_glob = "*.{yml,yaml}"
72
+ base_path = SpecForge.openapi_path.join("config")
73
+
74
+ root_paths = base_path.join(file_extension_glob)
75
+ path_paths = base_path.join("paths", "**", file_extension_glob)
76
+ component_paths = base_path.join("components", "**", file_extension_glob)
77
+
78
+ config = load_yml_from_paths(root_paths).to_merged_h
79
+ paths_config = load_yml_from_paths(path_paths).to_merged_h
80
+ component_config = load_yml_from_paths(component_paths).to_merged_h
81
+
82
+ (config["paths"] ||= {}).deep_merge!(paths_config)
83
+ (config["components"] ||= {}).deep_merge!(component_config)
84
+
85
+ config
86
+ end
87
+ end
88
+
89
+ private
90
+
91
+ def load_yml_from_paths(paths)
92
+ Dir[paths].map do |path|
93
+ YAML.safe_load_file(path)
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SpecForge
4
+ module Documentation
5
+ module Generators
6
+ module OpenAPI
7
+ #
8
+ # Formats OpenAPI validation errors into human-readable messages
9
+ #
10
+ # Takes validation errors from OpenAPI parsers and transforms them into
11
+ # structured, easy-to-understand error messages with context information
12
+ # and suggestions for resolution.
13
+ #
14
+ # @example Formatting validation errors
15
+ # errors = openapi_parser.errors
16
+ # formatted = ErrorFormatter.format(errors)
17
+ # puts formatted
18
+ #
19
+ class ErrorFormatter
20
+ #
21
+ # Regular expression for matching path-related validation errors
22
+ #
23
+ # Captures path, HTTP method, and response code from OpenAPI error contexts
24
+ # to provide meaningful location information in error messages.
25
+ #
26
+ # @api private
27
+ #
28
+ PATHS_REGEX = %r{#/paths/(.+?)/(get|post|put|patch|delete|head|options)/responses/(.+)}i
29
+
30
+ #
31
+ # Regular expression for matching schema-related validation errors
32
+ #
33
+ # Captures schema name and field path from OpenAPI error contexts
34
+ # to identify specific schema validation failures.
35
+ #
36
+ # @api private
37
+ #
38
+ SCHEMA_REGEX = %r{#/components/schemas/(.+?)/(.+)}i
39
+
40
+ #
41
+ # Formats an array of validation errors into a readable string
42
+ #
43
+ # @param errors [Array] Array of validation error objects
44
+ #
45
+ # @return [String, nil] Formatted error message or nil if no errors
46
+ #
47
+ def self.format(errors)
48
+ new(errors).format
49
+ end
50
+
51
+ #
52
+ # Creates a new error formatter
53
+ #
54
+ # @param errors [Array] Array of validation error objects to format
55
+ #
56
+ # @return [ErrorFormatter] A new formatter instance
57
+ #
58
+ def initialize(errors)
59
+ @errors = errors
60
+ end
61
+
62
+ #
63
+ # Formats the errors into a structured, readable message
64
+ #
65
+ # Groups errors by type (unexpected vs validation), formats each error
66
+ # with context and location information, and returns a comprehensive
67
+ # error report with resolution guidance.
68
+ #
69
+ # @return [String, nil] Formatted error message or nil if no errors
70
+ #
71
+ def format
72
+ return if @errors.blank?
73
+
74
+ unexpected_errors, errors = @errors.partition { |e| e.message.include?("Unexpected") }
75
+
76
+ unexpected_errors = format_errors(unexpected_errors)
77
+ errors = format_errors(errors, start_index: unexpected_errors.size)
78
+
79
+ if unexpected_errors.size > 0
80
+ unexpected_message = <<~STRING
81
+
82
+ Field errors (resolve these first):
83
+
84
+ #{unexpected_errors.join("\n\n")}
85
+
86
+ -------
87
+
88
+ Other validation errors:
89
+ STRING
90
+ end
91
+
92
+ <<~STRING
93
+ ========================================
94
+ 🚨 Validation Errors
95
+ ========================================
96
+ #{unexpected_message}
97
+ #{errors.join("\n\n")}
98
+
99
+ Total errors: #{errors.size}
100
+ STRING
101
+ end
102
+
103
+ private
104
+
105
+ def format_errors(errors, start_index: 0)
106
+ errors.map.with_index do |error, index|
107
+ format_single_error(error, start_index + index + 1)
108
+ end
109
+ end
110
+
111
+ def format_single_error(error, number)
112
+ context_path = simplify_context_path(error.context.to_s)
113
+ error_message =
114
+ <<~STRING
115
+ Error ##{number}:
116
+ Message: #{error.message}
117
+ Location: #{context_path}
118
+ STRING
119
+
120
+ error_message += " Type: #{error.for_type}" if error.for_type
121
+ error_message
122
+ end
123
+
124
+ def simplify_context_path(context_path)
125
+ # Clean up the basic encoding mess first
126
+ path = context_path.gsub(/.*source_location: /, "")
127
+ .gsub("%7B", "{")
128
+ .gsub("%7D", "}")
129
+ .gsub("~1", "/")
130
+ .gsub("~0", "~")
131
+ .gsub("%24", "$")
132
+
133
+ # Now try to make it human-readable
134
+ if (match = path.match(PATHS_REGEX))
135
+ full_path, method, rest = match.captures
136
+
137
+ "#{method.upcase} #{full_path} → responses/#{rest}"
138
+ elsif (match = path.match(SCHEMA_REGEX))
139
+ schema, rest = match.captures
140
+ "Schemas → #{schema} → #{rest}"
141
+ else
142
+ path
143
+ end
144
+ end
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SpecForge
4
+ module Documentation
5
+ module Generators
6
+ module OpenAPI
7
+ # https://spec.openapis.org/oas/v3.0.4.html
8
+ class V3_0 < Base # standard:disable Naming/ClassAndModuleCamelCase
9
+ #
10
+ # Current OpenAPI 3.0 version supported by this generator
11
+ #
12
+ # @api private
13
+ #
14
+ CURRENT_VERSION = "3.0.4"
15
+
16
+ #
17
+ # Alias for OpenAPI V3.0 classes for cleaner code
18
+ #
19
+ # @api private
20
+ #
21
+ OAS = Documentation::OpenAPI::V3_0
22
+
23
+ #
24
+ # Generates an OpenAPI 3.0 specification from the input document
25
+ #
26
+ # Creates a complete OpenAPI specification by combining the document's
27
+ # endpoint data with configuration files and ensuring compliance with
28
+ # OpenAPI 3.0.4 standards.
29
+ #
30
+ # @return [Hash] Complete OpenAPI 3.0 specification
31
+ #
32
+ def generate
33
+ output = {
34
+ openapi: CURRENT_VERSION,
35
+ paths:
36
+ }
37
+
38
+ output.deep_stringify_keys!
39
+ output.deep_merge!(config)
40
+
41
+ output
42
+ end
43
+
44
+ #
45
+ # Transforms document endpoints into OpenAPI paths structure
46
+ #
47
+ # Converts the internal endpoint representation into the OpenAPI paths
48
+ # format, with each path containing operations organized by HTTP method.
49
+ #
50
+ # @return [Hash] OpenAPI paths object with operations
51
+ #
52
+ def paths
53
+ paths = input.endpoints.deep_dup
54
+
55
+ paths.each do |path, operations|
56
+ operations.transform_values! do |document|
57
+ OAS::Operation.new(document).to_h
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "openapi/base"
4
+ require_relative "openapi/error_formatter"
5
+ require_relative "openapi/v3_0"
6
+
7
+ module SpecForge
8
+ module Documentation
9
+ module Generators
10
+ #
11
+ # Namespace for OpenAPI generators
12
+ #
13
+ # Contains version-specific OpenAPI generators and helper methods
14
+ # for selecting the appropriate generator.
15
+ #
16
+ module OpenAPI
17
+ #
18
+ # Current OpenAPI version used as default
19
+ #
20
+ # Points to the latest supported OpenAPI version for new specifications.
21
+ #
22
+ # @api private
23
+ #
24
+ CURRENT_VERSION = V3_0::CURRENT_VERSION
25
+
26
+ #
27
+ # Mapping of OpenAPI versions to their generator classes
28
+ #
29
+ # Used for version selection when generating OpenAPI documentation.
30
+ # Keys are SemVersion objects, values are generator classes.
31
+ #
32
+ # @api private
33
+ #
34
+ VERSIONS = {
35
+ V3_0.to_sem_version => V3_0
36
+ }.freeze
37
+
38
+ #
39
+ # Selects an OpenAPI generator by version
40
+ #
41
+ # @param version [String] OpenAPI version (e.g., "3.0")
42
+ #
43
+ # @return [Class] The appropriate generator class
44
+ # @raise [ArgumentError] If the version is not supported
45
+ #
46
+ def self.[](version)
47
+ version = SemVersion.from_loose_version(version)
48
+ generator = VERSIONS.value_where { |k, _v| k.satisfies?("~> #{version}") }
49
+
50
+ if generator.nil?
51
+ raise ArgumentError, "Invalid OpenAPI version provided: #{version.to_s.in_quotes}"
52
+ end
53
+
54
+ generator
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SpecForge
4
+ module Documentation
5
+ #
6
+ # Documentation rendering functionality
7
+ #
8
+ # Contains generator classes for transforming SpecForge documents
9
+ # into various output formats like OpenAPI specifications.
10
+ #
11
+ module Generators
12
+ end
13
+ end
14
+ end
15
+
16
+ require_relative "generators/base"
17
+ require_relative "generators/openapi"