|
1 | 1 | # Styx |
2 | 2 |
|
3 | | -A configuration language that's actually pleasant to use. |
| 3 | +At least it's not YAML! |
| 4 | + |
| 5 | +## Styx the straightforward |
| 6 | + |
| 7 | +Imagine JSON |
| 8 | + |
| 9 | +```json |
| 10 | +{ |
| 11 | + "key": "value" |
| 12 | +} |
| 13 | +``` |
| 14 | + |
| 15 | +But you remove everything that's getting in the way: the double quotes, the |
| 16 | +the colon, even the comma: |
| 17 | + |
| 18 | +```styx |
| 19 | +{ |
| 20 | + key value |
| 21 | +} |
| 22 | +``` |
| 23 | + |
| 24 | +Of course you can have the comma back if you want to put everything in a single line: |
| 25 | + |
| 26 | +```styx |
| 27 | +{key value, koi tuvalu} |
| 28 | +``` |
| 29 | + |
| 30 | +Not far enough? Wanna get rid of the brackets? Okay, but only for the top-level object: |
4 | 31 |
|
5 | 32 | ```styx |
6 | | -// Schema declaration - enables validation, completion, hover |
7 | | -@ examples/server.schema.styx |
| 33 | +key value |
| 34 | +koi tuvalu |
| 35 | +``` |
| 36 | + |
| 37 | +What about arrays? They're called sequences and they use parentheses: |
| 38 | + |
| 39 | +```styx |
| 40 | +methods (GET POST PUT) |
| 41 | +``` |
| 42 | + |
| 43 | +They're always whitespace-separated, never comma-separated. |
| 44 | + |
| 45 | +## Styx the typed |
| 46 | + |
| 47 | +```styx |
| 48 | +name "John Doe" |
| 49 | +age 97 |
| 50 | +retired true |
| 51 | +``` |
| 52 | + |
| 53 | +Hey, which type are those values? Any type you want them to. |
8 | 54 |
|
9 | | -/// The server's display name (this is a doc comment) |
10 | | -name my-server |
11 | | -port 8080 |
12 | | -enabled @true |
| 55 | +Scalars are just text atoms, `97` is not any more a number |
| 56 | +than `https://example.org/` is. |
13 | 57 |
|
14 | | -// Nested objects |
15 | | -tls { |
16 | | - cert /etc/ssl/cert.pem |
17 | | - key /etc/ssl/key.pem |
| 58 | +Types matter at exactly two times: |
| 59 | + |
| 60 | +- Validation via [schemas](https://styx.bearcove.eu/spec/schema/) (which are also Styx documents) |
| 61 | +- Deserialization, in either flavor (dynamic or static typing) |
| 62 | + |
| 63 | +In dynamic typing flavor, your Styx document gets parsed into a tree, |
| 64 | +and then you get to request "field name as type string" — and if it can't |
| 65 | +be coerced into a string, you get an error at that point. |
| 66 | + |
| 67 | +In static typing flavor, you may for example deserialize to: |
| 68 | + |
| 69 | +```rust |
| 70 | +#[derive(Facet)] |
| 71 | +struct Does { |
| 72 | + name: String, |
| 73 | + age: number, |
| 74 | + retired: bool, |
18 | 75 | } |
| 76 | +``` |
19 | 77 |
|
20 | | -// Newlines or commas - your choice |
21 | | -logging {level info, format {timestamp @true, colors @true}} |
| 78 | +And then the type mapping is, well, what you'd expect. |
22 | 79 |
|
23 | | -// Sequences |
24 | | -allowed_methods (GET POST PUT DELETE) |
| 80 | +This solves the Norway problem: |
25 | 81 |
|
26 | | -// Tagged values for enums |
27 | | -status @ok |
28 | | -log_level @warn |
29 | | -maybe_value @some(42) |
| 82 | +```styx |
| 83 | +country no |
| 84 | +``` |
| 85 | + |
| 86 | +This `no` is not a boolean, not a string, not a number, it's everything, everywhere, |
| 87 | +all at once, until you _need_ it to be something. |
| 88 | + |
| 89 | +## Styx the nerd |
| 90 | + |
| 91 | +Sometimes a value isn't quite enough, and you want to tag it: |
| 92 | + |
| 93 | +```styx |
| 94 | +this (is an untagged list) |
| 95 | +that @special(list I hold dear) |
| 96 | +``` |
30 | 97 |
|
31 | | -// Complex structures |
32 | | -routes ( |
33 | | - @route {path /api/v1, handler api} |
34 | | - @route {path /health, handler health_check} |
| 98 | +Remember `()` are for sequences. They're not for grouping/precedence/calls. |
| 99 | + |
| 100 | +You can tag objects, too: |
| 101 | + |
| 102 | +```styx |
| 103 | +rule @path_prefix{ |
| 104 | + prefix /api |
| 105 | + route_to localhost:9000 // still no need to double-quote anything |
| 106 | + // oh yeah also comments just work |
| 107 | +} |
| 108 | +``` |
| 109 | + |
| 110 | +That's because Styx was designed to play nice with sum types, |
| 111 | +like Rust enums: |
| 112 | + |
| 113 | +```rust |
| 114 | +enum Alternatives { |
| 115 | + NoPayload |
| 116 | + TuplePayload(u32, u32) |
| 117 | + StructPayload { name: String } |
| 118 | +} |
| 119 | +``` |
| 120 | + |
| 121 | +And so, tags are a natural way to _select_ a variant: |
| 122 | + |
| 123 | +```styx |
| 124 | +alts ( |
| 125 | + @no_payload@ |
| 126 | + @tuple_payload(3, 7) |
| 127 | + @struct_payload{name Gisèle} |
35 | 128 | ) |
| 129 | +``` |
| 130 | + |
| 131 | +Did you notice the `@` at the end of `@no_payload@`? Not a typo: |
| 132 | +that's the unit value. It means "nothing", "none", kinda like "null" |
| 133 | +but a little superior. |
| 134 | + |
| 135 | +`@` is a value like any other: |
| 136 | + |
| 137 | +```styx |
| 138 | +sparse_seq (1 2 @ 8 9) |
| 139 | +``` |
| 140 | + |
| 141 | +And in fact, wanna know a secret? `@` is not even the canonical form |
| 142 | +of unit: `@@` is. |
| 143 | + |
| 144 | +An empty tag degenerates to `@`, and a tag without a paylod defaults to |
| 145 | +a payload of `@`. |
| 146 | + |
| 147 | +Therefore: |
| 148 | + |
| 149 | +```styx |
| 150 | +@ // tag=@, payload=@ (implied) |
| 151 | +@@ // tag=@, payload=@ |
| 152 | +@tag // tag=tag, payload=@ (implied) |
| 153 | +@tag@ // tag=tag, payload=@ |
| 154 | +@tag"must" // tag=tag, payload=must |
| 155 | +@tag() // tag=tag, payload=() aka empty sequence |
| 156 | +``` |
| 157 | + |
| 158 | +Importantly, there is NEVER ANY SPACE between a tag and its payload. |
| 159 | +Spaces separate seq elements or key-value pairs in object context: |
| 160 | + |
| 161 | +```styx |
| 162 | +// this is a key-value pair: |
| 163 | +@tag () // key(tag=tag, payload=@) value(tag=@, payload=()) |
| 164 | +
|
| 165 | +// this is a DIFFERENT key-value pair |
| 166 | +@tag() // key(tag=tag, payload=()) value(tag=@, payload=@) |
| 167 | +``` |
| 168 | + |
| 169 | +Does it confusing? Maybe. Little bit. |
| 170 | + |
| 171 | +## Styx the objective |
| 172 | + |
| 173 | +We've just seen this in the last gotcha: |
| 174 | + |
| 175 | +```styx |
| 176 | +@tag() // key(tag=tag, payload=()) value(tag=@, payload=@) |
| 177 | +``` |
| 178 | + |
| 179 | +Which, okay, `@tag()` is the entire key. But where's the value? |
| 180 | + |
| 181 | +It's omitted. It defaults to `@`: |
36 | 182 |
|
37 | | -// Heredocs for multi-line content |
38 | | -query <<SQL |
39 | | -SELECT * FROM users |
40 | | -WHERE active = true |
41 | | -SQL |
| 183 | +```styx |
| 184 | +key @ // explicitly set to unit |
| 185 | +koi // implicitly set to unit |
42 | 186 | ``` |
43 | 187 |
|
44 | | -## Features |
| 188 | +So, key-value pairs can be missing a value, and... they can also |
| 189 | +have more than one key. |
| 190 | + |
| 191 | +```styx |
| 192 | +fee fi foe fum |
| 193 | +// equivalent to |
| 194 | +fee {fi {foe fum}} |
| 195 | +``` |
| 196 | + |
| 197 | +And that's /it/ with the weirdness. Some unfamiliar bits, but hopefully |
| 198 | +not too many, which lets us... |
| 199 | + |
| 200 | +## Styx the schematic |
| 201 | + |
| 202 | +...define Styx schemas in Styx itself. |
| 203 | + |
| 204 | +```styx |
| 205 | +schema { |
| 206 | + /// The root structure of a schema file. |
| 207 | + @ @object{ |
| 208 | + /// Schema metadata (required). |
| 209 | + meta @Meta |
| 210 | + /// External schema imports (optional). |
| 211 | + imports @optional(@map(@string @string)) |
| 212 | + /// Type definitions: @ for document root, strings for named types. |
| 213 | + schema @map(@union(@string @unit) @Schema) |
| 214 | + } |
| 215 | + |
| 216 | + // etc. |
| 217 | +} |
| 218 | +``` |
| 219 | + |
| 220 | +Are those doc comments? Yes. Parsers are taught to keep them and attach them to |
| 221 | +the next element. This means your styx documents can be validated against a |
| 222 | +schema: |
| 223 | + |
| 224 | + * by a CLI, locally, in CI |
| 225 | + * by an LSP, in your code editor |
| 226 | + * honestly anytime for any reason |
| 227 | + |
| 228 | +And that your code editor (mine's [Zed](https://zed.dev)) can have the full |
| 229 | +code editing experience: autocomplete, documentation on hover, jump to definition |
| 230 | +(in schema), hover for field documentation, etc. |
| 231 | + |
| 232 | +It's... so nice. |
| 233 | + |
| 234 | +## Styx the one last thing |
| 235 | + |
| 236 | +Oh! Also, HEREDOCs: |
| 237 | + |
| 238 | +```styx |
| 239 | +examples ( |
| 240 | + { |
| 241 | + name hello.rs |
| 242 | + source <<SRC,rust |
| 243 | + fn main() { |
| 244 | + println!("Hello from Rust!") |
| 245 | + } |
| 246 | + SRC |
| 247 | + } |
| 248 | +) |
| 249 | +``` |
| 250 | + |
| 251 | +The `,rust` is just a hint which is used by your editor to inject syntax |
| 252 | +highlighting from the embedded language :) |
| 253 | + |
| 254 | +## Implementations |
| 255 | + |
| 256 | +There is a spec for parsing, schema validation, and error reporting, |
| 257 | +tracked with [Tracey](https://github.com/bearcove/tracey) and available |
| 258 | +on the [styx website](https://styx.bearcove.eu). |
| 259 | + |
| 260 | +The flagship implementation is, of course, the Rust one — across multiple |
| 261 | +crates like `facet-styx` and `serde_styx`, but not just. |
45 | 262 |
|
46 | | -- **Schema validation** with helpful error messages |
47 | | -- **Comments** that don't get lost |
48 | | -- **Flexible syntax** - use newlines or commas, your choice |
49 | | -- **Tags** for type annotations and enums (`@optional`, `@default`, custom types) |
50 | | -- **LSP support** with completions, hover, go-to-definition, and more |
| 263 | +There's a TypeScript implementation in the repository, and probably more |
| 264 | +to come. |
51 | 265 |
|
52 | 266 | ## Editor Support |
53 | 267 |
|
|
0 commit comments