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