I've been fairly clear at various points that a big part of the reason I wrote Jacquard in the first place was because I was trying to write something else, but got frustrated with the existing Rust atproto ecosystem, and as a result wasn't really getting much done on the "something else" (that being Weaver, what you're reading this on). And it's been pretty great for that, as evidenced by the fact that you can read this on Weaver and I can write it on there (I could even, with a bit of finagling, have someone collaborate with me on this post, though I don't want to say too much more as there are still a number of things about that workflow in need of substantial refinement, though technically-speaking the live alpha.weaver.sh site has the current latest code, including real-time Google Docs-style collaboration.
But it wasn't just that, like I said to Cameron here. I'm an artist as much as an engineer sometimes, even about my programming, and sometimes you do stuff to make a point and prove that a thing is possible.
To be clear, I mean no disrespect toward Letta and the people who work there, they do cool stuff and their API SDKs are pretty average of those that I've used (the dubious honour of worst SDK I've used probably goes to the Chirpstack SDK, which is mostly just generated Protobuf+tonic bindings, and is, along with some unfortunate experiences in Android development, the reason I perhaps unfairly maligned that serialization format for a while), but nonetheless their client SDKs are generated code with the typical problems of that. My biggest atproto project before Weaver was in Kotlin, an Android app. I built it off of some code generation bindings someone else had made at the time, because it was the only thing available in the language. And one thing that I rapidly found with the generated bindings was that I needed to immediately write a bunch of wrapper code to make them actually usable for my purposes. Some of that is down to oddities of atproto, like how PostView's internal record field (which is always an app.bsky.feed.post) is marked, per the spec as type Unknown in the lexicon, meaning it requires a second stage of deserialization to get the typed data out of it, but a lot was down to the simple fact that the generated API bindings were extremely verbose, with some frustrating wrapper layers, and had zero real affordances around them that didn't require more or less writing wrappers for everything you needed to use regularly. This is pretty typical for generated bindings, I've found, and leads to problems, where you can tell when people don't really use the library code they write for anything substantial, at least not directly, or if they do, it feels like they didn't think things through first.
Finding the pain points
Writing by hand to a spec in the absence of a nontrivial production implementation that depends on your code is also not ideal. Here's an example from Jacquard. I initially wrote the handle validation to the spec exactly. This is nominally what the Typescript library does (though this is one spot where JavaScript leniency helps it), and it's what ATrium did. However, it's not uncommon in practice in an atproto app to have at least the occasional person with an invalid handle, indicated in Bluesky appview API responses by the handle itself being replaced with handle.invalid. This handle, perhaps obviously, fails the handle validation per the spec. And in Rust with serde that means you literally cannot deserialize an entire timeline API response because one post's author has an invalid handle. This is obviously not ideal. And the alternative would be handles either just being bare strings, or the handle container maybe not containing a handle without explicitly opting into that possibility (every atproto string type in Jacquard has unsafe constructors that don't perform the checks). In the end, I literally special-cased handle.invalid in the validation and added an additional validation function.
I have a whole list of things like this, from users of Jacquard and now from my own work on Weaver.
So there's as per usual a bit of a happy, or at least effective for making a good open-source library, medium. Where you do real stuff with the code you expect other people to use, but your use isn't so specific and dominant that it ends up skewing the code. But equally, good library design from the get-go can go a long way toward minimizing this sort of problem. I knew when I wrote Jacquard that I'd be inevitably working with a certain subset of its features as fit my needs, and I also knew that many people would be using it to do Bluesky stuff, and I didn't want either my idiosyncratic needs for Weaver or the dominance of Bluesky in the ecosystem to result in other stuff getting left by the wayside. That meant leaning hard on code generation, the thing I'd gotten so frustrated with as a user of other people's libraries, and that meant figuring out how to make generated code you could actually use as is.
I think I succeeded pretty well here. People report that Jacquard is extremely easy to use, once you wrap your head around the borrowed deserialization stuff. Having that single main entry point of .send() for making API calls is a big part of that, I think. It means that, aside from constructing appropriate data (and here generated builders patterned after the bon crate help a lot), you interact almost entirely with code someone designed and wrote, despite using generated API bindings and send() itself knowing nothing about what you're sending beyond what the type system tells it.
Weaver
The vast majority of 'wrapping' Weaver does of Jacquard is to do specific multi-step stuff on top of an API call (as Weaver currently is an "appview-less atproto app", it does this to produce 'view' types suitable for display that the indexer/appview will ultimately provide as that gets built, assembling data from multiple calls to Constellation, Slingshot, UFOs, and PDSs, as well as the Bluesky appview) and almost never just to wrap a single API call. Weaver also uses Jacquard's XrpcRequest derive macro to allow the constellation.microcosm.blue API to be used like any other atproto XRPC API with the library, without having to write out the JSON and run code generation. And that's exactly as intended.
let updated_at = record
.value
.query
.first
.or_else
.and_then
.and_then;
Ease of prototyping is another part of the story behind the suite of "work with freeform atproto data" tools in Jacquard. I knew that Rust's own rigorous type system could all too easily make working with data that only sort of fit the lexicon spec awkward or even impossible, and that there were levels of granularity about which you'd care about validity or shape. For example, the above function in weaver's collaboration code explicitly uses loosely typed values and the query methods because it needs to work with essentially any record that has the right fields, not just
sh.weaver.notebook.entry. Having easy access to those fields without having to know anything else about the data is incredibly powerful.
Some other pain points I found that I'm going to fix were the lack of a nice at-uri constructor that takes the individual components, some missing .as_str() or .as_ref() methods, Display impls, some stuff around sessions, in particular a clean way to abstract over unauthenticated and authenticated sessions, a bunch of pain points with dioxus server functions and Jacquard's borrowed serialization, the possible need for a way to specify a different base/host URL for an XRPC API call (not just the bit after /xrpc/) at a type/trait level or at least persistently without it affecting other functions (it would also be nice to specify if a request needs auth that way, but that's much harder, as it's not encoded in the lexicons), and the need for an http client with a built-in general cache layer. Also monomorphization is hell on wasm binary size, but that's a whole other kettle of fish.
Anyhow, I'm glad I made Jacquard, that people are finding it useful, I think I proved my point, and I still wish someone would look at what I'm doing there and here and decide that I'm worth paying real amounts of money to, but baby steps.