Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Validate bare items on construction #102

Open
wants to merge 17 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion benches/bench.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ extern crate criterion;

use criterion::{BenchmarkId, Criterion};
use rust_decimal::prelude::FromPrimitive;
use sfv::{BareItem, Decimal, Parser, SerializeValue};
use rust_decimal::Decimal;
use sfv::{Parser, SerializeValue};
use sfv::{RefBareItem, RefDictSerializer, RefItemSerializer, RefListSerializer};

criterion_main!(parsing, serializing, ref_serializing);
Expand Down
252 changes: 252 additions & 0 deletions src/bare_item.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
use crate::serializer::Serializer;
use std::{convert::TryFrom, fmt, ops::Deref};

#[derive(Debug, PartialEq, Clone)]
pub struct Decimal(pub(crate) rust_decimal::Decimal);

impl TryFrom<rust_decimal::Decimal> for Decimal {
type Error = &'static str;
fn try_from(value: rust_decimal::Decimal) -> Result<Self, Self::Error> {
let mut output = String::new();
Serializer::serialize_decimal(value, &mut output)?;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, doing an actual serialization in the try_from implementation seems less than optimal.
That means for every bare item that we construct, we allocate a string we then throw away. And then we do the serialization again when we actually serialize it into a buffer.

I think what we should do here is split the serialization from the validity checks, so try_from for Decimal would only check if the decimal is in the allowed range, and not actually serialize it.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I'm not happy with validating by serializing either, and as mentioned in the PR description we could (and should 😅) move validations to a place where both can access it.
To get my idea validated and not change another hundreds of lines, I decided to keep it like this for the moment - but I'm totally supportive of the fact that this needs to change.

Copy link
Author

@zcei zcei Feb 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Created a new ValidateValue trait that the value structs implement. It will either return a Result where the Ok value is either the input (passing back ownership) or a new "sanitized" value (important for Decimal as we can round to three decimal places already)


Ok(Decimal(value))
}
}

impl Deref for Decimal {
type Target = rust_decimal::Decimal;
fn deref(&self) -> &Self::Target {
&self.0
}
}

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

/// Integers have a range of -999,999,999,999,999 to 999,999,999,999,999 inclusive (i.e., up to fifteen digits, signed), for IEEE 754 compatibility.
///
/// The ABNF for Integers is:
/// ```abnf,ignore,no_run
/// sf-integer = ["-"] 1*15DIGIT
/// ```
#[derive(Debug, PartialEq, Clone)]
pub struct Integer(pub(crate) i64);

impl Deref for Integer {
type Target = i64;
fn deref(&self) -> &Self::Target {
&self.0
}
}

impl TryFrom<i64> for Integer {
type Error = &'static str;

fn try_from(value: i64) -> Result<Self, Self::Error> {
let mut output = String::new();
Serializer::serialize_integer(value, &mut output)?;
Ok(Integer(value))
}
}

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

// TODO: how to get around naming collision without using std::string::String everywhere?
zcei marked this conversation as resolved.
Show resolved Hide resolved
/// Strings are zero or more printable ASCII (RFC0020) characters (i.e., the range %x20 to %x7E). Note that this excludes tabs, newlines, carriage returns, etc.
///
/// The ABNF for Strings is:
/// ```abnf,ignore,no_run
/// sf-string = DQUOTE *chr DQUOTE
/// chr = unescaped / escaped
/// unescaped = %x20-21 / %x23-5B / %x5D-7E
/// escaped = "\" ( DQUOTE / "\" )
/// ```
#[derive(Debug, PartialEq, Clone)]
pub struct BareItemString(pub(crate) std::string::String);

impl Deref for BareItemString {
type Target = String;

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

impl TryFrom<String> for BareItemString {
type Error = &'static str;
fn try_from(value: String) -> Result<Self, Self::Error> {
let mut output = String::new();
Serializer::serialize_string(&value, &mut output)?;

Ok(BareItemString(value))
}
}

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

/// Byte Sequences can be conveyed in Structured Fields.
///
/// The ABNF for a Byte Sequence is:
/// ```abnf,ignore,no_run
/// sf-binary = ":" *(base64) ":"
/// base64 = ALPHA / DIGIT / "+" / "/" / "="
/// ```
#[derive(Debug, PartialEq, Clone)]
pub struct ByteSeq(pub(crate) Vec<u8>);

impl From<&[u8]> for ByteSeq {
fn from(value: &[u8]) -> Self {
ByteSeq(value.to_vec())
}
}

impl From<Vec<u8>> for ByteSeq {
fn from(value: Vec<u8>) -> Self {
ByteSeq(value)
}
}

impl Deref for ByteSeq {
type Target = [u8];
fn deref(&self) -> &Self::Target {
self.0.as_slice()
}
}

/// Boolean values can be conveyed in Structured Fields.
///
/// The ABNF for a Boolean is:
/// ```abnf,ignore,no_run
/// sf-boolean = "?" boolean
/// boolean = "0" / "1"
/// ```
#[derive(Debug, PartialEq, Clone)]
pub struct Boolean(pub(crate) bool);

impl From<bool> for Boolean {
fn from(value: bool) -> Self {
Boolean(value)
}
}

impl Deref for Boolean {
type Target = bool;
fn deref(&self) -> &Self::Target {
&self.0
}
}

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

/// Tokens are short textual words; their abstract model is identical to their expression in the HTTP field value serialization.
///
/// The ABNF for Tokens is:
/// ```abnf,ignore,no_run
/// sf-token = ( ALPHA / "*" ) *( tchar / ":" / "/" )
/// ```
///
/// # Example
/// ```
/// use sfv::{BareItem, Token};
/// use std::convert::{TryFrom, TryInto};
///
/// # fn main() -> Result<(), &'static str> {
/// let token_try_from = Token::try_from("foo")?;
/// let item = BareItem::Token(token_try_from);
///
/// let str_try_into: Token = "bar".try_into()?;
/// let item = BareItem::Token(str_try_into);
///
/// let direct_item_construction = BareItem::Token("baz".try_into()?);
/// # Ok(())
/// # }
/// ```
///
/// ```compile_fail
/// Token("foo"); // A Token can not be constructed directly
/// ```
#[derive(Debug, PartialEq, Clone)]
pub struct Token(pub(crate) String);

impl Deref for Token {
type Target = String;

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

impl TryFrom<String> for Token {
type Error = &'static str;
fn try_from(value: String) -> Result<Self, Self::Error> {
let mut output = String::new();
Serializer::serialize_token(&value, &mut output)?;

Ok(Token(value))
}
}

impl TryFrom<&str> for Token {
type Error = &'static str;
fn try_from(value: &str) -> Result<Self, Self::Error> {
let mut output = String::new();
Serializer::serialize_token(&value, &mut output)?;
Ok(Token(value.to_owned()))
}
}

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

#[cfg(test)]
mod tests {
use std::convert::TryInto;
use std::error::Error;
use std::str::FromStr;

use super::*;

#[test]
fn create_non_ascii_string_errors() -> Result<(), Box<dyn Error>> {
let disallowed_value: Result<BareItemString, &str> =
"non-ascii text 🐹".to_owned().try_into();

assert_eq!(
Err("serialize_string: non-ascii character"),
disallowed_value
);

Ok(())
}

#[test]
fn create_too_long_decimal_errors() -> Result<(), Box<dyn Error>> {
let disallowed_value: Result<Decimal, &str> =
rust_decimal::Decimal::from_str("12345678912345.123")?.try_into();
assert_eq!(
Err("serialize_decimal: integer component > 12 digits"),
disallowed_value
);

Ok(())
}
}
Loading