Skip to content

gilbert/zaml

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

npm Build Status

Zaml – The Final Form of Configuration Files

JSON is tedious to type for humans. YAML is error-prone and hard to parse. TOML is verbose for nested data structures.

Enter Zaml.

Zaml is the final form of configuration files. It's a zero-dependency, type-checking machine that points out your errors as graceful as a dove. It takes the pain out of your gain.

Never again deal with the boilerplate of validating config data structures. Make config easier for you and your users.

Zaml isn't even an ancronym. That's how good it is.

Install

npm install zaml

or

yarn add zaml

or

Check out the online editor!

Table of Contents

Introduction

Zaml's syntax is clean, effective, and not obsessed with your shift key:

import {parse} from 'zaml'

var schema = '{ fileRoots: { dev:list, prod:list } }'

var zamlContent = `

  # This is Zaml!
  # You'd probably load this from a file instead.

  fileRoots {
    dev {
      $HOME/dev/services
      $HOME/dev/states
    }
    prod {
      $HOME/prod/services-2
      $HOME/prod/states
    }
  }
`

var result = parse(zamlContent, schema, { vars: process.env })
console.log("Got result:", result)

(You can run this example in your browser)

Parsing the above will result in this data structure:

{
  "fileRoots": {
    "dev": [
      "/home/alice/dev/services",
      "/home/alice/dev/states"
    ],
    "prod": [
      "/home/alice/prod/services-2",
      "/home/alice/prod/states"
    ]
  }
}

No quotes, no commas, no colons. Only what you need and nothing else.

Your users also get nice, accurate error messages if they make a mistake writing their config. It's a win-win!

More Examples

See the examples/ folder for more complete syntax & schema examples like the above.

Features

Here are Zaml's features, each with an example use, from simplest to most complex.

Comments

A comment is any line that begins with #

# I am a comment
# title This is a comment
title This is # not a comment

bool

A bool accepts true or false

# if your schema is {autoCleanup:bool}

autoCleanup true

#=> { "autoCleanup": true }

View this example in the online editor

num

A num accepts a single numerical value.

# if your schema is {port:num}

port 3000

#=> { "port": 3000 }

View this example in the online editor

str

A str is the default type of any unspecified schema key. It accepts any character until it reaches a newline.

If you need multiline strings, you can use triple quotes ("""). Zaml will also un-indent your string automatically, just like Ruby squiggly heredoc.

# if your schema is {title,description} OR {title:str,description} OR etc.

title You, Yourself, and U
description """
  A story
    ...
  about your mirror.
"""

#=> { "title": "You, Yourself, and U", "description": "A story\n  ...\nabout your mirror." }

View this example in the online editor

enum

An enum is a str with restricted options. It's useful for a "choose one" situation.

# if your schema is { paymentMode: enum(test,live) }

paymentMode test

#=> { "paymentMode": "test" }

Naturally, if the user provides a value outside the schema, Zaml will reject it and report a readable error message.

View this example in the online editor

kv

A kv is a set of key-value pairs. It requires a block.

# if your schema is {redirects:kv}

redirects {
  /contact       /contact-us
  /profile/:user /u/:user
}

#=> { "redirects": { "/contact": "/contact-us", "/profile/:user": "/u/:user" } }

Please note Zaml is not indentation sensitive.

View this example in the online editor

hash block

A {} block is a specified inner schema. It translates to a hash that only allows your specified keys.

# if your schema is { project: { title, private:bool } }

project {
  title My Sweet App
  private true
}

#=> { "project": { "title": "My Sweet App", "private": true } }

View this example in the online editor

You can also enhance basic types with blocks. This lets you specify cleaner config:

# if your schema is { page|multi: str {hide:bool} }

page index.html
page wip.html {
  hide true
}

#=> { "page": [
#     ["index.html", {}],
#     ["index.html", { "hide": true }],
#   ] }

View this example in the online editor

list

A list is a sequence of values. A user can write lists either inline or with a block (but not both).

# if your schema is {tags:list} OR {tags:list(str)}

# Inline example
tags library, npm, with spaces, js

# Block example
tags {
  library
  npm
  with spaces
  js
}

#=> { "tags": ["library", "npm", "with spaces", "js"] }

View this example in the online editor

You can specify the type of items in your list by specifying it in parethesis. When no item type is specified, it is defaulted to str, resulting in list(str).

# if your schema is { fav_nums:list(num) }
fav_nums 10, 20

#=> { "fav-nums": [10, 20] }

View this example in the online editor

The list item type may also be enhanced with a block. Note that you must specify an item type when you do!

# if your schema is { users:list(str { admin:bool }) }

users {
  andy
  beth {
    admin true
  }
  carl
}

#=> { "users": [["andy", {}], ["beth", {admin: true}], ["carl", {}]] }

View this example in the online editor

Note how a block changes the shape of the above parsed result. This allows you to use destructuring for each list item:

var result = parse(source, schema)
for (let [user, options] of result.users) {
  //
  // `options` will be {admin: true} for beth,
  //  and undefined for andy & carl
  //
}

inline-list

NOTE: THIS FEATURE IS NOT IMPLEMENTED YET

An inline list a list that can only be written inline. This is useful when you want to extend your list with a block:

# if your schema is { env|multi: inline-list{require|multi} }

env development, test {
  require lib-1
  require lib-2
}
env production {
  require lib-3
}

# => { "env": [
#      [["development", "test"], { "require": ["lib-1","lib-2"] }],
#      [["production"], { "require": ["lib-3"] }]
#    ]}

key|req

Appending the |req attribute to a key will make that key requried.

# if your schema is { access_token|req:str }

access_token abc123

Open that in your browser and see what happens when you remove the access_token line.

If you specify a |req within a basic-type hash block, it will make that block required.

# if your schema is { table: str{ id|req: enum(INT,VARCHAR) } }

# This is invalid! It requires a block
# table users

# This works
table users {
  id INT
}

View this example in the online editor

key|multi

Appending the |multi attribute to a key allows your users to specify it more than once.

# if your schema is {project|multi:{title,type}}

project {
  title A
}
project {
  title B
  type personal
}

#=> { "project": [{ "title": "A" }, { "title": "B", "type": "personal" }] }

View this example in the online editor

|multi will also ensure your key is always present, even if the user does not provide any.

# if your schema is {project|multi:{title,type}}

# (intentionally left blank)

#=> { "project": [] }

View this example in the online editor

tuple

A tuple captures two or more values for a given key, separated by commas. You can specify one in the schema using parenthesis:

# if your schema is {redirect:(num,str,str)}

redirect 302, /old, /new

#=> { "redirect": [302, "old", "new"] }

View this example in the online editor

Please note that tuples may only contain basic types (str, num, bool, and enum). However, you're free to mix tuples with other features:

# if your schema is {redirect|multi:(num,str,str){enableAt}}

redirect 301, /x, /y

redirect 302, /old, /new {
  enableAt 2020-10-10
}

#=> { "redirect": [[301, "x", "y"], [302, "/old", "/new", { "enableAt": "2020-10-10" }]] }

View this example in the online editor

array block

A [] block is an array of items from a specified schema. It translates to an array of key-value tuples.

# if your schema is {sidebar:[header,link:(str,str)]}

sidebar {
  header Site
  link Home /
  link About Us, /about

  header Account
  link Settings, /account/settings
}

#=> { "sidebar": [
#       ["header", "Site"], ["link", ["Home", "/"]], ["link", ["About Us", "/about"]],
#       ["header", "Account"], ["link", ["Settings", "/account/settings"]]
#     ] }

Note how a block changes the shape of the above parsed result. This allows you to use destructuring for each item:

View this example in the online editor

var result = parse(source, schema)
for (let [type, value] of result.sidebar) {
  //
  // `type` will be "header" or "link",
  //  whereas `value` will be a string (for header) or an array of size 2 (for link).
  //
}

JavaScript API

parse

The primary way you interact with Zaml is through its parse function. This is how you convert Zaml source to a JavaScript data structure.

parse takes two, maybe three arguments:

  1. The Zaml source code (as a string)
  2. A schema to parse with (as a string)
  3. Options to enable extra features (optional parameter)

Here's a full example that includes reading from the file system. Assuming you have the following my-config.zaml:

host www.example.com
port 443
disallow /admin
disallow /dashboard

You can parse the above with this node.js code:

var fs = require('fs')
var zaml = require('zaml')

var source = fs.readFileSync(__dirname + '/my-config.zaml')
var result = zaml.parse(source, 'host,port:num,disallow|multi')
result
//=> { "host": "www.example.com", "port": 443, "disallow": ["/admin", "/dashboard"] }

Parse Options

parseOptions, the third parameter to parse(), has the following options:

vars

Type: Record<string,string>

Each key-value in the provided vars object will be made available for users to interpolate using the $ operator. Example:

let source = `
  title Welcome to $APP_NAME
`
zaml.parse(source, 'title:str', {
  vars: { APP_NAME: 'My App' }
})
//=> { "title": "Welcome to My App" }

Note that you can easily use this feature to provide the user access to their own environment variables in a node.js environment:

zaml.parse(source, 'title:str', {
  vars: process.env
})

failOnUndefinedVars

If vars is set, then setting failOnUndefinedVars to true will ensure users cannot use variables that are not defined. This is useful if you want to e.g. ensure a key is always defined:

let source = `
  title Welcome to $iDontExist
`
//
// This will throw an error!
//
zaml.parse(source, 'title:str', {
  vars: { anyOtherKey: '' },
  failOnUndefinedVars: true,
})

caseInsensitiveKeys

Setting this to true will allow users to write their config keys in a case-insensitive manner.

caseInsensitiveEnums

Setting this to true will allow users to write enum values in any case.

Spec

Zaml has not yet reached 1.0, so there is no spec as of yet. However, here's a rough ABNF grammar for the lexer:

line = statement | comment
statement = key rest ["{\n" *statement "}"] "\n"

key = string-with-no-whitespace
rest = string-with-no-newlines

comment = "#" rest ("\n" | EOF)

After lexing, the parser uses the schema to determine how to parse the rest and *statement for each statement.

Roadmap

  • New regular expression type
  • Allow inline kv?
  • Allow blocks on kv
  • New json type for arbitrarily-nested json-like data?
  • Multiline strings? text type?
  • Split num into int and float?
  • Pluggable validation?
  • Default values for tuple types?
  • Command line interface?

Regarding the online editor:

  • Get code button
  • Fancy explanations of schema on hover

Contributing

Interested in contributing? There are several ways you can help:

  • Open or discuss an issue for an item on the roadmap
  • Implement Zaml in another programming language
  • Report any bugs you come across
  • Report a behavior that you think should be bug
  • Help start a spec!

Developing

npm install
npm start # In a separate terminal
npm test