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);