Skip to content

Commit 63139f9

Browse files
tpoliawabbiemery
andauthored
Rework API to reflect desired user interface (#75)
Reduce the query space to searching by instrument session for runs and within runs for data. Stops the API being as tightly coupled to the internal structure of tiled. --------- Co-authored-by: Abigail Emery <[email protected]>
1 parent 01f835d commit 63139f9

File tree

12 files changed

+414
-550
lines changed

12 files changed

+414
-550
lines changed

Cargo.lock

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ edition = "2024"
66
[dependencies]
77
async-graphql = { version = "7.0.17", features = ["uuid"]}
88
tokio = { version = "1", features = ["full"]}
9-
reqwest = { version = "0.12.15", features = ["json", "rustls-tls"], default-features = false }
9+
reqwest = { version = "0.12.15", features = ["json", "rustls-tls", "stream"], default-features = false }
1010
serde_json = "1.0.143"
1111
serde = { version = "1.0.219", features = ["derive"] }
1212
axum = "0.8.4"

src/clients.rs

Lines changed: 48 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,30 @@
1+
use std::borrow::Cow;
12
use std::fmt;
23

34
use axum::http::HeaderMap;
45
#[cfg(test)]
56
use httpmock::MockServer;
67
use reqwest::{Client, Url};
78
use serde::de::DeserializeOwned;
8-
use tracing::{info, instrument};
9-
use uuid::Uuid;
9+
use tracing::{debug, info, instrument};
1010

11-
use crate::model::{app, array, container, event_stream, run, table};
11+
use crate::model::{app, node, table};
1212

1313
pub type ClientResult<T> = Result<T, ClientError>;
1414

15+
#[derive(Clone)]
1516
pub struct TiledClient {
1617
client: Client,
1718
address: Url,
1819
}
1920

2021
impl TiledClient {
2122
pub fn new(address: Url) -> Self {
23+
if address.cannot_be_a_base() {
24+
// Panicking is not great but if we've got this far, nothing else is going to work so
25+
// bail out early.
26+
panic!("Invalid tiled URL");
27+
}
2228
Self {
2329
client: Client::new(),
2430
address,
@@ -29,17 +35,16 @@ impl TiledClient {
2935
&self,
3036
endpoint: &str,
3137
headers: Option<HeaderMap>,
32-
query_params: Option<&[(&str, &str)]>,
38+
query_params: Option<&[(&str, Cow<'_, str>)]>,
3339
) -> ClientResult<T> {
34-
info!("Requesting from tiled: {}", endpoint);
3540
let url = self.address.join(endpoint)?;
3641

3742
let mut request = match headers {
3843
Some(headers) => self.client.get(url).headers(headers),
3944
None => self.client.get(url),
4045
};
4146
if let Some(params) = query_params {
42-
request = request.query(params);
47+
request = request.query(&params);
4348
}
4449
info!("Querying: {request:?}");
4550

@@ -50,84 +55,59 @@ impl TiledClient {
5055
pub async fn app_metadata(&self) -> ClientResult<app::AppMetadata> {
5156
self.request("/api/v1/", None, None).await
5257
}
53-
pub async fn run_metadata(&self, id: Uuid) -> ClientResult<run::RunMetadataRoot> {
54-
self.request(&format!("/api/v1/metadata/{id}"), None, None)
55-
.await
56-
}
57-
pub async fn event_stream_metadata(
58+
pub async fn search(
5859
&self,
59-
id: Uuid,
60-
stream: String,
61-
) -> ClientResult<event_stream::EventStreamMetadataRoot> {
62-
self.request(&format!("/api/v1/metadata/{id}/{stream}"), None, None)
60+
path: &str,
61+
query: &[(&str, Cow<'_, str>)],
62+
) -> ClientResult<node::Root> {
63+
self.request(&format!("api/v1/search/{}", path), None, Some(query))
6364
.await
6465
}
65-
pub async fn array_metadata(
66-
&self,
67-
id: Uuid,
68-
stream: String,
69-
array: String,
70-
) -> ClientResult<array::ArrayMetadataRoot> {
71-
self.request(
72-
&format!("/api/v1/metadata/{id}/{stream}/{array}"),
73-
None,
74-
Some(&[("include_data_sources", "true")]),
75-
)
76-
.await
77-
}
78-
pub async fn table_metadata(
79-
&self,
80-
id: Uuid,
81-
stream: String,
82-
table: String,
83-
) -> ClientResult<table::TableMetadataRoot> {
84-
self.request(
85-
&format!("/api/v1/metadata/{id}/{stream}/{table}"),
86-
None,
87-
Some(&[("include_data_sources", "true")]),
88-
)
89-
.await
90-
}
9166
pub async fn table_full(
9267
&self,
93-
id: Uuid,
94-
stream: String,
95-
table: String,
68+
path: &str,
69+
columns: Option<Vec<String>>,
9670
) -> ClientResult<table::Table> {
9771
let mut headers = HeaderMap::new();
9872
headers.insert("accept", "application/json".parse().unwrap());
73+
let query = columns.map(|columns| {
74+
columns
75+
.into_iter()
76+
.map(|col| ("column", col.into()))
77+
.collect::<Vec<_>>()
78+
});
9979

10080
self.request(
101-
&format!("/api/v1/table/full/{id}/{stream}/{table}"),
81+
&format!("/api/v1/table/full/{}", path),
10282
Some(headers),
103-
None,
83+
query.as_deref(),
10484
)
10585
.await
10686
}
107-
pub async fn search_root(&self) -> ClientResult<run::RunRoot> {
108-
self.request("/api/v1/search/", None, None).await
109-
}
110-
pub async fn search_run_container(
111-
&self,
112-
id: Uuid,
113-
) -> ClientResult<event_stream::EventStreamRoot> {
114-
self.request(&format!("/api/v1/search/{id}"), None, None)
115-
.await
116-
}
117-
pub async fn container_full(
118-
&self,
119-
id: Uuid,
120-
stream: Option<String>,
121-
) -> ClientResult<container::Container> {
122-
let mut headers = HeaderMap::new();
123-
headers.insert("accept", "application/json".parse().unwrap());
12487

125-
let endpoint = match stream {
126-
Some(stream) => &format!("/api/v1/container/full/{id}/{stream}"),
127-
None => &format!("/api/v1/container/full/{id}"),
128-
};
88+
pub(crate) async fn download(
89+
&self,
90+
run: String,
91+
stream: String,
92+
det: String,
93+
id: u32,
94+
) -> reqwest::Result<reqwest::Response> {
95+
let mut url = self
96+
.address
97+
.join("/api/v1/asset/bytes")
98+
.expect("Base address was cannot_be_a_base");
99+
url.path_segments_mut()
100+
.expect("Base address was cannot_be_a_base")
101+
.push(&run)
102+
.push(&stream)
103+
.push(&det);
129104

130-
self.request(endpoint, Some(headers), None).await
105+
debug!("Downloading id={id} from {url}");
106+
self.client
107+
.get(url)
108+
.query(&[("id", &id.to_string())])
109+
.send()
110+
.await
131111
}
132112

133113
/// Create a new client for the given mock server

src/download.rs

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
use axum::body::Body;
2+
use axum::extract::{Path, State};
3+
use axum::http::{HeaderMap, StatusCode};
4+
use serde_json::{Value, json};
5+
use tracing::{error, info};
6+
7+
use crate::clients::TiledClient;
8+
9+
const FORWARDED_HEADERS: [&str; 4] = [
10+
"content-disposition",
11+
"content-type",
12+
"content-length",
13+
"last-modified",
14+
];
15+
16+
pub async fn download(
17+
State(client): State<TiledClient>,
18+
Path((run, stream, det, id)): Path<(String, String, String, u32)>,
19+
) -> (StatusCode, HeaderMap, Body) {
20+
info!("Downloading {run}/{stream}/{det}/{id}");
21+
let req = client.download(run, stream, det, id).await;
22+
forward_download_response(req).await
23+
}
24+
25+
async fn forward_download_response(
26+
response: Result<reqwest::Response, reqwest::Error>,
27+
) -> (StatusCode, HeaderMap, Body) {
28+
match response {
29+
Ok(mut resp) => match resp.status().as_u16() {
30+
200..300 => {
31+
let mut headers = HeaderMap::new();
32+
for key in FORWARDED_HEADERS {
33+
if let Some(value) = resp.headers_mut().remove(key) {
34+
headers.insert(key, value);
35+
}
36+
}
37+
let stream = Body::from_stream(resp.bytes_stream());
38+
(StatusCode::OK, headers, stream)
39+
},
40+
400..500 => (
41+
// Probably permission error or non-existent file - forward error to client
42+
resp.status(),
43+
HeaderMap::new(),
44+
Body::from_stream(resp.bytes_stream())
45+
),
46+
100..200 | // ??? check tiled?
47+
300..400 | // should have followed a redirect
48+
0..100 | (600..) | // who needs standards anyway
49+
500..600 => {
50+
let status = resp.status().as_u16();
51+
let content = resp.text().await.unwrap_or_else(|e| format!("Unable to read error response: {e}"));
52+
(
53+
// Whatever we got back, it wasn't what we expected so blame it on tiled
54+
StatusCode::SERVICE_UNAVAILABLE,
55+
HeaderMap::new(),
56+
json!({
57+
"detail": "Unexpected response from tiled",
58+
"status": status,
59+
// Try to parse response as json before giving up and passing a string
60+
"response": serde_json::from_str(&content)
61+
.unwrap_or(Value::String(content))
62+
}).to_string().into()
63+
)
64+
}
65+
},
66+
Err(err) => {
67+
error!("Error sending request to tiled: {err}");
68+
let (status, message) = if err.is_connect() {
69+
(
70+
StatusCode::SERVICE_UNAVAILABLE,
71+
"Could not connect to tiled",
72+
)
73+
} else {
74+
(
75+
StatusCode::INTERNAL_SERVER_ERROR,
76+
"Error making request to tiled",
77+
)
78+
};
79+
80+
(status, HeaderMap::new(), message.into())
81+
}
82+
}
83+
}

src/main.rs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
use std::error;
2-
31
use async_graphql::{EmptyMutation, EmptySubscription, Schema};
42
use axum::http::StatusCode;
53
use axum::response::{Html, IntoResponse};
@@ -9,6 +7,7 @@ use axum::{Extension, Router};
97
mod cli;
108
mod clients;
119
mod config;
10+
mod download;
1211
mod handlers;
1312
mod model;
1413
#[cfg(test)]
@@ -25,7 +24,7 @@ use crate::handlers::{graphiql_handler, graphql_handler};
2524
use crate::model::TiledQuery;
2625

2726
#[tokio::main]
28-
async fn main() -> Result<(), Box<dyn error::Error>> {
27+
async fn main() -> Result<(), Box<dyn std::error::Error>> {
2928
let subscriber = tracing_subscriber::FmtSubscriber::new();
3029
tracing::subscriber::set_global_default(subscriber)?;
3130

@@ -45,14 +44,18 @@ async fn main() -> Result<(), Box<dyn error::Error>> {
4544
}
4645
}
4746

48-
async fn serve(config: GlazedConfig) -> Result<(), Box<dyn error::Error>> {
47+
async fn serve(config: GlazedConfig) -> Result<(), Box<dyn std::error::Error>> {
48+
let client = TiledClient::new(config.tiled_client.address);
4949
let schema = Schema::build(TiledQuery, EmptyMutation, EmptySubscription)
50-
.data(TiledClient::new(config.tiled_client.address))
50+
.data(config.bind_address)
51+
.data(client.clone())
5152
.finish();
5253

5354
let app = Router::new()
5455
.route("/graphql", post(graphql_handler).get(graphql_get_warning))
5556
.route("/graphiql", get(graphiql_handler))
57+
.route("/asset/{run}/{stream}/{det}/{id}", get(download::download))
58+
.with_state(client)
5659
.fallback((
5760
StatusCode::NOT_FOUND,
5861
Html(include_str!("../static/404.html")),

0 commit comments

Comments
 (0)