use bytes::Bytes;
use http::header::IntoHeaderName;
use http::{HeaderMap, HeaderValue, Method, Uri};
mod fingerprint;
mod normalize;
#[cfg(feature = "serde")]
mod repr;
pub use fingerprint::Fingerprint;
use crate::request::{Attempt, Body, Depth, Request, Tag};
use crate::{Error, Result};
#[derive(Debug, Clone)]
pub struct Seed {
method: Method,
uri: Uri,
headers: HeaderMap,
body: Bytes,
tag: Option<Tag>,
depth: Depth,
priority: i32,
attempt: Attempt,
}
impl Seed {
pub fn new(method: Method, uri: Uri) -> Result<Self> {
if uri.scheme().is_none() || uri.authority().is_none() {
return Err(Error::invalid(format!("seed uri `{uri}` is not absolute")));
}
Ok(Self {
method,
uri,
headers: HeaderMap::new(),
body: Bytes::new(),
tag: None,
depth: Depth::ROOT,
priority: 0,
attempt: Attempt::FIRST,
})
}
pub fn get<U>(uri: U) -> Result<Self>
where
U: TryInto<Uri>,
U::Error: Into<Error>,
{
Self::new(Method::GET, uri.try_into().map_err(Into::into)?)
}
#[must_use]
pub fn with_tag(mut self, tag: impl Into<Tag>) -> Self {
self.tag = Some(tag.into());
self
}
#[must_use]
pub const fn with_depth(mut self, depth: Depth) -> Self {
self.depth = depth;
self
}
#[must_use]
pub const fn with_priority(mut self, priority: i32) -> Self {
self.priority = priority;
self
}
#[must_use]
pub fn with_header(mut self, name: impl IntoHeaderName, value: HeaderValue) -> Self {
self.headers.insert(name, value);
self
}
#[must_use]
pub fn with_body(mut self, body: impl Into<Bytes>) -> Self {
self.body = body.into();
self
}
#[must_use]
pub(crate) fn next_attempt(mut self) -> Self {
self.attempt = self.attempt.next();
self
}
#[must_use]
pub const fn method(&self) -> &Method {
&self.method
}
#[must_use]
pub const fn uri(&self) -> &Uri {
&self.uri
}
#[must_use]
pub const fn headers(&self) -> &HeaderMap {
&self.headers
}
#[must_use]
pub const fn body(&self) -> &Bytes {
&self.body
}
#[must_use]
pub const fn tag(&self) -> Option<&Tag> {
self.tag.as_ref()
}
#[must_use]
pub const fn depth(&self) -> Depth {
self.depth
}
#[must_use]
pub const fn priority(&self) -> i32 {
self.priority
}
#[must_use]
pub const fn attempt(&self) -> Attempt {
self.attempt
}
#[must_use]
pub fn canonical_uri(&self) -> String {
normalize::canonical(&self.uri)
}
#[must_use]
pub fn fingerprint(&self) -> Fingerprint {
Fingerprint::of(&[
self.method.as_str().as_bytes(),
self.canonical_uri().as_bytes(),
&self.body,
])
}
#[must_use]
pub fn into_request(self) -> Request {
let mut request = Request::new(Body::from(self.body));
*request.method_mut() = self.method;
*request.uri_mut() = self.uri;
*request.headers_mut() = self.headers;
let extensions = request.extensions_mut();
if let Some(tag) = self.tag {
extensions.insert(tag);
}
extensions.insert(self.depth);
extensions.insert(self.attempt);
request
}
}
impl TryFrom<&str> for Seed {
type Error = Error;
fn try_from(uri: &str) -> Result<Self> {
Self::get(uri)
}
}
impl TryFrom<String> for Seed {
type Error = Error;
fn try_from(uri: String) -> Result<Self> {
Self::get(uri)
}
}
impl TryFrom<Uri> for Seed {
type Error = Error;
fn try_from(uri: Uri) -> Result<Self> {
Self::new(Method::GET, uri)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::request::RequestExt;
fn fingerprint(uri: &str) -> Fingerprint {
Seed::get(uri).unwrap().fingerprint()
}
#[test]
fn rejects_relative_uris() {
assert!(Seed::get("/relative").is_err());
assert!(Seed::get("not a uri").is_err());
assert!(Seed::get("https://example.com").is_ok());
}
#[test]
fn fingerprint_normalizes_scheme_host_and_port() {
let base = fingerprint("https://example.com/a?b=1");
assert_eq!(base, fingerprint("HTTPS://Example.COM:443/a?b=1"));
assert_ne!(base, fingerprint("https://example.com:8443/a?b=1"));
assert_ne!(base, fingerprint("https://example.com/a?b=2"));
assert_eq!(
base,
fingerprint("https://example.com/a?utm_source=x&b=1#top")
);
assert_eq!(
fingerprint("https://example.com"),
fingerprint("https://example.com/")
);
}
#[test]
fn fingerprint_ignores_crawl_metadata() {
let seed = Seed::get("https://example.com/").unwrap();
let tagged = seed.clone().with_tag("page").with_depth(Depth::new(3));
assert_eq!(seed.fingerprint(), tagged.fingerprint());
let post = Seed::new(Method::POST, seed.uri().clone()).unwrap();
assert_ne!(seed.fingerprint(), post.fingerprint());
assert_ne!(
post.fingerprint(),
post.clone().with_body("x").fingerprint()
);
}
#[test]
fn into_request_carries_metadata() {
let seed = Seed::get("https://example.com/")
.unwrap()
.with_tag("page")
.with_depth(Depth::new(2))
.next_attempt();
let request = seed.into_request();
assert_eq!(request.uri(), "https://example.com/");
assert_eq!(request.tag(), Some(&Tag::from("page")));
assert_eq!(request.depth(), Depth::new(2));
assert_eq!(request.attempt(), Attempt::new(1));
}
}