empty_eye 0.4.0

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,222 @@
1
+ module EmptyEye
2
+ class ViewExtensionCollection
3
+
4
+ #a collection of all the view_extensions
5
+ #these are wranglers for the shards
6
+ #uses 'array' as a proxy
7
+ #performs array methods by passing things off in method missing
8
+
9
+ def initialize(parent)
10
+ @parent = parent
11
+ @array = []
12
+ end
13
+
14
+ #the proxy object for instances
15
+ def array
16
+ @array
17
+ end
18
+
19
+ #we want to see the proxy object not the class info
20
+ def inspect
21
+ array.inspect
22
+ end
23
+
24
+ #the class to which the instance belongs
25
+ def parent
26
+ @parent
27
+ end
28
+
29
+ def descend(klass)
30
+ @parent = klass
31
+ self
32
+ end
33
+
34
+ #add extensions based on association from parent
35
+ def association(assoc)
36
+ new_extension = ViewExtension.new(assoc)
37
+ reject! {|extension| new_extension.name == extension.name}
38
+ push(new_extension)
39
+ new_extension
40
+ end
41
+
42
+ #add primary extension; needs a table only
43
+ def primary_table(table)
44
+ push(@primary = PrimaryViewExtension.new(table, parent))
45
+ end
46
+
47
+ #generates view sql
48
+ def create_view_sql
49
+ #determine what shard will handle what columns
50
+ map_attribute_management
51
+ #start with primary table
52
+ query = primary_arel_table
53
+
54
+ #build select clause with correct table handling the appropriate columns
55
+ arel_columns.each do |arel_column|
56
+ query = query.project(arel_column)
57
+ end
58
+
59
+ #build joins
60
+ without_primary.each do |ext|
61
+ current = ext.arel_table
62
+ key = ext.foreign_key.to_sym
63
+ if ext.type_column
64
+ query = query.join(current).on(
65
+ primary.key.eq(current[key]), ext.type_column.eq(ext.type_value)
66
+ )
67
+ else
68
+ query = query.join(current).on(
69
+ primary.key.eq(current[key])
70
+ )
71
+ end
72
+ end
73
+
74
+ #we dont need to keep this data
75
+ free_arel_columns
76
+
77
+ #STI condition if needed
78
+ if primary.sti_also?
79
+ query.where(primary.type_column.eq(primary.type_value))
80
+ end
81
+
82
+ #build veiw creation statement
83
+ "CREATE VIEW #{parent.table_name} AS\n#{query.to_sql}"
84
+ end
85
+
86
+ #takes the name of extension and a hash of intended updates from master instance
87
+ #returns a subset of hash with only values the extension handles
88
+ def delegate_map(name, hash)
89
+ keys = update_mapping[name] & hash.keys
90
+ keys.inject({}) do |res, col|
91
+ res[col] = hash[col] if hash[col]
92
+ res
93
+ end
94
+ end
95
+
96
+ #in the end this will be an array of argument arrays
97
+ #[[:validates_presence_of, :name, {}]]
98
+ #parent will call the method and associated args inheriting validations
99
+ def validations
100
+ @validations ||= []
101
+ end
102
+
103
+ #the primary extension
104
+ def primary
105
+ @primary
106
+ end
107
+
108
+ #array of shard classes
109
+ def shards
110
+ map(&:shard)
111
+ end
112
+
113
+ #this object responds to array methods
114
+ def respond_to?(m)
115
+ super || array.respond_to?(m)
116
+ end
117
+
118
+ #delegate to the array proxy when the method is missing
119
+ def method_missing(m, *args, &block)
120
+ if respond_to?(m)
121
+ array.send(m, *args, &block)
122
+ else
123
+ super
124
+ end
125
+ end
126
+
127
+ private
128
+
129
+ #all of the arel columns mapped to the right arel tables
130
+ def arel_columns
131
+ @arel_columns ||= []
132
+ end
133
+
134
+ #we dont need to keep this data
135
+ def free_arel_columns
136
+ @arel_columns = nil
137
+ end
138
+
139
+ #tracks the attributes with the view extension that will handle it
140
+ def update_mapping
141
+ @update_mapping ||= {}
142
+ end
143
+
144
+ #generate a foreign_key if it is missing
145
+ def default_foreign_key
146
+ view_name = parent.table_name.singularize
147
+ "#{view_name}_id"
148
+ end
149
+
150
+ #the primary arel table
151
+ def primary_arel_table
152
+ primary.arel_table
153
+ end
154
+
155
+ #all the tables
156
+ def tables
157
+ map(&:table)
158
+ end
159
+
160
+ #map the columns to the extension that will handle it
161
+ def map_attribute_management
162
+ #clear out what we know
163
+ arel_columns.clear
164
+ #use this to track and remove dupes
165
+ tracker = {}
166
+ each do |ext|
167
+ #mimic the parent's associations through primary shard
168
+ primary.have_one(ext)
169
+ ext.columns.each do |col|
170
+ column = col.to_sym
171
+ #skip if we already have this column
172
+ next if tracker[column]
173
+ #set to true so we wont do again
174
+ tracker[column] = true
175
+ #add the column based on the extension's arel_table
176
+ arel_columns << ext.arel_table[column]
177
+ #later we need to know how to update thing correctly
178
+ update_mapping[ext.name] = update_mapping[ext.name].to_a << col
179
+ #delegate the setter for column to shard of extension through primary shard
180
+ primary.delegate_to(column, ext) unless ext.primary
181
+ #mti class must inherit validations
182
+ add_validations(column, ext)
183
+ end
184
+ end
185
+ end
186
+
187
+ #tried a cleaner solution but it wouldnt work
188
+ #here i am stealing the arguments needed from the shards
189
+ #to call the same validation on the master class (parent)
190
+ def add_validations(column, ext)
191
+ return unless ext.shard._validators[column].present?
192
+ #primary either has no validations or they have already been inherited
193
+ return if ext.primary
194
+ rtn = ext.shard._validators[column].each do |validator|
195
+ meth = case validator.class.to_s
196
+ when /presence/i then :validates_presence_of
197
+ when /acceptance/i then :validates_acceptance_of
198
+ when /numericality/i then :validates_numericality_of
199
+ when /length/i then :validates_length_of
200
+ when /inclusion/i then :validates_inclusion_of
201
+ when /format/i then :validates_format_of
202
+ when /exclusion/i then :validates_exclusion_of
203
+ when /confirmation/i then :validates_confirmation_of
204
+ when /uniqueness/i then :validates_uniqueness_of
205
+ else nil
206
+ end
207
+ if meth
208
+ args = []
209
+ args << meth
210
+ args << column
211
+ args << validator.options
212
+ validations << args
213
+ end
214
+ end
215
+ end
216
+
217
+ #return a list of extensions without primary
218
+ def without_primary
219
+ array.select {|ext| ext != primary}
220
+ end
221
+ end
222
+ end
data/lib/empty_eye.rb ADDED
@@ -0,0 +1,30 @@
1
+ require "active_record"
2
+ require "arel"
3
+
4
+ require "empty_eye/version"
5
+
6
+ require "empty_eye/persistence"
7
+ require "empty_eye/relation"
8
+ require "empty_eye/errors"
9
+ require "empty_eye/view_extension"
10
+ require "empty_eye/primary_view_extension"
11
+ require "empty_eye/view_extension_collection"
12
+ require "empty_eye/shard"
13
+ require "empty_eye/associations/builder/shard_has_one"
14
+ require "empty_eye/associations/shard_has_one_association"
15
+ require "empty_eye/associations/shard_association_scope"
16
+ require "empty_eye/shard_association_reflection"
17
+
18
+ require "empty_eye/active_record/base"
19
+ require "empty_eye/active_record/schema_dumper"
20
+ require "empty_eye/active_record/connection_adapter"
21
+
22
+ module EmptyEye
23
+ # Your code goes here...
24
+ end
25
+
26
+ ::ActiveRecord::Base.send :include, EmptyEye::Persistence
27
+ ::ActiveRecord::Base.send :include, EmptyEye::Relation
28
+ ::ActiveRecord::Associations::Builder::HasOne.valid_options += [:except, :only]
29
+ ::ActiveRecord::Associations::Builder::BelongsTo.valid_options += [:except, :only]
30
+
@@ -0,0 +1,35 @@
1
+ require 'spec_helper'
2
+ require 'bundler/setup'
3
+
4
+ class SmallMechanic < ActiveRecord::Base
5
+ mti_class :mechanics_core do |t|
6
+ has_one :garage, :foreign_key => :mechanic_id, :except => 'specialty'
7
+ end
8
+ end
9
+
10
+ class TinyMechanic < ActiveRecord::Base
11
+ mti_class :mechanics_core do |t|
12
+ has_one :garage, :foreign_key => :mechanic_id, :only => 'specialty'
13
+ end
14
+ end
15
+
16
+ describe ActiveRecord::Base do
17
+
18
+ describe "MTI class configuration" do
19
+ it "should exclude columns with except option" do
20
+ mechanic_columns = Mechanic.column_names
21
+ small_mechanic_columns = SmallMechanic.column_names
22
+ delta = mechanic_columns - small_mechanic_columns
23
+ delta.should eq(["specialty"])
24
+ end
25
+ end
26
+
27
+ describe "MTI class configuration" do
28
+ it "should restrict columns with only option" do
29
+ garage_columns = Garage.column_names
30
+ tiny_mechanic_columns = TinyMechanic.column_names
31
+ intersection = garage_columns & tiny_mechanic_columns
32
+ intersection.should eq(["id", "specialty"])
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,130 @@
1
+ require 'spec_helper'
2
+ require 'bundler/setup'
3
+
4
+
5
+ describe ActiveRecord::Base do
6
+ before(:each) do
7
+ exec_sql "truncate bars_core"
8
+ exec_sql "truncate businesses"
9
+
10
+ @bar = Bar.create(
11
+ :music_genre => "Latin", :best_nights => "Tuesdays", :dress_code => "casual", # bar_core attributes
12
+ :address => "1904 Easy Kaley Orlando, FL 32806", :name => 'Chicos', :phone => '123456789' #business attributes
13
+ )
14
+ end
15
+
16
+ describe "create" do
17
+ it "should create a mti class correctly" do
18
+ @bar.music_genre.should eq("Latin")
19
+ @bar.best_nights.should eq("Tuesdays")
20
+ @bar.dress_code.should eq("casual")
21
+ @bar.address.should eq("1904 Easy Kaley Orlando, FL 32806")
22
+ @bar.name.should eq("Chicos")
23
+ @bar.phone.should eq("123456789")
24
+ end
25
+
26
+ it "should create associations correctly" do
27
+ @bar.business.class.should eq(Business)
28
+
29
+ @bar.business.address.should eq("1904 Easy Kaley Orlando, FL 32806")
30
+ @bar.business.name.should eq("Chicos")
31
+ @bar.business.phone.should eq("123456789")
32
+ end
33
+ end
34
+
35
+ describe "read" do
36
+ it "should find a mti class correctly" do
37
+ @found_bar = Bar.find_by_id(@bar.id)
38
+ @bar.should eq(@found_bar)
39
+ end
40
+ end
41
+
42
+ describe "update" do
43
+ it "should update a mti class correctly with update_attributes" do
44
+ @bar.phone.should eq("123456789")
45
+ @bar.update_attributes(:phone => '987654321') #attribute from business
46
+ @bar.reload
47
+ @bar.phone.should eq("987654321")
48
+ @bar.business.phone.should eq("987654321")
49
+ end
50
+
51
+ it "should update a mti class correctly with assignment" do
52
+ @bar.phone.should eq("123456789")
53
+ @bar.phone = '987654321' #attribute from business
54
+ @bar.save
55
+ @bar.reload
56
+ @bar.phone.should eq("987654321")
57
+ @bar.business.phone.should eq("987654321")
58
+ end
59
+
60
+ it "should update a mti class correctly with Class.update" do
61
+ Bar.update(@bar.id, :name => 'Betos')
62
+ @bar.reload
63
+ @bar.name.should eq('Betos')
64
+ end
65
+
66
+ it "should update a mti class correctly with Class.update_all" do
67
+ rtn = Bar.update_all(:name => 'Betos')
68
+ @bar.reload
69
+ rtn.should eq(1)
70
+ @bar.name.should eq('Betos')
71
+ end
72
+
73
+ it "should not update a mti class incorrectly with Class.update_all" do
74
+ rtn = Bar.update_all({:name => 'Betos'}, ["id = ?", @bar.id + 1]) #choose the wrong one
75
+ @bar.reload
76
+ rtn.should eq(0)
77
+ @bar.name.should eq('Chicos')
78
+ end
79
+ end
80
+
81
+ describe "delete" do
82
+ it "should destroy a mti class correctly" do
83
+ @business = @bar.business
84
+ @bar.destroy
85
+ @bar.destroyed?.should eq(true)
86
+ Bar.find_by_id(@bar.id).should eq(nil)
87
+ Business.find_by_id(@business.id).should eq(nil)
88
+ end
89
+
90
+ it "should destroy_all mti class correctly" do
91
+ Bar.count.should eq(1)
92
+ Business.count.should eq(1)
93
+ Bar.destroy_all
94
+ Bar.count.should eq(0)
95
+ Business.count.should eq(0)
96
+ end
97
+
98
+ it "should not destroy_all mti class incorrectly" do
99
+ Bar.count.should eq(1)
100
+ Business.count.should eq(1)
101
+ Bar.destroy_all(:id => @bar.id + 1) #choose the wrong one
102
+ Bar.count.should eq(1)
103
+ Business.count.should eq(1)
104
+ end
105
+
106
+ it "should delete a mti class correctly" do
107
+ @bar.delete
108
+ @bar.destroyed?.should eq(true)
109
+ Bar.find_by_id(@bar.id).should eq(nil)
110
+ end
111
+
112
+ it "should delete_all mti class correctly" do
113
+ Bar.count.should eq(1)
114
+ Business.count.should eq(1)
115
+ rtn = Bar.delete_all
116
+ rtn.should eq(1)
117
+ Bar.count.should eq(0)
118
+ Business.count.should eq(0)
119
+ end
120
+
121
+ it "should not delete_all mti class incorrectly" do
122
+ Bar.count.should eq(1)
123
+ Business.count.should eq(1)
124
+ rtn = Bar.delete_all(:id => @bar.id + 1) #choose the wrong one
125
+ rtn.should eq(0)
126
+ Bar.count.should eq(1)
127
+ Business.count.should eq(1)
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,160 @@
1
+ require 'spec_helper'
2
+ require 'bundler/setup'
3
+
4
+
5
+ describe ActiveRecord::Base do
6
+ before(:each) do
7
+ exec_sql "truncate eating_venues_core"
8
+ exec_sql "truncate restaurants"
9
+ exec_sql "truncate businesses"
10
+
11
+ @venue = EatingVenue.create(
12
+ :api_venue_id => 'abcdefg', :latitude => '122.11111', :longitude => '-81,11111', # eating venue attributes
13
+ :kids_area => false, :wifi => true, :food_genre => "mexican", # restaurant attributes
14
+ :address => "1904 Easy Kaley Orlando, FL 32806", :name => 'Chicos', :phone => '123456789' #business attributes
15
+ )
16
+ end
17
+
18
+ describe "create" do
19
+ it "should create a mti to sti to mti class correctly" do
20
+ @venue.api_venue_id.should eq('abcdefg')
21
+ @venue.latitude.should eq('122.11111')
22
+ @venue.longitude.should eq('-81,11111')
23
+ @venue.kids_area.should eq(false)
24
+ @venue.wifi.should eq(true)
25
+ @venue.food_genre.should eq("mexican")
26
+ @venue.address.should eq("1904 Easy Kaley Orlando, FL 32806")
27
+ @venue.name.should eq("Chicos")
28
+ @venue.phone.should eq("123456789")
29
+ end
30
+
31
+ it "should create mti to sti to mti associations correctly" do
32
+ @venue.mexican_restaurant.class.should eq(MexicanRestaurant)
33
+
34
+ @venue.mexican_restaurant.kids_area.should eq(false)
35
+ @venue.mexican_restaurant.wifi.should eq(true)
36
+ @venue.mexican_restaurant.food_genre.should eq("mexican")
37
+ @venue.mexican_restaurant.address.should eq("1904 Easy Kaley Orlando, FL 32806")
38
+ @venue.mexican_restaurant.name.should eq("Chicos")
39
+ @venue.mexican_restaurant.phone.should eq("123456789")
40
+ end
41
+ end
42
+
43
+ describe "read" do
44
+ it "should find a mti to sti to mti class correctly" do
45
+ @found_venue = EatingVenue.find_by_id(@venue.id)
46
+ @venue.should eq(@found_venue)
47
+ end
48
+ end
49
+
50
+ describe "update" do
51
+ it "should update a mti to sti to mti class correctly with update_attributes" do
52
+ @venue.phone.should eq("123456789")
53
+ @venue.wifi.should eq(true)
54
+ @venue.update_attributes(:phone => '987654321', :wifi => false) #attribute from business
55
+ @venue.reload
56
+ @venue.phone.should eq("987654321")
57
+ @venue.wifi.should eq(false)
58
+ @venue.mexican_restaurant.phone.should eq("987654321")
59
+ @venue.mexican_restaurant.wifi.should eq(false)
60
+ end
61
+
62
+ it "should update a mti to sti to mti class correctly with assignment" do
63
+ @venue.phone.should eq("123456789")
64
+ @venue.wifi.should eq(true)
65
+ @venue.wifi = false
66
+ @venue.phone = '987654321' #attribute from business
67
+ @venue.save
68
+ @venue.reload
69
+ @venue.wifi.should eq(false)
70
+ @venue.phone.should eq("987654321")
71
+ @venue.mexican_restaurant.phone.should eq("987654321")
72
+ @venue.mexican_restaurant.wifi.should eq(false)
73
+ end
74
+
75
+ it "should update a mti to sti to mti class correctly with Class.update" do
76
+ EatingVenue.update(@venue.id, :name => 'Betos', :food_genre => 'Italian')
77
+ @venue.reload
78
+ @venue.name.should eq('Betos')
79
+ @venue.food_genre.should eq('Italian')
80
+ end
81
+
82
+ it "should update a mti class correctly with Class.update_all" do
83
+ rtn = EatingVenue.update_all(:name => 'Betos', :food_genre => 'Italian')
84
+ @venue.reload
85
+ rtn.should eq(1)
86
+ @venue.name.should eq('Betos')
87
+ @venue.food_genre.should eq('Italian')
88
+ end
89
+
90
+ it "should not update a mti to sti to mti class incorrectly with Class.update_all" do
91
+ rtn = EatingVenue.update_all({:name => 'Betos', :food_genre => 'Italian'}, ["id = ?", @venue.id + 1]) #choose the wrong one
92
+ @venue.reload
93
+ rtn.should eq(0)
94
+ @venue.name.should eq('Chicos')
95
+ @venue.food_genre.should eq('mexican')
96
+ end
97
+ end
98
+
99
+ describe "delete" do
100
+ it "should destroy a mti to sti to mti class correctly" do
101
+ @restaurant = @venue.mexican_restaurant
102
+ @venue.destroy
103
+ @venue.destroyed?.should eq(true)
104
+ EatingVenue.find_by_id(@venue.id).should eq(nil)
105
+ @restaurant.business.should eq(nil)
106
+ MexicanRestaurant.find_by_id(@restaurant.id).should eq(nil)
107
+ end
108
+
109
+ it "should destroy_all mti to sti to mti class correctly" do
110
+ EatingVenue.count.should eq(1)
111
+ Business.count.should eq(1)
112
+ MexicanRestaurant.count.should eq(1)
113
+ EatingVenue.destroy_all
114
+ EatingVenue.count.should eq(0)
115
+ Business.count.should eq(0)
116
+ MexicanRestaurant.count.should eq(0)
117
+ end
118
+
119
+ it "should not destroy_all mti to sti to mti class incorrectly" do
120
+ EatingVenue.count.should eq(1)
121
+ Business.count.should eq(1)
122
+ EatingVenue.destroy_all(:id => @venue.id + 1) #choose the wrong one
123
+ EatingVenue.count.should eq(1)
124
+ Business.count.should eq(1)
125
+ MexicanRestaurant.count.should eq(1)
126
+ end
127
+
128
+ it "should delete a mti to sti to mti class correctly" do
129
+ Business.count.should eq(1)
130
+ MexicanRestaurant.count.should eq(1)
131
+ @venue.delete
132
+ @venue.destroyed?.should eq(true)
133
+ EatingVenue.find_by_id(@venue.id).should eq(nil)
134
+ Business.count.should eq(0)
135
+ MexicanRestaurant.count.should eq(0)
136
+ end
137
+
138
+ it "should delete_all mti to sti to mti class correctly" do
139
+ EatingVenue.count.should eq(1)
140
+ Business.count.should eq(1)
141
+ MexicanRestaurant.count.should eq(1)
142
+ rtn = EatingVenue.delete_all
143
+ rtn.should eq(1)
144
+ EatingVenue.count.should eq(0)
145
+ Business.count.should eq(0)
146
+ MexicanRestaurant.count.should eq(0)
147
+ end
148
+
149
+ it "should not delete_all mti to sti to mti class incorrectly" do
150
+ EatingVenue.count.should eq(1)
151
+ Business.count.should eq(1)
152
+ MexicanRestaurant.count.should eq(1)
153
+ rtn = EatingVenue.delete_all(:id => @venue.id + 1) #choose the wrong one
154
+ rtn.should eq(0)
155
+ EatingVenue.count.should eq(1)
156
+ Business.count.should eq(1)
157
+ MexicanRestaurant.count.should eq(1)
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,120 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+
4
+ require 'empty_eye'
5
+
6
+ # RSpec.configure do |config|
7
+ # # some (optional) config here
8
+ # end
9
+
10
+ ActiveRecord::Base.establish_connection(
11
+ :adapter => "mysql2",
12
+ :database => "empty_eye_test"
13
+ )
14
+
15
+ def exec_sql(sql)
16
+ ActiveRecord::Base.connection.execute sql
17
+ end
18
+
19
+ ActiveRecord::Migration.create_table :restaurants, :force => true do |t|
20
+ t.string :type
21
+ t.boolean :kids_area
22
+ t.boolean :wifi
23
+ t.integer :eating_venue_id
24
+ t.string :food_genre
25
+ t.datetime :created_at
26
+ t.datetime :updated_at
27
+ t.datetime :deleted_at
28
+ end
29
+
30
+ ActiveRecord::Migration.create_table :bars_core, :force => true do |t|
31
+ t.string :music_genre
32
+ t.string :best_nights
33
+ t.string :dress_code
34
+ t.datetime :created_at
35
+ t.datetime :updated_at
36
+ t.datetime :deleted_at
37
+ end
38
+
39
+ ActiveRecord::Migration.create_table :businesses, :force => true do |t|
40
+ t.integer :biz_id
41
+ t.string :biz_type
42
+ t.string :name
43
+ t.string :address
44
+ t.string :phone
45
+ end
46
+
47
+ ActiveRecord::Migration.create_table :eating_venues_core, :force => true do |t|
48
+ t.string :api_venue_id
49
+ t.string :latitude
50
+ t.string :longitude
51
+ end
52
+
53
+ ActiveRecord::Migration.create_table :eating_venues_core, :force => true do |t|
54
+ t.string :api_venue_id
55
+ t.string :latitude
56
+ t.string :longitude
57
+ end
58
+
59
+ ActiveRecord::Migration.create_table :garages, :force => true do |t|
60
+ t.boolean :privately_owned
61
+ t.integer :max_wait_days
62
+ t.string :specialty
63
+ t.string :email
64
+ t.integer :mechanic_id
65
+ end
66
+
67
+ ActiveRecord::Migration.create_table :mechanics_core, :force => true do |t|
68
+ t.string :name
69
+ end
70
+
71
+ class Business < ActiveRecord::Base
72
+ belongs_to :biz, :polymorphic => true
73
+ end
74
+
75
+ class Restaurant < ActiveRecord::Base
76
+ belongs_to :foursquare_venue
77
+ end
78
+
79
+ class MexicanRestaurant < Restaurant
80
+ mti_class do |t|
81
+ has_one :business, :as => :biz
82
+ end
83
+ end
84
+
85
+ class Bar < ActiveRecord::Base
86
+ mti_class do |t|
87
+ has_one :business, :as => :biz
88
+ end
89
+ end
90
+
91
+ class EatingVenue < ActiveRecord::Base
92
+ mti_class do |t|
93
+ has_one :mexican_restaurant
94
+ end
95
+ end
96
+
97
+ class Garage < ActiveRecord::Base
98
+ belongs_to :mechanic, :foreign_key => :mechanic_id
99
+
100
+ validates_presence_of :privately_owned
101
+ validates_numericality_of :max_wait_days
102
+ validates_length_of :email, :minimum => 7
103
+ validates_format_of :email, :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i
104
+ validates_uniqueness_of :email
105
+ validates_inclusion_of :specialty, :in => %w{foreign domestic antique something_crazy}
106
+ validates_exclusion_of :specialty, :in => %{ something_crazy }
107
+ end
108
+
109
+ class Mechanic < ActiveRecord::Base
110
+ mti_class :mechanics_core do |t|
111
+ has_one :garage, :foreign_key => :mechanic_id
112
+ end
113
+
114
+ validates_presence_of :name
115
+ validates_uniqueness_of :name
116
+ end
117
+
118
+
119
+
120
+