File Coverage

blib/lib/Mojolicious/Plugin/Inertia.pm
Criterion Covered Total %
statement 48 48 100.0
branch 22 22 100.0
condition 13 17 76.4
subroutine 6 6 100.0
pod 1 1 100.0
total 90 94 95.7


line stmt bran cond sub pod time code
1             package Mojolicious::Plugin::Inertia;
2 8     8   8116072 use Mojo::Base 'Mojolicious::Plugin';
  8         14541  
  8         67  
3 8     8   4363 use Mojo::JSON qw(encode_json);
  8         234334  
  8         596  
4 8     8   54 use Scalar::Util qw(reftype);
  8         15  
  8         394  
5 8     8   37 use Carp qw(croak);
  8         30  
  8         6725  
6              
7             our $VERSION = "0.01";
8              
9             sub register {
10 10     10 1 3134 my ($self, $app, $conf) = @_;
11 10   50     32 $conf ||= {};
12              
13 10 100       230 croak "Inertia plugin requires a 'version' configuration option" unless $conf->{version};
14 9 100       149 croak "Inertia plugin requires a 'layout' configuration option" unless $conf->{layout};
15              
16             # Asset versioning. e.g. md5sum of your assets manifest file.
17 8         15 my $version = $conf->{version};
18              
19             # Layout template for non-Inertia requests.
20             # It must contain a <%= data_page %> placeholder at the application root.
21 8 100       28 my $layout = ref $conf->{layout} ? $conf->{layout}->slurp : $conf->{layout};
22              
23             # History encryption settings (optional).
24             # Ref: https://inertiajs.com/history-encryption
25 8 100       119 my $default_encrypt_history = defined $conf->{encrypt_history} ? $conf->{encrypt_history} : 0;
26 8 100       22 my $default_clear_history = defined $conf->{clear_history} ? $conf->{clear_history} : 0;
27              
28             $app->helper(inertia => sub {
29 15     15   279804 my ($c, $component, $props, $options) = @_;
30 15   50     74 $props ||= {};
31 15   100     93 $options ||= {};
32              
33             # Options
34 15 100       145 my $encrypt_history = defined $options->{encrypt_history} ? $options->{encrypt_history} : $default_encrypt_history;
35 15 100       61 my $clear_history = defined $options->{clear_history} ? $options->{clear_history} : $default_clear_history;
36              
37             # If the client's asset version does not match the server's version,
38             # then the client must do a full page reload.
39             # So, we respond with a 409 status and an X-Inertia-Location header
40             # Ref: https://inertiajs.com/the-protocol#asset-versioning
41 15         70 my $inertia_version = $c->req->headers->header('X-Inertia-Version');
42 15 100 66     513 if ($c->req->method eq 'GET' && $inertia_version && $inertia_version ne $version) {
      100        
43 1         37 $c->res->headers->header('X-Inertia-Location' => $c->req->url->to_string);
44 1         236 return $c->rendered(409);
45             }
46              
47             # Partial reloads allows you to request a subset of the props (data) from the server on subsequent visits to the same page component.
48             # Ref: https://inertiajs.com/the-protocol#partial-reloads
49 14         242 my $partial_data = $c->req->headers->header('X-Inertia-Partial-Data');
50 14         317 my $partial_component = $c->req->headers->header('X-Inertia-Partial-Component');
51 14 100 66     275 if ($partial_data && $partial_component) {
52 4         19 my @only_keys = split /,/, $partial_data;
53 4         10 $props = { map { $_ => $props->{$_} } @only_keys };
  5         35  
54             }
55              
56             # Resolve props that are coderefs by calling them with the current controller context.
57             # Code refs are useful for lazy loading data only when needed.
58 14         31 my $resolved_props = {};
59 14         46 for my $key (keys %$props) {
60 21         43 my $prop = $props->{$key};
61 21 100 100     134 $resolved_props->{$key} = (reftype($prop) || '') eq 'CODE' ? $prop->($c) : $prop;
62             }
63              
64             # Construct the page object.
65             # Ref: https://inertiajs.com/the-protocol#the-page-object
66 14         79 my $page_object = {
67             component => $component,
68             props => $resolved_props,
69             url => $c->req->url->to_string,
70             version => $version,
71             encryptHistory => $encrypt_history,
72             clearHistory => $clear_history,
73             };
74              
75             # Check if the request is an Inertia request.
76             # If so, return a JSON response.
77             # Else, return an HTML response with embedded page object.
78             # Ref: https://inertiajs.com/the-protocol#inertia-responses
79 14         2525 my $is_inertia = $c->req->headers->header('X-Inertia');
80              
81 14 100       413 if ($is_inertia) {
82 11         98 $c->res->headers->header('X-Inertia' => 'true');
83 11         497 $c->res->headers->header('Vary' => 'X-Inertia');
84 11         402 return $c->render(json => $page_object);
85             }
86             else {
87 3         19 $c->res->headers->header('Vary' => 'X-Inertia');
88 3         131 return $c->render(
89             inline => $layout,
90             format => 'html',
91             data_page => encode_json($page_object)
92             );
93             }
94 8         94 });
95             }
96              
97             1;
98             __END__