smartdown 0.8.1 → 0.8.2

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.
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