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