Skip to content

Commit

Permalink
Merge pull request #5 from art19/tatthurs/merge-art19-patch
Browse files Browse the repository at this point in the history
Merge art19-patched and add instructions for gem publishing
  • Loading branch information
tatthurs authored Jun 14, 2024
2 parents abd5960 + b165593 commit 1e966e7
Show file tree
Hide file tree
Showing 10 changed files with 362 additions and 1 deletion.
76 changes: 76 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,17 @@ This will create a `self` reference for the relationship, and a `related` link f
}
```

Relationship links can also be configured to be defined as a callable.

```ruby
has_many :actors, links: -> (object, params) {
{
self: "https://movies.com/#{object.id}/relationships/actors",
next: "https://movies.com/#{object.id}/relationships/actors?page%5Bnumber%5D=2&page%5Bsize%5D=10"
}
}
```

### Meta Per Resource

For every resource in the collection, you can include a meta object containing non-standard meta-information about a resource that can not be represented as an attribute or relationship.
Expand Down Expand Up @@ -513,6 +524,29 @@ serializer = MovieSerializer.new(movie, { params: { admin: current_user.admin? }
serializer.serializable_hash
```

Sometimes it might be more performant to reduce the number of attributes getting serialized in a single call rather than specifying and executing a conditional Proc for every attribute. For this situation, `attributes_filter` can be used. It accepts both a method name representing a class method on the serializer class or a callable like a Proc. The class method or block provided receives three arguments. The first being the mapping of all attributes defined, the second is the object getting serialized and the last is the parameters passed to the serializer as the `params` option. The return value is then considered as starting point for the attributes to serialize. It will be further reduced by an eventually provided fieldset.

```ruby
class MovieSerializer
include JSONAPI::Serializer

attributes :name, :year, :release_year, :director

attributes_filter do |all_attributes, record, params|
permit = params[:permitted_by_policy]

case permit
when :all, nil
all_attributes
when :none, []
[]
else
all_attributes.slice(*permit)
end
end
end
```

### Conditional Relationships

Conditional relationships can be defined by passing a Proc to the `if` key. Return `true` if the relationship should be serialized, and `false` if not. The record and any params passed to the serializer are available inside the Proc as the first and second parameters, respectively.
Expand All @@ -534,6 +568,30 @@ serializer = MovieSerializer.new(movie, { params: { admin: current_user.admin? }
serializer.serializable_hash
```

Just like with attributes, it might sometimes be more performant to reduce the number of relationships getting serialized in a single call rather than specifying and executing a single conditional Proc for every relationship. For this situation, `relationships_filter` can be used. It accepts both a method name representing a class method on the serializer class or a callable like a Proc. The class method or block provided receives three arguments. The first being the mapping of all relationships defined, the second is the object getting serialized and the last is the parameters passed to the serializer as the `params` option. The return value is then considered as starting point for the relationships to serialize. It will be further reduced by an eventually provided fieldset.

```ruby
class MovieSerializer
include JSONAPI::Serializer

has_many :actors
belongs_to :owner

relationships_filter do |all_relationships, record, params|
permit = params[:permitted_by_policy]

case permit
when :all, nil
all_relationships
when :none, []
[]
else
all_relationships.slice(*permit)
end
end
end
```

### Specifying a Relationship Serializer

In many cases, the relationship can automatically detect the serializer to use.
Expand Down Expand Up @@ -779,3 +837,21 @@ pull request creation processes.
This project is intended to be a safe, welcoming space for collaboration, and
contributors are expected to adhere to the
[Contributor Covenant](https://contributor-covenant.org) code of conduct.

### Publishing

Releases are manual, performed locally on a developer's machine. Gems are published to Github Packages. A comprehesive outline of this process can be found here: https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-rubygems-registry.

1. Increment the `ART19_REVISION` in [lib/jsonapi/serializer/version.rb#L5](https://github.com/art19/jsonapi-serializer/blob/master/lib/jsonapi/serializer/version.rb#L5)

2. Build the gem:

```
gem build jsonapi-serializer.gemspec
```

3. Publish your gem, replacing $VERSION with the gem version. You'll see the generated file after running `gem build` above, eg: 'jsonapi-serializer-2.2.0.1.gem'.

```
gem push --key github --host https://rubygems.pkg.github.com/art19 jsonapi-serializer-$VERSION.gem
```
1 change: 1 addition & 0 deletions jsonapi-serializer.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ require 'jsonapi/serializer/version'
Gem::Specification.new do |gem|
gem.name = 'jsonapi-serializer'
gem.version = JSONAPI::Serializer::VERSION
gem.metadata["allowed_push_host"] = 'https://rubygems.pkg.github.com/art19'

gem.authors = ['JSON:API Serializer Community']
gem.email = ''
Expand Down
38 changes: 38 additions & 0 deletions lib/fast_jsonapi/object_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,9 @@ def is_collection?(resource, force_is_collection = nil)
def inherited(subclass)
super(subclass)
subclass.attributes_to_serialize = attributes_to_serialize.dup if attributes_to_serialize.present?
subclass.attributes_filter_method = attributes_filter_method.dup if attributes_filter_method.present?
subclass.relationships_to_serialize = relationships_to_serialize.dup if relationships_to_serialize.present?
subclass.relationships_filter_method = relationships_filter_method.dup if relationships_filter_method.present?
subclass.cachable_relationships_to_serialize = cachable_relationships_to_serialize.dup if cachable_relationships_to_serialize.present?
subclass.uncachable_relationships_to_serialize = uncachable_relationships_to_serialize.dup if uncachable_relationships_to_serialize.present?
subclass.transform_method = transform_method
Expand Down Expand Up @@ -291,6 +293,42 @@ def create_relationship(base_key, relationship_type, options, block)
)
end

##
# Add an extra layer for attribute filtering to the serializer operating on the full list of defined attributes,
# but before any fieldset is applied. This is a more performant option than providing conditionals to every attribute.
#
# @param filter_method_name [Symbol, nil]
# The name of a class method used to filter the set of attributes. This method will receive the superset of attributes,
# the current record getting serialized and the serializer parameters passed along.
#
# @param block [#call]
# If a block is provided instead of a method name, this is going to be called when building the attributes hash.
# The arguments to the block are the same as for the method: the superset of attributes, the record getting serialized
# and the serializer parameters.
def attributes_filter(filter_method_name = nil, &block)
raise ArgumentError, 'filter_method_name and block are mutually exclusive' if filter_method_name && block

self.attributes_filter_method = filter_method_name || block
end

##
# Add an extra layer for relationship filtering to the serializer operating on the full list of defined relationships,
# but before any fieldset is applied. This is a more performant option than providing conditionals to every relationship.
#
# @param filter_method_name [Symbol, nil]
# The name of a class method used to filter the set of relationships. This method will receive the superset of relationships,
# the current record getting serialized and the serializer parameters passed along.
#
# @param block [#call]
# If a block is provided instead of a method name, this is going to be called when building the relationships hash.
# The arguments to the block are the same as for the method: the superset of attributes, the record getting serialized
# and the serializer parameters.
def relationships_filter(filter_method_name = nil, &block)
raise ArgumentError, 'filter_method_name and block are mutually exclusive' if filter_method_name && block

self.relationships_filter_method = filter_method_name || block
end

def compute_id_method_name(custom_id_method_name, id_method_name_from_relationship, polymorphic, serializer, block)
if block.present? || serializer.is_a?(Proc) || polymorphic
custom_id_method_name || :id
Expand Down
2 changes: 2 additions & 0 deletions lib/fast_jsonapi/relationship.rb
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,8 @@ def fetch_id(record, params)
def add_links_hash(record, params, output_hash)
output_hash[key][:links] = if links.is_a?(Symbol)
record.public_send(links)
elsif links.respond_to?(:call)
FastJsonapi.call_proc(links, record, params)
else
links.each_with_object({}) do |(key, method), hash|
Link.new(key: key, method: method).serialize(record, params, hash)
Expand Down
32 changes: 32 additions & 0 deletions lib/fast_jsonapi/serialization_core.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ module SerializationCore
included do
class << self
attr_accessor :attributes_to_serialize,
:attributes_filter_method,
:relationships_to_serialize,
:relationships_filter_method,
:cachable_relationships_to_serialize,
:uncachable_relationships_to_serialize,
:transform_method,
Expand Down Expand Up @@ -43,6 +45,7 @@ def links_hash(record, params = {})

def attributes_hash(record, fieldset = nil, params = {})
attributes = attributes_to_serialize
attributes = filter_list(attributes_filter_method, attributes, record, params)
attributes = attributes.slice(*fieldset) if fieldset.present?
attributes = {} if fieldset == []

Expand All @@ -51,8 +54,37 @@ def attributes_hash(record, fieldset = nil, params = {})
end
end

##
# Eventually filter a list of attributes or relationships using a configured filter method/block
#
# @param filter [Symbol, #call, nil]
# If a Symbol the name of the filter method to call on the serializer.
# If something callable, the result of that callable.
#
# @param superset [Hash]
# The attributes or relationships to filter
#
# @param record [Object]
# The current record to get serialized
#
# @param params [Hash]
# The params provided to the serializer
#
# @return [Hash]
# The eventually filtered set of attributes or relationships
def filter_list(filter, superset, record, params = {})
return superset if filter.nil?

if filter.respond_to?(:call)
filter.call(superset, record, params)
else
send(filter, superset, record, params)
end
end

def relationships_hash(record, relationships = nil, fieldset = nil, includes_list = nil, params = {})
relationships = relationships_to_serialize if relationships.nil?
relationships = filter_list(relationships_filter_method, relationships, record, params)
relationships = relationships.slice(*fieldset) if fieldset.present?
relationships = {} if fieldset == []

Expand Down
5 changes: 4 additions & 1 deletion lib/jsonapi/serializer/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
module JSONAPI
module Serializer
VERSION = '2.2.0'.freeze
# ART19 maintains a fork with patches applied on top of the upstream gem.
# We publish our fork with a revision number appended to the upstream version.
ART19_REVISION = '1'.freeze
VERSION = "2.2.0.#{ART19_REVISION}".freeze
end
end
54 changes: 54 additions & 0 deletions spec/fixtures/actor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,60 @@ class CamelCaseActorSerializer
end
end

class MethodFilteredActorSerializer < UserSerializer
set_type :actor

attributes_filter :filtered_attributes_by_policy

has_many(
:played_movies,
serializer: :movie,
links: :movie_urls,
if: ->(_object, params) { params[:conditionals_off].nil? }
) do |object|
object.movies
end

def self.filtered_attributes_by_policy(superset, _record, params)
permit = params[:filter_attributes]

case permit
when :all
superset
when nil, :none, []
[]
else
superset.slice(*permit)
end
end
end

class CallableFilteredActorSerializer < UserSerializer
set_type :actor

attributes_filter do |superset, _record, params|
permit = params[:filter_attributes]

case permit
when :all
superset
when nil, :none, []
[]
else
superset.slice(*permit)
end
end

has_many(
:played_movies,
serializer: :movie,
links: :movie_urls,
if: ->(_object, params) { params[:conditionals_off].nil? }
) do |object|
object.movies
end
end

class BadMovieSerializerActorSerializer < ActorSerializer
has_many :played_movies, serializer: :bad, object_method_name: :movies
end
Expand Down
37 changes: 37 additions & 0 deletions spec/fixtures/movie.rb
Original file line number Diff line number Diff line change
Expand Up @@ -125,3 +125,40 @@ class MovieSerializer < ::MovieSerializer
end
end
end

class MethodFilteredMovieSerializer < ::MovieSerializer
relationships_filter :filtered_by_something

has_many(
:first_two_actors,
id_method_name: :uid
) do |record|
record.actors.take(2)
end

def self.filtered_by_something(superset, _record, params)
return superset unless params[:limit_relationships]

superset.slice(:actors, :creator)
end
end

class CallableFilteredMovieSerializer < ::MovieSerializer
relationships_filter do |superset, _record, params|
return superset unless params[:limit_relationships]

superset.slice(:actors, :creator)
end
end

class CallableLinksMovieSerializer < ::MovieSerializer
has_many(
:first_two_actors,
id_method_name: :uid,
links: lambda do |record|
{ some: record.id, fancy: 'here' }
end
) do |record|
record.actors.take(2)
end
end
Loading

0 comments on commit 1e966e7

Please sign in to comment.