Reading and Writing the Context

ctx is the function’s input: a tree of values addressed by dotted paths such as quote.driver.age. It is always available — it is the default argument of every function — and it is also what you pass on to decision tables, other functions and flows.

Reading a Value#

def name    = ctx.getString('quote.driver.firstname')
def age     = ctx.getInteger('quote.driver.age')
def premium = ctx.getDecimal('quote.premium')
def factor  = ctx.getNumber('quote.factor')
def active  = ctx.getBoolean('quote.active')
def born    = ctx.getDate('quote.driver.birthDate')
def bornOn  = ctx.getLocalDate('quote.driver.birthDate')
def created = ctx.getDatetime('quote.createdAt')        // Date including the time part
def createdOn = ctx.getLocalDatetime('quote.createdAt') // LocalDateTime
def raw     = ctx.get('quote.driver')                   // whatever sits at that path

On ctx the method is getLocalDatetime — lowercase t. On higson and type the same conversion is spelled getLocalDateTime, with a capital T. Using the wrong spelling fails at runtime with a missing-method error.

getNumber returns 0.0 and getBoolean returns false when the path holds nothing — the same silent substitution the primitive getters make everywhere else in Higson. Use getDecimal or getInteger when a missing value has to stay missing.

Reading a Collection#

getFirst returns the first element of a collection, or null when the path is absent or does not hold something iterable:

def firstRisk = ctx.getFirst('quote.risks')

For the whole collection, read the path and iterate it:

def risks = ctx.get('quote.risks')
def total = risks.sum { it.getDecimal('premium') ?: BigDecimal.ZERO }

Checking Before Reading#

if (ctx.has('quote.discountCode')) {
    discount = higson.getDecimal('motor.discount', ctx)
}

has does not walk sub-contexts. Unlike get, it is a flat key check, so ctx.has('quote.driver.age') is false when quote is a sub-context that holds driver.age. For a nested path, read it and test the result for null instead.

Writing a Value#

set stores a value and returns the context, so calls chain:

ctx.set('quote.calculatedPremium', premium)
   .set('quote.calculatedAt', date.currentDatetime())

set throws when the path already exists. A second set on the same path raises a duplicate-item error rather than overwriting. To replace a value deliberately, use the three-argument with:

ctx.with('quote.premium', premium, true)   // true = allow overwrite

with(path, value) is the two-argument form and behaves like set — it also refuses to overwrite.

Writing to the context is how you hand a computed value down to a nested lookup:

ctx.set('quote.riskClass', riskClass)
def rate = higson.getDecimal('motor.rate.by.riskclass', ctx)

When you only need the override for one lookup and do not want it to persist, prefer the token form instead — it leaves ctx untouched:

def rate = higson.getDecimal('motor.rate.by.riskclass', ctx, 'quote.riskClass', riskClass)

How Paths Resolve#

  • Paths are case-insensitive. Keys are lowercased on both write and read, so quote.driverAge and quote.driverage address the same value.
  • Resolution is recursive. ctx.get('a.b.c') tries the whole key first, then splits on the first dot and looks for b.c inside the sub-context at a, and so on. Deeply nested paths therefore cost more to resolve than flat ones — worth knowing in a function that reads the same deep path inside a loop. Read it once into a local variable.

Value Holders#

getNumber and getBoolean substitute 0.0 and false for a missing path, which makes “absent” and “zero” indistinguishable. The holder getters are the way out — they return the value together with its Higson type, and preserve absence:

def holder = ctx.getIntegerHolder('quote.driver.age')

if (holder.isNull()) {
    return null                 // the path really was not supplied
}
def age = holder.getInteger()   // null-preserving; intValue() would give 0

One per type:

ctx.getStringHolder(path)      ctx.getNumberHolder(path)
ctx.getIntegerHolder(path)     ctx.getBooleanHolder(path)
ctx.getDateHolder(path)        ctx.getDatetimeHolder(path)
ctx.getLocalDateHolder(path)   ctx.getLocalDatetimeHolder(path)

ctx.getLocalDatetimeHolder follows the same lowercase-t spelling as ctx.getLocalDatetime, while the equivalent on type is toLocalDateTimeHolder with a capital T.

The full holder surface — isNull / isNotNull / isBlank, the null-preserving object getters and the defaulting primitive ones — is described in Built-in Objects in a Function.

Traps#

  • ctx.set on an existing path throws. Use ctx.with(path, value, true) to overwrite.
  • ctx.has is flat, so it answers false for nested paths that get resolves fine.
  • getNumber is 0.0 and getBoolean is false for a missing path, with no error. Use ctx.getIntegerHolder(path).isNull() when you must tell absent from zero.
  • getLocalDatetime on ctx versus getLocalDateTime elsewhere — the capitalisation differs.
  • Avoid ctx.get(key, SomeClass). It casts without converting, so it throws a ClassCastException at the call site rather than converting the value. Use the typed getters, or type.getX(...).