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,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
@@ -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>