z_osmf 0.13.5

The Rust z/OSMF Client
Documentation
use std::collections::HashMap;
use std::marker::PhantomData;
use std::sync::Arc;

use bytes::Bytes;
use serde::{Deserialize, Serialize};
use z_osmf_macros::Endpoint;

use crate::convert::TryFromResponse;
use crate::ClientCore;

use super::get_subsystem;

#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
pub enum JclData {
    Binary(Bytes),
    Record(Bytes),
    Text(String),
}

#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
pub enum JobSource {
    Dataset(String),
    File(String),
    Jcl(JclData),
}

#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
pub enum JobNotificationEvent {
    Active,
    Complete,
    Ready,
}

#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
pub enum JobRecordFormat {
    Fixed,
    Variable,
}

impl From<JobRecordFormat> for reqwest::header::HeaderValue {
    fn from(value: JobRecordFormat) -> Self {
        match value {
            JobRecordFormat::Fixed => "F",
            JobRecordFormat::Variable => "V",
        }
        .try_into()
        .unwrap()
    }
}

#[derive(Clone, Debug, Endpoint)]
#[endpoint(method = put, path = "/zosmf/restjobs/jobs{subsystem}")]
pub struct JobSubmitBuilder<T>
where
    T: TryFromResponse,
{
    core: Arc<ClientCore>,

    #[endpoint(path, builder_fn = build_subsystem)]
    subsystem: Option<Arc<str>>,
    #[endpoint(header = "X-IBM-Intrdr-Class", skip_setter)]
    message_class: Option<Arc<str>>,
    #[endpoint(header = "X-IBM-Intrdr-Recfm")]
    record_format: Option<JobRecordFormat>,
    #[endpoint(header = "X-IBM-Intrdr-Lrecl")]
    record_length: Option<i32>,
    #[endpoint(header = "X-IBM-User-Correlator")]
    user_correlator: Option<Arc<str>>,
    #[endpoint(builder_fn = build_symbols)]
    symbols: Option<HashMap<Arc<str>, Arc<str>>>,
    #[endpoint(builder_fn = build_jcl_source)]
    jcl_source: JobSource,
    #[endpoint(header = "X-IBM-Notification-URL")]
    notification_url: Option<Arc<str>>,
    #[endpoint(builder_fn = build_notification_events)]
    notification_events: Option<Arc<[JobNotificationEvent]>>,
    #[endpoint(header = "X-IBM-Intrdr-File-Encoding")]
    encoding: Option<Arc<str>>,

    target_type: PhantomData<T>,
}

impl<T> JobSubmitBuilder<T>
where
    T: TryFromResponse,
{
    pub fn message_class<C>(mut self, value: C) -> Self
    where
        C: Into<char>,
    {
        self.message_class = Some(value.into().to_string().into());

        self
    }
}

#[derive(Serialize)]
struct Source<'a> {
    file: &'a str,
}

fn build_jcl_source<T>(
    request_builder: reqwest::RequestBuilder,
    builder: &JobSubmitBuilder<T>,
) -> reqwest::RequestBuilder
where
    T: TryFromResponse,
{
    match &builder.jcl_source {
        JobSource::Dataset(dataset) => request_builder
            .header("Content-Type", "application/json")
            .json(&Source {
                file: &format!("//'{}'", dataset),
            }),
        JobSource::File(file) => request_builder
            .header("Content-Type", "application/json")
            .json(&Source { file }),
        JobSource::Jcl(JclData::Binary(binary)) => request_builder
            .header("Content-Type", "application/octet-stream")
            .header("X-IBM-Intrdr-Mode", "BINARY")
            .body(binary.clone()),
        JobSource::Jcl(JclData::Record(record)) => request_builder
            .header("Content-Type", "application/octet-stream")
            .header("X-IBM-Intrdr-Mode", "RECORD")
            .body(record.clone()),
        JobSource::Jcl(JclData::Text(text)) => request_builder
            .header("Content-Type", "text/plain")
            .header("X-IBM-Intrdr-Mode", "TEXT")
            .body(text.to_string()),
    }
}

fn build_notification_events<T>(
    mut request_builder: reqwest::RequestBuilder,
    builder: &JobSubmitBuilder<T>,
) -> reqwest::RequestBuilder
where
    T: TryFromResponse,
{
    if let Some(events) = &builder.notification_events {
        if !events.is_empty() {
            let mut events: Vec<&'static str> = events
                .iter()
                .map(|e| match e {
                    JobNotificationEvent::Active => "active",
                    JobNotificationEvent::Complete => "complete",
                    JobNotificationEvent::Ready => "ready",
                })
                .collect();
            events.sort_unstable();
            events.dedup();

            let header_value = format!(
                r#"{{"events": [{}]}}"#,
                events
                    .iter()
                    .map(|e| format!(r#""{}""#, e))
                    .collect::<Vec<_>>()
                    .join(", ")
            );
            request_builder = request_builder.header("X-IBM-Notification-Options", header_value);
        }
    }

    request_builder
}

fn build_subsystem<T>(builder: &JobSubmitBuilder<T>) -> String
where
    T: TryFromResponse,
{
    get_subsystem(&builder.subsystem)
}

fn build_symbols<T>(
    mut request_builder: reqwest::RequestBuilder,
    builder: &JobSubmitBuilder<T>,
) -> reqwest::RequestBuilder
where
    T: TryFromResponse,
{
    if let Some(symbols) = &builder.symbols {
        for (name, value) in symbols.iter() {
            request_builder =
                request_builder.header(format!("X-IBM-JCL-Symbol-{}", name), value.as_ref());
        }
    }

    request_builder
}

#[cfg(test)]
mod tests {
    use crate::tests::*;

    use super::*;

    #[test]
    fn example_1() {
        let zosmf = get_zosmf();

        let jcl = r#"//TESTJOBX JOB (),MSGCLASS=H
        // EXEC PGM=IEFBR14
        "#;

        let manual_request = zosmf
            .core
            .client
            .put("https://test.com/zosmf/restjobs/jobs")
            .header("X-IBM-Intrdr-Class", "A")
            .header("X-IBM-Intrdr-Recfm", "F")
            .header("X-IBM-Intrdr-Lrecl", "80")
            .header("Content-Type", "text/plain")
            .header("X-IBM-Intrdr-Mode", "TEXT")
            .body(jcl.to_string())
            .build()
            .unwrap();

        let job_data = zosmf
            .jobs()
            .submit(JobSource::Jcl(JclData::Text(jcl.into())))
            .message_class('A')
            .record_format(JobRecordFormat::Fixed)
            .record_length(80)
            .get_request()
            .unwrap();

        assert_eq!(format!("{:?}", manual_request), format!("{:?}", job_data));

        assert_eq!(
            manual_request.body().unwrap().as_bytes(),
            job_data.body().unwrap().as_bytes()
        )
    }

    #[test]
    fn notification_events() {
        let zosmf = get_zosmf();

        let jcl = r#"//TESTJOBX JOB (),MSGCLASS=H
        // EXEC PGM=IEFBR14
        "#;

        let manual_request = zosmf
            .core
            .client
            .put("https://test.com/zosmf/restjobs/jobs")
            .header("Content-Type", "text/plain")
            .header("X-IBM-Intrdr-Mode", "TEXT")
            .header(
                "X-IBM-Notification-Options",
                r#"{"events": ["active", "ready"]}"#,
            )
            .body(jcl.to_string())
            .build()
            .unwrap();

        let job_data = zosmf
            .jobs()
            .submit(JobSource::Jcl(JclData::Text(jcl.into())))
            .notification_events([JobNotificationEvent::Active, JobNotificationEvent::Ready])
            .get_request()
            .unwrap();

        assert_eq!(format!("{:?}", manual_request), format!("{:?}", job_data));

        assert_eq!(
            manual_request.body().unwrap().as_bytes(),
            job_data.body().unwrap().as_bytes()
        )
    }
}