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.
Files changed (27) hide show
  1. checksums.yaml +7 -0
  2. data/CLAUDE.md +160 -0
  3. data/README.md +164 -0
  4. data/lib/rails_openapi_gen/configuration.rb +157 -0
  5. data/lib/rails_openapi_gen/engine.rb +11 -0
  6. data/lib/rails_openapi_gen/generators/yaml_generator.rb +302 -0
  7. data/lib/rails_openapi_gen/importer.rb +647 -0
  8. data/lib/rails_openapi_gen/parsers/comment_parser.rb +40 -0
  9. data/lib/rails_openapi_gen/parsers/comment_parsers/attribute_parser.rb +57 -0
  10. data/lib/rails_openapi_gen/parsers/comment_parsers/base_attribute_parser.rb +42 -0
  11. data/lib/rails_openapi_gen/parsers/comment_parsers/body_parser.rb +62 -0
  12. data/lib/rails_openapi_gen/parsers/comment_parsers/conditional_parser.rb +13 -0
  13. data/lib/rails_openapi_gen/parsers/comment_parsers/operation_parser.rb +50 -0
  14. data/lib/rails_openapi_gen/parsers/comment_parsers/param_parser.rb +62 -0
  15. data/lib/rails_openapi_gen/parsers/comment_parsers/query_parser.rb +62 -0
  16. data/lib/rails_openapi_gen/parsers/controller_parser.rb +153 -0
  17. data/lib/rails_openapi_gen/parsers/jbuilder_parser.rb +529 -0
  18. data/lib/rails_openapi_gen/parsers/routes_parser.rb +33 -0
  19. data/lib/rails_openapi_gen/parsers/template_processors/jbuilder_template_processor.rb +147 -0
  20. data/lib/rails_openapi_gen/parsers/template_processors/response_template_processor.rb +17 -0
  21. data/lib/rails_openapi_gen/railtie.rb +11 -0
  22. data/lib/rails_openapi_gen/tasks/openapi.rake +30 -0
  23. data/lib/rails_openapi_gen/version.rb +5 -0
  24. data/lib/rails_openapi_gen.rb +267 -0
  25. data/lib/tasks/openapi_import.rake +126 -0
  26. data/rails-openapi-gen.gemspec +30 -0
  27. 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