Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/GitLabApiClient/GitLabApiClient.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<TargetFramework>net8.0</TargetFramework>
<LangVersion>10</LangVersion>
<PackageId>Apiiro.GitLabApiClient</PackageId>
<Version>0.1.44</Version>
<Version>0.1.46</Version>
<Authors>Apiiro</Authors>
<Company>Apiiro</Company>
<PackageDescription>GitLabApiClient</PackageDescription>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using System;
using System.Globalization;
using Newtonsoft.Json;

namespace GitLabApiClient.Internal.Http.Serialization
{
internal sealed class TolerantNullableDateTimeConverter : JsonConverter
{
public override bool CanConvert(Type objectType) =>
objectType == typeof(DateTime?) || objectType == typeof(DateTime);

public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
switch (reader.TokenType)
{
case JsonToken.Null:
return null;
case JsonToken.Date:
return reader.Value is DateTime dateTime ? dateTime : (DateTime?)null;
case JsonToken.String:
return DateTime.TryParse((string)reader.Value, CultureInfo.InvariantCulture, DateTimeStyles.None, out var parsed)
? parsed
: (DateTime?)null;
default:
return null;
}
}

public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
if (value is DateTime dateTime)
{
writer.WriteValue(dateTime);
}
else
{
writer.WriteNull();
}
}
}
}
4 changes: 3 additions & 1 deletion src/GitLabApiClient/Models/Member.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using GitLabApiClient.Internal.Http.Serialization;
using Newtonsoft.Json;

namespace GitLabApiClient.Models
Expand All @@ -9,6 +10,7 @@ public sealed class Member : Account
public int AccessLevel { get; set; }

[JsonProperty("expires_at")]
public DateTime ExpiresAt { get; set; }
[JsonConverter(typeof(TolerantNullableDateTimeConverter))]
public DateTime? ExpiresAt { get; set; }
}
}
58 changes: 58 additions & 0 deletions test/GitLabApiClient.Test/Internal/MemberExpiresAtTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using System;
using System.Collections.Generic;
using FluentAssertions;
using GitLabApiClient.Models;
using Newtonsoft.Json;
using Xunit;

namespace GitLabApiClient.Test.Internal
{
public class MemberExpiresAtTest
{
[Fact]
public void UnparseableExpiresAt_YieldsNull_AndRestOfListStillDeserializes()
{
const string json = @"[
{ ""id"": 1, ""username"": ""valid"", ""access_level"": 30, ""expires_at"": ""2025-12-04"" },
{ ""id"": 2, ""username"": ""broken"", ""access_level"": 40, ""expires_at"": ""20235-12-04"" }
]";

var members = JsonConvert.DeserializeObject<IList<Member>>(json);

members.Should().HaveCount(2);
members[0].ExpiresAt.Should().Be(new DateTime(2025, 12, 4));
members[1].Username.Should().Be("broken");
members[1].ExpiresAt.Should().BeNull();
}

[Fact]
public void NullExpiresAt_YieldsNull()
{
const string json = @"{ ""id"": 1, ""access_level"": 30, ""expires_at"": null }";

var member = JsonConvert.DeserializeObject<Member>(json);

member.ExpiresAt.Should().BeNull();
}

[Fact]
public void MissingExpiresAt_YieldsNull()
{
const string json = @"{ ""id"": 1, ""access_level"": 30 }";

var member = JsonConvert.DeserializeObject<Member>(json);

member.ExpiresAt.Should().BeNull();
}

[Fact]
public void ValidExpiresAt_IsParsed()
{
const string json = @"{ ""id"": 1, ""access_level"": 30, ""expires_at"": ""2025-12-04"" }";

var member = JsonConvert.DeserializeObject<Member>(json);

member.ExpiresAt.Should().Be(new DateTime(2025, 12, 4));
}
}
}
Loading