Skip to content

Commit

Permalink
Integrate support for Kaduceus
Browse files Browse the repository at this point in the history
  • Loading branch information
garyttierney committed Oct 27, 2024
1 parent 4923dcb commit 5aabef6
Show file tree
Hide file tree
Showing 16 changed files with 464 additions and 209 deletions.
4 changes: 4 additions & 0 deletions .cargo/config.toml
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
[build]
rustflags = ["--cfg", "hyper_unstable_tracing", "--cfg", "tokio_unstable"]

[env]
CC = "clang"
CXX = "clang++"
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "kaduceus"]
path = kaduceus
url = https://github.com/digirati-co-uk/kaduceus.git
1 change: 1 addition & 0 deletions kaduceus
Submodule kaduceus added at 214c0f
220 changes: 98 additions & 122 deletions src/http.rs
Original file line number Diff line number Diff line change
@@ -1,102 +1,118 @@
use std::convert::Infallible;
use std::error::Error;
use std::fmt::{Debug, Display, Formatter};
use std::future::Future;
use std::future::{ready, Future};
use std::pin::Pin;
use std::sync::Arc;
use std::task::Poll;

use futures::Stream;
use http_body_util::combinators::BoxBody;
use http_body_util::{BodyExt, Empty, Full};
use hyper::body::{Body, Bytes, Incoming};
use hyper::service::Service;
use hyper::{Method, Request, Response, StatusCode};
use tracing::{error, info};

use crate::iiif::info::ImageInfo;
use crate::iiif::parse::ParseError as ImageRequestParseError;
use crate::iiif::{Format, ImageRequest, Quality, Region, Rotation, Size};
use crate::iiif::{Format, IiifRequest, Quality, Region, Rotation, Size};
use crate::image::{ImageMetadataResolver, ImagePipeline, ImageSourceResolver};

const PREFIX: &str = "/"; // TODO: read this from config

#[tracing::instrument]
pub async fn handle_request(
req: Request<Incoming>,
images: (),
) -> Result<Response<BoxBody<Bytes, std::io::Error>>, hyper::http::Error> {
match (req.method(), req.uri().path()) {
(&Method::GET, p) if p.ends_with("info.json") => info_request(p),
(&Method::GET, p) if p.starts_with(PREFIX) => image_request(p, images).await,
_ => Response::builder()
.status(StatusCode::NOT_FOUND)
.body(Empty::new().map_err(|e| unreachable!()).boxed()),
pub struct IiifImageService<L: ImageSourceResolver, R: ImageMetadataResolver> {
options: Arc<IiifImageServiceOptions>,
pipeline: Arc<ImagePipeline<L, R>>,
}

impl<L: ImageSourceResolver, R: ImageMetadataResolver> Clone for IiifImageService<L, R> {
fn clone(&self) -> Self {
Self { options: self.options.clone(), pipeline: self.pipeline.clone() }
}
}

#[tracing::instrument]
async fn image_request(
path: &str,
source: (),
) -> Result<Response<BoxBody<Bytes, std::io::Error>>, hyper::http::Error> {
let request = match decode_image_request(path) {
Ok(r) => r,
Err(e) => return bad_request(e.to_string()),
};
impl<L: ImageSourceResolver, R: ImageMetadataResolver> IiifImageService<L, R> {
pub fn new_with_prefix<S: Into<String>>(pipeline: ImagePipeline<L, R>, prefix: S) -> Self {
Self {
pipeline: Arc::new(pipeline),
options: Arc::new(IiifImageServiceOptions { prefix: prefix.into() }),
}
}
}

// let Ok(image) = todo!("fix this"); source.resolve(request.identifier()).await else {
// return Ok(bad_request("io error")); // TODO
// };
#[derive(Clone)]
pub struct IiifImageServiceOptions {
prefix: String,
}

Response::builder()
.status(StatusCode::OK)
.body(Empty::new().map_err(|e| unreachable!()).boxed())
impl<L: ImageSourceResolver + Send + Sync, R: ImageMetadataResolver + Send + Sync>
tower::Service<Request<Incoming>> for IiifImageService<L, R>
{
type Response = Response<BoxBody<Bytes, std::io::Error>>;
type Error = hyper::http::Error;
type Future = Pin<Box<dyn Sync + Send + Future<Output = Result<Self::Response, Self::Error>>>>;

fn call(&mut self, req: Request<Incoming>) -> Self::Future {
let options = self.options.clone();
let pipeline = self.pipeline.clone();

Box::pin(dispatch_request(req, options))
}

fn poll_ready(
&mut self,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
}

pub enum IiifImageServiceResponse {
Info(ImageInfo),
Image(Box<dyn Stream<Item = Bytes>>),
}

pub async fn dispatch_request(
req: Request<Incoming>,
options: Arc<IiifImageServiceOptions>,
) -> Result<Response<BoxBody<Bytes, std::io::Error>>, hyper::http::Error> {
let request = decode_request(req, &options);

match request {
Ok(IiifRequest::Image { .. }) => todo!(),
Ok(IiifRequest::Info { identifier }) => info_request(&identifier),
Err(e) => bad_request(e.to_string()),
}
}

fn decode_image_request(path: &str) -> Result<ImageRequest, ImageRequestError> {
let mut segments = path.split('/');
debug_assert!(segments.next().is_some_and(|s| s.is_empty()));

let identifier = segments
.next()
.ok_or(ImageRequestError::UriMissingElement("identifier"))
.and_then(|input| {
urlencoding::decode(input).map_err(|err| ImageRequestError::UriNotUtf8("identifier"))
})?;

let region = segments
.next()
.ok_or(ImageRequestError::UriMissingElement("region"))?
.parse::<Region>()
.map_err(ImageRequestError::from)?;

let size = segments
.next()
.ok_or(ImageRequestError::UriMissingElement("size"))?
.parse::<Size>()
.map_err(ImageRequestError::from)?;

let rotation = segments
.next()
.ok_or(ImageRequestError::UriMissingElement("rotation"))?
.parse::<Rotation>()
.map_err(ImageRequestError::from)?;

let (quality, format) = segments
.next()
.ok_or(ImageRequestError::UriMissingElement("quality"))?
.split_once('.')
.ok_or(ImageRequestError::UriMissingElement("format"))?;

let quality = quality
.parse::<Quality>()
.map_err(ImageRequestError::ParseError)?;

let format = format
.parse::<Format>()
.map_err(ImageRequestError::ParseError)?;

Ok(ImageRequest::new(identifier, region, size, rotation, quality, format))
#[tracing::instrument(skip_all, ret, err)]
fn decode_request(
req: Request<Incoming>,
options: &IiifImageServiceOptions,
) -> Result<IiifRequest, IiifRequestError> {
req.uri()
.path()
.trim_start_matches(&options.prefix.trim_end_matches("/"))
.parse::<IiifRequest>()
}

// #[tracing::instrument(skip_all, err)]
// async fn image_request(
// request: ImageRequest,
// source: (),
// ) -> Result<Response<BoxBody<Bytes, std::io::Error>>, hyper::http::Error> {
// // let Ok(image) = todo!("fix this"); source.resolve(request.identifier()).await else {
// // return Ok(bad_request("io error")); // TODO
// // };

// Response::builder()
// .status(StatusCode::OK)
// .body(Empty::new().map_err(|e| unreachable!()).boxed())
// }

#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ImageRequestError {
pub enum IiifRequestError {
/// If the URI did not contain an expected element.
UriMissingElement(&'static str),

Expand All @@ -107,34 +123,33 @@ pub enum ImageRequestError {
ParseError(ImageRequestParseError),
}

impl Error for ImageRequestError {}
impl Error for IiifRequestError {}

impl Display for ImageRequestError {
impl Display for IiifRequestError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
ImageRequestError::UriMissingElement(element) => {
IiifRequestError::UriMissingElement(element) => {
write!(f, "Request path missing {element}.")
}
ImageRequestError::ParseError(error) => Display::fmt(error, f),
ImageRequestError::UriNotUtf8(element) => {
IiifRequestError::ParseError(error) => Display::fmt(error, f),
IiifRequestError::UriNotUtf8(element) => {
write!(f, "Request path {element} was not in UTF-8.")
}
}
}
}

impl From<ImageRequestParseError> for ImageRequestError {
impl From<ImageRequestParseError> for IiifRequestError {
fn from(value: ImageRequestParseError) -> Self {
ImageRequestError::ParseError(value)
IiifRequestError::ParseError(value)
}
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub enum InfoRequestError {}

fn info_request(
path: &str,
) -> Result<Response<BoxBody<Bytes, std::io::Error>>, hyper::http::Error> {
#[tracing::instrument(ret, err)]
fn info_request(id: &str) -> Result<Response<BoxBody<Bytes, std::io::Error>>, hyper::http::Error> {
unimplemented!()
}

Expand All @@ -145,42 +160,3 @@ fn bad_request<I: Into<Bytes>>(
.status(StatusCode::BAD_REQUEST)
.body(Full::new(body.into()).map_err(|e| match e {}).boxed())
}

#[cfg(test)]
mod test {
use super::*;
use crate::iiif::Scale;

#[test]
fn decode_basic_image_request() {
let request = decode_image_request("/abcd1234/full/max/0/default.jpg");
assert_eq!(
request,
Ok(ImageRequest::new(
"abcd1234",
Region::Full,
Size::new(Scale::Max),
Rotation::new(0.0),
Quality::Default,
Format::Jpg,
))
);
}

#[test]
fn decode_encoded_image_request() {
// Image API 3.0, s 9: to-encode = "/" / "?" / "#" / "[" / "]" / "@" / "%"
let request = decode_image_request("/a%2F%3F%23%5B%5D%40%25z/full/max/0/default.jpg");
assert_eq!(
request,
Ok(ImageRequest::new(
"a/?#[]@%z",
Region::Full,
Size::new(Scale::Max),
Rotation::new(0.0),
Quality::Default,
Format::Jpg,
))
);
}
}
Loading

0 comments on commit 5aabef6

Please sign in to comment.