rails_lookup 0.0.3 → 0.0.4

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.
@@ -0,0 +1,9 @@
1
+ pkg
2
+ doc
3
+ Manifest
4
+ *.sw*
5
+ test/test.sqlite3
6
+ *.gem
7
+ .bundle
8
+ Gemfile.lock
9
+ pkg/*
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in rails_lookup.gemspec
4
+ gemspec
data/README CHANGED
@@ -140,3 +140,16 @@ I'll let you work out the details for actually migrating the data yourself.
140
140
 
141
141
  I hope this helped you and saved a lot of time and frustration. Follow me on twitter: @nimrodpriell
142
142
 
143
+ TESTING
144
+ -------
145
+
146
+ rvm gemset create rails_lookup_devel
147
+ gem install bundler
148
+ bundle install
149
+ rake test
150
+
151
+ That should basically do it. The gems required are managed in the Gemfile, they
152
+ are currently Rails and SQLite (which is used only for testing). This has been
153
+ tested to work with rails-3.0.9
154
+
155
+
data/Rakefile CHANGED
@@ -1,29 +1,8 @@
1
- require 'rubygems'
2
- require 'rake'
3
- require 'echoe'
4
-
5
- Echoe.new('rails_lookup', '0.0.3') do |s|
6
- s.description = File.read(File.join(File.dirname(__FILE__), 'README'))
7
- s.summary = "Lookup table macro for ActiveRecords"
8
- s.url = "http://github.com/Nimster/RailsLookup/"
9
- s.author = "Nimrod Priell"
10
- s.email = "@nimrodpriell" #Twitter
11
- s.ignore_pattern = ["tmp/*", "script/*", "Manifest"]
12
- s.development_dependencies = []
13
- end
14
-
15
- Dir["#{File.dirname(__FILE__)}/tasks/*.rake"].sort.each { |ext| load ext }
16
- =begin
17
- spec = Gem::Specification.new do |s|
18
- s.name = "rails_lookup"
19
- s.requirements = [ 'Rails >= 3.0.7, Ruby >= 1.9.1' ]
20
- s.version = "0.0.1"
21
- s.homepage = ""
22
- s.platform = "Gem::Platform::RUBY"
23
- s.required_ruby_version = '>=1.9'
24
- s.files = Dir['**/**']
25
- s.executables = []
26
- s.test_files = [] #Dir["test/test*.rb"]
27
- s.has_rdoc = false
28
- end
29
- =end
1
+ require 'bundler/gem_tasks'
2
+
3
+ # Adapted from matthewtodd's shoe gem at https://github.com/matthewtodd/shoe
4
+ desc "Runs all tests for the gem"
5
+ task :test do
6
+ spec = Gem::Specification.load(Dir.glob('*.gemspec')[0])
7
+ system('testrb', *spec.test_files) || exit(1)
8
+ end
@@ -0,0 +1,163 @@
1
+ require 'rails_lookup/version'
2
+ require 'active_record/base'
3
+ require 'active_record/relation'
4
+
5
+ # Author: Nimrod Priell (Twitter: @nimrodpriell)
6
+ #
7
+ # See README file or blog post for more.
8
+ # This is intended to be included into ActiveRecord entities: Simply
9
+ # include ActiveRecord::Lookup
10
+ # in your class that extends ActiveRecord::Base
11
+ module RailsLookup
12
+ #These will be defined as class methods so they can be called as "macros"
13
+ #like belongs_to
14
+ module ClassMethods
15
+ #We define only this "macro". In your ActiveRecord entity (say, Car), use
16
+ # lookup :car_type
17
+ # which will create Car#car_type, Car#car_type=, Car#car_type_id,
18
+ # Car#car_type_id= and CarType, an ActiveRecord with a name (String)
19
+ # attribute, that has_many :cars.
20
+ #
21
+ # You can also use
22
+ # lookup :car_type, :as => :type
23
+ # which will do the same thing but create Car#type, Car#type=, Car#type_id
24
+ # and Car#type_id= instead.
25
+ def lookup(lookup_name, opts = {})
26
+ as_name = opts[:as] || lookup_name
27
+
28
+ #Define the setter for the attribute by textual value
29
+ mycls = self #Class I'm defined in
30
+ #Ignore anonymous classes:
31
+ mycls_sym = mycls.name.tableize.to_sym unless mycls.to_s =~ /#<.*>/
32
+ lookup_cls_name = lookup_name.to_s.camelize
33
+ begin
34
+ #If it exists, just tack on the extra has_many
35
+ cls = Object.const_get lookup_cls_name
36
+ cls.has_many mycls_sym unless mycls_sym.blank? #for annon classes
37
+ rescue NameError
38
+ #Otherwise, actually define it
39
+ cls = Class.new(ActiveRecord::Base) do
40
+ has_many mycls_sym unless mycls_sym.blank? #For anon classes
41
+ validates_uniqueness_of :name
42
+ validates :name, :presence => true
43
+
44
+ def self.id_for(name, id = nil)
45
+ class_variable_get(:@@rcaches)[name] ||= id
46
+ end
47
+
48
+ def self.gen_id_for(val)
49
+ id = id_for val
50
+ if id.nil?
51
+ #Define this new possible value
52
+ #You could just override this...
53
+ new_db_obj = find_or_create_by_name val
54
+ id_for val, new_db_obj.id
55
+ name_for new_db_obj.id, val
56
+ id = new_db_obj.id
57
+ end
58
+ id
59
+ end
60
+
61
+ def self.name_for(id, name = nil)
62
+ class_variable_get(:@@caches)[id] ||= name
63
+ end
64
+
65
+ #This is especially useful for tests - you want deletion to also
66
+ #clear the caches otherwise odd stuff might happen
67
+ def self.delete_all(*args)
68
+ class_variable_set :@@caches, []
69
+ class_variable_set :@@rcaches, {}
70
+ super *args
71
+ end
72
+ end
73
+
74
+ Object.const_set lookup_cls_name, cls #Define it as a global class
75
+ end
76
+
77
+
78
+ define_method("#{as_name.to_s}_id=") do |id|
79
+ write_attribute "#{as_name.to_s}".to_sym, id
80
+ end
81
+
82
+ define_method("#{as_name.to_s}=") do |val|
83
+ id = cls.gen_id_for val #TODO: Just call #{as_name.to_s}_id=
84
+ write_attribute "#{as_name.to_s}".to_sym, id
85
+ end
86
+
87
+ define_method("#{as_name.to_s}_id") do
88
+ read_attribute "#{as_name.to_s}".to_sym
89
+ end
90
+
91
+ #Define the getter
92
+ define_method("#{as_name.to_s}") do
93
+ id = read_attribute "#{as_name.to_s}".to_sym
94
+ if not id.nil?
95
+ value = cls.name_for id
96
+ if value.nil?
97
+ lookup_obj = cls.find_by_id id
98
+ if not lookup_obj.nil?
99
+ #TODO: Extract to a method
100
+ cls.name_for id, lookup_obj.name
101
+ cls.id_for lookup_obj.name, id
102
+ end
103
+ end
104
+ end
105
+ value
106
+ end
107
+
108
+
109
+ #Rails ActiveRecord support:
110
+ @cls_for_name = {} if @cls_for_name.nil?
111
+ @cls_for_name[as_name.to_s.to_sym] = cls
112
+
113
+ #This makes find_and_create_by, find_all and where methods all work as
114
+ #they should. This will not work if you use reset_column_information,
115
+ #like if you use find_by_session_id in SessionStore. You must redefine
116
+ #it so it re-sets rel to this singleton object
117
+ rel = ActiveRecord::Relation.new(self, arel_table)
118
+ def rel.where(opts, *rest)
119
+ if opts.is_a? Hash
120
+ mapped = opts.map do |k,v|
121
+ if @cls_for_name.has_key?(k.to_sym)
122
+ [k, @cls_for_name[k.to_sym].id_for(v)]
123
+ else
124
+ [k, v]
125
+ end
126
+ end
127
+ opts = Hash[mapped]
128
+ end
129
+ super(opts, *rest)
130
+ end
131
+
132
+ def rel.cls_for_name=(cls_for_name)
133
+ @cls_for_name = cls_for_name
134
+ end
135
+
136
+ rel.cls_for_name = @cls_for_name
137
+ instance_variable_set(:@relation, rel)
138
+
139
+ #Might need to be in class_eval
140
+ belongs_to lookup_name.to_s.to_sym, :foreign_key => "#{as_name}"
141
+ validates as_name.to_s.to_sym, :presence => true
142
+
143
+ # Prefill the hashes from the DB - requires an active connection
144
+ all_vals = cls.all
145
+ # No need for the class_exec
146
+ cls.class_variable_set(:@@rcaches, all_vals.inject({}) do |r, obj|
147
+ r[obj.name] = obj.id
148
+ r
149
+ end)
150
+ cls.class_variable_set(:@@caches, all_vals.inject([]) do |r, obj|
151
+ r[obj.id] = obj.name
152
+ r
153
+ end)
154
+ end
155
+
156
+ end
157
+
158
+ # extend host class with class methods when we're included
159
+ def self.included(host_class)
160
+ host_class.extend(ClassMethods)
161
+ end
162
+ end
163
+
@@ -0,0 +1,3 @@
1
+ module RailsLookup
2
+ VERSION = "0.0.4"
3
+ end
@@ -1,172 +1,28 @@
1
1
  # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "rails_lookup/version"
2
4
 
3
5
  Gem::Specification.new do |s|
4
- s.name = %q{rails_lookup}
5
- s.version = "0.0.3"
6
+ s.name = %q{rails_lookup}
7
+ s.version = RailsLookup::VERSION
8
+ s.authors = ["Nimrod Priell"]
9
+ s.email = ["nimrod.priell@gmail.com"]
10
+ s.homepage = %q{http://github.com/Nimster/RailsLookup/}
11
+ s.summary = %q{Lookup table macro for ActiveRecords. See more in the README}
12
+
13
+ s.rubyforge_project = %q{rails_lookup}
6
14
 
7
15
  s.required_rubygems_version = Gem::Requirement.new(">= 1.2") if s.respond_to? :required_rubygems_version=
8
- s.authors = [%q{Nimrod Priell}]
9
16
  s.date = %q{2011-08-09}
10
- s.description = %q{Lookup tables with ruby-on-rails
11
- --------------------------------
12
-
13
- By: Nimrod Priell
14
-
15
- This gem adds an ActiveRecord macro to define memory-cached, dynamically growing, normalized lookup tables for entity 'type'-like objects. Or in plain English - if you want to have a table containing, say, ProductTypes which can grow with new types simply when you refer to them, and not keep the Product table containing a thousand repeating 'type="book"' entries - sit down and try to follow through.
16
-
17
- Installation is described down the page.
18
-
19
- Motivation
20
- ----------
21
-
22
- A [normalized DB][1] means that you want to keep types as separate tables, with foreign keys pointing from your main entity to its type. For instance, instead of
23
- ID | car_name | car_type
24
- 1 | Chevrolet Aveo | Compact
25
- 2 | Ford Fiesta | Compact
26
- 3 | BMW Z-5 | Sports
27
-
28
- You want to have two tables:
29
- ID | car_name | car_type_id
30
- 1 | Chevrolet Aveo | 1
31
- 2 | Ford Fiesta | 1
32
- 3 | BMW Z-5 | 2
33
-
34
- And
35
-
36
- car_type_id | car_type_name
37
- 1 | Compact
38
- 2 | Sports
39
-
40
- The pros/cons of a normalized DB can be discussed elsewhere. I'd just point out a denormalized solution is most useful in settings like [column oriented DBMSes][2]. For the rest of us folks using standard databases, we usually want to use lookups.
41
-
42
- The usual way to do this with ruby on rails is
43
- * Generate a CarType model using `rails generate model CarType name:string`
44
- * Link between CarType and Car tables using `belongs_to` and `has_many`
45
-
46
- Then to work with this you can transparently read the car type:
47
-
48
- car = Car.all.first
49
- car.car_type.name # returns "Compact"
50
-
51
- Ruby does an awesome job of caching the results for you, so that you'll probably not hit the DB every time you get the same car type from different car objects.
52
-
53
- You can even make this shorter, by defining a delegate to car_type_name from CarType:
54
-
55
- *in car_type_name.rb*
56
-
57
- delegate :name, :to => :car, :prefix => true
58
-
59
- And now you can access this as
60
-
61
- car.car_type_name
62
-
63
- However, it's less pleasant to insert with this technique:
64
-
65
- car.car_type.car_type_name = "Sports"
66
- car.car_type.save!
67
- #Now let's see what happened to the OTHER compact car
68
- Car.all.second.car_type_name #Oops, returns "Sports"
69
-
70
- Right, what are we doing? We should've used
71
-
72
- car.update_attributes(car_type: CarType.find_or_create_by_name(name: "Sports"))
73
-
74
- Okay. Probably want to shove that into its own method rather than have this repeated in the code several times. But you also need a helper method for creating cars that way…
75
-
76
- Furthermore, ruby is good about caching, but it caches by the exact query used, and the cache expires after the controller action ends. You can configure more advanced caches, perhaps.
77
-
78
- The thing is all this can get tedious if you use a normalized structure where you have 15 entities and each has at least one 'type-like' field. That's a whole lot of dangling Type objects. What you really want is an interface like this:
79
-
80
- car.all.first
81
- car.car_type #return "Compact"
82
- car.car_type = "Sports" #No effect on car.all.second, just automatically use the second constant
83
- car.car_type = "Sedan" #Magically create a new type
84
-
85
- Oh, and it'll be nice if all of this is cached and you can define car types as constants (or symbols). You obviously still want to be able to run:
86
-
87
- CarType.where (:id > 3) #Just an example of supposed "arbitrary" SQL involving a real live CarType class
88
-
89
- But you wanna minimize generating these numerous type classes. If you're like me, you don't even want to see them lying around in app/model. Who cares about them?
90
-
91
- I've looked thoroughly for a nice rails solution to this, but after failing to find one, I created my own rails metaprogramming hook.
92
-
93
- Installation
94
- ------------
95
- First, run `gem install rails_lookup` to install the latest version of the gem.
96
-
97
- The result of this hook is that you get the exact syntax described above, with only two lines of code (no extra classes or anything):
98
- In your ActiveRecord object simply add
99
-
100
- require 'active_record/lookup'
101
- class Car < ActiveRecord::Base
102
- #...
103
- include ActiveRecord::Lookup
104
- lookup :car_type
105
- #...
106
- end
107
-
108
- That's it. the generated CarType class (which you won't see as a car_type.rb file, obviously, as it is generated in real-time), contains some nice methods to look into the cache as well: So you can call
109
-
110
- CarType.id_for "Sports" #Returns 2
111
- CarType.name_for 1 #Returns "Compact"
112
-
113
- and you can still hack at the underlying ID for an object, if you need to:
114
-
115
- car = car.all.first
116
- car.car_type = "Sports"
117
- car.car_type_id #Returns 2
118
- car.car_type_id = 1
119
- car.car_type #Returns "Compact"
120
-
121
- The only remaining thing is to define your migrations for creating the actual database tables. After all, that's something you only want to do once and not every time this class loads, so this isn't the place for it. However, it's easy enough to create your own scaffolds so that
122
-
123
- rails generate migration create_car_type_lookup_for_car
124
-
125
- will automatically create the migration
126
-
127
- class CreateCarTypeLookupForCar < ActiveRecord::Migration
128
- def self.up
129
- create_table :car_types do |t|
130
- t.string :name
131
- t.timestamps #Btw you can remove these, I don't much like them in type tables anyway
132
- end
133
-
134
- remove_column :cars, :car_type #Let's assume you have one of those now…
135
- add_column :cars, :car_type_id, :integer #Maybe put not_null constraints here.
136
- end
137
-
138
- def self.down
139
- drop_table :car_types
140
- add_column :cars, :car_type, :string
141
- remove_column :cars, :car_type_id
142
- end
143
- end
144
-
145
- I'll let you work out the details for actually migrating the data yourself.
146
-
147
- [1] http://en.wikipedia.org/wiki/Database_normalization
148
- [2] http://en.wikipedia.org/wiki/Column-oriented_DBMS
149
-
150
- I hope this helped you and saved a lot of time and frustration. Follow me on twitter: @nimrodpriell
151
-
152
- }
153
- s.email = %q{@nimrodpriell}
154
- s.extra_rdoc_files = [%q{README}, %q{lib/active_record/lookup.rb}]
155
- s.files = [%q{README}, %q{Rakefile}, %q{lib/active_record/lookup.rb}, %q{rails_lookup.gemspec}, %q{test/20110808002412_create_test_lookup_db.rb}, %q{test/test_lookup.rb}]
156
- s.homepage = %q{http://github.com/Nimster/RailsLookup/}
157
- s.rdoc_options = [%q{--line-numbers}, %q{--inline-source}, %q{--title}, %q{Rails_lookup}, %q{--main}, %q{README}]
158
- s.require_paths = [%q{lib}]
159
- s.rubyforge_project = %q{rails_lookup}
160
- s.rubygems_version = %q{1.8.7}
161
- s.summary = %q{Lookup table macro for ActiveRecords}
162
- s.test_files = [%q{test/test_lookup.rb}]
17
+ s.description = %q{Lookup tables with ruby-on-rails}
163
18
 
164
- if s.respond_to? :specification_version then
165
- s.specification_version = 3
19
+ s.files = `git ls-files`.split("\n")
20
+ s.test_files = `git ls-files -- {test,spec,features}/*_test.rb`.split("\n")
21
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
22
+ s.require_paths = ["lib"]
23
+ s.extra_rdoc_files = [%q{README}, %q{lib/rails_lookup.rb}]
24
+ s.rdoc_options = [%q{--line-numbers}, %q{--inline-source}, %q{--title}, %q{rails_lookup}, %q{--main}, %q{README}]
166
25
 
167
- if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
168
- else
169
- end
170
- else
171
- end
26
+ s.add_dependency 'rails'
27
+ s.add_development_dependency 'sqlite3'
172
28
  end
@@ -1,18 +1,44 @@
1
- require 'lookup'
1
+ require 'active_record'
2
+ $:.push File.expand_path('lib')
3
+ #$:.push File.expand_path('../lib', __FILE__)
4
+ require 'rails_lookup'
2
5
  require 'minitest/autorun'
6
+ require '20110808002412_create_test_lookup_db'
7
+
8
+ # It sucks we have to do that in testing code, but I have to establish a
9
+ # connection prior to defining any classes that use lookups, and I see no other
10
+ # way around it other than extracting the test classes and requiring them only
11
+ # after the connection is established in setup() - which might be a nice
12
+ # solution but muddles things up.
13
+ # Establish a connection to the test database:
14
+ conn = ActiveRecord::Base.establish_connection(
15
+ adapter: "sqlite3",
16
+ database: "test/test.sqlite3",
17
+ pool: 5,
18
+ timeout: 5000
19
+ )
20
+ puts "Established connection!" unless conn.nil?
21
+ CreateTestLookupDb.migrate(:down) # Connection has to be established for this to work
22
+ CreateTestLookupDb.migrate(:up)
3
23
 
4
24
  class Car < ActiveRecord::Base
5
- include Lookup
25
+ include RailsLookup
6
26
  lookup :car_kind, :as => :kind
7
27
  lookup :car_color, :as => :color
28
+
29
+ # Example of how to scope for lookups
30
+ scope :by_color, lambda { |color|
31
+ joins(:car_color)\
32
+ .where({:car_colors => { name: color }})
33
+ }
8
34
  end
9
35
 
10
36
  class Plane < ActiveRecord::Base
11
- include Lookup
37
+ include RailsLookup
12
38
  lookup :plane_kind, :as => :kind
13
39
  end
14
40
 
15
- class TestLookup < MiniTest::Unit::TestCase
41
+ class LookupTest < MiniTest::Unit::TestCase
16
42
 
17
43
  def setup
18
44
  clear_tables
@@ -23,6 +49,9 @@ class TestLookup < MiniTest::Unit::TestCase
23
49
  end
24
50
 
25
51
  def clear_tables
52
+ # In every test you ever do, these calls will be required for all lookup
53
+ # tables involved, since they take care of clearing the (static) caches.
54
+ # Otherwise, you'd have a stale cache in the lookup class.
26
55
  Car.delete_all
27
56
  CarKind.delete_all
28
57
  CarColor.delete_all
@@ -129,11 +158,13 @@ class TestLookup < MiniTest::Unit::TestCase
129
158
  assert_equal "Yellow", bimba2.color
130
159
  assert_equal "Compact", bimba2.kind
131
160
  cars_before_creation = Car.count
132
- susita = Car.find_or_create_by_name_and_kind_and_color "Susita", "Compact", "Gray"
161
+ susita = Car.find_or_create_by_name_and_kind_and_color(\
162
+ "Susita", "Compact", "Gray")
133
163
  assert_equal "Gray", susita.color
134
164
  assert_equal "Compact", susita.kind
135
165
  assert_equal cars_before_creation + 1, Car.count
136
- bimba2 = Car.find_or_create_by_name_and_kind_and_color "Bimba", "Compact", "Yellow"
166
+ bimba2 = Car.find_or_create_by_name_and_kind_and_color(\
167
+ "Bimba", "Compact", "Yellow")
137
168
  assert_equal "Yellow", bimba2.color
138
169
  assert_equal "Compact", bimba2.kind
139
170
  assert_equal "Bimba", bimba2.name
@@ -179,7 +210,7 @@ class TestLookup < MiniTest::Unit::TestCase
179
210
  #We get a new CarKind class - but this one will be preloaded
180
211
  tmp = CarKind
181
212
  Class.new(ActiveRecord::Base) do
182
- include Lookup
213
+ include RailsLookup
183
214
  lookup :car_kind
184
215
  end
185
216
  refute_nil CarKind.id_for("Compact")
@@ -208,40 +239,13 @@ class TestLookup < MiniTest::Unit::TestCase
208
239
  # yellow = CarColor.find_by_name "Yellow"
209
240
  # assert_equal 2, yellow.cars.size
210
241
  end
242
+
243
+ # TODO: Test a scope
244
+ def test_scopes
245
+ bimba = Car.create! :name => "Bimba", :kind => "Compact", :color => "Yellow"
246
+ ferrari = Car.create! :kind => "Sports", :color => "Red"
247
+ assert_equal 1, Car.by_color("Yellow").count
248
+ assert_equal 1, Car.by_color("Red").count
249
+ end
211
250
  end
212
251
 
213
- =begin
214
- a = TestLookup.new
215
- puts "Cache hits: "
216
- puts(TestLookupKind.id_for "simian")
217
- puts "Setting test_lookup_kind: "
218
- a.test_lookup_kind="simian"
219
- puts "Reading test_lookup_kind: "
220
- puts a.test_lookup_kind
221
- puts a.test_lookup_kind_id
222
-
223
- puts "Hacking the ID: "
224
- a.test_lookup_kind_id = 1
225
- puts a.test_lookup_kind
226
-
227
- puts "Setting another_lookup_kind: "
228
- a.another_lookup_kind="simian"
229
- puts "Reading another_lookup_kind: "
230
- puts a.another_lookup_kind
231
- puts a.another_lookup_kind_id
232
-
233
- b = TestLookupKind.new
234
- puts "HERE"
235
- p TestLookupKind.all
236
-
237
- puts "Testing different class"
238
- b = OneMore.new
239
- b.taverna_kind = "williwok"
240
- puts b.taverna_kind
241
- puts b.taverna_kind_id
242
-
243
- puts "Testing original class: "
244
- puts a.test_lookup_kind
245
- puts a.another_lookup_kind
246
-
247
- =end
metadata CHANGED
@@ -1,115 +1,86 @@
1
- --- !ruby/object:Gem::Specification
1
+ --- !ruby/object:Gem::Specification
2
2
  name: rails_lookup
3
- version: !ruby/object:Gem::Version
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.4
4
5
  prerelease:
5
- version: 0.0.3
6
6
  platform: ruby
7
- authors:
7
+ authors:
8
8
  - Nimrod Priell
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
-
13
- date: 2011-08-09 00:00:00 Z
14
- dependencies: []
15
-
16
- description: "Lookup tables with ruby-on-rails\n\
17
- --------------------------------\n\n\
18
- By: Nimrod Priell\n\n\
19
- This gem adds an ActiveRecord macro to define memory-cached, dynamically growing, normalized lookup tables for entity 'type'-like objects. Or in plain English - if you want to have a table containing, say, ProductTypes which can grow with new types simply when you refer to them, and not keep the Product table containing a thousand repeating 'type=\"book\"' entries - sit down and try to follow through.\n\n\
20
- Installation is described down the page.\n\n\
21
- Motivation\n\
22
- ----------\n\n\
23
- A [normalized DB][1] means that you want to keep types as separate tables, with foreign keys pointing from your main entity to its type. For instance, instead of \n\
24
- ID | car_name | car_type\n\
25
- 1 | Chevrolet Aveo | Compact\n\
26
- 2 | Ford Fiesta | Compact\n\
27
- 3 | BMW Z-5 | Sports\n\n\
28
- You want to have two tables:\n\
29
- ID | car_name | car_type_id\n\
30
- 1 | Chevrolet Aveo | 1\n\
31
- 2 | Ford Fiesta | 1\n\
32
- 3 | BMW Z-5 | 2\n\n\
33
- And\n\n\
34
- car_type_id | car_type_name \n\
35
- 1 | Compact\n\
36
- 2 | Sports\n\n\
37
- The pros/cons of a normalized DB can be discussed elsewhere. I'd just point out a denormalized solution is most useful in settings like [column oriented DBMSes][2]. For the rest of us folks using standard databases, we usually want to use lookups.\n\n\
38
- The usual way to do this with ruby on rails is\n\
39
- * Generate a CarType model using `rails generate model CarType name:string`\n\
40
- * Link between CarType and Car tables using `belongs_to` and `has_many`\n\n\
41
- Then to work with this you can transparently read the car type:\n\n car = Car.all.first\n car.car_type.name # returns \"Compact\"\n\n\
42
- Ruby does an awesome job of caching the results for you, so that you'll probably not hit the DB every time you get the same car type from different car objects.\n\n\
43
- You can even make this shorter, by defining a delegate to car_type_name from CarType:\n\n\
44
- *in car_type_name.rb*\n \n delegate :name, :to => :car, :prefix => true\n\n\
45
- And now you can access this as \n\n car.car_type_name\n\n\
46
- However, it's less pleasant to insert with this technique:\n\n car.car_type.car_type_name = \"Sports\"\n car.car_type.save!\n #Now let's see what happened to the OTHER compact car\n Car.all.second.car_type_name #Oops, returns \"Sports\"\n\n\
47
- Right, what are we doing? We should've used\n\n car.update_attributes(car_type: CarType.find_or_create_by_name(name: \"Sports\"))\n\n\
48
- Okay. Probably want to shove that into its own method rather than have this repeated in the code several times. But you also need a helper method for creating cars that way\xE2\x80\xA6\n\n\
49
- Furthermore, ruby is good about caching, but it caches by the exact query used, and the cache expires after the controller action ends. You can configure more advanced caches, perhaps.\n\n\
50
- The thing is all this can get tedious if you use a normalized structure where you have 15 entities and each has at least one 'type-like' field. That's a whole lot of dangling Type objects. What you really want is an interface like this:\n\n car.all.first\n car.car_type #return \"Compact\"\n car.car_type = \"Sports\" #No effect on car.all.second, just automatically use the second constant\n car.car_type = \"Sedan\" #Magically create a new type\n\n\
51
- Oh, and it'll be nice if all of this is cached and you can define car types as constants (or symbols). You obviously still want to be able to run:\n\n CarType.where (:id > 3) #Just an example of supposed \"arbitrary\" SQL involving a real live CarType class\n\n\
52
- But you wanna minimize generating these numerous type classes. If you're like me, you don't even want to see them lying around in app/model. Who cares about them?\n\n\
53
- I've looked thoroughly for a nice rails solution to this, but after failing to find one, I created my own rails metaprogramming hook.\n\n\
54
- Installation\n\
55
- ------------\n\
56
- First, run `gem install rails_lookup` to install the latest version of the gem.\n\n\
57
- The result of this hook is that you get the exact syntax described above, with only two lines of code (no extra classes or anything):\n\
58
- In your ActiveRecord object simply add\n\n require 'active_record/lookup'\n class Car < ActiveRecord::Base\n #...\n include ActiveRecord::Lookup\n lookup :car_type\n #...\n end\n\n\
59
- That's it. the generated CarType class (which you won't see as a car_type.rb file, obviously, as it is generated in real-time), contains some nice methods to look into the cache as well: So you can call\n\n CarType.id_for \"Sports\" #Returns 2\n CarType.name_for 1 #Returns \"Compact\"\n\n\
60
- and you can still hack at the underlying ID for an object, if you need to:\n\n car = car.all.first\n car.car_type = \"Sports\"\n car.car_type_id #Returns 2\n car.car_type_id = 1\n car.car_type #Returns \"Compact\"\n\n\
61
- The only remaining thing is to define your migrations for creating the actual database tables. After all, that's something you only want to do once and not every time this class loads, so this isn't the place for it. However, it's easy enough to create your own scaffolds so that \n\n rails generate migration create_car_type_lookup_for_car\n\n\
62
- will automatically create the migration\n\n class CreateCarTypeLookupForCar < ActiveRecord::Migration\n def self.up\n create_table :car_types do |t|\n t.string :name\n t.timestamps #Btw you can remove these, I don't much like them in type tables anyway\n end\n \n remove_column :cars, :car_type #Let's assume you have one of those now\xE2\x80\xA6\n add_column :cars, :car_type_id, :integer #Maybe put not_null constraints here.\n end\n\n def self.down\n drop_table :car_types\n add_column :cars, :car_type, :string\n remove_column :cars, :car_type_id\n end\n end\n\n\
63
- I'll let you work out the details for actually migrating the data yourself. \n\n\
64
- [1] http://en.wikipedia.org/wiki/Database_normalization\n\
65
- [2] http://en.wikipedia.org/wiki/Column-oriented_DBMS\n\n\
66
- I hope this helped you and saved a lot of time and frustration. Follow me on twitter: @nimrodpriell\n\n"
67
- email: "@nimrodpriell"
12
+ date: 2011-08-09 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rails
16
+ requirement: &70313073195380 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *70313073195380
25
+ - !ruby/object:Gem::Dependency
26
+ name: sqlite3
27
+ requirement: &70313073194820 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: *70313073194820
36
+ description: Lookup tables with ruby-on-rails
37
+ email:
38
+ - nimrod.priell@gmail.com
68
39
  executables: []
69
-
70
40
  extensions: []
71
-
72
- extra_rdoc_files:
41
+ extra_rdoc_files:
73
42
  - README
74
- - lib/active_record/lookup.rb
75
- files:
43
+ - lib/rails_lookup.rb
44
+ files:
45
+ - .gitignore
46
+ - Gemfile
76
47
  - README
77
48
  - Rakefile
78
- - lib/active_record/lookup.rb
49
+ - lib/rails_lookup.rb
50
+ - lib/rails_lookup/version.rb
51
+ - rails_lookup-0.0.3.gem
79
52
  - rails_lookup.gemspec
80
53
  - test/20110808002412_create_test_lookup_db.rb
81
- - test/test_lookup.rb
54
+ - test/lookup_test.rb
82
55
  homepage: http://github.com/Nimster/RailsLookup/
83
56
  licenses: []
84
-
85
57
  post_install_message:
86
- rdoc_options:
58
+ rdoc_options:
87
59
  - --line-numbers
88
60
  - --inline-source
89
61
  - --title
90
- - Rails_lookup
62
+ - rails_lookup
91
63
  - --main
92
64
  - README
93
- require_paths:
65
+ require_paths:
94
66
  - lib
95
- required_ruby_version: !ruby/object:Gem::Requirement
67
+ required_ruby_version: !ruby/object:Gem::Requirement
96
68
  none: false
97
- requirements:
98
- - - ">="
99
- - !ruby/object:Gem::Version
100
- version: "0"
101
- required_rubygems_version: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - ! '>='
71
+ - !ruby/object:Gem::Version
72
+ version: '0'
73
+ required_rubygems_version: !ruby/object:Gem::Requirement
102
74
  none: false
103
- requirements:
104
- - - ">="
105
- - !ruby/object:Gem::Version
106
- version: "1.2"
75
+ requirements:
76
+ - - ! '>='
77
+ - !ruby/object:Gem::Version
78
+ version: '1.2'
107
79
  requirements: []
108
-
109
80
  rubyforge_project: rails_lookup
110
- rubygems_version: 1.8.7
81
+ rubygems_version: 1.8.15
111
82
  signing_key:
112
83
  specification_version: 3
113
- summary: Lookup table macro for ActiveRecords
114
- test_files:
115
- - test/test_lookup.rb
84
+ summary: Lookup table macro for ActiveRecords. See more in the README
85
+ test_files:
86
+ - test/lookup_test.rb
@@ -1,153 +0,0 @@
1
- # Author: Nimrod Priell (Twitter: @nimrodpriell)
2
- #
3
- # See README file or blog post for more.
4
- # This is intended to be included into ActiveRecord entities: Simply
5
- # include ActiveRecord::Lookup
6
- # in your class that extends ActiveRecord::Base
7
- module ActiveRecord
8
- module Lookup
9
- #These will be defined as class methods so they can be called as "macros"
10
- #like belongs_to
11
- module ClassMethods
12
- #We define only this "macro". In your ActiveRecord entity (say, Car), use
13
- # lookup :car_type
14
- # which will create Car#car_type, Car#car_type=, Car#car_type_id,
15
- # Car#car_type_id= and CarType, an ActiveRecord with a name (String)
16
- # attribute, that has_many :cars.
17
- #
18
- # You can also use
19
- # lookup :car_type, :as => :type
20
- # which will do the same thing but create Car#type, Car#type=, Car#type_id
21
- # and Car#type_id= instead.
22
- def lookup(lookup_name, opts = {})
23
- as_name = opts[:as] || lookup_name
24
-
25
- #Define the setter for the attribute by textual value
26
- mycls = self #Class I'm defined in
27
- #Ignore anonymous classes:
28
- mycls_sym = mycls.name.tableize.to_sym unless mycls.to_s =~ /#<.*>/
29
- lookup_cls_name = lookup_name.to_s.camelize
30
- begin
31
- #If it exists, just tack on the extra has_many
32
- cls = Object.const_get lookup_cls_name
33
- cls.has_many mycls_sym unless mycls_sym.blank? #for annon classes
34
- rescue NameError
35
- #Otherwise, actually define it
36
- cls = Class.new(ActiveRecord::Base) do
37
- has_many mycls_sym unless mycls_sym.blank? #For anon classes
38
- validates_uniqueness_of :name
39
- validates :name, :presence => true
40
-
41
- def self.id_for(name, id = nil)
42
- class_variable_get(:@@rcaches)[name] ||= id
43
- end
44
-
45
- def self.gen_id_for(val)
46
- id = id_for val
47
- if id.nil?
48
- #Define this new possible value
49
- new_db_obj = find_or_create_by_name val #You could just override this...
50
- id_for val, new_db_obj.id
51
- name_for new_db_obj.id, val
52
- id = new_db_obj.id
53
- end
54
- id
55
- end
56
-
57
- def self.name_for(id, name = nil)
58
- class_variable_get(:@@caches)[id] ||= name
59
- end
60
-
61
- #This is especially useful for tests - you want deletion to also clear
62
- #the caches otherwise odd stuff might happen
63
- def self.delete_all(*args)
64
- class_variable_set :@@caches, []
65
- class_variable_set :@@rcaches, {}
66
- super *args
67
- end
68
- end
69
-
70
- Object.const_set lookup_cls_name, cls #Define it as a global class
71
- end
72
-
73
-
74
- define_method("#{as_name.to_s}_id=") do |id|
75
- write_attribute "#{as_name.to_s}".to_sym, id
76
- end
77
-
78
- define_method("#{as_name.to_s}=") do |val|
79
- id = cls.gen_id_for val #TODO: Just call #{as_name.to_s}_id=
80
- write_attribute "#{as_name.to_s}".to_sym, id
81
- end
82
-
83
- define_method("#{as_name.to_s}_id") do
84
- read_attribute "#{as_name.to_s}".to_sym
85
- end
86
-
87
- #Define the getter
88
- define_method("#{as_name.to_s}") do
89
- id = read_attribute "#{as_name.to_s}".to_sym
90
- if not id.nil?
91
- value = cls.name_for id
92
- if value.nil?
93
- lookup_obj = cls.find_by_id id
94
- if not lookup_obj.nil?
95
- #TODO: Extract to a method
96
- cls.name_for id, lookup_obj.name
97
- cls.id_for lookup_obj.name, id
98
- end
99
- end
100
- end
101
- value
102
- end
103
-
104
-
105
- #Rails ActiveRecord support:
106
- @cls_for_name = {} if @cls_for_name.nil?
107
- @cls_for_name[as_name.to_s.to_sym] = cls
108
-
109
- #This makes find_and_create_by, find_all and where methods all work as
110
- #they should. This will not work if you use reset_column_information,
111
- #like if you use find_by_session_id in SessionStore. You must redefine
112
- #it so it re-sets rel to this singleton object
113
- rel = ActiveRecord::Relation.new(self, arel_table)
114
- def rel.where(opts, *rest)
115
- if opts.is_a? Hash
116
- mapped = opts.map { |k,v| @cls_for_name.has_key?(k.to_sym) ? [k, @cls_for_name[k.to_sym].id_for(v)] : [k, v] }
117
- opts = Hash[mapped]
118
- end
119
- super(opts, *rest)
120
- end
121
-
122
- def rel.cls_for_name=(cls_for_name)
123
- @cls_for_name = cls_for_name
124
- end
125
-
126
- rel.cls_for_name = @cls_for_name
127
- instance_variable_set(:@relation, rel)
128
-
129
- #Might need to be in class_eval
130
- belongs_to lookup_name.to_s.to_sym, :foreign_key => "#{as_name}_id"
131
- validates as_name.to_s.to_sym, :presence => true
132
-
133
- #Prefill the hashes from the DB:
134
- all_vals = cls.all
135
- #No need for the class_exec
136
- cls.class_variable_set(:@@rcaches, all_vals.inject({}) do |r, obj|
137
- r[obj.name] = obj.id
138
- r
139
- end)
140
- cls.class_variable_set(:@@caches, all_vals.inject([]) do |r, obj|
141
- r[obj.id] = obj.name
142
- r
143
- end)
144
- end
145
-
146
- end
147
-
148
- # extend host class with class methods when we're included
149
- def self.included(host_class)
150
- host_class.extend(ClassMethods)
151
- end
152
- end
153
- end