Skip to content

Commit

Permalink
Add Scope::Relationship which wraps a Core Data relationship set.
Browse files Browse the repository at this point in the history
  • Loading branch information
alloy committed Aug 11, 2012
1 parent d56cac1 commit 17976e7
Show file tree
Hide file tree
Showing 8 changed files with 248 additions and 83 deletions.
3 changes: 1 addition & 2 deletions Rakefile
Expand Up @@ -12,8 +12,7 @@ Motion::Project::App.setup do |app|
app/managed_object.rb
app/scope.rb

app/article.rb
app/author.rb
app/test_models.rb
app/app_delegate.rb
}
app.frameworks += %w{ CoreData }
Expand Down
7 changes: 0 additions & 7 deletions app/article.rb

This file was deleted.

5 changes: 0 additions & 5 deletions app/author.rb
@@ -1,5 +0,0 @@
class Author < MotionData::ManagedObject
#hasMany :articles, :class => 'Article'

property :name, String, :required => true
end
9 changes: 7 additions & 2 deletions app/schema.rb
Expand Up @@ -21,6 +21,10 @@ def hasMany(name, options = {})
relationshipDescriptionWithOptions({ :name => name, :maxCount => -1 }.merge(options))
end

def klass
@klass ||= Object.const_get(name)
end

private

def relationshipDescriptionWithOptions(options)
Expand All @@ -35,7 +39,7 @@ def relationshipDescriptionWithOptions(options)
# relationship doesn't exist yet, by the time this relationship is
# defined, if this is the first model that defines a part of this
# relationship.
if inverseName && inverse = rd.destinationEntity.relationshipsByName[inverseName.to_s]
if inverseName && inverse = rd.destinationEntity.relationshipsByName[inverseName]
rd.inverseRelationship = inverse
inverse.inverseRelationship = rd
#puts rd.debugDescription
Expand Down Expand Up @@ -151,12 +155,13 @@ def toRuby

private

# TODO .select { |p| p.is_a?(AttributeDescription) } is needed because we don't serialize relationships yet
def entityToRuby(entity)
%{
s.entity do |e|
e.name = '#{entity.name}'
e.managedObjectClassName = '#{entity.managedObjectClassName}'
#{entity.properties.map { |p| propertyToRuby(p) }.join("\n")}
#{entity.properties.select { |p| p.is_a?(AttributeDescription) }.map { |p| propertyToRuby(p) }.join("\n")}
end
}
end
Expand Down
190 changes: 137 additions & 53 deletions app/scope.rb
@@ -1,16 +1,17 @@
module MotionData
class Scope
attr_reader :target, :predicate, :sortDescriptors, :context
include Enumerable

attr_reader :target, :predicate, :sortDescriptors

def initWithTarget(target)
initWithTarget(target, predicate:nil, sortDescriptors:nil, inContext:nil)
initWithTarget(target, predicate:nil, sortDescriptors:nil)
end

def initWithTarget(target, predicate:predicate, sortDescriptors:sortDescriptors, inContext:context)
def initWithTarget(target, predicate:predicate, sortDescriptors:sortDescriptors)
if init
@target, @predicate = target, predicate
@sortDescriptors = sortDescriptors ? sortDescriptors.dup : []
@context = context || Context.current
end
self
end
Expand All @@ -34,81 +35,164 @@ def where(conditions, *formatArguments)
end

predicate = @predicate.and(predicate) if @predicate
Scope.alloc.initWithTarget(@target,
predicate:predicate,
sortDescriptors:@sortDescriptors,
inContext:@context)
scopeWithPredicate(predicate)
end

# Sort ascending by a key-path, or a NSSortDescriptor.
def sortBy(keyPathOrSortDescriptor)
if keyPathOrSortDescriptor.is_a?(NSSortDescriptor)
addSortDescriptor(keyPathOrSortDescriptor)
scopeByAddingSortDescriptor(keyPathOrSortDescriptor)
else
sortBy(keyPathOrSortDescriptor, ascending:true)
end
end

# Sort by a key-path.
def sortBy(keyPath, ascending:ascending)
addSortDescriptor NSSortDescriptor.alloc.initWithKey(keyPath.to_s, ascending:ascending)
scopeByAddingSortDescriptor(NSSortDescriptor.alloc.initWithKey(keyPath.to_s, ascending:ascending))
end

# Factory methods

# Returns a NSFetchRequest with the current scope.
def request

# Iterates over the array representation of the scope.
def each(&block)
array.each(&block)
end

# Executes the request and returns the results as a set.
def set
set = @target

if @predicate
if set.is_a?(NSOrderedSet)
# TODO not the most efficient way of doing this when there are also sort descriptors
filtered = set.array.filteredArrayUsingPredicate(@predicate)
set = NSOrderedSet.orderedSetWithArray(filtered)
else
set = set.filteredSetUsingPredicate(@predicate)
end
end

unless @sortDescriptors.empty?
set = set.set if set.is_a?(NSOrderedSet)
sorted = set.sortedArrayUsingDescriptors(@sortDescriptors)
set = NSOrderedSet.orderedSetWithArray(sorted)
end
# Factory methods that should be implemented by the subclass.

set
def array
raise "Not implemented"
end
alias_method :to_a, :array

# Returns a NSFetchedResultsController with this fetch request.
def controller(options = {})
NSFetchedResultsController.alloc.initWithFetchRequest(self,
managedObjectContext:MotionData::Context.current,
sectionNameKeyPath:options[:sectionNameKeyPath],
cacheName:options[:cacheName])
def set
raise "Not implemented."
end

private

def addSortDescriptor(sortDescriptor)
def scopeWithPredicate(predicate)
scopeWithPredicate(predicate, sortDescriptors:@sortDescriptors)
end

def scopeByAddingSortDescriptor(sortDescriptor)
sortDescriptors = @sortDescriptors.dup
sortDescriptors << sortDescriptor
Scope.alloc.initWithTarget(@target,
predicate:@predicate,
sortDescriptors:sortDescriptors,
inContext:@context)
scopeWithPredicate(@predicate, sortDescriptors:sortDescriptors)
end

def scopeWithPredicate(predicate, sortDescriptors:sortDescriptors)
self.class.alloc.initWithTarget(@target, predicate:predicate, sortDescriptors:sortDescriptors)
end
end

class Scope
class Set < Scope
def set
setByApplyingConditionsToSet(@target)
end

def array
set = self.set
set.is_a?(NSOrderedSet) ? set.array : set.allObjects
end

private

# Applies the finder and sort conditions and returns the result as a set.
def setByApplyingConditionsToSet(set)
if @predicate
if set.is_a?(NSOrderedSet)
# TODO not the most efficient way of doing this when there are also sort descriptors
filtered = set.array.filteredArrayUsingPredicate(@predicate)
set = NSOrderedSet.orderedSetWithArray(filtered)
else
set = set.filteredSetUsingPredicate(@predicate)
end
end

unless @sortDescriptors.empty?
set = set.set if set.is_a?(NSOrderedSet)
sorted = set.sortedArrayUsingDescriptors(@sortDescriptors)
set = NSOrderedSet.orderedSetWithArray(sorted)
end

set
end
end
end

class Scope
class Relationship < Scope::Set
attr_accessor :relationshipName, :owner, :ownerClass

def initWithTarget(target, relationshipName:relationshipName, owner:owner, ownerClass:ownerClass)
if initWithTarget(target)
@relationshipName, @owner, @ownerClass = relationshipName, owner, ownerClass
end
self
end

def new(properties = nil)
entity = targetClass.newInContext(@owner.managedObjectContext, properties)
# Uses the Core Data dynamically generated method to add objects to the relationship.
#
# E.g. if the relationship is called 'articles', then this will call: addArticles()
#
# TODO we currently use the one that takes a set instead of just one object, this is
# so we don't yet have to do any singularization
camelized = @relationshipName.to_s
camelized[0] = camelized[0,1].upcase
@owner.send("add#{camelized}", NSSet.setWithObject(entity))
entity
end

# Returns a NSFetchRequest with the current scope.
def fetchRequest
# Start with a predicate which selects those entities that belong to the owner.
predicate = Predicate::Builder.new(inverseRelationshipName) == @owner
# Then apply the scope's predicate.
predicate = predicate.and(@predicate) if @predicate

request = NSFetchRequest.new
request.entity = targetClass.entityDescription
request.predicate = predicate
request.sortDescriptors = @sortDescriptors unless @sortDescriptors.empty?
request
end

# Returns a NSFetchedResultsController with this fetch request.
def controller(options = {})
NSFetchedResultsController.alloc.initWithFetchRequest(self,
managedObjectContext:MotionData::Context.current,
sectionNameKeyPath:options[:sectionNameKeyPath],
cacheName:options[:cacheName])
end

private

def relationshipDescription
@ownerClass.entityDescription.relationshipsByName[@relationshipName]
end

def targetEntityDescription
relationshipDescription.destinationEntity
end

#class ToManyRelationship < Scope
## Returns the relationship set, normally provided by a Core Data to-many
## relationship.
#def set

#end
#end
def targetClass
targetEntityDescription.klass
end

def inverseRelationshipName
relationshipDescription.inverseRelationship.name
end

def scopeWithPredicate(predicate, sortDescriptors:sortDescriptors)
scope = super
scope.relationshipName = @relationshipName
scope.owner = @owner
scope.ownerClass = @ownerClass
scope
end
end
end
end
2 changes: 1 addition & 1 deletion app/store_coordinator.rb
Expand Up @@ -4,7 +4,7 @@ class << self
attr_accessor :default

def inMemory(schema)
store NSInMemoryStoreType
store schema, NSInMemoryStoreType
end

def onDiskStore(schema, path)
Expand Down
19 changes: 19 additions & 0 deletions app/test_models.rb
@@ -0,0 +1,19 @@
class Author < MotionData::ManagedObject
end

class Article < MotionData::ManagedObject
end

class Author
hasMany :articles, :destinationEntity => Article.entityDescription, :inverse => :author

property :name, String, :required => true
end

class Article
hasOne :author, :destinationEntity => Author.entityDescription, :inverse => :articles

property :title, String, :required => true
property :body, String, :required => true
property :published, Boolean, :default => false
end

0 comments on commit 17976e7

Please sign in to comment.