File Coverage

blib/lib/Data/NestedKey.pm
Criterion Covered Total %
statement 102 125 81.6
branch 25 56 44.6
condition 13 32 40.6
subroutine 17 18 94.4
pod 6 6 100.0
total 163 237 68.7


line stmt bran cond sub pod time code
1             package Data::NestedKey;
2              
3             # This module provides an object-oriented way to manipulate deeply nested hash
4             # structures using dot-separated keys, with flexible serialization options.
5              
6 2     2   231345 use strict;
  2         4  
  2         69  
7 2     2   21 use warnings;
  2         3  
  2         102  
8              
9 2     2   10 use Carp;
  2         3  
  2         153  
10 2     2   1204 use Data::Dumper;
  2         18812  
  2         190  
11 2     2   1400 use JSON;
  2         28229  
  2         11  
12 2     2   334 use List::Util qw(pairs);
  2         4  
  2         178  
13 2     2   12 use Scalar::Util qw(reftype);
  2         4  
  2         86  
14 2     2   1114 use Storable qw(nfreeze);
  2         8051  
  2         153  
15 2     2   1007 use YAML (); # Load YAML support
  2         17727  
  2         128  
16              
17             our $VERSION = '0.06';
18              
19             # Package variables for serialization options
20             our $JSON_PRETTY = 1; # Controls whether JSON output is pretty or compact
21             our $FORMAT = 'JSON'; # Default serialization format
22              
23 2     2   11 use overload '""' => \&as_string;
  2         4  
  2         10  
24              
25             ########################################################################
26             sub new {
27             ########################################################################
28 2     2 1 475572 my ( $class, @args ) = @_;
29              
30 2 50       13 my $init_data = ref $args[0] ? shift @args : {};
31 2         8 my @kv_list = @args;
32              
33             # If the first argument is a hash reference, use it; otherwise, start with an empty structure
34 2 50       10 my $self = bless { data => _is_hash($init_data) ? $init_data : {} }, $class;
35              
36             # If $init_data wasn't a hash ref, treat it as a key-value pair
37 2 50       13 if ( !_is_hash($init_data) ) {
38 0         0 @kv_list = ( $init_data, @kv_list );
39             }
40              
41             # Short-circuit if no key-value pairs are provided
42 2 50       10 return $self
43             if !@kv_list;
44              
45             # Ensure key-value pairs are valid
46 2 50 33     16 croak 'Must provide key-value pairs'
47             if @kv_list && @kv_list % 2 != 0;
48              
49             # Populate the structure using `set`
50 2         19 $self->set(@kv_list);
51              
52 2         10 return $self;
53             }
54              
55             ########################################################################
56 0   0 0   0 sub _is_array { return ref $_[0] && reftype( $_[0] ) eq 'ARRAY'; }
57             ########################################################################
58              
59             ########################################################################
60 25   66 25   220 sub _is_hash { return ref $_[0] && reftype( $_[0] ) eq 'HASH'; }
61             ########################################################################
62              
63             ########################################################################
64             sub set {
65             ########################################################################
66 3     3 1 13 my ( $self, @kv_list ) = @_;
67 3 50       14 croak 'Must provide key-value pairs' if @kv_list % 2 != 0;
68              
69 3         48 for my $p ( pairs @kv_list ) {
70 5         9 my ( $key_path, $value ) = @{$p};
  5         29  
71 5 50       41 my $action = $key_path =~ s/^([+-])// ? $1 : q{};
72              
73 5         18 my @keys = split /[.]/, $key_path;
74 5         73 my $current = $self->{data};
75              
76 5         20 for my $key ( @keys[ 0 .. $#keys - 1 ] ) {
77 6   100     28 $current->{$key} //= {};
78 6         14 $current = $current->{$key};
79             }
80              
81 5         12 my $final_key = $keys[-1];
82              
83 5 50       18 if ( $action eq q{+} ) {
    50          
84 0 0 0     0 if ( _is_array( $current->{$final_key} ) ) {
    0          
    0          
    0          
85 0         0 push @{ $current->{$final_key} }, $value;
  0         0  
86             }
87             elsif ( _is_hash( $current->{$final_key} ) && _is_hash($value) ) {
88 0         0 %{ $current->{$final_key} } = ( %{ $current->{$final_key} }, %{$value} );
  0         0  
  0         0  
  0         0  
89             }
90             elsif ( _is_hash( $current->{$final_key} ) ) {
91 0         0 croak sprintf q{Error: Attempting to merge a non-hash into a hash at key '%s'.}, $final_key;
92             }
93             elsif ( exists $current->{$final_key} ) {
94 0         0 $current->{$final_key} = [ $current->{$final_key}, $value ];
95             }
96             else {
97 0         0 $current->{$final_key} = [$value];
98             }
99             }
100             elsif ( $action eq q{-} ) {
101 0 0       0 if ( _is_array( $current->{$final_key} ) ) {
    0          
102 0         0 @{ $current->{$final_key} } = grep { $_ ne $value } @{ $current->{$final_key} };
  0         0  
  0         0  
  0         0  
103             }
104             elsif ( _is_hash( $current->{$final_key} ) ) {
105 0         0 delete $current->{$final_key}{$value};
106             }
107             else {
108 0         0 delete $current->{$final_key};
109             }
110             }
111             else {
112             croak sprintf q{Error: Attempting to replace a hash reference at key '%s' with a scalar value.},
113             $final_key
114 5 50 33     20 if _is_hash( $current->{$final_key} ) && !_is_hash($value);
115              
116 5         30 $current->{$final_key} = $value;
117             }
118             }
119              
120 3         23 return $self;
121             }
122              
123             ########################################################################
124             sub get {
125             ########################################################################
126 3     3 1 34 my ( $self, @key_paths ) = @_;
127 3         5 my @results;
128              
129 3         10 for my $key_path (@key_paths) {
130 3         11 my @keys = split /[.]/, $key_path;
131 3         7 my $current = $self->{data};
132              
133 3         7 for my $key (@keys) {
134 9 50 33     19 if ( _is_hash($current) && exists $current->{$key} ) {
135 9         23 $current = $current->{$key};
136             }
137             else {
138 0         0 $current = undef;
139 0         0 last;
140             }
141             }
142              
143 3         11 push @results, $current;
144             }
145              
146 3 50       36 return wantarray ? @results : $results[0]; # Ensure it works in scalar and list context
147             }
148              
149             ########################################################################
150             sub as_string {
151             ########################################################################
152 9     9 1 73927 my ($self) = @_;
153              
154 9 50 66     234 return JSON->new->pretty->encode( $self->{data} ) if $FORMAT eq 'JSON' && $JSON_PRETTY;
155 6 50       22 return JSON->new->encode( $self->{data} ) if $FORMAT eq 'JSON';
156 6 100       27 return YAML::Dump( $self->{data} ) if $FORMAT eq 'YAML';
157 4 100       23 return Dumper( $self->{data} ) if $FORMAT eq 'Dumper';
158 2 50       13 return nfreeze( $self->{data} ) if $FORMAT eq 'Storable';
159              
160 0         0 croak "Unsupported format: $FORMAT";
161             }
162              
163             ########################################################################
164             sub delete {
165             ########################################################################
166 1     1 1 5 my ( $self, @key_paths ) = @_;
167              
168 1         3 for my $key_path (@key_paths) {
169 1         5 my @keys = split /[.]/, $key_path;
170 1         4 my $current = $self->{data};
171 1         2 my @parents; # Track parent references
172              
173 1         5 for my $key ( @keys[ 0 .. $#keys - 1 ] ) {
174 2 50 33     5 last if !_is_hash($current) || !exists $current->{$key};
175              
176 2         7 push @parents, [ $current, $key ]; # Store parent reference
177 2         5 $current = $current->{$key};
178             }
179              
180 1         4 my $final_key = $keys[-1];
181 1 50       4 delete $current->{$final_key} if exists $current->{$final_key};
182              
183             # Cleanup empty parent hashes
184 1         11 while (@parents) {
185 2         4 my ( $parent, $key ) = @{ pop @parents };
  2         6  
186              
187 2 50 33     6 if ( _is_hash( $parent->{$key} ) && !%{ $parent->{$key} } ) {
  2         11  
188 0         0 delete $parent->{$key};
189             }
190             }
191             }
192              
193 1         3 return $self;
194             }
195              
196             ########################################################################
197             sub exists_key {
198             ########################################################################
199 1     1 1 3 my ( $self, @key_paths ) = @_;
200 1         2 my @results;
201              
202 1         3 for my $key_path (@key_paths) {
203 1         4 my @keys = split /[.]/, $key_path;
204 1         2 my $current = $self->{data};
205 1         2 my $exists = 1;
206              
207 1         2 for my $key (@keys) {
208 3 100 66     7 if ( _is_hash($current) && exists $current->{$key} ) {
209 2         4 $current = $current->{$key};
210             }
211             else {
212 1         2 $exists = 0;
213 1         2 last;
214             }
215             }
216              
217 1         4 push @results, $exists;
218             }
219              
220 1 50       7 return wantarray ? @results : $results[0]; # Ensures proper scalar context behavior
221             }
222              
223             1;
224              
225             __END__