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.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +40 -0
  3. data/CLAUDE.md +17 -5
  4. data/README.md +25 -0
  5. data/lib/rails-openapi-gen/ast_nodes/array_node.rb +101 -0
  6. data/lib/rails-openapi-gen/ast_nodes/base_node.rb +139 -0
  7. data/lib/rails-openapi-gen/ast_nodes/comment_data.rb +180 -0
  8. data/lib/rails-openapi-gen/ast_nodes/node_factory.rb +206 -0
  9. data/lib/rails-openapi-gen/ast_nodes/object_node.rb +129 -0
  10. data/lib/rails-openapi-gen/ast_nodes/partial_node.rb +111 -0
  11. data/lib/rails-openapi-gen/ast_nodes/property_node.rb +74 -0
  12. data/lib/rails-openapi-gen/ast_nodes.rb +129 -0
  13. data/lib/rails-openapi-gen/configuration.rb +154 -22
  14. data/lib/rails-openapi-gen/debug_helpers.rb +185 -0
  15. data/lib/rails-openapi-gen/engine.rb +1 -1
  16. data/lib/rails-openapi-gen/generators/yaml_generator.rb +242 -27
  17. data/lib/rails-openapi-gen/generators.rb +5 -0
  18. data/lib/rails-openapi-gen/importer.rb +164 -145
  19. data/lib/rails-openapi-gen/parsers/comment_parser.rb +1 -1
  20. data/lib/rails-openapi-gen/parsers/comment_parsers/attribute_parser.rb +7 -7
  21. data/lib/rails-openapi-gen/parsers/comment_parsers/base_attribute_parser.rb +5 -9
  22. data/lib/rails-openapi-gen/parsers/comment_parsers/body_parser.rb +6 -6
  23. data/lib/rails-openapi-gen/parsers/comment_parsers/conditional_parser.rb +1 -1
  24. data/lib/rails-openapi-gen/parsers/comment_parsers/operation_parser.rb +5 -5
  25. data/lib/rails-openapi-gen/parsers/comment_parsers/param_parser.rb +6 -6
  26. data/lib/rails-openapi-gen/parsers/comment_parsers/query_parser.rb +6 -6
  27. data/lib/rails-openapi-gen/parsers/controller_parser.rb +64 -20
  28. data/lib/rails-openapi-gen/parsers/jbuilder/ast_parser.rb +914 -0
  29. data/lib/rails-openapi-gen/parsers/jbuilder/call_detectors/array_call_detector.rb +103 -0
  30. data/lib/rails-openapi-gen/parsers/jbuilder/call_detectors/base_detector.rb +107 -0
  31. data/lib/rails-openapi-gen/parsers/jbuilder/call_detectors/cache_call_detector.rb +112 -0
  32. data/lib/rails-openapi-gen/parsers/jbuilder/call_detectors/json_call_detector.rb +91 -0
  33. data/lib/rails-openapi-gen/parsers/jbuilder/call_detectors/key_format_detector.rb +27 -0
  34. data/lib/rails-openapi-gen/parsers/jbuilder/call_detectors/null_handling_detector.rb +27 -0
  35. data/lib/rails-openapi-gen/parsers/jbuilder/call_detectors/object_manipulation_detector.rb +27 -0
  36. data/lib/rails-openapi-gen/parsers/jbuilder/call_detectors/partial_call_detector.rb +125 -0
  37. data/lib/rails-openapi-gen/parsers/jbuilder/call_detectors.rb +95 -0
  38. data/lib/rails-openapi-gen/parsers/jbuilder/jbuilder_parser.rb +39 -0
  39. data/lib/rails-openapi-gen/parsers/jbuilder/operation_comment_parser.rb +26 -0
  40. data/lib/rails-openapi-gen/parsers/jbuilder/processors/array_processor.rb +266 -0
  41. data/lib/rails-openapi-gen/parsers/jbuilder/processors/base_processor.rb +235 -0
  42. data/lib/rails-openapi-gen/parsers/jbuilder/processors/composite_processor.rb +97 -0
  43. data/lib/rails-openapi-gen/parsers/jbuilder/processors/object_processor.rb +176 -0
  44. data/lib/rails-openapi-gen/parsers/jbuilder/processors/partial_processor.rb +69 -0
  45. data/lib/rails-openapi-gen/parsers/jbuilder/processors/property_processor.rb +68 -0
  46. data/lib/rails-openapi-gen/parsers/jbuilder/processors.rb +10 -0
  47. data/lib/rails-openapi-gen/parsers/jbuilder/property_comment_parser.rb +26 -0
  48. data/lib/rails-openapi-gen/parsers/jbuilder.rb +10 -0
  49. data/lib/rails-openapi-gen/parsers/routes_parser.rb +83 -9
  50. data/lib/rails-openapi-gen/parsers/template_processors/jbuilder_template_processor.rb +125 -131
  51. data/lib/rails-openapi-gen/parsers/template_processors/response_template_processor.rb +8 -12
  52. data/lib/rails-openapi-gen/parsers/template_processors.rb +6 -0
  53. data/lib/rails-openapi-gen/parsers.rb +9 -0
  54. data/lib/rails-openapi-gen/processors/ast_to_schema_processor.rb +226 -0
  55. data/lib/rails-openapi-gen/processors/base_processor.rb +124 -0
  56. data/lib/rails-openapi-gen/processors/component_schema_processor.rb +35 -0
  57. data/lib/rails-openapi-gen/processors/openapi_schema_processor.rb +218 -0
  58. data/lib/rails-openapi-gen/processors.rb +7 -0
  59. data/lib/rails-openapi-gen/railtie.rb +1 -1
  60. data/lib/rails-openapi-gen/tasks/openapi.rake +4 -4
  61. data/lib/rails-openapi-gen/version.rb +1 -1
  62. data/lib/rails-openapi-gen.rb +169 -196
  63. data/lib/tasks/openapi_import.rake +35 -36
  64. data/rails-openapi-gen.gemspec +6 -5
  65. metadata +62 -23
  66. data/lib/rails-openapi-gen/parsers/jbuilder_parser.rb +0 -529
@@ -1,26 +1,25 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "rails-openapi-gen/version"
4
- require "rails-openapi-gen/configuration"
5
- require "rails-openapi-gen/parsers/comment_parser"
6
- require "rails-openapi-gen/generators/yaml_generator"
7
- require "rails-openapi-gen/importer"
3
+ require 'rails-openapi-gen/version'
4
+ require 'rails-openapi-gen/configuration'
5
+ require 'parser/current'
6
+ require 'pp'
8
7
 
9
- # Rails integration is handled by Engine
8
+ # Zeitwerk autoloading setup
9
+ module RailsOpenapiGen
10
+ autoload :AstNodes, "rails-openapi-gen/ast_nodes"
11
+ autoload :Parsers, "rails-openapi-gen/parsers"
12
+ autoload :Processors, "rails-openapi-gen/processors"
13
+ autoload :Generators, "rails-openapi-gen/generators"
14
+ autoload :Importer, "rails-openapi-gen/importer"
15
+ end
10
16
 
11
- # Load Rails Engine if Rails is available
17
+ # Direct requires for core components that don't follow autoload patterns
18
+ require 'rails-openapi-gen/generators/yaml_generator'
19
+
20
+ # Rails integration is handled by Engine
12
21
  if defined?(Rails::Engine)
13
- require "rails-openapi-gen/engine"
14
-
15
- # Only load parser-dependent components if parser gem is available
16
- begin
17
- require "parser/current"
18
- require "rails-openapi-gen/parsers/routes_parser"
19
- require "rails-openapi-gen/parsers/controller_parser"
20
- require "rails-openapi-gen/parsers/jbuilder_parser"
21
- rescue LoadError
22
- # parser gem not available, skip these components
23
- end
22
+ require 'rails-openapi-gen/engine'
24
23
  end
25
24
 
26
25
  module RailsOpenapiGen
@@ -47,221 +46,195 @@ module RailsOpenapiGen
47
46
  end
48
47
  end
49
48
 
49
+ class Checker
50
+ # Checks for missing OpenAPI comments and uncommitted changes
51
+ # @return [void]
52
+ def run
53
+ puts "🔍 Checking for missing comments and uncommitted changes..."
54
+
55
+ # Run OpenAPI generation to check for missing comments
56
+ system('bin/rails openapi:generate') || system('bundle exec rails openapi:generate')
57
+
58
+ # Check for uncommitted changes in openapi directory
59
+ if system('git diff --quiet docs/api/ 2>/dev/null')
60
+ puts "✅ All checks passed!"
61
+ else
62
+ puts "❌ Found uncommitted changes in OpenAPI files"
63
+ puts `git diff docs/api/`
64
+ exit 1
65
+ end
66
+ end
67
+ end
68
+
50
69
  class Generator
51
70
  # Runs the OpenAPI generation process
52
71
  # @return [void]
53
72
  def run
54
73
  # Load configuration
55
74
  RailsOpenapiGen.configuration.load_from_file
56
-
75
+
57
76
  routes = Parsers::RoutesParser.new.parse
58
- filtered_routes = routes.select { |route| RailsOpenapiGen.configuration.route_included?(route[:path]) }
59
-
77
+
78
+ # Debug: Show filtering configuration
79
+ config = RailsOpenapiGen.configuration
80
+
81
+ # Apply filtering with debug output
82
+ filtered_routes = routes.select do |route|
83
+ included = config.route_included?(route[:path])
84
+ included
85
+ end
86
+
60
87
  schemas = {}
88
+ all_components = {}
61
89
 
62
90
  filtered_routes.each do |route|
63
91
  controller_info = Parsers::ControllerParser.new(route).parse
64
-
65
- if controller_info[:jbuilder_path]
66
- jbuilder_result = Parsers::JbuilderParser.new(controller_info[:jbuilder_path]).parse
67
- schema = build_schema(jbuilder_result[:properties])
68
- schemas[route] = {
69
- schema: schema,
70
- parameters: controller_info[:parameters] || {},
71
- operation: jbuilder_result[:operation]
72
- }
92
+
93
+ next unless controller_info[:jbuilder_path]
94
+
95
+ jbuilder_parser = Parsers::Jbuilder::JbuilderParser.new(controller_info[:jbuilder_path])
96
+ ast_node = jbuilder_parser.parse
97
+
98
+ # Collect components from this parser
99
+ if ENV['RAILS_OPENAPI_DEBUG']
100
+ puts "🔍 DEBUG: jbuilder_parser has ast_parser: #{jbuilder_parser.respond_to?(:ast_parser)}"
101
+ puts "🔍 DEBUG: ast_parser is nil: #{jbuilder_parser.ast_parser.nil?}" if jbuilder_parser.respond_to?(:ast_parser)
102
+ if jbuilder_parser.respond_to?(:ast_parser) && jbuilder_parser.ast_parser && jbuilder_parser.ast_parser.respond_to?(:partial_components)
103
+ puts "🔍 DEBUG: partial_components count: #{jbuilder_parser.ast_parser.partial_components.size}"
104
+ end
105
+ end
106
+
107
+ if jbuilder_parser.respond_to?(:ast_parser) &&
108
+ jbuilder_parser.ast_parser &&
109
+ jbuilder_parser.ast_parser.respond_to?(:partial_components) &&
110
+ jbuilder_parser.ast_parser.partial_components.any?
111
+ puts "📦 Merging #{jbuilder_parser.ast_parser.partial_components.size} components" if ENV['RAILS_OPENAPI_DEBUG']
112
+ all_components.merge!(jbuilder_parser.ast_parser.partial_components)
73
113
  end
114
+
115
+ schema = Processors::AstToSchemaProcessor.new.process_to_schema(ast_node)
116
+ schemas[route] = {
117
+ schema: schema,
118
+ parameters: controller_info[:parameters] || {},
119
+ operation: {} # Operation data is not available in AST approach
120
+ }
121
+ rescue StandardError => e
122
+ puts "❌ ERROR processing route #{route[:method]} #{route[:path]}: #{e.class} - #{e.message}" if ENV['RAILS_OPENAPI_DEBUG']
123
+ puts "❌ ERROR at: #{e.backtrace.first(3).join("\n")}" if ENV['RAILS_OPENAPI_DEBUG']
124
+ raise
74
125
  end
75
126
 
76
- Generators::YamlGenerator.new(schemas).generate
77
- puts "✅ OpenAPI specification generated successfully!"
127
+ Generators::YamlGenerator.new(schemas, components: all_components).generate
128
+ puts '✅ OpenAPI specification generated successfully!'
78
129
  end
79
130
 
80
131
  private
81
132
 
82
- # Builds schema from parsed AST nodes
83
- # @param ast [Array<Hash>] Array of parsed AST nodes
84
- # @return [Hash] OpenAPI schema definition
85
- def build_schema(ast)
86
- # Check if this is an array response (json.array!)
87
- if ast.any? { |node| node[:is_array_root] }
88
- return build_array_schema(ast)
89
- end
90
-
91
- schema = { "type" => "object", "properties" => {}, "required" => [] }
92
-
93
- ast.each do |node|
94
- property = node[:property]
95
- comment_data = node[:comment_data] || {}
96
-
97
- # Skip array root items as they're handled separately
98
- next if node[:is_array_root]
99
-
100
- property_schema = build_property_schema(node)
101
-
102
- schema["properties"][property] = property_schema
103
- # Don't mark conditional properties as required, even if they have required:true
104
- if comment_data[:required] == "true" && !node[:is_conditional]
105
- schema["required"] << property
106
- end
107
- end
133
+ # Builds OpenAPI schema from AST properties array
134
+ # @param properties [Array<Hash>] Array of property hashes with property name and comment_data
135
+ # @return [Hash] OpenAPI schema object
136
+ def build_schema(properties)
137
+ schema = {
138
+ "type" => "object",
139
+ "properties" => {},
140
+ "required" => []
141
+ }
108
142
 
109
- schema
110
- end
143
+ properties.each do |prop|
144
+ property_name = prop[:property] || prop["property"]
145
+ comment_data = prop[:comment_data] || prop["comment_data"]
146
+ is_conditional = prop[:is_conditional] || prop["is_conditional"]
147
+
148
+ next unless property_name
111
149
 
112
- # Builds array schema from AST nodes containing json.array!
113
- # @param ast [Array<Hash>] Array of parsed AST nodes
114
- # @return [Hash] OpenAPI array schema definition
115
- def build_array_schema(ast)
116
- # For json.array! responses, return array schema
117
- item_properties = {}
118
- required_fields = []
119
-
120
- # Find the array root node to check for array_item_properties
121
- array_root_node = ast.find { |node| node[:is_array_root] }
122
-
123
- if array_root_node && array_root_node[:array_item_properties]
124
- # Use properties from the parsed partial
125
- array_root_node[:array_item_properties].each do |node|
126
- property = node[:property]
127
- comment_data = node[:comment_data] || {}
128
-
129
- property_schema = build_property_schema(node)
130
- item_properties[property] = property_schema
131
- if comment_data[:required] == "true" && !node[:is_conditional]
132
- required_fields << property
150
+ if comment_data
151
+ property_schema = build_property_schema(prop)
152
+ schema["properties"][property_name] = property_schema
153
+
154
+ # Add to required unless explicitly marked as not required OR is conditional
155
+ required = comment_data[:required] || comment_data["required"]
156
+ unless required == false || required == "false" || is_conditional
157
+ schema["required"] << property_name
133
158
  end
134
- end
135
- else
136
- # Fall back to looking for non-root properties
137
- ast.each do |node|
138
- next if node[:is_array_root]
139
-
140
- property = node[:property]
141
- comment_data = node[:comment_data] || {}
142
-
143
- property_schema = build_property_schema(node)
144
- item_properties[property] = property_schema
145
- if comment_data[:required] == "true" && !node[:is_conditional]
146
- required_fields << property
159
+ else
160
+ # Handle missing comments
161
+ schema["properties"][property_name] = {
162
+ "type" => "string",
163
+ "description" => "TODO: MISSING COMMENT"
164
+ }
165
+ # Don't add conditional properties to required even if they have missing comments
166
+ unless is_conditional
167
+ schema["required"] << property_name
147
168
  end
148
169
  end
149
170
  end
150
171
 
151
- {
152
- "type" => "array",
153
- "items" => {
154
- "type" => "object",
155
- "properties" => item_properties,
156
- "required" => required_fields
157
- }
158
- }
172
+ schema
159
173
  end
160
174
 
161
- # Builds property schema from a single AST node
162
- # @param node [Hash] Parsed AST node containing property information
163
- # @return [Hash] OpenAPI property schema
164
- def build_property_schema(node)
165
- comment_data = node[:comment_data] || {}
166
- property_schema = {}
167
-
168
- # Handle different property types
169
- if node[:is_object] || node[:is_nested]
170
- property_schema["type"] = "object"
171
-
172
- # Build nested properties if they exist
173
- if node[:nested_properties] && !node[:nested_properties].empty?
174
- nested_schema = build_nested_object_schema(node[:nested_properties])
175
- property_schema["properties"] = nested_schema["properties"]
176
- property_schema["required"] = nested_schema["required"] if nested_schema["required"] && !nested_schema["required"].empty?
177
- end
178
- elsif node[:is_array]
179
- property_schema["type"] = "array"
180
-
181
- if node[:array_item_properties] && !node[:array_item_properties].empty?
182
- # Build items schema from array iteration block
183
- items_schema = build_nested_object_schema(node[:array_item_properties])
184
- items_def = {
185
- "type" => "object",
186
- "properties" => items_schema["properties"]
187
- }
188
- items_def["required"] = items_schema["required"] if items_schema["required"] && !items_schema["required"].empty?
189
- property_schema["items"] = items_def
190
- elsif comment_data[:items]
191
- # Use specified items type from comment
192
- property_schema["items"] = { "type" => comment_data[:items] }
193
- else
194
- # Default to object items
195
- property_schema["items"] = { "type" => "object" }
196
- end
197
- elsif comment_data[:type] && comment_data[:type] != "TODO: MISSING COMMENT"
198
- property_schema["type"] = comment_data[:type]
199
-
200
- # Handle array types
201
- if comment_data[:type] == "array"
202
- if comment_data[:items]
203
- # Use specified items type
204
- property_schema["items"] = { "type" => comment_data[:items] }
205
- else
206
- # Default to string items if no items type is specified
207
- property_schema["items"] = { "type" => "string" }
208
- end
209
- end
210
- else
211
- # Only show TODO message if no @openapi comment exists at all
212
- property_schema["type"] = "string"
213
- if comment_data.nil? || comment_data.empty?
214
- property_schema["description"] = "TODO: MISSING COMMENT - Add @openapi comment"
215
- end
175
+ # Builds property schema for a single property
176
+ # @param prop [Hash] Property hash with comment_data and nested properties
177
+ # @return [Hash] Property schema
178
+ def build_property_schema(prop)
179
+ comment_data = prop[:comment_data] || prop["comment_data"]
180
+ property_type = comment_data[:type] || comment_data["type"] || "string"
181
+
182
+ property_schema = {
183
+ "type" => property_type
184
+ }
185
+
186
+ # Add description if present
187
+ if comment_data[:description] || comment_data["description"]
188
+ property_schema["description"] = comment_data[:description] || comment_data["description"]
216
189
  end
217
190
 
218
- # Add common properties
219
- property_schema["description"] = comment_data[:description] if comment_data[:description]
220
- property_schema["enum"] = comment_data[:enum] if comment_data[:enum]
191
+ # Add enum if present
192
+ if comment_data[:enum] || comment_data["enum"]
193
+ property_schema["enum"] = comment_data[:enum] || comment_data["enum"]
194
+ end
221
195
 
222
- property_schema
223
- end
224
-
225
- # Builds schema for nested object properties
226
- # @param nested_properties [Array<Hash>] Array of nested property nodes
227
- # @return [Hash] Schema with properties and required fields
228
- def build_nested_object_schema(nested_properties)
229
- schema = { "properties" => {}, "required" => [] }
230
-
231
- nested_properties.each do |node|
232
- property = node[:property]
233
- comment_data = node[:comment_data] || {}
234
-
235
- property_schema = build_property_schema(node)
236
- schema["properties"][property] = property_schema
237
- if comment_data[:required] == "true" && !node[:is_conditional]
238
- schema["required"] << property
196
+ # Handle nested object properties
197
+ if property_type == "object" && (prop[:nested_properties] || prop["nested_properties"])
198
+ nested_properties = prop[:nested_properties] || prop["nested_properties"]
199
+ nested_schema = build_schema(nested_properties)
200
+ property_schema["properties"] = nested_schema["properties"]
201
+
202
+ # Only add required array if there are non-conditional required properties
203
+ if nested_schema["required"] && !nested_schema["required"].empty?
204
+ property_schema["required"] = nested_schema["required"]
239
205
  end
240
206
  end
241
-
242
- schema
207
+
208
+ property_schema
243
209
  end
244
- end
245
210
 
246
- class Checker
247
- # Runs checks for missing comments and uncommitted changes
248
- # @return [void]
249
- def run
250
- system("bin/rails openapi:generate")
251
-
252
- missing_comments = `grep -r "TODO: MISSING COMMENT" openapi/`.strip
253
- unless missing_comments.empty?
254
- puts "❌ Missing @openapi comments found!"
255
- exit 1
211
+ # Builds array schema from properties containing array root
212
+ # @param properties [Array<Hash>] Array of property hashes
213
+ # @return [Hash] Array schema object
214
+ def build_array_schema(properties)
215
+ array_root = properties.find { |p| p[:is_array_root] || p["is_array_root"] }
216
+
217
+ unless array_root
218
+ raise ArgumentError, "No array root property found in properties"
256
219
  end
257
220
 
258
- diff = `git diff --name-only openapi/`.strip
259
- unless diff.empty?
260
- puts "❌ OpenAPI spec has uncommitted changes!"
261
- exit 1
221
+ schema = {
222
+ "type" => "array"
223
+ }
224
+
225
+ # Build items schema from array_item_properties
226
+ array_item_properties = array_root[:array_item_properties] || array_root["array_item_properties"]
227
+ if array_item_properties && !array_item_properties.empty?
228
+ items_schema = build_schema(array_item_properties)
229
+ schema["items"] = items_schema
230
+ else
231
+ schema["items"] = { "type" => "object" }
262
232
  end
263
233
 
264
- puts "✅ OpenAPI spec is up to date!"
234
+ schema
265
235
  end
236
+
237
+ # NOTE: Old hash-based processing methods have been removed
238
+ # The system now uses AST-based processing with AstToSchemaProcessor
266
239
  end
267
- end
240
+ end
@@ -4,59 +4,58 @@ namespace :openapi do
4
4
  desc "Import OpenAPI specification and add @openapi comments to Jbuilder files"
5
5
  task :import, [:openapi_file] => :environment do |_task, args|
6
6
  openapi_file = args[:openapi_file]
7
-
7
+
8
8
  # Set debug mode if verbose flag is passed
9
9
  ENV['DEBUG_OPENAPI_IMPORT'] = '1' if ENV['VERBOSE'] == 'true'
10
-
10
+
11
11
  RailsOpenapiGen.import(openapi_file)
12
12
  end
13
-
13
+
14
14
  desc "Diagnose import issues by comparing OpenAPI paths with Rails routes"
15
15
  task :diagnose_import, [:openapi_file] => :environment do |_task, args|
16
- openapi_file = args[:openapi_file] || File.join(RailsOpenapiGen.configuration.output_directory,
17
- RailsOpenapiGen.configuration.output_filename)
18
-
16
+ openapi_file = args[:openapi_file] || File.join(RailsOpenapiGen.configuration.output_directory,
17
+ RailsOpenapiGen.configuration.output_filename)
18
+
19
19
  unless File.exist?(openapi_file)
20
20
  puts "❌ OpenAPI file not found: #{openapi_file}"
21
21
  exit 1
22
22
  end
23
-
23
+
24
24
  puts "🔍 Diagnosing OpenAPI import for: #{openapi_file}"
25
25
  puts ""
26
-
26
+
27
27
  # Load OpenAPI spec
28
28
  openapi_spec = YAML.load_file(openapi_file)
29
29
  openapi_paths = openapi_spec['paths'] || {}
30
-
30
+
31
31
  # Get Rails routes
32
32
  routes_parser = RailsOpenapiGen::Parsers::RoutesParser.new
33
33
  rails_routes = routes_parser.parse
34
-
35
- # Group routes by path for easier lookup
36
- routes_by_path = rails_routes.group_by { |r| r[:path] }
37
-
34
+
35
+ # NOTE: routes_by_path grouping removed as it's not used in current implementation
36
+
38
37
  puts "📊 Summary:"
39
38
  puts " OpenAPI paths: #{openapi_paths.size}"
40
39
  puts " Rails routes: #{rails_routes.size}"
41
40
  puts ""
42
-
41
+
43
42
  # Check each OpenAPI path
44
43
  unmatched_paths = []
45
44
  matched_paths = []
46
-
45
+
47
46
  openapi_paths.each do |path, methods|
48
- methods.each do |method, operation|
47
+ methods.each do |method, _operation|
49
48
  next if method == 'parameters'
50
-
49
+
51
50
  # Convert OpenAPI path to Rails format
52
51
  rails_path = path.gsub(/\{(\w+)\}/, ':\1')
53
-
52
+
54
53
  # Try to find matching route
55
54
  matching_routes = rails_routes.select do |route|
56
- route[:method] == method.upcase &&
57
- route[:path].gsub(/\/$/, '') == rails_path.gsub(/\/$/, '')
55
+ route[:method] == method.upcase &&
56
+ route[:path].gsub(%r{/$}, '') == rails_path.gsub(%r{/$}, '')
58
57
  end
59
-
58
+
60
59
  if matching_routes.empty?
61
60
  unmatched_paths << { path: path, method: method.upcase, rails_path: rails_path }
62
61
  else
@@ -64,7 +63,7 @@ namespace :openapi do
64
63
  end
65
64
  end
66
65
  end
67
-
66
+
68
67
  # Report results
69
68
  if matched_paths.any?
70
69
  puts "✅ Matched paths (#{matched_paths.size}):"
@@ -76,35 +75,35 @@ namespace :openapi do
76
75
  end
77
76
  puts ""
78
77
  end
79
-
78
+
80
79
  if unmatched_paths.any?
81
80
  puts "❌ Unmatched OpenAPI paths (#{unmatched_paths.size}):"
82
81
  unmatched_paths.each do |unmatched|
83
82
  puts " #{unmatched[:method]} #{unmatched[:path]}"
84
-
83
+
85
84
  # Suggest similar routes
86
85
  similar_routes = rails_routes.select do |route|
87
- route[:method] == unmatched[:method] &&
88
- route[:path].include?(unmatched[:rails_path].split('/').reject(&:empty?).first.to_s)
86
+ route[:method] == unmatched[:method] &&
87
+ route[:path].include?(unmatched[:rails_path].split('/').reject(&:empty?).first.to_s)
89
88
  end
90
-
91
- if similar_routes.any?
92
- puts " Did you mean one of these?"
93
- similar_routes.take(3).each do |route|
94
- puts " - #{route[:path]}"
95
- end
89
+
90
+ next unless similar_routes.any?
91
+
92
+ puts " Did you mean one of these?"
93
+ similar_routes.take(3).each do |route|
94
+ puts " - #{route[:path]}"
96
95
  end
97
96
  end
98
97
  puts ""
99
98
  end
100
-
99
+
101
100
  # Check for Jbuilder templates
102
101
  puts "📄 Checking Jbuilder templates:"
103
102
  matched_paths.each do |match|
104
103
  route = match[:route]
105
104
  controller_parser = RailsOpenapiGen::Parsers::ControllerParser.new(route)
106
105
  controller_info = controller_parser.parse
107
-
106
+
108
107
  if controller_info[:jbuilder_path]
109
108
  if File.exist?(controller_info[:jbuilder_path])
110
109
  puts " ✅ #{match[:method]} #{match[:path]} → #{controller_info[:jbuilder_path]}"
@@ -115,7 +114,7 @@ namespace :openapi do
115
114
  puts " ⚠️ #{match[:method]} #{match[:path]} → No Jbuilder template found"
116
115
  end
117
116
  end
118
-
117
+
119
118
  puts ""
120
119
  puts "💡 Tips:"
121
120
  puts " - Ensure your OpenAPI paths match your Rails routes exactly"
@@ -123,4 +122,4 @@ namespace :openapi do
123
122
  puts " - Create Jbuilder templates for API endpoints that need them"
124
123
  puts " - Run 'DEBUG_OPENAPI_IMPORT=1 rake openapi:import[#{openapi_file}]' for detailed import logs"
125
124
  end
126
- end
125
+ end
@@ -2,7 +2,7 @@
2
2
 
3
3
  Gem::Specification.new do |spec|
4
4
  spec.name = "rails-openapi-gen"
5
- spec.version = "0.0.2"
5
+ spec.version = "0.0.3"
6
6
  spec.authors = ["myzkey"]
7
7
  spec.email = ["myzkey.dev@example.com"]
8
8
 
@@ -10,7 +10,7 @@ Gem::Specification.new do |spec|
10
10
  spec.description = "Generates OpenAPI specs from Rails apps by parsing routes, controllers, and view templates with @openapi comment annotations"
11
11
  spec.homepage = "https://github.com/myzkey/rails-openapi-gen"
12
12
  spec.license = "MIT"
13
- spec.required_ruby_version = ">= 2.7.0"
13
+ spec.required_ruby_version = ">= 3.0.0"
14
14
 
15
15
  spec.metadata["homepage_uri"] = spec.homepage
16
16
  spec.metadata["source_code_uri"] = spec.homepage
@@ -20,11 +20,12 @@ Gem::Specification.new do |spec|
20
20
  spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
21
21
  spec.require_paths = ["lib"]
22
22
 
23
+ spec.add_dependency "parser", "~> 3.1.0"
23
24
  spec.add_dependency "rails", ">= 6.0"
24
- spec.add_dependency "parser", "~> 3.2"
25
25
  spec.add_dependency "yaml", "~> 0.2"
26
26
 
27
+ spec.add_development_dependency "rake", "~> 13.0"
27
28
  spec.add_development_dependency "rspec", "~> 3.12"
28
29
  spec.add_development_dependency "rubocop", "~> 1.50"
29
- spec.add_development_dependency "rake", "~> 13.0"
30
- end
30
+ spec.metadata['rubygems_mfa_required'] = 'true'
31
+ end