zeroSteiner / rule-engine

A lightweight, optionally typed expression language with a custom grammar for matching arbitrary Python objects.
https://zerosteiner.github.io/rule-engine/
BSD 3-Clause "New" or "Revised" License
433 stars 54 forks source link

Does rule engine support functions? #32

Closed armona closed 1 year ago

armona commented 2 years ago

Was wondering if it's possible to implement custom functions to the rule engine, something of the sort of:

get_hostname("https://example.com/path") == "github.com"
zeroSteiner commented 2 years ago

It's possible and I've been asked about it before. It would just be alot of work.

armona commented 2 years ago

I might have a poke at it on my spare times. Any guidance on how it should be implemented? which areas need change/additions?

zeroSteiner commented 2 years ago

The ideal implementation would likely involve: 1) A new function / callable datatype. New data types are a lot of work. 2) Parser support for making a function call. 3) An AST node for the function call with a reduction method. The reduction should probably support the function being known at parse time which would involve a bit of research into solutions and patterns. 4) Type hinting and propagation as necessary for the AST node of the function call. 5) Unit tests for the whole thing. 6) Documentation updates describing how to use it.

rwspielman commented 2 years ago

This may be something that I work on if it becomes necessary in my project (right now I can get around it, but this is a fairly important feature). @armona have you done any work on this?

armona commented 2 years ago

@rwspielman I started working on it (not a lot though), but as @zeroSteiner said it is a lot of wok and I can't find the time to implement it.

nicolas-rdgs commented 1 year ago

I'm also interested about custom functions, I have an use case like this :

last_seen <= now('-15d')

Where last_seen is a date in ISO format with timezone aware.

zeroSteiner commented 1 year ago

@nicolas-rdgs

You can do that already as long as last_seen is a datetime.datetime instance. DATETIME objects are timezone aware already. If there's no timezone information associated with it, then the default_timezone is used from the context.

edit the 'context' and 'thing' objects as necessary
>>> import datetime
>>> thing = {'last_seen': datetime.datetime.now()}
>>> 
exiting the edit console...
rule > last_seen <= ($now - t"P15D")
result: 
False
rule > 
zeroSteiner commented 1 year ago

I've started on this in a feature branch. There's still quite a bit left to do. I still need to:

So far the parsing and AST node generation with reduction is in place and appears to be working just fine.

PYTHONPATH=lib python -m rule_engine.debug_repl --edit-console
edit the 'context' and 'thing' objects as necessary
>>> thing = {'noargs': lambda: 'hello!', 'add': lambda a,b: a + b, 'greet': lambda name: 'Hello ' + name}
>>> 
exiting the edit console...
rule > noargs()
result: 
'hello!'
rule > add(1, 2)
result: 
Decimal('3.0')
rule > greet('Spencer')
result: 
'Hello Spencer'
rule > 

The branch is feat/functions if anyone is interested in previewing it.

zeroSteiner commented 1 year ago

Progress is slow and steady. Will probably be done in another 3 weeks. Maybe less, maybe more.

zeroSteiner commented 1 year ago

This ticket has now been completed.

Functions are included in the latest release, version 4.0.0. This release included a few breaking changes that are noted in the change log. There are 12 builtin functions that are available out of the box. I included functions from all of the tickets I'd marked as duplicates of this request since this it was opened about a year a half ago (sum, parse_datetime, etc.). I've written documentation that describes the syntax and how function types can be defined.

amrabed commented 11 months ago

@zeroSteiner it's not clear from the documentation how/where to define a new custom function when the rule is to be applied to an object. Would you please provide an example of such a case or point me to where I can find one?

Also, this part of the documentation about FUNCTION is a bit confusing:

Additional functions can be either added them to the evaluated object or by extending the builtin symbols. It is only possible to call a function from within the rule text. Functions can not be defined as other data types can be.

zeroSteiner commented 1 month ago

I clarified that a bit in the documentation. For anyone else that comes across this in the future, you add a function just like you'd add any other value.

thing = {'name': 'Test Case', 'my_function': my_function}
Rule('my_function(name)').evaluate(thing)

Alternatively, you can expose it through changing the builtin symbols and access it with the $ prefix.