Module: Stuff

Defined in:
lib/aux_codes.rb

Class Method Summary collapse

Class Method Details

.included(base) ⇒ Object



12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
# File 'lib/aux_codes.rb', line 12

def self.included base
  base.class_eval do

    include DataMapper::Resource

    property :id,          DataMapper::Types::Serial
    property :aux_code_id, Integer, :default  => 0
    property :name,        String,  :required => true, :unique => :aux_code_id
    property :meta,        DataMapper::Types::Text

    # timestamps :at

    belongs_to :aux_code
    has n,     :aux_codes

    def self.name
      'AuxCode'
    end
    
    alias code     aux_code
    alias category aux_code
    alias codes    aux_codes

    # Helper methods to make DataMapper happy.
    #
    # In theory, we shouldn't use these in DataMapper but ... it's easy to port them

    def self.find_all_by_aux_code_id aux_code_id
      all :aux_code_id => aux_code_id
    end

    def self.find_by_name_and_aux_code_id name, aux_code_id
      first :name => name, :aux_code_id => aux_code_id
    end

    def self.find_or_create_by_name name
      first(:name => name) || create(:name => name)
    end

    def self.find_by_name name
      first :name => name
    end

    def self.find_all_by_name name
      all :name => name
    end

    def code_names
      codes.map &:name
    end

    def is_a_category?
      aux_code_id == 0
    end

    def class_name
      name.gsub(/[^[:alpha:]]/,'_').titleize.gsub(' ','').singularize
    end

    def to_s
      name
    end

    def [] attribute_or_code_name
      if attributes.include?attribute_or_code_name
        attributes[attribute_or_code_name]
      else
        found = codes.select {|c| c.name.to_s =~ /#{attribute_or_code_name}/ }
          if found.empty? # try case insensitive (sans underscores)
            found = codes.select {|c| c.name.downcase.gsub('_',' ').to_s =~ 
              /#{attribute_or_code_name.to_s.downcase.gsub('_',' ')}/ }
          end
        found.first if found
      end
    end

    def deserialized_meta_hash
      require 'yaml'
      self.meta ||= ""
      YAML::load(self.meta) || { }
    end

    def get_meta_attribute meta_attribute
      deserialized_meta_hash[meta_attribute.to_s]
    end

    def set_meta_attribute meta_attribute, value
      require 'yaml'
      meta_hash = deserialized_meta_hash
      meta_hash[meta_attribute.to_s] = value
      self.meta = meta_hash.to_yaml
    end

    # this allows us to say things like:
    #
    #   foo = AuxCode.create :name => 'foo'
    #   foo.codes.create :name => 'bar'
    #
    #   foo.bar # should return the bar aux code under the foo category
    #
    #   if bar doesn't exist, we throw a normal NoMethodError
    #
    #   this should check meta_attributes on the object too
    #
    def method_missing_with_indifferent_hash_style_values name, *args, &block
      method_missing_without_indifferent_hash_style_values name, *args, &block
    rescue NoMethodError => ex
      begin
        if name.to_s[/=$/]
          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
          save
        else
          code = self[name]
          code = self.get_meta_attribute(name) unless code
          raise ex unless code
          return code
        end
      rescue
        raise ex
      end
    end

    alias_method_chain :method_missing, :indifferent_hash_style_values

    # class methods
    class << self

      def categories
        AuxCode.find_all_by_aux_code_id(0)
      end

      def category_names
        AuxCode.categories.map &:name
      end

      def category category_object_or_id_or_name
        obj = category_object_or_id_or_name
        return obj if obj.is_a?(AuxCode)
        return AuxCode.get(obj) if obj.is_a?(Fixnum)
        if obj.is_a?(String) || obj.is_a?(Symbol)
          obj = obj.to_s
          found = AuxCode.find_by_name_and_aux_code_id(obj, 0)
          if found.nil?
            # try replacing underscores with spaces and doing a 'LIKE' search
            found = AuxCode.first :name.like => obj.gsub('_', ' '), :aux_code_id => 0
          end
          return found
        end
        raise "I don't know how to find an AuxCode of type #{ obj.class }"
      end
      alias [] category

      def category_codes category_object_or_id_or_name
        category( category_object_or_id_or_name ).codes
      end
      alias category_values category_codes

      def category_code_names category_object_or_id_or_name
        category( category_object_or_id_or_name ).code_names
      end

      def create_classes!
        AuxCode.categories.each do |category|
          Object.const_set category.class_name, category.aux_code_class unless Object.const_defined?(category.class_name)
        end
      end

      # this allows us to say things like:
      #
      #   AuxCode.create :name => 'foo'
      #
      #   AuxCode.foo # should return the foo category aux code
      #
      def method_missing_with_indifferent_hash_style_values name, *args, &block
        begin
          method_missing_without_indifferent_hash_style_values name, *args, &block
        rescue NoMethodError => ex
          begin
            self[name]
          rescue
            raise ex
          end
        end
      end
      
      alias_method_chain :method_missing, :indifferent_hash_style_values

      def load_yaml yaml_string
        require 'yaml'
        self.load_from YAML::load(yaml_string)
      end

      def load_file serialized_yaml_file_path
        load_yaml File.read(serialized_yaml_file_path)
      end

      # initialize AuxCodes ... looks for config/aux_codes.yml
      # and creates classes
      def init # should eventually take configuration options (hash || block)
        create_root_code!
        aux_codes_yml = File.join 'config', 'aux_codes.yml'
        if File.file? aux_codes_yml
          load_file aux_codes_yml
          create_classes!
        end
      end

      def create_root_code!
        # If there isn't an AuxCode yet, make one with the ID of 0.
        # This makes database foreign key constraints stop complaining 
        # when we create aux codes where the aux_code_id is 0.
        AuxCode.create :id => 0, :name => 'Aux Code Categories' unless AuxCode.get(0)
      end

      # 
      # loads AuxCodes (creates them) from a Hash, keyed on the name of the aux code categories to create
      #
      # hash: a Hash or an Array [ [key,value], [key,value] ] or anything with an enumerator 
      #       that'll work with `hash.each {|key,value| ... }`
      #
      def load_from hash
        return unless hash.is_a?(Hash)
        hash.each do |category_name, codes|
          category = AuxCode.find_or_create_by_name( category_name.to_s ).aux_code_class
          codes.each do |name, values|

            # only a name given
            if values.nil? or values.empty?
              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'
                category.create :name => name.to_s unless category.code_names.include?(name.to_s)

              elsif name.is_a? Hash # we have a Hash, likely with the create options, eg. { :name => 'hi', :foo =>'bar' }
                category.create name

              else
                raise "not sure how to create code in category #{ category.name } with: #{ name.inspect }"
              end

              # we have a name and values
            else
              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
                code = category[ name.to_s ]
                if code
                  values.each do |attribute, new_value|
                    code.send "#{attribute}=", new_value # update values
                  end
                else
                  code = category.create values.merge({ :name => name.to_s })
                end

              else
                raise "not sure how to create code in category #{ category.name } with: #{ name.inspect }, #{ values.inspect }"

              end
            end
          end
        end
      end

    end

  end

end