z_osmf/lib.rs
1//! # z_osmf
2//!
3//! The VERY work in progress Rust z/OSMF<sup>TM</sup> Client.
4//!
5//! ## Examples
6//!
7//! Create a ZOsmf client and authenticate:
8//! ```
9//! # async fn example() -> z_osmf::Result<()> {
10//! let client = reqwest::Client::new();
11//! let base_url = "https://mainframe.my-company.com";
12//! let zosmf = z_osmf::ZOsmf::new(client, base_url);
13//! zosmf.login("USERNAME", "PASSWORD").await?;
14//! # Ok(())
15//! # }
16//! ```
17//!
18//! List your datasets:
19//! ```
20//! # async fn example(zosmf: z_osmf::ZOsmf) -> z_osmf::Result<()> {
21//! let my_datasets = zosmf
22//! .datasets()
23//! .list("USERNAME")
24//! .build()
25//! .await?;
26//! for dataset in my_datasets.items().iter() {
27//! println!("{}", dataset.name());
28//! }
29//! # Ok(())
30//! # }
31//! ```
32//!
33//! List the files in your home directory:
34//! ```
35//! # async fn example(zosmf: z_osmf::ZOsmf) -> z_osmf::Result<()> {
36//! let my_files = zosmf
37//! .files()
38//! .list("/u/username")
39//! .build()
40//! .await?;
41//! for file in my_files.items().iter() {
42//! println!("{}", file.name());
43//! }
44//! # Ok(())
45//! # }
46//! ```
47//!
48//! List all active jobs:
49//! ```
50//! # async fn example(zosmf: z_osmf::ZOsmf) -> z_osmf::Result<()> {
51//! let active_jobs = zosmf
52//! .jobs()
53//! .list()
54//! .owner("*")
55//! .active_only(true)
56//! .build()
57//! .await?;
58//! for job in active_jobs.items().iter() {
59//! println!("{}", job.name());
60//! }
61//! # Ok(())
62//! # }
63//! ```
64
65#![cfg_attr(docsrs, feature(doc_auto_cfg))]
66#![forbid(unsafe_code)]
67
68pub use bytes::Bytes;
69
70pub use self::error::{Error, Result};
71
72pub mod info;
73pub mod error;
74
75#[cfg(feature = "datasets")]
76pub mod datasets;
77#[cfg(feature = "files")]
78pub mod files;
79#[cfg(feature = "jobs")]
80pub mod jobs;
81#[cfg(any(feature = "datasets", feature = "files"))]
82pub mod restfiles;
83#[cfg(feature = "system-variables")]
84pub mod system_variables;
85#[cfg(feature = "workflows")]
86pub mod workflows;
87
88use std::sync::{Arc, RwLock};
89
90use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
91use serde::{Deserialize, Serialize};
92
93use self::error::CheckStatus;
94
95mod convert;
96mod utils;
97
98/// # ZOsmf
99///
100/// Client for interacting with z/OSMF.
101///
102/// ```
103/// # async fn example() -> anyhow::Result<()> {
104/// # use z_osmf::ZOsmf;
105/// let client = reqwest::Client::new();
106/// let base_url = "https://zosmf.mainframe.my-company.com";
107/// let username = "USERNAME";
108///
109/// let zosmf = ZOsmf::new(client, base_url);
110/// zosmf.login(username, "PASSWORD").await?;
111///
112/// let my_datasets = zosmf.datasets().list(username).build().await?;
113///
114/// for dataset in my_datasets.items().iter() {
115/// println!("{:#?}", dataset);
116/// }
117/// # Ok(())
118/// # }
119/// ```
120#[derive(Clone, Debug)]
121pub struct ZOsmf {
122 core: ClientCore,
123}
124
125impl ZOsmf {
126 /// Create a new z/OSMF client.
127 ///
128 /// # Example
129 /// ```
130 /// # async fn example() {
131 /// # use z_osmf::ZOsmf;
132 /// let client = reqwest::Client::new();
133 /// let url = "https://zosmf.mainframe.my-company.com";
134 ///
135 /// let zosmf = ZOsmf::new(client, url);
136 /// # }
137 /// ```
138 pub fn new<U>(client: reqwest::Client, url: U) -> Self
139 where
140 U: std::fmt::Display,
141 {
142 let token = Arc::new(RwLock::new(None));
143 let url = url.to_string().into();
144
145 let core = ClientCore { client, token, url };
146
147 ZOsmf { core }
148 }
149
150 /// Retrieve information about z/OSMF.
151 ///
152 /// # Example
153 /// ```
154 /// # async fn example(zosmf: z_osmf::ZOsmf) -> anyhow::Result<()> {
155 /// let info = zosmf.info().await?;
156 /// # Ok(())
157 /// # }
158 /// ```
159 pub async fn info(&self) -> Result<info::Info> {
160 info::InfoBuilder::new(self.core.clone()).build().await
161 }
162
163 /// Authenticate with z/OSMF.
164 ///
165 /// # Example
166 /// ```
167 /// # async fn example(zosmf: z_osmf::ZOsmf) -> anyhow::Result<()> {
168 /// let auth_tokens = zosmf.login("USERNAME", "PASSWORD").await?;
169 /// # Ok(())
170 /// # }
171 /// ```
172 pub async fn login<U, P>(&self, username: U, password: P) -> Result<Vec<AuthToken>>
173 where
174 U: std::fmt::Display,
175 P: std::fmt::Display,
176 {
177 let response = self
178 .core
179 .client
180 .post(format!("{}/zosmf/services/authenticate", self.core.url))
181 .basic_auth(username, Some(password))
182 .send()
183 .await?
184 .check_status()
185 .await?;
186
187 let mut tokens: Vec<AuthToken> = response
188 .headers()
189 .get_all(reqwest::header::SET_COOKIE)
190 .iter()
191 .flat_map(|header_value| header_value.try_into().ok())
192 .collect();
193 tokens.sort_unstable();
194
195 self.set_token(tokens.first().cloned())?;
196
197 Ok(tokens)
198 }
199
200 /// Logout of z/OSMF.
201 ///
202 /// <p style="background:rgba(255,181,77,0.16);padding:0.75em;">
203 /// <strong>Warning:</strong> Logging out before an action has completed,
204 /// like immediately after submitting a job, can cause the action to fail.
205 /// </p>
206 ///
207 /// # Example
208 /// ```
209 /// # async fn example(zosmf: z_osmf::ZOsmf) -> anyhow::Result<()> {
210 /// zosmf.logout().await?;
211 /// # Ok(())
212 /// # }
213 /// ```
214 pub async fn logout(&self) -> Result<()> {
215 self.core
216 .client
217 .delete(format!("{}/zosmf/services/authenticate", self.core.url))
218 .send()
219 .await?
220 .check_status()
221 .await?;
222
223 self.set_token(None)?;
224
225 Ok(())
226 }
227
228 /// Create a sub-client for interacting with datasets.
229 ///
230 /// # Example
231 /// ```
232 /// # async fn example(zosmf: z_osmf::ZOsmf) -> anyhow::Result<()> {
233 /// let datasets_client = zosmf.datasets();
234 /// # Ok(())
235 /// # }
236 /// ```
237 #[cfg(feature = "datasets")]
238 pub fn datasets(&self) -> datasets::DatasetsClient {
239 datasets::DatasetsClient::new(self.core.clone())
240 }
241
242 /// Create a sub-client for interacting with files.
243 ///
244 /// # Example
245 /// ```
246 /// # async fn example(zosmf: z_osmf::ZOsmf) -> anyhow::Result<()> {
247 /// let files_client = zosmf.files();
248 /// # Ok(())
249 /// # }
250 /// ```
251 #[cfg(feature = "files")]
252 pub fn files(&self) -> files::FilesClient {
253 files::FilesClient::new(self.core.clone())
254 }
255
256 /// Create a sub-client for interacting with jobs.
257 ///
258 /// # Example
259 /// ```
260 /// # async fn example(zosmf: z_osmf::ZOsmf) -> anyhow::Result<()> {
261 /// let jobs_client = zosmf.jobs();
262 /// # Ok(())
263 /// # }
264 /// ```
265 #[cfg(feature = "jobs")]
266 pub fn jobs(&self) -> jobs::JobsClient {
267 jobs::JobsClient::new(self.core.clone())
268 }
269
270 /// Create a sub-client for interacting with system symbols and variables.
271 ///
272 /// # Example
273 /// ```
274 /// # async fn example(zosmf: z_osmf::ZOsmf) -> anyhow::Result<()> {
275 /// let system_variables = zosmf.system_variables();
276 /// # Ok(())
277 /// # }
278 /// ```
279 #[cfg(feature = "system-variables")]
280 pub fn system_variables(&self) -> system_variables::SystemVariablesClient {
281 system_variables::SystemVariablesClient::new(self.core.clone())
282 }
283
284 /// Create a sub-client for interacting with workflows.
285 ///
286 /// # Example
287 /// ```
288 /// # async fn example(zosmf: z_osmf::ZOsmf) -> anyhow::Result<()> {
289 /// let workflows = zosmf.workflows();
290 /// # Ok(())
291 /// # }
292 /// ```
293 #[cfg(feature = "workflows")]
294 pub fn workflows(&self) -> workflows::WorkflowsClient {
295 workflows::WorkflowsClient::new(self.core.clone())
296 }
297
298 fn set_token(&self, token: Option<AuthToken>) -> Result<()> {
299 let mut write = self
300 .core
301 .token
302 .write()
303 .map_err(|err| Error::RwLockPoisonError(err.to_string()))?;
304 *write = token;
305
306 Ok(())
307 }
308}
309
310#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
311pub enum AuthToken {
312 Jwt(String),
313 Ltpa2(String),
314}
315
316impl std::str::FromStr for AuthToken {
317 type Err = Error;
318
319 fn from_str(s: &str) -> Result<Self> {
320 let (name, value) = s
321 .split_once(';')
322 .ok_or(Error::InvalidValue(format!(
323 "invalid set-cookie header value: {}",
324 s
325 )))?
326 .0
327 .split_once('=')
328 .ok_or(Error::InvalidValue(format!(
329 "invalid set-cookie header value: {}",
330 s
331 )))?;
332
333 let token = match name {
334 "jwtToken" => AuthToken::Jwt(value.to_string()),
335 "LtpaToken2" => AuthToken::Ltpa2(value.to_string()),
336 _ => return Err(Error::InvalidValue(format!("invalid token name: {}", name))),
337 };
338
339 Ok(token)
340 }
341}
342
343impl TryFrom<&HeaderValue> for AuthToken {
344 type Error = Error;
345
346 fn try_from(value: &HeaderValue) -> Result<Self> {
347 value.to_str()?.parse()
348 }
349}
350
351impl std::fmt::Display for AuthToken {
352 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
353 let s = match self {
354 AuthToken::Jwt(token) => format!("jwtToken={};", token),
355 AuthToken::Ltpa2(token) => format!("LtpaToken2={};", token),
356 };
357
358 write!(f, "{}", s)
359 }
360}
361
362impl From<&AuthToken> for (HeaderName, HeaderValue) {
363 fn from(value: &AuthToken) -> Self {
364 match value {
365 AuthToken::Jwt(token_value) => (
366 reqwest::header::AUTHORIZATION,
367 format!("Bearer {}", token_value).parse().unwrap(),
368 ),
369 AuthToken::Ltpa2(_) => (reqwest::header::COOKIE, value.to_string().parse().unwrap()),
370 }
371 }
372}
373
374impl From<&AuthToken> for HeaderMap {
375 fn from(value: &AuthToken) -> Self {
376 let (key, val) = value.into();
377
378 let mut headers = HeaderMap::new();
379 headers.insert(key, val);
380
381 headers
382 }
383}
384
385#[derive(Clone, Debug)]
386struct ClientCore {
387 client: reqwest::Client,
388 token: Arc<RwLock<Option<AuthToken>>>,
389 url: Arc<str>,
390}
391
392#[cfg(test)]
393mod tests {
394 use super::*;
395
396 pub(crate) fn get_zosmf() -> ZOsmf {
397 ZOsmf::new(reqwest::Client::new(), "https://test.com")
398 }
399
400 pub(crate) trait GetJson {
401 fn json(&self) -> Option<serde_json::Value>;
402 }
403
404 impl GetJson for reqwest::Request {
405 fn json(&self) -> Option<serde_json::Value> {
406 Some(
407 serde_json::from_slice(self.body()?.as_bytes()?)
408 .expect("failed to deserialize JSON"),
409 )
410 }
411 }
412}