This repo provides base classes for modeling event sourced data and describing buisness logic w/ declarative rules.
Facts are immutable records of events that occur in our application or in the external world. Instead of storing the current state of objects in our problem domain, we can store their history of changes as events. From the history, we are able to recreate a state at any point in time, and to audit the events that caused it.
In terms of code, facts can be thought of as hashes w/ a unique id, type, set of causes, and any number of other named attributes.
fact = Fact.new name: 'A'
fact.name # 'A'
fact[:name] # 'A'
Rules are defined by describing a pattern using a block of PQL code, and profiding a block of ruby code to run for each successful match. The block is run in a context with methods defined for each of the pattern's named matches.
The action block is passed one argument, 'e', an Entry
instance. The entry has methods available to write each type of fact in the ontology.
Facts written by the rule will store their cause, a list of all the ids of the facts making up the current match. Facts written by the rule also include a number of default attributes enumerated as the rule's header. The rule will pass the value for each of these attributes from the most recent fact from the subject set to the entry, which will include them in every new fact that it creates.
class PerItemDiscountAccountingRule < Rule
description 'split order level discounts across individual items'
header :user_id, :order_id
pattern <<-PQL
MATCH EACH AS item WHERE type IS 'ItemAddedToCart';
MATCH EACH AS discount WHERE type IS 'OrderLevelDiscountApplied';
PQL
method :amount do
discount.percent * item.amount
end
action do |e|
e.order_level_discount_applied_to_item(
sku: item.sku,
promotion_id: discount.promotion_id,
amount: amount
)
end
end
Entries store metadata around the creation of facts and are produced each time a rule is applied. Entries provide a record of a group of facts created together, the reason for their creation, and the line number and git sha of the source code responsible for their creation.
Entries also provide a shorthand for writing verbose fact headers. When an entry is created, it is passed a hash as a header to be included as attributes of each fact it writes.
Rulesets wrap ordered sets of rules, and can apply them in order to a set of facts. Facts produced by each rule are appended to the set of facts before the next is applied.
Applying a ruleset returns a single Transaction
object.
ruleset = Ruleset.new(
InventoryCheckRule.new,
DiscountRule.new(threshold: 50, percent: 15),
StoreCreditApplicationRule.new
)
transaction = ruleset.apply(factset)
transaction.persist!
Transactions record many entries and facts in single round trip to the database.
transaction = FactStore::Transaction.new
transaction << entry
transaction.persist!
Transactions are by default non-atomic, but atomic transactions can be created by passing an atomic: true
option to their constructor.
The fact ontology represents types as a directed acyclic graph, where each type is a node having edges directed from itself to any number of parent types. A type's ancestors are the set of all reachable nodes. Facts are considered to be members of their own type and of each of its ancestor types.
Ontology.define do
type :A, [:B, :C]
type :D, [:A]
end
In this example, an fact of type 'A' will belong to the types 'A', 'B', and 'C', and an fact of type 'D' will belong to the types 'D', 'A', 'B', and 'C'.
The fact store is used to query persisted facts.
FactStore.query type: 'ItemAddedToCart', sku: 'ABC1'
A Query to the fact store returns a Factset, containing all facts matching the given conditions.
(still in progress)
Entry::initialize(description, header, cause = [], attrs = {})
Entry#facts
Entry#[](key)
Entry#{{fact_type}}
Fact::initialize(attributes)
creats a new, immutable, fact instance with the given attributes.
Fact#[](key)
returns the value of the attribute with the given key.
Fact#{{attribute}}
returns the value of the attribute
Fact#types
returns a list of all types the fact belongs to based on its:type
attribute and the fact ontology.
Fact#has_type?(type)
returnstrue
if the fact is a member of the given type,false
otherwise.
Fact#causes?(fact)
returnstrue
if the fact is a cause of the given fact,false
otherwise.
Fact#caused_by?(fact)
returnstrue
if the fact is caused by the given fact,false
otherwise.
Fact#to_hash
returns the facts attributes as aHash
.
Fact::Ontology::define(&block)
runs the given block in a context w/ theFact::Ontology::type
method available.
FactOntology::type(name, parents = [])
(private) defines a new type with the given name belonging to the given parent types. Expects name and parents to be symbols.
Fact::Ontology::types
returns an array of all defined types.
Fact::Ontology::include?(type)
returnstrue
if the given type has been defined,false
otherwise.
Fact::Ontology::lookup(type)
returns an array of all types the given type belongs to (including the given type).
FactStore::query(attributes)
queries the database for facts matching the given attributes, returns an enumerableFactset
object.
FactStore::Transaction::initialize(options)
creates a new, empty, trasaction. Accepts a boolean 'atomic' option.
FactStore::Transaction#<<(entry)
appends an entry to the transaction. If the transaction has already been persisted, instead raises an error.
FactStore::Transaction#persist!
writes the appended entries and facts to the database. Returnstrue
if the operation succeeds,false
otherwise.
FactSTore::Transaction#persisted?
returnstrue
if the transaction has sucessfully written facts to the database,false
otherwise.
Rule::description(description)
(private)
Rule::header(*columns)
(private)
Rule::pattern(pql)
(private)
Rule::method(name, &block)
(private)
Rule::action(&block)
(private)
Rule#description
Rule#pattern
Rule#methods
Rule#action
Rule#header_for(factset)
Rule#apply(factset)
Ruleset::initialize(*rules)
- creates a new ruleset w/ given rules
Ruleset#apply(factset)
- applies the rules in order to the factset, returns aTransaction
wrapping resulting entries
Factset::initialize(facts)