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
@@ -1,147 +1,141 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "parser/current"
|
4
|
-
require_relative '
|
4
|
+
require_relative '../template_processors'
|
5
5
|
|
6
|
-
module RailsOpenapiGen
|
7
|
-
|
8
|
-
|
9
|
-
class JbuilderTemplateProcessor
|
10
|
-
include ResponseTemplateProcessor
|
6
|
+
module RailsOpenapiGen::Parsers::TemplateProcessors
|
7
|
+
class JbuilderTemplateProcessor
|
8
|
+
include ResponseTemplateProcessor
|
11
9
|
|
12
|
-
|
13
|
-
|
14
|
-
|
10
|
+
def initialize(controller, action)
|
11
|
+
@controller = controller
|
12
|
+
@action = action
|
13
|
+
end
|
14
|
+
|
15
|
+
def extract_template_path(action_node, route)
|
16
|
+
return nil unless action_node
|
17
|
+
|
18
|
+
processor = JbuilderPathProcessor.new(route[:controller], route[:action])
|
19
|
+
processor.process(action_node)
|
20
|
+
processor.jbuilder_path
|
21
|
+
end
|
22
|
+
|
23
|
+
def find_default_template(route)
|
24
|
+
template_path = Rails.root.join("app", "views", route[:controller], "#{route[:action]}.json.jbuilder")
|
25
|
+
File.exist?(template_path) ? template_path.to_s : nil
|
26
|
+
end
|
27
|
+
|
28
|
+
class JbuilderPathProcessor < Parser::AST::Processor
|
29
|
+
attr_reader :jbuilder_path
|
30
|
+
|
31
|
+
def initialize(controller, action)
|
32
|
+
super()
|
33
|
+
@controller = controller
|
34
|
+
@action = action
|
35
|
+
@jbuilder_path = nil
|
36
|
+
end
|
37
|
+
|
38
|
+
def on_send(node)
|
39
|
+
if render_call?(node) && @jbuilder_path.nil?
|
40
|
+
extract_render_target(node)
|
15
41
|
end
|
42
|
+
super
|
43
|
+
end
|
16
44
|
|
17
|
-
|
18
|
-
return nil unless action_node
|
45
|
+
private
|
19
46
|
|
20
|
-
|
21
|
-
|
22
|
-
|
47
|
+
def render_call?(node)
|
48
|
+
receiver, method_name = node.children[0..1]
|
49
|
+
receiver.nil? && method_name == :render
|
50
|
+
end
|
51
|
+
|
52
|
+
def extract_render_target(node)
|
53
|
+
args = node.children[2..-1]
|
54
|
+
|
55
|
+
if args.empty?
|
56
|
+
@jbuilder_path = default_jbuilder_path
|
57
|
+
elsif args.first.type == :hash
|
58
|
+
parse_render_options(args.first)
|
59
|
+
elsif args.first.type == :str || args.first.type == :sym
|
60
|
+
template = args.first.children.first.to_s
|
61
|
+
@jbuilder_path = Rails.root.join("app", "views", @controller, "#{template}.json.jbuilder")
|
23
62
|
end
|
63
|
+
end
|
24
64
|
|
25
|
-
|
26
|
-
|
27
|
-
|
65
|
+
def parse_render_options(hash_node)
|
66
|
+
render_options = extract_render_hash_options(hash_node)
|
67
|
+
|
68
|
+
if render_options[:json]
|
69
|
+
@jbuilder_path = default_jbuilder_path
|
70
|
+
elsif render_options[:template]
|
71
|
+
template_path = render_options[:template]
|
72
|
+
formats = render_options[:formats] || :json
|
73
|
+
handlers = render_options[:handlers] || :jbuilder
|
74
|
+
|
75
|
+
# Build full template path with format and handler
|
76
|
+
full_template_path = build_template_path(template_path, formats, handlers)
|
77
|
+
@jbuilder_path = Rails.root.join("app", "views", full_template_path.to_s)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def extract_render_hash_options(hash_node)
|
82
|
+
options = {}
|
83
|
+
|
84
|
+
hash_node.children.each do |pair|
|
85
|
+
key_node, value_node = pair.children
|
86
|
+
next unless key_node.type == :sym
|
87
|
+
|
88
|
+
key = key_node.children.first
|
89
|
+
value = extract_node_value(value_node)
|
90
|
+
|
91
|
+
options[key] = value
|
28
92
|
end
|
29
93
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
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
|
94
|
+
options
|
95
|
+
end
|
96
|
+
|
97
|
+
def extract_node_value(node)
|
98
|
+
case node.type
|
99
|
+
when :str, :sym
|
100
|
+
node.children.first
|
101
|
+
when true
|
102
|
+
true
|
103
|
+
when false
|
104
|
+
false
|
105
|
+
else
|
106
|
+
# For other node types, try to extract the first child or return nil
|
107
|
+
node.children&.first
|
143
108
|
end
|
144
109
|
end
|
110
|
+
|
111
|
+
def build_template_path(template, formats, handlers)
|
112
|
+
# Handle different format specifications
|
113
|
+
format_str = case formats
|
114
|
+
when Symbol
|
115
|
+
formats.to_s
|
116
|
+
when String
|
117
|
+
formats
|
118
|
+
else
|
119
|
+
"json"
|
120
|
+
end
|
121
|
+
|
122
|
+
# Handle different handler specifications
|
123
|
+
handler_str = case handlers
|
124
|
+
when Symbol
|
125
|
+
handlers.to_s
|
126
|
+
when String
|
127
|
+
handlers
|
128
|
+
else
|
129
|
+
"jbuilder"
|
130
|
+
end
|
131
|
+
|
132
|
+
# Build the path: template.format.handler
|
133
|
+
"#{template.gsub('/', File::SEPARATOR)}.#{format_str}.#{handler_str}"
|
134
|
+
end
|
135
|
+
|
136
|
+
def default_jbuilder_path
|
137
|
+
Rails.root.join("app", "views", @controller, "#{@action}.json.jbuilder")
|
138
|
+
end
|
145
139
|
end
|
146
140
|
end
|
147
|
-
end
|
141
|
+
end
|
@@ -1,17 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
module RailsOpenapiGen
|
4
|
-
module
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
raise NotImplementedError, "#{self.class} must implement #extract_template_path"
|
9
|
-
end
|
3
|
+
module RailsOpenapiGen::Parsers::TemplateProcessors
|
4
|
+
module ResponseTemplateProcessor
|
5
|
+
def extract_template_path(_action_node, _route)
|
6
|
+
raise NotImplementedError, "#{self.class} must implement #extract_template_path"
|
7
|
+
end
|
10
8
|
|
11
|
-
|
12
|
-
|
13
|
-
end
|
14
|
-
end
|
9
|
+
def find_default_template(_route)
|
10
|
+
raise NotImplementedError, "#{self.class} must implement #find_default_template"
|
15
11
|
end
|
16
12
|
end
|
17
|
-
end
|
13
|
+
end
|
@@ -0,0 +1,6 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RailsOpenapiGen::Parsers::TemplateProcessors
|
4
|
+
autoload :ResponseTemplateProcessor, "rails-openapi-gen/parsers/template_processors/response_template_processor"
|
5
|
+
autoload :JbuilderTemplateProcessor, "rails-openapi-gen/parsers/template_processors/jbuilder_template_processor"
|
6
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RailsOpenapiGen::Parsers
|
4
|
+
autoload :CommentParser, "rails-openapi-gen/parsers/comment_parser"
|
5
|
+
autoload :ControllerParser, "rails-openapi-gen/parsers/controller_parser"
|
6
|
+
autoload :RoutesParser, "rails-openapi-gen/parsers/routes_parser"
|
7
|
+
autoload :Jbuilder, "rails-openapi-gen/parsers/jbuilder"
|
8
|
+
autoload :TemplateProcessors, "rails-openapi-gen/parsers/template_processors"
|
9
|
+
end
|
@@ -0,0 +1,226 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base_processor"
|
4
|
+
|
5
|
+
module RailsOpenapiGen::Processors
|
6
|
+
# Processor that converts AST nodes to OpenAPI schema format
|
7
|
+
# Implements the visitor pattern to handle different node types
|
8
|
+
class AstToSchemaProcessor < BaseProcessor
|
9
|
+
def initialize
|
10
|
+
super()
|
11
|
+
@schema = {}
|
12
|
+
@required_properties = []
|
13
|
+
end
|
14
|
+
|
15
|
+
# Process root node and return OpenAPI schema
|
16
|
+
# @param root_node [RailsOpenapiGen::AstNodes::BaseNode] Root AST node
|
17
|
+
# @return [Hash] OpenAPI schema
|
18
|
+
def process_to_schema(root_node)
|
19
|
+
@schema = {}
|
20
|
+
@required_properties = []
|
21
|
+
|
22
|
+
puts "🔍 DEBUG: Using NEW AstToSchemaProcessor with node: #{root_node.class.name}" if ENV['RAILS_OPENAPI_DEBUG']
|
23
|
+
if root_node.respond_to?(:properties) && ENV['RAILS_OPENAPI_DEBUG']
|
24
|
+
puts "🔍 DEBUG: Root node has #{root_node.properties.size} properties"
|
25
|
+
root_node.properties.each_with_index do |prop, i|
|
26
|
+
puts "🔍 DEBUG: Property #{i}: #{prop.property_name} (#{prop.class.name})"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
result = process(root_node)
|
31
|
+
|
32
|
+
# Handle root array case
|
33
|
+
if root_node.is_a?(RailsOpenapiGen::AstNodes::ArrayNode) && root_node.root_array?
|
34
|
+
# For root arrays, return the result directly since visit_array already creates the array schema
|
35
|
+
result || { 'type' => 'array', 'items' => { 'type' => 'object' } }
|
36
|
+
else
|
37
|
+
result || { 'type' => 'object' }
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Visit a property node
|
42
|
+
# @param node [RailsOpenapiGen::AstNodes::PropertyNode] Property node
|
43
|
+
# @return [Hash] Property schema
|
44
|
+
def visit_property(node)
|
45
|
+
# If this is a component reference, return a $ref with prefix removed
|
46
|
+
if node.is_component_ref && node.component_name
|
47
|
+
# Remove component prefix from schema name in $ref
|
48
|
+
config = RailsOpenapiGen.configuration
|
49
|
+
schema_name_without_prefix = config.remove_component_prefix(node.component_name)
|
50
|
+
return { '$ref' => "#/components/schemas/#{schema_name_without_prefix}" }
|
51
|
+
end
|
52
|
+
|
53
|
+
schema = build_basic_schema(node)
|
54
|
+
|
55
|
+
# Add enum if specified
|
56
|
+
if node.comment_data&.has_enum?
|
57
|
+
schema['enum'] = node.comment_data.enum
|
58
|
+
end
|
59
|
+
|
60
|
+
# Add format if specified
|
61
|
+
if node.comment_data&.has_format?
|
62
|
+
schema['format'] = node.comment_data.format
|
63
|
+
end
|
64
|
+
|
65
|
+
# Add example if specified
|
66
|
+
if node.comment_data&.has_example?
|
67
|
+
schema['example'] = node.comment_data.example
|
68
|
+
end
|
69
|
+
|
70
|
+
schema
|
71
|
+
end
|
72
|
+
|
73
|
+
# Visit an array node
|
74
|
+
# @param node [RailsOpenapiGen::AstNodes::ArrayNode] Array node
|
75
|
+
# @return [Hash] Array schema
|
76
|
+
def visit_array(node)
|
77
|
+
schema = {
|
78
|
+
'type' => 'array',
|
79
|
+
'description' => extract_description(node.comment_data)
|
80
|
+
}.compact
|
81
|
+
|
82
|
+
# Process array items
|
83
|
+
if node.items.any?
|
84
|
+
# Check if all items are properties (from partial processing)
|
85
|
+
# If so, combine them into a single object schema
|
86
|
+
if node.items.all? { |item| item.is_a?(RailsOpenapiGen::AstNodes::PropertyNode) }
|
87
|
+
# Combine all property nodes into a single object schema
|
88
|
+
properties = {}
|
89
|
+
required = []
|
90
|
+
|
91
|
+
node.items.each do |property_node|
|
92
|
+
prop_schema = process(property_node)
|
93
|
+
next unless prop_schema
|
94
|
+
|
95
|
+
properties[property_node.property_name] = prop_schema
|
96
|
+
|
97
|
+
# Add to required if property is required
|
98
|
+
if required?(property_node)
|
99
|
+
required << property_node.property_name
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
object_schema = { 'type' => 'object' }
|
104
|
+
object_schema['properties'] = properties if properties.any?
|
105
|
+
object_schema['required'] = required if required.any?
|
106
|
+
|
107
|
+
schema['items'] = object_schema
|
108
|
+
else
|
109
|
+
# If we have actual item nodes, process them
|
110
|
+
item_schemas = process_nodes(node.items)
|
111
|
+
|
112
|
+
schema['items'] = if item_schemas.length == 1
|
113
|
+
item_schemas.first
|
114
|
+
elsif item_schemas.length > 1
|
115
|
+
# Multiple item types - use oneOf
|
116
|
+
{ 'oneOf' => item_schemas }
|
117
|
+
else
|
118
|
+
{ 'type' => 'object' }
|
119
|
+
end
|
120
|
+
end
|
121
|
+
else
|
122
|
+
# Use comment data for item type
|
123
|
+
items_spec = node.comment_data&.array_items
|
124
|
+
schema['items'] = items_spec || { 'type' => 'object' }
|
125
|
+
end
|
126
|
+
|
127
|
+
schema
|
128
|
+
end
|
129
|
+
|
130
|
+
# Visit an object node
|
131
|
+
# @param node [RailsOpenapiGen::AstNodes::ObjectNode] Object node
|
132
|
+
# @return [Hash] Object schema
|
133
|
+
def visit_object(node)
|
134
|
+
schema = {
|
135
|
+
'type' => 'object',
|
136
|
+
'description' => extract_description(node.comment_data)
|
137
|
+
}.compact
|
138
|
+
|
139
|
+
# Process properties
|
140
|
+
if node.properties.any?
|
141
|
+
properties = {}
|
142
|
+
required = []
|
143
|
+
|
144
|
+
node.properties.each do |property|
|
145
|
+
prop_schema = process(property)
|
146
|
+
next unless prop_schema
|
147
|
+
|
148
|
+
properties[property.property_name] = prop_schema
|
149
|
+
|
150
|
+
# Add to required if property is required
|
151
|
+
if required?(property)
|
152
|
+
required << property.property_name
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
schema['properties'] = properties if properties.any?
|
157
|
+
schema['required'] = required if required.any?
|
158
|
+
end
|
159
|
+
|
160
|
+
schema
|
161
|
+
end
|
162
|
+
|
163
|
+
# Visit a partial node
|
164
|
+
# @param node [RailsOpenapiGen::AstNodes::PartialNode] Partial node
|
165
|
+
# @return [Hash] Schema from partial
|
166
|
+
def visit_partial(node)
|
167
|
+
puts "🔍 DEBUG: Processing partial node: #{node.partial_path}, properties: #{node.properties.size}" if ENV['RAILS_OPENAPI_DEBUG']
|
168
|
+
|
169
|
+
# Process the properties from the parsed partial
|
170
|
+
if node.properties.any?
|
171
|
+
# Create an object schema with the partial's properties
|
172
|
+
properties = {}
|
173
|
+
required = []
|
174
|
+
|
175
|
+
node.properties.each do |property|
|
176
|
+
puts "🔍 DEBUG: Processing partial property: #{property.property_name} (#{property.class.name})" if ENV['RAILS_OPENAPI_DEBUG']
|
177
|
+
prop_schema = process(property)
|
178
|
+
next unless prop_schema
|
179
|
+
|
180
|
+
properties[property.property_name] = prop_schema
|
181
|
+
|
182
|
+
if required?(property)
|
183
|
+
required << property.property_name
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
schema = { 'type' => 'object' }
|
188
|
+
schema['properties'] = properties if properties.any?
|
189
|
+
schema['required'] = required if required.any?
|
190
|
+
schema['description'] = extract_description(node.comment_data) if node.comment_data&.description
|
191
|
+
|
192
|
+
puts "🔍 DEBUG: Partial schema generated: #{schema.keys}" if ENV['RAILS_OPENAPI_DEBUG']
|
193
|
+
schema
|
194
|
+
else
|
195
|
+
puts "🔍 DEBUG: Partial has no properties, using fallback" if ENV['RAILS_OPENAPI_DEBUG']
|
196
|
+
# Fallback to basic object schema
|
197
|
+
{
|
198
|
+
'type' => 'object',
|
199
|
+
'description' => extract_description(node.comment_data) || "Partial: #{node.partial_path}"
|
200
|
+
}.compact
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
# Visit an unknown node type
|
205
|
+
# @param node [RailsOpenapiGen::AstNodes::BaseNode] Unknown node
|
206
|
+
# @return [Hash] Basic object schema
|
207
|
+
def visit_unknown(_node)
|
208
|
+
{
|
209
|
+
'type' => 'object',
|
210
|
+
'description' => 'Unknown node type'
|
211
|
+
}
|
212
|
+
end
|
213
|
+
|
214
|
+
private
|
215
|
+
|
216
|
+
# Build basic schema from node
|
217
|
+
# @param node [RailsOpenapiGen::AstNodes::BaseNode] Node
|
218
|
+
# @return [Hash] Basic schema
|
219
|
+
def build_basic_schema(node)
|
220
|
+
{
|
221
|
+
'type' => extract_type(node.comment_data),
|
222
|
+
'description' => extract_description(node.comment_data)
|
223
|
+
}.compact
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RailsOpenapiGen::Processors
|
4
|
+
# Base class for all processors that convert AST nodes to output formats
|
5
|
+
# Implements visitor pattern for processing different node types
|
6
|
+
class BaseProcessor
|
7
|
+
# Process an AST node and return converted output
|
8
|
+
# @param node [RailsOpenapiGen::AstNodes::BaseNode] AST node to process
|
9
|
+
# @return [Object] Processed output
|
10
|
+
def process(node)
|
11
|
+
return nil unless node
|
12
|
+
|
13
|
+
if node.respond_to?(:accept)
|
14
|
+
node.accept(self)
|
15
|
+
else
|
16
|
+
visit(node)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# Generic visit method for nodes that don't implement accept
|
21
|
+
# @param node [RailsOpenapiGen::AstNodes::BaseNode] Node to visit
|
22
|
+
# @return [Object] Processed output
|
23
|
+
def visit(node)
|
24
|
+
case node
|
25
|
+
when RailsOpenapiGen::AstNodes::PropertyNode
|
26
|
+
visit_property(node)
|
27
|
+
when RailsOpenapiGen::AstNodes::ArrayNode
|
28
|
+
visit_array(node)
|
29
|
+
when RailsOpenapiGen::AstNodes::ObjectNode
|
30
|
+
visit_object(node)
|
31
|
+
when RailsOpenapiGen::AstNodes::PartialNode
|
32
|
+
visit_partial(node)
|
33
|
+
else
|
34
|
+
visit_unknown(node)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Visit a property node
|
39
|
+
# @param node [RailsOpenapiGen::AstNodes::PropertyNode] Property node
|
40
|
+
# @return [Object] Processed output
|
41
|
+
def visit_property(_node)
|
42
|
+
raise NotImplementedError, "#{self.class} must implement #visit_property"
|
43
|
+
end
|
44
|
+
|
45
|
+
# Visit an array node
|
46
|
+
# @param node [RailsOpenapiGen::AstNodes::ArrayNode] Array node
|
47
|
+
# @return [Object] Processed output
|
48
|
+
def visit_array(_node)
|
49
|
+
raise NotImplementedError, "#{self.class} must implement #visit_array"
|
50
|
+
end
|
51
|
+
|
52
|
+
# Visit an object node
|
53
|
+
# @param node [RailsOpenapiGen::AstNodes::ObjectNode] Object node
|
54
|
+
# @return [Object] Processed output
|
55
|
+
def visit_object(_node)
|
56
|
+
raise NotImplementedError, "#{self.class} must implement #visit_object"
|
57
|
+
end
|
58
|
+
|
59
|
+
# Visit a partial node
|
60
|
+
# @param node [RailsOpenapiGen::AstNodes::PartialNode] Partial node
|
61
|
+
# @return [Object] Processed output
|
62
|
+
def visit_partial(_node)
|
63
|
+
raise NotImplementedError, "#{self.class} must implement #visit_partial"
|
64
|
+
end
|
65
|
+
|
66
|
+
# Visit an unknown node type
|
67
|
+
# @param node [RailsOpenapiGen::AstNodes::BaseNode] Unknown node
|
68
|
+
# @return [Object] Processed output
|
69
|
+
def visit_unknown(_node)
|
70
|
+
# Default implementation returns nil for unknown nodes
|
71
|
+
nil
|
72
|
+
end
|
73
|
+
|
74
|
+
# Process multiple nodes
|
75
|
+
# @param nodes [Array<RailsOpenapiGen::AstNodes::BaseNode>] Nodes to process
|
76
|
+
# @return [Array<Object>] Array of processed outputs
|
77
|
+
def process_nodes(nodes)
|
78
|
+
return [] unless nodes.respond_to?(:map)
|
79
|
+
|
80
|
+
nodes.map { |node| process(node) }.compact
|
81
|
+
end
|
82
|
+
|
83
|
+
# Check if node should be processed based on conditions
|
84
|
+
# @param node [RailsOpenapiGen::AstNodes::BaseNode] Node to check
|
85
|
+
# @return [Boolean] True if node should be processed
|
86
|
+
def should_process?(node)
|
87
|
+
return false unless node
|
88
|
+
|
89
|
+
# Can be overridden in subclasses for custom filtering
|
90
|
+
true
|
91
|
+
end
|
92
|
+
|
93
|
+
protected
|
94
|
+
|
95
|
+
# Extract description from comment data
|
96
|
+
# @param comment_data [RailsOpenapiGen::AstNodes::CommentData, nil] Comment data
|
97
|
+
# @return [String, nil] Description
|
98
|
+
def extract_description(comment_data)
|
99
|
+
comment_data&.description
|
100
|
+
end
|
101
|
+
|
102
|
+
# Extract OpenAPI type from comment data
|
103
|
+
# @param comment_data [RailsOpenapiGen::AstNodes::CommentData, nil] Comment data
|
104
|
+
# @param default_type [String] Default type if not specified
|
105
|
+
# @return [String] OpenAPI type
|
106
|
+
def extract_type(comment_data, default_type = 'string')
|
107
|
+
comment_data&.openapi_type || default_type
|
108
|
+
end
|
109
|
+
|
110
|
+
# Check if property is required
|
111
|
+
# @param node [RailsOpenapiGen::AstNodes::BaseNode] Node to check
|
112
|
+
# @return [Boolean] True if property is required
|
113
|
+
def required?(node)
|
114
|
+
node.respond_to?(:required?) ? node.required? : false
|
115
|
+
end
|
116
|
+
|
117
|
+
# Check if property is conditional
|
118
|
+
# @param node [RailsOpenapiGen::AstNodes::BaseNode] Node to check
|
119
|
+
# @return [Boolean] True if property is conditional
|
120
|
+
def conditional?(node)
|
121
|
+
node.respond_to?(:is_conditional) ? node.is_conditional : false
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|