1use serde::{Deserialize, Serialize};
2
3#[cfg(feature = "mcp-tool")]
4use schemars::JsonSchema;
5
6use crate::{API_VERSION, FirecrawlApp, FirecrawlError, error::FirecrawlAPIError};
7
8#[serde_with::skip_serializing_none]
9#[derive(Deserialize, Serialize, Debug, Default, Clone, PartialEq, Eq)]
10#[cfg_attr(feature = "mcp-tool", derive(JsonSchema))]
11#[serde(rename_all = "camelCase")]
12pub struct MapOptions {
13 pub search: Option<String>,
15
16 pub ignore_sitemap: Option<bool>,
18
19 pub sitemap_only: Option<bool>,
21
22 pub include_subdomains: Option<bool>,
24
25 pub limit: Option<u32>,
27
28 #[cfg_attr(feature = "mcp-tool", schemars(skip))]
30 pub timeout: Option<u32>,
31}
32
33#[derive(Deserialize, Serialize, Debug, Default, Clone, PartialEq, Eq)]
34#[serde(rename_all = "camelCase")]
35pub struct MapRequestBody {
36 pub url: String,
37
38 #[serde(flatten)]
39 pub options: MapOptions,
40}
41
42#[derive(Deserialize, Serialize, Debug, Default, Clone, PartialEq, Eq)]
43#[serde(rename_all = "camelCase")]
44struct MapResponse {
45 success: Option<bool>,
46 links: Option<Vec<String>>,
47 error: Option<String>,
48}
49
50#[derive(Deserialize, Serialize, Debug, Default, Clone, PartialEq, Eq)]
51#[cfg_attr(feature = "mcp-tool", derive(JsonSchema))]
52#[serde(rename_all = "camelCase")]
53pub struct MapUrlInput {
54 pub url: String,
55
56 #[serde(flatten)]
57 pub options: MapOptions,
58}
59
60impl FirecrawlApp {
61 pub async fn map_url(
63 &self,
64 url: impl AsRef<str>,
65 options: impl Into<Option<MapOptions>>,
66 ) -> Result<Vec<String>, FirecrawlError> {
67 let body = MapRequestBody {
68 url: url.as_ref().to_string(),
69 options: options.into().unwrap_or_default(),
70 };
71
72 let headers = self.prepare_headers(None);
73
74 let response = self
75 .client
76 .post(format!("{}/{}/map", self.api_url, API_VERSION))
77 .headers(headers)
78 .json(&body)
79 .send()
80 .await
81 .map_err(|e| FirecrawlError::HttpError(format!("Mapping {:?}", url.as_ref()), e))?;
82
83 let response = self
84 .handle_response::<MapResponse>(response, "map URL")
85 .await?;
86
87 if matches!(response.success, Some(false)) {
88 return Err(FirecrawlError::APIError(
89 "map request failed".to_string(),
90 FirecrawlAPIError {
91 error: response.error.unwrap_or_default(),
92 details: None,
93 },
94 ));
95 }
96
97 Ok(response.links.unwrap_or_default())
98 }
99}
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104 use serde_json::json;
105
106 #[test]
107 fn test_map_options_deserialization() {
108 let json_data = json!({
110 "search": "keyword",
111 "ignoreSitemap": true,
112 "sitemapOnly": false,
113 "includeSubdomains": true,
114 "limit": 100,
115 "timeout": 5000,
116 });
117
118 let options: MapOptions =
120 serde_json::from_value(json_data).expect("Failed to deserialize MapOptions");
121
122 let expected_options = MapOptions {
124 search: Some("keyword".to_string()),
125 ignore_sitemap: Some(true),
126 sitemap_only: Some(false),
127 include_subdomains: Some(true),
128 limit: Some(100),
129 timeout: Some(5000),
130 };
131
132 assert_eq!(options, expected_options);
134 }
135
136 #[test]
137 fn test_map_request_deserialization() {
138 let json_data = json!({
140 "url": "https://example.com",
141 "search": "keyword",
142 "ignoreSitemap": true,
143 "sitemapOnly": false,
144 "includeSubdomains": true,
145 "limit": 100,
146 "timeout": 5000,
147 });
148
149 let request_body: MapRequestBody =
151 serde_json::from_value(json_data).expect("Failed to deserialize MapRequestBody");
152
153 let expected_request_body = MapRequestBody {
155 url: "https://example.com".to_string(),
156 options: MapOptions {
157 search: Some("keyword".to_string()),
158 ignore_sitemap: Some(true),
159 sitemap_only: Some(false),
160 include_subdomains: Some(true),
161 limit: Some(100),
162 timeout: Some(5000),
163 },
164 };
165
166 assert_eq!(request_body, expected_request_body);
168 }
169
170 #[test]
171 fn test_map_response_deserialization() {
172 let json_data = json!({
174 "success": true,
175 "links": [
176 "https://example.com/page1",
177 "https://example.com/page2",
178 "https://example.com/page3"
179 ]
180 });
181
182 let response: MapResponse =
184 serde_json::from_value(json_data).expect("Failed to deserialize MapResponse");
185
186 let expected_response = MapResponse {
188 success: Some(true),
189 links: Some(vec![
190 "https://example.com/page1".to_string(),
191 "https://example.com/page2".to_string(),
192 "https://example.com/page3".to_string(),
193 ]),
194 error: None,
195 };
196
197 assert_eq!(response, expected_response);
199 }
200}