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.
Files changed (27) hide show
  1. checksums.yaml +7 -0
  2. data/CLAUDE.md +160 -0
  3. data/README.md +164 -0
  4. data/lib/rails_openapi_gen/configuration.rb +157 -0
  5. data/lib/rails_openapi_gen/engine.rb +11 -0
  6. data/lib/rails_openapi_gen/generators/yaml_generator.rb +302 -0
  7. data/lib/rails_openapi_gen/importer.rb +647 -0
  8. data/lib/rails_openapi_gen/parsers/comment_parser.rb +40 -0
  9. data/lib/rails_openapi_gen/parsers/comment_parsers/attribute_parser.rb +57 -0
  10. data/lib/rails_openapi_gen/parsers/comment_parsers/base_attribute_parser.rb +42 -0
  11. data/lib/rails_openapi_gen/parsers/comment_parsers/body_parser.rb +62 -0
  12. data/lib/rails_openapi_gen/parsers/comment_parsers/conditional_parser.rb +13 -0
  13. data/lib/rails_openapi_gen/parsers/comment_parsers/operation_parser.rb +50 -0
  14. data/lib/rails_openapi_gen/parsers/comment_parsers/param_parser.rb +62 -0
  15. data/lib/rails_openapi_gen/parsers/comment_parsers/query_parser.rb +62 -0
  16. data/lib/rails_openapi_gen/parsers/controller_parser.rb +153 -0
  17. data/lib/rails_openapi_gen/parsers/jbuilder_parser.rb +529 -0
  18. data/lib/rails_openapi_gen/parsers/routes_parser.rb +33 -0
  19. data/lib/rails_openapi_gen/parsers/template_processors/jbuilder_template_processor.rb +147 -0
  20. data/lib/rails_openapi_gen/parsers/template_processors/response_template_processor.rb +17 -0
  21. data/lib/rails_openapi_gen/railtie.rb +11 -0
  22. data/lib/rails_openapi_gen/tasks/openapi.rake +30 -0
  23. data/lib/rails_openapi_gen/version.rb +5 -0
  24. data/lib/rails_openapi_gen.rb +267 -0
  25. data/lib/tasks/openapi_import.rake +126 -0
  26. data/rails-openapi-gen.gemspec +30 -0
  27. 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