synit-manual/src/guide/working-with-schemas.md

256 lines
7.5 KiB
Markdown

# Working with schemas
- [Preserves schema specification](https://preserves.dev/preserves-schema.html)
- [Index of Preserves schema tools](https://preserves.dev/doc/schema-tools.html)
[preserves-schemac]: https://preserves.dev/doc/preserves-schemac.html
## Schema source code: *.prs files
Preserves schemas are written in a syntax that (ab)uses Preserves text syntax as a kind of
S-expression. Schema source code looks like this:
```preserves-schema
version 1 .
Present = <Present @username string> .
Says = <Says @who string @what string> .
UserStatus = <Status @username string @status Status> .
Status = =here / <away @since TimeStamp> .
TimeStamp = string .
```
Conventionally, schema source code is stored in `*.prs` files. In this example, the source code
above is placed in `simpleChatProtocol.prs`.
## Compiling source code to metaschema instances: *.prb files
Many of the code generator tools for Preserves schemas require not source code, but instances
of the [Preserves
metaschema](https://preserves.dev/preserves-schema.html#appendix-metaschema).
To compile schema source code to metaschema instances, use
[preserves-schemac][]:
```shell
yarn global add @preserves/schema
preserves-schemac .:simpleChatProtocol.prs > simpleChatProtocol.prb
```
Binary-syntax metaschema instances are conventionally stored in `*.prb` files.
If you have a whole directory tree of `*.prs` files, you can supply just "`.`" without the
"`:`"-prefixed fileglob part. See the [preserves-schemac documentation][preserves-schemac].
Converting the `simpleChatProtocol.prb` file to Preserves text syntax lets us read the
metaschema instance corresponding to the source code:
```shell
cat simpleChatProtocol.prb | preserves-tool convert
```
The result:
```preserves
<bundle {
[
simpleChatProtocol
]: <schema {
definitions: {
Present: <rec <lit Present> <tuple [
<named username <atom String>>
]>>
Says: <rec <lit Says> <tuple [
<named who <atom String>>
<named what <atom String>>
]>>
Status: <or [
[
"here"
<lit here>
]
[
"away"
<rec <lit away> <tuple [
<named since <ref [] TimeStamp>>
]>>
]
]>
TimeStamp: <atom String>
UserStatus: <rec <lit Status> <tuple [
<named username <atom String>>
<named status <ref [] Status>>
]>>
}
embeddedType: #f
version: 1
}>
}>
```
## Generating support code from metaschema instances
Support exists for working with schemas in many languages, including Python, Rust, TypeScript,
Racket, and Squeak Smalltalk.
### Python
Python doesn't have a separate compilation step: it loads binary metaschema instances at
runtime, generating classes on the fly.
After `pip install preserves`, load metaschemas with `preserves.schema.load_schema_file`:
```python
from preserves import stringify, schema, parse
S = schema.load_schema_file('./simpleChatProtocol.prb')
P = S.simpleChatProtocol
```
Then, members of `P` are the definitions from `simpleChatProtocol.prs`:
```python
>>> P.Present('me')
Present {'username': 'me'}
>>> stringify(P.Present('me'))
'<Present "me">'
>>> P.Present.decode(parse('<Present "me">'))
Present {'username': 'me'}
>>> P.Present.try_decode(parse('<Present "me">'))
Present {'username': 'me'}
>>> P.Present.try_decode(parse('<NotPresent "me">')) is None
True
>>> stringify(P.UserStatus('me', P.Status.here()))
'<Status "me" here>'
>>> stringify(P.UserStatus('me', P.Status.away('2022-03-08')))
'<Status "me" <away "2022-03-08">>'
>>> x = P.UserStatus.decode(parse('<Status "me" <away "2022-03-08">>'))
>>> x.status.VARIANT
#away
>>> x.status.VARIANT == Symbol('away')
True
```
### Rust
Generate Rust definitions corresponding to a metaschema instance with [preserves-schema-rs][].
The best way to use it is to integrate it into your `build.rs` (see [the
docs][preserves-schema-rs]), but you can also use it as a standalone command-line tool.
[preserves-schema-rs]: https://preserves.dev/doc/preserves-schema-rs.html
The following command generates a directory `./rs/chat` containing rust sources for a module
that expects to be called `chat` in Rust code:
```shell
preserves-schema-rs --output-dir rs/chat --prefix chat simpleChatProtocol.prb
```
Representative excerpts from one of the generated files, `./rs/chat/simple_chat_protocol.rs`:
```rust,noplayground
pub struct Present {
pub username: std::string::String
}
pub struct Says {
pub who: std::string::String,
pub what: std::string::String
}
pub struct UserStatus {
pub username: std::string::String,
pub status: Status
}
pub enum Status {
Here,
Away {
since: std::boxed::Box<TimeStamp>
}
}
pub struct TimeStamp(pub std::string::String);
```
### TypeScript
Generate TypeScript definitions from schema **sources** (not metaschema instances) using
[preserves-schema-ts][]. Unlike other code generators, this one understands schema source code
directly.
[preserves-schema-ts]: https://preserves.dev/doc/preserves-schema-ts.html
The following command generates a directory `./ts/gen` containing TypeScript sources:
```shell
preserves-schema-ts --output ./ts/gen .:simpleChatProtocol.prs
```
Representative excerpts from one of the generated files, `./ts/gen/simpleChatProtocol.ts`:
```typescript
export type Present = {"username": string};
export type Says = {"who": string, "what": string};
export type UserStatus = {"username": string, "status": Status};
export type Status = ({"_variant": "here"} | {"_variant": "away", "since": TimeStamp});
export type TimeStamp = string;
```
### Squeak Smalltalk
After loading the `Preserves` package from the [Preserves project SqueakSource
page](https://squeaksource.com/Preserves.html), perhaps via
```smalltalk
Installer squeaksource project: 'Preserves'; install: 'Preserves'.
```
you can load and compile the bundle using something like
```smalltalk
(PreservesSchemaEnvironment fromBundleFile: 'simpleChatProtocol.prb')
category: 'Example-Preserves-Schema-SimpleChat';
prefix: 'SimpleChat';
cleanCategoryOnCompile: true;
compileBundle.
```
which results in classes whose names are prefixed with `SimpleChat` being created in package
`Example-Preserves-Schema-SimpleChat`. Here's a screenshot of a browser showing the generated
classes:
![Screenshot of Squeak Browser on class SimpleChatSimpleChatProtocol](<../figures/System Browser: SimpleChatSimpleChatProtocol.png>)
Exploring the result of evaluating the following expression, which generates a Smalltalk object
in the specified schema, yields the following screenshot:
```smalltalk
SimpleChatSimpleChatProtocolStatus away
since: (SimpleChatSimpleChatProtocolTimeStamp new value: '2022-03-08')
```
![Exploration of a SimpleChatSimpleChatProtocolStatus object](<../figures/a SimpleChatSimpleChatProtocolStatus_away.png>)
Exploring the result of evaluating the following expression, which generates a Smalltalk object
representing the Preserves value corresponding to the value produced in the previous
expression, yields the following screenshot:
```smalltalk
(SimpleChatSimpleChatProtocolStatus away
since: (SimpleChatSimpleChatProtocolTimeStamp new value: '2022-03-08'))
asPreserves
```
![Exploration of a SimpleChatSimpleChatProtocolStatus preserves value object](<../figures/away-2022-03-08.png>)
Finally, the following expression parses a valid `Status` string input:
```smalltalk
SimpleChatSimpleChatProtocolStatus
from: '<away "2022-03-08">' parsePreserves
orTry: []
```
If it had been invalid, the answer would have been `nil` (because `[] value` is `nil`).