rails-openapi-gen 0.0.2 → 0.0.3
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/CHANGELOG.md +40 -0
- data/CLAUDE.md +17 -5
- data/README.md +25 -0
- data/lib/rails-openapi-gen/ast_nodes/array_node.rb +101 -0
- data/lib/rails-openapi-gen/ast_nodes/base_node.rb +139 -0
- data/lib/rails-openapi-gen/ast_nodes/comment_data.rb +180 -0
- data/lib/rails-openapi-gen/ast_nodes/node_factory.rb +206 -0
- data/lib/rails-openapi-gen/ast_nodes/object_node.rb +129 -0
- data/lib/rails-openapi-gen/ast_nodes/partial_node.rb +111 -0
- data/lib/rails-openapi-gen/ast_nodes/property_node.rb +74 -0
- data/lib/rails-openapi-gen/ast_nodes.rb +129 -0
- data/lib/rails-openapi-gen/configuration.rb +154 -22
- data/lib/rails-openapi-gen/debug_helpers.rb +185 -0
- data/lib/rails-openapi-gen/engine.rb +1 -1
- data/lib/rails-openapi-gen/generators/yaml_generator.rb +242 -27
- data/lib/rails-openapi-gen/generators.rb +5 -0
- data/lib/rails-openapi-gen/importer.rb +164 -145
- data/lib/rails-openapi-gen/parsers/comment_parser.rb +1 -1
- data/lib/rails-openapi-gen/parsers/comment_parsers/attribute_parser.rb +7 -7
- data/lib/rails-openapi-gen/parsers/comment_parsers/base_attribute_parser.rb +5 -9
- data/lib/rails-openapi-gen/parsers/comment_parsers/body_parser.rb +6 -6
- data/lib/rails-openapi-gen/parsers/comment_parsers/conditional_parser.rb +1 -1
- data/lib/rails-openapi-gen/parsers/comment_parsers/operation_parser.rb +5 -5
- data/lib/rails-openapi-gen/parsers/comment_parsers/param_parser.rb +6 -6
- data/lib/rails-openapi-gen/parsers/comment_parsers/query_parser.rb +6 -6
- data/lib/rails-openapi-gen/parsers/controller_parser.rb +64 -20
- data/lib/rails-openapi-gen/parsers/jbuilder/ast_parser.rb +914 -0
- data/lib/rails-openapi-gen/parsers/jbuilder/call_detectors/array_call_detector.rb +103 -0
- data/lib/rails-openapi-gen/parsers/jbuilder/call_detectors/base_detector.rb +107 -0
- data/lib/rails-openapi-gen/parsers/jbuilder/call_detectors/cache_call_detector.rb +112 -0
- data/lib/rails-openapi-gen/parsers/jbuilder/call_detectors/json_call_detector.rb +91 -0
- data/lib/rails-openapi-gen/parsers/jbuilder/call_detectors/key_format_detector.rb +27 -0
- data/lib/rails-openapi-gen/parsers/jbuilder/call_detectors/null_handling_detector.rb +27 -0
- data/lib/rails-openapi-gen/parsers/jbuilder/call_detectors/object_manipulation_detector.rb +27 -0
- data/lib/rails-openapi-gen/parsers/jbuilder/call_detectors/partial_call_detector.rb +125 -0
- data/lib/rails-openapi-gen/parsers/jbuilder/call_detectors.rb +95 -0
- data/lib/rails-openapi-gen/parsers/jbuilder/jbuilder_parser.rb +39 -0
- data/lib/rails-openapi-gen/parsers/jbuilder/operation_comment_parser.rb +26 -0
- data/lib/rails-openapi-gen/parsers/jbuilder/processors/array_processor.rb +266 -0
- data/lib/rails-openapi-gen/parsers/jbuilder/processors/base_processor.rb +235 -0
- data/lib/rails-openapi-gen/parsers/jbuilder/processors/composite_processor.rb +97 -0
- data/lib/rails-openapi-gen/parsers/jbuilder/processors/object_processor.rb +176 -0
- data/lib/rails-openapi-gen/parsers/jbuilder/processors/partial_processor.rb +69 -0
- data/lib/rails-openapi-gen/parsers/jbuilder/processors/property_processor.rb +68 -0
- data/lib/rails-openapi-gen/parsers/jbuilder/processors.rb +10 -0
- data/lib/rails-openapi-gen/parsers/jbuilder/property_comment_parser.rb +26 -0
- data/lib/rails-openapi-gen/parsers/jbuilder.rb +10 -0
- data/lib/rails-openapi-gen/parsers/routes_parser.rb +83 -9
- data/lib/rails-openapi-gen/parsers/template_processors/jbuilder_template_processor.rb +125 -131
- data/lib/rails-openapi-gen/parsers/template_processors/response_template_processor.rb +8 -12
- data/lib/rails-openapi-gen/parsers/template_processors.rb +6 -0
- data/lib/rails-openapi-gen/parsers.rb +9 -0
- data/lib/rails-openapi-gen/processors/ast_to_schema_processor.rb +226 -0
- data/lib/rails-openapi-gen/processors/base_processor.rb +124 -0
- data/lib/rails-openapi-gen/processors/component_schema_processor.rb +35 -0
- data/lib/rails-openapi-gen/processors/openapi_schema_processor.rb +218 -0
- data/lib/rails-openapi-gen/processors.rb +7 -0
- data/lib/rails-openapi-gen/railtie.rb +1 -1
- data/lib/rails-openapi-gen/tasks/openapi.rake +4 -4
- data/lib/rails-openapi-gen/version.rb +1 -1
- data/lib/rails-openapi-gen.rb +169 -196
- data/lib/tasks/openapi_import.rake +35 -36
- data/rails-openapi-gen.gemspec +6 -5
- metadata +62 -23
- data/lib/rails-openapi-gen/parsers/jbuilder_parser.rb +0 -529
@@ -2,16 +2,19 @@
|
|
2
2
|
|
3
3
|
require "yaml"
|
4
4
|
require "fileutils"
|
5
|
+
require_relative "../processors/component_schema_processor"
|
5
6
|
|
6
7
|
module RailsOpenapiGen
|
7
8
|
module Generators
|
8
9
|
class YamlGenerator
|
9
|
-
attr_reader :schemas
|
10
|
+
attr_reader :schemas, :components
|
10
11
|
|
11
12
|
# Initializes YAML generator with schemas data
|
12
13
|
# @param schemas [Hash] Hash of route information and schemas
|
13
|
-
|
14
|
+
# @param components [Hash] Hash of component schemas from partials
|
15
|
+
def initialize(schemas, components: {})
|
14
16
|
@schemas = schemas
|
17
|
+
@components = components
|
15
18
|
@config = RailsOpenapiGen.configuration
|
16
19
|
@base_path = @config.output_directory
|
17
20
|
end
|
@@ -34,7 +37,8 @@ module RailsOpenapiGen
|
|
34
37
|
end
|
35
38
|
|
36
39
|
if @config.split_files?
|
37
|
-
|
40
|
+
write_endpoint_files(paths_data)
|
41
|
+
write_component_files if @components && @components.any?
|
38
42
|
write_main_openapi_file
|
39
43
|
else
|
40
44
|
write_single_file(paths_data)
|
@@ -63,20 +67,54 @@ module RailsOpenapiGen
|
|
63
67
|
false
|
64
68
|
end
|
65
69
|
|
66
|
-
# Creates necessary output directories
|
70
|
+
# Creates necessary output directories and cleans existing path files
|
67
71
|
# @return [void]
|
68
72
|
def setup_directories
|
69
73
|
FileUtils.mkdir_p(@base_path)
|
70
74
|
if @config.split_files?
|
71
|
-
|
75
|
+
paths_dir = File.join(@base_path, "paths")
|
76
|
+
FileUtils.mkdir_p(paths_dir)
|
77
|
+
# Clean existing path files to prevent merging with stale data
|
78
|
+
clean_paths_directory(paths_dir)
|
79
|
+
|
80
|
+
# Create components directory for partial components
|
81
|
+
if @components && @components.any?
|
82
|
+
components_dir = File.join(@base_path, "components", "schemas")
|
83
|
+
FileUtils.mkdir_p(components_dir)
|
84
|
+
clean_components_directory(components_dir)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# Cleans existing YAML files from the paths directory
|
90
|
+
# @param paths_dir [String] Path to the paths directory
|
91
|
+
# @return [void]
|
92
|
+
def clean_paths_directory(paths_dir)
|
93
|
+
Dir[File.join(paths_dir, "*.yaml")].each do |file|
|
94
|
+
File.delete(file)
|
95
|
+
puts "🗑️ Removed existing path file: #{File.basename(file)}" if ENV['RAILS_OPENAPI_DEBUG']
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# Cleans existing YAML files from the components directory
|
100
|
+
# @param components_dir [String] Path to the components directory
|
101
|
+
# @return [void]
|
102
|
+
def clean_components_directory(components_dir)
|
103
|
+
Dir[File.join(components_dir, "*.yaml")].each do |file|
|
104
|
+
File.delete(file)
|
105
|
+
puts "🗑️ Removed existing component file: #{File.basename(file)}" if ENV['RAILS_OPENAPI_DEBUG']
|
72
106
|
end
|
73
107
|
end
|
74
108
|
|
75
|
-
# Converts Rails path format to OpenAPI format
|
76
|
-
# @param path [String] Rails path (e.g., "/users/:id")
|
109
|
+
# Converts Rails path format to OpenAPI format and removes API prefix if configured
|
110
|
+
# @param path [String] Rails path (e.g., "/api/v1/users/:id")
|
77
111
|
# @return [String] OpenAPI path (e.g., "/users/{id}")
|
78
112
|
def normalize_path(path)
|
79
|
-
|
113
|
+
# First remove API prefix if configured
|
114
|
+
path_without_prefix = @config.remove_api_prefix(path)
|
115
|
+
|
116
|
+
# Then convert Rails path parameters to OpenAPI format
|
117
|
+
path_without_prefix.gsub(/:(\w+)/, '{\\1}')
|
80
118
|
end
|
81
119
|
|
82
120
|
# Builds OpenAPI operation object for a route
|
@@ -86,10 +124,13 @@ module RailsOpenapiGen
|
|
86
124
|
# @param operation_info [Hash, nil] Operation metadata from comments
|
87
125
|
# @return [Hash] OpenAPI operation object
|
88
126
|
def build_operation(route, schema, parameters = {}, operation_info = nil)
|
127
|
+
# Generate operationId with prefix removal
|
128
|
+
default_operation_id = generate_operation_id(route)
|
129
|
+
|
89
130
|
operation = {
|
90
131
|
"summary" => operation_info&.dig(:summary) || "#{humanize(route[:action])} #{humanize(singularize(route[:controller]))}",
|
91
|
-
"operationId" => operation_info&.dig(:operationId) ||
|
92
|
-
"tags" => operation_info&.dig(:tags) ||
|
132
|
+
"operationId" => operation_info&.dig(:operationId) || default_operation_id,
|
133
|
+
"tags" => operation_info&.dig(:tags) || generate_tag_names(route)
|
93
134
|
}
|
94
135
|
|
95
136
|
# Add description if provided
|
@@ -121,18 +162,18 @@ module RailsOpenapiGen
|
|
121
162
|
operation
|
122
163
|
end
|
123
164
|
|
124
|
-
# Writes path data to separate YAML files
|
165
|
+
# Writes path data to separate YAML files for each endpoint
|
125
166
|
# @param paths_data [Hash] OpenAPI paths data
|
126
167
|
# @return [void]
|
127
|
-
def
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
file_data = {}
|
132
|
-
paths.each { |path, operations| file_data[path] = operations }
|
168
|
+
def write_endpoint_files(paths_data)
|
169
|
+
paths_data.each do |path, operations|
|
170
|
+
endpoint_name = generate_endpoint_filename(path)
|
171
|
+
file_data = { path => operations }
|
133
172
|
|
134
|
-
file_path = File.join(@base_path, "paths", "#{
|
173
|
+
file_path = File.join(@base_path, "paths", "#{endpoint_name}.yaml")
|
135
174
|
File.write(file_path, file_data.to_yaml)
|
175
|
+
|
176
|
+
puts "📝 Written endpoint file: #{endpoint_name}.yaml" if ENV['RAILS_OPENAPI_DEBUG']
|
136
177
|
end
|
137
178
|
end
|
138
179
|
|
@@ -155,6 +196,11 @@ module RailsOpenapiGen
|
|
155
196
|
end
|
156
197
|
end
|
157
198
|
|
199
|
+
# Add components section with external references if we have any components
|
200
|
+
if @components && @components.any?
|
201
|
+
openapi_data["components"] = generate_components_references
|
202
|
+
end
|
203
|
+
|
158
204
|
File.write(File.join(@base_path, @config.output_filename), openapi_data.to_yaml)
|
159
205
|
end
|
160
206
|
|
@@ -188,14 +234,19 @@ module RailsOpenapiGen
|
|
188
234
|
end
|
189
235
|
end
|
190
236
|
|
191
|
-
#
|
192
|
-
# @param path [String] API path
|
193
|
-
# @return [String]
|
194
|
-
def
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
237
|
+
# Generates filename for endpoint-specific files
|
238
|
+
# @param path [String] API path (e.g., "/api/users/{id}")
|
239
|
+
# @return [String] Filename (e.g., "api_users_id")
|
240
|
+
def generate_endpoint_filename(path)
|
241
|
+
# Remove leading slash and replace special characters
|
242
|
+
clean_path = path.sub(%r{^/+}, '')
|
243
|
+
.gsub('/', '_')
|
244
|
+
.gsub(/[{}]/, '')
|
245
|
+
.gsub(/[^a-zA-Z0-9_]/, '_')
|
246
|
+
.gsub(/_+/, '_')
|
247
|
+
.gsub(/^_|_$/, '')
|
248
|
+
|
249
|
+
clean_path.empty? ? 'root' : clean_path
|
199
250
|
end
|
200
251
|
|
201
252
|
# Converts snake_case string to human readable format
|
@@ -214,6 +265,104 @@ module RailsOpenapiGen
|
|
214
265
|
str.end_with?('s') ? str[0..-2] : str
|
215
266
|
end
|
216
267
|
|
268
|
+
# Convert PascalCase/camelCase string to kebab-case with optional prefix removal
|
269
|
+
# @param string [String] String to convert
|
270
|
+
# @return [String] kebab-case string
|
271
|
+
def to_kebab_case(string)
|
272
|
+
# First remove component prefix if configured
|
273
|
+
name_without_prefix = @config.remove_component_prefix(string.to_s)
|
274
|
+
|
275
|
+
name_without_prefix.gsub(/([a-z\d])([A-Z])/, '\1-\2') # Insert dash before capital letters
|
276
|
+
.downcase # Convert to lowercase
|
277
|
+
end
|
278
|
+
|
279
|
+
# Convert PascalCase/camelCase string to snake_case
|
280
|
+
# @param string [String] String to convert
|
281
|
+
# @return [String] snake_case string
|
282
|
+
def to_snake_case(string)
|
283
|
+
string.to_s
|
284
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2') # Insert underscore before capital letters
|
285
|
+
.downcase # Convert to lowercase
|
286
|
+
end
|
287
|
+
|
288
|
+
# Generate operationId with prefix removal from controller path
|
289
|
+
# @param route [Hash] Route information
|
290
|
+
# @return [String] Operation ID with prefix removed
|
291
|
+
def generate_operation_id(route)
|
292
|
+
controller_path = route[:controller]
|
293
|
+
action = route[:action]
|
294
|
+
|
295
|
+
# Remove API prefix from controller path if configured
|
296
|
+
controller_without_prefix = remove_controller_prefix(controller_path)
|
297
|
+
|
298
|
+
# Convert to underscore format for operationId
|
299
|
+
"#{controller_without_prefix.gsub('/', '_')}_#{action}"
|
300
|
+
end
|
301
|
+
|
302
|
+
# Remove API prefix from controller path
|
303
|
+
# @param controller_path [String] Controller path (e.g., "api/v1/users")
|
304
|
+
# @return [String] Controller path with prefix removed (e.g., "users")
|
305
|
+
def remove_controller_prefix(controller_path)
|
306
|
+
# Use the same API prefix configuration
|
307
|
+
api_prefix = @config.view_paths&.dig(:api_prefix)
|
308
|
+
return controller_path unless api_prefix
|
309
|
+
|
310
|
+
# Convert api_prefix to controller format (e.g., "api/v1" -> "api/v1")
|
311
|
+
normalized_prefix = api_prefix.gsub(%r{^/+|/+$}, '') # Remove leading/trailing slashes
|
312
|
+
|
313
|
+
# Remove the prefix if the controller path starts with it
|
314
|
+
if controller_path.start_with?(normalized_prefix + '/')
|
315
|
+
remaining_path = controller_path[(normalized_prefix.length + 1)..-1]
|
316
|
+
remaining_path.empty? ? controller_path : remaining_path
|
317
|
+
elsif controller_path == normalized_prefix
|
318
|
+
# If controller path is exactly the prefix, return empty or fallback
|
319
|
+
'root'
|
320
|
+
else
|
321
|
+
controller_path
|
322
|
+
end
|
323
|
+
end
|
324
|
+
|
325
|
+
# Generate multiple tags based on URL path and controller path
|
326
|
+
# @param route [Hash] Route information
|
327
|
+
# @return [Array<String>] Array of tag names for the resources
|
328
|
+
def generate_tag_names(route)
|
329
|
+
controller_path = route[:controller]
|
330
|
+
url_path = route[:path]
|
331
|
+
|
332
|
+
# Remove API prefix from controller path
|
333
|
+
controller_without_prefix = remove_controller_prefix(controller_path)
|
334
|
+
|
335
|
+
# Also extract resource names from URL path for nested routes
|
336
|
+
url_without_prefix = @config.remove_api_prefix(url_path)
|
337
|
+
|
338
|
+
tags = []
|
339
|
+
|
340
|
+
# Extract tags from controller path
|
341
|
+
controller_parts = controller_without_prefix.split('/')
|
342
|
+
controller_parts.each do |part|
|
343
|
+
next if part.empty?
|
344
|
+
|
345
|
+
tag = to_snake_case(part)
|
346
|
+
tags << tag unless tags.include?(tag)
|
347
|
+
end
|
348
|
+
|
349
|
+
# Extract additional tags from URL path for nested resources
|
350
|
+
# Match patterns like /users/{id}/posts to extract "users"
|
351
|
+
url_segments = url_without_prefix.split('/').reject(&:empty?)
|
352
|
+
|
353
|
+
url_segments.each do |segment|
|
354
|
+
# Skip parameter segments like {id} or :id
|
355
|
+
next if segment.match(/^[:{].*[}]?$/)
|
356
|
+
|
357
|
+
# Convert to snake_case for consistency
|
358
|
+
tag = segment.downcase.gsub('-', '_')
|
359
|
+
tags << tag unless tags.include?(tag)
|
360
|
+
end
|
361
|
+
|
362
|
+
# Ensure we always have at least one tag
|
363
|
+
tags.empty? ? ["Api"] : tags.uniq
|
364
|
+
end
|
365
|
+
|
217
366
|
# Builds OpenAPI parameter objects from route and parameter data
|
218
367
|
# @param route [Hash] Route information
|
219
368
|
# @param parameters [Hash] Parameter definitions
|
@@ -297,6 +446,72 @@ module RailsOpenapiGen
|
|
297
446
|
|
298
447
|
schema
|
299
448
|
end
|
449
|
+
|
450
|
+
# Generate components section from collected partial components
|
451
|
+
# @return [Hash] Components section for OpenAPI spec
|
452
|
+
def generate_components_section
|
453
|
+
return {} if @components.empty?
|
454
|
+
|
455
|
+
components = {
|
456
|
+
"schemas" => {}
|
457
|
+
}
|
458
|
+
|
459
|
+
@components.each do |component_name, ast_node|
|
460
|
+
puts "📦 Generating component schema for: #{component_name}" if ENV['RAILS_OPENAPI_DEBUG']
|
461
|
+
|
462
|
+
# Convert AST node to OpenAPI schema
|
463
|
+
schema = Processors::AstToSchemaProcessor.new.process_to_schema(ast_node)
|
464
|
+
components["schemas"][component_name] = schema
|
465
|
+
end
|
466
|
+
|
467
|
+
components
|
468
|
+
end
|
469
|
+
|
470
|
+
# Generate components section with external file references
|
471
|
+
# @return [Hash] Components section with $ref to external files
|
472
|
+
def generate_components_references
|
473
|
+
return {} if @components.empty?
|
474
|
+
|
475
|
+
components = {
|
476
|
+
"schemas" => {}
|
477
|
+
}
|
478
|
+
|
479
|
+
@components.each_key do |component_name|
|
480
|
+
# Remove prefix from both schema name and file name
|
481
|
+
schema_name_without_prefix = @config.remove_component_prefix(component_name)
|
482
|
+
kebab_filename = to_kebab_case(component_name)
|
483
|
+
|
484
|
+
puts "🔗 Creating reference for component: #{component_name} -> #{schema_name_without_prefix} (#{kebab_filename}.yaml)" if ENV['RAILS_OPENAPI_DEBUG']
|
485
|
+
|
486
|
+
# Create $ref to external component file (using kebab-case filename)
|
487
|
+
# Use schema name without prefix as the key
|
488
|
+
components["schemas"][schema_name_without_prefix] = {
|
489
|
+
"$ref" => "./components/schemas/#{kebab_filename}.yaml"
|
490
|
+
}
|
491
|
+
end
|
492
|
+
|
493
|
+
components
|
494
|
+
end
|
495
|
+
|
496
|
+
# Write individual component files to components/schemas directory
|
497
|
+
# @return [void]
|
498
|
+
def write_component_files
|
499
|
+
return unless @components && @components.any?
|
500
|
+
|
501
|
+
@components.each do |component_name, ast_node|
|
502
|
+
kebab_filename = to_kebab_case(component_name)
|
503
|
+
puts "📝 Writing component file: #{kebab_filename}.yaml" if ENV['RAILS_OPENAPI_DEBUG']
|
504
|
+
|
505
|
+
# Convert AST node to OpenAPI schema (with inline expansion for components)
|
506
|
+
schema = Processors::ComponentSchemaProcessor.new.process_to_schema(ast_node)
|
507
|
+
|
508
|
+
# Write to individual YAML file
|
509
|
+
file_path = File.join(@base_path, "components", "schemas", "#{kebab_filename}.yaml")
|
510
|
+
File.write(file_path, schema.to_yaml)
|
511
|
+
|
512
|
+
puts "✅ Component file written: #{kebab_filename}.yaml" if ENV['RAILS_OPENAPI_DEBUG']
|
513
|
+
end
|
514
|
+
end
|
300
515
|
end
|
301
516
|
end
|
302
|
-
end
|
517
|
+
end
|