mapstic_core/
lib.rs

1//! Core types and functionality used by the code generated by the `mapstic-derive` crate.
2
3use std::{
4    borrow::Cow,
5    cell::{Cell, RefCell},
6    cmp::Reverse,
7    collections::{BTreeSet, HashSet, LinkedList, VecDeque},
8    ffi::{CStr, CString},
9    net::{IpAddr, Ipv4Addr, Ipv6Addr},
10    num::{
11        NonZeroI16, NonZeroI32, NonZeroI64, NonZeroI8, NonZeroU16, NonZeroU32, NonZeroU64,
12        NonZeroU8,
13    },
14    sync::{
15        atomic::{
16            AtomicI16, AtomicI32, AtomicI64, AtomicI8, AtomicU16, AtomicU32, AtomicU64, AtomicU8,
17        },
18        Mutex, RwLock,
19    },
20};
21
22use indexmap::IndexMap;
23use serde::{Serialize, Serializer};
24
25mod param;
26pub use {param::Params, param::Value as ParamValue};
27
28/// A type that can be described as an explicit Elasticsearch mapping.
29///
30/// This trait must be derived, rather than being implemented directly onto a type.
31pub trait ToMapping {
32    /// Returns the Elasticsearch mapping for the type.
33    fn to_mapping() -> Mapping {
34        Self::to_mapping_with_params(Params::default())
35    }
36
37    /// Returns the Elasticsearch mapping for the type, mixing in the given options.
38    ///
39    /// This is for internal use only.
40    #[doc(hidden)]
41    fn to_mapping_with_params(options: Params) -> Mapping;
42}
43
44/// An Elasticsearch field mapping.
45///
46/// Generally speaking, this will be serialised and sent to Elasticsearch via [the update mapping
47/// API][update-mapping]. The default `serde_json` representation of this type is a valid request
48/// body for that API.
49///
50/// To use this with the `elasticsearch` crate:
51///
52/// ```ignore
53/// use elasticsearch::{http::transport::Transport, indices::IndicesPutMappingParts, Elasticsearch};
54/// use mapstic::ToMapping;
55///
56/// #[derive(ToMapping)]
57/// struct MyIndex {
58///     id: i64,
59/// }
60///
61/// #[tokio::main]
62/// async fn main() -> anyhow::Result<()> {
63///     let client = Elasticsearch::new(Transport::single_node("http://localhost:9200")?);
64///     let response = client
65///         .indices()
66///         .put_mapping(IndicesPutMappingParts::Index(&["my-index"]))
67///         .body(MyIndex::to_mapping())
68///         .send()
69///         .await?;
70///
71///     dbg!(response);
72///     Ok(())
73/// }
74/// ```
75///
76/// [update-mapping]: https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-put-mapping.html
77#[derive(Debug, Clone)]
78pub enum Mapping {
79    #[doc(hidden)]
80    Scalar(Property),
81    #[doc(hidden)]
82    Object(Object),
83}
84
85// These functions are considered internal and are not subject to any usual semver guarantees.
86#[doc(hidden)]
87impl Mapping {
88    pub fn scalar(type_name: &'static str, options: impl Into<Params>) -> Self {
89        Self::Scalar(Property {
90            ty: type_name,
91            options: options.into(),
92        })
93    }
94
95    pub fn object(
96        i: impl Iterator<Item = (&'static str, Mapping)>,
97        options: impl Into<Params>,
98    ) -> Self {
99        Self::Object(Object {
100            properties: i.collect(),
101            options: options.into(),
102        })
103    }
104}
105
106impl Serialize for Mapping {
107    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
108    where
109        S: Serializer,
110    {
111        match self {
112            Mapping::Scalar(type_name) => type_name.serialize(serializer),
113            Mapping::Object(map) => map.serialize(serializer),
114        }
115    }
116}
117
118/// A scalar field within an Elasticsearch mapping.
119#[derive(Serialize, Debug, Clone)]
120pub struct Property {
121    #[serde(rename = "type")]
122    ty: &'static str,
123
124    #[serde(flatten)]
125    options: Params,
126}
127
128/// A complex object within an Elasticsearch mapping.
129#[derive(Serialize, Debug, Clone)]
130pub struct Object {
131    properties: IndexMap<&'static str, Mapping>,
132
133    #[serde(flatten)]
134    options: Params,
135}
136
137macro_rules! impl_scalar_mapping {
138    ($name:literal, $($ty:ty),+) => {
139        $(
140            impl $crate::ToMapping for $ty {
141                fn to_mapping_with_params(options: $crate::Params) -> $crate::Mapping {
142                    $crate::Mapping::scalar($name, options)
143                }
144            }
145        )+
146    };
147}
148
149// Define common binary string mappings by default. Note that we can't impl ToMapping for Vec<u8>
150// as it'll conflict with the generic Vec<T> impl below.
151impl_scalar_mapping!("binary", &[u8], &CStr, CString);
152
153impl_scalar_mapping!("boolean", bool);
154
155// Numbers. We won't do u128 or i128 because there isn't an obvious type to map them to.
156impl_scalar_mapping!("long", i64, NonZeroI64, AtomicI64);
157impl_scalar_mapping!("unsigned_long", u64, NonZeroU64, AtomicU64);
158impl_scalar_mapping!("integer", i32, NonZeroI32, AtomicI32);
159impl_scalar_mapping!("unsigned_long", u32, NonZeroU32, AtomicU32);
160impl_scalar_mapping!("short", i16, NonZeroI16, AtomicI16);
161impl_scalar_mapping!("unsigned_long", u16, NonZeroU16, AtomicU16);
162impl_scalar_mapping!("byte", i8, NonZeroI8, AtomicI8);
163impl_scalar_mapping!("unsigned_long", u8, NonZeroU8, AtomicU8);
164impl_scalar_mapping!("double", f64);
165impl_scalar_mapping!("float", f32);
166
167#[cfg(feature = "nightly")]
168impl_scalar_mapping!("half_float", f16);
169
170// Date types. We won't use date_nanos by default, but that should be able to be opted into.
171impl_scalar_mapping!("date", std::time::SystemTime);
172
173// IP addresses.
174impl_scalar_mapping!("ip", IpAddr, Ipv4Addr, Ipv6Addr);
175
176// Strings.
177//
178// We're going to match the dynamic mapping behaviour of Elasticsearch by default, so this is not a
179// simple scalar mapping, since there are default options.
180macro_rules! impl_string_mapping {
181    ($($ty:ty),+) => {
182        $(
183            impl $crate::ToMapping for $ty {
184                fn to_mapping_with_params(options: $crate::Params) -> $crate::Mapping {
185                    let mut local = $crate::default_string_options();
186                    local.extend(options);
187
188                    $crate::Mapping::scalar("text", local)
189                }
190            }
191        )+
192    }
193}
194
195/// Default options to be applied to string types.
196///
197/// These match the defaults [when a string is dynamically mapped by Elasticsearch][dynamic].
198///
199/// [dynamic]: https://www.elastic.co/guide/en/elasticsearch/reference/current/dynamic-field-mapping.html
200pub fn default_string_options() -> Params {
201    [(
202        "fields",
203        ParamValue::Nested(
204            [(
205                "keyword",
206                ParamValue::Nested(
207                    [
208                        ("ignore_above", ParamValue::Uint(256)),
209                        ("type", ParamValue::String("keyword")),
210                    ]
211                    .into_iter()
212                    .collect(),
213                ),
214            )]
215            .into_iter()
216            .collect(),
217        ),
218    )]
219    .into_iter()
220    .collect()
221}
222
223impl_string_mapping!(String, &str, Cow<'_, str>);
224
225// chrono compatibility.
226#[cfg(feature = "chrono")]
227impl_scalar_mapping!("date", chrono::NaiveDateTime, chrono::NaiveDate);
228
229#[cfg(feature = "chrono")]
230macro_rules! impl_chrono_mapping {
231    ($name:literal, $generic:ident, $($ty:ty),+) => {
232        $(
233            #[allow(deprecated)]
234            #[cfg(feature = "chrono")]
235            impl<$generic: chrono::TimeZone> $crate::ToMapping for $ty {
236                fn to_mapping_with_params(options: $crate::Params) -> $crate::Mapping {
237                    $crate::Mapping::scalar($name, options)
238                }
239            }
240        )+
241    }
242}
243
244#[cfg(feature = "chrono")]
245impl_chrono_mapping!("date", T, chrono::DateTime<T>, chrono::Date<T>);
246
247macro_rules! impl_container_mapping {
248    (<$($generics:tt),+> $inner:ident for $ty:ty where $($where:ident: $trait:ident),*) => {
249        impl<$($generics),+> ToMapping for $ty
250        where
251            $inner: ToMapping,
252            $($where: $trait),*
253        {
254            fn to_mapping_with_params(options: Params) -> Mapping {
255                $inner::to_mapping_with_params(options)
256            }
257        }
258    };
259    (<$($generics:tt),+> $inner:ident for $ty:ty) => {
260        impl_container_mapping!(<$($generics),+> $inner for $ty where);
261    };
262    ($generic:ident for $ty:ty) => {
263        impl_container_mapping!(<$generic> $generic for $ty);
264    };
265}
266
267// Since ElasticSearch explicit mappings are always nullable, unwrap Option<> to make it easier.
268impl_container_mapping!(T for Option<T>);
269
270// Similarly, handle basic containers the same way Serde does. The collections in this section all
271// serialise into arrays, which Elasticsearch handles transparently.
272impl_container_mapping!(T for BTreeSet<T>);
273impl_container_mapping!(T for Box<T>);
274impl_container_mapping!(T for Cell<T>);
275impl_container_mapping!(<'a, T> T for Cow<'a, T> where T: Clone);
276impl_container_mapping!(T for HashSet<T>);
277impl_container_mapping!(T for LinkedList<T>);
278impl_container_mapping!(T for Mutex<T>);
279impl_container_mapping!(T for RefCell<T>);
280impl_container_mapping!(T for Reverse<T>);
281impl_container_mapping!(T for RwLock<T>);
282impl_container_mapping!(T for Vec<T>);
283impl_container_mapping!(T for VecDeque<T>);
284
285#[cfg(feature = "rc")]
286impl_container_mapping!(T for std::sync::Arc<T>);
287#[cfg(feature = "rc")]
288impl_container_mapping!(T for std::rc::Rc<T>);
289#[cfg(feature = "rc")]
290impl_container_mapping!(T for std::rc::Weak<T>);
291#[cfg(feature = "rc")]
292impl_container_mapping!(T for std::sync::Weak<T>);