Skip to content

Commit

Permalink
QuestionActivity: reimplement Closed property (resolves #165)
Browse files Browse the repository at this point in the history
* Implement new `FlagWithTimestamp` type and associated converter.
* Re-implement `QuestionActivity.Closed` as `FlagWithTimestamp`.
  • Loading branch information
warriordog committed Dec 27, 2023
1 parent 72569db commit 2027c5f
Show file tree
Hide file tree
Showing 6 changed files with 322 additions and 1 deletion.
17 changes: 17 additions & 0 deletions Source/ActivityPub.Types/AS/Extended/Activity/QuestionActivity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,19 @@ public LinkableList<ASObject>? Options
get => Entity.Options;
set => Entity.Options = value;
}

/// <summary>
/// Indicates that a question has been closed, and answers are no longer accepted.
/// </summary>
/// <remarks>
/// We don't support the Object or Link forms, because what would that even mean??
/// </remarks>
/// <seealso href="https://www.w3.org/TR/activitystreams-vocabulary/#dfn-closed" />
public FlagWithTimestamp? Closed
{
get => Entity.Closed;
set => Entity.Closed = value;
}
}

/// <inheritdoc cref="QuestionActivity" />
Expand Down Expand Up @@ -105,6 +118,10 @@ public sealed class QuestionActivityEntity : ASEntity<QuestionActivity, Question
[JsonIgnore]
public bool AllowMultiple { get; set; }

/// <inheritdoc cref="QuestionActivity.Closed" />
[JsonPropertyName("closed")]
public FlagWithTimestamp? Closed { get; set; }

void IJsonOnDeserialized.OnDeserialized()
{
AllowMultiple = false;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
// If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/.

using System.Text.Json;
using System.Text.Json.Serialization;
using ActivityPub.Types.Util;

namespace ActivityPub.Types.Conversion.Converters;

/// <summary>
/// Custom JSON converter for <see cref="FlagWithTimestamp"/>.
/// </summary>
public class FlagWithTimestampConverter : JsonConverter<FlagWithTimestamp>
{
/// <inheritdoc />
public override FlagWithTimestamp? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
=> reader.TokenType switch
{
JsonTokenType.Null => null,
JsonTokenType.True => new FlagWithTimestamp { Value = true },
JsonTokenType.False => new FlagWithTimestamp { Value = false },
JsonTokenType.String => new FlagWithTimestamp { Timestamp = reader.GetDateTime() },
_ => throw new JsonException($"Can't convert {reader.TokenType} as {nameof(FlagWithTimestamp)}")
};

/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, FlagWithTimestamp value, JsonSerializerOptions options)
{
if (value.Timestamp != null)
writer.WriteStringValue(value.Timestamp.Value);
else
writer.WriteBooleanValue(value.Value);
}
}
69 changes: 69 additions & 0 deletions Source/ActivityPub.Types/Util/FlagWithTimestamp.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
// If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/.

using System.Text.Json.Serialization;
using ActivityPub.Types.Conversion.Converters;

namespace ActivityPub.Types.Util;

/// <summary>
/// A value that can be a <see langword="boolean"/> flag or a <see cref="DateTime"/> timestamp.
/// For example, <see cref="ActivityPub.Types.AS.Extended.Activity.QuestionActivity.Closed"/>.
/// </summary>
[JsonConverter(typeof(FlagWithTimestampConverter))]
public class FlagWithTimestamp
{
/// <summary>
/// The <see langword="boolean"/> value of this flag.
/// Will be true if <see cref="Timestamp"/> has a value.
/// If set to false, then <see cref="Timestamp"/> will also be set to <see langword="null"/>.
/// </summary>
public bool Value
{
get => _value;
set
{
_value = value;
if (!value)
_timestamp = null;
}
}

/// <summary>
/// The timestamp of this flag, is present.
/// If set to a value, then <see cref="Timestamp"/> will automatically change to <see langword="true"/>.
/// If set to <see langword="null"/>, then <see cref="Timestamp"/> will automatically change to <see langword="false"/>.
/// </summary>
public DateTime? Timestamp
{
get => _timestamp;
set
{
_value = value != null;
_timestamp = value;
}
}

private bool _value;
private DateTime? _timestamp;

/// <summary>
/// Returns the value of <see cref="Value"/>.
/// </summary>
public static implicit operator bool(FlagWithTimestamp flag) => flag.Value;

/// <summary>
/// Returns the value of <see cref="Timestamp"/>;
/// </summary>
public static implicit operator DateTime?(FlagWithTimestamp flag) => flag.Timestamp;

/// <summary>
/// Creates a flag with the specified value for <see cref="Value"/>.
/// </summary>
public static implicit operator FlagWithTimestamp(bool value) => new() { Value = value };

/// <summary>
/// Creates a flag with the specified value for <see cref="Timestamp"/>.
/// </summary>
public static implicit operator FlagWithTimestamp(DateTime timestamp) => new() { Timestamp = timestamp };
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
// If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/.

using ActivityPub.Types.Conversion.Converters;
using ActivityPub.Types.Util;

namespace ActivityPub.Types.Tests.Unit.Conversion.Converters;

public abstract class FlagWithTimestampConverterTests : JsonConverterTests<FlagWithTimestamp, FlagWithTimestampConverter>
{
protected override FlagWithTimestampConverter ConverterUnderTest { get; set; } = new();

public class ReadShould : FlagWithTimestampConverterTests
{
[Fact]
public void ConvertFromNull()
{
var flag = Read("null"u8);
flag.Should().BeNull();
}

[Fact]
public void ConvertFromTrue()
{
var flag = Read("true"u8);
flag.Should().NotBeNull();
flag!.Value.Should().BeTrue();
flag.Timestamp.Should().BeNull();
}

[Fact]
public void ConvertFromFalse()
{
var flag = Read("false"u8);
flag.Should().NotBeNull();
flag!.Value.Should().BeFalse();
flag.Timestamp.Should().BeNull();
}

[Fact]
public void ConvertFromDateTime()
{
var flag = Read("\"2024-01-01T00:00:00\""u8);
flag.Should().NotBeNull();
flag!.Value.Should().BeTrue();
flag.Timestamp.Should().Be(new DateTime(2024, 1, 1));
}

[Theory]
[InlineData("1")]
[InlineData("{}")]
[InlineData("[]")]
public void ThrowOnOtherInput(string json)
{
Assert.Throws<JsonException>(() =>
{
Read(json);
});
}
}

public class WriteShould : FlagWithTimestampConverterTests
{
[Theory]
[InlineData(true, "true")]
[InlineData(false, "false")]
public void WriteValueToBool(bool value, string expectedJson)
{
var flag = new FlagWithTimestamp { Value = value };
var actualJson = Write(flag);
actualJson.Should().Be(expectedJson);
}

[Fact]
public void WriteTimestampToString()
{
var flag = new FlagWithTimestamp { Timestamp = new DateTime(2024, 1, 1) };
var json = Write(flag);
json.Should().Be("\"2024-01-01T00:00:00\"");
}

[Fact]
public void WriteTimestamp_WhenBothAreSet()
{
var flag = new FlagWithTimestamp
{
Timestamp = new DateTime(2024, 1, 1),
Value = true
};
var json = Write(flag);
json.Should().Be("\"2024-01-01T00:00:00\"");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,22 @@

using System.Text;
using System.Text.Json.Serialization;
using System.Text.Unicode;

namespace ActivityPub.Types.Tests.Unit.Conversion.Converters;

internal abstract class JsonConverterTests<T, TConverter>
public abstract class JsonConverterTests<T, TConverter>
where TConverter : JsonConverter<T>
{
protected abstract TConverter ConverterUnderTest { get; set; }
protected JsonSerializerOptions JsonSerializerOptions { get; set; } = JsonSerializerOptions.Default;

protected T? Read(string json)
{
var bytes = Encoding.UTF8.GetBytes(json).AsSpan();
return Read(bytes);
}

// Useful: https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/use-utf8jsonreader
protected T? Read(ReadOnlySpan<byte> json)
{
Expand Down
100 changes: 100 additions & 0 deletions Test/ActivityPub.Types.Tests/Unit/Util/FlagWithTimestampTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
// If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/.

using ActivityPub.Types.Util;

namespace ActivityPub.Types.Tests.Unit.Util;

public abstract class FlagWithTimestampTests
{
private FlagWithTimestamp? FlagUnderTest { get; set; }

public class ValueShould : FlagWithTimestampTests
{
[Fact]
public void ConvertFromTrue()
{
FlagUnderTest = true;
FlagUnderTest.Value.Should().BeTrue();
}

[Fact]
public void ConvertFromFalse()
{
FlagUnderTest = false;
FlagUnderTest.Value.Should().BeFalse();
}

[Fact]
public void ConvertToTrue()
{
bool value = new FlagWithTimestamp { Value = true };
value.Should().BeTrue();
}

[Fact]
public void ConvertToFalse()
{
bool value = new FlagWithTimestamp { Value = false };
value.Should().BeFalse();
}

[Fact]
public void SetTimeStampToNull_WhenSetToFalse()
{
FlagUnderTest = new DateTime(2024, 1, 1);
FlagUnderTest.Value = false;
FlagUnderTest.Timestamp.Should().BeNull();
}

[Fact]
public void IgnoreTimestamp_WhenSetToTrue()
{
FlagUnderTest = new DateTime(2024, 1, 1);
FlagUnderTest.Value = true;
FlagUnderTest.Timestamp.Should().NotBeNull();
}
}

public class TimestampShould : FlagWithTimestampTests
{
[Fact]
public void ConvertFromTimestamp()
{
var timestamp = new DateTime(2024, 1, 1);
FlagUnderTest = timestamp;
FlagUnderTest.Timestamp.Should().Be(timestamp);
}

[Fact]
public void ConvertToNull()
{
DateTime? timestamp = new FlagWithTimestamp { Timestamp = null };
timestamp.Should().BeNull();
}

[Fact]
public void ConvertToTimestamp()
{
var expectedTimestamp = new DateTime(2024, 1, 1);
DateTime? actualTimestamp = new FlagWithTimestamp { Timestamp = expectedTimestamp };
actualTimestamp.Should().Be(expectedTimestamp);
}

[Fact]
public void SetValueToTrue_WhenSetToTimestamp()
{
FlagUnderTest = false;
FlagUnderTest.Timestamp = new DateTime(2024, 1, 1);
FlagUnderTest.Value.Should().BeTrue();
}

[Fact]
public void SetValueToFalse_WhenSetToNull()
{
FlagUnderTest = true;
FlagUnderTest.Timestamp = null;
FlagUnderTest.Value.Should().BeFalse();
}
}
}

0 comments on commit 2027c5f

Please sign in to comment.