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}