diff --git a/src/Api/Card.cs b/src/Api/Card.cs index 63715f0..99974b5 100644 --- a/src/Api/Card.cs +++ b/src/Api/Card.cs @@ -1,14 +1,16 @@ -namespace JpnCardsPokemonSdk.Api; +using JpnCardsPokemonSdk.Client.Endpoints; -public class Card +namespace JpnCardsPokemonSdk.Api; + +public class Card : EndpointObject { - public string Name { get; set; } + public string? Name { get; set; } public int Id { get; set; } public Set? Set { get; set; } - public string[] Types { get; set; } + public string[]? Types { get; set; } public int Hp { get; set; } = -1; @@ -32,11 +34,11 @@ public class Card public int? ConvertedRetreadCost { get; set; } - public string Supertype { get; set; } + public string? Supertype { get; set; } public string[]? Subtypes { get; set; } - public string Rarity { get; set; } + public string? Rarity { get; set; } // TODO: Type of property is not documented. Has to be evaluated at a later time. // public Legality[]? Legalities { get; set; } @@ -47,7 +49,7 @@ public class Card public int Number { get; set; } - public string PrintedNumber { get; set; } + public string? PrintedNumber { get; set; } public int Uuid { get; set; } } \ No newline at end of file diff --git a/src/Api/Set.cs b/src/Api/Set.cs index 18f8ee5..8ba6e8c 100644 --- a/src/Api/Set.cs +++ b/src/Api/Set.cs @@ -1,8 +1,10 @@ -namespace JpnCardsPokemonSdk.Api; +using JpnCardsPokemonSdk.Client.Endpoints; -public class Set +namespace JpnCardsPokemonSdk.Api; + +public class Set : EndpointObject { - public string Name { get; set; } + public string? Name { get; set; } public int Id { get; set; } @@ -10,7 +12,7 @@ public class Set public string? ImageUrl { get; set; } - public string Language { get; set; } + public string? Language { get; set; } public int Year { get; set; } @@ -21,7 +23,7 @@ public class Set public int PrintedCardCount { get; set; } - public string SetCode { get; set; } + public string? SetCode { get; set; } public int Uuid { get; set; } } \ No newline at end of file diff --git a/src/Client/ApiClient.cs b/src/Client/ApiClient.cs new file mode 100644 index 0000000..d16cb8c --- /dev/null +++ b/src/Client/ApiClient.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Net.Http.Json; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using JpnCardsPokemonSdk.Client.Endpoints; +using JpnCardsPokemonSdk.Client.Responses; + +namespace JpnCardsPokemonSdk.Client; + +public class ApiClient +{ + private readonly HttpClient _client; + +#if NETCOREAPP3_1_OR_GREATER + public ApiClient(SocketsHttpHandler handler) : this(new HttpClient(handler)) + { + } +#endif + + public ApiClient() : this(new HttpClient()) + { + } + + public ApiClient(HttpClient client) + { + _client = client; + + _client.BaseAddress = new Uri("https://www.jpn-cards.com/v2/"); + _client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + _client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue( + new ProductHeaderValue("JpnCardsPokemonSdkCS", GetType().Assembly.GetName().Version?.ToString()))); + } + + public async Task FetchDataAsync(string requestUri) + where TResponseType : IApiResponse, new() + { + var options = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + IncludeFields = true, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + }; + + var response = await _client.GetFromJsonAsync(requestUri, options); + + if (response is IPageableApiResponse pageAbleApiResponse) + { + pageAbleApiResponse.CurrentApiClient = this; + pageAbleApiResponse.RememberRequestUri(requestUri); + } + + return response; + } + + public async Task?> FetchDataAsync(string? query = null, int page = 1) + where T : EndpointObject + { + var endpoint = EndpointFactory.GetApiEndpoint(); + + return await FetchDataAsync, IEnumerable>($"{endpoint.ApiUri()}?page={page}"); + } + + public async Task?> FetchByIdAsync(int id) where T : EndpointObject + { + var endpoint = EndpointFactory.GetApiEndpoint(); + + return await FetchDataAsync, T>($"{endpoint.ApiUri()}/id={id}"); + } + + public async Task?> FetchByUuigAsync(int uuid) where T : EndpointObject + { + var endpoint = EndpointFactory.GetApiEndpoint(); + + return await FetchDataAsync, T>($"{endpoint.ApiUri()}/uuid={uuid}"); + } +} \ No newline at end of file diff --git a/src/Client/Endpoints/CardEndpoint.cs b/src/Client/Endpoints/CardEndpoint.cs new file mode 100644 index 0000000..41ec19c --- /dev/null +++ b/src/Client/Endpoints/CardEndpoint.cs @@ -0,0 +1,9 @@ +namespace JpnCardsPokemonSdk.Client.Endpoints; + +internal class CardEndpoint : IApiEndpoint +{ + string IApiEndpoint.ApiUri() + { + return "card"; + } +} \ No newline at end of file diff --git a/src/Client/Endpoints/EndpointFactory.cs b/src/Client/Endpoints/EndpointFactory.cs new file mode 100644 index 0000000..0c71ca7 --- /dev/null +++ b/src/Client/Endpoints/EndpointFactory.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; + +namespace JpnCardsPokemonSdk.Client.Endpoints; + +internal static class EndpointFactory +{ + static EndpointFactory() + { + var knownTypes = Assembly.GetExecutingAssembly().GetTypes().Where(t => + typeof(EndpointObject).IsAssignableFrom(t) && + t != typeof(EndpointObject)); + + foreach (var knownType in knownTypes) RuntimeHelpers.RunClassConstructor(knownType.TypeHandle); + } + + private static Dictionary EndpointMapping { get; } = new(); + + public static void RegisterTypeEndpoint(IApiEndpoint endpoint) where T : EndpointObject + { + EndpointMapping.Add(typeof(T), endpoint); + } + + public static IApiEndpoint GetApiEndpoint() where T : EndpointObject + { + foreach (var endpointMappingKey in EndpointMapping.Keys.Where(endpointMappingKey => + typeof(T) == endpointMappingKey)) + return EndpointMapping[endpointMappingKey]; + + // Todo: Custom exception class + throw new Exception($"No endpoint had been found for ${typeof(T).FullName}"); + } +} \ No newline at end of file diff --git a/src/Client/Endpoints/EndpointObject.cs b/src/Client/Endpoints/EndpointObject.cs new file mode 100644 index 0000000..d84ecf2 --- /dev/null +++ b/src/Client/Endpoints/EndpointObject.cs @@ -0,0 +1,5 @@ +namespace JpnCardsPokemonSdk.Client.Endpoints; + +public abstract class EndpointObject +{ +} \ No newline at end of file diff --git a/src/Client/Endpoints/IApiEndpoint.cs b/src/Client/Endpoints/IApiEndpoint.cs new file mode 100644 index 0000000..2d5945a --- /dev/null +++ b/src/Client/Endpoints/IApiEndpoint.cs @@ -0,0 +1,6 @@ +namespace JpnCardsPokemonSdk.Client.Endpoints; + +public interface IApiEndpoint +{ + string ApiUri(); +} \ No newline at end of file diff --git a/src/Client/Endpoints/SetEndpoint.cs b/src/Client/Endpoints/SetEndpoint.cs new file mode 100644 index 0000000..130c939 --- /dev/null +++ b/src/Client/Endpoints/SetEndpoint.cs @@ -0,0 +1,9 @@ +namespace JpnCardsPokemonSdk.Client.Endpoints; + +internal class SetEndpoint : IApiEndpoint +{ + string IApiEndpoint.ApiUri() + { + return "card"; + } +} \ No newline at end of file diff --git a/src/Client/Responses/EnumerableApiResponse.cs b/src/Client/Responses/EnumerableApiResponse.cs new file mode 100644 index 0000000..2cbe2ec --- /dev/null +++ b/src/Client/Responses/EnumerableApiResponse.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using JpnCardsPokemonSdk.Client.Endpoints; + +namespace JpnCardsPokemonSdk.Client.Responses; + +public class EnumerableApiResponse : IApiResponse>, + IPageableApiResponse, IEnumerable> where T : EndpointObject +{ + private string? RequestUri { get; set; } + + public int TotalPages => (int)Math.Ceiling((decimal)( + (IPageableApiResponse, IEnumerable>)this).TotalCount / ( + (IPageableApiResponse, IEnumerable>)this).PageSize); + + IEnumerable? IApiResponse>.Data { get; set; } + + ApiClient? IPageableApiResponse, IEnumerable>.CurrentApiClient { get; set; } + + int IPageableApiResponse, IEnumerable>.Page { get; set; } + + int IPageableApiResponse, IEnumerable>.PageSize { get; set; } + + int IPageableApiResponse, IEnumerable>.Count { get; set; } + + int IPageableApiResponse, IEnumerable>.TotalCount { get; set; } + + async Task> IPageableApiResponse, IEnumerable>. + FetchNextPageAsync() + { + return await ((IPageableApiResponse, IEnumerable>)this).FetchPageAsync(( + (IPageableApiResponse, IEnumerable>)this).Page + 1); + } + + async Task?> IPageableApiResponse, IEnumerable>. + FetchPageAsync(int page) + { + var requestUri = RequestUri + "&page=" + page; + + return await ((IPageableApiResponse, IEnumerable>)this).CurrentApiClient + ?.FetchDataAsync, IEnumerable>(requestUri)!; + } + + void IPageableApiResponse, IEnumerable>.RememberRequestUri(string requestUri) + { + // Remember full Uri without page + RequestUri = Regex.Replace(requestUri, @"page=\d*&?", ""); + } +} \ No newline at end of file diff --git a/src/Client/Responses/IApiResponse.cs b/src/Client/Responses/IApiResponse.cs new file mode 100644 index 0000000..00bbbc8 --- /dev/null +++ b/src/Client/Responses/IApiResponse.cs @@ -0,0 +1,6 @@ +namespace JpnCardsPokemonSdk.Client.Responses; + +public interface IApiResponse +{ + T? Data { get; set; } +} \ No newline at end of file diff --git a/src/Client/Responses/IPageableApiResponse.cs b/src/Client/Responses/IPageableApiResponse.cs new file mode 100644 index 0000000..5fba181 --- /dev/null +++ b/src/Client/Responses/IPageableApiResponse.cs @@ -0,0 +1,23 @@ +using System.Threading.Tasks; + +namespace JpnCardsPokemonSdk.Client.Responses; + +public interface IPageableApiResponse + where TResponseType : IApiResponse +{ + ApiClient? CurrentApiClient { get; set; } + + int Page { get; set; } + + int PageSize { get; set; } + + int Count { get; set; } + + int TotalCount { get; set; } + + void RememberRequestUri(string requestUri); + + Task FetchNextPageAsync(); + + Task FetchPageAsync(int page); +} \ No newline at end of file diff --git a/src/Client/Responses/SingleApiResponse.cs b/src/Client/Responses/SingleApiResponse.cs new file mode 100644 index 0000000..6cbe381 --- /dev/null +++ b/src/Client/Responses/SingleApiResponse.cs @@ -0,0 +1,8 @@ +using JpnCardsPokemonSdk.Client.Endpoints; + +namespace JpnCardsPokemonSdk.Client.Responses; + +public class SingleApiResponse : IApiResponse where T : EndpointObject +{ + T? IApiResponse.Data { get; set; } +} \ No newline at end of file diff --git a/src/JpnCardsPokemonSdk.csproj b/src/JpnCardsPokemonSdk.csproj index 4d0d4f3..8b00418 100644 --- a/src/JpnCardsPokemonSdk.csproj +++ b/src/JpnCardsPokemonSdk.csproj @@ -1,7 +1,7 @@ - net7.0;net6.0;net461;netstandard2.0;netstandard2.1 + net7.0;net6.0;net462;netstandard2.0;netstandard2.1 True ..\docs\JpnCardsPokemonSdk.xml True @@ -38,6 +38,8 @@ + + all runtime; build; native; contentfiles; analyzers; buildtransitive