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.
@@ -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,7 @@
1
+ = dm-is-reflective todo list
2
+
3
+ * test Model#fields with different repo
4
+
5
+ * better doc...
6
+ * 0.7.1 for more data types for postgresql adapter
7
+ * 0.8 automatic determine model relationship
@@ -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