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,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsOpenapiGen::Parsers::Jbuilder::CallDetectors
4
+ autoload :BaseDetector, "rails-openapi-gen/parsers/jbuilder/call_detectors/base_detector"
5
+ autoload :JsonCallDetector, "rails-openapi-gen/parsers/jbuilder/call_detectors/json_call_detector"
6
+ autoload :ArrayCallDetector, "rails-openapi-gen/parsers/jbuilder/call_detectors/array_call_detector"
7
+ autoload :PartialCallDetector, "rails-openapi-gen/parsers/jbuilder/call_detectors/partial_call_detector"
8
+ autoload :CacheCallDetector, "rails-openapi-gen/parsers/jbuilder/call_detectors/cache_call_detector"
9
+ autoload :KeyFormatDetector, "rails-openapi-gen/parsers/jbuilder/call_detectors/key_format_detector"
10
+ autoload :NullHandlingDetector, "rails-openapi-gen/parsers/jbuilder/call_detectors/null_handling_detector"
11
+ autoload :ObjectManipulationDetector, "rails-openapi-gen/parsers/jbuilder/call_detectors/object_manipulation_detector"
12
+
13
+ # Registry for managing all call detectors
14
+ class DetectorRegistry
15
+ class << self
16
+ # Get all available detectors sorted by priority
17
+ # @return [Array<Class>] Detector classes sorted by priority (high to low)
18
+ def all_detectors
19
+ @all_detectors ||= [
20
+ ArrayCallDetector,
21
+ PartialCallDetector,
22
+ JsonCallDetector,
23
+ CacheCallDetector
24
+ ].sort_by { |detector| -detector.priority }
25
+ end
26
+
27
+ # Find the appropriate detector for a method call
28
+ # @param receiver [Parser::AST::Node, nil] Method receiver
29
+ # @param method_name [Symbol] Method name
30
+ # @param args [Array<Parser::AST::Node>] Method arguments
31
+ # @return [Class, nil] Appropriate detector class or nil
32
+ def find_detector(receiver, method_name, args = [])
33
+ if ENV['RAILS_OPENAPI_DEBUG']
34
+ puts "🔍 DEBUG: DetectorRegistry.find_detector called with method: #{method_name}"
35
+ puts "🔍 DEBUG: Available detectors: #{all_detectors.map(&:name)}"
36
+ end
37
+
38
+ result = all_detectors.find do |detector|
39
+ handles = detector.handles?(receiver, method_name, args)
40
+ if ENV['RAILS_OPENAPI_DEBUG']
41
+ puts "🔍 DEBUG: #{detector.name}.handles?(#{receiver}, #{method_name}, #{args}) = #{handles}"
42
+ end
43
+ handles
44
+ end
45
+
46
+ if ENV['RAILS_OPENAPI_DEBUG']
47
+ puts "🔍 DEBUG: Found detector: #{result ? result.name : 'nil'}"
48
+ end
49
+
50
+ result
51
+ end
52
+
53
+ # Get detectors by category
54
+ # @param category [Symbol] Detector category
55
+ # @return [Array<Class>] Detectors in the category
56
+ def by_category(category)
57
+ all_detectors.select { |detector| detector.category == category }
58
+ end
59
+
60
+ # Get all available categories
61
+ # @return [Array<Symbol>] Available categories
62
+ def categories
63
+ all_detectors.map(&:category).uniq
64
+ end
65
+
66
+ # Add a custom detector to the registry
67
+ # @param detector_class [Class] Detector class to add
68
+ # @return [void]
69
+ def register(detector_class)
70
+ return unless detector_class < BaseDetector
71
+
72
+ @all_detectors = nil # Reset cache
73
+ all_detectors << detector_class unless all_detectors.include?(detector_class)
74
+ @all_detectors = all_detectors.sort_by { |detector| -detector.priority }
75
+ end
76
+
77
+ # Remove a detector from the registry
78
+ # @param detector_class [Class] Detector class to remove
79
+ # @return [void]
80
+ def unregister(detector_class)
81
+ @all_detectors = nil # Reset cache
82
+ all_detectors.delete(detector_class)
83
+ end
84
+
85
+ # Check if a method call is handled by any detector
86
+ # @param receiver [Parser::AST::Node, nil] Method receiver
87
+ # @param method_name [Symbol] Method name
88
+ # @param args [Array<Parser::AST::Node>] Method arguments
89
+ # @return [Boolean] True if any detector handles the call
90
+ def handles?(receiver, method_name, args = [])
91
+ !find_detector(receiver, method_name, args).nil?
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "parser/current"
4
+ require "ostruct"
5
+ require_relative "operation_comment_parser"
6
+ require_relative "property_comment_parser"
7
+ require_relative "processors"
8
+ require_relative "ast_parser"
9
+
10
+ module RailsOpenapiGen::Parsers::Jbuilder
11
+ class JbuilderParser
12
+ attr_reader :jbuilder_path, :ast_parser
13
+
14
+ # Initializes Jbuilder parser with template path
15
+ # @param jbuilder_path [String] Path to Jbuilder template file
16
+ def initialize(jbuilder_path)
17
+ @jbuilder_path = jbuilder_path
18
+ @properties = []
19
+ @operation_info = nil
20
+ @parsed_files = Set.new
21
+ @operation_parser = nil
22
+ @property_parser = nil
23
+ @ast_parser = nil
24
+ end
25
+
26
+ # Main parsing method using AST-based architecture
27
+ # @return [RailsOpenapiGen::AstNodes::BaseNode] Root AST node
28
+ def parse
29
+ @ast_parser = AstParser.new(jbuilder_path)
30
+
31
+ return nil unless File.exist?(jbuilder_path)
32
+
33
+ @ast_parser.parse
34
+ end
35
+
36
+ # Alias for backward compatibility
37
+ alias parse_ast parse
38
+ end
39
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../comment_parser'
4
+
5
+ module RailsOpenapiGen::Parsers::Jbuilder
6
+ class OperationCommentParser
7
+ # Initializes operation 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
+ # Parses operation comments to extract operation information
15
+ # @return [Hash, nil] Operation information or nil if not found
16
+ def parse_operation_info
17
+ @comments.each do |comment|
18
+ parsed = @comment_parser.parse(comment.text)
19
+ if parsed&.dig(:operation)
20
+ return parsed[:operation]
21
+ end
22
+ end
23
+ nil
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,266 @@
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 ArrayProcessor < BaseProcessor
8
+ # Alias for shorter reference to call detectors
9
+ CallDetectors = RailsOpenapiGen::Parsers::Jbuilder::CallDetectors
10
+ # Alias for shorter reference to JbuilderParser
11
+ JbuilderParser = RailsOpenapiGen::Parsers::Jbuilder::JbuilderParser
12
+ # Lazy load CompositeProcessor to avoid circular dependency
13
+ def self.composite_processor_class
14
+ RailsOpenapiGen::Parsers::Jbuilder::Processors::CompositeProcessor
15
+ end
16
+
17
+ # Processes method call nodes for array-related operations
18
+ # @param node [Parser::AST::Node] Method call node
19
+ # @return [void]
20
+ def on_send(node)
21
+ receiver, method_name, *args = node.children
22
+
23
+ if CallDetectors::ArrayCallDetector.array_call?(receiver, method_name)
24
+ # Check if this is an array with partial
25
+ if args.any? && args.any? do |arg|
26
+ arg.type == :hash && CallDetectors::ArrayCallDetector.has_partial_key?(arg)
27
+ end
28
+ process_array_with_partial(node, args)
29
+ else
30
+ process_array_property(node)
31
+ end
32
+ end
33
+
34
+ super
35
+ end
36
+
37
+ # Processes block nodes for array iterations
38
+ # @param node [Parser::AST::Node] Block node
39
+ # @return [void]
40
+ def on_block(node)
41
+ send_node, args_node, = node.children
42
+ receiver, method_name, = send_node.children
43
+
44
+ if CallDetectors::ArrayCallDetector.array_call?(receiver, method_name)
45
+ # This is json.array! block
46
+ process_array_block(node)
47
+ elsif CallDetectors::JsonCallDetector.json_property?(receiver, method_name) && method_name != :array!
48
+ # Check if this is an array iteration block (has block arguments)
49
+ if args_node && args_node.type == :args && args_node.children.any?
50
+ # This is an array iteration block like json.tags @tags do |tag|
51
+ process_array_iteration_block(node, method_name.to_s)
52
+ else
53
+ super
54
+ end
55
+ else
56
+ super
57
+ end
58
+ end
59
+
60
+ private
61
+
62
+ # Processes json.array! block to create array schema with item properties
63
+ # @param node [Parser::AST::Node] Array block node
64
+ # @return [void]
65
+ def process_array_block(node)
66
+ _, _, body = node.children
67
+ comment_data = find_comment_for_node(node)
68
+
69
+ # Save current context
70
+ previous_properties = properties.dup
71
+ previous_partials = partials.dup
72
+
73
+ # Create a temporary properties array for array items
74
+ @properties = []
75
+ @partials = []
76
+ push_block(:array)
77
+
78
+ # Process the block contents using CompositeProcessor
79
+ if body
80
+ # Create a CompositeProcessor to handle all types of calls within the block
81
+ composite_processor = self.class.composite_processor_class.new(@file_path, @property_parser)
82
+ composite_processor.process(body)
83
+
84
+ # Merge results from the composite processor
85
+ @properties.concat(composite_processor.properties)
86
+ @partials.concat(composite_processor.partials)
87
+ end
88
+
89
+ # Collect item properties
90
+ item_properties = properties.dup
91
+
92
+ # Process any partials found in this block
93
+ puts "🔍 DEBUG: Found #{partials.size} partials in array block" if ENV['RAILS_OPENAPI_DEBUG']
94
+ partials.each do |partial_path|
95
+ puts "🔍 DEBUG: Processing partial: #{partial_path}" if ENV['RAILS_OPENAPI_DEBUG']
96
+ if File.exist?(partial_path)
97
+ partial_properties = parse_partial_for_nested_object(partial_path)
98
+ puts "🔍 DEBUG: Partial properties: #{partial_properties.size}" if ENV['RAILS_OPENAPI_DEBUG']
99
+ item_properties.concat(partial_properties)
100
+ elsif ENV['RAILS_OPENAPI_DEBUG']
101
+ puts "🔍 DEBUG: Partial file not found: #{partial_path}"
102
+ end
103
+ end
104
+
105
+ # Restore context
106
+ @properties = previous_properties
107
+ @partials = previous_partials
108
+ pop_block
109
+
110
+ # Convert item properties to structured nodes if needed
111
+ structured_item_properties = item_properties.map do |item|
112
+ if item.is_a?(Hash)
113
+ RailsOpenapiGen::AstNodes::PropertyNodeFactory.from_hash(item)
114
+ else
115
+ item
116
+ end
117
+ end
118
+
119
+ # Create comment data
120
+ comment_obj = if comment_data
121
+ RailsOpenapiGen::AstNodes::CommentData.new(
122
+ type: comment_data[:type] || 'array',
123
+ items: comment_data[:items] || { type: 'object' }
124
+ )
125
+ else
126
+ RailsOpenapiGen::AstNodes::CommentData.new(type: 'array', items: { type: 'object' })
127
+ end
128
+
129
+ # Create array root node
130
+ array_root_node = RailsOpenapiGen::AstNodes::PropertyNodeFactory.create_array_root(
131
+ comment_data: comment_obj,
132
+ array_item_properties: structured_item_properties
133
+ )
134
+
135
+ add_property(array_root_node)
136
+ end
137
+
138
+ # Processes json.array! calls to create array schema
139
+ # @param node [Parser::AST::Node] Array call node
140
+ # @return [void]
141
+ def process_array_property(node)
142
+ comment_data = find_comment_for_node(node)
143
+
144
+ # Mark this as an array root
145
+ property_info = {
146
+ node_type: 'array',
147
+ property: 'items', # Special property to indicate array items
148
+ comment_data: comment_data || { type: 'array', items: { type: 'object' } },
149
+ is_array_root: true
150
+ }
151
+
152
+ add_property(property_info)
153
+ end
154
+
155
+ # Processes json.array! with partial rendering
156
+ # @param node [Parser::AST::Node] Array call node
157
+ # @param args [Array] Array call arguments
158
+ # @return [void]
159
+ def process_array_with_partial(node, args)
160
+ # Extract partial path from the hash arguments
161
+ partial_path = nil
162
+ args.each do |arg|
163
+ next unless arg.type == :hash
164
+
165
+ arg.children.each do |pair|
166
+ next unless pair.type == :pair
167
+
168
+ key, value = pair.children
169
+ if key.type == :sym && key.children.first == :partial && value.type == :str
170
+ partial_path = value.children.first
171
+ break
172
+ end
173
+ end
174
+ end
175
+
176
+ if partial_path
177
+ # Resolve the partial path and parse it
178
+ resolved_path = resolve_partial_path(partial_path)
179
+ if resolved_path && File.exist?(resolved_path)
180
+ # Parse the partial to get its properties
181
+ partial_parser = JbuilderParser.new(resolved_path)
182
+ partial_result = partial_parser.parse
183
+
184
+ # Create array schema with items from the partial
185
+ property_info = {
186
+ node_type: 'array',
187
+ property: 'items',
188
+ comment_data: { type: 'array' },
189
+ is_array_root: true,
190
+ array_item_properties: partial_result[:properties]
191
+ }
192
+
193
+ add_property(property_info)
194
+ else
195
+ # Fallback to regular array processing
196
+ process_array_property(node)
197
+ end
198
+ else
199
+ # Fallback to regular array processing
200
+ process_array_property(node)
201
+ end
202
+ end
203
+
204
+ # Processes array iteration blocks (e.g., json.tags @tags do |tag|)
205
+ # @param node [Parser::AST::Node] Block node
206
+ # @param property_name [String] Array property name
207
+ # @return [void]
208
+ def process_array_iteration_block(node, property_name)
209
+ comment_data = find_comment_for_node(node)
210
+
211
+ # Save current context
212
+ previous_properties = properties.dup
213
+ previous_partials = partials.dup
214
+
215
+ # Create a temporary properties array for array items
216
+ @properties = []
217
+ @partials = []
218
+ push_block(:array)
219
+
220
+ # Process the block contents using CompositeProcessor
221
+ _, _args, body = node.children
222
+ if body
223
+ # Create a CompositeProcessor to handle all types of calls within the block
224
+ composite_processor = self.class.composite_processor_class.new(@file_path, @property_parser)
225
+ composite_processor.process(body)
226
+
227
+ # Merge results from the composite processor
228
+ @properties.concat(composite_processor.properties)
229
+ @partials.concat(composite_processor.partials)
230
+ end
231
+
232
+ # Collect item properties
233
+ item_properties = properties.dup
234
+ puts "🔍 DEBUG: Array block processed, item_properties size: #{item_properties.size}" if ENV['RAILS_OPENAPI_DEBUG']
235
+
236
+ # Process any partials found in this block
237
+ puts "🔍 DEBUG: Found #{partials.size} partials in array block" if ENV['RAILS_OPENAPI_DEBUG']
238
+ partials.each do |partial_path|
239
+ puts "🔍 DEBUG: Processing partial: #{partial_path}" if ENV['RAILS_OPENAPI_DEBUG']
240
+ if File.exist?(partial_path)
241
+ partial_properties = parse_partial_for_nested_object(partial_path)
242
+ puts "🔍 DEBUG: Partial properties: #{partial_properties.size}" if ENV['RAILS_OPENAPI_DEBUG']
243
+ item_properties.concat(partial_properties)
244
+ elsif ENV['RAILS_OPENAPI_DEBUG']
245
+ puts "🔍 DEBUG: Partial file not found: #{partial_path}"
246
+ end
247
+ end
248
+
249
+ # Restore context
250
+ @properties = previous_properties
251
+ @partials = previous_partials
252
+ pop_block
253
+
254
+ # Build array schema with items
255
+ property_info = {
256
+ node_type: 'property',
257
+ property: property_name,
258
+ comment_data: comment_data || { type: 'array' },
259
+ is_array: true,
260
+ array_item_properties: item_properties
261
+ }
262
+
263
+ add_property(property_info)
264
+ end
265
+ end
266
+ end
@@ -0,0 +1,235 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'parser/current'
4
+ require_relative '../../../ast_nodes'
5
+
6
+ module RailsOpenapiGen::Parsers::Jbuilder::Processors
7
+ class BaseProcessor < Parser::AST::Processor
8
+ # Alias for shorter reference to JbuilderParser
9
+ JbuilderParser = RailsOpenapiGen::Parsers::Jbuilder::JbuilderParser
10
+ # Ensure properties array is always initialized
11
+ def properties
12
+ @properties ||= []
13
+ end
14
+
15
+ # Ensure partials array is always initialized
16
+ def partials
17
+ @partials ||= []
18
+ end
19
+
20
+ # Initializes base processor
21
+ # @param file_path [String] Path to current file
22
+ # @param property_parser [PropertyCommentParser] Parser for property comments
23
+ def initialize(file_path, property_parser)
24
+ super() # Call parent initialize first, with no arguments
25
+ @file_path = file_path
26
+ @property_parser = property_parser
27
+ @properties = []
28
+ @partials = []
29
+ @block_stack = []
30
+ @current_object_properties = []
31
+ @nested_objects = {}
32
+ @conditional_stack = []
33
+ end
34
+
35
+ # Processes method call nodes - to be overridden by subclasses
36
+ # @param node [Parser::AST::Node] Method call node
37
+ # @return [void]
38
+
39
+ # Processes block nodes - to be overridden by subclasses
40
+ # @param node [Parser::AST::Node] Block node
41
+ # @return [void]
42
+
43
+ # Processes if statements to track conditional properties
44
+ # @param node [Parser::AST::Node] If statement node
45
+ # @return [void]
46
+ def on_if(node)
47
+ # Check if this if statement has a conditional comment
48
+ comment_data = find_comment_for_node(node)
49
+
50
+ if comment_data && comment_data[:conditional]
51
+ @conditional_stack.push(true)
52
+ super
53
+ @conditional_stack.pop
54
+ else
55
+ super
56
+ end
57
+ end
58
+
59
+ # Processes begin nodes (multiple statements)
60
+ # @param node [Parser::AST::Node] Begin node
61
+ # @return [void]
62
+
63
+ # Handler for missing node types
64
+ # @param node [Parser::AST::Node] Node to process
65
+ # @return [Parser::AST::Node, nil] The node or nil
66
+ def handler_missing(node)
67
+ node
68
+ end
69
+
70
+ protected
71
+
72
+ # Finds OpenAPI comment for a given AST node
73
+ # @param node [Parser::AST::Node] Node to find comment for
74
+ # @return [Hash, nil] Parsed comment data or nil
75
+ def find_comment_for_node(node)
76
+ line_number = node.location.line
77
+ @property_parser.find_property_comment_for_line(line_number)
78
+ end
79
+
80
+ # Resolves partial name to full file path
81
+ # @param partial_name [String] Partial name (e.g., "users/user")
82
+ # @return [String, nil] Full path to partial file or nil
83
+ def resolve_partial_path(partial_name)
84
+ return nil unless @file_path && partial_name
85
+
86
+ puts "🔍 DEBUG: resolve_partial_path called with: #{partial_name}, file_path: #{@file_path}" if ENV['RAILS_OPENAPI_DEBUG']
87
+
88
+ dir = File.dirname(@file_path)
89
+
90
+ if partial_name.include?('/')
91
+ # Find the app/views directory from the current file path
92
+ path_parts = @file_path.to_s.split('/')
93
+ puts "🔍 DEBUG: path_parts: #{path_parts}" if ENV['RAILS_OPENAPI_DEBUG']
94
+ views_index = path_parts.rindex('views')
95
+ if views_index
96
+ views_path = path_parts[0..views_index].join('/')
97
+ # For paths like 'users/user', convert to 'users/_user.json.jbuilder'
98
+ parts = partial_name.to_s.split('/')
99
+ dir_part = parts[0..-2].join('/')
100
+ file_part = "_#{parts[-1]}"
101
+ File.join(views_path, dir_part, "#{file_part}.json.jbuilder")
102
+ else
103
+ # For paths like 'users/user', convert to 'users/_user.json.jbuilder'
104
+ parts = partial_name.to_s.split('/')
105
+ dir_part = parts[0..-2].join('/')
106
+ file_part = "_#{parts[-1]}"
107
+ File.join(dir, dir_part, "#{file_part}.json.jbuilder")
108
+ end
109
+ else
110
+ # Add underscore prefix if not already present
111
+ filename = partial_name.start_with?('_') ? partial_name : "_#{partial_name}"
112
+ File.join(dir, "#{filename}.json.jbuilder")
113
+ end
114
+ end
115
+
116
+ # Parses a partial file to extract properties for nested objects
117
+ # @param partial_path [String] Path to partial file
118
+ # @return [Array] Array of property AST nodes
119
+ def parse_partial_for_nested_object(partial_path)
120
+ # Create a new parser to parse the partial independently
121
+ partial_parser = JbuilderParser.new(partial_path)
122
+ result = partial_parser.parse
123
+
124
+ # The new AST-based parser returns AST nodes directly, not hashes
125
+ if result.respond_to?(:children)
126
+ properties = result.children || []
127
+ puts "🔍 DEBUG: parse_partial_for_nested_object returned #{properties.size} properties" if ENV['RAILS_OPENAPI_DEBUG']
128
+ puts "🔍 DEBUG: first property type: #{properties.first.class}" if ENV['RAILS_OPENAPI_DEBUG'] && properties.any?
129
+ properties
130
+ else
131
+ puts "🔍 DEBUG: parse_partial_for_nested_object result is nil or has no children" if ENV['RAILS_OPENAPI_DEBUG']
132
+ []
133
+ end
134
+ end
135
+
136
+ # Adds a property to the properties array
137
+ # @param property_node [PropertyNode, Hash] Property node or hash (for backward compatibility)
138
+ # @return [void]
139
+ def add_property(property_node)
140
+ # Convert hash to PropertyNode if needed (backward compatibility)
141
+ if property_node.is_a?(Hash)
142
+ property_node = RailsOpenapiGen::AstNodes::PropertyNodeFactory.from_hash(property_node)
143
+ end
144
+
145
+ # Mark as conditional if inside a conditional block
146
+ if @conditional_stack.any?
147
+ # Create a new node with conditional flag set
148
+ property_node = create_conditional_node(property_node)
149
+ end
150
+
151
+ @properties << property_node
152
+ end
153
+
154
+ # Creates a conditional version of a property node
155
+ # @param node [PropertyNode] Original property node
156
+ # @return [PropertyNode] New conditional property node
157
+ def create_conditional_node(node)
158
+ case node
159
+ when RailsOpenapiGen::AstNodes::SimplePropertyNode
160
+ RailsOpenapiGen::AstNodes::PropertyNodeFactory.create_simple(
161
+ property: node.property,
162
+ comment_data: node.comment_data,
163
+ is_conditional: true
164
+ )
165
+ when RailsOpenapiGen::AstNodes::ArrayPropertyNode
166
+ RailsOpenapiGen::AstNodes::PropertyNodeFactory.create_array(
167
+ property: node.property,
168
+ comment_data: node.comment_data,
169
+ is_conditional: true,
170
+ array_item_properties: node.array_item_properties
171
+ )
172
+ when RailsOpenapiGen::AstNodes::ObjectPropertyNode
173
+ RailsOpenapiGen::AstNodes::PropertyNodeFactory.create_object(
174
+ property: node.property,
175
+ comment_data: node.comment_data,
176
+ is_conditional: true,
177
+ nested_properties: node.nested_properties
178
+ )
179
+ when RailsOpenapiGen::AstNodes::ArrayRootNode
180
+ # Array root nodes cannot be conditional, return as-is
181
+ node
182
+ else
183
+ # For unknown node types, create a warning comment
184
+ puts "Warning: Unknown node type #{node.class} encountered in conditional context" if ENV['RAILS_OPENAPI_DEBUG']
185
+ node
186
+ end
187
+ end
188
+
189
+ # Pushes a block type to the stack
190
+ # @param block_type [Symbol] Type of block (:array, :object, etc.)
191
+ # @return [void]
192
+ def push_block(block_type)
193
+ @block_stack.push(block_type)
194
+ end
195
+
196
+ # Pops a block type from the stack
197
+ # @return [Symbol, nil] Popped block type or nil
198
+ def pop_block
199
+ @block_stack.pop
200
+ end
201
+
202
+ # Checks if we're currently inside a specific block type
203
+ # @param block_type [Symbol] Block type to check
204
+ # @return [Boolean] True if inside the specified block type
205
+ def inside_block?(block_type)
206
+ @block_stack.last == block_type
207
+ end
208
+
209
+ # Processes a specific AST node
210
+ # @param node [Parser::AST::Node] Node to process
211
+ # @return [void]
212
+ def process_node(node)
213
+ return unless node
214
+
215
+ case node.type
216
+ when :send
217
+ on_send(node)
218
+ when :block
219
+ on_block(node)
220
+ when :if
221
+ on_if(node)
222
+ else
223
+ # For other node types, recursively process children
224
+ node.children.each { |child| process(child) if child.is_a?(Parser::AST::Node) }
225
+ end
226
+ end
227
+
228
+ # Clears processor results arrays
229
+ # @return [void]
230
+ def clear_results
231
+ @properties = []
232
+ @partials = []
233
+ end
234
+ end
235
+ end