Merge pull request #7424 from betagouv/condition_engine

Condition engine
This commit is contained in:
LeSim 2022-06-17 15:01:42 +02:00 committed by GitHub
commit b4ab3487de
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 808 additions and 14 deletions

View file

@ -516,9 +516,6 @@ Lint/UselessAssignment:
Lint/BinaryOperatorWithIdenticalOperands:
Enabled: true
Lint/UselessElseWithoutRescue:
Enabled: true
Lint/UselessSetterCall:
Enabled: true

View file

@ -464,7 +464,7 @@ GEM
validate_url
webfinger (>= 1.0.1)
orm_adapter (0.5.0)
parallel (1.20.1)
parallel (1.22.1)
parser (3.1.2.0)
ast (~> 2.4.1)
pdf-core (0.9.0)
@ -564,7 +564,7 @@ GEM
rb-fsevent (0.10.4)
rb-inotify (0.10.1)
ffi (~> 1.0)
regexp_parser (2.1.0)
regexp_parser (2.5.0)
request_store (1.5.0)
rack (>= 1.4)
responders (3.0.1)
@ -608,17 +608,17 @@ GEM
rspec-support (3.10.2)
rspec_junit_formatter (0.4.1)
rspec-core (>= 2, < 4, != 2.12.0)
rubocop (1.10.0)
rubocop (1.30.1)
parallel (~> 1.10)
parser (>= 3.0.0.0)
parser (>= 3.1.0.0)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0)
rexml
rubocop-ast (>= 1.2.0, < 2.0)
rexml (>= 3.2.5, < 4.0)
rubocop-ast (>= 1.18.0, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 3.0)
rubocop-ast (1.4.1)
parser (>= 2.7.1.5)
rubocop-ast (1.18.0)
parser (>= 3.1.1.0)
rubocop-performance (1.9.2)
rubocop (>= 0.90.0, < 2.0)
rubocop-ast (>= 0.4.0)

84
app/models/logic.rb Normal file
View file

@ -0,0 +1,84 @@
module Logic
def self.from_h(h)
class_from_name(h['op']).from_h(h)
end
def self.from_json(s)
from_h(JSON.parse(s))
end
def self.class_from_name(name)
[ChampValue, Constant, Empty, LessThan, LessThanEq, Eq, GreaterThanEq, GreaterThan, EmptyOperator, And, Or]
.find { |c| c.name == name }
end
def self.ensure_compatibility_from_left(condition)
left = condition.left
right = condition.right
operator_class = condition.class
case [left.type, condition]
in [:boolean, _]
operator_class = Eq
in [:empty, _]
operator_class = EmptyOperator
in [:enum, _]
operator_class = Eq
in [:number, EmptyOperator]
operator_class = Eq
in [:number, _]
in [:string, _]
operator_class = Eq
end
if !compatible_type?(left, right)
right = case left.type
when :boolean
Constant.new(true)
when :empty
Empty.new
when :enum
Constant.new(left.options.first)
when :number
Constant.new(0)
when :string
Constant.new('')
end
end
operator_class.new(left, right)
end
def self.compatible_type?(left, right)
case [left.type, right.type]
in [a, ^a] # syntax for same type
true
in [:enum, :string]
left.options.include?(right.value)
else
false
end
end
def ds_eq(left, right) = Logic::Eq.new(left, right)
def greater_than(left, right) = Logic::GreaterThan.new(left, right)
def greater_than_eq(left, right) = Logic::GreaterThanEq.new(left, right)
def less_than(left, right) = Logic::LessThan.new(left, right)
def less_than_eq(left, right) = Logic::LessThanEq.new(left, right)
def constant(value) = Logic::Constant.new(value)
def champ_value(stable_id) = Logic::ChampValue.new(stable_id)
def empty = Logic::Empty.new
def empty_operator(left, right) = Logic::EmptyOperator.new(left, right)
def ds_or(operands) = Logic::Or.new(operands)
def ds_and(operands) = Logic::And.new(operands)
end

11
app/models/logic/and.rb Normal file
View file

@ -0,0 +1,11 @@
class Logic::And < Logic::NAryOperator
attr_reader :operands
def operator_name = 'Et'
def compute(champs = [])
@operands.map { |operand| operand.compute(champs) }.all?
end
def to_s = "(#{@operands.map(&:to_s).join(' && ')})"
end

View file

@ -0,0 +1,46 @@
class Logic::BinaryOperator < Logic::Term
attr_reader :left, :right
def initialize(left, right)
@left, @right = left, right
end
def to_h
{
"op" => self.class.name,
"left" => @left.to_h,
"right" => @right.to_h
}
end
def self.from_h(h)
self.new(Logic.from_h(h['left']), Logic.from_h(h['right']))
end
def errors(stable_ids = [])
errors = []
if @left.type != :number || @right.type != :number
errors += ["les types sont incompatibles : #{self}"]
end
errors + @left.errors(stable_ids) + @right.errors(stable_ids)
end
def type = :boolean
def compute(champs = [])
l = @left.compute(champs)
r = @right.compute(champs)
l.send(operation, r)
end
def to_s = "(#{@left} #{operation} #{@right})"
def ==(other)
self.class == other.class &&
@left == other.left &&
@right == other.right
end
end

View file

@ -0,0 +1,76 @@
class Logic::ChampValue < Logic::Term
attr_reader :stable_id
def initialize(stable_id)
@stable_id = stable_id
end
def compute(champs)
case type_de_champ.type_champ
when all_types.fetch(:yes_no),
all_types.fetch(:checkbox)
champ(champs).true?
when all_types.fetch(:integer_number), all_types.fetch(:decimal_number)
champ(champs).for_api
when all_types.fetch(:drop_down_list), all_types.fetch(:text)
champ(champs).value
end
end
def to_s = "#{type_de_champ.libelle}#{stable_id}"
def type
case type_de_champ.type_champ
when all_types.fetch(:yes_no),
all_types.fetch(:checkbox)
:boolean
when all_types.fetch(:integer_number), all_types.fetch(:decimal_number)
:number
when all_types.fetch(:text)
:string
when all_types.fetch(:drop_down_list)
:enum
end
end
def errors(stable_ids)
if !stable_ids.include?(stable_id)
["le type de champ stable_id=#{stable_id} n'est pas disponible"]
else
[]
end
end
def to_h
{
"op" => self.class.name,
"stable_id" => @stable_id
}
end
def self.from_h(h)
self.new(h['stable_id'])
end
def ==(other)
self.class == other.class && @stable_id == other.stable_id
end
def options
type_de_champ.drop_down_list_enabled_non_empty_options
end
private
def type_de_champ
TypeDeChamp.find_by(stable_id: stable_id)
end
def champ(champs)
champs.find { |c| c.stable_id == stable_id }
end
def all_types
TypeDeChamp.type_champs
end
end

View file

@ -0,0 +1,39 @@
class Logic::Constant < Logic::Term
attr_reader :value
def initialize(value)
@value = value
end
def compute(_champs = nil) = @value
def to_s = @value.to_s
def type
case @value
when TrueClass, FalseClass
:boolean
when Integer, Float
:number
else
@value.class.name.downcase.to_sym
end
end
def errors(_stable_ids = nil) = []
def to_h
{
"op" => self.class.name,
"value" => @value
}
end
def self.from_h(h)
self.new(h['value'])
end
def ==(other)
self.class == other.class && @value == other.value
end
end

21
app/models/logic/empty.rb Normal file
View file

@ -0,0 +1,21 @@
class Logic::Empty < Logic::Term
def to_s = "empty member"
def type = :empty
def errors(_stable_ids = nil) = ['empty']
def to_h
{
"op" => self.class.name
}
end
def self.from_h(_h)
self.new
end
def ==(other)
self.class == other.class
end
end

View file

@ -0,0 +1,7 @@
class Logic::EmptyOperator < Logic::BinaryOperator
def to_s = "empty operator"
def type = :empty
def errors(_stable_ids = nil) = ['empty']
end

18
app/models/logic/eq.rb Normal file
View file

@ -0,0 +1,18 @@
class Logic::Eq < Logic::BinaryOperator
def operation = :==
def errors(stable_ids = [])
errors = []
if !Logic.compatible_type?(@left, @right)
errors += ["les types sont incompatibles : #{self}"]
end
errors + @left.errors(stable_ids) + @right.errors(stable_ids)
end
def ==(other)
self.class == other.class &&
[@left, @right].permutation.any? { |p| p == [other.left, other.right] }
end
end

View file

@ -0,0 +1,3 @@
class Logic::GreaterThan < Logic::BinaryOperator
def operation = :>
end

View file

@ -0,0 +1,3 @@
class Logic::GreaterThanEq < Logic::BinaryOperator
def operation = :>=
end

View file

@ -0,0 +1,3 @@
class Logic::LessThan < Logic::BinaryOperator
def operation = :<
end

View file

@ -0,0 +1,3 @@
class Logic::LessThanEq < Logic::BinaryOperator
def operation = :<=
end

View file

@ -0,0 +1,43 @@
class Logic::NAryOperator < Logic::Term
attr_reader :operands
def initialize(operands)
@operands = operands
end
def to_h
{
"op" => self.class.name,
"operands" => @operands.map(&:to_h)
}
end
def self.from_h(h)
self.new(h['operands'].map { |operand_h| Logic.from_h(operand_h) })
end
def errors(stable_ids = [])
errors = []
if @operands.empty?
errors += ["opérateur '#{operator_name}' vide"]
end
not_booleans = @operands.filter { |operand| operand.type != :boolean }
if not_booleans.present?
errors += ["'#{operator_name}' ne contient pas que des booléens : #{not_booleans.map(&:to_s).join(', ')}"]
end
errors + @operands.flat_map { |operand| operand.errors(stable_ids) }
end
def type = :boolean
def ==(other)
self.class == other.class &&
@operands.count == other.operands.count &&
@operands.all? do |operand|
@operands.count { |o| o == operand } == other.operands.count { |o| o == operand }
end
end
end

11
app/models/logic/or.rb Normal file
View file

@ -0,0 +1,11 @@
class Logic::Or < Logic::NAryOperator
attr_reader :operands
def operator_name = 'Ou'
def compute(champs = [])
@operands.map { |operand| operand.compute(champs) }.any?
end
def to_s = "(#{@operands.map(&:to_s).join(' || ')})"
end

5
app/models/logic/term.rb Normal file
View file

@ -0,0 +1,5 @@
class Logic::Term
def to_json
to_h.to_json
end
end

View file

@ -740,6 +740,10 @@ class Procedure < ApplicationRecord
move_new_children_to_new_parent_coordinate(new_draft)
# they are not aware of the new tdcs
new_draft.types_de_champ_public.reset
new_draft.types_de_champ_private.reset
new_draft
end
end

View file

@ -30,12 +30,16 @@ class ProcedureRevision < ApplicationRecord
scope :ordered, -> { order(:created_at) }
validate :conditions_are_valid?
def build_champs
types_de_champ_public.map { |tdc| tdc.build_champ(revision: self) }
# reload: it can be out of sync in test if some tdcs are added wihtout using add_tdc
types_de_champ_public.reload.map { |tdc| tdc.build_champ(revision: self) }
end
def build_champs_private
types_de_champ_private.map { |tdc| tdc.build_champ(revision: self) }
# reload: it can be out of sync in test if some tdcs are added wihtout using add_tdc
types_de_champ_private.reload.map { |tdc| tdc.build_champ(revision: self) }
end
def add_type_de_champ(params)
@ -62,6 +66,10 @@ class ProcedureRevision < ApplicationRecord
reorder(coordinate.siblings)
end
# they are not aware of the addition
types_de_champ_public.reset
types_de_champ_private.reset
tdc
rescue => e
TypeDeChamp.new.tap { |tdc| tdc.errors.add(:base, e.message) }
@ -99,6 +107,10 @@ class ProcedureRevision < ApplicationRecord
children.each(&:destroy_if_orphan)
tdc.destroy_if_orphan
# they are not aware of the removal
types_de_champ_public.reset
types_de_champ_private.reset
reorder(coordinate.siblings)
coordinate
@ -465,4 +477,14 @@ class ProcedureRevision < ApplicationRecord
last.present? ? last.position + 1 : 0
end
end
def conditions_are_valid?
stable_ids = types_de_champ_public.map(&:stable_id)
types_de_champ_public
.map.with_index
.filter_map { |tdc, i| tdc.condition.present? ? [tdc, i] : nil }
.flat_map { |tdc, i| tdc.condition.errors(stable_ids.take(i)) }
.each { |message| errors.add(:condition, message) }
end
end

View file

@ -3,6 +3,7 @@
# Table name: types_de_champ
#
# id :integer not null, primary key
# condition :jsonb
# description :text
# libelle :string
# mandatory :boolean default(FALSE)
@ -77,6 +78,22 @@ class TypeDeChamp < ApplicationRecord
serialize :options, WithIndifferentAccess
class ConditionSerializer
def self.load(condition)
if condition.present?
Logic.from_h(condition)
end
end
def self.dump(condition)
if condition.present?
condition.to_h
end
end
end
serialize :condition, ConditionSerializer
after_initialize :set_dynamic_type
after_create :populate_stable_id

View file

@ -0,0 +1,5 @@
class AddConditionColumnToTypeDeChamp < ActiveRecord::Migration[6.1]
def change
add_column :types_de_champ, :condition, :jsonb
end
end

View file

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2022_05_20_173939) do
ActiveRecord::Schema.define(version: 2022_05_31_100040) do
# These are extensions that must be enabled in order to support this database
enable_extension "pgcrypto"
@ -787,6 +787,7 @@ ActiveRecord::Schema.define(version: 2022_05_20_173939) do
end
create_table "types_de_champ", id: :serial, force: :cascade do |t|
t.jsonb "condition"
t.datetime "created_at"
t.text "description"
t.string "libelle"

View file

@ -0,0 +1,18 @@
describe Logic::And do
include Logic
describe '#compute' do
it { expect(and_from([true, true, true]).compute).to be true }
it { expect(and_from([true, true, false]).compute).to be false }
end
describe '#to_s' do
it do
expect(and_from([true, false, true]).to_s).to eq "(true && false && true)"
end
end
def and_from(boolean_to_constants)
ds_and(boolean_to_constants.map { |b| constant(b) })
end
end

View file

@ -0,0 +1,52 @@
include Logic
describe Logic::BinaryOperator do
let(:two_greater_than_one) { greater_than(constant(2), constant(1)) }
describe '#type' do
it { expect(two_greater_than_one.type).to eq(:boolean) }
end
describe '#to_s' do
it { expect(two_greater_than_one.to_s).to eq('(2 > 1)') }
end
describe '#==' do
it { expect(two_greater_than_one).to eq(greater_than(constant(2), constant(1))) }
it { expect(two_greater_than_one).not_to eq(greater_than(constant(1), constant(2))) }
end
describe '#errors' do
it { expect(greater_than(constant(1), constant(true)).errors).to eq(['les types sont incompatibles : (1 > true)']) }
end
end
describe Logic::GreaterThan do
it 'computes' do
expect(greater_than(constant(1), constant(1)).compute).to be(false)
expect(greater_than(constant(2), constant(1)).compute).to be(true)
end
end
describe Logic::GreaterThanEq do
it 'computes' do
expect(greater_than_eq(constant(0), constant(1)).compute).to be(false)
expect(greater_than_eq(constant(1), constant(1)).compute).to be(true)
expect(greater_than_eq(constant(2), constant(1)).compute).to be(true)
end
end
describe Logic::LessThan do
it 'computes' do
expect(less_than(constant(1), constant(1)).compute).to be(false)
expect(less_than(constant(1), constant(2)).compute).to be(true)
end
end
describe Logic::LessThanEq do
it 'computes' do
expect(less_than_eq(constant(0), constant(1)).compute).to be(true)
expect(less_than_eq(constant(1), constant(1)).compute).to be(true)
expect(less_than_eq(constant(2), constant(1)).compute).to be(false)
end
end

View file

@ -0,0 +1,64 @@
describe Logic::ChampValue do
include Logic
subject { champ_value(champ.stable_id).compute([champ]) }
context 'yes_no tdc' do
let(:value) { 'true' }
let(:champ) { create(:champ_yes_no, value: value) }
it { expect(champ_value(champ.stable_id).type).to eq(:boolean) }
context 'with true value' do
it { is_expected.to be(true) }
end
context 'with false value' do
let(:value) { 'false' }
it { is_expected.to be(false) }
end
end
context 'text tdc' do
let(:champ) { create(:champ_text, value: 'text') }
it { expect(champ_value(champ.stable_id).type).to eq(:string) }
it { is_expected.to eq('text') }
end
context 'integer tdc' do
let(:champ) { create(:champ_integer_number, value: '42') }
it { expect(champ_value(champ.stable_id).type).to eq(:number) }
it { is_expected.to eq(42) }
end
context 'decimal tdc' do
let(:champ) { create(:champ_decimal_number, value: '42.01') }
it { expect(champ_value(champ.stable_id).type).to eq(:number) }
it { is_expected.to eq(42.01) }
end
context 'dropdown tdc' do
let(:champ) { create(:champ_drop_down_list, value: 'choix 1') }
it { expect(champ_value(champ.stable_id).type).to eq(:enum) }
it { is_expected.to eq('choix 1') }
end
context 'checkbox tdc' do
let(:champ) { create(:champ_checkbox, value: 'on') }
it { expect(champ_value(champ.stable_id).type).to eq(:boolean) }
it { is_expected.to eq(true) }
end
describe 'errors' do
let(:champ) { create(:champ) }
it { expect(champ_value(champ.stable_id).errors([champ.stable_id])).to be_empty }
it { expect(champ_value(champ.stable_id).errors(['other stable ids'])).to eq(["le type de champ stable_id=#{champ.stable_id} n'est pas disponible"]) }
end
end

View file

@ -0,0 +1,24 @@
describe Logic::Constant do
include Logic
describe '#compute' do
it { expect(constant(1).compute).to eq(1) }
end
describe '#type' do
it { expect(constant(1).type).to eq(:number) }
it { expect(constant(1.0).type).to eq(:number) }
it { expect(constant('a').type).to eq(:string) }
it { expect(constant(true).type).to eq(:boolean) }
it { expect(constant(false).type).to eq(:boolean) }
end
describe '#errors' do
it { expect(constant(1).errors).to eq([]) }
end
describe '#==' do
it { expect(constant(1)).to eq(constant(1)) }
it { expect(constant(1)).not_to eq(constant('a')) }
end
end

View file

@ -0,0 +1,16 @@
describe Logic::Constant do
include Logic
describe '#type' do
it { expect(empty.type).to eq(:empty) }
end
describe '#errors' do
it { expect(empty.errors).to eq(['empty']) }
end
describe '#==' do
it { expect(empty).to eq(empty) }
it { expect(empty).not_to eq(constant(true)) }
end
end

View file

@ -0,0 +1,17 @@
describe Logic::Eq do
include Logic
describe '#compute' do
it { expect(ds_eq(constant(1), constant(1)).compute).to be(true) }
it { expect(ds_eq(constant(1), constant(2)).compute).to be(false) }
end
describe '#errors' do
it { expect(ds_eq(constant(true), constant(true)).errors).to be_empty }
it { expect(ds_eq(constant(true), constant(1)).errors).to eq(["les types sont incompatibles : (true == 1)"]) }
end
describe '#==' do
it { expect(ds_eq(constant(true), constant(false))).to eq(ds_eq(constant(false), constant(true))) }
end
end

View file

@ -0,0 +1,29 @@
describe Logic::NAryOperator do
include Logic
describe '#errors' do
it { expect(ds_and([]).errors).to eq(["opérateur 'Et' vide"]) }
it { expect(ds_and([constant(1), constant('toto')]).errors).to eq(["'Et' ne contient pas que des booléens : 1, toto"]) }
it { expect(ds_and([double(type: :boolean, errors: ['from double'])]).errors).to eq(["from double"]) }
end
describe '#==' do
it do
expect(and_from([true, true, false])).to eq(and_from([false, true, true]))
expect(and_from([true, true, false])).not_to eq(and_from([false, false, true]))
# perf test
left = [false, false] + Array.new(10) { true }
right = [false] + Array.new(11) { true }
expect(and_from(left)).not_to eq(and_from(right))
left = (1..10).to_a
right = (1..10).to_a.reverse
expect(and_from(left)).to eq(and_from(right))
end
end
def and_from(boolean_to_constants)
ds_and(boolean_to_constants.map { |b| constant(b) })
end
end

82
spec/models/logic_spec.rb Normal file
View file

@ -0,0 +1,82 @@
describe Logic do
include Logic
it 'serializes deserializes' do
expect(Logic.from_h(constant(1).to_h)).to eq(constant(1))
expect(Logic.from_json(constant(1).to_json)).to eq(constant(1))
expect(Logic.from_h(empty.to_h)).to eq(empty)
expect(Logic.from_h(champ_value(1).to_h)).to eq(champ_value(1))
expect(Logic.from_h(greater_than(constant(1), constant(2)).to_h)).to eq(greater_than(constant(1), constant(2)))
expect(Logic.from_h(ds_and([constant(true), constant(true), constant(false)]).to_h))
.to eq(ds_and([constant(true), constant(true), constant(false)]))
end
describe '.ensure_compatibility_from_left' do
subject { Logic.ensure_compatibility_from_left(condition) }
context 'when it s fine' do
let(:condition) { greater_than(constant(1), constant(1)) }
it { is_expected.to eq(condition) }
end
context 'when empty equal true' do
let(:condition) { ds_eq(empty, constant(true)) }
it { is_expected.to eq(empty_operator(empty, empty)) }
end
context 'when true greater_than 1' do
let(:condition) { greater_than(constant(true), constant(0)) }
it { is_expected.to eq(ds_eq(constant(true), constant(true))) }
end
context 'when number empty operator true' do
let(:condition) { empty_operator(constant(1), constant(true)) }
it { is_expected.to eq(ds_eq(constant(1), constant(0))) }
end
context 'when string empty operator true' do
let(:condition) { empty_operator(constant('a'), constant(true)) }
it { is_expected.to eq(ds_eq(constant('a'), constant(''))) }
end
context 'when dropdown empty operator true' do
let(:drop_down) { create(:type_de_champ_drop_down_list) }
let(:first_option) { drop_down.drop_down_list_enabled_non_empty_options.first }
let(:condition) { empty_operator(champ_value(drop_down), constant(true)) }
it { is_expected.to eq(ds_eq(champ_value(drop_down), constant(first_option))) }
end
end
describe '.compatible_type?' do
it { expect(Logic.compatible_type?(constant(true), constant(true))).to be true }
it { expect(Logic.compatible_type?(constant(1), constant(true))).to be false }
context 'with a dropdown' do
let(:drop_down) { create(:type_de_champ_drop_down_list) }
let(:first_option) { drop_down.drop_down_list_enabled_non_empty_options.first }
it do
expect(Logic.compatible_type?(champ_value(drop_down.stable_id), constant(first_option))).to be true
expect(Logic.compatible_type?(champ_value(drop_down.stable_id), constant('a'))).to be false
end
end
end
describe 'priority' do
# (false && true) || true = true
it { expect(ds_or([ds_and([constant(false), constant(true)]), constant(true)]).compute).to be true }
# false && (true || true) = false
it { expect(ds_and([constant(false), ds_or([constant(true), constant(true)])]).compute).to be false }
end
end

17
spec/models/or_spec.rb Normal file
View file

@ -0,0 +1,17 @@
describe Logic::Or do
include Logic
describe '#compute' do
it { expect(or_from([true, true, true]).compute).to be true }
it { expect(or_from([true, true, false]).compute).to be true }
it { expect(or_from([false, false, false]).compute).to be false }
end
describe '#to_s' do
it { expect(or_from([true, false, true]).to_s).to eq "(true || false || true)" }
end
def or_from(boolean_to_constants)
ds_or(boolean_to_constants.map { |b| constant(b) })
end
end

View file

@ -623,4 +623,49 @@ describe ProcedureRevision do
end
end
end
describe 'conditions_are_valid' do
include Logic
def first_champ = procedure.draft_revision.types_de_champ_public.first
def second_champ = procedure.draft_revision.types_de_champ_public.second
let(:procedure) { create(:procedure, :with_type_de_champ, types_de_champ_count: 2) }
let(:draft_revision) { procedure.draft_revision }
let(:condition) { nil }
subject do
draft_revision.save
draft_revision.errors
end
before { second_champ.update(condition: condition) }
context 'when a champ has a valid condition (type)' do
let(:condition) { ds_eq(constant(true), constant(true)) }
it { is_expected.to be_empty }
end
context 'when a champ has a valid condition: needed tdc is up in the forms' do
let(:condition) { ds_eq(constant('oui'), champ_value(first_champ.stable_id)) }
it { is_expected.to be_empty }
end
context 'when a champ has an invalid condition' do
let(:condition) { ds_eq(constant(true), constant(1)) }
it { expect(subject.first.attribute).to eq(:condition) }
end
context 'when a champ has an invalid condition: needed tdc is down in the forms' do
let(:need_second_champ) { ds_eq(constant('oui'), champ_value(second_champ.stable_id)) }
before { first_champ.update(condition: need_second_champ) }
it { expect(subject.first.attribute).to eq(:condition) }
end
end
end

View file

@ -11,4 +11,15 @@ describe TypeDeChamp do
expect(procedure.types_de_champ_private.count).to eq(1)
end
end
describe 'condition' do
let(:type_de_champ) { create(:type_de_champ) }
let(:condition) { Logic::Eq.new(Logic::Constant.new(true), Logic::Constant.new(true)) }
it 'saves and reload the condition' do
type_de_champ.update(condition: condition)
type_de_champ.reload
expect(type_de_champ.condition).to eq(condition)
end
end
end