smartdown 0.7.1 → 0.8.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.
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