From 7005d28e39ca5175cefd7ce1dd1befe7ede15e85 Mon Sep 17 00:00:00 2001 From: Phil Oyston Date: Tue, 8 Oct 2019 16:18:47 +0100 Subject: [PATCH] :semver:minor - return to multi-targetting (#33) --- .../Abstractions/IPaginationOptions.cs | 10 + src/Nest.Searchify/Nest.Searchify.csproj | 13 +- src/Nest.Searchify/Queries/ParametersQuery.cs | 15 +- .../Queries/QueryStringParser.cs | 398 ++++++++++++++++++ .../SearchResults/PaginationOptions.cs | 16 +- .../Nest.Searchify.Tests.csproj | 15 +- .../QueryStringParserContext.cs | 7 +- 7 files changed, 467 insertions(+), 7 deletions(-) diff --git a/src/Nest.Searchify/Abstractions/IPaginationOptions.cs b/src/Nest.Searchify/Abstractions/IPaginationOptions.cs index 89b0442..dd48161 100644 --- a/src/Nest.Searchify/Abstractions/IPaginationOptions.cs +++ b/src/Nest.Searchify/Abstractions/IPaginationOptions.cs @@ -23,11 +23,21 @@ public interface IPaginationOptions where TParameters : IPaging TParameters ForPage(int page); TParameters LastPage(); +#if NETSTANDARD /// /// Generates a group of pages around the current page, will always include /// /// the range of pages to generate, by default 5 pages either side of the current page will be generated (or the available pages if that is less than the /// Group of page numbers along with a dictionary containing the required querystring for the page IEnumerable>> PagingGroup(int range = 5); +#else + + /// + /// Generates a group of pages around the current page, will always include + /// + /// the range of pages to generate, by default 5 pages either side of the current page will be generated (or the available pages if that is less than the + /// Group of page numbers along with a dictionary containing the required querystring for the page + IEnumerable> PagingGroup(int range = 5); +#endif } } \ No newline at end of file diff --git a/src/Nest.Searchify/Nest.Searchify.csproj b/src/Nest.Searchify/Nest.Searchify.csproj index 5e12bf5..066f864 100644 --- a/src/Nest.Searchify/Nest.Searchify.csproj +++ b/src/Nest.Searchify/Nest.Searchify.csproj @@ -1,7 +1,7 @@  - netstandard2.0 + net461;netstandard2.0 Nest.Searchify Nest.Searchify false @@ -43,9 +43,18 @@ + + + + + + + + + + - diff --git a/src/Nest.Searchify/Queries/ParametersQuery.cs b/src/Nest.Searchify/Queries/ParametersQuery.cs index 2780db2..f8c9ca4 100644 --- a/src/Nest.Searchify/Queries/ParametersQuery.cs +++ b/src/Nest.Searchify/Queries/ParametersQuery.cs @@ -19,14 +19,25 @@ public class ParametersQuery { - public ParametersQuery() : this(Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery("")) +#if !NETSTANDARD + public ParametersQuery() : this(new System.Collections.Specialized.NameValueCollection()) { } - public ParametersQuery(Dictionary parameters) : base(QueryStringParser.Parse(parameters)) + public ParametersQuery(System.Collections.Specialized.NameValueCollection parameters) : base(QueryStringParser.Parse(parameters)) + { + } +#else + public ParametersQuery() : this(Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery("")) { } + + public ParametersQuery(Dictionary parameters) : base(QueryStringParser.Parse(parameters)) + { + } +#endif + public ParametersQuery(TParameters parameters) : base(parameters) { } diff --git a/src/Nest.Searchify/Queries/QueryStringParser.cs b/src/Nest.Searchify/Queries/QueryStringParser.cs index 25f2f97..eb131fd 100644 --- a/src/Nest.Searchify/Queries/QueryStringParser.cs +++ b/src/Nest.Searchify/Queries/QueryStringParser.cs @@ -2,6 +2,8 @@ namespace Nest.Searchify.Queries { +#if NETSTANDARD + using System.Text.Encodings.Web; using System; using System.Collections.Generic; @@ -405,3 +407,399 @@ public static string ToQueryString(TParameters parameters) } } } +#endif + +#if !NETSTANDARD + using System; + using System.Collections.Generic; + using System.Collections.Specialized; + using System.ComponentModel; + using System.Linq; + using System.Reflection; + using System.Web; + using Abstractions; + using Extensions; + using Newtonsoft.Json; + + public static class QueryStringParser where TParameters : class, new() + { + public static class TypeParsers + { + + #region Helpers + + public static NameValueCollection Sort(NameValueCollection nvc) + { + var sortedNvc = HttpUtility.ParseQueryString(""); + foreach (var key in nvc.AllKeys.OrderBy(s => s)) + { + var values = nvc.GetValues(key); + if (values != null) + { + foreach (var value in values.OrderBy(v => v)) + { + sortedNvc.Add(key, value); + } + } + } + return sortedNvc; + } + + public static void ParseFromStringArray(NameValueCollection nvc, object values, string propertyName) + { + var stringValues = values as IEnumerable; + if (stringValues != null) + { + foreach (var value in stringValues) + { + var strValue = value.ToString(); + if (!string.IsNullOrWhiteSpace(strValue)) + { + nvc.Add(propertyName, value.ToString()); + } + } + } + } + + public static void ParseBoolFromString(NameValueCollection nvc, object value, string propertyName) + { + if (value != null) + { + var boolValue = value.ToString().ToBool(); + if (boolValue) + { + nvc.Add(propertyName, bool.TrueString.ToLowerInvariant()); + } + } + } + + public static void ParseNullableBoolFromString(NameValueCollection nvc, object value, string propertyName) + { + var boolValue = value?.ToString().ToNullableBool(); + if (boolValue != null) + { + nvc.Add(propertyName, boolValue.Value.ToString().ToLowerInvariant()); + } + } + + public static void ParseFromString(NameValueCollection nvc, object value, string propertyName) + { + if (value != null) nvc.Add(propertyName, value.ToString()); + } + + //public static void ParseFromGeoLocation(NameValueCollection nvc, object value, string propertyName) + //{ + // var point = value as GeoLocation; + // if (point != null) nvc.Add(propertyName, point.ToString()); + //} + + //public static void ParseFromGeoLocationParameter(NameValueCollection nvc, object value, string propertyName) + //{ + // var point = value as GeoLocationParameter; + // if (point != null) nvc.Add(propertyName, point.ToString()); + //} + + public static IEnumerable ParseToStringArray(TParameters parameters, PropertyInfo prop, NameValueCollection nvc, string key) + { + var values = nvc.GetValues(key); + if (values != null && values.Any()) + { + return values; + } + throw new InvalidCastException($"Unable to parse [{key}] as array of string"); + } + + public static IEnumerable ParseToDoubleArray(TParameters parameters, PropertyInfo prop, NameValueCollection nvc, string key) + { + var values = nvc.GetValues(key)?.Select(Double.Parse).ToList(); + if (values != null && values.Any()) + { + return values; + } + return null; + } + + public static IEnumerable ParseToBoolArray(TParameters parameters, PropertyInfo prop, NameValueCollection nvc, string key) + { + var values = nvc.GetValues(key)?.Select(v => v.ToBool()).ToList(); + if (values != null && values.Any()) + { + return values; + } + return null; + } + + public static IEnumerable ParseToIntegerArray(TParameters parameters, PropertyInfo prop, NameValueCollection nvc, string key) + { + var values = nvc.GetValues(key)?.Select(int.Parse).ToList(); + if (values != null && values.Any()) + { + return values; + } + return null; + } + + public static IEnumerable ParseToLongArray(TParameters parameters, PropertyInfo prop, NameValueCollection nvc, string key) + { + var values = nvc.GetValues(key)?.Select(long.Parse).ToList(); + if (values != null && values.Any()) + { + return values; + } + return null; + } + + public static string ParseToString(TParameters parameters, PropertyInfo prop, NameValueCollection nvc, string key) + { + var value = nvc[key]; + return string.IsNullOrWhiteSpace(value) ? null : value; + } + + //public static GeoLocation ParseToGeoLocation(TParameters parameters, PropertyInfo prop, NameValueCollection nvc, string key) + //{ + // var value = nvc.Get(key); + // var points = value.Split(new[] {','}, StringSplitOptions.RemoveEmptyEntries).Select(double.Parse).ToArray(); + // return new GeoLocation(points[0], points[1]); + //} + + //public static GeoLocationParameter ParseToGeoLocationParameter(TParameters parameters, PropertyInfo prop, NameValueCollection nvc, string key) + //{ + // var value = nvc.Get(key); + // var points = value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(double.Parse).ToArray(); + // return new GeoLocationParameter(points[0], points[1]); + //} + + public static object ParseToInteger(TParameters parameters, PropertyInfo prop, NameValueCollection nvc, string key) + { + var value = ParseToString(parameters, prop, nvc, key); + if (value == null) return null; + + return int.Parse(value); + } + + public static object ParseToLong(TParameters parameters, PropertyInfo prop, NameValueCollection nvc, string key) + { + var value = ParseToString(parameters, prop, nvc, key); + if (value == null) return null; + + return long.Parse(value); + } + + public static object ParseToBool(TParameters parameters, PropertyInfo prop, NameValueCollection nvc, string key) + { + var value = ParseToString(parameters, prop, nvc, key)?.ToLowerInvariant(); + + return value.ToBool(); + } + + public static object ParseToNullableBool(TParameters parameters, PropertyInfo prop, NameValueCollection nvc, string key) + { + var value = ParseToString(parameters, prop, nvc, key)?.ToLowerInvariant(); + + return value.ToNullableBool(); + } + + public static object ParseToDouble(TParameters parameters, PropertyInfo prop, NameValueCollection nvc, string key) + { + var value = ParseToString(parameters, prop, nvc, key); + if (value == null) return null; + + return double.Parse(value); + } + + public static object ParseToEnum(TParameters parameters, PropertyInfo prop, NameValueCollection nvc, + string key) where TEnum : struct + { + if (nvc.AllKeys.Contains(key) && nvc[key] != null && Enum.TryParse(nvc[key], true, out TEnum enumValue)) + { + return enumValue; + } + + if (IsNullable(prop.PropertyType)) + { + return null; + } + + return default(TEnum); + } + + public static bool IsNullable(Type type) + { + if (type.IsGenericType) + { + return type.GetGenericTypeDefinition() == typeof(Nullable<>); + } + return false; + } + + #endregion + } + + private static readonly IDictionary> Resolvers = + new Dictionary>() + { + {typeof(IEnumerable), TypeParsers.ParseToStringArray}, + {typeof(string), TypeParsers.ParseToString}, + {typeof(IEnumerable), TypeParsers.ParseToIntegerArray}, + {typeof(IEnumerable), TypeParsers.ParseToLongArray}, + {typeof(IEnumerable), TypeParsers.ParseToDoubleArray}, + {typeof(double), TypeParsers.ParseToDouble}, + {typeof(int), TypeParsers.ParseToInteger}, + {typeof(long), TypeParsers.ParseToLong}, + {typeof(double?), TypeParsers.ParseToDouble}, + {typeof(int?), TypeParsers.ParseToInteger}, + {typeof(long?), TypeParsers.ParseToLong}, + {typeof(bool), TypeParsers.ParseToBool}, + {typeof(bool?), TypeParsers.ParseToNullableBool}, + {typeof(SortDirectionOption?), TypeParsers.ParseToEnum}, + // {typeof(GeoLocation), TypeParsers.ParseToGeoLocation}, // not currently supported + // {typeof(GeoLocationParameter), TypeParsers.ParseToGeoLocationParameter} // not currently supported + }; + + private static readonly IDictionary> Converters = new Dictionary + >() + { + {typeof(IEnumerable), TypeParsers.ParseFromStringArray}, + {typeof(IEnumerable), TypeParsers.ParseFromStringArray}, + {typeof(IEnumerable), TypeParsers.ParseFromStringArray}, + {typeof(IEnumerable), TypeParsers.ParseFromStringArray}, + {typeof(string), TypeParsers.ParseFromString}, + {typeof(int), TypeParsers.ParseFromString}, + {typeof(double), TypeParsers.ParseFromString}, + {typeof(long), TypeParsers.ParseFromString}, + {typeof(int?), TypeParsers.ParseFromString}, + {typeof(double?), TypeParsers.ParseFromString}, + {typeof(long?), TypeParsers.ParseFromString}, + {typeof(bool), TypeParsers.ParseBoolFromString}, + {typeof(bool?), TypeParsers.ParseNullableBoolFromString}, + {typeof(SortDirectionOption?), TypeParsers.ParseFromString}, + // {typeof(GeoLocation), TypeParsers.ParseFromGeoLocation}, // not currently supported + // {typeof (GeoLocationParameter), TypeParsers.ParseFromGeoLocationParameter}, // not currently supported + + }; + + /// + /// Resolvers are used to resolve from querystring value (string) to actual parameter property type + /// + /// + /// + public static void AddResolver(Func resolver) + { + if (!Resolvers.ContainsKey(typeof(T))) + { + Resolvers.Add(typeof(T), resolver); + } + else + { + Resolvers[typeof(T)] = resolver; + } + } + + /// + /// Converters are used to augment a name value collection with additional parameter properties + /// + /// + /// + public static void AddConverter(Action converter) + { + if (!Converters.ContainsKey(typeof(T))) + { + Converters.Add(typeof(T), converter); + } + else + { + Converters[typeof(T)] = converter; + } + } + + public static NameValueCollection Parse(TParameters parameters) + { + var nvc = HttpUtility.ParseQueryString(""); + var properties = typeof(TParameters).GetProperties(); + foreach (var prop in properties.OrderBy(o => o.Name)) + { + if (Converters.ContainsKey(prop.PropertyType)) + { + var o = prop.GetValue(parameters); + if (o != null) + { + var defaultValue = GetDefaultValue(prop); + if (defaultValue == null || o.ToString() != defaultValue.ToString()) + { + Converters[prop.PropertyType](nvc, o, GetParameterName(prop).Camelize()); + } + } + } + } + return TypeParsers.Sort(nvc); + } + + public static TParameters Parse(NameValueCollection queryString) + { + var p = new TParameters(); + Populate(queryString, p); + return p; + } + + public static TParameters Parse(string queryString) + { + var nvc = HttpUtility.ParseQueryString(queryString); + return Parse(nvc); + } + + public static void Populate(NameValueCollection nvc, TParameters parameters) + { + var properties = typeof(TParameters).GetProperties(); + foreach (var key in nvc.AllKeys) + { + var prop = + properties.FirstOrDefault(p => GetParameterName(p).Equals(key, StringComparison.InvariantCultureIgnoreCase)); + + if (prop != null) + { + if (Resolvers.ContainsKey(prop.PropertyType)) + { + try + { + var value = Resolvers[prop.PropertyType](parameters, prop, nvc, key); + if (value != null) + { + prop.SetValue(parameters, value); + } + } + catch (Exception ex) + { + throw new InvalidCastException($"Unable to parse [{key}] as [{prop.PropertyType.Name}]", ex); + } + } + } + } + } + + private static string GetParameterName(PropertyInfo propertyInfo) + { + if (propertyInfo == null) throw new ArgumentNullException(nameof(propertyInfo)); + return propertyInfo.GetCustomAttribute(true)?.PropertyName ?? propertyInfo.Name; + } + + private static object GetDefaultValue(PropertyInfo propertyInfo) + { + if (propertyInfo == null) throw new ArgumentNullException(nameof(propertyInfo)); + return propertyInfo.GetCustomAttribute(true)?.Value; + } + + public static TParameters Copy(TParameters parameters) + { + var p = new TParameters(); + Populate(Parse(parameters), p); + return p; + } + + public static string ToQueryString(TParameters parameters) + { + return Parse(parameters).ToString(); + } + } +} + +#endif \ No newline at end of file diff --git a/src/Nest.Searchify/SearchResults/PaginationOptions.cs b/src/Nest.Searchify/SearchResults/PaginationOptions.cs index abf61a8..9a7a16c 100644 --- a/src/Nest.Searchify/SearchResults/PaginationOptions.cs +++ b/src/Nest.Searchify/SearchResults/PaginationOptions.cs @@ -92,7 +92,8 @@ public TParameters LastPage() return p; } - public IEnumerable>> PagingGroup(int range = 5) +#if NETSTANDARD + public IEnumerable>> PagingGroup(int range = 5) { var fromPage = (Page - range) <= 0 ? 1 : Page - range; var toPage = (Page + range) > Pages ? Pages : Page + range; @@ -103,5 +104,18 @@ public TParameters LastPage() yield return new Tuple>(page, nvc); } } +#else + public IEnumerable> PagingGroup(int range = 5) + { + var fromPage = (Page - range) <= 0 ? 1 : Page - range; + var toPage = (Page + range) > Pages ? Pages : Page + range; + + for (var page = fromPage; page <= toPage; page++) + { + var nvc = QueryStringParser.Parse(ForPage(page)); + yield return new Tuple(page, nvc); + } + } +#endif } } \ No newline at end of file diff --git a/tests/Nest.Searchify.Tests/Nest.Searchify.Tests.csproj b/tests/Nest.Searchify.Tests/Nest.Searchify.Tests.csproj index cdd0fed..6f527f0 100644 --- a/tests/Nest.Searchify.Tests/Nest.Searchify.Tests.csproj +++ b/tests/Nest.Searchify.Tests/Nest.Searchify.Tests.csproj @@ -1,7 +1,7 @@  - netcoreapp3.0 + net461;netcoreapp3.0 Nest.Searchify.Tests Nest.Searchify.Tests false @@ -28,6 +28,19 @@ + + + + $(DefineConstants);NETSTANDARD + + + + + + + + + diff --git a/tests/Nest.Searchify.Tests/ParametersTests/QueryStringParserContext.cs b/tests/Nest.Searchify.Tests/ParametersTests/QueryStringParserContext.cs index 8eb02ef..603350c 100644 --- a/tests/Nest.Searchify.Tests/ParametersTests/QueryStringParserContext.cs +++ b/tests/Nest.Searchify.Tests/ParametersTests/QueryStringParserContext.cs @@ -132,9 +132,14 @@ public void ParseQueryStringForCustomParameters(string actual, string expected, [Fact] public void FromQueryString() { - var queryString = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery("q=test&options=o1&options=o2&numbers=1&numbers=5&doubles=99.99&doubles=8&sortdir=Asc&enumOptions=optionone"); var p = new MyParameters(); +#if NETSTANDARD + var queryString = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery("q=test&options=o1&options=o2&numbers=1&numbers=5&doubles=99.99&doubles=8&sortdir=Asc&enumOptions=optionone"); +#else + var queryString = System.Web.HttpUtility.ParseQueryString("q=test&options=o1&options=o2&numbers=1&numbers=5&doubles=99.99&doubles=8&sortdir=Asc&enumOptions=optionone"); +#endif + QueryStringParser.Populate(queryString, p); p.Query.Should().Be("test");