From 19e2a45ba81bdb9db4eaf476ada0ed859f4b7f24 Mon Sep 17 00:00:00 2001 From: cryptogopher Date: Thu, 7 Nov 2019 19:19:31 +0100 Subject: [PATCH] Moved formula code/validation to separate module --- app/models/quantity.rb | 43 +++--------------------- lib/body_tracking/filter.rb | 0 lib/body_tracking/formula.rb | 64 ++++++++++++++++++++++++++++++++++++ 3 files changed, 68 insertions(+), 39 deletions(-) delete mode 100644 lib/body_tracking/filter.rb create mode 100644 lib/body_tracking/formula.rb diff --git a/app/models/quantity.rb b/app/models/quantity.rb index 08077f8..5d97fd1 100644 --- a/app/models/quantity.rb +++ b/app/models/quantity.rb @@ -1,6 +1,5 @@ class Quantity < ActiveRecord::Base - require 'ripper' - QUANTITY_TTYPES = [:on_ident, :on_tstring_content, :on_const] + include BodyTracking::Formula enum domain: { diet: 0, @@ -16,41 +15,7 @@ class Quantity < ActiveRecord::Base validate if: -> { parent.present? } do errors.add(:parent, :parent_domain_mismatch) unless domain == parent.domain end - validate if: -> { formula.present? } do - # 1st: check if formula is valid Ruby code - tokenized_length = Ripper.tokenize(formula).join.length - unless tokenized_length == formula.length - errors.add(:formula, :invalid_formula, {part: formula[0...tokenized_length]}) - end - - # 2nd: check if formula contains only allowed token types - identifiers = [] - Ripper.lex(formula).each do |location, ttype, token| - case - when QUANTITY_TTYPES.include?(ttype) - identifiers << token - when [:on_sp, :on_int, :on_rational, :on_float, :on_tstring_beg, :on_tstring_end, - :on_lparen, :on_rparen].include?(ttype) - when :on_op == ttype && '+-*/'.include?(token) - else - errors.add(:formula, :disallowed_token, - {token: token, ttype: ttype, location: location}) - end - end - - # 3rd: check for disallowed function calls (they are not detected by Ripper.lex) - # FIXME: this is unreliable (?) detection of function calls, should be replaced - # with parsing Ripper.sexp if necessary - function = Ripper.slice(formula, 'ident [sp]* lparen') - errors.add(:formula, :disallowed_function_call, {function: function}) if function - - # 4th: check if identifiers used in formula correspond to existing quantities - identifiers.uniq! - quantities = self.project.quantities.where(name: identifiers).pluck(:name) - if quantities.length != identifiers.length - errors.add(:formula, :unknown_quantity, {quantities: identifiers - quantities}) - end - end + validates :formula, formula: true after_initialize do if new_record? @@ -64,14 +29,14 @@ class Quantity < ActiveRecord::Base def formula_quantities q_names = Ripper.lex(formula).map do |*, ttype, token| - token if QUANTITY_TTYPES.include?(ttype) + token if BodyTracking::Formula::QUANTITY_TTYPES.include?(ttype) end.compact self.project.quantities.where(name: q_names).to_a end def calculate(inputs) paramed_formula = Ripper.lex(formula).map do |*, ttype, token| - QUANTITY_TTYPES.include?(ttype) ? "params['#{token}']" : token + BodyTracking::Formula::QUANTITY_TTYPES.include?(ttype) ? "params['#{token}']" : token end.join inputs.map { |i, values| [i, get_binding(values).eval(paramed_formula)] } end diff --git a/lib/body_tracking/filter.rb b/lib/body_tracking/filter.rb deleted file mode 100644 index e69de29..0000000 diff --git a/lib/body_tracking/formula.rb b/lib/body_tracking/formula.rb new file mode 100644 index 0000000..271e1fb --- /dev/null +++ b/lib/body_tracking/formula.rb @@ -0,0 +1,64 @@ +module BodyTracking + module Formula + require 'ripper' + QUANTITY_TTYPES = [:on_ident, :on_tstring_content, :on_const] + + class Formula + def initialize(project, formula) + @project = project + @formula = formula + end + + def validate(options = {}) + errors = [] + + # 1st: check if formula is valid Ruby code + tokenized_length = Ripper.tokenize(@formula).join.length + unless tokenized_length == @formula.length + errors << [:invalid_formula, {part: @formula[0...tokenized_length]}] + end + + # 2nd: check if formula contains only allowed token types + identifiers = [] + Ripper.lex(@formula).each do |location, ttype, token| + case + when QUANTITY_TTYPES.include?(ttype) + identifiers << token + when [:on_sp, :on_int, :on_rational, :on_float, :on_tstring_beg, :on_tstring_end, + :on_lparen, :on_rparen].include?(ttype) + when :on_op == ttype && '+-*/'.include?(token) + else + errors << [:disallowed_token, {token: token, ttype: ttype, location: location}] + end + end + + # 3rd: check for disallowed function calls (they are not detected by Ripper.lex) + # FIXME: this is unreliable (?) detection of function calls, should be replaced + # with parsing Ripper.sexp if necessary + function = Ripper.slice(@formula, 'ident [sp]* lparen') + errors << [:disallowed_function_call, {function: function}] if function + + # 4th: check if identifiers used in formula correspond to existing quantities + identifiers.uniq! + quantities = @project.quantities.where(name: identifiers).pluck(:name) + if quantities.length != identifiers.length + errors << [:unknown_quantity, {quantities: identifiers - quantities}] + end + + errors + end + end + + class FormulaValidator < ActiveModel::EachValidator + def initialize(options) + super(options) + end + + def validate_each(record, attribute, value) + Formula.new(record.project, value).validate(options).each do |message, params| + record.errors.add(attribute, message, params) + end + end + end + end +end