mongoid-lookup 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/README.md ADDED
@@ -0,0 +1,276 @@
1
+
2
+ Mongoid::Lookup
3
+ ===============
4
+
5
+ Mongoid::Lookup is an extension for the Mongoid ODM providing
6
+ support for cross-model document lookups. It has a broad range of applications
7
+ including versatile app-wide search. Mongoid::Lookup leverages
8
+ Mongoid's polymorphism support to provide intuitive filtering by model.
9
+
10
+ Simple Lookup
11
+ -------------
12
+
13
+ An ideal use for Mongoid::Lookup is cross-model search, i.e. the
14
+ Facebook search box which returns Users, Groups, Pages, Topics, etc.
15
+
16
+ To begin, define a collection to use as a root for your lookup:
17
+
18
+ require 'mongoid_lookup'
19
+
20
+ class SearchListing
21
+ include Mongoid::Document
22
+ include Mongoid::Lookup
23
+
24
+ lookup_collection
25
+
26
+ field :label, type: String
27
+ end
28
+
29
+ Next, for each model you'd like to reference in the `SearchListing` collection,
30
+ add a `has_lookup` relationship:
31
+
32
+ class User
33
+ include Mongoid::Document
34
+ include Mongoid::Lookup
35
+
36
+ has_lookup :search, collection: SearchListing, map: { label: :full_name }
37
+
38
+ field :full_name, type: String
39
+ end
40
+
41
+ `has_lookup` does more than just add a relation to `SearchListing`. It actually creates
42
+ a new model extending `SearchListing`, which you can access by passing the name of the relation
43
+ to the `lookup` method:
44
+
45
+ User.lookup(:search) #=> User::SearchReference
46
+ User::SearchReference.superclass #=> SearchListing
47
+
48
+ Mongoid::Lookup will now maintain a reference document through the `User#search_reference` relation.
49
+ The `:map` option matches fields in `User::SearchReference` to fields in `User`.
50
+ `User::SearchReference#label` will be `User#full_name`. Whenever the user changes its name,
51
+ its `#search_reference` will be updated:
52
+
53
+ user = User.create(full_name: 'John Doe')
54
+ user.search_reference #=> #<User::SearchReference label: "John Doe">
55
+ user.update_attributes(full_name: "Jack Doe")
56
+ user.search_reference.reload #=> #<User::SearchReference label: "Jack Doe">
57
+
58
+ The lookup collection won't be particularly useful, though, until you add more models:
59
+
60
+ class Group
61
+ include Mongoid::Document
62
+ include Mongoid::Lookup
63
+ has_lookup :search, collection: SearchListing, map: { label: :name }
64
+ field :name, type: String
65
+ end
66
+
67
+ class Page
68
+ include Mongoid::Document
69
+ include Mongoid::Lookup
70
+ has_lookup :search, collection: SearchListing, map: { label: :title }
71
+ field :title, type: String
72
+ end
73
+
74
+ References in the lookup collection relate back to the source document, `referenced`:
75
+
76
+ User.create(full_name: 'John Doe')
77
+ listing = SearchListing.all.first
78
+ listing.referenced #=> #<User _id: 4f418f237742b50df7000001, _type: "User", name: "John Doe">
79
+
80
+ Now, you can add whatever functionality desired to your lookup collection. In our example,
81
+ we'd like to implement cross-model search:
82
+
83
+ class SearchListing
84
+ scope :label_like, ->(query) { where(label: Regexp.new(/#{query}/i)) }
85
+ end
86
+
87
+ SearchListing.label_like("J")
88
+ #=> [ #<User::SearchReference label: "J User">, <Group::SearchReference label: "J Group">, <Page::SearchReference label: "J Page"> ]
89
+
90
+ Advanced Lookup
91
+ ---------------
92
+
93
+ Building on the simple search example, suppose you have a structured tagging system:
94
+
95
+ class Tag
96
+ include Mongoid::Document
97
+ end
98
+ class Person < Tag; end
99
+ class Topic < Tag; end
100
+ class Place < Tag; end
101
+ class City < Place; end
102
+ class Region < Place; end
103
+ class Country < Place; end
104
+
105
+ Sometimes you'll want results across all models (User, Group, Page, Tag)
106
+ but at other times you may only be interested in Tags, or even just Places.
107
+ Mongoid makes type filtering easy, and Mongoid::Lookup leverages this feature.
108
+
109
+ Begin by defining a lookup on your parent class:
110
+
111
+ class Tag
112
+ include Mongoid::Lookup
113
+ has_lookup :search, collection: SearchListing, map: { label: :name }
114
+ field :name, type: String
115
+ end
116
+
117
+ Mongoid::Lookup allows you to directly query just for tags:
118
+
119
+ Tag.lookup(:search).label_like("B")
120
+ #=> [ #<Topic::SearchReference label: "Business">, <Person::SearchReference label: "Barack Obama">, <City::SearchReference label: "Boston"> ]
121
+
122
+ Alternatively, you can access the model by its constant, which has been named according to the
123
+ key `"#{name.to_s.classify}Reference"`:
124
+
125
+ Tag::SearchReference.label_like(query)
126
+
127
+ `has_lookup :screen_name` would generate a reference model `Tag::ScreenNameReference`:
128
+
129
+ ### Lookup Inheritance
130
+
131
+ What if you're only interested in Places?
132
+
133
+ Your `Place` model can define its own lookup.
134
+ Use the `inherit: true` option in any child class
135
+ to create a finer grained lookup:
136
+
137
+ class Place < Tag
138
+ has_lookup :search, inherit: true
139
+ end
140
+
141
+ The new lookup will inherit the configuration of Tag's `:search` lookup. Now you
142
+ can query Place references only, as needed:
143
+
144
+ Place.lookup(:search).label_like("C")
145
+ #=> [ #<Country::SearchReference label: "Canada">, <Region::SearchReference label: "California">, <City::SearchReference label: "Carson City"> ]
146
+
147
+ or
148
+
149
+ Place::SearchReference.label_like("C")
150
+
151
+ This does not effect SearchListing or Tag::SearchReference. Mongoid knows
152
+ hows to limit the query.
153
+
154
+ ### Extending Lookup Reference Models
155
+
156
+ If you would like your lookup references to maintain more data from your source model,
157
+ add the desired fields to the lookup collection:
158
+
159
+ class SearchListing
160
+ field :alias, type: String
161
+ end
162
+
163
+ Now update your `:map` option in your lookup declarations:
164
+
165
+ class User
166
+ has_lookup :search, collection: SearchListing, map: { label: :full_name, alias: :nickname }
167
+ field :full_name, type: String
168
+ field :nickname, type: String
169
+ end
170
+
171
+ The `#search_reference` will be updated whenever `#full_name` or `#nickname` are changed.
172
+
173
+ If you would like to include fields that only pertain to certain models, pass a block to
174
+ your `has_lookup` call. It will be evaluated in the context of the reference class.
175
+ The following:
176
+
177
+ class Place < Tag
178
+
179
+ has_lookup :search, inherit: true, map: { population: :population } do
180
+ # this block is evaluated
181
+ # in Place::SearchReference
182
+ field :population, type: Integer
183
+ end
184
+
185
+ field :population, type: Integer
186
+ end
187
+
188
+ ...adds a `Place::SearchReference#population` field and maps it to `Place#population`.
189
+ This additional field and mapping will only exist to `Place` and its child classes.
190
+
191
+ Anytime that you define a lookup, the parent configurations (fields and mappings) will
192
+ be inherited, and the new ones added:
193
+
194
+ class City < Place
195
+
196
+ has_lookup :search, inherit: true, map: { code: :zipcode } do
197
+ # this block is evaluated
198
+ # in City::SearchReference
199
+ field :code, type: Integer
200
+ end
201
+
202
+ field :zipcode, type: Integer
203
+ end
204
+
205
+ `City::SearchReference` will maintain `#label`, `#population`, and `#code`, as per mappings
206
+ given in `Tag`, `Place`, and `City`.
207
+
208
+ The ability to subclass lookup references allows for some flexibility. For instance,
209
+ if you'd like search references to provide a `#summary` method:
210
+
211
+ class SearchListing
212
+ lookup_collection
213
+
214
+ def summary
215
+ referenced_type.name
216
+ end
217
+ end
218
+
219
+ class Place < Tag
220
+ has_lookup :search, inherit: true, map: { population: :population } do
221
+ field :population, type: Integer
222
+
223
+ def summary
224
+ super + " of population #{population}"
225
+ end
226
+ end
227
+ end
228
+
229
+ Topic.create(title: "Politics")
230
+ City.create(name: 'New York City', population: 8125497)
231
+
232
+ SearchListing.all.each do |listing|
233
+ puts "#{listing.label} + (#{listing.summary})"
234
+ end
235
+
236
+ #=> "Politics (Topic)"
237
+ #=> "New York City (City of population 8125497)"
238
+
239
+ Notes
240
+ -----
241
+
242
+ Presumably, write heavy fields aren't great candidates for lookup. Every time
243
+ the field changes, the lookup reference will have to be updated.
244
+
245
+ The update currently takes place in a `before_save` callback. If performance were a
246
+ concern, the hook could instead create a delayed job to update the lookup (not currently supported).
247
+
248
+ Authors
249
+ -------
250
+
251
+ * [Jeff Magee](http://github.com/jmagee) (jmagee.osrc at gmail dot com)
252
+
253
+
254
+ License
255
+ -------
256
+
257
+ Copyright (c) 2012 Jeff Magee
258
+
259
+ Permission is hereby granted, free of charge, to any person obtaining
260
+ a copy of this software and associated documentation files (the
261
+ "Software"), to deal in the Software without restriction, including
262
+ without limitation the rights to use, copy, modify, merge, publish,
263
+ distribute, sublicense, and/or sell copies of the Software, and to
264
+ permit persons to whom the Software is furnished to do so, subject to
265
+ the following conditions:
266
+
267
+ The above copyright notice and this permission notice shall be
268
+ included in all copies or substantial portions of the Software.
269
+
270
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
271
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
272
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
273
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
274
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
275
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
276
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,27 @@
1
+
2
+ module Mongoid #:nodoc:
3
+ module Lookup #:nodoc:
4
+
5
+ module Collection
6
+ extend ActiveSupport::Concern
7
+
8
+ module ClassMethods
9
+
10
+ # configure the lookup collection
11
+ def build_lookup_collection
12
+ relate_lookup_collection
13
+ end
14
+
15
+ # add polymorphic relation to referenced document
16
+ #
17
+ # @private
18
+ def relate_lookup_collection
19
+ belongs_to :referenced, :polymorphic => true
20
+ end
21
+
22
+ end
23
+
24
+ end
25
+
26
+ end
27
+ end
@@ -0,0 +1,155 @@
1
+
2
+ module Mongoid #:nodoc:
3
+ module Lookup #:nodoc:
4
+
5
+ class InheritError < StandardError; end
6
+
7
+ module Model
8
+ extend ActiveSupport::Concern
9
+
10
+ # create or update the lookup reference if
11
+ # changes are present
12
+ #
13
+ # @param [Symbol] name name of lookup reference to update
14
+ def maintain_lookup_reference name
15
+ if has_lookup_changes?(name)
16
+ attrs = lookup_reference_attributes(name)
17
+
18
+ if ref = send("#{name}_reference")
19
+ ref.update_attributes(attrs)
20
+ else
21
+ send("create_#{name}_reference", attrs)
22
+ end
23
+ end
24
+ end
25
+
26
+ # checks for dirty fields on the given lookup
27
+ #
28
+ # @param [Symbol] name name of lookup to check
29
+ # @return [Boolean]
30
+ def has_lookup_changes? name
31
+ lookup_fields = self.class.lookup_fields(name)
32
+ changed_fields = changes.keys
33
+ (changed_fields - lookup_fields).count < changed_fields.count
34
+ end
35
+
36
+ # the attributes required for the given lookup reference
37
+ #
38
+ # @param [Symbol] name name of lookup to check
39
+ # @return [Hash]
40
+ def lookup_reference_attributes name
41
+ {}.tap do |attrs|
42
+ map = self.class.lookup(name).lookup_field_map.invert
43
+ map.keys.each do |source_field|
44
+ attrs[map[source_field]] = read_attribute(source_field)
45
+ end
46
+ end
47
+ end
48
+
49
+ module ClassMethods
50
+
51
+ # Create a lookup of the given name on the host model.
52
+ #
53
+ # @param [Symbol] name name of lookup
54
+ # @param [Hash] options
55
+ # @option options [Mongoid::Document] :collection
56
+ def build_lookup name, options, &block;
57
+ define_lookup_reference(name, options, &block)
58
+ relate_lookup_reference(name, options)
59
+ attach_lookup_reference_callback(name, options)
60
+ end
61
+
62
+ # Create a class to serve as the model for the lookup
63
+ # reference. Const is set within the current model.
64
+ #
65
+ # @private
66
+ def define_lookup_reference name, options, &block;
67
+ const_set("#{name.to_s.classify}Reference", Class.new(lookup_reference_parent(name, options)))
68
+ lookup(name).send(:include, Reference) unless included_modules.include?(Reference)
69
+ lookup(name).configure_lookup_reference(options)
70
+ if block_given?
71
+ lookup(name).class_eval(&block)
72
+ end
73
+ end
74
+
75
+ # lookup reference class for the given lookup name
76
+ #
77
+ # @param [String, Symbol] name
78
+ # @return [Mongoid::Lookup::Reference]
79
+ def lookup name
80
+ const_get("#{name.to_s.classify}Reference")
81
+ end
82
+
83
+ # the fields on the Model which map to fields on the
84
+ # lookup reference
85
+ #
86
+ # @param [Symbol] name
87
+ # @return [Array<String>]
88
+ def lookup_fields(name)
89
+ lookup(name).lookup_field_map.values.collect{ |v| v.to_s }
90
+ end
91
+
92
+ # whether or not the given lookup is defined
93
+ #
94
+ # @param [String, Symbol] name
95
+ # @return [Boolean]
96
+ def has_lookup? name
97
+ const_defined?("#{name.to_s.classify}Reference")
98
+ end
99
+
100
+ # returns the appropriate lookup reference parent for
101
+ # the given options
102
+ #
103
+ # @private
104
+ # @param [String, Symbol] name
105
+ # @param [Array] options
106
+ # @return [Mongoid::Lookup::Collection, Mongoid::Lookup::Reference]
107
+ # @raise KeyError if not inheriting and collection is not given
108
+ def lookup_reference_parent name, options
109
+ options[:inherit] ? nearest_lookup_reference(name) : options.fetch(:collection)
110
+ end
111
+
112
+ # returns the lookup reference for the given name
113
+ # belonging to the nearest ancestor of the calling class.
114
+ #
115
+ # @private
116
+ # @param [String, Symbol] name
117
+ # @return [Mongoid::Lookup::Reference]
118
+ def nearest_lookup_reference name
119
+ (ancestors - included_modules).each do |klass|
120
+ if klass.respond_to?(:has_lookup?)
121
+ if klass.has_lookup?(name)
122
+ return klass.lookup(name)
123
+ end
124
+ end
125
+ end
126
+
127
+ raise InheritError, "no ancestor of #{self.name} has a lookup named '#{name}'"
128
+ end
129
+
130
+ # relate the Model to the created lookup Reference
131
+ #
132
+ # @private
133
+ def relate_lookup_reference name, options
134
+ has_one "#{name}_reference".to_sym, :as => :referenced, :class_name => lookup(name).name, :dependent => :destroy
135
+ end
136
+
137
+ # add a save hook for the given reference unless
138
+ # already defined (inheriting)
139
+ #
140
+ # @private
141
+ def attach_lookup_reference_callback name, options
142
+ return if options[:inherit]
143
+
144
+ set_callback :save, :before do
145
+ maintain_lookup_reference(name)
146
+ true
147
+ end
148
+ end
149
+
150
+ end
151
+
152
+ end
153
+
154
+ end
155
+ end
@@ -0,0 +1,34 @@
1
+
2
+ module Mongoid #:nodoc:
3
+ module Lookup #:nodoc:
4
+
5
+ module Reference
6
+ extend ActiveSupport::Concern
7
+
8
+ module ClassMethods
9
+
10
+ # @param [Array] options
11
+ # @option options [Hash] :map
12
+ def configure_lookup_reference options
13
+ resolve_lookup_field_map(options)
14
+ end
15
+
16
+ # inherit field map and merge new fields
17
+ #
18
+ # @private
19
+ def resolve_lookup_field_map options
20
+ map = (superclass.instance_variable_get(:@field_map) or {}).dup.merge((options[:map] or {}))
21
+ instance_variable_set(:@field_map, map)
22
+ end
23
+
24
+ # @return [Hash] map of source fields to reference fields
25
+ def lookup_field_map
26
+ instance_variable_get(:@field_map)
27
+ end
28
+
29
+ end
30
+
31
+ end
32
+
33
+ end
34
+ end
@@ -0,0 +1,29 @@
1
+
2
+ require 'mongoid_lookup/collection'
3
+ require 'mongoid_lookup/model'
4
+ require 'mongoid_lookup/reference'
5
+
6
+ module Mongoid #:nodoc:
7
+ module Lookup #:nodoc:
8
+ extend ActiveSupport::Concern
9
+
10
+ module ClassMethods
11
+
12
+ # Configures calling model as a lookup collection
13
+ def lookup_collection
14
+ include Collection
15
+ build_lookup_collection
16
+ end
17
+
18
+ # Configures lookup on calling model.
19
+ # accepts a block which will be evaluated
20
+ # in the class of the generated lookup reference model
21
+ def has_lookup name, options, &block;
22
+ include Model unless included_modules.include?(Model)
23
+ build_lookup(name, options, &block)
24
+ end
25
+
26
+ end
27
+
28
+ end
29
+ end
metadata ADDED
@@ -0,0 +1,102 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mongoid-lookup
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: 0.0.1
6
+ platform: ruby
7
+ authors:
8
+ - Jeff Magee
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2012-03-12 00:00:00 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: mongoid
17
+ prerelease: false
18
+ requirement: &id001 !ruby/object:Gem::Requirement
19
+ none: false
20
+ requirements:
21
+ - - ~>
22
+ - !ruby/object:Gem::Version
23
+ version: "2.4"
24
+ type: :runtime
25
+ version_requirements: *id001
26
+ - !ruby/object:Gem::Dependency
27
+ name: rspec
28
+ prerelease: false
29
+ requirement: &id002 !ruby/object:Gem::Requirement
30
+ none: false
31
+ requirements:
32
+ - - ~>
33
+ - !ruby/object:Gem::Version
34
+ version: "2.6"
35
+ type: :development
36
+ version_requirements: *id002
37
+ - !ruby/object:Gem::Dependency
38
+ name: redcarpet
39
+ prerelease: false
40
+ requirement: &id003 !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ version: "2.1"
46
+ type: :development
47
+ version_requirements: *id003
48
+ - !ruby/object:Gem::Dependency
49
+ name: yard
50
+ prerelease: false
51
+ requirement: &id004 !ruby/object:Gem::Requirement
52
+ none: false
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: 0.7.5
57
+ type: :development
58
+ version_requirements: *id004
59
+ description: Mongoid::Lookup is an extension for the Mongoid ODM providing support for cross-model document lookups.
60
+ email: jmagee.osrc@gmail.com
61
+ executables: []
62
+
63
+ extensions: []
64
+
65
+ extra_rdoc_files: []
66
+
67
+ files:
68
+ - lib/mongoid_lookup/collection.rb
69
+ - lib/mongoid_lookup/model.rb
70
+ - lib/mongoid_lookup/reference.rb
71
+ - lib/mongoid_lookup.rb
72
+ - README.md
73
+ homepage: https://github.com/jmagee/mongoid-lookup
74
+ licenses: []
75
+
76
+ post_install_message:
77
+ rdoc_options: []
78
+
79
+ require_paths:
80
+ - lib
81
+ required_ruby_version: !ruby/object:Gem::Requirement
82
+ none: false
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: "0"
87
+ required_rubygems_version: !ruby/object:Gem::Requirement
88
+ none: false
89
+ requirements:
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ version: "0"
93
+ requirements: []
94
+
95
+ rubyforge_project:
96
+ rubygems_version: 1.8.17
97
+ signing_key:
98
+ specification_version: 3
99
+ summary: Cross-model lookup support for Mongoid
100
+ test_files: []
101
+
102
+ has_rdoc: