Skip to content

Commit b79a0d3

Browse files
authored
fix: deconstruct item collections when writing ndjson (#824)
Closes #823, cc @hrodmn ```sh $ cargo run -p rustac -- search https://stac.maap-project.org --collections icesat2-boreal-v3.1-agb --max-items 2 -o ndjson Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.19s Running `target/debug/rustac search 'https://stac.maap-project.org' --collections icesat2-boreal-v3.1-agb --max-items 2 -o ndjson` {"id":"boreal_agb_2020_202508201755714903_0003300","bbox":[-114.59879925054304,49.48077531986013,-112.85773713759698,50.60188181261153],"type":"Feature","links":[{"rel":"collection","type":"application/json","href":"https://stac.maap-project.org/collections/icesat2-boreal-v3.1-agb"},{"rel":"parent","type":"application/json","href":"https://stac.maap-project.org/collections/icesat2-boreal-v3.1-agb"},{"rel":"root","type":"application/json","href":"https://stac.maap-project.org/"},{"rel":"self","type":"application/geo+json","href":"https://stac.maap-project.org/collections/icesat2-boreal-v3.1-agb/items/boreal_agb_2020_202508201755714903_0003300"}],"assets":{"cog":{"gsd":30,"href":"s3://nasa-maap-data-store/file-staging/nasa-map/icesat2-boreal-v3.1/agb/0003300/boreal_agb_2020_202508201755714903_0003300.tif","type":"image/tiff; application=geotiff; profile=cloud-optimized","bands":[{"name":"mean_agbd","unit":"Mg ha-1","scale":1,"nodata":-9999.0,"offset":0,"sampling":"area","data_type":"float32","histogram":{"max":175.16749572753906,"min":0.6196765899658203,"count":11,"buckets":[7052664,441070,332270,294155,249099,195105,152856,51991,4914,174]},"statistics":{"mean":14.515077559481112,"stddev":27.601881177100985,"maximum":175.16749572753906,"minimum":0.6196765899658203,"valid_percent":97.4922},"spatial_resolution":30},{"name":"std_agbd","unit":"Mg ha-1","scale":1,"nodata":-9999.0,"offset":0,"sampling":"area","data_type":"float32","histogram":{"max":45.995460510253906,"min":0.112965427339077,"count":11,"buckets":[6914058,894421,634985,232124,73201,16131,5837,3247,282,12]},"statistics":{"mean":2.951711920429418,"stddev":4.408658052082873,"maximum":45.995460510253906,"minimum":0.112965427339077,"valid_percent":97.4922},"spatial_resolution":30}],"roles":["data"],"title":"Gridded predictions of aboveground biomass (Mg/ha)","description":"Gridded predictions of aboveground biomass (Mg/ha)","processing:level":"L4"},"training_data_parquet":{"href":"s3://nasa-maap-data-store/file-staging/nasa-map/icesat2-boreal-v3.1/agb/0003300/boreal_agb_2020_202508201755714903_0003300_train.parquet","type":"application/x-parquet","roles":["data"],"title":"Tabular training data","description":"Tabular training data with latitude, longitude, and biomass observations"}},"geometry":{"type":"Polygon","coordinates":[[[-114.59879925054304,50.15253728393152],[-113.90124100268127,49.48077531986013],[-112.85773713759698,49.92391646714159],[-113.54609166427896,50.60188181261153],[-114.59879925054304,50.15253728393152]]]},"collection":"icesat2-boreal-v3.1-agb","properties":{"datetime":"2020-07-01T23:59:59.500000Z","proj:bbox":[3968521.9999999953,3213304.0000000093,4058521.9999999953,3303304.0000000093],"proj:wkt2":"PROJCS[\"unnamed\",GEOGCS[\"GRS 1980(IUGG, 1980)\",DATUM[\"unknown\",SPHEROID[\"GRS80\",6378137,298.257222101],TOWGS84[0,0,0,0,0,0,0]],PRIMEM[\"Greenwich\",0],UNIT[\"degree\",0.0174532925199433,AUTHORITY[\"EPSG\",\"9122\"]]],PROJECTION[\"Albers_Conic_Equal_Area\"],PARAMETER[\"latitude_of_center\",40],PARAMETER[\"longitude_of_center\",180],PARAMETER[\"standard_parallel_1\",50],PARAMETER[\"standard_parallel_2\",70],PARAMETER[\"false_easting\",0],PARAMETER[\"false_northing\",0],UNIT[\"metre\",1,AUTHORITY[\"EPSG\",\"9001\"]],AXIS[\"Easting\",EAST],AXIS[\"Northing\",NORTH]]","proj:shape":[3000,3000],"end_datetime":"2020-12-31T23:59:59Z","proj:geometry":{"type":"Polygon","coordinates":[[[3968521.9999999953,3213304.0000000093],[4058521.9999999953,3213304.0000000093],[4058521.9999999953,3303304.0000000093],[3968521.9999999953,3303304.0000000093],[3968521.9999999953,3213304.0000000093]]]},"proj:transform":[30.0,0.0,3968521.9999999953,0.0,-30.0,3303304.0000000093,0.0,0.0,1.0],"start_datetime":"2020-01-01T00:00:00Z","created_datetime":"2025-08-20T00:00:00+00:00","icesat2-boreal:in_daac":true,"icesat2-boreal:tile_id":"0003300"},"stac_version":"1.1.0","stac_extensions":["https://stac-extensions.github.io/projection/v1.1.0/schema.json"]} {"id":"boreal_agb_2020_202508201755714861_0000544","bbox":[57.668175458260485,50.00125738393917,59.20217219488729,50.97761041743663],"type":"Feature","links":[{"rel":"collection","type":"application/json","href":"https://stac.maap-project.org/collections/icesat2-boreal-v3.1-agb"},{"rel":"parent","type":"application/json","href":"https://stac.maap-project.org/collections/icesat2-boreal-v3.1-agb"},{"rel":"root","type":"application/json","href":"https://stac.maap-project.org/"},{"rel":"self","type":"application/geo+json","href":"https://stac.maap-project.org/collections/icesat2-boreal-v3.1-agb/items/boreal_agb_2020_202508201755714861_0000544"}],"assets":{"cog":{"gsd":30,"href":"s3://nasa-maap-data-store/file-staging/nasa-map/icesat2-boreal-v3.1/agb/0000544/boreal_agb_2020_202508201755714861_0000544.tif","type":"image/tiff; application=geotiff; profile=cloud-optimized","bands":[{"name":"mean_agbd","unit":"Mg ha-1","scale":1,"nodata":-9999.0,"offset":0,"sampling":"area","data_type":"float32","histogram":{"max":176.0052032470703,"min":0.27263352274894714,"count":11,"buckets":[8785987,16705,5435,2314,1082,567,387,221,68,19]},"statistics":{"mean":0.9879209580172442,"stddev":2.5558262253122197,"maximum":176.0052032470703,"minimum":0.27263352274894714,"valid_percent":97.91983333333332},"spatial_resolution":30},{"name":"std_agbd","unit":"Mg ha-1","scale":1,"nodata":-9999.0,"offset":0,"sampling":"area","data_type":"float32","histogram":{"max":43.77642822265625,"min":0.10243990272283554,"count":11,"buckets":[8747047,39842,13419,6781,2524,1323,907,636,281,25]},"statistics":{"mean":0.44872608375218503,"stddev":1.0205389352986312,"maximum":43.77642822265625,"minimum":0.10243990272283554,"valid_percent":97.91983333333332},"spatial_resolution":30}],"roles":["data"],"title":"Gridded predictions of aboveground biomass (Mg/ha)","description":"Gridded predictions of aboveground biomass (Mg/ha)","processing:level":"L4"},"training_data_parquet":{"href":"s3://nasa-maap-data-store/file-staging/nasa-map/icesat2-boreal-v3.1/agb/0000544/boreal_agb_2020_202508201755714861_0000544_train.parquet","type":"application/x-parquet","roles":["data"],"title":"Tabular training data","description":"Tabular training data with latitude, longitude, and biomass observations"}},"geometry":{"type":"Polygon","coordinates":[[[59.20217219488729,50.19084051082751],[58.912567485634185,50.97761041743663],[57.668175458260485,50.78488521490738],[57.97909409514872,50.00125738393917],[59.20217219488729,50.19084051082751]]]},"collection":"icesat2-boreal-v3.1-agb","properties":{"datetime":"2020-07-01T23:59:59.500000Z","proj:bbox":[-4671478.000000006,6993304.000000009,-4581478.000000006,7083304.000000009],"proj:wkt2":"PROJCS[\"unnamed\",GEOGCS[\"GRS 1980(IUGG, 1980)\",DATUM[\"unknown\",SPHEROID[\"GRS80\",6378137,298.257222101],TOWGS84[0,0,0,0,0,0,0]],PRIMEM[\"Greenwich\",0],UNIT[\"degree\",0.0174532925199433,AUTHORITY[\"EPSG\",\"9122\"]]],PROJECTION[\"Albers_Conic_Equal_Area\"],PARAMETER[\"latitude_of_center\",40],PARAMETER[\"longitude_of_center\",180],PARAMETER[\"standard_parallel_1\",50],PARAMETER[\"standard_parallel_2\",70],PARAMETER[\"false_easting\",0],PARAMETER[\"false_northing\",0],UNIT[\"metre\",1,AUTHORITY[\"EPSG\",\"9001\"]],AXIS[\"Easting\",EAST],AXIS[\"Northing\",NORTH]]","proj:shape":[3000,3000],"end_datetime":"2020-12-31T23:59:59Z","proj:geometry":{"type":"Polygon","coordinates":[[[-4671478.000000006,6993304.000000009],[-4581478.000000006,6993304.000000009],[-4581478.000000006,7083304.000000009],[-4671478.000000006,7083304.000000009],[-4671478.000000006,6993304.000000009]]]},"proj:transform":[30.0,0.0,-4671478.000000006,0.0,-30.0,7083304.000000009,0.0,0.0,1.0],"start_datetime":"2020-01-01T00:00:00Z","created_datetime":"2025-08-20T00:00:00+00:00","icesat2-boreal:in_daac":false,"icesat2-boreal:tile_id":"0000544"},"stac_version":"1.1.0","stac_extensions":["https://stac-extensions.github.io/projection/v1.1.0/schema.json"]} ```
1 parent f02dc60 commit b79a0d3

File tree

3 files changed

+60
-5
lines changed

3 files changed

+60
-5
lines changed

crates/cli/src/lib.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -556,7 +556,9 @@ impl Rustac {
556556
Value::Stac(stac) => format.into_vec(stac)?,
557557
};
558558
// TODO allow disabling trailing newline
559-
bytes.push(b'\n');
559+
if !matches!(format, Format::NdJson) {
560+
bytes.push(b'\n');
561+
}
560562
std::io::stdout().write_all(&bytes)?;
561563
Ok(())
562564
}

crates/core/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66

77
## [Unreleased]
88

9+
### Fixed
10+
11+
- Deconstruct item collections when writing ndjson ([#824](https://github.com/stac-utils/rustac/pull/824))
12+
913
## [0.13.1] - 2025-09-23
1014

1115
### Changed

crates/core/src/ndjson.rs

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use crate::{Error, FromJson, Item, ItemCollection, Result, Value};
22
use bytes::Bytes;
33
use serde::Serialize;
4-
use std::io::Write;
4+
use std::io::{BufWriter, Write};
55

66
/// Create a STAC object from newline-delimited JSON.
77
pub trait FromNdjson: FromJson {
@@ -29,7 +29,7 @@ pub trait ToNdjson: Serialize {
2929
///
3030
/// # Examples
3131
///
32-
/// ```no_run
32+
/// ```
3333
/// use stac::{ToNdjson, ItemCollection, Item};
3434
///
3535
/// let item_collection: ItemCollection = vec![Item::new("a"), Item::new("b")].into();
@@ -109,6 +109,15 @@ impl ToNdjson for Item {}
109109
impl ToNdjson for crate::Catalog {}
110110
impl ToNdjson for crate::Collection {}
111111
impl ToNdjson for ItemCollection {
112+
fn to_ndjson_writer(&self, writer: impl Write) -> Result<()> {
113+
let mut writer = BufWriter::new(writer);
114+
for item in &self.items {
115+
serde_json::to_writer(&mut writer, item)?;
116+
writeln!(&mut writer)?;
117+
}
118+
Ok(())
119+
}
120+
112121
fn to_ndjson_vec(&self) -> Result<Vec<u8>> {
113122
let mut vec = Vec::new();
114123
self.to_ndjson_writer(&mut vec)?;
@@ -133,12 +142,35 @@ impl ToNdjson for serde_json::Value {
133142
self.to_ndjson_writer(&mut buf)?;
134143
Ok(buf)
135144
}
145+
fn to_ndjson_writer(&self, writer: impl Write) -> Result<()> {
146+
if self
147+
.get("type")
148+
.and_then(|type_| type_.as_str())
149+
.map(|type_| type_ == "FeatureCollection")
150+
.unwrap_or_default()
151+
{
152+
if let Some(features) = self
153+
.get("features")
154+
.and_then(|features| features.as_array())
155+
{
156+
let mut writer = BufWriter::new(writer);
157+
for feature in features {
158+
serde_json::to_writer(&mut writer, feature)?;
159+
writeln!(&mut writer)?;
160+
}
161+
}
162+
} else {
163+
serde_json::to_writer(writer, self)?;
164+
}
165+
Ok(())
166+
}
136167
}
137168

138169
#[cfg(test)]
139170
mod tests {
140-
use super::FromNdjson;
141-
use crate::{ItemCollection, Value};
171+
use super::{FromNdjson, ToNdjson};
172+
use crate::{FromJson, Item, ItemCollection, Value};
173+
use std::io::Cursor;
142174
use std::{fs::File, io::Read};
143175

144176
#[test]
@@ -161,4 +193,21 @@ mod tests {
161193
.unwrap();
162194
let _ = Value::from_ndjson_bytes(buf).unwrap();
163195
}
196+
197+
#[test]
198+
fn item_collection_write() {
199+
let item_collection = ItemCollection::from(vec![Item::new("an-item")]);
200+
let mut cursor = Cursor::new(Vec::new());
201+
item_collection.to_ndjson_writer(&mut cursor).unwrap();
202+
let _ = Item::from_json_slice(&cursor.into_inner()).unwrap();
203+
}
204+
205+
#[test]
206+
fn json_item_collection_write() {
207+
let item_collection =
208+
serde_json::to_value(ItemCollection::from(vec![Item::new("an-item")])).unwrap();
209+
let mut cursor = Cursor::new(Vec::new());
210+
item_collection.to_ndjson_writer(&mut cursor).unwrap();
211+
let _ = Item::from_json_slice(&cursor.into_inner()).unwrap();
212+
}
164213
}

0 commit comments

Comments
 (0)