influxdb-config 0.8.2

A helper package for quickly setting up an influxdb client from environment variables
Documentation
use core::fmt;
use core::str::FromStr;

use compact_str::format_compact;
use compact_str::CompactString;
#[cfg(feature = "client")]
use influxdb::Client;

#[derive(Debug, Clone)]
pub struct InfluxDbUrl {
	scheme: Scheme,
	connection: CompactString,
	auth: Option<(CompactString, CompactString)>,
	database: CompactString
}

#[derive(Debug, Clone, Copy)]
enum Scheme {
	Http,
	Https
}

impl FromStr for InfluxDbUrl {
	type Err = Error;

	fn from_str(s: &str) -> Result<Self, Self::Err> {
		let Some((scheme, rest)) = s.split_once("://") else {
			return Err(Error::MissingScheme);
		};
		let scheme = match scheme {
			"http" => Scheme::Http,
			"https" => Scheme::Https,
			s => return Err(Error::InvalidScheme(s.into()))
		};

		let (username, password, rest) = match rest.split_once('@') {
			Some((auth_part, rest)) => {
				let (username, password) = match auth_part.split_once(':') {
					Some((username, password)) => (username, Some(password)),
					None => (auth_part, None)
				};
				(Some(username), password, rest)
			},
			None => (None, None, rest)
		};
		let auth = match (username, password) {
			(Some(un), Some(pw)) => Some((un, pw)),
			(Some(_), None) => return Err(Error::InvalidAuth),
			(None, Some(_)) => return Err(Error::InvalidAuth),
			(None, None) => None
		};

		let Some((rest, database)) = rest.split_once('/') else {
			return Err(Error::MissingDatabase);
		};
		if !database.bytes().all(|b| b.is_ascii_alphanumeric()) {
			return Err(Error::InvalidDatabase(database.into()));
		}

		let connection = match rest.split_once(':') {
			Some((host, port)) => {
				if port.parse::<u16>().is_err() {
					return Err(Error::InvalidPort(port.into()));
				};
				format_compact!("{host}:{port}")
			},
			None => rest.into()
		};

		Ok(Self {
			scheme,
			connection,
			auth: auth.map(|(un, pw)| (un.into(), pw.into())),
			database: database.into()
		})
	}
}

impl InfluxDbUrl {
	// Consumes self, but doesn't strictly _need_ to.  Can switch it to borrow
	//    self.database, self.username, and self.password if there's a use
	//    case that that would satisfy.
	#[cfg(feature = "client")]
	#[inline]
	pub fn client(&self) -> Client {
		let client = Client::new(format!("{}://{}", self.scheme, self.connection), self.database.as_str());
		match self.auth.as_ref() {
			None => client,
			Some((un, pw)) => client.with_auth(un.as_str(), pw.as_str())
		}
	}
}

impl fmt::Display for InfluxDbUrl {
	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
		let scheme = match self.scheme {
			Scheme::Http => "http://",
			Scheme::Https => "https://"
		};
		f.write_str(scheme)?;
		if let Some((un, pw)) = self.auth.as_ref() {
			f.write_str(un.as_str())?;
			f.write_str(":")?;
			f.write_str(pw.as_str())?;
			f.write_str("@")?;
		}
		f.write_str(self.connection.as_str())?;
		f.write_str("/")?;
		f.write_str(self.database.as_str())
	}
}

impl fmt::Display for Scheme {
	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
		match self {
			Self::Http => f.write_str("http"),
			Self::Https => f.write_str("https")
		}
	}
}

#[derive(Debug, thiserror::Error)]
pub enum Error {
	#[error("Scheme (http:// or https://) is required")]
	MissingScheme,
	#[error("Invalid scheme '{0}'; must be http or https")]
	InvalidScheme(CompactString),
	#[error("If a username is present, a password must be present, and vice versa")]
	InvalidAuth,
	#[error("Invalid port '{0}'; must be a number between 1 and 65535")]
	InvalidPort(CompactString),
	#[error("Missing database")]
	MissingDatabase,
	#[error("Invalid database '{0}'; must be alphanumeric")]
	InvalidDatabase(CompactString)
}