From 448b0876a0fb36fc74f348752e7dd77f63f3f7dd Mon Sep 17 00:00:00 2001 From: Justin Canton <67930245+JustinCanton@users.noreply.github.com> Date: Mon, 13 May 2024 22:41:58 -0400 Subject: [PATCH 1/4] feat(radar): adding support for the Radar geocoding API (#101) --- CHANGELOG.md | 6 +- Geo.NET.sln | 14 + README.md | 5 + src/Geo.Core/GeoClient.cs | 12 +- src/Geo.MapBox/Services/MapBoxGeocoding.cs | 2 +- src/Geo.Radar/Abstractions/Enums/Layer.cs | 58 +++ src/Geo.Radar/Abstractions/IRadarGeocoding.cs | 46 ++ .../Abstractions/Models/ICountryParameter.cs | 20 + .../Abstractions/Models/ILayersParameter.cs | 20 + .../Converters/CoordinateConverter.cs | 98 ++++ .../ServiceCollectionExtensions.cs | 44 ++ src/Geo.Radar/Extensions/LoggerExtensions.cs | 62 +++ src/Geo.Radar/Geo.Radar.csproj | 45 ++ src/Geo.Radar/Models/Coordinate.cs | 36 ++ .../Parameters/AutocompleteParameters.cs | 44 ++ .../Models/Parameters/GeocodingParameters.cs | 30 ++ .../Parameters/ReverseGeocodingParameters.cs | 27 + src/Geo.Radar/Models/Responses/Address.cs | 124 +++++ .../Models/Responses/GeocodeAddress.cs | 21 + src/Geo.Radar/Models/Responses/Geometry.cs | 27 + src/Geo.Radar/Models/Responses/Meta.cs | 18 + src/Geo.Radar/Models/Responses/Response.cs | 29 ++ .../Models/Responses/ReverseGeocodeAddress.cs | 21 + src/Geo.Radar/Properties/AssemblyInfo.cs | 10 + src/Geo.Radar/README.md | 40 ++ .../Services/RadarGeocoding.Designer.cs | 135 +++++ .../Resources/Services/RadarGeocoding.en.resx | 144 ++++++ .../Resources/Services/RadarGeocoding.resx | 144 ++++++ src/Geo.Radar/Services/RadarGeocoding.cs | 273 ++++++++++ .../Converters/CoordinateConverterShould.cs | 30 ++ .../ServiceCollectionExtensionsTests.cs | 75 +++ test/Geo.Radar.Tests/Geo.Radar.Tests.csproj | 15 + test/Geo.Radar.Tests/GlobalSuppressions.cs | 13 + .../Models/CoordinateObject.cs | 14 + .../Services/RadarGeocodingShould.cs | 480 ++++++++++++++++++ .../TestData/CultureTestData.cs | 36 ++ 36 files changed, 2212 insertions(+), 6 deletions(-) create mode 100644 src/Geo.Radar/Abstractions/Enums/Layer.cs create mode 100644 src/Geo.Radar/Abstractions/IRadarGeocoding.cs create mode 100644 src/Geo.Radar/Abstractions/Models/ICountryParameter.cs create mode 100644 src/Geo.Radar/Abstractions/Models/ILayersParameter.cs create mode 100644 src/Geo.Radar/Converters/CoordinateConverter.cs create mode 100644 src/Geo.Radar/DependencyInjection/ServiceCollectionExtensions.cs create mode 100644 src/Geo.Radar/Extensions/LoggerExtensions.cs create mode 100644 src/Geo.Radar/Geo.Radar.csproj create mode 100644 src/Geo.Radar/Models/Coordinate.cs create mode 100644 src/Geo.Radar/Models/Parameters/AutocompleteParameters.cs create mode 100644 src/Geo.Radar/Models/Parameters/GeocodingParameters.cs create mode 100644 src/Geo.Radar/Models/Parameters/ReverseGeocodingParameters.cs create mode 100644 src/Geo.Radar/Models/Responses/Address.cs create mode 100644 src/Geo.Radar/Models/Responses/GeocodeAddress.cs create mode 100644 src/Geo.Radar/Models/Responses/Geometry.cs create mode 100644 src/Geo.Radar/Models/Responses/Meta.cs create mode 100644 src/Geo.Radar/Models/Responses/Response.cs create mode 100644 src/Geo.Radar/Models/Responses/ReverseGeocodeAddress.cs create mode 100644 src/Geo.Radar/Properties/AssemblyInfo.cs create mode 100644 src/Geo.Radar/README.md create mode 100644 src/Geo.Radar/Resources/Services/RadarGeocoding.Designer.cs create mode 100644 src/Geo.Radar/Resources/Services/RadarGeocoding.en.resx create mode 100644 src/Geo.Radar/Resources/Services/RadarGeocoding.resx create mode 100644 src/Geo.Radar/Services/RadarGeocoding.cs create mode 100644 test/Geo.Radar.Tests/Converters/CoordinateConverterShould.cs create mode 100644 test/Geo.Radar.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs create mode 100644 test/Geo.Radar.Tests/Geo.Radar.Tests.csproj create mode 100644 test/Geo.Radar.Tests/GlobalSuppressions.cs create mode 100644 test/Geo.Radar.Tests/Models/CoordinateObject.cs create mode 100644 test/Geo.Radar.Tests/Services/RadarGeocodingShould.cs create mode 100644 test/Geo.Radar.Tests/TestData/CultureTestData.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index eb59980..f785322 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,11 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org/) for commit guidelines. -## [2.0.0](https://github.com/JustinCanton/Geo.NET/compare/1.6.0...2.0.0) (2024-01-30) +## [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/)) + +## [2.0.0](https://github.com/JustinCanton/Geo.NET/compare/1.6.0...2.0.0) (2024-01-28) ### ⚠ BREAKING CHANGES - removed native support for net5.0 since it is an out of support item, and dropped netstandard2.1 since this supports netstandard2.0 - removed the usage of Newtonsoft.Json and moved to use System.Text.Json diff --git a/Geo.NET.sln b/Geo.NET.sln index d871edf..abbde23 100644 --- a/Geo.NET.sln +++ b/Geo.NET.sln @@ -45,6 +45,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Geo.MapBox.Tests", "test\Ge EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Geo.MapQuest.Tests", "test\Geo.MapQuest.Tests\Geo.MapQuest.Tests.csproj", "{85FF4115-0880-4DF6-816A-B314CA0432D3}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Geo.Radar", "src\Geo.Radar\Geo.Radar.csproj", "{38C213E3-55D6-4FC7-87CA-E28C3EDD42EA}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Geo.Radar.Tests", "test\Geo.Radar.Tests\Geo.Radar.Tests.csproj", "{1A320DE3-B14B-46EE-A0E6-C6783E585F73}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -107,6 +111,14 @@ Global {85FF4115-0880-4DF6-816A-B314CA0432D3}.Debug|Any CPU.Build.0 = Debug|Any CPU {85FF4115-0880-4DF6-816A-B314CA0432D3}.Release|Any CPU.ActiveCfg = Release|Any CPU {85FF4115-0880-4DF6-816A-B314CA0432D3}.Release|Any CPU.Build.0 = Release|Any CPU + {38C213E3-55D6-4FC7-87CA-E28C3EDD42EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {38C213E3-55D6-4FC7-87CA-E28C3EDD42EA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {38C213E3-55D6-4FC7-87CA-E28C3EDD42EA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {38C213E3-55D6-4FC7-87CA-E28C3EDD42EA}.Release|Any CPU.Build.0 = Release|Any CPU + {1A320DE3-B14B-46EE-A0E6-C6783E585F73}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {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 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -126,6 +138,8 @@ Global {3EE9598A-8464-450E-9BE4-C19E3FC1450D} = {67253D97-9FC9-4749-80DC-A5D84339DC05} {2D1EB4BD-E554-46B6-8FEE-73CC486341F2} = {67253D97-9FC9-4749-80DC-A5D84339DC05} {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} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {C4B688C4-40EC-4577-9EB2-4CF2412DA0B1} diff --git a/README.md b/README.md index db27805..fdac9a9 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,10 @@ 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/) + - Radar + - [Geocoding](https://radar.com/documentation/api#geocoding) + - [Reverse Geocoding](https://radar.com/documentation/api#reverse-geocode) + - [Autocomplete](https://radar.com/documentation/api#autocomplete) ## Configuration and Sample Usage @@ -47,6 +51,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) + - [Radar](https://github.com/JustinCanton/Geo.NET/tree/master/src/Geo.Radar) ## Suggestions or Discussion Points diff --git a/src/Geo.Core/GeoClient.cs b/src/Geo.Core/GeoClient.cs index 233e0df..fb58911 100644 --- a/src/Geo.Core/GeoClient.cs +++ b/src/Geo.Core/GeoClient.cs @@ -23,7 +23,6 @@ public abstract class GeoClient { private static readonly JsonSerializerOptions _options = GetJsonSerializerOptions(); - private readonly HttpClient _client; private readonly ILogger _logger; /// @@ -40,10 +39,15 @@ protected GeoClient( #endif { Resources.GeoClient.Culture = CultureInfo.InvariantCulture; - _client = client ?? throw new ArgumentNullException(nameof(client)); + Client = client ?? throw new ArgumentNullException(nameof(client)); _logger = loggerFactory?.CreateLogger() ?? NullLogger.Instance; } + /// + /// Gets the http client associated with the . + /// + protected HttpClient Client { get; private set; } + /// /// Gets the name of the API being called for exception logging purposes. /// @@ -287,11 +291,11 @@ internal async Task HttpCallAsync( { if (method.Method == HttpMethod.Get.Method) { - return await _client.GetAsync(uri, cancellationToken).ConfigureAwait(false); + return await Client.GetAsync(uri, cancellationToken).ConfigureAwait(false); } else { - return await _client.PostAsync(uri, content, cancellationToken).ConfigureAwait(false); + return await Client.PostAsync(uri, content, cancellationToken).ConfigureAwait(false); } } } diff --git a/src/Geo.MapBox/Services/MapBoxGeocoding.cs b/src/Geo.MapBox/Services/MapBoxGeocoding.cs index 1448b32..9ed5c8a 100644 --- a/src/Geo.MapBox/Services/MapBoxGeocoding.cs +++ b/src/Geo.MapBox/Services/MapBoxGeocoding.cs @@ -41,7 +41,7 @@ public class MapBoxGeocoding : GeoClient, IMapBoxGeocoding /// Initializes a new instance of the class. /// /// A used for placing calls to the here Geocoding API. - /// An of containing Google key information. + /// An of containing MapBox key information. /// An used to create a logger used for logging information. public MapBoxGeocoding( HttpClient client, diff --git a/src/Geo.Radar/Abstractions/Enums/Layer.cs b/src/Geo.Radar/Abstractions/Enums/Layer.cs new file mode 100644 index 0000000..b361cd0 --- /dev/null +++ b/src/Geo.Radar/Abstractions/Enums/Layer.cs @@ -0,0 +1,58 @@ +// +// Copyright (c) Geo.NET. +// Licensed under the MIT license. See the LICENSE file in the solution root for full license information. +// + +namespace Geo.Radar +{ + /// + /// The layers to return. + /// + public enum Layer + { + /// + /// Return the place information. + /// + Place, + + /// + /// Return the address information. + /// + Address, + + /// + /// Return the postal code information. + /// + PostalCode, + + /// + /// Return the locality information. + /// + Locality, + + /// + /// Return the county information. + /// + County, + + /// + /// Return the state information. + /// + State, + + /// + /// Return the country information. + /// + Country, + + /// + /// Coarse includes all of postalCode, locality, county, state, and country. + /// + Coarse, + + /// + /// Fine includes address and place + /// + Fine, + } +} diff --git a/src/Geo.Radar/Abstractions/IRadarGeocoding.cs b/src/Geo.Radar/Abstractions/IRadarGeocoding.cs new file mode 100644 index 0000000..bc910b2 --- /dev/null +++ b/src/Geo.Radar/Abstractions/IRadarGeocoding.cs @@ -0,0 +1,46 @@ +// +// Copyright (c) Geo.NET. +// Licensed under the MIT license. See the LICENSE file in the solution root for full license information. +// + +namespace Geo.Radar +{ + using System.Threading; + using System.Threading.Tasks; + using Geo.Core.Models.Exceptions; + using Geo.Radar.Models.Parameters; + using Geo.Radar.Models.Responses; + + /// + /// An interface for calling the Radar geocoding methods. + /// + public interface IRadarGeocoding + { + /// + /// Calls the Radar 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 Radar. + /// Thrown for multiple different reasons. Check the inner exception for more information. + Task> GeocodingAsync(GeocodingParameters parameters, CancellationToken cancellationToken = default); + + /// + /// Calls the Radar 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 Radar. + /// Thrown for multiple different reasons. Check the inner exception for more information. + Task> ReverseGeocodingAsync(ReverseGeocodingParameters parameters, CancellationToken cancellationToken = default); + + /// + /// Calls the Radar autocomplete API and returns the results. + /// + /// A with the parameters of the request. + /// A used to cancel the request. + /// A of with the response from Radar. + /// Thrown for multiple different reasons. Check the inner exception for more information. + Task> AutocompleteAsync(AutocompleteParameters parameters, CancellationToken cancellationToken = default); + } +} diff --git a/src/Geo.Radar/Abstractions/Models/ICountryParameter.cs b/src/Geo.Radar/Abstractions/Models/ICountryParameter.cs new file mode 100644 index 0000000..07d73e3 --- /dev/null +++ b/src/Geo.Radar/Abstractions/Models/ICountryParameter.cs @@ -0,0 +1,20 @@ +// +// Copyright (c) Geo.NET. +// Licensed under the MIT license. See the LICENSE file in the solution root for full license information. +// + +namespace Geo.Radar.Models +{ + using System.Collections.Generic; + + /// + /// A country filter for a query. + /// + public interface ICountryParameter + { + /// + /// Gets a list of countries to filter the request by. It uses the unique 2-letter country code (https://radar.com/documentation/regions/countries). Optional. + /// + IList Countries { get; } + } +} diff --git a/src/Geo.Radar/Abstractions/Models/ILayersParameter.cs b/src/Geo.Radar/Abstractions/Models/ILayersParameter.cs new file mode 100644 index 0000000..57e2b84 --- /dev/null +++ b/src/Geo.Radar/Abstractions/Models/ILayersParameter.cs @@ -0,0 +1,20 @@ +// +// Copyright (c) Geo.NET. +// Licensed under the MIT license. See the LICENSE file in the solution root for full license information. +// + +namespace Geo.Radar.Models +{ + using System.Collections.Generic; + + /// + /// A layers representation for a query. + /// + public interface ILayersParameter + { + /// + /// Gets what layers to return. If not provided, results from address and coarse layers will be returned. Optional. + /// + IList Layers { get; } + } +} diff --git a/src/Geo.Radar/Converters/CoordinateConverter.cs b/src/Geo.Radar/Converters/CoordinateConverter.cs new file mode 100644 index 0000000..82d016c --- /dev/null +++ b/src/Geo.Radar/Converters/CoordinateConverter.cs @@ -0,0 +1,98 @@ +// +// Copyright (c) Geo.NET. +// Licensed under the MIT license. See the LICENSE file in the solution root for full license information. +// + +namespace Geo.Radar.Converters +{ + using System; + using System.Collections.Generic; + using System.Globalization; + using System.Text.Json; + using System.Text.Json.Serialization; + using Geo.Core.Extensions; + using Geo.Radar.Models; + + /// + /// A converter for a [] to a . + /// + public class CoordinateConverter : JsonConverter + { + /// + public override Coordinate Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (typeToConvert == null) + { + throw new ArgumentNullException(nameof(typeToConvert)); + } + + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + if (reader.TokenType == JsonTokenType.Null) + { + return null; + } + + if (reader.TokenType != JsonTokenType.StartArray) + { + throw new JsonException(string.Format(CultureInfo.InvariantCulture, "Unexpected token while parsing the coordinate. Expected to find an array, instead found {0}", reader.TokenType.GetName())); + } + + var coordinate = new List(); + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndArray) + { + break; + } + else if (reader.TokenType == JsonTokenType.Number) + { + coordinate.Add(reader.GetDouble()); + } + else + { + throw new JsonException(string.Format(CultureInfo.InvariantCulture, "Unexpected token while parsing the coordinate. Expected to find a double, instead found '{0}'", reader.GetString())); + } + } + + if (coordinate.Count != 2) + { + throw new JsonException(string.Format(CultureInfo.InvariantCulture, "Unexpected end of array while parsing the coordinate. Expected to find a 2 doubles, instead found {0}", coordinate.Count)); + } + + return new Coordinate() + { + Latitude = coordinate[0], + Longitude = coordinate[1], + }; + } + + /// + public override void Write(Utf8JsonWriter writer, Coordinate value, JsonSerializerOptions options) + { + if (writer == null) + { + throw new ArgumentNullException(nameof(writer)); + } + + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + if (value == null) + { + writer.WriteNullValue(); + return; + } + + writer.WriteStartArray(); + writer.WriteNumberValue(value.Latitude); + writer.WriteNumberValue(value.Longitude); + writer.WriteEndArray(); + } + } +} diff --git a/src/Geo.Radar/DependencyInjection/ServiceCollectionExtensions.cs b/src/Geo.Radar/DependencyInjection/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..a088828 --- /dev/null +++ b/src/Geo.Radar/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.Radar; + using Geo.Radar.Services; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.Options; + + /// + /// Extension methods for the class. + /// + public static class ServiceCollectionExtensions + { + /// + /// Adds the Radar geocoding services to the service collection. + /// + /// Adds the services: + /// + /// of + /// + /// + /// + /// + /// An to add the Radar services to. + /// An to configure the Radar geocoding. + /// Thrown if is null. + public static KeyBuilder AddRadarGeocoding(this IServiceCollection services) + { + if (services == null) + { + throw new ArgumentNullException(nameof(services)); + } + + services.AddKeyOptions(); + + return new KeyBuilder(services.AddHttpClient()); + } + } +} diff --git a/src/Geo.Radar/Extensions/LoggerExtensions.cs b/src/Geo.Radar/Extensions/LoggerExtensions.cs new file mode 100644 index 0000000..aa31d2d --- /dev/null +++ b/src/Geo.Radar/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.Radar +{ + using System; + using Geo.Radar.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(RadarGeocoding)), + "RadarGeocoding: {ErrorMessage}"); + + private static readonly Action _warning = LoggerMessage.Define( + LogLevel.Warning, + new EventId(2, nameof(RadarGeocoding)), + "RadarGeocoding: {WarningMessage}"); + + private static readonly Action _debug = LoggerMessage.Define( + LogLevel.Debug, + new EventId(3, nameof(RadarGeocoding)), + "RadarGeocoding: {DebugMessage}"); + + /// + /// "RadarGeocoding: {ErrorMessage}". + /// + /// An used to log the error message. + /// The error message to log. + public static void RadarError(this ILogger logger, string errorMessage) + { + _error(logger, errorMessage, null); + } + + /// + /// "RadarGeocoding: {WarningMessage}". + /// + /// An used to log the warning message. + /// The warning message to log. + public static void RadarWarning(this ILogger logger, string warningMessage) + { + _warning(logger, warningMessage, null); + } + + /// + /// "RadarGeocoding: {DebugMessage}". + /// + /// An used to log the debug message. + /// The debug message to log. + public static void RadarDebug(this ILogger logger, string debugMessage) + { + _debug(logger, debugMessage, null); + } + } +} diff --git a/src/Geo.Radar/Geo.Radar.csproj b/src/Geo.Radar/Geo.Radar.csproj new file mode 100644 index 0000000..bdedb76 --- /dev/null +++ b/src/Geo.Radar/Geo.Radar.csproj @@ -0,0 +1,45 @@ + + + + netstandard2.0;net6.0;net8.0 + Justin Canton + Geo.NET + Geo.NET Radar + geocoding geo.net Radar + A lightweight method for communicating with the Radar geocoding APIs. This includes models and interfaces for calling Radar. + MIT + https://github.com/JustinCanton/Geo.NET + true + README.md + + + + + + + + + + + True + True + RadarGeocoding.resx + + + + + + ResXFileCodeGenerator + RadarGeocoding.Designer.cs + + + + + + True + \ + Always + + + + diff --git a/src/Geo.Radar/Models/Coordinate.cs b/src/Geo.Radar/Models/Coordinate.cs new file mode 100644 index 0000000..dafd973 --- /dev/null +++ b/src/Geo.Radar/Models/Coordinate.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.Radar.Models +{ + using System.Globalization; + using System.Text.Json.Serialization; + using Geo.Radar.Converters; + + /// + /// The coordinates (latitude, longitude) of a pin on a map corresponding to the searched place. + /// + [JsonConverter(typeof(CoordinateConverter))] + public class Coordinate + { + /// + /// 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; } + + /// + public override string ToString() + { + return string.Format(CultureInfo.InvariantCulture, "{0},{1}", Latitude, Longitude); + } + } +} diff --git a/src/Geo.Radar/Models/Parameters/AutocompleteParameters.cs b/src/Geo.Radar/Models/Parameters/AutocompleteParameters.cs new file mode 100644 index 0000000..a63abed --- /dev/null +++ b/src/Geo.Radar/Models/Parameters/AutocompleteParameters.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.Radar.Models.Parameters +{ + using System.Collections.Generic; + + /// + /// The parameters possible to use during an autocomplete request. + /// + public class AutocompleteParameters : ICountryParameter, ILayersParameter, IKeyParameters + { + /// + /// Gets or sets the partial address or place name to autocomplete. + /// + public string Query { get; set; } + + /// + /// Gets or sets the location to prefer search results near. If not provided, the request IP geolocation will be used to anchor the search. Optional. + /// + public Coordinate Near { get; set; } + + /// + public IList Countries { get; } = new List(); + + /// + public IList Layers { get; } = new List(); + + /// + /// Gets or sets the max number of addresses to return. A number between 1 and 100. Defaults to 10. Optional. + /// + public uint Limit { get; set; } = 10; + + /// + /// Gets or sets a value indicating whether to return validated addresses. Only available for US and Canada addresses for enterprise customers. Optional. + /// + public bool Mailable { get; set; } = false; + + /// + public string Key { get; set; } + } +} diff --git a/src/Geo.Radar/Models/Parameters/GeocodingParameters.cs b/src/Geo.Radar/Models/Parameters/GeocodingParameters.cs new file mode 100644 index 0000000..c2acbb8 --- /dev/null +++ b/src/Geo.Radar/Models/Parameters/GeocodingParameters.cs @@ -0,0 +1,30 @@ +// +// Copyright (c) Geo.NET. +// Licensed under the MIT license. See the LICENSE file in the solution root for full license information. +// + +namespace Geo.Radar.Models.Parameters +{ + using System.Collections.Generic; + using Geo.Radar.Models; + + /// + /// The parameters possible to use during a geocoding request. + /// + public class GeocodingParameters : ICountryParameter, ILayersParameter, IKeyParameters + { + /// + /// Gets or sets the address to geocode. + /// + public string Query { get; set; } + + /// + public IList Countries { get; } = new List(); + + /// + public IList Layers { get; } = new List(); + + /// + public string Key { get; set; } + } +} diff --git a/src/Geo.Radar/Models/Parameters/ReverseGeocodingParameters.cs b/src/Geo.Radar/Models/Parameters/ReverseGeocodingParameters.cs new file mode 100644 index 0000000..784a65c --- /dev/null +++ b/src/Geo.Radar/Models/Parameters/ReverseGeocodingParameters.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.Radar.Models.Parameters +{ + using System.Collections.Generic; + using Geo.Radar.Models; + + /// + /// The parameters possible to use during a reverse geocoding request. + /// + public class ReverseGeocodingParameters : ILayersParameter, IKeyParameters + { + /// + /// Gets or sets the coordinates to reverse geocode. + /// + public Coordinate Coordinate { get; set; } + + /// + public IList Layers { get; } = new List(); + + /// + public string Key { get; set; } + } +} diff --git a/src/Geo.Radar/Models/Responses/Address.cs b/src/Geo.Radar/Models/Responses/Address.cs new file mode 100644 index 0000000..058f57a --- /dev/null +++ b/src/Geo.Radar/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.Radar.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.Radar/Models/Responses/GeocodeAddress.cs b/src/Geo.Radar/Models/Responses/GeocodeAddress.cs new file mode 100644 index 0000000..04803e1 --- /dev/null +++ b/src/Geo.Radar/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.Radar.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.Radar/Models/Responses/Geometry.cs b/src/Geo.Radar/Models/Responses/Geometry.cs new file mode 100644 index 0000000..5e1013a --- /dev/null +++ b/src/Geo.Radar/Models/Responses/Geometry.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.Radar.Models.Responses +{ + using System.Text.Json.Serialization; + + /// + /// 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.Radar/Models/Responses/Meta.cs b/src/Geo.Radar/Models/Responses/Meta.cs new file mode 100644 index 0000000..f7ace53 --- /dev/null +++ b/src/Geo.Radar/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.Radar.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.Radar/Models/Responses/Response.cs b/src/Geo.Radar/Models/Responses/Response.cs new file mode 100644 index 0000000..87ff14c --- /dev/null +++ b/src/Geo.Radar/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.Radar.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.Radar/Models/Responses/ReverseGeocodeAddress.cs b/src/Geo.Radar/Models/Responses/ReverseGeocodeAddress.cs new file mode 100644 index 0000000..391467e --- /dev/null +++ b/src/Geo.Radar/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.Radar.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.Radar/Properties/AssemblyInfo.cs b/src/Geo.Radar/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..cd2805a --- /dev/null +++ b/src/Geo.Radar/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.Radar.Tests")] +[assembly: ResourceLocation("Resources")] diff --git a/src/Geo.Radar/README.md b/src/Geo.Radar/README.md new file mode 100644 index 0000000..ce90d78 --- /dev/null +++ b/src/Geo.Radar/README.md @@ -0,0 +1,40 @@ +# Radar Geocoding + +This allows the simple calling of Radar geocoding APIs. The supported Radar geocoding endpoints are: +- [Geocoding](https://api.radar.io/v1/geocode/forward) +- [Reverse Geocoding](https://api.radar.io/v1/geocode/reverse) +- [Autocomplete](https://api.radar.io/v1/search/autocomplete) + +## Configuration + +In the startup `ConfigureServices` method, add the configuration for the Radar service: +``` +using Geo.Extensions.DependencyInjection; +. +. +. +public void ConfigureServices(IServiceCollection services) +{ + . + . + . + var builder = services.AddRadarGeocoding(); + builder.AddKey(your_Radar_api_key_here); + builder.HttpClientBuilder.ConfigureHttpClient(configure_client); + . + . + . +} +``` + +## Sample Usage + +By calling `AddRadarGeocoding`, the `IRadarGeocoding` interface has been added to the IOC container. Just request it as a DI item: +``` +public MyService(IRadarGeocoding RadarGeocoding) +{ + ... +} +``` + +Now simply call the geocoding methods in the interface. \ No newline at end of file diff --git a/src/Geo.Radar/Resources/Services/RadarGeocoding.Designer.cs b/src/Geo.Radar/Resources/Services/RadarGeocoding.Designer.cs new file mode 100644 index 0000000..b6d3d14 --- /dev/null +++ b/src/Geo.Radar/Resources/Services/RadarGeocoding.Designer.cs @@ -0,0 +1,135 @@ +//------------------------------------------------------------------------------ +// +// 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.Radar.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 RadarGeocoding { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal RadarGeocoding() { + } + + /// + /// 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.Radar.Resources.Services.RadarGeocoding", typeof(RadarGeocoding).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 Radar 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 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 layers are invalid and will not be used.. + /// + internal static string Invalid_Layers { + get { + return ResourceManager.GetString("Invalid Layers", 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 near coordinates are invalid and will not be used.. + /// + internal static string Invalid_Near { + get { + return ResourceManager.GetString("Invalid Near", 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 Radar parameters are null.. + /// + internal static string Null_Parameters { + get { + return ResourceManager.GetString("Null Parameters", resourceCulture); + } + } + } +} diff --git a/src/Geo.Radar/Resources/Services/RadarGeocoding.en.resx b/src/Geo.Radar/Resources/Services/RadarGeocoding.en.resx new file mode 100644 index 0000000..97134bf --- /dev/null +++ b/src/Geo.Radar/Resources/Services/RadarGeocoding.en.resx @@ -0,0 +1,144 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 Radar uri. + + + The coordinates cannot be null or invalid. + + + The country is invalid and will not be used. + + + The layers are invalid and will not be used. + + + The limit is invalid and will not be used. + + + The near coordinates are invalid and will not be used. + + + The query cannot be null or invalid. + + + The Radar parameters are null. + + \ No newline at end of file diff --git a/src/Geo.Radar/Resources/Services/RadarGeocoding.resx b/src/Geo.Radar/Resources/Services/RadarGeocoding.resx new file mode 100644 index 0000000..97134bf --- /dev/null +++ b/src/Geo.Radar/Resources/Services/RadarGeocoding.resx @@ -0,0 +1,144 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 Radar uri. + + + The coordinates cannot be null or invalid. + + + The country is invalid and will not be used. + + + The layers are invalid and will not be used. + + + The limit is invalid and will not be used. + + + The near coordinates are invalid and will not be used. + + + The query cannot be null or invalid. + + + The Radar parameters are null. + + \ No newline at end of file diff --git a/src/Geo.Radar/Services/RadarGeocoding.cs b/src/Geo.Radar/Services/RadarGeocoding.cs new file mode 100644 index 0000000..c840bed --- /dev/null +++ b/src/Geo.Radar/Services/RadarGeocoding.cs @@ -0,0 +1,273 @@ +// +// Copyright (c) Geo.NET. +// Licensed under the MIT license. See the LICENSE file in the solution root for full license information. +// + +namespace Geo.Radar.Services +{ + using System; + using System.Linq; + using System.Net.Http; + using System.Net.Http.Headers; + using System.Threading; + using System.Threading.Tasks; + using Geo.Core; + using Geo.Core.Extensions; + using Geo.Core.Models.Exceptions; + using Geo.Radar.Models; + using Geo.Radar.Models.Parameters; + using Geo.Radar.Models.Responses; + using Microsoft.Extensions.Logging; + using Microsoft.Extensions.Logging.Abstractions; + using Microsoft.Extensions.Options; + + /// + /// A service to call the Radar geocoding API. + /// + public class RadarGeocoding : GeoClient, IRadarGeocoding + { + private const string GeocodeUri = "https://api.radar.io/v1/geocode/forward"; + private const string ReverseGeocodeUri = "https://api.radar.io/v1/geocode/reverse"; + private const string AutocompleteUri = "https://api.radar.io/v1/search/autocomplete"; + + private readonly IOptions> _options; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// A used for placing calls to the Radar Geocoding API. + /// An of containing Radar key information. + /// An used to create a logger used for logging information. + public RadarGeocoding( + 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 => "Radar"; + + /// + 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); + } + + /// + public async Task> AutocompleteAsync( + AutocompleteParameters parameters, + CancellationToken cancellationToken = default) + { + var uri = ValidateAndBuildUri(parameters, BuildAutocompleteRequest); + + 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.RadarError(Resources.Services.RadarGeocoding.Null_Parameters); + throw new GeoNETException(Resources.Services.RadarGeocoding.Null_Parameters, new ArgumentNullException(nameof(parameters))); + } + + try + { + return uriBuilderFunction(parameters); + } + catch (ArgumentException ex) + { + _logger.RadarError(Resources.Services.RadarGeocoding.Failed_To_Create_Uri); + throw new GeoNETException(Resources.Services.RadarGeocoding.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 Radar 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.RadarError(Resources.Services.RadarGeocoding.Invalid_Query); + throw new ArgumentException(Resources.Services.RadarGeocoding.Invalid_Query, nameof(parameters.Query)); + } + + query = query.Add("query", parameters.Query); + + AddCountry(parameters, ref query); + AddLayers(parameters, ref query); + AddRadarKey(parameters); + + 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 Radar 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.RadarError(Resources.Services.RadarGeocoding.Invalid_Coordinates); + throw new ArgumentException(Resources.Services.RadarGeocoding.Invalid_Coordinates, nameof(parameters.Coordinate)); + } + + query = query.Add("coordinates", parameters.Coordinate.ToString()); + + AddLayers(parameters, ref query); + AddRadarKey(parameters); + + uriBuilder.AddQuery(query); + + return uriBuilder.Uri; + } + + /// + /// Builds the autocomplete uri based on the passed parameters. + /// + /// A with the autocomplete parameters to build the uri with. + /// A with the completed Radar autocomplete uri. + /// Thrown when the parameter is null or invalid. + internal Uri BuildAutocompleteRequest(AutocompleteParameters parameters) + { + var uriBuilder = new UriBuilder(AutocompleteUri); + var query = QueryString.Empty; + + if (string.IsNullOrWhiteSpace(parameters.Query)) + { + _logger.RadarError(Resources.Services.RadarGeocoding.Invalid_Query); + throw new ArgumentException(Resources.Services.RadarGeocoding.Invalid_Query, nameof(parameters.Query)); + } + + query = query.Add("query", parameters.Query); + + if (parameters.Near != null) + { + query = query.Add("near", parameters.Near.ToString()); + } + else + { + _logger.RadarDebug(Resources.Services.RadarGeocoding.Invalid_Near); + } + + if (parameters.Limit > 0 || parameters.Limit <= 100) + { + query = query.Add("limit", parameters.Limit.ToString()); + } + else + { + _logger.RadarDebug(Resources.Services.RadarGeocoding.Invalid_Limit); + } + + query = query.Add("mailable", parameters.Mailable.ToString().ToLowerInvariant()); + + AddCountry(parameters, ref query); + AddLayers(parameters, ref query); + AddRadarKey(parameters); + + uriBuilder.AddQuery(query); + + return uriBuilder.Uri; + } + + /// + /// Adds the country filter information to the query. + /// + /// The used to get the country filter information from. + /// A with the query parameters. + internal void AddCountry(ICountryParameter countryParameter, ref QueryString query) + { + var countries = string.Join(",", countryParameter.Countries ?? Array.Empty()); + + if (!string.IsNullOrWhiteSpace(countries)) + { + query = query.Add("country", countries); + } + else + { + _logger.RadarDebug(Resources.Services.RadarGeocoding.Invalid_Country); + } + } + + /// + /// Adds the layers filter information to the query. + /// + /// The used to get the country filter information from. + /// A with the query parameters. + internal void AddLayers(ILayersParameter layersParameter, ref QueryString query) + { + var layers = string.Join(",", layersParameter.Layers.Select(x => + { + var name = x.GetName(); + return char.ToLowerInvariant(name[0]) + name.Substring(1); + }) ?? Array.Empty()); + + if (!string.IsNullOrWhiteSpace(layers)) + { + query = query.Add("layers", layers); + } + else + { + _logger.RadarDebug(Resources.Services.RadarGeocoding.Invalid_Layers); + } + } + + /// + /// Adds the Radar key to the request. + /// + /// An to conditionally get the key from. + internal void AddRadarKey(IKeyParameters keyParameter) + { + var key = _options.Value.Key; + + if (!string.IsNullOrWhiteSpace(keyParameter.Key)) + { + key = keyParameter.Key; + } + + Client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(key); + } + } +} diff --git a/test/Geo.Radar.Tests/Converters/CoordinateConverterShould.cs b/test/Geo.Radar.Tests/Converters/CoordinateConverterShould.cs new file mode 100644 index 0000000..88c03de --- /dev/null +++ b/test/Geo.Radar.Tests/Converters/CoordinateConverterShould.cs @@ -0,0 +1,30 @@ +// +// Copyright (c) Geo.NET. +// Licensed under the MIT license. See the LICENSE file in the solution root for full license information. +// + +namespace Geo.Radar.Tests.Converters +{ + using System.Text.Json; + using FluentAssertions; + using Geo.Radar.Converters; + using Geo.Radar.Tests.Models; + using Xunit; + + /// + /// Unit tests for the class. + /// + public class CoordinateConverterShould + { + /// + /// Tests the double array is successfully translated to a coordinate. + /// + [Fact] + public void CorrectlyParseCoordinate() + { + var obj = JsonSerializer.Deserialize("{\"Coordinate\":[-73.996387763584124,40.752777282429321]}"); + obj.Coordinate.Longitude.Should().Be(40.752777282429321); + obj.Coordinate.Latitude.Should().Be(-73.996387763584124); + } + } +} \ No newline at end of file diff --git a/test/Geo.Radar.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs b/test/Geo.Radar.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs new file mode 100644 index 0000000..de63672 --- /dev/null +++ b/test/Geo.Radar.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.Radar.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 AddRadarGeocoding_WithValidCall_ConfiguresAllServices() + { + // Arrange + var services = new ServiceCollection(); + + // Act + var builder = services.AddRadarGeocoding(); + 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 AddRadarGeocoding_WithNullOptions_ConfiguresAllServices() + { + // Arrange + var services = new ServiceCollection(); + + // Act + services.AddRadarGeocoding(); + + // 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 AddRadarGeocoding_WithClientConfiguration_ConfiguresHttpClientAllServices() + { + // Arrange + var services = new ServiceCollection(); + + // Act + var builder = services.AddRadarGeocoding(); + builder.AddKey("abc"); + builder.HttpClientBuilder.ConfigureHttpClient(httpClient => httpClient.Timeout = TimeSpan.FromSeconds(5)); + + // Assert + var provider = services.BuildServiceProvider(); + var client = provider.GetRequiredService().CreateClient("IRadarGeocoding"); + client.Timeout.Should().Be(TimeSpan.FromSeconds(5)); + } + } +} diff --git a/test/Geo.Radar.Tests/Geo.Radar.Tests.csproj b/test/Geo.Radar.Tests/Geo.Radar.Tests.csproj new file mode 100644 index 0000000..cf9b060 --- /dev/null +++ b/test/Geo.Radar.Tests/Geo.Radar.Tests.csproj @@ -0,0 +1,15 @@ + + + + net48;netcoreapp3.1;net6.0;net8.0 + + false + + + + + + + + + diff --git a/test/Geo.Radar.Tests/GlobalSuppressions.cs b/test/Geo.Radar.Tests/GlobalSuppressions.cs new file mode 100644 index 0000000..7c34f80 --- /dev/null +++ b/test/Geo.Radar.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.Radar.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.Radar.Tests")] diff --git a/test/Geo.Radar.Tests/Models/CoordinateObject.cs b/test/Geo.Radar.Tests/Models/CoordinateObject.cs new file mode 100644 index 0000000..c1eff53 --- /dev/null +++ b/test/Geo.Radar.Tests/Models/CoordinateObject.cs @@ -0,0 +1,14 @@ +// +// Copyright (c) Geo.NET. +// Licensed under the MIT license. See the LICENSE file in the solution root for full license information. +// + +namespace Geo.Radar.Tests.Models +{ + using Geo.Radar.Models; + + public class CoordinateObject + { + public Coordinate Coordinate { get; set; } + } +} diff --git a/test/Geo.Radar.Tests/Services/RadarGeocodingShould.cs b/test/Geo.Radar.Tests/Services/RadarGeocodingShould.cs new file mode 100644 index 0000000..a288754 --- /dev/null +++ b/test/Geo.Radar.Tests/Services/RadarGeocodingShould.cs @@ -0,0 +1,480 @@ +// +// Copyright (c) Geo.NET. +// Licensed under the MIT license. See the LICENSE file in the solution root for full license information. +// + +namespace Geo.Radar.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.Radar.Models; + using Geo.Radar.Models.Parameters; + using Geo.Radar.Services; + using Microsoft.Extensions.Localization; + using Microsoft.Extensions.Options; + using Moq; + using Moq.Protected; + using Xunit; + + /// + /// Unit tests for the class. + /// + public class RadarGeocodingShould : 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 RadarGeocodingShould() + { + _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 AddRadarKey_WithOptions_SuccessfullyAddsKey() + { + var sut = BuildService(); + + sut.AddRadarKey(new GeocodingParameters()); + + _httpClient.DefaultRequestHeaders.Authorization.Scheme.Should().Be("abc123"); + } + + [Fact] + public void AddHereKey_WithParameterOverride_SuccessfullyAddsKey() + { + var sut = BuildService(); + + sut.AddRadarKey(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 RadarGeocoding BuildService() + { + return new RadarGeocoding(_httpClient, _options.Object); + } + } +} \ No newline at end of file diff --git a/test/Geo.Radar.Tests/TestData/CultureTestData.cs b/test/Geo.Radar.Tests/TestData/CultureTestData.cs new file mode 100644 index 0000000..56c02b8 --- /dev/null +++ b/test/Geo.Radar.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.Radar.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 205676ba8a27451caf9000333fb0d8f67223b796 Mon Sep 17 00:00:00 2001 From: Justin Canton <67930245+JustinCanton@users.noreply.github.com> Date: Mon, 20 May 2024 12:30:30 -0400 Subject: [PATCH 2/4] feat(positionstack): adding support for the positionstack geocoding API (#102) --- CHANGELOG.md | 3 +- Geo.NET.sln | 14 + README.md | 4 + .../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 | 141 +++++++ .../Models/Responses/BoundingBoxModule.cs | 39 ++ .../Models/Responses/CountryModule.cs | 88 +++++ .../Models/Responses/Currency.cs | 45 +++ .../Models/Responses/Data.cs | 22 ++ .../Models/Responses/GlobalInformation.cs | 75 ++++ .../Models/Responses/Language.cs | 27 ++ .../Models/Responses/PhoneInformation.cs | 33 ++ .../Models/Responses/Response.cs | 21 ++ .../Models/Responses/SunInformation.cs | 39 ++ .../Models/Responses/SunModule.cs | 33 ++ .../Models/Responses/TimezoneModule.cs | 33 ++ .../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 | 272 ++++++++++++++ .../Services/MapBoxGeocodingShould.cs | 2 +- .../ServiceCollectionExtensionsTests.cs | 75 ++++ .../Geo.Positionstack.Tests.csproj | 15 + .../GlobalSuppressions.cs | 13 + .../Services/PositionstackGeocodingShould.cs | 348 ++++++++++++++++++ .../TestData/CultureTestData.cs | 36 ++ 36 files changed, 2334 insertions(+), 2 deletions(-) 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/BoundingBoxModule.cs create mode 100644 src/Geo.Positionstack/Models/Responses/CountryModule.cs create mode 100644 src/Geo.Positionstack/Models/Responses/Currency.cs create mode 100644 src/Geo.Positionstack/Models/Responses/Data.cs create mode 100644 src/Geo.Positionstack/Models/Responses/GlobalInformation.cs create mode 100644 src/Geo.Positionstack/Models/Responses/Language.cs create mode 100644 src/Geo.Positionstack/Models/Responses/PhoneInformation.cs create mode 100644 src/Geo.Positionstack/Models/Responses/Response.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 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/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) diff --git a/src/Geo.Positionstack/Abstractions/IPositionstackGeocoding.cs b/src/Geo.Positionstack/Abstractions/IPositionstackGeocoding.cs new file mode 100644 index 0000000..14a7efd --- /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 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 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..2a1c371 --- /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 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. + /// 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..4ee2df7 --- /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 Countries { 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..8229f8b --- /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 Countries { 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..1b488ff --- /dev/null +++ b/src/Geo.Positionstack/Models/Responses/Address.cs @@ -0,0 +1,141 @@ +// +// 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 coordinate associated with the location result. + /// + [JsonPropertyName("latitude")] + public double Latitude { get; set; } + + /// + /// Gets or sets the longitude coordinate associated with the location result. + /// + [JsonPropertyName("longitude")] + public double Longitude { get; set; } + + /// + /// Gets or sets the formatted place name or address. + /// + [JsonPropertyName("label")] + public string Label { get; set; } + + /// + /// Gets or sets the name of the location result. This could be a place name, address, postal code, and more. + /// + [JsonPropertyName("name")] + public string Name { get; set; } + + /// + /// Gets or sets the type of location result. + /// + [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 street or house number associated with the location result. + /// + [JsonPropertyName("number")] + public string Number { get; set; } + + /// + /// Gets or sets the street name associated with the location result. + /// + [JsonPropertyName("street")] + public string Street { get; set; } + + /// + /// Gets or sets the postal or ZIP code associated with the location result. + /// + [JsonPropertyName("postal_code")] + public string PostalCode { get; set; } + + /// + /// Gets or sets a confidence score between 0 (0% confidence) and 1 (100% confidence) associated with the location result. + /// + [JsonPropertyName("confidence")] + public double? Confidence { get; set; } + + /// + /// Gets or sets the name of the region associated with the location result. + /// + [JsonPropertyName("region")] + public string Region { get; set; } + + /// + /// 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 the localised country name. For example: "United States". + /// + [JsonPropertyName("country")] + public string Country { get; set; } + + /// + /// Gets or sets the ISO 3166 Alpha 2 (two letters) code of the country associated with the location result. + /// + [JsonPropertyName("country_code")] + public string CountryCode { get; set; } + + /// + /// Gets or sets an embeddable map associated with the location result. + /// + [JsonPropertyName("map_url")] + public string MapUrl { get; set; } + + /// + /// Gets or sets an extensive set of country information. + /// + [JsonPropertyName("country_module")] + public CountryModule CountryModule { get; set; } + + /// + /// Gets or sets astrology data for a location. + /// + [JsonPropertyName("sun_module")] + public SunModule SunModule { get; set; } + + /// + /// Gets or sets timezone information for a location. + /// + [JsonPropertyName("timezone_module")] + public TimezoneModule TimezoneModule { get; set; } + + /// + /// Gets or sets bounding box coordinates for a location. + /// + [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/Data.cs b/src/Geo.Positionstack/Models/Responses/Data.cs new file mode 100644 index 0000000..eb180d3 --- /dev/null +++ b/src/Geo.Positionstack/Models/Responses/Data.cs @@ -0,0 +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; + + /// + /// The data with the address results. + /// + public class Data + { + /// + /// Gets the address results. + /// + [JsonPropertyName("results")] + public IList
Results { get; } = new List
(); + } +} 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/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 new file mode 100644 index 0000000..4f19db9 --- /dev/null +++ b/src/Geo.Positionstack/Models/Responses/Response.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; + + /// + /// The result of a request. + /// + public class Response + { + /// + /// Gets or sets the data with the location information. + /// + [JsonPropertyName("data")] + public Data Data { 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/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..ece407b --- /dev/null +++ b/src/Geo.Positionstack/Services/PositionstackGeocoding.cs @@ -0,0 +1,272 @@ +// +// 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.Linq; + 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.Countries?.Count > 0) + { + var countries = string.Join(",", locationParameters.Countries ?? 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.Where(x => !string.IsNullOrWhiteSpace(x)) ?? 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.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/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..87c8f00 --- /dev/null +++ b/test/Geo.Positionstack.Tests/Services/PositionstackGeocodingShould.cs @@ -0,0 +1,348 @@ +// +// 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 bool _disposed; + + /// + /// Initializes a new instance of the class. + /// + public PositionstackGeocodingShould() + { + _options + .Setup(x => x.Value) + .Returns(new KeyOptions() + { + Key = "abc123", + }); + + var options = Options.Create(new LocalizationOptions { ResourcesPath = "Resources" }); + _httpClient = new HttpClient(new Mock().Object); + } + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + [Fact] + public void AddPositionstackKey_WithOptions_SuccessfullyAddsKey() + { + var sut = BuildService(); + + var query = QueryString.Empty; + + sut.AddPositionstackKey(new GeocodingParameters(), ref query); + + var queryParameters = HttpUtility.ParseQueryString(query.ToString()); + queryParameters.Count.Should().Be(1); + queryParameters["access_key"].Should().Be("abc123"); + } + + [Fact] + public void AddPositionstackKey_WithParameterOverride_SuccessfullyAddsKey() + { + var sut = BuildService(); + + var query = QueryString.Empty; + + sut.AddPositionstackKey(new GeocodingParameters() { Key = "123abc" }, ref query); + + var queryParameters = HttpUtility.ParseQueryString(query.ToString()); + queryParameters.Count.Should().Be(1); + queryParameters["access_key"].Should().Be("123abc"); + } + + [Fact] + public void AddFilterParameters_WithValidData_AddsSuccessfully() + { + var sut = BuildService(); + + var query = QueryString.Empty; + var parameters = new GeocodingParameters() + { + Language = "en", + CountryModule = true, + SunModule = true, + TimezoneModule = true, + BoundingBoxModule = true, + Limit = 7, + }; + + 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.AddFilterParameters(parameters, ref query); + + var queryParameters = HttpUtility.ParseQueryString(query.ToString()); + queryParameters.Count.Should().Be(1); + queryParameters["limit"].Should().Be("10"); + } + + [Fact] + public void AddLocationParameters_WithValidLocationInformation_AddsSuccessfully() + { + var sut = BuildService(); + + var query = QueryString.Empty; + var parameters = new GeocodingParameters() + { + Region = "Paris", + }; + + parameters.Countries.Add("CA"); + parameters.Countries.Add("FR"); + + sut.AddLocationParameters(parameters, ref query); + + var queryParameters = HttpUtility.ParseQueryString(query.ToString()); + queryParameters.Count.Should().Be(2); + queryParameters["country"].Should().Be("CA,FR"); + queryParameters["region"].Should().Be("Paris"); + } + + [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.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("access_key=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", + }; + + // Act + var uri = sut.BuildReverseGeocodingRequest(parameters); + + // Assert + var query = HttpUtility.UrlDecode(uri.PathAndQuery); + query.Should().Contain("query=56.78,78.91"); + query.Should().Contain("access_key=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 + } + + [Fact] + public void ValidateAndBuildUri_WithValidParameters_SuccessfullyBuildsUri() + { + var sut = BuildService(); + + var parameters = new ReverseGeocodingParameters() + { + Coordinate = new Coordinate() + { + Latitude = 56.78, + Longitude = 78.91, + }, + Key = "123abc", + }; + + var uri = sut.ValidateAndBuildUri(parameters, sut.BuildReverseGeocodingRequest); + var query = HttpUtility.UrlDecode(uri.PathAndQuery); + query.Should().Contain("query=56.78,78.91"); + query.Should().Contain("access_key=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 + } + + /// + /// 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(); + } + + _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 aeaabc7a0e1f69c524fd0141c92156b6f0a27658 Mon Sep 17 00:00:00 2001 From: JustinCanton <67930245+JustinCanton@users.noreply.github.com> Date: Mon, 20 May 2024 12:35:19 -0400 Subject: [PATCH 3/4] docs: updating the changelog --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a01930..0b0c043 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +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) ([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/)) +- **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)) ([205676b](https://github.com/JustinCanton/Geo.NET/commit/205676ba8a27451caf9000333fb0d8f67223b796)) ## [2.0.0](https://github.com/JustinCanton/Geo.NET/compare/1.6.0...2.0.0) (2024-01-28) ### ⚠ BREAKING CHANGES From f88b0d3b2efcd18e2f158c2a8af053f94a29268b Mon Sep 17 00:00:00 2001 From: JustinCanton <67930245+JustinCanton@users.noreply.github.com> Date: Mon, 20 May 2024 12:36:07 -0400 Subject: [PATCH 4/4] docs: updating the changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b0c043..f7f7380 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org/) for commit guidelines. -## [2.1.0](https://github.com/JustinCanton/Geo.NET/compare/2.0.0...2.1.0) (2024-06-30) +## [2.1.0](https://github.com/JustinCanton/Geo.NET/compare/2.0.0...2.1.0) (2024-05-20) ### Features - **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)) ([205676b](https://github.com/JustinCanton/Geo.NET/commit/205676ba8a27451caf9000333fb0d8f67223b796))