File Coverage

blib/lib/RDF/TrineX/Serializer/MockTurtleSoup.pm
Criterion Covered Total %
statement 21 23 91.3
branch n/a
condition n/a
subroutine 8 8 100.0
pod n/a
total 29 31 93.5


line stmt bran cond sub pod time code
1             package RDF::TrineX::Serializer::MockTurtleSoup;
2              
3 1     1   8764 use 5.010001;
  1         4  
  1         63  
4 1     1   7 use strict;
  1         2  
  1         38  
5 1     1   6 use warnings;
  1         11  
  1         47  
6 1     1   1749 use utf8;
  1         12  
  1         6  
7              
8             BEGIN {
9 1     1   118 $RDF::TrineX::Serializer::MockTurtleSoup::AUTHORITY = 'cpan:TOBYINK';
10 1         19 $RDF::TrineX::Serializer::MockTurtleSoup::VERSION = '0.005';
11             }
12              
13 1     1   7 use Carp;
  1         2  
  1         168  
14 1     1   2879 use Sort::Key;
  1         7552  
  1         70  
15 1     1   865 use RDF::Trine;
  0            
  0            
16             use RDF::Trine::Namespace qw( rdf rdfs );
17             use RDF::Prefixes;
18             use match::smart qw(match);
19              
20             use parent 'RDF::Trine::Serializer';
21              
22             sub new
23             {
24             my $class = shift;
25             my $self = bless { @_==1 ? %{$_[0]} : @_ } => $class;
26            
27             $self->{prefixes} ||= delete $self->{namespaces};
28             $self->{labelling} //= $rdfs->label->uri;
29             $self->{priorities} ||= undef;
30             $self->{abbreviate} //= undef;
31             $self->{prefixes} ||= {};
32             $self->{colspace} //= 20;
33             $self->{indent} ||= "\t";
34             $self->{repeats} //= 0;
35             $self->{encoding} ||= "utf8";
36             $self->{apostrophe} //= 0;
37            
38             croak("Bad indent")
39             unless $self->{indent} =~ /^\s+$/;
40             croak("Bad encoding: expected 'utf8' or 'ascii'")
41             unless $self->{encoding} =~ /^(ascii|utf8)$/;
42            
43             return $self;
44             }
45              
46             sub serialize_model_to_file
47             {
48             my $self = shift;
49             my ($fh, $model) = @_;
50            
51             local $self->{model} = $model;
52             local $self->{p} = RDF::Prefixes->new($self->{prefixes});
53             local $self->{B} = 0;
54             local $self->{b} = {};
55            
56             my $bunches = $self->_divvy_up;
57             $self->_sort_bunches($bunches);
58             $self->_serialize_bunches($bunches, $fh);
59             }
60              
61             sub _is_labelling
62             {
63             my $self = shift;
64             my ($st) = @_;
65             return 1 if match($st->predicate->uri, $self->{labelling});
66             return;
67             }
68              
69             sub _node
70             {
71             my $self = shift;
72             my ($c, $t, $n) = @_;
73             $n //= $t->$c;
74            
75             if ($c eq 'predicate' and $n->equal($rdf->type))
76             {
77             return 'a';
78             }
79            
80             if ($c eq 'object'
81             and defined $t
82             and $t->predicate->equal($rdf->type)
83             and $n->is_resource)
84             {
85             return $_ for grep defined, $self->{p}->get_qname($n->uri);
86             }
87            
88             if ($n->is_resource
89             and $c eq 'predicate' || match($n->uri, $self->{abbreviate}))
90             {
91             return $_ for grep defined, $self->{p}->get_qname($n->uri);
92             }
93            
94             if ($n->is_literal and $n->has_datatype)
95             {
96             my $dt = $self->{p}->get_qname($n->literal_datatype);
97             if ($dt eq 'xsd:integer' && $n->literal_value =~ /^[+-]?[0-9]+$/
98             or $dt eq 'xsd:decimal' && $n->literal_value =~ /^[+-]?[0-9]*\.[0-9]+$/
99             or $dt eq 'xsd:double' && $n->literal_value =~ /^(?:(?:[+-]?[0-9]+\.[0-9]+)|(?:[+-]?\.[0-9]+)|(?:[+-]?[0-9]))[Ee][+-]?[0-9]+$/)
100             {
101             return $n->literal_value;
102             }
103             elsif ($dt eq 'xsd:boolean' && $n->literal_value =~ /^(true|false)$/i)
104             {
105             return lc $n->literal_value;
106             }
107             elsif (defined $dt)
108             {
109             return sprintf('%s^^%s', $self->_escaped_quoted_string($n->literal_value), $dt);
110             }
111             }
112             elsif ($n->is_literal and $n->has_language)
113             {
114             return sprintf('%s@%s', $self->_escaped_quoted_string($n->literal_value), $n->literal_value_language);
115             }
116             elsif ($n->is_literal)
117             {
118             return $self->_escaped_quoted_string($n->literal_value);
119             }
120            
121             if ($n->is_blank)
122             {
123             return($self->{b}{$n} //= '_:B' . ++$self->{B});
124             }
125            
126             return $n->as_ntriples;
127             }
128              
129             {
130             my %ESCAPE = (
131             "\t" => "\\t",
132             "\r" => "\\r",
133             "\n" => "\\n",
134             "\"" => "\\\"",
135             "\'" => "\\\'",
136             "\\" => "\\\\",
137             );
138            
139             sub _escaped_quoted_string
140             {
141             my $self = shift;
142             my ($str) = @_;
143            
144             my $quote = '"';
145             my $chars = '\x00-\x1F\x5C';
146            
147             if ($self->{apostrophe} and $str =~ /\"/ and not $str =~ /\'/)
148             {
149             $quote = "'";
150             }
151             else
152             {
153             $chars .= '\x22'
154             }
155            
156             if ($self->{encoding} eq "ascii")
157             {
158             $chars .= '\x{0080}-\x{10FFFF}';
159             }
160            
161             $str =~ s{([$chars])}{
162             exists($ESCAPE{$1}) ? $ESCAPE{$1} :
163             ord($1) <= 0xFFFF ? sprintf('\u%04X', ord($1)) : sprintf('\U%08X', ord($1))
164             }xeg;
165            
166             "$quote$str$quote";
167             }
168             }
169              
170             sub _serialize_bunch
171             {
172             my $self = shift;
173             my ($bunch, $bunchmap, $indent, $in_brackets) = @_;
174             $bunch->{done}++;
175            
176             my @triples = sort
177             {
178             ($a->predicate->equal($rdf->type) && !$b->predicate->equal($rdf->type)) ? -1 :
179             ($b->predicate->equal($rdf->type) && !$a->predicate->equal($rdf->type)) ? 1 :
180             ($a->predicate->equal($rdf->type) && !$b->predicate->equal($rdf->type)) ? -1 :
181             ($b->predicate->equal($rdf->type) && !$a->predicate->equal($rdf->type)) ? 1 :
182             ($self->_is_labelling($a) && !$self->_is_labelling($b) ) ? -1 :
183             ($self->_is_labelling($b) && !$self->_is_labelling($a) ) ? 1 :
184             ($self->_node(predicate => $a) cmp $self->_node(predicate => $b) or $a->object->compare($b->object))
185             }
186             @{ $bunch->{triples} || [] };
187            
188             my $str;
189             my $last_p;
190            
191             # $str .= "$indent### $bunch->{subject}\n";
192            
193             my $smiple = 1;
194             if ($in_brackets)
195             {
196             $str .= "[\n";
197             $indent .= $self->{indent};
198             }
199             elsif ($bunch->{subject}->is_blank
200             and $bunch->{inline}
201             and !$bunch->{inlist}
202             and !$self->{model}->count_statements(undef, undef, $bunch->{subject}))
203             {
204             $str .= "$indent\[]\n";
205             }
206             else
207             {
208             $str .= $indent . $self->_node(subject => undef, $bunch->{subject}) . "\n" ;
209             }
210            
211             for my $t (@triples)
212             {
213             if (defined $last_p and $last_p->equal($t->predicate) and not $self->{repeats}) {
214             $str =~ s/;\n$/,/s;
215             }
216             else {
217             $str .= "$indent$self->{indent}";
218             $str .= sprintf($indent?"%s":"%-${\ $self->{colspace} }s", $self->_node(predicate => $t));
219             }
220             $str .= " ";
221            
222             $last_p = $t->predicate;
223            
224             if ($t->object->is_blank
225             and $bunchmap->{$t->object}{inline}
226             and $bunchmap->{$t->object}{list}
227             and not $bunchmap->{$t->object}{done})
228             {
229             my @str;
230             $smiple = 0;
231            
232             push my(@turds), (my $head = $t->object);
233             while ($head)
234             {
235             my ($next) = $self->{model}->objects($head, $rdf->rest);
236             last if $next->equal($rdf->nil);
237             push @turds, ($head = $next);
238             }
239             # $str .= "$indent#TURDS: @turds\n";
240             $bunchmap->{$_}{done}++ for @turds;
241            
242             for my $i (@{$bunchmap->{$t->object}{list}})
243             {
244             push @str, $self->_node(object => undef, $i);
245             }
246            
247             $str .= "(@str)";
248             }
249             elsif ($t->object->is_blank
250             and $bunchmap->{$t->object}{inline}
251             and not $bunchmap->{$t->object}{done})
252             {
253             if (not @{$bunchmap->{$t->object}{triples}||[]}) {
254             $str .= "[]";
255             }
256             else {
257             $smiple = 0;
258             $str .= $self->_serialize_bunch($bunchmap->{$t->object}, $bunchmap, "$indent", 1);
259             }
260             }
261             else
262             {
263             my $x = $self->_node(object => $t);
264             $str .= $x;
265             $smiple = 0 if length($x) > 40;
266             }
267             $str .= ";\n";
268             }
269            
270             if ($in_brackets)
271             {
272             $str .= "$indent]";
273             }
274             else
275             {
276             $str =~ s/;\n$/.\n/s ;
277             }
278            
279             if ($in_brackets
280             and length($str) < ($smiple ? 60 : 40)
281             and $str =~ m{^\s*\[\s*(.+);\s*\]\s*$}sm)
282             {
283             (my $new = $1)
284             =~ s/\;\n\s*/\; /gsm;
285             return "[ $new ]";
286             }
287            
288             return $str;
289             }
290              
291             sub _serialize_bunches
292             {
293             my $self = shift;
294             my ($bunches, $fh) = @_;
295            
296             my $bunchmap = {};
297             $bunchmap->{$_->{subject}} = $_ for @$bunches;
298            
299             my $str = "";
300            
301             for my $bunch (@$bunches) {
302             next if $bunch->{done};
303             next unless @{ $bunch->{triples} || [] };
304             $str .= $self->_serialize_bunch($bunch, $bunchmap, "") . "\n";
305             }
306            
307             print {$fh} $self->{p}->turtle, "\n", $str;
308             }
309              
310             sub _get_priority
311             {
312             my $self = shift;
313             my ($bunch) = @_;
314             my $n; $n = $self->{priorities}->($self, $bunch->{subject}, $self->{model}) if $self->{priorities};
315             return $n if defined $n;
316             return 0;
317             }
318              
319             sub _sort_bunches
320             {
321             my $self = shift;
322             my ($bunches) = @_;
323            
324             my $sorter = Sort::Key::multikeysorter_inplace(
325             sub {
326             no warnings;
327             $_->{isturd}+0,
328             $self->_get_priority($_),
329             ref($_->{subject}),
330             $_->{inlist}+0,
331             "$_->{subject}",
332             },
333             qw( int -int -str int str )
334             );
335             $sorter->($bunches);
336             }
337              
338             sub _divvy_up
339             {
340             my $self = shift;
341             my $model = $self->{model};
342            
343             my %bnodes;
344             my %d;
345             my $stream = $model->as_stream;
346             while (my $st = $stream->next)
347             {
348             $bnodes{$st->subject}//=0 if $st->subject->is_blank;
349             $bnodes{$st->object}++ if $st->object->is_blank;
350             push @{ $d{$st->subject}{triples} }, $st;
351             $d{$st->subject}{subject} = $st->subject;
352             }
353            
354             for my $k (keys %bnodes) {
355             $d{$k}{inline}++ if $bnodes{$k}<=1;
356             $d{$k}{subject} //= RDF::Trine::Node::Blank->new(substr $k, 2);
357             if ($self->_check_valid_rdf_list($d{$k}{subject}, $model))
358             {
359             $d{$k}{list} = [ $model->get_list($d{$k}{subject}) ];
360             # print "#LIST: ", join(" ", @{$d{$k}{list}}), "\n";
361             $d{$_}{inlist}++ for grep !$_->is_literal, @{$d{$k}{list}};
362             }
363             }
364            
365             $d{$_->subject}{isturd}++ for $model->get_statements(undef, $rdf->rest, undef)->get_all;
366            
367             # use Data::Dumper; print Dumper \%d;
368             [values %d];
369             }
370              
371             sub _check_valid_rdf_list {
372             require RDF::Trine::Serializer::Turtle;
373             goto \&RDF::Trine::Serializer::Turtle::_check_valid_rdf_list;
374             }
375              
376             1;
377              
378             __END__
379              
380             =pod
381              
382             =encoding utf-8
383              
384             =head1 NAME
385              
386             RDF::TrineX::Serializer::MockTurtleSoup - he's a bit slow, but he's sure good lookin'
387              
388             =head1 SYNOPSIS
389              
390             use RDF::TrineX::Serializer::MockTurtleSoup;
391            
392             my $ser = "RDF::TrineX::Serializer::MockTurtleSoup"->new(%opts);
393             $ser->serialize_model_to_file($fh, $model);
394              
395             =head1 DESCRIPTION
396              
397             Like L<RDF::Trine::Serializer::Turtle> but real pretty.
398              
399             And slower.
400              
401             And probably breaks with some complex graphs.
402              
403             =head2 What's so pretty?
404              
405             =over
406              
407             =item *
408              
409             Output interesting data first. Output URIs before bnodes. Output
410             rdf:type and rdfs:label before other predicates. Allow the user to
411             define criteria for what nodes are "interesting".
412              
413             =item *
414              
415             Use QNames for predicates, classes and datatypes, use full URIs
416             elsewhere. But also allow the user to supply a list of additional
417             URIs that will be abbreviated to QNames:
418              
419             "RDF::TrineX::Serializer::MockTurtleSoup"->new(
420             abbreviate => [
421             qr{^http://ontologi\.es/},
422             qr{^http://purl\.org/},
423             "http://www.google.com/",
424             ],
425             );
426              
427             =item *
428              
429             Generate those QNames using L<RDF::Prefixes> because it generates
430             awesome prefixes. (Better than "ns1", "ns2", etc.)
431              
432             =item *
433              
434             When data is equally interesting, sort alphabetically by subject,
435             predicate and object. When sorting by predicate, sort by the
436             predicate's QName, not its full URI.
437              
438             =item *
439              
440             Compact Turtle list syntax (mostly stolen from Greg's
441             L<RDF::Trine::Serializer::Turtle>)
442              
443             =item *
444              
445             Inline simple bnodes.
446              
447             =item *
448              
449             Indent nicely.
450              
451             =back
452              
453             =head2 Options
454              
455             The constructor supports the following options:
456              
457             =over
458              
459             =item C<abbreviate>
460              
461             This option will be used as the right-hand side of a smart match to
462             test URIs to see if they should be abbreviated to QNames.
463              
464             URIs used as predicates or as the object of rdf:type triples are always
465             abbreviated anyway. URIs which cannot be abbreviated to a legal QName
466             will just be output as URIs.
467              
468             =item C<apostrophe>
469              
470             Boolean; if true, then the serializer will sometimes quote literals with
471             an apostrophe instead of double-quote marks. This is allowed by recent
472             versions of the Turtle spec, but was disallowed by earlier specifications,
473             and not widely supported yet. Defaults to false.
474              
475             =item C<colspace>
476              
477             This allows your predicate-object pairs to line up as nice columns. The
478             smaller the number, the closer they get. Default is 20.
479              
480             =item C<encoding>
481              
482             Either "ascii" or "utf8". Default is "utf8".
483              
484             =item C<indent>
485              
486             A whitespace string to indent by. The default is one tab character.
487             (God's chosen indentation.)
488              
489             =item C<labelling>
490              
491             This option will be used as the right-hand side of a smart match to
492             determine which URIs are considered to be equivalent to C<rdfs:label>.
493             The default is just C<http://www.w3.org/2000/01/rdf-schema#label>.
494              
495             =item C<namespaces>
496              
497             A hashref of prefix => URI pairs to define preferred QName prefixes.
498             There is no guarantee that these will be honoured, but they usually
499             will. L<RDF::Prefixes> does a damn good job without any help, so this
500             is generally pretty unnecessary.
501              
502             =item C<priorities>
503              
504             If defined, must be a coderef. The coderef will be called with arguments:
505             the serializer object itself, a node and the L<RDF::Trine::Model> being
506             serialized.
507              
508             The coderef can use data within the model to determine how "interesting"
509             the node is. High numbers are very interesting. Negitive numbers are very
510             boring.
511              
512             Interesting nodes are more likely to appear earlier on in the output.
513              
514             Default is undef.
515              
516             =item C<repeats>
517              
518             Boolean. If false (the default), will output data like:
519              
520             <http://example.com/>
521             dc:title "Cat"@en, "Chat"@fr.
522              
523             If true, will output data like:
524              
525             <http://example.com/>
526             dc:title "Cat"@en;
527             dc:title "Chat"@fr.
528              
529             =back
530              
531             =head2 Methods
532              
533             This module provides the same API as L<RDF::Trine::Serializer>.
534              
535             =head1 BUGS
536              
537             Please report any bugs to
538             L<http://rt.cpan.org/Dist/Display.html?Queue=RDF-TrineX-Serializer-MockTurtleSoup>.
539              
540             =head1 SEE ALSO
541              
542             L<RDF::Trine::Serializer::Turtle>.
543              
544             =head1 AUTHOR
545              
546             Toby Inkster E<lt>tobyink@cpan.orgE<gt>.
547              
548             =head1 COPYRIGHT AND LICENCE
549              
550             This software is copyright (c) 2013 by Toby Inkster.
551              
552             This is free software; you can redistribute it and/or modify it under
553             the same terms as the Perl 5 programming language system itself.
554              
555             =head1 DISCLAIMER OF WARRANTIES
556              
557             THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED
558             WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
559             MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE.
560