cucumber-cucumber-expressions 8.3.1

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.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/.github/ISSUE_TEMPLATE.md +5 -0
  3. data/.github/PULL_REQUEST_TEMPLATE.md +5 -0
  4. data/.rspec +1 -0
  5. data/.rsync +4 -0
  6. data/.subrepo +1 -0
  7. data/Gemfile +3 -0
  8. data/LICENSE +21 -0
  9. data/Makefile +1 -0
  10. data/README.md +5 -0
  11. data/Rakefile +27 -0
  12. data/cucumber-cucumber-expressions.gemspec +33 -0
  13. data/default.mk +70 -0
  14. data/examples.txt +31 -0
  15. data/lib/cucumber/cucumber_expressions/argument.rb +37 -0
  16. data/lib/cucumber/cucumber_expressions/combinatorial_generated_expression_factory.rb +49 -0
  17. data/lib/cucumber/cucumber_expressions/cucumber_expression.rb +119 -0
  18. data/lib/cucumber/cucumber_expressions/cucumber_expression_generator.rb +105 -0
  19. data/lib/cucumber/cucumber_expressions/errors.rb +40 -0
  20. data/lib/cucumber/cucumber_expressions/generated_expression.rb +31 -0
  21. data/lib/cucumber/cucumber_expressions/group.rb +18 -0
  22. data/lib/cucumber/cucumber_expressions/group_builder.rb +42 -0
  23. data/lib/cucumber/cucumber_expressions/parameter_type.rb +81 -0
  24. data/lib/cucumber/cucumber_expressions/parameter_type_matcher.rb +59 -0
  25. data/lib/cucumber/cucumber_expressions/parameter_type_registry.rb +70 -0
  26. data/lib/cucumber/cucumber_expressions/regular_expression.rb +48 -0
  27. data/lib/cucumber/cucumber_expressions/tree_regexp.rb +76 -0
  28. data/scripts/update-gemspec +32 -0
  29. data/spec/capture_warnings.rb +74 -0
  30. data/spec/coverage.rb +7 -0
  31. data/spec/cucumber/cucumber_expressions/argument_spec.rb +17 -0
  32. data/spec/cucumber/cucumber_expressions/combinatorial_generated_expression_factory_test.rb +43 -0
  33. data/spec/cucumber/cucumber_expressions/cucumber_expression_generator_spec.rb +231 -0
  34. data/spec/cucumber/cucumber_expressions/cucumber_expression_regexp_spec.rb +57 -0
  35. data/spec/cucumber/cucumber_expressions/cucumber_expression_spec.rb +212 -0
  36. data/spec/cucumber/cucumber_expressions/custom_parameter_type_spec.rb +202 -0
  37. data/spec/cucumber/cucumber_expressions/expression_examples_spec.rb +30 -0
  38. data/spec/cucumber/cucumber_expressions/parameter_type_registry_spec.rb +86 -0
  39. data/spec/cucumber/cucumber_expressions/parameter_type_spec.rb +15 -0
  40. data/spec/cucumber/cucumber_expressions/regular_expression_spec.rb +80 -0
  41. data/spec/cucumber/cucumber_expressions/tree_regexp_spec.rb +133 -0
  42. metadata +162 -0
@@ -0,0 +1,105 @@
1
+ require 'cucumber/cucumber_expressions/parameter_type_matcher'
2
+ require 'cucumber/cucumber_expressions/generated_expression'
3
+ require 'cucumber/cucumber_expressions/combinatorial_generated_expression_factory'
4
+
5
+ module Cucumber
6
+ module CucumberExpressions
7
+ class CucumberExpressionGenerator
8
+ def initialize(parameter_type_registry)
9
+ @parameter_type_registry = parameter_type_registry
10
+ end
11
+
12
+ def generate_expression(text)
13
+ generate_expressions(text)[0]
14
+ end
15
+
16
+ def generate_expressions(text)
17
+ parameter_type_combinations = []
18
+ parameter_type_matchers = create_parameter_type_matchers(text)
19
+ expression_template = ""
20
+ pos = 0
21
+
22
+ loop do
23
+ matching_parameter_type_matchers = []
24
+ parameter_type_matchers.each do |parameter_type_matcher|
25
+ advanced_parameter_type_matcher = parameter_type_matcher.advance_to(pos)
26
+ if advanced_parameter_type_matcher.find
27
+ matching_parameter_type_matchers.push(advanced_parameter_type_matcher)
28
+ end
29
+ end
30
+
31
+ if matching_parameter_type_matchers.any?
32
+ matching_parameter_type_matchers = matching_parameter_type_matchers.sort
33
+ best_parameter_type_matcher = matching_parameter_type_matchers[0]
34
+ best_parameter_type_matchers = matching_parameter_type_matchers.select do |m|
35
+ (m <=> best_parameter_type_matcher).zero?
36
+ end
37
+
38
+ # Build a list of parameter types without duplicates. The reason there
39
+ # might be duplicates is that some parameter types have more than one regexp,
40
+ # which means multiple ParameterTypeMatcher objects will have a reference to the
41
+ # same ParameterType.
42
+ # We're sorting the list so prefer_for_regexp_match parameter types are listed first.
43
+ # Users are most likely to want these, so they should be listed at the top.
44
+ parameter_types = []
45
+ best_parameter_type_matchers.each do |parameter_type_matcher|
46
+ unless parameter_types.include?(parameter_type_matcher.parameter_type)
47
+ parameter_types.push(parameter_type_matcher.parameter_type)
48
+ end
49
+ end
50
+ parameter_types.sort!
51
+
52
+ parameter_type_combinations.push(parameter_types)
53
+
54
+ expression_template += escape(text.slice(pos...best_parameter_type_matcher.start))
55
+ expression_template += "{%s}"
56
+
57
+ pos = best_parameter_type_matcher.start + best_parameter_type_matcher.group.length
58
+ else
59
+ break
60
+ end
61
+
62
+ if pos >= text.length
63
+ break
64
+ end
65
+ end
66
+
67
+ expression_template += escape(text.slice(pos..-1))
68
+
69
+ CombinatorialGeneratedExpressionFactory.new(
70
+ expression_template,
71
+ parameter_type_combinations
72
+ ).generate_expressions
73
+ end
74
+
75
+ private
76
+
77
+ def create_parameter_type_matchers(text)
78
+ parameter_matchers = []
79
+ @parameter_type_registry.parameter_types.each do |parameter_type|
80
+ if parameter_type.use_for_snippets?
81
+ parameter_matchers += create_parameter_type_matchers2(parameter_type, text)
82
+ end
83
+ end
84
+ parameter_matchers
85
+ end
86
+
87
+ def create_parameter_type_matchers2(parameter_type, text)
88
+ result = []
89
+ regexps = parameter_type.regexps
90
+ regexps.each do |regexp|
91
+ regexp = Regexp.new("(#{regexp})")
92
+ result.push(ParameterTypeMatcher.new(parameter_type, regexp, text, 0))
93
+ end
94
+ result
95
+ end
96
+
97
+ def escape(s)
98
+ s.gsub(/%/, '%%')
99
+ .gsub(/\(/, '\\(')
100
+ .gsub(/\{/, '\\{')
101
+ .gsub(/\//, '\\/')
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,40 @@
1
+ module Cucumber
2
+ module CucumberExpressions
3
+ class CucumberExpressionError < StandardError
4
+ end
5
+
6
+ class UndefinedParameterTypeError < CucumberExpressionError
7
+ def initialize(type_name)
8
+ super("Undefined parameter type {#{type_name}}")
9
+ end
10
+ end
11
+
12
+ class AmbiguousParameterTypeError < CucumberExpressionError
13
+ def initialize(parameter_type_regexp, expression_regexp, parameter_types, generated_expressions)
14
+ super(<<-EOM)
15
+ Your Regular Expression /#{expression_regexp.source}/
16
+ matches multiple parameter types with regexp /#{parameter_type_regexp}/:
17
+ #{parameter_type_names(parameter_types)}
18
+
19
+ I couldn't decide which one to use. You have two options:
20
+
21
+ 1) Use a Cucumber Expression instead of a Regular Expression. Try one of these:
22
+ #{expressions(generated_expressions)}
23
+
24
+ 2) Make one of the parameter types preferential and continue to use a Regular Expression.
25
+
26
+ EOM
27
+ end
28
+
29
+ private
30
+
31
+ def parameter_type_names(parameter_types)
32
+ parameter_types.map{|p| "{#{p.name}}"}.join("\n ")
33
+ end
34
+
35
+ def expressions(generated_expressions)
36
+ generated_expressions.map{|ge| ge.source}.join("\n ")
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,31 @@
1
+ module Cucumber
2
+ module CucumberExpressions
3
+ class GeneratedExpression
4
+ attr_reader :parameter_types
5
+
6
+ def initialize(expression_template, parameters_types)
7
+ @expression_template, @parameter_types = expression_template, parameters_types
8
+ end
9
+
10
+ def source
11
+ sprintf(@expression_template, *@parameter_types.map(&:name))
12
+ end
13
+
14
+ def parameter_names
15
+ usage_by_type_name = Hash.new(0)
16
+ @parameter_types.map do |t|
17
+ get_parameter_name(t.name, usage_by_type_name)
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def get_parameter_name(type_name, usage_by_type_name)
24
+ count = usage_by_type_name[type_name]
25
+ count += 1
26
+ usage_by_type_name[type_name] = count
27
+ count == 1 ? type_name : "#{type_name}#{count}"
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,18 @@
1
+ module Cucumber
2
+ module CucumberExpressions
3
+ class Group
4
+ attr_reader :value, :start, :end, :children
5
+
6
+ def initialize(value, start, _end, children)
7
+ @value = value
8
+ @start = start
9
+ @end = _end
10
+ @children = children
11
+ end
12
+
13
+ def values
14
+ (children.empty? ? [self] : children).map(&:value).compact
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,42 @@
1
+ require 'cucumber/cucumber_expressions/group'
2
+
3
+ module Cucumber
4
+ module CucumberExpressions
5
+ class GroupBuilder
6
+ attr_accessor :source
7
+
8
+ def initialize
9
+ @group_builders = []
10
+ @capturing = true
11
+ end
12
+
13
+ def add(group_builder)
14
+ @group_builders.push(group_builder)
15
+ end
16
+
17
+ def build(match, group_indices)
18
+ group_index = group_indices.next
19
+ children = @group_builders.map {|gb| gb.build(match, group_indices)}
20
+ Group.new(match[group_index], match.offset(group_index)[0], match.offset(group_index)[1], children)
21
+ end
22
+
23
+ def set_non_capturing!
24
+ @capturing = false
25
+ end
26
+
27
+ def capturing?
28
+ @capturing
29
+ end
30
+
31
+ def move_children_to(group_builder)
32
+ @group_builders.each do |child|
33
+ group_builder.add(child)
34
+ end
35
+ end
36
+
37
+ def children
38
+ @group_builders
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,81 @@
1
+ require 'cucumber/cucumber_expressions/errors'
2
+
3
+ module Cucumber
4
+ module CucumberExpressions
5
+ class ParameterType
6
+ ILLEGAL_PARAMETER_NAME_PATTERN = /([\[\]()$.|?*+])/
7
+ UNESCAPE_PATTERN = /(\\([\[$.|?*+\]]))/
8
+
9
+ attr_reader :name, :type, :regexps
10
+
11
+ def self.check_parameter_type_name(type_name)
12
+ unescaped_type_name = type_name.gsub(UNESCAPE_PATTERN) do
13
+ $2
14
+ end
15
+ if ILLEGAL_PARAMETER_NAME_PATTERN =~ unescaped_type_name
16
+ raise CucumberExpressionError.new("Illegal character '#{$1}' in parameter name {#{unescaped_type_name}}")
17
+ end
18
+ end
19
+
20
+ def prefer_for_regexp_match?
21
+ @prefer_for_regexp_match
22
+ end
23
+
24
+ def use_for_snippets?
25
+ @use_for_snippets
26
+ end
27
+
28
+ # Create a new Parameter
29
+ #
30
+ # @param name the name of the parameter type
31
+ # @param regexp [Array] list of regexps for capture groups. A single regexp can also be used
32
+ # @param type the return type of the transformed
33
+ # @param transformer lambda that transforms a String to (possibly) another type
34
+ # @param use_for_snippets true if this should be used for snippet generation
35
+ # @param prefer_for_regexp_match true if this should be preferred over similar types
36
+ #
37
+ def initialize(name, regexp, type, transformer, use_for_snippets, prefer_for_regexp_match)
38
+ raise "regexp can't be nil" if regexp.nil?
39
+ raise "type can't be nil" if type.nil?
40
+ raise "transformer can't be nil" if transformer.nil?
41
+ raise "use_for_snippets can't be nil" if use_for_snippets.nil?
42
+ raise "prefer_for_regexp_match can't be nil" if prefer_for_regexp_match.nil?
43
+
44
+ self.class.check_parameter_type_name(name) unless name.nil?
45
+ @name, @type, @transformer, @use_for_snippets, @prefer_for_regexp_match = name, type, transformer, use_for_snippets, prefer_for_regexp_match
46
+ @regexps = string_array(regexp)
47
+ end
48
+
49
+ def transform(self_obj, group_values)
50
+ self_obj.instance_exec(*group_values, &@transformer)
51
+ end
52
+
53
+ def <=>(other)
54
+ return -1 if prefer_for_regexp_match? && !other.prefer_for_regexp_match?
55
+ return 1 if other.prefer_for_regexp_match? && !prefer_for_regexp_match?
56
+ return name <=> other.name
57
+ end
58
+
59
+ private
60
+
61
+ def string_array(regexps)
62
+ array = regexps.is_a?(Array) ? regexps : [regexps]
63
+ array.map {|regexp| regexp.is_a?(String) ? regexp : regexp_source(regexp)}
64
+ end
65
+
66
+ def regexp_source(regexp)
67
+ [
68
+ 'EXTENDED',
69
+ 'IGNORECASE',
70
+ 'MULTILINE'
71
+ ].each do |option_name|
72
+ option = Regexp.const_get(option_name)
73
+ if regexp.options & option != 0
74
+ raise CucumberExpressionError.new("ParameterType Regexps can't use option Regexp::#{option_name}")
75
+ end
76
+ end
77
+ regexp.source
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,59 @@
1
+ module Cucumber
2
+ module CucumberExpressions
3
+ class ParameterTypeMatcher
4
+ attr_reader :parameter_type
5
+
6
+ def initialize(parameter_type, regexp, text, match_position=0)
7
+ @parameter_type, @regexp, @text = parameter_type, regexp, text
8
+ @match = @regexp.match(@text, match_position)
9
+ end
10
+
11
+ def advance_to(new_match_position)
12
+ (new_match_position...@text.length).each {|advancedPos|
13
+ matcher = self.class.new(parameter_type, @regexp, @text, advancedPos)
14
+ if matcher.find && matcher.full_word?
15
+ return matcher
16
+ end
17
+ }
18
+
19
+ self.class.new(parameter_type, @regexp, @text, @text.length)
20
+ end
21
+
22
+ def find
23
+ !@match.nil? && !group.empty?
24
+ end
25
+
26
+ def full_word?
27
+ space_before_match_or_sentence_start? && space_after_match_or_sentence_end?
28
+ end
29
+
30
+ def start
31
+ @match.begin(0)
32
+ end
33
+
34
+ def group
35
+ @match.captures[0]
36
+ end
37
+
38
+ def <=>(other)
39
+ pos_comparison = start <=> other.start
40
+ return pos_comparison if pos_comparison != 0
41
+ length_comparison = other.group.length <=> group.length
42
+ return length_comparison if length_comparison != 0
43
+ 0
44
+ end
45
+
46
+ private
47
+
48
+ def space_before_match_or_sentence_start?
49
+ match_begin = @match.begin(0)
50
+ match_begin == 0 || @text[match_begin - 1].match(/\s|\p{P}/)
51
+ end
52
+
53
+ def space_after_match_or_sentence_end?
54
+ match_end = @match.end(0)
55
+ match_end == @text.length || @text[match_end].match(/\s|\p{P}/)
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,70 @@
1
+ require 'cucumber/cucumber_expressions/parameter_type'
2
+ require 'cucumber/cucumber_expressions/errors'
3
+ require 'cucumber/cucumber_expressions/cucumber_expression_generator'
4
+
5
+ module Cucumber
6
+ module CucumberExpressions
7
+ class ParameterTypeRegistry
8
+ INTEGER_REGEXPS = [/-?\d+/, /\d+/]
9
+ FLOAT_REGEXP = /(?=.*\d.*)[-+]?\d*(?:\.(?=\d.*))?\d*(?:\d+[E][-+]?\d+)?/
10
+ WORD_REGEXP = /[^\s]+/
11
+ STRING_REGEXP = /"([^"\\]*(\\.[^"\\]*)*)"|'([^'\\]*(\\.[^'\\]*)*)'/
12
+ ANONYMOUS_REGEXP = /.*/
13
+
14
+ def initialize
15
+ @parameter_type_by_name = {}
16
+ @parameter_types_by_regexp = Hash.new {|hash, regexp| hash[regexp] = []}
17
+
18
+ define_parameter_type(ParameterType.new('int', INTEGER_REGEXPS, Integer, lambda {|s = nil| s && s.to_i}, true, true))
19
+ define_parameter_type(ParameterType.new('float', FLOAT_REGEXP, Float, lambda {|s = nil| s && s.to_f}, true, false))
20
+ define_parameter_type(ParameterType.new('word', WORD_REGEXP, String, lambda {|s = nil| s}, false, false))
21
+ define_parameter_type(ParameterType.new('string', STRING_REGEXP, String, lambda {|s = nil| s && s.gsub(/\\"/, '"').gsub(/\\'/, "'")}, true, false))
22
+ define_parameter_type(ParameterType.new('', ANONYMOUS_REGEXP, String, lambda {|s = nil| s}, false, true))
23
+ end
24
+
25
+ def lookup_by_type_name(name)
26
+ @parameter_type_by_name[name]
27
+ end
28
+
29
+ def lookup_by_regexp(parameter_type_regexp, expression_regexp, text)
30
+ parameter_types = @parameter_types_by_regexp[parameter_type_regexp]
31
+ return nil if parameter_types.nil?
32
+ if parameter_types.length > 1 && !parameter_types[0].prefer_for_regexp_match?
33
+ # We don't do this check on insertion because we only want to restrict
34
+ # ambiguity when we look up by Regexp. Users of CucumberExpression should
35
+ # not be restricted.
36
+ generated_expressions = CucumberExpressionGenerator.new(self).generate_expressions(text)
37
+ raise AmbiguousParameterTypeError.new(parameter_type_regexp, expression_regexp, parameter_types, generated_expressions)
38
+ end
39
+ parameter_types.first
40
+ end
41
+
42
+ def parameter_types
43
+ @parameter_type_by_name.values
44
+ end
45
+
46
+ def define_parameter_type(parameter_type)
47
+ if parameter_type.name != nil
48
+ if @parameter_type_by_name.has_key?(parameter_type.name)
49
+ if parameter_type.name.length == 0
50
+ raise CucumberExpressionError.new("The anonymous parameter type has already been defined")
51
+ else
52
+ raise CucumberExpressionError.new("There is already a parameter with name #{parameter_type.name}")
53
+ end
54
+ end
55
+ @parameter_type_by_name[parameter_type.name] = parameter_type
56
+ end
57
+
58
+ parameter_type.regexps.each do |parameter_type_regexp|
59
+ parameter_types = @parameter_types_by_regexp[parameter_type_regexp]
60
+ if parameter_types.any? && parameter_types[0].prefer_for_regexp_match? && parameter_type.prefer_for_regexp_match?
61
+ raise CucumberExpressionError.new("There can only be one preferential parameter type per regexp. The regexp /#{parameter_type_regexp}/ is used for two preferential parameter types, {#{parameter_types[0].name}} and {#{parameter_type.name}}")
62
+ end
63
+ parameter_types.push(parameter_type)
64
+ parameter_types.sort!
65
+ end
66
+ end
67
+
68
+ end
69
+ end
70
+ end