aux_codes 1.0.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,247 @@
1
+ $:.unshift File.dirname(__FILE__)
2
+
3
+ %w( rubygems activerecord aux_codes/migration ).each {|lib| require lib }
4
+
5
+ # top-level class for AuxCodes
6
+ #
7
+ # used for namespacing and global configuration (once added)
8
+ #
9
+ class AuxCodes
10
+ end
11
+
12
+ #
13
+ # the basic AuxCode ActiveRecord class
14
+ #
15
+ class AuxCode < ActiveRecord::Base
16
+
17
+ validates_presence_of :name
18
+ validates_uniqueness_of :name, :scope => :aux_code_id
19
+
20
+ belongs_to :aux_code
21
+ alias code aux_code
22
+ alias category aux_code
23
+
24
+ has_many :aux_codes
25
+ alias codes aux_codes
26
+
27
+ before_create :set_default_values
28
+
29
+ def code_names
30
+ codes.map &:name
31
+ end
32
+
33
+ def is_a_category?
34
+ aux_code_id == 0
35
+ end
36
+
37
+ def class_name
38
+ name.gsub(/[^[:alpha:]]/,'_').titleize.gsub(' ','').singularize
39
+ end
40
+
41
+ def to_s
42
+ name
43
+ end
44
+
45
+ def [] attribute_or_code_name
46
+ if attributes.include?attribute_or_code_name
47
+ attributes[attribute_or_code_name]
48
+ else
49
+ found = codes.select {|c| c.name.to_s =~ /#{attribute_or_code_name}/ }
50
+ if found.empty? # try case insensitive (sans underscores)
51
+ found = codes.select {|c| c.name.downcase.gsub('_',' ').to_s =~
52
+ /#{attribute_or_code_name.to_s.downcase.gsub('_',' ')}/ }
53
+ end
54
+ found.first if found
55
+ end
56
+ end
57
+
58
+ def deserialized_meta_hash
59
+ require 'yaml'
60
+ self.meta ||= ""
61
+ YAML::load(self.meta) || { }
62
+ end
63
+
64
+ def get_meta_attribute meta_attribute
65
+ deserialized_meta_hash[meta_attribute.to_s]
66
+ end
67
+
68
+ def set_meta_attribute meta_attribute, value
69
+ require 'yaml'
70
+ meta_hash = deserialized_meta_hash
71
+ meta_hash[meta_attribute.to_s] = value
72
+ self.meta = meta_hash.to_yaml
73
+ end
74
+
75
+ # this allows us to say things like:
76
+ #
77
+ # foo = AuxCode.create :name => 'foo'
78
+ # foo.codes.create :name => 'bar'
79
+ #
80
+ # foo.bar # should return the bar aux code under the foo category
81
+ #
82
+ # if bar doesn't exist, we throw a normal NoMethodError
83
+ #
84
+ # this should check meta_attributes on the object too
85
+ #
86
+ def method_missing_with_indifferent_hash_style_values name, *args, &block
87
+ method_missing_without_indifferent_hash_style_values name, *args, &block
88
+ rescue NoMethodError => ex
89
+ begin
90
+ if name.to_s[/=$/]
91
+ self.set_meta_attribute(name.to_s.sub(/=$/,''), args.first) # we said `code.foo= X` so we should set the foo meta attribute to X
92
+ save
93
+ else
94
+ code = self[name]
95
+ code = self.get_meta_attribute(name) unless code
96
+ raise ex unless code
97
+ return code
98
+ end
99
+ rescue
100
+ raise ex
101
+ end
102
+ end
103
+ alias_method_chain :method_missing, :indifferent_hash_style_values
104
+
105
+ # class methods
106
+ class << self
107
+
108
+ def categories
109
+ AuxCode.find_all_by_aux_code_id(0)
110
+ end
111
+
112
+ def category_names
113
+ AuxCode.categories.map &:name
114
+ end
115
+
116
+ def category category_object_or_id_or_name
117
+ obj = category_object_or_id_or_name
118
+ return obj if obj.is_a?AuxCode
119
+ return AuxCode.find(obj) if obj.is_a?Fixnum
120
+ if obj.is_a?(String) || obj.is_a?(Symbol)
121
+ obj = obj.to_s
122
+ found = AuxCode.find_by_name_and_aux_code_id(obj, 0)
123
+ if found.nil?
124
+ # try replacing underscores with spaces and doing a 'LIKE' search
125
+ found = AuxCode.find :first, :conditions => ["name LIKE ? AND aux_code_id = ?", obj.gsub('_', ' '), 0]
126
+ end
127
+ return found
128
+ end
129
+ raise "I don't know how to find an AuxCode of type #{ obj.class }"
130
+ end
131
+ alias [] category
132
+
133
+ def category_codes category_object_or_id_or_name
134
+ category( category_object_or_id_or_name ).codes
135
+ end
136
+ alias category_values category_codes
137
+
138
+ def category_code_names category_object_or_id_or_name
139
+ category( category_object_or_id_or_name ).code_names
140
+ end
141
+
142
+ def create_classes!
143
+ AuxCode.categories.each do |category|
144
+ Kernel::const_set category.class_name, category.aux_code_class
145
+ end
146
+ end
147
+
148
+ # this allows us to say things like:
149
+ #
150
+ # AuxCode.create :name => 'foo'
151
+ #
152
+ # AuxCode.foo # should return the foo category aux code
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
+
171
+ def load_yaml yaml_string
172
+ require 'yaml'
173
+ self.load YAML::load(yaml_string)
174
+ end
175
+
176
+ def load_file serialized_yaml_file_path
177
+ load_yaml File.read(serialized_yaml_file_path)
178
+ end
179
+
180
+ # initialize AuxCodes ... looks for config/aux_codes.yml
181
+ # and creates classes
182
+ def init # should eventually take configuration options (hash || block)
183
+ if ActiveRecord::Base.connection.tables.include? 'aux_codes'
184
+ aux_codes_yml = File.join 'config', 'aux_codes.yml'
185
+ if File.file? aux_codes_yml
186
+ load_file aux_codes_yml
187
+ create_classes!
188
+ end
189
+ end
190
+ end
191
+
192
+ #
193
+ # loads AuxCodes (creates them) from a Hash, keyed on the name of the aux code categories to create
194
+ #
195
+ # hash: a Hash or an Array [ [key,value], [key,value] ] or anything with an enumerator
196
+ # that'll work with `hash.each {|key,value| ... }`
197
+ #
198
+ def load hash
199
+ return unless hash.is_a?Hash
200
+ hash.each do |category_name, codes|
201
+ category = AuxCode.find_or_create_by_name( category_name.to_s ).aux_code_class
202
+ codes.each do |name, values|
203
+
204
+ # only a name given
205
+ if values.nil? or values.empty?
206
+ if name.is_a? String or name.is_a? Symbol # we have a String || Symbol, it's likely the code's name, eg. :foo or 'bar'
207
+ category.create :name => name.to_s unless category.code_names.include?(name.to_s)
208
+
209
+ elsif name.is_a? Hash # we have a Hash, likely with the create options, eg. { :name => 'hi', :foo =>'bar' }
210
+ category.create name
211
+
212
+ else
213
+ raise "not sure how to create code in category #{ category.name } with: #{ name.inspect }"
214
+ end
215
+
216
+ # we have a name and values
217
+ else
218
+ if values.is_a? Hash and (name.is_a? String or name.is_a? Symbol) # we have a Hash, likely with the create options ... we'll merge the name in as :name and create
219
+ code = category[ name.to_s ]
220
+ if code
221
+ values.each do |attribute, new_value|
222
+ code.send "#{attribute}=", new_value # update values
223
+ end
224
+ else
225
+ code = category.create values.merge({ :name => name.to_s })
226
+ end
227
+
228
+ else
229
+ raise "not sure how to create code in category #{ category.name } with: #{ name.inspect }, #{ values.inspect }"
230
+
231
+ end
232
+ end
233
+ end
234
+ end
235
+ end
236
+
237
+ end
238
+
239
+ protected
240
+
241
+ def set_default_values
242
+ self.aux_code_id = 0 unless aux_code_id
243
+ end
244
+
245
+ end
246
+
247
+ %w( aux_codes/aux_code_class ).each {|lib| require lib } # define the class returned by #aux_code_class
@@ -0,0 +1,150 @@
1
+ #
2
+ # extend AuxCode to return a full-blown ActiveRecord class
3
+ #
4
+ class AuxCode
5
+
6
+ def aux_code_class &block
7
+ klass = Class.new(AuxCode) do
8
+
9
+ class << self
10
+
11
+ attr_accessor :aux_code_id, :aux_code
12
+
13
+ def aux_code
14
+ # @aux_code ||= AuxCode.find aux_code_id
15
+ AuxCode.find aux_code_id
16
+ end
17
+
18
+ def [] code
19
+ aux_code[code]
20
+ end
21
+
22
+ #
23
+ # this handles typical ActiveRecord::Base method_missing features, eg: aux_code.find_by_name 'foo'
24
+ #
25
+ # we wrap these methods in the scope of this aux code (category)
26
+ #
27
+ def method_missing_with_aux_code_scope name, *args, &block
28
+ if name.to_s[/^find/]
29
+ if name.to_s[/or_create_by_/]
30
+ name = "#{name}_and_aux_code_id".to_sym
31
+ args << self.aux_code_id
32
+ # method_missing_without_aux_code_scope name, *args
33
+ AuxCode.send name, *args, &block
34
+ else
35
+ with_scope(:find => { :conditions => ['aux_code_id = ?', self.aux_code_id] }) do
36
+ method_missing_without_aux_code_scope name, *args, &block
37
+ end
38
+ end
39
+ else
40
+ method_missing_without_aux_code_scope name, *args, &block
41
+ end
42
+ rescue NoMethodError => ex
43
+ begin
44
+ aux_code.send name, *args, &block # try on the AuxCode instance for this class ...
45
+ rescue
46
+ raise ex
47
+ end
48
+ end
49
+ alias_method_chain :method_missing, :aux_code_scope
50
+
51
+ def count_with_aux_code_scope options = {}
52
+ with_scope(:find => { :conditions => ['aux_code_id = ?', self.aux_code_id] }) do
53
+ count_without_aux_code_scope options
54
+ end
55
+ end
56
+ alias_method_chain :count, :aux_code_scope
57
+
58
+ def find_with_aux_code_scope first_or_all, options = {}
59
+ with_scope(:find => { :conditions => ['aux_code_id = ?', self.aux_code_id] }) do
60
+ find_without_aux_code_scope first_or_all, options
61
+ end
62
+ end
63
+ alias_method_chain :find, :aux_code_scope
64
+
65
+ def create_with_aux_code_scope options = {}
66
+ create_without_aux_code_scope options.merge({ :aux_code_id => self.aux_code_id })
67
+ end
68
+ alias_method_chain :create, :aux_code_scope
69
+
70
+ def create_with_aux_code_scope! options = {}
71
+ create_without_aux_code_scope! options.merge({ :aux_code_id => self.aux_code_id })
72
+ end
73
+ alias_method_chain :create!, :aux_code_scope
74
+
75
+ def new_with_aux_code_scope options = {}
76
+ begin
77
+ new_without_aux_code_scope options.merge({ :aux_code_id => self.aux_code_id })
78
+ rescue ActiveRecord::UnknownAttributeError => ex
79
+
80
+ # we were likely passed some unknown meta attributes ... define them ...
81
+ meta_attribute_name = /unknown attribute: (.*)/.match(ex.message).captures.first
82
+ meta_attributes << meta_attribute_name
83
+ self.reload_meta_attributes!
84
+ new_with_aux_code_scope options # re-call ... WARNING ... might end up in infinite loop!
85
+
86
+ end
87
+ end
88
+ alias_method_chain :new, :aux_code_scope
89
+
90
+ end
91
+ end
92
+
93
+ klass.aux_code_id = self.id # the class needs to know its own aux_code_id
94
+
95
+ #
96
+ # add custom attributes
97
+ #
98
+ klass.class.class_eval do
99
+
100
+ # an array of valid meta attribute names
101
+ attr_accessor :meta_attributes
102
+
103
+ def attr_meta *attribute_names
104
+ @meta_attributes ||= []
105
+ @meta_attributes += attribute_names.map {|attribute_name| attribute_name.to_s }
106
+ @meta_attributes
107
+ end
108
+ end
109
+
110
+ # class customizations (if block passed in)
111
+ klass.class_eval(&block) if block
112
+
113
+ # for each of the meta_attributes defined, create getter and setter methods
114
+ #
115
+ # CAUTION: the way we're currently doing this, this'll only work if attr_meta
116
+ # is set when you initially get the aux_code_class ... adding
117
+ # meta attributes later won't currently work!
118
+ #
119
+ klass.class_eval {
120
+
121
+ def self.reload_meta_attributes!
122
+ self.meta_attributes ||= []
123
+
124
+ self.meta_attributes.each do |meta_attribute|
125
+
126
+ unless self.respond_to? meta_attribute
127
+ define_method(meta_attribute) do
128
+ get_meta_attribute(meta_attribute)
129
+ end
130
+ end
131
+
132
+ unless self.respond_to? "#{meta_attribute}="
133
+ define_method("#{meta_attribute}=") do |value|
134
+ set_meta_attribute(meta_attribute, value)
135
+ end
136
+ end
137
+
138
+ end
139
+ end
140
+
141
+ reload_meta_attributes!
142
+
143
+ }
144
+
145
+ # wow, need to clean this up ...
146
+
147
+ klass
148
+ end
149
+
150
+ end
@@ -0,0 +1,31 @@
1
+ #
2
+ # ActiveRecord migration for creating the aux_codes table
3
+ #
4
+ class AuxCodes
5
+ class CreateAuxCodes < ActiveRecord::Migration
6
+
7
+ def self.up
8
+ create_table :aux_codes, :comment => 'Auxilary Codes' do |t|
9
+
10
+ t.integer :aux_code_id, :comment => 'ID of parent aux code (Category)', :null => false
11
+ t.string :name, :comment => 'Name of Category code (or child code)', :null => false
12
+
13
+ # disabled for now, as they're not needed - no specs using this functionality
14
+ #
15
+ # %w( integer decimal string text boolean datetime ).each do |field_type|
16
+ # t.column field_type.to_sym, "#{field_type}_field"
17
+ # end
18
+
19
+ # this should be added conditionally, based on whether or not meta attributes are desired
20
+ t.text :meta, :comment => 'Serialized meta_attributes'
21
+
22
+ t.timestamps
23
+ end
24
+ end
25
+
26
+ def self.down
27
+ drop_table :aux_codes
28
+ end
29
+
30
+ end
31
+ end
metadata ADDED
@@ -0,0 +1,57 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: aux_codes
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.6
5
+ platform: ruby
6
+ authors:
7
+ - remi
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2010-01-20 00:00:00 -07: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
+ - lib/aux_codes.rb
26
+ - lib/aux_codes/aux_code_class.rb
27
+ - lib/aux_codes/migration.rb
28
+ has_rdoc: true
29
+ homepage: http://github.com/remi/aux_codes
30
+ licenses: []
31
+
32
+ post_install_message:
33
+ rdoc_options: []
34
+
35
+ require_paths:
36
+ - lib
37
+ required_ruby_version: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: "0"
42
+ version:
43
+ required_rubygems_version: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: "0"
48
+ version:
49
+ requirements: []
50
+
51
+ rubyforge_project:
52
+ rubygems_version: 1.3.5
53
+ signing_key:
54
+ specification_version: 3
55
+ summary: ActiveRecord plugin for easily managing lots of enumeration-type data
56
+ test_files: []
57
+