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,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'base_attribute_parser'
|
4
|
+
|
5
|
+
module RailsOpenapiGen
|
6
|
+
module Parsers
|
7
|
+
class AttributeParser
|
8
|
+
include BaseAttributeParser
|
9
|
+
|
10
|
+
REGEX = /@openapi\s+(.+)$/
|
11
|
+
|
12
|
+
def parse(comment_text)
|
13
|
+
# Skip conditional pattern which is handled by ConditionalParser
|
14
|
+
return nil if comment_text.match?(/@openapi\s+conditional:true\s*$/)
|
15
|
+
|
16
|
+
match = comment_text.match(REGEX)
|
17
|
+
return nil unless match
|
18
|
+
|
19
|
+
openapi_content = match[1].strip
|
20
|
+
parse_attributes(openapi_content)
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def parse_attributes(content)
|
26
|
+
attributes = {}
|
27
|
+
|
28
|
+
parts = parse_key_value_pairs(content)
|
29
|
+
|
30
|
+
# First part should be field_name:type
|
31
|
+
if parts.any?
|
32
|
+
first_key, first_value = parts.first
|
33
|
+
attributes[:field_name] = first_key
|
34
|
+
attributes[:type] = clean_value(first_value)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Remaining parts are attributes
|
38
|
+
parts[1..-1]&.each do |key, value|
|
39
|
+
cleaned_value = clean_value(value)
|
40
|
+
|
41
|
+
case key
|
42
|
+
when "required"
|
43
|
+
attributes[:required] = cleaned_value
|
44
|
+
when "description"
|
45
|
+
attributes[:description] = cleaned_value
|
46
|
+
when "enum"
|
47
|
+
attributes[:enum] = parse_enum(cleaned_value)
|
48
|
+
else
|
49
|
+
attributes[key.to_sym] = cleaned_value
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
attributes
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RailsOpenapiGen
|
4
|
+
module Parsers
|
5
|
+
module BaseAttributeParser
|
6
|
+
private
|
7
|
+
|
8
|
+
def clean_value(value)
|
9
|
+
value = value.strip
|
10
|
+
|
11
|
+
if value.start_with?('"') && value.end_with?('"')
|
12
|
+
value[1..-2]
|
13
|
+
elsif value.start_with?('[') && value.end_with?(']')
|
14
|
+
value
|
15
|
+
else
|
16
|
+
value
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def parse_enum(value)
|
21
|
+
return value unless value.is_a?(String) && value.start_with?('[') && value.end_with?(']')
|
22
|
+
|
23
|
+
inner = value[1..-2]
|
24
|
+
|
25
|
+
items = inner.split(',').map do |item|
|
26
|
+
item = item.strip
|
27
|
+
if item.start_with?('"') && item.end_with?('"')
|
28
|
+
item[1..-2]
|
29
|
+
else
|
30
|
+
item
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
items
|
35
|
+
end
|
36
|
+
|
37
|
+
def parse_key_value_pairs(content)
|
38
|
+
content.scan(/(\w+):("[^"]*"|\[[^\]]*\]|\S+)/)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'base_attribute_parser'
|
4
|
+
|
5
|
+
module RailsOpenapiGen
|
6
|
+
module Parsers
|
7
|
+
class BodyParser
|
8
|
+
include BaseAttributeParser
|
9
|
+
|
10
|
+
REGEX = /@openapi_body\s+(.+)$/
|
11
|
+
|
12
|
+
def parse(comment_text)
|
13
|
+
match = comment_text.match(REGEX)
|
14
|
+
return nil unless match
|
15
|
+
|
16
|
+
openapi_content = match[1].strip
|
17
|
+
{ body_parameter: parse_parameter_attributes(openapi_content) }
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def parse_parameter_attributes(content)
|
23
|
+
attributes = {}
|
24
|
+
|
25
|
+
parts = parse_key_value_pairs(content)
|
26
|
+
|
27
|
+
# First part should be parameter_name:type
|
28
|
+
if parts.any?
|
29
|
+
first_key, first_value = parts.first
|
30
|
+
attributes[:name] = first_key
|
31
|
+
attributes[:type] = clean_value(first_value)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Remaining parts are attributes
|
35
|
+
parts[1..-1]&.each do |key, value|
|
36
|
+
cleaned_value = clean_value(value)
|
37
|
+
|
38
|
+
case key
|
39
|
+
when "required"
|
40
|
+
attributes[:required] = cleaned_value
|
41
|
+
when "description"
|
42
|
+
attributes[:description] = cleaned_value
|
43
|
+
when "enum"
|
44
|
+
attributes[:enum] = parse_enum(cleaned_value)
|
45
|
+
when "format"
|
46
|
+
attributes[:format] = cleaned_value
|
47
|
+
when "minimum", "min"
|
48
|
+
attributes[:minimum] = cleaned_value.to_i
|
49
|
+
when "maximum", "max"
|
50
|
+
attributes[:maximum] = cleaned_value.to_i
|
51
|
+
when "example"
|
52
|
+
attributes[:example] = cleaned_value
|
53
|
+
else
|
54
|
+
attributes[key.to_sym] = cleaned_value
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
attributes
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'base_attribute_parser'
|
4
|
+
|
5
|
+
module RailsOpenapiGen
|
6
|
+
module Parsers
|
7
|
+
class OperationParser
|
8
|
+
include BaseAttributeParser
|
9
|
+
|
10
|
+
REGEX = /@openapi_operation\s+(.+)$/
|
11
|
+
|
12
|
+
def parse(comment_text)
|
13
|
+
match = comment_text.match(REGEX)
|
14
|
+
return nil unless match
|
15
|
+
|
16
|
+
openapi_content = match[1].strip
|
17
|
+
{ operation: parse_operation_attributes(openapi_content) }
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def parse_operation_attributes(content)
|
23
|
+
attributes = {}
|
24
|
+
|
25
|
+
parts = parse_key_value_pairs(content)
|
26
|
+
|
27
|
+
parts.each do |key, value|
|
28
|
+
cleaned_value = clean_value(value)
|
29
|
+
|
30
|
+
case key
|
31
|
+
when "summary"
|
32
|
+
attributes[:summary] = cleaned_value
|
33
|
+
when "description"
|
34
|
+
attributes[:description] = cleaned_value
|
35
|
+
when "operationId"
|
36
|
+
attributes[:operationId] = cleaned_value
|
37
|
+
when "tags"
|
38
|
+
attributes[:tags] = parse_enum(cleaned_value)
|
39
|
+
when "status", "statusCode", "status_code"
|
40
|
+
attributes[:status] = cleaned_value
|
41
|
+
else
|
42
|
+
attributes[key.to_sym] = cleaned_value
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
attributes
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'base_attribute_parser'
|
4
|
+
|
5
|
+
module RailsOpenapiGen
|
6
|
+
module Parsers
|
7
|
+
class ParamParser
|
8
|
+
include BaseAttributeParser
|
9
|
+
|
10
|
+
REGEX = /@openapi_param\s+(.+)$/
|
11
|
+
|
12
|
+
def parse(comment_text)
|
13
|
+
match = comment_text.match(REGEX)
|
14
|
+
return nil unless match
|
15
|
+
|
16
|
+
openapi_content = match[1].strip
|
17
|
+
{ parameter: parse_parameter_attributes(openapi_content) }
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def parse_parameter_attributes(content)
|
23
|
+
attributes = {}
|
24
|
+
|
25
|
+
parts = parse_key_value_pairs(content)
|
26
|
+
|
27
|
+
# First part should be parameter_name:type
|
28
|
+
if parts.any?
|
29
|
+
first_key, first_value = parts.first
|
30
|
+
attributes[:name] = first_key
|
31
|
+
attributes[:type] = clean_value(first_value)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Remaining parts are attributes
|
35
|
+
parts[1..-1]&.each do |key, value|
|
36
|
+
cleaned_value = clean_value(value)
|
37
|
+
|
38
|
+
case key
|
39
|
+
when "required"
|
40
|
+
attributes[:required] = cleaned_value
|
41
|
+
when "description"
|
42
|
+
attributes[:description] = cleaned_value
|
43
|
+
when "enum"
|
44
|
+
attributes[:enum] = parse_enum(cleaned_value)
|
45
|
+
when "format"
|
46
|
+
attributes[:format] = cleaned_value
|
47
|
+
when "minimum", "min"
|
48
|
+
attributes[:minimum] = cleaned_value.to_i
|
49
|
+
when "maximum", "max"
|
50
|
+
attributes[:maximum] = cleaned_value.to_i
|
51
|
+
when "example"
|
52
|
+
attributes[:example] = cleaned_value
|
53
|
+
else
|
54
|
+
attributes[key.to_sym] = cleaned_value
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
attributes
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'base_attribute_parser'
|
4
|
+
|
5
|
+
module RailsOpenapiGen
|
6
|
+
module Parsers
|
7
|
+
class QueryParser
|
8
|
+
include BaseAttributeParser
|
9
|
+
|
10
|
+
REGEX = /@openapi_query\s+(.+)$/
|
11
|
+
|
12
|
+
def parse(comment_text)
|
13
|
+
match = comment_text.match(REGEX)
|
14
|
+
return nil unless match
|
15
|
+
|
16
|
+
openapi_content = match[1].strip
|
17
|
+
{ query_parameter: parse_parameter_attributes(openapi_content) }
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def parse_parameter_attributes(content)
|
23
|
+
attributes = {}
|
24
|
+
|
25
|
+
parts = parse_key_value_pairs(content)
|
26
|
+
|
27
|
+
# First part should be parameter_name:type
|
28
|
+
if parts.any?
|
29
|
+
first_key, first_value = parts.first
|
30
|
+
attributes[:name] = first_key
|
31
|
+
attributes[:type] = clean_value(first_value)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Remaining parts are attributes
|
35
|
+
parts[1..-1]&.each do |key, value|
|
36
|
+
cleaned_value = clean_value(value)
|
37
|
+
|
38
|
+
case key
|
39
|
+
when "required"
|
40
|
+
attributes[:required] = cleaned_value
|
41
|
+
when "description"
|
42
|
+
attributes[:description] = cleaned_value
|
43
|
+
when "enum"
|
44
|
+
attributes[:enum] = parse_enum(cleaned_value)
|
45
|
+
when "format"
|
46
|
+
attributes[:format] = cleaned_value
|
47
|
+
when "minimum", "min"
|
48
|
+
attributes[:minimum] = cleaned_value.to_i
|
49
|
+
when "maximum", "max"
|
50
|
+
attributes[:maximum] = cleaned_value.to_i
|
51
|
+
when "example"
|
52
|
+
attributes[:example] = cleaned_value
|
53
|
+
else
|
54
|
+
attributes[key.to_sym] = cleaned_value
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
attributes
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,153 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "parser/current"
|
4
|
+
require_relative "template_processors/jbuilder_template_processor"
|
5
|
+
|
6
|
+
module RailsOpenapiGen
|
7
|
+
module Parsers
|
8
|
+
class ControllerParser
|
9
|
+
attr_reader :route, :template_processor
|
10
|
+
|
11
|
+
# Initializes controller parser with route information and template processor
|
12
|
+
# @param route [Hash] Route hash containing controller and action info
|
13
|
+
# @param template_processor [ResponseTemplateProcessor] Template processor for extracting template paths
|
14
|
+
def initialize(route, template_processor: nil)
|
15
|
+
@route = route
|
16
|
+
@template_processor = template_processor || TemplateProcessors::JbuilderTemplateProcessor.new(route[:controller], route[:action])
|
17
|
+
end
|
18
|
+
|
19
|
+
# Parses controller to find action method and response template
|
20
|
+
# @return [Hash] Controller info including paths and parameters
|
21
|
+
def parse
|
22
|
+
controller_path = find_controller_file
|
23
|
+
return {} unless controller_path
|
24
|
+
|
25
|
+
content = File.read(controller_path)
|
26
|
+
ast = Parser::CurrentRuby.parse(content)
|
27
|
+
|
28
|
+
action_node = find_action_method(ast)
|
29
|
+
return {} unless action_node
|
30
|
+
|
31
|
+
template_path = extract_template_path(action_node)
|
32
|
+
parameters = extract_parameters_from_comments(content, action_node)
|
33
|
+
|
34
|
+
{
|
35
|
+
controller_path: controller_path,
|
36
|
+
jbuilder_path: template_path, # Keep existing key name for backward compatibility
|
37
|
+
action: route[:action],
|
38
|
+
parameters: parameters
|
39
|
+
}
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
# Finds the controller file path based on route information
|
45
|
+
# @return [String, nil] Path to controller file or nil if not found
|
46
|
+
def find_controller_file
|
47
|
+
controller_name = "#{route[:controller]}_controller"
|
48
|
+
possible_paths = [
|
49
|
+
Rails.root.join("app", "controllers", "#{controller_name}.rb")
|
50
|
+
]
|
51
|
+
|
52
|
+
# Handle nested controllers
|
53
|
+
if route[:controller].include?("/")
|
54
|
+
parts = route[:controller].split("/")
|
55
|
+
nested_path = Rails.root.join("app", "controllers", *parts[0..-2], "#{parts.last}_controller.rb")
|
56
|
+
possible_paths << nested_path
|
57
|
+
end
|
58
|
+
|
59
|
+
possible_paths.find { |path| File.exist?(path) }
|
60
|
+
end
|
61
|
+
|
62
|
+
# Finds action method node in the AST
|
63
|
+
# @param ast [Parser::AST::Node] Controller AST
|
64
|
+
# @return [Parser::AST::Node, nil] Action method node or nil
|
65
|
+
def find_action_method(ast)
|
66
|
+
return nil unless ast
|
67
|
+
|
68
|
+
processor = ActionMethodProcessor.new(route[:action])
|
69
|
+
processor.process(ast)
|
70
|
+
processor.action_node
|
71
|
+
end
|
72
|
+
|
73
|
+
# Extracts template path from action method using template processor
|
74
|
+
# @param action_node [Parser::AST::Node] Action method node
|
75
|
+
# @return [String, nil] Path to template or nil
|
76
|
+
def extract_template_path(action_node)
|
77
|
+
return nil unless action_node
|
78
|
+
|
79
|
+
# Try to extract explicit template path from action
|
80
|
+
template_path = template_processor.extract_template_path(action_node, route)
|
81
|
+
|
82
|
+
# If no explicit template found, check for default template
|
83
|
+
template_path ||= template_processor.find_default_template(route)
|
84
|
+
|
85
|
+
template_path
|
86
|
+
end
|
87
|
+
|
88
|
+
# Extracts parameter definitions from comments above action method
|
89
|
+
# @param content [String] Controller file content
|
90
|
+
# @param action_node [Parser::AST::Node] Action method node
|
91
|
+
# @return [Hash] Parameters hash with path, query, and body parameters
|
92
|
+
def extract_parameters_from_comments(content, action_node)
|
93
|
+
return {} unless action_node
|
94
|
+
|
95
|
+
lines = content.lines
|
96
|
+
action_line = action_node.location.line - 1 # Convert to 0-based index
|
97
|
+
|
98
|
+
# Look for comments before the action method
|
99
|
+
parameters = {
|
100
|
+
path_parameters: [],
|
101
|
+
query_parameters: [],
|
102
|
+
body_parameters: []
|
103
|
+
}
|
104
|
+
|
105
|
+
comment_parser = RailsOpenapiGen::Parsers::CommentParser.new
|
106
|
+
|
107
|
+
# Scan backwards from the action line to find comments
|
108
|
+
(action_line - 1).downto(0) do |line_index|
|
109
|
+
line = lines[line_index].strip
|
110
|
+
|
111
|
+
# Stop if we encounter a non-comment line that's not empty
|
112
|
+
break if !line.empty? && !line.start_with?('#')
|
113
|
+
|
114
|
+
next if line.empty? || !line.include?('@openapi')
|
115
|
+
|
116
|
+
parsed = comment_parser.parse(line)
|
117
|
+
next unless parsed
|
118
|
+
|
119
|
+
if parsed[:parameter]
|
120
|
+
parameters[:path_parameters] << parsed[:parameter]
|
121
|
+
elsif parsed[:query_parameter]
|
122
|
+
parameters[:query_parameters] << parsed[:query_parameter]
|
123
|
+
elsif parsed[:body_parameter]
|
124
|
+
parameters[:body_parameters] << parsed[:body_parameter]
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
parameters
|
129
|
+
end
|
130
|
+
|
131
|
+
class ActionMethodProcessor < Parser::AST::Processor
|
132
|
+
attr_reader :action_node
|
133
|
+
|
134
|
+
# Initializes processor to find specific action method
|
135
|
+
# @param action_name [String, Symbol] Name of action to find
|
136
|
+
def initialize(action_name)
|
137
|
+
@action_name = action_name.to_sym
|
138
|
+
@action_node = nil
|
139
|
+
end
|
140
|
+
|
141
|
+
# Processes method definition nodes
|
142
|
+
# @param node [Parser::AST::Node] Method definition node
|
143
|
+
# @return [void]
|
144
|
+
def on_def(node)
|
145
|
+
method_name = node.children[0]
|
146
|
+
@action_node = node if method_name == @action_name
|
147
|
+
super
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|