smartdown 0.0.4 → 0.1.0
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/README.md +1 -1
- data/lib/smartdown/engine/transition.rb +18 -19
- data/lib/smartdown/engine.rb +17 -2
- data/lib/smartdown/model/node.rb +14 -4
- data/lib/smartdown/version.rb +1 -1
- data/spec/engine/interpolator_spec.rb +1 -1
- data/spec/engine/transition_spec.rb +4 -3
- data/spec/engine_spec.rb +121 -24
- data/spec/support/model_builder.rb +2 -2
- data/spec/support_specs/model_builder_spec.rb +6 -3
- metadata +12 -32
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
|
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, :
|
7
|
+
attr_reader :state, :node, :inputs
|
8
8
|
|
9
|
-
def initialize(state, node,
|
9
|
+
def initialize(state, node, input_array, options = {})
|
10
10
|
@state = state
|
11
11
|
@node = node
|
12
|
-
@
|
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
|
-
|
19
|
-
|
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
|
-
|
23
|
+
state_with_inputs
|
24
24
|
.put(:path, state.get(:path) + [node.name])
|
25
|
-
.put(:responses, state.get(:responses) +
|
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
|
42
|
-
|
43
|
-
|
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,
|
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,
|
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
|
83
|
-
state.put(node.name,
|
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
|
data/lib/smartdown/engine.rb
CHANGED
@@ -25,10 +25,25 @@ module Smartdown
|
|
25
25
|
end
|
26
26
|
|
27
27
|
def process(responses, start_state = nil)
|
28
|
-
|
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
|
-
|
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)
|
data/lib/smartdown/model/node.rb
CHANGED
@@ -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)} }
|
data/lib/smartdown/version.rb
CHANGED
@@ -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
|
-
|
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,
|
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,
|
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("
|
22
|
+
start_button("passport_question")
|
10
23
|
end
|
11
24
|
|
12
|
-
node("
|
25
|
+
node("passport_question") do
|
13
26
|
heading("What passport do you have?")
|
14
27
|
multiple_choice(
|
15
|
-
|
16
|
-
|
17
|
-
|
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
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
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("
|
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 "
|
72
|
-
|
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
|
-
|
75
|
-
|
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
|
-
|
79
|
-
|
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 "
|
84
|
-
let(:
|
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
|
-
|
87
|
-
|
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(
|
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(
|
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
|
-
|
44
|
-
|
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
|
+
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:
|
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:
|
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:
|
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:
|
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:
|
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:
|
169
|
+
hash: -476092451501996578
|
190
170
|
requirements: []
|
191
171
|
rubyforge_project:
|
192
|
-
rubygems_version: 1.8.
|
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
|