Skip to content

Instantly share code, notes, and snippets.

@hannahwhy
Created April 10, 2012 08:06
Show Gist options
  • Select an option

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

Select an option

Save hannahwhy/2349228 to your computer and use it in GitHub Desktop.
This feels wrong, but it works
require 'catalog/model'
module Catalog::Model
##
# Defines a schema for a model object.
#
# Schemas look like this:
#
# class Video
# include Catalog::Model
#
# schema [
# 'title',
# { 'audio' => ['artist', 'title'] },
# 'members'
# ]
# end
#
# This would create expectations for a document matching any of these
# structures:
#
#
# { "title": ...,
# "audio": { "artist": "...", "title": ... },
# "members": ...
# }
#
# or
#
# { "title": ...,
# "audio": [
# { "artist": ..., "title": ... },
# ...
# ],
# "members": [...]
# }
#
# In the latter case, each element of the audio list will have artist and
# title accessors.
#
# Proxy objects are eagerly instantiated; this decision was made to mesh
# well with Catalog::Model's intended use, viz. presenting and massaging
# data to and from views.
#
# Both strings and symbols may be used as keys; internally, all keys are
# coerced to strings.
module SchemaDefinition
extend ActiveSupport::Concern
included do
include ActiveModel::MassAssignmentSecurity
end
def initialize(doc = nil)
self._doc = reify(sanitize_for_mass_assignment(doc || {}))
end
def reify(doc)
{}.tap do |h|
doc.each do |field, value|
wrapper = self.class.class_for_field(field)
h[field] = if wrapper
if Array === value
value.map { |v| wrapper.new(v) }
else
wrapper.new(value)
end
else
value
end
end
end
end
def as_json(options = nil)
_doc
end
module ClassMethods
class ObjectProxy
include SchemaDefinition
attr_accessor :_doc
def self.[](subschema)
Class.new(self) { schema subschema }
end
end
def class_for_field(f)
@class_for_field[f]
end
def schema(s)
@class_for_field = {}
s.each do |field|
if field.is_a?(Hash)
k = field.keys.first
v = field[k]
@class_for_field[k.to_s] = ObjectProxy[v]
else
k = field
end
attr_accessible k
define_method(k) { _doc[k.to_s] }
end
end
end
end
end
# vim:ts=2:sw=2:et:tw=78
require 'spec_helper'
require 'catalog/model'
module Catalog::Model
describe SchemaDefinition do
subject do
Class.new do
include SchemaDefinition
attr_accessor :_doc
def initialize(doc = nil)
self._doc = doc || {}
super
end
def _id
_doc['_id']
end
end
end
let(:doc) do
{
'title' => 'A video'
}
end
def json(doc)
JSON.parse(subject.new(doc).to_json)
end
describe '#to_json' do
before do
subject.schema ['title', { 'image' => [ 'position', 'url' ] }, { 'songs' => [ 'artist', 'title' ] } ]
end
describe 'on top-level fields' do
let(:doc) do
{ 'title' => 'A video' }
end
it 'outputs whitelisted fields' do
json(doc)['title'].should == 'A video'
end
end
describe 'on nested objects' do
let(:doc) do
{ 'image' => { 'position' => '10% 20%' } }
end
it 'outputs whitelisted fields' do
json(doc)['image'].should == { 'position' => '10% 20%' }
end
end
describe 'on lists of nested objects' do
let(:doc) do
{ 'songs' => [
{ 'artist' => 'Lights', 'title' => 'Siberia' }
]
}
end
it 'outputs whitelisted fields' do
json(doc)['songs'][0].should ==
{ 'artist' => 'Lights', 'title' => 'Siberia' }
end
end
end
describe '#schema' do
describe 'on top-level fields' do
before do
subject.schema ['title']
end
it 'provides readers' do
subject.new(doc).title.should == 'A video'
end
it 'whitelists fields' do
doc.update('_id' => '3abc')
json(doc)['_id'].should be_nil
end
end
describe 'on nested objects' do
before do
subject.schema [ { 'image' => [ 'position', 'url' ] } ]
end
it 'provides readers' do
doc.update('image' => {
'position' => '10% 20%',
'url' => '/example.jpg'
})
subject.new(doc).image.position.should == '10% 20%'
subject.new(doc).image.url.should == '/example.jpg'
end
it 'whitelists fields' do
doc.update('image' => {
'position' => '10% 20%',
'url' => '/example.jpg',
'foo' => 'bar'
})
json(doc)['image']['foo'].should be_nil
end
end
describe 'on multi-level nested objects' do
before do
subject.schema [
{ 'image' => [
'tags' => [ 'name', 'category' ]
]
}
]
end
it 'provides readers' do
fragment = {
'image' => {
'tags' => [
{ 'name' => 'A tag', 'category' => 'A category' }
]
}
}
doc.update(fragment)
subject.new(doc).image.tags[0].name.should == 'A tag'
subject.new(doc).image.tags[0].category.should == 'A category'
end
end
describe 'on lists of nested objects' do
before do
subject.schema [ { 'songs' => [ 'artist', 'title' ] } ]
end
it 'provides readers' do
doc.update('songs' => [
{ 'artist' => 'Lights', 'title' => 'Siberia' }
])
subject.new(doc).songs[0].artist.should == 'Lights'
subject.new(doc).songs[0].title.should == 'Siberia'
end
it 'whitelists fields' do
doc.update('songs' => [
{ 'artist' => 'Lights', 'title' => 'Siberia', 'verified' => false }
])
json(doc)['songs'][0]['verified'].should be_nil
end
end
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment