use crate::Result;
use futures::{Stream, StreamExt};
use serde_json::Value;
use stac::api::{ItemCollection, Search};
use std::{future::Future, io::Write, pin::Pin};
pub type ItemStream = Pin<Box<dyn Stream<Item = Result<Value>> + Send>>;
pub type Finalize = Box<
dyn FnOnce(
Option<Value>,
Option<Value>,
u64,
) -> Pin<Box<dyn Future<Output = Result<ItemCollection>> + Send>>
+ Send,
>;
pub struct StreamedSearch {
pub items: ItemStream,
pub finalize: Finalize,
}
pub trait StreamSearch: Send + Sync {
fn stream_search(
&self,
search: Search,
max_items: Option<usize>,
context: bool,
self_href: Option<String>,
) -> impl Future<Output = Result<StreamedSearch>> + Send;
fn write_search<W: Write>(
&self,
search: Search,
max_items: Option<usize>,
context: bool,
self_href: Option<String>,
writer: W,
pretty: bool,
) -> impl Future<Output = Result<u64>> {
async move {
let StreamedSearch { items, finalize } = self
.stream_search(search, max_items, context, self_href)
.await?;
write_item_collection(writer, items, pretty, finalize).await
}
}
}
pub async fn write_item_collection<W, S, F, Fut>(
mut writer: W,
items: S,
pretty: bool,
finalize: F,
) -> Result<u64>
where
W: Write,
S: Stream<Item = Result<Value>>,
F: FnOnce(Option<Value>, Option<Value>, u64) -> Fut,
Fut: Future<Output = Result<ItemCollection>>,
{
writer.write_all(if pretty {
b"{\n \"type\": \"FeatureCollection\",\n \"features\": ["
} else {
b"{\"type\":\"FeatureCollection\",\"features\":["
})?;
futures::pin_mut!(items);
let mut first: Option<Value> = None;
let mut pending: Option<Value> = None;
let mut count: u64 = 0;
while let Some(item) = items.next().await {
let item = item?;
if let Some(previous) = pending.take() {
write_element(&mut writer, &previous, count, pretty)?;
count += 1;
} else {
first = Some(item.clone());
}
pending = Some(item);
}
if let Some(last) = &pending {
write_element(&mut writer, last, count, pretty)?;
count += 1;
}
writer.write_all(if pretty && count > 0 { b"\n ]" } else { b"]" })?;
let mut collection = finalize(first, pending, count).await?;
collection.number_returned = Some(count);
let value = serde_json::to_value(&collection)?;
let members: serde_json::Map<String, Value> = value
.as_object()
.expect("an ItemCollection serializes to a JSON object")
.iter()
.filter(|(key, _)| key.as_str() != "type" && key.as_str() != "features")
.map(|(key, value)| (key.clone(), value.clone()))
.collect();
if !members.is_empty() {
let object = if pretty {
serde_json::to_string_pretty(&Value::Object(members))?
} else {
serde_json::to_string(&Value::Object(members))?
};
let inner = object
.strip_prefix('{')
.and_then(|rest| rest.strip_suffix('}'))
.expect("serde_json serializes an object with braces");
writer.write_all(b",")?;
writer.write_all(inner.trim_end().as_bytes())?;
}
writer.write_all(if pretty { b"\n}" } else { b"}" })?;
Ok(count)
}
fn write_element<W: Write>(writer: &mut W, item: &Value, index: u64, pretty: bool) -> Result<()> {
if pretty {
writer.write_all(if index == 0 { b"\n" } else { b",\n" })?;
let element = serde_json::to_string_pretty(item)?;
for (line_index, line) in element.lines().enumerate() {
if line_index > 0 {
writer.write_all(b"\n")?;
}
writer.write_all(b" ")?;
writer.write_all(line.as_bytes())?;
}
} else {
if index > 0 {
writer.write_all(b",")?;
}
serde_json::to_writer(&mut *writer, item)?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::write_item_collection;
use futures::stream;
use serde_json::Value;
use stac::{Item, Link, api::ItemCollection};
fn items(n: usize) -> (Vec<Value>, Vec<stac::api::Item>) {
let api: Vec<stac::api::Item> = (0..n)
.map(|i| Item::new(format!("item-{i}")).try_into().unwrap())
.collect();
let values = api
.iter()
.map(|i| serde_json::to_value(i).unwrap())
.collect();
(values, api)
}
async fn run(
values: Vec<Value>,
links: Vec<Link>,
matched: Option<u64>,
pretty: bool,
) -> Vec<u8> {
let footer_links = links;
let mut buf = Vec::new();
write_item_collection(
&mut buf,
stream::iter(values.into_iter().map(Ok)),
pretty,
|_first, _last, _count| async move {
let mut collection = ItemCollection::new(Vec::<stac::api::Item>::new()).unwrap();
collection.links = footer_links;
collection.number_matched = matched;
Ok(collection)
},
)
.await
.unwrap();
buf
}
#[tokio::test]
async fn byte_identical_to_buffered() {
let links = vec![Link::new("http://example.com/next?token=abc", "next")];
for n in [0usize, 1, 2, 5] {
for pretty in [false, true] {
let matched = Some(n as u64 + 100);
let (values, api) = items(n);
let got = run(values, links.clone(), matched, pretty).await;
let mut want_ic = ItemCollection::new(api).unwrap();
want_ic.links = links.clone();
want_ic.number_matched = matched;
let want = if pretty {
serde_json::to_vec_pretty(&want_ic).unwrap()
} else {
serde_json::to_vec(&want_ic).unwrap()
};
assert_eq!(
String::from_utf8(got).unwrap(),
String::from_utf8(want).unwrap(),
"n={n} pretty={pretty}"
);
}
}
}
}