diff --git a/src/Nest.JsonNetSerializer/Converters/HandleNestTypesOnSourceJsonConverter.cs b/src/Nest.JsonNetSerializer/Converters/HandleNestTypesOnSourceJsonConverter.cs index 2560fa55c20..bb70b476f64 100644 --- a/src/Nest.JsonNetSerializer/Converters/HandleNestTypesOnSourceJsonConverter.cs +++ b/src/Nest.JsonNetSerializer/Converters/HandleNestTypesOnSourceJsonConverter.cs @@ -22,7 +22,8 @@ public class HandleNestTypesOnSourceJsonConverter : JsonConverter typeof(ILazyDocument), typeof(LazyDocument), typeof(GeoCoordinate), - typeof(GeoLocation) + typeof(GeoLocation), + typeof(CartesianPoint), }; private readonly IElasticsearchSerializer _builtInSerializer; diff --git a/src/Nest/Mapping/DynamicTemplate/SingleMapping.cs b/src/Nest/Mapping/DynamicTemplate/SingleMapping.cs index 745d57a219a..a6379289a81 100644 --- a/src/Nest/Mapping/DynamicTemplate/SingleMapping.cs +++ b/src/Nest/Mapping/DynamicTemplate/SingleMapping.cs @@ -54,6 +54,10 @@ public IProperty GeoShape(Func, IGeoShapeProperty> public IProperty Shape(Func, IShapeProperty> selector) => selector?.Invoke(new ShapePropertyDescriptor()); + /// + public IProperty Point(Func, IPointProperty> selector) => + selector?.Invoke(new PointPropertyDescriptor()); + /// public IProperty IntegerRange(Func, IIntegerRangeProperty> selector) => selector?.Invoke(new IntegerRangePropertyDescriptor()); diff --git a/src/Nest/Mapping/Types/FieldType.cs b/src/Nest/Mapping/Types/FieldType.cs index 5330839af90..6323698daa0 100644 --- a/src/Nest/Mapping/Types/FieldType.cs +++ b/src/Nest/Mapping/Types/FieldType.cs @@ -5,7 +5,6 @@ using System.Runtime.Serialization; using Elasticsearch.Net; - namespace Nest { /// @@ -152,5 +151,8 @@ public enum FieldType [EnumMember(Value = "wildcard")] Wildcard, + + [EnumMember(Value = "point")] + Point, } } diff --git a/src/Nest/Mapping/Types/Properties.cs b/src/Nest/Mapping/Types/Properties.cs index e2c285f5d2e..43c3cbd4b28 100644 --- a/src/Nest/Mapping/Types/Properties.cs +++ b/src/Nest/Mapping/Types/Properties.cs @@ -95,6 +95,9 @@ TReturnType Nested(Func, INestedProp /// TReturnType Shape(Func, IShapeProperty> selector); + /// + TReturnType Point(Func, IPointProperty> selector); + /// TReturnType Completion(Func, ICompletionProperty> selector); @@ -156,42 +159,64 @@ public PropertiesDescriptor() : base(new Properties()) { } public PropertiesDescriptor(IProperties properties) : base(properties ?? new Properties()) { } + /// public PropertiesDescriptor Binary(Func, IBinaryProperty> selector) => SetProperty(selector); + /// public PropertiesDescriptor Boolean(Func, IBooleanProperty> selector) => SetProperty(selector); + /// public PropertiesDescriptor Completion(Func, ICompletionProperty> selector) => SetProperty(selector); + /// public PropertiesDescriptor Date(Func, IDateProperty> selector) => SetProperty(selector); + /// public PropertiesDescriptor DateNanos(Func, IDateNanosProperty> selector) => SetProperty(selector); + /// public PropertiesDescriptor DateRange(Func, IDateRangeProperty> selector) => SetProperty(selector); + /// public PropertiesDescriptor DoubleRange(Func, IDoubleRangeProperty> selector) => SetProperty(selector); + /// public PropertiesDescriptor FloatRange(Func, IFloatRangeProperty> selector) => SetProperty(selector); + /// public PropertiesDescriptor GeoPoint(Func, IGeoPointProperty> selector) => SetProperty(selector); + /// public PropertiesDescriptor GeoShape(Func, IGeoShapeProperty> selector) => SetProperty(selector); + /// public PropertiesDescriptor Shape(Func, IShapeProperty> selector) => SetProperty(selector); + /// + public PropertiesDescriptor Point(Func, IPointProperty> selector) => SetProperty(selector); + + /// public PropertiesDescriptor IntegerRange(Func, IIntegerRangeProperty> selector) => SetProperty(selector); + /// public PropertiesDescriptor Ip(Func, IIpProperty> selector) => SetProperty(selector); + /// public PropertiesDescriptor IpRange(Func, IIpRangeProperty> selector) => SetProperty(selector); + /// public PropertiesDescriptor Join(Func, IJoinProperty> selector) => SetProperty(selector); + /// public PropertiesDescriptor Keyword(Func, IKeywordProperty> selector) => SetProperty(selector); + /// public PropertiesDescriptor LongRange(Func, ILongRangeProperty> selector) => SetProperty(selector); + /// public PropertiesDescriptor Murmur3Hash(Func, IMurmur3HashProperty> selector) => SetProperty(selector); + /// public PropertiesDescriptor Nested(Func, INestedProperty> selector) where TChild : class => SetProperty(selector); @@ -202,18 +227,23 @@ public PropertiesDescriptor Nested(Func public PropertiesDescriptor Number(Func, INumberProperty> selector) => SetProperty(selector); + /// public PropertiesDescriptor Object(Func, IObjectProperty> selector) where TChild : class => SetProperty(selector); + /// public PropertiesDescriptor Percolator(Func, IPercolatorProperty> selector) => SetProperty(selector); + /// public PropertiesDescriptor Text(Func, ITextProperty> selector) => SetProperty(selector); /// public PropertiesDescriptor SearchAsYouType(Func, ISearchAsYouTypeProperty> selector) => SetProperty(selector); + /// public PropertiesDescriptor TokenCount(Func, ITokenCountProperty> selector) => SetProperty(selector); + /// public PropertiesDescriptor FieldAlias(Func, IFieldAliasProperty> selector) => SetProperty(selector); /// @@ -236,6 +266,9 @@ public PropertiesDescriptor ConstantKeyword(Func Wildcard(Func, IWildcardProperty> selector) => SetProperty(selector); + /// + /// Map a custom property. + /// public PropertiesDescriptor Custom(IProperty customType) => SetProperty(customType); private PropertiesDescriptor SetProperty(Func selector) diff --git a/src/Nest/Mapping/Types/PropertyFormatter.cs b/src/Nest/Mapping/Types/PropertyFormatter.cs index 10f2832d242..c700f7958b6 100644 --- a/src/Nest/Mapping/Types/PropertyFormatter.cs +++ b/src/Nest/Mapping/Types/PropertyFormatter.cs @@ -81,6 +81,7 @@ public IProperty Deserialize(ref JsonReader reader, IJsonFormatterResolver forma case FieldType.GeoPoint: return Deserialize(ref segmentReader, formatterResolver); case FieldType.GeoShape: return Deserialize(ref segmentReader, formatterResolver); case FieldType.Shape: return Deserialize(ref segmentReader, formatterResolver); + case FieldType.Point: return Deserialize(ref segmentReader, formatterResolver); case FieldType.Completion: return Deserialize(ref segmentReader, formatterResolver); case FieldType.TokenCount: return Deserialize(ref segmentReader, formatterResolver); case FieldType.Murmur3Hash: return Deserialize(ref segmentReader, formatterResolver); @@ -159,6 +160,9 @@ public void Serialize(ref JsonWriter writer, IProperty value, IJsonFormatterReso case IShapeProperty shapeProperty: Serialize(ref writer, shapeProperty, formatterResolver); break; + case IPointProperty pointProperty: + Serialize(ref writer, pointProperty, formatterResolver); + break; case ICompletionProperty completionProperty: Serialize(ref writer, completionProperty, formatterResolver); break; diff --git a/src/Nest/Mapping/Types/Specialized/Point/PointAttribute.cs b/src/Nest/Mapping/Types/Specialized/Point/PointAttribute.cs new file mode 100644 index 00000000000..464a9e1c7c2 --- /dev/null +++ b/src/Nest/Mapping/Types/Specialized/Point/PointAttribute.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 + + namespace Nest +{ + /// + public class PointAttribute : ElasticsearchPropertyAttributeBase, IPointProperty + { + public PointAttribute() : base(FieldType.Point) { } + + bool? IPointProperty.IgnoreMalformed { get; set; } + bool? IPointProperty.IgnoreZValue { get; set; } + CartesianPoint IPointProperty.NullValue { get; set; } + + private IPointProperty Self => this; + + /// + public bool IgnoreMalformed + { + get => Self.IgnoreMalformed.GetValueOrDefault(false); + set => Self.IgnoreMalformed = value; + } + + /// + public bool IgnoreZValue + { + get => Self.IgnoreZValue.GetValueOrDefault(true); + set => Self.IgnoreZValue = value; + } + + /// + public CartesianPoint NullValue + { + get => Self.NullValue; + set => Self.NullValue = value; + } + } +} diff --git a/src/Nest/Mapping/Types/Specialized/Point/PointProperty.cs b/src/Nest/Mapping/Types/Specialized/Point/PointProperty.cs new file mode 100644 index 00000000000..77d9442c31a --- /dev/null +++ b/src/Nest/Mapping/Types/Specialized/Point/PointProperty.cs @@ -0,0 +1,87 @@ +// 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 +{ + /// + /// The point datatype facilitates the indexing of and searching + /// arbitrary `x, y` pairs that fall in a 2-dimensional planar + /// coordinate system. + /// + /// You can query documents using this type using . + /// + /// Available in Elasticsearch 7.8.0+ with at least basic license level + /// + [InterfaceDataContract] + public interface IPointProperty : IProperty + { + /// + /// If true, malformed geojson shapes are ignored. If false (default), + /// malformed geojson shapes throw an exception and reject the whole document. + /// + [DataMember(Name ="ignore_malformed")] + bool? IgnoreMalformed { get; set; } + + /// + /// If true (default) three dimension points will be accepted (stored in source) but + /// only x and y values will be indexed; the third dimension is ignored. If false, + /// geo-points containing any more than x and y (two dimensions) values throw + /// an exception and reject the whole document. + /// + [DataMember(Name ="ignore_z_value")] + bool? IgnoreZValue { get; set; } + + /// + /// Accepts an point value which is substituted for any explicit `null` values. + /// Defaults to `null`, which means the field is treated as missing. + /// + [DataMember(Name = "null_value")] + CartesianPoint NullValue { get; set; } + } + + /// + [DebuggerDisplay("{DebugDisplay}")] + public class PointProperty : PropertyBase, IPointProperty + { + public PointProperty() : base(FieldType.Point) { } + + /// + public bool? IgnoreMalformed { get; set; } + + /// + public bool? IgnoreZValue { get; set; } + + /// + public CartesianPoint NullValue { get; set; } + } + + /// + [DebuggerDisplay("{DebugDisplay}")] + public class PointPropertyDescriptor + : PropertyDescriptorBase, IPointProperty, T>, IPointProperty + where T : class + { + public PointPropertyDescriptor() : base(FieldType.Point) { } + + bool? IPointProperty.IgnoreMalformed { get; set; } + bool? IPointProperty.IgnoreZValue { get; set; } + CartesianPoint IPointProperty.NullValue { get; set; } + + /// + public PointPropertyDescriptor IgnoreMalformed(bool? ignoreMalformed = true) => + Assign(ignoreMalformed, (a, v) => a.IgnoreMalformed = v); + + /// + public PointPropertyDescriptor IgnoreZValue(bool? ignoreZValue = true) => + Assign(ignoreZValue, (a, v) => a.IgnoreZValue = v); + + /// + public PointPropertyDescriptor NullValue(CartesianPoint nullValue) => + Assign(nullValue, (a, v) => a.NullValue = v); + } +} diff --git a/src/Nest/Mapping/Visitor/IMappingVisitor.cs b/src/Nest/Mapping/Visitor/IMappingVisitor.cs index 2e4fe6f126c..01fb10b6bf8 100644 --- a/src/Nest/Mapping/Visitor/IMappingVisitor.cs +++ b/src/Nest/Mapping/Visitor/IMappingVisitor.cs @@ -34,6 +34,8 @@ public interface IMappingVisitor void Visit(IShapeProperty property); + void Visit(IPointProperty property); + void Visit(INumberProperty property); void Visit(ICompletionProperty property); @@ -89,6 +91,8 @@ public virtual void Visit(IBooleanProperty property) { } public virtual void Visit(IBinaryProperty property) { } + public virtual void Visit(IPointProperty property) { } + public virtual void Visit(INumberProperty property) { } public virtual void Visit(IObjectProperty property) { } diff --git a/src/Nest/Mapping/Visitor/IPropertyVisitor.cs b/src/Nest/Mapping/Visitor/IPropertyVisitor.cs index 43482148a15..9d8809ad244 100644 --- a/src/Nest/Mapping/Visitor/IPropertyVisitor.cs +++ b/src/Nest/Mapping/Visitor/IPropertyVisitor.cs @@ -32,6 +32,8 @@ public interface IPropertyVisitor void Visit(IShapeProperty type, PropertyInfo propertyInfo, ElasticsearchPropertyAttributeBase attribute); + void Visit(IPointProperty type, PropertyInfo propertyInfo, ElasticsearchPropertyAttributeBase attribute); + void Visit(ICompletionProperty type, PropertyInfo propertyInfo, ElasticsearchPropertyAttributeBase attribute); void Visit(IIpProperty type, PropertyInfo propertyInfo, ElasticsearchPropertyAttributeBase attribute); diff --git a/src/Nest/Mapping/Visitor/MappingWalker.cs b/src/Nest/Mapping/Visitor/MappingWalker.cs index 3b8f9d7442f..04802d77c64 100644 --- a/src/Nest/Mapping/Visitor/MappingWalker.cs +++ b/src/Nest/Mapping/Visitor/MappingWalker.cs @@ -165,6 +165,12 @@ public void Accept(IProperties properties) Accept(t.Fields); }); break; + case FieldType.Point: + Visit(field, t => + { + _visitor.Visit(t); + }); + break; case FieldType.Completion: Visit(field, t => { diff --git a/src/Nest/Mapping/Visitor/NoopPropertyVisitor.cs b/src/Nest/Mapping/Visitor/NoopPropertyVisitor.cs index 0738b069083..32281de535a 100644 --- a/src/Nest/Mapping/Visitor/NoopPropertyVisitor.cs +++ b/src/Nest/Mapping/Visitor/NoopPropertyVisitor.cs @@ -21,6 +21,8 @@ public virtual void Visit(IGeoShapeProperty type, PropertyInfo propertyInfo, Ela public virtual void Visit(IShapeProperty type, PropertyInfo propertyInfo, ElasticsearchPropertyAttributeBase attribute) { } + public virtual void Visit(IPointProperty type, PropertyInfo propertyInfo, ElasticsearchPropertyAttributeBase attribute) { } + public virtual void Visit(ICompletionProperty type, PropertyInfo propertyInfo, ElasticsearchPropertyAttributeBase attribute) { } public virtual void Visit(IMurmur3HashProperty type, PropertyInfo propertyInfo, ElasticsearchPropertyAttributeBase attribute) { } @@ -165,6 +167,9 @@ public void Visit(IProperty type, PropertyInfo propertyInfo, ElasticsearchProper case IConstantKeywordProperty constantKeyword: Visit(constantKeyword, propertyInfo, attribute); break; + case IPointProperty point: + Visit(point, propertyInfo, attribute); + break; case ISearchAsYouTypeProperty searchAsYouType: Visit(searchAsYouType, propertyInfo, attribute); break; diff --git a/src/Nest/QueryDsl/Geo/Shape/GeoShapeBase.cs b/src/Nest/QueryDsl/Geo/Shape/GeoShapeBase.cs index 79ccaea398d..1eb4729e975 100644 --- a/src/Nest/QueryDsl/Geo/Shape/GeoShapeBase.cs +++ b/src/Nest/QueryDsl/Geo/Shape/GeoShapeBase.cs @@ -27,6 +27,7 @@ internal enum GeoFormat WellKnownText } + // TODO: Rename to ShapeType in 8.x internal static class GeoShapeType { // WKT uses BBOX for envelope geo shape diff --git a/src/Nest/QueryDsl/Geo/WKT/GeoWKTException.cs b/src/Nest/QueryDsl/Geo/WKT/GeoWKTException.cs index 3d223b58063..9a78167caf0 100644 --- a/src/Nest/QueryDsl/Geo/WKT/GeoWKTException.cs +++ b/src/Nest/QueryDsl/Geo/WKT/GeoWKTException.cs @@ -6,8 +6,9 @@ namespace Nest { + // TODO: Change to WKTException in 8.x /// - /// An exception when handling in Well-Known Text format + /// An exception when handling shapes in Well-Known Text (WKT) format /// public class GeoWKTException : Exception { diff --git a/src/Nest/QueryDsl/Geo/WKT/GeoWKTReader.cs b/src/Nest/QueryDsl/Geo/WKT/GeoWKTReader.cs index fc8b03a94ab..49c884cdf12 100644 --- a/src/Nest/QueryDsl/Geo/WKT/GeoWKTReader.cs +++ b/src/Nest/QueryDsl/Geo/WKT/GeoWKTReader.cs @@ -279,7 +279,7 @@ internal static double NextNumber(WellKnownTextTokenizer tokenizer) $"Expected number but found: {tokenizer.TokenString()}", tokenizer.LineNumber, tokenizer.Position); } - private static bool IsNumberNext(WellKnownTextTokenizer tokenizer) + internal static bool IsNumberNext(WellKnownTextTokenizer tokenizer) { var token = tokenizer.PeekToken(); return token == TokenType.Word; diff --git a/src/Nest/QueryDsl/Specialized/Shape/CartesianPoint.cs b/src/Nest/QueryDsl/Specialized/Shape/CartesianPoint.cs new file mode 100644 index 00000000000..bc2debdf54d --- /dev/null +++ b/src/Nest/QueryDsl/Specialized/Shape/CartesianPoint.cs @@ -0,0 +1,263 @@ +// 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 System.IO; +using System.Text; +using Elasticsearch.Net.Utf8Json; +using Elasticsearch.Net.Utf8Json.Internal; +using Nest; + +namespace Nest +{ + internal enum ShapeFormat + { + Object, + Array, + WellKnownText, + String, + } + + /// + /// Represents a point in the cartesian plane. + /// + [JsonFormatter(typeof(CartesianPointFormatter))] + public class CartesianPoint : IEquatable + { + internal ShapeFormat Format = ShapeFormat.Object; + + public float X { get; set; } + public float Y { get; set; } + + public CartesianPoint() + { + } + + public CartesianPoint(float x, float y) + { + X = x; + Y = y; + } + + public bool Equals(CartesianPoint other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + + return X.Equals(other.X) && Y.Equals(other.Y); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + + return Equals((CartesianPoint)obj); + } + + public override int GetHashCode() + { + unchecked + { + return (X.GetHashCode() * 397) ^ Y.GetHashCode(); + } + } + + public static CartesianPoint FromCoordinates(string coordinates) + { + var values = coordinates.Split(','); + if (values.Length > 3 || values.Length < 2) + throw new InvalidOperationException( + $"failed to parse {coordinates}, expected 2 or 3 coordinates but found: {values.Length}"); + + var s = values[0].Trim(); + if (!float.TryParse(s, out var x)) + throw new InvalidOperationException($"failed to parse float for x from {s}"); + + s = values[1].Trim(); + if (!float.TryParse(s, out var y)) + throw new InvalidOperationException($"failed to parse float for y from {s}"); + + if (values.Length > 2) + { + s = values[2].Trim(); + if (!float.TryParse(s, out var _)) + throw new InvalidOperationException($"failed to parse float for z from {s}"); + } + + return new CartesianPoint(x, y) { Format = ShapeFormat.String }; + } + + public static CartesianPoint FromWellKnownText(string wkt) + { + using var tokenizer = new WellKnownTextTokenizer(new StringReader(wkt)); + var token = tokenizer.NextToken(); + + if (token != TokenType.Word) + throw new GeoWKTException( + $"Expected word but found {tokenizer.TokenString()}", tokenizer.LineNumber, tokenizer.Position); + + var type = tokenizer.TokenValue.ToUpperInvariant(); + if (type != GeoShapeType.Point) + throw new GeoWKTException( + $"Expected {GeoShapeType.Point} but found {type}", tokenizer.LineNumber, tokenizer.Position); + + if (GeoWKTReader.NextEmptyOrOpen(tokenizer) == TokenType.Word) + return null; + + var x = Convert.ToSingle(GeoWKTReader.NextNumber(tokenizer)); + var y = Convert.ToSingle(GeoWKTReader.NextNumber(tokenizer)); + + // ignore any z value for now + if (GeoWKTReader.IsNumberNext(tokenizer)) + GeoWKTReader.NextNumber(tokenizer); + + var point = new CartesianPoint(x, y) { Format = ShapeFormat.WellKnownText }; + GeoWKTReader.NextCloser(tokenizer); + + return point; + } + + public static implicit operator CartesianPoint(string value) + { + try + { + return value.IndexOf(",", StringComparison.InvariantCultureIgnoreCase) > -1 + ? FromCoordinates(value) + : FromWellKnownText(value); + } + catch + { + // implicit conversions should never fail + return null; + } + } + + public static bool operator ==(CartesianPoint left, CartesianPoint right) => Equals(left, right); + + public static bool operator !=(CartesianPoint left, CartesianPoint right) => !Equals(left, right); + } + + internal class CartesianPointFormatter : IJsonFormatter + { + private static readonly AutomataDictionary Fields = new AutomataDictionary { { "x", 0 }, { "y", 1 }, { "z", 2 } }; + + public void Serialize(ref JsonWriter writer, CartesianPoint value, IJsonFormatterResolver formatterResolver) + { + if (value is null) + { + writer.WriteNull(); + return; + } + + switch (value.Format) + { + case ShapeFormat.Object: + writer.WriteBeginObject(); + writer.WritePropertyName("x"); + writer.WriteSingle(value.X); + writer.WriteValueSeparator(); + writer.WritePropertyName("y"); + writer.WriteSingle(value.Y); + writer.WriteEndObject(); + break; + case ShapeFormat.Array: + writer.WriteBeginArray(); + writer.WriteSingle(value.X); + writer.WriteValueSeparator(); + writer.WriteSingle(value.Y); + writer.WriteEndArray(); + break; + case ShapeFormat.WellKnownText: + writer.WriteQuotation(); + writer.WriteRaw(Encoding.UTF8.GetBytes(GeoShapeType.Point)); + writer.WriteRaw((byte)' '); + writer.WriteRaw((byte)'('); + writer.WriteSingle(value.X); + writer.WriteRaw((byte)' '); + writer.WriteSingle(value.Y); + writer.WriteRaw((byte)')'); + writer.WriteQuotation(); + break; + case ShapeFormat.String: + writer.WriteQuotation(); + writer.WriteSingle(value.X); + writer.WriteValueSeparator(); + writer.WriteSingle(value.Y); + writer.WriteQuotation(); + break; + } + } + + public CartesianPoint Deserialize(ref JsonReader reader, IJsonFormatterResolver formatterResolver) + { + var token = reader.GetCurrentJsonToken(); + switch (token) + { + case JsonToken.BeginObject: + { + var count = 0; + var point = new CartesianPoint { Format = ShapeFormat.Object }; + while (reader.ReadIsInObject(ref count)) + { + var property = reader.ReadPropertyNameSegmentRaw(); + if (Fields.TryGetValue(property, out var value)) + { + switch (value) + { + case 0: + point.X = reader.ReadSingle(); + break; + case 1: + point.Y = reader.ReadSingle(); + break; + case 2: + reader.ReadSingle(); + break; + } + } + else + throw new JsonParsingException($"Unknown property {property.Utf8String()} when parsing {nameof(CartesianPoint)}"); + } + + return point; + } + case JsonToken.BeginArray: + { + var count = 0; + var point = new CartesianPoint { Format = ShapeFormat.Array }; + while (reader.ReadIsInArray(ref count)) + { + switch (count) + { + case 1: + point.X = reader.ReadSingle(); + break; + case 2: + point.Y = reader.ReadSingle(); + break; + case 3: + reader.ReadSingle(); + break; + default: + throw new JsonParsingException($"Expected 2 or 3 coordinates but found {count}"); + } + } + + return point; + } + case JsonToken.String: + { + var value = reader.ReadString(); + return value.IndexOf(",", StringComparison.InvariantCultureIgnoreCase) > -1 + ? CartesianPoint.FromCoordinates(value) + : CartesianPoint.FromWellKnownText(value); + } + default: + throw new JsonParsingException($"Unexpected token type {token} when parsing {nameof(CartesianPoint)}"); + } + } + } +} diff --git a/tests/Tests/Indices/MappingManagement/GetMapping/GetMappingApiTest.cs b/tests/Tests/Indices/MappingManagement/GetMapping/GetMappingApiTest.cs index c6b0861489e..645f0c90719 100644 --- a/tests/Tests/Indices/MappingManagement/GetMapping/GetMappingApiTest.cs +++ b/tests/Tests/Indices/MappingManagement/GetMapping/GetMappingApiTest.cs @@ -203,6 +203,8 @@ internal class TestVisitor : IMappingVisitor public void Visit(IShapeProperty mapping) => Increment("shape"); + public void Visit(IPointProperty mapping) => Increment("point"); + public void Visit(IIpProperty mapping) => Increment("ip"); public void Visit(IObjectProperty mapping) => Increment("object"); diff --git a/tests/Tests/Mapping/Types/Specialized/Point/CartesianPointSerializationTests.cs b/tests/Tests/Mapping/Types/Specialized/Point/CartesianPointSerializationTests.cs new file mode 100644 index 00000000000..a46f3426d0d --- /dev/null +++ b/tests/Tests/Mapping/Types/Specialized/Point/CartesianPointSerializationTests.cs @@ -0,0 +1,68 @@ +// 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.IO; +using System.Text; +using Elastic.Elasticsearch.Xunit.XunitPlumbing; +using FluentAssertions; +using Nest; +using Tests.Core.Client; +using Tests.Core.Extensions; +using Tests.Core.Serialization; + +namespace Tests.Mapping.Types.Specialized.Point +{ + public class CartesianPointSerializationTests + { + [U] + public void CanSerializeCartesianPointFromCoordinates() + { + var point = new PointDocument { Point = CartesianPoint.FromCoordinates("90.0,-90.0") }; + SerializationTester.Default.AssertRoundTrip(point, new { point = "90.0,-90.0" }); + } + + [U] + public void CanSerializeCartesianPointFromWellKnownText() + { + var point = new PointDocument { Point = CartesianPoint.FromWellKnownText("POINT (90.0 -90.0)") }; + SerializationTester.Default.AssertRoundTrip(point, new { point = "POINT (90.0 -90.0)" }); + } + + [U] + public void CanSerializeCartesianPoint() + { + var point = new PointDocument { Point = new CartesianPoint(90, -90) }; + SerializationTester.Default.AssertRoundTrip(point, new { point = new { x = 90f, y = -90f } }); + } + + [U] + public void CanDeserializeCartesianPointFromArray() + { + var point = new CartesianPoint(90, -90); + + CartesianPoint deserialized = null; + using (var stream = new MemoryStream(Encoding.UTF8.GetBytes("[90, -90]"))) + deserialized = TestClient.DefaultInMemoryClient.SourceSerializer.Deserialize(stream); + + deserialized.Should().Be(point); + } + + [U] + public void CanDeserializeCartesianPointFromObject() + { + var point = new CartesianPoint(90, -90); + + CartesianPoint deserialized = null; + using (var stream = new MemoryStream(Encoding.UTF8.GetBytes("{ \"x\": 90, \"y\": -90 }"))) + deserialized = TestClient.DefaultInMemoryClient.SourceSerializer.Deserialize(stream); + + deserialized.Should().Be(point); + } + + private class PointDocument + { + public CartesianPoint Point { get; set; } + } + } +} diff --git a/tests/Tests/Mapping/Types/Specialized/Point/PointAttributeTests.cs b/tests/Tests/Mapping/Types/Specialized/Point/PointAttributeTests.cs new file mode 100644 index 00000000000..2155bcfc839 --- /dev/null +++ b/tests/Tests/Mapping/Types/Specialized/Point/PointAttributeTests.cs @@ -0,0 +1,40 @@ +// 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.Point +{ + public class PointTest + { + [Point(IgnoreMalformed = true, IgnoreZValue = true)] + public object Full { get; set; } + + [Point] + public object Minimal { get; set; } + } + + [SkipVersion("<7.8.0", "Points introduced in 7.8.0+")] + public class PointAttributeTests : AttributeTestsBase + { + protected override object ExpectJson => new + { + properties = new + { + full = new + { + type = "point", + ignore_z_value = true, + ignore_malformed = true + + }, + minimal = new + { + type = "point" + } + } + }; + } +} diff --git a/tests/Tests/Mapping/Types/Specialized/Point/PointPropertyTests.cs b/tests/Tests/Mapping/Types/Specialized/Point/PointPropertyTests.cs new file mode 100644 index 00000000000..8f54a25a520 --- /dev/null +++ b/tests/Tests/Mapping/Types/Specialized/Point/PointPropertyTests.cs @@ -0,0 +1,48 @@ +// 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.Point +{ + [SkipVersion("<7.8.0", "Points introduced in 7.8.0+")] + public class PointPropertyTests : PropertyTestsBase + { + public PointPropertyTests(WritableCluster cluster, EndpointUsage usage) : base(cluster, usage) { } + + protected override object ExpectJson => new + { + properties = new + { + arbitraryPoint = new + { + type = "point", + ignore_z_value = true + } + } + }; + + protected override Func, IPromise> FluentProperties => f => f + .Point(p => p + .Name("arbitraryPoint") + .IgnoreZValue() + ); + + + protected override IProperties InitializerProperties => new Properties + { + { + "arbitraryPoint", new PointProperty + { + IgnoreZValue = true + } + } + }; + } +}