intermine 0.98.01
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/Gemfile +4 -0
- data/LICENCE +165 -0
- data/MANIFEST +0 -0
- data/README.rdoc +79 -0
- data/Rakefile +67 -0
- data/lib/intermine/lists.rb +716 -0
- data/lib/intermine/model.rb +867 -0
- data/lib/intermine/query.rb +1569 -0
- data/lib/intermine/results.rb +196 -0
- data/lib/intermine/service.rb +253 -0
- data/lib/intermine/version.rb +3 -0
- data/test/data/lists.json +29 -0
- data/test/data/model.json +3 -0
- data/test/data/resultobjs.json +3 -0
- data/test/data/resultrow.json +1 -0
- data/test/data/resultset.json +3 -0
- data/test/data/testmodel_model.xml +94 -0
- data/test/live_test.rb +35 -0
- data/test/test.rb +84 -0
- data/test/test_helper.rb +67 -0
- data/test/test_lists.rb +68 -0
- data/test/test_model.rb +417 -0
- data/test/test_query.rb +1202 -0
- data/test/test_result_row.rb +114 -0
- data/test/test_results.rb +22 -0
- data/test/test_service.rb +86 -0
- data/test/test_sugar.rb +219 -0
- data/test/unit_tests.rb +6 -0
- metadata +192 -0
@@ -0,0 +1,867 @@
|
|
1
|
+
# Classes that represent the data model of an InterMine data-warehouse,
|
2
|
+
# and elements within that data model.
|
3
|
+
#
|
4
|
+
|
5
|
+
require 'rubygems'
|
6
|
+
require 'json'
|
7
|
+
|
8
|
+
module InterMine
|
9
|
+
module Metadata
|
10
|
+
|
11
|
+
#
|
12
|
+
# == Description
|
13
|
+
#
|
14
|
+
# A representation of the data model of an InterMine data warehouse.
|
15
|
+
# This class contains access to all aspects of the model, including the tables
|
16
|
+
# of data stored, and the kinds of data in those tables. It is also the
|
17
|
+
# mechanism for creating objects which are representations of data within
|
18
|
+
# the data model, including records, paths and columns.
|
19
|
+
#
|
20
|
+
# model = Model.new(data)
|
21
|
+
#
|
22
|
+
# model.classes.each do |c|
|
23
|
+
# puts "#{c.name} has #{c.fields.size} fields"
|
24
|
+
# end
|
25
|
+
#
|
26
|
+
#:include:contact_header.rdoc
|
27
|
+
#
|
28
|
+
class Model
|
29
|
+
|
30
|
+
|
31
|
+
# The name of the model
|
32
|
+
attr_reader :name
|
33
|
+
|
34
|
+
# The classes within this model
|
35
|
+
attr_reader :classes
|
36
|
+
|
37
|
+
# The Service this model belongs to
|
38
|
+
attr_reader :service
|
39
|
+
|
40
|
+
# Construct a new model from its textual json representation
|
41
|
+
#
|
42
|
+
# Arguments:
|
43
|
+
# [+model_data+] The JSON serialization of the model
|
44
|
+
# [+service+] The Service this model belongs to
|
45
|
+
#
|
46
|
+
# model = Model.new(json)
|
47
|
+
#
|
48
|
+
def initialize(model_data, service=nil)
|
49
|
+
result = JSON.parse(model_data)
|
50
|
+
@model = result["model"]
|
51
|
+
@service = service
|
52
|
+
@name = @model["name"]
|
53
|
+
@classes = {}
|
54
|
+
@model["classes"].each do |k, v|
|
55
|
+
@classes[k] = ClassDescriptor.new(v, self)
|
56
|
+
end
|
57
|
+
@classes.each do |name, cld|
|
58
|
+
cld.fields.each do |fname, fd|
|
59
|
+
if fd.respond_to?(:referencedType)
|
60
|
+
refCd = self.get_cd(fd.referencedType)
|
61
|
+
fd.referencedType = refCd
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
|
68
|
+
# call-seq:
|
69
|
+
# get_cd(name) => ClassDescriptor
|
70
|
+
#
|
71
|
+
# Get a ClassDescriptor from the model by name.
|
72
|
+
#
|
73
|
+
# If a ClassDescriptor itself is passed as the argument,
|
74
|
+
# it will be passed through.
|
75
|
+
#
|
76
|
+
def get_cd(cls)
|
77
|
+
if cls.is_a?(ClassDescriptor)
|
78
|
+
return cls
|
79
|
+
else
|
80
|
+
return @classes[cls.to_s]
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
alias cd get_cd
|
85
|
+
alias table get_cd
|
86
|
+
|
87
|
+
# call-seq:
|
88
|
+
# make_new(name=nil, opts={}) => InterMineObject
|
89
|
+
#
|
90
|
+
# Make a new InterMineObject which is an instantiation of a class a ClassDescriptor represents
|
91
|
+
#
|
92
|
+
# Arguments:
|
93
|
+
# [+name+] The name of the class to instantiate
|
94
|
+
# [+opts+] The values to assign to the new object
|
95
|
+
#
|
96
|
+
# gene = model.make_new 'Gene', {
|
97
|
+
# "symbol" => "zen",
|
98
|
+
# "name" => "zerknullt",
|
99
|
+
# "organism => {
|
100
|
+
# "shortName" => "D. melanogaster",
|
101
|
+
# "taxonId" => 7217
|
102
|
+
# }
|
103
|
+
# }
|
104
|
+
#
|
105
|
+
# puts gene.organism.taxonId
|
106
|
+
# >>> 7217
|
107
|
+
#
|
108
|
+
def make_new(class_name=nil, opts={})
|
109
|
+
# Support calling with just opts
|
110
|
+
if class_name.is_a?(Hash)
|
111
|
+
opts = class_name
|
112
|
+
class_name = nil
|
113
|
+
end
|
114
|
+
if class_name && opts["class"] && (class_name != opts["class"]) && !get_cd(opts["class"]).subclass_of?(class_name)
|
115
|
+
raise ArgumentError, "class name in options hash is not compatible with passed class name: #{opts["class"]} is not a subclass of #{class_name}"
|
116
|
+
end
|
117
|
+
# Prefer the options value to the passed value
|
118
|
+
cd_name = opts["class"] || class_name
|
119
|
+
cls = get_cd(cd_name).to_class
|
120
|
+
obj = cls.new(opts)
|
121
|
+
obj.send(:__cd__=, get_cd(cd_name))
|
122
|
+
return obj
|
123
|
+
end
|
124
|
+
|
125
|
+
# === Resolve the value referred to by a path on an object
|
126
|
+
#
|
127
|
+
# The path may be either a string such as "Department.employees[2].name",
|
128
|
+
# or a Path object
|
129
|
+
def resolve_path(obj, path)
|
130
|
+
return obj._resolve(path)
|
131
|
+
end
|
132
|
+
|
133
|
+
private
|
134
|
+
|
135
|
+
# For mocking in tests
|
136
|
+
def set_service(service)
|
137
|
+
@service = service
|
138
|
+
end
|
139
|
+
|
140
|
+
end
|
141
|
+
|
142
|
+
# == A base class for all objects instantiated from a ClassDescriptor
|
143
|
+
#
|
144
|
+
# This class described the common behaviour for all objects instantiated
|
145
|
+
# as representations of classes defined by a ClassDescriptor. It is not intended
|
146
|
+
# to be instantiated directly, but inherited from.
|
147
|
+
#
|
148
|
+
#:include:contact_header.rdoc
|
149
|
+
#
|
150
|
+
class InterMineObject
|
151
|
+
|
152
|
+
# The database internal id in the originating mine. Serves as a guarantor
|
153
|
+
# of object identity.
|
154
|
+
attr_reader :objectId
|
155
|
+
|
156
|
+
# The ClassDescriptor for this object
|
157
|
+
attr_reader :__cd__
|
158
|
+
|
159
|
+
# Arguments:
|
160
|
+
# hash:: The properties of this object represented as a Hash. Nested
|
161
|
+
# Arrays and Hashes are expected for collections and references.
|
162
|
+
#
|
163
|
+
def initialize(hash=nil)
|
164
|
+
hash ||= {}
|
165
|
+
hash.each do |key, value|
|
166
|
+
if key.to_s != "class"
|
167
|
+
self.send(key.to_s + "=", value)
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
# call-seq:
|
173
|
+
# is_a?(other) => bool
|
174
|
+
#
|
175
|
+
# Determine if this class is a subclass of other.
|
176
|
+
#
|
177
|
+
# Overridden to provide support for querying against ClassDescriptors and Strings.
|
178
|
+
#
|
179
|
+
def is_a?(other)
|
180
|
+
if other.is_a?(ClassDescriptor)
|
181
|
+
return is_a?(other.to_module)
|
182
|
+
elsif other.is_a?(String)
|
183
|
+
return is_a?(@__cd__.model.cd(other))
|
184
|
+
else
|
185
|
+
return super
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
# call-seq:
|
190
|
+
# to_s() => human-readable string
|
191
|
+
#
|
192
|
+
# Serialise to a readable representation
|
193
|
+
def to_s
|
194
|
+
parts = [@__cd__.name + ':' + self.objectId.to_s]
|
195
|
+
self.instance_variables.reject{|var| var.to_s.end_with?("objectId")}.each do |var|
|
196
|
+
parts << "#{var}=#{self.instance_variable_get(var).inspect}"
|
197
|
+
end
|
198
|
+
return "<#{parts.join(' ')}>"
|
199
|
+
end
|
200
|
+
|
201
|
+
# call-seq:
|
202
|
+
# [key] => value
|
203
|
+
#
|
204
|
+
# Alias property fetches as item retrieval, so the following are equivalent:
|
205
|
+
#
|
206
|
+
# organism = gene.organism
|
207
|
+
# organism = gene["organism"]
|
208
|
+
#
|
209
|
+
def [](key)
|
210
|
+
if @__cd__.has_field?(key):
|
211
|
+
return self.send(key)
|
212
|
+
end
|
213
|
+
raise IndexError, "No field #{key} found for #{@__cd__.name}"
|
214
|
+
end
|
215
|
+
|
216
|
+
# call-seq:
|
217
|
+
# _resolve(path) => value
|
218
|
+
#
|
219
|
+
# Resolve a path represented as a String or as a Path into a value
|
220
|
+
#
|
221
|
+
# This is designed to automate access to values in deeply nested objects. So:
|
222
|
+
#
|
223
|
+
# name = gene._resolve('Gene.organism.name')
|
224
|
+
#
|
225
|
+
# Array indices are supported:
|
226
|
+
#
|
227
|
+
# symbol = gene._resolve('Gene.alleles[3].symbol')
|
228
|
+
#
|
229
|
+
def _resolve(path)
|
230
|
+
begin
|
231
|
+
parts = path.split(/(?:\.|\[|\])/).reject {|x| x.empty?}
|
232
|
+
rescue NoMethodError
|
233
|
+
parts = path.elements.map { |x| x.name }
|
234
|
+
end
|
235
|
+
root = parts.shift
|
236
|
+
if !is_a?(root)
|
237
|
+
raise ArgumentError, "Incompatible path '#{path}': #{self} is not a #{root}"
|
238
|
+
end
|
239
|
+
begin
|
240
|
+
res = parts.inject(self) do |memo, part|
|
241
|
+
part = part.to_i if (memo.is_a?(Array) and part.to_i.to_s == part)
|
242
|
+
begin
|
243
|
+
new = memo[part]
|
244
|
+
rescue TypeError
|
245
|
+
raise ArgumentError, "Incompatible path '#{path}' for #{self}, expected an index"
|
246
|
+
end
|
247
|
+
new
|
248
|
+
end
|
249
|
+
rescue IndexError => e
|
250
|
+
raise ArgumentError, "Incompatible path '#{path}' for #{self}, #{e}"
|
251
|
+
end
|
252
|
+
return res
|
253
|
+
end
|
254
|
+
|
255
|
+
alias inspect to_s
|
256
|
+
|
257
|
+
private
|
258
|
+
|
259
|
+
def __cd__=(cld)
|
260
|
+
@__cd__ = cld
|
261
|
+
end
|
262
|
+
|
263
|
+
def objectId=(val)
|
264
|
+
@objectId = val
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
# == A base module that provides helpers for setting up classes bases on the contents of a Hash
|
269
|
+
#
|
270
|
+
# ClassDescriptors and FieldDescriptors are instantiated
|
271
|
+
# with hashes that provide their properties. This module
|
272
|
+
# makes sure that the appropriate instance variables are set
|
273
|
+
#
|
274
|
+
#:include:contact_header.rdoc
|
275
|
+
#
|
276
|
+
module SetHashKey
|
277
|
+
|
278
|
+
# call-seq:
|
279
|
+
# set_key_value(key, value)
|
280
|
+
#
|
281
|
+
# Set up instance variables based on the contents of a hash
|
282
|
+
def set_key_value(k, v)
|
283
|
+
if (k == "type")
|
284
|
+
k = "dataType"
|
285
|
+
end
|
286
|
+
## create and initialize an instance variable for this
|
287
|
+
## key/value pair
|
288
|
+
self.instance_variable_set("@#{k}", v)
|
289
|
+
## create the getter that returns the instance variable
|
290
|
+
self.class.send(:define_method, k,
|
291
|
+
proc{self.instance_variable_get("@#{k}")})
|
292
|
+
## create the setter that sets the instance variable
|
293
|
+
self.class.send(:define_method, "#{k}=",
|
294
|
+
proc{|v| self.instance_variable_set("@#{k}", v)})
|
295
|
+
return
|
296
|
+
end
|
297
|
+
|
298
|
+
# call-seq:
|
299
|
+
# inspect() => readable-string
|
300
|
+
#
|
301
|
+
# Produce a readable string
|
302
|
+
def inspect
|
303
|
+
parts = []
|
304
|
+
self.instance_variables.each do |x|
|
305
|
+
var = self.instance_variable_get(x)
|
306
|
+
if var.is_a?(ClassDescriptor) || var.is_a?(Model)
|
307
|
+
parts << x.to_s + "=" + var.to_s
|
308
|
+
else
|
309
|
+
parts << x.to_s + "=" + var.inspect
|
310
|
+
end
|
311
|
+
end
|
312
|
+
return "<#{parts.join(' ')}>"
|
313
|
+
end
|
314
|
+
end
|
315
|
+
|
316
|
+
# == A class representing a table in the InterMine data model
|
317
|
+
#
|
318
|
+
# A class descriptor represents a logical abstraction of a table in the
|
319
|
+
# InterMine model, and contains information about the columns in the table
|
320
|
+
# and the other tables that are referenced by this table.
|
321
|
+
#
|
322
|
+
# It can be used to construct queries directly, when obtained from a webservice.
|
323
|
+
#
|
324
|
+
# cld = service.model.table('Gene')
|
325
|
+
# cld.where(:symbol => 'zen').each_row {|row| puts row}
|
326
|
+
#
|
327
|
+
#:include:contact_header.rdoc
|
328
|
+
#
|
329
|
+
class ClassDescriptor
|
330
|
+
include SetHashKey
|
331
|
+
|
332
|
+
# The InterMine Model
|
333
|
+
attr_reader :model
|
334
|
+
|
335
|
+
# The Hash containing the fields of this model
|
336
|
+
attr_reader :fields
|
337
|
+
|
338
|
+
# ClassDescriptors are constructed automatically when the model itself is
|
339
|
+
# parsed. They should not be constructed on their own.
|
340
|
+
#
|
341
|
+
# Arguments:
|
342
|
+
# [+opts+] A Hash containing the information to initialise this ClassDescriptor.
|
343
|
+
# [+model+] The model this ClassDescriptor belongs to.
|
344
|
+
#
|
345
|
+
def initialize(opts, model)
|
346
|
+
@model = model
|
347
|
+
@fields = {}
|
348
|
+
@klass = nil
|
349
|
+
@module = nil
|
350
|
+
|
351
|
+
field_types = {
|
352
|
+
"attributes" => AttributeDescriptor,
|
353
|
+
"references" => ReferenceDescriptor,
|
354
|
+
"collections" => CollectionDescriptor
|
355
|
+
}
|
356
|
+
|
357
|
+
opts.each do |k,v|
|
358
|
+
if (field_types.has_key?(k))
|
359
|
+
v.each do |name, field|
|
360
|
+
@fields[name] = field_types[k].new(field, model)
|
361
|
+
end
|
362
|
+
else
|
363
|
+
set_key_value(k, v)
|
364
|
+
end
|
365
|
+
end
|
366
|
+
end
|
367
|
+
|
368
|
+
# call-seq:
|
369
|
+
# new_query => PathQuery::Query
|
370
|
+
#
|
371
|
+
# Construct a new query for the service this ClassDescriptor belongs to
|
372
|
+
# rooted on this table.
|
373
|
+
#
|
374
|
+
# query = model.table('Gene').new_query
|
375
|
+
#
|
376
|
+
def new_query
|
377
|
+
q = @model.service.new_query(self.name)
|
378
|
+
return q
|
379
|
+
end
|
380
|
+
|
381
|
+
alias query new_query
|
382
|
+
|
383
|
+
# call-seq:
|
384
|
+
# select(*columns) => PathQuery::Query
|
385
|
+
#
|
386
|
+
# Construct a new query on this table in the originating
|
387
|
+
# service with given columns selected for output.
|
388
|
+
#
|
389
|
+
# query = model.table('Gene').select(:symbol, :name, "organism.name", "alleles.*")
|
390
|
+
#
|
391
|
+
# query.each_result do |gene|
|
392
|
+
# puts "#{gene.symbol} (#{gene.organism.name}): #{gene.alleles.size} Alleles"
|
393
|
+
# end
|
394
|
+
#
|
395
|
+
def select(*cols)
|
396
|
+
q = new_query
|
397
|
+
q.add_views(cols)
|
398
|
+
return q
|
399
|
+
end
|
400
|
+
|
401
|
+
# call-seq:
|
402
|
+
# where(*constraints) => PathQuery::Query
|
403
|
+
#
|
404
|
+
# Returns a new query on this table in the originating
|
405
|
+
# service will all attribute columns selected for output
|
406
|
+
# and the given constraints applied.
|
407
|
+
#
|
408
|
+
# zen = model.table('Gene').where(:symbol => 'zen').one
|
409
|
+
# puts "Zen is short for #{zen.name}, and has a length of #{zen.length}"
|
410
|
+
#
|
411
|
+
def where(*args)
|
412
|
+
q = new_query
|
413
|
+
q.select("*")
|
414
|
+
q.where(*args)
|
415
|
+
return q
|
416
|
+
end
|
417
|
+
|
418
|
+
# call-seq:
|
419
|
+
# get_field(name) => FieldDescriptor
|
420
|
+
#
|
421
|
+
# Returns the field of the given name if it exists in the
|
422
|
+
# referenced table.
|
423
|
+
#
|
424
|
+
def get_field(name)
|
425
|
+
return @fields[name]
|
426
|
+
end
|
427
|
+
|
428
|
+
alias field get_field
|
429
|
+
|
430
|
+
# call-seq:
|
431
|
+
# has_field?(name) => bool
|
432
|
+
#
|
433
|
+
# Returns true if the table has a field of the given name.
|
434
|
+
#
|
435
|
+
def has_field?(name)
|
436
|
+
return @fields.has_key?(name)
|
437
|
+
end
|
438
|
+
|
439
|
+
# call-seq:
|
440
|
+
# attributes => Array[AttributeDescriptor]
|
441
|
+
#
|
442
|
+
# Returns an Array of all fields in the current table that represent
|
443
|
+
# attributes (ie. columns that can hold values, rather than references to
|
444
|
+
# other tables.)
|
445
|
+
#
|
446
|
+
def attributes
|
447
|
+
return @fields.select {|k, v| v.is_a?(AttributeDescriptor)}.map {|pair| pair[1]}
|
448
|
+
end
|
449
|
+
|
450
|
+
# Returns a human readable string
|
451
|
+
def to_s
|
452
|
+
return "#{@model.name}.#{@name}"
|
453
|
+
end
|
454
|
+
|
455
|
+
# Return a fuller string representation.
|
456
|
+
def inspect
|
457
|
+
return "<#{self.class.name}:#{self.object_id} #{to_s}>"
|
458
|
+
end
|
459
|
+
|
460
|
+
# call-seq:
|
461
|
+
# subclass_of?(other) => bool
|
462
|
+
#
|
463
|
+
# Returns true if the class this ClassDescriptor describes is a
|
464
|
+
# subclass of the class the other element evaluates to. The other
|
465
|
+
# may be a ClassDescriptor, or a Path, or a String describing a path.
|
466
|
+
#
|
467
|
+
# model.table('Gene').subclass_of?(model.table('SequenceFeature'))
|
468
|
+
# >>> true
|
469
|
+
#
|
470
|
+
# model.table('Gene').subclass_of?(model.table('Protein'))
|
471
|
+
# >>> false
|
472
|
+
#
|
473
|
+
def subclass_of?(other)
|
474
|
+
path = Path.new(other, @model)
|
475
|
+
if @extends.include? path.end_type
|
476
|
+
return true
|
477
|
+
else
|
478
|
+
@extends.each do |x|
|
479
|
+
superCls = @model.get_cd(x)
|
480
|
+
if superCls.subclass_of?(path)
|
481
|
+
return true
|
482
|
+
end
|
483
|
+
end
|
484
|
+
end
|
485
|
+
return false
|
486
|
+
end
|
487
|
+
|
488
|
+
# call-seq:
|
489
|
+
# to_module => Module
|
490
|
+
#
|
491
|
+
# Produces a module containing the logic this ClassDescriptor represents,
|
492
|
+
# suitable for including into a class definition.
|
493
|
+
#
|
494
|
+
# The use of modules enables multiple inheritance, which is supported in
|
495
|
+
# the InterMine data model, to be represented in the classes instantiated
|
496
|
+
# in the client.
|
497
|
+
#
|
498
|
+
def to_module
|
499
|
+
if @module.nil?
|
500
|
+
nums = ["Float", "Double", "float", "double"]
|
501
|
+
ints = ["Integer", "int"]
|
502
|
+
bools = ["Boolean", "boolean"]
|
503
|
+
|
504
|
+
supers = @extends.map { |x| @model.get_cd(x).to_module }
|
505
|
+
|
506
|
+
klass = Module.new
|
507
|
+
fd_names = @fields.values.map { |x| x.name }
|
508
|
+
klass.class_eval do
|
509
|
+
include *supers
|
510
|
+
attr_reader *fd_names
|
511
|
+
|
512
|
+
end
|
513
|
+
|
514
|
+
@fields.values.each do |fd|
|
515
|
+
if fd.is_a?(CollectionDescriptor)
|
516
|
+
klass.class_eval do
|
517
|
+
define_method("add" + fd.name.capitalize) do |*vals|
|
518
|
+
type = fd.referencedType
|
519
|
+
instance_var = instance_variable_get("@" + fd.name)
|
520
|
+
instance_var ||= []
|
521
|
+
vals.each do |item|
|
522
|
+
if item.is_a?(Hash)
|
523
|
+
item = type.model.make_new(type.name, item)
|
524
|
+
end
|
525
|
+
if !item.is_a?(type)
|
526
|
+
raise ArgumentError, "Arguments to #{fd.name} in #{@name} must be #{type.name}s"
|
527
|
+
end
|
528
|
+
instance_var << item
|
529
|
+
end
|
530
|
+
instance_variable_set("@" + fd.name, instance_var)
|
531
|
+
end
|
532
|
+
end
|
533
|
+
end
|
534
|
+
klass.class_eval do
|
535
|
+
define_method(fd.name + "=") do |val|
|
536
|
+
if fd.is_a?(AttributeDescriptor)
|
537
|
+
type = fd.dataType
|
538
|
+
if nums.include?(type)
|
539
|
+
if !val.is_a?(Numeric)
|
540
|
+
raise ArgumentError, "Arguments to #{fd.name} in #{@name} must be numeric"
|
541
|
+
end
|
542
|
+
elsif ints.include?(type)
|
543
|
+
if !val.is_a?(Integer)
|
544
|
+
raise ArgumentError, "Arguments to #{fd.name} in #{@name} must be integers"
|
545
|
+
end
|
546
|
+
elsif bools.include?(type)
|
547
|
+
if !val.is_a?(TrueClass) && !val.is_a?(FalseClass)
|
548
|
+
raise ArgumentError, "Arguments to #{fd.name} in #{@name} must be booleans"
|
549
|
+
end
|
550
|
+
end
|
551
|
+
instance_variable_set("@" + fd.name, val)
|
552
|
+
else
|
553
|
+
type = fd.referencedType
|
554
|
+
if fd.is_a?(CollectionDescriptor)
|
555
|
+
instance_var = []
|
556
|
+
val.each do |item|
|
557
|
+
if item.is_a?(Hash)
|
558
|
+
item = type.model.make_new(type.name, item)
|
559
|
+
end
|
560
|
+
if !item.is_a?(type)
|
561
|
+
raise ArgumentError, "Arguments to #{fd.name} in #{@name} must be #{type.name}s"
|
562
|
+
end
|
563
|
+
instance_var << item
|
564
|
+
end
|
565
|
+
instance_variable_set("@" + fd.name, instance_var)
|
566
|
+
else
|
567
|
+
if val.is_a?(Hash)
|
568
|
+
val = type.model.make_new(type.name, val)
|
569
|
+
end
|
570
|
+
if !val.is_a?(type)
|
571
|
+
raise ArgumentError, "Arguments to #{fd.name} in #{@name} must be #{type.name}s"
|
572
|
+
end
|
573
|
+
instance_variable_set("@" + fd.name, val)
|
574
|
+
end
|
575
|
+
end
|
576
|
+
end
|
577
|
+
|
578
|
+
end
|
579
|
+
end
|
580
|
+
@module = klass
|
581
|
+
end
|
582
|
+
return @module
|
583
|
+
end
|
584
|
+
|
585
|
+
# call-seq:
|
586
|
+
# to_class => Class
|
587
|
+
#
|
588
|
+
# Returns a Class that can be used to instantiate new objects
|
589
|
+
# representing rows of data in the InterMine database.
|
590
|
+
#
|
591
|
+
def to_class
|
592
|
+
if @klass.nil?
|
593
|
+
mod = to_module
|
594
|
+
kls = Class.new(InterMineObject)
|
595
|
+
cd = self
|
596
|
+
kls.class_eval do
|
597
|
+
include mod
|
598
|
+
@__cd__ = cd
|
599
|
+
end
|
600
|
+
@klass = kls
|
601
|
+
end
|
602
|
+
return @klass
|
603
|
+
end
|
604
|
+
|
605
|
+
end
|
606
|
+
|
607
|
+
# A representation of a database column. The characteristics of
|
608
|
+
# these classes are defined by the model information received
|
609
|
+
# from the webservice
|
610
|
+
class FieldDescriptor
|
611
|
+
include SetHashKey
|
612
|
+
|
613
|
+
# The data model this field descriptor belongs to.
|
614
|
+
attr_accessor :model
|
615
|
+
|
616
|
+
# Constructor.
|
617
|
+
#
|
618
|
+
# [+opts+] The hash of parameters received from the webservice
|
619
|
+
# [+model+] The parental data model
|
620
|
+
#
|
621
|
+
def initialize(opts, model)
|
622
|
+
@model = model
|
623
|
+
opts.each do |k, v|
|
624
|
+
set_key_value(k, v)
|
625
|
+
end
|
626
|
+
end
|
627
|
+
|
628
|
+
end
|
629
|
+
|
630
|
+
# A class representing columns that contain data.
|
631
|
+
class AttributeDescriptor < FieldDescriptor
|
632
|
+
end
|
633
|
+
|
634
|
+
# A class representing columns that reference other tables.
|
635
|
+
class ReferenceDescriptor < FieldDescriptor
|
636
|
+
end
|
637
|
+
|
638
|
+
# A class representing a virtual column that contains multiple references.
|
639
|
+
class CollectionDescriptor < ReferenceDescriptor
|
640
|
+
end
|
641
|
+
|
642
|
+
# A representation of a path through the data model, starting at a table/class,
|
643
|
+
# and descending ultimately to an attribute. A path represents a valid
|
644
|
+
# sequence of joins and column accesses according to the webservice's database schema.
|
645
|
+
#
|
646
|
+
# In string format, a path can be represented using dotted notation:
|
647
|
+
#
|
648
|
+
# Gene.proteins.proteinDomains.name
|
649
|
+
#
|
650
|
+
# Which is a valid path through the data-model, starting in the gene table, following
|
651
|
+
# a reference to the protein table (via x-to-many relationship) and then to the
|
652
|
+
# protein-domain table and then finally to the name column in the protein domain table.
|
653
|
+
# Joins are implicitly implied.
|
654
|
+
#
|
655
|
+
#:include:contact_header.rdoc
|
656
|
+
#
|
657
|
+
class Path
|
658
|
+
|
659
|
+
# The data model that this path describes.
|
660
|
+
attr_reader :model
|
661
|
+
|
662
|
+
# The objects represented by each section of the path. The first is always a ClassDescriptor.
|
663
|
+
attr_reader :elements
|
664
|
+
|
665
|
+
# The subclass information used to create this path.
|
666
|
+
attr_reader :subclasses
|
667
|
+
|
668
|
+
# The root class of this path. This is the same as the first element.
|
669
|
+
attr_reader :rootClass
|
670
|
+
|
671
|
+
# Construct a Path
|
672
|
+
#
|
673
|
+
# The standard mechanism is to parse a string representing a path
|
674
|
+
# with information about the model and the subclasses that are in force.
|
675
|
+
# However, it is also possible to clone a path by passing a Path through
|
676
|
+
# as the first element, and also to construct a path from a ClassDescriptor.
|
677
|
+
# In both cases the new Path will inherit the model of the object used to
|
678
|
+
# construct it, this avoid the need for a model in these cases.
|
679
|
+
#
|
680
|
+
def initialize(pathstring, model=nil, subclasses={})
|
681
|
+
@model = model
|
682
|
+
@subclasses = subclasses
|
683
|
+
@elements = []
|
684
|
+
@rootClass = nil
|
685
|
+
parse(pathstring)
|
686
|
+
end
|
687
|
+
|
688
|
+
# call-seq:
|
689
|
+
# end_type => String
|
690
|
+
#
|
691
|
+
# Return the string that describes the kind of thing this path represents.
|
692
|
+
# eg:
|
693
|
+
# [+Gene+] "Gene"
|
694
|
+
# [+Gene.symbol+] "java.lang.String"
|
695
|
+
# [+Gene.proteins+] "Protein"
|
696
|
+
#
|
697
|
+
def end_type
|
698
|
+
last = @elements.last
|
699
|
+
if last.is_a?(ClassDescriptor)
|
700
|
+
return last.name
|
701
|
+
elsif last.respond_to?(:referencedType)
|
702
|
+
return last.referencedType.name
|
703
|
+
else
|
704
|
+
return last.dataType
|
705
|
+
end
|
706
|
+
end
|
707
|
+
|
708
|
+
# call-seq:
|
709
|
+
# end_cd => ClassDescriptor
|
710
|
+
#
|
711
|
+
# Return the last ClassDescriptor mentioned in this path.
|
712
|
+
# eg:
|
713
|
+
# [+Gene+] Gene
|
714
|
+
# [+Gene.symbol+] Gene
|
715
|
+
# [+Gene.proteins+] Protein
|
716
|
+
# [+Gene.proteins.name+] Protein
|
717
|
+
#
|
718
|
+
def end_cd
|
719
|
+
last = @elements.last
|
720
|
+
if last.is_a?(ClassDescriptor)
|
721
|
+
return last
|
722
|
+
elsif last.respond_to?(:referencedType)
|
723
|
+
return last.referencedType
|
724
|
+
else
|
725
|
+
penult = @elements[-2]
|
726
|
+
if penult.is_a?(ClassDescriptor)
|
727
|
+
return penult
|
728
|
+
else
|
729
|
+
return penult.referencedType
|
730
|
+
end
|
731
|
+
end
|
732
|
+
end
|
733
|
+
|
734
|
+
# Two paths can be said to be equal when they stringify to the same representation.
|
735
|
+
def ==(other)
|
736
|
+
return self.to_s == other.to_s
|
737
|
+
end
|
738
|
+
|
739
|
+
# Get the number of elements in the path
|
740
|
+
def length
|
741
|
+
return @elements.length
|
742
|
+
end
|
743
|
+
|
744
|
+
# Return the string representation of this path, eg: "Gene.proteins.name"
|
745
|
+
def to_s
|
746
|
+
return @elements.map {|x| x.name}.join(".")
|
747
|
+
end
|
748
|
+
|
749
|
+
# Returns a string as to_s without the first element. eg: "proteins.name"
|
750
|
+
def to_headless_s
|
751
|
+
return @elements[1, @elements.size - 1].map {|x| x.name}.join(".")
|
752
|
+
end
|
753
|
+
|
754
|
+
# Return true if the Path ends in an attribute
|
755
|
+
def is_attribute?
|
756
|
+
return @elements.last.is_a?(AttributeDescriptor)
|
757
|
+
end
|
758
|
+
|
759
|
+
# Return true if the last element is a class (ie. a path of length 1)
|
760
|
+
def is_class?
|
761
|
+
return @elements.last.is_a?(ClassDescriptor)
|
762
|
+
end
|
763
|
+
|
764
|
+
# Return true if the last element is a reference.
|
765
|
+
def is_reference?
|
766
|
+
return @elements.last.is_a?(ReferenceDescriptor)
|
767
|
+
end
|
768
|
+
|
769
|
+
# Return true if the last element is a collection
|
770
|
+
def is_collection?
|
771
|
+
return @elements.last.is_a?(CollectionDescriptor)
|
772
|
+
end
|
773
|
+
|
774
|
+
private
|
775
|
+
|
776
|
+
# Perform the parsing of the input into a sequence of elements.
|
777
|
+
def parse(pathstring)
|
778
|
+
if pathstring.is_a?(ClassDescriptor)
|
779
|
+
@rootClass = pathstring
|
780
|
+
@elements << pathstring
|
781
|
+
@model = pathstring.model
|
782
|
+
return
|
783
|
+
elsif pathstring.is_a?(Path)
|
784
|
+
@rootClass = pathstring.rootClass
|
785
|
+
@elements = pathstring.elements
|
786
|
+
@model = pathstring.model
|
787
|
+
@subclasses = pathstring.subclasses
|
788
|
+
return
|
789
|
+
end
|
790
|
+
|
791
|
+
bits = pathstring.split(".")
|
792
|
+
rootName = bits.shift
|
793
|
+
@rootClass = @model.get_cd(rootName)
|
794
|
+
if @rootClass.nil?
|
795
|
+
raise PathException.new(pathstring, subclasses, "Invalid root class '#{rootName}'")
|
796
|
+
end
|
797
|
+
|
798
|
+
@elements << @rootClass
|
799
|
+
processed = [rootName]
|
800
|
+
|
801
|
+
current_cd = @rootClass
|
802
|
+
|
803
|
+
while (bits.length > 0)
|
804
|
+
this_bit = bits.shift
|
805
|
+
fd = current_cd.get_field(this_bit)
|
806
|
+
if fd.nil?
|
807
|
+
subclassKey = processed.join(".")
|
808
|
+
if @subclasses.has_key?(subclassKey)
|
809
|
+
subclass = model.get_cd(@subclasses[subclassKey])
|
810
|
+
if subclass.nil?
|
811
|
+
raise PathException.new(pathstring, subclasses,
|
812
|
+
"'#{subclassKey}' constrained to be a '#{@subclasses[subclassKey]}', but that is not a valid class in the model")
|
813
|
+
end
|
814
|
+
current_cd = subclass
|
815
|
+
fd = current_cd.get_field(this_bit)
|
816
|
+
end
|
817
|
+
if fd.nil?
|
818
|
+
raise PathException.new(pathstring, subclasses,
|
819
|
+
"giving up at '#{subclassKey}.#{this_bit}'. Could not find '#{this_bit}' in '#{current_cd}'")
|
820
|
+
end
|
821
|
+
end
|
822
|
+
@elements << fd
|
823
|
+
if fd.respond_to?(:referencedType)
|
824
|
+
current_cd = fd.referencedType
|
825
|
+
elsif bits.length > 0
|
826
|
+
raise PathException.new(pathstring, subclasses,
|
827
|
+
"Attributes must be at the end of the path. Giving up at '#{this_bit}'")
|
828
|
+
else
|
829
|
+
current_cd = nil
|
830
|
+
end
|
831
|
+
processed << this_bit
|
832
|
+
end
|
833
|
+
end
|
834
|
+
end
|
835
|
+
|
836
|
+
# An exception class for handling path parsing errors.
|
837
|
+
class PathException < RuntimeError
|
838
|
+
|
839
|
+
attr_reader :pathstring, :subclasses
|
840
|
+
|
841
|
+
def initialize(pathstring=nil, subclasses={}, message=nil)
|
842
|
+
@pathstring = pathstring
|
843
|
+
@subclasses = subclasses
|
844
|
+
@message = message
|
845
|
+
end
|
846
|
+
|
847
|
+
# The string representation.
|
848
|
+
def to_s
|
849
|
+
if @pathstring.nil?
|
850
|
+
if @message.nil?
|
851
|
+
return self.class.name
|
852
|
+
else
|
853
|
+
return @message
|
854
|
+
end
|
855
|
+
end
|
856
|
+
preamble = "Unable to resolve '#{@pathstring}': "
|
857
|
+
footer = " (SUBCLASSES => #{@subclasses.inspect})"
|
858
|
+
if @message.nil?
|
859
|
+
return preamble + footer
|
860
|
+
else
|
861
|
+
return preamble + @message + footer
|
862
|
+
end
|
863
|
+
end
|
864
|
+
end
|
865
|
+
end
|
866
|
+
end
|
867
|
+
|