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:

Screenshot of Squeak Browser on class SimpleChatSimpleChatProtocol

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')

Exploration of a SimpleChatSimpleChatProtocolStatus object

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

Exploration of a SimpleChatSimpleChatProtocolStatus preserves value object

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).