ActiveModel::Serializer Subclass that will accept any attribute dynamically? - ruby-on-rails

I'm building an API engine for an existing app, which will serve JSON with ActiveModel::Serializer. On the existing app, There are some controllers that just render regular old hashes that aren't instances of any ActiveModel subclass - originally, these were AJAX endpoints so it didn't matter what class the response body was.
I need to recreate some of these existing endpoints in the API module, so for instances like these, I want to build a custom serializer that will accept whatever attributes you throw at it. something like...
In the Controller:
def show
response = {
key: "this is a custom object and not an AM instance"
}
render json: response, serializer: Api::V1::CustomSerializer
end
And the serializer:
module Api
module V1
class CustomSerializer < ActiveModel::Serializer
def attributes
*object.keys.map(&:to_sym)
end
def read_attribute_for_serialization(attr)
object[attr.to_s]
end
end
end
end
Couple of problems:
a) The call to render in the controller doesn't seem to like the amount of args I've passed to render, which supposedly takes *args, which suggests to me there is something wrong with the override methods I've written.
b) If I were to just put attributes *object.class.column_names.map(&:to_sym) in the first line of the class, object is undefined outside of a method.
c) I call it inside a method, the resulting hash is nested inside whatever I choose to call that method. Not really what I had in mind.
My question: has anyone successfully created a serializer that will accept any attribute? Would love to know how.
PLEASE NOTE: I would like achieve this with AMS if I can - we're using the adapter for JSON API for all our response bodies. I would much rather make this work then initialize a hash identical to the json api standard we're using every time the response is not an AM instance.

For the folks who might have stumbled across the same problem, I ended up assembling a catch-all serializer class for anything i want to render that isn't a subclass of Active Record. Like so:
module Api
module V1
class CustomSerializer
def initialize(obj, error: false, type: nil)
#hash = error ? error_hash(obj) : success_hash(type, obj)
end
def to_h
#hash
end
private
def error_hash(obj)
{
errors: {
pointer: obj[:error] || obj[:errors]
},
detail: detail(obj)
}
end
def success_hash(type, obj)
{
id: obj.try(:id) ? obj[:id] : nil,
type: type.nil? ? 'hash' : type,
data: obj.try(:id) ? obj.except(:id) : obj,
links: ''
}
end
def detail(obj)
obj[:detail] || obj[:message] || obj[:msg]
end
end
end
end
Note that I'm working with the JSON API standard. Then, instead of doing something like this for an activemodel serializer:
render json: #device, serializer: Api::V1::DeviceSerializer
I can do something like this:
render json: Api::V1::CustomSerializer.new(#response, error: false, type: 'feed').to_h
basically just means I can still render the JSON api standard for anything that is just an instance of a Hash class, or anything for which I'm doing external API requests and storing in a Hash. Hope this helps someone someday.

Related

Rails 5 API: custom hidden responder that would process value returned by the action

I have rails 5 based api app, using fast_jsonapi
and after a while I observe that all most all my actions are having one common pattern
def action_name
#some_object.perform_action_name # this returns #some_object
render json: ControllerNameSerializer.new(#some_object).to_h
end
I do not wish to write the last render line here and it should work, for that I want that the returned value by the action should be processed by any hidden responder like thing, Serializer klass can be made out looking at controller name.
Perhaps this could be achieved by adding a small middleware. However at first, I find it not a good idea/practise to go for a middleware. In middleware, we do get rendered response, we need a hook prior to that.
I would imagine like
class SomeController ...
respond_with_returned_value
def action_name
#some_object.perform_action_name # this returns #some_object
end
Any suggestions?
Note, do not worry about error/failure cases, #some_object.errors could hold them and I have a mechanism to handle that separately.
Sketched out...
class ApplicationController < ...
def respond_with_returned_value
include MyWrapperModule
end
...
end
module MyWrapperModule
def self.included(base)
base.public_instance_methods.each do |method_name|
original_method_name = "original_#{method_name}".to_sym
rename method_name -> original_method_name
define_method(method_name) { render json: send(original_method_name) }
end
end
end
Seems like there really should be some blessed way to do this - or like someone must have already done it.

Dynamically include associations for objects in Rails

I am currently developing a small Rails 5 application where I need to pass on an ActiveRecord object to an external service based on certain events. In my model I have defined the following:
# /models/user.rb
after_create :notify_external_service_of_user_creation
def notify_external_service_of_user_creation
EventHandler.new(
event_kind: :create_user,
content: self
)
end
The EventHandler is then converting this object to JSON and is sending it through an HTTP request to the external service. By calling .to_json on the object this renders a JSON output which would look something like this:
{
"id":1234,
"email":"test#testemail.dk",
"first_name":"Thomas",
"last_name":"Anderson",
"association_id":12,
"another_association_id":356
}
Now, I need a way to include all first level associations directly into this, instead of just showing the foreign_key. So the construct I am looking for would be something like this:
{
"id":1234,
"email":"test#testemail.dk",
"first_name":"Thomas",
"last_name":"Anderson",
"association_id":{
"attr1":"some_data",
"attr2":"another_value"
},
"another_association_id":{
"attr1":"some_data",
"attr2":"another_value"
},
}
My first idea was to reflect upon the Model like so: object.class.name.constantize.reflect_on_all_associations.map(&:name), where object is an instance of a user in this case, and use this list to loop over the associations and include them in the output. This seems rather tedious though, so I was wondering if there would be a better way of achieving this using Ruby 2.4 and Rails 5.
If you don't want to use an external serializer, you can override as_json for each model. as_json gets called by to_json.
module JsonWithAssociations
def as_json
json_hash = super
self.class.reflect_on_all_associations.map(&:name).each do |assoc_name|
assoc_hash = if send(assoc_name).respond_to?(:first)
send(assoc_name).try(:map, &:as_json) || []
else
send(assoc_name).as_json
end
json_hash.merge!(assoc_name.to_s => assoc_hash)
end
json_hash
end
end
You'll need to prepend this particular module so that it overrides the default as_json method.
User.prepend(JsonWithAssociations)
or
class User
prepend JsonWithAssociations
end

How to define a Sparse Fieldset in a Rails request

Is there a simple way to implement requesting a Sparse Fieldset in a rails JSON request?
/some/endpoint.json?fields=id,name,favourite_colour
One solution I've found is to do it within a serialiser.
module V2
class BaseSerializer < ActiveModel::Serializer
self.root = true
def include?(field)
if #options.key?(:fields)
return #options[:fields].include? field.to_s
end
super field
end
end
end
In your controller you can do something like
render json: #sth, only: params[:fields].split(',').map(&:to_sym)
you should also wrap it in some strong params, to disallow non existing attributes
(but I doubt for it to be best possible solution)

Access to Rails request inside ActiveSupport::LogSubscriber subclass

I am trying to make a bit of a custom Rails logger which ultimately will log to a database. However, I don't have access to things like the request object, which I very much would like to have.
I'm currently trying to use the LogSubscriber (notification) interface to do the bulk of this; perhaps this is not the right approach. I do know I could abuse Thread.current[] but I was hoping to avoid doing that.
Here's the code I have which is as basic as I can get it for an example. This is loaded in an initializer.
module RequestLogging
class LogSubscriber < ActiveSupport::LogSubscriber
def process_action(event)
pp request # <--- does not work
pp event
end
end
RequestLogging::LogSubscriber.attach_to :action_controller
Probably you need to override process_action in ActionController::Instrumentation and then request object will be accessible like event.payload[:request]. I think you can put code somewhere in config/initializers, code example:
ActionController::Instrumentation.class_eval do
def process_action(*args)
raw_payload = {
controller: self.class.name,
action: self.action_name,
params: request.filtered_parameters,
format: request.format.try(:ref),
method: request.method,
path: (request.fullpath rescue "unknown"),
request: request,
session: session
}
ActiveSupport::Notifications.instrument("start_processing.action_controller", raw_payload.dup)
ActiveSupport::Notifications.instrument("process_action.action_controller", raw_payload) do |payload|
result = super
payload[:status] = response.status
append_info_to_payload(payload)
result
end
end
end
you can get the even.payload then pass it your own CustomLogger(formatted_log(even.payload) and then there you can define a module and save it.
You may want to customise your formatted_log function to beautify the payload accordingly.
def process_action(event)
CustomLogger.application(formattedLog(event.payload))
end
def formattedLog(payload)
# some restructuring of data.
end

Rails: do non-ActiveRecord models need to include ActiveModel::Serializers, or just respond to #as_json?

Using Rails 3.2, I'm working on an API backed model (not ActiveRecord). I want to be able to call to_json on this model in Rails controllers. After reading through a bunch of the ActiveModel docs I'm still not clear on one thing:
Given a model like this:
class MyModel
attr_accessor :id, :name
def initialize(data)
#id = data[:id]
#name = data[:name]
end
def as_json
{id: #id, name: #name}
end
end
Should this work as expected, or do I also need to include ActiveModel::Serializers::JSON? I'm having a hard time figuring out where the as_json / to_json methods are normally defined and when Rails calls which ones automatically in different circumstances...
Thanks for any insight!
Yes this does work, however not quote as you've written in.
When you render json in a controller using
def action
render :json => #my_model
end
Then Rails will automatically call to_json on your object and as long as you've defined to_json this will work as expected.
If your controller uses the Rails 3 content negotiation shenanigans, ie.
class Controller < ApplicationController
respond_to :json, :html
def action
respond_with(#my_model)
end
Then you will need to override as_json on your class, but the method signature requires an optional hash of options to be compatible with ActiveSupport so in this case you want
def as_json(options={})
...
end
Alternatively If you include ActiveModel::Serializers::JSON into your Class and your class supports the attributes method that returns a hash of attrs and their values - then you'll get as_json for free - but without the control of the resultant structure that you have if you just override the method by hand.
Hope this helps.

Resources