skytool 0.1.0-pre.2

an experimental API client for BlueSky / ATProto
Documentation
pub(crate) mod data;
mod patch;
mod query;

pub(crate) use data::{InputData, InputSource};
pub(crate) use patch::InputPatch;
pub(crate) use query::InputQuery;

pub(crate) const INPUT_SOURCE_LONG_HELP: &str = "
If the value is '-', the value will be read from STDIN.
If the value begins with '|', the rest is a command to execute.
If the value begins with '$', the rest is an environment variable.
If the value begins with '@', the rest is a filename.
If the value begins with '!', the rest is a shell script to execute.
Otherwise, the value will be read as inline data.
";

#[derive(Debug, thiserror::Error, miette::Diagnostic)]
#[error(transparent)]
#[remain::sorted]
pub(crate) enum Error {
  #[error("FATAL CONFLICT: --input-data and --input-patch cannot both read from STDIN.")]
  ConflictingStdinUsage,
  Data(#[from] data::Error),
  Patch(#[from] crate::patch::Error),
  Query(#[from] crate::query::Error),
}

#[derive(Debug, Clone, clap::Args)]
#[group(id = "input")]
#[remain::sorted]
#[rustfmt::skip]
pub(crate) struct Input {
  #[command(flatten)]
  data: InputData,
  #[command(flatten)]
  patch: InputPatch,
  #[command(flatten)]
  query: InputQuery,
}

impl Input {
  pub(crate) fn consume<T: Clone + Send + Sync + serde::de::DeserializeOwned>(self) -> Result<T, Error> {
    let Self {
      data,
      patch,
      query: InputQuery { data: query },
      ..
    } = self;

    if data.source.is_stdin() && patch.data.source.is_stdin() {
      return Err(Error::ConflictingStdinUsage);
    }

    let value = {
      let mut value = query.evaluate(&data.consume()?)?.to_owned();

      let patch: crate::patch::Patch = patch.data.consume()?;
      patch.apply(&mut value)?;

      serde_json::from_value(value.to_owned()).map_err(data::Error::from)?
    };

    Ok(value)
  }
}

#[derive(Debug, Clone, clap::Args)]
#[group(id = "input")]
#[remain::sorted]
#[rustfmt::skip]
pub(crate) struct InputOrOptions<T> where T: Clone + Send + Sync + clap::Args {
  #[arg(
    env  = "INPUT_DATA",
    long = "input-data",
    id   = "input.data.source",
    value_name = "SOURCE",
    help_heading = "Input Options",
    help         = data::HELP,
    long_help    = data::LONG_HELP,
  )]
  data: Option<InputSource>,
  #[command(flatten)]
  inline: T,
  #[command(flatten)]
  patch: InputPatch,
  #[command(flatten)]
  query: InputQuery,
}

impl<T> InputOrOptions<T>
where
  T: Clone + Send + Sync + clap::Args + serde::de::DeserializeOwned,
{
  pub(crate) fn consume(self) -> Result<T, Error> {
    let Self {
      data,
      inline,
      patch,
      query,
      ..
    } = self;

    let value = match data {
      None => inline,
      Some(source) => Input {
        data: InputData { source },
        patch,
        query,
      }
      .consume()?,
    };

    Ok(value)
  }
}