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..66f8247 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).unwrap(); + /// ``` + #[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 } @@ -448,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).unwrap(); + /// ``` + #[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 } @@ -478,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 @@ -506,14 +583,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).unwrap(); + /// ``` + #[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 } @@ -692,6 +799,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"); @@ -700,6 +808,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/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 diff --git a/tests/client.rs b/tests/client.rs index 8777748..d09762d 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,34 @@ async fn insert() -> Result<(), Box> { } #[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> { let client = Postgrest::new(REST_URL); let resp = client @@ -110,6 +139,40 @@ async fn upsert() -> Result<(), Box> { } #[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> { let client = Postgrest::new(REST_URL); let resp = client @@ -128,6 +191,35 @@ async fn upsert_existing() -> Result<(), Box> { } #[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> { let client = Postgrest::new(REST_URL); let resp = client @@ -145,6 +237,34 @@ async fn upsert_nonexisting() -> Result<(), Box> { } #[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> { let client = Postgrest::new(REST_URL); let resp = client @@ -163,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 d09bec9..21442fe 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 @@ -65,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"); @@ -81,6 +119,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 @@ -100,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");