From 5ff0a4e970840542bb81f1ab99f1599a789e11d0 Mon Sep 17 00:00:00 2001 From: Pete Gadomski Date: Fri, 1 Nov 2024 13:09:27 -0600 Subject: [PATCH] feat: add tree structure (#509) That was a journey, but we got there. --- .github/workflows/ci.yml | 3 +- crates/api/Cargo.toml | 1 + crates/api/src/client.rs | 20 +- crates/api/src/collections.rs | 10 +- crates/api/src/item_collection.rs | 10 +- crates/cli/src/subcommand/serve.rs | 2 +- crates/core/CHANGELOG.md | 5 + crates/core/Cargo.toml | 2 +- crates/core/src/catalog.rs | 10 +- crates/core/src/collection.rs | 21 +- crates/core/src/format.rs | 71 +++--- crates/core/src/geoparquet/feature.rs | 4 +- crates/core/src/io.rs | 33 +-- crates/core/src/item.rs | 10 +- crates/core/src/item_collection.rs | 10 +- crates/core/src/json.rs | 16 +- crates/core/src/lib.rs | 4 +- crates/core/src/ndjson.rs | 10 +- crates/core/src/node.rs | 239 +++++++++++++++++++ crates/core/src/value.rs | 33 +-- crates/derive/src/lib.rs | 17 +- crates/server/src/api.rs | 30 +-- crates/types/Cargo.toml | 6 +- crates/types/src/href.rs | 317 ++++++++++++++++++++++++-- crates/types/src/lib.rs | 2 +- crates/types/src/link.rs | 251 ++++---------------- 26 files changed, 754 insertions(+), 383 deletions(-) create mode 100644 crates/core/src/node.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9ac11a08..27d58b59 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,6 +19,7 @@ jobs: name: Test stac runs-on: ${{ matrix.os }} strategy: + fail-fast: false matrix: os: - ubuntu-latest @@ -28,7 +29,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 -p stac-types --all-features check-features-core: name: Check stac features runs-on: ubuntu-latest diff --git a/crates/api/Cargo.toml b/crates/api/Cargo.toml index 686ff6ad..dd466d88 100644 --- a/crates/api/Cargo.toml +++ b/crates/api/Cargo.toml @@ -18,6 +18,7 @@ client = [ "dep:http", "dep:reqwest", "dep:tokio", + "stac-types/reqwest", ] geo = ["dep:geo", "stac/geo"] diff --git a/crates/api/src/client.rs b/crates/api/src/client.rs index 17d6a6da..da808ac8 100644 --- a/crates/api/src/client.rs +++ b/crates/api/src/client.rs @@ -7,7 +7,7 @@ use http::header::{HeaderName, USER_AGENT}; use reqwest::{header::HeaderMap, ClientBuilder, IntoUrl, Method, StatusCode}; use serde::{de::DeserializeOwned, Serialize}; use serde_json::{Map, Value}; -use stac::{Collection, Href, Link, Links}; +use stac::{Collection, Link, Links, SelfHref}; use std::pin::Pin; use tokio::{ runtime::{Builder, Runtime}, @@ -170,13 +170,13 @@ impl Client { async fn get(&self, url: impl IntoUrl) -> Result where - V: DeserializeOwned + Href, + V: DeserializeOwned + SelfHref, { let url = url.into_url()?; let mut value = self .request::<(), V>(Method::GET, url.clone(), None, None) .await?; - value.set_href(url); + *value.self_href_mut() = Some(url.into()); Ok(value) } @@ -243,7 +243,7 @@ impl Client { } else { None }; - self.request::, R>(method, link.href, &link.body, headers) + self.request::, R>(method, link.href.as_str(), &link.body, headers) .await } } @@ -404,7 +404,7 @@ mod tests { let mut page_1_body: ItemCollection = serde_json::from_str(include_str!("../mocks/search-page-1.json")).unwrap(); let mut next_link = page_1_body.link("next").unwrap().clone(); - next_link.href = format!("{}/search", server.url()); + next_link.href = format!("{}/search", server.url()).into(); page_1_body.set_link(next_link); let page_1 = server .mock("POST", "/search") @@ -454,13 +454,14 @@ mod tests { let mut page_1_body: ItemCollection = serde_json::from_str(include_str!("../mocks/items-page-1.json")).unwrap(); let mut next_link = page_1_body.link("next").unwrap().clone(); - let url: Url = next_link.href.parse().unwrap(); + let url: Url = next_link.href.as_str().parse().unwrap(); let query = url.query().unwrap(); next_link.href = format!( "{}/collections/sentinel-2-l2a/items?{}", server.url(), query - ); + ) + .into(); page_1_body.set_link(next_link); let page_1 = server .mock("GET", "/collections/sentinel-2-l2a/items?limit=1") @@ -500,13 +501,14 @@ mod tests { let mut page_body: ItemCollection = serde_json::from_str(include_str!("../mocks/items-page-1.json")).unwrap(); let mut next_link = page_body.link("next").unwrap().clone(); - let url: Url = next_link.href.parse().unwrap(); + let url: Url = next_link.href.as_str().parse().unwrap(); let query = url.query().unwrap(); next_link.href = format!( "{}/collections/sentinel-2-l2a/items?{}", server.url(), query - ); + ) + .into(); page_body.set_link(next_link); page_body.items = vec![]; let page = server diff --git a/crates/api/src/collections.rs b/crates/api/src/collections.rs index d3875f07..28718b1c 100644 --- a/crates/api/src/collections.rs +++ b/crates/api/src/collections.rs @@ -1,10 +1,10 @@ use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; -use stac::{Collection, Link}; -use stac_derive::{Href, Links}; +use stac::{Collection, Href, Link}; +use stac_derive::{Links, SelfHref}; /// Object containing an array of collections and an array of links. -#[derive(Debug, Serialize, Deserialize, Href, Links)] +#[derive(Debug, Serialize, Deserialize, SelfHref, Links)] pub struct Collections { /// The [Collection] objects in the [stac::Catalog]. pub collections: Vec, @@ -17,7 +17,7 @@ pub struct Collections { pub additional_fields: Map, #[serde(skip)] - href: Option, + self_href: Option, } impl From> for Collections { @@ -26,7 +26,7 @@ impl From> for Collections { collections, links: Vec::new(), additional_fields: Map::new(), - href: None, + self_href: None, } } } diff --git a/crates/api/src/item_collection.rs b/crates/api/src/item_collection.rs index f309cc9b..2a6df3c9 100644 --- a/crates/api/src/item_collection.rs +++ b/crates/api/src/item_collection.rs @@ -1,8 +1,8 @@ use crate::{Item, Result}; use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; -use stac::Link; -use stac_derive::{Href, Links}; +use stac::{Href, Link}; +use stac_derive::{Links, SelfHref}; /// The return value of the `/items` and `/search` endpoints. /// @@ -10,7 +10,7 @@ use stac_derive::{Href, Links}; /// extension](https://github.com/stac-api-extensions/fields) is used, it might /// not be. Defined by the [itemcollection /// fragment](https://github.com/radiantearth/stac-api-spec/blob/main/fragments/itemcollection/README.md). -#[derive(Debug, Serialize, Deserialize, Default, Links, Href)] +#[derive(Debug, Serialize, Deserialize, Default, Links, SelfHref)] #[serde(tag = "type", rename = "FeatureCollection")] pub struct ItemCollection { /// A possibly-empty array of Item objects. @@ -67,7 +67,7 @@ pub struct ItemCollection { pub last: Option>, #[serde(skip)] - href: Option, + self_href: Option, } /// The search-related metadata for the [ItemCollection]. @@ -114,7 +114,7 @@ impl ItemCollection { prev: None, first: None, last: None, - href: None, + self_href: None, }) } } diff --git a/crates/cli/src/subcommand/serve.rs b/crates/cli/src/subcommand/serve.rs index 55d0160c..0644e825 100644 --- a/crates/cli/src/subcommand/serve.rs +++ b/crates/cli/src/subcommand/serve.rs @@ -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); diff --git a/crates/core/CHANGELOG.md b/crates/core/CHANGELOG.md index 4dbd1053..d7c066f7 100644 --- a/crates/core/CHANGELOG.md +++ b/crates/core/CHANGELOG.md @@ -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 diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index d8f351d2..845f7ac3 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -42,7 +42,7 @@ object-store-all = [ "object-store-gcp", "object-store-http", ] -reqwest = ["dep:reqwest"] +reqwest = ["dep:reqwest", "stac-types/reqwest"] reqwest-rustls = ["reqwest/rustls-tls"] validate = ["dep:jsonschema", "dep:reqwest", "dep:tokio", "dep:fluent-uri"] validate-blocking = ["validate", "tokio/rt"] diff --git a/crates/core/src/catalog.rs b/crates/core/src/catalog.rs index f0b406dc..8391c87c 100644 --- a/crates/core/src/catalog.rs +++ b/crates/core/src/catalog.rs @@ -1,7 +1,7 @@ -use crate::{Error, Link, Result, Version, STAC_VERSION}; +use crate::{Error, Href, Link, Result, Version, STAC_VERSION}; use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; -use stac_derive::{Fields, Href, Links, Migrate}; +use stac_derive::{Fields, Links, Migrate, SelfHref}; /// A STAC Catalog object represents a logical group of other `Catalog`, /// [Collection](crate::Collection), and [Item](crate::Item) objects. @@ -15,7 +15,7 @@ use stac_derive::{Fields, Href, Links, Migrate}; /// A `Catalog` object will typically be the entry point into a STAC catalog. /// Their purpose is discovery: to be browsed by people or be crawled by clients /// to build a searchable index. -#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Href, Migrate, Links, Fields)] +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, SelfHref, Migrate, Links, Fields)] #[serde(tag = "type")] pub struct Catalog { /// The STAC version the `Catalog` implements. @@ -51,7 +51,7 @@ pub struct Catalog { pub additional_fields: Map, #[serde(skip)] - href: Option, + self_href: Option, } impl Catalog { @@ -74,7 +74,7 @@ impl Catalog { description: description.to_string(), links: Vec::new(), additional_fields: Map::new(), - href: None, + self_href: None, } } } diff --git a/crates/core/src/collection.rs b/crates/core/src/collection.rs index 90bc03a3..76bf5f2e 100644 --- a/crates/core/src/collection.rs +++ b/crates/core/src/collection.rs @@ -1,11 +1,11 @@ use crate::{ - Asset, Assets, Bbox, Error, Href, Item, ItemAsset, Link, Links, Migrate, Result, Version, - STAC_VERSION, + Asset, Assets, Bbox, Error, Href, Item, ItemAsset, Link, Links, Migrate, Result, SelfHref, + Version, STAC_VERSION, }; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; -use stac_derive::{Fields, Href, Links}; +use stac_derive::{Fields, Links, SelfHref}; use std::collections::HashMap; const DEFAULT_LICENSE: &str = "proprietary"; @@ -22,7 +22,7 @@ const DEFAULT_LICENSE: &str = "proprietary"; /// A STAC `Collection` is represented in JSON format. Any JSON object that /// contains all the required fields is a valid STAC `Collection` and also a valid /// STAC `Catalog`. -#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Href, Links, Fields)] +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, SelfHref, Links, Fields)] #[serde(tag = "type")] pub struct Collection { /// The STAC version the `Collection` implements. @@ -93,7 +93,7 @@ pub struct Collection { pub additional_fields: Map, #[serde(skip)] - href: Option, + self_href: Option, } /// This object provides information about a provider. @@ -184,7 +184,7 @@ impl Collection { assets: HashMap::new(), item_assets: HashMap::new(), additional_fields: Map::new(), - href: None, + self_href: None, } } @@ -249,11 +249,8 @@ impl Collection { } fn maybe_add_item_link(&mut self, item: &Item) -> Option<&Link> { - if let Some(href) = item - .href() - .or(item.self_link().map(|link| link.href.as_str())) - { - self.links.push(Link::item(href)); + if let Some(href) = item.self_href().or(item.self_link().map(|link| &link.href)) { + self.links.push(Link::item(href.clone())); self.links.last() } else { None @@ -448,7 +445,7 @@ mod tests { .unwrap() ); let link = collection.link("item").unwrap(); - assert_eq!(link.href, "examples/simple-item.json"); + assert!(link.href.to_string().ends_with("simple-item.json")); } } diff --git a/crates/core/src/format.rs b/crates/core/src/format.rs index aad24745..d34cb395 100644 --- a/crates/core/src/format.rs +++ b/crates/core/src/format.rs @@ -1,10 +1,10 @@ use crate::{ geoparquet::{Compression, FromGeoparquet, IntoGeoparquet}, - Error, FromJson, FromNdjson, Href, Result, ToJson, ToNdjson, + Error, FromJson, FromNdjson, Href, Result, SelfHref, ToJson, ToNdjson, }; use bytes::Bytes; +use stac_types::RealizedHref; use std::{fmt::Display, path::Path, str::FromStr}; -use url::Url; /// The format of STAC data. #[derive(Debug, Copy, Clone, PartialEq)] @@ -45,28 +45,31 @@ impl Format { /// let item: Item = Format::json().read("examples/simple-item.json").unwrap(); /// ``` #[allow(unused_variables)] - pub fn read( + pub fn read( &self, - href: impl ToString, + href: impl Into, ) -> Result { - let href = href.to_string(); - let mut value: T = if let Some(url) = Url::parse(&href) - .ok() - .filter(|url| url.scheme().starts_with("http")) - { - #[cfg(feature = "reqwest")] - { - let bytes = reqwest::blocking::get(url)?.bytes()?; - self.from_bytes(bytes)? + let mut href = href.into(); + let mut value: T = match href.clone().realize() { + RealizedHref::Url(url) => { + #[cfg(feature = "reqwest")] + { + let bytes = reqwest::blocking::get(url)?.bytes()?; + self.from_bytes(bytes)? + } + #[cfg(not(feature = "reqwest"))] + { + return Err(Error::FeatureNotEnabled("reqwest")); + } } - #[cfg(not(feature = "reqwest"))] - { - return Err(Error::FeatureNotEnabled("reqwest")); + RealizedHref::PathBuf(path) => { + let path = path.canonicalize()?; + let value = self.from_path(&path)?; + href = path.as_path().into(); + value } - } else { - self.from_path(&href)? }; - value.set_href(href); + *value.self_href_mut() = Some(href); Ok(value) } @@ -79,7 +82,7 @@ impl Format { /// /// let item: Item = Format::json().from_path("examples/simple-item.json").unwrap(); /// ``` - pub fn from_path( + pub fn from_path( &self, path: impl AsRef, ) -> Result { @@ -128,25 +131,25 @@ impl Format { /// } /// ``` #[cfg(feature = "object-store")] - pub async fn get_opts(&self, href: impl ToString, options: I) -> Result + pub async fn get_opts(&self, href: impl Into, options: I) -> Result where - T: Href + FromJson + FromNdjson + FromGeoparquet, + T: SelfHref + FromJson + FromNdjson + FromGeoparquet, I: IntoIterator, K: AsRef, V: Into, { - let href = href.to_string(); - let mut value: T = if let Ok(url) = Url::parse(&href) { - use object_store::ObjectStore; + match href.into().realize() { + RealizedHref::Url(url) => { + use object_store::ObjectStore; - let (object_store, path) = object_store::parse_url_opts(&url, options)?; - let get_result = object_store.get(&path).await?; - self.from_bytes(get_result.bytes().await?)? - } else { - self.from_path(&href)? - }; - value.set_href(href); - Ok(value) + let (object_store, path) = object_store::parse_url_opts(&url, options)?; + let get_result = object_store.get(&path).await?; + let mut value: T = self.from_bytes(get_result.bytes().await?)?; + *value.self_href_mut() = Some(Href::Url(url)); + Ok(value) + } + RealizedHref::PathBuf(path) => self.from_path(path), + } } /// Writes a STAC value to the provided path. @@ -217,7 +220,7 @@ impl Format { V: Into, { let href = href.to_string(); - if let Ok(url) = Url::parse(&href) { + if let Ok(url) = url::Url::parse(&href) { use object_store::ObjectStore; let (object_store, path) = object_store::parse_url_opts(&url, options)?; diff --git a/crates/core/src/geoparquet/feature.rs b/crates/core/src/geoparquet/feature.rs index bb2164d5..1c276d04 100644 --- a/crates/core/src/geoparquet/feature.rs +++ b/crates/core/src/geoparquet/feature.rs @@ -182,7 +182,7 @@ impl IntoGeoparquet for serde_json::Value { #[cfg(test)] mod tests { - use crate::{FromGeoparquet, Href, Item, ItemCollection, Value}; + use crate::{FromGeoparquet, Item, ItemCollection, SelfHref, Value}; use bytes::Bytes; use std::{ fs::File, @@ -207,7 +207,7 @@ mod tests { #[test] fn roundtrip() { let mut item: Item = crate::read("examples/simple-item.json").unwrap(); - item.clear_href(); + *item.self_href_mut() = None; let mut cursor = Cursor::new(Vec::new()); super::into_writer(&mut cursor, vec![item.clone()]).unwrap(); let bytes = Bytes::from(cursor.into_inner()); diff --git a/crates/core/src/io.rs b/crates/core/src/io.rs index 430c12fb..f5b12cfe 100644 --- a/crates/core/src/io.rs +++ b/crates/core/src/io.rs @@ -76,14 +76,13 @@ //! } //! ``` -use std::path::Path; - use crate::{ geoparquet::{FromGeoparquet, IntoGeoparquet}, json::{FromJson, ToJson}, ndjson::{FromNdjson, ToNdjson}, - Format, Href, Result, + Format, Href, Result, SelfHref, }; +use std::path::Path; /// Reads a STAC value from an href. /// @@ -95,9 +94,11 @@ use crate::{ /// ``` /// let item: stac::Item = stac::read("examples/simple-item.json").unwrap(); /// ``` -pub fn read(href: impl ToString) -> Result { - let href = href.to_string(); - let format = Format::infer_from_href(&href).unwrap_or_default(); +pub fn read( + href: impl Into, +) -> Result { + let href = href.into(); + let format = Format::infer_from_href(href.as_str()).unwrap_or_default(); format.read(href) } @@ -116,8 +117,8 @@ pub fn read(href: impl ToStrin /// } /// ``` #[cfg(feature = "object-store")] -pub async fn get( - href: impl ToString, +pub async fn get( + href: impl Into, ) -> Result { let options: [(&str, &str); 0] = []; get_opts(href, options).await @@ -140,15 +141,15 @@ pub async fn get( /// } /// ``` #[cfg(feature = "object-store")] -pub async fn get_opts(href: impl ToString, options: I) -> Result +pub async fn get_opts(href: impl Into, options: I) -> Result where - T: Href + FromJson + FromNdjson + FromGeoparquet, + T: SelfHref + FromJson + FromNdjson + FromGeoparquet, I: IntoIterator, K: AsRef, V: Into, { - let href = href.to_string(); - let format = Format::infer_from_href(&href).unwrap_or_default(); + let href = href.into(); + let format = Format::infer_from_href(href.as_str()).unwrap_or_default(); format.get_opts(href, options).await } @@ -244,10 +245,10 @@ mod tests { #[test] $(#[$meta])? fn $function() { - use crate::Href; + use crate::SelfHref; let value: $value = crate::read($filename).unwrap(); - assert!(value.href().is_some()); + assert!(value.self_href().is_some()); } }; } @@ -324,7 +325,7 @@ mod tests { let tempdir = TempDir::new().unwrap(); let item = Item::new("an-id"); super::write(tempdir.path().join("item.json"), item).unwrap(); - let item: Item = super::read(tempdir.path().join("item.json").to_string_lossy()).unwrap(); + let item: Item = super::read(tempdir.path().join("item.json")).unwrap(); assert_eq!(item.id, "an-id"); } @@ -338,7 +339,7 @@ mod tests { ); let item = Item::new("an-id"); assert!(super::put(path, item).await.unwrap().is_some()); - let item: Item = crate::read(tempdir.path().join("item.json").to_string_lossy()).unwrap(); + let item: Item = crate::read(tempdir.path().join("item.json")).unwrap(); assert_eq!(item.id, "an-id"); } } diff --git a/crates/core/src/item.rs b/crates/core/src/item.rs index 4035a69b..85dd04fe 100644 --- a/crates/core/src/item.rs +++ b/crates/core/src/item.rs @@ -1,11 +1,11 @@ //! STAC Items. -use crate::{Asset, Assets, Bbox, Error, Fields, Link, Result, Version, STAC_VERSION}; +use crate::{Asset, Assets, Bbox, Error, Fields, Href, Link, Result, Version, STAC_VERSION}; use chrono::{DateTime, FixedOffset, Utc}; use geojson::{feature::Id, Feature, Geometry}; use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; -use stac_derive::{Href, Links, Migrate}; +use stac_derive::{Links, Migrate, SelfHref}; use std::{collections::HashMap, path::Path}; use url::Url; @@ -27,7 +27,7 @@ const TOP_LEVEL_ATTRIBUTES: [&str; 8] = [ /// `Item` is the core object in a STAC catalog, containing the core metadata that /// enables any client to search or crawl online catalogs of spatial 'assets' /// (e.g., satellite imagery, derived data, DEMs). -#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Href, Links, Migrate)] +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, SelfHref, Links, Migrate)] #[serde(tag = "type", rename = "Feature")] pub struct Item { /// The STAC version the `Item` implements. @@ -92,7 +92,7 @@ pub struct Item { pub additional_fields: Map, #[serde(skip)] - href: Option, + self_href: Option, } /// A [FlatItem] has all of its properties at the top level. @@ -336,7 +336,7 @@ impl Item { assets: HashMap::new(), collection: None, additional_fields: Map::new(), - href: None, + self_href: None, } } diff --git a/crates/core/src/item_collection.rs b/crates/core/src/item_collection.rs index 439f8ea2..2f682857 100644 --- a/crates/core/src/item_collection.rs +++ b/crates/core/src/item_collection.rs @@ -1,13 +1,13 @@ -use crate::{Error, Item, Link, Migrate, Version}; +use crate::{Error, Href, Item, Link, Migrate, Version}; use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; -use stac_derive::{Href, Links}; +use stac_derive::{Links, SelfHref}; use std::{ops::Deref, vec::IntoIter}; /// A [GeoJSON FeatureCollection](https://www.rfc-editor.org/rfc/rfc7946#page-12) of items. /// /// While not part of the STAC specification, ItemCollections are often used to store many items in a single file. -#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Href, Links)] +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, SelfHref, Links)] #[serde(tag = "type", rename = "FeatureCollection")] pub struct ItemCollection { /// The list of [Items](Item). @@ -25,7 +25,7 @@ pub struct ItemCollection { pub additional_fields: Map, #[serde(skip)] - href: Option, + self_href: Option, } impl From> for ItemCollection { @@ -34,7 +34,7 @@ impl From> for ItemCollection { items, links: Vec::new(), additional_fields: Map::new(), - href: None, + self_href: None, } } } diff --git a/crates/core/src/json.rs b/crates/core/src/json.rs index 9e468ee5..5ee2032b 100644 --- a/crates/core/src/json.rs +++ b/crates/core/src/json.rs @@ -1,4 +1,4 @@ -use crate::{Error, Href, Result}; +use crate::{Error, Result, SelfHref}; use serde::{de::DeserializeOwned, Serialize}; use std::{ fs::File, @@ -7,7 +7,7 @@ use std::{ }; /// Create a STAC object from JSON. -pub trait FromJson: DeserializeOwned + Href { +pub trait FromJson: DeserializeOwned + SelfHref { /// Reads JSON data from a file. /// /// # Examples @@ -22,7 +22,7 @@ pub trait FromJson: DeserializeOwned + Href { let mut buf = Vec::new(); let _ = File::open(path)?.read_to_end(&mut buf)?; let mut value = Self::from_json_slice(&buf)?; - value.set_href(path.to_string_lossy()); + *value.self_href_mut() = Some(path.into()); Ok(value) } @@ -95,17 +95,21 @@ pub trait ToJson: Serialize { } } -impl FromJson for T {} +impl FromJson for T {} impl ToJson for T {} #[cfg(test)] mod tests { use super::FromJson; - use crate::{Href, Item}; + use crate::{Item, SelfHref}; #[test] fn set_href() { let item = Item::from_json_path("examples/simple-item.json").unwrap(); - assert!(item.href().unwrap().ends_with("examples/simple-item.json")); + assert!(item + .self_href() + .unwrap() + .as_str() + .ends_with("examples/simple-item.json")); } } diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 65f7e2a6..e60f7ea1 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -168,6 +168,7 @@ mod item_asset; mod item_collection; mod json; mod ndjson; +mod node; mod statistics; #[cfg(feature = "validate")] mod validate; @@ -175,7 +176,7 @@ mod value; use std::fmt::Display; -pub use stac_types::{mime, Fields, Href, Link, Links, Migrate, Version, STAC_VERSION}; +pub use stac_types::{mime, Fields, Href, Link, Links, Migrate, SelfHref, Version, STAC_VERSION}; #[cfg(feature = "validate-blocking")] pub use validate::ValidateBlocking; #[cfg(feature = "validate")] @@ -196,6 +197,7 @@ pub use { item_collection::ItemCollection, json::{FromJson, ToJson}, ndjson::{FromNdjson, ToNdjson}, + node::Node, statistics::Statistics, value::Value, }; diff --git a/crates/core/src/ndjson.rs b/crates/core/src/ndjson.rs index ddcaa4dd..c3874bdf 100644 --- a/crates/core/src/ndjson.rs +++ b/crates/core/src/ndjson.rs @@ -1,6 +1,7 @@ -use crate::{Error, FromJson, Href, Item, ItemCollection, Result, Value}; +use crate::{Error, FromJson, Item, ItemCollection, Result, Value}; use bytes::Bytes; use serde::Serialize; +use stac_types::SelfHref; use std::{ fs::File, io::{BufRead, BufReader, BufWriter, Write}, @@ -99,7 +100,7 @@ impl FromNdjson for ItemCollection { items.push(serde_json::from_str(&line?)?); } let mut item_collection = ItemCollection::from(items); - item_collection.set_href(path.to_string_lossy()); + *item_collection.self_href_mut() = Some(path.into()); Ok(item_collection) } fn from_ndjson_bytes(bytes: impl Into) -> Result { @@ -249,7 +250,7 @@ impl ToNdjson for serde_json::Value { #[cfg(test)] mod tests { use super::FromNdjson; - use crate::{Href, ItemCollection, Value}; + use crate::{ItemCollection, SelfHref, Value}; use std::{fs::File, io::Read}; #[test] @@ -257,8 +258,9 @@ mod tests { let item_collection = ItemCollection::from_ndjson_path("data/items.ndjson").unwrap(); assert_eq!(item_collection.items.len(), 2); assert!(item_collection - .href() + .self_href() .unwrap() + .as_str() .ends_with("data/items.ndjson")); } diff --git a/crates/core/src/node.rs b/crates/core/src/node.rs new file mode 100644 index 00000000..13c4720e --- /dev/null +++ b/crates/core/src/node.rs @@ -0,0 +1,239 @@ +use crate::{Catalog, Collection, Error, Href, Item, Link, Links, Result, SelfHref, 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, + + /// The node's items. + pub items: VecDeque, +} + +/// 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, + children: VecDeque, + items: VecDeque, +} + +impl Node { + /// Resolves all child and item links in this node. + /// + /// # Examples + /// + /// ``` + /// use stac::{Catalog, Node}; + /// + /// let mut node: Node = stac::read::("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.self_href().cloned(); + for mut link in links { + if link.is_child() { + if let Some(href) = &href { + link.make_absolute(href)?; + } + // TODO enable object store + tracing::debug!("resolving child: {}", link.href); + println!("resolving: {}", link.href); + let child: Container = crate::read::(link.href)?.try_into()?; + self.children.push_back(child.into()); + } else if link.is_item() { + if let Some(href) = &href { + link.make_absolute(href)?; + } + tracing::debug!("resolving item: {}", link.href); + println!("resolving: {}", link.href); + let item = crate::read::(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::>().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; + + fn next(&mut self) -> Option { + 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 for Node { + fn from(value: Catalog) -> Self { + Container::from(value).into() + } +} + +impl From for Container { + fn from(value: Catalog) -> Self { + Container::Catalog(value) + } +} + +impl From for Node { + fn from(value: Collection) -> Self { + Container::from(value).into() + } +} + +impl From for Container { + fn from(value: Collection) -> Self { + Container::Collection(value) + } +} + +impl From for Node { + fn from(value: Container) -> Self { + Node { + value, + children: VecDeque::new(), + items: VecDeque::new(), + } + } +} + +impl TryFrom for Container { + type Error = Error; + + fn try_from(value: Value) -> std::result::Result { + 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 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 { + match self { + Container::Catalog(c) => c.links_mut(), + Container::Collection(c) => c.links_mut(), + } + } +} + +impl SelfHref for Container { + fn self_href(&self) -> Option<&Href> { + match self { + Container::Catalog(c) => c.self_href(), + Container::Collection(c) => c.self_href(), + } + } + + fn self_href_mut(&mut self) -> &mut Option { + match self { + Container::Catalog(c) => c.self_href_mut(), + Container::Collection(c) => c.self_href_mut(), + } + } +} + +#[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::("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()); + } +} diff --git a/crates/core/src/value.rs b/crates/core/src/value.rs index 617f7b2b..799e24d8 100644 --- a/crates/core/src/value.rs +++ b/crates/core/src/value.rs @@ -3,6 +3,7 @@ use crate::{ }; use serde::{Deserialize, Serialize}; use serde_json::Map; +use stac_types::SelfHref; use std::convert::TryFrom; /// An enum that can hold any STAC object type. @@ -186,34 +187,24 @@ impl Value { } } -impl Href for Value { - fn href(&self) -> Option<&str> { +impl SelfHref for Value { + fn self_href(&self) -> Option<&Href> { use Value::*; match self { - Catalog(catalog) => catalog.href(), - Collection(collection) => collection.href(), - Item(item) => item.href(), - ItemCollection(item_collection) => item_collection.href(), + Catalog(catalog) => catalog.self_href(), + Collection(collection) => collection.self_href(), + Item(item) => item.self_href(), + ItemCollection(item_collection) => item_collection.self_href(), } } - fn set_href(&mut self, href: impl ToString) { + fn self_href_mut(&mut self) -> &mut Option { use Value::*; match self { - Catalog(catalog) => catalog.set_href(href), - Collection(collection) => collection.set_href(href), - Item(item) => item.set_href(href), - ItemCollection(item_collection) => item_collection.set_href(href), - } - } - - fn clear_href(&mut self) { - use Value::*; - match self { - Catalog(catalog) => catalog.clear_href(), - Collection(collection) => collection.clear_href(), - Item(item) => item.clear_href(), - ItemCollection(item_collection) => item_collection.clear_href(), + Catalog(catalog) => catalog.self_href_mut(), + Collection(collection) => collection.self_href_mut(), + Item(item) => item.self_href_mut(), + ItemCollection(item_collection) => item_collection.self_href_mut(), } } } diff --git a/crates/derive/src/lib.rs b/crates/derive/src/lib.rs index 59418c0c..c6a86a46 100644 --- a/crates/derive/src/lib.rs +++ b/crates/derive/src/lib.rs @@ -2,20 +2,17 @@ use proc_macro::TokenStream; use quote::quote; use syn::{parse_macro_input, DeriveInput}; -#[proc_macro_derive(Href)] -pub fn href_derive(input: TokenStream) -> TokenStream { +#[proc_macro_derive(SelfHref)] +pub fn self_href_derive(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); let name = input.ident; let expanded = quote! { - impl stac_types::Href for #name { - fn href(&self) -> Option<&str> { - self.href.as_deref() + impl stac_types::SelfHref for #name { + fn self_href(&self) -> Option<&stac_types::Href> { + self.self_href.as_ref() } - fn set_href(&mut self, href: impl ToString) { - self.href = Some(href.to_string()); - } - fn clear_href(&mut self) { - self.href = None; + fn self_href_mut(&mut self) -> &mut Option { + &mut self.self_href } } }; diff --git a/crates/server/src/api.rs b/crates/server/src/api.rs index 57706360..04188676 100644 --- a/crates/server/src/api.rs +++ b/crates/server/src/api.rs @@ -90,8 +90,8 @@ impl Api { /// ``` pub async fn root(&self) -> Result { let mut catalog = Catalog::new(&self.id, &self.description); - catalog.set_link(Link::root(&self.root).json()); - catalog.set_link(Link::self_(&self.root).json()); + catalog.set_link(Link::root(self.root.clone()).json()); + catalog.set_link(Link::self_(self.root.clone()).json()); catalog.set_link( Link::new(self.url("/api")?, "service-desc") .r#type(APPLICATION_OPENAPI_3_0.to_string()), @@ -107,9 +107,11 @@ impl Api { .push(Link::child(self.url(&format!("/collections/{}", collection.id))?).json()); } let search_url = self.url("/search")?; - catalog - .links - .push(Link::new(&search_url, "search").geojson().method("GET")); + catalog.links.push( + Link::new(search_url.clone(), "search") + .geojson() + .method("GET"), + ); catalog .links .push(Link::new(search_url, "search").geojson().method("POST")); @@ -151,7 +153,7 @@ impl Api { /// ``` pub async fn collections(&self) -> Result { let mut collections: Collections = self.backend.collections().await?.into(); - collections.set_link(Link::root(&self.root).json()); + collections.set_link(Link::root(self.root.clone()).json()); collections.set_link(Link::self_(self.url("/collections")?).json()); for collection in collections.collections.iter_mut() { self.set_collection_links(collection)?; @@ -205,7 +207,7 @@ impl Api { if let Some(mut item_collection) = self.backend.items(collection_id, items.clone()).await? { let collection_url = self.url(&format!("/collections/{}", collection_id))?; let items_url = self.url(&format!("/collections/{}/items", collection_id))?; - item_collection.set_link(Link::root(&self.root).json()); + item_collection.set_link(Link::root(self.root.clone()).json()); item_collection.set_link(Link::self_(items_url.clone()).geojson()); item_collection.set_link(Link::collection(collection_url).json()); if let Some(next) = item_collection.next.take() { @@ -254,7 +256,7 @@ impl Api { /// ``` pub async fn item(&self, collection_id: &str, item_id: &str) -> Result> { if let Some(mut item) = self.backend.item(collection_id, item_id).await? { - item.set_link(Link::root(&self.root).json()); + item.set_link(Link::root(self.root.clone()).json()); item.set_link( Link::self_( self.url(&format!("/collections/{}/items/{}", collection_id, item_id))?, @@ -286,7 +288,7 @@ impl Api { /// ``` pub async fn search(&self, search: Search, method: Method) -> Result { let mut item_collection = self.backend.search(search.clone()).await?; - item_collection.set_link(Link::root(&self.root).json()); + item_collection.set_link(Link::root(self.root.clone()).json()); let search_url = self.url("/search")?; if let Some(next) = item_collection.next.take() { item_collection.set_link(self.pagination_link( @@ -308,10 +310,10 @@ impl Api { } fn set_collection_links(&self, collection: &mut Collection) -> Result<()> { - collection.set_link(Link::root(&self.root).json()); + collection.set_link(Link::root(self.root.clone()).json()); collection .set_link(Link::self_(self.url(&format!("/collections/{}", collection.id))?).json()); - collection.set_link(Link::parent(&self.root).json()); + collection.set_link(Link::parent(self.root.clone()).json()); collection.set_link( Link::new( self.url(&format!("/collections/{}/items", collection.id))?, @@ -368,15 +370,15 @@ impl Api { let _ = item.insert("links".to_string(), Value::Array(Vec::new())); } let links = item.get_mut("links").unwrap().as_array_mut().unwrap(); - links.push(serde_json::to_value(Link::root(&self.root).json())?); + links.push(serde_json::to_value(Link::root(self.root.clone()).json())?); if let Some(item_link) = item_link { links.push(item_link); } if let Some(collection_url) = collection_url { links.push(serde_json::to_value( - Link::collection(&collection_url).json(), + Link::collection(collection_url.clone()).json(), )?); - links.push(serde_json::to_value(Link::parent(&collection_url).json())?); + links.push(serde_json::to_value(Link::parent(collection_url).json())?); } Ok(()) } diff --git a/crates/types/Cargo.toml b/crates/types/Cargo.toml index df0c1ed7..6ecd9755 100644 --- a/crates/types/Cargo.toml +++ b/crates/types/Cargo.toml @@ -9,12 +9,16 @@ license.workspace = true categories.workspace = true rust-version.workspace = true +[features] +reqwest = ["dep:reqwest"] + [dependencies] mime.workspace = true +reqwest = { workspace = true, optional = true } serde = { workspace = true, features = ["derive"] } serde_json.workspace = true thiserror.workspace = true -url.workspace = true +url = { workspace = true, features = ["serde"] } tracing.workspace = true [dev-dependencies] diff --git a/crates/types/src/href.rs b/crates/types/src/href.rs index ba827931..dec2eaa8 100644 --- a/crates/types/src/href.rs +++ b/crates/types/src/href.rs @@ -1,53 +1,330 @@ -/// Implemented by all three STAC objects, the [Href] trait allows getting and setting an object's href. +use crate::{Error, Result}; +use serde::{Deserialize, Serialize}; +use std::{ + fmt::Display, + path::{Path, PathBuf}, +}; +use url::Url; + +/// An href. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(untagged)] +pub enum Href { + /// A url href. + /// + /// This _can_ have a `file:` scheme. + Url(Url), + + /// A string href. + /// + /// This is expected to have `/` delimiters. Windows-style `\` delimiters are not supported. + String(String), +} + +#[derive(Debug)] +pub enum RealizedHref { + /// A path buf + PathBuf(PathBuf), + + /// A url + Url(Url), +} + +/// Implemented by all three STAC objects, the [SelfHref] trait allows getting +/// and setting an object's href. /// -/// Though the href isn't part of the data structure, it is useful to know where a given STAC object was read from. -/// Objects created from scratch don't have an href. +/// Though the self href isn't part of the data structure, it is useful to know +/// where a given STAC object was read from. Objects created from scratch don't +/// have an href. /// /// # Examples /// /// ``` -/// use stac::{Item, Href}; +/// use stac::{Item, SelfHref}; /// /// let item = Item::new("an-id"); -/// assert!(item.href().is_none()); +/// assert!(item.self_href().is_none()); /// let item: Item = stac::read("examples/simple-item.json").unwrap(); -/// assert!(item.href().is_some()); +/// assert!(item.self_href().is_some()); /// ``` -pub trait Href { +pub trait SelfHref { /// Gets this object's href. /// /// # Examples /// /// ``` - /// use stac::{Href, Item}; + /// use stac::{SelfHref, Item}; /// /// let item: Item = stac::read("examples/simple-item.json").unwrap(); - /// assert_eq!(item.href(), Some("examples/simple-item.json")); + /// assert!(item.self_href().unwrap().to_string().ends_with("simple-item.json")); /// ``` - fn href(&self) -> Option<&str>; + fn self_href(&self) -> Option<&Href>; - /// Sets this object's href. + /// Returns a mutable reference to this object's self href. /// /// # Examples /// /// ``` - /// use stac::{Item, Href}; + /// use stac::{Item, SelfHref}; /// /// let mut item = Item::new("an-id"); - /// item.set_href("http://stac.test/item.json"); + /// *item.self_href_mut() = Option::Some("./a/relative/path.json".into()); /// ``` - fn set_href(&mut self, href: impl ToString); + fn self_href_mut(&mut self) -> &mut Option; +} - /// Clears this object's href. +impl Href { + /// Convert this href into an absolute href using the given base. /// /// # Examples /// /// ``` - /// use stac::{Href, Item}; + /// use stac::Href; /// - /// let mut item: Item = stac::read("examples/simple-item.json").unwrap(); - /// item.clear_href(); - /// assert!(item.href().is_none()); + /// let href = Href::from("./a/b.json").absolute(&"/c/d/e.json".into()).unwrap(); + /// assert_eq!(href, "/c/d/a/b.json"); /// ``` - fn clear_href(&mut self); + pub fn absolute(&self, base: &Href) -> Result { + tracing::debug!("making href={self} absolute with base={base}"); + match base { + Href::Url(url) => url.join(self.as_str()).map(Href::Url).map_err(Error::from), + Href::String(s) => Ok(Href::String(make_absolute(self.as_str(), s))), + } + } + + /// Convert this href into an relative href using to the given base. + /// + /// # Examples + /// + /// ``` + /// use stac::Href; + /// + /// let href = Href::from("/a/b/c.json").relative(&"/a/d.json".into()).unwrap(); + /// assert_eq!(href, "./b/c.json"); + /// ``` + pub fn relative(&self, base: &Href) -> Result { + tracing::debug!("making href={self} relative with base={base}"); + match base { + Href::Url(base) => match self { + Href::Url(url) => Ok(base + .make_relative(url) + .map(Href::String) + .unwrap_or_else(|| self.clone())), + Href::String(s) => { + let url = s.parse()?; + Ok(base + .make_relative(&url) + .map(Href::String) + .unwrap_or_else(|| self.clone())) + } + }, + Href::String(s) => Ok(Href::String(make_relative(self.as_str(), s))), + } + } + + /// Returns true if this href is absolute. + /// + /// Urls are always absolute. Strings are absolute if they start with a `/`. + pub fn is_absolute(&self) -> bool { + match self { + Href::Url(_) => true, + Href::String(s) => s.starts_with('/'), + } + } + + /// Returns this href as a str. + pub fn as_str(&self) -> &str { + match self { + Href::Url(url) => url.as_str(), + Href::String(s) => s.as_str(), + } + } + + /// If the url scheme is `file`, convert it to a path string. + pub fn realize(self) -> RealizedHref { + match self { + Href::Url(url) => { + if url.scheme() == "file" { + url.to_file_path() + .map(RealizedHref::PathBuf) + .unwrap_or_else(|_| RealizedHref::Url(url)) + } else { + RealizedHref::Url(url) + } + } + Href::String(s) => RealizedHref::PathBuf(PathBuf::from(s)), + } + } +} + +impl Display for Href { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Href::Url(url) => url.fmt(f), + Href::String(s) => s.fmt(f), + } + } +} + +impl From<&str> for Href { + fn from(value: &str) -> Self { + if let Ok(url) = Url::parse(value) { + Href::Url(url) + } else { + Href::String(value.to_string()) + } + } +} + +impl From for Href { + fn from(value: String) -> Self { + if let Ok(url) = Url::parse(&value) { + Href::Url(url) + } else { + Href::String(value) + } + } +} + +impl From<&Path> for Href { + fn from(value: &Path) -> Self { + if cfg!(target_os = "windows") { + if let Ok(url) = Url::from_file_path(value) { + Href::Url(url) + } else { + Href::String(value.to_string_lossy().into_owned()) + } + } else { + Href::String(value.to_string_lossy().into_owned()) + } + } +} + +impl From for Href { + fn from(value: PathBuf) -> Self { + if cfg!(target_os = "windows") { + if let Ok(url) = Url::from_file_path(&value) { + Href::Url(url) + } else { + Href::String(value.to_string_lossy().into_owned()) + } + } else { + Href::String(value.to_string_lossy().into_owned()) + } + } +} + +#[cfg(feature = "reqwest")] +impl From for Href { + fn from(value: reqwest::Url) -> Self { + Href::Url(url::Url::from(value)) + } +} + +#[cfg(not(feature = "reqwest"))] +impl From for Href { + fn from(value: Url) -> Self { + Href::Url(value) + } +} + +impl PartialEq<&str> for Href { + fn eq(&self, other: &&str) -> bool { + self.as_str().eq(*other) + } +} + +fn make_absolute(href: &str, base: &str) -> String { + // TODO if we make this interface public, make this an impl Option + if href.starts_with('/') { + href.to_string() + } else { + let (base, _) = base.split_at(base.rfind('/').unwrap_or(0)); + if base.is_empty() { + normalize_path(&href) + } else { + normalize_path(&format!("{}/{}", base, href)) + } + } +} + +fn normalize_path(path: &str) -> String { + let mut parts = if path.starts_with('/') { + Vec::new() + } else { + vec![""] + }; + for part in path.split('/') { + match part { + "." => {} + ".." => { + let _ = parts.pop(); + } + s => parts.push(s), + } + } + parts.join("/") +} + +fn make_relative(href: &str, base: &str) -> String { + // Cribbed from `Url::make_relative` + let mut relative = String::new(); + + fn extract_path_filename(s: &str) -> (&str, &str) { + let last_slash_idx = s.rfind('/').unwrap_or(0); + let (path, filename) = s.split_at(last_slash_idx); + if filename.is_empty() { + (path, "") + } else { + (path, &filename[1..]) + } + } + + let (base_path, base_filename) = extract_path_filename(base); + let (href_path, href_filename) = extract_path_filename(href); + + let mut base_path = base_path.split('/').peekable(); + let mut href_path = href_path.split('/').peekable(); + + while base_path.peek().is_some() && base_path.peek() == href_path.peek() { + let _ = base_path.next(); + let _ = href_path.next(); + } + + for base_path_segment in base_path { + if base_path_segment.is_empty() { + break; + } + + if !relative.is_empty() { + relative.push('/'); + } + + relative.push_str(".."); + } + + for href_path_segment in href_path { + if relative.is_empty() { + relative.push_str("./"); + } else { + relative.push('/'); + } + + relative.push_str(href_path_segment); + } + + if !relative.is_empty() || base_filename != href_filename { + if href_filename.is_empty() { + relative.push('/'); + } else { + if relative.is_empty() { + relative.push_str("./"); + } else { + relative.push('/'); + } + relative.push_str(href_filename); + } + } + + relative } diff --git a/crates/types/src/lib.rs b/crates/types/src/lib.rs index 7352a03d..1ec46f0e 100644 --- a/crates/types/src/lib.rs +++ b/crates/types/src/lib.rs @@ -11,7 +11,7 @@ pub type Result = std::result::Result; pub use { error::Error, fields::Fields, - href::Href, + href::{Href, RealizedHref, SelfHref}, link::{Link, Links}, migrate::Migrate, version::Version, diff --git a/crates/types/src/link.rs b/crates/types/src/link.rs index 95438ac5..ed6372db 100644 --- a/crates/types/src/link.rs +++ b/crates/types/src/link.rs @@ -1,10 +1,9 @@ //! Links. -use crate::{mime::APPLICATION_GEOJSON, Error, Href, Result}; +use crate::{mime::APPLICATION_GEOJSON, Error, Href, Result, SelfHref}; use mime::APPLICATION_JSON; use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; -use url::Url; /// Child links. pub const CHILD_REL: &str = "child"; @@ -37,7 +36,7 @@ pub struct Link { /// The actual link in the format of an URL. /// /// Relative and absolute links are both allowed. - pub href: String, + pub href: Href, /// Relationship between the current document and the linked document. /// @@ -88,7 +87,7 @@ pub struct Link { } /// Implemented by any object that has links. -pub trait Links: Href { +pub trait Links: SelfHref { /// Returns a reference to this object's links. /// /// # Examples @@ -217,23 +216,11 @@ pub trait Links: Href { Box::new(self.links().iter().filter(|link| link.is_item())) } - /// Makes all relative links absolute with respect to this object's href. - /// - /// # Examples - /// - /// ``` - /// use stac::{Links, Catalog, Error, Href}; - /// - /// let mut catalog: stac::Catalog = stac::read("examples/catalog.json").unwrap(); - /// assert!(!catalog.root_link().unwrap().is_absolute()); - /// catalog.make_relative_links_absolute().unwrap(); - /// assert!(catalog.root_link().unwrap().is_absolute()); - /// ``` - fn make_relative_links_absolute(&mut self) -> Result<()> { - if let Some(href) = self.href() { - let href = make_absolute(href.to_string(), None)?; + /// Makes all relative links absolute with respect to this object's self href. + fn make_links_absolute(&mut self) -> Result<()> { + if let Some(href) = self.self_href().cloned() { for link in self.links_mut() { - link.href = make_absolute(std::mem::take(&mut link.href), Some(&href))?; + link.make_absolute(&href)?; } Ok(()) } else { @@ -241,29 +228,16 @@ pub trait Links: Href { } } - /// Makes all absolute links relative with respect to an href. - /// - /// If they do not share a root, the link will be made absolute. - /// - /// # Examples - /// - /// ``` - /// use stac::{Links, Catalog, Error, Href}; - /// - /// let mut catalog: stac::Catalog = stac::read("examples/catalog.json").unwrap(); - /// assert!(!catalog.root_link().unwrap().is_absolute()); - /// catalog.make_relative_links_absolute().unwrap(); - /// assert!(catalog.root_link().unwrap().is_absolute()); - /// catalog.make_absolute_links_relative("examples/catalog.json").unwrap(); - /// assert!(catalog.root_link().unwrap().is_relative()); - /// ``` - fn make_absolute_links_relative(&mut self, href: impl ToString) -> Result<()> { - let href = make_absolute(href.to_string(), None)?; - for link in self.links_mut() { - let absolute_link_href = make_absolute(std::mem::take(&mut link.href), Some(&href))?; - link.href = make_relative(&absolute_link_href, &href); + /// Makes all links relative with respect to this object's self href. + fn make_links_relative(&mut self) -> Result<()> { + if let Some(href) = self.self_href().cloned() { + for link in self.links_mut() { + link.make_relative(&href)?; + } + Ok(()) + } else { + Err(Error::NoHref) } - Ok(()) } /// Removes all relative links. @@ -314,9 +288,9 @@ impl Link { /// assert_eq!(link.href, "an-href"); /// assert_eq!(link.rel, "a-rel"); /// ``` - pub fn new(href: impl ToString, rel: impl ToString) -> Link { + pub fn new(href: impl Into, rel: impl ToString) -> Link { Link { - href: href.to_string(), + href: href.into(), rel: rel.to_string(), r#type: None, title: None, @@ -426,7 +400,7 @@ impl Link { /// assert!(link.is_root()); /// assert_eq!(link.r#type.as_ref().unwrap(), ::mime::APPLICATION_JSON.as_ref()); /// ``` - pub fn root(href: impl ToString) -> Link { + pub fn root(href: impl Into) -> Link { Link::new(href, ROOT_REL).json() } @@ -440,7 +414,7 @@ impl Link { /// assert!(link.is_self()); /// assert_eq!(link.r#type.as_ref().unwrap(), ::mime::APPLICATION_JSON.as_ref()); /// ``` - pub fn self_(href: impl ToString) -> Link { + pub fn self_(href: impl Into) -> Link { Link::new(href, SELF_REL).json() } @@ -454,7 +428,7 @@ impl Link { /// assert!(link.is_child()); /// assert_eq!(link.r#type.as_ref().unwrap(), ::mime::APPLICATION_JSON.as_ref()); /// ``` - pub fn child(href: impl ToString) -> Link { + pub fn child(href: impl Into) -> Link { Link::new(href, CHILD_REL).json() } @@ -468,7 +442,7 @@ impl Link { /// assert!(link.is_item()); /// assert_eq!(link.r#type.as_ref().unwrap(), ::mime::APPLICATION_JSON.as_ref()); /// ``` - pub fn item(href: impl ToString) -> Link { + pub fn item(href: impl Into) -> Link { Link::new(href, ITEM_REL).json() } @@ -482,7 +456,7 @@ impl Link { /// assert!(link.is_parent()); /// assert_eq!(link.r#type.as_ref().unwrap(), ::mime::APPLICATION_JSON.as_ref()); /// ``` - pub fn parent(href: impl ToString) -> Link { + pub fn parent(href: impl Into) -> Link { Link::new(href, PARENT_REL).json() } @@ -496,7 +470,7 @@ impl Link { /// assert!(link.is_collection()); /// assert_eq!(link.r#type.as_ref().unwrap(), ::mime::APPLICATION_JSON.as_ref()); /// ``` - pub fn collection(href: impl ToString) -> Link { + pub fn collection(href: impl Into) -> Link { Link::new(href, COLLECTION_REL).json() } @@ -635,7 +609,7 @@ impl Link { /// assert!(!Link::new("./not/an/absolute/path", "rel").is_absolute()); /// ``` pub fn is_absolute(&self) -> bool { - is_absolute(&self.href) + self.href.is_absolute() } /// Returns true if this link's href is a relative path. @@ -650,7 +624,7 @@ impl Link { /// assert!(Link::new("./not/an/absolute/path", "rel").is_relative()); /// ``` pub fn is_relative(&self) -> bool { - !is_absolute(&self.href) + !self.href.is_absolute() } /// Sets the method attribute on this link. @@ -688,115 +662,30 @@ impl Link { }), } } -} - -fn is_absolute(href: &str) -> bool { - Url::parse(href).is_ok() || href.starts_with('/') -} - -fn make_absolute(href: String, base: Option<&str>) -> Result { - // TODO if we make this interface public, make this an impl Option - if is_absolute(&href) { - Ok(href) - } else if let Some(base) = base { - if let Ok(base) = Url::parse(base) { - base.join(&href) - .map(|url| url.to_string()) - .map_err(Error::from) - } else { - let (base, _) = base.split_at(base.rfind('/').unwrap_or(0)); - if base.is_empty() { - Ok(normalize_path(&href)) - } else { - Ok(normalize_path(&format!("{}/{}", base, href))) - } - } - } else { - std::fs::canonicalize(href) - .map(|p| p.to_string_lossy().into_owned()) - .map_err(Error::from) - } -} - -fn make_relative(href: &str, base: &str) -> String { - // Cribbed from `Url::make_relative` - let mut relative = String::new(); - - fn extract_path_filename(s: &str) -> (&str, &str) { - let last_slash_idx = s.rfind('/').unwrap_or(0); - let (path, filename) = s.split_at(last_slash_idx); - if filename.is_empty() { - (path, "") - } else { - (path, &filename[1..]) - } - } - - let (base_path, base_filename) = extract_path_filename(base); - let (href_path, href_filename) = extract_path_filename(href); - - let mut base_path = base_path.split('/').peekable(); - let mut href_path = href_path.split('/').peekable(); - - while base_path.peek().is_some() && base_path.peek() == href_path.peek() { - let _ = base_path.next(); - let _ = href_path.next(); - } - - for base_path_segment in base_path { - if base_path_segment.is_empty() { - break; - } - - if !relative.is_empty() { - relative.push('/'); - } - relative.push_str(".."); - } - - for href_path_segment in href_path { - if relative.is_empty() { - relative.push_str("./"); - } else { - relative.push('/'); - } - - relative.push_str(href_path_segment); - } - - if !relative.is_empty() || base_filename != href_filename { - if href_filename.is_empty() { - relative.push('/'); - } else { - if relative.is_empty() { - relative.push_str("./"); - } else { - relative.push('/'); - } - relative.push_str(href_filename); - } + /// Makes this link absolute. + /// + /// If the href is relative, use the passed in value as a base. + /// + /// # Examples + /// + /// ``` + /// use stac::Link; + /// + /// let mut link = Link::new("./b/item.json", "rel"); + /// link.make_absolute(&"/a/base/catalog.json".into()).unwrap(); + /// assert_eq!(link.href, "/a/base/b/item.json") + /// ``` + pub fn make_absolute(&mut self, base: &Href) -> Result<()> { + self.href = self.href.absolute(base)?; + Ok(()) } - relative -} - -fn normalize_path(path: &str) -> String { - let mut parts = if path.starts_with('/') { - Vec::new() - } else { - vec![""] - }; - for part in path.split('/') { - match part { - "." => {} - ".." => { - let _ = parts.pop(); - } - s => parts.push(s), - } + /// Makes this link relative + pub fn make_relative(&mut self, base: &Href) -> Result<()> { + self.href = self.href.relative(base)?; + Ok(()) } - parts.join("/") } #[cfg(test)] @@ -821,7 +710,7 @@ mod tests { } mod links { - use stac::{Catalog, Href, Item, Link, Links}; + use stac::{Catalog, Item, Link, Links}; #[test] fn link() { @@ -847,52 +736,6 @@ mod tests { assert!(item.self_link().is_some()); } - #[test] - fn make_relative_links_absolute_path() { - let mut catalog: Catalog = stac::read("examples/catalog.json").unwrap(); - catalog.make_relative_links_absolute().unwrap(); - for link in catalog.links() { - assert!(link.is_absolute()); - } - } - - #[test] - fn make_relative_links_absolute_url() { - let mut catalog: Catalog = stac::read("examples/catalog.json").unwrap(); - catalog.set_href("http://stac-rs.test/catalog.json"); - catalog.make_relative_links_absolute().unwrap(); - for link in catalog.links() { - assert!(link.is_absolute()); - } - assert_eq!( - catalog.root_link().unwrap().href, - "http://stac-rs.test/catalog.json" - ); - } - - #[test] - fn make_absolute_links_relative_path() { - let mut catalog: Catalog = stac::read("examples/catalog.json").unwrap(); - catalog.make_relative_links_absolute().unwrap(); - catalog.make_absolute_links_relative("examples/").unwrap(); - for link in catalog.links() { - if !link.is_self() { - assert!(link.is_relative(), "{}", link.href); - } - } - } - - #[test] - fn make_absolute_links_relative_url() { - let mut catalog: Catalog = stac::read("examples/catalog.json").unwrap(); - catalog.set_href("http://stac-rs.test/catalog.json"); - catalog.make_relative_links_absolute().unwrap(); - catalog - .make_absolute_links_relative("http://stac-rs.test/") - .unwrap(); - assert_eq!(catalog.root_link().unwrap().href, "./catalog.json"); - } - #[test] fn remove_relative_links() { let mut catalog = Catalog::new("an-id", "a description");