File Coverage

blib/lib/Concierge/Users/YAML.pm
Criterion Covered Total %
statement 101 129 78.2
branch 32 68 47.0
condition 7 13 53.8
subroutine 15 17 88.2
pod 7 9 77.7
total 162 236 68.6


line stmt bran cond sub pod time code
1             package Concierge::Users::YAML v0.8.0;
2 6     6   910 use v5.36;
  6         50  
3 6     6   31 use Carp qw/ croak /;
  6         8  
  6         487  
4 6     6   32 use YAML::Tiny;
  6         9  
  6         300  
5 6     6   32 use File::Path qw/ make_path /;
  6         44  
  6         271  
6 6     6   44 use File::Spec;
  6         9  
  6         186  
7 6     6   25 use parent qw/ Concierge::Users::Meta /;
  6         17  
  6         56  
8              
9             # ABSTRACT: YAML file backend for Concierge::Users
10              
11             # ==============================================================================
12             # Configure Class Method - One-time setup (called by Users->setup)
13             # ==============================================================================
14              
15             sub configure {
16 16     16 1 53 my ($class, $setup_config) = @_;
17              
18             # Extract storage_dir
19 16         56 my $storage_dir = $setup_config->{storage_dir};
20              
21             # For YAML backend, storage is just the directory
22             # No additional setup needed beyond ensuring storage_dir exists
23             # (already done by Users->setup before calling configure)
24              
25             # Create temporary object for archiving
26             my $temp_backend = bless {
27             storage_dir => $storage_dir,
28             fields => $setup_config->{fields} || [],
29             field_definitions => $setup_config->{field_definitions},
30 16   50     106 }, $class;
31              
32             # Check for existing YAML files and archive if present
33 16 50       995 if (opendir(my $dh, $storage_dir)) {
34 16 50       611 my @yaml_files = grep { /\.yaml$/ && -f "$storage_dir/$_" } readdir $dh;
  32         149  
35 16         221 closedir $dh;
36              
37             # Archive if YAML files exist
38 16 50       62 if (@yaml_files) {
39 0         0 my $archive_result = $temp_backend->_archive_user_data();
40 0 0       0 unless ($archive_result->{success}) {
41             return {
42             success => 0,
43             message => $archive_result->{message},
44 0         0 };
45             }
46             }
47             }
48              
49             # Return success with config
50             return {
51             success => 1,
52             message => "YAML backend configured successfully",
53             config => {
54             storage_dir => $storage_dir,
55             fields => $setup_config->{fields} || [],
56             field_definitions => $setup_config->{field_definitions},
57             },
58 16   50     289 };
59             }
60              
61             # ==============================================================================
62             # Constructor - Runtime instantiation (called by Users->new)
63             # ==============================================================================
64              
65             sub new {
66 17     17 1 90 my ($class, $runtime_config) = @_;
67              
68             # Extract parameters from saved config (no validation needed)
69 17         64 my $storage_dir = $runtime_config->{storage_dir};
70              
71             return bless {
72             storage_dir => $storage_dir,
73             fields => $runtime_config->{fields} || [],
74             field_definitions => $runtime_config->{field_definitions} || {},
75 17   50     191 }, $class;
      50        
76             }
77              
78             # Report backend configuration (for debugging/info)
79             sub config {
80 0     0 0 0 my ($self) = @_;
81              
82             return {
83             storage_dir => $self->{storage_dir},
84             fields => $self->{fields},
85             field_definitions => $self->{field_definitions},
86 0         0 };
87             }
88              
89             # Get user file path
90             sub _get_user_file {
91 110     110   227 my ($self, $user_id) = @_;
92              
93 110         2074 return File::Spec->catfile($self->{storage_dir}, "$user_id.yaml");
94             }
95              
96             # Archive existing user data (internal method, called by configure)
97             sub _archive_user_data {
98 0     0   0 my ($self) = @_;
99              
100             # Generate timestamp for archive directory name
101 0         0 my $timestamp = $self->archive_timestamp();
102 0         0 my $archive_dir = "$self->{storage_dir}/users_$timestamp";
103              
104             # Create archive directory
105 0 0       0 unless (mkdir $archive_dir) {
106             return {
107 0         0 success => 0,
108             message => "Failed to create archive directory: $!"
109             };
110             }
111              
112             # Find and move all .yaml files
113 0         0 my $dh;
114 0 0       0 unless (opendir($dh, $self->{storage_dir})) {
115             return {
116 0         0 success => 0,
117             message => "Failed to open storage directory: $!"
118             };
119             }
120              
121 0 0       0 my @yaml_files = grep { /\.yaml$/ && -f "$self->{storage_dir}/$_" } readdir $dh;
  0         0  
122 0         0 closedir $dh;
123              
124 0         0 foreach my $file (@yaml_files) {
125 0         0 my $old_path = "$self->{storage_dir}/$file";
126 0         0 my $new_path = "$archive_dir/$file";
127              
128 0 0       0 unless (rename $old_path, $new_path) {
129             return {
130 0         0 success => 0,
131             message => "Failed to archive YAML file '$file': $!"
132             };
133             }
134             }
135              
136 0         0 return { success => 1 };
137             }
138              
139             # Add bare record with user_id, moniker, defaults, and null_values from Users.pm
140             sub add {
141 26     26 1 78 my ($self, $user_id, $initial_record) = @_;
142 26 50       78 return { success => 0, message => "Add Record failed: missing user_id" }
143             unless $user_id;
144 26 50       75 return { success => 0, message => "Add Record failed: missing initial record" }
145             unless $initial_record;
146              
147 26         262 my %record = $initial_record->%*;
148 26         209 $record{created_date} = $self->current_timestamp();
149             # Add last_mod_date timestamp
150 26         84 $record{last_mod_date} = $self->current_timestamp();
151              
152 26         90 my $user_file = $self->_get_user_file($user_id);
153              
154 26         59 eval {
155 26         123 YAML::Tiny::DumpFile($user_file, \%record);
156             };
157              
158 26 50       36027 if ($@) {
159 0         0 return { success => 0, message => "Failed to create initial user record: $@" };
160             }
161              
162 26         325 return { success => 1, message => "Initial record created for user '$user_id'" };
163             }
164              
165             # Fetch user by ID
166             sub fetch {
167 53     53 1 134 my ($self, $user_id) = @_;
168              
169 53         163 my $user_file = $self->_get_user_file($user_id);
170              
171             return {
172 53 100       2734 success => 0,
173             data => '',
174             message => "User '$user_id' not found"
175             } unless -f $user_file;
176              
177 22         61 my $user_data;
178 22         50 eval {
179 22         90 $user_data = YAML::Tiny::LoadFile($user_file);
180             };
181              
182 22 50       21447 if ($@) {
183             return {
184 0         0 success => 0,
185             data => '',
186             message => "Failed to load user data: $@"
187             };
188             }
189              
190             return {
191 22         258 success => 1,
192             data => $user_data,
193             message => ''
194             };
195             }
196              
197             # Update user
198             sub update {
199 29     29 1 90 my ($self, $user_id, $updates) = @_;
200              
201             # Remove readonly fields from updates
202 29         80 my %readonly = map { $_ => 1 } qw(user_id created_date last_mod_date);
  87         330  
203 29         190 delete $updates->{$_} for keys %readonly;
204              
205             # Add last_mod_date timestamp
206 29         114 $updates->{last_mod_date} = $self->current_timestamp();
207              
208 29         94 my $user_file = $self->_get_user_file($user_id);
209              
210 29 50       736 return { success => 0, message => "User '$user_id' not found" } unless -f $user_file;
211              
212             # Load existing data
213 29         73 my $user_data;
214 29         82 eval {
215 29         120 $user_data = YAML::Tiny::LoadFile($user_file);
216             };
217              
218 29 50       30256 return { success => 0, message => "Failed to load user data: $@" } if $@;
219              
220             # Apply updates
221 29         130 foreach my $field (keys %$updates) {
222 74         248 $user_data->{$field} = $updates->{$field};
223             }
224              
225             # Save back
226 29         68 eval {
227 29         147 YAML::Tiny::DumpFile($user_file, $user_data);
228             };
229              
230 29 50       29390 if ($@) {
231 0         0 return { success => 0, message => "Failed to update user file: $@" };
232             }
233              
234 29         434 return { success => 1, message => "User '$user_id' updated" };
235             }
236              
237             # List users with filters
238             sub list {
239 6     6 1 17 my ($self, $filters, $options) = @_;
240              
241             # Read all YAML files
242 6 50       326 opendir my $dh, $self->{storage_dir} or return { data => [], total_count => 0 };
243 6         215 my @files = grep { /\.yaml$/ } readdir $dh;
  38         146  
244 6         78 closedir $dh;
245              
246 6         12 my @users;
247 6         20 foreach my $file (@files) {
248 20         334 my $user_file = File::Spec->catfile($self->{storage_dir}, $file);
249 20         52 my $user_data;
250              
251 20         40 eval {
252 20         65 $user_data = YAML::Tiny::LoadFile($user_file);
253             };
254              
255 20 100       36289 next if $@;
256              
257             # Apply DSL filters
258 14         30 my $match = 1;
259              
260 14 100 66     103 if (ref $filters eq 'HASH' && exists $filters->{or_groups}) {
261 6         11 $match = 0; # Start with no match, need at least one OR group to match
262              
263 6         12 foreach my $and_group (@{$filters->{or_groups}}) {
  6         44  
264 6         11 my $group_match = 1; # All conditions in this AND group must match
265              
266 6         14 foreach my $condition (@$and_group) {
267 6         24 my ($field, $op, $value) = ($condition->{field}, $condition->{op}, $condition->{value});
268 6   50     21 my $user_value = $user_data->{$field} || '';
269              
270 6 100       22 if ($op eq '=') {
    50          
    0          
    0          
    0          
271 3 100       13 $group_match = 0 unless $user_value eq $value;
272             } elsif ($op eq ':') {
273 3 100       39 $group_match = 0 unless $user_value =~ /\Q$value\E/i;
274             } elsif ($op eq '!') {
275 0 0       0 $group_match = 0 if $user_value =~ /\Q$value\E/i;
276             } elsif ($op eq '>') {
277 0 0       0 $group_match = 0 unless $user_value gt $value;
278             } elsif ($op eq '<') {
279 0 0       0 $group_match = 0 unless $user_value lt $value;
280             }
281             }
282              
283 6 100       16 $match = 1 if $group_match; # At least one OR group matched
284 6 100       18 last if $match;
285             }
286             }
287              
288 14 100       64 push @users, $user_data if $match;
289             }
290              
291             return {
292 6         49 data => \@users,
293             total_count => scalar @users,
294             };
295             }
296              
297             # Delete user
298             sub delete {
299 2     2 1 8 my ($self, $user_id) = @_;
300              
301 2         7 my $user_file = $self->_get_user_file($user_id);
302              
303 2 50       56 return { success => 0, message => "User '$user_id' not found" } unless -f $user_file;
304              
305 2 50       229 unlink $user_file or return { success => 0, message => "Failed to delete user file: $!" };
306              
307 2         20 return { success => 1, message => "User '$user_id' deleted" };
308             }
309              
310             # Cleanup
311             sub disconnect {
312 17     17 0 1131 my $self = shift;
313             # No resources to clean up for YAML backend
314             }
315              
316             1;
317              
318             __END__