Skip to content

Commit 5d1ba04

Browse files
committed
PC-24: Improve formula validation with dentaku
1 parent 1260b94 commit 5d1ba04

2 files changed

Lines changed: 73 additions & 45 deletions

File tree

app/models/item.rb

Lines changed: 1 addition & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ class Item < ApplicationRecord
88
validate :fixed_parameters_values_must_be_numeric
99
validate :pricing_options_values_must_be_numeric
1010
validates :calculation_formula, presence: true, if: :requires_calculation_formula?
11-
validate :calculation_formula_must_be_valid, if: :requires_calculation_formula?
11+
validates_with ItemFormulaSyntaxValidator
1212

1313
def self.ransackable_attributes(_auth_object = nil)
1414
%w[category_id created_at id is_disabled name updated_at]
@@ -73,48 +73,4 @@ def numeric?(val)
7373
def requires_calculation_formula?
7474
is_fixed || is_open || is_selectable_options
7575
end
76-
77-
def calculation_formula_must_be_valid
78-
return if calculation_formula.blank?
79-
80-
check_all_formula_parameters_present
81-
check_allowed_parameters
82-
check_parentheses_balanced
83-
end
84-
85-
def check_all_formula_parameters_present
86-
missing_parameters = formula_parameters.reject do |param|
87-
calculation_formula.match?(/\b#{Regexp.escape(param)}\b/)
88-
end
89-
90-
return unless missing_parameters.any?
91-
92-
errors.add(:calculation_formula, "is missing parameters: #{missing_parameters.join(', ')}")
93-
end
94-
95-
def check_allowed_parameters
96-
operators = %w[+ - * / % ( )]
97-
98-
invalid_parameters = calculation_formula.split(' ').reject do |param|
99-
param.match?(/\A\d+(\.\d+)?\z/) || # Matches numbers
100-
operators.include?(param) || # Matches operators
101-
formula_parameters.include?(param) # Matches formula parameters
102-
end
103-
104-
return unless invalid_parameters.any?
105-
106-
errors.add(:calculation_formula, "contains invalid parameters: #{invalid_parameters.join(', ')}")
107-
end
108-
109-
def check_parentheses_balanced
110-
parentheses = []
111-
calculation_formula.each_char do |char|
112-
parentheses.push(char) if char == '('
113-
parentheses.pop if char == ')' && parentheses.any?
114-
end
115-
116-
return unless parentheses.any? || calculation_formula.count('(') != calculation_formula.count(')')
117-
118-
errors.add(:calculation_formula, 'has unbalanced parentheses')
119-
end
12076
end
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
class ItemFormulaSyntaxValidator < ActiveModel::Validator
2+
def validate(record)
3+
return if record.calculation_formula.blank?
4+
return unless requires_calculation_formula?(record)
5+
6+
check_all_formula_parameters_present(record)
7+
check_allowed_parameters(record)
8+
check_operator_placement(record)
9+
validate_formula_syntax(record)
10+
end
11+
12+
private
13+
14+
def requires_calculation_formula?(record)
15+
record.is_fixed || record.is_open || record.is_selectable_options
16+
end
17+
18+
def check_all_formula_parameters_present(record)
19+
missing_parameters = record.formula_parameters.reject do |param|
20+
record.calculation_formula.match?(/\b#{Regexp.escape(param)}\b/)
21+
end
22+
23+
return if missing_parameters.empty?
24+
25+
record.errors.add(:calculation_formula, "is missing parameters: #{missing_parameters.join(', ')}")
26+
end
27+
28+
def check_allowed_parameters(record)
29+
operators = %w[+ - * / % ( )]
30+
31+
invalid_parameters = record.calculation_formula.split(' ').reject do |param|
32+
param.match?(/\A\d+(\.\d+)?\z/) ||
33+
operators.include?(param) ||
34+
record.formula_parameters.include?(param)
35+
end
36+
37+
return if invalid_parameters.empty?
38+
39+
record.errors.add(:calculation_formula, "contains invalid parameters: #{invalid_parameters.join(', ')}")
40+
end
41+
42+
def check_operator_placement(record)
43+
if record.calculation_formula.match?(%r{\A\s*[+\-*/%]})
44+
record.errors.add(:calculation_formula, "cannot start with a mathematical operator")
45+
end
46+
return unless record.calculation_formula.match?(%r{[+\-*/%]\s*\z})
47+
48+
record.errors.add(:calculation_formula, "cannot end with a mathematical operator")
49+
end
50+
51+
def validate_formula_syntax(record)
52+
calculator = Dentaku::Calculator.new
53+
calculator.dependencies(record.calculation_formula)
54+
rescue Dentaku::ParseError => e
55+
handle_parse_error(record, e)
56+
rescue StandardError => e
57+
record.errors.add(:calculation_formula, "could not validate formula: #{e.message}")
58+
end
59+
60+
def handle_parse_error(record, error)
61+
case error.message
62+
when /Undefined function/
63+
record.errors.add(:calculation_formula, "references an undefined function. Please review formula structure.")
64+
when /too few operands/
65+
record.errors.add(:calculation_formula, "has missing operands. Ensure the correct number of arguments.")
66+
when /has too many/
67+
record.errors.add(:calculation_formula, "has extra operands. Ensure the correct number of arguments.")
68+
else
69+
record.errors.add(:calculation_formula, "has a syntax error: #{error.message}")
70+
end
71+
end
72+
end

0 commit comments

Comments
 (0)