File Coverage

blib/lib/PAGI/Stash.pm
Criterion Covered Total %
statement 53 53 100.0
branch 23 24 95.8
condition 9 10 90.0
subroutine 12 12 100.0
pod 9 9 100.0
total 106 108 98.1


line stmt bran cond sub pod time code
1             package PAGI::Stash;
2              
3 17     17   757567 use strict;
  17         22  
  17         545  
4 17     17   71 use warnings;
  17         19  
  17         698  
5 17     17   59 use Scalar::Util 'blessed';
  17         19  
  17         13765  
6              
7             =head1 NAME
8              
9             PAGI::Stash - Standalone helper for per-request shared state
10              
11             =head1 SYNOPSIS
12              
13             use PAGI::Stash;
14              
15             # Middleware sets shared state for downstream handlers
16             my $auth_middleware = sub ($app) {
17             async sub ($scope, $receive, $send) {
18             my $stash = PAGI::Stash->new($scope);
19             $stash->set(user => authenticate($scope));
20             await $app->($scope, $receive, $send);
21             };
22             };
23              
24             # Handler reads what middleware stored
25             async sub ($scope, $receive, $send) {
26             my $stash = PAGI::Stash->new($scope);
27             my $user = $stash->get('user'); # dies if missing
28             my $theme = $stash->get('theme', 'dark'); # default if missing
29             ...
30             };
31              
32             =head1 DESCRIPTION
33              
34             PAGI::Stash wraps C<< $scope->{'pagi.stash'} >> and provides a clean
35             accessor interface with strict key checking. It is a standalone helper
36             not attached to any protocol object.
37              
38             The strict C method dies when a key does not exist, catching
39             typos at runtime. Use the two-argument form C
40             for keys that may or may not be present.
41              
42             The stash lives in the PAGI scope hashref and is shared across all
43             middleware, handlers, and protocol objects processing the same request.
44              
45             =head1 CONSTRUCTOR
46              
47             =head2 new
48              
49             my $stash = PAGI::Stash->new($scope);
50             my $stash = PAGI::Stash->new($request); # any object with ->scope
51             my $stash = PAGI::Stash->new(@_); # extra args ignored
52              
53             Scope-based constructor. Resolves to the C<< $scope->{'pagi.stash'} >>
54             hashref, creating it lazily if it does not exist. Accepts a scope hashref
55             directly, or any blessed object with a C method. Extra positional
56             arguments are silently ignored.
57              
58             =head2 from_data
59              
60             my $stash = PAGI::Stash->from_data({ user => 'alice' });
61              
62             Test convenience constructor. Wraps a raw hashref directly as the
63             backing data, bypassing scope resolution.
64              
65             =cut
66              
67             sub new {
68 54     54 1 17174 my ($class, @args) = @_;
69              
70 54         84 my $arg = $args[0];
71              
72             # Object with ->scope method (e.g., PAGI::Request, PAGI::SSE)
73 54 100 66     269 if (blessed($arg) && $arg->can('scope')) {
74 21         64 my $scope = $arg->scope;
75 21 50       71 die "PAGI::Stash requires scope hashref from ->scope method\n"
76             unless ref $scope eq 'HASH';
77 21   100     96 $scope->{'pagi.stash'} //= {};
78 21         107 return bless { _data => $scope->{'pagi.stash'} }, $class;
79             }
80              
81             # Unblessed hashref — treat as scope
82 33 100       80 if (ref $arg eq 'HASH') {
83 30   100     120 $arg->{'pagi.stash'} //= {};
84 30         182 return bless { _data => $arg->{'pagi.stash'} }, $class;
85             }
86              
87 3         17 die "PAGI::Stash requires a scope hashref or object with ->scope method\n";
88             }
89              
90             sub from_data {
91 27     27 1 195166 my ($class, $data) = @_;
92 27 100       104 die "from_data() requires a hashref\n" unless ref $data eq 'HASH';
93 25         100 return bless { _data => $data }, $class;
94             }
95              
96             =head1 METHODS
97              
98             =head2 get
99              
100             my $val = $stash->get('user'); # strict: dies if missing
101             my $val = $stash->get('theme', 'dark'); # permissive: returns default
102              
103             With one argument, dies if the key does not exist. The error message
104             lists available keys (10 or fewer) or reports the count.
105              
106             With two arguments, returns the default if the key is missing.
107              
108             =cut
109              
110             sub get {
111 57     57 1 322 my ($self, @args) = @_;
112 57 100 100     277 die "get() requires 1 or 2 arguments\n" if @args == 0 || @args > 2;
113 55         192 my ($key, @rest) = @args;
114 55 100       130 if (!exists $self->{_data}{$key}) {
115 7 100       31 return $rest[0] if @rest;
116 3         7 my @all_keys = sort keys %{$self->{_data}};
  3         24  
117 3 100       11 if (@all_keys <= 10) {
118 2         23 die "Stash key '$key' does not exist. Available keys: "
119             . join(', ', @all_keys) . "\n";
120             }
121             else {
122 1         11 die "Stash key '$key' does not exist (stash has "
123             . scalar(@all_keys) . " keys)\n";
124             }
125             }
126 48         242 return $self->{_data}{$key};
127             }
128              
129             =head2 set
130              
131             $stash->set(user => $u);
132             $stash->set(user => $u, role => 'admin');
133             $stash->set(user => $u)->set(role => 'admin');
134              
135             Sets key-value pairs. Returns C<$self> for chaining. No-ops on zero
136             args. Dies on odd number of args.
137              
138             =cut
139              
140             sub set {
141 46     46 1 1149 my ($self, @args) = @_;
142 46 100       110 return $self unless @args;
143 45 100       129 die "set() requires key => value pairs\n" if @args % 2;
144 43         117 my %pairs = @args;
145 43         179 $self->{_data}{$_} = $pairs{$_} for CORE::keys %pairs;
146 43         117 return $self;
147             }
148              
149             =head2 exists
150              
151             if ($stash->exists('user')) { ... }
152              
153             Returns true (1) if the key exists, false (0) otherwise.
154              
155             =cut
156              
157             sub exists {
158 10     10 1 33 my ($self, $key) = @_;
159 10 100       55 return exists $self->{_data}{$key} ? 1 : 0;
160             }
161              
162             =head2 delete
163              
164             $stash->delete('user');
165             $stash->delete('user', 'role', 'debug');
166              
167             Removes one or more keys. Returns C<$self> for chaining.
168              
169             =cut
170              
171             sub delete {
172 4     4 1 338 my ($self, @keys) = @_;
173 4         14 delete $self->{_data}{$_} for @keys;
174 4         13 return $self;
175             }
176              
177             =head2 keys
178              
179             my @keys = $stash->keys;
180              
181             Returns all keys in the stash.
182              
183             =cut
184              
185             sub keys {
186 2     2 1 10 my ($self) = @_;
187 2         4 return keys %{$self->{_data}};
  2         12  
188             }
189              
190             =head2 slice
191              
192             my %subset = $stash->slice('user', 'role', 'theme');
193              
194             Returns a hash of key-value pairs for the requested keys. Missing keys
195             are silently skipped.
196              
197             =cut
198              
199             sub slice {
200 3     3 1 21 my ($self, @keys) = @_;
201 3 100       7 return map { CORE::exists($self->{_data}{$_}) ? ($_ => $self->{_data}{$_}) : () } @keys;
  7         31  
202             }
203              
204             =head2 data
205              
206             my $href = $stash->data;
207             $href->{user} = $val;
208              
209             Returns the raw backing hashref. Mutations are visible through
210             C/C since they operate on the same reference.
211              
212             =cut
213              
214             sub data {
215 15     15 1 63 my ($self) = @_;
216 15         79 return $self->{_data};
217             }
218              
219             1;
220              
221             __END__