File Coverage

blib/lib/IO/K8s/Resource.pm
Criterion Covered Total %
statement 105 110 95.4
branch 59 76 77.6
condition 15 21 71.4
subroutine 17 17 100.0
pod n/a
total 196 224 87.5


line stmt bran cond sub pod time code
1             package IO::K8s::Resource;
2             # ABSTRACT: Base class for all Kubernetes resources
3             our $VERSION = '1.009';
4 28     28   109586 use v5.10;
  28         114  
5 28     28   4626 use Moo ();
  28         74692  
  28         592  
6 28     28   11310 use Moo::Role ();
  28         212722  
  28         825  
7 28     28   16061 use Import::Into;
  28         41109  
  28         1140  
8 28     28   4508 use Package::Stash;
  28         52628  
  28         1208  
9 28     28   24474 use Types::Standard qw( ArrayRef Bool HashRef InstanceOf Int Maybe Str );
  28         3791495  
  28         438  
10 28     28   137099 use IO::K8s::Types qw( IntOrStr Quantity Time );
  28         194  
  28         334  
11 28     28   117711 use Scalar::Util qw(blessed);
  28         63  
  28         34057  
12              
13             # Registry: class -> attr -> { type, class, is_array, is_hash, is_bool, is_int }
14             # Use 'our' to make it a proper package variable accessible via symbol table
15             our %_attr_registry;
16              
17             # Class name expansion map
18             my %_class_prefix = (
19             'Core' => 'IO::K8s::Api::Core',
20             'Apps' => 'IO::K8s::Api::Apps',
21             'Batch' => 'IO::K8s::Api::Batch',
22             'Networking' => 'IO::K8s::Api::Networking',
23             'Rbac' => 'IO::K8s::Api::Rbac',
24             'Storage' => 'IO::K8s::Api::Storage',
25             'Policy' => 'IO::K8s::Api::Policy',
26             'Autoscaling' => 'IO::K8s::Api::Autoscaling',
27             'Admissionregistration' => 'IO::K8s::Api::Admissionregistration',
28             'Coordination' => 'IO::K8s::Api::Coordination',
29             'Discovery' => 'IO::K8s::Api::Discovery',
30             'Events' => 'IO::K8s::Api::Events',
31             'Flowcontrol' => 'IO::K8s::Api::Flowcontrol',
32             'Node' => 'IO::K8s::Api::Node',
33             'Scheduling' => 'IO::K8s::Api::Scheduling',
34             'Certificates' => 'IO::K8s::Api::Certificates',
35             'Authentication' => 'IO::K8s::Api::Authentication',
36             'Authorization' => 'IO::K8s::Api::Authorization',
37             'Resource' => 'IO::K8s::Api::Resource',
38             'Storagemigration' => 'IO::K8s::Api::Storagemigration',
39             'Meta' => 'IO::K8s::Apimachinery::Pkg::Apis::Meta',
40             'Apiextensions' => 'IO::K8s::ApiextensionsApiserver::Pkg::Apis::Apiextensions',
41             'KubeAggregator' => 'IO::K8s::KubeAggregator::Pkg::Apis::Apiregistration',
42             );
43              
44             # Type flag lookup table
45             my %TYPE_FLAGS = (
46             Str => { is_str => 1 },
47             Int => { is_int => 1 },
48             Bool => { is_bool => 1 },
49             IntOrStr => { is_int_or_string => 1 },
50             Quantity => { is_quantity => 1 },
51             Time => { is_time => 1 },
52             );
53              
54             # For string path: map type name to base Type::Tiny constraint
55             # Custom K8s types (IntOrStr, Quantity, Time) fall back to Str
56             my %STR_ISA_MAP = (
57             Str => Str,
58             Int => Int,
59             Bool => Bool,
60             );
61              
62             sub import {
63 1076     1076   84448 my $class = shift;
64 1076         4425 my $caller = caller;
65 1076         13449 $class->_setup_class($caller);
66             }
67              
68             sub _setup_class {
69 1081     1081   4025 my ($class, $target) = @_;
70 1081         11160 Moo->import::into($target);
71 1081         816400 Types::Standard->import::into($target, qw( Str Int Bool ));
72 1081         3922363 IO::K8s::Types->import::into($target, qw( IntOrStr Quantity Time ));
73 1081         5012374 Moo::Role->apply_roles_to_package($target, 'IO::K8s::Role::Resource');
74 1081         4453002 my $stash = Package::Stash->new($target);
75 1081     4905   116974 $stash->add_symbol('&k8s', sub { $class->_k8s($target, @_) });
  4905         49797  
76             }
77              
78             sub _expand_class {
79 1854     1854   4765 my ($short) = @_;
80              
81             # +FullClassName - strip + and use as-is
82 1854 50       6532 return substr($short, 1) if $short =~ /^\+/;
83              
84             # Already fully qualified?
85 1854 50       6177 return $short if $short =~ /^IO::K8s::/;
86              
87             # Check for prefix match (e.g., Core::V1::Pod)
88 1854 100       12907 if ($short =~ /^([A-Z][a-z]+)::/) {
89 1850         7360 my $prefix = $1;
90 1850 100       8000 if (my $expansion = $_class_prefix{$prefix}) {
91 1846         29012 $short =~ s/^$prefix/$expansion/;
92 1846         7730 return $short;
93             }
94             }
95              
96             # Default: assume it's under IO::K8s::Api
97 8         22 return "IO::K8s::Api::$short";
98             }
99              
100             sub _is_type_tiny {
101 7748     7748   14589 my ($obj) = @_;
102 7748   66     51280 return blessed($obj) && $obj->isa('Type::Tiny');
103             }
104              
105             # Sanitize JSON field names into valid Perl identifiers for Moo attributes
106             # $ref -> _ref, $schema -> _schema, x-kubernetes-foo -> x_kubernetes_foo
107             sub _sanitize_attr_name {
108 4905     4905   10763 my ($name) = @_;
109 4905 100       29597 return $name unless $name =~ /[^a-zA-Z0-9_]/;
110 9         52 (my $safe = $name) =~ s/^\$/_/;
111 9         52 $safe =~ s/-/_/g;
112 9         29 return $safe;
113             }
114              
115             sub _k8s {
116 4905     4905   16588 my ($class, $caller, $name, $type_spec, $required_marker) = @_;
117              
118 4905         9830 my $json_key = $name;
119 4905         16101 my $attr_name = _sanitize_attr_name($name);
120              
121             # Ensure the registry entry exists
122 4905 100       19878 $_attr_registry{$caller} = {} unless exists $_attr_registry{$caller};
123              
124 4905         9254 my %info;
125             my $isa;
126 4905 100 66     21093 my $required = $required_marker && $required_marker eq 'required' ? 1 : 0;
127              
128             # Check for ! suffix on strings (legacy/alternative required syntax)
129 4905 50 66     33988 if (!ref $type_spec && !_is_type_tiny($type_spec) && $type_spec =~ s/!$//) {
    50 66        
      100        
      66        
130 0         0 $required = 1;
131             } elsif (ref $type_spec eq 'ARRAY' && !_is_type_tiny($type_spec->[0]) && $type_spec->[0] =~ s/!$//) {
132 0         0 $required = 1;
133             }
134              
135             # Handle Type::Tiny objects directly (Str, Int, Bool, IntOrStr, Quantity, Time)
136 4905 100       21154 if (_is_type_tiny($type_spec)) {
    100          
    100          
    50          
137 2374         42251 my $flags = $TYPE_FLAGS{$type_spec->name};
138 2374 50       77264 if ($flags) {
139 2374         9535 %info = %$flags;
140 2374 100       12855 $isa = $required ? $type_spec : Maybe[$type_spec];
141             }
142             } elsif (!ref $type_spec) {
143 1389 100       5326 if (my $flags = $TYPE_FLAGS{$type_spec}) {
144 19         75 %info = %$flags;
145 19   66     77 my $base = $STR_ISA_MAP{$type_spec} // Str;
146 19 50       276 $isa = $required ? $base : Maybe[$base];
147             } else {
148 1370         4367 my $full_class = _expand_class($type_spec);
149 1370         4427 $info{is_object} = 1;
150 1370         3887 $info{class} = $full_class;
151 1370 100       9410 $isa = $required ? InstanceOf[$full_class] : Maybe[InstanceOf[$full_class]];
152             }
153             } elsif (ref $type_spec eq 'ARRAY') {
154 727         1887 my $inner = $type_spec->[0];
155             # Handle [Str] with Type::Tiny object
156 727 100       1863 if (_is_type_tiny($inner)) {
    100          
    50          
157 248         3016 my $type_name = $inner->name;
158 248 100       6958 if ($type_name eq 'Str') {
    50          
159 243         774 $info{is_array_of_str} = 1;
160             } elsif ($type_name eq 'Int') {
161 5         21 $info{is_array_of_int} = 1;
162             }
163 248 100       1862 $isa = $required ? ArrayRef[$inner] : Maybe[ArrayRef[$inner]];
164             } elsif ($inner eq 'Str') {
165 1         3 $info{is_array_of_str} = 1;
166 1 50       6 $isa = $required ? ArrayRef[Str] : Maybe[ArrayRef[Str]];
167             } elsif ($inner eq 'Int') {
168 0         0 $info{is_array_of_int} = 1;
169 0 0       0 $isa = $required ? ArrayRef[Int] : Maybe[ArrayRef[Int]];
170             } else {
171 478         1846 my $full_class = _expand_class($inner);
172 478         1769 $info{is_array_of_objects} = 1;
173 478         1313 $info{class} = $full_class;
174 478 100       3576 $isa = $required ? ArrayRef[InstanceOf[$full_class]] : Maybe[ArrayRef[InstanceOf[$full_class]]];
175             }
176             } elsif (ref $type_spec eq 'HASH') {
177 415         1329 my ($inner) = keys %$type_spec;
178 415 100       1269 if ($inner eq 'Str') {
179 409         1143 $info{is_hash_of_str} = 1;
180             # Use plain HashRef without inner constraint - K8s has nested hashes
181             # in fields like fieldsV1, annotations, labels which can have any structure
182 409 50       2443 $isa = $required ? HashRef : Maybe[HashRef];
183             } else {
184 6         23 my $full_class = _expand_class($inner);
185 6         23 $info{is_hash_of_objects} = 1;
186 6         19 $info{class} = $full_class;
187 6 50       59 $isa = $required ? HashRef[InstanceOf[$full_class]] : Maybe[HashRef[InstanceOf[$full_class]]];
188             }
189             }
190              
191              
192             # Store json_key when it differs from the Perl attribute name
193 4905 100       3031581 $info{json_key} = $json_key if $attr_name ne $json_key;
194              
195             # Register - use hash slice to copy values, not reference
196 4905         32398 $_attr_registry{$caller}{$attr_name} = { %info };
197 28     28   254 no strict 'refs';
  28         61  
  28         8780  
198 4905         9286 push @{"${caller}::_k8s_attributes"}, $attr_name;
  4905         39239  
199              
200             # Only create the attribute if it doesn't already exist (e.g., from a role)
201 4905 100       81031 return if $caller->can($attr_name);
202              
203             # Call Moo's has — use init_arg to map JSON key to Perl-safe attribute name
204 4584         26653 my $has = $caller->can('has');
205 4584         8862 my @coerce;
206             # Bool attributes: coerce \0/\1 refs and JSON booleans to plain 0/1
207 4584 100       12869 if ($info{is_bool}) {
208 274 0   82   1968 @coerce = (coerce => sub { ref $_[0] ? (${$_[0]} ? 1 : 0) : ($_[0] ? 1 : 0) });
  82 100       130605  
  0 50       0  
209             }
210 4584 100       24478 $has->($attr_name, is => 'rw', isa => $isa, @coerce,
    100          
211             ($required ? (required => 1) : ()),
212             ($attr_name ne $json_key ? (init_arg => $json_key) : ()),
213             );
214             }
215              
216             1;
217              
218             __END__