smartdown 0.7.1 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. data/README.md +9 -0
  2. data/lib/smartdown/api/flow.rb +4 -3
  3. data/lib/smartdown/api/node.rb +1 -1
  4. data/lib/smartdown/api/state.rb +10 -9
  5. data/lib/smartdown/engine/state.rb +2 -1
  6. data/lib/smartdown/engine/transition.rb +10 -10
  7. data/lib/smartdown/engine.rb +5 -0
  8. data/lib/smartdown/model/answer/base.rb +33 -5
  9. data/lib/smartdown/model/answer/date.rb +6 -1
  10. data/lib/smartdown/model/answer/money.rb +4 -1
  11. data/lib/smartdown/model/answer/multiple_choice.rb +11 -0
  12. data/lib/smartdown/model/answer/salary.rb +15 -5
  13. data/lib/smartdown/model/front_matter.rb +2 -2
  14. data/lib/smartdown/model/node.rb +17 -3
  15. data/lib/smartdown/parser/node_transform.rb +15 -15
  16. data/lib/smartdown/parser/predicates.rb +1 -1
  17. data/lib/smartdown/version.rb +1 -1
  18. data/spec/acceptance/flow_spec.rb +64 -0
  19. data/spec/acceptance/parsing_spec.rb +6 -0
  20. data/spec/api/previous_question_spec.rb +5 -1
  21. data/spec/api/state_spec.rb +2 -2
  22. data/spec/engine/interpolator_spec.rb +0 -11
  23. data/spec/engine/state_spec.rb +3 -2
  24. data/spec/engine/transition_spec.rb +1 -1
  25. data/spec/engine_spec.rb +79 -1
  26. data/spec/fixtures/acceptance/one-question/questions/q1.txt +2 -0
  27. data/spec/model/answer/base_spec.rb +22 -0
  28. data/spec/model/answer/date_spec.rb +15 -0
  29. data/spec/model/answer/money_spec.rb +6 -0
  30. data/spec/model/answer/multiple_choice_spec.rb +14 -1
  31. data/spec/model/answer/salary_spec.rb +29 -3
  32. data/spec/model/front_matter_spec.rb +35 -0
  33. data/spec/model/predicates/comparison_spec.rb +3 -3
  34. data/spec/model/predicates/function_spec.rb +11 -0
  35. data/spec/parser/predicates_spec.rb +15 -0
  36. metadata +13 -11
data/README.md CHANGED
@@ -295,6 +295,15 @@ outcome_the_result
295
295
  outcome_the_other_result
296
296
  ```
297
297
 
298
+ ##Code terminology
299
+
300
+ ####Answers vs responses
301
+
302
+ The words 'answers' and 'responses' are used for various variable names and method names throughout the gem.
303
+ Both are used to describe an answer to a question, but indicate two different formats:
304
+ * ```response``` is used for raw string inputs
305
+ * ```answer``` is used for Model::Answer objects
306
+
298
307
  ## Software design
299
308
 
300
309
  The initial plan for software design can be seen in this diagram:
@@ -22,7 +22,8 @@ module Smartdown
22
22
  state = smartdown_state(started, responses)
23
23
  State.new(transform_node(evaluate_node(node_by_name(state.get(:current_node)), state)),
24
24
  previous_question_nodes_for(state),
25
- responses
25
+ state.get(:accepted_responses)[1..-1] || [],
26
+ state.get(:current_answers)
26
27
  )
27
28
  end
28
29
 
@@ -35,11 +36,11 @@ module Smartdown
35
36
  end
36
37
 
37
38
  def meta_description
38
- front_matter.meta_description
39
+ front_matter.fetch meta_description, nil
39
40
  end
40
41
 
41
42
  def need_id
42
- front_matter.satisfies_need
43
+ front_matter.fetch satisfies_need, nil
43
44
  end
44
45
 
45
46
  def status
@@ -26,7 +26,7 @@ module Smartdown
26
26
  build_govspeak(elements_before_smartdown)
27
27
  end
28
28
 
29
- def devolved_body
29
+ def post_body
30
30
  elements_after_smartdown = elements.drop_while{|element| !smartdown_element?(element)}
31
31
  build_govspeak(elements_after_smartdown)
32
32
  end
@@ -4,22 +4,23 @@ module Smartdown
4
4
  module Api
5
5
  class State
6
6
 
7
- attr_reader :responses, :current_node
7
+ attr_reader :accepted_responses, :current_node, :current_answers
8
8
 
9
- def initialize(current_node, previous_questionpage_smartdown_nodes, responses)
9
+ def initialize(current_node, previous_questionpage_smartdown_nodes, accepted_responses, current_answers)
10
10
  @current_node = current_node
11
11
  @previous_questionpage_smartdown_nodes = previous_questionpage_smartdown_nodes
12
- @responses = responses
12
+ @accepted_responses = accepted_responses
13
+ @current_answers = current_answers
13
14
  end
14
15
 
15
- def answers
16
- previous_question_pages(responses).map { |previous_question_page|
16
+ def previous_answers
17
+ previous_question_pages.map { |previous_question_page|
17
18
  previous_question_page.answers
18
19
  }.flatten
19
20
  end
20
21
 
21
- def previous_question_pages(responses)
22
- @previous_question_pages ||= build_question_pages(responses)
22
+ def previous_question_pages
23
+ @previous_question_pages ||= build_question_pages(accepted_responses)
23
24
  end
24
25
 
25
26
  def started?
@@ -31,12 +32,12 @@ module Smartdown
31
32
  end
32
33
 
33
34
  def current_question_number
34
- responses.count + 1
35
+ accepted_responses.count + 1
35
36
  end
36
37
 
37
38
  private
38
39
 
39
- attr_reader :smartdown_state, :previous_questionpage_smartdown_nodes
40
+ attr_reader :previous_questionpage_smartdown_nodes
40
41
 
41
42
  def build_question_pages(responses)
42
43
  resp = responses.dup
@@ -8,7 +8,8 @@ module Smartdown
8
8
  def initialize(data = {})
9
9
  @data = duplicate_and_normalize_hash(data)
10
10
  @data["path"] ||= []
11
- @data["responses"] ||= []
11
+ @data["accepted_responses"] ||= []
12
+ @data["current_answers"] ||= []
12
13
  @cached = {}
13
14
  raise ArgumentError, "must specify current_node" unless has_key?("current_node")
14
15
  end
@@ -3,12 +3,12 @@ require 'smartdown/engine/errors'
3
3
  module Smartdown
4
4
  class Engine
5
5
  class Transition
6
- attr_reader :state, :node, :inputs
6
+ attr_reader :state, :node, :answers
7
7
 
8
- def initialize(state, node, input_array, options = {})
8
+ def initialize(state, node, answers, options = {})
9
9
  @state = state
10
10
  @node = node
11
- @inputs = input_array
11
+ @answers = answers
12
12
  end
13
13
 
14
14
  def next_node
@@ -18,9 +18,9 @@ module Smartdown
18
18
  end
19
19
 
20
20
  def next_state
21
- state_with_inputs
21
+ state_with_responses
22
22
  .put(:path, state.get(:path) + [node.name])
23
- .put(:responses, state.get(:responses) + inputs)
23
+ .put(:accepted_responses, state.get(:accepted_responses) + answers.map(&:to_s))
24
24
  .put(:current_node, next_node)
25
25
  end
26
26
 
@@ -47,11 +47,11 @@ module Smartdown
47
47
  rules.each do |rule|
48
48
  case rule
49
49
  when Smartdown::Model::Rule
50
- if rule.predicate.evaluate(state_with_inputs)
50
+ if rule.predicate.evaluate(state_with_responses)
51
51
  throw(:match, rule)
52
52
  end
53
53
  when Smartdown::Model::NestedRule
54
- if rule.predicate.evaluate(state_with_inputs)
54
+ if rule.predicate.evaluate(state_with_responses)
55
55
  throw_first_matching_rule_in(rule.children)
56
56
  end
57
57
  else
@@ -61,10 +61,10 @@ module Smartdown
61
61
  raise Smartdown::Engine::IndeterminateNextNode
62
62
  end
63
63
 
64
- def state_with_inputs
65
- result = state.put(node.name, inputs)
64
+ def state_with_responses
65
+ result = state.put(node.name, answers.map(&:to_s))
66
66
  input_variable_names_from_question.each_with_index do |input_variable_name, index|
67
- result = result.put(input_variable_name, inputs[index])
67
+ result = result.put(input_variable_name, answers[index])
68
68
  end
69
69
  result
70
70
  end
@@ -25,6 +25,7 @@ module Smartdown
25
25
  end
26
26
 
27
27
  def process(raw_responses, test_start_state = nil)
28
+ #TODO: change interface to match started, raw_responses like API state...no need for shifting etc...
28
29
  state = test_start_state || build_start_state
29
30
  unprocessed_responses = raw_responses
30
31
  while !unprocessed_responses.empty? do
@@ -38,6 +39,10 @@ module Smartdown
38
39
  question.answer_type.new(unprocessed_responses.shift, question)
39
40
  end
40
41
 
42
+ unless answers.all?(&:valid?)
43
+ state = state.put(:current_answers, answers)
44
+ break
45
+ end
41
46
  transition = Transition.new(state, current_node, answers)
42
47
  end
43
48
  state = transition.next_state
@@ -5,25 +5,46 @@ module Smartdown
5
5
  extend Forwardable
6
6
  include Comparable
7
7
 
8
- def_delegators :value, :to_s, :humanize, :to_i, :to_f, :+, :-, :*, :/
8
+ def_delegators :value, :to_s, :humanize, :to_i, :to_f
9
9
 
10
10
  def value_type
11
11
  ::String
12
12
  end
13
13
 
14
+ def -(other)
15
+ value - parse_other_object(other)
16
+ end
17
+
18
+ def +(other)
19
+ value + parse_other_object(other)
20
+ end
21
+
22
+ def *(other)
23
+ value * parse_other_object(other)
24
+ end
25
+
26
+ def /(other)
27
+ value / parse_other_object(other)
28
+ end
29
+
14
30
  def <=>(other)
15
- value <=> parse_comparison_object(other)
31
+ value <=> parse_other_object(other)
16
32
  end
17
33
 
18
- attr_reader :question, :value
34
+ attr_reader :question, :value, :error
19
35
 
20
36
  def initialize(value, question=nil)
21
- @value = parse_value(value)
22
37
  @question = question
38
+ @value = check_value_not_nil(value)
39
+ @value = parse_value(value) if valid?
40
+ end
41
+
42
+ def valid?
43
+ @error.nil?
23
44
  end
24
45
 
25
46
  private
26
- def parse_comparison_object(comparison_object)
47
+ def parse_other_object(comparison_object)
27
48
  if comparison_object.is_a? Base
28
49
  comparison_object.value
29
50
  elsif comparison_object.is_a? value_type
@@ -36,6 +57,13 @@ module Smartdown
36
57
  def parse_value(value)
37
58
  value
38
59
  end
60
+
61
+ def check_value_not_nil(value)
62
+ unless value
63
+ @error = "Please answer this question"
64
+ end
65
+ value
66
+ end
39
67
  end
40
68
  end
41
69
  end
@@ -18,7 +18,12 @@ module Smartdown
18
18
 
19
19
  private
20
20
  def parse_value(value)
21
- ::Date.parse(value)
21
+ begin
22
+ ::Date.parse(value)
23
+ rescue ArgumentError
24
+ @error = "Invalid date"
25
+ return
26
+ end
22
27
  end
23
28
  end
24
29
  end
@@ -14,7 +14,10 @@ module Smartdown
14
14
  end
15
15
 
16
16
  def humanize
17
- "£#{'%.2f' % value}"
17
+ number_string = "£#{'%.2f' % value}".gsub(/(\d)(?=(\d\d\d)+(?!\d))/) do |digit_to_delimit|
18
+ "#{digit_to_delimit},"
19
+ end
20
+ number_string.end_with?(".00") ? number_string[0..-4] : number_string
18
21
  end
19
22
  end
20
23
  end
@@ -11,6 +11,17 @@ module Smartdown
11
11
  def humanize
12
12
  question.choices.fetch(value)
13
13
  end
14
+
15
+ private
16
+ def parse_value(value)
17
+ check_value_not_nil(value)
18
+ if valid?
19
+ unless question.choices.keys.include? value
20
+ @error = "Invalid choice"
21
+ end
22
+ end
23
+ value
24
+ end
14
25
  end
15
26
  end
16
27
  end
@@ -7,23 +7,34 @@ module Smartdown
7
7
  class Salary < Base
8
8
  attr_reader :period, :amount_per_period
9
9
 
10
+ FORMAT_REGEX = /^£?\W*([\d|,|]+[\.]?[\d]*)[-|\W*per\W*](week|month|year)$/
11
+
10
12
  def value_type
11
13
  ::Float
12
14
  end
13
15
 
14
16
  def to_s
15
- "#{'%.2f' % amount_per_period} per #{period}"
17
+ "#{'%.2f' % amount_per_period}-#{period}"
16
18
  end
17
19
 
18
20
  def humanize
19
21
  whole, decimal = separate_by_comma(amount_per_period)
20
- "£#{whole}.#{decimal} per #{period}"
22
+ if decimal == "00"
23
+ "£#{whole} per #{period}"
24
+ else
25
+ "£#{whole}.#{decimal} per #{period}"
26
+ end
21
27
  end
22
28
 
23
29
  private
24
30
  def parse_value(value)
25
- @amount_per_period, @period = value.split(/-|\W*per\W*/)
26
- @amount_per_period = @amount_per_period.to_f
31
+ matched_value = value.strip.match FORMAT_REGEX
32
+ unless matched_value
33
+ @error = "Invalid format"
34
+ return
35
+ end
36
+ @amount_per_period, @period = *matched_value[1..2]
37
+ @amount_per_period = @amount_per_period.gsub(",","").to_f
27
38
  yearly_total
28
39
  end
29
40
 
@@ -43,7 +54,6 @@ module Smartdown
43
54
  left.gsub!(/(\d)(?=(\d\d\d)+(?!\d))/) do |digit_to_delimit|
44
55
  "#{digit_to_delimit},"
45
56
  end
46
- right = "%02d" % right.to_i
47
57
  [left, right]
48
58
  end
49
59
  end
@@ -21,8 +21,8 @@ module Smartdown
21
21
  @attributes.has_key?(name.to_s)
22
22
  end
23
23
 
24
- def fetch(name)
25
- @attributes.fetch(name.to_s)
24
+ def fetch(name, *args)
25
+ @attributes.fetch(name.to_s, *args)
26
26
  end
27
27
 
28
28
  def to_hash
@@ -12,7 +12,11 @@ module Smartdown
12
12
  end
13
13
 
14
14
  def body
15
- markdown_blocks[1..-1].map { |block| as_markdown(block) }.compact.join("\n")
15
+ markdown_blocks_before_question.map { |block| as_markdown(block) }.compact.join("\n")
16
+ end
17
+
18
+ def post_body
19
+ markdown_blocks_after_question.map { |block| as_markdown(block) }.compact.join("\n")
16
20
  end
17
21
 
18
22
  def questions
@@ -35,8 +39,18 @@ module Smartdown
35
39
 
36
40
  private
37
41
 
38
- def markdown_blocks
39
- elements_of_kind(Smartdown::Model::Element::MarkdownHeading, Smartdown::Model::Element::MarkdownParagraph)
42
+ def markdown_blocks_before_question
43
+ elements.take_while { |e|
44
+ e.is_a?(Smartdown::Model::Element::MarkdownHeading) ||
45
+ e.is_a?(Smartdown::Model::Element::MarkdownParagraph)
46
+ }[1..-1]
47
+ end
48
+
49
+ def markdown_blocks_after_question
50
+ elements.reverse.take_while { |e|
51
+ e.is_a?(Smartdown::Model::Element::MarkdownHeading) ||
52
+ e.is_a?(Smartdown::Model::Element::MarkdownParagraph)
53
+ }.reverse
40
54
  end
41
55
 
42
56
  def h1s
@@ -33,15 +33,15 @@ module Smartdown
33
33
  }
34
34
 
35
35
  rule(h1: simple(:content)) {
36
- Smartdown::Model::Element::MarkdownHeading.new(content)
36
+ Smartdown::Model::Element::MarkdownHeading.new(content.to_s)
37
37
  }
38
38
 
39
39
  rule(p: simple(:content)) {
40
- Smartdown::Model::Element::MarkdownParagraph.new(content)
40
+ Smartdown::Model::Element::MarkdownParagraph.new(content.to_s)
41
41
  }
42
42
 
43
43
  rule(:start_button => simple(:start_node)) {
44
- Smartdown::Model::Element::StartButton.new(start_node)
44
+ Smartdown::Model::Element::StartButton.new(start_node.to_s)
45
45
  }
46
46
 
47
47
  rule(:front_matter => subtree(:attrs), body: subtree(:body)) {
@@ -66,7 +66,7 @@ module Smartdown
66
66
 
67
67
  rule(:multiple_choice => {identifier: simple(:identifier), options: subtree(:choices)}) {
68
68
  Smartdown::Model::Element::Question::MultipleChoice.new(
69
- identifier, Hash[choices]
69
+ identifier.to_s, Hash[choices]
70
70
  )
71
71
  }
72
72
 
@@ -115,17 +115,17 @@ module Smartdown
115
115
  }
116
116
 
117
117
  rule(:equality_predicate => { varname: simple(:varname), expected_value: simple(:expected_value) }) {
118
- Smartdown::Model::Predicate::Equality.new(varname, expected_value)
118
+ Smartdown::Model::Predicate::Equality.new(varname.to_s, expected_value.to_s)
119
119
  }
120
120
 
121
121
  rule(:set_value => simple(:value)) { value }
122
122
 
123
123
  rule(:set_membership_predicate => { varname: simple(:varname), values: subtree(:values) }) {
124
- Smartdown::Model::Predicate::SetMembership.new(varname, values)
124
+ Smartdown::Model::Predicate::SetMembership.new(varname.to_s, values)
125
125
  }
126
126
 
127
127
  rule(:named_predicate => simple(:name) ) {
128
- Smartdown::Model::Predicate::Named.new(name)
128
+ Smartdown::Model::Predicate::Named.new(name.to_s)
129
129
  }
130
130
 
131
131
  rule(:otherwise_predicate => simple(:name) ) {
@@ -136,14 +136,14 @@ module Smartdown
136
136
  Smartdown::Model::Predicate::Combined.new([first_predicate]+and_predicates)
137
137
  }
138
138
 
139
- rule(:function_argument => simple(:argument)) { argument }
139
+ rule(:function_argument => simple(:argument)) { argument.to_s }
140
140
 
141
141
  rule(:function_predicate => { name: simple(:name), arguments: subtree(:arguments) }) {
142
- Smartdown::Model::Predicate::Function.new(name, Array(arguments))
142
+ Smartdown::Model::Predicate::Function.new(name.to_s, Array(arguments))
143
143
  }
144
144
 
145
145
  rule(:function_predicate => { name: simple(:name) }) {
146
- Smartdown::Model::Predicate::Function.new(name, [])
146
+ Smartdown::Model::Predicate::Function.new(name.to_s, [])
147
147
  }
148
148
 
149
149
  rule(:comparison_predicate => { varname: simple(:varname),
@@ -152,20 +152,20 @@ module Smartdown
152
152
  }) {
153
153
  case operator
154
154
  when "<="
155
- Smartdown::Model::Predicate::Comparison::LessOrEqual.new(varname, value)
155
+ Smartdown::Model::Predicate::Comparison::LessOrEqual.new(varname.to_s, value.to_s)
156
156
  when "<"
157
- Smartdown::Model::Predicate::Comparison::Less.new(varname, value)
157
+ Smartdown::Model::Predicate::Comparison::Less.new(varname.to_s, value.to_s)
158
158
  when ">="
159
- Smartdown::Model::Predicate::Comparison::GreaterOrEqual.new(varname, value)
159
+ Smartdown::Model::Predicate::Comparison::GreaterOrEqual.new(varname.to_s, value.to_s)
160
160
  when ">"
161
- Smartdown::Model::Predicate::Comparison::Greater.new(varname, value)
161
+ Smartdown::Model::Predicate::Comparison::Greater.new(varname.to_s, value.to_s)
162
162
  else
163
163
  raise "Comparison operator not recognised"
164
164
  end
165
165
  }
166
166
 
167
167
  rule(:rule => {predicate: subtree(:predicate), outcome: simple(:outcome_name) } ) {
168
- Smartdown::Model::Rule.new(predicate, outcome_name)
168
+ Smartdown::Model::Rule.new(predicate, outcome_name.to_s)
169
169
  }
170
170
  rule(:nested_rule => {predicate: subtree(:predicate), child_rules: subtree(:child_rules) } ) {
171
171
  Smartdown::Model::NestedRule.new(predicate, child_rules)
@@ -60,7 +60,7 @@ module Smartdown
60
60
  }
61
61
 
62
62
  rule (:function_predicate) {
63
- identifier.as(:name) >>
63
+ (identifier >> str('?').maybe).as(:name) >>
64
64
  str('(') >>
65
65
  function_arguments.as(:arguments).maybe >>
66
66
  str(')')
@@ -1,3 +1,3 @@
1
1
  module Smartdown
2
- VERSION = "0.7.1"
2
+ VERSION = "0.8.0"
3
3
  end
@@ -32,6 +32,26 @@ describe Smartdown::Api::Flow do
32
32
  expect(tigers_and_cats_scenario.question_groups.first.first.name).to eq("question_1")
33
33
  expect(tigers_and_cats_scenario.question_groups.first.first.answer).to eq("cat")
34
34
  end
35
+
36
+ context "flow state" do
37
+ context "with no answers given" do
38
+ specify { expect(flow.state("y",[]).current_answers).to be_empty }
39
+ specify { expect(flow.state("y",[]).accepted_responses).to eq [] }
40
+ end
41
+
42
+ context "with a valid answer given to the first question" do
43
+ specify { expect(flow.state("y",["cat"]).current_node.name).to eq "outcome_safe_pet" }
44
+ specify { expect(flow.state("y",["cat"]).accepted_responses).to eq ["cat"] }
45
+ specify { expect(flow.state("y",["cat"]).current_answers).to eq [] }
46
+ end
47
+
48
+ context "with an invalid answer given to the first question" do
49
+ specify { expect(flow.state("y",["lynx"]).current_node.name).to eq "question_1" }
50
+ specify { expect(flow.state("y",["lynx"]).accepted_responses).to eq [] }
51
+ specify { expect(flow.state("y",["lynx"]).current_answers.first.error).to eq "Invalid choice" }
52
+ end
53
+ end
54
+
35
55
  end
36
56
 
37
57
  context "flow with two questions per page" do
@@ -60,6 +80,50 @@ describe Smartdown::Api::Flow do
60
80
  expect(tigers_and_cats_scenario.question_groups[1][1].name).to eq("trained_for_tigers")
61
81
  expect(tigers_and_cats_scenario.question_groups[1][1].answer).to eq("no")
62
82
  end
83
+
84
+ context "flow state" do
85
+ context "with no answers given" do
86
+ specify { expect(flow.state("y",[]).current_answers).to be_empty }
87
+ end
88
+
89
+ context "with a valid answer given to the first question, only first answer to second page" do
90
+ specify { expect(flow.state("y",["lion", "yes", nil]).current_node.name).to eq "question_2" }
91
+ specify { expect(flow.state("y",["lion", "yes", nil]).accepted_responses).to eq ["lion"] }
92
+ specify { expect(flow.state("y",["lion", "yes", nil]).current_answers.count).to eq 2 }
93
+ specify { expect(flow.state("y",["lion", "yes", nil]).current_answers[0].valid?).to be true }
94
+ specify { expect(flow.state("y",["lion", "yes", nil]).current_answers[1].error).to eq "Please answer this question" }
95
+ end
96
+
97
+ context "with a valid answer given to the first question, only second answer to second page" do
98
+ specify { expect(flow.state("y",["lion", nil, "yes"]).current_node.name).to eq "question_2" }
99
+ specify { expect(flow.state("y",["lion", nil, "yes"]).accepted_responses).to eq ["lion"] }
100
+ specify { expect(flow.state("y",["lion", nil, "yes"]).current_answers.count).to eq 2 }
101
+ specify { expect(flow.state("y",["lion", nil, "yes"]).current_answers[1].valid?).to be true }
102
+ specify { expect(flow.state("y",["lion", nil, "yes"]).current_answers[0].error).to eq "Please answer this question" }
103
+ end
104
+
105
+ context "with a valid answer given to the first question, only first invalid answer to second page" do
106
+ specify { expect(flow.state("y",["lion", "lynx", nil]).current_node.name).to eq "question_2" }
107
+ specify { expect(flow.state("y",["lion", "lynx", nil]).accepted_responses).to eq ["lion"] }
108
+ specify { expect(flow.state("y",["lion", "lynx", nil]).current_answers.count).to eq 2 }
109
+ specify { expect(flow.state("y",["lion", "lynx", nil]).current_answers[0].error).to eq "Invalid choice" }
110
+ specify { expect(flow.state("y",["lion", "lynx", nil]).current_answers[1].error).to eq "Please answer this question" }
111
+ end
112
+
113
+ context "with a valid answer given to the first question, empty answers to second page" do
114
+ specify { expect(flow.state("y",["lion", nil, nil]).current_node.name).to eq "question_2" }
115
+ specify { expect(flow.state("y",["lion", nil, nil]).accepted_responses).to eq ["lion"] }
116
+ specify { expect(flow.state("y",["lion", nil, nil]).current_answers.count).to eq 2 }
117
+ specify { expect(flow.state("y",["lion", nil, nil]).current_answers[0].error).to eq "Please answer this question" }
118
+ specify { expect(flow.state("y",["lion", nil, nil]).current_answers[1].error).to eq "Please answer this question" }
119
+ end
120
+
121
+ context "with valid answers to both pages" do
122
+ specify { expect(flow.state("y",["lion", "no", "no"]).current_node.name).to eq "outcome_untrained_with_lions" }
123
+ specify { expect(flow.state("y",["lion", "no", "no"]).accepted_responses).to eq ["lion", "no", "no"] }
124
+ specify { expect(flow.state("y",["lion", "no", "no"]).current_answers).to eq [] }
125
+ end
126
+ end
63
127
  end
64
128
 
65
129
  end
@@ -87,6 +87,12 @@ para 2.
87
87
  EXPECTED
88
88
  end
89
89
 
90
+ it "has a post body" do
91
+ expect(question_node.post_body).to eq(<<-EXPECTED)
92
+ Text after the question.
93
+ EXPECTED
94
+ end
95
+
90
96
  it "has a multiple choice question" do
91
97
  expect(question_node.questions).to match([instance_of(Smartdown::Model::Element::Question::MultipleChoice)])
92
98
  end
@@ -5,7 +5,11 @@ describe Smartdown::Api::PreviousQuestion do
5
5
 
6
6
  subject(:previous_question) { Smartdown::Api::PreviousQuestion.new(elements, response)}
7
7
  let(:elements) { [ multiple_choice_element ] }
8
- let(:multiple_choice_element) { Smartdown::Model::Element::Question::MultipleChoice.new }
8
+ let(:multiple_choice_element) {
9
+ Smartdown::Model::Element::Question::MultipleChoice.new("question",
10
+ {"answer" => "Beautiful answer"}
11
+ )
12
+ }
9
13
  let(:response) { double(:response) }
10
14
  let(:multiple_choice_class) { double(:multiple_choice_class, :new => nil) }
11
15
  let(:answer_type) { double(:answer_type) }
@@ -2,7 +2,7 @@ require 'smartdown/api/state'
2
2
 
3
3
  describe Smartdown::Api::State do
4
4
 
5
- subject(:state) { Smartdown::Api::State.new(current_node, previous_questionpage_smartdown_nodes, responses)}
5
+ subject(:state) { Smartdown::Api::State.new(current_node, previous_questionpage_smartdown_nodes, responses, double)}
6
6
  let(:current_node) { double(:current_node) }
7
7
  let(:previous_questionpage_smartdown_nodes) { [question_page_node_1, question_page_node_2] }
8
8
  let(:question_page_node_1) { double(:question_page_node_1, :questions => [double, double]) }
@@ -13,7 +13,7 @@ describe Smartdown::Api::State do
13
13
  describe "#previous_question_pages" do
14
14
  it "creates question pages with their corresponding responses" do
15
15
  stub_const("Smartdown::Api::PreviousQuestionPage", previous_question_page_class)
16
- state.previous_question_pages(responses)
16
+ state.previous_question_pages
17
17
  expect(Smartdown::Api::PreviousQuestionPage).to have_received(:new)
18
18
  .with(question_page_node_1, ["a", "b"])
19
19
  expect(Smartdown::Api::PreviousQuestionPage).to have_received(:new)
@@ -72,17 +72,6 @@ describe Smartdown::Engine::Interpolator do
72
72
  end
73
73
  end
74
74
 
75
- context "a paragraph containing a parslet slice" do
76
- let(:elements) { [Smartdown::Model::Element::MarkdownParagraph.new(Parslet::Slice.new(0, 'Hello %{name}'))] }
77
-
78
- it "interpolates without raising an error about gsub missing" do
79
- # Note: the parser actually produces parslet 'slices' rather than strings.
80
- # A parslet slice behaves like a string but doesn't have all of the methods of string.
81
- # This test is to document that fact and catch any regressions.
82
- expect(interpolated_node.elements.first.content).to eq("Hello #{example_name}")
83
- end
84
- end
85
-
86
75
  context "a paragraph containing function call" do
87
76
  let(:elements) { [Smartdown::Model::Element::MarkdownParagraph.new('%{double(number)}')] }
88
77
  let(:state) {
@@ -10,8 +10,9 @@ describe Smartdown::Engine::State do
10
10
  end
11
11
 
12
12
  it "initializes path and responses" do
13
- expect(subject.get(:responses)).to eq []
13
+ expect(subject.get(:accepted_responses)).to eq []
14
14
  expect(subject.get(:path)).to eq []
15
+ expect(subject.get(:current_answers)).to eq []
15
16
  end
16
17
 
17
18
  describe "#get" do
@@ -27,7 +28,7 @@ describe Smartdown::Engine::State do
27
28
 
28
29
  describe "#keys" do
29
30
  it "returns a set of all keys in the state" do
30
- expect(subject.keys).to eq(Set.new(["current_node", "path", "responses"]))
31
+ expect(subject.keys).to eq(Set.new(["current_node", "path", "accepted_responses", "current_answers"]))
31
32
  end
32
33
  end
33
34
 
@@ -88,7 +88,7 @@ describe Smartdown::Engine::Transition do
88
88
  describe "#next_state" do
89
89
  it "returns a state including a record of responses, path, and new current_node" do
90
90
  expected_state = start_state
91
- .put(:responses, [input])
91
+ .put(:accepted_responses, [input])
92
92
  .put(:path, [current_node_name])
93
93
  .put(:current_node, outcome_name1)
94
94
  .put(current_node.name, input_array)
data/spec/engine_spec.rb CHANGED
@@ -83,9 +83,30 @@ describe Smartdown::Engine do
83
83
  outcome("outcome_imaginary_country")
84
84
  end
85
85
  rule do
86
- set_membership_predicate("what_passport_do_you_have?", ["greek", "british"])
86
+ set_membership_predicate("what_passport_do_you_have?", ["greek"])
87
87
  outcome("outcome_no_visa_needed")
88
88
  end
89
+ rule do
90
+ set_membership_predicate("what_passport_do_you_have?", ["british"])
91
+ outcome("second_passport_question")
92
+ end
93
+ end
94
+ end
95
+
96
+ node("second_passport_question") do
97
+ heading("What colour is your passport?")
98
+ multiple_choice(
99
+ "what_colour_is_your_passport?",
100
+ {
101
+ red: "Red",
102
+ white: "White",
103
+ blue: "Blue"
104
+ }
105
+ )
106
+ next_node_rules do
107
+ rule do
108
+ outcome("outcome_passport_colour_specified")
109
+ end
89
110
  end
90
111
  end
91
112
 
@@ -108,6 +129,10 @@ describe Smartdown::Engine do
108
129
  node("outcome_with_interpolation") do
109
130
  paragraph("The answer is %{interpolated_variable}")
110
131
  end
132
+
133
+ node("outcome_passport_colour_specified") do
134
+ paragraph("What a pretty passport")
135
+ end
111
136
  end
112
137
  }
113
138
 
@@ -165,6 +190,16 @@ describe Smartdown::Engine do
165
190
  expect { subject }.to raise_error(Smartdown::Engine::IndeterminateNextNode)
166
191
  end
167
192
  end
193
+
194
+ context "no passport answer entered" do
195
+ let(:responses) { ["yes", nil] }
196
+
197
+ it "raises parsing errors" do
198
+ expect(subject.get(:current_node)).to eq("passport_question")
199
+ expect(subject.get("current_answers").count).to eq 1
200
+ expect(subject.get("current_answers").first.error).to eq "Please answer this question"
201
+ end
202
+ end
168
203
  end
169
204
 
170
205
  context "two questions per page" do
@@ -194,6 +229,49 @@ describe Smartdown::Engine do
194
229
  expect(subject.get("what_country_are_you_going_to?")).to eq("usa")
195
230
  end
196
231
  end
232
+
233
+ context "no answers given" do
234
+ let(:responses) { ["yes", nil, nil] }
235
+ it "raises parsing errors" do
236
+ expect(subject.get(:current_node)).to eq("passport_question")
237
+ expect(subject.get("current_answers").count).to eq 2
238
+ expect(subject.get("current_answers")[0].error).to eq "Please answer this question"
239
+ expect(subject.get("current_answers")[1].error).to eq "Please answer this question"
240
+ expect(subject.get("accepted_responses")).to eq ["yes"]
241
+ end
242
+ end
243
+
244
+ context "only second answer given" do
245
+ let(:responses) { ["yes", nil, "narnia"] }
246
+ it "raises parsing errors" do
247
+ expect(subject.get(:current_node)).to eq("passport_question")
248
+ expect(subject.get("current_answers").count).to eq 2
249
+ expect(subject.get("current_answers")[0].error).to eq "Please answer this question"
250
+ expect(subject.get("current_answers")[1].error).to be nil
251
+ expect(subject.get("accepted_responses")).to eq ["yes"]
252
+ end
253
+ end
254
+
255
+ context "only first answer given" do
256
+ let(:responses) { ["yes", "greek", nil] }
257
+ it "raises parsing errors" do
258
+ expect(subject.get(:current_node)).to eq("passport_question")
259
+ expect(subject.get("current_answers").count).to eq 2
260
+ expect(subject.get("current_answers")[0].error).to be nil
261
+ expect(subject.get("current_answers")[1].error).to eq "Please answer this question"
262
+ expect(subject.get("accepted_responses")).to eq ["yes"]
263
+ end
264
+ end
265
+
266
+ context "british, going to usa" do
267
+ let(:responses) { ["yes", "british", "usa", nil] }
268
+ it "raises parsing errors" do
269
+ expect(subject.get(:current_node)).to eq("second_passport_question")
270
+ expect(subject.get("current_answers").count).to eq 1
271
+ expect(subject.get("current_answers").first.error).to eq "Please answer this question"
272
+ expect(subject.get("accepted_responses")).to eq ["yes", "british", "usa"]
273
+ end
274
+ end
197
275
  end
198
276
  end
199
277
 
@@ -8,3 +8,5 @@ para 2.
8
8
  [choice: q1]
9
9
  * yes: Yes
10
10
  * no: No
11
+
12
+ Text after the question.
@@ -20,4 +20,26 @@ describe Smartdown::Model::Answer::Base do
20
20
  it { should respond_to(method) }
21
21
  end
22
22
  end
23
+
24
+ describe 'validations' do
25
+ describe 'valid?' do
26
+ it 'returns true if there are no errors on the question' do
27
+ expect(instance).to be_valid
28
+ end
29
+
30
+ it "has no error defined" do
31
+ expect(instance.error).to be nil
32
+ end
33
+
34
+ context "answer has been given a nil value" do
35
+ let(:value) { nil }
36
+ it 'returns false if there are no errors on the question' do
37
+ expect(instance).not_to be_valid
38
+ end
39
+ it "has an error asking for user to input a value" do
40
+ expect(instance.error).to eq "Please answer this question"
41
+ end
42
+ end
43
+ end
44
+ end
23
45
  end
@@ -14,6 +14,21 @@ describe Smartdown::Model::Answer::Date do
14
14
  specify { expect(instance.humanize).to eql "10 January 2000" }
15
15
  end
16
16
 
17
+ describe "errors" do
18
+
19
+ context "format is incorrect" do
20
+ let(:date_string) { "tomorrow" }
21
+ specify { expect(instance.error).to eql "Invalid date" }
22
+ specify { expect(instance.value).to eql nil }
23
+ end
24
+
25
+ context "answer not filled in" do
26
+ let(:date_string) { nil }
27
+ specify { expect(instance.error).to eql "Please answer this question" }
28
+ specify { expect(instance.value).to eql nil }
29
+ end
30
+ end
31
+
17
32
  describe "comparisons" do
18
33
  let(:date_string) { "2000-1-10" }
19
34
 
@@ -28,5 +28,11 @@ describe Smartdown::Model::Answer::Money do
28
28
  expect(instance.humanize).to eql("£523.42")
29
29
  end
30
30
  end
31
+ context "no pence" do
32
+ let(:money_float) { 523.00 }
33
+ it "rounds down amounts of money correctly" do
34
+ expect(instance.humanize).to eql("£523")
35
+ end
36
+ end
31
37
  end
32
38
  end
@@ -3,7 +3,8 @@ require 'smartdown/model/answer/multiple_choice'
3
3
 
4
4
  describe Smartdown::Model::Answer::MultipleChoice do
5
5
 
6
- subject(:answer) { described_class.new("answer", multiple_choice_question) }
6
+ let(:answer_string) { "answer" }
7
+ subject(:answer) { described_class.new(answer_string, multiple_choice_question) }
7
8
  let(:multiple_choice_question) {
8
9
  Smartdown::Model::Element::Question::MultipleChoice.new("question",
9
10
  {"answer" => "Beautiful answer"}
@@ -15,4 +16,16 @@ describe Smartdown::Model::Answer::MultipleChoice do
15
16
  expect(answer.humanize).to eq "Beautiful answer"
16
17
  end
17
18
  end
19
+
20
+ describe "errors" do
21
+ context "format is incorrect" do
22
+ let(:answer_string) { "kasjdf" }
23
+ specify { expect(answer.error).to eql "Invalid choice" }
24
+ end
25
+
26
+ context "answer not filled in" do
27
+ let(:answer_string) { nil }
28
+ specify { expect(answer.error).to eql "Please answer this question" }
29
+ end
30
+ end
18
31
  end
@@ -11,18 +11,18 @@ describe Smartdown::Model::Answer::Salary do
11
11
  specify { expect(instance.amount_per_period).to eql(500.00) }
12
12
 
13
13
  it "as a string, it should declare itself in the initial format provided" do
14
- expect(instance.to_s).to eql("500.00 per week")
14
+ expect(instance.to_s).to eql("500.00-week")
15
15
  end
16
16
 
17
17
  describe "#humanize" do
18
18
  it "declares itself in the initial format provided" do
19
- expect(instance.humanize).to eql("£500.00 per week")
19
+ expect(instance.humanize).to eql("£500 per week")
20
20
  end
21
21
 
22
22
  context "amounts over 999" do
23
23
  let(:salary_string) { "15000-week" }
24
24
  it "adds commas" do
25
- expect(instance.humanize).to eql("£15,000.00 per week")
25
+ expect(instance.humanize).to eql("£15,000 per week")
26
26
  end
27
27
  end
28
28
 
@@ -46,6 +46,32 @@ describe Smartdown::Model::Answer::Salary do
46
46
  expect(instance.humanize).to eql("£15,000.56 per week")
47
47
  end
48
48
  end
49
+
50
+ context "amounts with commas" do
51
+ let(:salary_string) { "15,0000-week" }
52
+ it "correct comma location" do
53
+ expect(instance.humanize).to eql("£150,000 per week")
54
+ end
55
+ end
56
+ end
57
+
58
+ describe "errors" do
59
+ context "invalid formatting" do
60
+ let(:salary_string) {"Loads'a'money"}
61
+
62
+ it "Has errors" do
63
+ expect(instance.error).to eql("Invalid format")
64
+ end
65
+ end
66
+
67
+ context "no input" do
68
+ let(:salary_string) { nil }
69
+
70
+ it "Has errors" do
71
+ expect(instance.error).to eql("Please answer this question")
72
+ end
73
+ end
74
+
49
75
  end
50
76
 
51
77
  context "declared by week" do
@@ -0,0 +1,35 @@
1
+ require 'smartdown/model/front_matter.rb'
2
+
3
+ describe Smartdown::Model::FrontMatter do
4
+
5
+ subject { described_class.new({ 'key' => :thing }) }
6
+
7
+ describe "fetch" do
8
+ context 'Has the attribute desired' do
9
+ it { expect(subject.fetch 'key').to eq(:thing) }
10
+
11
+ it "doesn't use the default" do
12
+ expect(subject.fetch 'key', 'default').to eq(:thing)
13
+ end
14
+ end
15
+
16
+ context 'Does not have the attribute desired' do
17
+ it 'raises error by default if the attribute is missing' do
18
+ expect { subject.fetch :hahhad }.to raise_error
19
+ end
20
+
21
+ it 'allows passing a default to return on failure' do
22
+ expect(subject.fetch :askdjf, 'draft').to eq ('draft')
23
+ end
24
+
25
+ it 'allows passing a nil default to return on failure' do
26
+ expect(subject.fetch :askdjf, nil).to be_nil
27
+ end
28
+
29
+ it 'raises an error if more than 2 arguments are passed' do
30
+ expect { subject.fetch :askdjf, 'draft', 'x' }.to raise_error(ArgumentError)
31
+ end
32
+ end
33
+ end
34
+ end
35
+
@@ -114,7 +114,7 @@ describe "comparison predicates" do
114
114
  end
115
115
 
116
116
  context "state has lower value" do
117
- let(:state) { Smartdown::Engine::State.new(current_node: "n", my_var: "2014-1-30") }
117
+ let(:state) { Smartdown::Engine::State.new(current_node: "n", my_var: Smartdown::Model::Answer::Date.new("2014-1-30")) }
118
118
  let(:results) { {
119
119
  :greater => false,
120
120
  :greater_or_equal => false,
@@ -129,7 +129,7 @@ describe "comparison predicates" do
129
129
  end
130
130
 
131
131
  context "state has identical value" do
132
- let(:state) { Smartdown::Engine::State.new(current_node: "n", my_var: "2014-1-31") }
132
+ let(:state) { Smartdown::Engine::State.new(current_node: "n", my_var: Smartdown::Model::Answer::Date.new("2014-1-31")) }
133
133
  let(:results) { {
134
134
  :greater => false,
135
135
  :greater_or_equal => true,
@@ -144,7 +144,7 @@ describe "comparison predicates" do
144
144
  end
145
145
 
146
146
  context "state has higher value" do
147
- let(:state) { Smartdown::Engine::State.new(current_node: "n", my_var: "2014-2-1") }
147
+ let(:state) { Smartdown::Engine::State.new(current_node: "n", my_var: Smartdown::Model::Answer::Date.new("2014-2-1")) }
148
148
  let(:results) { {
149
149
  :greater => true,
150
150
  :greater_or_equal => true,
@@ -45,6 +45,17 @@ describe Smartdown::Model::Predicate::Function do
45
45
  end
46
46
  end
47
47
 
48
+ context "with a ? in the function name" do
49
+ let(:function_name) { "is_odd?" }
50
+ let(:my_function) { ->(number) { number.odd? } }
51
+ subject(:predicate) { described_class.new(function_name, ["number"]) }
52
+ let(:state) { Smartdown::Engine::State.new(current_node: "n", "number" => 3, function_name => my_function) }
53
+
54
+ it "can be called normally" do
55
+ expect(predicate.evaluate(state)).to eq(true)
56
+ end
57
+ end
58
+
48
59
  context "with nested functions" do
49
60
  # nesting looks like: function_1(function_2(5))
50
61
  let(:function_1) { ->(x) { x - 1 } }
@@ -140,6 +140,21 @@ describe Smartdown::Parser::Predicates do
140
140
  end
141
141
  end
142
142
 
143
+ context "with ? in name" do
144
+ let(:source) { "function_name?()" }
145
+
146
+ it { should parse(source).as(function_predicate: { name: "function_name?" }) }
147
+
148
+ describe "transformed" do
149
+ let(:node_name) { "my_node" }
150
+ subject(:transformed) {
151
+ Smartdown::Parser::NodeInterpreter.new(node_name, source, parser: parser).interpret
152
+ }
153
+
154
+ it { should eq(Smartdown::Model::Predicate::Function.new("function_name?", [])) }
155
+ end
156
+ end
157
+
143
158
  context "single argument" do
144
159
  let(:source) { "function_name(arg_1)" }
145
160
 
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.7.1
4
+ version: 0.8.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: &17430720 !ruby/object:Gem::Requirement
16
+ requirement: &20602040 !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: *17430720
24
+ version_requirements: *20602040
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: rspec
27
- requirement: &17430000 !ruby/object:Gem::Requirement
27
+ requirement: &20601400 !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: *17430000
35
+ version_requirements: *20601400
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: rake
38
- requirement: &17429460 !ruby/object:Gem::Requirement
38
+ requirement: &20600760 !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: *17429460
46
+ version_requirements: *20600760
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: gem_publisher
49
- requirement: &17428800 !ruby/object:Gem::Requirement
49
+ requirement: &20600200 !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: *17428800
57
+ version_requirements: *20600200
58
58
  description:
59
59
  email: david.heath@digital.cabinet-office.gov.uk
60
60
  executables:
@@ -213,6 +213,7 @@ files:
213
213
  - spec/support_specs/model_builder_spec.rb
214
214
  - spec/engine_spec.rb
215
215
  - spec/model/node_spec.rb
216
+ - spec/model/front_matter_spec.rb
216
217
  - spec/model/flow_spec.rb
217
218
  - spec/model/answer/money_spec.rb
218
219
  - spec/model/answer/multiple_choice_spec.rb
@@ -241,7 +242,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
241
242
  version: '0'
242
243
  segments:
243
244
  - 0
244
- hash: 3609502559032787740
245
+ hash: -4304436723939506743
245
246
  required_rubygems_version: !ruby/object:Gem::Requirement
246
247
  none: false
247
248
  requirements:
@@ -250,7 +251,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
250
251
  version: '0'
251
252
  segments:
252
253
  - 0
253
- hash: 3609502559032787740
254
+ hash: -4304436723939506743
254
255
  requirements: []
255
256
  rubyforge_project:
256
257
  rubygems_version: 1.8.11
@@ -327,6 +328,7 @@ test_files:
327
328
  - spec/support_specs/model_builder_spec.rb
328
329
  - spec/engine_spec.rb
329
330
  - spec/model/node_spec.rb
331
+ - spec/model/front_matter_spec.rb
330
332
  - spec/model/flow_spec.rb
331
333
  - spec/model/answer/money_spec.rb
332
334
  - spec/model/answer/multiple_choice_spec.rb