Enumerable changes my `to_json` behavior - ruby

I have a rails application and a class I've wrote as a part of it (not an ActiveRecord or anything..). The data is stored in simple instance variables (string, integers, arrays...)
When I invoke to_json on an instance of it I get what I expect to. A JSON object, containing all instance variables as JSON objects too.
However, when I add include Enumerable to the class definition, the behavior of to_json changes and I get an empty object: "[]"
Any idea why is that? Does Enumerable somehow defined or undefines something that affects to_json?
Thanks!

So, what happens is:
Rails loads in ActiveSupport. ActiveSupport injects (monkey patches) these as_json methods into several classes and modules, including Enumerable:
module Enumerable
def as_json(options = nil) #:nodoc:
to_a.as_json(options)
end
end
You're probably returning nothing for the each method Enumerable requires you to have, so to_a returns [], and an empty array gets converted into the String "[]".
What you can do here, is, to bind your object into a non-enumerable inherited class, and use its .as_json method.
Like this:
class A
def as_json(*)
Object.instance_method(:as_json).bind(self).call
end
end
Demo:
➜ pry
require 'active_support/all'
=> true
class A
def initialize
#a = 1
end
end
=> nil
A.new.to_json
=> "{\"a\":1}"
class A
include Enumerable
def each
end
end
=> nil
A.new.to_json
=> "[]"
class A
def as_json(*)
Object.instance_method(:as_json).bind(self).call
end
end
=> nil
A.new.to_json
=> "{\"a\":1}"

Related

Ruby inheritance, method not being passed to a child class

I've got a ruby exercise, and cannot quite get past one point.
When I run the tests, it throws undefined method "attribute" for CommentSerializer:Class.
Though, there is such a method defined in serializer.rb, from which it's being inherited.
Am I missing something about inheritance in ruby here?
Note: I am neither allowed to add any gems other than the two listed below, nor to modify any file other than serializer.rb.
Here are the files:
Gemfile:
gem 'rspec'
gem 'pry'
app/comment.rb:
Comment = Struct.new(:id, :body)
app/comment_serializer.rb:
require_relative "serializer"
class CommentSerializer < Serializer
attribute :id
attribute :body
end
app/serializer.rb:
class Serializer
def initialize(object)
#obj = object
end
def serialize
obj.members.inject({}) do |hash, member|
hash[member] = obj[member]
hash
end
end
def attribute(key)
end
private
def obj
#obj
end
end
spec/comment_serializer_spec.rb:
require "date"
require_relative "spec_helper"
require_relative "../app/comment"
require_relative "../app/comment_serializer"
RSpec.describe CommentSerializer do
subject { described_class.new(comment) }
let(:comment) do
Comment.new(1, "Foo bar")
end
it "serializes object" do
expect(subject.serialize).to eq({
id: 1,
body: "Foo bar",
})
end
end
If you call something like attribute in the body of the class definition then it happens in the class context at that exact moment, as in:
class Example < Serializer
# This is evaluated immediately, as in self.attribute(:a) or Example.attribute(:a)
attribute :a
end
There must be a corresponding class method to receive that call, as in:
class Serializer
def self.attribute(name)
# ...
end
end
Since you're inheriting that method it will be defined prior to calling it, but that's not the case if you have something like:
class Example
attribute :a # undefined method `attribute' for Example:Class (NoMethodError)
def self.attribute(name)
end
end
The method is defined after it's called, so you get this error. You must either reverse the order, define first, call second, or put it into a parent class.

How to modify OpenStruct in the instantiated class

I have the following code where I instantiate an object that returns an OpenStruct result.
require 'ostruct'
class TestModel
attr_accessor :result
def initializer
end
def result
OpenStruct.new(successful?: false)
end
def successful!
result.send('successful?', true)
end
end
I want the class to work so that I could modify any attributes of my result on the fly.
test = TestModel.new
test.result
=> #<OpenStruct successful?=false>
test.result.successful!
=> #<OpenStruct successful?=true>
This syntax comes from the official OpenStruct page, and it works just on its own but not inside an instantiated class - https://ruby-doc.org/stdlib-2.5.3/libdoc/ostruct/rdoc/OpenStruct.html
result.send('successful?', true)
I've also tried to use lambda but to no avail
def result
OpenStruct.new(successful?: false, :successful! => lamdba {self.uccessful? = true})
end
Any ideas? I really want to know how to do that.
OpenStruct requires you to use Object#send or Hash-like keys to make use of predicate symbols. The documentation says:
Hash keys with spaces or characters that could normally not be used for method calls (e.g. ()[]*) will not be immediately available on the OpenStruct object as a method for retrieval or assignment, but can still be reached through the Object#send method or using [].
In addition, it's unclear why you want to define #result as writable, or why you would override the getter method so that TestModel#result always returns false. That's probably causing at least part of your problem.
Instead, I'd rewrite the code as follows:
require 'ostruct'
class TestModel
attr_reader :result
def initialize
#result = OpenStruct.new :successful? => nil
end
def unsuccessful
#result.send "successful?=", false
end
def successful!
#result.send "successful?=", true
end
end
test_model = TestModel.new
#=> #<TestModel:0x00007faf5c1b9528 #result=#<OpenStruct successful?=nil>>
test_model.result
#=> nil
test_model.successful!
#=> true
test_model.result
#=> #<OpenStruct successful?=true>
test_model.unsuccessful
#=> false
test_model.result
#=> #<OpenStruct successful?=false>
You could certainly initialize the struct member as false rather than nil if you prefer, but I think this is semantically clearer. Your mileage in that regard may vary.

Method chaining in ruby

I want to build an API client that has an interface similar to rails active record. I want the consumers to be able to chain methods and after the last method is chained, the client requests a url based on the methods called. So it's method chaining with some lazy evaluation. I looked into Active Record but this is very complicated (spawning proceses, etc).
Here is a toy example of the sort of thing I am talking about. You can chain as many 'bar' methods together as you like before calling 'get', like this:
puts Foo.bar.bar.get # => 'bar,bar'
puts Foo.bar.bar.bar.get # => 'bar,bar,bar'
I have successfully implemented this, but I would rather not need to call the 'get' method. So what I want is this:
puts Foo.bar.bar # => 'bar,bar'
But my current implementation does this:
puts Foo.bar.bar #=> [:bar, :bar]
I have thought of overriding array methods like each and to_s but I am sure there is a better solution.
How would I chain the methods and know which was the last one so I could return something like the string returned in the get method?
Here is my current implementation:
#!/usr/bin/env ruby
class Bar
def get(args)
# does a request to an API and returns things but this will do for now.
args.join(',')
end
end
class Foo < Array
def self.bar
#q = new
#q << :bar
#q
end
def bar
self << :bar
self
end
def get
Bar.new.get(self)
end
end
Also see: Ruby Challenge - Method chaining and Lazy Evaluation
How it works with activerecord is that the relation is a wrapper around the array, delegating any undefined method to this internal array (called target). So what you need is to start with a BasicObject instead of Object:
class Foo < BasicObject
then you need to create internal variable, to which you will delegate all the methods:
def method_missing(*args, &block)
reload! unless loaded?
#target.send(*args, &block)
end
def reload!
# your logic to populate target, e.g:
#target = #counter
#loaded = true
end
def loaded?
!!#loaded
end
To chain methods, your methods need to return new instance of your class, e.g:
def initialize(counter=0)
#counter = counter
end
def bar
_class.new(#counter + 1)
end
private
# BasicObject does not define class method. If you want to wrap your target
# completely (like ActiveRecord does before rails 4), you want to delegate it
# to #target as well. Still you need to access the instance class to create
# new instances. That's the way (if there are any suggestion how to improve it,
# please comment!)
def _class
(class << self; self end).superclass
end
Now you can check it in action:
p Foo.new.bar.bar.bar #=> 3
(f = Foo.new) && nil # '&& nil' added to prevent execution of inspect
# object in the console , as it will force #target
# to be loaded
f.loaded? #=> false
puts f #=> 0
f.loaded? #=> true
A (very simple, maybe simplistic) option would be to implement the to_s method - as it is used to "coerce" to string (for instance in a puts), you could have your specific "this is the end of the chain" code there.

HTTParty - JSON to strongly typed object

Is it possible to have HTTParty deserialize the results from a GET to a strongly typed ruby object? For example
class Myclass
include HTTParty
end
x = Myclass.get('http://api.stackoverflow.com/1.0/questions?tags=HTTParty')
puts x.total
puts x.questions[0].title
Right now it deserializes it into a hash
puts x["total"]
My question is actually if HTTParty supports this OTB, not by installing additional gems.
Edit:
I'm still new to Ruby, but I recall that class fields are all private so they would need to be accessed through getter/setter methods. So maybe this question isn't a valid one?
If you are just wanting method syntax, you can use an open struct.
require 'httparty'
require 'ostruct'
result = HTTParty.get 'http://api.stackoverflow.com/1.0/questions?tags=HTTParty'
object = OpenStruct.new result
object.total # => 2634237
A possible downside is that this object is totally open such that if you invoke a nonexistent method on it, it will just return nil (if you invoke a setter, it will create both the setter and getter)
It sounds like you want the return value of Myclass::get to be an instance of Myclass. If that's the case, you could cache the return value from the HTTP request and implement method_missing to return values from that hash:
class Myclass
include HTTParty
attr_accessor :retrieved_values
def method_missing(method, *args, &block)
if retrieved_values.key?(method)
retrieved_values[method]
else
super
end
end
def self.get_with_massaging(url)
new.tap do |instance|
instance.retrieved_values = get_without_massaging(url)
end
end
class << self
alias_method :get_without_massaging, :get
alias_method :get, :get_with_massaging
end
end
This isn't exactly what you asked for, because it only works one level deep — i.e., x.questions[0].title would need to be x.questions[0][:title]
x = Myclass.get('http://api.stackoverflow.com/1.0/questions?tags=HTTParty')
p x.total
p x.questions[0][:title]
Perhaps you could come up with some hybrid of this answer and Joshua Creek's to take advantage of OpenStruct.
I should also point out that all the method aliasing trickery isn't necessary if your method doesn't have to be named get.

Using custom to_json method in nested objects

I have a data structure that uses the Set class from the Ruby Standard Library. I'd like to be able to serialize my data structure to a JSON string.
By default, Set serializes as an Array:
>> s = Set.new [1,2,3]
>> s.to_json
=> "[1,2,3]"
Which is fine until you try to deserialize it.
So I defined a custom to_json method:
class Set
def to_json(*a)
{
"json_class" => self.class.name,
"data" => {
"elements" => self.to_a
}
}.to_json(*a)
end
def self.json_create(o)
new o["data"]["elements"]
end
end
Which works great:
>> s = Set.new [1,2,3]
>> s.to_json
=> "{\"data\":{\"elements\":[1,2,3]},\"json_class\":\"Set\"}"
Until I put the Set into a Hash or something:
>> a = { 'set' => s }
>> a.to_json
=> "{\"set\":[1,2,3]}"
Any idea why my custom to_json doesn't get called when the Set is nested inside another object?
The first chunk is for Rails 3.1 (older versions will be pretty much the same); the second chunk is for the standard non-Rails JSON. Skip to the end if tl;dr.
Your problem is that Rails does this:
[Object, Array, FalseClass, Float, Hash, Integer, NilClass, String, TrueClass].each do |klass|
klass.class_eval <<-RUBY, __FILE__, __LINE__
# Dumps object in JSON (JavaScript Object Notation). See www.json.org for more info.
def to_json(options = nil)
ActiveSupport::JSON.encode(self, options)
end
RUBY
end
in active_support/core_ext/object/to_json.rb. In particular, that changes Hash's to_json method into just an ActiveSupport::JSON.encode call.
Then, looking at ActiveSupport::JSON::Encoding::Encoder, we see this:
def encode(value, use_options = true)
check_for_circular_references(value) do
jsonified = use_options ? value.as_json(options_for(value)) : value.as_json
jsonified.encode_json(self)
end
end
So all the Rails JSON encoding goes through as_json. But, you're not defining your own as_json for Set, you're just setting up to_json and getting confused when Rails ignores something that it doesn't use.
If you set up your own Set#as_json:
class Set
def as_json(options = { })
{
"json_class" => self.class.name,
"data" => { "elements" => self.to_a }
}
end
end
then you'll get what you're after in the Rails console and Rails in general:
> require 'set'
> s = Set.new([1,2,3])
> s.to_json
=> "{\"json_class\":\"Set\",\"data\":{\"elements\":[1,2,3]}}"
> h = { :set => s }
> h.to_json
=> "{\"set\":{\"json_class\":\"Set\",\"data\":{\"elements\":[1,2,3]}}}"
Keep in mind that as_json is used to prepare an object for JSON serialization and then to_json produces the actual JSON string. The as_json methods generally return simple serializable data structures, such as Hash and Array, and have direct analogues in JSON; then, once you have something that is structured like JSON, to_json is used to serialize it into a linear JSON string.
When we look at the standard non-Rails JSON library, we see things like this:
def to_json(*a)
as_json.to_json(*a)
end
monkey patched into the basic classes (Symbol, Time, Date, ...). So once again, to_json is generally implemented in terms of as_json. In this environment, we need to include the standard to_json as well as the above as_json for Set:
class Set
def as_json(options = { })
{
"json_class" => self.class.name,
"data" => { "elements" => self.to_a }
}
end
def to_json(*a)
as_json.to_json(*a)
end
def self.json_create(o)
new o["data"]["elements"]
end
end
And we include your json_create class method for the decoder. Once that's all properly set up, we get things like this in irb:
>> s = Set.new([1,2,3])
>> s.as_json
=> {"json_class"=>"Set", "data"=>{"elements"=>[1, 2, 3]}}
>> h = { :set => s }
>> h.to_json
=> "{"set":{"json_class":"Set","data":{"elements":[1,2,3]}}}"
Executive Summary: If you're in Rails, don't worry about doing anything with to_json, as_json is what you want to play with. If you're not in Rails, implement most of your logic in as_json (despite what the documentation says) and add the standard to_json implementation (def to_json(*a);as_json.to_json(*a);end) as well.
Here is my approach to getting to_json method for custom classes which most probably wouldn't contain to_a method (it has been removed from Object class implementation lately)
There is a little magic here using self.included in a module. Here is a very nice article from 2006 about module having both instance and class methods http://blog.jayfields.com/2006/12/ruby-instance-and-class-methods-from.html
The module is designed to be included in any class to provide seamless to_json functionality. It intercepts attr_accessor method rather than uses its own in order to require minimal changes for existing classes.
module JSONable
module ClassMethods
attr_accessor :attributes
def attr_accessor *attrs
self.attributes = Array attrs
super
end
end
def self.included(base)
base.extend(ClassMethods)
end
def as_json options = {}
serialized = Hash.new
self.class.attributes.each do |attribute|
serialized[attribute] = self.public_send attribute
end
serialized
end
def to_json *a
as_json.to_json *a
end
end
class CustomClass
include JSONable
attr_accessor :b, :c
def initialize b: nil, c: nil
self.b, self.c = b, c
end
end
a = CustomClass.new(b: "q", c: 23)
puts JSON.pretty_generate a
{
"b": "q",
"c": 23
}
Looking for a solution on the same problem, i found this bug report on the Rails issue tracker. Besides it was closed, i suppose it still happens on the earlier versions. Hope it could help.
https://github.com/rails/rails/issues/576

Resources