Skip to content

Commit

Permalink
Improve K/V pair API
Browse files Browse the repository at this point in the history
  • Loading branch information
Techassi committed Oct 31, 2023
1 parent 4c89e5b commit c465f5b
Show file tree
Hide file tree
Showing 3 changed files with 254 additions and 72 deletions.
221 changes: 188 additions & 33 deletions rust/stackable-cockpit/src/platform/label/key.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use std::{fmt::Display, str::FromStr};
use std::{fmt::Display, ops::Deref, str::FromStr};

use lazy_static::lazy_static;
use regex::Regex;
use snafu::{ensure, Snafu};
use snafu::{ensure, ResultExt, Snafu};

const LABEL_KEY_PREFIX_MAX_LEN: usize = 253;
const LABEL_KEY_NAME_MAX_LEN: usize = 63;
Expand All @@ -14,23 +14,62 @@ lazy_static! {
Regex::new(r"^[a-z0-9A-Z]([a-z0-9A-Z-_.]*[a-z0-9A-Z]+)?$").unwrap();
}

#[derive(Debug, Snafu)]
pub enum LabelKeyError {}
#[derive(Debug, PartialEq, Snafu)]
pub enum KeyError {
#[snafu(display("key input cannot be empty"))]
EmptyInput,

pub struct LabelKey {
prefix: Option<LabelKeyPrefix>,
name: LabelKeyName,
#[snafu(display("invalid number of slashes in key - expected 0 or 1, got {count}"))]
InvalidSlashCharCount { count: usize },

#[snafu(display("failed to parse key prefix"))]
KeyPrefixError { source: KeyPrefixError },

#[snafu(display("failed to parse key name"))]
KeyNameError { source: KeyNameError },
}

impl FromStr for LabelKey {
type Err = LabelKeyError;
#[derive(Debug, PartialEq)]
pub struct Key {
prefix: Option<KeyPrefix>,
name: KeyName,
}

impl FromStr for Key {
type Err = KeyError;

fn from_str(input: &str) -> Result<Self, Self::Err> {
todo!()
let input = input.trim();

// The input cannot be empty
ensure!(!input.is_empty(), EmptyInputSnafu);

// Split the input up into the optional prefix and name
let parts: Vec<_> = input.split('/').collect();

// Ensure we have 2 or less parts. More parts are a result of too many
// slashes
ensure!(
parts.len() <= 2,
InvalidSlashCharCountSnafu {
count: parts.len() - 1
}
);

let (prefix, name) = if parts.len() == 1 {
(None, KeyName::from_str(parts[0]).context(KeyNameSnafu)?)
} else {
(
Some(KeyPrefix::from_str(parts[0]).context(KeyPrefixSnafu)?),
KeyName::from_str(parts[1]).context(KeyNameSnafu)?,
)
};

Ok(Self { prefix, name })
}
}

impl Display for LabelKey {
impl Display for Key {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &self.prefix {
Some(prefix) => write!(f, "{}/{}", prefix, self.name),
Expand All @@ -39,8 +78,70 @@ impl Display for LabelKey {
}
}

#[derive(Debug, Snafu)]
pub enum LabelKeyPrefixError {
impl Key {
/// Retrieves the key's prefix.
///
/// ```
/// use std::str::FromStr;
/// use stackable_cockpit::platform::label::{Key, KeyPrefix};
///
/// let key = Key::from_str("stackable.tech/vendor").unwrap();
/// let prefix = KeyPrefix::from_str("stackable.tech").unwrap();
///
/// assert_eq!(key.prefix(), Some(&prefix));
/// ```
pub fn prefix(&self) -> Option<&KeyPrefix> {
self.prefix.as_ref()
}

/// Adds or replaces the key prefix. This takes a parsed and validated
/// [`KeyPrefix`] as a parameter. If instead you want to use a raw value,
/// use the [`Key::try_add_prefix()`] function instead.
pub fn add_prefix(&mut self, prefix: KeyPrefix) {
self.prefix = Some(prefix)
}

/// Adds or replaces the key prefix by parsing and validation raw input. If
/// instead you already have a parsed and validated [`KeyPrefix`], use the
/// [`Key::add_prefix()`] function instead.
pub fn try_add_prefix(&mut self, prefix: impl AsRef<str>) -> Result<&mut Self, KeyError> {
self.prefix = Some(KeyPrefix::from_str(prefix.as_ref()).context(KeyPrefixSnafu)?);
Ok(self)
}

/// Retrieves the key's name.
///
/// ```
/// use std::str::FromStr;
/// use stackable_cockpit::platform::label::{Key, KeyName};
///
/// let key = Key::from_str("stackable.tech/vendor").unwrap();
/// let name = KeyName::from_str("vendor").unwrap();
///
/// assert_eq!(key.name(), &name);
/// ```
pub fn name(&self) -> &KeyName {
&self.name
}

/// Sets the key name. This takes a parsed and validated [`KeyName`] as a
/// parameter. If instead you want to use a raw value, use the
/// [`Key::try_set_name()`] function instead.
pub fn set_name(&mut self, name: KeyName) {
self.name = name
}

/// Sets the key name by parsing and validation raw input. If instead you
/// already have a parsed and validated [`KeyName`], use the
/// [`Key::set_name()`] function instead.
pub fn try_set_name(&mut self, name: impl AsRef<str>) -> Result<&mut Self, KeyError> {
self.name = KeyName::from_str(name.as_ref()).context(KeyNameSnafu)?;
Ok(self)
}
}

#[derive(Debug, PartialEq, Snafu)]
pub enum KeyPrefixError {
#[snafu(display("prefix segment of key cannot be empty"))]
PrefixEmpty,

Expand All @@ -54,16 +155,11 @@ pub enum LabelKeyPrefixError {
PrefixInvalid,
}

pub struct LabelKeyPrefix(String);
#[derive(Debug, PartialEq)]
pub struct KeyPrefix(String);

impl Display for LabelKeyPrefix {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}

impl FromStr for LabelKeyPrefix {
type Err = LabelKeyPrefixError;
impl FromStr for KeyPrefix {
type Err = KeyPrefixError;

fn from_str(input: &str) -> Result<Self, Self::Err> {
// The prefix cannot be empty when one is provided
Expand All @@ -87,8 +183,22 @@ impl FromStr for LabelKeyPrefix {
}
}

#[derive(Debug, Snafu)]
pub enum LabelKeyNameError {
impl Deref for KeyPrefix {
type Target = String;

fn deref(&self) -> &Self::Target {
&self.0
}
}

impl Display for KeyPrefix {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}

#[derive(Debug, PartialEq, Snafu)]
pub enum KeyNameError {
#[snafu(display("name segment of key cannot be empty"))]
NameEmpty,

Expand All @@ -102,16 +212,11 @@ pub enum LabelKeyNameError {
NameInvalid,
}

pub struct LabelKeyName(String);

impl Display for LabelKeyName {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, PartialEq)]
pub struct KeyName(String);

impl FromStr for LabelKeyName {
type Err = LabelKeyNameError;
impl FromStr for KeyName {
type Err = KeyNameError;

fn from_str(input: &str) -> Result<Self, Self::Err> {
// The name cannot be empty
Expand All @@ -134,3 +239,53 @@ impl FromStr for LabelKeyName {
Ok(Self(input.to_string()))
}
}

impl Deref for KeyName {
type Target = String;

fn deref(&self) -> &Self::Target {
&self.0
}
}

impl Display for KeyName {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}

#[cfg(test)]
mod test {
use super::*;

#[test]
fn key_with_prefix() {
let key = Key::from_str("stackable.tech/vendor").unwrap();

assert_eq!(key.prefix, Some(KeyPrefix("stackable.tech".into())));
assert_eq!(key.name, KeyName("vendor".into()));
assert_eq!(key.to_string(), "stackable.tech/vendor");
}

#[test]
fn key_without_prefix() {
let key = Key::from_str("vendor").unwrap();

assert_eq!(key.prefix, None);
assert_eq!(key.name, KeyName("vendor".into()));
assert_eq!(key.to_string(), "vendor");
}

#[test]
fn invalid_key_prefix() {
// TODO (Techassi): Add more invalid test cases
let err = Key::from_str("stackable/vendor").unwrap_err();

assert_eq!(
err,
KeyError::KeyPrefixError {
source: KeyPrefixError::PrefixInvalid
}
)
}
}
63 changes: 41 additions & 22 deletions rust/stackable-cockpit/src/platform/label/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,28 +9,28 @@ pub use key::*;
pub use value::*;

#[derive(Debug, Snafu)]
pub enum LabelParseError {
pub enum KeyPairError {
#[snafu(display("label input cannot be empty"))]
EmptyInput,

#[snafu(display("invalid number of equal signs - expected exactly 1, got {signs}"))]
InvalidEqualSignCount { signs: usize },

#[snafu(display("failed to parse label key"))]
LabelKeyError { source: LabelKeyError },
KeyError { source: KeyError },

#[snafu(display("failed to parse label value"))]
LabelValueError { source: LabelValueError },
ValueError { source: ValueError },
}

// NOTE (Techassi): This should probably be upstreamed into operator-rs
pub struct Label {
key: LabelKey,
value: LabelValue,
pub struct KeyValuePair {
key: Key,
value: Value,
}

impl FromStr for Label {
type Err = LabelParseError;
impl FromStr for KeyValuePair {
type Err = KeyPairError;

fn from_str(input: &str) -> Result<Self, Self::Err> {
let input = input.trim();
Expand All @@ -49,44 +49,63 @@ impl FromStr for Label {
);

// Parse key and value parts
let key = LabelKey::from_str(parts[0]).context(LabelKeySnafu)?;
let value = LabelValue::new(parts[1]).context(LabelValueSnafu)?;
let key = Key::from_str(parts[0]).context(KeySnafu)?;
let value = Value::from_str(parts[1]).context(ValueSnafu)?;

Ok(Self { key, value })
}
}

impl Display for Label {
impl Display for KeyValuePair {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}={}", self.key, self.value)
}
}

impl Label {
pub fn new(key: LabelKey, value: LabelValue) -> Self {
impl KeyValuePair {
pub fn new(key: Key, value: Value) -> Self {
Self { key, value }
}

pub fn key(&self) -> &Key {
&self.key
}

pub fn value(&self) -> &Value {
&self.value
}
}

macro_rules! kvp {

Check failure on line 79 in rust/stackable-cockpit/src/platform/label/mod.rs

View workflow job for this annotation

GitHub Actions / clippy

[clippy] rust/stackable-cockpit/src/platform/label/mod.rs#L79

error: unused macro definition: `kvp` --> rust/stackable-cockpit/src/platform/label/mod.rs:79:14 | 79 | macro_rules! kvp { | ^^^ | = note: `-D unused-macros` implied by `-D warnings`
Raw output
rust/stackable-cockpit/src/platform/label/mod.rs:79:14:e:error: unused macro definition: `kvp`
  --> rust/stackable-cockpit/src/platform/label/mod.rs:79:14
   |
79 | macro_rules! kvp {
   |              ^^^
   |
   = note: `-D unused-macros` implied by `-D warnings`


__END__
($Input:literal) => {
$crate::platform::label::KeyValuePair::from_str($Input)
};
}

macro_rules! label {

Check failure on line 85 in rust/stackable-cockpit/src/platform/label/mod.rs

View workflow job for this annotation

GitHub Actions / clippy

[clippy] rust/stackable-cockpit/src/platform/label/mod.rs#L85

error: unused macro definition: `label` --> rust/stackable-cockpit/src/platform/label/mod.rs:85:14 | 85 | macro_rules! label { | ^^^^^
Raw output
rust/stackable-cockpit/src/platform/label/mod.rs:85:14:e:error: unused macro definition: `label`
  --> rust/stackable-cockpit/src/platform/label/mod.rs:85:14
   |
85 | macro_rules! label {
   |              ^^^^^


__END__
($Input:expr) => {
$crate::platform::label::Label::from_str($Input)
($Input:literal) => {
$crate::platform::label::kvp!($Input)
};
}

macro_rules! annotation {

Check failure on line 91 in rust/stackable-cockpit/src/platform/label/mod.rs

View workflow job for this annotation

GitHub Actions / clippy

[clippy] rust/stackable-cockpit/src/platform/label/mod.rs#L91

error: unused macro definition: `annotation` --> rust/stackable-cockpit/src/platform/label/mod.rs:91:14 | 91 | macro_rules! annotation { | ^^^^^^^^^^ | = note: `-D unused-macros` implied by `-D warnings`
Raw output
rust/stackable-cockpit/src/platform/label/mod.rs:91:14:e:error: unused macro definition: `annotation`
  --> rust/stackable-cockpit/src/platform/label/mod.rs:91:14
   |
91 | macro_rules! annotation {
   |              ^^^^^^^^^^
   |
   = note: `-D unused-macros` implied by `-D warnings`


__END__

Check failure on line 91 in rust/stackable-cockpit/src/platform/label/mod.rs

View workflow job for this annotation

GitHub Actions / clippy

[clippy] rust/stackable-cockpit/src/platform/label/mod.rs#L91

error: unused macro definition: `annotation` --> rust/stackable-cockpit/src/platform/label/mod.rs:91:14 | 91 | macro_rules! annotation { | ^^^^^^^^^^
Raw output
rust/stackable-cockpit/src/platform/label/mod.rs:91:14:e:error: unused macro definition: `annotation`
  --> rust/stackable-cockpit/src/platform/label/mod.rs:91:14
   |
91 | macro_rules! annotation {
   |              ^^^^^^^^^^


__END__
($Input:literal) => {
$crate::platform::label::kvp!($Input)
};
}

pub(crate) use annotation;

Check failure on line 97 in rust/stackable-cockpit/src/platform/label/mod.rs

View workflow job for this annotation

GitHub Actions / clippy

[clippy] rust/stackable-cockpit/src/platform/label/mod.rs#L97

error: unused import: `annotation` --> rust/stackable-cockpit/src/platform/label/mod.rs:97:16 | 97 | pub(crate) use annotation; | ^^^^^^^^^^ | = note: `-D unused-imports` implied by `-D warnings`
Raw output
rust/stackable-cockpit/src/platform/label/mod.rs:97:16:e:error: unused import: `annotation`
  --> rust/stackable-cockpit/src/platform/label/mod.rs:97:16
   |
97 | pub(crate) use annotation;
   |                ^^^^^^^^^^
   |
   = note: `-D unused-imports` implied by `-D warnings`


__END__

Check failure on line 97 in rust/stackable-cockpit/src/platform/label/mod.rs

View workflow job for this annotation

GitHub Actions / clippy

[clippy] rust/stackable-cockpit/src/platform/label/mod.rs#L97

error: unused import: `annotation` --> rust/stackable-cockpit/src/platform/label/mod.rs:97:16 | 97 | pub(crate) use annotation; | ^^^^^^^^^^ | = note: `-D unused-imports` implied by `-D warnings`
Raw output
rust/stackable-cockpit/src/platform/label/mod.rs:97:16:e:error: unused import: `annotation`
  --> rust/stackable-cockpit/src/platform/label/mod.rs:97:16
   |
97 | pub(crate) use annotation;
   |                ^^^^^^^^^^
   |
   = note: `-D unused-imports` implied by `-D warnings`


__END__
pub(crate) use kvp;

Check failure on line 98 in rust/stackable-cockpit/src/platform/label/mod.rs

View workflow job for this annotation

GitHub Actions / clippy

[clippy] rust/stackable-cockpit/src/platform/label/mod.rs#L98

error: unused import: `kvp` --> rust/stackable-cockpit/src/platform/label/mod.rs:98:16 | 98 | pub(crate) use kvp; | ^^^
Raw output
rust/stackable-cockpit/src/platform/label/mod.rs:98:16:e:error: unused import: `kvp`
  --> rust/stackable-cockpit/src/platform/label/mod.rs:98:16
   |
98 | pub(crate) use kvp;
   |                ^^^


__END__
pub(crate) use label;

Check failure on line 99 in rust/stackable-cockpit/src/platform/label/mod.rs

View workflow job for this annotation

GitHub Actions / clippy

[clippy] rust/stackable-cockpit/src/platform/label/mod.rs#L99

error: unused import: `label` --> rust/stackable-cockpit/src/platform/label/mod.rs:99:16 | 99 | pub(crate) use label; | ^^^^^
Raw output
rust/stackable-cockpit/src/platform/label/mod.rs:99:16:e:error: unused import: `label`
  --> rust/stackable-cockpit/src/platform/label/mod.rs:99:16
   |
99 | pub(crate) use label;
   |                ^^^^^


__END__

#[cfg(test)]
mod test {
use super::*;

#[test]
fn test_basic() {
// let l = label!("stackable.tech/vendor=Stackable").unwrap();

// let key_prefix = LabelKeyPrefix::new("stackable.tech").unwrap();
// let key = LabelKey::new(Some(key_prefix), "vendor").unwrap();
// let value = LabelValue::new("Stackable").unwrap();
let label = label!("stackable.tech/vendor=Stackable").unwrap();

// let label = Label::new(key, value);
// assert_eq!(label.to_string(), "stackable.tech/vendor=Stackable")
assert_eq!(label.to_string(), "stackable.tech/vendor=Stackable")
}
}
Loading

0 comments on commit c465f5b

Please sign in to comment.