diff --git a/build/scripts/Commandline.fsx b/build/scripts/Commandline.fsx index 2f1e13d0ae5..d50f260c673 100644 --- a/build/scripts/Commandline.fsx +++ b/build/scripts/Commandline.fsx @@ -36,7 +36,7 @@ Execution hints can be provided anywhere on the command line - skiptests : skip running tests as part of the target chain - skipdocs : skip generating documentation - seed: : provide a seed to run the tests with. -- random:<:B> : sets random K to bool B if if B is ommitted will default to true +- random:<:B> : sets random K to bool B if if B is omitted will default to true K can be: sourceserializer, typedkeys or oldconnection (only valid on windows) """ diff --git a/src/Nest/QueryDsl/Geo/GeoLocation.cs b/src/Nest/QueryDsl/Geo/GeoLocation.cs index 65af3d1ca72..4047d8b89bd 100644 --- a/src/Nest/QueryDsl/Geo/GeoLocation.cs +++ b/src/Nest/QueryDsl/Geo/GeoLocation.cs @@ -8,7 +8,6 @@ namespace Nest { - /// /// Represents a Latitude/Longitude as a 2 dimensional point that gets serialized as { lat, lon } /// @@ -18,18 +17,16 @@ public class GeoLocation : IEquatable, IFormattable /// Latitude /// [JsonProperty("lat")] - public double Latitude => _latitude; - private readonly double _latitude; + public double Latitude { get; } /// /// Longitude /// [JsonProperty("lon")] - public double Longitude => _longitude; - private readonly double _longitude; + public double Longitude { get; } /// - /// Represents a Latitude/Longitude as a 2 dimensional point. + /// Represents a Latitude/Longitude as a 2 dimensional point. /// /// Value between -90 and 90 /// Value between -180 and 180 @@ -42,8 +39,8 @@ public GeoLocation(double latitude, double longitude) if (!IsValidLongitude(longitude)) throw new ArgumentOutOfRangeException(string.Format(CultureInfo.InvariantCulture, "Invalid longitude '{0}'. Valid values are between -180 and 180", longitude)); - _latitude = latitude; - _longitude = longitude; + Latitude = latitude; + Longitude = longitude; } /// @@ -51,23 +48,17 @@ public GeoLocation(double latitude, double longitude) /// /// /// - public static bool IsValidLatitude(double latitude) - { - return latitude >= -90 && latitude <= 90; - } + public static bool IsValidLatitude(double latitude) => latitude >= -90 && latitude <= 90; /// /// True if is a valid longitude. Otherwise false. /// /// /// - public static bool IsValidLongitude(double longitude) - { - return longitude >= -180 && longitude <= 180; - } + public static bool IsValidLongitude(double longitude) => longitude >= -180 && longitude <= 180; /// - /// Try to create a . + /// Try to create a . /// Return null if either or are invalid. /// /// Value between -90 and 90 @@ -80,10 +71,9 @@ public static GeoLocation TryCreate(double latitude, double longitude) return null; } - public override string ToString() - { - return _latitude.ToString("#0.0#######", CultureInfo.InvariantCulture) + "," + _longitude.ToString("#0.0#######", CultureInfo.InvariantCulture); - } + public override string ToString() => + Latitude.ToString("#0.0#######", CultureInfo.InvariantCulture) + "," + + Longitude.ToString("#0.0#######", CultureInfo.InvariantCulture); public bool Equals(GeoLocation other) { @@ -91,7 +81,7 @@ public bool Equals(GeoLocation other) return false; if (ReferenceEquals(this, other)) return true; - return _latitude.Equals(other._latitude) && _longitude.Equals(other._longitude); + return Latitude.Equals(other.Latitude) && Longitude.Equals(other.Longitude); } public override bool Equals(object obj) @@ -106,7 +96,7 @@ public override bool Equals(object obj) } public override int GetHashCode() => - unchecked((_latitude.GetHashCode() * 397) ^ _longitude.GetHashCode()); + unchecked((Latitude.GetHashCode() * 397) ^ Longitude.GetHashCode()); public string ToString(string format, IFormatProvider formatProvider) => ToString(); @@ -117,21 +107,18 @@ public static implicit operator GeoLocation(string latLon) var parts = latLon.Split(','); if (parts.Length != 2) throw new ArgumentException("Invalid format: string must be in the form of lat,lon"); - double lat; - if (!double.TryParse(parts[0], NumberStyles.Any, CultureInfo.InvariantCulture, out lat)) + if (!double.TryParse(parts[0], NumberStyles.Any, CultureInfo.InvariantCulture, out var lat)) throw new ArgumentException("Invalid latitude value"); - double lon; - if (!double.TryParse(parts[1], NumberStyles.Any, CultureInfo.InvariantCulture, out lon)) + if (!double.TryParse(parts[1], NumberStyles.Any, CultureInfo.InvariantCulture, out var lon)) throw new ArgumentException("Invalid longitude value"); return new GeoLocation(lat, lon); } public static implicit operator GeoLocation(double[] lonLat) { - if (lonLat.Length != 2) - return null; - - return new GeoLocation(lonLat[1], lonLat[0]); + return lonLat.Length != 2 + ? null + : new GeoLocation(lonLat[1], lonLat[0]); } } @@ -141,14 +128,21 @@ public static implicit operator GeoLocation(double[] lonLat) [JsonConverter(typeof(GeoCoordinateJsonConverter))] public class GeoCoordinate : GeoLocation { - public GeoCoordinate(double latitude, double longitude) : base(latitude, longitude) - { - } + /// + /// Creates a new instance of + /// + public GeoCoordinate(double latitude, double longitude) : base(latitude, longitude) { } + /// + /// Creates a new instance of from a pair of coordinates + /// in the order Latitude then Longitude. + /// public static implicit operator GeoCoordinate(double[] coordinates) { if (coordinates == null || coordinates.Length != 2) - throw new ArgumentOutOfRangeException(nameof(coordinates), "Can not create a GeoCoordinate from an array that does not have two doubles"); + throw new ArgumentOutOfRangeException( + nameof(coordinates), + $"Can not create a {nameof(GeoCoordinate)} from an array that does not have two doubles"); return new GeoCoordinate(coordinates[0], coordinates[1]); } diff --git a/src/Nest/QueryDsl/Geo/Shape/Envelope/EnvelopeGeoShape.cs b/src/Nest/QueryDsl/Geo/Shape/Envelope/EnvelopeGeoShape.cs index a326224afac..96f56ee1c6c 100644 --- a/src/Nest/QueryDsl/Geo/Shape/Envelope/EnvelopeGeoShape.cs +++ b/src/Nest/QueryDsl/Geo/Shape/Envelope/EnvelopeGeoShape.cs @@ -13,11 +13,8 @@ public class EnvelopeGeoShape : GeoShapeBase, IEnvelopeGeoShape { public EnvelopeGeoShape() : this(null) { } - public EnvelopeGeoShape(IEnumerable coordinates) - : base("envelope") - { + public EnvelopeGeoShape(IEnumerable coordinates) : base("envelope") => this.Coordinates = coordinates; - } public IEnumerable Coordinates { get; set; } } diff --git a/src/Nest/QueryDsl/Geo/Shape/GeoShapeBase.cs b/src/Nest/QueryDsl/Geo/Shape/GeoShapeBase.cs index 83d6cceb6a1..7e291f28d66 100644 --- a/src/Nest/QueryDsl/Geo/Shape/GeoShapeBase.cs +++ b/src/Nest/QueryDsl/Geo/Shape/GeoShapeBase.cs @@ -1,30 +1,144 @@ -using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; namespace Nest { + [ContractJsonConverterAttribute(typeof(GeoShapeConverter))] public interface IGeoShape { + /// + /// The type of geo shape + /// [JsonProperty("type")] string Type { get; } + /// + /// Will ignore an unmapped field and will not match any documents for this query. + /// This can be useful when querying multiple indexes which might have different mappings. + /// [JsonProperty("ignore_unmapped")] bool? IgnoreUnmapped { get; set; } } public abstract class GeoShapeBase : IGeoShape { - protected GeoShapeBase(string type) - { - this.Type = type; - } + protected GeoShapeBase(string type) => this.Type = type; + /// public string Type { get; protected set; } - /// - /// Will ignore an unmapped field and will not match any documents for this query. - /// This can be useful when querying multiple indexes which might have different mappings. - /// - [JsonProperty("ignore_unmapped")] + /// public bool? IgnoreUnmapped { get; set; } } + + internal class GeoShapeConverter : JsonConverter + { + public override bool CanWrite => false; + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) => + throw new NotSupportedException(); + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.Null) + return null; + + var shape = JObject.Load(reader); + return ReadJToken(shape, serializer); + } + + internal static object ReadJToken(JToken shape, JsonSerializer serializer) + { + var type = shape["type"]; + var typeName = type?.Value(); + switch (typeName) + { + case "circle": + var radius = shape["radius"]; + return ParseCircleGeoShape(shape, serializer, radius); + case "envelope": + return ParseEnvelopeGeoShape(shape, serializer); + case "linestring": + return ParseLineStringGeoShape(shape, serializer); + case "multilinestring": + return ParseMultiLineStringGeoShape(shape, serializer); + case "point": + return ParsePointGeoShape(shape, serializer); + case "multipoint": + return ParseMultiPointGeoShape(shape, serializer); + case "polygon": + return ParsePolygonGeoShape(shape, serializer); + case "multipolygon": + return ParseMultiPolygonGeoShape(shape, serializer); + case "geometrycollection": + return ParseGeometryCollection(shape, serializer); + default: + return null; + } + } + + public override bool CanConvert(Type objectType) => typeof(IGeoShape).IsAssignableFrom(objectType) || + typeof(IGeometryCollection).IsAssignableFrom(objectType); + + private static GeometryCollection ParseGeometryCollection(JToken shape, JsonSerializer serializer) + { + if (!(shape["geometries"] is JArray geometries)) + return new GeometryCollection { Geometries = Enumerable.Empty() }; + + var geoShapes = new List(geometries.Count); + for (var index = 0; index < geometries.Count; index++) + { + var geometry = geometries[index]; + if (ReadJToken(geometry, serializer) is IGeoShape innerShape) + geoShapes.Add(innerShape); + } + + return new GeometryCollection { Geometries = geoShapes }; + } + + private static MultiPolygonGeoShape ParseMultiPolygonGeoShape(JToken shape, JsonSerializer serializer) => + new MultiPolygonGeoShape + { + Coordinates = GetCoordinates>>>(shape, serializer) + }; + + private static PolygonGeoShape ParsePolygonGeoShape(JToken shape, JsonSerializer serializer) => + new PolygonGeoShape {Coordinates = GetCoordinates>>(shape, serializer)}; + + private static MultiPointGeoShape ParseMultiPointGeoShape(JToken shape, JsonSerializer serializer) => + new MultiPointGeoShape {Coordinates = GetCoordinates>(shape, serializer)}; + + private static PointGeoShape ParsePointGeoShape(JToken shape, JsonSerializer serializer) => + new PointGeoShape {Coordinates = GetCoordinates(shape, serializer)}; + + private static MultiLineStringGeoShape ParseMultiLineStringGeoShape(JToken shape, JsonSerializer serializer) => + new MultiLineStringGeoShape + { + Coordinates = GetCoordinates>>(shape, serializer) + }; + + private static LineStringGeoShape ParseLineStringGeoShape(JToken shape, JsonSerializer serializer) => + new LineStringGeoShape {Coordinates = GetCoordinates>(shape, serializer)}; + + private static EnvelopeGeoShape ParseEnvelopeGeoShape(JToken shape, JsonSerializer serializer) => + new EnvelopeGeoShape {Coordinates = GetCoordinates>(shape, serializer)}; + + private static CircleGeoShape ParseCircleGeoShape(JToken shape, JsonSerializer serializer, JToken radius) => + new CircleGeoShape + { + Coordinates = GetCoordinates(shape, serializer), + Radius = radius?.Value() + }; + + private static T GetCoordinates(JToken shape, JsonSerializer serializer) + { + var coordinates = shape["coordinates"]; + return coordinates != null + ? coordinates.ToObject(serializer) + : default(T); + } + } } diff --git a/src/Nest/QueryDsl/Geo/Shape/GeoShapeQueryJsonConverter.cs b/src/Nest/QueryDsl/Geo/Shape/GeoShapeQueryJsonConverter.cs index 0cb40b796ca..2ae6714d889 100644 --- a/src/Nest/QueryDsl/Geo/Shape/GeoShapeQueryJsonConverter.cs +++ b/src/Nest/QueryDsl/Geo/Shape/GeoShapeQueryJsonConverter.cs @@ -11,7 +11,7 @@ namespace Nest /// internal class GeoShapeQueryFieldNameConverter : FieldNameQueryJsonConverter { - private static string[] SkipProperties = {"boost", "_name"}; + private static readonly string[] SkipProperties = {"boost", "_name"}; protected override bool SkipWriteProperty(string propertyName) => SkipProperties.Contains(propertyName); protected override void SerializeJson(JsonWriter writer, object value, IFieldNameQuery castValue, JsonSerializer serializer) @@ -33,20 +33,13 @@ protected override void SerializeJson(JsonWriter writer, object value, IFieldNam writer.WriteEndObject(); } } + internal class GeoShapeQueryJsonConverter : JsonConverter { public override bool CanConvert(Type objectType) => true; public override bool CanRead => true; public override bool CanWrite => false; - public virtual T GetCoordinates(JToken shape, JsonSerializer serializer) - { - var coordinates = shape["coordinates"]; - return coordinates != null - ? coordinates.ToObject(serializer) - : default(T); - } - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { var j = JObject.Load(reader); @@ -87,159 +80,80 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist return query; } - private IGeoShapeQuery ParseIndexedShapeQuery(JToken indexedShape) => + private static IGeoShapeQuery ParseIndexedShapeQuery(JToken indexedShape) => new GeoIndexedShapeQuery {IndexedShape = (indexedShape as JObject)?.ToObject()}; - private IGeoShapeQuery ParseShapeQuery(JToken shape, JsonSerializer serializer) + private static IGeoShapeQuery ParseShapeQuery(JToken shape, JsonSerializer serializer) { var type = shape["type"]; var typeName = type?.Value(); var ignoreUnmapped = shape["ignore_unmapped"]?.Value(); + + var geometry = GeoShapeConverter.ReadJToken(shape, serializer); + switch (typeName) { case "circle": - var radius = shape["radius"]; return new GeoShapeCircleQuery { - Shape = SetIgnoreUnmapped(ParseCircleGeoShape(shape, serializer, radius), ignoreUnmapped) + Shape = SetIgnoreUnmapped(geometry as ICircleGeoShape, ignoreUnmapped) }; case "envelope": return new GeoShapeEnvelopeQuery { - Shape = SetIgnoreUnmapped(ParseEnvelopeGeoShape(shape, serializer), ignoreUnmapped) + Shape = SetIgnoreUnmapped(geometry as IEnvelopeGeoShape, ignoreUnmapped) }; case "linestring": return new GeoShapeLineStringQuery { - Shape = SetIgnoreUnmapped(ParseLineStringGeoShape(shape, serializer), ignoreUnmapped) + Shape = SetIgnoreUnmapped(geometry as ILineStringGeoShape, ignoreUnmapped) }; case "multilinestring": return new GeoShapeMultiLineStringQuery { - Shape = SetIgnoreUnmapped(ParseMultiLineStringGeoShape(shape, serializer), ignoreUnmapped) + Shape = SetIgnoreUnmapped(geometry as IMultiLineStringGeoShape, ignoreUnmapped) }; case "point": return new GeoShapePointQuery { - Shape = SetIgnoreUnmapped(ParsePointGeoShape(shape, serializer), ignoreUnmapped) + Shape = SetIgnoreUnmapped(geometry as IPointGeoShape, ignoreUnmapped) }; case "multipoint": return new GeoShapeMultiPointQuery { - Shape = SetIgnoreUnmapped(ParseMultiPointGeoShape(shape, serializer), ignoreUnmapped) + Shape = SetIgnoreUnmapped(geometry as IMultiPointGeoShape, ignoreUnmapped) }; case "polygon": return new GeoShapePolygonQuery { - Shape = SetIgnoreUnmapped(ParsePolygonGeoShape(shape, serializer), ignoreUnmapped) + Shape = SetIgnoreUnmapped(geometry as IPolygonGeoShape, ignoreUnmapped) }; case "multipolygon": return new GeoShapeMultiPolygonQuery { - Shape = SetIgnoreUnmapped(ParseMultiPolygonGeoShape(shape, serializer), ignoreUnmapped) + Shape = SetIgnoreUnmapped(geometry as IMultiPolygonGeoShape, ignoreUnmapped) }; case "geometrycollection": - return new GeoShapeGeometryCollectionQuery + var geometryCollection = geometry as IGeometryCollection; + if (geometryCollection != null) { - Shape = ParseGeometryCollection(shape, serializer) - }; + foreach (var innerGeometry in geometryCollection.Geometries) + SetIgnoreUnmapped(innerGeometry, ignoreUnmapped); + } + + return new GeoShapeGeometryCollectionQuery { Shape = geometryCollection }; default: return null; } } - private GeometryCollection ParseGeometryCollection(JToken shape, JsonSerializer serializer) - { - if (!(shape["geometries"] is JArray geometries)) - return new GeometryCollection { Geometries = Enumerable.Empty() }; - - var geoShapes = new List(geometries.Count); - - void AddGeoShape(TShape s, bool? ignoreUnmapped) where TShape : IGeoShape - { - s = SetIgnoreUnmapped(s, ignoreUnmapped); - geoShapes.Add(s); - } - - foreach (var geometry in geometries) - { - var ignoreUnmapped = geometry["ignore_unmapped"]?.Value(); - var type = geometry["type"]; - var typeName = type?.Value(); - switch (typeName) - { - case "circle": - var radius = geometry["radius"]; - AddGeoShape(ParseCircleGeoShape(geometry, serializer, radius), ignoreUnmapped); - break; - case "envelope": - AddGeoShape(ParseEnvelopeGeoShape(geometry, serializer), ignoreUnmapped); - break; - case "linestring": - AddGeoShape(ParseLineStringGeoShape(geometry, serializer), ignoreUnmapped); - break; - case "multilinestring": - AddGeoShape(ParseMultiLineStringGeoShape(geometry, serializer), ignoreUnmapped); - break; - case "point": - AddGeoShape(ParsePointGeoShape(geometry, serializer), ignoreUnmapped); - break; - case "multipoint": - AddGeoShape(ParseMultiPointGeoShape(geometry, serializer), ignoreUnmapped); - break; - case "polygon": - AddGeoShape(ParsePolygonGeoShape(geometry, serializer), ignoreUnmapped); - break; - case "multipolygon": - AddGeoShape(ParseMultiPolygonGeoShape(geometry, serializer), ignoreUnmapped); - break; - default: - throw new ArgumentException($"cannot parse geo_shape. unknown type '{typeName}'"); - } - } - - return new GeometryCollection { Geometries = geoShapes }; - } - - private MultiPolygonGeoShape ParseMultiPolygonGeoShape(JToken shape, JsonSerializer serializer) => - new MultiPolygonGeoShape - { - Coordinates = GetCoordinates>>>(shape, serializer) - }; - - private PolygonGeoShape ParsePolygonGeoShape(JToken shape, JsonSerializer serializer) => - new PolygonGeoShape {Coordinates = GetCoordinates>>(shape, serializer)}; - - private MultiPointGeoShape ParseMultiPointGeoShape(JToken shape, JsonSerializer serializer) => - new MultiPointGeoShape {Coordinates = GetCoordinates>(shape, serializer)}; - - private PointGeoShape ParsePointGeoShape(JToken shape, JsonSerializer serializer) => - new PointGeoShape {Coordinates = GetCoordinates(shape, serializer)}; - - private MultiLineStringGeoShape ParseMultiLineStringGeoShape(JToken shape, JsonSerializer serializer) => new MultiLineStringGeoShape - { - Coordinates = GetCoordinates>>(shape, serializer) - }; - - private LineStringGeoShape ParseLineStringGeoShape(JToken shape, JsonSerializer serializer) => - new LineStringGeoShape {Coordinates = GetCoordinates>(shape, serializer)}; - - private EnvelopeGeoShape ParseEnvelopeGeoShape(JToken shape, JsonSerializer serializer) => - new EnvelopeGeoShape {Coordinates = GetCoordinates>(shape, serializer)}; - - private CircleGeoShape ParseCircleGeoShape(JToken shape, JsonSerializer serializer, JToken radius) => new CircleGeoShape - { - Coordinates = GetCoordinates(shape, serializer), - Radius = radius?.Value() - }; - - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) => throw new NotSupportedException(); private static TShape SetIgnoreUnmapped(TShape shape, bool? ignoreUnmapped) where TShape : IGeoShape { - shape.IgnoreUnmapped = ignoreUnmapped; + if (shape != null) + shape.IgnoreUnmapped = ignoreUnmapped; return shape; } } diff --git a/src/Nest/QueryDsl/Geo/Shape/GeometryCollection/GeometryCollection.cs b/src/Nest/QueryDsl/Geo/Shape/GeometryCollection/GeometryCollection.cs index 2ac691d9bac..6fd1540a80e 100644 --- a/src/Nest/QueryDsl/Geo/Shape/GeometryCollection/GeometryCollection.cs +++ b/src/Nest/QueryDsl/Geo/Shape/GeometryCollection/GeometryCollection.cs @@ -3,19 +3,39 @@ namespace Nest { + /// + /// A geo shape representing a collection of geometries + /// + [ContractJsonConverter(typeof(GeoShapeConverter))] public interface IGeometryCollection { + /// + /// The type of geo shape + /// [JsonProperty("type")] string Type { get; } + /// + /// A collection of geometries + /// [JsonProperty("geometries")] IEnumerable Geometries { get; set; } } - public class GeometryCollection : IGeometryCollection + // TODO: IGeometryCollection should implement IGeoShape + /// + public class GeometryCollection : IGeometryCollection, IGeoShape { + /// public string Type => "geometrycollection"; + /// + string IGeoShape.Type => this.Type; + + /// + public bool? IgnoreUnmapped { get; set; } + + /// public IEnumerable Geometries { get; set; } } } diff --git a/src/Serializers/Nest.JsonNetSerializer/Converters/HandleNestTypesOnSourceJsonConverter.cs b/src/Serializers/Nest.JsonNetSerializer/Converters/HandleNestTypesOnSourceJsonConverter.cs index 41e815e4b5d..bc0d402a7ea 100644 --- a/src/Serializers/Nest.JsonNetSerializer/Converters/HandleNestTypesOnSourceJsonConverter.cs +++ b/src/Serializers/Nest.JsonNetSerializer/Converters/HandleNestTypesOnSourceJsonConverter.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; @@ -40,14 +41,19 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist return _builtInSerializer.Deserialize(objectType, ms); } - private static readonly Type[] NestTypesThatCanAppearInSource = { + private static readonly HashSet NestTypesThatCanAppearInSource = new HashSet + { typeof(JoinField), typeof(QueryContainer), typeof(CompletionField), typeof(Attachment), - typeof(ILazyDocument) + typeof(ILazyDocument), + typeof(GeoCoordinate) }; - public override bool CanConvert(Type objectType) => NestTypesThatCanAppearInSource.Contains(objectType); + public override bool CanConvert(Type objectType) => + NestTypesThatCanAppearInSource.Contains(objectType) || + typeof(IGeoShape).IsAssignableFrom(objectType) || + typeof(IGeometryCollection).IsAssignableFrom(objectType); } } diff --git a/src/Tests/Framework/MockData/Developer.cs b/src/Tests/Framework/MockData/Developer.cs index 82da2ce9d52..aa46309ce67 100644 --- a/src/Tests/Framework/MockData/Developer.cs +++ b/src/Tests/Framework/MockData/Developer.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using System.Threading; using Bogus; using Nest; @@ -18,7 +19,7 @@ public class Developer : Person public new static Faker Generator { get; } = new Faker() .UseSeed(TestClient.Configuration.Seed) - .RuleFor(p => p.Id, p => IdState++) + .RuleFor(p => p.Id, p => Interlocked.Increment(ref IdState)) .RuleFor(p => p.FirstName, p => p.Name.FirstName()) .RuleFor(p => p.LastName, p => p.Name.LastName()) .RuleFor(p => p.JobTitle, p => p.Name.JobTitle()) diff --git a/src/Tests/Framework/MockData/Person.cs b/src/Tests/Framework/MockData/Person.cs index 6bbd706aa28..2c2b8cdaac7 100644 --- a/src/Tests/Framework/MockData/Person.cs +++ b/src/Tests/Framework/MockData/Person.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using System.Threading; using Bogus; using Nest; @@ -17,7 +18,7 @@ public class Person public static Faker Generator { get; } = new Faker() - .RuleFor(p => p.Id, p => IdState++) + .RuleFor(p => p.Id, p => Interlocked.Increment(ref IdState)) .RuleFor(p => p.FirstName, p => p.Name.FirstName()) .RuleFor(p => p.LastName, p => p.Name.LastName()) .RuleFor(p => p.JobTitle, p => p.Name.JobTitle()) diff --git a/src/Tests/Framework/MockData/Shape.cs b/src/Tests/Framework/MockData/Shape.cs new file mode 100644 index 00000000000..83a1402795c --- /dev/null +++ b/src/Tests/Framework/MockData/Shape.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using Bogus; +using Nest; + +namespace Tests.Framework.MockData +{ + public class Shape + { + private static int _idState = 0; + + public int Id { get; set; } + + public IGeometryCollection GeometryCollection { get; set; } + public IEnvelopeGeoShape Envelope { get; set; } + public ICircleGeoShape Circle { get; set; } + + public static Faker Generator { get; } = + new Faker() + .UseSeed(TestClient.Configuration.Seed) + .RuleFor(p => p.Id, p => Interlocked.Increment(ref _idState)) + .RuleFor(p => p.GeometryCollection, p => + new GeometryCollection + { + Geometries = new List + { + GenerateRandomPoint(p), + GenerateRandomMultiPoint(p), + GenerateLineString(p), + GenerateMultiLineString(p), + GeneratePolygon(p), + GenerateMultiPolygon(p) + } + }) + .RuleFor(p => p.Envelope, p => new EnvelopeGeoShape(new [] + { + new GeoCoordinate(0, 0), + new GeoCoordinate(45, 45) + })) + .RuleFor(p => p.Circle, p => new CircleGeoShape(GenerateGeoCoordinate(p)) + { + Radius = $"{p.Random.Int(1, 100)}km" + }) + ; + + public static IList Shapes { get; } = Shape.Generator.Clone().Generate(10); + + private static IPointGeoShape GenerateRandomPoint(Faker p) => + new PointGeoShape { Coordinates = GenerateGeoCoordinate(p) }; + + private static IMultiPointGeoShape GenerateRandomMultiPoint(Faker p) => + new MultiPointGeoShape { Coordinates = GenerateGeoCoordinates(p, p.Random.Int(1, 5)) }; + + private static ILineStringGeoShape GenerateLineString(Faker p) => + new LineStringGeoShape { Coordinates = GenerateGeoCoordinates(p, 3) }; + + private static IMultiLineStringGeoShape GenerateMultiLineString(Faker p) + { + var coordinates = new List>(); + for (var i = 0; i < p.Random.Int(1, 5); i++) + coordinates.Add(GenerateGeoCoordinates(p, 3)); + + return new MultiLineStringGeoShape {Coordinates = coordinates }; + } + + private static IPolygonGeoShape GeneratePolygon(Faker p) + { + return new PolygonGeoShape + { + Coordinates = new List> + { + GeneratePolygonCoordinates(p, GenerateGeoCoordinate(p)) + } + }; + } + + private static IMultiPolygonGeoShape GenerateMultiPolygon(Faker p) + { + return new MultiPolygonGeoShape + { + Coordinates = new List>> + { + new [] { GeneratePolygonCoordinates(p, GenerateGeoCoordinate(p)) } + } + }; + } + + private static GeoCoordinate GenerateGeoCoordinate(Faker p) => + new GeoCoordinate(p.Address.Latitude(), p.Address.Longitude()); + + private static IEnumerable GenerateGeoCoordinates(Faker p, int count) + { + var points = new List(); + + for (var i = 0; i < count; i++) + points.Add(GenerateGeoCoordinate(p)); + + return points; + } + + // adapted from https://gis.stackexchange.com/a/103465/30046 + private static IEnumerable GeneratePolygonCoordinates(Faker p, GeoCoordinate centroid, double maxDistance = 0.0002) + { + const int maxPoints = 20; + var points = new List(maxPoints); + double startingAngle = (int)(p.Random.Double() * (1d / 3) * Math.PI); + var angle = startingAngle; + for (var i = 0; i < maxPoints; i++) + { + var distance = p.Random.Double() * maxDistance; + points.Add(new GeoCoordinate(centroid.Latitude + Math.Sin(angle)*distance, centroid.Longitude + Math.Cos(angle)*distance)); + angle = angle + p.Random.Double() * (2d / 3) * Math.PI; + if (angle > 2 * Math.PI) break; + } + + // close the polygon + points.Add(points[0]); + return points; + } + } +} diff --git a/src/Tests/Framework/SerializationTests/GeoShapeSerializationTests.cs b/src/Tests/Framework/SerializationTests/GeoShapeSerializationTests.cs new file mode 100644 index 00000000000..1b2c0f69225 --- /dev/null +++ b/src/Tests/Framework/SerializationTests/GeoShapeSerializationTests.cs @@ -0,0 +1,14 @@ +using Tests.Framework.MockData; + +namespace Tests.Framework +{ + public class GeoShapeSerializationTests : SerializationTestBase + { + [U] + public void CanSerializeShapes() + { + var shape = Shape.Generator.Generate(); + this.AssertSerializesAndRoundTrips(shape); + } + } +} diff --git a/src/Tests/Framework/TestClient.cs b/src/Tests/Framework/TestClient.cs index 9e3beb46bb9..01956a9b1e1 100644 --- a/src/Tests/Framework/TestClient.cs +++ b/src/Tests/Framework/TestClient.cs @@ -96,6 +96,10 @@ private static ConnectionSettings DefaultSettings(ConnectionSettings settings) = .IndexName("server-metrics") .TypeName("metric") ) + .DefaultMappingFor(map => map + .IndexName("shapes") + .TypeName("doc") + ) .ConnectionLimit(ConnectionLimitDefault) //TODO make this random //.EnableHttpCompression() diff --git a/src/Tests/QueryDsl/Geo/GeoShapeQueryUsageTests.cs b/src/Tests/QueryDsl/Geo/GeoShapeQueryUsageTests.cs new file mode 100644 index 00000000000..d87765e66e1 --- /dev/null +++ b/src/Tests/QueryDsl/Geo/GeoShapeQueryUsageTests.cs @@ -0,0 +1,130 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Elasticsearch.Net; +using FluentAssertions; +using Nest; +using Tests.Framework; +using Tests.Framework.Integration; +using Tests.Framework.ManagedElasticsearch.Clusters; + +namespace Tests.QueryDsl.Geo +{ + public class GeoShapeQueryUsageTests : + ApiIntegrationTestBase, + ISearchRequest, + SearchDescriptor, + SearchRequest> + { + public GeoShapeQueryUsageTests(IntrusiveOperationCluster cluster, EndpointUsage usage) + : base(cluster, usage) { } + + private const string Index = "shapes"; + + protected override void IntegrationSetup(IElasticClient client, CallUniqueValues values) + { + if (client.IndexExists(Index).Exists) + return; + + var createIndexResponse = client.CreateIndex(Index, c => c + .Settings(s => s + .NumberOfShards(1) + ) + .Mappings(m => m + .Map(mm => mm + .AutoMap() + .Properties(p => p + .GeoShape(g => g + .Name(n => n.GeometryCollection) + ) + .GeoShape(g => g + .Name(n => n.Envelope) + ) + .GeoShape(g => g + .Name(n => n.Circle) + ) + ) + ) + ) + ); + + if (!createIndexResponse.IsValid) + throw new Exception($"Error creating index for integration test: {createIndexResponse.DebugInformation}"); + + var bulkResponse = this.Client.Bulk(b => b + .IndexMany(Framework.MockData.Shape.Shapes) + .Refresh(Refresh.WaitFor) + ); + + if (!bulkResponse.IsValid) + throw new Exception($"Error indexing shapes for integration test: {bulkResponse.DebugInformation}"); + } + + protected override LazyResponses ClientUsage() => Calls( + fluent: (client, f) => client.Search(f), + fluentAsync: (client, f) => client.SearchAsync(f), + request: (client, r) => client.Search(r), + requestAsync: (client, r) => client.SearchAsync(r) + ); + + private readonly IEnumerable _coordinates = + Framework.MockData.Shape.Shapes.First().Envelope.Coordinates; + + protected override object ExpectJson => new + { + query = new + { + geo_shape = new + { + _name="named_query", + boost = 1.1, + envelope = new + { + relation = "intersects", + shape = new + { + type = "envelope", + ignore_unmapped = true, + coordinates = this._coordinates + } + } + } + } + }; + + protected override int ExpectStatusCode => 200; + protected override bool ExpectIsValid => true; + protected override string UrlPath => $"/shapes/doc/_search"; + protected override HttpMethod HttpMethod => HttpMethod.POST; + + protected override SearchRequest Initializer => new SearchRequest + { + Query = new GeoShapeEnvelopeQuery + { + Name = "named_query", + Boost = 1.1, + Field = Infer.Field(p => p.Envelope), + Shape = new EnvelopeGeoShape(this._coordinates) { IgnoreUnmapped = true}, + Relation = GeoShapeRelation.Intersects, + } + }; + + protected override Func, ISearchRequest> Fluent => s => s + .Query(q => q + .GeoShapeEnvelope(c => c + .Name("named_query") + .Boost(1.1) + .Field(p => p.Envelope) + .Coordinates(this._coordinates, ignoreUnmapped: true) + .Relation(GeoShapeRelation.Intersects) + ) + ); + + protected override void ExpectResponse(ISearchResponse response) + { + response.ShouldBeValid(); + response.Documents.Count.Should().Be(10); + } + } +}