cucumber-cucumber-expressions 8.3.1

Sign up to get free protection for your applications and to get access to all the features.
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