mongoid-lookup 0.0.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.
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: