#![deny(
elided_lifetimes_in_paths,
explicit_outlives_requirements,
keyword_idents,
macro_use_extern_crate,
meta_variable_misuse,
missing_abi,
missing_debug_implementations,
non_ascii_idents,
noop_method_call,
rust_2021_incompatible_closure_captures,
rust_2021_incompatible_or_patterns,
rust_2021_prefixes_incompatible_syntax,
rust_2021_prelude_collisions,
single_use_lifetimes,
trivial_casts,
trivial_numeric_casts,
unreachable_pub,
unsafe_code,
unsafe_op_in_unsafe_fn,
unused_crate_dependencies,
unused_extern_crates,
unused_import_braces,
unused_lifetimes,
unused_qualifications,
unused_results
)]
#![warn(missing_docs)]
mod client;
mod page;
pub use client::Client;
pub use page::Page;
use serde::{Serialize, de::DeserializeOwned};
use stac::api::{ItemCollection, Search};
use tokio_postgres::{GenericClient, NoTls, Row, types::ToSql};
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
#[error(transparent)]
SerdeJson(#[from] serde_json::Error),
#[error(transparent)]
Stac(#[from] stac::Error),
#[error(transparent)]
TokioPostgres(#[from] tokio_postgres::Error),
#[error(transparent)]
TryFromInt(#[from] std::num::TryFromIntError),
}
pub type Result<T> = std::result::Result<T, Error>;
pub type JsonValue = serde_json::Value;
pub async fn search(
connection_string: &str,
mut search: Search,
max_items: Option<usize>,
) -> Result<ItemCollection> {
let (client, connection) = tokio_postgres::connect(connection_string, NoTls).await?;
let task = tokio::spawn(async move {
if let Err(e) = connection.await {
tracing::error!("pgstac connection error: {}", e);
}
});
let mut all_items = if let Some(max_items) = max_items {
if max_items == 0 {
return Ok(ItemCollection::new(Vec::new())?);
}
Vec::with_capacity(max_items)
} else {
Vec::new()
};
if search.items.limit.is_none()
&& let Some(max_items) = max_items
{
search.items.limit = Some(max_items.try_into()?);
}
loop {
tracing::info!("Fetching page");
let page = client.search(search.clone()).await?;
let next_token = page.next_token();
let has_next_token = next_token.is_some();
if let Some(token) = next_token {
let _ = search
.additional_fields
.insert("token".into(), token.into());
}
for item in page.features {
all_items.push(item);
if let Some(max_items) = max_items
&& all_items.len() >= max_items
{
break;
}
}
let should_continue = if let Some(max_items) = max_items {
all_items.len() < max_items && has_next_token
} else {
has_next_token
};
if !should_continue {
break;
}
tracing::debug!("Found {} item(s), continuing...", all_items.len());
}
drop(task);
Ok(ItemCollection::new(all_items)?)
}
#[allow(async_fn_in_trait)]
pub trait Pgstac: GenericClient {
async fn pgstac_version(&self) -> Result<String> {
self.pgstac_string("get_version", &[]).await
}
async fn readonly(&self) -> Result<bool> {
self.pgstac_bool("readonly", &[]).await
}
async fn context(&self) -> Result<bool> {
self.pgstac_string("get_setting", &[&"context"])
.await
.map(|value| value == "on")
}
async fn set_pgstac_setting(&self, key: &str, value: &str) -> Result<()> {
self.execute(
"INSERT INTO pgstac_settings (name, value) VALUES ($1, $2) ON CONFLICT ON CONSTRAINT pgstac_settings_pkey DO UPDATE SET value = excluded.value;",
&[&key, &value],
).await.map(|_| ()).map_err(Error::from)
}
async fn collections(&self) -> Result<Vec<JsonValue>> {
self.pgstac_vec("all_collections", &[]).await
}
async fn collection(&self, id: &str) -> Result<Option<JsonValue>> {
self.pgstac_opt("get_collection", &[&id]).await
}
async fn add_collection<T>(&self, collection: T) -> Result<()>
where
T: Serialize,
{
let collection = serde_json::to_value(collection)?;
self.pgstac_void("create_collection", &[&collection]).await
}
async fn upsert_collection<T>(&self, collection: T) -> Result<()>
where
T: Serialize,
{
let collection = serde_json::to_value(collection)?;
self.pgstac_void("upsert_collection", &[&collection]).await
}
async fn update_collection_extents(&self) -> Result<()> {
self.pgstac_void("update_collection_extents", &[]).await
}
async fn update_collection<T>(&self, collection: T) -> Result<()>
where
T: Serialize,
{
let collection = serde_json::to_value(collection)?;
self.pgstac_void("update_collection", &[&collection]).await
}
async fn delete_collection(&self, id: &str) -> Result<()> {
self.pgstac_void("delete_collection", &[&id]).await
}
async fn item(&self, id: &str, collection: Option<&str>) -> Result<Option<JsonValue>> {
self.pgstac_opt("get_item", &[&id, &collection]).await
}
async fn add_item<T>(&self, item: T) -> Result<()>
where
T: Serialize,
{
let item = serde_json::to_value(item)?;
self.pgstac_void("create_item", &[&item]).await
}
async fn add_items<T>(&self, items: &[T]) -> Result<()>
where
T: Serialize,
{
let items = serde_json::to_value(items)?;
self.pgstac_void("create_items", &[&items]).await
}
async fn update_item<T>(&self, item: T) -> Result<()>
where
T: Serialize,
{
let item = serde_json::to_value(item)?;
self.pgstac_void("update_item", &[&item]).await
}
async fn upsert_item<T>(&self, item: T) -> Result<()>
where
T: Serialize,
{
let item = serde_json::to_value(item)?;
self.pgstac_void("upsert_item", &[&item]).await
}
async fn upsert_items<T>(&self, items: &[T]) -> Result<()>
where
T: Serialize,
{
let items = serde_json::to_value(items)?;
self.pgstac_void("upsert_items", &[&items]).await
}
async fn delete_item(&self, id: &str, collection: Option<&str>) -> Result<()> {
self.pgstac_void("delete_item", &[&id, &collection]).await
}
async fn search(&self, search: Search) -> Result<Page> {
let search = search.into_cql2_json()?;
let search = serde_json::to_value(search)?;
self.pgstac_value("search", &[&search]).await
}
async fn pgstac(
&self,
function: &str,
params: &[&(dyn ToSql + Sync)],
) -> std::result::Result<Row, tokio_postgres::Error> {
let param_string = (0..params.len())
.map(|i| format!("${}", i + 1))
.collect::<Vec<_>>()
.join(", ");
let query = format!("SELECT * from pgstac.{function}({param_string})");
self.query_one(&query, params).await
}
async fn pgstac_string(
&self,
function: &str,
params: &[&(dyn ToSql + Sync)],
) -> Result<String> {
let row = self.pgstac(function, params).await?;
row.try_get(function).map_err(Error::from)
}
async fn pgstac_bool(&self, function: &str, params: &[&(dyn ToSql + Sync)]) -> Result<bool> {
let row = self.pgstac(function, params).await?;
row.try_get(function).map_err(Error::from)
}
async fn pgstac_vec<T>(&self, function: &str, params: &[&(dyn ToSql + Sync)]) -> Result<Vec<T>>
where
T: DeserializeOwned,
{
if let Some(value) = self.pgstac_opt(function, params).await? {
Ok(value)
} else {
Ok(Vec::new())
}
}
async fn pgstac_opt<T>(
&self,
function: &str,
params: &[&(dyn ToSql + Sync)],
) -> Result<Option<T>>
where
T: DeserializeOwned,
{
let row = self.pgstac(function, params).await?;
let option: Option<JsonValue> = row.try_get(function)?;
let option = option.map(|v| serde_json::from_value(v)).transpose()?;
Ok(option)
}
async fn pgstac_value<T>(&self, function: &str, params: &[&(dyn ToSql + Sync)]) -> Result<T>
where
T: DeserializeOwned,
{
let row = self.pgstac(function, params).await?;
let value = row.try_get(function)?;
serde_json::from_value(value).map_err(Error::from)
}
async fn pgstac_void(&self, function: &str, params: &[&(dyn ToSql + Sync)]) -> Result<()> {
let _ = self.pgstac(function, params).await?;
Ok(())
}
}
impl<T> Pgstac for T where T: GenericClient {}
#[cfg(test)]
pub(crate) mod tests {
use super::Pgstac;
use geojson::Geometry;
use rstest::{fixture, rstest};
use serde_json::{Map, json};
use stac::api::{Fields, Filter, Search, Sortby};
use stac::{Collection, Item};
use std::{
ops::Deref,
sync::{LazyLock, atomic::AtomicU16},
};
use tokio::sync::Mutex;
use tokio_postgres::{Client, Config, NoTls};
use tokio_test as _;
static MUTEX: LazyLock<Mutex<()>> = LazyLock::new(|| Mutex::new(()));
struct TestClient {
client: Client,
config: Config,
dbname: String,
}
pub(crate) fn config() -> Config {
std::env::var("PGSTAC_RS_TEST_DB")
.unwrap_or("postgresql://username:password@localhost:5432/postgis".to_string())
.parse()
.unwrap()
}
impl TestClient {
async fn new(id: u16) -> TestClient {
let dbname = format!("pgstac_test_{id}");
let config = config();
{
let _mutex = MUTEX.lock().await;
let (client, connection) = config.connect(NoTls).await.unwrap();
let _handle = tokio::spawn(async move { connection.await.unwrap() });
let _ = client
.execute(
&format!(
"CREATE DATABASE {} TEMPLATE {}",
dbname,
config.get_dbname().unwrap()
),
&[],
)
.await
.unwrap();
}
let mut test_config = config.clone();
let (client, connection) = test_config.dbname(&dbname).connect(NoTls).await.unwrap();
let _handle = tokio::spawn(async move { connection.await.unwrap() });
TestClient {
client,
config,
dbname,
}
}
async fn terminate(&mut self) {
let (client, connection) = self.config.connect(NoTls).await.unwrap();
let _handle = tokio::spawn(async move { connection.await.unwrap() });
let _ = client
.execute(
"SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = $1",
&[&self.dbname],
)
.await
.unwrap();
let _ = client
.execute(&format!("DROP DATABASE {}", self.dbname), &[])
.await
.unwrap();
}
}
impl Drop for TestClient {
fn drop(&mut self) {
std::thread::scope(|s| {
let _ = s.spawn(|| {
let runtime = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.unwrap();
runtime.block_on(self.terminate());
});
});
}
}
impl Deref for TestClient {
type Target = Client;
fn deref(&self) -> &Self::Target {
&self.client
}
}
fn longmont() -> Geometry {
Geometry::new_point(vec![-105.1019, 40.1672])
}
#[fixture]
fn id() -> u16 {
static COUNTER: AtomicU16 = AtomicU16::new(0);
COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
}
#[fixture]
async fn client(id: u16) -> TestClient {
TestClient::new(id).await
}
#[rstest]
#[tokio::test]
async fn pgstac_version(#[future(awt)] client: TestClient) {
let _ = client.pgstac_version().await.unwrap();
}
#[rstest]
#[tokio::test]
async fn readonly(#[future(awt)] client: TestClient) {
assert!(!client.readonly().await.unwrap());
}
#[rstest]
#[tokio::test]
async fn context(#[future(awt)] client: TestClient) {
assert!(!client.context().await.unwrap());
}
#[rstest]
#[tokio::test]
async fn set_context(#[future(awt)] client: TestClient) {
client.set_pgstac_setting("context", "on").await.unwrap();
assert!(client.context().await.unwrap());
}
#[rstest]
#[tokio::test]
async fn collections(#[future(awt)] client: TestClient) {
assert!(client.collections().await.unwrap().is_empty());
client
.add_collection(Collection::new("an-id", "a description"))
.await
.unwrap();
assert_eq!(client.collections().await.unwrap().len(), 1);
}
#[rstest]
#[tokio::test]
async fn add_collection_duplicate(#[future(awt)] client: TestClient) {
assert!(client.collections().await.unwrap().is_empty());
let collection = Collection::new("an-id", "a description");
client.add_collection(collection.clone()).await.unwrap();
assert!(client.add_collection(collection).await.is_err());
}
#[rstest]
#[tokio::test]
async fn upsert_collection(#[future(awt)] client: TestClient) {
assert!(client.collections().await.unwrap().is_empty());
let mut collection = Collection::new("an-id", "a description");
client.upsert_collection(collection.clone()).await.unwrap();
collection.title = Some("a title".to_string());
client.upsert_collection(collection).await.unwrap();
assert_eq!(
client.collection("an-id").await.unwrap().unwrap()["title"],
"a title"
);
}
#[rstest]
#[tokio::test]
async fn update_collection(#[future(awt)] client: TestClient) {
let mut collection = Collection::new("an-id", "a description");
client.add_collection(collection.clone()).await.unwrap();
assert!(
client
.collection("an-id")
.await
.unwrap()
.unwrap()
.get("title")
.is_none()
);
collection.title = Some("a title".to_string());
client.update_collection(collection).await.unwrap();
assert_eq!(client.collections().await.unwrap().len(), 1);
assert_eq!(
client.collection("an-id").await.unwrap().unwrap()["title"],
"a title"
);
}
#[rstest]
#[tokio::test]
async fn update_collection_does_not_exit(#[future(awt)] client: TestClient) {
let collection = Collection::new("an-id", "a description");
assert!(client.update_collection(collection).await.is_err());
}
#[rstest]
#[tokio::test]
async fn collection_not_found(#[future(awt)] client: TestClient) {
assert!(client.collection("not-an-id").await.unwrap().is_none());
}
#[rstest]
#[tokio::test]
async fn delete_collection(#[future(awt)] client: TestClient) {
let collection = Collection::new("an-id", "a description");
client.add_collection(collection.clone()).await.unwrap();
assert!(client.collection("an-id").await.unwrap().is_some());
client.delete_collection("an-id").await.unwrap();
assert!(client.collection("an-id").await.unwrap().is_none());
}
#[rstest]
#[tokio::test]
async fn delete_collection_does_not_exist(#[future(awt)] client: TestClient) {
assert!(client.delete_collection("not-an-id").await.is_err());
}
#[rstest]
#[tokio::test]
async fn item(#[future(awt)] client: TestClient) {
assert!(
client
.item("an-id", Some("collection-id"))
.await
.unwrap()
.is_none()
);
let collection = Collection::new("collection-id", "a description");
client.add_collection(collection).await.unwrap();
let mut item = Item::new("an-id");
item.collection = Some("collection-id".to_string());
item.geometry = Some(longmont());
let _ = item
.additional_fields
.insert("type".into(), "Feature".into());
client.add_item(item.clone()).await.unwrap();
assert_eq!(
client
.item("an-id", Some("collection-id"))
.await
.unwrap()
.unwrap(),
serde_json::to_value(item).unwrap(),
);
client.update_collection_extents().await.unwrap();
}
#[rstest]
#[tokio::test]
async fn item_without_collection(#[future(awt)] client: TestClient) {
let item = Item::new("an-id");
assert!(client.add_item(item.clone()).await.is_err());
}
#[rstest]
#[tokio::test]
async fn update_item(#[future(awt)] client: TestClient) {
let collection = Collection::new("collection-id", "a description");
client.add_collection(collection).await.unwrap();
let mut item = Item::new("an-id");
item.collection = Some("collection-id".to_string());
item.geometry = Some(longmont());
client.add_item(item.clone()).await.unwrap();
let _ = item
.properties
.additional_fields
.insert("foo".into(), "bar".into());
client.update_item(item).await.unwrap();
assert_eq!(
client
.item("an-id", Some("collection-id"))
.await
.unwrap()
.unwrap()["properties"]["foo"],
"bar"
);
}
#[rstest]
#[tokio::test]
async fn delete_item(#[future(awt)] client: TestClient) {
let collection = Collection::new("collection-id", "a description");
client.add_collection(collection).await.unwrap();
let mut item = Item::new("an-id");
item.collection = Some("collection-id".to_string());
item.geometry = Some(longmont());
client.add_item(item.clone()).await.unwrap();
client
.delete_item(&item.id, Some("collection-id"))
.await
.unwrap();
assert_eq!(
client.item("an-id", Some("collection-id")).await.unwrap(),
None,
);
}
#[rstest]
#[tokio::test]
async fn upsert_item(#[future(awt)] client: TestClient) {
let collection = Collection::new("collection-id", "a description");
client.add_collection(collection).await.unwrap();
let mut item = Item::new("an-id");
item.collection = Some("collection-id".to_string());
item.geometry = Some(longmont());
client.upsert_item(item.clone()).await.unwrap();
client.upsert_item(item).await.unwrap();
}
#[rstest]
#[tokio::test]
async fn add_items(#[future(awt)] client: TestClient) {
let collection = Collection::new("collection-id", "a description");
client.add_collection(collection).await.unwrap();
let mut item = Item::new("an-id");
item.collection = Some("collection-id".to_string());
item.geometry = Some(longmont());
let mut other_item = item.clone();
other_item.id = "other-id".to_string();
client.add_items(&[item, other_item]).await.unwrap();
assert!(
client
.item("an-id", Some("collection-id"))
.await
.unwrap()
.is_some()
);
assert!(
client
.item("other-id", Some("collection-id"))
.await
.unwrap()
.is_some()
);
}
#[rstest]
#[tokio::test]
async fn upsert_items(#[future(awt)] client: TestClient) {
let collection = Collection::new("collection-id", "a description");
client.add_collection(collection).await.unwrap();
let mut item = Item::new("an-id");
item.collection = Some("collection-id".to_string());
item.geometry = Some(longmont());
let mut other_item = item.clone();
other_item.id = "other-id".to_string();
let items = vec![item, other_item];
client.upsert_items(&items).await.unwrap();
client.upsert_items(&items).await.unwrap();
}
#[rstest]
#[tokio::test]
async fn search_everything(#[future(awt)] client: TestClient) {
assert!(
client
.search(Search::default())
.await
.unwrap()
.features
.is_empty()
);
let collection = Collection::new("collection-id", "a description");
client.add_collection(collection).await.unwrap();
let mut item = Item::new("an-id");
item.collection = Some("collection-id".to_string());
item.geometry = Some(longmont());
client.add_item(item.clone()).await.unwrap();
assert_eq!(
client.search(Search::default()).await.unwrap().features[0],
*serde_json::to_value(item).unwrap().as_object().unwrap()
);
}
#[rstest]
#[tokio::test]
async fn search_ids(#[future(awt)] client: TestClient) {
let collection = Collection::new("collection-id", "a description");
client.add_collection(collection).await.unwrap();
let mut item = Item::new("an-id");
item.collection = Some("collection-id".to_string());
item.geometry = Some(longmont());
client.add_item(item.clone()).await.unwrap();
let search = Search {
ids: vec!["an-id".to_string()],
..Default::default()
};
assert_eq!(client.search(search).await.unwrap().features.len(), 1);
let search = Search {
ids: vec!["not-an-id".to_string()],
..Default::default()
};
assert!(client.search(search).await.unwrap().features.is_empty());
}
#[rstest]
#[tokio::test]
async fn search_collections(#[future(awt)] client: TestClient) {
let collection = Collection::new("collection-id", "a description");
client.add_collection(collection).await.unwrap();
let mut item = Item::new("an-id");
item.collection = Some("collection-id".to_string());
item.geometry = Some(longmont());
client.add_item(item.clone()).await.unwrap();
let search = Search {
collections: vec!["collection-id".to_string()],
..Default::default()
};
assert_eq!(client.search(search).await.unwrap().features.len(), 1);
let search = Search {
collections: vec!["not-an-id".to_string()],
..Default::default()
};
assert!(client.search(search).await.unwrap().features.is_empty());
}
#[rstest]
#[tokio::test]
async fn search_limit(#[future(awt)] client: TestClient) {
let collection = Collection::new("collection-id", "a description");
client.add_collection(collection).await.unwrap();
let mut item = Item::new("an-id");
item.collection = Some("collection-id".to_string());
item.geometry = Some(longmont());
client.add_item(item.clone()).await.unwrap();
item.id = "another-id".to_string();
client.add_item(item).await.unwrap();
let mut search = Search::default();
search.items.limit = Some(1);
let page = client.search(search).await.unwrap();
assert_eq!(page.features.len(), 1);
if let Some(context) = page.context {
assert_eq!(context.limit.unwrap(), 1);
} else {
assert_eq!(page.number_returned.unwrap(), 1);
}
}
#[rstest]
#[tokio::test]
async fn search_bbox(#[future(awt)] client: TestClient) {
let collection = Collection::new("collection-id", "a description");
client.add_collection(collection).await.unwrap();
let mut item = Item::new("an-id");
item.collection = Some("collection-id".to_string());
item.geometry = Some(longmont());
client.add_item(item.clone()).await.unwrap();
let mut search = Search::default();
search.items.bbox = Some(vec![-106., 40., -105., 41.].try_into().unwrap());
assert_eq!(
client.search(search.clone()).await.unwrap().features.len(),
1
);
search.items.bbox = Some(vec![-106., 41., -105., 42.].try_into().unwrap());
assert!(client.search(search).await.unwrap().features.is_empty());
}
#[rstest]
#[tokio::test]
async fn search_datetime(#[future(awt)] client: TestClient) {
let collection = Collection::new("collection-id", "a description");
client.add_collection(collection).await.unwrap();
let mut item = Item::new("an-id");
item.collection = Some("collection-id".to_string());
item.geometry = Some(longmont());
item.properties.datetime = Some("2023-01-07T00:00:00Z".parse().unwrap());
client.add_item(item.clone()).await.unwrap();
let mut search = Search::default();
search.items.datetime = Some("2023-01-07T00:00:00Z".to_string());
assert_eq!(
client.search(search.clone()).await.unwrap().features.len(),
1
);
search.items.datetime = Some("2023-01-08T00:00:00Z".to_string());
assert!(client.search(search).await.unwrap().features.is_empty());
}
#[rstest]
#[tokio::test]
async fn search_intersects(#[future(awt)] client: TestClient) {
let collection = Collection::new("collection-id", "a description");
client.add_collection(collection).await.unwrap();
let mut item = Item::new("an-id");
item.collection = Some("collection-id".to_string());
item.geometry = Some(longmont());
client.add_item(item.clone()).await.unwrap();
let search = Search {
intersects: Some(
serde_json::from_value(
serde_json::to_value(Geometry::new_polygon(vec![vec![
vec![-106., 40.],
vec![-106., 41.],
vec![-105., 41.],
vec![-105., 40.],
vec![-106., 40.],
]]))
.unwrap(),
)
.unwrap(),
),
..Default::default()
};
assert_eq!(client.search(search).await.unwrap().features.len(), 1);
let search = Search {
intersects: Some(
serde_json::from_value(
serde_json::to_value(Geometry::new_polygon(vec![vec![
vec![-104., 40.],
vec![-104., 41.],
vec![-103., 41.],
vec![-103., 40.],
vec![-104., 40.],
]]))
.unwrap(),
)
.unwrap(),
),
..Default::default()
};
assert!(client.search(search).await.unwrap().features.is_empty());
}
#[rstest]
#[tokio::test]
async fn pagination(#[future(awt)] client: TestClient) {
let collection = Collection::new("collection-id", "a description");
client.add_collection(collection).await.unwrap();
let mut item = Item::new("an-id");
item.collection = Some("collection-id".to_string());
item.properties.datetime = Some("2023-01-08T00:00:00Z".parse().unwrap());
item.geometry = Some(longmont());
client.add_item(item.clone()).await.unwrap();
item.id = "another-id".to_string();
item.properties.datetime = Some("2023-01-07T00:00:00Z".parse().unwrap());
client.add_item(item).await.unwrap();
let mut search = Search::default();
search.items.limit = Some(1);
let page = client.search(search.clone()).await.unwrap();
assert_eq!(page.features[0]["id"], "an-id");
let _ = search
.additional_fields
.insert("token".to_string(), "next:collection-id:an-id".into());
let page = client.search(search.clone()).await.unwrap();
assert_eq!(page.features[0]["id"], "another-id");
let _ = search
.additional_fields
.insert("token".to_string(), "prev:collection-id:another-id".into());
let page = client.search(search).await.unwrap();
assert_eq!(page.features[0]["id"], "an-id");
}
#[rstest]
#[tokio::test]
async fn base_url(#[future(awt)] client: TestClient) {
client
.set_pgstac_setting("base_url", "http://pgstac.test")
.await
.unwrap();
let collection = Collection::new("collection-id", "a description");
client.add_collection(collection).await.unwrap();
let mut item = Item::new("an-id");
item.collection = Some("collection-id".to_string());
item.properties.datetime = Some("2023-01-08T00:00:00Z".parse().unwrap());
item.geometry = Some(longmont());
client.add_item(item.clone()).await.unwrap();
item.id = "another-id".to_string();
item.properties.datetime = Some("2023-01-07T00:00:00Z".parse().unwrap());
client.add_item(item).await.unwrap();
let mut search = Search::default();
search.items.limit = Some(1);
let page = client.search(search.clone()).await.unwrap();
if client.pgstac_version().await.unwrap().starts_with("0.9") {
let _ = page.links.iter().find(|link| link.rel == "next").unwrap();
}
}
#[rstest]
#[tokio::test]
async fn fields(#[future(awt)] client: TestClient) {
let collection = Collection::new("collection-id", "a description");
client.add_collection(collection).await.unwrap();
let mut item = Item::new("an-id");
item.collection = Some("collection-id".to_string());
item.geometry = Some(longmont());
let _ = item
.properties
.additional_fields
.insert("foo".into(), 42.into());
let _ = item
.properties
.additional_fields
.insert("bar".into(), 43.into());
client.add_item(item).await.unwrap();
let mut search = Search::default();
search.items.fields = Some(Fields {
include: vec!["properties.foo".to_string()],
exclude: vec!["properties.bar".to_string()],
});
let page = client.search(search).await.unwrap();
let item = &page.features[0];
assert!(item["properties"].as_object().unwrap().get("foo").is_some());
assert!(item["properties"].as_object().unwrap().get("bar").is_none());
}
#[rstest]
#[tokio::test]
async fn sortby(#[future(awt)] client: TestClient) {
let collection = Collection::new("collection-id", "a description");
client.add_collection(collection).await.unwrap();
let mut item = Item::new("a");
item.collection = Some("collection-id".to_string());
item.geometry = Some(longmont());
client.add_item(item.clone()).await.unwrap();
item.id = "b".to_string();
client.add_item(item).await.unwrap();
let mut search = Search::default();
search.items.sortby = vec![Sortby::asc("id")];
let page = client.search(search.clone()).await.unwrap();
assert_eq!(page.features[0]["id"], "a");
assert_eq!(page.features[1]["id"], "b");
search.items.sortby = vec![Sortby::desc("id")];
let page = client.search(search).await.unwrap();
assert_eq!(page.features[0]["id"], "b");
assert_eq!(page.features[1]["id"], "a");
}
#[rstest]
#[tokio::test]
async fn filter(#[future(awt)] client: TestClient) {
let collection = Collection::new("collection-id", "a description");
client.add_collection(collection).await.unwrap();
let mut item = Item::new("a");
item.collection = Some("collection-id".to_string());
item.geometry = Some(longmont());
let _ = item
.properties
.additional_fields
.insert("foo".into(), 42.into());
client.add_item(item.clone()).await.unwrap();
item.id = "b".to_string();
let _ = item
.properties
.additional_fields
.insert("foo".into(), 43.into());
client.add_item(item).await.unwrap();
let mut filter = Map::new();
let _ = filter.insert("op".into(), "=".into());
let _ = filter.insert("args".into(), json!([{"property": "foo"}, 42]));
let mut search = Search::default();
search.items.filter = Some(Filter::Cql2Json(filter));
let page = client.search(search).await.unwrap();
assert_eq!(page.features.len(), 1);
}
#[rstest]
#[tokio::test]
async fn query(#[future(awt)] client: TestClient) {
let collection = Collection::new("collection-id", "a description");
client.add_collection(collection).await.unwrap();
let mut item = Item::new("a");
item.collection = Some("collection-id".to_string());
item.geometry = Some(longmont());
let _ = item
.properties
.additional_fields
.insert("foo".into(), 42.into());
client.add_item(item.clone()).await.unwrap();
item.id = "b".to_string();
let _ = item
.properties
.additional_fields
.insert("foo".into(), 43.into());
client.add_item(item).await.unwrap();
let mut query = Map::new();
let _ = query.insert("foo".into(), json!({"eq": 42}));
let mut search = Search::default();
search.items.query = Some(query);
let page = client.search(search).await.unwrap();
assert_eq!(page.features.len(), 1);
}
}