Apply a lens
A lens is a structure-preserving translation between two schemas.
On the wire it is a dev.panproto.schema.lens record (re-exported
by idiolect-records as PanprotoLens); at runtime it is a
panproto_lens::Lens instantiated against a Schema graph.
idiolect-lens bridges the
two: it resolves a lens record by at-uri, loads both schemas, and
runs the lens.
What a lens does
A lens has a forward direction and a backward direction:
get translates a source record A into a target view B plus a
complement (the data that the projection
discarded). put reconstructs A from a (possibly modified) B
and the complement. The two directions obey the GetPut and PutGet
laws covered in Lens semantics and laws.
Wire it up
idiolect-lens is publish = false; depend on it via git (or a
path, when working inside the workspace):
# in Cargo.toml
idiolect-lens = { git = "https://github.com/idiolect-dev/idiolect", tag = "v0.10.0", features = ["pds-reqwest"] }
panproto-schema = { git = "https://github.com/panproto/panproto.git", tag = "v0.39.0" }
tokio = { version = "1", features = ["full"] }
The lens runtime needs three pieces: a Resolver (fetches the
lens record by at-uri), a SchemaLoader (turns the
dev.panproto.schema.schema at-uris on the lens record into
typed panproto_schema::Schema values), and a Protocol. v0.9
ships PdsSchemaLoader to pair with PdsResolver; both share a
ReqwestPdsClient.
src/main.rs:
use idiolect_lens::{ apply_lens, ApplyLensInput, AtUri, PdsResolver, PdsSchemaLoader, ReqwestPdsClient, }; use panproto_schema::Protocol; const PDS: &str = "https://jellybaby.us-east.host.bsky.network"; const LENS: &str = "at://did:plc:wdl4nnvxxdy4mc5vddxlm6f3/dev.panproto.schema.lens/tutorial-rename-sort-string-to-text"; #[tokio::main] async fn main() -> anyhow::Result<()> { let client = ReqwestPdsClient::with_service_url(PDS); let resolver = PdsResolver::new(client.clone()); let loader = PdsSchemaLoader::new(client); let protocol = Protocol::default(); let lens_uri = AtUri::parse(LENS)?; let source_record: serde_json::Value = serde_json::from_str(r#"{ "text": "hello, world" }"#)?; let out = apply_lens(&resolver, &loader, &protocol, ApplyLensInput { lens_uri, source_record, source_root_vertex: None, }).await?; println!("{}", serde_json::to_string_pretty(&out.target_record)?); Ok(()) }
Run it:
cargo run
{
"text": "hello, world"
}
The lens referenced above is real. The project DID has the three records the runtime needs to resolve it published on its PDS:
dev.panproto.schema.schema/tutorial-post-body-v1— a single-field "post:body" record with a stringtextchild.dev.panproto.schema.schema/tutorial-post-body-v2— the same shape with the kind relabelled totext.dev.panproto.schema.lens/tutorial-rename-sort-string-to-text— a single-steprename_sortchain. The optic class isIso; round-trip is byte-equal.
apply_lens is one async call. It does five things in order:
- Resolve the lens record from the PDS via
PdsResolver. - Load the source and target schemas from the schema loader.
- Instantiate the protolens (or protolens chain) against the source schema under the given protocol.
- Parse
source_recordinto a panproto w-type instance, project it throughget, and serialize the view back to JSON under the target schema. - Return the target record together with the complement.
The complement is a typed panproto_lens::Complement. Treat it
as an opaque token: store it next to the target view, hand it
back to apply_lens_put when you want to run the reverse
direction, do not edit it.
Reverse the direction
#![allow(unused)] fn main() { use idiolect_lens::{apply_lens_put, ApplyLensPutInput}; let back = apply_lens_put( &resolver, &loader, &protocol, ApplyLensPutInput { lens_uri: lens_uri.clone(), target_record: out.target_record, complement: out.complement, target_root_vertex: None, }, ) .await?; assert_eq!(back.source_record, source_record); }
If the lens is an isomorphism, put(get(a)) returns the original
a byte-for-byte. If it is a projection (information was dropped on
the way through), put reconstructs a from the target plus the
complement. If you modify the target between the calls, put
applies the modification on top of the original source.
What can go wrong
| Symptom | Cause |
|---|---|
LensError::NotFound | The lens at-uri did not resolve. Check the DID and the rkey. |
LensError::LexiconParse | The schema loader returned bytes that were not a valid panproto schema. |
LensError::Translate | The source record did not parse as an instance of the source schema. |
| Output complement is huge | The lens is closer to a projection than you thought. See Lens semantics. |
The runtime is Send-clean. You can hold an Arc<dyn Resolver> and
call apply_lens from inside an #[async_trait] handler in an
HTTP server.
The next chapter takes that lens and runs a verification against it.