stellar-xdr 27.0.0

Stellar XDR types, encoding, and decoding.
Documentation
use std::ffi::OsString;
use std::io::{stdout, Write};
use std::{fmt::Debug, str::FromStr};

use clap::{Args, ValueEnum};
use serde::Serialize;

use crate::cli::util;

#[derive(thiserror::Error, Debug)]
pub enum Error {
    #[error("unknown type {0}, choose one of {1:?}")]
    UnknownType(String, &'static [&'static str]),
    #[error("error decoding XDR: {0}")]
    ReadXdr(#[from] crate::Error),
    #[error("error reading file: {0}")]
    ReadFile(std::io::Error),
    #[error("error writing output: {0}")]
    WriteOutput(std::io::Error),
    #[error("error generating JSON: {0}")]
    GenerateJson(#[from] serde_json::Error),
    #[error("type doesn't have a text representation, use 'json' as output")]
    TextUnsupported,
}

#[derive(Args, Debug, Clone)]
#[command()]
pub struct Cmd {
    /// XDR or files containing XDR to decode, or stdin if empty
    #[arg()]
    pub input: Vec<OsString>,

    /// XDR type to decode
    #[arg(long)]
    pub r#type: String,

    // Input format of the XDR
    #[arg(long = "input", value_enum, default_value_t)]
    pub input_format: InputFormat,

    // Output format
    #[arg(long = "output", value_enum, default_value_t)]
    pub output_format: OutputFormat,
}

#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, ValueEnum)]
pub enum InputFormat {
    Single,
    SingleBase64,
    Stream,
    StreamBase64,
    StreamFramed,
}

impl Default for InputFormat {
    fn default() -> Self {
        Self::StreamBase64
    }
}

#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, ValueEnum)]
pub enum OutputFormat {
    Json,
    JsonFormatted,
    Text,
    RustDebug,
    RustDebugFormatted,
}

impl Default for OutputFormat {
    fn default() -> Self {
        Self::Json
    }
}

// TODO: Remove run_x macro, it exists only to reduce the diff from when curr/next
// channels existed and each had their own run_curr/run_next invocation.
macro_rules! run_x {
    ($f:ident) => {
        fn $f(&self) -> Result<(), Error> {
            let mut input = util::parse_input(&self.input).map_err(Error::ReadFile)?;
            let r#type = crate::TypeVariant::from_str(&self.r#type).map_err(|_| {
                Error::UnknownType(self.r#type.clone(), &crate::TypeVariant::VARIANTS_STR)
            })?;
            for f in &mut input {
                match self.input_format {
                    InputFormat::Single => {
                        let mut l = crate::Limited::new(f, crate::Limits::none());
                        let t = crate::Type::read_xdr_to_end(r#type, &mut l)?;
                        self.out(&t)?;
                    }
                    InputFormat::SingleBase64 => {
                        let mut l = crate::Limited::new(f, crate::Limits::none());
                        let t = crate::Type::read_xdr_base64_to_end(r#type, &mut l)?;
                        self.out(&t)?;
                    }
                    InputFormat::Stream => {
                        let mut l = crate::Limited::new(f, crate::Limits::none());
                        for t in crate::Type::read_xdr_iter(r#type, &mut l) {
                            self.out(&t?)?;
                        }
                    }
                    InputFormat::StreamBase64 => {
                        let mut l = crate::Limited::new(f, crate::Limits::none());
                        for t in crate::Type::read_xdr_base64_iter(r#type, &mut l) {
                            self.out(&t?)?;
                        }
                    }
                    InputFormat::StreamFramed => {
                        let mut l = crate::Limited::new(f, crate::Limits::none());
                        for t in crate::Type::read_xdr_framed_iter(r#type, &mut l) {
                            self.out(&t?)?;
                        }
                    }
                };
            }
            Ok(())
        }
    };
}

impl Cmd {
    /// Run the CLIs decode command.
    ///
    /// ## Errors
    ///
    /// If the command is configured with state that is invalid.
    pub fn run(&self) -> Result<(), Error> {
        let result = self.run_inner();
        match result {
            Ok(()) => Ok(()),
            Err(Error::WriteOutput(e)) if e.kind() == std::io::ErrorKind::BrokenPipe => Ok(()),
            Err(e) => Err(e),
        }
    }

    run_x!(run_inner);

    fn out(&self, v: &(impl Serialize + Debug)) -> Result<(), Error> {
        let text = match self.output_format {
            OutputFormat::Json => serde_json::to_string(v)?,
            OutputFormat::JsonFormatted => serde_json::to_string_pretty(v)?,
            OutputFormat::Text => {
                let v = serde_json::to_value(v)?;
                util::serde_json_value_to_text(v).ok_or(Error::TextUnsupported)?
            }
            OutputFormat::RustDebug => format!("{v:?}"),
            OutputFormat::RustDebugFormatted => format!("{v:#?}"),
        };
        writeln!(stdout(), "{text}").map_err(Error::WriteOutput)?;
        Ok(())
    }
}