File Coverage

blib/lib/Trickster.pm
Criterion Covered Total %
statement 80 112 71.4
branch 13 26 50.0
condition 9 17 52.9
subroutine 21 26 80.7
pod 9 13 69.2
total 132 194 68.0


line stmt bran cond sub pod time code
1             package Trickster;
2              
3 5     5   1568298 use strict;
  5         9  
  5         174  
4 5     5   22 use warnings;
  5         6  
  5         261  
5 5     5   58 use v5.14;
  5         20  
6              
7             our $VERSION = '0.01';
8              
9 5     5   2600 use Plack::Request;
  5         354117  
  5         213  
10 5     5   2657 use Plack::Response;
  5         11091  
  5         192  
11 5     5   30 use Scalar::Util qw(blessed);
  5         7  
  5         273  
12 5     5   2814 use Trickster::Request;
  5         15  
  5         224  
13 5     5   2559 use Trickster::Response;
  5         23  
  5         190  
14 5     5   2645 use Trickster::Router;
  5         18  
  5         189  
15 5     5   2582 use Trickster::Exception;
  5         19  
  5         164  
16 5     5   2798 use Trickster::Logger;
  5         20  
  5         6594  
17              
18             sub new {
19 7     7 1 4988 my ($class, %opts) = @_;
20            
21             my $self = bless {
22             router => Trickster::Router->new,
23             middleware => [],
24             error_handler => undef,
25             logger => $opts{logger} || Trickster::Logger->new(level => 'info'),
26 7   66     46 debug => $opts{debug} || 0,
      100        
27             %opts,
28             }, $class;
29            
30 7         25 return $self;
31             }
32              
33 13     13 1 141 sub get { shift->_add_route('GET', @_) }
34 3     3 1 26 sub post { shift->_add_route('POST', @_) }
35 1     1 1 10 sub put { shift->_add_route('PUT', @_) }
36 1     1 1 9 sub patch { shift->_add_route('PATCH', @_) }
37 1     1 1 7 sub delete { shift->_add_route('DELETE', @_) }
38             sub any {
39 0     0 0 0 my ($self, $methods, $path, $handler, %opts) = @_;
40 0         0 for my $method (@$methods) {
41 0         0 $self->_add_route($method, $path, $handler, %opts);
42             }
43 0         0 return $self;
44             }
45              
46             sub _add_route {
47 19     19   52 my ($self, $method, $path, $handler, %opts) = @_;
48            
49 19         78 $self->{router}->add_route($method, $path, $handler, %opts);
50            
51 19         49 return $self;
52             }
53              
54             sub middleware {
55 0     0 1 0 my ($self, $mw) = @_;
56 0         0 push @{$self->{middleware}}, $mw;
  0         0  
57 0         0 return $self;
58             }
59              
60             sub error_handler {
61 0     0 1 0 my ($self, $handler) = @_;
62 0         0 $self->{error_handler} = $handler;
63 0         0 return $self;
64             }
65              
66             sub logger {
67 0     0 0 0 my ($self, $logger) = @_;
68 0 0       0 $self->{logger} = $logger if $logger;
69 0         0 return $self->{logger};
70             }
71              
72             sub url_for {
73 1     1 0 2643 my ($self, $name, %params) = @_;
74 1         8 return $self->{router}->url_for($name, %params);
75             }
76              
77             sub routes {
78 0     0 0 0 my ($self, $method) = @_;
79 0         0 return $self->{router}->routes($method);
80             }
81              
82             sub to_app {
83 7     7 1 33 my ($self) = @_;
84            
85             my $app = sub {
86 22     22   203661 my ($env) = @_;
87            
88 22         213 my $req = Trickster::Request->new($env);
89 22         341 my $res = Trickster::Response->new(404);
90 22         455 my $psgi_response;
91            
92 22         44 eval {
93 22         80 my $method = $req->method;
94 22         278 my $path = $req->path_info;
95            
96 22         193 my $match = $self->{router}->match($method, $path);
97            
98 22 100       58 if ($match) {
99 21         96 $req->env->{'trickster.params'} = $match->{params};
100 21         125 $req->env->{'trickster.route'} = $match->{route};
101            
102 21         135 my $result = $match->{route}{handler}->($req, $res);
103            
104             # Handle different return types
105 20 100 33     238 if (blessed($result) && ($result->isa('Plack::Response') || $result->isa('Trickster::Response'))) {
    50 66        
    50          
106 13         49 $res = $result;
107             } elsif (ref($result) eq 'ARRAY') {
108 0         0 $psgi_response = $result;
109             } elsif (defined $result) {
110 7         73 require Encode;
111 7         64 $res->body(Encode::encode_utf8($result));
112 7 50       57 $res->status(200) if $res->status == 404;
113             }
114             } else {
115             # No route matched
116 1         5 $res->status(404);
117 1         13 $res->content_type('text/plain');
118 1         33 $res->body('Not Found');
119             }
120             };
121            
122             # Return early if we have a PSGI response
123 22 50       136 return $psgi_response if $psgi_response;
124            
125 22 100       99 if ($@) {
126 1         3 my $error = $@;
127            
128 1         6 $self->{logger}->error("Request error: $error");
129            
130             # Handle Trickster exceptions
131 1 50 33     16 if (blessed($error) && $error->isa('Trickster::Exception')) {
132 1 50       4 if ($self->{error_handler}) {
133 0         0 return $self->{error_handler}->($error, $req, $res);
134             }
135            
136 1         5 $res = Trickster::Response->new;
137            
138             # Return JSON for API requests
139 1 50 33     28 if ($req->header('Accept') && $req->header('Accept') =~ /application\/json/) {
140 1         343 return $res->json($error->as_hash, $error->status)->finalize;
141             }
142            
143 0         0 require Encode;
144 0         0 $res->status($error->status);
145 0         0 $res->content_type('text/plain');
146 0         0 $res->body(Encode::encode_utf8($error->message));
147            
148 0         0 return $res->finalize;
149             }
150            
151             # Handle other errors
152 0 0       0 if ($self->{error_handler}) {
153 0         0 return $self->{error_handler}->($error, $req, $res);
154             }
155            
156 0         0 require Encode;
157 0         0 $res = Trickster::Response->new(500);
158 0         0 $res->content_type('text/plain');
159            
160 0 0       0 if ($self->{debug}) {
161 0         0 $res->body(Encode::encode_utf8("Internal Server Error: $error"));
162             } else {
163 0         0 $res->body("Internal Server Error");
164             }
165             }
166            
167 21         72 return $res->finalize;
168 7         70 };
169            
170             # Apply middleware in reverse order
171 7         15 for my $mw (reverse @{$self->{middleware}}) {
  7         19  
172 0         0 $app = $mw->($app);
173             }
174            
175 7         53 return $app;
176             }
177              
178             1;
179              
180             __END__