remi-aux_codes 1.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.markdown ADDED
@@ -0,0 +1,98 @@
1
+ aux codes
2
+ =========
3
+
4
+ Way back when, when I was learning database development, a mentor of mine showed me
5
+ a technique that he liked to use to consolidate similar enumeration-esque database tables.
6
+
7
+ Often, applications have enumeration-esque tables that merely store simply key / value pairs,
8
+ typically a unique ID and a string. Take for instance, a `genders` table:
9
+
10
+ [ genders ]
11
+ [id] [name]
12
+ 1 Male
13
+ 2 Female
14
+
15
+ That's a bit of a waste of a table, *especially* if your application has *TONS* of tables
16
+ just like this. There's where aux codes (or auxilary codes) come in!
17
+
18
+ [ aux_codes ]
19
+ [id] [category_id] [name]
20
+ 1 0 Genders <--- this defines a 'table' (because category_id = 0)
21
+ 2 1 Male <--- these are under category_id 1 (Genders)
22
+ 3 1 Female
23
+ 4 0 Colors <--- defines a color 'table'
24
+ 5 4 Red <--- category_id 4 = 'Colors', so this is a color
25
+ 6 4 Blue
26
+
27
+ Simple, eh?
28
+
29
+ Now, this is great, but this might get in the way of our business logic or it might
30
+ dirty up our code. We don't want to always be using the AuxCode object with complex
31
+ queries ... we probable want a Gender object that behaves just like it would with a
32
+ full-blown genders table.
33
+
34
+ Gender = AuxCode.category('Gender').aux_code_class
35
+
36
+ # the Gender class is a full-blown ActiveRecord class
37
+ # that should behave as if there's actually a `genders` table!
38
+
39
+ Gender.find_by_name 'Male'
40
+
41
+ Gender.find_or_create_by_name 'Female'
42
+
43
+ Gender.create :name => 'Female'
44
+
45
+ male = Gender.new :name => 'Male'
46
+ male.save
47
+
48
+ Gender.find :conditions => ['name = ?', 'Male']
49
+
50
+ Gender.count
51
+
52
+ Gender.codes
53
+ Gender.code_names
54
+ Gender.aux_code
55
+ Gender.aux_codes
56
+
57
+ If you want to create all of these classes at once, as constants:
58
+
59
+ AuxCode.create_classes!
60
+
61
+ You can also access codes with a Hash-like syntax
62
+
63
+ AuxCode['Genders']['Male']
64
+ AuxCode[:Genders][:Female]
65
+
66
+ Gender[:Male]
67
+
68
+ Or with an Indifferent Hash-like syntax
69
+
70
+ AuxCode.genders['Male']
71
+ AuxCode.genders[:Male]
72
+ AuxCode.genders.male
73
+
74
+ Gender.male
75
+
76
+ # these all return the same result
77
+ Breed.find_by_name 'Golden Retriever'
78
+ Breed['Golden Retriever']
79
+ Breed[:golden_retriever]
80
+ Breed.golden_retriever
81
+ AuxCodes.breeds.golden_retriever
82
+
83
+ TODO
84
+ ----
85
+
86
+ * custom fields, eg:
87
+ * `Breed#acronym`
88
+ * `Breed#description`
89
+ * `Breed#kennel_association_id` ...
90
+ * these need an easy way to be defined! `alias_attribute`?
91
+ * this needs to work in development mode, when the models are constantly being reloaded!
92
+ * you shouldn't have to know the name of the special field to use it ... something like `attr_string :name` could just grab the (first) string column!
93
+ * create (or update) method(s) for helping with the original AuxCode migration and for creating the special additional fields
94
+
95
+ NOTES
96
+ -----
97
+
98
+ this implementation is for ActiveRecord only! i hope to eventually make a similar DataMapper implementation
data/VERSION.yml ADDED
@@ -0,0 +1,4 @@
1
+ ---
2
+ :patch: 1
3
+ :major: 1
4
+ :minor: 0
@@ -0,0 +1,24 @@
1
+ #
2
+ # ActiveRecord migration for creating the aux_codes table
3
+ #
4
+ class CreateAuxCodes < ActiveRecord::Migration
5
+
6
+ def self.up
7
+ create_table :aux_codes, :comment => 'Auxilary Codes' do |t|
8
+
9
+ t.integer :aux_code_id, :comment => 'ID of parent aux code (Category)', :null => false
10
+ t.string :name, :comment => 'Name of Category code (or child code)', :null => false
11
+
12
+ %w( integer decimal string text boolean datetime ).each do |field_type|
13
+ t.column field_type.to_sym, "#{field_type}_field"
14
+ end
15
+
16
+ t.timestamps
17
+ end
18
+ end
19
+
20
+ def self.down
21
+ drop_table :aux_codes
22
+ end
23
+
24
+ end
data/lib/aux_codes.rb ADDED
@@ -0,0 +1,178 @@
1
+ $:.unshift File.dirname(__FILE__)
2
+
3
+ %w( rubygems activerecord aux_codes/migration ).each {|lib| require lib }
4
+
5
+ class AuxCode < ActiveRecord::Base
6
+
7
+ validates_presence_of :name
8
+ validates_uniqueness_of :name, :scope => :aux_code_id
9
+
10
+ belongs_to :aux_code
11
+ alias code aux_code
12
+ alias category aux_code
13
+
14
+ has_many :aux_codes
15
+ alias codes aux_codes
16
+
17
+ before_create :set_default_values
18
+
19
+ def code_names
20
+ codes.map &:name
21
+ end
22
+
23
+ def is_a_category?
24
+ aux_code_id == 0
25
+ end
26
+
27
+ def class_name
28
+ name.gsub(/[^[:alpha:]]/,'_').titleize.gsub(' ','').singularize
29
+ end
30
+
31
+ def [] attribute_or_code_name
32
+ if attributes.include?attribute_or_code_name
33
+ attributes[attribute_or_code_name]
34
+ else
35
+ found = codes.select {|c| c.name.to_s =~ /#{attribute_or_code_name}/ }
36
+ if found.empty? # try case insensitive (sans underscores)
37
+ found = codes.select {|c| c.name.downcase.gsub('_',' ').to_s =~
38
+ /#{attribute_or_code_name.to_s.downcase.gsub('_',' ')}/ }
39
+ end
40
+ found.first if found
41
+ end
42
+ end
43
+
44
+ def method_missing_with_indifferent_hash_style_values name, *args, &block
45
+ method_missing_without_indifferent_hash_style_values name, *args, &block
46
+ rescue NoMethodError => ex
47
+ begin
48
+ self[name]
49
+ rescue
50
+ raise ex
51
+ end
52
+ end
53
+ alias_method_chain :method_missing, :indifferent_hash_style_values
54
+
55
+ def aux_code_class
56
+ klass = Class.new(AuxCode) do
57
+ class << self
58
+ attr_accessor :aux_code_id, :aux_code
59
+
60
+ def aux_code
61
+ # @aux_code ||= AuxCode.find aux_code_id
62
+ AuxCode.find aux_code_id
63
+ end
64
+
65
+ def count_with_aux_code_scope options = {}
66
+ with_scope(:find => { :conditions => ['aux_code_id = ?', self.aux_code_id] }) do
67
+ count_without_aux_code_scope options
68
+ end
69
+ end
70
+ def method_missing_with_aux_code_scope name, *args, &block
71
+ if name.to_s[/^find/]
72
+ if name.to_s[/or_create_by_/]
73
+ name = "#{name}_and_aux_code_id".to_sym
74
+ args << self.aux_code_id
75
+ # method_missing_without_aux_code_scope name, *args
76
+ AuxCode.send name, *args, &block
77
+ else
78
+ with_scope(:find => { :conditions => ['aux_code_id = ?', self.aux_code_id] }) do
79
+ method_missing_without_aux_code_scope name, *args, &block
80
+ end
81
+ end
82
+ else
83
+ method_missing_without_aux_code_scope name, *args, &block
84
+ end
85
+ rescue NoMethodError => ex
86
+ begin
87
+ aux_code.send name, *args, &block # try on the AuxCode instance for this class ...
88
+ rescue
89
+ raise ex
90
+ end
91
+ end
92
+ def find_with_aux_code_scope first_or_all, options = {}
93
+ with_scope(:find => { :conditions => ['aux_code_id = ?', self.aux_code_id] }) do
94
+ find_without_aux_code_scope first_or_all, options
95
+ end
96
+ end
97
+ def create_with_aux_code_scope options = {}
98
+ create_without_aux_code_scope options.merge({ :aux_code_id => self.aux_code_id })
99
+ end
100
+ def create_with_aux_code_scope! options = {}
101
+ create_without_aux_code_scope! options.merge({ :aux_code_id => self.aux_code_id })
102
+ end
103
+ def new_with_aux_code_scope options = {}
104
+ new_without_aux_code_scope options.merge({ :aux_code_id => self.aux_code_id })
105
+ end
106
+
107
+ alias_method_chain :count, :aux_code_scope
108
+ alias_method_chain :method_missing, :aux_code_scope
109
+ alias_method_chain :find, :aux_code_scope
110
+ alias_method_chain :create, :aux_code_scope
111
+ alias_method_chain :create!, :aux_code_scope
112
+ alias_method_chain :new, :aux_code_scope
113
+ end
114
+ end
115
+
116
+ klass.aux_code_id = self.id # the class needs to know its own aux_code_id
117
+ klass
118
+ end
119
+
120
+ class << self
121
+ def categories
122
+ AuxCode.find_all_by_aux_code_id(0)
123
+ end
124
+
125
+ def category_names
126
+ AuxCode.categories.map &:name
127
+ end
128
+
129
+ def category category_object_or_id_or_name
130
+ obj = category_object_or_id_or_name
131
+ return obj if obj.is_a?AuxCode
132
+ return AuxCode.find(obj) if obj.is_a?Fixnum
133
+ return AuxCode.find_by_name_and_aux_code_id(obj, 0) if obj.is_a?String
134
+ return AuxCode.find_by_name_and_aux_code_id(obj.to_s, 0) if obj.is_a?Symbol
135
+ raise "I don't know how to find an AuxCode of type #{ obj.class }"
136
+ end
137
+ alias [] category
138
+
139
+ def category_codes category_object_or_id_or_name
140
+ category( category_object_or_id_or_name ).codes
141
+ end
142
+ alias category_values category_codes
143
+
144
+ def category_code_names category_object_or_id_or_name
145
+ category( category_object_or_id_or_name ).code_names
146
+ end
147
+
148
+ def create_classes!
149
+ AuxCode.categories.each do |category|
150
+ Kernel::const_set category.class_name, category.aux_code_class
151
+ end
152
+ end
153
+
154
+ def method_missing_with_indifferent_hash_style_values name, *args, &block
155
+ unless self.respond_to?:aux_code_id # in which case, this is a *derived* class, not AuxCode
156
+ begin
157
+ method_missing_without_indifferent_hash_style_values name, *args, &block
158
+ rescue NoMethodError => ex
159
+ begin
160
+ self[name]
161
+ rescue
162
+ raise ex
163
+ end
164
+ end
165
+ else
166
+ method_missing_without_indifferent_hash_style_values name, *args, &block
167
+ end
168
+ end
169
+ alias_method_chain :method_missing, :indifferent_hash_style_values
170
+ end
171
+
172
+ protected
173
+
174
+ def set_default_values
175
+ self.aux_code_id = 0 unless aux_code_id
176
+ end
177
+
178
+ end
@@ -0,0 +1,205 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+
3
+ describe AuxCode do
4
+
5
+ it 'should require a name' do
6
+ AuxCode.create( :name => nil ).should_not be_valid
7
+ AuxCode.create( :name => ' ' ).should_not be_valid
8
+ AuxCode.create( :name => 'foo' ).should be_valid
9
+ end
10
+
11
+ it 'should have many aux codes (if category)' do
12
+ code = AuxCode.create! :name => 'foo'
13
+ code.codes.should be_empty
14
+ w00t = code.codes.create :name => 'w00t'
15
+ code.codes.should include(w00t)
16
+ end
17
+
18
+ it 'should have code names (if category)' do
19
+ code = AuxCode.create! :name => 'foo'
20
+ code.codes.create :name => 'chunky'
21
+ code.codes.create :name => 'bacon'
22
+
23
+ code.codes.length.should == 2
24
+ code.code_names.length.should == 2
25
+ code.code_names.should include('chunky')
26
+ code.code_names.should include('bacon')
27
+ end
28
+
29
+ it "should know whether it's a category or a category value / code" do
30
+ code = AuxCode.create! :name => 'foo'
31
+ w00t = code.codes.create :name => 'w00t'
32
+
33
+ w00t.is_a_category?.should be_false
34
+ code.is_a_category?.should be_true
35
+ end
36
+
37
+ it 'should belong to a category (if not a category)' do
38
+ code = AuxCode.create! :name => 'foo'
39
+ w00t = code.codes.create! :name => 'w00t'
40
+ w00t.aux_code.should == code
41
+ w00t.code.should == code
42
+ w00t.category.should == code
43
+ w00t.category.codes.should include(w00t)
44
+
45
+ w00t.codes.should be_empty
46
+ code.category.should be_nil
47
+ end
48
+
49
+ it 'should require a name unique to category' do
50
+ first_category = AuxCode.create! :name => 'foo'
51
+ second_category = AuxCode.create! :name => 'bar'
52
+
53
+ first_category.aux_codes.create( :name => 'w00t' ).should be_valid
54
+ first_category.aux_codes.create( :name => 'w00t' ).should_not be_valid
55
+ second_category.aux_codes.create( :name => 'w00t' ).should be_valid
56
+ end
57
+
58
+ it 'should be able to easily get all categories' do
59
+ AuxCode.categories.should be_empty
60
+
61
+ first_category = AuxCode.create! :name => 'foo'
62
+ second_category = AuxCode.create! :name => 'bar'
63
+ code_1 = first_category.codes.create :name => 'w00t'
64
+ code_2 = first_category.codes.create :name => 'w00t'
65
+
66
+ AuxCode.categories.should include(first_category)
67
+ AuxCode.categories.should include(second_category)
68
+ AuxCode.categories.should_not include(code_1)
69
+ AuxCode.categories.should_not include(code_2)
70
+ end
71
+
72
+ it 'should be able to easily get all category names' do
73
+ AuxCode.category_names.should be_empty
74
+
75
+ first_category = AuxCode.create! :name => 'foo'
76
+ second_category = AuxCode.create! :name => 'bar'
77
+ code_1 = first_category.codes.create :name => 'w00t'
78
+
79
+ AuxCode.category_names.should include('foo')
80
+ AuxCode.category_names.should include('bar')
81
+ AuxCode.category_names.should_not include('w00t')
82
+ end
83
+
84
+ it 'should be able to fetch a category give the category, its name, or its id' do
85
+ cat = AuxCode.create! :name => 'foo'
86
+ AuxCode.category( cat ).should == cat
87
+ AuxCode.category( cat.id ).should == cat
88
+ AuxCode.category( cat.name ).should == cat
89
+ AuxCode.category( cat.name.to_sym ).should == cat
90
+ end
91
+
92
+ it 'should be able to easily get all values (codes) for a category' do
93
+ first_category = AuxCode.create! :name => 'foo'
94
+ code_1 = first_category.codes.create :name => 'w00t'
95
+ code_2 = first_category.codes.create :name => 'chunky'
96
+ code_3 = first_category.codes.create :name => 'bacon'
97
+
98
+ [ AuxCode.category_values( first_category ), AuxCode.category_values( 'foo' ) ].each do |codes|
99
+ codes.should include(code_1)
100
+ codes.should include(code_2)
101
+ codes.should include(code_3)
102
+ codes.should_not include(first_category)
103
+ end
104
+ AuxCode.category_values( first_category ).should == AuxCode.category_codes( first_category )
105
+ end
106
+
107
+ it 'should be able to easily get all value names (code names) for a category' do
108
+ first_category = AuxCode.create! :name => 'foo'
109
+ code_1 = first_category.codes.create :name => 'w00t'
110
+ code_2 = first_category.codes.create :name => 'chunky'
111
+ code_3 = first_category.codes.create :name => 'bacon'
112
+
113
+ [ AuxCode.category_code_names( first_category ), AuxCode.category_code_names( 'foo' ) ].each do |codes|
114
+ codes.should include('w00t')
115
+ codes.should include('chunky')
116
+ codes.should include('bacon')
117
+ codes.should_not include('foo')
118
+ end
119
+ end
120
+
121
+ it 'should be able to get a Class for each category' do
122
+ random_category = AuxCode.create! :name => 'random-category-name'
123
+ foo_category = AuxCode.create! :name => 'foo'
124
+ foo_class = foo_category.aux_code_class
125
+ foo_class.count.should == 0
126
+
127
+ x = foo_category.codes.create :name => 'chunky'
128
+ foo_class.count.should == 1
129
+
130
+ foo_class.create :name => 'bacon'
131
+ foo_class.count.should == 2
132
+
133
+ foo = foo_class.create! :name => 'foo'
134
+ foo_class.count.should == 3
135
+
136
+ bar = foo_class.new :name => 'bar'
137
+ bar.save
138
+ foo_class.count.should == 4
139
+
140
+ w00t = foo_class.new :name => 'w00t'
141
+ w00t.save!
142
+ foo_class.count.should == 5
143
+
144
+ foo_class.find( :first, :conditions => ['name = ?', 'foo'] ).should == foo
145
+ foo_class.find( :first, :conditions => ['name = ?', 'foo'] ).should_not == foo_category
146
+
147
+ foo_class.find_by_name( 'foo' ).should == foo
148
+ foo_class.find_by_name( 'foo' ).should_not == foo_category
149
+
150
+ foo_class.find_all_by_name( 'foo' ).should include(foo)
151
+ foo_class.find_all_by_name( 'foo' ).should_not include(foo_category)
152
+
153
+ foo_class.all.should include(foo)
154
+ foo_class.all.should_not include(foo_category)
155
+
156
+ foo_class.find_by_name( 'neat' ).should be_nil
157
+ foo_class.find_or_create_by_name( 'neat' ).should_not be_nil
158
+ foo_class.find_by_name( 'neat' ).should_not be_nil
159
+ foo_class.code_names.should include('neat')
160
+ end
161
+
162
+ it 'should create good class names from category names for #create_classes!' do
163
+ AuxCode.new( :name => 'foos' ).class_name.should == 'Foo'
164
+ AuxCode.new( :name => 'foo Bars' ).class_name.should == 'FooBar'
165
+ AuxCode.new( :name => 'foo Bar lar Tars' ).class_name.should == 'FooBarLarTar'
166
+ AuxCode.new( :name => 'foo_Bar lar-Tars' ).class_name.should == 'FooBarLarTar'
167
+ AuxCode.new( :name => 'foo_Bar0lar1Tar5s' ).class_name.should == 'FooBarLarTar'
168
+ AuxCode.new( :name => 'Dog' ).class_name.should == 'Dog'
169
+ AuxCode.new( :name => 'Dogs' ).class_name.should == 'Dog'
170
+ end
171
+
172
+ it 'should be able to get categories and their values using Hash syntax' do
173
+ foo_category = AuxCode.create! :name => 'foo'
174
+ bar_category = AuxCode.create! :name => 'bar'
175
+ chunky = foo_category.codes.create :name => 'chunky'
176
+ bacon = foo_category.codes.create :name => 'bacon'
177
+
178
+ AuxCode['foo'].should == foo_category
179
+ AuxCode[:foo].should == foo_category
180
+
181
+ AuxCode['foo'][:chunky].should == chunky
182
+ AuxCode[:foo]['chunky'].should == chunky
183
+ end
184
+
185
+ it 'should be able to get categories and their values using indifferent Hash syntax' do
186
+ foo_category = AuxCode.create! :name => 'foo'
187
+ bar_category = AuxCode.create! :name => 'bar'
188
+ chunky = foo_category.codes.create :name => 'chunky'
189
+ bacon = foo_category.codes.create :name => 'bacon'
190
+
191
+ AuxCode.foo.should == foo_category
192
+ AuxCode.foo.chunky.should == chunky
193
+ end
194
+
195
+ it 'should be able to handle names with spaces (with Hash syntax)' do
196
+ foo_category = AuxCode.create! :name => 'foo'
197
+ bar_category = AuxCode.create! :name => 'bar'
198
+ chunky = foo_category.codes.create :name => 'I am Chunky'
199
+ bacon = foo_category.codes.create :name => 'Yay for Bacon'
200
+
201
+ foo_category.i_am_chunky.should == chunky
202
+ foo_category[:yay_for_bacon].should == bacon
203
+ end
204
+
205
+ end
@@ -0,0 +1,21 @@
1
+ require File.dirname(__FILE__) + '/../lib/aux_codes'
2
+ require 'spec'
3
+
4
+ ActiveRecord::Base.establish_connection :adapter => 'sqlite3', :database => ':memory:'
5
+ CreateAuxCodes.verbose = false
6
+ CreateAuxCodes.migrate :up
7
+
8
+ # use transactions
9
+ AuxCode; # hit one of the AR classes
10
+ Spec::Runner.configure do |config|
11
+ config.before(:each) do
12
+ ActiveRecord::Base.connection.increment_open_transactions
13
+ ActiveRecord::Base.connection.begin_db_transaction
14
+ end
15
+ config.after(:each) do
16
+ if ActiveRecord::Base.connection.open_transactions != 0
17
+ ActiveRecord::Base.connection.rollback_db_transaction
18
+ ActiveRecord::Base.connection.decrement_open_transactions
19
+ end
20
+ end
21
+ end
metadata ADDED
@@ -0,0 +1,60 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: remi-aux_codes
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.1
5
+ platform: ruby
6
+ authors:
7
+ - remi
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-01-27 00:00:00 -08:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: ActiveRecord plugin for easily managing lots of enumeration-type data
17
+ email: remi@remitaylor.com
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files: []
23
+
24
+ files:
25
+ - VERSION.yml
26
+ - README.markdown
27
+ - lib/aux_codes
28
+ - lib/aux_codes/migration.rb
29
+ - lib/aux_codes.rb
30
+ - spec/aux_code_spec.rb
31
+ - spec/spec_helper.rb
32
+ has_rdoc: true
33
+ homepage: http://github.com/remi/aux_codes
34
+ post_install_message:
35
+ rdoc_options:
36
+ - --inline-source
37
+ - --charset=UTF-8
38
+ require_paths:
39
+ - lib
40
+ required_ruby_version: !ruby/object:Gem::Requirement
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ version: "0"
45
+ version:
46
+ required_rubygems_version: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ version: "0"
51
+ version:
52
+ requirements: []
53
+
54
+ rubyforge_project:
55
+ rubygems_version: 1.2.0
56
+ signing_key:
57
+ specification_version: 2
58
+ summary: ActiveRecord plugin for easily managing lots of enumeration-type data
59
+ test_files: []
60
+