ap: refactor CachedLoader
This commit is contained in:
parent
78fbbdfedd
commit
bda3a9027d
8 changed files with 191 additions and 248 deletions
|
@ -1,111 +1,35 @@
|
||||||
// This file is an adaptation of
|
// This file was originally based on
|
||||||
// <https://github.com/timothee-haudebourg/json-ld/blob/0.12.1/core/src/loader/reqwest/content_type.rs>.
|
// <https://github.com/timothee-haudebourg/json-ld/blob/0.12.1/core/src/loader/reqwest/content_type.rs>.
|
||||||
//
|
//
|
||||||
// Copyright (C) 2022 Timothée Haudebourg et al.
|
// Copyright (C) 2022 Timothée Haudebourg et al.
|
||||||
// Licensed under either the Apache License, Version 2.0; or the MIT License.
|
// Licensed under either the Apache License, Version 2.0; or the MIT License.
|
||||||
// See <https://github.com/timothee-haudebourg/json-ld/tree/0.12.1> for details.
|
// See <https://github.com/timothee-haudebourg/json-ld/tree/0.12.1> for details.
|
||||||
//
|
//
|
||||||
// Modified by anna <owo@fef.moe> to accept the `application/activity+json`
|
// Heavily modified by anna <owo@fef.moe> to use nyanoblog's own Structured Header
|
||||||
// MIME type as proper JSON-LD.
|
// parser and accept the `application/activity+json` MIME type as proper JSON-LD.
|
||||||
|
|
||||||
use hashbrown::HashMap;
|
|
||||||
use mime::Mime;
|
use mime::Mime;
|
||||||
use reqwest::header::HeaderValue;
|
use reqwest::header::HeaderValue;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use crate::util::header::{Item, ParseHeader};
|
||||||
|
|
||||||
/// Helper structure for parsing the `Content-Type` header for JSON-LD.
|
/// Helper structure for parsing the `Content-Type` header for JSON-LD.
|
||||||
pub struct ContentType {
|
pub struct ContentType {
|
||||||
mime: Mime,
|
mime: Mime,
|
||||||
params: HashMap<Vec<u8>, Vec<u8>>,
|
profile: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ContentType {
|
impl ContentType {
|
||||||
pub fn from_header(value: &HeaderValue) -> Option<ContentType> {
|
pub fn from_header(value: &HeaderValue) -> Option<ContentType> {
|
||||||
enum State {
|
let item = Item::parse_from_header(value, true).ok()?;
|
||||||
Mime,
|
let mime = Mime::from_str(item.as_token()?).ok()?;
|
||||||
NextParam,
|
let profile = item.param("profile").and_then(|profile| {
|
||||||
BeginKey,
|
profile
|
||||||
Key,
|
.as_string()
|
||||||
BeginValue,
|
.or_else(|| profile.as_token().map(String::from))
|
||||||
QuotedValue,
|
});
|
||||||
Value,
|
Some(ContentType { mime, profile })
|
||||||
}
|
|
||||||
|
|
||||||
let mut state = State::Mime;
|
|
||||||
let mut mime = Vec::new();
|
|
||||||
let mut current_key = Vec::new();
|
|
||||||
let mut current_value = Vec::new();
|
|
||||||
let mut params = HashMap::new();
|
|
||||||
|
|
||||||
let mut bytes = value.as_bytes().iter();
|
|
||||||
|
|
||||||
loop {
|
|
||||||
match state {
|
|
||||||
State::Mime => match bytes.next().copied() {
|
|
||||||
Some(b';') => state = State::BeginKey,
|
|
||||||
Some(b) => mime.push(b),
|
|
||||||
None => break,
|
|
||||||
},
|
|
||||||
State::NextParam => match bytes.next().copied() {
|
|
||||||
Some(b';') => state = State::BeginKey,
|
|
||||||
Some(_) => return None,
|
|
||||||
None => break,
|
|
||||||
},
|
|
||||||
State::BeginKey => match bytes.next().copied() {
|
|
||||||
Some(b' ') => {}
|
|
||||||
Some(b) => {
|
|
||||||
current_key.push(b);
|
|
||||||
state = State::Key;
|
|
||||||
}
|
|
||||||
None => return None,
|
|
||||||
},
|
|
||||||
State::Key => match bytes.next().copied() {
|
|
||||||
Some(b'=') => state = State::BeginValue,
|
|
||||||
Some(b) => current_key.push(b),
|
|
||||||
None => return None,
|
|
||||||
},
|
|
||||||
State::BeginValue => match bytes.next().copied() {
|
|
||||||
Some(b'"') => state = State::QuotedValue,
|
|
||||||
Some(b) => {
|
|
||||||
state = State::Value;
|
|
||||||
current_value.push(b);
|
|
||||||
}
|
|
||||||
_ => return None,
|
|
||||||
},
|
|
||||||
State::QuotedValue => match bytes.next().copied() {
|
|
||||||
Some(b'"') => {
|
|
||||||
params.insert(
|
|
||||||
std::mem::take(&mut current_key),
|
|
||||||
std::mem::take(&mut current_value),
|
|
||||||
);
|
|
||||||
state = State::NextParam;
|
|
||||||
}
|
|
||||||
Some(b) => current_value.push(b),
|
|
||||||
None => return None,
|
|
||||||
},
|
|
||||||
State::Value => match bytes.next().copied() {
|
|
||||||
Some(b';') => {
|
|
||||||
params.insert(
|
|
||||||
std::mem::take(&mut current_key),
|
|
||||||
std::mem::take(&mut current_value),
|
|
||||||
);
|
|
||||||
state = State::BeginKey;
|
|
||||||
}
|
|
||||||
Some(b) => current_value.push(b),
|
|
||||||
None => {
|
|
||||||
params.insert(
|
|
||||||
std::mem::take(&mut current_key),
|
|
||||||
std::mem::take(&mut current_value),
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Mime::from_str(std::str::from_utf8(&mime).ok()?)
|
|
||||||
.map(|mime| ContentType { mime, params })
|
|
||||||
.ok()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_json_ld(&self) -> bool {
|
pub fn is_json_ld(&self) -> bool {
|
||||||
|
@ -114,14 +38,11 @@ impl ContentType {
|
||||||
"application/ld+json",
|
"application/ld+json",
|
||||||
"application/json",
|
"application/json",
|
||||||
]
|
]
|
||||||
.iter()
|
.contains(&self.mime.as_ref())
|
||||||
.any(|mime| *mime == self.mime)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_proper_json_ld(&self) -> bool {
|
pub fn is_proper_json_ld(&self) -> bool {
|
||||||
["application/activity+json", "application/ld+json"]
|
["application/activity+json", "application/ld+json"].contains(&self.mime.as_ref())
|
||||||
.iter()
|
|
||||||
.any(|mime| *mime == self.mime)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn mime(&self) -> &Mime {
|
pub fn mime(&self) -> &Mime {
|
||||||
|
@ -132,8 +53,8 @@ impl ContentType {
|
||||||
self.mime
|
self.mime
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn profile(&self) -> Option<&[u8]> {
|
pub fn profile(&self) -> Option<&str> {
|
||||||
self.params.get(b"profile".as_slice()).map(Vec::as_slice)
|
self.profile.as_deref()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -153,7 +74,7 @@ mod tests {
|
||||||
assert_eq!(*content_type.mime(), "application/ld+json");
|
assert_eq!(*content_type.mime(), "application/ld+json");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
content_type.profile(),
|
content_type.profile(),
|
||||||
Some(b"http://www.w3.org/ns/json-ld#expanded".as_slice())
|
Some("http://www.w3.org/ns/json-ld#expanded")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -169,7 +90,7 @@ mod tests {
|
||||||
assert_eq!(*content_type.mime(), "application/ld+json");
|
assert_eq!(*content_type.mime(), "application/ld+json");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
content_type.profile(),
|
content_type.profile(),
|
||||||
Some(b"http://www.w3.org/ns/json-ld#expanded".as_slice())
|
Some("http://www.w3.org/ns/json-ld#expanded")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -185,7 +106,7 @@ mod tests {
|
||||||
assert_eq!(*content_type.mime(), "application/ld+json");
|
assert_eq!(*content_type.mime(), "application/ld+json");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
content_type.profile(),
|
content_type.profile(),
|
||||||
Some(b"http://www.w3.org/ns/json-ld#expanded".as_slice())
|
Some("http://www.w3.org/ns/json-ld#expanded")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -201,7 +122,7 @@ mod tests {
|
||||||
assert_eq!(*content_type.mime(), "application/ld+json");
|
assert_eq!(*content_type.mime(), "application/ld+json");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
content_type.profile(),
|
content_type.profile(),
|
||||||
Some(b"http://www.w3.org/ns/json-ld#expanded".as_slice())
|
Some("http://www.w3.org/ns/json-ld#expanded")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -217,7 +138,7 @@ mod tests {
|
||||||
assert_eq!(*content_type.mime(), "application/ld+json");
|
assert_eq!(*content_type.mime(), "application/ld+json");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
content_type.profile(),
|
content_type.profile(),
|
||||||
Some(b"http://www.w3.org/ns/json-ld#expanded".as_slice())
|
Some("http://www.w3.org/ns/json-ld#expanded")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -233,7 +154,7 @@ mod tests {
|
||||||
assert_eq!(*content_type.mime(), "application/ld+json");
|
assert_eq!(*content_type.mime(), "application/ld+json");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
content_type.profile(),
|
content_type.profile(),
|
||||||
Some(b"http://www.w3.org/ns/json-ld#expanded".as_slice())
|
Some("http://www.w3.org/ns/json-ld#expanded")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -243,10 +164,7 @@ mod tests {
|
||||||
assert_eq!(*content_type.mime(), "application/ld+json");
|
assert_eq!(*content_type.mime(), "application/ld+json");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
content_type.profile(),
|
content_type.profile(),
|
||||||
Some(
|
Some("http://www.w3.org/ns/json-ld#flattened http://www.w3.org/ns/json-ld#compacted")
|
||||||
b"http://www.w3.org/ns/json-ld#flattened http://www.w3.org/ns/json-ld#compacted"
|
|
||||||
.as_slice()
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,110 +1,49 @@
|
||||||
// This file is an adaptation of
|
// This file was originally based on
|
||||||
// <https://github.com/timothee-haudebourg/json-ld/blob/0.12.1/core/src/loader/reqwest/link.rs>.
|
// <https://github.com/timothee-haudebourg/json-ld/blob/0.12.1/core/src/loader/reqwest/link.rs>.
|
||||||
//
|
//
|
||||||
// Copyright (C) 2022 Timothée Haudebourg et al.
|
// Copyright (C) 2022 Timothée Haudebourg et al.
|
||||||
// Licensed under either the Apache License, Version 2.0; or the MIT License.
|
// Licensed under either the Apache License, Version 2.0; or the MIT License.
|
||||||
// See <https://github.com/timothee-haudebourg/json-ld/tree/0.12.1> for details.
|
// See <https://github.com/timothee-haudebourg/json-ld/tree/0.12.1> for details.
|
||||||
|
//
|
||||||
|
// Heavily modified by anna <owo@fef.moe> to use nyanoblog's own Structured Header
|
||||||
|
// parser and accept the `application/activity+json` MIME type as proper JSON-LD.
|
||||||
|
|
||||||
use iref::{IriRef, IriRefBuf};
|
use iref::{IriRef, IriRefBuf};
|
||||||
use reqwest::header::HeaderValue;
|
use reqwest::header::HeaderValue;
|
||||||
use std::collections::HashMap;
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use crate::util::header::{Item, ParseHeader};
|
||||||
|
|
||||||
pub struct Link {
|
pub struct Link {
|
||||||
href: IriRefBuf,
|
href: IriRefBuf,
|
||||||
params: HashMap<Vec<u8>, Vec<u8>>,
|
rel: Option<String>,
|
||||||
|
typ: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Link {
|
impl Link {
|
||||||
pub fn from_header(value: &HeaderValue) -> Option<Self> {
|
pub fn from_header(value: &HeaderValue) -> Option<Self> {
|
||||||
enum State {
|
let item = Item::parse_from_header(value, false).unwrap();
|
||||||
BeginHref,
|
let href = IriRefBuf::from_str(item.as_url()?).ok()?;
|
||||||
Href,
|
let rel = item.param("rel").and_then(|rel| rel.as_string());
|
||||||
NextParam,
|
let typ = item.param("type").and_then(|typ| typ.as_string());
|
||||||
BeginKey,
|
Some(Link { href, rel, typ })
|
||||||
Key,
|
|
||||||
BeginValue,
|
|
||||||
Value,
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut state = State::BeginHref;
|
|
||||||
let mut href = Vec::new();
|
|
||||||
let mut current_key = Vec::new();
|
|
||||||
let mut current_value = Vec::new();
|
|
||||||
let mut params = HashMap::new();
|
|
||||||
|
|
||||||
let mut bytes = value.as_bytes().iter();
|
|
||||||
|
|
||||||
loop {
|
|
||||||
match state {
|
|
||||||
State::BeginHref => match bytes.next().copied() {
|
|
||||||
Some(b'<') => state = State::Href,
|
|
||||||
_ => break None,
|
|
||||||
},
|
|
||||||
State::Href => match bytes.next().copied() {
|
|
||||||
Some(b'>') => state = State::NextParam,
|
|
||||||
Some(b) => {
|
|
||||||
href.push(b);
|
|
||||||
}
|
|
||||||
None => break None,
|
|
||||||
},
|
|
||||||
State::NextParam => match bytes.next().copied() {
|
|
||||||
Some(b';') => state = State::BeginKey,
|
|
||||||
Some(_) => break None,
|
|
||||||
None => {
|
|
||||||
break match IriRefBuf::from_vec(href) {
|
|
||||||
Ok(href) => Some(Self { href, params }),
|
|
||||||
Err(_) => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
State::BeginKey => match bytes.next().copied() {
|
|
||||||
Some(b' ') => {}
|
|
||||||
Some(b) => {
|
|
||||||
current_key.push(b);
|
|
||||||
state = State::Key
|
|
||||||
}
|
|
||||||
None => break None,
|
|
||||||
},
|
|
||||||
State::Key => match bytes.next().copied() {
|
|
||||||
Some(b'=') => state = State::BeginValue,
|
|
||||||
Some(b) => current_key.push(b),
|
|
||||||
None => break None,
|
|
||||||
},
|
|
||||||
State::BeginValue => match bytes.next().copied() {
|
|
||||||
Some(b'"') => state = State::Value,
|
|
||||||
_ => break None,
|
|
||||||
},
|
|
||||||
State::Value => match bytes.next().copied() {
|
|
||||||
Some(b'"') => {
|
|
||||||
params.insert(
|
|
||||||
std::mem::take(&mut current_key),
|
|
||||||
std::mem::take(&mut current_value),
|
|
||||||
);
|
|
||||||
|
|
||||||
state = State::NextParam
|
|
||||||
}
|
|
||||||
Some(b) => current_value.push(b),
|
|
||||||
None => break None,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn href(&self) -> IriRef {
|
pub fn href(&self) -> IriRef {
|
||||||
self.href.as_iri_ref()
|
self.href.as_iri_ref()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn rel(&self) -> Option<&[u8]> {
|
pub fn rel(&self) -> Option<&str> {
|
||||||
self.params.get(b"rel".as_slice()).map(Vec::as_slice)
|
self.rel.as_deref()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn typ(&self) -> Option<&[u8]> {
|
pub fn typ(&self) -> Option<&str> {
|
||||||
self.params.get(b"type".as_slice()).map(Vec::as_slice)
|
self.typ.as_deref()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_proper_json_ld(&self) -> bool {
|
pub fn is_proper_json_ld(&self) -> bool {
|
||||||
self.typ()
|
self.typ()
|
||||||
.map(|typ| typ == b"application/activity+json" || typ == b"application/ld+json")
|
.map(|typ| ["application/activity+json", "application/ld+json"].contains(&typ))
|
||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -123,16 +62,16 @@ mod tests {
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(link.href(), "http://www.example.org/context");
|
assert_eq!(link.href(), "http://www.example.org/context");
|
||||||
assert_eq!(link.rel(), Some(b"context".as_slice()));
|
assert_eq!(link.rel(), Some("context"));
|
||||||
assert_eq!(link.typ(), Some(b"application/ld+json".as_slice()))
|
assert_eq!(link.typ(), Some("application/ld+json"))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_link_2() {
|
fn parse_link_2() {
|
||||||
let link = Link::from_header(&HeaderValue::from_str("<http://www.example.org/context>; rel=\"context\"; type=\"application/ld+json\"; foo=\"bar\"").unwrap()).unwrap();
|
let link = Link::from_header(&HeaderValue::from_str("<http://www.example.org/context>; rel=\"context\"; type=\"application/ld+json\"; foo=\"bar\"").unwrap()).unwrap();
|
||||||
assert_eq!(link.href(), "http://www.example.org/context");
|
assert_eq!(link.href(), "http://www.example.org/context");
|
||||||
assert_eq!(link.rel(), Some(b"context".as_slice()));
|
assert_eq!(link.rel(), Some("context"));
|
||||||
assert_eq!(link.typ(), Some(b"application/ld+json".as_slice()))
|
assert_eq!(link.typ(), Some("application/ld+json"))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
@ -13,18 +13,19 @@
|
||||||
use crate::state::AppState;
|
use crate::state::AppState;
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use futures::future::{BoxFuture, FutureExt};
|
use futures::future::{BoxFuture, FutureExt};
|
||||||
use hashbrown::{HashMap, HashSet};
|
use hashbrown::HashSet;
|
||||||
use iref::{Iri, IriBuf};
|
use iref::{Iri, IriBuf};
|
||||||
use json_ld::{syntax::Parse, Loader, LoadingResult, Profile, RemoteDocument, Value};
|
use json_ld::{
|
||||||
|
syntax::{Parse, Value},
|
||||||
|
Loader, Profile, RemoteDocument,
|
||||||
|
};
|
||||||
use locspan::{Meta, Span};
|
use locspan::{Meta, Span};
|
||||||
use mime::Mime;
|
|
||||||
use rdf_types::vocabulary::Index;
|
|
||||||
use rdf_types::{IriVocabulary, IriVocabularyMut};
|
use rdf_types::{IriVocabulary, IriVocabularyMut};
|
||||||
use reqwest::header::{HeaderValue, ACCEPT, CONTENT_TYPE, LINK, LOCATION};
|
use reqwest::{
|
||||||
use reqwest::StatusCode;
|
header::{ACCEPT, CONTENT_TYPE, LINK, LOCATION},
|
||||||
|
StatusCode,
|
||||||
|
};
|
||||||
use std::hash::Hash;
|
use std::hash::Hash;
|
||||||
use std::str::FromStr;
|
|
||||||
use tokio::sync::OnceCell;
|
|
||||||
|
|
||||||
mod content_type;
|
mod content_type;
|
||||||
use content_type::*;
|
use content_type::*;
|
||||||
|
@ -34,8 +35,9 @@ use link::*;
|
||||||
use crate::core::*;
|
use crate::core::*;
|
||||||
use crate::headers;
|
use crate::headers;
|
||||||
use crate::util::http;
|
use crate::util::http;
|
||||||
|
use crate::util::http::Response;
|
||||||
|
|
||||||
pub struct CachedLoader<I = IriBuf, M = Span, T = json_ld::syntax::Value<M>> {
|
pub struct CachedLoader<I = IriBuf, M = Span, T = Value<M>> {
|
||||||
state: AppState,
|
state: AppState,
|
||||||
parser: Box<DynParser<I, M, T>>,
|
parser: Box<DynParser<I, M, T>>,
|
||||||
}
|
}
|
||||||
|
@ -94,7 +96,7 @@ impl<I: Clone + Eq + Hash + Send + Sync, M: Send, T: Clone + Send> CachedLoader<
|
||||||
mut url: I,
|
mut url: I,
|
||||||
) -> Result<RemoteDocument<I, M, T>> {
|
) -> Result<RemoteDocument<I, M, T>> {
|
||||||
const MAX_REDIRECTS: u32 = 8;
|
const MAX_REDIRECTS: u32 = 8;
|
||||||
const ACCEPT_ACTIVITY_PUB: &'static str =
|
const ACCEPT_ACTIVITY_PUB: &str =
|
||||||
"application/activity+json, application/ld+json, application/json";
|
"application/activity+json, application/ld+json, application/json";
|
||||||
|
|
||||||
let mut redirect_count = 0;
|
let mut redirect_count = 0;
|
||||||
|
@ -129,18 +131,16 @@ impl<I: Clone + Eq + Hash + Send + Sync, M: Send, T: Clone + Send> CachedLoader<
|
||||||
context_url = self.get_context_url(vocabulary, &url, &response)?;
|
context_url = self.get_context_url(vocabulary, &url, &response)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut profile = HashSet::new();
|
let profile: HashSet<Profile<I>> = content_type
|
||||||
for p in content_type
|
|
||||||
.profile()
|
.profile()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.flat_map(|p| p.split(|b| *b == b' '))
|
.flat_map(|profile| profile.split(' '))
|
||||||
{
|
.filter_map(|p| {
|
||||||
if let Ok(iri) = Iri::new(p) {
|
Iri::new(p).ok().map(|iri| Profile::new(iri, vocabulary))
|
||||||
profile.insert(Profile::new(iri, vocabulary));
|
})
|
||||||
}
|
.collect();
|
||||||
}
|
|
||||||
|
|
||||||
let bytes = response.bytes().await.map_err(|e| Error::Reqwest(e))?;
|
let bytes = response.bytes().await?;
|
||||||
let document = (*self.parser)(vocabulary, &url, bytes)
|
let document = (*self.parser)(vocabulary, &url, bytes)
|
||||||
.map_err(|e| Error::MalformedApub(e.to_string()))?;
|
.map_err(|e| Error::MalformedApub(e.to_string()))?;
|
||||||
|
|
||||||
|
@ -156,8 +156,7 @@ impl<I: Clone + Eq + Hash + Send + Sync, M: Send, T: Clone + Send> CachedLoader<
|
||||||
debug!(target: "ap", "no valid media type found");
|
debug!(target: "ap", "no valid media type found");
|
||||||
for link in response.headers().get_all(LINK).into_iter() {
|
for link in response.headers().get_all(LINK).into_iter() {
|
||||||
if let Some(link) = Link::from_header(link) {
|
if let Some(link) = Link::from_header(link) {
|
||||||
if link.rel() == Some(b"alternate") && link.is_proper_json_ld()
|
if link.rel() == Some("alternate") && link.is_proper_json_ld() {
|
||||||
{
|
|
||||||
debug!(target: "ap", "link found");
|
debug!(target: "ap", "link found");
|
||||||
let u = link.href().resolved(vocabulary.iri(&url).unwrap());
|
let u = link.href().resolved(vocabulary.iri(&url).unwrap());
|
||||||
url = vocabulary.insert(u.as_iri());
|
url = vocabulary.insert(u.as_iri());
|
||||||
|
@ -204,13 +203,13 @@ impl<I: Clone + Eq + Hash + Send + Sync, M: Send, T: Clone + Send> CachedLoader<
|
||||||
&self,
|
&self,
|
||||||
vocabulary: &mut (impl Send + Sync + IriVocabularyMut<Iri = I>),
|
vocabulary: &mut (impl Send + Sync + IriVocabularyMut<Iri = I>),
|
||||||
url: &I,
|
url: &I,
|
||||||
response: &reqwest::Response,
|
response: &Response,
|
||||||
) -> Result<Option<I>> {
|
) -> Result<Option<I>> {
|
||||||
let mut context_url = None;
|
let mut context_url = None;
|
||||||
for link in response.headers().get_all(LINK).into_iter() {
|
for link in response.headers().get_all(LINK).into_iter() {
|
||||||
if let Some(link) = Link::from_header(link) {
|
if let Some(link) = Link::from_header(link) {
|
||||||
if link.rel() == Some(b"alternate")
|
if link.rel() == Some("alternate")
|
||||||
&& link.typ() == Some(b"https://www.w3.org/ns/json-ld#context")
|
&& link.typ() == Some("https://www.w3.org/ns/json-ld#context")
|
||||||
{
|
{
|
||||||
if context_url.is_some() {
|
if context_url.is_some() {
|
||||||
return Err(Error::MalformedApub(String::from(
|
return Err(Error::MalformedApub(String::from(
|
||||||
|
|
|
@ -14,6 +14,7 @@ pub type Result<T> = std::result::Result<T, Error>;
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
BadBearcap,
|
BadBearcap,
|
||||||
BadCredentials,
|
BadCredentials,
|
||||||
|
BadHeader(String),
|
||||||
BadRequest,
|
BadRequest,
|
||||||
BadSignature,
|
BadSignature,
|
||||||
BadToken(jsonwebtoken::errors::Error),
|
BadToken(jsonwebtoken::errors::Error),
|
||||||
|
@ -33,6 +34,7 @@ impl ResponseError for Error {
|
||||||
match self {
|
match self {
|
||||||
Error::BadBearcap => StatusCode::UNPROCESSABLE_ENTITY,
|
Error::BadBearcap => StatusCode::UNPROCESSABLE_ENTITY,
|
||||||
Error::BadCredentials => StatusCode::UNAUTHORIZED,
|
Error::BadCredentials => StatusCode::UNAUTHORIZED,
|
||||||
|
Error::BadHeader(_) => StatusCode::BAD_REQUEST,
|
||||||
Error::BadRequest => StatusCode::BAD_REQUEST,
|
Error::BadRequest => StatusCode::BAD_REQUEST,
|
||||||
Error::BadSignature => StatusCode::UNAUTHORIZED,
|
Error::BadSignature => StatusCode::UNAUTHORIZED,
|
||||||
Error::BadToken(_) => StatusCode::UNAUTHORIZED,
|
Error::BadToken(_) => StatusCode::UNAUTHORIZED,
|
||||||
|
@ -55,6 +57,7 @@ impl fmt::Display for Error {
|
||||||
match self {
|
match self {
|
||||||
Error::BadBearcap => write!(f, "Invalid bearcap URL"),
|
Error::BadBearcap => write!(f, "Invalid bearcap URL"),
|
||||||
Error::BadCredentials => write!(f, "Invalid user name or password"),
|
Error::BadCredentials => write!(f, "Invalid user name or password"),
|
||||||
|
Error::BadHeader(name) => write!(f, "Value for header \"{name}\" is invalid"),
|
||||||
Error::BadRequest => write!(f, "Bad request"),
|
Error::BadRequest => write!(f, "Bad request"),
|
||||||
Error::BadSignature => write!(f, "Bad signature"),
|
Error::BadSignature => write!(f, "Bad signature"),
|
||||||
Error::BadToken(jwt_error) => jwt_error.fmt(f),
|
Error::BadToken(jwt_error) => jwt_error.fmt(f),
|
||||||
|
|
|
@ -6,11 +6,15 @@ use crate::core::*;
|
||||||
use crate::util::slice::SliceCursor;
|
use crate::util::slice::SliceCursor;
|
||||||
use crate::util::transcode;
|
use crate::util::transcode;
|
||||||
|
|
||||||
|
/// Parse an HTTP Structured Field Value according to
|
||||||
|
/// [RFC 8941](https://www.rfc-editor.org/info/rfc8941).
|
||||||
|
/// Note: This only parses one "line" although the RFC says conforming
|
||||||
|
/// software MUST support values split over several headers.
|
||||||
pub trait ParseHeader<'a>: Sized {
|
pub trait ParseHeader<'a>: Sized {
|
||||||
fn parse_from_ascii(header: &'a [u8]) -> Result<Self>;
|
fn parse_from_ascii(header: &'a [u8], strict: bool) -> Result<Self>;
|
||||||
|
|
||||||
fn parse_from_header(header: &'a HeaderValue) -> Result<Self> {
|
fn parse_from_header(header: &'a HeaderValue, strict: bool) -> Result<Self> {
|
||||||
Self::parse_from_ascii(header.as_bytes())
|
Self::parse_from_ascii(header.as_bytes(), strict)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,6 +48,7 @@ pub enum BareItem<'a> {
|
||||||
Decimal(f32),
|
Decimal(f32),
|
||||||
String(StringItem<'a>),
|
String(StringItem<'a>),
|
||||||
Token(TokenItem<'a>),
|
Token(TokenItem<'a>),
|
||||||
|
Url(UrlItem<'a>),
|
||||||
ByteSequence(ByteSequenceItem<'a>),
|
ByteSequence(ByteSequenceItem<'a>),
|
||||||
Boolean(bool),
|
Boolean(bool),
|
||||||
}
|
}
|
||||||
|
@ -53,11 +58,13 @@ pub struct StringItem<'a>(&'a str);
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
pub struct TokenItem<'a>(&'a str);
|
pub struct TokenItem<'a>(&'a str);
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
|
pub struct UrlItem<'a>(&'a str);
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
pub struct ByteSequenceItem<'a>(&'a str);
|
pub struct ByteSequenceItem<'a>(&'a str);
|
||||||
|
|
||||||
impl<'a> ParseHeader<'a> for Dictionary<'a> {
|
impl<'a> ParseHeader<'a> for Dictionary<'a> {
|
||||||
fn parse_from_ascii(header: &'a [u8]) -> Result<Self> {
|
fn parse_from_ascii(header: &'a [u8], strict: bool) -> Result<Self> {
|
||||||
Parser::new(header)?.parse_dictionary()
|
Parser::new(header, strict)?.parse_dictionary()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,8 +79,8 @@ impl<'a> Dictionary<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> ParseHeader<'a> for List<'a> {
|
impl<'a> ParseHeader<'a> for List<'a> {
|
||||||
fn parse_from_ascii(header: &'a [u8]) -> Result<Self> {
|
fn parse_from_ascii(header: &'a [u8], strict: bool) -> Result<Self> {
|
||||||
Parser::new(header)?.parse_list()
|
Parser::new(header, strict)?.parse_list()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -92,12 +99,16 @@ impl<'a> List<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> ParseHeader<'a> for Item<'a> {
|
impl<'a> ParseHeader<'a> for Item<'a> {
|
||||||
fn parse_from_ascii(header: &'a [u8]) -> Result<Self> {
|
fn parse_from_ascii(header: &'a [u8], strict: bool) -> Result<Self> {
|
||||||
Parser::new(header)?.parse_item()
|
Parser::new(header, strict)?.parse_item(!strict)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Item<'a> {
|
impl<'a> Item<'a> {
|
||||||
|
pub fn get_params(&self) -> &[(&'a str, BareItem<'a>)] {
|
||||||
|
self.params.as_slice()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn param<K>(&self, key: K) -> Option<&BareItem<'a>>
|
pub fn param<K>(&self, key: K) -> Option<&BareItem<'a>>
|
||||||
where
|
where
|
||||||
K: Into<&'a str>,
|
K: Into<&'a str>,
|
||||||
|
@ -132,6 +143,10 @@ impl<'a> Item<'a> {
|
||||||
self.bare_item.as_token()
|
self.bare_item.as_token()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn as_url(&self) -> Option<&'a str> {
|
||||||
|
self.bare_item.as_url()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn as_byte_sequence(&self) -> Option<Bytes> {
|
pub fn as_byte_sequence(&self) -> Option<Bytes> {
|
||||||
self.bare_item.as_byte_sequence()
|
self.bare_item.as_byte_sequence()
|
||||||
}
|
}
|
||||||
|
@ -170,6 +185,13 @@ impl<'a> BareItem<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn as_url(&self) -> Option<&'a str> {
|
||||||
|
match self {
|
||||||
|
BareItem::Url(u) => Some(u.0),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn as_byte_sequence(&self) -> Option<Bytes> {
|
pub fn as_byte_sequence(&self) -> Option<Bytes> {
|
||||||
match self {
|
match self {
|
||||||
BareItem::ByteSequence(bs) => Some(bs.into()),
|
BareItem::ByteSequence(bs) => Some(bs.into()),
|
||||||
|
@ -186,6 +208,13 @@ impl<'a> BareItem<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Member<'a> {
|
impl<'a> Member<'a> {
|
||||||
|
pub fn get_params(&self) -> &[(&'a str, BareItem<'a>)] {
|
||||||
|
match self {
|
||||||
|
Member::Item(i) => i.get_params(),
|
||||||
|
Member::InnerList(l) => l.get_params(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn param<K>(&self, key: K) -> Option<&BareItem<'a>>
|
pub fn param<K>(&self, key: K) -> Option<&BareItem<'a>>
|
||||||
where
|
where
|
||||||
K: Into<&'a str>,
|
K: Into<&'a str>,
|
||||||
|
@ -269,6 +298,10 @@ impl<'a> InnerList<'a> {
|
||||||
self.items.len()
|
self.items.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_params(&self) -> &[(&'a str, BareItem<'a>)] {
|
||||||
|
self.params.as_slice()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn param<K>(&self, key: K) -> Option<&BareItem<'a>>
|
pub fn param<K>(&self, key: K) -> Option<&BareItem<'a>>
|
||||||
where
|
where
|
||||||
K: Into<&'a str>,
|
K: Into<&'a str>,
|
||||||
|
@ -290,13 +323,15 @@ impl<'a> InnerList<'a> {
|
||||||
|
|
||||||
struct Parser<'a> {
|
struct Parser<'a> {
|
||||||
cursor: SliceCursor<'a, u8>,
|
cursor: SliceCursor<'a, u8>,
|
||||||
|
strict: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Parser<'a> {
|
impl<'a> Parser<'a> {
|
||||||
fn new(data: &'a [u8]) -> Result<Parser> {
|
fn new(data: &'a [u8], strict: bool) -> Result<Parser> {
|
||||||
if data.is_ascii() {
|
if data.is_ascii() || (std::str::from_utf8(data).is_ok() && !strict) {
|
||||||
Ok(Parser {
|
Ok(Parser {
|
||||||
cursor: SliceCursor::new(data),
|
cursor: SliceCursor::new(data),
|
||||||
|
strict,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
Err(Error::BadHeader(String::from(
|
Err(Error::BadHeader(String::from(
|
||||||
|
@ -335,7 +370,7 @@ impl<'a> Parser<'a> {
|
||||||
if self.cursor.peek().copied() == Some(b'(') {
|
if self.cursor.peek().copied() == Some(b'(') {
|
||||||
self.parse_inner_list().map(Member::InnerList)
|
self.parse_inner_list().map(Member::InnerList)
|
||||||
} else {
|
} else {
|
||||||
self.parse_item().map(Member::Item)
|
self.parse_item(false).map(Member::Item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -377,7 +412,7 @@ impl<'a> Parser<'a> {
|
||||||
if self.cursor.peek().copied() == Some(b'(') {
|
if self.cursor.peek().copied() == Some(b'(') {
|
||||||
Member::InnerList(self.parse_inner_list()?)
|
Member::InnerList(self.parse_inner_list()?)
|
||||||
} else {
|
} else {
|
||||||
Member::Item(self.parse_item()?)
|
Member::Item(self.parse_item(false)?)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// parameters
|
// parameters
|
||||||
|
@ -403,7 +438,7 @@ impl<'a> Parser<'a> {
|
||||||
if self.skip_if(|c| c == b')') {
|
if self.skip_if(|c| c == b')') {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
items.push(self.parse_item()?);
|
items.push(self.parse_item(false)?);
|
||||||
// > Parsers MUST support Inner Lists containing at least 256 members.
|
// > Parsers MUST support Inner Lists containing at least 256 members.
|
||||||
if items.len() == 256 {
|
if items.len() == 256 {
|
||||||
break;
|
break;
|
||||||
|
@ -424,8 +459,8 @@ impl<'a> Parser<'a> {
|
||||||
/// ```notrust
|
/// ```notrust
|
||||||
/// sf-item = bare-item parameters
|
/// sf-item = bare-item parameters
|
||||||
/// ```
|
/// ```
|
||||||
fn parse_item(&mut self) -> Result<Item<'a>> {
|
fn parse_item(&mut self, allow_url: bool) -> Result<Item<'a>> {
|
||||||
let bare_item = self.parse_bare_item()?;
|
let bare_item = self.parse_bare_item(allow_url)?;
|
||||||
let params = self.parse_parameters()?;
|
let params = self.parse_parameters()?;
|
||||||
Ok(Item { bare_item, params })
|
Ok(Item { bare_item, params })
|
||||||
}
|
}
|
||||||
|
@ -457,7 +492,7 @@ impl<'a> Parser<'a> {
|
||||||
fn parse_parameter(&mut self) -> Result<(&'a str, BareItem<'a>)> {
|
fn parse_parameter(&mut self) -> Result<(&'a str, BareItem<'a>)> {
|
||||||
let key = self.parse_key()?;
|
let key = self.parse_key()?;
|
||||||
let value = if self.skip_if(|c| c == b'=') {
|
let value = if self.skip_if(|c| c == b'=') {
|
||||||
self.parse_bare_item()?
|
self.parse_bare_item(false)?
|
||||||
} else {
|
} else {
|
||||||
BareItem::Boolean(true)
|
BareItem::Boolean(true)
|
||||||
};
|
};
|
||||||
|
@ -484,7 +519,7 @@ impl<'a> Parser<'a> {
|
||||||
/// bare-item = sf-integer / sf-decimal / sf-string
|
/// bare-item = sf-integer / sf-decimal / sf-string
|
||||||
/// / sf-token / sf-binary / sf-boolean
|
/// / sf-token / sf-binary / sf-boolean
|
||||||
/// ```
|
/// ```
|
||||||
fn parse_bare_item(&mut self) -> Result<BareItem<'a>> {
|
fn parse_bare_item(&mut self, allow_url: bool) -> Result<BareItem<'a>> {
|
||||||
match self
|
match self
|
||||||
.cursor
|
.cursor
|
||||||
.peek()
|
.peek()
|
||||||
|
@ -493,10 +528,11 @@ impl<'a> Parser<'a> {
|
||||||
{
|
{
|
||||||
c if is_numeric_start(c) => self.parse_numeric(),
|
c if is_numeric_start(c) => self.parse_numeric(),
|
||||||
b'"' => self.parse_string(),
|
b'"' => self.parse_string(),
|
||||||
|
b'<' if allow_url => self.parse_url(),
|
||||||
c if is_token_start(c) => self.parse_token(),
|
c if is_token_start(c) => self.parse_token(),
|
||||||
b':' => self.parse_byte_sequence(),
|
b':' => self.parse_byte_sequence(),
|
||||||
b'?' => self.parse_boolean(),
|
b'?' => self.parse_boolean(),
|
||||||
_ => Err(self.make_error("Unexpected character")),
|
c => Err(self.make_error(format!("Unexpected character {:?}", c as char))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -550,6 +586,21 @@ impl<'a> Parser<'a> {
|
||||||
Ok(BareItem::String(StringItem(slice)))
|
Ok(BareItem::String(StringItem(slice)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_url(&mut self) -> Result<BareItem<'a>> {
|
||||||
|
if self.strict {
|
||||||
|
return Err(
|
||||||
|
self.make_error("URLs enclosed in <angle brackets> are forbidden in strict mode")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.assert_next(|c| c == b'<')?;
|
||||||
|
self.chop();
|
||||||
|
self.skip_while(|c| c != b'>');
|
||||||
|
let slice = self.chop();
|
||||||
|
self.assert_next(|c| c == b'>')?;
|
||||||
|
Ok(BareItem::Url(UrlItem(slice)))
|
||||||
|
}
|
||||||
|
|
||||||
/// Parse a Token item (section 3.3.4).
|
/// Parse a Token item (section 3.3.4).
|
||||||
///
|
///
|
||||||
/// ```notrust
|
/// ```notrust
|
||||||
|
@ -736,15 +787,15 @@ mod tests {
|
||||||
use crate::util::transcode::base64_decode;
|
use crate::util::transcode::base64_decode;
|
||||||
|
|
||||||
fn mklist(header: &'static str) -> Result<List<'static>> {
|
fn mklist(header: &'static str) -> Result<List<'static>> {
|
||||||
List::parse_from_ascii(header.as_bytes())
|
List::parse_from_ascii(header.as_bytes(), true)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn mkdict(header: &'static str) -> Result<Dictionary<'static>> {
|
fn mkdict(header: &'static str) -> Result<Dictionary<'static>> {
|
||||||
Dictionary::parse_from_ascii(header.as_bytes())
|
Dictionary::parse_from_ascii(header.as_bytes(), true)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn mkitem(header: &'static str) -> Result<Item<'static>> {
|
fn mkitem(header: &'static str) -> Result<Item<'static>> {
|
||||||
Item::parse_from_ascii(header.as_bytes())
|
Item::parse_from_ascii(header.as_bytes(), true)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
@ -1,10 +1,15 @@
|
||||||
use reqwest::header::{HeaderName, AUTHORIZATION, USER_AGENT};
|
use bytes::Bytes;
|
||||||
use reqwest::{RequestBuilder, Response};
|
use reqwest::header::{HeaderMap, HeaderName, AUTHORIZATION, USER_AGENT};
|
||||||
|
use reqwest::{RequestBuilder, StatusCode};
|
||||||
|
|
||||||
use crate::core::*;
|
use crate::core::*;
|
||||||
use crate::state::AppState;
|
use crate::state::AppState;
|
||||||
use crate::util::bear::Bearcap;
|
use crate::util::bear::Bearcap;
|
||||||
|
|
||||||
|
pub enum Response {
|
||||||
|
Reqwest(reqwest::Response),
|
||||||
|
}
|
||||||
|
|
||||||
/// Perform an HTTP GET request to the specified URL (supports bearcaps).
|
/// Perform an HTTP GET request to the specified URL (supports bearcaps).
|
||||||
/// Use the [`crate::headers`] macro for the `headers` parameter.
|
/// Use the [`crate::headers`] macro for the `headers` parameter.
|
||||||
///
|
///
|
||||||
|
@ -39,7 +44,7 @@ use crate::util::bear::Bearcap;
|
||||||
/// let response = get(&state, "bear:?t=b4dc0ffee&u=https://www.example.com", None).await?;
|
/// let response = get(&state, "bear:?t=b4dc0ffee&u=https://www.example.com", None).await?;
|
||||||
/// ```
|
/// ```
|
||||||
pub async fn get(
|
pub async fn get(
|
||||||
state: &AppState,
|
_state: &AppState,
|
||||||
url: &str,
|
url: &str,
|
||||||
headers: Option<&[(GenericHeaderName<'_>, String)]>,
|
headers: Option<&[(GenericHeaderName<'_>, String)]>,
|
||||||
) -> Result<Response> {
|
) -> Result<Response> {
|
||||||
|
@ -52,15 +57,15 @@ pub async fn get(
|
||||||
} else {
|
} else {
|
||||||
client.get(url)
|
client.get(url)
|
||||||
};
|
};
|
||||||
perform_request(state, builder, headers.unwrap_or(&[])).await
|
|
||||||
|
let response = perform_request(builder, headers.unwrap_or(&[])).await?;
|
||||||
|
Ok(Response::Reqwest(response))
|
||||||
}
|
}
|
||||||
|
|
||||||
// the state reference will be used for caching
|
|
||||||
async fn perform_request(
|
async fn perform_request(
|
||||||
_state: &AppState,
|
|
||||||
mut builder: RequestBuilder,
|
mut builder: RequestBuilder,
|
||||||
headers: &[(GenericHeaderName<'_>, String)],
|
headers: &[(GenericHeaderName<'_>, String)],
|
||||||
) -> Result<Response> {
|
) -> Result<reqwest::Response> {
|
||||||
builder = headers.iter().fold(builder, |b, (k, v)| match k {
|
builder = headers.iter().fold(builder, |b, (k, v)| match k {
|
||||||
GenericHeaderName::Standard(hdr) => b.header(hdr, v),
|
GenericHeaderName::Standard(hdr) => b.header(hdr, v),
|
||||||
GenericHeaderName::CustomOwned(hdr) => b.header(hdr, v),
|
GenericHeaderName::CustomOwned(hdr) => b.header(hdr, v),
|
||||||
|
@ -70,6 +75,26 @@ async fn perform_request(
|
||||||
Ok(builder.send().await?)
|
Ok(builder.send().await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Response {
|
||||||
|
pub fn status(&self) -> StatusCode {
|
||||||
|
match self {
|
||||||
|
Response::Reqwest(r) => r.status(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn headers(&self) -> &HeaderMap {
|
||||||
|
match self {
|
||||||
|
Response::Reqwest(r) => r.headers(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn bytes(self) -> Result<Bytes> {
|
||||||
|
match self {
|
||||||
|
Response::Reqwest(r) => r.bytes().await.map_err(Error::from),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Generate a list of HTTP request headers for passing to [`get`].
|
/// Generate a list of HTTP request headers for passing to [`get`].
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! headers {
|
macro_rules! headers {
|
||||||
|
@ -106,3 +131,13 @@ impl<'a> From<HeaderName> for GenericHeaderName<'a> {
|
||||||
GenericHeaderName::Standard(val)
|
GenericHeaderName::Standard(val)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl PartialEq<str> for GenericHeaderName<'_> {
|
||||||
|
fn eq(&self, other: &str) -> bool {
|
||||||
|
match self {
|
||||||
|
GenericHeaderName::CustomOwned(s) => s.eq(other),
|
||||||
|
GenericHeaderName::CustomBorrow(s) => (*s).eq(other),
|
||||||
|
GenericHeaderName::Standard(v) => v.as_str().eq(other),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
pub mod bear;
|
pub mod bear;
|
||||||
pub mod crypto;
|
pub mod crypto;
|
||||||
/// Almost complete implementation of [RFC 8941](https://www.rfc-editor.org/rfc/rfc8941).
|
/// Almost complete implementation of [RFC 8941](https://www.rfc-editor.org/info/rfc8941).
|
||||||
pub mod header;
|
pub mod header;
|
||||||
/// Wrappers for [`reqwest`].
|
/// Wrappers for [`reqwest`].
|
||||||
pub mod http;
|
pub mod http;
|
||||||
|
|
|
@ -25,9 +25,7 @@ impl<'a, T> SliceCursor<'a, T> {
|
||||||
chop: 0,
|
chop: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, T> SliceCursor<'a, T> {
|
|
||||||
/// Return the item at the current position, if any.
|
/// Return the item at the current position, if any.
|
||||||
pub fn current(&self) -> Option<&'a T> {
|
pub fn current(&self) -> Option<&'a T> {
|
||||||
self.pos.index().map(|index| &self.data[index])
|
self.pos.index().map(|index| &self.data[index])
|
||||||
|
@ -43,7 +41,7 @@ impl<'a, T> SliceCursor<'a, T> {
|
||||||
self.pos.next_index().map(|index| &self.data[index])
|
self.pos.next_index().map(|index| &self.data[index])
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the current item and reverse the cursor.
|
/// Reverse the cursor to the previous item and return that item.
|
||||||
pub fn prev(&mut self) -> Option<&'a T> {
|
pub fn prev(&mut self) -> Option<&'a T> {
|
||||||
self.pos.reverse().map(|index| &self.data[index])
|
self.pos.reverse().map(|index| &self.data[index])
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue