File Coverage

blib/lib/Data/NestedKey.pm
Criterion Covered Total %
statement 119 126 94.4
branch 41 56 73.2
condition 19 32 59.3
subroutine 18 18 100.0
pod 6 6 100.0
total 203 238 85.2


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