diff --git a/doc/smart-answer-flows.md b/doc/smart-answer-flows.md index 0ccbdec8398..826afbfacc1 100644 --- a/doc/smart-answer-flows.md +++ b/doc/smart-answer-flows.md @@ -107,19 +107,6 @@ next_node(permitted: permitted_next_nodes) do |response| end ``` -#### DEPRECATED: Using predicates - -This is the same example from above expressed using predicates: - -```ruby -next_node_if(:green, responded_with('green')) ) -next_node(:red) -``` - -The `responded_with` function actually returns a [predicate](http://en.wikipedia.org/wiki/Predicate_%28mathematical_logic%29) which will be invoked during processing. If the predicate returns `true` then the `:green` node will be next, otherwise the next rule will be evaluated. In this case the next rule says `:red` is the next node with no condition. - -See [Smart Answer predicates](smart-answers-predicates.md) for more detailed information about this style. - ### Storing data for later use You can use the `precalculate`, `next_node_calculation`, `save_input_as` and `calculate` methods to store data for later use. diff --git a/doc/smart-answers-predicates.md b/doc/smart-answers-predicates.md deleted file mode 100644 index f8071a4fae4..00000000000 --- a/doc/smart-answers-predicates.md +++ /dev/null @@ -1,47 +0,0 @@ -## Predicate helpers - -* `responded_with(value)` - `value` can be either a string or an array of values -* `variable_matches(varname, value)` - `varname` is a symbol representing the name of the variable to test, `value` can be either a single value or an array -* `response_has_all_of(required_responses)` - only for checkbox questions, true if all of the required responses were checked. `required_responses` can be a single value or an array. -* `response_is_one_of(responses)` - only for checkbox questions, true if ANY of the responses were checked. `responses` can be a single value or an array. - -## Combining predicates - -Predicates can be combined using logical conjunctions `|` or `&`: - -```ruby -next_node_if(:orange, variable_matches(:first_colour, "red") & variable_matches(:second_colour, "yellow")) -next_node_if(:monochrome, variable_matches(:first_colour, "black") | variable_matches(:second_colour, "white")) -``` - -## Structuring rules by nesting - -Predicates can also be organised by nesting using `on_condition`, e.g. - -```ruby -on_condition(responded_with("red")) do - next_node_if(:orange, variable_matches(:first_color, "yellow")) - next_node(:red) -end -next_node(:blue) -``` - -Here's a truth table for the above scenario: - -``` -| | Yellow | other -| Red | orange | red -| other | blue | blue -``` - -## Defining named predicates - -Named predicates can also be defined using - -```ruby -define_predicate(:can_has_cheesburger?) do |response| - # logic hereā¦ -end - -next_node_if(:something, can_has_cheesburger?) -``` diff --git a/lib/graph_presenter.rb b/lib/graph_presenter.rb index 5eb820bc114..9ee9052b995 100644 --- a/lib/graph_presenter.rb +++ b/lib/graph_presenter.rb @@ -13,9 +13,6 @@ def adjacency_list adjacency_list = {} @flow.questions.each do |node| adjacency_list[node.name] = [] - node.next_node_function_chain.each do |(nextnode, predicates)| - adjacency_list[node.name] << [nextnode, predicates.map(&:label).compact.join(" AND\n")] - end node.permitted_next_nodes.each do |permitted_next_node| existing_next_nodes = adjacency_list[node.name].map(&:first) unless existing_next_nodes.include?(permitted_next_node) diff --git a/lib/smart_answer/calculators/registrations_data_query.rb b/lib/smart_answer/calculators/registrations_data_query.rb index e47fec5123c..c20e8f98627 100644 --- a/lib/smart_answer/calculators/registrations_data_query.rb +++ b/lib/smart_answer/calculators/registrations_data_query.rb @@ -60,10 +60,6 @@ def commonwealth_country?(country_slug) COMMONWEALTH_COUNTRIES.include?(country_slug) end - def responded_with_commonwealth_country? - SmartAnswer::Predicate::RespondedWith.new(COMMONWEALTH_COUNTRIES, "commonwealth country") - end - def has_consulate?(country_slug) COUNTRIES_WITH_CONSULATES.include?(country_slug) end diff --git a/lib/smart_answer/predicate/base.rb b/lib/smart_answer/predicate/base.rb deleted file mode 100644 index c200f72d3f8..00000000000 --- a/lib/smart_answer/predicate/base.rb +++ /dev/null @@ -1,35 +0,0 @@ -module SmartAnswer - module Predicate - class Base - def initialize(label = nil, callable = nil) - @label = label - @callable = callable - end - - def call(state, input) - @callable.call(state, input) - end - - def or(other, new_label = nil) - SmartAnswer::Predicate::Base.new(new_label || "#{self.label} | #{other.label}", ->(state, input) { - self.call(state, input) || other.call(state, input) - }) - end - - alias_method :|, :or - - def and(other, new_label = nil) - SmartAnswer::Predicate::Base.new(new_label || "#{self.label} AND #{other.label}", ->(state, input) { - self.call(state, input) && other.call(state, input) - }) - end - - alias_method :&, :and - - def label - @label || "-- undefined --" - end - - end - end -end diff --git a/lib/smart_answer/predicate/callable.rb b/lib/smart_answer/predicate/callable.rb deleted file mode 100644 index 31bb04efdca..00000000000 --- a/lib/smart_answer/predicate/callable.rb +++ /dev/null @@ -1,18 +0,0 @@ -module SmartAnswer - module Predicate - class Callable < Base - def initialize(label = nil, callable = nil, &block) - callable_expecting_state_as_binding = block_given? ? block : callable - callable_taking_state_as_arg = ->(state, input) { - if callable_expecting_state_as_binding.arity == 0 - state.instance_exec &callable_expecting_state_as_binding - else - state.instance_exec(input, &callable_expecting_state_as_binding) - end - } - - super(label || "--defined by code--", callable_taking_state_as_arg) - end - end - end -end diff --git a/lib/smart_answer/predicate/responded_with.rb b/lib/smart_answer/predicate/responded_with.rb deleted file mode 100644 index 1cdb13f30ff..00000000000 --- a/lib/smart_answer/predicate/responded_with.rb +++ /dev/null @@ -1,22 +0,0 @@ -module SmartAnswer - module Predicate - class RespondedWith < Base - attr_reader :acceptable_responses - - def initialize(acceptable_responses, label = nil) - @acceptable_responses = [*acceptable_responses] - @label = label - end - - def call(state, response) - @acceptable_responses.include?(response.to_s) - end - - alias_method :|, :or - - def label - @label || @acceptable_responses.join(" | ") - end - end - end -end diff --git a/lib/smart_answer/predicate/response_has_all_of.rb b/lib/smart_answer/predicate/response_has_all_of.rb deleted file mode 100644 index 74ad3e44f5b..00000000000 --- a/lib/smart_answer/predicate/response_has_all_of.rb +++ /dev/null @@ -1,17 +0,0 @@ -module SmartAnswer - module Predicate - class ResponseHasAllOf < Base - def initialize(required_responses) - @required_responses = [*required_responses] - end - - def call(state, response) - (response.split(",") & @required_responses).size == @required_responses.size - end - - def label - @required_responses.join(" & ") - end - end - end -end diff --git a/lib/smart_answer/predicate/response_is_one_of.rb b/lib/smart_answer/predicate/response_is_one_of.rb deleted file mode 100644 index 2bb8696469a..00000000000 --- a/lib/smart_answer/predicate/response_is_one_of.rb +++ /dev/null @@ -1,17 +0,0 @@ -module SmartAnswer - module Predicate - class ResponseIsOneOf < Base - def initialize(accepted_responses) - @accepted_responses = [*accepted_responses] - end - - def call(state, response) - (response.split(",") & @accepted_responses).any? - end - - def label - @accepted_responses.join(" | ") - end - end - end -end diff --git a/lib/smart_answer/predicate/variable_matches.rb b/lib/smart_answer/predicate/variable_matches.rb deleted file mode 100644 index ae71071b7e3..00000000000 --- a/lib/smart_answer/predicate/variable_matches.rb +++ /dev/null @@ -1,53 +0,0 @@ -module SmartAnswer - module Predicate - class VariableMatches < Base - attr_reader :variable_name, :acceptable_responses - - def initialize(variable_name, acceptable_responses, match_description = nil, label = nil) - @variable_name = variable_name - @acceptable_responses = [*acceptable_responses] - @match_description = match_description - @label = label - end - - def call(state, response) - acceptable_responses.include?(state.send(variable_name)) - end - - def or(other) - if other.is_a?(VariableMatches) && other.variable_name == self.variable_name - super(other, "#{@variable_name} == #{self.match_description} | #{other.match_description}") - else - super - end - end - - alias_method :|, :or - - def match_description - @match_description || generate_match_description - end - - def label - @label || "#{variable_name} == #{match_description}" - end - - private - def generate_match_description - if multiple_acceptable_responses? - wrap_in_braces(acceptable_responses) - else - acceptable_responses.first || "" - end - end - - def multiple_acceptable_responses? - acceptable_responses.size > 1 - end - - def wrap_in_braces(set) - "{ #{set.join(" | ")} }" - end - end - end -end diff --git a/lib/smart_answer/question/base.rb b/lib/smart_answer/question/base.rb index 730c0f7a007..56b7a33352b 100644 --- a/lib/smart_answer/question/base.rb +++ b/lib/smart_answer/question/base.rb @@ -1,17 +1,13 @@ module SmartAnswer module Question class Base < Node - attr_reader :next_node_function_chain class NextNodeUndefined < StandardError; end def initialize(flow, name, options = {}, &block) @save_input_as = nil @validations ||= [] - @next_node_function_chain ||= [] @default_next_node_function ||= lambda {|_|} @permitted_next_nodes = [] - @predicate_stack = [] - @predicates = {} @uses_erb_template = options[:use_erb_template] super end @@ -24,18 +20,13 @@ def next_node(next_node = nil, permitted: [], &block) @permitted_next_nodes += permitted @default_next_node_function = block elsif next_node - next_node_if(next_node) + @permitted_next_nodes = [next_node] + @default_next_node_function = proc { next_node } else raise ArgumentError end end - def next_node_if(next_node, *predicates, &block) - predicates << block if block_given? - @next_node_function_chain << [next_node, normalize_predicates(@predicate_stack + predicates)] - @permitted_next_nodes << next_node - end - def validate(message = nil, &block) @validations << [message, block] end @@ -46,7 +37,7 @@ def permitted_next_nodes(*args) def next_node_for(current_state, input) validate!(current_state, input) - next_node = next_node_from_function_chain(current_state, input) || next_node_from_default_function(current_state, input) + next_node = next_node_from_default_function(current_state, input) responses_and_input = current_state.responses + [input] raise NextNodeUndefined.new("Next node undefined. Node: #{current_state.current_node}. Responses: #{responses_and_input}") unless next_node unless @permitted_next_nodes.include?(next_node) @@ -74,47 +65,6 @@ def transition(current_state, raw_input) new_state end - # Within an #on_condition block, all #next_node and #next_node_if - # clauses must additionally satisfy the given predicate. Nesting of - # #on_condition blocks is permitted. - # - # Example: - # - # on_condition(->(r) {r == 'tourism'}) do - # next_node_if(:outcome_visit_waiver) { %w(oman qatar united-arab-emirates).include?(passport_country) } - # next_node_if(:outcome_taiwan_exception) { %w(taiwan).include?(passport_country) } - # next_node_if(:outcome_school_n) do - # country_group_non_visa_national.include?(passport_country) or country_group_ukot.include?(passport_country)) - # end - # next_node(:outcome_general_y) - # end - def on_condition(predicate, &block) - @predicate_stack << predicate - instance_eval(&block) - @predicate_stack.pop - end - - def responded_with(acceptable_responses) - SmartAnswer::Predicate::RespondedWith.new(acceptable_responses) - end - - def variable_matches(variable_name, acceptable_responses) - SmartAnswer::Predicate::VariableMatches.new(variable_name, acceptable_responses) - end - - def define_predicate(identifier, label = nil, &block) - raise "method #{identifier} already defined" if self.class.method_defined?(identifier) - @predicates[identifier] = SmartAnswer::Predicate::Callable.new(label || identifier.to_s, &block) - end - - def respond_to_missing?(method, include_private = false) - @predicates.has_key?(method) - end - - def method_missing(method, *args, &block) - @predicates.fetch(method) - end - def parse_input(raw_input) raw_input end @@ -148,25 +98,6 @@ def validate!(current_state, input) end end - def normalize_predicates(predicates) - predicates.map do |predicate| - if predicate.is_a?(SmartAnswer::Predicate::Base) - predicate - else - SmartAnswer::Predicate::Callable.new(nil, predicate) - end - end - end - - def next_node_from_function_chain(current_state, input) - found = @next_node_function_chain.find do |(_, predicates)| - predicates.all? do |predicate| - predicate.call(current_state, input) - end - end - found && found.first - end - def next_node_from_default_function(current_state, input) current_state.instance_exec(input, &@default_next_node_function) end diff --git a/lib/smart_answer/question/checkbox.rb b/lib/smart_answer/question/checkbox.rb index 1a250adc679..e666327d6d6 100644 --- a/lib/smart_answer/question/checkbox.rb +++ b/lib/smart_answer/question/checkbox.rb @@ -33,21 +33,6 @@ def parse_input(raw_input) def to_response(input) input.split(',').reject {|v| v == NONE_OPTION } end - - # Returns a predicate function which when given a response - # returns a boolean indicating whether the response was one - # of the accepted responses. - def response_is_one_of(accepted_responses) - SmartAnswer::Predicate::ResponseIsOneOf.new(accepted_responses) - end - - def response_has_all_of(required_responses) - SmartAnswer::Predicate::ResponseHasAllOf.new(required_responses) - end - - def response_is_only(accepted_response) - ->(response) { response.split(",") == [accepted_response] } - end end end end diff --git a/test/data/marriage-abroad-files.yml b/test/data/marriage-abroad-files.yml index fab18b4ff8f..914d5cea98e 100644 --- a/test/data/marriage-abroad-files.yml +++ b/test/data/marriage-abroad-files.yml @@ -123,4 +123,4 @@ lib/smart_answer_flows/marriage-abroad/questions/what_is_your_partners_nationali lib/smart_answer_flows/shared/_overseas_passports_embassies.govspeak.erb: 2f521bae99c2f48b49d07bcb283a8063 lib/smart_answer/calculators/marriage_abroad_data_query.rb: b69ed89bee92f2361f86e7509fc27ebf lib/smart_answer/calculators/country_name_formatter.rb: 0cb274748f0daf3965451b6230c183fd -lib/smart_answer/calculators/registrations_data_query.rb: cbb0fad828d74606eb708d3442cf4ca5 +lib/smart_answer/calculators/registrations_data_query.rb: 563c5db7d7272bc07bd636a92b40f3ce diff --git a/test/data/register-a-birth-files.yml b/test/data/register-a-birth-files.yml index e00e239f855..6de695f6614 100644 --- a/test/data/register-a-birth-files.yml +++ b/test/data/register-a-birth-files.yml @@ -23,7 +23,7 @@ lib/data/rates/births_and_deaths_document_return_fees.yml: 4ac203e9fd076c12f62b5 lib/data/translators.yml: d86f628f0b85e24ffb0dc0ec77401387 lib/data/registrations.yml: 2eb5c61678f21bd9cc06af67c230279f lib/smart_answer/calculators/country_name_formatter.rb: 0cb274748f0daf3965451b6230c183fd -lib/smart_answer/calculators/registrations_data_query.rb: cbb0fad828d74606eb708d3442cf4ca5 +lib/smart_answer/calculators/registrations_data_query.rb: 563c5db7d7272bc07bd636a92b40f3ce lib/smart_answer/calculators/translator_links.rb: 079592f6c5ede06d7c6ae4e8c5553127 test/fixtures/worldwide_locations.yml: 063711faceec3a4081d6d5fb386c8029 test/fixtures/worldwide/afghanistan_organisations.json: 1fd89b60eb079c7b3fddcab985f37366 diff --git a/test/data/register-a-death-files.yml b/test/data/register-a-death-files.yml index 6ffa16a6e7d..8ba46117492 100644 --- a/test/data/register-a-death-files.yml +++ b/test/data/register-a-death-files.yml @@ -20,7 +20,7 @@ lib/smart_answer_flows/shared/births_and_deaths_registration/_button.govspeak.er lib/data/rates/births_and_deaths_document_return_fees.yml: 4ac203e9fd076c12f62b57f8d1b64ffc lib/data/translators.yml: d86f628f0b85e24ffb0dc0ec77401387 test/fixtures/worldwide/italy_organisations.json: dc5feb424edeeb12835be916605caf46 -lib/smart_answer/calculators/registrations_data_query.rb: cbb0fad828d74606eb708d3442cf4ca5 +lib/smart_answer/calculators/registrations_data_query.rb: 563c5db7d7272bc07bd636a92b40f3ce lib/smart_answer/calculators/country_name_formatter.rb: 0cb274748f0daf3965451b6230c183fd lib/smart_answer/calculators/translator_links.rb: 079592f6c5ede06d7c6ae4e8c5553127 test/fixtures/worldwide_locations.yml: 063711faceec3a4081d6d5fb386c8029 diff --git a/test/unit/checkbox_question_test.rb b/test/unit/checkbox_question_test.rb index 67974351fa7..551f146d330 100644 --- a/test/unit/checkbox_question_test.rb +++ b/test/unit/checkbox_question_test.rb @@ -93,19 +93,5 @@ class CheckboxQuestionTest < ActiveSupport::TestCase assert_equal [], @question.to_response('none') end end - - context "predicate helper functions" do - setup do - @question = Question::Checkbox.new(nil, :something) - end - - should "define response_has_all_of" do - assert @question.response_has_all_of([]).is_a?(SmartAnswer::Predicate::ResponseHasAllOf) - end - - should "define response_is_one_of" do - assert @question.response_is_one_of([]).is_a?(SmartAnswer::Predicate::ResponseIsOneOf) - end - end end end diff --git a/test/unit/predicates_test.rb b/test/unit/predicates_test.rb deleted file mode 100644 index 7d77eebc0b4..00000000000 --- a/test/unit/predicates_test.rb +++ /dev/null @@ -1,197 +0,0 @@ - -require_relative '../test_helper' - -module SmartAnswer - module Predicate - class PredicatesTest < ActiveSupport::TestCase - setup do - @state = State.new("start") - end - - context "Base predicate" do - should "take a label as its first argument" do - example_label = "my label" - assert_equal example_label, SmartAnswer::Predicate::Base.new(example_label).label - end - - should "take a callable as its second argument" do - @state.calls_received = [] - callable = ->(state, response) { state.calls_received << response } - predicate = SmartAnswer::Predicate::Base.new(nil, callable) - predicate.call(@state, "a") - assert_equal ['a'], @state.calls_received - end - - should "be able to create a logical union of two predicates with shortcut execution" do - @state.p1_calls_received = [] - @state.p2_calls_received = [] - p1 = SmartAnswer::Predicate::Base.new('p1', ->(state, response) { state.p1_calls_received << response; response == 'a' }) - p2 = SmartAnswer::Predicate::Base.new('p2', ->(state, response) { state.p2_calls_received << response; response == 'b' }) - union = p1 | p2 - assert union.call(@state, 'a') - assert_equal ['a'], @state.p1_calls_received - assert_equal [], @state.p2_calls_received - assert union.call(@state, 'b') - assert_equal ['a', 'b'], @state.p1_calls_received - assert_equal ['b'], @state.p2_calls_received - refute union.call(@state, 'c') - assert_equal ['a', 'b', 'c'], @state.p1_calls_received - assert_equal ['b', 'c'], @state.p2_calls_received - end - - should "combine using | operator" do - p1 = SmartAnswer::Predicate::Base.new('p1') - p2 = SmartAnswer::Predicate::Base.new('p2') - assert_equal "p1 | p2", (p1 | p2).label - end - - should "be able to override lable when combining using or()" do - p1 = SmartAnswer::Predicate::Base.new('p1') - p2 = SmartAnswer::Predicate::Base.new('p2') - assert_equal "custom label", (p1.or(p2, "custom label")).label - end - - should "be able to create a logical conjunction of two predicates with shortcut execution" do - p_true = SmartAnswer::Predicate::Base.new('true', ->(_, _) { true }) - p_false = SmartAnswer::Predicate::Base.new('true', ->(_, _) { false }) - - assert_equal true, (p_true & p_true).call(@state, 'any input') - assert_equal false, (p_true & p_false).call(@state, 'any input') - assert_equal false, (p_false & p_true).call(@state, 'any input') - assert_equal false, (p_false & p_false).call(@state, 'any input') - end - - should "predicate formed by logical conjunction has meaningful label" do - p1 = SmartAnswer::Predicate::Base.new('p1', ->(_, _) { true }) - p2 = SmartAnswer::Predicate::Base.new('p2', ->(_, _) { true }) - - assert_equal "p1 AND p2", (p1 & p2).label - end - - should "be able to override lable when combining using and()" do - p1 = SmartAnswer::Predicate::Base.new('p1') - p2 = SmartAnswer::Predicate::Base.new('p2') - assert_equal "custom label", (p1.and(p2, "custom label")).label - end - end - - context "Callable predicate" do - should "take a label as its first argument" do - example_label = "my label" - assert_equal example_label, Callable.new(example_label).label - end - - should "take a block" do - call_count = 0 - predicate = Callable.new { call_count += 1 } - predicate.call(@state, nil) - assert_equal 1, call_count - end - - context "take a lambda" do - should "accept a lambda with arity of 1" do - call_count = 0 - predicate = Callable.new("label", ->(_) { call_count += 1 }) - predicate.call(@state, nil) - assert_equal 1, call_count - end - - should "accept a lambda with arity of 0" do - call_count = 0 - predicate = Callable.new("label", -> { call_count += 1 }) - predicate.call(@state, nil) - assert_equal 1, call_count - end - end - - should "evaluate its callable in the context of the given state" do - @state.calls_received = [] - predicate = Callable.new(nil, ->(response) { calls_received << response; calls_received.size == 1 }) - assert predicate.call(@state, 'response1') - refute predicate.call(@state, 'response2') - assert_equal ['response1', 'response2'], @state.calls_received - end - end - - context "RespondedWith predicate" do - setup do - @predicate = RespondedWith.new(%w{a b c}) - end - - should "check whether the response was one of the permitted options" do - assert @predicate.call(@state, 'a') - refute @predicate.call(@state, 'd') - end - - should "make label from the alternation of options" do - assert_equal "a | b | c", @predicate.label - end - end - - context "ResponseHasAllOf predicate" do - should "return true if all responses met" do - predicate = ResponseHasAllOf.new(%w{red green}) - refute predicate.call(@state, '') - refute predicate.call(@state, 'red') - refute predicate.call(@state, 'blue') - refute predicate.call(@state, 'green') - refute predicate.call(@state, 'red,blue') - assert predicate.call(@state, 'red,green') - refute predicate.call(@state, 'blue,green') - assert predicate.call(@state, 'red,blue,green') - end - - should "always be true if empty requirements" do - predicate = ResponseHasAllOf.new([]) - assert predicate.call(@state, '') - assert predicate.call(@state, 'red') - end - - should "make label from options" do - predicate = ResponseHasAllOf.new(%w{red green}) - assert_equal "red & green", predicate.label - end - end - - context "ResponseIsOneOf predicate" do - setup do - @predicate = ResponseIsOneOf.new(%w{a b c}) - end - - should "splits the response on comma and indicates if any of the options present" do - assert @predicate.call(@state, 'a,b,c') - assert @predicate.call(@state, 'a,c,d') - assert @predicate.call(@state, 'a') - refute @predicate.call(@state, 'd,e,f') - end - - should "make label from options" do - assert_equal "a | b | c", @predicate.label - end - end - - context "VariableMatches predicate" do - setup do - @predicate = VariableMatches.new(:my_var, %w{a b}) - end - - should "tests if the named variable matches any of the values" do - @state.my_var = 'a' - assert @predicate.call(@state, '') - @state.my_var = 'c' - refute @predicate.call(@state, '') - end - - should "make label from options" do - assert_equal "my_var == { a | b }", @predicate.label - end - - should "generate nice label when combining" do - p1 = VariableMatches.new(:a, 1, "one") - p2 = VariableMatches.new(:a, 2, "two") - assert_equal "a == one | two", (p1 | p2).label - end - end - end - end -end diff --git a/test/unit/question_base_test.rb b/test/unit/question_base_test.rb index 8443194a4c8..2a0ab341541 100644 --- a/test/unit/question_base_test.rb +++ b/test/unit/question_base_test.rb @@ -153,10 +153,9 @@ class QuestionBaseTest < ActiveSupport::TestCase next_node_calculation :blah do "blah" end - next_node_if(:done) { - complementary_colour == :green - } - next_node(:shouldnt_go_here) + next_node(permitted: [:done]) do + :done if complementary_colour == :green + end end initial_state = SmartAnswer::State.new(q.name) new_state = q.transition(initial_state, :red) @@ -165,93 +164,13 @@ class QuestionBaseTest < ActiveSupport::TestCase assert_equal :done, new_state.current_node end - test "conditional next node can be specified using next_node_if passing a block" do - q = SmartAnswer::Question::Base.new(nil, :example) { - next_node_if(:bar) { true } - } - initial_state = SmartAnswer::State.new(q.name) - assert_equal :bar, q.next_node_for(initial_state, :red) - end - - test "conditional next node can be specified using next_node_if passing a predicate" do - q = SmartAnswer::Question::Base.new(nil, :example) { - next_node_if(:bar, ->(_) {true}) - } - initial_state = SmartAnswer::State.new(q.name) - assert_equal :bar, q.next_node_for(initial_state, :red) - end - - test "conditional next node can be specified using next_node_if passing a list of predicates all of which must be true" do - q = SmartAnswer::Question::Base.new(nil, :example) { - next_node_if(:bar, ->(_) {true}, ->(_) {false}) - next_node_if(:baz, ->(_) {true}) - } - initial_state = SmartAnswer::State.new(q.name) - assert_equal :baz, q.next_node_for(initial_state, :red) - end - - test "conditional next nodes are evaluated in order" do - q = SmartAnswer::Question::Base.new(nil, :example) { - next_node_if(:skipped) { false } - next_node_if(:first) { true } - next_node_if(:second) { true } - } - initial_state = SmartAnswer::State.new(q.name) - assert_equal :first, q.next_node_for(initial_state, :red) - end - - test "conditional block is passed input and can refer to state" do - q = SmartAnswer::Question::Base.new(nil, :example) { - next_node_if(:is_red) do |input| - color == input - end - } - initial_state = SmartAnswer::State.new(q.name) - initial_state.color = :red - assert_equal :is_red, q.next_node_for(initial_state, :red) - end - - test "next_node block used as fallback" do - q = SmartAnswer::Question::Base.new(nil, :example) { - next_node_if(:skipped) { false } - next_node(permitted: [:next]) { :next } - } - initial_state = SmartAnswer::State.new(q.name) - assert_equal :next, q.next_node_for(initial_state, :red) - end - - test "conditional next_node used if triggered ignoring fallback" do - q = SmartAnswer::Question::Base.new(nil, :example) { - next_node_if(:next) { true } - next_node(permitted: [:ignored]) { :ignored } - } - initial_state = SmartAnswer::State.new(q.name) - assert_equal :next, q.next_node_for(initial_state, :red) - end - - test "next_node value used as fallback" do - q = SmartAnswer::Question::Base.new(nil, :example) { - next_node_if(:skipped) { false } - next_node(:next) - } - initial_state = SmartAnswer::State.new(q.name) - assert_equal :next, q.next_node_for(initial_state, :red) - end - - test "next_node value ignored if conditional fires" do - q = SmartAnswer::Question::Base.new(nil, :example) { - next_node_if(:next) { true } - next_node(:ignored) - } - initial_state = SmartAnswer::State.new(q.name) - assert_equal :next, q.next_node_for(initial_state, :red) - end - test "error if no conditional transitions found and no fallback" do question_name = :example responses = [:blue, :red] q = SmartAnswer::Question::Base.new(nil, question_name) { - next_node_if(:skipped) { false } + next_node(permitted: [:skipped]) do + :skipped if false + end } initial_state = SmartAnswer::State.new(q.name) initial_state.responses << responses[0] @@ -261,31 +180,4 @@ class QuestionBaseTest < ActiveSupport::TestCase expected_message = "Next node undefined. Node: #{question_name}. Responses: #{responses}" assert_equal expected_message, error.message end - - test "can define a predicate on a node" do - q = SmartAnswer::Question::Base.new(nil, :example) { - define_predicate(:response_red?) { |response| response == 'red' } - define_predicate(:color_red?) { color == 'red' } - } - - assert q.response_red?.is_a?(SmartAnswer::Predicate::Callable) - assert q.color_red?.is_a?(SmartAnswer::Predicate::Callable) - - state = SmartAnswer::State.new(q.name) - state.color = nil - assert q.response_red?.call(state, "red") - refute q.response_red?.call(state, "green") - - refute q.color_red?.call(state, "") - state.color = 'red' - assert q.color_red?.call(state, "") - end - - test "may not define a predicate for a method which already exists" do - assert_raises RuntimeError, /method define_predicate already defined/ do - SmartAnswer::Question::Base.new(nil, :example) { - define_predicate(:define_predicate) {} - } - end - end end