Active Record to_json\as_json on Array of Models - ruby

First off, I am not using Rails. I am using Sinatra for this project with Active Record.
I want to be able to override either to_json or as_json on my Model class and have it define some 'default' options. For example I have the following:
class Vendor < ActiveRecord::Base
def to_json(options = {})
if options.empty?
super :only => [:id, :name]
else
super options
end
end
end
where Vendor has more attributes than just id and name. In my route I have something like the following:
#vendors = Vendor.where({})
#vendors.to_json
Here #vendors is an Array vendor objects (obviously). The returned json is, however, not invoking my to_json method and is returning all of the models attributes.
I don't really have the option of modifying the route because I am actually using a modified sinatra-rest gem (http://github.com/mikeycgto/sinatra-rest).
Any ideas on how to achieve this functionality? I could do something like the following in my sinatra-rest gem but this seems silly:
#PLURAL.collect! { |obj| obj.to_json }

Try overriding serializable_hash intead:
def serializable_hash(options = nil)
{ :id => id, :name => name }
end
More information here.

If you override as_json instead of to_json, each element in the array will format with as_json before the array is converted to JSON
I'm using the following to only expose only accessible attributes:
def as_json(options = {})
options[:only] ||= self.class.accessible_attributes.to_a
super(options)
end

Related

Mongoid embedded collection response to :find

I'm sending serialized data to a class which need to access a Mongoid document which may or may not be embedded.
In case of embedded document, I'm accepting a variable number of arguments which I reduce to get the embedded document.
The code is pretty simple:
def perform(object, *arguments)
#opts = arguments.extract_options!
#object = arguments.reduce(object){|object, args| object.public_send(*args)}
# [...]
I used public_send because AFAIK I only need to call public methods.
However, when I try to access an embedded document I have some really strange result where #object is an enumerator.
After some debugging, this is what I found that for any root document object and an embedded collection items, I have:
object.items.public_send(:find)
# => #<Enumerator: ...>
object.items.send(:find) # or __send__
# => nil
The method called is not the same at all when I call public_send or send!
How is it even possible?
Is it normal? Is that a bug?
public_send seems to invoke the find method of Array (Enumerable) but send (or __send__) invokes the find method of Mongoid
Edit: simple reproductible case:
require 'mongoid'
class User
include Mongoid::Document
field :name, type: String
embeds_many :groups
end
class Group
include Mongoid::Document
field :name, type: String
embedded_in :user
end
Mongoid.load_configuration({
sessions: {
default: {
database: 'send_find',
hosts: [
'localhost:27017'
]
}
}
})
user = User.create(name: 'john')
user.groups.create(name: 'g1')
user.groups.create(name: 'g2')
puts "public_send :find"
puts user.groups.public_send(:find).inspect
# => #<Enumerator: [#<Group _id: 5530dea57735334b69010000, name: "g1">, #<Group _id: 5530dea57735334b69020000, name: "g2">]:find>
puts "send :find"
puts user.groups.send(:find).inspect
# => nil
puts "__send__ :find"
puts user.groups.__send__(:find).inspect
# => nil
Okay, after a few hours of debugging, I found that it is actually a bug in Mongoid.
The relation is not an array but a proxy around the array, which delegates most methods to the array.
As public_send was also delegated but not send and __send__, the behavior was not the same.
For more information, see my pull request and the associated commit.

data_mapper, attr_accessor, & serialization only serializing properties not attr_accessor attributes

I'm using data_mapper/sinatra and trying to create some attributes with attr_accessor. The following example code:
require 'json'
class Person
include DataMapper::Resource
property :id, Serial
property :first_name, String
attr_accessor :last_name
end
ps = Person.new
ps.first_name = "Mike"
ps.last_name = "Smith"
p ps.to_json
produces this output:
"{\"id\":null,\"first_name\":\"Mike\"}"
Obviously I would like for it to give me both the first and last name attributes. Any ideas on how to get this to work in the way one would expect so that my json has all of the attributes?
Also, feel free to also explain why my expectation (that I'd get all of the attributes) is incorrect. I'm guessing some internal list of attributes isn't getting the attr_accessor instance variables added to it or something. But even so, why?
Datamapper has it’s own serialization library, dm-serializer, that provides a to_json method for any Datamapper resource. If you require Datamapper with require 'data_mapper' in your code, you are using the data_mapper meta-gem that requires dm-serializer as part of it’s set up.
The to_json method provided by dm-serializer only serializes the Datamapper properties of your object (i.e. those you’ve specified with property) and not the “normal” properties (that you’ve defined with attr_accessor). This is why you get id and first_name but not last_name.
In order to avoid using dm-serializer you need to explicitly require those libraries you need, rather than rely on data_mapper. You will need at least dm-core and maybe others.
The “normal” json library doesn’t include any attributes in the default to_json call on an object, it just uses the objects to_s method. So in this case, if you replace require 'data_mapper' with require 'dm-core', you will get something like "\"#<Person:0x000001013a0320>\"".
To create json representations of your own objects you need to create your own to_json method. A simple example would be to just hard code the attributes you want in the json:
def to_json
{:id => id, :first_name => first_name, :last_name => last_name}.to_json
end
You could create a method that looks at the attributes and properties of the object and create the appropriate json from that instead of hardcoding them this way.
Note that if you create your own to_json method you could still call require 'data_mapper', your to_json will replace the one provided by dm-serializer. In fact dm-serializer also adds an as_json method that you could use to create the combined to_json method, e.g.:
def to_json
as_json.merge({:last_name => last_name}).to_json
end
Thanks to Matt I did some digging and found the :method param for dm-serializer's to_json method. Their to_json method was pretty decent and was basically just a wrapper for an as_json helper method so I overwrote it by just adding a few lines:
if options[:include_attributes]
options[:methods] = [] if options[:methods].nil?
options[:methods].concat(model.attributes).uniq!
end
The completed method override looks like:
module DataMapper
module Serializer
def to_json(*args)
options = args.first
options = {} unless options.kind_of?(Hash)
if options[:include_attributes]
options[:methods] = [] if options[:methods].nil?
options[:methods].concat(model.attributes).uniq!
end
result = as_json(options)
# default to making JSON
if options.fetch(:to_json, true)
MultiJson.dump(result)
else
result
end
end
end
end
This works along with an attributes method I added to a base module I use with my models. The relevant section is below:
module Base
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def attr_accessor(*vars)
#attributes ||= []
#attributes.concat vars
super(*vars)
end
def attributes
#attributes || []
end
end
def attributes
self.class.attributes
end
end
now my original example:
require 'json'
class Person
include DataMapper::Resource
include Base
property :id, Serial
property :first_name, String
attr_accessor :last_name
end
ps = Person.new
ps.first_name = "Mike"
ps.last_name = "Smith"
p ps.to_json :include_attributes => true
Works as expected, with the new option parameter.
What I could have done to selectively get the attributes I wanted without having to do the extra work was to just pass the attribute names into the :methods param.
p ps.to_json :methods => [:last_name]
Or, since I already had my Base class:
p ps.to_json :methods => Person.attributes
Now I just need to figure out how I want to support collections.

Generating JSON for Sinatra

I'm having an issue with passing the generated JSON notation of my object to my Sinatra application. The problem I have is twofold:
I have 2 classes that are mapped to a database using the Sequel gem. When they generate JSON it is ok and properly implemented.
I have a custom class called registration that maps one of the classes with an additional field. The goal is to generate JSON out of this and pass that JSON to the application using cucumber (test purpose)
The application code responsible for handling the request has the following function defined:
post '/users' do
begin
hash = JSON.parse(self.request.body.read)
registration = Registration.new.from_json(#request.body.read)
registration.user.country = Database::Alaplaya.get_country_by_iso_code(registration.user.country.iso_code)
return 400 unless(registration.is_valid?)
id = Database::Alaplaya.create_user(registration.user)
# If the registration failed in our system, return a page 400.
return 400 if id < 1
end
problem 1: I cannot use the params hash. It exists but is just an empty hash. Why?
problem 2: I cannot deserialize the JSON generated by the class itself. Why?
The registration class looks like this:
require 'json'
class Registration
attr_accessor :user, :project_id
def to_json(*a)
{
'json_class' => self.class.name,
'data' => [#user.to_json(*a), #project_id]
}.to_json(*a)
end
def self.json_create(o)
new(*o['data'])
end
# Creates a new instance of the class using the information provided in the
# hash. If a field is missing in the hash, nil will be assigned to that field
# instead.
def initialize(params = {})
#user = params[:user]
#project_id = params[:project_id]
end
# Returns a string representing the entire Registration.
def inspect
"#{#user.inspect} - #{#user.country.inspect} - #{#project_id}"
end
# Returns a boolean valid representing whether the Registration instance is
# considered valid for the API or not. True if the instance is considered
# valid; otherwise false.
def is_valid?
return false if #user.nil? || #project_id.nil?
return false if !#user.is_a?(User) || !#project_id.is_a?(Fixnum)
return false if !#user.is_valid?
true
end
end
I had to implement the methods to generate the JSON output correctly. When I run this in console I get the following output generated:
irb(main):004:0> r = Registration.new(:user => u, :project_id => 1)
=> new_login - nil - 1
irb(main):005:0> r.to_json
=> "{\"json_class\":\"Registration\",\"data\":[\"{\\\"json_class\\\":\\\"User\\\
",\\\"login\\\":\\\"new_login\\\"}\",1]}"
Which looks like valid JSON to me. However when I POST this to the application server and try to parse this, JSON complains that at least 2 octets are needed and refuses to deserialize the object.
If you're using Sequel as your ORM, try something like this:
In your model:
class Registration < Sequel::Model
many_to_one :user
many_to_one :project
plugin :json_serializer
end
The server:
before do
#data = JSON.parse(request.body.read) rescue {}
end
post '/users' do
#registration = Registration.new #data
if #registration.valid?
#registration.save
#registration.to_json #return a JSON representation of the resource
else
status 422 #proper status code for invalid input
#registration.errors.to_json
end
end
I think you may be overcomplicating your registration process. If the HTTP action is POST /users then why not create a user? Seems like creating a registration is overly complex. Unless your user already exists, in which case POST /users would be incorrect. If what you're really intending to do is add a user to to a project, then you should PUT /projects/:project_id/users/:user_id and the action would look something like this:
class User < Sequel::Model
many_to_many :projects
end
class Project < Sequel::Model
many_to_many :users
end
#make sure your db schema has a table called users_projects or projects_users
put '/projects/:project_id/users/:user_id' do
#find the project
#project = Project.find params[:project_id]
raise Sinatra::NotFound unless #project
#find the user
#user = Project.find params[:project_id]
raise Sinatra::NotFound unless #user
#add user to project's users collection
#project.add_user #user
#send a new representation of the parent resource back to the client
#i like to include the child resources as well
#json might look something like this
#{ 'name' : 'a project name', 'users' : ['/users/:user_id', '/users/:another_user_id'] }
#project.to_json
end

Rendering a simple Ruby hash with RABL

I have a ruby hash that I'd like to render using RABL. The hash looks something like this:
#my_hash = {
:c => {
:d => "e"
}
}
I'm trying to render this with some RABL code:
object #my_hash => :some_object
attributes :d
node(:c) { |n| n[:d] }
but I'm receiving {"c":null}
How can I render this with RABL?
This works for arbitrary hash values.
object false
#values.keys.each do |key|
node(key){ #values[key] }
end
Worked for me using Rails 3.2.13 and Ruby 2.0.0-p195
Currently RABL doesn't play too nicely with hashes. I was able to work around this by converting my hash to an OpenStruct format (which uses a more RABL-friendly dot-notation). Using your example:
your_controller.rb
require 'ostruct'
#my_hash = OpenStruct.new
#my_hash.d = 'e'
your_view.rabl
object false
child #my_hash => :c do
attributes :d
end
results
{
"c":{
"d":"e"
}
}
Sometimes its easy to do too much imho.
How about just
render json: my_hash
And just like magic we can delete some code !
RABL deals in objects but does not require a particular ORM. Just objects that support dot notation. If you want to use rabl and all you have is a hash:
#user = { :name => "Bob", :age => 27, :year => 1976 }
then you need to first turn the hash into an object that supports dot notation:
#user = OpenStruct.new({ :name => "Bob", :age => 27, :year => 1976 })
and then within a RABL template treat the OpenStruct as any other object:
object #user
attributes :name, :age, :year
Consider that if everything you are doing in your app is just dealing in hashes and there is no objects or databases involved, you may be better off with an alternative more custom JSON builder such as json_builder or jbuilder.
Pasted from the official wiki page on RABL's github: https://github.com/nesquena/rabl/wiki/Rendering-hash-objects-in-rabl
RABL actually can render ruby hashes and arrays easily, as attributes, just not as the root object. So, for instance, if you create an OpenStruct like this for the root object:
#my_object = OpenStruct.new
#my_object.data = {:c => {:d => 'e'}}
Then you could use this RABL template:
object #my_object
attributes :data
And that would render:
{"data": {"c":{"d":"e"}} }
Alternatively, if you want :c to be a property of your root object, you can use "node" to create that node, and render the hash inside that node:
# -- rails controller or whatever --
#my_hash = {:c => {:d => :e}}
# -- RABL file --
object #my_hash
# Create a node with a block which receives #my_hash as an argument:
node { |my_hash|
# The hash returned from this unnamed node will be merged into the parent, so we
# just return the hash we want to be represented in the root of the response.
# RABL will render anything inside this hash as JSON (nested hashes, arrays, etc)
# Note: we could also return a new hash of specific keys and values if we didn't
# want the whole hash
my_hash
end
# renders:
{"c": {"d": "e"}}
Incidentally, this is exactly the same as just using render :json => #my_hash in rails, so RABL is not particularly useful in this trivial case ;) But it demonstrates the mechanics anyway.
By specifying a node like that, you are given access to the #my_hash object which you can then access attributes of. So I would just slightly change your code to be:
object #my_hash
node(:c) do |c_node|
{:d => c_node.d}
end
where c_node is essentially the #my_hash object. This should give you what you're expecting (shown here in JSON):
{
"my_hash":{
"c":{
"d":"e"
}
}
}
My answer is partially based on the below listed site:
Adapted from this site:
http://www.rubyquiz.com/quiz81.html
require "ostruct"
class Object
def to_openstruct
self
end
end
class Array
def to_openstruct
map{ |el| el.to_openstruct }
end
end
class Hash
def to_openstruct
mapped = {}
each{ |key,value| mapped[key] = value.to_openstruct }
OpenStruct.new(mapped)
end
end
Define this perhaps in an initializer and then for any hash just put to_openstruct and send that over to the rabl template and basically do what jnunn shows in the view.

Static local variables for methods in Ruby?

I have this:
def valid_attributes
{ :email => "some_#{rand(9999)}#thing.com" }
end
For Rspec testing right? But I would like to do something like this:
def valid_attributes
static user_id = 0
user_id += 1
{ :email => "some_#{user_id}#thing.com" }
end
I don't want user_id to be accessible from anywhere but that method,
is this possible with Ruby?
This is a closure case. Try this
lambda {
user_id = 0
self.class.send(:define_method, :valid_attributes) do
user_id += 1
{ :email => "some_#{user_id}#thing.com" }
end
}.call
Wrapping everything in lambda allows the variables defined within lambda to only exist in the scope. You can add other methods also. Good luck!
This answer is a little larger in scope than your question, but I think it gets at the root of what you're trying to do, and will be the easiest and most maintainable.
I think what you're really looking for here is factories. Try using something like factory_girl, which will make a lot of testing much easier.
First, you'd set up a factory to create whatever type of object it is you're testing, and use a sequence for the email attribute:
FactoryGirl.define do
factory :model do
sequence(:email) {|n| "person#{n}#example.com" }
# include whatever else is required to make your model valid
end
end
Then, when you need valid attributes, you can use
Factory.attributes_for(:model)
You can also use Factory.create and Factory.build to create saved and unsaved instances of the model.
There's explanation of a lot more of the features in the getting started document, as well as instructions on how to add factories to your project.
You can use a closure:
def validator_factory
user_id = 0
lambda do
user_id += 1
{ :email => "some_#{user_id}#thing.com" }
end
end
valid_attributes = validator_factory
valid_attributes.call #=> {:email=>"some_1#thing.com"}
valid_attributes.call #=> {:email=>"some_2#thing.com"}
This way user_id won't be accessible outside.
I'd use an instance variable:
def valid_attributes
#user_id ||= 0
#user_id += 1
{ :email => "some_#{#user_id}#thing.com" }
end
The only variables Ruby has are local variables, instance variables, class variables and global variables. None of them fit what you're after.
What you probably need is a singleton that stores the user_id, and gives you a new ID number each time. Otherwise, your code won't be thread-safe.

Resources