oxide_generation/lib.rs
1//! Untyped and typed generation numbers.
2//!
3//! # Motivation
4//!
5//! Oxide's [Omicron](https://github.com/oxidecomputer/omicron) tracks many
6//! different kinds of generation numbers: versions attached to individual
7//! resources, configuration generations, and so on. However, a bare integer
8//! does not carry information about the kind of counter it belongs to, which
9//! can lead to comparing or assigning the wrong generation number at runtime.
10//!
11//! This crate provides a [`Generation`] type for untyped generation numbers,
12//! along with a wrapper type around it that allows you to specify the kind of
13//! counter a generation number belongs to.
14//!
15//! This crate is modeled after [newtype-uuid](https://docs.rs/newtype-uuid).
16//! Rust doesn't have [higher-kinded
17//! types](https://en.wikipedia.org/wiki/Kind_(type_theory)), so one must write
18//! separate libraries for each kind of thing one might want to add sets of
19//! newtypes around.
20//!
21//! # Example
22//!
23//! ```
24//! use oxide_generation::{
25//! GenericGeneration, TypedGeneration, TypedGenerationKind, TypedGenerationTag,
26//! };
27//!
28//! // First, define a type that represents the kind of generation number this is.
29//! enum MyKind {}
30//!
31//! impl TypedGenerationKind for MyKind {
32//! // Tags are required to be ASCII identifiers, with underscores
33//! // and dashes also supported. Because the tag is an associated
34//! // constant, its validity is checked at compile time, at the point
35//! // where the tag is first used.
36//! const TAG: TypedGenerationTag = TypedGenerationTag::new("my_kind");
37//! }
38//!
39//! // Now, a generation number can be created with this kind.
40//! let generation: TypedGeneration<MyKind> = "5".parse().unwrap();
41//!
42//! // The Display (and therefore ToString) impls still show the same value.
43//! assert_eq!(generation.to_string(), "5");
44//!
45//! // The Debug impl will show the tag as well.
46//! assert_eq!(format!("{:?}", generation), "5 (my_kind)");
47//! ```
48//!
49//! If you have a large number of generation kinds, consider using
50//! [`oxide-generation-macros`] which comes with several convenience features.
51//!
52//! ```
53//! use oxide_generation_macros::impl_typed_generation_kinds;
54//!
55//! // Invoke this macro with:
56//! impl_typed_generation_kinds! {
57//! kinds = {
58//! User = {},
59//! Project = {},
60//! // ...
61//! },
62//! }
63//! ```
64//!
65//! See [`oxide-generation-macros`] for more information.
66//!
67//! [`oxide-generation-macros`]: https://docs.rs/oxide-generation-macros
68//!
69//! For simpler cases, you can also write your own declarative macro. Use this
70//! template to get started:
71//!
72//! ```rust
73//! # use oxide_generation::{TypedGenerationKind, TypedGenerationTag};
74//! macro_rules! impl_kinds {
75//! ($($kind:ident => $tag:literal),* $(,)?) => {
76//! $(
77//! pub enum $kind {}
78//!
79//! impl TypedGenerationKind for $kind {
80//! const TAG: TypedGenerationTag = TypedGenerationTag::new($tag);
81//! }
82//! )*
83//! };
84//! }
85//!
86//! // Invoke this macro with:
87//! impl_kinds! {
88//! UserKind => "user",
89//! ProjectKind => "project",
90//! }
91//! ```
92//!
93//! # Implementations
94//!
95//! In general, [`TypedGeneration`] uses the same wire and serialization formats as [`Generation`].
96//! This means that persistent representations of [`TypedGeneration`] are the same as
97//! [`Generation`]; [`TypedGeneration`] is intended to be helpful within Rust code, not across
98//! serialization boundaries.
99//!
100//! - The `Display` and `FromStr` impls are forwarded to the underlying [`Generation`].
101//! - If the `serde` feature is enabled, `TypedGeneration` will serialize and deserialize using the
102//! same format as [`Generation`].
103//! - If the `schemars08` feature is enabled, [`TypedGeneration`] will implement `JsonSchema` if the
104//! corresponding [`TypedGenerationKind`] implements `JsonSchema`.
105//!
106//! To abstract over typed and untyped generation numbers, the [`GenericGeneration`] trait is
107//! provided. This trait also permits conversions between typed and untyped generation numbers.
108//!
109//! # Dependencies
110//!
111//! - This crate has no required dependencies. Optional features may add further dependencies.
112//!
113//! # Features
114//!
115//! - `default`: Enables default features in the oxide-generation crate.
116//! - `std`: Enables the use of the standard library, and the `std` feature of any enabled
117//! optional dependencies. *Enabled by default.*
118//! - `serde`: Enables serialization and deserialization support via Serde. *Not enabled by
119//! default.*
120//! - `schemars08`: Enables support for generating JSON schemas via schemars 0.8. *Not enabled by
121//! default.* Note that the format of the generated schema is **not currently part** of the stable
122//! API, though we hope to stabilize it in the future. Enabling this feature also enables `std`.
123//! - `proptest1`: Enables support for generating `proptest::Arbitrary` instances of generation
124//! numbers. *Not enabled by default.* Enabling this feature also enables `std`.
125//! - `daft01`: Enables diffing support via [`daft`](https://docs.rs/daft) 0.1, treating generation
126//! numbers as leaf values. *Not enabled by default.*
127//! - `slog2`: Enables logging support via [`slog`](https://docs.rs/slog) 2.x, emitting generation
128//! numbers as integers. *Not enabled by default.*
129//!
130//! # Minimum supported Rust version (MSRV)
131//!
132//! The MSRV of this crate is **Rust 1.85.** In general, this crate will follow the MSRV of its
133//! dependencies, with an aim to be conservative.
134//!
135//! Within the 0.x series, MSRV updates will be accompanied by a minor version bump.
136
137#![forbid(unsafe_code)]
138#![warn(missing_docs)]
139#![cfg_attr(not(feature = "std"), no_std)]
140#![cfg_attr(doc_cfg, feature(doc_cfg))]
141
142/// Macro support for [`oxide-generation-macros`].
143///
144/// This module re-exports types needed for [`oxide-generation-macros`] to work.
145///
146/// [`oxide-generation-macros`]: https://docs.rs/oxide-generation-macros
147#[doc(hidden)]
148pub mod macro_support {
149 #[cfg(feature = "schemars08")]
150 pub use schemars as schemars08;
151 #[cfg(feature = "schemars08")]
152 pub use serde_json;
153}
154
155use core::{
156 cmp::Ordering,
157 fmt,
158 hash::{Hash, Hasher},
159 marker::PhantomData,
160 num::ParseIntError,
161 str::FromStr,
162};
163
164/// Generation numbers stored in the database, used for optimistic concurrency control
165///
166/// Generation numbers are monotonic counters: each successive version of a resource is assigned a
167/// generation number greater than the one before it.
168//
169// A generation is a value between 0 and 2**63-1, i.e. equivalent to a u63.
170// The reason is that generations are commonly stored as an i64 in databases,
171// and we want to disallow negative values. (We could potentially use two's
172// complement to store values greater than that as negative values, but surely
173// 2**63 is enough.)
174#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
175#[repr(transparent)]
176pub struct Generation(
177 // Generations are restricted to 2**63 - 1 as documented above.
178 u64,
179);
180
181impl Generation {
182 /// The generation number before any change has been applied, 0.
183 ///
184 /// Generation numbers conventionally start at 1 (see [`Self::new`]). Zero is
185 /// reserved to represent an initial or empty state that precedes the first
186 /// generation.
187 pub const ZERO: Generation = Generation(0);
188
189 /// The largest possible generation number, `2**63 - 1`.
190 //
191 // `as` is a little distasteful because it allows lossy conversion, but we
192 // know converting `i64::MAX` to `u64` will always succeed losslessly.
193 pub const MAX: Generation = Generation(i64::MAX as u64);
194
195 /// Creates the first generation number, which is 1.
196 ///
197 /// There is deliberately no `Default` impl: whether a value should start at
198 /// [`Self::ZERO`] or at the first generation is a choice callers must make
199 /// explicitly.
200 #[inline]
201 #[must_use]
202 #[allow(clippy::new_without_default)]
203 pub const fn new() -> Generation {
204 Generation(1)
205 }
206
207 /// Creates a generation number from a `u32`.
208 ///
209 /// Every `u32` is a valid generation number, so this conversion is infallible.
210 #[inline]
211 #[must_use]
212 pub const fn from_u32(value: u32) -> Generation {
213 // `as` is a little distasteful because it allows lossy conversion, but
214 // (a) we know converting `u32` to `u64` will always succeed
215 // losslessly, and (b) it allows to make this function `const`, unlike
216 // if we were to use `u64::from(value)`.
217 Generation(value as u64)
218 }
219
220 /// Returns the next generation number.
221 ///
222 /// # Panics
223 ///
224 /// Panics if the next generation number would exceed [`Generation::MAX`]. Use
225 /// [`Self::checked_next`] to handle overflow instead.
226 #[inline]
227 #[must_use]
228 pub const fn next(&self) -> Generation {
229 // It should technically be an operational error if this wraps or even
230 // exceeds the value allowed by an i64. But it seems unlikely enough to
231 // happen in practice that we can probably feel safe with this.
232 let next_gen = self.0 + 1;
233 assert!(
234 next_gen <= Generation::MAX.0,
235 "attempt to overflow generation number"
236 );
237 Generation(next_gen)
238 }
239
240 /// Returns the next generation number, or `None` if it would exceed [`Generation::MAX`].
241 #[inline]
242 #[must_use]
243 pub const fn checked_next(&self) -> Option<Generation> {
244 let next_gen = self.0 + 1;
245 if next_gen <= Generation::MAX.0 {
246 Some(Generation(next_gen))
247 } else {
248 None
249 }
250 }
251
252 /// Returns the previous generation number, or `None` if this is the first generation.
253 ///
254 /// Both 1 and [`Self::ZERO`] are treated as having no predecessor: `prev` never
255 /// returns [`Self::ZERO`].
256 #[inline]
257 #[must_use]
258 pub const fn prev(&self) -> Option<Generation> {
259 if self.0 > 1 {
260 Some(Generation(self.0 - 1))
261 } else {
262 None
263 }
264 }
265
266 /// Returns the generation number as a `u64`.
267 #[inline]
268 pub const fn as_u64(self) -> u64 {
269 self.0
270 }
271
272 /// Returns the generation number as an `i64`.
273 ///
274 /// This conversion is infallible because generation numbers are restricted to `2**63 - 1`.
275 #[inline]
276 pub const fn as_i64(self) -> i64 {
277 // `as` is a little distasteful because it allows lossy conversion, but
278 // we know that generation numbers never exceed `i64::MAX`, so this
279 // conversion always succeeds losslessly.
280 self.0 as i64
281 }
282}
283
284// ---
285// Trait impls
286// ---
287
288impl fmt::Display for Generation {
289 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
290 fmt::Display::fmt(&self.0, f)
291 }
292}
293
294impl FromStr for Generation {
295 type Err = ParseIntError;
296
297 fn from_str(s: &str) -> Result<Self, Self::Err> {
298 // Try to parse `s` as both an i64 and u64, returning the error from
299 // either.
300 let _ = i64::from_str(s)?;
301 Ok(Generation(u64::from_str(s)?))
302 }
303}
304
305impl From<u32> for Generation {
306 #[inline]
307 fn from(value: u32) -> Self {
308 Generation::from_u32(value)
309 }
310}
311
312impl From<Generation> for u64 {
313 #[inline]
314 fn from(g: Generation) -> Self {
315 g.0
316 }
317}
318
319impl From<Generation> for i64 {
320 #[inline]
321 fn from(g: Generation) -> Self {
322 g.as_i64()
323 }
324}
325
326impl From<&Generation> for i64 {
327 #[inline]
328 fn from(g: &Generation) -> Self {
329 // We have already validated that the value is within range.
330 g.as_i64()
331 }
332}
333
334impl TryFrom<u64> for Generation {
335 type Error = GenerationOverflowError;
336
337 fn try_from(value: u64) -> Result<Self, Self::Error> {
338 i64::try_from(value).map_err(|_| GenerationOverflowError(()))?;
339 Ok(Generation(value))
340 }
341}
342
343impl TryFrom<i64> for Generation {
344 type Error = GenerationNegativeError;
345
346 fn try_from(value: i64) -> Result<Self, Self::Error> {
347 Ok(Generation(
348 u64::try_from(value).map_err(|_| GenerationNegativeError(()))?,
349 ))
350 }
351}
352
353/// An error that occurred while converting a `u64` into a [`Generation`].
354///
355/// The value was greater than [`Generation::MAX`].
356#[derive(Clone, Debug)]
357#[non_exhaustive]
358pub struct GenerationOverflowError(());
359
360impl fmt::Display for GenerationOverflowError {
361 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
362 f.write_str("generation number too large")
363 }
364}
365
366impl core::error::Error for GenerationOverflowError {}
367
368/// An error that occurred while converting an `i64` into a [`Generation`].
369///
370/// The value was negative.
371#[derive(Clone, Debug)]
372#[non_exhaustive]
373pub struct GenerationNegativeError(());
374
375impl fmt::Display for GenerationNegativeError {
376 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
377 f.write_str("negative generation number")
378 }
379}
380
381impl core::error::Error for GenerationNegativeError {}
382
383/// A generation number with type-level information about what it's used for.
384///
385/// For more, see [the library documentation](crate).
386#[repr(transparent)]
387pub struct TypedGeneration<T: TypedGenerationKind> {
388 generation: Generation,
389 _phantom: PhantomData<T>,
390}
391
392impl<T: TypedGenerationKind> TypedGeneration<T> {
393 /// The generation number before any change has been applied, 0.
394 ///
395 /// Generation numbers conventionally start at 1 (see [`Self::new`]). Zero is
396 /// reserved to represent an initial or empty state that precedes the first
397 /// generation.
398 pub const ZERO: Self = Self {
399 generation: Generation::ZERO,
400 _phantom: PhantomData,
401 };
402
403 /// The largest possible generation number, `2**63 - 1`.
404 pub const MAX: Self = Self {
405 generation: Generation::MAX,
406 _phantom: PhantomData,
407 };
408
409 /// Creates the first generation number of this type, which is 1.
410 ///
411 /// There is deliberately no `Default` impl: whether a value should start at
412 /// [`Self::ZERO`] or at the first generation is a choice callers must make
413 /// explicitly.
414 #[inline]
415 #[must_use]
416 #[allow(clippy::new_without_default)]
417 pub const fn new() -> Self {
418 Self {
419 generation: Generation::new(),
420 _phantom: PhantomData,
421 }
422 }
423
424 /// Creates a generation number of this type from a `u32`.
425 ///
426 /// Every `u32` is a valid generation number, so this conversion is infallible.
427 #[inline]
428 #[must_use]
429 pub const fn from_u32(value: u32) -> Self {
430 Self {
431 generation: Generation::from_u32(value),
432 _phantom: PhantomData,
433 }
434 }
435
436 /// Returns the next generation number.
437 ///
438 /// # Panics
439 ///
440 /// Panics if the next generation number would exceed [`Self::MAX`]. Use
441 /// [`Self::checked_next`] to handle overflow instead.
442 #[inline]
443 #[must_use]
444 pub const fn next(&self) -> Self {
445 Self {
446 generation: self.generation.next(),
447 _phantom: PhantomData,
448 }
449 }
450
451 /// Returns the next generation number, or `None` if it would exceed [`Self::MAX`].
452 #[inline]
453 #[must_use]
454 pub const fn checked_next(&self) -> Option<Self> {
455 match self.generation.checked_next() {
456 Some(generation) => Some(Self {
457 generation,
458 _phantom: PhantomData,
459 }),
460 None => None,
461 }
462 }
463
464 /// Returns the previous generation number, or `None` if this is the first generation.
465 ///
466 /// Both 1 and [`Self::ZERO`] are treated as having no predecessor: `prev` never
467 /// returns [`Self::ZERO`].
468 #[inline]
469 #[must_use]
470 pub const fn prev(&self) -> Option<Self> {
471 match self.generation.prev() {
472 Some(generation) => Some(Self {
473 generation,
474 _phantom: PhantomData,
475 }),
476 None => None,
477 }
478 }
479
480 /// Returns the generation number as a `u64`.
481 #[inline]
482 pub const fn as_u64(self) -> u64 {
483 self.generation.as_u64()
484 }
485
486 /// Returns the generation number as an `i64`.
487 ///
488 /// This conversion is infallible because generation numbers are restricted to `2**63 - 1`.
489 #[inline]
490 pub const fn as_i64(self) -> i64 {
491 self.generation.as_i64()
492 }
493
494 /// Converts the generation number to one with looser semantics.
495 ///
496 /// By default, generation kinds are considered independent, and conversions
497 /// between them must happen via the [`GenericGeneration`] interface. But in
498 /// some cases, there may be a relationship between two different generation
499 /// kinds, and you may wish to easily convert generation numbers from one
500 /// kind to another.
501 ///
502 /// Typically, a conversion from `TypedGeneration<T>` to `TypedGeneration<U>`
503 /// is most useful when `T`'s semantics are a superset of `U`'s, or in other
504 /// words, when every `TypedGeneration<T>` is logically also a
505 /// `TypedGeneration<U>`.
506 ///
507 /// For instance:
508 ///
509 /// * Imagine you have [`TypedGenerationKind`]s for different types of
510 /// database connections, where `DbConnKind` is the general type
511 /// and `PgConnKind` is a specific kind for Postgres.
512 /// * Since every Postgres connection is also a database connection,
513 /// a cast from `TypedGeneration<PgConnKind>` to `TypedGeneration<DbConnKind>`
514 /// makes sense.
515 /// * The inverse cast would not make sense, as a database connection may not
516 /// necessarily be a Postgres connection.
517 ///
518 /// This interface provides an alternative, safer way to perform this
519 /// conversion. Indicate your intention to allow a conversion between kinds
520 /// by implementing `From<T> for U`, as shown in the example below.
521 ///
522 /// # Examples
523 ///
524 /// ```
525 /// use oxide_generation::{TypedGeneration, TypedGenerationKind, TypedGenerationTag};
526 ///
527 /// // Let's say that these generation numbers track repositories for
528 /// // different version control systems, such that you have a generic
529 /// // RepoKind:
530 /// pub enum RepoKind {}
531 /// impl TypedGenerationKind for RepoKind {
532 /// const TAG: TypedGenerationTag = TypedGenerationTag::new("repo");
533 /// }
534 ///
535 /// // You also have more specific kinds:
536 /// pub enum GitRepoKind {}
537 /// impl TypedGenerationKind for GitRepoKind {
538 /// const TAG: TypedGenerationTag = TypedGenerationTag::new("git_repo");
539 /// }
540 /// // (and HgRepoKind, JujutsuRepoKind, etc...)
541 ///
542 /// // First, define a `From` impl. This impl indicates your desire
543 /// // to convert from one kind to another.
544 /// impl From<GitRepoKind> for RepoKind {
545 /// fn from(value: GitRepoKind) -> Self {
546 /// match value {}
547 /// }
548 /// }
549 ///
550 /// // Now you can convert between them:
551 /// let git_generation: TypedGeneration<GitRepoKind> = TypedGeneration::from_u32(5);
552 /// let repo_generation: TypedGeneration<RepoKind> = git_generation.upcast();
553 /// ```
554 #[inline]
555 #[must_use]
556 pub const fn upcast<U: TypedGenerationKind>(self) -> TypedGeneration<U>
557 where
558 T: Into<U>,
559 {
560 TypedGeneration {
561 generation: self.generation,
562 _phantom: PhantomData,
563 }
564 }
565}
566
567// ---
568// Trait impls
569// ---
570
571impl<T: TypedGenerationKind> PartialEq for TypedGeneration<T> {
572 #[inline]
573 fn eq(&self, other: &Self) -> bool {
574 self.generation.eq(&other.generation)
575 }
576}
577
578impl<T: TypedGenerationKind> Eq for TypedGeneration<T> {}
579
580impl<T: TypedGenerationKind> PartialOrd for TypedGeneration<T> {
581 #[inline]
582 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
583 Some(self.cmp(other))
584 }
585}
586
587impl<T: TypedGenerationKind> Ord for TypedGeneration<T> {
588 #[inline]
589 fn cmp(&self, other: &Self) -> core::cmp::Ordering {
590 self.generation.cmp(&other.generation)
591 }
592}
593
594impl<T: TypedGenerationKind> Hash for TypedGeneration<T> {
595 #[inline]
596 fn hash<H: Hasher>(&self, state: &mut H) {
597 self.generation.hash(state);
598 }
599}
600
601impl<T: TypedGenerationKind> fmt::Debug for TypedGeneration<T> {
602 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
603 write!(f, "{} ({})", self.generation, T::TAG)
604 }
605}
606
607impl<T: TypedGenerationKind> fmt::Display for TypedGeneration<T> {
608 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
609 self.generation.fmt(f)
610 }
611}
612
613impl<T: TypedGenerationKind> Clone for TypedGeneration<T> {
614 #[inline]
615 fn clone(&self) -> Self {
616 *self
617 }
618}
619
620impl<T: TypedGenerationKind> Copy for TypedGeneration<T> {}
621
622impl<T: TypedGenerationKind> FromStr for TypedGeneration<T> {
623 type Err = ParseError;
624
625 fn from_str(s: &str) -> Result<Self, Self::Err> {
626 let generation =
627 Generation::from_str(s).map_err(|error| ParseError { error, tag: T::TAG })?;
628 Ok(Self::from_untyped_generation(generation))
629 }
630}
631
632impl<T: TypedGenerationKind> From<u32> for TypedGeneration<T> {
633 #[inline]
634 fn from(value: u32) -> Self {
635 Self::from_u32(value)
636 }
637}
638
639impl<T: TypedGenerationKind> From<TypedGeneration<T>> for u64 {
640 #[inline]
641 fn from(g: TypedGeneration<T>) -> Self {
642 g.as_u64()
643 }
644}
645
646impl<T: TypedGenerationKind> From<TypedGeneration<T>> for i64 {
647 #[inline]
648 fn from(g: TypedGeneration<T>) -> Self {
649 g.as_i64()
650 }
651}
652
653impl<T: TypedGenerationKind> From<&TypedGeneration<T>> for i64 {
654 #[inline]
655 fn from(g: &TypedGeneration<T>) -> Self {
656 g.as_i64()
657 }
658}
659
660impl<T: TypedGenerationKind> TryFrom<u64> for TypedGeneration<T> {
661 type Error = GenerationOverflowError;
662
663 fn try_from(value: u64) -> Result<Self, Self::Error> {
664 Ok(Self::from_untyped_generation(Generation::try_from(value)?))
665 }
666}
667
668impl<T: TypedGenerationKind> TryFrom<i64> for TypedGeneration<T> {
669 type Error = GenerationNegativeError;
670
671 fn try_from(value: i64) -> Result<Self, Self::Error> {
672 Ok(Self::from_untyped_generation(Generation::try_from(value)?))
673 }
674}
675
676#[cfg(feature = "serde")]
677mod serde_imp {
678 use super::*;
679 use serde::{Deserialize, Deserializer, Serialize, Serializer};
680
681 impl<'de> Deserialize<'de> for Generation {
682 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
683 where
684 D: Deserializer<'de>,
685 {
686 let value = u64::deserialize(deserializer)?;
687 Generation::try_from(value).map_err(|GenerationOverflowError(_)| {
688 serde::de::Error::invalid_value(
689 serde::de::Unexpected::Unsigned(value),
690 &"an integer between 0 and 9223372036854775807",
691 )
692 })
693 }
694 }
695
696 // This is the equivalent of applying `#[serde(transparent)]`, written out by
697 // hand to match the manual `Deserialize` impl above.
698 impl Serialize for Generation {
699 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
700 where
701 S: Serializer,
702 {
703 self.0.serialize(serializer)
704 }
705 }
706
707 /// Deserializes a `TypedGeneration<T>` using the same format as [`Generation`].
708 ///
709 /// This impl does not require `T` to implement `Deserialize`.
710 impl<'de, T: TypedGenerationKind> Deserialize<'de> for TypedGeneration<T> {
711 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
712 where
713 D: Deserializer<'de>,
714 {
715 Ok(Self::from_untyped_generation(Generation::deserialize(
716 deserializer,
717 )?))
718 }
719 }
720
721 /// Serializes a `TypedGeneration<T>` using the same format as [`Generation`].
722 ///
723 /// This impl does not require `T` to implement `Serialize`.
724 impl<T: TypedGenerationKind> Serialize for TypedGeneration<T> {
725 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
726 where
727 S: Serializer,
728 {
729 self.generation.serialize(serializer)
730 }
731 }
732}
733
734#[cfg(feature = "schemars08")]
735mod schemars08_imp {
736 use super::*;
737 use schemars::{
738 JsonSchema, SchemaGenerator,
739 schema::{InstanceType, Metadata, NumberValidation, Schema, SchemaObject},
740 schema_for,
741 };
742
743 // This must be the first line of the doc comment on `Generation`.
744 const GENERATION_DESCRIPTION: &str =
745 "Generation numbers stored in the database, used for optimistic concurrency control";
746
747 const CRATE_NAME: &str = "oxide-generation";
748 // 0.x crates are semver-compatible only within a minor version, so this is
749 // "0.1" rather than "0".
750 const CRATE_VERSION: &str = "0.1";
751 const CRATE_PATH: &str = "oxide_generation::TypedGeneration";
752
753 /// Implements `JsonSchema` for `Generation`.
754 ///
755 /// The schema is that of a `u64` with a minimum of 0; the upper bound is not expressed in the
756 /// schema. The description matches the first line of the doc comment on [`Generation`].
757 impl JsonSchema for Generation {
758 #[inline]
759 fn schema_name() -> String {
760 "Generation".to_owned()
761 }
762
763 #[inline]
764 fn schema_id() -> std::borrow::Cow<'static, str> {
765 std::borrow::Cow::Borrowed("oxide_generation::Generation")
766 }
767
768 #[inline]
769 fn json_schema(_: &mut SchemaGenerator) -> Schema {
770 generation_schema_object().into()
771 }
772 }
773
774 /// Implements `JsonSchema` for `TypedGeneration<T>`, if `T` implements `JsonSchema`.
775 ///
776 /// * `schema_name` is set to `"TypedGenerationFor"`, concatenated by the schema name of `T`.
777 /// * `schema_id` is set to `format!("oxide_generation::TypedGeneration<{}>",
778 /// T::schema_id())`.
779 /// * `json_schema` is the same as the one for `Generation` (including its description), with
780 /// the `x-rust-type` extension to allow automatic replacement in typify and progenitor.
781 impl<T> JsonSchema for TypedGeneration<T>
782 where
783 T: TypedGenerationKind + JsonSchema,
784 {
785 #[inline]
786 fn schema_name() -> String {
787 // Use the alias if available, otherwise generate our own schema name.
788 if let Some(alias) = T::ALIAS {
789 alias.to_owned()
790 } else {
791 format!("TypedGenerationFor{}", T::schema_name())
792 }
793 }
794
795 #[inline]
796 fn schema_id() -> std::borrow::Cow<'static, str> {
797 std::borrow::Cow::Owned(format!(
798 "oxide_generation::TypedGeneration<{}>",
799 T::schema_id()
800 ))
801 }
802
803 #[inline]
804 fn json_schema(generator: &mut SchemaGenerator) -> Schema {
805 // Look at the schema for `T`. If it has `x-rust-type`, *and* if an
806 // alias is available, we can lift up the `x-rust-type` into our own schema.
807 //
808 // We use a new schema generator for `T` to avoid T's schema being
809 // added to the list of schemas in `generator` in case the lifting
810 // is successful.
811 let t_schema = schema_for!(T);
812 if let Some(schema) = lift_json_schema(&t_schema.schema, T::ALIAS) {
813 return schema.into();
814 }
815
816 SchemaObject {
817 extensions: [(
818 "x-rust-type".to_string(),
819 serde_json::json!({
820 "crate": CRATE_NAME,
821 "version": CRATE_VERSION,
822 "path": CRATE_PATH,
823 "parameters": [generator.subschema_for::<T>()]
824 }),
825 )]
826 .into_iter()
827 .collect(),
828 ..generation_schema_object()
829 }
830 .into()
831 }
832 }
833
834 // The schema shared by `Generation` and `TypedGeneration<T>`: a `u64` with a
835 // minimum of 0, carrying the `Generation` description.
836 fn generation_schema_object() -> SchemaObject {
837 SchemaObject {
838 metadata: Some(Box::new(Metadata {
839 description: Some(GENERATION_DESCRIPTION.to_owned()),
840 ..Default::default()
841 })),
842 instance_type: Some(InstanceType::Integer.into()),
843 format: Some("uint64".to_string()),
844 number: Some(Box::new(NumberValidation {
845 minimum: Some(0.0),
846 ..Default::default()
847 })),
848 ..Default::default()
849 }
850 }
851
852 // ? on Option is too easy to make mistakes with, so we use `let Some(..) =
853 // .. else` instead.
854 #[allow(clippy::question_mark)]
855 fn lift_json_schema(schema: &SchemaObject, alias: Option<&str>) -> Option<SchemaObject> {
856 let Some(alias) = alias else {
857 return None;
858 };
859
860 let Some(v) = schema.extensions.get("x-rust-type") else {
861 return None;
862 };
863
864 // The crate, version and path must all be present.
865 let Some(crate_) = v.get("crate") else {
866 return None;
867 };
868 let Some(version) = v.get("version") else {
869 return None;
870 };
871 let Some(path) = v.get("path").and_then(|p| p.as_str()) else {
872 return None;
873 };
874 let Some((module_path, _)) = path.rsplit_once("::") else {
875 return None;
876 };
877
878 // The preconditions are all met. We can lift the schema by appending
879 // the alias to the module path.
880 let alias_path = format!("{module_path}::{alias}");
881
882 Some(SchemaObject {
883 extensions: [(
884 "x-rust-type".to_string(),
885 serde_json::json!({
886 "crate": crate_,
887 "version": version,
888 "path": alias_path,
889 }),
890 )]
891 .into_iter()
892 .collect(),
893 ..generation_schema_object()
894 })
895 }
896}
897
898#[cfg(feature = "proptest1")]
899mod proptest1_imp {
900 use super::*;
901 use proptest::{
902 arbitrary::Arbitrary,
903 strategy::{BoxedStrategy, Strategy},
904 };
905
906 /// Parameters for use with `proptest` instances.
907 ///
908 /// This is currently not exported as a type because it has no options. But
909 /// it's left in as an extension point for the future.
910 #[derive(Clone, Debug, Default)]
911 pub struct GenerationParams(());
912
913 /// Generates random `Generation` instances.
914 ///
915 /// Values are drawn uniformly from the range `0..=`[`Generation::MAX`].
916 impl Arbitrary for Generation {
917 type Parameters = GenerationParams;
918 type Strategy = BoxedStrategy<Self>;
919
920 fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
921 (0..=Generation::MAX.as_u64()).prop_map(Generation).boxed()
922 }
923 }
924
925 /// Parameters for use with `proptest` instances.
926 ///
927 /// This is currently not exported as a type because it has no options. But
928 /// it's left in as an extension point for the future.
929 #[derive(Clone, Debug, Default)]
930 pub struct TypedGenerationParams(());
931
932 /// Generates random `TypedGeneration<T>` instances.
933 ///
934 /// Values are drawn uniformly from the range `0..=`[`TypedGeneration::MAX`].
935 impl<T> Arbitrary for TypedGeneration<T>
936 where
937 T: TypedGenerationKind,
938 {
939 type Parameters = TypedGenerationParams;
940 type Strategy = BoxedStrategy<Self>;
941
942 fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
943 (0..=Generation::MAX.as_u64())
944 .prop_map(|value| TypedGeneration::<T>::from_untyped_generation(Generation(value)))
945 .boxed()
946 }
947 }
948}
949
950#[cfg(feature = "daft01")]
951mod daft01_imp {
952 use super::*;
953
954 /// Diffs a `Generation` as a leaf value.
955 ///
956 /// Generation numbers are scalars, so diffing stops at them.
957 impl daft::Diffable for Generation {
958 type Diff<'daft> = daft::Leaf<&'daft Self>;
959
960 fn diff<'daft>(&'daft self, other: &'daft Self) -> Self::Diff<'daft> {
961 daft::Leaf {
962 before: self,
963 after: other,
964 }
965 }
966 }
967
968 /// Diffs a `TypedGeneration<T>` as a leaf value.
969 ///
970 /// This impl does not require `T` to implement `Diffable`.
971 impl<T: TypedGenerationKind> daft::Diffable for TypedGeneration<T> {
972 type Diff<'daft> = daft::Leaf<&'daft Self>;
973
974 fn diff<'daft>(&'daft self, other: &'daft Self) -> Self::Diff<'daft> {
975 daft::Leaf {
976 before: self,
977 after: other,
978 }
979 }
980 }
981}
982
983#[cfg(feature = "slog2")]
984mod slog2_imp {
985 use super::*;
986
987 /// Logs a `Generation` as an integer.
988 impl slog::Value for Generation {
989 fn serialize(
990 &self,
991 _rec: &slog::Record,
992 key: slog::Key,
993 serializer: &mut dyn slog::Serializer,
994 ) -> slog::Result {
995 serializer.emit_u64(key, self.as_u64())
996 }
997 }
998
999 /// Logs a `TypedGeneration<T>` as an integer, using the same format as [`Generation`].
1000 ///
1001 /// This impl does not require `T` to implement `slog::Value`.
1002 impl<T: TypedGenerationKind> slog::Value for TypedGeneration<T> {
1003 fn serialize(
1004 &self,
1005 _rec: &slog::Record,
1006 key: slog::Key,
1007 serializer: &mut dyn slog::Serializer,
1008 ) -> slog::Result {
1009 serializer.emit_u64(key, self.as_u64())
1010 }
1011 }
1012}
1013
1014/// Represents marker types that can be used as a type parameter for [`TypedGeneration`].
1015///
1016/// Generally, an implementation of this will be a zero-sized type that can never be constructed. An
1017/// empty struct or enum works well for this.
1018///
1019/// # Implementations
1020///
1021/// If the `schemars08` feature is enabled, and [`JsonSchema`] is implemented for a kind `T`, then
1022/// [`TypedGeneration`]`<T>` will also implement [`JsonSchema`].
1023///
1024/// If you have a large number of generation kinds, consider using
1025/// [`oxide-generation-macros`] which comes with several convenience features.
1026///
1027/// ```
1028/// use oxide_generation_macros::impl_typed_generation_kinds;
1029///
1030/// // Invoke this macro with:
1031/// impl_typed_generation_kinds! {
1032/// kinds = {
1033/// User = {},
1034/// Project = {},
1035/// // ...
1036/// },
1037/// }
1038/// ```
1039///
1040/// See [`oxide-generation-macros`] for more information.
1041///
1042/// [`oxide-generation-macros`]: https://docs.rs/oxide-generation-macros
1043/// [`JsonSchema`]: schemars::JsonSchema
1044pub trait TypedGenerationKind: Send + Sync + 'static {
1045 /// The corresponding tag for this kind.
1046 ///
1047 /// The tag forms a runtime representation of this type.
1048 ///
1049 /// The tag is required to be a static string.
1050 const TAG: TypedGenerationTag;
1051
1052 /// A string that corresponds to a type alias for `TypedGeneration<Self>`,
1053 /// if one is defined.
1054 ///
1055 /// The type alias must be defined in the same module as `Self`. This
1056 /// constant is used by the schemars integration to refer to embed a
1057 /// reference to that alias in the schema, if available.
1058 ///
1059 /// This is usually defined by the [`oxide-generation-macros`] crate.
1060 ///
1061 /// [`oxide-generation-macros`]: https://docs.rs/oxide-generation-macros
1062 const ALIAS: Option<&'static str> = None;
1063}
1064
1065/// Describes what kind of [`TypedGeneration`] something is.
1066///
1067/// This is the runtime equivalent of [`TypedGenerationKind`].
1068#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
1069pub struct TypedGenerationTag(&'static str);
1070
1071impl TypedGenerationTag {
1072 /// Creates a new `TypedGenerationTag` from a static string.
1073 ///
1074 /// The string must be non-empty, and consist of:
1075 /// - ASCII letters
1076 /// - digits (only after the first character)
1077 /// - underscores
1078 /// - hyphens (only after the first character)
1079 ///
1080 /// # Panics
1081 ///
1082 /// Panics if the above conditions aren't met. Use [`Self::try_new`] to handle errors instead.
1083 #[must_use]
1084 pub const fn new(tag: &'static str) -> Self {
1085 match Self::try_new_impl(tag) {
1086 Ok(tag) => tag,
1087 Err(message) => panic!("{}", message),
1088 }
1089 }
1090
1091 /// Attempts to create a new `TypedGenerationTag` from a static string.
1092 ///
1093 /// The string must be non-empty, and consist of:
1094 /// - ASCII letters
1095 /// - digits (only after the first character)
1096 /// - underscores
1097 /// - hyphens (only after the first character)
1098 ///
1099 /// # Errors
1100 ///
1101 /// Returns a [`TagError`] if the above conditions aren't met.
1102 pub const fn try_new(tag: &'static str) -> Result<Self, TagError> {
1103 match Self::try_new_impl(tag) {
1104 Ok(tag) => Ok(tag),
1105 Err(message) => Err(TagError {
1106 input: tag,
1107 message,
1108 }),
1109 }
1110 }
1111
1112 const fn try_new_impl(tag: &'static str) -> Result<Self, &'static str> {
1113 if tag.is_empty() {
1114 return Err("tag must not be empty");
1115 }
1116
1117 let bytes = tag.as_bytes();
1118 if !(bytes[0].is_ascii_alphabetic() || bytes[0] == b'_') {
1119 return Err("first character of tag must be an ASCII letter or underscore");
1120 }
1121
1122 let mut bytes = match bytes {
1123 [_, rest @ ..] => rest,
1124 [] => panic!("already checked that it's non-empty"),
1125 };
1126 while let [rest @ .., last] = &bytes {
1127 if !(last.is_ascii_alphanumeric() || *last == b'_' || *last == b'-') {
1128 break;
1129 }
1130 bytes = rest;
1131 }
1132
1133 if !bytes.is_empty() {
1134 return Err("tag must only contain ASCII letters, digits, underscores, or hyphens");
1135 }
1136
1137 Ok(Self(tag))
1138 }
1139
1140 /// Returns the tag as a string.
1141 pub const fn as_str(&self) -> &'static str {
1142 self.0
1143 }
1144}
1145
1146impl fmt::Display for TypedGenerationTag {
1147 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1148 f.write_str(self.0)
1149 }
1150}
1151
1152impl AsRef<str> for TypedGenerationTag {
1153 fn as_ref(&self) -> &str {
1154 self.0
1155 }
1156}
1157
1158/// An error that occurred while creating a [`TypedGenerationTag`].
1159#[derive(Clone, Debug)]
1160#[non_exhaustive]
1161pub struct TagError {
1162 /// The input string.
1163 pub input: &'static str,
1164
1165 /// The error message.
1166 pub message: &'static str,
1167}
1168
1169impl fmt::Display for TagError {
1170 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1171 write!(
1172 f,
1173 "error creating tag from '{}': {}",
1174 self.input, self.message
1175 )
1176 }
1177}
1178
1179impl core::error::Error for TagError {}
1180
1181/// An error that occurred while parsing a [`TypedGeneration`].
1182#[derive(Clone, Debug)]
1183#[non_exhaustive]
1184pub struct ParseError {
1185 /// The underlying error.
1186 pub error: ParseIntError,
1187
1188 /// The tag of the generation number that failed to parse.
1189 pub tag: TypedGenerationTag,
1190}
1191
1192impl fmt::Display for ParseError {
1193 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1194 write!(f, "error parsing generation number ({})", self.tag)
1195 }
1196}
1197
1198impl core::error::Error for ParseError {
1199 fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
1200 Some(&self.error)
1201 }
1202}
1203
1204/// A trait abstracting over typed and untyped generation numbers.
1205///
1206/// This can be used to write code that's generic over [`TypedGeneration`], [`Generation`], and
1207/// other types that may wrap [`TypedGeneration`] (due to e.g. orphan rules).
1208///
1209/// This trait is similar to `From`, but a bit harder to get wrong -- in general, the conversion
1210/// from and to untyped generation numbers should be careful and explicit.
1211pub trait GenericGeneration {
1212 /// Creates a new instance of `Self` from an untyped [`Generation`].
1213 #[must_use]
1214 fn from_untyped_generation(generation: Generation) -> Self
1215 where
1216 Self: Sized;
1217
1218 /// Converts `self` into an untyped [`Generation`].
1219 #[must_use]
1220 fn into_untyped_generation(self) -> Generation
1221 where
1222 Self: Sized;
1223
1224 /// Returns the inner [`Generation`].
1225 ///
1226 /// Generally, [`into_untyped_generation`](Self::into_untyped_generation) should be preferred.
1227 /// However, in some cases it may be necessary to use this method to satisfy lifetime
1228 /// constraints.
1229 fn as_untyped_generation(&self) -> &Generation;
1230}
1231
1232impl GenericGeneration for Generation {
1233 #[inline]
1234 fn from_untyped_generation(generation: Generation) -> Self {
1235 generation
1236 }
1237
1238 #[inline]
1239 fn into_untyped_generation(self) -> Generation {
1240 self
1241 }
1242
1243 #[inline]
1244 fn as_untyped_generation(&self) -> &Generation {
1245 self
1246 }
1247}
1248
1249impl<T: TypedGenerationKind> GenericGeneration for TypedGeneration<T> {
1250 #[inline]
1251 fn from_untyped_generation(generation: Generation) -> Self {
1252 Self {
1253 generation,
1254 _phantom: PhantomData,
1255 }
1256 }
1257
1258 #[inline]
1259 fn into_untyped_generation(self) -> Generation {
1260 self.generation
1261 }
1262
1263 #[inline]
1264 fn as_untyped_generation(&self) -> &Generation {
1265 &self.generation
1266 }
1267}
1268
1269#[cfg(test)]
1270mod tests {
1271 use super::*;
1272
1273 enum MyKind {}
1274
1275 impl TypedGenerationKind for MyKind {
1276 const TAG: TypedGenerationTag = TypedGenerationTag::new("my_kind");
1277 }
1278
1279 #[test]
1280 fn test_validate_tags() {
1281 for &valid_tag in &[
1282 "a", "a-", "a_", "a-b", "a_b", "a1", "a1-", "a1_", "a1-b", "a1_b", "_a",
1283 ] {
1284 TypedGenerationTag::try_new(valid_tag).expect("tag is valid");
1285 // Should not panic
1286 _ = TypedGenerationTag::new(valid_tag);
1287 }
1288
1289 for invalid_tag in &["", "1", "-", "a1b!", "a1-b!", "a1_b:", "\u{1f4a9}"] {
1290 TypedGenerationTag::try_new(invalid_tag).unwrap_err();
1291 }
1292 }
1293
1294 // This test just ensures that `GenericGeneration` is object-safe.
1295 #[test]
1296 #[cfg(feature = "std")]
1297 fn test_generic_generation_object_safe() {
1298 let generation = Generation::new();
1299 let box_generation = Box::new(generation) as Box<dyn GenericGeneration>;
1300 assert_eq!(box_generation.as_untyped_generation(), &generation);
1301 }
1302
1303 #[test]
1304 fn test_next_and_prev() {
1305 // The first generation is 1, and it has no predecessor.
1306 let first = Generation::new();
1307 assert_eq!(first.as_u64(), 1);
1308 assert_eq!(first.prev(), None);
1309 assert_eq!(first.next(), Generation::from_u32(2));
1310 assert_eq!(first.checked_next(), Some(Generation::from_u32(2)));
1311
1312 // Generation 0 also has no predecessor.
1313 let zero = Generation::ZERO;
1314 assert_eq!(zero.as_u64(), 0);
1315 assert_eq!(zero.prev(), None);
1316 assert_eq!(zero.next(), first);
1317
1318 // One below the maximum still has a successor.
1319 let almost_max = Generation::try_from(Generation::MAX.as_u64() - 1).unwrap();
1320 assert_eq!(almost_max.next(), Generation::MAX);
1321 assert_eq!(almost_max.checked_next(), Some(Generation::MAX));
1322 assert_eq!(almost_max.prev().unwrap().as_u64(), i64::MAX as u64 - 2);
1323
1324 // The maximum has no successor.
1325 assert_eq!(Generation::MAX.checked_next(), None);
1326 assert_eq!(Generation::MAX.prev(), Some(almost_max));
1327
1328 // The same holds for typed generation numbers.
1329 let typed_max = TypedGeneration::<MyKind>::MAX;
1330 assert_eq!(typed_max.as_u64(), Generation::MAX.as_u64());
1331 assert_eq!(typed_max.checked_next(), None);
1332 assert_eq!(
1333 typed_max.prev().map(|g| g.as_u64()),
1334 Some(Generation::MAX.as_u64() - 1)
1335 );
1336 assert_eq!(TypedGeneration::<MyKind>::new().as_u64(), 1);
1337 assert_eq!(TypedGeneration::<MyKind>::ZERO.as_u64(), 0);
1338 assert_eq!(TypedGeneration::<MyKind>::ZERO.next().as_u64(), 1);
1339 }
1340
1341 #[test]
1342 #[should_panic(expected = "attempt to overflow generation number")]
1343 fn test_next_at_max_panics() {
1344 _ = Generation::MAX.next();
1345 }
1346
1347 #[test]
1348 #[should_panic(expected = "attempt to overflow generation number")]
1349 fn test_typed_next_at_max_panics() {
1350 _ = TypedGeneration::<MyKind>::MAX.next();
1351 }
1352
1353 #[test]
1354 fn test_try_from() {
1355 // Zero and the maximum are accepted, but one past the maximum is not.
1356 assert_eq!(Generation::try_from(0_u64).unwrap(), Generation::ZERO);
1357 assert_eq!(
1358 Generation::try_from(i64::MAX as u64).unwrap(),
1359 Generation::MAX
1360 );
1361 Generation::try_from(i64::MAX as u64 + 1).unwrap_err();
1362 Generation::try_from(u64::MAX).unwrap_err();
1363
1364 // Non-negative i64s are accepted, negative ones are not.
1365 assert_eq!(Generation::try_from(0_i64).unwrap().as_u64(), 0);
1366 assert_eq!(Generation::try_from(i64::MAX).unwrap(), Generation::MAX);
1367 Generation::try_from(-1_i64).unwrap_err();
1368 Generation::try_from(i64::MIN).unwrap_err();
1369
1370 // The same holds for typed generation numbers.
1371 assert_eq!(
1372 TypedGeneration::<MyKind>::try_from(i64::MAX as u64).unwrap(),
1373 TypedGeneration::<MyKind>::MAX
1374 );
1375 TypedGeneration::<MyKind>::try_from(i64::MAX as u64 + 1).unwrap_err();
1376 TypedGeneration::<MyKind>::try_from(-1_i64).unwrap_err();
1377 }
1378
1379 #[test]
1380 fn test_conversions() {
1381 let generation = Generation::from_u32(5);
1382 assert_eq!(u64::from(generation), 5);
1383 assert_eq!(i64::from(generation), 5);
1384 assert_eq!(i64::from(&generation), 5);
1385 assert_eq!(Generation::from(5_u32), generation);
1386
1387 let typed = TypedGeneration::<MyKind>::from_u32(5);
1388 assert_eq!(u64::from(typed), 5);
1389 assert_eq!(i64::from(typed), 5);
1390 assert_eq!(i64::from(&typed), 5);
1391 assert_eq!(TypedGeneration::<MyKind>::from(5_u32), typed);
1392 assert_eq!(typed.into_untyped_generation(), generation);
1393 assert_eq!(typed.as_untyped_generation(), &generation);
1394 assert_eq!(typed.as_i64(), 5);
1395 }
1396
1397 #[test]
1398 fn test_from_str() {
1399 assert_eq!(Generation::from_str("0").unwrap().as_u64(), 0);
1400 assert_eq!(Generation::from_str("1").unwrap(), Generation::new());
1401 assert_eq!(
1402 Generation::from_str("9223372036854775807").unwrap(),
1403 Generation::MAX
1404 );
1405
1406 // Negative numbers and numbers greater than the maximum are both
1407 // rejected.
1408 Generation::from_str("-1").unwrap_err();
1409 Generation::from_str("9223372036854775808").unwrap_err();
1410
1411 // The same holds for typed generation numbers.
1412 assert_eq!(
1413 "9223372036854775807"
1414 .parse::<TypedGeneration<MyKind>>()
1415 .unwrap(),
1416 TypedGeneration::<MyKind>::MAX
1417 );
1418 "-1".parse::<TypedGeneration<MyKind>>().unwrap_err();
1419 "9223372036854775808"
1420 .parse::<TypedGeneration<MyKind>>()
1421 .unwrap_err();
1422 }
1423
1424 // The remaining tests format values, which requires an allocator.
1425
1426 #[test]
1427 #[cfg(feature = "std")]
1428 fn test_display_and_debug() {
1429 assert_eq!(Generation::new().to_string(), "1");
1430
1431 let generation = Generation::from_u32(5);
1432 assert_eq!(generation.to_string(), "5");
1433
1434 let typed = TypedGeneration::<MyKind>::from_u32(5);
1435 assert_eq!(typed.to_string(), "5");
1436 assert_eq!(format!("{typed:?}"), "5 (my_kind)");
1437 }
1438
1439 #[test]
1440 #[cfg(feature = "std")]
1441 fn test_error_displays() {
1442 assert_eq!(
1443 Generation::try_from(i64::MAX as u64 + 1)
1444 .unwrap_err()
1445 .to_string(),
1446 "generation number too large"
1447 );
1448 assert_eq!(
1449 Generation::try_from(-1_i64).unwrap_err().to_string(),
1450 "negative generation number"
1451 );
1452 assert_eq!(
1453 "-1".parse::<TypedGeneration<MyKind>>()
1454 .unwrap_err()
1455 .to_string(),
1456 "error parsing generation number (my_kind)"
1457 );
1458 }
1459
1460 #[test]
1461 #[cfg(all(feature = "serde", feature = "std"))]
1462 fn test_serde() {
1463 let generation = Generation::from_u32(5);
1464 assert_eq!(serde_json::to_string(&generation).unwrap(), "5");
1465 assert_eq!(serde_json::from_str::<Generation>("5").unwrap(), generation);
1466
1467 // The first generation round-trips as the bare integer 1.
1468 assert_eq!(serde_json::to_string(&Generation::new()).unwrap(), "1");
1469 assert_eq!(
1470 serde_json::from_str::<Generation>("1").unwrap(),
1471 Generation::new()
1472 );
1473
1474 // Both ends of the valid range deserialize, and values outside it --
1475 // whether above the maximum or negative -- do not.
1476 assert_eq!(
1477 serde_json::from_str::<Generation>("0").unwrap(),
1478 Generation::ZERO
1479 );
1480 assert_eq!(
1481 serde_json::from_str::<Generation>(&Generation::MAX.as_u64().to_string()).unwrap(),
1482 Generation::MAX
1483 );
1484 for bad_value in [Generation::MAX.as_u64() + 1, u64::MAX] {
1485 serde_json::from_str::<Generation>(&bad_value.to_string()).unwrap_err();
1486 }
1487 for bad_value in [-1_i64, i64::MIN] {
1488 serde_json::from_str::<Generation>(&bad_value.to_string()).unwrap_err();
1489 }
1490
1491 let typed = TypedGeneration::<MyKind>::from_u32(5);
1492 assert_eq!(serde_json::to_string(&typed).unwrap(), "5");
1493 assert_eq!(
1494 serde_json::from_str::<TypedGeneration<MyKind>>("5").unwrap(),
1495 typed
1496 );
1497
1498 // One past the maximum is rejected with a message that describes the
1499 // valid range.
1500 let error = serde_json::from_str::<Generation>("9223372036854775808").unwrap_err();
1501 assert_eq!(
1502 error.to_string(),
1503 "invalid value: integer `9223372036854775808`, \
1504 expected an integer between 0 and 9223372036854775807"
1505 );
1506 serde_json::from_str::<TypedGeneration<MyKind>>("9223372036854775808").unwrap_err();
1507 }
1508
1509 #[test]
1510 #[cfg(feature = "daft01")]
1511 fn test_daft() {
1512 use daft::Diffable;
1513
1514 let before = Generation::from_u32(5);
1515 let after = Generation::from_u32(6);
1516 let diff = before.diff(&after);
1517 assert_eq!(diff.before, &before);
1518 assert_eq!(diff.after, &after);
1519
1520 // The same holds for typed generation numbers.
1521 let typed_before = TypedGeneration::<MyKind>::from_u32(5);
1522 let typed_after = TypedGeneration::<MyKind>::from_u32(6);
1523 let typed_diff = typed_before.diff(&typed_after);
1524 assert_eq!(typed_diff.before, &typed_before);
1525 assert_eq!(typed_diff.after, &typed_after);
1526 }
1527
1528 #[test]
1529 #[cfg(feature = "slog2")]
1530 fn test_slog() {
1531 // Records the value passed to `emit_u64`, so that the test can check
1532 // that generation numbers are logged as integers rather than strings.
1533 struct RecordingSerializer(Option<u64>);
1534
1535 impl slog::Serializer for RecordingSerializer {
1536 fn emit_u64(&mut self, _key: slog::Key, value: u64) -> slog::Result {
1537 self.0 = Some(value);
1538 Ok(())
1539 }
1540
1541 fn emit_arguments(&mut self, _key: slog::Key, _value: &fmt::Arguments) -> slog::Result {
1542 panic!("generation numbers are emitted via emit_u64")
1543 }
1544 }
1545
1546 // The record is built inline because `format_args!` produces a
1547 // temporary that only lives until the end of the statement.
1548 //
1549 // slog's `Key` is a plain `&'static str` unless the `dynamic-keys`
1550 // feature is enabled, so the conversion below is sometimes a no-op.
1551 #[allow(clippy::useless_conversion)]
1552 fn emitted_u64(value: &dyn slog::Value) -> Option<u64> {
1553 let mut serializer = RecordingSerializer(None);
1554 value
1555 .serialize(
1556 &slog::record!(
1557 slog::Level::Info,
1558 "",
1559 &format_args!(""),
1560 slog::BorrowedKV(&())
1561 ),
1562 "generation".into(),
1563 &mut serializer,
1564 )
1565 .expect("generation number was serialized");
1566 serializer.0
1567 }
1568
1569 assert_eq!(emitted_u64(&Generation::from_u32(5)), Some(5));
1570
1571 // The same holds for typed generation numbers.
1572 assert_eq!(
1573 emitted_u64(&TypedGeneration::<MyKind>::from_u32(6)),
1574 Some(6)
1575 );
1576 }
1577
1578 #[test]
1579 #[cfg(feature = "schemars08")]
1580 fn test_generation_schema() {
1581 // This schema must match the one omicron generates today, so that wire
1582 // schemas are unchanged.
1583 let schema = <Generation as schemars::JsonSchema>::json_schema(
1584 &mut schemars::SchemaGenerator::default(),
1585 );
1586 assert_eq!(
1587 serde_json::to_value(&schema).unwrap(),
1588 serde_json::json!({
1589 "description": "Generation numbers stored in the database, \
1590 used for optimistic concurrency control",
1591 "type": "integer",
1592 "format": "uint64",
1593 "minimum": 0.0,
1594 })
1595 );
1596 }
1597}