Skip to content

Commit

Permalink
overhaul JSON implementation - now it works! (resolves #21)
Browse files Browse the repository at this point in the history
  • Loading branch information
warriordog committed Jul 15, 2023
1 parent cc76ca3 commit 90a3a55
Show file tree
Hide file tree
Showing 36 changed files with 992 additions and 630 deletions.
7 changes: 5 additions & 2 deletions ActivityPub.Client/ActivityPubClient.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
// 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.Net.Http.Headers;
using System.Text.Json;
using ActivityPub.Common.TypeInfo;
using ActivityPub.Common.Util;
using ActivityPub.Types;
using ActivityPub.Types.Json;
using ActivityPub.Types.Util;

namespace ActivityPub.Client;
Expand All @@ -17,8 +19,9 @@ public class ActivityPubClient : IActivityPubClient
private readonly HttpClient _httpClient = new();
private readonly ITypeInfoCache _typeInfoCache;
private readonly ActivityPubOptions _apOptions;
private readonly IJsonLdSerializer _jsonLdSerializer;

public ActivityPubClient(ITypeInfoCache typeInfoCache, ActivityPubOptions apOptions)
public ActivityPubClient(ITypeInfoCache typeInfoCache, ActivityPubOptions apOptions, IJsonLdSerializer jsonLdSerializer)
{
_typeInfoCache = typeInfoCache;
_apOptions = apOptions;
Expand Down Expand Up @@ -50,7 +53,7 @@ private async Task<ASType> Get(Uri uri, Type targetType, int? maxRecursion, Canc
throw new ApplicationException($"Request failed: unsupported content type {mediaType}");

var json = await resp.Content.ReadAsStringAsync(cancellationToken);
var jsonObj = JsonSerializer.Deserialize(json, targetType, _apOptions.SerializerOptions);
var jsonObj = _jsonLdSerializer.Deserialize(json, targetType);
if (jsonObj is not ASType obj)
throw new JsonException($"Failed to deserialize object - parser returned unsupported object {jsonObj?.GetType()}");

Expand Down
4 changes: 1 addition & 3 deletions ActivityPub.Client/ClientModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,9 @@ namespace ActivityPub.Client;

public static class ClientModule
{
public const string ConfigSection = "ClientModule";

public static void TryAddClientModule(this HostApplicationBuilder builder)
{
builder.TryAddCommonModule();
builder.Services.TryAddCommonModule();
builder.Services.TryAddSingleton<IActivityPubClient, ActivityPubClient>();
}
}
5 changes: 2 additions & 3 deletions ActivityPub.Common/ActivityPub.Common.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,11 @@
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\ActivityPub.Types\ActivityPub.Types.csproj"/>
<ProjectReference Include="..\ActivityPub.Types\ActivityPub.Types.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0"/>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="7.0.1"/>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" />
</ItemGroup>

<ItemGroup>
Expand Down
13 changes: 7 additions & 6 deletions ActivityPub.Common/CommonModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,19 @@

using ActivityPub.Common.TypeInfo;
using ActivityPub.Common.Util;
using ActivityPub.Types;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting;

namespace ActivityPub.Common;

public static class CommonModule
{
public const string ConfigSection = "CommonModule";

public static void TryAddCommonModule(this HostApplicationBuilder builder)
public static void TryAddCommonModule(this IServiceCollection services)
{
builder.Services.TryAddSingleton<ITypeInfoCache, TypeInfoCache>();
builder.Services.TryAddSingleton<ActivityPubOptions>();
services.TryAddTypesModule();

services.TryAddSingleton<ITypeInfoCache, TypeInfoCache>();
services.TryAddSingleton<ActivityPubOptions>();
}
}
3 changes: 3 additions & 0 deletions ActivityPub.Common/TypeInfo/TypeInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@

namespace ActivityPub.Common.TypeInfo;

// TODO move this entire module into the sample app.
// Why did I put it here in the first place??

public class TypeInfo
{
public required Type Type { get; init; }
Expand Down
72 changes: 15 additions & 57 deletions ActivityPub.Types/ASLink.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
// 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.Diagnostics.CodeAnalysis;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
using ActivityPub.Types.Internal;
using ActivityPub.Types.Json;
using ActivityPub.Types.Util;

Expand All @@ -18,9 +18,8 @@ namespace ActivityPub.Types;
/// Properties of the Link are properties of the reference as opposed to properties of the resource.
/// </summary>
/// <seealso href="https://www.w3.org/TR/activitystreams-vocabulary/#dfn-link"/>
[JsonConverter(typeof(ASLinkConverter))]
[ASTypeKey(LinkType)]
public class ASLink : ASType, IJsonConvertible<ASLink>
public class ASLink : ASType
{
public const string LinkType = "Link";

Expand Down Expand Up @@ -79,77 +78,36 @@ protected ASLink(string type) : base(type) {}
public static implicit operator ASUri(ASLink link) => link.HRef;
public static implicit operator ASLink(ASUri asUri) => new() { HRef = asUri };


protected override void ReadJson(JsonElement element, JsonOptions options)
{
base.ReadJson(element, options);

// Skip HRef - it has to be pre-initialized
if (element.TryGetProperty("hreflang", out var hRefLang))
HRefLang = hRefLang.GetString();
if (element.TryGetProperty("width", out var width))
Width = width.GetInt32();
if (element.TryGetProperty("height", out var height))
Height = height.GetInt32();
if (element.TryGetProperty("rel", out var rel))
Rel = rel.Deserialize<HashSet<string>>(options.SerializerOptions)!;
}

protected override void WriteJson(JsonNode node, JsonOptions options)
{
base.WriteJson(node, options);

node["href"] = JsonValue.Create(HRef, options.NodeOptions);
if (HRefLang != null)
node["hreflang"] = JsonValue.Create(HRefLang, options.NodeOptions);
if (Width != null)
node["width"] = JsonValue.Create(Width, options.NodeOptions);
if (Height != null)
node["height"] = JsonValue.Create(Height, options.NodeOptions);
if (Rel.Count > 0)
node["rel"] = JsonSerializer.SerializeToNode(Rel, options.SerializerOptions);
}

public new static ASLink? Deserialize(JsonElement element, JsonOptions options)
[CustomJsonDeserializer]
public static bool TryDeserialize(JsonElement element, JsonSerializerOptions options, [NotNullWhen(true)] out ASLink? obj)
{
if (element.ValueKind == JsonValueKind.Null)
return null;

// Parse flattened form
// We either parse from string, or allow parser to use default logic
if (element.ValueKind == JsonValueKind.String)
{
return new ASLink()
obj = new ASLink
{
HRef = element.GetString()!
};
return true;
}

if (element.ValueKind == JsonValueKind.Object && element.TryGetProperty("href", out var href) && href.TryGetString(out var hrefString))
{
var link = new ASLink
{
HRef = hrefString
};
link.ReadJson(element, options);
return link;
}

throw new JsonException("Can't deserialize ASLink - not in any supported form");
obj = null;
return false;
}

public static JsonNode? Serialize(ASLink obj, JsonOptions options)
[CustomJsonSerializer]
public static bool TrySerialize(ASLink obj, JsonSerializerOptions options, JsonNodeOptions nodeOptions, [NotNullWhen(true)] out JsonNode? node)
{
// TODO split ASLink into flat and object form

// If its only a link, then use the flattened form
if (obj.HasOnlyHRef)
{
return JsonValue.Create(obj.HRef.Uri.ToString(), options.NodeOptions);
node = JsonValue.Create(obj.HRef.Uri.ToString(), nodeOptions)!;
return true;
}

var node = new JsonObject(options.NodeOptions);
obj.WriteJson(node, options);
return node;
node = null;
return false;
}

/// <summary>
Expand Down
49 changes: 1 addition & 48 deletions ActivityPub.Types/ASType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,7 @@
// 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.Diagnostics.CodeAnalysis;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
using ActivityPub.Types.Internal;
using ActivityPub.Types.Json;
using ActivityPub.Types.Util;

Expand All @@ -18,7 +15,7 @@ namespace ActivityPub.Types;
/// This is a synthetic type created to help adapt ActivityStreams to the .NET object model.
/// It does not exist in the ActivityStreams standard.
/// </remarks>
public abstract class ASType : IJsonConvertible<ASType>
public abstract class ASType
{
protected ASType(string defaultType) => Types.Add(defaultType);

Expand Down Expand Up @@ -108,48 +105,4 @@ public string? Id
/// <seealso href="https://www.w3.org/TR/activitystreams-vocabulary/#dfn-mediaType"/>
[JsonPropertyName("mediaType")]
public Linkable<ASObject>? MediaType { get; set; }


protected virtual void ReadJson(JsonElement element, JsonOptions options)
{
if (element.TryGetProperty("type", out var type))
Types = type.Deserialize<HashSet<string>>(options.SerializerOptions)!;

if (element.TryGetProperty("@context", out var ldContext))
JsonLdContexts = ldContext.Deserialize<HashSet<string>>(options.SerializerOptions)!;

if (element.TryGetProperty("id", out var id) && id.TryGetString(out var idStr))
Id = idStr;

if (element.TryGetProperty("attributedTo", out var attributedTo))
AttributedTo = attributedTo.Deserialize<LinkableList<ASObject>>(options.SerializerOptions)!;

if (element.TryGetProperty("preview", out var preview))
Preview = preview.Deserialize<Linkable<ASObject>>(options.SerializerOptions)!;

if (element.TryGetProperty("name", out var name))
Name = name.Deserialize<NaturalLanguageString>(options.SerializerOptions)!;

if (element.TryGetProperty("mediaType", out var mediaType))
MediaType = mediaType.Deserialize<Linkable<ASObject>>(options.SerializerOptions)!;
}

protected virtual void WriteJson(JsonNode node, JsonOptions options)
{
node["type"] = JsonSerializer.SerializeToNode(Types, options.SerializerOptions);
node["@context"] = JsonSerializer.SerializeToNode(JsonLdContexts, options.SerializerOptions);
if (!IsAnonymous)
node["id"] = JsonValue.Create(Id, options.NodeOptions);
if (AttributedTo.Count > 0)
node["attributedTo"] = JsonSerializer.SerializeToNode(AttributedTo, options.SerializerOptions);
if (Preview != null)
node["preview"] = JsonSerializer.SerializeToNode(Preview, options.SerializerOptions);
if (Name != null)
node["name"] = JsonSerializer.SerializeToNode(Name, options.SerializerOptions);
if (MediaType != null)
node["mediaType"] = JsonSerializer.SerializeToNode(MediaType, options.SerializerOptions);
}

public static ASType Deserialize(JsonElement element, JsonOptions options) => throw new NotSupportedException("ASType is abstract and should be deserialized through a subclass");
public static JsonNode Serialize(ASType obj, JsonOptions options) => throw new NotSupportedException("ASType is abstract and should be serialized through a subclass");
}
4 changes: 3 additions & 1 deletion ActivityPub.Types/ActivityPub.Types.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="System.Text.Json" Version="7.0.3" />
<PackageReference Include="JetBrains.Annotations" Version="2023.2.0"/>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0"/>
<PackageReference Include="System.Text.Json" Version="7.0.3"/>
</ItemGroup>

</Project>
42 changes: 3 additions & 39 deletions ActivityPub.Types/Internal/JsonExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace ActivityPub.Types.Internal;

Expand Down Expand Up @@ -31,19 +30,6 @@ public static bool TryGetString(this Utf8JsonReader reader, [NotNullWhen(true)]
return type != null;
}

/// <summary>
/// Modifies the JsonSerializerOptions to remove all JsonConverters of the specified type
/// </summary>
/// <param name="options"></param>
/// <typeparam name="T"></typeparam>
/// <returns>Returns the same object for chaining</returns>
public static JsonSerializerOptions RemoveConvertersOfType<T>(this JsonSerializerOptions options)
where T : JsonConverter
{
options.Converters.RemoveWhere(c => c is T);
return options;
}

/// <summary>
/// Attempts to read the element as a string.
/// Returns true on success.
Expand All @@ -67,37 +53,15 @@ public static bool TryGetString(this JsonElement element, [NotNullWhen(true)] ou
/// Attempts to read the provided <see cref="JsonElement"/> as an ActivityStreams object and return its type.
/// Returns false if the element does not contain an object or the object does not contain a valid type.
/// </summary>
/// <param name="objectElement">object to read</param>
/// <param name="element">object to read</param>
/// <param name="type">Set to the AS type on success, or null on failure</param>
/// <returns>Returns true on success, false on failure</returns>
public static bool TryGetASType(this JsonElement objectElement, [NotNullWhen(true)] out string? type)
public static bool TryGetASType(this JsonElement element, [NotNullWhen(true)] out string? type)
{
if (objectElement.TryGetProperty("type", out var asTypeElement) && asTypeElement.TryGetString(out type))
if (element.ValueKind == JsonValueKind.Object && element.TryGetProperty("type", out var asTypeElement) && asTypeElement.TryGetString(out type))
return true;

type = null;
return false;
}

// /// <summary>
// /// TODO docs
// /// </summary>
// /// <param name="element"></param>
// /// <param name="name"></param>
// /// <param name="options"></param>
// /// <param name="value"></param>
// /// <typeparam name="T"></typeparam>
// /// <returns></returns>
// /// <exception cref="JsonException"></exception>
// public static bool TryGetProperty<T>(this JsonElement element, string name, JsonSerializerOptions options, [NotNullWhen(true)] out T? value)
// {
// if (!element.TryGetProperty(name, out var prop))
// {
// value = default;
// return false;
// }
//
// value = prop.Deserialize<T>() ?? throw new JsonException($"Conversion to {typeof(T)} failed");
// return true;
// }
}
Loading

0 comments on commit 90a3a55

Please sign in to comment.