File Coverage

blib/lib/PomBase/Chobo/OntologyTerm.pm
Criterion Covered Total %
statement 141 152 92.7
branch 50 60 83.3
condition 36 48 75.0
subroutine 17 17 100.0
pod 2 8 25.0
total 246 285 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.039'; # VERSION
39              
40 5     5   88526 use Mouse;
  5         29865  
  5         28  
41 5     5   1827 use Carp;
  5         10  
  5         368  
42              
43 5     5   2542 use PomBase::Chobo::OntologyConf;
  5         11  
  5         203  
44              
45 5     5   487 use Clone qw(clone);
  5         2575  
  5         254  
46 5     5   2490 use Data::Compare;
  5         57965  
  5         34  
47 5     5   23585 use List::Compare;
  5         117338  
  5         1625  
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 5     5   99 %field_conf = %PomBase::Chobo::OntologyConf::field_conf;
73 5         29 @field_names = qw(id name);
74              
75 5 100       28 for my $field_name (sort grep { $_ ne 'id' && $_ ne 'name' } keys %field_conf) {
  80         300  
76 70         8516 push @field_names, $field_name;
77             }
78             }
79              
80             sub synonyms
81             {
82 6     6 0 12 my $self = shift;
83              
84 6   100     12 return @{$self->{synonym} // []};
  6         87  
85             }
86              
87             sub alt_ids
88             {
89 149     149 0 220 my $self = shift;
90              
91             return map {
92 41         77 my $val = $_;
93              
94 41 100       189 if ($val =~ /(\S+):(\S+)/) {
95             {
96 37         268 id => $val,
97             db_name => $1,
98             accession => $2,
99             };
100             } else {
101 4         12 my $db_name;
102 4 50 33     39 if (defined $self->metadata()->{ontology} &&
103             $self->metadata()->{ontology} eq 'ro') {
104 0         0 $db_name = 'OBO_REL'
105             } else {
106 4         11 $db_name = '_global';
107             }
108             {
109 4         22 id => $val,
110             db_name => $db_name,
111             accession => $val,
112             };
113             }
114 149   50     197 } @{$self->{alt_id} // []};
  149         571  
115             }
116              
117             sub property_values
118             {
119 6     6 0 1106 my $self = shift;
120              
121 6   100     12 return @{$self->{property_value} // []};
  6         52  
122             }
123              
124             sub subsets
125             {
126 2     2 0 2649 my $self = shift;
127              
128 2   100     4 return @{$self->{subset} // []};
  2         27  
129             }
130              
131             sub xrefs
132             {
133 77     77 0 180 my $self = shift;
134              
135 77   100     110 return @{$self->{xref} // []};
  77         364  
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 79     79 1 12451 my $class = shift;
148 79         116 my $object = shift;
149 79         121 my $options = shift;
150              
151 79 50       177 if (!defined $object) {
152 0         0 croak "no argument passed to new()";
153             }
154              
155 79 100 66     396 if ($object->{def} && $object->{def}->{dbxrefs} && $object->{alt_id}) {
      66        
156 10         19 for my $alt_id (@{$object->{alt_id}}) {
  10         41  
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 15         47 @{$object->{def}->{dbxrefs}} =
161             grep {
162 11         19 my $xref = $_;
163 11         31 $alt_id ne $xref;
164 15         20 } @{$object->{def}->{dbxrefs}};
  15         31  
165             }
166             }
167              
168 79 100 66     249 if ($object->{is_obsolete} && $object->{name} && $object->{name} !~ /^obsolete/i) {
      66        
169 2         14 $object->{name} = "OBSOLETE " . $object->{id} . " " . $object->{name};
170             }
171              
172 79 50 66     200 if ($object->{is_relationshiptype} && $object->{name}) {
173 10         38 $object->{name} =~ s/ /_/g;
174             }
175              
176 79         160 $object->{_namespace_from_metadata} = 0;
177              
178 79 100       172 if ($options) {
179 72 100       160 if ($options->{namespace_from_metadata}) {
180 34         56 $object->{_namespace_from_metadata} = 1;
181             }
182             }
183              
184 79   100     311 $object->{alt_id} //= [];
185              
186 79         163 my ($db_name, $accession);
187              
188 79 100       564 unless (($db_name, $accession) = $object->{id} =~ /^(\S+):(.+?)\s*$/) {
189 11 50       48 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 11         23 $db_name = '_global';
201 11         20 $accession = $object->{id};
202             }
203             }
204              
205 79         185 $object->{accession} = $accession;
206 79         189 $object->{db_name} = $db_name;
207              
208 79 50       226 if (!defined $object->{source_file}) {
209 0         0 confess "source_file attribute of object is required\n";
210             }
211              
212 79 50       172 if (!defined $object->{source_file_line_number}) {
213 0         0 confess "source_file_line attribute of object is required\n";
214             }
215              
216 79         250 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 14     14 1 4772 my $self = shift;
233 14         24 my $other_term = shift;
234              
235 14 100       49 return if $self == $other_term;
236              
237 13         40 my $lc = List::Compare->new([$self->{id}, @{$self->{alt_id}}],
238 13         30 [$other_term->{id}, @{$other_term->{alt_id}}]);
  13         90  
239              
240 13 50       1905 if (scalar($lc->get_intersection()) == 0) {
241 0         0 return undef;
242             }
243              
244 13         236 my @new_alt_id = List::Compare->new([$lc->get_union()], [$self->id()])->get_unique(1);
245              
246 13         1897 $self->{alt_id} = \@new_alt_id;
247              
248             my $merge_field = sub {
249 28     28   45 my $name = shift;
250 28         44 my $other_term = shift;
251              
252 28         51 my $field_conf = $PomBase::Chobo::OntologyConf::field_conf{$name};
253              
254 28 50       60 if (defined $field_conf) {
255 28 100 100     152 if (defined $field_conf->{type} &&
      66        
256             ($field_conf->{type} eq 'SINGLE' || $field_conf->{type} eq 'SINGLE_HASH')) {
257 15         26 my $res = undef;
258 15 100       33 if (defined $field_conf->{merge}) {
259 7         30 $res = $field_conf->{merge}->($self, $other_term);
260             }
261              
262 15 100       36 if (defined $res) {
263 1         4 $self->{$name} = $res;
264             } else {
265 14         23 my $new_field_value = $other_term->{$name};
266              
267 14 100       39 if (defined $new_field_value) {
268 10 100 66     61 if (!defined $self->{$name} ||
      100        
269             ($name eq 'namespace' &&
270             $self->{_namespace_from_metadata})) {
271 7         26 $self->{$name} = $new_field_value;
272             } else {
273 3 50 33     17 if ($name ne 'namespace' || !$other_term->{_namespace_from_metadata}) {
274             warn qq|new "$name" tag of this stanza (from |,
275             $other_term->source_file(), " line ",
276             $other_term->source_file_line_number(), ") ",
277             "differs from previously ",
278             "seen value (from ", $self->source_file(),
279             " line ", $self->source_file_line_number(), q|) "|,
280 3         56 $self->{$name}, '" ',
281             qq(- ignoring new value: "$new_field_value"\n\n),
282             "while merging: \n" . $other_term->to_string() . "\n\n",
283             "into existing term:\n",
284             $self->to_string(), "\n\n";
285             }
286             }
287             } else {
288             # no merging to do
289             }
290             }
291             } else {
292 13         23 my $new_field_value = $other_term->{$name};
293 13         40 for my $single_value (@$new_field_value) {
294 9 100       32 if (!grep { Compare($_, $single_value) } @{$self->{$name}}) {
  5         144  
  9         23  
295 8         622 push @{$self->{$name}}, clone $single_value;
  8         99  
296             }
297             }
298             }
299             } else {
300 0         0 die "unhandled field in merge(): $name\n";
301             }
302 13         86 };
303              
304 13         34 for my $field_name (@field_names) {
305 208 100 100     9486 next if $field_name eq 'id' or $field_name eq 'alt_id';
306              
307 182 100       578 if (!Compare($self->{$field_name}, $other_term->{$field_name})) {
308 28         1348 $merge_field->($field_name, $other_term);
309             }
310             }
311              
312 13         787 return $self;
313             }
314              
315             sub to_string
316             {
317 12     12 0 1926 my $self = shift;
318              
319 12         23 my @lines = ();
320              
321 12 50       55 if ($self->is_relationshiptype()) {
322 0         0 push @lines, "[Typedef]";
323             } else {
324 12         25 push @lines, "[Term]";
325             }
326              
327             my $line_maker = sub {
328 60     60   88 my $name = shift;
329 60         82 my $value = shift;
330              
331 60         83 my @ret_lines = ();
332              
333 60 100       126 if (ref $value) {
334 26         33 my @values;
335 26 100       77 if ($field_conf{$name}->{type} eq 'SINGLE_HASH') {
336 2         4 push @values, $value;
337             } else {
338 24         48 @values = @$value;
339             }
340 26         40 for my $single_value (@values) {
341 41         69 my $to_string_proc = $field_conf{$name}->{to_string};
342 41         84 my $value_as_string;
343 41 100       87 if (defined $to_string_proc) {
344 19         58 $value_as_string = $to_string_proc->($single_value);
345             } else {
346 22         31 $value_as_string = $single_value;
347             }
348 41         114 push @ret_lines, "$name: $value_as_string";
349             }
350             } else {
351 34         89 push @ret_lines, "$name: $value";
352             }
353              
354 60         144 return @ret_lines;
355 12         54 };
356              
357 12         38 for my $field_name (@field_names) {
358 192         287 my $field_value = $self->{$field_name};
359              
360 192 100       342 if (defined $field_value) {
361 60         101 push @lines, $line_maker->($field_name, $field_value);
362             }
363             }
364              
365 12         355 return join "\n", @lines;
366             }
367              
368             1;