oc2_hlapi_v2 0.2.0

A client for the new version of OpenComputers II's HLAPI
Documentation
//! The file import export card's device and its associated types
//!
//! [OC2 Docs](https://fnuecke.github.io/oc2/manual/en_us/device/file_import_export.html)

use std::convert::Infallible;

use serde::{Deserialize, Serialize};
use thiserror::Error;
use uuid::Uuid;

use crate::{
    client::{DeviceBase, HlapiClient, HlapiClientError},
    messages::BlobMarker,
};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum State {
    Idle,
    Exporting,
    ImportRequested,
    Importing,
}

#[derive(Debug)]
/// The device for the file import export card
///
/// To prevent calls being made in the wrong order, the state of the card is tracked in this
/// wrapper. As a result, two independent wrappers to one card should not be made, and this
/// type is not [`Clone`] to make accidentally doing that harder. [`reset`] must be called after a
/// fatal card error during an export or import, before another import or export is done, to
/// resynchronize the wrapper's state with the host. On creation of a wrapper, the card is
/// automatically reset.
///
/// [`reset`]: Self::reset
pub struct FileImportExportCard {
    uuid: Uuid,
    client: HlapiClient,
    state: State,
}

/// The possible errors for device functions
#[non_exhaustive]
#[derive(Error, Debug)]
pub enum FileImportExportError {
    #[error("HLAPI client error: {0}")]
    /// HLAPI client error
    HlapiClientError(#[from] HlapiClientError),
    #[error("No one present to request the file to import from")]
    /// Cannot request import file
    CannotRequestImportFile,
    #[error("The card is in an invalid state for the requested operation")]
    /// Invalid state for operation
    InvalidState,
}

/// Info about the file chosen by the user for import
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct FileInfo {
    /// The file's name
    pub name: String,
    /// The file's size in bytes
    pub size: u32,
}

impl FileImportExportCard {
    /// Cancels an in progress import or export and returns the card to its idle state. Necessary
    /// after an error while importing or exporting.
    pub fn reset(&mut self) -> Result<(), HlapiClientError> {
        self.call::<()>("reset", &[], &[])?;
        self.state = State::Idle;
        Ok(())
    }

    /// Begin a file import by requesting a file from the users at the terminal. Call
    /// [`begin_import_file`] until it returns [`Some`] next.
    ///
    /// [`begin_import_file`]: FileImportExportCard::begin_import_file
    pub fn request_import_file(&mut self) -> Result<(), FileImportExportError> {
        if self.state != State::Idle {
            return Err(FileImportExportError::InvalidState);
        }
        if !self.call::<bool>("requestImportFile", &[], &[])?.0 {
            return Err(FileImportExportError::CannotRequestImportFile);
        }
        self.state = State::ImportRequested;
        Ok(())
    }

    /// If the user has picked a file, start reading it and return info about the file. Returns
    /// [`None`] if no file has been picked yet. Once this returns [`Some`], call [`read_import_file`]
    /// until it returns [`None`] next.
    ///
    /// [`read_import_file`]: FileImportExportCard::read_import_file
    pub fn begin_import_file(&mut self) -> Result<Option<FileInfo>, FileImportExportError> {
        if self.state != State::ImportRequested {
            return Err(FileImportExportError::InvalidState);
        }
        let res = self
            .call::<Option<FileInfo>>("beginImportFile", &[], &[])?
            .0;
        if res.is_some() {
            self.state = State::Importing;
        }
        Ok(res)
    }

    /// Reads the next chunk of the file being imported, up to 4KiB. Returns [`None`] once the file
    /// is imported, and returns to the idle state ready for another operation.
    pub fn read_import_file(&mut self) -> Result<Option<Vec<u8>>, FileImportExportError> {
        if self.state != State::Importing {
            return Err(FileImportExportError::InvalidState);
        }
        let blobs = self
            .call::<Option<BlobMarker>>("readImportFile", &[], &[])?
            .1;
        if let Some(blob) = blobs.into_iter().next() {
            Ok(Some(blob))
        } else {
            self.state = State::Idle;
            Ok(None)
        }
    }

    /// Begins exporting a file with the specified name. Call [`write_export_file`] after this to
    /// write the file data.
    ///
    /// [`write_export_file`]: FileImportExportCard::write_export_file
    pub fn begin_export_file(&mut self, name: &str) -> Result<(), FileImportExportError> {
        if self.state != State::Idle {
            return Err(FileImportExportError::InvalidState);
        }
        self.call::<()>("beginExportFile", &[&name as _], &[])?;
        self.state = State::Exporting;
        Ok(())
    }

    /// Writes file data for a file being exported, up to 512 KiB in total. Once all data is written,
    /// call [`finish_export_file`].
    ///
    /// [`finish_export_file`]: FileImportExportCard::finish_export_file
    pub fn write_export_file(&mut self, data: &[u8]) -> Result<(), FileImportExportError> {
        if self.state != State::Exporting {
            return Err(FileImportExportError::InvalidState);
        }
        self.call::<()>(
            "writeExportFile",
            &[&BlobMarker { blob_idx: 0 } as _],
            &[data],
        )?;
        Ok(())
    }

    /// Finishes an in progress export and returns to the idle state ready for another operation.
    pub fn finish_export_file(&mut self) -> Result<(), FileImportExportError> {
        if self.state != State::Exporting {
            return Err(FileImportExportError::InvalidState);
        }
        self.call::<()>("finishExportFile", &[], &[])?;
        self.state = State::Idle;
        Ok(())
    }
}

impl DeviceBase for FileImportExportCard {
    const TYPE: &'static str = "file_import_export";

    type Event = Infallible;

    fn new(client: HlapiClient, uuid: Uuid) -> Result<Self, HlapiClientError> {
        let mut slf = Self {
            client,
            uuid,
            state: State::Idle,
        };
        slf.reset()?;
        Ok(slf)
    }

    fn uuid_base(&self) -> Uuid {
        self.uuid
    }

    fn client(&self) -> &HlapiClient {
        &self.client
    }
}