From db7356d754b1cb9b7f16c03b01861cca5fb6b957 Mon Sep 17 00:00:00 2001 From: JustinCanton <67930245+JustinCanton@users.noreply.github.com> Date: Tue, 14 May 2024 22:36:00 -0400 Subject: [PATCH 1/4] feat(positionstack): adding support for the positionstack geocoding API --- CHANGELOG.md | 3 +- Geo.NET.sln | 14 + .../Abstractions/IPositionstackGeocoding.cs | 37 ++ .../Models/IFilterGeocodeParameters.cs | 63 +++ .../Models/ILocationGeocodeParameters.cs | 27 + .../ServiceCollectionExtensions.cs | 44 ++ .../Extensions/LoggerExtensions.cs | 62 +++ .../Geo.Positionstack.csproj | 45 ++ .../Models/Parameters/Coordinate.cs | 31 ++ .../Models/Parameters/GeocodingParameters.cs | 51 ++ .../Parameters/ReverseGeocodingParameters.cs | 51 ++ .../Models/Responses/Address.cs | 124 +++++ .../Models/Responses/GeocodeAddress.cs | 21 + .../Models/Responses/Geometry.cs | 28 + .../Models/Responses/Meta.cs | 18 + .../Models/Responses/Response.cs | 29 ++ .../Models/Responses/ReverseGeocodeAddress.cs | 21 + .../Properties/AssemblyInfo.cs | 10 + src/Geo.Positionstack/README.md | 39 ++ .../PositionstackGeocoding.Designer.cs | 180 +++++++ .../Services/PositionstackGeocoding.en.resx | 159 ++++++ .../Services/PositionstackGeocoding.resx | 159 ++++++ .../Services/PositionstackGeocoding.cs | 271 ++++++++++ .../ServiceCollectionExtensionsTests.cs | 75 +++ .../Geo.Positionstack.Tests.csproj | 15 + .../GlobalSuppressions.cs | 13 + .../Services/PositionstackGeocodingShould.cs | 479 ++++++++++++++++++ .../TestData/CultureTestData.cs | 36 ++ 28 files changed, 2104 insertions(+), 1 deletion(-) create mode 100644 src/Geo.Positionstack/Abstractions/IPositionstackGeocoding.cs create mode 100644 src/Geo.Positionstack/Abstractions/Models/IFilterGeocodeParameters.cs create mode 100644 src/Geo.Positionstack/Abstractions/Models/ILocationGeocodeParameters.cs create mode 100644 src/Geo.Positionstack/DependencyInjection/ServiceCollectionExtensions.cs create mode 100644 src/Geo.Positionstack/Extensions/LoggerExtensions.cs create mode 100644 src/Geo.Positionstack/Geo.Positionstack.csproj create mode 100644 src/Geo.Positionstack/Models/Parameters/Coordinate.cs create mode 100644 src/Geo.Positionstack/Models/Parameters/GeocodingParameters.cs create mode 100644 src/Geo.Positionstack/Models/Parameters/ReverseGeocodingParameters.cs create mode 100644 src/Geo.Positionstack/Models/Responses/Address.cs create mode 100644 src/Geo.Positionstack/Models/Responses/GeocodeAddress.cs create mode 100644 src/Geo.Positionstack/Models/Responses/Geometry.cs create mode 100644 src/Geo.Positionstack/Models/Responses/Meta.cs create mode 100644 src/Geo.Positionstack/Models/Responses/Response.cs create mode 100644 src/Geo.Positionstack/Models/Responses/ReverseGeocodeAddress.cs create mode 100644 src/Geo.Positionstack/Properties/AssemblyInfo.cs create mode 100644 src/Geo.Positionstack/README.md create mode 100644 src/Geo.Positionstack/Resources/Services/PositionstackGeocoding.Designer.cs create mode 100644 src/Geo.Positionstack/Resources/Services/PositionstackGeocoding.en.resx create mode 100644 src/Geo.Positionstack/Resources/Services/PositionstackGeocoding.resx create mode 100644 src/Geo.Positionstack/Services/PositionstackGeocoding.cs create mode 100644 test/Geo.Positionstack.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs create mode 100644 test/Geo.Positionstack.Tests/Geo.Positionstack.Tests.csproj create mode 100644 test/Geo.Positionstack.Tests/GlobalSuppressions.cs create mode 100644 test/Geo.Positionstack.Tests/Services/PositionstackGeocodingShould.cs create mode 100644 test/Geo.Positionstack.Tests/TestData/CultureTestData.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index f785322..7a01930 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,8 @@ All notable changes to this project will be documented in this file. See [Conve ## [2.1.0](https://github.com/JustinCanton/Geo.NET/compare/2.0.0...2.1.0) (2024-06-30) ### Features -- **radar**: adding support for the Radar geocoding API ([#100](https://github.com/JustinCanton/Geo.NET/issues/100) ([](https://github.com/JustinCanton/Geo.NET/commit/)) +- **radar**: adding support for the Radar geocoding API ([#100](https://github.com/JustinCanton/Geo.NET/issues/100) ([448b087](https://github.com/JustinCanton/Geo.NET/commit/448b0876a0fb36fc74f348752e7dd77f63f3f7dd)) +- **positionstack**: adding support for the positionstack geocoding API ([#65](https://github.com/JustinCanton/Geo.NET/issues/65) ([](https://github.com/JustinCanton/Geo.NET/commit/)) ## [2.0.0](https://github.com/JustinCanton/Geo.NET/compare/1.6.0...2.0.0) (2024-01-28) ### ⚠ BREAKING CHANGES diff --git a/Geo.NET.sln b/Geo.NET.sln index abbde23..be941ab 100644 --- a/Geo.NET.sln +++ b/Geo.NET.sln @@ -49,6 +49,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Geo.Radar", "src\Geo.Radar\ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Geo.Radar.Tests", "test\Geo.Radar.Tests\Geo.Radar.Tests.csproj", "{1A320DE3-B14B-46EE-A0E6-C6783E585F73}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Geo.Positionstack", "src\Geo.Positionstack\Geo.Positionstack.csproj", "{03C7B4AE-7905-4292-8133-3F4AA6EDCA3A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Geo.Positionstack.Tests", "test\Geo.Positionstack.Tests\Geo.Positionstack.Tests.csproj", "{E09BD60D-6E8A-4210-9274-695A2DFFE976}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -119,6 +123,14 @@ Global {1A320DE3-B14B-46EE-A0E6-C6783E585F73}.Debug|Any CPU.Build.0 = Debug|Any CPU {1A320DE3-B14B-46EE-A0E6-C6783E585F73}.Release|Any CPU.ActiveCfg = Release|Any CPU {1A320DE3-B14B-46EE-A0E6-C6783E585F73}.Release|Any CPU.Build.0 = Release|Any CPU + {03C7B4AE-7905-4292-8133-3F4AA6EDCA3A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {03C7B4AE-7905-4292-8133-3F4AA6EDCA3A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {03C7B4AE-7905-4292-8133-3F4AA6EDCA3A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {03C7B4AE-7905-4292-8133-3F4AA6EDCA3A}.Release|Any CPU.Build.0 = Release|Any CPU + {E09BD60D-6E8A-4210-9274-695A2DFFE976}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E09BD60D-6E8A-4210-9274-695A2DFFE976}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E09BD60D-6E8A-4210-9274-695A2DFFE976}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E09BD60D-6E8A-4210-9274-695A2DFFE976}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -140,6 +152,8 @@ Global {85FF4115-0880-4DF6-816A-B314CA0432D3} = {67253D97-9FC9-4749-80DC-A5D84339DC05} {38C213E3-55D6-4FC7-87CA-E28C3EDD42EA} = {8F3BA9BC-542C-450C-96C9-F0D72FECC930} {1A320DE3-B14B-46EE-A0E6-C6783E585F73} = {67253D97-9FC9-4749-80DC-A5D84339DC05} + {03C7B4AE-7905-4292-8133-3F4AA6EDCA3A} = {8F3BA9BC-542C-450C-96C9-F0D72FECC930} + {E09BD60D-6E8A-4210-9274-695A2DFFE976} = {67253D97-9FC9-4749-80DC-A5D84339DC05} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {C4B688C4-40EC-4577-9EB2-4CF2412DA0B1} diff --git a/src/Geo.Positionstack/Abstractions/IPositionstackGeocoding.cs b/src/Geo.Positionstack/Abstractions/IPositionstackGeocoding.cs new file mode 100644 index 0000000..8f1488f --- /dev/null +++ b/src/Geo.Positionstack/Abstractions/IPositionstackGeocoding.cs @@ -0,0 +1,37 @@ +// +// Copyright (c) Geo.NET. +// Licensed under the MIT license. See the LICENSE file in the solution root for full license information. +// + +namespace Geo.Positionstack +{ + using System.Threading; + using System.Threading.Tasks; + using Geo.Core.Models.Exceptions; + using Geo.Positionstack.Models.Parameters; + using Geo.Positionstack.Models.Responses; + + /// + /// An interface for calling the Positionstack geocoding methods. + /// + public interface IPositionstackGeocoding + { + /// + /// Calls the Positionstack geocoding API and returns the results. + /// + /// A with the parameters of the request. + /// A used to cancel the request. + /// A of with the response from Positionstack. + /// Thrown for multiple different reasons. Check the inner exception for more information. + Task> GeocodingAsync(GeocodingParameters parameters, CancellationToken cancellationToken = default); + + /// + /// Calls the Positionstack reverse geocoding API and returns the results. + /// + /// A with the parameters of the request. + /// A used to cancel the request. + /// A of with the response from Positionstack. + /// Thrown for multiple different reasons. Check the inner exception for more information. + Task> ReverseGeocodingAsync(ReverseGeocodingParameters parameters, CancellationToken cancellationToken = default); + } +} diff --git a/src/Geo.Positionstack/Abstractions/Models/IFilterGeocodeParameters.cs b/src/Geo.Positionstack/Abstractions/Models/IFilterGeocodeParameters.cs new file mode 100644 index 0000000..81bad25 --- /dev/null +++ b/src/Geo.Positionstack/Abstractions/Models/IFilterGeocodeParameters.cs @@ -0,0 +1,63 @@ +// +// Copyright (c) Geo.NET. +// Licensed under the MIT license. See the LICENSE file in the solution root for full license information. +// + +namespace Geo.Positionstack.Models +{ + using System.Collections.Generic; + + /// + /// The base filter parameters used for geocoding or reverse geocoding. + /// + public interface IFilterGeocodeParameters + { + /// + /// Gets or sets the 2-letter(e.g.en) or the 3-letter code(e.g.eng) of your preferred language to translate specific API response objects. + /// Optional. + /// Default: English. + /// + string Language { get; set; } + + /// + /// Gets or sets a value indicating whether the country module should be enabled to include more extensive country data in your API response. + /// Optional. + /// Default: false. + /// + bool CountryModule { get; set; } + + /// + /// Gets or sets a value indicating whether the sun module should be enabled to include astrology data in your API response. + /// Optional. + /// Default: false. + /// + bool SunModule { get; set; } + + /// + /// Gets or sets a value indicating whether the timezone module should be enabled to include timezone data in your API response. + /// Optional. + /// Default: false. + /// + bool TimezoneModule { get; set; } + + /// + /// Gets or sets a value indicating whether the bounding box module should be enabled to include boundary coordinates in your API response. + /// Optional. + /// Default: false. + /// + bool BoundingBoxModule { get; set; } + + /// + /// Gets or sets a limit between 1 and 80 to limit the number of results returned per geocoding query. + /// Optional. + /// Default: 10. + /// + uint Limit { get; set; } + + /// + /// Gets a list of one or more response fields to decrease API response size. + /// Optional. + /// + IList Fields { get; } + } +} diff --git a/src/Geo.Positionstack/Abstractions/Models/ILocationGeocodeParameters.cs b/src/Geo.Positionstack/Abstractions/Models/ILocationGeocodeParameters.cs new file mode 100644 index 0000000..83bf49d --- /dev/null +++ b/src/Geo.Positionstack/Abstractions/Models/ILocationGeocodeParameters.cs @@ -0,0 +1,27 @@ +// +// Copyright (c) Geo.NET. +// Licensed under the MIT license. See the LICENSE file in the solution root for full license information. +// + +namespace Geo.Positionstack.Models +{ + using System.Collections.Generic; + + /// + /// The location based filter parameters used for geocoding or reverse geocoding. + /// + public interface ILocationGeocodeParameters + { + /// + /// Gets one or more 2-letter(e.g.AU) or 3-letter country codes(e.g.AUS) to filter the geocoding results. + /// Optional. + /// + IList Country { get; } + + /// + /// Gets or sets a filter for the geocoding results specifying a region. This could be a neighbourhood, district, city, county, state or administrative area. Example: region= Berlin to filter by locations in Berlin. + /// Optional. + /// + string Region { get; set; } + } +} diff --git a/src/Geo.Positionstack/DependencyInjection/ServiceCollectionExtensions.cs b/src/Geo.Positionstack/DependencyInjection/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..25bd43f --- /dev/null +++ b/src/Geo.Positionstack/DependencyInjection/ServiceCollectionExtensions.cs @@ -0,0 +1,44 @@ +// +// Copyright (c) Geo.NET. +// Licensed under the MIT license. See the LICENSE file in the solution root for full license information. +// + +namespace Geo.Extensions.DependencyInjection +{ + using System; + using Geo.Positionstack; + using Geo.Positionstack.Services; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.Options; + + /// + /// Extension methods for the class. + /// + public static class ServiceCollectionExtensions + { + /// + /// Adds the Positionstack geocoding services to the service collection. + /// + /// Adds the services: + /// + /// of + /// + /// + /// + /// + /// An to add the Positionstack services to. + /// An to configure the Positionstack geocoding. + /// Thrown if is null. + public static KeyBuilder AddPositionstackGeocoding(this IServiceCollection services) + { + if (services == null) + { + throw new ArgumentNullException(nameof(services)); + } + + services.AddKeyOptions(); + + return new KeyBuilder(services.AddHttpClient()); + } + } +} diff --git a/src/Geo.Positionstack/Extensions/LoggerExtensions.cs b/src/Geo.Positionstack/Extensions/LoggerExtensions.cs new file mode 100644 index 0000000..6c7738c --- /dev/null +++ b/src/Geo.Positionstack/Extensions/LoggerExtensions.cs @@ -0,0 +1,62 @@ +// +// Copyright (c) Geo.NET. +// Licensed under the MIT license. See the LICENSE file in the solution root for full license information. +// + +namespace Geo.Positionstack +{ + using System; + using Geo.Positionstack.Services; + using Microsoft.Extensions.Logging; + + /// + /// Extension methods for the class. + /// + internal static class LoggerExtensions + { + private static readonly Action _error = LoggerMessage.Define( + LogLevel.Error, + new EventId(1, nameof(PositionstackGeocoding)), + "PositionstackGeocoding: {ErrorMessage}"); + + private static readonly Action _warning = LoggerMessage.Define( + LogLevel.Warning, + new EventId(2, nameof(PositionstackGeocoding)), + "PositionstackGeocoding: {WarningMessage}"); + + private static readonly Action _debug = LoggerMessage.Define( + LogLevel.Debug, + new EventId(3, nameof(PositionstackGeocoding)), + "PositionstackGeocoding: {DebugMessage}"); + + /// + /// "PositionstackGeocoding: {ErrorMessage}". + /// + /// An used to log the error message. + /// The error message to log. + public static void PositionstackError(this ILogger logger, string errorMessage) + { + _error(logger, errorMessage, null); + } + + /// + /// "PositionstackGeocoding: {WarningMessage}". + /// + /// An used to log the warning message. + /// The warning message to log. + public static void PositionstackWarning(this ILogger logger, string warningMessage) + { + _warning(logger, warningMessage, null); + } + + /// + /// "PositionstackGeocoding: {DebugMessage}". + /// + /// An used to log the debug message. + /// The debug message to log. + public static void PositionstackDebug(this ILogger logger, string debugMessage) + { + _debug(logger, debugMessage, null); + } + } +} diff --git a/src/Geo.Positionstack/Geo.Positionstack.csproj b/src/Geo.Positionstack/Geo.Positionstack.csproj new file mode 100644 index 0000000..bb14ee6 --- /dev/null +++ b/src/Geo.Positionstack/Geo.Positionstack.csproj @@ -0,0 +1,45 @@ + + + + netstandard2.0;net6.0;net8.0 + Justin Canton + Geo.NET + Geo.NET Positionstack + geocoding geo.net Positionstack + A lightweight method for communicating with the Positionstack geocoding APIs. This includes models and interfaces for calling Positionstack. + MIT + https://github.com/JustinCanton/Geo.NET + true + README.md + + + + + + + + + + + True + True + PositionstackGeocoding.resx + + + + + + ResXFileCodeGenerator + PositionstackGeocoding.Designer.cs + + + + + + True + \ + Always + + + + diff --git a/src/Geo.Positionstack/Models/Parameters/Coordinate.cs b/src/Geo.Positionstack/Models/Parameters/Coordinate.cs new file mode 100644 index 0000000..1974a0f --- /dev/null +++ b/src/Geo.Positionstack/Models/Parameters/Coordinate.cs @@ -0,0 +1,31 @@ +// +// Copyright (c) Geo.NET. +// Licensed under the MIT license. See the LICENSE file in the solution root for full license information. +// + +namespace Geo.Positionstack.Models.Parameters +{ + using System.Globalization; + + /// + /// The coordinates (latitude, longitude) of a pin on a map corresponding to the searched place. + /// + public class Coordinate + { + /// + /// Gets or sets the latitude of the address. For example: "52.19404". + /// + public double Latitude { get; set; } + + /// + /// Gets or sets the longitude of the address. For example: "8.80135". + /// + public double Longitude { get; set; } + + /// + public override string ToString() + { + return string.Format(CultureInfo.InvariantCulture, "{0},{1}", Latitude, Longitude); + } + } +} diff --git a/src/Geo.Positionstack/Models/Parameters/GeocodingParameters.cs b/src/Geo.Positionstack/Models/Parameters/GeocodingParameters.cs new file mode 100644 index 0000000..99e1cdb --- /dev/null +++ b/src/Geo.Positionstack/Models/Parameters/GeocodingParameters.cs @@ -0,0 +1,51 @@ +// +// Copyright (c) Geo.NET. +// Licensed under the MIT license. See the LICENSE file in the solution root for full license information. +// + +namespace Geo.Positionstack.Models.Parameters +{ + using System.Collections.Generic; + using Geo.Positionstack.Models; + + /// + /// The parameters possible to use during a geocoding request. + /// + public class GeocodingParameters : ILocationGeocodeParameters, IFilterGeocodeParameters, IKeyParameters + { + /// + /// Gets or sets the address to geocode. + /// + public string Query { get; set; } + + /// + public IList Country { get; } = new List(); + + /// + public string Region { get; set; } + + /// + public string Language { get; set; } + + /// + public bool CountryModule { get; set; } + + /// + public bool SunModule { get; set; } + + /// + public bool TimezoneModule { get; set; } + + /// + public bool BoundingBoxModule { get; set; } + + /// + public uint Limit { get; set; } = 10; + + /// + public IList Fields { get; } = new List(); + + /// + public string Key { get; set; } + } +} diff --git a/src/Geo.Positionstack/Models/Parameters/ReverseGeocodingParameters.cs b/src/Geo.Positionstack/Models/Parameters/ReverseGeocodingParameters.cs new file mode 100644 index 0000000..79986a5 --- /dev/null +++ b/src/Geo.Positionstack/Models/Parameters/ReverseGeocodingParameters.cs @@ -0,0 +1,51 @@ +// +// Copyright (c) Geo.NET. +// Licensed under the MIT license. See the LICENSE file in the solution root for full license information. +// + +namespace Geo.Positionstack.Models.Parameters +{ + using System.Collections.Generic; + using Geo.Positionstack.Models; + + /// + /// The parameters possible to use during a reverse geocoding request. + /// + public class ReverseGeocodingParameters : ILocationGeocodeParameters, IFilterGeocodeParameters, IKeyParameters + { + /// + /// Gets or sets the coordinates to reverse geocode. + /// + public Coordinate Coordinate { get; set; } + + /// + public IList Country { get; } = new List(); + + /// + public string Region { get; set; } + + /// + public string Language { get; set; } + + /// + public bool CountryModule { get; set; } + + /// + public bool SunModule { get; set; } + + /// + public bool TimezoneModule { get; set; } + + /// + public bool BoundingBoxModule { get; set; } + + /// + public uint Limit { get; set; } = 10; + + /// + public IList Fields { get; } = new List(); + + /// + public string Key { get; set; } + } +} diff --git a/src/Geo.Positionstack/Models/Responses/Address.cs b/src/Geo.Positionstack/Models/Responses/Address.cs new file mode 100644 index 0000000..03e055e --- /dev/null +++ b/src/Geo.Positionstack/Models/Responses/Address.cs @@ -0,0 +1,124 @@ +// +// Copyright (c) Geo.NET. +// Licensed under the MIT license. See the LICENSE file in the solution root for full license information. +// + +namespace Geo.Positionstack.Models.Responses +{ + using System.Text.Json.Serialization; + + /// + /// The address information for the location. + /// + public class Address + { + /// + /// Gets or sets the latitude of the address. For example: "52.19404". + /// + [JsonPropertyName("latitude")] + public double Latitude { get; set; } + + /// + /// Gets or sets the longitude of the address. For example: "8.80135". + /// + [JsonPropertyName("longitude")] + public double Longitude { get; set; } + + /// + /// Gets or sets the geometry information for the address. + /// + [JsonPropertyName("geometry")] + public Geometry Geometry { get; set; } + + /// + /// Gets or sets the localised country name. For example: "United States". + /// + [JsonPropertyName("country")] + public string Country { get; set; } + + /// + /// Gets or sets a two-letter country code. For example: "US". + /// + [JsonPropertyName("countryCode")] + public string CountryCode { get; set; } + + /// + /// Gets or sets the country flag. + /// + [JsonPropertyName("countryFlag")] + public string CountryFlag { get; set; } + + /// + /// Gets or sets the county information for an address. + /// + [JsonPropertyName("county")] + public string County { get; set; } + + /// + /// Gets or sets the borough information for an address. + /// + [JsonPropertyName("borough")] + public string Borough { get; set; } + + /// + /// Gets or sets the name of the primary locality of the place. For example: "Bad Oyenhausen". + /// + [JsonPropertyName("city")] + public string City { get; set; } + + /// + /// Gets or sets the number. For example: "32547". + /// + [JsonPropertyName("number")] + public string Number { get; set; } + + /// + /// Gets or sets the name of neighbourhood of the address. + /// + [JsonPropertyName("neighborhood")] + public string Neighborhood { get; set; } + + /// + /// Gets or sets an alphanumeric string included in a postal address to facilitate mail sorting, such as post code, postcode, or ZIP code. + /// For example: "32547". + /// + [JsonPropertyName("postalCode")] + public string PostalCode { get; set; } + + /// + /// Gets or sets the state code for the state. For example: "NY". + /// + [JsonPropertyName("stateCode")] + public string StateCode { get; set; } + + /// + /// Gets or sets a code/abbreviation for the state division of a country. For example: "North Rhine-Westphalia". + /// + [JsonPropertyName("state")] + public string State { get; set; } + + /// + /// Gets or sets the name of street. For example: "Schulstrasse". + /// + [JsonPropertyName("street")] + public string Street { get; set; } + + /// + /// Gets or sets the layer information for the address. + /// + [JsonPropertyName("layer")] + public string Layer { get; set; } + + /// + /// Gets or sets the formatted address. For example: "20 Jay St, Brooklyn, New York, NY 11201 USA". + /// + [JsonPropertyName("formattedAddress")] + public string FormattedAddress { get; set; } + + /// + /// Gets or sets the address label. For example: "20 Jay St". + /// + [JsonPropertyName("addressLabel")] + public string AddressLabel { get; set; } + } +} diff --git a/src/Geo.Positionstack/Models/Responses/GeocodeAddress.cs b/src/Geo.Positionstack/Models/Responses/GeocodeAddress.cs new file mode 100644 index 0000000..727364b --- /dev/null +++ b/src/Geo.Positionstack/Models/Responses/GeocodeAddress.cs @@ -0,0 +1,21 @@ +// +// Copyright (c) Geo.NET. +// Licensed under the MIT license. See the LICENSE file in the solution root for full license information. +// + +namespace Geo.Positionstack.Models.Responses +{ + using System.Text.Json.Serialization; + + /// + /// Address information for a geocode result. + /// + public class GeocodeAddress : Address + { + /// + /// Gets or sets the confidence of the result. + /// + [JsonPropertyName("confidence")] + public string Confidence { get; set; } + } +} diff --git a/src/Geo.Positionstack/Models/Responses/Geometry.cs b/src/Geo.Positionstack/Models/Responses/Geometry.cs new file mode 100644 index 0000000..56f2c30 --- /dev/null +++ b/src/Geo.Positionstack/Models/Responses/Geometry.cs @@ -0,0 +1,28 @@ +// +// Copyright (c) Geo.NET. +// Licensed under the MIT license. See the LICENSE file in the solution root for full license information. +// + +namespace Geo.Positionstack.Models.Responses +{ + using System.Text.Json.Serialization; + using Geo.Positionstack.Models.Parameters; + + /// + /// The geometry information for the address. + /// + public class Geometry + { + /// + /// Gets or sets the type of the coordinate. + /// + [JsonPropertyName("type")] + public string Type { get; set; } + + /// + /// Gets or sets the coordinates of the address. + /// + [JsonPropertyName("coordinates")] + public Coordinate Coordinate { get; set; } + } +} diff --git a/src/Geo.Positionstack/Models/Responses/Meta.cs b/src/Geo.Positionstack/Models/Responses/Meta.cs new file mode 100644 index 0000000..112f5e4 --- /dev/null +++ b/src/Geo.Positionstack/Models/Responses/Meta.cs @@ -0,0 +1,18 @@ +// +// Copyright (c) Geo.NET. +// Licensed under the MIT license. See the LICENSE file in the solution root for full license information. +// + +namespace Geo.Positionstack.Models.Responses +{ + /// + /// Metadata associate with the request. + /// + public class Meta + { + /// + /// Gets or sets the response code from the request. + /// + public int Code { get; set; } + } +} diff --git a/src/Geo.Positionstack/Models/Responses/Response.cs b/src/Geo.Positionstack/Models/Responses/Response.cs new file mode 100644 index 0000000..e35361e --- /dev/null +++ b/src/Geo.Positionstack/Models/Responses/Response.cs @@ -0,0 +1,29 @@ +// +// Copyright (c) Geo.NET. +// Licensed under the MIT license. See the LICENSE file in the solution root for full license information. +// + +namespace Geo.Positionstack.Models.Responses +{ + using System.Collections.Generic; + using System.Text.Json.Serialization; + + /// + /// The result of a request. + /// + /// The type of the address. + public class Response + { + /// + /// Gets or sets the metadata associate with the request. + /// + [JsonPropertyName("meta")] + public Meta Meta { get; set; } + + /// + /// Gets or sets the addresses that are associated with the request. + /// + [JsonPropertyName("addresses")] + public IList Addresses { get; set; } = new List(); + } +} diff --git a/src/Geo.Positionstack/Models/Responses/ReverseGeocodeAddress.cs b/src/Geo.Positionstack/Models/Responses/ReverseGeocodeAddress.cs new file mode 100644 index 0000000..2227904 --- /dev/null +++ b/src/Geo.Positionstack/Models/Responses/ReverseGeocodeAddress.cs @@ -0,0 +1,21 @@ +// +// Copyright (c) Geo.NET. +// Licensed under the MIT license. See the LICENSE file in the solution root for full license information. +// + +namespace Geo.Positionstack.Models.Responses +{ + using System.Text.Json.Serialization; + + /// + /// Address information for a reverse geocode result. + /// + public class ReverseGeocodeAddress : Address + { + /// + /// Gets or sets the distance of the result from the point. + /// + [JsonPropertyName("distance")] + public int Distance { get; set; } + } +} diff --git a/src/Geo.Positionstack/Properties/AssemblyInfo.cs b/src/Geo.Positionstack/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..2003e95 --- /dev/null +++ b/src/Geo.Positionstack/Properties/AssemblyInfo.cs @@ -0,0 +1,10 @@ +// +// Copyright (c) Geo.NET. +// Licensed under the MIT license. See the LICENSE file in the solution root for full license information. +// + +using System.Runtime.CompilerServices; +using Microsoft.Extensions.Localization; + +[assembly: InternalsVisibleTo("Geo.Positionstack.Tests")] +[assembly: ResourceLocation("Resources")] diff --git a/src/Geo.Positionstack/README.md b/src/Geo.Positionstack/README.md new file mode 100644 index 0000000..1f2f7a9 --- /dev/null +++ b/src/Geo.Positionstack/README.md @@ -0,0 +1,39 @@ +# Positionstack Geocoding + +This allows the simple calling of Positionstack geocoding APIs. The supported Positionstack geocoding endpoints are: +- [Geocoding](https://api.positionstack.com/v1/forward) +- [Reverse Geocoding](https://api.positionstack.com/v1/reverse) + +## Configuration + +In the startup `ConfigureServices` method, add the configuration for the Positionstack service: +``` +using Geo.Extensions.DependencyInjection; +. +. +. +public void ConfigureServices(IServiceCollection services) +{ + . + . + . + var builder = services.AddPositionstackGeocoding(); + builder.AddKey(your_Positionstack_api_key_here); + builder.HttpClientBuilder.ConfigureHttpClient(configure_client); + . + . + . +} +``` + +## Sample Usage + +By calling `AddPositionstackGeocoding`, the `IPositionstackGeocoding` interface has been added to the IOC container. Just request it as a DI item: +``` +public MyService(IPositionstackGeocoding PositionstackGeocoding) +{ + ... +} +``` + +Now simply call the geocoding methods in the interface. \ No newline at end of file diff --git a/src/Geo.Positionstack/Resources/Services/PositionstackGeocoding.Designer.cs b/src/Geo.Positionstack/Resources/Services/PositionstackGeocoding.Designer.cs new file mode 100644 index 0000000..90f8438 --- /dev/null +++ b/src/Geo.Positionstack/Resources/Services/PositionstackGeocoding.Designer.cs @@ -0,0 +1,180 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Geo.Positionstack.Resources.Services { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class PositionstackGeocoding { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal PositionstackGeocoding() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Geo.Positionstack.Resources.Services.PositionstackGeocoding", typeof(PositionstackGeocoding).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Failed to create the Positionstack uri.. + /// + internal static string Failed_To_Create_Uri { + get { + return ResourceManager.GetString("Failed To Create Uri", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The bounding box module will not be returned.. + /// + internal static string Invalid_BoundingBox_Module { + get { + return ResourceManager.GetString("Invalid BoundingBox Module", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The coordinates cannot be null or invalid.. + /// + internal static string Invalid_Coordinates { + get { + return ResourceManager.GetString("Invalid Coordinates", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The country is invalid and will not be used.. + /// + internal static string Invalid_Country { + get { + return ResourceManager.GetString("Invalid Country", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The country module will not be returned.. + /// + internal static string Invalid_Country_Module { + get { + return ResourceManager.GetString("Invalid Country Module", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The fields are invalid and will not be used.. + /// + internal static string Invalid_Fields { + get { + return ResourceManager.GetString("Invalid Fields", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The language is invalid and will not be used.. + /// + internal static string Invalid_Language { + get { + return ResourceManager.GetString("Invalid Language", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The limit is invalid and will not be used.. + /// + internal static string Invalid_Limit { + get { + return ResourceManager.GetString("Invalid Limit", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The query cannot be null or invalid.. + /// + internal static string Invalid_Query { + get { + return ResourceManager.GetString("Invalid Query", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The region is invalid and will not be used.. + /// + internal static string Invalid_Region { + get { + return ResourceManager.GetString("Invalid Region", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The sun module will not be returned.. + /// + internal static string Invalid_Sun_Module { + get { + return ResourceManager.GetString("Invalid Sun Module", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The timezone module will not be returned.. + /// + internal static string Invalid_Timezone_Module { + get { + return ResourceManager.GetString("Invalid Timezone Module", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The Positionstack parameters are null.. + /// + internal static string Null_Parameters { + get { + return ResourceManager.GetString("Null Parameters", resourceCulture); + } + } + } +} diff --git a/src/Geo.Positionstack/Resources/Services/PositionstackGeocoding.en.resx b/src/Geo.Positionstack/Resources/Services/PositionstackGeocoding.en.resx new file mode 100644 index 0000000..c70a34c --- /dev/null +++ b/src/Geo.Positionstack/Resources/Services/PositionstackGeocoding.en.resx @@ -0,0 +1,159 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Failed to create the Positionstack uri. + + + The bounding box module will not be returned. + + + The coordinates cannot be null or invalid. + + + The country is invalid and will not be used. + + + The country module will not be returned. + + + The fields are invalid and will not be used. + + + The language is invalid and will not be used. + + + The limit is invalid and will not be used. + + + The query cannot be null or invalid. + + + The region is invalid and will not be used. + + + The sun module will not be returned. + + + The timezone module will not be returned. + + + The Positionstack parameters are null. + + \ No newline at end of file diff --git a/src/Geo.Positionstack/Resources/Services/PositionstackGeocoding.resx b/src/Geo.Positionstack/Resources/Services/PositionstackGeocoding.resx new file mode 100644 index 0000000..c70a34c --- /dev/null +++ b/src/Geo.Positionstack/Resources/Services/PositionstackGeocoding.resx @@ -0,0 +1,159 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Failed to create the Positionstack uri. + + + The bounding box module will not be returned. + + + The coordinates cannot be null or invalid. + + + The country is invalid and will not be used. + + + The country module will not be returned. + + + The fields are invalid and will not be used. + + + The language is invalid and will not be used. + + + The limit is invalid and will not be used. + + + The query cannot be null or invalid. + + + The region is invalid and will not be used. + + + The sun module will not be returned. + + + The timezone module will not be returned. + + + The Positionstack parameters are null. + + \ No newline at end of file diff --git a/src/Geo.Positionstack/Services/PositionstackGeocoding.cs b/src/Geo.Positionstack/Services/PositionstackGeocoding.cs new file mode 100644 index 0000000..90ca035 --- /dev/null +++ b/src/Geo.Positionstack/Services/PositionstackGeocoding.cs @@ -0,0 +1,271 @@ +// +// Copyright (c) Geo.NET. +// Licensed under the MIT license. See the LICENSE file in the solution root for full license information. +// + +namespace Geo.Positionstack.Services +{ + using System; + using System.Net.Http; + using System.Threading; + using System.Threading.Tasks; + using Geo.Core; + using Geo.Core.Extensions; + using Geo.Core.Models.Exceptions; + using Geo.Positionstack.Models; + using Geo.Positionstack.Models.Parameters; + using Geo.Positionstack.Models.Responses; + using Microsoft.Extensions.Logging; + using Microsoft.Extensions.Logging.Abstractions; + using Microsoft.Extensions.Options; + + /// + /// A service to call the Positionstack geocoding API. + /// + public class PositionstackGeocoding : GeoClient, IPositionstackGeocoding + { + private const string GeocodeUri = "https://api.positionstack.com/v1/forward"; + private const string ReverseGeocodeUri = "https://api.positionstack.com/v1/reverse"; + + private readonly IOptions> _options; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// A used for placing calls to the Positionstack Geocoding API. + /// An of containing Positionstack key information. + /// An used to create a logger used for logging information. + public PositionstackGeocoding( + HttpClient client, + IOptions> options, + ILoggerFactory loggerFactory = null) + : base(client, loggerFactory) + { + _options = options ?? throw new ArgumentNullException(nameof(options)); + _logger = loggerFactory?.CreateLogger() ?? NullLogger.Instance; + } + + /// + protected override string ApiName => "Positionstack"; + + /// + public async Task> GeocodingAsync( + GeocodingParameters parameters, + CancellationToken cancellationToken = default) + { + var uri = ValidateAndBuildUri(parameters, BuildGeocodingRequest); + + return await GetAsync>(uri, cancellationToken).ConfigureAwait(false); + } + + /// + public async Task> ReverseGeocodingAsync( + ReverseGeocodingParameters parameters, + CancellationToken cancellationToken = default) + { + var uri = ValidateAndBuildUri(parameters, BuildReverseGeocodingRequest); + + return await GetAsync>(uri, cancellationToken).ConfigureAwait(false); + } + + /// + /// Validates the uri and builds it based on the parameter type. + /// + /// The type of the parameters. + /// The parameters to validate and create a uri from. + /// The method to use to create the uri. + /// A with the uri crafted from the parameters. + internal Uri ValidateAndBuildUri(TParameters parameters, Func uriBuilderFunction) + where TParameters : class + { + if (parameters is null) + { + _logger.PositionstackError(Resources.Services.PositionstackGeocoding.Null_Parameters); + throw new GeoNETException(Resources.Services.PositionstackGeocoding.Null_Parameters, new ArgumentNullException(nameof(parameters))); + } + + try + { + return uriBuilderFunction(parameters); + } + catch (ArgumentException ex) + { + _logger.PositionstackError(Resources.Services.PositionstackGeocoding.Failed_To_Create_Uri); + throw new GeoNETException(Resources.Services.PositionstackGeocoding.Failed_To_Create_Uri, ex); + } + } + + /// + /// Builds the geocoding uri based on the passed parameters. + /// + /// A with the geocoding parameters to build the uri with. + /// A with the completed Positionstack geocoding uri. + /// Thrown when the parameter is null or invalid. + internal Uri BuildGeocodingRequest(GeocodingParameters parameters) + { + var uriBuilder = new UriBuilder(GeocodeUri); + var query = QueryString.Empty; + + if (string.IsNullOrWhiteSpace(parameters.Query)) + { + _logger.PositionstackError(Resources.Services.PositionstackGeocoding.Invalid_Query); + throw new ArgumentException(Resources.Services.PositionstackGeocoding.Invalid_Query, nameof(parameters.Query)); + } + + query = query.Add("query", parameters.Query); + + AddFilterParameters(parameters, ref query); + AddLocationParameters(parameters, ref query); + AddPositionstackKey(parameters, ref query); + + uriBuilder.AddQuery(query); + + return uriBuilder.Uri; + } + + /// + /// Builds the reverse geocoding uri based on the passed parameters. + /// + /// A with the reverse geocoding parameters to build the uri with. + /// A with the completed Positionstack reverse geocoding uri. + /// Thrown when the parameter is null or invalid. + internal Uri BuildReverseGeocodingRequest(ReverseGeocodingParameters parameters) + { + var uriBuilder = new UriBuilder(ReverseGeocodeUri); + var query = QueryString.Empty; + + if (parameters.Coordinate is null) + { + _logger.PositionstackError(Resources.Services.PositionstackGeocoding.Invalid_Coordinates); + throw new ArgumentException(Resources.Services.PositionstackGeocoding.Invalid_Coordinates, nameof(parameters.Coordinate)); + } + + query = query.Add("query", parameters.Coordinate.ToString()); + + AddFilterParameters(parameters, ref query); + AddLocationParameters(parameters, ref query); + AddPositionstackKey(parameters, ref query); + + uriBuilder.AddQuery(query); + + return uriBuilder.Uri; + } + + /// + /// Adds the location information to the query. + /// + /// The used to get the location information from. + /// A with the query parameters. + internal void AddLocationParameters(ILocationGeocodeParameters locationParameters, ref QueryString query) + { + if (locationParameters.Country?.Count > 0) + { + var countries = string.Join(",", locationParameters.Country ?? Array.Empty()); + query = query.Add("country", countries); + } + else + { + _logger.PositionstackWarning(Resources.Services.PositionstackGeocoding.Invalid_Country); + } + + if (!string.IsNullOrWhiteSpace(locationParameters.Region)) + { + query = query.Add("region", locationParameters.Region); + } + else + { + _logger.PositionstackDebug(Resources.Services.PositionstackGeocoding.Invalid_Region); + } + } + + /// + /// Adds the filter information to the query. + /// + /// The used to get the filter information from. + /// A with the query parameters. + internal void AddFilterParameters(IFilterGeocodeParameters filterParameters, ref QueryString query) + { + if (!string.IsNullOrWhiteSpace(filterParameters.Language)) + { + query = query.Add("language", filterParameters.Language); + } + else + { + _logger.PositionstackDebug(Resources.Services.PositionstackGeocoding.Invalid_Language); + } + + if (filterParameters.CountryModule) + { + query = query.Add("country_module", "1"); + } + else + { + _logger.PositionstackDebug(Resources.Services.PositionstackGeocoding.Invalid_Country_Module); + } + + if (filterParameters.SunModule) + { + query = query.Add("sun_module", "1"); + } + else + { + _logger.PositionstackDebug(Resources.Services.PositionstackGeocoding.Invalid_Sun_Module); + } + + if (filterParameters.TimezoneModule) + { + query = query.Add("timezone_module", "1"); + } + else + { + _logger.PositionstackDebug(Resources.Services.PositionstackGeocoding.Invalid_Timezone_Module); + } + + if (filterParameters.BoundingBoxModule) + { + query = query.Add("bbox_module", "1"); + } + else + { + _logger.PositionstackDebug(Resources.Services.PositionstackGeocoding.Invalid_BoundingBox_Module); + } + + if (filterParameters.Limit > 0 && filterParameters.Limit <= 80) + { + query = query.Add("limit", filterParameters.Limit.ToString()); + } + else + { + _logger.PositionstackWarning(Resources.Services.PositionstackGeocoding.Invalid_Limit); + } + + if (filterParameters.Fields?.Count > 0) + { + var fields = string.Join(",", filterParameters.Fields ?? Array.Empty()); + query = query.Add("fields", fields); + } + else + { + _logger.PositionstackWarning(Resources.Services.PositionstackGeocoding.Invalid_Fields); + } + } + + /// + /// Adds the Positionstack key to the request. + /// + /// An to conditionally get the key from. + /// A with the query parameters. + internal void AddPositionstackKey(IKeyParameters keyParameter, ref QueryString query) + { + var key = _options.Value.Key; + + if (!string.IsNullOrWhiteSpace(keyParameter.Key)) + { + key = keyParameter.Key; + } + + query = query.Add("access_key", key); + } + } +} diff --git a/test/Geo.Positionstack.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs b/test/Geo.Positionstack.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs new file mode 100644 index 0000000..8fe3a86 --- /dev/null +++ b/test/Geo.Positionstack.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs @@ -0,0 +1,75 @@ +// +// Copyright (c) Geo.NET. +// Licensed under the MIT license. See the LICENSE file in the solution root for full license information. +// + +namespace Geo.Positionstack.Tests.DependencyInjection +{ + using System; + using System.Net.Http; + using FluentAssertions; + using Geo.Extensions.DependencyInjection; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.Options; + using Xunit; + + /// + /// Unit tests for the class. + /// + public class ServiceCollectionExtensionsTests + { + [Fact] + public void AddPositionstackGeocoding_WithValidCall_ConfiguresAllServices() + { + // Arrange + var services = new ServiceCollection(); + + // Act + var builder = services.AddPositionstackGeocoding(); + builder.AddKey("abc"); + + // Assert + var provider = services.BuildServiceProvider(); + + var options = provider.GetRequiredService>>(); + options.Should().NotBeNull(); + options.Value.Key.Should().Be("abc"); + provider.GetRequiredService().Should().NotBeNull(); + } + + [Fact] + public void AddPositionstackGeocoding_WithNullOptions_ConfiguresAllServices() + { + // Arrange + var services = new ServiceCollection(); + + // Act + services.AddPositionstackGeocoding(); + + // Assert + var provider = services.BuildServiceProvider(); + + var options = provider.GetRequiredService>>(); + options.Should().NotBeNull(); + options.Value.Key.Should().Be(string.Empty); + provider.GetRequiredService().Should().NotBeNull(); + } + + [Fact] + public void AddPositionstackGeocoding_WithClientConfiguration_ConfiguresHttpClientAllServices() + { + // Arrange + var services = new ServiceCollection(); + + // Act + var builder = services.AddPositionstackGeocoding(); + builder.AddKey("abc"); + builder.HttpClientBuilder.ConfigureHttpClient(httpClient => httpClient.Timeout = TimeSpan.FromSeconds(5)); + + // Assert + var provider = services.BuildServiceProvider(); + var client = provider.GetRequiredService().CreateClient("IPositionstackGeocoding"); + client.Timeout.Should().Be(TimeSpan.FromSeconds(5)); + } + } +} diff --git a/test/Geo.Positionstack.Tests/Geo.Positionstack.Tests.csproj b/test/Geo.Positionstack.Tests/Geo.Positionstack.Tests.csproj new file mode 100644 index 0000000..0c0308c --- /dev/null +++ b/test/Geo.Positionstack.Tests/Geo.Positionstack.Tests.csproj @@ -0,0 +1,15 @@ + + + + net48;netcoreapp3.1;net6.0;net8.0 + + false + + + + + + + + + diff --git a/test/Geo.Positionstack.Tests/GlobalSuppressions.cs b/test/Geo.Positionstack.Tests/GlobalSuppressions.cs new file mode 100644 index 0000000..c4a79d9 --- /dev/null +++ b/test/Geo.Positionstack.Tests/GlobalSuppressions.cs @@ -0,0 +1,13 @@ +// +// Copyright (c) Geo.NET. +// Licensed under the MIT license. See the LICENSE file in the solution root for full license information. +// + +// This file is used by Code Analysis to maintain SuppressMessage +// attributes that are applied to this project. +// Project-level suppressions either have no target or are given +// a specific target and scoped to a namespace, type, member, etc. +using System.Diagnostics.CodeAnalysis; + +[assembly: SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "Using Microsoft recommended unit test naming", Scope = "namespaceanddescendants", Target = "~N:Geo.Positionstack.Tests")] +[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "The name of the test should explain the test", Scope = "namespaceanddescendants", Target = "~N:Geo.Positionstack.Tests")] diff --git a/test/Geo.Positionstack.Tests/Services/PositionstackGeocodingShould.cs b/test/Geo.Positionstack.Tests/Services/PositionstackGeocodingShould.cs new file mode 100644 index 0000000..fd302b2 --- /dev/null +++ b/test/Geo.Positionstack.Tests/Services/PositionstackGeocodingShould.cs @@ -0,0 +1,479 @@ +// +// Copyright (c) Geo.NET. +// Licensed under the MIT license. See the LICENSE file in the solution root for full license information. +// + +namespace Geo.Positionstack.Tests.Services +{ + using System; + using System.Collections.Generic; + using System.Globalization; + using System.Net; + using System.Net.Http; + using System.Threading; + using System.Threading.Tasks; + using System.Web; + using FluentAssertions; + using Geo.Core; + using Geo.Core.Models.Exceptions; + using Geo.Positionstack.Models.Parameters; + using Geo.Positionstack.Services; + using Microsoft.Extensions.Localization; + using Microsoft.Extensions.Options; + using Moq; + using Moq.Protected; + using Xunit; + + /// + /// Unit tests for the class. + /// + public class PositionstackGeocodingShould : IDisposable + { + private readonly HttpClient _httpClient; + private readonly Mock>> _options = new Mock>>(); + private readonly List _responseMessages = new List(); + private bool _disposed; + + /// + /// Initializes a new instance of the class. + /// + public PositionstackGeocodingShould() + { + _options + .Setup(x => x.Value) + .Returns(new KeyOptions() + { + Key = "abc123", + }); + + var mockHandler = new Mock(); + + _responseMessages.Add(new HttpResponseMessage() + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent( + "{\"meta\":{\"code\":200},\"addresses\":[{\"latitude\":47.576575,\"longitude\":16.430711,\"geometry\":{\"type\":\"Point\",\"coordinates\":[16.430711,47.576575]},\"country\":\"Austria\",\"countryCode\":\"AT\",\"countryFlag\":\"🇦🇹\",\"county\":\"Oberpullendorf\",\"distance\":6962472,\"confidence\":\"exact\",\"city\":\"Weppersdorf\",\"number\":\"123\",\"postalCode\":\"7331\",\"stateCode\":\"BU\",\"state\":\"Burgenland\",\"street\":\"Hauptstraße\",\"layer\":\"address\",\"formattedAddress\":\"123 Hauptstraße, BU 7331 AUT\",\"addressLabel\":\"Hauptstraße 123\"}]}"), + }); + + // For reverse geocoding, use the places endpoint type + mockHandler + .Protected() + .Setup>( + "SendAsync", + ItExpr.Is(x => x.RequestUri.PathAndQuery.Contains("geocode/forward")), + ItExpr.IsAny()) + .ReturnsAsync(_responseMessages[_responseMessages.Count - 1]); + + _responseMessages.Add(new HttpResponseMessage() + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent( + "{\"meta\":{\"code\":200},\"addresses\":[{\"latitude\":47.576174,\"longitude\":16.43042,\"geometry\":{\"type\":\"Point\",\"coordinates\":[16.43042,47.576174]},\"country\":\"Austria\",\"countryCode\":\"AT\",\"countryFlag\":\"🇦🇹\",\"county\":\"Oberpullendorf\",\"distance\":50,\"city\":\"Weppersdorf\",\"number\":\"123\",\"postalCode\":\"7331\",\"stateCode\":\"BU\",\"state\":\"Burgenland\",\"street\":\"Hauptstraße\",\"layer\":\"address\",\"formattedAddress\":\"123 Hauptstraße, BU 7331 AUT\",\"addressLabel\":\"Hauptstraße 123\"}]}"), + }); + + // For reverse geocoding, use the permanent endpoint type + mockHandler + .Protected() + .Setup>( + "SendAsync", + ItExpr.Is(x => x.RequestUri.PathAndQuery.Contains("geocode/reverse")), + ItExpr.IsAny()) + .ReturnsAsync(_responseMessages[_responseMessages.Count - 1]); + + _responseMessages.Add(new HttpResponseMessage() + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent( + "{\"meta\":{\"code\":200},\"addresses\":[{\"latitude\":43.76866,\"longitude\":-79.40616,\"geometry\":{\"type\":\"Point\",\"coordinates\":[-79.40616,43.76866]},\"country\":\"Canada\",\"countryCode\":\"CA\",\"countryFlag\":\"🇨🇦\",\"county\":\"Toronto\",\"distance\":381,\"borough\":\"North York\",\"city\":\"Toronto\",\"number\":\"123\",\"neighborhood\":\"Willowdale East\",\"postalCode\":\"M2N 3N8\",\"stateCode\":\"ON\",\"state\":\"Ontario\",\"street\":\"Hillcrest Ave\",\"layer\":\"address\",\"formattedAddress\":\"123 Hillcrest Ave, North York, Toronto, ON M2N 3N8 CAN\",\"addressLabel\":\"123 Hillcrest Ave\"},{\"latitude\":43.7678,\"longitude\":-79.40562,\"geometry\":{\"type\":\"Point\",\"coordinates\":[-79.40562,43.7678]},\"country\":\"Canada\",\"countryCode\":\"CA\",\"countryFlag\":\"🇨🇦\",\"county\":\"Toronto\",\"distance\":436,\"borough\":\"North York\",\"city\":\"Toronto\",\"number\":\"123\",\"neighborhood\":\"Willowdale East\",\"postalCode\":\"M2N 3M1\",\"stateCode\":\"ON\",\"state\":\"Ontario\",\"street\":\"Elmwood Ave\",\"layer\":\"address\",\"formattedAddress\":\"123 Elmwood Ave, North York, Toronto, ON M2N 3M1 CAN\",\"addressLabel\":\"123 Elmwood Ave\"},{\"latitude\":43.764905,\"longitude\":-79.408365,\"geometry\":{\"type\":\"Point\",\"coordinates\":[-79.408365,43.764905]},\"country\":\"Canada\",\"countryCode\":\"CA\",\"countryFlag\":\"🇨🇦\",\"county\":\"Toronto\",\"distance\":469,\"borough\":\"North York\",\"city\":\"Toronto\",\"number\":\"123\",\"neighborhood\":\"Willowdale East\",\"postalCode\":\"M2N 4T2\",\"stateCode\":\"ON\",\"state\":\"Ontario\",\"street\":\"Doris Avenue\",\"layer\":\"address\",\"formattedAddress\":\"123 Doris Avenue, North York, Toronto, ON M2N 4T2 CAN\",\"addressLabel\":\"123 Doris Avenue\"},{\"latitude\":43.766997,\"longitude\":-79.405112,\"geometry\":{\"type\":\"Point\",\"coordinates\":[-79.405112,43.766997]},\"country\":\"Canada\",\"countryCode\":\"CA\",\"countryFlag\":\"🇨🇦\",\"county\":\"Toronto\",\"distance\":502,\"borough\":\"North York\",\"city\":\"Toronto\",\"number\":\"123\",\"neighborhood\":\"Willowdale East\",\"postalCode\":\"M2N 3K2\",\"stateCode\":\"ON\",\"state\":\"Ontario\",\"street\":\"Hollywood Avenue\",\"layer\":\"address\",\"formattedAddress\":\"123 Hollywood Avenue, North York, Toronto, ON M2N 3K2 CAN\",\"addressLabel\":\"123 Hollywood Avenue\"},{\"latitude\":43.773507,\"longitude\":-79.405844,\"geometry\":{\"type\":\"Point\",\"coordinates\":[-79.405844,43.773507]},\"country\":\"Canada\",\"countryCode\":\"CA\",\"countryFlag\":\"🇨🇦\",\"county\":\"Toronto\",\"distance\":672,\"borough\":\"North York\",\"city\":\"Toronto\",\"number\":\"123\",\"neighborhood\":\"Willowdale East\",\"postalCode\":\"M2N 4A7\",\"stateCode\":\"ON\",\"state\":\"Ontario\",\"street\":\"Norton Avenue\",\"layer\":\"address\",\"formattedAddress\":\"123 Norton Avenue, North York, Toronto, ON M2N 4A7 CAN\",\"addressLabel\":\"123 Norton Avenue\"},{\"latitude\":43.76654,\"longitude\":-79.40308,\"geometry\":{\"type\":\"Point\",\"coordinates\":[-79.40308,43.76654]},\"country\":\"Canada\",\"countryCode\":\"CA\",\"countryFlag\":\"🇨🇦\",\"county\":\"Toronto\",\"distance\":673,\"borough\":\"North York\",\"city\":\"Toronto\",\"number\":\"123\",\"neighborhood\":\"Willowdale East\",\"postalCode\":\"M2N 3J1\",\"stateCode\":\"ON\",\"state\":\"Ontario\",\"street\":\"Alfred Ave\",\"layer\":\"address\",\"formattedAddress\":\"123 Alfred Ave, North York, Toronto, ON M2N 3J1 CAN\",\"addressLabel\":\"123 Alfred Ave\"},{\"latitude\":43.7752,\"longitude\":-79.41155,\"geometry\":{\"type\":\"Point\",\"coordinates\":[-79.41155,43.7752]},\"country\":\"Canada\",\"countryCode\":\"CA\",\"countryFlag\":\"🇨🇦\",\"county\":\"Toronto\",\"distance\":725,\"borough\":\"North York\",\"city\":\"Toronto\",\"number\":\"123\",\"neighborhood\":\"Willowdale East\",\"postalCode\":\"M2N 6V2\",\"stateCode\":\"ON\",\"state\":\"Ontario\",\"street\":\"Grandview Way\",\"layer\":\"address\",\"formattedAddress\":\"123 Grandview Way, North York, Toronto, ON M2N 6V2 CAN\",\"addressLabel\":\"123 Grandview Way\"},{\"latitude\":43.7753,\"longitude\":-79.40628,\"geometry\":{\"type\":\"Point\",\"coordinates\":[-79.40628,43.7753]},\"country\":\"Canada\",\"countryCode\":\"CA\",\"countryFlag\":\"🇨🇦\",\"county\":\"Toronto\",\"distance\":823,\"borough\":\"North York\",\"city\":\"Toronto\",\"number\":\"123\",\"neighborhood\":\"Willowdale East\",\"postalCode\":\"M2N 4G3\",\"stateCode\":\"ON\",\"state\":\"Ontario\",\"street\":\"Church Ave\",\"layer\":\"address\",\"formattedAddress\":\"123 Church Ave, North York, Toronto, ON M2N 4G3 CAN\",\"addressLabel\":\"123 Church Ave\"},{\"latitude\":43.76244,\"longitude\":-79.41773,\"geometry\":{\"type\":\"Point\",\"coordinates\":[-79.41773,43.76244]},\"country\":\"Canada\",\"countryCode\":\"CA\",\"countryFlag\":\"🇨🇦\",\"county\":\"Toronto\",\"distance\":887,\"borough\":\"North York\",\"city\":\"Toronto\",\"number\":\"123\",\"neighborhood\":\"Lansing-Westgate\",\"postalCode\":\"M2N 1S8\",\"stateCode\":\"ON\",\"state\":\"Ontario\",\"street\":\"Burndale Ave\",\"layer\":\"address\",\"formattedAddress\":\"123 Burndale Ave, North York, Toronto, ON M2N 1S8 CAN\",\"addressLabel\":\"123 Burndale Ave\"},{\"latitude\":43.773987,\"longitude\":-79.420541,\"geometry\":{\"type\":\"Point\",\"coordinates\":[-79.420541,43.773987]},\"country\":\"Canada\",\"countryCode\":\"CA\",\"countryFlag\":\"🇨🇦\",\"county\":\"Toronto\",\"distance\":973,\"borough\":\"North York\",\"city\":\"Toronto\",\"number\":\"123\",\"neighborhood\":\"Willowdale West\",\"postalCode\":\"M2N 2B1\",\"stateCode\":\"ON\",\"state\":\"Ontario\",\"street\":\"Hounslow Avenue\",\"layer\":\"address\",\"formattedAddress\":\"123 Hounslow Avenue, North York, Toronto, ON M2N 2B1 CAN\",\"addressLabel\":\"123 Hounslow Avenue\"}]}"), + }); + + // For reverse geocoding, use the places endpoint type + mockHandler + .Protected() + .Setup>( + "SendAsync", + ItExpr.Is(x => x.RequestUri.PathAndQuery.Contains("search/autocomplete")), + ItExpr.IsAny()) + .ReturnsAsync(_responseMessages[_responseMessages.Count - 1]); + + var options = Options.Create(new LocalizationOptions { ResourcesPath = "Resources" }); + _httpClient = new HttpClient(mockHandler.Object); + } + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + [Fact] + public void AddPositionstackKey_WithOptions_SuccessfullyAddsKey() + { + var sut = BuildService(); + + sut.AddPositionstackKey(new GeocodingParameters()); + + _httpClient.DefaultRequestHeaders.Authorization.Scheme.Should().Be("abc123"); + } + + [Fact] + public void AddHereKey_WithParameterOverride_SuccessfullyAddsKey() + { + var sut = BuildService(); + + sut.AddPositionstackKey(new GeocodingParameters() { Key = "123abc" }); + + _httpClient.DefaultRequestHeaders.Authorization.Scheme.Should().Be("123abc"); + } + + [Fact] + public void AddCountry_WithValidCountries_AddsSuccessfully() + { + var sut = BuildService(); + + var query = QueryString.Empty; + var parameters = new GeocodingParameters(); + + parameters.Countries.Add("CA"); + parameters.Countries.Add("FR"); + + sut.AddCountry(parameters, ref query); + + var queryParameters = HttpUtility.ParseQueryString(query.ToString()); + queryParameters.Count.Should().Be(1); + queryParameters["country"].Should().Be("CA,FR"); + } + + [Fact] + public void AddLayers_WithValidLayers_AddsSuccessfully() + { + var sut = BuildService(); + + var query = QueryString.Empty; + var parameters = new GeocodingParameters(); + + parameters.Layers.Add(Layer.PostalCode); + parameters.Layers.Add(Layer.Country); + parameters.Layers.Add(Layer.Coarse); + + sut.AddLayers(parameters, ref query); + + var queryParameters = HttpUtility.ParseQueryString(query.ToString()); + queryParameters.Count.Should().Be(1); + queryParameters["layers"].Should().Be("postalCode,country,coarse"); + } + + [Theory] + [ClassData(typeof(CultureTestData))] + public void BuildGeocodingRequest_WithValidParameters_SuccessfullyBuildsUrl(CultureInfo culture) + { + // Arrange + var oldCulture = Thread.CurrentThread.CurrentCulture; + Thread.CurrentThread.CurrentCulture = culture; + + var sut = BuildService(); + + var parameters = new GeocodingParameters() + { + Query = "123 East", + Key = "123abc", + }; + + parameters.Layers.Add(Layer.PostalCode); + parameters.Layers.Add(Layer.Country); + + parameters.Countries.Add("CA"); + + // Act + var uri = sut.BuildGeocodingRequest(parameters); + + // Assert + var query = HttpUtility.UrlDecode(uri.PathAndQuery); + query.Should().Contain("query=123 East"); + query.Should().Contain("country=CA"); + query.Should().Contain("layers=postalCode,country"); + + _httpClient.DefaultRequestHeaders.Authorization.Scheme.Should().Be("123abc"); + + Thread.CurrentThread.CurrentCulture = oldCulture; + } + + [Fact] + public void BuildGeocodingRequest_WithCharacterNeedingEncoding_SuccessfullyBuildsAnEncodedUrl() + { + var sut = BuildService(); + + var parameters = new GeocodingParameters() + { + Query = "123 East #425", + }; + + var uri = sut.BuildGeocodingRequest(parameters); + var query = HttpUtility.UrlDecode(uri.PathAndQuery); + query.Should().Contain("query=123 East #425"); + uri.PathAndQuery.Should().Contain("query=123%20East%20%23425"); + } + + [Fact] + public void BuildGeocodingRequest_WithInvalidParameters_FailsWithException() + { + var sut = BuildService(); + + Action act = () => sut.BuildGeocodingRequest(new GeocodingParameters()); + + act.Should() + .Throw() +#if NETCOREAPP3_1_OR_GREATER + .WithMessage("*(Parameter 'Query')"); +#else + .WithMessage("*Parameter name: Query"); +#endif + } + + [Theory] + [ClassData(typeof(CultureTestData))] + public void BuildReverseGeocodingRequest_WithValidParameters_SuccessfullyBuildsUrl(CultureInfo culture) + { + // Arrange + var oldCulture = Thread.CurrentThread.CurrentCulture; + Thread.CurrentThread.CurrentCulture = culture; + + var sut = BuildService(); + + var parameters = new ReverseGeocodingParameters() + { + Coordinate = new Coordinate() + { + Latitude = 56.78, + Longitude = 78.91, + }, + Key = "123abc", + }; + + parameters.Layers.Add(Layer.Locality); + parameters.Layers.Add(Layer.Fine); + + // Act + var uri = sut.BuildReverseGeocodingRequest(parameters); + + // Assert + var query = HttpUtility.UrlDecode(uri.PathAndQuery); + query.Should().Contain("coordinates=56.78,78.91"); + query.Should().Contain("layers=locality,fine"); + + _httpClient.DefaultRequestHeaders.Authorization.Scheme.Should().Be("123abc"); + + Thread.CurrentThread.CurrentCulture = oldCulture; + } + + [Fact] + public void BuildReverseGeocodingRequest_WithInvalidParameters_FailsWithException() + { + var sut = BuildService(); + + Action act = () => sut.BuildReverseGeocodingRequest(new ReverseGeocodingParameters()); + + act.Should() + .Throw() +#if NETCOREAPP3_1_OR_GREATER + .WithMessage("*(Parameter 'Coordinate')"); +#else + .WithMessage("*Parameter name: Coordinate"); +#endif + } + + [Theory] + [ClassData(typeof(CultureTestData))] + public void BuildAutocompleteRequest_WithValidParameters_SuccessfullyBuildsUrl(CultureInfo culture) + { + // Arrange + var oldCulture = Thread.CurrentThread.CurrentCulture; + Thread.CurrentThread.CurrentCulture = culture; + + var sut = BuildService(); + + var parameters = new AutocompleteParameters() + { + Query = "123 East", + Near = new Coordinate() + { + Latitude = 56.78, + Longitude = 78.91, + }, + Limit = 14, + Mailable = true, + Key = "123abc", + }; + + parameters.Layers.Add(Layer.PostalCode); + parameters.Layers.Add(Layer.Country); + + parameters.Countries.Add("CA"); + + // Act + var uri = sut.BuildAutocompleteRequest(parameters); + + // Assert + var query = HttpUtility.UrlDecode(uri.PathAndQuery); + query.Should().Contain("query=123 East"); + query.Should().Contain("near=56.78,78.91"); + query.Should().Contain("limit=14"); + query.Should().Contain("mailable=true"); + query.Should().Contain("country=CA"); + query.Should().Contain("layers=postalCode,country"); + + _httpClient.DefaultRequestHeaders.Authorization.Scheme.Should().Be("123abc"); + + Thread.CurrentThread.CurrentCulture = oldCulture; + } + + [Fact] + public void BuildAutocompleteRequest_WithInvalidParameters_FailsWithException() + { + var sut = BuildService(); + + Action act = () => sut.BuildAutocompleteRequest(new AutocompleteParameters()); + + act.Should() + .Throw() +#if NETCOREAPP3_1_OR_GREATER + .WithMessage("*(Parameter 'Query')"); +#else + .WithMessage("*Parameter name: Query"); +#endif + } + + [Fact] + public void ValidateAndBuildUri_WithValidParameters_SuccessfullyBuildsUri() + { + var sut = BuildService(); + + var parameters = new ReverseGeocodingParameters() + { + Coordinate = new Coordinate() + { + Latitude = 56.78, + Longitude = 78.91, + }, + Key = "123abc", + }; + + parameters.Layers.Add(Layer.Locality); + parameters.Layers.Add(Layer.Fine); + + var uri = sut.ValidateAndBuildUri(parameters, sut.BuildReverseGeocodingRequest); + var query = HttpUtility.UrlDecode(uri.PathAndQuery); + query.Should().Contain("coordinates=56.78,78.91"); + query.Should().Contain("layers=locality,fine"); + + _httpClient.DefaultRequestHeaders.Authorization.Scheme.Should().Be("123abc"); + } + + [Fact] + public void ValidateAndBuildUri_WithNullParameters_FailsWithException() + { + var sut = BuildService(); + + Action act = () => sut.ValidateAndBuildUri(null, sut.BuildReverseGeocodingRequest); + + act.Should() + .Throw() + .WithMessage("*See the inner exception for more information.") + .WithInnerException(); + } + + [Fact] + public void ValidateAndBuildUri_WithInvalidParameters_FailsWithException() + { + var sut = BuildService(); + + Action act = () => sut.ValidateAndBuildUri(new ReverseGeocodingParameters(), sut.BuildReverseGeocodingRequest); + + act.Should() + .Throw() + .WithMessage("*See the inner exception for more information.") + .WithInnerException() +#if NETCOREAPP3_1_OR_GREATER + .WithMessage("*(Parameter 'Coordinate')"); +#else + .WithMessage("*Parameter name: Coordinate"); +#endif + } + + [Fact] + public async Task GeocodingAsync_WithValidParameters_ReturnsSuccessfully() + { + var sut = BuildService(); + + var parameters = new GeocodingParameters() + { + Query = "123 East", + }; + + var result = await sut.GeocodingAsync(parameters); + result.Addresses.Count.Should().Be(1); + } + + [Fact] + public async Task ReverseGeocodingAsync_WithValidParameters_ReturnsSuccessfully() + { + var sut = BuildService(); + + var parameters = new ReverseGeocodingParameters() + { + Coordinate = new Coordinate() + { + Latitude = 56.78, + Longitude = 78.91, + }, + }; + + var result = await sut.ReverseGeocodingAsync(parameters); + result.Addresses.Count.Should().Be(1); + } + + [Fact] + public async Task AutocompleteAsync_WithValidParameters_ReturnsSuccessfully() + { + var sut = BuildService(); + + var parameters = new AutocompleteParameters() + { + Query = "123 East", + }; + + var result = await sut.AutocompleteAsync(parameters); + result.Addresses.Count.Should().Be(10); + } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + /// A boolean flag indicating whether or not to dispose of objects. + protected virtual void Dispose(bool disposing) + { + if (_disposed) + { + return; + } + + if (disposing) + { + _httpClient?.Dispose(); + + foreach (var message in _responseMessages) + { + message?.Dispose(); + } + } + + _disposed = true; + } + + private PositionstackGeocoding BuildService() + { + return new PositionstackGeocoding(_httpClient, _options.Object); + } + } +} \ No newline at end of file diff --git a/test/Geo.Positionstack.Tests/TestData/CultureTestData.cs b/test/Geo.Positionstack.Tests/TestData/CultureTestData.cs new file mode 100644 index 0000000..4c4fd2a --- /dev/null +++ b/test/Geo.Positionstack.Tests/TestData/CultureTestData.cs @@ -0,0 +1,36 @@ +// +// Copyright (c) Geo.NET. +// Licensed under the MIT license. See the LICENSE file in the solution root for full license information. +// + +namespace Geo.Positionstack.Tests +{ + using System.Collections; + using System.Collections.Generic; + using System.Globalization; + + /// + /// Test data when testing different cultures. This test data returns all cultures in dotnet. + /// + public class CultureTestData : IEnumerable + { + /// + /// Gets the enumerator for the test data. + /// + /// An of []. + public IEnumerator GetEnumerator() + { + var cultures = CultureInfo.GetCultures(CultureTypes.AllCultures); + foreach (var culture in cultures) + { + yield return new object[] { culture }; + } + } + + /// + /// Gets the enumerator for the test data. + /// + /// An . + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } +} From b8384707d533706a467c95406c3e011d949c2c95 Mon Sep 17 00:00:00 2001 From: JustinCanton <67930245+JustinCanton@users.noreply.github.com> Date: Mon, 20 May 2024 11:52:49 -0400 Subject: [PATCH 2/4] feat(positionstack): adding the response classes for the api responses --- .../Abstractions/IPositionstackGeocoding.cs | 8 +- .../Models/ILocationGeocodeParameters.cs | 2 +- .../Models/Parameters/GeocodingParameters.cs | 2 +- .../Parameters/ReverseGeocodingParameters.cs | 2 +- .../Models/Responses/Address.cs | 115 ++++++++++-------- .../Models/Responses/BoundingBoxModule.cs | 39 ++++++ .../Models/Responses/CountryModule.cs | 88 ++++++++++++++ .../Models/Responses/Currency.cs | 45 +++++++ .../Responses/{GeocodeAddress.cs => Data.cs} | 13 +- .../Models/Responses/Geometry.cs | 28 ----- .../Models/Responses/GlobalInformation.cs | 75 ++++++++++++ .../Models/Responses/Language.cs | 27 ++++ .../Models/Responses/Meta.cs | 18 --- .../Models/Responses/PhoneInformation.cs | 33 +++++ .../Models/Responses/Response.cs | 16 +-- .../Models/Responses/ReverseGeocodeAddress.cs | 21 ---- .../Models/Responses/SunInformation.cs | 39 ++++++ .../Models/Responses/SunModule.cs | 33 +++++ .../Models/Responses/TimezoneModule.cs | 33 +++++ .../Services/PositionstackGeocoding.cs | 12 +- 20 files changed, 502 insertions(+), 147 deletions(-) create mode 100644 src/Geo.Positionstack/Models/Responses/BoundingBoxModule.cs create mode 100644 src/Geo.Positionstack/Models/Responses/CountryModule.cs create mode 100644 src/Geo.Positionstack/Models/Responses/Currency.cs rename src/Geo.Positionstack/Models/Responses/{GeocodeAddress.cs => Data.cs} (52%) delete mode 100644 src/Geo.Positionstack/Models/Responses/Geometry.cs create mode 100644 src/Geo.Positionstack/Models/Responses/GlobalInformation.cs create mode 100644 src/Geo.Positionstack/Models/Responses/Language.cs delete mode 100644 src/Geo.Positionstack/Models/Responses/Meta.cs create mode 100644 src/Geo.Positionstack/Models/Responses/PhoneInformation.cs delete mode 100644 src/Geo.Positionstack/Models/Responses/ReverseGeocodeAddress.cs create mode 100644 src/Geo.Positionstack/Models/Responses/SunInformation.cs create mode 100644 src/Geo.Positionstack/Models/Responses/SunModule.cs create mode 100644 src/Geo.Positionstack/Models/Responses/TimezoneModule.cs diff --git a/src/Geo.Positionstack/Abstractions/IPositionstackGeocoding.cs b/src/Geo.Positionstack/Abstractions/IPositionstackGeocoding.cs index 8f1488f..14a7efd 100644 --- a/src/Geo.Positionstack/Abstractions/IPositionstackGeocoding.cs +++ b/src/Geo.Positionstack/Abstractions/IPositionstackGeocoding.cs @@ -21,17 +21,17 @@ public interface IPositionstackGeocoding /// /// A with the parameters of the request. /// A used to cancel the request. - /// A of with the response from Positionstack. + /// A with the response from Positionstack. /// Thrown for multiple different reasons. Check the inner exception for more information. - Task> GeocodingAsync(GeocodingParameters parameters, CancellationToken cancellationToken = default); + Task GeocodingAsync(GeocodingParameters parameters, CancellationToken cancellationToken = default); /// /// Calls the Positionstack reverse geocoding API and returns the results. /// /// A with the parameters of the request. /// A used to cancel the request. - /// A of with the response from Positionstack. + /// A with the response from Positionstack. /// Thrown for multiple different reasons. Check the inner exception for more information. - Task> ReverseGeocodingAsync(ReverseGeocodingParameters parameters, CancellationToken cancellationToken = default); + Task ReverseGeocodingAsync(ReverseGeocodingParameters parameters, CancellationToken cancellationToken = default); } } diff --git a/src/Geo.Positionstack/Abstractions/Models/ILocationGeocodeParameters.cs b/src/Geo.Positionstack/Abstractions/Models/ILocationGeocodeParameters.cs index 83bf49d..2a1c371 100644 --- a/src/Geo.Positionstack/Abstractions/Models/ILocationGeocodeParameters.cs +++ b/src/Geo.Positionstack/Abstractions/Models/ILocationGeocodeParameters.cs @@ -16,7 +16,7 @@ public interface ILocationGeocodeParameters /// Gets one or more 2-letter(e.g.AU) or 3-letter country codes(e.g.AUS) to filter the geocoding results. /// Optional. /// - IList Country { get; } + IList Countries { get; } /// /// Gets or sets a filter for the geocoding results specifying a region. This could be a neighbourhood, district, city, county, state or administrative area. Example: region= Berlin to filter by locations in Berlin. diff --git a/src/Geo.Positionstack/Models/Parameters/GeocodingParameters.cs b/src/Geo.Positionstack/Models/Parameters/GeocodingParameters.cs index 99e1cdb..4ee2df7 100644 --- a/src/Geo.Positionstack/Models/Parameters/GeocodingParameters.cs +++ b/src/Geo.Positionstack/Models/Parameters/GeocodingParameters.cs @@ -19,7 +19,7 @@ public class GeocodingParameters : ILocationGeocodeParameters, IFilterGeocodePar public string Query { get; set; } /// - public IList Country { get; } = new List(); + public IList Countries { get; } = new List(); /// public string Region { get; set; } diff --git a/src/Geo.Positionstack/Models/Parameters/ReverseGeocodingParameters.cs b/src/Geo.Positionstack/Models/Parameters/ReverseGeocodingParameters.cs index 79986a5..8229f8b 100644 --- a/src/Geo.Positionstack/Models/Parameters/ReverseGeocodingParameters.cs +++ b/src/Geo.Positionstack/Models/Parameters/ReverseGeocodingParameters.cs @@ -19,7 +19,7 @@ public class ReverseGeocodingParameters : ILocationGeocodeParameters, IFilterGeo public Coordinate Coordinate { get; set; } /// - public IList Country { get; } = new List(); + public IList Countries { get; } = new List(); /// public string Region { get; set; } diff --git a/src/Geo.Positionstack/Models/Responses/Address.cs b/src/Geo.Positionstack/Models/Responses/Address.cs index 03e055e..1b488ff 100644 --- a/src/Geo.Positionstack/Models/Responses/Address.cs +++ b/src/Geo.Positionstack/Models/Responses/Address.cs @@ -13,112 +13,129 @@ namespace Geo.Positionstack.Models.Responses public class Address { /// - /// Gets or sets the latitude of the address. For example: "52.19404". + /// Gets or sets the latitude coordinate associated with the location result. /// [JsonPropertyName("latitude")] public double Latitude { get; set; } /// - /// Gets or sets the longitude of the address. For example: "8.80135". + /// Gets or sets the longitude coordinate associated with the location result. /// [JsonPropertyName("longitude")] public double Longitude { get; set; } /// - /// Gets or sets the geometry information for the address. + /// Gets or sets the formatted place name or address. /// - [JsonPropertyName("geometry")] - public Geometry Geometry { get; set; } + [JsonPropertyName("label")] + public string Label { get; set; } /// - /// Gets or sets the localised country name. For example: "United States". + /// Gets or sets the name of the location result. This could be a place name, address, postal code, and more. /// - [JsonPropertyName("country")] - public string Country { get; set; } + [JsonPropertyName("name")] + public string Name { get; set; } /// - /// Gets or sets a two-letter country code. For example: "US". + /// Gets or sets the type of location result. /// - [JsonPropertyName("countryCode")] - public string CountryCode { get; set; } + [JsonPropertyName("type")] + public string Type { get; set; } + + /// + /// Gets or sets the distance (in meters) between the location result and the coordinates specified in the query parameter. Only applicable for reverse geocoding. + /// + [JsonPropertyName("distance")] + public uint? Distance { get; set; } /// - /// Gets or sets the country flag. + /// Gets or sets the street or house number associated with the location result. /// - [JsonPropertyName("countryFlag")] - public string CountryFlag { get; set; } + [JsonPropertyName("number")] + public string Number { get; set; } /// - /// Gets or sets the county information for an address. + /// Gets or sets the street name associated with the location result. /// - [JsonPropertyName("county")] - public string County { get; set; } + [JsonPropertyName("street")] + public string Street { get; set; } /// - /// Gets or sets the borough information for an address. + /// Gets or sets the postal or ZIP code associated with the location result. /// - [JsonPropertyName("borough")] - public string Borough { get; set; } + [JsonPropertyName("postal_code")] + public string PostalCode { get; set; } /// - /// Gets or sets the name of the primary locality of the place. For example: "Bad Oyenhausen". + /// Gets or sets a confidence score between 0 (0% confidence) and 1 (100% confidence) associated with the location result. /// - [JsonPropertyName("city")] - public string City { get; set; } + [JsonPropertyName("confidence")] + public double? Confidence { get; set; } /// - /// Gets or sets the number. For example: "32547". + /// Gets or sets the name of the region associated with the location result. /// - [JsonPropertyName("number")] - public string Number { get; set; } + [JsonPropertyName("region")] + public string Region { get; set; } /// - /// Gets or sets the name of neighbourhood of the address. + /// Gets or sets the region code associated with the location result. + /// + [JsonPropertyName("region_code")] + public string RegionCode { get; set; } + + /// + /// Gets or sets the name of the administrative area associated with the location result. + /// + [JsonPropertyName("administrative_area")] + public string AdministrativeArea { get; set; } + + /// + /// Gets or sets the name of the neighbourhood associated with the location result. /// [JsonPropertyName("neighborhood")] public string Neighborhood { get; set; } /// - /// Gets or sets an alphanumeric string included in a postal address to facilitate mail sorting, such as post code, postcode, or ZIP code. - /// For example: "32547". + /// Gets or sets the localised country name. For example: "United States". /// - [JsonPropertyName("postalCode")] - public string PostalCode { get; set; } + [JsonPropertyName("country")] + public string Country { get; set; } /// - /// Gets or sets the state code for the state. For example: "NY". + /// Gets or sets the ISO 3166 Alpha 2 (two letters) code of the country associated with the location result. /// - [JsonPropertyName("stateCode")] - public string StateCode { get; set; } + [JsonPropertyName("country_code")] + public string CountryCode { get; set; } /// - /// Gets or sets a code/abbreviation for the state division of a country. For example: "North Rhine-Westphalia". + /// Gets or sets an embeddable map associated with the location result. /// - [JsonPropertyName("state")] - public string State { get; set; } + [JsonPropertyName("map_url")] + public string MapUrl { get; set; } /// - /// Gets or sets the name of street. For example: "Schulstrasse". + /// Gets or sets an extensive set of country information. /// - [JsonPropertyName("street")] - public string Street { get; set; } + [JsonPropertyName("country_module")] + public CountryModule CountryModule { get; set; } /// - /// Gets or sets the layer information for the address. + /// Gets or sets astrology data for a location. /// - [JsonPropertyName("layer")] - public string Layer { get; set; } + [JsonPropertyName("sun_module")] + public SunModule SunModule { get; set; } /// - /// Gets or sets the formatted address. For example: "20 Jay St, Brooklyn, New York, NY 11201 USA". + /// Gets or sets timezone information for a location. /// - [JsonPropertyName("formattedAddress")] - public string FormattedAddress { get; set; } + [JsonPropertyName("timezone_module")] + public TimezoneModule TimezoneModule { get; set; } /// - /// Gets or sets the address label. For example: "20 Jay St". + /// Gets or sets bounding box coordinates for a location. /// - [JsonPropertyName("addressLabel")] - public string AddressLabel { get; set; } + [JsonPropertyName("bbox_module")] + public BoundingBoxModule BoundingBoxModule { get; set; } } } diff --git a/src/Geo.Positionstack/Models/Responses/BoundingBoxModule.cs b/src/Geo.Positionstack/Models/Responses/BoundingBoxModule.cs new file mode 100644 index 0000000..ff25362 --- /dev/null +++ b/src/Geo.Positionstack/Models/Responses/BoundingBoxModule.cs @@ -0,0 +1,39 @@ +// +// Copyright (c) Geo.NET. +// Licensed under the MIT license. See the LICENSE file in the solution root for full license information. +// + +namespace Geo.Positionstack.Models.Responses +{ + using System.Text.Json.Serialization; + + /// + /// Bounding box coordinates for a location. + /// + public class BoundingBoxModule + { + /// + /// Gets or sets the minimum longitude coordinate point associated with the location result. + /// + [JsonPropertyName("min_longitude")] + public double? MinimumLongitude { get; set; } + + /// + /// Gets or sets the minimum latitude coordinate point associated with the location result. + /// + [JsonPropertyName("min_latitude")] + public double? MinimumLatitude { get; set; } + + /// + /// Gets or sets the maximum longitude coordinate point associated with the location result. + /// + [JsonPropertyName("max_longitude")] + public double? MaximumLongitude { get; set; } + + /// + /// Gets or sets the maximum latitude coordinate point associated with the location result. + /// + [JsonPropertyName("max_latitude")] + public double? MaximumLatitude { get; set; } + } +} diff --git a/src/Geo.Positionstack/Models/Responses/CountryModule.cs b/src/Geo.Positionstack/Models/Responses/CountryModule.cs new file mode 100644 index 0000000..e090f76 --- /dev/null +++ b/src/Geo.Positionstack/Models/Responses/CountryModule.cs @@ -0,0 +1,88 @@ +// +// Copyright (c) Geo.NET. +// Licensed under the MIT license. See the LICENSE file in the solution root for full license information. +// + +namespace Geo.Positionstack.Models.Responses +{ + using System.Collections.Generic; + using System.Text.Json.Serialization; + + /// + /// An extensive set of country information. + /// + public class CountryModule + { + /// + /// Gets or sets the country's latitude coordinate. + /// + [JsonPropertyName("latitude")] + public double Latitude { get; set; } + + /// + /// Gets or sets the country's longitude coordinate. + /// + [JsonPropertyName("longitude")] + public double Longitude { get; set; } + + /// + /// Gets or sets the common name of the country associated with the location result. + /// + [JsonPropertyName("common_name")] + public string CommonName { get; set; } + + /// + /// Gets or sets the official name of the country associated with the location result. + /// + [JsonPropertyName("official_name")] + public string OfficialName { get; set; } + + /// + /// Gets or sets the capital of the country associated with the location result. + /// + [JsonPropertyName("capital")] + public string Capital { get; set; } + + /// + /// Gets or sets a flag icon of the country associated with the location result. + /// + [JsonPropertyName("flag")] + public string Flag { get; set; } + + /// + /// Gets or sets a value indicationg whether or not the country is landlocked. + /// + [JsonPropertyName("landlocked")] + public bool? Landlocked { get; set; } + + /// + /// Gets or sets a value indicationg whether or not the country is independent. + /// + [JsonPropertyName("independent")] + public bool? Independent { get; set; } + + /// + /// Gets or sets global information about a country. + /// + [JsonPropertyName("global")] + public GlobalInformation GlobalInformation { get; set; } + + /// + /// Gets or sets information about the phone for a country. + /// + [JsonPropertyName("dial")] + public PhoneInformation PhoneInformation { get; set; } + + /// + /// Gets the currency associated with the location result. + /// + [JsonPropertyName("currencies")] + public IList Currencies { get; } = new List(); + + /// + /// Gets the language of the country associated with the location result. + /// + [JsonPropertyName("languages")] + public IList Languages { get; } = new List(); + } +} diff --git a/src/Geo.Positionstack/Models/Responses/Currency.cs b/src/Geo.Positionstack/Models/Responses/Currency.cs new file mode 100644 index 0000000..0d9e6c0 --- /dev/null +++ b/src/Geo.Positionstack/Models/Responses/Currency.cs @@ -0,0 +1,45 @@ +// +// Copyright (c) Geo.NET. +// Licensed under the MIT license. See the LICENSE file in the solution root for full license information. +// + +namespace Geo.Positionstack.Models.Responses +{ + using System.Text.Json.Serialization; + + /// + /// Currency associated with the location result. + /// + public class Currency + { + /// + /// Gets or sets the currency symbol of the country associated with the location result. + /// + [JsonPropertyName("symbol")] + public string Symbol { get; set; } + + /// + /// Gets or sets the currency code of the country associated with the location result. + /// + [JsonPropertyName("code")] + public string Code { get; set; } + + /// + /// Gets or sets the currency name of the country associated with the location result. + /// + [JsonPropertyName("name")] + public string Name { get; set; } + + /// + /// Gets or sets the numeric currency code of the country associated with the location result. + /// + [JsonPropertyName("numeric")] + public uint? Numeric { get; set; } + + /// + /// Gets or sets the minor currency unit of the country associated with the location result. + /// + [JsonPropertyName("minor_unit")] + public uint? MinorUnit { get; set; } + } +} diff --git a/src/Geo.Positionstack/Models/Responses/GeocodeAddress.cs b/src/Geo.Positionstack/Models/Responses/Data.cs similarity index 52% rename from src/Geo.Positionstack/Models/Responses/GeocodeAddress.cs rename to src/Geo.Positionstack/Models/Responses/Data.cs index 727364b..eb180d3 100644 --- a/src/Geo.Positionstack/Models/Responses/GeocodeAddress.cs +++ b/src/Geo.Positionstack/Models/Responses/Data.cs @@ -1,21 +1,22 @@ -// +// // Copyright (c) Geo.NET. // Licensed under the MIT license. See the LICENSE file in the solution root for full license information. // namespace Geo.Positionstack.Models.Responses { + using System.Collections.Generic; using System.Text.Json.Serialization; /// - /// Address information for a geocode result. + /// The data with the address results. /// - public class GeocodeAddress : Address + public class Data { /// - /// Gets or sets the confidence of the result. + /// Gets the address results. /// - [JsonPropertyName("confidence")] - public string Confidence { get; set; } + [JsonPropertyName("results")] + public IList
Results { get; } = new List
(); } } diff --git a/src/Geo.Positionstack/Models/Responses/Geometry.cs b/src/Geo.Positionstack/Models/Responses/Geometry.cs deleted file mode 100644 index 56f2c30..0000000 --- a/src/Geo.Positionstack/Models/Responses/Geometry.cs +++ /dev/null @@ -1,28 +0,0 @@ -// -// Copyright (c) Geo.NET. -// Licensed under the MIT license. See the LICENSE file in the solution root for full license information. -// - -namespace Geo.Positionstack.Models.Responses -{ - using System.Text.Json.Serialization; - using Geo.Positionstack.Models.Parameters; - - /// - /// The geometry information for the address. - /// - public class Geometry - { - /// - /// Gets or sets the type of the coordinate. - /// - [JsonPropertyName("type")] - public string Type { get; set; } - - /// - /// Gets or sets the coordinates of the address. - /// - [JsonPropertyName("coordinates")] - public Coordinate Coordinate { get; set; } - } -} diff --git a/src/Geo.Positionstack/Models/Responses/GlobalInformation.cs b/src/Geo.Positionstack/Models/Responses/GlobalInformation.cs new file mode 100644 index 0000000..a257c94 --- /dev/null +++ b/src/Geo.Positionstack/Models/Responses/GlobalInformation.cs @@ -0,0 +1,75 @@ +// +// Copyright (c) Geo.NET. +// Licensed under the MIT license. See the LICENSE file in the solution root for full license information. +// + +namespace Geo.Positionstack.Models.Responses +{ + using System.Text.Json.Serialization; + + /// + /// Global information about a country. + /// + public class GlobalInformation + { + /// + /// Gets or sets the ISO 3166 Alpha 2 code of the country associated with the location result. + /// + [JsonPropertyName("alpha2")] + public string Alpha2 { get; set; } + + /// + /// Gets or sets the ISO 3166 Alpha 3 code of the country associated with the location result. + /// + [JsonPropertyName("alpha3")] + public string Alpha3 { get; set; } + + /// + /// Gets or sets the numeric ISO 3166 code of the country associated with the location result. + /// + [JsonPropertyName("numeric_code")] + public string NumericCode { get; set; } + + /// + /// Gets or sets the country's region name. Example: Americas. + /// + [JsonPropertyName("region")] + public string Region { get; set; } + + /// + /// Gets or sets the country's sub-region name. Example: Northern America. + /// + [JsonPropertyName("subregion")] + public string Subregion { get; set; } + + /// + /// Gets or sets the country's world region name. Example: AMER. + /// + [JsonPropertyName("world_region")] + public string WorldRegion { get; set; } + + /// + /// Gets or sets the country's region name. Example: Americas. + /// + [JsonPropertyName("region_code")] + public string RegionCode { get; set; } + + /// + /// Gets or sets the country's sub-region code. Example: 021. + /// + [JsonPropertyName("subregion_code")] + public string SubregionCode { get; set; } + + /// + /// Gets or sets the continent name of the country associated with the location result. + /// + [JsonPropertyName("continent_name")] + public string ContinentName { get; set; } + + /// + /// Gets or sets the continent code of the country associated with the location result. + /// + [JsonPropertyName("continent_code")] + public string ContinentCode { get; set; } + } +} diff --git a/src/Geo.Positionstack/Models/Responses/Language.cs b/src/Geo.Positionstack/Models/Responses/Language.cs new file mode 100644 index 0000000..19ff41e --- /dev/null +++ b/src/Geo.Positionstack/Models/Responses/Language.cs @@ -0,0 +1,27 @@ +// +// Copyright (c) Geo.NET. +// Licensed under the MIT license. See the LICENSE file in the solution root for full license information. +// + +namespace Geo.Positionstack.Models.Responses +{ + using System.Text.Json.Serialization; + + /// + /// Language of the country associated with the location result. + /// + public class Language + { + /// + /// Gets or sets the 2-letter language code of the given language. + /// + [JsonPropertyName("code")] + public string Code { get; set; } + + /// + /// Gets or sets the official name of the given language. + /// + [JsonPropertyName("name")] + public string Name { get; set; } + } +} diff --git a/src/Geo.Positionstack/Models/Responses/Meta.cs b/src/Geo.Positionstack/Models/Responses/Meta.cs deleted file mode 100644 index 112f5e4..0000000 --- a/src/Geo.Positionstack/Models/Responses/Meta.cs +++ /dev/null @@ -1,18 +0,0 @@ -// -// Copyright (c) Geo.NET. -// Licensed under the MIT license. See the LICENSE file in the solution root for full license information. -// - -namespace Geo.Positionstack.Models.Responses -{ - /// - /// Metadata associate with the request. - /// - public class Meta - { - /// - /// Gets or sets the response code from the request. - /// - public int Code { get; set; } - } -} diff --git a/src/Geo.Positionstack/Models/Responses/PhoneInformation.cs b/src/Geo.Positionstack/Models/Responses/PhoneInformation.cs new file mode 100644 index 0000000..99d310d --- /dev/null +++ b/src/Geo.Positionstack/Models/Responses/PhoneInformation.cs @@ -0,0 +1,33 @@ +// +// Copyright (c) Geo.NET. +// Licensed under the MIT license. See the LICENSE file in the solution root for full license information. +// + +namespace Geo.Positionstack.Models.Responses +{ + using System.Text.Json.Serialization; + + /// + /// Information about the phone for a country. + /// + public class PhoneInformation + { + /// + /// Gets or sets the calling code of the country associated with the location result. + /// + [JsonPropertyName("calling_code")] + public string CallingCode { get; set; } + + /// + /// Gets or sets the national calling prefix of the country associated with the location result. + /// + [JsonPropertyName("national_prefix")] + public string NationalPrefix { get; set; } + + /// + /// Gets or sets the international calling prefix of the country associated with the location result. + /// + [JsonPropertyName("international_prefix")] + public string InternationalPrefix { get; set; } + } +} diff --git a/src/Geo.Positionstack/Models/Responses/Response.cs b/src/Geo.Positionstack/Models/Responses/Response.cs index e35361e..4f19db9 100644 --- a/src/Geo.Positionstack/Models/Responses/Response.cs +++ b/src/Geo.Positionstack/Models/Responses/Response.cs @@ -5,25 +5,17 @@ namespace Geo.Positionstack.Models.Responses { - using System.Collections.Generic; using System.Text.Json.Serialization; /// /// The result of a request. /// - /// The type of the address. - public class Response + public class Response { /// - /// Gets or sets the metadata associate with the request. + /// Gets or sets the data with the location information. /// - [JsonPropertyName("meta")] - public Meta Meta { get; set; } - - /// - /// Gets or sets the addresses that are associated with the request. - /// - [JsonPropertyName("addresses")] - public IList Addresses { get; set; } = new List(); + [JsonPropertyName("data")] + public Data Data { get; set; } } } diff --git a/src/Geo.Positionstack/Models/Responses/ReverseGeocodeAddress.cs b/src/Geo.Positionstack/Models/Responses/ReverseGeocodeAddress.cs deleted file mode 100644 index 2227904..0000000 --- a/src/Geo.Positionstack/Models/Responses/ReverseGeocodeAddress.cs +++ /dev/null @@ -1,21 +0,0 @@ -// -// Copyright (c) Geo.NET. -// Licensed under the MIT license. See the LICENSE file in the solution root for full license information. -// - -namespace Geo.Positionstack.Models.Responses -{ - using System.Text.Json.Serialization; - - /// - /// Address information for a reverse geocode result. - /// - public class ReverseGeocodeAddress : Address - { - /// - /// Gets or sets the distance of the result from the point. - /// - [JsonPropertyName("distance")] - public int Distance { get; set; } - } -} diff --git a/src/Geo.Positionstack/Models/Responses/SunInformation.cs b/src/Geo.Positionstack/Models/Responses/SunInformation.cs new file mode 100644 index 0000000..a31de39 --- /dev/null +++ b/src/Geo.Positionstack/Models/Responses/SunInformation.cs @@ -0,0 +1,39 @@ +// +// Copyright (c) Geo.NET. +// Licensed under the MIT license. See the LICENSE file in the solution root for full license information. +// + +namespace Geo.Positionstack.Models.Responses +{ + using System.Text.Json.Serialization; + + /// + /// Information about the sun and its rising and setting. + /// + public class SunInformation + { + /// + /// Gets or sets the sunrise/sunset time as a UNIX timestamp (UTC). + /// + [JsonPropertyName("time")] + public uint? Time { get; set; } + + /// + /// Gets or sets the astronomical sunrise/sunset time as a UNIX timestamp (UTC). + /// + [JsonPropertyName("astronomical")] + public uint? Astronomical { get; set; } + + /// + /// Gets or sets the civil sunrise/sunset time as a UNIX timestamp (UTC). + /// + [JsonPropertyName("civil")] + public uint? Civil { get; set; } + + /// + /// Gets or sets the nautical sunrise/sunset time as a UNIX timestamp (UTC). + /// + [JsonPropertyName("nautical")] + public uint? Nautical { get; set; } + } +} diff --git a/src/Geo.Positionstack/Models/Responses/SunModule.cs b/src/Geo.Positionstack/Models/Responses/SunModule.cs new file mode 100644 index 0000000..dde51f1 --- /dev/null +++ b/src/Geo.Positionstack/Models/Responses/SunModule.cs @@ -0,0 +1,33 @@ +// +// Copyright (c) Geo.NET. +// Licensed under the MIT license. See the LICENSE file in the solution root for full license information. +// + +namespace Geo.Positionstack.Models.Responses +{ + using System.Text.Json.Serialization; + + /// + /// Astrology data for a location. + /// + public class SunModule + { + /// + /// Gets or sets the sunrise information. + /// + [JsonPropertyName("rise")] + public SunInformation Sunrise { get; set; } + + /// + /// Gets or sets the sunset information. + /// + [JsonPropertyName("set")] + public SunInformation Sunset { get; set; } + + /// + /// Gets or sets the sun transit time as a UNIX timestamp (UTC). + /// + [JsonPropertyName("transit")] + public uint? Transit { get; set; } + } +} diff --git a/src/Geo.Positionstack/Models/Responses/TimezoneModule.cs b/src/Geo.Positionstack/Models/Responses/TimezoneModule.cs new file mode 100644 index 0000000..f0e99c0 --- /dev/null +++ b/src/Geo.Positionstack/Models/Responses/TimezoneModule.cs @@ -0,0 +1,33 @@ +// +// Copyright (c) Geo.NET. +// Licensed under the MIT license. See the LICENSE file in the solution root for full license information. +// + +namespace Geo.Positionstack.Models.Responses +{ + using System.Text.Json.Serialization; + + /// + /// Timezone information for a location. + /// + public class TimezoneModule + { + /// + /// Gets or sets the common name of the timezone. + /// + [JsonPropertyName("name")] + public string Name { get; set; } + + /// + /// Gets or sets the GMT offset of the timezone in seconds. + /// + [JsonPropertyName("offset_sec")] + public int? OffsetInSeconds { get; set; } + + /// + /// Gets or sets the GMT offset of the timezone as a string. + /// + [JsonPropertyName("offset_string")] + public string OffsetString { get; set; } + } +} diff --git a/src/Geo.Positionstack/Services/PositionstackGeocoding.cs b/src/Geo.Positionstack/Services/PositionstackGeocoding.cs index 90ca035..d943773 100644 --- a/src/Geo.Positionstack/Services/PositionstackGeocoding.cs +++ b/src/Geo.Positionstack/Services/PositionstackGeocoding.cs @@ -50,23 +50,23 @@ public PositionstackGeocoding( protected override string ApiName => "Positionstack"; /// - public async Task> GeocodingAsync( + public async Task GeocodingAsync( GeocodingParameters parameters, CancellationToken cancellationToken = default) { var uri = ValidateAndBuildUri(parameters, BuildGeocodingRequest); - return await GetAsync>(uri, cancellationToken).ConfigureAwait(false); + return await GetAsync(uri, cancellationToken).ConfigureAwait(false); } /// - public async Task> ReverseGeocodingAsync( + public async Task ReverseGeocodingAsync( ReverseGeocodingParameters parameters, CancellationToken cancellationToken = default) { var uri = ValidateAndBuildUri(parameters, BuildReverseGeocodingRequest); - return await GetAsync>(uri, cancellationToken).ConfigureAwait(false); + return await GetAsync(uri, cancellationToken).ConfigureAwait(false); } /// @@ -159,9 +159,9 @@ internal Uri BuildReverseGeocodingRequest(ReverseGeocodingParameters parameters) /// A with the query parameters. internal void AddLocationParameters(ILocationGeocodeParameters locationParameters, ref QueryString query) { - if (locationParameters.Country?.Count > 0) + if (locationParameters.Countries?.Count > 0) { - var countries = string.Join(",", locationParameters.Country ?? Array.Empty()); + var countries = string.Join(",", locationParameters.Countries ?? Array.Empty()); query = query.Add("country", countries); } else From a0c983fe37c98f406e28f75030680a2f7f52c0e9 Mon Sep 17 00:00:00 2001 From: JustinCanton <67930245+JustinCanton@users.noreply.github.com> Date: Mon, 20 May 2024 12:16:39 -0400 Subject: [PATCH 3/4] test(positionstack): adding tests for the positionstack api --- .../Services/PositionstackGeocoding.cs | 3 +- .../Services/MapBoxGeocodingShould.cs | 2 +- .../Services/PositionstackGeocodingShould.cs | 275 +++++------------- 3 files changed, 75 insertions(+), 205 deletions(-) diff --git a/src/Geo.Positionstack/Services/PositionstackGeocoding.cs b/src/Geo.Positionstack/Services/PositionstackGeocoding.cs index d943773..ece407b 100644 --- a/src/Geo.Positionstack/Services/PositionstackGeocoding.cs +++ b/src/Geo.Positionstack/Services/PositionstackGeocoding.cs @@ -6,6 +6,7 @@ namespace Geo.Positionstack.Services { using System; + using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; @@ -242,7 +243,7 @@ internal void AddFilterParameters(IFilterGeocodeParameters filterParameters, ref if (filterParameters.Fields?.Count > 0) { - var fields = string.Join(",", filterParameters.Fields ?? Array.Empty()); + var fields = string.Join(",", filterParameters.Fields.Where(x => !string.IsNullOrWhiteSpace(x)) ?? Array.Empty()); query = query.Add("fields", fields); } else diff --git a/test/Geo.MapBox.Tests/Services/MapBoxGeocodingShould.cs b/test/Geo.MapBox.Tests/Services/MapBoxGeocodingShould.cs index c681515..ae63b25 100644 --- a/test/Geo.MapBox.Tests/Services/MapBoxGeocodingShould.cs +++ b/test/Geo.MapBox.Tests/Services/MapBoxGeocodingShould.cs @@ -119,7 +119,7 @@ public void AddMapBoxKey_WithOptions_SuccessfullyAddsKey() } [Fact] - public void AddHereKey_WithParameterOverride_SuccessfullyAddsKey() + public void AddMapBoxKey_WithParameterOverride_SuccessfullyAddsKey() { var sut = BuildService(); diff --git a/test/Geo.Positionstack.Tests/Services/PositionstackGeocodingShould.cs b/test/Geo.Positionstack.Tests/Services/PositionstackGeocodingShould.cs index fd302b2..87c8f00 100644 --- a/test/Geo.Positionstack.Tests/Services/PositionstackGeocodingShould.cs +++ b/test/Geo.Positionstack.Tests/Services/PositionstackGeocodingShould.cs @@ -31,7 +31,6 @@ public class PositionstackGeocodingShould : IDisposable { private readonly HttpClient _httpClient; private readonly Mock>> _options = new Mock>>(); - private readonly List _responseMessages = new List(); private bool _disposed; /// @@ -46,58 +45,8 @@ public PositionstackGeocodingShould() Key = "abc123", }); - var mockHandler = new Mock(); - - _responseMessages.Add(new HttpResponseMessage() - { - StatusCode = HttpStatusCode.OK, - Content = new StringContent( - "{\"meta\":{\"code\":200},\"addresses\":[{\"latitude\":47.576575,\"longitude\":16.430711,\"geometry\":{\"type\":\"Point\",\"coordinates\":[16.430711,47.576575]},\"country\":\"Austria\",\"countryCode\":\"AT\",\"countryFlag\":\"🇦🇹\",\"county\":\"Oberpullendorf\",\"distance\":6962472,\"confidence\":\"exact\",\"city\":\"Weppersdorf\",\"number\":\"123\",\"postalCode\":\"7331\",\"stateCode\":\"BU\",\"state\":\"Burgenland\",\"street\":\"Hauptstraße\",\"layer\":\"address\",\"formattedAddress\":\"123 Hauptstraße, BU 7331 AUT\",\"addressLabel\":\"Hauptstraße 123\"}]}"), - }); - - // For reverse geocoding, use the places endpoint type - mockHandler - .Protected() - .Setup>( - "SendAsync", - ItExpr.Is(x => x.RequestUri.PathAndQuery.Contains("geocode/forward")), - ItExpr.IsAny()) - .ReturnsAsync(_responseMessages[_responseMessages.Count - 1]); - - _responseMessages.Add(new HttpResponseMessage() - { - StatusCode = HttpStatusCode.OK, - Content = new StringContent( - "{\"meta\":{\"code\":200},\"addresses\":[{\"latitude\":47.576174,\"longitude\":16.43042,\"geometry\":{\"type\":\"Point\",\"coordinates\":[16.43042,47.576174]},\"country\":\"Austria\",\"countryCode\":\"AT\",\"countryFlag\":\"🇦🇹\",\"county\":\"Oberpullendorf\",\"distance\":50,\"city\":\"Weppersdorf\",\"number\":\"123\",\"postalCode\":\"7331\",\"stateCode\":\"BU\",\"state\":\"Burgenland\",\"street\":\"Hauptstraße\",\"layer\":\"address\",\"formattedAddress\":\"123 Hauptstraße, BU 7331 AUT\",\"addressLabel\":\"Hauptstraße 123\"}]}"), - }); - - // For reverse geocoding, use the permanent endpoint type - mockHandler - .Protected() - .Setup>( - "SendAsync", - ItExpr.Is(x => x.RequestUri.PathAndQuery.Contains("geocode/reverse")), - ItExpr.IsAny()) - .ReturnsAsync(_responseMessages[_responseMessages.Count - 1]); - - _responseMessages.Add(new HttpResponseMessage() - { - StatusCode = HttpStatusCode.OK, - Content = new StringContent( - "{\"meta\":{\"code\":200},\"addresses\":[{\"latitude\":43.76866,\"longitude\":-79.40616,\"geometry\":{\"type\":\"Point\",\"coordinates\":[-79.40616,43.76866]},\"country\":\"Canada\",\"countryCode\":\"CA\",\"countryFlag\":\"🇨🇦\",\"county\":\"Toronto\",\"distance\":381,\"borough\":\"North York\",\"city\":\"Toronto\",\"number\":\"123\",\"neighborhood\":\"Willowdale East\",\"postalCode\":\"M2N 3N8\",\"stateCode\":\"ON\",\"state\":\"Ontario\",\"street\":\"Hillcrest Ave\",\"layer\":\"address\",\"formattedAddress\":\"123 Hillcrest Ave, North York, Toronto, ON M2N 3N8 CAN\",\"addressLabel\":\"123 Hillcrest Ave\"},{\"latitude\":43.7678,\"longitude\":-79.40562,\"geometry\":{\"type\":\"Point\",\"coordinates\":[-79.40562,43.7678]},\"country\":\"Canada\",\"countryCode\":\"CA\",\"countryFlag\":\"🇨🇦\",\"county\":\"Toronto\",\"distance\":436,\"borough\":\"North York\",\"city\":\"Toronto\",\"number\":\"123\",\"neighborhood\":\"Willowdale East\",\"postalCode\":\"M2N 3M1\",\"stateCode\":\"ON\",\"state\":\"Ontario\",\"street\":\"Elmwood Ave\",\"layer\":\"address\",\"formattedAddress\":\"123 Elmwood Ave, North York, Toronto, ON M2N 3M1 CAN\",\"addressLabel\":\"123 Elmwood Ave\"},{\"latitude\":43.764905,\"longitude\":-79.408365,\"geometry\":{\"type\":\"Point\",\"coordinates\":[-79.408365,43.764905]},\"country\":\"Canada\",\"countryCode\":\"CA\",\"countryFlag\":\"🇨🇦\",\"county\":\"Toronto\",\"distance\":469,\"borough\":\"North York\",\"city\":\"Toronto\",\"number\":\"123\",\"neighborhood\":\"Willowdale East\",\"postalCode\":\"M2N 4T2\",\"stateCode\":\"ON\",\"state\":\"Ontario\",\"street\":\"Doris Avenue\",\"layer\":\"address\",\"formattedAddress\":\"123 Doris Avenue, North York, Toronto, ON M2N 4T2 CAN\",\"addressLabel\":\"123 Doris Avenue\"},{\"latitude\":43.766997,\"longitude\":-79.405112,\"geometry\":{\"type\":\"Point\",\"coordinates\":[-79.405112,43.766997]},\"country\":\"Canada\",\"countryCode\":\"CA\",\"countryFlag\":\"🇨🇦\",\"county\":\"Toronto\",\"distance\":502,\"borough\":\"North York\",\"city\":\"Toronto\",\"number\":\"123\",\"neighborhood\":\"Willowdale East\",\"postalCode\":\"M2N 3K2\",\"stateCode\":\"ON\",\"state\":\"Ontario\",\"street\":\"Hollywood Avenue\",\"layer\":\"address\",\"formattedAddress\":\"123 Hollywood Avenue, North York, Toronto, ON M2N 3K2 CAN\",\"addressLabel\":\"123 Hollywood Avenue\"},{\"latitude\":43.773507,\"longitude\":-79.405844,\"geometry\":{\"type\":\"Point\",\"coordinates\":[-79.405844,43.773507]},\"country\":\"Canada\",\"countryCode\":\"CA\",\"countryFlag\":\"🇨🇦\",\"county\":\"Toronto\",\"distance\":672,\"borough\":\"North York\",\"city\":\"Toronto\",\"number\":\"123\",\"neighborhood\":\"Willowdale East\",\"postalCode\":\"M2N 4A7\",\"stateCode\":\"ON\",\"state\":\"Ontario\",\"street\":\"Norton Avenue\",\"layer\":\"address\",\"formattedAddress\":\"123 Norton Avenue, North York, Toronto, ON M2N 4A7 CAN\",\"addressLabel\":\"123 Norton Avenue\"},{\"latitude\":43.76654,\"longitude\":-79.40308,\"geometry\":{\"type\":\"Point\",\"coordinates\":[-79.40308,43.76654]},\"country\":\"Canada\",\"countryCode\":\"CA\",\"countryFlag\":\"🇨🇦\",\"county\":\"Toronto\",\"distance\":673,\"borough\":\"North York\",\"city\":\"Toronto\",\"number\":\"123\",\"neighborhood\":\"Willowdale East\",\"postalCode\":\"M2N 3J1\",\"stateCode\":\"ON\",\"state\":\"Ontario\",\"street\":\"Alfred Ave\",\"layer\":\"address\",\"formattedAddress\":\"123 Alfred Ave, North York, Toronto, ON M2N 3J1 CAN\",\"addressLabel\":\"123 Alfred Ave\"},{\"latitude\":43.7752,\"longitude\":-79.41155,\"geometry\":{\"type\":\"Point\",\"coordinates\":[-79.41155,43.7752]},\"country\":\"Canada\",\"countryCode\":\"CA\",\"countryFlag\":\"🇨🇦\",\"county\":\"Toronto\",\"distance\":725,\"borough\":\"North York\",\"city\":\"Toronto\",\"number\":\"123\",\"neighborhood\":\"Willowdale East\",\"postalCode\":\"M2N 6V2\",\"stateCode\":\"ON\",\"state\":\"Ontario\",\"street\":\"Grandview Way\",\"layer\":\"address\",\"formattedAddress\":\"123 Grandview Way, North York, Toronto, ON M2N 6V2 CAN\",\"addressLabel\":\"123 Grandview Way\"},{\"latitude\":43.7753,\"longitude\":-79.40628,\"geometry\":{\"type\":\"Point\",\"coordinates\":[-79.40628,43.7753]},\"country\":\"Canada\",\"countryCode\":\"CA\",\"countryFlag\":\"🇨🇦\",\"county\":\"Toronto\",\"distance\":823,\"borough\":\"North York\",\"city\":\"Toronto\",\"number\":\"123\",\"neighborhood\":\"Willowdale East\",\"postalCode\":\"M2N 4G3\",\"stateCode\":\"ON\",\"state\":\"Ontario\",\"street\":\"Church Ave\",\"layer\":\"address\",\"formattedAddress\":\"123 Church Ave, North York, Toronto, ON M2N 4G3 CAN\",\"addressLabel\":\"123 Church Ave\"},{\"latitude\":43.76244,\"longitude\":-79.41773,\"geometry\":{\"type\":\"Point\",\"coordinates\":[-79.41773,43.76244]},\"country\":\"Canada\",\"countryCode\":\"CA\",\"countryFlag\":\"🇨🇦\",\"county\":\"Toronto\",\"distance\":887,\"borough\":\"North York\",\"city\":\"Toronto\",\"number\":\"123\",\"neighborhood\":\"Lansing-Westgate\",\"postalCode\":\"M2N 1S8\",\"stateCode\":\"ON\",\"state\":\"Ontario\",\"street\":\"Burndale Ave\",\"layer\":\"address\",\"formattedAddress\":\"123 Burndale Ave, North York, Toronto, ON M2N 1S8 CAN\",\"addressLabel\":\"123 Burndale Ave\"},{\"latitude\":43.773987,\"longitude\":-79.420541,\"geometry\":{\"type\":\"Point\",\"coordinates\":[-79.420541,43.773987]},\"country\":\"Canada\",\"countryCode\":\"CA\",\"countryFlag\":\"🇨🇦\",\"county\":\"Toronto\",\"distance\":973,\"borough\":\"North York\",\"city\":\"Toronto\",\"number\":\"123\",\"neighborhood\":\"Willowdale West\",\"postalCode\":\"M2N 2B1\",\"stateCode\":\"ON\",\"state\":\"Ontario\",\"street\":\"Hounslow Avenue\",\"layer\":\"address\",\"formattedAddress\":\"123 Hounslow Avenue, North York, Toronto, ON M2N 2B1 CAN\",\"addressLabel\":\"123 Hounslow Avenue\"}]}"), - }); - - // For reverse geocoding, use the places endpoint type - mockHandler - .Protected() - .Setup>( - "SendAsync", - ItExpr.Is(x => x.RequestUri.PathAndQuery.Contains("search/autocomplete")), - ItExpr.IsAny()) - .ReturnsAsync(_responseMessages[_responseMessages.Count - 1]); - var options = Options.Create(new LocalizationOptions { ResourcesPath = "Resources" }); - _httpClient = new HttpClient(mockHandler.Object); + _httpClient = new HttpClient(new Mock().Object); } /// @@ -112,56 +61,103 @@ public void AddPositionstackKey_WithOptions_SuccessfullyAddsKey() { var sut = BuildService(); - sut.AddPositionstackKey(new GeocodingParameters()); + var query = QueryString.Empty; + + sut.AddPositionstackKey(new GeocodingParameters(), ref query); - _httpClient.DefaultRequestHeaders.Authorization.Scheme.Should().Be("abc123"); + var queryParameters = HttpUtility.ParseQueryString(query.ToString()); + queryParameters.Count.Should().Be(1); + queryParameters["access_key"].Should().Be("abc123"); } [Fact] - public void AddHereKey_WithParameterOverride_SuccessfullyAddsKey() + public void AddPositionstackKey_WithParameterOverride_SuccessfullyAddsKey() { var sut = BuildService(); - sut.AddPositionstackKey(new GeocodingParameters() { Key = "123abc" }); + var query = QueryString.Empty; + + sut.AddPositionstackKey(new GeocodingParameters() { Key = "123abc" }, ref query); - _httpClient.DefaultRequestHeaders.Authorization.Scheme.Should().Be("123abc"); + var queryParameters = HttpUtility.ParseQueryString(query.ToString()); + queryParameters.Count.Should().Be(1); + queryParameters["access_key"].Should().Be("123abc"); } [Fact] - public void AddCountry_WithValidCountries_AddsSuccessfully() + public void AddFilterParameters_WithValidData_AddsSuccessfully() { var sut = BuildService(); var query = QueryString.Empty; - var parameters = new GeocodingParameters(); + var parameters = new GeocodingParameters() + { + Language = "en", + CountryModule = true, + SunModule = true, + TimezoneModule = true, + BoundingBoxModule = true, + Limit = 7, + }; - parameters.Countries.Add("CA"); - parameters.Countries.Add("FR"); + parameters.Fields.Add("country.flag"); + parameters.Fields.Add("results.map_url"); + parameters.Fields.Add(string.Empty); + + sut.AddFilterParameters(parameters, ref query); + + var queryParameters = HttpUtility.ParseQueryString(query.ToString()); + queryParameters.Count.Should().Be(7); + queryParameters["language"].Should().Be("en"); + queryParameters["country_module"].Should().Be("1"); + queryParameters["sun_module"].Should().Be("1"); + queryParameters["timezone_module"].Should().Be("1"); + queryParameters["bbox_module"].Should().Be("1"); + queryParameters["limit"].Should().Be("7"); + queryParameters["fields"].Should().Be("country.flag,results.map_url"); + } + + [Fact] + public void AddFilterParameters_WithLessData_AddsSuccessfully() + { + var sut = BuildService(); + + var query = QueryString.Empty; + var parameters = new GeocodingParameters() + { + CountryModule = false, + SunModule = false, + TimezoneModule = false, + BoundingBoxModule = false, + }; - sut.AddCountry(parameters, ref query); + sut.AddFilterParameters(parameters, ref query); var queryParameters = HttpUtility.ParseQueryString(query.ToString()); queryParameters.Count.Should().Be(1); - queryParameters["country"].Should().Be("CA,FR"); + queryParameters["limit"].Should().Be("10"); } [Fact] - public void AddLayers_WithValidLayers_AddsSuccessfully() + public void AddLocationParameters_WithValidLocationInformation_AddsSuccessfully() { var sut = BuildService(); var query = QueryString.Empty; - var parameters = new GeocodingParameters(); + var parameters = new GeocodingParameters() + { + Region = "Paris", + }; - parameters.Layers.Add(Layer.PostalCode); - parameters.Layers.Add(Layer.Country); - parameters.Layers.Add(Layer.Coarse); + parameters.Countries.Add("CA"); + parameters.Countries.Add("FR"); - sut.AddLayers(parameters, ref query); + sut.AddLocationParameters(parameters, ref query); var queryParameters = HttpUtility.ParseQueryString(query.ToString()); - queryParameters.Count.Should().Be(1); - queryParameters["layers"].Should().Be("postalCode,country,coarse"); + queryParameters.Count.Should().Be(2); + queryParameters["country"].Should().Be("CA,FR"); + queryParameters["region"].Should().Be("Paris"); } [Theory] @@ -180,9 +176,6 @@ public void BuildGeocodingRequest_WithValidParameters_SuccessfullyBuildsUrl(Cult Key = "123abc", }; - parameters.Layers.Add(Layer.PostalCode); - parameters.Layers.Add(Layer.Country); - parameters.Countries.Add("CA"); // Act @@ -192,9 +185,7 @@ public void BuildGeocodingRequest_WithValidParameters_SuccessfullyBuildsUrl(Cult var query = HttpUtility.UrlDecode(uri.PathAndQuery); query.Should().Contain("query=123 East"); query.Should().Contain("country=CA"); - query.Should().Contain("layers=postalCode,country"); - - _httpClient.DefaultRequestHeaders.Authorization.Scheme.Should().Be("123abc"); + query.Should().Contain("access_key=123abc"); Thread.CurrentThread.CurrentCulture = oldCulture; } @@ -251,18 +242,13 @@ public void BuildReverseGeocodingRequest_WithValidParameters_SuccessfullyBuildsU Key = "123abc", }; - parameters.Layers.Add(Layer.Locality); - parameters.Layers.Add(Layer.Fine); - // Act var uri = sut.BuildReverseGeocodingRequest(parameters); // Assert var query = HttpUtility.UrlDecode(uri.PathAndQuery); - query.Should().Contain("coordinates=56.78,78.91"); - query.Should().Contain("layers=locality,fine"); - - _httpClient.DefaultRequestHeaders.Authorization.Scheme.Should().Be("123abc"); + query.Should().Contain("query=56.78,78.91"); + query.Should().Contain("access_key=123abc"); Thread.CurrentThread.CurrentCulture = oldCulture; } @@ -283,67 +269,6 @@ public void BuildReverseGeocodingRequest_WithInvalidParameters_FailsWithExceptio #endif } - [Theory] - [ClassData(typeof(CultureTestData))] - public void BuildAutocompleteRequest_WithValidParameters_SuccessfullyBuildsUrl(CultureInfo culture) - { - // Arrange - var oldCulture = Thread.CurrentThread.CurrentCulture; - Thread.CurrentThread.CurrentCulture = culture; - - var sut = BuildService(); - - var parameters = new AutocompleteParameters() - { - Query = "123 East", - Near = new Coordinate() - { - Latitude = 56.78, - Longitude = 78.91, - }, - Limit = 14, - Mailable = true, - Key = "123abc", - }; - - parameters.Layers.Add(Layer.PostalCode); - parameters.Layers.Add(Layer.Country); - - parameters.Countries.Add("CA"); - - // Act - var uri = sut.BuildAutocompleteRequest(parameters); - - // Assert - var query = HttpUtility.UrlDecode(uri.PathAndQuery); - query.Should().Contain("query=123 East"); - query.Should().Contain("near=56.78,78.91"); - query.Should().Contain("limit=14"); - query.Should().Contain("mailable=true"); - query.Should().Contain("country=CA"); - query.Should().Contain("layers=postalCode,country"); - - _httpClient.DefaultRequestHeaders.Authorization.Scheme.Should().Be("123abc"); - - Thread.CurrentThread.CurrentCulture = oldCulture; - } - - [Fact] - public void BuildAutocompleteRequest_WithInvalidParameters_FailsWithException() - { - var sut = BuildService(); - - Action act = () => sut.BuildAutocompleteRequest(new AutocompleteParameters()); - - act.Should() - .Throw() -#if NETCOREAPP3_1_OR_GREATER - .WithMessage("*(Parameter 'Query')"); -#else - .WithMessage("*Parameter name: Query"); -#endif - } - [Fact] public void ValidateAndBuildUri_WithValidParameters_SuccessfullyBuildsUri() { @@ -359,15 +284,10 @@ public void ValidateAndBuildUri_WithValidParameters_SuccessfullyBuildsUri() Key = "123abc", }; - parameters.Layers.Add(Layer.Locality); - parameters.Layers.Add(Layer.Fine); - var uri = sut.ValidateAndBuildUri(parameters, sut.BuildReverseGeocodingRequest); var query = HttpUtility.UrlDecode(uri.PathAndQuery); - query.Should().Contain("coordinates=56.78,78.91"); - query.Should().Contain("layers=locality,fine"); - - _httpClient.DefaultRequestHeaders.Authorization.Scheme.Should().Be("123abc"); + query.Should().Contain("query=56.78,78.91"); + query.Should().Contain("access_key=123abc"); } [Fact] @@ -401,52 +321,6 @@ public void ValidateAndBuildUri_WithInvalidParameters_FailsWithException() #endif } - [Fact] - public async Task GeocodingAsync_WithValidParameters_ReturnsSuccessfully() - { - var sut = BuildService(); - - var parameters = new GeocodingParameters() - { - Query = "123 East", - }; - - var result = await sut.GeocodingAsync(parameters); - result.Addresses.Count.Should().Be(1); - } - - [Fact] - public async Task ReverseGeocodingAsync_WithValidParameters_ReturnsSuccessfully() - { - var sut = BuildService(); - - var parameters = new ReverseGeocodingParameters() - { - Coordinate = new Coordinate() - { - Latitude = 56.78, - Longitude = 78.91, - }, - }; - - var result = await sut.ReverseGeocodingAsync(parameters); - result.Addresses.Count.Should().Be(1); - } - - [Fact] - public async Task AutocompleteAsync_WithValidParameters_ReturnsSuccessfully() - { - var sut = BuildService(); - - var parameters = new AutocompleteParameters() - { - Query = "123 East", - }; - - var result = await sut.AutocompleteAsync(parameters); - result.Addresses.Count.Should().Be(10); - } - /// /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. /// @@ -461,11 +335,6 @@ protected virtual void Dispose(bool disposing) if (disposing) { _httpClient?.Dispose(); - - foreach (var message in _responseMessages) - { - message?.Dispose(); - } } _disposed = true; From 614143ef6fadf429c970add55ae2cb54dc504197 Mon Sep 17 00:00:00 2001 From: JustinCanton <67930245+JustinCanton@users.noreply.github.com> Date: Mon, 20 May 2024 12:23:46 -0400 Subject: [PATCH 4/4] docs: updating the README --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index fdac9a9..3fc4e99 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,9 @@ The support for this project includes: - Licensed API - [Geocoding](https://developer.mapquest.com/documentation/geocoding-api/address/get/) - [Reverse Geocoding](https://developer.mapquest.com/documentation/geocoding-api/reverse/get/) + - Positionstack + - [Geocoding](https://positionstack.com/documentation#forward_geocoding) + - [Reverse Geocoding](https://positionstack.com/documentation#reverse_geocoding) - Radar - [Geocoding](https://radar.com/documentation/api#geocoding) - [Reverse Geocoding](https://radar.com/documentation/api#reverse-geocode) @@ -51,6 +54,7 @@ The configuration and sample usage for each supported interface can be found wit - [HERE](https://github.com/JustinCanton/Geo.NET/tree/master/src/Geo.Here) - [MapBox](https://github.com/JustinCanton/Geo.NET/tree/master/src/Geo.MapBox) - [MapQuest](https://github.com/JustinCanton/Geo.NET/tree/master/src/Geo.MapQuest) + - [Positionstack](https://github.com/JustinCanton/Geo.NET/tree/master/src/Geo.Positionstack) - [Radar](https://github.com/JustinCanton/Geo.NET/tree/master/src/Geo.Radar)