Skip to content

Instantly share code, notes, and snippets.

@joenoon
Created January 17, 2014 09:58
Show Gist options
  • Save joenoon/8470866 to your computer and use it in GitHub Desktop.
Save joenoon/8470866 to your computer and use it in GitHub Desktop.
Rubymotion wrapper for YapDatabase. Tested on 1.2.2. Makes some assumptions about your objects, currently not abstract enough to be a gem. See the top of the file for some example usage.
# Copyright (c) 2013 Joe Noon (https://github.com/joenoon)
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
# example usage:
# class MyModel < YapModel
#
# attributes :_id, # currently required
# :_type, # currently required
# :title
#
# has_many :objects
# has_one :user
#
# end
#
# This is not abstract enough right now to turn it into a gem that anyone could just
# drop into their app and use. It assumes your json api returns some specific
# attributes (_id and _type) to define the keys it will store ("#{_type}:#{_id}")
# and to make the casting work. It assumes you want your database to be yap.db.
# It probably doesn't make use of all the awesome features YapDatabase has to offer,
# especially in the 2.X branch, etc.
#
# YapModel.cast({ "_id" => "abc", "_type" => "MyModel", "title" => "Test" }) #=> #<MyModel:0x...>
# YapModel.cast(1) #=> 1 (its safe to try and cast anything)
# casting is recursive. If you have an embedded "user" key that conforms to
# the assumed _id and _type spec, it will become its own User object, and only
# a key will be stored in the MyModel object internally by YapDatabase
#
class YapModel
class Database
attr_accessor :database,
:dbconnection,
:memory,
:memory_queue,
:cast_queue
def initialize(db_path)
@database = YapDatabase.alloc.initWithPath(db_path)
@dbconnection = @database.newConnection
# stores objects in memory, to form an identity map, so the same
# object (by key) is not instantiated twice.
@memory = NSMapTable.weakToWeakObjectsMapTable
# a queue which all access to @memory will use
@memory_queue = Dispatch::Queue.new("#{NSBundle.mainBundle.bundleIdentifier}.YapModel.memory")
# a queue which casting will use
@cast_queue = Dispatch::Queue.new("#{NSBundle.mainBundle.bundleIdentifier}.YapModel.cast")
self
end
def self.shared
Dispatch.once { @shared = new(NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, true)[0] + "/yap.db") }
@shared
end
def set_in_memory(key, val)
return unless key
memory_queue.sync do
unless memory.objectForKey(key)
memory.setObject(val, forKey:key)
end
end
nil
end
def get_in_memory(key)
memory_queue.sync do
return memory.objectForKey(key)
end
end
def memory_count
memory_queue.sync do
return memory.dictionaryRepresentation.values.compact.size
end
end
def setObject(object, forKey:key)
# we use a clone of the object to pass off to YapDatabase to persist,
# otherwise, the object gets permanently trapped in memory.
clone_object = object.clone
dbconnection.readWriteWithBlock(lambda do |transaction|
transaction.setObject(clone_object, forKey:key)
end)
end
def objectForKey(key)
model = nil
dbconnection.readWithBlock(lambda do |transaction|
model = transaction.objectForKey(key)
end)
model
end
def objectsForKeys(keys)
models = []
dbconnection.readWithBlock(lambda do |transaction|
transaction.enumerateObjects(lambda do |keyIndex, object, stop|
models.push(object)
end, forKeys:keys)
end)
models
end
def removeObjectForKey(key)
dbconnection.readWriteWithBlock(lambda do |transaction|
transaction.removeObjectForKey(key)
end)
end
def removeAllObjects
p "WARNING: removeAllObjects"
dbconnection.readWriteWithBlock(lambda do |transaction|
transaction.removeAllObjects
end)
end
end
# instance methods
def key
@key ||= self.class.key_for_id(_id)
end
def attributes=(attrs=nil)
return self unless attrs
keys = [] + attributes_to_use + attributes_to_serialize
while key = keys.pop
val = nil
if attrs.key?(key) # sym is always stored in the attributes_to_use array
val = attrs[key]
elsif attrs.key?(key.to_s) # string
val = attrs[key.to_s]
else
next
end
casted = self.class._cast(val)
send("#{key}=", casted)
end
self
end
def attributes_to_serialize
self.class.real_class.attributes_to_serialize
end
def attributes_to_use
self.class.real_class.attributes_to_use
end
def attributes
keys = [] + attributes_to_use
res = {}
while key = keys.pop
res[key] = send(key)
end
res
end
def initialize(attrs=nil)
self.database = attrs.delete(:database)
self.attributes = attrs
database.set_in_memory(key, self)
self
end
def initWithCoder(decoder)
attrs = {}
keys = [] + attributes_to_serialize
while key = keys.pop
attrs[key] = decoder.decodeObjectForKey(key.to_s)
end
initialize(attrs)
self
end
def encodeWithCoder(encoder)
keys = [] + attributes_to_serialize
while key = keys.pop
encoder.encodeObject(send(key), forKey: key.to_s)
end
encoder
end
def ==(another_object)
return true if object_id == another_object.object_id
# p "YapModel ==", self.class.name, another_object.class.name
return true if another_object.respond_to?(:key) && (key == another_object.key)
false
end
attr_writer :database
def database
@database || self.class.database
end
def reload
if obj = self.class.find_key(key)
self.attributes = obj.attributes
end
self
end
def save
return false unless key
database.setObject(self, forKey:key)
true
end
def destroy
return false unless key
database.removeObjectForKey(key)
true
end
# class methods
def self.database
Database.shared
end
def self.wipeAll!
database.removeAllObjects
end
def self.keys_for(key) # keys_for :place
key_keys = "#{key}_keys".to_sym
key_objects = "#{key}_objects".to_sym
real_class.attributes_to_serialize.push(key_keys)
real_class.attributes_to_use.push(key.to_sym)
real_class.send(:define_method, key_keys) do # #place_keys
i = instance_variable_get("@#{key_keys}") # @place_keys
return i if i
x = []
instance_variable_set("@#{key_keys}", x)
x
end
real_class.send(:define_method, "#{key_keys}=") do |keys| # #place_keys=(x)
instance_variable_set("@#{key_keys}", keys)
instance_variable_set("@#{key_objects}", nil)
keys
end
real_class.send(:define_method, key_objects) do # #place_objects
i = instance_variable_get("@#{key_objects}") # @place_objects
return i if i
x = send(key_keys)
models = self.class.find_keys(x)
instance_variable_set("@#{key_objects}", models)
models
end
end
def self.has_one(key)
keys_for(key)
define_method(key) do
send("#{key}_objects").first
end
define_method("#{key}=") do |model|
# p "#{key}= 1"
keys = [ model ].compact.map(&:key).compact
# p "#{key}= 2"
send("#{key}_keys=", keys)
model
end
end
def self.has_many(key)
keys_for(key)
define_method(key) do
send("#{key}_objects")
end
define_method("#{key}=") do |models|
# p "#{key}= 3", models, caller
keys = (models || []).compact.map(&:key).compact
# p "#{key}= 4"
send("#{key}_keys=", keys)
models
end
end
def self.find_by_id(_id)
if key = key_for_id(_id)
find_key(key)
end
end
def self.find_key(key)
model = nil
if model = database.get_in_memory(key)
# p "returning model in memory", key
return model
end
model = database.objectForKey(key)
# p "find_key", key, model
model
end
def self.find_all(_ids)
keys = _ids.map { |x| key_for_id(x) }.compact
find_keys(keys)
end
def self.find_keys(keys)
keys_not_in_memory = []
models = []
keys.each do |k|
if m = database.get_in_memory(k)
# p "returning model in memory", k
models.push(m)
else
keys_not_in_memory.push(k)
end
end
if keys_not_in_memory.size > 0
models += database.objectsForKeys(keys_not_in_memory)
# p "find_keys", keys, models
end
arr = []
models.each do |model|
if idx = keys.index(model.key)
arr[idx] = model
end
end
arr
end
def self._cast(attributes_array_or_object)
if attributes_array_or_object.is_a?(Array)
return attributes_array_or_object.map {|x| _cast(x) }
elsif attributes_array_or_object.is_a?(Hash)
attrs = attributes_array_or_object
if (type = attrs["_type"] || attrs[:_type]) && (id = attrs["_id"] || attrs[:_id])
if model_klass = Module.const_get(type)
if existing = model_klass.find_by_id(id)
# p "existing", existing.key
existing.attributes = attrs
existing.save
return existing
else
model = model_klass.new(attrs)
# p "new", model.key
model.save
return model
end
end
else
casted_attrs = {}
attrs.keys.each do |key|
casted_attrs[key] = _cast(attrs[key])
end
return casted_attrs
end
end
attributes_array_or_object
end
def self.cast(attributes_array_or_object)
database.cast_queue.sync do
return _cast(attributes_array_or_object)
end
end
def self.attributes_to_serialize
@attributes_to_serialize ||= []
end
def self.attributes_to_use
@attributes_to_use ||= []
end
# prevent NSKVONotifying_ prefix on class name from automatic subclass
def self.model_name
@model_name ||= self.superclass == YapModel ? name : superclass.name
end
def self.real_class
@real_class ||= Module.const_get(model_name)
end
def self.attributes(*args)
args.flatten.compact.each do |key|
real_class.attributes_to_serialize.push(key.to_sym)
real_class.attributes_to_use.push(key.to_sym)
real_class.send(:attr_accessor, key)
end
end
def self.key_for_id(_id)
_id ? "#{model_name}:#{_id}" : nil
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment