pvoutput-client 0.0.3

Client for the PVOutput.org API
Documentation
//! Write-only API endpoint methods.

use std::collections::HashMap;

use const_format::formatcp;
use jiff::civil::{Date, Time};
use serde::Serialize;

use crate::{
	API_BASE_URL, Client,
	parser::{parse_add_output_error, parse_add_status_error, parse_datetime_error, parse_post_system_error},
	types::{
		error::{AddOutputError, AddStatusError, DateTimeError, PostSystemError},
		params::{
			AddBatchStatusParams, AddNetBatchStatusParams, AddOutputParams, AddStatusParams, DeleteStatusParams,
			PostSystemParams,
		},
		response::{BatchStatusReport, Response},
	},
};

impl Client {
	/// Adds one or more outputs for the [`Client`]'s system.
	///
	/// If the user has [donation status](https://pvoutput.org/help/donations.html), a maximum of **one hundred** outputs
	/// can be provided at once; otherwise, only **one** may be provided.
	///
	/// [PVOutput docs](https://pvoutput.org/help/api_specification.html#add-output-service)
	///
	/// # Errors
	/// Returns [`crate::ClientError::InvalidData`] containing [`DataError::NoData`] if the `body` vector is empty, or
	/// any other [`DataError`] variant if the provided parameters are invalid.
	pub async fn add_outputs(&self, body: Vec<AddOutputParams>) -> crate::Result<Response<()>, AddOutputError> {
		if body.is_empty() {
			return Err(AddOutputError::NoData.into());
		}
		self.post_empty_csv(formatcp!("{API_BASE_URL}/addoutput.jsp"), body, parse_add_output_error).await
	}

	/// Adds a single status for the [`Client`]'s system.
	///
	/// If the user has [donation status](https://pvoutput.org/help/donations.html), statuses from up to **ninety days**
	/// in the past may be added; otherwise, statuses can only be from the past **fourteen days**.
	///
	/// [PVOutput docs](https://pvoutput.org/help/api_specification.html#add-status-service)
	pub async fn add_status(&self, body: AddStatusParams) -> crate::Result<Response<()>, AddStatusError> {
		self.post_empty(formatcp!("{API_BASE_URL}/addstatus.jsp"), body, parse_add_status_error).await
	}

	/// Functionality for the [`add_batch_status`] and [`add_batch_status_net`] endpoints.
	async fn add_batch_common(
		&self,
		regular_statuses: Option<Vec<AddBatchStatusParams>>,
		net_statuses: Option<Vec<AddNetBatchStatusParams>>,
		cumulative: bool,
	) -> crate::Result<Response<Vec<BatchStatusReport>>, AddStatusError> {
		#[derive(Serialize)]
		struct RegularBatchForm {
			data: String,
			c1: u8,
		}

		#[derive(Serialize)]
		struct NetBatchForm {
			data: String,
			n: u8,
		}

		let url = formatcp!("{API_BASE_URL}/addbatchstatus.jsp");

		let mut writer = Self::writer();
		let regular = regular_statuses.is_some();
		if let Some(statuses) = regular_statuses {
			// regular statuses with optional extended data
			for status in statuses {
				if let Some(extended_data) = status.extended_data {
					writer.serialize((status.common, extended_data))?;
				} else {
					writer.serialize(status.common)?;
				}
			}
		} else if let Some(statuses) = net_statuses {
			// net statuses
			for status in statuses {
				writer.serialize(status)?;
			}
		} else {
			panic!("At least one of regular_statuses or net_statuses must be provided!");
		}

		let data = String::from_utf8_lossy(&writer.into_inner()?).to_string().replace('\n', ";");

		if regular {
			self
				.post_multiple(
					url,
					RegularBatchForm {
						data,
						c1: cumulative.into(),
					},
					parse_add_status_error,
				)
				.await
		} else {
			self.post_multiple(url, NetBatchForm { data, n: 1 }, parse_add_status_error).await
		}
	}

	/// Adds multiple statuses for the [`Client`]'s system.
	///
	/// If the user has [donation status](https://pvoutput.org/help/donations.html), statuses may be from up to
	/// **ninety days** in the past (up from **fourteen days**), and **one hundred** statuses may be uploaded at a time
	/// (up from **thirty**).
	///
	/// This method *does not* set the `n` (net) flag.
	/// To upload net statuses, use the [`Client::add_batch_status_net`] method.
	///
	/// [PVOutput docs](https://pvoutput.org/help/api_specification.html#add-batch-status-service)
	pub async fn add_batch_status(
		&self,
		statuses: Vec<AddBatchStatusParams>,
		cumulative: bool,
	) -> crate::Result<Response<Vec<BatchStatusReport>>, AddStatusError> {
		if statuses.is_empty() {
			return Err(AddStatusError::NoData.into());
		}
		self.add_batch_common(Some(statuses), None, cumulative).await
	}

	/// Adds multiple statuses for the [`Client`]'s system.
	///
	/// If the user has [donation status](https://pvoutput.org/help/donations.html), statuses may be from up to
	/// **ninety days** in the past (up from **fourteen days**), and **one hundred** statuses may be uploaded at a time
	/// (up from **thirty**).
	///
	/// This method sets the `n` (net) flag.
	/// To upload statuses without the net flag set, use the [`Client::add_batch_status`] method.
	///
	/// [PVOutput docs](https://pvoutput.org/help/api_specification.html#add-batch-status-service)
	pub async fn add_batch_status_net(
		&self,
		statuses: Vec<AddNetBatchStatusParams>,
	) -> crate::Result<Response<Vec<BatchStatusReport>>, AddStatusError> {
		if statuses.is_empty() {
			return Err(AddStatusError::NoData.into());
		}
		self.add_batch_common(None, Some(statuses), false).await
	}

	/// Updates system configuration.
	///
	/// [PVOutput docs](https://pvoutput.org/help/api_specification.html#post-system-service)
	///
	/// # Errors
	/// In addition to the standard errors, this method can return a wide array of [`crate::ClientError::InvalidData`]
	/// variants:
	/// - [`DataError::SystemNameTooLong`]
	/// - [`DataError::MissingExtendedDataLabel`]: An extended variable update was provided with no `label`.
	///   The `u8` value carried by this variant refers to the variable that was missing its `label`.
	/// - [`DataError::MissingExtendedDataUnit`]: The same as above, but for the `unit` field.
	/// - [`DataError::ExtendedDataLabelTooLong`]
	/// - [`DataError::ExtendedDataUnitTooLong`]
	/// - [`DataError::InvalidExtendedDataField`]: The variable specified by this variant's first value had an invalid
	///   field (specified by the variant's second value).
	pub async fn post_system(&self, system: PostSystemParams) -> crate::Result<Response<()>, PostSystemError> {
		let mut form = HashMap::new();
		system.name.map(|name| form.insert(String::from("name"), name));

		for (v, update) in system.extended_variables {
			let v = v as u8;
			update.label.map(|l| form.insert(format!("v{v}l"), l));
			update.unit.map(|u| form.insert(format!("v{v}u"), u));
			update.colour.map(|c| form.insert(format!("v{v}c"), c));
			update.axis.map(|a| form.insert(format!("v{v}a"), a.to_string()));
			update.graph.map(|g| form.insert(format!("v{v}g"), g.to_string()));
		}

		self.post_empty(formatcp!("{API_BASE_URL}/postsystem.jsp"), form, parse_post_system_error).await
	}

	/// Deletes a status, or all statuses for a day.
	///
	/// If `time` is set, the specific status with the provided `date` and `time` will be deleted.
	/// Otherwise, all statuses for the given `date` will be deleted.
	///
	/// [PVOutput docs](https://pvoutput.org/help/api_specification.html#delete-status-service)
	pub async fn delete_status(&self, date: Date, time: Option<Time>) -> crate::Result<Response<()>, DateTimeError> {
		self
			.post_empty(
				formatcp!("{API_BASE_URL}/deletestatus.jsp"),
				DeleteStatusParams { d: date, t: time },
				parse_datetime_error,
			)
			.await
	}
}