Skip to content

thisisqubika/ruy

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ruy

Ruy is a library for defining a set of conditions and evaluating them against a context.

# discount_day.rb
gifter = Ruy::Rule.new

gifter.set :name, 'Unforgettable Fridays'

gifter.eq :friday, :day_of_week

gifter.outcome 8 do
  greater_than_or_equal 300, :amount
end

gifter.outcome 7 do
  greater_than_or_equal 100, :amount
end

gifter.outcome 3

gifter.fallback 0

Rules are evaluated against a context (the Hash being passed to #call) and return the first outcome that matches.

gifter.call(day_of_week: :friday, amount: 314)

# => 8
gifter.call(day_of_week: :friday, amount: 256)

# => 7

If no outcome matches, the default one is returned.

gifter.call(day_of_week: :friday, amount: 99)

# => 3

If conditions are not met, the fallback value is returned.

gifter.call(day_of_week: :monday, amount: 124)

# => 0

Retrieve rule attributes.

gifter.get(:name)

# => 'Unforgettable Fridays'

Key concepts

Ruy at its core is about evaluating a set of conditions against a context in order to return a result.

Conditions

A condition evaluates the state of the context.

Available conditions:

  • all All of the nested conditions must suffice
  • any At least one of its nested conditions must suffice
  • assert Tests that a given context value must be a truthy value
  • between Evaluates that a given context value must belong to a specified range
  • cond At least one slice of two nested conditions must suffice
  • day_of_week Evaluates that a Date/DateTime/Time weekday is matched
  • dig Digs into a Hash allowing to define conditions over nested attributes
  • eq Tests a context value for equality
  • every Evaluates that at all the elements of a context enumerable matches the nested conditions
  • except Evaluates that any of the nested conditions does not suffice
  • greater_than Tests that a given context value is greater than something
  • greater_than_or_equal Tests that a given context value is greater than or equal to something
  • in Given context value must belong to a specified list of values
  • in_cyclic_order Expects that a given context value is included in a cyclic order
  • include The context value must include a specified value
  • less_than_or_equal Tests that a given context value is less than or equal to something
  • less_than Tests that a given context value is less than something
  • some Evaluates that at least one element of a context enumerable matches the nested conditions

Rules

A Rule is a set of conditions that must suffice, and return a value resulting from either an outcome or a fallback.

Contexts

A context is a Hash from which values are fetched in order to evaluate a Rule.

Lazy values

Rules can define lazy values. The context must provide a Proc which is evaluted only once the first time the value is needed. The result returned by the proc is memoized and used to evaluate subsequent conditions.

# premium_discount_day.rb
gifter = Ruy::Rule.new

gifter.let :amounts_average # an expensive calculation

gifter.eq :friday, :day_of_week

gifter.greater_than_or_equal 10_000, :amounts_average

gifter.outcome true
gifter.call(day_of_week: :friday, amounts_average: -> { Stats::Amounts.compute_average })

Outcomes

An outcome is the result of a successful Rule evaluation. An outcome can also have nested conditions, in such case, if the conditions meet, the outcome value is returned.

A Rule can have multiple outcomes, the first matching one is returned.

Time Zone awareness

When it comes to matching times in different time zones, Ruy is bundled with a built in tz block that will enable specific matchers to make time zone-aware comparisons.

rule = Ruy::Rule.new

rule.tz 'America/New_York' do
  eq '2015-01-01T00:00:00', :timestamp
end

rule.outcome 'Happy New Year, NYC!'

For example, if the timestamp provided in the context is a Ruby Time object in UTC (zero offset from UTC), eq as child of a tz block will take the time zone passed as argument to the block (America/New_York) to calculate the current offset and make the comparison.

String time patterns follow the Ruy's well-formed time pattern structure as follows:

YYYY-MM-DDTHH:MM:SS[z<IANA Time Zone Database identifier>]

Where the time zone identifier is optional, but if you specify it, will take precedence over the block's identifier. In case you don't specify it, Ruy will get the time zone from the tz block's argument. If neither the block nor the pattern specify it, UTC will be used.

Days of week matcher

Inside any tz block, there's a matcher to look for a specific day of the week in the time zone of the block.

rule = Ruy::Rule.new

rule.any do
  tz 'America/New_York' do
      day_of_week :saturday, :timestamp
  end

  tz 'America/New_York' do
      day_of_week 0, :timestamp # Sunday
  end
end

rule.outcome 'Have a nice weekend, NYC!'

This matcher supports both the Symbol and number syntax in the range (0..6) starting on Sunday.

The day of week matcher will try to parse timestamps using the ISO8601 format unless the context passes a Time object.

Nested blocks support

You cannot use matchers inside nested blocks in a tz block expecting them to work as if they were immediate children of tz.

A possible workaround for this is to use tz blocks inside the nested block in question:

rule = Ruy::Rule.new

rule.any do
  tz 'America/New_York' { eq '2015-01-01T00:00:00', :timestamp }
  tz 'America/New_York' { eq '2015-01-01T02:00:00zUTC', :timestamp }
end

rule.outcome 'Happy New Year, NYC!'

The following won't do what you expect. Instead, equality will be evaluated interpreting the time as a simple string.

rule.tz 'America/New_York' do
  any do
    eq '2015-01-01T00:00:00', :timestamp
    eq '2015-01-01T02:00:00zUTC', :timestamp
  end
end

Ruy depends on TZInfo to calculate offsets using IANA's Time Zone Database. Check their website for information about time zone identifiers.

Documentation

RubyDoc.info