Skip to main content

sl_types/
search.rs

1//! Search related types
2
3#[cfg(feature = "chumsky")]
4use chumsky::{Parser, prelude::just};
5
6/// Search categories
7#[derive(
8    Debug,
9    Clone,
10    Hash,
11    PartialEq,
12    Eq,
13    PartialOrd,
14    Ord,
15    strum::EnumIs,
16    serde::Serialize,
17    serde::Deserialize,
18)]
19#[expect(
20    clippy::module_name_repetitions,
21    reason = "the type is used outside this module"
22)]
23pub enum SearchCategory {
24    /// search in all categories
25    All,
26    /// search for an avatar
27    People,
28    /// search for a parcel
29    Places,
30    /// search for an event
31    Events,
32    /// search for a group
33    Groups,
34    /// search the wiki
35    Wiki,
36    /// search the destination guide
37    Destinations,
38    /// search the classifieds
39    Classifieds,
40}
41
42/// parse a search category
43///
44/// # Errors
45///
46/// returns an error if the string could not be parsed
47#[cfg(feature = "chumsky")]
48#[must_use]
49#[expect(
50    clippy::module_name_repetitions,
51    reason = "the parser is used outside this module"
52)]
53pub fn search_category_parser<'src>()
54-> impl Parser<'src, &'src str, SearchCategory, chumsky::extra::Err<chumsky::error::Rich<'src, char>>>
55{
56    just("all")
57        .to(SearchCategory::All)
58        .or(just("people").to(SearchCategory::People))
59        .or(just("places").to(SearchCategory::Places))
60        .or(just("events").to(SearchCategory::Events))
61        .or(just("groups").to(SearchCategory::Groups))
62        .or(just("wiki").to(SearchCategory::Wiki))
63        .or(just("destinations").to(SearchCategory::Destinations))
64        .or(just("classifieds").to(SearchCategory::Classifieds))
65}
66
67impl std::fmt::Display for SearchCategory {
68    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69        match self {
70            Self::All => write!(f, "all"),
71            Self::People => write!(f, "people"),
72            Self::Places => write!(f, "places"),
73            Self::Events => write!(f, "events"),
74            Self::Groups => write!(f, "groups"),
75            Self::Wiki => write!(f, "wiki"),
76            Self::Destinations => write!(f, "destinations"),
77            Self::Classifieds => write!(f, "classifieds"),
78        }
79    }
80}
81
82/// Error deserializing SearchCategory from String
83#[derive(Debug, Clone)]
84#[expect(
85    clippy::module_name_repetitions,
86    reason = "the type is used outside this module"
87)]
88pub struct SearchCategoryParseError {
89    /// the value that could not be parsed
90    value: String,
91}
92
93impl std::fmt::Display for SearchCategoryParseError {
94    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95        write!(f, "Could not parse as SearchCategory: {}", self.value)
96    }
97}
98
99impl std::str::FromStr for SearchCategory {
100    type Err = SearchCategoryParseError;
101
102    fn from_str(s: &str) -> Result<Self, Self::Err> {
103        match s {
104            "all" => Ok(Self::All),
105            "people" => Ok(Self::People),
106            "places" => Ok(Self::Places),
107            "events" => Ok(Self::Events),
108            "groups" => Ok(Self::Groups),
109            "wiki" => Ok(Self::Wiki),
110            "destinations" => Ok(Self::Destinations),
111            "classifieds" => Ok(Self::Classifieds),
112            _ => Err(SearchCategoryParseError {
113                value: s.to_owned(),
114            }),
115        }
116    }
117}
118
119/// The id of an in-world scheduled **event** in the Second Life *events
120/// directory* (Search → Events) — the numeric handle an events-directory search
121/// result carries and the `secondlife:///app/event/<id>/about` viewer URI
122/// references.
123///
124/// Unlike the UUID-based [keys](crate::key) this is a 32-bit integer (the
125/// reference viewer parses the app-URI id with `asInteger()` and the
126/// events-directory messages carry it as a `U32`). It is a newtype rather than a
127/// bare `u32` so an events-directory id can't be transposed with any other
128/// 32-bit field.
129#[derive(
130    Debug,
131    Clone,
132    Copy,
133    PartialEq,
134    Eq,
135    Hash,
136    PartialOrd,
137    Ord,
138    Default,
139    serde::Serialize,
140    serde::Deserialize,
141)]
142pub struct EventId(pub u32);
143
144impl EventId {
145    /// Builds an event-directory id from its raw `u32` value.
146    #[must_use]
147    pub const fn new(id: u32) -> Self {
148        Self(id)
149    }
150
151    /// Returns the raw `u32` value.
152    #[must_use]
153    pub const fn get(self) -> u32 {
154        self.0
155    }
156}
157
158impl std::fmt::Display for EventId {
159    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
160        write!(f, "{}", self.0)
161    }
162}
163
164/// parse an event id
165///
166/// "12345"
167///
168/// # Errors
169///
170/// returns an error if the string could not be parsed
171#[cfg(feature = "chumsky")]
172#[must_use]
173pub fn event_id_parser<'src>()
174-> impl Parser<'src, &'src str, EventId, chumsky::extra::Err<chumsky::error::Rich<'src, char>>> {
175    crate::utils::u32_parser().map(EventId)
176}
177
178/// A classified-ad search category — the listing's classified-directory
179/// classification (a [`ClassifiedInfo`]/`DirClassifiedQuery` `Category`). The
180/// wire value is a `u32` (the viewer's classified category combo; `0` for "any
181/// category").
182///
183/// Not to be confused with a parcel's land classification or with
184/// [`SearchCategory`] (the Search-floater tab, a viewer-UI concept).
185///
186/// [`ClassifiedInfo`]: https://wiki.secondlife.com/wiki/ClassifiedInfoReply
187#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
188#[non_exhaustive]
189pub enum ClassifiedCategory {
190    /// Any category (`0`); the "any" filter in a query, or an unset listing.
191    #[default]
192    AnyCategory,
193    /// Shopping (`1`).
194    Shopping,
195    /// Land rental (`2`).
196    LandRental,
197    /// Property rental (`3`).
198    PropertyRental,
199    /// A special attraction (`4`).
200    SpecialAttraction,
201    /// New products (`5`).
202    NewProducts,
203    /// Employment (`6`).
204    Employment,
205    /// Wanted (`7`).
206    Wanted,
207    /// A service (`8`).
208    Service,
209    /// Personal (`9`).
210    Personal,
211    /// An unrecognised category value, preserved verbatim. Has no named textual
212    /// form (its [`Display`](std::fmt::Display) is the raw number).
213    Unknown(u32),
214}
215
216impl ClassifiedCategory {
217    /// Classifies a classified-category wire value.
218    #[must_use]
219    pub const fn from_u32(value: u32) -> Self {
220        match value {
221            0 => Self::AnyCategory,
222            1 => Self::Shopping,
223            2 => Self::LandRental,
224            3 => Self::PropertyRental,
225            4 => Self::SpecialAttraction,
226            5 => Self::NewProducts,
227            6 => Self::Employment,
228            7 => Self::Wanted,
229            8 => Self::Service,
230            9 => Self::Personal,
231            other => Self::Unknown(other),
232        }
233    }
234
235    /// The wire value for this category.
236    #[must_use]
237    pub const fn to_u32(self) -> u32 {
238        match self {
239            Self::AnyCategory => 0,
240            Self::Shopping => 1,
241            Self::LandRental => 2,
242            Self::PropertyRental => 3,
243            Self::SpecialAttraction => 4,
244            Self::NewProducts => 5,
245            Self::Employment => 6,
246            Self::Wanted => 7,
247            Self::Service => 8,
248            Self::Personal => 9,
249            Self::Unknown(value) => value,
250        }
251    }
252}
253
254impl std::fmt::Display for ClassifiedCategory {
255    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
256        match self {
257            Self::AnyCategory => write!(f, "any"),
258            Self::Shopping => write!(f, "shopping"),
259            Self::LandRental => write!(f, "land rental"),
260            Self::PropertyRental => write!(f, "property rental"),
261            Self::SpecialAttraction => write!(f, "special attraction"),
262            Self::NewProducts => write!(f, "new products"),
263            Self::Employment => write!(f, "employment"),
264            Self::Wanted => write!(f, "wanted"),
265            Self::Service => write!(f, "service"),
266            Self::Personal => write!(f, "personal"),
267            Self::Unknown(value) => write!(f, "{value}"),
268        }
269    }
270}
271
272impl serde::Serialize for ClassifiedCategory {
273    /// Serialized as its raw `u32` wire value, which keeps unrecognised
274    /// categories ([`ClassifiedCategory::Unknown`]) lossless across a round
275    /// trip.
276    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
277    where
278        S: serde::Serializer,
279    {
280        serializer.serialize_u32(self.to_u32())
281    }
282}
283
284impl<'de> serde::Deserialize<'de> for ClassifiedCategory {
285    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
286    where
287        D: serde::Deserializer<'de>,
288    {
289        Ok(Self::from_u32(u32::deserialize(deserializer)?))
290    }
291}
292
293/// Error deserializing `ClassifiedCategory` from a string
294#[derive(Debug, Clone)]
295pub struct ClassifiedCategoryParseError {
296    /// the value that could not be parsed
297    value: String,
298}
299
300impl std::fmt::Display for ClassifiedCategoryParseError {
301    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
302        write!(f, "Could not parse as ClassifiedCategory: {}", self.value)
303    }
304}
305
306impl std::str::FromStr for ClassifiedCategory {
307    type Err = ClassifiedCategoryParseError;
308
309    fn from_str(s: &str) -> Result<Self, Self::Err> {
310        match s {
311            "any" => Ok(Self::AnyCategory),
312            "shopping" => Ok(Self::Shopping),
313            "land rental" => Ok(Self::LandRental),
314            "property rental" => Ok(Self::PropertyRental),
315            "special attraction" => Ok(Self::SpecialAttraction),
316            "new products" => Ok(Self::NewProducts),
317            "employment" => Ok(Self::Employment),
318            "wanted" => Ok(Self::Wanted),
319            "service" => Ok(Self::Service),
320            "personal" => Ok(Self::Personal),
321            _ => Err(ClassifiedCategoryParseError {
322                value: s.to_owned(),
323            }),
324        }
325    }
326}
327
328/// parse a classified-ad category (the named variants only; the
329/// [`Unknown`](ClassifiedCategory::Unknown) catch-all has no textual form)
330///
331/// # Errors
332///
333/// returns an error if the string could not be parsed
334#[cfg(feature = "chumsky")]
335#[must_use]
336pub fn classified_category_parser<'src>() -> impl Parser<
337    'src,
338    &'src str,
339    ClassifiedCategory,
340    chumsky::extra::Err<chumsky::error::Rich<'src, char>>,
341> {
342    just("any")
343        .to(ClassifiedCategory::AnyCategory)
344        .or(just("shopping").to(ClassifiedCategory::Shopping))
345        .or(just("land rental").to(ClassifiedCategory::LandRental))
346        .or(just("property rental").to(ClassifiedCategory::PropertyRental))
347        .or(just("special attraction").to(ClassifiedCategory::SpecialAttraction))
348        .or(just("new products").to(ClassifiedCategory::NewProducts))
349        .or(just("employment").to(ClassifiedCategory::Employment))
350        .or(just("wanted").to(ClassifiedCategory::Wanted))
351        .or(just("service").to(ClassifiedCategory::Service))
352        .or(just("personal").to(ClassifiedCategory::Personal))
353}