File Coverage

blib/lib/IO/K8s/Resource.pm
Criterion Covered Total %
statement 101 105 96.1
branch 54 68 79.4
condition 15 21 71.4
subroutine 16 16 100.0
pod n/a
total 186 210 88.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.008';
4 28     28   154861 use v5.10;
  28         108  
5 28     28   4923 use Moo ();
  28         79591  
  28         560  
6 28     28   11386 use Moo::Role ();
  28         243309  
  28         853  
7 28     28   15602 use Import::Into;
  28         47461  
  28         1246  
8 28     28   5345 use Package::Stash;
  28         61934  
  28         1250  
9 28     28   17740 use Types::Standard qw( ArrayRef Bool HashRef InstanceOf Int Maybe Str );
  28         3789922  
  28         370  
10 28     28   130887 use IO::K8s::Types qw( IntOrStr Quantity Time );
  28         240  
  28         411  
11 28     28   133721 use Scalar::Util qw(blessed);
  28         66  
  28         34480  
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   91413 my $class = shift;
64 1076         4345 my $caller = caller;
65 1076         13442 $class->_setup_class($caller);
66             }
67              
68             sub _setup_class {
69 1081     1081   3837 my ($class, $target) = @_;
70 1081         11796 Moo->import::into($target);
71 1081         897596 Types::Standard->import::into($target, qw( Str Int Bool ));
72 1081         4297031 IO::K8s::Types->import::into($target, qw( IntOrStr Quantity Time ));
73 1081         5777054 Moo::Role->apply_roles_to_package($target, 'IO::K8s::Role::Resource');
74 1081         4800277 my $stash = Package::Stash->new($target);
75 1081     4905   125340 $stash->add_symbol('&k8s', sub { $class->_k8s($target, @_) });
  4905         56770  
76             }
77              
78             sub _expand_class {
79 1854     1854   4722 my ($short) = @_;
80              
81             # +FullClassName - strip + and use as-is
82 1854 50       6486 return substr($short, 1) if $short =~ /^\+/;
83              
84             # Already fully qualified?
85 1854 50       5949 return $short if $short =~ /^IO::K8s::/;
86              
87             # Check for prefix match (e.g., Core::V1::Pod)
88 1854 100       12578 if ($short =~ /^([A-Z][a-z]+)::/) {
89 1850         7293 my $prefix = $1;
90 1850 100       7763 if (my $expansion = $_class_prefix{$prefix}) {
91 1846         33335 $short =~ s/^$prefix/$expansion/;
92 1846         8876 return $short;
93             }
94             }
95              
96             # Default: assume it's under IO::K8s::Api
97 8         28 return "IO::K8s::Api::$short";
98             }
99              
100             sub _is_type_tiny {
101 7748     7748   15731 my ($obj) = @_;
102 7748   66     57002 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   12488 my ($name) = @_;
109 4905 100       32537 return $name unless $name =~ /[^a-zA-Z0-9_]/;
110 9         38 (my $safe = $name) =~ s/^\$/_/;
111 9         50 $safe =~ s/-/_/g;
112 9         24 return $safe;
113             }
114              
115             sub _k8s {
116 4905     4905   16817 my ($class, $caller, $name, $type_spec, $required_marker) = @_;
117              
118 4905         10650 my $json_key = $name;
119 4905         16174 my $attr_name = _sanitize_attr_name($name);
120              
121             # Ensure the registry entry exists
122 4905 100       35629 $_attr_registry{$caller} = {} unless exists $_attr_registry{$caller};
123              
124 4905         10060 my %info;
125             my $isa;
126 4905 100 66     21783 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     37475 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       18678 if (_is_type_tiny($type_spec)) {
    100          
    100          
    50          
137 2374         43419 my $flags = $TYPE_FLAGS{$type_spec->name};
138 2374 50       82474 if ($flags) {
139 2374         9936 %info = %$flags;
140 2374 100       14425 $isa = $required ? $type_spec : Maybe[$type_spec];
141             }
142             } elsif (!ref $type_spec) {
143 1389 100       5439 if (my $flags = $TYPE_FLAGS{$type_spec}) {
144 19         73 %info = %$flags;
145 19   66     100 my $base = $STR_ISA_MAP{$type_spec} // Str;
146 19 50       347 $isa = $required ? $base : Maybe[$base];
147             } else {
148 1370         4789 my $full_class = _expand_class($type_spec);
149 1370         4888 $info{is_object} = 1;
150 1370         3598 $info{class} = $full_class;
151 1370 100       9526 $isa = $required ? InstanceOf[$full_class] : Maybe[InstanceOf[$full_class]];
152             }
153             } elsif (ref $type_spec eq 'ARRAY') {
154 727         1870 my $inner = $type_spec->[0];
155             # Handle [Str] with Type::Tiny object
156 727 100       1806 if (_is_type_tiny($inner)) {
    100          
    50          
157 248         3173 my $type_name = $inner->name;
158 248 100       8262 if ($type_name eq 'Str') {
    50          
159 243         861 $info{is_array_of_str} = 1;
160             } elsif ($type_name eq 'Int') {
161 5         23 $info{is_array_of_int} = 1;
162             }
163 248 100       1931 $isa = $required ? ArrayRef[$inner] : Maybe[ArrayRef[$inner]];
164             } elsif ($inner eq 'Str') {
165 1         3 $info{is_array_of_str} = 1;
166 1 50       5 $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         1670 my $full_class = _expand_class($inner);
172 478         1733 $info{is_array_of_objects} = 1;
173 478         1244 $info{class} = $full_class;
174 478 100       4647 $isa = $required ? ArrayRef[InstanceOf[$full_class]] : Maybe[ArrayRef[InstanceOf[$full_class]]];
175             }
176             } elsif (ref $type_spec eq 'HASH') {
177 415         1476 my ($inner) = keys %$type_spec;
178 415 100       1359 if ($inner eq 'Str') {
179 409         1168 $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       2389 $isa = $required ? HashRef : Maybe[HashRef];
183             } else {
184 6         22 my $full_class = _expand_class($inner);
185 6         20 $info{is_hash_of_objects} = 1;
186 6         22 $info{class} = $full_class;
187 6 50       75 $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       3269988 $info{json_key} = $json_key if $attr_name ne $json_key;
194              
195             # Register - use hash slice to copy values, not reference
196 4905         36354 $_attr_registry{$caller}{$attr_name} = { %info };
197 28     28   246 no strict 'refs';
  28         54  
  28         7141  
198 4905         11175 push @{"${caller}::_k8s_attributes"}, $attr_name;
  4905         40159  
199              
200             # Only create the attribute if it doesn't already exist (e.g., from a role)
201 4905 100       83314 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         21273 my $has = $caller->can('has');
205 4584 100       29492 $has->($attr_name, is => 'rw', isa => $isa,
    100          
206             ($required ? (required => 1) : ()),
207             ($attr_name ne $json_key ? (init_arg => $json_key) : ()),
208             );
209             }
210              
211             1;
212              
213             __END__