256 lines
7.5 KiB
Markdown
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`).
|