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: explore possibility of non-object-oriented type model #74

Closed warriordog closed 1 year ago

warriordog commented 1 year ago

This would be a drastic and extreme change that impacts the entire project, but there are valid reasons to explore is.

I guess while we're on the topic of high-level refactors, we could also consider dropping the object-oriented model entirely and using composition. Instead of "extends", we "link to". That would really help for edge cases like OrderedCollectionPage, which is meant to be both a CollectionPage and an OrderedCollection.

The downside is that since we don't have inheritance, subtypes can't override behavior. We would also have to hack in the synthetic types, like ASActor.

Originally posted by @warriordog in https://github.com/warriordog/ActivityPubSharp/issues/73#issuecomment-1656786675

This change, if we decide to make it, will drop the object-oriented structure of the ActivityStreams types in favor of a more compositional structure. Types would contain only their unique properties and a link to their own "base" type and the root ASType / ASObject instance(s). We would, effectively, create a small graph out of each "object". The "root" (ASType, most likely) will additionally have functions to find and access any "leaf" in the graph. This allows type-safe navigation from any object through its base type hierarchy, and also safe navigation from the root to any standard or extension type associated with the object.

For this to work we will still need TypeMap for extension support, but it can be attached to ASType and continue to work as-designed. In fact, the implementation of TypeMap will be much simpler with this change.

Constructing an object without JSON:

warriordog commented 1 year ago

if this option is taken, it will become a prerequisite for the other work in the project.

warriordog commented 1 year ago

I'm going to prototype this in the linked branch. A bit of unimportant tracking:

Features to re-implement later:

warriordog commented 1 year ago

Idea - what if TypeMap becomes the object root, instead of ASType? It would become the central node on the type graph and would make a convenient type for "any valid ActivityPub type". We should probably rename it then.

The alternative is to implement a common ASBase or something that contains no properties (since it wont be managed by the graph) but does contain the TypeMap pointer.

warriordog commented 1 year ago

Found a way to make the type graph more ergonomic and consistent with C# principles. Put simply, we implement both a type graph and an inheritance chain. The inheritance chain becomes the normal public API, and should be drop-in compatible with the existing type model. However, those types will contain no data at all. Instead, they each wrap an entity class, which is a singleton node from the type graph. The entity classes contain all the data, along with serialization logic. Thus, multiple objects with common base types can exist, while the shared base types still act like singletons.

Here's an example diagram (sorry, its hard to read): example diagram

JSON / JSON-LD conversion will now consist of two steps:

  1. Construct a type graph from the input JSON. This will require a rebuild of ASTypeConverter, but shouldn't be too hard. I believe we can create a new TypeMap then construct each entity from the same JsonElement. Since the entity constructor automatically attaches to the TypeMap, everything will fit into place as we go.
  2. Construct an inheritance chain for the requested target type. There can only be one target type due to the design of System.Text.Json, so anything after that will require explicit calls from upstream code. The type constructors are designed to accept and pass down a TypeMap instance, and each intermediate constructor will extract the appropriate entity.
warriordog commented 1 year ago

Research Results

Summary:

Overall, this seems to be a fully viable solution to the problem!

Comparison:

Pros:

Cons:

Neutrals:

Next Steps:

  1. 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.
  2. Research - design new TypeInfo cache. Ideally, we can make the entire conversion process fully generic like JsonPropertyInfo<T>. That opens up the possibility of using Static Virtual Interface Members to avoid some reflection.
  3. Implement new type cache and JSON converters
  4. Allow entities to define a custom JSON-LD context (no support for expanded form yet, but that should be a separate story)
  5. Fix unit tests
  6. Revisit API (see if we can simplify / cleanup / encapsulate anything)
  7. New unit tests for new functionality
  8. Expose AS type name constant on the inheritance class, for convenience. Currently it is only on the entity.
  9. Fix and resolve any bugs