Permalink
Browse files

Add Scope::Relationship which wraps a Core Data relationship set.

  • Loading branch information...
1 parent d56cac1 commit 17976e7ebba63d01c9d10d9211fa550b5d07b8c0 @alloy committed Aug 11, 2012
Showing with 248 additions and 83 deletions.
  1. +1 −2 Rakefile
  2. +0 −7 app/article.rb
  3. +0 −5 app/author.rb
  4. +7 −2 app/schema.rb
  5. +137 −53 app/scope.rb
  6. +1 −1 app/store_coordinator.rb
  7. +19 −0 app/test_models.rb
  8. +83 −13 spec/scope_spec.rb
View
@@ -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 }
View
@@ -1,7 +0,0 @@
-class Article < MotionData::ManagedObject
- #hasOne :author, :class => 'Author'
-
- property :title, String, :required => true
- property :body, String, :required => true
- property :published, Boolean, :default => false
-end
View
@@ -1,5 +0,0 @@
-class Author < MotionData::ManagedObject
- #hasMany :articles, :class => 'Article'
-
- property :name, String, :required => true
-end
View
@@ -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)
@@ -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
@@ -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
View
@@ -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
@@ -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
View
@@ -4,7 +4,7 @@ class << self
attr_accessor :default
def inMemory(schema)
- store NSInMemoryStoreType
+ store schema, NSInMemoryStoreType
end
def onDiskStore(schema, path)
View
@@ -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
Oops, something went wrong.

0 comments on commit 17976e7

Please sign in to comment.