Skip to main content

wowlab_engine_ports/
types.rs

1//! Spell-ID polarity newtypes for the engine-ports crate boundary.
2
3use serde::{Deserialize, Serialize};
4use wowlab_types::sim::SpellIdx;
5
6/// Database-polarity (`i32`) spell identifier matching `PostgREST` `int4` columns.
7#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
8#[serde(transparent)]
9#[repr(transparent)]
10pub struct SpellId(i32);
11
12impl SpellId {
13    #[inline]
14    #[must_use]
15    pub const fn new(id: i32) -> Self {
16        Self(id)
17    }
18
19    #[inline]
20    #[must_use]
21    pub const fn as_i32(&self) -> i32 {
22        self.0
23    }
24}
25
26impl std::fmt::Display for SpellId {
27    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28        self.0.fmt(f)
29    }
30}
31
32impl From<SpellId> for i32 {
33    #[inline]
34    fn from(id: SpellId) -> Self {
35        id.0
36    }
37}
38
39/// Returned when converting a negative [`SpellId`] into a [`SpellIdx`].
40#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
41#[error("negative spell id {value} cannot be converted to SpellIdx")]
42pub struct NegativeIdError {
43    value: i32,
44    #[source]
45    source: std::num::TryFromIntError,
46}
47
48impl NegativeIdError {
49    /// Returns the rejected negative identifier.
50    #[must_use]
51    pub const fn value(self) -> i32 {
52        self.value
53    }
54}
55
56impl TryFrom<SpellId> for SpellIdx {
57    type Error = NegativeIdError;
58
59    #[inline]
60    fn try_from(id: SpellId) -> Result<Self, Self::Error> {
61        let raw = u32::try_from(id.0).map_err(|source| NegativeIdError {
62            value: id.0,
63            source,
64        })?;
65
66        Ok(SpellIdx::from_raw(raw))
67    }
68}
69
70/// Returned when a runtime [`SpellIdx`] exceeds the database `int4` range.
71#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
72#[error("runtime spell id {value} cannot be represented by database int4 SpellId")]
73pub struct SpellIdConversionError {
74    value: u32,
75    #[source]
76    source: std::num::TryFromIntError,
77}
78
79impl SpellIdConversionError {
80    /// Returns the rejected runtime identifier.
81    #[must_use]
82    pub const fn value(self) -> u32 {
83        self.value
84    }
85}
86
87impl TryFrom<SpellIdx> for SpellId {
88    type Error = SpellIdConversionError;
89
90    #[inline]
91    fn try_from(idx: SpellIdx) -> Result<Self, Self::Error> {
92        let value = idx.as_u32();
93        let database_id =
94            i32::try_from(value).map_err(|source| SpellIdConversionError { value, source })?;
95
96        Ok(Self(database_id))
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use googletest::prelude::*;
103
104    use super::*;
105
106    #[gtest]
107    fn round_trip_positive() -> Result<()> {
108        let id = SpellId::new(12345);
109        let idx: SpellIdx = id.try_into().or_fail()?;
110
111        verify_that!(idx.as_u32(), eq(12345))?;
112        let back = SpellId::try_from(idx).or_fail()?;
113
114        verify_that!(back, eq(id))
115    }
116
117    #[gtest]
118    fn negative_rejected() -> Result<()> {
119        let id = SpellId::new(-1);
120        let error = SpellIdx::try_from(id).err().or_fail()?;
121
122        verify_that!(error.value(), eq(-1))?;
123
124        verify_that!(std::error::Error::source(&error), some(anything()))
125    }
126
127    #[gtest]
128    fn database_minimum_rejected_by_runtime_boundary() -> Result<()> {
129        let error = SpellIdx::try_from(SpellId::new(i32::MIN)).err().or_fail()?;
130
131        verify_that!(error.value(), eq(i32::MIN))?;
132
133        verify_that!(std::error::Error::source(&error), some(anything()))
134    }
135
136    #[gtest]
137    fn zero_ok() -> Result<()> {
138        let id = SpellId::new(0);
139        let idx: SpellIdx = id.try_into().or_fail()?;
140
141        verify_that!(idx.as_u32(), eq(0))
142    }
143
144    #[gtest]
145    fn runtime_boundary_accepts_database_int4_maximum() -> Result<()> {
146        let idx = SpellIdx::from_raw(i32::MAX as u32);
147        let id = SpellId::try_from(idx).or_fail()?;
148
149        verify_that!(id.as_i32(), eq(i32::MAX))?;
150
151        verify_that!(SpellIdx::try_from(id).or_fail()?, eq(idx))
152    }
153
154    #[gtest]
155    fn runtime_boundary_rejects_first_value_above_database_int4() -> Result<()> {
156        let value = i32::MAX as u32 + 1;
157        let error = SpellId::try_from(SpellIdx::from_raw(value))
158            .err()
159            .or_fail()?;
160
161        verify_that!(error.value(), eq(value))?;
162        verify_that!(
163            error.to_string(),
164            eq("runtime spell id 2147483648 cannot be represented by database int4 SpellId")
165        )?;
166
167        verify_that!(std::error::Error::source(&error), some(anything()))
168    }
169
170    #[gtest]
171    fn runtime_boundary_rejects_u32_maximum() -> Result<()> {
172        let error = SpellId::try_from(SpellIdx::from_raw(u32::MAX))
173            .err()
174            .or_fail()?;
175
176        verify_that!(error.value(), eq(u32::MAX))
177    }
178}