diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..8c606e1 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,19 @@ +name: Build for CI + +on: + push: + branches: [ "main" ] + paths-ignore: + - "**.md" + pull_request: + branches: [ "main" ] + +jobs: + build-plugin-ci: + + runs-on: windows-latest + + steps: + - uses: actions/checkout@v3 + - name: Build + run: dotnet build src\Indiko.OpenWeatherClient.sln -c Release diff --git a/.github/workflows/release-nuget.yml b/.github/workflows/release-nuget.yml new file mode 100644 index 0000000..0d74ea0 --- /dev/null +++ b/.github/workflows/release-nuget.yml @@ -0,0 +1,27 @@ +name: Create a (Pre)release on NuGet + +on: + push: + tags: + - "v[0-9]+.[0-9]+.[0-9]+" + - "v[0-9]+.[0-9]+.[0-9]+-preview[0-9]+" +jobs: + release-nuget: + + runs-on: windows-latest + + steps: + - uses: actions/checkout@v3 + - name: Verify commit exists in origin/main + run: | + git fetch --no-tags --prune --depth=1 origin +refs/heads/*:refs/remotes/origin/* + git branch --remote --contains | grep origin/main + - name: Get version information from tag + id: get_version + uses: battila7/get-version-action@v2 + - name: Pack + run: dotnet pack src\Indiko.OpenWeatherClient.sln -c Release -p:PackageVersion=${{ steps.get_version.outputs.version-without-v }} + - name: Push + run: dotnet nuget push src\Indiko.OpenWeatherClient\bin\Release\Indiko.OpenWeatherClient.${{ steps.get_version.outputs.version-without-v }}.nupkg -s https://api.nuget.org/v3/index.json -k ${{ secrets.NUGET_API_KEY }} + env: + GITHUB_TOKEN: ${{ secrets.NUGET_API_KEY }} diff --git a/README.md b/README.md index b41d628..2c7d2a1 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,89 @@ +![](nuget.png) + # Indiko.OpenWeatherClient -Another C# client for openweathermap.org API +The `Indiko.OpenWeatherClient` is a .NET library for interacting with the OpenWeatherMap APIs. It provides easy access to real-time weather data and map tiles, ensuring seamless integration with .NET applications. + +## Build Status +![ci](https://github.com/0xc3u/Indiko.OpenWeatherClient/actions/workflows/ci.yml/badge.svg) + +## Install Controls +[![NuGet](https://img.shields.io/nuget/v/Indiko.OpenWeatherClient.svg?label=NuGet)](https://www.nuget.org/packages/Indiko.OpenWeatherClient/) + +Available on [NuGet](http://www.nuget.org/packages/Indiko.OpenWeatherClient). + +Install with the dotnet CLI: `dotnet add package Indiko.OpenWeatherClient`, or through the NuGet Package Manager in Visual Studio. + +## Features +- Fetch current weather, hourly forecast, and daily forecast. +- Access to weather map tiles. +- Supports Dependency Injection and manual instantiation. +- Configurable through a fluent API for building request parameters. + + +> **Important note**: +> To use this client wrapper for openweathermap.org you need have a valid api-key from https://openweathermap.org. + +## Platform supported + +| Platform | Version Supported | Reference | +|----------|--------------------------|-| +| .NET | 8.0 | + + +## API Coverage + +- Weather Data: Access current weather, hourly forecast, daily forecast. (https://openweathermap.org/api/one-call-3) +- Maps API: Retrieve specific map tiles based on geographical and zoom parameters. (https://openweathermap.org/api/weathermaps) + +> For detailed API usage and more examples, please refer to the official documentation or explore the OpenWeatherClient class definitions within the package. + + +### Dependency Injection + +```csharp +// Add the service to the service collection +services.AddOpenWeatherClient(); +``` + +```csharp +// Inject the service +public class MyClass +{ + private readonly IOpenWeatherClient _openWeatherClient; + + public MyClass(IOpenWeatherClient openWeatherClient) + { + _openWeatherClient = openWeatherClient; + } +} + +``` + +### Manual instantiation + +```csharp +// Create a new instance of the client +IOpenWeatherClient _openWeatherClient = new OpenWeatherClient(); +``` + +### Usage + +```csharp + +// Get the current weather for a New York + +var request = new OpenWeatherRequest +{ + ApiKey = "YOUR_API_KEY_HERE", + Latitude = 40.712776, + Longitude = -74.005974, + Language = Constants.Languages.English, + Unit = Constants.Units.Metric, + Excludes = [Constants.Excludes.Minutely, Constants.Excludes.Daily, Constants.Excludes.Hourly] +}; + +var response = await openWeatherClient.GetWeatherAsync(request); + +``` + + diff --git a/nuget.png b/nuget.png new file mode 100644 index 0000000..7ed7e39 Binary files /dev/null and b/nuget.png differ diff --git a/src/Indiko.OpenWeatherClient.sln b/src/Indiko.OpenWeatherClient.sln new file mode 100644 index 0000000..29e0aa8 --- /dev/null +++ b/src/Indiko.OpenWeatherClient.sln @@ -0,0 +1,44 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.9.34728.123 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Indiko.OpenWeatherClient", "Indiko.OpenWeatherClient\Indiko.OpenWeatherClient.csproj", "{C0DCC5AF-4C25-4BCA-AACE-095515D0C873}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{72619858-9C14-4668-8430-0A3BCFD33768}" + ProjectSection(SolutionItems) = preProject + ..\README.md = ..\README.md + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{FAE6981C-18D1-42A7-8F35-F4B249BCEDAE}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{BFB88B54-1F7C-4529-8C92-AD66B8DF2EF5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Indiko.OpenWeatherClient.Tests", "..\tests\Indiko.OpenWeatherClient.Tests\Indiko.OpenWeatherClient.Tests.csproj", "{0584C53B-D607-439C-81D8-6D3D712B9944}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {C0DCC5AF-4C25-4BCA-AACE-095515D0C873}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C0DCC5AF-4C25-4BCA-AACE-095515D0C873}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C0DCC5AF-4C25-4BCA-AACE-095515D0C873}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C0DCC5AF-4C25-4BCA-AACE-095515D0C873}.Release|Any CPU.Build.0 = Release|Any CPU + {0584C53B-D607-439C-81D8-6D3D712B9944}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0584C53B-D607-439C-81D8-6D3D712B9944}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0584C53B-D607-439C-81D8-6D3D712B9944}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0584C53B-D607-439C-81D8-6D3D712B9944}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {C0DCC5AF-4C25-4BCA-AACE-095515D0C873} = {FAE6981C-18D1-42A7-8F35-F4B249BCEDAE} + {0584C53B-D607-439C-81D8-6D3D712B9944} = {BFB88B54-1F7C-4529-8C92-AD66B8DF2EF5} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {B487E051-95D4-4F59-AE56-6F806E31652D} + EndGlobalSection +EndGlobal diff --git a/src/Indiko.OpenWeatherClient/Builder/OpenWeatherRequestBuilder.cs b/src/Indiko.OpenWeatherClient/Builder/OpenWeatherRequestBuilder.cs new file mode 100644 index 0000000..a705c34 --- /dev/null +++ b/src/Indiko.OpenWeatherClient/Builder/OpenWeatherRequestBuilder.cs @@ -0,0 +1,135 @@ +using Indiko.OpenWeatherClient.Constants; + +namespace Indiko.OpenWeatherClient.Builder; + +/// +/// Provides a builder pattern to create and configure an OpenWeather API request. +/// +/// The API key to authenticate requests. +public class OpenWeatherRequestBuilder(string apiKey) +{ + private readonly string _apiKey = apiKey; + private string _language = Languages.English; + private string _unit = Units.Metric; + private string[] _excludes = []; + private double _latitude; + private double _longitude; + private string _city; + private string _country; + private DateTime? _date; + private bool _useGeoCoding; + + /// + /// Specifies the date for the request, typically used for historical data. + /// + /// The date of interest. + public OpenWeatherRequestBuilder WithDate(DateTime date) + { + _date = date; + return this; + } + + /// + /// Specifies the language for the response data. + /// + /// The language code (ISO 639-1). + public OpenWeatherRequestBuilder WithLanguage(string language) + { + _language = language; + return this; + } + + /// + /// Specifies the unit of measurement for temperature and other values. + /// + /// The unit system to use ('metric' or 'imperial'). + public OpenWeatherRequestBuilder WithUnit(string unit) + { + _unit = unit; + return this; + } + /// + /// Specifies the geographical coordinates for the weather data request. + /// + /// The latitude coordinate. + /// The longitude coordinate. + public OpenWeatherRequestBuilder WithLocation(double latitude, double longitude) + { + _latitude = latitude; + _longitude = longitude; + _useGeoCoding = false; + return this; + } + + /// + /// Specifies the city and country for the weather data request. + /// + /// The name of the city. + /// The country code (ISO 3166). + public OpenWeatherRequestBuilder WithCity(string city, string country) + { + _city = city; + _country = country; + _useGeoCoding = true; + return this; + } + + /// + /// Specifies the features to exclude from the response data. + /// + /// An array of features to exclude. + public OpenWeatherRequestBuilder WithExcludes(string[] excludes) + { + _excludes = excludes; + return this; + } + + /// + /// Builds the final URI for the OpenWeather API request based on the configured options. + /// Throws an exception if required parameters are not provided or incorrectly configured. + /// + /// The URI of the configured OpenWeather API request. + public Uri Build() + { + if (string.IsNullOrEmpty(_apiKey)) + { + throw new ArgumentException("API Key must be provided"); + } + + string requestUrl = $"https://api.openweathermap.org/data/3.0/onecall?appid={_apiKey}&lang={_language}&units={_unit}"; + if (_useGeoCoding) + { + if (_latitude != 0 && _longitude != 0) + { + throw new ArgumentException("Latitude and Longitude must be 0 when using city and country"); + } + if (string.IsNullOrEmpty(_city) || string.IsNullOrEmpty(_country)) + { + throw new ArgumentException("City and Country must be provided when using city and country"); + } + + requestUrl = $"{requestUrl}&q={_city},{_country}"; + } + else + { + if (_latitude == 0 || _longitude == 0) + { + throw new ArgumentException("Latitude and Longitude must be provided when not using city and country"); + } + + requestUrl = $"{requestUrl}&lat={_latitude}&lon={_longitude}"; + } + + if (_date.HasValue) + { + requestUrl = $"{requestUrl}&date={_date.Value.ToShortDateString()}"; + } + + if (_excludes != null) + { + requestUrl = $"{requestUrl}&exclude={string.Join(",", _excludes)}"; + } + + return new Uri(requestUrl); + } +} diff --git a/src/Indiko.OpenWeatherClient/Constants/Excludes.cs b/src/Indiko.OpenWeatherClient/Constants/Excludes.cs new file mode 100644 index 0000000..0e1430b --- /dev/null +++ b/src/Indiko.OpenWeatherClient/Constants/Excludes.cs @@ -0,0 +1,37 @@ +namespace Indiko.OpenWeatherClient.Constants; + +/// +/// Contains constant values that specify which data components to exclude from the responses in OpenWeather API requests. +/// +public static class Excludes +{ + /// + /// Excludes the current weather data from the API response. + /// + public const string Current = "current"; + + /// + /// Excludes the minutely weather data (e.g., precipitation) from the API response. + /// + public const string Minutely = "minutely"; + + /// + /// Excludes the hourly forecast data from the API response. + /// + public const string Hourly = "hourly"; + + /// + /// Excludes the daily forecast data from the API response. + /// + public const string Daily = "daily"; + + /// + /// Excludes weather alerts from the API response. + /// + public const string Alerts = "alerts"; + + /// + /// Excludes all optional data components (current, minutely, hourly, daily, and alerts) from the API response. + /// + public const string All = "current,minutely,hourly,daily,alerts"; +} \ No newline at end of file diff --git a/src/Indiko.OpenWeatherClient/Constants/Languages.cs b/src/Indiko.OpenWeatherClient/Constants/Languages.cs new file mode 100644 index 0000000..ac41884 --- /dev/null +++ b/src/Indiko.OpenWeatherClient/Constants/Languages.cs @@ -0,0 +1,54 @@ +namespace Indiko.OpenWeatherClient.Constants; + +/// +/// Contains constant values representing the language codes supported by the OpenWeather API for localizing responses. +/// +public static class Languages +{ + public const string Afrikaans = "af"; + public const string Albanian = "al"; + public const string Arabic = "ar"; + public const string Azerbaijani = "az"; + public const string Bulgarian = "bg"; + public const string Catalan = "ca"; + public const string Czech = "cz"; + public const string Danish = "da"; + public const string German = "de"; + public const string Greek = "el"; + public const string English = "en"; + public const string Basque = "eu"; + public const string Persian = "fa"; + public const string Finnish = "fi"; + public const string French = "fr"; + public const string Galician = "gl"; + public const string Hebrew = "he"; + public const string Hindi = "hi"; + public const string Croatian = "hr"; + public const string Hungarian = "hu"; + public const string Indonesian = "id"; + public const string Italian = "it"; + public const string Japanese = "ja"; + public const string Korean = "kr"; + public const string Latvian = "la"; + public const string Lithuanian = "lt"; + public const string Macedonian = "mk"; + public const string Norwegian = "no"; + public const string Dutch = "nl"; + public const string Polish = "pl"; + public const string Portuguese = "pt"; + public const string PortugueseBr = "pt_br"; + public const string Romanian = "ro"; + public const string Russian = "ru"; + public const string Swedish = "se"; + public const string Slovak = "sk"; + public const string Slovenian = "sl"; + public const string Spanish = "es"; + public const string Serbian = "sr"; + public const string Thai = "th"; + public const string Turkish = "tr"; + public const string Ukrainian = "ua"; + public const string Vietnamese = "vi"; + public const string ChineseSimplified = "zh_cn"; + public const string ChineseTraditional = "zh_tw"; + public const string Zulu = "zu"; +} \ No newline at end of file diff --git a/src/Indiko.OpenWeatherClient/Constants/MapLayers.cs b/src/Indiko.OpenWeatherClient/Constants/MapLayers.cs new file mode 100644 index 0000000..3bf32f7 --- /dev/null +++ b/src/Indiko.OpenWeatherClient/Constants/MapLayers.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Indiko.OpenWeatherClient.Constants; +public static class MapLayers +{ + public static string Clouds = "clouds_new"; + public static string Precipitation = "precipitation_new"; + public static string SealevelPressure = "pressure_new"; + public static string WindSpeed = "wind_new"; + public static string Temperature = "temp_new"; +} diff --git a/src/Indiko.OpenWeatherClient/Constants/Units.cs b/src/Indiko.OpenWeatherClient/Constants/Units.cs new file mode 100644 index 0000000..f691664 --- /dev/null +++ b/src/Indiko.OpenWeatherClient/Constants/Units.cs @@ -0,0 +1,22 @@ +namespace Indiko.OpenWeatherClient.Constants; + +/// +/// Contains constant values that specify the unit systems used for reporting temperature and other measurements in the OpenWeather API responses. +/// +public static class Units +{ + /// + /// Uses the Standard unit system. Temperature in Kelvin, speed in meter/sec, and other measurements in standard metric units. + /// + public const string Standard = "standard"; + + /// + /// Uses the Metric unit system. Temperature in Celsius, speed in meter/sec, and other measurements in metric units. + /// + public const string Metric = "metric"; + + /// + /// Uses the Imperial unit system. Temperature in Fahrenheit, speed in miles/hour, and other measurements in imperial units. + /// + public const string Imperial = "imperial"; +} \ No newline at end of file diff --git a/src/Indiko.OpenWeatherClient/Converter/UnixDateTimeConverter.cs b/src/Indiko.OpenWeatherClient/Converter/UnixDateTimeConverter.cs new file mode 100644 index 0000000..79a6cc1 --- /dev/null +++ b/src/Indiko.OpenWeatherClient/Converter/UnixDateTimeConverter.cs @@ -0,0 +1,47 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Indiko.OpenWeatherClient.Converter; + +/// +/// A custom JSON converter that converts between Unix timestamps and nullable DateTime objects. +/// This converter handles the conversion of DateTime values to and from Unix timestamps in JSON. +/// +public class UnixDateTimeConverter : JsonConverter +{ + /// + /// Reads and converts the JSON token into a nullable DateTime value. + /// + /// The reader to read from. + /// The type of object to convert. + /// Options for the serializer. + /// A DateTime value if the JSON token is a number; otherwise, null. + public override DateTime? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Number) + { + long unixTime = reader.GetInt64(); + return DateTimeOffset.FromUnixTimeSeconds(unixTime).UtcDateTime; + } + return null; + } + + /// + /// Writes a DateTime value as a Unix timestamp to the JSON writer. + /// + /// The writer to write to. + /// The DateTime value to write. + /// Options for the serializer. + public override void Write(Utf8JsonWriter writer, DateTime? value, JsonSerializerOptions options) + { + if (value.HasValue) + { + long unixTime = ((DateTimeOffset)value.Value).ToUnixTimeSeconds(); + writer.WriteNumberValue(unixTime); + } + else + { + writer.WriteNullValue(); + } + } +} diff --git a/src/Indiko.OpenWeatherClient/Exceptions/OpenWeatherException.cs b/src/Indiko.OpenWeatherClient/Exceptions/OpenWeatherException.cs new file mode 100644 index 0000000..55b0002 --- /dev/null +++ b/src/Indiko.OpenWeatherClient/Exceptions/OpenWeatherException.cs @@ -0,0 +1,17 @@ +namespace Indiko.OpenWeatherClient.Exceptions; + +/// +/// Represents errors that occur during OpenWeather API operations. +/// +public sealed class OpenWeatherException(int statusCode, string responseMessage) : Exception(responseMessage) +{ + /// + /// Gets the HTTP status code returned from the OpenWeather API when the error occurred. + /// + public int ResponseCode { get; } = statusCode; + + /// + /// Gets the message describing the error, as returned by the OpenWeather API. + /// + public string ResponseMessage { get; } = responseMessage; +} \ No newline at end of file diff --git a/src/Indiko.OpenWeatherClient/Extensions/IServiceCollectionExtensions.cs b/src/Indiko.OpenWeatherClient/Extensions/IServiceCollectionExtensions.cs new file mode 100644 index 0000000..8223754 --- /dev/null +++ b/src/Indiko.OpenWeatherClient/Extensions/IServiceCollectionExtensions.cs @@ -0,0 +1,27 @@ +using Indiko.OpenWeatherClient.Interfaces; +using Microsoft.Extensions.DependencyInjection; + +namespace Indiko.OpenWeatherClient.Extensions; + +/// +/// Provides extension methods for the to facilitate the registration of OpenWeather API related services. +/// +public static class IServiceCollectionExtensions +{ + /// + /// Adds the OpenWeather client to the specified . + /// + /// The to add services to. + /// The so that additional calls can be chained. + /// + /// This method configures the dependency injection container to provide an + /// implementation whenever it is needed. It registers the as a scoped service, + /// which means a new instance is created once per client request. + /// + public static IServiceCollection AddOpenWeatherClient(this IServiceCollection services) + { + services.AddScoped(); + + return services; + } +} \ No newline at end of file diff --git a/src/Indiko.OpenWeatherClient/Indiko.OpenWeatherClient.csproj b/src/Indiko.OpenWeatherClient/Indiko.OpenWeatherClient.csproj new file mode 100644 index 0000000..2a92c33 --- /dev/null +++ b/src/Indiko.OpenWeatherClient/Indiko.OpenWeatherClient.csproj @@ -0,0 +1,39 @@ + + + + net8.0 + disable + disable + + + INDIKO + Copyright © INDIKO and contributors + True + https://github.com/0xc3u/Indiko.OpenWeatherClient + https://github.com/0xc3u/Indiko.OpenWeatherClient + git + dotnet-maui;maui;ui;chips;control; + True + true + true + snupkg + INDIKO OpenWeather API Client + This package allows developers to easily integrate OpenWeatherMap's APIs into their .NET applications. + MIT + True + portable + icon.png + weather; api; openweathermap; .NET; maps; forecast + Initial release of the `Indiko.OpenWeatherClient` library. + + + + + + + + + + + + diff --git a/src/Indiko.OpenWeatherClient/Interfaces/IOpenWeatherClient.cs b/src/Indiko.OpenWeatherClient/Interfaces/IOpenWeatherClient.cs new file mode 100644 index 0000000..beaf67e --- /dev/null +++ b/src/Indiko.OpenWeatherClient/Interfaces/IOpenWeatherClient.cs @@ -0,0 +1,17 @@ +using Indiko.OpenWeatherClient.Models; +using System.Threading.Tasks; + +namespace Indiko.OpenWeatherClient.Interfaces; +public interface IOpenWeatherClient +{ + Task GetCurrentWeatherAsync(OpenWeatherRequest request, CancellationToken cancellationToken = default); + + Task GetDailyWeatherAsync(OpenWeatherRequest request, CancellationToken cancellationToken = default); + + Task GetHourlyWeatherAsync(OpenWeatherRequest request, CancellationToken cancellationToken = default); + + Task GetWeatherAsync(OpenWeatherRequest request, CancellationToken cancellationToken = default); + + Uri GetMapTileUri(OpenWeatherMapTileRequest mapTileRequest); + Task GetMapTileAsync(OpenWeatherMapTileRequest mapTileRequest, CancellationToken cancellationToken = default); +} diff --git a/src/Indiko.OpenWeatherClient/Models/Alert.cs b/src/Indiko.OpenWeatherClient/Models/Alert.cs new file mode 100644 index 0000000..5eb4dcf --- /dev/null +++ b/src/Indiko.OpenWeatherClient/Models/Alert.cs @@ -0,0 +1,60 @@ +using Indiko.OpenWeatherClient.Converter; +using System.Text.Json.Serialization; + +namespace Indiko.OpenWeatherClient.Models; + +/// +/// Represents a weather alert issued by a weather data provider, detailing significant weather events. +/// +public sealed class Alert +{ + /// + /// Gets the name of the organization that issued the alert. + /// This property will be ignored when writing to JSON if the value is null. + /// + [JsonPropertyName("sender_name")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string SenderName { get; init; } + + /// + /// Gets the name of the weather event that triggered the alert. + /// This property will be ignored when writing to JSON if the value is null. + /// + [JsonPropertyName("event")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string Event { get; init; } + + /// + /// Gets the start time of the weather event, represented as a nullable DateTime. + /// This property will be ignored when writing to JSON if the value is null. + /// + [JsonPropertyName("start")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonConverter(typeof(UnixDateTimeConverter))] + public DateTime? Start { get; init; } + + /// + /// Gets the end time of the weather event, represented as a nullable DateTime. + /// This property will be ignored when writing to JSON if the value is null. + /// + [JsonPropertyName("end")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonConverter(typeof(UnixDateTimeConverter))] + public DateTime? End { get; init; } + + /// + /// Gets the detailed description of the weather event and its potential impact. + /// This property will be ignored when writing to JSON if the value is null. + /// + [JsonPropertyName("description")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string Description { get; init; } + + /// + /// Gets the tags associated with the alert, which categorize the nature of the event. + /// This property will be ignored when writing to JSON if the value is null. + /// + [JsonPropertyName("tags")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string[] Tags { get; init; } +} diff --git a/src/Indiko.OpenWeatherClient/Models/Current.cs b/src/Indiko.OpenWeatherClient/Models/Current.cs new file mode 100644 index 0000000..6ff2e4b --- /dev/null +++ b/src/Indiko.OpenWeatherClient/Models/Current.cs @@ -0,0 +1,53 @@ +using Indiko.OpenWeatherClient.Converter; +using System.Text.Json.Serialization; + +namespace Indiko.OpenWeatherClient.Models; + +public sealed class Current : Weather +{ + /// + /// Gets the sunrise time for the current day. + /// This property is represented as a Unix timestamp in the JSON data and will be ignored when writing to JSON if the value is null. + /// + [JsonPropertyName("sunrise")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonConverter(typeof(UnixDateTimeConverter))] + public DateTime? Sunrise { get; init; } + + /// + /// Gets the sunset time for the current day. + /// This property is represented as a Unix timestamp in the JSON data and will be ignored when writing to JSON if the value is null. + /// + [JsonPropertyName("sunset")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonConverter(typeof(UnixDateTimeConverter))] + public DateTime? Sunset { get; init; } + + /// + /// Gets the current temperature in unit specified. + /// + [JsonPropertyName("temp")] + public float Temperature { get; init; } + + /// + /// Gets the perceived temperature in unit specified, considering the impact of wind and humidity. + /// + [JsonPropertyName("feels_like")] + public float FeelsLike { get; init; } + + /// + /// Gets the rainfall data for the last hour unit specified. + /// This property will be ignored when writing to JSON if the value is null. + /// + [JsonPropertyName("rain")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public OneHour Rain { get; init; } + + /// + /// Gets the snowfall data for the last hour unit specified. + /// This property will be ignored when writing to JSON if the value is null. + /// + [JsonPropertyName("snow")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public OneHour Snow { get; init; } +} diff --git a/src/Indiko.OpenWeatherClient/Models/Day.cs b/src/Indiko.OpenWeatherClient/Models/Day.cs new file mode 100644 index 0000000..18f2e10 --- /dev/null +++ b/src/Indiko.OpenWeatherClient/Models/Day.cs @@ -0,0 +1,91 @@ +using Indiko.OpenWeatherClient.Converter; +using System.Text.Json.Serialization; + +namespace Indiko.OpenWeatherClient.Models; +public sealed class Day : Weather +{ + /// + /// Gets the sunrise time for the day. + /// This property is represented as a Unix timestamp in the JSON data and will be ignored when writing to JSON if the value is null. + /// + [JsonPropertyName("sunrise")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonConverter(typeof(UnixDateTimeConverter))] + public DateTime? Sunrise { get; init; } + + /// + /// Gets the sunset time for the day. + /// This property is represented as a Unix timestamp in the JSON data and will be ignored when writing to JSON if the value is null. + /// + [JsonPropertyName("sunset")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonConverter(typeof(UnixDateTimeConverter))] + public DateTime? Sunset { get; init; } + + /// + /// Gets the moonrise time for the day. + /// This property is represented as a Unix timestamp in the JSON data and will be ignored when writing to JSON if the value is null. + /// + [JsonPropertyName("moonrise")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonConverter(typeof(UnixDateTimeConverter))] + public DateTime? Moonrise { get; init; } + + /// + /// Gets the moonset time for the day. + /// This property is represented as a Unix timestamp in the JSON data and will be ignored when writing to JSON if the value is null. + /// + [JsonPropertyName("moonset")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonConverter(typeof(UnixDateTimeConverter))] + public DateTime? Moonset { get; init; } + + /// + /// Gets the moon phase for the day. + /// The value ranges from 0.0 (new moon) to 1.0 (full moon). + /// This property will be ignored when writing to JSON if the value is null. + /// + [JsonPropertyName("moon_phase")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public float? MoonPhase { get; init; } + + /// + /// Gets a summary of the day's weather conditions. + /// + [JsonPropertyName("summary")] + public string Summary { get; init; } + + /// + /// Gets the detailed temperature information for the day, + /// including minimum, maximum, and average temperatures in unit specified. + /// This property will be ignored when writing to JSON if the value is null. + /// + [JsonPropertyName("temp")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public Temperature Temperature { get; init; } + + /// + /// Gets the perceived temperature details for the day, + /// considering factors such as wind and humidity in unit specified. + /// This property will be ignored when writing to JSON if the value is null. + /// + [JsonPropertyName("feels_like")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public Temperature FeelsLike { get; init; } + + /// + /// Gets the recorded rainfall in unit specified for the day. + /// This property will be ignored when writing to JSON if the value is null. + /// + [JsonPropertyName("rain")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public float? Rain { get; init; } + + /// + /// Gets the recorded snowfall in unit specified for the day. + /// This property will be ignored when writing to JSON if the value is null. + /// + [JsonPropertyName("snow")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public float? Snow { get; init; } +} diff --git a/src/Indiko.OpenWeatherClient/Models/Hour.cs b/src/Indiko.OpenWeatherClient/Models/Hour.cs new file mode 100644 index 0000000..40324e4 --- /dev/null +++ b/src/Indiko.OpenWeatherClient/Models/Hour.cs @@ -0,0 +1,34 @@ +using System.Text.Json.Serialization; + +namespace Indiko.OpenWeatherClient.Models; +public sealed class Hour : Weather +{ + /// + /// Gets the temperature at the specific hour in unit specified. + /// + [JsonPropertyName("temp")] + public float Temperature { get; init; } + + /// + /// Gets the perceived temperature at the specific hour in unit specified, + /// considering factors like wind and humidity. + /// + [JsonPropertyName("feels_like")] + public float FeelsLike { get; init; } + + /// + /// Gets the rainfall data for the specific hour unit specified. + /// This property will be ignored when writing to JSON if the value is null. + /// + [JsonPropertyName("rain")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public OneHour Rain { get; init; } + + /// + /// Gets the snowfall data for the specific hour unit specified. + /// This property will be ignored when writing to JSON if the value is null. + /// + [JsonPropertyName("snow")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public OneHour Snow { get; init; } +} diff --git a/src/Indiko.OpenWeatherClient/Models/OneHour.cs b/src/Indiko.OpenWeatherClient/Models/OneHour.cs new file mode 100644 index 0000000..d56dfa4 --- /dev/null +++ b/src/Indiko.OpenWeatherClient/Models/OneHour.cs @@ -0,0 +1,16 @@ +using System.Text.Json.Serialization; + +namespace Indiko.OpenWeatherClient.Models; + +/// +/// Represents precipitation data for a one-hour period. +/// +public class OneHour +{ + /// + /// Gets the amount of precipitation in unit specified measured during the one-hour period. + /// This property is serialized into JSON using the key "1h". + /// + [JsonPropertyName("1h")] + public float Amount { get; init; } +} diff --git a/src/Indiko.OpenWeatherClient/Models/OpenWeatherMapTileRequest.cs b/src/Indiko.OpenWeatherClient/Models/OpenWeatherMapTileRequest.cs new file mode 100644 index 0000000..3b051ed --- /dev/null +++ b/src/Indiko.OpenWeatherClient/Models/OpenWeatherMapTileRequest.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Indiko.OpenWeatherClient.Models; +public record OpenWeatherMapTileRequest +{ + public string ApiKey { get; init; } + public string Layer { get; init; } + public int ZoomLevel { get; init; } + public int X { get; init; } + public int Y { get; init; } +} diff --git a/src/Indiko.OpenWeatherClient/Models/OpenWeatherRequest.cs b/src/Indiko.OpenWeatherClient/Models/OpenWeatherRequest.cs new file mode 100644 index 0000000..366c108 --- /dev/null +++ b/src/Indiko.OpenWeatherClient/Models/OpenWeatherRequest.cs @@ -0,0 +1,53 @@ +namespace Indiko.OpenWeatherClient.Models; + +/// +/// Represents a request to the OpenWeather API, containing all necessary parameters to retrieve weather data. +/// +public record OpenWeatherRequest +{ + /// + /// Gets the API key used for authentication with the OpenWeather API. + /// + public string ApiKey { get; init; } + + /// + /// Gets the name of the city for which weather data is requested. + /// + public string City { get; init; } + + /// + /// Gets the country code (ISO 3166) of the city for which weather data is requested. + /// + public string Country { get; init; } + + /// + /// Gets the latitude coordinate for the location for which weather data is requested. + /// + public double Latitude { get; init; } + + /// + /// Gets the longitude coordinate for the location for which weather data is requested. + /// + public double Longitude { get; init; } + + /// + /// Gets the language code (ISO 639-1) for the response data from the OpenWeather API. + /// + public string Language { get; init; } + + /// + /// Gets the unit of measurement for temperature and wind speed (e.g., 'metric', 'imperial'). + /// + public string Unit { get; init; } + + /// + /// Gets the array of weather data features to exclude from the API response (e.g., 'current', 'minutely', 'daily'). + /// + public string[] Excludes { get; init; } + + /// + /// Gets the specific date for which historical weather data is requested. + /// Optional; only needed for historical data requests. + /// + public DateTime? Date { get; init; } +} diff --git a/src/Indiko.OpenWeatherClient/Models/OpenWeatherResponse.cs b/src/Indiko.OpenWeatherClient/Models/OpenWeatherResponse.cs new file mode 100644 index 0000000..d9968ce --- /dev/null +++ b/src/Indiko.OpenWeatherClient/Models/OpenWeatherResponse.cs @@ -0,0 +1,57 @@ +using System.Text.Json.Serialization; + +namespace Indiko.OpenWeatherClient.Models; +public sealed class OpenWeatherResponse +{ + /// + /// Gets or sets the latitude of the location for which weather data is provided. + /// + [JsonPropertyName("lat")] + public double Latitude { get; init; } + + /// + /// Gets or sets the longitude of the location for which weather data is provided. + /// + [JsonPropertyName("lon")] + public double Longitude { get; init; } + + /// + /// Gets or sets the IANA timezone name for the location. + /// + [JsonPropertyName("timezone")] + public string Timezone { get; init; } + + /// + /// Gets or sets the offset in seconds from UTC for the location's timezone. + /// + [JsonPropertyName("timezone_offset")] + public int TimezoneOffset { get; init; } + + /// + /// Gets or sets the current weather conditions data. + /// This property will be ignored when writing to JSON if the value is null. + /// + [JsonPropertyName("current")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public Current Current { get; init; } + + /// + /// Gets or sets the array of hourly weather forecasts. + /// This property will be ignored when writing to JSON if the value is null. + /// + [JsonPropertyName("hourly")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public Hour[] Hourly { get; init; } + + /// + /// Gets or sets the array of daily weather forecasts. + /// This property will be ignored when writing to JSON if the value is null. + /// + [JsonPropertyName("daily")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public Day[] Daily { get; init; } + + [JsonPropertyName("alerts")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public Alert[] Alerts { get; init; } +} diff --git a/src/Indiko.OpenWeatherClient/Models/Temperature.cs b/src/Indiko.OpenWeatherClient/Models/Temperature.cs new file mode 100644 index 0000000..2df59e8 --- /dev/null +++ b/src/Indiko.OpenWeatherClient/Models/Temperature.cs @@ -0,0 +1,53 @@ +using System.Text.Json.Serialization; + +namespace Indiko.OpenWeatherClient.Models; +public class Temperature +{ + /// + /// Gets the average daytime temperature in unit specified. + /// This property will be ignored when writing to JSON if the value is null. + /// + [JsonPropertyName("day")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public float? Day { get; init; } + + /// + /// Gets the minimum recorded temperature in unit specified for the day. + /// This property will be ignored when writing to JSON if the value is null. + /// + [JsonPropertyName("min")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public float? Min { get; init; } + + /// + /// Gets the maximum recorded temperature in unit specified for the day. + /// This property will be ignored when writing to JSON if the value is null. + /// + [JsonPropertyName("max")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public float? Max { get; init; } + + /// + /// Gets the average nighttime temperature in unit specified. + /// This property will be ignored when writing to JSON if the value is null. + /// + [JsonPropertyName("night")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public float? Night { get; init; } + + /// + /// Gets the average evening temperature in unit specified. + /// This property will be ignored when writing to JSON if the value is null. + /// + [JsonPropertyName("eve")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public float? Eve { get; init; } + + /// + /// Gets the average morning temperature in unit specified. + /// This property will be ignored when writing to JSON if the value is null. + /// + [JsonPropertyName("morn")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public float? Morn { get; init; } +} \ No newline at end of file diff --git a/src/Indiko.OpenWeatherClient/Models/Weather.cs b/src/Indiko.OpenWeatherClient/Models/Weather.cs new file mode 100644 index 0000000..66a6f74 --- /dev/null +++ b/src/Indiko.OpenWeatherClient/Models/Weather.cs @@ -0,0 +1,76 @@ +using Indiko.OpenWeatherClient.Converter; +using System.Text.Json.Serialization; + +namespace Indiko.OpenWeatherClient.Models; +public class Weather +{ + /// + /// Gets the date and time of the weather data point. + /// This property is represented as a Unix timestamp in the JSON data. + /// + [JsonPropertyName("dt")] + [JsonConverter(typeof(UnixDateTimeConverter))] + public DateTime? DateTime { get; init; } + + /// + /// Gets the atmospheric pressure in unit specified. + /// + [JsonPropertyName("pressure")] + public int Pressure { get; init; } + + /// + /// Gets the humidity percentage. + /// + [JsonPropertyName("humidity")] + public int Humidity { get; init; } + + /// + /// Gets the dew point temperature in unit specified. + /// + [JsonPropertyName("dew_point")] + public float DewPoint { get; init; } + + /// + /// Gets the current ultraviolet index. + /// + [JsonPropertyName("uvi")] + public float UvIndex { get; init; } + + /// + /// Gets the cloud coverage percentage. + /// + [JsonPropertyName("clouds")] + public int Clouds { get; init; } + + /// + /// Gets the visibility in unit specified. + /// + [JsonPropertyName("visibility")] + public int Visibility { get; init; } + + /// + /// Gets the wind speed in unit specified. + /// + [JsonPropertyName("wind_speed")] + public float WindSpeed { get; init; } + + /// + /// Gets the wind direction in degrees (meteorological). + /// + [JsonPropertyName("wind_deg")] + public int WindDeg { get; init; } + + /// + /// Gets the wind gust speed in unit specified. + /// + [JsonPropertyName("wind_gust")] + public float WindGust { get; init; } + + /// + /// Gets an array of weather conditions information. + /// This property will be ignored when writing to JSON if the value is null. + /// + [JsonPropertyName("weather")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public WeatherInformation[] WeatherInformation { get; init; } +} \ No newline at end of file diff --git a/src/Indiko.OpenWeatherClient/Models/WeatherInformation.cs b/src/Indiko.OpenWeatherClient/Models/WeatherInformation.cs new file mode 100644 index 0000000..5a825d6 --- /dev/null +++ b/src/Indiko.OpenWeatherClient/Models/WeatherInformation.cs @@ -0,0 +1,46 @@ +using System.Text.Json.Serialization; + +namespace Indiko.OpenWeatherClient.Models; + +/// +/// Represents detailed information about weather conditions. +/// +public class WeatherInformation +{ + /// + /// Gets the weather condition ID, a unique identifier for a specific weather condition type. + /// + [JsonPropertyName("id")] + [JsonIgnore(Condition = JsonIgnoreCondition.Never)] + public int Id { get; init; } + + /// + /// Gets the main group to which the weather condition belongs, such as "Rain", "Snow", etc. + /// This property will be ignored when writing to JSON if the value is null. + /// + [JsonPropertyName("main")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string Main { get; init; } + + /// + /// Gets a more detailed description of the weather condition. + /// This property will be ignored when writing to JSON if the value is null. + /// + [JsonPropertyName("description")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string Description { get; init; } + + /// + /// Gets the icon code representing the weather condition visually. + /// This property will be ignored when writing to JSON if the value is null. + /// + [JsonPropertyName("icon")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string Icon { get; init; } + + [JsonIgnore(Condition = JsonIgnoreCondition.Always)] + public string Icon100Url => !string.IsNullOrEmpty(Icon) ? $"https://openweathermap.org/img/wn/{Icon}@2x.png" : string.Empty; + + [JsonIgnore(Condition = JsonIgnoreCondition.Always)] + public string Icon200Url => !string.IsNullOrEmpty(Icon) ? $"https://openweathermap.org/img/wn/{Icon}@4x.png" : string.Empty; +} diff --git a/src/Indiko.OpenWeatherClient/OpenWeatherClient.cs b/src/Indiko.OpenWeatherClient/OpenWeatherClient.cs new file mode 100644 index 0000000..9b62415 --- /dev/null +++ b/src/Indiko.OpenWeatherClient/OpenWeatherClient.cs @@ -0,0 +1,189 @@ +using Indiko.OpenWeatherClient.Builder; +using Indiko.OpenWeatherClient.Exceptions; +using Indiko.OpenWeatherClient.Interfaces; +using Indiko.OpenWeatherClient.Models; +using System.Net.Http; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace Indiko.OpenWeatherClient; + +/// +/// A client for interacting with the OpenWeather API to fetch weather data and map tile data. +/// +public class OpenWeatherClient : IOpenWeatherClient, IDisposable +{ + private readonly JsonSerializerOptions _jsonSerializerOptions; + private HttpClient _httpClient; + + public OpenWeatherClient() + { + _jsonSerializerOptions = new JsonSerializerOptions + { + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + WriteIndented = true, + ReferenceHandler = ReferenceHandler.Preserve + }; + _jsonSerializerOptions.Converters.Add(new Converter.UnixDateTimeConverter()); + + _httpClient = new HttpClient(); + } + + /// + /// Asynchronously retrieves the current weather data. + /// + /// Configuration for the API request. + /// Cancellation token for the async operation. + /// A object containing the current weather data. + public async Task GetCurrentWeatherAsync(OpenWeatherRequest request, CancellationToken cancellationToken = default) + { + var openWeatherResponse = await GetResponseInternal(request, cancellationToken); + + return openWeatherResponse.Current; + } + + /// + /// Asynchronously retrieves the daily weather forecast. + /// + /// Configuration for the API request. + /// Cancellation token for the async operation. + /// An array of objects containing daily weather data. + public async Task GetDailyWeatherAsync(OpenWeatherRequest request, CancellationToken cancellationToken = default) + { + var openWeatherResponse = await GetResponseInternal(request, cancellationToken); + return openWeatherResponse.Daily; + } + + /// + /// Asynchronously retrieves the hourly weather forecast. + /// + /// Configuration for the API request. + /// Cancellation token for the async operation. + /// An array of objects containing hourly weather data. + public async Task GetHourlyWeatherAsync(OpenWeatherRequest request, CancellationToken cancellationToken = default) + { + var openWeatherResponse = await GetResponseInternal(request, cancellationToken); + return openWeatherResponse.Hourly; + } + + /// + /// Asynchronously retrieves the complete weather data based on the specified request configuration. + /// + /// Configuration for the API request. + /// Cancellation token for the async operation. + /// An object containing comprehensive weather data. + public async Task GetWeatherAsync(OpenWeatherRequest request, CancellationToken cancellationToken = default) + { + var openWeatherResponse = await GetResponseInternal(request, cancellationToken); + return openWeatherResponse; + } + + private async Task GetResponseInternal(OpenWeatherRequest request, CancellationToken cancellationToken = default) + { + OpenWeatherRequestBuilder requestBuilder = new(request.ApiKey); + + if (!string.IsNullOrWhiteSpace(request.City) && !string.IsNullOrWhiteSpace(request.Country)) + { + requestBuilder.WithCity(request.City, request.Country); + } + else if (request.Latitude != 0 && request.Longitude != 0) + { + requestBuilder.WithLocation(request.Latitude, request.Longitude); + } + + if (!string.IsNullOrWhiteSpace(request.Language)) + { + requestBuilder.WithLanguage(request.Language); + } + + if (!string.IsNullOrWhiteSpace(request.Unit)) + { + requestBuilder.WithUnit(request.Unit); + } + + if (request.Date.HasValue) + { + requestBuilder.WithDate(request.Date.Value); + } + + if (request.Excludes?.Length > 0) + { + requestBuilder.WithExcludes(request.Excludes); + } + + var requestUrl = requestBuilder.Build(); + var requestMessage = new HttpRequestMessage(HttpMethod.Get, requestUrl); + + var response = await _httpClient.SendAsync(requestMessage, cancellationToken) + .ConfigureAwait(false); + + switch (response.StatusCode) + { + case System.Net.HttpStatusCode.OK: + { + var content = await response.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); + + return JsonSerializer.Deserialize(content, _jsonSerializerOptions); + } + default: + { + var content = await response.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); + throw new OpenWeatherException((int)response.StatusCode, content); + } + } + } + + /// + /// Constructs a URI for a specific map tile from the OpenWeatherMap tile service. + /// + /// Configuration for the map tile request. + /// A URI pointing to the map tile image. + public Uri GetMapTileUri(OpenWeatherMapTileRequest mapTileRequest) + { + if (string.IsNullOrEmpty(mapTileRequest.ApiKey)) + { + throw new ArgumentNullException(nameof(mapTileRequest.ApiKey)); + } + + if (string.IsNullOrEmpty(mapTileRequest.Layer)) + { + throw new ArgumentNullException(nameof(mapTileRequest.Layer)); + } + + string url = $"https://tile.openweathermap.org/map/{mapTileRequest.Layer}/{mapTileRequest.ZoomLevel}/{mapTileRequest.X}/{mapTileRequest.Y}.png?appid={mapTileRequest.ApiKey}"; + return new Uri(url); + } + + /// + /// Asynchronously retrieves a map tile image from the OpenWeatherMap tile service. + /// + /// Configuration for the map tile request. + /// Cancellation token for the async operation. + /// A byte array containing the map tile image. + public async Task GetMapTileAsync(OpenWeatherMapTileRequest mapTileRequest, CancellationToken cancellationToken = default) + { + var url = GetMapTileUri(mapTileRequest); + return await GetInternalMapTileAsync(url, cancellationToken); + } + + public async Task GetInternalMapTileAsync(Uri url, CancellationToken cancellationToken = default) + { + var response = await _httpClient.GetByteArrayAsync(url, cancellationToken) + .ConfigureAwait(false); + + return response; + } + + public void Dispose() + { + if (_httpClient != null) + { + _httpClient.CancelPendingRequests(); + _httpClient.Dispose(); + _httpClient = null; + } + } +} \ No newline at end of file diff --git a/src/Indiko.OpenWeatherClient/Usings.cs b/src/Indiko.OpenWeatherClient/Usings.cs new file mode 100644 index 0000000..fb5a3e3 --- /dev/null +++ b/src/Indiko.OpenWeatherClient/Usings.cs @@ -0,0 +1,2 @@ +global using System; +global using System.Threading; diff --git a/src/Indiko.OpenWeatherClient/icon.png b/src/Indiko.OpenWeatherClient/icon.png new file mode 100644 index 0000000..7ed7e39 Binary files /dev/null and b/src/Indiko.OpenWeatherClient/icon.png differ diff --git a/tests/Indiko.OpenWeatherClient.Tests/Indiko.OpenWeatherClient.Tests.csproj b/tests/Indiko.OpenWeatherClient.Tests/Indiko.OpenWeatherClient.Tests.csproj new file mode 100644 index 0000000..05cfa3d --- /dev/null +++ b/tests/Indiko.OpenWeatherClient.Tests/Indiko.OpenWeatherClient.Tests.csproj @@ -0,0 +1,34 @@ + + + + net8.0 + enable + enable + + false + true + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + diff --git a/tests/Indiko.OpenWeatherClient.Tests/IntegrationTestFixture.cs b/tests/Indiko.OpenWeatherClient.Tests/IntegrationTestFixture.cs new file mode 100644 index 0000000..ef224c6 --- /dev/null +++ b/tests/Indiko.OpenWeatherClient.Tests/IntegrationTestFixture.cs @@ -0,0 +1,21 @@ +namespace Indiko.OpenWeatherClient.Tests; +public class IntegrationTestFixture : IDisposable +{ + + public string ApiKey { get; } + + public OpenWeatherClient OpenWeatherClient { get; } + + public IntegrationTestFixture() + { + // Initialize the test environment + OpenWeatherClient = new OpenWeatherClient(); + ApiKey = "912478bc99153f9f23fbbfbb05572322"; + } + + public void Dispose() + { + // Clean up the test environment + OpenWeatherClient.Dispose(); + } +} diff --git a/tests/Indiko.OpenWeatherClient.Tests/OpenWeatherClientIntegrationTests.cs b/tests/Indiko.OpenWeatherClient.Tests/OpenWeatherClientIntegrationTests.cs new file mode 100644 index 0000000..339dc66 --- /dev/null +++ b/tests/Indiko.OpenWeatherClient.Tests/OpenWeatherClientIntegrationTests.cs @@ -0,0 +1,93 @@ +using Indiko.OpenWeatherClient.Models; +using FluentAssertions; + +namespace Indiko.OpenWeatherClient.Tests; + +public class OpenWeatherClientIntegrationTests(IntegrationTestFixture fixture) : IClassFixture +{ + private readonly IntegrationTestFixture _fixture = fixture; + + [Fact] + public async Task GetCurrentWeatherAsync_ReturnsCorrectWeatherData() + { + // Arrange + var request = new OpenWeatherRequest + { + ApiKey = _fixture.ApiKey, + Latitude = 40.712776, + Longitude = -74.005974, + Language = Constants.Languages.English, + Unit = Constants.Units.Metric, + Excludes = [Constants.Excludes.Minutely, Constants.Excludes.Daily, Constants.Excludes.Hourly] + }; + + // Act + var result = await _fixture.OpenWeatherClient.GetCurrentWeatherAsync(request); + + // Assert + result.Should().NotBeNull(); + } + + [Fact] + public async Task GetDailyWeatherAsync_ReturnsCorrectWeatherData() + { + // Arrange + var request = new OpenWeatherRequest + { + ApiKey = _fixture.ApiKey, + Latitude = 40.712776, + Longitude = -74.005974, + Language = Constants.Languages.English, + Unit = Constants.Units.Metric, + Excludes = [Constants.Excludes.Minutely, Constants.Excludes.Hourly, Constants.Excludes.Current] + }; + + // Act + var result = await _fixture.OpenWeatherClient.GetDailyWeatherAsync(request); + + // Assert + result.Should().NotBeNull(); + } + + [Fact] + public async Task GetHourlyWeatherAsync_ReturnsCorrectWeatherData() + { + // Arrange + var request = new OpenWeatherRequest + { + ApiKey = _fixture.ApiKey, + Latitude = 40.712776, + Longitude = -74.005974, + Language = Constants.Languages.English, + Unit = Constants.Units.Metric, + Excludes = [Constants.Excludes.Minutely, Constants.Excludes.Daily, Constants.Excludes.Current] + }; + + // Act + var result = await _fixture.OpenWeatherClient.GetHourlyWeatherAsync(request); + + // Assert + result.Should().NotBeNull(); + } + + [Fact] + public async Task GetWeatherAsync_ReturnsCorrectWeatherData() + { + // Arrange + var request = new OpenWeatherRequest + { + ApiKey = _fixture.ApiKey, + Latitude = 40.712776, + Longitude = -74.005974, + Language = Constants.Languages.English, + Unit = Constants.Units.Metric, + Excludes = [Constants.Excludes.Minutely] + }; + + // Act + var result = await _fixture.OpenWeatherClient.GetWeatherAsync(request); + + // Assert + result.Should().NotBeNull(); + } +} \ No newline at end of file