macros4cuke 0.1.07 → 0.2.00

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/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: ! '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~