macros4cuke 0.1.07 → 0.2.00

Sign up to get free protection for your applications and to get access to all the features.
data/HISTORY.md CHANGED
@@ -1,3 +1,15 @@
1
+ ## [0.2.00] Version number was bumped
2
+ ### Changes:
3
+ * Replaced the Mustache template engine by own lightweight TemplateEngine
4
+ * Macro-step arguments have a syntax that is closer to Gherkin's scenario outlines.
5
+ * Upgraded TemplateEngine class to become a simple templating system.
6
+ * Remove dependency to mustache gem.
7
+
8
+ ### Documentation:
9
+ * All demos updated to new syntax.
10
+ * Updated the examples in README.md
11
+
12
+
1
13
  ## [0.1.07]
2
14
  ### Changes:
3
15
  * Added new class TemplateEngine.
data/README.md CHANGED
@@ -19,19 +19,18 @@ To each macro-step, it is possible to associate a sequence of sub-steps
19
19
  ## Synopsis ##
20
20
  Here is an example taken from our demo files:
21
21
  ```cucumber
22
- Given I define the step "When I [enter my userid {{userid}} and password {{password}}]" to mean:
22
+ Given I define the step "When I [enter my userid <userid> and password <password>]" to mean:
23
23
  """
24
24
  Given I landed in the homepage
25
25
  When I click "Sign in"
26
- And I fill in "Username" with "{{userid}}"
27
- And I fill in "Password" with "{{password}}"
26
+ And I fill in "Username" with "<userid>"
27
+ And I fill in "Password" with "<password>"
28
28
  And I click "Submit"
29
29
  """
30
30
  ```
31
31
 
32
- Notice how the arguments are enclosed between curly braces {{..}}. In its current incarnation,
33
- Macros4Cuke relies on the [Mustache](http://mustache.github.io/mustache.5.html) template engine
34
- for generating the sub-steps.
32
+ Notice how the arguments _userid_ and _password_ are enclosed between chevrons (angle brackets) <...>.
33
+
35
34
 
36
35
  That macro-step can then be used in a scenario like this:
37
36
  ```cucumber
@@ -73,7 +72,7 @@ World(Macros4Cuke::MacroStepSupport)
73
72
 
74
73
  * Step 3: Import the macro-management steps
75
74
  In your /features/step_definitions/ folder:
76
- - Create a ruby file (say, 'use\_macro\_steps.rb') with the following line:
75
+ - Create a Ruby file (say, 'use\_macro\_steps.rb') with the following line:
77
76
 
78
77
  ```ruby
79
78
  require 'macros4cuke/../macro_steps'
@@ -9,12 +9,12 @@ Scenario: Creating a basic scenario with one argument
9
9
  # The next step creates a macro(-step)
10
10
  # The syntax of the new macro-step is specified between the double quotes.
11
11
  # The steps to execute when the macro is used/invoked are listed in the multiline triple quotes arguments.
12
- # The macro argument is put between double(triple) curly braces {{...}} as required by the Mustache template library.
13
- Given I define the step "When I [log in as {{userid}}]" to mean:
12
+ # The macro argument is put between chevrons <...>.
13
+ Given I define the step "When I [log in as <userid>]" to mean:
14
14
  """
15
15
  Given I landed in the homepage
16
16
  When I click "Sign in"
17
- And I fill in "Username" with "{{userid}}"
17
+ And I fill in "Username" with "<userid>"
18
18
  And I fill in "Password" with "unguessable"
19
19
  And I click "Submit"
20
20
  """
@@ -8,13 +8,13 @@ Feature: Show the use of a basic macro with multiple arguments
8
8
  Scenario: defining basic macro with multiple arguments
9
9
  # The next step creates a macro(-step)double quotes.
10
10
  # The steps to execute when the macro is used/invoked are listed in the multiline triple quotes arguments.
11
- # The macro argument is put between double(triple) curly braces {{...}} as required by the Mustache template library.
12
- Given I define the step "When I [enter my userid {{userid}} and password {{password}}]" to mean:
11
+ # The macro-step arguments are put between chevrons <...>.
12
+ Given I define the step "When I [enter my userid <userid> and password <password>]" to mean:
13
13
  """
14
14
  Given I landed in the homepage
15
15
  When I click "Sign in"
16
- And I fill in "Username" with "{{userid}}"
17
- And I fill in "Password" with "{{password}}"
16
+ And I fill in "Username" with "<userid>"
17
+ And I fill in "Password" with "<password>"
18
18
  And I click "Submit"
19
19
  """
20
20
 
@@ -35,7 +35,7 @@ Invoked step: ... I click "Submit"
35
35
  Scenario: A macro invoking another macro (YES, it's possible!)
36
36
  Given I define the step "When I [enter my credentials]" to mean:
37
37
  """
38
- {{! Notice that the next step is invoking the first macro above}}
38
+ # Notice that the next step is invoking the first macro above
39
39
  When I [enter my userid "guest" and password "unguessable"]
40
40
  """
41
41
 
@@ -9,13 +9,13 @@ Scenario: Defining a macro to be used with multiple arguments in a table
9
9
  # The next step creates a macro(-step)
10
10
  # The syntax of the new macro-step is specified between double quotes.
11
11
  # The steps to execute when the macro is used/invoked are listed in the multiline triple quotes arguments.
12
- # The macro argument is put between double(triple) curly braces {{...}} as required by the Mustache template library.
12
+ # The macro arguments are put between chevrons <...>.
13
13
  Given I define the step "When I [enter my credentials as]:" to mean:
14
14
  """
15
15
  Given I landed in the homepage
16
16
  When I click "Sign in"
17
- And I fill in "Username" with "{{userid}}"
18
- And I fill in "Password" with "{{password}}"
17
+ And I fill in "Username" with "<userid>"
18
+ And I fill in "Password" with "<password>"
19
19
  And I click "Submit"
20
20
  """
21
21
 
@@ -6,10 +6,10 @@ Feature: Show -visually- the several ways to use macros
6
6
 
7
7
 
8
8
  Scenario: Definition of a simple macro-step with two arguments
9
- Given I define the step "When I [travel from {{origin}} to {{destination}}]" to mean:
9
+ Given I define the step "When I [travel from <origin> to <destination>]" to mean:
10
10
  """
11
- When I leave {{origin}}
12
- And I arrive in {{destination}}
11
+ When I leave <origin>
12
+ And I arrive in <destination>
13
13
  """
14
14
 
15
15
  Scenario: Do a simple travel
@@ -23,16 +23,15 @@ Scenario: Do a simple travel
23
23
  # Actual values can have embedded double quotes provided they are escaped.
24
24
  When I [travel from "Tampa" to "\"Little Italy\""]
25
25
 
26
- # Notice the HTML-escaping done by Mustache
27
26
 
28
27
 
29
28
 
30
29
  Scenario: Defining a macro calling other macro(s)
31
- Given I define the step "When I [travel from {{origin}} to {{destination}} and back]" to mean:
30
+ Given I define the step "When I [travel from <origin> to <destination> and back]" to mean:
32
31
  """
33
32
  # The next two steps are, in fact, macro-step invokations
34
- When I [travel from "{{origin}}" to "{{destination}}"]
35
- When I [travel from "{{destination}}" to "{{origin}}"]
33
+ When I [travel from "<origin>" to "<destination>"]
34
+ When I [travel from "{{destination}}" to "<origin>"]
36
35
  """
37
36
 
38
37
  Scenario: Do a travel back and forth
@@ -48,12 +47,12 @@ Scenario: Do a travel back and forth
48
47
  Scenario: Defining a macro that requires a data table
49
48
  Given I define the step "When I [fill in the form with]:" to mean:
50
49
  """
51
- When I type "{{firstname}}"
52
- And I type "{{lastname}}"
53
- And I type "{{street_address}}"
54
- And I type "{{postcode}}"
55
- And I type "{{city}}"
56
- And I type "{{country}}"
50
+ When I type "<firstname>"
51
+ And I type "<lastname>"
52
+ And I type "<street_address>"
53
+ And I type "<postcode>"
54
+ And I type "<city>"
55
+ And I type "<country>"
57
56
  """
58
57
 
59
58
  Scenario: Using a macro-step with a data table
@@ -96,10 +95,10 @@ Scenario: Demonstrate that it is possible to use a sub-step with a data table
96
95
  Given I define the step "When I [fill in, as a Londonian, the form with]:" to mean:
97
96
  """
98
97
  When I [fill in the form with]:
99
- |firstname| {{firstname}}|
100
- |lastname | {{lastname}} |
101
- |street_address| {{street_address}}|
102
- |postcode|{{postcode}} |
98
+ |firstname| <firstname>|
99
+ |lastname | <lastname> |
100
+ |street_address| <street_address>|
101
+ |postcode|<postcode> |
103
102
  # The next two lines have hard-coded values
104
103
  |city |London |
105
104
  |country | U.K. |
data/lib/macro_steps.rb CHANGED
@@ -5,11 +5,11 @@
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
  # """
@@ -3,7 +3,7 @@
3
3
 
4
4
  module Macros4Cuke # Module used as a namespace
5
5
  # This constant keeps the current version of the gem.
6
- Version = '0.1.07'
6
+ Version = '0.2.00'
7
7
 
8
8
  Description = "Macros for Cucumber"
9
9
 
@@ -1,243 +1,174 @@
1
- # File: macro-step.rb
2
- # Purpose: Implementation of the MacroStep class.
3
-
4
- require 'mustache' # Load the Mustache template library
5
-
6
- module Macros4Cuke # Module used as a namespace
7
-
8
- # In essence, a macro step object represents a Cucumber step that is itself
9
- # an aggregation of lower-level Cucumber steps.
10
- class MacroStep
11
- # A Mustache instance that expands the steps upon request.
12
- attr_reader(:renderer)
13
-
14
- # Name of the macro as derived from the macro phrase.
15
- attr_reader(:name)
16
-
17
- # The list of macro arguments that appears in the macro phrase.
18
- attr_reader(:phrase_args)
19
-
20
- # The list of macro argument names (as appearing in the Mustache template and in the macro phrase).
21
- attr_reader(:args)
22
-
23
-
24
- # Constructor.
25
- # [aMacroPhrase] The text from the macro step definition that is between the square brackets.
26
- # [theSubsteps] The source text of the steps to be expanded upon macro invokation.
27
- def initialize(aMacroPhrase, theSubsteps)
28
- @name = self.class.macro_key(aMacroPhrase, :definition)
29
-
30
- # Retrieve the macro arguments embedded in the phrase.
31
- @phrase_args = scan_arguments(aMacroPhrase, :definition)
32
- @args = @phrase_args.dup()
33
-
34
- # Manipulate the substeps source text (e.g. remove comment lines)
35
- substeps_processed = preprocess(theSubsteps)
36
-
37
- # The expansion text is a Mustache template
38
- @renderer = Mustache.new
39
- renderer.template = substeps_processed
40
-
41
- # Retrieve the Mustache tag names from the template and add them as macro arguments
42
- add_tags_multi(renderer.template.tokens())
43
- @args = @args.flatten.uniq.sort
44
- end
45
-
46
-
47
- # Compute the identifier of the macro from the given macro phrase.
48
- # A macro phrase is a text that must start with a recognised verb and may contain zero or more placeholders.
49
- # In definition mode, a placeholder is delimited by double or triple mustaches (accolades)
50
- # In invokation mode, a placeholder is delimited by double quotes.
51
- # The rule for building the identifier are:
52
- # - Leading and trailing space(s) are removed.
53
- # - Each underscore character is removed.
54
- # - Every sequence of one or more space(s) is converted into an underscore
55
- # - Each placeholder (i.e. = delimiters + enclosed text) is converted into a letter X.
56
- # - The endings are transformed as follows: ] => '', ]: => _T
57
- # Example:
58
- # Consider the macro phrase: 'create the following "contactType" contact]:'
59
- # The resulting macro_key is: 'create_the_following_X_contact_T'
60
- def self.macro_key(aMacroPhrase, mode)
61
- stripped_phrase = aMacroPhrase.strip # Remove leading ... trailing space(s)
62
-
63
- # Remove every underscore
64
- stripped_phrase.gsub!(/_/, '')
65
-
66
- # Replace all consecutive whitespaces by an underscore
67
- stripped_phrase.gsub!(/\s+/, '_')
68
-
69
-
70
- # Determine the pattern to isolate each argument/parameter with its delimiters
71
- pattern = case mode
72
- when :definition
73
- /\{{2,3}[^}]*\}{2,3}/
74
- when :invokation
75
- /"([^\\"]|\\.)*"/
76
-
77
- end
78
-
79
- # Each text between quotes or mustaches is replaced by the letter X
80
- normalized = stripped_phrase.gsub(pattern, 'X')
81
-
82
- # Drop the "]" ending or replace the "]:# ending by "_T"
83
- key = if normalized.end_with?("]")
84
- normalized.chop()
85
- else
86
- normalized.sub(/\]:/, '_T')
87
- end
88
-
89
- return key
90
- end
91
-
92
-
93
- # Render the steps from the template, given the values
94
- # taken by the parameters
95
- # [macro_parameters] a Hash with pairs of the kind: macro argument name => value
96
- def expand(macro_parameters)
97
- return renderer.render(macro_parameters)
98
- end
99
-
100
-
101
- # Build a Hash from the given raw data.
102
- # [aPhrase] an instance of the macro phrase.
103
- # [rawData] An Array of couples.
104
- # Each couple is of the form: argument name, a value.
105
- # Multiple rows with same argument name are acceptable.
106
- def validate_params(aPhrase, rawData)
107
- macro_parameters = {}
108
-
109
- # Retrieve the value(s) per variable in the phrase.
110
- quoted_values = scan_arguments(aPhrase, :invokation)
111
- quoted_values.each_with_index do |val, index|
112
- macro_parameters[phrase_args[index]] = val
113
- end
114
-
115
-
116
- unless rawData.nil?
117
- rawData.each do |(key, value)|
118
- raise UnknownArgumentError.new(key) unless @args.include? key
119
- if macro_parameters.include? key
120
- if macro_parameters[key].kind_of?(Array)
121
- macro_parameters[key] << value
122
- else
123
- macro_parameters[key] = [macro_parameters[key], value]
124
- end
125
- else
126
- macro_parameters[key] = value
127
- end
128
- end
129
- end
130
-
131
- return macro_parameters
132
- end
133
-
134
-
135
-
136
- private
137
- # Retrieve from the macro phrase, all the text between "mustaches" or double quotes.
138
- # Returns an array. Each of its elements corresponds to quoted text.
139
- # Example:
140
- # aMacroPhrase = 'a "qualifier" text with "quantity" placeholders.'
141
- # Results in : ["qualifier", "quantity"]
142
- # [aMacroPhrase] A phrase
143
- # [mode] one of the following: :definition, :invokation
144
- def scan_arguments(aMacroPhrase, mode)
145
- # determine the syntax of the arguments/parameters
146
- # as a regular expression with one capturing group
147
- pattern = case mode
148
- when :definition
149
- /{{{([^}]*)}}}|{{([^}]*)}}/ # Two capturing groups!...
150
- when :invokation
151
- /"((?:[^\\"]|\\.)*)"/
152
- else
153
- raise InternalError, "Internal error: Unknown mode argument #{mode}"
154
- end
155
- raw_result = aMacroPhrase.scan(pattern)
156
- return raw_result.flatten.compact
157
- end
158
-
159
- # Return the substeps text after some transformation
160
- # [theSubstepsSource] The source text of the steps to be expanded upon macro invokation.
161
- def preprocess(theSubstepsSource)
162
- # Split text into lines
163
- lines = theSubstepsSource.split(/\r\n?|\n/)
164
-
165
- # Reject comment lines. This necessary because Cucumber::RbSupport::RbWorld#steps complains when it sees a comment.
166
- processed = lines.reject { |a_line| a_line =~ /\s*#/ }
167
-
168
- return processed.join("\n")
169
- end
170
-
171
- # Visit an array of tokens of which the first element is the :multi symbol.
172
- # Every found template variable is added to the 'args' attribute.
173
- # [tokens] An array that begins with the :multi symbol
174
- def add_tags_multi(tokens)
175
- first_token = tokens.shift
176
- unless first_token == :multi
177
- raise InternalError, "Expecting a :multi token instead of a #{first_token}"
178
- end
179
-
180
- tokens.each do |an_opcode|
181
- case an_opcode[0]
182
- when :static
183
- # Do nothing...
184
-
185
- when :mustache
186
- add_tags_mustache(an_opcode)
187
-
188
- when String
189
- #Do nothing...
190
- else
191
- raise InternalError, "Unknown Mustache token type #{an_opcode.first}"
192
- end
193
- end
194
- end
195
-
196
- # [mustache_opcode] An array with the first element being :mustache
197
- def add_tags_mustache(mustache_opcode)
198
- mustache_opcode.shift() # Drop the :mustache symbol
199
-
200
- case mustache_opcode[0]
201
- when :etag
202
- triplet = mustache_opcode[1]
203
- raise InternalError, "expected 'mustache' token instead of '#{triplet[0]}'" unless triplet[0] == :mustache
204
- raise InternalError, "expected 'fetch' token instead of '#{triplet[1]}'" unless triplet[1] == :fetch
205
- @args << triplet.last
206
-
207
- when :fetch
208
- @args << mustache_opcode.last
209
-
210
- when :section
211
- add_tags_section(mustache_opcode)
212
-
213
- else
214
- raise InternalError, "Unknown Mustache token type #{mustache_opcode.first}"
215
- end
216
- end
217
-
218
-
219
- def add_tags_section(opcodes)
220
- opcodes.shift() # Drop the :section symbol
221
-
222
- opcodes.each do |op|
223
- case op[0]
224
- when :mustache
225
- add_tags_mustache(op)
226
-
227
- when :multi
228
- add_tags_multi(op)
229
-
230
- when String
231
- return
232
- else
233
- raise InternalError, "Unknown Mustache token type #{op.first}"
234
- end
235
- end
236
- end
237
-
238
- end # class
239
-
240
- end # module
241
-
242
-
1
+ # File: macro-step.rb
2
+ # Purpose: Implementation of the MacroStep class.
3
+
4
+
5
+ require_relative 'template-engine'
6
+
7
+ module Macros4Cuke # Module used as a namespace
8
+
9
+ # In essence, a macro step object represents a Cucumber step that is itself
10
+ # an aggregation of lower-level Cucumber steps.
11
+ class MacroStep
12
+ # A template engine that expands the substeps upon request.
13
+ attr_reader(:renderer)
14
+
15
+ # Name of the macro as derived from the macro phrase.
16
+ attr_reader(:name)
17
+
18
+ # The list of macro arguments that appears in the macro phrase.
19
+ attr_reader(:phrase_args)
20
+
21
+ # The list of macro argument names (as appearing in the substeps and in the macro phrase).
22
+ attr_reader(:args)
23
+
24
+
25
+ # Constructor.
26
+ # [aMacroPhrase] The text from the macro step definition that is between the square brackets.
27
+ # [theSubsteps] The source text of the steps to be expanded upon macro invokation.
28
+ def initialize(aMacroPhrase, theSubsteps)
29
+ @name = self.class.macro_key(aMacroPhrase, :definition)
30
+
31
+ # Retrieve the macro arguments embedded in the phrase.
32
+ @phrase_args = scan_arguments(aMacroPhrase, :definition)
33
+ @args = @phrase_args.dup()
34
+
35
+ # Manipulate the substeps source text
36
+ substeps_processed = preprocess(theSubsteps)
37
+
38
+ @renderer = TemplateEngine.new(substeps_processed)
39
+ @args.concat(renderer.variables)
40
+ @args.uniq!
41
+ end
42
+
43
+
44
+ # Compute the identifier of the macro from the given macro phrase.
45
+ # A macro phrase is a text that must start with a recognised verb and may contain zero or more placeholders.
46
+ # In definition mode, a placeholder is delimited by chevrons <..>
47
+ # In invokation mode, a placeholder is delimited by double quotes.
48
+ # The rule for building the identifier are:
49
+ # - Leading and trailing space(s) are removed.
50
+ # - Each underscore character is removed.
51
+ # - Every sequence of one or more space(s) is converted into an underscore
52
+ # - Each placeholder (i.e. = delimiters + enclosed text) is converted into a letter X.
53
+ # - The endings are transformed as follows: ] => '', ]: => _T
54
+ # Example:
55
+ # Consider the macro phrase: 'create the following "contactType" contact]:'
56
+ # The resulting macro_key is: 'create_the_following_X_contact_T'
57
+ def self.macro_key(aMacroPhrase, mode)
58
+ stripped_phrase = aMacroPhrase.strip # Remove leading ... trailing space(s)
59
+
60
+ # Remove every underscore
61
+ stripped_phrase.gsub!(/_/, '')
62
+
63
+ # Replace all consecutive whitespaces by an underscore
64
+ stripped_phrase.gsub!(/\s+/, '_')
65
+
66
+
67
+ # Determine the pattern to isolate each argument/parameter with its delimiters
68
+ pattern = case mode
69
+ when :definition
70
+ /<(?:[^\\<>]|\\.)*>/
71
+ when :invokation
72
+ /"([^\\"]|\\.)*"/
73
+
74
+ end
75
+
76
+ # Each text between quotes or chevron is replaced by the letter X
77
+ normalized = stripped_phrase.gsub(pattern, 'X')
78
+
79
+ # Drop the "]" ending or replace the "]:# ending by "_T"
80
+ key = if normalized.end_with?("]")
81
+ normalized.chop()
82
+ else
83
+ normalized.sub(/\]:/, '_T')
84
+ end
85
+
86
+ return key
87
+ end
88
+
89
+
90
+ # Render the steps from the template, given the values
91
+ # taken by the parameters
92
+ # [macro_parameters] a Hash with pairs of the kind: macro argument name => value
93
+ def expand(macro_parameters)
94
+ return renderer.render(nil, macro_parameters)
95
+ end
96
+
97
+
98
+ # Build a Hash from the given raw data.
99
+ # [aPhrase] an instance of the macro phrase.
100
+ # [rawData] An Array of couples.
101
+ # Each couple is of the form: argument name, a value.
102
+ # Multiple rows with same argument name are acceptable.
103
+ def validate_params(aPhrase, rawData)
104
+ macro_parameters = {}
105
+
106
+ # Retrieve the value(s) per variable in the phrase.
107
+ quoted_values = scan_arguments(aPhrase, :invokation)
108
+ quoted_values.each_with_index do |val, index|
109
+ macro_parameters[phrase_args[index]] = val
110
+ end
111
+
112
+
113
+ unless rawData.nil?
114
+ rawData.each do |(key, value)|
115
+ raise UnknownArgumentError.new(key) unless @args.include? key
116
+ if macro_parameters.include? key
117
+ if macro_parameters[key].kind_of?(Array)
118
+ macro_parameters[key] << value
119
+ else
120
+ macro_parameters[key] = [macro_parameters[key], value]
121
+ end
122
+ else
123
+ macro_parameters[key] = value
124
+ end
125
+ end
126
+ end
127
+
128
+ return macro_parameters
129
+ end
130
+
131
+
132
+
133
+ private
134
+ # Retrieve from the macro phrase, all the text between <..> or double quotes.
135
+ # Returns an array. Each of its elements corresponds to quoted text.
136
+ # Example:
137
+ # aMacroPhrase = 'a "qualifier" text with "quantity" placeholders.'
138
+ # Results in : ["qualifier", "quantity"]
139
+ # [aMacroPhrase] A phrase
140
+ # [mode] one of the following: :definition, :invokation
141
+ def scan_arguments(aMacroPhrase, mode)
142
+ # determine the syntax of the arguments/parameters
143
+ # as a regular expression with one capturing group
144
+ pattern = case mode
145
+ when :definition
146
+ /<((?:[^\\<>]|\\.)*)>/
147
+ # /{{{([^}]*)}}}|{{([^}]*)}}/ # Two capturing groups!...
148
+ when :invokation
149
+ /"((?:[^\\"]|\\.)*)"/
150
+ else
151
+ raise InternalError, "Internal error: Unknown mode argument #{mode}"
152
+ end
153
+ raw_result = aMacroPhrase.scan(pattern)
154
+ return raw_result.flatten.compact
155
+ end
156
+
157
+ # Return the substeps text after some transformation
158
+ # [theSubstepsSource] The source text of the steps to be expanded upon macro invokation.
159
+ def preprocess(theSubstepsSource)
160
+ # Split text into lines
161
+ lines = theSubstepsSource.split(/\r\n?|\n/)
162
+
163
+ # Reject comment lines. This necessary because Cucumber::RbSupport::RbWorld#steps complains when it sees a comment.
164
+ processed = lines.reject { |a_line| a_line =~ /\s*#/ }
165
+
166
+ return processed.join("\n")
167
+ end
168
+
169
+ end # class
170
+
171
+ end # module
172
+
173
+
243
174
  # End of file
@@ -6,8 +6,88 @@ require 'strscan' # Use the StringScanner for lexical analysis.
6
6
  module Macros4Cuke # Module used as a namespace
7
7
 
8
8
 
9
+
10
+ class StaticRep
11
+ attr_reader(:source)
12
+
13
+ def initialize(aSourceText)
14
+ @source = aSourceText
15
+ end
16
+
17
+ public
18
+ def render(aContextObject = nil, theLocals)
19
+ return source
20
+ end
21
+ end # class
22
+
23
+
24
+ class VariableRep
25
+ attr_reader(:name)
26
+
27
+ def initialize(aVarName)
28
+ @name = aVarName
29
+ end
30
+
31
+ public
32
+ # The signature of this method should comply to the Tilt API.
33
+ # Actual values from the 'locals' Hash take precedence over the context object.
34
+ def render(aContextObject, theLocals)
35
+ actual_value = theLocals[name]
36
+ if actual_value.nil?
37
+ actual_value = aContextObject.send(name.to_sym) if aContextObject.respond_to?(name.to_sym)
38
+ actual_value = '' if actual_value.nil?
39
+ end
40
+
41
+ return actual_value.is_a?(String) ? actual_value : actual_value.to_s
42
+ end
43
+
44
+ end # class
45
+
46
+
47
+ class EOLRep
48
+ public
49
+ def render(aContextObject, theLocals)
50
+ return "\n"
51
+ end
52
+ end # class
53
+
54
+
9
55
  class TemplateEngine
56
+ # The original text of the template is kept here.
57
+ attr_reader(:source)
58
+
59
+ # Constructor.
60
+ # [aSourceTemplate]
61
+ def initialize(aSourceTemplate)
62
+ @source = aSourceTemplate
63
+ @representation = compile(aSourceTemplate)
64
+ end
65
+
66
+ public
67
+ def render(aContextObject = nil, theLocals)
68
+ return '' if @representation.empty?
69
+ context = aContextObject.nil? ? Object.new : aContextObject
70
+
71
+ result = @representation.each_with_object('') do |element, subResult|
72
+ subResult << element.render(context, theLocals)
73
+ end
74
+
75
+ return result
76
+ end
77
+
78
+ # Return all variable names that appear in the template.
79
+ def variables()
80
+ # The result will be cached/memoized...
81
+ @variables ||= begin
82
+ vars = @representation.select { |element| element.is_a?(VariableRep) }
83
+ vars.map(&:name)
84
+ end
85
+
86
+ return @variables
87
+ end
10
88
 
89
+ # Class method. Parse the given line text.
90
+ # Returns an array of couples.
11
91
  def self.parse(aTextLine)
12
92
  scanner = StringScanner.new(aTextLine)
13
93
  result = []
@@ -47,6 +127,43 @@ class TemplateEngine
47
127
  return result
48
128
  end
49
129
 
130
+ private
131
+ # Create the internal representation of the given template.
132
+ def compile(aSourceTemplate)
133
+ # Split the input text into lines.
134
+ input_lines = aSourceTemplate.split(/\r\n?|\n/)
135
+
136
+ # Parse the input text into raw data.
137
+ raw_lines = input_lines.map { |line| self.class.parse(line) }
138
+
139
+ compiled_lines = raw_lines.map { |line| compile_line(line) }
140
+ return compiled_lines.flatten()
141
+ end
142
+
143
+
144
+ def compile_line(aRawLine)
145
+ line_rep = aRawLine.map { |couple| compile_couple(couple) }
146
+ line_rep << EOLRep.new
147
+ end
148
+
149
+
150
+ # [aCouple] a two-element array of the form: [kind, text]
151
+ # Where kind must be one of :static, :dynamic
152
+ def compile_couple(aCouple)
153
+ (kind, text) = aCouple
154
+
155
+ result = case kind
156
+ when :static
157
+ StaticRep.new(text)
158
+ when :dynamic
159
+ VariableRep.new(text)
160
+ else
161
+ raise StandardError, "Internal error: Don't know template element of kind #{kind}"
162
+ end
163
+
164
+ return result
165
+ end
166
+
50
167
  end # class
51
168
 
52
169
  end # module
@@ -0,0 +1,60 @@
1
+ # File: macro-step_spec.rb
2
+
3
+
4
+ require_relative '../spec_helper'
5
+ require_relative '../../lib/macros4cuke/macro-step' # The class under test
6
+
7
+
8
+
9
+ module Macros4Cuke # Open the module to avoid lengthy qualified names
10
+
11
+ describe MacroStep do
12
+ let(:sample_phrase) { "enter my credentials as <userid>]:" }
13
+ let(:sample_template) do
14
+ snippet = <<-SNIPPET
15
+ Given I landed in the homepage
16
+ When I click "Sign in"
17
+ And I fill in "Username" with "<userid>"
18
+ And I fill in "Password" with "<password>"
19
+ And I click "Submit"
20
+ SNIPPET
21
+
22
+ snippet
23
+ end
24
+
25
+ # Default instantiation rule
26
+ subject { MacroStep.new(sample_phrase, sample_template) }
27
+
28
+
29
+ context "Creation & initialization" do
30
+ it "should be created with a phrase and a template" do
31
+ lambda { MacroStep.new(sample_phrase, sample_template) }.should_not raise_error
32
+ end
33
+
34
+
35
+ it "should know its key/name" do
36
+ subject.name.should == "enter_my_credentials_as_X_T"
37
+ end
38
+
39
+ it "should know the tags(placeholders) from its phrase" do
40
+ subject.phrase_args.should == %w[userid]
41
+ end
42
+
43
+ it "should know the tags(placeholders) from its phrase and template" do
44
+ subject.args.should == %w[userid password]
45
+ end
46
+
47
+ end # context
48
+
49
+
50
+ context "Provided services" do
51
+ it "should render the substeps" do
52
+ end
53
+ end # context
54
+
55
+
56
+ end # describe
57
+
58
+ end # module
59
+
60
+ # End of file
@@ -0,0 +1,205 @@
1
+ # encoding: utf-8 -- You should see a paragraph character: §
2
+ # File: template-engine_spec.rb
3
+
4
+ require_relative '../spec_helper'
5
+ require_relative '../../lib/macros4cuke/template-engine' # Load the class under test
6
+
7
+ module Macros4Cuke # Open this namespace to get rid of module qualifier prefixes
8
+
9
+ describe TemplateEngine do
10
+ let(:sample_template) do
11
+ source = <<-SNIPPET
12
+ Given I landed in the homepage
13
+ # The credentials are entered here
14
+ And I fill in "Username" with "<userid>"
15
+ And I fill in "Password" with "<password>"
16
+ And I click "Sign in"
17
+ SNIPPET
18
+ end
19
+
20
+ # Rule for default instantiation
21
+ subject { TemplateEngine.new sample_template }
22
+
23
+
24
+ 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)
31
+ def strip_chevrons(aText)
32
+ return aText.gsub(/^<|>$/, '')
33
+ end
34
+
35
+ it "should parse an empty text line" do
36
+ # Expectation: result should be an empty array.
37
+ parse_it('').should be_empty
38
+ end
39
+
40
+ it "should parse a text line without tag" do
41
+ sample_text = 'Mary has a little lamb'
42
+ result = parse_it(sample_text)
43
+
44
+ # Expectation: an array with one couple: [:static, the source text]
45
+ result.should have(1).items
46
+ result[0].should == [:static, sample_text]
47
+ end
48
+
49
+ it "should parse a text line that consists of just a tag" do
50
+ sample_text = '<some_tag>'
51
+ result = parse_it(sample_text)
52
+
53
+ # Expectation: an array with one couple: [:static, the source text]
54
+ result.should have(1).items
55
+ result[0].should == [:dynamic, strip_chevrons(sample_text)]
56
+ end
57
+
58
+ it "should parse a text line with a tag at the start" do
59
+ sample_text = '<some_tag>some text'
60
+ result = parse_it(sample_text)
61
+
62
+ # Expectation: an array with two couples: [dynamic, 'some_tag'][:static, some text]
63
+ result.should have(2).items
64
+ result[0].should == [:dynamic, 'some_tag']
65
+ result[1].should == [:static, 'some text']
66
+ end
67
+
68
+ it "should parse a text line with a tag at the end" do
69
+ sample_text = 'some text<some_tag>'
70
+ result = parse_it(sample_text)
71
+
72
+ # Expectation: an array with two couples: [:static, some text] [dynamic, 'some_tag']
73
+ result.should have(2).items
74
+ result[0].should == [:static, 'some text']
75
+ result[1].should == [:dynamic, 'some_tag']
76
+ end
77
+
78
+ it "should parse a text line with a tag in the middle" do
79
+ sample_text = 'begin <some_tag> end'
80
+ result = parse_it(sample_text)
81
+
82
+ # Expectation: an array with three couples:
83
+ result.should have(3).items
84
+ result[0].should == [:static, 'begin ']
85
+ result[1].should == [:dynamic, 'some_tag']
86
+ result[2].should == [:static, ' end']
87
+ end
88
+
89
+ it "should parse a text line with two tags in the middle" do
90
+ sample_text = 'begin <some_tag>middle<another_tag> end'
91
+ result = parse_it(sample_text)
92
+
93
+ # Expectation: an array with items couples:
94
+ result.should have(5).items
95
+ result[0].should == [:static , 'begin ']
96
+ result[1].should == [:dynamic, 'some_tag']
97
+ result[2].should == [:static , 'middle']
98
+ result[3].should == [:dynamic, 'another_tag']
99
+ result[4].should == [:static, ' end']
100
+
101
+ # Case: two consecutive tags
102
+ sample_text = 'begin <some_tag><another_tag> end'
103
+ result = parse_it(sample_text)
104
+
105
+ # Expectation: an array with four couples:
106
+ result.should have(4).items
107
+ result[0].should == [:static, 'begin ']
108
+ result[1].should == [:dynamic, 'some_tag']
109
+ result[2].should == [:dynamic, 'another_tag']
110
+ result[3].should == [:static, ' end']
111
+ end
112
+
113
+ it "should parse a text line with escaped chevrons" do
114
+ sample_text = 'Mary has a \<little\> lamb'
115
+ result = parse_it(sample_text)
116
+
117
+ # Expectation: an array with one couple: [:static, the source text]
118
+ result.should have(1).items
119
+ result[0].should == [:static, sample_text]
120
+ end
121
+
122
+ it "should parse a text line with escaped chevrons in a tag" do
123
+ sample_text = 'begin <some_\<\\>weird\>_tag> end'
124
+ result = parse_it(sample_text)
125
+
126
+ # Expectation: an array with three couples:
127
+ result.should have(3).items
128
+ result[0].should == [:static, 'begin ']
129
+ result[1].should == [:dynamic, 'some_\<\\>weird\>_tag']
130
+ result[2].should == [:static, ' end']
131
+ end
132
+
133
+ it "should complain if a tag misses an closing chevron" do
134
+ sample_text = 'begin <some_tag\> end'
135
+ error_message = "Missing closing chevron '>'."
136
+ lambda { parse_it(sample_text) }.should raise_error(StandardError, error_message)
137
+ end
138
+
139
+ end # context
140
+
141
+ context "Creation and initialization" do
142
+
143
+ it "should accept an empty template text" do
144
+ lambda { TemplateEngine.new '' }.should_not raise_error
145
+ end
146
+
147
+ it "should be created with a template text" do
148
+ lambda { TemplateEngine.new sample_template }.should_not raise_error
149
+ end
150
+
151
+ it "should know the source text" do
152
+ subject.source.should == sample_template
153
+
154
+ # Case of an empty template
155
+ instance = TemplateEngine.new ''
156
+ instance.source.should be_empty
157
+ end
158
+ end
159
+
160
+ context "Provided services" do
161
+
162
+ it "should know the variable(s) it contains" do
163
+ subject.variables == [:userid, :password]
164
+
165
+ # Case of an empty source template text
166
+ instance = TemplateEngine.new ''
167
+ instance.variables.should be_empty
168
+ end
169
+
170
+ it "should render the text given the actuals" do
171
+ locals = {'userid' => "johndoe"}
172
+
173
+ rendered_text = subject.render(nil, locals)
174
+ expected = <<-SNIPPET
175
+ Given I landed in the homepage
176
+ # The credentials are entered here
177
+ And I fill in "Username" with "johndoe"
178
+ And I fill in "Password" with ""
179
+ And I click "Sign in"
180
+ SNIPPET
181
+
182
+ # Place actual value in context object
183
+ Context = Struct.new(:userid, :password)
184
+ context = Context.new("sherlock", "holmes")
185
+ rendered_text = subject.render(context, {'userid' => 'susan'})
186
+ expected = <<-SNIPPET
187
+ Given I landed in the homepage
188
+ # The credentials are entered here
189
+ And I fill in "Username" with "susan"
190
+ And I fill in "Password" with "holmes"
191
+ And I click "Sign in"
192
+ SNIPPET
193
+
194
+
195
+ # Case of an empty source template text
196
+ instance = TemplateEngine.new ''
197
+ instance.render(nil, {}).should be_empty
198
+ end
199
+ end
200
+
201
+ end # describe
202
+
203
+ end # module
204
+
205
+ # End of file
@@ -0,0 +1,8 @@
1
+ # File: spec_helper.rb
2
+ # Purpose: utility file that is loaded by all our RSpec files
3
+
4
+
5
+ require 'rspec' # Use the RSpec framework
6
+ require 'pp' # Use pretty-print for debugging purposes
7
+
8
+ # 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.1.07
4
+ version: 0.2.00
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-04-24 00:00:00.000000000 Z
12
+ date: 2013-04-25 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rake
@@ -43,25 +43,10 @@ dependencies:
43
43
  - - ! '>='
44
44
  - !ruby/object:Gem::Version
45
45
  version: '0'
46
- - !ruby/object:Gem::Dependency
47
- name: mustache
48
- requirement: !ruby/object:Gem::Requirement
49
- none: false
50
- requirements:
51
- - - ! '>='
52
- - !ruby/object:Gem::Version
53
- version: '0'
54
- type: :runtime
55
- prerelease: false
56
- version_requirements: !ruby/object:Gem::Requirement
57
- none: false
58
- requirements:
59
- - - ! '>='
60
- - !ruby/object:Gem::Version
61
- version: '0'
62
46
  description: ! "\tMacros4Cuke is a lightweight library that adds a macro facility
63
- your Cucumber scenarios.\n In short, you can create new steps that replace a sequence
64
- of lower-level steps. \n"
47
+ your Cucumber scenarios.\n In short, you can create any new step that replaces
48
+ a sequence of lower-level steps.\n All this can be done directly in your feature
49
+ files without programming step definitions.\n"
65
50
  email: famished.tiger@yahoo.com
66
51
  executables: []
67
52
  extensions: []
@@ -88,6 +73,9 @@ files:
88
73
  - features/step_definitions/use_macro_steps.rb
89
74
  - features/support/env.rb
90
75
  - features/support/macro_support.rb
76
+ - spec/spec_helper.rb
77
+ - spec/macros4cuke/macro-step_spec.rb
78
+ - spec/macros4cuke/template-engine_spec.rb
91
79
  homepage: https://github.com/famished-tiger/Macros4Cuke
92
80
  licenses: []
93
81
  post_install_message: ! '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~