hobofields 0.7.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,57 @@
1
+ # HoboFields
2
+
3
+ ## About Doctests
4
+
5
+ HoboFields is documented and tested using *doctests*. This is an idea that comes from Python that we've been experimenting with for Hobo. Whenever you see code-blocks that start "`>>`", read them as IRB sessions. The `rdoctest` tool extracts these and runs them to verify they behave as advertised.
6
+
7
+ Doctests are a great way to get both documentation and tests from the same source. We're still experimenting with exactly how this all works though, so if the docs seem strange in places, please bear with us!
8
+
9
+ ## Introduction
10
+
11
+ HoboFields lives on GitHub: [http://github.com/tablatom/hobofields](http://github.com/tablatom/hobofields)
12
+
13
+ You can install it with git:
14
+
15
+ git clone git://github.com/tablatom/hobofields.git vendor/plugins/hobofields
16
+
17
+ Or subversion:
18
+
19
+ svn export svn://hobocentral.net/hobofields/tags/rel_0.7.4 vendor/plugins/hobofields
20
+
21
+ HoboFields provides two main features:
22
+
23
+ * An extension to ActiveRecord that provides rich field types such as "markdown text" or "email address"
24
+ * A generator that writes your migrations for you. Your migration writing days are over.
25
+
26
+ This is all done using a declaration of your fields that you put in your models, for example
27
+
28
+ class BlogPost < ActiveRecord::Base
29
+ fields do
30
+ title :string
31
+ body :text
32
+ end
33
+ end
34
+ {: .ruby}
35
+
36
+ **NOTE:** If you're going to use the migration generator outside of Hobo, do remember the `--skip-migration` option when generating your models:
37
+
38
+ ./script/generate model post --skip-migration
39
+
40
+
41
+ ## [Migration Generator](/hobofields/migration_generator)
42
+
43
+ Once you have declared your fields like this, you can run the following, at any time during the development of your project:
44
+
45
+ $ ./script/generate hobo_migration
46
+
47
+ The migration generator will create a migration to change from the schema that is currently in your database, to the schema that your models need. That's really all there is to it. Note that the migration generator is interactive -- it can't tell the difference between renaming something vs. adding one thing and removing another, so it has to ask you.
48
+
49
+ The [migration generator doctests](/hobofields/migration_generator) provide a lot more detail. They're not really that great as documentation because doctests run in a single irb session, and that doesn't fit well with the concept of a generator. Skip these unless you're really keen to see the details of the migration generator in action
50
+
51
+ ## [HoboFields API](/hobofields/hobofields_api)
52
+
53
+ As well as the migration generator, HoboFields provides a bunch of small extensions to ActiveRecord. The [HoboFields API doctests](/hobofields/hobofields_api) provide a useful reference to these.
54
+
55
+ ## [Rich Types](/hobofields/rich_types)
56
+
57
+ Documentation for the full set of rich types bundled with HoboFields, and how to create your own, is [here](/hobofields/rich_types).
@@ -0,0 +1,247 @@
1
+ # HoboFields API
2
+
3
+ ## Connect to the Database
4
+
5
+ In order for the API examples to run we need a connection to a database. You can ignore this if you're just looking for documentation.
6
+
7
+ >> require 'rubygems'
8
+ >> require 'activesupport'
9
+ >> Dependencies.load_paths << '.'
10
+ >> Dependencies.mechanism = :require
11
+ >> require 'activerecord'
12
+ >> require 'hobofields'
13
+ >> mysql_database = "hobofields_doctest"
14
+ >> system("mysqladmin create #{mysql_database}") or raise "could not create database"
15
+ >> ActiveRecord::Base.establish_connection(:adapter => "mysql",
16
+ :database => mysql_database,
17
+ :host => "localhost")
18
+
19
+ ## Example Models
20
+
21
+ Let's define some example models that we can use to demonstrate the API. With HoboFields we define the model's fields, with their name and type, directly in the model like so:
22
+
23
+ >>
24
+ class Advert < ActiveRecord::Base
25
+ fields do
26
+ title :string
27
+ body :text
28
+ contact_address :email_address
29
+ end
30
+ end
31
+
32
+ (Note: `:email_address` is an example of a "Rich Type" provided by HoboFields -- more on those later)
33
+
34
+ The migration generator uses this information to create a migration. The following creates and runs the migration so we're ready to go.
35
+
36
+ >> up, down = HoboFields::MigrationGenerator.run
37
+ >> ActiveRecord::Migration.class_eval up
38
+
39
+ We're now ready to start demonstrating the API
40
+
41
+ ## The Basics
42
+
43
+ The main feature of HoboFields, aside from the migration generator, is the ability to declare rich types for your fields. For example, you can declare that a field is an email address, and the field will be automatically validated for correct email address syntax.
44
+
45
+ ### Field Types
46
+
47
+ Field values are returned as the type you specify.
48
+
49
+ >> a = Advert.new :body => "This is the body"
50
+ >> a.body.class
51
+ => HoboFields::Text
52
+
53
+ This also works after a round-trip to the database
54
+
55
+ >> a.save
56
+ >> b = Advert.find(a.id)
57
+ >> b.body.class
58
+ => HoboFields::Text
59
+
60
+ HoboFields::Text is a simple subclass of string. It's a "wrapper type", by which we mean you pass the underlying value to the constructor.
61
+
62
+ >> t = HoboFields::Text.new("hello")
63
+ => "hello"
64
+ >> t.class
65
+ => HoboFields::Text
66
+
67
+ If you define your own rich types, they need to support a one argument constructor in the same way.
68
+
69
+ Although the body of our advert is really just a string, it's very useful that it has a different type. For example, the view layer in Hobo Rapid would use this information to render a `<textarea>` rather than an `<input type='text'>` in an Advert form.
70
+
71
+
72
+ ## Names vs. Classes
73
+
74
+ In the `fields do ... end` block you can give the field-type either as a name (symbol) or a class. For example, we could have said
75
+
76
+ body HoboFields::Text
77
+
78
+ Obviously the symbol form is a nicer:
79
+
80
+ body :text
81
+
82
+ If you provide a class it must define the `COLUMN_TYPE` constant. This instructs the migration generator to create the appropriate underlying database column type. It should be a symbol that is a valid column type in a Rails migration.
83
+
84
+ >> HoboFields::Text::COLUMN_TYPE
85
+ => :text
86
+
87
+ The full set of available symbolic names is
88
+
89
+ * `:integer`
90
+ * `:big_integer`
91
+ * `:float`
92
+ * `:string`
93
+ * `:text`
94
+ * `:boolean`
95
+ * `:date`
96
+ * `:datetime`
97
+ * `:html`
98
+ * `:textile`
99
+ * `:markdown`
100
+ * `:password`
101
+ * `:email_addresss`
102
+
103
+ You can add your own types too. More on that later.
104
+
105
+
106
+ ## Model extensions
107
+
108
+ HoboFields adds a few features to your models.
109
+
110
+ ### `Model.attr_type`
111
+
112
+ Returns the type (i.e. class) declared for a given field or attribute
113
+
114
+ >> Advert.attr_type :title
115
+ => String
116
+ >> Advert.attr_type :body
117
+ => HoboFields::Text
118
+
119
+ ### `Model.column`
120
+
121
+ A shorthand for accessing column metadata
122
+
123
+ >> col = Advert.column :title
124
+ >> col.name
125
+ "title"
126
+ >> col.klass
127
+ >> String
128
+
129
+ ### `Model.attr_accessor` with types
130
+
131
+ In your HoboFields models you can also give type information to "virtual fields" (i.e. regular Ruby attributes)
132
+
133
+ >>
134
+ class Advert
135
+ attr_accessor :my_attr, :type => :text
136
+ end
137
+ >> a = Advert.new
138
+ >> a.my_attr = "hello"
139
+ >> a.my_attr.class
140
+ => HoboFields::Text
141
+
142
+
143
+ ## Field validations
144
+
145
+ HoboFields gives you some shorthands for declaring some common validations right in the field declaration
146
+
147
+ ### Required fields
148
+
149
+ The `:required` argument to a field gives a `validates_presence_of`:
150
+
151
+ >>
152
+ class Advert
153
+ fields do
154
+ title :string, :required
155
+ end
156
+ end
157
+ >> a = Advert.new
158
+ >> a.valid?
159
+ => false
160
+ >> a.errors.full_messages
161
+ => ["Title can't be blank"]
162
+ >> a.title = "Jimbo"
163
+ >> a.save
164
+ => true
165
+
166
+
167
+ ### Unique fields
168
+
169
+ The `:unique` argument in a field declaration gives `validates_uniqueness_of`:
170
+
171
+ >>
172
+ class Advert < ActiveRecord::Base
173
+ fields do
174
+ title :string, :unique
175
+ end
176
+ end
177
+ >> a = Advert.new :title => "Jimbo"
178
+ >> a.valid?
179
+ => false
180
+ >> a.errors.full_messages
181
+ => ["Title has already been taken"]
182
+ >> a.title = "Sambo"
183
+ >> a.save
184
+ => true
185
+
186
+ Let's get back to the basic Advert class with no validations before we continue:
187
+
188
+ >> Dependencies.remove_constant "Advert"
189
+ >>
190
+ class Advert < ActiveRecord::Base
191
+ fields do
192
+ title :string
193
+ body :text
194
+ contact_address :email_address
195
+ end
196
+ end
197
+
198
+
199
+ ### Type specific validations
200
+
201
+ Rich types can define there own validations by a `#validate` method. It should return an error message if the value is invalid, otherwise nil. We can call that method directly to show how it works:
202
+
203
+ >> a = Advert.new :contact_address => "not really an email address"
204
+ >> a.contact_address.class
205
+ => HoboFields::EmailAddress
206
+ >> a.contact_address.validate
207
+ => "is not valid"
208
+
209
+ But normally that method would be called for us during validation:
210
+
211
+ >> a.valid?
212
+ => false
213
+ >> a.errors.full_messages
214
+ => ["Contact address is not valid"]
215
+ >> a.contact_address = "me@me.com"
216
+ >> a.valid?
217
+ => true
218
+
219
+ You can add this capability to your own rich types just by defining `#validate`
220
+
221
+ ### Validating virtual fields
222
+
223
+ You can set the type of a virtual field to a rich type, e.g.
224
+
225
+ >>
226
+ class Advert
227
+ attr_accessor :alternative_email, :type => :email_address
228
+ end
229
+
230
+ By default, virtual fields are not subject to validation.
231
+
232
+ >> a = Advert.new :alternative_email => "woot!"
233
+ >> a.valid?
234
+ => true
235
+
236
+ To have them validated use `validate_virtual_field`:
237
+
238
+ >>
239
+ class Advert
240
+ validate_virtual_field :alternative_email
241
+ end
242
+ >> a.valid?
243
+ => false
244
+
245
+ ## Cleanup
246
+
247
+ >> system "mysqladmin --force drop #{mysql_database}"
@@ -0,0 +1,410 @@
1
+ # HoboFields - Migration Generator
2
+
3
+ Note that these doctests are good tests but not such good docs. The migration generator doesn't really fit well with the doctest concept of a single IRB session. As you'll see, there's a lot of jumping-through-hoops and doing stuff that no normal user of the migration generator would ever do.
4
+
5
+ Firstly, in order to test the migration generator outside of a full Rails stack, there's a few things we need to do. First off we need to configure ActiveSupport for auto-loading
6
+
7
+ >> require 'rubygems'
8
+ >> require 'activesupport'
9
+ >> Dependencies.load_paths << '.'
10
+ >> Dependencies.mechanism = :require
11
+
12
+ And we'll require:
13
+
14
+ >> require 'activerecord'
15
+ >> require 'hobofields'
16
+
17
+ We also need to get ActiveRecord set up with a database connection
18
+
19
+ >> mysql_database = "hobofields_doctest"
20
+ >> system("mysqladmin create #{mysql_database}") or raise "could not create database"
21
+ >> ActiveRecord::Base.establish_connection(:adapter => "mysql",
22
+ :database => mysql_database,
23
+ :host => "localhost")
24
+
25
+ OK we're ready to get going.
26
+
27
+
28
+ ## The migration generator -- introduction
29
+
30
+ The migration generator works by:
31
+
32
+ * Loading all of the models in your Rails app
33
+ * Using the Rails schema-dumper to extract information about the current state of the database.
34
+ * Calculating the changes that are required to bring the database into sync with your application.
35
+
36
+ Normally you would run the migration generator as a regular Rails generator. You would type
37
+
38
+ $ script/generator hobo_migration
39
+
40
+ in your Rails app, and the migration file would be created in `db/migrate`.
41
+
42
+ In order to demonstrate the generator in this doctest script however, we'll be using the Ruby API instead. The method `HoboFields::MigrationGenerator.run` returns a pair of strings -- the up migration and the down migration.
43
+
44
+ At the moment the database is empty and no ActiveRecord models exist, so the generator is going to tell us there is nothing to do.
45
+
46
+ >> HoboFields::MigrationGenerator.run
47
+ => ["", ""]
48
+
49
+
50
+ ### Models without `fields do` are ignored
51
+
52
+ The migration generator only takes into account classes that use HoboFields, i.e. classes with a `fields do` declaration. Models without this are ignored:
53
+
54
+ >> class Advert < ActiveRecord::Base; end
55
+ >> HoboFields::MigrationGenerator.run
56
+ => ["", ""]
57
+
58
+
59
+ ### Create the table
60
+
61
+ Here we see a simple `create_table` migration along with the `drop_table` down migration
62
+
63
+ >>
64
+ class Advert < ActiveRecord::Base
65
+ fields do
66
+ name :string
67
+ end
68
+ end
69
+ >> up, down = HoboFields::MigrationGenerator.run
70
+ >> up
71
+ =>""
72
+ create_table :adverts do |t|
73
+ t.string :name
74
+ end
75
+ >> down
76
+ => "drop_table :adverts"
77
+
78
+ Normally we would run the generated migration with `rake db:create`. We can achieve the same effect directly in Ruby like this:
79
+
80
+ >> ActiveRecord::Migration.class_eval up
81
+ >> Advert.columns.*.name
82
+ => ["id", "name"]
83
+
84
+ We'll define a method to make that easier next time
85
+
86
+ >>
87
+ def migrate(renames={})
88
+ up, down = HoboFields::MigrationGenerator.run(renames)
89
+ puts up
90
+ ActiveRecord::Migration.class_eval(up)
91
+ ActiveRecord::Base.send(:subclasses).each { |model| model.reset_column_information }
92
+ [up, down]
93
+ end
94
+
95
+ We'll have a look at the migration generator in more detail later, first we'll have a look at the extra features HoboFields has added to the model.
96
+
97
+
98
+ ### Add fields
99
+
100
+ If we add a new field to the model, the migration generator will add it to the database.
101
+
102
+ >>
103
+ class Advert
104
+ fields do
105
+ name :string
106
+ body :text
107
+ published_at :datetime
108
+ end
109
+ end
110
+ >> up, down = migrate
111
+ >> up
112
+ =>""
113
+ add_column :adverts, :body, :text
114
+ add_column :adverts, :published_at, :datetime
115
+ >> down
116
+ =>""
117
+ remove_column :adverts, :body
118
+ remove_column :adverts, :published_at
119
+ >>
120
+
121
+ ### Remove fields
122
+
123
+ If we remove a field from the model, the migration generator removes the database column. Note that we have to explicitly clear the known fields to achieve this in rdoctest -- in a Rails context you would simply edit the file
124
+
125
+ >> Advert.field_specs.clear # not normally needed
126
+ class Advert < ActiveRecord::Base
127
+ fields do
128
+ name :string
129
+ body :text
130
+ end
131
+ end
132
+ >> up, down = migrate
133
+ >> up
134
+ => "remove_column :adverts, :published_at"
135
+ >> down
136
+ => "add_column :adverts, :published_at, :datetime"
137
+
138
+ ### Rename a field
139
+
140
+ Here we rename the `name` field to `title`. By default the generator sees this as removing `name` and adding `title`.
141
+
142
+ >> Advert.field_specs.clear # not normally needed
143
+ class Advert < ActiveRecord::Base
144
+ fields do
145
+ title :string
146
+ body :text
147
+ end
148
+ end
149
+ >> # Just generate - don't run the migration:
150
+ >> up, down = HoboFields::MigrationGenerator.run
151
+ >> up
152
+ =>""
153
+ add_column :adverts, :title, :string
154
+ remove_column :adverts, :name
155
+ >> down
156
+ =>""
157
+ remove_column :adverts, :title
158
+ add_column :adverts, :name, :string
159
+ >>
160
+
161
+ When run as a generator, the migration-generator won't make this assumption. Instead it will prompt for user input to resolve the ambiguity. When using the Ruby API, we can ask for a rename instead of an add + drop by passing in a hash:
162
+
163
+ >> up, down = HoboFields::MigrationGenerator.run(:adverts => { :name => :title })
164
+ >> up
165
+ => "rename_column :adverts, :name, :title"
166
+ >> down
167
+ => "rename_column :adverts, :title, :name"
168
+
169
+ Let's apply that change to the database
170
+
171
+ >> migrate
172
+
173
+
174
+ ### Change a type
175
+
176
+ >> Advert.attr_type :title
177
+ => String
178
+ >>
179
+ class Advert
180
+ fields do
181
+ title :text
182
+ body :text
183
+ end
184
+ end
185
+ >> up, down = HoboFields::MigrationGenerator.run
186
+ >> up
187
+ => "change_column :adverts, :title, :text"
188
+ >> down
189
+ => "change_column :adverts, :title, :string"
190
+
191
+
192
+ ### Add a default
193
+
194
+ >>
195
+ class Advert
196
+ fields do
197
+ title :string, :default => "Untitled"
198
+ body :text
199
+ end
200
+ end
201
+ >> up, down = migrate
202
+ >> up
203
+ => 'change_column :adverts, :title, :string, :default => "Untitled"'
204
+ >> down
205
+ => "change_column :adverts, :title, :string"
206
+
207
+
208
+ ### Foreign Keys
209
+
210
+ HoboFields extends the `belongs_to` macro so that it also declares the foreign-key field.
211
+
212
+ >>
213
+ class Advert
214
+ belongs_to :category
215
+ end
216
+ >> up, down = HoboFields::MigrationGenerator.run
217
+ >> up
218
+ => 'add_column :adverts, :category_id, :integer'
219
+ >> down
220
+ => 'remove_column :adverts, :category_id'
221
+
222
+ Cleanup:
223
+
224
+ >> Advert.field_specs.delete(:category_id)
225
+
226
+ If you specify a custom foreign key, the migration generator observes that:
227
+
228
+ >>
229
+ class Advert
230
+ belongs_to :category, :foreign_key => "c_id"
231
+ end
232
+ >> up, down = HoboFields::MigrationGenerator.run
233
+ >> up
234
+ => 'add_column :adverts, :c_id, :integer'
235
+ >> down
236
+ => 'remove_column :adverts, :c_id'
237
+
238
+ Cleanup:
239
+
240
+ >> Advert.field_specs.delete(:c_id)
241
+
242
+
243
+ ### Timestamps
244
+
245
+ `updated_at` and `created_at` can be declared with the shorthand `timestamps`
246
+
247
+ >>
248
+ class Advert
249
+ fields do
250
+ timestamps
251
+ end
252
+ end
253
+ >> up, down = HoboFields::MigrationGenerator.run
254
+ >> up
255
+ =>""
256
+ add_column :adverts, :created_at, :datetime
257
+ add_column :adverts, :updated_at, :datetime
258
+ >> down
259
+ =>""
260
+ remove_column :adverts, :created_at
261
+ remove_column :adverts, :updated_at
262
+ >>
263
+
264
+ Cleanup:
265
+
266
+ >> Advert.field_specs.delete(:updated_at)
267
+ >> Advert.field_specs.delete(:created_at)
268
+
269
+
270
+ ### Rename a table
271
+
272
+ The migration generator respects the `set_table_name` declaration, although as before, we need to explicitly tell the generator that we want a rename rather than a create and a drop.
273
+
274
+ >>
275
+ class Advert
276
+ set_table_name "ads"
277
+ fields do
278
+ title :string, :default => "Untitled"
279
+ body :text
280
+ end
281
+ end
282
+ >> up, down = HoboFields::MigrationGenerator.run(:adverts => :ads)
283
+ >> up
284
+ => "rename_table :adverts, :ads"
285
+ >> down
286
+ => "rename_table :ads, :adverts"
287
+
288
+ Set the table name back to what it should be and confirm we're in sync:
289
+
290
+ >> class Advert; set_table_name "adverts"; end
291
+ >> HoboFields::MigrationGenerator.run
292
+ => ["", ""]
293
+
294
+ ### Drop a table
295
+
296
+ If you delete a model, the migration generator will create a `drop_table` migration. Unfortunately there's no way to fully remove the Advert class we've defined from the doctest session, but we can tell the migration generator to ignore it.
297
+
298
+ >> HoboFields::MigrationGenerator.ignore_models = [ :advert ]
299
+
300
+ Dropping tables is where the automatic down-migration really comes in handy:
301
+
302
+ >> up, down = HoboFields::MigrationGenerator.run
303
+ >> up
304
+ => "drop_table :adverts"
305
+ >> down
306
+ =>""
307
+ create_table "adverts", :force => true do |t|
308
+ t.text "body"
309
+ t.string "title", :default => "Untitled"
310
+ end
311
+ >>
312
+
313
+ ### Rename a table
314
+
315
+ As with renaming columns, we have to tell the migration generator about the rename. Here we create a new class 'Advertisement'. Remember 'Advert' is being ignored so it's as if we renamed the definition in our models directory.
316
+
317
+ >>
318
+ class Advertisement < ActiveRecord::Base
319
+ fields do
320
+ title :string, :default => "Untitled"
321
+ body :text
322
+ end
323
+ end
324
+ >> up, down = HoboFields::MigrationGenerator.run(:adverts => :advertisements)
325
+ >> up
326
+ => "rename_table :adverts, :advertisements"
327
+ >> down
328
+ => "rename_table :advertisements, :adverts"
329
+
330
+ Now that we've seen the renaming we'll switch the 'ignore' setting to ignore that 'Advertisements' class.
331
+
332
+ >> HoboFields::MigrationGenerator.ignore_models = [ :advertisement ]
333
+
334
+ ## STI
335
+
336
+ ### Adding an STI subclass
337
+
338
+ Adding a subclass should introduce the 'type' column and no other changes
339
+
340
+ >>
341
+ class FancyAdvert < Advert
342
+ end
343
+ >> up, down = HoboFields::MigrationGenerator.run
344
+ >> up
345
+ => "add_column :adverts, :type, :string"
346
+ >> down
347
+ => "remove_column :adverts, :type"
348
+
349
+ Cleanup
350
+
351
+ >> Advert.field_specs.delete(:type)
352
+
353
+
354
+ ## Coping with multiple changes
355
+
356
+ The migration generator is designed to create complete migrations even if many changes to the models have taken place.
357
+
358
+ First let's confirm we're in a known state. One model, 'Advert', with a string 'title' and text 'body':
359
+
360
+ >> Advert.connection.tables
361
+ => ["adverts"]
362
+ >> Advert.columns.*.name
363
+ => ["id", "body", "title"]
364
+ >> HoboFields::MigrationGenerator.run
365
+ => ["", ""]
366
+
367
+
368
+ ### Rename a column and change the default
369
+
370
+ >> Advert.field_specs.clear
371
+ >>
372
+ class Advert
373
+ fields do
374
+ name :string, :default => "No Name"
375
+ body :text
376
+ end
377
+ end
378
+ >> up, down = HoboFields::MigrationGenerator.run(:adverts => {:title => :name})
379
+ >> up
380
+ =>""
381
+ rename_column :adverts, :title, :name
382
+ change_column :adverts, :name, :string, :default => "No Name"
383
+ >> down
384
+ =>""
385
+ rename_column :adverts, :name, :title
386
+ change_column :adverts, :title, :string, :default => "Untitled"
387
+ >>
388
+
389
+
390
+ ### Rename a table and add a column
391
+
392
+ >> HoboFields::MigrationGenerator.ignore_models << :advert
393
+ class Ad < ActiveRecord::Base
394
+ fields do
395
+ title :string, :default => "Untitled"
396
+ body :text
397
+ created_at :datetime
398
+ end
399
+ end
400
+ >> up, down = HoboFields::MigrationGenerator.run(:adverts => :ads)
401
+ >> up
402
+ =>""
403
+ rename_table :adverts, :ads
404
+
405
+ add_column :ads, :created_at, :datetime
406
+ >>
407
+
408
+ ## Cleanup
409
+
410
+ >> system "mysqladmin --force drop #{mysql_database}"