logo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
//! With scripting, you can evaluate custom expressions in Elasticsearch. For example, you can use
//! a script to return a computed value as a field or evaluate a custom score for a query.
//!
//! The default scripting language is [Painless](https://www.elastic.co/guide/en/elasticsearch/reference/current/modules-scripting-painless.html).
//! Additional `lang` plugins are available to run scripts written in other languages. You can
//! specify the language of the script anywhere that scripts run.
//!
//! <https://www.elastic.co/guide/en/elasticsearch/reference/current/modules-scripting.html>

use crate::util::*;
use serde::{Serialize, Serializer};
use std::collections::BTreeMap;

/// Wherever scripting is supported in the Elasticsearch APIs, the syntax follows the same pattern;
/// you specify the language of your script, provide the script logic (or source, and add parameters
/// that are passed into the script.
///
/// <https://www.elastic.co/guide/en/elasticsearch/reference/current/modules-scripting-using.html>
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct Script {
    #[serde(flatten)]
    source: ScriptSource,

    #[serde(skip_serializing_if = "ShouldSkip::should_skip")]
    lang: Option<ScriptLang>,

    #[serde(skip_serializing_if = "ShouldSkip::should_skip")]
    params: BTreeMap<String, serde_json::Value>,
}

/// The script itself, which you specify as `source` for an inline script or
/// `id` for a stored script. Use the
/// [stored script APIs](https://www.elastic.co/guide/en/elasticsearch/reference/current/modules-scripting-using.html#prefer-params)
/// to create and manage stored scripts.
#[derive(Debug, Clone, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ScriptSource {
    /// Inline script
    Source(String),

    /// Stored script
    Id(String),
}

impl Script {
    /// Creates an instance of inlined [`Script`]
    pub fn source<S>(source: S) -> Self
    where
        S: ToString,
    {
        Self {
            source: ScriptSource::Source(source.to_string()),
            lang: None,
            params: BTreeMap::new(),
        }
    }

    /// Creates an instance of stored [`Script`]
    pub fn id<S>(id: S) -> Self
    where
        S: ToString,
    {
        Self {
            source: ScriptSource::Id(id.to_string()),
            lang: None,
            params: BTreeMap::new(),
        }
    }

    /// Specifies the language the script is written in. Defaults to `painless`.
    pub fn lang<S>(mut self, lang: S) -> Self
    where
        S: Into<ScriptLang>,
    {
        self.lang = Some(lang.into());
        self
    }

    /// Specifies any named parameters that are passed into the script as variables. [Use parameters](https://www.elastic.co/guide/en/elasticsearch/reference/current/modules-scripting-using.html#prefer-params)
    /// instead of hard-coded values to decrease compile time.
    pub fn param<T, S>(mut self, name: S, param: T) -> Self
    where
        S: ToString,
        T: Serialize,
    {
        if let Ok(param) = serde_json::to_value(param) {
            let _ = self.params.entry(name.to_string()).or_insert(param);
        }
        self
    }
}
/// Available scripting language
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ScriptLang {
    /// ***Painless*** is a performant, secure scripting language designed specifically for Elasticsearch.
    /// You can use Painless to safely write inline and stored scripts anywhere scripts are supported
    /// in Elasticsearch.
    ///
    /// Painless provides numerous capabilities that center around the following core principles:
    ///
    /// - **Safety**: Ensuring the security of your cluster is of utmost importance. To that end, Painless
    /// uses a fine-grained allowlist with a granularity down to the members of a class. Anything
    /// that is not part of the allowlist results in a compilation error. See the [Painless API
    /// Reference](https://www.elastic.co/guide/en/elasticsearch/painless/7.15/painless-api-reference.html)
    /// for a complete list of available classes, methods, and fields per script context.
    ///
    /// - **Performance**: Painless compiles directly into JVM bytecode to take advantage of all possible
    /// optimizations that the JVM provides. Also, Painless typically avoids features that require
    /// additional slower checks at runtime.
    ///
    /// - **Simplicity**: Painless implements a syntax with a natural familiarity to anyone with some
    /// basic coding experience. Painless uses a subset of Java syntax with some additional
    /// improvements to enhance readability and remove boilerplate.
    ///
    /// <https://www.elastic.co/guide/en/elasticsearch/reference/current/modules-scripting-painless.html>
    Painless,

    /// Lucene’s expressions compile a `javascript` expression to bytecode. They are designed for
    /// high-performance custom ranking and sorting functions and are enabled for `inline` and `stored`
    /// scripting by default.
    ///
    /// <https://www.elastic.co/guide/en/elasticsearch/reference/current/modules-scripting-expression.html>
    Expression,

    /// A template language to search with templates
    ///
    /// <https://www.elastic.co/guide/en/elasticsearch/reference/current/search-template.html>
    Mustache,

    /// Custom language refer to [Advanced scripts using script engines](https://www.elastic.co/guide/en/elasticsearch/reference/current/modules-scripting-engine.html)
    Custom(String),
}

impl Serialize for ScriptLang {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        match self {
            Self::Painless => serializer.serialize_str("painless"),
            Self::Expression => serializer.serialize_str("expression"),
            Self::Mustache => serializer.serialize_str("mustache"),
            Self::Custom(lang) => lang.serialize(serializer),
        }
    }
}

impl<T> From<T> for ScriptLang
where
    T: ToString,
{
    fn from(value: T) -> Self {
        let value = value.to_string();

        match value.as_str() {
            "painless" => Self::Painless,
            "expression" => Self::Expression,
            "mustache" => Self::Mustache,
            _ => Self::Custom(value),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn serialization() {
        assert_serialize(
            Script::source("Math.log(_score * 2) * params['multiplier'].len()")
                .param("multiplier", [1, 2, 3])
                .lang(ScriptLang::Painless),
            json!({
                "source": "Math.log(_score * 2) * params['multiplier'].len()",
                "lang": "painless",
                "params": {
                    "multiplier": [1, 2, 3]
                }
            }),
        );

        assert_serialize(
            Script::source("doc['my_field'].value * params['multiplier']")
                .param("multiplier", 1)
                .lang("my_lang"),
            json!({
                "source": "doc['my_field'].value * params['multiplier']",
                "lang": "my_lang",
                "params": {
                    "multiplier": 1
                }
            }),
        );

        assert_serialize(
            Script::id(123).param("multiplier", [1, 2, 3]),
            json!({
                "id": "123",
                "params": {
                    "multiplier": [1, 2, 3]
                }
            }),
        );
    }
}