From 2027c5fa19ba2b4c9b7df36b52a71cbb2e304605 Mon Sep 17 00:00:00 2001 From: Hazel Koehler Date: Wed, 27 Dec 2023 00:23:02 -0500 Subject: [PATCH] QuestionActivity: reimplement `Closed` property (resolves #165) * Implement new `FlagWithTimestamp` type and associated converter. * Re-implement `QuestionActivity.Closed` as `FlagWithTimestamp`. --- .../AS/Extended/Activity/QuestionActivity.cs | 17 +++ .../Converters/FlagWithTimestampConverter.cs | 34 ++++++ .../Util/FlagWithTimestamp.cs | 69 ++++++++++++ .../FlagWithTimestampConverterTests.cs | 94 ++++++++++++++++ .../Converters/JsonConverterTests.cs | 9 +- .../Unit/Util/FlagWithTimestampTests.cs | 100 ++++++++++++++++++ 6 files changed, 322 insertions(+), 1 deletion(-) create mode 100644 Source/ActivityPub.Types/Conversion/Converters/FlagWithTimestampConverter.cs create mode 100644 Source/ActivityPub.Types/Util/FlagWithTimestamp.cs create mode 100644 Test/ActivityPub.Types.Tests/Unit/Conversion/Converters/FlagWithTimestampConverterTests.cs create mode 100644 Test/ActivityPub.Types.Tests/Unit/Util/FlagWithTimestampTests.cs diff --git a/Source/ActivityPub.Types/AS/Extended/Activity/QuestionActivity.cs b/Source/ActivityPub.Types/AS/Extended/Activity/QuestionActivity.cs index dcc1745..295d0af 100644 --- a/Source/ActivityPub.Types/AS/Extended/Activity/QuestionActivity.cs +++ b/Source/ActivityPub.Types/AS/Extended/Activity/QuestionActivity.cs @@ -78,6 +78,19 @@ public LinkableList? Options get => Entity.Options; set => Entity.Options = value; } + + /// + /// Indicates that a question has been closed, and answers are no longer accepted. + /// + /// + /// We don't support the Object or Link forms, because what would that even mean?? + /// + /// + public FlagWithTimestamp? Closed + { + get => Entity.Closed; + set => Entity.Closed = value; + } } /// @@ -105,6 +118,10 @@ public sealed class QuestionActivityEntity : ASEntity + [JsonPropertyName("closed")] + public FlagWithTimestamp? Closed { get; set; } + void IJsonOnDeserialized.OnDeserialized() { AllowMultiple = false; diff --git a/Source/ActivityPub.Types/Conversion/Converters/FlagWithTimestampConverter.cs b/Source/ActivityPub.Types/Conversion/Converters/FlagWithTimestampConverter.cs new file mode 100644 index 0000000..496f6bf --- /dev/null +++ b/Source/ActivityPub.Types/Conversion/Converters/FlagWithTimestampConverter.cs @@ -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; + +/// +/// Custom JSON converter for . +/// +public class FlagWithTimestampConverter : JsonConverter +{ + /// + 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)}") + }; + + /// + public override void Write(Utf8JsonWriter writer, FlagWithTimestamp value, JsonSerializerOptions options) + { + if (value.Timestamp != null) + writer.WriteStringValue(value.Timestamp.Value); + else + writer.WriteBooleanValue(value.Value); + } +} \ No newline at end of file diff --git a/Source/ActivityPub.Types/Util/FlagWithTimestamp.cs b/Source/ActivityPub.Types/Util/FlagWithTimestamp.cs new file mode 100644 index 0000000..cf6d049 --- /dev/null +++ b/Source/ActivityPub.Types/Util/FlagWithTimestamp.cs @@ -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; + +/// +/// A value that can be a flag or a timestamp. +/// For example, . +/// +[JsonConverter(typeof(FlagWithTimestampConverter))] +public class FlagWithTimestamp +{ + /// + /// The value of this flag. + /// Will be true if has a value. + /// If set to false, then will also be set to . + /// + public bool Value + { + get => _value; + set + { + _value = value; + if (!value) + _timestamp = null; + } + } + + /// + /// The timestamp of this flag, is present. + /// If set to a value, then will automatically change to . + /// If set to , then will automatically change to . + /// + public DateTime? Timestamp + { + get => _timestamp; + set + { + _value = value != null; + _timestamp = value; + } + } + + private bool _value; + private DateTime? _timestamp; + + /// + /// Returns the value of . + /// + public static implicit operator bool(FlagWithTimestamp flag) => flag.Value; + + /// + /// Returns the value of ; + /// + public static implicit operator DateTime?(FlagWithTimestamp flag) => flag.Timestamp; + + /// + /// Creates a flag with the specified value for . + /// + public static implicit operator FlagWithTimestamp(bool value) => new() { Value = value }; + + /// + /// Creates a flag with the specified value for . + /// + public static implicit operator FlagWithTimestamp(DateTime timestamp) => new() { Timestamp = timestamp }; +} \ No newline at end of file diff --git a/Test/ActivityPub.Types.Tests/Unit/Conversion/Converters/FlagWithTimestampConverterTests.cs b/Test/ActivityPub.Types.Tests/Unit/Conversion/Converters/FlagWithTimestampConverterTests.cs new file mode 100644 index 0000000..327bc99 --- /dev/null +++ b/Test/ActivityPub.Types.Tests/Unit/Conversion/Converters/FlagWithTimestampConverterTests.cs @@ -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 +{ + 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(() => + { + 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\""); + } + } +} \ No newline at end of file diff --git a/Test/ActivityPub.Types.Tests/Unit/Conversion/Converters/JsonConverterTests.cs b/Test/ActivityPub.Types.Tests/Unit/Conversion/Converters/JsonConverterTests.cs index a0f1db3..bc38f99 100644 --- a/Test/ActivityPub.Types.Tests/Unit/Conversion/Converters/JsonConverterTests.cs +++ b/Test/ActivityPub.Types.Tests/Unit/Conversion/Converters/JsonConverterTests.cs @@ -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 +public abstract class JsonConverterTests where TConverter : JsonConverter { 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 json) { diff --git a/Test/ActivityPub.Types.Tests/Unit/Util/FlagWithTimestampTests.cs b/Test/ActivityPub.Types.Tests/Unit/Util/FlagWithTimestampTests.cs new file mode 100644 index 0000000..940f77a --- /dev/null +++ b/Test/ActivityPub.Types.Tests/Unit/Util/FlagWithTimestampTests.cs @@ -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(); + } + } +} \ No newline at end of file