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