initial v0.3.0

This commit is contained in:
2025-10-28 19:56:16 -04:00
commit f39a88b301
5 changed files with 225 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
/target
.env

32
Cargo.lock generated Normal file
View File

@@ -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",
]

7
Cargo.toml Normal file
View File

@@ -0,0 +1,7 @@
[package]
name = "urigen"
version = "0.3.0"
edition = "2024"
[dependencies]
indexmap = "2.6.0"

18
src/error.rs Normal file
View File

@@ -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 {}

166
src/lib.rs Normal file
View File

@@ -0,0 +1,166 @@
mod error;
pub use error::Error;
use indexmap::IndexMap;
type Result<T> = std::result::Result<T, Error>;
/// 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<String>,
host: String,
port: Option<u16>,
}
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<String>,
authority: Option<Authority>,
path: Option<String>,
query: IndexMap<String, String>,
fragment: Option<String>,
}
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<K: ToString, V: ToString>(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<K: ToString, V: ToString>(self, queries: impl IntoIterator<Item = (K, V)>) -> 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<String> {
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::<Vec<String>>().join("&"))
} else {
"".to_string()
};
let fragment = self.fragment.map(|x| format!("#{x}")).unwrap_or("".to_string());
Ok(format!("{scheme}:{authority}{path}{query}{fragment}"))
}
}