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 e73e906c02e4cba8f6c77bd588a540b07dd13fcb
Parent: 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-cospan
2 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+}
cospan · schematic version control on atproto built on AT Protocol