intermine 0.98.01

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+