From 2989ab8a2210c5aaebe7a7fc6cb305c4777ac707 Mon Sep 17 00:00:00 2001 From: Kurtis Rainbolt-Greene Date: Sat, 28 Jul 2018 01:26:18 -0700 Subject: [PATCH 1/3] Detailed guide on how to use context and integrate policies Also first introduction to if: --- README.md | 116 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) diff --git a/README.md b/README.md index 10bfd01..454e331 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ This library is up-to-date with the finalized v1 JSON API spec. * [Define a serializer](#define-a-serializer) * [Serialize an object](#serialize-an-object) * [Serialize a collection](#serialize-a-collection) + * [Context (and policies)](#context-and-policies) * [Null handling](#null-handling) * [Multiple attributes](#multiple-attributes) * [Custom attributes](#custom-attributes) @@ -131,6 +132,121 @@ Returns: You must always pass `is_collection: true` when serializing a collection, see [Null handling](#null-handling). +### Context (and policies) + +Sometimes you need to give a serializer instance some extra information that will possibly be different on each instantiation. A good example of this is policies (objects that determine if something can happen): + +``` ruby +class PostPolicy + attr_reader :actor + attr_reader :record + + def initialize(actor, record) + @actor = actor + @post = record + end + + def read_attribute_title? + actor.admin? or not record.published? + end + + def read_related_comments? + actor.present? + end +end +``` + +This policy will let us know if we can see the title of a post and what associated comments we can see. Now we just pass that policy as a context property: + +``` ruby +JSONAPI::Serializer.serialize(post, context: {policy: PostPolicy.new(current_user, post)}) +``` + +And we can access this inside the anonymous functions for each property: + +``` ruby +class PostSerializer + include JSONAPI::Serializer + + attribute :title, if: -> { context.policy.read_attribute_title? } + attribute :content + + has_many :comments, if: -> { context.policy.read_related_comments? } +end +``` + +This of course doesn't scale well so lets create some helpers: + +``` ruby +class ApplicationSerializer + include JSONAPI::Serializer + + private_class_method def self.policy_allows(name, type) + define_method("allow_#{type}_#{name}?") + if context.key?(:policy) + context.fetch(:policy).public_send("read_#{type}?", name) + else + raise MissingContextPolicyError + end + end + end + + private_class_method def self.policy_allows_attribute?(name) + police(name, :attribute) + end + + private_class_method def self.policy_allows_relation?(name) + police(name, :related) + end + + private_class_method def self.policy_scoped(association) + if context.key?(:policy) + context.fetch(:policy).public_send("related_#{association}", if block_given? then yield end) + else + raise MissingContextPolicyError + end + end +end +``` + +And using those helpers: + +``` ruby +class PostSerializer + include JSONAPI::Serializer + + attribute :title, if: policy_allows_attribute?(:title) + attribute :content + + has_many :comments, if: policy_allows_related(:comments), &policy_scoped(:comments) +end +``` + +``` ruby +class PostPolicy + attr_reader :actor + attr_reader :record + + def initialize(actor, record) + @actor = actor + @post = record + end + + def read_attribute_title? + actor.admin? or not record.published? + end + + def read_related_comments? + actor.present? + end + + def related_comments(relation = nil) + CommentPolicy::Scope.new(requester, relation || record.comments).resolve + end +end +``` + + ### Null handling ```ruby From 90d4f8204e62d3ec10e8392a474cea32c627e45f Mon Sep 17 00:00:00 2001 From: Kurtis Rainbolt-Greene Date: Sat, 28 Jul 2018 01:27:41 -0700 Subject: [PATCH 2/3] Fixing syntax --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 454e331..405f4e8 100644 --- a/README.md +++ b/README.md @@ -168,10 +168,10 @@ And we can access this inside the anonymous functions for each property: class PostSerializer include JSONAPI::Serializer - attribute :title, if: -> { context.policy.read_attribute_title? } + attribute :title, if: -> { context.fetch(:policy).read_attribute_title? } attribute :content - has_many :comments, if: -> { context.policy.read_related_comments? } + has_many :comments, if: -> { context.fetch(:policy).read_related_comments? } end ``` From 809160f3adc10ea3074c61d6a4c49484b0a20f5e Mon Sep 17 00:00:00 2001 From: Kurtis Rainbolt-Greene Date: Mon, 6 Aug 2018 20:33:59 -0700 Subject: [PATCH 3/3] Fixing internal api --- README.md | 56 ++++++++++++++++++++++++++++--------------------------- 1 file changed, 29 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 405f4e8..9ad6b13 100644 --- a/README.md +++ b/README.md @@ -181,31 +181,33 @@ This of course doesn't scale well so lets create some helpers: class ApplicationSerializer include JSONAPI::Serializer - private_class_method def self.policy_allows(name, type) - define_method("allow_#{type}_#{name}?") - if context.key?(:policy) - context.fetch(:policy).public_send("read_#{type}?", name) - else - raise MissingContextPolicyError - end - end - end - - private_class_method def self.policy_allows_attribute?(name) - police(name, :attribute) - end - - private_class_method def self.policy_allows_relation?(name) - police(name, :related) - end - - private_class_method def self.policy_scoped(association) - if context.key?(:policy) - context.fetch(:policy).public_send("related_#{association}", if block_given? then yield end) - else - raise MissingContextPolicyError - end - end + private_class_method def self.policy_allows(name, type) + define_method("allow_#{type}_#{name}?") do + if context.key?(:policy) + context.fetch(:policy).public_send("read_#{type}?", name) + else + raise MissingContextPolicyError + end + end + end + + private_class_method def self.policy_allows_attribute?(name) + policy_allows(name, :attribute) + end + + private_class_method def self.policy_allows_relation?(name) + policy_allows(name, :related) + end + + private_class_method def self.policy_scoped(association) + ->(_) do + if context.key?(:policy) + context.fetch(:policy).public_send("related_#{association}", if block_given? then yield end) + else + raise MissingContextPolicyError + end + end + end end ``` @@ -215,10 +217,10 @@ And using those helpers: class PostSerializer include JSONAPI::Serializer - attribute :title, if: policy_allows_attribute?(:title) + attribute :title, if: policy_allows_attribute??(:title) attribute :content - has_many :comments, if: policy_allows_related(:comments), &policy_scoped(:comments) + has_many :comments, if: policy_allows_relation?(:comments), &policy_scoped(:comments) end ```