refactor: use panproto combinators with dependent optics for view types Replace elementary::drop_sort/add_sort/rename_sort with high-level combinators::remove_field, add_field, rename_field, pipeline. add_field correctly decomposes to add_sort + add_op (creates prop edge). rename_field uses rename_edge_name dependent optic (fiber-level rename). pipeline composes all steps into a single ProtolensChain.
Author: Aaron Steven White
Commit
e73e906c02e4cba8f6c77bd588a540b07dd13fcbParent: 04cb1d84a0
Structural diff unavailable
These commits were pushed via plain git push, so no pre-parsed
schemas are available. Install git-remote-cospan and re-push via panproto:// to
see scope-level changes, breaking change detection, and semantic diffs.
brew install panproto/tap/git-remote-cospan2 files changed +99 -93
@@ -1,8 +1,8 @@
1-// Auto-generated by cospan-codegen via panproto Protolens. 1+// Auto-generated by cospan-codegen via panproto protolens combinators. 22 // Source: Lexicon schemas transformed through DB projection lens. 33 // Do not edit manually. 44 5-// dev.cospan.node (via Protolens) 5+// dev.cospan.node (via panproto combinators) 66 export interface NodeView { 77 did: string; 88 rkey: string;
@@ -21,7 +21,7 @@ export function normalizeNodeView(raw: Partial<NodeView>): NodeView {
2121 }; 2222 } 2323 24-// dev.cospan.actor.profile (via Protolens) 24+// dev.cospan.actor.profile (via panproto combinators) 2525 export interface ActorProfileView { 2626 did: string; 2727 bluesky: string;
@@ -42,7 +42,7 @@ export function normalizeActorProfileView(raw: Partial<ActorProfileView>): Actor
4242 }; 4343 } 4444 45-// dev.cospan.repo (via Protolens) 45+// dev.cospan.repo (via panproto combinators) 4646 export interface RepoView { 4747 did: string; 4848 rkey: string;
@@ -87,7 +87,7 @@ export function normalizeRepoView(raw: Partial<RepoView>): RepoView {
8787 }; 8888 } 8989 90-// dev.cospan.vcs.refUpdate (via Protolens) 90+// dev.cospan.vcs.refUpdate (via panproto combinators) 9191 export interface RefUpdateView { 9292 rkey: string; 9393 committerDid: string;
@@ -126,7 +126,7 @@ export function normalizeRefUpdateView(raw: Partial<RefUpdateView>): RefUpdateVi
126126 }; 127127 } 128128 129-// dev.cospan.repo.issue (via Protolens) 129+// dev.cospan.repo.issue (via panproto combinators) 130130 export interface IssueView { 131131 did: string; 132132 rkey: string;
@@ -155,7 +155,7 @@ export function normalizeIssueView(raw: Partial<IssueView>): IssueView {
155155 }; 156156 } 157157 158-// dev.cospan.repo.issue.comment (via Protolens) 158+// dev.cospan.repo.issue.comment (via panproto combinators) 159159 export interface IssueCommentView { 160160 did: string; 161161 rkey: string;
@@ -174,7 +174,7 @@ export function normalizeIssueCommentView(raw: Partial<IssueCommentView>): Issue
174174 }; 175175 } 176176 177-// dev.cospan.repo.issue.state (via Protolens) 177+// dev.cospan.repo.issue.state (via panproto combinators) 178178 export interface IssueStateView { 179179 did: string; 180180 rkey: string;
@@ -195,7 +195,7 @@ export function normalizeIssueStateView(raw: Partial<IssueStateView>): IssueStat
195195 }; 196196 } 197197 198-// dev.cospan.repo.pull (via Protolens) 198+// dev.cospan.repo.pull (via panproto combinators) 199199 export interface PullView { 200200 did: string; 201201 rkey: string;
@@ -230,7 +230,7 @@ export function normalizePullView(raw: Partial<PullView>): PullView {
230230 }; 231231 } 232232 233-// dev.cospan.repo.pull.comment (via Protolens) 233+// dev.cospan.repo.pull.comment (via panproto combinators) 234234 export interface PullCommentView { 235235 did: string; 236236 rkey: string;
@@ -251,7 +251,7 @@ export function normalizePullCommentView(raw: Partial<PullCommentView>): PullCom
251251 }; 252252 } 253253 254-// dev.cospan.repo.pull.state (via Protolens) 254+// dev.cospan.repo.pull.state (via panproto combinators) 255255 export interface PullStateView { 256256 did: string; 257257 rkey: string;
@@ -272,7 +272,7 @@ export function normalizePullStateView(raw: Partial<PullStateView>): PullStateVi
272272 }; 273273 } 274274 275-// dev.cospan.feed.star (via Protolens) 275+// dev.cospan.feed.star (via panproto combinators) 276276 export interface StarView { 277277 did: string; 278278 rkey: string;
@@ -291,7 +291,7 @@ export function normalizeStarView(raw: Partial<StarView>): StarView {
291291 }; 292292 } 293293 294-// dev.cospan.feed.reaction (via Protolens) 294+// dev.cospan.feed.reaction (via panproto combinators) 295295 export interface ReactionView { 296296 did: string; 297297 rkey: string;
@@ -312,7 +312,7 @@ export function normalizeReactionView(raw: Partial<ReactionView>): ReactionView
312312 }; 313313 } 314314 315-// dev.cospan.graph.follow (via Protolens) 315+// dev.cospan.graph.follow (via panproto combinators) 316316 export interface FollowView { 317317 did: string; 318318 rkey: string;
@@ -331,7 +331,7 @@ export function normalizeFollowView(raw: Partial<FollowView>): FollowView {
331331 }; 332332 } 333333 334-// dev.cospan.label.definition (via Protolens) 334+// dev.cospan.label.definition (via panproto combinators) 335335 export interface LabelView { 336336 did: string; 337337 rkey: string;
@@ -358,7 +358,7 @@ export function normalizeLabelView(raw: Partial<LabelView>): LabelView {
358358 }; 359359 } 360360 361-// dev.cospan.org (via Protolens) 361+// dev.cospan.org (via panproto combinators) 362362 export interface OrgView { 363363 did: string; 364364 rkey: string;
@@ -381,7 +381,7 @@ export function normalizeOrgView(raw: Partial<OrgView>): OrgView {
381381 }; 382382 } 383383 384-// dev.cospan.org.member (via Protolens) 384+// dev.cospan.org.member (via panproto combinators) 385385 export interface OrgMemberView { 386386 did: string; 387387 rkey: string;
@@ -400,7 +400,7 @@ export function normalizeOrgMemberView(raw: Partial<OrgMemberView>): OrgMemberVi
400400 }; 401401 } 402402 403-// dev.cospan.repo.collaborator (via Protolens) 403+// dev.cospan.repo.collaborator (via panproto combinators) 404404 export interface CollaboratorView { 405405 did: string; 406406 rkey: string;
@@ -423,7 +423,7 @@ export function normalizeCollaboratorView(raw: Partial<CollaboratorView>): Colla
423423 }; 424424 } 425425 426-// dev.cospan.repo.dependency (via Protolens) 426+// dev.cospan.repo.dependency (via panproto combinators) 427427 export interface DependencyView { 428428 did: string; 429429 rkey: string;
@@ -456,7 +456,7 @@ export function normalizeDependencyView(raw: Partial<DependencyView>): Dependenc
456456 }; 457457 } 458458 459-// dev.cospan.pipeline (via Protolens) 459+// dev.cospan.pipeline (via panproto combinators) 460460 export interface PipelineView { 461461 did: string; 462462 rkey: string;
@@ -1,72 +1,89 @@
1-//! Emit TypeScript view types by applying panproto Protolens transforms to Lexicon schemas. 1+//! Emit TypeScript view types by applying panproto protolens combinators to Lexicon schemas. 22 //! 3-//! The DB projection lens is expressed as elementary Protolens transforms: 4-//! - drop_sort("{body}.{field}") for skip_fields 5-//! - add_sort("{body}.repoDid") + add_op for uri_decompositions 6-//! - rename_sort("{body}.{old}", "{body}.{new}") for field_renames 7-//! - add_sort("{body}.{col}") for extra_columns 3+//! The DB projection lens is built from high-level combinators (v0.23.0): 4+//! - combinators::remove_field for skip_fields 5+//! - combinators::add_field for uri_decompositions and extra_columns 6+//! - combinators::rename_field for uri_storages and field_renames (uses rename_edge_name dependent optic) 7+//! - combinators::pipeline to compose all steps 88 //! 9-//! These are composed via vertical_compose, applied to the source Lexicon schema 10-//! via target_schema(), and the target schema is walked to emit TypeScript. 9+//! The pipeline is instantiated against the source Lexicon schema to produce 10+//! the target (view) schema, which is walked to emit TypeScript interfaces. 1111 1212 use panproto_gat::Name; 1313 use panproto_inst::value::Value; 14-use panproto_lens::{ 15- elementary, protolens_vertical as vertical_compose, Protolens, ProtolensChain, 16-}; 14+use panproto_lens::{combinators, ProtolensChain}; 1715 use panproto_protocols::emit::children_by_edge; 1816 use panproto_schema::{Protocol, Schema}; 1917 2018 use crate::record_config::RecordConfig; 2119 22-/// Build a ProtolensChain from RecordConfig lens operations, scoped to a specific body vertex. 20+/// Build a ProtolensChain from RecordConfig using panproto combinators. 21+/// 22+/// Each RecordConfig operation maps to a combinator: 23+/// - skip_fields → combinators::remove_field(vertex) 24+/// - uri_decompositions → remove_field(source) + add_field(did) + add_field(name) 25+/// - uri_storages → combinators::rename_field(parent, field, old, new) 26+/// - field_renames → combinators::rename_field(parent, field, old, new) 27+/// - type_overrides → remove_field + add_field (with correct kind) 28+/// - extra_columns → combinators::add_field(parent, name, kind, default) 2329 fn build_lens_chain(body_id: &str, config: &RecordConfig) -> ProtolensChain { 24- let mut steps: Vec<Protolens> = Vec::new(); 30+ let mut chains: Vec<ProtolensChain> = Vec::new(); 2531 26- // 1. Drop skipped fields: remove vertex "{body}.{field}" and its edges 32+ // 1. Remove skipped fields 2733 for field in config.skip_fields { 2834 let vertex_id = format!("{body_id}.{field}"); 29- steps.push(elementary::drop_sort(vertex_id)); 35+ chains.push(combinators::remove_field(vertex_id)); 3036 } 3137 32- // 2. URI decompositions: drop source field, add decomposed fields (camelCase) 38+ // 2. URI decompositions: remove source, add decomposed fields 3339 for decomp in config.uri_decompositions { 3440 let source_vertex = format!("{body_id}.{}", decomp.source_field); 35- steps.push(elementary::drop_sort(source_vertex)); 41+ chains.push(combinators::remove_field(source_vertex)); 3642 3743 let did_camel = snake_to_camel(decomp.did_column); 3844 let did_vertex = format!("{body_id}.{did_camel}"); 39- steps.push(elementary::add_sort( 45+ chains.push(combinators::add_field( 46+ body_id, 4047 did_vertex, 4148 "string", 4249 Value::Str(String::new()), 4350 )); 51+ 4452 let name_camel = snake_to_camel(decomp.name_column); 4553 let name_vertex = format!("{body_id}.{name_camel}"); 46- steps.push(elementary::add_sort( 54+ chains.push(combinators::add_field( 55+ body_id, 4756 name_vertex, 4857 "string", 4958 Value::Str(String::new()), 5059 )); 5160 } 5261 53- // 3. URI storages: rename source field to camelCase column name 62+ // 3. URI storages: rename field via dependent optic (rename_edge_name) 5463 for storage in config.uri_storages { 55- let old_vertex = format!("{body_id}.{}", storage.source_field); 64+ let field_vertex = format!("{body_id}.{}", storage.source_field); 5665 let new_camel = snake_to_camel(storage.column_name); 57- let new_vertex = format!("{body_id}.{new_camel}"); 58- steps.push(elementary::rename_sort(old_vertex, new_vertex)); 66+ chains.push(combinators::rename_field( 67+ body_id, 68+ field_vertex, 69+ storage.source_field, 70+ &*new_camel, 71+ )); 5972 } 6073 61- // 4. Field renames (to camelCase) 74+ // 4. Field renames via dependent optic 6275 for rename in config.field_renames { 63- let old_vertex = format!("{body_id}.{}", rename.source_field); 76+ let field_vertex = format!("{body_id}.{}", rename.source_field); 6477 let new_camel = snake_to_camel(rename.column_name); 65- let new_vertex = format!("{body_id}.{new_camel}"); 66- steps.push(elementary::rename_sort(old_vertex, new_vertex)); 78+ chains.push(combinators::rename_field( 79+ body_id, 80+ field_vertex, 81+ rename.source_field, 82+ &*new_camel, 83+ )); 6784 } 6885 69- // 5. Type overrides: drop vertex with wrong kind, re-add with correct kind 86+ // 5. Type overrides: remove + add with correct kind 7087 for ovr in config.type_overrides { 7188 let vertex_id = format!("{body_id}.{}", ovr.source_field); 7289 let kind = match ovr.rust_type {
@@ -75,10 +92,7 @@ fn build_lens_chain(body_id: &str, config: &RecordConfig) -> ProtolensChain {
7592 t if t.contains("bool") => "boolean", 7693 _ => "string", 7794 }; 78- let is_optional = ovr.rust_type.starts_with("Option<"); 79- // Drop the original vertex and re-add with correct kind 80- steps.push(elementary::drop_sort(vertex_id.clone())); 81- let default = if is_optional { 95+ let default = if ovr.rust_type.starts_with("Option<") { 8296 Value::Null 8397 } else { 8498 match kind {
@@ -88,11 +102,11 @@ fn build_lens_chain(body_id: &str, config: &RecordConfig) -> ProtolensChain {
88102 _ => Value::Str(String::new()), 89103 } 90104 }; 91- steps.push(elementary::add_sort(vertex_id, kind, default)); 105+ chains.push(combinators::remove_field(vertex_id.clone())); 106+ chains.push(combinators::add_field(body_id, vertex_id, kind, default)); 92107 } 93108 94- // 6. Extra columns (state, comment_count, etc.) 95- // Use camelCase for vertex IDs to match serde(rename_all = "camelCase") 109+ // 6. Extra columns via add_field combinator 96110 for extra in config.extra_columns { 97111 let camel = snake_to_camel(extra.name); 98112 let vertex_id = format!("{body_id}.{camel}");
@@ -102,10 +116,10 @@ fn build_lens_chain(body_id: &str, config: &RecordConfig) -> ProtolensChain {
102116 "bool" => ("boolean", Value::Bool(false)), 103117 _ => ("string", Value::Str(String::new())), 104118 }; 105- steps.push(elementary::add_sort(vertex_id, kind, default)); 119+ chains.push(combinators::add_field(body_id, vertex_id, kind, default)); 106120 } 107121 108- ProtolensChain::new(steps) 122+ combinators::pipeline(chains) 109123 } 110124 111125 /// Apply the lens chain to a source schema, producing the target (view) schema.
@@ -115,10 +129,7 @@ fn apply_lens(source: &Schema, nsid: &str, config: &RecordConfig) -> Schema {
115129 let protocol = Protocol::default(); 116130 117131 match chain.instantiate(source, &protocol) { 118- Ok(lens) => { 119- // The lens contains the target schema 120- lens.tgt_schema 121- } 132+ Ok(lens) => lens.tgt_schema, 122133 Err(e) => { 123134 eprintln!(" warn: lens instantiation for {nsid}: {e:?}, using source"); 124135 source.clone()
@@ -138,7 +149,7 @@ fn emit_view_from_target(target: &Schema, nsid: &str, config: &RecordConfig) ->
138149 let body_id = find_record_body(target, nsid); 139150 140151 let mut out = String::new(); 141- out.push_str(&format!("// {nsid} (via Protolens)\n")); 152+ out.push_str(&format!("// {nsid} (via panproto combinators)\n")); 142153 out.push_str(&format!("export interface {view_name} {{\n")); 143154 144155 // Standard ATProto columns
@@ -166,13 +177,11 @@ fn emit_view_from_target(target: &Schema, nsid: &str, config: &RecordConfig) ->
166177 } 167178 } 168179 169- // Extra columns added by the lens (they appear as new vertices, not as prop edges) 170- // Walk vertices that start with body_id but aren't in the prop edges 171- let prop_targets: std::collections::HashSet<String> = props 172- .iter() 173- .map(|(_, v)| v.id.to_string()) 174- .collect(); 180+ // Fields added by add_field combinator (they have prop edges now!) 181+ // Walk vertices that are children of body but not in the original prop list 175182 let body_prefix = format!("{body_id}."); 183+ let prop_targets: std::collections::HashSet<String> = 184+ props.iter().map(|(_, v)| v.id.to_string()).collect(); 176185 let mut extra_vertices: Vec<_> = target 177186 .vertices 178187 .iter()
@@ -189,7 +198,6 @@ fn emit_view_from_target(target: &Schema, nsid: &str, config: &RecordConfig) ->
189198 for (id, v) in &extra_vertices { 190199 let field_name = id.as_str().strip_prefix(&body_prefix).unwrap_or(id.as_str()); 191200 let ts_type = vertex_kind_to_ts(&v.kind); 192- // Extra columns from add_sort are always present (have defaults) 193201 out.push_str(&format!(" {field_name}: {ts_type};\n")); 194202 } 195203
@@ -239,7 +247,7 @@ fn emit_list_response(type_name: &str, wrapper_key: &str) -> String {
239247 /// Emit the full generated TypeScript views file. 240248 pub fn emit_all_views(schemas: &[(Schema, String)], configs: &[RecordConfig]) -> String { 241249 let mut out = String::new(); 242- out.push_str("// Auto-generated by cospan-codegen via panproto Protolens.\n"); 250+ out.push_str("// Auto-generated by cospan-codegen via panproto protolens combinators.\n"); 243251 out.push_str("// Source: Lexicon schemas transformed through DB projection lens.\n"); 244252 out.push_str("// Do not edit manually.\n\n"); 245253
@@ -275,7 +283,7 @@ pub fn emit_all_views(schemas: &[(Schema, String)], configs: &[RecordConfig]) ->
275283 } 276284 277285 // --------------------------------------------------------------------------- 278-// Schema helpers — these use panproto's schema structure, not string munging 286+// Schema helpers 279287 // --------------------------------------------------------------------------- 280288 281289 fn find_record_body(schema: &Schema, nsid: &str) -> String {
@@ -301,26 +309,7 @@ fn is_field_required(schema: &Schema, body_id: &str, field_name: &str) -> bool {
301309 .unwrap_or(false) 302310 } 303311 304-fn snake_to_camel(s: &str) -> String { 305- let mut result = String::new(); 306- let mut capitalize_next = false; 307- for (i, c) in s.chars().enumerate() { 308- if c == '_' { 309- capitalize_next = true; 310- } else if capitalize_next { 311- result.push(c.to_ascii_uppercase()); 312- capitalize_next = false; 313- } else if i == 0 { 314- result.push(c.to_ascii_lowercase()); 315- } else { 316- result.push(c); 317- } 318- } 319- result 320-} 321- 322312 /// Map panproto vertex kind → TypeScript type. 323-/// The vertex kind IS the panproto type — no Rust type string mapping. 324313 fn vertex_kind_to_ts(kind: &Name) -> &'static str { 325314 match kind.as_str() { 326315 "string" | "cid-link" | "ref" | "token" | "bytes" => "string",
@@ -329,7 +318,6 @@ fn vertex_kind_to_ts(kind: &Name) -> &'static str {
329318 "array" => "unknown[]", 330319 "object" | "union" => "Record<string, unknown>", 331320 _ => { 332- // Log unhandled kinds during codegen for debugging 333321 eprintln!(" warn: unhandled vertex kind '{}', mapping to unknown", kind); 334322 "unknown" 335323 }
@@ -351,9 +339,27 @@ fn default_for_kind(kind: &Name, field_name: &str, config: &RecordConfig) -> &'s
351339 } 352340 } 353341 match kind.as_str() { 354- "string" | "cid-link" | "ref" | "token" => "''", 355- "integer" | "number" => "0", 342+ "string" | "cid-link" | "ref" | "token" | "bytes" => "''", 343+ "integer" | "number" | "float" => "0", 356344 "boolean" => "false", 357345 _ => "''", 358346 } 359347 } 348+ 349+fn snake_to_camel(s: &str) -> String { 350+ let mut result = String::new(); 351+ let mut capitalize_next = false; 352+ for (i, c) in s.chars().enumerate() { 353+ if c == '_' { 354+ capitalize_next = true; 355+ } else if capitalize_next { 356+ result.push(c.to_ascii_uppercase()); 357+ capitalize_next = false; 358+ } else if i == 0 { 359+ result.push(c.to_ascii_lowercase()); 360+ } else { 361+ result.push(c); 362+ } 363+ } 364+ result 365+}