smartdown 0.8.1 → 0.8.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (32) hide show
  1. data/README.md +37 -0
  2. data/lib/smartdown/engine/conditional_resolver.rb +6 -4
  3. data/lib/smartdown/engine/transition.rb +5 -6
  4. data/lib/smartdown/model/element/question/date.rb +1 -1
  5. data/lib/smartdown/model/element/question/multiple_choice.rb +1 -1
  6. data/lib/smartdown/model/element/question/salary.rb +1 -1
  7. data/lib/smartdown/model/element/question/text.rb +1 -1
  8. data/lib/smartdown/parser/base.rb +12 -0
  9. data/lib/smartdown/parser/element/conditional.rb +8 -1
  10. data/lib/smartdown/parser/element/date_question.rb +1 -0
  11. data/lib/smartdown/parser/element/multiple_choice_question.rb +1 -0
  12. data/lib/smartdown/parser/element/salary_question.rb +1 -0
  13. data/lib/smartdown/parser/element/text_question.rb +1 -0
  14. data/lib/smartdown/parser/node_transform.rb +17 -10
  15. data/lib/smartdown/parser/option_pairs_transform.rb +11 -0
  16. data/lib/smartdown/version.rb +1 -1
  17. data/spec/acceptance/flow_spec.rb +4 -0
  18. data/spec/engine/conditional_resolver_spec.rb +82 -0
  19. data/spec/engine_spec.rb +6 -1
  20. data/spec/fixtures/acceptance/animal-example-multiple/questions/question_1.txt +3 -1
  21. data/spec/fixtures/acceptance/animal-example-multiple/questions/question_2.txt +1 -1
  22. data/spec/fixtures/acceptance/animal-example-multiple/questions/question_4.txt +10 -0
  23. data/spec/parser/element/conditional_spec.rb +180 -1
  24. data/spec/parser/element/date_question_spec.rb +28 -0
  25. data/spec/parser/element/multiple_choice_question_spec.rb +39 -1
  26. data/spec/parser/element/salary_question_spec.rb +33 -4
  27. data/spec/parser/element/text_question_spec.rb +33 -4
  28. data/spec/parser/node_parser_spec.rb +4 -2
  29. data/spec/parser/option_pairs_transform_spec.rb +36 -0
  30. data/spec/support/model_builder.rb +16 -22
  31. data/spec/support_specs/model_builder_spec.rb +39 -2
  32. metadata +16 -11
data/README.md CHANGED
@@ -128,6 +128,18 @@ Asks for an arbitrary text input.
128
128
  [salary: salary_value]
129
129
  ```
130
130
 
131
+ ## Aliases
132
+
133
+ An alias lets you referrer to any question identifier by its question intentifer or its alias.
134
+
135
+ ```markdown
136
+ ## Are you Clark Kent?
137
+
138
+ [choice: clark_kent, alias: superman]
139
+ * yes: Yes
140
+ * no: No
141
+ ```
142
+
131
143
  ## Next steps
132
144
 
133
145
  Markdown to be displayed as part of an outcome to direct the users to other information of potential interest to them.
@@ -228,6 +240,31 @@ Text if true
228
240
  $ENDIF
229
241
  ```
230
242
 
243
+ Similarly, it is also possible to specify an elseif clause. These can be
244
+ chained together indefinitely. It is also possible to keep an else
245
+ clause at the end like so:
246
+
247
+ ```markdown
248
+ $IF pred1?
249
+
250
+ Text if pred1 true
251
+
252
+ $ELSEIF pred2?
253
+
254
+ Text if pred1 false and pred2 true
255
+
256
+ $ELSEIF pred3?
257
+
258
+ Text if pred1 and pred2 false, and pred3 true
259
+
260
+ $ELSE
261
+
262
+ Text if pred1, pred2, pred3 are false
263
+
264
+ $ENDIF
265
+ ```
266
+
267
+
231
268
  ## Interpolation
232
269
 
233
270
  It's possible to interpolate values from calculations, responses to questions, plugins etc.
@@ -10,20 +10,22 @@ module Smartdown
10
10
  private
11
11
  def evaluate(conditional, state)
12
12
  if conditional.predicate.evaluate(state)
13
- conditional.true_case
13
+ selected_branch = conditional.true_case
14
14
  else
15
- conditional.false_case
15
+ selected_branch = conditional.false_case
16
16
  end
17
+ resolve_conditionals selected_branch, state
17
18
  end
18
19
 
19
20
  def resolve_conditionals(elements, state)
21
+ return unless elements
20
22
  elements.map do |element|
21
- if element.is_a?(Smartdown::Model::Element::Conditional)
23
+ if element.is_a? Smartdown::Model::Element::Conditional
22
24
  evaluate(element, state)
23
25
  else
24
26
  element
25
27
  end
26
- end.flatten
28
+ end.flatten.compact
27
29
  end
28
30
  end
29
31
  end
@@ -33,10 +33,6 @@ module Smartdown
33
33
  node.start_button && node.start_button.start_node
34
34
  end
35
35
 
36
- def input_variable_names_from_question
37
- node.questions.map(&:name)
38
- end
39
-
40
36
  def first_matching_rule(rules)
41
37
  catch(:match) do
42
38
  throw_first_matching_rule_in(rules)
@@ -63,8 +59,11 @@ module Smartdown
63
59
 
64
60
  def state_with_responses
65
61
  result = state.put(node.name, answers.map(&:to_s))
66
- input_variable_names_from_question.each_with_index do |input_variable_name, index|
67
- result = result.put(input_variable_name, answers[index])
62
+ node.questions.each_with_index do |question, index|
63
+ result = result.put(question.name, answers[index])
64
+ if question.alias
65
+ result = result.put(question.alias, answers[index])
66
+ end
68
67
  end
69
68
  result
70
69
  end
@@ -4,7 +4,7 @@ module Smartdown
4
4
  module Model
5
5
  module Element
6
6
  module Question
7
- class Date < Struct.new(:name)
7
+ class Date < Struct.new(:name, :alias)
8
8
  def answer_type
9
9
  Smartdown::Model::Answer::Date
10
10
  end
@@ -4,7 +4,7 @@ module Smartdown
4
4
  module Model
5
5
  module Element
6
6
  module Question
7
- class MultipleChoice < Struct.new(:name, :choices)
7
+ class MultipleChoice < Struct.new(:name, :choices, :alias)
8
8
  def answer_type
9
9
  Smartdown::Model::Answer::MultipleChoice
10
10
  end
@@ -4,7 +4,7 @@ module Smartdown
4
4
  module Model
5
5
  module Element
6
6
  module Question
7
- class Salary < Struct.new(:name)
7
+ class Salary < Struct.new(:name, :alias)
8
8
  def answer_type
9
9
  Smartdown::Model::Answer::Salary
10
10
  end
@@ -4,7 +4,7 @@ module Smartdown
4
4
  module Model
5
5
  module Element
6
6
  module Question
7
- class Text < Struct.new(:name)
7
+ class Text < Struct.new(:name, :alias)
8
8
  def answer_type
9
9
  Smartdown::Model::Answer::Text
10
10
  end
@@ -14,6 +14,8 @@ module Smartdown
14
14
  rule(:some_space) { space_char.repeat(1) }
15
15
  rule(:ws) { ws_char.repeat }
16
16
  rule(:non_ws) { non_ws.repeat }
17
+ rule(:comma) { str(",") }
18
+ rule(:colon) { str(":") }
17
19
 
18
20
  rule(:whitespace_terminated_string) {
19
21
  non_ws_char >> (non_ws_char | space_char.repeat(1) >> non_ws_char).repeat
@@ -30,6 +32,16 @@ module Smartdown
30
32
  rule(:bullet) {
31
33
  match('[*-]')
32
34
  }
35
+
36
+ rule(:option_pair) {
37
+ comma >>
38
+ optional_space >>
39
+ identifier.as(:key) >>
40
+ colon >>
41
+ optional_space >>
42
+ identifier.as(:value) >>
43
+ optional_space
44
+ }
33
45
  end
34
46
  end
35
47
  end
@@ -19,13 +19,20 @@ module Smartdown
19
19
  (markdown_blocks_inside_conditional.as(:false_case) >> newline).maybe
20
20
  }
21
21
 
22
+ rule(:elseif_clause) {
23
+ str("$ELSEIF ") >> (Predicates.new.as(:predicate) >>
24
+ optional_space >> newline.repeat(2) >>
25
+ (markdown_blocks_inside_conditional.as(:true_case) >> newline).maybe >>
26
+ ((elseif_clause | else_clause).maybe)).as(:conditional).repeat(1,1).as(:false_case)
27
+ }
28
+
22
29
  rule(:conditional_clause) {
23
30
  (
24
31
  str("$IF ") >>
25
32
  Predicates.new.as(:predicate) >>
26
33
  optional_space >> newline.repeat(2) >>
27
34
  (markdown_blocks_inside_conditional.as(:true_case) >> newline).maybe >>
28
- else_clause.maybe >>
35
+ (else_clause | elseif_clause).maybe >>
29
36
  str("$ENDIF") >> optional_space >> line_ending
30
37
  ).as(:conditional)
31
38
  }
@@ -10,6 +10,7 @@ module Smartdown
10
10
  optional_space >>
11
11
  question_identifier.as(:identifier) >>
12
12
  optional_space >>
13
+ option_pair.repeat.as(:option_pairs) >>
13
14
  str("]") >>
14
15
  optional_space >>
15
16
  line_ending
@@ -9,6 +9,7 @@ module Smartdown
9
9
  optional_space >>
10
10
  question_identifier.as(:identifier) >>
11
11
  optional_space >>
12
+ option_pair.repeat.as(:option_pairs) >>
12
13
  str("]") >>
13
14
  optional_space >>
14
15
  line_ending
@@ -10,6 +10,7 @@ module Smartdown
10
10
  optional_space >>
11
11
  question_identifier.as(:identifier) >>
12
12
  optional_space >>
13
+ option_pair.repeat.as(:option_pairs) >>
13
14
  str("]") >>
14
15
  optional_space >>
15
16
  line_ending
@@ -10,6 +10,7 @@ module Smartdown
10
10
  optional_space >>
11
11
  question_identifier.as(:identifier) >>
12
12
  optional_space >>
13
+ option_pair.repeat.as(:option_pairs) >>
13
14
  str("]") >>
14
15
  optional_space >>
15
16
  line_ending
@@ -1,4 +1,5 @@
1
1
  require 'parslet/transform'
2
+ require 'smartdown/parser/option_pairs_transform'
2
3
  require 'smartdown/model/node'
3
4
  require 'smartdown/model/front_matter'
4
5
  require 'smartdown/model/rule'
@@ -64,27 +65,33 @@ module Smartdown
64
65
  [url.to_s, label.to_s]
65
66
  }
66
67
 
67
- rule(:multiple_choice => {identifier: simple(:identifier), options: subtree(:choices)}) {
68
+
69
+ rule(:multiple_choice => {identifier: simple(:identifier), :option_pairs => subtree(:option_pairs), options: subtree(:choices)}) {
68
70
  Smartdown::Model::Element::Question::MultipleChoice.new(
69
- identifier.to_s, Hash[choices]
71
+ identifier.to_s,
72
+ Hash[choices],
73
+ Smartdown::Parser::OptionPairs.transform(option_pairs).fetch('alias', nil),
70
74
  )
71
75
  }
72
76
 
73
- rule(:date => {identifier: simple(:identifier)}) {
77
+ rule(:date => {identifier: simple(:identifier), :option_pairs => subtree(:option_pairs)}) {
74
78
  Smartdown::Model::Element::Question::Date.new(
75
- identifier.to_s
79
+ identifier.to_s,
80
+ Smartdown::Parser::OptionPairs.transform(option_pairs).fetch('alias', nil)
76
81
  )
77
82
  }
78
83
 
79
- rule(:salary => {identifier: simple(:identifier)}) {
84
+ rule(:salary => {identifier: simple(:identifier), :option_pairs => subtree(:option_pairs)}) {
80
85
  Smartdown::Model::Element::Question::Salary.new(
81
- identifier.to_s
86
+ identifier.to_s,
87
+ Smartdown::Parser::OptionPairs.transform(option_pairs).fetch('alias', nil)
82
88
  )
83
89
  }
84
90
 
85
- rule(:text => {identifier: simple(:identifier)}) {
91
+ rule(:text => {identifier: simple(:identifier), :option_pairs => subtree(:option_pairs)}) {
86
92
  Smartdown::Model::Element::Question::Text.new(
87
- identifier.to_s
93
+ identifier.to_s,
94
+ Smartdown::Parser::OptionPairs.transform(option_pairs).fetch('alias', nil)
88
95
  )
89
96
  }
90
97
 
@@ -146,10 +153,10 @@ module Smartdown
146
153
  Smartdown::Model::Predicate::Function.new(name.to_s, [])
147
154
  }
148
155
 
149
- rule(:comparison_predicate => { varname: simple(:varname),
156
+ rule(:comparison_predicate => { varname: simple(:varname),
150
157
  value: simple(:value),
151
158
  operator: simple(:operator)
152
- }) {
159
+ }) {
153
160
  case operator
154
161
  when "<="
155
162
  Smartdown::Model::Predicate::Comparison::LessOrEqual.new(varname.to_s, value.to_s)
@@ -0,0 +1,11 @@
1
+ module Smartdown
2
+ module Parser
3
+ module OptionPairs
4
+ def self.transform(option_pairs)
5
+ Hash[option_pairs.map { |option_pair|
6
+ option_pair.values.map(&:to_s)
7
+ }]
8
+ end
9
+ end
10
+ end
11
+ end
@@ -1,3 +1,3 @@
1
1
  module Smartdown
2
- VERSION = "0.8.1"
2
+ VERSION = "0.8.2"
3
3
  end
@@ -123,6 +123,10 @@ describe Smartdown::Api::Flow do
123
123
  specify { expect(flow.state("y",["lion", "no", "no"]).accepted_responses).to eq ["lion", "no", "no"] }
124
124
  specify { expect(flow.state("y",["lion", "no", "no"]).current_answers).to eq [] }
125
125
  end
126
+
127
+ context "with aliased identifier overwritten with later answer" do
128
+ specify { expect(flow.state("y",["bulldog", "lion", "no", "no"]).current_node.name).to eq "outcome_untrained_with_lions" }
129
+ end
126
130
  end
127
131
  end
128
132
 
@@ -57,4 +57,86 @@ describe Smartdown::Engine::ConditionalResolver do
57
57
  end
58
58
  end
59
59
  end
60
+ context "a node with nested conditionals" do
61
+ let(:node) {
62
+ model_builder.node("outcome_no_visa_needed") do
63
+ conditional do
64
+ named_predicate "pred1?"
65
+ true_case do
66
+ paragraph("True case")
67
+ end
68
+ false_case do
69
+ conditional do
70
+ named_predicate "pred2?"
71
+ true_case do
72
+ paragraph("False True case")
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
78
+ }
79
+
80
+ context "first pred is true" do
81
+ let(:state) {
82
+ Smartdown::Engine::State.new(
83
+ current_node: node.name,
84
+ pred1?: true,
85
+ pred2?: "Doesn't matter"
86
+ )
87
+ }
88
+
89
+ let(:expected_node_after_presentation) {
90
+ model_builder.node("outcome_no_visa_needed") do
91
+ paragraph("True case")
92
+ end
93
+ }
94
+
95
+ it "should resolve the conditional and preserve the 'True case' paragraph block" do
96
+ expect(conditional_resolver.call(node, state)).to eq(expected_node_after_presentation)
97
+ end
98
+ end
99
+
100
+
101
+ context "first pred is false" do
102
+ context "second pred is true" do
103
+ let(:state) {
104
+ Smartdown::Engine::State.new(
105
+ current_node: node.name,
106
+ pred1?: false,
107
+ pred2?: true
108
+ )
109
+ }
110
+
111
+ let(:expected_node_after_presentation) {
112
+ model_builder.node("outcome_no_visa_needed") do
113
+ paragraph("False True case")
114
+ end
115
+ }
116
+
117
+ it "should resolve the conditional and preserve the 'True case' paragraph block" do
118
+ expect(conditional_resolver.call(node, state)).to eq(expected_node_after_presentation)
119
+ end
120
+ end
121
+
122
+ context "second pred is false" do
123
+ let(:state) {
124
+ Smartdown::Engine::State.new(
125
+ current_node: node.name,
126
+ pred1?: false,
127
+ pred2?: false
128
+ )
129
+ }
130
+
131
+ let(:expected_node_after_presentation) {
132
+ model_builder.node("outcome_no_visa_needed") do
133
+ end
134
+ }
135
+
136
+ it "should resolve the conditional and resolve no false case to be empty" do
137
+ expect(conditional_resolver.call(node, state)).to eq(expected_node_after_presentation)
138
+ end
139
+ end
140
+ end
141
+ end
60
142
  end
data/spec/engine_spec.rb CHANGED
@@ -24,7 +24,8 @@ describe Smartdown::Engine do
24
24
  greek: "Greek",
25
25
  british: "British",
26
26
  usa: "USA"
27
- }
27
+ },
28
+ "passport_type?"
28
29
  )
29
30
  next_node_rules do
30
31
  rule do
@@ -181,6 +182,10 @@ describe Smartdown::Engine do
181
182
  it "has recorded input" do
182
183
  expect(subject.get("what_passport_do_you_have?")).to eq("greek")
183
184
  end
185
+
186
+ it "recorded input is accessiable via question alias" do
187
+ expect(subject.get("passport_type?")).to eq("greek")
188
+ end
184
189
  end
185
190
 
186
191
  context "USA passport" do
@@ -4,12 +4,14 @@
4
4
 
5
5
  Text before the options.
6
6
 
7
- [choice: question_1]
7
+ [choice: question_1, alias: type_of_feline]
8
8
  * lion: Lion
9
9
  * tiger: Tiger
10
10
  * cat: Cat
11
+ * bulldog: Bulldog
11
12
 
12
13
  This is an optional note after question.
13
14
 
15
+ * type_of_feline is 'bulldog' => question_4
14
16
  * question_1 in {lion tiger} => question_2
15
17
  * otherwise => outcome_safe_pet
@@ -17,7 +17,7 @@ Think very carefully again.
17
17
  * maybe: I've been to a zoo once
18
18
  * no: No
19
19
 
20
- * question_1 is 'lion'
20
+ * type_of_feline is 'lion'
21
21
  * trained_for_lions is 'yes' => question_3
22
22
  * otherwise => outcome_untrained_with_lions
23
23
  * otherwise
@@ -0,0 +1,10 @@
1
+ # That's not a cat!!
2
+
3
+ # Which cat do you actually have you
4
+
5
+ [choice: question_1, alias: type_of_feline]
6
+ * lion: Lion
7
+ * tiger: Tiger
8
+ * cat: Cat
9
+
10
+ * type_of_feline in {lion tiger cat} => question_2
@@ -102,7 +102,6 @@ SOURCE
102
102
  }
103
103
  end
104
104
  end
105
- end
106
105
 
107
106
  context "simple IF-ELSE" do
108
107
  let(:source) { <<-SOURCE
@@ -150,5 +149,185 @@ SOURCE
150
149
  end
151
150
  end
152
151
 
152
+ context "simple ELSE-IF" do
153
+ let(:source) { <<-SOURCE
154
+ $IF pred1?
155
+
156
+ #{true1_body}
157
+
158
+ $ELSEIF pred2?
159
+
160
+ #{true2_body}
161
+
162
+ $ENDIF
163
+ SOURCE
164
+ }
165
+
166
+ context "with one-line true case" do
167
+ let(:true1_body) { "Text if first predicate is true" }
168
+ let(:true2_body) { "Text if first predicate is false and the second is true" }
169
+
170
+
171
+ it {
172
+ should parse(source).as(
173
+ conditional: {
174
+ predicate: {named_predicate: "pred1?"},
175
+ true_case: [{p: "#{true1_body}\n"}],
176
+ false_case: [{conditional: {
177
+ predicate: {named_predicate: "pred2?"},
178
+ true_case: [{p: "#{true2_body}\n"}],
179
+ }}]
180
+ }
181
+ )
182
+ }
183
+
184
+ describe "transformed" do
185
+ subject(:transformed) {
186
+ Smartdown::Parser::NodeInterpreter.new(node_name, source, parser: parser).interpret
187
+ }
188
+
189
+ it {
190
+ should eq(
191
+ Smartdown::Model::Element::Conditional.new(
192
+ Smartdown::Model::Predicate::Named.new("pred1?"),
193
+ [Smartdown::Model::Element::MarkdownParagraph.new(true1_body + "\n")],
194
+ [Smartdown::Model::Element::Conditional.new(
195
+ Smartdown::Model::Predicate::Named.new("pred2?"),
196
+ [Smartdown::Model::Element::MarkdownParagraph.new(true2_body + "\n")]
197
+ )]
198
+ )
199
+ )
200
+ }
201
+ end
202
+ end
203
+ end
204
+ end
205
+
206
+ context "double ELSE-IF" do
207
+ let(:source) { <<-SOURCE
208
+ $IF pred1?
209
+
210
+ #{true1_body}
211
+
212
+ $ELSEIF pred2?
213
+
214
+ #{true2_body}
215
+
216
+ $ELSEIF pred3?
217
+
218
+ #{true3_body}
219
+
220
+ $ENDIF
221
+ SOURCE
222
+ }
223
+
224
+ context "with one-line true case" do
225
+ let(:true1_body) { "Text if first predicate is true" }
226
+ let(:true2_body) { "Text if first predicate is false and the second is true" }
227
+ let(:true3_body) { "Text if first and second predicates are false and the third is true" }
228
+
229
+ it {
230
+ should parse(source).as(
231
+ conditional: {
232
+ predicate: {named_predicate: "pred1?"},
233
+ true_case: [{p: "#{true1_body}\n"}],
234
+ false_case: [{conditional: {
235
+ predicate: {named_predicate: "pred2?"},
236
+ true_case: [{p: "#{true2_body}\n"}],
237
+ false_case: [{conditional: {
238
+ predicate: {named_predicate: "pred3?"},
239
+ true_case: [{p: "#{true3_body}\n"}],
240
+ }}]
241
+ }}]
242
+ }
243
+ )
244
+ }
245
+
246
+ describe "transformed" do
247
+ subject(:transformed) {
248
+ Smartdown::Parser::NodeInterpreter.new(node_name, source, parser: parser).interpret
249
+ }
250
+
251
+ it {
252
+ should eq(
253
+ Smartdown::Model::Element::Conditional.new(
254
+ Smartdown::Model::Predicate::Named.new("pred1?"),
255
+ [Smartdown::Model::Element::MarkdownParagraph.new(true1_body + "\n")],
256
+ [Smartdown::Model::Element::Conditional.new(
257
+ Smartdown::Model::Predicate::Named.new("pred2?"),
258
+ [Smartdown::Model::Element::MarkdownParagraph.new(true2_body + "\n")],
259
+ [Smartdown::Model::Element::Conditional.new(
260
+ Smartdown::Model::Predicate::Named.new("pred3?"),
261
+ [Smartdown::Model::Element::MarkdownParagraph.new(true3_body + "\n")]
262
+ )]
263
+ )]
264
+ )
265
+ )
266
+ }
267
+ end
268
+ end
269
+ end
270
+
271
+ context "ELSE-IF with an ELSE" do
272
+ let(:source) { <<-SOURCE
273
+ $IF pred1?
274
+
275
+ #{true1_body}
276
+
277
+ $ELSEIF pred2?
278
+
279
+ #{true2_body}
280
+
281
+ $ELSE
282
+
283
+ #{false_body}
284
+
285
+ $ENDIF
286
+ SOURCE
287
+ }
288
+
289
+ context "with one-line true case" do
290
+ let(:true1_body) { "Text if first predicate is true" }
291
+ let(:true2_body) { "Text if first predicate is false and the second is true" }
292
+ let(:false_body) { "Text both predicates are false" }
293
+
294
+
295
+ it {
296
+ should parse(source).as(
297
+ conditional: {
298
+ predicate: {named_predicate: "pred1?"},
299
+ true_case: [{p: "#{true1_body}\n"}],
300
+ false_case: [{conditional: {
301
+ predicate: {named_predicate: "pred2?"},
302
+ true_case: [{p: "#{true2_body}\n"}],
303
+ false_case: [{p: "#{false_body}\n"}]
304
+ }}]
305
+ }
306
+ )
307
+ }
308
+
309
+
310
+ describe "transformed" do
311
+ subject(:transformed) {
312
+ Smartdown::Parser::NodeInterpreter.new(node_name, source, parser: parser).interpret
313
+ }
314
+
315
+ it {
316
+ should eq(
317
+ Smartdown::Model::Element::Conditional.new(
318
+ Smartdown::Model::Predicate::Named.new("pred1?"),
319
+ [Smartdown::Model::Element::MarkdownParagraph.new(true1_body + "\n")],
320
+ [Smartdown::Model::Element::Conditional.new(
321
+ Smartdown::Model::Predicate::Named.new("pred2?"),
322
+ [Smartdown::Model::Element::MarkdownParagraph.new(true2_body + "\n")],
323
+ [Smartdown::Model::Element::MarkdownParagraph.new(false_body + "\n")]
324
+ )]
325
+ )
326
+ )
327
+ }
328
+ end
329
+
330
+ end
331
+ end
153
332
  end
154
333
 
@@ -12,6 +12,7 @@ describe Smartdown::Parser::Element::DateQuestion do
12
12
  should parse(source).as(
13
13
  date: {
14
14
  identifier: "date_of_birth",
15
+ option_pairs: [],
15
16
  }
16
17
  )
17
18
  end
@@ -25,4 +26,31 @@ describe Smartdown::Parser::Element::DateQuestion do
25
26
  it { should eq(Smartdown::Model::Element::Question::Date.new("date_of_birth")) }
26
27
  end
27
28
  end
29
+
30
+ context "with question tag and an alias" do
31
+ let(:source) { "[date: date_of_birth, alias: date_for_adoption_or_birth]" }
32
+
33
+ it "parses" do
34
+ should parse(source).as(
35
+ date: {
36
+ identifier: "date_of_birth",
37
+ option_pairs: [
38
+ {
39
+ key: 'alias',
40
+ value: 'date_for_adoption_or_birth',
41
+ }
42
+ ]
43
+ }
44
+ )
45
+ end
46
+
47
+ describe "transformed" do
48
+ let(:node_name) { "my_node" }
49
+ subject(:transformed) {
50
+ Smartdown::Parser::NodeInterpreter.new(node_name, source, parser: parser).interpret
51
+ }
52
+
53
+ it { should eq(Smartdown::Model::Element::Question::Date.new("date_of_birth", "date_for_adoption_or_birth")) }
54
+ end
55
+ end
28
56
  end
@@ -21,7 +21,8 @@ describe Smartdown::Parser::Element::MultipleChoiceQuestion do
21
21
  options: [
22
22
  {value: "yes", label: "Yes"},
23
23
  {value: "no", label: "No"}
24
- ]
24
+ ],
25
+ option_pairs: [],
25
26
  }
26
27
  )
27
28
  end
@@ -36,6 +37,43 @@ describe Smartdown::Parser::Element::MultipleChoiceQuestion do
36
37
  end
37
38
  end
38
39
 
40
+ context "with question tag and alias" do
41
+ let(:source) {
42
+ [
43
+ "[choice: yes_or_no, alias: no_or_yes]",
44
+ "* yes: Yes",
45
+ "* no: No"
46
+ ].join("\n")
47
+ }
48
+
49
+ it "parses" do
50
+ should parse(source).as(
51
+ multiple_choice: {
52
+ identifier: "yes_or_no",
53
+ options: [
54
+ {value: "yes", label: "Yes"},
55
+ {value: "no", label: "No"},
56
+ ],
57
+ option_pairs: [
58
+ {
59
+ key: 'alias',
60
+ value: 'no_or_yes',
61
+ }
62
+ ],
63
+ }
64
+ )
65
+ end
66
+
67
+ describe "transformed" do
68
+ let(:node_name) { "my_node" }
69
+ subject(:transformed) {
70
+ Smartdown::Parser::NodeInterpreter.new(node_name, source, parser: parser).interpret
71
+ }
72
+
73
+ it { should eq(Smartdown::Model::Element::Question::MultipleChoice.new("yes_or_no", {"yes"=>"Yes", "no"=>"No"}, "no_or_yes")) }
74
+ end
75
+ end
76
+
39
77
  context "without question tag" do
40
78
  let(:source) {
41
79
  [
@@ -10,10 +10,11 @@ describe Smartdown::Parser::Element::SalaryQuestion do
10
10
 
11
11
  it "parses" do
12
12
  should parse(source).as(
13
- salary: {
14
- identifier: "mother_salary",
15
- }
16
- )
13
+ salary: {
14
+ identifier: "mother_salary",
15
+ option_pairs:[],
16
+ }
17
+ )
17
18
  end
18
19
 
19
20
  describe "transformed" do
@@ -25,4 +26,32 @@ describe Smartdown::Parser::Element::SalaryQuestion do
25
26
  it { should eq(Smartdown::Model::Element::Question::Salary.new("mother_salary")) }
26
27
  end
27
28
  end
29
+
30
+ context "with question tag and alias" do
31
+ let(:source) { "[salary: mother_salary, alias: mums_salary]" }
32
+
33
+ it "parses" do
34
+ should parse(source).as(
35
+ salary: {
36
+ identifier: "mother_salary",
37
+ option_pairs: [
38
+ {
39
+ key: 'alias',
40
+ value: 'mums_salary',
41
+ }
42
+ ]
43
+ }
44
+ )
45
+ end
46
+
47
+ describe "transformed" do
48
+ let(:node_name) { "my_node" }
49
+ subject(:transformed) {
50
+ Smartdown::Parser::NodeInterpreter.new(node_name, source, parser: parser).interpret
51
+ }
52
+
53
+ it { should eq(Smartdown::Model::Element::Question::Salary.new("mother_salary", "mums_salary")) }
54
+ end
55
+ end
56
+
28
57
  end
@@ -10,10 +10,11 @@ describe Smartdown::Parser::Element::TextQuestion do
10
10
 
11
11
  it "parses" do
12
12
  should parse(source).as(
13
- text: {
14
- identifier: "hometown",
15
- }
16
- )
13
+ text: {
14
+ identifier: "hometown",
15
+ option_pairs: [],
16
+ },
17
+ )
17
18
  end
18
19
 
19
20
  describe "transformed" do
@@ -25,4 +26,32 @@ describe Smartdown::Parser::Element::TextQuestion do
25
26
  it { should eq(Smartdown::Model::Element::Question::Text.new("hometown")) }
26
27
  end
27
28
  end
29
+
30
+ context "with question tag and alias" do
31
+ let(:source) { "[text: hometown, alias: birthplace]" }
32
+
33
+ it "parses" do
34
+ should parse(source).as(
35
+ text: {
36
+ identifier: "hometown",
37
+ option_pairs:[
38
+ {
39
+ key: 'alias',
40
+ value: 'birthplace',
41
+ }
42
+ ]
43
+ }
44
+ )
45
+ end
46
+
47
+ describe "transformed" do
48
+ let(:node_name) { "my_node" }
49
+ subject(:transformed) {
50
+ Smartdown::Parser::NodeInterpreter.new(node_name, source, parser: parser).interpret
51
+ }
52
+
53
+ it { should eq(Smartdown::Model::Element::Question::Text.new("hometown", "birthplace")) }
54
+ end
55
+ end
56
+
28
57
  end
@@ -75,7 +75,8 @@ SOURCE
75
75
  options: [
76
76
  {value: "yes", label: "Yes"},
77
77
  {value: "no", label: "No"}
78
- ]}
78
+ ],
79
+ option_pairs: []}
79
80
  }
80
81
  ]
81
82
  })
@@ -106,7 +107,8 @@ SOURCE
106
107
  options: [
107
108
  {value: "yes", label: "Yes"},
108
109
  {value: "no", label: "No"}
109
- ]}
110
+ ],
111
+ option_pairs: []}
110
112
  },
111
113
  {h1: "Next node rules"},
112
114
  {next_node_rules: [{rule: {predicate: {named_predicate: "pred1?"}, outcome: "outcome"}}]}
@@ -0,0 +1,36 @@
1
+ require 'smartdown/parser/option_pairs_transform'
2
+
3
+ describe Smartdown::Parser::OptionPairs do
4
+ describe '.transform' do
5
+ subject(:transform) { Smartdown::Parser::OptionPairs.transform(input) }
6
+
7
+ context 'blank array' do
8
+ let(:input) { [] }
9
+ it 'returns empty hash' do
10
+ expect(transform).to eql({})
11
+ end
12
+ end
13
+
14
+ context 'single array of option pairs hash passed' do
15
+ let(:input) { [{key: 'dog', value: 'woof'}] }
16
+ it 'returns hash containing key value pairs' do
17
+ expect(transform).to eql({'dog' => 'woof'})
18
+ end
19
+ end
20
+
21
+ context 'single array of option pairs hash passed' do
22
+ let(:input) { [{key: 'dog', value: 'woof'}, {key: 'cat', value: 'meow'}] }
23
+ it 'returns hash containing key value pairs' do
24
+ expect(transform).to eql({'dog' => 'woof', 'cat' => 'meow'})
25
+ end
26
+ end
27
+
28
+ context 'single array of option pairs hash passed value not string' do
29
+ let(:input) { [{key: 'number', value: 1}] }
30
+ it 'returns hash containing key value pairs calling after calling .to_s' do
31
+ expect(transform).to eql({'number' => '1'})
32
+ end
33
+ end
34
+
35
+ end
36
+ end
@@ -53,10 +53,10 @@ class ModelBuilder
53
53
  @elements.last
54
54
  end
55
55
 
56
- def multiple_choice(name, options)
56
+ def multiple_choice(name, options, question_alias=nil)
57
57
  @elements ||= []
58
58
  options_with_string_keys = ::Hash[options.map {|k,v| [k.to_s, v]}]
59
- @elements << Smartdown::Model::Element::Question::MultipleChoice.new(name, options_with_string_keys)
59
+ @elements << Smartdown::Model::Element::Question::MultipleChoice.new(name, options_with_string_keys, question_alias)
60
60
  @elements.last
61
61
  end
62
62
 
@@ -81,52 +81,46 @@ class ModelBuilder
81
81
  end
82
82
 
83
83
  def rule(predicate = nil, outcome = nil, &block)
84
- @predicate = predicate
85
- @outcome = outcome
84
+ @predicate = [predicate].compact
85
+ @outcome = [outcome].compact
86
86
  @rules ||= []
87
87
  instance_eval(&block) if block_given?
88
- @rules << Smartdown::Model::Rule.new(@predicate, @outcome)
88
+ @rules << Smartdown::Model::Rule.new(@predicate.pop, @outcome.pop)
89
89
  @rules.last
90
90
  end
91
91
 
92
92
  def conditional(&block)
93
- @predicate = nil
94
- @true_case = nil
95
- @false_case = nil
93
+ @predicate ||= []
94
+ @true_case ||= []
95
+ @false_case ||= []
96
96
  @elements ||= []
97
97
  instance_eval(&block) if block_given?
98
- @elements << Smartdown::Model::Element::Conditional.new(@predicate, @true_case, @false_case)
98
+ @elements << Smartdown::Model::Element::Conditional.new(@predicate.pop, [@true_case.pop], [@false_case.pop])
99
99
  @elements.last
100
100
  end
101
101
 
102
102
  def true_case(&block)
103
- @outer_elements = @elements
104
- @elements = []
105
103
  instance_eval(&block) if block_given?
106
- @true_case = @elements
107
- @elements = @outer_elements
108
- @true_case
104
+ @true_case << @elements.pop
105
+ @true_case.last
109
106
  end
110
107
 
111
108
  def false_case(&block)
112
- @outer_elements = @elements
113
- @elements = []
114
109
  instance_eval(&block) if block_given?
115
- @false_case = @elements
116
- @elements = @outer_elements
117
- @false_case
110
+ @false_case << @elements.pop
111
+ @false_case.last
118
112
  end
119
113
 
120
114
  def named_predicate(name)
121
- @predicate = Smartdown::Model::Predicate::Named.new(name)
115
+ @predicate << Smartdown::Model::Predicate::Named.new(name)
122
116
  end
123
117
 
124
118
  def set_membership_predicate(varname, values)
125
- @predicate = Smartdown::Model::Predicate::SetMembership.new(varname, values)
119
+ @predicate << Smartdown::Model::Predicate::SetMembership.new(varname, values)
126
120
  end
127
121
 
128
122
  def outcome(name)
129
- @outcome = name
123
+ @outcome << name
130
124
  end
131
125
 
132
126
  module DSL
@@ -15,7 +15,7 @@ describe ModelBuilder do
15
15
  let(:node2) {
16
16
  Smartdown::Model::Node.new("what_passport_do_you_have?", [
17
17
  Smartdown::Model::Element::MarkdownHeading.new("What passport do you have?"),
18
- Smartdown::Model::Element::Question::MultipleChoice.new("what_passport_do_you_have?", {"greek" => "Greek", "british" => "British"}),
18
+ Smartdown::Model::Element::Question::MultipleChoice.new("what_passport_do_you_have?", {"greek" => "Greek", "british" => "British"}, "passport_type?"),
19
19
  Smartdown::Model::NextNodeRules.new([
20
20
  Smartdown::Model::Rule.new(
21
21
  Smartdown::Model::Predicate::Named.new("eea_passport?"),
@@ -44,7 +44,8 @@ describe ModelBuilder do
44
44
  {
45
45
  greek: "Greek",
46
46
  british: "British"
47
- }
47
+ },
48
+ "passport_type?"
48
49
  )
49
50
  next_node_rules do
50
51
  rule do
@@ -89,6 +90,42 @@ describe ModelBuilder do
89
90
  it "builds a conditional" do
90
91
  should eq(expected)
91
92
  end
93
+
94
+ context "nested conditionals" do
95
+ let(:expected) {
96
+ Smartdown::Model::Element::Conditional.new(
97
+ Smartdown::Model::Predicate::Named.new("pred1?"),
98
+ [Smartdown::Model::Element::MarkdownParagraph.new("True case")],
99
+ [Smartdown::Model::Element::Conditional.new(
100
+ Smartdown::Model::Predicate::Named.new("pred2?"),
101
+ [Smartdown::Model::Element::MarkdownParagraph.new("False True case")],
102
+ [Smartdown::Model::Element::MarkdownParagraph.new("False False case")]
103
+ )]
104
+ )
105
+ }
106
+
107
+ subject(:model) {
108
+ builder.conditional do
109
+ named_predicate "pred1?"
110
+ true_case do
111
+ paragraph("True case")
112
+ end
113
+ false_case do
114
+ conditional do
115
+ named_predicate "pred2?"
116
+ true_case do
117
+ paragraph("False True case")
118
+ end
119
+ false_case do
120
+ paragraph("False False case")
121
+ end
122
+ end
123
+ end
124
+ end
125
+ }
126
+
127
+ it { should eq(expected) }
128
+ end
92
129
  end
93
130
 
94
131
  describe "#rule" do
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: smartdown
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.1
4
+ version: 0.8.2
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -13,7 +13,7 @@ date: 2014-07-09 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: parslet
16
- requirement: &15145500 !ruby/object:Gem::Requirement
16
+ requirement: &5594140 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ~>
@@ -21,10 +21,10 @@ dependencies:
21
21
  version: 1.6.1
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *15145500
24
+ version_requirements: *5594140
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: rspec
27
- requirement: &15144780 !ruby/object:Gem::Requirement
27
+ requirement: &5593360 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - ~>
@@ -32,10 +32,10 @@ dependencies:
32
32
  version: 3.0.0
33
33
  type: :development
34
34
  prerelease: false
35
- version_requirements: *15144780
35
+ version_requirements: *5593360
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: rake
38
- requirement: &15144020 !ruby/object:Gem::Requirement
38
+ requirement: &5592880 !ruby/object:Gem::Requirement
39
39
  none: false
40
40
  requirements:
41
41
  - - ! '>='
@@ -43,10 +43,10 @@ dependencies:
43
43
  version: '0'
44
44
  type: :development
45
45
  prerelease: false
46
- version_requirements: *15144020
46
+ version_requirements: *5592880
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: gem_publisher
49
- requirement: &15143120 !ruby/object:Gem::Requirement
49
+ requirement: &5592080 !ruby/object:Gem::Requirement
50
50
  none: false
51
51
  requirements:
52
52
  - - ! '>='
@@ -54,7 +54,7 @@ dependencies:
54
54
  version: '0'
55
55
  type: :development
56
56
  prerelease: false
57
- version_requirements: *15143120
57
+ version_requirements: *5592080
58
58
  description:
59
59
  email: david.heath@digital.cabinet-office.gov.uk
60
60
  executables:
@@ -85,6 +85,7 @@ files:
85
85
  - lib/smartdown/parser/element/markdown_heading.rb
86
86
  - lib/smartdown/parser/element/text_question.rb
87
87
  - lib/smartdown/parser/flow_interpreter.rb
88
+ - lib/smartdown/parser/option_pairs_transform.rb
88
89
  - lib/smartdown/api/date_question.rb
89
90
  - lib/smartdown/api/coversheet.rb
90
91
  - lib/smartdown/api/question.rb
@@ -163,6 +164,7 @@ files:
163
164
  - spec/parser/element/front_matter_spec.rb
164
165
  - spec/parser/element/salary_question_spec.rb
165
166
  - spec/parser/element/text_question_spec.rb
167
+ - spec/parser/option_pairs_transform_spec.rb
166
168
  - spec/parser/node_parser_spec.rb
167
169
  - spec/parser/snippet_pre_parser_spec.rb
168
170
  - spec/support/flow_input_interface.rb
@@ -204,6 +206,7 @@ files:
204
206
  - spec/fixtures/acceptance/animal-example-multiple/questions/question_3.txt
205
207
  - spec/fixtures/acceptance/animal-example-multiple/questions/question_1.txt
206
208
  - spec/fixtures/acceptance/animal-example-multiple/questions/question_2.txt
209
+ - spec/fixtures/acceptance/animal-example-multiple/questions/question_4.txt
207
210
  - spec/fixtures/acceptance/cover-sheet/cover-sheet.txt
208
211
  - spec/fixtures/acceptance/one-question/one-question.txt
209
212
  - spec/fixtures/acceptance/one-question/questions/q1.txt
@@ -242,7 +245,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
242
245
  version: '0'
243
246
  segments:
244
247
  - 0
245
- hash: -1564980358341002898
248
+ hash: -1665618218665725274
246
249
  required_rubygems_version: !ruby/object:Gem::Requirement
247
250
  none: false
248
251
  requirements:
@@ -251,7 +254,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
251
254
  version: '0'
252
255
  segments:
253
256
  - 0
254
- hash: -1564980358341002898
257
+ hash: -1665618218665725274
255
258
  requirements: []
256
259
  rubyforge_project:
257
260
  rubygems_version: 1.8.11
@@ -278,6 +281,7 @@ test_files:
278
281
  - spec/parser/element/front_matter_spec.rb
279
282
  - spec/parser/element/salary_question_spec.rb
280
283
  - spec/parser/element/text_question_spec.rb
284
+ - spec/parser/option_pairs_transform_spec.rb
281
285
  - spec/parser/node_parser_spec.rb
282
286
  - spec/parser/snippet_pre_parser_spec.rb
283
287
  - spec/support/flow_input_interface.rb
@@ -319,6 +323,7 @@ test_files:
319
323
  - spec/fixtures/acceptance/animal-example-multiple/questions/question_3.txt
320
324
  - spec/fixtures/acceptance/animal-example-multiple/questions/question_1.txt
321
325
  - spec/fixtures/acceptance/animal-example-multiple/questions/question_2.txt
326
+ - spec/fixtures/acceptance/animal-example-multiple/questions/question_4.txt
322
327
  - spec/fixtures/acceptance/cover-sheet/cover-sheet.txt
323
328
  - spec/fixtures/acceptance/one-question/one-question.txt
324
329
  - spec/fixtures/acceptance/one-question/questions/q1.txt