firecrawl_sdk/
map.rs

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    /// Optional search term to filter URLs
14    pub search: Option<String>,
15
16    /// Skip sitemap.xml discovery and only use HTML links
17    pub ignore_sitemap: Option<bool>,
18
19    /// Only use sitemap.xml for discovery, ignore HTML links
20    pub sitemap_only: Option<bool>,
21
22    /// Include URLs from subdomains in results
23    pub include_subdomains: Option<bool>,
24
25    /// Maximum number of URLs to return
26    pub limit: Option<u32>,
27
28    /// Timeout in milliseconds. There is no timeout by default.
29    #[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    /// Returns links from a URL using the Firecrawl API.
62    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        // Create test JSON data
109        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        // Deserialize the JSON to our struct
119        let options: MapOptions =
120            serde_json::from_value(json_data).expect("Failed to deserialize MapOptions");
121
122        // Create expected struct directly
123        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        // Compare the entire structs
133        assert_eq!(options, expected_options);
134    }
135
136    #[test]
137    fn test_map_request_deserialization() {
138        // Create test JSON data
139        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        // Deserialize the JSON to our struct
150        let request_body: MapRequestBody =
151            serde_json::from_value(json_data).expect("Failed to deserialize MapRequestBody");
152
153        // Create expected struct directly
154        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        // Compare the entire structs
167        assert_eq!(request_body, expected_request_body);
168    }
169
170    #[test]
171    fn test_map_response_deserialization() {
172        // Create test JSON data
173        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        // Deserialize the JSON to our struct
183        let response: MapResponse =
184            serde_json::from_value(json_data).expect("Failed to deserialize MapResponse");
185
186        // Create expected struct directly
187        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        // Compare the entire structs
198        assert_eq!(response, expected_response);
199    }
200}