1
0

Quantity formula assignment and validation

This commit is contained in:
cryptogopher 2019-11-03 18:35:22 +01:00
parent fd24bced60
commit fce6ee57b0
4 changed files with 50 additions and 2 deletions

View File

@ -60,10 +60,11 @@ class QuantitiesController < ApplicationController
def quantity_params def quantity_params
params.require(:quantity).permit( params.require(:quantity).permit(
:name,
:description,
:domain, :domain,
:parent_id, :parent_id,
:name,
:description,
:formula,
:primary :primary
) )
end end

View File

@ -1,4 +1,6 @@
class Quantity < ActiveRecord::Base class Quantity < ActiveRecord::Base
require 'ripper'
enum domain: { enum domain: {
diet: 0, diet: 0,
measurement: 1, measurement: 1,
@ -13,6 +15,41 @@ 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
# 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 after_initialize do
if new_record? if new_record?

View File

@ -10,6 +10,7 @@
<% end %> <% end %>
<p><%= f.text_field :name, size: 25, required: true %></p> <p><%= f.text_field :name, size: 25, required: true %></p>
<p><%= f.text_field :description, size: 200 %></p> <p><%= f.text_field :description, size: 200 %></p>
<p><%= f.text_field :formula, size: 200, placeholder: t('.formula_placeholder') %></p>
<p><%= f.check_box :primary %></p> <p><%= f.check_box :primary %></p>
</div> </div>

View File

@ -10,6 +10,7 @@ en:
field_nutrients: 'Nutrients:' field_nutrients: 'Nutrients:'
field_domain: 'Domain' field_domain: 'Domain'
field_parent_quantity: 'Parent' field_parent_quantity: 'Parent'
field_formula: 'Formula'
field_primary: 'Primary' field_primary: 'Primary'
field_shortname: 'Short name' field_shortname: 'Short name'
button_primary: 'Primary' button_primary: 'Primary'
@ -24,6 +25,12 @@ en:
attributes: attributes:
parent: parent:
parent_domain_mismatch: 'parent quantity has to be in the same domain' parent_domain_mismatch: 'parent quantity has to be in the same domain'
formula:
invalid_formula: 'interpretation failed after: %{part}'
disallowed_token: 'includes disallowed token "%{token}"
(of type %{ttype}) at %{location}'
disallowed_function_call: 'includes disallowed function call %{function}'
unknown_quantity: 'contains undefined quantities: %{quantities}'
body_trackers: body_trackers:
index: index:
heading: 'Summary' heading: 'Summary'
@ -82,6 +89,8 @@ en:
measurement: 'measurement' measurement: 'measurement'
exercise: 'exercise' exercise: 'exercise'
null_parent: '- none -' null_parent: '- none -'
formula_placeholder: 'provide if value of quantity has to be computed in terms of
other quantities'
units: units:
index: index:
heading: 'Units' heading: 'Units'