Skip to content

Instantly share code, notes, and snippets.

@hannahwhy
Created April 8, 2012 19:16
Show Gist options
  • Select an option

  • Save hannahwhy/2339394 to your computer and use it in GitHub Desktop.

Select an option

Save hannahwhy/2339394 to your computer and use it in GitHub Desktop.
I can't tell if this is smart or stupid
The problem:
I want to be able to have attribute readers for a CouchDB document. Why? So I
can use Action Pack's form builder with said document.
Here's an example document:
{ "title": "A video",
"audio": [
{ "artist": "Dot Dot Dot",
"title": "Edge Of The World"
},
{ "title": "Birds in my backyard" }
],
"video": [
"Macross Frontier",
"Original live-action footage"
]
}
So I want this sort of thing to work:
doc.title
doc.audio[0].artist
doc.video[1]
because Action Pack's form builder expects it to work.
So far I've come up with the code below. It walks a JSON structure at class
definition time and builds out attribute methods for each field it finds. If
it finds a field that references an object, it creates a proxy class and walks
that object. If it finds a field that references an array, it creates an object
proxy class and a list proxy class.
For simplicity, none of the attributes in any of the proxy classes are
mutable. (And they don't need to be, ever: in CouchDB, document update is
document replacement, so a document can be updated by with two PUTs of
different instances of a proxy object to a given ID.)
module AttributeWrapper
extend ActiveSupport::Concern
# _doc is expected to be an instance method that returns the original document hash
# this requires Ruby 1.9: in 1.8, String.respond_to?(:each) => true
module ClassMethods
class EnumerableProxy
include Enumerable
def initialize(list, object_proxy)
@list = list || []
@prox = list.map { |v| object_proxy.new(v) }
end
def each(&block)
@prox.each(&block)
end
def [](index)
@prox[index]
end
end
class ObjectProxy
include Catalog::ActiveModel
def self.[](s)
Class.new(self) { schema s }
end
end
def schema(str)
return schema(JSON.parse(str)) if String === str
str.each do |k, v|
if v.respond_to?(:keys)
proxy = ObjectProxy[v]
define_method(k) { proxy.new(_doc[k]) }
elsif v.respond_to?(:first) && v.respond_to?(:empty?)
if v.empty?
define_method(k) { _doc[k] }
else
proxy = ObjectProxy[v.first]
define_method(k) { EnumerableProxy.new(_doc[k], proxy) }
end
else
define_method(k) { _doc[k] }
end
end
end
end
end
class Video
include AttributeWrapper
schema %q{
{ "title": "",
"banner_image": {
"position": "",
"url": ""
},
"audio": [
{ "artist": "Ellie Goulding",
"title": "Animal"
}
],
"video": ["Macross Frontier"]
}
}
end
# v = Video.new(doc)
# v.title # => "A video"
# v.banner_image #= ><#<Class:0x2c0>:0x2f1 ...>
# v.banner_image.position => "10% 20%"
# v.banner_image.url => "http://www.example.org/image.jpg"
# v.audio[0].artist => "Ellie Goulding"
# v.video[0] => "Macross Frontier"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment