diff --git a/src/SUMMARY.md b/src/SUMMARY.md index 2cf1d5d..d87a4ad 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -77,7 +77,7 @@ - [Overview](./guide/index.md) - [Preserves](./guide/preserves.md) -- [Working with schemas]() +- [Working with schemas](./guide/working-with-schemas.md) - [Language-neutral protocols]() - [Rust]() - [Python]() diff --git a/src/figures/System Browser: SimpleChatSimpleChatProtocol.png b/src/figures/System Browser: SimpleChatSimpleChatProtocol.png new file mode 100644 index 0000000..1091fdc Binary files /dev/null and b/src/figures/System Browser: SimpleChatSimpleChatProtocol.png differ diff --git a/src/figures/a SimpleChatSimpleChatProtocolStatus_away.png b/src/figures/a SimpleChatSimpleChatProtocolStatus_away.png new file mode 100644 index 0000000..c5c9648 Binary files /dev/null and b/src/figures/a SimpleChatSimpleChatProtocolStatus_away.png differ diff --git a/src/figures/away-2022-03-08.png b/src/figures/away-2022-03-08.png new file mode 100644 index 0000000..d4def94 Binary files /dev/null and b/src/figures/away-2022-03-08.png differ diff --git a/src/guide/schemas/.envrc b/src/guide/schemas/.envrc new file mode 100644 index 0000000..e99400e --- /dev/null +++ b/src/guide/schemas/.envrc @@ -0,0 +1,4 @@ +[ -d .venv ] || python -m venv .venv +. .venv/bin/activate +pip install -U preserves +PATH=$PATH:~/src/preserves/implementations/rust/target/debug diff --git a/src/guide/schemas/.gitignore b/src/guide/schemas/.gitignore new file mode 100644 index 0000000..1d17dae --- /dev/null +++ b/src/guide/schemas/.gitignore @@ -0,0 +1 @@ +.venv diff --git a/src/guide/schemas/Makefile b/src/guide/schemas/Makefile new file mode 100644 index 0000000..35e1882 --- /dev/null +++ b/src/guide/schemas/Makefile @@ -0,0 +1,15 @@ +all: simpleChatProtocol.prb rs ts + +simpleChatProtocol.prb: simpleChatProtocol.prs + preserves-schemac .:$< > $@ + +clean: + rm -f simpleChatProtocol.prb + rm -rf rs + rm -rf ts + +rs: simpleChatProtocol.prb + preserves-schema-rs --output-dir rs/chat --prefix chat simpleChatProtocol.prb + +ts: simpleChatProtocol.prs + preserves-schema-ts --output ./ts/gen .:simpleChatProtocol.prs diff --git a/src/guide/schemas/example.py b/src/guide/schemas/example.py new file mode 100644 index 0000000..d974f6e --- /dev/null +++ b/src/guide/schemas/example.py @@ -0,0 +1,38 @@ +from preserves import stringify, schema, parse, Symbol +S = schema.load_schema_file('./simpleChatProtocol.prb') +P = S.simpleChatProtocol + +def hook_for_doctests(): + ''' + >>> P.Present('me') + Present {'username': 'me'} + + >>> stringify(P.Present('me')) + '' + + >>> P.Present.decode(parse('')) + Present {'username': 'me'} + + >>> P.Present.try_decode(parse('')) + Present {'username': 'me'} + + >>> P.Present.try_decode(parse('')) is None + True + + >>> stringify(P.UserStatus('me', P.Status.here())) + '' + + >>> stringify(P.UserStatus('me', P.Status.away('2022-03-08'))) + '>' + + >>> x = P.UserStatus.decode(parse('>')) + >>> x.status.VARIANT + #away + >>> x.status.VARIANT == Symbol('away') + True + ''' + pass + +if __name__ == '__main__': + import doctest + doctest.testmod() diff --git a/src/guide/schemas/rs/chat/mod.rs b/src/guide/schemas/rs/chat/mod.rs new file mode 100644 index 0000000..22dddf9 --- /dev/null +++ b/src/guide/schemas/rs/chat/mod.rs @@ -0,0 +1,25 @@ +pub mod simple_chat_protocol; + +use preserves_schema::support as _support; +use _support::preserves; + +#[allow(non_snake_case)] +pub struct Language { + pub LIT_0_PRESENT: N /* Present */, + pub LIT_1_SAYS: N /* Says */, + pub LIT_4_STATUS: N /* Status */, + pub LIT_3_AWAY: N /* away */, + pub LIT_2_HERE: N /* here */ +} + +impl Default for Language { + fn default() -> Self { + Language { + LIT_0_PRESENT: /* Present */ _support::decode_lit(&vec![179, 7, 80, 114, 101, 115, 101, 110, 116]).unwrap(), + LIT_1_SAYS: /* Says */ _support::decode_lit(&vec![179, 4, 83, 97, 121, 115]).unwrap(), + LIT_4_STATUS: /* Status */ _support::decode_lit(&vec![179, 6, 83, 116, 97, 116, 117, 115]).unwrap(), + LIT_3_AWAY: /* away */ _support::decode_lit(&vec![179, 4, 97, 119, 97, 121]).unwrap(), + LIT_2_HERE: /* here */ _support::decode_lit(&vec![179, 4, 104, 101, 114, 101]).unwrap() + } + } +} diff --git a/src/guide/schemas/rs/chat/simple_chat_protocol.rs b/src/guide/schemas/rs/chat/simple_chat_protocol.rs new file mode 100644 index 0000000..6effe18 --- /dev/null +++ b/src/guide/schemas/rs/chat/simple_chat_protocol.rs @@ -0,0 +1,337 @@ +#![allow(unused_parens)] +#![allow(unused_imports)] + +use std::convert::TryFrom; +use preserves_schema::support as _support; +use _support::Deserialize; +use _support::Parse; +use _support::Unparse; +use _support::preserves; +use preserves::value::Domain; +use preserves::value::NestedValue; + +#[derive(Debug, PartialOrd, Ord, PartialEq, Eq, Clone, Hash)] +pub struct Present { + pub username: std::string::String +} + +impl preserves::value::Domain for Present {} + +impl<'de, _Value: preserves::value::NestedValue, R: _support::Reader<'de, _Value>> _support::Deserialize<'de, _Value, R> for Present { + fn deserialize(r: &mut R) -> std::result::Result { + r.open_record(None)?; + let mut _tmp0 = _support::B::Type::default(); + _tmp0.shift(Some(_support::B::Item::RecordLabel)); + r.boundary(&_tmp0)?; + match r.next_token(true)? { + preserves::value::Token::Atom(v) => match v.value() { + preserves::value::Value::Symbol(w) if w == "Present" => {} + _ => return Err(_support::ParseError::conformance_error("simpleChatProtocol.Present"))?, + } + _ => return Err(_support::ParseError::conformance_error("simpleChatProtocol.Present"))?, + } + let _tmp1 = (); + _tmp0.shift(Some(_support::B::Item::RecordField)); + r.boundary(&_tmp0)?; + let _tmp2 = r.next_str()?.into_owned(); + r.ensure_complete(_tmp0, &_support::B::Item::RecordField)?; + Ok(Present {username: _tmp2}) + } +} + +impl< + 'a, + _L: Copy + Into<&'a chat::Language<_Value>>, + _Value: preserves::value::NestedValue + 'a +> _support::Parse<_L, _Value> for Present { + fn parse(_ctxt: _L, value: &_Value) -> std::result::Result { + let _tmp0 = value.value().to_record(None)?; + if _tmp0.label() != &<_L as Into<&'a chat::Language<_Value>>>::into(_ctxt).LIT_0_PRESENT { return Err(_support::ParseError::conformance_error("simpleChatProtocol.Present")); } + let _tmp1 = (); + if _tmp0.fields().len() < 1 { return Err(_support::ParseError::conformance_error("simpleChatProtocol.Present")); } + let _tmp2 = (&_tmp0.fields()[0]).value().to_string()?; + Ok(Present {username: _tmp2.clone()}) + } +} + +impl< + 'a, + _L: Copy + Into<&'a chat::Language<_Value>>, + _Value: preserves::value::NestedValue + 'a +> _support::Unparse<_L, _Value> for Present { + fn unparse(&self, _ctxt: _L) -> _Value { + let Present {username: _tmp0} = self; + { + let mut _tmp1 = preserves::value::Record(vec![(&<_L as Into<&'a chat::Language<_Value>>>::into(_ctxt).LIT_0_PRESENT).clone()]); + _tmp1.fields_vec_mut().push(preserves::value::Value::from(_tmp0).wrap()); + _tmp1.finish().wrap() + } + } +} + +#[derive(Debug, PartialOrd, Ord, PartialEq, Eq, Clone, Hash)] +pub struct Says { + pub who: std::string::String, + pub what: std::string::String +} + +impl preserves::value::Domain for Says {} + +impl<'de, _Value: preserves::value::NestedValue, R: _support::Reader<'de, _Value>> _support::Deserialize<'de, _Value, R> for Says { + fn deserialize(r: &mut R) -> std::result::Result { + r.open_record(None)?; + let mut _tmp0 = _support::B::Type::default(); + _tmp0.shift(Some(_support::B::Item::RecordLabel)); + r.boundary(&_tmp0)?; + match r.next_token(true)? { + preserves::value::Token::Atom(v) => match v.value() { + preserves::value::Value::Symbol(w) if w == "Says" => {} + _ => return Err(_support::ParseError::conformance_error("simpleChatProtocol.Says"))?, + } + _ => return Err(_support::ParseError::conformance_error("simpleChatProtocol.Says"))?, + } + let _tmp1 = (); + _tmp0.shift(Some(_support::B::Item::RecordField)); + r.boundary(&_tmp0)?; + let _tmp2 = r.next_str()?.into_owned(); + _tmp0.shift(Some(_support::B::Item::RecordField)); + r.boundary(&_tmp0)?; + let _tmp3 = r.next_str()?.into_owned(); + r.ensure_complete(_tmp0, &_support::B::Item::RecordField)?; + Ok(Says {who: _tmp2, what: _tmp3}) + } +} + +impl< + 'a, + _L: Copy + Into<&'a chat::Language<_Value>>, + _Value: preserves::value::NestedValue + 'a +> _support::Parse<_L, _Value> for Says { + fn parse(_ctxt: _L, value: &_Value) -> std::result::Result { + let _tmp0 = value.value().to_record(None)?; + if _tmp0.label() != &<_L as Into<&'a chat::Language<_Value>>>::into(_ctxt).LIT_1_SAYS { return Err(_support::ParseError::conformance_error("simpleChatProtocol.Says")); } + let _tmp1 = (); + if _tmp0.fields().len() < 2 { return Err(_support::ParseError::conformance_error("simpleChatProtocol.Says")); } + let _tmp2 = (&_tmp0.fields()[0]).value().to_string()?; + let _tmp3 = (&_tmp0.fields()[1]).value().to_string()?; + Ok(Says {who: _tmp2.clone(), what: _tmp3.clone()}) + } +} + +impl< + 'a, + _L: Copy + Into<&'a chat::Language<_Value>>, + _Value: preserves::value::NestedValue + 'a +> _support::Unparse<_L, _Value> for Says { + fn unparse(&self, _ctxt: _L) -> _Value { + let Says {who: _tmp0, what: _tmp1} = self; + { + let mut _tmp2 = preserves::value::Record(vec![(&<_L as Into<&'a chat::Language<_Value>>>::into(_ctxt).LIT_1_SAYS).clone()]); + _tmp2.fields_vec_mut().push(preserves::value::Value::from(_tmp0).wrap()); + _tmp2.fields_vec_mut().push(preserves::value::Value::from(_tmp1).wrap()); + _tmp2.finish().wrap() + } + } +} + +#[derive(Debug, PartialOrd, Ord, PartialEq, Eq, Clone, Hash)] +pub enum Status { + Here, + Away { + since: std::boxed::Box + } +} + +impl preserves::value::Domain for Status {} + +fn read_status_here<'de, _Value: preserves::value::NestedValue, R: _support::Reader<'de, _Value>>(r: &mut R) -> std::result::Result { + match r.next_token(true)? { + preserves::value::Token::Atom(v) => match v.value() { + preserves::value::Value::Symbol(w) if w == "here" => {} + _ => return Err(_support::ParseError::conformance_error("simpleChatProtocol.Status::here"))?, + } + _ => return Err(_support::ParseError::conformance_error("simpleChatProtocol.Status::here"))?, + } + let _tmp0 = (); + Ok(Status::Here) +} + +fn read_status_away<'de, _Value: preserves::value::NestedValue, R: _support::Reader<'de, _Value>>(r: &mut R) -> std::result::Result { + r.open_record(None)?; + let mut _tmp0 = _support::B::Type::default(); + _tmp0.shift(Some(_support::B::Item::RecordLabel)); + r.boundary(&_tmp0)?; + match r.next_token(true)? { + preserves::value::Token::Atom(v) => match v.value() { + preserves::value::Value::Symbol(w) if w == "away" => {} + _ => return Err(_support::ParseError::conformance_error("simpleChatProtocol.Status::away"))?, + } + _ => return Err(_support::ParseError::conformance_error("simpleChatProtocol.Status::away"))?, + } + let _tmp1 = (); + _tmp0.shift(Some(_support::B::Item::RecordField)); + r.boundary(&_tmp0)?; + let _tmp2 = TimeStamp::deserialize(r)?; + r.ensure_complete(_tmp0, &_support::B::Item::RecordField)?; + Ok(Status::Away {since: std::boxed::Box::new(_tmp2)}) +} + +impl<'de, _Value: preserves::value::NestedValue, R: _support::Reader<'de, _Value>> _support::Deserialize<'de, _Value, R> for Status { + fn deserialize(r: &mut R) -> std::result::Result { + let _mark = r.mark()?; + match read_status_here(r) { Err(e) if e.is_conformance_error() => r.restore(&_mark)?, result => return result } + match read_status_away(r) { Err(e) if e.is_conformance_error() => r.restore(&_mark)?, result => return result } + Err(_support::ParseError::conformance_error("simpleChatProtocol.Status")) + } +} + +fn parse_status_here< + 'a, + _L: Copy + Into<&'a chat::Language<_Value>>, + _Value: preserves::value::NestedValue + 'a +>(_ctxt: _L, value: &_Value) -> std::result::Result { + if value != &<_L as Into<&'a chat::Language<_Value>>>::into(_ctxt).LIT_2_HERE { return Err(_support::ParseError::conformance_error("simpleChatProtocol.Status::here")); } + let _tmp0 = (); + Ok(Status::Here) +} + +fn parse_status_away< + 'a, + _L: Copy + Into<&'a chat::Language<_Value>>, + _Value: preserves::value::NestedValue + 'a +>(_ctxt: _L, value: &_Value) -> std::result::Result { + let _tmp0 = value.value().to_record(None)?; + if _tmp0.label() != &<_L as Into<&'a chat::Language<_Value>>>::into(_ctxt).LIT_3_AWAY { return Err(_support::ParseError::conformance_error("simpleChatProtocol.Status::away")); } + let _tmp1 = (); + if _tmp0.fields().len() < 1 { return Err(_support::ParseError::conformance_error("simpleChatProtocol.Status::away")); } + let _tmp2 = TimeStamp::parse(_ctxt, (&_tmp0.fields()[0]))?; + Ok(Status::Away {since: std::boxed::Box::new(_tmp2)}) +} + +impl< + 'a, + _L: Copy + Into<&'a chat::Language<_Value>>, + _Value: preserves::value::NestedValue + 'a +> _support::Parse<_L, _Value> for Status { + fn parse(_ctxt: _L, value: &_Value) -> std::result::Result { + if let Ok(r) = parse_status_here(_ctxt, value) { return Ok(r); } + if let Ok(r) = parse_status_away(_ctxt, value) { return Ok(r); } + Err(_support::ParseError::conformance_error("simpleChatProtocol.Status")) + } +} + +impl< + 'a, + _L: Copy + Into<&'a chat::Language<_Value>>, + _Value: preserves::value::NestedValue + 'a +> _support::Unparse<_L, _Value> for Status { + fn unparse(&self, _ctxt: _L) -> _Value { + match self { + Status::Here => (&<_L as Into<&'a chat::Language<_Value>>>::into(_ctxt).LIT_2_HERE).clone(), + Status::Away {since: _tmp0} => { + let mut _tmp1 = preserves::value::Record(vec![(&<_L as Into<&'a chat::Language<_Value>>>::into(_ctxt).LIT_3_AWAY).clone()]); + _tmp1.fields_vec_mut().push(_tmp0.as_ref().unparse(_ctxt)); + _tmp1.finish().wrap() + }, + } + } +} + +#[derive(Debug, PartialOrd, Ord, PartialEq, Eq, Clone, Hash)] +pub struct TimeStamp(pub std::string::String); + +impl preserves::value::Domain for TimeStamp {} + +impl<'de, _Value: preserves::value::NestedValue, R: _support::Reader<'de, _Value>> _support::Deserialize<'de, _Value, R> for TimeStamp { + fn deserialize(r: &mut R) -> std::result::Result { + let _tmp0 = r.next_str()?.into_owned(); + Ok(TimeStamp(_tmp0)) + } +} + +impl< + 'a, + _L: Copy + Into<&'a chat::Language<_Value>>, + _Value: preserves::value::NestedValue + 'a +> _support::Parse<_L, _Value> for TimeStamp { + fn parse(_ctxt: _L, value: &_Value) -> std::result::Result { + let _tmp0 = value.value().to_string()?; + Ok(TimeStamp(_tmp0.clone())) + } +} + +impl< + 'a, + _L: Copy + Into<&'a chat::Language<_Value>>, + _Value: preserves::value::NestedValue + 'a +> _support::Unparse<_L, _Value> for TimeStamp { + fn unparse(&self, _ctxt: _L) -> _Value { + let TimeStamp(_tmp0) = self; + preserves::value::Value::from(_tmp0).wrap() + } +} + +#[derive(Debug, PartialOrd, Ord, PartialEq, Eq, Clone, Hash)] +pub struct UserStatus { + pub username: std::string::String, + pub status: Status +} + +impl preserves::value::Domain for UserStatus {} + +impl<'de, _Value: preserves::value::NestedValue, R: _support::Reader<'de, _Value>> _support::Deserialize<'de, _Value, R> for UserStatus { + fn deserialize(r: &mut R) -> std::result::Result { + r.open_record(None)?; + let mut _tmp0 = _support::B::Type::default(); + _tmp0.shift(Some(_support::B::Item::RecordLabel)); + r.boundary(&_tmp0)?; + match r.next_token(true)? { + preserves::value::Token::Atom(v) => match v.value() { + preserves::value::Value::Symbol(w) if w == "Status" => {} + _ => return Err(_support::ParseError::conformance_error("simpleChatProtocol.UserStatus"))?, + } + _ => return Err(_support::ParseError::conformance_error("simpleChatProtocol.UserStatus"))?, + } + let _tmp1 = (); + _tmp0.shift(Some(_support::B::Item::RecordField)); + r.boundary(&_tmp0)?; + let _tmp2 = r.next_str()?.into_owned(); + _tmp0.shift(Some(_support::B::Item::RecordField)); + r.boundary(&_tmp0)?; + let _tmp3 = Status::deserialize(r)?; + r.ensure_complete(_tmp0, &_support::B::Item::RecordField)?; + Ok(UserStatus {username: _tmp2, status: _tmp3}) + } +} + +impl< + 'a, + _L: Copy + Into<&'a chat::Language<_Value>>, + _Value: preserves::value::NestedValue + 'a +> _support::Parse<_L, _Value> for UserStatus { + fn parse(_ctxt: _L, value: &_Value) -> std::result::Result { + let _tmp0 = value.value().to_record(None)?; + if _tmp0.label() != &<_L as Into<&'a chat::Language<_Value>>>::into(_ctxt).LIT_4_STATUS { return Err(_support::ParseError::conformance_error("simpleChatProtocol.UserStatus")); } + let _tmp1 = (); + if _tmp0.fields().len() < 2 { return Err(_support::ParseError::conformance_error("simpleChatProtocol.UserStatus")); } + let _tmp2 = (&_tmp0.fields()[0]).value().to_string()?; + let _tmp3 = Status::parse(_ctxt, (&_tmp0.fields()[1]))?; + Ok(UserStatus {username: _tmp2.clone(), status: _tmp3}) + } +} + +impl< + 'a, + _L: Copy + Into<&'a chat::Language<_Value>>, + _Value: preserves::value::NestedValue + 'a +> _support::Unparse<_L, _Value> for UserStatus { + fn unparse(&self, _ctxt: _L) -> _Value { + let UserStatus {username: _tmp0, status: _tmp1} = self; + { + let mut _tmp2 = preserves::value::Record(vec![(&<_L as Into<&'a chat::Language<_Value>>>::into(_ctxt).LIT_4_STATUS).clone()]); + _tmp2.fields_vec_mut().push(preserves::value::Value::from(_tmp0).wrap()); + _tmp2.fields_vec_mut().push(_tmp1.unparse(_ctxt)); + _tmp2.finish().wrap() + } + } +} diff --git a/src/guide/schemas/simpleChatProtocol.pr b/src/guide/schemas/simpleChatProtocol.pr new file mode 100644 index 0000000..f805728 --- /dev/null +++ b/src/guide/schemas/simpleChatProtocol.pr @@ -0,0 +1,13 @@ + > + ]>> + Says: > + > + ]>> + } + embeddedType: #f + version: 1 +}> diff --git a/src/guide/schemas/simpleChatProtocol.prb b/src/guide/schemas/simpleChatProtocol.prb new file mode 100644 index 0000000..15947af --- /dev/null +++ b/src/guide/schemas/simpleChatProtocol.prb @@ -0,0 +1,2 @@ +´³bundle·µ³simpleChatProtocol„´³schema·³version‘³ definitions·³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„„„„„„„„³Present´³rec´³lit³Present„´³tupleµ´³named³username´³atom³String„„„„„³ TimeStamp´³atom³String„³ +UserStatus´³rec´³lit³Status„´³tupleµ´³named³username´³atom³String„„´³named³status´³refµ„³Status„„„„„„³ embeddedType€„„„„ \ No newline at end of file diff --git a/src/guide/schemas/simpleChatProtocol.prs b/src/guide/schemas/simpleChatProtocol.prs new file mode 100644 index 0000000..b907784 --- /dev/null +++ b/src/guide/schemas/simpleChatProtocol.prs @@ -0,0 +1,6 @@ +version 1 . +Present = . +Says = . +UserStatus = . +Status = =here / . +TimeStamp = string . diff --git a/src/guide/schemas/ts/gen/simpleChatProtocol.ts b/src/guide/schemas/ts/gen/simpleChatProtocol.ts new file mode 100644 index 0000000..4c419b8 --- /dev/null +++ b/src/guide/schemas/ts/gen/simpleChatProtocol.ts @@ -0,0 +1,283 @@ +import * as _ from "@preserves/core"; + +export const $Present = _.Symbol.for("Present"); +export const $Says = _.Symbol.for("Says"); +export const $Status = _.Symbol.for("Status"); +export const $away = _.Symbol.for("away"); +export const $here = _.Symbol.for("here"); + +let __schema: _.Value | null = null; + +export function _schema() { + if (__schema === null) { + __schema = _.decode<_.GenericEmbedded>(_.Bytes.fromHex("b4b306736368656d61b7b30776657273696f6e91b30b646566696e6974696f6e73b7b30453617973b4b303726563b4b3036c6974b3045361797384b4b3057475706c65b5b4b3056e616d6564b30377686fb4b30461746f6db306537472696e678484b4b3056e616d6564b30477686174b4b30461746f6db306537472696e678484848484b306537461747573b4b3026f72b5b5b10468657265b4b3036c6974b304686572658484b5b10461776179b4b303726563b4b3036c6974b3046177617984b4b3057475706c65b5b4b3056e616d6564b30573696e6365b4b303726566b584b30954696d655374616d708484848484848484b30750726573656e74b4b303726563b4b3036c6974b30750726573656e7484b4b3057475706c65b5b4b3056e616d6564b308757365726e616d65b4b30461746f6db306537472696e678484848484b30954696d655374616d70b4b30461746f6db306537472696e6784b30a55736572537461747573b4b303726563b4b3036c6974b30653746174757384b4b3057475706c65b5b4b3056e616d6564b308757365726e616d65b4b30461746f6db306537472696e678484b4b3056e616d6564b306737461747573b4b303726566b584b306537461747573848484848484b30c656d62656464656454797065808484")); + }; + return __schema; +} + +export const _imports = {} + + +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; + + +export function Present(username: string): ( + Present & + {__as_preserve__<_embedded = _.GenericEmbedded>(): _.Value<_embedded>} +) {return {"username": username, __as_preserve__() {return fromPresent(this);}};} + +Present.schema = function () { + return {schema: _schema(), imports: _imports, definitionName: _.Symbol.for("Present")}; +} + +export function Says({who, what}: {who: string, what: string}): (Says & {__as_preserve__<_embedded = _.GenericEmbedded>(): _.Value<_embedded>}) {return {"who": who, "what": what, __as_preserve__() {return fromSays(this);}};} + +Says.schema = function () { + return {schema: _schema(), imports: _imports, definitionName: _.Symbol.for("Says")}; +} + +export function UserStatus({username, status}: {username: string, status: Status}): ( + UserStatus & + {__as_preserve__<_embedded = _.GenericEmbedded>(): _.Value<_embedded>} +) { + return { + "username": username, + "status": status, + __as_preserve__() {return fromUserStatus(this);} + }; +} + +UserStatus.schema = function () { + return { + schema: _schema(), + imports: _imports, + definitionName: _.Symbol.for("UserStatus") + }; +} + +export namespace Status { + export function here(): ( + Status & + {__as_preserve__<_embedded = _.GenericEmbedded>(): _.Value<_embedded>} + ) {return {"_variant": "here", __as_preserve__() {return fromStatus(this);}};}; + here.schema = function () { + return { + schema: _schema(), + imports: _imports, + definitionName: _.Symbol.for("Status"), + variant: _.Symbol.for("here") + }; + }; + export function away(since: TimeStamp): ( + Status & + {__as_preserve__<_embedded = _.GenericEmbedded>(): _.Value<_embedded>} + ) { + return { + "_variant": "away", + "since": since, + __as_preserve__() {return fromStatus(this);} + }; + }; + away.schema = function () { + return { + schema: _schema(), + imports: _imports, + definitionName: _.Symbol.for("Status"), + variant: _.Symbol.for("away") + }; + }; +} + +export function TimeStamp(value: string): TimeStamp {return value;} + +TimeStamp.schema = function () { + return { + schema: _schema(), + imports: _imports, + definitionName: _.Symbol.for("TimeStamp") + }; +} + +export function asPresent<_embedded = _.GenericEmbedded>(v: _.Value<_embedded>): ( + Present & + {__as_preserve__<_embedded = _.GenericEmbedded>(): _.Value<_embedded>} +) { + let result = toPresent(v); + if (result === void 0) throw new TypeError(`Invalid Present: ${_.stringify(v)}`); + return result; +} + +export function toPresent<_embedded = _.GenericEmbedded>(v: _.Value<_embedded>): undefined | ( + Present & + {__as_preserve__<_embedded = _.GenericEmbedded>(): _.Value<_embedded>} +) { + let result: undefined | ( + Present & + {__as_preserve__<_embedded = _.GenericEmbedded>(): _.Value<_embedded>} + ); + if (_.Record.isRecord<_.Value<_embedded>, _.Tuple<_.Value<_embedded>>, _embedded>(v)) { + let _tmp0: ({}) | undefined; + _tmp0 = _.is(v.label, $Present) ? {} : void 0; + if (_tmp0 !== void 0) { + let _tmp1: (string) | undefined; + _tmp1 = typeof v[0] === 'string' ? v[0] : void 0; + if (_tmp1 !== void 0) {result = {"username": _tmp1, __as_preserve__() {return fromPresent(this);}};}; + }; + }; + return result; +} + +Present.__from_preserve__ = toPresent; + +export function fromPresent<_embedded = _.GenericEmbedded>(_v: Present): _.Value<_embedded> {return _.Record($Present, [_v["username"]]);} + +export function asSays<_embedded = _.GenericEmbedded>(v: _.Value<_embedded>): (Says & {__as_preserve__<_embedded = _.GenericEmbedded>(): _.Value<_embedded>}) { + let result = toSays(v); + if (result === void 0) throw new TypeError(`Invalid Says: ${_.stringify(v)}`); + return result; +} + +export function toSays<_embedded = _.GenericEmbedded>(v: _.Value<_embedded>): undefined | (Says & {__as_preserve__<_embedded = _.GenericEmbedded>(): _.Value<_embedded>}) { + let result: undefined | (Says & {__as_preserve__<_embedded = _.GenericEmbedded>(): _.Value<_embedded>}); + if (_.Record.isRecord<_.Value<_embedded>, _.Tuple<_.Value<_embedded>>, _embedded>(v)) { + let _tmp0: ({}) | undefined; + _tmp0 = _.is(v.label, $Says) ? {} : void 0; + if (_tmp0 !== void 0) { + let _tmp1: (string) | undefined; + _tmp1 = typeof v[0] === 'string' ? v[0] : void 0; + if (_tmp1 !== void 0) { + let _tmp2: (string) | undefined; + _tmp2 = typeof v[1] === 'string' ? v[1] : void 0; + if (_tmp2 !== void 0) { + result = {"who": _tmp1, "what": _tmp2, __as_preserve__() {return fromSays(this);}}; + }; + }; + }; + }; + return result; +} + +Says.__from_preserve__ = toSays; + +export function fromSays<_embedded = _.GenericEmbedded>(_v: Says): _.Value<_embedded> {return _.Record($Says, [_v["who"], _v["what"]]);} + +export function asUserStatus<_embedded = _.GenericEmbedded>(v: _.Value<_embedded>): ( + UserStatus & + {__as_preserve__<_embedded = _.GenericEmbedded>(): _.Value<_embedded>} +) { + let result = toUserStatus(v); + if (result === void 0) throw new TypeError(`Invalid UserStatus: ${_.stringify(v)}`); + return result; +} + +export function toUserStatus<_embedded = _.GenericEmbedded>(v: _.Value<_embedded>): undefined | ( + UserStatus & + {__as_preserve__<_embedded = _.GenericEmbedded>(): _.Value<_embedded>} +) { + let result: undefined | ( + UserStatus & + {__as_preserve__<_embedded = _.GenericEmbedded>(): _.Value<_embedded>} + ); + if (_.Record.isRecord<_.Value<_embedded>, _.Tuple<_.Value<_embedded>>, _embedded>(v)) { + let _tmp0: ({}) | undefined; + _tmp0 = _.is(v.label, $Status) ? {} : void 0; + if (_tmp0 !== void 0) { + let _tmp1: (string) | undefined; + _tmp1 = typeof v[0] === 'string' ? v[0] : void 0; + if (_tmp1 !== void 0) { + let _tmp2: (Status) | undefined; + _tmp2 = toStatus(v[1]); + if (_tmp2 !== void 0) { + result = { + "username": _tmp1, + "status": _tmp2, + __as_preserve__() {return fromUserStatus(this);} + }; + }; + }; + }; + }; + return result; +} + +UserStatus.__from_preserve__ = toUserStatus; + +export function fromUserStatus<_embedded = _.GenericEmbedded>(_v: UserStatus): _.Value<_embedded> { + return _.Record($Status, [_v["username"], fromStatus<_embedded>(_v["status"])]); +} + +export function asStatus<_embedded = _.GenericEmbedded>(v: _.Value<_embedded>): ( + Status & + {__as_preserve__<_embedded = _.GenericEmbedded>(): _.Value<_embedded>} +) { + let result = toStatus(v); + if (result === void 0) throw new TypeError(`Invalid Status: ${_.stringify(v)}`); + return result; +} + +export function toStatus<_embedded = _.GenericEmbedded>(v: _.Value<_embedded>): undefined | ( + Status & + {__as_preserve__<_embedded = _.GenericEmbedded>(): _.Value<_embedded>} +) { + let _tmp0: ({}) | undefined; + let result: undefined | ( + Status & + {__as_preserve__<_embedded = _.GenericEmbedded>(): _.Value<_embedded>} + ); + _tmp0 = _.is(v, $here) ? {} : void 0; + if (_tmp0 !== void 0) {result = {"_variant": "here", __as_preserve__() {return fromStatus(this);}};}; + if (result === void 0) { + if (_.Record.isRecord<_.Value<_embedded>, _.Tuple<_.Value<_embedded>>, _embedded>(v)) { + let _tmp1: ({}) | undefined; + _tmp1 = _.is(v.label, $away) ? {} : void 0; + if (_tmp1 !== void 0) { + let _tmp2: (TimeStamp) | undefined; + _tmp2 = toTimeStamp(v[0]); + if (_tmp2 !== void 0) { + result = { + "_variant": "away", + "since": _tmp2, + __as_preserve__() {return fromStatus(this);} + }; + }; + }; + }; + }; + return result; +} + +export namespace Status {export const __from_preserve__ = toStatus;} + +export function fromStatus<_embedded = _.GenericEmbedded>(_v: Status): _.Value<_embedded> { + switch (_v._variant) { + case "here": {return $here;}; + case "away": {return _.Record($away, [fromTimeStamp<_embedded>(_v["since"])]);}; + }; +} + +export function asTimeStamp<_embedded = _.GenericEmbedded>(v: _.Value<_embedded>): TimeStamp { + let result = toTimeStamp(v); + if (result === void 0) throw new TypeError(`Invalid TimeStamp: ${_.stringify(v)}`); + return result; +} + +export function toTimeStamp<_embedded = _.GenericEmbedded>(v: _.Value<_embedded>): undefined | TimeStamp { + let _tmp0: (string) | undefined; + let result: undefined | TimeStamp; + _tmp0 = typeof v === 'string' ? v : void 0; + if (_tmp0 !== void 0) {result = _tmp0;}; + return result; +} + +TimeStamp.__from_preserve__ = toTimeStamp; + +export function fromTimeStamp<_embedded = _.GenericEmbedded>(_v: TimeStamp): _.Value<_embedded> {return _v;} + diff --git a/src/guide/working-with-schemas.md b/src/guide/working-with-schemas.md new file mode 100644 index 0000000..63b634d --- /dev/null +++ b/src/guide/working-with-schemas.md @@ -0,0 +1,255 @@ +# Working with schemas + + - [Preserves schema specification](https://preserves.gitlab.io/preserves/preserves-schema.html) + - [Index of Preserves schema tools](https://preserves.gitlab.io/preserves/doc/schema-tools.html) + +[preserves-schemac]: https://preserves.gitlab.io/preserves/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 = . +Says = . +UserStatus = . +Status = =here / . +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.gitlab.io/preserves/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 + > + ]>> + Says: > + > + ]>> + Status: + ] + [ + "away" + > + ]>> + ] + ]> + TimeStamp: + UserStatus: > + > + ]>> + } + 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')) +'' + +>>> P.Present.decode(parse('')) +Present {'username': 'me'} + +>>> P.Present.try_decode(parse('')) +Present {'username': 'me'} + +>>> P.Present.try_decode(parse('')) is None +True + +>>> stringify(P.UserStatus('me', P.Status.here())) +'' + +>>> stringify(P.UserStatus('me', P.Status.away('2022-03-08'))) +'>' + +>>> x = P.UserStatus.decode(parse('>')) +>>> 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.gitlab.io/preserves/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 + } +} +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.gitlab.io/preserves/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: '' parsePreserves + orTry: [] +``` + +If it had been invalid, the answer would have been `nil` (because `[] value` is `nil`).