cdq 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +15 -0
- data/.gitignore +11 -0
- data/Gemfile +7 -0
- data/Gemfile.lock +32 -0
- data/README.md +173 -0
- data/Rakefile +24 -0
- data/app/app_delegate.rb +13 -0
- data/app/test_models.rb +15 -0
- data/cdq.gemspec +18 -0
- data/lib/cdq.rb +12 -0
- data/lib/cdq/version.rb +4 -0
- data/motion/cdq.rb +58 -0
- data/motion/cdq/collection_proxy.rb +29 -0
- data/motion/cdq/config.rb +67 -0
- data/motion/cdq/context.rb +190 -0
- data/motion/cdq/model.rb +32 -0
- data/motion/cdq/object.rb +83 -0
- data/motion/cdq/object_proxy.rb +30 -0
- data/motion/cdq/partial_predicate.rb +53 -0
- data/motion/cdq/query.rb +128 -0
- data/motion/cdq/relationship_query.rb +122 -0
- data/motion/cdq/store.rb +52 -0
- data/motion/cdq/targeted_query.rb +170 -0
- data/motion/managed_object.rb +98 -0
- data/resources/CDQ.xcdatamodeld/.xccurrentversion +8 -0
- data/resources/CDQ.xcdatamodeld/0.0.1.xcdatamodel/contents +34 -0
- data/resources/Default-568h@2x.png +0 -0
- data/resources/KEEPME +0 -0
- data/schemas/001_baseline.rb +44 -0
- data/spec/cdq/collection_proxy_spec.rb +51 -0
- data/spec/cdq/config_spec.rb +74 -0
- data/spec/cdq/context_spec.rb +92 -0
- data/spec/cdq/managed_object_spec.rb +81 -0
- data/spec/cdq/model_spec.rb +14 -0
- data/spec/cdq/module_spec.rb +44 -0
- data/spec/cdq/object_proxy_spec.rb +37 -0
- data/spec/cdq/object_spec.rb +58 -0
- data/spec/cdq/partial_predicate_spec.rb +52 -0
- data/spec/cdq/query_spec.rb +127 -0
- data/spec/cdq/relationship_query_spec.rb +75 -0
- data/spec/cdq/store_spec.rb +39 -0
- data/spec/cdq/targeted_query_spec.rb +120 -0
- data/spec/helpers/thread_helper.rb +16 -0
- data/spec/integration_spec.rb +38 -0
- data/vendor/cdq/ext/CoreDataQueryManagedObjectBase.h +8 -0
- data/vendor/cdq/ext/CoreDataQueryManagedObjectBase.m +22 -0
- metadata +138 -0
@@ -0,0 +1,122 @@
|
|
1
|
+
|
2
|
+
module CDQ
|
3
|
+
|
4
|
+
class CDQRelationshipQuery < CDQTargetedQuery
|
5
|
+
|
6
|
+
def initialize(owner, name, set = nil, opts = {})
|
7
|
+
@owner = owner
|
8
|
+
@relationship_name = name
|
9
|
+
@set = set || @owner.send(name)
|
10
|
+
relationship = owner.entity.relationshipsByName[name]
|
11
|
+
@inverse_rel = relationship.inverseRelationship
|
12
|
+
entity_description = relationship.destinationEntity
|
13
|
+
target_class = constantize(entity_description.managedObjectClassName)
|
14
|
+
super(entity_description, target_class, opts)
|
15
|
+
if @inverse_rel.isToMany
|
16
|
+
@predicate = self.where(@inverse_rel.name.to_sym).contains(@owner).predicate
|
17
|
+
else
|
18
|
+
@predicate = self.where(@inverse_rel.name.to_sym => @owner).predicate
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Creates a new managed object within the target relationship
|
23
|
+
#
|
24
|
+
def new(opts = {})
|
25
|
+
super(opts).tap do |obj|
|
26
|
+
add(obj)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Add an existing object to the relationship
|
31
|
+
#
|
32
|
+
def add(obj)
|
33
|
+
if @inverse_rel.isToMany
|
34
|
+
obj.send(@inverse_rel.name).addObject(@owner)
|
35
|
+
else
|
36
|
+
obj.send("#{@inverse_rel.name}=", @owner)
|
37
|
+
end
|
38
|
+
@set.addObject obj
|
39
|
+
end
|
40
|
+
alias_method :<<, :add
|
41
|
+
|
42
|
+
def self.extend_set(set, owner, name)
|
43
|
+
set.extend SetExt
|
44
|
+
set.extend Enumerable
|
45
|
+
set.__query__ = self.new(owner, name, set)
|
46
|
+
set
|
47
|
+
end
|
48
|
+
|
49
|
+
# A Core Data relationship set is extended with this module to provide
|
50
|
+
# scoping by forwarding messages to a CDQRelationshipQuery instance knows
|
51
|
+
# how to create further queries based on the underlying relationship.
|
52
|
+
module SetExt
|
53
|
+
attr_accessor :__query__
|
54
|
+
|
55
|
+
def set
|
56
|
+
self
|
57
|
+
end
|
58
|
+
|
59
|
+
# This works in a special way. If we're extending a regular NSSet, it will
|
60
|
+
# create a new method that calls allObjects. If we're extending a NSOrderedSet,
|
61
|
+
# the override will not work, and we get the array method already defined on
|
62
|
+
# NSOrderedSet, which is actually exactly what we want.
|
63
|
+
def array
|
64
|
+
self.allObjects
|
65
|
+
end
|
66
|
+
|
67
|
+
def first
|
68
|
+
array.first
|
69
|
+
end
|
70
|
+
|
71
|
+
# duplicating a lot of common methods because it's way faster than using method_missing
|
72
|
+
#
|
73
|
+
def each(*args, &block)
|
74
|
+
array.each(*args, &block)
|
75
|
+
end
|
76
|
+
|
77
|
+
def add(obj)
|
78
|
+
@__query__.add(obj)
|
79
|
+
end
|
80
|
+
alias_method :<<, :add
|
81
|
+
|
82
|
+
def create(opts = {})
|
83
|
+
@__query__.create(opts)
|
84
|
+
end
|
85
|
+
|
86
|
+
def new(opts = {})
|
87
|
+
@__query__.new(opts)
|
88
|
+
end
|
89
|
+
|
90
|
+
def where(*args)
|
91
|
+
@__query__.where(*args)
|
92
|
+
end
|
93
|
+
|
94
|
+
def sort_by(*args)
|
95
|
+
@__query__.sort_by(*args)
|
96
|
+
end
|
97
|
+
|
98
|
+
def limit(*args)
|
99
|
+
@__query__.limit(*args)
|
100
|
+
end
|
101
|
+
|
102
|
+
def offset(*args)
|
103
|
+
@__query__.offset(*args)
|
104
|
+
end
|
105
|
+
|
106
|
+
def respond_to?(method)
|
107
|
+
super(method) || @__query__.respond_to?(method)
|
108
|
+
end
|
109
|
+
|
110
|
+
def method_missing(method, *args, &block)
|
111
|
+
if @__query__.respond_to?(method)
|
112
|
+
@__query__.send(method, *args, &block)
|
113
|
+
else
|
114
|
+
super
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
end
|
119
|
+
|
120
|
+
end
|
121
|
+
|
122
|
+
end
|
data/motion/cdq/store.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
|
2
|
+
module CDQ
|
3
|
+
|
4
|
+
class CDQStoreManager
|
5
|
+
|
6
|
+
attr_writer :current
|
7
|
+
|
8
|
+
def initialize(opts = {})
|
9
|
+
@config = opts[:config] || CDQConfig.default
|
10
|
+
@model_manager = opts[:model_manager]
|
11
|
+
end
|
12
|
+
|
13
|
+
def current
|
14
|
+
@current ||= create_store
|
15
|
+
end
|
16
|
+
|
17
|
+
def reset!
|
18
|
+
NSFileManager.defaultManager.removeItemAtURL(@config.database_url, error: nil)
|
19
|
+
end
|
20
|
+
|
21
|
+
def invalid?
|
22
|
+
!@current && @model_manager.invalid?
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def create_store
|
28
|
+
if invalid?
|
29
|
+
raise "No model found. Can't create a persistent store coordinator without it."
|
30
|
+
else
|
31
|
+
coordinator = NSPersistentStoreCoordinator.alloc.initWithManagedObjectModel(@model_manager.current)
|
32
|
+
error = Pointer.new(:object)
|
33
|
+
options = { NSMigratePersistentStoresAutomaticallyOption => true,
|
34
|
+
NSInferMappingModelAutomaticallyOption => true }
|
35
|
+
url = @config.database_url
|
36
|
+
store = coordinator.addPersistentStoreWithType(NSSQLiteStoreType,
|
37
|
+
configuration:nil,
|
38
|
+
URL:url,
|
39
|
+
options:options,
|
40
|
+
error:error)
|
41
|
+
if store.nil?
|
42
|
+
error[0].userInfo['metadata'] && error[0].userInfo['metadata'].each do |key, value|
|
43
|
+
NSLog "#{key}: #{value}"
|
44
|
+
end
|
45
|
+
raise error[0].userInfo['reason']
|
46
|
+
end
|
47
|
+
coordinator
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
@@ -0,0 +1,170 @@
|
|
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 <tt>in_context</tt>
|
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
|
+
|
40
|
+
# Return all matching entities.
|
41
|
+
#
|
42
|
+
# Causes execution.
|
43
|
+
#
|
44
|
+
def array
|
45
|
+
raise("No context has been set. Probably need to run cdq.setup") unless context
|
46
|
+
with_error_object([]) do |error|
|
47
|
+
context.executeFetchRequest(fetch_request, error:error)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
alias_method :to_a, :array
|
51
|
+
|
52
|
+
# Convenience method for referring to all matching entities. No-op. You must
|
53
|
+
# still call <tt>array</tt> or another executing method
|
54
|
+
#
|
55
|
+
def all
|
56
|
+
self
|
57
|
+
end
|
58
|
+
|
59
|
+
# Return the first entity matching the query.
|
60
|
+
#
|
61
|
+
# Causes execution.
|
62
|
+
#
|
63
|
+
def first
|
64
|
+
limit(1).array.first
|
65
|
+
end
|
66
|
+
|
67
|
+
# Fetch a single entity from the query by index. If the optional
|
68
|
+
# <tt>length</tt> parameter is supplied, fetch a range of length <tt>length</tt>
|
69
|
+
# starting at <tt>index</tt>
|
70
|
+
#
|
71
|
+
# Causes execution.
|
72
|
+
#
|
73
|
+
def [](index, length = nil)
|
74
|
+
if length
|
75
|
+
offset(index).limit(length).array
|
76
|
+
else
|
77
|
+
offset(index).first
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# Iterate over each entity matched by the query. You can also use any method from the
|
82
|
+
# Enumerable module in the standard library that does not depend on ordering.
|
83
|
+
#
|
84
|
+
# Causes execution.
|
85
|
+
#
|
86
|
+
def each(&block)
|
87
|
+
array.each(&block)
|
88
|
+
end
|
89
|
+
|
90
|
+
# Returns the fully-contstructed fetch request, which can be executed outside of CDQ.
|
91
|
+
#
|
92
|
+
def fetch_request
|
93
|
+
super.tap do |req|
|
94
|
+
req.entity = @entity_description
|
95
|
+
req.predicate ||= NSPredicate.predicateWithValue(true)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# Create a new entity in the current context. Accepts a hash of attributes that will be assigned to
|
100
|
+
# the newly-created entity. Does not save the context.
|
101
|
+
#
|
102
|
+
def new(opts = {})
|
103
|
+
@target_class.alloc.initWithEntity(@entity_description, insertIntoManagedObjectContext: context).tap do |entity|
|
104
|
+
opts.each { |k, v| entity.send("#{k}=", v) }
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# Create a new entity in the current context. Accepts a hash of attributes that will be assigned to
|
109
|
+
# the newly-created entity. Does not save the context.
|
110
|
+
#
|
111
|
+
# [TODO: Will apply validation.]
|
112
|
+
#
|
113
|
+
def create(*args)
|
114
|
+
new(*args)
|
115
|
+
end
|
116
|
+
|
117
|
+
# Create a named scope. The query is any valid CDQ query.
|
118
|
+
#
|
119
|
+
# Example:
|
120
|
+
#
|
121
|
+
# cdq('Author').scope(:first_published, cdq(:published).eq(true).sort_by(:published_at).limit(1))
|
122
|
+
#
|
123
|
+
# cdq('Author').first_published.first => #<Author>
|
124
|
+
#
|
125
|
+
def scope(name, query = nil, &block)
|
126
|
+
if query.nil? && block_given?
|
127
|
+
named_scopes[name] = block
|
128
|
+
elsif query
|
129
|
+
named_scopes[name] = query
|
130
|
+
else
|
131
|
+
raise ArgumentError.new("You must supply a query OR a block that returns a query to scope")
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
# Override the context in which to perform this query. This forever forces the
|
136
|
+
# specified context for this particular query, so if you save the it for later
|
137
|
+
# use (such as defining a scope) bear in mind that changes in the default context
|
138
|
+
# will have no effect when running this.
|
139
|
+
#
|
140
|
+
def in_context(context)
|
141
|
+
clone(context: context)
|
142
|
+
end
|
143
|
+
|
144
|
+
# Any unknown method will be checked against the list of named scopes.
|
145
|
+
#
|
146
|
+
def method_missing(name, *args)
|
147
|
+
scope = named_scopes[name]
|
148
|
+
case scope
|
149
|
+
when CDQQuery
|
150
|
+
where(scope)
|
151
|
+
when Proc
|
152
|
+
where(scope.call(*args))
|
153
|
+
else
|
154
|
+
super(name, *args)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
private
|
159
|
+
|
160
|
+
def named_scopes
|
161
|
+
@@named_scopes ||= {}
|
162
|
+
@@named_scopes[@entity_description] ||= {}
|
163
|
+
end
|
164
|
+
|
165
|
+
def clone(opts = {})
|
166
|
+
CDQTargetedQuery.new(@entity_description, @target_class, locals.merge(opts))
|
167
|
+
end
|
168
|
+
|
169
|
+
end
|
170
|
+
end
|
@@ -0,0 +1,98 @@
|
|
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).<em>method</em>
|
4
|
+
# syntax, but this enables the nicer-looking and more convenient Class.<em>method</em> 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.<em>method</em>.
|
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
|
+
# Shortcut to look up the entity description for this class
|
40
|
+
#
|
41
|
+
def entity_description
|
42
|
+
cdq.models.current.entitiesByName[name]
|
43
|
+
end
|
44
|
+
|
45
|
+
# Creates a CDQ scope, but also defines a method on the class that returns the
|
46
|
+
# query directly.
|
47
|
+
#
|
48
|
+
def scope(name, query = nil, &block)
|
49
|
+
cdq.scope(name, query, &block)
|
50
|
+
if query
|
51
|
+
self.class.send(:define_method, name) do
|
52
|
+
query
|
53
|
+
end
|
54
|
+
else
|
55
|
+
self.class.send(:define_method, name) do |*args|
|
56
|
+
block.call(*args)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def new(*args)
|
62
|
+
cdq.new(*args)
|
63
|
+
end
|
64
|
+
|
65
|
+
# Pass any unknown methods on to cdq.
|
66
|
+
#
|
67
|
+
def method_missing(name, *args, &block)
|
68
|
+
cdq.send(name, *args, &block)
|
69
|
+
end
|
70
|
+
|
71
|
+
def responds_to?(name)
|
72
|
+
super || cdq.respond_to?(name)
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
|
77
|
+
# Register this object for destruction with the current context. Will not
|
78
|
+
# actually be removed until the context is saved.
|
79
|
+
#
|
80
|
+
def destroy
|
81
|
+
managedObjectContext.deleteObject(self)
|
82
|
+
end
|
83
|
+
|
84
|
+
def inspect
|
85
|
+
description
|
86
|
+
end
|
87
|
+
|
88
|
+
protected
|
89
|
+
|
90
|
+
# Called from method that's dynamically added from
|
91
|
+
# +[CoreDataManagedObjectBase defineRelationshipMethod:]
|
92
|
+
def relationshipByName(name)
|
93
|
+
willAccessValueForKey(name)
|
94
|
+
set = CDQRelationshipQuery.extend_set(primitiveValueForKey(name), self, name)
|
95
|
+
didAccessValueForKey(name)
|
96
|
+
set
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,8 @@
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
2
|
+
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
3
|
+
<plist version="1.0">
|
4
|
+
<dict>
|
5
|
+
<key>_XCCurrentVersionName</key>
|
6
|
+
<string>0.0.1.xcdatamodel</string>
|
7
|
+
</dict>
|
8
|
+
</plist>
|