jsonapi-rb / jsonapi-rails

Rails gem for fast jsonapi-compliant APIs.
http://jsonapi-rb.org
MIT License
319 stars 63 forks source link

Setting custom classes #68

Open ZempTime opened 6 years ago

ZempTime commented 6 years ago

I don't have a solid mental model for what's happening when I'm trying to set a class -> serializable class mapping.

I've got two classes:

class Relationship < ApplicationRecord
  # relationship stuff. baggage, if you will.
end

# then, nested in app/models/relationships:
class Relationships::Approve < Relationship
  # approval related things, so all that logic is only here
end

I've got a SerializableRelationship class that's working. I'd like to reuse that with these subclasses.

class Api::Relationships::ApprovesController < Api::ApiController
  before_action :set_relationship

  def update
    if @relationship.update(status: "approved", action_user: current_user)
      render jsonapi: @relationship
    else
      render jsonapi_errors: @relationship.errors
    end
  end

  private
    def set_relationship
      @relationship = Relationships::Approve.find(params[:id])
    end
end

How do I:

I know that, once I get it, it'll probably be an "ohhhh" moment. I conceptually understand it's a big hash that makes certain keys to certain other classes that serialize those things. But I need to see an example, and get details, because I don't understand:

I've been guessing things and not getting feedback I know how to understand from the errors.

I think this would make some great addition to the docs! I'll check back in when I figure it out, and maybe PR in some updates if you're ok with that.

beauby commented 6 years ago

Hi @ZempTime

  • set this option inside my controller action?
render jsonapi: @relationship, class: { 'Relationship::Approve': SerializableRelationship }
  • set this option at the controller wide level?
def jsonapi_class
  super.merge(
    'Relationship::Approve': SerializableRelationship
  )
end
  • application-wide level

Override jsonapi_class in your ApplicationController.

The key is the class name of the object you are passing to render jsonapi:. Usually, it is an ActiveRecord model class name.

The value is a Serializable class (usually a subclass of JSONAPI::Serializable::Resource).

Not sure I understand but I'd say no: the models are not modified, and the serializable classes are just instantiated.

I think this would make some great addition to the docs! I'll check back in when I figure it out, and maybe PR in some updates if you're ok with that.

I agree and that would be of great help! (:

ZempTime commented 6 years ago

Oh my gosh! So helpful! I'm not doing Ruby/Rails full time anymore, but next coding session I'll sit down and get a PR in before starting on my personal stuff.

bprotas commented 6 years ago

One thing that I am struggling with is how to "duck-type" the rendering; for example, I have more than one class (specifically, one is a model, the other is a double) that both respond to the same API and I want to render with the same subclass of JSONAPI::Serializable::Resource. I was hoping that specifying jsonapi_class in a call to render jsonapi: would let me say "use this renderer", but that doesn't appear to be the case. Is there a way I can request that jsonapi-rails render an object with a given serializable, regardless of what it's underlying class is?

beauby commented 6 years ago

@bprotas Yes, you were almost there: the class option of the render method does that for you, and the default value for the class option can be overridden by overriding the jsonapi_class method. Note that when explicitly specifying a class option, you will prevent the default mapping to kick in (User -> SerializableUser), but you can alway do either

render jsonapi: users, class: jsonapi_class.merge(Article: MyCustomSerializableArticle)

or overriding jsonapi_class as

def jsonapi_class
  super.merge(Article: MyCustomSerializableArticle)
end
bprotas commented 6 years ago

Thanks @beauby ; I appreciate the response. This isn't quite what I was looking for - it assumes that here my class is always Article to be serialized by MyCustomSerializableArticle, but in fact I want MyCustomSerializableArticle to be used to render regardless of if the object is of class Article or not (it might be a subclass of Article, or a class of Rspec::Mocks::Double if I'm writing a test.

I ended up with this line of code to always use the same serializer, regardless of the class of object I'm serializing:

render jsonapi: article, class: {article.class.name.to_sym => MyCustomSerializableArticle}

This feels like a bit of a hack; I would rather not have to use the hash:

render jsonapi: article, class: MyCustomSerializableArticle

I might still be misunderstanding how the library is supposed to work?

Thank you again for the quick response!

beauby commented 6 years ago

@bprotas I see. The json:api standard is based on the idea of serializing a graph of resources, so the general case is that one needs to map each resource type to a serializer. In your case, you could do

render jsonapi: article, class: Hash.new { |h, k| h[k] = MyCustomSerializableArticle }

i.e. use a hash with a default value, or even

render jsonapi: article, class: ->(_) { MyCustomSerializableArticle }

using a lambda.

bprotas commented 6 years ago

ah interesting - that is much cleaner, thank you!

JoeWoodward commented 6 years ago

I've created a PR that allows customizing the default serializer mappings in the config file. https://github.com/jsonapi-rb/jsonapi-rails/pull/92

I think it makes sense for situations like this one. i.e. STI models normally will use the same serializer so would be good if you could manually specify that.