use crate::{
collection::{ProfileId, RecipeId},
database::{
CollectionId, CollectionMetadata, DatabaseError, ProfileFilter,
},
http::{
Exchange, ExchangeSummary, HttpMethod, HttpVersion, RequestBody,
RequestBodyKind, RequestId, RequestRecord, ResponseRecord,
},
};
use bytes::Bytes;
use core::str;
use derive_more::Display;
use reqwest::{
StatusCode,
header::{HeaderMap, HeaderName, HeaderValue},
};
use rusqlite::{
Row, ToSql,
types::{FromSql, FromSqlError, FromSqlResult, ToSqlOutput, ValueRef},
};
use slumber_util::{ResultTraced, paths};
use std::{
env,
fmt::Debug,
ops::Deref,
path::{Path, PathBuf},
str::Utf8Error,
sync::Arc,
};
use thiserror::Error;
use url::Url;
use uuid::Uuid;
use winnow::{
ModalResult, Parser,
combinator::{repeat, terminated},
token::take_while,
};
impl ToSql for CollectionId {
fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
self.0.to_sql()
}
}
impl FromSql for CollectionId {
fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
Ok(Self(Uuid::column_result(value)?))
}
}
impl ToSql for ProfileId {
fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
self.deref().to_sql()
}
}
impl FromSql for ProfileId {
fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
Ok(String::column_result(value)?.into())
}
}
impl ToSql for RequestId {
fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
self.0.to_sql()
}
}
impl FromSql for RequestId {
fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
Ok(Self(Uuid::column_result(value)?))
}
}
impl ToSql for RecipeId {
fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
self.deref().to_sql()
}
}
impl FromSql for RecipeId {
fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
Ok(String::column_result(value)?.into())
}
}
impl ToSql for HttpVersion {
fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
self.to_str().to_sql()
}
}
impl FromSql for HttpVersion {
fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
String::column_result(value)?.parse().map_err(error_other)
}
}
impl ToSql for HttpMethod {
fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
self.to_str().to_sql()
}
}
impl FromSql for HttpMethod {
fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
String::column_result(value)?.parse().map_err(error_other)
}
}
#[derive(Clone, Debug, Display)]
#[display("{}", _0.display())]
pub struct CollectionPath(PathBuf);
impl CollectionPath {
pub fn try_from_path(path: &Path) -> Result<Self, DatabaseError> {
path.canonicalize()
.map_err(|error| DatabaseError::Path {
path: path.to_owned(),
error,
})
.traced()
.map(Self)
}
pub fn try_from_path_maybe_missing(
path: &Path,
) -> Result<Self, DatabaseError> {
Self::try_from_path(path).or_else(|_| {
let base = if path.is_relative() {
env::current_dir().map_err(|error| DatabaseError::Path {
path: path.to_owned(),
error,
})?
} else {
PathBuf::new()
};
Ok(Self(paths::normalize_path(&base, path)))
})
}
}
impl From<CollectionPath> for PathBuf {
fn from(value: CollectionPath) -> Self {
value.0
}
}
#[cfg(test)]
impl From<PathBuf> for CollectionPath {
fn from(path: PathBuf) -> Self {
Self(path)
}
}
impl ToSql for CollectionPath {
fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
#[derive(Debug, Error)]
#[error("Collection path `{0:?}` is not valid UTF-8 as UTF-8")]
struct PathStringifyError(PathBuf);
self.0
.to_str()
.ok_or_else(|| {
rusqlite::Error::ToSqlConversionFailure(
PathStringifyError(self.0.clone()).into(),
)
})?
.as_bytes()
.to_sql()
}
}
impl FromSql for CollectionPath {
fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
#[derive(Debug, Error)]
#[error("Error parsing collection path as UTF-8")]
struct PathParseError(Utf8Error);
let path = str::from_utf8(value.as_blob()?)
.map_err(PathParseError)
.map_err(error_other)?
.to_owned();
Ok(Self(path.into()))
}
}
impl RequestBodyKind {
const KIND_NONE: u8 = 0;
const KIND_SOME: u8 = 1;
const KIND_STREAM: u8 = 2;
const KIND_TOO_LARGE: u8 = 3;
}
impl ToSql for RequestBodyKind {
fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
match self {
RequestBodyKind::None => Self::KIND_NONE.to_sql(),
RequestBodyKind::Some => Self::KIND_SOME.to_sql(),
RequestBodyKind::Stream => Self::KIND_STREAM.to_sql(),
RequestBodyKind::TooLarge => Self::KIND_TOO_LARGE.to_sql(),
}
}
}
impl FromSql for RequestBodyKind {
fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
let code: u8 = u8::column_result(value)?;
match code {
Self::KIND_NONE => Ok(Self::None),
Self::KIND_SOME => Ok(Self::Some),
Self::KIND_STREAM => Ok(Self::Stream),
Self::KIND_TOO_LARGE => Ok(Self::TooLarge),
_ => Err(FromSqlError::Other(
format!("Invalid request_body_kind {code}").into(),
)),
}
}
}
impl<'a, 'b> TryFrom<&'a Row<'b>> for CollectionMetadata {
type Error = rusqlite::Error;
fn try_from(row: &'a Row<'b>) -> Result<Self, Self::Error> {
Ok(Self {
id: row.get("id")?,
path: row.get::<_, CollectionPath>("path")?.0,
name: row.get("name")?,
})
}
}
impl<'a, 'b> TryFrom<&'a Row<'b>> for Exchange {
type Error = rusqlite::Error;
fn try_from(row: &'a Row<'b>) -> Result<Self, Self::Error> {
let id: RequestId = row.get("id")?;
let request_body_kind: RequestBodyKind =
row.get("request_body_kind")?;
let request_body = match request_body_kind {
RequestBodyKind::None => RequestBody::None,
RequestBodyKind::Some => {
let bytes = row.get::<_, SqlWrap<Bytes>>("request_body")?.0;
RequestBody::Some(bytes)
}
RequestBodyKind::Stream => RequestBody::Stream,
RequestBodyKind::TooLarge => RequestBody::TooLarge,
};
Ok(Self {
id,
start_time: row.get("start_time")?,
end_time: row.get("end_time")?,
request: Arc::new(RequestRecord {
id,
profile_id: row.get("profile_id")?,
recipe_id: row.get("recipe_id")?,
http_version: row.get("http_version")?,
method: row.get("method")?,
url: row.get::<_, SqlWrap<_>>("url")?.0,
headers: row.get::<_, SqlWrap<HeaderMap>>("request_headers")?.0,
body: request_body,
}),
response: Arc::new(ResponseRecord {
id,
status: row.get::<_, SqlWrap<StatusCode>>("status_code")?.0,
headers: row
.get::<_, SqlWrap<HeaderMap>>("response_headers")?
.0,
body: row.get::<_, SqlWrap<Bytes>>("response_body")?.0.into(),
}),
})
}
}
impl<'a, 'b> TryFrom<&'a Row<'b>> for ExchangeSummary {
type Error = rusqlite::Error;
fn try_from(row: &'a Row<'b>) -> Result<Self, Self::Error> {
Ok(Self {
id: row.get("id")?,
recipe_id: row.get("recipe_id")?,
profile_id: row.get("profile_id")?,
start_time: row.get("start_time")?,
end_time: row.get("end_time")?,
status: row.get::<_, SqlWrap<StatusCode>>("status_code")?.0,
})
}
}
pub struct SqlWrap<T>(pub T);
impl FromSql for SqlWrap<Url> {
fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
value.as_str()?.parse().map(Self).map_err(error_other)
}
}
impl FromSql for SqlWrap<Bytes> {
fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
let bytes = value.as_blob()?.to_owned();
Ok(Self(bytes.into()))
}
}
impl FromSql for SqlWrap<StatusCode> {
fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
let code: u16 = value.as_i64()?.try_into().map_err(error_other)?;
let code = StatusCode::from_u16(code as u16).map_err(error_other)?;
Ok(Self(code))
}
}
const HEADER_FIELD_DELIM: u8 = b':';
const HEADER_LINE_DELIM: u8 = b'\n';
impl ToSql for SqlWrap<&HeaderMap> {
fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
let capacity = self
.0
.iter()
.map(|(name, value)| {
name.as_str().len() + 1 + value.as_bytes().len() + 1
})
.sum();
let mut buf: Vec<u8> = Vec::with_capacity(capacity);
for (name, value) in self.0 {
buf.extend(name.as_str().as_bytes());
buf.push(HEADER_FIELD_DELIM);
buf.extend(value.as_bytes());
buf.push(HEADER_LINE_DELIM);
}
Ok(buf.into())
}
}
impl FromSql for SqlWrap<HeaderMap> {
fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
fn header_line(
input: &mut &[u8],
) -> ModalResult<(HeaderName, HeaderValue)> {
(
terminated(
take_while(1.., |c| c != HEADER_FIELD_DELIM)
.try_map(HeaderName::from_bytes),
HEADER_FIELD_DELIM,
),
terminated(
take_while(0.., |c| c != HEADER_LINE_DELIM)
.try_map(HeaderValue::from_bytes),
HEADER_LINE_DELIM,
),
)
.parse_next(input)
}
let bytes = value.as_blob()?;
let lines = repeat(0.., header_line)
.fold(HeaderMap::new, |mut acc, (name, value)| {
acc.insert(name, value);
acc
})
.parse(bytes)
.map_err(|error| {
#[derive(Debug, Error)]
#[error("{0}")]
struct HeaderParseError(String);
error_other(HeaderParseError(error.to_string()))
})?;
Ok(Self(lines))
}
}
impl ToSql for ProfileFilter<'_> {
fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
match self {
Self::None => None::<&ProfileId>.to_sql(),
Self::Some(id) => id.to_sql(),
Self::All => None::<&ProfileId>.to_sql(),
}
}
}
fn error_other<T>(error: T) -> FromSqlError
where
T: 'static + std::error::Error + Send + Sync,
{
FromSqlError::Other(Box::new(error))
}