#Medea/JasonObject - Written by Michael Jensen module Medea require 'rest_client' require 'json' require 'uuidtools' class JasonObject < Medea::JasonBase include Medea::ActiveModelMethods if defined? ActiveModel extend ActiveModel::Naming end include JasonObjectMetaProperties attr_accessor :attachments #end meta #Here we're going to put the "query" interface #create a JasonDeferredQuery for all records of this class & with some optional conditions def JasonObject.all(opts=nil) q = JasonDeferredQuery.new :class => self, :filters => {:VERSION0 => nil, :FILTER => {:HTTP_X_CLASS => self, :HTTP_X_ACTION => :POST}} if opts q.limit = opts[:limit] if opts[:limit] q.since = opts[:since] if opts[:since] end q end #here we will capture: #members_of(object) (where object is an instance of a class that this class can be a member of) #find_by_(value) #Will return a JasonDeferredQuery for this class with the appropriate data filter set def JasonObject.method_missing(name, *args, &block) q = all if name.to_s =~ /^members_of$/ #use the type and key of the first arg (being a JasonObject) return q.members_of args[0] elsif name.to_s =~ /^find_by_(.*)$/ #use the property name from the name variable, and the value from the first arg q.add_data_filter $1, args[0] return q else #no method! super end end #end query interface #returns the JasonObject by directly querying the URL #if mode is :lazy, we return a GHOST, if mode is :eager, we return a STALE JasonObject def self.get_by_key(key, mode=:eager) return self.new key, mode end #"flexihash" access interface def []=(key, value) if @attachments.keys.include? key.to_sym @attachments[key.to_sym] = Medea::JasonBlob.new({:parent => self, :name => key, :content => value}) return end @__jason_data ||= {} @__jason_state = :dirty if jason_state == :stale @__jason_data[key] = value end def [](key) if @attachments.keys.include? key.to_sym if not @attachments[key.to_sym] #retrieve the JasonBlob for this key @attachments[key.to_sym] = Medea::JasonBlob.new({:parent => self, :name => key}) end return @attachments[key.to_sym] end @__jason_data[key] end #The "Magic" component of candy (https://github.com/SFEley/candy), repurposed to make this a # "weak object" that can take any attribute. # Assigning any attribute will add it to the object's hash (and then be POSTed to JasonDB on the next save) def method_missing(name, *args, &block) load_from_jasondb if @__jason_state == :ghost field = name.to_s if field =~ /(.*)=$/ # We're assigning self[$1] = args[0] elsif field =~ /(.*)\?$/ # We're asking (self[$1] ? true : false) else self[field] end end #end "flexihash" access def sanitize hash #remove the keys in hash that aren't allowed forbidden_keys = ["jason_key", "jason_state", "jason_parent", "jason_parent_key", "jason_parent_list"] hash.delete_if { |k,v| forbidden_keys.include? k } result = {} hash.each { |k, v| result[k.to_s] = v } result end def initialize initialiser = nil, mode = :eager @attachments = {} if opts[:attachments] opts[:attachments].each do |k| @attachments[k] = nil end end @public = [] if opts[:public] opts[:public].each {|i| @public << i} end if initialiser if initialiser.is_a? Hash @__jason_state = :new @__jason_data = sanitize initialiser else @__id = initialiser if mode == :eager load_from_jasondb else @__jason_state = :ghost end end else @__jason_state = :new @__jason_data = {} end end def to_s jason_key end #converts the data hash (that is, @__jason_data) to JSON format def serialise JSON.generate(@__jason_data) end #object persistence methods def update_attributes attributes @__jason_data = sanitize attributes @__jason_state = :dirty unless @__jason_state == :new save end #POSTs the current values of this object back to JasonDB #on successful post, sets state to STALE and updates eTag def save return false if @__jason_state == :stale or @__jason_state == :ghost begin save! return true rescue return false end end def save! @attachments.each do |k, v| if v v.save! end end #no changes? no save! return if @__jason_state == :stale or @__jason_state == :ghost #validations if self.class.owned #the parent object needs to be defined! raise "#{self.class.name} cannot be saved without setting a parent and list!" unless self.jason_parent && self.jason_parent_list end persist_changes :post end def to_url mode=:secure "#{JasonDB::db_auth_url mode}#{self.class.name}/#{self.jason_key}" end def to_public_url to_url :public end def persist_changes method = :post payload = self.serialise post_headers = { :content_type => 'application/json', "X-KEY" => self.jason_key, "X-CLASS" => self.class.name #also want to add the eTag here! #may also want to add any other indexable fields that the user specifies? } post_headers["IF-MATCH"] = @__jason_etag if @__jason_state == :dirty post_headers["X-PARENT"] = self.jason_parent.jason_key if self.jason_parent post_headers["X-LIST"] = self.jason_parent_list if self.jason_parent_list if opts[:located] #set the location headers if geohash? post_headers["X-GEOHASH"] = geohash end if latitude? && longitude? post_headers["X-LATITUDE"] = latitude post_headers["X-LONGITUDE"] = longitude end end post_headers.merge! permissions_header url = to_url() #puts "Saving to #{url}" if method == :post response = RestClient.post(url, payload, post_headers) elsif method == :delete response = RestClient.delete(url, post_headers) else raise "Unknown method '#{method.to_s}'" end if response.code == 201 #save successful! #store the new eTag for this object #puts response.raw_headers @__jason_etag = response.headers[:Etag] @__jason_timestamp = response.headers[:timestamp] else raise "#{method.to_s.upcase} failed! Could not persist changes" end @__jason_state = :stale end #end object persistence end end