dm-is-reflective 1.1.0 → 1.2.0

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,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