fraiseql_db/view_name.rs
1//! [`ViewName`] — a typed identifier for SQL views and tables.
2//!
3//! Wraps an `Arc<str>` so cloning a name across cache index entries, cache
4//! reverse indexes, and `Box<[ViewName]>` storage is a single atomic
5//! reference-count bump instead of a heap allocation.
6//!
7//! ## Why a newtype
8//!
9//! View names flow through cache invalidation, SQL generation, and observer
10//! triggers. Before this newtype existed they were passed as bare `&str`,
11//! `String`, `&String`, `Vec<String>`, and `Box<[String]>` interchangeably.
12//! Mixing a *view name* with an *arbitrary identifier* at one of those
13//! boundaries was a silent class-of-bug: a typo or a misordered argument
14//! compiled and ran without complaint.
15//!
16//! Wrapping the value in `ViewName(Arc<str>)` lets the type system enforce
17//! the distinction at API boundaries while keeping look-ups ergonomic via
18//! [`Borrow<str>`] and [`Deref<Target = str>`].
19//!
20//! ## Serialization
21//!
22//! `ViewName` is `#[serde(transparent)]` — the wire form is identical to the
23//! raw string, so it can drop into any existing JSON/`bincode` payload that
24//! previously held a `String`.
25
26use std::{borrow::Borrow, fmt, ops::Deref, sync::Arc};
27
28use serde::{Deserialize, Deserializer, Serialize, Serializer};
29
30/// Typed name of a SQL view or table.
31///
32/// Backed by `Arc<str>` so cloning is a single atomic reference-count bump,
33/// not a heap allocation. The type is intentionally distinct from `String`
34/// and `&str` so callers cannot pass an arbitrary identifier where a view
35/// name is required.
36///
37/// # Construction
38///
39/// ```rust
40/// use fraiseql_db::ViewName;
41///
42/// let from_str: ViewName = "v_user".into();
43/// let from_string: ViewName = String::from("v_user").into();
44/// assert_eq!(from_str, from_string);
45/// ```
46///
47/// # Look-up by `&str`
48///
49/// [`ViewName`] implements [`Borrow<str>`] so it can be used as a
50/// `HashMap`/`DashMap` key looked up via `&str`:
51///
52/// ```rust
53/// use std::collections::HashMap;
54/// use fraiseql_db::ViewName;
55///
56/// let mut m: HashMap<ViewName, u32> = HashMap::new();
57/// m.insert("v_user".into(), 1);
58/// assert_eq!(m.get("v_user"), Some(&1));
59/// ```
60#[derive(Clone, Debug, Eq, Hash, PartialEq, Ord, PartialOrd)]
61pub struct ViewName(Arc<str>);
62
63// Reason: hand-written serde impls keep the wire form identical to the raw
64// string (matching `#[serde(transparent)]`) without forcing the
65// workspace `serde` declaration to enable the `rc` feature flag.
66impl Serialize for ViewName {
67 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
68 where
69 S: Serializer,
70 {
71 serializer.serialize_str(&self.0)
72 }
73}
74
75impl<'de> Deserialize<'de> for ViewName {
76 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
77 where
78 D: Deserializer<'de>,
79 {
80 let s = String::deserialize(deserializer)?;
81 Ok(Self::from(s))
82 }
83}
84
85impl ViewName {
86 /// Returns the view name as `&str` without allocating.
87 #[must_use]
88 pub fn as_str(&self) -> &str {
89 &self.0
90 }
91
92 /// Returns a cheap clone of the underlying `Arc<str>` for callers that
93 /// need to thread the name through long-lived structures without bumping
94 /// the `ViewName` wrapper.
95 #[must_use]
96 pub fn as_arc(&self) -> Arc<str> {
97 Arc::clone(&self.0)
98 }
99}
100
101impl fmt::Display for ViewName {
102 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103 f.write_str(&self.0)
104 }
105}
106
107impl Deref for ViewName {
108 type Target = str;
109
110 fn deref(&self) -> &Self::Target {
111 &self.0
112 }
113}
114
115impl AsRef<str> for ViewName {
116 fn as_ref(&self) -> &str {
117 &self.0
118 }
119}
120
121impl Borrow<str> for ViewName {
122 fn borrow(&self) -> &str {
123 &self.0
124 }
125}
126
127impl From<&str> for ViewName {
128 fn from(value: &str) -> Self {
129 Self(Arc::from(value))
130 }
131}
132
133impl From<String> for ViewName {
134 fn from(value: String) -> Self {
135 Self(Arc::from(value.into_boxed_str()))
136 }
137}
138
139impl From<&String> for ViewName {
140 fn from(value: &String) -> Self {
141 Self(Arc::from(value.as_str()))
142 }
143}
144
145impl From<Arc<str>> for ViewName {
146 fn from(value: Arc<str>) -> Self {
147 Self(value)
148 }
149}
150
151impl From<ViewName> for String {
152 fn from(value: ViewName) -> Self {
153 value.0.as_ref().to_owned()
154 }
155}
156
157impl PartialEq<str> for ViewName {
158 fn eq(&self, other: &str) -> bool {
159 self.as_str() == other
160 }
161}
162
163impl PartialEq<&str> for ViewName {
164 fn eq(&self, other: &&str) -> bool {
165 self.as_str() == *other
166 }
167}
168
169impl PartialEq<String> for ViewName {
170 fn eq(&self, other: &String) -> bool {
171 self.as_str() == other.as_str()
172 }
173}
174
175#[cfg(test)]
176mod tests;