Skip to main content

playhard_cdp/
client.rs

1use crate::{
2    error::{CdpError, Result},
3    types::{CdpRequest, CdpResponse, CdpValue},
4};
5use serde::de::DeserializeOwned;
6use serde::Serialize;
7use std::sync::atomic::{AtomicU64, Ordering};
8
9/// Abstract transport boundary for the typed CDP client.
10///
11/// A future `playhard-transport` crate can implement this trait directly.
12#[allow(async_fn_in_trait)]
13pub trait CdpTransport: Send + Sync {
14    /// Send a single CDP request and return the matching response.
15    async fn send(&self, request: CdpRequest) -> Result<CdpResponse>;
16}
17
18/// Typed command interface.
19pub trait Command {
20    /// Command parameters.
21    type Params: Serialize;
22    /// Command result.
23    type Output;
24    /// Command method string.
25    const METHOD: &'static str;
26
27    /// Decode the protocol response into the typed output.
28    fn decode(response: CdpResponse) -> Result<Self::Output>;
29}
30
31/// Async typed CDP client.
32pub struct CdpClient<T> {
33    transport: T,
34    next_id: AtomicU64,
35}
36
37impl<T> CdpClient<T> {
38    /// Create a new client over a transport.
39    pub const fn new(transport: T) -> Self {
40        Self {
41            transport,
42            next_id: AtomicU64::new(1),
43        }
44    }
45
46    /// Access the underlying transport.
47    pub const fn transport(&self) -> &T {
48        &self.transport
49    }
50}
51
52impl<T> CdpClient<T>
53where
54    T: CdpTransport,
55{
56    /// Send a typed CDP command.
57    pub async fn execute<C>(&self, params: &C::Params) -> Result<C::Output>
58    where
59        C: Command,
60    {
61        let request = CdpRequest {
62            id: self.next_id.fetch_add(1, Ordering::Relaxed),
63            method: C::METHOD.to_owned(),
64            params: serde_json::to_value(params)?,
65            session_id: None,
66        };
67        let response = self.transport.send(request).await?;
68        C::decode(response)
69    }
70
71    /// Send a typed CDP command within a specific target session.
72    pub async fn execute_in_session<C>(
73        &self,
74        session_id: impl Into<String>,
75        params: &C::Params,
76    ) -> Result<C::Output>
77    where
78        C: Command,
79    {
80        let request = CdpRequest {
81            id: self.next_id.fetch_add(1, Ordering::Relaxed),
82            method: C::METHOD.to_owned(),
83            params: serde_json::to_value(params)?,
84            session_id: Some(session_id.into()),
85        };
86        let response = self.transport.send(request).await?;
87        C::decode(response)
88    }
89
90    /// Send a raw request and return the raw JSON payload.
91    pub async fn call_raw(
92        &self,
93        method: impl Into<String>,
94        params: impl Serialize,
95        session_id: Option<String>,
96    ) -> Result<CdpValue> {
97        let method = method.into();
98        let request = CdpRequest {
99            id: self.next_id.fetch_add(1, Ordering::Relaxed),
100            method: method.clone(),
101            params: serde_json::to_value(params)?,
102            session_id,
103        };
104        let response = self.transport.send(request).await?;
105        match (response.error, response.result) {
106            (Some(error), _) => Err(CdpError::Command {
107                method,
108                message: error.message,
109            }),
110            (None, Some(result)) => Ok(result),
111            (None, None) => Err(CdpError::InvalidResponse(
112                "missing result payload".to_owned(),
113            )),
114        }
115    }
116}
117
118fn decode_json<T>(method: &str, response: CdpResponse) -> Result<T>
119where
120    T: DeserializeOwned,
121{
122    if let Some(error) = response.error {
123        return Err(CdpError::Command {
124            method: method.to_owned(),
125            message: error.message,
126        });
127    }
128    let result = response
129        .result
130        .ok_or_else(|| CdpError::InvalidResponse("missing result payload".to_owned()))?;
131    Ok(serde_json::from_value(result)?)
132}
133
134impl Command for crate::BrowserGetVersionParams {
135    type Params = Self;
136    type Output = crate::BrowserVersion;
137    const METHOD: &'static str = "Browser.getVersion";
138
139    fn decode(response: CdpResponse) -> Result<Self::Output> {
140        decode_json(Self::METHOD, response)
141    }
142}
143
144impl Command for crate::TargetCreateTargetParams {
145    type Params = Self;
146    type Output = crate::TargetCreateTargetResult;
147    const METHOD: &'static str = "Target.createTarget";
148
149    fn decode(response: CdpResponse) -> Result<Self::Output> {
150        decode_json(Self::METHOD, response)
151    }
152}
153
154impl Command for crate::TargetAttachToTargetParams {
155    type Params = Self;
156    type Output = crate::TargetAttachToTargetResult;
157    const METHOD: &'static str = "Target.attachToTarget";
158
159    fn decode(response: CdpResponse) -> Result<Self::Output> {
160        decode_json(Self::METHOD, response)
161    }
162}
163
164impl Command for crate::TargetSetAutoAttachParams {
165    type Params = Self;
166    type Output = ();
167    const METHOD: &'static str = "Target.setAutoAttach";
168
169    fn decode(response: CdpResponse) -> Result<Self::Output> {
170        if let Some(error) = response.error {
171            return Err(CdpError::Command {
172                method: Self::METHOD.to_owned(),
173                message: error.message,
174            });
175        }
176        Ok(())
177    }
178}
179
180impl Command for crate::TargetSetDiscoverTargetsParams {
181    type Params = Self;
182    type Output = ();
183    const METHOD: &'static str = "Target.setDiscoverTargets";
184
185    fn decode(response: CdpResponse) -> Result<Self::Output> {
186        if let Some(error) = response.error {
187            return Err(CdpError::Command {
188                method: Self::METHOD.to_owned(),
189                message: error.message,
190            });
191        }
192        Ok(())
193    }
194}
195
196impl Command for crate::PageEnableParams {
197    type Params = Self;
198    type Output = ();
199    const METHOD: &'static str = "Page.enable";
200
201    fn decode(response: CdpResponse) -> Result<Self::Output> {
202        if let Some(error) = response.error {
203            return Err(CdpError::Command {
204                method: Self::METHOD.to_owned(),
205                message: error.message,
206            });
207        }
208        Ok(())
209    }
210}
211
212impl Command for crate::PageSetLifecycleEventsEnabledParams {
213    type Params = Self;
214    type Output = ();
215    const METHOD: &'static str = "Page.setLifecycleEventsEnabled";
216
217    fn decode(response: CdpResponse) -> Result<Self::Output> {
218        if let Some(error) = response.error {
219            return Err(CdpError::Command {
220                method: Self::METHOD.to_owned(),
221                message: error.message,
222            });
223        }
224        Ok(())
225    }
226}
227
228impl Command for crate::RuntimeEnableParams {
229    type Params = Self;
230    type Output = ();
231    const METHOD: &'static str = "Runtime.enable";
232
233    fn decode(response: CdpResponse) -> Result<Self::Output> {
234        if let Some(error) = response.error {
235            return Err(CdpError::Command {
236                method: Self::METHOD.to_owned(),
237                message: error.message,
238            });
239        }
240        Ok(())
241    }
242}
243
244impl Command for crate::NetworkEnableParams {
245    type Params = Self;
246    type Output = ();
247    const METHOD: &'static str = "Network.enable";
248
249    fn decode(response: CdpResponse) -> Result<Self::Output> {
250        if let Some(error) = response.error {
251            return Err(CdpError::Command {
252                method: Self::METHOD.to_owned(),
253                message: error.message,
254            });
255        }
256        Ok(())
257    }
258}
259
260impl Command for crate::PageNavigateParams {
261    type Params = Self;
262    type Output = crate::PageNavigateResult;
263    const METHOD: &'static str = "Page.navigate";
264
265    fn decode(response: CdpResponse) -> Result<Self::Output> {
266        decode_json(Self::METHOD, response)
267    }
268}
269
270impl Command for crate::PageGetFrameTreeParams {
271    type Params = Self;
272    type Output = crate::PageGetFrameTreeResult;
273    const METHOD: &'static str = "Page.getFrameTree";
274
275    fn decode(response: CdpResponse) -> Result<Self::Output> {
276        decode_json(Self::METHOD, response)
277    }
278}
279
280impl Command for crate::PageCreateIsolatedWorldParams {
281    type Params = Self;
282    type Output = crate::PageCreateIsolatedWorldResult;
283    const METHOD: &'static str = "Page.createIsolatedWorld";
284
285    fn decode(response: CdpResponse) -> Result<Self::Output> {
286        decode_json(Self::METHOD, response)
287    }
288}
289
290impl Command for crate::RuntimeEvaluateParams {
291    type Params = Self;
292    type Output = crate::RuntimeEvaluateResult;
293    const METHOD: &'static str = "Runtime.evaluate";
294
295    fn decode(response: CdpResponse) -> Result<Self::Output> {
296        decode_json(Self::METHOD, response)
297    }
298}
299
300impl Command for crate::RuntimeCallFunctionOnParams {
301    type Params = Self;
302    type Output = crate::RuntimeCallFunctionOnResult;
303    const METHOD: &'static str = "Runtime.callFunctionOn";
304
305    fn decode(response: CdpResponse) -> Result<Self::Output> {
306        decode_json(Self::METHOD, response)
307    }
308}
309
310impl Command for crate::RuntimeReleaseObjectParams {
311    type Params = Self;
312    type Output = ();
313    const METHOD: &'static str = "Runtime.releaseObject";
314
315    fn decode(response: CdpResponse) -> Result<Self::Output> {
316        if let Some(error) = response.error {
317            return Err(CdpError::Command {
318                method: Self::METHOD.to_owned(),
319                message: error.message,
320            });
321        }
322        Ok(())
323    }
324}
325
326impl Command for crate::PageCaptureScreenshotParams {
327    type Params = Self;
328    type Output = crate::PageCaptureScreenshotResult;
329    const METHOD: &'static str = "Page.captureScreenshot";
330
331    fn decode(response: CdpResponse) -> Result<Self::Output> {
332        decode_json(Self::METHOD, response)
333    }
334}
335
336impl Command for crate::InputDispatchKeyEventParams {
337    type Params = Self;
338    type Output = ();
339    const METHOD: &'static str = "Input.dispatchKeyEvent";
340
341    fn decode(response: CdpResponse) -> Result<Self::Output> {
342        if let Some(error) = response.error {
343            return Err(CdpError::Command {
344                method: Self::METHOD.to_owned(),
345                message: error.message,
346            });
347        }
348        Ok(())
349    }
350}
351
352impl Command for crate::InputInsertTextParams {
353    type Params = Self;
354    type Output = ();
355    const METHOD: &'static str = "Input.insertText";
356
357    fn decode(response: CdpResponse) -> Result<Self::Output> {
358        if let Some(error) = response.error {
359            return Err(CdpError::Command {
360                method: Self::METHOD.to_owned(),
361                message: error.message,
362            });
363        }
364        Ok(())
365    }
366}
367
368impl Command for crate::FetchEnableParams {
369    type Params = Self;
370    type Output = ();
371    const METHOD: &'static str = "Fetch.enable";
372
373    fn decode(response: CdpResponse) -> Result<Self::Output> {
374        if let Some(error) = response.error {
375            return Err(CdpError::Command {
376                method: Self::METHOD.to_owned(),
377                message: error.message,
378            });
379        }
380        Ok(())
381    }
382}
383
384impl Command for crate::FetchContinueRequestParams {
385    type Params = Self;
386    type Output = ();
387    const METHOD: &'static str = "Fetch.continueRequest";
388
389    fn decode(response: CdpResponse) -> Result<Self::Output> {
390        if let Some(error) = response.error {
391            return Err(CdpError::Command {
392                method: Self::METHOD.to_owned(),
393                message: error.message,
394            });
395        }
396        Ok(())
397    }
398}
399
400impl Command for crate::FetchFulfillRequestParams {
401    type Params = Self;
402    type Output = ();
403    const METHOD: &'static str = "Fetch.fulfillRequest";
404
405    fn decode(response: CdpResponse) -> Result<Self::Output> {
406        if let Some(error) = response.error {
407            return Err(CdpError::Command {
408                method: Self::METHOD.to_owned(),
409                message: error.message,
410            });
411        }
412        Ok(())
413    }
414}
415
416impl Command for crate::FetchGetResponseBodyParams {
417    type Params = Self;
418    type Output = crate::FetchGetResponseBodyResult;
419    const METHOD: &'static str = "Fetch.getResponseBody";
420
421    fn decode(response: CdpResponse) -> Result<Self::Output> {
422        decode_json(Self::METHOD, response)
423    }
424}
425
426impl Command for crate::FetchFailRequestParams {
427    type Params = Self;
428    type Output = ();
429    const METHOD: &'static str = "Fetch.failRequest";
430
431    fn decode(response: CdpResponse) -> Result<Self::Output> {
432        if let Some(error) = response.error {
433            return Err(CdpError::Command {
434                method: Self::METHOD.to_owned(),
435                message: error.message,
436            });
437        }
438        Ok(())
439    }
440}