commit f39a88b301f5b264e2519ceb120e375f25f49007 Author: minneelyyyy Date: Tue Oct 28 19:56:16 2025 -0400 initial v0.3.0 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0b745e2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +.env \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..31e2626 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,32 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "hashbrown" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" + +[[package]] +name = "indexmap" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "urigen" +version = "0.3.0" +dependencies = [ + "indexmap", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..48b2bce --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "urigen" +version = "0.3.0" +edition = "2024" + +[dependencies] +indexmap = "2.6.0" diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..adf187f --- /dev/null +++ b/src/error.rs @@ -0,0 +1,18 @@ +use std::{error, fmt}; + +#[derive(Debug)] +pub enum Error { + SchemeRequired, + PathRequired, +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::SchemeRequired => write!(f, "A scheme is required in order to construct a URI"), + Self::PathRequired => write!(f, "A path is required in order to construct a URI"), + } + } +} + +impl error::Error for Error {} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..4b3e506 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,166 @@ +mod error; +pub use error::Error; + +use indexmap::IndexMap; + +type Result = std::result::Result; + +/// Represents an authority of a URI. +/// +/// ``` +/// use urigen::Authority; +/// +/// let auth = Authority::new("example.com") +/// .userinfo("admin") +/// .port(100) +/// .build(); +/// +/// assert_eq!(auth, "admin@example.com:100"); +/// ``` +pub struct Authority { + userinfo: Option, + host: String, + port: Option, +} + +impl Authority { + /// Instantiates a new Authority. + pub fn new(host: &str) -> Self { + Self { + userinfo: None, + host: host.to_string(), + port: None, + } + } + + /// Sets the user info of the Authority. + pub fn userinfo(mut self, info: &str) -> Self { + self.userinfo = Some(info.to_string()); + self + } + + /// Sets the port of the Authority + pub fn port(mut self, port: u16) -> Self { + self.port = Some(port); + self + } + + /// Convert an Authority into a String representation. + pub fn build(self) -> String { + let userinfo = self.userinfo.map(|x| format!("{x}@")).unwrap_or("".to_string()); + let host = self.host; + let port = self.port.map(|x| format!(":{x}")).unwrap_or("".to_string()); + + format!("{userinfo}{host}{port}") + } +} + +/// A builder type for a URI +/// +/// ``` +/// use urigen::{URI, Authority}; +/// +/// let uri = URI::new() +/// .scheme("https") +/// .authority(Authority::new("example.com")) +/// .path("/index.html") +/// .build() +/// .unwrap(); +/// +/// assert_eq!(uri, "https://example.com/index.html".to_string()); +/// ``` +pub struct URI { + scheme: Option, + authority: Option, + path: Option, + query: IndexMap, + fragment: Option, +} + +impl URI { + /// Instantiates a new URI. + pub fn new() -> Self { + Self { + scheme: None, + authority: None, + path: None, + query: IndexMap::new(), + fragment: None, + } + } + + /// Sets the scheme for the URI + pub fn scheme(mut self, scheme: &str) -> Self { + self.scheme = Some(scheme.to_string()); + self + } + + /// Sets an authority for the URI + pub fn authority(mut self, authority: Authority) -> Self { + self.authority = Some(authority); + self + } + + /// Sets the path for the URI + pub fn path(mut self, path: &str) -> Self { + self.path = Some(path.to_string()); + self + } + + /// Adds a query to the URI + /// + /// Queries will appear in the output string of a URI in the order that they are added. + /// + /// ``` + /// use urigen::{URI, Authority}; + /// + /// let uri = URI::new() + /// .scheme("https") + /// .authority(Authority::new("example.com")) + /// .path("/v1/images/search") + /// .add_query("limit", "10") + /// .add_query("api_key", "API_KEY") + /// .build() + /// .unwrap(); + /// + /// assert_eq!(uri, "https://example.com/v1/images/search?limit=10&api_key=API_KEY"); + /// ``` + pub fn add_query(mut self, key: K, value: V) -> Self { + self.query.insert(key.to_string(), value.to_string()); + self + } + + /// Helper function to add multiple queries to a URI. + /// + /// ``` + /// use urigen::{URI, Authority}; + /// + /// let uri = URI::new() + /// .scheme("https") + /// .authority(Authority::new("example.com")) + /// .path("/v1/images/search") + /// .add_queries([("limit", "10"), ("api_key", "API_KEY")]) + /// .build() + /// .unwrap(); + /// + /// assert_eq!(uri, "https://example.com/v1/images/search?limit=10&api_key=API_KEY"); + /// ``` + pub fn add_queries(self, queries: impl IntoIterator) -> Self { + queries.into_iter().fold(self, |acc, (key, value)| acc.add_query(key, value)) + } + + /// Convert a URI into a String representation. + pub fn build(self) -> Result { + let scheme = self.scheme.ok_or(error::Error::SchemeRequired)?; + let authority = self.authority.map(|x| format!("//{}", x.build())).unwrap_or("".to_string()); + let path = self.path.ok_or(error::Error::PathRequired)?; + let query = if self.query.len() > 0 { + format!("?{}", self.query.into_iter().map(|(key, value)| format!("{key}={value}")).collect::>().join("&")) + } else { + "".to_string() + }; + let fragment = self.fragment.map(|x| format!("#{x}")).unwrap_or("".to_string()); + + Ok(format!("{scheme}:{authority}{path}{query}{fragment}")) + } +} \ No newline at end of file