playwright_core/
object_factory.rs

1// Copyright 2024 Paul Adamson
2// Licensed under the Apache License, Version 2.0
3//
4// Object Factory - Creates protocol objects from type names
5//
6// Architecture Reference:
7// - Python: playwright-python/playwright/_impl/_connection.py (_create_remote_object)
8// - Java: playwright-java/.../impl/Connection.java (createRemoteObject)
9// - JavaScript: playwright/.../client/connection.ts (_createRemoteObject)
10//
11// The object factory maps protocol type names (strings) to Rust constructors.
12// When the server sends a `__create__` message, the factory instantiates
13// the appropriate Rust object based on the type name.
14
15use crate::channel_owner::{ChannelOwner, ParentOrConnection};
16use crate::error::{Error, Result};
17use crate::protocol::{
18    artifact::Artifact, Browser, BrowserContext, BrowserType, Dialog, Frame, Page, Playwright,
19    Request, ResponseObject, Route,
20};
21use serde_json::Value;
22use std::sync::Arc;
23
24/// Creates a protocol object from a `__create__` message.
25///
26/// This function is the central object factory for the Playwright protocol.
27/// It maps type names from the server to Rust struct constructors.
28///
29/// # Arguments
30///
31/// * `parent` - Either a parent ChannelOwner or the root Connection
32/// * `type_name` - Protocol type name (e.g., "Playwright", "BrowserType")
33/// * `guid` - Unique GUID assigned by the server
34/// * `initializer` - JSON object with initial state
35///
36/// # Returns
37///
38/// An `Arc<dyn ChannelOwner>` pointing to the newly created object.
39///
40/// # Errors
41///
42/// Returns `Error::ProtocolError` if the type name is unknown or if
43/// object construction fails.
44///
45/// # Example
46///
47/// ```ignore
48/// # use playwright_core::object_factory::create_object;
49/// # use playwright_core::channel_owner::ParentOrConnection;
50/// # use playwright_core::connection::ConnectionLike;
51/// # use std::sync::Arc;
52/// # use serde_json::json;
53/// # async fn example(connection: Arc<dyn ConnectionLike>) -> Result<(), Box<dyn std::error::Error>> {
54/// let playwright_obj = create_object(
55///     ParentOrConnection::Connection(connection),
56///     "Playwright".to_string(),
57///     Arc::from("playwright@1"),
58///     json!({
59///         "chromium": { "guid": "browserType@chromium" },
60///         "firefox": { "guid": "browserType@firefox" },
61///         "webkit": { "guid": "browserType@webkit" }
62///     })
63/// ).await?;
64/// # Ok(())
65/// # }
66/// ```
67pub async fn create_object(
68    parent: ParentOrConnection,
69    type_name: String,
70    guid: Arc<str>,
71    initializer: Value,
72) -> Result<Arc<dyn ChannelOwner>> {
73    // Match on type name and call appropriate constructor
74    let object: Arc<dyn ChannelOwner> = match type_name.as_str() {
75        "Playwright" => {
76            // Playwright is the root object, so parent must be Connection
77            let connection = match parent {
78                ParentOrConnection::Connection(conn) => conn,
79                ParentOrConnection::Parent(_) => {
80                    return Err(Error::ProtocolError(
81                        "Playwright must have Connection as parent".to_string(),
82                    ))
83                }
84            };
85
86            Arc::new(Playwright::new(connection, type_name, guid, initializer).await?)
87        }
88
89        "BrowserType" => {
90            // BrowserType has Playwright as parent
91            let parent_owner = match parent {
92                ParentOrConnection::Parent(p) => p,
93                ParentOrConnection::Connection(_) => {
94                    return Err(Error::ProtocolError(
95                        "BrowserType must have Playwright as parent".to_string(),
96                    ))
97                }
98            };
99
100            Arc::new(BrowserType::new(
101                parent_owner,
102                type_name,
103                guid,
104                initializer,
105            )?)
106        }
107
108        "Browser" => {
109            // Browser has BrowserType as parent
110            let parent_owner = match parent {
111                ParentOrConnection::Parent(p) => p,
112                ParentOrConnection::Connection(_) => {
113                    return Err(Error::ProtocolError(
114                        "Browser must have BrowserType as parent".to_string(),
115                    ))
116                }
117            };
118
119            Arc::new(Browser::new(parent_owner, type_name, guid, initializer)?)
120        }
121
122        "BrowserContext" => {
123            // BrowserContext has Browser as parent
124            let parent_owner = match parent {
125                ParentOrConnection::Parent(p) => p,
126                ParentOrConnection::Connection(_) => {
127                    return Err(Error::ProtocolError(
128                        "BrowserContext must have Browser as parent".to_string(),
129                    ))
130                }
131            };
132
133            Arc::new(BrowserContext::new(
134                parent_owner,
135                type_name,
136                guid,
137                initializer,
138            )?)
139        }
140
141        "Page" => {
142            // Page has BrowserContext as parent
143            let parent_owner = match parent {
144                ParentOrConnection::Parent(p) => p,
145                ParentOrConnection::Connection(_) => {
146                    return Err(Error::ProtocolError(
147                        "Page must have BrowserContext as parent".to_string(),
148                    ))
149                }
150            };
151
152            Arc::new(Page::new(parent_owner, type_name, guid, initializer)?)
153        }
154
155        "Frame" => {
156            // Frame has Page as parent
157            let parent_owner = match parent {
158                ParentOrConnection::Parent(p) => p,
159                ParentOrConnection::Connection(_) => {
160                    return Err(Error::ProtocolError(
161                        "Frame must have Page as parent".to_string(),
162                    ))
163                }
164            };
165
166            Arc::new(Frame::new(parent_owner, type_name, guid, initializer)?)
167        }
168
169        "Request" => {
170            // Request has Frame as parent
171            let parent_owner = match parent {
172                ParentOrConnection::Parent(p) => p,
173                ParentOrConnection::Connection(_) => {
174                    return Err(Error::ProtocolError(
175                        "Request must have Frame as parent".to_string(),
176                    ))
177                }
178            };
179
180            Arc::new(Request::new(parent_owner, type_name, guid, initializer)?)
181        }
182
183        "Route" => {
184            // Route has Frame as parent (created during network interception)
185            let parent_owner = match parent {
186                ParentOrConnection::Parent(p) => p,
187                ParentOrConnection::Connection(_) => {
188                    return Err(Error::ProtocolError(
189                        "Route must have Frame as parent".to_string(),
190                    ))
191                }
192            };
193
194            Arc::new(Route::new(parent_owner, type_name, guid, initializer)?)
195        }
196
197        "Response" => {
198            // Response has Request as parent (not Frame!)
199            let parent_owner = match parent {
200                ParentOrConnection::Parent(p) => p,
201                ParentOrConnection::Connection(_) => {
202                    return Err(Error::ProtocolError(
203                        "Response must have Request as parent".to_string(),
204                    ))
205                }
206            };
207
208            Arc::new(ResponseObject::new(
209                parent_owner,
210                type_name,
211                guid,
212                initializer,
213            )?)
214        }
215
216        "ElementHandle" => {
217            // ElementHandle has Frame as parent
218            let parent_owner = match parent {
219                ParentOrConnection::Parent(p) => p,
220                ParentOrConnection::Connection(_) => {
221                    return Err(Error::ProtocolError(
222                        "ElementHandle must have Frame as parent".to_string(),
223                    ))
224                }
225            };
226
227            Arc::new(crate::protocol::ElementHandle::new(
228                parent_owner,
229                type_name,
230                guid,
231                initializer,
232            )?)
233        }
234
235        "Artifact" => {
236            // Artifact has BrowserContext as parent
237            let parent_owner = match parent {
238                ParentOrConnection::Parent(p) => p,
239                ParentOrConnection::Connection(_) => {
240                    return Err(Error::ProtocolError(
241                        "Artifact must have BrowserContext as parent".to_string(),
242                    ))
243                }
244            };
245
246            Arc::new(Artifact::new(parent_owner, type_name, guid, initializer)?)
247        }
248
249        "Dialog" => {
250            // Dialog has Page as parent
251            let parent_owner = match parent {
252                ParentOrConnection::Parent(p) => p,
253                ParentOrConnection::Connection(_) => {
254                    return Err(Error::ProtocolError(
255                        "Dialog must have Page as parent".to_string(),
256                    ))
257                }
258            };
259
260            Arc::new(Dialog::new(parent_owner, type_name, guid, initializer)?)
261        }
262
263        _ => {
264            // Unknown type - log warning and return error
265            tracing::warn!("Unknown protocol type: {}", type_name);
266            return Err(Error::ProtocolError(format!(
267                "Unknown protocol type: {}",
268                type_name
269            )));
270        }
271    };
272
273    Ok(object)
274}
275
276// Note: Object factory testing is done via integration tests since it requires:
277// - Real Connection with object registry
278// - Protocol messages from the server
279// See: crates/playwright-core/tests/connection_integration.rs