1use crate::{Api, Backend};
4use axum::{
5 Json, Router,
6 extract::{Path, Query, State, rejection::JsonRejection},
7 http::{HeaderValue, StatusCode, header::CONTENT_TYPE},
8 response::{Html, IntoResponse, Response},
9 routing::{get, post},
10};
11use bytes::{BufMut, BytesMut};
12use http::Method;
13use serde::Serialize;
14use stac::{
15 Collection, Item,
16 mime::{APPLICATION_GEOJSON, APPLICATION_OPENAPI_3_0},
17};
18use stac_api::{Collections, GetItems, GetSearch, ItemCollection, Items, Root, Search};
19use tower_http::{cors::CorsLayer, trace::TraceLayer};
20
21#[derive(Debug)]
23pub enum Error {
24 Server(crate::Error),
26
27 NotFound(String),
29
30 BadRequest(String),
32}
33
34type Result<T> = std::result::Result<T, Error>;
35
36#[derive(Debug)]
39pub struct GeoJson<T>(pub T);
40
41impl IntoResponse for Error {
42 fn into_response(self) -> Response {
43 match self {
44 Error::Server(error) => (StatusCode::INTERNAL_SERVER_ERROR, error.to_string()),
45 Error::NotFound(message) => (StatusCode::NOT_FOUND, message),
46 Error::BadRequest(message) => (StatusCode::BAD_REQUEST, message),
47 }
48 .into_response()
49 }
50}
51
52impl From<crate::Error> for Error {
53 fn from(error: crate::Error) -> Self {
54 Error::Server(error)
55 }
56}
57
58impl From<JsonRejection> for Error {
59 fn from(json_rejection: JsonRejection) -> Self {
60 Error::BadRequest(format!("bad request, json rejection: {json_rejection}"))
61 }
62}
63
64impl<T> IntoResponse for GeoJson<T>
65where
66 T: Serialize,
67{
68 fn into_response(self) -> Response {
69 let mut buf = BytesMut::with_capacity(128).writer();
72 match serde_json::to_writer(&mut buf, &self.0) {
73 Ok(()) => (
74 [(CONTENT_TYPE, HeaderValue::from_static(APPLICATION_GEOJSON))],
75 buf.into_inner().freeze(),
76 )
77 .into_response(),
78 Err(err) => (
79 StatusCode::INTERNAL_SERVER_ERROR,
80 [(
81 CONTENT_TYPE,
82 HeaderValue::from_static(mime::TEXT_PLAIN_UTF_8.as_ref()),
83 )],
84 err.to_string(),
85 )
86 .into_response(),
87 }
88 }
89}
90
91pub fn from_api<B: Backend>(api: Api<B>) -> Router {
102 Router::new()
103 .route("/", get(root))
104 .route("/api", get(service_desc))
105 .route("/api.html", get(service_doc))
106 .route("/conformance", get(conformance))
107 .route("/queryables", get(queryables))
108 .route("/collections", get(collections))
109 .route("/collections/{collection_id}", get(collection))
110 .route("/collections/{collection_id}/items", get(items))
111 .route("/collections/{collection_id}/items/{item_id}", get(item))
112 .route("/search", get(get_search))
113 .route("/search", post(post_search))
114 .layer(CorsLayer::permissive()) .layer(TraceLayer::new_for_http())
116 .with_state(api)
117}
118
119pub async fn root<B: Backend>(State(api): State<Api<B>>) -> Result<Json<Root>> {
122 api.root().await.map(Json).map_err(Error::from)
123}
124
125pub async fn service_desc() -> Response {
128 (
132 [(CONTENT_TYPE, APPLICATION_OPENAPI_3_0)],
133 include_str!("openapi.yaml"),
134 )
135 .into_response()
136}
137
138pub async fn service_doc() -> Response {
141 Html(include_str!("redoc.html")).into_response()
143}
144
145pub async fn conformance<B: Backend>(State(api): State<Api<B>>) -> Response {
148 Json(api.conformance()).into_response()
149}
150
151pub async fn queryables<B: Backend>(State(api): State<Api<B>>) -> Response {
153 (
154 [(CONTENT_TYPE, "application/schema+json")],
155 Json(api.queryables()),
156 )
157 .into_response()
158}
159
160pub async fn collections<B: Backend>(State(api): State<Api<B>>) -> Result<Json<Collections>> {
163 api.collections().await.map(Json).map_err(Error::from)
164}
165
166pub async fn collection<B: Backend>(
170 State(api): State<Api<B>>,
171 Path(collection_id): Path<String>,
172) -> Result<Json<Collection>> {
173 api.collection(&collection_id)
174 .await
175 .map_err(Error::from)
176 .and_then(|option| {
177 option
178 .ok_or_else(|| Error::NotFound(format!("no collection with id='{collection_id}'")))
179 })
180 .map(Json)
181}
182
183pub async fn items<B: Backend>(
187 State(api): State<Api<B>>,
188 Path(collection_id): Path<String>,
189 items: Query<GetItems>,
190) -> Result<GeoJson<ItemCollection>> {
191 let items = Items::try_from(items.0)
192 .and_then(Items::valid)
193 .map_err(|error| Error::BadRequest(format!("invalid query: {error}")))?;
194 api.items(&collection_id, items)
195 .await
196 .map_err(Error::from)
197 .and_then(|option| {
198 option
199 .ok_or_else(|| Error::NotFound(format!(" no collection with id='{collection_id}'")))
200 })
201 .map(GeoJson)
202}
203
204pub async fn item<B: Backend>(
208 State(api): State<Api<B>>,
209 Path((collection_id, item_id)): Path<(String, String)>,
210) -> Result<GeoJson<Item>> {
211 api.item(&collection_id, &item_id)
212 .await?
213 .ok_or_else(|| {
214 Error::NotFound(format!(
215 "no item with id='{item_id}' in collection='{collection_id}'"
216 ))
217 })
218 .map(GeoJson)
219}
220
221pub async fn get_search<B: Backend>(
224 State(api): State<Api<B>>,
225 search: Query<GetSearch>,
226) -> Result<GeoJson<ItemCollection>> {
227 tracing::debug!("GET /search: {:?}", search.0);
228 let search = Search::try_from(search.0)
229 .and_then(Search::valid)
230 .map_err(|error| Error::BadRequest(error.to_string()))?;
231
232 Ok(GeoJson(api.search(search, Method::GET).await?))
233}
234
235pub async fn post_search<B: Backend>(
238 State(api): State<Api<B>>,
239 search: std::result::Result<Json<Search>, JsonRejection>,
240) -> Result<GeoJson<ItemCollection>> {
241 let search = search?
242 .0
243 .valid()
244 .map_err(|error| Error::BadRequest(error.to_string()))?;
245 Ok(GeoJson(api.search(search, Method::POST).await?))
246}
247
248#[cfg(test)]
249mod tests {
250 use crate::{Api, Backend, MemoryBackend};
251 use axum::{
252 body::Body,
253 http::{Request, Response, StatusCode, header::CONTENT_TYPE},
254 };
255 use stac::{Collection, Item};
256 use tower::util::ServiceExt;
257
258 async fn get(backend: MemoryBackend, uri: &str) -> Response<Body> {
259 let router = super::from_api(
260 Api::new(backend, "http://stac.test/")
261 .unwrap()
262 .id("an-id")
263 .description("a description"),
264 );
265 router
266 .oneshot(Request::builder().uri(uri).body(Body::empty()).unwrap())
267 .await
268 .unwrap()
269 }
270
271 async fn post(backend: MemoryBackend, uri: &str) -> Response<Body> {
272 let router = super::from_api(
273 Api::new(backend, "http://stac.test/")
274 .unwrap()
275 .id("an-id")
276 .description("a description"),
277 );
278 router
279 .oneshot(
280 Request::builder()
281 .uri(uri)
282 .method("POST")
283 .header("Content-Type", "application/json")
284 .body("{}".to_string())
285 .unwrap(),
286 )
287 .await
288 .unwrap()
289 }
290
291 #[tokio::test]
292 async fn root() {
293 let response = get(MemoryBackend::new(), "/").await;
294 assert_eq!(response.status(), StatusCode::OK);
295 assert_eq!(
296 response.headers().get(CONTENT_TYPE).unwrap(),
297 "application/json"
298 );
299 }
300
301 #[tokio::test]
302 async fn service_description() {
303 let response = get(MemoryBackend::new(), "/api").await;
304 assert_eq!(response.status(), StatusCode::OK);
305 assert_eq!(
306 response.headers().get(CONTENT_TYPE).unwrap(),
307 "application/vnd.oai.openapi+json;version=3.0"
308 );
309 }
310
311 #[tokio::test]
312 async fn service_doc() {
313 let response = get(MemoryBackend::new(), "/api.html").await;
314 assert_eq!(response.status(), StatusCode::OK);
315 assert_eq!(
316 response.headers().get(CONTENT_TYPE).unwrap(),
317 "text/html; charset=utf-8"
318 );
319 }
320
321 #[tokio::test]
322 async fn conformance() {
323 let response = get(MemoryBackend::new(), "/conformance").await;
324 assert_eq!(response.status(), StatusCode::OK);
325 assert_eq!(
326 response.headers().get(CONTENT_TYPE).unwrap(),
327 "application/json"
328 );
329 }
330
331 #[tokio::test]
332 async fn collections() {
333 let response = get(MemoryBackend::new(), "/collections").await;
334 assert_eq!(response.status(), StatusCode::OK);
335 assert_eq!(
336 response.headers().get(CONTENT_TYPE).unwrap(),
337 "application/json"
338 );
339 }
340
341 #[tokio::test]
342 async fn collection() {
343 let response = get(MemoryBackend::new(), "/collections/an-id").await;
344 assert_eq!(response.status(), StatusCode::NOT_FOUND);
345 let mut backend = MemoryBackend::new();
346 backend
347 .add_collection(Collection::new("an-id", "A description"))
348 .await
349 .unwrap();
350 let response = get(backend, "/collections/an-id").await;
351 assert_eq!(response.status(), StatusCode::OK);
352 assert_eq!(
353 response.headers().get(CONTENT_TYPE).unwrap(),
354 "application/json"
355 );
356 }
357
358 #[tokio::test]
359 async fn items() {
360 let response = get(MemoryBackend::new(), "/collections/collection-id/items").await;
361 assert_eq!(response.status(), StatusCode::NOT_FOUND);
362
363 let mut backend = MemoryBackend::new();
364 backend
365 .add_collection(Collection::new("collection-id", "A description"))
366 .await
367 .unwrap();
368 backend
369 .add_item(Item::new("item-id").collection("collection-id"))
370 .await
371 .unwrap();
372 let response = get(backend, "/collections/collection-id/items").await;
373 assert_eq!(response.status(), StatusCode::OK);
374 assert_eq!(
375 response.headers().get(CONTENT_TYPE).unwrap(),
376 "application/geo+json"
377 );
378 }
379
380 #[tokio::test]
381 async fn item() {
382 let response = get(
383 MemoryBackend::new(),
384 "/collections/collection-id/items/item-id",
385 )
386 .await;
387 assert_eq!(response.status(), StatusCode::NOT_FOUND);
388
389 let mut backend = MemoryBackend::new();
390 backend
391 .add_collection(Collection::new("collection-id", "A description"))
392 .await
393 .unwrap();
394 backend
395 .add_item(Item::new("item-id").collection("collection-id"))
396 .await
397 .unwrap();
398 let response = get(backend, "/collections/collection-id/items/item-id").await;
399 assert_eq!(response.status(), StatusCode::OK);
400 assert_eq!(
401 response.headers().get(CONTENT_TYPE).unwrap(),
402 "application/geo+json"
403 );
404 }
405
406 #[tokio::test]
407 async fn get_search() {
408 let response = get(MemoryBackend::new(), "/search").await;
409 assert_eq!(response.status(), StatusCode::OK);
410 assert_eq!(
411 response.headers().get(CONTENT_TYPE).unwrap(),
412 "application/geo+json"
413 );
414 }
415
416 #[tokio::test]
417 async fn post_search() {
418 let response = post(MemoryBackend::new(), "/search").await;
419 assert_eq!(response.status(), StatusCode::OK);
420 assert_eq!(
421 response.headers().get(CONTENT_TYPE).unwrap(),
422 "application/geo+json"
423 );
424 }
425}