Skip to content

Commit

Permalink
feat: add a basic tree structure (#504)
Browse files Browse the repository at this point in the history
* feat(core): add node

* feat(core): implement iterator
  • Loading branch information
gadomski authored Oct 31, 2024
1 parent b0e591c commit 8d70a20
Show file tree
Hide file tree
Showing 10 changed files with 312 additions and 29 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ jobs:
- uses: actions/checkout@v4
- uses: Swatinem/rust-cache@v2
- name: Test
run: cargo test -p stac --all-features
run: cargo test -p stac stac-types --all-features
check-features-core:
name: Check stac features
runs-on: ubuntu-latest
Expand Down
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ object_store = "0.11.0"
openssl = { version = "0.10.68", features = ["vendored"] }
openssl-src = "=300.3.1" # joinked from https://github.com/iopsystems/rpc-perf/commit/705b290d2105af6f33150da04b217422c6d68701#diff-2e9d962a08321605940b5a657135052fbcef87b5e360662bb527c96d9a615542R41 to cross-compile Python
parquet = { version = "52.2", default-features = false }
path-slash = "0.2.1"
pgstac = { version = "0.2.1", path = "crates/pgstac" }
pyo3 = "0.22.3"
pythonize = "0.22.0"
Expand Down
2 changes: 1 addition & 1 deletion crates/cli/src/subcommand/serve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ impl Run for Args {
}
Value::Collection(mut collection) => {
if self.load_collection_items {
collection.make_relative_links_absolute()?;
collection.make_links_absolute()?;
for link in collection.iter_item_links() {
let href = link.href.to_string();
let input = input.with_href(href);
Expand Down
5 changes: 5 additions & 0 deletions crates/core/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
### Added

- `version` ([#476](https://github.com/stac-utils/stac-rs/pull/476))
- `Node` and friends ([#504](https://github.com/stac-utils/stac-rs/pull/504))

### Changed

- `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))
- Permissive deserialization ([#505](https://github.com/stac-utils/stac-rs/pull/505))

### Removed
Expand Down
1 change: 1 addition & 0 deletions crates/core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ jsonschema = { workspace = true, optional = true }
log.workspace = true
object_store = { workspace = true, optional = true }
parquet = { workspace = true, optional = true }
path-slash.workspace = true
reqwest = { workspace = true, features = ["json", "blocking"], optional = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true, features = ["preserve_order"] }
Expand Down
5 changes: 3 additions & 2 deletions crates/core/src/item.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
use crate::{Asset, Assets, Bbox, Error, Fields, Link, Result, Version, STAC_VERSION};
use chrono::{DateTime, FixedOffset, Utc};
use geojson::{feature::Id, Feature, Geometry};
use path_slash::PathBufExt;
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use stac_derive::{Href, Links, Migrate};
use std::{collections::HashMap, path::Path};
use std::{collections::HashMap, path::PathBuf};
use url::Url;

const TOP_LEVEL_ATTRIBUTES: [&str; 8] = [
Expand Down Expand Up @@ -285,7 +286,7 @@ impl Builder {
let mut item = Item::new(self.id);
for (key, mut asset) in self.assets {
if Url::parse(&asset.href).is_err() && self.canonicalize_paths {
asset.href = Path::new(&asset.href)
asset.href = PathBuf::from_slash(&asset.href)
.canonicalize()?
.to_string_lossy()
.into_owned();
Expand Down
2 changes: 2 additions & 0 deletions crates/core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ mod item_asset;
mod item_collection;
mod json;
mod ndjson;
mod node;
mod statistics;
#[cfg(feature = "validate")]
mod validate;
Expand Down Expand Up @@ -196,6 +197,7 @@ pub use {
item_collection::ItemCollection,
json::{FromJson, ToJson},
ndjson::{FromNdjson, ToNdjson},
node::Node,
statistics::Statistics,
value::Value,
};
Expand Down
240 changes: 240 additions & 0 deletions crates/core/src/node.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
use crate::{Catalog, Collection, Error, Href, Item, Link, Links, Result, Value};
use std::collections::VecDeque;

/// A node in a STAC tree.
#[derive(Debug)]
pub struct Node {
/// The value of the node.
pub value: Container,

/// The child nodes.
pub children: VecDeque<Node>,

/// The node's items.
pub items: VecDeque<Item>,
}

/// A STAC container, i.e. a [Catalog] or a [Collection].
#[derive(Debug)]
pub enum Container {
/// A [Collection].
Collection(Collection),

/// A [Catalog].
Catalog(Catalog),
}

/// An iterator over a node and all of its descendants.
#[derive(Debug)]
pub struct IntoValues {
node: Option<Node>,
children: VecDeque<Node>,
items: VecDeque<Item>,
}

impl Node {
/// Resolves all child and item links in this node.
///
/// # Examples
///
/// ```
/// use stac::{Catalog, Node};
///
/// let mut node: Node = stac::read::<Catalog>("examples/catalog.json").unwrap().into();
/// node.resolve().unwrap();
/// ```
pub fn resolve(&mut self) -> Result<()> {
let links = std::mem::take(self.value.links_mut());
let href = self.value.href().map(String::from);
for mut link in links {
if link.is_child() {
link.make_absolute(href.as_deref())?;
// TODO enable object store
tracing::debug!("resolving child: {}", link.href);
let child: Container = crate::read::<Value>(link.href)?.try_into()?;
self.children.push_back(child.into());
} else if link.is_item() {
link.make_absolute(href.as_deref())?;
tracing::debug!("resolving item: {}", link.href);
let item = crate::read::<Item>(link.href)?;
self.items.push_back(item);
} else {
self.value.links_mut().push(link);
}
}
Ok(())
}

/// Creates a consuming iterator over this node and its children and items.
///
/// This iterator will visit all children (catalogs and collections) first,
/// then visit all the items.
///
/// # Examples
///
/// ```
/// use stac::{Node, Catalog};
///
/// let mut node: Node = Catalog::new("an-id", "a description").into();
/// node.children
/// .push_back(Catalog::new("child", "child catalog").into());
/// let values: Vec<_> = node.into_values().collect::<Result<_, _>>().unwrap();
/// assert_eq!(values.len(), 2);
/// ```
pub fn into_values(self) -> IntoValues {
IntoValues {
node: Some(self),
children: VecDeque::new(),
items: VecDeque::new(),
}
}
}

impl Iterator for IntoValues {
type Item = Result<Value>;

fn next(&mut self) -> Option<Self::Item> {
if let Some(mut node) = self.node.take() {
self.children.append(&mut node.children);
self.items.append(&mut node.items);
Some(Ok(node.value.into()))
} else if let Some(child) = self.children.pop_front() {
self.node = Some(child);
self.next()
} else {
self.items.pop_front().map(|item| Ok(item.into()))
}
}
}

impl From<Catalog> for Node {
fn from(value: Catalog) -> Self {
Container::from(value).into()
}
}

impl From<Catalog> for Container {
fn from(value: Catalog) -> Self {
Container::Catalog(value)
}
}

impl From<Collection> for Node {
fn from(value: Collection) -> Self {
Container::from(value).into()
}
}

impl From<Collection> for Container {
fn from(value: Collection) -> Self {
Container::Collection(value)
}
}

impl From<Container> for Node {
fn from(value: Container) -> Self {
Node {
value,
children: VecDeque::new(),
items: VecDeque::new(),
}
}
}

impl TryFrom<Value> for Container {
type Error = Error;

fn try_from(value: Value) -> std::result::Result<Self, Self::Error> {
match value {
Value::Catalog(c) => Ok(c.into()),
Value::Collection(c) => Ok(c.into()),
_ => Err(stac_types::Error::IncorrectType {
actual: value.type_name().to_string(),
expected: "Catalog or Collection".to_string(),
}
.into()),
}
}
}

impl From<Container> for Value {
fn from(value: Container) -> Self {
match value {
Container::Catalog(c) => Value::Catalog(c),
Container::Collection(c) => Value::Collection(c),
}
}
}

impl Links for Container {
fn links(&self) -> &[Link] {
match self {
Container::Catalog(c) => c.links(),
Container::Collection(c) => c.links(),
}
}

fn links_mut(&mut self) -> &mut Vec<Link> {
match self {
Container::Catalog(c) => c.links_mut(),
Container::Collection(c) => c.links_mut(),
}
}
}

impl Href for Container {
fn href(&self) -> Option<&str> {
match self {
Container::Catalog(c) => c.href(),
Container::Collection(c) => c.href(),
}
}

fn set_href(&mut self, href: impl ToString) {
match self {
Container::Catalog(c) => c.set_href(href),
Container::Collection(c) => c.set_href(href),
}
}

fn clear_href(&mut self) {
match self {
Container::Catalog(c) => c.clear_href(),
Container::Collection(c) => c.clear_href(),
}
}
}

#[cfg(test)]
mod tests {
use super::Node;
use crate::{Catalog, Collection, Links};

#[test]
fn into_node() {
let _ = Node::from(Catalog::new("an-id", "a description"));
let _ = Node::from(Collection::new("an-id", "a description"));
}

#[test]
fn resolve() {
let mut node: Node = crate::read::<Catalog>("examples/catalog.json")
.unwrap()
.into();
node.resolve().unwrap();
assert_eq!(node.children.len(), 3);
assert_eq!(node.items.len(), 1);
assert_eq!(node.value.links().len(), 2);
}

#[test]
fn into_values() {
let mut node: Node = Catalog::new("an-id", "a description").into();
node.children
.push_back(Catalog::new("child", "child catalog").into());
let mut iter = node.into_values();
let _root = iter.next().unwrap().unwrap();
let _child = iter.next().unwrap().unwrap();
assert!(iter.next().is_none());
}
}
1 change: 1 addition & 0 deletions crates/types/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ rust-version.workspace = true

[dependencies]
mime.workspace = true
path-slash.workspace = true
serde = { workspace = true, features = ["derive"] }
serde_json.workspace = true
thiserror.workspace = true
Expand Down
Loading

0 comments on commit 8d70a20

Please sign in to comment.