From 4456015020c2d91fec3b49f1107c1685928721e7 Mon Sep 17 00:00:00 2001 From: colinnuk Date: Thu, 29 Aug 2024 06:58:41 -0700 Subject: [PATCH 1/3] Add apiKey to the constructor and remove rethrowExceptions from the constructor (this will be set by a setter method instead) --- OpenMeteo/OpenMeteoClient.cs | 47 +++++++++++++++++++++----- OpenMeteoTests/WeatherForecastTests.cs | 8 +++-- 2 files changed, 43 insertions(+), 12 deletions(-) diff --git a/OpenMeteo/OpenMeteoClient.cs b/OpenMeteo/OpenMeteoClient.cs index cb8d057..45bf524 100644 --- a/OpenMeteo/OpenMeteoClient.cs +++ b/OpenMeteo/OpenMeteoClient.cs @@ -18,7 +18,13 @@ public class OpenMeteoClient private readonly HttpController httpController; private readonly IOpenMeteoLogger? _logger = default!; - private readonly bool _rethrowExceptions = false; + private readonly string _apiKey = string.Empty; + + /// + /// If set to true, exceptions from the OpenMeteo API will be rethrown. Default is false. + /// + /// + public bool RethrowExceptions { get; set; } = false; /// /// Creates a new object and sets the neccessary variables (httpController, CultureInfo) @@ -29,13 +35,36 @@ public OpenMeteoClient() } /// - /// Creates a new object and sets the neccessary variables (httpController, CultureInfo) + /// Creates a new object with a logger /// - public OpenMeteoClient(bool rethrowExceptions, IOpenMeteoLogger? logger = null) + /// An object which implements an interface that can be used for logging from this class + public OpenMeteoClient(IOpenMeteoLogger logger) { httpController = new HttpController(); _logger = logger; - _rethrowExceptions = rethrowExceptions; + } + + /// + /// Creates a new object with a logger and an API key + /// + /// The API key to use the customer OpenMeteo URLs such as https://customer-api.open-meteo.com + public OpenMeteoClient(string apiKey) + { + httpController = new HttpController(); + _apiKey = apiKey; + } + + /// + /// Creates a new object with a logger and an API key + /// + /// An object which implements an interface that can be used for logging from this class + /// The API key to use the customer OpenMeteo URLs such as https://customer-api.open-meteo.com + + public OpenMeteoClient(IOpenMeteoLogger logger, string apiKey) + { + httpController = new HttpController(); + _logger = logger; + _apiKey = apiKey; } /// @@ -45,7 +74,7 @@ public OpenMeteoClient(bool rethrowExceptions, IOpenMeteoLogger? logger = null) /// If successful returns an awaitable Task containing WeatherForecast or NULL if request failed public async Task QueryAsync(string location) { - GeocodingOptions geocodingOptions = new GeocodingOptions(location); + GeocodingOptions geocodingOptions = new(location); // Get location Information GeocodingApiResponse? response = await GetGeocodingDataAsync(geocodingOptions); @@ -234,7 +263,7 @@ public OpenMeteoClient(bool rethrowExceptions, IOpenMeteoLogger? logger = null) catch (HttpRequestException e) { _logger?.Warning($"{nameof(OpenMeteoClient)}.GetAirQualityAsync(). Message: {e.Message} StackTrace: {e.StackTrace}"); - if (_rethrowExceptions) + if (RethrowExceptions) throw; return null; } @@ -341,7 +370,7 @@ public string WeathercodeToString(int weathercode) catch (Exception e) { _logger?.Warning($"{nameof(OpenMeteoClient)}.GetWeatherForecastAsync(). Message: {e.Message} StackTrace: {e.StackTrace}"); - if (_rethrowExceptions) + if (RethrowExceptions) throw; return null; } @@ -365,7 +394,7 @@ public string WeathercodeToString(int weathercode) catch (HttpRequestException e) { _logger?.Warning($"{nameof(OpenMeteoClient)}.GetGeocodingDataAsync(). Message: {e.Message} StackTrace: {e.StackTrace}"); - if (_rethrowExceptions) + if (RethrowExceptions) throw; return null; } @@ -388,7 +417,7 @@ public string WeathercodeToString(int weathercode) { _logger?.Warning($"Can't find elevation for latitude {options.Latitude} & longitude {options.Longitude}. Please make sure that they are valid."); _logger?.Warning($"Error in {nameof(OpenMeteoClient)}.GetElevationAsync(). Message: {e.Message} StackTrace: {e.StackTrace}"); - if (_rethrowExceptions) + if (RethrowExceptions) throw; return null; } diff --git a/OpenMeteoTests/WeatherForecastTests.cs b/OpenMeteoTests/WeatherForecastTests.cs index 0e3301a..1b944ec 100644 --- a/OpenMeteoTests/WeatherForecastTests.cs +++ b/OpenMeteoTests/WeatherForecastTests.cs @@ -1,6 +1,5 @@ using System.Globalization; using System.Linq; -using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -130,9 +129,12 @@ public void WeatherForecast_With_All_Options_Test() [TestMethod] public async Task Latitude_Longitude_No_Data_For_Selected_Forecast_Rethrows_Test() { - OpenMeteoClient client = new(true); + OpenMeteoClient client = new() + { + RethrowExceptions = true + }; - WeatherForecastOptions options = new WeatherForecastOptions + WeatherForecastOptions options = new() { Latitude = 1, Longitude = 1, From 0ed740fa19cfeafc657bdb668fc5a29c4f1b3ff3 Mon Sep 17 00:00:00 2001 From: colinnuk Date: Thu, 29 Aug 2024 07:05:21 -0700 Subject: [PATCH 2/3] Move the WeatherCode method to its own class --- OpenMeteo/OpenMeteoClient.cs | 71 ------------------------ OpenMeteo/WeatherCodeHelper.cs | 44 +++++++++++++++ OpenMeteoTests/OpenMeteoClientTests.cs | 27 --------- OpenMeteoTests/WeatherCodeHelperTests.cs | 26 +++++++++ 4 files changed, 70 insertions(+), 98 deletions(-) create mode 100644 OpenMeteo/WeatherCodeHelper.cs delete mode 100644 OpenMeteoTests/OpenMeteoClientTests.cs create mode 100644 OpenMeteoTests/WeatherCodeHelperTests.cs diff --git a/OpenMeteo/OpenMeteoClient.cs b/OpenMeteo/OpenMeteoClient.cs index 45bf524..8368acf 100644 --- a/OpenMeteo/OpenMeteoClient.cs +++ b/OpenMeteo/OpenMeteoClient.cs @@ -268,77 +268,6 @@ public OpenMeteoClient(IOpenMeteoLogger logger, string apiKey) return null; } } - - /// - /// Converts a given weathercode to it's string representation - /// - /// - /// Weathercode string representation - public string WeathercodeToString(int weathercode) - { - switch (weathercode) - { - case 0: - return "Clear sky"; - case 1: - return "Mainly clear"; - case 2: - return "Partly cloudy"; - case 3: - return "Overcast"; - case 45: - return "Fog"; - case 48: - return "Depositing rime Fog"; - case 51: - return "Light drizzle"; - case 53: - return "Moderate drizzle"; - case 55: - return "Dense drizzle"; - case 56: - return "Light freezing drizzle"; - case 57: - return "Dense freezing drizzle"; - case 61: - return "Slight rain"; - case 63: - return "Moderate rain"; - case 65: - return "Heavy rain"; - case 66: - return "Light freezing rain"; - case 67: - return "Heavy freezing rain"; - case 71: - return "Slight snow fall"; - case 73: - return "Moderate snow fall"; - case 75: - return "Heavy snow fall"; - case 77: - return "Snow grains"; - case 80: - return "Slight rain showers"; - case 81: - return "Moderate rain showers"; - case 82: - return "Violent rain showers"; - case 85: - return "Slight snow showers"; - case 86: - return "Heavy snow showers"; - case 95: - return "Thunderstorm"; - case 96: - return "Thunderstorm with light hail"; - case 99: - return "Thunderstorm with heavy hail"; - default: - return "Invalid weathercode"; - } - } - private async Task GetWeatherForecastAsync(WeatherForecastOptions options) { try diff --git a/OpenMeteo/WeatherCodeHelper.cs b/OpenMeteo/WeatherCodeHelper.cs new file mode 100644 index 0000000..22e1f7a --- /dev/null +++ b/OpenMeteo/WeatherCodeHelper.cs @@ -0,0 +1,44 @@ +namespace OpenMeteo; +public static class WeatherCodeHelper +{ + /// + /// Converts a given weathercode to it's string representation + /// + /// + /// Weathercode string representation + public static string WeathercodeToString(int weathercode) + { + return weathercode switch + { + 0 => "Clear sky", + 1 => "Mainly clear", + 2 => "Partly cloudy", + 3 => "Overcast", + 45 => "Fog", + 48 => "Depositing rime Fog", + 51 => "Light drizzle", + 53 => "Moderate drizzle", + 55 => "Dense drizzle", + 56 => "Light freezing drizzle", + 57 => "Dense freezing drizzle", + 61 => "Slight rain", + 63 => "Moderate rain", + 65 => "Heavy rain", + 66 => "Light freezing rain", + 67 => "Heavy freezing rain", + 71 => "Slight snow fall", + 73 => "Moderate snow fall", + 75 => "Heavy snow fall", + 77 => "Snow grains", + 80 => "Slight rain showers", + 81 => "Moderate rain showers", + 82 => "Violent rain showers", + 85 => "Slight snow showers", + 86 => "Heavy snow showers", + 95 => "Thunderstorm", + 96 => "Thunderstorm with light hail", + 99 => "Thunderstorm with heavy hail", + _ => "Invalid weathercode", + }; + } +} diff --git a/OpenMeteoTests/OpenMeteoClientTests.cs b/OpenMeteoTests/OpenMeteoClientTests.cs deleted file mode 100644 index 68254d7..0000000 --- a/OpenMeteoTests/OpenMeteoClientTests.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; -using OpenMeteo; - -namespace OpenMeteoTests -{ - [TestClass] - public class OpenMeteoClientTests - { - [TestMethod] - public void Weather_Codes_To_String_Tests() - { - OpenMeteoClient client = new OpenMeteoClient(); - int[] testWeatherCodes = { 0, 1, 2, 3, 51, 53, 96, 99, 100 }; - foreach (var weatherCode in testWeatherCodes) - { - string weatherCodeString = client.WeathercodeToString(weatherCode); - Assert.IsInstanceOfType(weatherCodeString, typeof(string)); - - if (weatherCode == 0) - Assert.AreEqual("Clear sky", weatherCodeString); - - if (weatherCode == 100) - Assert.AreEqual("Invalid weathercode", weatherCodeString); - } - } - } -} \ No newline at end of file diff --git a/OpenMeteoTests/WeatherCodeHelperTests.cs b/OpenMeteoTests/WeatherCodeHelperTests.cs new file mode 100644 index 0000000..676412f --- /dev/null +++ b/OpenMeteoTests/WeatherCodeHelperTests.cs @@ -0,0 +1,26 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using OpenMeteo; + +namespace OpenMeteoTests +{ + [TestClass] + public class WeatherCodeHelperTests + { + [DataTestMethod] + [DataRow(0, "Clear sky")] + [DataRow(1, "Mainly clear")] + [DataRow(2, "Partly cloudy")] + [DataRow(3, "Overcast")] + [DataRow(51, "Light drizzle")] + [DataRow(53, "Moderate drizzle")] + [DataRow(96, "Thunderstorm with light hail")] + [DataRow(99, "Thunderstorm with heavy hail")] + [DataRow(100, "Invalid weathercode")] + public void Weather_Codes_To_String_Tests(int weatherCode, string expectedString) + { + string weatherCodeString = WeatherCodeHelper.WeathercodeToString(weatherCode); + Assert.IsInstanceOfType(weatherCodeString, typeof(string)); + Assert.AreEqual(expectedString, weatherCodeString); + } + } +} \ No newline at end of file From 7e802f04bf86d1e37d640905a9039245086ee37b Mon Sep 17 00:00:00 2001 From: colinnuk Date: Thu, 29 Aug 2024 08:30:34 -0700 Subject: [PATCH 3/3] Refactor URL builder code into its own class and support apiKeys --- OpenMeteo/ElevationOptions.cs | 2 +- OpenMeteo/OpenMeteoClient.cs | 276 +----------------------------- OpenMeteo/UrlFactory.cs | 245 ++++++++++++++++++++++++++ OpenMeteoTests/UrlFactoryTests.cs | 123 +++++++++++++ 4 files changed, 376 insertions(+), 270 deletions(-) create mode 100644 OpenMeteo/UrlFactory.cs create mode 100644 OpenMeteoTests/UrlFactoryTests.cs diff --git a/OpenMeteo/ElevationOptions.cs b/OpenMeteo/ElevationOptions.cs index 70459f8..2d0b449 100644 --- a/OpenMeteo/ElevationOptions.cs +++ b/OpenMeteo/ElevationOptions.cs @@ -1,6 +1,6 @@ namespace OpenMeteo { - internal class ElevationOptions + public class ElevationOptions { public ElevationOptions(float latitude, float longitude) { diff --git a/OpenMeteo/OpenMeteoClient.cs b/OpenMeteo/OpenMeteoClient.cs index 8368acf..835f621 100644 --- a/OpenMeteo/OpenMeteoClient.cs +++ b/OpenMeteo/OpenMeteoClient.cs @@ -2,7 +2,6 @@ using System.Net.Http; using System.Threading.Tasks; using System.Text.Json; -using System.Globalization; namespace OpenMeteo { @@ -11,14 +10,9 @@ namespace OpenMeteo /// public class OpenMeteoClient { - private readonly string _weatherApiUrl = "https://api.open-meteo.com/v1/forecast"; - private readonly string _geocodeApiUrl = "https://geocoding-api.open-meteo.com/v1/search"; - private readonly string _airQualityApiUrl = "https://air-quality-api.open-meteo.com/v1/air-quality"; - private readonly string _elevationApiUrl = "https://api.open-meteo.com/v1/elevation"; private readonly HttpController httpController; - + private readonly UrlFactory _urlFactory = new(); private readonly IOpenMeteoLogger? _logger = default!; - private readonly string _apiKey = string.Empty; /// /// If set to true, exceptions from the OpenMeteo API will be rethrown. Default is false. @@ -51,7 +45,7 @@ public OpenMeteoClient(IOpenMeteoLogger logger) public OpenMeteoClient(string apiKey) { httpController = new HttpController(); - _apiKey = apiKey; + _urlFactory = new UrlFactory(apiKey); } /// @@ -64,7 +58,7 @@ public OpenMeteoClient(IOpenMeteoLogger logger, string apiKey) { httpController = new HttpController(); _logger = logger; - _apiKey = apiKey; + _urlFactory = new UrlFactory(apiKey); } /// @@ -252,7 +246,7 @@ public OpenMeteoClient(IOpenMeteoLogger logger, string apiKey) { try { - var url = MergeUrlWithOptions(_airQualityApiUrl, options); + var url = _urlFactory.GetUrlWithOptions(options); _logger?.Debug($"{nameof(OpenMeteoClient)}.GetAirQualityAsync(). URL: {url}"); HttpResponseMessage response = await httpController.Client.GetAsync(url); response.EnsureSuccessStatusCode(); @@ -272,7 +266,7 @@ public OpenMeteoClient(IOpenMeteoLogger logger, string apiKey) { try { - var url = MergeUrlWithOptions(_weatherApiUrl, options); + var url = _urlFactory.GetUrlWithOptions(options); _logger?.Debug($"{nameof(OpenMeteoClient)}.GetElevationAsync(). URL: {url}"); HttpResponseMessage response = await httpController.Client.GetAsync(url); if(response.IsSuccessStatusCode) @@ -311,7 +305,7 @@ public OpenMeteoClient(IOpenMeteoLogger logger, string apiKey) try { - var url = MergeUrlWithOptions(_geocodeApiUrl, options); + var url = _urlFactory.GetUrlWithOptions(options); _logger?.Debug($"{nameof(OpenMeteoClient)}.GetGeocodingDataAsync(). URL: {url}"); HttpResponseMessage response = await httpController.Client.GetAsync(url); response.EnsureSuccessStatusCode(); @@ -333,7 +327,7 @@ public OpenMeteoClient(IOpenMeteoLogger logger, string apiKey) { try { - var url = MergeUrlWithOptions(_elevationApiUrl, options); + var url = _urlFactory.GetUrlWithOptions(options); _logger?.Debug($"{nameof(OpenMeteoClient)}.GetElevationAsync(). URL: {url}"); HttpResponseMessage response = await httpController.Client.GetAsync(url); response.EnsureSuccessStatusCode(); @@ -351,262 +345,6 @@ public OpenMeteoClient(IOpenMeteoLogger logger, string apiKey) return null; } } - - private string MergeUrlWithOptions(string url, WeatherForecastOptions? options) - { - if (options == null) return url; - - UriBuilder uri = new UriBuilder(url); - bool isFirstParam = false; - - // If no query given, add '?' to start the query string - if (uri.Query == string.Empty) - { - uri.Query = "?"; - - // isFirstParam becomes true because the query string is new - isFirstParam = true; - } - - // Add the properties - - // Begin with Latitude and Longitude since they're required - if (isFirstParam) - uri.Query += "latitude=" + options.Latitude.ToString(CultureInfo.InvariantCulture); - else - uri.Query += "&latitude=" + options.Latitude.ToString(CultureInfo.InvariantCulture); - - uri.Query += "&longitude=" + options.Longitude.ToString(CultureInfo.InvariantCulture); - - uri.Query += "&temperature_unit=" + options.Temperature_Unit.ToString(); - uri.Query += "&windspeed_unit=" + options.Windspeed_Unit.ToString(); - uri.Query += "&precipitation_unit=" + options.Precipitation_Unit.ToString(); - if (options.Timezone != string.Empty) - uri.Query += "&timezone=" + options.Timezone; - - uri.Query += "&timeformat=" + options.Timeformat.ToString(); - - uri.Query += "&past_days=" + options.Past_Days; - - if (options.Start_date != string.Empty) - uri.Query += "&start_date=" + options.Start_date; - if (options.End_date != string.Empty) - uri.Query += "&end_date=" + options.End_date; - - // Now we iterate through hourly and daily - - // Hourly - if (options.Hourly.Count > 0) - { - bool firstHourlyElement = true; - uri.Query += "&hourly="; - - foreach (var option in options.Hourly) - { - if (firstHourlyElement) - { - uri.Query += option.ToString(); - firstHourlyElement = false; - } - else - { - uri.Query += "," + option.ToString(); - } - } - } - - // Daily - if (options.Daily.Count > 0) - { - bool firstDailyElement = true; - uri.Query += "&daily="; - foreach (var option in options.Daily) - { - if (firstDailyElement) - { - uri.Query += option.ToString(); - firstDailyElement = false; - } - else - { - uri.Query += "," + option.ToString(); - } - } - } - - // 0.2.0 Weather models - // cell_selection - uri.Query += "&cell_selection=" + options.Cell_Selection; - - // Models - if (options.Models.Count > 0) - { - bool firstModelsElement = true; - uri.Query += "&models="; - foreach (var option in options.Models) - { - if (firstModelsElement) - { - uri.Query += option.ToString(); - firstModelsElement = false; - } - else - { - uri.Query += "," + option.ToString(); - } - } - } - - // new current parameter - if (options.Current.Count > 0) - { - bool firstCurrentElement = true; - uri.Query += "¤t="; - foreach (var option in options.Current) - { - if (firstCurrentElement) - { - uri.Query += option.ToString(); - firstCurrentElement = false; - } - else - { - uri.Query += "," + option.ToString(); - } - } - } - - // new minutely_15 parameter - if (options.Minutely15.Count > 0) - { - bool firstMinutelyElement = true; - uri.Query += "&minutely_15="; - foreach (var option in options.Minutely15) - { - if (firstMinutelyElement) - { - uri.Query += option.ToString(); - firstMinutelyElement = false; - } - else - { - uri.Query += "," + option.ToString(); - } - } - } - - return uri.ToString(); - } - - /// - /// Combines a given url with an options object to create a url for GET requests - /// - /// url+queryString - private string MergeUrlWithOptions(string url, GeocodingOptions options) - { - if (options == null) return url; - - UriBuilder uri = new UriBuilder(url); - bool isFirstParam = false; - - // If no query given, add '?' to start the query string - if (uri.Query == string.Empty) - { - uri.Query = "?"; - - // isFirstParam becomes true because the query string is new - isFirstParam = true; - } - - // Now we check every property and set the value, if neccessary - if (isFirstParam) - uri.Query += "name=" + options.Name; - else - uri.Query += "&name=" + options.Name; - - if(options.Count >0) - uri.Query += "&count=" + options.Count; - - if (options.Format != string.Empty) - uri.Query += "&format=" + options.Format; - - if (options.Language != string.Empty) - uri.Query += "&language=" + options.Language; - - return uri.ToString(); - } - - /// - /// Combines a given url with an options object to create a url for GET requests - /// - /// url+queryString - private string MergeUrlWithOptions(string url, AirQualityOptions options) - { - if (options == null) return url; - - UriBuilder uri = new UriBuilder(url); - bool isFirstParam = false; - - // If no query given, add '?' to start the query string - if (uri.Query == string.Empty) - { - uri.Query = "?"; - - // isFirstParam becomes true because the query string is new - isFirstParam = true; - } - - // Now we check every property and set the value, if neccessary - if (isFirstParam) - uri.Query += "latitude=" + options.Latitude.ToString(CultureInfo.InvariantCulture); - else - uri.Query += "&latitude=" + options.Latitude.ToString(CultureInfo.InvariantCulture); - - uri.Query += "&longitude=" + options.Longitude.ToString(CultureInfo.InvariantCulture); - - if (options.Domains != string.Empty) - uri.Query += "&domains=" + options.Domains; - - if (options.Timeformat != string.Empty) - uri.Query += "&timeformat=" + options.Timeformat; - - if (options.Timezone != string.Empty) - uri.Query += "&timezone=" + options.Timezone; - - // Finally add hourly array - if (options.Hourly.Count >= 0) - { - bool firstHourlyElement = true; - uri.Query += "&hourly="; - - foreach (var option in options.Hourly) - { - if (firstHourlyElement) - { - uri.Query += option.ToString(); - firstHourlyElement = false; - } - else - { - uri.Query += "," + option.ToString(); - } - } - } - - return uri.ToString(); - } - - private string MergeUrlWithOptions(string url, ElevationOptions options) - { - if (options == null) return url; - - UriBuilder uri = new UriBuilder(url) - { - Query = $"?latitude={options.Latitude.ToString(CultureInfo.InvariantCulture)}&longitude={options.Longitude.ToString(CultureInfo.InvariantCulture)}" - }; - - return uri.ToString(); - } } } diff --git a/OpenMeteo/UrlFactory.cs b/OpenMeteo/UrlFactory.cs new file mode 100644 index 0000000..a5cd343 --- /dev/null +++ b/OpenMeteo/UrlFactory.cs @@ -0,0 +1,245 @@ +using System; +using System.Globalization; + +namespace OpenMeteo; +public class UrlFactory +{ + private readonly string _weatherApiUrl = "api.open-meteo.com/v1/forecast"; + private readonly string _geocodeApiUrl = "geocoding-api.open-meteo.com/v1/search"; + private readonly string _airQualityApiUrl = "air-quality-api.open-meteo.com/v1/air-quality"; + private readonly string _elevationApiUrl = "api.open-meteo.com/v1/elevation"; + private readonly string _customerApiUrlFragment = "customer-"; + + private readonly string _apiKey = string.Empty; + + public UrlFactory() + { + } + + public UrlFactory(string apiKey) + { + _apiKey = apiKey; + } + + public string GetUrlWithOptions(WeatherForecastOptions options) + { + UriBuilder uri = new(GetBaseUrl(_weatherApiUrl)); + + // Add the properties + // Begin with Latitude and Longitude since they're required + uri.Query = "latitude=" + options.Latitude.ToString(CultureInfo.InvariantCulture); + uri.Query += "&longitude=" + options.Longitude.ToString(CultureInfo.InvariantCulture); + + uri.Query += "&temperature_unit=" + options.Temperature_Unit.ToString(); + uri.Query += "&windspeed_unit=" + options.Windspeed_Unit.ToString(); + uri.Query += "&precipitation_unit=" + options.Precipitation_Unit.ToString(); + if (options.Timezone != string.Empty) + uri.Query += "&timezone=" + options.Timezone; + + uri.Query += "&timeformat=" + options.Timeformat.ToString(); + + uri.Query += "&past_days=" + options.Past_Days; + + if (options.Start_date != string.Empty) + uri.Query += "&start_date=" + options.Start_date; + if (options.End_date != string.Empty) + uri.Query += "&end_date=" + options.End_date; + + // Now we iterate through hourly and daily + + // Hourly + if (options.Hourly.Count > 0) + { + bool firstHourlyElement = true; + uri.Query += "&hourly="; + + foreach (var option in options.Hourly) + { + if (firstHourlyElement) + { + uri.Query += option.ToString(); + firstHourlyElement = false; + } + else + { + uri.Query += "," + option.ToString(); + } + } + } + + // Daily + if (options.Daily.Count > 0) + { + bool firstDailyElement = true; + uri.Query += "&daily="; + foreach (var option in options.Daily) + { + if (firstDailyElement) + { + uri.Query += option.ToString(); + firstDailyElement = false; + } + else + { + uri.Query += "," + option.ToString(); + } + } + } + + // 0.2.0 Weather models + // cell_selection + uri.Query += "&cell_selection=" + options.Cell_Selection; + + // Models + if (options.Models.Count > 0) + { + bool firstModelsElement = true; + uri.Query += "&models="; + foreach (var option in options.Models) + { + if (firstModelsElement) + { + uri.Query += option.ToString(); + firstModelsElement = false; + } + else + { + uri.Query += "," + option.ToString(); + } + } + } + + // new current parameter + if (options.Current.Count > 0) + { + bool firstCurrentElement = true; + uri.Query += "¤t="; + foreach (var option in options.Current) + { + if (firstCurrentElement) + { + uri.Query += option.ToString(); + firstCurrentElement = false; + } + else + { + uri.Query += "," + option.ToString(); + } + } + } + + // new minutely_15 parameter + if (options.Minutely15.Count > 0) + { + bool firstMinutelyElement = true; + uri.Query += "&minutely_15="; + foreach (var option in options.Minutely15) + { + if (firstMinutelyElement) + { + uri.Query += option.ToString(); + firstMinutelyElement = false; + } + else + { + uri.Query += "," + option.ToString(); + } + } + } + + SetApiKeyIfNeeded(uri); + return uri.ToString(); + } + + /// + /// Combines a given url with an options object to create a url for GET requests + /// + /// url+queryString + public string GetUrlWithOptions(GeocodingOptions options) + { + UriBuilder uri = new(GetBaseUrl(_geocodeApiUrl)); + + // Now we check every property and set the value, if neccessary + uri.Query = "name=" + options.Name; + + if (options.Count > 0) + uri.Query += "&count=" + options.Count; + + if (options.Format != string.Empty) + uri.Query += "&format=" + options.Format; + + if (options.Language != string.Empty) + uri.Query += "&language=" + options.Language; + + SetApiKeyIfNeeded(uri); + return uri.ToString(); + } + + /// + /// Combines a given url with an options object to create a url for GET requests + /// + /// url+queryString + public string GetUrlWithOptions(AirQualityOptions options) + { + UriBuilder uri = new(GetBaseUrl(_airQualityApiUrl)); + + // Now we check every property and set the value, if neccessary + uri.Query += "latitude=" + options.Latitude.ToString(CultureInfo.InvariantCulture); + uri.Query += "&longitude=" + options.Longitude.ToString(CultureInfo.InvariantCulture); + + if (options.Domains != string.Empty) + uri.Query += "&domains=" + options.Domains; + + if (options.Timeformat != string.Empty) + uri.Query += "&timeformat=" + options.Timeformat; + + if (options.Timezone != string.Empty) + uri.Query += "&timezone=" + options.Timezone; + + // Finally add hourly array + if (options.Hourly.Count >= 0) + { + bool firstHourlyElement = true; + uri.Query += "&hourly="; + + foreach (var option in options.Hourly) + { + if (firstHourlyElement) + { + uri.Query += option.ToString(); + firstHourlyElement = false; + } + else + { + uri.Query += "," + option.ToString(); + } + } + } + + SetApiKeyIfNeeded(uri); + return uri.ToString(); + } + + public string GetUrlWithOptions(ElevationOptions options) + { + UriBuilder uri = new(GetBaseUrl(_elevationApiUrl)) + { + Query = $"latitude={options.Latitude.ToString(CultureInfo.InvariantCulture)}&longitude={options.Longitude.ToString(CultureInfo.InvariantCulture)}" + }; + + SetApiKeyIfNeeded(uri); + return uri.ToString(); + } + + private void SetApiKeyIfNeeded(UriBuilder uri) + { + if (!string.IsNullOrEmpty(_apiKey)) + uri.Query += $"&apikey={_apiKey}"; + } + + private string GetBaseUrl(string url) + { + var prependCustomerIfHasApiKey = string.IsNullOrEmpty(_apiKey) ? string.Empty : _customerApiUrlFragment; + return $"https://{prependCustomerIfHasApiKey}{url}"; + } +} diff --git a/OpenMeteoTests/UrlFactoryTests.cs b/OpenMeteoTests/UrlFactoryTests.cs new file mode 100644 index 0000000..9fef65f --- /dev/null +++ b/OpenMeteoTests/UrlFactoryTests.cs @@ -0,0 +1,123 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using OpenMeteo; + +namespace OpenMeteoTests +{ + [TestClass] + public class UrlFactoryTests + { + [TestMethod] + public void GetUrlWithOptions_WeatherForecastOptions_Test() + { + var factory = new UrlFactory(); + var url = factory.GetUrlWithOptions(GetWeatherForecastOptions()); + + var expectedUrl = "https://api.open-meteo.com:443/v1/forecast?latitude=40.7128&longitude=-74.006&temperature_unit=celsius&windspeed_unit=kmh&precipitation_unit=mm&timezone=America/New_York&timeformat=iso8601&past_days=2&start_date=2023-01-01&end_date=2023-01-02&hourly=temperature_2m,windspeed_10m&daily=temperature_2m_max,temperature_2m_min&cell_selection=nearest&models=gfs_hrrr,gfs_global¤t=temperature_2m&minutely_15=precipitation"; + Assert.AreEqual(expectedUrl, url); + } + + [TestMethod] + public void GetUrlWithOptions_GeocodingOptions_Test() + { + var factory = new UrlFactory(); + var url = factory.GetUrlWithOptions(GetGeocodingOptions()); + + var expectedUrl = "https://geocoding-api.open-meteo.com:443/v1/search?name=New York&count=100&format=json&language=en"; + Assert.AreEqual(expectedUrl, url); + } + + [TestMethod] + public void GetUrlWithOptions_AirQualityOptions_Test() + { + var factory = new UrlFactory(); + var url = factory.GetUrlWithOptions(GetAirQualityOptions()); + + var expectedUrl = "https://air-quality-api.open-meteo.com:443/v1/air-quality?latitude=40.7128&longitude=-74.006&domains=global&timeformat=iso8601&timezone=America/New_York&hourly=pm10,pm2_5"; + Assert.AreEqual(expectedUrl, url); + } + + [TestMethod] + public void GetUrlWithOptions_ElevationOptions_Test() + { + var factory = new UrlFactory(); + var url = factory.GetUrlWithOptions(GetElevationOptions()); + + var expectedUrl = "https://api.open-meteo.com:443/v1/elevation?latitude=40.7128&longitude=-74.006"; + Assert.AreEqual(expectedUrl, url); + } + + [TestMethod] + public void GetUrlWithOptions_WeatherForecastOptions_WithApiKey_Test() + { + var factory = new UrlFactory("testApiKey"); + var url = factory.GetUrlWithOptions(GetWeatherForecastOptions()); + + var expectedUrl = "https://customer-api.open-meteo.com:443/v1/forecast?latitude=40.7128&longitude=-74.006&temperature_unit=celsius&windspeed_unit=kmh&precipitation_unit=mm&timezone=America/New_York&timeformat=iso8601&past_days=2&start_date=2023-01-01&end_date=2023-01-02&hourly=temperature_2m,windspeed_10m&daily=temperature_2m_max,temperature_2m_min&cell_selection=nearest&models=gfs_hrrr,gfs_global¤t=temperature_2m&minutely_15=precipitation&apikey=testApiKey"; + Assert.AreEqual(expectedUrl, url); + } + + [TestMethod] + public void GetUrlWithOptions_GeocodingOptions_WithApiKey_Test() + { + var factory = new UrlFactory("testApiKey"); + var url = factory.GetUrlWithOptions(GetGeocodingOptions()); + + var expectedUrl = "https://customer-geocoding-api.open-meteo.com:443/v1/search?name=New York&count=100&format=json&language=en&apikey=testApiKey"; + Assert.AreEqual(expectedUrl, url); + } + + [TestMethod] + public void GetUrlWithOptions_AirQualityOptions_WithApiKey_Test() + { + var factory = new UrlFactory("testApiKey"); + var url = factory.GetUrlWithOptions(GetAirQualityOptions()); + + var expectedUrl = "https://customer-air-quality-api.open-meteo.com:443/v1/air-quality?latitude=40.7128&longitude=-74.006&domains=global&timeformat=iso8601&timezone=America/New_York&hourly=pm10,pm2_5&apikey=testApiKey"; + Assert.AreEqual(expectedUrl, url); + } + + [TestMethod] + public void GetUrlWithOptions_ElevationOptions_WithApiKey_Test() + { + var factory = new UrlFactory("testApiKey"); + var url = factory.GetUrlWithOptions(GetElevationOptions()); + + var expectedUrl = "https://customer-api.open-meteo.com:443/v1/elevation?latitude=40.7128&longitude=-74.006&apikey=testApiKey"; + Assert.AreEqual(expectedUrl, url); + } + + private static WeatherForecastOptions GetWeatherForecastOptions() => new() + { + Latitude = 40.7128f, + Longitude = -74.006f, + Temperature_Unit = TemperatureUnitType.celsius, + Windspeed_Unit = WindspeedUnitType.kmh, + Precipitation_Unit = PrecipitationUnitType.mm, + Timezone = "America/New_York", + Timeformat = TimeformatType.iso8601, + Past_Days = 2, + Start_date = "2023-01-01", + End_date = "2023-01-02", + Hourly = new HourlyOptions([HourlyOptionsParameter.temperature_2m, HourlyOptionsParameter.windspeed_10m]), + Daily = new DailyOptions([DailyOptionsParameter.temperature_2m_max, DailyOptionsParameter.temperature_2m_min]), + Cell_Selection = CellSelectionType.nearest, + Models = new WeatherModelOptions([WeatherModelOptionsParameter.gfs_hrrr, WeatherModelOptionsParameter.gfs_global]), + Current = new CurrentOptions([CurrentOptionsParameter.temperature_2m]), + Minutely15 = new Minutely15Options([Minutely15OptionsParameter.precipitation]) + }; + + private static GeocodingOptions GetGeocodingOptions() => new("New York"); + + private static AirQualityOptions GetAirQualityOptions() => new() + { + Latitude = 40.7128f, + Longitude = -74.006f, + Domains = "global", + Timeformat = "iso8601", + Timezone = "America/New_York", + Hourly = new AirQualityOptions.HourlyOptions([AirQualityOptions.HourlyOptionsParameter.pm10, AirQualityOptions.HourlyOptionsParameter.pm2_5]) + }; + + private static ElevationOptions GetElevationOptions() => new(40.7128f, -74.006f); + } +}