hobofields 0.8.10 → 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Rakefile +6 -3
- data/lib/hobo_fields/email_address.rb +1 -1
- data/lib/hobo_fields/enum_string.rb +1 -0
- data/lib/hobo_fields/field_spec.rb +22 -3
- data/lib/hobo_fields/index_spec.rb +45 -0
- data/lib/hobo_fields/migration_generator.rb +47 -7
- data/lib/hobo_fields/model_extensions.rb +29 -7
- data/lib/hobo_fields.rb +3 -1
- data/rails_generators/hobo_migration/USAGE +47 -0
- data/rails_generators/hobo_migration/hobo_migration_generator.rb +2 -2
- data/rails_generators/hobofield_model/hobofield_model_generator.rb +1 -1
- data/test/hobofields.rdoctest +1 -1
- data/test/hobofields_api.rdoctest +12 -7
- data/test/migration_generator.rdoctest +224 -35
- data/test/migration_generator_primary_key.rdoctest +10 -5
- data/test/rich_types.rdoctest +9 -23
- data/test/test_hobofield_model_generator.rb +3 -0
- metadata +6 -4
data/Rakefile
CHANGED
@@ -2,6 +2,9 @@ require 'rubygems'
|
|
2
2
|
require 'activerecord'
|
3
3
|
ActiveRecord::ActiveRecordError # hack for https://rails.lighthouseapp.com/projects/8994/tickets/2577-when-using-activerecordassociations-outside-of-rails-a-nameerror-is-thrown
|
4
4
|
|
5
|
+
RUBY = ENV['RUBY'] || (defined?(JRUBY_VERSION) ? 'jruby' : 'ruby')
|
6
|
+
RUBYDOCTEST = ENV['RUBYDOCTEST'] || "#{RUBY} `which rubydoctest`"
|
7
|
+
|
5
8
|
$:.unshift File.join(File.expand_path(File.dirname(__FILE__)), '/../hobofields/lib')
|
6
9
|
$:.unshift File.join(File.expand_path(File.dirname(__FILE__)), '/../hobosupport/lib')
|
7
10
|
require 'hobosupport'
|
@@ -10,13 +13,13 @@ require 'hobofields'
|
|
10
13
|
namespace "test" do
|
11
14
|
desc "Run the doctests"
|
12
15
|
task :doctest do |t|
|
13
|
-
exit(1) if !system("
|
16
|
+
exit(1) if !system("#{RUBYDOCTEST} test/*.rdoctest")
|
14
17
|
end
|
15
18
|
|
16
19
|
desc "Run the unit tests"
|
17
20
|
task :unit do |t|
|
18
21
|
Dir["test/test_*.rb"].each do |f|
|
19
|
-
exit(1) if !system("
|
22
|
+
exit(1) if !system("#{RUBY} #{f}")
|
20
23
|
end
|
21
24
|
end
|
22
25
|
end
|
@@ -30,7 +33,7 @@ begin
|
|
30
33
|
gemspec.summary = "Rich field types and migration generator for Rails"
|
31
34
|
gemspec.homepage = "http://hobocentral.net/"
|
32
35
|
gemspec.authors = ["Tom Locke"]
|
33
|
-
gemspec.rubyforge_project = "
|
36
|
+
gemspec.rubyforge_project = "hobo"
|
34
37
|
gemspec.add_dependency("rails", [">= 2.2.2"])
|
35
38
|
gemspec.add_dependency("hobosupport", ["= #{HoboFields::VERSION}"])
|
36
39
|
end
|
@@ -28,6 +28,7 @@ module HoboFields
|
|
28
28
|
c = Class.new(EnumString) do
|
29
29
|
values.each do |v|
|
30
30
|
const_name = v.upcase.gsub(/[^a-z0-9_]/i, '_').gsub(/_+/, '_')
|
31
|
+
const_name = "V" + const_name if const_name =~ /^[0-9_]/
|
31
32
|
const_set(const_name, self.new(v)) unless const_defined?(const_name)
|
32
33
|
|
33
34
|
method_name = "is_#{v.underscore}?"
|
@@ -17,6 +17,18 @@ module HoboFields
|
|
17
17
|
|
18
18
|
TYPE_SYNONYMS = [[:timestamp, :datetime]]
|
19
19
|
|
20
|
+
begin
|
21
|
+
MYSQL_COLUMN_CLASS = ActiveRecord::ConnectionAdapters::MysqlColumn
|
22
|
+
rescue NameError
|
23
|
+
MYSQL_COLUMN_CLASS = NilClass
|
24
|
+
end
|
25
|
+
|
26
|
+
begin
|
27
|
+
SQLITE_COLUMN_CLASS = ActiveRecord::ConnectionAdapters::SQLiteColumn
|
28
|
+
rescue NameError
|
29
|
+
SQLITE_COLUMN_CLASS = NilClass
|
30
|
+
end
|
31
|
+
|
20
32
|
def sql_type
|
21
33
|
options[:sql_type] or begin
|
22
34
|
if native_type?(type)
|
@@ -55,7 +67,7 @@ module HoboFields
|
|
55
67
|
return col_spec.type.in?(synonyms)
|
56
68
|
end
|
57
69
|
end
|
58
|
-
t
|
70
|
+
t == col_spec.type
|
59
71
|
end
|
60
72
|
|
61
73
|
|
@@ -63,9 +75,16 @@ module HoboFields
|
|
63
75
|
!same_type?(col_spec) ||
|
64
76
|
begin
|
65
77
|
check_attributes = [:null, :default]
|
66
|
-
check_attributes += [:precision, :scale] if sql_type == :decimal
|
78
|
+
check_attributes += [:precision, :scale] if sql_type == :decimal && !col_spec.is_a?(SQLITE_COLUMN_CLASS) # remove when rails fixes https://rails.lighthouseapp.com/projects/8994-ruby-on-rails/tickets/2872
|
79
|
+
check_attributes -= [:default] if sql_type == :text && col_spec.is_a?(MYSQL_COLUMN_CLASS)
|
67
80
|
check_attributes << :limit if sql_type.in?([:string, :text, :binary, :integer])
|
68
|
-
check_attributes.any?
|
81
|
+
check_attributes.any? do |k|
|
82
|
+
if k==:default && sql_type==:datetime
|
83
|
+
col_spec.default.try.to_datetime != default.try.to_datetime
|
84
|
+
else
|
85
|
+
col_spec.send(k) != self.send(k)
|
86
|
+
end
|
87
|
+
end
|
69
88
|
end
|
70
89
|
end
|
71
90
|
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module HoboFields
|
2
|
+
|
3
|
+
class IndexSpec
|
4
|
+
|
5
|
+
def initialize(model, fields, options={})
|
6
|
+
@model = model
|
7
|
+
self.table = options.delete(:table_name) || model.table_name
|
8
|
+
self.fields = Array.wrap(fields).*.to_s
|
9
|
+
self.name = options.delete(:name) || model.connection.index_name(self.table, :column => self.fields)
|
10
|
+
self.unique = options.delete(:unique) || false
|
11
|
+
end
|
12
|
+
|
13
|
+
attr_accessor :table, :fields, :name, :unique
|
14
|
+
|
15
|
+
# extract IndexSpecs from an existing table
|
16
|
+
def self.for_model(model, old_table_name=nil)
|
17
|
+
t = old_table_name || model.table_name
|
18
|
+
model.connection.indexes(t).map do |i|
|
19
|
+
self.new(model, i.columns, :name => i.name, :unique => i.unique, :table_name => old_table_name)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def default_name?
|
24
|
+
name == @model.connection.index_name(table, :column => fields)
|
25
|
+
end
|
26
|
+
|
27
|
+
def to_add_statement(new_table_name)
|
28
|
+
r = "add_index :#{new_table_name}, #{fields.*.to_sym.inspect}"
|
29
|
+
r += ", :unique => true" if unique
|
30
|
+
r += ", :name => '#{name}'" unless default_name?
|
31
|
+
r
|
32
|
+
end
|
33
|
+
|
34
|
+
def hash
|
35
|
+
[table, fields, name, unique].hash
|
36
|
+
end
|
37
|
+
|
38
|
+
def ==(v)
|
39
|
+
v.hash == hash
|
40
|
+
end
|
41
|
+
alias_method :eql?, :==
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
@@ -8,7 +8,7 @@ module HoboFields
|
|
8
8
|
@ignore_tables = []
|
9
9
|
|
10
10
|
class << self
|
11
|
-
attr_accessor :ignore_models, :ignore_tables
|
11
|
+
attr_accessor :ignore_models, :ignore_tables, :disable_indexing
|
12
12
|
end
|
13
13
|
|
14
14
|
def self.run(renames={})
|
@@ -63,13 +63,22 @@ module HoboFields
|
|
63
63
|
end
|
64
64
|
def native_types; self.class.native_types; end
|
65
65
|
|
66
|
+
# list habtm join tables
|
67
|
+
def habtm_tables
|
68
|
+
ActiveRecord::Base.send(:subclasses).map do |c|
|
69
|
+
c.reflect_on_all_associations(:has_and_belongs_to_many).map { |a| a.options[:join_table] }
|
70
|
+
end.flatten.compact.*.to_s
|
71
|
+
end
|
72
|
+
|
66
73
|
# Returns an array of model classes and an array of table names
|
67
74
|
# that generation needs to take into account
|
68
75
|
def models_and_tables
|
69
76
|
ignore_model_names = MigrationGenerator.ignore_models.map &it.to_s.underscore
|
70
|
-
|
71
|
-
|
72
|
-
|
77
|
+
all_models = table_model_classes
|
78
|
+
hobo_models = all_models.select { |m| m < HoboFields::ModelExtensions && m.name.underscore.not_in?(ignore_model_names) }
|
79
|
+
non_hobo_models = all_models - hobo_models
|
80
|
+
db_tables = connection.tables - MigrationGenerator.ignore_tables.*.to_s - non_hobo_models.*.table_name - habtm_tables
|
81
|
+
[hobo_models, db_tables]
|
73
82
|
end
|
74
83
|
|
75
84
|
|
@@ -128,6 +137,7 @@ module HoboFields
|
|
128
137
|
|
129
138
|
|
130
139
|
def always_ignore_tables
|
140
|
+
# TODO: figure out how to do this in a sane way and be compatible with 2.2 and 2.3 - class has moved
|
131
141
|
sessions_table = CGI::Session::ActiveRecordStore::Session.table_name if
|
132
142
|
defined?(CGI::Session::ActiveRecordStore::Session) &&
|
133
143
|
defined?(ActionController::Base) &&
|
@@ -200,7 +210,11 @@ module HoboFields
|
|
200
210
|
primary_key_option = ", :primary_key => :#{model.primary_key}" if model.primary_key != "id"
|
201
211
|
(["create_table :#{model.table_name}#{primary_key_option} do |t|"] +
|
202
212
|
model.field_specs.values.sort_by{|f| f.position}.map {|f| create_field(f, longest_field_name)} +
|
203
|
-
["end"]) * "\n"
|
213
|
+
["end"] + (MigrationGenerator.disable_indexing ? [] : create_indexes(model))) * "\n"
|
214
|
+
end
|
215
|
+
|
216
|
+
def create_indexes(model)
|
217
|
+
model.index_specs.map { |i| i.to_add_statement(model.table_name) }
|
204
218
|
end
|
205
219
|
|
206
220
|
def create_field(field_spec, field_name_width)
|
@@ -276,10 +290,36 @@ module HoboFields
|
|
276
290
|
end
|
277
291
|
end.compact
|
278
292
|
|
279
|
-
|
280
|
-
|
293
|
+
index_changes, undo_index_changes = change_indexes(model, current_table_name)
|
294
|
+
|
295
|
+
[(renames + adds + removes + changes + index_changes) * "\n",
|
296
|
+
(undo_renames + undo_adds + undo_removes + undo_changes + undo_index_changes) * "\n"]
|
281
297
|
end
|
282
298
|
|
299
|
+
def change_indexes(model, old_table_name)
|
300
|
+
return [[],[]] if MigrationGenerator.disable_indexing
|
301
|
+
new_table_name = model.table_name
|
302
|
+
existing_indexes = IndexSpec.for_model(model, old_table_name)
|
303
|
+
model_indexes = model.index_specs
|
304
|
+
add_indexes = model_indexes - existing_indexes
|
305
|
+
drop_indexes = existing_indexes - model_indexes
|
306
|
+
undo_add_indexes = []
|
307
|
+
undo_drop_indexes = []
|
308
|
+
add_indexes.map! do |i|
|
309
|
+
undo_add_indexes << drop_index(old_table_name, i.name)
|
310
|
+
i.to_add_statement(new_table_name)
|
311
|
+
end
|
312
|
+
drop_indexes.map! do |i|
|
313
|
+
undo_drop_indexes << i.to_add_statement(old_table_name)
|
314
|
+
drop_index(new_table_name, i.name)
|
315
|
+
end
|
316
|
+
# the order is important here - adding a :unique, for instance needs to remove then add
|
317
|
+
[drop_indexes + add_indexes, undo_add_indexes + undo_drop_indexes]
|
318
|
+
end
|
319
|
+
|
320
|
+
def drop_index(table, name)
|
321
|
+
"remove_index :#{table}, :name => :#{name}"
|
322
|
+
end
|
283
323
|
|
284
324
|
def format_options(options, type, changing=false)
|
285
325
|
options.map do |k, v|
|
@@ -15,16 +15,19 @@ module HoboFields
|
|
15
15
|
# and speeds things up a little.
|
16
16
|
inheriting_cattr_reader :field_specs => HashWithIndifferentAccess.new
|
17
17
|
|
18
|
+
# index_specs holds IndexSpec objects for all the declared indexes.
|
19
|
+
inheriting_cattr_reader :index_specs => []
|
20
|
+
|
18
21
|
def self.inherited(klass)
|
19
22
|
fields do |f|
|
20
23
|
f.field(inheritance_column, :string)
|
21
24
|
end
|
25
|
+
index(inheritance_column) unless index_specs.*.fields.include?([inheritance_column])
|
22
26
|
super
|
23
27
|
end
|
24
28
|
|
25
|
-
|
26
|
-
|
27
|
-
@field_specs ||= HashWithIndifferentAccess.new
|
29
|
+
def self.index(fields, options = {})
|
30
|
+
index_specs << IndexSpec.new(self, fields, options)
|
28
31
|
end
|
29
32
|
|
30
33
|
|
@@ -52,8 +55,9 @@ module HoboFields
|
|
52
55
|
type = HoboFields.to_class(type)
|
53
56
|
attrs.each do |attr|
|
54
57
|
declare_attr_type attr, type, options
|
58
|
+
type_wrapper = attr_type(attr)
|
55
59
|
define_method "#{attr}=" do |val|
|
56
|
-
if !val.is_a?(type) && HoboFields.can_wrap?(type, val)
|
60
|
+
if type_wrapper.not_in?(HoboFields::PLAIN_TYPES.values) && !val.is_a?(type) && HoboFields.can_wrap?(type, val)
|
57
61
|
val = type.new(val.to_s)
|
58
62
|
end
|
59
63
|
instance_variable_set("@#{attr}", val)
|
@@ -68,11 +72,19 @@ module HoboFields
|
|
68
72
|
column_options = {}
|
69
73
|
column_options[:null] = options.delete(:null) if options.has_key?(:null)
|
70
74
|
|
75
|
+
index_options = {}
|
76
|
+
index_options[:name] = options.delete(:index) if options.has_key?(:index)
|
77
|
+
|
71
78
|
returning belongs_to_without_field_declarations(name, options, &block) do
|
72
79
|
refl = reflections[name.to_sym]
|
73
80
|
fkey = refl.primary_key_name
|
74
81
|
declare_field(fkey.to_sym, :integer, column_options)
|
75
|
-
|
82
|
+
if refl.options[:polymorphic]
|
83
|
+
declare_polymorphic_type_field(name, column_options)
|
84
|
+
index(["#{name}_type", fkey], index_options) if index_options[:name]!=false
|
85
|
+
else
|
86
|
+
index(fkey, index_options) if index_options[:name]!=false
|
87
|
+
end
|
76
88
|
end
|
77
89
|
end
|
78
90
|
class << self
|
@@ -110,6 +122,7 @@ module HoboFields
|
|
110
122
|
try.field_added(name, type, args, options)
|
111
123
|
add_formatting_for_field(name, type, args)
|
112
124
|
add_validations_for_field(name, type, args)
|
125
|
+
add_index_for_field(name, args, options)
|
113
126
|
declare_attr_type(name, type, options) unless HoboFields.plain_type?(type)
|
114
127
|
field_specs[name] = FieldSpec.new(self, name, type, options)
|
115
128
|
attr_order << name unless name.in?(attr_order)
|
@@ -120,7 +133,7 @@ module HoboFields
|
|
120
133
|
# field declaration
|
121
134
|
def self.add_validations_for_field(name, type, args)
|
122
135
|
validates_presence_of name if :required.in?(args)
|
123
|
-
validates_uniqueness_of name if :unique.in?(args)
|
136
|
+
validates_uniqueness_of name, :allow_nil => !:required.in?(args) if :unique.in?(args)
|
124
137
|
|
125
138
|
type_class = HoboFields.to_class(type)
|
126
139
|
if type_class && "validate".in?(type_class.public_instance_methods)
|
@@ -131,7 +144,6 @@ module HoboFields
|
|
131
144
|
end
|
132
145
|
end
|
133
146
|
|
134
|
-
|
135
147
|
def self.add_formatting_for_field(name, type, args)
|
136
148
|
type_class = HoboFields.to_class(type)
|
137
149
|
if type_class && "format".in?(type_class.instance_methods)
|
@@ -141,6 +153,16 @@ module HoboFields
|
|
141
153
|
end
|
142
154
|
end
|
143
155
|
|
156
|
+
def self.add_index_for_field(name, args, options)
|
157
|
+
to_name = options.delete(:index)
|
158
|
+
return unless to_name
|
159
|
+
index_opts = {}
|
160
|
+
index_opts[:unique] = :unique.in?(args) || options.delete(:unique)
|
161
|
+
# support :index => true declaration
|
162
|
+
index_opts[:name] = to_name unless to_name == true
|
163
|
+
index(name, index_opts)
|
164
|
+
end
|
165
|
+
|
144
166
|
|
145
167
|
# Extended version of the acts_as_list declaration that
|
146
168
|
# automatically delcares the 'position' field
|
data/lib/hobo_fields.rb
CHANGED
@@ -9,7 +9,7 @@ end
|
|
9
9
|
|
10
10
|
module HoboFields
|
11
11
|
|
12
|
-
VERSION = "0.
|
12
|
+
VERSION = "0.9.0"
|
13
13
|
|
14
14
|
extend self
|
15
15
|
|
@@ -32,7 +32,9 @@ module HoboFields
|
|
32
32
|
# Provide a lookup for these rather than loading them all preemptively
|
33
33
|
|
34
34
|
STANDARD_TYPES = {
|
35
|
+
:raw_html => "RawHtmlString",
|
35
36
|
:html => "HtmlString",
|
37
|
+
:raw_markdown => "RawMarkdownString",
|
36
38
|
:markdown => "MarkdownString",
|
37
39
|
:textile => "TextileString",
|
38
40
|
:password => "PasswordString",
|
@@ -0,0 +1,47 @@
|
|
1
|
+
Description:
|
2
|
+
|
3
|
+
This generator compares your existing schema against the
|
4
|
+
schema declared inside your fields declarations in your
|
5
|
+
models.
|
6
|
+
|
7
|
+
If the generator finds differences, it will display the
|
8
|
+
migration it has created, and ask you if you wish to
|
9
|
+
[g]enerate migration, generate and [m]igrate now or [c]ancel?
|
10
|
+
Enter "g" to just generate the migration but do not run it.
|
11
|
+
Enter "m" to generate the migration and run it, or press "c"
|
12
|
+
to do nothing.
|
13
|
+
|
14
|
+
The generator will then prompt you for the generator name,
|
15
|
+
supplying a numbered default name.
|
16
|
+
|
17
|
+
The generator is conservative and will prompt you to resolve
|
18
|
+
any ambiguities.
|
19
|
+
|
20
|
+
Examples:
|
21
|
+
|
22
|
+
$ ./script/generate hobo_migration
|
23
|
+
|
24
|
+
---------- Up Migration ----------
|
25
|
+
create_table :foos do |t|
|
26
|
+
t.datetime :created_at
|
27
|
+
t.datetime :updated_at
|
28
|
+
end
|
29
|
+
----------------------------------
|
30
|
+
|
31
|
+
---------- Down Migration --------
|
32
|
+
drop_table :foos
|
33
|
+
----------------------------------
|
34
|
+
What now: [g]enerate migration, generate and [m]igrate now or [c]ancel? m
|
35
|
+
|
36
|
+
Migration filename:
|
37
|
+
(you can type spaces instead of '_' -- every little helps)
|
38
|
+
Filename [hobo_migration_2]: create_foo
|
39
|
+
exists db/migrate
|
40
|
+
create db/migrate/20091023183838_create_foo.rb
|
41
|
+
(in /work/foo)
|
42
|
+
== CreateFoo: migrating ======================================================
|
43
|
+
-- create_table(:yos)
|
44
|
+
-> 0.0856s
|
45
|
+
== CreateFoo: migrated (0.0858s) =============================================
|
46
|
+
|
47
|
+
|
@@ -133,8 +133,8 @@ class HoboMigrationGenerator < Rails::Generator::Base
|
|
133
133
|
|
134
134
|
opt.on("--force-drop", "Don't prompt with 'drop or rename' - just drop everything") { |v| options[:force_drop] = true }
|
135
135
|
opt.on("--default-name", "Dont' prompt for a migration name - just pick one") { |v| options[:default_name] = true }
|
136
|
-
opt.on("--generate", "
|
137
|
-
opt.on("--migrate", "
|
136
|
+
opt.on("--generate", "Don't prompt for action - generate the migration") { |v| options[:action] = 'g' }
|
137
|
+
opt.on("--migrate", "Don't prompt for action - generate and migrate") { |v| options[:action] = 'm' }
|
138
138
|
end
|
139
139
|
|
140
140
|
|
@@ -33,6 +33,6 @@ class HobofieldModelGenerator < Rails::Generator::NamedBase
|
|
33
33
|
opt.on("--skip-timestamps",
|
34
34
|
"Don't add timestamps to the migration file for this model") { |v| options[:skip_timestamps] = v }
|
35
35
|
opt.on("--skip-fixture",
|
36
|
-
"Don't
|
36
|
+
"Don't generate a fixture file for this model") { |v| options[:skip_fixture] = v}
|
37
37
|
end
|
38
38
|
end
|
data/test/hobofields.rdoctest
CHANGED
@@ -16,12 +16,17 @@ Our test requires rails:
|
|
16
16
|
We need a database connection for this test:
|
17
17
|
{.hidden}
|
18
18
|
|
19
|
+
>> mysql_adapter = defined?(JRUBY_VERSION) ? 'jdbcmysql' : 'mysql'
|
20
|
+
>> mysql_user = 'root'; mysql_password = ''
|
21
|
+
>> mysql_login = "-u #{mysql_user} --password='#{mysql_password}'"
|
19
22
|
>> mysql_database = "hobofields_doctest"
|
20
|
-
>> system "mysqladmin --force drop #{mysql_database} 2> /dev/null"
|
21
|
-
>> system("mysqladmin create #{mysql_database}") or raise "could not create database"
|
22
|
-
>> ActiveRecord::Base.establish_connection(:adapter =>
|
23
|
+
>> system "mysqladmin #{mysql_login} --force drop #{mysql_database} 2> /dev/null"
|
24
|
+
>> system("mysqladmin #{mysql_login} create #{mysql_database}") or raise "could not create database"
|
25
|
+
>> ActiveRecord::Base.establish_connection(:adapter => mysql_adapter,
|
23
26
|
:database => mysql_database,
|
24
|
-
:host => "localhost"
|
27
|
+
:host => "localhost",
|
28
|
+
:username => mysql_user,
|
29
|
+
:password => mysql_password)
|
25
30
|
{.hidden}
|
26
31
|
|
27
32
|
Some load path manipulation you shouldn't need:
|
@@ -225,14 +230,14 @@ Rich types can define there own validations by a `#validate` method. It should r
|
|
225
230
|
>> a.contact_address.class
|
226
231
|
=> HoboFields::EmailAddress
|
227
232
|
>> a.contact_address.validate
|
228
|
-
=> "is
|
233
|
+
=> "is invalid"
|
229
234
|
|
230
235
|
But normally that method would be called for us during validation:
|
231
236
|
|
232
237
|
>> a.valid?
|
233
238
|
=> false
|
234
239
|
>> a.errors.full_messages
|
235
|
-
=> ["Contact address is
|
240
|
+
=> ["Contact address is invalid"]
|
236
241
|
>> a.contact_address = "me@me.com"
|
237
242
|
>> a.valid?
|
238
243
|
=> true
|
@@ -266,5 +271,5 @@ To have them validated use `validate_virtual_field`:
|
|
266
271
|
Cleanup
|
267
272
|
{.hidden}
|
268
273
|
|
269
|
-
>> system "mysqladmin --force drop #{mysql_database} 2> /dev/null"
|
274
|
+
>> system "mysqladmin #{mysql_login} --force drop #{mysql_database} 2> /dev/null"
|
270
275
|
{.hidden}
|
@@ -14,12 +14,17 @@ Firstly, in order to test the migration generator outside of a full Rails stack,
|
|
14
14
|
We also need to get ActiveRecord set up with a database connection
|
15
15
|
{.hidden}
|
16
16
|
|
17
|
+
>> mysql_adapter = defined?(JRUBY_VERSION) ? 'jdbcmysql' : 'mysql'
|
18
|
+
>> mysql_user = 'root'; mysql_password = ''
|
19
|
+
>> mysql_login = "-u #{mysql_user} --password='#{mysql_password}'"
|
17
20
|
>> mysql_database = "hobofields_doctest"
|
18
|
-
>> system "mysqladmin --force drop #{mysql_database} 2> /dev/null"
|
19
|
-
>> system("mysqladmin create #{mysql_database}") or raise "could not create database"
|
20
|
-
>> ActiveRecord::Base.establish_connection(:adapter =>
|
21
|
+
>> system "mysqladmin #{mysql_login} --force drop #{mysql_database} 2> /dev/null"
|
22
|
+
>> system("mysqladmin #{mysql_login} create #{mysql_database}") or raise "could not create database"
|
23
|
+
>> ActiveRecord::Base.establish_connection(:adapter => mysql_adapter,
|
21
24
|
:database => mysql_database,
|
22
|
-
:host => "localhost"
|
25
|
+
:host => "localhost",
|
26
|
+
:username => mysql_user,
|
27
|
+
:password => mysql_password)
|
23
28
|
{.hidden}
|
24
29
|
|
25
30
|
Some load path manipulation you shouldn't need:
|
@@ -242,13 +247,16 @@ Note that limit on a decimal column is ignored (use :scale and :precision)
|
|
242
247
|
=> "add_column :adverts, :price, :decimal"
|
243
248
|
|
244
249
|
Cleanup
|
250
|
+
{.hidden}
|
245
251
|
|
246
252
|
>> Advert.field_specs.delete :price
|
253
|
+
{.hidden}
|
247
254
|
|
248
255
|
|
249
256
|
### Foreign Keys
|
250
257
|
|
251
|
-
HoboFields extends the `belongs_to` macro so that it also declares the
|
258
|
+
HoboFields extends the `belongs_to` macro so that it also declares the
|
259
|
+
foreign-key field. It also generates an index on the field.
|
252
260
|
|
253
261
|
>>
|
254
262
|
class Advert
|
@@ -256,13 +264,20 @@ HoboFields extends the `belongs_to` macro so that it also declares the foreign-k
|
|
256
264
|
end
|
257
265
|
>> up, down = HoboFields::MigrationGenerator.run
|
258
266
|
>> up
|
259
|
-
=>
|
267
|
+
=>
|
268
|
+
"add_column :adverts, :category_id, :integer
|
269
|
+
add_index :adverts, [:category_id]"
|
260
270
|
>> down
|
261
|
-
=>
|
271
|
+
=>
|
272
|
+
"remove_column :adverts, :category_id
|
273
|
+
remove_index :adverts, :name => :index_adverts_on_category_id"
|
262
274
|
|
263
275
|
Cleanup:
|
276
|
+
{.hidden}
|
264
277
|
|
265
278
|
>> Advert.field_specs.delete(:category_id)
|
279
|
+
>> Advert.index_specs.delete_if {|spec| spec.fields==["category_id"]}
|
280
|
+
{.hidden}
|
266
281
|
|
267
282
|
If you specify a custom foreign key, the migration generator observes that:
|
268
283
|
|
@@ -272,14 +287,52 @@ If you specify a custom foreign key, the migration generator observes that:
|
|
272
287
|
end
|
273
288
|
>> up, down = HoboFields::MigrationGenerator.run
|
274
289
|
>> up
|
275
|
-
=>
|
276
|
-
|
277
|
-
|
290
|
+
=>
|
291
|
+
"add_column :adverts, :c_id, :integer
|
292
|
+
add_index :adverts, [:c_id]"
|
278
293
|
|
279
294
|
Cleanup:
|
295
|
+
{.hidden}
|
280
296
|
|
281
297
|
>> Advert.field_specs.delete(:c_id)
|
298
|
+
>> Advert.index_specs.delete_if {|spec| spec.fields==["c_id"]}
|
299
|
+
{.hidden}
|
300
|
+
|
301
|
+
You can avoid generating the index by specifying `:index => false`
|
302
|
+
|
303
|
+
>>
|
304
|
+
class Advert
|
305
|
+
belongs_to :category, :index => false
|
306
|
+
end
|
307
|
+
>> up, down = HoboFields::MigrationGenerator.run
|
308
|
+
>> up
|
309
|
+
=> "add_column :adverts, :category_id, :integer"
|
310
|
+
|
311
|
+
Cleanup:
|
312
|
+
{.hidden}
|
313
|
+
|
314
|
+
>> Advert.field_specs.delete(:category_id)
|
315
|
+
>> Advert.index_specs.delete_if {|spec| spec.fields==["category_id"]}
|
316
|
+
{.hidden}
|
282
317
|
|
318
|
+
You can specify the index name with :index
|
319
|
+
|
320
|
+
>>
|
321
|
+
class Advert
|
322
|
+
belongs_to :category, :index => 'my_index'
|
323
|
+
end
|
324
|
+
>> up, down = HoboFields::MigrationGenerator.run
|
325
|
+
>> up
|
326
|
+
=>
|
327
|
+
"add_column :adverts, :category_id, :integer
|
328
|
+
add_index :adverts, [:category_id], :name => 'my_index'"
|
329
|
+
|
330
|
+
Cleanup:
|
331
|
+
{.hidden}
|
332
|
+
|
333
|
+
>> Advert.field_specs.delete(:category_id)
|
334
|
+
>> Advert.index_specs.delete_if {|spec| spec.fields==["category_id"]}
|
335
|
+
{.hidden}
|
283
336
|
|
284
337
|
### Timestamps
|
285
338
|
|
@@ -303,10 +356,115 @@ Cleanup:
|
|
303
356
|
>>
|
304
357
|
|
305
358
|
Cleanup:
|
359
|
+
{.hidden}
|
306
360
|
|
307
361
|
>> Advert.field_specs.delete(:updated_at)
|
308
362
|
>> Advert.field_specs.delete(:created_at)
|
363
|
+
{.hidden}
|
364
|
+
|
365
|
+
### Indices
|
366
|
+
|
367
|
+
You can add an index to a field definition
|
368
|
+
|
369
|
+
>>
|
370
|
+
class Advert
|
371
|
+
fields do
|
372
|
+
title :string, :index => true
|
373
|
+
end
|
374
|
+
end
|
375
|
+
>> up, down = HoboFields::MigrationGenerator.run
|
376
|
+
>> up.split("\n")[1]
|
377
|
+
=> 'add_index :adverts, [:title]'
|
378
|
+
|
379
|
+
Cleanup:
|
380
|
+
{.hidden}
|
381
|
+
|
382
|
+
>> Advert.index_specs.delete_if {|spec| spec.fields==["title"]}
|
383
|
+
{.hidden}
|
384
|
+
|
385
|
+
You can ask for a unique index
|
386
|
+
|
387
|
+
>>
|
388
|
+
class Advert
|
389
|
+
fields do
|
390
|
+
title :string, :index => true, :unique => true
|
391
|
+
end
|
392
|
+
end
|
393
|
+
>> up, down = HoboFields::MigrationGenerator.run
|
394
|
+
>> up.split("\n")[1]
|
395
|
+
=> 'add_index :adverts, [:title], :unique => true'
|
396
|
+
|
397
|
+
Cleanup:
|
398
|
+
{.hidden}
|
399
|
+
|
400
|
+
>> Advert.index_specs.delete_if {|spec| spec.fields==["title"]}
|
401
|
+
{.hidden}
|
402
|
+
|
403
|
+
You can specify the name for the index
|
404
|
+
|
405
|
+
>>
|
406
|
+
class Advert
|
407
|
+
fields do
|
408
|
+
title :string, :index => 'my_index'
|
409
|
+
end
|
410
|
+
end
|
411
|
+
>> up, down = HoboFields::MigrationGenerator.run
|
412
|
+
>> up.split("\n")[1]
|
413
|
+
=> "add_index :adverts, [:title], :name => 'my_index'"
|
414
|
+
|
415
|
+
Cleanup:
|
416
|
+
{.hidden}
|
417
|
+
|
418
|
+
>> Advert.index_specs.delete_if {|spec| spec.fields==["title"]}
|
419
|
+
{.hidden}
|
420
|
+
|
421
|
+
You can ask for an index outside of the fields block
|
422
|
+
|
423
|
+
>>
|
424
|
+
class Advert
|
425
|
+
index :title
|
426
|
+
end
|
427
|
+
>> up, down = HoboFields::MigrationGenerator.run
|
428
|
+
>> up.split("\n")[1]
|
429
|
+
=> "add_index :adverts, [:title]"
|
430
|
+
|
431
|
+
Cleanup:
|
432
|
+
{.hidden}
|
433
|
+
|
434
|
+
>> Advert.index_specs.delete_if {|spec| spec.fields==["title"]}
|
435
|
+
{.hidden}
|
436
|
+
|
437
|
+
The available options for the index function are `:unique` and `:name`
|
438
|
+
|
439
|
+
>>
|
440
|
+
class Advert
|
441
|
+
index :title, :unique => true, :name => 'my_index'
|
442
|
+
end
|
443
|
+
>> up, down = HoboFields::MigrationGenerator.run
|
444
|
+
>> up.split("\n")[1]
|
445
|
+
=> "add_index :adverts, [:title], :unique => true, :name => 'my_index'"
|
446
|
+
|
447
|
+
Cleanup:
|
448
|
+
{.hidden}
|
449
|
+
|
450
|
+
>> Advert.index_specs.delete_if {|spec| spec.fields==["title"]}
|
451
|
+
{.hidden}
|
452
|
+
|
453
|
+
You can create an index on more than one field
|
454
|
+
|
455
|
+
>>
|
456
|
+
class Advert
|
457
|
+
index [:title, :category_id]
|
458
|
+
end
|
459
|
+
>> up, down = HoboFields::MigrationGenerator.run
|
460
|
+
>> up.split("\n")[1]
|
461
|
+
=> "add_index :adverts, [:title, :category_id]"
|
462
|
+
|
463
|
+
Cleanup:
|
464
|
+
{.hidden}
|
309
465
|
|
466
|
+
>> Advert.index_specs.delete_if {|spec| spec.fields==["title", "category_id"]}
|
467
|
+
{.hidden}
|
310
468
|
|
311
469
|
### Rename a table
|
312
470
|
|
@@ -332,27 +490,18 @@ Set the table name back to what it should be and confirm we're in sync:
|
|
332
490
|
>> HoboFields::MigrationGenerator.run
|
333
491
|
=> ["", ""]
|
334
492
|
|
335
|
-
### Drop a table
|
336
|
-
|
337
|
-
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.
|
338
|
-
|
339
|
-
>> HoboFields::MigrationGenerator.ignore_models = [ :advert ]
|
340
|
-
|
341
|
-
Dropping tables is where the automatic down-migration really comes in handy:
|
342
|
-
|
343
|
-
>> up, down = HoboFields::MigrationGenerator.run
|
344
|
-
>> up
|
345
|
-
=> "drop_table :adverts"
|
346
|
-
>> down
|
347
|
-
=>
|
348
|
-
"create_table "adverts", :force => true do |t|
|
349
|
-
t.text "body"
|
350
|
-
t.string "title", :default => "Untitled"
|
351
|
-
end"
|
352
|
-
|
353
493
|
### Rename a table
|
354
494
|
|
355
|
-
As with renaming columns, we have to tell the migration generator about the rename. Here we create a new class 'Advertisement'
|
495
|
+
As with renaming columns, we have to tell the migration generator about the rename. Here we create a new class 'Advertisement', and tell ActiveRecord to forget about the Advert class. This requires code that shouldn't be shown to impressionable children.
|
496
|
+
{.hidden}
|
497
|
+
|
498
|
+
>>
|
499
|
+
def nuke_model_class(klass)
|
500
|
+
ActiveRecord::Base.instance_eval { class_variable_get('@@subclasses')[klass.superclass].delete(klass) }
|
501
|
+
Object.instance_eval { remove_const klass.name.to_sym }
|
502
|
+
end
|
503
|
+
>> nuke_model_class(Advert)
|
504
|
+
{.hidden}
|
356
505
|
|
357
506
|
>>
|
358
507
|
class Advertisement < ActiveRecord::Base
|
@@ -367,9 +516,24 @@ As with renaming columns, we have to tell the migration generator about the rena
|
|
367
516
|
>> down
|
368
517
|
=> "rename_table :advertisements, :adverts"
|
369
518
|
|
370
|
-
|
519
|
+
### Drop a table
|
520
|
+
|
521
|
+
>> nuke_model_class(Advertisement)
|
522
|
+
{.hidden}
|
523
|
+
|
524
|
+
If you delete a model, the migration generator will create a `drop_table` migration.
|
371
525
|
|
372
|
-
|
526
|
+
Dropping tables is where the automatic down-migration really comes in handy:
|
527
|
+
|
528
|
+
>> up, down = HoboFields::MigrationGenerator.run
|
529
|
+
>> up
|
530
|
+
=> "drop_table :adverts"
|
531
|
+
>> down
|
532
|
+
=>
|
533
|
+
"create_table "adverts", :force => true do |t|
|
534
|
+
t.text "body"
|
535
|
+
t.string "title", :default => "Untitled"
|
536
|
+
end"
|
373
537
|
|
374
538
|
## STI
|
375
539
|
|
@@ -378,19 +542,34 @@ Now that we've seen the renaming we'll switch the 'ignore' setting to ignore tha
|
|
378
542
|
Adding a subclass or two should introduce the 'type' column and no other changes
|
379
543
|
|
380
544
|
>>
|
545
|
+
class Advert < ActiveRecord::Base
|
546
|
+
fields do
|
547
|
+
body :text
|
548
|
+
title :string, :default => "Untitled"
|
549
|
+
end
|
550
|
+
end
|
381
551
|
class FancyAdvert < Advert
|
382
552
|
end
|
383
553
|
class SuperFancyAdvert < FancyAdvert
|
384
554
|
end
|
385
555
|
>> up, down = HoboFields::MigrationGenerator.run
|
386
556
|
>> up
|
387
|
-
=>
|
557
|
+
=>
|
558
|
+
"add_column :adverts, :type, :string
|
559
|
+
add_index :adverts, [:type]"
|
388
560
|
>> down
|
389
|
-
=>
|
561
|
+
=>
|
562
|
+
"remove_column :adverts, :type
|
563
|
+
remove_index :adverts, :name => :index_adverts_on_type"
|
390
564
|
|
391
565
|
Cleanup
|
566
|
+
{.hidden}
|
392
567
|
|
393
568
|
>> Advert.field_specs.delete(:type)
|
569
|
+
>> nuke_model_class(SuperFancyAdvert)
|
570
|
+
>> nuke_model_class(FancyAdvert)
|
571
|
+
>> Advert.index_specs.delete_if {|spec| spec.fields==["type"]}
|
572
|
+
{.hidden}
|
394
573
|
|
395
574
|
|
396
575
|
## Coping with multiple changes
|
@@ -430,7 +609,9 @@ First let's confirm we're in a known state. One model, 'Advert', with a string '
|
|
430
609
|
|
431
610
|
### Rename a table and add a column
|
432
611
|
|
433
|
-
>>
|
612
|
+
>> nuke_model_class(Advert)
|
613
|
+
{.hidden}
|
614
|
+
|
434
615
|
>>
|
435
616
|
class Ad < ActiveRecord::Base
|
436
617
|
fields do
|
@@ -445,7 +626,15 @@ First let's confirm we're in a known state. One model, 'Advert', with a string '
|
|
445
626
|
"rename_table :adverts, :ads
|
446
627
|
|
447
628
|
add_column :ads, :created_at, :datetime"
|
629
|
+
|
448
630
|
>>
|
631
|
+
class Advert < ActiveRecord::Base
|
632
|
+
fields do
|
633
|
+
body :text
|
634
|
+
title :string, :default => "Untitled"
|
635
|
+
end
|
636
|
+
end
|
637
|
+
{.hidden}
|
449
638
|
|
450
639
|
## Legacy Keys
|
451
640
|
|
@@ -468,5 +657,5 @@ HoboFields has some support for legacy keys.
|
|
468
657
|
Cleanup
|
469
658
|
{.hidden}
|
470
659
|
|
471
|
-
>> system "mysqladmin --force drop #{mysql_database} 2> /dev/null"
|
660
|
+
>> system "mysqladmin #{mysql_login} --force drop #{mysql_database} 2> /dev/null"
|
472
661
|
{.hidden}
|
@@ -12,12 +12,17 @@ Firstly, in order to test the migration generator outside of a full Rails stack,
|
|
12
12
|
|
13
13
|
We also need to get ActiveRecord set up with a database connection
|
14
14
|
|
15
|
+
>> mysql_adapter = defined?(JRUBY_VERSION) ? 'jdbcmysql' : 'mysql'
|
16
|
+
>> mysql_user = 'root'; mysql_password = ''
|
17
|
+
>> mysql_login = "-u #{mysql_user} --password='#{mysql_password}'"
|
15
18
|
>> mysql_database = "hobofields_doctest"
|
16
|
-
>> system "mysqladmin --force drop #{mysql_database} 2> /dev/null"
|
17
|
-
>> system("mysqladmin create #{mysql_database}") or raise "could not create database"
|
18
|
-
>> ActiveRecord::Base.establish_connection(:adapter =>
|
19
|
+
>> system "mysqladmin #{mysql_login} --force drop #{mysql_database} 2> /dev/null"
|
20
|
+
>> system("mysqladmin #{mysql_login} create #{mysql_database}") or raise "could not create database"
|
21
|
+
>> ActiveRecord::Base.establish_connection(:adapter => mysql_adapter,
|
19
22
|
:database => mysql_database,
|
20
|
-
:host => "localhost"
|
23
|
+
:host => "localhost",
|
24
|
+
:username => mysql_user,
|
25
|
+
:password => mysql_password)
|
21
26
|
|
22
27
|
Some load path manipulation you shouldn't need:
|
23
28
|
|
@@ -91,4 +96,4 @@ We'll define a method to make that easier next time
|
|
91
96
|
|
92
97
|
## Cleanup
|
93
98
|
|
94
|
-
>> system "mysqladmin --force drop #{mysql_database} 2> /dev/null"
|
99
|
+
>> system "mysqladmin #{mysql_login} --force drop #{mysql_database} 2> /dev/null"
|
data/test/rich_types.rdoctest
CHANGED
@@ -17,12 +17,15 @@ ActiveRecord 2.3.2 doesn't work very well without a connection, even
|
|
17
17
|
though we don't need it for this test:
|
18
18
|
{.hidden}
|
19
19
|
|
20
|
+
>> mysql_adapter = defined?(JRUBY_VERSION) ? 'jdbcmysql' : 'mysql'
|
21
|
+
>> mysql_user = 'root'; mysql_password = ''
|
22
|
+
>> mysql_login = "-u #{mysql_user} --password='#{mysql_password}'"
|
20
23
|
>> mysql_database = "hobofields_doctest"
|
21
|
-
>> system "mysqladmin --force drop #{mysql_database} 2> /dev/null"
|
22
|
-
>> system("mysqladmin create #{mysql_database}") or raise "could not create database"
|
24
|
+
>> system "mysqladmin #{mysql_login} --force drop #{mysql_database} 2> /dev/null"
|
25
|
+
>> system("mysqladmin #{mysql_login} create #{mysql_database}") or raise "could not create database"
|
23
26
|
>> ActiveRecord::Base.establish_connection(:adapter => "mysql",
|
24
|
-
|
25
|
-
|
27
|
+
:database => mysql_database,
|
28
|
+
:host => "localhost", :user => mysql_user, :password => mysql_password)
|
26
29
|
{.hidden}
|
27
30
|
|
28
31
|
Some load path manipulation you shouldn't need:
|
@@ -100,7 +103,7 @@ Provides validation of correct email address format.
|
|
100
103
|
>> !!bad.valid?
|
101
104
|
=> false
|
102
105
|
>> bad.validate
|
103
|
-
=> "is
|
106
|
+
=> "is invalid"
|
104
107
|
|
105
108
|
### `HoboFields::HtmlString`
|
106
109
|
|
@@ -234,23 +237,6 @@ Sometimes it's nice to have a proper type name. Here's one way you might go abou
|
|
234
237
|
## Cleanup
|
235
238
|
{.hidden}
|
236
239
|
|
237
|
-
>> system "mysqladmin --force drop #{mysql_database} 2> /dev/null"
|
240
|
+
>> system "mysqladmin #{mysql_login} --force drop #{mysql_database} 2> /dev/null"
|
238
241
|
{.hidden}
|
239
242
|
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: hobofields
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.9.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Tom Locke
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2009-
|
12
|
+
date: 2009-11-17 00:00:00 +00:00
|
13
13
|
default_executable:
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
@@ -30,7 +30,7 @@ dependencies:
|
|
30
30
|
requirements:
|
31
31
|
- - "="
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: 0.
|
33
|
+
version: 0.9.0
|
34
34
|
version:
|
35
35
|
description:
|
36
36
|
email: tom@tomlocke.com
|
@@ -54,6 +54,7 @@ files:
|
|
54
54
|
- lib/hobo_fields/field_spec.rb
|
55
55
|
- lib/hobo_fields/fields_declaration.rb
|
56
56
|
- lib/hobo_fields/html_string.rb
|
57
|
+
- lib/hobo_fields/index_spec.rb
|
57
58
|
- lib/hobo_fields/markdown_string.rb
|
58
59
|
- lib/hobo_fields/migration_generator.rb
|
59
60
|
- lib/hobo_fields/model_extensions.rb
|
@@ -65,6 +66,7 @@ files:
|
|
65
66
|
- lib/hobo_fields/text.rb
|
66
67
|
- lib/hobo_fields/textile_string.rb
|
67
68
|
- lib/hobofields.rb
|
69
|
+
- rails_generators/hobo_migration/USAGE
|
68
70
|
- rails_generators/hobo_migration/hobo_migration_generator.rb
|
69
71
|
- rails_generators/hobo_migration/templates/migration.rb
|
70
72
|
- rails_generators/hobofield_model/USAGE
|
@@ -104,7 +106,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
104
106
|
version:
|
105
107
|
requirements: []
|
106
108
|
|
107
|
-
rubyforge_project:
|
109
|
+
rubyforge_project: hobo
|
108
110
|
rubygems_version: 1.3.5
|
109
111
|
signing_key:
|
110
112
|
specification_version: 3
|