1use std::collections::{HashMap, BTreeMap};
2
3use serde::{Serialize, Deserialize};
4use serde_json::json;
5use struct_metadata::{Described, Descriptor, Kind, MetadataKind};
6use crate::serialize::{deserialize_bool, deserialize_string_or_list};
7
8
9pub fn flatten_fields(target: &Descriptor<ElasticMeta>) -> HashMap<Vec<String>, (&Descriptor<ElasticMeta>, bool, bool)> {
14 match &target.kind {
18 Kind::Struct { children , ..} => {
19 let mut fields = HashMap::<_, _>::new();
20 for child in children {
21 for (mut path, (subfield, multivalued, optional)) in flatten_fields(&child.type_info) {
22 let mut label = vec![child.label.to_owned()];
23 if !path.is_empty() {
24 label.append(&mut path);
25 }
26 fields.insert(label, (subfield, multivalued, optional));
27 }
28 }
29 fields
30 },
31 Kind::Sequence(kind) => {
32 let mut fields = flatten_fields(kind);
33 for row in fields.iter_mut() {
34 row.1.1 = true
35 }
36 fields
37 }
38 Kind::Option(kind) => {
39 let mut fields = flatten_fields(kind);
40 for (_, _, optional) in fields.values_mut() {
41 *optional = true;
42 }
43 fields
44 },
45 Kind::Aliased { kind, .. } => flatten_fields(kind),
46 _ => [(vec![], (target, false, false))].into_iter().collect(),
47 }
48}
49
50#[derive(Default, PartialEq, Eq, Debug, Clone)]
52pub struct ElasticMeta {
53 pub index: Option<bool>,
54 pub store: Option<bool>,
55 pub copyto: Option<&'static str>,
56 pub mapping: Option<&'static str>,
57 pub analyzer: Option<&'static str>,
58 pub normalizer: Option<&'static str>,
59}
60
61impl MetadataKind for ElasticMeta {
62 fn forward_propagate_context(&mut self, context: &Self) {
63 self.index = self.index.or(context.index);
64 self.store = self.store.or(context.store);
65 self.copyto = self.copyto.or(context.copyto);
66 self.mapping = self.mapping.or(context.mapping);
67 self.analyzer = self.analyzer.or(context.analyzer);
68 self.normalizer = self.normalizer.or(context.normalizer);
69 }
70
71 fn forward_propagate_child_defaults(&mut self, kind: &ElasticMeta) {
72 self.index = self.index.or(kind.index);
73 self.store = self.store.or(kind.store);
74 self.copyto = self.copyto.or(kind.copyto);
75 self.mapping = self.mapping.or(kind.mapping);
76 self.analyzer = self.analyzer.or(kind.analyzer);
77 self.normalizer = self.normalizer.or(kind.normalizer);
78 }
79
80 fn forward_propagate_entry_defaults(&mut self, context: &ElasticMeta, kind: &ElasticMeta) {
81 self.index = self.index.or(kind.index).or(context.index);
82 self.store = self.store.or(kind.store).or(context.store);
83 self.copyto = self.copyto.or(kind.copyto).or(context.copyto);
84 self.mapping = self.mapping.or(kind.mapping).or(context.mapping);
85 self.analyzer = self.analyzer.or(kind.analyzer).or(context.analyzer);
86 self.normalizer = self.normalizer.or(kind.normalizer).or(context.normalizer);
87 }
88}
89
90#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Default)]
91#[serde(default)]
92pub struct FieldMapping {
93 #[serde(skip_serializing_if = "Option::is_none")]
94 pub enabled: Option<bool>,
95 #[serde(rename="type", skip_serializing_if = "Option::is_none")]
96 pub type_: Option<String>,
97 #[serde(skip_serializing_if = "Option::is_none")]
98 pub index: Option<bool>,
99 #[serde(skip_serializing_if = "Option::is_none")]
100 pub store: Option<bool>,
101 #[serde(skip_serializing_if = "Option::is_none")]
102 pub ignore_malformed: Option<bool>,
103 #[serde(skip_serializing_if = "Option::is_none")]
104 pub doc_values: Option<bool>,
105 #[serde(skip_serializing_if = "Option::is_none")]
106 pub ignore_above: Option<u32>,
107 #[serde(skip_serializing_if = "Vec::is_empty", deserialize_with="deserialize_string_or_list")]
108 pub copy_to: Vec<String>,
109 #[serde(skip_serializing_if = "Option::is_none")]
110 pub analyzer: Option<String>,
111 #[serde(skip_serializing_if = "Option::is_none")]
112 pub normalizer: Option<String>,
113 #[serde(skip_serializing_if = "Option::is_none")]
114 pub format: Option<String>,
115 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
116 pub properties: BTreeMap<String, FieldMapping>,
117}
118
119
120#[derive(Serialize, Deserialize, Default, PartialEq, Eq, Debug)]
121#[serde(default)]
122pub struct DynamicTemplate {
123 #[serde(rename="match", skip_serializing_if = "Option::is_none")]
124 pub match_: Option<String>,
125 #[serde(skip_serializing_if = "Option::is_none")]
126 pub match_mapping_type: Option<String>,
127 #[serde(skip_serializing_if = "Option::is_none")]
128 pub path_match: Option<String>,
129 pub mapping: FieldMapping,
130}
131
132
133fn dynamic_default() -> bool { true }
134
135#[derive(Serialize, Deserialize, PartialEq, Eq, Debug)]
136#[serde(default)]
137pub struct Mappings {
138 #[serde(default="dynamic_default", deserialize_with="deserialize_bool")]
139 pub dynamic: bool,
140 pub properties: BTreeMap<String, FieldMapping>,
141 pub dynamic_templates: Vec<HashMap<String, DynamicTemplate>>,
142}
143
144impl Default for Mappings {
145 fn default() -> Self {
146 Self { dynamic: dynamic_default(), properties: Default::default(), dynamic_templates: Default::default() }
147 }
148}
149
150impl Mappings {
151 fn insert(&mut self, name: &str, meta: &ElasticMeta, mut field: FieldMapping) {
152 field.index = field.index.or(meta.index);
153 field.store = field.store.or(meta.store);
154
155 if field.type_.clone().is_some_and(|x| x != "text") {
156 field.doc_values = meta.index;
157 }
158
159 if let Some(value) = meta.copyto {
160 field.copy_to.push(value.to_owned());
161 field.copy_to.sort_unstable();
162 field.copy_to.dedup();
163 field.copy_to.retain(|s|!s.is_empty());
164 }
165 field.analyzer = field.analyzer.or(meta.analyzer.map(ToOwned::to_owned));
170 field.normalizer = field.normalizer.or(meta.normalizer.map(ToOwned::to_owned));
171
172 self.properties.insert(name.trim_matches('.').to_owned(), field);
173 }
174
175 fn build_field(&mut self, label: Option<&str>, kind: &Kind<ElasticMeta>, meta: &ElasticMeta, prefix: &[&str], _allow_refuse_implicit: bool) -> Result<(), MappingError> {
176 let mut path = Vec::from(prefix);
178 if let Some(label) = label {
179 path.push(label);
180 }
181 let full_name = path.join(".");
182
183 let simple_mapping = meta.mapping.or(simple_mapping(kind));
185 if let Some(mapping) = simple_mapping {
188 if mapping.eq_ignore_ascii_case("classification") {
189 self.insert(&full_name, meta, FieldMapping{ type_: "keyword".to_owned().into(), ..Default::default() });
190 if !full_name.contains('.') {
191 self.properties.insert("__access_lvl__".to_owned(), FieldMapping{type_: "integer".to_owned().into(), index: true.into(), ..Default::default()});
192 self.properties.insert("__access_req__".to_owned(), FieldMapping{type_: "keyword".to_owned().into(), index: true.into(), ..Default::default()});
193 self.properties.insert("__access_grp1__".to_owned(), FieldMapping{type_: "keyword".to_owned().into(), index: true.into(), ..Default::default()});
194 self.properties.insert("__access_grp2__".to_owned(), FieldMapping{type_: "keyword".to_owned().into(), index: true.into(), ..Default::default()});
195 }
196 } else if mapping.eq_ignore_ascii_case("classification_string") {
197 self.insert(&full_name, meta, FieldMapping{ type_: "keyword".to_owned().into(), ..Default::default() });
198 } else if mapping.eq_ignore_ascii_case("flattenedobject") {
199 if let Kind::Mapping(key_type, child_type) = kind {
200 if key_type.kind != Kind::String {
201 return Err(MappingError::OnlyStringKeys)
202 }
203
204 let index = meta.index.or(child_type.metadata.index).unwrap_or_default();
205 if !index || matches!(child_type.kind, Kind::Any) {
207 self.insert(&full_name, meta, FieldMapping{
208 type_: Some("object".to_owned()),
209 enabled: Some(false),
210 ..Default::default()
211 })
212 } else {
213 self.build_dynamic(&(full_name + ".*"), &child_type.kind, meta, false, index, true)?;
214 }
215 } else {
216 return Err(MappingError::UnsupportedType(full_name, format!("{kind:?}")))
217 }
218 } else if mapping.eq_ignore_ascii_case("date") {
219 self.insert(&full_name, meta, FieldMapping{
220 type_: Some(mapping.to_owned()),
221 format: Some("date_optional_time||epoch_millis".to_owned()),
223 ..Default::default()
224 });
225 } else if mapping.eq_ignore_ascii_case("keyword") {
226 self.insert(&full_name, meta, FieldMapping{
227 type_: Some(mapping.to_owned()),
228 ignore_above: Some(8191),
230 ..Default::default()
231 });
232 } else if mapping.eq_ignore_ascii_case("wildcard") {
233 self.properties.insert(full_name, FieldMapping{
234 type_: Some(mapping.to_owned()),
235 copy_to: meta.copyto.map(|item| vec![item.to_owned()]).unwrap_or_default(),
236 ..Default::default()
237 });
238 } else {
239 self.insert(&full_name, meta, FieldMapping{
240 type_: Some(mapping.to_owned()),
241 ..Default::default()
242 });
243 }
244 return Ok(());
245 };
246 match kind {
248 Kind::Struct { children, .. } => {
249 for child in children {
250 self.build_field(Some(child.label), &child.type_info.kind, &child.metadata, &path, _allow_refuse_implicit)?;
251 }
252 },
253 Kind::Aliased { kind, .. } => {
254 self.build_field(None, &kind.kind, meta, &path, _allow_refuse_implicit)?;
255 },
256 Kind::Enum { .. } => {
257 self.insert(&full_name, meta, FieldMapping{
258 type_: Some("keyword".to_owned()),
259 ignore_above: Some(8191),
261 ..Default::default()
262 });
263 },
264 Kind::Sequence(kind) => {
265 self.build_field(None, &kind.kind, meta, &path, _allow_refuse_implicit)?;
266 },
267 Kind::Option(kind) => {
268 self.build_field(None, &kind.kind, meta, &path, _allow_refuse_implicit)?;
269 },
270 Kind::Mapping(key, value) => {
271 if key.kind != Kind::String {
272 return Err(MappingError::OnlyStringKeys)
273 }
274 let index = meta.index.unwrap_or(false);
286 if !index || value.kind == struct_metadata::Kind::Any {
287 let mut meta = meta.clone();
288 meta.index = None;
289 meta.store = None;
290 self.insert(&full_name, &meta, FieldMapping{
291 type_: "object".to_owned().into(),
292 enabled: false.into(),
293 ..Default::default()
294 });
295 } else {
296 self.build_dynamic(&(full_name + ".*"), &value.kind, &value.metadata, false, index, false)?;
297 }
298 },
299 Kind::Any | Kind::JSON => {
300 let index = meta.index.unwrap_or(false);
301 if index {
302 return Err(MappingError::NoIndexedAny(full_name));
303 }
304
305 self.insert(&full_name, meta, FieldMapping{
306 type_: Some("keyword".to_string()),
307 index: Some(false),
308 ignore_above: Some(8191),
309 doc_values: Some(false),
310 ..Default::default()
311 });
312 },
313 _ => return Err(MappingError::UnsupportedType(full_name, format!("{kind:?}")))
314 };
315 Ok(())
316 }
317
318 fn build_dynamic(&mut self, name: &str, kind: &Kind<ElasticMeta>, meta: &ElasticMeta, nested_template: bool, index: bool, flatten: bool) -> Result<(), MappingError> {
321 match kind {
324 Kind::JSON | Kind::String | Kind::Enum { .. } |
325 Kind::U64 | Kind::I64 | Kind::U32 | Kind::I32 | Kind::U16 | Kind::I16 | Kind::U8 | Kind::I8 |
326 Kind::F64 | Kind::F32 |
327 Kind::Bool => {
328 if nested_template {
329 self.insert_dynamic("nested_".to_owned() + name, DynamicTemplate {
330 match_: Some(name.to_string()),
331 mapping: FieldMapping {
332 type_: "nested".to_string().into(),
333 ..Default::default()
334 },
335 ..Default::default()
336 });
337 } else if let Some(mapping) = meta.mapping.or_else(|| simple_mapping(kind)) {
338 if flatten {
339
340 self.insert_dynamic(format!("{name}_object_tpl"), DynamicTemplate {
341 path_match: Some(name.to_owned()),
342 match_mapping_type: Some("object".to_owned()),
343 mapping: FieldMapping{
344 type_: Some("object".to_owned()),
345 ..Default::default()
346 },
347 ..Default::default()
348 });
349
350 self.insert_dynamic(format!("{name}_wildcard_tpl"), DynamicTemplate {
351 path_match: Some(name.to_owned()),
352 mapping: FieldMapping{
353 type_: mapping.to_owned().into(),
354 index: if meta.index == Some(false) { Some(false) } else { None },
355 store: if meta.store == Some(true) { Some(true) } else { None },
356 copy_to: meta.copyto.map(|item| vec![item.to_owned()]).unwrap_or_default(),
357 ..Default::default()
358 },
359 ..Default::default()
360 })
361
362 } else {
363 self.insert_dynamic(format!("{name}_tpl"), DynamicTemplate {
364 path_match: Some(name.to_owned()),
365 mapping: FieldMapping{
366 type_: mapping.to_owned().into(),
367 index: if meta.index == Some(false) { Some(false) } else { None },
368 store: if meta.store == Some(true) { Some(true) } else { None },
369 copy_to: meta.copyto.map(|item| vec![item.to_owned()]).unwrap_or_default(),
370 ..Default::default()
371 },
372 ..Default::default()
373 })
374 }
375 } else {
376 return Err(MappingError::UnsupportedType(name.to_owned(), format!("{kind:?}")))
377 }
378 return Ok(())
379 },
380
381 Kind::Aliased {name: _, kind: k } => {
383 return self.build_dynamic(name, &k.kind, &k.metadata, nested_template, index, flatten);
384 }
385
386 Kind::Mapping(_, child) => {
387 return self.build_dynamic(name, &child.kind, &child.metadata, true, index, flatten)
388 },
389
390 Kind::Sequence(child) => {
391 return self.build_dynamic(name, &child.kind, &child.metadata, nested_template, index, flatten)
392 },
393
394 Kind::Struct { children, .. } => {
395
396 for child in children {
397 let sub_name = format!("{name}.{}", child.label);
398 let index = child.metadata.index.or(meta.index).unwrap_or(index);
399 self.build_dynamic(&sub_name, &child.type_info.kind, &child.metadata, nested_template, index, flatten)?;
400 }
401
402 return Ok(())
403 }
404
405 Kind::Option(kind) => {
407 return self.build_dynamic(name, &kind.kind, meta, nested_template, index, flatten);
408 }
410
411 _ => {}
412 }
413
414 if matches!(kind, Kind::Any) || !index {
416 if index {
417 return Err(MappingError::NoIndexedAny(name.to_owned()))
418 }
419
420 self.insert_dynamic(format!("{name}_tpl"), DynamicTemplate {
421 path_match: Some(name.to_owned()),
422 mapping: FieldMapping {
423 type_: "keyword".to_owned().into(),
424 index: false.into(),
425 ..Default::default()
426 },
427 ..Default::default()
428 });
429
430 return Ok(())
431 }
432
433 todo!("Unknown type for elasticsearch dynamic mapping: {kind:?}");
434 }
435
436 pub fn insert_dynamic(&mut self, name: String, template: DynamicTemplate) {
437 self.dynamic_templates.push([(name, template)].into_iter().collect());
438 }
439
440 pub fn apply_defaults(&mut self) {
441 self.dynamic_templates.push([("strings_as_keywords".to_owned(), DynamicTemplate {
442 match_: None,
443 path_match: None,
444 match_mapping_type: Some("string".to_owned()),
445 mapping: FieldMapping {
446 type_: "keyword".to_owned().into(),
447 ignore_above: Some(8191),
448 ..Default::default()
449 }
450 })].into_iter().collect());
451
452 if !self.properties.contains_key("id") {
453 self.properties.insert("id".to_owned(), FieldMapping {
454 store: true.into(),
455 doc_values: Some(true),
456 type_: "keyword".to_owned().into(),
457 copy_to: vec!["__text__".to_string()],
458 ..Default::default()
459 });
460 }
461
462 self.properties.insert("__text__".to_owned(), FieldMapping {
463 store: false.into(),
464 type_: "text".to_owned().into(),
465 ..Default::default()
466 });
467
468 }
469
470}
471
472pub fn default_settings(index: serde_json::Value) -> serde_json::Value {
473 json!({
474 "analysis": {
475 "filter": {
476 "text_ws_dsplit": {
477 "type": "pattern_replace",
478 "pattern": r"(\.)",
479 "replacement": " "
480 }
481 },
482 "analyzer": {
483 "string_ci": {
484 "type": "custom",
485 "tokenizer": "keyword",
486 "filter": ["lowercase"]
487 },
488 "text_fuzzy": {
489 "type": "pattern",
490 "pattern": r"\s*:\s*",
491 "lowercase": false
492 },
493 "text_whitespace": {
494 "type": "whitespace"
495 },
496 "text_ws_dsplit": {
497 "type": "custom",
498 "tokenizer": "whitespace",
499 "filters": ["text_ws_dsplit"]
500 }
501 },
502 "normalizer": {
503 "lowercase_normalizer": {
504 "type": "custom",
505 "char_filter": [],
506 "filter": ["lowercase"]
507 }
508 }
509 },
510 "index": index,
511 })
512}
513
514pub fn build_mapping<T: Described<ElasticMeta>>() -> Result<Mappings, MappingError> {
515 let metadata = T::metadata();
516 if let struct_metadata::Kind::Struct { children, .. } = metadata.kind {
517 let children: Vec<_> = children.iter().map(|entry|(Some(entry.label), &entry.type_info.kind, &entry.metadata)).collect();
518 build_mapping_inner(&children, vec![], true)
519 } else {
520 Err(MappingError::OnlyStructs)
521 }
522}
523
524pub fn build_mapping_inner(children: &[(Option<&'static str>, &Kind<ElasticMeta>, &ElasticMeta)], prefix: Vec<&str>, allow_refuse_implicit: bool) -> Result<Mappings, MappingError> {
526 let mut mappings = Mappings::default();
527
528 for (label, kind, meta) in children {
530 mappings.build_field(*label, kind, meta, &prefix, allow_refuse_implicit)?;
531 }
532
533 if mappings.dynamic_templates.is_empty() && allow_refuse_implicit {
537 mappings.insert_dynamic("refuse_all_implicit_mappings".to_owned(), DynamicTemplate {
540 match_: Some("*".to_owned()),
541 mapping: FieldMapping {
542 index: false.into(),
543 ignore_malformed: true.into(),
544 ..Default::default()
545 },
546 ..Default::default()
547 });
548 }
549
550 Ok(mappings)
551}
552
553fn simple_mapping(kind: &Kind<ElasticMeta>) -> Option<&'static str> {
559 match kind {
560 Kind::Struct { .. } => None,
561 Kind::Aliased { kind, .. } => match kind.metadata.mapping {
562 Some(mapping) => Some(mapping),
563 None => simple_mapping(&kind.kind),
564 },
565 Kind::Enum { .. } => Some("keyword"),
566 Kind::Sequence(kind) => match kind.metadata.mapping {
567 Some(mapping) => Some(mapping),
568 None => simple_mapping(&kind.kind),
569 },
570 Kind::Option(kind) => match kind.metadata.mapping {
571 Some(mapping) => Some(mapping),
572 None => simple_mapping(&kind.kind),
573 },
574 Kind::Mapping(..) => None,
575 Kind::DateTime => Some("date"),
576 Kind::String => Some("keyword"),
577 Kind::U128 | Kind::I128 => None,
578 Kind::U64 => Some("unsigned_long"),
579 Kind::I64 | Kind::U32 => Some("long"),
580 Kind::I32 | Kind::U16 | Kind::I16 | Kind::U8 | Kind::I8 => Some("integer"),
581 Kind::F64 => Some("double"),
582 Kind::F32 => Some("float"),
583 Kind::Bool => Some("boolean"),
584 Kind::Any => Some("keyword"),
585 Kind::JSON => None,
586 _ => todo!("{kind:?}"),
587 }
588 }
611#[derive(Debug)]
628pub enum MappingError {
629 OnlyStructs,
630 UnsupportedType(String, String),
631 NoIndexedAny(String),
632 OnlyStringKeys
633}
634
635impl std::fmt::Display for MappingError {
636 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
637 match self {
638 MappingError::OnlyStructs => f.write_str("Mappings can only be created for structs"),
639 MappingError::UnsupportedType(name, kind) => f.write_fmt(format_args!("The field {name} is assigned an unsupported type {kind}")),
640 MappingError::NoIndexedAny(name) => f.write_fmt(format_args!("The field {name} can't be Any type while being indexed.")),
641 MappingError::OnlyStringKeys => f.write_str("Mapping keys must be strings"),
642 }
643 }
644}
645
646impl std::error::Error for MappingError {
647
648}