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.
Files changed (47) hide show
  1. checksums.yaml +15 -0
  2. data/.gitignore +11 -0
  3. data/Gemfile +7 -0
  4. data/Gemfile.lock +32 -0
  5. data/README.md +173 -0
  6. data/Rakefile +24 -0
  7. data/app/app_delegate.rb +13 -0
  8. data/app/test_models.rb +15 -0
  9. data/cdq.gemspec +18 -0
  10. data/lib/cdq.rb +12 -0
  11. data/lib/cdq/version.rb +4 -0
  12. data/motion/cdq.rb +58 -0
  13. data/motion/cdq/collection_proxy.rb +29 -0
  14. data/motion/cdq/config.rb +67 -0
  15. data/motion/cdq/context.rb +190 -0
  16. data/motion/cdq/model.rb +32 -0
  17. data/motion/cdq/object.rb +83 -0
  18. data/motion/cdq/object_proxy.rb +30 -0
  19. data/motion/cdq/partial_predicate.rb +53 -0
  20. data/motion/cdq/query.rb +128 -0
  21. data/motion/cdq/relationship_query.rb +122 -0
  22. data/motion/cdq/store.rb +52 -0
  23. data/motion/cdq/targeted_query.rb +170 -0
  24. data/motion/managed_object.rb +98 -0
  25. data/resources/CDQ.xcdatamodeld/.xccurrentversion +8 -0
  26. data/resources/CDQ.xcdatamodeld/0.0.1.xcdatamodel/contents +34 -0
  27. data/resources/Default-568h@2x.png +0 -0
  28. data/resources/KEEPME +0 -0
  29. data/schemas/001_baseline.rb +44 -0
  30. data/spec/cdq/collection_proxy_spec.rb +51 -0
  31. data/spec/cdq/config_spec.rb +74 -0
  32. data/spec/cdq/context_spec.rb +92 -0
  33. data/spec/cdq/managed_object_spec.rb +81 -0
  34. data/spec/cdq/model_spec.rb +14 -0
  35. data/spec/cdq/module_spec.rb +44 -0
  36. data/spec/cdq/object_proxy_spec.rb +37 -0
  37. data/spec/cdq/object_spec.rb +58 -0
  38. data/spec/cdq/partial_predicate_spec.rb +52 -0
  39. data/spec/cdq/query_spec.rb +127 -0
  40. data/spec/cdq/relationship_query_spec.rb +75 -0
  41. data/spec/cdq/store_spec.rb +39 -0
  42. data/spec/cdq/targeted_query_spec.rb +120 -0
  43. data/spec/helpers/thread_helper.rb +16 -0
  44. data/spec/integration_spec.rb +38 -0
  45. data/vendor/cdq/ext/CoreDataQueryManagedObjectBase.h +8 -0
  46. data/vendor/cdq/ext/CoreDataQueryManagedObjectBase.m +22 -0
  47. 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
@@ -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
+
@@ -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
+