Skip to content

Commit

Permalink
Merge pull request #109 from xsnippet/serialization
Browse files Browse the repository at this point in the history
Make Snippet and Changeset serializable
  • Loading branch information
malor committed Jan 17, 2021
2 parents bfaaba3 + 8c2115b commit f10eded
Show file tree
Hide file tree
Showing 6 changed files with 154 additions and 31 deletions.
16 changes: 16 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,7 @@ diesel = { version = "1.4.5", features = ["chrono", "postgres", "r2d2"] }
rand = "0.7.3"
rocket = "0.4.5"
rocket_contrib = {version = "0.4.5", features = ["json"]}
serde = { version = "1.0", features = ["derive"] }

[dev-dependencies]
serde_json = "1.0"
2 changes: 2 additions & 0 deletions src/storage/mod.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
mod errors;
mod models;
mod sql;
mod util;

pub use errors::StorageError;
pub use models::{Changeset, ListSnippetsQuery, Snippet};
pub use sql::SqlStorage;
pub use util::DateTime;

/// CRUD interface for storing/loading snippets from a persistent storage.
///
Expand Down
51 changes: 44 additions & 7 deletions src/storage/models.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
use std::iter;

use chrono::{DateTime, Utc};
use rand::Rng;
use serde::Serialize;

use crate::storage::DateTime;

const DEFAULT_LIMIT_SIZE: usize = 20;
const DEFAULT_SLUG_LENGTH: usize = 8;

/// A code snippet
#[derive(Debug, Default, Eq, PartialEq)]
#[derive(Debug, Default, Eq, PartialEq, Serialize)]
pub struct Snippet {
/// Slug that uniquely identifies the snippet
pub id: String,
Expand All @@ -24,10 +26,10 @@ pub struct Snippet {
/// List of tags attached to the snippet
pub tags: Vec<String>,
/// Timestamp of when the snippet was created
pub created_at: Option<DateTime<Utc>>,
pub created_at: Option<DateTime>,
/// Timestamp of when the snippet was last modified. Defaults to creation
/// time
pub updated_at: Option<DateTime<Utc>>,
pub updated_at: Option<DateTime>,
}

impl Snippet {
Expand Down Expand Up @@ -90,17 +92,17 @@ impl Default for ListSnippetsQuery {
}

/// A particular snippet revision
#[derive(Debug, Default, Eq, Ord, PartialEq, PartialOrd)]
#[derive(Debug, Default, Eq, Ord, PartialEq, PartialOrd, Serialize)]
pub struct Changeset {
/// Changeset index. Version numbers start from 0 and are incremented by 1
pub version: usize,
/// Changeset content
pub content: String,
/// Timestamp of when the changeset was created
pub created_at: Option<DateTime<Utc>>,
pub created_at: Option<DateTime>,
/// Timestamp of when the changeset was last modified. Defaults to creation
/// time
pub updated_at: Option<DateTime<Utc>>,
pub updated_at: Option<DateTime>,
}

impl Changeset {
Expand Down Expand Up @@ -291,4 +293,39 @@ mod tests {
assert_ne!(different_version, reference);
assert_ne!(different_content, reference);
}

#[test]
fn test_serialization() {
let dt = chrono::DateTime::parse_from_rfc3339("2020-08-09T10:39:57+00:00")
.unwrap()
.with_timezone(&chrono::Utc);
let reference = Snippet {
id: "spam".to_string(),
title: Some("Hello".to_string()),
syntax: Some("python".to_string()),
changesets: vec![Changeset::new(0, "print('Hello, World!')".to_string())],
tags: vec!["foo".to_string(), "bar".to_string()],
created_at: Some(dt.into()),
updated_at: None,
};

let expected = "{\
\"id\":\"spam\",\
\"title\":\"Hello\",\
\"syntax\":\"python\",\
\"changesets\":[\
{\
\"version\":0,\
\"content\":\"print(\'Hello, World!\')\",\
\"created_at\":null,\
\"updated_at\":null\
}\
],\
\"tags\":[\"foo\",\"bar\"],\
\"created_at\":\"2020-08-09T10:39:57+00:00\",\
\"updated_at\":null\
}";
let actual = serde_json::to_string(&reference).expect("failed to serialize snippet");
assert_eq!(actual, expected);
}
}
48 changes: 24 additions & 24 deletions src/storage/sql/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ impl From<(SnippetRow, Vec<ChangesetRow>, Vec<TagRow>)> for Snippet {
updated_at,
}| {
let mut changeset = Changeset::new(version as usize, content);
changeset.updated_at = Some(updated_at);
changeset.created_at = Some(created_at);
changeset.updated_at = Some(updated_at.into());
changeset.created_at = Some(created_at.into());

changeset
},
Expand All @@ -60,8 +60,8 @@ impl From<(SnippetRow, Vec<ChangesetRow>, Vec<TagRow>)> for Snippet {

let mut snippet = Snippet::new(snippet_row.title, snippet_row.syntax, changesets, tags);
snippet.id = snippet_row.slug;
snippet.created_at = Some(snippet_row.created_at);
snippet.updated_at = Some(snippet_row.updated_at);
snippet.created_at = Some(snippet_row.created_at.into());
snippet.updated_at = Some(snippet_row.updated_at.into());

snippet
}
Expand Down Expand Up @@ -166,19 +166,19 @@ mod tests {
Changeset {
version: 1,
content: "print('Hello!')".to_string(),
created_at: Some(dt1),
updated_at: Some(dt1),
created_at: Some(dt1.into()),
updated_at: Some(dt1.into()),
},
Changeset {
version: 2,
content: "print('Hello, World!')".to_string(),
created_at: Some(dt2),
updated_at: Some(dt2),
created_at: Some(dt2.into()),
updated_at: Some(dt2.into()),
},
],
tags: vec!["spam".to_string(), "eggs".to_string()],
created_at: Some(dt1),
updated_at: Some(dt2),
created_at: Some(dt1.into()),
updated_at: Some(dt2.into()),
};

assert_eq!(actual, expected);
Expand Down Expand Up @@ -281,19 +281,19 @@ mod tests {
Changeset {
version: 1,
content: "print('Hello!')".to_string(),
created_at: Some(dt1),
updated_at: Some(dt1),
created_at: Some(dt1.into()),
updated_at: Some(dt1.into()),
},
Changeset {
version: 2,
content: "print('Hello, World!')".to_string(),
created_at: Some(dt2),
updated_at: Some(dt2),
created_at: Some(dt2.into()),
updated_at: Some(dt2.into()),
},
],
tags: vec!["spam".to_string(), "eggs".to_string()],
created_at: Some(dt1),
updated_at: Some(dt2),
created_at: Some(dt1.into()),
updated_at: Some(dt2.into()),
},
Snippet {
id: "eggs".to_string(),
Expand All @@ -302,12 +302,12 @@ mod tests {
changesets: vec![Changeset {
version: 1,
content: "println!(42);".to_string(),
created_at: Some(dt1),
updated_at: Some(dt1),
created_at: Some(dt1.into()),
updated_at: Some(dt1.into()),
}],
tags: vec![],
created_at: Some(dt1),
updated_at: Some(dt2),
created_at: Some(dt1.into()),
updated_at: Some(dt2.into()),
},
Snippet {
id: "bar".to_string(),
Expand All @@ -316,12 +316,12 @@ mod tests {
changesets: vec![Changeset {
version: 1,
content: "std::cout << 42;".to_string(),
created_at: Some(dt1),
updated_at: Some(dt1),
created_at: Some(dt1.into()),
updated_at: Some(dt1.into()),
}],
tags: vec!["bar".to_string()],
created_at: Some(dt1),
updated_at: Some(dt2),
created_at: Some(dt1.into()),
updated_at: Some(dt2.into()),
},
];
assert_eq!(actual, expected);
Expand Down
64 changes: 64 additions & 0 deletions src/storage/util.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
use serde::{Serialize, Serializer};

type ChronoUtc = chrono::DateTime<chrono::Utc>;

/// A wrapper around DateTime<Utc> from chrono that allows us
/// to implement Serialize (otherwise, Rust forbids implementing
/// a foreign trait for a foreign type from a different crate).
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct DateTime(ChronoUtc);

impl From<ChronoUtc> for DateTime {
fn from(value: ChronoUtc) -> Self {
Self(value)
}
}
impl From<DateTime> for ChronoUtc {
fn from(value: DateTime) -> Self {
value.0
}
}

impl Serialize for DateTime {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.0.to_rfc3339())
}
}

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

#[test]
fn test_datetime_from() {
let reference = chrono::DateTime::parse_from_rfc3339("2020-08-09T10:39:57+00:00")
.unwrap()
.with_timezone(&chrono::Utc);

// from() conversions work in both directions: chrono::DateTime --> DateTime
let dt = DateTime::from(reference);
assert_eq!(dt.0, reference);

// and DateTime --> chrono::DateTime
let chrono_dt = chrono::DateTime::from(dt);
assert_eq!(chrono_dt, reference);
}

#[test]
fn test_datetime_serialize() {
let reference = chrono::DateTime::parse_from_rfc3339("2020-08-09T10:39:57+00:00")
.unwrap()
.with_timezone(&chrono::Utc);

let dt = DateTime::from(reference);

// DateTime is serialized to its string representation (JSON strings are
// enclosed in double quotes)
let expected = "\"2020-08-09T10:39:57+00:00\"";
let actual = serde_json::to_string(&dt).expect("failed to serilize DateTime");
assert_eq!(actual, expected);
}
}

0 comments on commit f10eded

Please sign in to comment.