hobofields 0.7.5 → 0.8

Sign up to get free protection for your applications and to get access to all the features.
data/lib/hobo_fields.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  require 'hobosupport'
2
2
 
3
- Dependencies.load_paths |= [ File.dirname(__FILE__) ] if defined?(Dependencies)
3
+ Dependencies.load_paths |= [ File.dirname(__FILE__) ]
4
4
 
5
5
  module Hobo
6
6
  # Empty class to represent the boolean type.
@@ -8,21 +8,22 @@ module Hobo
8
8
  end
9
9
 
10
10
  module HoboFields
11
-
12
- VERSION = "0.7.5"
13
-
11
+
12
+ VERSION = "0.8"
13
+
14
14
  extend self
15
-
16
- PLAIN_TYPES = {
15
+
16
+ PLAIN_TYPES = {
17
17
  :boolean => Hobo::Boolean,
18
18
  :date => Date,
19
- :datetime => Time,
19
+ :datetime => (defined?(ActiveSupport::TimeWithZone) ? ActiveSupport::TimeWithZone : Time),
20
20
  :integer => Fixnum,
21
21
  :big_integer => BigDecimal,
22
+ :decimal => BigDecimal,
22
23
  :float => Float,
23
24
  :string => String
24
25
  }
25
-
26
+
26
27
  # Provide a lookup for these rather than loading them all preemptively
27
28
  STANDARD_TYPES = {
28
29
  :html => "HtmlString",
@@ -32,12 +33,12 @@ module HoboFields
32
33
  :text => "Text",
33
34
  :email_address => "EmailAddress"
34
35
  }
35
-
36
+
36
37
  @field_types = HashWithIndifferentAccess.new(PLAIN_TYPES)
37
38
  @never_wrap_types = Set.new([NilClass, Hobo::Boolean, TrueClass, FalseClass])
38
39
 
39
40
  attr_reader :field_types
40
-
41
+
41
42
  def to_class(type)
42
43
  if type.is_a?(Symbol, String)
43
44
  type = type.to_sym
@@ -46,51 +47,51 @@ module HoboFields
46
47
  type # assume it's already a class
47
48
  end
48
49
  end
49
-
50
-
50
+
51
+
51
52
  def to_name(type)
52
53
  field_types.index(type)
53
54
  end
54
55
 
55
-
56
+
56
57
  def can_wrap?(val)
57
58
  # Make sure we get the *real* class
58
59
  klass = Object.instance_method(:class).bind(val).call
59
60
  !@never_wrap_types.any? { |c| klass <= c }
60
61
  end
61
-
62
-
62
+
63
+
63
64
  def never_wrap(type)
64
65
  @never_wrap_types << type
65
66
  end
66
-
67
-
67
+
68
+
68
69
  def register_type(name, klass)
69
70
  field_types[name] = klass
70
71
  end
71
-
72
-
72
+
73
+
73
74
  def plain_type?(type_name)
74
75
  type_name.in?(PLAIN_TYPES)
75
76
  end
76
-
77
-
77
+
78
+
78
79
  def standard_class(name)
79
80
  class_name = STANDARD_TYPES[name]
80
81
  "HoboFields::#{class_name}".constantize if class_name
81
82
  end
82
-
83
+
83
84
  def enable
84
85
  require "hobo_fields/enum_string"
85
86
  require "hobo_fields/fields_declaration"
86
87
 
87
88
  # Add the fields do declaration to ActiveRecord::Base
88
89
  ActiveRecord::Base.send(:include, HoboFields::FieldsDeclaration)
89
-
90
+
90
91
  # Monkey patch ActiveRecord so that the attribute read & write methods
91
92
  # automatically wrap richly-typed fields.
92
- ActiveRecord::AttributeMethods::ClassMethods.class_eval do
93
-
93
+ ActiveRecord::AttributeMethods::ClassMethods.class_eval do
94
+
94
95
  # Define an attribute reader method. Cope with nil column.
95
96
  def define_read_method(symbol, attr_name, column)
96
97
  cast_code = column.type_cast_code('v') if column
@@ -110,11 +111,11 @@ module HoboFields
110
111
  else
111
112
  access_code
112
113
  end
113
-
114
- evaluate_attribute_method(attr_name,
114
+
115
+ evaluate_attribute_method(attr_name,
115
116
  "def #{symbol}; @attributes_cache['#{attr_name}'] ||= begin; #{src}; end; end")
116
117
  end
117
-
118
+
118
119
  def define_write_method(attr_name)
119
120
  src = if connected? && (type_wrapper = try.attr_type(attr_name)) &&
120
121
  type_wrapper.is_a?(Class) && type_wrapper.not_in?(HoboFields::PLAIN_TYPES.values)
@@ -125,14 +126,14 @@ module HoboFields
125
126
  end
126
127
  evaluate_attribute_method(attr_name,
127
128
  "def #{attr_name}=(val); write_attribute('#{attr_name}', #{src});end", "#{attr_name}=")
128
-
129
+
129
130
  end
130
131
 
131
132
  end
132
-
133
-
133
+
134
+
134
135
  end
135
-
136
+
136
137
  end
137
138
 
138
139
 
@@ -1,3 +1,4 @@
1
+ require File.dirname(__FILE__) + '/../../lib/hobofields'
1
2
  class HoboMigrationGenerator < Rails::Generator::Base
2
3
 
3
4
  def initialize(runtime_args, runtime_options = {})
@@ -7,46 +8,63 @@ class HoboMigrationGenerator < Rails::Generator::Base
7
8
  "hobo_migration_#{i+1}"
8
9
  end
9
10
  end
10
-
11
+
12
+ def migrations_pending?
13
+ pending_migrations = ActiveRecord::Migrator.new(:up, 'db/migrate').pending_migrations
14
+
15
+ if pending_migrations.any?
16
+ puts "You have #{pending_migrations.size} pending migration#{'s' if pending_migrations.size > 1}:"
17
+ pending_migrations.each do |pending_migration|
18
+ puts ' %4d %s' % [pending_migration.version, pending_migration.name]
19
+ end
20
+ true
21
+ else
22
+ false
23
+ end
24
+ end
25
+
26
+
11
27
  def manifest
28
+ return record {|m| } if migrations_pending?
29
+
12
30
  generator = HoboFields::MigrationGenerator.new(self)
13
31
  up, down = generator.generate
14
32
 
15
33
  if up.blank?
16
34
  puts "Database and models match -- nothing to change"
17
- return record {|m| }
35
+ return record {|m| }
18
36
  end
19
-
37
+
20
38
  puts "\n---------- Up Migration ----------", up, "----------------------------------"
21
39
  puts "\n---------- Down Migration --------", down, "----------------------------------"
22
-
23
- action = input("What now: [g]enerate migration, generate and [m]igrate now or [c]ancel?", %w(g m c))
40
+
41
+ action = input("What now: [g]enerate migration, generate and [m]igrate now or [c]ancel?", /^(g|m|c)$/)
24
42
 
25
43
  if action == 'c'
26
44
  # record nothing to keep the generator happy
27
45
  record {|m| }
28
46
  else
29
47
  puts "\nMigration filename:", "(you can type spaces instead of '_' -- every little helps)"
30
- migration_name = input("Filename [#@migration_name]:").strip.gsub(' ', '_')
48
+ migration_name = input("Filename [#@migration_name]:", /^[a-z0-9_ ]/).strip.gsub(' ', '_')
31
49
  migration_name = @migration_name if migration_name.blank?
32
-
33
- at_exit { system "rake db:migrate" } if action == 'm'
34
-
50
+
51
+ at_exit { rake_migrate } if action == 'm'
52
+
35
53
  up.gsub!("\n", "\n ")
36
54
  down.gsub!("\n", "\n ")
37
55
 
38
56
  record do |m|
39
- m.migration_template 'migration.rb', 'db/migrate',
40
- :assigns => { :up => up, :down => down, :migration_name => migration_name.camelize },
57
+ m.migration_template 'migration.rb', 'db/migrate',
58
+ :assigns => { :up => up, :down => down, :migration_name => migration_name.camelize },
41
59
  :migration_file_name => migration_name
42
60
  end
43
61
  end
44
62
  rescue HoboFields::FieldSpec::UnknownSqlTypeError => e
45
- puts "Invalid field type '#{e.message[2]}' for #{e.message[0]}.#{e.message[1]}"
63
+ puts "Invalid field type: #{e}"
46
64
  record {|m| }
47
65
  end
48
-
49
-
66
+
67
+
50
68
  def extract_renames!(to_create, to_drop, kind_str, name_prefix="")
51
69
  to_rename = {}
52
70
  rename_to_choices = to_create
@@ -62,7 +80,7 @@ class HoboMigrationGenerator < Rails::Generator::Base
62
80
  puts "Rename choices: #{to_create * ', '}"
63
81
  resp = input("Enter either 'drop #{t}' or one of the rename choices:")
64
82
  resp.strip!
65
-
83
+
66
84
  if resp == "drop " + t
67
85
  # Leave things as they are
68
86
  else
@@ -78,11 +96,11 @@ class HoboMigrationGenerator < Rails::Generator::Base
78
96
  to_rename
79
97
  end
80
98
 
81
-
82
- def input(prompt, options=nil)
99
+
100
+ def input(prompt, format=nil)
83
101
  print(prompt + " ")
84
- if options
85
- while !(response = STDIN.readline.strip.downcase).in?(options);
102
+ if format
103
+ while (response = STDIN.readline.strip.downcase) !~ format
86
104
  print(prompt + " ")
87
105
  end
88
106
  response
@@ -90,6 +108,14 @@ class HoboMigrationGenerator < Rails::Generator::Base
90
108
  STDIN.readline
91
109
  end
92
110
  end
93
-
111
+
112
+ def rake_migrate
113
+ if RUBY_PLATFORM =~ /mswin32/
114
+ system "rake.bat db:migrate"
115
+ else
116
+ system "rake db:migrate"
117
+ end
118
+ end
119
+
94
120
  end
95
121
 
@@ -0,0 +1,29 @@
1
+ Description:
2
+ Stubs out a new model. Pass the model name, either CamelCased or
3
+ under_scored, and an optional list of attribute pairs as arguments.
4
+
5
+ Attribute pairs are column_name:sql_type arguments specifying the
6
+ model's attributes. Timestamps are added by default, so you don't have to
7
+ specify them by hand as 'created_at:datetime updated_at:datetime'.
8
+
9
+ You don't have to think up every attribute up front, but it helps to
10
+ sketch out a few so you can start working with the model immediately.
11
+
12
+ This generates a model class in app/models, a unit test in test/unit,
13
+ a test fixture in test/fixtures/singular_name.yml, and a migration in
14
+ db/migrate.
15
+
16
+ Examples:
17
+ `./script/generate hobofield_model account`
18
+
19
+ creates an Account model, test, fixture, and migration:
20
+ Model: app/models/account.rb
21
+ Test: test/unit/account_test.rb
22
+ Fixtures: test/fixtures/accounts.yml
23
+
24
+ `./script/generate hobofield_model post title:string body:text published:boolean`
25
+
26
+ creates a Post model with a string title, text body, and published flag.
27
+
28
+ After the model is created, and the fields are specified, use hobofield
29
+ to create the migrations incrementally.
@@ -0,0 +1,38 @@
1
+ class HobofieldModelGenerator < Rails::Generator::NamedBase
2
+ default_options :skip_timestamps => false, :skip_fixture => false
3
+
4
+ def manifest
5
+ record do |m|
6
+ # Check for class naming collisions.
7
+ m.class_collisions class_path, class_name, "#{class_name}Test"
8
+
9
+ # Model, test, and fixture directories.
10
+ m.directory File.join('app/models', class_path)
11
+ m.directory File.join('test/unit', class_path)
12
+ m.directory File.join('test/fixtures', class_path)
13
+
14
+ # Create stubs
15
+ m.template "model.rb.erb", "app/models/#{file_name}.rb"
16
+ m.template "test.rb.erb", "test/unit/#{file_name}_test.rb"
17
+
18
+ unless options[:skip_fixture]
19
+ m.template 'fixtures.yml.erb', File.join('test/fixtures', "#{file_name.pluralize}.yml")
20
+ end
21
+
22
+ end
23
+ end
24
+
25
+ protected
26
+ def banner
27
+ "Usage: #{$0} #{spec.name} ModelName [field:type, field:type]"
28
+ end
29
+
30
+ def add_options!(opt)
31
+ opt.separator ''
32
+ opt.separator 'Options:'
33
+ opt.on("--skip-timestamps",
34
+ "Don't add timestamps to the migration file for this model") { |v| options[:skip_timestamps] = v }
35
+ opt.on("--skip-fixture",
36
+ "Don't generation a fixture file for this model") { |v| options[:skip_fixture] = v}
37
+ end
38
+ end
@@ -0,0 +1,19 @@
1
+ # Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
2
+
3
+ <% unless attributes.empty? -%>
4
+ one:
5
+ <% for attribute in attributes -%>
6
+ <%= attribute.name %>: <%= attribute.default %>
7
+ <% end -%>
8
+
9
+ two:
10
+ <% for attribute in attributes -%>
11
+ <%= attribute.name %>: <%= attribute.default %>
12
+ <% end -%>
13
+ <% else -%>
14
+ # one:
15
+ # column: value
16
+ #
17
+ # two:
18
+ # column: value
19
+ <% end -%>
@@ -0,0 +1,10 @@
1
+ class <%= class_name %> < ActiveRecord::Base
2
+ fields do
3
+ <% for attribute in attributes -%>
4
+ <%= attribute.name %> :<%= attribute.type %>
5
+ <% end -%>
6
+ <% unless options[:skip_timestamps] %>
7
+ timestamps
8
+ <% end -%>
9
+ end
10
+ end
@@ -0,0 +1,8 @@
1
+ require 'test_helper'
2
+
3
+ class <%= class_name %>Test < ActiveSupport::TestCase
4
+ # Replace this with your real tests.
5
+ def test_truth
6
+ assert true
7
+ end
8
+ end
data/script/destroy ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+ APP_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..'))
3
+
4
+ begin
5
+ require 'rubigen'
6
+ rescue LoadError
7
+ require 'rubygems'
8
+ require 'rubigen'
9
+ end
10
+ require 'rubigen/scripts/destroy'
11
+
12
+ ARGV.shift if ['--help', '-h'].include?(ARGV[0])
13
+ RubiGen::Base.use_component_sources! [:rubygems, :test_unit]
14
+ RubiGen::Scripts::Destroy.new.run(ARGV)
data/script/generate ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+ APP_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..'))
3
+
4
+ begin
5
+ require 'rubigen'
6
+ rescue LoadError
7
+ require 'rubygems'
8
+ require 'rubigen'
9
+ end
10
+ require 'rubigen/scripts/generate'
11
+
12
+ ARGV.shift if ['--help', '-h'].include?(ARGV[0])
13
+ RubiGen::Base.use_component_sources! [:rubygems, :test_unit]
14
+ RubiGen::Scripts::Generate.new.run(ARGV)
@@ -1,57 +1,94 @@
1
1
  # HoboFields
2
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.
3
+ ## Introduction
6
4
 
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!
5
+ Welcome to HoboFields -- a spin-off from the Hobo project (Hobo not required!).
8
6
 
9
- ## Introduction
7
+ **HoboFields writes your Rails migrations for you! Your migration writing days are over!**
10
8
 
11
- HoboFields lives on GitHub: [http://github.com/tablatom/hobofields](http://github.com/tablatom/hobofields)
9
+ All we ask is that you declare your fields in the model. It's still perfectly DRY because you're not having to repeat that in the migration -- HoboFields does that for you. In fact, you'll come to love having them there.
12
10
 
13
- You can install it with git:
11
+ It still has all the benefits of writing your own migrations, for example if you want to add some special code to migrate your old data, you can just edit the generated migration.
14
12
 
15
- git clone git://github.com/tablatom/hobofields.git vendor/plugins/hobofields
13
+ ## Example
16
14
 
17
- Or subversion:
15
+ First off, if you're using the migration generator outside of Hobo, do remember the `--skip-migration` option when generating your models:
18
16
 
19
- svn export svn://hobocentral.net/hobofields/tags/rel_0.7.4 vendor/plugins/hobofields
20
-
21
- HoboFields provides two main features:
17
+ $ ./script/generate model blog_post --skip-migration
22
18
 
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
19
+ Now edit your model as follows:
27
20
 
28
21
  class BlogPost < ActiveRecord::Base
29
22
  fields do
30
23
  title :string
31
24
  body :text
25
+ timestamps
32
26
  end
33
27
  end
34
28
  {: .ruby}
35
29
 
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
30
 
38
- ./script/generate model post --skip-migration
31
+ Then, simply run
32
+
33
+ $ ./script/genearte hobo_migration
34
+
35
+ And voila
36
+
37
+ ---------- Up Migration ----------
38
+ create_table :blog_posts do |t|
39
+ t.string :title
40
+ t.text :body
41
+ t.datetime :created_at
42
+ t.datetime :updated_at
43
+ end
44
+ ----------------------------------
45
+
46
+ ---------- Down Migration --------
47
+ drop_table :blog_posts
48
+ ----------------------------------
49
+ {: .ruby}
50
+
51
+ The migration generator has created 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. You are now free to simply hack away on your app and run the migration generator every time the database needs to play catch-up.
52
+
53
+ Note that the migration generator is interactive -- it can't tell the difference between renaming something vs. adding one thing and removing another, so sometimes it will ask you to clarify. It's a bit picky about what it makes you type in response, because we really don't want you to lose data when someone's amazing twitter distracts you at the wrong moment.
54
+
55
+ ## Installing
56
+
57
+ The simplest and recommended way to install HoboFeilds is as a gem:
39
58
 
59
+ $ gem install hobofields
40
60
 
41
- ## [Migration Generator](/hobofields/migration_generator)
61
+ The source lives on GitHub as part of the main Hobo repo:
42
62
 
43
- Once you have declared your fields like this, you can run the following, at any time during the development of your project:
63
+ - [http://github.com/tablatom/hobo](http://github.com/tablatom/hobo)
44
64
 
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.
65
+ The simplest way to install HoboFields as a plugin is to use the subversion mirror:
48
66
 
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
67
+ svn export svn://hobocentral.net/hobo/tags/rel_0.7.5/hobofields vendor/plugins
50
68
 
51
- ## [HoboFields API](/hobofields/hobofields_api)
69
+ ## Rich Types
52
70
 
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.
71
+ In addition to the migration generator, HoboFields provides a Rich-Type mechanism that allows you to declare your fields using higher level concepts like "email-address" or "markdown text". This can give you both automatic validations, and automatic rendering of those values in the UI (this works particularly well if you use the rest of Hobo).
54
72
 
55
- ## [Rich Types](/hobofields/rich_types)
73
+ Read more:
74
+
75
+ - [HoboFields Rich Types](/hobofields/rich_types)
76
+
77
+ ## API
78
+
79
+ HoboFields provides various API methods. Read more:
80
+
81
+ - [HoboFields API](/hobofields/hobofields_api)
82
+
83
+ ## Migration Generator Details
84
+
85
+ The migration generator doctests 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
86
+
87
+ - [Migration Generator Details](/hobofields/migration_generator)
88
+
89
+ ## About Doctests
90
+
91
+ 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.
92
+
93
+ 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!
56
94
 
57
- Documentation for the full set of rich types bundled with HoboFields, and how to create your own, is [here](/hobofields/rich_types).