At least it's not YAML!
Imagine JSON
{
"key": "value"
}But you remove everything that's getting in the way: the double quotes, the colon, even the comma:
{
key value
}
Of course you can have the comma back if you want to put everything in a single line:
{key value, koi tuvalu}
Not far enough? Wanna get rid of the brackets? Okay, but only for the top-level object:
key value
koi tuvalu
What about arrays? They're called sequences and they use parentheses:
methods (GET POST PUT)
They're always whitespace-separated, never comma-separated.
name "John Doe"
age 97
retired true
Hey, which type are those values? Any type you want them to.
Scalars are just text atoms, 97 is not any more a number
than https://example.org/ is.
Types matter at exactly two times:
- Validation via schemas (which are also Styx documents)
- Deserialization, in either flavor (dynamic or static typing)
In dynamic typing flavor, your Styx document gets parsed into a tree, and then you get to request "field name as type string" — and if it can't be coerced into a string, you get an error at that point.
In static typing flavor, you may for example deserialize to:
#[derive(Facet)]
struct Does {
name: String,
age: number,
retired: bool,
}And then the type mapping is, well, what you'd expect.
This solves the Norway problem:
country no
This no is not a boolean, not a string, not a number, it's everything, everywhere,
all at once, until you need it to be something.
Sometimes a value isn't quite enough, and you want to tag it:
this (is an untagged list)
that @special(list I hold dear)
Remember () are for sequences. They're not for grouping/precedence/calls.
You can tag objects, too:
rule @path_prefix{
prefix /api
route_to localhost:9000 // still no need to double-quote anything
// oh yeah also comments just work
}
That's because Styx was designed to play nice with sum types, like Rust enums:
enum Alternatives {
NoPayload,
TuplePayload(u32, u32),
StructPayload { name: String },
}And so, tags are a natural way to select a variant:
alts (
@no_payload@
@tuple_payload(3 7)
@struct_payload{name Gisèle}
)
Did you notice the @ at the end of @no_payload@? Not a typo:
that's the unit value. It means "nothing", "none", kinda like "null"
but a little superior.
@ is a value like any other:
sparse_seq (1 2 @ 8 9)
And in fact, wanna know a secret? @ is not even the canonical form
of unit: @@ is.
An empty tag degenerates to @, and a tag without a paylod defaults to
a payload of @.
Therefore:
@ // tag=@, payload=@ (implied)
@@ // tag=@, payload=@
@tag // tag=tag, payload=@ (implied)
@tag@ // tag=tag, payload=@
@tag"must" // tag=tag, payload=must
@tag() // tag=tag, payload=() aka empty sequence
Importantly, there is NEVER ANY SPACE between a tag and its payload. Spaces separate seq elements or key-value pairs in object context:
// this is a key-value pair:
@tag () // key(tag=tag, payload=@) value(tag=@, payload=())
// this is a DIFFERENT key-value pair
@tag() // key(tag=tag, payload=()) value(tag=@, payload=@)
Is it confusing? Maybe. Little bit.
We've just seen this in the last gotcha:
@tag() // key(tag=tag, payload=()) value(tag=@, payload=@)
Which, okay, @tag() is the entire key. But where's the value?
It's omitted. It defaults to @:
key @ // explicitly set to unit
koi // implicitly set to unit
So, key-value pairs can be missing a value. And dotted keys create nested structure:
server.host localhost
// equivalent to
server {host localhost}
And object attribute syntax provides a compact way to build objects:
{
web domain>example.org port>9000
api domain>api.example.org port>9001
}
And that's /it/ with the weirdness. (Don't worry, there are comprehensive specifications and test suites).
Some unfamiliar bits, but hopefully not too many, which lets us...
...define Styx schemas in Styx itself.
schema {
/// The root structure of a schema file.
@ @object{
/// Schema metadata (required).
meta @Meta
/// External schema imports (optional).
imports @optional(@map(@string @string))
/// Type definitions: @ for document root, strings for named types.
schema @map(@union(@string @unit) @Schema)
}
// etc.
}
Are those doc comments? Yes. Parsers are taught to keep them and attach them to the next element. This means your styx documents can be validated against a schema:
- by a CLI, locally, in CI
- by an LSP, in your code editor
- honestly anytime for any reason
And that your code editor (mine's Zed) can have the full code editing experience: autocomplete, documentation on hover, jump to definition (in schema), hover for field documentation, etc.
It's... so nice.
Oh! Also, HEREDOCs:
examples (
{
name hello.rs
source <<SRC,rust
fn main() {
println!("Hello from Rust!")
}
SRC
}
)
The ,rust is just a hint which is used by your editor to inject syntax
highlighting from the embedded language :)
There is a spec for parsing, schema validation, and error reporting, tracked with Tracey and available on the styx website.
The flagship implementation is, of course, the Rust one — across multiple
crates like facet-styx and serde_styx, but not just.
There's a TypeScript implementation in the repository, and probably more to come.
Styx has first-class support for Zed with syntax highlighting, LSP integration, and more.
See styx.bearcove.eu for full documentation.
Thanks to all individual sponsors:
...along with corporate sponsors:
CI runs on Depot runners.
MIT OR Apache-2.0
Thanks to all individual sponsors:
...along with corporate sponsors:
...without whom this work could not exist.
Licensed under either of:
- Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)
at your option.