Basic Syntax

.dog Files

In general, patterns are authored in plaintext UTF-8 files with the suffix of .dog.

Delimiters & Whitespace

First, Dogma does not require a semicolon (;) or any other statement-ending marker. Line delimeters are non-notable. Trailing commas are allowed in any comma-delimited sequence. Generally, whitespace is also non-notable and does not affect the definition of a pattern.

Comments

Single-line comments are supported using // and elide everything until the end of the line. Documentation comments may be applied to patterns using /// and should be marked up using AsciiDoc format.

File Structure

There are two primary top-level "statements" in the Dogma language:

  • use statements, which import other patterns into visible scope.

  • pattern definitions

The general structure of a given .dog file is as follows:

use ...
use ...
use ...

pattern ...
pattern ...

Directory Structure

When a directory is provided as a policy library, the directory will be traversed and all files with the extension of .dog will be parsed, compiled, and made available as evaluatable policies.

From the root library directory, the organizational structure of the directories is reflected in both the names of the policies, and the URLs through which they are exposed if using the swio serve.

For instance, if the directory ./policies/ is used as the root and contains the following structure:

policies/
  productization/
    build.dog
    promote.dog
  sre/
    deploy.dog

The productization/build.dog file will contain policies that end up in the productization::build:: package, while the sre/deploy.dog will result in policies existing in the package of sre::deploy::.

use statements

The use statement brings other packages into visible scope. The usage of use statements is not required, as all patterns are always addressable using their fully-qualified package-prefixed names. The use statement allows importing as a simple name (assuming no conflicts), or importing as a different name to avoid conflicts.

Currently only full patterns can be imported using use statements, not packages.

Simple use

The simplest use statement brings a pattern into visible scope for use by its simple name.

use list::all
use sre::deploy::allowed

Within a pattern defined in the same file, referencing these two imports is possible using purely their tail-end simple name of all and allowed.

The use …​ as …​ variant

In the event a policy author wants to use two different patterns that share a simple name, an as …​ suffix is allowed to rename the pattern in this file only.

use productization::build::allowed as build-allowed
use productization::promote::allowed as promote-allowed

These two patterns can now be used unambiguous as build-allowed and promote-allowed.

Patterns

Patterns define named patterns that ultimately end up being policies.

Patterns are defined using the pattern keyword, followed by the name of the pattern, followed by an equal sign (=) and then the definition of the pattern.

Patterns and functions can be quite flexibly named. They may include alphanumeric characters, underscores (_) and dashes (-). Functions, by convention, start with a capital letter, while pattern identifiers start with a lowercase letter.

Every pattern is built up from other patterns. It’s turtles all the way down, until you reach the primordial patterns.

Simple Primordial Patterns

The simplest pattern is a primordial pattern.

pattern all-the-things = anything
pattern a-string = string
pattern some-integer = integer
pattern true-or-false = boolean

In all four cases, these simple patterns really only define an alias to the right-hand-side of the pattern definition.

Simple Primordial value-based Patterns

One step beyond specifying that a pattern matches all integers, string or booleans is restricting which set of integers, strings and booleans it may match.

When the answer is "exactly this one string" or "this exact number", then using value-esque primordial patterns is useful.

pattern bob-and-only-bob = "bob"
pattern the-number-forty-two = 42

Object-shaped Patterns

Patterns that match object-shaped input values (useful for applying policy to a JSON object) are defined using { and } with field patterns within.

A field pattern includes a field name (without quotes, unlike JSON), a colon (:) and the field’s own pattern to match against the input field value.

If the field name has a suffix of ?, the field is considered optional. If an optional field is not present, the containing object-shaped pattern may continue to satisfy and result in a positive decision. If an option field is present, then it must match the specified target pattern.

A simple object pattern that matches any object that has at least a single field named version:

pattern versioned = {
  version: anything
}

The above specifies that to match, the input value must have a the version field, but that field can be anything; a string, a piece of chalk, a mustache, whatever.

Object patterns do not fail if additional fields are submitted in the input value.

For instance, the versioned pattern will succeed even if the input data is this JSON:

{
  "name": "seedwing-policy-server",
  "version": "8.2.0",
  "authors": [ "bob", "ulf", "jim", "jens" ]
}

Of course, field-level patterns can also specify more distinct patterns than simply anything. Additionally, they may specify as many fields as necessary. Since object-shaped patterns are patterns, nesting is fully supported.

pattern versioned = {
  version: {
    major: integer,
    minor: integer,
    patch?: integer,
  }
}

This pattern would match this input:

{
  "name": "seedwing-policy-server",
  "version": {
    "major": 8,
    "minor": 2,
    "patch": 0
  }
}

and this input

{
  "name": "seedwing-policy-server",
  "version": {
    "major": 8,
    "minor": 2
  }
}

but it would not match

{
  "name": "seedwing-policy-server",
  "version": {
    "major": 8,
    "minor": 2,
    "patch": "of course I patch my stuff"
  }
}

List-based Patterns

List-based patterns are dependent on the content and the sequence of items contained within the input value.

A list pattern is constructed using [ and ], with a sequence of patterns denoting which patterns each term should satisfy.

Just as with strings matching a specific sequence of characters, a list pattern matches a sequence of items, aligned to the terms within the pattern.

Lists may be somewhat counter-intuitive if you’re familiar with syntax from other languages

For instance, pattern list-of-numbers = [ integer ] actually only defines a pattern that matches a list with exactly a single integer value.

If an input value is expected to contain three strings, in a given order, the relevant pattern might look like:

pattern list-of-names = [ "bob", "ulf", "jim" ]

This would then match

[ "bob", "ulf", "jim" ]

But it would not match a permutation of that input:

[ "jim", "ulf", "bob"]

Lists as primary patterns may not represent a large amount of functionality, but they are useful when working with parameterized items, described below.

There are core language functions available to work with lists in a more comprehensive way.

Logical Expressions

Given that a pattern only worries about the bits of an input that it can decide upon, it’s useful to combine multiple patterns to each separately evaluate their subset of the input.

If we had two distinct patterns:

pattern named = {
  name: string,
}

pattern versioned = {
  version: {
    major: integer,
    minor: integer,
    patch?: integer,
  }
}

We can construct a pattern that ensures that both patterns are satisfied by the same input value:

pattern named-and-versions = named && versioned

Likewise, we can construct a pattern that could be satisfied by matching at least one of several distinct patterns.

Rewriting the versioned pattern, we could support an object-shaped version input, or a simple string:

pattern versioned = {
  version: string || {
    major: integer,
    minor: integer,
    patch?: integer,
  }
}

Short-circuiting applies to ||, as a success is a success. Short-circuiting does not apply to &&, so that all viable failures can be detected early, instead of piece-meal.

Expression Types

Some patterns need to match uncountable sets. This includes things such as "all numbers greater than 42". It would be impossible to construct a concrete set of all numbers greater than 42.

Expression patterns allow for defining patterns using basic arithmetic expressions.

The expression langauge may grow or shrink; we are still iterating.

Expression patterns are denoted by the $( prefix and the ) suffix.

The self keyword

Within an expression pattern, the self keyword refers to the input value.

pattern alpha-sofware = {
  version: {
    major: $(self < 1),
    minor: integer,
    patch?: integer,
  }
}

pattern patched-software = {
  version: {
    patch: $(self > 0)
  }
}

Traversals

A traversal looks not unlike dot-notation in object-oriented languages to navigate within an object. When combining patterns, traversals provide a terser way of specifying details for small portions of a larger pattern.

For instance, if we have a versioned pattern as before:

pattern versioned = {
  version: string || {
    major: integer,
    minor: integer,
    patch?: integer,
  }
}

We can use traversals to apply additional patterns when mixed with && to match all things that both match versioned and contain the optional patch field.

pattern patched = versioned && self.version.patch

The self keyword

Within a traversal, the self keyword once again refers to the input value. Navigation to deeper levels uses the dot (.) and nested field names. If traversing does not succeed, it is considered to fail matching. Traversals (and all patterns) are combinable with refinements described below.

Refinements

While the above simply tested for the existance of a given field, sometimes we want to refine the acceptability of a field.

The refinement construct allows applying additional patterns to the value at that point in the evaluation. Refinements are specified using parentheses as a postfix to any other pattern.

Within the parentheses, any pattern can be specified to further restrict viable values.

If we want to match versioned items where the major field has more constraints than simply integer, we could write combining pattern, without having to recopy the entire versioned pattern:

pattern version-nine = versioned && self.version.major(9)

This also demonstrates that the self.version.major is not only testing for existance, but after being evaluated, the input value under consideration is the result of having made the traversal.

Refinements are also useful when working with functions, described below.

Functions

Functions are another construct that effectively work as patterns. Unlike simpler patterns, the output of the function can be not only the identity (or failure), but can be a different transformed value.

Like traversals, the function construct takes the input value under consideration as an implicit argument, does whatever it wants to do, and produces a result, which roughly boils down to:

  • Identity: the same value that came in popped out the far side

  • Transform: the value that came in was transformed/replaced with a different value on the far side

  • None: the value that came in failed to produce the identity or a transformed value, thus the function fails to match.

Within the core library is, for instance, a Base64 function, which expects a string input, and if it can successfully decode the string as a base64 entity, produces the decoded octets as the output.

pattern base64encoded = Base64

This pattern would accept the following JSON as valid input:

"U2VlZHdpbmcgaXMgYXdlc29tZSE="

And the output would be octets underlying Seedwing is awesome!.

Functions can be refined using the parenthesis notation described above:

pattern base64-seedwing = Base64("Seedwing is awesome!")

This pattern will only accept the input of U2VlZHdpbmcgaXMgYXdlc29tZSE= and all other base64-encoded strings will fail.

Parameterized Patterns

So far all patterns have been standalone and independent, other than the components that comprise them.

Patterns may be written in a parameterized style, to allow specialization at the site of usage rather than at the point of definition.

Parameters are defined using < and > after the name of the pattern when defining it, and once again arguments are passed to patterns using the same notation when used.

An example:

pattern named<NAME> = {
  name: NAME
}

pattern named-bob-or-jim = named<"bob"> || named<"jim">

Parameters can be any pattern; they are not required to be value-esque patterns.

For instance, the logical || operator is actually syntactic sugar for lang::or<TERMS>.

This next two patterns are semantically and implementationally identical:

pattern sugared = something-borrowed || something-blue

pattern unsugared = lang::or<[ something-borrowed, something-blue ]>

Here we finally discover where lists (described above) become useful.

Dereferencing (a.k.a. Eager Evaluation)

Given that patterns are first-class constructs, passing them as parameters can sometimes be problematic.

Consider this pattern:

pattern people = lang::or<data::from<"people.json">>

And this people.json:

[
  "bob",
  "jim"
]

Initially, we think this might be equivalent to

pattern people = lang::or[ "bob", "jim" ]>

But alas, it is not. Instead of receiving the array of people that data::from<…​> provides, it receives the actual pattern data::from<…​>. That pattern has not yet been evaluated to provide the underlying data. The lang::or<…​> function expects a list-shaped pattern with terms to be or'd together. Instead, it’s receiving the function pattern underlying data::from<…​>.

To resolve a pattern against the input prior to passing it as a parameter, the dereference/eager-evaluation operator is used: *. By placing a * as a prefix to a pattern, it will be evaluated, and the resulting value is then treated as a pattern and passed further.

The pattern that behaves the way one might expect looks like:

pattern people = lang::or<*data::from<"people.json">>

Now indeed the lang::or will received a list-ish pattern full of string-ish patterns (the values bob and jim) and perform as expected.