Skip to main content

concur_api/api/
identity.rs

1//! SAP Concur Identity API v4.1.
2//!
3//! Endpoints: `profile/identity/v4.1/Users`
4
5use crate::client::ConcurClient;
6use crate::error::Result;
7use crate::models::{Address, Email, Meta, UserName};
8use reqwest::Method;
9use serde::{Deserialize, Serialize};
10
11/// Identity API resource.
12pub struct IdentityApi<'a> {
13    client: &'a ConcurClient,
14}
15
16impl<'a> IdentityApi<'a> {
17    pub fn new(client: &'a ConcurClient) -> Self {
18        Self { client }
19    }
20
21    /// List users for the company.
22    pub async fn list_users(&self, count: Option<u32>, cursor: Option<&str>) -> Result<UserList> {
23        let mut path = "/profile/identity/v4.1/Users".to_string();
24        let mut sep = '?';
25        if let Some(c) = count {
26            path.push_str(&format!("{}count={}", sep, c));
27            sep = '&';
28        }
29        if let Some(c) = cursor {
30            path.push_str(&format!("{}cursor={}", sep, urlencoding::encode(c)));
31        }
32        self.client
33            .request(Method::GET, &path)
34            .await?
35            .send()
36            .await?
37            .json()
38            .await
39            .map_err(Into::into)
40    }
41
42    /// Search users via POST `/.search`.
43    pub async fn search_users(
44        &self,
45        filter: &str,
46        attributes: Option<Vec<String>>,
47    ) -> Result<UserList> {
48        let body = SearchRequest {
49            schemas: vec!["urn:ietf:params:scim:api:messages:concur:2.0:SearchRequest".to_string()],
50            filter: filter.to_string(),
51            attributes,
52        };
53        let req = self
54            .client
55            .request(Method::POST, "/profile/identity/v4.1/Users/.search")
56            .await?;
57        let req = ConcurClient::with_json(req, &body)?;
58        self.client.execute(req).await
59    }
60
61    /// Get a user by UUID.
62    pub async fn get_user(&self, id: &str) -> Result<User> {
63        self.client
64            .request(Method::GET, &format!("/profile/identity/v4.1/Users/{}", id))
65            .await?
66            .send()
67            .await?
68            .json()
69            .await
70            .map_err(Into::into)
71    }
72
73    /// Create a new user.
74    pub async fn create_user(&self, user: &User) -> Result<User> {
75        let req = self
76            .client
77            .request(Method::POST, "/profile/identity/v4.1/Users")
78            .await?;
79        let req = ConcurClient::with_json(req, user)?;
80        self.client.execute(req).await
81    }
82
83    /// Update a user with PATCH (RFC 7644).
84    pub async fn patch_user(&self, id: &str, ops: &[PatchOp]) -> Result<User> {
85        let body = PatchRequest {
86            schemas: vec!["urn:ietf:params:scim:api:messages:2.0:PatchOp".to_string()],
87            operations: ops.to_vec(),
88        };
89        let req = self
90            .client
91            .request(
92                Method::PATCH,
93                &format!("/profile/identity/v4.1/Users/{}", id),
94            )
95            .await?;
96        let req = ConcurClient::with_json(req, &body)?;
97        self.client.execute(req).await
98    }
99
100    /// Delete a user.
101    pub async fn delete_user(&self, id: &str) -> Result<()> {
102        self.client
103            .execute_unit(
104                self.client
105                    .request(
106                        Method::DELETE,
107                        &format!("/profile/identity/v4.1/Users/{}", id),
108                    )
109                    .await?,
110            )
111            .await
112    }
113}
114
115#[derive(Debug, Clone, Serialize, Deserialize)]
116pub struct SearchRequest {
117    pub schemas: Vec<String>,
118    pub filter: String,
119    #[serde(skip_serializing_if = "Option::is_none")]
120    pub attributes: Option<Vec<String>>,
121}
122
123#[derive(Debug, Clone, Serialize, Deserialize)]
124pub struct PatchRequest {
125    pub schemas: Vec<String>,
126    pub operations: Vec<PatchOp>,
127}
128
129#[derive(Debug, Clone, Serialize, Deserialize)]
130pub struct PatchOp {
131    pub op: String,
132    #[serde(skip_serializing_if = "Option::is_none")]
133    pub path: Option<String>,
134    #[serde(skip_serializing_if = "Option::is_none")]
135    pub value: Option<serde_json::Value>,
136}
137
138#[derive(Debug, Clone, Serialize, Deserialize)]
139pub struct UserList {
140    pub schemas: Vec<String>,
141    #[serde(rename = "totalResults")]
142    pub total_results: u64,
143    #[serde(rename = "itemsPerPage")]
144    pub items_per_page: u32,
145    pub resources: Vec<User>,
146}
147
148#[derive(Debug, Clone, Default, Serialize, Deserialize)]
149pub struct User {
150    pub schemas: Vec<String>,
151    pub id: Option<String>,
152    #[serde(skip_serializing_if = "Option::is_none")]
153    pub user_name: Option<String>,
154    #[serde(skip_serializing_if = "Option::is_none")]
155    pub active: Option<bool>,
156    #[serde(skip_serializing_if = "Option::is_none")]
157    pub display_name: Option<String>,
158    #[serde(skip_serializing_if = "Option::is_none")]
159    pub name: Option<UserName>,
160    #[serde(skip_serializing_if = "Option::is_none")]
161    pub emails: Option<Vec<Email>>,
162    #[serde(skip_serializing_if = "Option::is_none")]
163    pub addresses: Option<Vec<Address>>,
164    #[serde(skip_serializing_if = "Option::is_none")]
165    pub meta: Option<Meta>,
166    #[serde(skip_serializing_if = "Option::is_none")]
167    pub external_id: Option<String>,
168    #[serde(
169        rename = "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User",
170        skip_serializing_if = "Option::is_none"
171    )]
172    pub enterprise_extension: Option<EnterpriseExtension>,
173    #[serde(
174        rename = "urn:ietf:params:scim:schemas:extension:sap:2.0:User",
175        skip_serializing_if = "Option::is_none"
176    )]
177    pub sap_extension: Option<serde_json::Value>,
178}
179
180#[derive(Debug, Clone, Default, Serialize, Deserialize)]
181pub struct EnterpriseExtension {
182    #[serde(skip_serializing_if = "Option::is_none")]
183    pub company_id: Option<String>,
184    #[serde(skip_serializing_if = "Option::is_none")]
185    pub employee_number: Option<String>,
186    #[serde(skip_serializing_if = "Option::is_none")]
187    pub cost_center: Option<String>,
188    #[serde(skip_serializing_if = "Option::is_none")]
189    pub manager: Option<serde_json::Value>,
190    #[serde(skip_serializing_if = "Option::is_none")]
191    pub start_date: Option<String>,
192    #[serde(skip_serializing_if = "Option::is_none")]
193    pub termination_date: Option<String>,
194}