From b28cd0eb86e12596a6ffd4654ff4933378c3c5da Mon Sep 17 00:00:00 2001 From: Anders Eie <1128648+strykejern@users.noreply.github.com> Date: Thu, 10 Oct 2024 22:25:57 +0200 Subject: [PATCH 1/5] Add support for serde behind feature flag for insert and update --- Cargo.toml | 6 +++++ src/builder.rs | 68 +++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 70 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c8d28b2..7c9ed91 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,13 @@ edition = "2021" [dependencies] reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] } +serde_json = { version = "1.0.128", optional = true } +serde = { version = "1.0.210", optional = true } [dev-dependencies] json = "0.12" tokio = { version = "1", features = ["full"] } +serde = { version = "1.0.210", features = ["derive"] } + +[features] +serde = ["dep:serde_json", "dep:serde"] diff --git a/src/builder.rs b/src/builder.rs index fa4fa56..b4991da 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -419,14 +419,44 @@ impl Builder { /// .insert(r#"[{ "username": "soedirgo", "status": "online" }, /// { "username": "jose", "status": "offline" }]"#); /// ``` - pub fn insert(mut self, body: T) -> Self + #[cfg(not(feature = "serde"))] + pub fn insert(self, body: T) -> Self where T: Into, { + self.insert_impl(body.into()) + } + + /// Performs an INSERT of the `body` (in JSON) into the table. + /// + /// # Example + /// + /// ``` + /// use postgrest::Postgrest; + /// + /// #[derive(serde::Serialize)] + /// struct MyStruct {} + /// + /// let my_serializable_struct = MyStruct {}; + /// + /// let client = Postgrest::new("https://your.postgrest.endpoint"); + /// client + /// .from("users") + /// .insert(&my_serializable_struct)?; + /// ``` + #[cfg(feature = "serde")] + pub fn insert(self, body: &T) -> serde_json::Result + where + T: serde::Serialize, + { + Ok(self.insert_impl(serde_json::to_string(body)?)) + } + + fn insert_impl(mut self, body: String) -> Self { self.method = Method::POST; self.headers .insert("Prefer", HeaderValue::from_static("return=representation")); - self.body = Some(body.into()); + self.body = Some(body); self } @@ -506,14 +536,44 @@ impl Builder { /// .eq("username", "soedirgo") /// .update(r#"{ "status": "offline" }"#); /// ``` - pub fn update(mut self, body: T) -> Self + #[cfg(not(feature = "serde"))] + pub fn update(self, body: T) -> Self where T: Into, { + self.update_impl(body.into()) + } + + /// Performs an UPDATE using the `body` (in JSON) on the table. + /// + /// # Example + /// + /// ``` + /// use postgrest::Postgrest; + /// + /// #[derive(serde::Serialize)] + /// struct MyStruct {} + /// + /// let my_serializable_struct = MyStruct {}; + /// + /// let client = Postgrest::new("https://your.postgrest.endpoint"); + /// client + /// .from("users") + /// .eq("username", "soedirgo") + /// .update(&my_serializable_struct)?; + /// ``` + #[cfg(feature = "serde")] + pub fn update(self, body: &T) -> serde_json::Result + where + T: serde::Serialize { + Ok(self.update_impl(serde_json::to_string(body)?)) + } + + fn update_impl(mut self, body: String) -> Self { self.method = Method::PATCH; self.headers .insert("Prefer", HeaderValue::from_static("return=representation")); - self.body = Some(body.into()); + self.body = Some(body); self } From 4b6724cce16713a2856cc08bc5887926d3eb67bf Mon Sep 17 00:00:00 2001 From: Anders Eie <1128648+strykejern@users.noreply.github.com> Date: Fri, 11 Oct 2024 21:04:27 +0200 Subject: [PATCH 2/5] builder: Enable serde support for upsert body --- src/builder.rs | 39 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/src/builder.rs b/src/builder.rs index b4991da..1d62eee 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -478,16 +478,51 @@ impl Builder { /// .upsert(r#"[{ "username": "soedirgo", "status": "online" }, /// { "username": "jose", "status": "offline" }]"#); /// ``` - pub fn upsert(mut self, body: T) -> Self + #[cfg(not(feature = "serde"))] + pub fn upsert(self, body: T) -> Self where T: Into, { + self.upsert_impl(body.into()) + } + + /// Performs an upsert of the `body` (in JSON) into the table. + /// + /// # Note + /// + /// This merges duplicates by default. Ignoring duplicates is possible via + /// PostgREST, but is currently unsupported. + /// + /// # Example + /// + /// ``` + /// use postgrest::Postgrest; + /// + /// #[derive(serde::Serialize)] + /// struct MyStruct {} + /// + /// let my_serializable_struct = MyStruct {}; + /// + /// let client = Postgrest::new("https://your.postgrest.endpoint"); + /// client + /// .from("users") + /// .upsert(&my_serializable_struct)?; + /// ``` + #[cfg(feature = "serde")] + pub fn upsert(self, body: &T) -> serde_json::Result + where + T: serde::Serialize, + { + Ok(self.upsert_impl(serde_json::to_string(body)?)) + } + + fn upsert_impl(mut self, body: String) -> Self { self.method = Method::POST; self.headers.insert( "Prefer", HeaderValue::from_static("return=representation,resolution=merge-duplicates"), ); - self.body = Some(body.into()); + self.body = Some(body); self } From 80619866316d36ab0528e16cc99f25cbe9fdb3dd Mon Sep 17 00:00:00 2001 From: Anders Eie <1128648+strykejern@users.noreply.github.com> Date: Fri, 11 Oct 2024 21:09:29 +0200 Subject: [PATCH 3/5] Conditionally disable tests not compiling with serde feature --- src/builder.rs | 1 + tests/client.rs | 5 +++++ tests/multi_schema.rs | 2 ++ 3 files changed, 8 insertions(+) diff --git a/src/builder.rs b/src/builder.rs index 1d62eee..b3eec84 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -787,6 +787,7 @@ mod tests { } #[test] + #[cfg(not(feature = "serde"))] fn upsert_assert_prefer_header() { let client = Client::new(); let builder = Builder::new(TABLE_URL, None, HeaderMap::new(), client).upsert("ignored"); diff --git a/tests/client.rs b/tests/client.rs index 8777748..a62510e 100644 --- a/tests/client.rs +++ b/tests/client.rs @@ -75,6 +75,7 @@ async fn relational_join() -> Result<(), Box> { } #[tokio::test] +#[cfg(not(feature = "serde"))] async fn insert() -> Result<(), Box> { let client = Postgrest::new(REST_URL); let resp = client @@ -90,6 +91,7 @@ async fn insert() -> Result<(), Box> { } #[tokio::test] +#[cfg(not(feature = "serde"))] async fn upsert() -> Result<(), Box> { let client = Postgrest::new(REST_URL); let resp = client @@ -110,6 +112,7 @@ async fn upsert() -> Result<(), Box> { } #[tokio::test] +#[cfg(not(feature = "serde"))] async fn upsert_existing() -> Result<(), Box> { let client = Postgrest::new(REST_URL); let resp = client @@ -128,6 +131,7 @@ async fn upsert_existing() -> Result<(), Box> { } #[tokio::test] +#[cfg(not(feature = "serde"))] async fn upsert_nonexisting() -> Result<(), Box> { let client = Postgrest::new(REST_URL); let resp = client @@ -145,6 +149,7 @@ async fn upsert_nonexisting() -> Result<(), Box> { } #[tokio::test] +#[cfg(not(feature = "serde"))] async fn update() -> Result<(), Box> { let client = Postgrest::new(REST_URL); let resp = client diff --git a/tests/multi_schema.rs b/tests/multi_schema.rs index d09bec9..d25a803 100644 --- a/tests/multi_schema.rs +++ b/tests/multi_schema.rs @@ -37,6 +37,7 @@ async fn read_other_schema() -> Result<(), Box> { } #[tokio::test] +#[cfg(not(feature = "serde"))] async fn write_other_schema() -> Result<(), Box> { let client = Postgrest::new(REST_URL); let resp = client @@ -81,6 +82,7 @@ async fn read_nonexisting_schema() -> Result<(), Box> { } #[tokio::test] +#[cfg(not(feature = "serde"))] async fn write_nonexisting_schema() -> Result<(), Box> { let client = Postgrest::new(REST_URL).schema("private"); let resp = client From 5afc9844043987334de7ee7764d2242217a72b7f Mon Sep 17 00:00:00 2001 From: Anders Eie <1128648+strykejern@users.noreply.github.com> Date: Fri, 11 Oct 2024 21:58:54 +0200 Subject: [PATCH 4/5] Add serde versions of non-serde tests --- src/builder.rs | 13 ++++ tests/client.rs | 142 ++++++++++++++++++++++++++++++++++++++++++ tests/multi_schema.rs | 65 +++++++++++++++++++ 3 files changed, 220 insertions(+) diff --git a/src/builder.rs b/src/builder.rs index b3eec84..d699f79 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -796,6 +796,19 @@ mod tests { HeaderValue::from_static("return=representation,resolution=merge-duplicates") ); } + + #[test] + #[cfg(feature = "serde")] + fn upsert_assert_prefer_header_serde() { + let client = Client::new(); + let builder = Builder::new(TABLE_URL, None, HeaderMap::new(), client) + .upsert(&()) + .unwrap(); + assert_eq!( + builder.headers.get("Prefer").unwrap(), + HeaderValue::from_static("return=representation,resolution=merge-duplicates") + ); + } #[test] fn not_rpc_should_not_have_flag() { diff --git a/tests/client.rs b/tests/client.rs index a62510e..d09762d 100644 --- a/tests/client.rs +++ b/tests/client.rs @@ -90,6 +90,33 @@ async fn insert() -> Result<(), Box> { Ok(()) } +#[tokio::test] +#[cfg(feature = "serde")] +async fn insert_serde() -> Result<(), Box> { + #[derive(serde::Serialize)] + struct Message { + message: String, + channel_id: i32, + username: String, + } + + let client = Postgrest::new(REST_URL); + let resp = client + .from("messages") + .insert(&[Message { + message: "Test message 1".to_string(), + channel_id: 1, + username: "kiwicopple".to_string(), + }])? + .execute() + .await?; + let status = resp.status(); + + assert_eq!(status.as_u16(), 201); + + Ok(()) +} + #[tokio::test] #[cfg(not(feature = "serde"))] async fn upsert() -> Result<(), Box> { @@ -111,6 +138,39 @@ async fn upsert() -> Result<(), Box> { Ok(()) } +#[tokio::test] +#[cfg(feature = "serde")] +async fn upsert_serde() -> Result<(), Box> { + #[derive(serde::Serialize)] + struct User { + username: String, + status: String, + } + + let client = Postgrest::new(REST_URL); + let resp = client + .from("users") + .upsert(&[ + User { + username: "dragarcia".to_string(), + status: "OFFLINE".to_string(), + }, + User { + username: "supabot2".to_string(), + status: "ONLINE".to_string(), + }, + ])? + .execute() + .await?; + let body = resp.text().await?; + let body = json::parse(&body)?; + + assert_eq!(body[0]["username"], "dragarcia"); + assert_eq!(body[1]["username"], "supabot2"); + + Ok(()) +} + #[tokio::test] #[cfg(not(feature = "serde"))] async fn upsert_existing() -> Result<(), Box> { @@ -130,6 +190,34 @@ async fn upsert_existing() -> Result<(), Box> { Ok(()) } +#[tokio::test] +#[cfg(feature = "serde")] +async fn upsert_existing_serde() -> Result<(), Box> { + #[derive(serde::Serialize)] + struct User { + username: String, + status: String, + } + + let client = Postgrest::new(REST_URL); + let resp = client + .from("users") + .upsert(&User { + username: "dragarcia".to_string(), + status: "ONLINE".to_string(), + })? + .on_conflict("username") + .execute() + .await?; + let body = resp.text().await?; + let body = json::parse(&body)?; + + assert_eq!(body[0]["username"], "dragarcia"); + assert_eq!(body[0]["status"], "ONLINE"); + + Ok(()) +} + #[tokio::test] #[cfg(not(feature = "serde"))] async fn upsert_nonexisting() -> Result<(), Box> { @@ -148,6 +236,33 @@ async fn upsert_nonexisting() -> Result<(), Box> { Ok(()) } +#[tokio::test] +#[cfg(feature = "serde")] +async fn upsert_nonexisting_serde() -> Result<(), Box> { + #[derive(serde::Serialize)] + struct User { + username: String, + status: String, + } + + let client = Postgrest::new(REST_URL); + let resp = client + .from("users") + .upsert(&User { + username: "supabot3".to_string(), + status: "ONLINE".to_string(), + })? + .execute() + .await?; + let body = resp.text().await?; + let body = json::parse(&body)?; + + assert_eq!(body[0]["username"], "supabot3"); + assert_eq!(body[0]["status"], "ONLINE"); + + Ok(()) +} + #[tokio::test] #[cfg(not(feature = "serde"))] async fn update() -> Result<(), Box> { @@ -168,6 +283,33 @@ async fn update() -> Result<(), Box> { Ok(()) } +#[tokio::test] +#[cfg(feature = "serde")] +async fn update_serde() -> Result<(), Box> { + #[derive(serde::Serialize)] + struct User { + status: String, + } + + let client = Postgrest::new(REST_URL); + let resp = client + .from("users") + .eq("status", "ONLINE") + .update(&User { + status: "ONLINE".to_string(), + })? + .execute() + .await?; + let status = resp.status(); + let body = resp.text().await?; + let body = json::parse(&body)?; + + assert_eq!(status.as_u16(), 200); + assert_eq!(body[0]["status"], "ONLINE"); + + Ok(()) +} + #[tokio::test] async fn delete() -> Result<(), Box> { let client = Postgrest::new(REST_URL); diff --git a/tests/multi_schema.rs b/tests/multi_schema.rs index d25a803..21442fe 100644 --- a/tests/multi_schema.rs +++ b/tests/multi_schema.rs @@ -66,6 +66,43 @@ async fn write_other_schema() -> Result<(), Box> { Ok(()) } +#[tokio::test] +#[cfg(feature = "serde")] +async fn write_other_schema_serde() -> Result<(), Box> { + #[derive(serde::Serialize)] + struct User { + status: String, + } + + let client = Postgrest::new(REST_URL); + let resp = client + .from("users") + .select("status") + .eq("username", "supabot") + .execute() + .await?; + let body = resp.text().await?; + let body = json::parse(&body)?; + + assert_eq!(body[0]["status"], "ONLINE"); + + let other_client = Postgrest::new(REST_URL).schema("personal"); + let other_resp = other_client + .from("users") + .update(&User { + status: "OFFLINE".to_string(), + })? + .eq("username", "supabot") + .execute() + .await?; + let other_body = other_resp.text().await?; + let other_body = json::parse(&other_body)?; + + assert_eq!(other_body[0]["status"], "OFFLINE"); + + Ok(()) +} + #[tokio::test] async fn read_nonexisting_schema() -> Result<(), Box> { let client = Postgrest::new(REST_URL).schema("private"); @@ -102,6 +139,34 @@ async fn write_nonexisting_schema() -> Result<(), Box> { Ok(()) } +#[tokio::test] +#[cfg(feature = "serde")] +async fn write_nonexisting_schema_serde() -> Result<(), Box> { + #[derive(serde::Serialize)] + struct Channel { + slug: String, + } + + let client = Postgrest::new(REST_URL).schema("private"); + let resp = client + .from("channels") + .update(&Channel { + slug: "private".to_string(), + })? + .eq("slug", "random") + .execute() + .await?; + let body = resp.text().await?; + let body = json::parse(&body)?; + + assert_eq!( + body["message"], + "The schema must be one of the following: public, personal" + ); + + Ok(()) +} + #[tokio::test] async fn other_schema_rpc() -> Result<(), Box> { let client = Postgrest::new(REST_URL).schema("personal"); From e43c6d499e92f902b75463f5fb4b3262058b8e9b Mon Sep 17 00:00:00 2001 From: Anders Eie <1128648+strykejern@users.noreply.github.com> Date: Fri, 11 Oct 2024 21:59:33 +0200 Subject: [PATCH 5/5] Fix doctests with new serde feature --- src/builder.rs | 28 ++++++++++++++++++++-------- src/lib.rs | 1 + 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/builder.rs b/src/builder.rs index d699f79..66f8247 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -442,7 +442,7 @@ impl Builder { /// let client = Postgrest::new("https://your.postgrest.endpoint"); /// client /// .from("users") - /// .insert(&my_serializable_struct)?; + /// .insert(&my_serializable_struct).unwrap(); /// ``` #[cfg(feature = "serde")] pub fn insert(self, body: &T) -> serde_json::Result @@ -506,7 +506,7 @@ impl Builder { /// let client = Postgrest::new("https://your.postgrest.endpoint"); /// client /// .from("users") - /// .upsert(&my_serializable_struct)?; + /// .upsert(&my_serializable_struct).unwrap(); /// ``` #[cfg(feature = "serde")] pub fn upsert(self, body: &T) -> serde_json::Result @@ -543,11 +543,23 @@ impl Builder { /// let client = Postgrest::new("https://your.postgrest.endpoint"); /// // Suppose `users` are keyed an SERIAL primary key, /// // but have a unique index on `username`. - /// client - /// .from("users") - /// .upsert(r#"[{ "username": "soedirgo", "status": "online" }, - /// { "username": "jose", "status": "offline" }]"#) - /// .on_conflict("username"); + #[cfg_attr(not(feature = "serde"), doc = r##" + client + .from("users") + .upsert(r#"[{ "username": "soedirgo", "status": "online" }, + { "username": "jose", "status": "offline" }]"#) + .on_conflict("username"); + "##)] + #[cfg_attr(feature = "serde", doc = r##" + #[derive(serde::Serialize)] + struct MyStruct {} + + let my_serializable_struct = MyStruct {}; + + client + .from("users") + .upsert(&my_serializable_struct).unwrap(); + "##)] /// ``` pub fn on_conflict(mut self, columns: T) -> Self where @@ -595,7 +607,7 @@ impl Builder { /// client /// .from("users") /// .eq("username", "soedirgo") - /// .update(&my_serializable_struct)?; + /// .update(&my_serializable_struct).unwrap(); /// ``` #[cfg(feature = "serde")] pub fn update(self, body: &T) -> serde_json::Result diff --git a/src/lib.rs b/src/lib.rs index f017056..b7aeac5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -44,6 +44,7 @@ //! Updating a table: //! ``` //! # use postgrest::Postgrest; +//! # #[cfg(not(feature = "serde"))] //! # async fn run() -> Result<(), Box> { //! # let client = Postgrest::new("https://your.postgrest.endpoint"); //! let resp = client