swagger_docs_rails 0.1.0

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.
@@ -0,0 +1,556 @@
1
+ # frozen_string_literal: true
2
+ # lib/swagger_docs_rails/controller_parser.rb
3
+ #
4
+ # Escaneia app/controllers/api/**/*.rb e extrai por controller:
5
+ #
6
+ # Ações CRUD padrão (index, show, create, update, destroy)
7
+ # Ações customizadas (importar, upload_arquivo, etc.)
8
+ # params.permit por ação — direto no corpo ou via método privado referenciado
9
+ # Campos de arquivo (format: binary) detectados pelo nome e pelo corpo da ação
10
+ # Detecção de multipart/form-data (Roo::, .original_filename, params[:arquivo], etc.)
11
+ # Padrão Model.column_names.reject{}.permit(*permitted, :extra_field)
12
+ # Método HTTP de ações customizadas (heurística por nome + @swagger_method_X)
13
+ # Metadados de documentação via comentários @swagger_*
14
+ # Estruturas aninhadas recursivas (ex: responsaveis: [ { nome:, enderecos: [] } ])
15
+
16
+ module SwaggerDocsRails
17
+ class ControllerParser
18
+ CRUD_ACTIONS = %w[index show create update destroy].freeze
19
+
20
+ # Heurística nome → HTTP para ações customizadas
21
+ CUSTOM_HTTP_HINTS = {
22
+ /\A(import|importar|upload|enviar|processar|executar|sincronizar|
23
+ gerar|calcular|acionar|criar|cadastrar)/x => :post,
24
+ /\A(download|exportar|relatorio|report|preview|visualizar|listar|buscar)/x => :get,
25
+ /\A(ativar|desativar|aprovar|rejeitar|cancelar|bloquear|
26
+ desbloquear|definir|remover|atualizar|alterar)/x => :patch,
27
+ /\Adelete_/ => :delete
28
+ }.freeze
29
+
30
+ # Tipo OpenAPI pelo nome do campo
31
+ FIELD_TYPE_MAP = {
32
+ /_id\z/ => { type: "integer", format: "int64" },
33
+ /\Aemail|_email\z/ => { type: "string", format: "email" },
34
+ /password|senha/ => { type: "string", format: "password" },
35
+ /\Acpf\z|\Acnpj\z|cpf_cnpj/ => { type: "string" },
36
+ /\Adata_|\A.*_at\z|data_nascimento/ => { type: "string", format: "date" },
37
+ /valor|preco|percentual|taxa/ => { type: "number", format: "double" },
38
+ /\Aativo\z|proprio|embutido|possui|admin|bloqueado/ => { type: "boolean" },
39
+ /nivel|numero|tentativas/ => { type: "integer" }
40
+ }.freeze
41
+
42
+ # Indicadores de upload no corpo de uma ação ou nos seus params privados
43
+ MULTIPART_INDICATORS = [
44
+ /\.original_filename\b/,
45
+ /\.content_type\b/,
46
+ /\.read\b/,
47
+ /Roo::/,
48
+ /ActionDispatch.*UploadedFile/,
49
+ /CarrierWave|Shrine|ActiveStorage\.attach/,
50
+ /params\[:arquivo\]/,
51
+ /params\[:file\]/,
52
+ /params\[:anexo\]/,
53
+ /params\[:imagem\]/,
54
+ /params\[:foto\]/,
55
+ /\.tempfile\b/
56
+ ].freeze
57
+
58
+ # Padrões de campo de arquivo pelo nome (excluindo _url)
59
+ FILE_FIELD_PATTERN = /
60
+ \Aimagem_|\Afoto_|\Aanexo_|\Adocumento_|\Aplanilha_| # prefixo
61
+ _imagem\z|_foto\z|_anexo\z|_arquivo\z| # sufixo
62
+ \Aarquivo\z|\Afile\z|_file\z| # exatos
63
+ \Aanexo\z|\Aimagem\z|\Afoto\z| # exatos pt-br
64
+ \Alogo\z|\Aavatar\z|\Athumbnail\z|\Acover\z| # imagens comuns
65
+ \Adocumento\z|\Aplanilha\z|\Arelatorio\z # docs
66
+ /x.freeze
67
+
68
+ attr_reader :controllers
69
+
70
+ def initialize(paths = nil)
71
+ @search_paths = Array(paths || [Rails.root.join("app", "controllers", "api")])
72
+ @controllers = []
73
+ @routes_map = parse_routes_file!
74
+ parse!
75
+ end
76
+
77
+ def to_resources
78
+ @controllers
79
+ end
80
+
81
+ def self.infer_field_type(field_name)
82
+ FIELD_TYPE_MAP.each do |pattern, type_def|
83
+ return type_def.dup if field_name.to_s.match?(pattern)
84
+ end
85
+ { type: "string" }
86
+ end
87
+
88
+ private
89
+
90
+ # ─────────────────────────────────────────────────────── parse ──
91
+
92
+ def parse!
93
+ @search_paths.each do |base|
94
+ Dir.glob(File.join(base.to_s, "**", "*.rb")).sort.each do |file|
95
+ info = extract_info(file, File.read(file))
96
+ @controllers << info if info
97
+ end
98
+ end
99
+ end
100
+
101
+ def extract_info(file, content)
102
+ klass_match = content.match(/class\s+([\w:]+Controller)/)
103
+ return nil unless klass_match
104
+
105
+ klass = klass_match[1]
106
+ return nil unless klass.start_with?("Api::")
107
+
108
+ version = extract_version(klass)
109
+ resource_name = File.basename(file, ".rb").sub(/_controller$/, "")
110
+ model_name = infer_model_name(resource_name)
111
+
112
+ public_body, private_body = split_public_private(content)
113
+ public_actions = scan_public_actions(public_body)
114
+ return nil if public_actions.empty?
115
+
116
+ unauthenticated_actions = extract_unauthenticated_actions(content)
117
+ private_permit_map = extract_private_permit_methods(private_body)
118
+ actions_meta = build_actions_meta(public_actions, public_body, private_permit_map,
119
+ unauthenticated_actions)
120
+
121
+ crud = public_actions & CRUD_ACTIONS
122
+ custom = public_actions - CRUD_ACTIONS
123
+
124
+ {
125
+ controller_class: klass,
126
+ resource: resource_name,
127
+ model: model_name,
128
+ version: version,
129
+ actions: crud,
130
+ custom_actions: build_custom_actions(custom, content, actions_meta),
131
+ actions_meta: actions_meta,
132
+ unauthenticated_actions: unauthenticated_actions,
133
+ extra_meta: extract_swagger_meta(content)
134
+ }
135
+ end
136
+
137
+ def split_public_private(content)
138
+ idx = content.index(/^\s+private\b/)
139
+ idx ? [content[0...idx], content[idx..]] : [content, ""]
140
+ end
141
+
142
+ def scan_public_actions(body)
143
+ body.scan(/^\s+def\s+(\w+)\b/).flatten.uniq
144
+ end
145
+
146
+ def extract_private_permit_methods(private_body)
147
+ result = {}
148
+ private_body.scan(/def\s+(\w+)(.*?)(?=\n\s+def\s|\z)/m) do |mname, mbody|
149
+ meta = extract_permit_meta(mbody)
150
+ result[mname] = meta if meta[:fields].any? || meta[:uses_column_names] || meta[:nested_arrays].any?
151
+ end
152
+ result
153
+ end
154
+
155
+ def extract_permit_meta(body)
156
+ structure = extract_permit_fields(body)
157
+ uses_column_names = body.match?(/\.column_names\b/)
158
+ model_name = nil
159
+ extra_fields = []
160
+
161
+ if uses_column_names
162
+ m = body.match(/(\w+)\.column_names/)
163
+ model_name = m[1] if m
164
+ extra_fields = extract_extra_fields_after_splat(body)
165
+ structure[:scalar_fields] = (structure[:scalar_fields] | extra_fields)
166
+ end
167
+
168
+ file_fields = detect_file_fields_by_name(structure[:scalar_fields]) | detect_file_fields_by_access(body)
169
+
170
+ {
171
+ fields: structure[:scalar_fields],
172
+ array_fields: structure[:array_fields],
173
+ nested_arrays: structure[:nested_arrays],
174
+ file_fields: file_fields,
175
+ extra_fields: extra_fields,
176
+ uses_column_names: uses_column_names,
177
+ column_names_model: model_name
178
+ }
179
+ end
180
+
181
+ def extract_extra_fields_after_splat(body)
182
+ extras = []
183
+ body.scan(/\.permit\((?:[^)]*\*\w+[^,)]*),\s*(.*?)\)/m) do |after|
184
+ after.first.to_s.scan(/[:"'](\w+)["']?/).flatten.each { |f| extras << f unless f.empty? }
185
+ end
186
+ extras.uniq
187
+ end
188
+
189
+ def build_actions_meta(actions, public_body, private_permit_map, unauthenticated_actions = [])
190
+ meta = {}
191
+ actions.each do |action|
192
+ body = extract_action_body(action, public_body)
193
+ next unless body
194
+
195
+ permit_meta = resolve_permit_meta(body, private_permit_map)
196
+ body_multipart = detect_multipart(body)
197
+ all_file_fields = (permit_meta[:file_fields] + detect_file_fields_by_access(body)).uniq
198
+
199
+ permit_has_file = permit_meta[:fields].any? { |f| file_field_by_name?(f) }
200
+ is_multipart = body_multipart || all_file_fields.any? || permit_has_file
201
+
202
+ if permit_has_file
203
+ permit_files = permit_meta[:fields].select { |f| file_field_by_name?(f) }
204
+ all_file_fields = (all_file_fields + permit_files).uniq
205
+ end
206
+
207
+ meta[action] = {
208
+ permit_fields: permit_meta[:fields],
209
+ array_fields: permit_meta[:array_fields] || [],
210
+ nested_arrays: permit_meta[:nested_arrays] || {},
211
+ file_fields: all_file_fields,
212
+ extra_fields: permit_meta[:extra_fields] || [],
213
+ uses_column_names: permit_meta[:uses_column_names],
214
+ column_names_model: permit_meta[:column_names_model],
215
+ grouped_params: permit_meta[:grouped_params],
216
+ is_multipart: is_multipart,
217
+ http_method: crud_http_method(action),
218
+ uses_service: body.match?(/Service\.new|\.call\b/),
219
+ unauthenticated: unauthenticated_actions.include?(action)
220
+ }
221
+ end
222
+ meta
223
+ end
224
+
225
+ def extract_action_body(action, content)
226
+ m = content.match(/def\s+#{Regexp.escape(action)}\b(.*?)(?=\n\s+def\s|\z)/m)
227
+ m ? m[1] : nil
228
+ end
229
+
230
+ def resolve_permit_meta(action_body, private_permit_map)
231
+ inline = extract_permit_meta(action_body)
232
+ if inline[:fields].any? || inline[:array_fields].any? || inline[:nested_arrays].any?
233
+ return inline
234
+ end
235
+
236
+ called = action_body.scan(/\b(\w+_params?\b|\w+_permit\w*)\b/).flatten.uniq
237
+ matched = called.filter_map { |m| [m, private_permit_map[m]] if private_permit_map[m] }
238
+
239
+ return empty_permit_meta if matched.empty?
240
+
241
+ if matched.size == 1
242
+ return matched.first[1].merge(grouped_params: nil)
243
+ end
244
+
245
+ groups = matched.map do |method_name, meta|
246
+ group_key = method_name.sub(/_params?\z/, "").sub(/\Aparams_/, "")
247
+ { key: group_key, meta: meta }
248
+ end
249
+
250
+ {
251
+ fields: [],
252
+ array_fields: [],
253
+ nested_arrays: {},
254
+ file_fields: [],
255
+ extra_fields: [],
256
+ uses_column_names: false,
257
+ column_names_model: nil,
258
+ grouped_params: groups
259
+ }
260
+ end
261
+
262
+ def empty_permit_meta
263
+ { fields: [], array_fields: [], nested_arrays: {}, file_fields: [], extra_fields: [],
264
+ uses_column_names: false, column_names_model: nil, grouped_params: nil }
265
+ end
266
+
267
+ def extract_permit_fields(text)
268
+ result = { scalar_fields: [], array_fields: [], nested_arrays: {} }
269
+
270
+ normalized = text.gsub(/\n/, ' ').gsub(/\s+/, ' ')
271
+
272
+ normalized.scan(/params(?:\[[\w:"' ]+\])?\.(?:require\([\w:"']+\)\.)?permit\((.*?)\)/m) do |args|
273
+ parsed = parse_permit_args_recursive(args.first.to_s)
274
+ merge_permit_result(result, parsed)
275
+ end
276
+
277
+ normalized.scan(/params\.expect\(\s*\w+:\s*\[(.*?)\]\s*\)/m) do |args|
278
+ parsed = parse_permit_args_recursive(args.first.to_s)
279
+ merge_permit_result(result, parsed)
280
+ end
281
+
282
+ normalized.scan(/\b(\w+)\.permit\((.*?)\)/m) do |var, args|
283
+ next if var == "params"
284
+ if text.match?(/\b#{Regexp.escape(var)}\s*=.*\bparams\b/m)
285
+ parsed = parse_permit_args_recursive(args)
286
+ merge_permit_result(result, parsed)
287
+ end
288
+ end
289
+
290
+ result
291
+ end
292
+
293
+ def merge_permit_result(target, source)
294
+ target[:scalar_fields] |= source[:scalar_fields]
295
+ target[:array_fields] |= source[:array_fields]
296
+ source[:nested_arrays].each do |k, v|
297
+ target[:nested_arrays][k] ||= { scalar_fields: [], array_fields: [], nested_arrays: {} }
298
+ merge_permit_result(target[:nested_arrays][k], v)
299
+ end
300
+ end
301
+
302
+ def parse_permit_args_recursive(args_str)
303
+ result = { scalar_fields: [], array_fields: [], nested_arrays: {} }
304
+ return result if args_str.strip.empty?
305
+
306
+ tokens = tokenize_permit_args(args_str)
307
+ tokens.each do |token|
308
+ token = token.strip
309
+ next if token.empty?
310
+
311
+ if token.include?(':') && !token.start_with?(':')
312
+ key, value = token.split(':', 2)
313
+ key = clean_symbol(key)
314
+ value = value.strip
315
+
316
+ if value.start_with?('[') && value.end_with?(']')
317
+ inner = value[1..-2]
318
+ if inner.include?(':') && !inner.match(/^:\w+$/)
319
+ result[:nested_arrays][key] = parse_permit_args_recursive(inner)
320
+ else
321
+ result[:array_fields] << key
322
+ end
323
+ elsif value.start_with?('{') && value.end_with?('}')
324
+ inner = value[1..-2]
325
+ result[:nested_arrays][key] = parse_permit_args_recursive(inner)
326
+ else
327
+ result[:scalar_fields] << key
328
+ end
329
+ else
330
+ field = clean_symbol(token)
331
+ result[:scalar_fields] << field unless field.empty?
332
+ end
333
+ end
334
+
335
+ result
336
+ end
337
+
338
+ def clean_symbol(str)
339
+ str.to_s.strip.gsub(/\A[:"]|["]\z/, '')
340
+ end
341
+
342
+ def tokenize_permit_args(str)
343
+ tokens = []
344
+ current = +''
345
+ depth = 0
346
+ in_string = false
347
+
348
+ str.each_char do |ch|
349
+ case ch
350
+ when '[', '{', '('
351
+ depth += 1
352
+ current << ch
353
+ when ']', '}', ')'
354
+ depth -= 1
355
+ current << ch
356
+ when ','
357
+ if depth == 0 && !in_string
358
+ tokens << current
359
+ current = +''
360
+ else
361
+ current << ch
362
+ end
363
+ when '"', "'"
364
+ in_string = !in_string
365
+ current << ch
366
+ else
367
+ current << ch
368
+ end
369
+ end
370
+ tokens << current unless current.empty?
371
+ tokens.map(&:strip)
372
+ end
373
+
374
+ def build_custom_actions(action_names, full_content, actions_meta)
375
+ action_names.map do |name|
376
+ ameta = actions_meta[name] || {}
377
+ all_file_fields = ameta[:file_fields] || []
378
+ {
379
+ name: name,
380
+ http_method: infer_custom_http_method(name, full_content),
381
+ on_member: infer_on_member(name, full_content),
382
+ extra_path_params: extra_path_params_for(name),
383
+ permit_fields: ameta[:permit_fields] || [],
384
+ array_fields: ameta[:array_fields] || [],
385
+ nested_arrays: ameta[:nested_arrays] || {},
386
+ file_fields: all_file_fields,
387
+ extra_fields: ameta[:extra_fields] || [],
388
+ grouped_params: ameta[:grouped_params],
389
+ uses_service: ameta[:uses_service] || false,
390
+ is_multipart: ameta[:is_multipart] || all_file_fields.any?,
391
+ uses_column_names: ameta[:uses_column_names] || false,
392
+ column_names_model: ameta[:column_names_model],
393
+ unauthenticated: ameta[:unauthenticated] || false
394
+ }
395
+ end
396
+ end
397
+
398
+ def infer_custom_http_method(action_name, content)
399
+ comment = content.match(/#\s*@swagger_method_#{action_name}:\s*(\w+)/i)
400
+ return comment[1].downcase.to_sym if comment
401
+
402
+ if @routes_map.key?(action_name.to_s)
403
+ entry = @routes_map[action_name.to_s]
404
+ return entry.is_a?(Hash) ? entry[:method] : entry
405
+ end
406
+
407
+ CUSTOM_HTTP_HINTS.each { |pattern, method| return method if action_name.match?(pattern) }
408
+ :post
409
+ end
410
+
411
+ def extra_path_params_for(action_name)
412
+ entry = @routes_map[action_name.to_s]
413
+ return [] unless entry.is_a?(Hash)
414
+ entry[:path_params] || []
415
+ end
416
+
417
+ def infer_on_member(action_name, full_content)
418
+ routes_path = Rails.root.join("config", "routes.rb")
419
+ return false unless File.exist?(routes_path)
420
+
421
+ routes_content = File.read(routes_path)
422
+
423
+ return false if routes_content.match?(/\b(get|post|put|patch|delete)\s+['":][^'"\n]*\b#{Regexp.escape(action_name)}\b[^'"\n]*['"]?[^\n]*on:\s*:collection/i)
424
+ return false if inside_block?(routes_content, "collection", action_name)
425
+ return true if routes_content.match?(/\b(get|post|put|patch|delete)\s+['":][^'"\n]*\b#{Regexp.escape(action_name)}\b[^'"\n]*['"]?[^\n]*on:\s*:member/i)
426
+ return true if inside_block?(routes_content, "member", action_name)
427
+
428
+ false
429
+ end
430
+
431
+ def inside_block?(content, block_type, action_name)
432
+ lines = content.lines
433
+ lines.each_with_index do |line, idx|
434
+ next unless line.match?(/\b#{block_type}\s+do\b/)
435
+
436
+ depth = 1
437
+ block_lines = []
438
+ lines[(idx + 1)..].each do |inner_line|
439
+ depth += inner_line.scan(/\bdo\b|\bbegin\b/).size
440
+ depth -= inner_line.scan(/\bend\b/).size
441
+ break if depth <= 0
442
+ block_lines << inner_line
443
+ end
444
+
445
+ block_text = block_lines.join
446
+ return true if block_text.match?(/\b(get|post|put|patch|delete)\s+:#{Regexp.escape(action_name)}\b/) ||
447
+ block_text.match?(/\b(get|post|put|patch|delete)\s+['"][^'"]*#{Regexp.escape(action_name)}[^'"]*['"]/) ||
448
+ block_text.match?(/action:\s*:#{Regexp.escape(action_name)}\b/)
449
+ end
450
+ false
451
+ end
452
+
453
+ def extract_unauthenticated_actions(content)
454
+ actions = []
455
+ content.scan(/skip_before_action\s+:(?:authenticate\w*)\s*(?:,\s*only:\s*[\[%]i?\[?(.*?)\]?\s*\n)/m) do |match|
456
+ match.first.to_s.scan(/[:"'](\w+)["']?/).flatten.each { |a| actions << a }
457
+ end
458
+ content.scan(/skip_before_action\s+[:'"]\w+['"]?\s*,\s*only:\s*[\[%]i?\[?([^\]]+)\]?/m) do |match|
459
+ match.first.to_s.scan(/[:"'](\w+)["']?/).flatten.each { |a| actions << a }
460
+ end
461
+ actions.uniq
462
+ end
463
+
464
+ def detect_multipart(body)
465
+ MULTIPART_INDICATORS.any? { |p| body.match?(p) }
466
+ end
467
+
468
+ def detect_file_fields_by_name(fields)
469
+ fields.select { |f| file_field_by_name?(f) }
470
+ end
471
+
472
+ def detect_file_fields_by_access(body)
473
+ fields = []
474
+ body.scan(/params\[:(\w+)\]/).flatten.each do |f|
475
+ fields << f if f.match?(/arquivo|file|planilha|documento|imagem|foto|anexo/)
476
+ end
477
+ body.scan(/params\[:(\w+)\][^\n]*(?:\.original_filename|\.read|\.content_type)/).flatten.each do |f|
478
+ fields << f
479
+ end
480
+ fields.uniq
481
+ end
482
+
483
+ def file_field_by_name?(name)
484
+ n = name.to_s
485
+ return false if n.end_with?("_url") || n.end_with?("url")
486
+ n.match?(FILE_FIELD_PATTERN)
487
+ end
488
+
489
+ def parse_routes_file!
490
+ routes_path = Rails.root.join("config", "routes.rb")
491
+ return {} unless File.exist?(routes_path)
492
+
493
+ content = File.read(routes_path)
494
+ routes_map = {}
495
+
496
+ content.scan(/\b(get|post|put|patch|delete)\s+:(\w+)/) do |method, action|
497
+ next if CRUD_ACTIONS.include?(action)
498
+ routes_map[action] ||= { method: method.downcase.to_sym, path_params: [] }
499
+ end
500
+
501
+ content.scan(/\b(get|post|put|patch|delete)\s+['"]([^'"]+)['"]\s*,\s*action:\s*:(\w+)/) do |method, path, action|
502
+ next if CRUD_ACTIONS.include?(action)
503
+ path_params = path.scan(/:(\w+)/).flatten
504
+ routes_map[action] ||= { method: method.downcase.to_sym, path_params: path_params }
505
+ end
506
+
507
+ content.scan(/\b(get|post|put|patch|delete)\s+['"]([^'"]+)['"]\s*,\s*to:\s*['"][^'"#]*#(\w+)['"]/) do |method, path, action|
508
+ next if CRUD_ACTIONS.include?(action)
509
+ path_params = path.scan(/:(\w+)/).flatten
510
+ routes_map[action] ||= { method: method.downcase.to_sym, path_params: path_params }
511
+ end
512
+
513
+ content.scan(/\b(get|post|put|patch|delete)\s+['"]([^'"]+)['"]\s*(?!,)/) do |method, path|
514
+ action = path.split("/").last.to_s.gsub(/:.*/, "").gsub(/[^a-z_]/, "")
515
+ next if action.empty? || CRUD_ACTIONS.include?(action)
516
+ path_params = path.scan(/:(\w+)/).flatten
517
+ routes_map[action] ||= { method: method.downcase.to_sym, path_params: path_params }
518
+ end
519
+
520
+ routes_map
521
+ end
522
+
523
+ def extract_version(klass)
524
+ m = klass.match(/::V(\d+)::/)
525
+ m ? "v#{m[1]}" : "v1"
526
+ end
527
+
528
+ def infer_model_name(resource_name)
529
+ singular = resource_name.singularize rescue resource_name.sub(/s$/, "")
530
+ parts = singular.split("_")
531
+ parts.first == "g" ? "G" + parts[1..].map(&:capitalize).join : parts.map(&:capitalize).join
532
+ end
533
+
534
+ def crud_http_method(action)
535
+ { "index" => :get, "show" => :get, "create" => :post,
536
+ "update" => :patch, "destroy" => :delete }.fetch(action, :get)
537
+ end
538
+
539
+ def extract_swagger_meta(content)
540
+ meta = {}
541
+ tag_m = content.match(/#\s*@swagger_tag:\s*(.+)/)
542
+ meta[:tag] = tag_m[1].strip if tag_m
543
+
544
+ content.scan(/#\s*@swagger_summary_(\w+):\s*(.+)/).each { |a, v| meta[:"summary_#{a}"] = v.strip }
545
+ content.scan(/#\s*@swagger_description_(\w+):\s*(.+)/).each { |a, v| meta[:"description_#{a}"] = v.strip }
546
+ content.scan(/#\s*@swagger_params_(\w+):\s*(.+)/).each do |action, fields_str|
547
+ meta[:"params_#{action}"] = fields_str.strip.split(/\s*,\s*/).map(&:strip)
548
+ end
549
+ content.scan(/#\s*@swagger_response_(\w+):\s*(.+)/).each do |action, fields_str|
550
+ meta[:"response_#{action}"] = fields_str.strip.split(/\s*,\s*/).map(&:strip)
551
+ end
552
+
553
+ meta
554
+ end
555
+ end
556
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails"
4
+ require "action_controller/railtie"
5
+
6
+ module SwaggerDocsRails
7
+ class Engine < ::Rails::Engine
8
+ isolate_namespace SwaggerDocsRails
9
+
10
+ rake_tasks do
11
+ load File.expand_path("../tasks/swagger_docs_rails.rake", __dir__)
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+ # lib/swagger_docs_rails/generator.rb
3
+
4
+ module SwaggerDocsRails
5
+ class Generator
6
+ def initialize(opts = {})
7
+ @schema_path = opts[:schema_path]
8
+ @controllers_path = opts[:controllers_path]
9
+ @config = opts.fetch(:config, {})
10
+ end
11
+
12
+ def generate
13
+ schema_parser = SchemaParser.new(@schema_path)
14
+ controller_parser = ControllerParser.new(controller_paths)
15
+ OpenapiBuilder.new(schema_parser, controller_parser, @config).build
16
+ end
17
+
18
+ def generate_json
19
+ JSON.pretty_generate(generate)
20
+ end
21
+
22
+ def write(output_path = nil)
23
+ output_path ||= SwaggerDocsRails.configuration.output_path
24
+ FileUtils.mkdir_p(File.dirname(output_path.to_s))
25
+ File.write(output_path.to_s, JSON.pretty_generate(generate))
26
+ output_path
27
+ end
28
+
29
+ private
30
+
31
+ def controller_paths
32
+ return @controllers_path if @controllers_path
33
+
34
+ [
35
+ Rails.root.join("app", "controllers", "api"),
36
+ *Array(SwaggerDocsRails.configuration.additional_controller_paths)
37
+ ]
38
+ end
39
+ end
40
+ end