silk.schema.schema.
Read and write your own types in any data format, from the types alone.
When to use#
Write impl Schema for T {} for a struct or a union of unit variants, and read or write it with
a format such as silk.schema.json.SchemaJson. Give a type its own read and write when its
wire form differs from its fields, such as a port written as text, and build them from a codec
chain with Schemas.readVia and Schemas.writeVia.
Details#
A type is declared once, and the schema derives what to do from it. The default read and
write walk the type's public fields with silk.core.reflect, so a struct is read from an
object with one member per field, in any order, and written in declaration order. A union whose
variants have no fields is the name of its variant, such as "Admin". A union with a variant
that has fields is an object whose type member names the variant and whose other members are
that variant's fields, such as {"type": "Circle", "radius": 2}; Spec.tag picks another
member name. An Option field may be absent, or null, when it is None, and is left out when
written.
Spec holds what the types cannot say, such as a field whose name on the wire differs. A type
returns its spec from Schema.spec.
A format implements SchemaReader and SchemaWriter, so the same types read JSON today and
other formats later without changes.
Gotchas#
Every field of a derived struct or variant must be public, because reflection sees only public
fields. A variant field cannot be named type, or whatever Spec.tag picks.
Examples#
Read and write a struct as JSON#
import silk.core.effect { Effect }
import silk.core.result { Result }
import silk.mem.allocator { Allocator, OutOfMemoryError }
import silk.schema.issue { SchemaError }
import silk.schema.json { SchemaJson }
import silk.schema.schema { Schema, Spec }
import silk.text.string { String }
struct User {
pub id: u64,
pub name: String,
pub admin: bool,
}
impl Schema for User {
static fn spec() -> Spec<User> {
return Spec<User>.of() |> Spec<User>.rename("id", "user_id")
}
}
effect fn load() -> u64 ! SchemaError | OutOfMemoryError ? &mut Allocator {
let user = run SchemaJson.decode<User>(b"{\"name\": \"Ada\", \"user_id\": 7, \"admin\": true}")
if !user.admin { return 0 }
return user.id
}
pub effect fn main() -> i32 ! OutOfMemoryError {
let mut allocator = Allocator.systemAllocatorProvider()
let loaded = run Effect.result(load()) |> Effect.provideMut<Allocator>(&mut allocator)
return match move loaded {
Result<u64, SchemaError | OutOfMemoryError>.Success {value} => if value == 7 { 0 } else { 1 }
Result<u64, SchemaError | OutOfMemoryError>.Failure {..} => 2
}
}Import as Schema with import silk.schema.schema { Schema }.
Public declarations: 8.
PathSegment#
pub union PathSegmentOne step of a SchemaPath: a struct field by its Silk name, or a list element by its index.
Field#
FieldA struct field.
Field name#
pub name: string<'static>The field's name in Silk, not its name on the wire.
Index#
IndexA list element.
Field at#
pub at: usizeThe position of the element, from 0.
Implementation Copy for PathSegment#
impl Copy for PathSegmentSchemaPath#
pub struct SchemaPathThe path from the root value to the part a reader is reading, such as settings.ports[2].
When to use#
A format's SchemaReader keeps one and returns it from SchemaReader.path. The derived
read operations push a segment before they read a part and pop it after, so when a read fails
the path still names the part that failed.
Implementation SchemaPath#
impl SchemaPathAssociated function make#
pub fn make() -> SchemaPathReturns an empty path, which names the root value.
Method push#
pub effect fn push(self: &mut Self, segment: PathSegment) -> () ! OutOfMemoryError ? &mut AllocatorAdds segment at the end of the path.
Errors#
OutOfMemoryErrorif theAllocatorcannot grow the path.
Method pop#
pub fn pop(self: &mut Self) -> ()Removes the last segment. It does nothing to an empty path.
Method length#
pub fn length(self: &Self) -> usizeReturns the number of segments.
Method text#
pub effect fn text(self: &Self) -> String ! OutOfMemoryError ? &mut AllocatorReturns the path as text, such as settings.ports[2]. The root value is empty text.
Errors#
OutOfMemoryErrorif theAllocatorcannot supply the text.
SchemaReader#
pub interface SchemaReaderThe reading side of a data format, in terms every format shares: scalars, text, lists and objects.
When to use#
Implement it to let every Schema type read a new format. Programs call a format's own entry
point, such as silk.schema.json.SchemaJson.decode, rather than these operations.
Details#
The schema reads an object by calling beginObject, then member once for each field it
needs, in its own order, and then endObject. member finds the member wherever it is in the
object and leaves the reader at its value.
Operation path#
fn path(self: &mut Self) -> &mut SchemaPathReturns the path of the part being read.
Operation boolean#
effect fn boolean(self: &mut Self) -> bool ! Issue | OutOfMemoryError ? &mut AllocatorReads true or false.
Errors#
IssuewithIssueReason.InvalidTypefor another kind of value.
Operation signed#
effect fn signed(self: &mut Self) -> i64 ! Issue | OutOfMemoryError ? &mut AllocatorReads an integer that fits in i64.
Errors#
IssuewithIssueReason.InvalidTypefor another kind of value, orInvalidValuefor an integer outsidei64.
Operation unsigned#
effect fn unsigned(self: &mut Self) -> u64 ! Issue | OutOfMemoryError ? &mut AllocatorReads an integer that fits in u64.
Errors#
IssuewithIssueReason.InvalidTypefor another kind of value, orInvalidValuefor an integer outsideu64.
Operation float#
effect fn float(self: &mut Self) -> f64 ! Issue | OutOfMemoryError ? &mut AllocatorReads a number as an f64.
Errors#
IssuewithIssueReason.InvalidTypefor another kind of value.
Operation text#
effect fn text(self: &mut Self) -> String ! Issue | OutOfMemoryError ? &mut AllocatorReads text.
Errors#
IssuewithIssueReason.InvalidTypefor another kind of value.OutOfMemoryErrorif theAllocatorcannot supply the text.
Operation null#
effect fn null(self: &mut Self) -> bool ! Issue | OutOfMemoryError ? &mut AllocatorReads past a null value and returns true, or returns false and leaves another value
unread.
Operation beginList#
effect fn beginList(self: &mut Self) -> () ! Issue | OutOfMemoryError ? &mut AllocatorStarts reading a list.
Errors#
IssuewithIssueReason.InvalidTypefor another kind of value.
Operation nextElement#
effect fn nextElement(self: &mut Self) -> bool ! Issue | OutOfMemoryError ? &mut AllocatorReturns true and leaves the reader at the next element, or returns false and reads past
the end of the list.
Operation beginObject#
effect fn beginObject(self: &mut Self) -> () ! Issue | OutOfMemoryError ? &mut AllocatorStarts reading an object.
Errors#
IssuewithIssueReason.InvalidTypefor another kind of value.
Operation member#
effect fn member(self: &mut Self, key: string) -> bool ! Issue | OutOfMemoryError ? &mut AllocatorFinds the member key of the object being read and leaves the reader at its value, or
returns false if the object has no such member.
Operation endObject#
effect fn endObject(self: &mut Self) -> () ! Issue | OutOfMemoryError ? &mut AllocatorReads past the end of the object being read, whatever members were read.
Operation checkFailed#
effect fn checkFailed(self: &mut Self, field: Option<string<'static>>, expected: string<'static>)
-> () ! Issue | OutOfMemoryError ? &mut AllocatorReports a check that failed on the value just read: on its field field, or on the whole
value when field is None. The default fails with it; a reader that collects every
problem records it and returns.
Errors#
IssuewithIssueReason.CheckFailed, at the field's path.OutOfMemoryErrorif the path cannot grow.
SchemaWriter#
pub interface SchemaWriterThe writing side of a data format, in terms every format shares: scalars, text, lists and objects.
When to use#
Implement it to let every Schema type write a new format. Programs call a format's own entry
point, such as silk.schema.json.SchemaJson.encode.
Details#
The schema writes a list as beginList, then element before each element, then endList. It
writes an object as beginObject, then key before each member's value, then endObject.
Operation boolean#
effect fn boolean(self: &mut Self, value: bool) -> () ! Issue | WriterError ? &mut WriterWrites true or false.
Operation signed#
effect fn signed(self: &mut Self, value: i64) -> () ! Issue | WriterError ? &mut WriterWrites a signed integer.
Operation unsigned#
effect fn unsigned(self: &mut Self, value: u64) -> () ! Issue | WriterError ? &mut WriterWrites an unsigned integer.
Operation float#
effect fn float(self: &mut Self, value: f64) -> () ! Issue | WriterError ? &mut WriterWrites a number.
Errors#
IssuewithIssueReason.InvalidValueif the format cannot holdvalue, such as a NaN in JSON.
Operation text#
effect fn text(self: &mut Self, value: string) -> () ! Issue | WriterError ? &mut WriterWrites text.
Operation null#
effect fn null(self: &mut Self) -> () ! Issue | WriterError ? &mut WriterWrites a null value.
Operation beginList#
effect fn beginList(self: &mut Self) -> () ! Issue | WriterError ? &mut WriterStarts a list.
Operation element#
effect fn element(self: &mut Self) -> () ! Issue | WriterError ? &mut WriterStarts the next element of the list being written.
Operation endList#
effect fn endList(self: &mut Self) -> () ! Issue | WriterError ? &mut WriterEnds the list being written.
Operation beginObject#
effect fn beginObject(self: &mut Self) -> () ! Issue | WriterError ? &mut WriterStarts an object.
Operation key#
effect fn key(self: &mut Self, name: string) -> () ! Issue | WriterError ? &mut WriterWrites the key of the next member of the object being written. Its value comes next.
Operation endObject#
effect fn endObject(self: &mut Self) -> () ! Issue | WriterError ? &mut WriterEnds the object being written.
Spec#
pub struct Spec<T>What a type's schema says beyond its declaration, such as a field whose name on the wire differs.
When to use#
Return one from Schema.spec and build it with pipes:
Spec<User>.of() |> Spec<User>.rename("id", "user_id").
Details#
A spec exists only during static evaluation. Field names are checked against T where they
are written, and the editor links them to the field.
Implementation Spec<T>#
impl<T> Spec<T>Associated function of#
pub static fn of() -> SelfReturns the spec that keeps every field's own name.
Method rename#
pub static fn rename(self: Self, field: Intrinsic.FieldName<T>, wire: string<'static>) -> SelfReturns the spec in which field is called wire in the data format.
Method tag#
pub static fn tag(self: Self, name: string<'static>) -> SelfReturns the spec in which a union's variant is named by the field name, instead of type.
Details#
A union with a variant that has fields is written as an object holding the variant's name in
this field next to the variant's own fields, such as {"type": "Circle", "radius": 2}.
Method tagName#
pub static fn tagName(self: &Self) -> string<'static>Returns the name of the field that holds a union's variant name.
Method wireName#
pub static fn wireName(self: &Self, label: string<'static>) -> string<'static>Returns the name of the field label in the data format.
Schema#
pub interface SchemaA type that can be read from and written to any data format.
When to use#
Write impl Schema for T {} for a struct or union whose fields are all public and implement
Schema. Override spec to rename fields. Override read and
write for a type whose wire form is not its fields, usually with Schemas.readVia and
Schemas.writeVia.
Details#
The standard library implements it for bool, the integer types, f64, String, Option<T>
and List<T>.
Operation spec#
static fn spec() -> Spec<Self>Returns what the type's schema says beyond its declaration. The default renames nothing.
Operation read#
effect fn read<R: SchemaReader>(reader: &mut R) -> Self ! Issue | OutOfMemoryError ? &mut AllocatorReads a value. The default reads a struct from an object, a union of unit variants from its variant name, and any other union from an object tagged with its variant name.
Errors#
Issueif the input does not hold a value of the type.OutOfMemoryErrorif theAllocatorcannot supply the value.
Operation write#
effect fn write<W: SchemaWriter>(self: &Self, writer: &mut W)
-> () ! Issue | WriterError | OutOfMemoryError ? &mut Writer | &mut AllocatorWrites the value. The default writes a struct as an object, a union of unit variants as its variant name, and any other union as an object tagged with its variant name.
Errors#
Issueif the format cannot hold the value.WriterErrorif theWriterfails.OutOfMemoryErrorif a codec needs memory that theAllocatorcannot supply.
Operation missing#
effect fn missing() -> Self ! IssueReturns the value of a field whose key is absent. The default fails, because the key is
required; Option returns None.
Errors#
IssuewithIssueReason.MissingKey.
Operation omitted#
fn omitted(self: &Self) -> boolReturns whether a field holding the value is left out when written. The default is false;
Option returns true for None.
Operation check#
fn check(self: &Self, checks: &mut Checks<Self>) -> ()Checks a value after it is read. The default accepts every value.
When to use#
Override it to state what the type alone cannot, such as a port between 1 and 65535 or a
name of at most 64 bytes, with Checks.field and Checks.that. A failed check becomes an
Issue at the path of the field it names: the read's only one, or one of all of them when
the format collects every problem, as SchemaJson.decodeAll does.
Checks#
pub struct Checks<T>The checks of one value of T, run by Schema.check after the value is read.
Examples#
import silk.core.effect { Effect }
import silk.core.result { Result }
import silk.mem.allocator { Allocator, OutOfMemoryError }
import silk.schema.codec { Between, Length }
import silk.schema.issue { IssueReason, SchemaError }
import silk.schema.json { SchemaJson }
import silk.schema.schema { Checks, Schema }
import silk.text.string { String }
struct Server {
pub host: String,
pub port: u16,
}
impl Schema for Server {
fn check(self: &Self, checks: &mut Checks<Server>) -> () {
checks.field(self, "host", Length {min: 1, max: 253, message: "a host name"})
checks.field(self, "port", Between<u16> {low: 1, high: 65535, message: "a port"})
}
}
effect fn load() -> Server ! SchemaError | OutOfMemoryError ? &mut Allocator {
return run SchemaJson.decode<Server>(b"{\"host\": \"silk.dev\", \"port\": 0}")
}
pub effect fn main() -> i32 ! OutOfMemoryError {
let mut allocator = Allocator.systemAllocatorProvider()
return match run Effect.result(load()) |> Effect.provideMut<Allocator>(&mut allocator) {
Result<Server, SchemaError | OutOfMemoryError>.Failure {error: SchemaError {path, issue}} =>
if path.view() == "port" && issue.reason == IssueReason.CheckFailed { 0 } else { 1 }
_ => 2
}
}Gotchas#
One value records at most 16 failed checks; later ones are dropped.
Implementation Checks<T>#
impl<T> Checks<T>Associated function make#
pub fn make() -> Checks<T>Returns the checks with nothing failed yet.
Method field#
pub fn field<V, Owner, K: Check<V>>(self: &mut Self, owner: &Owner, static name: Intrinsic.FieldName<Owner>, check: K) -> ()Checks the field name of owner with check, whose type says the field's type, such as
Between<u16> for a u16 field.
Method that#
pub fn that(self: &mut Self, holds: bool, expected: string<'static>) -> ()Checks a condition on the whole value, such as two fields that must agree. expected says
what the value must be when holds is false.
Schemas#
pub struct SchemasThe owner of the functions that read and write Schema types. It has no fields and is never
constructed.
Implementation Schemas#
impl SchemasAssociated function read#
pub effect fn read<T: Schema, R: SchemaReader>(reader: &mut R) -> T ! Issue | OutOfMemoryError ? &mut AllocatorReads a T with its Schema implementation.
Errors#
Issueif the input does not hold a value of the type.OutOfMemoryErrorif theAllocatorcannot supply the value.
Associated function write#
pub effect fn write<T: Schema, W: SchemaWriter>(value: &T, writer: &mut W)
-> () ! Issue | WriterError | OutOfMemoryError ? &mut Writer | &mut AllocatorWrites value with its Schema implementation.
Errors#
Issueif the format cannot hold the value.WriterErrorif theWriterfails.
Associated function spec#
pub static fn spec<T: Schema>() -> Spec<T>Returns the spec of T.
Associated function readVia#
pub effect fn readVia<A, I: Schema, C: Codec<A, I>, R: SchemaReader>(codec: &C, reader: &mut R)
-> A ! Issue | OutOfMemoryError ? &mut AllocatorReads the wire form I and decodes it into an A with codec.
When to use#
Call it from the read of a type whose wire form is another type, such as a port written as
text.
Errors#
Issueif the input does not hold anI, or ifcodecrejects it.OutOfMemoryErrorif theAllocatorcannot supply the value.
Associated function writeVia#
pub effect fn writeVia<A, I: Schema, C: Codec<A, I>, W: SchemaWriter>(codec: &C, value: &A, writer: &mut W)
-> () ! Issue | WriterError | OutOfMemoryError ? &mut Writer | &mut AllocatorEncodes value into the wire form I with codec and writes it.
Errors#
Issueif the format cannot hold the wire value.OutOfMemoryErrorifcodecneeds memory that theAllocatorcannot supply.WriterErrorif theWriterfails.
Implementation Schema for bool#
impl Schema for boolOperation read#
effect fn read<R: SchemaReader>(reader: &mut R) -> bool ! Issue | OutOfMemoryError ? &mut AllocatorReads true or false.
Operation write#
effect fn write<W: SchemaWriter>(self: &Self, writer: &mut W)
-> () ! Issue | WriterError | OutOfMemoryError ? &mut Writer | &mut AllocatorWrites true or false.
Implementation Schema for u64#
impl Schema for u64Operation read#
effect fn read<R: SchemaReader>(reader: &mut R) -> u64 ! Issue | OutOfMemoryError ? &mut AllocatorReads a number.
Operation write#
effect fn write<W: SchemaWriter>(self: &Self, writer: &mut W)
-> () ! Issue | WriterError | OutOfMemoryError ? &mut Writer | &mut AllocatorWrites the number.
Implementation Schema for i64#
impl Schema for i64Operation read#
effect fn read<R: SchemaReader>(reader: &mut R) -> i64 ! Issue | OutOfMemoryError ? &mut AllocatorReads a number.
Operation write#
effect fn write<W: SchemaWriter>(self: &Self, writer: &mut W)
-> () ! Issue | WriterError | OutOfMemoryError ? &mut Writer | &mut AllocatorWrites the number.
Implementation Schema for f64#
impl Schema for f64Operation read#
effect fn read<R: SchemaReader>(reader: &mut R) -> f64 ! Issue | OutOfMemoryError ? &mut AllocatorReads a number.
Operation write#
effect fn write<W: SchemaWriter>(self: &Self, writer: &mut W)
-> () ! Issue | WriterError | OutOfMemoryError ? &mut Writer | &mut AllocatorWrites the number. A format that cannot hold a NaN or an infinity fails with an Issue.
Implementation Schema for String#
impl Schema for StringOperation read#
effect fn read<R: SchemaReader>(reader: &mut R) -> String ! Issue | OutOfMemoryError ? &mut AllocatorReads text.
Operation write#
effect fn write<W: SchemaWriter>(self: &Self, writer: &mut W)
-> () ! Issue | WriterError | OutOfMemoryError ? &mut Writer | &mut AllocatorWrites the text.
Implementation Schema for Option<T>#
impl<T: Schema> Schema for Option<T>Operation read#
effect fn read<R: SchemaReader>(reader: &mut R) -> Option<T> ! Issue | OutOfMemoryError ? &mut AllocatorReads None from a null value, and Some from any other value.
Operation write#
effect fn write<W: SchemaWriter>(self: &Self, writer: &mut W)
-> () ! Issue | WriterError | OutOfMemoryError ? &mut Writer | &mut AllocatorWrites the value of Some, or a null value for None. A None field is left out instead.
Operation missing#
effect fn missing() -> Option<T> ! IssueReturns None, so an Option field may be absent.
Operation omitted#
fn omitted(self: &Self) -> boolReturns true for None, so a None field is left out.
Implementation Schema for List<T>#
impl<T: Schema> Schema for List<T>Operation read#
effect fn read<R: SchemaReader>(reader: &mut R) -> List<T> ! Issue | OutOfMemoryError ? &mut AllocatorReads a list, element by element.
Operation write#
effect fn write<W: SchemaWriter>(self: &Self, writer: &mut W)
-> () ! Issue | WriterError | OutOfMemoryError ? &mut Writer | &mut AllocatorWrites the list, element by element.
Implementation Schema for u8#
impl Schema for u8Operation read#
effect fn read<R: SchemaReader>(reader: &mut R) -> u8 ! Issue | OutOfMemoryError ? &mut AllocatorReads a number and fails with IssueReason.InvalidValue if it does not fit.
Operation write#
effect fn write<W: SchemaWriter>(self: &Self, writer: &mut W)
-> () ! Issue | WriterError | OutOfMemoryError ? &mut Writer | &mut AllocatorWrites the number.
Implementation Schema for u16#
impl Schema for u16Operation read#
effect fn read<R: SchemaReader>(reader: &mut R) -> u16 ! Issue | OutOfMemoryError ? &mut AllocatorReads a number and fails with IssueReason.InvalidValue if it does not fit.
Operation write#
effect fn write<W: SchemaWriter>(self: &Self, writer: &mut W)
-> () ! Issue | WriterError | OutOfMemoryError ? &mut Writer | &mut AllocatorWrites the number.
Implementation Schema for u32#
impl Schema for u32Operation read#
effect fn read<R: SchemaReader>(reader: &mut R) -> u32 ! Issue | OutOfMemoryError ? &mut AllocatorReads a number and fails with IssueReason.InvalidValue if it does not fit.
Operation write#
effect fn write<W: SchemaWriter>(self: &Self, writer: &mut W)
-> () ! Issue | WriterError | OutOfMemoryError ? &mut Writer | &mut AllocatorWrites the number.
Implementation Schema for usize#
impl Schema for usizeOperation read#
effect fn read<R: SchemaReader>(reader: &mut R) -> usize ! Issue | OutOfMemoryError ? &mut AllocatorReads a number and fails with IssueReason.InvalidValue if it does not fit.
Operation write#
effect fn write<W: SchemaWriter>(self: &Self, writer: &mut W)
-> () ! Issue | WriterError | OutOfMemoryError ? &mut Writer | &mut AllocatorWrites the number.
Implementation Schema for i8#
impl Schema for i8Operation read#
effect fn read<R: SchemaReader>(reader: &mut R) -> i8 ! Issue | OutOfMemoryError ? &mut AllocatorReads a number and fails with IssueReason.InvalidValue if it does not fit.
Operation write#
effect fn write<W: SchemaWriter>(self: &Self, writer: &mut W)
-> () ! Issue | WriterError | OutOfMemoryError ? &mut Writer | &mut AllocatorWrites the number.
Implementation Schema for i16#
impl Schema for i16Operation read#
effect fn read<R: SchemaReader>(reader: &mut R) -> i16 ! Issue | OutOfMemoryError ? &mut AllocatorReads a number and fails with IssueReason.InvalidValue if it does not fit.
Operation write#
effect fn write<W: SchemaWriter>(self: &Self, writer: &mut W)
-> () ! Issue | WriterError | OutOfMemoryError ? &mut Writer | &mut AllocatorWrites the number.
Implementation Schema for i32#
impl Schema for i32Operation read#
effect fn read<R: SchemaReader>(reader: &mut R) -> i32 ! Issue | OutOfMemoryError ? &mut AllocatorReads a number and fails with IssueReason.InvalidValue if it does not fit.
Operation write#
effect fn write<W: SchemaWriter>(self: &Self, writer: &mut W)
-> () ! Issue | WriterError | OutOfMemoryError ? &mut Writer | &mut AllocatorWrites the number.
Category: Data formats.
Profiles: aarch64-apple-darwin, aarch64-unknown-linux-gnu, aarch64-unknown-linux-gnu-no-libc, x86_64-unknown-linux-gnu, x86_64-unknown-linux-gnu-no-libc, wasm32-unknown-unknown, wasm32-wasip1, wasm32-wasip2, x86_64-pc-windows-msvc, wasm64-unknown-unknown.