warriordog / ActivityPubSharp

Modular implementation of ActivityPub in C#
https://warriordog.github.io/ActivityPubSharp/
Mozilla Public License 2.0
46 stars 10 forks source link

Research - design new custom serializer callbacks #77

Closed warriordog closed 1 year ago

warriordog commented 1 year ago

Research - design new CustomJson(De)Serializer callback. We need to pass the TypeMap through to custom logic, and/or create a new callback to override the type mapping.

More info - https://github.com/warriordog/ActivityPubSharp/issues/74#issuecomment-1660685784

warriordog commented 1 year ago

Proposed new API for TrySerializeDelegate / TryDeserializeDelegate. This introduces a metadata object to allow backwards-compatible changes in the future. This loses support for custom conversion into a non-object type. That will need to be a new attribute.


/// <summary>
/// Context for a particular JSON conversion operation.
/// Singleton - created once and reused for entire graph.
/// </summary>
public class ConversionMetadata
{

    /// <summary>
    /// JSON serializer options in use for the conversion.
    /// MUST be passed on - do not assume default values!
    /// </summary>
    public required JsonSerializerOptions JsonSerializerOptions { get; init; }
}

/// <summary>
/// Context for a particular JSON serialization operation.
/// Singleton - created once and reused for entire graph.
/// </summary>
public class SerializationMetadata : ConversionMetadata
{
    /// <summary>
    /// JSON node options in use for the conversion.
    /// MUST be passed on - do not assume default values!
    /// </summary>
    public required JsonNodeOptions JsonNodeOptions { get; init; }
}

/// <summary>
/// Context for a particular JSON deserialization operation.
/// Singleton - created once and reused for entire graph.
/// </summary>
public class DeserializationMetadata : ConversionMetadata
{
    /// <summary>
    /// TypeMap of the object being converted.
    /// </summary>
    public required TypeMap TypeMap { get; init; }

    /// <summary>
    /// JSON-LD context in effect for this conversion.
    /// Will always be present, even if not included in the JSON.
    /// </summary>
    public required JsonLDContext LDContext { get; init; }
}

/// <summary>
/// Serialize the type into JSON.
/// </summary>
/// <param name="obj">Object to convert</param>
/// <param name="meta">Context for the conversion</param>
/// <param name="node">Node to write values into</param>
/// <returns>Return true on success, or false to fall back on default logic.</returns>
/// <typeparam name="T">Type of object to convert. Must derive from <see cref="ASBase"/>.</typeparam>
public delegate bool TrySerializeDelegate<in T>(T obj, SerializationMetadata meta, JsonObject node)
    where T : ASBase;

/// <summary>
/// Deserialize the type from JSON.
/// </summary>
/// <param name="element">Element containing JSON data for this object</param>
/// <param name="meta">Context for the conversion</param>
/// <param name="obj">Object constructed by the converter</param>
/// <returns>Return true on success, or false to fall back on default logic.</returns>
/// <typeparam name="T">Type of object to convert. Must derive from <see cref="ASBase"/>.</typeparam>
public delegate bool TryDeserializeDelegate<T>(JsonElement element, DeserializationMetadata meta, out T? obj)
    where T : ASBase;
warriordog commented 1 year ago

Proposed NEW attribute to remap a type on deserialization:

/// <summary>
/// Selects a more narrow type to convert instead of the containing type.
/// This will be called on deserialization.
/// The returned type MUST be or derive from the containing type.
/// </summary>
/// <param name="element">Element containing JSON data for this object</param>
/// <param name="meta">Context for the conversion</param>
/// <returns>Type of object to convert</returns>
public delegate Type NarrowTypeDelegate(JsonElement element, DeserializationMetadata meta);

/// <summary>
/// Indicates that the target method should be called to narrow the type of this object before deserialization.
/// Only valid on subtypes of <see cref="ASBase"/>.
/// </summary>
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
[MeansImplicitUse]
public sealed class NarrowJsonTypeAttribute : Attribute
{
    /// <summary>
    /// Name of the method that will narrow the type of this object
    /// Must be public, static, and conform to the signature of <see cref="NarrowTypeDelegate"/>.
    /// </summary>
    public string MethodName { get; }

    public NarrowJsonTypeAttribute(string methodName) => MethodName = methodName;
}
warriordog commented 1 year ago

Proposed NEW attribute to special-convert the entire graph into a JSON value type:

/// <summary>
/// Serialize the type into a JSON value.
/// This will supersede all conversion for the ENTIRE object graph, so use it carefully!
/// </summary>
/// <param name="obj">Object to convert</param>
/// <param name="meta">Context for the conversion</param>
/// <param name="node">Node to use as value for the object</param>
/// <returns>Return true on success, or false to fall back on default logic.</returns>
/// <typeparam name="T">Type of object to convert. Must derive from <see cref="ASBase"/>.</typeparam>
public delegate bool TrySerializeIntoValueDelegate<in T>(T obj, SerializationMetadata meta, [NotNullWhen(true)] out JsonValue? node)
    where T : ASBase;

/// <summary>
/// Indicates that the target method should be called to serialize this type into a non-object value.
/// Only valid on subtypes of <see cref="ASBase"/>.
/// </summary>
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
[MeansImplicitUse]
public sealed class CustomJsonValueSerializerAttribute : Attribute
{
    /// <summary>
    /// Name of the method that can serialize this type.
    /// Must be public, static, and conform to the signature of <see cref="TrySerializeIntoValueDelegate{T}"/> where T is substituted for the type.
    /// </summary>
    public string MethodName { get; }

    public CustomJsonValueSerializerAttribute(string methodName) => MethodName = methodName;
}