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
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_processor'
4
+ require_relative 'array_processor'
5
+ require_relative 'object_processor'
6
+ require_relative 'property_processor'
7
+ require_relative 'partial_processor'
8
+ require_relative '../call_detectors'
9
+
10
+ module RailsOpenapiGen::Parsers::Jbuilder::Processors
11
+ class CompositeProcessor < BaseProcessor
12
+ # Alias for shorter reference to call detectors
13
+ CallDetectors = RailsOpenapiGen::Parsers::Jbuilder::CallDetectors
14
+ # Initializes composite processor with all sub-processors
15
+ # @param file_path [String] Path to current file
16
+ # @param property_parser [PropertyCommentParser] Parser for property comments
17
+ def initialize(file_path, property_parser)
18
+ super(file_path, property_parser)
19
+
20
+ # Initialize sub-processors
21
+ @array_processor = ArrayProcessor.new(file_path, property_parser)
22
+ @object_processor = ObjectProcessor.new(file_path, property_parser)
23
+ @property_processor = PropertyProcessor.new(file_path, property_parser)
24
+ @partial_processor = PartialProcessor.new(file_path, property_parser)
25
+ end
26
+
27
+ # Processes method call nodes by delegating to appropriate processors
28
+ # @param node [Parser::AST::Node] Method call node
29
+ # @return [void]
30
+ def on_send(node)
31
+ receiver, method_name, = node.children
32
+
33
+ # Skip Jbuilder helper methods - they are not JSON properties
34
+ if CallDetectors::CacheCallDetector.cache_call?(receiver, method_name) ||
35
+ CallDetectors::CacheCallDetector.cache_if_call?(receiver, method_name) ||
36
+ CallDetectors::KeyFormatDetector.key_format?(receiver, method_name) ||
37
+ CallDetectors::NullHandlingDetector.null_handling?(receiver, method_name) ||
38
+ CallDetectors::ObjectManipulationDetector.object_manipulation?(receiver, method_name)
39
+ super
40
+ elsif CallDetectors::ArrayCallDetector.array_call?(receiver, method_name)
41
+ @array_processor.on_send(node)
42
+ merge_processor_results(@array_processor)
43
+ elsif CallDetectors::PartialCallDetector.partial_call?(receiver, method_name)
44
+ @partial_processor.on_send(node)
45
+ merge_processor_results(@partial_processor)
46
+ elsif CallDetectors::JsonCallDetector.json_property?(receiver, method_name)
47
+ @property_processor.on_send(node)
48
+ merge_processor_results(@property_processor)
49
+ end
50
+ end
51
+
52
+ # Processes block nodes by delegating to appropriate processors
53
+ # @param node [Parser::AST::Node] Block node
54
+ # @return [void]
55
+ def on_block(node)
56
+ send_node, args_node, body = node.children
57
+ receiver, method_name, = send_node.children
58
+
59
+ if CallDetectors::CacheCallDetector.cache_call?(receiver, method_name) ||
60
+ CallDetectors::CacheCallDetector.cache_if_call?(receiver, method_name)
61
+ # This is json.cache! or json.cache_if! block - just process the block contents
62
+ process(body) if body
63
+ elsif CallDetectors::JsonCallDetector.json_property?(receiver, method_name) && method_name != :array!
64
+ # Check if this is an array iteration block (has block arguments)
65
+ if args_node && args_node.type == :args && args_node.children.any?
66
+ # This is an array iteration block like json.tags @tags do |tag|
67
+ @array_processor.on_block(node)
68
+ merge_processor_results(@array_processor)
69
+ else
70
+ # This is a nested object block like json.profile do
71
+ @object_processor.on_block(node)
72
+ merge_processor_results(@object_processor)
73
+ end
74
+ elsif CallDetectors::ArrayCallDetector.array_call?(receiver, method_name)
75
+ # This is json.array! block
76
+ @array_processor.on_block(node)
77
+ merge_processor_results(@array_processor)
78
+ else
79
+ super
80
+ end
81
+ end
82
+
83
+ private
84
+
85
+ # Merges results from a sub-processor into this processor
86
+ # @param processor [BaseProcessor] Sub-processor to merge results from
87
+ # @return [void]
88
+ def merge_processor_results(processor)
89
+ # Use getter methods to ensure arrays exist
90
+ properties.concat(processor.properties)
91
+ partials.concat(processor.partials)
92
+
93
+ # Clear the sub-processor's results by calling private clear methods
94
+ processor.send(:clear_results)
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,176 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_processor'
4
+ require_relative '../call_detectors'
5
+
6
+ module RailsOpenapiGen::Parsers::Jbuilder::Processors
7
+ class ObjectProcessor < BaseProcessor
8
+ # Alias for shorter reference to call detectors
9
+ CallDetectors = RailsOpenapiGen::Parsers::Jbuilder::CallDetectors
10
+ # Lazy load CompositeProcessor to avoid circular dependency
11
+ def self.composite_processor_class
12
+ RailsOpenapiGen::Parsers::Jbuilder::Processors::CompositeProcessor
13
+ end
14
+
15
+ # Processes block nodes for nested object blocks
16
+ # @param node [Parser::AST::Node] Block node
17
+ # @return [void]
18
+ def on_block(node)
19
+ send_node, args_node, = node.children
20
+ receiver, method_name, = send_node.children
21
+
22
+ if CallDetectors::JsonCallDetector.json_property?(receiver, method_name) && method_name != :array!
23
+ # Check if this is a nested object block (no block arguments)
24
+ if !args_node || args_node.type != :args || args_node.children.empty?
25
+ # This is a nested object block like json.profile do
26
+ process_nested_object_block(node, method_name.to_s)
27
+ else
28
+ super
29
+ end
30
+ else
31
+ super
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ # Processes nested object blocks (e.g., json.profile do)
38
+ # @param node [Parser::AST::Node] Block node
39
+ # @param property_name [String] Object property name
40
+ # @return [void]
41
+ def process_nested_object_block(node, property_name)
42
+ comment_data = find_comment_for_node(node)
43
+
44
+ # Save current context
45
+ previous_nested_objects = @nested_objects.dup
46
+ previous_properties = properties.dup
47
+ previous_partials = partials.dup
48
+
49
+ # Create a temporary properties array for this nested object
50
+ @properties = []
51
+ @partials = []
52
+ push_block(:object)
53
+
54
+ # Process the block contents using CompositeProcessor
55
+ _, _args, body = node.children
56
+ if body
57
+ # Create a CompositeProcessor to handle all types of calls within the block
58
+ composite_processor = self.class.composite_processor_class.new(@file_path, @property_parser)
59
+ composite_processor.process(body)
60
+
61
+ # Merge results from the composite processor
62
+ @properties.concat(composite_processor.properties)
63
+ @partials.concat(composite_processor.partials)
64
+ end
65
+
66
+ # Collect nested properties from direct block processing
67
+ nested_properties = properties.dup
68
+
69
+ # Process any partials found in this block
70
+ partials.each do |partial_path|
71
+ if File.exist?(partial_path)
72
+ partial_properties = parse_partial_for_nested_object(partial_path)
73
+ nested_properties.concat(partial_properties)
74
+ end
75
+ end
76
+
77
+ # Check if this object block contains only a json.array! call
78
+ # In that case, we should treat this as a direct array instead of an object
79
+ if ENV['RAILS_OPENAPI_DEBUG'] && nested_properties.size == 1
80
+ prop = nested_properties.first
81
+ if prop.respond_to?(:property_name)
82
+ is_root = prop.respond_to?(:is_root_array) ? prop.is_root_array : false
83
+ puts "🔍 DEBUG: Checking if object contains only array - property: #{prop.property_name}, is_array_root: #{is_root}, class: #{prop.class.name}"
84
+ else
85
+ puts "🔍 DEBUG: Checking if object contains only array - property: #{prop[:property_name] || prop[:property]}, is_array_root: #{prop[:is_array_root]}, node_type: #{prop[:node_type]}"
86
+ end
87
+ end
88
+ has_only_array_root = nested_properties.size == 1 && is_array_root_property(nested_properties.first)
89
+
90
+ # Restore context but keep partials for higher level processing
91
+ @properties = previous_properties
92
+ @partials = previous_partials
93
+ @nested_objects = previous_nested_objects
94
+ pop_block
95
+
96
+ if has_only_array_root
97
+ # This is a json.property do + json.array! pattern
98
+ # Treat the property as a direct array instead of an object with items
99
+ array_root_node = nested_properties.first
100
+
101
+ # Create comment data
102
+ comment_obj = if comment_data
103
+ RailsOpenapiGen::AstNodes::CommentData.new(
104
+ type: comment_data[:type] || 'array',
105
+ items: comment_data[:items] || { type: 'object' }
106
+ )
107
+ else
108
+ RailsOpenapiGen::AstNodes::CommentData.new(type: 'array', items: { type: 'object' })
109
+ end
110
+
111
+ # Create array property node
112
+ array_item_properties = get_array_item_properties(array_root_node)
113
+ property_node = RailsOpenapiGen::AstNodes::PropertyNodeFactory.create_array(
114
+ property: property_name,
115
+ comment_data: comment_obj,
116
+ array_item_properties: array_item_properties
117
+ )
118
+ else
119
+ # Store nested object info
120
+ @nested_objects[property_name] = nested_properties
121
+
122
+ # Create comment data
123
+ comment_obj = if comment_data
124
+ RailsOpenapiGen::AstNodes::CommentData.new(
125
+ type: comment_data[:type] || 'object'
126
+ )
127
+ else
128
+ RailsOpenapiGen::AstNodes::CommentData.new(type: 'object')
129
+ end
130
+
131
+ # Add the parent property as a regular object
132
+ property_node = RailsOpenapiGen::AstNodes::PropertyNodeFactory.create_object(
133
+ property: property_name,
134
+ comment_data: comment_obj,
135
+ nested_properties: nested_properties
136
+ )
137
+ end
138
+
139
+ add_property(property_node)
140
+ end
141
+
142
+ # Helper methods for structured AST support
143
+
144
+ # Checks if a property is an array root property
145
+ # @param property [PropertyNode, Hash] Property to check
146
+ # @return [Boolean] True if it's an array root property
147
+ def is_array_root_property(property)
148
+ if property.is_a?(Hash)
149
+ property_name = property[:property_name] || property[:property]
150
+ (property[:is_array_root] || property[:node_type] == 'array') &&
151
+ (property_name == 'items' || property[:is_array_root])
152
+ elsif property.respond_to?(:is_root_array)
153
+ # Structured AST node
154
+ property.is_root_array ||
155
+ (property.respond_to?(:property_name) && property.property_name == 'items')
156
+ else
157
+ property.is_a?(RailsOpenapiGen::AstNodes::ArrayRootNode) ||
158
+ (property.is_a?(RailsOpenapiGen::AstNodes::ArrayPropertyNode) && property.property == 'items')
159
+ end
160
+ end
161
+
162
+ # Gets array item properties from an array root node
163
+ # @param array_node [PropertyNode, Hash] Array root node
164
+ # @return [Array] Array of item properties
165
+ def get_array_item_properties(array_node)
166
+ if array_node.is_a?(Hash)
167
+ array_node[:array_item_properties] || []
168
+ elsif array_node.respond_to?(:children)
169
+ # For ArrayNode, the children are the item properties
170
+ array_node.children || []
171
+ else
172
+ array_node.array_item_properties || []
173
+ end
174
+ end
175
+ end
176
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_processor'
4
+ require_relative '../call_detectors'
5
+
6
+ module RailsOpenapiGen::Parsers::Jbuilder::Processors
7
+ class PartialProcessor < BaseProcessor
8
+ # Alias for shorter reference to call detectors
9
+ CallDetectors = RailsOpenapiGen::Parsers::Jbuilder::CallDetectors
10
+ # Processes method call nodes for partial render calls
11
+ # @param node [Parser::AST::Node] Method call node
12
+ # @return [void]
13
+ def on_send(node)
14
+ receiver, method_name, *args = node.children
15
+
16
+ process_partial(args) if RailsOpenapiGen::Parsers::Jbuilder::CallDetectors::PartialCallDetector.partial_call?(
17
+ receiver, method_name
18
+ )
19
+
20
+ super
21
+ end
22
+
23
+ private
24
+
25
+ # Processes partial render calls to track dependencies
26
+ # @param args [Array] Partial call arguments
27
+ # @return [void]
28
+ def process_partial(args)
29
+ return if args.empty?
30
+
31
+ partial_name = extract_partial_name(args)
32
+ return unless partial_name
33
+
34
+ puts "🔍 DEBUG: Found partial: #{partial_name}" if ENV['RAILS_OPENAPI_DEBUG']
35
+ partial_path = resolve_partial_path(partial_name)
36
+ puts "🔍 DEBUG: Resolved partial path: #{partial_path}" if ENV['RAILS_OPENAPI_DEBUG']
37
+ puts "🔍 DEBUG: Partial exists: #{File.exist?(partial_path)}" if ENV['RAILS_OPENAPI_DEBUG'] && partial_path
38
+ @partials << partial_path if partial_path
39
+ end
40
+
41
+ # Extracts partial name from arguments (handles both string and hash syntax)
42
+ # @param args [Array] Partial call arguments
43
+ # @return [String, nil] Partial name or nil
44
+ def extract_partial_name(args)
45
+ first_arg = args.first
46
+
47
+ # Handle simple string case: json.partial! 'path/to/partial'
48
+ if first_arg.type == :str
49
+ return first_arg.children.first
50
+ end
51
+
52
+ # Handle hash case: json.partial! partial: 'path/to/partial', locals: {...}
53
+ if first_arg.type == :hash
54
+ first_arg.children.each do |pair|
55
+ next unless pair&.type == :pair
56
+
57
+ key, value = pair.children
58
+ next unless key && value
59
+
60
+ if key.type == :sym && key.children.first == :partial && value.type == :str
61
+ return value.children.first
62
+ end
63
+ end
64
+ end
65
+
66
+ nil
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_processor'
4
+ require_relative '../call_detectors'
5
+
6
+ module RailsOpenapiGen::Parsers::Jbuilder::Processors
7
+ class PropertyProcessor < BaseProcessor
8
+ # Alias for shorter reference to call detectors
9
+ CallDetectors = RailsOpenapiGen::Parsers::Jbuilder::CallDetectors
10
+ # Processes method call nodes for simple property assignments
11
+ # @param node [Parser::AST::Node] Method call node
12
+ # @return [void]
13
+ def on_send(node)
14
+ receiver, method_name, *args = node.children
15
+
16
+ if CallDetectors::JsonCallDetector.json_property?(receiver, method_name)
17
+ process_json_property(node, method_name.to_s, args)
18
+ else
19
+ # For non-JSON property calls, don't process anything to avoid creating
20
+ # properties for variable access calls
21
+ super
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ # Processes a simple JSON property assignment
28
+ # @param node [Parser::AST::Node] Property node
29
+ # @param property_name [String] Name of the property
30
+ # @param args [Array] Method arguments
31
+ # @return [void]
32
+ def process_json_property(node, property_name, _args)
33
+ comment_data = find_comment_for_node(node)
34
+
35
+ # Process simple property regardless of context
36
+ # Note: In the future, different processing could be added based on context
37
+ process_simple_property(node, property_name, comment_data)
38
+ end
39
+
40
+ # Processes a simple property assignment
41
+ # @param node [Parser::AST::Node] Property node
42
+ # @param property_name [String] Name of the property
43
+ # @param comment_data [Hash, nil] Parsed comment data
44
+ # @return [void]
45
+ def process_simple_property(_node, property_name, comment_data)
46
+ # Create comment data object
47
+ comment_obj = if comment_data && !comment_data.empty?
48
+ RailsOpenapiGen::AstNodes::CommentData.new(
49
+ type: comment_data[:type],
50
+ description: comment_data[:description],
51
+ required: comment_data[:required],
52
+ enum: comment_data[:enum],
53
+ field_name: comment_data[:field_name]
54
+ )
55
+ else
56
+ RailsOpenapiGen::AstNodes::CommentData.new(type: 'TODO: MISSING COMMENT')
57
+ end
58
+
59
+ # Create simple property node
60
+ property_node = RailsOpenapiGen::AstNodes::PropertyNodeFactory.create_simple(
61
+ property: property_name,
62
+ comment_data: comment_obj
63
+ )
64
+
65
+ add_property(property_node)
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsOpenapiGen::Parsers::Jbuilder::Processors
4
+ autoload :BaseProcessor, "rails-openapi-gen/parsers/jbuilder/processors/base_processor"
5
+ autoload :CompositeProcessor, "rails-openapi-gen/parsers/jbuilder/processors/composite_processor"
6
+ autoload :ArrayProcessor, "rails-openapi-gen/parsers/jbuilder/processors/array_processor"
7
+ autoload :ObjectProcessor, "rails-openapi-gen/parsers/jbuilder/processors/object_processor"
8
+ autoload :PropertyProcessor, "rails-openapi-gen/parsers/jbuilder/processors/property_processor"
9
+ autoload :PartialProcessor, "rails-openapi-gen/parsers/jbuilder/processors/partial_processor"
10
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../comment_parser'
4
+
5
+ module RailsOpenapiGen::Parsers::Jbuilder
6
+ class PropertyCommentParser
7
+ # Initializes property comment parser
8
+ # @param comments [Array] Array of comment objects
9
+ def initialize(comments)
10
+ @comments = comments
11
+ @comment_parser = RailsOpenapiGen::Parsers::CommentParser.new
12
+ end
13
+
14
+ # Finds property comment for a specific line
15
+ # @param line_number [Integer] Line number to find comment for
16
+ # @return [Hash, nil] Parsed comment data or nil
17
+ def find_property_comment_for_line(line_number)
18
+ @comments.reverse.find do |comment|
19
+ comment_line = comment.location.line
20
+ comment_line == line_number - 1 || comment_line == line_number
21
+ end&.then do |comment|
22
+ @comment_parser.parse(comment.text)
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsOpenapiGen::Parsers::Jbuilder
4
+ autoload :JbuilderParser, "rails-openapi-gen/parsers/jbuilder/jbuilder_parser"
5
+ autoload :AstParser, "rails-openapi-gen/parsers/jbuilder/ast_parser"
6
+ autoload :CallDetectors, "rails-openapi-gen/parsers/jbuilder/call_detectors"
7
+ autoload :Processors, "rails-openapi-gen/parsers/jbuilder/processors"
8
+ autoload :OperationCommentParser, "rails-openapi-gen/parsers/jbuilder/operation_comment_parser"
9
+ autoload :PropertyCommentParser, "rails-openapi-gen/parsers/jbuilder/property_comment_parser"
10
+ end
@@ -1,33 +1,107 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'active_support/inflector'
4
+
3
5
  module RailsOpenapiGen
4
6
  module Parsers
5
7
  class RoutesParser
8
+ # Initialize with optional file existence checker for testability
9
+ # @param file_checker [#call] Callable that checks if file exists (defaults to File.exist?)
10
+ def initialize(file_checker: File.method(:exist?))
11
+ @file_checker = file_checker
12
+ end
13
+
6
14
  # Parses Rails application routes to extract route information
7
15
  # @return [Array<Hash>] Array of route hashes with method, path, controller, action, and name
8
16
  def parse
9
17
  routes = []
10
-
18
+
11
19
  Rails.application.routes.routes.each do |route|
12
20
  next unless route.defaults[:controller] && route.defaults[:action]
13
21
  next if route.respond_to?(:internal?) ? route.internal? : route.instance_variable_get(:@internal)
14
-
15
- method = route.verb.is_a?(Array) ? route.verb.first : route.verb
16
- path = route.path.spec.to_s.gsub(/\(\.:format\)$/, "")
17
- controller = route.defaults[:controller]
22
+
23
+ # Skip Rails internal and asset routes
24
+ controller_name = route.defaults[:controller]
25
+ next if controller_name.to_s.start_with?('rails/')
26
+ next if controller_name.to_s == 'assets'
27
+
28
+ # Extract HTTP method from route.verb (which can be a Regexp like /^GET$/)
29
+ raw_method = route.verb.is_a?(Array) ? route.verb.first : route.verb
30
+ method = if raw_method.is_a?(Regexp)
31
+ raw_method.source.gsub(/[\^$()?\-:mix]/, '')
32
+ else
33
+ raw_method.to_s
34
+ end
35
+ # Remove format suffix patterns more robustly
36
+ path = route.path.spec.to_s
37
+ .gsub(/\(\.:format\)$/, "") # Standard format pattern
38
+ .gsub(/\(\.\*format\)$/, "") # Wildcard format pattern
39
+ .gsub(/\(\.[\w|*]*\)$/, "") # Complex format patterns like (.json|.xml|.csv)
40
+ .gsub(/\(\.[^)]*\)$/, "")
41
+ controller = infer_controller_from_route(route)
18
42
  action = route.defaults[:action]
19
-
43
+
20
44
  routes << {
21
- method: method,
45
+ verb: method, # Test expects 'verb'
46
+ method: method, # Keep for backward compatibility
22
47
  path: path,
23
48
  controller: controller,
24
49
  action: action,
25
50
  name: route.name
26
51
  }
27
52
  end
28
-
53
+
29
54
  routes
30
55
  end
56
+
57
+ private
58
+
59
+ # Infers the correct controller name from route information
60
+ # Uses route name pattern to identify nested resources
61
+ # @param route [ActionDispatch::Journey::Route] Rails route object
62
+ # @return [String] Controller name
63
+ def infer_controller_from_route(route)
64
+ default_controller = route.defaults[:controller]
65
+ route_name = route.name
66
+
67
+ # If route name indicates a nested resource, use it to infer the controller
68
+ if route_name && route_name.include?('_')
69
+ # Parse route name to extract controller path
70
+ # Example: "api_user_orders" -> "api/users/orders"
71
+ parts = route_name.split('_')
72
+
73
+ # Remove action suffix if present (index, show, create, etc.)
74
+ action = route.defaults[:action]
75
+ if action && parts.last == action
76
+ parts.pop
77
+ end
78
+
79
+ # Convert route name parts to controller path
80
+ if parts.length > 1
81
+ # Check if this looks like a nested resource
82
+ # api_user_orders -> api/users/orders
83
+ # api_post_comments -> api/posts/comments
84
+ potential_nested = parts.map.with_index do |part, index|
85
+ if index == 0 || index == parts.length - 1
86
+ part # Keep namespace and final resource as-is (e.g., "api", "orders")
87
+ else
88
+ # Skip pluralization for version numbers (v1, v2, etc.)
89
+ part.match?(/^v\d+$/) ? part : part.pluralize
90
+ end
91
+ end.join('/')
92
+
93
+ # Check if the nested controller file exists
94
+ nested_controller_path = Rails.root.join("app", "controllers", "#{potential_nested}_controller.rb")
95
+
96
+ if @file_checker.call(nested_controller_path.to_s)
97
+ return potential_nested
98
+ end
99
+ end
100
+ end
101
+
102
+ # Fall back to default controller name
103
+ default_controller
104
+ end
31
105
  end
32
106
  end
33
- end
107
+ end