diff --git a/SpotifyAPI.Web.Tests/Clients/SpotifyClientConfigTest.cs b/SpotifyAPI.Web.Tests/Clients/SpotifyClientConfigTest.cs index 8560ae31..2d30b967 100644 --- a/SpotifyAPI.Web.Tests/Clients/SpotifyClientConfigTest.cs +++ b/SpotifyAPI.Web.Tests/Clients/SpotifyClientConfigTest.cs @@ -15,7 +15,7 @@ namespace SpotifyAPI.Web Assert.IsInstanceOf(typeof(SimplePaginator), defaultConfig.DefaultPaginator); Assert.IsInstanceOf(typeof(NetHttpClient), defaultConfig.HTTPClient); - Assert.IsInstanceOf(typeof(NewtonsoftJSONSerializer), defaultConfig.JSONSerializer); + Assert.IsInstanceOf(typeof(TextJsonSerializer), defaultConfig.JSONSerializer); Assert.AreEqual(SpotifyUrls.APIV1, defaultConfig.BaseAddress); Assert.AreEqual(null, defaultConfig.Authenticator); Assert.AreEqual(null, defaultConfig.HTTPLogger); @@ -32,7 +32,7 @@ namespace SpotifyAPI.Web Assert.IsInstanceOf(typeof(SimplePaginator), defaultConfig.DefaultPaginator); Assert.IsInstanceOf(typeof(NetHttpClient), defaultConfig.HTTPClient); - Assert.IsInstanceOf(typeof(NewtonsoftJSONSerializer), defaultConfig.JSONSerializer); + Assert.IsInstanceOf(typeof(TextJsonSerializer), defaultConfig.JSONSerializer); Assert.AreEqual(SpotifyUrls.APIV1, defaultConfig.BaseAddress); Assert.AreEqual(null, defaultConfig.HTTPLogger); Assert.AreEqual(null, defaultConfig.RetryHandler); diff --git a/SpotifyAPI.Web.Tests/Http/ExampleResponses.cs b/SpotifyAPI.Web.Tests/Http/ExampleResponses.cs new file mode 100644 index 00000000..0d0b7362 --- /dev/null +++ b/SpotifyAPI.Web.Tests/Http/ExampleResponses.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SpotifyAPI.Web.Tests +{ + public static class ExampleResponses + { + public static readonly string CurrentlyPlayingContext = "{\n \"device\" : {\n \"id\" : \"~~~~~~~~~~~~~~~~~~~~\",\n \"is_active\" : true,\n \"is_private_session\" : false,\n \"is_restricted\" : false,\n \"name\" : \"aj\",\n \"type\" : \"Smartphone\",\n \"volume_percent\" : 100\n },\n \"shuffle_state\" : false,\n \"repeat_state\" : \"off\",\n \"timestamp\" : 1640197100877,\n \"context\" : {\n \"external_urls\" : {\n \"spotify\" : \"https://open.spotify.com/playlist/4UKLNvyjaoifjseoifjVj0ff\"\n },\n \"href\" : \"https://api.spotify.com/v1/playlists/4UKaegijsogkUyn74Vj0ff\",\n \"type\" : \"playlist\",\n \"uri\" : \"spotify:playlist:wioajdiawfawfaowidjawio\"\n },\n \"progress_ms\" : 82163,\n \"item\" : {\n \"album\" : {\n \"album_type\" : \"album\",\n \"artists\" : [ {\n \"external_urls\" : {\n \"spotify\" : \"https://open.spotify.com/artist/2nSY0EIzz3eM6Y6Pc7VXkO\"\n },\n \"href\" : \"https://api.spotify.com/v1/artists/2nSY0awfawfeM6Y6Pc7VXkO\",\n \"id\" : \"2nSaefaefefY6Pc7VXkO\",\n \"name\" : \"Ill Considered\",\n \"type\" : \"artist\",\n \"uri\" : \"spotify:artist:2nSY0EIzz3eM6Y6Pc7VXkO\"\n } ],\n \"available_markets\" : [ \"AD\", \"AE\", \"AG\", \"AL\", \"AM\", \"AO\", \"AR\", \"AT\", \"AU\", \"AZ\", \"BA\", \"BB\", \"BD\", \"BE\", \"BF\", \"BG\", \"BH\", \"BI\", \"BJ\", \"BN\", \"BO\", \"BR\", \"BS\", \"BT\", \"BW\", \"BY\", \"BZ\", \"CA\", \"CD\", \"CG\", \"CH\", \"CI\", \"CL\", \"CM\", \"CO\", \"CR\", \"CV\", \"CW\", \"CY\", \"CZ\", \"DE\", \"DJ\", \"DK\", \"DM\", \"DO\", \"DZ\", \"EC\", \"EE\", \"EG\", \"ES\", \"FI\", \"FJ\", \"FM\", \"FR\", \"GA\", \"GB\", \"GD\", \"GE\", \"GH\", \"GM\", \"GN\", \"GQ\", \"GR\", \"GT\", \"GW\", \"GY\", \"HK\", \"HN\", \"HR\", \"HT\", \"HU\", \"ID\", \"IE\", \"IL\", \"IN\", \"IQ\", \"IS\", \"IT\", \"JM\", \"JO\", \"JP\", \"KE\", \"KG\", \"KH\", \"KI\", \"KM\", \"KN\", \"KR\", \"KW\", \"KZ\", \"LA\", \"LB\", \"LC\", \"LI\", \"LK\", \"LR\", \"LS\", \"LT\", \"LU\", \"LV\", \"LY\", \"MA\", \"MC\", \"MD\", \"ME\", \"MG\", \"MH\", \"MK\", \"ML\", \"MN\", \"MO\", \"MR\", \"MT\", \"MU\", \"MV\", \"MW\", \"MX\", \"MY\", \"MZ\", \"NA\", \"NE\", \"NG\", \"NI\", \"NL\", \"NO\", \"NP\", \"NR\", \"NZ\", \"OM\", \"PA\", \"PE\", \"PG\", \"PH\", \"PK\", \"PL\", \"PS\", \"PT\", \"PW\", \"PY\", \"QA\", \"RO\", \"RS\", \"RU\", \"RW\", \"SA\", \"SB\", \"SC\", \"SE\", \"SG\", \"SI\", \"SK\", \"SL\", \"SM\", \"SN\", \"SR\", \"ST\", \"SV\", \"SZ\", \"TD\", \"TG\", \"TH\", \"TJ\", \"TL\", \"TN\", \"TO\", \"TR\", \"TT\", \"TV\", \"TW\", \"TZ\", \"UA\", \"UG\", \"US\", \"UY\", \"UZ\", \"VC\", \"VE\", \"VN\", \"VU\", \"WS\", \"XK\", \"ZA\", \"ZM\", \"ZW\" ],\n \"external_urls\" : {\n \"spotify\" : \"https://open.spotify.com/album/3i62521pVAxy4LG7dQOSYQ\"\n },\n \"href\" : \"https://api.spotify.com/v1/albums/3i62521pVAxy4LG7dQOSYQ\",\n \"id\" : \"3i62521pVAxy4LG7dQOSYQ\",\n \"images\" : [ {\n \"height\" : 640,\n \"url\" : \"https://i.scdn.co/image/ab67616d0000b27374680cadadf4e40653791fb1\",\n \"width\" : 640\n }, {\n \"height\" : 300,\n \"url\" : \"https://i.scdn.co/image/ab67616d00001e0274680cadadf4e40653791fb1\",\n \"width\" : 300\n }, {\n \"height\" : 64,\n \"url\" : \"https://i.scdn.co/image/ab67616d0000485174680cadadf4e40653791fb1\",\n \"width\" : 64\n } ],\n \"name\" : \"Liminal Space\",\n \"release_date\" : \"2021-11-12\",\n \"release_date_precision\" : \"day\",\n \"total_tracks\" : 10,\n \"type\" : \"album\",\n \"uri\" : \"spotify:album:3i62521pVAxy4LG7dQOSYQ\"\n },\n \"artists\" : [ {\n \"external_urls\" : {\n \"spotify\" : \"https://open.spotify.com/artist/2nSY0EIzz3eM6Y6Pc7VXkO\"\n },\n \"href\" : \"https://api.spotify.com/v1/artists/2nSY0EIzz3eM6Y6Pc7VXkO\",\n \"id\" : \"2nSY0EIzz3eM6Y6Pc7VXkO\",\n \"name\" : \"Ill Considered\",\n \"type\" : \"artist\",\n \"uri\" : \"spotify:artist:2nSY0EIzz3eM6Y6Pc7VXkO\"\n } ],\n \"available_markets\" : [ \"AD\", \"AE\", \"AG\", \"AL\", \"AM\", \"AO\", \"AR\", \"AT\", \"AU\", \"AZ\", \"BA\", \"BB\", \"BD\", \"BE\", \"BF\", \"BG\", \"BH\", \"BI\", \"BJ\", \"BN\", \"BO\", \"BR\", \"BS\", \"BT\", \"BW\", \"BY\", \"BZ\", \"CA\", \"CD\", \"CG\", \"CH\", \"CI\", \"CL\", \"CM\", \"CO\", \"CR\", \"CV\", \"CW\", \"CY\", \"CZ\", \"DE\", \"DJ\", \"DK\", \"DM\", \"DO\", \"DZ\", \"EC\", \"EE\", \"EG\", \"ES\", \"FI\", \"FJ\", \"FM\", \"FR\", \"GA\", \"GB\", \"GD\", \"GE\", \"GH\", \"GM\", \"GN\", \"GQ\", \"GR\", \"GT\", \"GW\", \"GY\", \"HK\", \"HN\", \"HR\", \"HT\", \"HU\", \"ID\", \"IE\", \"IL\", \"IN\", \"IQ\", \"IS\", \"IT\", \"JM\", \"JO\", \"JP\", \"KE\", \"KG\", \"KH\", \"KI\", \"KM\", \"KN\", \"KR\", \"KW\", \"KZ\", \"LA\", \"LB\", \"LC\", \"LI\", \"LK\", \"LR\", \"LS\", \"LT\", \"LU\", \"LV\", \"LY\", \"MA\", \"MC\", \"MD\", \"ME\", \"MG\", \"MH\", \"MK\", \"ML\", \"MN\", \"MO\", \"MR\", \"MT\", \"MU\", \"MV\", \"MW\", \"MX\", \"MY\", \"MZ\", \"NA\", \"NE\", \"NG\", \"NI\", \"NL\", \"NO\", \"NP\", \"NR\", \"NZ\", \"OM\", \"PA\", \"PE\", \"PG\", \"PH\", \"PK\", \"PL\", \"PS\", \"PT\", \"PW\", \"PY\", \"QA\", \"RO\", \"RS\", \"RU\", \"RW\", \"SA\", \"SB\", \"SC\", \"SE\", \"SG\", \"SI\", \"SK\", \"SL\", \"SM\", \"SN\", \"SR\", \"ST\", \"SV\", \"SZ\", \"TD\", \"TG\", \"TH\", \"TJ\", \"TL\", \"TN\", \"TO\", \"TR\", \"TT\", \"TV\", \"TW\", \"TZ\", \"UA\", \"UG\", \"US\", \"UY\", \"UZ\", \"VC\", \"VE\", \"VN\", \"VU\", \"WS\", \"XK\", \"ZA\", \"ZM\", \"ZW\" ],\n \"disc_number\" : 1,\n \"duration_ms\" : 375520,\n \"explicit\" : false,\n \"external_ids\" : {\n \"isrc\" : \"GBX722100380\"\n },\n \"external_urls\" : {\n \"spotify\" : \"https://open.spotify.com/track/0aCRNUPT7heEbCOlfE1ToW\"\n },\n \"href\" : \"https://api.spotify.com/v1/tracks/0aCRNUPT7heEbCOlfE1ToW\",\n \"id\" : \"0aCRNUPT7heEbCOlfE1ToW\",\n \"is_local\" : false,\n \"name\" : \"Loosed\",\n \"popularity\" : 30,\n \"preview_url\" : \"https://p.scdn.co/mp3-preview/9439ee3d3ca90405ab20079ef88411283612ad4b?cid=7939a979a2384b4fac7cd23e48fb1b85\",\n \"track_number\" : 3,\n \"type\" : \"track\",\n \"uri\" : \"spotify:track:0aCRNUPT7heEbCOlfE1ToW\"\n },\n \"currently_playing_type\" : \"track\",\n \"actions\" : {\n \"disallows\" : {\n \"resuming\" : true\n }\n },\n \"is_playing\" : true\n}"; + + public static readonly string PublicUser = "{\n \"display_name\" : \"username\",\n \"external_urls\" : {\n \"spotify\" : \"https://open.spotify.com/user/username\"\n },\n \"followers\" : {\n \"href\" : null,\n \"total\" : 42\n },\n \"href\" : \"https://api.spotify.com/v1/users/username\",\n \"id\" : \"username\",\n \"images\" : [ {\n \"height\" : null,\n \"url\" : \"https://i.scdn.co/image/aawdagsghdthftjssefsfsdf14c4\",\n \"width\" : null\n } ],\n \"type\" : \"user\",\n \"uri\" : \"spotify:user:username\"\n}"; + } +} diff --git a/SpotifyAPI.Web.Tests/Http/NewtonsoftJSONSerializerTest.cs b/SpotifyAPI.Web.Tests/Http/NewtonsoftJSONSerializerTest.cs index c51fd953..f09dc9c9 100644 --- a/SpotifyAPI.Web.Tests/Http/NewtonsoftJSONSerializerTest.cs +++ b/SpotifyAPI.Web.Tests/Http/NewtonsoftJSONSerializerTest.cs @@ -5,6 +5,8 @@ using Moq; using NUnit.Framework; using SpotifyAPI.Web.Http; using System.Net.Http; +using System.Threading.Tasks; +using System.Linq; namespace SpotifyAPI.Web.Tests { @@ -82,9 +84,60 @@ namespace SpotifyAPI.Web.Tests Assert.AreEqual(apiResonse.Response, response.Object); } - public class TestResponseObject + [TestCase] + public async Task DeserializeResponse_TimeCurrentlyPlayingTestMessage() { - public bool HelloWorld { get; set; } + var serializer = new NewtonsoftJSONSerializer(); + var response = new Mock(); + response.SetupGet(r => r.Body).Returns(ExampleResponses.CurrentlyPlayingContext); + response.SetupGet(r => r.ContentType).Returns("application/json"); + + var times = new List(); + foreach (var iter in Enumerable.Range(0, 50)) + { + var watch = System.Diagnostics.Stopwatch.StartNew(); + + IAPIResponse apiResonse = serializer.DeserializeResponse(response.Object); + + watch.Stop(); + times.Add(watch.ElapsedMilliseconds); + + Assert.AreEqual(apiResonse.Response, response.Object); + } + + var mean = times.Sum() / 50; + + using StreamWriter file = new("newtonsoft.json_test.txt", append: true); + await file.WriteLineAsync($"CurrentlyPlayingContext: {mean}ms"); + + } + + [TestCase] + public async Task DeserializeResponse_TimeUserTestMessage() + { + var serializer = new NewtonsoftJSONSerializer(); + var response = new Mock(); + response.SetupGet(r => r.Body).Returns(ExampleResponses.PublicUser); + response.SetupGet(r => r.ContentType).Returns("application/json"); + + var times = new List(); + foreach (var iter in Enumerable.Range(0, 50)) + { + var watch = System.Diagnostics.Stopwatch.StartNew(); + + IAPIResponse apiResonse = serializer.DeserializeResponse(response.Object); + + watch.Stop(); + times.Add(watch.ElapsedMilliseconds); + + Assert.AreEqual(apiResonse.Response, response.Object); + } + + var mean = times.Sum() / 50; + + using StreamWriter file = new("newtonsoft.json_test.txt", append: true); + await file.WriteLineAsync($"User: {mean}ms"); + } } } diff --git a/SpotifyAPI.Web.Tests/Http/SystemTextTests.cs b/SpotifyAPI.Web.Tests/Http/SystemTextTests.cs new file mode 100644 index 00000000..3a2016b5 --- /dev/null +++ b/SpotifyAPI.Web.Tests/Http/SystemTextTests.cs @@ -0,0 +1,134 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using Moq; +using NUnit.Framework; +using SpotifyAPI.Web.Http; + +namespace SpotifyAPI.Web.Tests +{ + public class SystemTextTests + { + public static IEnumerable DontSerializeTestSource + { + get + { + yield return new TestCaseData(null); + yield return new TestCaseData("string"); + yield return new TestCaseData(new MemoryStream(Encoding.UTF8.GetBytes("string"))); + yield return new TestCaseData(new StringContent("string")); + } + } + + [TestCaseSource(nameof(DontSerializeTestSource))] + public void SerializeRequest_SkipsAlreadySerialized(object input) + { + var serializer = new TextJsonSerializer(); + var request = new Mock(); + request.SetupGet(r => r.Body).Returns(input); + + serializer.SerializeRequest(request.Object); + + Assert.AreEqual(input, request.Object.Body); + } + + [TestCase] + public void DeserializeResponse_SkipsNonJson() + { + var serializer = new TextJsonSerializer(); + var response = new Mock(); + response.SetupGet(r => r.Body).Returns("hello"); + response.SetupGet(r => r.ContentType).Returns("media/mp4"); + + IAPIResponse apiResonse = serializer.DeserializeResponse(response.Object); + Assert.AreEqual(apiResonse.Body, null); + Assert.AreEqual(apiResonse.Response, response.Object); + } + + [TestCase] + public void DeserializeResponse_HandlesJson() + { + var serializer = new TextJsonSerializer(); + var response = new Mock(); + response.SetupGet(r => r.Body).Returns("{\"hello_world\": false}"); + response.SetupGet(r => r.ContentType).Returns("application/json"); + + IAPIResponse apiResonse = serializer.DeserializeResponse(response.Object); + Assert.AreEqual(apiResonse.Body?.HelloWorld, false); + Assert.AreEqual(apiResonse.Response, response.Object); + } + + [TestCase] + public void DeserializeResponse_TestMessage() + { + var serializer = new TextJsonSerializer(); + var response = new Mock(); + response.SetupGet(r => r.Body).Returns(ExampleResponses.CurrentlyPlayingContext); + response.SetupGet(r => r.ContentType).Returns("application/json"); + + IAPIResponse apiResonse = serializer.DeserializeResponse(response.Object); + + Assert.AreEqual(apiResonse.Response, response.Object); + } + + [TestCase] + public async Task DeserializeResponse_TimeCurrentlyPlayingTestMessage() + { + var serializer = new TextJsonSerializer(); + var response = new Mock(); + response.SetupGet(r => r.Body).Returns(ExampleResponses.CurrentlyPlayingContext); + response.SetupGet(r => r.ContentType).Returns("application/json"); + + var times = new List(); + foreach(var iter in Enumerable.Range(0, 50)) + { + var watch = System.Diagnostics.Stopwatch.StartNew(); + + IAPIResponse apiResonse = serializer.DeserializeResponse(response.Object); + + watch.Stop(); + times.Add(watch.ElapsedMilliseconds); + + Assert.AreEqual(apiResonse.Response, response.Object); + } + + var mean = times.Sum() / 50; + + using StreamWriter file = new("system.text.json_test.txt", append: true); + await file.WriteLineAsync($"CurrentlyPlayingContext: {mean}ms"); + + } + + [TestCase] + public async Task DeserializeResponse_TimeUserTestMessage() + { + var serializer = new TextJsonSerializer(); + var response = new Mock(); + response.SetupGet(r => r.Body).Returns(ExampleResponses.PublicUser); + response.SetupGet(r => r.ContentType).Returns("application/json"); + + var times = new List(); + foreach (var iter in Enumerable.Range(0, 50)) + { + var watch = System.Diagnostics.Stopwatch.StartNew(); + + IAPIResponse apiResonse = serializer.DeserializeResponse(response.Object); + + watch.Stop(); + times.Add(watch.ElapsedMilliseconds); + + Assert.AreEqual(apiResonse.Response, response.Object); + } + + var mean = times.Sum() / 50; + + using StreamWriter file = new("system.text.json_test.txt", append: true); + await file.WriteLineAsync($"User: {mean}ms"); + + } + } +} diff --git a/SpotifyAPI.Web.Tests/SpotifyAPI.Web.Tests.csproj b/SpotifyAPI.Web.Tests/SpotifyAPI.Web.Tests.csproj index a441945d..e40de689 100644 --- a/SpotifyAPI.Web.Tests/SpotifyAPI.Web.Tests.csproj +++ b/SpotifyAPI.Web.Tests/SpotifyAPI.Web.Tests.csproj @@ -12,6 +12,7 @@ + diff --git a/SpotifyAPI.Web/Clients/Interfaces/IPlaylistsClient.cs b/SpotifyAPI.Web/Clients/Interfaces/IPlaylistsClient.cs index ce6c0786..368cd40c 100644 --- a/SpotifyAPI.Web/Clients/Interfaces/IPlaylistsClient.cs +++ b/SpotifyAPI.Web/Clients/Interfaces/IPlaylistsClient.cs @@ -38,7 +38,7 @@ namespace SpotifyAPI.Web /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-get-playlists-tracks /// /// - Task>> GetItems(string playlistId); + Task>> GetItems(string playlistId); /// /// Get full details of the items of a playlist owned by a Spotify user. @@ -49,7 +49,7 @@ namespace SpotifyAPI.Web /// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-get-playlists-tracks /// /// - Task>> GetItems(string playlistId, PlaylistGetItemsRequest request); + Task>> GetItems(string playlistId, PlaylistGetItemsRequest request); /// /// Create a playlist for a Spotify user. (The playlist will be empty until you add tracks.) diff --git a/SpotifyAPI.Web/Clients/PlaylistsClient.cs b/SpotifyAPI.Web/Clients/PlaylistsClient.cs index 6a598d00..b95aeef1 100644 --- a/SpotifyAPI.Web/Clients/PlaylistsClient.cs +++ b/SpotifyAPI.Web/Clients/PlaylistsClient.cs @@ -26,19 +26,19 @@ namespace SpotifyAPI.Web return API.Post(URLs.PlaylistTracks(playlistId), null, request.BuildBodyParams()); } - public Task>> GetItems(string playlistId) + public Task>> GetItems(string playlistId) { var request = new PlaylistGetItemsRequest(); return GetItems(playlistId, request); } - public Task>> GetItems(string playlistId, PlaylistGetItemsRequest request) + public Task>> GetItems(string playlistId, PlaylistGetItemsRequest request) { Ensure.ArgumentNotNullOrEmptyString(playlistId, nameof(playlistId)); Ensure.ArgumentNotNull(request, nameof(request)); - return API.Get>>(URLs.PlaylistTracks(playlistId), request.BuildQueryParams()); + return API.Get>>(URLs.PlaylistTracks(playlistId), request.BuildQueryParams()); } public Task Create(string userId, PlaylistCreateRequest request) diff --git a/SpotifyAPI.Web/Clients/SpotifyClientConfig.cs b/SpotifyAPI.Web/Clients/SpotifyClientConfig.cs index 56a419cd..de00eb09 100644 --- a/SpotifyAPI.Web/Clients/SpotifyClientConfig.cs +++ b/SpotifyAPI.Web/Clients/SpotifyClientConfig.cs @@ -187,7 +187,7 @@ namespace SpotifyAPI.Web return new SpotifyClientConfig( SpotifyUrls.APIV1, null, - new NewtonsoftJSONSerializer(), + new TextJsonSerializer(), new NetHttpClient(), null, null, diff --git a/SpotifyAPI.Web/Http/APIConnector.cs b/SpotifyAPI.Web/Http/APIConnector.cs index c6945201..12725524 100644 --- a/SpotifyAPI.Web/Http/APIConnector.cs +++ b/SpotifyAPI.Web/Http/APIConnector.cs @@ -18,7 +18,7 @@ namespace SpotifyAPI.Web.Http public event EventHandler? ResponseReceived; public APIConnector(Uri baseAddress, IAuthenticator authenticator) : - this(baseAddress, authenticator, new NewtonsoftJSONSerializer(), new NetHttpClient(), null, null) + this(baseAddress, authenticator, new TextJsonSerializer(), new NetHttpClient(), null, null) { } public APIConnector( Uri baseAddress, diff --git a/SpotifyAPI.Web/Http/TextJsonSerializer.cs b/SpotifyAPI.Web/Http/TextJsonSerializer.cs new file mode 100644 index 00000000..f82259a3 --- /dev/null +++ b/SpotifyAPI.Web/Http/TextJsonSerializer.cs @@ -0,0 +1,93 @@ +using System; +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; +using System.IO; +using System.Net.Http; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; + +namespace SpotifyAPI.Web.Http +{ + public class SnakeCase : JsonNamingPolicy + { + public override string ConvertName(string name) + { + if(string.IsNullOrWhiteSpace(name)) + { + return name; + }else + { + return Regex.Replace(string.Concat(name[0].ToString().ToLowerInvariant(), name.Substring(1)), "[A-Z]", "_$0").ToLowerInvariant(); + } + } + } + + public class TextJsonSerializer : IJSONSerializer + { + private ModelJsonContext JsonContext; + + public TextJsonSerializer() + { + JsonContext = ModelJsonContext.Get(); + } + + public IAPIResponse DeserializeResponse(IResponse response) + { + Ensure.ArgumentNotNull(response, nameof(response)); + + if ( + ( + response.ContentType?.Equals("application/json", StringComparison.Ordinal) is true || response.ContentType == null + )) + { + if(response.Body is string bodyString && !string.IsNullOrWhiteSpace(bodyString)) + { + var body = (T?)JsonSerializer.Deserialize(response.Body as string ?? "", typeof(T), JsonContext); + + // In order to work out whether track or episode has been returned, first deserialise as BasePlayableItem + // which has enum of current playing type, then deserialise again with concrete playing type + if (body is CurrentlyPlaying currentlyPlaying) + { + if(currentlyPlaying.Item.Type is ItemType.Track) + { + body = (T?) JsonSerializer.Deserialize(response.Body as string ?? "", typeof(CurrentlyPlaying), JsonContext); + } + else if (currentlyPlaying.Item.Type is ItemType.Episode) + { + body = (T?) JsonSerializer.Deserialize(response.Body as string ?? "", typeof(CurrentlyPlaying), JsonContext); + } + } + if (body is CurrentlyPlayingContext currentlyPlayingContext) + { + if (currentlyPlayingContext.Item.Type is ItemType.Track) + { + body = (T?)JsonSerializer.Deserialize(response.Body as string ?? "", typeof(CurrentlyPlayingContext), JsonContext); + } + else if (currentlyPlayingContext.Item.Type is ItemType.Episode) + { + body = (T?)JsonSerializer.Deserialize(response.Body as string ?? "", typeof(CurrentlyPlayingContext), JsonContext); + } + } + + return new APIResponse(response, body!); + } + } + return new APIResponse(response); + } + + public void SerializeRequest(IRequest request) + { + Ensure.ArgumentNotNull(request, nameof(request)); + + if (request.Body is string || request.Body is Stream || request.Body is HttpContent || request.Body is null) + { + return; + } + + request.Body = JsonSerializer.Serialize(request.Body, request.Body.GetType(), JsonContext); + } + } +} diff --git a/SpotifyAPI.Web/Models/ModelJsonContext.cs b/SpotifyAPI.Web/Models/ModelJsonContext.cs new file mode 100644 index 00000000..52a22099 --- /dev/null +++ b/SpotifyAPI.Web/Models/ModelJsonContext.cs @@ -0,0 +1,88 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SpotifyAPI.Web.Http; + +namespace SpotifyAPI.Web +{ + [JsonSerializable(typeof(Actions))] + [JsonSerializable(typeof(AlbumsResponse))] + [JsonSerializable(typeof(ArtistsRelatedArtistsResponse))] + [JsonSerializable(typeof(ArtistsResponse))] + [JsonSerializable(typeof(ArtistsTopTracksResponse))] + [JsonSerializable(typeof(AuthorizationCodeRefreshResponse))] + [JsonSerializable(typeof(AuthorizationCodeTokenResponse))] + [JsonSerializable(typeof(CategoriesResponse))] + [JsonSerializable(typeof(Category))] + [JsonSerializable(typeof(CategoryPlaylistsResponse))] + [JsonSerializable(typeof(ClientCredentialsTokenResponse))] + [JsonSerializable(typeof(Context))] + [JsonSerializable(typeof(Copyright))] + [JsonSerializable(typeof(CurrentlyPlaying))] + [JsonSerializable(typeof(CurrentlyPlaying))] + [JsonSerializable(typeof(CurrentlyPlaying))] + [JsonSerializable(typeof(CurrentlyPlayingContext))] + [JsonSerializable(typeof(CurrentlyPlayingContext))] + [JsonSerializable(typeof(CurrentlyPlayingContext))] + [JsonSerializable(typeof(Cursor))] + [JsonSerializable(typeof(Device))] + [JsonSerializable(typeof(DeviceResponse))] + [JsonSerializable(typeof(EpisodesResponse))] + [JsonSerializable(typeof(FeaturedPlaylistsResponse))] + [JsonSerializable(typeof(FollowedArtistsResponse))] + [JsonSerializable(typeof(Followers))] + [JsonSerializable(typeof(FullAlbum))] + [JsonSerializable(typeof(FullEpisode))] + [JsonSerializable(typeof(FullPlaylist))] + [JsonSerializable(typeof(FullShow))] + [JsonSerializable(typeof(FullTrack))] + [JsonSerializable(typeof(Image))] + [JsonSerializable(typeof(LinkedTrack))] + [JsonSerializable(typeof(NewReleasesResponse))] + [JsonSerializable(typeof(PKCETokenResponse))] + [JsonSerializable(typeof(PlayHistoryItem))] + [JsonSerializable(typeof(PlaylistTrack))] + [JsonSerializable(typeof(PlaylistTrack))] + [JsonSerializable(typeof(PrivateUser))] + [JsonSerializable(typeof(PublicUser))] + [JsonSerializable(typeof(RecommendationGenresResponse))] + [JsonSerializable(typeof(RecommendationSeed))] + [JsonSerializable(typeof(RecommendationsResponse))] + [JsonSerializable(typeof(ResumePoint))] + [JsonSerializable(typeof(SavedAlbum))] + [JsonSerializable(typeof(SavedEpisodes))] + [JsonSerializable(typeof(SavedShow))] + [JsonSerializable(typeof(SavedTrack))] + [JsonSerializable(typeof(SearchResponse))] + [JsonSerializable(typeof(Section))] + [JsonSerializable(typeof(Segment))] + [JsonSerializable(typeof(ShowsResponse))] + [JsonSerializable(typeof(SimpleAlbum))] + [JsonSerializable(typeof(SimpleArtist))] + [JsonSerializable(typeof(SimpleEpisode))] + [JsonSerializable(typeof(SimplePlaylist))] + [JsonSerializable(typeof(SimpleShow))] + [JsonSerializable(typeof(SimpleTrack))] + [JsonSerializable(typeof(SnapshotResponse))] + [JsonSerializable(typeof(TestResponseObject))] + [JsonSerializable(typeof(TimeInterval))] + [JsonSerializable(typeof(TrackAudio))] + [JsonSerializable(typeof(TrackAudioAnalysis))] + [JsonSerializable(typeof(TrackMeta))] + [JsonSerializable(typeof(TracksAudioFeaturesResponse))] + [JsonSerializable(typeof(TracksResponse))] + public partial class ModelJsonContext : JsonSerializerContext + { + public static ModelJsonContext Get() + { + return new ModelJsonContext(new JsonSerializerOptions() + { + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + PropertyNamingPolicy = new SnakeCase(), + Converters = + { + new JsonStringEnumConverter() + } + }); + } + } +} diff --git a/SpotifyAPI.Web/Models/Response/Interfaces/IPlaylistElement.cs b/SpotifyAPI.Web/Models/Response/BasePlayableItem.cs similarity index 75% rename from SpotifyAPI.Web/Models/Response/Interfaces/IPlaylistElement.cs rename to SpotifyAPI.Web/Models/Response/BasePlayableItem.cs index 56a5f5ec..9c7fe51c 100644 --- a/SpotifyAPI.Web/Models/Response/Interfaces/IPlaylistElement.cs +++ b/SpotifyAPI.Web/Models/Response/BasePlayableItem.cs @@ -9,10 +9,10 @@ namespace SpotifyAPI.Web Episode } - public interface IPlayableItem + public class BasePlayableItem { [JsonConverter(typeof(StringEnumConverter))] - ItemType Type { get; } + public ItemType Type { get; } } } diff --git a/SpotifyAPI.Web/Models/Response/CurrentlyPlaying.cs b/SpotifyAPI.Web/Models/Response/CurrentlyPlaying.cs index 03b49536..83b43050 100644 --- a/SpotifyAPI.Web/Models/Response/CurrentlyPlaying.cs +++ b/SpotifyAPI.Web/Models/Response/CurrentlyPlaying.cs @@ -13,9 +13,17 @@ namespace SpotifyAPI.Web /// /// [JsonConverter(typeof(PlayableItemConverter))] - public IPlayableItem Item { get; set; } = default!; + public BasePlayableItem Item { get; set; } = default!; public int? ProgressMs { get; set; } public long Timestamp { get; set; } } + + public class CurrentlyPlaying: CurrentlyPlaying where T : BasePlayableItem + { + public new T Item { + get => (T) base.Item; + set => base.Item = value; + } + } } diff --git a/SpotifyAPI.Web/Models/Response/CurrentlyPlayingContext.cs b/SpotifyAPI.Web/Models/Response/CurrentlyPlayingContext.cs index 0ac9c4c1..21e38c77 100644 --- a/SpotifyAPI.Web/Models/Response/CurrentlyPlayingContext.cs +++ b/SpotifyAPI.Web/Models/Response/CurrentlyPlayingContext.cs @@ -17,10 +17,19 @@ namespace SpotifyAPI.Web /// /// [JsonConverter(typeof(PlayableItemConverter))] - public IPlayableItem Item { get; set; } = default!; + public BasePlayableItem Item { get; set; } = default!; public string CurrentlyPlayingType { get; set; } = default!; public Actions Actions { get; set; } = default!; } + + public class CurrentlyPlayingContext : CurrentlyPlayingContext where T : BasePlayableItem + { + public new T Item + { + get => (T)base.Item; + set => base.Item = value; + } + } } diff --git a/SpotifyAPI.Web/Models/Response/FullEpisode.cs b/SpotifyAPI.Web/Models/Response/FullEpisode.cs index 733595df..8e3caa87 100644 --- a/SpotifyAPI.Web/Models/Response/FullEpisode.cs +++ b/SpotifyAPI.Web/Models/Response/FullEpisode.cs @@ -4,7 +4,7 @@ using Newtonsoft.Json.Converters; namespace SpotifyAPI.Web { - public class FullEpisode : IPlayableItem + public class FullEpisode : BasePlayableItem { public string AudioPreviewUrl { get; set; } = default!; public string Description { get; set; } = default!; diff --git a/SpotifyAPI.Web/Models/Response/FullPlaylist.cs b/SpotifyAPI.Web/Models/Response/FullPlaylist.cs index 0a6bc643..9dec1000 100644 --- a/SpotifyAPI.Web/Models/Response/FullPlaylist.cs +++ b/SpotifyAPI.Web/Models/Response/FullPlaylist.cs @@ -19,7 +19,7 @@ namespace SpotifyAPI.Web /// A list of PlaylistTracks, which items can be a FullTrack or FullEpisode /// /// - public Paging>? Tracks { get; set; } = default!; + public Paging>? Tracks { get; set; } = default!; public string? Type { get; set; } = default!; public string? Uri { get; set; } = default!; } diff --git a/SpotifyAPI.Web/Models/Response/FullTrack.cs b/SpotifyAPI.Web/Models/Response/FullTrack.cs index 71331bc8..b6764812 100644 --- a/SpotifyAPI.Web/Models/Response/FullTrack.cs +++ b/SpotifyAPI.Web/Models/Response/FullTrack.cs @@ -4,7 +4,7 @@ using Newtonsoft.Json.Converters; namespace SpotifyAPI.Web { - public class FullTrack : IPlayableItem + public class FullTrack : BasePlayableItem { public SimpleAlbum Album { get; set; } = default!; public List Artists { get; set; } = default!; diff --git a/SpotifyAPI.Web/Models/Response/Image.cs b/SpotifyAPI.Web/Models/Response/Image.cs index 83d70e59..390f317c 100644 --- a/SpotifyAPI.Web/Models/Response/Image.cs +++ b/SpotifyAPI.Web/Models/Response/Image.cs @@ -2,8 +2,8 @@ namespace SpotifyAPI.Web { public class Image { - public int Height { get; set; } - public int Width { get; set; } + public int? Height { get; set; } + public int? Width { get; set; } public string Url { get; set; } = default!; } } diff --git a/SpotifyAPI.Web/Models/Response/SimplePlaylist.cs b/SpotifyAPI.Web/Models/Response/SimplePlaylist.cs index 3dc42554..945fa40b 100644 --- a/SpotifyAPI.Web/Models/Response/SimplePlaylist.cs +++ b/SpotifyAPI.Web/Models/Response/SimplePlaylist.cs @@ -21,7 +21,7 @@ namespace SpotifyAPI.Web /// A list of PlaylistTracks, which items can be a FullTrack or FullEpisode /// /// - public Paging> Tracks { get; set; } = default!; + public Paging> Tracks { get; set; } = default!; public string Type { get; set; } = default!; public string Uri { get; set; } = default!; } diff --git a/SpotifyAPI.Web/Models/Response/TestResponseObject.cs b/SpotifyAPI.Web/Models/Response/TestResponseObject.cs new file mode 100644 index 00000000..b8a48949 --- /dev/null +++ b/SpotifyAPI.Web/Models/Response/TestResponseObject.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SpotifyAPI.Web +{ + public class TestResponseObject + { + public bool HelloWorld { get; set; } + } +} diff --git a/SpotifyAPI.Web/SpotifyAPI.Web.csproj b/SpotifyAPI.Web/SpotifyAPI.Web.csproj index 3e35f452..1d7ad73e 100644 --- a/SpotifyAPI.Web/SpotifyAPI.Web.csproj +++ b/SpotifyAPI.Web/SpotifyAPI.Web.csproj @@ -31,10 +31,11 @@ - + None +