aml

package module
v0.0.0-...-c922539 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jan 23, 2024 License: Apache-2.0 Imports: 15 Imported by: 14

README

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.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var ErrNoOutput = errors.New("value did not produce any output")

Functions

func Format

func Format(data []byte) ([]byte, error)

func Marshal

func Marshal(v any, opts ...EncoderOption) ([]byte, error)

func NewValueReader

func NewValueReader(value value.Value) io.Reader

func Open

func Open(name string) (io.ReadCloser, error)

func ReadFile

func ReadFile(name string) ([]byte, error)

func Unmarshal

func Unmarshal(data []byte, v any, opts ...DecoderOption) error

func UnmarshalFile

func UnmarshalFile(name string, out any) error

Types

type Decoder

type Decoder struct {
	// contains filtered or unexported fields
}

func NewDecoder

func NewDecoder(input io.Reader, opts ...DecoderOption) *Decoder

func (*Decoder) Decode

func (d *Decoder) Decode(out any) error

type DecoderOption

type DecoderOption struct {
	PositionalArgs   []any
	Args             map[string]any
	Profiles         []string
	SourceName       string
	SchemaSourceName string
	Schema           io.Reader
	SchemaValue      value.Value
	Globals          map[string]any
	GlobalsLookup    eval.ScopeFunc
	Context          context.Context
}

func (DecoderOption) Complete

func (o DecoderOption) Complete() DecoderOption

type DecoderOptions

type DecoderOptions []DecoderOption

func (DecoderOptions) Merge

func (o DecoderOptions) Merge() (result DecoderOption)

type Encoder

type Encoder struct {
	// contains filtered or unexported fields
}

func NewEncoder

func NewEncoder(output io.Writer, opts ...EncoderOption) *Encoder

func (*Encoder) Encode

func (d *Encoder) Encode(out any) error

type EncoderOption

type EncoderOption struct {
}

func (EncoderOption) Complete

func (o EncoderOption) Complete() EncoderOption

type EncoderOptions

type EncoderOptions []EncoderOption

func (EncoderOptions) Merge

func (o EncoderOptions) Merge() (result EncoderOption)

Directories

Path Synopsis
cli module
legacy module
pkg
ast
scanner
Package scanner implements a scanner for CUE source text.
Package scanner implements a scanner for CUE source text.
std
token
Package token defines constants representing the lexical tokens of the Go programming language and basic operations on tokens (printing, predicates).
Package token defines constants representing the lexical tokens of the Go programming language and basic operations on tokens (printing, predicates).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL