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
@@ -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(
|
12
|
-
|
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(
|
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? &&
|
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(
|
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 |
|
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
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
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
|
-
|
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(
|
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 |
|
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
|
-
|
315
|
-
|
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
|
-
|
342
|
-
|
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,
|
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
|
-
|
419
|
-
|
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
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
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
|
-
|
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(
|
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(
|
504
|
+
match = line.strip.match(%r{^json\.partial!.*['"](\w+)/_(\w+)['"]})
|
480
505
|
if match
|
481
|
-
|
482
|
-
|
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
|
-
|
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
|