elastic_mapping/lib.rs
1//! Generate Elasticsearch mapping definitions from Rust structs and enums.
2//!
3//! This crate provides a derive macro to automatically generate Elasticsearch mapping
4//! definitions from Rust types. It respects serde attributes and supports various
5//! Elasticsearch-specific field configurations.
6//!
7//! # Features
8//!
9//! - **Derive macro** - Automatic mapping generation using `#[derive(Document)]`
10//! - **Serde compatibility** - Respects `#[serde(rename)]`, `#[serde(rename_all)]`, `#[serde(flatten)]`
11//! - **Enum support** - Handles externally tagged, internally tagged, and adjacently tagged enums
12//! - **Field annotations** - Configure analyzers, keyword fields, and other Elasticsearch settings
13//! - **Optional features** - Support for `chrono`, `url`, and `uuid` types
14//!
15//! # Quick Start
16//!
17//! ```rust
18//! use elastic_mapping::{Document, MappingObject};
19//!
20//! #[derive(Document)]
21//! struct BlogPost {
22//! title: String,
23//! content: String,
24//! published: bool,
25//! views: i64,
26//! }
27//!
28//! fn main() {
29//! let mapping = BlogPost::document_mapping();
30//! println!("{}", serde_json::to_string_pretty(mapping.inner()).unwrap());
31//! }
32//! ```
33//!
34//! # Field Annotations
35//!
36//! Use the `#[document(...)]` attribute to configure Elasticsearch-specific field settings:
37//!
38//! ```rust
39//! use elastic_mapping::Document;
40//!
41//! #[derive(Document)]
42//! struct Article {
43//! /// Configure an analyzer for text analysis
44//! #[document(analyzer = "english")]
45//! title: String,
46//!
47//! /// Add a keyword subfield that is not indexed
48//! #[document(keyword(index = false))]
49//! internal_id: String,
50//!
51//! /// Add a keyword subfield with ignore_above setting
52//! #[document(keyword(ignore_above = 256))]
53//! category: String,
54//! }
55//! ```
56//!
57//! # Serde Compatibility
58//!
59//! The macro respects common serde attributes:
60//!
61//! ```rust
62//! use elastic_mapping::Document;
63//! use serde::Serialize;
64//!
65//! #[derive(Serialize, Document)]
66//! #[serde(rename_all = "camelCase")]
67//! struct User {
68//! first_name: String, // Maps to "firstName"
69//! #[serde(rename = "mail")]
70//! email: String, // Maps to "mail"
71//! }
72//! ```
73//!
74//! # Enum Support
75//!
76//! Different enum representations are supported:
77//!
78//! ```rust
79//! use elastic_mapping::Document;
80//! use serde::Serialize;
81//!
82//! // Externally tagged (default)
83//! #[derive(Document)]
84//! enum Status {
85//! Active { since: i64 },
86//! Pending { until: i64 },
87//! }
88//!
89//! // Internally tagged
90//! #[derive(Serialize, Document)]
91//! #[serde(tag = "type")]
92//! enum Message {
93//! Text { content: String },
94//! Image { url: String },
95//! }
96//!
97//! // Adjacently tagged
98//! #[derive(Serialize, Document)]
99//! #[serde(tag = "type", content = "data")]
100//! enum Event {
101//! Created { id: String },
102//! Updated { id: String, changes: String },
103//! }
104//! ```
105
106pub use elastic_mapping_macro::Document;
107pub use serde_json;
108
109const TYPE_INT: &str = "long";
110const TYPE_FLOAT: &str = "float";
111const TYPE_TEXT: &str = "text";
112const TYPE_BOOL: &str = "boolean";
113
114/// A wrapper around the generated Elasticsearch mapping JSON.
115///
116/// This struct contains the complete mapping definition including the `"mappings"`
117/// root object with its `"properties"` field.
118///
119/// # Example
120///
121/// ```rust
122/// use elastic_mapping::{Document, MappingObject};
123///
124/// #[derive(Document)]
125/// struct MyDoc {
126/// field: String,
127/// }
128///
129/// let mapping = MyDoc::document_mapping();
130/// let json = mapping.inner();
131/// println!("{}", serde_json::to_string_pretty(json).unwrap());
132/// ```
133#[derive(Debug)]
134pub struct MappingDocument(serde_json::Value);
135
136impl MappingDocument {
137 /// Returns a reference to the inner JSON value.
138 ///
139 /// The JSON structure follows Elasticsearch's mapping format:
140 /// ```json
141 /// {
142 /// "mappings": {
143 /// "properties": { ... }
144 /// }
145 /// }
146 /// ```
147 pub fn inner(&self) -> &serde_json::Value {
148 &self.0
149 }
150
151 /// Consumes the wrapper and returns the inner JSON value.
152 pub fn into_inner(self) -> serde_json::Value {
153 self.0
154 }
155}
156
157/// Trait for types that can be used as Elasticsearch document mappings.
158///
159/// This trait is automatically implemented by the `#[derive(Document)]` macro.
160/// It provides the `document_mapping()` method to generate the complete mapping definition.
161///
162/// # Example
163///
164/// ```rust
165/// use elastic_mapping::{Document, MappingObject};
166///
167/// #[derive(Document)]
168/// struct Product {
169/// name: String,
170/// price: f64,
171/// }
172///
173/// let mapping = Product::document_mapping();
174/// ```
175pub trait MappingObject: MappingType {
176 /// Generates the complete Elasticsearch mapping definition for this type.
177 ///
178 /// Returns a [`MappingDocument`] containing the mapping JSON with the
179 /// `"mappings"` root object.
180 fn document_mapping() -> MappingDocument {
181 let json = serde_json::json!({
182 "mappings": {
183 "properties": Self::fields()
184 }
185 });
186
187 MappingDocument(json)
188 }
189}
190
191/// Trait for types that can be mapped to Elasticsearch field types.
192///
193/// This trait is implemented for all Rust primitive types and can be implemented
194/// for custom types. It's automatically implemented by the `#[derive(Document)]` macro
195/// for structs.
196///
197/// You generally don't need to implement this trait manually unless you're adding
198/// support for custom types.
199pub trait MappingType {
200 /// Returns the properties map for complex types (objects).
201 ///
202 /// For scalar types (like `String`, `i32`), this returns an empty map.
203 /// For object types, this returns the field definitions.
204 fn fields() -> serde_json::Map<String, serde_json::Value> {
205 serde_json::Map::default()
206 }
207
208 /// Returns the Elasticsearch field type name.
209 ///
210 /// Examples: `"text"`, `"long"`, `"boolean"`, `"object"`, etc.
211 fn field_type() -> &'static str;
212
213 /// Generates the complete mapping definition for this type.
214 ///
215 /// This includes the `"type"` field and optionally a `"properties"` field
216 /// for nested objects.
217 fn mapping() -> serde_json::Map<String, serde_json::Value> {
218 let mut m = serde_json::Map::default();
219 m.insert("type".into(), Self::field_type().into());
220
221 let fields = Self::fields();
222 if !fields.is_empty() {
223 m.insert("properties".into(), fields.into());
224 }
225
226 m
227 }
228}
229
230impl MappingType for String {
231 fn field_type() -> &'static str {
232 TYPE_TEXT
233 }
234}
235
236impl MappingType for bool {
237 fn field_type() -> &'static str {
238 TYPE_BOOL
239 }
240}
241
242impl<T: MappingType> MappingType for Option<T> {
243 fn field_type() -> &'static str {
244 T::field_type()
245 }
246}
247
248impl<T: MappingType> MappingType for Vec<T> {
249 fn fields() -> serde_json::Map<String, serde_json::Value> {
250 T::fields()
251 }
252
253 fn field_type() -> &'static str {
254 T::field_type()
255 }
256}
257
258#[cfg(feature = "chrono")]
259impl MappingType for chrono::DateTime<chrono::Utc> {
260 fn field_type() -> &'static str {
261 "date"
262 }
263}
264
265#[cfg(feature = "url")]
266impl MappingType for url::Url {
267 fn field_type() -> &'static str {
268 TYPE_TEXT
269 }
270}
271
272#[cfg(feature = "uuid")]
273impl MappingType for uuid::Uuid {
274 fn field_type() -> &'static str {
275 TYPE_TEXT
276 }
277}
278
279macro_rules! int_types {
280 ( $($type:ty),* ) => {
281 $(
282 impl MappingType for $type {
283 fn field_type() -> &'static str {
284 TYPE_INT
285 }
286 }
287 )*
288 };
289}
290
291macro_rules! float_types {
292 ( $($type:ty),* ) => {
293 $(
294 impl MappingType for $type {
295 fn field_type() -> &'static str {
296 TYPE_FLOAT
297 }
298 }
299 )*
300 };
301}
302
303int_types!(u8, u16, u32, u64, i8, i16, i32, i64);
304float_types!(f32, f64);