Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a basic tree structure #504

Merged
merged 12 commits into from
Oct 31, 2024
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