rails-openapi-gen 0.0.1
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 +7 -0
- data/CLAUDE.md +160 -0
- data/README.md +164 -0
- data/lib/rails_openapi_gen/configuration.rb +157 -0
- data/lib/rails_openapi_gen/engine.rb +11 -0
- data/lib/rails_openapi_gen/generators/yaml_generator.rb +302 -0
- data/lib/rails_openapi_gen/importer.rb +647 -0
- data/lib/rails_openapi_gen/parsers/comment_parser.rb +40 -0
- data/lib/rails_openapi_gen/parsers/comment_parsers/attribute_parser.rb +57 -0
- data/lib/rails_openapi_gen/parsers/comment_parsers/base_attribute_parser.rb +42 -0
- data/lib/rails_openapi_gen/parsers/comment_parsers/body_parser.rb +62 -0
- data/lib/rails_openapi_gen/parsers/comment_parsers/conditional_parser.rb +13 -0
- data/lib/rails_openapi_gen/parsers/comment_parsers/operation_parser.rb +50 -0
- data/lib/rails_openapi_gen/parsers/comment_parsers/param_parser.rb +62 -0
- data/lib/rails_openapi_gen/parsers/comment_parsers/query_parser.rb +62 -0
- data/lib/rails_openapi_gen/parsers/controller_parser.rb +153 -0
- data/lib/rails_openapi_gen/parsers/jbuilder_parser.rb +529 -0
- data/lib/rails_openapi_gen/parsers/routes_parser.rb +33 -0
- data/lib/rails_openapi_gen/parsers/template_processors/jbuilder_template_processor.rb +147 -0
- data/lib/rails_openapi_gen/parsers/template_processors/response_template_processor.rb +17 -0
- data/lib/rails_openapi_gen/railtie.rb +11 -0
- data/lib/rails_openapi_gen/tasks/openapi.rake +30 -0
- data/lib/rails_openapi_gen/version.rb +5 -0
- data/lib/rails_openapi_gen.rb +267 -0
- data/lib/tasks/openapi_import.rake +126 -0
- data/rails-openapi-gen.gemspec +30 -0
- metadata +155 -0
@@ -0,0 +1,302 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "yaml"
|
4
|
+
require "fileutils"
|
5
|
+
|
6
|
+
module RailsOpenapiGen
|
7
|
+
module Generators
|
8
|
+
class YamlGenerator
|
9
|
+
attr_reader :schemas
|
10
|
+
|
11
|
+
# Initializes YAML generator with schemas data
|
12
|
+
# @param schemas [Hash] Hash of route information and schemas
|
13
|
+
def initialize(schemas)
|
14
|
+
@schemas = schemas
|
15
|
+
@config = RailsOpenapiGen.configuration
|
16
|
+
@base_path = @config.output_directory
|
17
|
+
end
|
18
|
+
|
19
|
+
# Generates OpenAPI YAML files from schemas
|
20
|
+
# @return [void]
|
21
|
+
def generate
|
22
|
+
setup_directories
|
23
|
+
|
24
|
+
paths_data = {}
|
25
|
+
|
26
|
+
@schemas.each do |route, data|
|
27
|
+
next if data.nil? || data[:schema].nil? || should_skip_schema?(data[:schema])
|
28
|
+
|
29
|
+
path_key = normalize_path(route[:path])
|
30
|
+
method = route[:method].downcase
|
31
|
+
|
32
|
+
paths_data[path_key] ||= {}
|
33
|
+
paths_data[path_key][method] = build_operation(route, data[:schema], data[:parameters], data[:operation])
|
34
|
+
end
|
35
|
+
|
36
|
+
if @config.split_files?
|
37
|
+
write_paths_files(paths_data)
|
38
|
+
write_main_openapi_file
|
39
|
+
else
|
40
|
+
write_single_file(paths_data)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
# Checks if a schema should be skipped based on validity
|
47
|
+
# @param schema [Hash] Schema to check
|
48
|
+
# @return [Boolean] True if schema should be skipped
|
49
|
+
def should_skip_schema?(schema)
|
50
|
+
return true if schema.nil?
|
51
|
+
|
52
|
+
# For object schemas, check if properties exist and are not empty
|
53
|
+
if schema["type"] == "object"
|
54
|
+
return schema["properties"].nil? || schema["properties"].empty?
|
55
|
+
end
|
56
|
+
|
57
|
+
# For array schemas, check if items are properly defined
|
58
|
+
if schema["type"] == "array"
|
59
|
+
return schema["items"].nil?
|
60
|
+
end
|
61
|
+
|
62
|
+
# For other types, don't skip
|
63
|
+
false
|
64
|
+
end
|
65
|
+
|
66
|
+
# Creates necessary output directories
|
67
|
+
# @return [void]
|
68
|
+
def setup_directories
|
69
|
+
FileUtils.mkdir_p(@base_path)
|
70
|
+
if @config.split_files?
|
71
|
+
FileUtils.mkdir_p(File.join(@base_path, "paths"))
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# Converts Rails path format to OpenAPI format
|
76
|
+
# @param path [String] Rails path (e.g., "/users/:id")
|
77
|
+
# @return [String] OpenAPI path (e.g., "/users/{id}")
|
78
|
+
def normalize_path(path)
|
79
|
+
path.gsub(/:(\w+)/, '{\\1}')
|
80
|
+
end
|
81
|
+
|
82
|
+
# Builds OpenAPI operation object for a route
|
83
|
+
# @param route [Hash] Route information
|
84
|
+
# @param schema [Hash] Response schema
|
85
|
+
# @param parameters [Hash] Request parameters
|
86
|
+
# @param operation_info [Hash, nil] Operation metadata from comments
|
87
|
+
# @return [Hash] OpenAPI operation object
|
88
|
+
def build_operation(route, schema, parameters = {}, operation_info = nil)
|
89
|
+
operation = {
|
90
|
+
"summary" => operation_info&.dig(:summary) || "#{humanize(route[:action])} #{humanize(singularize(route[:controller]))}",
|
91
|
+
"operationId" => operation_info&.dig(:operationId) || "#{route[:controller].gsub('/', '_')}_#{route[:action]}",
|
92
|
+
"tags" => operation_info&.dig(:tags) || [humanize(route[:controller].split('/').first)]
|
93
|
+
}
|
94
|
+
|
95
|
+
# Add description if provided
|
96
|
+
operation["description"] = operation_info[:description] if operation_info&.dig(:description)
|
97
|
+
|
98
|
+
# Add parameters if they exist
|
99
|
+
openapi_parameters = build_parameters(route, parameters)
|
100
|
+
operation["parameters"] = openapi_parameters unless openapi_parameters.empty?
|
101
|
+
|
102
|
+
# Add request body if body parameters exist
|
103
|
+
request_body = build_request_body(parameters)
|
104
|
+
operation["requestBody"] = request_body if request_body
|
105
|
+
|
106
|
+
# Add responses with configurable status code
|
107
|
+
status_code = operation_info&.dig(:status) || "200"
|
108
|
+
response_description = "Successful response"
|
109
|
+
|
110
|
+
operation["responses"] = {
|
111
|
+
status_code => {
|
112
|
+
"description" => response_description,
|
113
|
+
"content" => {
|
114
|
+
"application/json" => {
|
115
|
+
"schema" => schema
|
116
|
+
}
|
117
|
+
}
|
118
|
+
}
|
119
|
+
}
|
120
|
+
|
121
|
+
operation
|
122
|
+
end
|
123
|
+
|
124
|
+
# Writes path data to separate YAML files grouped by resource
|
125
|
+
# @param paths_data [Hash] OpenAPI paths data
|
126
|
+
# @return [void]
|
127
|
+
def write_paths_files(paths_data)
|
128
|
+
grouped_paths = paths_data.group_by { |path, _| extract_resource_name(path) }
|
129
|
+
|
130
|
+
grouped_paths.each do |resource, paths|
|
131
|
+
file_data = {}
|
132
|
+
paths.each { |path, operations| file_data[path] = operations }
|
133
|
+
|
134
|
+
file_path = File.join(@base_path, "paths", "#{resource}.yaml")
|
135
|
+
File.write(file_path, file_data.to_yaml)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
# Writes main OpenAPI specification file
|
140
|
+
# @return [void]
|
141
|
+
def write_main_openapi_file
|
142
|
+
config = RailsOpenapiGen.configuration
|
143
|
+
|
144
|
+
openapi_data = {
|
145
|
+
"openapi" => config.openapi_version,
|
146
|
+
"info" => deep_stringify_keys(config.info),
|
147
|
+
"servers" => config.servers.map { |server| deep_stringify_keys(server) },
|
148
|
+
"paths" => {}
|
149
|
+
}
|
150
|
+
|
151
|
+
Dir[File.join(@base_path, "paths", "*.yaml")].each do |path_file|
|
152
|
+
paths = YAML.load_file(path_file)
|
153
|
+
paths.each do |path, operations|
|
154
|
+
openapi_data["paths"][path] = operations
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
File.write(File.join(@base_path, @config.output_filename), openapi_data.to_yaml)
|
159
|
+
end
|
160
|
+
|
161
|
+
# Writes all OpenAPI data to a single file
|
162
|
+
# @param paths_data [Hash] OpenAPI paths data
|
163
|
+
# @return [void]
|
164
|
+
def write_single_file(paths_data)
|
165
|
+
config = RailsOpenapiGen.configuration
|
166
|
+
|
167
|
+
openapi_data = {
|
168
|
+
"openapi" => config.openapi_version,
|
169
|
+
"info" => deep_stringify_keys(config.info),
|
170
|
+
"servers" => config.servers.map { |server| deep_stringify_keys(server) },
|
171
|
+
"paths" => paths_data
|
172
|
+
}
|
173
|
+
|
174
|
+
File.write(File.join(@base_path, @config.output_filename), openapi_data.to_yaml)
|
175
|
+
end
|
176
|
+
|
177
|
+
# Recursively converts hash keys to strings
|
178
|
+
# @param obj [Object] Object to process
|
179
|
+
# @return [Object] Object with stringified keys
|
180
|
+
def deep_stringify_keys(obj)
|
181
|
+
case obj
|
182
|
+
when Hash
|
183
|
+
obj.transform_keys(&:to_s).transform_values { |v| deep_stringify_keys(v) }
|
184
|
+
when Array
|
185
|
+
obj.map { |v| deep_stringify_keys(v) }
|
186
|
+
else
|
187
|
+
obj
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
# Extracts resource name from API path
|
192
|
+
# @param path [String] API path
|
193
|
+
# @return [String] Resource name
|
194
|
+
def extract_resource_name(path)
|
195
|
+
parts = path.split('/')
|
196
|
+
return "root" if parts.empty? || parts.all?(&:empty?)
|
197
|
+
|
198
|
+
parts.reject { |p| p.empty? || p.start_with?('{') }.first || "root"
|
199
|
+
end
|
200
|
+
|
201
|
+
# Converts snake_case string to human readable format
|
202
|
+
# @param string [String] String to humanize
|
203
|
+
# @return [String] Humanized string
|
204
|
+
def humanize(string)
|
205
|
+
string.to_s.gsub('_', ' ').split.map(&:capitalize).join(' ')
|
206
|
+
end
|
207
|
+
|
208
|
+
# Simple singularization of a string
|
209
|
+
# @param string [String] String to singularize
|
210
|
+
# @return [String] Singularized string
|
211
|
+
def singularize(string)
|
212
|
+
# Simple singularization - remove trailing 's' if present
|
213
|
+
str = string.to_s
|
214
|
+
str.end_with?('s') ? str[0..-2] : str
|
215
|
+
end
|
216
|
+
|
217
|
+
# Builds OpenAPI parameter objects from route and parameter data
|
218
|
+
# @param route [Hash] Route information
|
219
|
+
# @param parameters [Hash] Parameter definitions
|
220
|
+
# @return [Array<Hash>] Array of OpenAPI parameter objects
|
221
|
+
def build_parameters(route, parameters)
|
222
|
+
openapi_params = []
|
223
|
+
|
224
|
+
# Add path parameters from route
|
225
|
+
path_vars = route[:path].scan(/:(\w+)/).flatten
|
226
|
+
path_vars.each do |path_var|
|
227
|
+
# Look for matching parameter definition
|
228
|
+
path_param = parameters[:path_parameters]&.find { |p| p[:name] == path_var }
|
229
|
+
|
230
|
+
param = {
|
231
|
+
"name" => path_var,
|
232
|
+
"in" => "path",
|
233
|
+
"required" => true,
|
234
|
+
"schema" => {
|
235
|
+
"type" => path_param&.dig(:type) || "string"
|
236
|
+
}
|
237
|
+
}
|
238
|
+
param["description"] = path_param[:description] if path_param&.dig(:description)
|
239
|
+
openapi_params << param
|
240
|
+
end
|
241
|
+
|
242
|
+
# Add query parameters
|
243
|
+
parameters[:query_parameters]&.each do |query_param|
|
244
|
+
param = {
|
245
|
+
"name" => query_param[:name],
|
246
|
+
"in" => "query",
|
247
|
+
"required" => query_param[:required] != "false",
|
248
|
+
"schema" => build_parameter_schema(query_param)
|
249
|
+
}
|
250
|
+
param["description"] = query_param[:description] if query_param[:description]
|
251
|
+
openapi_params << param
|
252
|
+
end
|
253
|
+
|
254
|
+
openapi_params
|
255
|
+
end
|
256
|
+
|
257
|
+
# Builds OpenAPI request body object from body parameters
|
258
|
+
# @param parameters [Hash] Parameter definitions
|
259
|
+
# @return [Hash, nil] OpenAPI request body object or nil
|
260
|
+
def build_request_body(parameters)
|
261
|
+
return nil if parameters[:body_parameters].nil? || parameters[:body_parameters].empty?
|
262
|
+
|
263
|
+
properties = {}
|
264
|
+
required = []
|
265
|
+
|
266
|
+
parameters[:body_parameters].each do |body_param|
|
267
|
+
properties[body_param[:name]] = build_parameter_schema(body_param)
|
268
|
+
required << body_param[:name] if body_param[:required] != "false"
|
269
|
+
end
|
270
|
+
|
271
|
+
{
|
272
|
+
"required" => true,
|
273
|
+
"content" => {
|
274
|
+
"application/json" => {
|
275
|
+
"schema" => {
|
276
|
+
"type" => "object",
|
277
|
+
"properties" => properties,
|
278
|
+
"required" => required
|
279
|
+
}
|
280
|
+
}
|
281
|
+
}
|
282
|
+
}
|
283
|
+
end
|
284
|
+
|
285
|
+
# Builds parameter schema from parameter definition
|
286
|
+
# @param param [Hash] Parameter definition
|
287
|
+
# @return [Hash] OpenAPI parameter schema
|
288
|
+
def build_parameter_schema(param)
|
289
|
+
schema = { "type" => param[:type] || "string" }
|
290
|
+
|
291
|
+
schema["description"] = param[:description] if param[:description]
|
292
|
+
schema["enum"] = param[:enum] if param[:enum]
|
293
|
+
schema["format"] = param[:format] if param[:format]
|
294
|
+
schema["minimum"] = param[:minimum] if param[:minimum]
|
295
|
+
schema["maximum"] = param[:maximum] if param[:maximum]
|
296
|
+
schema["example"] = param[:example] if param[:example]
|
297
|
+
|
298
|
+
schema
|
299
|
+
end
|
300
|
+
end
|
301
|
+
end
|
302
|
+
end
|