shape

How to use the string DSL#

Goal: express a shape as a compact string, or expand a whole JSON structure whose string leaves are expressions. Useful for config files and serialized schemas.

expr—parse one expression#

expr(source) turns a string into a shape node.

TS

const { Shape, expr } = require('shape')

const shape = Shape(expr('String.Min(2).Max(10)'))
shape('hi')     // OK
shape('h')
// throws: Value "h" for property "" must be a minimum length of 2 (was 1).

Go

n := shape.MustExpr("String.Min(2).Max(10)")
s, _ := shape.Shape(n)

Grammar#

build—expand a JSON structure#

build(value) walks a JSON value and replaces every string leaf with the result of expr, then compiles the whole thing.

TS

const { build } = require('shape')

const shape = build({
  name: 'Min(1,String)',
  age:  'Min(0,Number)',
  tags: ['String'],
})

shape({ name: 'a', age: 3, tags: ['x'] })   // OK

Go

s, _ := shape.Build(map[string]any{
    "name": "Min(1,String)",
    "age":  "Min(0,Number)",
    "tags": []any{"String"},
})

Non-string leaves (numbers, booleans, null) are kept as literal defaults. A key that is exactly $$ is a value expression, applied to the object that holds it; the $$0, $$1, ... keys beside it are the shapes that expression takes as arguments. The example of a key expression is a value, so a string there is that string rather than an expression.

Rust

use shape::{build, Schema, Value};

let spec = build(&Value::from(serde_json::json!({
    "name": "Min(1,String)",
    "tags": ["String"],
})))
.expect("reads");
let s = Schema::new(spec);

Round-tripping a compiled shape#

shape.json() (TS) / s.JSON() (Go) / s.json() (Rust) writes a compiled shape back as the JSON build reads, so a shape survives being stored or sent. See Serialize a shape.

shape.stringify() (TS) / s.String() (Go) renders a shape in a DSL-ish form for logging, which is not read back.

See also#