From bfaf4ba770414c44bb0a4ae186a6c703a0dc3774 Mon Sep 17 00:00:00 2001
From: Russ Cam <russ.cam@elastic.co>
Date: Fri, 31 Jul 2020 11:11:10 +1000
Subject: [PATCH] Add support for wildcard property (#4890)

Relates: elastic/elasticsearch#49993
---
 .../Mapping/DynamicTemplate/SingleMapping.cs  |  4 ++
 src/Nest/Mapping/Types/FieldType.cs           |  5 +-
 src/Nest/Mapping/Types/Properties.cs          |  7 +++
 src/Nest/Mapping/Types/PropertyFormatter.cs   |  4 ++
 .../Specialized/Wildcard/WildcardAttribute.cs | 26 ++++++++
 .../Specialized/Wildcard/WildcardProperty.cs  | 62 +++++++++++++++++++
 .../Wildcard/WildcardAttributeTests.cs        | 39 ++++++++++++
 .../Wildcard/WildcardPropertyTests.cs         | 42 +++++++++++++
 8 files changed, 188 insertions(+), 1 deletion(-)
 create mode 100644 src/Nest/Mapping/Types/Specialized/Wildcard/WildcardAttribute.cs
 create mode 100644 src/Nest/Mapping/Types/Specialized/Wildcard/WildcardProperty.cs
 create mode 100644 tests/Tests/Mapping/Types/Specialized/Wildcard/WildcardAttributeTests.cs
 create mode 100644 tests/Tests/Mapping/Types/Specialized/Wildcard/WildcardPropertyTests.cs

diff --git a/src/Nest/Mapping/DynamicTemplate/SingleMapping.cs b/src/Nest/Mapping/DynamicTemplate/SingleMapping.cs
index bca5839f97c..f8d08f12716 100644
--- a/src/Nest/Mapping/DynamicTemplate/SingleMapping.cs
+++ b/src/Nest/Mapping/DynamicTemplate/SingleMapping.cs
@@ -141,6 +141,10 @@ public IProperty SearchAsYouType(Func<SearchAsYouTypePropertyDescriptor<T>, ISea
 		public IProperty ConstantKeyword(Func<ConstantKeywordPropertyDescriptor<T>, IConstantKeywordProperty> selector) =>
 			selector?.Invoke(new ConstantKeywordPropertyDescriptor<T>());
 
+		/// <inheritdoc />
+		public IProperty Wildcard(Func<WildcardPropertyDescriptor<T>, IWildcardProperty> selector) =>
+			selector?.Invoke(new WildcardPropertyDescriptor<T>());
+
 #pragma warning disable CS3001 // Argument type is not CLS-compliant
 		public IProperty Scalar(Expression<Func<T, int>> field, Func<NumberPropertyDescriptor<T>, INumberProperty> selector = null) =>
 			selector.InvokeOrDefault(new NumberPropertyDescriptor<T>().Name(field).Type(NumberType.Integer));
diff --git a/src/Nest/Mapping/Types/FieldType.cs b/src/Nest/Mapping/Types/FieldType.cs
index acca3bddb36..25c34f10b23 100644
--- a/src/Nest/Mapping/Types/FieldType.cs
+++ b/src/Nest/Mapping/Types/FieldType.cs
@@ -148,6 +148,9 @@ public enum FieldType
 		Histogram,
 
 		[EnumMember(Value = "constant_keyword")]
-		ConstantKeyword
+		ConstantKeyword,
+
+		[EnumMember(Value = "wildcard")]
+		Wildcard,
 	}
 }
diff --git a/src/Nest/Mapping/Types/Properties.cs b/src/Nest/Mapping/Types/Properties.cs
index b942c3cac41..a56cb48978a 100644
--- a/src/Nest/Mapping/Types/Properties.cs
+++ b/src/Nest/Mapping/Types/Properties.cs
@@ -145,6 +145,9 @@ TReturnType Nested<TChild>(Func<NestedPropertyDescriptor<T, TChild>, INestedProp
 
 		/// <inheritdoc cref="IConstantKeywordProperty"/>
 		TReturnType ConstantKeyword(Func<ConstantKeywordPropertyDescriptor<T>, IConstantKeywordProperty> selector);
+
+		/// <inheritdoc cref="IWildcardProperty"/>
+		TReturnType Wildcard(Func<WildcardPropertyDescriptor<T>, IWildcardProperty> selector);
 	}
 
 	public partial class PropertiesDescriptor<T> where T : class
@@ -229,6 +232,10 @@ public PropertiesDescriptor<T> Object<TChild>(Func<ObjectTypeDescriptor<T, TChil
 		public PropertiesDescriptor<T> ConstantKeyword(Func<ConstantKeywordPropertyDescriptor<T>, IConstantKeywordProperty> selector) =>
 			SetProperty(selector);
 
+		/// <inheritdoc cref="IWildcardProperty"/>
+		public PropertiesDescriptor<T> Wildcard(Func<WildcardPropertyDescriptor<T>, IWildcardProperty> selector) =>
+			SetProperty(selector);
+
 		public PropertiesDescriptor<T> Custom(IProperty customType) => SetProperty(customType);
 
 		private PropertiesDescriptor<T> SetProperty<TDescriptor, TInterface>(Func<TDescriptor, TInterface> selector)
diff --git a/src/Nest/Mapping/Types/PropertyFormatter.cs b/src/Nest/Mapping/Types/PropertyFormatter.cs
index b24eb2052c8..fcf91a0c105 100644
--- a/src/Nest/Mapping/Types/PropertyFormatter.cs
+++ b/src/Nest/Mapping/Types/PropertyFormatter.cs
@@ -98,6 +98,7 @@ public IProperty Deserialize(ref JsonReader reader, IJsonFormatterResolver forma
 				case FieldType.Flattened: return Deserialize<FlattenedProperty>(ref segmentReader, formatterResolver);
 				case FieldType.Histogram: return Deserialize<HistogramProperty>(ref segmentReader, formatterResolver);
 				case FieldType.ConstantKeyword: return Deserialize<ConstantKeywordProperty>(ref segmentReader, formatterResolver);
+				case FieldType.Wildcard: return Deserialize<WildcardProperty>(ref segmentReader, formatterResolver);
 				case FieldType.None:
 					// no "type" field in the property mapping, or FieldType enum could not be parsed from typeString
 					return Deserialize<ObjectProperty>(ref segmentReader, formatterResolver);
@@ -209,6 +210,9 @@ public void Serialize(ref JsonWriter writer, IProperty value, IJsonFormatterReso
 				case IConstantKeywordProperty constantKeywordProperty:
 					Serialize(ref writer, constantKeywordProperty, formatterResolver);
 					break;
+				case IWildcardProperty wildcardProperty:
+					Serialize(ref writer, wildcardProperty, formatterResolver);
+					break;
 				case IGenericProperty genericProperty:
 					Serialize(ref writer, genericProperty, formatterResolver);
 					break;
diff --git a/src/Nest/Mapping/Types/Specialized/Wildcard/WildcardAttribute.cs b/src/Nest/Mapping/Types/Specialized/Wildcard/WildcardAttribute.cs
new file mode 100644
index 00000000000..8080b356b18
--- /dev/null
+++ b/src/Nest/Mapping/Types/Specialized/Wildcard/WildcardAttribute.cs
@@ -0,0 +1,26 @@
+// Licensed to Elasticsearch B.V under one or more agreements.
+// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information
+
+namespace Nest
+{
+	/// <inheritdoc cref="IWildcardProperty"/>
+	public class WildcardAttribute : ElasticsearchPropertyAttributeBase, IWildcardProperty
+	{
+		public WildcardAttribute() : base(FieldType.Wildcard) { }
+
+		int? IWildcardProperty.IgnoreAbove { get; set; }
+
+		private IWildcardProperty Self => this;
+
+		/// <inheritdoc cref="IWildcardProperty.IgnoreAbove" />
+		public int IgnoreAbove
+		{
+			get => Self.IgnoreAbove.GetValueOrDefault(2147483647);
+			set => Self.IgnoreAbove = value;
+		}
+
+		/// <inheritdoc cref="IWildcardProperty.NullValue" />
+		public string NullValue { get; set; }
+	}
+}
diff --git a/src/Nest/Mapping/Types/Specialized/Wildcard/WildcardProperty.cs b/src/Nest/Mapping/Types/Specialized/Wildcard/WildcardProperty.cs
new file mode 100644
index 00000000000..5d2d66a1a40
--- /dev/null
+++ b/src/Nest/Mapping/Types/Specialized/Wildcard/WildcardProperty.cs
@@ -0,0 +1,62 @@
+// Licensed to Elasticsearch B.V under one or more agreements.
+// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information
+
+using System.Diagnostics;
+using System.Runtime.Serialization;
+using Elasticsearch.Net.Utf8Json;
+
+namespace Nest
+{
+	/// <summary>
+	/// A wildcard field stores values optimised for wildcard grep-like queries.
+	/// <para />
+	/// Available in Elasticsearch 7.9.0+ with at least a basic license level
+	/// </summary>
+	[InterfaceDataContract]
+	public interface IWildcardProperty : IProperty
+	{
+		/// <summary>
+		/// Do not index any string longer than this value. Defaults to 2147483647 so that all values would be accepted.
+		/// </summary>
+		[DataMember(Name = "ignore_above")]
+		int? IgnoreAbove { get; set; }
+
+		/// <summary>
+		/// Accepts a string value which is substituted for any explicit null values. Defaults to null, which means the field is treated as missing.
+		/// </summary>
+		[DataMember(Name ="null_value")]
+		string NullValue { get; set; }
+	}
+
+	/// <inheritdoc cref="IWildcardProperty" />
+	[DebuggerDisplay("{DebugDisplay}")]
+	public class WildcardProperty : PropertyBase, IWildcardProperty
+	{
+		public WildcardProperty() : base(FieldType.Wildcard) { }
+
+		/// <inheritdoc cref="IWildcardProperty.IgnoreAbove" />
+		public int? IgnoreAbove { get; set; }
+
+		/// <inheritdoc cref="IWildcardProperty.NullValue" />
+		public string NullValue { get; set; }
+	}
+
+	/// <inheritdoc cref="IWildcardProperty" />
+	[DebuggerDisplay("{DebugDisplay}")]
+	public class WildcardPropertyDescriptor<T>
+		: PropertyDescriptorBase<WildcardPropertyDescriptor<T>, IWildcardProperty, T>, IWildcardProperty
+		where T : class
+	{
+		public WildcardPropertyDescriptor() : base(FieldType.Wildcard) { }
+
+		int? IWildcardProperty.IgnoreAbove { get; set; }
+		string IWildcardProperty.NullValue { get; set; }
+
+		/// <inheritdoc cref="IWildcardProperty.IgnoreAbove" />
+		public WildcardPropertyDescriptor<T> IgnoreAbove(int? ignoreAbove) => Assign(ignoreAbove, (a, v) => a.IgnoreAbove = v);
+
+		/// <inheritdoc cref="IWildcardProperty.NullValue" />
+		public WildcardPropertyDescriptor<T> NullValue(string nullValue) => Assign(nullValue, (a, v) => a.NullValue = v);
+	}
+}
diff --git a/tests/Tests/Mapping/Types/Specialized/Wildcard/WildcardAttributeTests.cs b/tests/Tests/Mapping/Types/Specialized/Wildcard/WildcardAttributeTests.cs
new file mode 100644
index 00000000000..865834b84a5
--- /dev/null
+++ b/tests/Tests/Mapping/Types/Specialized/Wildcard/WildcardAttributeTests.cs
@@ -0,0 +1,39 @@
+// Licensed to Elasticsearch B.V under one or more agreements.
+// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information
+
+using Elastic.Elasticsearch.Xunit.XunitPlumbing;
+using Nest;
+
+namespace Tests.Mapping.Types.Specialized.Wildcard
+{
+	public class WildcardTest
+	{
+		[Wildcard(IgnoreAbove = 512, NullValue = "foo")]
+		public string Full { get; set; }
+
+		[Wildcard]
+		public string Simple { get; set; }
+	}
+
+	[SkipVersion("<7.9.0", "introduced in 7.9.0")]
+	public class WildcardAttributeTests : AttributeTestsBase<WildcardTest>
+	{
+		protected override object ExpectJson => new
+		{
+			properties = new
+			{
+				full = new
+				{
+					type = "wildcard",
+					ignore_above = 512,
+					null_value = "foo"
+				},
+				simple = new
+				{
+					type = "wildcard"
+				}
+			}
+		};
+	}
+}
diff --git a/tests/Tests/Mapping/Types/Specialized/Wildcard/WildcardPropertyTests.cs b/tests/Tests/Mapping/Types/Specialized/Wildcard/WildcardPropertyTests.cs
new file mode 100644
index 00000000000..11e7fe25a5f
--- /dev/null
+++ b/tests/Tests/Mapping/Types/Specialized/Wildcard/WildcardPropertyTests.cs
@@ -0,0 +1,42 @@
+// Licensed to Elasticsearch B.V under one or more agreements.
+// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information
+
+using System;
+using Elastic.Elasticsearch.Xunit.XunitPlumbing;
+using Nest;
+using Tests.Core.ManagedElasticsearch.Clusters;
+using Tests.Domain;
+using Tests.Framework.EndpointTests.TestState;
+
+namespace Tests.Mapping.Types.Specialized.Wildcard
+{
+	[SkipVersion("<7.9.0", "introduced in 7.9.0")]
+	public class WildcardPropertyTests : PropertyTestsBase
+	{
+		public WildcardPropertyTests(WritableCluster cluster, EndpointUsage usage) : base(cluster, usage) { }
+
+		protected override object ExpectJson => new
+		{
+			properties = new
+			{
+				description = new
+				{
+					type = "wildcard"
+				}
+			}
+		};
+
+		protected override Func<PropertiesDescriptor<Project>, IPromise<IProperties>> FluentProperties => f => f
+			.Wildcard(s => s
+				.Name(n => n.Description)
+			);
+
+		protected override IProperties InitializerProperties => new Properties
+		{
+			{
+				"description", new WildcardProperty()
+			}
+		};
+	}
+}