macros4cuke 0.2.13 → 0.2.14

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.
data/.yardopts ADDED
@@ -0,0 +1,6 @@
1
+ --exclude features
2
+ --no-private
3
+ --markup markdown
4
+ -
5
+ Changelog.md
6
+ License.txt
data/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ ## 0.2.14 / 2013-05-03
2
+ * [CHANGE] Code comments reformatted to YARD. The command line `yard stats`display a 100% documented score!
3
+ * [CHANGE] Moved all classes related to the template engine to the new module Templating.
4
+ * [CHANGE] Classes `StaticRep, EOLRep, VariableRep, TemplateEngine` renamed to `StaticText, EOLine, Placeholder, Engine` respectively.
5
+ * [CHANGE] Added spec file for `MacroStepSupport` module.
6
+ * [CHANGE] Initialization of `MacroStepSupport` singleton changed (no dependency on `extended` hook).
7
+
1
8
  ## 0.2.13 / 2013-05-02
2
9
  * [NEW] File `macro-collection_spec.rb`: partial spec file for the `MacroCollection` class.
3
10
  * [FIX] `DuplicateMacroError#initialize`: Removed superfluous [ in error message.
data/lib/macro_steps.rb CHANGED
@@ -5,17 +5,17 @@
5
5
 
6
6
  # This step is used to define a macro-step
7
7
  # Example:
8
- # Given I define the step "When I [log in as {{userid}}]" to mean:
8
+ # Given I define the step "When I [log in as <userid>]" to mean:
9
9
  # """
10
10
  # Given I landed in the homepage
11
11
  # When I click "Sign in"
12
- # And I fill in "Username" with "{{userid}}"
12
+ # And I fill in "Username" with "<userid>"
13
13
  # And I fill in "Password" with "unguessable"
14
14
  # And I click "Submit"
15
15
  # """
16
16
  # The regexp has two capturing group: one for the phrase, a second for the terminating colon (:)
17
- Given(/^I define the step "(?:Given|When|Then) I \[((?:[^\\\]]|\\.)+)\](:?)" to mean:$/) do |macro_phrase, colon_present, template|
18
- use_table = (colon_present == ':')
17
+ Given(/^I define the step "(?:Given|When|Then) I \[((?:[^\\\]]|\\.)+)\](:?)" to mean:$/) do |macro_phrase, colon_capture, template|
18
+ use_table = (colon_capture == ':')
19
19
  add_macro(macro_phrase, template, use_table)
20
20
  end
21
21
 
@@ -45,4 +45,5 @@ When(/^I \[([^\]]+)\]:$/) do |macro_phrase, table_argument|
45
45
  end
46
46
 
47
47
 
48
+
48
49
  # End of file
@@ -3,20 +3,20 @@
3
3
 
4
4
  module Macros4Cuke # Module used as a namespace
5
5
  # The version number of the gem.
6
- Version = '0.2.13'
7
-
6
+ Version = '0.2.14'
7
+
8
8
  # Brief description of the gem.
9
9
  Description = "Macros for Cucumber"
10
-
10
+
11
11
  # Constant Macros4Cuke::RootDir contains the absolute path of Rodent's root directory. Note: it also ends with a slash character.
12
12
  unless defined?(RootDir)
13
13
  # The initialisation of constant RootDir is guarded in order to avoid multiple initialisation (not allowed for constants)
14
-
14
+
15
15
  # The root folder of Macros4Cuke.
16
16
  RootDir = begin
17
17
  require 'pathname' # Load Pathname class from standard library
18
18
  rootdir = Pathname(__FILE__).dirname.parent.parent.expand_path()
19
- rootdir.to_s() + '/' # Append trailing slash character to it
19
+ rootdir.to_s() + '/' # Append trailing slash character to it
20
20
  end
21
21
  end
22
22
  end # module
@@ -1,82 +1,83 @@
1
- # encoding: utf-8 -- You should see a paragraph character: §
2
- # File: exceptions.rb
3
-
4
- module Macros4Cuke # Module used as a namespace
5
-
6
- # Base class for any exception explicitly raised in the Macros4Cuke methods.
7
- class Macros4CukeError < StandardError
8
- end # class
9
-
10
- # Raised when one attempts to define a new macro
11
- # that has the same phrase as an existing macro.
12
- class DuplicateMacroError < Macros4CukeError
13
- def initialize(aPhrase)
14
- super("A macro-step with phrase '#{aPhrase}' already exist.")
15
- end
16
- end # class
17
-
18
- # Raised when one defines an argument name in a macro-step's phrase
19
- # and that argument name does not appear in any sub-step.
20
- class UselessPhraseArgument < Macros4CukeError
21
- def initialize(anArgName)
22
- super("The phrase argument '#{anArgName}' does not appear in a sub-step.")
23
- end
24
- end # class
25
-
26
-
27
-
28
- # Raised when one defines an argument name in a macro-step's phrase
29
- # and that argument name does not appear in any sub-step.
30
- class UnreachableSubstepArgument < Macros4CukeError
31
- def initialize(anArgName)
32
- super("The sub-step argument '#{anArgName}' does not appear in the phrase.")
33
- end
34
- end # class
35
-
36
-
37
-
38
- # Raised when a sub-step has an empty or blank argument name.
39
- class EmptyArgumentError < Macros4CukeError
40
- def initialize(aText)
41
- super("An empty or blank argument occurred in '#{aText}'.")
42
- end
43
- end # class
44
-
45
-
46
- # Raised when one invokes a macro-step with an unknown phrase.
47
- class UnknownMacroError < Macros4CukeError
48
- def initialize(aPhrase)
49
- super("Unknown macro-step with phrase: '[#{aPhrase}'.")
50
- end
51
- end # class
52
-
53
-
54
-
55
- # Raised when one invokes a macro-step with an argument
56
- # that has an unknown name.
57
- class UnknownArgumentError < Macros4CukeError
58
- def initialize(argName)
59
- super("Unknown macro-step argument '#{argName}'.")
60
- end
61
- end # class
62
-
63
-
64
-
65
- # Raised when one invokes a macro-step without a required data table argument
66
- class DataTableNotFound < Macros4CukeError
67
- def initialize(argName)
68
- super("The macro-step is missing a data table argument.")
69
- end
70
- end # class
71
-
72
-
73
-
74
- # Raised when Macros4Cuke encountered an issue
75
- # that it can't handle properly.
76
- class InternalError < Macros4CukeError
77
- end # class
78
-
79
-
80
- end # module
81
-
1
+ # encoding: utf-8 -- You should see a paragraph character: §
2
+ # File: exceptions.rb
3
+
4
+ module Macros4Cuke # Module used as a namespace
5
+
6
+ # @abstract
7
+ # Base class for any exception explicitly raised in the Macros4Cuke methods.
8
+ class Macros4CukeError < StandardError
9
+ end # class
10
+
11
+ # Raised when one attempts to define a new macro
12
+ # that has the same phrase as an existing macro.
13
+ class DuplicateMacroError < Macros4CukeError
14
+ def initialize(aPhrase)
15
+ super("A macro-step with phrase '#{aPhrase}' already exist.")
16
+ end
17
+ end # class
18
+
19
+ # Raised when one defines an argument name in a macro-step's phrase
20
+ # and that argument name does not appear in any sub-step.
21
+ class UselessPhraseArgument < Macros4CukeError
22
+ def initialize(anArgName)
23
+ super("The phrase argument '#{anArgName}' does not appear in a sub-step.")
24
+ end
25
+ end # class
26
+
27
+
28
+
29
+ # Raised when one defines an argument name in a macro-step's phrase
30
+ # and that argument name does not appear in any sub-step.
31
+ class UnreachableSubstepArgument < Macros4CukeError
32
+ def initialize(anArgName)
33
+ super("The sub-step argument '#{anArgName}' does not appear in the phrase.")
34
+ end
35
+ end # class
36
+
37
+
38
+
39
+ # Raised when a sub-step has an empty or blank argument name.
40
+ class EmptyArgumentError < Macros4CukeError
41
+ def initialize(aText)
42
+ super("An empty or blank argument occurred in '#{aText}'.")
43
+ end
44
+ end # class
45
+
46
+
47
+ # Raised when one invokes a macro-step with an unknown phrase.
48
+ class UnknownMacroError < Macros4CukeError
49
+ def initialize(aPhrase)
50
+ super("Unknown macro-step with phrase: '[#{aPhrase}'.")
51
+ end
52
+ end # class
53
+
54
+
55
+
56
+ # Raised when one invokes a macro-step with an argument
57
+ # that has an unknown name.
58
+ class UnknownArgumentError < Macros4CukeError
59
+ def initialize(argName)
60
+ super("Unknown macro-step argument '#{argName}'.")
61
+ end
62
+ end # class
63
+
64
+
65
+
66
+ # Raised when one invokes a macro-step without a required data table argument
67
+ class DataTableNotFound < Macros4CukeError
68
+ def initialize(argName)
69
+ super("The macro-step is missing a data table argument.")
70
+ end
71
+ end # class
72
+
73
+
74
+
75
+ # Raised when Macros4Cuke encountered an issue
76
+ # that it can't handle properly.
77
+ class InternalError < Macros4CukeError
78
+ end # class
79
+
80
+
81
+ end # module
82
+
82
83
  # End of file
@@ -6,32 +6,22 @@ require_relative "macro-step"
6
6
 
7
7
  module Macros4Cuke # Module used as a namespace
8
8
 
9
- # Represents a container of macros.
10
- # It gather all the macros encountered by Cucumber while "executing" the feature files.
9
+ # Represents a container of macros.
10
+ # It gathers all the macros encountered by Cucumber while "executing" the feature files.
11
+ # @note This is a singleton class (i.e. there is only one macro collection object).
11
12
  class MacroCollection
12
13
  include Singleton # Use the Singleton design pattern.
13
14
 
14
- # A Hash with pairs of the form: phrase => MacroStep object
15
- attr_reader(:macro_steps)
15
+ # @!attribute [r] macro_steps.
16
+ # A Hash with pairs of the form: macro key => MacroStep object
16
17
 
17
- # Init the pool if it was not done yet.
18
- def init()
19
- @macro_steps = {} if @macro_steps.nil?
20
- end
21
18
 
22
19
  public
23
- # Return true iff the host has a macro with the given key.
24
- def has_macro?(aPhrase, mode)
25
- key = MacroStep::macro_key(aPhrase, mode)
26
- return @macro_steps.include? key
27
- end
28
-
29
-
30
20
  # Add a new macro.
31
21
  # Pre-condition: there is no existing macro with the same key.
32
- # [aPhrase] The text that is enclosed between the square brackets.
33
- # [aTemplate] A text that consists of a sequence of Cucumber steps.
34
- # [useTable] A boolean that indicates whether a table should be used to pass actual values.
22
+ # @param aPhrase [String] The text that is enclosed between the square brackets.
23
+ # @param aTemplate [String] A text that consists of a sequence of sub-steps.
24
+ # @param useTable [boolean] A flag indicating whether a table should be used to pass actual values.
35
25
  def add_macro(aPhrase, aTemplate, useTable)
36
26
  new_macro = MacroStep.new(aPhrase, aTemplate, useTable)
37
27
 
@@ -40,17 +30,17 @@ public
40
30
  # An exception is raised if the phrase syntax of both macros are the
41
31
  raise DuplicateMacroError.new(aPhrase) if find_macro(aPhrase, useTable)
42
32
 
43
- @macro_steps[new_macro.key] = new_macro
33
+ macro_steps[new_macro.key] = new_macro
44
34
 
45
35
  end
46
-
36
+
47
37
  # Render the steps associated to the macro with given phrase
48
38
  # and (optionally) given a table of values.
49
39
  # Return the rendered steps as a text.
50
- # [aPhrase] an instance of the macro phrase.
51
- # [rawData] An Array of couples.
52
- # Each couple is of the form: argument name, a value.
53
- # Multiple rows with same argument name are acceptable.
40
+ # @param aPhrase [String] an instance of the macro phrase.
41
+ # @param rawData [Array] An array of couples of the form: [ argument name, a value].
42
+ # Multiple rows with same argument name are acceptable.
43
+ # @return [String]
54
44
  def render_steps(aPhrase, rawData = nil)
55
45
  useTable = ! rawData.nil?
56
46
  macro = find_macro(aPhrase, useTable)
@@ -60,10 +50,24 @@ public
60
50
  return macro.expand(aPhrase, rawData)
61
51
  end
62
52
 
53
+
54
+ # Clear/remove all macro definitions from the collection.
55
+ # Post-condition: we are back to the same situation as no macro was ever defined.
56
+ def clear()
57
+ macro_steps.clear()
58
+ end
59
+
60
+
61
+ # Read accessor for the @macro_steps attribute.
62
+ def macro_steps()
63
+ @macro_steps ||= {}
64
+ return @macro_steps
65
+ end
66
+
63
67
  private
64
68
  # Retrieve the macro, given a phrase.
65
69
  def find_macro(aMacroPhrase, useTable)
66
- return @macro_steps[MacroStep::macro_key(aMacroPhrase, useTable, :invokation)]
70
+ return macro_steps[MacroStep::macro_key(aMacroPhrase, useTable, :invokation)]
67
71
  end
68
72
 
69
73
  end # class
@@ -1,52 +1,52 @@
1
- # File: macro-step-support.rb
2
-
3
- require_relative "exceptions"
4
- require_relative "macro-collection"
5
-
6
- module Macros4Cuke # Module used as a namespace
7
-
8
- # Mix-in module that should be extending World objects in Cucumber.
9
- # Synopsis (in env.rb):
10
- # require 'macros4cuke'
11
- # ...
12
- # World(Macros4Cuke::MacroStepSupport) # Extend the world object with this module.
13
- #
14
- module MacroStepSupport
15
-
16
- # Callback invoked when a World object is extend(ed) with this module.
17
- def self.extended(world)
18
- # Add & initialize an instance variable for macro support.
19
- MacroCollection::instance.init()
20
- end
21
-
22
-
23
- public
24
-
25
- # Add a new macro.
26
- # Pre-condition: there is no existing macro with the same key.
27
- # [aPhrase] The text that is enclosed between the square brackets.
28
- # [aTemplate] A text that consists of a sequence of Cucumber steps.
29
- # [useTable] A boolean that indicates whether a table should be used to pass actual values.
30
- def add_macro(aPhrase, aTemplate, useTable)
31
- MacroCollection::instance.add_macro(aPhrase, aTemplate, useTable)
32
- end
33
-
34
-
35
- # Invoke a macro with given phrase and (optionally) a table of values
36
- # [aPhrase] an instance of the macro phrase.
37
- # [rawData] An Array of couples.
38
- # Each couple is of the form: argument name, a value.
39
- # Multiple rows with same argument name are acceptable.
40
- def invoke_macro(aPhrase, rawData = nil)
41
- # Generate a text rendition of the step to be executed.
42
- rendered_steps = MacroCollection::instance.render_steps(aPhrase, rawData)
43
-
44
- # Execute the steps
45
- steps(rendered_steps)
46
- end
47
-
48
- end # module
49
-
50
- end # module
51
-
1
+ # File: macro-step-support.rb
2
+
3
+ require_relative "exceptions"
4
+ require_relative "macro-collection"
5
+
6
+ module Macros4Cuke # Module used as a namespace
7
+
8
+ # Mix-in module that should be extending World objects in Cucumber.
9
+ # Synopsis (in env.rb):
10
+ #
11
+ # require 'macros4cuke'
12
+ # ...
13
+ # World(Macros4Cuke::MacroStepSupport) # Extend the world object with this module.
14
+ #
15
+ module MacroStepSupport
16
+ public
17
+
18
+ # Add a new macro.
19
+ # Pre-condition: there is no existing macro with the same key.
20
+ # @param aPhrase [String] The text that is enclosed between the square brackets [...].
21
+ # @param aTemplate [String] The text template that consists of a sequence of sub-steps.
22
+ # @param useTable [boolean] A flag that indicates whether a table should be used to pass actual values.
23
+ def add_macro(aPhrase, aTemplate, useTable)
24
+ MacroCollection::instance.add_macro(aPhrase, aTemplate, useTable)
25
+ end
26
+
27
+
28
+ # Invoke a macro with given phrase and (optionally) a table of values
29
+ # @param aPhraseInstance [String] an instance of the macro phrase. That is, the text between [...] and with zero or more actual values.
30
+ # @param rawData [Array or nil] An array of couples. Each couple is of the form: [macro argument name, a value].
31
+ # Multiple rows with same argument name are acceptable.
32
+ def invoke_macro(aPhraseInstance, rawData = nil)
33
+ # Generate a text rendition of the step to be executed.
34
+ rendered_steps = MacroCollection::instance.render_steps(aPhraseInstance, rawData)
35
+
36
+ # Let Cucumber execute the sub-steps
37
+ steps(rendered_steps)
38
+ end
39
+
40
+
41
+ # Clear (remove) all the macro-step definitions.
42
+ # After this, we are in the same situation when no macro-step was ever defined.
43
+ def clear_macros()
44
+ MacroCollection::instance.clear()
45
+ end
46
+
47
+ end # module
48
+
49
+ end # module
50
+
51
+
52
52
  # End of file
@@ -3,14 +3,18 @@
3
3
 
4
4
 
5
5
  require_relative 'exceptions'
6
- require_relative 'template-engine'
6
+ require_relative 'templating/engine'
7
7
 
8
8
  module Macros4Cuke # Module used as a namespace
9
9
 
10
- # In essence, a macro step object represents a Cucumber step that is itself
11
- # an aggregation of lower-level Cucumber steps.
10
+ # A macro-step object is a Cucumber step that is itself
11
+ # an aggregation of lower-level sub-steps.
12
+ # When a macro-step is used in a scenario, then its execution is equivalent
13
+ # to the execution of its sub-steps.
14
+ # A macro-step may have zero or more arguments. The actual values bound to these arguments
15
+ # are passed to the sub-steps at execution time.
12
16
  class MacroStep
13
- # A template engine that expands the substeps upon request.
17
+ # A template engine that expands the sub-steps upon request.
14
18
  attr_reader(:renderer)
15
19
 
16
20
  # Unique key of the macro as derived from the macro phrase.
@@ -24,54 +28,56 @@ class MacroStep
24
28
 
25
29
 
26
30
  # Constructor.
27
- # [aMacroPhrase] The text from the macro step definition that is between the square brackets.
28
- # [theSubsteps] The source text of the steps to be expanded upon macro invokation.
29
- # [useTable] A boolean that indicates whether a data table must be used to pass actual values.
31
+ # @param aMacroPhrase[String] The text from the macro step definition that is between the square brackets.
32
+ # @param theSubsteps [String] The source text of the steps to be expanded upon macro invokation.
33
+ # @param useTable [boolean] A flag indicating whether a data table must be used to pass actual values.
30
34
  def initialize(aMacroPhrase, theSubsteps, useTable)
31
35
  @key = self.class.macro_key(aMacroPhrase, useTable, :definition)
32
-
36
+
33
37
  # Retrieve the macro arguments embedded in the phrase.
34
38
  @phrase_args = scan_arguments(aMacroPhrase, :definition)
35
-
39
+
36
40
  # Manipulate the substeps source text
37
41
  substeps_processed = preprocess(theSubsteps)
38
42
 
39
- @renderer = TemplateEngine.new(substeps_processed)
43
+ @renderer = Templating::Engine.new(substeps_processed)
40
44
  substeps_vars = renderer.variables
41
-
42
-
45
+
46
+
43
47
  @args = validate_phrase_args(@phrase_args, substeps_vars)
44
48
  @args.concat(substeps_vars)
45
49
  @args.uniq!
46
50
  end
47
-
48
-
49
- # Compute the identifier of the macro from the given macro phrase.
50
- # A macro phrase is a text that must start with a recognised verb and may contain zero or more placeholders.
51
- # In definition mode, a placeholder is delimited by chevrons <..>
52
- # In invokation mode, a placeholder is delimited by double quotes.
53
- # The rule for building the identifier are:
54
- # - Leading and trailing space(s) are removed.
55
- # - Each underscore character is removed.
56
- # - Every sequence of one or more space(s) is converted into an underscore
57
- # - Each placeholder (i.e. = delimiters + enclosed text) is converted into a letter X.
58
- # - when useTable is true, concatenate: _T
59
- # Example:
60
- # Consider the macro phrase: 'create the following "contactType" contact]:'
61
- # The resulting macro_key is: 'create_the_following_X_contact_T'
62
- # [aMacroPhrase] The text from the macro step definition that is between the square brackets.
63
- # [useTable] A boolean that indicates whether a table should be used to pass actual values.
64
- # [mode] one of the following: :definition, :invokation
51
+
52
+
53
+ # Compute the identifier of the macro from the given macro phrase.
54
+ # A macro phrase is a text that may contain zero or more placeholders.
55
+ # In definition mode, a placeholder is delimited by chevrons <..>.
56
+ # In invokation mode, a value bound to a placeholder is delimited by double quotes.
57
+ # The rule for building the identifying key are:
58
+ # - Leading and trailing space(s) are removed.
59
+ # - Each underscore character is removed.
60
+ # - Every sequence of one or more space(s) is converted into an underscore
61
+ # - Each placeholder (i.e. = delimiters + enclosed text) is converted into a letter X.
62
+ # - when useTable is true, concatenate: _T
63
+ # @example:
64
+ # Consider the macro phrase: 'create the following "contactType" contact'
65
+ # The resulting macro_key is: 'create_the_following_X_contact_T'
66
+ #
67
+ # @param aMacroPhrase [String] The text from the macro step definition that is between the square brackets.
68
+ # @param useTable [boolean] A flag indicating whether a table should be used to pass actual values.
69
+ # @param mode [:definition, :invokation]
70
+ # @return [String] the key of the phrase/macro.
65
71
  def self.macro_key(aMacroPhrase, useTable, mode)
66
72
  stripped_phrase = aMacroPhrase.strip # Remove leading ... trailing space(s)
67
-
73
+
68
74
  # Remove every underscore
69
75
  stripped_phrase.gsub!(/_/, '')
70
-
76
+
71
77
  # Replace all consecutive whitespaces by an underscore
72
78
  stripped_phrase.gsub!(/\s+/, '_')
73
-
74
-
79
+
80
+
75
81
  # Determine the pattern to isolate each argument/parameter with its delimiters
76
82
  pattern = case mode
77
83
  when :definition
@@ -80,28 +86,27 @@ class MacroStep
80
86
  /"([^\\"]|\\.)*"/
81
87
 
82
88
  end
83
-
89
+
84
90
  # Each text between quotes or chevron is replaced by the letter X
85
91
  normalized = stripped_phrase.gsub(pattern, 'X')
86
-
92
+
87
93
  key = normalized + (useTable ? '_T' : '')
88
-
94
+
89
95
  return key
90
- end
91
-
92
-
96
+ end
97
+
98
+
93
99
  # Render the steps from the template, given the values
94
100
  # taken by the parameters
95
- # [aPhrase] an instance of the macro phrase.
96
- # [rawData] An Array of couples.
97
- # Each couple is of the form: argument name, a value.
98
- # Multiple rows with same argument name are acceptable.
101
+ # @param aPhrase [String] an instance of the macro phrase.
102
+ # @param rawData [Array] An array of couples of the form: [argument name, a value].
103
+ # Multiple rows with same argument name are acceptable.
99
104
  def expand(aPhrase, rawData)
100
105
  params = validate_params(aPhrase, rawData)
101
106
  return renderer.render(nil, params)
102
107
  end
103
108
 
104
- private
109
+ private
105
110
  # Build a Hash from the given raw data.
106
111
  # [aPhrase] an instance of the macro phrase.
107
112
  # [rawData] An Array of couples.
@@ -109,7 +114,7 @@ private
109
114
  # Multiple rows with same argument name are acceptable.
110
115
  def validate_params(aPhrase, rawData)
111
116
  macro_parameters = {}
112
-
117
+
113
118
  # Retrieve the value(s) per variable in the phrase.
114
119
  quoted_values = scan_arguments(aPhrase, :invokation)
115
120
  quoted_values.each_with_index do |val, index|
@@ -1,37 +1,59 @@
1
- # File: template-engine.rb
1
+ # File: engine.rb
2
2
  # Purpose: Implementation of the MacroStep class.
3
3
 
4
4
  require 'strscan' # Use the StringScanner for lexical analysis.
5
- require_relative 'exceptions' # Load the
5
+ require_relative '../exceptions' # Load the custom exception classes.
6
+
6
7
 
7
8
  module Macros4Cuke # Module used as a namespace
8
9
 
9
10
 
11
+ # Module containing all classes implementing the simple template engine
12
+ # used internally in Macros4Cuke.
13
+ module Templating
10
14
 
11
- class StaticRep
15
+ # Class used internally by the template engine.
16
+ # Represents a static piece of text from a template.
17
+ # A static text is a text that is reproduced verbatim when rendering a template.
18
+ class StaticText
19
+ # The static text extracted from the original template.
12
20
  attr_reader(:source)
13
21
 
22
+
23
+ # @param aSourceText [String] A piece of text extracted from the template that must be rendered verbatim.
14
24
  def initialize(aSourceText)
15
25
  @source = aSourceText
16
26
  end
17
-
27
+
18
28
  public
19
- def render(aContextObject = nil, theLocals)
29
+
30
+ # Render the static text.
31
+ # This method has the same signature as the {Engine#render} method.
32
+ # @return [String] Static text is returned verbatim ("as is")
33
+ def render(aContextObject, theLocals)
20
34
  return source
21
35
  end
22
36
  end # class
23
37
 
24
38
 
25
- class VariableRep
39
+ # Class used internally by the template engine.
40
+ # Represents a named placeholder in a template, that is,
41
+ # a name placed between <..> in the template.
42
+ # At rendition, a placeholder is replaced by the text value that is associated with it.
43
+ class Placeholder
44
+ # The name of the placeholder/variable.
26
45
  attr_reader(:name)
27
46
 
47
+ # @param aVarName [String] The name of the placeholder from a template.
28
48
  def initialize(aVarName)
29
49
  @name = aVarName
30
50
  end
31
-
51
+
32
52
  public
33
- # The signature of this method should comply to the Tilt API.
34
- # Actual values from the 'locals' Hash take precedence over the context object.
53
+ # Render the placeholder given the passed arguments.
54
+ # This method has the same signature as the {Engine#render} method.
55
+ # @return [String] The text value assigned to the placeholder.
56
+ # Returns an empty string when no value is assigned to the placeholder.
35
57
  def render(aContextObject, theLocals)
36
58
  actual_value = theLocals[name]
37
59
  if actual_value.nil?
@@ -41,59 +63,74 @@ public
41
63
 
42
64
  return actual_value.is_a?(String) ? actual_value : actual_value.to_s
43
65
  end
44
-
66
+
45
67
  end # class
46
68
 
47
69
 
48
- class EOLRep
70
+ # Class used internally by the template engine.
71
+ # Represents an end of line that must be rendered as such.
72
+ class EOLine
49
73
  public
74
+ # Render an end of line.
75
+ # This method has the same signature as the {Engine#render} method.
76
+ # @return [String] An end of line marker. Its exact value is OS-dependent.
50
77
  def render(aContextObject, theLocals)
51
78
  return "\n"
52
79
  end
53
80
  end # class
54
81
 
55
82
 
56
- # A very simple implementation of a templating engine.
57
- class TemplateEngine
83
+ # A very simple implementation of a templating engine.
84
+ # Earlier versions of Macros4Cuke relied on the logic-less Mustache template engine.
85
+ # But it was decided afterwards to replace it by a very simple template engine.
86
+ # The reasons were the following:
87
+ # - Be closer to the usual Gherkin syntax (parameters of scenario outlines use chevrons <...>,
88
+ # while Mustache use !{{...}} delimiters),
89
+ # - Feature files are meant to be simple, so should the template engine be.
90
+ class Engine
58
91
  # The original text of the template is kept here.
59
92
  attr_reader(:source)
60
93
 
61
- # Constructor.
62
- # [aSourceTemplate] the template source text. It may contain zero or tags enclosed between chevrons <...>.
94
+ # Builds an Engine and compiles the given template text into an internal representation.
95
+ # @param aSourceTemplate [String] The template source text. It may contain zero or tags enclosed between chevrons <...>.
63
96
  def initialize(aSourceTemplate)
64
97
  @source = aSourceTemplate
65
98
  @representation = compile(aSourceTemplate)
66
99
  end
67
100
 
68
101
  public
69
- # [aContextObject] context object to get actual values (when not present in the Hash parameter).
70
- # [theLocals] a Hash with pairs of the form: argument name => actual value.
71
- def render(aContextObject = nil, theLocals)
102
+ # Render the template within the given scope object and with the locals specified.
103
+ # The method mimicks the signature of the Tilt::Template#render method.
104
+ # @param aContextObject [anything] context object to get actual values (when not present in the locals Hash).
105
+ # @param theLocals [Hash] Contains one or more pairs of the form: tag/placeholder name => actual value.
106
+ # @return [String] The rendition of the template given the passed argument values.
107
+ def render(aContextObject = Object.new, theLocals)
72
108
  return '' if @representation.empty?
73
- context = aContextObject.nil? ? Object.new : aContextObject
74
109
 
75
110
  result = @representation.each_with_object('') do |element, subResult|
76
- subResult << element.render(context, theLocals)
111
+ subResult << element.render(aContextObject, theLocals)
77
112
  end
78
113
 
79
114
  return result
80
115
  end
116
+
81
117
 
82
- # Return all variable names that appear in the template.
118
+ # Retrieve all placeholder names that appear in the template.
119
+ # @return [Array] The list of placeholder names.
83
120
  def variables()
84
121
  # The result will be cached/memoized...
85
122
  @variables ||= begin
86
- vars = @representation.select { |element| element.is_a?(VariableRep) }
123
+ vars = @representation.select { |element| element.is_a?(Placeholder) }
87
124
  vars.map(&:name)
88
125
  end
89
126
 
90
127
  return @variables
91
128
  end
92
129
 
93
- # Class method. Parse the given line text.
94
- # Returns an array of couples.
95
- # Couples are of the form:
96
- # [:static, text] or [:dynamic, text]
130
+
131
+ # Class method. Parse the given line text into a raw representation.
132
+ # @return [Array] Couples of the form:
133
+ # [:static, text] or [:dynamic, tag text]
97
134
  def self.parse(aTextLine)
98
135
  scanner = StringScanner.new(aTextLine)
99
136
  result = []
@@ -157,7 +194,7 @@ private
157
194
  # Convert the array of raw entries into full-fledged template elements.
158
195
  def compile_line(aRawLine)
159
196
  line_rep = aRawLine.map { |couple| compile_couple(couple) }
160
- line_rep << EOLRep.new
197
+ line_rep << EOLine.new
161
198
  end
162
199
 
163
200
 
@@ -168,9 +205,10 @@ private
168
205
 
169
206
  result = case kind
170
207
  when :static
171
- StaticRep.new(text)
208
+ StaticText.new(text)
209
+
172
210
  when :dynamic
173
- VariableRep.new(text)
211
+ Placeholder.new(text)
174
212
  else
175
213
  raise StandardError, "Internal error: Don't know template element of kind #{kind}"
176
214
  end
@@ -182,4 +220,6 @@ end # class
182
220
 
183
221
  end # module
184
222
 
223
+ end # module
224
+
185
225
  # End of file
@@ -8,20 +8,15 @@ module Macros4Cuke # Open this namespace to get rid of module qualifier prefixes
8
8
 
9
9
  describe MacroCollection do
10
10
 
11
- before(:all) do
12
- # Initialize the sole instance
13
- MacroCollection.instance.init()
14
- end
15
-
16
11
  let(:singleton) { MacroCollection.instance() }
17
-
12
+
18
13
  context "Initialization:" do
19
14
  it "should be empty" do
20
15
  singleton.macro_steps.should be_empty
21
16
  end
22
-
17
+
23
18
  end
24
-
19
+
25
20
  context "Provided services:" do
26
21
  let(:sample_substeps) do
27
22
  snippet = <<-SNIPPET
@@ -32,20 +27,20 @@ describe MacroCollection do
32
27
  And I click "Submit"
33
28
  SNIPPET
34
29
 
35
- snippet
30
+ snippet
36
31
  end
37
32
 
38
33
  it "should accept the addition of a new macro-step" do
39
34
  phrase = "[enter my credentials]"
40
35
  lambda { singleton.add_macro(phrase, sample_substeps, true)}.should_not raise_error
41
36
  singleton.should have(1).macro_steps
42
-
37
+
43
38
  # Error case: inserting another macro with same phrase.
44
39
  error_message = "A macro-step with phrase '[enter my credentials]' already exist."
45
40
  lambda { singleton.add_macro(phrase, sample_substeps, true) }.should raise_error(Macros4Cuke::DuplicateMacroError, error_message)
46
41
  end
47
42
  end
48
-
43
+
49
44
  end # describe
50
45
 
51
46
  end # module
@@ -0,0 +1,50 @@
1
+ # File: macro-step_spec.rb
2
+
3
+ require_relative '../spec_helper'
4
+ require_relative '../../lib/macros4cuke/macro-step-support' # The class under test
5
+
6
+
7
+ module Macros4Cuke # Open the module to avoid lengthy qualified names
8
+
9
+ # Class created just for testing purposes.
10
+ class MyWorld
11
+ include Macros4Cuke::MacroStepSupport
12
+ end # class
13
+
14
+ describe MacroStepSupport do
15
+ # Rule to build a bland world object
16
+ let(:world) do
17
+ w = Object.new
18
+ w.extend(Macros4Cuke::MacroStepSupport)
19
+ w
20
+ end
21
+
22
+ context "Defining macro(s):" do
23
+ let(:m1_phrase) { "enter the credentials" }
24
+ let(:m1_substeps) do
25
+ ssteps = <<-SNIPPET
26
+ Given I landed in the homepage
27
+ When I click "Sign in"
28
+ And I fill in "Username" with "<userid>"
29
+ And I fill in "Password" with "<password>"
30
+ And I click "Submit"
31
+ SNIPPET
32
+ ssteps
33
+ end
34
+ it "should add valid new macro" do
35
+ lambda { world.add_macro(m1_phrase, m1_substeps, true) }.should_not raise_error
36
+ end
37
+
38
+ it "should complain when entering the same macro again" do
39
+ # Error case: trying to register another macro with same key/phrase.
40
+ error_message = "A macro-step with phrase 'enter the credentials' already exist."
41
+ lambda { world.add_macro(m1_phrase, m1_substeps, true) }.should raise_error(Macros4Cuke::DuplicateMacroError, error_message)
42
+ end
43
+ end # context
44
+
45
+ end # describe
46
+
47
+ end # module
48
+
49
+
50
+ # End of file
@@ -10,7 +10,7 @@ module Macros4Cuke # Open the module to avoid lengthy qualified names
10
10
 
11
11
  describe MacroStep do
12
12
  let(:sample_phrase) { "enter my credentials as <userid>" }
13
-
13
+
14
14
  let(:sample_template) do
15
15
  snippet = <<-SNIPPET
16
16
  Given I landed in the homepage
@@ -29,42 +29,42 @@ end
29
29
 
30
30
  context "Creation & initialization" do
31
31
  it "should be created with a phrase, substeps and a table use indicator" do
32
- lambda { MacroStep.new(sample_phrase, sample_template, true) }.should_not raise_error
32
+ lambda { MacroStep.new(sample_phrase, sample_template, true) }.should_not raise_error
33
33
  end
34
-
35
-
34
+
35
+
36
36
  it "should complain when a sub-step argument can never be assigned a value via the phrase" do
37
37
  error_message = "The sub-step argument 'password' does not appear in the phrase."
38
- lambda { MacroStep.new(sample_phrase, sample_template, false) }.should raise_error(Macros4Cuke::UnreachableSubstepArgument, error_message)
38
+ lambda { MacroStep.new(sample_phrase, sample_template, false) }.should raise_error(Macros4Cuke::UnreachableSubstepArgument, error_message)
39
39
  end
40
-
41
-
40
+
41
+
42
42
  it "should complain when an argument from the phrase never occurs in a substep" do
43
43
  a_phrase = "enter my credentials as <foobar>"
44
44
  error_message = "The phrase argument 'foobar' does not appear in a sub-step."
45
- lambda { MacroStep.new(a_phrase, sample_template, true) }.should raise_error(Macros4Cuke::UselessPhraseArgument, error_message)
46
- end
45
+ lambda { MacroStep.new(a_phrase, sample_template, true) }.should raise_error(Macros4Cuke::UselessPhraseArgument, error_message)
46
+ end
47
+
47
48
 
48
-
49
49
  it "should know its key" do
50
50
  subject.key.should == "enter_my_credentials_as_X_T"
51
51
  end
52
-
52
+
53
53
  it "should know the tags(placeholders) from its phrase" do
54
54
  subject.phrase_args.should == %w[userid]
55
55
  end
56
-
56
+
57
57
  it "should know the tags(placeholders) from its phrase and template" do
58
- subject.args.should == %w[userid password]
58
+ subject.args.should == %w[userid password]
59
59
  end
60
-
60
+
61
61
  end # context
62
-
63
-
62
+
63
+
64
64
  context "Provided services" do
65
-
65
+
66
66
  let(:phrase_instance) {%Q|enter my credentials as "nobody"|}
67
-
67
+
68
68
  it "should render the substeps" do
69
69
  text = subject.expand(phrase_instance, [ ['password', 'no-secret'] ])
70
70
  expectation = <<-SNIPPET
@@ -74,10 +74,10 @@ end
74
74
  And I fill in "Password" with "no-secret"
75
75
  And I click "Submit"
76
76
  SNIPPET
77
-
77
+
78
78
  text.should == expectation
79
79
  end
80
-
80
+
81
81
  it "should render steps even when one argument has no actual value" do
82
82
  # Special case: password has no value...
83
83
  text = subject.expand(phrase_instance, [ ])
@@ -88,10 +88,10 @@ SNIPPET
88
88
  And I fill in "Password" with ""
89
89
  And I click "Submit"
90
90
  SNIPPET
91
-
92
- text.should == expectation
91
+
92
+ text.should == expectation
93
93
  end
94
-
94
+
95
95
  it "should complain when an unknown variable is used" do
96
96
  # Error case: there is no macro argument called <unknown>
97
97
  error_message = "Unknown macro-step argument 'unknown'."
@@ -99,7 +99,7 @@ SNIPPET
99
99
  end
100
100
 
101
101
  end # context
102
-
102
+
103
103
 
104
104
  end # describe
105
105
 
@@ -1,12 +1,15 @@
1
1
  # encoding: utf-8 -- You should see a paragraph character: §
2
- # File: template-engine_spec.rb
2
+ # File: engine_spec.rb
3
3
 
4
- require_relative '../spec_helper'
5
- require_relative '../../lib/macros4cuke/template-engine' # Load the class under test
4
+ require_relative '../../spec_helper'
5
+ require_relative '../../../lib/macros4cuke/templating/engine' # Load the class under test
6
6
 
7
- module Macros4Cuke # Open this namespace to get rid of module qualifier prefixes
7
+ module Macros4Cuke
8
8
 
9
- describe TemplateEngine do
9
+ module Templating # Open this namespace to get rid of module qualifier prefixes
10
+
11
+
12
+ describe Engine do
10
13
  let(:sample_template) do
11
14
  source = <<-SNIPPET
12
15
  Given I landed in the homepage
@@ -17,29 +20,25 @@ describe TemplateEngine do
17
20
  SNIPPET
18
21
  end
19
22
 
20
- # Rule for default instantiation
21
- subject { TemplateEngine.new sample_template }
23
+ # Rule for default instantiation
24
+ subject { Engine.new sample_template }
22
25
 
23
26
 
24
27
  context "Class services" do
25
- # Convenience method used to shorten method call.
26
- def parse_it(aText)
27
- return TemplateEngine::parse(aText)
28
- end
29
-
30
- # remove enclosing chevrons <..> (if any)
28
+ # Helper method.
29
+ # Remove enclosing chevrons <..> (if any)
31
30
  def strip_chevrons(aText)
32
31
  return aText.gsub(/^<|>$/, '')
33
32
  end
34
33
 
35
34
  it "should parse an empty text line" do
36
35
  # Expectation: result should be an empty array.
37
- parse_it('').should be_empty
36
+ Engine::parse('').should be_empty
38
37
  end
39
38
 
40
39
  it "should parse a text line without tag" do
41
40
  sample_text = 'Mary has a little lamb'
42
- result = parse_it(sample_text)
41
+ result = Engine::parse(sample_text)
43
42
 
44
43
  # Expectation: an array with one couple: [:static, the source text]
45
44
  result.should have(1).items
@@ -48,7 +47,7 @@ SNIPPET
48
47
 
49
48
  it "should parse a text line that consists of just a tag" do
50
49
  sample_text = '<some_tag>'
51
- result = parse_it(sample_text)
50
+ result = Engine::parse(sample_text)
52
51
 
53
52
  # Expectation: an array with one couple: [:static, the source text]
54
53
  result.should have(1).items
@@ -57,7 +56,7 @@ SNIPPET
57
56
 
58
57
  it "should parse a text line with a tag at the start" do
59
58
  sample_text = '<some_tag>some text'
60
- result = parse_it(sample_text)
59
+ result = Engine::parse(sample_text)
61
60
 
62
61
  # Expectation: an array with two couples: [dynamic, 'some_tag'][:static, some text]
63
62
  result.should have(2).items
@@ -67,7 +66,7 @@ SNIPPET
67
66
 
68
67
  it "should parse a text line with a tag at the end" do
69
68
  sample_text = 'some text<some_tag>'
70
- result = parse_it(sample_text)
69
+ result = Engine::parse(sample_text)
71
70
 
72
71
  # Expectation: an array with two couples: [:static, some text] [dynamic, 'some_tag']
73
72
  result.should have(2).items
@@ -77,7 +76,7 @@ SNIPPET
77
76
 
78
77
  it "should parse a text line with a tag in the middle" do
79
78
  sample_text = 'begin <some_tag> end'
80
- result = parse_it(sample_text)
79
+ result = Engine::parse(sample_text)
81
80
 
82
81
  # Expectation: an array with three couples:
83
82
  result.should have(3).items
@@ -88,7 +87,7 @@ SNIPPET
88
87
 
89
88
  it "should parse a text line with two tags in the middle" do
90
89
  sample_text = 'begin <some_tag>middle<another_tag> end'
91
- result = parse_it(sample_text)
90
+ result = Engine::parse(sample_text)
92
91
 
93
92
  # Expectation: an array with items couples:
94
93
  result.should have(5).items
@@ -100,7 +99,7 @@ SNIPPET
100
99
 
101
100
  # Case: two consecutive tags
102
101
  sample_text = 'begin <some_tag><another_tag> end'
103
- result = parse_it(sample_text)
102
+ result = Engine::parse(sample_text)
104
103
 
105
104
  # Expectation: an array with four couples:
106
105
  result.should have(4).items
@@ -112,7 +111,7 @@ SNIPPET
112
111
 
113
112
  it "should parse a text line with escaped chevrons" do
114
113
  sample_text = 'Mary has a \<little\> lamb'
115
- result = parse_it(sample_text)
114
+ result = Engine::parse(sample_text)
116
115
 
117
116
  # Expectation: an array with one couple: [:static, the source text]
118
117
  result.should have(1).items
@@ -121,7 +120,7 @@ SNIPPET
121
120
 
122
121
  it "should parse a text line with escaped chevrons in a tag" do
123
122
  sample_text = 'begin <some_\<\\>weird\>_tag> end'
124
- result = parse_it(sample_text)
123
+ result = Engine::parse(sample_text)
125
124
 
126
125
  # Expectation: an array with three couples:
127
126
  result.should have(3).items
@@ -133,7 +132,7 @@ SNIPPET
133
132
  it "should complain if a tag misses an closing chevron" do
134
133
  sample_text = 'begin <some_tag\> end'
135
134
  error_message = "Missing closing chevron '>'."
136
- lambda { parse_it(sample_text) }.should raise_error(StandardError, error_message)
135
+ lambda { Engine::parse(sample_text) }.should raise_error(StandardError, error_message)
137
136
  end
138
137
 
139
138
  end # context
@@ -141,43 +140,43 @@ SNIPPET
141
140
  context "Creation and initialization" do
142
141
 
143
142
  it "should accept an empty template text" do
144
- lambda { TemplateEngine.new '' }.should_not raise_error
143
+ lambda { Engine.new '' }.should_not raise_error
145
144
  end
146
145
 
147
146
  it "should be created with a template text" do
148
- lambda { TemplateEngine.new sample_template }.should_not raise_error
147
+ lambda { Engine.new sample_template }.should_not raise_error
149
148
  end
150
149
 
151
150
  it "should know the source text" do
152
151
  subject.source.should == sample_template
153
152
 
154
153
  # Case of an empty template
155
- instance = TemplateEngine.new ''
154
+ instance = Engine.new ''
156
155
  instance.source.should be_empty
157
156
  end
158
-
157
+
159
158
  it "should complain when a placeholder is empty or blank" do
160
159
  text_w_empty_arg = sample_template.sub(/userid/, '')
161
160
  error_message = %Q|An empty or blank argument occurred in 'And I fill in "Username" with "<>"'.|
162
- lambda { TemplateEngine.new text_w_empty_arg }.should raise_error(Macros4Cuke::EmptyArgumentError, error_message)
163
-
161
+ lambda { Engine.new text_w_empty_arg }.should raise_error(Macros4Cuke::EmptyArgumentError, error_message)
162
+
164
163
  end
165
164
  end
166
-
165
+
167
166
  context "Provided services" do
168
-
167
+
169
168
  it "should know the variable(s) it contains" do
170
169
  subject.variables == [:userid, :password]
171
170
 
172
171
  # Case of an empty source template text
173
- instance = TemplateEngine.new ''
172
+ instance = Engine.new ''
174
173
  instance.variables.should be_empty
175
174
  end
176
-
175
+
177
176
  it "should render the text given the actuals" do
178
177
  locals = {'userid' => "johndoe"}
179
-
180
- rendered_text = subject.render(nil, locals)
178
+
179
+ rendered_text = subject.render(Object.new, locals)
181
180
  expected = <<-SNIPPET
182
181
  Given I landed in the homepage
183
182
  # The credentials are entered here
@@ -197,10 +196,10 @@ SNIPPET
197
196
  And I fill in "Password" with "holmes"
198
197
  And I click "Sign in"
199
198
  SNIPPET
200
-
199
+
201
200
 
202
201
  # Case of an empty source template text
203
- instance = TemplateEngine.new ''
202
+ instance = Engine.new ''
204
203
  instance.render(nil, {}).should be_empty
205
204
  end
206
205
  end
@@ -209,4 +208,6 @@ end # describe
209
208
 
210
209
  end # module
211
210
 
211
+ end # module
212
+
212
213
  # End of file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: macros4cuke
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.13
4
+ version: 0.2.14
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-05-02 00:00:00.000000000 Z
12
+ date: 2013-05-03 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: cucumber
@@ -49,6 +49,7 @@ executables: []
49
49
  extensions: []
50
50
  extra_rdoc_files: []
51
51
  files:
52
+ - .yardopts
52
53
  - cucumber.yml
53
54
  - CHANGELOG.md
54
55
  - LICENSE.txt
@@ -60,7 +61,7 @@ files:
60
61
  - lib/macros4cuke/macro-collection.rb
61
62
  - lib/macros4cuke/macro-step-support.rb
62
63
  - lib/macros4cuke/macro-step.rb
63
- - lib/macros4cuke/template-engine.rb
64
+ - lib/macros4cuke/templating/engine.rb
64
65
  - features/demo01.feature
65
66
  - features/demo02.feature
66
67
  - features/demo03.feature
@@ -72,8 +73,9 @@ files:
72
73
  - features/support/macro_support.rb
73
74
  - spec/spec_helper.rb
74
75
  - spec/macros4cuke/macro-collection_spec.rb
76
+ - spec/macros4cuke/macro-step-support_spec.rb
75
77
  - spec/macros4cuke/macro-step_spec.rb
76
- - spec/macros4cuke/template-engine_spec.rb
78
+ - spec/macros4cuke/templating/engine_spec.rb
77
79
  homepage: https://github.com/famished-tiger/Macros4Cuke
78
80
  licenses:
79
81
  - MIT