diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 25bd47eb1..0a4d73876 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,16 +17,17 @@ jobs: os: - ubuntu-latest - windows-latest + dotnet: + - netcoreapp3.1 + - netcoreapp2.1 + - net461 + exclude: + - os: ubuntu-latest + dotnet: net461 steps: - uses: actions/checkout@v2 - - uses: actions/setup-dotnet@v1 - name: Install .NET Core 3.1 - with: - dotnet-version: "3.1.102" - - name: Run dotnet build - run: dotnet build Gw2Sharp -c Release - name: Run dotnet test - run: dotnet test -c Release + run: dotnet test -c Release -f ${{ matrix.dotnet }} nightly: name: Nightly @@ -36,10 +37,6 @@ jobs: - uses: actions/checkout@v2 - run: | git fetch --prune --unshallow - - uses: actions/setup-dotnet@v1 - name: Install .NET Core 3.1 - with: - dotnet-version: "3.1.102" - name: Install InheritDocTool shell: powershell run: dotnet tool install --global InheritDocTool diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index eb5a8a4fb..9a164b88f 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -12,13 +12,14 @@ jobs: os: - ubuntu-latest - windows-latest + dotnet: + - netcoreapp3.1 + - netcoreapp2.1 + - net461 + exclude: + - os: ubuntu-latest + dotnet: net461 steps: - uses: actions/checkout@v2 - - uses: actions/setup-dotnet@v1 - name: Install .NET Core 3.1 - with: - dotnet-version: "3.1.102" - - name: Run dotnet build - run: dotnet build Gw2Sharp -c Release - name: Run dotnet test - run: dotnet test -c Release + run: dotnet test -c Release -f ${{ matrix.dotnet }} diff --git a/.github/workflows/publish-version.yml b/.github/workflows/publish-version.yml index 85f3b37c1..e5450504d 100644 --- a/.github/workflows/publish-version.yml +++ b/.github/workflows/publish-version.yml @@ -15,16 +15,17 @@ jobs: os: - ubuntu-latest - windows-latest + dotnet: + - netcoreapp3.1 + - netcoreapp2.1 + - net461 + exclude: + - os: ubuntu-latest + dotnet: net461 steps: - uses: actions/checkout@v2 - - uses: actions/setup-dotnet@v1 - name: Install .NET Core 3.1 - with: - dotnet-version: "3.1.102" - - name: Run dotnet build - run: dotnet build Gw2Sharp -c Release - name: Run dotnet test - run: dotnet test -c Release + run: dotnet test -c Release -f ${{ matrix.dotnet }} release: name: Release @@ -34,10 +35,6 @@ jobs: - uses: actions/checkout@v2 - run: | git fetch --prune --unshallow - - uses: actions/setup-dotnet@v1 - name: Install .NET Core 3.1 - with: - dotnet-version: "3.1.102" - name: Install InheritDocTool shell: powershell run: dotnet tool install --global InheritDocTool diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index ce4e6b06e..adc66df3e 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -10,10 +10,6 @@ jobs: - uses: actions/checkout@v2 - run: | git fetch --prune --unshallow - - uses: actions/setup-dotnet@v1 - name: Install .NET Core 3.1 - with: - dotnet-version: "3.1.102" - name: Install Coverlet run: dotnet tool install --global coverlet.console - name: Install SonarScanner diff --git a/Gw2Sharp.Tests/ConnectionTests.cs b/Gw2Sharp.Tests/ConnectionTests.cs index c9dbfecf2..c949ec230 100644 --- a/Gw2Sharp.Tests/ConnectionTests.cs +++ b/Gw2Sharp.Tests/ConnectionTests.cs @@ -115,7 +115,11 @@ public async Task ValidRequestTest() [InlineData("membership required", HttpStatusCode.Forbidden, typeof(MembershipRequiredException))] [InlineData("access restricted to guild leaders", HttpStatusCode.Forbidden, typeof(RestrictedToGuildLeadersException))] [InlineData("not found", HttpStatusCode.NotFound, typeof(NotFoundException))] +#if NET461 + [InlineData("too many requests", (HttpStatusCode)429, typeof(TooManyRequestsException))] +#else [InlineData("too many requests", HttpStatusCode.TooManyRequests, typeof(TooManyRequestsException))] +#endif [InlineData("server error", HttpStatusCode.InternalServerError, typeof(ServerErrorException))] [InlineData("service unavailable", HttpStatusCode.ServiceUnavailable, typeof(ServiceUnavailableException))] public async Task ExceptionRequestTest(string errorText, HttpStatusCode statusCode, Type exceptionType) diff --git a/Gw2Sharp.Tests/Gw2Sharp.Tests.csproj b/Gw2Sharp.Tests/Gw2Sharp.Tests.csproj index 50e584b72..6e89221aa 100644 --- a/Gw2Sharp.Tests/Gw2Sharp.Tests.csproj +++ b/Gw2Sharp.Tests/Gw2Sharp.Tests.csproj @@ -1,7 +1,7 @@ - netcoreapp3.1 + netcoreapp2.1;netcoreapp3.1;net461 preview annotations true diff --git a/Gw2Sharp.Tests/TestFiles/Achievements/AchievementsDaily.json b/Gw2Sharp.Tests/TestFiles/Achievements/AchievementsDaily.json index b5226eb99..8faa85d03 100644 --- a/Gw2Sharp.Tests/TestFiles/Achievements/AchievementsDaily.json +++ b/Gw2Sharp.Tests/TestFiles/Achievements/AchievementsDaily.json @@ -27,7 +27,7 @@ "level": { "min": 80, "max": 80 - }, + } }, { "id": 1839, diff --git a/Gw2Sharp.Tests/TestFiles/CreateSubtoken/CreateSubtoken.json b/Gw2Sharp.Tests/TestFiles/CreateSubtoken/CreateSubtoken.json index 77e45a7ad..2f20df147 100644 --- a/Gw2Sharp.Tests/TestFiles/CreateSubtoken/CreateSubtoken.json +++ b/Gw2Sharp.Tests/TestFiles/CreateSubtoken/CreateSubtoken.json @@ -1,3 +1,3 @@ { - "subtoken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ3YlRodVdNNGExMUduZlpYSTdaa0pHck52SVVPUWhMejZHTXpOeE9TUC1rIiwiaWF0IjoxNTU4Mzk3OTUwLCJleHAiOjE1NzczMDYwOTYsInBlcm1pc3Npb25zIjpbInByb2dyZXNzaW9uIiwiYWNjb3VudCIsInVubG9ja3MiXSwidXJscyI6WyIvdjIvY2hhcmFjdGVycy9NeSUyMENvb2wlMjBDaGFyYWN0ZXIiLCIvdjIvYWNjb3VudC9ob21lL2NhdHMiXX0.UdLlafgo8lxkb1Hn88paZT83aw_9mHEYVZJLDgObNSc", + "subtoken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ3YlRodVdNNGExMUduZlpYSTdaa0pHck52SVVPUWhMejZHTXpOeE9TUC1rIiwiaWF0IjoxNTU4Mzk3OTUwLCJleHAiOjE1NzczMDYwOTYsInBlcm1pc3Npb25zIjpbInByb2dyZXNzaW9uIiwiYWNjb3VudCIsInVubG9ja3MiXSwidXJscyI6WyIvdjIvY2hhcmFjdGVycy9NeSUyMENvb2wlMjBDaGFyYWN0ZXIiLCIvdjIvYWNjb3VudC9ob21lL2NhdHMiXX0.UdLlafgo8lxkb1Hn88paZT83aw_9mHEYVZJLDgObNSc" } diff --git a/Gw2Sharp.Tests/WebApi/Http/TooManyRequestsExceptionTests.cs b/Gw2Sharp.Tests/WebApi/Http/TooManyRequestsExceptionTests.cs index a60f83779..3480a3d95 100644 --- a/Gw2Sharp.Tests/WebApi/Http/TooManyRequestsExceptionTests.cs +++ b/Gw2Sharp.Tests/WebApi/Http/TooManyRequestsExceptionTests.cs @@ -14,7 +14,11 @@ public class TooManyRequestsExceptionTests public void SerializableTest() { var request = new HttpRequest(new Uri("http://localhost"), new Dictionary { { "hello", "tyria" } }); +#if NET461 + var response = new HttpResponse(new ErrorObject { Text = "Error" }, (HttpStatusCode)429, null, null); +#else var response = new HttpResponse(new ErrorObject { Text = "Error" }, HttpStatusCode.TooManyRequests, null, null); +#endif var exception = new TooManyRequestsException(request, response); exception.Should().BeBinarySerializable(); } diff --git a/Gw2Sharp.Tests/WebApi/V2/Clients/BaseEndpointClientTests.cs b/Gw2Sharp.Tests/WebApi/V2/Clients/BaseEndpointClientTests.cs index 255007a56..80e341210 100644 --- a/Gw2Sharp.Tests/WebApi/V2/Clients/BaseEndpointClientTests.cs +++ b/Gw2Sharp.Tests/WebApi/V2/Clients/BaseEndpointClientTests.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Net; using System.Reflection; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Gw2Sharp.Extensions; @@ -16,7 +17,6 @@ using Gw2Sharp.WebApi.V2; using Gw2Sharp.WebApi.V2.Clients; using Gw2Sharp.WebApi.V2.Models; -using Newtonsoft.Json.Linq; using NSubstitute; using NSubstitute.Core; using Xunit; @@ -80,7 +80,7 @@ protected virtual async Task AssertPaginatedDataAsync(IPaginatedClient< }); var actual = await client.PageAsync(2, 100); - this.AssertJsonObject(expected, actual); + this.AssertJsonObject(expected.RootElement, actual); } protected virtual async Task AssertBlobDataAsync(IBlobClient client, string file) @@ -97,14 +97,14 @@ protected virtual async Task AssertBlobDataAsync(IBlobClient c }); var actual = await client.GetAsync(); - this.AssertJsonObject(expected, actual!); + this.AssertJsonObject(expected.RootElement, actual!); } protected virtual async Task AssertGetDataAsync(IBulkExpandableClient client, string file, string idName = "id") where TObject : IApiV2Object, IIdentifiable { var (data, expected) = this.GetTestData(file); - var id = this.GetId(expected[idName]); + var id = this.GetId(expected.RootElement.GetProperty(idName)); ((IClientInternal)this.Client).Connection.HttpClient.RequestAsync(Arg.Any(), CancellationToken.None).Returns(callInfo => { @@ -120,7 +120,7 @@ protected virtual async Task AssertGetDataAsync(IBulkExpandableCli }); var actual = await client.GetAsync(id); - this.AssertJsonObject(expected, actual); + this.AssertJsonObject(expected.RootElement, actual); } protected virtual async Task AssertAllDataAsync(IAllExpandableClient client, string file, string idsName = "ids") @@ -137,17 +137,14 @@ protected virtual async Task AssertAllDataAsync(IAllExpandableClient(IBulkExpandableClient client, string file, string idName = "id", string idsName = "ids") where TObject : IApiV2Object, IIdentifiable { var (data, expected) = this.GetTestData(file); - var ids = this.GetIds(expected.Select(i => - { - return i is JProperty prop ? prop.Value[idName] : i[idName]; - })); + var ids = this.GetIds(expected.RootElement.EnumerateArray().Select(i => i.GetProperty(idName))); ((IClientInternal)this.Client).Connection.HttpClient.RequestAsync(Arg.Any(), CancellationToken.None).Returns(callInfo => { @@ -167,7 +164,7 @@ protected virtual async Task AssertBulkDataAsync(IBulkExpandableCl }); var actual = await client.ManyAsync(ids); - this.AssertJsonObject(expected, actual); + this.AssertJsonObject(expected.RootElement, actual); } protected virtual async Task AssertIdsDataAsync(IBulkExpandableClient client, string file) @@ -185,7 +182,7 @@ protected virtual async Task AssertIdsDataAsync(IBulkExpandableCli }); var actual = await client.IdsAsync(); - this.AssertJsonObject(expected, actual); + this.AssertJsonObject(expected.RootElement, actual); } protected virtual void AssertRequest(CallInfo callInfo, IEndpointClient client, string pathAndQuery) @@ -244,7 +241,7 @@ protected virtual void AssertSchemaVersionRequest(CallInfo callInfo, IEndpointCl } - protected (string, JToken) GetTestData(string fileResourceName) + protected (string, JsonDocument) GetTestData(string fileResourceName) { using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream($"Gw2Sharp.Tests.{fileResourceName}")) { @@ -253,37 +250,35 @@ protected virtual void AssertSchemaVersionRequest(CallInfo callInfo, IEndpointCl using (var reader = new StreamReader(stream)) { string data = reader.ReadToEnd(); - return (data, JToken.Parse(data)); + return (data, JsonDocument.Parse(data)); } } } - protected TObject GetId(JToken id) => - typeof(TObject) == typeof(Guid) ? (TObject)(object)Guid.Parse(id.Value()) : id.Value(); + protected TObject GetId(JsonElement id) => + typeof(TObject) == typeof(Guid) ? (TObject)(object)id.GetGuid() : JsonSerializer.Deserialize(id.GetRawText()); - protected IEnumerable GetIds(IEnumerable ids) => - typeof(TObject) == typeof(Guid) ? ids.Select(i => Guid.Parse(i.Value())).Cast() : ids.Select(i => i.Value()); + protected IEnumerable GetIds(IEnumerable ids) => + typeof(TObject) == typeof(Guid) ? ids.Select(i => i.GetGuid()).Cast() : ids.Select(i => JsonSerializer.Deserialize(i.GetRawText())); - protected void AssertJsonObject(object expected, object actual) + protected void AssertJsonObject(JsonElement expected, object actual) { - switch (expected) + switch (expected.ValueKind) { - case JObject jObject: - this.AssertJsonObject(jObject, actual); - break; - case JArray jArray: - this.AssertJsonObject(jArray, actual); + case JsonValueKind.Object: + this.AssertJsonObject(expected.EnumerateObject(), actual); break; - case JValue jValue: - this.AssertJsonObject(jValue, actual); + case JsonValueKind.Array: + this.AssertJsonObject(expected.EnumerateArray(), actual); break; default: - throw new InvalidOperationException($"{nameof(expected)} is not an object, array or value"); + this.AssertJsonValue(expected, actual); + break; } } - protected void AssertJsonObject(JObject expected, object actual) + protected void AssertJsonObject(JsonElement.ObjectEnumerator expected, object actual) { var type = actual.GetType(); if (type.IsGenericType && (type.GetGenericTypeDefinition() == typeof(ReadOnlyDictionary<,>) || type.GetGenericTypeDefinition() == typeof(Dictionary<,>))) @@ -296,11 +291,11 @@ protected void AssertJsonObject(JObject expected, object actual) dynamic key; if (keyType == typeof(string)) { - key = kvp.Key; + key = kvp.Name; } else { - string keyString = string.Concat(kvp.Key.Split('_').Select(s => string.Concat( + string keyString = string.Concat(kvp.Name.Split('_').Select(s => string.Concat( s[0].ToString().ToUpper(), s.Substring(1)))); key = Convert.ChangeType(keyString, keyType); @@ -316,7 +311,7 @@ protected void AssertJsonObject(JObject expected, object actual) // Specific object foreach (var kvp in expected) { - string key = string.Concat(kvp.Key.Split('_').Select(s => string.Concat( + string key = string.Concat(kvp.Name.Split('_').Select(s => string.Concat( s[0].ToString().ToUpper(), s.Substring(1)))); var property = type.GetProperty(key); @@ -328,7 +323,7 @@ protected void AssertJsonObject(JObject expected, object actual) } } - protected void AssertJsonObject(JArray expected, object actual) + protected void AssertJsonObject(JsonElement.ArrayEnumerator expected, object actual) { var actualList = (actual as IEnumerable)?.Cast().ToList(); if (actualList is null && actual.GetType().GetGenericTypeDefinition() == typeof(KeyValuePair<,>)) @@ -340,45 +335,52 @@ protected void AssertJsonObject(JArray expected, object actual) if (actualList is null) throw new InvalidOperationException($"Expected an object that's castable to an enumerable for {expected}"); - for (int i = 0; i < expected.Count; i++) - this.AssertJsonObject(expected[i], actualList[i]); + var expectedList = expected.ToList(); + for (int i = 0; i < expectedList.Count; i++) + this.AssertJsonObject(expectedList[i], actualList[i]); } - protected void AssertJsonObject(JValue expected, object actual) + protected void AssertJsonValue(JsonElement expected, object actual) { bool switched = true; switch (actual) { case Guid guid: - Assert.Equal(new Guid(expected.Value()), guid); + Assert.Equal(new Guid(expected.GetString()), guid); break; case DateTime dateTime: - Assert.Equal(expected.Value(), dateTime); + Assert.Equal(expected.GetDateTime(), dateTime); break; case DateTimeOffset dateTime: - Assert.Equal(expected.Value(), dateTime); + Assert.Equal(expected.GetDateTimeOffset(), dateTime); break; case TimeSpan timeSpan: - Assert.Equal(TimeSpan.FromSeconds(expected.Value()), timeSpan); + Assert.Equal(TimeSpan.FromSeconds(expected.GetInt32()), timeSpan); break; case int @int: - Assert.Equal(expected.Value(), @int); + if (expected.ValueKind == JsonValueKind.String) + Assert.Equal(int.Parse(expected.GetString()), @int); + else + Assert.Equal(expected.GetInt32(), @int); break; case long @long: - Assert.Equal(expected.Value(), @long); + if (expected.ValueKind == JsonValueKind.String) + Assert.Equal(long.Parse(expected.GetString()), @long); + else + Assert.Equal(expected.GetInt64(), @long); break; case double @double: - Assert.Equal(expected.Value(), @double, 10); + Assert.Equal(expected.GetDouble(), @double, 10); break; case bool @bool: - Assert.Equal(expected.Value(), @bool); + Assert.Equal(expected.GetBoolean(), @bool); break; case string @string: - Assert.Equal(expected.Value, @string); + Assert.Equal(expected.GetString(), @string); break; case null: - if (expected.Type == JTokenType.String) - Assert.Equal(expected.Value, string.Empty); + if (expected.ValueKind == JsonValueKind.String) + Assert.Equal(expected.GetString(), string.Empty); break; default: switched = false; @@ -393,20 +395,20 @@ protected void AssertJsonObject(JValue expected, object actual) var enumType = typeInfo.GenericTypeArguments[0]; dynamic @enum = actual; // Just for easiness - Assert.Equal(expected.Value(), @enum.RawValue); + Assert.Equal(expected.GetString(), @enum.RawValue); if (@enum.IsUnknown) { var enumNames = Enum.GetNames(enumType).Select(x => x.Replace("_", "")); Assert.True(enumNames.Contains((string)@enum.RawValue, StringComparer.OrdinalIgnoreCase), $"Expected '{expected}' to be a value in enumerator {@enum.Value.GetType().FullName}; detected value '{@enum.Value}'"); } - Assert.Equal(expected.Value().ParseEnum(enumType), @enum.Value); + Assert.Equal(expected.GetString().ParseEnum(enumType), @enum.Value); switched = true; } } if (!switched) - Assert.Equal(expected.Value, actual!.ToString()); + Assert.Equal(expected.GetString(), actual!.ToString()); } #endregion diff --git a/Gw2Sharp/Gw2Sharp.csproj b/Gw2Sharp/Gw2Sharp.csproj index c75be80b7..87146352f 100644 --- a/Gw2Sharp/Gw2Sharp.csproj +++ b/Gw2Sharp/Gw2Sharp.csproj @@ -1,28 +1,20 @@ - + - netstandard2.0 + netcoreapp3.1;netstandard2.0 preview true true enable - 3.0.0 - true Gw2Sharp Archomeda - -Gw2Sharp is a .NET wrapper library for the official Guild Wars 2 API. - -It uses the latest C# 8.0 features and targets .NET Standard 2.0 for cross platform compatibility. -It aims to be as close as possible to the official API structure, with a few exceptions to make life easier. - -Visit https://archomeda.github.io/Gw2Sharp for more information. - + Gw2Sharp is a cross-platform .NET wrapper library for the official Guild Wars 2 API. + https://archomeda.github.io/Gw2Sharp + https://github.com/Archomeda/Gw2Sharp true MIT true - https://github.com/Archomeda/Gw2Sharp true true @@ -35,11 +27,12 @@ Visit https://archomeda.github.io/Gw2Sharp for more information. + + + - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - + + diff --git a/README.md b/README.md index 5c7822ec9..ef0216416 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,7 @@ [![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=Archomeda_Gw2Sharp&metric=sqale_rating)](https://sonarcloud.io/dashboard?id=Archomeda_Gw2Sharp) [![Reliability Rating](https://sonarcloud.io/api/project_badges/measure?project=Archomeda_Gw2Sharp&metric=reliability_rating)](https://sonarcloud.io/dashboard?id=Archomeda_Gw2Sharp) -Gw2Sharp is a .NET wrapper library for the [official Guild Wars 2 API](https://wiki.guildwars2.com/wiki/API) written in C#. -It uses the latest C# 8.0 features like Nullable Reference Types because they're awesome. +Gw2Sharp is a cross-platform .NET wrapper library for the [official Guild Wars 2 API](https://wiki.guildwars2.com/wiki/API) written in C#. This library has been written to be as close as possible to the official API structure, with a few exceptions to make life easier. *Make sure to read the [introductory guide](https://archomeda.github.io/Gw2Sharp/master/guides/introduction.html) to get started.* @@ -21,8 +20,8 @@ The following services supported by Gw2Sharp: - Chat links - [introduction](https://archomeda.github.io/Gw2Sharp/master/guides/introduction.html#chat-links) ## Requirements -This project targets .NET Standard 2.0. -It supports the C# 8.0 Nullable Reference Types feature for your convenience (which is available since Visual Studio 2019), but it's not required when consuming the library. +This project targets .NET Core 3.1 and .NET Standard 2.0 for compatibility with older .NET Frameworks. +It supports the C# 8.0 nullable reference types feature for your convenience, but it's not required when consuming the library. ## Installing ### Release builds