Ludwig Validations

What is a Ludwig Validation?

Ludwig supports client-side validations. A validation is a type of function that tests a property of your code, and if any portion of the code fails that test, it will not compile and cannot be executed. For example, your organization may have a policy that inbound SSH access is not allowed except under certain circumstances, or that EC2 instance types must adhere to a pre-approved list. Through validations, policies such as these can be enforced locally at compile-time instead of at runtime, ensuring they are applied consistently and programmatically across your code base.

Validations are provided through the Ludwig.Validation module of the Fugue Standard Library. These validations can be applied to Ludwig sum types, including custom sum types. Sum types are types with a constructor. For example, you can define a validation for type A below, which has two constructors, A and B:

type A:
  | A
  | B

But you can’t write a validation for type C below, because it’s not a sum type:

type C:
  d: Int

You can write a validation or series of validations once in a library and import that library into as many compositions as needed.

When the composition is compiled locally – either by running the Ludwig compiler, lwc, or by executing fugue run or fugue update – Fugue will test the validations. If any validation fails, the compiler throws an error.

Writing Client-Side Ludwig Validations

There are a couple steps to writing a Ludwig validation. First, you need to write the validation function itself. Then, you need to register the validation by writing a little more Ludwig, a step that allows the compiler to apply the validation to all relevant types in scope. Once you’ve done that, you’re free to write the rest of your code as usual. For modularity’s sake, we recommend keeping your validations in a separate module from the rest of your compositions, and simply importing the validation module wherever it’s needed. However, you can write validation and code in the same composition if desired.

First, we’re going to talk about the validation function.

Writing the Validation Function

A validation is just a type of function. For the purposes of this tutorial, we’ll assume you know how to write functions already. But don’t worry – if you need a refresher on how they work, check out our Functions Tutorial.

We’ll use a simple example. Any composition that imports this validation must run in the us-west-2 region, or it will fail compilation.

To start, we’ll import the Fugue.AWS module. This module contains the region types.

import Fugue.AWS as AWS

Next comes the actual function.

fun usWest2Only(region: AWS.Region) -> Validation:
  case region of
  | AWS.Us-west-2  -> Validation.success
  | _              -> Validation.failure("Infrastructure is allowed in us-west-2 only.")

Let’s unpack this a bit. As you may recall from Functions Tutorial, Part 2, this is a case statement. In the first line, we declare a function called usWest2Only, which takes an AWS.Region and returns a Validation.

As you may have guessed, Validation is a Ludwig type. The Validation type has two constructors, Success and Failure. Success indicates that the compiler may proceed. Failure indicates that the compiler must throw an error and halt.

This case statement specifies that a value of AWS.Us-west-2 returns a Validation.success function, which in turn returns a Success value. Any other value returns a Validation.failure function, which in turn returns a Failure value and raises the error given in the string – in this case, “Infrastructure is allowed in us-west-2 only.”

Put simply, this function dictates that the only acceptable value for AWS.Region is AWS.Us-west-2. Anything else will cause a compiler error.

Registering the Validation

This part is nice and simple. In order to apply our validation to all AWS.Region occurrences in scope at compile time, we must register the validation. Registering the validation just means using the validate keyword, along with the name of the validation function, like so:

validate usWest2Only

Now that we’ve registered our validation, we can ensure that every AWS.Region constructor we use in our composition will be tested for compliance.

Importing the Validation Module

We’ve written a validation! That’s all well and good, but validations are not very interesting unless they’re actually implemented. So, to put it to the test, we’re going to import our validation module into a composition that creates a simple VPC in the us-east-1 region.

composition

import Fugue.AWS as AWS
import Fugue.AWS.EC2 as EC2
import UsWest2OnlyValidation as .

#########################
# NETWORKS
#########################

my-vpc: EC2.Vpc.new {
  cidrBlock: "10.0.0.0/16",
  region: AWS.Us-east-1,
}

Testing the Validation

To test the validation, we need to compile the composition. You can either do that by running lwc or attempting a fugue run (or fugue update). We’re going to use lwc:

lwc UsEast1VPC.lw

And, we get this error:

lwc UsEast1VPC.lw
ludwig (validation error):
  "UsEast1VPC.lw" (line 13, column 11):
  Validations failed:

    13|   region: AWS.Us-east-1,
                  ^^^^^^^^^^^^^

    - Infrastructure is allowed in us-west-2 only.
      (from UsWest2OnlyValidation.usWest2Only)

Which is exactly what we want to see! Fugue has enforced our policy of not creating infrastructure outside us-west-2, just as we specified.

To prove this, we’ll change the VPC’s region to AWS.Us-west-2, in order to see the composition compile successfully. Here’s the revised composition:

composition

import Fugue.AWS as AWS
import Fugue.AWS.EC2 as EC2
import UsWest2OnlyValidation as .

#########################
# NETWORKS
#########################

my-vpc: EC2.Vpc.new {
  cidrBlock: "10.0.0.0/16",
  region: AWS.Us-west-2,
}

Once again, we’ll run lwc:

lwc UsEast1VPC.lw

And this time, we’ll see that there is no output, meaning the composition has compiled without error. Hooray!

Further Reading

This is just a simple example of how validations can be used, but the sky’s the limit. For a more complex demonstration, see our policy-as-code Github repo.