summaryrefslogtreecommitdiffstats
path: root/sq
diff options
context:
space:
mode:
authorLars Wirzenius <liw@sequoia-pgp.org>2022-07-06 11:07:40 +0300
committerLars Wirzenius <liw@sequoia-pgp.org>2022-07-15 15:49:15 +0300
commit360d6ddb605088da78da67a6f63126c8d149595a (patch)
treea0077201b476365482bc416e70795d72a91ab6de /sq
parent545fcfb5e0deef8c0c6618fb478fc12294ecfc1d (diff)
sq: add data types for output format and version of output format
These are not yet used, this is preparation for future changes. Sponsored-by: NLnet Foundation; NGI Assure
Diffstat (limited to 'sq')
-rw-r--r--sq/src/output.rs216
-rw-r--r--sq/src/sq.rs2
2 files changed, 218 insertions, 0 deletions
diff --git a/sq/src/output.rs b/sq/src/output.rs
new file mode 100644
index 00000000..ac3f0e33
--- /dev/null
+++ b/sq/src/output.rs
@@ -0,0 +1,216 @@
+//! Data types for output format and format version choice.
+//!
+//! These data types express the values of the `--output-format` and
+//! `--output-version` global options to `sq`.
+
+use std::fmt;
+use std::str::FromStr;
+
+use anyhow::{anyhow, Result};
+use serde::Serialize;
+
+/// What output format to prefer, when there's an option?
+#[derive(Clone)]
+pub enum OutputFormat {
+ /// Output that is meant to be read by humans, instead of programs.
+ ///
+ /// This type of output has no version, and is not meant to be
+ /// parsed by programs.
+ HumanReadable,
+
+ /// Output as JSON.
+ Json,
+}
+
+impl FromStr for OutputFormat {
+ type Err = anyhow::Error;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ match s {
+ "human-readable" => Ok(Self::HumanReadable),
+ "json" => Ok(Self::Json),
+ _ => Err(anyhow!("unknown output format {:?}", s)),
+ }
+ }
+}
+
+/// What version of the output format is used or requested?
+///
+/// As `sq` evolves, the machine-readable output format may need to
+/// change. Consumers should be able to know what version of the output
+/// format has been produced. This is expressed using a three-part
+/// version number, which is always included in the output, similar to
+/// [Semantic Versions][]. The parts are known as "major", "minor",
+/// and "patch", and have the following semantics:
+///
+/// * patch: incremented if there are no semantic changes
+/// * minor: one or more fields were added
+/// * major: one or more fields were dropped
+///
+/// [Semantic Version]: https://semver.org/
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize)]
+pub struct OutputVersion {
+ major: usize,
+ minor: usize,
+ patch: usize,
+}
+
+impl OutputVersion {
+ /// Create a new version number from constituent parts.
+ pub const fn new(major: usize, minor: usize, patch: usize) -> Self {
+ Self {
+ major,
+ minor,
+ patch,
+ }
+ }
+
+ /// Does this version fulfill the needs of the version that is requested?
+ pub fn is_acceptable_for(&self, wanted: Self) -> bool {
+ self.major == wanted.major &&
+ (self.minor > wanted.minor ||
+ (self.minor == wanted.minor && self.patch >= wanted.patch))
+ }
+}
+
+impl FromStr for OutputVersion {
+ type Err = anyhow::Error;
+
+ fn from_str(v: &str) -> Result<Self, Self::Err> {
+ let ints = parse_ints(v)?;
+ match ints.len() {
+ 0 => Err(anyhow!("doesn't look like a version: {}", v)),
+ 1 => Ok(Self::new(ints[0], 0, 0)),
+ 2 => Ok(Self::new(ints[0], ints[1], 0)),
+ 3 => Ok(Self::new(ints[0], ints[1], ints[2])),
+ _ => Err(anyhow!("too many components in version (at most three allowed): {}", v)),
+ }
+ }
+}
+
+impl fmt::Display for OutputVersion {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
+ }
+}
+
+fn parse_ints(s: &str) -> Result<Vec<usize>> {
+ let mut ints = vec![];
+ let mut v = s;
+ while !v.is_empty() {
+ if let Some(i) = v.find('.') {
+ ints.push(parse_component(&v[..i])?);
+ v = &v[i+1..];
+ if v.is_empty() {
+ return Err(anyhow!("trailing dot in version: {}", s));
+ }
+ } else {
+ ints.push(parse_component(v)?);
+ v = "";
+ }
+ }
+ Ok(ints)
+}
+
+fn parse_component(s: &str) -> Result<usize> {
+ Ok(FromStr::from_str(s)?)
+}
+
+
+#[cfg(test)]
+mod test {
+ use super::{FromStr, OutputVersion};
+
+ #[test]
+ fn empty_string() {
+ assert!(OutputVersion::from_str("").is_err());
+ }
+
+ #[test]
+ fn not_int() {
+ assert!(OutputVersion::from_str("foo").is_err());
+ }
+
+ #[test]
+ fn not_int2() {
+ assert!(OutputVersion::from_str("1.foo").is_err());
+ }
+
+ #[test]
+ fn leading_dot() {
+ assert!(OutputVersion::from_str(".1").is_err());
+ }
+
+ #[test]
+ fn trailing_dot() {
+ assert!(OutputVersion::from_str("1.").is_err());
+ }
+
+ #[test]
+ fn one_int() {
+ assert_eq!(OutputVersion::from_str("1").unwrap(), OutputVersion::new(1, 0, 0));
+ }
+
+ #[test]
+ fn two_ints() {
+ assert_eq!(OutputVersion::from_str("1.2").unwrap(), OutputVersion::new(1, 2, 0));
+ }
+
+ #[test]
+ fn three_ints() {
+ assert_eq!(OutputVersion::from_str("1.2.3").unwrap(), OutputVersion::new(1, 2, 3));
+ }
+
+ #[test]
+ fn four_ints() {
+ assert!(OutputVersion::from_str("1.2.3.4").is_err());
+ }
+
+ #[test]
+ fn acceptable_if_same() {
+ let a = OutputVersion::new(0, 0, 0);
+ assert!(a.is_acceptable_for(a));
+ }
+
+ #[test]
+ fn acceptable_if_newer_patch() {
+ let wanted = OutputVersion::new(0, 0, 0);
+ let actual = OutputVersion::new(0, 0, 1);
+ assert!(actual.is_acceptable_for(wanted));
+ }
+
+ #[test]
+ fn not_acceptable_if_older_patch() {
+ let wanted = OutputVersion::new(0, 0, 1);
+ let actual = OutputVersion::new(0, 0, 0);
+ assert!(!actual.is_acceptable_for(wanted));
+ }
+
+ #[test]
+ fn acceptable_if_newer_minor() {
+ let wanted = OutputVersion::new(0, 0, 0);
+ let actual = OutputVersion::new(0, 1, 0);
+ assert!(actual.is_acceptable_for(wanted));
+ }
+
+ #[test]
+ fn not_acceptable_if_older_minor() {
+ let wanted = OutputVersion::new(0, 1, 0);
+ let actual = OutputVersion::new(0, 0, 0);
+ assert!(!actual.is_acceptable_for(wanted));
+ }
+
+ #[test]
+ fn not_acceptable_if_newer_major() {
+ let wanted = OutputVersion::new(0, 0, 0);
+ let actual = OutputVersion::new(1, 0, 0);
+ assert!(!actual.is_acceptable_for(wanted));
+ }
+
+ #[test]
+ fn not_acceptable_if_older_major() {
+ let wanted = OutputVersion::new(1, 0, 0);
+ let actual = OutputVersion::new(0, 0, 0);
+ assert!(!actual.is_acceptable_for(wanted));
+ }
+}
diff --git a/sq/src/sq.rs b/sq/src/sq.rs
index aaabbd87..7457e23e 100644
--- a/sq/src/sq.rs
+++ b/sq/src/sq.rs
@@ -30,6 +30,8 @@ use sq_cli::SqSubcommands;
mod sq_cli;
mod commands;
+mod output;
+use output::{OutputFormat, OutputVersion};
fn open_or_stdin(f: Option<&str>)
-> Result<Box<dyn BufferedReader<()>>> {