Ship

Book · Projecting records

Projecting records

Draft

Materialize a focused nominal record from selected fields.

On this page · 7 sections

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

test✓
  ✓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

test✓
  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

test✓
  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

test✓
  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

test✓
  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

test✓
  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

test✓
  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

test✓
  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; };