···11+// @generated by jacquard-lexicon. DO NOT EDIT.
22+//
33+// This file was automatically generated from Lexicon schemas.
44+// Any manual changes will be overwritten on the next regeneration.
55+16pub mod embed;
27pub mod feed;
33-pub mod richtext;
88+pub mod richtext;
···11+// @generated by jacquard-lexicon. DO NOT EDIT.
22+//
33+// This file was automatically generated from Lexicon schemas.
44+// Any manual changes will be overwritten on the next regeneration.
55+16pub mod external;
27pub mod images;
38pub mod record;
49pub mod record_with_media;
55-pub mod video;
1010+pub mod video;
···11+// @generated by jacquard-lexicon. DO NOT EDIT.
22+//
33+// This file was automatically generated from Lexicon schemas.
44+// Any manual changes will be overwritten on the next regeneration.
55+16pub mod get_author_feed;
22-pub mod post;
77+pub mod post;
···11-pub mod facet;
11+// @generated by jacquard-lexicon. DO NOT EDIT.
22+//
33+// This file was automatically generated from Lexicon schemas.
44+// Any manual changes will be overwritten on the next regeneration.
55+66+pub mod facet;
···11+// @generated by jacquard-lexicon. DO NOT EDIT.
22+//
33+// Lexicon: app.bsky.richtext.facet
44+//
55+// This file was automatically generated from Lexicon schemas.
66+// Any manual changes will be overwritten on the next regeneration.
77+18///Specifies the sub-string range a facet feature applies to. Start index is inclusive, end index is exclusive. Indices are zero-indexed, counting bytes of the UTF-8 encoded text. NOTE: some languages, like Javascript, use UTF-16 or Unicode codepoints for string slice indexing; in these languages, convert to byte arrays before working with facets.
99+#[jacquard_derive::lexicon]
210#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
311#[serde(rename_all = "camelCase")]
412pub struct ByteSlice<'a> {
513 pub byte_end: i64,
614 pub byte_start: i64,
715}
1616+1717+impl jacquard_common::IntoStatic for ByteSlice<'_> {
1818+ type Output = ByteSlice<'static>;
1919+ fn into_static(self) -> Self::Output {
2020+ ByteSlice {
2121+ byte_end: self.byte_end.into_static(),
2222+ byte_start: self.byte_start.into_static(),
2323+ extra_data: self.extra_data.into_static(),
2424+ }
2525+ }
2626+}
2727+828///Facet feature for a URL. The text URL may have been simplified or truncated, but the facet reference should be a complete URL.
2929+#[jacquard_derive::lexicon]
930#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
1031#[serde(rename_all = "camelCase")]
1132pub struct Link<'a> {
3333+ #[serde(borrow)]
1234 pub uri: jacquard_common::types::string::Uri<'a>,
1335}
3636+3737+impl jacquard_common::IntoStatic for Link<'_> {
3838+ type Output = Link<'static>;
3939+ fn into_static(self) -> Self::Output {
4040+ Link {
4141+ uri: self.uri.into_static(),
4242+ extra_data: self.extra_data.into_static(),
4343+ }
4444+ }
4545+}
4646+1447///Annotation of a sub-string within rich text.
4848+#[jacquard_derive::lexicon]
1549#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
1650#[serde(rename_all = "camelCase")]
1751pub struct Facet<'a> {
5252+ #[serde(borrow)]
1853 pub features: Vec<jacquard_common::types::value::Data<'a>>,
1919- pub index: jacquard_common::types::value::Data<'a>,
5454+ #[serde(borrow)]
5555+ pub index: test_generated::app_bsky::richtext::facet::ByteSlice<'a>,
5656+}
5757+5858+impl jacquard_common::IntoStatic for Facet<'_> {
5959+ type Output = Facet<'static>;
6060+ fn into_static(self) -> Self::Output {
6161+ Facet {
6262+ features: self.features.into_static(),
6363+ index: self.index.into_static(),
6464+ extra_data: self.extra_data.into_static(),
6565+ }
6666+ }
2067}
6868+2169///Facet feature for mention of another account. The text is usually a handle, including a '@' prefix, but the facet reference is a DID.
7070+#[jacquard_derive::lexicon]
2271#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
2372#[serde(rename_all = "camelCase")]
2473pub struct Mention<'a> {
7474+ #[serde(borrow)]
2575 pub did: jacquard_common::types::string::Did<'a>,
2676}
7777+7878+impl jacquard_common::IntoStatic for Mention<'_> {
7979+ type Output = Mention<'static>;
8080+ fn into_static(self) -> Self::Output {
8181+ Mention {
8282+ did: self.did.into_static(),
8383+ extra_data: self.extra_data.into_static(),
8484+ }
8585+ }
8686+}
8787+2788///Facet feature for a hashtag. The text usually includes a '#' prefix, but the facet reference should not (except in the case of 'double hash tags').
8989+#[jacquard_derive::lexicon]
2890#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
2991#[serde(rename_all = "camelCase")]
3092pub struct Tag<'a> {
9393+ #[serde(borrow)]
3194 pub tag: jacquard_common::CowStr<'a>,
3295}
9696+9797+impl jacquard_common::IntoStatic for Tag<'_> {
9898+ type Output = Tag<'static>;
9999+ fn into_static(self) -> Self::Output {
100100+ Tag {
101101+ tag: self.tag.into_static(),
102102+ extra_data: self.extra_data.into_static(),
103103+ }
104104+ }
105105+}
···11+// @generated by jacquard-lexicon. DO NOT EDIT.
22+//
33+// This file was automatically generated from Lexicon schemas.
44+// Any manual changes will be overwritten on the next regeneration.
55+16pub mod label;
22-pub mod repo;
77+pub mod repo;
···11-pub mod strong_ref;
11+// @generated by jacquard-lexicon. DO NOT EDIT.
22+//
33+// This file was automatically generated from Lexicon schemas.
44+// Any manual changes will be overwritten on the next regeneration.
55+66+pub mod strong_ref;
···11+// @generated by jacquard-lexicon. DO NOT EDIT.
22+//
33+// This file was automatically generated from Lexicon schemas.
44+// Any manual changes will be overwritten on the next regeneration.
55+16pub mod app_bsky;
22-pub mod com_atproto;
77+pub mod com_atproto;
+149-25
crates/jacquard/src/client.rs
···44use std::fmt::Display;
55use std::future::Future;
6677-pub use error::{ClientError, Result};
87use bytes::Bytes;
88+pub use error::{ClientError, Result};
99use http::{
1010 HeaderName, HeaderValue, Request,
1111 header::{AUTHORIZATION, CONTENT_TYPE, InvalidHeaderValue},
1212};
1313pub use response::Response;
1414-use serde::Serialize;
1414+1515+use jacquard_common::{
1616+ CowStr, IntoStatic,
1717+ types::{
1818+ string::{Did, Handle},
1919+ xrpc::{XrpcMethod, XrpcRequest},
2020+ },
2121+};
2222+2323+/// Implement HttpClient for reqwest::Client
2424+impl HttpClient for reqwest::Client {
2525+ type Error = reqwest::Error;
2626+2727+ async fn send_http(
2828+ &self,
2929+ request: Request<Vec<u8>>,
3030+ ) -> core::result::Result<http::Response<Vec<u8>>, Self::Error> {
3131+ // Convert http::Request to reqwest::Request
3232+ let (parts, body) = request.into_parts();
15331616-use jacquard_common::{CowStr, types::xrpc::{XrpcMethod, XrpcRequest}};
3434+ let mut req = self.request(parts.method, parts.uri.to_string()).body(body);
3535+3636+ // Copy headers
3737+ for (name, value) in parts.headers.iter() {
3838+ req = req.header(name.as_str(), value.as_bytes());
3939+ }
4040+4141+ // Send request
4242+ let resp = req.send().await?;
4343+4444+ // Convert reqwest::Response to http::Response
4545+ let mut builder = http::Response::builder().status(resp.status());
4646+4747+ // Copy headers
4848+ for (name, value) in resp.headers().iter() {
4949+ builder = builder.header(name.as_str(), value.as_bytes());
5050+ }
5151+5252+ // Read body
5353+ let body = resp.bytes().await?.to_vec();
5454+5555+ Ok(builder.body(body).expect("Failed to build response"))
5656+ }
5757+}
17581859pub trait HttpClient {
1960 type Error: std::error::Error + Display + Send + Sync + 'static;
···9813999140 // Add query parameters for Query methods
100141 if let XrpcMethod::Query = R::METHOD {
101101- if let Ok(qs) = serde_html_form::to_string(&request) {
102102- if !qs.is_empty() {
103103- uri.push('?');
104104- uri.push_str(&qs);
105105- }
142142+ let qs = serde_html_form::to_string(&request).map_err(error::EncodeError::from)?;
143143+ if !qs.is_empty() {
144144+ uri.push('?');
145145+ uri.push_str(&qs);
106146 }
107147 }
108148···139179 }
140180141181 // Serialize body for procedures
142142- let body = if let XrpcMethod::Procedure(encoding) = R::METHOD {
143143- if encoding == "application/json" {
144144- serde_json::to_vec(&request).map_err(error::EncodeError::Json)?
145145- } else {
146146- // For other encodings, we'd need different serialization
147147- vec![]
148148- }
182182+ let body = if let XrpcMethod::Procedure(_) = R::METHOD {
183183+ request.encode_body()?
149184 } else {
150185 vec![]
151186 };
152187188188+ // TODO: make this not panic
153189 let http_request = builder.body(body).expect("Failed to build HTTP request");
154190155191 // Send HTTP request
156156- let http_response = client.send_http(http_request).await.map_err(|e| {
157157- error::TransportError::Other(Box::new(e))
158158- })?;
192192+ let http_response = client
193193+ .send_http(http_request)
194194+ .await
195195+ .map_err(|e| error::TransportError::Other(Box::new(e)))?;
196196+197197+ let status = http_response.status();
198198+ let buffer = Bytes::from(http_response.into_body());
159199160160- // Check status
161161- if !http_response.status().is_success() {
200200+ // XRPC errors come as 400/401 with structured error bodies
201201+ // Other error status codes (404, 500, etc.) are generic HTTP errors
202202+ if !status.is_success() && !matches!(status.as_u16(), 400 | 401) {
162203 return Err(ClientError::Http(error::HttpError {
163163- status: http_response.status(),
164164- body: Some(Bytes::from(http_response.body().clone())),
204204+ status,
205205+ body: Some(buffer),
165206 }));
166207 }
167208168168- // Convert to Response
169169- let buffer = Bytes::from(http_response.into_body());
170170- Ok(Response::new(buffer))
209209+ // Response will parse XRPC errors for 400/401, or output for 2xx
210210+ Ok(Response::new(buffer, status))
211211+}
212212+213213+/// Session information from createSession
214214+#[derive(Debug, Clone)]
215215+pub struct Session {
216216+ pub access_jwt: CowStr<'static>,
217217+ pub refresh_jwt: CowStr<'static>,
218218+ pub did: Did<'static>,
219219+ pub handle: Handle<'static>,
220220+}
221221+222222+impl From<jacquard_api::com_atproto::server::create_session::CreateSessionOutput<'_>> for Session {
223223+ fn from(
224224+ output: jacquard_api::com_atproto::server::create_session::CreateSessionOutput<'_>,
225225+ ) -> Self {
226226+ Self {
227227+ access_jwt: output.access_jwt.into_static(),
228228+ refresh_jwt: output.refresh_jwt.into_static(),
229229+ did: output.did.into_static(),
230230+ handle: output.handle.into_static(),
231231+ }
232232+ }
233233+}
234234+235235+/// Authenticated XRPC client that includes session tokens
236236+pub struct AuthenticatedClient<C> {
237237+ client: C,
238238+ base_uri: CowStr<'static>,
239239+ session: Option<Session>,
240240+}
241241+242242+impl<C> AuthenticatedClient<C> {
243243+ /// Create a new authenticated client with a base URI
244244+ pub fn new(client: C, base_uri: CowStr<'static>) -> Self {
245245+ Self {
246246+ client,
247247+ base_uri: base_uri,
248248+ session: None,
249249+ }
250250+ }
251251+252252+ /// Set the session
253253+ pub fn set_session(&mut self, session: Session) {
254254+ self.session = Some(session);
255255+ }
256256+257257+ /// Get the current session
258258+ pub fn session(&self) -> Option<&Session> {
259259+ self.session.as_ref()
260260+ }
261261+262262+ /// Clear the session
263263+ pub fn clear_session(&mut self) {
264264+ self.session = None;
265265+ }
266266+}
267267+268268+impl<C: HttpClient> HttpClient for AuthenticatedClient<C> {
269269+ type Error = C::Error;
270270+271271+ fn send_http(
272272+ &self,
273273+ request: Request<Vec<u8>>,
274274+ ) -> impl Future<Output = core::result::Result<http::Response<Vec<u8>>, Self::Error>> {
275275+ self.client.send_http(request)
276276+ }
277277+}
278278+279279+impl<C: HttpClient> XrpcClient for AuthenticatedClient<C> {
280280+ fn base_uri(&self) -> CowStr<'_> {
281281+ self.base_uri.clone()
282282+ }
283283+284284+ async fn authorization_token(&self, is_refresh: bool) -> Option<AuthorizationToken<'_>> {
285285+ if is_refresh {
286286+ self.session
287287+ .as_ref()
288288+ .map(|s| AuthorizationToken::Bearer(s.refresh_jwt.clone()))
289289+ } else {
290290+ self.session
291291+ .as_ref()
292292+ .map(|s| AuthorizationToken::Bearer(s.access_jwt.clone()))
293293+ }
294294+ }
171295}