smartdown 0.0.4 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -83,7 +83,7 @@ You may need a visa to come to the UK to visit, study or work.
83
83
  Question nodes follow the same standard structure outlined above.
84
84
 
85
85
  Smartdown currently allows multiple questions to be defined per node, but this
86
- feature is in development and the behaviour may change.
86
+ feature has only [recently been introduced](CHANGELOG.md#010) and may still change.
87
87
 
88
88
  The next sections define the various question types available.
89
89
 
@@ -4,26 +4,25 @@ require 'smartdown/engine/errors'
4
4
  module Smartdown
5
5
  class Engine
6
6
  class Transition
7
- attr_reader :state, :node, :input
7
+ attr_reader :state, :node, :inputs
8
8
 
9
- def initialize(state, node, input, options = {})
9
+ def initialize(state, node, input_array, options = {})
10
10
  @state = state
11
11
  @node = node
12
- @input = input
12
+ @inputs = input_array
13
13
  @predicate_evaluator = options[:predicate_evaluator] || PredicateEvaluator.new
14
14
  end
15
15
 
16
16
  def next_node
17
17
  next_node_from_next_node_rules ||
18
- next_node_from_start_button ||
19
- raise(Smartdown::Engine::IndeterminateNextNode, "No next node rules defined for '#{node.name}'", caller)
18
+ next_node_from_start_button ||
19
+ raise(Smartdown::Engine::IndeterminateNextNode, "No next node rules defined for '#{node.name}'", caller)
20
20
  end
21
21
 
22
22
  def next_state
23
- state_with_input
23
+ state_with_inputs
24
24
  .put(:path, state.get(:path) + [node.name])
25
- .put(:responses, state.get(:responses) + [input])
26
- .put(input_variable_name, input)
25
+ .put(:responses, state.get(:responses) + inputs)
27
26
  .put(:current_node, next_node)
28
27
  end
29
28
 
@@ -38,13 +37,9 @@ module Smartdown
38
37
  start_button && start_button.start_node
39
38
  end
40
39
 
41
- def input_variable_name
42
- input_variable_name_from_question || node.name
43
- end
44
-
45
- def input_variable_name_from_question
46
- question = node.elements.find { |e| e.is_a?(Smartdown::Model::Element::MultipleChoice) }
47
- question && question.name
40
+ def input_variable_names_from_question
41
+ questions = node.elements.select { |e| e.is_a?(Smartdown::Model::Element::MultipleChoice) }
42
+ questions.map(&:name)
48
43
  end
49
44
 
50
45
  def next_node_rules
@@ -65,11 +60,11 @@ module Smartdown
65
60
  rules.each do |rule|
66
61
  case rule
67
62
  when Smartdown::Model::Rule
68
- if predicate_evaluator.evaluate(rule.predicate, state_with_input)
63
+ if predicate_evaluator.evaluate(rule.predicate, state_with_inputs)
69
64
  throw(:match, rule)
70
65
  end
71
66
  when Smartdown::Model::NestedRule
72
- if predicate_evaluator.evaluate(rule.predicate, state_with_input)
67
+ if predicate_evaluator.evaluate(rule.predicate, state_with_inputs)
73
68
  throw_first_matching_rule_in(rule.children)
74
69
  end
75
70
  else
@@ -79,8 +74,12 @@ module Smartdown
79
74
  raise Smartdown::Engine::IndeterminateNextNode
80
75
  end
81
76
 
82
- def state_with_input
83
- state.put(node.name, input)
77
+ def state_with_inputs
78
+ result = state.put(node.name, inputs)
79
+ input_variable_names_from_question.each_with_index do |input_variable_name, index|
80
+ result = result.put(input_variable_name, inputs[index])
81
+ end
82
+ result
84
83
  end
85
84
  end
86
85
  end
@@ -25,10 +25,25 @@ module Smartdown
25
25
  end
26
26
 
27
27
  def process(responses, start_state = nil)
28
- responses.inject(start_state || default_start_state) do |state, input|
28
+ state = start_state || default_start_state
29
+ unprocessed_responses = responses
30
+ while !unprocessed_responses.empty? do
31
+ nb_questions = 0
29
32
  current_node = flow.node(state.get(:current_node))
30
- Transition.new(state, current_node, input).next_state
33
+ nb_questions += current_node.elements.select{|element|
34
+ element.is_a? Smartdown::Model::Element::MultipleChoice
35
+ }.count
36
+
37
+ #There is at least one relevant input per transition for now:
38
+ #Transition from start to first question relies on an input, regardless of its value
39
+ nb_relevant_inputs = [nb_questions, 1].max
40
+ input_array = unprocessed_responses.take(nb_relevant_inputs)
41
+ unprocessed_responses = unprocessed_responses.drop(nb_relevant_inputs)
42
+
43
+ transition = Transition.new(state, current_node, input_array)
44
+ state = transition.next_state
31
45
  end
46
+ state
32
47
  end
33
48
 
34
49
  def evaluate_node(state)
@@ -11,10 +11,24 @@ module Smartdown
11
11
  elements_of_kind(Smartdown::Model::Element::MultipleChoice)
12
12
  end
13
13
 
14
+ #Because question titles and page titles use the same markdown,
15
+ #there are at least as many or more headings than questions on each page
16
+ #To get only the question titles, we are assuming that all the headings that
17
+ #are not question headings come first in the markdown, then question headings
18
+ def question_titles
19
+ h1s.drop(h1s.count - questions.count)
20
+ end
21
+
14
22
  def title
15
23
  h1s.first ? h1s.first.content : ""
16
24
  end
17
25
 
26
+ def body
27
+ markdown_blocks[1..-1].map { |block| as_markdown(block) }.compact.join("\n")
28
+ end
29
+
30
+ private
31
+
18
32
  def markdown_blocks
19
33
  elements_of_kind(Smartdown::Model::Element::MarkdownHeading, Smartdown::Model::Element::MarkdownParagraph)
20
34
  end
@@ -23,10 +37,6 @@ module Smartdown
23
37
  elements_of_kind(Smartdown::Model::Element::MarkdownHeading)
24
38
  end
25
39
 
26
- def body
27
- markdown_blocks[1..-1].map { |block| as_markdown(block) }.compact.join("\n")
28
- end
29
-
30
40
  private
31
41
  def elements_of_kind(*kinds)
32
42
  elements.select {|e| kinds.any? {|k| e.is_a?(k)} }
@@ -1,3 +1,3 @@
1
1
  module Smartdown
2
- VERSION = "0.0.4"
2
+ VERSION = "0.1.0"
3
3
  end
@@ -42,7 +42,7 @@ describe Smartdown::Engine::Interpolator do
42
42
  paragraph("False case")
43
43
  end
44
44
  end
45
- multiple_choice({})
45
+ multiple_choice("example", {})
46
46
  next_node_rules
47
47
  end
48
48
  }
@@ -7,10 +7,11 @@ describe Smartdown::Engine::Transition do
7
7
  let(:current_node_name) { "q1" }
8
8
  let(:start_state) { Smartdown::Engine::State.new(current_node: current_node_name) }
9
9
  let(:input) { "yes" }
10
- subject(:transition) { described_class.new(start_state, current_node, input, predicate_evaluator: predicate_evaluator) }
10
+ let(:input_array) { [input] }
11
+ subject(:transition) { described_class.new(start_state, current_node, input_array, predicate_evaluator: predicate_evaluator) }
11
12
  let(:predicate_evaluator) { instance_double("Smartdown::Engine::PredicateEvaluator", evaluate: true) }
12
13
  let(:state_including_input) {
13
- start_state.put(current_node.name, input)
14
+ start_state.put(current_node.name, input_array)
14
15
  }
15
16
 
16
17
  context "no next node rules" do
@@ -91,7 +92,7 @@ describe Smartdown::Engine::Transition do
91
92
  .put(:responses, [input])
92
93
  .put(:path, [current_node_name])
93
94
  .put(:current_node, outcome_name1)
94
- .put(current_node.name, input)
95
+ .put(current_node.name, input_array)
95
96
 
96
97
  expect(transition.next_state).to eq(expected_state)
97
98
  end
data/spec/engine_spec.rb CHANGED
@@ -1,20 +1,36 @@
1
1
  require 'smartdown/engine'
2
2
 
3
3
  describe Smartdown::Engine do
4
+
5
+ subject(:engine) { Smartdown::Engine.new(flow) }
6
+ let(:start_state) {
7
+ engine.default_start_state
8
+ .put(:eea_passport?, ->(state) {
9
+ %w{greek british}.include?(state.get(:what_passport_do_you_have?))
10
+ })
11
+ .put(:imaginary?, ->(state) {
12
+ %w{narnia}.include?(state.get(:what_country_are_you_going_to?))
13
+ })
14
+ .put(:otherwise, true)
15
+ }
16
+
4
17
  let(:flow) {
5
18
  build_flow("check-uk-visa") do
6
19
  node("check-uk-visa") do
7
20
  heading("Check uk visa")
8
21
  paragraph("This is the paragraph")
9
- start_button("what_passport_do_you_have?")
22
+ start_button("passport_question")
10
23
  end
11
24
 
12
- node("what_passport_do_you_have?") do
25
+ node("passport_question") do
13
26
  heading("What passport do you have?")
14
27
  multiple_choice(
15
- greek: "Greek",
16
- british: "British",
17
- usa: "USA"
28
+ "what_passport_do_you_have?",
29
+ {
30
+ greek: "Greek",
31
+ british: "British",
32
+ usa: "USA"
33
+ }
18
34
  )
19
35
  next_node_rules do
20
36
  rule do
@@ -41,14 +57,64 @@ describe Smartdown::Engine do
41
57
  end
42
58
  end
43
59
  }
60
+ let(:two_questions_per_page_flow) {
61
+ build_flow("check-uk-visa") do
62
+ node("check-uk-visa") do
63
+ heading("Check uk visa")
64
+ paragraph("This is the paragraph")
65
+ start_button("passport_question")
66
+ end
44
67
 
45
- subject(:engine) { Smartdown::Engine.new(flow) }
46
- let(:start_state) {
47
- engine.default_start_state
48
- .put(:eea_passport?, ->(state) {
49
- %w{greek british}.include?(state.get(:what_passport_do_you_have?))
50
- })
51
- .put(:otherwise, true)
68
+ node("passport_question") do
69
+ heading("What passport do you have?")
70
+ multiple_choice(
71
+ "what_passport_do_you_have?",
72
+ {
73
+ greek: "Greek",
74
+ british: "British",
75
+ usa: "USA"
76
+ }
77
+ )
78
+ heading("What country are you going to?")
79
+ multiple_choice(
80
+ "what_country_are_you_going_to?",
81
+ {
82
+ usa: "USA",
83
+ narnia: "Narnia"
84
+ }
85
+ )
86
+ next_node_rules do
87
+ rule do
88
+ named_predicate("imaginary?")
89
+ outcome("outcome_imaginary_country")
90
+ end
91
+ rule do
92
+ named_predicate("eea_passport?")
93
+ outcome("outcome_no_visa_needed")
94
+ end
95
+ end
96
+ end
97
+
98
+ node("outcome_no_visa_needed") do
99
+ conditional do
100
+ named_predicate "pred?"
101
+ true_case do
102
+ paragraph("True case")
103
+ end
104
+ false_case do
105
+ paragraph("False case")
106
+ end
107
+ end
108
+ end
109
+
110
+ node("outcome_imaginary_country") do
111
+ paragraph("Imaginary country")
112
+ end
113
+
114
+ node("outcome_with_interpolation") do
115
+ paragraph("The answer is %{interpolated_variable}")
116
+ end
117
+ end
52
118
  }
53
119
 
54
120
  describe "#process" do
@@ -60,31 +126,62 @@ describe Smartdown::Engine do
60
126
  it { should be_a(Smartdown::Engine::State) }
61
127
 
62
128
  it "current_node of 'what_passport_do_you_have?'" do
63
- expect(subject.get(:current_node)).to eq("what_passport_do_you_have?")
129
+ expect(subject.get(:current_node)).to eq("passport_question")
64
130
  end
65
131
 
66
132
  it "input recorded in state variable corresponding to node name" do
67
- expect(subject.get("check-uk-visa")).to eq("yes")
133
+ expect(subject.get("check-uk-visa")).to eq(["yes"])
68
134
  end
69
135
  end
70
136
 
71
- context "greek passport" do
72
- let(:responses) { %w{yes greek} }
137
+ context "one question per page" do
138
+ context "greek passport" do
139
+ let(:responses) { %w{yes greek} }
140
+
141
+ it "is on outcome_no_visa_needed" do
142
+ expect(subject.get(:current_node)).to eq("outcome_no_visa_needed")
143
+ end
73
144
 
74
- it "is on outcome_no_visa_needed" do
75
- expect(subject.get(:current_node)).to eq("outcome_no_visa_needed")
145
+ it "has recorded input" do
146
+ expect(subject.get("what_passport_do_you_have?")).to eq("greek")
147
+ end
76
148
  end
77
149
 
78
- it "has recorded input" do
79
- expect(subject.get("what_passport_do_you_have?")).to eq("greek")
150
+ context "USA passport" do
151
+ let(:responses) { %w{yes usa} }
152
+
153
+ it "raises IndeterminateNextNode error" do
154
+ expect { subject }.to raise_error(Smartdown::Engine::IndeterminateNextNode)
155
+ end
80
156
  end
81
157
  end
82
158
 
83
- context "USA passport" do
84
- let(:responses) { %w{yes usa} }
159
+ context "two questions per page" do
160
+ let(:flow) { two_questions_per_page_flow }
161
+ context "narnia" do
162
+ let(:responses) { %w{yes greek narnia} }
85
163
 
86
- it "raises IndeterminateNextNode error" do
87
- expect { subject }.to raise_error(Smartdown::Engine::IndeterminateNextNode)
164
+ it "is on outcome_no_visa_needed" do
165
+ expect(subject.get(:current_node)).to eq("outcome_imaginary_country")
166
+ end
167
+
168
+ it "has recorded inputs" do
169
+ expect(subject.get("what_passport_do_you_have?")).to eq("greek")
170
+ expect(subject.get("what_country_are_you_going_to?")).to eq("narnia")
171
+ end
172
+ end
173
+
174
+ context "usa" do
175
+ let(:responses) { %w{yes greek usa} }
176
+
177
+ it "is on outcome_no_visa_needed" do
178
+ expect(subject.get(:current_node)).to eq("outcome_no_visa_needed")
179
+ end
180
+
181
+ it "has recorded inputs" do
182
+ expect(subject.get("what_passport_do_you_have?")).to eq("greek")
183
+ expect(subject.get("what_country_are_you_going_to?")).to eq("usa")
184
+ end
88
185
  end
89
186
  end
90
187
  end
@@ -45,10 +45,10 @@ class ModelBuilder
45
45
  @elements.last
46
46
  end
47
47
 
48
- def multiple_choice(options)
48
+ def multiple_choice(name, options)
49
49
  @elements ||= []
50
50
  options_with_string_keys = ::Hash[options.map {|k,v| [k.to_s, v]}]
51
- @elements << Smartdown::Model::Element::MultipleChoice.new(nil, options_with_string_keys)
51
+ @elements << Smartdown::Model::Element::MultipleChoice.new(name, options_with_string_keys)
52
52
  @elements.last
53
53
  end
54
54
 
@@ -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::MultipleChoice.new(nil, {"greek" => "Greek", "british" => "British"}),
18
+ Smartdown::Model::Element::MultipleChoice.new("what_passport_do_you_have?", {"greek" => "Greek", "british" => "British"}),
19
19
  Smartdown::Model::NextNodeRules.new([
20
20
  Smartdown::Model::Rule.new(
21
21
  Smartdown::Model::Predicate::Named.new("eea_passport?"),
@@ -40,8 +40,11 @@ describe ModelBuilder do
40
40
  node("what_passport_do_you_have?") do
41
41
  heading("What passport do you have?")
42
42
  multiple_choice(
43
- greek: "Greek",
44
- british: "British"
43
+ "what_passport_do_you_have?",
44
+ {
45
+ greek: "Greek",
46
+ british: "British"
47
+ }
45
48
  )
46
49
  next_node_rules do
47
50
  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.0.4
4
+ version: 0.1.0
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: !ruby/object:Gem::Requirement
16
+ requirement: &10785560 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ~>
@@ -21,15 +21,10 @@ dependencies:
21
21
  version: 1.6.1
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: !ruby/object:Gem::Requirement
25
- none: false
26
- requirements:
27
- - - ~>
28
- - !ruby/object:Gem::Version
29
- version: 1.6.1
24
+ version_requirements: *10785560
30
25
  - !ruby/object:Gem::Dependency
31
26
  name: rspec
32
- requirement: !ruby/object:Gem::Requirement
27
+ requirement: &10784820 !ruby/object:Gem::Requirement
33
28
  none: false
34
29
  requirements:
35
30
  - - ~>
@@ -37,15 +32,10 @@ dependencies:
37
32
  version: 3.0.0
38
33
  type: :development
39
34
  prerelease: false
40
- version_requirements: !ruby/object:Gem::Requirement
41
- none: false
42
- requirements:
43
- - - ~>
44
- - !ruby/object:Gem::Version
45
- version: 3.0.0
35
+ version_requirements: *10784820
46
36
  - !ruby/object:Gem::Dependency
47
37
  name: rake
48
- requirement: !ruby/object:Gem::Requirement
38
+ requirement: &10784240 !ruby/object:Gem::Requirement
49
39
  none: false
50
40
  requirements:
51
41
  - - ! '>='
@@ -53,15 +43,10 @@ dependencies:
53
43
  version: '0'
54
44
  type: :development
55
45
  prerelease: false
56
- version_requirements: !ruby/object:Gem::Requirement
57
- none: false
58
- requirements:
59
- - - ! '>='
60
- - !ruby/object:Gem::Version
61
- version: '0'
46
+ version_requirements: *10784240
62
47
  - !ruby/object:Gem::Dependency
63
48
  name: gem_publisher
64
- requirement: !ruby/object:Gem::Requirement
49
+ requirement: &10783580 !ruby/object:Gem::Requirement
65
50
  none: false
66
51
  requirements:
67
52
  - - ! '>='
@@ -69,12 +54,7 @@ dependencies:
69
54
  version: '0'
70
55
  type: :development
71
56
  prerelease: false
72
- version_requirements: !ruby/object:Gem::Requirement
73
- none: false
74
- requirements:
75
- - - ! '>='
76
- - !ruby/object:Gem::Version
77
- version: '0'
57
+ version_requirements: *10783580
78
58
  description:
79
59
  email: david.heath@digital.cabinet-office.gov.uk
80
60
  executables:
@@ -177,7 +157,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
177
157
  version: '0'
178
158
  segments:
179
159
  - 0
180
- hash: 3856298649637769047
160
+ hash: -476092451501996578
181
161
  required_rubygems_version: !ruby/object:Gem::Requirement
182
162
  none: false
183
163
  requirements:
@@ -186,10 +166,10 @@ required_rubygems_version: !ruby/object:Gem::Requirement
186
166
  version: '0'
187
167
  segments:
188
168
  - 0
189
- hash: 3856298649637769047
169
+ hash: -476092451501996578
190
170
  requirements: []
191
171
  rubyforge_project:
192
- rubygems_version: 1.8.23
172
+ rubygems_version: 1.8.11
193
173
  signing_key:
194
174
  specification_version: 3
195
175
  summary: Interactive question-answer flows using markdown-like external DSL