godfat-dm-is-reflective 0.8.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.
- data/CHANGES +188 -0
- data/LICENSE +201 -0
- data/NOTICE +30 -0
- data/README +122 -0
- data/Rakefile +45 -0
- data/TODO +7 -0
- data/lib/dm-is-reflective.rb +18 -0
- data/lib/dm-is-reflective/is/adapters/abstract_adapter.rb +141 -0
- data/lib/dm-is-reflective/is/adapters/mysql_adapter.rb +68 -0
- data/lib/dm-is-reflective/is/adapters/postgres_adapter.rb +90 -0
- data/lib/dm-is-reflective/is/adapters/sqlite3_adapter.rb +59 -0
- data/lib/dm-is-reflective/is/reflective.rb +78 -0
- data/lib/dm-is-reflective/is/version.rb +7 -0
- data/lib/dm-is-reflective/version.rb +7 -0
- data/test/abstract.rb +250 -0
- data/test/test_dm-is-reflexible.rb +48 -0
- metadata +106 -0
data/Rakefile
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'bones'
|
4
|
+
Bones.setup
|
5
|
+
|
6
|
+
PROJ.name = 'dm-is-reflective'
|
7
|
+
PROJ.authors = 'Lin Jen-Shin (aka godfat 真常)'
|
8
|
+
PROJ.email = 'godfat (XD) godfat.org'
|
9
|
+
PROJ.url = "http://github.com/godfat/#{PROJ.name}"
|
10
|
+
PROJ.rubyforge.name = 'ludy'
|
11
|
+
|
12
|
+
PROJ.gem.dependencies << ['dm-core', '>=0.10.0'] << ['extlib', '>=0.9.13']
|
13
|
+
# PROJ.gem.development_dependencies << ['minitest', '>=1.3.0']
|
14
|
+
# PROJ.gem.executables = ["bin/#{PROJ.name}"]
|
15
|
+
|
16
|
+
PROJ.ruby_opts.delete '-w' # too many warnings in addressable, dm-core, extlib...
|
17
|
+
|
18
|
+
PROJ.description = PROJ.summary = paragraphs_of('README', 'description').join("\n\n")
|
19
|
+
PROJ.changes = paragraphs_of('CHANGES', 0..1).join("\n\n")
|
20
|
+
PROJ.version = File.read("lib/#{PROJ.name}/version.rb").gsub(/.*VERSION = '(.*)'.*/m, '\1')
|
21
|
+
|
22
|
+
PROJ.exclude += ['^tmp', 'tmp$', '^pkg', '^\.gitignore$',
|
23
|
+
'^ann-', '\.sqlite3$', '\.db$']
|
24
|
+
|
25
|
+
PROJ.rdoc.remote_dir = PROJ.name
|
26
|
+
|
27
|
+
PROJ.readme_file = 'README'
|
28
|
+
PROJ.rdoc.main = 'README'
|
29
|
+
PROJ.rdoc.exclude += ['Rakefile', '^tasks', '^test']
|
30
|
+
PROJ.rdoc.include << '\w+'
|
31
|
+
# PROJ.rdoc.opts << '--diagram' if !Rake::WIN32 and `which dot` =~ %r/\/dot/
|
32
|
+
PROJ.rdoc.opts += ['--charset=utf-8', '--inline-source',
|
33
|
+
'--line-numbers', '--promiscuous']
|
34
|
+
|
35
|
+
PROJ.spec.opts << '--color'
|
36
|
+
|
37
|
+
PROJ.ann.file = "ann-#{PROJ.name}-#{PROJ.version}"
|
38
|
+
PROJ.ann.paragraphs.concat %w[LINKS SYNOPSIS REQUIREMENTS INSTALL LICENSE]
|
39
|
+
|
40
|
+
CLEAN.include Dir['**/*.rbc']
|
41
|
+
|
42
|
+
task :default do
|
43
|
+
Rake.application.options.show_task_pattern = /./
|
44
|
+
Rake.application.display_tasks_and_comments
|
45
|
+
end
|
data/TODO
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
|
2
|
+
gem 'dm-core', '>=0.10.0'
|
3
|
+
require 'dm-core'
|
4
|
+
|
5
|
+
require 'extlib/hook'
|
6
|
+
require 'extlib/inflection'
|
7
|
+
|
8
|
+
module DataMapper
|
9
|
+
include Extlib::Hook
|
10
|
+
after_class_method :setup do
|
11
|
+
adapter_name = repository.adapter.class.to_s.split('::').last
|
12
|
+
require "dm-is-reflective/is/adapters/#{Extlib::Inflection.underscore(adapter_name)}"
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
|
17
|
+
require 'dm-is-reflective/is/reflective'
|
18
|
+
DataMapper::Model.append_extensions DataMapper::Is::Reflective
|
@@ -0,0 +1,141 @@
|
|
1
|
+
|
2
|
+
module DataMapper
|
3
|
+
module Is::Reflective
|
4
|
+
module AbstractAdapter
|
5
|
+
# returns all tables' name in the repository.
|
6
|
+
# e.g.
|
7
|
+
# ['comments', 'users']
|
8
|
+
def storages
|
9
|
+
raise NotImplementedError
|
10
|
+
end
|
11
|
+
|
12
|
+
# returns all fields, with format [[name, type, attrs]]
|
13
|
+
# e.g.
|
14
|
+
# [[:created_at, DateTime, {:nullable => true}],
|
15
|
+
# [:email, String, {:nullable => true, :size => 255,
|
16
|
+
# :default => 'nospam@nospam.tw'}],
|
17
|
+
# [:id, DataMapper::Types::Serial, {:nullable => false, :serial => true,
|
18
|
+
# :key => true}],
|
19
|
+
# [:salt_first, String, {:nullable => true, :size => 50}],
|
20
|
+
# [:salt_second, String, {:nullable => true, :size => 50}]]
|
21
|
+
def fields storage
|
22
|
+
dmm_query_storage(storage).map{ |field|
|
23
|
+
primitive = dmm_primitive(field)
|
24
|
+
|
25
|
+
type = self.class.type_map.find{ |klass, attrs|
|
26
|
+
next false if [DataMapper::Types::Object, Time].include?(klass)
|
27
|
+
attrs[:primitive] == primitive
|
28
|
+
}
|
29
|
+
type = type ? type.first : dmm_lookup_primitive(primitive)
|
30
|
+
|
31
|
+
attrs = dmm_attributes(field)
|
32
|
+
|
33
|
+
type = if attrs[:serial] && type == Integer
|
34
|
+
DataMapper::Types::Serial
|
35
|
+
|
36
|
+
elsif type == TrueClass
|
37
|
+
DataMapper::Types::Boolean
|
38
|
+
|
39
|
+
else
|
40
|
+
type
|
41
|
+
end
|
42
|
+
|
43
|
+
[dmm_field_name(field).to_sym, type, attrs]
|
44
|
+
}
|
45
|
+
end
|
46
|
+
|
47
|
+
# returns a hash with storage names in keys and
|
48
|
+
# corresponded fields in values. e.g.
|
49
|
+
# {'users' => [[:id, Integer, {:nullable => false,
|
50
|
+
# :serial => true,
|
51
|
+
# :key => true}],
|
52
|
+
# [:email, String, {:nullable => true,
|
53
|
+
# :default => 'nospam@nospam.tw'}],
|
54
|
+
# [:created_at, DateTime, {:nullable => true}],
|
55
|
+
# [:salt_first, String, {:nullable => true, :size => 50}],
|
56
|
+
# [:salt_second, String, {:nullable => true, :size => 50}]]}
|
57
|
+
# see Migration#storages and Migration#fields for detail
|
58
|
+
def storages_and_fields
|
59
|
+
storages.inject({}){ |result, storage|
|
60
|
+
result[storage] = fields(storage)
|
61
|
+
result
|
62
|
+
}
|
63
|
+
end
|
64
|
+
|
65
|
+
# automaticly generate model class(es) and mapping
|
66
|
+
# all fields with mapping /.*/ for you.
|
67
|
+
# e.g.
|
68
|
+
# dm.auto_genclass!
|
69
|
+
# # => [DataMapper::Is::Reflective::User,
|
70
|
+
# # DataMapper::Is::Reflective::SchemaInfo,
|
71
|
+
# # DataMapper::Is::Reflective::Session]
|
72
|
+
#
|
73
|
+
# you can change the scope of generated models:
|
74
|
+
# e.g.
|
75
|
+
# dm.auto_genclass! :scope => Object
|
76
|
+
# # => [User, SchemaInfo, Session]
|
77
|
+
#
|
78
|
+
# you can generate classes for tables you specified only:
|
79
|
+
# e.g.
|
80
|
+
# dm.auto_genclass! :scope => Object, :storages => /^phpbb_/
|
81
|
+
# # => [PhpbbUser, PhpbbPost, PhpbbConfig]
|
82
|
+
#
|
83
|
+
# you can generate classes with String too:
|
84
|
+
# e.g.
|
85
|
+
# dm.auto_genclass! :storages => ['users', 'config'], :scope => Object
|
86
|
+
# # => [User, Config]
|
87
|
+
#
|
88
|
+
# you can generate a class only:
|
89
|
+
# e.g.
|
90
|
+
# dm.auto_genclass! :storages => 'users'
|
91
|
+
# # => [DataMapper::Is::Reflective::User]
|
92
|
+
def auto_genclass! opts = {}
|
93
|
+
opts[:scope] ||= DataMapper::Is::Reflective
|
94
|
+
opts[:storages] ||= /.*/
|
95
|
+
opts[:storages] = [opts[:storages]].flatten
|
96
|
+
|
97
|
+
storages.map{ |storage|
|
98
|
+
|
99
|
+
mapped = opts[:storages].each{ |target|
|
100
|
+
case target
|
101
|
+
when Regexp;
|
102
|
+
break storage if storage =~ target
|
103
|
+
|
104
|
+
when Symbol, String;
|
105
|
+
break storage if storage == target.to_s
|
106
|
+
|
107
|
+
else
|
108
|
+
raise ArgumentError.new("invalid argument: #{target.inspect}")
|
109
|
+
end
|
110
|
+
}
|
111
|
+
|
112
|
+
dmm_genclass mapped, opts[:scope] if mapped.kind_of?(String)
|
113
|
+
}.compact
|
114
|
+
end
|
115
|
+
|
116
|
+
private
|
117
|
+
def dmm_query_storage
|
118
|
+
raise NotImplementError.new("#{self.class}#fields is not implemented.")
|
119
|
+
end
|
120
|
+
|
121
|
+
def dmm_genclass storage, scope
|
122
|
+
model = Class.new
|
123
|
+
model.__send__ :include, DataMapper::Resource
|
124
|
+
model.is(:reflective)
|
125
|
+
model.storage_names[:default] = storage
|
126
|
+
model.__send__ :mapping, /.*/
|
127
|
+
scope.const_set(Extlib::Inflection.classify(storage), model)
|
128
|
+
end
|
129
|
+
|
130
|
+
def dmm_lookup_primitive primitive
|
131
|
+
raise TypeError.new("#{primitive} not found for #{self.class}")
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
module DataMapper
|
138
|
+
module Adapters
|
139
|
+
AbstractAdapter.send(:include, Is::Reflective::AbstractAdapter)
|
140
|
+
end
|
141
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
|
2
|
+
require 'dm-is-reflective/is/adapters/abstract_adapter'
|
3
|
+
|
4
|
+
module DataMapper
|
5
|
+
module Is::Reflective
|
6
|
+
module MysqlAdapter
|
7
|
+
def storages
|
8
|
+
query 'SHOW TABLES'
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
# construct needed table metadata
|
13
|
+
def dmm_query_storage storage
|
14
|
+
sql = <<-SQL.compress_lines
|
15
|
+
SELECT column_name, column_default, is_nullable, data_type,
|
16
|
+
character_maximum_length, column_key, extra
|
17
|
+
FROM `information_schema`.`columns`
|
18
|
+
WHERE `table_schema` = ? AND `table_name` = ?
|
19
|
+
SQL
|
20
|
+
|
21
|
+
query(sql, db_name, storage)
|
22
|
+
end
|
23
|
+
|
24
|
+
def dmm_field_name field
|
25
|
+
field.column_name
|
26
|
+
end
|
27
|
+
|
28
|
+
def dmm_primitive field
|
29
|
+
field.data_type
|
30
|
+
end
|
31
|
+
|
32
|
+
def dmm_attributes field, attrs = {}
|
33
|
+
attrs[:serial] = true if field.extra == 'auto_increment'
|
34
|
+
attrs[:key] = true if field.column_key == 'PRI'
|
35
|
+
attrs[:nullable] = field.is_nullable == 'YES'
|
36
|
+
|
37
|
+
attrs[:default] = field.column_default if
|
38
|
+
field.column_default
|
39
|
+
|
40
|
+
attrs[:length] = field.character_maximum_length if
|
41
|
+
field.character_maximum_length
|
42
|
+
|
43
|
+
attrs
|
44
|
+
end
|
45
|
+
|
46
|
+
def dmm_lookup_primitive primitive
|
47
|
+
p = primitive.upcase
|
48
|
+
|
49
|
+
return Integer if p == 'YEAR'
|
50
|
+
return Integer if p =~ /\w*INT(EGER)?( SIGNED| UNSIGNED)?( ZEROFILL)?/
|
51
|
+
return BigDecimal if p =~ /(DOUBLE|FLOAT|DECIMAL)( SIGNED| UNSIGNED)?( ZEROFILL)?/
|
52
|
+
return String if p =~ /\w*BLOB|\w*BINARY|ENUM|SET|CHAR/
|
53
|
+
return Time if p == 'TIME'
|
54
|
+
return DateTime if p == 'DATETIME'
|
55
|
+
return DataMapper::Types::Boolean if %w[BOOL BOOLEAN].member?(p)
|
56
|
+
return DataMapper::Types::Text if p =~ /\w*TEXT/
|
57
|
+
|
58
|
+
super(primitive)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
module DataMapper
|
65
|
+
module Adapters
|
66
|
+
MysqlAdapter.send(:include, Is::Reflective::MysqlAdapter)
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
|
2
|
+
require 'dm-is-reflective/is/adapters/abstract_adapter'
|
3
|
+
|
4
|
+
module DataMapper
|
5
|
+
module Is::Reflective
|
6
|
+
module PostgresAdapter
|
7
|
+
def storages
|
8
|
+
sql = <<-SQL.compress_lines
|
9
|
+
SELECT table_name FROM "information_schema"."tables"
|
10
|
+
WHERE table_schema = current_schema()
|
11
|
+
SQL
|
12
|
+
|
13
|
+
query(sql)
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
def dmm_query_storage storage
|
18
|
+
sql = <<-SQL.compress_lines
|
19
|
+
SELECT column_name FROM "information_schema"."key_column_usage"
|
20
|
+
WHERE table_schema = current_schema() AND table_name = ?
|
21
|
+
SQL
|
22
|
+
|
23
|
+
keys = query(sql, storage).to_set
|
24
|
+
|
25
|
+
sql = <<-SQL.compress_lines
|
26
|
+
SELECT column_name, column_default, is_nullable,
|
27
|
+
character_maximum_length, udt_name
|
28
|
+
FROM "information_schema"."columns"
|
29
|
+
WHERE table_schema = current_schema() AND table_name = ?
|
30
|
+
SQL
|
31
|
+
|
32
|
+
query(sql, storage).map{ |struct|
|
33
|
+
struct.instance_eval <<-END_EVAL
|
34
|
+
def key?
|
35
|
+
#{keys.member?(struct.column_name)}
|
36
|
+
end
|
37
|
+
END_EVAL
|
38
|
+
struct
|
39
|
+
}
|
40
|
+
end
|
41
|
+
|
42
|
+
def dmm_field_name field
|
43
|
+
field.column_name
|
44
|
+
end
|
45
|
+
|
46
|
+
def dmm_primitive field
|
47
|
+
field.udt_name
|
48
|
+
end
|
49
|
+
|
50
|
+
def dmm_attributes field, attrs = {}
|
51
|
+
# strip data type
|
52
|
+
field.column_default.gsub!(/(.*?)::[\w\s]*/, '\1') if field.column_default
|
53
|
+
|
54
|
+
attrs[:serial] = true if field.column_default =~ /nextval\('\w+'\)/
|
55
|
+
attrs[:key] = true if field.key?
|
56
|
+
attrs[:nullable] = field.is_nullable == 'YES'
|
57
|
+
# strip string quotation
|
58
|
+
attrs[:default] = field.column_default.gsub(/^'(.*?)'$/, '\1') if
|
59
|
+
field.column_default && !attrs[:serial]
|
60
|
+
|
61
|
+
if field.character_maximum_length
|
62
|
+
attrs[:length] = field.character_maximum_length
|
63
|
+
elsif field.udt_name.upcase == 'TEXT'
|
64
|
+
attrs[:length] = DataMapper::Types::Text.size
|
65
|
+
end
|
66
|
+
|
67
|
+
attrs
|
68
|
+
end
|
69
|
+
|
70
|
+
def dmm_lookup_primitive primitive
|
71
|
+
p = primitive.upcase
|
72
|
+
|
73
|
+
return Integer if p =~ /^INT\d+$/
|
74
|
+
return String if p == 'VARCHAR'
|
75
|
+
return DateTime if p == 'TIMESTAMP'
|
76
|
+
return DataMapper::Types::Text if p == 'TEXT'
|
77
|
+
return DataMapper::Types::Boolean if p == 'BOOL'
|
78
|
+
|
79
|
+
super(primitive)
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
module DataMapper
|
87
|
+
module Adapters
|
88
|
+
PostgresAdapter.send(:include, Is::Reflective::PostgresAdapter)
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
|
2
|
+
require 'dm-is-reflective/is/adapters/abstract_adapter'
|
3
|
+
|
4
|
+
module DataMapper
|
5
|
+
module Is::Reflective
|
6
|
+
module Sqlite3Adapter
|
7
|
+
def storages
|
8
|
+
# activerecord-2.1.0/lib/active_record/connection_adapters/sqlite_adapter.rb: 177
|
9
|
+
sql = <<-SQL.compress_lines
|
10
|
+
SELECT name
|
11
|
+
FROM sqlite_master
|
12
|
+
WHERE type = 'table' AND NOT name = 'sqlite_sequence'
|
13
|
+
SQL
|
14
|
+
# activerecord-2.1.0/lib/active_record/connection_adapters/sqlite_adapter.rb: 181
|
15
|
+
|
16
|
+
query sql
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
# alias_method :dmm_query_storages, :query_table
|
21
|
+
def dmm_query_storage *args, &block
|
22
|
+
query_table(*args, &block)
|
23
|
+
end
|
24
|
+
|
25
|
+
def dmm_field_name field
|
26
|
+
field.name
|
27
|
+
end
|
28
|
+
|
29
|
+
def dmm_primitive field
|
30
|
+
field.type.gsub(/\(\d+\)/, '')
|
31
|
+
end
|
32
|
+
|
33
|
+
def dmm_attributes field, attrs = {}
|
34
|
+
if field.pk != 0
|
35
|
+
attrs[:key] = true
|
36
|
+
attrs[:serial] = true if supports_serial?
|
37
|
+
end
|
38
|
+
attrs[:nullable] = field.notnull != 1
|
39
|
+
attrs[:default] = field.dflt_value[1..-2] if field.dflt_value
|
40
|
+
|
41
|
+
if field.type.upcase == 'TEXT'
|
42
|
+
attrs[:length] = DataMapper::Types::Text.size
|
43
|
+
else
|
44
|
+
ergo = field.type.match(/\((\d+)\)/)
|
45
|
+
size = ergo && ergo[1].to_i
|
46
|
+
attrs[:length] = size if size
|
47
|
+
end
|
48
|
+
|
49
|
+
attrs
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
module DataMapper
|
56
|
+
module Adapters
|
57
|
+
Sqlite3Adapter.send(:include, Is::Reflective::Sqlite3Adapter)
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
|
2
|
+
module DataMapper
|
3
|
+
module Is
|
4
|
+
module Reflective
|
5
|
+
|
6
|
+
def is_reflective
|
7
|
+
extend ClassMethod
|
8
|
+
end
|
9
|
+
|
10
|
+
module ClassMethod
|
11
|
+
# it simply calls Migration#fields(self.storage_name)
|
12
|
+
# e.g.
|
13
|
+
# DataMapper.repository.adapter.fields storage_name
|
14
|
+
def fields repo = default_repository_name
|
15
|
+
DataMapper.repository(repo).adapter.fields(storage_name(repo))
|
16
|
+
end
|
17
|
+
|
18
|
+
# it automaticly creates mappings from storage fields to properties.
|
19
|
+
# i.e. you don't have to specify any property if you are connecting
|
20
|
+
# to an existing database.
|
21
|
+
# you can pass it Regexp to map any field it matched, or just
|
22
|
+
# the field name in Symbol or String, or a Class telling it
|
23
|
+
# map any field which type equals to the Class.
|
24
|
+
# returned value is an array of properties indicating fields it mapped
|
25
|
+
# e.g.
|
26
|
+
# class User
|
27
|
+
# include DataMapper::Resource
|
28
|
+
# # mapping all
|
29
|
+
# mapping /.*/ # e.g. => [#<Property:#<Class:0x18f89b8>:id>,
|
30
|
+
# # #<Property:#<Class:0x18f89b8>:title>,
|
31
|
+
# # #<Property:#<Class:0x18f89b8>:body>,
|
32
|
+
# # #<Property:#<Class:0x18f89b8>:user_id>]
|
33
|
+
#
|
34
|
+
# # mapping all (with no argument at all)
|
35
|
+
# mapping
|
36
|
+
#
|
37
|
+
# # mapping for field name ended with _at, and started with salt_
|
38
|
+
# mapping /_at$/, /^salt_/
|
39
|
+
#
|
40
|
+
# # mapping id and email
|
41
|
+
# mapping :id, :email
|
42
|
+
#
|
43
|
+
# # mapping all fields with type String, and id
|
44
|
+
# mapping String, :id
|
45
|
+
#
|
46
|
+
# # mapping login, and all fields with type Integer
|
47
|
+
# mapping :login, Integer
|
48
|
+
# end
|
49
|
+
def mapping *targets
|
50
|
+
targets << /.*/ if targets.empty?
|
51
|
+
|
52
|
+
fields.map{ |field|
|
53
|
+
name, type, attrs = field
|
54
|
+
|
55
|
+
mapped = targets.each{ |target|
|
56
|
+
case target
|
57
|
+
when Regexp;
|
58
|
+
break name if name.to_s =~ target
|
59
|
+
|
60
|
+
when Symbol, String;
|
61
|
+
break name if name == target.to_sym
|
62
|
+
|
63
|
+
when Class;
|
64
|
+
break name if type == target
|
65
|
+
|
66
|
+
else
|
67
|
+
raise ArgumentError.new("invalid argument: #{target.inspect}")
|
68
|
+
end
|
69
|
+
}
|
70
|
+
|
71
|
+
property(mapped, type, attrs) if mapped.kind_of?(Symbol)
|
72
|
+
}.compact
|
73
|
+
end
|
74
|
+
end # of ClassMethod
|
75
|
+
|
76
|
+
end # of Reflective
|
77
|
+
end # of Is
|
78
|
+
end # of DataMapper
|