frakt 0.1.0

Ergonomic platform HTTP client bindings for Rust
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
//! Cookie management using backend abstraction

use crate::{Result, backend::CookieStorage};

/// Policy for cookie acceptance.
///
/// This enum defines how cookies should be handled by the HTTP client.
/// It maps directly to NSHTTPCookieAcceptPolicy values.
///
/// # Examples
///
/// ```rust
/// use frakt::{Client, CookieAcceptPolicy};
///
/// # #[tokio::main]
/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let client = Client::builder()
///     .use_cookies(true)
///     .build()?;
///
/// if let Some(jar) = client.cookie_jar() {
///     jar.set_cookie_accept_policy(CookieAcceptPolicy::Always);
/// }
/// # Ok(())
/// # }
/// ```
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CookieAcceptPolicy {
    /// Accept all cookies.
    ///
    /// Cookies will be accepted from all domains and stored in the cookie jar.
    Always,
    /// Never accept cookies.
    ///
    /// All cookies will be rejected and not stored.
    Never,
    /// Accept cookies only from the main document domain.
    ///
    /// Cookies will only be accepted if they come from the same domain as
    /// the main document URL. This helps prevent third-party tracking cookies.
    OnlyFromMainDocumentDomain,
}

/// A cookie jar that manages HTTP cookies using backend abstraction.
///
/// `CookieJar` provides a high-level interface for managing HTTP cookies. It uses
/// the appropriate backend for the platform and provides methods for adding, removing, and querying cookies.
/// Cookies are automatically sent with requests and stored from responses when enabled.
///
/// # Examples
///
/// ```rust
/// use frakt::{Client, CookieJar, Cookie};
///
/// # #[tokio::main]
/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
/// // Create a client with cookies enabled
/// let client = Client::builder()
///     .use_cookies(true)
///     .build()?;
///
/// // Access the cookie jar
/// if let Some(jar) = client.cookie_jar() {
///     // Add a custom cookie
///     let cookie = Cookie::new("session", "abc123")
///         .domain("example.com")
///         .path("/")
///         .secure(true);
///     jar.add_cookie(cookie)?;
///
///     // Get all cookies
///     let cookies = jar.all_cookies();
///     println!("Found {} cookies", cookies.len());
/// }
/// # Ok(())
/// # }
/// ```
#[derive(Debug, Clone)]
pub struct CookieJar {
    storage: CookieStorage,
}

impl CookieJar {
    /// Create a new cookie jar with default storage.
    ///
    /// This creates a cookie jar using the appropriate backend for the platform.
    /// On Apple platforms, it uses NSHTTPCookieStorage. On other platforms, it uses reqwest cookie jar.
    ///
    /// # Examples
    ///
    /// ```rust
    /// use frakt::CookieJar;
    ///
    /// # fn main() {
    /// let jar = CookieJar::new();
    /// # }
    /// ```
    pub fn new() -> Self {
        Self {
            storage: CookieStorage::new(),
        }
    }

    /// Create a new cookie jar with storage for a specific group container.
    ///
    /// This creates a cookie jar that uses a separate cookie storage for the specified
    /// group container identifier. This is useful for app extensions or when you need
    /// isolated cookie storage. Only available on Apple platforms.
    ///
    /// # Arguments
    ///
    /// * `identifier` - The group container identifier
    ///
    /// # Examples
    ///
    /// ```rust
    /// use frakt::CookieJar;
    ///
    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
    /// # #[cfg(target_vendor = "apple")]
    /// let jar = CookieJar::for_group_container("group.com.example.app")?;
    /// # Ok(())
    /// # }
    /// ```
    #[cfg(target_vendor = "apple")]
    pub fn for_group_container(identifier: &str) -> Result<Self> {
        Ok(Self {
            storage: CookieStorage::for_group_container(identifier)?,
        })
    }

    /// Get all cookies stored in this jar.
    ///
    /// Returns a vector containing all cookies currently stored in the cookie jar.
    /// Note: On some backends (like reqwest), this may return an empty vector due to API limitations.
    ///
    /// # Examples
    ///
    /// ```rust
    /// use frakt::{Client, Cookie};
    ///
    /// # #[tokio::main]
    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
    /// let client = Client::builder().use_cookies(true).build()?;
    ///
    /// if let Some(jar) = client.cookie_jar() {
    ///     let cookies = jar.all_cookies();
    ///     for cookie in cookies {
    ///         println!("Cookie: {}={}", cookie.name, cookie.value);
    ///     }
    /// }
    /// # Ok(())
    /// # }
    /// ```
    pub fn all_cookies(&self) -> Vec<Cookie> {
        self.storage.all_cookies()
    }

    /// Get cookies for a specific URL.
    ///
    /// Returns only the cookies that would be sent with a request to the given URL,
    /// taking into account domain, path, and security restrictions.
    ///
    /// # Arguments
    ///
    /// * `url` - The URL to get cookies for
    ///
    /// # Examples
    ///
    /// ```rust
    /// use frakt::{Client, Cookie};
    ///
    /// # #[tokio::main]
    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
    /// let client = Client::builder().use_cookies(true).build()?;
    ///
    /// if let Some(jar) = client.cookie_jar() {
    ///     let cookies = jar.cookies_for_url("https://example.com/api")?;
    ///     println!("Found {} cookies for example.com", cookies.len());
    /// }
    /// # Ok(())
    /// # }
    /// ```
    pub fn cookies_for_url(&self, url: &str) -> Result<Vec<Cookie>> {
        self.storage.cookies_for_url(url)
    }

    /// Add a cookie to the jar.
    ///
    /// The cookie will be stored and automatically sent with future requests
    /// that match the cookie's domain, path, and security requirements.
    ///
    /// # Arguments
    ///
    /// * `cookie` - The cookie to add
    ///
    /// # Examples
    ///
    /// ```rust
    /// use frakt::{Client, Cookie};
    ///
    /// # #[tokio::main]
    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
    /// let client = Client::builder().use_cookies(true).build()?;
    ///
    /// if let Some(jar) = client.cookie_jar() {
    ///     let cookie = Cookie::new("session_id", "abc123")
    ///         .domain("example.com")
    ///         .path("/")
    ///         .secure(true);
    ///     jar.add_cookie(cookie)?;
    /// }
    /// # Ok(())
    /// # }
    /// ```
    pub fn add_cookie(&self, cookie: Cookie) -> Result<()> {
        self.storage.add_cookie(cookie)
    }

    /// Remove a cookie from the jar.
    ///
    /// The cookie will no longer be stored or sent with requests.
    /// Note: On some backends (like reqwest), this operation may not be supported due to API limitations.
    ///
    /// # Arguments
    ///
    /// * `cookie` - The cookie to remove
    ///
    /// # Examples
    ///
    /// ```rust
    /// use frakt::{Client, Cookie};
    ///
    /// # #[tokio::main]
    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
    /// let client = Client::builder().use_cookies(true).build()?;
    ///
    /// if let Some(jar) = client.cookie_jar() {
    ///     let cookie = Cookie::new("session_id", "abc123")
    ///         .domain("example.com");
    ///     jar.remove_cookie(cookie)?;
    /// }
    /// # Ok(())
    /// # }
    /// ```
    pub fn remove_cookie(&self, cookie: Cookie) -> Result<()> {
        self.storage.remove_cookie(cookie)
    }

    /// Remove all cookies from the jar.
    ///
    /// This clears all stored cookies. Use with caution as this will affect
    /// all HTTP clients using the same cookie storage.
    /// Note: On some backends (like reqwest), this operation may not be supported due to API limitations.
    ///
    /// # Examples
    ///
    /// ```rust
    /// use frakt::Client;
    ///
    /// # #[tokio::main]
    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
    /// let client = Client::builder().use_cookies(true).build()?;
    ///
    /// if let Some(jar) = client.cookie_jar() {
    ///     jar.clear();
    ///     println!("All cookies cleared");
    /// }
    /// # Ok(())
    /// # }
    /// ```
    pub fn clear(&self) {
        self.storage.clear()
    }

    /// Set the cookie acceptance policy.
    ///
    /// This controls how cookies are handled when received from servers.
    /// The policy affects all HTTP clients using the same cookie storage.
    /// Note: On some backends (like reqwest), this operation may not be supported due to API limitations.
    ///
    /// # Arguments
    ///
    /// * `policy` - The cookie acceptance policy to use
    ///
    /// # Examples
    ///
    /// ```rust
    /// use frakt::{Client, CookieAcceptPolicy};
    ///
    /// # #[tokio::main]
    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
    /// let client = Client::builder().use_cookies(true).build()?;
    ///
    /// if let Some(jar) = client.cookie_jar() {
    ///     // Only accept cookies from the main document domain
    ///     jar.set_cookie_accept_policy(CookieAcceptPolicy::OnlyFromMainDocumentDomain);
    /// }
    /// # Ok(())
    /// # }
    /// ```
    pub fn set_cookie_accept_policy(&self, policy: CookieAcceptPolicy) {
        self.storage.set_cookie_accept_policy(policy)
    }
}

impl Default for CookieJar {
    fn default() -> Self {
        Self::new()
    }
}

/// An HTTP cookie.
///
/// This struct represents an HTTP cookie with all its attributes including name, value,
/// domain, path, security flags, and expiration. Cookies can be created manually or
/// extracted from HTTP responses.
///
/// # Examples
///
/// ```rust
/// use frakt::Cookie;
///
/// // Create a simple session cookie
/// let session_cookie = Cookie::new("session_id", "abc123")
///     .domain("example.com")
///     .path("/")
///     .secure(true);
///
/// // Create a cookie with expiration
/// let persistent_cookie = Cookie::new("user_pref", "dark_mode")
///     .domain("myapp.com")
///     .path("/settings")
///     .expires("Wed, 09 Jun 2024 10:18:14 GMT");
/// ```
#[derive(Debug, Clone)]
pub struct Cookie {
    /// Cookie name
    pub name: String,
    /// Cookie value
    pub value: String,
    /// Domain
    pub domain: String,
    /// Path
    pub path: String,
    /// Whether the cookie is secure (HTTPS only)
    pub secure: bool,
    /// Whether the cookie is HTTP only
    pub http_only: bool,
    /// Expiration date as string (None for session cookies)
    pub expires: Option<String>,
}

impl Cookie {
    /// Create a new cookie with the given name and value.
    ///
    /// The cookie is created with default values: domain empty, path "/",
    /// not secure, not HTTP-only, and no expiration (session cookie).
    ///
    /// # Arguments
    ///
    /// * `name` - The cookie name
    /// * `value` - The cookie value
    ///
    /// # Examples
    ///
    /// ```rust
    /// use frakt::Cookie;
    ///
    /// let cookie = Cookie::new("session_id", "abc123");
    /// assert_eq!(cookie.name, "session_id");
    /// assert_eq!(cookie.value, "abc123");
    /// assert_eq!(cookie.path, "/");
    /// ```
    pub fn new(name: impl Into<String>, value: impl Into<String>) -> Self {
        Self {
            name: name.into(),
            value: value.into(),
            domain: String::new(),
            path: "/".to_string(),
            secure: false,
            http_only: false,
            expires: None,
        }
    }

    /// Set the domain for this cookie.
    ///
    /// The domain determines which hosts will receive this cookie.
    /// If not set, the cookie will only be sent to the exact host that set it.
    ///
    /// # Arguments
    ///
    /// * `domain` - The domain (e.g., "example.com" or ".example.com")
    ///
    /// # Examples
    ///
    /// ```rust
    /// use frakt::Cookie;
    ///
    /// let cookie = Cookie::new("session", "abc123")
    ///     .domain("example.com");
    /// ```
    pub fn domain(mut self, domain: impl Into<String>) -> Self {
        self.domain = domain.into();
        self
    }

    /// Set the path for this cookie.
    ///
    /// The path determines which URLs will receive this cookie.
    /// Only URLs that start with this path will get the cookie.
    ///
    /// # Arguments
    ///
    /// * `path` - The path (e.g., "/" or "/api")
    ///
    /// # Examples
    ///
    /// ```rust
    /// use frakt::Cookie;
    ///
    /// let cookie = Cookie::new("api_token", "token123")
    ///     .path("/api");
    /// ```
    pub fn path(mut self, path: impl Into<String>) -> Self {
        self.path = path.into();
        self
    }

    /// Set whether the cookie should only be sent over HTTPS.
    ///
    /// Secure cookies are only sent over encrypted connections,
    /// helping to prevent interception by attackers.
    ///
    /// # Arguments
    ///
    /// * `secure` - Whether the cookie should be secure
    ///
    /// # Examples
    ///
    /// ```rust
    /// use frakt::Cookie;
    ///
    /// let cookie = Cookie::new("auth", "sensitive_token")
    ///     .secure(true);
    /// ```
    pub fn secure(mut self, secure: bool) -> Self {
        self.secure = secure;
        self
    }

    /// Set whether the cookie should be HTTP-only.
    ///
    /// HTTP-only cookies cannot be accessed via JavaScript,
    /// helping to prevent XSS attacks.
    ///
    /// # Arguments
    ///
    /// * `http_only` - Whether the cookie should be HTTP-only
    ///
    /// # Examples
    ///
    /// ```rust
    /// use frakt::Cookie;
    ///
    /// let cookie = Cookie::new("session", "abc123")
    ///     .http_only(true);
    /// ```
    pub fn http_only(mut self, http_only: bool) -> Self {
        self.http_only = http_only;
        self
    }

    /// Set the expiration date for this cookie.
    ///
    /// The expiration date should be in RFC 1123 format. If not set,
    /// the cookie will be a session cookie that expires when the browser closes.
    ///
    /// # Arguments
    ///
    /// * `expires` - The expiration date string
    ///
    /// # Examples
    ///
    /// ```rust
    /// use frakt::Cookie;
    ///
    /// let cookie = Cookie::new("remember_me", "true")
    ///     .expires("Wed, 09 Jun 2024 10:18:14 GMT");
    /// ```
    pub fn expires(mut self, expires: impl Into<String>) -> Self {
        self.expires = Some(expires.into());
        self
    }
}