Skip to content

Commit

Permalink
QuestionActivity: map oneOf and anyOf to same Options property (r…
Browse files Browse the repository at this point in the history
…esolves #53)
  • Loading branch information
warriordog committed Dec 27, 2023
1 parent ed5d2a0 commit 72569db
Show file tree
Hide file tree
Showing 2 changed files with 204 additions and 63 deletions.
129 changes: 66 additions & 63 deletions Source/ActivityPub.Types/AS/Extended/Activity/QuestionActivity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,95 +43,98 @@ public QuestionActivity(TypeMap typeMap, QuestionActivityEntity? entity) : base(
private QuestionActivityEntity Entity { get; }

/// <summary>
/// Identifies an exclusive option for a Question.
/// Use of oneOf implies that the Question can have only a single answer.
/// To indicate that a Question can have multiple answers, use anyOf.
/// </summary>
/// <seealso href="https://www.w3.org/TR/activitystreams-vocabulary/#dfn-oneOf" />
public LinkableList<ASObject>? OneOf
{
get => Entity.OneOf;
set => Entity.OneOf = value;
}

/// <summary>
/// Identifies an inclusive option for a Question.
/// Use of anyOf implies that the Question can have multiple answers.
/// To indicate that a Question can have only one answer, use oneOf.
/// </summary>
/// <seealso href="https://www.w3.org/TR/activitystreams-vocabulary/#dfn-anyof" />
public LinkableList<ASObject>? AnyOf
{
get => Entity.AnyOf;
set => Entity.AnyOf = value;
}

/// <summary>
/// Contains the time at which a question was closed.
/// Indicates whether this question accepts multiple responses.
/// If <see langword="true"/>, then multiple options can be selected.
/// If <see langword="false"/> (default), then only one option may be selected.
/// </summary>
/// <remarks>
/// * Won't always be set - can be null even if <see cref="Closed" /> is true.
/// * We don't support the Object or Link forms, because what would that even mean??
/// * May possibly be in the future? Spec does not specify.
/// <para>
/// This is a synthetic field implemented to simplify parts of the Question schema.
/// It does not exist in the ActivityStreams specification.
/// </para>
/// <para>
/// This field controls how the question is serialized.
/// If <see langword="true"/>, then <see cref="Options"/> will map to "anyOf".
/// Otherwise, it maps to "oneOf".
/// </para>
/// </remarks>
/// <seealso href="https://www.w3.org/TR/activitystreams-vocabulary/#dfn-closed" />
public DateTime? ClosedAt
public bool AllowMultiple
{
get => Entity.ClosedAt;
set => Entity.ClosedAt = value;
get => Entity.AllowMultiple;
set => Entity.AllowMultiple = value;
}

/// <summary>
/// Indicates that a question has been closed, and answers are no longer accepted.
/// The list of options for this Question.
/// </summary>
/// <remarks>
/// We don't support the Object or Link forms, because what would that even mean??
/// This is a semi-synthetic property.
/// It does not exist in the ActivityStreams specification, but encapsulates the usage of both "oneOf" and "anyOf".
/// </remarks>
/// <seealso href="https://www.w3.org/TR/activitystreams-vocabulary/#dfn-closed" />
public bool? Closed
/// <seealso href="https://www.w3.org/TR/activitystreams-vocabulary/#dfn-oneOf" />
/// <seealso href="https://www.w3.org/TR/activitystreams-vocabulary/#dfn-anyof" />
public LinkableList<ASObject>? Options
{
get => Entity.Closed;
set => Entity.Closed = value;
get => Entity.Options;
set => Entity.Options = value;
}
}

/// <inheritdoc cref="QuestionActivity" />
public sealed class QuestionActivityEntity : ASEntity<QuestionActivity, QuestionActivityEntity>
public sealed class QuestionActivityEntity : ASEntity<QuestionActivity, QuestionActivityEntity>, IJsonOnDeserialized, IJsonOnSerializing
{
private bool? _closed;
private DateTime? _closedAt;

/// <inheritdoc cref="QuestionActivity.OneOf" />
/// <summary>
/// Use <see cref="Options"/> instead.
/// This internal property exists only for serialization purposes.
/// </summary>
[JsonPropertyName("oneOf")]
public LinkableList<ASObject>? OneOf { get; set; }

/// <inheritdoc cref="QuestionActivity.AnyOf" />
/// <summary>
/// Use <see cref="Options"/> instead.
/// This internal property exists only for serialization purposes.
/// </summary>
[JsonPropertyName("anyOf")]
public LinkableList<ASObject>? AnyOf { get; set; }

/// <inheritdoc cref="QuestionActivity.ClosedAt" />
[JsonPropertyName("closed")]
public DateTime? ClosedAt
/// <inheritdoc cref="QuestionActivity.Options" />
[JsonIgnore]
public LinkableList<ASObject>? Options { get; set; }

/// <inheritdoc cref="QuestionActivity.AllowMultiple" />
[JsonIgnore]
public bool AllowMultiple { get; set; }

void IJsonOnDeserialized.OnDeserialized()
{
get => _closedAt;
set
AllowMultiple = false;

if (AnyOf != null)
{
_closedAt = value;
if (_closedAt == null)
_closed = null;
AllowMultiple = true;

Options = AnyOf;
AnyOf = null;
}
}

/// <inheritdoc cref="QuestionActivity.Closed" />
[JsonPropertyName("closed")]
public bool? Closed
{
get => _closed ?? _closedAt != null;
set
if (OneOf != null)
{
_closed = value;
if (value != true)
_closedAt = null;
if (Options == null)
Options = OneOf;
else
Options.AddRange(OneOf);

OneOf = null;
}
}
void IJsonOnSerializing.OnSerializing()
{
OneOf = null;
AnyOf = null;

if (AllowMultiple)
AnyOf = Options;
else
OneOf = Options;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
// 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.AS;
using ActivityPub.Types.AS.Extended.Activity;
using ActivityPub.Types.Conversion;
using ActivityPub.Types.Tests.Util.Fixtures;

namespace ActivityPub.Types.Tests.Unit.AS.Extended.Activity;

public abstract class QuestionActivityTests : IClassFixture<JsonLdSerializerFixture>
{
private string JsonUnderTest
{
get => _jsonUnderTest.Value;
set
{
_jsonUnderTest = new Lazy<string>(value);
_questionUnderTest = new Lazy<QuestionActivity>(() => _jsonLdSerializer.Deserialize<QuestionActivity>(value)!);
}
}

private QuestionActivity QuestionUnderTest
{
get => _questionUnderTest.Value;
set
{
_jsonUnderTest = new Lazy<string>(() => _jsonLdSerializer.Serialize(value));
_questionUnderTest = new Lazy<QuestionActivity>(value);
}
}

private Lazy<string> _jsonUnderTest = new(() => throw new InvalidOperationException("Please populate JsonUnderTest or QuestionUnderTest"));
private Lazy<QuestionActivity> _questionUnderTest = new(() => throw new InvalidOperationException("Please populate JsonUnderTest or QuestionUnderTest"));

private readonly IJsonLdSerializer _jsonLdSerializer;
protected QuestionActivityTests(JsonLdSerializerFixture fixture) => _jsonLdSerializer = fixture.JsonLdSerializer;

public class OptionsShould : QuestionActivityTests
{
[Fact]
public void SerializeToOneOf_WhenAllowMultipleIsFalse()
{
QuestionUnderTest = new QuestionActivity
{
AllowMultiple = false,
Options = new ASObject()
};
JsonUnderTest.Should().Contain("\"oneOf\":");
JsonUnderTest.Should().NotContain("\"anyOf\":");
}

[Fact]
public void SerializeToAnyOf_WhenAllowMultipleIsTrue()
{
QuestionUnderTest = new QuestionActivity
{
AllowMultiple = true,
Options = new ASObject()
};
JsonUnderTest.Should().NotContain("\"oneOf\":");
JsonUnderTest.Should().Contain("\"anyOf\":");
}

[Fact]
public void DeserializeFromOneOf()
{
JsonUnderTest = """{"type":"Question","oneOf":{}}""";
QuestionUnderTest.Options.Should()
.NotBeNull()
.And.HaveCount(1);
}

[Fact]
public void DeserializeFromAnyOf()
{
JsonUnderTest = """{"type":"Question","anyOf":{}}""";
QuestionUnderTest.Options.Should()
.NotBeNull()
.And.HaveCount(1);
}

[Fact]
public void DeserializeFromOneOfAndAnyOf()
{
JsonUnderTest = """{"type":"Question","oneOf":{},"anyOf":{}}""";
QuestionUnderTest.Options.Should()
.NotBeNull()
.And.HaveCount(2);
}

[Fact]
public void NotSerializeToOptions()
{
QuestionUnderTest = new QuestionActivity
{
Options = new ASObject()
};
JsonUnderTest.ToLower().Should().NotContain("\"options\"");
}

[Fact]
public void NotDeserializeFromOptions()
{
JsonUnderTest = """{"type":"Question","options":{}}""";
QuestionUnderTest.Options.Should().BeNull();
}

public OptionsShould(JsonLdSerializerFixture fixture) : base(fixture) {}
}

public class AllowMultipleShould : QuestionActivityTests
{
[Fact]
public void DeserializeToFalse_WhenOnOfIsPresent()
{
JsonUnderTest = """{"type":"Question","oneOf":{}}""";
QuestionUnderTest.AllowMultiple.Should().BeFalse();
}

[Fact]
public void DeserializeToTrue_WhenAnyOfIsPresent()
{
JsonUnderTest = """{"type":"Question","anyOf":{}}""";
QuestionUnderTest.AllowMultiple.Should().BeTrue();
}

[Fact]
public void DeserializeToTrue_WhenOneOfAndAnyOfArePresent()
{

JsonUnderTest = """{"type":"Question","anyOf":{},"oneOf":{}}""";
QuestionUnderTest.AllowMultiple.Should().BeTrue();
}

public AllowMultipleShould(JsonLdSerializerFixture fixture) : base(fixture) {}
}
}

0 comments on commit 72569db

Please sign in to comment.