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.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +40 -0
  3. data/CLAUDE.md +17 -5
  4. data/README.md +25 -0
  5. data/lib/rails-openapi-gen/ast_nodes/array_node.rb +101 -0
  6. data/lib/rails-openapi-gen/ast_nodes/base_node.rb +139 -0
  7. data/lib/rails-openapi-gen/ast_nodes/comment_data.rb +180 -0
  8. data/lib/rails-openapi-gen/ast_nodes/node_factory.rb +206 -0
  9. data/lib/rails-openapi-gen/ast_nodes/object_node.rb +129 -0
  10. data/lib/rails-openapi-gen/ast_nodes/partial_node.rb +111 -0
  11. data/lib/rails-openapi-gen/ast_nodes/property_node.rb +74 -0
  12. data/lib/rails-openapi-gen/ast_nodes.rb +129 -0
  13. data/lib/rails-openapi-gen/configuration.rb +154 -22
  14. data/lib/rails-openapi-gen/debug_helpers.rb +185 -0
  15. data/lib/rails-openapi-gen/engine.rb +1 -1
  16. data/lib/rails-openapi-gen/generators/yaml_generator.rb +242 -27
  17. data/lib/rails-openapi-gen/generators.rb +5 -0
  18. data/lib/rails-openapi-gen/importer.rb +164 -145
  19. data/lib/rails-openapi-gen/parsers/comment_parser.rb +1 -1
  20. data/lib/rails-openapi-gen/parsers/comment_parsers/attribute_parser.rb +7 -7
  21. data/lib/rails-openapi-gen/parsers/comment_parsers/base_attribute_parser.rb +5 -9
  22. data/lib/rails-openapi-gen/parsers/comment_parsers/body_parser.rb +6 -6
  23. data/lib/rails-openapi-gen/parsers/comment_parsers/conditional_parser.rb +1 -1
  24. data/lib/rails-openapi-gen/parsers/comment_parsers/operation_parser.rb +5 -5
  25. data/lib/rails-openapi-gen/parsers/comment_parsers/param_parser.rb +6 -6
  26. data/lib/rails-openapi-gen/parsers/comment_parsers/query_parser.rb +6 -6
  27. data/lib/rails-openapi-gen/parsers/controller_parser.rb +64 -20
  28. data/lib/rails-openapi-gen/parsers/jbuilder/ast_parser.rb +914 -0
  29. data/lib/rails-openapi-gen/parsers/jbuilder/call_detectors/array_call_detector.rb +103 -0
  30. data/lib/rails-openapi-gen/parsers/jbuilder/call_detectors/base_detector.rb +107 -0
  31. data/lib/rails-openapi-gen/parsers/jbuilder/call_detectors/cache_call_detector.rb +112 -0
  32. data/lib/rails-openapi-gen/parsers/jbuilder/call_detectors/json_call_detector.rb +91 -0
  33. data/lib/rails-openapi-gen/parsers/jbuilder/call_detectors/key_format_detector.rb +27 -0
  34. data/lib/rails-openapi-gen/parsers/jbuilder/call_detectors/null_handling_detector.rb +27 -0
  35. data/lib/rails-openapi-gen/parsers/jbuilder/call_detectors/object_manipulation_detector.rb +27 -0
  36. data/lib/rails-openapi-gen/parsers/jbuilder/call_detectors/partial_call_detector.rb +125 -0
  37. data/lib/rails-openapi-gen/parsers/jbuilder/call_detectors.rb +95 -0
  38. data/lib/rails-openapi-gen/parsers/jbuilder/jbuilder_parser.rb +39 -0
  39. data/lib/rails-openapi-gen/parsers/jbuilder/operation_comment_parser.rb +26 -0
  40. data/lib/rails-openapi-gen/parsers/jbuilder/processors/array_processor.rb +266 -0
  41. data/lib/rails-openapi-gen/parsers/jbuilder/processors/base_processor.rb +235 -0
  42. data/lib/rails-openapi-gen/parsers/jbuilder/processors/composite_processor.rb +97 -0
  43. data/lib/rails-openapi-gen/parsers/jbuilder/processors/object_processor.rb +176 -0
  44. data/lib/rails-openapi-gen/parsers/jbuilder/processors/partial_processor.rb +69 -0
  45. data/lib/rails-openapi-gen/parsers/jbuilder/processors/property_processor.rb +68 -0
  46. data/lib/rails-openapi-gen/parsers/jbuilder/processors.rb +10 -0
  47. data/lib/rails-openapi-gen/parsers/jbuilder/property_comment_parser.rb +26 -0
  48. data/lib/rails-openapi-gen/parsers/jbuilder.rb +10 -0
  49. data/lib/rails-openapi-gen/parsers/routes_parser.rb +83 -9
  50. data/lib/rails-openapi-gen/parsers/template_processors/jbuilder_template_processor.rb +125 -131
  51. data/lib/rails-openapi-gen/parsers/template_processors/response_template_processor.rb +8 -12
  52. data/lib/rails-openapi-gen/parsers/template_processors.rb +6 -0
  53. data/lib/rails-openapi-gen/parsers.rb +9 -0
  54. data/lib/rails-openapi-gen/processors/ast_to_schema_processor.rb +226 -0
  55. data/lib/rails-openapi-gen/processors/base_processor.rb +124 -0
  56. data/lib/rails-openapi-gen/processors/component_schema_processor.rb +35 -0
  57. data/lib/rails-openapi-gen/processors/openapi_schema_processor.rb +218 -0
  58. data/lib/rails-openapi-gen/processors.rb +7 -0
  59. data/lib/rails-openapi-gen/railtie.rb +1 -1
  60. data/lib/rails-openapi-gen/tasks/openapi.rake +4 -4
  61. data/lib/rails-openapi-gen/version.rb +1 -1
  62. data/lib/rails-openapi-gen.rb +169 -196
  63. data/lib/tasks/openapi_import.rake +35 -36
  64. data/rails-openapi-gen.gemspec +6 -5
  65. metadata +62 -23
  66. data/lib/rails-openapi-gen/parsers/jbuilder_parser.rb +0 -529
@@ -2,7 +2,7 @@
2
2
 
3
3
  module RailsOpenapiGen
4
4
  class Configuration
5
- attr_accessor :openapi_version, :info, :servers, :route_patterns, :output
5
+ attr_accessor :openapi_version, :info, :servers, :route_patterns, :output, :view_paths
6
6
 
7
7
  # Initializes configuration with default values
8
8
  def initialize
@@ -27,15 +27,38 @@ module RailsOpenapiGen
27
27
  filename: "openapi.yaml",
28
28
  split_files: true
29
29
  }
30
+ @view_paths = {
31
+ api_prefix: nil, # No API path prefix removal by default
32
+ component_prefix: nil # No component name prefix removal by default
33
+ }
30
34
  end
31
35
 
32
36
  # TODO: Loads configuration from Ruby file
33
37
  # @param file_path [String, nil] Path to configuration file (defaults to config/openapi.rb or config/initializers/openapi.rb)
34
38
  # @return [void]
35
39
  def load_from_file(file_path = nil)
36
- # TODO: Implement configuration loading from file
37
- # This method should load configuration from Ruby files and apply it to the instance
38
- return
40
+ # If no file path provided, try to find configuration file automatically
41
+ if file_path.nil?
42
+ file_path = find_config_file
43
+ return unless file_path
44
+ end
45
+
46
+ # Check if file exists
47
+ unless File.exist?(file_path)
48
+ puts "āš ļø Configuration file not found: #{file_path}" if ENV['RAILS_OPENAPI_DEBUG']
49
+ return
50
+ end
51
+
52
+ puts "šŸ“ Loading configuration from: #{file_path}" if ENV['RAILS_OPENAPI_DEBUG']
53
+
54
+ # Load the configuration file
55
+ if file_path.end_with?('.rb')
56
+ load_ruby_config(file_path)
57
+ elsif file_path.end_with?('.yml', '.yaml')
58
+ load_yaml_config(file_path)
59
+ else
60
+ puts "āŒ Unsupported configuration file format: #{file_path}"
61
+ end
39
62
  end
40
63
 
41
64
  # Returns the full path to the output directory
@@ -71,41 +94,150 @@ module RailsOpenapiGen
71
94
  # @param path [String] Route path to check
72
95
  # @return [Boolean] True if route should be included
73
96
  def route_included?(path)
97
+ # Ensure @route_patterns is properly initialized
98
+ @route_patterns ||= { include: [/.*/], exclude: [] }
99
+
74
100
  # Check if path matches any include pattern
75
- included = @route_patterns[:include].any? { |pattern| path.match?(pattern) }
101
+ included = (@route_patterns[:include] || []).any? { |pattern| path.match?(pattern) }
76
102
  return false unless included
77
103
 
78
104
  # Check if path matches any exclude pattern
79
- excluded = @route_patterns[:exclude].any? { |pattern| path.match?(pattern) }
105
+ excluded = (@route_patterns[:exclude] || []).any? { |pattern| path.match?(pattern) }
80
106
  !excluded
81
107
  end
82
108
 
109
+ # Removes API prefix from path if configured
110
+ # @param path [String] Original route path
111
+ # @return [String] Path with prefix removed
112
+ def remove_api_prefix(path)
113
+ # Ensure @view_paths is properly initialized
114
+ @view_paths ||= { api_prefix: nil }
115
+
116
+ api_prefix = @view_paths[:api_prefix]
117
+ return path unless api_prefix
118
+
119
+ # Normalize the prefix (ensure it starts and ends properly)
120
+ normalized_prefix = "/#{api_prefix}".gsub(%r{/+}, '/').chomp('/')
121
+
122
+ # Remove the prefix if the path starts with it
123
+ if path.start_with?(normalized_prefix)
124
+ remaining_path = path[normalized_prefix.length..-1]
125
+ # Ensure the result starts with / or is empty
126
+ remaining_path.start_with?('/') ? remaining_path : "/#{remaining_path}"
127
+ else
128
+ path
129
+ end
130
+ end
131
+
132
+ # Removes component prefix from component name if configured
133
+ # @param component_name [String] Original component name (e.g., "ApiPostsPost")
134
+ # @return [String] Component name with prefix removed (e.g., "PostsPost")
135
+ def remove_component_prefix(component_name)
136
+ # Ensure @view_paths is properly initialized
137
+ @view_paths ||= { component_prefix: nil }
138
+
139
+ component_prefix = @view_paths[:component_prefix]
140
+ return component_name unless component_prefix
141
+
142
+ # Convert to PascalCase for matching
143
+ normalized_prefix = component_prefix.split(%r{[/\-_]}).map(&:capitalize).join('')
144
+
145
+ # Remove the prefix if the component name starts with it
146
+ if component_name.start_with?(normalized_prefix)
147
+ remaining_name = component_name[normalized_prefix.length..-1]
148
+ remaining_name.empty? ? component_name : remaining_name
149
+ else
150
+ component_name
151
+ end
152
+ end
153
+
83
154
  private
84
155
 
156
+ # Find configuration file in Rails application
157
+ # @return [String, nil] Path to configuration file or nil if not found
158
+ def find_config_file
159
+ # Priority order for configuration files
160
+ candidates = [
161
+ 'config/openapi.rb',
162
+ 'config/initializers/openapi.rb',
163
+ 'config/openapi.yml',
164
+ 'config/openapi.yaml'
165
+ ]
166
+
167
+ if defined?(Rails) && Rails.root
168
+ candidates.each do |candidate|
169
+ full_path = File.join(Rails.root, candidate)
170
+ return full_path if File.exist?(full_path)
171
+ end
172
+ end
173
+
174
+ nil
175
+ end
176
+
85
177
  # Loads Ruby configuration file
86
178
  # @param file_path [String] Path to configuration file
87
179
  # @return [void]
88
180
  def load_ruby_config(file_path)
89
- # Load Ruby configuration file
90
- # The file should call RailsOpenapiGen.configure block
91
- load file_path
92
-
93
- # Copy configuration from global singleton to this instance
94
- global_config = RailsOpenapiGen.configuration
95
- @openapi_version = global_config.openapi_version
96
- @info = global_config.info.dup
97
- @servers = global_config.servers.dup
98
- @route_patterns = global_config.route_patterns.dup
99
- @output = global_config.output.dup
181
+ # Save current configuration as backup
182
+ old_config = RailsOpenapiGen.instance_variable_get(:@configuration)
183
+
184
+ # Temporarily set this instance as the global configuration
185
+ # so that the configure block updates this instance
186
+ RailsOpenapiGen.instance_variable_set(:@configuration, self)
187
+
188
+ begin
189
+ # Load Ruby configuration file
190
+ # The file should call RailsOpenapiGen.configure block
191
+ load file_path
192
+ puts "āœ… Configuration loaded successfully" if ENV['RAILS_OPENAPI_DEBUG']
193
+ rescue StandardError => e
194
+ puts "āŒ Error loading configuration: #{e.message}" if ENV['RAILS_OPENAPI_DEBUG']
195
+ raise e
196
+ ensure
197
+ # Restore original global configuration
198
+ RailsOpenapiGen.instance_variable_set(:@configuration, old_config)
199
+ end
200
+ end
201
+
202
+ # Loads YAML configuration file
203
+ # @param file_path [String] Path to YAML configuration file
204
+ # @return [void]
205
+ def load_yaml_config(file_path)
206
+ require 'yaml'
207
+ config_hash = YAML.load_file(file_path)
208
+
209
+ # Apply YAML configuration to this instance
210
+ config_hash.each do |key, value|
211
+ case key.to_s
212
+ when 'openapi_version'
213
+ @openapi_version = value
214
+ when 'info'
215
+ @info = symbolize_keys(value)
216
+ when 'servers'
217
+ @servers = value.is_a?(Array) ? value : [value]
218
+ when 'route_patterns'
219
+ patterns = symbolize_keys(value)
220
+ @route_patterns[:include] = Array(patterns[:include]).map { |p| Regexp.new(p) }
221
+ @route_patterns[:exclude] = Array(patterns[:exclude]).map { |p| Regexp.new(p) }
222
+ when 'output'
223
+ @output = symbolize_keys(value)
224
+ when 'view_paths'
225
+ @view_paths = symbolize_keys(value)
226
+ end
227
+ end
228
+
229
+ puts "āœ… YAML configuration loaded successfully" if ENV['RAILS_OPENAPI_DEBUG']
100
230
  end
101
231
 
102
232
  # Returns default application name
103
233
  # @return [String] Default app name
104
234
  def default_app_name
105
235
  if defined?(Rails) && Rails.application
106
- Rails.application.class.respond_to?(:module_parent_name) ?
107
- Rails.application.class.module_parent_name :
236
+ if Rails.application.class.respond_to?(:module_parent_name)
237
+ Rails.application.class.module_parent_name
238
+ else
108
239
  "RailsApp"
240
+ end
109
241
  else
110
242
  "API"
111
243
  end
@@ -116,16 +248,16 @@ module RailsOpenapiGen
116
248
  # @return [Hash] Hash with symbolized keys
117
249
  def symbolize_keys(hash)
118
250
  return hash unless hash.is_a?(Hash)
251
+
119
252
  hash.transform_keys(&:to_sym)
120
253
  end
121
254
 
122
-
123
255
  # Loads output configuration settings
124
256
  # @param output_config [Hash] Output configuration hash
125
257
  # @return [void]
126
258
  def load_output_config(output_config)
127
259
  output_settings = symbolize_keys(output_config)
128
-
260
+
129
261
  @output[:directory] = output_settings[:directory] if output_settings[:directory]
130
262
  @output[:filename] = output_settings[:filename] if output_settings[:filename]
131
263
  @output[:split_files] = output_settings[:split_files] if output_settings.key?(:split_files)
@@ -154,4 +286,4 @@ module RailsOpenapiGen
154
286
  @configuration = Configuration.new
155
287
  end
156
288
  end
157
- end
289
+ end
@@ -0,0 +1,185 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Debug helpers for Rails OpenAPI Gen development
4
+ module RailsOpenapiGen
5
+ module DebugHelpers
6
+ # Quick debug a Jbuilder file
7
+ # @param file_path [String] Path to Jbuilder file
8
+ # @param mode [Symbol] Debug mode (:compact, :full, :export)
9
+ # @return [void]
10
+ def self.debug_jbuilder(file_path, mode: :compact)
11
+ parser = RailsOpenapiGen::Parsers::Jbuilder::JbuilderParser.new(file_path)
12
+
13
+ case mode
14
+ when :compact
15
+ parser.debug_print_compact
16
+ when :full
17
+ parser.debug_print_result
18
+ when :export
19
+ parser.debug_export_ast
20
+ else
21
+ puts "Unknown mode: #{mode}. Use :compact, :full, or :export"
22
+ end
23
+ end
24
+
25
+ # Debug multiple Jbuilder files at once
26
+ # @param pattern [String] Glob pattern for files
27
+ # @param mode [Symbol] Debug mode
28
+ # @return [void]
29
+ def self.debug_multiple(pattern = "**/*.jbuilder", mode: :compact)
30
+ files = Dir.glob(pattern)
31
+
32
+ if files.empty?
33
+ puts "No files found with pattern: #{pattern}"
34
+ return
35
+ end
36
+
37
+ puts "šŸ” Debugging #{files.size} files with pattern: #{pattern}"
38
+ puts "=" * 60
39
+
40
+ files.each_with_index do |file, index|
41
+ puts "\n[#{index + 1}/#{files.size}] #{file}"
42
+ debug_jbuilder(file, mode: mode)
43
+ end
44
+ end
45
+
46
+ # Create a sample AST tree for testing
47
+ # @return [RailsOpenapiGen::AstNodes::ObjectNode] Sample AST tree
48
+ def self.create_sample_ast
49
+ root = RailsOpenapiGen::AstNodes::ObjectNode.new(
50
+ property_name: "user",
51
+ comment_data: RailsOpenapiGen::AstNodes::CommentData.new(
52
+ type: "object",
53
+ description: "User information"
54
+ )
55
+ )
56
+
57
+ # Add name property
58
+ name_prop = RailsOpenapiGen::AstNodes::PropertyNode.new(
59
+ property_name: "name",
60
+ comment_data: RailsOpenapiGen::AstNodes::CommentData.new(
61
+ type: "string",
62
+ description: "User's full name",
63
+ required: true
64
+ )
65
+ )
66
+ root.add_child(name_prop)
67
+
68
+ # Add email property
69
+ email_prop = RailsOpenapiGen::AstNodes::PropertyNode.new(
70
+ property_name: "email",
71
+ comment_data: RailsOpenapiGen::AstNodes::CommentData.new(
72
+ type: "string",
73
+ description: "User's email address",
74
+ required: true
75
+ )
76
+ )
77
+ root.add_child(email_prop)
78
+
79
+ # Add posts array
80
+ posts_array = RailsOpenapiGen::AstNodes::ArrayNode.new(
81
+ property_name: "posts",
82
+ comment_data: RailsOpenapiGen::AstNodes::CommentData.new(
83
+ type: "array",
84
+ description: "User's posts"
85
+ )
86
+ )
87
+
88
+ # Add post title to array items
89
+ title_prop = RailsOpenapiGen::AstNodes::PropertyNode.new(
90
+ property_name: "title",
91
+ comment_data: RailsOpenapiGen::AstNodes::CommentData.new(
92
+ type: "string",
93
+ description: "Post title"
94
+ )
95
+ )
96
+ posts_array.add_child(title_prop)
97
+
98
+ root.add_child(posts_array)
99
+
100
+ root
101
+ end
102
+
103
+ # Demo the pretty print functionality
104
+ # @return [void]
105
+ def self.demo_pretty_print
106
+ puts "šŸŽØ Pretty Print Demo"
107
+ puts "=" * 50
108
+
109
+ ast = create_sample_ast
110
+
111
+ puts "\nšŸ“‹ Sample AST Structure:"
112
+ ast.pretty_print
113
+
114
+ puts "\nšŸ“„ Debug Line Format:"
115
+ puts "Root: #{ast.debug_line}"
116
+ ast.children.each { |child| puts " Child: #{child.debug_line}" }
117
+
118
+ puts "\nšŸ’¾ Export to File:"
119
+ File.open("sample_ast_debug.txt", 'w') do |f|
120
+ original_stdout = $stdout
121
+ $stdout = f
122
+ ast.pretty_print
123
+ $stdout = original_stdout
124
+ end
125
+ puts "Exported to: sample_ast_debug.txt"
126
+ end
127
+
128
+ # Show all available example files
129
+ # @return [void]
130
+ def self.list_examples
131
+ patterns = [
132
+ "examples/**/*.jbuilder",
133
+ "spec/**/*.jbuilder",
134
+ "**/*.jbuilder"
135
+ ]
136
+
137
+ puts "šŸ“ Available Example Files:"
138
+ puts "=" * 40
139
+
140
+ patterns.each do |pattern|
141
+ files = Dir.glob(pattern)
142
+ next if files.empty?
143
+
144
+ puts "\n#{pattern}:"
145
+ files.each { |file| puts " #{file}" }
146
+ end
147
+ end
148
+
149
+ # Quick analysis of a Jbuilder file
150
+ # @param file_path [String] Path to file
151
+ # @return [void]
152
+ def self.analyze_file(file_path)
153
+ unless File.exist?(file_path)
154
+ puts "āŒ File not found: #{file_path}"
155
+ return
156
+ end
157
+
158
+ content = File.read(file_path)
159
+ lines = content.lines
160
+
161
+ puts "šŸ“Š Quick Analysis: #{File.basename(file_path)}"
162
+ puts "=" * 40
163
+ puts "Lines: #{lines.size}"
164
+ puts "Size: #{content.size} bytes"
165
+
166
+ # Count different elements
167
+ openapi_comments = lines.count { |line| line.include?('@openapi') }
168
+ json_calls = lines.count { |line| line.include?('json.') }
169
+ partials = lines.count { |line| line.include?('partial!') }
170
+ arrays = lines.count { |line| line.include?('array!') }
171
+
172
+ puts "OpenAPI comments: #{openapi_comments}"
173
+ puts "JSON calls: #{json_calls}"
174
+ puts "Partials: #{partials}"
175
+ puts "Arrays: #{arrays}"
176
+
177
+ puts "\nšŸ” Preview (first 10 lines):"
178
+ lines.first(10).each_with_index do |line, i|
179
+ puts "#{(i + 1).to_s.rjust(2)}: #{line.chomp}"
180
+ end
181
+
182
+ puts "\nšŸš€ Parse with: RailsOpenapiGen::DebugHelpers.debug_jbuilder('#{file_path}')"
183
+ end
184
+ end
185
+ end
@@ -8,4 +8,4 @@ module RailsOpenapiGen
8
8
  load File.expand_path("tasks/openapi.rake", __dir__)
9
9
  end
10
10
  end
11
- end
11
+ end