diff --git a/app/lib/activitypub/adapter.rb b/app/lib/activitypub/adapter.rb index 99f4d9305..7e0b16c25 100644 --- a/app/lib/activitypub/adapter.rb +++ b/app/lib/activitypub/adapter.rb @@ -1,30 +1,23 @@ # frozen_string_literal: true class ActivityPub::Adapter < ActiveModelSerializers::Adapter::Base - CONTEXT = { - '@context': [ - 'https://www.w3.org/ns/activitystreams', - 'https://w3id.org/security/v1', - - { - 'manuallyApprovesFollowers' => 'as:manuallyApprovesFollowers', - 'sensitive' => 'as:sensitive', - 'movedTo' => { '@id' => 'as:movedTo', '@type' => '@id' }, - 'alsoKnownAs' => { '@id' => 'as:alsoKnownAs', '@type' => '@id' }, - 'Hashtag' => 'as:Hashtag', - 'ostatus' => 'http://ostatus.org#', - 'atomUri' => 'ostatus:atomUri', - 'inReplyToAtomUri' => 'ostatus:inReplyToAtomUri', - 'conversation' => 'ostatus:conversation', - 'toot' => 'http://joinmastodon.org/ns#', - 'Emoji' => 'toot:Emoji', - 'focalPoint' => { '@container' => '@list', '@id' => 'toot:focalPoint' }, - 'featured' => { '@id' => 'toot:featured', '@type' => '@id' }, - 'schema' => 'http://schema.org#', - 'PropertyValue' => 'schema:PropertyValue', - 'value' => 'schema:value', - }, - ], + NAMED_CONTEXT_MAP = { + activitystreams: 'https://www.w3.org/ns/activitystreams', + security: 'https://w3id.org/security/v1', + }.freeze + + CONTEXT_EXTENSION_MAP = { + manually_approves_followers: { 'manuallyApprovesFollowers' => 'as:manuallyApprovesFollowers' }, + sensitive: { 'sensitive' => 'as:sensitive' }, + hashtag: { 'Hashtag' => 'as:Hashtag' }, + moved_to: { 'movedTo' => { '@id' => 'as:movedTo', '@type' => '@id' } }, + also_known_as: { 'alsoKnownAs' => { '@id' => 'as:alsoKnownAs', '@type' => '@id' } }, + emoji: { 'toot' => 'http://joinmastodon.org/ns#', 'Emoji' => 'toot:Emoji' }, + featured: { 'toot' => 'http://joinmastodon.org/ns#', 'featured' => { '@id' => 'toot:featured', '@type' => '@id' } }, + property_value: { 'schema' => 'http://schema.org#', 'PropertyValue' => 'schema:PropertyValue', 'value' => 'schema:value' }, + atom_uri: { 'ostatus' => 'http://ostatus.org#', 'atomUri' => 'ostatus:atomUri' }, + conversation: { 'ostatus' => 'http://ostatus.org#', 'inReplyToAtomUri' => 'ostatus:inReplyToAtomUri', 'conversation' => 'ostatus:conversation' }, + focal_point: { 'toot' => 'http://joinmastodon.org/ns#', 'focalPoint' => { '@container' => '@list', '@id' => 'toot:focalPoint' } }, }.freeze def self.default_key_transform @@ -36,8 +29,36 @@ class ActivityPub::Adapter < ActiveModelSerializers::Adapter::Base end def serializable_hash(options = nil) - options = serialization_options(options) - serialized_hash = ActiveModelSerializers::Adapter::Attributes.new(serializer, instance_options).serializable_hash(options) - CONTEXT.merge(self.class.transform_key_casing!(serialized_hash, instance_options)) + options = serialization_options(options) + serialized_hash = serializer.serializable_hash(options) + serialized_hash = self.class.transform_key_casing!(serialized_hash, instance_options) + + { '@context' => serialized_context }.merge(serialized_hash) + end + + private + + def serialized_context + context_array = [] + + serializer_options = serializer.send(:instance_options) || {} + named_contexts = [:activitystreams] + serializer._named_contexts.keys + serializer_options.fetch(:named_contexts, {}).keys + context_extensions = serializer._context_extensions.keys + serializer_options.fetch(:context_extensions, {}).keys + + named_contexts.each do |key| + context_array << NAMED_CONTEXT_MAP[key] + end + + extensions = context_extensions.each_with_object({}) do |key, h| + h.merge!(CONTEXT_EXTENSION_MAP[key]) + end + + context_array << extensions unless extensions.empty? + + if context_array.size == 1 + context_array.first + else + context_array + end end end diff --git a/app/lib/activitypub/serializer.rb b/app/lib/activitypub/serializer.rb new file mode 100644 index 000000000..07bd8c494 --- /dev/null +++ b/app/lib/activitypub/serializer.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +class ActivityPub::Serializer < ActiveModel::Serializer + with_options instance_writer: false, instance_reader: true do |serializer| + serializer.class_attribute :_named_contexts + serializer.class_attribute :_context_extensions + + self._named_contexts ||= {} + self._context_extensions ||= {} + end + + def self.inherited(base) + super + + base._named_contexts = _named_contexts.dup + base._context_extensions = _context_extensions.dup + end + + def self.context(*named_contexts) + named_contexts.each do |context| + _named_contexts[context] = true + end + end + + def self.context_extensions(*extension_names) + extension_names.each do |extension_name| + _context_extensions[extension_name] = true + end + end +end diff --git a/app/serializers/activitypub/accept_follow_serializer.rb b/app/serializers/activitypub/accept_follow_serializer.rb index 3e23591a5..1c1c6ab73 100644 --- a/app/serializers/activitypub/accept_follow_serializer.rb +++ b/app/serializers/activitypub/accept_follow_serializer.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class ActivityPub::AcceptFollowSerializer < ActiveModel::Serializer +class ActivityPub::AcceptFollowSerializer < ActivityPub::Serializer attributes :id, :type, :actor has_one :object, serializer: ActivityPub::FollowSerializer diff --git a/app/serializers/activitypub/activity_serializer.rb b/app/serializers/activitypub/activity_serializer.rb index c001e28aa..c06d5c87c 100644 --- a/app/serializers/activitypub/activity_serializer.rb +++ b/app/serializers/activitypub/activity_serializer.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class ActivityPub::ActivitySerializer < ActiveModel::Serializer +class ActivityPub::ActivitySerializer < ActivityPub::Serializer attributes :id, :type, :actor, :published, :to, :cc has_one :proper, key: :object, serializer: ActivityPub::NoteSerializer, if: :serialize_object? diff --git a/app/serializers/activitypub/actor_serializer.rb b/app/serializers/activitypub/actor_serializer.rb index 6746c1782..4b982b955 100644 --- a/app/serializers/activitypub/actor_serializer.rb +++ b/app/serializers/activitypub/actor_serializer.rb @@ -1,8 +1,13 @@ # frozen_string_literal: true -class ActivityPub::ActorSerializer < ActiveModel::Serializer +class ActivityPub::ActorSerializer < ActivityPub::Serializer include RoutingHelper + context :security + + context_extensions :manually_approves_followers, :featured, :also_known_as, + :moved_to, :property_value, :hashtag, :emoji + attributes :id, :type, :following, :followers, :inbox, :outbox, :featured, :preferred_username, :name, :summary, @@ -16,7 +21,7 @@ class ActivityPub::ActorSerializer < ActiveModel::Serializer attribute :moved_to, if: :moved? attribute :also_known_as, if: :also_known_as? - class EndpointsSerializer < ActiveModel::Serializer + class EndpointsSerializer < ActivityPub::Serializer include RoutingHelper attributes :shared_inbox @@ -124,7 +129,7 @@ class ActivityPub::ActorSerializer < ActiveModel::Serializer class CustomEmojiSerializer < ActivityPub::EmojiSerializer end - class TagSerializer < ActiveModel::Serializer + class TagSerializer < ActivityPub::Serializer include RoutingHelper attributes :type, :href, :name @@ -142,7 +147,7 @@ class ActivityPub::ActorSerializer < ActiveModel::Serializer end end - class Account::FieldSerializer < ActiveModel::Serializer + class Account::FieldSerializer < ActivityPub::Serializer attributes :type, :name, :value def type diff --git a/app/serializers/activitypub/add_serializer.rb b/app/serializers/activitypub/add_serializer.rb index c0906e8d0..6f5aab17f 100644 --- a/app/serializers/activitypub/add_serializer.rb +++ b/app/serializers/activitypub/add_serializer.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class ActivityPub::AddSerializer < ActiveModel::Serializer +class ActivityPub::AddSerializer < ActivityPub::Serializer include RoutingHelper attributes :type, :actor, :target diff --git a/app/serializers/activitypub/block_serializer.rb b/app/serializers/activitypub/block_serializer.rb index 624ce2fce..e6c69329d 100644 --- a/app/serializers/activitypub/block_serializer.rb +++ b/app/serializers/activitypub/block_serializer.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class ActivityPub::BlockSerializer < ActiveModel::Serializer +class ActivityPub::BlockSerializer < ActivityPub::Serializer attributes :id, :type, :actor attribute :virtual_object, key: :object diff --git a/app/serializers/activitypub/collection_serializer.rb b/app/serializers/activitypub/collection_serializer.rb index b03609957..da1ba735f 100644 --- a/app/serializers/activitypub/collection_serializer.rb +++ b/app/serializers/activitypub/collection_serializer.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class ActivityPub::CollectionSerializer < ActiveModel::Serializer +class ActivityPub::CollectionSerializer < ActivityPub::Serializer def self.serializer_for(model, options) return ActivityPub::NoteSerializer if model.class.name == 'Status' return ActivityPub::CollectionSerializer if model.class.name == 'ActivityPub::CollectionPresenter' diff --git a/app/serializers/activitypub/delete_actor_serializer.rb b/app/serializers/activitypub/delete_actor_serializer.rb index ddf59be97..a6c5e2385 100644 --- a/app/serializers/activitypub/delete_actor_serializer.rb +++ b/app/serializers/activitypub/delete_actor_serializer.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class ActivityPub::DeleteActorSerializer < ActiveModel::Serializer +class ActivityPub::DeleteActorSerializer < ActivityPub::Serializer attributes :id, :type, :actor, :to attribute :virtual_object, key: :object diff --git a/app/serializers/activitypub/delete_serializer.rb b/app/serializers/activitypub/delete_serializer.rb index 5012a8383..a7d5bd469 100644 --- a/app/serializers/activitypub/delete_serializer.rb +++ b/app/serializers/activitypub/delete_serializer.rb @@ -1,7 +1,9 @@ # frozen_string_literal: true -class ActivityPub::DeleteSerializer < ActiveModel::Serializer - class TombstoneSerializer < ActiveModel::Serializer +class ActivityPub::DeleteSerializer < ActivityPub::Serializer + class TombstoneSerializer < ActivityPub::Serializer + context_extensions :atom_uri + attributes :id, :type, :atom_uri def id diff --git a/app/serializers/activitypub/emoji_serializer.rb b/app/serializers/activitypub/emoji_serializer.rb index 7b06b1e5d..4dc38f3ea 100644 --- a/app/serializers/activitypub/emoji_serializer.rb +++ b/app/serializers/activitypub/emoji_serializer.rb @@ -1,8 +1,10 @@ # frozen_string_literal: true -class ActivityPub::EmojiSerializer < ActiveModel::Serializer +class ActivityPub::EmojiSerializer < ActivityPub::Serializer include RoutingHelper + context_extensions :emoji + attributes :id, :type, :name, :updated has_one :icon, serializer: ActivityPub::ImageSerializer diff --git a/app/serializers/activitypub/flag_serializer.rb b/app/serializers/activitypub/flag_serializer.rb index 1e7a46dd9..2f2a707d3 100644 --- a/app/serializers/activitypub/flag_serializer.rb +++ b/app/serializers/activitypub/flag_serializer.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class ActivityPub::FlagSerializer < ActiveModel::Serializer +class ActivityPub::FlagSerializer < ActivityPub::Serializer attributes :id, :type, :actor, :content attribute :virtual_object, key: :object diff --git a/app/serializers/activitypub/follow_serializer.rb b/app/serializers/activitypub/follow_serializer.rb index bb204ee8f..9228d7716 100644 --- a/app/serializers/activitypub/follow_serializer.rb +++ b/app/serializers/activitypub/follow_serializer.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class ActivityPub::FollowSerializer < ActiveModel::Serializer +class ActivityPub::FollowSerializer < ActivityPub::Serializer attributes :id, :type, :actor attribute :virtual_object, key: :object diff --git a/app/serializers/activitypub/image_serializer.rb b/app/serializers/activitypub/image_serializer.rb index 3c08f77e8..1060f9691 100644 --- a/app/serializers/activitypub/image_serializer.rb +++ b/app/serializers/activitypub/image_serializer.rb @@ -1,8 +1,10 @@ # frozen_string_literal: true -class ActivityPub::ImageSerializer < ActiveModel::Serializer +class ActivityPub::ImageSerializer < ActivityPub::Serializer include RoutingHelper + context_extensions :focal_point + attributes :type, :media_type, :url attribute :focal_point, if: :focal_point? diff --git a/app/serializers/activitypub/like_serializer.rb b/app/serializers/activitypub/like_serializer.rb index c1a7ff6f6..0f170ddb4 100644 --- a/app/serializers/activitypub/like_serializer.rb +++ b/app/serializers/activitypub/like_serializer.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class ActivityPub::LikeSerializer < ActiveModel::Serializer +class ActivityPub::LikeSerializer < ActivityPub::Serializer attributes :id, :type, :actor attribute :virtual_object, key: :object diff --git a/app/serializers/activitypub/note_serializer.rb b/app/serializers/activitypub/note_serializer.rb index 553f333d8..0666bea5a 100644 --- a/app/serializers/activitypub/note_serializer.rb +++ b/app/serializers/activitypub/note_serializer.rb @@ -1,6 +1,9 @@ # frozen_string_literal: true -class ActivityPub::NoteSerializer < ActiveModel::Serializer +class ActivityPub::NoteSerializer < ActivityPub::Serializer + context_extensions :atom_uri, :conversation, :sensitive, + :hashtag, :emoji, :focal_point + attributes :id, :type, :summary, :in_reply_to, :published, :url, :attributed_to, :to, :cc, :sensitive, @@ -147,7 +150,7 @@ class ActivityPub::NoteSerializer < ActiveModel::Serializer object.poll&.expired? end - class MediaAttachmentSerializer < ActiveModel::Serializer + class MediaAttachmentSerializer < ActivityPub::Serializer include RoutingHelper attributes :type, :media_type, :url, :name @@ -178,7 +181,7 @@ class ActivityPub::NoteSerializer < ActiveModel::Serializer end end - class MentionSerializer < ActiveModel::Serializer + class MentionSerializer < ActivityPub::Serializer attributes :type, :href, :name def type @@ -194,7 +197,7 @@ class ActivityPub::NoteSerializer < ActiveModel::Serializer end end - class TagSerializer < ActiveModel::Serializer + class TagSerializer < ActivityPub::Serializer include RoutingHelper attributes :type, :href, :name @@ -215,8 +218,8 @@ class ActivityPub::NoteSerializer < ActiveModel::Serializer class CustomEmojiSerializer < ActivityPub::EmojiSerializer end - class OptionSerializer < ActiveModel::Serializer - class RepliesSerializer < ActiveModel::Serializer + class OptionSerializer < ActivityPub::Serializer + class RepliesSerializer < ActivityPub::Serializer attributes :type, :total_items def type diff --git a/app/serializers/activitypub/public_key_serializer.rb b/app/serializers/activitypub/public_key_serializer.rb index 38e9e93ba..62ed49e81 100644 --- a/app/serializers/activitypub/public_key_serializer.rb +++ b/app/serializers/activitypub/public_key_serializer.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true -class ActivityPub::PublicKeySerializer < ActiveModel::Serializer +class ActivityPub::PublicKeySerializer < ActivityPub::Serializer + context :security + attributes :id, :owner, :public_key_pem def id diff --git a/app/serializers/activitypub/reject_follow_serializer.rb b/app/serializers/activitypub/reject_follow_serializer.rb index 7814f4f57..4996c9a3c 100644 --- a/app/serializers/activitypub/reject_follow_serializer.rb +++ b/app/serializers/activitypub/reject_follow_serializer.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class ActivityPub::RejectFollowSerializer < ActiveModel::Serializer +class ActivityPub::RejectFollowSerializer < ActivityPub::Serializer attributes :id, :type, :actor has_one :object, serializer: ActivityPub::FollowSerializer diff --git a/app/serializers/activitypub/remove_serializer.rb b/app/serializers/activitypub/remove_serializer.rb index c2a5ae1b3..7fefda59d 100644 --- a/app/serializers/activitypub/remove_serializer.rb +++ b/app/serializers/activitypub/remove_serializer.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class ActivityPub::RemoveSerializer < ActiveModel::Serializer +class ActivityPub::RemoveSerializer < ActivityPub::Serializer include RoutingHelper attributes :type, :actor, :target diff --git a/app/serializers/activitypub/undo_announce_serializer.rb b/app/serializers/activitypub/undo_announce_serializer.rb index 4fc042727..6758af679 100644 --- a/app/serializers/activitypub/undo_announce_serializer.rb +++ b/app/serializers/activitypub/undo_announce_serializer.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class ActivityPub::UndoAnnounceSerializer < ActiveModel::Serializer +class ActivityPub::UndoAnnounceSerializer < ActivityPub::Serializer attributes :id, :type, :actor, :to has_one :object, serializer: ActivityPub::ActivitySerializer diff --git a/app/serializers/activitypub/undo_block_serializer.rb b/app/serializers/activitypub/undo_block_serializer.rb index 2f43d8402..b4f049377 100644 --- a/app/serializers/activitypub/undo_block_serializer.rb +++ b/app/serializers/activitypub/undo_block_serializer.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class ActivityPub::UndoBlockSerializer < ActiveModel::Serializer +class ActivityPub::UndoBlockSerializer < ActivityPub::Serializer attributes :id, :type, :actor has_one :object, serializer: ActivityPub::BlockSerializer diff --git a/app/serializers/activitypub/undo_follow_serializer.rb b/app/serializers/activitypub/undo_follow_serializer.rb index e5b7f143d..9b3e0ca3c 100644 --- a/app/serializers/activitypub/undo_follow_serializer.rb +++ b/app/serializers/activitypub/undo_follow_serializer.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class ActivityPub::UndoFollowSerializer < ActiveModel::Serializer +class ActivityPub::UndoFollowSerializer < ActivityPub::Serializer attributes :id, :type, :actor has_one :object, serializer: ActivityPub::FollowSerializer diff --git a/app/serializers/activitypub/undo_like_serializer.rb b/app/serializers/activitypub/undo_like_serializer.rb index 25f4ccaae..20c786cb7 100644 --- a/app/serializers/activitypub/undo_like_serializer.rb +++ b/app/serializers/activitypub/undo_like_serializer.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class ActivityPub::UndoLikeSerializer < ActiveModel::Serializer +class ActivityPub::UndoLikeSerializer < ActivityPub::Serializer attributes :id, :type, :actor has_one :object, serializer: ActivityPub::LikeSerializer diff --git a/app/serializers/activitypub/update_poll_serializer.rb b/app/serializers/activitypub/update_poll_serializer.rb index f7933346f..a9a09747f 100644 --- a/app/serializers/activitypub/update_poll_serializer.rb +++ b/app/serializers/activitypub/update_poll_serializer.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class ActivityPub::UpdatePollSerializer < ActiveModel::Serializer +class ActivityPub::UpdatePollSerializer < ActivityPub::Serializer attributes :id, :type, :actor, :to has_one :object, serializer: ActivityPub::NoteSerializer diff --git a/app/serializers/activitypub/update_serializer.rb b/app/serializers/activitypub/update_serializer.rb index 48d7a1929..a5eb857d3 100644 --- a/app/serializers/activitypub/update_serializer.rb +++ b/app/serializers/activitypub/update_serializer.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class ActivityPub::UpdateSerializer < ActiveModel::Serializer +class ActivityPub::UpdateSerializer < ActivityPub::Serializer attributes :id, :type, :actor, :to has_one :object, serializer: ActivityPub::ActorSerializer diff --git a/app/serializers/activitypub/vote_serializer.rb b/app/serializers/activitypub/vote_serializer.rb index 248190404..71ef5c77e 100644 --- a/app/serializers/activitypub/vote_serializer.rb +++ b/app/serializers/activitypub/vote_serializer.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true -class ActivityPub::VoteSerializer < ActiveModel::Serializer - class NoteSerializer < ActiveModel::Serializer +class ActivityPub::VoteSerializer < ActivityPub::Serializer + class NoteSerializer < ActivityPub::Serializer attributes :id, :type, :name, :attributed_to, :in_reply_to, :to diff --git a/config/initializers/active_model_serializers.rb b/config/initializers/active_model_serializers.rb index 0e69e1d96..329a5fb2c 100644 --- a/config/initializers/active_model_serializers.rb +++ b/config/initializers/active_model_serializers.rb @@ -3,3 +3,22 @@ ActiveModelSerializers.config.tap do |config| end ActiveSupport::Notifications.unsubscribe(ActiveModelSerializers::Logging::RENDER_EVENT) + +class ActiveModel::Serializer::Reflection + # We monkey-patch this method so that when we include associations in a serializer, + # the nested serializers can send information about used contexts upwards back to + # the root. We do this via instance_options because the nesting can be dynamic. + def build_association(parent_serializer, parent_serializer_options, include_slice = {}) + serializer = options[:serializer] + + parent_serializer_options.merge!(named_contexts: serializer._named_contexts, context_extensions: serializer._context_extensions) if serializer.respond_to?(:_named_contexts) + + association_options = { + parent_serializer: parent_serializer, + parent_serializer_options: parent_serializer_options, + include_slice: include_slice, + } + + ActiveModel::Serializer::Association.new(self, association_options) + end +end diff --git a/spec/lib/activitypub/adapter_spec.rb b/spec/lib/activitypub/adapter_spec.rb new file mode 100644 index 000000000..ea03797aa --- /dev/null +++ b/spec/lib/activitypub/adapter_spec.rb @@ -0,0 +1,88 @@ +require 'rails_helper' + +RSpec.describe ActivityPub::Adapter do + class TestObject < ActiveModelSerializers::Model + attributes :foo + end + + class TestWithBasicContextSerializer < ActivityPub::Serializer + attributes :foo + end + + class TestWithNamedContextSerializer < ActivityPub::Serializer + context :security + attributes :foo + end + + class TestWithNestedNamedContextSerializer < ActivityPub::Serializer + attributes :foo + + has_one :virtual_object, key: :baz, serializer: TestWithNamedContextSerializer + + def virtual_object + object + end + end + + class TestWithContextExtensionSerializer < ActivityPub::Serializer + context_extensions :sensitive + attributes :foo + end + + class TestWithNestedContextExtensionSerializer < ActivityPub::Serializer + context_extensions :manually_approves_followers + attributes :foo + + has_one :virtual_object, key: :baz, serializer: TestWithContextExtensionSerializer + + def virtual_object + object + end + end + + describe '#serializable_hash' do + let(:serializer_class) {} + + subject { ActiveModelSerializers::SerializableResource.new(TestObject.new(foo: 'bar'), serializer: serializer_class, adapter: described_class).as_json } + + context 'when serializer defines no context' do + let(:serializer_class) { TestWithBasicContextSerializer } + + it 'renders a basic @context' do + expect(subject).to include({ '@context' => 'https://www.w3.org/ns/activitystreams' }) + end + end + + context 'when serializer defines a named context' do + let(:serializer_class) { TestWithNamedContextSerializer } + + it 'renders a @context with both items' do + expect(subject).to include({ '@context' => ['https://www.w3.org/ns/activitystreams', 'https://w3id.org/security/v1'] }) + end + end + + context 'when serializer has children that define a named context' do + let(:serializer_class) { TestWithNestedNamedContextSerializer } + + it 'renders a @context with both items' do + expect(subject).to include({ '@context' => ['https://www.w3.org/ns/activitystreams', 'https://w3id.org/security/v1'] }) + end + end + + context 'when serializer defines context extensions' do + let(:serializer_class) { TestWithContextExtensionSerializer } + + it 'renders a @context with the extension' do + expect(subject).to include({ '@context' => ['https://www.w3.org/ns/activitystreams', { 'sensitive' => 'as:sensitive' }] }) + end + end + + context 'when serializer has children that define context extensions' do + let(:serializer_class) { TestWithNestedContextExtensionSerializer } + + it 'renders a @context with both extensions' do + expect(subject).to include({ '@context' => ['https://www.w3.org/ns/activitystreams', { 'manuallyApprovesFollowers' => 'as:manuallyApprovesFollowers', 'sensitive' => 'as:sensitive' }] }) + end + end + end +end