Mooncake: A simplistic validation language

Mooncake: A simplistic validation language
Garry meets Mooncake [Credits: Final Space]

Validating objects in code is an important part of almost every other project. Having gone through significantly large code bases where objects easily get more complex over time. The conventional validation methods can grow even more verbose, less scalable and maintainable.

The conventional methods usually comprise of several conditional checks, or perhaps a fancy framework on top using designs like method-chaining, schema validation etc. Looking at those tremendously huge code files with such verbose statements and checks. It only comes naturally to take the usual behaviour of conditionals and turn it into a simplistic language. The language may seem to be specific for a certain solution, but the idea is have it evolve over time while always keeping simplicity in mind.

Meet "Mooncake", a descriptive yet less verbose validation language.

The name is inspired from the character Mooncake in the tv-series Final Space. Mooncake has a minimal vocabulary; yet he manages to convey his feelings conveniently...thus the inspiration :)

How it works

The language is built with Antlr for Golang runtime. Antlr is a compiler-compiler which makes it easer to design and test languages.

Currently, objects in Json form can be handled, since behind the scene JsonPath is being used to locate attributes in objects. But this would eventually be replaced by language specific constructs e.g., Golang's structs using methods like reflection.

So to begin with, we need to compose rules that validate different fields/attributes of an object. A simplistic rule composition is as follows:

item.id eq nil => [E0123, 'id can't be null']!!! # this causes fatal error
This says:
if expression: item.id eq nil holds
then implication: [E0123, 'id can't be null']!!!

Breaking down further we have:

expression:

  • item.id -> identifier (attribute path)
  • eq -> operator (equal)
  • nil -> literal (null)

implication:

  • E0123 -> Error code
  • 'id can't be null' -> Error message
  • !!! -> Error severity, here three exclamation marks mean Fatal
  • # this causes ... -> Can be any statement related comment (not required)

Now, lets see how a group of statements look like:

item.id   eq nil => [E0123, 'id can't be null']!!!  # this is a fatal error
item.name eq nil => [E0124, 'name can't be null']!! # this is a severe error
item.addr eq nil => [E0125, 'addr can't be null']!  # this is a warning error

All simple and straightforward.

But what about statements which depend on each other? Example: item.name eq nil should be evaluated only if the expression item.id eq nil doesn't hold.

This can easily be done by appending ~ which allows linking of statements:

~item.id   eq nil => [E0123, 'id can't be null']!!!  # fatal
~item.name eq nil => [E0124, 'name can't be null']!! # severe
~item.addr eq nil => [E0125, 'addr can't be null']!  # warning

So now all three statements depend on each other. The evaluation is done sequentially in order, which also allows for circuit-breaking.

Statements can also be grouped together in associated blocks. For example, a top level attribute item should always exist, in order to check all the remaining attributes:

item eq nil => [E0122, 'item can't be null']!!!  # fatal
{
    ~item.id   eq nil => [E0123, 'id can't be null']!!!  # fatal
    ~item.name eq nil => [E0124, 'name can't be null']!! # severe
    ~item.addr eq nil => [E0125, 'addr can't be null']!  # warning
}

Infact another way of writing linked statements can be with blocks:

item eq nil => [E0122, 'item can't be null']!!!  # fatal
{
    item.id eq nil => [E0123, 'id can't be null']!!!  # fatal
    {
        item.name eq nil => [E0124, 'name can't be null']!! # severe
        {
            item.addr eq nil => [E0125, 'addr can't be null']!  # warning   
        }
    }
}

Which is not so cool. Agreed!

So the blocks are more helpful when using declarations. More on this after functions.

Mooncake offers some built-in functions that can be applied to the identifiers and then checked with a literal within an expression.

Here's an example of a length(@len) function:

@len item.list eq 0 => [E0126, 'list is empty']!! # severe

It could be the case that the calculated length has to be further filtered or reused in other statements. And so we have a useful case for declarations and blocks:

_x @len item.list eq 0 => [E0126, 'list is empty']!! # severe
{
    _x lt 2 => [E0127, 'I'd have loved more']! # warning
    
    _x gt ${ctx.threshold} => [E0128, 'Ok, that's way too much']! # warning
}

Here, a declaration _x holds the function @len value after applying to item.list identifier. Later on, inside the block, _x is used for validating some warnings.

The funny looking ${ctx.threshold} literal, happens to reference a field in the context struct.

With ${...} you have the ability to check against the referenced structures within the code that you pass to the mooncake executor.

Now that the rules are written and also executed (see github for execution details), we would have our result in a structure composing of arrays of all the fatals', severe's, warnings' encountered during validation.

Pretty much like this:

{
   "Fatal":[
      {
         "Code":"E0122",
         "Info":"item is nil"
      }
   ],
   "Severe":[],
   "Warning":[]
}

For the list of all operators, functions, errors and syntax related stuff please see the Readme on mooncake's repository: https://github.com/talal830/mooncake

Let's see where it goes from here :)