File Coverage

blib/lib/PomBase/Chobo/OntologyTerm.pm
Criterion Covered Total %
statement 142 153 92.8
branch 50 60 83.3
condition 36 48 75.0
subroutine 17 17 100.0
pod 2 8 25.0
total 247 286 86.3


line stmt bran cond sub pod time code
1             package PomBase::Chobo::OntologyTerm;
2              
3             =head1 NAME
4              
5             PomBase::Chobo::OntologyTerm - Code for holding term data read from an OBO file
6              
7             =head1 SYNOPSIS
8              
9             =head1 AUTHOR
10              
11             Kim Rutherford C<< >>
12              
13             =head1 BUGS
14              
15             Please report any bugs or feature requests to C.
16              
17             =head1 SUPPORT
18              
19             You can find documentation for this module with the perldoc command.
20              
21             perldoc PomBase::Chobo::OntologyTerm
22              
23             =over 4
24              
25             =back
26              
27             =head1 COPYRIGHT & LICENSE
28              
29             Copyright 2012 Kim Rutherford, all rights reserved.
30              
31             This program is free software; you can redistribute it and/or modify it
32             under the same terms as Perl itself.
33              
34             =head1 FUNCTIONS
35              
36             =cut
37              
38             our $VERSION = '0.041'; # VERSION
39              
40 6     6   159002 use Mouse;
  6         40916  
  6         54  
41 6     6   3036 use Carp;
  6         13  
  6         568  
42              
43 6     6   3717 use PomBase::Chobo::OntologyConf;
  6         18  
  6         403  
44              
45 6     6   578 use Clone qw(clone);
  6         644  
  6         469  
46 6     6   3463 use Data::Compare;
  6         89952  
  6         77  
47 6     6   34476 use List::Compare;
  6         183461  
  6         2605  
48              
49             has id => (is => 'ro', isa => 'Int', required => 1);
50             has cvterm_id => (is => 'ro', isa => 'Int', required => 0);
51             has cv_id => (is => 'ro', isa => 'Int', required => 0);
52             has name => (is => 'ro', isa => 'Str');
53             has def => (is => 'ro', isa => 'Str');
54             has namespace => (is => 'ro', isa => 'Str');
55             has comment => (is => 'ro', isa => 'Str');
56             has alt_id => (is => 'ro', isa => 'ArrayRef');
57             has xref => (is => 'ro', isa => 'ArrayRef');
58             has subset => (is => 'ro', isa => 'ArrayRef');
59             has is_relationshiptype => (is => 'ro', isa => 'Bool');
60             has is_obsolete => (is => 'ro', isa => 'Bool');
61             has replaced_by => (is => 'ro', isa => 'Str');
62             has consider => (is => 'ro', isa => 'Str');
63             has property_value => (is => 'ro', isa => 'ArrayRef');
64             has source_file => (is => 'ro', isa => 'Str', required => 1);
65             has source_file_line_number => (is => 'ro', isa => 'Str', required => 1);
66             has metadata => (is => 'ro');
67              
68             our @field_names;
69             our %field_conf;
70              
71             BEGIN {
72 6     6   145 %field_conf = %PomBase::Chobo::OntologyConf::field_conf;
73 6         41 @field_names = qw(id name);
74              
75 6 100       71 for my $field_name (sort grep { $_ ne 'id' && $_ ne 'name' } keys %field_conf) {
  96         378  
76 84         13904 push @field_names, $field_name;
77             }
78             }
79              
80             sub synonyms
81             {
82 8     8 0 124 my $self = shift;
83              
84 8   100     15 return @{$self->{synonym} // []};
  8         49  
85             }
86              
87             sub alt_ids
88             {
89 169     169 0 250 my $self = shift;
90              
91             return map {
92 43         90 my $val = $_;
93              
94 43 100       228 if ($val =~ /(\S+):(\S+)/) {
95             {
96 39         326 id => $val,
97             db_name => $1,
98             accession => $2,
99             };
100             } else {
101 4         6 my $db_name;
102 4 50 33     38 if (defined $self->metadata()->{ontology} &&
103             $self->metadata()->{ontology} eq 'ro') {
104 0         0 $db_name = 'OBO_REL'
105             } else {
106 4         10 $db_name = '_global';
107             }
108             {
109 4         21 id => $val,
110             db_name => $db_name,
111             accession => $val,
112             };
113             }
114 169   50     220 } @{$self->{alt_id} // []};
  169         761  
115             }
116              
117             sub property_values
118             {
119 8     8 0 1405 my $self = shift;
120              
121 8   100     12 return @{$self->{property_value} // []};
  8         49  
122             }
123              
124             sub subsets
125             {
126 2     2 0 2899 my $self = shift;
127              
128 2   100     4 return @{$self->{subset} // []};
  2         54  
129             }
130              
131             sub xrefs
132             {
133 88     88 0 130 my $self = shift;
134              
135 88   100     113 return @{$self->{xref} // []};
  88         418  
136             }
137              
138             =head2 make_object
139              
140             Usage : my $object = PomBase::Chobo::OntologyTerm->make_object($args);
141             Function: Turn $args into an OntologyTerm
142              
143             =cut
144              
145             sub make_object
146             {
147 88     88 1 291990 my $class = shift;
148 88         141 my $object = shift;
149 88         117 my $options = shift;
150              
151 88 50       209 if (!defined $object) {
152 0         0 croak "no argument passed to new()";
153             }
154              
155 88 100 66     452 if ($object->{def} && $object->{def}->{dbxrefs} && $object->{alt_id}) {
      66        
156 11         22 for my $alt_id (@{$object->{alt_id}}) {
  11         75  
157             # filter alt_ids from the definition xrefs to avoid:
158             # duplicate key value violates unique constraint "cvterm_dbxref_c1"
159             # see also: https://github.com/kimrutherford/go-ontology/commit/92dca313a69ffb073c226b94242faa8f321efcf2
160 16         47 @{$object->{def}->{dbxrefs}} =
161             grep {
162 12         23 my $xref = $_;
163 12         35 $alt_id ne $xref;
164 16         30 } @{$object->{def}->{dbxrefs}};
  16         58  
165             }
166             }
167              
168 88 100 66     240 if ($object->{is_obsolete} && $object->{name} && $object->{name} !~ /^obsolete/i) {
      66        
169 2         10 $object->{name} = "OBSOLETE " . $object->{id} . " " . $object->{name};
170             }
171              
172 88 50 66     219 if ($object->{is_relationshiptype} && $object->{name}) {
173 14         39 $object->{name} =~ s/ /_/g;
174             }
175              
176 88         238 $object->{_namespace_from_metadata} = 0;
177              
178 88 100       168 if ($options) {
179 81 100       195 if ($options->{namespace_from_metadata}) {
180 40         84 $object->{_namespace_from_metadata} = 1;
181             }
182             }
183              
184 88   100     408 $object->{alt_id} //= [];
185              
186 88         143 my ($db_name, $accession);
187              
188 88 100       904 unless (($db_name, $accession) = $object->{id} =~ /^(\S+):(.+?)\s*$/) {
189 10 50       34 if ($object->{id} eq 'part_of') {
190             # special case to make sure all the part_of terms are merged - the "part_of"
191             # in the GO and FYPO OBO files has the namespace "external" (and a variety of
192             # others) and the ID is "part_of"
193             # we normalise the id and namespace to match RO
194 0         0 $db_name = 'BFO';
195 0         0 $accession = '0000050';
196              
197 0         0 $object->{id} = "$db_name:$accession";
198 0         0 $object->{namespace} = "relationship";
199             } else {
200 10         20 $db_name = '_global';
201 10         41 $accession = $object->{id};
202             }
203             }
204              
205 88         308 $object->{accession} = $accession;
206 88         173 $object->{db_name} = $db_name;
207              
208 88 50       304 if (!defined $object->{source_file}) {
209 0         0 confess "source_file attribute of object is required\n";
210             }
211              
212 88 50       186 if (!defined $object->{source_file_line_number}) {
213 0         0 confess "source_file_line attribute of object is required\n";
214             }
215              
216 88         311 return bless $object, $class;
217             }
218              
219             =head2 merge
220              
221             Usage : my $merged_term = $term->merge($other_term);
222             Function: Attempt to merge $other_term into this term. Only merges if at least
223             one of the ID or alt_ids from this term match the ID or an alt_id
224             from $other_term
225             Args : $other_term - the term to merge with
226             Return : undef - if no id from this term matches one from $other_term
227             $self - if there is a match
228             =cut
229              
230             sub merge
231             {
232 15     15 1 4456 my $self = shift;
233 15         27 my $other_term = shift;
234              
235 15         600 my $orig_term = clone $self;
236              
237 15 100       87 return if $self == $other_term;
238              
239 14         57 my $lc = List::Compare->new([$self->{id}, @{$self->{alt_id}}],
240 14         35 [$other_term->{id}, @{$other_term->{alt_id}}]);
  14         236  
241              
242 14 50       2502 if (scalar($lc->get_intersection()) == 0) {
243 0         0 return undef;
244             }
245              
246 14         266 my @new_alt_id = List::Compare->new([$lc->get_union()], [$self->id()])->get_unique(1);
247              
248 14         2142 $self->{alt_id} = \@new_alt_id;
249              
250             my $merge_field = sub {
251 32     32   47 my $name = shift;
252 32         48 my $other_term = shift;
253              
254 32         56 my $field_conf = $PomBase::Chobo::OntologyConf::field_conf{$name};
255              
256 32 50       82 if (defined $field_conf) {
257 32 100 100     157 if (defined $field_conf->{type} &&
      66        
258             ($field_conf->{type} eq 'SINGLE' || $field_conf->{type} eq 'SINGLE_HASH')) {
259 18         29 my $res = undef;
260 18 100       52 if (defined $field_conf->{merge}) {
261 9         33 $res = $field_conf->{merge}->($self, $other_term);
262             }
263              
264 18 100       70 if (defined $res) {
265 2         8 $self->{$name} = $res;
266             } else {
267 16         45 my $new_field_value = $other_term->{$name};
268              
269 16 100       58 if (defined $new_field_value) {
270 12 100 66     87 if (!defined $self->{$name} ||
      100        
271             ($name eq 'namespace' &&
272             $self->{_namespace_from_metadata})) {
273 9         38 $self->{$name} = $new_field_value;
274             } else {
275 3 50 33     13 if ($name ne 'namespace' || !$other_term->{_namespace_from_metadata}) {
276             warn qq|new "$name" tag of this stanza (from |,
277             $other_term->source_file(), " line ",
278             $other_term->source_file_line_number(), ") ",
279             "differs from previously ",
280             "seen value (from ", $self->source_file(),
281             " line ", $self->source_file_line_number(), q|) "|,
282 3         43 $orig_term->{$name}, '" ',
283             qq(- ignoring new value: "$new_field_value"\n\n),
284             "while merging: \n" . $other_term->to_string() . "\n\n",
285             "into existing term:\n",
286             $orig_term->to_string(), "\n\n";
287             }
288             }
289             } else {
290             # no merging to do
291             }
292             }
293             } else {
294 14         21 my $new_field_value = $other_term->{$name};
295 14         39 for my $single_value (@$new_field_value) {
296 10 100       17 if (!grep { Compare($_, $single_value) } @{$self->{$name}}) {
  5         180  
  10         29  
297 9         502 push @{$self->{$name}}, clone $single_value;
  9         99  
298             }
299             }
300             }
301             } else {
302 0         0 die "unhandled field in merge(): $name\n";
303             }
304 14         93 };
305              
306 14         34 for my $field_name (@field_names) {
307 224 100 100     8497 next if $field_name eq 'id' or $field_name eq 'alt_id';
308              
309 196 100       572 if (!Compare($self->{$field_name}, $other_term->{$field_name})) {
310 32         1446 $merge_field->($field_name, $other_term);
311             }
312             }
313              
314 14         831 return $self;
315             }
316              
317             sub to_string
318             {
319 12     12 0 3293 my $self = shift;
320              
321 12         23 my @lines = ();
322              
323 12 50       38 if ($self->is_relationshiptype()) {
324 0         0 push @lines, "[Typedef]";
325             } else {
326 12         21 push @lines, "[Term]";
327             }
328              
329             my $line_maker = sub {
330 60     60   74 my $name = shift;
331 60         97 my $value = shift;
332              
333 60         74 my @ret_lines = ();
334              
335 60 100       86 if (ref $value) {
336 26         33 my @values;
337 26 100       57 if ($field_conf{$name}->{type} eq 'SINGLE_HASH') {
338 2         5 push @values, $value;
339             } else {
340 24         65 @values = @$value;
341             }
342 26         49 for my $single_value (@values) {
343 40         55 my $to_string_proc = $field_conf{$name}->{to_string};
344 40         48 my $value_as_string;
345 40 100       53 if (defined $to_string_proc) {
346 19         46 $value_as_string = $to_string_proc->($single_value);
347             } else {
348 21         26 $value_as_string = $single_value;
349             }
350 40         83 push @ret_lines, "$name: $value_as_string";
351             }
352             } else {
353 34         63 push @ret_lines, "$name: $value";
354             }
355              
356 60         148 return @ret_lines;
357 12         51 };
358              
359 12         22 for my $field_name (@field_names) {
360 192         267 my $field_value = $self->{$field_name};
361              
362 192 100       331 if (defined $field_value) {
363 60         87 push @lines, $line_maker->($field_name, $field_value);
364             }
365             }
366              
367 12         333 return join "\n", @lines;
368             }
369              
370             1;