diff --git a/Rakefile b/Rakefile index abf98a6..15c6d95 100644 --- a/Rakefile +++ b/Rakefile @@ -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 } diff --git a/app/article.rb b/app/article.rb deleted file mode 100644 index f9e63e2..0000000 --- a/app/article.rb +++ /dev/null @@ -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 diff --git a/app/author.rb b/app/author.rb index fce8695..e69de29 100644 --- a/app/author.rb +++ b/app/author.rb @@ -1,5 +0,0 @@ -class Author < MotionData::ManagedObject - #hasMany :articles, :class => 'Article' - - property :name, String, :required => true -end diff --git a/app/schema.rb b/app/schema.rb index 4121958..d304a31 100644 --- a/app/schema.rb +++ b/app/schema.rb @@ -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 diff --git a/app/scope.rb b/app/scope.rb index 1a18c29..6e714d3 100644 --- a/app/scope.rb +++ b/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 @@ -34,16 +35,13 @@ 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 @@ -51,64 +49,150 @@ def sortBy(keyPathOrSortDescriptor) # 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 diff --git a/app/store_coordinator.rb b/app/store_coordinator.rb index 59ad80d..b216e76 100644 --- a/app/store_coordinator.rb +++ b/app/store_coordinator.rb @@ -4,7 +4,7 @@ class << self attr_accessor :default def inMemory(schema) - store NSInMemoryStoreType + store schema, NSInMemoryStoreType end def onDiskStore(schema, path) diff --git a/app/test_models.rb b/app/test_models.rb new file mode 100644 index 0000000..fce506c --- /dev/null +++ b/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 diff --git a/spec/scope_spec.rb b/spec/scope_spec.rb index 518c962..16df066 100644 --- a/spec/scope_spec.rb +++ b/spec/scope_spec.rb @@ -8,18 +8,30 @@ def ==(other) end end +class TestScope < MotionData::Scope + def array + @target + end +end + module MotionData describe Scope do - it "initializes with a class target and current context" do - scope = Scope.alloc.initWithTarget(Author) - scope.target.should == Author - scope.context.should == Context.current + it "initializes with a target" do + target = [] + scope = TestScope.alloc.initWithTarget(target) + scope.target.object_id.should == target.object_id end - it "stores a copy of the given sort descriptors" do + it "enumerates over the array version of the scope" do + target = [21, 42] + scope = TestScope.alloc.initWithTarget(target) + scope.map { |object| object }.should == target + end + + it "makes a copy of the given sort descriptors" do descriptors = [Object.new] - scope = Scope.alloc.initWithTarget(Author, predicate:nil, sortDescriptors:descriptors, inContext:nil) + scope = TestScope.alloc.initWithTarget([], predicate:nil, sortDescriptors:descriptors) scope.sortDescriptors.should == descriptors scope.sortDescriptors.object_id.should.not == descriptors.object_id end @@ -139,11 +151,11 @@ module MotionData end end - shared "Scope#set" do + shared "Scope::Set#set" do extend Predicate::Builder::Mixin before do - @scope = Scope.alloc.initWithTarget(@set) + @scope = Scope::Set.alloc.initWithTarget(@set) end it "returns the original set when there are no finder or sort conditions" do @@ -158,16 +170,21 @@ module MotionData it "returns an ordered set if sort conditions have been assigned" do @scope.sortBy(:name).set.should == NSOrderedSet.orderedSetWithArray([@alfred, @appie, @bob]) end + + it "returns an array representation" do + scope = @scope.where(( value(:name) == 'bob' ).or( value(:name) == 'appie' )) + scope.sortBy(:name).map { |object| object }.should == [@appie, @bob] + end end - describe Scope, "#set" do + describe Scope::Set, "#set" do before do @appie = { 'name' => 'appie' } @bob = { 'name' => 'bob' } @alfred = { 'name' => 'alfred' } end - describe Scope, "with a unordered set" do + describe "with a unordered set" do def set(*objects) NSSet.setWithArray(objects) end @@ -176,10 +193,10 @@ def set(*objects) @set = set(@appie, @bob, @alfred) end - behaves_like "Scope#set" + behaves_like "Scope::Set#set" end - describe Scope, "with a ordered set" do + describe "with a ordered set" do def set(*objects) NSOrderedSet.orderedSetWithArray(objects) end @@ -188,7 +205,60 @@ def set(*objects) @set = set(@appie, @bob, @alfred) end - behaves_like "Scope#set" + behaves_like "Scope::Set#set" + end + end + + describe Scope::Relationship do + before do + MotionData.setupCoreDataStackWithInMemoryStore + + @context = Context.context + @context.perform do + @author = Author.new(:name => 'Edgar Allan Poe') + Article.new(:author => @author, :title => 'article1', :published => true) + Article.new(:author => @author, :title => 'article2') + Article.new(:author => @author, :title => 'article3', :published => true) + end + + @articles = Scope::Relationship.alloc.initWithTarget(@author.primitiveValueForKey('articles'), + relationshipName: :articles, + owner:@author, + ownerClass:Author) + end + + it "wraps a Core Data relationship set" do + @articles.set.should == @author.primitiveValueForKey('articles') + + scope = @articles.where(:published => true).sortBy(:title) + scope.map(&:title).should == %w{ article1 article3 } + end + + it "inserts a new instance of the destination entity in the owner's context and associates it to the owner" do + article = @articles.new(:title => 'article4', :published => true) + article.author.should == @author + article.managedObjectContext.should == @context + + scope = @articles.where(:published => true).sortBy(:title) + scope.map(&:title).should == %w{ article1 article3 article4 } + end + + it "returns a NSFetchRequest that represents the scope" do + request = @articles.fetchRequest + request.entity.should == Article.entityDescription + request.sortDescriptors.should == nil + + format = request.predicate.predicateFormat + format.should == NSPredicate.predicateWithFormat('author == %@', argumentArray:[@author]).predicateFormat + + scope = @articles.where(:published => true).sortBy(:title) + request = scope.fetchRequest + request.entity.should == Article.entityDescription + request.sortDescriptors.should == scope.sortDescriptors + + predicate = NSPredicate.predicateWithFormat('author == %@', argumentArray:[@author]) + predicate = predicate.and(scope.predicate) + request.predicate.predicateFormat.should == predicate.predicateFormat end end end