modalfields 1.1.1

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.
@@ -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