File Coverage

blib/lib/Catalyst/View/Vega.pm
Criterion Covered Total %
statement 59 60 98.3
branch 8 10 80.0
condition 2 3 66.6
subroutine 17 17 100.0
pod 4 6 66.6
total 90 96 93.7


line stmt bran cond sub pod time code
1             package Catalyst::View::Vega;
2 1     1   11654 use strict;
  1         1  
  1         25  
3 1     1   4 use warnings;
  1         1  
  1         17  
4 1     1   3 use utf8;
  1         1  
  1         4  
5 1     1   31 use 5.008_005;
  1         2  
6             our $VERSION = '0.02';
7              
8 1     1   463 use Moose;
  1         297150  
  1         5  
9 1     1   5047 use Types::Standard qw< :types >;
  1         44990  
  1         8  
10 1     1   2954 use JSON::MaybeXS;
  1         3920  
  1         46  
11 1     1   684 use Path::Tiny;
  1         7102  
  1         47  
12 1     1   5 use List::Util qw< first >;
  1         1  
  1         51  
13 1     1   11 use namespace::autoclean;
  1         1  
  1         7  
14              
15             =encoding utf-8
16              
17             =head1 NAME
18              
19             Catalyst::View::Vega - A Catalyst view for pre-processing Vega specs
20              
21             =head1 SYNOPSIS
22              
23             # In YourApplication.pm
24             #
25             YourApplication->inject_component( 'View::Vega' => { from_component => 'Catalyst::View::Vega' } );
26             YourApplication->config(
27             'View::Vega' => {
28             path => YourApplication->path_to("root/vega")->stringify,
29             }
30             );
31              
32             # In a controller action
33             #
34             my $vega = $c->view('Vega');
35             $vega->specfile('patient-chart.json');
36             $vega->bind_data({
37             "patient" => [{
38             id => $patient->id,
39             name => $patient->name,
40             }],
41             "medications" => [ $meds->all ],
42             "samples" => [ $samples->all ],
43             });
44             $c->detach($vega);
45              
46             =head1 DESCRIPTION
47              
48             This class lets you bind data to the datasets declared in a
49             L<Vega|https://vega.github.io/vega/> spec and output the spec with the bound
50             data inlined. This is useful for inlining data dynamically without using a
51             templating library. Inlining data reduces request overhead and creates
52             standalone Vega specs which can be rendered as easily offline as they are
53             online.
54              
55             A new instance of this view is created for each request, so it is safe to set
56             attributes and use the view's API in multiple controllers or actions. Each new
57             view instance is based on the application's global instance of the view so that
58             initial attribute values are from your application config.
59              
60             =cut
61              
62             extends 'Catalyst::View';
63             with 'Catalyst::Component::InstancePerContext';
64             with 'MooseX::Clone';
65              
66             # Create a new instance of this view per request so object attributes are
67             # per-request. This lets controllers and actions access the same instance by
68             # calling $c->view("Vega").
69             sub build_per_context_instance {
70 4     4 0 191230 my ($self, $c, @args) = @_;
71 4         19 return $self->clone(@args);
72             }
73              
74             =head1 ATTRIBUTES
75              
76             =head2 json
77              
78             Read-only. Object with C<encode> and C<decode> methods for reading and writing
79             JSON. Defaults to:
80              
81             JSON::MaybeXS->new->utf8->convert_blessed->canonical->pretty
82              
83             You can either set this at application start time via L<Catalyst/config>:
84              
85             YourApplication->config(
86             'View::Vega' => {
87             json => ...
88             }
89             );
90              
91             or pass it in during the request-specific object construction:
92              
93             my $vega = $c->view("Vega", json => ...);
94              
95             =head2 path
96              
97             Read-only. Filesystem path under which L</specfile>s are located. Usually set
98             by your application's config file or via L<Catalyst/config>, e.g.:
99              
100             YourApplication->config(
101             'View::Vega' => {
102             path => YourApplication->path_to("root/vega")->stringify,
103             }
104             );
105              
106             =head2 specfile
107              
108             Read-write. A file relative to L</path> which contains the Vega spec to
109             process. Usually set in your controller's actions.
110              
111             =cut
112              
113             has json => (
114             is => 'ro',
115             isa => HasMethods['encode', 'decode'],
116             default => sub { JSON->new->utf8->convert_blessed->canonical->pretty },
117             );
118              
119             has path => (
120             is => 'ro',
121             isa => Str,
122             required => 1,
123             );
124              
125             has specfile => (
126             is => 'rw',
127             isa => Str,
128             );
129              
130             has _data => (
131             is => 'rw',
132             isa => HashRef,
133             default => sub { +{} },
134             );
135              
136             =head1 METHODS
137              
138             =head2 bind_data
139              
140             Takes a hashref or list of key-value pairs and merges them into the view
141             object's dataset bindings.
142              
143             Keys should be dataset names which match those in the Vega L</specfile>. Any
144             existing binding in this view for a given dataset name is overwritten.
145              
146             Values may be either references or strings. References are serialized and
147             inlined as the C<values> dataset property. Strings are serialized as the
148             C<url> property, which allows you to dynamically reference external datasets.
149             See L<Vega's documentation on dataset properties|https://github.com/vega/vega/wiki/Data#data-properties>
150             for more details on the properties themselves.
151              
152             Note that Vega expects the C<values> property to be an array, although this
153             view does not enforce that. Make sure your references are arrayrefs or objects
154             that serialize to an arrayref.
155              
156             Returns nothing.
157              
158             =head2 unbind_data
159              
160             Takes a dataset name as the sole argument and deletes any data bound in the
161             view object for that dataset. Returns the now unbound data, if any.
162              
163             =cut
164              
165             sub bind_data {
166 5     5 1 2170 my $self = shift;
167 5         119 my $data = $self->_data;
168 5 50       13 if (@_) {
169 5 100 66     28 if (@_ == 1 and ref($_[0]) eq 'HASH') {
    50          
170 1         2 $self->_data({ %$data, %{ $_[0] } });
  1         27  
171             }
172             elsif (@_ % 2 == 0) {
173 4         94 $self->_data({ %$data, @_ });
174             }
175             else {
176 0         0 die "View::Vega->data() takes a hashref or list of key-value pairs ",
177             "but an odd number of arguments were passed";
178             }
179             }
180 5         12 return;
181             }
182              
183             sub unbind_data {
184 1     1 1 506 my $self = shift;
185 1         1 my $name = shift;
186 1         23 return delete $self->_data->{$name};
187             }
188              
189             =head2 process_spec
190              
191             Returns the Vega specification as a Perl data structure, with bound data
192             inlined into the spec.
193              
194             =cut
195              
196             sub process_spec {
197 4     4 1 7 my $self = shift;
198 4         14 my $spec = $self->read_specfile;
199              
200             # Inject data bindings into the Vega spec either as URLs or inline values
201 4         690 for my $name (keys %{ $self->_data }) {
  4         117  
202 5 100   8   17 my $dataset = first { $_->{name} eq $name } @{ $spec->{data} }
  8         28  
  5         15  
203             or die "View::Vega cannot find a dataset named «$name» in the spec";
204 4         93 my $value = $self->_data->{$name};
205 4 100       12 $dataset->{ ref($value) ? 'values' : 'url' } = $value;
206             }
207              
208 3         112 return $spec;
209             }
210              
211             =head2 process
212              
213             Sets up up a JSON response using the results of L</process_spec>. You should
214             usually call this implicitly via L<Catalyst/detach> using the idiom:
215              
216             my $vega = $c->view("Vega");
217             ...
218             $c->detach($vega);
219              
220             This is the most "viewish" part of this class.
221              
222             =cut
223              
224             sub process {
225 4     4 1 2897 my ($self, $c) = @_;
226 4         67 my $res = $c->response;
227 4         30 $res->content_type('application/json; charset="UTF-8"');
228 4         850 $res->body( $self->json->encode( $self->process_spec ) );
229             }
230              
231             sub read_specfile {
232 4     4 0 5 my ($self, $file) = @_;
233 4         93 my $spec = path($self->path, $self->specfile);
234 4         207 return $self->json->decode( $spec->slurp_raw );
235             }
236              
237             1;
238             __END__
239              
240             =head1 AUTHOR
241              
242             Thomas Sibley E<lt>trsibley@uw.eduE<gt>
243              
244             =head1 THANKS
245              
246             Thanks to Evan Silberman E<lt>silby@uw.eduE<gt> for suggesting dynamic inlining
247             of datasets.
248              
249             =head1 COPYRIGHT
250              
251             Copyright 2016- by the University of Washington
252              
253             =head1 LICENSE
254              
255             This library is free software; you can redistribute it and/or modify
256             it under the same terms as Perl itself.
257              
258             =head1 SEE ALSO
259              
260             =over
261              
262             =item L<Vega data specs|https://github.com/vega/vega/wiki/Data>
263              
264             =item L<Vega documentation|https://github.com/vega/vega/wiki/Documentation>
265              
266             =item L<Vega|https://vega.github.io/vega/>
267              
268             =back
269              
270             =cut