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,190 @@
|
|
1
|
+
|
2
|
+
module CDQ
|
3
|
+
|
4
|
+
class CDQContextManager
|
5
|
+
|
6
|
+
BACKGROUND_SAVE_NOTIFICATION = 'com.infinitered.cdq.context.background_save_completed'
|
7
|
+
|
8
|
+
def initialize(opts = {})
|
9
|
+
@store_manager = opts[:store_manager]
|
10
|
+
end
|
11
|
+
|
12
|
+
# Push a new context onto the stack for the current thread, making that context the
|
13
|
+
# default. If a block is supplied, push for the duration of the block and then
|
14
|
+
# return to the previous state.
|
15
|
+
#
|
16
|
+
def push(context, &block)
|
17
|
+
if block_given?
|
18
|
+
save_stack do
|
19
|
+
push_to_stack(context)
|
20
|
+
block.call
|
21
|
+
end
|
22
|
+
else
|
23
|
+
push_to_stack(context)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Pop the top context off the stack. If a block is supplied, pop for the
|
28
|
+
# duration of the block and then return to the previous state.
|
29
|
+
#
|
30
|
+
def pop(&block)
|
31
|
+
if block_given?
|
32
|
+
save_stack do
|
33
|
+
rval = pop_from_stack
|
34
|
+
block.call
|
35
|
+
end
|
36
|
+
else
|
37
|
+
pop_from_stack
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# The current context at the top of the stack.
|
42
|
+
#
|
43
|
+
def current
|
44
|
+
stack.last
|
45
|
+
end
|
46
|
+
|
47
|
+
# An array of all contexts, from bottom to top of the stack.
|
48
|
+
#
|
49
|
+
def all
|
50
|
+
stack.dup
|
51
|
+
end
|
52
|
+
|
53
|
+
# Remove all contexts.
|
54
|
+
#
|
55
|
+
def reset!
|
56
|
+
self.stack = []
|
57
|
+
end
|
58
|
+
|
59
|
+
# Create and push a new context with the specified concurrency type. Its parent
|
60
|
+
# will be set to the previous head context. If a block is supplied, the new context
|
61
|
+
# will exist for the duration of the block and then the previous state will be restore_managerd.
|
62
|
+
#
|
63
|
+
def new(concurrency_type, &block)
|
64
|
+
context = NSManagedObjectContext.alloc.initWithConcurrencyType(concurrency_type)
|
65
|
+
if current
|
66
|
+
context.parentContext = current
|
67
|
+
else
|
68
|
+
if @store_manager.invalid?
|
69
|
+
raise "store coordinator not found. Cannot create the first context without one."
|
70
|
+
else
|
71
|
+
context.persistentStoreCoordinator = @store_manager.current
|
72
|
+
end
|
73
|
+
end
|
74
|
+
push(context, &block)
|
75
|
+
end
|
76
|
+
|
77
|
+
# Save all contexts in the stack, starting with the current and working down.
|
78
|
+
#
|
79
|
+
def save(options = {})
|
80
|
+
always_wait = options[:always_wait]
|
81
|
+
stack.reverse.each do |context|
|
82
|
+
if always_wait || context.concurrencyType == NSMainQueueConcurrencyType
|
83
|
+
context.performBlockAndWait( -> {
|
84
|
+
|
85
|
+
with_error_object do |error|
|
86
|
+
context.save(error)
|
87
|
+
end
|
88
|
+
|
89
|
+
} )
|
90
|
+
elsif context.concurrencyType == NSPrivateQueueConcurrencyType
|
91
|
+
task_id = UIApplication.sharedApplication.beginBackgroundTaskWithExpirationHandler( -> { NSLog "CDQ Save Timed Out" } )
|
92
|
+
|
93
|
+
if task_id == UIBackgroundTaskInvalid
|
94
|
+
context.performBlockAndWait( -> {
|
95
|
+
|
96
|
+
with_error_object do |error|
|
97
|
+
context.save(error)
|
98
|
+
end
|
99
|
+
|
100
|
+
} )
|
101
|
+
else
|
102
|
+
context.performBlock( -> {
|
103
|
+
|
104
|
+
# Let the application know we're doing something important
|
105
|
+
with_error_object do |error|
|
106
|
+
context.save(error)
|
107
|
+
end
|
108
|
+
|
109
|
+
UIApplication.sharedApplication.endBackgroundTask(task_id)
|
110
|
+
|
111
|
+
NSNotificationCenter.defaultCenter.postNotificationName(BACKGROUND_SAVE_NOTIFICATION, object: context)
|
112
|
+
|
113
|
+
} )
|
114
|
+
end
|
115
|
+
else
|
116
|
+
with_error_object do |error|
|
117
|
+
context.save(error)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
true
|
122
|
+
end
|
123
|
+
|
124
|
+
private
|
125
|
+
|
126
|
+
def push_to_stack(value)
|
127
|
+
lstack = stack
|
128
|
+
lstack << value
|
129
|
+
self.stack = lstack
|
130
|
+
value
|
131
|
+
end
|
132
|
+
|
133
|
+
def pop_from_stack
|
134
|
+
lstack = stack
|
135
|
+
value = lstack.pop
|
136
|
+
self.stack = lstack
|
137
|
+
value
|
138
|
+
end
|
139
|
+
|
140
|
+
def save_stack(&block)
|
141
|
+
begin
|
142
|
+
saved_stack = all
|
143
|
+
block.call
|
144
|
+
ensure
|
145
|
+
self.stack = saved_stack
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def stack
|
150
|
+
Thread.current[:"cdq.context.stack.#{object_id}"] || []
|
151
|
+
end
|
152
|
+
|
153
|
+
def stack=(value)
|
154
|
+
Thread.current[:"cdq.context.stack.#{object_id}"] = value
|
155
|
+
end
|
156
|
+
|
157
|
+
def with_error_object(default = nil, &block)
|
158
|
+
error = Pointer.new(:object)
|
159
|
+
result = block.call(error)
|
160
|
+
if error[0]
|
161
|
+
print_error("Error while fetching", error[0])
|
162
|
+
raise "Error while fetching: #{error[0].debugDescription}"
|
163
|
+
end
|
164
|
+
result || default
|
165
|
+
end
|
166
|
+
|
167
|
+
def print_error(message, error, indent = "")
|
168
|
+
puts indent + message + error.localizedDescription
|
169
|
+
if error.userInfo['reason']
|
170
|
+
puts indent + error.userInfo['reason']
|
171
|
+
end
|
172
|
+
if error.userInfo['metadata']
|
173
|
+
error.userInfo['metadata'].each do |key, value|
|
174
|
+
puts indent + "#{key}: #{value}"
|
175
|
+
end
|
176
|
+
end
|
177
|
+
if !error.userInfo[NSDetailedErrorsKey].nil?
|
178
|
+
error.userInfo[NSDetailedErrorsKey].each do |key, value|
|
179
|
+
if key.instance_of? NSError
|
180
|
+
print_error("Sub-Error: ", key, indent + " ")
|
181
|
+
else
|
182
|
+
puts indent + "#{key}: #{value}"
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
end
|
189
|
+
|
190
|
+
end
|
data/motion/cdq/model.rb
ADDED
@@ -0,0 +1,32 @@
|
|
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
|
+
private
|
21
|
+
|
22
|
+
def load_model
|
23
|
+
if invalid?
|
24
|
+
raise "No model file. Cannot create an NSManagedObjectModel without one."
|
25
|
+
else
|
26
|
+
NSManagedObjectModel.alloc.initWithContentsOfURL(@config.model_url)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
@@ -0,0 +1,83 @@
|
|
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!
|
22
|
+
@@context_manager = nil
|
23
|
+
@@store_manager.reset!
|
24
|
+
@@store_manager = nil
|
25
|
+
end
|
26
|
+
|
27
|
+
def setup(opts = {})
|
28
|
+
if opts[:context]
|
29
|
+
contexts.push(opts[:context])
|
30
|
+
return true
|
31
|
+
elsif opts[:store]
|
32
|
+
stores.current = opts[:store]
|
33
|
+
elsif opts[:model]
|
34
|
+
models.current = opts[:model]
|
35
|
+
end
|
36
|
+
contexts.new(NSMainQueueConcurrencyType)
|
37
|
+
true
|
38
|
+
end
|
39
|
+
|
40
|
+
def save(*args)
|
41
|
+
contexts.save(*args)
|
42
|
+
end
|
43
|
+
|
44
|
+
protected
|
45
|
+
|
46
|
+
def with_error_object(default, &block)
|
47
|
+
error = Pointer.new(:object)
|
48
|
+
result = block.call(error)
|
49
|
+
if error[0]
|
50
|
+
raise "Error while fetching: #{error[0].debugDescription}"
|
51
|
+
end
|
52
|
+
result || default
|
53
|
+
end
|
54
|
+
|
55
|
+
def constantize(camel_cased_word)
|
56
|
+
names = camel_cased_word.split('::')
|
57
|
+
names.shift if names.empty? || names.first.empty?
|
58
|
+
|
59
|
+
names.inject(Object) do |constant, name|
|
60
|
+
if constant == Object
|
61
|
+
constant.const_get(name)
|
62
|
+
else
|
63
|
+
candidate = constant.const_get(name)
|
64
|
+
next candidate if constant.const_defined?(name, false)
|
65
|
+
next candidate unless Object.const_defined?(name)
|
66
|
+
|
67
|
+
# Go down the ancestors to check it it's owned
|
68
|
+
# directly before we reach Object or the end of ancestors.
|
69
|
+
constant = constant.ancestors.inject do |const, ancestor|
|
70
|
+
break const if ancestor == Object
|
71
|
+
break ancestor if ancestor.const_defined?(name, false)
|
72
|
+
const
|
73
|
+
end
|
74
|
+
|
75
|
+
# owner is in Object, so raise
|
76
|
+
constant.const_get(name, false)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
|
@@ -0,0 +1,30 @@
|
|
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
|
+
|
@@ -0,0 +1,53 @@
|
|
1
|
+
|
2
|
+
module CDQ
|
3
|
+
class CDQPartialPredicate < CDQObject
|
4
|
+
|
5
|
+
attr_reader :key, :scope, :operation
|
6
|
+
|
7
|
+
OPERATORS = {
|
8
|
+
:eq => [NSEqualToPredicateOperatorType, :equal],
|
9
|
+
:ne => [NSNotEqualToPredicateOperatorType, :not_equal],
|
10
|
+
:lt => [NSLessThanPredicateOperatorType, :less_than],
|
11
|
+
:le => [NSLessThanOrEqualToPredicateOperatorType, :less_than_or_equal],
|
12
|
+
:gt => [NSGreaterThanPredicateOperatorType, :greater_than],
|
13
|
+
:ge => [NSGreaterThanOrEqualToPredicateOperatorType, :greater_than_or_equal],
|
14
|
+
:contains => [NSContainsPredicateOperatorType, :include],
|
15
|
+
:matches => [NSMatchesPredicateOperatorType],
|
16
|
+
:in => [NSInPredicateOperatorType],
|
17
|
+
:begins_with => [NSBeginsWithPredicateOperatorType],
|
18
|
+
:ends_with => [NSEndsWithPredicateOperatorType]
|
19
|
+
}
|
20
|
+
|
21
|
+
def initialize(key, scope, operation = :and)
|
22
|
+
@key = key
|
23
|
+
@scope = scope
|
24
|
+
@operation = operation
|
25
|
+
end
|
26
|
+
|
27
|
+
OPERATORS.each do |op, (type, synonym)|
|
28
|
+
define_method(op) do |value, options = 0|
|
29
|
+
make_scope(type, value, options)
|
30
|
+
end
|
31
|
+
alias_method synonym, op if synonym
|
32
|
+
end
|
33
|
+
|
34
|
+
def between(min, max); make_scope(NSBetweenPredicateOperatorType, [min, max]); end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def make_pred(key, type, value, options = 0)
|
39
|
+
NSComparisonPredicate.predicateWithLeftExpression(
|
40
|
+
NSExpression.expressionForKeyPath(key.to_s),
|
41
|
+
rightExpression:NSExpression.expressionForConstantValue(value),
|
42
|
+
modifier:NSDirectPredicateModifier,
|
43
|
+
type:type,
|
44
|
+
options:options)
|
45
|
+
end
|
46
|
+
|
47
|
+
def make_scope(type, value, options = 0)
|
48
|
+
scope.send(operation, make_pred(key, type, value, options), key)
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
data/motion/cdq/query.rb
ADDED
@@ -0,0 +1,128 @@
|
|
1
|
+
|
2
|
+
module CDQ
|
3
|
+
class CDQQuery < CDQObject
|
4
|
+
|
5
|
+
EMPTY = Object.new
|
6
|
+
|
7
|
+
attr_reader :predicate, :sort_descriptors
|
8
|
+
|
9
|
+
def initialize(opts = {})
|
10
|
+
@predicate = opts[:predicate]
|
11
|
+
@limit = opts[:limit]
|
12
|
+
@offset = opts[:offset]
|
13
|
+
@sort_descriptors = opts[:sort_descriptors] || []
|
14
|
+
@saved_key = opts[:saved_key]
|
15
|
+
end
|
16
|
+
|
17
|
+
def limit(value = EMPTY)
|
18
|
+
if value == EMPTY
|
19
|
+
@limit
|
20
|
+
else
|
21
|
+
clone(limit: value)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def offset(value = EMPTY)
|
26
|
+
if value == EMPTY
|
27
|
+
@offset
|
28
|
+
else
|
29
|
+
clone(offset: value)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Combine this query with others in an intersection ("and") relationship
|
34
|
+
def and(query = nil, *args)
|
35
|
+
merge_query(query, :and, *args) do |left, right|
|
36
|
+
NSCompoundPredicate.andPredicateWithSubpredicates([left, right])
|
37
|
+
end
|
38
|
+
end
|
39
|
+
alias_method :where, :and
|
40
|
+
|
41
|
+
# Combine this query with others in a union ("or") relationship
|
42
|
+
def or(query = nil, *args)
|
43
|
+
merge_query(query, :or, *args) do |left, right|
|
44
|
+
NSCompoundPredicate.orPredicateWithSubpredicates([left, right])
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Create a new query with the same values as this one, optionally overriding
|
49
|
+
# any of them in the options
|
50
|
+
def clone(opts = {})
|
51
|
+
self.class.new(locals.merge(opts))
|
52
|
+
end
|
53
|
+
|
54
|
+
def locals
|
55
|
+
{ sort_descriptors: sort_descriptors,
|
56
|
+
predicate: predicate,
|
57
|
+
limit: limit,
|
58
|
+
offset: offset }
|
59
|
+
end
|
60
|
+
|
61
|
+
def sort_by(key, dir = :ascending)
|
62
|
+
if dir.to_s[0,4].downcase == 'desc'
|
63
|
+
ascending = false
|
64
|
+
else
|
65
|
+
ascending = true
|
66
|
+
end
|
67
|
+
|
68
|
+
clone(sort_descriptors: @sort_descriptors + [NSSortDescriptor.sortDescriptorWithKey(key, ascending: ascending)])
|
69
|
+
end
|
70
|
+
|
71
|
+
def fetch_request
|
72
|
+
NSFetchRequest.new.tap do |req|
|
73
|
+
req.predicate = predicate
|
74
|
+
req.fetchLimit = limit if limit
|
75
|
+
req.fetchOffset = offset if offset
|
76
|
+
req.sortDescriptors = sort_descriptors unless sort_descriptors.empty?
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
def merge_query(query, operation, *args, &block)
|
83
|
+
key_to_save = nil
|
84
|
+
case query
|
85
|
+
when Hash
|
86
|
+
subquery = query.inject(CDQQuery.new) do |memo, (key, value)|
|
87
|
+
memo.and(key).eq(value)
|
88
|
+
end
|
89
|
+
other_predicate = subquery.predicate
|
90
|
+
new_limit = limit
|
91
|
+
new_offset = offset
|
92
|
+
new_sort_descriptors = sort_descriptors
|
93
|
+
when Symbol
|
94
|
+
return CDQPartialPredicate.new(query, self, operation)
|
95
|
+
when NilClass
|
96
|
+
if @saved_key
|
97
|
+
return CDQPartialPredicate.new(@saved_key, self, operation)
|
98
|
+
else
|
99
|
+
raise "Zero-argument 'and' and 'or' can only be used if there is a key in the preceding predicate"
|
100
|
+
end
|
101
|
+
when CDQQuery
|
102
|
+
new_limit = [limit, query.limit].compact.last
|
103
|
+
new_offset = [offset, query.offset].compact.last
|
104
|
+
new_sort_descriptors = sort_descriptors + query.sort_descriptors
|
105
|
+
other_predicate = query.predicate
|
106
|
+
when NSPredicate
|
107
|
+
other_predicate = query
|
108
|
+
new_limit = limit
|
109
|
+
new_offset = offset
|
110
|
+
new_sort_descriptors = sort_descriptors
|
111
|
+
key_to_save = args.first
|
112
|
+
when String
|
113
|
+
other_predicate = NSPredicate.predicateWithFormat(query, argumentArray: args)
|
114
|
+
new_limit = limit
|
115
|
+
new_offset = offset
|
116
|
+
new_sort_descriptors = sort_descriptors
|
117
|
+
end
|
118
|
+
if predicate
|
119
|
+
new_predicate = block.call(predicate, other_predicate)
|
120
|
+
else
|
121
|
+
new_predicate = other_predicate
|
122
|
+
end
|
123
|
+
clone(predicate: new_predicate, limit: new_limit, offset: new_offset, sort_descriptors: new_sort_descriptors, saved_key: key_to_save)
|
124
|
+
end
|
125
|
+
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|