Focused boundary types
Project creates a new nominal record from selected fields of an existing record. Fields(...) is ordered compile-time shape data. Its .field selectors are checked against the source record; they are not strings or runtime values.
type Account = {
id: Int;
email: String #{ Wire.name = "contact_email""contact_email": String; };
display_name: String #{ default = "Anonymous""Anonymous": String; };
active: Bool;
internal_note: String #{ visibility = Secret; };
}
type AccountCard = Project(Account, Fields(.display_name, .id, .active))
A projection is a fresh nominal type. Construct it by its own name rather than passing an Account where an AccountCard is expected.
let cardcard: Book.Records.ProjectingRecords.AccountCard = AccountCard { display_name = "Ada""Ada": String; id = 77: Int; active = TrueTrue: Bool; };
Focused Boundary
✓cardcard: Book.Records.ProjectingRecords.AccountCard: AccountCard;
expect ✓cardcard: Book.Records.ProjectingRecords.AccountCard == AccountCard { display_name = "Ada""Ada": String; id = 77: Int; active = TrueTrue: Bool; };Authored order
Projected fields are declared in Fields(...) order. That order is stable for generated display, inspection, wire codecs, and documentation.
Projection Order
expect ✓Inspect.inspectinspect: pure fn!('a: Inspect)(value: Book.Records.ProjectingRecords.AccountCard) -> String(cardcard: Book.Records.ProjectingRecords.AccountCard) == "AccountCard { display_name = \"Ada\"; id = 7; active = True }""AccountCard { display_name = \"Ada\"; id = 7; active = True }": String;
expect ✓must String.of_bytesof_bytes: pure fn(bytes: Bytes) -> String, exn [ :utf8(Int) ](Json.encodeencode: pure fn('a: Encode)(value: Book.Records.ProjectingRecords.AccountCard) -> Bytes, exn [ :encode(Wire.EncodeError) ](cardcard: Book.Records.ProjectingRecords.AccountCard))
== "{\"display_name\":\"Ada\",\"id\":7,\"active\":true}""{\"display_name\":\"Ada\",\"id\":7,\"active\":true}": String;Field metadata
Selected fields retain their field metadata. Whole-record validation and type-level protocol configuration belong to the complete source shape and are not inherited; the projected declaration may define its own.
type AccountContact = Project(Account, Fields(.id, .email))
type AccountName = Project(Account, Fields(.display_name))
Field Metadata
let contactcontact: Book.Records.ProjectingRecords.AccountContact = AccountContact { id = 77: Int; email = "ada@example.com""ada@example.com": String; };
expect ✓must String.of_bytesof_bytes: pure fn(bytes: Bytes) -> String, exn [ :utf8(Int) ](Json.encodeencode: pure fn('a: Encode)(value: Book.Records.ProjectingRecords.AccountContact) -> Bytes, exn [ :encode(Wire.EncodeError) ](contactcontact: Book.Records.ProjectingRecords.AccountContact))
== "{\"id\":7,\"contact_email\":\"ada@example.com\"}""{\"id\":7,\"contact_email\":\"ada@example.com\"}": String;
let default_namedefault_name: Book.Records.ProjectingRecords.AccountName = must Json.decodedecode: pure fn('target: Decode)(body: Bytes, target: Struct(Book.Records.ProjectingRecords.AccountName), ~max_depth: Int = 128) -> Book.Records.ProjectingRecords.AccountName, exn [ :decode(Wire.DecodeError) ](Bytes.of_stringof_string: pure fn!(string: String) -> Bytes("{}""{}": String), AccountName);
expect ✓default_namedefault_name: Book.Records.ProjectingRecords.AccountName == AccountName { display_name = "Anonymous""Anonymous": String; };Sparse projections
Prefixing a projection with sparse makes every selected field omittable. A supplied Option field gains an outer presence layer, preserving the difference between omission and an explicit clear.
type AccountPatch = sparse Project(Account, Fields(.display_name, .email))
Sparse Result
let emptyempty: Book.Records.ProjectingRecords.AccountPatch = AccountPatch {};
✓emptyempty: Book.Records.ProjectingRecords.AccountPatch.display_namedisplay_name: Option(String): Option(String);
expect ✓must String.of_bytesof_bytes: pure fn(bytes: Bytes) -> String, exn [ :utf8(Int) ](Json.encodeencode: pure fn('a: Encode)(value: Book.Records.ProjectingRecords.AccountPatch) -> Bytes, exn [ :encode(Wire.EncodeError) ](emptyempty: Book.Records.ProjectingRecords.AccountPatch)) == "{}""{}": String;
expect ✓must Json.decodedecode: pure fn('target: Decode)(body: Bytes, target: Struct(Book.Records.ProjectingRecords.AccountPatch), ~max_depth: Int = 128) -> Book.Records.ProjectingRecords.AccountPatch, exn [ :decode(Wire.DecodeError) ](Bytes.of_stringof_string: pure fn!(string: String) -> Bytes("{}""{}": String), AccountPatch) == emptyempty: Book.Records.ProjectingRecords.AccountPatch;
let renamerename: Book.Records.ProjectingRecords.AccountPatch = AccountPatch { display_name = "Grace""Grace": String; };
expect ✓must String.of_bytesof_bytes: pure fn(bytes: Bytes) -> String, exn [ :utf8(Int) ](Json.encodeencode: pure fn('a: Encode)(value: Book.Records.ProjectingRecords.AccountPatch) -> Bytes, exn [ :encode(Wire.EncodeError) ](renamerename: Book.Records.ProjectingRecords.AccountPatch)) == "{\"display_name\":\"Grace\"}""{\"display_name\":\"Grace\"}": String;
expect ✓must Json.decodedecode: pure fn('target: Decode)(body: Bytes, target: Struct(Book.Records.ProjectingRecords.AccountPatch), ~max_depth: Int = 128) -> Book.Records.ProjectingRecords.AccountPatch, exn [ :decode(Wire.DecodeError) ](
Bytes.of_stringof_string: pure fn!(string: String) -> Bytes("{\"contact_email\":\"ada@example.com\"}""{\"contact_email\":\"ada@example.com\"}": String),
AccountPatch,
) == AccountPatch { email = "ada@example.com""ada@example.com": String; };Source field defaults do not carry into a sparse projection: omission is data. Other field metadata, such as the email wire name above, remains.
Sparse Drops Defaults
let emptyempty: Book.Records.ProjectingRecords.AccountPatch = must Json.decodedecode: pure fn('target: Decode)(body: Bytes, target: Struct(Book.Records.ProjectingRecords.AccountPatch), ~max_depth: Int = 128) -> Book.Records.ProjectingRecords.AccountPatch, exn [ :decode(Wire.DecodeError) ](Bytes.of_stringof_string: pure fn!(string: String) -> Bytes("{}""{}": String), AccountPatch);
expect ✓emptyempty: Book.Records.ProjectingRecords.AccountPatch.display_namedisplay_name: Option(String) == NoneNone: Option(String);Projecting sparse sources
Projecting a sparse record preserves sparse construction and stored Option fields.
type AccountChanges = sparse {
display_name: String;
active: Bool;
internal_note: String;
}
type PublicAccountChanges = Project(AccountChanges, Fields(.display_name, .active))
Sparse Projection
let emptyempty: Book.Records.ProjectingRecords.PublicAccountChanges = PublicAccountChanges {};
✓emptyempty: Book.Records.ProjectingRecords.PublicAccountChanges.display_namedisplay_name: Option(String): Option(String);
let renamerename: Book.Records.ProjectingRecords.PublicAccountChanges = PublicAccountChanges { display_name = "Grace""Grace": String; };
expect ✓emptyempty: Book.Records.ProjectingRecords.PublicAccountChanges == PublicAccountChanges {};
expect ✓renamerename: Book.Records.ProjectingRecords.PublicAccountChanges == PublicAccountChanges { display_name = "Grace""Grace": String; };Composing named shapes
A projected record is an ordinary named product and may be projected again.
type AccountLabel = Project(AccountCard, Fields(.display_name))
Composed Projection
let labellabel: Book.Records.ProjectingRecords.AccountLabel = AccountLabel { display_name = "Ada""Ada": String; };
expect ✓Display.displaydisplay: pure fn!('a: Display)(value: Book.Records.ProjectingRecords.AccountLabel) -> String(labellabel: Book.Records.ProjectingRecords.AccountLabel) == "AccountLabel { display_name = Ada }""AccountLabel { display_name = Ada }": String;Generic products
A projection may select fields from a fully applied generic product. The result declares its own parameters explicitly, like every generic type.
type Envelope('a) = {
request_id: Int;
value: 'a;
}
type Payload('a) = Project(Envelope('a), Fields(.value))
type PayloadPatch('a) = sparse Project(Envelope('a), Fields(.value))
type StringEnvelope = Project(Envelope(String), Fields(.request_id, .value))
Generic Projection
let payloadpayload: Book.Records.ProjectingRecords.Payload(String): Payload(String) = Payload { value = "ship""ship": String; };
✓payloadpayload: Book.Records.ProjectingRecords.Payload(String).valuevalue: String: String;
let patchpatch: Book.Records.ProjectingRecords.PayloadPatch(String): PayloadPatch(String) = PayloadPatch {};
✓patchpatch: Book.Records.ProjectingRecords.PayloadPatch(String).valuevalue: Option(String): Option(String);
let envelopeenvelope: Book.Records.ProjectingRecords.StringEnvelope = StringEnvelope { request_id = 77: Int; value = "ship""ship": String; };
expect ✓envelopeenvelope: Book.Records.ProjectingRecords.StringEnvelope == StringEnvelope { request_id = 77: Int; value = "ship""ship": String; };