-
-
Save fgrehm/1917450 to your computer and use it in GitHub Desktop.
require 'json' | |
Virtus::Coercion::String.class_eval do | |
def self.to_hash(value) | |
JSON.parse(value) | |
end | |
end | |
module MyApp | |
module Attributes | |
class JSON < Virtus::Attribute::Object | |
primitive ::Hash | |
coercion_method :to_hash | |
end | |
end | |
class User | |
include Virtus | |
attribute :info, Attributes::JSON | |
end | |
end | |
@user = MyApp::User.new(:info => '{"email":"[email protected]"}') | |
@user.info.class # => Hash | |
@user.info = {'new' => 'info'} # => Doesn't throw an exception :) |
Another option could be to push this even higher up and be able to coerce any object that responds to #to_json
into JSON, not just a string, eg:
def Virtus::Coercion::Object.to_json(value)
coerce_with_method(value, :to_json)
end
What this will do is call #to_json
on the value if it responds to it, otherwise it will return the value as-is.
The only thing I'm not sure of, and this will require some testing, is how the ::String#to_json
method is defined. I don't know if it calls JSON.parse(self)
or not. I assume it does, but you probably want to test it to be sure.
oh, woops. I realize I might be confusing #to_json
with the fact that it's really being converted from json. sorry about that. lemme think about a more clear way to handle that.
How about something like this:
def Virtus::Coercion::String.to_json(value)
JSON.parse(value)
end
def Virtus::Coercion::Object.to_json(value)
json = coerce_with_method(value, :to_json)
json.equal?(value) ? value : String.to_json(json)
end
The implementation of this might work, but I'm having trouble with the proposed name of your Attribute class. I mean, if it really was a JSON field, then I would assume it was a JSON string, not an arbitrary ruby Object (or a Hash if you decide to restrict it that way). The naming is what threw me off above, and it still sticks out as something I might consider changing if I were modelling this.
With Virtus you're more focused around what the type of the attribute in the instance should be, not necessarily the serialization method used for the input. Sure, there are common coercions where it's unambiguous, like coercing "1"
into 1
for an Integer attribute, but there's no way to say "this is a Hash, but it could come in as a Hash or as a String in JSON encoding".
I'm almost wondering if this is the kind of thing a form plugin would handle, like @emmanuel's conformitas. If I was writing a pure ruby object without Virtus or another framework I would almost certainly not handle deserialization inside the object, but I would have something that knows how to take the input and then create the object from it. It's often in bloated frameworks where the model handles this (and usually dozens of other responsibilities), but I'm not sure the core objects should know about that kind of thing.
I might suggest doing something like:
By default
Virtus::Attribute::Object
allows you to use any ruby object, and since JSON can also be an Array, any string that is valid JSON could be coerced.