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
@@ -8,8 +8,10 @@ module RailsOpenapiGen
8
8
  # Initializes importer with OpenAPI specification file
9
9
  # @param openapi_file [String, nil] Path to OpenAPI spec file (defaults to configured output)
10
10
  def initialize(openapi_file = nil)
11
- @openapi_file = openapi_file || File.join(RailsOpenapiGen.configuration.output_directory,
12
- RailsOpenapiGen.configuration.output_filename)
11
+ @openapi_file = openapi_file || File.join(
12
+ RailsOpenapiGen.configuration.output_directory,
13
+ RailsOpenapiGen.configuration.output_filename
14
+ )
13
15
  @routes_parser = Parsers::RoutesParser.new
14
16
  @processed_files = Set.new
15
17
  end
@@ -24,15 +26,15 @@ module RailsOpenapiGen
24
26
 
25
27
  openapi_spec = YAML.load_file(@openapi_file)
26
28
  routes = @routes_parser.parse
27
-
29
+
28
30
  processed_count = 0
29
31
  @partial_schemas = {} # Store schemas for partials
30
-
32
+
31
33
  # First pass: collect all response schemas
32
34
  openapi_spec['paths']&.each do |path, methods|
33
35
  methods.each do |method, operation|
34
36
  next if method == 'parameters' # Skip path-level parameters
35
-
37
+
36
38
  response_schema = extract_response_schema(operation)
37
39
  if response_schema
38
40
  if response_schema['type'] == 'array' && response_schema['items']
@@ -49,7 +51,7 @@ module RailsOpenapiGen
49
51
  elsif response_schema['properties']
50
52
  # For object responses
51
53
  collect_partial_schemas(response_schema['properties'])
52
-
54
+
53
55
  # Store the root schema if it's a detail endpoint
54
56
  if path.match?(/\{(id|\w+_id)\}$/)
55
57
  resource_name = extract_resource_name_from_path(path)
@@ -61,32 +63,52 @@ module RailsOpenapiGen
61
63
  end
62
64
  end
63
65
  end
64
-
66
+
65
67
  # Second pass: process files and add comments
66
68
  openapi_spec['paths']&.each do |path, methods|
67
69
  methods.each do |method, operation|
68
70
  next if method == 'parameters' # Skip path-level parameters
69
-
71
+
70
72
  matching_route = find_matching_route(path, method.upcase, routes)
71
73
  next unless matching_route
72
-
74
+
73
75
  controller_info = Parsers::ControllerParser.new(matching_route).parse
74
76
  next unless controller_info[:jbuilder_path]
75
-
77
+
76
78
  if add_comments_to_jbuilder(controller_info, operation, matching_route)
77
79
  processed_count += 1
78
80
  end
79
81
  end
80
82
  end
81
-
83
+
82
84
  # Third pass: process partial files
83
- process_partial_files()
84
-
85
+ process_partial_files
86
+
85
87
  puts "✅ Updated #{processed_count} Jbuilder templates with OpenAPI comments"
86
88
  end
87
89
 
88
90
  private
89
91
 
92
+ # Extracts properties from AST node for backward compatibility
93
+ # @param ast_node [RailsOpenapiGen::AstNodes::BaseNode] AST node
94
+ # @return [Array<Hash>] Properties in legacy format
95
+ def extract_properties_from_ast(ast_node)
96
+ properties = []
97
+
98
+ if ast_node.respond_to?(:properties)
99
+ ast_node.properties.each do |property|
100
+ properties << {
101
+ name: property.property_name,
102
+ type: property.comment_data&.openapi_type || 'string',
103
+ description: property.comment_data&.description,
104
+ required: property.comment_data&.required != false
105
+ }
106
+ end
107
+ end
108
+
109
+ properties
110
+ end
111
+
90
112
  # Finds Rails route matching OpenAPI path and method
91
113
  # @param openapi_path [String] OpenAPI path format (e.g., "/users/{id}")
92
114
  # @param method [String] HTTP method
@@ -95,7 +117,7 @@ module RailsOpenapiGen
95
117
  def find_matching_route(openapi_path, method, routes)
96
118
  # Convert OpenAPI path format {id} to Rails format :id
97
119
  rails_path = openapi_path.gsub(/\{(\w+)\}/, ':\1')
98
-
120
+
99
121
  routes.find do |route|
100
122
  route[:method] == method && normalize_path(route[:path]) == normalize_path(rails_path)
101
123
  end
@@ -106,9 +128,9 @@ module RailsOpenapiGen
106
128
  # @return [String] Normalized path
107
129
  def normalize_path(path)
108
130
  # Remove trailing slashes and normalize
109
- path.gsub(/\/$/, '')
131
+ path.gsub(%r{/$}, '')
110
132
  end
111
-
133
+
112
134
  # Extracts resource name from OpenAPI path
113
135
  # @param path [String] OpenAPI path (e.g., "/users/{id}/posts")
114
136
  # @return [String, nil] Singular resource name or nil
@@ -119,17 +141,17 @@ module RailsOpenapiGen
119
141
  # /users/{id} -> users
120
142
  # /users/{user_id}/posts -> posts
121
143
  # /posts/{post_id}/comments -> comments
122
-
144
+
123
145
  # Remove leading slash and split by '/'
124
- segments = path.sub(/^\//, '').split('/')
125
-
146
+ segments = path.sub(%r{^/}, '').split('/')
147
+
126
148
  # Find the last segment that doesn't contain parameters
127
149
  resource_segment = segments.reverse.find { |segment| !segment.include?('{') }
128
-
150
+
129
151
  # Return singular form if found
130
152
  resource_segment&.singularize
131
153
  end
132
-
154
+
133
155
  # Collects schemas that might be used in partials
134
156
  # @param properties [Hash] Properties hash from schema
135
157
  # @param parent_key [String, nil] Parent property key for nested objects
@@ -140,13 +162,13 @@ module RailsOpenapiGen
140
162
  if schema['type'] == 'object' && schema['properties']
141
163
  # Store schema for potential partial use
142
164
  @partial_schemas[key] = schema
143
-
165
+
144
166
  # Also store the full schema if it's from the root level
145
167
  # This helps with matching post/user objects at the top level
146
- if parent_key.nil? && (key == 'post' || key == 'posts' || key == 'user' || key == 'users')
168
+ if parent_key.nil? && %w[post posts user users].include?(key)
147
169
  @partial_schemas["_#{key.singularize}"] = schema
148
170
  end
149
-
171
+
150
172
  # Recursively collect nested schemas
151
173
  collect_partial_schemas(schema['properties'], key)
152
174
  elsif schema['type'] == 'array' && schema['items'] && schema['items']['properties']
@@ -156,170 +178,172 @@ module RailsOpenapiGen
156
178
  end
157
179
  end
158
180
  end
159
-
181
+
160
182
  # Processes partial files to add OpenAPI comments
161
183
  # @return [void]
162
184
  def process_partial_files
163
185
  # Find and process partial files
164
186
  Dir.glob(Rails.root.join('app/views/**/_*.json.jbuilder')).each do |partial_path|
165
187
  next if @processed_files.include?(partial_path)
166
-
188
+
167
189
  # Try to match partial with collected schemas
168
190
  partial_name = File.basename(partial_path, '.json.jbuilder').sub(/^_/, '')
169
-
191
+
170
192
  # Look for matching schema based on partial name
171
193
  matching_schema = find_schema_for_partial(partial_name)
172
194
  next unless matching_schema
173
-
195
+
174
196
  if add_comments_to_partial(partial_path, matching_schema)
175
197
  @processed_files << partial_path
176
198
  puts " 📝 Updated partial: #{partial_path}"
177
199
  end
178
200
  end
179
201
  end
180
-
202
+
181
203
  # Finds appropriate schema for a partial template
182
204
  # @param partial_name [String] Name of the partial (e.g., "user")
183
205
  # @return [Hash, nil] Schema for the partial or nil
184
206
  def find_schema_for_partial(partial_name)
185
207
  # Try to find a schema that matches the partial name
186
208
  # Look for singular and plural forms
187
-
209
+
188
210
  # First, try direct name matching
189
211
  return @partial_schemas[partial_name] if @partial_schemas[partial_name]
190
212
  return @partial_schemas[partial_name.pluralize] if @partial_schemas[partial_name.pluralize]
191
213
  return @partial_schemas[partial_name.singularize] if @partial_schemas[partial_name.singularize]
192
-
214
+
193
215
  # Try to find common base properties across all schemas
194
216
  # This is a more generic approach that doesn't hardcode specific models
195
217
  base_schema = find_common_properties_schema(partial_name)
196
218
  return base_schema if base_schema
197
-
219
+
198
220
  # Fall back to property matching
199
221
  find_schema_by_properties(partial_name)
200
222
  end
201
-
223
+
202
224
  # Finds schema with common properties for partial
203
225
  # @param partial_name [String] Name of the partial
204
226
  # @return [Hash, nil] Schema with common properties or nil
205
- def find_common_properties_schema(partial_name)
227
+ def find_common_properties_schema(_partial_name)
206
228
  # Find schemas that might represent this partial
207
229
  # by looking for schemas with properties that match the partial content
208
230
  partial_path = Dir.glob(Rails.root.join("app/views/**/_{partial_name}.json.jbuilder")).first
209
231
  return nil unless partial_path && File.exist?(partial_path)
210
-
232
+
211
233
  # Parse the partial to get its properties
212
234
  partial_content = File.read(partial_path)
213
235
  partial_properties = partial_content.scan(/json\.(\w+)/).flatten.uniq
214
-
236
+
215
237
  # Find all schemas that could match
216
238
  candidate_schemas = []
217
- @partial_schemas.each do |key, schema|
239
+ @partial_schemas.each do |_key, schema|
218
240
  next unless schema['properties']
219
-
241
+
220
242
  # Calculate how many properties match
221
243
  matching_props = partial_properties & schema['properties'].keys
222
- if matching_props.size >= partial_properties.size * 0.5 # At least 50% match
223
- candidate_schemas << {
224
- schema: schema,
225
- match_count: matching_props.size,
226
- properties: schema['properties'].slice(*matching_props)
227
- }
228
- end
244
+ next unless matching_props.size >= partial_properties.size * 0.5 # At least 50% match
245
+
246
+ candidate_schemas << {
247
+ schema: schema,
248
+ match_count: matching_props.size,
249
+ properties: schema['properties'].slice(*matching_props)
250
+ }
229
251
  end
230
-
252
+
231
253
  return nil if candidate_schemas.empty?
232
-
254
+
233
255
  # If we have multiple candidates, find common properties across all of them
234
256
  if candidate_schemas.size > 1
235
257
  common_properties = extract_common_properties(candidate_schemas)
236
258
  return { 'properties' => common_properties } if common_properties.any?
237
259
  end
238
-
260
+
239
261
  # Otherwise return the best matching schema
240
262
  best_match = candidate_schemas.max_by { |c| c[:match_count] }
241
263
  { 'properties' => best_match[:properties] }
242
264
  end
243
-
265
+
244
266
  # Extracts common properties from multiple schemas
245
267
  # @param candidate_schemas [Array<Hash>] Array of candidate schemas
246
268
  # @return [Hash] Schema with common properties
247
269
  def extract_common_properties(candidate_schemas)
248
270
  # Find properties that appear in all candidate schemas with the same type
249
271
  common_props = {}
250
-
272
+
251
273
  # Get all property names from the first schema
252
274
  first_schema_props = candidate_schemas.first[:schema]['properties']
253
-
275
+
254
276
  first_schema_props.each do |prop_name, prop_schema|
255
277
  # Check if this property exists in all candidates with the same type
256
278
  is_common = candidate_schemas.all? do |candidate|
257
279
  candidate[:schema]['properties'][prop_name] &&
258
- candidate[:schema]['properties'][prop_name]['type'] == prop_schema['type']
259
- end
260
-
261
- if is_common
262
- # Use the most detailed schema for this property
263
- # (the one with the most attributes like description, enum, etc.)
264
- most_detailed = candidate_schemas.map { |c| c[:schema]['properties'][prop_name] }
265
- .max_by { |p| p.keys.size }
266
- common_props[prop_name] = most_detailed
280
+ candidate[:schema]['properties'][prop_name]['type'] == prop_schema['type']
267
281
  end
282
+
283
+ next unless is_common
284
+
285
+ # Use the most detailed schema for this property
286
+ # (the one with the most attributes like description, enum, etc.)
287
+ most_detailed = candidate_schemas.map { |c| c[:schema]['properties'][prop_name] }
288
+ .max_by { |p| p.keys.size }
289
+ common_props[prop_name] = most_detailed
268
290
  end
269
-
291
+
270
292
  common_props
271
293
  end
272
-
294
+
273
295
  # Finds schema by matching property names in partial
274
296
  # @param partial_name [String] Name of the partial
275
297
  # @return [Hash, nil] Matching schema or nil
276
- def find_schema_by_properties(partial_name)
298
+ def find_schema_by_properties(_partial_name)
277
299
  # Try to match by analyzing the partial content
278
300
  # This is a more complex matching strategy
279
301
  partial_path = Dir.glob(Rails.root.join("app/views/**/_{partial_name}.json.jbuilder")).first
280
302
  return nil unless partial_path && File.exist?(partial_path)
281
-
303
+
282
304
  content = File.read(partial_path)
283
305
  property_names = content.scan(/json\.(\w+)/).flatten.uniq
284
-
306
+
285
307
  # Find schema that has most matching properties
286
308
  best_match = nil
287
309
  best_score = 0
288
-
289
- @partial_schemas.each do |key, schema|
310
+
311
+ @partial_schemas.each do |_key, schema|
290
312
  next unless schema['properties']
291
-
313
+
292
314
  matching_props = property_names & schema['properties'].keys
293
315
  score = matching_props.size.to_f / property_names.size
294
-
316
+
295
317
  if score > best_score && score > 0.5 # At least 50% match
296
318
  best_match = schema
297
319
  best_score = score
298
320
  end
299
321
  end
300
-
322
+
301
323
  best_match
302
324
  end
303
-
325
+
304
326
  # Adds OpenAPI comments to a partial file
305
327
  # @param partial_path [String] Path to partial file
306
328
  # @param schema [Hash] Schema to use for comments
307
329
  # @return [void]
308
330
  def add_comments_to_partial(partial_path, schema)
309
331
  return false unless File.exist?(partial_path)
310
-
332
+
311
333
  content = File.read(partial_path)
312
-
313
- # Parse the partial file
314
- jbuilder_result = Parsers::JbuilderParser.new(partial_path).parse
315
- properties = jbuilder_result[:properties]
316
-
334
+
335
+ # Parse the partial file using new AST approach
336
+ jbuilder_parser = Parsers::JbuilderParser.new(partial_path)
337
+ ast_node = jbuilder_parser.parse_ast
338
+ # Convert AST to properties for backward compatibility
339
+ properties = ast_node ? extract_properties_from_ast(ast_node) : []
340
+
317
341
  # Generate new content with comments
318
342
  new_content = generate_commented_jbuilder(content, properties, schema, nil, nil)
319
-
343
+
320
344
  # Write back to file
321
345
  File.write(partial_path, new_content)
322
-
346
+
323
347
  true
324
348
  end
325
349
 
@@ -332,25 +356,27 @@ module RailsOpenapiGen
332
356
  jbuilder_path = controller_info[:jbuilder_path]
333
357
  return false unless File.exist?(jbuilder_path)
334
358
  return false if @processed_files.include?(jbuilder_path)
335
-
359
+
336
360
  @processed_files << jbuilder_path
337
-
361
+
338
362
  content = File.read(jbuilder_path)
339
-
340
- # Parse the Jbuilder file to understand its structure
341
- jbuilder_result = Parsers::JbuilderParser.new(jbuilder_path).parse
342
- properties = jbuilder_result[:properties]
343
-
363
+
364
+ # Parse the Jbuilder file to understand its structure using new AST approach
365
+ jbuilder_parser = Parsers::JbuilderParser.new(jbuilder_path)
366
+ ast_node = jbuilder_parser.parse_ast
367
+ # Convert AST to properties for backward compatibility
368
+ properties = ast_node ? extract_properties_from_ast(ast_node) : []
369
+
344
370
  # Get the response schema from the operation
345
371
  response_schema = extract_response_schema(operation)
346
372
  return false unless response_schema
347
-
373
+
348
374
  # Generate new content with comments
349
375
  new_content = generate_commented_jbuilder(content, properties, response_schema, operation, route)
350
-
376
+
351
377
  # Write back to file
352
378
  File.write(jbuilder_path, new_content)
353
-
379
+
354
380
  puts " 📝 Updated: #{jbuilder_path}"
355
381
  true
356
382
  end
@@ -361,10 +387,10 @@ module RailsOpenapiGen
361
387
  def extract_response_schema(operation)
362
388
  # Look for 200 response first, then any other successful response
363
389
  responses = operation['responses'] || {}
364
-
390
+
365
391
  success_response = responses['200'] || responses['201'] || responses.values.first
366
392
  return nil unless success_response
367
-
393
+
368
394
  content = success_response['content'] || {}
369
395
  json_content = content['application/json'] || {}
370
396
  json_content['schema']
@@ -377,23 +403,23 @@ module RailsOpenapiGen
377
403
  # @param operation [Hash, nil] OpenAPI operation data
378
404
  # @param route [Hash, nil] Route information
379
405
  # @return [String] Updated content with comments
380
- def generate_commented_jbuilder(content, properties, response_schema, operation, route)
406
+ def generate_commented_jbuilder(content, _properties, response_schema, operation, _route)
381
407
  lines = content.lines
382
408
  new_lines = []
383
409
  current_schema_stack = [response_schema]
384
410
  indent_stack = [0]
385
411
  in_block_stack = [] # Track if we're inside a do block
386
-
412
+
387
413
  # Add operation comment if needed at the very beginning
388
414
  if should_add_operation_comment(content, operation)
389
415
  new_lines << generate_operation_comment(operation)
390
416
  new_lines << "\n"
391
417
  end
392
-
418
+
393
419
  # Process each line
394
420
  lines.each_with_index do |line, index|
395
421
  current_indent = line.match(/^(\s*)/)[1].length
396
-
422
+
397
423
  # Check for block end
398
424
  if line.strip == 'end' && in_block_stack.any?
399
425
  in_block_stack.pop
@@ -403,29 +429,28 @@ module RailsOpenapiGen
403
429
  indent_stack.pop
404
430
  end
405
431
  end
406
-
432
+
407
433
  # Update schema stack based on indentation (for other cases)
408
434
  while indent_stack.last && current_indent < indent_stack.last && in_block_stack.empty?
409
435
  current_schema_stack.pop
410
436
  indent_stack.pop
411
437
  end
412
-
438
+
413
439
  # Check for json.array! patterns first (before general json property check)
414
440
  if line.strip.include?('json.array!')
415
441
  # Handle json.array! @posts do |post| patterns
416
442
  match = line.strip.match(/^json\.array!\s+@(\w+)\s+do\s+\|(\w+)\|/)
417
443
  if match
418
- collection_name = match[1] # e.g., "posts"
419
- item_name = match[2] # e.g., "post"
420
-
444
+ item_name = match[2] # e.g., "post"
445
+
421
446
  # Add comment for the array itself if this is a root-level array
422
- if current_schema_stack.size == 1 && current_schema_stack.first&.dig('type') == 'array'
423
- unless has_openapi_comment?(lines, index)
424
- array_comment = "# @openapi root:array items:object"
425
- new_lines << (' ' * current_indent) + array_comment + "\n"
426
- end
447
+ if current_schema_stack.size == 1 && current_schema_stack.first&.dig('type') == 'array' && !has_openapi_comment?(
448
+ lines, index
449
+ )
450
+ array_comment = "# @openapi root:array items:object"
451
+ new_lines << ((' ' * current_indent) + array_comment + "\n")
427
452
  end
428
-
453
+
429
454
  # Look for array item schema - try response_schema['items'] first
430
455
  item_schema = current_schema_stack.last&.dig('items')
431
456
  # For root-level arrays, use the response schema items directly
@@ -433,7 +458,7 @@ module RailsOpenapiGen
433
458
  item_schema = current_schema_stack.first['items']
434
459
  end
435
460
  item_schema ||= @partial_schemas[item_name.singularize]
436
-
461
+
437
462
  if item_schema
438
463
  current_schema_stack << item_schema
439
464
  indent_stack << current_indent
@@ -446,24 +471,24 @@ module RailsOpenapiGen
446
471
  if match
447
472
  property_name = match[1] # e.g., "tags"
448
473
  item_name = match[2] # e.g., "tag"
449
-
474
+
450
475
  # Look for this property in current schema
451
476
  current_schema = current_schema_stack.last
452
477
  if current_schema && current_schema['properties']
453
478
  property_schema = find_property_in_schema(current_schema['properties'], property_name)
454
-
479
+
455
480
  # Add comment for the array property itself if needed
456
481
  if property_schema && !has_openapi_comment?(lines, index)
457
482
  comment = generate_property_comment(property_name, property_schema)
458
- new_lines << (' ' * current_indent) + comment + "\n" if comment
483
+ new_lines << ((' ' * current_indent) + comment + "\n") if comment
459
484
  end
460
-
485
+
461
486
  # If it's an array with items, push the items schema
462
487
  if property_schema && property_schema['type'] == 'array' && property_schema['items'] && property_schema['items']['properties']
463
488
  current_schema_stack << property_schema['items']
464
489
  indent_stack << current_indent
465
490
  in_block_stack << true
466
- elif property_schema && property_schema['type'] == 'array' && property_schema['items']
491
+ elif property_schema && property_schema['type'] == 'array' && property_schema['items']
467
492
  # Try to find schema by item name
468
493
  item_schema = @partial_schemas[item_name.singularize] || @partial_schemas[item_name]
469
494
  if item_schema
@@ -474,36 +499,29 @@ module RailsOpenapiGen
474
499
  end
475
500
  end
476
501
  end
477
- elsif line.strip.match(/^json\.partial!.*['"](\w+)\/_(\w+)['"]/)
502
+ elsif line.strip.match(%r{^json\.partial!.*['"](\w+)/_(\w+)['"]})
478
503
  # Handle partials - check if next line should have nested properties
479
- match = line.strip.match(/^json\.partial!.*['"](\w+)\/_(\w+)['"]/)
504
+ match = line.strip.match(%r{^json\.partial!.*['"](\w+)/_(\w+)['"]})
480
505
  if match
481
- partial_dir = match[1]
482
- partial_name = match[2]
483
-
484
- # Check if this is inside a block (like json.author do)
485
- if in_block_stack.any? && current_schema_stack.last
486
- # We're inside a block, current schema should have the right context
487
- current_schema = current_schema_stack.last
488
- # The partial will handle its own properties
489
- end
506
+ # Extract but don't store partial info as it's not used in current implementation
507
+ # The partial will handle its own properties when processed
490
508
  end
491
509
  # Check if this line is a json property (general case)
492
510
  elsif json_property_line?(line)
493
511
  property_name = extract_property_name(line)
494
-
512
+
495
513
  if property_name
496
514
  current_schema = current_schema_stack.last
497
-
515
+
498
516
  if current_schema && current_schema['properties']
499
517
  property_schema = find_property_in_schema(current_schema['properties'], property_name)
500
-
518
+
501
519
  if property_schema && !has_openapi_comment?(lines, index)
502
520
  # Add comment before this line
503
521
  comment = generate_property_comment(property_name, property_schema)
504
- new_lines << (' ' * current_indent) + comment + "\n" if comment
522
+ new_lines << ((' ' * current_indent) + comment + "\n") if comment
505
523
  end
506
-
524
+
507
525
  # Check if this line opens a block
508
526
  if line.include?(' do')
509
527
  in_block_stack << true
@@ -528,10 +546,10 @@ module RailsOpenapiGen
528
546
  end
529
547
  end
530
548
  end
531
-
549
+
532
550
  new_lines << line
533
551
  end
534
-
552
+
535
553
  new_lines.join
536
554
  end
537
555
 
@@ -542,7 +560,8 @@ module RailsOpenapiGen
542
560
  def should_add_operation_comment(content, operation)
543
561
  # Check if operation comment already exists
544
562
  return false unless operation
545
- !content.include?('@openapi_operation') &&
563
+
564
+ !content.include?('@openapi_operation') &&
546
565
  (operation['summary'] || operation['description'] || operation['tags'])
547
566
  end
548
567
 
@@ -553,14 +572,14 @@ module RailsOpenapiGen
553
572
  parts = []
554
573
  parts << "summary:\"#{operation['summary']}\"" if operation['summary']
555
574
  parts << "description:\"#{operation['description']}\"" if operation['description']
556
-
575
+
557
576
  if operation['tags'] && operation['tags'].any?
558
577
  tags = operation['tags'].map { |tag| tag.to_s }.join(',')
559
578
  parts << "tags:[#{tags}]"
560
579
  end
561
-
580
+
562
581
  return nil if parts.empty?
563
-
582
+
564
583
  "# @openapi_operation #{parts.join(' ')}"
565
584
  end
566
585
 
@@ -586,7 +605,7 @@ module RailsOpenapiGen
586
605
  def has_openapi_comment?(lines, current_index)
587
606
  # Check the previous line for @openapi comment
588
607
  return false if current_index == 0
589
-
608
+
590
609
  prev_line = lines[current_index - 1].strip
591
610
  prev_line.include?('@openapi')
592
611
  end
@@ -605,43 +624,43 @@ module RailsOpenapiGen
605
624
  # @return [String] Generated comment string
606
625
  def generate_property_comment(property_name, property_schema)
607
626
  return nil unless property_schema
608
-
627
+
609
628
  type = property_schema['type'] || 'string'
610
629
  parts = ["#{property_name}:#{type}"]
611
-
630
+
612
631
  # Handle array items type
613
632
  if type == 'array' && property_schema['items']
614
633
  items_type = property_schema['items']['type'] || 'string'
615
634
  parts[0] = "#{property_name}:array"
616
635
  parts << "items:#{items_type}"
617
636
  end
618
-
637
+
619
638
  if property_schema['required'] == false
620
639
  parts << "required:false"
621
640
  end
622
-
641
+
623
642
  if property_schema['description']
624
643
  parts << "description:\"#{property_schema['description']}\""
625
644
  end
626
-
645
+
627
646
  if property_schema['enum']
628
647
  enum_values = property_schema['enum'].map(&:to_s).join(',')
629
648
  parts << "enum:[#{enum_values}]"
630
649
  end
631
-
650
+
632
651
  if property_schema['format']
633
652
  parts << "format:#{property_schema['format']}"
634
653
  end
635
-
654
+
636
655
  if property_schema['minimum']
637
656
  parts << "minimum:#{property_schema['minimum']}"
638
657
  end
639
-
658
+
640
659
  if property_schema['maximum']
641
660
  parts << "maximum:#{property_schema['maximum']}"
642
661
  end
643
-
662
+
644
663
  "# @openapi #{parts.join(' ')}"
645
664
  end
646
665
  end
647
- end
666
+ end