aml icon indicating copy to clipboard operation
aml copied to clipboard

Acorn Markup Language

Acorn Markup Language

Acorn Markup Language (AML) is the markup language that is used to write Acornfiles in Acorn. The language is a configuration file format to describe json data using expressions, functions, and has a corresponding schema language to validate data.

JSON Superset

AML is a superset of JSON. This means that any valid JSON is also valid AML. All AML after being evaluated produces valid JSON.

Syntax simplifications

// Comments are allowed in AML
// Notice this document doesn't start with a curly brace. The outer '{' and '}' are optional and will
// still yield a valid JSON object.

// Keys that start with a letter and only have letters, numbers, and underscores can be written
// without quotes.
simpleKey: "value"

// Object that have a single key can be written without curly braces.
simpleObject: singleKey: "value"

// Commas at that end of the line are optional. Also a trailing comma is allowed.
trailingComma: "value",

The above AML will evaluate to the following JSON:

{
  "simpleKey": "value",
  "simpleObject": {
    "singleKey": "value"
  },
  "trailingComma": "value"
}

Native Data Types

The native data type in AML are the exact same as JSON, which are as below

aNumber: 1
aString: "string"
aBoolean: true
aNull: null
anArray: [1, "string", true, null]
anObject: {
  "key": "value"
}

Strings

multiline: """
    Multiline string in AML are written using triple quotes. The body of the string must still be 
    escaped like a normal string, but the triple quotes allow for newlines and quotes to be written.
    The following " does not need to be escaped, but \n \t and \r will still be treated as newlinee,
    tab, and carriage return respectively.
    
    If all of the lines of the multiline string are indented with the same amount of whitespace the
    leading whitespace will be trimmed from the evaluated string.
    """
rawString: `This is a raw string. The body of the string is not escaped, but the backtick must be`

rawMultiLine: ```
This is a raw multiline string. The body of the string is not escaped,
but the backtick must be.```

Numbers

// Integer
aInteger: 1

// This is the same 1000000, the _ is just a separator that is ignored. Numbers can have as many _ as you
// want, but can not start or end with an underscore.
aIntegerWithUnderscore: 1_000_000

// A suffix of K, M, G, T, or P can be added implying the number is multiplied by 1000, 1000000, 1000000000, etc
oneMillion: 1M

// A suffix of Ki, Mi, Gi, Ti, or Pi can be added implying the number is multiplied by 1024, 1048576, 1073741824, etc
megabyte: 1Mi

// Float
aFloat: 1.0

// Scientific notation, following the same format as JSON
aFloatWithE: 1.0e10

Multiple Definitions

anObject: {
    key: "value"
}

// Objects can be defined multiple time as long as the keys are new or the values for existing keys are the
// same as previously defined
anObject: {
    aNewKey: "value"
    key: "value"
}

anObject: thirdKey: "value"

aNumber: 4

The above AML will produce the following JSON.

{
  "anObject": {
    "aNewKey": "value",
    "key": "value",
    "thirdKey": "value"
  },
  "aNumber": 4
}

Expressions

Math

addition: 1 + 2
subtraction: 1 - 2
multiplication: 1 * 2
division: 1 / 2
parens: (1 + 2) * 3

Comparisons

lessThan: 1 < 2 
lessThanEquals: 1 <= 2
greaterThan: 1 > 2
greaterThanEquals: 1 >= 2
equals: 1 == 2
notEquals: 1 != 2
regexpMatch: "string" =~ "str.*"
regexpNotMatch: "string" !~ "str.*"

References, Lookup

value: {
    nested: 1
}
reference: value.nested

Object Merge

The + operator can be used to recursively merge objects where the non-object values from the right object will the value from the left.

{
    a: 1
    b: 2
} + {
    b: 3
    c: 4
    d: e: 5
}

The above will evaluate to

{
  "a": 1,
  "b": 3,
  "c": 4,
  "d": {
    "e": 5
  }
}

Index, Slice

array: [1, 2, 3, 4, 5]
index: array[0]
// Slices are inclusive of the start index and exclusive of the end index
slice: array[0:2]
tail: array[2:]
head: array[:2]

String Interpolation

value: 1
// Interpolation in a string starts with \( and ends with ) and can contain any expression.
output: "the value is \(value)"

// Interpolation can also be used in keys
"key\(value)": "dynamic key"

The above will produce the following JSON.

{
  "value": 1,
  "output": "the value is 1",
  "key1": "dynamic key"
}

Conditions, If

value: 1

if value == 2 {
    output: "value is 2"
} else if value == 3 {
    output: "value is 3"
} else {
    output: "value is not 2 or 3"
}

The above will produce the following JSON.

{
  "value": 1,
  "output": "value is not 2 or 3"
}

Loops, For

for i, v in ["a", "b", "c"] {
    // This key is using string interpolation which is described above
    "key\(i)": v
}

for v in ["a", "b", "c"] {
    // This key is using string interpolation which is described above
    "key\(v)": v
}

The above will produce the following JSON.

{
  "key0": "a",
  "key1": "b",
  "key2": "c",
  "keya": "a",
  "keyb": "b",
  "keyc": "c"
}

List comprehension

list: [1, 2, 3, 4, 5]

listOfInts: [for i in list if i > 2 {
    i * 2
}]

listOfObjects: [for i in list if i > 2 {
    "key\(i)": i * 2
}]

The above will produce the following JSON

{
  "list": [1, 2, 3, 4, 5],
  "listOfInts": [6, 8, 10],
  "listOfObjects": [
    {"key3": 6},
    {"key4": 8},
    {"key5": 10}
  ]
}

Let

// Let is used to define a variable that can be used in the current scope but is not in the output data.
let x: 1
y: x

The above will produce the following JSON

{
  "y": 1
}

Embedding

subObject: {
    a: 1
}

parentObject: {
    // This object will be embedded into the parent object allowing composition
    subObject
    
    b: 2
}

The above will produces the following JSON

{
   "subObject": {
        "a": 1
    },
    "parentObject": {
        "a": 1,
        "b": 2
    }
}

Functions

// Functions are defined using the function keyword
myAppend: function {
    // Args are defined using the args keyword
    args: {
        // Args are defined using the name of the arg and the type of the arg. The type of the arg
        // follows the schema syntax described later in this document
        head: string
        tail: string
    }
    
    someVariable: "some value"
    
    // The return of the function should be defined using the return key
    return: args.head + args.tail + someVariable
}

// The arguments will be applied by the order they are assigned
callByPosition: myAppend("head", "tail")

// The arguments can also be applied by name
callByName: myAppend(tail: "tail", head: "head")

The above will produce the following JSON

{
  "callByPosition": "headtailsome value",
  "callByName": "headtailsome value"
}

Evaluation Args and Profiles

When evaluating AML using the go library or CLI you can pass in args and profiles. Args are used to pass in parameterized data and profiles are used to provide alternative set of default values.

// Args are defined using the args keyword at the top level of the document.  They can not be nested
// in any scopes.
args: {
    // A name you want outputted
    someName: "default"
}

profiles: {
    one: {
        someName: "Bob"
    }
    two: {
        someName: "Alice"
    }
}

theName: args.someName

Running the above AML with the following command

aml eval file.acorn --someName John

Will produce the following JSON

{
  "theName": "John"
}

The following command

aml eval file.acorn --profile one

Will produce the following JSON

{
  "theName": "Bob"
}

The following command

aml eval file.acorn --profile two

Will produce the following JSON

{
  "theName": "Alice"
}

Args and profiles in help text in CLI

The following command

aml eval file.acorn --help

Will produce the following output

Usage of file.acorn:
      --profile strings   Available profiles (one, two)
      --someName string   An name you want outputted

Schema

AML defines a full schema language used to validate the structure of the data. The schema language is designed to be written in a style that is similar to the data it is validating.

Schema can be validated using the go library or CLI. The CLI can be used as follows.

aml eval --schema-file schema.acorn file.acorn

Simple Data Fields

// A key call requiredString must in the data and must be a string
requiredString: string
// A key call optionalString may or may not be in the data and must be a string if it is
optionalString?: string
// A key call requiredNumber must in the data and must be a number
requiredNumber: number
// A key call optionalNumber may or may not be in the data and must be a number if it is
optionalNumber?: number
// A key call requiredBool must in the data and must be a bool
requiredBool: bool
// A key call optionalBool may or may not be in the data and must be a bool if it is
optionalBool?: bool

Objects

// An object is defined looking like a regular object
someObject: {
    fieldOne: string
    fieldTwo: number
    
    // Dynamic keys can be matched using regular expressions.  The regular expression will only be checked
    // if no other required or optional keys match first
    match "field.*": string
}

Arrays

arrayOfStrings: [string]
arrayOfNumbers: [number]
arrayOfObjects: [{key: "value"}]

// The below is interpreted as a key someArray is required, must be an array and the
// values of the array must match the schema `string` or `{key: "value"}`
mixedArrayOfStringAndObject: [string, {key: "value"}]

Default values

// The following schema means that this key is required and must be a string, but if it is not in the
// data the default value of "hi" will be used.
defaultedString: "hi"
// This can also be written using the default keyword, but is unnecessary for in most situations, but
// may more clearly describe your intent.
defaultedStringWithKeyword: default "hi"

Conditions and Expressions

Conditions >, >=, <, <=, ==, !=, =~, !~ can be used in the schema to validate the data. The condition expressions must be written as the type is on the left and the condition is on the right.

aPositionNumber: number > 0
anExplicitConstant: string == "value"
aRegex: string =~ "str.*"

Complex expressions can be written by using the operators && and || and using parens to group expressions.

aNumberRange: number > 0 && number < 10 || default 1

Types (pseudo)

The following pattern can be used to define reusable types. Custom types are not a first class object in the language but instead objects with schema fields can be reused.

// By convention put types in a let field named types. This ensures the objects
// are not included the evaluated output.
let types: {
    // Types by convention start with an uppercase letter following PascalCase
    StringItem: {
        item: string
    }
    NumberItem: {
        item: number
    }
    
    Item: StringItem || NumberItem
}

items: [types.Item]

Examples

As this is the language used by Acorn, Acornfiles are a great place to look for examples of AML syntax. Try this GitHub search

For an example of schema, you can refer the schema file used to validate Acornfile which is quite a complete example of most all schema features.

Go API

The go API is documented at pkg.go.dev. The main supported API is the github.com/acorn-io/aml package. Every thing else in pkg/ is considered internal and subject to change. It really should be marked named internal, but I personally feel internal is just mean.

License

It's Apache 2.0. See LICENSE.