File Coverage

blib/lib/Mojolicious/Plugin/Ical.pm
Criterion Covered Total %
statement 86 86 100.0
branch 7 12 58.3
condition 23 57 40.3
subroutine 12 12 100.0
pod 1 1 100.0
total 129 168 76.7


line stmt bran cond sub pod time code
1             package Mojolicious::Plugin::Ical;
2 2     2   2307267 use Mojo::Base 'Mojolicious::Plugin';
  2         7  
  2         22  
3              
4 2     2   878 use POSIX ();
  2         4  
  2         31  
5 2     2   1014 use Sys::Hostname ();
  2         2902  
  2         75  
6 2     2   1401 use Text::vFile::asData;
  2         12458  
  2         14  
7              
8             our $VERSION = '1.00';
9              
10             sub register {
11 2     2 1 120 my ($self, $app, $config) = @_;
12              
13 2   50     18 $config->{handler} //= 'ical';
14              
15 2   50     13 $self->{properties} = $config->{properties} || {};
16 2   50     14 $self->{properties}{calscale} ||= 'GREGORIAN';
17 2   50     10 $self->{properties}{method} ||= 'PUBLISH';
18 2   33     13 $self->{properties}{prodid} ||= sprintf '-//%s//NONSGML %s//EN', Sys::Hostname::hostname, $app->moniker;
19 2   50     140 $self->{properties}{version} ||= '2.0';
20 2   50     9 $self->{properties}{x_wr_caldesc} ||= '';
21 2   33     13 $self->{properties}{x_wr_calname} ||= $app->moniker;
22 2   33     166 $self->{properties}{x_wr_timezone} ||= POSIX::strftime('%Z', localtime);
23              
24 2   33     26 $self->{vfile} ||= Text::vFile::asData->new;
25              
26 2     1   35 $app->helper('reply.ical' => sub { $self->_reply_ical(@_) });
  1         13740  
27 2         1650 $app->types->type(ical => 'text/calendar');
28              
29 2 50       209 if ($config->{handler}) {
30             $app->renderer->add_handler(
31             $config->{handler},
32             sub {
33 1     1   39272 my ($renderer, $c, $output, $options) = @_;
34 1 50       6 return undef unless my $ical = $c->stash('ical');
35 1         21 $$output = join '', map {"$_\n"} $self->_render_ical($c, $ical);
  22         534  
36 1         8 return 1;
37             }
38 2         8 );
39             }
40             }
41              
42             sub _event_to_properties {
43 2     2   7 my ($event, $defaults) = @_;
44 2         3 my $properties = {};
45              
46 2         33 for my $k (keys %$event) {
47 14   50     33 my $v = $event->{$k} //= '';
48 14         29 my $p = _vkey($k);
49 14 100       47 if (UNIVERSAL::isa($v, 'Mojo::Date')) {
50 6         17 $v = $v->to_datetime;
51 6         115 $v =~ s![:-]!!g; # 1994-11-06T08:49:37Z => 19941106T084937Z
52             }
53 14         45 $properties->{$p} = [{value => $v}];
54             }
55              
56 2   50     17 $properties->{DTSTAMP} ||= [{value => $defaults->{now}}];
57 2   50     11 $properties->{SEQUENCE} ||= [{value => 0}];
58 2   50     15 $properties->{STATUS} ||= [{value => 'CONFIRMED'}];
59 2   50     32 $properties->{TRANSP} ||= [{value => 'OPAQUE'}];
60 2   50     13 $properties->{UID} ||= [{value => sprintf '%s@%s', _md5($event), $defaults->{hostname}}];
61 2         11 $properties;
62             }
63              
64             sub _render_ical {
65 2     2   8 my ($self, $c, $data) = @_;
66 2 50       5 my %properties = %{$data->{properties} || {}};
  2         34  
67 2         7 my $ical = {};
68 2         5 my %defaults;
69              
70 2         25 $ical->{objects} = [];
71 2         7 $ical->{properties} = {};
72 2         12 $ical->{type} = 'VCALENDAR';
73              
74 2   33     35 $properties{calscale} ||= $self->{properties}{calscale};
75 2   33     14 $properties{method} ||= $self->{properties}{method};
76 2   33     51 $properties{prodid} ||= $self->{properties}{prodid};
77 2   33     15 $properties{version} ||= $self->{properties}{version};
78 2   33     12 $properties{x_wr_caldesc} ||= $self->{properties}{x_wr_caldesc};
79 2   33     21 $properties{x_wr_calname} ||= $self->{properties}{x_wr_calname};
80 2   33     12 $properties{x_wr_timezone} ||= $self->{properties}{x_wr_timezone};
81              
82 2         8 for my $k (keys %properties) {
83 14         28 my $p = _vkey($k);
84 14         50 $ical->{properties}{$p} = [{value => $properties{$k}}];
85             }
86              
87 2         11 $defaults{hostname} = Sys::Hostname::hostname;
88 2         20 $defaults{now} = Mojo::Date->new->to_datetime;
89 2         166 $defaults{now} =~ s![:-]!!g; # 1994-11-06T08:49:37Z => 19941106T084937Z
90              
91 2 50       5 for my $event (@{$data->{events} || []}) {
  2         12  
92 2         4 push @{$ical->{objects}}, {properties => _event_to_properties($event, \%defaults), type => 'VEVENT'};
  2         10  
93             }
94              
95 2         14 return $self->{vfile}->generate_lines($ical);
96             }
97              
98             sub _reply_ical {
99 1     1   3 my ($self, $c, $data) = @_;
100 1         4 $c->res->headers->content_type('text/calendar');
101 1         39 $c->render(text => join '', map {"$_\n"} $self->_render_ical($c, $data));
  22         308  
102             }
103              
104             sub _md5 {
105 2     2   4 my $data = $_[0];
106 2         13 Mojo::Util::md5_sum(join ':', map {"$_=$data->{$_}"} grep { $_ ne 'dtstamp' } sort keys %$data);
  14         137  
  14         25  
107             }
108              
109             sub _vkey {
110 28 50   28   65 return $_[0] if $_[0] =~ /^[A-Z]/;
111 28         54 local $_ = uc $_[0];
112 28         51 s!_!-!g;
113 28         51 $_;
114             }
115              
116             1;
117              
118             =encoding utf8
119              
120             =head1 NAME
121              
122             Mojolicious::Plugin::Ical - Generate .ical documents
123              
124             =head1 VERSION
125              
126             0.05
127              
128             =head1 SYNOPSIS
129              
130             =head2 Application
131              
132             use Mojolicious::Lite;
133             plugin ical => {
134             properties => {
135             calscale => "GREGORIAN" # default GREGORIAN
136             method => "REQUEST", # default PUBLISH
137             prodid => "-//ABC Corporation//NONSGML My Product//EN",
138             version => "1.0", # default to 2.0
139             x_wr_caldesc => "Some description",
140             x_wr_calname => "My calender",
141             x_wr_timezone => "EDT", # default to timezone for localhost
142             }
143             };
144              
145             get '/calendar' => sub {
146             my $c = shift;
147             $c->reply->ical({
148             events => [
149             {
150             created => $date,
151             description => $str, # http://www.kanzaki.com/docs/ical/description.html
152             dtend => $date,
153             dtstamp => $date, # UTC time format, defaults to "now"
154             dtstart => $date,
155             last_modified => $date, # defaults to "now"
156             location => $str, # http://www.kanzaki.com/docs/ical/location.html
157             sequence => $int, # default 0
158             status => $str, # default CONFIRMED
159             summary => $str, # http://www.kanzaki.com/docs/ical/summary.html
160             transp => $str, # default OPAQUE
161             uid => $str, # default to md5 of the values @hostname
162             },
163             ...
164             ],
165             });
166             };
167              
168             # or using respond_to()
169             get '/events' => sub {
170             my $c = shift;
171             my $ical = { events => [...] };
172             $c->respond_to(
173             ical => {handler => 'ical', ical => $ical},
174             json => {json => $ical}
175             );
176             };
177              
178             =head1 DESCRIPTION
179              
180             L is a L plugin for generating
181             L documents.
182              
183             This plugin will...
184              
185             =over 4
186              
187             =item *
188              
189             Add the helper L.
190              
191             =item *
192              
193             Add ".ical" type to L.
194              
195             =item *
196              
197             Add a handler "ical" to L.
198              
199             =back
200              
201             =head1 HELPERS
202              
203             =head2 reply.ical
204              
205             $c = $c->reply->ical({ events => [...], properties => {...} });
206              
207             Will render a iCal document with the Content-Type "text/calender".
208              
209             C is an array ref of calendar events.
210             C will override the defaults given to L.
211              
212             See L for more details.
213              
214             =head1 METHODS
215              
216             =head2 register
217              
218             plugin ical => {properties => {...}};
219              
220             Register L helper.
221              
222             =head1 COPYRIGHT AND LICENSE
223              
224             Copyright (C) 2014, Jan Henning Thorsen
225              
226             This program is free software, you can redistribute it and/or modify it under
227             the terms of the Artistic License version 2.0.
228              
229             =head1 AUTHOR
230              
231             Jan Henning Thorsen - C
232              
233             =cut