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,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,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsOpenapiGen
4
+ module Parsers
5
+ class ConditionalParser
6
+ REGEX = /@openapi\s+conditional:true\s*$/
7
+
8
+ def parse(_comment_text)
9
+ { conditional: true }
10
+ end
11
+ end
12
+ end
13
+ 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