modalfields 1.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
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
@@ -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
@@ -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.
@@ -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
+
@@ -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
@@ -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