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.
@@ -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
+