1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::fmt;
5
6macro_rules! string_newtype {
7 ($(#[$meta:meta])* $name:ident) => {
8 $(#[$meta])*
9 #[derive(Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
10 pub struct $name(String);
11
12 impl $name {
13 pub fn new(value: impl Into<String>) -> Self {
15 Self(value.into())
16 }
17
18 pub fn as_str(&self) -> &str {
20 &self.0
21 }
22 }
23
24 impl AsRef<str> for $name {
25 fn as_ref(&self) -> &str {
26 self.as_str()
27 }
28 }
29
30 impl From<String> for $name {
31 fn from(value: String) -> Self {
32 Self::new(value)
33 }
34 }
35
36 impl From<&str> for $name {
37 fn from(value: &str) -> Self {
38 Self::new(value)
39 }
40 }
41
42 impl fmt::Display for $name {
43 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
44 formatter.write_str(self.as_str())
45 }
46 }
47 };
48}
49
50string_newtype! {
51 IndexName
53}
54string_newtype! {
55 SearchDocumentId
57}
58string_newtype! {
59 SearchField
61}
62string_newtype! {
63 SearchTerm
65}
66string_newtype! {
67 SearchAnalyzer
69}
70string_newtype! {
71 SearchFilter
73}
74
75#[derive(Clone, Debug, Eq, PartialEq)]
77pub struct SearchIndexDocument {
78 id: SearchDocumentId,
79 fields: Vec<(SearchField, String)>,
80}
81
82impl SearchIndexDocument {
83 pub fn new(id: SearchDocumentId) -> Self {
85 Self {
86 id,
87 fields: Vec::new(),
88 }
89 }
90
91 pub fn with_field(mut self, field: SearchField, value: impl Into<String>) -> Self {
93 self.fields.push((field, value.into()));
94 self
95 }
96
97 pub const fn id(&self) -> &SearchDocumentId {
99 &self.id
100 }
101
102 pub fn fields(&self) -> &[(SearchField, String)] {
104 &self.fields
105 }
106}
107
108#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
110pub enum SearchQueryShape {
111 Term,
112 Phrase,
113 Boolean,
114 Vector,
115 Hybrid,
116 #[default]
117 Unknown,
118}
119
120impl SearchQueryShape {
121 pub const fn as_str(self) -> &'static str {
123 match self {
124 Self::Term => "term",
125 Self::Phrase => "phrase",
126 Self::Boolean => "boolean",
127 Self::Vector => "vector",
128 Self::Hybrid => "hybrid",
129 Self::Unknown => "unknown",
130 }
131 }
132}
133
134impl fmt::Display for SearchQueryShape {
135 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
136 formatter.write_str(self.as_str())
137 }
138}
139
140#[derive(Clone, Debug, Eq, PartialEq)]
142pub struct SearchSort {
143 field: SearchField,
144 descending: bool,
145}
146
147impl SearchSort {
148 pub const fn ascending(field: SearchField) -> Self {
150 Self {
151 field,
152 descending: false,
153 }
154 }
155
156 pub const fn descending(field: SearchField) -> Self {
158 Self {
159 field,
160 descending: true,
161 }
162 }
163
164 pub const fn field(&self) -> &SearchField {
166 &self.field
167 }
168
169 pub const fn is_descending(&self) -> bool {
171 self.descending
172 }
173}
174
175#[cfg(test)]
176mod tests {
177 use super::{
178 IndexName, SearchAnalyzer, SearchDocumentId, SearchField, SearchFilter,
179 SearchIndexDocument, SearchQueryShape, SearchSort, SearchTerm,
180 };
181
182 #[test]
183 fn constructs_search_labels() {
184 assert_eq!(IndexName::new("reviews").to_string(), "reviews");
185 assert_eq!(SearchTerm::new("plumber").as_ref(), "plumber");
186 assert_eq!(SearchAnalyzer::new("standard").as_str(), "standard");
187 assert_eq!(SearchFilter::new("rating >= 4").to_string(), "rating >= 4");
188 }
189
190 #[test]
191 fn builds_documents_and_sorts() {
192 let document = SearchIndexDocument::new(SearchDocumentId::new("review_1"))
193 .with_field(SearchField::new("title"), "Great service");
194 let sort = SearchSort::descending(SearchField::new("rating"));
195
196 assert_eq!(document.fields().len(), 1);
197 assert!(sort.is_descending());
198 assert_eq!(SearchQueryShape::Hybrid.to_string(), "hybrid");
199 }
200}