hobofields 0.8.10 → 0.9.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.
- 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
|