├── resources ├── KEEPME └── Default-568h@2x.png ├── .yardopts ├── lib ├── cdq │ ├── version.rb │ ├── generators.rb │ └── cli.rb └── cdq.rb ├── templates ├── model │ ├── app │ │ └── models │ │ │ └── name.rb │ └── spec │ │ └── models │ │ └── name.rb └── init │ ├── spec │ └── helpers │ │ └── cdq.rb │ └── schemas │ └── 0001_initial.rb ├── Gemfile ├── bin └── cdq ├── vendor └── cdq │ └── ext │ ├── CoreDataQueryManagedObjectBase.h │ └── CoreDataQueryManagedObjectBase.m ├── motion ├── cdq │ ├── deprecation.rb │ ├── object_proxy.rb │ ├── collection_proxy.rb │ ├── model.rb │ ├── store.rb │ ├── object.rb │ ├── partial_predicate.rb │ ├── relationship_query.rb │ ├── config.rb │ ├── query.rb │ ├── targeted_query.rb │ └── context.rb ├── cdq.rb └── managed_object.rb ├── .gitignore ├── app ├── app_delegate.rb └── test_models.rb ├── spec ├── cdq │ ├── model_spec.rb │ ├── object_proxy_spec.rb │ ├── store_spec.rb │ ├── module_spec.rb │ ├── object_spec.rb │ ├── calculation_spec.rb │ ├── partial_predicate_spec.rb │ ├── collection_proxy_spec.rb │ ├── config_spec.rb │ ├── context_spec.rb │ ├── targeted_query_spec.rb │ ├── query_spec.rb │ ├── relationship_query_spec.rb │ └── managed_object_spec.rb ├── helpers │ └── thread_helper.rb ├── thread_spec.rb ├── integration_spec.rb └── timestamp_spec.rb ├── Rakefile ├── .travis.yml ├── cdq.gemspec ├── LICENSE ├── schemas └── 001_baseline.rb └── README.md /resources/KEEPME: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --no-private motion/**/*.rb 2 | -------------------------------------------------------------------------------- /lib/cdq/version.rb: -------------------------------------------------------------------------------- 1 | 2 | module CDQ 3 | VERSION = '2.0.0' 4 | end 5 | -------------------------------------------------------------------------------- /templates/model/app/models/name.rb: -------------------------------------------------------------------------------- 1 | class <%= @name_camel_case %> < CDQManagedObject 2 | 3 | end 4 | -------------------------------------------------------------------------------- /resources/Default-568h@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kemiller/cdq/HEAD/resources/Default-568h@2x.png -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | gem 'rake' 6 | gem 'motion-stump', :group => :spec 7 | -------------------------------------------------------------------------------- /templates/init/spec/helpers/cdq.rb: -------------------------------------------------------------------------------- 1 | 2 | module Bacon 3 | class Context 4 | include CDQ 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /bin/cdq: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "erb" 4 | require "optparse" 5 | require "rubygems" 6 | require "cdq/cli" 7 | 8 | CDQ::CommandLine.run_all 9 | 10 | -------------------------------------------------------------------------------- /vendor/cdq/ext/CoreDataQueryManagedObjectBase.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface CoreDataQueryManagedObjectBase : NSManagedObject 4 | 5 | - (id)relationshipByName:(NSString *)name; 6 | + (void)defineRelationshipMethod:(NSString *)name; 7 | 8 | @end 9 | -------------------------------------------------------------------------------- /motion/cdq/deprecation.rb: -------------------------------------------------------------------------------- 1 | 2 | module CDQ 3 | module Deprecation 4 | 5 | class << self 6 | attr_accessor :silence_deprecation 7 | end 8 | 9 | def deprecate(message) 10 | puts message unless CDQ::Deprecation.silence_deprecation 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .repl_history 2 | .*.sw? 3 | build 4 | vendor/cdq/ext/build-* 5 | vendor/cdq/ext/ext.bridgesupport 6 | resources/*.nib 7 | resources/*.momd 8 | resources/*.storyboardc 9 | resources/*.xcdatamodeld 10 | examples/**/*.nib 11 | tags 12 | .rbenv-version 13 | pkg 14 | .yardoc 15 | *.gem 16 | Gemfile.lock 17 | -------------------------------------------------------------------------------- /app/app_delegate.rb: -------------------------------------------------------------------------------- 1 | class AppDelegate 2 | include CDQ 3 | 4 | # OS X entry point 5 | def applicationDidFinishLaunching(notification) 6 | cdq.setup 7 | end 8 | 9 | # iOS entry point 10 | def application(application, didFinishLaunchingWithOptions:launchOptions) 11 | cdq.setup 12 | true 13 | end 14 | end 15 | 16 | class TopLevel 17 | include CDQ 18 | end 19 | 20 | -------------------------------------------------------------------------------- /templates/model/spec/models/name.rb: -------------------------------------------------------------------------------- 1 | describe '<%= @name_camel_case %>' do 2 | 3 | before do 4 | class << self 5 | include CDQ 6 | end 7 | cdq.setup 8 | end 9 | 10 | after do 11 | cdq.reset! 12 | end 13 | 14 | it 'should be a <%= @name_camel_case %> entity' do 15 | <%= @name_camel_case %>.entity_description.name.should == '<%= @name_camel_case %>' 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/cdq/model_spec.rb: -------------------------------------------------------------------------------- 1 | 2 | module CDQ 3 | 4 | describe "CDQ Model Manager" do 5 | 6 | it "can be created with a default name" do 7 | @mm = CDQModelManager.new 8 | @mm.current.class.should == NSManagedObjectModel 9 | end 10 | 11 | it 'should log models' do 12 | @mm = CDQModelManager.new 13 | @mm.log(:string).should != nil 14 | end 15 | 16 | end 17 | 18 | end 19 | 20 | -------------------------------------------------------------------------------- /spec/helpers/thread_helper.rb: -------------------------------------------------------------------------------- 1 | module Bacon 2 | class Context 3 | # Executes a given block on an async concurrent GCD queue (which is a 4 | # different thread) and returns the return value of the block, which is 5 | # expected to be either true or false. 6 | def on_thread(&block) 7 | @result = false 8 | group = Dispatch::Group.new 9 | Dispatch::Queue.concurrent.async(group) do 10 | @result = block.call 11 | end 12 | group.wait 13 | @result 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/thread_spec.rb: -------------------------------------------------------------------------------- 1 | describe "multi threading" do 2 | 3 | before do 4 | class << self 5 | include CDQ 6 | end 7 | end 8 | 9 | it 'should word background' do 10 | Author.count.should == 0 11 | 12 | parent = cdq.contexts.current 13 | Dispatch::Queue.concurrent.sync do 14 | cdq.contexts.push(parent) 15 | cdq.contexts.push(NSConfinementConcurrencyType) do 16 | Author.create(name:"George") 17 | @result = cdq.save 18 | end 19 | end 20 | Author.count.should == 1 21 | end 22 | 23 | end 24 | -------------------------------------------------------------------------------- /templates/init/schemas/0001_initial.rb: -------------------------------------------------------------------------------- 1 | 2 | schema "0001 initial" do 3 | 4 | # Examples: 5 | # 6 | # entity "Person" do 7 | # string :name, optional: false 8 | # 9 | # has_many :posts 10 | # end 11 | # 12 | # entity "Post" do 13 | # string :title, optional: false 14 | # string :body 15 | # 16 | # datetime :created_at 17 | # datetime :updated_at 18 | # 19 | # has_many :replies, inverse: "Post.parent" 20 | # belongs_to :parent, inverse: "Post.replies" 21 | # 22 | # belongs_to :person 23 | # end 24 | 25 | end 26 | -------------------------------------------------------------------------------- /vendor/cdq/ext/CoreDataQueryManagedObjectBase.m: -------------------------------------------------------------------------------- 1 | #import "CoreDataQueryManagedObjectBase.h" 2 | #import 3 | 4 | @implementation CoreDataQueryManagedObjectBase 5 | 6 | - (id)relationshipByName:(NSString *)name; 7 | { 8 | // should be overriden by the subclass 9 | printf("Unimplemented\n"); 10 | abort(); 11 | return nil; 12 | } 13 | 14 | + (void)defineRelationshipMethod:(NSString *)name; 15 | { 16 | IMP imp = imp_implementationWithBlock(^id(CoreDataQueryManagedObjectBase *entity) { 17 | return [entity relationshipByName:name]; 18 | }); 19 | class_addMethod([self class], NSSelectorFromString(name), imp, "@@:"); 20 | } 21 | 22 | @end 23 | -------------------------------------------------------------------------------- /app/test_models.rb: -------------------------------------------------------------------------------- 1 | class Author < CDQManagedObject 2 | end 3 | 4 | class Article < CDQManagedObject 5 | scope :clashing, where(:title).eq('war & peace') 6 | scope :all_published, where(:published).eq(true) 7 | scope :with_title, where(:title).ne(nil).sort_by(:title, order: :descending) 8 | scope :published_since { |date| where(:publishedAt).ge(date) } 9 | end 10 | 11 | class Citation < CDQManagedObject 12 | end 13 | 14 | class Writer < CDQManagedObject 15 | scope :clashing, where(:fee).eq(42.0) 16 | end 17 | 18 | class Timestamp < CDQManagedObject 19 | end 20 | 21 | class Discussion < CDQManagedObject 22 | end 23 | 24 | class Message < CDQManagedObject 25 | end 26 | -------------------------------------------------------------------------------- /motion/cdq/object_proxy.rb: -------------------------------------------------------------------------------- 1 | 2 | module CDQ 3 | class CDQObjectProxy < CDQObject 4 | 5 | def initialize(object) 6 | @object = object 7 | end 8 | 9 | def get 10 | @object 11 | end 12 | 13 | def respond_to?(method) 14 | super(method) || @object.entity.relationshipsByName[method] 15 | end 16 | 17 | def method_missing(*args) 18 | if @object.entity.relationshipsByName[args.first] 19 | CDQRelationshipQuery.new(@object, args.first) 20 | else 21 | super(*args) 22 | end 23 | end 24 | 25 | def destroy 26 | @object.managedObjectContext.deleteObject(@object) 27 | end 28 | end 29 | end 30 | 31 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | $:.unshift("/Library/RubyMotion/lib") 3 | $:.unshift("~/.rubymotion/rubymotion-templates") 4 | 5 | platform = ENV.fetch('platform', 'ios') 6 | require "motion/project/template/#{platform}" 7 | require 'bundler/setup' 8 | require 'motion/project/template/gem/gem_tasks' 9 | 10 | if ARGV.join(' ') =~ /spec/ 11 | Bundler.require :default, :spec 12 | else 13 | Bundler.require 14 | end 15 | 16 | require 'cdq' 17 | require 'motion-stump' 18 | require 'ruby-xcdm' 19 | require 'motion-yaml' 20 | 21 | Motion::Project::App.setup do |app| 22 | # Use `rake config' to see complete project settings. 23 | app.name = 'CDQ' 24 | app.vendor_project('vendor/cdq/ext', :static) 25 | end 26 | 27 | task :"build:simulator" => :"schema:build" 28 | -------------------------------------------------------------------------------- /motion/cdq/collection_proxy.rb: -------------------------------------------------------------------------------- 1 | 2 | module CDQ 3 | class CDQCollectionProxy < CDQTargetedQuery 4 | 5 | def initialize(objects, entity_description) 6 | @objects = objects 7 | super(entity_description, constantize(entity_description.managedObjectClassName)) 8 | @predicate = self.where("%@ CONTAINS SELF", @objects).predicate 9 | end 10 | 11 | def count 12 | @objects.size 13 | end 14 | alias :length :count 15 | alias :size :count 16 | 17 | def get 18 | @objects 19 | end 20 | 21 | def array 22 | @objects 23 | end 24 | 25 | def first(n = 1) 26 | n == 1 ? @objects.first : @objects.first(n) 27 | end 28 | 29 | def last(n = 1) 30 | n == 1 ? @objects.last : @objects.last(n) 31 | end 32 | 33 | end 34 | end 35 | 36 | -------------------------------------------------------------------------------- /spec/cdq/object_proxy_spec.rb: -------------------------------------------------------------------------------- 1 | 2 | module CDQ 3 | describe "CDQ Object Proxy" do 4 | 5 | before do 6 | class << self 7 | include CDQ 8 | end 9 | 10 | cdq.setup 11 | 12 | @author = Author.create(name: "Stephen King") 13 | @article = Article.create(title: "IT", author: @author) 14 | 15 | @op = CDQObjectProxy.new(@author) 16 | end 17 | 18 | after do 19 | cdq.reset! 20 | end 21 | 22 | it "wraps an NSManagedObject" do 23 | @op.get.should == @author 24 | end 25 | 26 | it "wraps relations in CDQRelationshipQuery objects" do 27 | @op.articles.class.should == CDQRelationshipQuery 28 | @op.articles.first.should == @article 29 | end 30 | 31 | it "can delete the underlying object" do 32 | @op.destroy 33 | Author.count.should == 0 34 | end 35 | 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/cdq.rb: -------------------------------------------------------------------------------- 1 | 2 | unless defined?(Motion::Project::App) 3 | raise "This must be required from within a RubyMotion Rakefile" 4 | end 5 | 6 | require 'ruby-xcdm' 7 | require 'yaml' 8 | require 'motion-yaml' 9 | 10 | ENV['COLUMNS'] ||= `tput cols`.strip 11 | 12 | Motion::Project::App.setup do |app| 13 | parent = File.join(File.dirname(__FILE__), '..') 14 | app.files.unshift(Dir.glob(File.join(parent, "motion/cdq/**/*.rb"))) 15 | app.files.unshift(Dir.glob(File.join(parent, "motion/*.rb"))) 16 | app.frameworks += %w{ CoreData } 17 | app.vendor_project(File.join(parent, 'vendor/cdq/ext'), :static) 18 | if app.respond_to?(:xcdm) 19 | cdqfile = File.join(app.project_dir, 'resources/cdq.yml') 20 | if File.exists?(cdqfile) 21 | hash = YAML.load(File.read(cdqfile)) 22 | if hash 23 | app.xcdm.name = hash['model_name'] || hash['name'] 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | os: osx 2 | osx_image: xcode11.2 3 | env: 4 | global: 5 | - RUBYMOTION_LICENSE=1dcac45cc434293009f74b33037bdf7361a3a1ff # Official license key for open-source projects 6 | - TMP_DIR=./tmp # For motion repo, so it doesn't attempt to use /tmp, to which it has no access 7 | before_install: 8 | - wget http://travisci.rubymotion.com/ -O RubyMotion-TravisCI.pkg 9 | - sudo installer -pkg RubyMotion-TravisCI.pkg -target / 10 | - cp -r /usr/lib/swift/*.dylib /Applications/Xcode.app/Contents/Frameworks/ 11 | - touch /Applications/Xcode.app/Contents/Frameworks/.swift-5-staged 12 | - sudo mkdir -p ~/Library/RubyMotion/build 13 | - sudo chown -R travis ~/Library/RubyMotion 14 | - eval "sudo motion activate $RUBYMOTION_LICENSE" 15 | - sudo motion update && motion repo 16 | - bundle install 17 | - bundle exec rake clean 18 | gemfile: 19 | - Gemfile 20 | script: 21 | - bundle exec rake spec 22 | - bundle exec rake spec platform=osx 23 | -------------------------------------------------------------------------------- /spec/cdq/store_spec.rb: -------------------------------------------------------------------------------- 1 | 2 | module CDQ 3 | 4 | describe "CDQ Store Manager" do 5 | 6 | before do 7 | CDQ.cdq.setup 8 | @sm = CDQStoreManager.new(model_manager: CDQ.cdq.models) 9 | end 10 | 11 | after do 12 | CDQ.cdq.reset! 13 | end 14 | 15 | it "can set up a store coordinator with default name" do 16 | @sm.current.should != nil 17 | @sm.current.class.should == NSPersistentStoreCoordinator 18 | end 19 | 20 | it "rejects attempt to create without a valid model" do 21 | c = CDQConfig.new(name: "foo") 22 | mm = CDQModelManager.new(config: c) 23 | sm = CDQStoreManager.new(config: c, model_manager: mm) 24 | should.raise do 25 | sm.current 26 | end 27 | end 28 | 29 | it "permits setting custom store manager" do 30 | nsm = CDQStoreManager.new(model: nil) 31 | should.not.raise do 32 | nsm.current = @sm.current 33 | nsm.current 34 | end 35 | end 36 | 37 | end 38 | 39 | end 40 | -------------------------------------------------------------------------------- /cdq.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | require File.expand_path('../lib/cdq/version', __FILE__) 3 | 4 | Gem::Specification.new do |gem| 5 | gem.authors = ["infinitered", "kemiller"] 6 | gem.email = ["ken@infinitered.com"] 7 | gem.description = "Core Data Query for RubyMotion" 8 | gem.summary = "A streamlined library for working with Core Data outside XCode" 9 | gem.homepage = "http://infinitered.com/cdq" 10 | gem.license = 'MIT' 11 | 12 | files = [] 13 | files << 'README.md' 14 | files << 'LICENSE' 15 | files.concat(Dir.glob('lib/**/*.rb')) 16 | files.concat(Dir.glob('motion/**/*.rb')) 17 | files.concat(Dir.glob('templates/**/*.rb')) 18 | files.concat(Dir.glob('vendor/**/*.{rb,m,h}')) 19 | gem.files = files 20 | gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) 21 | gem.name = "cdq" 22 | gem.require_paths = ["lib"] 23 | gem.add_runtime_dependency 'ruby-xcdm', '>= 0.0.9' 24 | gem.add_runtime_dependency 'motion-yaml', '>= 1.6' 25 | gem.executables << 'cdq' 26 | 27 | gem.version = CDQ::VERSION 28 | end 29 | -------------------------------------------------------------------------------- /spec/cdq/module_spec.rb: -------------------------------------------------------------------------------- 1 | 2 | module CDQ 3 | 4 | describe "CDQ Magic Method" do 5 | 6 | before do 7 | class << Author 8 | include CDQ 9 | end 10 | 11 | class << self 12 | include CDQ 13 | end 14 | end 15 | 16 | it "wraps an NSManagedObject class in a CDQTargetedQuery" do 17 | cdq(Author).class.should == CDQTargetedQuery 18 | end 19 | 20 | it "treats a string as an entity name and returns a CDQTargetedQuery" do 21 | cdq('Author').class.should == CDQTargetedQuery 22 | end 23 | 24 | it "treats a symbol as an attribute key and returns a CDQPartialPredicate" do 25 | cdq(:name).class.should == CDQPartialPredicate 26 | end 27 | 28 | it "passes through existing CDQObjects unchanged" do 29 | query = CDQQuery.new 30 | cdq(query).should == query 31 | end 32 | 33 | it "uses 'self' if no object passed in" do 34 | Author.cdq.class.should == CDQTargetedQuery 35 | end 36 | 37 | it "works with entities that do not have a specific implementation class" do 38 | cdq('Publisher').class.should == CDQTargetedQuery 39 | end 40 | 41 | end 42 | 43 | end 44 | 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 InfiniteRed LLC (http://infinitered.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /spec/integration_spec.rb: -------------------------------------------------------------------------------- 1 | 2 | describe "Integration Tests" do 3 | 4 | before do 5 | 6 | class << self 7 | include CDQ 8 | end 9 | 10 | cdq.setup 11 | 12 | @author = Author.create(name: "Albert Einstein") 13 | 14 | @fundamentals = @author.articles.create(body: "...", published: true, publishedAt: Time.local(1940), 15 | title: "Considerations concering the fundamentals of theoretical physics") 16 | 17 | @gravitation = @author.articles.create(body: "...", published: true, publishedAt: Time.local(1937), 18 | title: "On gravitational waves") 19 | 20 | @fcite = @fundamentals.citations.create(journal: "Science", timestamp: Time.local(1940)) 21 | @gcite = @gravitation.citations.create(journal: "Nature", timestamp: Time.local(1941)) 22 | 23 | cdq.save(always_wait: true) 24 | end 25 | 26 | after do 27 | cdq.reset! 28 | end 29 | 30 | it "should be able to combine simple queries" do 31 | @author.articles.count.should == 2 32 | @author.articles.first.citations.count.should == 1 33 | @author.articles.where(:title).matches('.*fundamentals.*').first.citations.array.should == [@fcite] 34 | 35 | @gcite.article.author.should == @author 36 | end 37 | 38 | end 39 | -------------------------------------------------------------------------------- /schemas/001_baseline.rb: -------------------------------------------------------------------------------- 1 | 2 | schema "0.0.1" do 3 | 4 | entity "Article" do 5 | 6 | string :body, optional: false 7 | integer32 :length 8 | boolean :published, default: false 9 | datetime :publishedAt, default: false 10 | string :title, optional: false 11 | string :title2 12 | 13 | belongs_to :author 14 | has_many :citations 15 | end 16 | 17 | entity "Author" do 18 | string :name, optional: false 19 | float :fee 20 | has_many :articles 21 | end 22 | 23 | entity "Writer" do 24 | string :name, optional: false 25 | float :fee 26 | 27 | has_many :spouses, inverse: "Spouse.writers", ordered: true 28 | end 29 | 30 | entity "Spouse", class_name: "CDQManagedObject" do 31 | string :name, optional: true 32 | has_many :writers, inverse: "Writer.spouses" 33 | end 34 | 35 | entity "Publisher", class_name: "CDQManagedObject" do 36 | string :name, optional: false 37 | end 38 | 39 | entity "Citation" do 40 | string :journal 41 | datetime :timestamp 42 | belongs_to :article 43 | end 44 | 45 | entity "Timestamp" do 46 | boolean :flag 47 | datetime :created_at 48 | datetime :updated_at 49 | end 50 | 51 | entity "Discussion" do 52 | has_many :messages, deletionRule: "Cascade" 53 | string :name 54 | end 55 | 56 | entity "Message" do 57 | belongs_to :discussion 58 | string :content 59 | end 60 | 61 | end 62 | -------------------------------------------------------------------------------- /spec/timestamp_spec.rb: -------------------------------------------------------------------------------- 1 | 2 | describe "Timestamp Tests" do 3 | 4 | before do 5 | 6 | class << self 7 | include CDQ 8 | end 9 | 10 | cdq.setup 11 | 12 | @now = Time.new(2014, 6, 2, 0, 0, 0) 13 | @after_1_sec = Time.new(2014, 6, 2, 0, 0, 1) 14 | 15 | Time.stub!(:now, :return => @now) 16 | 17 | @timestamp = Timestamp.create 18 | end 19 | 20 | after do 21 | cdq.reset! 22 | end 23 | 24 | it "should be nil initally" do 25 | @timestamp.created_at.should == nil 26 | @timestamp.updated_at.should == nil 27 | end 28 | 29 | describe "Timestamp Create Case" do 30 | 31 | before do 32 | cdq.save(always_wait: true) 33 | end 34 | 35 | it "should set created_at" do 36 | @timestamp.created_at.should == @now 37 | end 38 | 39 | it "should set updated_at" do 40 | @timestamp.updated_at.should == @now 41 | end 42 | end 43 | 44 | describe "Timestamp Update Case" do 45 | before do 46 | cdq.save(always_wait: true) 47 | 48 | Time.stub!(:now, :return => @after_1_sec) 49 | @timestamp.flag = true 50 | cdq.save(always_wait: true) 51 | end 52 | 53 | it "should not set created_at" do 54 | @timestamp.created_at.should == @now 55 | end 56 | 57 | # This is failing when it shouldn't. Suspect RM bug. 58 | # 59 | # it "should set updated_at" do 60 | # @timestamp.updated_at.should == @after_1_sec 61 | # end 62 | end 63 | 64 | end 65 | -------------------------------------------------------------------------------- /motion/cdq/model.rb: -------------------------------------------------------------------------------- 1 | 2 | module CDQ 3 | 4 | class CDQModelManager 5 | 6 | attr_writer :current 7 | 8 | def initialize(opts = {}) 9 | @config = opts[:config] || CDQConfig.default 10 | end 11 | 12 | def current 13 | @current ||= load_model 14 | end 15 | 16 | def invalid? 17 | !@current && @config.model_url.nil? 18 | end 19 | 20 | def log(log_type = nil) 21 | out = "\n\n MODELS" 22 | out << "\n Model | count |" 23 | line = "\n - - - - - - - - - - - - - | - - - - - |" 24 | out << line 25 | 26 | self.current.entities.each do |entity| 27 | out << "\n #{entity.name.ljust(25)}|" 28 | out << " #{CDQ.cdq(entity.name).count.to_s.rjust(9)} |" 29 | end 30 | 31 | out << line 32 | 33 | entities = CDQ.cdq.models.current.entities 34 | if entities && (entity_count = entities.length) && entity_count > 0 35 | out << "\n#{entity_count} models" 36 | out << "\n\nYou can log a model like so: #{self.current.entities.first.name}.log" 37 | end 38 | 39 | if log_type == :string 40 | out 41 | else 42 | NSLog out 43 | end 44 | end 45 | 46 | private 47 | 48 | def load_model 49 | if invalid? 50 | raise "No model file. Cannot create an NSManagedObjectModel without one." 51 | else 52 | NSManagedObjectModel.alloc.initWithContentsOfURL(@config.model_url) 53 | end 54 | end 55 | 56 | end 57 | 58 | end 59 | -------------------------------------------------------------------------------- /spec/cdq/object_spec.rb: -------------------------------------------------------------------------------- 1 | 2 | module CDQ 3 | 4 | describe "CDQ Object" do 5 | 6 | before do 7 | class << self 8 | include CDQ 9 | end 10 | 11 | cdq.setup 12 | end 13 | 14 | after do 15 | cdq.reset! 16 | end 17 | 18 | it "has a contexts method" do 19 | cdq.contexts.class.should == CDQContextManager 20 | end 21 | 22 | it "has a stores method" do 23 | cdq.stores.class.should == CDQStoreManager 24 | end 25 | 26 | it "has a models method" do 27 | cdq.models.class.should == CDQModelManager 28 | end 29 | 30 | it "can override model" do 31 | model = cdq.models.current 32 | 33 | cdq.reset! 34 | 35 | cdq.setup(model: model) 36 | cdq.models.current.should == model 37 | end 38 | 39 | it "can override store" do 40 | store = cdq.stores.current 41 | 42 | cdq.reset! 43 | 44 | cdq.setup(store: store) 45 | cdq.stores.current.should == store 46 | end 47 | 48 | it "can override context" do 49 | context = cdq.contexts.current 50 | 51 | cdq.reset! 52 | 53 | cdq.setup(context: context) 54 | cdq.contexts.current.should == context 55 | end 56 | 57 | it "can open different database without deleting the previous one" do 58 | org_database_url = CDQConfig.default.database_url.path 59 | File.exist?(org_database_url).should == true 60 | cdq.close 61 | 62 | config = CDQConfig.new(name: "foo") 63 | File.exist?(config.database_url.path).should == false 64 | 65 | cdq.setup(config: config) 66 | 67 | CDQConfig.default.should == config 68 | File.exist?(config.database_url.path).should == true 69 | File.exist?(org_database_url).should == true 70 | end 71 | 72 | end 73 | 74 | end 75 | -------------------------------------------------------------------------------- /spec/cdq/calculation_spec.rb: -------------------------------------------------------------------------------- 1 | module CDQ 2 | describe "CDQ Calculation Methods" do 3 | 4 | before do 5 | CDQ.cdq.setup 6 | 7 | class << self 8 | include CDQ 9 | end 10 | 11 | @fees = [1.0, 2.0, 3.0] 12 | @author_foo = Author.create(name: 'foo', fee: @fees[0]) 13 | Author.create(name: 'foo', fee: @fees[1]) 14 | Author.create(name: 'bar', fee: @fees[2]) 15 | 16 | @lengths = [1, 2, 3, 4] 17 | @author_foo.articles.create(title: 'foo', body: 'bar', length: @lengths[0]) 18 | @author_foo.articles.create(title: 'foo', body: 'bar', length: @lengths[1]) 19 | Article.create(title: 'foo', body: 'bar', length: @lengths[2]) 20 | Article.create(title: 'foo', body: 'bar', length: @lengths[3]) 21 | cdq.save(always_wait: true) 22 | end 23 | 24 | after do 25 | CDQ.cdq.reset! 26 | end 27 | 28 | it "can calculate sum of float values" do 29 | Author.sum(:fee).should == @fees.inject(:+) 30 | end 31 | 32 | it "can calculate average of float values" do 33 | Author.average(:fee).should == (@fees.inject(:+).to_f / @fees.size) 34 | end 35 | 36 | it "can calculate min of float values" do 37 | Author.min(:fee).should == @fees[0] 38 | Author.minimum(:fee).should == @fees[0] 39 | end 40 | 41 | it "can calculate max of float values" do 42 | Author.max(:fee).should == @fees[2] 43 | Author.maximum(:fee).should == @fees[2] 44 | end 45 | 46 | it "can calculate sum of integer values" do 47 | Article.sum(:length).should == @lengths.inject(:+) 48 | end 49 | 50 | it "can do calculation with chained query" do 51 | Author.where(:name).eq('foo').calculate(:sum, :fee).should == @fees[0..1].inject(:+) 52 | end 53 | 54 | it "can do calculation on relations" do 55 | @author_foo.articles.calculate(:sum, :length).should == @lengths[0..1].inject(:+) 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /spec/cdq/partial_predicate_spec.rb: -------------------------------------------------------------------------------- 1 | 2 | module CDQ 3 | describe "CDQ Partial Predicates" do 4 | 5 | before do 6 | @scope = CDQQuery.new 7 | @ppred = CDQPartialPredicate.new(:count, @scope) 8 | end 9 | 10 | it "is composed of a key symbol and a scope" do 11 | @ppred.key.should == :count 12 | @ppred.scope.should.not == nil 13 | end 14 | 15 | it "creates an equality predicate" do 16 | scope = @ppred.eq(1) 17 | scope.predicate.should == make_pred('count', NSEqualToPredicateOperatorType, 1) 18 | 19 | scope = @ppred.equal(1) 20 | scope.predicate.should == make_pred('count', NSEqualToPredicateOperatorType, 1) 21 | end 22 | 23 | it "creates a less-than predicate" do 24 | scope = @ppred.lt(1) 25 | scope.predicate.should == make_pred('count', NSLessThanPredicateOperatorType, 1) 26 | end 27 | 28 | it "preserves the previous scope" do 29 | scope = CDQQuery.new(predicate: NSPredicate.predicateWithValue(false)) 30 | ppred = CDQPartialPredicate.new(:count, scope) 31 | ppred.eq(1).predicate.should == NSCompoundPredicate.andPredicateWithSubpredicates( 32 | [NSPredicate.predicateWithValue(false), make_pred('count', NSEqualToPredicateOperatorType, 1)] 33 | ) 34 | end 35 | 36 | it "works with 'or' too" do 37 | scope = CDQQuery.new(predicate: NSPredicate.predicateWithValue(true)) 38 | ppred = CDQPartialPredicate.new(:count, scope, :or) 39 | ppred.eq(1).predicate.should == NSCompoundPredicate.orPredicateWithSubpredicates( 40 | [NSPredicate.predicateWithValue(true), make_pred('count', NSEqualToPredicateOperatorType, 1)] 41 | ) 42 | end 43 | def make_pred(key, type, value, options = 0) 44 | NSComparisonPredicate.predicateWithLeftExpression( 45 | NSExpression.expressionForKeyPath(key.to_s), 46 | rightExpression:NSExpression.expressionForConstantValue(value), 47 | modifier:NSDirectPredicateModifier, 48 | type:type, 49 | options:options) 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /spec/cdq/collection_proxy_spec.rb: -------------------------------------------------------------------------------- 1 | 2 | module CDQ 3 | describe "CDQ Collection Proxy" do 4 | before do 5 | class << self 6 | include CDQ 7 | end 8 | 9 | cdq.setup 10 | 11 | @author = Author.create(name: "Stephen King") 12 | @articles = [ 13 | Article.create(title: "The Gunslinger", author: @author), 14 | Article.create(title: "The Drawing of the Three", author: @author), 15 | Article.create(title: "The Waste Lands", author: @author), 16 | Article.create(title: "Wizard and Glass", author: @author), 17 | Article.create(title: "The Wolves of the Calla", author: @author), 18 | Article.create(title: "Song of Susannah", author: @author), 19 | Article.create(title: "The Dark Tower", author: @author) 20 | ] 21 | 22 | @cp = CDQCollectionProxy.new(@articles, @articles.first.entity) 23 | end 24 | 25 | after do 26 | cdq.reset! 27 | end 28 | 29 | it "wraps an array of objects" do 30 | @cp.get.should == @articles 31 | end 32 | 33 | it "gets the first object" do 34 | @cp.first.should == @articles.first 35 | end 36 | 37 | it "gets the first n objects" do 38 | @cp.first(5).should == @articles.first(5) 39 | @cp.first(2).should == [@articles[0], @articles[1]] 40 | @cp.first(3).should == [@articles[0], @articles[1], @articles[2]] 41 | end 42 | 43 | it "gets the last object" do 44 | @cp.last.should == @articles.last 45 | end 46 | 47 | it "gets the last n objects" do 48 | @cp.last(5).should == @articles.last(5) 49 | @cp.last(2).should == [@articles[5], @articles[6]] 50 | @cp.last(3).should == [@articles[4], @articles[5], @articles[6]] 51 | end 52 | 53 | it "can use a where query" do 54 | q = @cp.where(:title).contains(" of ").sort_by(:title) 55 | q.count.should == 3 56 | q.array.should == [1,4,5].map { |i| @articles[i] }.sort_by(&:title) 57 | end 58 | 59 | it "behaves properly when given an empty set" do 60 | cp = CDQCollectionProxy.new([], @articles.first.entity) 61 | cp.get.should == [] 62 | cp.count.should == 0 63 | q = cp.or(:title).contains(" of ").sort_by(:title) 64 | q.count.should == 3 65 | q.array.should == [1,4,5].map { |i| @articles[i] }.sort_by(&:title) 66 | end 67 | 68 | it "allows you to grab count with size and length aliases" do 69 | @author.articles.count.should == 7 70 | @author.articles.size.should == 7 71 | @author.articles.length.should == 7 72 | end 73 | 74 | end 75 | end 76 | 77 | -------------------------------------------------------------------------------- /lib/cdq/generators.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'fileutils' 3 | 4 | module CDQ 5 | class Generator 6 | def initialize(options = nil) 7 | @dry_run = true if options == 'dry_run' 8 | end 9 | 10 | def create(template, name = nil) 11 | insert_from_template(template, name) 12 | end 13 | 14 | def template_path(template_name) 15 | sub_path = "templates/#{template_name}/" 16 | 17 | # First check local directory, use that if it exists 18 | if Dir.exist?("#{Dir.pwd}/#{sub_path}") 19 | "#{Dir.pwd}/#{sub_path}" 20 | else # Then check the gem 21 | begin 22 | spec = Gem::Specification.find_by_name("cdq") 23 | gem_root = spec.gem_dir 24 | "#{gem_root}/#{sub_path}" 25 | rescue Exception => e 26 | puts "CDQ - could not find template directory\n" 27 | nil 28 | end 29 | end 30 | end 31 | 32 | def insert_from_template(template_name, name = nil) 33 | puts "\n Creating #{template_name}: #{name}\n\n" 34 | 35 | return unless (@template_path = template_path(template_name)) 36 | files = Dir["#{@template_path}**/*"].select {|f| !File.directory? f} 37 | 38 | if name 39 | @name = name 40 | @name_camel_case = @name.split('_').map{|word| word.capitalize}.join 41 | end 42 | 43 | files.each do |template_file_path_and_name| 44 | @in_app_path = File.dirname(template_file_path_and_name).gsub(@template_path, '') 45 | @ext = File.extname(template_file_path_and_name) 46 | @file_name = File.basename(template_file_path_and_name, @ext) 47 | 48 | @new_file_name = @file_name.gsub('name', @name || 'name') 49 | @new_file_path_name = "#{Dir.pwd}/#{@in_app_path}/#{@new_file_name}#{@ext}" 50 | 51 | if @dry_run 52 | puts "\n Instance vars:" 53 | self.instance_variables.each{|var| puts " #{var} = #{self.instance_variable_get(var)}"} 54 | puts 55 | end 56 | 57 | if Dir.exist?(@in_app_path) 58 | puts " Using existing directory: #{@in_app_path}" 59 | else 60 | puts " \u0394 Creating directory: #{@in_app_path}" 61 | FileUtils.mkdir_p(@in_app_path) unless @dry_run 62 | end 63 | 64 | results = load_and_parse_erb(template_file_path_and_name) 65 | 66 | if File.exists?(@new_file_path_name) 67 | puts " X File exists, SKIPPING: #{@new_file_path_name}" 68 | else 69 | puts " \u0394 Creating file: #{@new_file_path_name}" 70 | File.open(@new_file_path_name, 'w+') { |file| file.write(results) } unless @dry_run 71 | end 72 | end 73 | 74 | puts "\n Done" 75 | end 76 | 77 | def load_and_parse_erb(template_file_name_and_path) 78 | template_file = File.open(template_file_name_and_path, 'r').read 79 | erb = ERB.new(template_file) 80 | erb.result(binding) 81 | end 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /motion/cdq/store.rb: -------------------------------------------------------------------------------- 1 | 2 | module CDQ 3 | 4 | class CDQStoreManager 5 | 6 | STORE_DID_INITIALIZE_NOTIFICATION = 'com.infinitered.cdq.store.did_initialize' 7 | 8 | attr_writer :current 9 | 10 | def initialize(opts = {}) 11 | @config = opts[:config] || CDQConfig.default 12 | @model_manager = opts[:model_manager] 13 | end 14 | 15 | def new(opts = {}) 16 | @config = opts[:config] || CDQConfig.default 17 | @model_manager = opts[:model_manager] || CDQ.cdq.models 18 | end 19 | 20 | def current 21 | @current ||= create_store 22 | end 23 | 24 | def reset! 25 | path = @config.database_url.absoluteString 26 | NSFileManager.defaultManager.removeItemAtURL(@config.database_url, error: nil) 27 | NSFileManager.defaultManager.removeItemAtURL(NSURL.URLWithString("#{path}-shm"), error: nil) 28 | NSFileManager.defaultManager.removeItemAtURL(NSURL.URLWithString("#{path}-wal"), error: nil) 29 | end 30 | 31 | def invalid? 32 | !@current && @model_manager.invalid? 33 | end 34 | 35 | private 36 | 37 | def create_store 38 | if invalid? 39 | raise "No model found. Can't create a persistent store coordinator without it." 40 | else 41 | create_local_store 42 | end 43 | end 44 | 45 | def create_local_store 46 | coordinator = NSPersistentStoreCoordinator.alloc.initWithManagedObjectModel(@model_manager.current) 47 | error = Pointer.new(:object) 48 | options = { NSMigratePersistentStoresAutomaticallyOption => true, 49 | NSInferMappingModelAutomaticallyOption => true } 50 | url = @config.database_url 51 | mkdir_p File.dirname(url.path) 52 | store = coordinator.addPersistentStoreWithType(NSSQLiteStoreType, 53 | configuration:nil, 54 | URL:url, 55 | options:options, 56 | error:error) 57 | if store.nil? 58 | error[0].userInfo['metadata'] && error[0].userInfo['metadata'].each do |key, value| 59 | NSLog "#{key}: #{value}" 60 | end 61 | raise error[0].userInfo['reason'] 62 | end 63 | Dispatch::Queue.main.after(0) { 64 | # This block is executed in a next run loop. 65 | # So the managed object context has a store coordinator in this point. 66 | NSNotificationCenter.defaultCenter.postNotificationName(STORE_DID_INITIALIZE_NOTIFICATION, object:coordinator) 67 | } 68 | coordinator 69 | end 70 | 71 | def mkdir_p dir 72 | error = Pointer.new(:object) 73 | m = NSFileManager.defaultManager 74 | r = m.createDirectoryAtPath dir, withIntermediateDirectories:true, attributes:nil, error:error 75 | unless r 76 | NSLog "#{error[0].localizedDescription}" 77 | raise error[0].localizedDescription 78 | end 79 | end 80 | 81 | end 82 | 83 | end 84 | -------------------------------------------------------------------------------- /spec/cdq/config_spec.rb: -------------------------------------------------------------------------------- 1 | 2 | 3 | module CDQ 4 | 5 | describe "CDQ Config" do 6 | 7 | before do 8 | @bundle_name = NSBundle.mainBundle.objectForInfoDictionaryKey("CFBundleExecutable") 9 | end 10 | 11 | it "sets default values when no config file present" do 12 | config = CDQConfig.new(nil) 13 | config.name.should == @bundle_name 14 | config.database_name.should == config.name 15 | config.model_name.should == config.name 16 | end 17 | 18 | it "can initialize values from a hash" do 19 | config = CDQConfig.new(name: "foo") 20 | config.name.should == "foo" 21 | config.database_name.should == "foo" 22 | config.model_name.should == "foo" 23 | end 24 | 25 | it "can override the default name" do 26 | cf_file = File.join(NSBundle.mainBundle.bundlePath, "cdq.yml") 27 | yaml_to_file(cf_file, name: "foo") 28 | config = CDQConfig.new(cf_file) 29 | config.name.should == "foo" 30 | config.database_name.should == "foo" 31 | config.model_name.should == "foo" 32 | File.unlink(cf_file) 33 | end 34 | 35 | it "can override database_name specifically" do 36 | cf_file = File.join(NSBundle.mainBundle.bundlePath, "cdq.yml") 37 | yaml_to_file(cf_file, database_name: "foo") 38 | config = CDQConfig.new(cf_file) 39 | config.name.should == @bundle_name 40 | config.database_name.should == "foo" 41 | config.model_name.should == config.name 42 | File.unlink(cf_file) 43 | end 44 | 45 | it "can override model_name specifically" do 46 | cf_file = File.join(NSBundle.mainBundle.bundlePath, "cdq.yml") 47 | yaml_to_file(cf_file, model_name: "foo") 48 | config = CDQConfig.new(cf_file) 49 | config.name.should == @bundle_name 50 | config.database_name.should == config.name 51 | config.model_name.should == "foo" 52 | File.unlink(cf_file) 53 | end 54 | 55 | it "can override database_url specifically NSDocument" do 56 | cf_file = File.join(NSBundle.mainBundle.bundlePath, "cdq.yml") 57 | yaml_to_file(cf_file, database_dir: "NSDocument") 58 | config = CDQConfig.new(cf_file) 59 | config.database_url.path.should =~ %r{Documents/#{@bundle_name}.sqlite$} 60 | end 61 | 62 | it "can override database_url specifically NSApplicationSupportDirectory" do 63 | cf_file = File.join(NSBundle.mainBundle.bundlePath, "cdq.yml") 64 | yaml_to_file(cf_file, database_dir: "NSApplicationSupportDirectory") 65 | config = CDQConfig.new(cf_file) 66 | config.database_url.path.should =~ %r{Library/Application Support/#{@bundle_name}.sqlite$} 67 | end 68 | 69 | it "constructs database_url" do 70 | config = CDQConfig.new(nil) 71 | config.database_url.class.should == NSURL 72 | config.database_url.path.should =~ %r{Documents/#{@bundle_name}.sqlite$} 73 | end 74 | 75 | it "should parse an empty config" do 76 | cf_file = File.join(NSBundle.mainBundle.bundlePath, "cdq.yml") 77 | yaml_to_file(cf_file, {}) 78 | config = CDQConfig.new(cf_file) 79 | config.should != nil 80 | File.unlink(cf_file) 81 | end 82 | 83 | it "constructs model_url" do 84 | config = CDQConfig.new(nil) 85 | config.model_url.class.should == NSURL 86 | config.model_url.path.should =~ %r{#{@bundle_name}_spec.app/.*#{@bundle_name}.momd$} 87 | end 88 | 89 | def yaml_to_file(file, hash) 90 | contents = YAML.dump(hash) 91 | File.open(file,'w+') { |f| f.write(contents) } 92 | end 93 | 94 | def text_to_file(file, text) 95 | File.open(file,'w+') { |f| f.write(text) } 96 | end 97 | 98 | end 99 | 100 | end 101 | -------------------------------------------------------------------------------- /motion/cdq.rb: -------------------------------------------------------------------------------- 1 | 2 | module CDQ 3 | 4 | class CDQObject; end 5 | class CDQQuery < CDQObject; end 6 | class CDQPartialPredicate < CDQObject; end 7 | class CDQTargetedQuery < CDQQuery; end 8 | 9 | extend self 10 | 11 | # The master method that will lift a variety of arguments into the CDQ 12 | # ecosystem. What it returns depends on the type of the argument passed: 13 | # 14 | # Class: Finds an entity with the same name as the class and returns a 15 | # targeted query. (This is also used internally for dedicated models.) 16 | # 17 | # cdq(Author).where(:attribute).eq(1).limit(3) 18 | # 19 | # String: Finds an entity with the name provided in the string and returns a 20 | # targeted query. 21 | # 22 | # cdq('Author').where(:attribute).eq(1).limit(3) 23 | # 24 | # Symbol: Returns an untargeted partial predicate. This is useful for nested 25 | # queries, and for defining scopes. 26 | # 27 | # Author.scope :singletons, cdq(:attribute).eq(1) 28 | # Author.where( cdq(:attribute).eq(1).or.eq(3) ).and(:name).ne("Roger") 29 | # 30 | # CDQObject: returns the object itself (no-op). 31 | # 32 | # NSManagedObject: wraps the object in a CDQObjectProxy, which permits 33 | # cdq-style queries on the object's relationships. 34 | # 35 | # emily_dickinson = Author.first 36 | # cdq(emily_dickinson).articles.where(:page_count).lt(5).array 37 | # 38 | # Array: wraps the array in a CDQCollectionProxy, which lets you run queries 39 | # relative to the members of the collection. 40 | # 41 | # emily_dickinson = Author.first 42 | # edgar_allen_poe = Author.all[4] 43 | # charles_dickens = Author.all[7] 44 | # cdq([emily_dickinson, edgar_allen_poe, charles_dickens]).where(:avg_rating).eq(1) 45 | # 46 | def cdq(obj = nil) 47 | obj ||= self 48 | 49 | @@base_object ||= CDQObject.new 50 | 51 | case obj 52 | when Class 53 | if obj.isSubclassOfClass(NSManagedObject) 54 | entities = NSDictionary.dictionaryWithDictionary( 55 | @@base_object.models.current.entitiesByName) 56 | entity_name = obj.name.split("::").last 57 | # NOTE attempt to look up the entity 58 | entity_description = 59 | entities[entity_name] || 60 | entities[obj.ancestors[1].name] 61 | if entity_description.nil? 62 | raise "Cannot find an entity named #{obj.name}" 63 | end 64 | CDQTargetedQuery.new(entity_description, obj) 65 | else 66 | @@base_object 67 | end 68 | when String 69 | entities = NSDictionary.dictionaryWithDictionary( 70 | @@base_object.models.current.entitiesByName) 71 | entity_description = entities[obj] 72 | target_class = NSClassFromString(entity_description.managedObjectClassName) 73 | if entity_description.nil? 74 | raise "Cannot find an entity named #{obj}" 75 | end 76 | CDQTargetedQuery.new(entity_description, target_class) 77 | when NSEntityDescription 78 | entity_description = obj 79 | target_class = NSClassFromString(entity_description.managedObjectClassName) 80 | CDQTargetedQuery.new(entity_description, target_class) 81 | when Symbol 82 | CDQPartialPredicate.new(obj, CDQQuery.new) 83 | when CDQObject 84 | obj 85 | when NSManagedObject 86 | CDQObjectProxy.new(obj) 87 | when Array 88 | if obj.first.class.isSubclassOfClass(NSManagedObject) 89 | CDQCollectionProxy.new(obj, obj.first.entity) 90 | else 91 | @@base_object 92 | end 93 | else 94 | @@base_object 95 | end 96 | end 97 | 98 | end 99 | 100 | 101 | # @private 102 | class UIResponder 103 | include CDQ 104 | end 105 | 106 | # @private 107 | class TopLevel 108 | include CDQ 109 | end 110 | -------------------------------------------------------------------------------- /motion/cdq/object.rb: -------------------------------------------------------------------------------- 1 | 2 | module CDQ 3 | 4 | class CDQObject 5 | 6 | include CDQ 7 | 8 | def contexts 9 | @@context_manager ||= CDQContextManager.new(store_manager: stores) 10 | end 11 | 12 | def stores 13 | @@store_manager ||= CDQStoreManager.new(model_manager: models) 14 | end 15 | 16 | def models 17 | @@model_manager ||= CDQModelManager.new 18 | end 19 | 20 | def reset!(opts = {}) 21 | @@context_manager.reset! if @@context_manager 22 | @@context_manager = nil 23 | @@store_manager.reset! if @@store_manager 24 | @@store_manager = nil 25 | end 26 | 27 | # Save any data and close down the contexts and store manager. 28 | # You should be able create a new CDQConfig object and run setup 29 | # again to attach to a different database. However, you need to be sure 30 | # that all activity is finished, and that any exisitng model instances 31 | # have been deallocated. 32 | #------------------------------------------------------------------------------ 33 | def close 34 | save 35 | @@context_manager.reset! if @@context_manager 36 | @@context_manager = nil 37 | @@store_manager = nil 38 | CDQConfig.default_config = nil 39 | end 40 | 41 | # You can now pass in a CDQConfig object, which will be used instead of the 42 | # one loaded from the cdq.yml file. However, the model file is loaded during 43 | # the loading of the code - so it can only be overridden using the cdq.yml. 44 | #------------------------------------------------------------------------------ 45 | def setup(opts = {}) 46 | CDQConfig.default_config = opts[:config] || nil 47 | if opts[:context] 48 | contexts.push(opts[:context]) 49 | return true 50 | elsif opts[:store] 51 | stores.current = opts[:store] 52 | elsif opts[:model] 53 | models.current = opts[:model] 54 | end 55 | contexts.push(NSMainQueueConcurrencyType) 56 | true 57 | end 58 | 59 | def save(*args) 60 | contexts.save(*args) 61 | end 62 | 63 | def background(*args, &block) 64 | contexts.background(*args, &block) 65 | end 66 | 67 | def find(oid) 68 | url = NSURL.URLWithString(oid) 69 | object_id = stores.current.managedObjectIDForURIRepresentation(url) 70 | object_id ? contexts.current.existingObjectWithID(object_id, error: nil) : nil 71 | end 72 | 73 | protected 74 | 75 | def with_error_object(default, &block) 76 | error = Pointer.new(:object) 77 | result = block.call(error) 78 | if error[0] 79 | p error[0].debugDescription 80 | raise "Error while fetching: #{error[0].debugDescription}" 81 | end 82 | result || default 83 | end 84 | 85 | def constantize(camel_cased_word) 86 | names = camel_cased_word.split('::') 87 | names.shift if names.empty? || names.first.empty? 88 | 89 | names.inject(Object) do |constant, name| 90 | if constant == Object 91 | constant.const_get(name) 92 | else 93 | candidate = constant.const_get(name) 94 | next candidate if constant.const_defined?(name, false) 95 | next candidate unless Object.const_defined?(name) 96 | 97 | # Go down the ancestors to check it it's owned 98 | # directly before we reach Object or the end of ancestors. 99 | constant = constant.ancestors.inject do |const, ancestor| 100 | break const if ancestor == Object 101 | break ancestor if ancestor.const_defined?(name, false) 102 | const 103 | end 104 | 105 | # owner is in Object, so raise 106 | constant.const_get(name, false) 107 | end 108 | end 109 | end 110 | end 111 | 112 | end 113 | 114 | -------------------------------------------------------------------------------- /motion/cdq/partial_predicate.rb: -------------------------------------------------------------------------------- 1 | 2 | module CDQ 3 | 4 | # A partial predicate is an intermediate state while constructing a 5 | # query. It knows which attribute to use as the left operand, and 6 | # then offers a range of methods to specify which operation to use, 7 | # and what value to use as the right operand. They are most commonly 8 | # created via the where, and, and or 9 | # methods on query, and sometimes via the main cdq method. 10 | 11 | class CDQPartialPredicate < CDQObject 12 | 13 | attr_reader :key, :scope, :operation 14 | 15 | def initialize(key, scope, operation = :and) 16 | @key = key 17 | @scope = scope 18 | @operation = operation 19 | end 20 | 21 | # Equality 22 | # @returns a new CDQQuery with the predicate appended 23 | def eq(value, options = 0); make_scope(NSEqualToPredicateOperatorType, value, options); end 24 | 25 | # Inequality 26 | # @returns a new CDQQuery with the predicate appended 27 | def ne(value, options = 0); make_scope(NSNotEqualToPredicateOperatorType, value, options); end 28 | 29 | # Less Than 30 | # @returns a new CDQQuery with the predicate appended 31 | def lt(value, options = 0); make_scope(NSLessThanPredicateOperatorType, value, options); end 32 | 33 | # Less Than or Equal To 34 | # @returns a new CDQQuery with the predicate appended 35 | def le(value, options = 0); make_scope(NSLessThanOrEqualToPredicateOperatorType, value, options); end 36 | 37 | # Greater Than 38 | # @returns a new CDQQuery with the predicate appended 39 | def gt(value, options = 0); make_scope(NSGreaterThanPredicateOperatorType, value, options); end 40 | 41 | # Greater Than or Equal To 42 | # @returns a new CDQQuery with the predicate appended 43 | def ge(value, options = 0); make_scope(NSGreaterThanOrEqualToPredicateOperatorType, value, options); end 44 | 45 | # Contains Substring 46 | # @returns a new CDQQuery with the predicate appended 47 | def contains(substr, options = 0); make_scope(NSContainsPredicateOperatorType, substr, options); end 48 | 49 | # Matches Regexp 50 | # @returns a new CDQQuery with the predicate appended 51 | def matches(regexp, options = 0); make_scope(NSMatchesPredicateOperatorType, regexp, options); end 52 | 53 | # List membership 54 | # @returns a new CDQQuery with the predicate appended 55 | def in(list, options = 0); make_scope(NSInPredicateOperatorType, list, options); end 56 | 57 | # Begins With String 58 | # @returns a new CDQQuery with the predicate appended 59 | def begins_with(substr, options = 0); make_scope(NSBeginsWithPredicateOperatorType, substr, options); end 60 | 61 | # Ends With String 62 | # @returns a new CDQQuery with the predicate appended 63 | def ends_with(substr, options = 0); make_scope(NSEndsWithPredicateOperatorType, substr, options); end 64 | 65 | # Between Min and Max Values 66 | # @returns a new CDQQuery with the predicate appended 67 | def between(min, max); make_scope(NSBetweenPredicateOperatorType, [min, max]); end 68 | 69 | 70 | alias_method :equal, :eq 71 | alias_method :not_equal, :ne 72 | alias_method :less_than, :lt 73 | alias_method :less_than_or_equal, :le 74 | alias_method :greater_than, :gt 75 | alias_method :greater_than_or_equal, :ge 76 | alias_method :include, :contains 77 | 78 | 79 | private 80 | 81 | def make_pred(key, type, value, options = 0) 82 | NSComparisonPredicate.predicateWithLeftExpression( 83 | NSExpression.expressionForKeyPath(key.to_s), 84 | rightExpression:NSExpression.expressionForConstantValue(value), 85 | modifier:NSDirectPredicateModifier, 86 | type:type, 87 | options:options) 88 | end 89 | 90 | def make_scope(type, value, options = 0) 91 | scope.send(operation, make_pred(key, type, value, options), key) 92 | end 93 | 94 | end 95 | end 96 | 97 | -------------------------------------------------------------------------------- /motion/cdq/relationship_query.rb: -------------------------------------------------------------------------------- 1 | 2 | module CDQ 3 | 4 | class CDQRelationshipQuery < CDQTargetedQuery 5 | 6 | def initialize(owner, name, set = nil, opts = {}) 7 | @owner = owner ? WeakRef.new(owner) : nil 8 | @relationship_name = name 9 | @set = set ? WeakRef.new(set) : nil 10 | relationship = owner.entity.relationshipsByName[name] 11 | if relationship.isToMany 12 | if @owner.ordered_set?(name) 13 | @set ||= @owner.mutableOrderedSetValueForKey(name) 14 | else 15 | @set ||= @owner.mutableSetValueForKey(name) 16 | end 17 | end 18 | @inverse_rel = relationship.inverseRelationship 19 | entity_description = relationship.destinationEntity 20 | target_class = constantize(entity_description.managedObjectClassName) 21 | super(entity_description, target_class, opts) 22 | if @inverse_rel.isToMany 23 | @predicate = self.where(@inverse_rel.name.to_sym).contains(owner).predicate 24 | else 25 | @predicate = self.where(@inverse_rel.name.to_sym => @owner).predicate 26 | end 27 | end 28 | 29 | def dealloc 30 | super 31 | end 32 | 33 | # Creates a new managed object within the target relationship 34 | # 35 | def new(opts = {}) 36 | super(opts).tap do |obj| 37 | add(obj) 38 | end 39 | end 40 | 41 | # Add an existing object to the relationship 42 | # 43 | def add(obj) 44 | @set.addObject obj 45 | end 46 | alias_method :<<, :add 47 | 48 | # Remove objects from the relationship 49 | # 50 | def remove(obj) 51 | @set.removeObject obj 52 | end 53 | 54 | def remove_all 55 | @set.removeAllObjects 56 | end 57 | 58 | def self.extend_set(set, owner, name) 59 | set.extend SetExt 60 | set.extend Enumerable 61 | set.__query__ = self.new(owner, name, set) 62 | set 63 | end 64 | 65 | # A Core Data relationship set is extended with this module to provide 66 | # scoping by forwarding messages to a CDQRelationshipQuery instance knows 67 | # how to create further queries based on the underlying relationship. 68 | module SetExt 69 | attr_accessor :__query__ 70 | 71 | def set 72 | self 73 | end 74 | 75 | # This works in a special way. If we're extending a regular NSSet, it will 76 | # create a new method that calls allObjects. If we're extending a NSOrderedSet, 77 | # the override will not work, and we get the array method already defined on 78 | # NSOrderedSet, which is actually exactly what we want. 79 | def array 80 | self.allObjects 81 | end 82 | 83 | def first(n = 1) 84 | n == 1 ? array.first : array.first(n) 85 | end 86 | 87 | def last(n = 1) 88 | n == 1 ? array.last : array.last(n) 89 | end 90 | 91 | # duplicating a lot of common methods because it's way faster than using method_missing 92 | # 93 | def each(*args, &block) 94 | array.each(*args, &block) 95 | end 96 | 97 | def add(obj) 98 | @__query__.add(obj) 99 | end 100 | alias_method :<<, :add 101 | 102 | def create(opts = {}) 103 | @__query__.create(opts) 104 | end 105 | 106 | def new(opts = {}) 107 | @__query__.new(opts) 108 | end 109 | 110 | def remove(opts = {}) 111 | @__query__.remove(opts) 112 | end 113 | 114 | def where(*args) 115 | @__query__.where(*args) 116 | end 117 | 118 | def sort_by(*args) 119 | @__query__.sort_by(*args) 120 | end 121 | 122 | def limit(*args) 123 | @__query__.limit(*args) 124 | end 125 | 126 | def offset(*args) 127 | @__query__.offset(*args) 128 | end 129 | 130 | def respond_to?(method) 131 | super(method) || @__query__.respond_to?(method) 132 | end 133 | 134 | def method_missing(method, *args, &block) 135 | if @__query__.respond_to?(method) 136 | @__query__.send(method, *args, &block) 137 | else 138 | super 139 | end 140 | end 141 | 142 | end 143 | 144 | end 145 | 146 | end 147 | -------------------------------------------------------------------------------- /lib/cdq/cli.rb: -------------------------------------------------------------------------------- 1 | 2 | require "cdq/version" 3 | require "cdq/generators" 4 | 5 | module CDQ 6 | class CommandLine 7 | HELP_TEXT = %{Usage: 8 | cdq [options] [arguments] 9 | 10 | Commands: 11 | cdq init # Add boilerplate setup to use CDQ 12 | cdq create model # Create a model and associated files 13 | 14 | Options: 15 | } 16 | 17 | attr_reader :singleton_options_passed 18 | 19 | def option_parser(help_text = HELP_TEXT) 20 | OptionParser.new do |opts| 21 | opts.banner = help_text 22 | 23 | opts.on("-v", "--version", "Print Version") do 24 | @singleton_options_passed = true 25 | puts CDQ::VERSION 26 | end 27 | 28 | opts.on("-h", "--help", "Show this message") do 29 | @singleton_options_passed = true 30 | puts opts 31 | end 32 | end 33 | end 34 | 35 | def self.run_all 36 | 37 | actions = { "init" => InitAction, "create" => CreateAction } 38 | 39 | cli = self.new 40 | opts = cli.option_parser 41 | opts.order! 42 | action = ARGV.shift 43 | 44 | if actions[action] 45 | actions[action].new.run 46 | elsif !cli.singleton_options_passed 47 | puts opts 48 | end 49 | 50 | end 51 | end 52 | 53 | class InitAction < CommandLine 54 | HELP_TEXT = %{Usage: 55 | cdq init [options] 56 | 57 | Run inside a motion directory, it will: 58 | * Add cdq and ruby-xcdm to Gemfile, if necessary 59 | * Set rake to automatically run schema:build 60 | * Create an initial schema file 61 | 62 | Options: 63 | } 64 | 65 | def option_parser 66 | super(HELP_TEXT).tap do |opts| 67 | opts.program_name = "cdq init" 68 | 69 | opts.on("-d", "--dry-run", "Do a Dry Run") do 70 | @dry_run = "dry_run" 71 | end 72 | end 73 | end 74 | 75 | def run 76 | opts = option_parser 77 | opts.order! 78 | 79 | unless singleton_options_passed 80 | CDQ::Generator.new(@dry_run).create('init') 81 | 82 | print " \u0394 Checking bundle for cdq... " 83 | unless system('bundle show cdq') 84 | print " \u0394 Adding cdq to Gemfile... " 85 | File.open("Gemfile", "at") do |gemfile| 86 | gemfile.puts("gem 'cdq'") 87 | end 88 | puts "Done." 89 | end 90 | 91 | # print " \u0394 Checking bundle for ruby-xcdm... " 92 | # unless system('bundle show ruby-xcdm') 93 | # print " \u0394 Adding ruby-xcdm to Gemfile... " 94 | # File.open("Gemfile", "at") do |gemfile| 95 | # gemfile.puts("gem 'ruby-xcdm'") 96 | # end 97 | # puts "Done." 98 | # end 99 | 100 | print " \u0394 Adding schema:build hook to Rakefile... " 101 | File.open("Rakefile", "at") do |rakefile| 102 | rakefile.puts(%{\ntask :"build:simulator" => :"schema:build"}) 103 | end 104 | puts "Done." 105 | 106 | puts %{\n Now edit schemas/0001_initial.rb to define your schema, and you're off and running. } 107 | 108 | end 109 | end 110 | 111 | end 112 | 113 | class CreateAction < CommandLine 114 | HELP_TEXT = %{ 115 | Usage: 116 | cdq create [options] model # Create a CDQ model and associated test 117 | 118 | Options: 119 | } 120 | 121 | def option_parser 122 | super(HELP_TEXT).tap do |opts| 123 | opts.program_name = "cdq create" 124 | 125 | opts.on("-d", "--dry-run", "Do a Dry Run") do 126 | @dry_run = "dry_run" 127 | end 128 | end 129 | end 130 | 131 | def run 132 | opts = option_parser 133 | opts.order! 134 | 135 | object = ARGV.shift 136 | 137 | unless singleton_options_passed 138 | case object 139 | when 'model' 140 | model_name = ARGV.shift 141 | if model_name 142 | 143 | #camelized = model_name.gsub(/[A-Z]/) { |m| "_#{m.downcase}" }.gsub 144 | CDQ::Generator.new(@dry_run).create('model', model_name) 145 | else 146 | puts "Please supply a model name" 147 | puts opts 148 | end 149 | else 150 | puts "Invalid object type: #{object}" 151 | puts opts 152 | end 153 | end 154 | end 155 | 156 | end 157 | end 158 | -------------------------------------------------------------------------------- /spec/cdq/context_spec.rb: -------------------------------------------------------------------------------- 1 | 2 | module CDQ 3 | 4 | describe "CDQ Context Manager" do 5 | 6 | before do 7 | class << self 8 | include CDQ 9 | end 10 | end 11 | 12 | after do 13 | @cc.class.send(:undef_method, :main) if @cc.respond_to?(:main) 14 | @cc.class.send(:undef_method, :root) if @cc.respond_to?(:root) 15 | CDQ::Deprecation.silence_deprecation = false 16 | CDQ.cdq.reset! 17 | end 18 | 19 | 20 | before do 21 | @cc = CDQContextManager.new(store_manager: cdq.stores) 22 | @context = NSManagedObjectContext.alloc.initWithConcurrencyType(NSPrivateQueueConcurrencyType) 23 | end 24 | 25 | it "should raise an exception if not given a store coordinator" do 26 | c = CDQConfig.new(name: "foo") 27 | mm = CDQModelManager.new(config: c) 28 | sm = CDQStoreManager.new(config: c, model_manager: mm) 29 | 30 | cc = CDQContextManager.new(store_manager: sm) 31 | 32 | 33 | should.raise(RuntimeError) do 34 | cc.push(:private) 35 | end 36 | end 37 | 38 | it "can push a NSManagedObjectContext onto its stack" do 39 | @cc.push(@context) 40 | @cc.current.should == @context 41 | @cc.all.should == [@context] 42 | end 43 | 44 | it "can push a named NSManagedObjectContext onto its stack" do 45 | @cc.push(@context, named: :ordinary) 46 | @cc.current.should == @context 47 | @cc.ordinary.should == @context 48 | @cc.all.should == [@context] 49 | end 50 | 51 | it "can pop a NSManagedObjectContext off its stack" do 52 | @cc.push(@context) 53 | @cc.pop.should == @context 54 | @cc.current.should == nil 55 | @cc.all.should == [] 56 | end 57 | 58 | it "sets a default context if used for the first time" do 59 | @cc.current.should != nil 60 | end 61 | 62 | it "pushes temporarily if passed a block" do 63 | @cc.push(NSMainQueueConcurrencyType) 64 | @cc.pop 65 | @cc.current.should == nil 66 | @cc.push(@context) do 67 | @cc.current.should == @context 68 | end 69 | @cc.current.should == nil 70 | end 71 | 72 | it "pops temporarily if passed a block" do 73 | @cc.push(@context) 74 | @cc.pop do 75 | @cc.current.should == nil 76 | end 77 | @cc.current.should == @context 78 | end 79 | 80 | it "can create a new context and push it to the top of the stack" do 81 | first = @cc.push(:private) 82 | @cc.current.should == first 83 | first.persistentStoreCoordinator.should.not == nil 84 | second = @cc.push(:main) 85 | @cc.current.should == second 86 | second.parentContext.should == first 87 | end 88 | 89 | it "can create a new context WITHOUT pushing it to the top of the stack" do 90 | first = @cc.create(:private) 91 | first.class.should == NSManagedObjectContext 92 | @cc.current.should == nil 93 | end 94 | 95 | it "can create named contexts" do 96 | first = @cc.create(:private, named: :special) 97 | @cc.special.should == first 98 | end 99 | 100 | it "can run code on foreign contexts" do 101 | @cc.create(:private, named: :foreign) 102 | @cc.foreign.should.not == nil 103 | @cc.on(:foreign) do 104 | @cc.all.should == [] 105 | end 106 | end 107 | 108 | it "saves all contexts" do 109 | root = @cc.push(:private) 110 | main = @cc.push(:main) 111 | root_saved = false 112 | main_saved = false 113 | 114 | root.stub!(:save) { root_saved = true } 115 | main.stub!(:save) { main_saved = true } 116 | 117 | @cc.save(always_wait: true) 118 | 119 | root_saved.should == true 120 | main_saved.should == true 121 | end 122 | 123 | it "saves specific contexts" do 124 | root = @cc.push(:private) 125 | main = @cc.push(:main) 126 | root_saved = false 127 | main_saved = false 128 | 129 | root.stub!(:save) { root_saved = true } 130 | main.stub!(:save) { main_saved = true } 131 | 132 | @cc.save(main, always_wait: true) 133 | 134 | root_saved.should == false 135 | main_saved.should == true 136 | end 137 | 138 | it "saves contexts by name" do 139 | main = @cc.push(:main, named: :main) 140 | 141 | main_saved = false 142 | 143 | main.stub!(:save) { main_saved = true } 144 | 145 | @cc.save(:main) 146 | 147 | main_saved.should == true 148 | end 149 | 150 | it "automatically gives names to :root and :main contexts" do 151 | @cc.push(:root) 152 | @cc.push(:main) 153 | 154 | @cc.should.respond_to :root 155 | @cc.should.respond_to :main 156 | end 157 | end 158 | 159 | end 160 | -------------------------------------------------------------------------------- /motion/cdq/config.rb: -------------------------------------------------------------------------------- 1 | module CDQ 2 | 3 | # = Configure the CDQ Stack 4 | # 5 | # This class wraps the YAML configuration file that will allow you to 6 | # override the names used when setting up the database file and finding the 7 | # model file. This file is named cdq.yml and must be found at the 8 | # root of your resources directory. It supports the following top-level keys: 9 | # 10 | # [name] The root name for both database and model 11 | # [database_dir] The root name for the database directory (NSDocumentDirectory or NSApplicationSupportDirectory) 12 | # [database_name] The root name for the database file (relative to the database_dir) 13 | # [model_name] The root name for the model file (relative to the bundle directory) 14 | # [app_group_id] The app group id set in iTunes member center (group.com.mycompany.myapp) 15 | # [app_group_container_uuid] WORKAROUND: The app group's UUID for iOS Simulator 8.1 which doesn't return an app group container path from the id 16 | # 17 | # Using the config file is not necessary. If you do not include it, the bundle display name 18 | # will be used. For most people with a new app, this is what you want to do, especially if 19 | # you are using ruby-xcdm schemas. The only case where using the config file is required 20 | # is when you want to use CDQManagedObject-based models with a custom model or database, because 21 | # class loading order of operations makes it impossible to configure from within your 22 | # AppDelegate. 23 | # 24 | class CDQConfig 25 | 26 | attr_reader :config_file, :database_name, :database_dir, :model_name, :name, :app_group_id, :app_group_container_uuid 27 | 28 | def initialize(config_file) 29 | h = nil 30 | case config_file 31 | when String 32 | @config_file = config_file 33 | h = nil 34 | if File.file?(config_file) 35 | h = File.open(config_file) { |f| YAML.load(f.read) } 36 | # If a file was consisted comments only, it may parse as an Array. 37 | h = nil unless h.is_a? Hash 38 | end 39 | when Hash 40 | h = config_file 41 | end 42 | h ||= {} 43 | 44 | @name = h['name'] || h[:name] || NSBundle.mainBundle.objectForInfoDictionaryKey("CFBundleExecutable") 45 | @database_dir = search_directory_for h['database_dir'] || h[:database_dir] 46 | @database_name = h['database_name'] || h[:database_name] || name 47 | @model_name = h['model_name'] || h[:model_name] || name 48 | @app_group_id = h['app_group_id'] || h[:app_group_id] 49 | @app_group_container_uuid = h['app_group_container_uuid'] || h[:app_group_container_uuid] 50 | end 51 | 52 | def database_url 53 | if app_group_id.nil? 54 | dir = NSSearchPathForDirectoriesInDomains(database_dir, NSUserDomainMask, true).last 55 | else 56 | dir = app_group_container 57 | end 58 | 59 | path = File.join(dir, database_name + '.sqlite') 60 | NSURL.fileURLWithPath(path) 61 | end 62 | 63 | def model_url 64 | NSBundle.mainBundle.URLForResource(model_name, withExtension: "momd"); 65 | end 66 | 67 | def app_group_container 68 | if (UIDevice.currentDevice.model =~ /simulator/i).nil? # device 69 | dir = NSFileManager.defaultManager.containerURLForSecurityApplicationGroupIdentifier(app_group_id).path 70 | elsif ! app_group_container_uuid.nil? # simulator with app group uuid workaround 71 | dev_container = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, true).last.stringByDeletingLastPathComponent.stringByDeletingLastPathComponent.stringByDeletingLastPathComponent.stringByDeletingLastPathComponent 72 | dir = dev_container.stringByAppendingPathComponent("Shared").stringByAppendingPathComponent("AppGroup").stringByAppendingPathComponent(app_group_container_uuid) 73 | else # simulator no workaround, fallback to default dir 74 | dir = NSSearchPathForDirectoriesInDomains(database_dir, NSUserDomainMask, true).last 75 | end 76 | end 77 | 78 | def self.default 79 | @default ||= 80 | begin 81 | cf_file = NSBundle.mainBundle.pathForResource("cdq", ofType: "yml"); 82 | new(cf_file) 83 | end 84 | end 85 | 86 | def self.default_config=(config_obj) 87 | @default = config_obj 88 | end 89 | 90 | private 91 | 92 | def search_directory_for dir_name 93 | supported_dirs = { 94 | "NSDocumentDirectory" => NSDocumentDirectory, 95 | :NSDocumentDirectory => NSDocumentDirectory, 96 | "NSApplicationSupportDirectory" => NSApplicationSupportDirectory, 97 | :NSApplicationSupportDirectory => NSApplicationSupportDirectory, 98 | } 99 | supported_dirs[dir_name] || NSDocumentDirectory 100 | end 101 | 102 | end 103 | 104 | end 105 | 106 | 107 | -------------------------------------------------------------------------------- /spec/cdq/targeted_query_spec.rb: -------------------------------------------------------------------------------- 1 | 2 | module CDQ 3 | describe "CDQ Targeted Query" do 4 | 5 | before do 6 | CDQ.cdq.setup 7 | end 8 | 9 | after do 10 | CDQ.cdq.reset! 11 | end 12 | 13 | it "reflects a base state" do 14 | tq = CDQTargetedQuery.new(Author.entity_description, Author) 15 | tq.count.should == 0 16 | tq.array.should == [] 17 | end 18 | 19 | it "can count objects" do 20 | tq = CDQTargetedQuery.new(Author.entity_description, Author) 21 | Author.create(name: "eecummings") 22 | tq.count.should == 1 23 | Author.create(name: "T. S. Eliot") 24 | tq.count.should == 2 25 | tq.size.should == 2 26 | tq.length.should == 2 27 | end 28 | 29 | it "can fetch objects" do 30 | tq = CDQTargetedQuery.new(Author.entity_description, Author) 31 | eecummings = Author.create(name: "eecummings") 32 | tseliot = Author.create(name: "T. S. Eliot") 33 | tq.array.sort_by(&:name).should == [tseliot, eecummings] 34 | end 35 | 36 | it "can create objects" do 37 | tq = CDQTargetedQuery.new(Author.entity_description, Author) 38 | maya = tq.create(name: "maya angelou") 39 | tq.where(:name).eq("maya angelou").first.should == maya 40 | end 41 | 42 | it 'should log query' do 43 | Article.create(title: "thing", body: "thing", author: Author.create(name: "eecummings")) 44 | Author.log(:string).should != nil 45 | Article.log(:string).should != nil 46 | end 47 | 48 | end 49 | 50 | describe "CDQ Targeted Query with data" do 51 | 52 | before do 53 | CDQ.cdq.setup 54 | 55 | class << self 56 | include CDQ 57 | end 58 | 59 | @tq = cdq(Author) 60 | @eecummings = Author.create(name: "eecummings") 61 | @tseliot = Author.create(name: "T. S. Eliot") 62 | @dante = Author.create(name: "dante") 63 | cdq.save 64 | end 65 | 66 | after do 67 | CDQ.cdq.reset! 68 | end 69 | 70 | it "performs a sorted fetch" do 71 | @tq.sort_by(:name).array.should == [@tseliot, @dante, @eecummings] 72 | end 73 | 74 | it "performs a limited fetch" do 75 | @tq.sort_by(:name).limit(1).array.should == [@tseliot] 76 | end 77 | 78 | it "performs an offset fetch" do 79 | @tq.sort_by(:name).offset(1).array.should == [@dante, @eecummings] 80 | @tq.sort_by(:name).offset(1).limit(1).array.should == [@dante] 81 | end 82 | 83 | it "performs a restricted search" do 84 | @tq.where(:name).eq("dante").array.should == [@dante] 85 | end 86 | 87 | it "gets the first entry" do 88 | @tq.sort_by(:name).first.should == @tseliot 89 | end 90 | 91 | it "gets the first n entries" do 92 | result = @tq.sort_by(:name).first(2) 93 | 94 | result.class.should == Array 95 | result.should == [@tseliot, @dante] 96 | end 97 | 98 | it "gets the last entry" do 99 | @tq.sort_by(:name).last.should == @eecummings 100 | end 101 | 102 | it "gets the last n entries" do 103 | result = @tq.sort_by(:name).last(2) 104 | 105 | result.class.should == Array 106 | result.should == [@dante, @eecummings] 107 | end 108 | 109 | it "gets entries by index" do 110 | @tq.sort_by(:name)[0].should == @tseliot 111 | @tq.sort_by(:name)[1].should == @dante 112 | @tq.sort_by(:name)[2].should == @eecummings 113 | end 114 | 115 | it "can iterate over entries" do 116 | entries = [@tseliot, @dante, @eecummings] 117 | 118 | @tq.sort_by(:name).each do |e| 119 | e.should == entries.shift 120 | end 121 | end 122 | 123 | it "returns nil if no last entry is present" do 124 | CDQ.cdq.reset! 125 | @tq.sort_by(:name).last.should == nil 126 | end 127 | 128 | it "can map over entries" do 129 | entries = [@tseliot, @dante, @eecummings] 130 | 131 | @tq.sort_by(:name).map { |e| e }.should == entries 132 | end 133 | 134 | it "can create a named scope" do 135 | @tq.scope :two_sorted_by_name, @tq.sort_by(:name).limit(2) 136 | @tq.two_sorted_by_name.array.should == [@tseliot, @dante] 137 | end 138 | 139 | it "can create a dynamic named scope" do 140 | tq = cdq(Article) 141 | a = tq.create(publishedAt: Time.local(2001)) 142 | b = tq.create(publishedAt: Time.local(2002)) 143 | c = tq.create(publishedAt: Time.local(2003)) 144 | d = tq.create(publishedAt: Time.local(2004)) 145 | 146 | tq.scope :date_span { |start_date, end_date| cdq(:publishedAt).lt(end_date).and.ge(start_date) } 147 | tq.date_span(Time.local(2002), Time.local(2004)).sort_by(:publishedAt).array.should == [b, c] 148 | Article.published_since(Time.local(2003)).sort_by(:publishedAt).array.should == [c, d] 149 | end 150 | 151 | it "can create a scope with the same name for two entities without clashing" do 152 | article = cdq('Article') 153 | a = article.create(publishedAt: Time.local(2001)) 154 | author = @tq 155 | 156 | article.scope :past, cdq(:publishedAt).lt(Time.now) 157 | author.scope :past, cdq(:name).eq('eecummings') 158 | 159 | article.past.array.should == [a] 160 | author.past.array.should == [@eecummings] 161 | 162 | end 163 | end 164 | end 165 | -------------------------------------------------------------------------------- /spec/cdq/query_spec.rb: -------------------------------------------------------------------------------- 1 | 2 | module CDQ 3 | describe "CDQ Query" do 4 | 5 | before do 6 | @query = CDQQuery.new 7 | end 8 | 9 | it "creates a query with a simple true predicate" do 10 | @query.predicate.should == nil 11 | @query.limit.should == nil 12 | @query.offset.should == nil 13 | end 14 | 15 | it "can set a limit on a query" do 16 | @query = CDQQuery.new(limit: 1) 17 | @query.limit.should == 1 18 | @query.offset.should == nil 19 | end 20 | 21 | it "can set a offset on a query" do 22 | @query = CDQQuery.new(offset: 1) 23 | @query.limit.should == nil 24 | @query.offset.should == 1 25 | end 26 | 27 | it "can 'and' itself with another query" do 28 | @query = CDQQuery.new(limit: 1, offset: 1) 29 | @other = CDQQuery.new(predicate: NSPredicate.predicateWithValue(false), limit: 2) 30 | @compound = @query.and(@other) 31 | @compound.predicate.should == NSPredicate.predicateWithValue(false) 32 | @compound.limit.should == 2 33 | @compound.offset.should == 1 34 | end 35 | 36 | it "can 'and' itself with an NSPredicate" do 37 | @compound = @query.and(NSPredicate.predicateWithValue(false)) 38 | @compound.predicate.should == NSPredicate.predicateWithValue(false) 39 | end 40 | 41 | it "can 'and' itself with a string-based predicate query" do 42 | query = @query.where(:name).begins_with('foo') 43 | compound = query.and("name != %@", 'fool') 44 | compound.predicate.predicateFormat.should == 'name BEGINSWITH "foo" AND name != "fool"' 45 | end 46 | 47 | it "can 'and' itself with a hash" do 48 | compound = @query.and(name: "foo", fee: 2) 49 | compound.predicate.predicateFormat.should == 'name == "foo" AND fee == 2' 50 | end 51 | 52 | it "starts a partial predicate when 'and'-ing a symbol" do 53 | ppred = @query.and(:name) 54 | ppred.class.should == CDQPartialPredicate 55 | ppred.key.should == :name 56 | end 57 | 58 | it "can 'or' itself with another query" do 59 | @query = CDQQuery.new(limit: 1, offset: 1) 60 | @other = CDQQuery.new(predicate: NSPredicate.predicateWithValue(false), limit: 2) 61 | @compound = @query.or(@other) 62 | @compound.predicate.should == NSPredicate.predicateWithValue(false) 63 | @compound.limit.should == 2 64 | @compound.offset.should == 1 65 | end 66 | 67 | it "can 'or' itself with an NSPredicate" do 68 | @compound = @query.or(NSPredicate.predicateWithValue(false)) 69 | @compound.predicate.should == NSPredicate.predicateWithValue(false) 70 | end 71 | 72 | it "can 'or' itself with a string-based predicate query" do 73 | query = @query.where(:name).begins_with('foo') 74 | compound = query.or("name != %@", 'fool') 75 | compound.predicate.predicateFormat.should == 'name BEGINSWITH "foo" OR name != "fool"' 76 | end 77 | 78 | it "can sort by a key" do 79 | @query.sort_by(:name).sort_descriptors.should == [ 80 | NSSortDescriptor.sortDescriptorWithKey('name', ascending: true) 81 | ] 82 | end 83 | 84 | it "can sort descending" do 85 | @query.sort_by(:name, order: :desc).sort_descriptors.should == [ 86 | NSSortDescriptor.sortDescriptorWithKey('name', ascending: false) 87 | ] 88 | end 89 | 90 | it "can sort descending (older API)" do 91 | @query.sort_by(:name, :desc).sort_descriptors.should == [ 92 | NSSortDescriptor.sortDescriptorWithKey('name', ascending: false) 93 | ] 94 | end 95 | 96 | it "can sort case insensitive" do 97 | @query.sort_by(:name, case_insensitive: true).sort_descriptors.should == [ 98 | NSSortDescriptor.sortDescriptorWithKey('name', ascending: true, selector: "localizedCaseInsensitiveCompare:") 99 | ] 100 | end 101 | 102 | it "can chain sorts" do 103 | @query.sort_by(:name).sort_by(:title).sort_descriptors.should == [ 104 | NSSortDescriptor.sortDescriptorWithKey('name', ascending: true), 105 | NSSortDescriptor.sortDescriptorWithKey('title', ascending: true) 106 | ] 107 | end 108 | 109 | it "reuses the previous key when calling 'and' or 'or' with no arguments" do 110 | compound = @query.where(:name).begins_with('foo').and.ne('fool') 111 | compound.predicate.predicateFormat.should == 'name BEGINSWITH "foo" AND name != "fool"' 112 | 113 | compound = @query.where(:name).begins_with('foo').or.eq('loofa') 114 | compound.predicate.predicateFormat.should == 'name BEGINSWITH "foo" OR name == "loofa"' 115 | end 116 | 117 | it "handles complex examples" do 118 | query1 = CDQQuery.new 119 | query2 = query1.where(CDQQuery.new.where(:name).ne('bob', NSCaseInsensitivePredicateOption).or(:amount).gt(42).sort_by(:name)) 120 | query3 = query1.where(CDQQuery.new.where(:enabled).eq(true).and(:'job.title').ne(nil).sort_by(:amount, order: :desc)) 121 | 122 | query4 = query3.where(query2) 123 | query4.predicate.predicateFormat.should == '(enabled == 1 AND job.title != nil) AND (name !=[c] "bob" OR amount > 42)' 124 | query4.sort_descriptors.should == [ 125 | NSSortDescriptor.alloc.initWithKey('amount', ascending:false), 126 | NSSortDescriptor.alloc.initWithKey('name', ascending:true) 127 | ] 128 | end 129 | 130 | it "can make a new query with a new limit" do:w 131 | @query = CDQQuery.new 132 | new_query = @query.limit(1) 133 | 134 | new_query.limit.should == 1 135 | new_query.offset.should == nil 136 | end 137 | 138 | end 139 | end 140 | -------------------------------------------------------------------------------- /spec/cdq/relationship_query_spec.rb: -------------------------------------------------------------------------------- 1 | 2 | module CDQ 3 | 4 | describe "CDQ Relationship Query" do 5 | 6 | before do 7 | 8 | class << self 9 | include CDQ 10 | end 11 | 12 | cdq.setup 13 | 14 | @author = Author.create(name: "eecummings") 15 | @article1 = @author.articles.create(author: @author, body: "", published: true, publishedAt: Time.local(1922), title: "The Enormous Room") 16 | cdq.save(always_wait: true) 17 | 18 | @article2 = @author.articles.create(author: @author, body: "", published: true, publishedAt: Time.local(1922), title: "The Ginormous Room") 19 | cdq.save(always_wait: true) 20 | 21 | @article3 = @author.articles.create(author: @author, body: "", published: true, publishedAt: Time.local(1922), title: "The Even Bigger Room") 22 | cdq.save(always_wait: true) 23 | 24 | @rq = CDQRelationshipQuery.new(@author, 'articles') 25 | end 26 | 27 | after do 28 | cdq.reset! 29 | @rq = nil 30 | end 31 | 32 | it "performs queries against the target entity" do 33 | @rq.first.should != nil 34 | @rq.first.class.should == Article_Article_ 35 | @rq.first.should == @article1 36 | end 37 | 38 | it "should be able to get the first n of the query" do 39 | @rq.first(2).should == [@article1, @article2] 40 | @rq.first(3).should == [@article1, @article2, @article3] 41 | end 42 | 43 | it "should be able to get the last of the query" do 44 | @rq.last.should != nil 45 | @rq.last.class.should == Article_Article_ 46 | @rq.last.should == @article3 47 | end 48 | 49 | it "should be able to get the last n of the query" do 50 | @rq.last(2).should == [@article2, @article3] 51 | @rq.last(3).should == [@article1, @article2, @article3] 52 | end 53 | 54 | it "should be able to use named scopes" do 55 | cdq(@author).articles.all_published.array.should == [@article1, @article2, @article3] 56 | end 57 | 58 | it "can handle many-to-many correctly" do 59 | ram = Writer.create(name: "Ram Das") 60 | first = ram.spouses.create 61 | second = ram.spouses.create 62 | ram.spouses.array.should == [first, second] 63 | cdq(first).writers.array.should == [ram] 64 | cdq(second).writers.array.should == [ram] 65 | cdq(first).writers.where(:name).contains("o").array.should == [] 66 | cdq(first).writers.where(:name).contains("a").array.should == [ram] 67 | end 68 | 69 | it "can add objects to the relationship" do 70 | article = Article.create(body: "bank") 71 | @author.articles.add(article) 72 | @author.articles.where(body: "bank").first.should == article 73 | article.author.should == @author 74 | 75 | ram = Writer.create(name: "Ram Das") 76 | ram.spouses.add cdq('Spouse').create 77 | ram.spouses << cdq('Spouse').create 78 | 79 | ram.spouses.count.should == 2 80 | ram.spouses.first.writers.count.should == 1 81 | end 82 | 83 | it "can remove a relationship and persist it" do 84 | ram = Writer.create(name: "Ram Das") 85 | ram.spouses.add cdq('Spouse').create(name: "First Spouse") 86 | ram.spouses << cdq('Spouse').create 87 | ram.spouses.count.should == 2 88 | cdq.save(always_wait: true) 89 | 90 | cdq.contexts.reset!; ram = nil; cdq.setup 91 | ram = Writer.where(:name).eq("Ram Das").first 92 | ram.spouses.count.should == 2 93 | first_spouse = ram.spouses.first 94 | first_spouse.writers.count.should == 1 95 | ram.spouses.remove(first_spouse) 96 | ram.spouses.count.should == 1 97 | first_spouse.writers.count.should == 0 98 | cdq.save(always_wait: true) 99 | 100 | cdq.contexts.reset!; ram = nil; cdq.setup 101 | ram = Writer.where(:name).eq("Ram Das").first 102 | ram.spouses.count.should == 1 103 | ex_spouse = cdq('Spouse').where(:name).eq("First Spouse").first 104 | ex_spouse.writers.count.should == 0 105 | end 106 | 107 | it "can remove objects from the relationship" do 108 | article = Article.create(title: "thing", body: "bank") 109 | cdq.save 110 | @author.articles.count.should == 3 111 | @author.articles.remove(@article1) 112 | cdq.save 113 | @author.articles.count.should == 2 114 | @author.articles.remove(@article2) 115 | cdq.save 116 | @author.articles.count.should == 1 117 | end 118 | 119 | it "can remove all objects from the relationship without deleting the linked object" do 120 | Article.count.should == 3 121 | @author.articles.count.should == 3 122 | @author.articles.remove_all 123 | cdq.save 124 | @author.articles.count.should == 0 125 | Article.count.should == 3 126 | end 127 | 128 | it "iterates over ordered sets correctly" do 129 | writer = Writer.create 130 | two = cdq('Spouse').create(name: "1") 131 | three = cdq('Spouse').create(name: "2") 132 | one = writer.spouses.create(name: "3") 133 | writer.spouses << two 134 | writer.spouses << three 135 | writer.spouses.map(&:name).should == ["3", "1", "2"] 136 | writer.spouses.array.map(&:name).should == ["3", "1", "2"] 137 | end 138 | 139 | it "cascades deletions properly" do 140 | Discussion.destroy_all! 141 | Message.destroy_all! 142 | 143 | message_count = 20 144 | 145 | discussion = Discussion.create(name: "Test Discussion") 146 | 147 | message_count.times do |i| 148 | message = Message.create(content: "Message #{i}") 149 | discussion.messages << message 150 | end 151 | cdq.save 152 | 153 | discussion.messages.count.should == message_count 154 | Message.count.should == message_count 155 | 156 | discussion.destroy 157 | cdq.save 158 | 159 | Message.count.should == 0 160 | end 161 | 162 | end 163 | 164 | end 165 | -------------------------------------------------------------------------------- /motion/managed_object.rb: -------------------------------------------------------------------------------- 1 | 2 | # CDQ Extensions for your custom entity objects. This is mostly convenience 3 | # and syntactic sugar -- you can access every feature using the cdq(Class).method 4 | # syntax, but this enables the nicer-looking and more convenient Class.method style. 5 | # Any method availble on cdq(Class) is now available directly on Class. 6 | # 7 | # If there is a conflict between a CDQ method and one of yours, or one of Core Data's, 8 | # your code will always win. In that case you can get at the CDQ method by calling 9 | # Class.cdq.method. 10 | # 11 | # Examples: 12 | # 13 | # MyEntity.where(:name).eq("John").limit(2) 14 | # MyEntity.first 15 | # MyEntity.create(name: "John") 16 | # MyEntity.sort_by(:title)[4] 17 | # 18 | # class MyEntity < CDQManagedObject 19 | # scope :last_week, where(:created_at).ge(date.delta(weeks: -2)).and.lt(date.delta(weeks: -1)) 20 | # end 21 | # 22 | # MyEntity.last_week.where(:created_by => john) 23 | # 24 | class CDQManagedObject < CoreDataQueryManagedObjectBase 25 | 26 | extend CDQ 27 | include CDQ 28 | 29 | class << self 30 | 31 | def inherited(klass) #:nodoc: 32 | cdq(klass).entity_description.relationshipsByName.each do |name, rdesc| 33 | if rdesc.isToMany 34 | klass.defineRelationshipMethod(name) 35 | end 36 | end 37 | end 38 | 39 | # Creates a CDQ scope, but also defines a method on the class that returns the 40 | # query directly. 41 | # 42 | def scope(name, query = nil, &block) 43 | cdq.scope(name, query, &block) 44 | if query 45 | define_singleton_method(name) do 46 | where(query) 47 | end 48 | else 49 | define_singleton_method(name) do |*args| 50 | where(block.call(*args)) 51 | end 52 | end 53 | end 54 | 55 | def new(*args) 56 | cdq.new(*args) 57 | end 58 | 59 | # Pass any unknown methods on to cdq. 60 | # 61 | def method_missing(name, *args, &block) 62 | cdq.send(name, *args, &block) 63 | end 64 | 65 | def respond_to?(name) 66 | if cdq_initialized? 67 | super(name) || cdq.respond_to?(name) 68 | else 69 | super(name) 70 | end 71 | end 72 | 73 | def destroy_all 74 | self.all.array.each do |instance| 75 | instance.destroy 76 | end 77 | end 78 | 79 | def destroy_all! 80 | destroy_all 81 | cdq.save 82 | end 83 | 84 | def attribute_names 85 | self.entity_description.attributesByName.keys 86 | end 87 | 88 | def cdq(obj = nil) 89 | if obj 90 | super(obj) 91 | else 92 | @cdq_object ||= super(nil) 93 | end 94 | end 95 | 96 | def cdq_initialized? 97 | !@cdq_object.nil? 98 | end 99 | 100 | end 101 | 102 | def update(args) 103 | args.each do |k,v| 104 | if respond_to?("#{k}=") 105 | self.send("#{k}=", v) 106 | else 107 | raise UnknownAttributeError.new("#{self.class} does not respond to `#{k}=`") 108 | end 109 | end if args.is_a?(Hash) 110 | end 111 | 112 | # Register this object for destruction with the current context. Will not 113 | # actually be removed until the context is saved. 114 | # 115 | def destroy 116 | managedObjectContext.deleteObject(self) 117 | end 118 | 119 | def inspect 120 | description 121 | end 122 | 123 | # Returns a hash of attributes for the object 124 | def attributes 125 | h = {} 126 | entity.attributesByName.each do |name, desc| 127 | h[name] = send name 128 | end 129 | h 130 | end 131 | 132 | def log(log_type = nil) 133 | out = "\nOID: " 134 | out << oid 135 | out << "\n" 136 | 137 | atts = entity.attributesByName 138 | rels = entity.relationshipsByName 139 | 140 | width = (atts.keys.map(&:length) + rels.keys.map(&:length)).max || 0 141 | 142 | atts.each do |name, desc| 143 | out << " #{name.ljust(width)} : " 144 | out << send(name).inspect[0,95 - width] 145 | out << "\n" 146 | end 147 | 148 | rels.each do |name, desc| 149 | rel = CDQRelationshipQuery.new(self, name, nil, context: managedObjectContext) 150 | if desc.isToMany 151 | out << " #{name.ljust(width)} : " 152 | out << rel.count.to_s 153 | out << ' (count)' 154 | else 155 | out << " #{name.ljust(width)} : " 156 | out << (rel.first && rel.first.oid || "nil") 157 | end 158 | end 159 | out << "\n" 160 | 161 | if log_type == :string 162 | out 163 | else 164 | NSLog out 165 | end 166 | end 167 | 168 | # Opt-in support for motion_print 169 | def motion_print(mp, options) 170 | if respond_to? :attributes 171 | "OID: " + mp.colorize(oid.gsub('"',''), options[:force_color]) + "\n" + mp.l_hash(attributes, options) 172 | else 173 | # old colorless method, still more informative than nothing 174 | log 175 | end 176 | end 177 | 178 | def ordered_set?(name) 179 | # isOrdered is returning 0/1 instead of documented BOOL 180 | ordered = entity.relationshipsByName[name].isOrdered 181 | return true if ordered == true || ordered == 1 182 | return false if ordered == false || ordered == 0 183 | end 184 | 185 | def set_to_extend(name) 186 | if ordered_set?(name) 187 | mutableOrderedSetValueForKey(name) 188 | else 189 | mutableSetValueForKey(name) 190 | end 191 | end 192 | 193 | def oid 194 | objectID.URIRepresentation.absoluteString.inspect 195 | end 196 | 197 | def method_missing(method, *args, &block) 198 | name = method.to_s 199 | 200 | if name.end_with? "?" 201 | property_name = name.chop 202 | property = entity.propertiesByName[property_name] 203 | 204 | if property && property.attributeType == NSBooleanAttributeType 205 | return send(property_name) == 1 206 | end 207 | end 208 | 209 | super 210 | end 211 | 212 | protected 213 | 214 | # Called from method that's dynamically added from 215 | # +[CoreDataManagedObjectBase defineRelationshipMethod:] 216 | def relationshipByName(name) 217 | willAccessValueForKey(name) 218 | set = CDQRelationshipQuery.extend_set(set_to_extend(name), self, name) 219 | didAccessValueForKey(name) 220 | set 221 | end 222 | 223 | end 224 | 225 | class UnknownAttributeError < StandardError; end 226 | -------------------------------------------------------------------------------- /spec/cdq/managed_object_spec.rb: -------------------------------------------------------------------------------- 1 | module CDQ 2 | describe "CDQ Managed Object" do 3 | 4 | before do 5 | CDQ.cdq.setup 6 | 7 | class << self 8 | include CDQ 9 | end 10 | end 11 | 12 | after do 13 | CDQ.cdq.reset! 14 | end 15 | 16 | it "provides a cdq class method" do 17 | Writer.cdq.class.should == CDQTargetedQuery 18 | end 19 | 20 | it "has a where method" do 21 | Writer.where(:name).eq('eecummings').class.should == CDQTargetedQuery 22 | end 23 | 24 | it "has a sort_by method" do 25 | Writer.sort_by(:name).class.should == CDQTargetedQuery 26 | end 27 | 28 | it "has a first method" do 29 | eec = cdq(Writer).create(name: 'eecummings') 30 | Writer.first.should == eec 31 | end 32 | 33 | it "has an all method" do 34 | eec = cdq(Writer).create(name: 'eecummings') 35 | Writer.all.array.should == [eec] 36 | end 37 | 38 | it "can destroy itself" do 39 | eec = cdq(Writer).create(name: 'eecummings') 40 | eec.destroy 41 | Writer.all.array.should == [] 42 | end 43 | 44 | it "returns the attributes of the entity" do 45 | Writer.attribute_names.should.include(:name) 46 | end 47 | 48 | it "allows boolean access via ? methods" do 49 | first = Article.create(published: true, title: "First Article") 50 | second = Article.create(published: false, title: "Second Article") 51 | 52 | first.published.should == 1 53 | first.published?.should == true 54 | 55 | second.published.should == 0 56 | second.published?.should == false 57 | end 58 | 59 | it "allows single property updates with `update()`" do 60 | article = Article.create(published: true, title: "First Article") 61 | article.published?.should == true 62 | article.title.should == "First Article" 63 | article_object_id = article.object_id 64 | 65 | article.update(title: "First Article Fixed") 66 | article.published?.should == true 67 | article.title.should == "First Article Fixed" 68 | article.object_id.should == article_object_id 69 | end 70 | 71 | it "allows multiple property updates with `update()`" do 72 | article = Article.create(published: true, title: "First Article") 73 | article.published?.should == true 74 | article.title.should == "First Article" 75 | article_object_id = article.object_id 76 | 77 | article.update(published: false, title: "First Article Fixed") 78 | article.published?.should == false 79 | article.title.should == "First Article Fixed" 80 | article.object_id.should == article_object_id 81 | end 82 | 83 | it "raises an error when trying to update a property that doesn't exist with `update()`" do 84 | article = Article.create(published: true, title: "First Article") 85 | 86 | should.raise(UnknownAttributeError) do 87 | article.update(doesnt_exist: true) 88 | end 89 | end 90 | 91 | it "does not crash when respond_to? called on CDQManagedObject directly" do 92 | should.not.raise do 93 | CDQManagedObject.respond_to?(:foo) 94 | end 95 | end 96 | 97 | it "can destroy all instances of itself" do 98 | cdq(Writer).create(name: 'Dean Kuntz') 99 | cdq(Writer).create(name: 'Stephen King') 100 | cdq(Writer).create(name: 'Tom Clancy') 101 | Writer.count.should == 3 102 | 103 | Writer.destroy_all! 104 | Writer.count.should == 0 105 | end 106 | 107 | it "works with entities that do not have a specific implementation class" do 108 | rh = cdq('Publisher').create(name: "Random House") 109 | cdq.save 110 | cdq('Publisher').where(:name).include("Random").first.should == rh 111 | rh.destroy 112 | cdq.save 113 | cdq('Publisher').where(:name).include("Random").first.should == nil 114 | end 115 | 116 | it "returns relationship sets which can behave like CDQRelationshipQuery objects" do 117 | eec = Author.create(name: 'eecummings') 118 | art = eec.articles.create(title: 'something here') 119 | eec.articles.sort_by(:title).first.should == art 120 | end 121 | 122 | it "returns a hash of attributes" do 123 | john = cdq(Writer).create(fee: 21.2, name: 'John Grisham') 124 | john.attributes['fee'].round(1).should.to_s == '21.2' # ugh floating point 125 | john.attributes['name'].should == 'John Grisham' 126 | end 127 | 128 | describe "properly raises NoMehtodError" do 129 | before do 130 | @art = Article.new 131 | end 132 | 133 | it "with getter method" do 134 | should.raise(NoMethodError) do 135 | @art.oh_my_how_we_will_even_survived_this 136 | end 137 | end 138 | 139 | it "with setter method" do 140 | should.raise(NoMethodError) do 141 | @art.oh_my_how_we_will_even_survived_this = true 142 | end 143 | end 144 | 145 | it "with question mark method" do 146 | should.raise(NoMethodError) do 147 | @art.oh_my_how_we_will_even_survived_this? 148 | end 149 | end 150 | end 151 | 152 | describe "respond_to?" do 153 | 154 | before do 155 | @art = Article.new 156 | end 157 | 158 | it "works with setters" do 159 | @art.respond_to?(:"title=").should == true 160 | end 161 | 162 | end 163 | 164 | describe "CDQ Managed Object scopes" do 165 | 166 | before do 167 | class Writer 168 | scope :eecummings, where(:name).eq('eecummings') 169 | scope :edgaralpoe, cdq(:name).eq('edgar allen poe') 170 | scope :by_name { |name| cdq(:name).eq(name) } 171 | end 172 | @eec = cdq(Writer).create(name: 'eecummings') 173 | @poe = cdq(Writer).create(name: 'edgar allen poe') 174 | end 175 | 176 | it "defines scopes straight on the class object" do 177 | Writer.eecummings.array.should == [@eec] 178 | Writer.edgaralpoe.array.should == [@poe] 179 | end 180 | 181 | it "also defines scopes on the cdq object" do 182 | cdq('Writer').eecummings.array.should == [@eec] 183 | cdq('Writer').edgaralpoe.array.should == [@poe] 184 | end 185 | 186 | it "are not clashing" do 187 | article = Article.create(title: 'war & peace') 188 | writer = Writer.create(name: 'rbachman', fee: 42.0) 189 | 190 | Article.clashing.array.should == [article] 191 | Writer.clashing.array.should == [writer] 192 | 193 | cdq('Article').clashing.array.should == [article] 194 | cdq('Writer').clashing.array.should == [writer] 195 | end 196 | 197 | it "are available to respond_to?" do 198 | Writer.respond_to?(:by_name).should == true 199 | end 200 | 201 | describe "CDQ Managed Object dynamic scopes" do 202 | 203 | class Writer 204 | scope :by_name { |name| cdq(:name).eq(name) } 205 | end 206 | 207 | it "uses the variable you passed in" do 208 | Writer.by_name('eecummings').array.should == [@eec] 209 | Writer.by_name('edgar allen poe').array.should == [@poe] 210 | end 211 | end 212 | end 213 | end 214 | end 215 | -------------------------------------------------------------------------------- /motion/cdq/query.rb: -------------------------------------------------------------------------------- 1 | 2 | module CDQ 3 | 4 | # 5 | # CDQ Queries are the primary way of describing a set of objects. 6 | # 7 | class CDQQuery < CDQObject 8 | 9 | # @private 10 | # 11 | # This is a singleton object needed to represent "empty" in limit and 12 | # offset, because they need to be able to accept nil as a real value. 13 | # 14 | EMPTY = Object.new 15 | 16 | attr_reader :predicate, :sort_descriptors 17 | 18 | def initialize(opts = {}) 19 | @predicate = opts[:predicate] 20 | @limit = opts[:limit] 21 | @offset = opts[:offset] 22 | @sort_descriptors = opts[:sort_descriptors] || [] 23 | @saved_key = opts[:saved_key] 24 | end 25 | 26 | # Return or set the fetch limit. If passed an argument, return a new 27 | # query with the specified limit value. Otherwise, return the current 28 | # value. 29 | # 30 | def limit(value = EMPTY) 31 | if value == EMPTY 32 | @limit 33 | else 34 | clone(limit: value) 35 | end 36 | end 37 | 38 | # Return or set the fetch offset. If passed an argument, return a new 39 | # query with the specified offset value. Otherwise, return the current 40 | # value. 41 | # 42 | def offset(value = EMPTY) 43 | if value == EMPTY 44 | @offset 45 | else 46 | clone(offset: value) 47 | end 48 | end 49 | 50 | # Combine this query with others in an intersection ("and") relationship. Can be 51 | # used to begin a new query as well, especially when called in its where 52 | # variant. 53 | # 54 | # The query passed in can be a wide variety of types: 55 | # 56 | # Symbol: This is by far the most common, and it is also a special 57 | # case -- the return value when passing a symbol is a CDQPartialPredicate, 58 | # rather than CDQQuery. Methods on CDQPartialPredicate are then comparison 59 | # operators against the attribute indicated by the symbol itself, which take 60 | # a value operand. For example: 61 | # 62 | # query.where(:name).equal("Chuck").and(:title).not_equal("Manager") 63 | # 64 | # @see CDQPartialPredicate 65 | # 66 | # String: Interpreted as an NSPredicate format string. Additional arguments are 67 | # the positional parameters. 68 | # 69 | # NilClass: If the argument is nil (most likely because it was omitted), and there 70 | # was a previous use of a symbol, then reuse that last symbol. For example: 71 | # 72 | # query.where(:name).contains("Chuck").and.contains("Norris") 73 | # 74 | # CDQQuery: If you have another CDQQuery from somewhere else, you can pass it in directly. 75 | # 76 | # NSPredicate: You can pass in a raw NSPredicate and it will work as you'd expect. 77 | # 78 | # Hash: Each key/value pair is treated as equality and anded together. 79 | # 80 | def and(query = nil, *args) 81 | merge_query(query, :and, *args) do |left, right| 82 | NSCompoundPredicate.andPredicateWithSubpredicates([left, right]) 83 | end 84 | end 85 | alias_method :where, :and 86 | 87 | # Combine this query with others in a union ("or") relationship. Accepts 88 | # all the same argument types as and. 89 | def or(query = nil, *args) 90 | merge_query(query, :or, *args) do |left, right| 91 | NSCompoundPredicate.orPredicateWithSubpredicates([left, right]) 92 | end 93 | end 94 | 95 | # Add a new sort key. Multiple invocations add additional sort keys rather than replacing 96 | # old ones. 97 | # 98 | # @param key The attribute to sort on 99 | # @param options Optional options. 100 | # 101 | # Options include: 102 | # order: If :descending or (or :desc), order is descrnding. Otherwise, 103 | # it is ascending. 104 | # case_insensitive: True or false. If true, the sort descriptor is 105 | # built using localizedCaseInsensitiveCompare 106 | # 107 | def sort_by(key, options = {}) 108 | # backwards compat: if options is not a hash, it is a sort ordering. 109 | unless options.is_a?Hash 110 | sort_order = options 111 | options = { 112 | order: sort_order, 113 | case_insensitive: false, 114 | } 115 | end 116 | 117 | options = { 118 | order: :ascending, 119 | }.merge(options) 120 | 121 | order = options[:order].to_s 122 | 123 | if order[0,4].downcase == 'desc' 124 | ascending = false 125 | else 126 | ascending = true 127 | end 128 | 129 | if options[:case_insensitive] 130 | descriptor = NSSortDescriptor.sortDescriptorWithKey(key, ascending: ascending, selector: "localizedCaseInsensitiveCompare:") 131 | else 132 | descriptor = NSSortDescriptor.sortDescriptorWithKey(key, ascending: ascending) 133 | end 134 | 135 | clone(sort_descriptors: @sort_descriptors + [descriptor]) 136 | end 137 | 138 | # Return an NSFetchRequest that will implement this query 139 | def fetch_request 140 | NSFetchRequest.new.tap do |req| 141 | req.predicate = predicate 142 | req.fetchLimit = limit if limit 143 | req.fetchOffset = offset if offset 144 | req.sortDescriptors = sort_descriptors unless sort_descriptors.empty? 145 | end 146 | end 147 | 148 | private 149 | 150 | # Create a new query with the same values as this one, optionally overriding 151 | # any of them in the options 152 | def clone(opts = {}) 153 | self.class.new(locals.merge(opts)) 154 | end 155 | 156 | def locals 157 | { sort_descriptors: sort_descriptors, 158 | predicate: predicate, 159 | limit: limit, 160 | offset: offset } 161 | end 162 | 163 | def merge_query(query, operation, *args, &block) 164 | key_to_save = nil 165 | case query 166 | when Hash 167 | subquery = query.inject(CDQQuery.new) do |memo, (key, value)| 168 | memo.and(key).eq(value) 169 | end 170 | other_predicate = subquery.predicate 171 | new_limit = limit 172 | new_offset = offset 173 | new_sort_descriptors = sort_descriptors 174 | when Symbol 175 | return CDQPartialPredicate.new(query, self, operation) 176 | when NilClass 177 | if @saved_key 178 | return CDQPartialPredicate.new(@saved_key, self, operation) 179 | else 180 | raise "Zero-argument 'and' and 'or' can only be used if there is a key in the preceding predicate" 181 | end 182 | when CDQQuery 183 | new_limit = [limit, query.limit].compact.last 184 | new_offset = [offset, query.offset].compact.last 185 | new_sort_descriptors = sort_descriptors + query.sort_descriptors 186 | other_predicate = query.predicate 187 | when NSPredicate 188 | other_predicate = query 189 | new_limit = limit 190 | new_offset = offset 191 | new_sort_descriptors = sort_descriptors 192 | key_to_save = args.first 193 | when String 194 | other_predicate = NSPredicate.predicateWithFormat(query, argumentArray: args) 195 | new_limit = limit 196 | new_offset = offset 197 | new_sort_descriptors = sort_descriptors 198 | end 199 | if predicate 200 | new_predicate = block.call(predicate, other_predicate) 201 | else 202 | new_predicate = other_predicate 203 | end 204 | clone(predicate: new_predicate, limit: new_limit, offset: new_offset, sort_descriptors: new_sort_descriptors, saved_key: key_to_save) 205 | end 206 | 207 | end 208 | end 209 | 210 | -------------------------------------------------------------------------------- /motion/cdq/targeted_query.rb: -------------------------------------------------------------------------------- 1 | 2 | module CDQ #:nodoc: 3 | 4 | class CDQTargetedQuery < CDQQuery 5 | 6 | include Enumerable 7 | 8 | attr_reader :entity_description 9 | 10 | # Create a new CDQTargetedContext. Takes an entity description, an optional 11 | # implementation class, and a hash of options that will be passed to the CDQQuery 12 | # constructor. 13 | # 14 | def initialize(entity_description, target_class = nil, opts = {}) 15 | @entity_description = entity_description 16 | @target_class = target_class || 17 | NSClassFromString(entity_description.managedObjectClassName) || 18 | CDQManagedObject 19 | @context = opts.delete(:context) 20 | super(opts) 21 | end 22 | 23 | # The current context, taken from the environment or overriden by in_context 24 | # 25 | def context 26 | @context || contexts.current 27 | end 28 | 29 | # Return the number of matching entities. 30 | # 31 | # Causes execution. 32 | # 33 | def count 34 | raise("No context has been set. Probably need to run cdq.setup") unless context 35 | with_error_object(0) do |error| 36 | context.countForFetchRequest(fetch_request, error:error) 37 | end 38 | end 39 | alias :length :count 40 | alias :size :count 41 | 42 | # Return all matching entities. 43 | # 44 | # Causes execution. 45 | # 46 | def array 47 | raise("No context has been set. Probably need to run cdq.setup") unless context 48 | with_error_object([]) do |error| 49 | context.executeFetchRequest(fetch_request, error:error) 50 | end 51 | end 52 | alias_method :to_a, :array 53 | 54 | # Convenience method for referring to all matching entities. No-op. You must 55 | # still call array or another executing method 56 | # 57 | def all 58 | self 59 | end 60 | 61 | # Return the first entity matching the query. 62 | # 63 | # Causes execution. 64 | # 65 | def first(n = 1) 66 | result = limit(n).array 67 | n == 1 ? result.first : result 68 | end 69 | 70 | # Return the last entity matching the query. 71 | # 72 | # Causes execution. 73 | # 74 | def last(n = 1) 75 | return nil if count == 0 76 | result = offset(count - n).limit(n).array 77 | n == 1 ? result.first : result 78 | end 79 | 80 | # Fetch a single entity from the query by index. If the optional 81 | # length parameter is supplied, fetch a range of length length 82 | # starting at index 83 | # 84 | # Causes execution. 85 | # 86 | def [](index, length = nil) 87 | if length 88 | offset(index).limit(length).array 89 | else 90 | offset(index).first 91 | end 92 | end 93 | 94 | # Iterate over each entity matched by the query. You can also use any method from the 95 | # Enumerable module in the standard library that does not depend on ordering. 96 | # 97 | # Causes execution. 98 | # 99 | def each(*args, &block) 100 | array.each(*args, &block) 101 | end 102 | 103 | # Calculation method based on core data aggregate functions. 104 | def calculate(operation, column_name) 105 | raise("No context has been set. Probably need to run cdq.setup") unless context 106 | raise("Cannot find attribute #{column_name} while calculating #{operation}") unless @entity_description.attributesByName[column_name.to_s] 107 | desc_name = operation.to_s + '_of_' + column_name.to_s 108 | fr = fetch_request.tap do |req| 109 | req.propertiesToFetch = [ 110 | NSExpressionDescription.alloc.init.tap do |desc| 111 | desc.name = desc_name 112 | desc.expression = NSExpression.expressionForFunction(operation.to_s + ':', arguments: [ NSExpression.expressionForKeyPath(column_name.to_s) ]) 113 | desc.expressionResultType = @entity_description.attributesByName[column_name.to_s].attributeType 114 | end 115 | ] 116 | req.resultType = NSDictionaryResultType 117 | end 118 | with_error_object([]) do |error| 119 | r = context.executeFetchRequest(fr, error:error) 120 | r.first[desc_name] 121 | end 122 | end 123 | 124 | # Calculates the sum of values on a given column. 125 | # 126 | # Author.sum(:fee) # => 6.0 127 | def sum(column_name) 128 | calculate(:sum, column_name) 129 | end 130 | 131 | # Calculates the average of values on a given column. 132 | # 133 | # Author.average(:fee) # => 2.0 134 | def average(column_name) 135 | calculate(:average, column_name) 136 | end 137 | 138 | # Calculates the minimum of values on a given column. 139 | # 140 | # Author.min(:fee) # => 1.0 141 | def min(column_name) 142 | calculate(:min, column_name) 143 | end 144 | alias :minimum :min 145 | 146 | # Calculates the maximum of values on a given column. 147 | # 148 | # Author.max(:fee) # => 3.0 149 | def max(column_name) 150 | calculate(:max, column_name) 151 | end 152 | alias :maximum :max 153 | 154 | # Returns the fully-contstructed fetch request, which can be executed outside of CDQ. 155 | # 156 | def fetch_request 157 | super.tap do |req| 158 | req.entity = @entity_description 159 | req.predicate ||= NSPredicate.predicateWithValue(true) 160 | end 161 | end 162 | 163 | # Create a new entity in the current context. Accepts a hash of attributes that will be assigned to 164 | # the newly-created entity. Does not save the context. 165 | # 166 | def new(opts = {}) 167 | @target_class.alloc.initWithEntity(@entity_description, insertIntoManagedObjectContext: context).tap do |entity| 168 | opts.each { |k, v| entity.send("#{k}=", v) } 169 | end 170 | end 171 | 172 | # Create a new entity in the current context. Accepts a hash of attributes that will be assigned to 173 | # the newly-created entity. Does not save the context. 174 | # 175 | def create(*args) 176 | new(*args) 177 | end 178 | 179 | # Create a named scope. The query is any valid CDQ query. 180 | # 181 | # Example: 182 | # 183 | # cdq('Author').scope(:first_published, cdq(:published).eq(true).sort_by(:published_at).limit(1)) 184 | # 185 | # cdq('Author').first_published.first => # 186 | # 187 | def scope(name, query = nil, &block) 188 | if query.nil? && block_given? 189 | named_scopes[name] = block 190 | elsif query 191 | named_scopes[name] = query 192 | else 193 | raise ArgumentError.new("You must supply a query OR a block that returns a query to scope") 194 | end 195 | end 196 | 197 | # Override the context in which to perform this query. This forever forces the 198 | # specified context for this particular query, so if you save the it for later 199 | # use (such as defining a scope) bear in mind that changes in the default context 200 | # will have no effect when running this. 201 | # 202 | def in_context(context) 203 | clone(context: context) 204 | end 205 | 206 | # Any unknown method will be checked against the list of named scopes. 207 | # 208 | def method_missing(name, *args) 209 | scope = named_scopes[name] 210 | case scope 211 | when CDQQuery 212 | where(scope) 213 | when Proc 214 | where(scope.call(*args)) 215 | else 216 | super(name, *args) 217 | end 218 | end 219 | 220 | def log(log_type = nil) 221 | out = "\n\n ATTRIBUTES" 222 | out << " \n Name | type | default |" 223 | line = " \n- - - - - - - - - - - | - - - - - - - - - - | - - - - - - - - - - - - - - - |" 224 | out << line 225 | 226 | entity_description.attributesByName.each do |name, desc| 227 | out << " \n #{name.ljust(21)}|" 228 | out << " #{desc.attributeValueClassName.ljust(20)}|" 229 | out << " #{desc.defaultValue.to_s.ljust(30)}|" 230 | end 231 | 232 | out << line 233 | out << "\n\n" 234 | 235 | self.each do |o| 236 | out << o.log(:string) 237 | end 238 | 239 | if log_type == :string 240 | out 241 | else 242 | NSLog out 243 | end 244 | end 245 | 246 | private 247 | 248 | def oid(obj) 249 | obj ? obj.oid : "nil" 250 | end 251 | 252 | def named_scopes 253 | @@named_scopes ||= {} 254 | @@named_scopes[@entity_description] ||= {} 255 | end 256 | 257 | def clone(opts = {}) 258 | CDQTargetedQuery.new(@entity_description, @target_class, locals.merge(opts)) 259 | end 260 | 261 | end 262 | end 263 | -------------------------------------------------------------------------------- /motion/cdq/context.rb: -------------------------------------------------------------------------------- 1 | module CDQ 2 | 3 | class CDQContextManager 4 | 5 | include CDQ::Deprecation 6 | 7 | BACKGROUND_SAVE_NOTIFICATION = 'com.infinitered.cdq.context.background_save_completed' 8 | DID_FINISH_IMPORT_NOTIFICATION = 'com.infinitered.cdq.context.did_finish_import' 9 | 10 | def initialize(opts = {}) 11 | @store_manager = opts[:store_manager] 12 | end 13 | 14 | def dealloc 15 | NSNotificationCenter.defaultCenter.removeObserver(self) if @observed_context 16 | super 17 | end 18 | 19 | # Push a new context onto the stack for the current thread, making that context the 20 | # default. If a block is supplied, push for the duration of the block and then 21 | # return to the previous state. 22 | # 23 | def push(context, options = {}, &block) 24 | @has_been_set_up = true 25 | 26 | if !context.is_a?(NSManagedObjectContext) 27 | context = create(context, options) 28 | elsif options[:named] 29 | assign_name(options[:named], context) 30 | end 31 | 32 | if block_given? 33 | save_stack do 34 | context = push_to_stack(context) 35 | block.call 36 | context 37 | end 38 | else 39 | push_to_stack(context) 40 | end 41 | end 42 | 43 | # Pop the top context off the stack. If a block is supplied, pop for the 44 | # duration of the block and then return to the previous state. 45 | # 46 | def pop(&block) 47 | if block_given? 48 | save_stack do 49 | rval = pop_from_stack 50 | block.call 51 | rval 52 | end 53 | else 54 | pop_from_stack 55 | end 56 | end 57 | 58 | # The current context at the top of the stack. 59 | # 60 | def current 61 | if stack.empty? && !@has_been_set_up 62 | push(NSMainQueueConcurrencyType) 63 | end 64 | stack.last 65 | end 66 | 67 | # An array of all contexts, from bottom to top of the stack. 68 | # 69 | def all 70 | stack.dup 71 | end 72 | 73 | # Remove all contexts. 74 | # 75 | def reset! 76 | self.stack = [] 77 | end 78 | 79 | # Create a new context by type, setting upstream to the topmost context if available, 80 | # or to the persistent store coordinator if not. Return the context but do NOT push it 81 | # onto the stack. 82 | # 83 | # Options: 84 | # 85 | # :named - Assign the context a name, making it available as cdq.contexts.. The 86 | # name is permanent, and should only be used for contexts that are intended to be global, 87 | # since the object will never get released. 88 | # 89 | def create(concurrency_type, options = {}, &block) 90 | @has_been_set_up = true 91 | 92 | case concurrency_type 93 | when :main 94 | context = NSManagedObjectContext.alloc.initWithConcurrencyType(NSMainQueueConcurrencyType) 95 | options[:named] = :main unless options.has_key?(:named) 96 | when :private_queue, :private 97 | context = NSManagedObjectContext.alloc.initWithConcurrencyType(NSPrivateQueueConcurrencyType) 98 | when :root 99 | context = NSManagedObjectContext.alloc.initWithConcurrencyType(NSPrivateQueueConcurrencyType) 100 | options[:named] = :root unless options.has_key?(:named) 101 | else 102 | context = NSManagedObjectContext.alloc.initWithConcurrencyType(concurrency_type) 103 | end 104 | 105 | if stack.empty? 106 | if @store_manager.invalid? 107 | raise "store coordinator not found. Cannot create the first context without one." 108 | else 109 | context.mergePolicy = NSMergePolicy.alloc.initWithMergeType(NSMergeByPropertyObjectTrumpMergePolicyType) 110 | context.performBlockAndWait ->{ 111 | coordinator = @store_manager.current 112 | context.persistentStoreCoordinator = coordinator 113 | 114 | NSNotificationCenter.defaultCenter.addObserver(self, selector:"did_finish_import:", name:NSPersistentStoreDidImportUbiquitousContentChangesNotification, object:nil) 115 | @observed_context = context 116 | } 117 | end 118 | else 119 | context.parentContext = stack.last 120 | end 121 | 122 | if options[:named] 123 | assign_name(options[:named], context) 124 | end 125 | context 126 | end 127 | 128 | # Save all passed contexts in order. If none are supplied, save all 129 | # contexts in the stack, starting with the current and working down. If 130 | # you pass a symbol instead of a context, it will look up context with 131 | # that name. 132 | # 133 | # Options: 134 | # 135 | # always_wait: If true, force use of performBlockAndWait for synchronous 136 | # saves. By default, private queue saves are performed asynchronously. 137 | # Main queue saves are always synchronous if performed from the main 138 | # queue. 139 | # 140 | def save(*contexts_and_options) 141 | 142 | if contexts_and_options.last.is_a? Hash 143 | options = contexts_and_options.pop 144 | else 145 | options = {} 146 | end 147 | 148 | if contexts_and_options.empty? 149 | contexts = stack.reverse 150 | else 151 | # resolve named contexts 152 | contexts = contexts_and_options.map do |c| 153 | if c.is_a? Symbol 154 | send(c) 155 | else 156 | c 157 | end 158 | end 159 | end 160 | 161 | set_timestamps 162 | always_wait = options[:always_wait] 163 | contexts.each do |context| 164 | if context.concurrencyType == NSMainQueueConcurrencyType && NSThread.isMainThread 165 | with_error_object do |error| 166 | context.save(error) 167 | end 168 | elsif always_wait 169 | context.performBlockAndWait( -> { 170 | 171 | with_error_object do |error| 172 | context.save(error) 173 | end 174 | 175 | } ) 176 | elsif context.concurrencyType == NSPrivateQueueConcurrencyType 177 | task_id = UIApplication.sharedApplication.beginBackgroundTaskWithExpirationHandler( -> { NSLog "CDQ Save Timed Out" } ) 178 | 179 | if task_id == UIBackgroundTaskInvalid 180 | context.performBlockAndWait( -> { 181 | 182 | with_error_object do |error| 183 | context.save(error) 184 | end 185 | 186 | } ) 187 | else 188 | context.performBlock( -> { 189 | 190 | # Let the application know we're doing something important 191 | with_error_object do |error| 192 | context.save(error) 193 | end 194 | 195 | UIApplication.sharedApplication.endBackgroundTask(task_id) 196 | 197 | NSNotificationCenter.defaultCenter.postNotificationName(BACKGROUND_SAVE_NOTIFICATION, object: context) 198 | 199 | } ) 200 | end 201 | else 202 | with_error_object do |error| 203 | context.save(error) 204 | end 205 | end 206 | end 207 | true 208 | end 209 | 210 | # Run the supplied block in a new context with a private queue. Once the 211 | # block exits, the context will be forgotten, so any changes made must be 212 | # saved within the block. 213 | # 214 | # Note that the CDQ context stack, which is used when deciding what to save 215 | # with `cdq.save` is stored per-thread, so the stack inside the block is 216 | # different from the stack outside the block. If you push any more contexts 217 | # inside, they will also disappear when the thread terminates. 218 | # 219 | # The thread is also unique. If you call `background` multiple times, it will 220 | # be a different thread each time with no persisted state. 221 | # 222 | # Options: 223 | # wait: If true, run the block synchronously 224 | # 225 | def background(options = {}, &block) 226 | # Create a new private queue context with the main context as its parent 227 | context = create(NSPrivateQueueConcurrencyType) 228 | 229 | on(context, options) do 230 | push(context, {}, &block) 231 | end 232 | 233 | end 234 | 235 | # Run a block on the supplied context using performBlock. If context is a 236 | # symbol, it will look up the corresponding named context and use that 237 | # instead. 238 | # 239 | # Options: 240 | # wait: If true, run the block synchronously 241 | # 242 | def on(context, options = {}, &block) 243 | 244 | if context.is_a? Symbol 245 | context = send(context) 246 | end 247 | 248 | if options[:wait] 249 | context.performBlockAndWait(block) 250 | else 251 | context.performBlock(block) 252 | end 253 | end 254 | 255 | def did_finish_import(notification) 256 | @observed_context.performBlockAndWait ->{ 257 | @observed_context.mergeChangesFromContextDidSaveNotification(notification) 258 | NSNotificationCenter.defaultCenter.postNotificationName(DID_FINISH_IMPORT_NOTIFICATION, object:self, userInfo:{context: @observed_context}) 259 | } 260 | end 261 | 262 | 263 | private 264 | 265 | def assign_name(name, context) 266 | if respond_to?(name) 267 | raise "Cannot name a context '#{name}': conflicts with existing method" 268 | end 269 | self.class.send(:define_method, name) do 270 | context 271 | end 272 | end 273 | 274 | def push_to_stack(value) 275 | lstack = stack 276 | lstack << value 277 | self.stack = lstack 278 | value 279 | end 280 | 281 | def pop_from_stack 282 | lstack = stack 283 | value = lstack.pop 284 | self.stack = lstack 285 | value 286 | end 287 | 288 | def save_stack(&block) 289 | begin 290 | saved_stack = all 291 | block.call 292 | ensure 293 | self.stack = saved_stack 294 | end 295 | end 296 | 297 | def stack 298 | Thread.current[:"cdq.context.stack.#{object_id}"] || [] 299 | end 300 | 301 | def stack=(value) 302 | Thread.current[:"cdq.context.stack.#{object_id}"] = value 303 | end 304 | 305 | def with_error_object(default = nil, &block) 306 | error = Pointer.new(:object) 307 | result = block.call(error) 308 | if error[0] 309 | print_error("Error while fetching", error[0]) 310 | raise "Error while fetching: #{error[0].debugDescription}" 311 | end 312 | result || default 313 | end 314 | 315 | def print_error(message, error, indent = "") 316 | puts indent + message + error.localizedDescription 317 | if error.userInfo['reason'] 318 | puts indent + error.userInfo['reason'] 319 | end 320 | if error.userInfo['metadata'] 321 | error.userInfo['metadata'].each do |key, value| 322 | puts indent + "#{key}: #{value}" 323 | end 324 | end 325 | if !error.userInfo[NSDetailedErrorsKey].nil? 326 | error.userInfo[NSDetailedErrorsKey].each do |key, value| 327 | if key.instance_of? NSError 328 | print_error("Sub-Error: ", key, indent + " ") 329 | else 330 | puts indent + "#{key}: #{value}" 331 | end 332 | end 333 | end 334 | end 335 | 336 | def set_timestamps 337 | now = Time.now 338 | 339 | current.insertedObjects.allObjects.each do |e| 340 | e.created_at = now if e.respond_to? :created_at= 341 | e.updated_at = now if e.respond_to? :updated_at= 342 | end 343 | 344 | current.updatedObjects.allObjects.each do |e| 345 | e.updated_at = now if e.respond_to? :updated_at= 346 | end 347 | 348 | end 349 | 350 | end 351 | 352 | end 353 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > [!IMPORTANT] 2 | > CDQ has been archived is no longer maintained. Thanks everyone for their support over the years! 3 | 4 | # Streamlined Core Data for RubyMotion 5 | 6 | Core Data Query (CDQ) is a library to help you manage your Core Data stack 7 | while using RubyMotion. It uses a data model file, which you can generate in 8 | XCode, or you can use [ruby-xcdm](https://github.com/infinitered/ruby-xcdm). 9 | 10 | [![Build Status](https://travis-ci.org/infinitered/cdq.png?branch=master)](https://travis-ci.org/infinitered/cdq) 11 | [![Gem Version](https://badge.fury.io/rb/cdq.png)](http://badge.fury.io/rb/cdq) 12 | 13 | CDQ was created by Ken Miller at [Infinite Red](http://infinite.red), a React Native expert consulting company based the US. 14 | 15 | ## Get Started 16 | 1. [Introducing CDQ](#introducingCDQ) 17 | 2. [Greenfield Quick Start Tutorial](https://github.com/infinitered/cdq/wiki/Greenfield-Quick-Start) 18 | 3. [Cheat Sheet](https://github.com/infinitered/cdq/wiki/CDQ-Cheat-Sheet) 19 | 4. [API docs](http://rubydoc.info/github/infinitered/cdq) 20 | 21 | ## Introducing CDQ 22 | 23 | CDQ began its life as a fork of 24 | [MotionData](https://github.com/alloy/MotionData), but it became obvious I 25 | wanted to take things in a different direction, so I cut loose and ended up 26 | rewriting almost everything. If you pay attention, you can still find the 27 | genetic traces, so thanks to @alloy for sharing his work and letting me learn 28 | so much. 29 | 30 | CDQ aims to streamline the process of getting you up and running Core Data, while 31 | avoiding too much abstraction or method pollution on top of the SDK. While it 32 | borrows many ideas from ActiveRecord (especially AREL), it is designed to 33 | harmonize with Core Data's way of doing things first. 34 | 35 | ### Why use a static Data Model? 36 | 37 | By using a real data model file that gets compiled and included in your bundle, 38 | you can take advantage of automatic migration, which simplifies managing your 39 | schema as it grows, if you can follow a few [simple rules](https://developer.apple.com/library/ios/documentation/cocoa/conceptual/CoreDataVersioning/Articles/vmLightweightMigration.html#//apple_ref/doc/uid/TP40004399-CH4-SW2). 40 | 41 | ## Installing 42 | 43 | ```bash 44 | $ gem install cdq 45 | $ motion create my_app # if needed 46 | $ cd my_app 47 | $ cdq init 48 | ``` 49 | 50 | This way assumes you want to use ruby-xcdm. Run `cdq -h` for list of more generators. 51 | 52 | ### Using Bundler: 53 | 54 | ```ruby 55 | gem 'cdq' 56 | ``` 57 | 58 | If you want to see bleeding-edge changes, point Bundler at the git repo: 59 | 60 | ```ruby 61 | gem 'cdq', git: 'git://github.com/infinitered/cdq.git' 62 | ``` 63 | 64 | ## Setting up your stack 65 | 66 | You will need a data model file. If you've created one in XCode, move or copy 67 | it to your resources file and make sure it's named the same as your RubyMotion 68 | project. If you're using `ruby-xcdm` (which I highly recommend) then it will 69 | create the datamodel file automatically and put it in the right place. 70 | 71 | Now include the setup code in your `app_delegate.rb` file: 72 | 73 | ```ruby 74 | class AppDelegate 75 | include CDQ 76 | 77 | def application(application, didFinishLaunchingWithOptions:launchOptions) 78 | cdq.setup 79 | true 80 | end 81 | end 82 | ``` 83 | 84 | That's it! You can create specific implementation classes for your entities if 85 | you want, but it's not required. You can start running queries on the console or 86 | in your code right away. 87 | 88 | ## Schema 89 | 90 | The best way to use CDQ is together with ruby-xcdm, which is installed as a 91 | dependency. For the full docs, see its [github page](http://github.com/infinitered/ruby-xcdm), 92 | but here's a taste. Schema files are found in the "schemas" directory within your 93 | app root, and they are versioned for automatic migrations, and this is what they look like: 94 | 95 | ```ruby 96 | schema "0001 initial" do 97 | 98 | entity "Article" do 99 | string :body, optional: false 100 | integer32 :length 101 | boolean :published, default: false 102 | datetime :publishedAt, default: false 103 | string :title, optional: false 104 | 105 | belongs_to :author 106 | end 107 | 108 | entity "Author" do 109 | float :fee 110 | string :name, optional: false 111 | 112 | # Deleting an author will delete all associated articles 113 | has_many :articles, deletionRule: "Cascade" 114 | end 115 | 116 | end 117 | ``` 118 | 119 | Ruby-xcdm translates these files straight into the XML format that Xcode uses for datamodels. 120 | 121 | ### Boolean Values 122 | 123 | Since CoreData stores boolean values as an `NSNumber`, cdq provides helper 124 | methods to allow you to get the boolean value of the property. Take the `Article` 125 | model from above with the `boolean`:`published`. If you call `published` directly 126 | you'll get the `NSNumber` `0` or `1`. If you call `published?` you'll get a 127 | boolean `true` or `false` 128 | 129 | ```ruby 130 | article_1 = Article.create(published: true) 131 | article_2 = Article.create(published: false) 132 | 133 | article_1.published # => 1 134 | article_2.published # => 0 135 | 136 | article_1.published? # => true 137 | article_2.published? # => false 138 | ``` 139 | 140 | ## Context Management 141 | 142 | Managing NSManagedObjectContext objects in Core Data can be tricky, especially 143 | if you are trying to take advantage of nested contexts for better threading 144 | behavior. One of the best parts of CDQ is that it handles contexts for you 145 | relatively seamlessly. If you have a simple app, you may never need to worry 146 | about contexts at all. 147 | 148 | ### Nested Contexts 149 | 150 | For a great discussion of why you might want to use nested contexts, see [here](http://www.cocoanetics.com/2012/07/multi-context-coredata/). 151 | 152 | CDQ maintains a stack of contexts (one stack per thread), and by default, all 153 | operations on objects use the topmost context. You just call `cdq.save` 154 | and it saves the whole stack. Or you can get a list of all the contexts in 155 | order with `cdq.contexts.all` and do more precise work. 156 | 157 | To access the `cdq` object from a class method inside a class that is not a `CDQManagedObject` 158 | subclass, make sure to include the `CDQ` module in your class like this: 159 | 160 | ```ruby 161 | class MyClass 162 | class << self 163 | include CDQ 164 | 165 | def my_class_method 166 | # Do something 167 | cdq.save 168 | end 169 | end 170 | end 171 | 172 | # Elsewhere 173 | MyClass.my_class_method 174 | ``` 175 | 176 | Settings things up the way you want is easy. Here's how you'd set it up for asynchronous 177 | saves: 178 | 179 | ```ruby 180 | cdq.contexts.push(:root) 181 | cdq.contexts.push(:main) 182 | ``` 183 | 184 | This pushes a private queue context onto the bottom of the stack, then a main queue context on top of it. 185 | Since the main queue is on top, all your data operations will use that. `cdq.save` then saves the 186 | main context, and schedules a save on the root context. 187 | 188 | In addition, since these two contexts are globally important, it makes them available at `cdq.contexts.main` and 189 | `cdq.contexts.root`. 190 | 191 | ### Temporary Contexts 192 | 193 | From time to time, you may need to use a temporary context. For example, on 194 | importing a large amount of data from the network, it's best to process and 195 | load into a temporary context (possibly in a background thread) and then move 196 | all the data over to your main context all at once. CDQ makes that easy too: 197 | 198 | ```ruby 199 | cdq.background do 200 | 201 | # Your work here 202 | 203 | cdq.save 204 | end 205 | ``` 206 | 207 | ## Object Lifecycle 208 | 209 | ### Creating 210 | ```ruby 211 | Author.create(name: "Le Guin", publish_count: 150, first_published: 1970) 212 | Author.create(name: "Shakespeare", publish_count: 400, first_published: 1550) 213 | Author.create(name: "Blake", publish_count: 100, first_published: 1778) 214 | cdq.save 215 | ``` 216 | 217 | CDQ will automatically set the object's property `created_at` to `Time.now` if it exists. If you want to use this ActiveRecord-like automatic attribute, make sure to add `datetime :created_at` to your schema's model definition. 218 | 219 | ### Reading 220 | 221 | ```ruby 222 | author = Author.create(name: "Le Guin", publish_count: 150, first_published: 1970) 223 | author.name # => "Le Guin" 224 | author.publish_count # => 150 225 | author.attributes # => { "name" => "Le Guin", "publish_count" => 150, "first_published" => 1970 } 226 | ``` 227 | 228 | ### Updating 229 | ```ruby 230 | author = Author.first 231 | author.name = "Ursula K. Le Guin" 232 | cdq.save 233 | ``` 234 | 235 | You can also update multiple attributes of a single object: 236 | 237 | ```ruby 238 | author = Author.first 239 | author.update(name: "Mark Twain", publish_count: 30, first_published: 1865) 240 | cdq.save 241 | ``` 242 | 243 | The update command will raise an `UnknownAttributeError` if you try and set an attribute that doesn't exist on the object so it's good practice to sanitize the data before you call `update`: 244 | 245 | ```ruby 246 | new_author_data = { 247 | name: "Mark Twain", 248 | publish_count: 30, 249 | first_published: 1865, 250 | some_attribute_that_doesnt_exist_on_author: "balderdash!" 251 | } 252 | sanitized = new_author_data.keep_if{|k,_| Author.attribute_names.include?(k) } 253 | 254 | author = Author.first 255 | author.update(sanitized) 256 | cdq.save 257 | ``` 258 | 259 | **NOTE** Custom class methods will have to `include CDQ` in order to have access to the `cdq` object. If you're calling `cdq` from a class method, you also have to `extend CDQ`. 260 | 261 | CDQ will automatically set the object's property `updated_at` to `Time.now` if it exists. If you want to use this ActiveRecord-like automatic attribute, make sure to add `datetime :updated_at` to your schema's model definition. 262 | 263 | ### Deleting 264 | ```ruby 265 | author = Author.first 266 | author.destroy 267 | cdq.save 268 | ``` 269 | 270 | ## Queries 271 | 272 | A quick aside about queries in Core Data. You should avoid them whenever 273 | possible in your production code. Core Data is designed to work efficiently 274 | when you hang on to references to specific objects and use them as you would 275 | any in-memory object, letting Core Data handle your memory usage for you. If 276 | you're coming from a server-side rails background, this can be pretty hard to 277 | get used to, but this is a very different environment. So if you find yourself 278 | running queries that only return a single object, consider rearchitecting. 279 | That said, queries are sometimes the only solution, and it's very handy to be 280 | able to use them easily when debugging from the console, or in unit tests. 281 | 282 | All of these queries are infinitely daisy-chainable, and almost everything is 283 | possible to do using only chained methods, no need to drop into NSPredicate format 284 | strings unless you want to. 285 | 286 | Here are some examples. **See the [cheat sheet](https://github.com/infinitered/cdq/wiki/CDQ-Cheat-Sheet) for a complete list.** 287 | 288 | ### Conditions 289 | 290 | ```ruby 291 | Author.where(:name).eq('Shakespeare') 292 | Author.where(:publish_count).gt(10) 293 | Author.where(name: 'Shakespeare', publish_count: 15) 294 | Author.where("name LIKE %@", '*kesp*') 295 | Author.where("name LIKE %@", 'Shakespear?') 296 | ``` 297 | 298 | ### Sorts, Limits and Offsets 299 | 300 | ```ruby 301 | Author.sort_by(:created_at).limit(1).offset(10) 302 | Author.sort_by(:created_at, order: :descending) 303 | Author.sort_by(:created_at, case_insensitive: true) 304 | ``` 305 | 306 | ### Conjunctions 307 | 308 | ```ruby 309 | Author.where(:name).eq('Blake').and(:first_published).le(Time.local(1700)) 310 | 311 | # Multiple comparisons against the same attribute 312 | Author.where(:created_at).ge(yesterday).and.lt(today) 313 | ``` 314 | 315 | #### Nested Conjunctions 316 | 317 | ```ruby 318 | Author.where(:name).contains("Emily").and(cdq(:pub_count).gt(100).or.lt(10)) 319 | ``` 320 | 321 | ### Calculations 322 | 323 | ```ruby 324 | Author.sum(:fee) 325 | Author.average(:fee) 326 | Author.min(:fee) 327 | Author.max(:fee) 328 | Author.where(:name).eq("Emily").sum(:fee) 329 | ``` 330 | 331 | ### Fetching 332 | 333 | Like ActiveRecord, CDQ will not run a fetch until you actually request specific 334 | objects. There are several methods for getting at the data: 335 | 336 | * `array` 337 | * `first` 338 | * `last` 339 | * `each` 340 | * `[]` 341 | * `map` 342 | * Anything else in `Enumerable` 343 | 344 | ## Dedicated Models 345 | 346 | If you're using CDQ in a brand new project, you'll probably want to use 347 | dedicated model classes for your entities. 348 | familiar-looking and natural syntax for queries and scopes: 349 | 350 | ```ruby 351 | class Author < CDQManagedObject 352 | end 353 | ``` 354 | 355 | ## Named Scopes 356 | 357 | You can save up partially-constructed queries for later use using named scopes, even 358 | combining them seamlessly with other queries or other named scopes: 359 | 360 | ```ruby 361 | class Author < CDQManagedObject 362 | scope :a_authors, where(:name).begins_with('A') 363 | scope :prolific, where(:publish_count).gt(99) 364 | end 365 | 366 | Author.prolific.a_authors.limit(5) 367 | ``` 368 | 369 | ## Using CDQ with a pre-existing model 370 | 371 | If you have an existing app that already manages its own data model, you can 372 | still use CDQ, overriding its stack at any layer: 373 | 374 | ```ruby 375 | cdq.setup(context: App.delegate.mainContext) # don't set up model or store coordinator 376 | cdq.setup(store: App.delegate.persistentStoreCoordinator) # Don't set up model 377 | cdq.setup(model: App.delegate.managedObjectModel) # Don't load model 378 | ``` 379 | 380 | You cannot use CDQManagedObject as a base class when overriding this way, 381 | you'll need to use the master method, described below. If you have an 382 | existing model and want to use it with CDQManagedObject without changing its 383 | name, You'll need to use a cdq.yml config file. See 384 | [CDQConfig](http://github.com/infinitered/cdq/tree/master/motion/cdq/config.rb). 385 | 386 | ### Working without model classes using the master method 387 | 388 | If you need or want to work without using CDQManagedObject as your base class, 389 | you can use the `cdq()`master method. This is a "magic" method, like 390 | `rmq()` in [RubyMotionQuery](http://github.com/infinitered/rmq) or 391 | `$()` in jQuery, which will lift whatever you pass into it into the CDQ 392 | universe. The method is available inside all UIResponder classes (so, views and 393 | controllers) as well as in the console. You can use it anywhere else by 394 | including the model `CDQ` into your classes. To use an entity without a 395 | model class, just pass its name as a string into the master method, like so 396 | 397 | ```ruby 398 | cdq('Author').where(:name).eq('Shakespeare') 399 | cdq('Author').where(:publish_count).gt(10) 400 | cdq('Author').sort_by(:created_at).limit(1).offset(10) 401 | ``` 402 | 403 | Anything you can do with a model, you can also do with the master method, including 404 | defining and using named scopes: 405 | 406 | ```ruby 407 | cdq('Author').scope :a_authors, cdq(:name).begins_with('A') 408 | cdq('Author').scope :prolific, cdq(:publish_count).gt(99) 409 | ``` 410 | > NOTE: strings and symbols are NOT interchangeable. `cdq('Entity')` gives you a 411 | query generator for an entity, but `cdq(:attribute)` starts a predicate for an 412 | attribute. 413 | 414 | ## Reserved model attributes 415 | 416 | CDQ does some smart automatic attribute setting. If you add attributes `:created_at` and/or `:updated_at` to a model in your schema file, whenever a record is created or updated, these properties will be updated accordingly. Therefore, you can not define your own `:created_at` or `:updated_at` model attributes. These attributes must be of type `datetime`. Note that these attributes aren't set until you call `cdq.save` 417 | 418 | Example: 419 | 420 | ```ruby 421 | schema "0001 initial" do 422 | entity "Author" do 423 | string :name, optional: false 424 | 425 | datetime :created_at 426 | datetime :updated_at 427 | end 428 | end 429 | ``` 430 | 431 | ```ruby 432 | a = Author.create(name: "Le Guin") 433 | # Notice that the properties aren't set yet 434 | # 435 | # (entity: Author; id: 0x117504810 436 | # ; data: { 437 | # name: "Le Guin"; 438 | # created_at: nil; 439 | # updated_at: nil; 440 | # }) 441 | 442 | cdq.save 443 | 444 | puts a # Original reference to created Author object 445 | # (entity: Author; id: 0x117504810 446 | # ; data: { 447 | # name: "Le Guin"; 448 | # created_at: 2015-08-19 20:44:40 +0000; 449 | # updated_at: 2015-08-19 20:44:40 +0000; 450 | # }) 451 | 452 | a.name = "Some Other Guy" 453 | puts a 454 | # Note that nothing has changed except the name: 455 | # 456 | # (entity: Author; id: 0x117504810 457 | # ; data: { 458 | # name: "Some Other Guy"; 459 | # created_at: 2015-08-19 20:44:40 +0000; 460 | # updated_at: 2015-08-19 20:44:40 +0000; 461 | # }) 462 | 463 | cdq.save 464 | puts a 465 | # (entity: Author; id: 0x117504810 466 | # ; data: { 467 | # name: "Some Other Guy"; 468 | # created_at: 2015-08-19 20:44:40 +0000; 469 | # updated_at: 2015-08-19 20:47:40 +0000; 470 | # }) 471 | ``` 472 | 473 | Also note that you should never use `object_id` as a model attribute as it will conflict with an internally generated property. 474 | 475 | ## iCloud 476 | 477 | **Removed as of version 2.0.0. If you still need this, pin cdq gem to before version 2.0.0** 478 | 479 | As of version 0.1.10, there is some experimental support for iCloud, written by 480 | @katsuyoshi. Please try it out and let us know how it's working for you. To 481 | enable, initialize like this: 482 | 483 | ```ruby 484 | cdq.stores.new(iCloud: true, container: "com.your.container.id") 485 | ``` 486 | 487 | You can also set up iCloud in your cdq.yml file. 488 | 489 | ## Documentation 490 | 491 | * [API](http://rubydoc.info/github/infinitered/cdq) 492 | * [Cheat Sheet](https://github.com/infinitered/cdq/wiki/CDQ-Cheat-Sheet) 493 | * [Tutorial](https://github.com/infinitered/cdq/wiki/Greenfield-Quick-Start) 494 | 495 | ## Things that are currently missing 496 | 497 | * There is no facility for custom migrations yet 498 | * There are no explicit validations (but you can define them on your data model) 499 | * Lifecycle Callbacks or Observers 500 | 501 | ## Tips 502 | 503 | If you need, you could watch SQL statements by setting the following launch argument through `args` environment variable: 504 | 505 | ``` 506 | $ rake args='-com.apple.CoreData.SQLDebug 3' 507 | ``` 508 | 509 | `com.apple.CoreData.SQLDebug` takes a value between 1 and 3; the higher the value, the more verbose the output. 510 | --------------------------------------------------------------------------------