modalfields 1.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/Gemfile +15 -0
- data/Gemfile.lock +43 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +133 -0
- data/Rakefile +49 -0
- data/TODO +33 -0
- data/VERSION +1 -0
- data/lib/modalfields.rb +13 -0
- data/lib/modalfields/modalfields.rb +428 -0
- data/lib/modalfields/standardfields.rb +13 -0
- data/lib/modalfields/tasks.rb +1 -0
- data/lib/tasks/check.rake +6 -0
- data/lib/tasks/migrate.rake +17 -0
- data/lib/tasks/update.rake +6 -0
- data/test/create_database.rb +47 -0
- data/test/database.yml +11 -0
- data/test/helper.rb +61 -0
- data/test/model_cases/bare/after/author.rb +12 -0
- data/test/model_cases/bare/after/book.rb +12 -0
- data/test/model_cases/bare/before/author.rb +3 -0
- data/test/model_cases/bare/before/book.rb +4 -0
- data/test/model_cases/clean/after/author.rb +14 -0
- data/test/model_cases/clean/after/book.rb +14 -0
- data/test/model_cases/clean/before/author.rb +14 -0
- data/test/model_cases/clean/before/book.rb +14 -0
- data/test/model_cases/dirty/after/author.rb +14 -0
- data/test/model_cases/dirty/after/book.rb +14 -0
- data/test/model_cases/dirty/before/author.rb +15 -0
- data/test/model_cases/dirty/before/book.rb +14 -0
- data/test/schema.rb +17 -0
- data/test/test_diff.rb +127 -0
- data/test/test_update.rb +52 -0
- metadata +228 -0
data/.document
ADDED
data/Gemfile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
source "http://rubygems.org"
|
2
|
+
gem "activesupport", ">= 2.3.5"
|
3
|
+
gem "activerecord", ">= 2.3.5"
|
4
|
+
# gem "rails", ">= 2.3.5"
|
5
|
+
|
6
|
+
# Add dependencies to develop your gem here.
|
7
|
+
# Include everything needed to run rake, tests, features, etc.
|
8
|
+
group :development do
|
9
|
+
gem "shoulda", ">= 0"
|
10
|
+
gem "rdoc", "~> 3.12"
|
11
|
+
gem "bundler", "~> 1"
|
12
|
+
gem "jeweler", "~> 1.8.3"
|
13
|
+
gem "sqlite3"
|
14
|
+
gem "pg"
|
15
|
+
end
|
data/Gemfile.lock
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
GEM
|
2
|
+
remote: http://rubygems.org/
|
3
|
+
specs:
|
4
|
+
activemodel (3.0.3)
|
5
|
+
activesupport (= 3.0.3)
|
6
|
+
builder (~> 2.1.2)
|
7
|
+
i18n (~> 0.4)
|
8
|
+
activerecord (3.0.3)
|
9
|
+
activemodel (= 3.0.3)
|
10
|
+
activesupport (= 3.0.3)
|
11
|
+
arel (~> 2.0.2)
|
12
|
+
tzinfo (~> 0.3.23)
|
13
|
+
activesupport (3.0.3)
|
14
|
+
arel (2.0.4)
|
15
|
+
builder (2.1.2)
|
16
|
+
git (1.2.5)
|
17
|
+
i18n (0.4.2)
|
18
|
+
jeweler (1.8.3)
|
19
|
+
bundler (~> 1.0)
|
20
|
+
git (>= 1.2.5)
|
21
|
+
rake
|
22
|
+
rdoc
|
23
|
+
json (1.6.6)
|
24
|
+
pg (0.10.1)
|
25
|
+
rake (0.9.2.2)
|
26
|
+
rdoc (3.12)
|
27
|
+
json (~> 1.4)
|
28
|
+
shoulda (2.10.3)
|
29
|
+
sqlite3 (1.3.3)
|
30
|
+
tzinfo (0.3.23)
|
31
|
+
|
32
|
+
PLATFORMS
|
33
|
+
ruby
|
34
|
+
|
35
|
+
DEPENDENCIES
|
36
|
+
activerecord (>= 2.3.5)
|
37
|
+
activesupport (>= 2.3.5)
|
38
|
+
bundler (~> 1)
|
39
|
+
jeweler (~> 1.8.3)
|
40
|
+
pg
|
41
|
+
rdoc (~> 3.12)
|
42
|
+
shoulda
|
43
|
+
sqlite3
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2011 Javier Goizueta
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,133 @@
|
|
1
|
+
= ModalFields
|
2
|
+
|
3
|
+
This is a Rails Plugin to maintain schema information in the models' definitions.
|
4
|
+
It is a hybrid between HoboFields and model annotators.
|
5
|
+
|
6
|
+
It works like other annotators, by adding documentation to the model classes
|
7
|
+
from the DB schema. But the annotations are syntactic Ruby as in HoboFields rather than comments:
|
8
|
+
|
9
|
+
class User < ActiveRecord::Base
|
10
|
+
fields do
|
11
|
+
name :string
|
12
|
+
birthdate :date
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
Apart from looking prettier to my eyes, this allows triggering special functionality
|
17
|
+
from the field declarations (such as specifying validations).
|
18
|
+
|
19
|
+
Fields that are foreign_keys of belongs_to associations are not annotated; it is assumed that
|
20
|
+
belongs_to and other associations follow the fields block declaration, so the information
|
21
|
+
is readily available.
|
22
|
+
|
23
|
+
Primary keys named are also not annotated (unless the ModalFields.show_primary_keys property is changed)
|
24
|
+
|
25
|
+
The annotations are kept up to date by the migration tasks (currently only if the plugin is installed under vendor)
|
26
|
+
Comments and validation, etc. specifications modified manually are preserved, at least
|
27
|
+
if the field block syntax is kept as generated (one line per field, one line for the
|
28
|
+
block start and end...)
|
29
|
+
|
30
|
+
Custom type fields and hooks can be define in files (e.g. fields.rb) in config/initializers/
|
31
|
+
|
32
|
+
== Rake Tasks
|
33
|
+
|
34
|
+
There's a couple of Rake tasks:
|
35
|
+
* fields:update is what's called after a migration; it updates the fields blocks in the model class definitions.
|
36
|
+
* fields:check shows the difference between the declared fields and the DB schema (what would be modified by fields:update)
|
37
|
+
|
38
|
+
Under Rails 2, you need to add this to your Rakefile to make the tasks available:
|
39
|
+
|
40
|
+
require 'modalfields/tasks'
|
41
|
+
|
42
|
+
== Some customization examples:
|
43
|
+
|
44
|
+
ModalFields.hook do
|
45
|
+
|
46
|
+
# Declare serialized fields as
|
47
|
+
# field_name :serialized, :class=>Array
|
48
|
+
# another option would be: (using the generic hook)
|
49
|
+
# field_name :text, :serialize=>Array
|
50
|
+
serialized do |model, declaration|
|
51
|
+
model.serialize declaration.name, declaration.attributes[:class].class || Object
|
52
|
+
declaration.replace!(:type=>:text).remove_attributes!(:class)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Add specific support for date fields (_ui virtual attributes)
|
56
|
+
date do |model, declaration|
|
57
|
+
model.date_ui declaration.name
|
58
|
+
end
|
59
|
+
|
60
|
+
# Add specific support for date and datetime and detect fields with units
|
61
|
+
all_fields do |model, declaration|
|
62
|
+
date_ui name if [:date, :datetime].include?(declaration.type)
|
63
|
+
if ModalSupport::Units.valid_units?(units = declaration.name.to_s.split('_').last)
|
64
|
+
prec = {'m'=>1, 'mm'=>0, 'cm'=>0, 'km'=>3}[units] || 0
|
65
|
+
magnitude_ui name, prec, units
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
|
71
|
+
# Spatial Adapter columns: require specific column to declaration conversion and field types
|
72
|
+
|
73
|
+
ModalFields.column_to_field_declaration do |column|
|
74
|
+
type = column.type.to_sym
|
75
|
+
type = column.geometry_type if type==:geometry
|
76
|
+
attributes = {}
|
77
|
+
attrs = ModalFields.definitions[type]
|
78
|
+
attrs.keys.each do |attr|
|
79
|
+
v = column.send(attr)
|
80
|
+
attributes[attr] = v unless attrs[attr]==v
|
81
|
+
end
|
82
|
+
ModalFields::FieldDeclaration.new(column.name.to_sym, type, [], attributes)
|
83
|
+
end
|
84
|
+
|
85
|
+
ModalFields.define do
|
86
|
+
point :srid=>nil, :with_z=>false, :with_m=>false, :sql_type=>'POINT'
|
87
|
+
line_string :srid=>nil, :with_z=>false, :with_m=>false, :sql_type=>'LINESTRING'
|
88
|
+
polygon :srid=>nil, :with_z=>false, :with_m=>false, :sql_type=>'POLYGON'
|
89
|
+
geometry_collection :srid=>nil, :with_z=>false, :with_m=>false, :sql_type=>'GEOMETRYCOLLECTION'
|
90
|
+
multi_point :srid=>nil, :with_z=>false, :with_m=>false, :sql_type=>'MULTIPOINT'
|
91
|
+
multi_line_string :srid=>nil, :with_z=>false, :with_m=>false, :sql_type=>'MULTILINESTRING'
|
92
|
+
multi_polygon :srid=>nil, :with_z=>false, :with_m=>false, :sql_type=>'MULTIPOLYGON'
|
93
|
+
geometry :srid=>nil, :with_z=>false, :with_m=>false, :sql_type=>nil
|
94
|
+
end
|
95
|
+
|
96
|
+
ModalFields.hook do
|
97
|
+
%w{point line_string polygon geometry_collection multi_point multi_line_string multi_polygon}.each do |spatial_type|
|
98
|
+
field_type spatial_type.to_sym do |model, declaration|
|
99
|
+
declaration.replace!(:type=>:geometry).add!(:sql_type=>spatial_type.upcase.tr('_',''))
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
|
105
|
+
# Enumerated field with symbolic constants associated (and translated literals) using the enum_id plugin
|
106
|
+
# Use:
|
107
|
+
# enum :name, :values=>{id1=>:first_symbol, id2=>:second_symbol, ...}
|
108
|
+
# Or: (ids are sequential values starting in 1)
|
109
|
+
# enum :name, :values=>[:first_symbol, :second_symbol, ...]
|
110
|
+
ModalFields.hook do
|
111
|
+
enum do |model, declaration|
|
112
|
+
values = declaration.attributes[:values]
|
113
|
+
if values.kind_of?(Array)
|
114
|
+
values = (1..values.size).map_hash{|i| values[i-1]}
|
115
|
+
end
|
116
|
+
model.enum_id declaration.name, values
|
117
|
+
declaration.replace! :type=>:integer, :name=>"#{declaration.name}_id"
|
118
|
+
end
|
119
|
+
|
120
|
+
class ModalFields::Declaration
|
121
|
+
def enum(*values)
|
122
|
+
values = values.first if values.size==1 && values.first.kind_of?(Hash)
|
123
|
+
{:values=>values}
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
|
129
|
+
== Copyright
|
130
|
+
|
131
|
+
Copyright (c) 2011 Javier Goizueta. See LICENSE.txt for
|
132
|
+
further details.
|
133
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler'
|
3
|
+
begin
|
4
|
+
Bundler.setup(:default, :development)
|
5
|
+
rescue Bundler::BundlerError => e
|
6
|
+
$stderr.puts e.message
|
7
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
8
|
+
exit e.status_code
|
9
|
+
end
|
10
|
+
require 'rake'
|
11
|
+
|
12
|
+
require 'jeweler'
|
13
|
+
Jeweler::Tasks.new do |gem|
|
14
|
+
# gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
|
15
|
+
gem.name = "modalfields"
|
16
|
+
gem.homepage = "http://github.com/jgoizueta/modalfields"
|
17
|
+
gem.license = "MIT"
|
18
|
+
gem.summary = %Q{Model annotator with Ruby (Hobo-like) syntax and hooks.}
|
19
|
+
gem.description = %Q{ModelFields is a Rails plugin that adds fields declarations to your models.}
|
20
|
+
gem.email = "jgoizueta@gmail.com"
|
21
|
+
gem.authors = ["Javier Goizueta"]
|
22
|
+
gem.add_runtime_dependency 'rails', '>= 2.3.0'
|
23
|
+
|
24
|
+
# Include your dependencies below. Runtime dependencies are required when using your gem,
|
25
|
+
# and development dependencies are only needed for development (ie running rake tasks, tests, etc)
|
26
|
+
# gem.add_runtime_dependency 'jabber4r', '> 0.1'
|
27
|
+
# gem.add_development_dependency 'rspec', '> 1.2.3'
|
28
|
+
end
|
29
|
+
Jeweler::RubygemsDotOrgTasks.new
|
30
|
+
|
31
|
+
require 'rake/testtask'
|
32
|
+
Rake::TestTask.new(:test) do |test|
|
33
|
+
test.libs << 'lib' << 'test'
|
34
|
+
test.pattern = 'test/**/test_*.rb'
|
35
|
+
test.verbose = true
|
36
|
+
end
|
37
|
+
|
38
|
+
|
39
|
+
task :default => :test
|
40
|
+
|
41
|
+
require 'rake/rdoctask'
|
42
|
+
Rake::RDocTask.new do |rdoc|
|
43
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
44
|
+
|
45
|
+
rdoc.rdoc_dir = 'rdoc'
|
46
|
+
rdoc.title = "modalfields #{version}"
|
47
|
+
rdoc.rdoc_files.include('README*')
|
48
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
49
|
+
end
|
data/TODO
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
type synonyms (timestamp, datetime)
|
2
|
+
|
3
|
+
process specifiers (to add validations for required, etc.)(detect index modifications) decide what to do with multiple-column indices
|
4
|
+
|
5
|
+
refactor into multiple files
|
6
|
+
|
7
|
+
add extensible specifiers:
|
8
|
+
ModalFields.specify do
|
9
|
+
required do |model, column|
|
10
|
+
model.validates_presence_of column.name
|
11
|
+
end
|
12
|
+
unique do |model, column|
|
13
|
+
model.validates_uniqueness_of name, :allow_nil => !column.specifiers.include?(:required)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
rename hook to... filter? process? declared? transformation?
|
18
|
+
|
19
|
+
complete field declaration validation
|
20
|
+
|
21
|
+
helper methods for field declaration: (can be used instead of the type and dispense with the need
|
22
|
+
of extra attributes)
|
23
|
+
status enum_field(:draft, :approved, :published), :required
|
24
|
+
instead of
|
25
|
+
status :enum_field, :required, :values=>[:draft, :approved, :published]
|
26
|
+
|
27
|
+
Vendorized Installation:
|
28
|
+
Rails 2
|
29
|
+
script/plugin install git://github.com/jgoizueta/modalfields.git
|
30
|
+
Rails 3
|
31
|
+
rails plugin install git://github.com/jgoizueta/modalfields.git
|
32
|
+
|
33
|
+
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
1.1.1
|
data/lib/modalfields.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'modalfields/modalfields'
|
2
|
+
require 'modalfields/standardfields'
|
3
|
+
|
4
|
+
if defined?(Rails)
|
5
|
+
if Rails.version.split('.').first.to_i > 2
|
6
|
+
class BackupTask < Rails::Railtie
|
7
|
+
rake_tasks do
|
8
|
+
Dir[File.join(File.dirname(__FILE__), 'tasks', '**/*.rake')].each { |f| load f }
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
ModalFields.enable if defined?(ActiveRecord::Base)
|
13
|
+
end
|
@@ -0,0 +1,428 @@
|
|
1
|
+
# This is a hybrid between HoboFields and model annotators.
|
2
|
+
#
|
3
|
+
# It works like other annotators, by updating the model annotations from the DB schema.
|
4
|
+
# But the annotations are syntactic Ruby as in HoboFields rather than comments.
|
5
|
+
#
|
6
|
+
# Apart from looking better to my eyes, this allows triggering special functionality
|
7
|
+
# from the field declations (such as specifying validations).
|
8
|
+
#
|
9
|
+
# The annotations are kept up to date by the migration tasks.
|
10
|
+
# Comments and validation, etc. specifications modified manually are preserved, at least
|
11
|
+
# if the field block syntax is kept as generated (one line per field, one line for the
|
12
|
+
# block start and end...)
|
13
|
+
#
|
14
|
+
# Custom type fields and hooks can be define in files (e.g. fields.rb) in config/initializers/
|
15
|
+
#
|
16
|
+
module ModalFields
|
17
|
+
|
18
|
+
SPECIFIERS = [:indexed, :unique, :required]
|
19
|
+
COMMON_ATTRIBUTES = {:default=>nil, :null=>true}
|
20
|
+
|
21
|
+
class FieldDeclaration < Struct.new(:name, :type, :specifiers, :attributes)
|
22
|
+
|
23
|
+
def self.declare(name, type, *args)
|
24
|
+
attributes = args.extract_options!
|
25
|
+
new(name, type, args, attributes)
|
26
|
+
end
|
27
|
+
|
28
|
+
def replace!(replacements={})
|
29
|
+
replacements.each_pair do |key, value|
|
30
|
+
self[key] = value
|
31
|
+
end
|
32
|
+
self
|
33
|
+
end
|
34
|
+
|
35
|
+
def remove_attributes!(*attrs)
|
36
|
+
self.attributes = self.attributes.except(*attrs)
|
37
|
+
self
|
38
|
+
end
|
39
|
+
|
40
|
+
def add!(attrs)
|
41
|
+
self.attributes.merge! attrs
|
42
|
+
self
|
43
|
+
end
|
44
|
+
|
45
|
+
def to_s
|
46
|
+
code = "#{name} :#{type}"
|
47
|
+
code << ", "+specifiers.inspect[1...-1] unless specifiers.empty?
|
48
|
+
unless attributes.empty?
|
49
|
+
code << ", "+attributes.keys.sort_by{|attr| attr.to_s}.map{|attr|
|
50
|
+
v = attributes[attr]
|
51
|
+
v = v.kind_of?(BigDecimal) ? "BigDecimal('#{v.to_s}')" : v.inspect
|
52
|
+
":#{attr}=>#{v}"
|
53
|
+
}*", "
|
54
|
+
end
|
55
|
+
code
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
|
60
|
+
|
61
|
+
class DefinitionsDsl
|
62
|
+
def field(name, attributes={})
|
63
|
+
ModalFields.definitions[name.to_sym] = COMMON_ATTRIBUTES.merge(attributes)
|
64
|
+
end
|
65
|
+
def method_missing(name, *args)
|
66
|
+
field(name, *args)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
class HooksDsl
|
71
|
+
def field_type(type, &blk)
|
72
|
+
ModalFields.hooks[type.to_sym] = lambda{|model, column_declaration|
|
73
|
+
blk[model, column_declaration]
|
74
|
+
}
|
75
|
+
end
|
76
|
+
# geric filter applied to all the fields (after a specific filter for the type, if there is one)
|
77
|
+
def all_fields(&blk)
|
78
|
+
field_type :all_fields, &blk
|
79
|
+
end
|
80
|
+
def method_missing(name, *args, &blk)
|
81
|
+
field_type name, *args, &blk
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
class DeclarationsDsl
|
86
|
+
def initialize(model)
|
87
|
+
@model = model
|
88
|
+
end
|
89
|
+
def field(name, type, *args)
|
90
|
+
declaration = FieldDeclaration.declare(name, type, *args)
|
91
|
+
specific_hook = ModalFields.hooks[type.to_sym]
|
92
|
+
general_hook = ModalFields.hooks[:all_fields]
|
93
|
+
[specific_hook, general_hook].compact.each do |hook|
|
94
|
+
hook[@model, declaration] if hook
|
95
|
+
end
|
96
|
+
if ModalFields.validate(declaration)
|
97
|
+
@model.fields_info << declaration
|
98
|
+
end
|
99
|
+
end
|
100
|
+
def timestamps
|
101
|
+
field :created_at, :datetime
|
102
|
+
field :updated_at, :datetime
|
103
|
+
end
|
104
|
+
def method_missing(name, type, *args)
|
105
|
+
field(name, type, *args)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
module FieldDeclarationClassMethods
|
110
|
+
def fields(&blk)
|
111
|
+
@fields_info ||= []
|
112
|
+
unless self.respond_to?(:fields_info)
|
113
|
+
self.instance_eval do
|
114
|
+
def fields_info
|
115
|
+
@fields_info
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
DeclarationsDsl.new(self).instance_eval(&blk)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
@show_primary_keys = false
|
124
|
+
@hooks = {}
|
125
|
+
@definitions = {}
|
126
|
+
@column_to_field_declaration_hook = nil
|
127
|
+
|
128
|
+
class <<self
|
129
|
+
attr_reader :hooks, :definitions
|
130
|
+
# Define declaration of primary keys
|
131
|
+
# ModalFields.show_primary_keys = false # the default: do not show primary keys
|
132
|
+
# ModalFields.show_primary_keys = true # always declare primary keys
|
133
|
+
# ModalFields.show_primary_keys = :id # only declare if named 'id' (otherwise the model will have a primary_key declaration)
|
134
|
+
# ModalFields.show_primary_keys = :except_id # only declare if named differently from 'id'
|
135
|
+
attr_accessor :show_primary_keys
|
136
|
+
|
137
|
+
# Run a definition block that executes field type definitions
|
138
|
+
def define(&blk)
|
139
|
+
DefinitionsDsl.new.instance_eval(&blk)
|
140
|
+
end
|
141
|
+
|
142
|
+
# Run a hooks block that defines field declaration processors
|
143
|
+
def hook(&blk)
|
144
|
+
HooksDsl.new.instance_eval(&blk)
|
145
|
+
end
|
146
|
+
|
147
|
+
# Define a custom column to field declaration conversion
|
148
|
+
def column_to_field_declaration(&blk)
|
149
|
+
@column_to_field_declaration_hook = blk
|
150
|
+
end
|
151
|
+
|
152
|
+
# Enable the ModalFields plugin (adds the fields declarator to model classes)
|
153
|
+
def enable
|
154
|
+
if defined?(::Rails)
|
155
|
+
# class ::ActiveRecord::Base
|
156
|
+
# extend FieldDeclarationClassMethods
|
157
|
+
# end
|
158
|
+
::ActiveRecord::Base.send :extend, FieldDeclarationClassMethods
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
# Update the field declarations of all the models.
|
163
|
+
# This modifies the source files of all the models (touches only the fields block or adds one if not present).
|
164
|
+
# It is recommended to run this on a clearn working directory (no uncommitted changes), so that the
|
165
|
+
# changes can be easily reviewed.
|
166
|
+
def update(modify=true)
|
167
|
+
dbmodels.each do |model, file|
|
168
|
+
new_fields, modified_fields, deleted_fields = diff(model)
|
169
|
+
unless new_fields.empty? && modified_fields.empty? && deleted_fields.empty?
|
170
|
+
pre, start_fields, fields, end_fields, post = split_model_file(file)
|
171
|
+
deleted_names = deleted_fields.map{|f| f.name.to_s}
|
172
|
+
fields = fields.reject{|line, name, comment| deleted_names.include?(name)}
|
173
|
+
fields = fields.map{|line, name, comment|
|
174
|
+
mod_field = modified_fields.detect{|f| f.name.to_s==name}
|
175
|
+
if mod_field
|
176
|
+
line = " "+mod_field.to_s
|
177
|
+
line << " #{comment}" if comment
|
178
|
+
line << "\n"
|
179
|
+
end
|
180
|
+
[line, name, comment]
|
181
|
+
}
|
182
|
+
pk_names = Array(model.primary_key).map(&:to_s)
|
183
|
+
created_at = new_fields.detect{|f| f.name.to_s=='created_at'}
|
184
|
+
updated_at = new_fields.detect{|f| f.name.to_s=='updated_at'}
|
185
|
+
if created_at && updated_at && created_at.type.to_sym==:datetime && updated_at.type.to_sym==:datetime
|
186
|
+
with_timestamps = true
|
187
|
+
new_fields -= [created_at, updated_at]
|
188
|
+
end
|
189
|
+
fields += new_fields.map{|f|
|
190
|
+
comments = pk_names.include?(f.name.to_s) ? " \# PK" : ""
|
191
|
+
[" #{f}#{comments}\n" ]
|
192
|
+
}
|
193
|
+
fields << [" timestamps\n"] if with_timestamps
|
194
|
+
output_file = modify ? file : "#{file}_with_fields.rb"
|
195
|
+
join_model_file(output_file, pre, start_fields, fields, end_fields, post)
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
def check
|
201
|
+
dbmodels.each do |model, file|
|
202
|
+
new_fields, modified_fields, deleted_fields = diff(model)
|
203
|
+
unless new_fields.empty? && modified_fields.empty? && deleted_fields.empty?
|
204
|
+
rel_file = file.sub(/\A#{Rails.root}/,'')
|
205
|
+
puts "#{model} (#{rel_file}):"
|
206
|
+
[['+',new_fields],['*',modified_fields],['-',deleted_fields]].each do |prefix, fields|
|
207
|
+
puts fields.map{|field| " #{prefix} #{field}"}*"\n" unless fields.empty?
|
208
|
+
# TODO: report index differences
|
209
|
+
end
|
210
|
+
puts ""
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
def validate(declaration)
|
216
|
+
definition = definitions[declaration.type.to_sym]
|
217
|
+
raise "Field type #{declaration.type} not defined" unless definition
|
218
|
+
# TODO: validate declaration.specifiers
|
219
|
+
# TODO: validate declaration.attributes with definition
|
220
|
+
true
|
221
|
+
end
|
222
|
+
|
223
|
+
private
|
224
|
+
|
225
|
+
# return ActiveRecord classes corresponding to tables, without STI derived classes, but including indirectly
|
226
|
+
# derived classes that do have their own tables (to achieve this we use the convention that in such cases
|
227
|
+
# the base class, directly derived from ActiveRecord::Base has a nil table_name)
|
228
|
+
def dbmodels
|
229
|
+
models = Dir.glob(File.join(Rails.root,"app/models/**/*.rb"))\
|
230
|
+
.map{|f| [File.basename(f).chomp(".rb").camelize.constantize, f]}\
|
231
|
+
.select{|c,f| has_table(c)}\
|
232
|
+
.reject{|c,f| has_table(c.superclass)}
|
233
|
+
models.uniq
|
234
|
+
end
|
235
|
+
|
236
|
+
def has_table(cls)
|
237
|
+
(cls!=ActiveRecord::Base) && cls.respond_to?(:table_name) && !cls.table_name.blank?
|
238
|
+
end
|
239
|
+
|
240
|
+
def map_column_to_field_declaration(column)
|
241
|
+
if @column_to_field_declaration_hook
|
242
|
+
@column_to_field_declaration_hook[column]
|
243
|
+
else
|
244
|
+
type = column.type.to_sym
|
245
|
+
attributes = {}
|
246
|
+
attrs = definitions[type]
|
247
|
+
attrs.keys.each do |attr|
|
248
|
+
v = column.send(attr)
|
249
|
+
attributes[attr] = v unless attrs[attr]==v
|
250
|
+
end
|
251
|
+
FieldDeclaration.new(column.name.to_sym, type, [], attributes)
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
# Compare the declared fields of a model (in the fields block) to the actual model columns (in the schema).
|
256
|
+
# returns the difference as [new_fields, modified_fields, deleted_fields]
|
257
|
+
# where:
|
258
|
+
# * new_fields are field declarations not present in the model (corresponding to model columns not declared
|
259
|
+
# in the fields declaration block).
|
260
|
+
# * modified_fields are field declarations corresponding to model columns that are different from their existing
|
261
|
+
# declarations.
|
262
|
+
# * deleted_fields are fields declared in the fields block but not present in the current schema.
|
263
|
+
def diff(model)
|
264
|
+
# model.columns will fail if the table does not exist
|
265
|
+
existing_fields = model.columns rescue []
|
266
|
+
association_fields = model.reflect_on_all_associations(:belongs_to).map(&:primary_key_name).flatten.map(&:to_s)
|
267
|
+
pk_fields = Array(model.primary_key).map(&:to_s)
|
268
|
+
case show_primary_keys
|
269
|
+
when true
|
270
|
+
pk_fields = []
|
271
|
+
when :id
|
272
|
+
pk_fields = pk_fields.reject{|pk| pk=='id'}
|
273
|
+
when :except_id
|
274
|
+
pk_fields = pk_fields.select{|pk| pk=='id'}
|
275
|
+
end
|
276
|
+
if model.respond_to?(:fields_info)
|
277
|
+
declared_fields = model.fields_info
|
278
|
+
indices = model.connection.indexes(model.table_name) # name, columns, unique, spatial
|
279
|
+
|
280
|
+
existing_declared_fields = []
|
281
|
+
existing_undeclared_fields = []
|
282
|
+
existing_fields.each do |f|
|
283
|
+
name = f.name.to_s
|
284
|
+
if declared_fields.detect{|df| df.name.to_s==name} || association_fields.include?(name) || pk_fields.include?(name)
|
285
|
+
existing_declared_fields << f
|
286
|
+
else
|
287
|
+
existing_undeclared_fields << f
|
288
|
+
end
|
289
|
+
end
|
290
|
+
deleted_fields = declared_fields.reject{|f|
|
291
|
+
name = f.name.to_s
|
292
|
+
existing_declared_fields.detect{|df| df.name.to_s==name}
|
293
|
+
}
|
294
|
+
modified_fields = (declared_fields - deleted_fields).map{ |field_declaration|
|
295
|
+
column = existing_declared_fields.detect{|f| f.name.to_s == field_declaration.name.to_s}
|
296
|
+
identical = false
|
297
|
+
column = map_column_to_field_declaration(column)
|
298
|
+
if field_declaration.type.to_sym == column.type.to_sym
|
299
|
+
attrs = definitions[column.type.to_sym]
|
300
|
+
attr_keys = attrs.keys
|
301
|
+
decl_attrs = attr_keys.map{|a|
|
302
|
+
v = field_declaration.attributes[a]
|
303
|
+
v==attrs[a] ? nil : v
|
304
|
+
}
|
305
|
+
col_attrs = attr_keys.map{|a|
|
306
|
+
v = column.attributes[a]
|
307
|
+
v==attrs[a] ? nil : v
|
308
|
+
}
|
309
|
+
if decl_attrs == col_attrs
|
310
|
+
identical=true
|
311
|
+
# specifiers are defined only in declarations
|
312
|
+
end
|
313
|
+
end
|
314
|
+
column.specifiers = field_declaration.specifiers
|
315
|
+
identical ? nil : column
|
316
|
+
}.compact
|
317
|
+
else
|
318
|
+
modified_fields = deleted_fields = []
|
319
|
+
existing_undeclared_fields = existing_fields.reject{|f|
|
320
|
+
name = f.name.to_s
|
321
|
+
association_fields.include?(name) || pk_fields.include?(name)
|
322
|
+
}
|
323
|
+
end
|
324
|
+
new_fields = existing_undeclared_fields.map { |f|
|
325
|
+
attributes = {}
|
326
|
+
attrs = definitions[f.type.to_sym]
|
327
|
+
attrs.keys.each do |attr|
|
328
|
+
v = f.send(attr)
|
329
|
+
attributes[attr] = v unless attrs[attr]==v
|
330
|
+
end
|
331
|
+
FieldDeclaration.new(f.name.to_sym, f.type.to_sym, [], attributes)
|
332
|
+
}
|
333
|
+
[new_fields, modified_fields, deleted_fields]
|
334
|
+
end
|
335
|
+
|
336
|
+
# Break up the lines of a model definition file into sections delimited by the fields declaration.
|
337
|
+
# An empty fields declaration is added to the result if none is present in the file.
|
338
|
+
# The split result is an array with these elements:
|
339
|
+
# * pre: array of lines before the fields declaration
|
340
|
+
# * start_fields: line which opens the fields block
|
341
|
+
# * fields: array of triplets [line, name, comment] with the lines inside th fields block.
|
342
|
+
# Name is the name of the field defined in the line, if any and comment is a comment including in the line;
|
343
|
+
# both name and comment may be absent.
|
344
|
+
# * end_fields: line which closes the fields declaration
|
345
|
+
# * post: array of lines after the fields block
|
346
|
+
# All the lines include a trailing end-of-line separator.
|
347
|
+
def split_model_file(file)
|
348
|
+
code = File.read(file)
|
349
|
+
pre = []
|
350
|
+
start_fields = nil
|
351
|
+
fields = []
|
352
|
+
end_fields = nil
|
353
|
+
post = []
|
354
|
+
state = :pre
|
355
|
+
line_no = 0
|
356
|
+
field_block_end = nil
|
357
|
+
code.each_line do |line|
|
358
|
+
# line.chomp!
|
359
|
+
line_no += 1
|
360
|
+
case state
|
361
|
+
when :pre
|
362
|
+
if line =~ /^\s*fields\s+do(?:\s(.+))?$/
|
363
|
+
field_block_end = /^\s*end(?:\s(.+))?$/
|
364
|
+
start_fields = line
|
365
|
+
state = :fields
|
366
|
+
elsif line =~ /^\s*fields\s+\{(?:\s(.+))?$/
|
367
|
+
field_block_end = /^\s*\}(?:\s(.+))?$/
|
368
|
+
start_fields = line
|
369
|
+
state = :fields
|
370
|
+
else
|
371
|
+
pre << line
|
372
|
+
end
|
373
|
+
when :fields
|
374
|
+
if line =~ field_block_end
|
375
|
+
end_fields = line
|
376
|
+
state = :post
|
377
|
+
else
|
378
|
+
if line =~ /^\s*field\s+:(\w+).+?(#.+)?$/
|
379
|
+
name = $1
|
380
|
+
comment = $2
|
381
|
+
elsif line =~ /^\s*field\s+['"](.+?)['"].+?(#.+)?$/
|
382
|
+
name = $1
|
383
|
+
comment = $2
|
384
|
+
elsif line =~ /^\s*(\w+).+?(#.+)?$/
|
385
|
+
name = $1
|
386
|
+
comment = $2
|
387
|
+
else
|
388
|
+
name = comment = nil
|
389
|
+
end
|
390
|
+
fields << [line, name, comment]
|
391
|
+
end
|
392
|
+
when :post
|
393
|
+
post << line
|
394
|
+
end
|
395
|
+
end
|
396
|
+
if !start_fields
|
397
|
+
i = 0
|
398
|
+
(0...pre.size).each do |i|
|
399
|
+
break if pre[i] =~ /^\s*class\b/
|
400
|
+
end
|
401
|
+
raise "Model declaration not found in #{file}" unless i<pre.size
|
402
|
+
post = pre[i+1..-1]
|
403
|
+
pre = pre[0..i]
|
404
|
+
pre << "\n"
|
405
|
+
start_fields = " fields do\n"
|
406
|
+
end_fields = " end\n"
|
407
|
+
post.unshift "\n" unless post.first.strip.empty?
|
408
|
+
fields = []
|
409
|
+
end
|
410
|
+
[pre,start_fields,fields,end_fields,post]
|
411
|
+
end
|
412
|
+
|
413
|
+
# Write a model definition file from its broken up parts
|
414
|
+
def join_model_file(output_file, pre, start_fields, fields, end_fields, post)
|
415
|
+
File.open(output_file,"w"){ |output|
|
416
|
+
output.write pre*""
|
417
|
+
output.write start_fields
|
418
|
+
output.write fields.map{|f| f.first}*""
|
419
|
+
output.write end_fields
|
420
|
+
output.write post*""
|
421
|
+
}
|
422
|
+
end
|
423
|
+
|
424
|
+
end
|
425
|
+
|
426
|
+
|
427
|
+
|
428
|
+
end
|