File Coverage

blib/lib/TBX/Min.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1             #
2             # This file is part of TBX-Min
3             #
4             # This software is copyright (c) 2013 by Alan Melby.
5             #
6             # This is free software; you can redistribute it and/or modify it under
7             # the same terms as the Perl 5 programming language system itself.
8             #
9             package TBX::Min;
10 6     6   277540 use strict;
  6         15  
  6         205  
11 6     6   31 use warnings;
  6         12  
  6         272  
12             our $VERSION = '0.06'; # VERSION
13             # ABSTRACT: Read, write and edit TBX-Min files
14 6     6   12883 use XML::Twig;
  0            
  0            
15             use autodie;
16             use Path::Tiny;
17             use Carp;
18             use TBX::Min::Entry;
19             use TBX::Min::LangGroup;
20             use TBX::Min::TermGroup;
21             use Import::Into;
22             use DateTime::Format::ISO8601;
23             use Try::Tiny;
24              
25             # Use Import::Into to export subclasses into caller
26             sub import {
27             my $target = caller;
28             TBX::Min::Entry->import::into($target);
29             TBX::Min::LangGroup->import::into($target);
30             TBX::Min::TermGroup->import::into($target);
31             return;
32             }
33              
34             sub new_from_xml {
35             my ($class, $data) = @_;
36              
37             if(!$data){
38             croak 'missing required data argument';
39             }
40              
41             my $fh = _get_handle($data);
42              
43             # build a twig out of the input document
44             my $twig = XML::Twig->new(
45             output_encoding => 'UTF-8',
46             # do_not_chain_handlers => 1, #can be important when things get complicated
47             keep_spaces => 0,
48              
49             # these store new entries, langGroups and termGroups
50             start_tag_handlers => {
51             entry => \&_conceptStart,
52             langGroup => \&_langStart,
53             termGroup => \&_termGrpStart,
54             },
55              
56             TwigHandlers => {
57             TBX => \&_check_dialect,
58             # header attributes become attributes of the TBX::Min object
59             id => \&_headerAtt,
60             description => \&_headerAtt,
61             dateCreated => \&_date_created,
62             creator => \&_headerAtt,
63             license => \&_headerAtt,
64             directionality => \&_directionality,
65             languages => \&_languages,
66              
67             # becomes part of the current TBX::Min::Entry object
68             subjectField => sub {
69             shift->{tbx_min_entries}->[-1]->subject_field($_->text)},
70              
71             # these become attributes of the current TBX::Min::TermGroup object
72             term => sub {shift->{tbx_min_current_term_grp}->term($_->text)},
73             partOfSpeech => sub {
74             shift->{tbx_min_current_term_grp}->part_of_speech($_->text)},
75             note => sub {shift->{tbx_min_current_term_grp}->note($_->text)},
76             customer => sub {
77             shift->{tbx_min_current_term_grp}->customer($_->text)},
78             termStatus => sub {
79             shift->{tbx_min_current_term_grp}->status($_->text)},
80             }
81             );
82              
83             # use handlers to process individual tags, then grab the result
84             $twig->parse($fh);
85             my $self = $twig->{tbx_min_att};
86             $self->{entries} = $twig->{tbx_min_entries} || [];
87             bless $self, $class;
88             return $self;
89             }
90              
91             sub _get_handle {
92             my ($data) = @_;
93             my $fh;
94             if((ref $data) eq 'SCALAR'){
95             open $fh, '<', $data; ## no critic(RequireBriefOpen)
96             }else{
97             $fh = path($data)->filehandle('<');
98             }
99             return $fh;
100             }
101              
102             sub new {
103             my ($class, $args) = @_;
104             my $self;
105             if((ref $args) eq 'HASH'){
106             #don't store a plain string for datetime
107             if(my $dt_string = $args->{date_created}){
108             $args->{date_created} = _parse_datetime($dt_string);
109             }
110             if(exists $args->{directionality}){
111             _validate_dir($args->{directionality});
112             }
113             $self = $args;
114             }else{
115             $self = {};
116             }
117             $self->{entries} ||= [];
118             return bless $self, $class;
119             }
120              
121             sub id {
122             my ($self, $id) = @_;
123             if($id) {
124             return $self->{id} = $id;
125             }
126             return $self->{id};
127             }
128              
129             sub description {
130             my ($self, $description) = @_;
131             if($description) {
132             return $self->{description} = $description;
133             }
134             return $self->{description};
135             }
136              
137             sub date_created {
138             my ($self, $date_created) = @_;
139             if($date_created) {
140             return $self->{date_created} =
141             _parse_datetime($date_created);
142             }
143             if(my $dt = $self->{date_created}){
144             return $dt->iso8601;
145             }
146             return;
147             }
148              
149             sub _parse_datetime {
150             my ($dt_string) = @_;
151             my $dt;
152             try{
153             $dt = DateTime::Format::ISO8601->parse_datetime($dt_string);
154             }catch{
155             croak 'date is not in ISO8601 format';
156             };
157             return $dt;
158             }
159              
160             sub creator {
161             my ($self, $creator) = @_;
162             if($creator) {
163             return $self->{creator} = $creator;
164             }
165             return $self->{creator};
166             }
167              
168             sub license {
169             my ($self, $license) = @_;
170             if($license) {
171             return $self->{license} = $license;
172             }
173             return $self->{license};
174             }
175              
176             sub directionality {
177             my ($self, $directionality) = @_;
178             if(defined $directionality) {
179             _validate_dir($directionality);
180             return $self->{directionality} = $directionality;
181             }
182             return $self->{directionality};
183             }
184              
185             sub _validate_dir {
186             my ($dir) = @_;
187             if($dir ne 'bidirectional' and $dir ne 'monodirectional'){
188             croak "Illegal directionality '$dir'";
189             }
190             return;
191             }
192              
193              
194             sub source_lang {
195             my ($self, $source_lang) = @_;
196             if($source_lang) {
197             return $self->{source_lang} = $source_lang;
198             }
199             return $self->{source_lang};
200             }
201              
202             sub target_lang {
203             my ($self, $target_lang) = @_;
204             if($target_lang) {
205             return $self->{target_lang} = $target_lang;
206             }
207             return $self->{target_lang};
208             }
209              
210             sub entries { ## no critic(RequireArgUnpacking)
211             my ($self) = @_;
212             if (@_ > 1){
213             croak 'extra argument found (entries is a getter only)';
214             }
215             return $self->{entries};
216             }
217              
218             sub add_entry {
219             my ($self, $entry) = @_;
220             if( !$entry || !$entry->isa('TBX::Min::Entry') ){
221             croak 'argument to add_entry should be a TBx::Min::Entry';
222             }
223             push @{$self->{entries}}, $entry;
224             return;
225             }
226              
227             sub as_xml {
228             my ($self) = @_;
229              
230             # construct the whole document using XML::Twig::El's
231             my $root = XML::Twig::Elt->new(TBX => {dialect => 'TBX-Min'});
232             my $header = XML::Twig::Elt->new('header')->paste($root);
233              
234             # each of these header elements is a simple element with text
235             for my $header_att (
236             qw(id creator license directionality description)){
237             next unless $self->{$header_att};
238             XML::Twig::Elt->new($header_att,
239             $self->{$header_att})->paste(last_child => $header);
240             }
241             if($self->source_lang || $self->target_lang){
242             my @atts;
243             push @atts, (source => $self->source_lang) if $self->source_lang;
244             push @atts, (target => $self->target_lang) if $self->target_lang;
245             XML::Twig::Elt->new(languages => {@atts})->paste(
246             last_child => $header)
247             }
248             if(my $dt = $self->{date_created}){
249             XML::Twig::Elt->new(dateCreated => $dt->iso8601)->paste(
250             last_child => $header);
251             }
252              
253             my $body = XML::Twig::Elt->new('body')->paste(last_child => $root);
254             for my $entry (@{$self->entries}){
255             my $entry_el = XML::Twig::Elt->new(
256             entry => {$entry->id ? (id => $entry->id) : ()})->
257             paste(last_child => $body);
258             if(my $sf = $entry->subject_field){
259             XML::Twig::Elt->new(subjectField => $sf)->paste(
260             last_child => $entry_el);
261             }
262             for my $langGrp (@{$entry->lang_groups}){
263             my $lang_el = XML::Twig::Elt->new(langGroup =>
264             {$langGrp->code ? ('xml:lang' => $langGrp->code) : ()}
265             )->paste(last_child => $entry_el);
266             for my $termGrp (@{$langGrp->term_groups}){
267             my $term_el = XML::Twig::Elt->new('termGroup')->paste(
268             last_child => $lang_el);
269             if (my $term = $termGrp->term){
270             XML::Twig::Elt->new(term => $term)->paste(
271             last_child => $term_el);
272             }
273              
274             if (my $customer = $termGrp->customer){
275             XML::Twig::Elt->new(customer => $customer)->paste(
276             last_child => $term_el);
277             }
278              
279             if (my $note = $termGrp->note){
280             XML::Twig::Elt->new(note => $note)->paste(
281             last_child => $term_el);
282             }
283              
284             if (my $status = $termGrp->status){
285             XML::Twig::Elt->new(termStatus => $status )->paste(
286             last_child => $term_el);
287             }
288              
289             if (my $pos = $termGrp->part_of_speech){
290             XML::Twig::Elt->new(partOfSpeech => $pos)->paste(
291             last_child => $term_el);
292             }
293              
294             } # end termGroup
295             } # end langGroup
296             } # end entry
297              
298             # return pretty-printed string
299             XML::Twig->set_pretty_print('indented');
300             return \$root->sprint;
301             }
302              
303             ######################
304             ### XML TWIG HANDLERS
305             ######################
306              
307             # croak if the user happened to use the wrong dialect of TBX
308             sub _check_dialect {
309             my (undef, $node) = @_;
310             my $type = $node->att('dialect') || 'unknown';
311             my $expected = 'TBX-Min';
312             if($type ne $expected){
313             croak "Input TBX is $type (should be '$expected')";
314             }
315             return 1;
316             }
317              
318             # most of the twig handlers store state on the XML::Twig object.
319             # A bit kludgy, but it works.
320              
321             sub _headerAtt {
322             my ($twig, $node) = @_;
323             $twig->{tbx_min_att}->{_decamel($node->name)} = $node->text;
324             return 1;
325             }
326              
327             sub _directionality {
328             my ($twig, $node) = @_;
329             _validate_dir($node->text);
330             $twig->{tbx_min_att}->{directionality} = $node->text;
331             return 1;
332             }
333              
334             sub _date_created {
335             my ($twig, $node) = @_;
336             $twig->{tbx_min_att}->{date_created} =
337             _parse_datetime($node->text);
338             return;
339             }
340              
341             # turn camelCase into camel_case
342             sub _decamel {
343             my ($camel) = @_;
344             $camel =~ s/([A-Z])/_\l$1/g;
345             return $camel;
346             }
347              
348             sub _languages{
349             my ($twig, $node) = @_;
350             if(my $source = $node->att('source')){
351             ${ $twig->{'tbx_min_att'} }{'source_lang'} = $source;
352             }
353             if(my $target = $node->att('target')){
354             ${ $twig->{'tbx_min_att'} }{'target_lang'} = $target;
355             }
356             return 1;
357             }
358              
359             # add a new concept entry to the list of those found in this file
360             sub _conceptStart {
361             my ($twig, $node) = @_;
362             my $concept = TBX::Min::Entry->new();
363             if($node->att('id')){
364             $concept->id($node->att('id'));
365             }else{
366             carp 'found entry missing id attribute';
367             }
368             push @{ $twig->{tbx_min_entries} }, $concept;
369             return 1;
370             }
371              
372             #just set the subject_field of the current concept
373             sub _subjectField {
374             my ($twig, $node) = @_;
375             $twig->{tbx_min_entries}->[-1]->
376             subject_field($node->text);
377             return 1;
378             }
379              
380             # Create a new LangGroup, add it to the current concept,
381             # and set it as the current LangGroup.
382             sub _langStart {
383             my ($twig, $node) = @_;
384             my $lang = TBX::Min::LangGroup->new();
385             if($node->att('xml:lang')){
386             $lang->code($node->att('xml:lang'));
387             }else{
388             carp 'found langGroup missing xml:lang attribute';
389             }
390              
391             $twig->{tbx_min_entries}->[-1]->add_lang_group($lang);
392             $twig->{tbx_min_current_lang_grp} = $lang;
393             return 1;
394             }
395              
396             # Create a new termGroup, add it to the current langGroup,
397             # and set it as the current termGroup.
398             sub _termGrpStart {
399             my ($twig) = @_;
400             my $term = TBX::Min::TermGroup->new();
401             $twig->{tbx_min_current_lang_grp}->add_term_group($term);
402             $twig->{tbx_min_current_term_grp} = $term;
403             return 1;
404             }
405              
406             1;
407              
408             __END__