dm-reflection 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,326 @@
1
+ module DataMapper
2
+ module Reflection
3
+ module Builders
4
+ module Source
5
+
6
+ def to_ruby
7
+ Model.to_ruby(self)
8
+ end
9
+
10
+ class Model
11
+
12
+ attr_accessor :model
13
+
14
+ def self.to_ruby(model)
15
+ builder = new(model)
16
+ raise DataMapper::IncompleteModelError unless builder.complete_model?
17
+ builder.to_ruby
18
+ end
19
+
20
+
21
+ def to_ruby
22
+ ruby = "class #{model}\n"
23
+ ruby += "\n"
24
+ ruby += " include DataMapper::Resource\n"
25
+ property_definitions(key_properties).each do |key_definition|
26
+ ruby += "\n #{key_definition}"
27
+ end
28
+ ruby += "\n"
29
+ property_definitions(foreign_key_properties).each do |foreign_key_definition|
30
+ ruby += "\n #{foreign_key_definition}"
31
+ end
32
+ property_definitions(regular_properties).each do |property_definition|
33
+ ruby += "\n #{property_definition}"
34
+ end
35
+ ruby += "\n"
36
+ relationship_definitions(many_to_one_relationships).each do |relationship_definition|
37
+ ruby += "\n #{relationship_definition}"
38
+ end
39
+ relationship_definitions(one_to_one_relationships).each do |relationship_definition|
40
+ ruby += "\n #{relationship_definition}"
41
+ end
42
+ relationship_definitions(one_to_many_relationships).each do |relationship_definition|
43
+ ruby += "\n #{relationship_definition}"
44
+ end
45
+ relationship_definitions(many_to_many_relationships).each do |relationship_definition|
46
+ ruby += "\n #{relationship_definition}"
47
+ end
48
+ ruby += "\n"
49
+ ruby += "\nend\n"
50
+ end
51
+
52
+
53
+
54
+ def key_properties
55
+ model.key
56
+ end
57
+
58
+ def foreign_key_properties
59
+ []
60
+ end
61
+
62
+ def regular_properties
63
+ model.properties - model.key - foreign_key_properties
64
+ end
65
+
66
+
67
+ def many_to_one_relationships(repository = :default)
68
+ model.relationships(repository).select do |_, relationship|
69
+ relationship.is_a?(DataMapper::Associations::ManyToOne::Relationship)
70
+ end
71
+ end
72
+
73
+ def one_to_one_relationships(repository = :default)
74
+ model.relationships(repository).select do |_, relationship|
75
+ relationship.is_a?(DataMapper::Associations::OneToOne::Relationship)
76
+ end
77
+ end
78
+
79
+ def one_to_many_relationships(repository = :default)
80
+ model.relationships(repository).select do |_, relationship|
81
+ relationship.is_a?(DataMapper::Associations::OneToMany::Relationship) &&
82
+ !relationship.is_a?(DataMapper::Associations::ManyToMany::Relationship)
83
+ end
84
+ end
85
+
86
+ def many_to_many_relationships(repository = :default)
87
+ model.relationships(repository).select do |_, relationship|
88
+ relationship.is_a?(DataMapper::Associations::ManyToMany::Relationship)
89
+ end
90
+ end
91
+
92
+
93
+ def complete_model?
94
+ key_properties.any?
95
+ end
96
+
97
+
98
+ private
99
+
100
+ def initialize(model)
101
+ @model = model
102
+ end
103
+
104
+ def property_definitions(properties)
105
+ properties.map { |property| Property.for(property).to_ruby }
106
+ end
107
+
108
+ def relationship_definitions(relationships)
109
+ relationships.map { |_, relationship| Relationship.for(relationship).to_ruby }
110
+ end
111
+
112
+ end
113
+
114
+
115
+ module OptionBuilder
116
+
117
+ def options
118
+ option_string(prioritized_options + rest_options)
119
+ end
120
+
121
+ def option_priorities
122
+ []
123
+ end
124
+
125
+ def prioritized_options
126
+ []
127
+ end
128
+
129
+ def rest_options
130
+ backend_options.select { |k,v| !option_priorities.include?(k) && !irrelevant_options.include?(k) }
131
+ end
132
+
133
+ def irrelevant_options
134
+ []
135
+ end
136
+
137
+ def backend_options
138
+ @backend_options ||= backend.options.dup
139
+ end
140
+
141
+
142
+ private
143
+
144
+ def option_string(relevant_options)
145
+ relevant_options.map do |key, value|
146
+ ", #{key.inspect} => #{value == 'Resource' ? value : value.inspect}"
147
+ end.join
148
+ end
149
+
150
+ end
151
+
152
+
153
+ class Property
154
+
155
+ include OptionBuilder
156
+
157
+ attr_accessor :backend
158
+
159
+ def self.for(backend)
160
+ new(backend)
161
+ end
162
+
163
+ def to_ruby
164
+ "property :#{name}, #{type}#{options}"
165
+ end
166
+
167
+ def name
168
+ backend.name
169
+ end
170
+
171
+ def type
172
+ Extlib::Inflection.demodulize(backend.type)
173
+ end
174
+
175
+
176
+ def options
177
+ backend.type == DataMapper::Types::Serial ? '' : super
178
+ end
179
+
180
+ def option_priorities
181
+ [ :key, :required, :unique, :unique_index ]
182
+ end
183
+
184
+ def prioritized_options
185
+ option_priorities.inject([]) do |memo, name|
186
+ if name == :required
187
+ memo << [ name, backend_options[name] ] if backend_options[name] && !backend_options[:key]
188
+ else
189
+ memo << [ name, backend_options[name] ] if backend_options[name]
190
+ end
191
+ memo
192
+ end
193
+ end
194
+
195
+ def irrelevant_options
196
+ []
197
+ end
198
+
199
+ private
200
+
201
+ def initialize(backend)
202
+ @backend = backend
203
+ end
204
+
205
+ end
206
+
207
+ class Relationship
208
+
209
+ include OptionBuilder
210
+
211
+ attr_accessor :backend
212
+
213
+ def self.for(backend)
214
+ case backend
215
+ when DataMapper::Associations::ManyToOne::Relationship then ManyToOne. new(backend)
216
+ when DataMapper::Associations::OneToOne::Relationship then OneToOne. new(backend)
217
+ when DataMapper::Associations::OneToMany::Relationship then OneToMany. new(backend)
218
+ when DataMapper::Associations::ManyToMany::Relationship then ManyToMany.new(backend)
219
+ else
220
+ raise ArgumentError, "#{backend.class} is no valid datamapper relationship"
221
+ end
222
+ end
223
+
224
+ def to_ruby
225
+ "#{type} #{cardinality}, :#{name}#{options}"
226
+ end
227
+
228
+
229
+ def type
230
+ raise NotImplementedError
231
+ end
232
+
233
+ def name
234
+ backend.name
235
+ end
236
+
237
+
238
+ def cardinality
239
+ "#{min}..#{max}"
240
+ end
241
+
242
+ def min
243
+ backend.min
244
+ end
245
+
246
+ def max
247
+ backend.max
248
+ end
249
+
250
+
251
+ def option_priorities
252
+ [ :through, :constraint ]
253
+ end
254
+
255
+ def prioritized_options
256
+ option_priorities.inject([]) do |memo, name|
257
+ if name == :through && through = backend_options[:through]
258
+ value = through.is_a?(Symbol) ? through : Extlib::Inflection.demodulize(through)
259
+ memo << [ :through, value ]
260
+ end
261
+ memo
262
+ end
263
+ end
264
+
265
+ def irrelevant_options
266
+ [ :min, :max, :parent_repository_name, :child_repository_name ]
267
+ end
268
+
269
+ private
270
+
271
+ def initialize(backend)
272
+ @backend = backend
273
+ end
274
+
275
+ end
276
+
277
+ class ManyToOne < Relationship
278
+ def to_ruby
279
+ "#{type} :#{name}#{options}"
280
+ end
281
+ def type
282
+ :belongs_to
283
+ end
284
+ def cardinality
285
+ ''
286
+ end
287
+ end
288
+
289
+ class OneToOne < Relationship
290
+ def type
291
+ :has
292
+ end
293
+ def cardinality
294
+ min == 1 && max == 1 ? '1' : super
295
+ end
296
+ end
297
+
298
+ class OneToMany < Relationship
299
+ def type
300
+ :has
301
+ end
302
+ def cardinality
303
+ max_string = max == Infinity ? 'n' : max.to_s
304
+
305
+ if min == 0 || min == max
306
+ max_string
307
+ elsif max == Infinity
308
+ "#{min}..#{max_string}"
309
+ else
310
+ super
311
+ end
312
+ end
313
+ end
314
+
315
+ class ManyToMany < OneToMany
316
+ end
317
+
318
+
319
+ end
320
+ end
321
+ end
322
+
323
+ # Ensure activation when this file is required
324
+ Model.append_extensions(Reflection::Builders::Source)
325
+
326
+ end
@@ -0,0 +1,73 @@
1
+ module DataMapper
2
+ module Reflection
3
+ ##
4
+ # Main reflection method reflects models out of a repository.
5
+ # @param [Slug] repository is the key to the repository that will be reflected.
6
+ # @param [Constant] namespace is the namespace into which the reflected models will be added
7
+ # @param [Boolean] overwrite indicates the reflected models should replace existing models or not.
8
+ # @return [DataMapper::Model Array] the reflected models.
9
+ #
10
+ def self.reflect(repository, namespace = Object, overwrite = false)
11
+ adapter = DataMapper.repository(repository).adapter
12
+
13
+ models = []
14
+
15
+ adapter.get_storage_names.each do |storage_name|
16
+ namespace_parts = storage_name.split('__').map do |part|
17
+ Extlib::Inflection.classify(part)
18
+ end
19
+
20
+ model_name = namespace_parts.pop
21
+
22
+ namespace = if namespace_parts.any?
23
+ Object.make_module(namespace_parts.join('::'))
24
+ else
25
+ Object
26
+ end
27
+
28
+ next if namespace.const_defined?(model_name) && !overwrite
29
+
30
+ anonymous_model = DataMapper::Model.new do
31
+ class_eval <<-RUBY, __FILE__, __LINE__
32
+ storage_names[#{repository.inspect}]='#{storage_name}'
33
+ RUBY
34
+ unless repository == DataMapper::Repository.default_name
35
+ class_eval <<-RUBY, __FILE__, __LINE__
36
+ def self.default_repository_name
37
+ #{repository.inspect}
38
+ end
39
+ RUBY
40
+ end
41
+ end
42
+
43
+ model = namespace.const_set(model_name, anonymous_model)
44
+
45
+ adapter.get_properties(storage_name).each do |attribute|
46
+ attribute.delete_if { |k,v| v.nil? }
47
+ model.property(attribute.delete(:name).to_sym, attribute.delete(:type), attribute)
48
+ end
49
+
50
+ models << model
51
+ end
52
+ models
53
+ end
54
+ end # module Reflection
55
+
56
+ module Adapters
57
+ extendable do
58
+ ##
59
+ # Glue method that will register reflection extensions for adapters if the adapters are loaded.
60
+ #
61
+ # @param [Constant] const_name is the constant defined by the adapter.
62
+ #
63
+ # @api private
64
+ def const_added(const_name)
65
+ if DataMapper::Reflection.const_defined?(const_name)
66
+ adapter = const_get(const_name)
67
+ adapter.send(:include, DataMapper::Reflection.const_get(const_name))
68
+ end
69
+ super
70
+ end # const_added
71
+ end # extendable block
72
+ end # module Adapters
73
+ end # module DataMapper
@@ -0,0 +1,5 @@
1
+ module DataMapper
2
+ module Reflection
3
+ VERSION = '0.0.5'.freeze
4
+ end
5
+ end
@@ -0,0 +1,6 @@
1
+ require 'dm-reflection/reflection'
2
+ require 'dm-reflection/adapters/sqlite3'
3
+ require 'dm-reflection/adapters/persevere'
4
+ require 'dm-reflection/adapters/mysql'
5
+ require 'dm-reflection/adapters/postgres'
6
+ require 'dm-reflection/builders/source_builder'
@@ -0,0 +1,86 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+
3
+ if ENV['ADAPTER'] == 'persevere'
4
+
5
+ POST = <<-RUBY
6
+ class Post
7
+
8
+ include DataMapper::Resource
9
+
10
+ property :id, Serial
11
+
12
+ property :created_at, DateTime
13
+ property :body, Text, :length => 65535, :lazy => true
14
+ property :updated_at, DateTime
15
+
16
+
17
+ end
18
+ RUBY
19
+
20
+ POST_COMMENT = <<-RUBY
21
+ class PostComment
22
+
23
+ include DataMapper::Resource
24
+
25
+ property :id, Serial
26
+
27
+ property :created_at, DateTime
28
+ property :body, Text, :length => 65535, :lazy => true
29
+ property :updated_at, DateTime
30
+ property :score, Integer
31
+ property :blog_post, DataMapper::Types::JsonReference, :reference => :Post
32
+
33
+
34
+ end
35
+ RUBY
36
+
37
+ PERSEVERE_REFLECTION_SOURCES = [ POST, POST_COMMENT ]
38
+
39
+ describe 'The Persevere DataMapper reflection module' do
40
+
41
+ before(:each) do
42
+ @adapter = repository(:default).adapter
43
+
44
+ PERSEVERE_REFLECTION_SOURCES.each { |source| eval(source) }
45
+
46
+ @models = {
47
+ :Post => POST,
48
+ :PostComment => POST_COMMENT,
49
+ }
50
+
51
+ @models.each_key { |model| Extlib::Inflection.constantize(model.to_s).auto_migrate! }
52
+ Post.send(:property, :comments, DataMapper::Types::JsonReferenceCollection, :reference => :PostComment)
53
+ Post.auto_upgrade!
54
+ @models.each_key { |model| remove_model_from_memory( Extlib::Inflection.constantize(model.to_s) ) }
55
+ end
56
+
57
+ after(:each) do
58
+ @models.each_key do |model_name|
59
+ next unless Object.const_defined?(model_name)
60
+ model = Extlib::Inflection.constantize(model_name.to_s)
61
+ remove_model_from_memory(model)
62
+ end
63
+ @models.each_key do |model_name|
64
+ next unless Object.const_defined?(model_name)
65
+ model = Extlib::Inflection.constantize(model_name.to_s)
66
+ remove_model_from_memory(model)
67
+ end
68
+ end
69
+
70
+ it "should return an array of tables" do
71
+ @adapter.get_storage_names.should be_kind_of(Array)
72
+ end
73
+
74
+ it "should return a table definition" do
75
+ result = @adapter.get_properties("post")
76
+ result.should be_kind_of(Array)
77
+ end
78
+
79
+ it "should reflect out JsonReference types" do
80
+ DataMapper::Reflection.reflect(:default)
81
+ PostComment.properties[:blog_post].type.should eql DataMapper::Types::JsonReference
82
+ Post.properties[:comments].type.should eql DataMapper::Types::JsonReferenceCollection
83
+ end
84
+
85
+ end
86
+ end
data/spec/rcov.opts ADDED
@@ -0,0 +1,6 @@
1
+ --exclude "spec"
2
+ --sort coverage
3
+ --callsites
4
+ --xrefs
5
+ --profile
6
+ --text-summary
@@ -0,0 +1,108 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+ require 'dm-reflection/builders/source_builder'
3
+
4
+ BLOGPOST = <<-RUBY
5
+ class BlogPost
6
+
7
+ include DataMapper::Resource
8
+
9
+ property :id, Serial
10
+
11
+ property :created_at, DateTime
12
+ property :body, Text, :length => 65535, :lazy => true
13
+ property :updated_at, DateTime
14
+
15
+
16
+ end
17
+ RUBY
18
+
19
+ COMMENT = <<-RUBY
20
+ class Comment
21
+
22
+ include DataMapper::Resource
23
+
24
+ property :id, Serial
25
+
26
+ property :created_at, DateTime
27
+ property :body, Text, :length => 65535, :lazy => true
28
+ property :updated_at, DateTime
29
+ property :score, Integer
30
+
31
+
32
+ end
33
+ RUBY
34
+
35
+ REFLECTION_SOURCES = [ BLOGPOST, COMMENT ]
36
+
37
+ describe 'The DataMapper reflection module' do
38
+
39
+ before(:each) do
40
+ @adapter = repository(:default).adapter
41
+
42
+ REFLECTION_SOURCES.each { |source| eval(source) }
43
+
44
+ @models = {
45
+ :BlogPost => BLOGPOST,
46
+ :Comment => COMMENT,
47
+ }
48
+
49
+ @models.each_key { |model| Extlib::Inflection.constantize(model.to_s).auto_migrate! }
50
+ @models.each_key { |model| remove_model_from_memory( Extlib::Inflection.constantize(model.to_s) ) }
51
+ end
52
+
53
+ after(:each) do
54
+ @models.each_key do |model_name|
55
+ next unless Object.const_defined?(model_name)
56
+ model = Extlib::Inflection.constantize(model_name.to_s)
57
+ model.auto_migrate_down!
58
+ remove_model_from_memory(model)
59
+ end
60
+ end
61
+
62
+ describe 'repository(:name).reflect' do
63
+ it 'should reflect all the models in a repository' do
64
+ # Reflect the models back into memory.
65
+ DataMapper::Reflection.reflect(:default)
66
+
67
+ # Iterate through each model in memory and verify the source is the same as the original.
68
+ # using model.to_ruby
69
+ @models.each_pair do |model_name, source|
70
+ model = Extlib::Inflection.constantize(model_name.to_s)
71
+ reflected_source = model.to_ruby
72
+ model.to_ruby.should == source
73
+ end
74
+ end
75
+ end
76
+
77
+ describe 'reflected model instance' do
78
+ it 'should respond to default_repository_name and return the correct repo for a reflected model' do
79
+ # Reflect the models back into memory.
80
+ DataMapper::Reflection.reflect(:default)
81
+
82
+ @models.each_key do |model_name|
83
+ model = Extlib::Inflection.constantize(model_name.to_s)
84
+ model.should respond_to(:default_repository_name)
85
+ model.default_repository_name.should == :default
86
+ end
87
+ end
88
+ end
89
+
90
+ describe 'reflective adapter' do
91
+ it 'should respond to get_storage_names and return an array of models' do
92
+ @adapter.should respond_to(:get_storage_names)
93
+ @adapter.get_storage_names.should be_kind_of(Array)
94
+ end
95
+
96
+ it "should respond to get_properties and return an array of properties" do
97
+ @adapter.should respond_to(:get_properties)
98
+ tables = @adapter.get_storage_names
99
+ properties = @adapter.get_properties(tables[0])
100
+ properties.should be_kind_of(Array)
101
+ a_property = properties[0]
102
+ a_property.should be_kind_of(Hash)
103
+ a_property.keys.should include(:name)
104
+ a_property.keys.should include(:type)
105
+ end
106
+ end
107
+
108
+ end