Skip to content

Commit 8d70a20

Browse files
authored
feat: add a basic tree structure (#504)
* feat(core): add node * feat(core): implement iterator
1 parent b0e591c commit 8d70a20

File tree

10 files changed

+312
-29
lines changed

10 files changed

+312
-29
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ jobs:
2828
- uses: actions/checkout@v4
2929
- uses: Swatinem/rust-cache@v2
3030
- name: Test
31-
run: cargo test -p stac --all-features
31+
run: cargo test -p stac stac-types --all-features
3232
check-features-core:
3333
name: Check stac features
3434
runs-on: ubuntu-latest

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ object_store = "0.11.0"
6262
openssl = { version = "0.10.68", features = ["vendored"] }
6363
openssl-src = "=300.3.1" # joinked from https://github.com/iopsystems/rpc-perf/commit/705b290d2105af6f33150da04b217422c6d68701#diff-2e9d962a08321605940b5a657135052fbcef87b5e360662bb527c96d9a615542R41 to cross-compile Python
6464
parquet = { version = "52.2", default-features = false }
65+
path-slash = "0.2.1"
6566
pgstac = { version = "0.2.1", path = "crates/pgstac" }
6667
pyo3 = "0.22.3"
6768
pythonize = "0.22.0"

crates/cli/src/subcommand/serve.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ impl Run for Args {
9393
}
9494
Value::Collection(mut collection) => {
9595
if self.load_collection_items {
96-
collection.make_relative_links_absolute()?;
96+
collection.make_links_absolute()?;
9797
for link in collection.iter_item_links() {
9898
let href = link.href.to_string();
9999
let input = input.with_href(href);

crates/core/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
99
### Added
1010

1111
- `version` ([#476](https://github.com/stac-utils/stac-rs/pull/476))
12+
- `Node` and friends ([#504](https://github.com/stac-utils/stac-rs/pull/504))
13+
14+
### Changed
15+
16+
- `make_links_absolute` instead of `make_relative_links_absolute`, `make_links_relative` instead of `make_absolute_links_relative` ([#504](https://github.com/stac-utils/stac-rs/pull/504))
1217
- Permissive deserialization ([#505](https://github.com/stac-utils/stac-rs/pull/505))
1318

1419
### Removed

crates/core/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ jsonschema = { workspace = true, optional = true }
6363
log.workspace = true
6464
object_store = { workspace = true, optional = true }
6565
parquet = { workspace = true, optional = true }
66+
path-slash.workspace = true
6667
reqwest = { workspace = true, features = ["json", "blocking"], optional = true }
6768
serde = { workspace = true, features = ["derive"] }
6869
serde_json = { workspace = true, features = ["preserve_order"] }

crates/core/src/item.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@
33
use crate::{Asset, Assets, Bbox, Error, Fields, Link, Result, Version, STAC_VERSION};
44
use chrono::{DateTime, FixedOffset, Utc};
55
use geojson::{feature::Id, Feature, Geometry};
6+
use path_slash::PathBufExt;
67
use serde::{Deserialize, Serialize};
78
use serde_json::{Map, Value};
89
use stac_derive::{Href, Links, Migrate};
9-
use std::{collections::HashMap, path::Path};
10+
use std::{collections::HashMap, path::PathBuf};
1011
use url::Url;
1112

1213
const TOP_LEVEL_ATTRIBUTES: [&str; 8] = [
@@ -285,7 +286,7 @@ impl Builder {
285286
let mut item = Item::new(self.id);
286287
for (key, mut asset) in self.assets {
287288
if Url::parse(&asset.href).is_err() && self.canonicalize_paths {
288-
asset.href = Path::new(&asset.href)
289+
asset.href = PathBuf::from_slash(&asset.href)
289290
.canonicalize()?
290291
.to_string_lossy()
291292
.into_owned();

crates/core/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ mod item_asset;
168168
mod item_collection;
169169
mod json;
170170
mod ndjson;
171+
mod node;
171172
mod statistics;
172173
#[cfg(feature = "validate")]
173174
mod validate;
@@ -196,6 +197,7 @@ pub use {
196197
item_collection::ItemCollection,
197198
json::{FromJson, ToJson},
198199
ndjson::{FromNdjson, ToNdjson},
200+
node::Node,
199201
statistics::Statistics,
200202
value::Value,
201203
};

crates/core/src/node.rs

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
use crate::{Catalog, Collection, Error, Href, Item, Link, Links, Result, Value};
2+
use std::collections::VecDeque;
3+
4+
/// A node in a STAC tree.
5+
#[derive(Debug)]
6+
pub struct Node {
7+
/// The value of the node.
8+
pub value: Container,
9+
10+
/// The child nodes.
11+
pub children: VecDeque<Node>,
12+
13+
/// The node's items.
14+
pub items: VecDeque<Item>,
15+
}
16+
17+
/// A STAC container, i.e. a [Catalog] or a [Collection].
18+
#[derive(Debug)]
19+
pub enum Container {
20+
/// A [Collection].
21+
Collection(Collection),
22+
23+
/// A [Catalog].
24+
Catalog(Catalog),
25+
}
26+
27+
/// An iterator over a node and all of its descendants.
28+
#[derive(Debug)]
29+
pub struct IntoValues {
30+
node: Option<Node>,
31+
children: VecDeque<Node>,
32+
items: VecDeque<Item>,
33+
}
34+
35+
impl Node {
36+
/// Resolves all child and item links in this node.
37+
///
38+
/// # Examples
39+
///
40+
/// ```
41+
/// use stac::{Catalog, Node};
42+
///
43+
/// let mut node: Node = stac::read::<Catalog>("examples/catalog.json").unwrap().into();
44+
/// node.resolve().unwrap();
45+
/// ```
46+
pub fn resolve(&mut self) -> Result<()> {
47+
let links = std::mem::take(self.value.links_mut());
48+
let href = self.value.href().map(String::from);
49+
for mut link in links {
50+
if link.is_child() {
51+
link.make_absolute(href.as_deref())?;
52+
// TODO enable object store
53+
tracing::debug!("resolving child: {}", link.href);
54+
let child: Container = crate::read::<Value>(link.href)?.try_into()?;
55+
self.children.push_back(child.into());
56+
} else if link.is_item() {
57+
link.make_absolute(href.as_deref())?;
58+
tracing::debug!("resolving item: {}", link.href);
59+
let item = crate::read::<Item>(link.href)?;
60+
self.items.push_back(item);
61+
} else {
62+
self.value.links_mut().push(link);
63+
}
64+
}
65+
Ok(())
66+
}
67+
68+
/// Creates a consuming iterator over this node and its children and items.
69+
///
70+
/// This iterator will visit all children (catalogs and collections) first,
71+
/// then visit all the items.
72+
///
73+
/// # Examples
74+
///
75+
/// ```
76+
/// use stac::{Node, Catalog};
77+
///
78+
/// let mut node: Node = Catalog::new("an-id", "a description").into();
79+
/// node.children
80+
/// .push_back(Catalog::new("child", "child catalog").into());
81+
/// let values: Vec<_> = node.into_values().collect::<Result<_, _>>().unwrap();
82+
/// assert_eq!(values.len(), 2);
83+
/// ```
84+
pub fn into_values(self) -> IntoValues {
85+
IntoValues {
86+
node: Some(self),
87+
children: VecDeque::new(),
88+
items: VecDeque::new(),
89+
}
90+
}
91+
}
92+
93+
impl Iterator for IntoValues {
94+
type Item = Result<Value>;
95+
96+
fn next(&mut self) -> Option<Self::Item> {
97+
if let Some(mut node) = self.node.take() {
98+
self.children.append(&mut node.children);
99+
self.items.append(&mut node.items);
100+
Some(Ok(node.value.into()))
101+
} else if let Some(child) = self.children.pop_front() {
102+
self.node = Some(child);
103+
self.next()
104+
} else {
105+
self.items.pop_front().map(|item| Ok(item.into()))
106+
}
107+
}
108+
}
109+
110+
impl From<Catalog> for Node {
111+
fn from(value: Catalog) -> Self {
112+
Container::from(value).into()
113+
}
114+
}
115+
116+
impl From<Catalog> for Container {
117+
fn from(value: Catalog) -> Self {
118+
Container::Catalog(value)
119+
}
120+
}
121+
122+
impl From<Collection> for Node {
123+
fn from(value: Collection) -> Self {
124+
Container::from(value).into()
125+
}
126+
}
127+
128+
impl From<Collection> for Container {
129+
fn from(value: Collection) -> Self {
130+
Container::Collection(value)
131+
}
132+
}
133+
134+
impl From<Container> for Node {
135+
fn from(value: Container) -> Self {
136+
Node {
137+
value,
138+
children: VecDeque::new(),
139+
items: VecDeque::new(),
140+
}
141+
}
142+
}
143+
144+
impl TryFrom<Value> for Container {
145+
type Error = Error;
146+
147+
fn try_from(value: Value) -> std::result::Result<Self, Self::Error> {
148+
match value {
149+
Value::Catalog(c) => Ok(c.into()),
150+
Value::Collection(c) => Ok(c.into()),
151+
_ => Err(stac_types::Error::IncorrectType {
152+
actual: value.type_name().to_string(),
153+
expected: "Catalog or Collection".to_string(),
154+
}
155+
.into()),
156+
}
157+
}
158+
}
159+
160+
impl From<Container> for Value {
161+
fn from(value: Container) -> Self {
162+
match value {
163+
Container::Catalog(c) => Value::Catalog(c),
164+
Container::Collection(c) => Value::Collection(c),
165+
}
166+
}
167+
}
168+
169+
impl Links for Container {
170+
fn links(&self) -> &[Link] {
171+
match self {
172+
Container::Catalog(c) => c.links(),
173+
Container::Collection(c) => c.links(),
174+
}
175+
}
176+
177+
fn links_mut(&mut self) -> &mut Vec<Link> {
178+
match self {
179+
Container::Catalog(c) => c.links_mut(),
180+
Container::Collection(c) => c.links_mut(),
181+
}
182+
}
183+
}
184+
185+
impl Href for Container {
186+
fn href(&self) -> Option<&str> {
187+
match self {
188+
Container::Catalog(c) => c.href(),
189+
Container::Collection(c) => c.href(),
190+
}
191+
}
192+
193+
fn set_href(&mut self, href: impl ToString) {
194+
match self {
195+
Container::Catalog(c) => c.set_href(href),
196+
Container::Collection(c) => c.set_href(href),
197+
}
198+
}
199+
200+
fn clear_href(&mut self) {
201+
match self {
202+
Container::Catalog(c) => c.clear_href(),
203+
Container::Collection(c) => c.clear_href(),
204+
}
205+
}
206+
}
207+
208+
#[cfg(test)]
209+
mod tests {
210+
use super::Node;
211+
use crate::{Catalog, Collection, Links};
212+
213+
#[test]
214+
fn into_node() {
215+
let _ = Node::from(Catalog::new("an-id", "a description"));
216+
let _ = Node::from(Collection::new("an-id", "a description"));
217+
}
218+
219+
#[test]
220+
fn resolve() {
221+
let mut node: Node = crate::read::<Catalog>("examples/catalog.json")
222+
.unwrap()
223+
.into();
224+
node.resolve().unwrap();
225+
assert_eq!(node.children.len(), 3);
226+
assert_eq!(node.items.len(), 1);
227+
assert_eq!(node.value.links().len(), 2);
228+
}
229+
230+
#[test]
231+
fn into_values() {
232+
let mut node: Node = Catalog::new("an-id", "a description").into();
233+
node.children
234+
.push_back(Catalog::new("child", "child catalog").into());
235+
let mut iter = node.into_values();
236+
let _root = iter.next().unwrap().unwrap();
237+
let _child = iter.next().unwrap().unwrap();
238+
assert!(iter.next().is_none());
239+
}
240+
}

crates/types/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ rust-version.workspace = true
1111

1212
[dependencies]
1313
mime.workspace = true
14+
path-slash.workspace = true
1415
serde = { workspace = true, features = ["derive"] }
1516
serde_json.workspace = true
1617
thiserror.workspace = true

0 commit comments

Comments
 (0)