#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone)]
pub struct JsonString {
quoted: String,
}
#[derive(Debug)]
pub(crate) struct JsonStringBuilder {
quoted: String,
}
impl std::fmt::Debug for JsonString {
#[inline]
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
write!(f, "JsonString({})", self.quoted)
}
}
impl JsonStringBuilder {
pub(crate) fn new() -> Self {
Self {
quoted: String::from('"'),
}
}
pub(crate) fn push(&mut self, char: char) -> &mut Self {
self.quoted.push(char);
self
}
pub(crate) fn finish(mut self) -> JsonString {
self.quoted.push('"');
JsonString { quoted: self.quoted }
}
}
impl From<JsonStringBuilder> for JsonString {
#[inline(always)]
fn from(value: JsonStringBuilder) -> Self {
value.finish()
}
}
impl From<&str> for JsonString {
#[inline(always)]
fn from(value: &str) -> Self {
Self::new(value)
}
}
impl FromIterator<char> for JsonString {
#[inline]
fn from_iter<T: IntoIterator<Item = char>>(iter: T) -> Self {
let mut quoted = String::new();
quoted.push('"');
for c in iter {
quoted.push(c);
}
quoted.push('"');
Self { quoted }
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum EscapeMode {
SingleQuoted,
DoubleQuoted,
}
#[inline]
#[must_use]
pub fn escape(str: &str, mode: EscapeMode) -> String {
use std::fmt::Write as _;
let mut result = String::new();
for c in str.chars() {
match c {
'\'' if mode == EscapeMode::SingleQuoted => result.push_str(r"\'"),
'\'' if mode == EscapeMode::DoubleQuoted => result.push('\''),
'"' if mode == EscapeMode::SingleQuoted => result.push('"'),
'"' if mode == EscapeMode::DoubleQuoted => result.push_str(r#"\""#),
'\\' => result.push_str(r"\\"),
'\u{0008}' => result.push_str(r"\b"),
'\u{000C}' => result.push_str(r"\f"),
'\n' => result.push_str(r"\n"),
'\r' => result.push_str(r"\r"),
'\t' => result.push_str(r"\t"),
'\u{0000}'..='\u{001F}' => write!(result, "\\u{:0>4x}", c as u8).expect("writing to string never fails"),
_ => result.push(c),
}
}
result
}
impl JsonString {
#[inline]
#[must_use]
pub fn new(string: &str) -> Self {
let mut quoted = String::with_capacity(string.len() + 2);
quoted.push('"');
quoted += string;
quoted.push('"');
Self { quoted }
}
#[must_use]
#[inline(always)]
pub fn unquoted(&self) -> &str {
let len = self.quoted.len();
debug_assert!(len >= 2, "self.quoted must contain at least the two quote characters");
&self.quoted[1..len - 1]
}
#[must_use]
#[inline(always)]
pub fn quoted(&self) -> &str {
&self.quoted
}
}
impl PartialEq<Self> for JsonString {
#[inline(always)]
fn eq(&self, other: &Self) -> bool {
self.unquoted() == other.unquoted()
}
}
impl Eq for JsonString {}
impl std::hash::Hash for JsonString {
#[inline(always)]
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.unquoted().hash(state);
}
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::{assert_eq, assert_ne};
use std::{
collections::hash_map::DefaultHasher,
hash::{Hash, Hasher},
};
use test_case::test_case;
#[test_case("dog", "dog"; "dog")]
#[test_case("", ""; "empty")]
fn equal_json_strings_are_equal(s1: &str, s2: &str) {
let string1 = JsonString::new(s1);
let string2 = JsonString::new(s2);
assert_eq!(string1, string2);
}
#[test]
fn different_json_strings_are_not_equal() {
let string1 = JsonString::new("dog");
let string2 = JsonString::new("doc");
assert_ne!(string1, string2);
}
#[test_case("dog", "dog"; "dog")]
#[test_case("", ""; "empty")]
fn equal_json_strings_have_equal_hashes(s1: &str, s2: &str) {
let string1 = JsonString::new(s1);
let string2 = JsonString::new(s2);
let mut hasher1 = DefaultHasher::new();
string1.hash(&mut hasher1);
let hash1 = hasher1.finish();
let mut hasher2 = DefaultHasher::new();
string2.hash(&mut hasher2);
let hash2 = hasher2.finish();
assert_eq!(hash1, hash2);
}
}