Skip to content

Commit

Permalink
implement lazy type graphs (resolves #153)
Browse files Browse the repository at this point in the history
* Reverse control of type parsing / projection
* Implement `TypeGraphReader`
* Replace `ASTypeInfoCache` with static `ASNameTree`
* Remove pivots
* Remove `IAnonymousEntity`, `IAnonymousEntitySelector`, `ICustomConvertedEntity`, and `INamelessEntity` in favor of `IASModel.ShouldConvertFrom()` and STJ native conversion callbacks
  • Loading branch information
warriordog committed Dec 25, 2023
1 parent b27bc66 commit 16ddc8f
Show file tree
Hide file tree
Showing 90 changed files with 696 additions and 1,501 deletions.
116 changes: 92 additions & 24 deletions Source/ActivityPub.Types/AS/APActor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,8 @@
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using System.Text.Json.Serialization;
using ActivityPub.Types.Conversion.Overrides;
using ActivityPub.Types.Internal;
using ActivityPub.Types.Util;
using JetBrains.Annotations;

namespace ActivityPub.Types.AS;

Expand All @@ -22,11 +20,11 @@ namespace ActivityPub.Types.AS;
public class APActor : ASObject, IASModel<APActor, APActorEntity, ASObject>
{
/// <inheritdoc />
public APActor() => Entity = TypeMap.Extend<APActorEntity>();
public APActor() => Entity = TypeMap.Extend<APActor, APActorEntity>();

/// <inheritdoc />
public APActor(TypeMap typeMap, bool isExtending = true) : base(typeMap, false)
=> Entity = TypeMap.ProjectTo<APActorEntity>(isExtending);
=> Entity = TypeMap.ProjectTo<APActor, APActorEntity>(isExtending);

/// <inheritdoc />
public APActor(ASType existingGraph) : this(existingGraph.TypeMap) {}
Expand All @@ -35,7 +33,7 @@ public APActor(ASType existingGraph) : this(existingGraph.TypeMap) {}
[SetsRequiredMembers]
public APActor(TypeMap typeMap, APActorEntity? entity) : base(typeMap, null)
{
Entity = entity ?? typeMap.AsEntity<APActorEntity>();
Entity = entity ?? typeMap.AsEntity<APActor, APActorEntity>();
Inbox = Entity.Inbox ?? throw new ArgumentException($"The provided entity is invalid - required {nameof(APActorEntity.Inbox)} property is missing");
Outbox = Entity.Outbox ?? throw new ArgumentException($"The provided entity is invalid - required {nameof(APActorEntity.Outbox)} property is missing");
}
Expand Down Expand Up @@ -134,10 +132,21 @@ public Linkable<ActorEndpoints>? Endpoints
get => Entity.Endpoints;
set => Entity.Endpoints = value;
}

/// <inheritdoc />
static bool? IASModel<APActor>.ShouldConvertFrom(JsonElement inputJson, TypeMap typeMap)
{
if (inputJson.ValueKind != JsonValueKind.Object)
return false;

return
inputJson.HasProperty("inbox") &&
inputJson.HasProperty("outbox");
}
}

/// <inheritdoc cref="APActor" />
public sealed class APActorEntity : ASEntity<APActor, APActorEntity>, IAnonymousEntity
public sealed class APActorEntity : ASEntity<APActor, APActorEntity>
{
/// <inheritdoc cref="APActor.Inbox" />
[JsonPropertyName("inbox")]
Expand Down Expand Up @@ -170,62 +179,121 @@ public sealed class APActorEntity : ASEntity<APActor, APActorEntity>, IAnonymous
/// <inheritdoc cref="APActor.Endpoints" />
[JsonPropertyName("endpoints")]
public Linkable<ActorEndpoints>? Endpoints { get; set; }

/// <inheritdoc />
public static bool ShouldConvertFrom(JsonElement inputJson, DeserializationMetadata meta)
{
if (inputJson.ValueKind != JsonValueKind.Object)
return false;

return
inputJson.HasProperty("inbox") &&
inputJson.HasProperty("outbox");
}
}

/// <summary>
/// A json object which maps additional (typically server/domain-wide) endpoints which may be useful for an actor.
/// </summary>
/// <seealso href="https://www.w3.org/TR/activitypub/#actor-objects" />
[UsedImplicitly(ImplicitUseKindFlags.InstantiatedWithFixedConstructorSignature, ImplicitUseTargetFlags.WithMembers)]
public class ActorEndpoints
public class ActorEndpoints : ASType, IASModel<ActorEndpoints, ActorEndpointsEntity, ASType>
{
/// <inheritdoc />
public ActorEndpoints() => Entity = TypeMap.Extend<ActorEndpoints, ActorEndpointsEntity>();

/// <inheritdoc />
public ActorEndpoints(TypeMap typeMap, bool isExtending = true) : base(typeMap, false)
=> Entity = TypeMap.ProjectTo<ActorEndpoints, ActorEndpointsEntity>(isExtending);

/// <inheritdoc />
public ActorEndpoints(ASType existingGraph) : this(existingGraph.TypeMap) {}

/// <inheritdoc />
[SetsRequiredMembers]
public ActorEndpoints(TypeMap typeMap, ActorEndpointsEntity? entity) : base(typeMap, null)
=> Entity = entity ?? typeMap.AsEntity<ActorEndpoints, ActorEndpointsEntity>();

static ActorEndpoints IASModel<ActorEndpoints>.FromGraph(TypeMap typeMap) => new(typeMap, null);

private ActorEndpointsEntity Entity { get; }

/// <summary>
/// Endpoint URI so this actor's clients may access remote ActivityStreams objects which require authentication to access.
/// To use this endpoint, the client posts an x-www-form-urlencoded id parameter with the value being the id of the requested ActivityStreams object.
/// </summary>
[JsonPropertyName("proxyUrl")]
public ASLink? ProxyUrl { get; set; }
public ASLink? ProxyUrl
{
get => Entity.ProxyUrl;
set => Entity.ProxyUrl = value;
}

/// <summary>
/// If OAuth 2.0 bearer tokens [RFC6749] [RFC6750] are being used for authenticating client to server interactions, this endpoint specifies a URI at which a browser-authenticated user may obtain a new authorization grant.
/// </summary>
[JsonPropertyName("oauthAuthorizationEndpoint")]
public ASLink? OAuthAuthorizationEndpoint { get; set; }
public ASLink? OAuthAuthorizationEndpoint
{
get => Entity.OAuthAuthorizationEndpoint;
set => Entity.OAuthAuthorizationEndpoint = value;
}

/// <summary>
/// If OAuth 2.0 bearer tokens [RFC6749] [RFC6750] are being used for authenticating client to server interactions, this endpoint specifies a URI at which a client may acquire an access token.
/// </summary>
[JsonPropertyName("oauthTokenEndpoint")]
public ASLink? OAuthTokenEndpoint { get; set; }
public ASLink? OAuthTokenEndpoint
{
get => Entity.OAuthTokenEndpoint;
set => Entity.OAuthTokenEndpoint = value;
}

/// <summary>
/// If Linked Data Signatures and HTTP Signatures are being used for authentication and authorization, this endpoint specifies a URI at which browser-authenticated users may authorize a client's public key for client to server interactions.
/// </summary>
[JsonPropertyName("provideClientKey")]
public ASLink? ProvideClientKey { get; set; }
public ASLink? ProvideClientKey
{
get => Entity.ProvideClientKey;
set => Entity.ProvideClientKey = value;
}

/// <summary>
/// If Linked Data Signatures and HTTP Signatures are being used for authentication and authorization, this endpoint specifies a URI at which a client key may be signed by the actor's key for a time window to act on behalf of the actor in interacting with foreign servers.
/// </summary>
[JsonPropertyName("signClientKey")]
public ASLink? SignClientKey { get; set; }
public ASLink? SignClientKey
{
get => Entity.SignClientKey;
set => Entity.SignClientKey = value;
}

/// <summary>
/// An optional endpoint used for wide delivery of publicly addressed activities and activities sent to followers.
/// SharedInbox endpoints SHOULD also be publicly readable OrderedCollection objects containing objects addressed to the Public special collection.
/// Reading from the sharedInbox endpoint MUST NOT present objects which are not addressed to the Public endpoint.
/// </summary>
[JsonPropertyName("sharedInbox")]
public ASLink? SharedInbox
{
get => Entity.SharedInbox;
set => Entity.SharedInbox = value;
}
}

/// <inheritdoc cref="ActorEndpoints"/>
public sealed class ActorEndpointsEntity : ASEntity<ActorEndpoints, ActorEndpointsEntity>
{
/// <inheritdoc cref="ActorEndpoints"/>
[JsonPropertyName("proxyUrl")]
public ASLink? ProxyUrl { get; set; }

/// <inheritdoc cref="ActorEndpoints"/>
[JsonPropertyName("oauthAuthorizationEndpoint")]
public ASLink? OAuthAuthorizationEndpoint { get; set; }

/// <inheritdoc cref="ActorEndpoints"/>
[JsonPropertyName("oauthTokenEndpoint")]
public ASLink? OAuthTokenEndpoint { get; set; }

/// <inheritdoc cref="ActorEndpoints"/>
[JsonPropertyName("provideClientKey")]
public ASLink? ProvideClientKey { get; set; }

/// <inheritdoc cref="ActorEndpoints"/>
[JsonPropertyName("signClientKey")]
public ASLink? SignClientKey { get; set; }

/// <inheritdoc cref="ActorEndpoints"/>
[JsonPropertyName("sharedInbox")]
public ASLink? SharedInbox { get; set; }
}
6 changes: 3 additions & 3 deletions Source/ActivityPub.Types/AS/ASActivity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,19 @@ public class ASActivity : ASObject, IASModel<ASActivity, ASActivityEntity, ASObj
static string IASModel<ASActivity>.ASTypeName => ActivityType;

/// <inheritdoc />
public ASActivity() => Entity = TypeMap.Extend<ASActivityEntity>();
public ASActivity() => Entity = TypeMap.Extend<ASActivity, ASActivityEntity>();

/// <inheritdoc />
public ASActivity(TypeMap typeMap, bool isExtending = true) : base(typeMap, false)
=> Entity = TypeMap.ProjectTo<ASActivityEntity>(isExtending);
=> Entity = TypeMap.ProjectTo<ASActivity, ASActivityEntity>(isExtending);

/// <inheritdoc />
public ASActivity(ASType existingGraph) : this(existingGraph.TypeMap) {}

/// <inheritdoc />
[SetsRequiredMembers]
public ASActivity(TypeMap typeMap, ASActivityEntity? entity) : base(typeMap, null)
=> Entity = entity ?? typeMap.AsEntity<ASActivityEntity>();
=> Entity = entity ?? typeMap.AsEntity<ASActivity, ASActivityEntity>();

static ASActivity IASModel<ASActivity>.FromGraph(TypeMap typeMap) => new(typeMap, null);

Expand Down
6 changes: 3 additions & 3 deletions Source/ActivityPub.Types/AS/ASIntransitiveActivity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,19 @@ public class ASIntransitiveActivity : ASActivity, IASModel<ASIntransitiveActivit
static string IASModel<ASIntransitiveActivity>.ASTypeName => IntransitiveActivityType;

/// <inheritdoc />
public ASIntransitiveActivity() => Entity = TypeMap.Extend<ASIntransitiveActivityEntity>();
public ASIntransitiveActivity() => Entity = TypeMap.Extend<ASIntransitiveActivity, ASIntransitiveActivityEntity>();

/// <inheritdoc />
public ASIntransitiveActivity(TypeMap typeMap, bool isExtending = true) : base(typeMap, false)
=> Entity = TypeMap.ProjectTo<ASIntransitiveActivityEntity>(isExtending);
=> Entity = TypeMap.ProjectTo<ASIntransitiveActivity, ASIntransitiveActivityEntity>(isExtending);

/// <inheritdoc />
public ASIntransitiveActivity(ASType existingGraph) : this(existingGraph.TypeMap) {}

/// <inheritdoc />
[SetsRequiredMembers]
public ASIntransitiveActivity(TypeMap typeMap, ASIntransitiveActivityEntity? entity) : base(typeMap, null)
=> Entity = entity ?? typeMap.AsEntity<ASIntransitiveActivityEntity>();
=> Entity = entity ?? typeMap.AsEntity<ASIntransitiveActivity, ASIntransitiveActivityEntity>();

static ASIntransitiveActivity IASModel<ASIntransitiveActivity>.FromGraph(TypeMap typeMap) => new(typeMap, null);

Expand Down
6 changes: 3 additions & 3 deletions Source/ActivityPub.Types/AS/ASLink.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,11 @@ public class ASLink : ASType, IASModel<ASLink, ASLinkEntity, ASType>
static string IASModel<ASLink>.ASTypeName => LinkType;

/// <inheritdoc />
public ASLink() => Entity = TypeMap.Extend<ASLinkEntity>();
public ASLink() => Entity = TypeMap.Extend<ASLink, ASLinkEntity>();

/// <inheritdoc />
public ASLink(TypeMap typeMap, bool isExtending = true) : base(typeMap, false)
=> Entity = TypeMap.ProjectTo<ASLinkEntity>(isExtending);
=> Entity = TypeMap.ProjectTo<ASLink, ASLinkEntity>(isExtending);

/// <inheritdoc />
public ASLink(ASType existingGraph) : this(existingGraph.TypeMap) {}
Expand All @@ -39,7 +39,7 @@ public ASLink(ASType existingGraph) : this(existingGraph.TypeMap) {}
[SetsRequiredMembers]
public ASLink(TypeMap typeMap, ASLinkEntity? entity) : base(typeMap, null)
{
Entity = entity ?? typeMap.AsEntity<ASLinkEntity>();
Entity = entity ?? typeMap.AsEntity<ASLink, ASLinkEntity>();
HRef = Entity.HRef ?? throw new ArgumentException($"The provided entity is invalid - required {nameof(ASLinkEntity.HRef)} property is missing");
}

Expand Down
59 changes: 27 additions & 32 deletions Source/ActivityPub.Types/AS/ASObject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,9 @@
// 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.AS.Collection;
using ActivityPub.Types.AS.Extended.Object;
using ActivityPub.Types.Conversion.Overrides;
using ActivityPub.Types.Util;
using JetBrains.Annotations;

Expand All @@ -28,22 +25,22 @@ public class ASObject : ASType, IASModel<ASObject, ASObjectEntity, ASType>
static string IASModel<ASObject>.ASTypeName => ObjectType;

/// <inheritdoc />
public ASObject() => Entity = TypeMap.Extend<ASObjectEntity>();
public ASObject() => Entity = TypeMap.Extend<ASObject, ASObjectEntity>();

/// <inheritdoc />
public ASObject(TypeMap typeMap, bool isExtending = true) : base(typeMap, false)
=> Entity = TypeMap.ProjectTo<ASObjectEntity>(isExtending);
=> Entity = TypeMap.ProjectTo<ASObject, ASObjectEntity>(isExtending);

/// <summary>
/// Constructs a new instance and extends an existing type graph from a provided model.
/// </summary>
/// <seealso cref="TypeMap.Extend{TEntity}()" />
/// <seealso cref="TypeMap.Extend{TModel, TEntity}()" />
public ASObject(ASType existingGraph) : this(existingGraph.TypeMap) {}

/// <inheritdoc />
[SetsRequiredMembers]
public ASObject(TypeMap typeMap, ASObjectEntity? entity) : base(typeMap, null)
=> Entity = entity ?? typeMap.AsEntity<ASObjectEntity>();
=> Entity = entity ?? typeMap.AsEntity<ASObject, ASObjectEntity>();

static ASObject IASModel<ASObject>.FromGraph(TypeMap typeMap) => new(typeMap, null);

Expand Down Expand Up @@ -222,8 +219,8 @@ public List<ASLink> Url
/// <seealso href="https://www.w3.org/TR/activitystreams-vocabulary/#dfn-content" />
public NaturalLanguageString? Content
{
get => Entity.Content;
set => Entity.Content = value;
get => Entity.ContentMap;
set => Entity.ContentMap = value;
}

/// <summary>
Expand Down Expand Up @@ -329,7 +326,7 @@ public Linkable<ASCollection>? Shares
}

/// <inheritdoc cref="ASObject" />
public sealed class ASObjectEntity : ASEntity<ASObject, ASObjectEntity>, ICustomConvertedEntity<ASObjectEntity>
public sealed class ASObjectEntity : ASEntity<ASObject, ASObjectEntity>, IJsonOnDeserialized, IJsonOnSerializing
{
/// <inheritdoc cref="ASObject.Attachment" />
[JsonPropertyName("attachment")]
Expand Down Expand Up @@ -393,7 +390,11 @@ public sealed class ASObjectEntity : ASEntity<ASObject, ASObjectEntity>, ICustom

/// <inheritdoc cref="ASObject.Content" />
[JsonPropertyName("contentMap")]
public NaturalLanguageString? Content { get; set; }
public NaturalLanguageString? ContentMap { get; set; }

/// <inheritdoc cref="ASObject.Content" />
[JsonPropertyName("content")]
public string? Content { get; set; }

/// <inheritdoc cref="ASObject.Duration" />
[JsonPropertyName("duration")]
Expand Down Expand Up @@ -430,31 +431,25 @@ public sealed class ASObjectEntity : ASEntity<ASObject, ASObjectEntity>, ICustom
/// <inheritdoc cref="ASObject.Shares" />
[JsonPropertyName("shares")]
public Linkable<ASCollection>? Shares { get; set; }

static void ICustomConvertedEntity<ASObjectEntity>.PostReadEntity(JsonElement jsonElement, DeserializationMetadata meta, ASObjectEntity entity)

// Sync up "content" and "contentMap" properties
void IJsonOnDeserialized.OnDeserialized()
{
if (!jsonElement.TryGetProperty("content", out var contentProperty))
return;

var content = contentProperty.GetString();
if (content == null)
if (Content == null)
return;

entity.Content ??= new NaturalLanguageString();
entity.Content.DefaultValue = content;
if (ContentMap == null)
ContentMap = Content;
else
ContentMap.DefaultValue = Content;
}

static void ICustomConvertedEntity<ASObjectEntity>.PostWriteEntity(ASObjectEntity entity, SerializationMetadata meta, JsonElement entityJson, JsonObject outputJson)

// Sync up "content" and "contentMap" properties
void IJsonOnSerializing.OnSerializing()
{
if (entity.Content == null)
return;

// Copy Content.DefaultValue to "content"
if (entity.Content.DefaultValue != null)
outputJson["content"] = JsonValue.Create(entity.Content.DefaultValue, meta.JsonNodeOptions);

// Remove "contentMap" if it's empty
if (!entity.Content.LanguageMap.Any())
outputJson.Remove("contentMap");
Content = ContentMap?.DefaultValue;

if (ContentMap?.HasLanguages == false)
ContentMap = null;
}
}
Loading

0 comments on commit 16ddc8f

Please sign in to comment.