diff --git a/Cargo.lock b/Cargo.lock index 690e83505..e0c932a6f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -299,6 +299,72 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +[[package]] +name = "darling" +version = "0.20.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54e36fcd13ed84ffdfda6f5be89b31287cbb80c439841fe69e04841435464391" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c2cf1c23a687a1feeb728783b993c4e1ad83d99f351801977dd809b48d0a70f" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "derive_builder" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0350b5cb0331628a5916d6c5c0b72e97393b8b6b03b47a9284f4e7f5a405ffd7" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d48cda787f839151732d396ac69e3473923d54312c070ee21e9effcaa8ca0b1d" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "derive_builder_macro" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "206868b8242f27cecce124c19fd88157fbd0dd334df2587f36417bafbc85097b" +dependencies = [ + "derive_builder_core", + "syn", +] + [[package]] name = "dissimilar" version = "1.0.7" @@ -592,6 +658,12 @@ dependencies = [ "tracing", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "0.5.0" @@ -1919,6 +1991,7 @@ dependencies = [ name = "wp_api" version = "0.1.0" dependencies = [ + "derive_builder", "http 1.1.0", "parse_link_header", "serde", @@ -1926,6 +1999,7 @@ dependencies = [ "thiserror", "uniffi", "url", + "wp_derive", ] [[package]] diff --git a/native/swift/Sources/wordpress-api/Login/API+Login.swift b/native/swift/Sources/wordpress-api/Login/API+Login.swift index f134ffe8c..7424c3137 100644 --- a/native/swift/Sources/wordpress-api/Login/API+Login.swift +++ b/native/swift/Sources/wordpress-api/Login/API+Login.swift @@ -7,7 +7,7 @@ import FoundationNetworking public extension WordPressAPI { static func findRestApiEndpointRoot(forSiteUrl url: URL, using session: URLSession) async throws -> URL? { - let request = WpNetworkRequest(method: .head, url: url) + let request = WpNetworkRequest(method: .head, url: url, headerMap: [:]) let ephemeralClient = WordPressAPI(urlSession: session, baseUrl: url, authenticationStategy: .none) let response = try await ephemeralClient.perform(request: request) @@ -15,7 +15,7 @@ public extension WordPressAPI { } func getRestAPICapabilities(forApiRoot url: URL, using session: URLSession) async throws -> WpapiDetails { - let wpResponse = try await self.perform(request: WpNetworkRequest(method: .get, url: url, headerMap: nil)) + let wpResponse = try await self.perform(request: WpNetworkRequest(method: .get, url: url, headerMap: [:])) return try wordpress_api_wrapper.parseApiDetailsResponse(response: wpResponse) } } diff --git a/native/swift/Sources/wordpress-api/WordPressAPI.swift b/native/swift/Sources/wordpress-api/WordPressAPI.swift index 9b32323c8..06643d745 100644 --- a/native/swift/Sources/wordpress-api/WordPressAPI.swift +++ b/native/swift/Sources/wordpress-api/WordPressAPI.swift @@ -159,8 +159,8 @@ extension RequestMethod { } extension WpNetworkRequest { - init(method: RequestMethod, url: URL, headerMap: [String: String]? = nil) { - self.init(method: method, url: url.absoluteString, headerMap: headerMap) + init(method: RequestMethod, url: URL, headerMap: [String: String]) { + self.init(method: method, url: url.absoluteString, headerMap: headerMap, body: nil) } } diff --git a/wp_api/Cargo.toml b/wp_api/Cargo.toml index 787e92fad..06ee836ea 100644 --- a/wp_api/Cargo.toml +++ b/wp_api/Cargo.toml @@ -8,6 +8,7 @@ crate-type = ["lib", "cdylib", "staticlib"] name = "wp_api" [dependencies] +derive_builder = "0.20" http = { workspace = true } url = "2.5" parse_link_header = "0.3" @@ -15,6 +16,7 @@ serde = { version = "1.0", features = [ "derive" ] } serde_json = "1.0" thiserror = { workspace = true } uniffi = { workspace = true } +wp_derive = { path = "../wp_derive" } [build-dependencies] uniffi = { workspace = true , features = [ "build", "cli" ] } diff --git a/wp_api/src/lib.rs b/wp_api/src/lib.rs index 1d4829b9a..b9fd9f987 100644 --- a/wp_api/src/lib.rs +++ b/wp_api/src/lib.rs @@ -7,12 +7,16 @@ pub use login::*; pub use pages::*; pub use posts::*; pub use url::*; +pub use users::*; pub mod api_error; pub mod login; pub mod pages; pub mod posts; pub mod url; +pub mod users; + +const CONTENT_TYPE_JSON: &str = "application/json"; #[derive(uniffi::Object)] pub struct WPApiHelper { @@ -33,19 +37,11 @@ impl WPApiHelper { } pub fn raw_request(&self, url: String) -> WPNetworkRequest { - let mut header_map = HashMap::new(); - - match &self.authentication { - WPAuthentication::AuthorizationHeader { token } => { - header_map.insert("Authorization".into(), format!("Basic {}", token)); - } - WPAuthentication::None => (), - } - WPNetworkRequest { method: RequestMethod::GET, url: Url::parse(url.as_str()).unwrap().into(), - header_map: Some(header_map), + header_map: self.header_map(), + body: None, } } @@ -55,15 +51,6 @@ impl WPApiHelper { .join("/wp-json/wp/v2/posts?context=edit") .unwrap(); - let mut header_map = HashMap::new(); - - match &self.authentication { - WPAuthentication::AuthorizationHeader { token } => { - header_map.insert("Authorization".into(), format!("Basic {}", token)); - } - WPAuthentication::None => (), - } - url.query_pairs_mut() .append_pair("page", params.page.to_string().as_str()); url.query_pairs_mut() @@ -72,7 +59,139 @@ impl WPApiHelper { WPNetworkRequest { method: RequestMethod::GET, url: url.into(), - header_map: Some(header_map), + header_map: self.header_map(), + body: None, + } + } + + pub fn list_users_request( + &self, + context: WPContext, + params: &Option, // UniFFI doesn't support Option<&T> + ) -> WPNetworkRequest { + WPNetworkRequest { + method: RequestMethod::GET, + url: UsersEndpoint::list_users(&self.site_url, context, params.as_ref()).into(), + header_map: self.header_map(), + body: None, + } + } + + pub fn retrieve_user_request(&self, user_id: UserId, context: WPContext) -> WPNetworkRequest { + WPNetworkRequest { + method: RequestMethod::GET, + url: UsersEndpoint::retrieve_user(&self.site_url, user_id, context).into(), + header_map: self.header_map(), + body: None, + } + } + + pub fn retrieve_current_user_request(&self, context: WPContext) -> WPNetworkRequest { + WPNetworkRequest { + method: RequestMethod::GET, + url: UsersEndpoint::retrieve_current_user(&self.site_url, context).into(), + header_map: self.header_map(), + body: None, + } + } + + pub fn create_user_request(&self, params: &UserCreateParams) -> WPNetworkRequest { + WPNetworkRequest { + method: RequestMethod::POST, + url: UsersEndpoint::create_user(&self.site_url).into(), + header_map: self.header_map_for_post_request(), + body: serde_json::to_vec(¶ms).ok(), + } + } + + pub fn update_user_request( + &self, + user_id: UserId, + params: &UserUpdateParams, + ) -> WPNetworkRequest { + WPNetworkRequest { + method: RequestMethod::POST, + url: UsersEndpoint::update_user(&self.site_url, user_id, params).into(), + header_map: self.header_map_for_post_request(), + body: serde_json::to_vec(¶ms).ok(), + } + } + + pub fn update_current_user_request(&self, params: &UserUpdateParams) -> WPNetworkRequest { + WPNetworkRequest { + method: RequestMethod::POST, + url: UsersEndpoint::update_current_user(&self.site_url).into(), + header_map: self.header_map_for_post_request(), + body: serde_json::to_vec(¶ms).ok(), + } + } + + pub fn delete_user_request( + &self, + user_id: UserId, + params: &UserDeleteParams, + ) -> WPNetworkRequest { + WPNetworkRequest { + method: RequestMethod::DELETE, + url: UsersEndpoint::delete_user(&self.site_url, user_id, params).into(), + header_map: self.header_map(), + body: None, + } + } + + pub fn delete_current_user_request(&self, params: &UserDeleteParams) -> WPNetworkRequest { + WPNetworkRequest { + method: RequestMethod::DELETE, + url: UsersEndpoint::delete_current_user(&self.site_url, params).into(), + header_map: self.header_map(), + body: None, + } + } + + fn header_map(&self) -> HashMap { + let mut header_map = HashMap::new(); + header_map.insert( + http::header::ACCEPT.to_string(), + CONTENT_TYPE_JSON.to_string(), + ); + match &self.authentication { + WPAuthentication::None => None, + WPAuthentication::AuthorizationHeader { token } => { + header_map.insert("Authorization".to_string(), format!("Basic {}", token)) + } + }; + header_map + } + + fn header_map_for_post_request(&self) -> HashMap { + let mut header_map = self.header_map(); + header_map.insert( + http::header::CONTENT_TYPE.to_string(), + CONTENT_TYPE_JSON.to_string(), + ); + header_map + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, uniffi::Enum)] +pub enum WPContext { + Edit, + Embed, + View, +} + +impl Default for WPContext { + fn default() -> Self { + Self::View + } +} + +impl WPContext { + fn as_str(&self) -> &str { + match self { + Self::Edit => "edit", + Self::Embed => "embed", + Self::View => "view", } } } @@ -92,6 +211,27 @@ pub enum RequestMethod { HEAD, } +#[derive(Debug, Clone, Copy, PartialEq, Eq, uniffi::Enum)] +pub enum WPApiParamOrder { + Asc, + Desc, +} + +impl Default for WPApiParamOrder { + fn default() -> Self { + Self::Asc + } +} + +impl WPApiParamOrder { + fn as_str(&self) -> &str { + match self { + Self::Asc => "asc", + Self::Desc => "desc", + } + } +} + #[derive(uniffi::Record)] pub struct WPNetworkRequest { pub method: RequestMethod, @@ -101,7 +241,8 @@ pub struct WPNetworkRequest { // // It could be something similar to `reqwest`'s [`header`](https://docs.rs/reqwest/latest/reqwest/header/index.html) // module. - pub header_map: Option>, + pub header_map: HashMap, + pub body: Option>, } #[derive(uniffi::Record)] @@ -139,24 +280,11 @@ impl WPNetworkResponse { pub fn parse_post_list_response( response: WPNetworkResponse, ) -> Result { - // TODO: Further parse the response body to include error message - // TODO: Lots of unwraps to get a basic setup working - if let Some(client_error_type) = ClientErrorType::from_status_code(response.status_code) { - return Err(WPApiError::ClientError { - error_type: client_error_type, - status_code: response.status_code, - }); - } - let status = http::StatusCode::from_u16(response.status_code).unwrap(); - if status.is_server_error() { - return Err(WPApiError::ServerError { - status_code: response.status_code, - }); - } + parse_response_for_generic_errors(&response)?; let post_list: Vec = serde_json::from_slice(&response.body).map_err(|err| WPApiError::ParsingError { reason: err.to_string(), - response: std::str::from_utf8(&response.body).unwrap().to_string(), + response: String::from_utf8_lossy(&response.body).to_string(), })?; let mut next_page: Option = None; @@ -176,12 +304,30 @@ pub fn parse_api_details_response(response: WPNetworkResponse) -> Result Result<(), WPApiError> { + // TODO: Further parse the response body to include error message + // TODO: Lots of unwraps to get a basic setup working + if let Some(client_error_type) = ClientErrorType::from_status_code(response.status_code) { + return Err(WPApiError::ClientError { + error_type: client_error_type, + status_code: response.status_code, + }); + } + let status = http::StatusCode::from_u16(response.status_code).unwrap(); + if status.is_server_error() { + return Err(WPApiError::ServerError { + status_code: response.status_code, + }); + } + Ok(()) +} + // TODO: Figure out why we can't expose this method on `WPNetworkResponse` via UniFFI #[uniffi::export] pub fn get_link_header(response: &WPNetworkResponse, name: &str) -> Option { diff --git a/wp_api/src/users.rs b/wp_api/src/users.rs new file mode 100644 index 000000000..432ae1227 --- /dev/null +++ b/wp_api/src/users.rs @@ -0,0 +1,390 @@ +use std::collections::HashMap; + +use derive_builder::Builder; +use serde::{Deserialize, Serialize}; +use url::Url; +use wp_derive::WPContextual; + +use crate::{ + parse_response_for_generic_errors, WPApiError, WPApiParamOrder, WPContext, WPNetworkResponse, +}; + +#[uniffi::export] +pub fn parse_list_users_response_with_edit_context( + response: &WPNetworkResponse, +) -> Result, WPApiError> { + parse_users_response(response) +} + +#[uniffi::export] +pub fn parse_list_users_response_with_embed_context( + response: &WPNetworkResponse, +) -> Result, WPApiError> { + parse_users_response(response) +} + +#[uniffi::export] +pub fn parse_list_users_response_with_view_context( + response: &WPNetworkResponse, +) -> Result, WPApiError> { + parse_users_response(response) +} + +#[uniffi::export] +pub fn parse_retrieve_user_response_with_edit_context( + response: &WPNetworkResponse, +) -> Result { + parse_users_response(response) +} + +#[uniffi::export] +pub fn parse_retrieve_user_response_with_embed_context( + response: &WPNetworkResponse, +) -> Result { + parse_users_response(response) +} + +#[uniffi::export] +pub fn parse_retrieve_user_response_with_view_context( + response: &WPNetworkResponse, +) -> Result { + parse_users_response(response) +} + +pub fn parse_users_response<'de, T: Deserialize<'de>>( + response: &'de WPNetworkResponse, +) -> Result { + parse_response_for_generic_errors(response)?; + serde_json::from_slice(&response.body).map_err(|err| WPApiError::ParsingError { + reason: err.to_string(), + response: String::from_utf8_lossy(&response.body).to_string(), + }) +} + +pub struct UsersEndpoint {} + +impl UsersEndpoint { + pub fn list_users(site_url: &Url, context: WPContext, params: Option<&UserListParams>) -> Url { + let mut url = site_url.join("/wp-json/wp/v2/users").unwrap(); + url.query_pairs_mut() + .append_pair("context", context.as_str()); + if let Some(params) = params { + url.query_pairs_mut().extend_pairs(params.query_pairs()); + } + url + } + + pub fn retrieve_user(site_url: &Url, user_id: UserId, context: WPContext) -> Url { + let mut url = site_url + .join(format!("/wp-json/wp/v2/users/{}", user_id).as_str()) + .unwrap(); + url.query_pairs_mut() + .append_pair("context", context.as_str()); + url + } + + pub fn retrieve_current_user(site_url: &Url, context: WPContext) -> Url { + let mut url = site_url.join("/wp-json/wp/v2/users/me").unwrap(); + url.query_pairs_mut() + .append_pair("context", context.as_str()); + url + } + + pub fn create_user(site_url: &Url) -> Url { + site_url.join("/wp-json/wp/v2/users").unwrap() + } + + pub fn update_user(site_url: &Url, user_id: UserId, params: &UserUpdateParams) -> Url { + site_url + .join(format!("/wp-json/wp/v2/users/{}", user_id).as_str()) + .unwrap() + } + + pub fn update_current_user(site_url: &Url) -> Url { + site_url.join("/wp-json/wp/v2/users/me").unwrap() + } + + pub fn delete_user(site_url: &Url, user_id: UserId, params: &UserDeleteParams) -> Url { + let mut url = site_url + .join(format!("/wp-json/wp/v2/users/{}", user_id).as_str()) + .unwrap(); + url.query_pairs_mut().extend_pairs(params.query_pairs()); + url + } + + pub fn delete_current_user(site_url: &Url, params: &UserDeleteParams) -> Url { + let mut url = site_url.join("/wp-json/wp/v2/users/me").unwrap(); + url.query_pairs_mut().extend_pairs(params.query_pairs()); + url + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, uniffi::Enum)] +pub enum WPApiParamUsersOrderBy { + Id, + Include, + Name, + RegisteredDate, + Slug, + IncludeSlugs, + Email, + Url, +} + +impl Default for WPApiParamUsersOrderBy { + fn default() -> Self { + Self::Name + } +} + +impl WPApiParamUsersOrderBy { + fn as_str(&self) -> &str { + match self { + Self::Id => "id", + Self::Include => "include", + Self::Name => "name", + Self::RegisteredDate => "registered_date", + Self::Slug => "slug", + Self::IncludeSlugs => "include_slugs", + Self::Email => "email", + Self::Url => "url", + } + } +} + +#[derive(Default, uniffi::Record)] +pub struct UserListParams { + // TODO: Implement the `_filter` + /// Current page of the collection. + /// Default: `1` + pub page: Option, + /// Maximum number of items to be returned in result set. + /// Default: `10` + pub per_page: Option, + /// Limit results to those matching a string. + pub search: Option, + /// Ensure result set excludes specific IDs. + pub exclude: Option, + /// Limit result set to specific IDs. + pub include: Option, + /// Offset the result set by a specific number of items. + pub offset: Option, + /// Order sort attribute ascending or descending. + /// Default: `asc` + /// One of: `asc`, `desc` + pub order: Option, + /// Sort collection by user attribute. + /// Default: `name` + /// One of: `id`, `include`, `name`, `registered_date`, `slug`, `include_slugs`, `email`, `url` + pub order_by: Option, + /// Limit result set to users with one or more specific slugs. + pub slug: Vec, + /// Limit result set to users matching at least one specific role provided. Accepts csv list or single role. + pub roles: Vec, + /// Limit result set to users matching at least one specific capability provided. Accepts csv list or single capability. + pub capabilities: Vec, + /// Limit result set to users who are considered authors. + /// One of: `authors` + pub who: Option, + /// Limit result set to users who have published posts. + pub has_published_posts: Option, +} + +impl UserListParams { + pub fn query_pairs(&self) -> impl IntoIterator { + [ + ("page", self.page.map(|x| x.to_string())), + ("per_page", self.per_page.map(|x| x.to_string())), + ("search", self.search.clone()), + ("exclude", self.exclude.clone()), + ("include", self.include.clone()), + ("offset", self.offset.map(|x| x.to_string())), + ("order", self.order.map(|x| x.as_str().to_string())), + ("order_by", self.order_by.map(|x| x.as_str().to_string())), + ("slug", Some(self.slug.join(","))), + ("roles", Some(self.roles.join(","))), + ("capabilities", Some(self.capabilities.join(","))), + ("who", self.who.clone()), + ( + "has_published_post", + self.has_published_posts.map(|x| x.to_string()), + ), + ] + .into_iter() + // Remove `None` values + .filter_map(|(k, opt_v)| opt_v.map(|v| (k, v))) + } +} + +#[derive(Builder, Serialize, uniffi::Record)] +pub struct UserCreateParams { + /// Login name for the user. + pub username: String, + /// The email address for the user. + pub email: String, + /// Password for the user (never included). + pub password: String, + /// Display name for the user. + #[builder(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, + /// First name for the user. + #[builder(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub first_name: Option, + /// Last name for the user. + #[builder(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub last_name: Option, + /// URL of the user. + #[builder(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub url: Option, + /// Description of the user. + #[builder(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + /// Locale for the user. + /// One of: , `en_US` + #[builder(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub locale: Option, + /// The nickname for the user. + #[builder(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub nickname: Option, + /// An alphanumeric identifier for the user. + #[builder(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub slug: Option, + /// Roles assigned to the user. + #[builder(default)] + #[serde(skip_serializing_if = "Vec::is_empty")] + pub roles: Vec, + /// Meta fields. + #[builder(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub meta: Option, +} + +#[derive(Builder, Serialize, uniffi::Record)] +pub struct UserUpdateParams { + /// Display name for the user. + #[builder(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, + /// First name for the user. + #[builder(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub first_name: Option, + /// Last name for the user. + #[builder(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub last_name: Option, + /// The email address for the user. + #[builder(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub email: Option, + /// URL of the user. + #[builder(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub url: Option, + /// Description of the user. + #[builder(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + /// Locale for the user. + /// One of: , `en_US` + #[builder(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub locale: Option, + /// The nickname for the user. + #[builder(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub nickname: Option, + /// An alphanumeric identifier for the user. + #[builder(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub slug: Option, + /// Roles assigned to the user. + #[builder(default)] + #[serde(skip_serializing_if = "Vec::is_empty")] + pub roles: Vec, + /// Password for the user (never included). + #[builder(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub password: Option, + /// Meta fields. + #[builder(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub meta: Option, +} + +#[derive(uniffi::Record)] +pub struct UserDeleteParams { + /// Reassign the deleted user's posts and links to this user ID. + pub reassign: UserId, +} + +impl UserDeleteParams { + pub fn query_pairs(&self) -> impl IntoIterator { + [ + ("reassign", self.reassign.to_string()), + // From the [documentation](https://developer.wordpress.org/rest-api/reference/users/#delete-a-user): + // > Required to be true, as users do not support trashing. + // Since this argument always has to be `true`, we don't include it in the parameter + // fields + ("force", true.to_string()), + ] + .into_iter() + } +} + +uniffi::custom_newtype!(UserId, i32); +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub struct UserId(i32); + +impl std::fmt::Display for UserId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +#[derive(Debug, Serialize, Deserialize, uniffi::Record, WPContextual)] +pub struct SparseUser { + #[WPContext(edit, embed, view)] + pub id: Option, + #[WPContext(edit)] + pub username: Option, + #[WPContext(edit, embed, view)] + pub name: Option, + #[WPContext(edit)] + pub first_name: Option, + #[WPContext(edit)] + pub last_name: Option, + #[WPContext(edit)] + pub email: Option, + #[WPContext(edit, embed, view)] + pub url: Option, + #[WPContext(edit, embed, view)] + pub description: Option, + #[WPContext(edit, embed, view)] + pub link: Option, + #[WPContext(edit)] + pub locale: Option, + #[WPContext(edit)] + pub nickname: Option, + #[WPContext(edit, embed, view)] + pub slug: Option, + #[WPContext(edit)] + pub registered_date: Option, + #[WPContext(edit)] + pub roles: Option>, + #[WPContext(edit)] + pub capabilities: Option>, + #[WPContext(edit)] + pub extra_capabilities: Option>, + #[WPContext(edit, embed, view)] + pub avatar_urls: Option>, + // meta field is omitted for now: https://github.com/Automattic/wordpress-rs/issues/57 +} diff --git a/wp_cli/src/main.rs b/wp_cli/src/main.rs index 74cf76536..c33736ee0 100644 --- a/wp_cli/src/main.rs +++ b/wp_cli/src/main.rs @@ -1,6 +1,8 @@ use std::fs::read_to_string; -use wp_api::WPAuthentication; +use wp_api::{ + UserCreateParamsBuilder, UserDeleteParams, UserUpdateParamsBuilder, WPAuthentication, WPContext, +}; use wp_networking::WPNetworking; fn main() { @@ -16,6 +18,119 @@ fn main() { }; let wp_networking = WPNetworking::new(url.into(), authentication); - let post_list = wp_networking.list_posts(None).unwrap(); - println!("{:?}", post_list); + + let user_list_request = wp_networking + .api_helper + .list_users_request(WPContext::Edit, &None); + let user_list = wp_api::parse_list_users_response_with_edit_context( + &wp_networking.request(user_list_request).unwrap(), + ) + .unwrap(); + println!("User List: {:?}", user_list); + + let first_user = user_list.first().unwrap(); + let user_retrieve_request = wp_networking + .api_helper + .retrieve_user_request(first_user.id, WPContext::Embed); + println!( + "{:?}", + wp_api::parse_retrieve_user_response_with_embed_context( + &wp_networking.request(user_retrieve_request).unwrap() + ) + ); + + let user_create_params = UserCreateParamsBuilder::default() + .username("t_username".to_string()) + .email("t_email@foo.com".to_string()) + .password("t_password".to_string()) + .build() + .unwrap(); + + let user_create_request = wp_networking + .api_helper + .create_user_request(&user_create_params); + let user_create_response = wp_networking.request(user_create_request).unwrap(); + let created_user = + wp_api::parse_retrieve_user_response_with_edit_context(&user_create_response); + + println!( + "Create user response: {:?}", + String::from_utf8_lossy(&user_create_response.body) + ); + println!("Created User: {:?}", created_user); + + let created_user = created_user.unwrap(); + let user_update_params = UserUpdateParamsBuilder::default() + .email(Some("t_email_updated@foo.com".to_string())) + .build() + .unwrap(); + let user_update_request = wp_networking + .api_helper + .update_user_request(created_user.id, &user_update_params); + let user_update_response = wp_networking.request(user_update_request).unwrap(); + let updated_user = + wp_api::parse_retrieve_user_response_with_edit_context(&user_update_response); + + println!( + "Update user response: {:?}", + String::from_utf8_lossy(&user_update_response.body) + ); + println!("Updated User: {:?}", updated_user); + + let user_delete_params = UserDeleteParams { + reassign: first_user.id, + }; + let user_delete_request = wp_networking + .api_helper + .delete_user_request(created_user.id, &user_delete_params); + let user_delete_response = wp_networking.request(user_delete_request).unwrap(); + println!( + "Delete user response: {:?}", + String::from_utf8_lossy(&user_delete_response.body) + ); + println!( + "Retrieve current user: {:?}", + wp_api::parse_retrieve_user_response_with_edit_context( + &wp_networking + .request( + wp_networking + .api_helper + .retrieve_current_user_request(WPContext::Edit) + ) + .unwrap() + ) + ); + + let update_current_user_params = UserUpdateParamsBuilder::default() + .description(Some("updated_description".to_string())) + .build() + .unwrap(); + let update_current_user_request = wp_networking + .api_helper + .update_current_user_request(&update_current_user_params); + let update_current_user_response = wp_networking.request(update_current_user_request).unwrap(); + let updated_current_user = + wp_api::parse_retrieve_user_response_with_edit_context(&update_current_user_response); + println!( + "Update current user response: {:?}", + String::from_utf8_lossy(&update_current_user_response.body) + ); + println!("Updated Current User: {:?}", updated_current_user); + + // Remember to use a temporary user to test this + // println!( + // "Delete current user: {:?}", + // String::from_utf8_lossy( + // &wp_networking + // .request( + // wp_networking + // .api_helper + // .delete_current_user_request(&UserDeleteParams { + // reassign: first_user.id + // }) + // ) + // .unwrap() + // .body + // ) + // ); } diff --git a/wp_networking/src/lib.rs b/wp_networking/src/lib.rs index fc78b2d4a..40ed0cd16 100644 --- a/wp_networking/src/lib.rs +++ b/wp_networking/src/lib.rs @@ -2,36 +2,36 @@ use http::HeaderMap; use reqwest::blocking::Client; -use wp_api::{ - PostListParams, PostListResponse, WPApiError, WPApiHelper, WPAuthentication, WPNetworkResponse, -}; +use wp_api::{WPApiHelper, WPAuthentication, WPNetworkRequest, WPNetworkResponse}; pub struct WPNetworking { client: Client, - helper: WPApiHelper, + pub api_helper: WPApiHelper, } impl WPNetworking { pub fn new(site_url: String, authentication: WPAuthentication) -> Self { Self { client: reqwest::blocking::Client::new(), - helper: WPApiHelper::new(site_url, authentication), + api_helper: WPApiHelper::new(site_url, authentication), } } - pub fn list_posts( + pub fn request( &self, - params: Option, - ) -> Result { - let wp_request = self.helper.post_list_request(params.unwrap_or_default()); - let request_headers: HeaderMap = (&wp_request.header_map.unwrap()).try_into().unwrap(); - let response = self + wp_request: WPNetworkRequest, + ) -> Result { + let request_headers: HeaderMap = (&wp_request.header_map).try_into().unwrap(); + + let mut request = self .client .request(request_method(wp_request.method), wp_request.url) - .headers(request_headers) - .send() - .unwrap(); - wp_api::parse_post_list_response(wp_network_response(response)) + .headers(request_headers); + if let Some(body) = wp_request.body { + request = request.body(body); + } + let response = request.send()?; + Ok(wp_network_response(response)) } }