Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for custom object evaluation #227

Open
bbugh opened this issue Dec 1, 2020 · 4 comments
Open

Support for custom object evaluation #227

bbugh opened this issue Dec 1, 2020 · 4 comments

Comments

@bbugh
Copy link

bbugh commented Dec 1, 2020

Hi! 👋 This gem looks awesome, thanks for making it. It's a perfect fit for some needs we have to do simple user template evaluation to generate documents. Our use case includes object evaluation, though, and I couldn't see a way for it to work with dentaku. For example, "user.age >= 18, where user is a simple class like this:

class User
  attr_reader :age
end

I tried doing something like

calculator.store(user: ->{ User.last })
calculator.evaluate("user.age >= 15")
=> nil

We managed to make it work by adding a string representing the object chain...

calculator.store('user.age', ->{ User.last.age })
calculator.evaluate("user.age >= 15")
=> true

...but we have hundreds of variations and nestings we'd have to support (like user.profile.default_value) and it wouldn't be practical to generate them all.

Is there a mechanism that would allow for the dot chain evaluation that I missed, maybe some kind of custom evaluator? If not, do you think it would be feasible to add?

(We can also pre-evaluate the values into a string, and then use Dentaku to calculate the final results, but that would unfortunately require a large refactor of our parser and generator, so this seems like the simplest route to start).

Thanks!

@rubysolo
Copy link
Owner

rubysolo commented Dec 2, 2020

I would approach this by figuring out a way to transform your objects into hashes, and then pass those in as the context data for your formulas. If these are something like ActiveRecord or ActiveModel objects, then you can probably use attributes or as_json to help. If these are just simple Ruby classes, then a quick attribute serializer module might do the trick:

module Hashify
  def hashify
    instance_variables.each_with_object({}) do |var, h|
      key = var.to_s[1..] # transform :@var into "var"
      value = instance_variable_get(var)
      h[key] = value.respond_to?(:hashify) ? value.hashify : value
    end
  end
end

class User
  include Hashify
  attr_reader :age, :profile

  def initialize(age, profile)
    @age = age
    @profile = profile
  end
end

class Profile
  include Hashify
  attr_reader :favorite_color

  def initialize(favorite_color)
    @favorite_color = favorite_color
  end
end

user = User.new(21, Profile.new("red"))
Dentaku("user.age >= 18 AND user.profile.favorite_color = 'red'", user: user.hashify)
# => true

@bbugh
Copy link
Author

bbugh commented Dec 2, 2020

Thanks for the reply! We looked at hashing, but unfortunately all of the fields need to be lazy-loaded. We have hundreds of calculated value methods used in these templates, and in aggregate would be way too slow. Generating a full hash for just one object could take 10+ seconds, and in many cases dozens of objects are used in one template.

If supporting dot notation is not a good fit, another solution that could work is making it possible to override/subclass variable lookup. Something like:

class CustomVariableLookup < DentakuLookup
  def initialize(context)
    @context = context 
  end

  def value(name)
    # look up name and method availability (split on first dots?)
    if found
      result
    else
      super
    end
  end
end

calculator = Dentaku::Calculator.new(lookup: CustomVariableLookup.new(our_context))

@rubysolo
Copy link
Owner

rubysolo commented Dec 2, 2020

Ah, sorry -- I guess that's what you meant by "it wouldn't be practical to generate them all". 😆

Currently Dentaku eagerly flattens hash values into dot-separated keys when storing things in memory by default, but you can disable this by setting nested_data_support to false on the Calculator instance. However, that would probably not be enough because variable lookup assumes a flattened key. I can look into how to best support this.

As a temporary workaround, would it be feasible to make the "hashify" code a little smarter so that you could feed in just the values you need flattened? If so, you could parse a formula, get the unbound variables needed to evaluate it and then feed that to hashify to get just the data you need.

@rubysolo
Copy link
Owner

rubysolo commented Dec 2, 2020

I pushed up a lazy-resolver branch, can you take a look and see if this would work for your use case?

(updated to fix branch reference) 😊

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants