lib/cuke_modeler/adapters/gherkin_9_adapter.rb
require_relative 'gherkin_base_adapter'
# Some things just aren't going to get better due to the inherent complexity of the AST
# rubocop:disable Metrics/ClassLength, Metrics/AbcSize, Metrics/MethodLength
module CukeModeler
# @api private
#
# An adapter that can convert the output of version 9.x of the *cucumber-gherkin* gem into input that is consumable
# by this gem. Internal helper class.
class Gherkin9Adapter < GherkinBaseAdapter
# Adapts the given AST into the shape that this gem expects
def adapt(ast)
adapted_ast = {}
# Saving off the original data and removing parsed data for child elements in order to avoid duplicating data
save_original_data(adapted_ast, ast)
clear_child_elements(adapted_ast, [[:feature],
[:comments]])
adapted_ast['comments'] = adapt_comments(ast)
adapted_ast['feature'] = adapt_feature(ast[:feature])
adapted_ast
end
# Adapts the AST sub-tree that is rooted at the given feature node.
def adapt_feature(feature_ast)
return nil unless feature_ast
adapted_feature = {}
# Saving off the original data and removing parsed data for child elements in order to avoid duplicating data
save_original_data(adapted_feature, feature_ast)
clear_child_elements(adapted_feature, [[:tags],
[:children]])
adapted_feature['language'] = feature_ast[:language]
adapted_feature['keyword'] = feature_ast[:keyword]
adapted_feature['name'] = feature_ast[:name]
adapted_feature['description'] = feature_ast[:description] || ''
adapted_feature['line'] = feature_ast[:location][:line]
adapted_feature['column'] = feature_ast[:location][:column]
adapted_feature['elements'] = adapt_child_elements(feature_ast)
adapted_feature['tags'] = adapt_tags(feature_ast)
adapted_feature
end
# Adapts the AST sub-tree that is rooted at the given background node.
def adapt_background(background_ast)
adapted_background = {}
# Saving off the original data and removing parsed data for child elements in order to avoid duplicating data
save_original_data(adapted_background, background_ast)
clear_child_elements(adapted_background, [[:background, :steps]])
adapted_background['type'] = 'Background'
adapted_background['keyword'] = background_ast[:background][:keyword]
adapted_background['name'] = background_ast[:background][:name]
adapted_background['description'] = background_ast[:background][:description] || ''
adapted_background['line'] = background_ast[:background][:location][:line]
adapted_background['column'] = background_ast[:background][:location][:column]
adapted_background['steps'] = adapt_steps(background_ast[:background])
adapted_background
end
# Adapts the AST sub-tree that is rooted at the given rule node.
def adapt_rule(rule_ast)
adapted_rule = {}
# Saving off the original data and removing parsed data for child elements in order to avoid duplicating data
save_original_data(adapted_rule, rule_ast)
clear_child_elements(adapted_rule, [[:rule, :children]])
adapted_rule['type'] = 'Rule'
adapted_rule['keyword'] = rule_ast[:rule][:keyword]
adapted_rule['name'] = rule_ast[:rule][:name]
adapted_rule['description'] = rule_ast[:rule][:description] || ''
adapted_rule['line'] = rule_ast[:rule][:location][:line]
adapted_rule['column'] = rule_ast[:rule][:location][:column]
adapted_rule['elements'] = adapt_child_elements(rule_ast[:rule])
adapted_rule
end
# Adapts the AST sub-tree that is rooted at the given scenario node.
def adapt_scenario(test_ast)
adapted_scenario = {}
# Saving off the original data and removing parsed data for child elements in order to avoid duplicating data
save_original_data(adapted_scenario, test_ast)
clear_child_elements(adapted_scenario, [[:scenario, :tags],
[:scenario, :steps]])
adapted_scenario['type'] = 'Scenario'
adapted_scenario['keyword'] = test_ast[:scenario][:keyword]
adapted_scenario['name'] = test_ast[:scenario][:name]
adapted_scenario['description'] = test_ast[:scenario][:description] || ''
adapted_scenario['line'] = test_ast[:scenario][:location][:line]
adapted_scenario['column'] = test_ast[:scenario][:location][:column]
adapted_scenario['tags'] = adapt_tags(test_ast[:scenario])
adapted_scenario['steps'] = adapt_steps(test_ast[:scenario])
adapted_scenario
end
# Adapts the AST sub-tree that is rooted at the given outline node.
def adapt_outline(test_ast)
adapted_outline = {}
# Saving off the original data and removing parsed data for child elements in order to avoid duplicating data
save_original_data(adapted_outline, test_ast)
clear_child_elements(adapted_outline, [[:scenario, :tags],
[:scenario, :steps],
[:scenario, :examples]])
adapted_outline['type'] = 'ScenarioOutline'
adapted_outline['keyword'] = test_ast[:scenario][:keyword]
adapted_outline['name'] = test_ast[:scenario][:name]
adapted_outline['description'] = test_ast[:scenario][:description] || ''
adapted_outline['line'] = test_ast[:scenario][:location][:line]
adapted_outline['column'] = test_ast[:scenario][:location][:column]
adapted_outline['tags'] = adapt_tags(test_ast[:scenario])
adapted_outline['steps'] = adapt_steps(test_ast[:scenario])
adapted_outline['examples'] = adapt_examples(test_ast[:scenario])
adapted_outline
end
# Adapts the AST sub-tree that is rooted at the given example node.
def adapt_example(example_ast)
adapted_example = {}
# Saving off the original data and removing parsed data for child elements in order to avoid duplicating data
save_original_data(adapted_example, example_ast)
clear_child_elements(adapted_example, [[:tags],
[:table_header],
[:table_body]])
adapted_example['keyword'] = example_ast[:keyword]
adapted_example['name'] = example_ast[:name]
adapted_example['line'] = example_ast[:location][:line]
adapted_example['column'] = example_ast[:location][:column]
adapted_example['description'] = example_ast[:description] || ''
adapted_example['rows'] = []
adapted_example['rows'] << adapt_table_row(example_ast[:table_header]) if example_ast[:table_header]
example_ast[:table_body]&.each do |row|
adapted_example['rows'] << adapt_table_row(row)
end
adapted_example['tags'] = adapt_tags(example_ast)
adapted_example
end
# Adapts the AST sub-tree that is rooted at the given tag node.
def adapt_tag(tag_ast)
adapted_tag = {}
# Saving off the original data
save_original_data(adapted_tag, tag_ast)
adapted_tag['name'] = tag_ast[:name]
adapted_tag['line'] = tag_ast[:location][:line]
adapted_tag['column'] = tag_ast[:location][:column]
adapted_tag
end
# Adapts the AST sub-tree that is rooted at the given comment node.
def adapt_comment(comment_ast)
adapted_comment = {}
# Saving off the original data
save_original_data(adapted_comment, comment_ast)
adapted_comment['text'] = comment_ast[:text]
adapted_comment['line'] = comment_ast[:location][:line]
adapted_comment['column'] = comment_ast[:location][:column]
adapted_comment
end
# Adapts the AST sub-tree that is rooted at the given step node.
def adapt_step(step_ast)
adapted_step = {}
# Saving off the original data and removing parsed data for child elements in order to avoid duplicating data
save_original_data(adapted_step, step_ast)
clear_child_elements(adapted_step, [[:data_table],
[:doc_string]])
adapted_step['keyword'] = step_ast[:keyword]
adapted_step['name'] = step_ast[:text]
adapted_step['line'] = step_ast[:location][:line]
adapted_step['column'] = step_ast[:location][:column]
if step_ast[:doc_string]
adapted_step['doc_string'] = adapt_doc_string(step_ast[:doc_string])
elsif step_ast[:data_table]
adapted_step['table'] = adapt_step_table(step_ast[:data_table])
end
adapted_step
end
# Adapts the AST sub-tree that is rooted at the given doc string node.
def adapt_doc_string(doc_string_ast)
adapted_doc_string = {}
# Saving off the original data
save_original_data(adapted_doc_string, doc_string_ast)
adapted_doc_string['value'] = doc_string_ast[:content]
adapted_doc_string['content_type'] = doc_string_ast[:media_type]
adapted_doc_string['line'] = doc_string_ast[:location][:line]
adapted_doc_string['column'] = doc_string_ast[:location][:column]
adapted_doc_string
end
# Adapts the AST sub-tree that is rooted at the given table node.
def adapt_step_table(step_table_ast)
adapted_step_table = {}
# Saving off the original data and removing parsed data for child elements in order to avoid duplicating data
save_original_data(adapted_step_table, step_table_ast)
clear_child_elements(adapted_step_table, [[:rows]])
adapted_step_table['rows'] = []
step_table_ast[:rows].each do |row|
adapted_step_table['rows'] << adapt_table_row(row)
end
adapted_step_table['line'] = step_table_ast[:location][:line]
adapted_step_table['column'] = step_table_ast[:location][:column]
adapted_step_table
end
# Adapts the AST sub-tree that is rooted at the given row node.
def adapt_table_row(table_row_ast)
adapted_table_row = {}
# Saving off the original data and removing parsed data for child elements in order to avoid duplicating data
save_original_data(adapted_table_row, table_row_ast)
clear_child_elements(adapted_table_row, [[:cells]])
adapted_table_row['line'] = table_row_ast[:location][:line]
adapted_table_row['column'] = table_row_ast[:location][:column]
adapted_table_row['cells'] = []
table_row_ast[:cells].each do |row|
adapted_table_row['cells'] << adapt_table_cell(row)
end
adapted_table_row
end
# Adapts the AST sub-tree that is rooted at the given cell node.
def adapt_table_cell(cell_ast)
adapted_cell = {}
# Saving off the original data
save_original_data(adapted_cell, cell_ast)
adapted_cell['value'] = cell_ast[:value]
adapted_cell['line'] = cell_ast[:location][:line]
adapted_cell['column'] = cell_ast[:location][:column]
adapted_cell
end
private
def adapt_comments(file_ast)
return [] unless file_ast[:comments]
file_ast[:comments].map { |comment| adapt_comment(comment) }
end
def adapt_tags(element_ast)
return [] unless element_ast[:tags]
element_ast[:tags].map { |tag| adapt_tag(tag) }
end
def adapt_steps(element_ast)
return [] unless element_ast[:steps]
element_ast[:steps].map { |step| adapt_step(step) }
end
def adapt_examples(element_ast)
return [] unless element_ast[:examples]
element_ast[:examples].map { |example| adapt_example(example) }
end
def adapt_child_elements(element_ast)
return [] unless element_ast[:children]
adapted_children = []
element_ast[:children].each do |child_element|
adapted_children << if child_element[:background]
adapt_background(child_element)
elsif child_element[:rule]
adapt_rule(child_element)
else
adapt_test(child_element)
end
end
adapted_children
end
def adapt_test(test_ast)
if (test_node?(test_ast) && test_has_examples?(test_ast)) ||
(test_node?(test_ast) && test_uses_outline_keyword?(test_ast))
adapt_outline(test_ast)
elsif test_node?(test_ast)
adapt_scenario(test_ast)
else
raise(ArgumentError, "Unknown test type with keys: #{test_ast.keys}")
end
end
def clear_child_elements(ast, child_paths)
child_paths.each do |traversal_path|
# Wipe the value if it's there but don't add any keys to the hash if it didn't already have them
if ast['cuke_modeler_parsing_data'].dig(*traversal_path)
bury(ast['cuke_modeler_parsing_data'], traversal_path, nil)
end
end
end
def bury(hash, traversal_path, value)
keys = *traversal_path
current = hash
(keys.count - 1).times do |index|
current = hash[keys[index]]
end
current[keys.last] = value
end
def test_node?(ast_node)
!ast_node[:scenario].nil?
end
def test_has_examples?(ast_node)
!ast_node[:scenario][:examples].nil?
end
def test_uses_outline_keyword?(test_ast)
Parsing.dialects[Parsing.dialect]['scenarioOutline'].include?(test_ast[:scenario][:keyword])
end
end
private_constant :Gherkin9Adapter
end
# rubocop:enable Metrics/ClassLength, Metrics/AbcSize, Metrics/MethodLength