bddgenx 0.1.49 → 1.0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 230aa1f7581925ef006f2a3a433edc0aafdd3054839ebaef6b0f1da0118b4753
4
- data.tar.gz: 4107e5a54d49b50db2b705b78dcc0384d4469fa875d35cc781113fc70880d15f
3
+ metadata.gz: 691d4d259476c68894a8859cc74ab4d926fdfe3b7304f5a020f05843442f33d2
4
+ data.tar.gz: 0aeff404aee94ac4d8419dec2772379a0816fdb5ce69a1e8cf967d47499e2f48
5
5
  SHA512:
6
- metadata.gz: d99c0c4be91f5ff9f78c4d4cea0280effff83cd0c25db1ac408a99e5f439fdd70a59c06b6d4b5fa0c25eb2ec8a681e86c085438535f99ec51ee4032a2e2ba000
7
- data.tar.gz: dde6912e1e6a73b64a447d1516e5f79e84d75120083e0a79460e4acec59c7f3a6dc6b24da9f744c368db9993cb77f8ae951b8dea761121b4b64bb222455f992f
6
+ metadata.gz: c08188d0e944723b8e109063d2c2e682e335be6c05ff731f51962daba3a3dccac072201107fb23dda9f0d0b3901f75b97a10e1bcb289cf3305ad6deab8ccc198
7
+ data.tar.gz: fd8bf60a0b13bb05e63486daa3b4ccead3ddfdc4653046e5f48b252eede5cb8af35498d0d259a23870c203a5912a63a1c70cfc01fdd9b1e5141e58bc56d4c986
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.49
1
+ 1.0.0
@@ -7,67 +7,68 @@
7
7
  # e esquemas de cenário com exemplos.
8
8
 
9
9
  require 'fileutils'
10
+ require 'set'
11
+ require_relative 'utils/gherkin_cleaner'
12
+ require_relative 'ia/gemini_cliente'
13
+ require_relative 'utils/remover_steps_duplicados'
14
+ # require_relative 'clients/chatgpt_cliente'
10
15
 
11
16
  module Bddgenx
12
- # Gera cenários e arquivos .feature baseados em histórias e grupos de passos.
13
17
  class Generator
14
- # Palavras-chave Gherkin em Português
15
- # @return [Array<String>]
16
18
  GHERKIN_KEYS_PT = %w[Dado Quando Então E Mas].freeze
17
-
18
- # Palavras-chave Gherkin em Inglês
19
- # @return [Array<String>]
20
19
  GHERKIN_KEYS_EN = %w[Given When Then And But].freeze
21
-
22
- # Mapeamento PT -> EN
23
- # @return [Hash{String=>String}]
24
20
  GHERKIN_MAP_PT_EN = GHERKIN_KEYS_PT.zip(GHERKIN_KEYS_EN).to_h
25
-
26
- # Mapeamento EN -> PT
27
- # @return [Hash{String=>String}]
28
21
  GHERKIN_MAP_EN_PT = GHERKIN_KEYS_EN.zip(GHERKIN_KEYS_PT).to_h
29
-
30
- # Conjunto de todas as palavras-chave suportadas (PT + EN)
31
- # @return [Array<String>]
32
22
  ALL_KEYS = GHERKIN_KEYS_PT + GHERKIN_KEYS_EN
33
23
 
34
- # Seleciona apenas as linhas que representam exemplos do cenário
35
- #
36
- # @param raw [Array<String>] Lista de linhas brutas do bloco de exemplos
37
- # @return [Array<String>] Linhas que começam com '|' representando a tabela de exemplos
38
24
  def self.dividir_examples(raw)
39
25
  raw.select { |l| l.strip.start_with?('|') }
40
26
  end
41
27
 
42
- # Gera conteúdo de um arquivo .feature a partir de um hash de história ou caminho para arquivo
43
- #
44
- # @param input [Hash, String]
45
- # Objeto de história com chaves :idioma, :quero, :como, :para, :grupos
46
- # Ou caminho para um arquivo de história que será lido via Parser.ler_historia
47
- # @param override_path [String, nil]
48
- # Caminho de saída alternativo para o arquivo .feature
49
- # @raise [ArgumentError] Se Parser.ler_historia lançar erro ao ler arquivo
50
- # @return [Array(String, String)] Array com caminho e conteúdo gerado
51
28
  def self.gerar_feature(input, override_path = nil)
52
- historia = input.is_a?(String) ? Parser.ler_historia(input) : input
53
- idioma = historia[:idioma] || 'pt'
29
+ modo = ENV['BDD_MODE']&.to_sym || :static
30
+
31
+ # IA: Lê o arquivo txt e passa para IA → aplica limpeza
32
+ if input.is_a?(String) && input.end_with?('.txt') && [:gemini, :chatgpt].include?(modo)
33
+ raw_txt = File.read(input)
34
+ historia = {
35
+ idioma: 'pt',
36
+ quero: File.basename(input, '.txt').tr('_', ' ').capitalize,
37
+ como: '',
38
+ para: '',
39
+ grupos: []
40
+ }
41
+
42
+ texto_gerado = if modo == :gemini
43
+ GeminiCliente.gerar_cenarios(raw_txt)
44
+ else
45
+ ChatGPTCliente.gerar_cenarios(raw_txt)
46
+ end
47
+
48
+ historia[:grupos] << {
49
+ tipo: 'gerado',
50
+ tag: 'ia',
51
+ passos: GherkinCleaner.limpar(texto_gerado).lines.map(&:strip).reject(&:empty?)
52
+ }
53
+
54
+ else
55
+ # Caminho tradicional (hash de entrada ou parser de arquivo)
56
+ historia = input.is_a?(String) ? Parser.ler_historia(input) : input
57
+ end
58
+
59
+ idioma = historia[:idioma] || 'pt'
54
60
  cont = 1
55
61
 
56
- # Geração do nome base do arquivo
57
62
  nome_base = historia[:quero]
58
63
  .gsub(/[^a-z0-9]/i, '_')
59
64
  .downcase
60
- .split('_', 3)
61
- .first(3)
65
+ .split('_')
66
+ .reject(&:empty?)
67
+ .first(5)
62
68
  .join('_')
63
69
 
64
- caminho = if override_path.is_a?(String)
65
- override_path
66
- else
67
- "features/#{nome_base}.feature"
68
- end
70
+ caminho = override_path || "features/#{nome_base}.feature"
69
71
 
70
- # Definição das palavras-chave Gherkin conforme idioma
71
72
  palavras = {
72
73
  feature: idioma == 'en' ? 'Feature' : 'Funcionalidade',
73
74
  contexto: idioma == 'en' ? 'Background' : 'Contexto',
@@ -77,75 +78,70 @@ module Bddgenx
77
78
  regra: idioma == 'en' ? 'Rule' : 'Regra'
78
79
  }
79
80
 
80
- # Cabeçalho do arquivo .feature
81
81
  conteudo = <<~GHK
82
- # language: #{idioma}
83
- #{palavras[:feature]}: #{historia[:quero].sub(/^Quero\s*/i,'')}
84
- # #{historia[:como]}
85
- # #{historia[:quero]}
86
- # #{historia[:para]}
82
+ # language: #{idioma}
83
+ #{palavras[:feature]}: #{historia[:quero].sub(/^Quero\s*/i,'')}
84
+ # #{historia[:como]}
85
+ # #{historia[:quero]}
86
+ # #{historia[:para]}
87
87
 
88
88
  GHK
89
89
 
90
- pt_map = GHERKIN_MAP_PT_EN
91
- en_map = GHERKIN_MAP_EN_PT
92
- detect = ALL_KEYS
90
+ # Set para rastrear steps únicos
91
+ passos_unicos = Set.new
92
+ pt_map = GHERKIN_MAP_PT_EN
93
+ en_map = GHERKIN_MAP_EN_PT
94
+ detect = ALL_KEYS
95
+ cont = 1
93
96
 
94
97
  historia[:grupos].each do |grupo|
95
98
  passos = grupo[:passos] || []
96
99
  exemplos = grupo[:exemplos] || []
97
100
  next if passos.empty?
98
101
 
99
- # Linha de tags para o grupo
100
- tag_line = ["@#{grupo[:tipo].downcase}",
101
- ("@#{grupo[:tag]}" if grupo[:tag])]
102
- .compact.join(' ')
102
+ tag_line = ["@#{grupo[:tipo].downcase}", ("@#{grupo[:tag]}" if grupo[:tag])].compact.join(' ')
103
+
104
+ conteudo << " #{tag_line}\n"
103
105
 
104
106
  if exemplos.any?
105
- # Cenário com Esquema
106
- conteudo << " #{tag_line}\n"
107
- conteudo << " #{palavras[:esquema]}: Exemplo #{cont} \n"
107
+ conteudo << " #{palavras[:esquema]}: Exemplo #{cont}\n"
108
108
  cont += 1
109
+ else
110
+ conteudo << " #{palavras[:cenario]}: #{grupo[:tipo].capitalize}\n"
111
+ end
109
112
 
110
- # Passos do cenário
111
- passos.each do |p|
112
- parts = p.strip.split(' ', 2)
113
- con_in = detect.find { |k| k.casecmp(parts[0]) == 0 } || parts[0]
114
- text = parts[1] || ''
115
- out_conn = idioma == 'en' ? pt_map[con_in] || con_in : en_map[con_in] || con_in
116
- conteudo << " #{out_conn} #{text}\n"
117
- end
113
+ passos.each do |p|
114
+ parts = p.strip.split(' ', 2)
115
+ con_in = detect.find { |k| k.casecmp(parts[0]) == 0 } || parts[0]
116
+ text = parts[1] || ''
117
+ out_conn = idioma == 'en' ? pt_map[con_in] || con_in : en_map[con_in] || con_in
118
+ linha_step = " #{out_conn} #{text}"
119
+
120
+ next if passos_unicos.include?(linha_step)
121
+
122
+ passos_unicos << linha_step
123
+ conteudo << "#{linha_step}\n"
124
+ end
118
125
 
119
- # Bloco de exemplos original
126
+ if exemplos.any?
120
127
  conteudo << "\n #{palavras[:exemplos]}:\n"
121
128
  exemplos.select { |l| l.strip.start_with?('|') }.each do |line|
122
129
  cleaned = line.strip.gsub(/^"|"$/, '')
123
130
  conteudo << " #{cleaned}\n"
124
131
  end
125
- conteudo << "\n"
126
- else
127
- # Cenário simples
128
- conteudo << " #{tag_line}\n"
129
- conteudo << " #{palavras[:cenario]}: #{grupo[:tipo].capitalize}\n"
130
- passos.each do |p|
131
- parts = p.strip.split(' ', 2)
132
- con_in = detect.find { |k| k.casecmp(parts[0]) == 0 } || parts[0]
133
- text = parts[1] || ''
134
- out_conn = idioma == 'en' ? pt_map[con_in] || con_in : en_map[con_in] || con_in
135
- conteudo << " #{out_conn} #{text}\n"
136
- end
137
- conteudo << "\n"
138
132
  end
133
+
134
+ conteudo << "\n"
139
135
  end
140
136
 
141
137
  [caminho, conteudo]
142
138
  end
143
139
 
144
- # Salva o conteúdo gerado em arquivo .feature no disco
145
- #
146
- # @param caminho [String] Caminho completo para salvar o arquivo
147
- # @param conteudo [String] Conteúdo do arquivo .feature
148
- # @return [nil]
140
+ def self.path_para_feature(arquivo_txt)
141
+ nome = File.basename(arquivo_txt, '.txt')
142
+ File.join('features', "#{nome}.feature")
143
+ end
144
+
149
145
  def self.salvar_feature(caminho, conteudo)
150
146
  FileUtils.mkdir_p(File.dirname(caminho))
151
147
  File.write(caminho, conteudo)
@@ -0,0 +1,120 @@
1
+ # lib/bddgenx/ia/gemini_cliente.rb
2
+ require 'net/http'
3
+ require 'json'
4
+ require 'uri'
5
+ require_relative '../utils/gherkin_cleaner'
6
+ require_relative '../utils/remover_steps_duplicados'
7
+
8
+ module Bddgenx
9
+ module IA
10
+ class GeminiCliente
11
+ GEMINI_API_URL = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent'.freeze
12
+
13
+ def self.gerar_cenarios(historia, idioma = 'pt')
14
+ api_key = ENV['GEMINI_API_KEY']
15
+
16
+ keywords_pt = {
17
+ feature: "Funcionalidade",
18
+ scenario: "Cenário",
19
+ scenario_outline: "Esquema do Cenário",
20
+ examples: "Exemplos",
21
+ given: "Dado",
22
+ when: "Quando",
23
+ then: "Então",
24
+ and: "E"
25
+ }
26
+
27
+ keywords_en = {
28
+ feature: "Feature",
29
+ scenario: "Scenario",
30
+ scenario_outline: "Scenario Outline",
31
+ examples: "Examples",
32
+ given: "Given",
33
+ when: "When",
34
+ then: "Then",
35
+ and: "And"
36
+ }
37
+
38
+ keywords = idioma == 'en' ? keywords_en : keywords_pt
39
+
40
+ # Prompt base para solicitar saída Gherkin estruturada da IA
41
+ prompt_base = <<~PROMPT
42
+ Gere cenários BDD no formato Gherkin, usando as palavras-chave de estrutura no idioma "#{idioma}":
43
+ Feature: #{keywords[:feature]}
44
+ Scenario: #{keywords[:scenario]}
45
+ Scenario Outline: #{keywords[:scenario_outline]}
46
+ Examples: #{keywords[:examples]}
47
+ Given: #{keywords[:given]}
48
+ When: #{keywords[:when]}
49
+ Then: #{keywords[:then]}
50
+ And: #{keywords[:and]}
51
+
52
+ Atenção: Os textos e descrições dos cenários e passos devem ser escritos em português, mesmo que as palavras-chave estejam em inglês.
53
+
54
+ História:
55
+ #{historia}
56
+ PROMPT
57
+ unless api_key
58
+ warn "❌ API Key do Gemini não encontrada no .env (GEMINI_API_KEY)"
59
+ return nil
60
+ end
61
+
62
+ uri = URI("#{GEMINI_API_URL}?key=#{api_key}")
63
+ prompt = prompt_base % { historia: historia }
64
+
65
+ request_body = {
66
+ contents: [
67
+ {
68
+ role: "user",
69
+ parts: [{ text: prompt }]
70
+ }
71
+ ]
72
+ }
73
+
74
+ response = Net::HTTP.post(uri, request_body.to_json, { "Content-Type" => "application/json" })
75
+
76
+ if response.is_a?(Net::HTTPSuccess)
77
+ json = JSON.parse(response.body)
78
+
79
+ unless json["candidates"]&.is_a?(Array) && json["candidates"].any?
80
+ warn "❌ Resposta da IA sem candidatos válidos:"
81
+ warn JSON.pretty_generate(json)
82
+ return nil
83
+ end
84
+
85
+ texto_ia = json["candidates"].first.dig("content", "parts", 0, "text")
86
+ if texto_ia
87
+ # Sanitiza o texto para garantir formato Gherkin correto
88
+ texto_limpo = Bddgenx::GherkinCleaner.limpar(texto_ia)
89
+ Utils::StepCleaner.remover_steps_duplicados(texto_ia, idioma)
90
+
91
+ # Insere a diretiva language dinamicamente com base no idioma detectado
92
+ texto_limpo.sub!(/^# language: .*/, "# language: #{idioma}")
93
+ texto_limpo.prepend("# language: #{idioma}\n") unless texto_limpo.start_with?("# language:")
94
+
95
+ return texto_limpo
96
+ else
97
+ warn "❌ Resposta da IA sem conteúdo de texto"
98
+ warn JSON.pretty_generate(json)
99
+ return nil
100
+ end
101
+ else
102
+ warn "❌ Erro ao chamar Gemini: #{response.code} - #{response.body}"
103
+ return nil
104
+ end
105
+ end
106
+
107
+ def self.detecta_idioma_arquivo(caminho_arquivo)
108
+ return 'pt' unless File.exist?(caminho_arquivo)
109
+
110
+ File.foreach(caminho_arquivo) do |linha|
111
+ if linha =~ /^#\s*language:\s*(\w{2})/i
112
+ return $1.downcase
113
+ end
114
+ end
115
+
116
+ 'pt' # padrão
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,127 @@
1
+ # lib/bddgenx/cli.rb
2
+ # encoding: utf-8
3
+ #
4
+ # Este arquivo define a classe Runner (CLI) da gem bddgenx,
5
+ # responsável por orquestrar o fluxo de leitura de histórias,
6
+ # validação, geração de features, steps, backups e exportação de PDFs.
7
+ require 'dotenv/load'
8
+ require 'fileutils'
9
+ require_relative 'utils/parser'
10
+ require_relative 'generator'
11
+ require_relative 'utils/pdf_exporter'
12
+ require_relative 'steps_generator'
13
+ require_relative 'utils/validator'
14
+ require_relative 'utils/backup'
15
+ require_relative 'ia/gemini_cliente'
16
+ require_relative 'utils/gherkin_cleaner'
17
+ require_relative 'gemini_generator'
18
+
19
+ module Bddgenx
20
+ # Ponto de entrada da gem: coordena todo o processo de geração BDD.
21
+ class Runner
22
+ def self.choose_files(input_dir)
23
+ ARGV.any? ? selecionar_arquivos_txt(input_dir) : choose_input(input_dir)
24
+ end
25
+
26
+ def self.selecionar_arquivos_txt(input_dir)
27
+ ARGV.map do |arg|
28
+ nome = arg.end_with?('.txt') ? arg : "#{arg}.txt"
29
+ path = File.join(input_dir, nome)
30
+ unless File.exist?(path)
31
+ warn "⚠️ Arquivo não encontrado: #{path}"
32
+ next
33
+ end
34
+ path
35
+ end.compact
36
+ end
37
+
38
+ def self.choose_input(input_dir)
39
+ files = Dir.glob(File.join(input_dir, '*.txt'))
40
+ if files.empty?
41
+ warn "❌ Não há arquivos .txt no diretório #{input_dir}"; exit 1
42
+ end
43
+
44
+ puts "Selecione o arquivo de história para processar:"
45
+ files.each_with_index { |f, i| puts "#{i+1}. #{File.basename(f)}" }
46
+ print "Digite o número correspondente (ou ENTER para todos): "
47
+ choice = STDIN.gets.chomp
48
+
49
+ return files if choice.empty?
50
+ idx = choice.to_i - 1
51
+ unless idx.between?(0, files.size - 1)
52
+ warn "❌ Escolha inválida."; exit 1
53
+ end
54
+ [files[idx]]
55
+ end
56
+
57
+ def self.execute
58
+ modo = ENV['BDDGENX_MODE'] || 'static'
59
+
60
+ input_dir = 'input'
61
+ Dir.mkdir(input_dir) unless Dir.exist?(input_dir)
62
+
63
+ arquivos = choose_files(input_dir)
64
+ if arquivos.empty?
65
+ warn "❌ Nenhum arquivo de história para processar."; exit 1
66
+ end
67
+
68
+ total = features = steps = ignored = 0
69
+ skipped_steps = []
70
+ generated_pdfs = []
71
+ skipped_pdfs = []
72
+
73
+ arquivos.each do |arquivo|
74
+ total += 1
75
+ puts "\n🔍 Processando: #{arquivo}"
76
+
77
+ historia =
78
+ if modo == 'gemini'
79
+ puts "🤖 Gerando cenários com IA (Gemini)..."
80
+ begin
81
+ idioma = GeminiCliente.detecta_idioma_arquivo(arquivo)
82
+ historia = File.read(arquivo)
83
+ GeminiCliente.gerar_cenarios(historia, idioma)
84
+ rescue => e
85
+ ignored += 1
86
+ puts "❌ Falha ao gerar com Gemini: #{e.message}"
87
+ next
88
+ end
89
+ else
90
+ historia = Parser.ler_historia(arquivo)
91
+ unless Validator.validar(historia)
92
+ ignored += 1
93
+ puts "❌ História inválida: #{arquivo}"
94
+ next
95
+ end
96
+ historia
97
+ end
98
+
99
+ historia_limpa = GherkinCleaner.limpar(historia_ia_gerada)
100
+ feature_path, feature_content = Generator.gerar_feature(historia_limpa)
101
+
102
+ Backup.salvar_versao_antiga(feature_path)
103
+ features += 1 if Generator.salvar_feature(feature_path, feature_content)
104
+
105
+ if StepsGenerator.gerar_passos(feature_path)
106
+ steps += 1
107
+ else
108
+ skipped_steps << feature_path
109
+ end
110
+
111
+ FileUtils.mkdir_p('reports')
112
+ result = PDFExporter.exportar_todos(only_new: true)
113
+ generated_pdfs.concat(result[:generated])
114
+ skipped_pdfs.concat(result[:skipped])
115
+ end
116
+
117
+ puts "\n✅ Processamento concluído"
118
+ puts "- Total de histórias: #{total}"
119
+ puts "- Features geradas: #{features}"
120
+ puts "- Steps gerados: #{steps}"
121
+ puts "- Steps ignorados: #{skipped_steps.size}"
122
+ puts "- PDFs gerados: #{generated_pdfs.size}"
123
+ puts "- PDFs já existentes: #{skipped_pdfs.size}"
124
+ puts "- Histórias ignoradas: #{ignored}"
125
+ end
126
+ end
127
+ end
@@ -4,7 +4,7 @@
4
4
  # Este arquivo define a classe Runner (CLI) da gem bddgenx,
5
5
  # responsável por orquestrar o fluxo de leitura de histórias,
6
6
  # validação, geração de features, steps, backups e exportação de PDFs.
7
-
7
+ require 'dotenv/load'
8
8
  require 'fileutils'
9
9
  require_relative 'utils/parser'
10
10
  require_relative 'generator'
@@ -12,6 +12,8 @@ require_relative 'utils/pdf_exporter'
12
12
  require_relative 'steps_generator'
13
13
  require_relative 'utils/validator'
14
14
  require_relative 'utils/backup'
15
+ require_relative 'ia/gemini_cliente'
16
+ require_relative 'utils/gherkin_cleaner'
15
17
 
16
18
  module Bddgenx
17
19
  # Ponto de entrada da gem: coordena todo o processo de geração BDD.
@@ -80,6 +82,8 @@ module Bddgenx
80
82
  #
81
83
  # @return [void]
82
84
  def self.execute
85
+ modo = ENV['BDDGENX_MODE'] || 'static'
86
+
83
87
  input_dir = 'input'
84
88
  Dir.mkdir(input_dir) unless Dir.exist?(input_dir)
85
89
 
@@ -106,7 +110,38 @@ module Bddgenx
106
110
  end
107
111
 
108
112
  # Geração de feature
109
- feature_path, feature_content = Generator.gerar_feature(historia)
113
+ if modo == 'gemini'
114
+ puts "🤖 Gerando cenários com IA (Gemini)..."
115
+ idioma = IA::GeminiCliente.detecta_idioma_arquivo(arquivo) # Seu método que detecta o idioma no .txt (ex: 'pt' ou 'en')
116
+ spinner = Thread.new do
117
+ loop do
118
+ print "\r⏳ Aguardando resposta da IA "
119
+ 3.times do |i|
120
+ print "." * (i + 1)
121
+ sleep(0.4)
122
+ print "\r⏳ Aguardando resposta da IA#{'.' * (i + 1)} "
123
+ end
124
+ end
125
+ end
126
+ begin
127
+ feature_text = IA::GeminiCliente.gerar_cenarios(historia, idioma)
128
+ ensure
129
+ Thread.kill(spinner)
130
+ print "\r✅ Resposta da IA recebida! \n"
131
+ end
132
+ # feature_text = IA::GeminiCliente.gerar_cenarios(historia, idioma)
133
+ if feature_text
134
+ feature_path = Generator.path_para_feature(arquivo)
135
+ feature_content = Bddgenx::GherkinCleaner.limpar(feature_text)
136
+ else
137
+ ignored += 1
138
+ puts "❌ Falha ao gerar com IA: #{arquivo}"
139
+ next
140
+ end
141
+ else
142
+ feature_path, feature_content = Generator.gerar_feature(historia)
143
+ end
144
+
110
145
  Backup.salvar_versao_antiga(feature_path)
111
146
  features += 1 if Generator.salvar_feature(feature_path, feature_content)
112
147
 
@@ -8,80 +8,54 @@
8
8
 
9
9
  require 'fileutils'
10
10
  require 'strscan' # Para uso de StringScanner
11
+ require 'set'
11
12
 
12
13
  module Bddgenx
13
- # Gera arquivos de definições de passos Ruby para Cucumber
14
- # com base em arquivos .feature.
15
14
  class StepsGenerator
16
- # Palavras-chave Gherkin em Português usadas em arquivos de feature
17
- # @return [Array<String>]
18
15
  GHERKIN_KEYS_PT = %w[Dado Quando Então E Mas].freeze
19
-
20
- # Palavras-chave Gherkin em Inglês usadas em arquivos de feature
21
- # @return [Array<String>]
22
16
  GHERKIN_KEYS_EN = %w[Given When Then And But].freeze
23
-
24
- # Conjunto de todas as palavras-chave suportadas (PT + EN)
25
- # @return [Array<String>]
26
17
  ALL_KEYS = GHERKIN_KEYS_PT + GHERKIN_KEYS_EN
27
18
 
28
- # Converte uma string para camelCase, útil para nomes de argumentos
29
- #
30
- # @param [String] str Texto de entrada a ser convertido
31
- # @return [String] Versão em camelCase do texto
32
19
  def self.camelize(str)
33
20
  partes = str.strip.split(/[^a-zA-Z0-9]+/)
34
21
  partes.map.with_index { |palavra, i| i.zero? ? palavra.downcase : palavra.capitalize }.join
35
22
  end
36
23
 
37
- # Gera definições de passos a partir de um arquivo .feature
38
- #
39
- # @param [String] feature_path Caminho para o arquivo .feature
40
- # @raise [ArgumentError] Se feature_path não for String
41
- # @return [Boolean] Retorna true se passos foram gerados, false se não houver passos
42
24
  def self.gerar_passos(feature_path)
43
- # Valida tipo de entrada
44
- unless feature_path.is_a?(String)
45
- raise ArgumentError, "Caminho esperado como String, recebeu #{feature_path.class}"
46
- end
25
+ raise ArgumentError, "Caminho esperado como String, recebeu #{feature_path.class}" unless feature_path.is_a?(String)
47
26
 
48
27
  linhas = File.readlines(feature_path)
49
28
 
50
- # Detecta idioma no cabeçalho: "# language: pt" ou "# language: en"
51
29
  lang = if (m = linhas.find { |l| l =~ /^#\s*language:\s*(\w+)/i })
52
30
  m[/^#\s*language:\s*(\w+)/i, 1].downcase
53
31
  else
54
32
  'pt'
55
33
  end
56
34
 
57
- # Mapas de tradução entre PT e EN
58
35
  pt_para_en = GHERKIN_KEYS_PT.zip(GHERKIN_KEYS_EN).to_h
59
36
  en_para_pt = GHERKIN_KEYS_EN.zip(GHERKIN_KEYS_PT).to_h
60
37
 
61
- # Seleciona linhas que começam com palavras-chave Gherkin
62
38
  linhas_passos = linhas.map(&:strip).select do |linha|
63
39
  ALL_KEYS.any? { |chave| linha.start_with?(chave + ' ') }
64
40
  end
65
41
 
66
- # Se não encontrar passos, retorna false
67
42
  return false if linhas_passos.empty?
68
43
 
69
- # Cria diretório e arquivo de saída
70
44
  dir_saida = File.join(File.dirname(feature_path), 'steps')
71
45
  FileUtils.mkdir_p(dir_saida)
72
46
  arquivo_saida = File.join(dir_saida, "#{File.basename(feature_path, '.feature')}_steps.rb")
73
47
 
74
- # Cabeçalho do arquivo gerado
75
48
  conteudo = +"# encoding: utf-8\n"
76
49
  conteudo << "# Definições de passos geradas automaticamente para #{File.basename(feature_path)}\n\n"
77
50
 
51
+ passos_unicos = Set.new
52
+
78
53
  linhas_passos.each do |linha|
79
54
  palavra_original, restante = linha.split(' ', 2)
80
55
 
81
- # Define palavra-chave no idioma de saída
82
56
  chave = case lang
83
57
  when 'en' then pt_para_en[palavra_original] || palavra_original
84
- else en_para_pt[palavra_original] || palavra_original
58
+ else en_para_pt[palavra_original] || palavra_original
85
59
  end
86
60
 
87
61
  texto_bruto = restante.dup
@@ -111,11 +85,14 @@ module Bddgenx
111
85
  end
112
86
  end
113
87
 
114
- # Escapa aspas no padrão
115
88
  padrao_seguro = padrao.gsub('"', '\\"')
116
- assinatura = "#{chave}(\"#{padrao_seguro}\")"
117
89
 
118
- # Adiciona parâmetros se existirem tokens
90
+ # Impede duplicatas
91
+ next if passos_unicos.include?(padrao_seguro)
92
+
93
+ passos_unicos << padrao_seguro
94
+
95
+ assinatura = "#{chave}(\"#{padrao_seguro}\")"
119
96
  if tokens.any?
120
97
  argumentos = tokens.each_index.map { |i| "arg#{i+1}" }.join(', ')
121
98
  assinatura << " do |#{argumentos}|"
@@ -128,7 +105,6 @@ module Bddgenx
128
105
  conteudo << "end\n\n"
129
106
  end
130
107
 
131
- # Escreve arquivo de saída
132
108
  File.write(arquivo_saida, conteudo)
133
109
  puts "✅ Steps gerados: #{arquivo_saida}"
134
110
  true
@@ -0,0 +1,57 @@
1
+ require 'set'
2
+ require 'unicode'
3
+
4
+ module Bddgenx
5
+ class GherkinCleaner
6
+ def self.limpar(texto)
7
+ texto = remover_blocos_markdown(texto)
8
+ texto = corrigir_language(texto)
9
+ texto = corrigir_indentacao(texto)
10
+ texto.strip
11
+ end
12
+
13
+ def self.remover_blocos_markdown(texto)
14
+ texto.gsub(/```[a-z]*\n?/i, '').gsub(/```/, '')
15
+ end
16
+
17
+ def self.corrigir_language(texto)
18
+ linhas = texto.lines
19
+ primeira_language = linhas.find { |linha| linha.strip.start_with?('# language:') }
20
+
21
+ # Remove duplicações de # language
22
+ linhas.reject! { |linha| linha.strip.start_with?('# language:') }
23
+
24
+ if primeira_language
25
+ linhas.unshift(primeira_language.strip + "\n")
26
+ else
27
+ idioma = detectar_idioma(linhas.join)
28
+ linhas.unshift("# language: #{idioma}\n")
29
+ end
30
+
31
+ linhas.join
32
+ end
33
+
34
+ def self.detectar_idioma(texto)
35
+ return 'pt' if texto =~ /Dado|Quando|Então|E /i
36
+ return 'en' if texto =~ /Given|When|Then|And /i
37
+ 'pt' # padrão
38
+ end
39
+
40
+ def self.corrigir_indentacao(texto)
41
+ linhas = texto.lines.map do |linha|
42
+ if linha.strip.start_with?('Feature', 'Funcionalidade')
43
+ linha.strip + "\n"
44
+ elsif linha.strip.start_with?('Scenario', 'Cenário', 'Scenario Outline', 'Esquema do Cenário')
45
+ " #{linha.strip}\n"
46
+ elsif linha.strip.start_with?('Given', 'When', 'Then', 'And', 'Dado', 'Quando', 'Então', 'E')
47
+ " #{linha.strip}\n"
48
+ elsif linha.strip.start_with?('|')
49
+ " #{linha.strip}\n"
50
+ else
51
+ " #{linha.strip}\n"
52
+ end
53
+ end
54
+ linhas.join
55
+ end
56
+ end
57
+ end
@@ -5,6 +5,7 @@
5
5
  # arquivos de história (.txt), extraindo cabeçalho e blocos de passos e exemplos.
6
6
  # Utiliza constantes para identificação de tipos de blocos e suporta idiomas
7
7
  # Português e Inglês na marcação de idioma e blocos de exemplos.
8
+ require_relative '../ia/gemini_cliente'
8
9
 
9
10
  module Bddgenx
10
11
  # Tipos de blocos reconhecidos na história (.txt), incluindo variações em Português
@@ -42,10 +43,13 @@ module Bddgenx
42
43
  def self.ler_historia(caminho_arquivo)
43
44
  # Carrega linhas do arquivo, preservando linhas vazias e encoding UTF-8
44
45
  linhas = File.readlines(caminho_arquivo, encoding: 'utf-8').map(&:rstrip)
46
+ # parser.rb (durante leitura do .txt)
47
+ primeira_linha = File.readlines(caminho_arquivo).first
48
+ idioma_forcado = primeira_linha&.match(/^#\s*lang(?:uage)?\s*:\s*(\w+)/i)&.captures&.first
45
49
 
46
50
  # Detecta idioma no topo do arquivo: suporta '# lang: <codigo>' ou '# language: <codigo>'
47
51
  idioma = 'pt'
48
- if linhas.first =~ /^#\s*lang(?:uage)?\s*:\s*(\w+)/i
52
+ if linhas.first == idioma_forcado
49
53
  idioma = Regexp.last_match(1).downcase
50
54
  linhas.shift
51
55
  end
@@ -9,7 +9,7 @@
9
9
  require 'prawn'
10
10
  require 'prawn/table'
11
11
  require 'fileutils'
12
- require_relative 'fontLoader'
12
+ require_relative 'font_loader'
13
13
 
14
14
  # Suprime aviso de internacionalização para fontes AFM internas
15
15
  Prawn::Fonts::AFM.hide_m17n_warning = true
@@ -0,0 +1,44 @@
1
+ require 'set'
2
+ require 'unicode'
3
+
4
+ module Bddgenx
5
+ module Utils
6
+ class StepCleaner
7
+ def self.remover_steps_duplicados(texto, idioma)
8
+ keywords = idioma == 'en' ? %w[Given When Then And] : %w[Dado Quando Então E]
9
+ seen = Set.new
10
+ resultado = []
11
+
12
+ texto.each_line do |linha|
13
+ if keywords.any? { |kw| linha.strip.start_with?(kw) }
14
+ canonical = canonicalize_step(linha, keywords)
15
+ unless seen.include?(canonical)
16
+ seen.add(canonical)
17
+ resultado << linha
18
+ end
19
+ else
20
+ resultado << linha
21
+ end
22
+ end
23
+
24
+ resultado.join
25
+ end
26
+
27
+ def self.canonicalize_step(linha, keywords)
28
+ # Remove keyword
29
+ texto = linha.dup.strip
30
+ keywords.each do |kw|
31
+ texto.sub!(/^#{kw}\s+/i, '')
32
+ end
33
+
34
+ # Generaliza: substitui textos entre aspas, colchetes e números por <param>
35
+ texto.gsub!(/"[^"]*"|<[^>]*>|\b\d+\b/, '<param>')
36
+
37
+ # Remove acentos e pontuação, normaliza espaços
38
+ texto = Unicode.normalize_KD(texto).gsub(/\p{Mn}/, '')
39
+ texto.gsub!(/[^a-zA-Z0-9\s<>]/, '')
40
+ texto.downcase.strip.squeeze(" ")
41
+ end
42
+ end
43
+ end
44
+ end
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.49
4
+ version: 1.0.0
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-16 00:00:00.000000000 Z
11
+ date: 2025-05-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: prawn
@@ -28,28 +28,28 @@ dependencies:
28
28
  name: prawn-table
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - "~>"
31
+ - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: 0.37.0
33
+ version: 0.2.0
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - "~>"
38
+ - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: 0.37.0
40
+ version: 0.2.0
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: prawn-svg
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
- - - "~>"
45
+ - - ">="
46
46
  - !ruby/object:Gem::Version
47
47
  version: 0.2.2
48
48
  type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
- - - "~>"
52
+ - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: 0.2.2
55
55
  description: Transforma arquivos .txt com histórias em arquivos .feature, com steps,
@@ -71,12 +71,16 @@ files:
71
71
  - lib/bddgenx/assets/fonts/DejaVuSansMono-Oblique.ttf
72
72
  - lib/bddgenx/assets/fonts/DejaVuSansMono.ttf
73
73
  - lib/bddgenx/generator.rb
74
+ - lib/bddgenx/ia/gemini_cliente.rb
75
+ - lib/bddgenx/ia/gemini_generator.rb
74
76
  - lib/bddgenx/runner.rb
75
77
  - lib/bddgenx/steps_generator.rb
76
78
  - lib/bddgenx/utils/backup.rb
77
- - lib/bddgenx/utils/fontLoader.rb
79
+ - lib/bddgenx/utils/font_loader.rb
80
+ - lib/bddgenx/utils/gherkin_cleaner.rb
78
81
  - lib/bddgenx/utils/parser.rb
79
82
  - lib/bddgenx/utils/pdf_exporter.rb
83
+ - lib/bddgenx/utils/remover_steps_duplicados.rb
80
84
  - lib/bddgenx/utils/tracer.rb
81
85
  - lib/bddgenx/utils/validator.rb
82
86
  - lib/bddgenx/version.rb