use serde::{Deserialize, Serialize, Serializer};
use std::fmt::Display;
#[derive(Clone, Hash, PartialEq, Eq, Debug, Serialize)]
enum Error {
Empty,
InvalidEscapeSequence,
DoubleOrTrailingSlash,
}
impl Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Error::Empty => write!(f, "Reference cannot be empty"),
Error::InvalidEscapeSequence => write!(f, "Reference contains invalid escape sequence"),
Error::DoubleOrTrailingSlash => {
write!(f, "Reference contains double or trailing slash")
}
}
}
}
#[derive(Clone, Hash, PartialEq, Eq, Debug)]
pub struct Reference {
variant: Variant,
input: String,
}
#[derive(Clone, Hash, PartialEq, Eq, Debug)]
enum Variant {
PlainName,
Pointer(Vec<String>),
Error(Error),
}
impl Serialize for Reference {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.input)
}
}
impl<'de> Deserialize<'de> for Reference {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Ok(Reference::new(s))
}
}
impl Reference {
pub fn new<S: AsRef<str>>(value: S) -> Self {
let value = value.as_ref();
if value.is_empty() || value == "/" {
return Self {
variant: Variant::Error(Error::Empty),
input: value.to_owned(),
};
}
if !value.starts_with('/') {
return Self {
variant: Variant::PlainName,
input: value.to_owned(),
};
}
let component_result = value[1..]
.split('/')
.map(|part| {
if part.is_empty() {
return Err(Error::DoubleOrTrailingSlash);
}
Reference::unescape_path(part)
})
.collect::<Result<Vec<String>, Error>>();
match component_result {
Ok(components) => Self {
variant: Variant::Pointer(components),
input: value.to_owned(),
},
Err(e) => Self {
variant: Variant::Error(e),
input: value.to_owned(),
},
}
}
pub fn is_valid(&self) -> bool {
!matches!(&self.variant, Variant::Error(_))
}
pub fn error(&self) -> String {
match &self.variant {
Variant::Error(e) => e.to_string(),
_ => "".to_owned(),
}
}
pub fn depth(&self) -> usize {
match &self.variant {
Variant::Pointer(components) => components.len(),
Variant::PlainName => 1,
_ => 0,
}
}
pub fn component(&self, index: usize) -> Option<&str> {
match (&self.variant, index) {
(Variant::Pointer(components), _) => components.get(index).map(|c| c.as_str()),
(Variant::PlainName, 0) => Some(&self.input),
_ => None,
}
}
pub(crate) fn is_kind(&self) -> bool {
matches!((self.depth(), self.component(0)), (1, Some(comp)) if comp == "kind")
}
fn unescape_path(path: &str) -> Result<String, Error> {
if !path.contains('~') {
return Ok(path.to_string());
}
let mut out = String::new();
let mut iter = path.chars().peekable();
while let Some(c) = iter.next() {
if c != '~' {
out.push(c);
continue;
}
if iter.peek().is_none() {
return Err(Error::InvalidEscapeSequence);
}
let unescaped = match iter.next().unwrap() {
'0' => '~',
'1' => '/',
_ => return Err(Error::InvalidEscapeSequence),
};
out.push(unescaped);
}
Ok(out)
}
}
impl Default for Reference {
fn default() -> Self {
Reference::new("")
}
}
impl Display for Reference {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
write!(f, "{}", self.input)
}
}
impl<S> From<S> for Reference
where
S: AsRef<str>,
{
fn from(reference: S) -> Self {
Reference::new(reference)
}
}
impl From<Reference> for String {
fn from(r: Reference) -> Self {
r.input
}
}
#[derive(Debug, Deserialize, PartialEq)]
#[serde(transparent)]
pub(crate) struct AttributeName(String);
impl AttributeName {
#[cfg(test)]
pub(crate) fn new(s: String) -> Self {
Self(s)
}
}
impl Default for AttributeName {
fn default() -> Self {
Self("".to_owned())
}
}
impl From<AttributeName> for Reference {
fn from(name: AttributeName) -> Self {
if !name.0.starts_with('/') {
return Self::new(name.0);
}
let mut escaped = name.0.replace('~', "~0").replace('/', "~1");
escaped.insert(0, '/');
Self::new(escaped)
}
}
#[cfg(test)]
pub(crate) mod proptest_generators {
use super::{AttributeName, Reference};
use proptest::prelude::*;
prop_compose! {
pub(crate) fn any_valid_ref_string()(s in "([^/].*|(/([^/~]|~[01])+)+)") -> String {
s
}
}
prop_compose! {
pub(crate) fn any_valid_plain_name()(s in "([^/].*)") -> String {
s
}
}
prop_compose! {
pub(crate) fn any_attribute_name()(s in any_valid_ref_string()) -> AttributeName {
AttributeName::new(s)
}
}
prop_compose! {
pub(crate) fn any_valid_ref()(s in any_valid_ref_string()) -> Reference {
Reference::new(s)
}
}
prop_compose! {
pub(crate) fn any_ref()(s in any::<String>()) -> Reference {
Reference::new(s)
}
}
prop_compose! {
pub(crate) fn any_valid_ref_transformed_from_attribute_name()(s in any_valid_ref_string()) -> Reference {
Reference::from(AttributeName::new(s))
}
}
prop_compose! {
pub(crate) fn any_ref_transformed_from_attribute_name()(s in any::<String>()) -> Reference {
Reference::from(AttributeName::new(s))
}
}
prop_compose! {
pub(crate) fn any_valid_plain_ref()(s in any_valid_plain_name()) -> Reference {
Reference::new(s)
}
}
}
#[cfg(test)]
mod tests {
use super::{AttributeName, Error, Reference};
use crate::proptest_generators::*;
use proptest::prelude::*;
use test_case::test_case;
proptest! {
#[test]
fn regex_creates_valid_references(reference in any_valid_ref()) {
prop_assert!(reference.is_valid());
}
}
proptest! {
#[test]
fn regex_creates_valid_plain_references(reference in any_valid_plain_ref()) {
prop_assert!(reference.is_valid());
}
}
proptest! {
#[test]
fn plain_references_have_single_component(reference in any_valid_plain_ref()) {
prop_assert_eq!(reference.depth(), 1);
}
}
proptest! {
#[test]
fn attribute_names_are_valid_references(reference in any_valid_ref_transformed_from_attribute_name()) {
prop_assert!(reference.is_valid());
prop_assert_eq!(reference.depth(), 1);
}
}
proptest! {
#[test]
fn attribute_name_references_have_single_component(reference in any_valid_ref_transformed_from_attribute_name()) {
prop_assert_eq!(reference.depth(), 1);
let component = reference.component(0);
prop_assert!(component.is_some(), "component 0 should exist");
}
}
proptest! {
#[test]
fn raw_returns_input_unmodified(s in any::<String>()) {
let a = Reference::new(s.clone());
prop_assert_eq!(a.to_string(), s);
}
}
#[test]
fn default_reference_is_invalid() {
assert!(!Reference::default().is_valid());
}
#[test_case("", Error::Empty; "Empty reference")]
#[test_case("/", Error::Empty; "Single slash")]
#[test_case("//", Error::DoubleOrTrailingSlash; "Double slash")]
#[test_case("/a//b", Error::DoubleOrTrailingSlash; "Double slash in middle")]
#[test_case("/a/b/", Error::DoubleOrTrailingSlash; "Trailing slash")]
#[test_case("/~3", Error::InvalidEscapeSequence; "Tilde must be followed by 0 or 1 only")]
#[test_case("/testing~something", Error::InvalidEscapeSequence; "Tilde cannot be alone")]
#[test_case("/m~~0", Error::InvalidEscapeSequence; "Extra tilde before valid escape")]
#[test_case("/a~", Error::InvalidEscapeSequence; "Tilde cannot be followed by nothing")]
fn invalid_references(input: &str, error: Error) {
let reference = Reference::new(input);
assert!(!reference.is_valid());
assert_eq!(error.to_string(), reference.error());
}
#[test_case("key")]
#[test_case("kind")]
#[test_case("name")]
#[test_case("name/with/slashes")]
#[test_case("name~0~1with-what-looks-like-escape-sequences")]
fn plain_reference_syntax(input: &str) {
let reference = Reference::new(input);
assert!(reference.is_valid());
assert_eq!(input, reference.to_string());
assert_eq!(
input,
reference
.component(0)
.expect("Failed to get first component")
);
assert_eq!(1, reference.depth());
}
#[test_case("/key", "key")]
#[test_case("/kind", "kind")]
#[test_case("/name", "name")]
#[test_case("/custom", "custom")]
fn pointer_syntax(input: &str, path: &str) {
let reference = Reference::new(input);
assert!(reference.is_valid());
assert_eq!(input, reference.to_string());
assert_eq!(
path,
reference
.component(0)
.expect("Failed to get first component")
);
assert_eq!(1, reference.depth())
}
#[test_case("/a/b", 2, 0, "a")]
#[test_case("/a/b", 2, 1, "b")]
#[test_case("/a~1b/c", 2, 0, "a/b")]
#[test_case("/a~1b/c", 2, 1, "c")]
#[test_case("/a/10/20/30x", 4, 1, "10")]
#[test_case("/a/10/20/30x", 4, 2, "20")]
#[test_case("/a/10/20/30x", 4, 3, "30x")]
fn handles_subcomponents(input: &str, len: usize, index: usize, expected_name: &str) {
let reference = Reference::new(input);
assert!(reference.is_valid());
assert_eq!(input, reference.input);
assert_eq!(len, reference.depth());
assert_eq!(expected_name, reference.component(index).unwrap());
}
#[test]
fn can_handle_invalid_index_requests() {
let reference = Reference::new("/a/b/c");
assert!(reference.is_valid());
assert!(reference.component(0).is_some());
assert!(reference.component(1).is_some());
assert!(reference.component(2).is_some());
assert!(reference.component(3).is_none());
}
#[test_case("/a/b", "/~1a~1b")]
#[test_case("a", "a")]
#[test_case("a~1b", "a~1b")]
#[test_case("/a~1b", "/~1a~01b")]
#[test_case("/a~0b", "/~1a~00b")]
#[test_case("", "")]
#[test_case("/", "/~1")]
fn attribute_name_equality(name: &str, reference: &str) {
let as_name = AttributeName::new(name.to_owned());
let reference = Reference::new(reference);
assert_eq!(Reference::from(as_name), reference);
}
#[test]
fn is_kind() {
assert!(Reference::new("/kind").is_kind());
assert!(Reference::new("kind").is_kind());
assert!(Reference::from(AttributeName::new("kind".to_owned())).is_kind());
assert!(!Reference::from(AttributeName::new("/kind".to_owned())).is_kind());
assert!(!Reference::new("foo").is_kind());
}
}