rails-openapi-gen 0.0.1
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 +7 -0
- data/CLAUDE.md +160 -0
- data/README.md +164 -0
- data/lib/rails_openapi_gen/configuration.rb +157 -0
- data/lib/rails_openapi_gen/engine.rb +11 -0
- data/lib/rails_openapi_gen/generators/yaml_generator.rb +302 -0
- data/lib/rails_openapi_gen/importer.rb +647 -0
- data/lib/rails_openapi_gen/parsers/comment_parser.rb +40 -0
- data/lib/rails_openapi_gen/parsers/comment_parsers/attribute_parser.rb +57 -0
- data/lib/rails_openapi_gen/parsers/comment_parsers/base_attribute_parser.rb +42 -0
- data/lib/rails_openapi_gen/parsers/comment_parsers/body_parser.rb +62 -0
- data/lib/rails_openapi_gen/parsers/comment_parsers/conditional_parser.rb +13 -0
- data/lib/rails_openapi_gen/parsers/comment_parsers/operation_parser.rb +50 -0
- data/lib/rails_openapi_gen/parsers/comment_parsers/param_parser.rb +62 -0
- data/lib/rails_openapi_gen/parsers/comment_parsers/query_parser.rb +62 -0
- data/lib/rails_openapi_gen/parsers/controller_parser.rb +153 -0
- data/lib/rails_openapi_gen/parsers/jbuilder_parser.rb +529 -0
- data/lib/rails_openapi_gen/parsers/routes_parser.rb +33 -0
- data/lib/rails_openapi_gen/parsers/template_processors/jbuilder_template_processor.rb +147 -0
- data/lib/rails_openapi_gen/parsers/template_processors/response_template_processor.rb +17 -0
- data/lib/rails_openapi_gen/railtie.rb +11 -0
- data/lib/rails_openapi_gen/tasks/openapi.rake +30 -0
- data/lib/rails_openapi_gen/version.rb +5 -0
- data/lib/rails_openapi_gen.rb +267 -0
- data/lib/tasks/openapi_import.rake +126 -0
- data/rails-openapi-gen.gemspec +30 -0
- metadata +155 -0
@@ -0,0 +1,529 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "parser/current"
|
4
|
+
require "ostruct"
|
5
|
+
|
6
|
+
module RailsOpenapiGen
|
7
|
+
module Parsers
|
8
|
+
class JbuilderParser
|
9
|
+
attr_reader :jbuilder_path
|
10
|
+
|
11
|
+
# Initializes Jbuilder parser with template path
|
12
|
+
# @param jbuilder_path [String] Path to Jbuilder template file
|
13
|
+
def initialize(jbuilder_path)
|
14
|
+
@jbuilder_path = jbuilder_path
|
15
|
+
@properties = []
|
16
|
+
@operation_info = nil
|
17
|
+
@parsed_files = Set.new
|
18
|
+
end
|
19
|
+
|
20
|
+
# Parses Jbuilder template to extract properties and operation info
|
21
|
+
# @return [Hash] Hash with properties array and operation info
|
22
|
+
def parse
|
23
|
+
return { properties: @properties, operation: @operation_info } unless File.exist?(jbuilder_path)
|
24
|
+
|
25
|
+
parse_file(jbuilder_path)
|
26
|
+
{ properties: @properties, operation: @operation_info }
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
# Recursively parses a Jbuilder file and its partials
|
32
|
+
# @param file_path [String] Path to file to parse
|
33
|
+
# @return [void]
|
34
|
+
def parse_file(file_path)
|
35
|
+
return if @parsed_files.include?(file_path)
|
36
|
+
@parsed_files << file_path
|
37
|
+
|
38
|
+
content = File.read(file_path)
|
39
|
+
|
40
|
+
# Extract block comments first
|
41
|
+
block_comments = extract_block_comments(content)
|
42
|
+
|
43
|
+
ast, comments = Parser::CurrentRuby.parse_with_comments(content)
|
44
|
+
|
45
|
+
# Combine line comments and block comments
|
46
|
+
all_comments = comments + block_comments
|
47
|
+
|
48
|
+
processor = JbuilderProcessor.new(file_path, all_comments)
|
49
|
+
processor.process(ast)
|
50
|
+
|
51
|
+
@properties.concat(processor.properties)
|
52
|
+
@operation_info ||= processor.operation_info
|
53
|
+
|
54
|
+
processor.partials.each do |partial_path|
|
55
|
+
parse_file(partial_path) if File.exist?(partial_path)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Extracts block comments (=begin/=end) from file content
|
60
|
+
# @param content [String] File content
|
61
|
+
# @return [Array<OpenStruct>] Array of mock comment objects
|
62
|
+
def extract_block_comments(content)
|
63
|
+
block_comments = []
|
64
|
+
lines = content.lines
|
65
|
+
|
66
|
+
i = 0
|
67
|
+
while i < lines.length
|
68
|
+
line = lines[i].strip
|
69
|
+
if line.start_with?('=begin')
|
70
|
+
# Found start of block comment
|
71
|
+
comment_lines = []
|
72
|
+
i += 1
|
73
|
+
|
74
|
+
while i < lines.length && !lines[i].strip.start_with?('=end')
|
75
|
+
comment_lines << lines[i]
|
76
|
+
i += 1
|
77
|
+
end
|
78
|
+
|
79
|
+
# Create a mock comment object
|
80
|
+
comment_text = comment_lines.join
|
81
|
+
if comment_text.include?('@openapi')
|
82
|
+
mock_comment = OpenStruct.new(
|
83
|
+
text: comment_text,
|
84
|
+
location: OpenStruct.new(line: i - comment_lines.length)
|
85
|
+
)
|
86
|
+
block_comments << mock_comment
|
87
|
+
end
|
88
|
+
end
|
89
|
+
i += 1
|
90
|
+
end
|
91
|
+
|
92
|
+
block_comments
|
93
|
+
end
|
94
|
+
|
95
|
+
class JbuilderProcessor < Parser::AST::Processor
|
96
|
+
attr_reader :properties, :partials, :operation_info
|
97
|
+
|
98
|
+
# Initializes AST processor for Jbuilder parsing
|
99
|
+
# @param file_path [String] Path to current file
|
100
|
+
# @param comments [Array] Array of comment objects
|
101
|
+
def initialize(file_path, comments)
|
102
|
+
@file_path = file_path
|
103
|
+
@comments = comments
|
104
|
+
@operation_info = nil
|
105
|
+
@properties = []
|
106
|
+
@partials = []
|
107
|
+
@comment_parser = CommentParser.new
|
108
|
+
@block_stack = []
|
109
|
+
@current_object_properties = []
|
110
|
+
@nested_objects = {}
|
111
|
+
@conditional_stack = []
|
112
|
+
end
|
113
|
+
|
114
|
+
# Processes method call nodes to extract JSON properties
|
115
|
+
# @param node [Parser::AST::Node] Method call node
|
116
|
+
# @return [void]
|
117
|
+
def on_send(node)
|
118
|
+
receiver, method_name, *args = node.children
|
119
|
+
|
120
|
+
if cache_call?(receiver, method_name) || cache_if_call?(receiver, method_name) || jbuilder_helper?(receiver, method_name)
|
121
|
+
# Skip Jbuilder helper methods - they are not JSON properties
|
122
|
+
super
|
123
|
+
elsif array_call?(receiver, method_name)
|
124
|
+
# Check if this is an array with partial
|
125
|
+
if args.any? && args.any? { |arg| arg.type == :hash && has_partial_key?(arg) }
|
126
|
+
process_array_with_partial(node, args)
|
127
|
+
else
|
128
|
+
process_array_property(node)
|
129
|
+
end
|
130
|
+
elsif partial_call?(receiver, method_name)
|
131
|
+
process_partial(args)
|
132
|
+
elsif json_property?(receiver, method_name)
|
133
|
+
process_json_property(node, method_name.to_s, args)
|
134
|
+
end
|
135
|
+
|
136
|
+
super
|
137
|
+
end
|
138
|
+
|
139
|
+
# Processes block nodes for nested objects and array iterations
|
140
|
+
# @param node [Parser::AST::Node] Block node
|
141
|
+
# @return [void]
|
142
|
+
def on_block(node)
|
143
|
+
send_node, args_node, body = node.children
|
144
|
+
receiver, method_name, *send_args = send_node.children
|
145
|
+
|
146
|
+
if cache_call?(receiver, method_name) || cache_if_call?(receiver, method_name)
|
147
|
+
# This is json.cache! or json.cache_if! block - just process the block contents
|
148
|
+
process(body) if body
|
149
|
+
elsif json_property?(receiver, method_name) && method_name != :array!
|
150
|
+
# Check if this is an array iteration block (has block arguments)
|
151
|
+
if args_node && args_node.type == :args && args_node.children.any?
|
152
|
+
# This is an array iteration block like json.tags @tags do |tag|
|
153
|
+
process_array_iteration_block(node, method_name.to_s)
|
154
|
+
else
|
155
|
+
# This is a nested object block like json.profile do
|
156
|
+
process_nested_object_block(node, method_name.to_s)
|
157
|
+
end
|
158
|
+
elsif array_call?(receiver, method_name)
|
159
|
+
# This is json.array! block
|
160
|
+
@block_stack.push(:array)
|
161
|
+
super
|
162
|
+
@block_stack.pop
|
163
|
+
else
|
164
|
+
super
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
# Processes if statements to track conditional properties
|
169
|
+
# @param node [Parser::AST::Node] If statement node
|
170
|
+
# @return [void]
|
171
|
+
def on_if(node)
|
172
|
+
# Check if this if statement has a conditional comment
|
173
|
+
comment_data = find_comment_for_node(node)
|
174
|
+
|
175
|
+
if comment_data && comment_data[:conditional]
|
176
|
+
@conditional_stack.push(true)
|
177
|
+
super
|
178
|
+
@conditional_stack.pop
|
179
|
+
else
|
180
|
+
super
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
private
|
185
|
+
|
186
|
+
# Checks if node represents a json property call
|
187
|
+
# @param receiver [Parser::AST::Node] Receiver node
|
188
|
+
# @param method_name [Symbol] Method name
|
189
|
+
# @return [Boolean] True if json property call
|
190
|
+
def json_property?(receiver, method_name)
|
191
|
+
receiver && receiver.type == :send && receiver.children[1] == :json
|
192
|
+
end
|
193
|
+
|
194
|
+
# Checks if node represents a partial render call
|
195
|
+
# @param receiver [Parser::AST::Node] Receiver node
|
196
|
+
# @param method_name [Symbol] Method name
|
197
|
+
# @return [Boolean] True if partial call
|
198
|
+
def partial_call?(receiver, method_name)
|
199
|
+
method_name == :partial! && (!receiver || receiver.type == :send && receiver.children[1] == :json)
|
200
|
+
end
|
201
|
+
|
202
|
+
# Checks if node represents a json.array! call
|
203
|
+
# @param receiver [Parser::AST::Node] Receiver node
|
204
|
+
# @param method_name [Symbol] Method name
|
205
|
+
# @return [Boolean] True if array call
|
206
|
+
def array_call?(receiver, method_name)
|
207
|
+
method_name == :array! && receiver && receiver.type == :send && receiver.children[1] == :json
|
208
|
+
end
|
209
|
+
|
210
|
+
# Checks if node represents a json.cache! call
|
211
|
+
# @param receiver [Parser::AST::Node] Receiver node
|
212
|
+
# @param method_name [Symbol] Method name
|
213
|
+
# @return [Boolean] True if cache call
|
214
|
+
def cache_call?(receiver, method_name)
|
215
|
+
method_name == :cache! && receiver && receiver.type == :send && receiver.children[1] == :json
|
216
|
+
end
|
217
|
+
|
218
|
+
# Checks if node represents a json.cache_if! call
|
219
|
+
# @param receiver [Parser::AST::Node] Receiver node
|
220
|
+
# @param method_name [Symbol] Method name
|
221
|
+
# @return [Boolean] True if cache_if call
|
222
|
+
def cache_if_call?(receiver, method_name)
|
223
|
+
method_name == :cache_if! && receiver && receiver.type == :send && receiver.children[1] == :json
|
224
|
+
end
|
225
|
+
|
226
|
+
# Checks if node represents a Jbuilder helper method that should be ignored
|
227
|
+
# @param receiver [Parser::AST::Node] Receiver node
|
228
|
+
# @param method_name [Symbol] Method name
|
229
|
+
# @return [Boolean] True if helper method
|
230
|
+
def jbuilder_helper?(receiver, method_name)
|
231
|
+
helper_methods = [:key_format!, :ignore_nil!, :merge!, :deep_format_keys!, :set!, :child!, :nil!, :null!, :cache_root!]
|
232
|
+
helper_methods.include?(method_name) && receiver && receiver.type == :send && receiver.children[1] == :json
|
233
|
+
end
|
234
|
+
|
235
|
+
# Processes a simple JSON property assignment
|
236
|
+
# @param node [Parser::AST::Node] Property node
|
237
|
+
# @param property_name [String] Name of the property
|
238
|
+
# @param args [Array] Method arguments
|
239
|
+
# @return [void]
|
240
|
+
def process_json_property(node, property_name, args)
|
241
|
+
comment_data = find_comment_for_node(node)
|
242
|
+
|
243
|
+
# Check if we're inside an array block
|
244
|
+
if @block_stack.last == :array
|
245
|
+
process_simple_property(node, property_name, comment_data)
|
246
|
+
else
|
247
|
+
process_simple_property(node, property_name, comment_data)
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
# Processes json.array! calls to create array schema
|
252
|
+
# @param node [Parser::AST::Node] Array call node
|
253
|
+
# @return [void]
|
254
|
+
def process_array_property(node)
|
255
|
+
comment_data = find_comment_for_node(node)
|
256
|
+
|
257
|
+
# Mark this as an array root
|
258
|
+
property_info = {
|
259
|
+
property: "items", # Special property to indicate array items
|
260
|
+
comment_data: comment_data || { type: "array", items: { type: "object" } },
|
261
|
+
is_array_root: true
|
262
|
+
}
|
263
|
+
|
264
|
+
@properties << property_info
|
265
|
+
end
|
266
|
+
|
267
|
+
# Processes json.array! with partial rendering
|
268
|
+
# @param node [Parser::AST::Node] Array call node
|
269
|
+
# @param args [Array] Array call arguments
|
270
|
+
# @return [void]
|
271
|
+
def process_array_with_partial(node, args)
|
272
|
+
# Extract partial path from the hash arguments
|
273
|
+
partial_path = nil
|
274
|
+
args.each do |arg|
|
275
|
+
if arg.type == :hash
|
276
|
+
arg.children.each do |pair|
|
277
|
+
if pair.type == :pair
|
278
|
+
key, value = pair.children
|
279
|
+
if key.type == :sym && key.children.first == :partial && value.type == :str
|
280
|
+
partial_path = value.children.first
|
281
|
+
break
|
282
|
+
end
|
283
|
+
end
|
284
|
+
end
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
if partial_path
|
289
|
+
# Resolve the partial path and parse it
|
290
|
+
resolved_path = resolve_partial_path(partial_path)
|
291
|
+
if resolved_path && File.exist?(resolved_path)
|
292
|
+
# Parse the partial to get its properties
|
293
|
+
partial_parser = JbuilderParser.new(resolved_path)
|
294
|
+
partial_result = partial_parser.parse
|
295
|
+
|
296
|
+
# Create array schema with items from the partial
|
297
|
+
property_info = {
|
298
|
+
property: "items",
|
299
|
+
comment_data: { type: "array" },
|
300
|
+
is_array_root: true,
|
301
|
+
array_item_properties: partial_result[:properties]
|
302
|
+
}
|
303
|
+
|
304
|
+
@properties << property_info
|
305
|
+
else
|
306
|
+
# Fallback to regular array processing
|
307
|
+
process_array_property(node)
|
308
|
+
end
|
309
|
+
else
|
310
|
+
# Fallback to regular array processing
|
311
|
+
process_array_property(node)
|
312
|
+
end
|
313
|
+
end
|
314
|
+
|
315
|
+
# Checks if hash node contains a :partial key
|
316
|
+
# @param hash_node [Parser::AST::Node] Hash node to check
|
317
|
+
# @return [Boolean] True if partial key exists
|
318
|
+
def has_partial_key?(hash_node)
|
319
|
+
hash_node.children.any? do |pair|
|
320
|
+
if pair.type == :pair
|
321
|
+
key, _value = pair.children
|
322
|
+
key.type == :sym && key.children.first == :partial
|
323
|
+
end
|
324
|
+
end
|
325
|
+
end
|
326
|
+
|
327
|
+
# Processes array iteration blocks (e.g., json.tags @tags do |tag|)
|
328
|
+
# @param node [Parser::AST::Node] Block node
|
329
|
+
# @param property_name [String] Array property name
|
330
|
+
# @return [void]
|
331
|
+
def process_array_iteration_block(node, property_name)
|
332
|
+
comment_data = find_comment_for_node(node)
|
333
|
+
|
334
|
+
# Save current context
|
335
|
+
previous_properties = @properties.dup
|
336
|
+
previous_partials = @partials.dup
|
337
|
+
|
338
|
+
# Create a temporary properties array for array items
|
339
|
+
@properties = []
|
340
|
+
@partials = []
|
341
|
+
@block_stack.push(:array)
|
342
|
+
|
343
|
+
# Process the block contents
|
344
|
+
send_node, _args, body = node.children
|
345
|
+
process(body) if body
|
346
|
+
|
347
|
+
# Collect item properties
|
348
|
+
item_properties = @properties.dup
|
349
|
+
|
350
|
+
# Process any partials found in this block
|
351
|
+
@partials.each do |partial_path|
|
352
|
+
if File.exist?(partial_path)
|
353
|
+
partial_properties = parse_partial_for_nested_object(partial_path)
|
354
|
+
item_properties.concat(partial_properties)
|
355
|
+
end
|
356
|
+
end
|
357
|
+
|
358
|
+
# Restore context
|
359
|
+
@properties = previous_properties
|
360
|
+
@partials = previous_partials
|
361
|
+
@block_stack.pop
|
362
|
+
|
363
|
+
# Build array schema with items
|
364
|
+
property_info = {
|
365
|
+
property: property_name,
|
366
|
+
comment_data: comment_data || { type: "array" },
|
367
|
+
is_array: true,
|
368
|
+
array_item_properties: item_properties
|
369
|
+
}
|
370
|
+
|
371
|
+
@properties << property_info
|
372
|
+
end
|
373
|
+
|
374
|
+
# Processes nested object blocks (e.g., json.profile do)
|
375
|
+
# @param node [Parser::AST::Node] Block node
|
376
|
+
# @param property_name [String] Object property name
|
377
|
+
# @return [void]
|
378
|
+
def process_nested_object_block(node, property_name)
|
379
|
+
comment_data = find_comment_for_node(node)
|
380
|
+
|
381
|
+
# Save current context
|
382
|
+
previous_nested_objects = @nested_objects.dup
|
383
|
+
previous_properties = @properties.dup
|
384
|
+
previous_partials = @partials.dup
|
385
|
+
|
386
|
+
# Create a temporary properties array for this nested object
|
387
|
+
@properties = []
|
388
|
+
@partials = []
|
389
|
+
@block_stack.push(:object)
|
390
|
+
|
391
|
+
# Process the block contents
|
392
|
+
send_node, _args, body = node.children
|
393
|
+
process(body) if body
|
394
|
+
|
395
|
+
# Collect nested properties
|
396
|
+
nested_properties = @properties.dup
|
397
|
+
|
398
|
+
# Process any partials found in this block
|
399
|
+
@partials.each do |partial_path|
|
400
|
+
if File.exist?(partial_path)
|
401
|
+
partial_properties = parse_partial_for_nested_object(partial_path)
|
402
|
+
nested_properties.concat(partial_properties)
|
403
|
+
end
|
404
|
+
end
|
405
|
+
|
406
|
+
# Restore context
|
407
|
+
@properties = previous_properties
|
408
|
+
@partials = previous_partials # Don't add nested partials to main partials
|
409
|
+
@nested_objects = previous_nested_objects
|
410
|
+
@block_stack.pop
|
411
|
+
|
412
|
+
# Store nested object info
|
413
|
+
@nested_objects[property_name] = nested_properties
|
414
|
+
|
415
|
+
# Add the parent property
|
416
|
+
property_info = {
|
417
|
+
property: property_name,
|
418
|
+
comment_data: comment_data || { type: "object" },
|
419
|
+
is_object: true,
|
420
|
+
nested_properties: nested_properties
|
421
|
+
}
|
422
|
+
|
423
|
+
# Mark as optional if inside a conditional block
|
424
|
+
if @conditional_stack.any?
|
425
|
+
property_info[:is_conditional] = true
|
426
|
+
end
|
427
|
+
|
428
|
+
@properties << property_info
|
429
|
+
end
|
430
|
+
|
431
|
+
# Processes a simple property assignment
|
432
|
+
# @param node [Parser::AST::Node] Property node
|
433
|
+
# @param property_name [String] Name of the property
|
434
|
+
# @param comment_data [Hash, nil] Parsed comment data
|
435
|
+
# @return [void]
|
436
|
+
def process_simple_property(node, property_name, comment_data)
|
437
|
+
property_info = {
|
438
|
+
property: property_name,
|
439
|
+
comment_data: comment_data
|
440
|
+
}
|
441
|
+
|
442
|
+
unless comment_data && !comment_data.empty?
|
443
|
+
property_info[:comment_data] = { type: "TODO: MISSING COMMENT" }
|
444
|
+
end
|
445
|
+
|
446
|
+
# Mark as optional if inside a conditional block
|
447
|
+
if @conditional_stack.any?
|
448
|
+
property_info[:is_conditional] = true
|
449
|
+
end
|
450
|
+
|
451
|
+
@properties << property_info
|
452
|
+
end
|
453
|
+
|
454
|
+
|
455
|
+
# Processes partial render calls to track dependencies
|
456
|
+
# @param args [Array] Partial call arguments
|
457
|
+
# @return [void]
|
458
|
+
def process_partial(args)
|
459
|
+
return if args.empty?
|
460
|
+
|
461
|
+
partial_arg = args.first
|
462
|
+
if partial_arg.type == :str
|
463
|
+
partial_name = partial_arg.children.first
|
464
|
+
partial_path = resolve_partial_path(partial_name)
|
465
|
+
@partials << partial_path if partial_path
|
466
|
+
end
|
467
|
+
end
|
468
|
+
|
469
|
+
# Finds OpenAPI comment for a given AST node
|
470
|
+
# @param node [Parser::AST::Node] Node to find comment for
|
471
|
+
# @return [Hash, nil] Parsed comment data or nil
|
472
|
+
def find_comment_for_node(node)
|
473
|
+
line_number = node.location.line
|
474
|
+
|
475
|
+
# First check all comments for operation info
|
476
|
+
@comments.each do |comment|
|
477
|
+
parsed = @comment_parser.parse(comment.text)
|
478
|
+
if parsed&.dig(:operation) && @operation_info.nil?
|
479
|
+
@operation_info = parsed[:operation]
|
480
|
+
end
|
481
|
+
end
|
482
|
+
|
483
|
+
# Then find comment for the specific node
|
484
|
+
@comments.reverse.find do |comment|
|
485
|
+
comment_line = comment.location.line
|
486
|
+
comment_line == line_number - 1 || comment_line == line_number
|
487
|
+
end&.then do |comment|
|
488
|
+
@comment_parser.parse(comment.text)
|
489
|
+
end
|
490
|
+
end
|
491
|
+
|
492
|
+
# Resolves partial name to full file path
|
493
|
+
# @param partial_name [String] Partial name (e.g., "users/user")
|
494
|
+
# @return [String, nil] Full path to partial file or nil
|
495
|
+
def resolve_partial_path(partial_name)
|
496
|
+
dir = File.dirname(@file_path)
|
497
|
+
|
498
|
+
if partial_name.include?("/")
|
499
|
+
# Find the app/views directory from the current file path
|
500
|
+
path_parts = @file_path.split('/')
|
501
|
+
views_index = path_parts.rindex('views')
|
502
|
+
if views_index
|
503
|
+
views_path = path_parts[0..views_index].join('/')
|
504
|
+
# For paths like 'users/user', convert to 'users/_user.json.jbuilder'
|
505
|
+
parts = partial_name.split('/')
|
506
|
+
dir_part = parts[0..-2].join('/')
|
507
|
+
file_part = "_#{parts[-1]}"
|
508
|
+
File.join(views_path, dir_part, "#{file_part}.json.jbuilder")
|
509
|
+
else
|
510
|
+
File.join(dir, "#{partial_name}.json.jbuilder")
|
511
|
+
end
|
512
|
+
else
|
513
|
+
File.join(dir, "_#{partial_name}.json.jbuilder")
|
514
|
+
end
|
515
|
+
end
|
516
|
+
|
517
|
+
# Parses a partial file to extract properties for nested objects
|
518
|
+
# @param partial_path [String] Path to partial file
|
519
|
+
# @return [Array<Hash>] Array of property definitions
|
520
|
+
def parse_partial_for_nested_object(partial_path)
|
521
|
+
# Create a new parser to parse the partial independently
|
522
|
+
partial_parser = JbuilderParser.new(partial_path)
|
523
|
+
result = partial_parser.parse
|
524
|
+
result[:properties]
|
525
|
+
end
|
526
|
+
end
|
527
|
+
end
|
528
|
+
end
|
529
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RailsOpenapiGen
|
4
|
+
module Parsers
|
5
|
+
class RoutesParser
|
6
|
+
# Parses Rails application routes to extract route information
|
7
|
+
# @return [Array<Hash>] Array of route hashes with method, path, controller, action, and name
|
8
|
+
def parse
|
9
|
+
routes = []
|
10
|
+
|
11
|
+
Rails.application.routes.routes.each do |route|
|
12
|
+
next unless route.defaults[:controller] && route.defaults[:action]
|
13
|
+
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]
|
18
|
+
action = route.defaults[:action]
|
19
|
+
|
20
|
+
routes << {
|
21
|
+
method: method,
|
22
|
+
path: path,
|
23
|
+
controller: controller,
|
24
|
+
action: action,
|
25
|
+
name: route.name
|
26
|
+
}
|
27
|
+
end
|
28
|
+
|
29
|
+
routes
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,147 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "parser/current"
|
4
|
+
require_relative 'response_template_processor'
|
5
|
+
|
6
|
+
module RailsOpenapiGen
|
7
|
+
module Parsers
|
8
|
+
module TemplateProcessors
|
9
|
+
class JbuilderTemplateProcessor
|
10
|
+
include ResponseTemplateProcessor
|
11
|
+
|
12
|
+
def initialize(controller, action)
|
13
|
+
@controller = controller
|
14
|
+
@action = action
|
15
|
+
end
|
16
|
+
|
17
|
+
def extract_template_path(action_node, route)
|
18
|
+
return nil unless action_node
|
19
|
+
|
20
|
+
processor = JbuilderPathProcessor.new(route[:controller], route[:action])
|
21
|
+
processor.process(action_node)
|
22
|
+
processor.jbuilder_path
|
23
|
+
end
|
24
|
+
|
25
|
+
def find_default_template(route)
|
26
|
+
template_path = Rails.root.join("app", "views", route[:controller], "#{route[:action]}.json.jbuilder")
|
27
|
+
File.exist?(template_path) ? template_path.to_s : nil
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
class JbuilderPathProcessor < Parser::AST::Processor
|
33
|
+
attr_reader :jbuilder_path
|
34
|
+
|
35
|
+
def initialize(controller, action)
|
36
|
+
@controller = controller
|
37
|
+
@action = action
|
38
|
+
@jbuilder_path = nil
|
39
|
+
end
|
40
|
+
|
41
|
+
def on_send(node)
|
42
|
+
if render_call?(node)
|
43
|
+
extract_render_target(node)
|
44
|
+
end
|
45
|
+
super
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def render_call?(node)
|
51
|
+
receiver, method_name = node.children[0..1]
|
52
|
+
receiver.nil? && method_name == :render
|
53
|
+
end
|
54
|
+
|
55
|
+
def extract_render_target(node)
|
56
|
+
args = node.children[2..-1]
|
57
|
+
|
58
|
+
if args.empty?
|
59
|
+
@jbuilder_path = default_jbuilder_path
|
60
|
+
elsif args.first.type == :hash
|
61
|
+
parse_render_options(args.first)
|
62
|
+
elsif args.first.type == :str || args.first.type == :sym
|
63
|
+
template = args.first.children.first.to_s
|
64
|
+
@jbuilder_path = Rails.root.join("app", "views", @controller, "#{template}.json.jbuilder")
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def parse_render_options(hash_node)
|
69
|
+
render_options = extract_render_hash_options(hash_node)
|
70
|
+
|
71
|
+
if render_options[:json]
|
72
|
+
@jbuilder_path = default_jbuilder_path
|
73
|
+
elsif render_options[:template]
|
74
|
+
template_path = render_options[:template]
|
75
|
+
formats = render_options[:formats] || :json
|
76
|
+
handlers = render_options[:handlers] || :jbuilder
|
77
|
+
|
78
|
+
# Build full template path with format and handler
|
79
|
+
full_template_path = build_template_path(template_path, formats, handlers)
|
80
|
+
@jbuilder_path = Rails.root.join("app", "views", "#{full_template_path}")
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
private
|
85
|
+
|
86
|
+
def extract_render_hash_options(hash_node)
|
87
|
+
options = {}
|
88
|
+
|
89
|
+
hash_node.children.each do |pair|
|
90
|
+
key_node, value_node = pair.children
|
91
|
+
next unless key_node.type == :sym
|
92
|
+
|
93
|
+
key = key_node.children.first
|
94
|
+
value = extract_node_value(value_node)
|
95
|
+
|
96
|
+
options[key] = value
|
97
|
+
end
|
98
|
+
|
99
|
+
options
|
100
|
+
end
|
101
|
+
|
102
|
+
def extract_node_value(node)
|
103
|
+
case node.type
|
104
|
+
when :str, :sym
|
105
|
+
node.children.first
|
106
|
+
when :true
|
107
|
+
true
|
108
|
+
when :false
|
109
|
+
false
|
110
|
+
else
|
111
|
+
node.children.first
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def build_template_path(template, formats, handlers)
|
116
|
+
# Handle different format specifications
|
117
|
+
format_str = case formats
|
118
|
+
when Symbol
|
119
|
+
formats.to_s
|
120
|
+
when String
|
121
|
+
formats
|
122
|
+
else
|
123
|
+
"json"
|
124
|
+
end
|
125
|
+
|
126
|
+
# Handle different handler specifications
|
127
|
+
handler_str = case handlers
|
128
|
+
when Symbol
|
129
|
+
handlers.to_s
|
130
|
+
when String
|
131
|
+
handlers
|
132
|
+
else
|
133
|
+
"jbuilder"
|
134
|
+
end
|
135
|
+
|
136
|
+
# Build the path: template.format.handler
|
137
|
+
"#{template.gsub('/', File::SEPARATOR)}.#{format_str}.#{handler_str}"
|
138
|
+
end
|
139
|
+
|
140
|
+
def default_jbuilder_path
|
141
|
+
Rails.root.join("app", "views", @controller, "#{@action}.json.jbuilder")
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|