wdpe

Crates.iowdpe
lib.rswdpe
version0.2.1
created_at2025-07-31 14:15:34.775773+00
updated_at2025-09-12 07:27:04.890683+00
descriptionWebDynpro Parse Engine
homepage
repositoryhttps://github.com/EATSTEAK/wdpe
max_upload_size
id1775129
size340,203
Hyomin Koo (EATSTEAK)

documentation

README

WDPE - WebDynpro Parse Engine

WDPE는 WebDynpro 페이지 스크래핑과 입력 조작을 위한 기반 엔진입니다. WebDynpro 의 Lightspeed 라이브러리를 통해 구현되는 페이지에 대한 분석을 수행할 수 있습니다.

WebDynpro 페이지 분석하기

직접 WebDynpro 페이지를 분석하여 이에 대한 자동화된 애플리케이션을 구현하려면, 애플리케이션에서 조작하고자 하는 주요 요소들에 대해 정의하는 작업이 선행되어야 합니다.

대부분의 조작하고자 하는 요소는 WebDynpro 엘리먼트로 구성되어 있으므로, 조작/분석하고자 하는 엘리먼트의 종류, ID를 미리 컴파일 타임에 정의하여 파싱/조작 작업을 원활히 수행할 수 있습니다.

페이지 내 엘리먼트가 정적인 ID를 가지도록 하기

기본적으로 WebDynpro 페이지의 엘리먼트 ID는 동적으로 구성되도록 되어 있습니다. 대략적으로 엘리먼트가 렌더링되는 순서대로 ID가 부여되며, 따라서 동일한 위치에 있는 동일한 엘리먼트임에도 불구하고 수행한 동작의 순서나 종류에 따라 다른 ID를 가질 수 있습니다.

이러한 현상을 방지하려면 WebDynpro 에서 자동 테스팅 목적으로 제공하는 파라메터 SAP-WD-STABLEIDSX 로 표시하면 정적인 ID를 가진 페이지를 확인할 수 있습니다. 예시

엘리먼트의 종류 알아내기

엘리먼트의 종류는 해당 엘리먼트 태그의 ct HTML 어트리뷰트 값을 rusaint WebDynpro 엘리먼트의 [CONTROL_ID]와 매칭시켜 알아낼 수 있습니다.

예시: ct="B" 어트리뷰트의 경우 WebDynpro 엘리먼트의 [Button]과 매칭됩니다.

엘리먼트의 종류나 렌더링 방법에 따라 엘리먼트 자체를 감싸는 태그가 있을 수 있습니다. 이런 태그는 보통 해당 엘리먼트의 ID 뒤에 -r 접미사가 붙는것으로 확인할 수 있습니다.

애플리케이션 정의 예시

예시 애플리케이션 상단의 "담당자문의 정보" 텍스트를 파싱하는 예시 애플리케이션입니다.

추가 정보는 [LoadingPlaceholder], [ClientInspector] 와 [Custom] 엘리먼트를 참고하십시오.

use futures::executor::block_on;
use wdpe::event::event_queue::EnqueueEventResult;
use wdpe::requests::WebDynproRequests as _;
use wdpe::requests::{EventProcessResult, WebDynproState};
use wdpe::{define_elements, element::{text::Caption, system::{ClientInspector, Custom, CustomClientInfo, LoadingPlaceholder}}};

// 상태 관리를 위한 `WebDynproState`, 웹 요청을 위한 `reqwest::Client`를 가진 ExampleApplication
pub struct ExampleApplication {
  client: WebDynproState,
  client: reqwest::Client
};

const DEFAULT_USER_AGENT: &str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36";
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";

impl<'a> ExampleApplication {
    // 엘리먼트를 정의하는 매크로
    define_elements! {
        // 담당자문의 정보에 해당하는 캡션의 ID 정의
        CAPTION: Caption<'a> = "ZCMW_DEVINFO_RE.ID_D080C16F227F4D68751326DC40BB6BE0:MAIN.CAPTION";
        CLIENT_INSPECTOR_WD01: ClientInspector<'a> = "WD01";
        CLIENT_INSPECTOR_WD02: ClientInspector<'a> = "WD02";
        LOADING_PLACEHOLDER: LoadingPlaceholder<'a> = "_loadingPlaceholder_";
    }

    // 애플리케이션의 생성자
    pub async fn new() -> Result<ExampleApplication, WebDynproError> {
        let client = reqwest::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);
        let mut app = ExampleApplication { state, client };
        app.load_placeholder().await?;
        Ok(app)
    }


    // 캡션의 데이터를 읽는 함수
    pub fn read_caption(&self) -> Result<String, WebDynproError> {
        // 엘리먼트 파서를 생성
        let parser = ElementParser::new(self.client.body());
        // 캡션 정의와 현재 애플리케이션 바디로부터 엘리먼트 객체 생성
        let caption = parser.element_from_def(&Self::CAPTION)?;
        // 캡션 엘리먼트로부터 텍스트를 반환
        Ok(caption.text().to_string())
    }

    // 이벤트를 처리합니다.
    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?
            };
            self.state.mutate_body(update)?;
            Ok(EventProcessResult::Sent)
        } 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(())
    }
}

async fn test_caption() {
    let app = ExampleApplication::new().await.unwrap();
    let text = app.read_caption().unwrap();
    assert_eq!(text, "담당자문의 정보");
}

fn main() {
    block_on(test_caption());
}
Commit count: 11

cargo fmt