1
0
This repository has been archived on 2023-12-07. You can view files and clone it, but cannot push or open issues or pull requests.
body_tracking/app/models/quantity.rb
2019-11-05 16:02:53 +01:00

70 lines
2.2 KiB
Ruby

class Quantity < ActiveRecord::Base
require 'ripper'
enum domain: {
diet: 0,
measurement: 1,
exercise: 2
}
acts_as_nested_set dependent: :destroy, scope: :project
belongs_to :project, required: false
validates :name, presence: true, uniqueness: {scope: :project_id}
validates :domain, inclusion: {in: domains.keys}
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 [:on_ident, :on_tstring_content, :on_const].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
if new_record?
self.primary = false if self.primary.nil?
end
end
def toggle_primary!
self.toggle!(:primary)
end
def formula_quantities
end
def calculate(inputs)
end
end