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

Rules that pass when finding facts, or give an error otherwise #88

Open
mannerheim77 opened this issue Aug 25, 2024 · 2 comments
Open

Comments

@mannerheim77
Copy link

mannerheim77 commented Aug 25, 2024

I need to run a ruleset, and for each rule that doesn't pass, I need to know why (via error message). Is it possible to implement rules which pass when finding facts, but generate an error when they don't? At first I thought it was a simple as this:

engine = Wongi::Engine.create
debit_rule = engine.rule "transaction type is debit" do
   forall {
     has :TRANSACTION, :transaction_type, :TX_TYPE
     equal :TX_TYPE, 2
   }
   make {
     error "Transaction is not a debit"
   }
end

engine << ["A", :transaction_type, 1]

But after scratching my head for a while, I eventually learned that this will throw an error if the forall finds those facts. I could negate the conditions in the forall, but I'd really rather have the conditions in the positive, since I feel like it reads better.

Any ideas?

@ulfurinn
Copy link
Owner

ulfurinn commented Sep 3, 2024

Well you should be able to wrap the entire condition block in a none, I think that's your best way out.

Otherwise you'd need to generate a surrogate fact after the positive match and then have a rule generating errors on the absence of those surrogate facts, which would quickly get pretty cumbersome to declare unless you find a generic pattern you can use.

@mannerheim77
Copy link
Author

I ended up writing an on_fail DSL extension, which serves as a simple holder of error messages for each rule. Kinda lame, but it works. Probably won't scale for large rulesets though. I might just go with your none suggestion after some thinking. But here's my code regardless:

include Wongi::Engine
include Wongi::Engine::DSL

dsl do
  section :forall
  clause :on_fail
  accept ::MyRules::OnFail
end

...
...

module MyRules
  class OnFail
    attr_reader :message
    def initialize(message = nil)
      @message = message
    end

    def compile(context)
      context
    end
  end
end

...
...

engine = Wongi::Engine.create

debit_rule = engine.rule "transaction type is debit" do
 forall {
   has :TRANSACTION, :transaction_type, :TX_TYPE
   equal :TX_TYPE, 2
   on_fail "Transaction is not of type debit."
 }
end

...more rules...

After ingesting the rules and facts into my engine, I then just iterate through the productions and check which ones have tokens (pass) vs not (fail):

   rule_results = []
   
   engine.with_overlay do |overlay|
      my_facts.each { |fact| overlay << fact }
      rule_results.concat(build_rule_results)
   end

   def build_rule_results
      engine.productions.map do |rule_name, production_node|
        run_status = production_node.tokens.any? ? :passed : :failed
        rule_result = RuleResult.new(rule_name, run_status)

        if run_status == :failed
          rule_result.error = error_message(production_node)
        end

        rule_result
      end
    end
      
    def error_message(production_node)
      production_node.compilation_context.conditions.detect do |c|
        c.is_a?(::MyRules::OnFail)
      end&.message
    end

    RuleResult = Struct.new(:rule_name, :result, :error) do
      def failure?
        result == :failed
      end
      def success?
        result == :passed
      end
    end

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