elastic-mapping 0.1.0

Generate Elasticsearch mapping definitions from Rust structs and enums using derive macros
Documentation
//! Generate Elasticsearch mapping definitions from Rust structs and enums.
//!
//! This crate provides a derive macro to automatically generate Elasticsearch mapping
//! definitions from Rust types. It respects serde attributes and supports various
//! Elasticsearch-specific field configurations.
//!
//! # Features
//!
//! - **Derive macro** - Automatic mapping generation using `#[derive(Document)]`
//! - **Serde compatibility** - Respects `#[serde(rename)]`, `#[serde(rename_all)]`, `#[serde(flatten)]`
//! - **Enum support** - Handles externally tagged, internally tagged, and adjacently tagged enums
//! - **Field annotations** - Configure analyzers, keyword fields, and other Elasticsearch settings
//! - **Optional features** - Support for `chrono`, `url`, and `uuid` types
//!
//! # Quick Start
//!
//! ```rust
//! use elastic_mapping::{Document, MappingObject};
//!
//! #[derive(Document)]
//! struct BlogPost {
//!     title: String,
//!     content: String,
//!     published: bool,
//!     views: i64,
//! }
//!
//! fn main() {
//!     let mapping = BlogPost::document_mapping();
//!     println!("{}", serde_json::to_string_pretty(mapping.inner()).unwrap());
//! }
//! ```
//!
//! # Field Annotations
//!
//! Use the `#[document(...)]` attribute to configure Elasticsearch-specific field settings:
//!
//! ```rust
//! use elastic_mapping::Document;
//!
//! #[derive(Document)]
//! struct Article {
//!     /// Configure an analyzer for text analysis
//!     #[document(analyzer = "english")]
//!     title: String,
//!     
//!     /// Add a keyword subfield that is not indexed
//!     #[document(keyword(index = false))]
//!     internal_id: String,
//!     
//!     /// Add a keyword subfield with ignore_above setting
//!     #[document(keyword(ignore_above = 256))]
//!     category: String,
//! }
//! ```
//!
//! # Serde Compatibility
//!
//! The macro respects common serde attributes:
//!
//! ```rust
//! use elastic_mapping::Document;
//! use serde::Serialize;
//!
//! #[derive(Serialize, Document)]
//! #[serde(rename_all = "camelCase")]
//! struct User {
//!     first_name: String,      // Maps to "firstName"
//!     #[serde(rename = "mail")]
//!     email: String,            // Maps to "mail"
//! }
//! ```
//!
//! # Enum Support
//!
//! Different enum representations are supported:
//!
//! ```rust
//! use elastic_mapping::Document;
//! use serde::Serialize;
//!
//! // Externally tagged (default)
//! #[derive(Document)]
//! enum Status {
//!     Active { since: i64 },
//!     Pending { until: i64 },
//! }
//!
//! // Internally tagged
//! #[derive(Serialize, Document)]
//! #[serde(tag = "type")]
//! enum Message {
//!     Text { content: String },
//!     Image { url: String },
//! }
//!
//! // Adjacently tagged
//! #[derive(Serialize, Document)]
//! #[serde(tag = "type", content = "data")]
//! enum Event {
//!     Created { id: String },
//!     Updated { id: String, changes: String },
//! }
//! ```

pub use elastic_mapping_macro::Document;
pub use serde_json;

const TYPE_INT: &str = "long";
const TYPE_FLOAT: &str = "float";
const TYPE_TEXT: &str = "text";
const TYPE_BOOL: &str = "boolean";

/// A wrapper around the generated Elasticsearch mapping JSON.
///
/// This struct contains the complete mapping definition including the `"mappings"`
/// root object with its `"properties"` field.
///
/// # Example
///
/// ```rust
/// use elastic_mapping::{Document, MappingObject};
///
/// #[derive(Document)]
/// struct MyDoc {
///     field: String,
/// }
///
/// let mapping = MyDoc::document_mapping();
/// let json = mapping.inner();
/// println!("{}", serde_json::to_string_pretty(json).unwrap());
/// ```
#[derive(Debug)]
pub struct MappingDocument(serde_json::Value);

impl MappingDocument {
    /// Returns a reference to the inner JSON value.
    ///
    /// The JSON structure follows Elasticsearch's mapping format:
    /// ```json
    /// {
    ///   "mappings": {
    ///     "properties": { ... }
    ///   }
    /// }
    /// ```
    pub fn inner(&self) -> &serde_json::Value {
        &self.0
    }

    /// Consumes the wrapper and returns the inner JSON value.
    pub fn into_inner(self) -> serde_json::Value {
        self.0
    }
}

/// Trait for types that can be used as Elasticsearch document mappings.
///
/// This trait is automatically implemented by the `#[derive(Document)]` macro.
/// It provides the `document_mapping()` method to generate the complete mapping definition.
///
/// # Example
///
/// ```rust
/// use elastic_mapping::{Document, MappingObject};
///
/// #[derive(Document)]
/// struct Product {
///     name: String,
///     price: f64,
/// }
///
/// let mapping = Product::document_mapping();
/// ```
pub trait MappingObject: MappingType {
    /// Generates the complete Elasticsearch mapping definition for this type.
    ///
    /// Returns a [`MappingDocument`] containing the mapping JSON with the
    /// `"mappings"` root object.
    fn document_mapping() -> MappingDocument {
        let json = serde_json::json!({
            "mappings": {
                "properties": Self::fields()
            }
        });

        MappingDocument(json)
    }
}

/// Trait for types that can be mapped to Elasticsearch field types.
///
/// This trait is implemented for all Rust primitive types and can be implemented
/// for custom types. It's automatically implemented by the `#[derive(Document)]` macro
/// for structs.
///
/// You generally don't need to implement this trait manually unless you're adding
/// support for custom types.
pub trait MappingType {
    /// Returns the properties map for complex types (objects).
    ///
    /// For scalar types (like `String`, `i32`), this returns an empty map.
    /// For object types, this returns the field definitions.
    fn fields() -> serde_json::Map<String, serde_json::Value> {
        serde_json::Map::default()
    }

    /// Returns the Elasticsearch field type name.
    ///
    /// Examples: `"text"`, `"long"`, `"boolean"`, `"object"`, etc.
    fn field_type() -> &'static str;

    /// Generates the complete mapping definition for this type.
    ///
    /// This includes the `"type"` field and optionally a `"properties"` field
    /// for nested objects.
    fn mapping() -> serde_json::Map<String, serde_json::Value> {
        let mut m = serde_json::Map::default();
        m.insert("type".into(), Self::field_type().into());

        let fields = Self::fields();
        if !fields.is_empty() {
            m.insert("properties".into(), fields.into());
        }

        m
    }
}

impl MappingType for String {
    fn field_type() -> &'static str {
        TYPE_TEXT
    }
}

impl MappingType for bool {
    fn field_type() -> &'static str {
        TYPE_BOOL
    }
}

impl<T: MappingType> MappingType for Option<T> {
    fn field_type() -> &'static str {
        T::field_type()
    }
}

impl<T: MappingType> MappingType for Vec<T> {
    fn fields() -> serde_json::Map<String, serde_json::Value> {
        T::fields()
    }

    fn field_type() -> &'static str {
        T::field_type()
    }
}

#[cfg(feature = "chrono")]
impl MappingType for chrono::DateTime<chrono::Utc> {
    fn field_type() -> &'static str {
        "date"
    }
}

#[cfg(feature = "url")]
impl MappingType for url::Url {
    fn field_type() -> &'static str {
        TYPE_TEXT
    }
}

#[cfg(feature = "uuid")]
impl MappingType for uuid::Uuid {
    fn field_type() -> &'static str {
        TYPE_TEXT
    }
}

macro_rules! int_types {
    ( $($type:ty),* ) => {
        $(
			impl MappingType for $type {
				fn field_type() -> &'static str {
					TYPE_INT
				}
			}
		)*
    };
}

macro_rules! float_types {
    ( $($type:ty),* ) => {
        $(
			impl MappingType for $type {
				fn field_type() -> &'static str {
					TYPE_FLOAT
				}
			}
		)*
    };
}

int_types!(u8, u16, u32, u64, i8, i16, i32, i64);
float_types!(f32, f64);