Skip to content

Commit 1c8a200

Browse files
authored
Merge pull request #5 from nyris/feature/serde
Add serde support
2 parents 1f7ca9b + b93a01c commit 1c8a200

File tree

8 files changed

+262
-15
lines changed

8 files changed

+262
-15
lines changed

.github/workflows/rust.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,6 @@ jobs:
1919
- name: Build
2020
run: cargo build --verbose
2121
- name: Run tests
22-
run: cargo test --verbose
22+
run: cargo test --verbose --features=serde
2323
- name: Run doctests
24-
run: cargo test --doc --verbose
24+
run: cargo test --doc --verbose --features=serde

CHANGELOG.md

+8
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,14 @@
33
All notable changes to this project will be documented in this file.
44
This project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
55

6+
## Unreleased
7+
8+
### Added
9+
10+
- Added a `FromStr` implementation, allowing for `parse::<ShortGuid>("...")`.
11+
- Added `from_slice` to construct from a `&[u8]`.
12+
- Added support for Serde.
13+
614
## 0.4.0 - 2023-06-24
715

816
### Added

Cargo.toml

+17-5
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,31 @@ authors = ["Markus Mayer <[email protected]>"]
99
readme = "README.md"
1010
rust-version = "1.67.1"
1111

12-
[dependencies]
13-
arbitrary = { version = "1.3.0", optional = true }
14-
base64 = "0.21.2"
15-
uuid = "1.3.4"
16-
1712
[features]
1813
default = ["random"]
1914
arbitrary = ["uuid/arbitrary", "arbitrary/derive"]
2015
random = ["uuid/v4"]
16+
serde = ["dep:serde", "uuid/serde"]
17+
18+
[[example]]
19+
name = "shortguid"
20+
path = "examples/shortguid.rs"
21+
22+
[[test]]
23+
name = "serde"
24+
path = "tests/serde.rs"
25+
required-features = ["serde"]
26+
27+
[dependencies]
28+
arbitrary = { version = "1.3.0", optional = true }
29+
base64 = "0.21.2"
30+
serde = { version = "1.0.164", optional = true }
31+
uuid = "1.3.4"
2132

2233
[dev-dependencies]
2334
hex = "0.4.3"
2435
clap = "4.3.8"
36+
serde_test = "1.0.164"
2537

2638
[package.metadata.docs.rs]
2739
all-features = true

LICENSE.md

+34-1
Original file line numberDiff line numberDiff line change
@@ -285,4 +285,37 @@ the rights granted in Article 2 of this Licence and protect the covered Source
285285
Code from exclusive appropriation.
286286

287287
All other changes or additions to this Appendix require the production of a new
288-
EUPL version.
288+
EUPL version.
289+
290+
---
291+
292+
# MIT
293+
294+
The serde related code is licensed under an MIT license shown below.
295+
296+
Copyright (c) 2014 The Rust Project Developers
297+
Copyright (c) 2018 Ashley Mannix, Christopher Armstrong, Dylan DPC, Hunar Roop Kahlon
298+
299+
Permission is hereby granted, free of charge, to any
300+
person obtaining a copy of this software and associated
301+
documentation files (the "Software"), to deal in the
302+
Software without restriction, including without
303+
limitation the rights to use, copy, modify, merge,
304+
publish, distribute, sublicense, and/or sell copies of
305+
the Software, and to permit persons to whom the Software
306+
is furnished to do so, subject to the following
307+
conditions:
308+
309+
The above copyright notice and this permission notice
310+
shall be included in all copies or substantial portions
311+
of the Software.
312+
313+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
314+
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
315+
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
316+
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
317+
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
318+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
319+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
320+
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
321+
DEALINGS IN THE SOFTWARE.

fuzz/Cargo.lock

+5-5
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/lib.rs

+24-2
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,14 @@
1515
// the `docsrs` configuration attribute is defined
1616
#![cfg_attr(docsrs, feature(doc_cfg))]
1717

18+
#[cfg(feature = "serde")]
19+
mod serde;
20+
1821
use base64::{DecodeError, Engine};
1922
use std::borrow::Borrow;
2023
use std::error::Error;
2124
use std::fmt::{Debug, Display, Formatter};
25+
use std::str::FromStr;
2226
use uuid::Uuid;
2327

2428
/// A short, URL-safe UUID representation.
@@ -77,15 +81,22 @@ impl ShortGuid {
7781
Ok(Self(uuid))
7882
}
7983

84+
/// Creates a [`ShortGuid`] using the supplied bytes.
85+
#[inline]
86+
pub fn from_slice<B: AsRef<[u8]>>(bytes: B) -> Result<Self, ParseError> {
87+
let uuid = Uuid::from_slice(bytes.as_ref()).map_err(|e| ParseError::InvalidSlice(e))?;
88+
Ok(Self(uuid))
89+
}
90+
8091
/// Constructs a [`ShortGuid`] instance based on a byte slice.
8192
///
8293
/// ## Notes
8394
/// This will clone the underlying data. If you wish to return a
8495
/// transparent reference around the provided slice, use [`ShortGuid::from_bytes_ref`]
8596
/// instead.
8697
#[inline]
87-
pub fn from_bytes(bytes: &[u8; 16]) -> Self {
88-
Self(Uuid::from_bytes_ref(bytes).clone())
98+
pub fn from_bytes<B: Borrow<[u8; 16]>>(bytes: B) -> Self {
99+
Self(Uuid::from_bytes_ref(bytes.borrow()).clone())
89100
}
90101

91102
/// Returns a slice of 16 octets containing the value.
@@ -373,6 +384,8 @@ pub enum ParseError {
373384
/// The provided input had an invalid format.
374385
/// The contained value is the underlying decoding error.
375386
InvalidFormat(DecodeError),
387+
/// The provided slice input was invalid.
388+
InvalidSlice(uuid::Error),
376389
}
377390

378391
impl From<DecodeError> for ParseError {
@@ -395,10 +408,19 @@ impl Display for ParseError {
395408
"Invalid ID length; expected 22 characters, but got {len}"
396409
),
397410
ParseError::InvalidFormat(err) => write!(f, "Invalid ID format: {err}"),
411+
ParseError::InvalidSlice(err) => write!(f, "Invalid slice: {err}"),
398412
}
399413
}
400414
}
401415

416+
impl FromStr for ShortGuid {
417+
type Err = ParseError;
418+
419+
fn from_str(s: &str) -> Result<Self, Self::Err> {
420+
ShortGuid::try_parse(s)
421+
}
422+
}
423+
402424
impl Error for ParseError {}
403425

404426
#[cfg(test)]

src/serde.rs

+91
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
// The original serde code is taken from the uuid crate at https://github.com/uuid-rs/uuid,
2+
// licensed under an MIT or Apache-2.0 license and copyrighted as follows:
3+
//
4+
// Copyright (c) 2014 The Rust Project Developers
5+
// Copyright (c) 2018 Ashley Mannix, Christopher Armstrong, Dylan DPC, Hunar Roop Kahlon
6+
// Copyright (c) 2023 Markus Mayer
7+
//
8+
// SPDX-License-Identifier: EUPL-1.2 or MIT or Apache-2.0
9+
10+
use crate::{ParseError, ShortGuid};
11+
use std::fmt::Formatter;
12+
use uuid::Uuid;
13+
14+
#[cfg(feature = "serde")]
15+
impl serde::Serialize for ShortGuid {
16+
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
17+
where
18+
S: serde::Serializer,
19+
{
20+
if serializer.is_human_readable() {
21+
serializer.serialize_str(Self::encode(self.0).as_str())
22+
} else {
23+
serializer.serialize_bytes(self.as_bytes())
24+
}
25+
}
26+
}
27+
28+
#[cfg(feature = "serde")]
29+
impl<'de> serde::Deserialize<'de> for ShortGuid {
30+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
31+
where
32+
D: serde::Deserializer<'de>,
33+
{
34+
fn de_error<E: serde::de::Error>(e: ParseError) -> E {
35+
E::custom(format_args!("ShortGuid parsing failed: {}", e))
36+
}
37+
38+
if deserializer.is_human_readable() {
39+
struct ShortGuidVisitor;
40+
41+
impl<'vi> serde::de::Visitor<'vi> for ShortGuidVisitor {
42+
type Value = ShortGuid;
43+
44+
fn expecting(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
45+
write!(formatter, "a ShortGuid string")
46+
}
47+
48+
fn visit_str<E: serde::de::Error>(self, value: &str) -> Result<ShortGuid, E> {
49+
value.parse::<ShortGuid>().map_err(de_error)
50+
}
51+
52+
fn visit_bytes<E: serde::de::Error>(self, value: &[u8]) -> Result<ShortGuid, E> {
53+
ShortGuid::from_slice(value).map_err(de_error)
54+
}
55+
56+
fn visit_seq<A>(self, mut seq: A) -> Result<ShortGuid, A::Error>
57+
where
58+
A: serde::de::SeqAccess<'vi>,
59+
{
60+
use serde::de::Error;
61+
#[rustfmt::skip]
62+
let bytes: [u8; 16] = [
63+
match seq.next_element()? { Some(e) => e, None => return Err(Error::invalid_length(16, &self)) },
64+
match seq.next_element()? { Some(e) => e, None => return Err(Error::invalid_length(16, &self)) },
65+
match seq.next_element()? { Some(e) => e, None => return Err(Error::invalid_length(16, &self)) },
66+
match seq.next_element()? { Some(e) => e, None => return Err(Error::invalid_length(16, &self)) },
67+
match seq.next_element()? { Some(e) => e, None => return Err(Error::invalid_length(16, &self)) },
68+
match seq.next_element()? { Some(e) => e, None => return Err(Error::invalid_length(16, &self)) },
69+
match seq.next_element()? { Some(e) => e, None => return Err(Error::invalid_length(16, &self)) },
70+
match seq.next_element()? { Some(e) => e, None => return Err(Error::invalid_length(16, &self)) },
71+
match seq.next_element()? { Some(e) => e, None => return Err(Error::invalid_length(16, &self)) },
72+
match seq.next_element()? { Some(e) => e, None => return Err(Error::invalid_length(16, &self)) },
73+
match seq.next_element()? { Some(e) => e, None => return Err(Error::invalid_length(16, &self)) },
74+
match seq.next_element()? { Some(e) => e, None => return Err(Error::invalid_length(16, &self)) },
75+
match seq.next_element()? { Some(e) => e, None => return Err(Error::invalid_length(16, &self)) },
76+
match seq.next_element()? { Some(e) => e, None => return Err(Error::invalid_length(16, &self)) },
77+
match seq.next_element()? { Some(e) => e, None => return Err(Error::invalid_length(16, &self)) },
78+
match seq.next_element()? { Some(e) => e, None => return Err(Error::invalid_length(16, &self)) },
79+
];
80+
81+
Ok(ShortGuid::from_bytes(&bytes))
82+
}
83+
}
84+
85+
deserializer.deserialize_str(ShortGuidVisitor)
86+
} else {
87+
let uuid = Uuid::deserialize(deserializer)?;
88+
Ok(ShortGuid::from(uuid))
89+
}
90+
}
91+
}

tests/serde.rs

+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
// The original test code is taken from the uuid crate at https://github.com/uuid-rs/uuid,
2+
// licensed under an MIT or Apache-2.0 license and copyrighted as follows:
3+
//
4+
// Copyright (c) 2014 The Rust Project Developers
5+
// Copyright (c) 2018 Ashley Mannix, Christopher Armstrong, Dylan DPC, Hunar Roop Kahlon
6+
// Copyright (c) 2023 Markus Mayer
7+
//
8+
// SPDX-License-Identifier: EUPL-1.2 or MIT or Apache-2.0
9+
10+
use serde_test::{Compact, Configure, Readable, Token};
11+
use shortguid::ShortGuid;
12+
13+
#[test]
14+
fn test_serialize_readable_string() {
15+
let uuid_str = "f9168c5e-ceb2-4faa-b6bf-329bf39fa1e4";
16+
let shortguid_str = "-RaMXs6yT6q2vzKb85-h5A";
17+
let u = ShortGuid::try_parse(uuid_str).unwrap();
18+
serde_test::assert_tokens(&u.readable(), &[Token::Str(shortguid_str)]);
19+
}
20+
21+
#[test]
22+
fn test_deserialize_readable_compact() {
23+
let uuid_bytes = b"F9168C5E-CEB2-4F";
24+
let u = ShortGuid::from_slice(uuid_bytes).unwrap();
25+
26+
serde_test::assert_de_tokens(
27+
&u.readable(),
28+
&[
29+
Token::Tuple { len: 16 },
30+
Token::U8(uuid_bytes[0]),
31+
Token::U8(uuid_bytes[1]),
32+
Token::U8(uuid_bytes[2]),
33+
Token::U8(uuid_bytes[3]),
34+
Token::U8(uuid_bytes[4]),
35+
Token::U8(uuid_bytes[5]),
36+
Token::U8(uuid_bytes[6]),
37+
Token::U8(uuid_bytes[7]),
38+
Token::U8(uuid_bytes[8]),
39+
Token::U8(uuid_bytes[9]),
40+
Token::U8(uuid_bytes[10]),
41+
Token::U8(uuid_bytes[11]),
42+
Token::U8(uuid_bytes[12]),
43+
Token::U8(uuid_bytes[13]),
44+
Token::U8(uuid_bytes[14]),
45+
Token::U8(uuid_bytes[15]),
46+
Token::TupleEnd,
47+
],
48+
);
49+
}
50+
51+
#[test]
52+
fn test_deserialize_readable_bytes() {
53+
let uuid_bytes = b"F9168C5E-CEB2-4F";
54+
let u = ShortGuid::from_slice(uuid_bytes).unwrap();
55+
serde_test::assert_de_tokens(&u.readable(), &[Token::Bytes(uuid_bytes)]);
56+
}
57+
58+
#[test]
59+
fn test_serialize_non_human_readable() {
60+
let uuid_bytes = b"F9168C5E-CEB2-4F";
61+
let u = ShortGuid::from_slice(uuid_bytes).unwrap();
62+
serde_test::assert_tokens(
63+
&u.compact(),
64+
&[Token::Bytes(&[
65+
70, 57, 49, 54, 56, 67, 53, 69, 45, 67, 69, 66, 50, 45, 52, 70,
66+
])],
67+
);
68+
}
69+
70+
#[test]
71+
fn test_de_failure() {
72+
serde_test::assert_de_tokens_error::<Readable<ShortGuid>>(
73+
&[Token::Str("hello_world")],
74+
"ShortGuid parsing failed: Invalid ID length; expected 22 characters, but got 11",
75+
);
76+
77+
serde_test::assert_de_tokens_error::<Compact<ShortGuid>>(
78+
&[Token::Bytes(b"hello_world")],
79+
"UUID parsing failed: invalid length: expected 16 bytes, found 11",
80+
);
81+
}

0 commit comments

Comments
 (0)