rusaint 0.16.3

Easy-to-use SSU u-saint client
Documentation
use std::sync::Arc;

use url::Url;
use wdpe::{
    body::Body,
    command::{
        WebDynproCommandExecutor as _,
        element::system::{
            ClientInspectorNotifyEventCommand, CustomClientInfoEventCommand,
            LoadingPlaceholderLoadEventCommand,
        },
    },
    define_elements,
    element::{
        parser::ElementParser,
        system::{ClientInspector, Custom, CustomClientInfo, LoadingPlaceholder},
    },
    error::WebDynproError,
    event::{Event, event_queue::EnqueueEventResult},
    requests::WebDynproRequests as _,
    state::{EventProcessResult, WebDynproState},
};

use crate::{RusaintError, USaintSession, utils::DEFAULT_USER_AGENT};

const SSU_WEBDYNPRO_BASE_URL: &str = "https://ecc.ssu.ac.kr/sap/bc/webdynpro/SAP/";
const INITIAL_CLIENT_DATA_WD01: &str = "ClientWidth:1920px;ClientHeight:1000px;ScreenWidth:1920px;ScreenHeight:1080px;ScreenOrientation:landscape;ThemedTableRowHeight:33px;ThemedFormLayoutRowHeight:32px;ThemedSvgLibUrls:{\"SAPGUI-icons\":\"https://ecc.ssu.ac.kr:8443/sap/public/bc/ur/nw5/themes/~cache-20210223121230/Base/baseLib/sap_fiori_3/svg/libs/SAPGUI-icons.svg\",\"SAPWeb-icons\":\"https://ecc.ssu.ac.kr:8443/sap/public/bc/ur/nw5/themes/~cache-20210223121230/Base/baseLib/sap_fiori_3/svg/libs/SAPWeb-icons.svg\"};ThemeTags:Fiori_3,Touch;ThemeID:sap_fiori_3;SapThemeID:sap_fiori_3;DeviceType:DESKTOP";
const INITIAL_CLIENT_DATA_WD02: &str = "ThemedTableRowHeight:25px";
/// u-saint에 접속하기 위한 기본 클라이언트
#[derive(Debug)]
pub struct USaintClient {
    state: WebDynproState,
    client: reqwest::Client,
}

impl<'a> USaintClient {
    define_elements! {
        CLIENT_INSPECTOR_WD01: ClientInspector<'a> = "WD01";
        CLIENT_INSPECTOR_WD02: ClientInspector<'a> = "WD02";
        LOADING_PLACEHOLDER: LoadingPlaceholder<'a> = "_loadingPlaceholder_";
    }

    const CUSTOM: Custom = Custom::new(std::borrow::Cow::Borrowed("WD01"));

    async fn new(
        state: WebDynproState,
        client: reqwest::Client,
    ) -> Result<USaintClient, WebDynproError> {
        let mut client = USaintClient { state, client };
        client.load_placeholder().await?;
        Ok(client)
    }

    /// WebDynpro 애플리케이션의 이름을 반환합니다.
    pub fn name(&self) -> &str {
        self.state.name()
    }

    /// WebDynpro 애플리케이션의 기본 URL을 반환합니다.
    pub fn base_url(&self) -> &Url {
        self.state.base_url()
    }

    /// 내부 reqwest 클라이언트의 참조를 반환합니다.
    pub fn http_client(&self) -> &reqwest::Client {
        &self.client
    }

    /// WebDynpro 애플리케이션의 페이지 문서를 반환합니다.
    pub fn body(&self) -> &Body {
        self.state.body()
    }

    /// 실제로 요청하는 애플리케이션의 URL을 반환합니다.
    pub fn client_url(&self) -> String {
        self.state.client_url()
    }

    /// 페이지를 새로고침합니다.
    pub async fn reload(&mut self) -> Result<(), WebDynproError> {
        let body = self
            .client
            .navigate(self.state.base_url(), self.state.name())
            .await?;
        self.state = WebDynproState::new(
            self.state.base_url().clone(),
            self.state.name().to_string(),
            body,
        );
        self.load_placeholder().await?;
        Ok(())
    }

    /// 이벤트를 처리합니다. [`process_event()`](WebDynproClient::process_event)를 참조하세요.
    pub async fn process_event(
        &mut self,
        force_send: bool,
        event: Event,
    ) -> Result<EventProcessResult, WebDynproError> {
        let enqueue_result = self.state.add_event(event).await;

        if (matches!(enqueue_result, EnqueueEventResult::ShouldProcess)) || force_send {
            let serialized_events = self.state.serialize_and_clear_with_form_event().await?;
            let update = {
                self.client
                    .send_events(
                        self.state.base_url(),
                        self.state.body().ssr_client(),
                        &serialized_events,
                    )
                    .await?
            };
            let result = self.state.mutate_body(update)?;
            Ok(EventProcessResult::Sent(result))
        } else {
            Ok(EventProcessResult::Enqueued)
        }
    }

    async fn load_placeholder(&mut self) -> Result<(), WebDynproError> {
        let parser = ElementParser::new(self.body());
        let notify_wd01 = parser.read(ClientInspectorNotifyEventCommand::new(
            Self::CLIENT_INSPECTOR_WD01,
            INITIAL_CLIENT_DATA_WD01,
        ))?;
        let notify_wd02 = parser.read(ClientInspectorNotifyEventCommand::new(
            Self::CLIENT_INSPECTOR_WD02,
            INITIAL_CLIENT_DATA_WD02,
        ))?;
        let load = parser.read(LoadingPlaceholderLoadEventCommand::new(
            Self::LOADING_PLACEHOLDER,
        ))?;
        let custom = parser.read(CustomClientInfoEventCommand::new(
            Self::CUSTOM,
            CustomClientInfo {
                client_url: self.client_url(),
                document_domain: "ssu.ac.kr".to_owned(),
                ..CustomClientInfo::default()
            },
        ))?;
        self.process_event(false, notify_wd01).await?;
        self.process_event(false, notify_wd02).await?;
        self.process_event(false, load).await?;
        self.process_event(false, custom).await?;
        Ok(())
    }
}

/// U-Saint 애플리케이션이 구현하는 트레이트
pub trait USaintApplication: Sized {
    /// U-Saint WebDynpro 애플리케이션 이름
    const APP_NAME: &'static str;

    /// U-Saint 클라이언트를 애플리케이션으로 변환합니다.
    fn from_client(client: USaintClient) -> Result<Self, RusaintError>;
}

/// 새로운 [`USaintClient`]를 생성하는 빌더
pub struct USaintClientBuilder {
    session: Option<Arc<USaintSession>>,
}

impl USaintClientBuilder {
    /// 새로운 빌더를 만듭니다.
    pub fn new() -> USaintClientBuilder {
        USaintClientBuilder { session: None }
    }

    /// 빌더에 [`USaintSession`]을 추가합니다.
    pub fn session(mut self, session: Arc<USaintSession>) -> USaintClientBuilder {
        self.session = Some(session);
        self
    }

    /// 애플리케이션 이름과 함께 [`USaintClient`]을 생성합니다.
    pub async fn build(self, name: &str) -> Result<USaintClient, WebDynproError> {
        #[cfg(feature = "rustls-no-provider")]
        {
            let _ = rustls::crypto::ring::default_provider().install_default();
        }
        let base_url = Url::parse(SSU_WEBDYNPRO_BASE_URL).unwrap();

        let builder = if let Some(session) = self.session {
            reqwest::Client::builder().cookie_provider(session)
        } else {
            reqwest::Client::builder()
        };

        let client = builder.user_agent(DEFAULT_USER_AGENT).build().unwrap();

        let body = client.navigate(&base_url, name).await?;
        let state = WebDynproState::new(base_url, name.to_string(), body);
        USaintClient::new(state, client).await
    }

    /// 특정 [`USaintApplication`]을 만듭니다.
    pub async fn build_into<T: USaintApplication>(self) -> Result<T, RusaintError> {
        let name = T::APP_NAME;
        let client = self.build(name).await?;
        T::from_client(client)
    }
}

impl Default for USaintClientBuilder {
    fn default() -> Self {
        Self::new()
    }
}