dm-is-reflective 1.1.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,63 @@
1
+
2
+ module DmIsReflective::MysqlAdapter
3
+ include DataMapper
4
+
5
+ def storages
6
+ select('SHOW TABLES')
7
+ end
8
+
9
+ private
10
+ # construct needed table metadata
11
+ def reflective_query_storage storage
12
+ sql = <<-SQL
13
+ SELECT column_name, column_default, is_nullable, data_type,
14
+ character_maximum_length, column_key, extra
15
+ FROM `information_schema`.`columns`
16
+ WHERE `table_schema` = ? AND `table_name` = ?
17
+ SQL
18
+
19
+ # TODO: can we fix this shit in dm-mysql-adapter?
20
+ path = options[:path] || options['path'] ||
21
+ options[:database] || options['database']
22
+
23
+ select(Ext::String.compress_lines(sql), path.sub('/', ''), storage)
24
+ end
25
+
26
+ def reflective_field_name field
27
+ field.column_name
28
+ end
29
+
30
+ def reflective_primitive field
31
+ field.data_type
32
+ end
33
+
34
+ def reflective_attributes field, attrs = {}
35
+ attrs[:serial] = true if field.extra == 'auto_increment'
36
+ attrs[:key] = true if field.column_key == 'PRI'
37
+
38
+ attrs[:allow_nil] = field.is_nullable == 'YES'
39
+ attrs[:default] = field.column_default if
40
+ field.column_default
41
+
42
+ attrs[:length] = field.character_maximum_length if
43
+ field.character_maximum_length
44
+
45
+ attrs
46
+ end
47
+
48
+ def reflective_lookup_primitive primitive
49
+ case primitive.upcase
50
+ when 'YEAR' ; Integer
51
+ when /\w*INT(EGER)?( SIGNED| UNSIGNED)?( ZEROFILL)?/
52
+ ; Integer
53
+ when /(DOUBLE|FLOAT|DECIMAL)( SIGNED| UNSIGNED)?( ZEROFILL)?/
54
+ ; Property::Decimal
55
+ when /\w*BLOB|\w*BINARY|ENUM|SET|CHAR/; String
56
+ when 'TIME' ; Time
57
+ when 'DATE' ; Date
58
+ when 'DATETIME', 'TIMESTAMP' ; DateTime
59
+ when 'BOOL', 'BOOLEAN' ; Property::Boolean
60
+ when /\w*TEXT/ ; Property::Text
61
+ end || super(primitive)
62
+ end
63
+ end
@@ -0,0 +1,80 @@
1
+
2
+ module DmIsReflective::PostgresAdapter
3
+ include DataMapper
4
+
5
+ def storages
6
+ sql = <<-SQL
7
+ SELECT table_name FROM "information_schema"."tables"
8
+ WHERE table_schema = current_schema()
9
+ SQL
10
+
11
+ select(Ext::String.compress_lines(sql))
12
+ end
13
+
14
+ private
15
+ def reflective_query_storage storage
16
+ sql = <<-SQL
17
+ SELECT column_name FROM "information_schema"."key_column_usage"
18
+ WHERE table_schema = current_schema() AND table_name = ?
19
+ SQL
20
+
21
+ keys = select(Ext::String.compress_lines(sql), storage).to_set
22
+
23
+ sql = <<-SQL
24
+ SELECT column_name, column_default, is_nullable,
25
+ character_maximum_length, udt_name
26
+ FROM "information_schema"."columns"
27
+ WHERE table_schema = current_schema() AND table_name = ?
28
+ SQL
29
+
30
+ select(Ext::String.compress_lines(sql), storage).map{ |struct|
31
+ struct.instance_eval <<-RUBY
32
+ def key?
33
+ #{keys.member?(struct.column_name)}
34
+ end
35
+ RUBY
36
+ struct
37
+ }
38
+ end
39
+
40
+ def reflective_field_name field
41
+ field.column_name
42
+ end
43
+
44
+ def reflective_primitive field
45
+ field.udt_name
46
+ end
47
+
48
+ def reflective_attributes field, attrs = {}
49
+ # strip data type
50
+ field.column_default.gsub!(/(.*?)::[\w\s]*/, '\1') if
51
+ field.column_default
52
+
53
+ attrs[:serial] = true if field.column_default =~ /nextval\('\w+'\)/
54
+ attrs[:key] = true if field.key?
55
+ attrs[:allow_nil] = field.is_nullable == 'YES'
56
+ # strip string quotation
57
+ attrs[:default] = field.column_default.gsub(/^'(.*?)'$/, '\1') if
58
+ field.column_default && !attrs[:serial]
59
+
60
+ if field.character_maximum_length
61
+ attrs[:length] = field.character_maximum_length
62
+ elsif field.udt_name.upcase == 'TEXT'
63
+ attrs[:length] = Property::Text.length
64
+ end
65
+
66
+ attrs
67
+ end
68
+
69
+ def reflective_lookup_primitive primitive
70
+ case primitive.upcase
71
+ when /^INT\d+$/ ; Integer
72
+ when /^FLOAT\d+$/ ; Float
73
+ when 'VARCHAR', 'BPCHAR'; String
74
+ when 'TIMESTAMP', 'DATE'; DateTime
75
+ when 'TEXT' ; Property::Text
76
+ when 'BOOL' ; Property::Boolean
77
+ when 'NUMERIC' ; Property::Decimal
78
+ end || super(primitive)
79
+ end
80
+ end
@@ -0,0 +1,57 @@
1
+
2
+ module DmIsReflective::SqliteAdapter
3
+ include DataMapper
4
+
5
+ def storages
6
+ sql = <<-SQL
7
+ SELECT name
8
+ FROM sqlite_master
9
+ WHERE type = 'table' AND NOT name = 'sqlite_sequence'
10
+ SQL
11
+
12
+ select(Ext::String.compress_lines(sql))
13
+ end
14
+
15
+ private
16
+ def reflective_query_storage storage
17
+ select('PRAGMA table_info(?)', storage)
18
+ end
19
+
20
+ def reflective_field_name field
21
+ field.name
22
+ end
23
+
24
+ def reflective_primitive field
25
+ field.type.gsub(/\(\d+\)/, '')
26
+ end
27
+
28
+ def reflective_attributes field, attrs = {}
29
+ if field.pk != 0
30
+ attrs[:key] = true
31
+ attrs[:serial] = true
32
+ end
33
+ attrs[:allow_nil] = field.notnull == 0
34
+ attrs[:default] = field.dflt_value[1..-2] if field.dflt_value
35
+
36
+ if field.type.upcase == 'TEXT'
37
+ attrs[:length] = Property::Text.length
38
+ else
39
+ ergo = field.type.match(/\((\d+)\)/)
40
+ size = ergo && ergo[1].to_i
41
+ attrs[:length] = size if size
42
+ end
43
+
44
+ attrs
45
+ end
46
+
47
+ def reflective_lookup_primitive primitive
48
+ case primitive.upcase
49
+ when 'INTEGER' ; Integer
50
+ when 'REAL', 'NUMERIC'; Float
51
+ when 'VARCHAR' ; String
52
+ when 'TIMESTAMP' ; DateTime
53
+ when 'BOOLEAN' ; Property::Boolean
54
+ when 'TEXT' ; Property::Text
55
+ end || super(primitive)
56
+ end
57
+ end
@@ -1,7 +1,8 @@
1
1
 
2
- module DataMapper
3
- module Is
4
- module Reflective
2
+ module DmIsReflective
3
+ autoload :VERSION, 'dm-is-reflective/version'
4
+
5
+ include DataMapper
5
6
 
6
7
  def is_reflective
7
8
  extend ClassMethod
@@ -74,8 +75,18 @@ module Reflective
74
75
  finalize if respond_to?(:finalize)
75
76
  result
76
77
  end
77
- end # of ClassMethod
78
78
 
79
- end # of Reflective
80
- end # of Is
81
- end # of DataMapper
79
+ def to_source scope=nil
80
+ <<-RUBY
81
+ class #{scope}::#{name} < #{superclass}
82
+ include DataMapper::Resource
83
+ #{
84
+ properties.map do |prop|
85
+ "property :#{prop.name}, #{prop.class.name}, #{prop.options}"
86
+ end.join("\n")
87
+ }
88
+ end
89
+ RUBY
90
+ end
91
+ end # of ClassMethod
92
+ end # of DmIsReflective
@@ -0,0 +1,87 @@
1
+
2
+ require 'dm-is-reflective'
3
+
4
+ module DmIsReflective::Runner
5
+ module_function
6
+ def options
7
+ @options ||=
8
+ [['-s, --scope SCOPE' ,
9
+ 'SCOPE where the models should go (default: Object)' ],
10
+ ['-o, --output DIRECTORY' ,
11
+ 'DIRECTORY where the output goes (default: dm-is-reflective)'],
12
+ ['-h, --help' , 'Print this message' ],
13
+ ['-v, --version', 'Print the version' ]]
14
+ end
15
+
16
+ def run argv=ARGV
17
+ puts(help) and exit if argv.empty?
18
+ generate(*parse(argv))
19
+ end
20
+
21
+ def generate uri, scope, output
22
+ require 'fileutils'
23
+ FileUtils.mkdir_p(output)
24
+ DataMapper.setup(:default, uri).auto_genclass!(:scope => scope).
25
+ each do |model|
26
+ path = "#{output}/#{model.name.gsub(/::/, '').
27
+ gsub(/([A-Z])/, '_\1').
28
+ downcase[1..-1]}.rb"
29
+ File.open(path, 'w') do |file|
30
+ file.puts model.to_source
31
+ end
32
+ end
33
+ end
34
+
35
+ def parse argv
36
+ uri, scope, output = ['sqlite::memory:', Object, 'dm-is-reflective']
37
+ until argv.empty?
38
+ case arg = argv.shift
39
+ when /^-s=?(.+)?/, /^--scope=?(.+)?/
40
+ name = $1 || argv.shift
41
+ scope = if Object.const_defined?(name)
42
+ Object.const_get(name)
43
+ else
44
+ mkconst_p(name)
45
+ end
46
+
47
+ when /^-o=?(.+)?/, /^--output=?(.+)?/
48
+ output = $1 || argv.shift
49
+
50
+ when /^-h/, '--help'
51
+ puts(help)
52
+ exit
53
+
54
+ when /^-v/, '--version'
55
+ puts(DmIsReflective::VERSION)
56
+ exit
57
+
58
+ else
59
+ uri = arg
60
+ end
61
+ end
62
+ [uri, scope, output]
63
+ end
64
+
65
+ def mkconst_p name
66
+ name.split('::').inject(Object) do |ret, mod|
67
+ if Object.const_defined?(mod)
68
+ ret.const_get(mod)
69
+ else
70
+ ret.const_set(mod, Module.new)
71
+ end
72
+ end
73
+ end
74
+
75
+ def help
76
+ maxn = options.transpose.first.map(&:size).max
77
+ maxd = options.transpose.last .map(&:size).max
78
+ "Usage: dm-is-reflective DATABASE_URI\n" +
79
+ options.map{ |(name, desc)|
80
+ if desc.empty?
81
+ name
82
+ else
83
+ sprintf(" %-*s %-*s", maxn, name, maxd, desc)
84
+ end
85
+ }.join("\n")
86
+ end
87
+ end
@@ -0,0 +1,279 @@
1
+
2
+ require 'bacon'
3
+ Bacon.summary_on_exit
4
+
5
+ require 'dm-core'
6
+ require 'dm-migrations'
7
+ require 'dm-is-reflective'
8
+
9
+ module Abstract
10
+ class User
11
+ include DataMapper::Resource
12
+ has n, :comments
13
+
14
+ property :id, Serial
15
+ property :login, String, :length => 70
16
+ property :sig, Text
17
+ property :created_at, DateTime
18
+
19
+ is :reflective
20
+ end
21
+
22
+ class SuperUser
23
+ include DataMapper::Resource
24
+ property :id, Serial
25
+ property :bool, Boolean
26
+
27
+ is :reflective
28
+ end
29
+
30
+ class Comment
31
+ include DataMapper::Resource
32
+ belongs_to :user, :required => false
33
+
34
+ property :id, Serial
35
+ property :title, String, :length => 50, :default => 'default title',
36
+ :allow_nil => false
37
+ property :body, Text
38
+
39
+ is :reflective
40
+ end
41
+
42
+ Tables = ['abstract_comments', 'abstract_super_users', 'abstract_users']
43
+
44
+ AttrCommon = {:allow_nil => true}
45
+ AttrCommonPK = {:serial => true, :key => true, :allow_nil => false}
46
+ AttrText = {:length => 65535}.merge(AttrCommon)
47
+
48
+ def self.next_id
49
+ @id ||= 0
50
+ @id += 1
51
+ end
52
+ end
53
+
54
+ include Abstract
55
+
56
+ shared :reflective do
57
+ def user_fields
58
+ [[:created_at, DateTime, AttrCommon],
59
+ [:id, DataMapper::Property::Serial, AttrCommonPK],
60
+ [:login, String, {:length => 70}.merge(AttrCommon)],
61
+ [:sig, DataMapper::Property::Text, AttrText]]
62
+ end
63
+
64
+ def comment_fields
65
+ [[:body, DataMapper::Property::Text, AttrText],
66
+ [:id, DataMapper::Property::Serial, AttrCommonPK],
67
+ [:title, String, {:length => 50, :default => 'default title',
68
+ :allow_nil => false}],
69
+ [:user_id, Integer, AttrCommon]]
70
+ end
71
+
72
+ # there's differences between adapters
73
+ def super_user_fields
74
+ mysql = defined?(DataMapper::Adapters::MysqlAdapter) &&
75
+ DataMapper::Adapters::MysqlAdapter
76
+ case DataMapper.repository.adapter
77
+ when mysql
78
+ # Mysql couldn't tell it's boolean or tinyint
79
+ [[:bool, Integer, AttrCommon],
80
+ [:id, DataMapper::Property::Serial, AttrCommonPK]]
81
+
82
+ else
83
+ [[:bool, DataMapper::Property::Boolean, AttrCommon],
84
+ [:id, DataMapper::Property::Serial, AttrCommonPK]]
85
+ end
86
+ end
87
+
88
+ before do
89
+ @dm = setup_data_mapper
90
+ [User, Comment, SuperUser].each(&:auto_migrate!)
91
+ end
92
+
93
+ def sort_fields fields
94
+ fields.sort_by{ |f| f.first.to_s }
95
+ end
96
+
97
+ def create_fake_model
98
+ model = Class.new
99
+ model.module_eval do
100
+ include DataMapper::Resource
101
+ property :id, DataMapper::Property::Serial
102
+ is :reflective
103
+ end
104
+ Abstract.const_set("Model#{Abstract.next_id}", model)
105
+ [model, setup_data_mapper]
106
+ end
107
+
108
+ def new_scope
109
+ Abstract.const_set("Scope#{Abstract.next_id}", Module.new)
110
+ end
111
+
112
+ def test_create_comment
113
+ Comment.create(:title => 'XD')
114
+ Comment.first.title.should.eq 'XD'
115
+ end
116
+
117
+ def test_create_user
118
+ now = Time.now
119
+ User.create(:created_at => now)
120
+ User.first.created_at.asctime.should.eq now.asctime
121
+ now
122
+ end
123
+
124
+ should 'create comment' do
125
+ test_create_comment
126
+ end
127
+
128
+ should 'create user' do
129
+ test_create_user
130
+ end
131
+
132
+ should 'storages' do
133
+ @dm.storages.sort.should.eq Tables
134
+ sort_fields(@dm.fields('abstract_comments')).should.eq comment_fields
135
+ end
136
+
137
+ should 'reflect all' do
138
+ test_create_comment # for fixtures
139
+ model, local_dm = create_fake_model
140
+ model.storage_names[:default] = 'abstract_comments'
141
+
142
+ local_dm.storages.sort.should.eq Tables
143
+ model.storage_name.should.eq 'abstract_comments'
144
+
145
+ model.send :reflect
146
+ model.all.size .should.eq 1
147
+ sort_fields(model.fields).should.eq comment_fields
148
+ model.first.title .should.eq 'XD'
149
+ end
150
+
151
+ should 'reflect and create' do
152
+ model, local_dm = create_fake_model
153
+ model.storage_names[:default] = 'abstract_comments'
154
+ model.send :reflect
155
+
156
+ model.create(:title => 'orz')
157
+ model.first.title.should.eq 'orz'
158
+
159
+ model.create
160
+ model.last.title.should.eq 'default title'
161
+ end
162
+
163
+ should 'storages and fields' do
164
+ sort_fields(@dm.fields('abstract_users')).should.eq user_fields
165
+
166
+ @dm.storages_and_fields.inject({}){ |r, i|
167
+ key, value = i
168
+ r[key] = value.sort_by{ |v| v.first.to_s }
169
+ r
170
+ }.should.eq('abstract_users' => user_fields ,
171
+ 'abstract_comments' => comment_fields ,
172
+ 'abstract_super_users' => super_user_fields)
173
+ end
174
+
175
+ should 'reflect type' do
176
+ model, local_dm = create_fake_model
177
+ model.storage_names[:default] = 'abstract_comments'
178
+
179
+ model.send :reflect, DataMapper::Property::Serial
180
+ model.properties.map(&:name).map(&:to_s).sort.should.eq ['id']
181
+
182
+ model.send :reflect, Integer
183
+ model.properties.map(&:name).map(&:to_s).sort.should.eq \
184
+ ['id', 'user_id']
185
+ end
186
+
187
+ should 'reflect multiple' do
188
+ model, local_dm = create_fake_model
189
+ model.storage_names[:default] = 'abstract_users'
190
+ model.send :reflect, :login, DataMapper::Property::Serial
191
+
192
+ model.properties.map(&:name).map(&:to_s).sort.should.eq \
193
+ ['id', 'login']
194
+ end
195
+
196
+ should 'reflect regexp' do
197
+ model, local_dm = create_fake_model
198
+ model.storage_names[:default] = 'abstract_comments'
199
+ model.send :reflect, /id$/
200
+
201
+ model.properties.map(&:name).map(&:to_s).sort.should.eq \
202
+ ['id', 'user_id']
203
+ end
204
+
205
+ should 'raise ArgumentError when giving invalid argument' do
206
+ lambda{
207
+ User.send :reflect, 29
208
+ }.should.raise ArgumentError
209
+ end
210
+
211
+ should 'allow empty string' do
212
+ Comment.new(:title => '').save.should.eq true
213
+ end
214
+
215
+ should 'auto_genclasses' do
216
+ scope = new_scope
217
+ @dm.auto_genclass!(:scope => scope).map(&:to_s).sort.should.eq \
218
+ ["#{scope}::AbstractComment",
219
+ "#{scope}::AbstractSuperUser",
220
+ "#{scope}::AbstractUser"]
221
+
222
+ comment = scope.const_get('AbstractComment')
223
+
224
+ sort_fields(comment.fields).should.eq comment_fields
225
+
226
+ test_create_comment
227
+
228
+ comment.first.title.should.eq 'XD'
229
+ comment.create(:title => 'orz', :body => 'dm-reflect')
230
+ comment.last.body.should.eq 'dm-reflect'
231
+ end
232
+
233
+ should 'auto_genclass' do
234
+ scope = new_scope
235
+ @dm.auto_genclass!(:scope => scope,
236
+ :storages => 'abstract_users').map(&:to_s).should.eq \
237
+ ["#{scope}::AbstractUser"]
238
+
239
+ user = scope.const_get('AbstractUser')
240
+ sort_fields(user.fields).should.eq user_fields
241
+
242
+ now = test_create_user
243
+
244
+ user.first.created_at.asctime.should.eq now.asctime
245
+ user.create(:login => 'godfat')
246
+ user.last.login.should.eq 'godfat'
247
+ end
248
+
249
+ should 'auto_genclass with regexp' do
250
+ scope = new_scope
251
+ @dm.auto_genclass!(:scope => scope,
252
+ :storages => /_users$/).map(&:to_s).sort.should.eq \
253
+ ["#{scope}::AbstractSuperUser", "#{scope}::AbstractUser"]
254
+
255
+ user = scope.const_get('AbstractSuperUser')
256
+ sort_fields(user.fields).should.eq sort_fields(SuperUser.fields)
257
+ end
258
+
259
+ should 'reflect return value' do
260
+ model, local_dm = create_fake_model
261
+ model.storage_names[:default] = 'abstract_comments'
262
+ mapped = model.send :reflect, /.*/
263
+
264
+ mapped.map(&:object_id).sort.should.eq \
265
+ model.properties.map(&:object_id).sort
266
+ end
267
+ end
268
+
269
+ module Kernel
270
+ def eq? rhs
271
+ self == rhs
272
+ end
273
+
274
+ def require_adapter adapter
275
+ require "dm-#{adapter}-adapter"
276
+ rescue LoadError
277
+ puts "skip #{adapter} test since it's not installed"
278
+ end
279
+ end