Working with schemas
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:
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. To compile schema source code to metaschema instances, use preserves-schemac:
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.
Converting the simpleChatProtocol.prb
file to Preserves text syntax lets us read the
metaschema instance corresponding to the source code:
cat simpleChatProtocol.prb | preserves-tool convert
The result:
<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
:
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
:
>>> 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), but you can also use it as a standalone command-line tool.
The following command generates a directory ./rs/chat
containing rust sources for a module
that expects to be called chat
in Rust code:
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
:
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.
The following command generates a directory ./ts/gen
containing TypeScript sources:
preserves-schema-ts --output ./ts/gen .:simpleChatProtocol.prs
Representative excerpts from one of the generated files, ./ts/gen/simpleChatProtocol.ts
:
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, perhaps via
Installer squeaksource project: 'Preserves'; install: 'Preserves'.
you can load and compile the bundle using something like
(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:
Exploring the result of evaluating the following expression, which generates a Smalltalk object in the specified schema, yields the following screenshot:
SimpleChatSimpleChatProtocolStatus away
since: (SimpleChatSimpleChatProtocolTimeStamp new value: '2022-03-08')
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:
(SimpleChatSimpleChatProtocolStatus away
since: (SimpleChatSimpleChatProtocolTimeStamp new value: '2022-03-08'))
asPreserves
Finally, the following expression parses a valid Status
string input:
SimpleChatSimpleChatProtocolStatus
from: '<away "2022-03-08">' parsePreserves
orTry: []
If it had been invalid, the answer would have been nil
(because [] value
is nil
).