pvoutput-client 0.0.3

Client for the PVOutput.org API
Documentation
//! Error types and methods.

use std::fmt::{Debug, Display};

use displaydoc::Display;
use jiff::civil::{Date, Time};

pub type Result<T, Endpoint> = std::result::Result<T, ClientError<Endpoint>>;

/// Errors that can occur when using the [`Client`](crate::Client).
///
/// ## Endpoint-Specific Errors
/// A [`ClientError`] may contain an endpoint-specific error.
///
/// Most endpoints return a [`ClientError`] with [`NoEndpointSpecificError`], indicating that no endpoint-specific
/// errors can result from using the endpoint.
///
/// Some endpoints have their own error types, such as [`AddOutputError`], which is returned by the
/// [`crate::Client::add_outputs`] endpoint.
#[derive(Debug, Display)]
#[ignore_extra_doc_attributes]
pub enum ClientError<Endpoint: Debug + Display> {
	/// The provided system ID is invalid.
	InvalidSystemId,

	/// The provided API key is invalid.
	InvalidApiKey,

	/// The provided API key has not been enabled in the Settings.
	DisabledApiKey,

	/// The provided API key is a read-only key and cannot access the requested service which updates system data.
	/// Use the standard key to update system data.
	ReadOnlyKey,

	/// The provided system ID and API key combination is invalid,
	/// or the API key is missing, invalid, or inactive.
	#[displaydoc("Bad API key.")]
	BadApiKey,

	/// The maximum number of requests per hour has been reached for the API key.
	///
	/// The rate limit will refresh at the time specified by the returned
	/// [`RateLimit`](crate::types::response::RateLimit) struct.
	RateLimitExceeded,

	/// [Donation mode](https://pvoutput.org/help/donations.html) is required for this request.
	DonationModeRequired,

	/// Another system ID was requested by an account without
	/// [donation mode](https://pvoutput.org/help/donations.html) enabled.
	InaccessibleSystemId,

	/// The requested data were not found.
	NotFound,

	/// An unrecognised unsuccessful [HTTP status code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status) was
	/// returned.
	#[displaydoc("An unrecognised unsuccessful HTTP status code of {0} was returned with this message: ({1:?})")]
	UnrecognisedStatusCode(reqwest::StatusCode, Option<String>),

	/// An error occurred while making the request.
	#[displaydoc("An error occurred while making the request: {0}")]
	Reqwest(reqwest::Error),

	/// Deserialisation error.
	#[displaydoc("Deserialisation error: {0}")]
	Parse(csv::Error),

	/// Error writing CSV data to a buffer.
	#[displaydoc("Error writing CSV data to a buffer: {0}")]
	Write(Box<csv::IntoInnerError<csv::Writer<Vec<u8>>>>),

	/// Failed to parse [rate limit](crate::types::response::RateLimit) information from the response's
	/// [HTTP headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers).
	#[displaydoc("Failed to parse rate limit information from the response's HTTP headers: {0}")]
	RateLimitParse(RateLimitError),

	/// Missing field in API response.
	MissingField,

	/// An endpoint-specific error occurred.
	#[displaydoc("An endpoint-specific error occurred: {0}")]
	EndpointSpecific(Endpoint),
}

impl<Endpoint> From<reqwest::Error> for ClientError<Endpoint>
where
	Endpoint: Debug + Display,
{
	fn from(error: reqwest::Error) -> Self { Self::Reqwest(error) }
}

impl<Endpoint> From<csv::Error> for ClientError<Endpoint>
where
	Endpoint: Debug + Display,
{
	fn from(error: csv::Error) -> Self { Self::Parse(error) }
}

impl<Endpoint> From<RateLimitError> for ClientError<Endpoint>
where
	Endpoint: Debug + Display,
{
	fn from(error: RateLimitError) -> Self { Self::RateLimitParse(error) }
}

impl<Endpoint> From<csv::IntoInnerError<csv::Writer<Vec<u8>>>> for ClientError<Endpoint>
where
	Endpoint: Debug + Display,
{
	fn from(error: csv::IntoInnerError<csv::Writer<Vec<u8>>>) -> Self { Self::Write(Box::new(error)) }
}

impl From<AddOutputError> for ClientError<AddOutputError> {
	fn from(error: AddOutputError) -> Self { Self::EndpointSpecific(error) }
}

impl From<AddStatusError> for ClientError<AddStatusError> {
	fn from(error: AddStatusError) -> Self { Self::EndpointSpecific(error) }
}

impl From<PostSystemError> for ClientError<PostSystemError> {
	fn from(error: PostSystemError) -> Self { Self::EndpointSpecific(error) }
}

impl<Endpoint> std::error::Error for ClientError<Endpoint> where Endpoint: Debug + Display {}

/// No endpoint-specific error can occur for this endpoint.
#[derive(Debug, Display, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
pub enum NoEndpointSpecificError {}

/// The provided date or time was invalid.
#[derive(Debug, Display, PartialEq, Eq, Clone)]
pub enum DateTimeError {
	/// An invalid date was provided.
	#[displaydoc("An invalid date was provided: {0}")]
	InvalidDate(String),

	/// An invalid time was provided.
	#[displaydoc("An invalid time was provided: {0}")]
	InvalidTime(String),

	/// The date is too old.
	#[displaydoc("The date \"{0}\" is too old.")]
	DateTooOld(Date),

	/// The date is too new.
	#[displaydoc("The date \"{0}\" is too new.")]
	DateTooNew(Date),

	/// No date or time was provided to an endpoint that requires a date or time.
	MissingDateOrTime,
}

impl std::error::Error for DateTimeError {}

/// Errors that can occur when adding output data to a system using [`crate::Client::add_outputs`].
#[derive(Debug, Display, PartialEq, Clone)]
pub enum AddOutputError {
	/// No data were provided.
	NoData,

	/// The provided date or time was invalid.
	InvalidDateTime(DateTimeError),

	/// The generation value is too high for the system size.
	#[displaydoc("The generation value {generation} is too high for the system size {system_size} on {date}.")]
	GenerationTooHigh {
		generation: f32,
		system_size: u32,
		date: Date,
	},

	/// The export value is too high for the system size.
	#[displaydoc("The export value {export} is too high for the system size {system_size} on {date}.")]
	ExportTooHigh { export: f32, system_size: u32, date: Date },

	/// The export value exceeds the generation value by more than 15%.
	#[displaydoc("The export value {export} exceeds the generation value {generation} by more than 15%.")]
	ExportExceedsGeneration { export: f32, generation: f32 },

	/// The consumption value is above 999,999,999 Wh for the provided date.
	#[displaydoc("The consumption value {0} is above 999,999,999 Wh on {1}.")]
	ConsumptionTooHigh(f32, Date),

	/// The peak power on the provided date is ≥50% greater than the system size.
	#[displaydoc("The peak power {peak_power} is ≥50% greater than the system size {system_size} on {date}.")]
	PeakPowerTooHigh {
		peak_power: f32,
		system_size: u32,
		date: Date,
	},

	/// A maximum temperature was specified for this date without a minimum temperature.
	#[displaydoc("A maximum temperature was specified on {0} without a minimum temperature.")]
	MaxTemperatureWithoutMin(Date),

	/// A minimum temperature was specified for this date without a maximum temperature.
	#[displaydoc("A minimum temperature was specified on {0} without a maximum temperature.")]
	MinTemperatureWithoutMax(Date),
}

impl std::error::Error for AddOutputError {}

/// Errors that can occur when adding statuses to a system using [`crate::Client::add_status`],
/// [`crate::Client::add_batch_status`], or [`crate::Client::add_batch_status_net`].
#[derive(Debug, Display, PartialEq, Eq, Clone)]
pub enum AddStatusError {
	/// No data were provided.
	NoData,

	/// The provided date or time was invalid.
	InvalidDateTime(DateTimeError),

	/// None of energy generation, power generation, energy consumption, or power consumption were provided.
	NoEnergyData,

	/// Systems cannot generate power during hours without sunlight!
	MoonPowered,

	/// Both the cumulative and net flags were set.
	BothCumulativeAndNet,

	/// The energy generation value is too high for the time period.
	#[displaydoc("The energy generation value {0} is too high for the time period {1}.")]
	EnergyTooHighForTime(u32, Time),

	/// The power generation value is too high for this system.
	#[displaydoc("The power generation value {power} is too high for this system with size {system_size}.")]
	ExcessivePowerGeneration { power: u32, system_size: u32 },

	/// The energy generation value is too high for this system.
	#[displaydoc("The energy generation value {energy} is too high for this system with size {system_size}.")]
	ExcessiveEnergyGeneration { energy: u32, system_size: u32 },

	/// Required fields were missing.
	MissingFields,

	/// Invalid data format.
	#[displaydoc("Invalid data format: {0}.")]
	InvalidDataFormat(String),
}

impl std::error::Error for AddStatusError {}

/// Errors that can occur when updating a system using [`crate::Client::post_system`].
#[derive(Debug, Display, Copy, Clone, PartialEq, Eq)]
#[ignore_extra_doc_attributes]
pub enum PostSystemError {
	/// System name is too long.
	///
	/// System names can be at most **thirty** characters long.
	SystemNameTooLong,

	/// Missing label for extended data.
	#[displaydoc("Missing label for extended data variable v{0}.")]
	MissingExtendedDataLabel(u8),

	/// Missing unit for extended data.
	#[displaydoc("Missing unit for extended data variable v{0}.")]
	MissingExtendedDataUnit(u8),

	/// Extended data label too long.
	///
	/// Extended data labels can be at most **twenty** characters long.
	#[displaydoc("Label too long for extended data variable v{0}.")]
	ExtendedDataLabelTooLong(u8),

	/// Extended data unit too long.
	///
	/// Extended data units can be at most **ten** characters long.
	#[displaydoc("Unit too long for extended data variable v{0}.")]
	ExtendedDataUnitTooLong(u8),

	/// Invalid extended data field.
	#[displaydoc("Field {1} is invalid for extended data variable v{0}.")]
	InvalidExtendedDataField(u8, ExtendedDataField),
}

impl std::error::Error for PostSystemError {}

/// [Extended data](https://pvoutput.org/help/extended_data.html) fields.
/// Used to represent invalid field values for [`PostSystemError`]s.
#[derive(Debug, Display, PartialEq, Eq, Clone, Copy, Hash)]
pub enum ExtendedDataField {
	/// Colour.
	Colour,

	/// Axis.
	Axis,

	/// Graph.
	Graph,
}

/// Errors that can occur when parsing [rate limit](crate::types::response::RateLimit) information from the response's
/// [HTTP headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers).
#[derive(Debug, Display)]
pub enum RateLimitError {
	/// A header was missing.
	MissingValues,

	/// A header contained non-ASCII characters.
	NonAsciiHeader,

	/// Failed to parse a header value as an integer.
	#[displaydoc("Failed to parse integer header value: {0}")]
	InvalidInteger(std::num::ParseIntError),

	/// Failed to parse the `reset` header value.
	#[displaydoc("Failed to parse the `reset` header value: {0}")]
	InvalidTimestamp(jiff::Error),
}

impl std::error::Error for RateLimitError {}

impl From<http::header::ToStrError> for RateLimitError {
	fn from(_: http::header::ToStrError) -> Self { Self::NonAsciiHeader }
}

impl From<std::num::ParseIntError> for RateLimitError {
	fn from(error: std::num::ParseIntError) -> Self { Self::InvalidInteger(error) }
}

impl From<jiff::Error> for RateLimitError {
	fn from(error: jiff::Error) -> Self { Self::InvalidTimestamp(error) }
}