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
@@ -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
|