bddgenx 0.1.32 → 0.1.34

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fda31f4771f3fc150f52a9a6242c1e0fac890009c3cf66281640c9f58152f53c
4
- data.tar.gz: 60ec20719d98e9f32151f5f4cebeff5123d237d158a8495ce389d7ec65f201bc
3
+ metadata.gz: 9bf7bc9f9f2860f8e305530e4e445ecdc67a9f45bfc4d6dca751f954bea45931
4
+ data.tar.gz: cdff8e55fea97da8d77fce3fde6d01d53de653a2bbaa15ce5af84bb27d58f700
5
5
  SHA512:
6
- metadata.gz: c1f92ae05c89f892f8f206535cf3928be7037ca95f71787f01e74a8ecd401cdf7fa7b852f22b2e0d45de5a757b7ae385dd705bfbb53e28b5c3694d2a398608cd
7
- data.tar.gz: 99964cbae7ef80abb75792c6629932efd9a66d4b9000d0e839c7b8ec94f57b793065a04c17722ec72f508df76185ff3d8a13c69008805c040042b3232316fee1
6
+ metadata.gz: 830ff28e5997763916c6ff02cd51d5c2eb20cdb8933c2f6a695391f39a327a6c1ad330e578855e8048afc00eb5d96006d4435f56e36459ce32d41624a6fa2301
7
+ data.tar.gz: e365e0c9f7edbb717538b26bbee7dce27b9621f2fb54b26db8fde1d3996636ec4c408f9b0f50f1b2d5d81b3c8c59b494454d75516b8a0def0f6b198c7b065cba
data/README.md CHANGED
@@ -18,8 +18,6 @@ bddgenx/
18
18
  │ └── pdf/ # relatórios camelCase
19
19
  ├── lib/
20
20
  │ ├── bddgenx/
21
- │ │ ├── integrations
22
- │ │ │ └── jira.rb
23
21
  │ │ ├── parser.rb
24
22
  │ │ ├── validator.rb
25
23
  │ │ ├── generator.rb
@@ -59,11 +57,11 @@ Como um usuario do sistema
59
57
  Quero fazer login com sucesso
60
58
  Para acessar minha conta
61
59
 
62
- [SUCCESS]@mobile
60
+ [FAILURE]
63
61
  Quando preencho email e senha válidos
64
62
  Então vejo a tela inicial
65
63
 
66
- [SUCCESS]@regressivo
64
+ [SUCCESS]
67
65
  Quando tento logar com "<email>" e "<senha>"
68
66
  Então recebo "<resultado>"
69
67
 
@@ -179,7 +177,6 @@ namespace :bddgenx do
179
177
  puts "✅ Geração BDD concluída com sucesso!"
180
178
  end
181
179
  end
182
-
183
180
  ```
184
181
 
185
182
  👨‍💻 Autor
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.32
1
+ 0.1.34
File without changes
File without changes
File without changes
File without changes
@@ -1,73 +1,97 @@
1
1
  require 'fileutils'
2
+ require_relative 'utils/tipo_param'
2
3
 
3
4
  module Bddgenx
4
5
  class Generator
5
- TIPOS_ISTQB = {
6
- "SUCCESS" => "Teste Positivo",
7
- "FAILURE" => "Teste Negativo",
8
- "ERROR" => "Teste de Erro",
9
- "EXCEPTION" => "Teste de Exceção",
10
- "VALIDATION" => "Teste de Validação",
11
- "PERMISSION" => "Teste de Permissão",
12
- "EDGE_CASE" => "Teste de Limite",
13
- "PERFORMANCE" => "Teste de Desempenho"
14
- }.freeze
6
+ GHERKIN_KEYS_PT = %w[Dado Quando Então E Mas].freeze
7
+ GHERKIN_KEYS_EN = %w[Given When Then And But].freeze
8
+ GHERKIN_MAP_PT_EN = GHERKIN_KEYS_PT.zip(GHERKIN_KEYS_EN).to_h
9
+ GHERKIN_MAP_EN_PT = GHERKIN_KEYS_EN.zip(GHERKIN_KEYS_PT).to_h
10
+ ALL_KEYS = GHERKIN_KEYS_PT + GHERKIN_KEYS_EN
11
+
12
+ # Divide raw example blocks into a single table
13
+ def self.dividir_examples(raw)
14
+ raw.select { |l| l.strip.start_with?('|') }
15
+ end
16
+
17
+ # Gera .feature a partir de hash ou arquivo de história
18
+ # Respeita idioma em historia[:idioma]
19
+ def self.gerar_feature(input, override_path = nil)
20
+ historia = input.is_a?(String) ? Parser.ler_historia(input) : input
21
+ idioma = historia[:idioma] || 'pt'
22
+ nome_base = historia[:quero].gsub(/[^a-z0-9]/i, '_')
23
+ .downcase.split('_',3)[0,3].join('_')
24
+ caminho = override_path.is_a?(String) ? override_path : "features/#{nome_base}.feature"
15
25
 
16
- def self.gerar_feature(historia)
17
- idioma = historia[:idioma]
18
26
  palavras = {
19
- contexto: idioma == 'en' ? 'Background' : 'Contexto',
20
- cenario: idioma == 'en' ? 'Scenario' : 'Cenário',
21
- esquema: idioma == 'en' ? 'Scenario Outline' : 'Esquema do Cenário',
22
- exemplos: idioma == 'en' ? 'Examples' : 'Exemplos',
23
- regra: idioma == 'en' ? 'Rule' : 'Regra'
27
+ feature: idioma=='en' ? 'Feature' : 'Funcionalidade',
28
+ contexto: idioma=='en' ? 'Background' : 'Contexto',
29
+ cenario: idioma=='en' ? 'Scenario' : 'Cenário',
30
+ esquema: idioma=='en' ? 'Scenario Outline' : 'Esquema do Cenário',
31
+ exemplos: idioma=='en' ? 'Examples' : 'Exemplos',
32
+ regra: idioma=='en' ? 'Rule' : 'Regra'
24
33
  }
25
34
 
26
- frase_quero = historia[:quero].sub(/^\s*quero\s*/i, '')
27
- partes = frase_quero.split(/\s+/)[0,3] # pega só as 3 primeiras palavras
28
- slug = partes.join('_')
29
- .gsub(/[^a-z0-9_]/i, '') # remove caracteres especiais
30
- .downcase
31
- nome_base = slug
32
- caminho = "features/#{nome_base}.feature"
33
-
34
- conteudo = <<~GHERKIN
35
+ conteudo = <<~GHK
35
36
  # language: #{idioma}
36
- Funcionalidade: #{historia[:quero].sub(/^Quero\s*/, '')}
37
+ #{palavras[:feature]}: #{historia[:quero].sub(/^Quero\s*/i,'')}
38
+ # #{historia[:como]}
39
+ # #{historia[:quero]}
40
+ # #{historia[:para]}
37
41
 
38
- #{historia[:como]}
39
- #{historia[:quero]}
40
- #{historia[:para]}
42
+ GHK
41
43
 
42
- GHERKIN
44
+ pt_map = GHERKIN_MAP_PT_EN
45
+ en_map = GHERKIN_MAP_EN_PT
46
+ detect = ALL_KEYS
43
47
 
44
48
  historia[:grupos].each_with_index do |grupo, idx|
45
- tipo = grupo[:tipo]
46
- tag = grupo[:tag]
47
- passos = grupo[:passos]
48
- exemplos = grupo[:exemplos]
49
-
49
+ passos = grupo[:passos] || []
50
+ exemplos = grupo[:exemplos] || []
50
51
  next if passos.empty?
51
52
 
52
- linha_tag = ["@#{tipo.downcase}", ("@#{tag}" if tag)].compact.join(' ')
53
- possui_parametros = passos.any? { |p| p.include?('<') } && exemplos.any?
53
+ tag_line = ["@#{grupo[:tipo].downcase}", ("@#{grupo[:tag]}" if grupo[:tag])].compact.join(' ')
54
+
55
+ if exemplos.any?
56
+ # Scenario Outline
57
+ conteudo << " #{tag_line}\n"
58
+ conteudo << " #{palavras[:esquema]}: Exemplo #{idx+1}\n"
54
59
 
55
- if possui_parametros
56
- conteudo << " #{linha_tag}\n"
57
- conteudo << " #{palavras[:esquema]}: Exemplo #{idx + 1}\n"
58
- passos.each { |p| conteudo << " #{p}\n" }
60
+ # Renderiza cada passo, mapeando connector mesmo se não for Gherkin padrão
61
+ passos.each do |p|
62
+ line = p.strip
63
+ parts = line.split(' ', 2)
64
+ # match connector case-insensitive
65
+ con_in = detect.find { |k| k.casecmp(parts[0]) == 0 } || parts[0]
66
+ text = parts[1] || ''
67
+ out_conn = idioma=='en' ? pt_map[con_in] || con_in : en_map[con_in] || con_in
68
+ conteudo << " #{out_conn} #{text}
69
+ "
70
+ end
71
+
72
+ # Monta tabela de exemplos completa
73
+ # Renderiza o bloco de Examples exatamente como veio no TXT
59
74
  conteudo << "\n #{palavras[:exemplos]}:\n"
60
- exemplos.each { |linha| conteudo << " #{linha}\n" }
75
+ exemplos.select { |l| l.strip.start_with?('|') }.each do |line|
76
+ # Remove aspas apenas das células, mantendo todas as colunas e valores originais
77
+ cleaned = line.strip.gsub(/^"|"$/, '')
78
+ conteudo << " #{cleaned}\n"
79
+ end
61
80
  conteudo << "\n"
62
81
  else
63
- nome_teste = TIPOS_ISTQB[tipo] || palavras[:cenario]
64
- contexto = passos.first.gsub(/^(Dado|Quando|Então|E|Mas)\s+/, '').strip
65
- resultado = passos.last .gsub(/^(Dado|Quando|Então|E|Mas)\s+/, '').strip
66
- nome_ceno = "#{nome_teste} - #{contexto} - #{resultado}"
67
-
68
- conteudo << " #{linha_tag}\n"
69
- conteudo << " #{palavras[:cenario]}: #{nome_ceno}\n"
70
- passos.each { |p| conteudo << " #{p}\n" }
82
+ # Scenario simples
83
+ conteudo << " #{tag_line}\n"
84
+ conteudo << " #{palavras[:cenario]}: #{grupo[:tipo].capitalize}\n"
85
+ passos.each do |p|
86
+ line = p.strip
87
+ parts = line.split(' ', 2)
88
+ # match connector case-insensitive
89
+ con_in = detect.find { |k| k.casecmp(parts[0]) == 0 } || parts[0]
90
+ text = parts[1] || ''
91
+ out_conn = idioma=='en' ? pt_map[con_in] || con_in : en_map[con_in] || con_in
92
+ conteudo << " #{out_conn} #{text}
93
+ "
94
+ end
71
95
  conteudo << "\n"
72
96
  end
73
97
  end
@@ -79,7 +103,6 @@ module Bddgenx
79
103
  FileUtils.mkdir_p(File.dirname(caminho))
80
104
  File.write(caminho, conteudo)
81
105
  puts "✅ Arquivo .feature gerado: #{caminho}"
82
- true
83
106
  end
84
107
  end
85
- end
108
+ end
@@ -1,47 +1,38 @@
1
1
  module Bddgenx
2
+ # Tipos de blocos GUARDADOS, incluindo português EXEMPLO/EXEMPLOS
2
3
  TIPOS_BLOCOS = %w[
3
4
  CONTEXT SUCCESS FAILURE ERROR EXCEPTION
4
5
  VALIDATION PERMISSION EDGE_CASE PERFORMANCE
5
- EXAMPLES REGRA RULE
6
+ EXEMPLO EXEMPLOS RULE
6
7
  ].freeze
7
8
 
8
9
  class Parser
10
+ # Lê arquivo de história (.txt) e retorna hash com atributos e grupos
9
11
  def self.ler_historia(caminho_arquivo)
10
- linhas = File.readlines(caminho_arquivo, encoding: 'utf-8')
11
- .map(&:strip)
12
- .reject(&:empty?)
12
+ # todas as linhas, mantendo vazias
13
+ linhas = File.readlines(caminho_arquivo, encoding: 'utf-8').map(&:rstrip)
13
14
 
14
- # Detecta idioma (padrão pt, usa en se encontrar '# lang: en')
15
- idioma = linhas.first.downcase.include?('# lang: en') ? 'en' : 'pt'
16
- linhas.shift if linhas.first.downcase.start_with?('# lang:')
17
-
18
- # Cabeçalho Gherkin (case-insensitive): Como, Eu Como ou As a
19
- como = nil
20
- linhas.each_with_index do |l, i|
21
- if l =~ /^\s*(?:como|eu como|as a)\b/i || l =~ /^\s*(?:COMO|EU COMO|AS A)\b/i
22
- como = l
23
- linhas = linhas[(i+1)..]
24
- break
25
- end
15
+ # Detecta idioma: aceita "# lang: en" ou "# language: en"
16
+ idioma = 'pt'
17
+ if linhas.first =~ /^#\s*lang(?:uage)?\s*:\s*(\w+)/i
18
+ idioma = Regexp.last_match(1).downcase
19
+ linhas.shift
26
20
  end
27
21
 
28
- # 'Quero' ou 'Eu Quero'
29
- quero = nil
30
- linhas.each_with_index do |l, i|
31
- if l =~ /^\s*(?:quero|eu quero|quero que)\b/i || l =~ /^\s*(?:QUERO|EU QUERO|QUERO QUE)\b/i
22
+ # Extrai Cabeçalho Como/Quero/Para
23
+ como, quero, para = nil, nil, nil
24
+ linhas.reject! do |l|
25
+ if l =~ /^\s*(?:como|eu como|as a)/i && como.nil?
26
+ como = l
27
+ true
28
+ elsif l =~ /^\s*(?:quero|eu quero|quero que)/i && quero.nil?
32
29
  quero = l
33
- linhas = linhas[(i+1)..]
34
- break
35
- end
36
- end
37
-
38
- # 'Para', 'Para Eu' ou 'Para Que'
39
- para = nil
40
- linhas.each_with_index do |l, i|
41
- if l =~ /^\s*(?:para|para eu|para que)\b/i || l =~ /^\s*(?:PRA|PARA EU|PARA QUE)\b/i
30
+ true
31
+ elsif l =~ /^\s*(?:para|para eu|para que)/i && para.nil?
42
32
  para = l
43
- linhas = linhas[(i+1)..]
44
- break
33
+ true
34
+ else
35
+ false
45
36
  end
46
37
  end
47
38
 
@@ -49,30 +40,30 @@ module Bddgenx
49
40
  exemplos_mode = false
50
41
 
51
42
  linhas.each do |linha|
52
- # Início de bloco de exemplos
53
- if linha =~ /^\[EXAMPLES\](?:@(\w+))?$/i
43
+ # Início de bloco de exemplos: [EXEMPLO] ou [EXEMPLOS] ou [EXAMPLES]
44
+ if linha =~ /^\[(?:EXEMPLO|EXEMPLOS|EXAMPLES)\](?:@(\w+))?$/i
54
45
  exemplos_mode = true
46
+ # inicia array se for o primeiro exemplo
55
47
  historia[:grupos].last[:exemplos] = []
56
48
  next
57
49
  end
58
50
 
59
- # Início de bloco de tipo (SUCCESS, FAILURE etc.)
51
+ # Início de bloco de tipo (SUCCESS, FAILURE, REGRA etc.)
60
52
  if linha =~ /^\[(#{TIPOS_BLOCOS.join('|')})\](?:@(\w+))?$/i
61
53
  exemplos_mode = false
62
- tipo = $1.upcase
63
- tag = $2
54
+ tipo = Regexp.last_match(1).upcase
55
+ tag = Regexp.last_match(2)
64
56
  historia[:grupos] << { tipo: tipo, tag: tag, passos: [], exemplos: [] }
65
57
  next
66
58
  end
67
59
 
68
- # Atribui linhas ao último grupo existente
60
+ # Atribui linhas ao último grupo
69
61
  next if historia[:grupos].empty?
70
- atual = historia[:grupos].last
71
-
62
+ bloco = historia[:grupos].last
72
63
  if exemplos_mode
73
- atual[:exemplos] << linha
64
+ bloco[:exemplos] << linha
74
65
  else
75
- atual[:passos] << linha
66
+ bloco[:passos] << linha
76
67
  end
77
68
  end
78
69
 
@@ -0,0 +1,115 @@
1
+ # lib/bddgenx/cli.rb
2
+ require 'fileutils'
3
+ require_relative 'parser'
4
+ require_relative 'generator'
5
+ require_relative 'steps_generator'
6
+ require_relative 'validator'
7
+ require_relative 'backup'
8
+ require_relative 'pdf_exporter'
9
+
10
+ module Bddgenx
11
+ class Runner
12
+ # Retorna lista de arquivos .txt em input/ ou só aqueles baseados em ARGV
13
+ def self.selecionar_arquivos_txt(input_dir)
14
+ ARGV.map do |arg|
15
+ nome = arg.end_with?('.txt') ? arg : "#{arg}.txt"
16
+ path = File.join(input_dir, nome)
17
+ unless File.exist?(path)
18
+ warn "⚠️ Arquivo não encontrado: #{path}"
19
+ next
20
+ end
21
+ path
22
+ end.compact
23
+ end
24
+
25
+ # Interativo: permite ao usuário escolher entre os .txt em input/
26
+ def self.choose_input(input_dir)
27
+ files = Dir.glob(File.join(input_dir, '*.txt'))
28
+ if files.empty?
29
+ warn "❌ Não há arquivos .txt no diretório #{input_dir}"
30
+ exit 1
31
+ end
32
+
33
+ puts "Selecione o arquivo de história para processar:"
34
+ files.each_with_index do |f, i|
35
+ puts "#{i+1}. #{File.basename(f)}"
36
+ end
37
+ print "Digite o número correspondente (ou ENTER para todos): "
38
+ choice = STDIN.gets.chomp
39
+
40
+ return files if choice.empty?
41
+
42
+ idx = choice.to_i - 1
43
+ unless idx.between?(0, files.size - 1)
44
+ warn "❌ Escolha inválida."
45
+ exit 1
46
+ end
47
+ [files[idx]]
48
+ end
49
+
50
+ def self.execute
51
+ history_dir = 'input'
52
+ Dir.mkdir(history_dir) unless Dir.exist?(history_dir)
53
+
54
+ # Determina quais arquivos processar
55
+ arquivos = if ARGV.any?
56
+ selecionar_arquivos_txt(history_dir)
57
+ else
58
+ choose_input(history_dir)
59
+ end
60
+
61
+ if arquivos.empty?
62
+ warn "❌ Nenhum arquivo de história para processar."
63
+ exit 1
64
+ end
65
+
66
+ # Contadores e coleções
67
+ total = features = steps = ignored = 0
68
+ skipped_steps = []
69
+ generated_pdfs = []
70
+ skipped_pdfs = []
71
+
72
+ arquivos.each do |arquivo|
73
+ total += 1
74
+ puts "\n🔍 Processando: #{arquivo}"
75
+
76
+ historia = Parser.ler_historia(arquivo)
77
+ unless Validator.validar(historia)
78
+ ignored += 1
79
+ puts "❌ Arquivo inválido: #{arquivo}"
80
+ next
81
+ end
82
+
83
+ # Geração de feature
84
+ feature_path, feature_content = Bddgenx::Generator.gerar_feature(historia)
85
+ Backup.salvar_versao_antiga(feature_path)
86
+ if Bddgenx::Generator.salvar_feature(feature_path, feature_content)
87
+ features += 1
88
+ end
89
+
90
+ # Geração de steps
91
+ if StepsGenerator.gerar_passos(feature_path)
92
+ steps += 1
93
+ else
94
+ skipped_steps << feature_path
95
+ end
96
+
97
+ # Exportação de PDFs
98
+ FileUtils.mkdir_p('reports')
99
+ results = PDFExporter.exportar_todos(only_new: true)
100
+ generated_pdfs.concat(results[:generated])
101
+ skipped_pdfs.concat(results[:skipped])
102
+ end
103
+
104
+ # Resumo final
105
+ puts "\n✅ Processamento concluído"
106
+ puts "- Total de histórias: #{total}"
107
+ puts "- Features geradas: #{features}"
108
+ puts "- Steps gerados: #{steps}"
109
+ puts "- Steps ignorados: #{skipped_steps.size}"
110
+ puts "- PDFs gerados: #{generated_pdfs.size}"
111
+ puts "- PDFs já existentes: #{skipped_pdfs.size}"
112
+ puts "- Arquivos ignorados: #{ignored}"
113
+ end
114
+ end
115
+ end
@@ -1,138 +1,108 @@
1
1
  require 'fileutils'
2
- require_relative 'utils/verificador'
2
+ require_relative 'utils/tipo_param'
3
+ require 'strscan' # para usar ::StringScanner
4
+
5
+ # lib/bddgenx/steps_generator.rb
3
6
 
4
7
  module Bddgenx
5
8
  class StepsGenerator
6
- PADROES = {
7
- 'pt' => %w[Dado Quando Então E Mas],
8
- 'en' => %w[Given When Then And But]
9
- }
10
-
11
- # Gera step definitions a partir da estrutura historia[:grupos]
12
- def self.gerar_passos(historia, nome_arquivo_feature)
13
- idioma = historia[:idioma] || 'pt'
14
- conectores = PADROES[idioma]
15
- passos_gerados = []
16
-
17
- historia[:grupos].each do |grupo|
18
- tipo = grupo[:tipo]
19
- passos = grupo[:passos]
20
- exemplos_brutos = grupo[:exemplos]
21
- exemplos = exemplos_brutos&.any? ? dividir_examples(exemplos_brutos) : nil
22
-
23
- next unless passos.is_a?(Array) && passos.any?
24
-
25
- passos.each do |linha|
26
- conector = conectores.find { |c| linha.strip.start_with?(c) }
27
- next unless conector
28
-
29
- corpo = linha.strip.sub(/^#{conector}\s*/, '')
30
- corpo_sanitizado = corpo.gsub(/"(<[^>]+>)"/, '\1')
31
-
32
- # Identifica grupo de exemplo compatível
33
- grupo_exemplo_compat = nil
34
- if exemplos
35
- exemplos.each do |tabela|
36
- cabecalho = tabela.first.gsub('|', '').split.map(&:strip)
37
- if cabecalho.any? { |col| corpo.include?("<#{col}>") }
38
- linhas = tabela[1..].map { |l| l.gsub('|', '').split.map(&:strip) }
39
- grupo_exemplo_compat = { cabecalho: cabecalho, linhas: linhas }
40
- break
41
- end
42
- end
43
- end
9
+ GHERKIN_KEYS_PT = %w[Dado Quando Então E Mas].freeze
10
+ GHERKIN_KEYS_EN = %w[Given When Then And But].freeze
11
+ ALL_KEYS = GHERKIN_KEYS_PT + GHERKIN_KEYS_EN
12
+
13
+ # Converte texto para camelCase (para nomes de argumentos)
14
+ def self.camelize(str)
15
+ parts = str.strip.split(/[^a-zA-Z0-9]+/)
16
+ parts.map.with_index { |w, i| i.zero? ? w.downcase : w.capitalize }.join
17
+ end
44
18
 
45
- # Detecta parâmetros e gera corpo parametrizado
46
- nomes_param = corpo.scan(/<([^>]+)>/).flatten.map(&:strip)
47
- if nomes_param.any?
48
- corpo_param = corpo_sanitizado.dup
49
- nomes_param.each do |nome|
50
- tipo_param = grupo_exemplo_compat ? detectar_tipo_param(nome, grupo_exemplo_compat) : 'string'
51
- corpo_param.gsub!(/<\s*#{Regexp.escape(nome)}\s*>/, "{#{tipo_param}}")
52
- end
53
- args_list = nomes_param.map { |p| p.gsub(/\s+/, '_') }.join(', ')
54
- pending_msg = corpo
19
+ # Gera step definitions a partir de um arquivo .feature
20
+ # - "<nome>" => {string}
21
+ # - <nome> => {int}
22
+ # - "texto" => {string}
23
+ # - numeros inteiros ou floats soltos => {int}
24
+ # Respeita idioma de entrada (pt/en) para keywords geradas
25
+ def self.gerar_passos(feature_path)
26
+ raise ArgumentError, "Caminho esperado como String, recebeu #{feature_path.class}" unless feature_path.is_a?(String)
27
+
28
+ lines = File.readlines(feature_path)
29
+ # Detecta idioma no cabeçalho: "# language: pt" ou "# language: en"
30
+ lang = if (m = lines.find { |l| l =~ /^#\s*language:\s*(\w+)/i })
31
+ m[/^#\s*language:\s*(\w+)/i, 1].downcase
32
+ else
33
+ 'pt'
34
+ end
35
+
36
+ pt_to_en = GHERKIN_KEYS_PT.zip(GHERKIN_KEYS_EN).to_h
37
+ en_to_pt = GHERKIN_KEYS_EN.zip(GHERKIN_KEYS_PT).to_h
38
+
39
+ # Seleciona apenas linhas de passo
40
+ step_lines = lines.map(&:strip).select do |l|
41
+ ALL_KEYS.any? { |k| l.start_with?(k + ' ') }
42
+ end
43
+ return false if step_lines.empty?
44
+
45
+ dir = File.join(File.dirname(feature_path), 'steps')
46
+ FileUtils.mkdir_p(dir)
47
+ file = File.join(dir, "#{File.basename(feature_path, '.feature')}_steps.rb")
48
+
49
+ content = +"# encoding: utf-8\n# Auto-generated step definitions for #{File.basename(feature_path)}\n\n"
50
+
51
+ step_lines.each do |line|
52
+ # Extrai keyword original e resto do passo
53
+ orig_kw, rest = line.split(' ', 2)
54
+ # Converte keyword conforme idioma de entrada
55
+ kw = case lang
56
+ when 'en' then pt_to_en[orig_kw] || orig_kw
57
+ else en_to_pt[orig_kw] || orig_kw
58
+ end
59
+ raw = rest.dup
60
+
61
+ scanner = ::StringScanner.new(rest)
62
+ pattern = ''
63
+ tokens = []
64
+
65
+ until scanner.eos?
66
+ if scanner.check(/"<([^>]+)>"/)
67
+ scanner.scan(/"<([^>]+)>"/)
68
+ tokens << scanner[1]
69
+ pattern << '{string}'
70
+ elsif scanner.check(/<([^>]+)>/)
71
+ scanner.scan(/<([^>]+)>/)
72
+ tokens << scanner[1]
73
+ pattern << '{int}'
74
+ elsif scanner.check(/"([^"<>]+)"/)
75
+ scanner.scan(/"([^"<>]+)"/)
76
+ tokens << scanner[1]
77
+ pattern << '{string}'
78
+ elsif scanner.check(/\d+(?:\.\d+)?/)
79
+ num = scanner.scan(/\d+(?:\.\d+)?/)
80
+ tokens << num
81
+ pattern << '{int}'
55
82
  else
56
- corpo_param = corpo
57
- args_list = ''
58
- pending_msg = corpo
83
+ pattern << scanner.getch
59
84
  end
60
-
61
- passos_gerados << {
62
- conector: conector,
63
- raw: pending_msg,
64
- param: corpo_param,
65
- args: args_list,
66
- tipo: tipo
67
- } unless passos_gerados.any? { |p| p[:param] == corpo_param }
68
85
  end
69
- end
70
86
 
71
- if passos_gerados.empty?
72
- puts "⚠️ Nenhum passo detectado em: #{nome_arquivo_feature} (arquivo não gerado)"
73
- return false
74
- end
75
-
76
- nome_base = File.basename(nome_arquivo_feature, '.feature')
77
-
78
- # Define caminho de saída: prioriza pasta steps dentro de features/<nome>
79
- feature_dir = File.dirname(nome_arquivo_feature)
80
- feature_steps_dir = File.join(feature_dir, 'steps')
81
- if Dir.exist?(feature_steps_dir)
82
- FileUtils.mkdir_p(feature_steps_dir)
83
- caminho = File.join(feature_steps_dir, "#{nome_base}_steps.rb")
84
- else
85
- FileUtils.mkdir_p('steps')
86
- caminho = "steps/#{nome_base}_steps.rb"
87
- end
87
+ # Escapa aspas no padrão final
88
+ safe_pattern = pattern.gsub('"', '\\"')
89
+ signature = "#{kw}(\"#{safe_pattern}\")"
88
90
 
89
- comentario = "# Step definitions para #{File.basename(nome_arquivo_feature)}"
90
- comentario += idioma == 'en' ? " (English)" : " (Português)"
91
- conteudo = "#{comentario}\n\n"
92
-
93
- passos_gerados.each do |passo|
94
- conteudo += <<~STEP
95
- #{passo[:conector]}('#{passo[:param]}') do#{passo[:args].empty? ? '' : " |#{passo[:args]}|"}
96
- pending 'Implementar passo: #{passo[:raw]}'
97
- end
91
+ if tokens.any?
92
+ args = tokens.each_index.map { |i| "args#{i+1}" }.join(', ')
93
+ signature += " do |#{args}|"
94
+ else
95
+ signature += ' do'
96
+ end
98
97
 
99
- STEP
98
+ content << signature << "\n"
99
+ content << " pending 'Implementar passo: #{raw}'\n"
100
+ content << "end\n\n"
100
101
  end
101
102
 
102
- if Bddgenx::Verificador.gerar_arquivo_se_novo(caminho, conteudo)
103
- puts "✅ Step definitions gerados: #{caminho}"
104
- else
105
- puts "⏭️ Steps mantidos: #{caminho}"
106
- end
103
+ File.write(file, content)
104
+ puts "✅ Steps gerados: #{file}"
107
105
  true
108
106
  end
109
-
110
- def self.detectar_tipo_param(nome_coluna, exemplos)
111
- return 'string' unless exemplos[:cabecalho].include?(nome_coluna)
112
-
113
- idx = exemplos[:cabecalho].index(nome_coluna)
114
- valores = exemplos[:linhas].map { |l| l[idx].to_s.strip }
115
-
116
- return 'boolean' if valores.all? { |v| %w[true false].include?(v.downcase) }
117
- return 'int' if valores.all? { |v| v.match?(/^\d+$/) }
118
- return 'float' if valores.all? { |v| v.match?(/^\d+\.\d+$/) }
119
-
120
- 'string'
121
- end
122
-
123
- def self.dividir_examples(tabela_bruta)
124
- grupos = []
125
- grupo = []
126
- tabela_bruta.each do |linha|
127
- if linha.strip =~ /^\|.*\|$/ && grupo.any? && linha.strip == linha.strip.squeeze(' ')
128
- grupos << grupo
129
- grupo = [linha]
130
- else
131
- grupo << linha
132
- end
133
- end
134
- grupos << grupo unless grupo.empty?
135
- grupos
136
- end
137
107
  end
138
- end
108
+ end
@@ -0,0 +1,16 @@
1
+
2
+ module Bddgenx
3
+ # Módulo para inferir o tipo de parâmetro a partir de exemplos
4
+ class TipoParam
5
+ # Retorna 'string', 'int', 'float' ou 'boolean'
6
+ def self.determine_type(nome, estrutura)
7
+ return 'string' unless estrutura[:cabecalho].include?(nome)
8
+ idx = estrutura[:cabecalho].index(nome)
9
+ vals = estrutura[:linhas].map { |l| l[idx] }
10
+ return 'boolean' if vals.all? { |v| %w[true false].include?(v.downcase) }
11
+ return 'int' if vals.all? { |v| v.match?(/^\d+$/) }
12
+ return 'float' if vals.all? { |v| v.match?(/^\d+\.\d+$/) }
13
+ 'string'
14
+ end
15
+ end
16
+ end
data/lib/bddgenx.rb CHANGED
@@ -1,59 +1,3 @@
1
- require_relative 'bddgenx/parser'
2
- require_relative 'bddgenx/validator'
3
- require_relative 'bddgenx/generator'
4
- require_relative 'bddgenx/steps_generator'
5
- require_relative 'bddgenx/tracer'
6
- require_relative 'bddgenx/backup'
7
- require_relative 'bddgenx/cli'
8
- require_relative 'bddgenx/pdf_exporter'
9
- require_relative 'bddgenx/utils/verificador'
1
+ require_relative 'bddgenx/runner'
10
2
 
11
- cont_total = 0
12
- cont_features = 0
13
- cont_steps = 0
14
- cont_ignorados = 0
15
-
16
- # Seleciona arquivos .txt
17
- arquivos = Bddgenx::Cli.selecionar_arquivos_txt('input')
18
- # Antes do loop
19
- skipped_steps = []
20
- skipped_pdfs = []
21
- generated_pdfs = []
22
- arquivos.each do |arquivo_path|
23
- cont_total += 1
24
- puts "\n🔍 Processando: #{arquivo_path}"
25
-
26
- historia = Bddgenx::Parser.ler_historia(arquivo_path)
27
- unless Bddgenx::Validator.validar(historia)
28
- cont_ignorados += 1
29
- puts "❌ Arquivo inválido: #{arquivo_path}"
30
- next
31
- end
32
-
33
- # Gera feature e steps
34
- nome_feature, conteudo_feature = Bddgenx::Generator.gerar_feature(historia)
35
- Bddgenx::Backup.salvar_versao_antiga(nome_feature)
36
- cont_features += 1 if Bddgenx::Generator.salvar_feature(nome_feature, conteudo_feature)
37
- cont_steps += 1 if Bddgenx::StepsGenerator.gerar_passos(historia, nome_feature)
38
-
39
- # Rastreabilidade, PDF
40
- FileUtils.mkdir_p('reports')
41
- # steps
42
- if Bddgenx::StepsGenerator.gerar_passos(historia, nome_feature)
43
- cont_steps += 1
44
- else
45
- skipped_steps << nome_feature
46
- end
47
- # substituir chamada direta por:
48
- results = Bddgenx::PDFExporter.exportar_todos(only_new: true)
49
- # exportar_todos agora fornece [:generated, :skipped]
50
- generated_pdfs.concat(results[:generated])
51
- skipped_pdfs.concat(results[:skipped])
52
- end
53
-
54
- puts "\n✅ Processamento finalizado."
55
- puts "- Features geradas: #{cont_features}"
56
- puts "- Steps gerados: #{cont_steps}"
57
- puts "- Steps mantidos: #{skipped_steps.size}"
58
- puts "- PDFs gerados: #{generated_pdfs.size}"
59
- puts "- PDFs já existentes: #{skipped_pdfs.size}"
3
+ Bddgenx::Runner.execute
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bddgenx
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.32
4
+ version: 0.1.34
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Nascimento
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-05-14 00:00:00.000000000 Z
11
+ date: 2025-05-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: prawn
@@ -24,20 +24,6 @@ dependencies:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: '2.0'
27
- - !ruby/object:Gem::Dependency
28
- name: jira-ruby
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - ">="
32
- - !ruby/object:Gem::Version
33
- version: '2.0'
34
- type: :runtime
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - ">="
39
- - !ruby/object:Gem::Version
40
- version: '2.0'
41
27
  description: Transforma arquivos .txt com histórias em arquivos .feature, com steps,
42
28
  rastreabilidade e integração com CI/CD.
43
29
  email:
@@ -50,6 +36,10 @@ files:
50
36
  - README.md
51
37
  - Rakefile
52
38
  - VERSION
39
+ - assets/fonts/DejaVuSansMono-Bold.ttf
40
+ - assets/fonts/DejaVuSansMono-BoldOblique.ttf
41
+ - assets/fonts/DejaVuSansMono-Oblique.ttf
42
+ - assets/fonts/DejaVuSansMono.ttf
53
43
  - bin/bddgenx
54
44
  - lib/bddgenx.rb
55
45
  - lib/bddgenx/assets/fonts/DejaVuSansMono-Bold.ttf
@@ -57,13 +47,13 @@ files:
57
47
  - lib/bddgenx/assets/fonts/DejaVuSansMono-Oblique.ttf
58
48
  - lib/bddgenx/assets/fonts/DejaVuSansMono.ttf
59
49
  - lib/bddgenx/backup.rb
60
- - lib/bddgenx/cli.rb
61
50
  - lib/bddgenx/generator.rb
62
51
  - lib/bddgenx/parser.rb
63
52
  - lib/bddgenx/pdf_exporter.rb
53
+ - lib/bddgenx/runner.rb
64
54
  - lib/bddgenx/steps_generator.rb
65
55
  - lib/bddgenx/tracer.rb
66
- - lib/bddgenx/utils/verificador.rb
56
+ - lib/bddgenx/utils/tipo_param.rb
67
57
  - lib/bddgenx/validator.rb
68
58
  - lib/bddgenx/version.rb
69
59
  homepage: https://github.com/David-Nascimento/bdd-generation
data/lib/bddgenx/cli.rb DELETED
@@ -1,48 +0,0 @@
1
- module Bddgenx
2
- class Cli
3
- def self.confirm(message)
4
- print "#{message} "
5
- answer = $stdin.gets.to_s.strip.downcase
6
- %w[s sim y yes].include?(answer)
7
- end
8
-
9
- # Exibe uma mensagem de pergunta e retorna a string digitada pelo usuário
10
- def self.ask(message)
11
- print "#{message} "
12
- $stdin.gets.to_s.strip
13
- end
14
-
15
- def self.selecionar_arquivos_txt(diretorio)
16
- arquivos = Dir.glob("#{diretorio}/*.txt")
17
-
18
- if arquivos.empty?
19
- puts "❌ Nenhum arquivo .txt encontrado no diretório '#{diretorio}'"
20
- exit
21
- end
22
-
23
- arquivos
24
-
25
- puts "📂 Arquivos disponíveis em '#{diretorio}':"
26
- arquivos.each_with_index do |arquivo, i|
27
- puts " #{i + 1}. #{File.basename(arquivo)}"
28
- end
29
-
30
- print "\nDigite os números dos arquivos que deseja processar (ex: 1,2,3 ou 'todos'): "
31
- entrada = gets.chomp
32
-
33
- selecionados = if entrada.downcase == 'todos'
34
- arquivos
35
- else
36
- indices = entrada.split(',').map { |n| n.strip.to_i - 1 }
37
- indices.map { |i| arquivos[i] }.compact
38
- end
39
-
40
- if selecionados.empty?
41
- puts "❌ Nenhum arquivo válido selecionado."
42
- exit
43
- end
44
-
45
- selecionados
46
- end
47
- end
48
- end
@@ -1,20 +0,0 @@
1
- require 'fileutils'
2
-
3
- module Bddgenx
4
- module Verificador
5
- # Impede sobrescrita de arquivos existentes
6
- def self.gerar_arquivo_se_novo(caminho, novo_conteudo)
7
- if File.exist?(caminho)
8
- conteudo_atual = File.read(caminho, encoding: 'utf-8').strip
9
- return false if conteudo_atual == novo_conteudo.strip
10
-
11
- puts "⚠️ Arquivo já existe: #{caminho} — não será sobrescrito."
12
- return false
13
- end
14
-
15
- FileUtils.mkdir_p(File.dirname(caminho))
16
- File.write(caminho, novo_conteudo)
17
- true
18
- end
19
- end
20
- end