routes_api_spec_generator 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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f58458c8891010c8773f1701fb8f8b7d1ba98eb5faf24c5fa67abcf82e214153
4
+ data.tar.gz: '094794045e0f83ad55ac08ee3ebcd7a9006ee48e5f8ef877297bd6bf55250a5b'
5
+ SHA512:
6
+ metadata.gz: 5908ac4e089e60052dbe987ed96ddf9173375b1e010eb4e589254de07d943635fe9ad51d1e720a3eabe8b6c9488b06cb0fea8572aea90c61414af8b37930cd61
7
+ data.tar.gz: ebeb96493e737cbdb7b9cf882a7dcde4ecf126296ca7b7b657769bb4b8b1bdbd976e030bad2ab1ede3134cea830f44277ce001df451d59bcee681cc98163fe28
data/CHANGELOG.md ADDED
@@ -0,0 +1,14 @@
1
+ # Changelog
2
+
3
+ ## [Unreleased]
4
+
5
+ ## [0.1.0] - 2026-07-09
6
+
7
+ ### Added
8
+
9
+ - Coletor de rotas Rails (`/api/v1` por default)
10
+ - Inspector de controllers (services, tenant header, template)
11
+ - Loader de config YAML opcional
12
+ - Generator com templates ERB (channels, big_numbers, insights, general_metrics)
13
+ - CLI `routes-api-spec-generator`
14
+ - Rake task `routes_api_spec:generate` via Railtie
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Saulo Filho
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,129 @@
1
+ # routes_api_spec_generator
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/routes_api_spec_generator.svg)](https://badge.fury.io/rb/routes_api_spec_generator)
4
+
5
+ **Site:** [saulofilho.github.io/routes-api-spec-generator](https://saulofilho.github.io/routes-api-spec-generator/)
6
+
7
+ Gera **request specs RSpec com rswag** a partir de **rotas Rails** e inspeção de controllers.
8
+
9
+ Ferramenta open source para times que usam o fluxo **spec → swagger** (`rake rswag:specs:swaggerize`) e querem reduzir o boilerplate ao cobrir novos endpoints.
10
+
11
+ ## O que ela faz
12
+
13
+ - Lê rotas Rails (ex.: `/api/v1/*`)
14
+ - Inspeciona controllers para inferir services, headers de tenant e parâmetros
15
+ - Aplica overrides opcionais via `config/routes_api_spec_generator.yml`
16
+ - Materializa arquivos em `spec/requests/**/_spec.rb` via templates ERB
17
+
18
+ ## O que ela NÃO faz
19
+
20
+ - Gerar specs a partir do Swagger (fluxo circular no rswag)
21
+ - Substituir mocks ou schemas detalhados específicos do seu domínio
22
+ - Rodar testes ou publicar o contrato OpenAPI automaticamente
23
+
24
+ ## Instalação
25
+
26
+ ```bash
27
+ gem install routes_api_spec_generator
28
+ ```
29
+
30
+ Ou no `Gemfile` de um app Rails:
31
+
32
+ ```ruby
33
+ group :development, :test do
34
+ gem 'routes_api_spec_generator'
35
+ end
36
+ ```
37
+
38
+ ## Uso via Rake (app Rails)
39
+
40
+ ```bash
41
+ bundle exec rake routes_api_spec:generate
42
+ FORCE=true bundle exec rake routes_api_spec:generate # sobrescreve existentes
43
+ DRY_RUN=true bundle exec rake routes_api_spec:generate # apenas lista
44
+ ```
45
+
46
+ ## Uso via CLI
47
+
48
+ ```bash
49
+ routes-api-spec-generator generate
50
+ routes-api-spec-generator generate --force --root /path/to/rails/app
51
+ routes-api-spec-generator generate --dry-run --namespace /api/v2
52
+ ```
53
+
54
+ ### Opções
55
+
56
+ | Flag | Descrição |
57
+ |------|-----------|
58
+ | `--root PATH` | Raiz do app Rails (default: diretório atual) |
59
+ | `--force` | Sobrescrever specs existentes |
60
+ | `--dry-run` | Listar arquivos sem gravar |
61
+ | `--namespace PATH` | Prefixo de rotas (default: `/api/v1`) |
62
+
63
+ ## Configuração
64
+
65
+ Opcional: `config/routes_api_spec_generator.yml`
66
+
67
+ ```yaml
68
+ defaults:
69
+ namespace: /api/v1
70
+
71
+ endpoints:
72
+ api/v1/insights#email_metrics:
73
+ template: insights
74
+ tag: Insights
75
+ summary: email origin metrics
76
+ tenant_header: X-Tenant-ID
77
+ service_class: Insights::EmailService
78
+ channel: email
79
+ ```
80
+
81
+ Chaves em `endpoints` seguem `controller#action` (ex.: `api/v1/channels#index`).
82
+
83
+ ## Uso programático
84
+
85
+ ```ruby
86
+ require 'routes_api_spec_generator'
87
+
88
+ result = RoutesApiSpecGenerator::Generator.new(
89
+ rails_root: Rails.root,
90
+ force: false
91
+ ).run
92
+
93
+ puts "Gerados: #{result[:written]}, ignorados: #{result[:skipped]}"
94
+ ```
95
+
96
+ ## Publicar no RubyGems.org
97
+
98
+ ```bash
99
+ bundle install
100
+ bundle exec rspec
101
+ gem build routes_api_spec_generator.gemspec
102
+ gem push routes_api_spec_generator-0.1.0.gem
103
+ ```
104
+
105
+ > Requer conta no [RubyGems.org](https://rubygems.org) e MFA habilitado.
106
+
107
+ ## Desenvolvimento
108
+
109
+ ```bash
110
+ git clone https://github.com/saulofilho/routes-api-spec-generator.git
111
+ cd routes-api-spec-generator
112
+ bundle install
113
+ bundle exec rspec
114
+ bundle exec rake
115
+ ```
116
+
117
+ ## Fluxo recomendado
118
+
119
+ ```
120
+ routes.rb + controllers → routes_api_spec_generator → spec/requests/**/*
121
+
122
+ rspec (green)
123
+
124
+ rake rswag:specs:swaggerize
125
+ ```
126
+
127
+ ## Licença
128
+
129
+ MIT — veja [LICENSE.txt](LICENSE.txt).
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'routes_api_spec_generator'
5
+
6
+ RoutesApiSpecGenerator::CLI.start
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'optparse'
4
+
5
+ module RoutesApiSpecGenerator
6
+ class CLI
7
+ def self.start(argv = ARGV)
8
+ new.run(argv)
9
+ end
10
+
11
+ def run(argv)
12
+ options = {
13
+ force: false,
14
+ dry_run: false,
15
+ namespace: '/api/v1'
16
+ }
17
+
18
+ parser = OptionParser.new do |opts|
19
+ opts.banner = 'Usage: routes-api-spec-generator generate [options]'
20
+
21
+ opts.on('--root PATH', 'Rails app root (default: Dir.pwd)') { |v| options[:root] = v }
22
+ opts.on('--force', 'Overwrite existing spec files') { options[:force] = true }
23
+ opts.on('--dry-run', 'Print paths without writing files') { options[:dry_run] = true }
24
+ opts.on('--namespace PATH', 'Route prefix (default: /api/v1)') { |v| options[:namespace] = v }
25
+ opts.on('-h', '--help', 'Show help') { puts opts; exit }
26
+ end
27
+
28
+ parser.parse!(argv)
29
+ command = argv.first || 'generate'
30
+
31
+ case command
32
+ when 'generate'
33
+ generate!(options)
34
+ else
35
+ warn "Unknown command: #{command}"
36
+ puts parser
37
+ exit 1
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def generate!(options)
44
+ root = options[:root] || Dir.pwd
45
+ generator = Generator.new(
46
+ rails_root: root,
47
+ force: options[:force],
48
+ dry_run: options[:dry_run],
49
+ namespace: options[:namespace]
50
+ )
51
+ result = generator.run
52
+ report(result)
53
+ end
54
+
55
+ def report(result)
56
+ result[:files].each do |file|
57
+ case file[:status]
58
+ when :written
59
+ puts "✓ wrote #{file[:path]}"
60
+ when :skipped
61
+ puts "○ skipped #{file[:path]} (exists; use --force)"
62
+ when :dry_run
63
+ puts "→ would write #{file[:path]}"
64
+ end
65
+ end
66
+
67
+ puts "\nDone: #{result[:written]} written, #{result[:skipped]} skipped, #{result[:dry_run]} dry-run"
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RoutesApiSpecGenerator
4
+ class ConfigLoader
5
+ DEFAULT_PATH = 'config/routes_api_spec_generator.yml'
6
+
7
+ def initialize(rails_root:, config_path: nil)
8
+ @rails_root = Pathname.new(rails_root)
9
+ @config_path = config_path || @rails_root.join(DEFAULT_PATH)
10
+ end
11
+
12
+ def load
13
+ return empty_config unless File.exist?(@config_path)
14
+
15
+ YAML.safe_load(File.read(@config_path), aliases: true) || empty_config
16
+ end
17
+
18
+ private
19
+
20
+ def empty_config
21
+ { 'defaults' => {}, 'endpoints' => {} }
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pathname'
4
+
5
+ module RoutesApiSpecGenerator
6
+ class ControllerInspector
7
+ SERVICE_PATTERNS = {
8
+ /metrics_payload\(::Insights::(\w+)\)/ => :insights,
9
+ /::Channels::ResultsAggregatorService/ => :aggregator,
10
+ /::Channels::StatsFinder/ => :stats_finder
11
+ }.freeze
12
+
13
+ def initialize(rails_root:)
14
+ @rails_root = Pathname.new(rails_root)
15
+ end
16
+
17
+ def inspect(controller_path, action)
18
+ file = controller_file(controller_path)
19
+ return {} unless file && File.exist?(file)
20
+
21
+ source = File.read(file)
22
+ hints = {
23
+ permitted_params: extract_permit(source),
24
+ skip_timezone: source.include?('skip_before_action :validate_timezone'),
25
+ service_class: extract_service_for_action(source, action),
26
+ service_pattern: nil
27
+ }
28
+
29
+ hints[:service_pattern] = infer_service_pattern(hints[:service_class], source)
30
+ hints[:template] = infer_template(controller_path, action, hints, source)
31
+ hints[:tenant_header] = controller_path.include?('insights') ? 'X-Tenant-ID' : 'Tenant-ID'
32
+ hints[:channel] = infer_channel(action)
33
+ hints
34
+ end
35
+
36
+ private
37
+
38
+ def controller_file(controller_path)
39
+ @rails_root.join('app/controllers', "#{controller_path}_controller.rb")
40
+ end
41
+
42
+ def extract_permit(source)
43
+ match = source.match(/params\.permit\(([^)]+)\)/m)
44
+ return [] unless match
45
+
46
+ match[1].scan(/:(\w+)/).flatten.map(&:to_sym)
47
+ end
48
+
49
+ def extract_service_for_action(source, action)
50
+ match = source.match(/def #{action}\s*\n.*?metrics_payload\(::Insights::(\w+)\)/m)
51
+ return "Insights::#{match[1]}" if match
52
+
53
+ return 'Channels::ResultsAggregatorService' if source.include?('ResultsAggregatorService')
54
+ return 'Channels::StatsFinder' if source.include?('StatsFinder')
55
+
56
+ nil
57
+ end
58
+
59
+ def infer_service_pattern(service_class, source)
60
+ return :insights if service_class&.start_with?('Insights::')
61
+ return :aggregator if service_class == 'Channels::ResultsAggregatorService'
62
+ return :stats_finder if service_class == 'Channels::StatsFinder' || source.include?('StatsFinder')
63
+
64
+ nil
65
+ end
66
+
67
+ def infer_channel(action)
68
+ return 'email' if action.include?('email')
69
+ return 'sms' if action.include?('sms')
70
+ return 'whatsapp' if action.include?('whatsapp')
71
+
72
+ nil
73
+ end
74
+
75
+ def infer_template(_controller_path, action, hints, _source)
76
+ return 'general_metrics' if action == 'general_metrics'
77
+ return 'insights' if hints[:service_pattern] == :insights
78
+ return 'big_numbers' if hints[:service_pattern] == :aggregator
79
+ return 'channels' if hints[:service_pattern] == :stats_finder
80
+
81
+ action == 'index' ? 'resource_index' : 'insights'
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RoutesApiSpecGenerator
4
+ Endpoint = Struct.new(
5
+ :verb,
6
+ :path,
7
+ :controller,
8
+ :action,
9
+ :template,
10
+ :tag,
11
+ :summary,
12
+ :tenant_header,
13
+ :service_class,
14
+ :service_pattern,
15
+ :spec_filename,
16
+ :describe_name,
17
+ :extra_params,
18
+ :timezone,
19
+ :channel,
20
+ :resource_name,
21
+ keyword_init: true
22
+ ) do
23
+ def route_key
24
+ "#{controller}##{action}"
25
+ end
26
+
27
+ def insights?
28
+ template == 'insights'
29
+ end
30
+
31
+ def channels?
32
+ template == 'channels'
33
+ end
34
+
35
+ def big_numbers?
36
+ template == 'big_numbers'
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+
5
+ module RoutesApiSpecGenerator
6
+ class Generator
7
+ OUTPUT_DIR = 'spec/requests'
8
+
9
+ def initialize(rails_root:, force: false, dry_run: false, namespace: '/api/v1')
10
+ @rails_root = Pathname.new(rails_root)
11
+ @force = force
12
+ @dry_run = dry_run
13
+ @namespace = namespace
14
+ @config = ConfigLoader.new(rails_root: @rails_root).load
15
+ @inspector = ControllerInspector.new(rails_root: @rails_root)
16
+ end
17
+
18
+ def run
19
+ require_routes!
20
+ routes = RouteCollector.new(routes: @routes, namespace_prefix: @namespace).collect
21
+ endpoints = routes.map { |route| build_endpoint(route) }
22
+
23
+ results = endpoints.map { |endpoint| write_endpoint(endpoint) }
24
+ summary(results)
25
+ end
26
+
27
+ private
28
+
29
+ def require_routes!
30
+ require File.expand_path('config/environment', @rails_root)
31
+ @routes = Rails.application.routes.routes
32
+ end
33
+
34
+ def build_endpoint(route)
35
+ route_key = "#{route[:controller]}##{route[:action]}"
36
+ config = (@config.dig('endpoints', route_key) || {}).transform_keys(&:to_s)
37
+ hints = @inspector.inspect(route[:controller], route[:action])
38
+ defaults = (@config['defaults'] || {}).transform_keys(&:to_s)
39
+
40
+ template = config['template'] || hints[:template] || 'resource_index'
41
+ action = route[:action]
42
+ resource = route[:controller].split('/').last
43
+
44
+ spec_filename = config['spec_filename'] || spec_filename_for(resource, action)
45
+ describe_name = config['describe_name'] || route[:path].delete_prefix('/')
46
+
47
+ Endpoint.new(
48
+ verb: route[:verb],
49
+ path: route[:path],
50
+ controller: route[:controller],
51
+ action: action,
52
+ template: template,
53
+ tag: config['tag'] || tag_for(resource, action),
54
+ summary: config['summary'] || summary_for(resource, action),
55
+ tenant_header: config['tenant_header'] || hints[:tenant_header] || 'Tenant-ID',
56
+ service_class: config['service_class'] || hints[:service_class],
57
+ service_pattern: config['service_pattern'] || hints[:service_pattern]&.to_s,
58
+ spec_filename: spec_filename,
59
+ describe_name: describe_name,
60
+ extra_params: config['extra_params'] || extra_params_for(hints[:permitted_params]),
61
+ timezone: config.fetch('timezone', !hints[:skip_timezone]),
62
+ channel: config['channel'] || hints[:channel],
63
+ resource_name: resource
64
+ )
65
+ end
66
+
67
+ def spec_filename_for(resource, action)
68
+ return "#{resource}_spec.rb" if action == 'index'
69
+
70
+ "#{action}_spec.rb"
71
+ end
72
+
73
+ def tag_for(resource, action)
74
+ return resource.singularize.camelize if action == 'index'
75
+
76
+ 'Insights'
77
+ end
78
+
79
+ def summary_for(resource, action)
80
+ return "list #{resource.tr('_', ' ')}" if action == 'index'
81
+
82
+ "#{action.tr('_', ' ')}"
83
+ end
84
+
85
+ def extra_params_for(permitted)
86
+ Array(permitted).map(&:to_s) - %w[start_date end_date]
87
+ end
88
+
89
+ def write_endpoint(endpoint)
90
+ relative = File.join(OUTPUT_DIR, endpoint.path.delete_prefix('/').gsub(%r{/[^/]+$}, '') || '',
91
+ endpoint.spec_filename).squeeze('/')
92
+ # path like /api/v1/channels -> spec/requests/api/v1/channels_spec.rb
93
+ output = @rails_root.join(OUTPUT_DIR, path_to_spec_dir(endpoint.path), endpoint.spec_filename)
94
+ content = render(endpoint)
95
+
96
+ if File.exist?(output) && !@force
97
+ return { path: output, status: :skipped }
98
+ end
99
+
100
+ if @dry_run
101
+ return { path: output, status: :dry_run, content: content }
102
+ end
103
+
104
+ FileUtils.mkdir_p(output.dirname)
105
+ File.write(output, content)
106
+ { path: output, status: :written }
107
+ end
108
+
109
+ def path_to_spec_dir(path)
110
+ # /api/v1/channels -> api/v1
111
+ # /api/v1/insights/email_metrics -> api/v1
112
+ segments = path.delete_prefix('/').split('/')
113
+ segments[0..1].join('/') # api/v1
114
+ end
115
+
116
+ def render(endpoint)
117
+ template_file = template_path(endpoint.template)
118
+ context = TemplateContext.new(endpoint)
119
+ ERB.new(File.read(template_file), trim_mode: '-').result(context.instance_eval { binding })
120
+ end
121
+
122
+ def template_path(name)
123
+ File.join(__dir__, 'templates', "#{name}.erb")
124
+ end
125
+
126
+ def summary(results)
127
+ written = results.count { |r| r[:status] == :written }
128
+ skipped = results.count { |r| r[:status] == :skipped }
129
+ dry = results.count { |r| r[:status] == :dry_run }
130
+
131
+ {
132
+ written: written,
133
+ skipped: skipped,
134
+ dry_run: dry,
135
+ files: results
136
+ }
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RoutesApiSpecGenerator
4
+ class Railtie < Rails::Railtie
5
+ rake_tasks do
6
+ load 'routes_api_spec_generator/tasks/routes_api_spec_generator.rake'
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RoutesApiSpecGenerator
4
+ class RouteCollector
5
+ def initialize(routes:, namespace_prefix: '/api/v1', verbs: %w[GET])
6
+ @routes = routes
7
+ @namespace_prefix = namespace_prefix
8
+ @verbs = verbs.map(&:upcase)
9
+ end
10
+
11
+ def collect
12
+ @routes.filter_map do |route|
13
+ next unless matching_route?(route)
14
+
15
+ verb = primary_verb(route)
16
+ next unless @verbs.include?(verb)
17
+
18
+ {
19
+ verb: verb,
20
+ path: route.path.spec.to_s.gsub('(.:format)', ''),
21
+ controller: route.defaults[:controller],
22
+ action: route.defaults[:action].to_s
23
+ }
24
+ end.uniq { |r| [r[:path], r[:verb], r[:action]] }
25
+ end
26
+
27
+ private
28
+
29
+ def matching_route?(route)
30
+ path = route.path.spec.to_s
31
+ return false unless path.start_with?(@namespace_prefix)
32
+ return false if route.defaults[:controller].blank?
33
+
34
+ route.defaults[:controller].exclude?('rails/')
35
+ end
36
+
37
+ def primary_verb(route)
38
+ verb = route.verb
39
+ return 'GET' if verb.blank?
40
+
41
+ case verb
42
+ when String
43
+ verb.upcase
44
+ when Regexp
45
+ verb.source.gsub(/[\^\$\(\)]/, '').split('|').first.to_s.upcase
46
+ else
47
+ 'GET'
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'routes_api_spec_generator'
4
+ require 'rake'
5
+
6
+ namespace :routes_api_spec do
7
+ desc 'Generate rswag request specs from Rails routes'
8
+ task generate: :environment do
9
+ result = RoutesApiSpecGenerator::Generator.new(
10
+ rails_root: Rails.root,
11
+ force: ENV['FORCE'] == 'true',
12
+ dry_run: ENV['DRY_RUN'] == 'true'
13
+ ).run
14
+
15
+ result[:files].each do |file|
16
+ case file[:status]
17
+ when :written then puts "✓ #{file[:path]}"
18
+ when :skipped then puts "○ #{file[:path]}"
19
+ when :dry_run then puts "→ #{file[:path]}"
20
+ end
21
+ end
22
+
23
+ puts "Done: #{result[:written]} written, #{result[:skipped]} skipped"
24
+ end
25
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RoutesApiSpecGenerator
4
+ class TemplateContext
5
+ attr_reader :endpoint
6
+
7
+ def initialize(endpoint)
8
+ @endpoint = endpoint
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+ require 'swagger_helper'
3
+
4
+ RSpec.describe '<%= endpoint.describe_name %>', type: :request do
5
+ let(:'<%= endpoint.tenant_header %>') { '176980161229322768' }
6
+ let(:start_date) { '2026-03-01' }
7
+ let(:end_date) { '2026-03-31' }
8
+ <% if endpoint.extra_params.include?('period') -%>
9
+ let(:period) { 'custom' }
10
+ <% end -%>
11
+
12
+ path '<%= endpoint.path %>' do
13
+ get('<%= endpoint.summary %>') do
14
+ tags '<%= endpoint.tag %>'
15
+ produces 'application/json'
16
+
17
+ parameter name: '<%= endpoint.tenant_header %>', in: :header, schema: { type: :string }, required: true
18
+ parameter name: :start_date, in: :query, schema: { type: :string, format: :date }, required: true
19
+ parameter name: :end_date, in: :query, schema: { type: :string, format: :date }, required: true
20
+ <% endpoint.extra_params.each do |param| -%>
21
+ parameter name: :<%= param %>, in: :query, schema: { type: :string }, required: false
22
+ <% end -%>
23
+
24
+ response(200, 'successful') do
25
+ schema type: :object
26
+
27
+ let(:aggregator) { instance_double(Channels::ResultsAggregatorService) }
28
+
29
+ before do
30
+ allow(Channels::ResultsAggregatorService).to receive(:new).and_return(aggregator)
31
+ allow(aggregator).to receive(:call).and_return([])
32
+ end
33
+
34
+ run_test!
35
+ end
36
+
37
+ response(400, 'bad request - invalid dates') do
38
+ let(:start_date) { 'data-invalida' }
39
+ run_test!
40
+ end
41
+
42
+ response(400, 'bad request - missing tenant') do
43
+ let(:'<%= endpoint.tenant_header %>') { nil }
44
+ run_test!
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+ require 'swagger_helper'
3
+
4
+ RSpec.describe '<%= endpoint.describe_name %>', type: :request do
5
+ let(:'<%= endpoint.tenant_header %>') { 'test-tenant' }
6
+ <% if endpoint.timezone -%>
7
+ let(:Timezone) { 'UTC' }
8
+ <% end -%>
9
+ let(:start_date) { '2026-01-01' }
10
+ let(:end_date) { '2026-01-31' }
11
+
12
+ path '<%= endpoint.path %>' do
13
+ get('<%= endpoint.summary %>') do
14
+ tags '<%= endpoint.tag %>'
15
+ produces 'application/json'
16
+
17
+ parameter name: '<%= endpoint.tenant_header %>', in: :header, schema: { type: :string }, required: true
18
+ <% if endpoint.timezone -%>
19
+ parameter name: 'Timezone', in: :header, schema: { type: :string }, required: false
20
+ <% end -%>
21
+ parameter name: :start_date, in: :query, schema: { type: :string, format: :date }, required: true
22
+ parameter name: :end_date, in: :query, schema: { type: :string, format: :date }, required: true
23
+
24
+ response(200, 'successful') do
25
+ schema type: :object
26
+
27
+ before do
28
+ allow_any_instance_of(Channels::EmailService).to receive(:fetch_stats)
29
+ .and_return({ channel: 'email', sent: 100, delivered: 90, clicked: 10 })
30
+ allow_any_instance_of(Channels::WhatsappService).to receive(:fetch_stats)
31
+ .and_return({ channel: 'whatsapp', sent: 50, delivered: 45, clicked: nil })
32
+ allow_any_instance_of(Channels::SmsService).to receive(:fetch_stats)
33
+ .and_return({ channel: 'sms', sent: 30, delivered: 25, clicked: 5 })
34
+ end
35
+
36
+ run_test!
37
+ end
38
+
39
+ response(400, 'bad request - missing dates') do
40
+ let(:start_date) { nil }
41
+ let(:end_date) { nil }
42
+ run_test!
43
+ end
44
+
45
+ response(400, 'bad request - missing tenant') do
46
+ let(:'<%= endpoint.tenant_header %>') { nil }
47
+ run_test!
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+ require 'swagger_helper'
3
+
4
+ RSpec.describe '<%= endpoint.describe_name %>', type: :request do
5
+ let(:'<%= endpoint.tenant_header %>') { '176980161229322768' }
6
+ let(:start_date) { '2026-03-01' }
7
+ let(:end_date) { '2026-03-31' }
8
+
9
+ path '<%= endpoint.path %>' do
10
+ get('<%= endpoint.summary %>') do
11
+ tags '<%= endpoint.tag %>'
12
+ produces 'application/json'
13
+
14
+ parameter name: '<%= endpoint.tenant_header %>', in: :header, schema: { type: :string }, required: true
15
+ parameter name: :start_date, in: :query, schema: { type: :string, format: :date }, required: true
16
+ parameter name: :end_date, in: :query, schema: { type: :string, format: :date }, required: true
17
+
18
+ response(200, 'successful') do
19
+ schema type: :object, required: ['data']
20
+
21
+ let(:service) { instance_double(<%= endpoint.service_class %>) }
22
+
23
+ before do
24
+ allow(<%= endpoint.service_class %>).to receive(:new)
25
+ .with('176980161229322768', period: { start_date: start_date, end_date: end_date })
26
+ .and_return(service)
27
+ allow(service).to receive(:call).and_return(
28
+ data: {
29
+ period: { start_date: start_date, end_date: end_date },
30
+ result: [
31
+ { channel: 'email', total_sent: 100, total_delivered: 90 }
32
+ ]
33
+ }
34
+ )
35
+ end
36
+
37
+ run_test!
38
+ end
39
+
40
+ response(400, 'bad request - invalid dates') do
41
+ let(:start_date) { 'data-invalida' }
42
+ run_test!
43
+ end
44
+
45
+ response(400, 'bad request - missing tenant') do
46
+ let(:'<%= endpoint.tenant_header %>') { nil }
47
+ run_test!
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+ require 'swagger_helper'
3
+
4
+ RSpec.describe '<%= endpoint.describe_name %>', type: :request do
5
+ let(:'<%= endpoint.tenant_header %>') { '176980161229322768' }
6
+ let(:start_date) { '2026-03-01' }
7
+ let(:end_date) { '2026-03-31' }
8
+
9
+ path '<%= endpoint.path %>' do
10
+ get('<%= endpoint.summary %>') do
11
+ tags '<%= endpoint.tag %>'
12
+ produces 'application/json'
13
+
14
+ parameter name: '<%= endpoint.tenant_header %>', in: :header, schema: { type: :string }, required: true
15
+ parameter name: :start_date, in: :query, schema: { type: :string, format: :date }, required: true
16
+ parameter name: :end_date, in: :query, schema: { type: :string, format: :date }, required: true
17
+
18
+ response(200, 'successful') do
19
+ schema type: :object, required: ['data']
20
+
21
+ let(:service) { instance_double(<%= endpoint.service_class %>) }
22
+
23
+ before do
24
+ allow(<%= endpoint.service_class %>).to receive(:new)
25
+ .with('176980161229322768', period: { start_date: start_date, end_date: end_date })
26
+ .and_return(service)
27
+ allow(service).to receive(:call).and_return(
28
+ data: {
29
+ period: { start_date: start_date, end_date: end_date },
30
+ <% if endpoint.channel -%>
31
+ channel: '<%= endpoint.channel %>',
32
+ <% end -%>
33
+ result: []
34
+ }
35
+ )
36
+ end
37
+
38
+ run_test!
39
+ end
40
+
41
+ response(400, 'bad request - invalid dates') do
42
+ let(:start_date) { 'data-invalida' }
43
+ run_test!
44
+ end
45
+
46
+ response(400, 'bad request - missing tenant') do
47
+ let(:'<%= endpoint.tenant_header %>') { nil }
48
+ run_test!
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+ require 'swagger_helper'
3
+
4
+ RSpec.describe '<%= endpoint.describe_name %>', type: :request do
5
+ let(:'<%= endpoint.tenant_header %>') { '176980161229322768' }
6
+ let(:start_date) { '2026-03-01' }
7
+ let(:end_date) { '2026-03-31' }
8
+
9
+ path '<%= endpoint.path %>' do
10
+ get('<%= endpoint.summary %>') do
11
+ tags '<%= endpoint.tag %>'
12
+ produces 'application/json'
13
+
14
+ parameter name: '<%= endpoint.tenant_header %>', in: :header, schema: { type: :string }, required: true
15
+ parameter name: :start_date, in: :query, schema: { type: :string, format: :date }, required: true
16
+ parameter name: :end_date, in: :query, schema: { type: :string, format: :date }, required: true
17
+
18
+ response(200, 'successful') do
19
+ schema type: :object
20
+
21
+ run_test!
22
+ end
23
+
24
+ response(400, 'bad request - invalid dates') do
25
+ let(:start_date) { 'data-invalida' }
26
+ run_test!
27
+ end
28
+
29
+ response(400, 'bad request - missing tenant') do
30
+ let(:'<%= endpoint.tenant_header %>') { nil }
31
+ run_test!
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RoutesApiSpecGenerator
4
+ VERSION = '0.1.0'
5
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/core_ext/string/inflections'
4
+ require 'erb'
5
+ require 'yaml'
6
+
7
+ require_relative 'routes_api_spec_generator/version'
8
+ require_relative 'routes_api_spec_generator/endpoint'
9
+ require_relative 'routes_api_spec_generator/config_loader'
10
+ require_relative 'routes_api_spec_generator/route_collector'
11
+ require_relative 'routes_api_spec_generator/controller_inspector'
12
+ require_relative 'routes_api_spec_generator/generator'
13
+ require_relative 'routes_api_spec_generator/template_context'
14
+ require_relative 'routes_api_spec_generator/cli'
15
+
16
+ require_relative 'routes_api_spec_generator/railtie' if defined?(Rails::Railtie)
17
+
18
+ module RoutesApiSpecGenerator
19
+ class Error < StandardError; end
20
+ end
metadata ADDED
@@ -0,0 +1,109 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: routes_api_spec_generator
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Saulo Filho
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: activesupport
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '7.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '7.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: rake
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '13.0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '13.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: rspec
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '3.12'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '3.12'
54
+ description: |
55
+ Ruby gem que lê rotas e controllers de uma API Rails e materializa
56
+ request specs RSpec com rswag — alinhado ao fluxo spec → swagger
57
+ (rswag:specs:swaggerize).
58
+ email:
59
+ - saulofilho@users.noreply.github.com
60
+ executables:
61
+ - routes-api-spec-generator
62
+ extensions: []
63
+ extra_rdoc_files: []
64
+ files:
65
+ - CHANGELOG.md
66
+ - LICENSE.txt
67
+ - README.md
68
+ - exe/routes-api-spec-generator
69
+ - lib/routes_api_spec_generator.rb
70
+ - lib/routes_api_spec_generator/cli.rb
71
+ - lib/routes_api_spec_generator/config_loader.rb
72
+ - lib/routes_api_spec_generator/controller_inspector.rb
73
+ - lib/routes_api_spec_generator/endpoint.rb
74
+ - lib/routes_api_spec_generator/generator.rb
75
+ - lib/routes_api_spec_generator/railtie.rb
76
+ - lib/routes_api_spec_generator/route_collector.rb
77
+ - lib/routes_api_spec_generator/tasks/routes_api_spec_generator.rake
78
+ - lib/routes_api_spec_generator/template_context.rb
79
+ - lib/routes_api_spec_generator/templates/big_numbers.erb
80
+ - lib/routes_api_spec_generator/templates/channels.erb
81
+ - lib/routes_api_spec_generator/templates/general_metrics.erb
82
+ - lib/routes_api_spec_generator/templates/insights.erb
83
+ - lib/routes_api_spec_generator/templates/resource_index.erb
84
+ - lib/routes_api_spec_generator/version.rb
85
+ homepage: https://github.com/saulofilho/routes-api-spec-generator
86
+ licenses:
87
+ - MIT
88
+ metadata:
89
+ homepage_uri: https://github.com/saulofilho/routes-api-spec-generator
90
+ source_code_uri: https://github.com/saulofilho/routes-api-spec-generator
91
+ rubygems_mfa_required: 'true'
92
+ rdoc_options: []
93
+ require_paths:
94
+ - lib
95
+ required_ruby_version: !ruby/object:Gem::Requirement
96
+ requirements:
97
+ - - ">="
98
+ - !ruby/object:Gem::Version
99
+ version: 3.1.0
100
+ required_rubygems_version: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ version: '0'
105
+ requirements: []
106
+ rubygems_version: 3.7.1
107
+ specification_version: 4
108
+ summary: Gera request specs rswag a partir de rotas Rails
109
+ test_files: []