Moved formula code/validation to separate module
This commit is contained in:
parent
884f7b2366
commit
19e2a45ba8
@ -1,6 +1,5 @@
|
|||||||
class Quantity < ActiveRecord::Base
|
class Quantity < ActiveRecord::Base
|
||||||
require 'ripper'
|
include BodyTracking::Formula
|
||||||
QUANTITY_TTYPES = [:on_ident, :on_tstring_content, :on_const]
|
|
||||||
|
|
||||||
enum domain: {
|
enum domain: {
|
||||||
diet: 0,
|
diet: 0,
|
||||||
@ -16,41 +15,7 @@ class Quantity < ActiveRecord::Base
|
|||||||
validate if: -> { parent.present? } do
|
validate if: -> { parent.present? } do
|
||||||
errors.add(:parent, :parent_domain_mismatch) unless domain == parent.domain
|
errors.add(:parent, :parent_domain_mismatch) unless domain == parent.domain
|
||||||
end
|
end
|
||||||
validate if: -> { formula.present? } do
|
validates :formula, formula: true
|
||||||
# 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
|
|
||||||
|
|
||||||
after_initialize do
|
after_initialize do
|
||||||
if new_record?
|
if new_record?
|
||||||
@ -64,14 +29,14 @@ class Quantity < ActiveRecord::Base
|
|||||||
|
|
||||||
def formula_quantities
|
def formula_quantities
|
||||||
q_names = Ripper.lex(formula).map do |*, ttype, token|
|
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
|
end.compact
|
||||||
self.project.quantities.where(name: q_names).to_a
|
self.project.quantities.where(name: q_names).to_a
|
||||||
end
|
end
|
||||||
|
|
||||||
def calculate(inputs)
|
def calculate(inputs)
|
||||||
paramed_formula = Ripper.lex(formula).map do |*, ttype, token|
|
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
|
end.join
|
||||||
inputs.map { |i, values| [i, get_binding(values).eval(paramed_formula)] }
|
inputs.map { |i, values| [i, get_binding(values).eval(paramed_formula)] }
|
||||||
end
|
end
|
||||||
|
64
lib/body_tracking/formula.rb
Normal file
64
lib/body_tracking/formula.rb
Normal file
@ -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
|
Reference in New Issue
Block a user