File Coverage

blib/lib/IO/K8s/Role/Resource.pm
Criterion Covered Total %
statement 91 128 71.0
branch 48 90 53.3
condition 19 54 35.1
subroutine 16 17 94.1
pod 0 7 0.0
total 174 296 58.7


line stmt bran cond sub pod time code
1             package IO::K8s::Role::Resource;
2             # ABSTRACT: Role providing Kubernetes resource instance behavior
3             our $VERSION = '1.009';
4 28     28   19113 use v5.10;
  28         103  
5 28     28   164 use Moo::Role;
  28         48  
  28         227  
6 28     28   20226 use JSON::MaybeXS ();
  28         84991  
  28         862  
7 28     28   151 use Scalar::Util qw(blessed);
  28         82  
  28         5327  
8              
9             has json => (
10             is => 'ro',
11             lazy => 1,
12             builder => '_build_json',
13             );
14              
15             sub _build_json {
16 35     35   695 return JSON::MaybeXS->new(utf8 => 1, canonical => 1);
17             }
18              
19             # Get attribute info from the global registry in IO::K8s::Resource
20             sub _k8s_attr_info {
21 2895     2895   18465 my ($class) = @_;
22 2895 100       7282 $class = ref($class) if ref($class);
23 2895   100     10504 return $IO::K8s::Resource::_attr_registry{$class} // {};
24             }
25              
26             # Get attribute list (stored as per-class package variable)
27             sub _k8s_attributes {
28 1320     1320   2468 my ($self) = @_;
29 1320   33     3606 my $class = ref($self) || $self;
30 28     28   202 no strict 'refs';
  28         51  
  28         38033  
31 1320         1886 return \@{"${class}::_k8s_attributes"};
  1320         8607  
32             }
33              
34             sub TO_JSON {
35 1320     1320 0 373699 my $self = shift;
36 1320         2172 my %data;
37 1320         3637 my $attrs = $self->_k8s_attributes;
38 1320         2769 my $info = _k8s_attr_info($self);
39              
40             # Add apiVersion, kind, and metadata for APIObjects (those with the role)
41 1320 100 66     7742 if ($self->can('_is_resource') && $self->_is_resource) {
42 203 50       845 $data{apiVersion} = $self->api_version if $self->api_version;
43 203 50       848 $data{kind} = $self->kind if $self->kind;
44             # metadata comes from the Role, not from k8s DSL
45 203 100 66     6317 if ($self->can('metadata') && $self->metadata) {
46 189         5014 $data{metadata} = $self->metadata->TO_JSON;
47             }
48             }
49              
50 1320         3212 for my $attr (@$attrs) {
51 13518         254923 my $value = $self->$attr;
52 13518 100       89935 next unless defined $value;
53              
54 2785   50     6580 my $attr_info = $info->{$attr} // {};
55             # Use json_key for output when attr name differs from JSON field name
56 2785   33     8626 my $key = $attr_info->{json_key} // $attr;
57              
58 2785 100 66     19986 if ($attr_info->{is_bool}) {
    100 66        
    100          
    100          
    100          
    50          
    50          
    100          
    100          
59 65 100       273 $data{$key} = $value ? JSON::MaybeXS::true : JSON::MaybeXS::false;
60             } elsif ($attr_info->{is_int}) {
61 181         596 $data{$key} = int($value);
62             } elsif ($attr_info->{is_int_or_string}) {
63 56 100       455 $data{$key} = ($value =~ /\A-?\d+\z/) ? int($value) : $value;
64             } elsif ($attr_info->{is_object} && blessed($value) && $value->can('TO_JSON')) {
65 646         1863 $data{$key} = $value->TO_JSON;
66             } elsif ($attr_info->{is_array_of_objects}) {
67 217         565 $data{$key} = [ map { $_->TO_JSON } @$value ];
  311         1708  
68             } elsif ($attr_info->{is_hash_of_objects}) {
69 0         0 $data{$key} = { map { $_ => $value->{$_}->TO_JSON } keys %$value };
  0         0  
70             } elsif ($attr_info->{is_array_of_int}) {
71 0         0 $data{$key} = [ map { int($_) } @$value ];
  0         0  
72             } elsif (ref $value eq 'ARRAY') {
73 101         351 $data{$key} = $value;
74             } elsif (ref $value eq 'HASH') {
75 361         1095 $data{$key} = $value;
76             } else {
77 1158         3391 $data{$key} = $value;
78             }
79             }
80 1320         7101 return \%data;
81             }
82              
83             sub to_json {
84 36     36 0 14461 my $self = shift;
85 36         1062 return $self->json->encode($self->TO_JSON);
86             }
87              
88             sub TO_YAML {
89 18     18 0 3870 my $self = shift;
90 18         5760 require YAML::PP;
91 18         660510 my $yp = YAML::PP->new(schema => [qw/JSON/], boolean => 'JSON::PP');
92 18         19361 return $yp->dump_string($self->TO_JSON);
93             }
94              
95             sub to_yaml {
96 17     17 0 21717 my $self = shift;
97 17         89 return $self->TO_YAML;
98             }
99              
100             sub FROM_HASH {
101 1     1 0 4915 my ($class, $hash) = @_;
102 1         45 return $class->new(%$hash);
103             }
104              
105             sub from_json {
106 0     0 0 0 my ($class, $json_str) = @_;
107 0         0 state $json = JSON::MaybeXS->new;
108 0         0 return $class->FROM_HASH($json->decode($json_str));
109             }
110              
111             # Compare local class attributes against OpenAPI schema
112             # Returns hashref with differences:
113             # missing_locally => [ attrs in schema but not in class ]
114             # missing_in_schema => [ attrs in class but not in schema ]
115             # type_mismatch => [ { attr => $name, local => $type, schema => $type } ]
116             sub compare_to_schema {
117 1     1 0 1124 my ($class, $schema) = @_;
118 1 50       6 $class = ref($class) if ref($class);
119              
120 1   50     8 my $local_attrs = $IO::K8s::Resource::_attr_registry{$class} // {};
121 1   50     5 my $schema_props = $schema->{properties} // {};
122              
123             # Build json_key -> attr_name mapping for lookup
124 1         1 my %json_to_attr;
125 1         5 for my $attr (keys %$local_attrs) {
126 4   33     14 my $jk = $local_attrs->{$attr}{json_key} // $attr;
127 4         9 $json_to_attr{$jk} = $attr;
128             }
129              
130 1         8 my %result = (
131             missing_locally => [],
132             missing_in_schema => [],
133             type_mismatch => [],
134             );
135              
136             # Check schema properties against local attributes
137 1         3 for my $prop (keys %$schema_props) {
138 4         5 my $attr = $json_to_attr{$prop};
139 4 50       9 if (!defined $attr) {
140             # Special case: metadata comes from Role, not k8s DSL
141 0 0 0     0 next if $prop eq 'metadata' && $class->can('metadata');
142             # apiVersion and kind also come from Role
143 0 0 0     0 next if ($prop eq 'apiVersion' || $prop eq 'kind') && $class->can('_is_resource');
      0        
144 0         0 push @{$result{missing_locally}}, $prop;
  0         0  
145             } else {
146             # Compare types
147 4         7 my $local_type = _describe_local_type($local_attrs->{$attr});
148 4         9 my $schema_type = _describe_schema_type($schema_props->{$prop});
149 4 50       8 if ($local_type ne $schema_type) {
150 0         0 push @{$result{type_mismatch}}, {
  0         0  
151             attr => $prop,
152             local => $local_type,
153             schema => $schema_type,
154             };
155             }
156             }
157             }
158              
159             # Check local attributes not in schema
160 1         3 for my $attr (keys %$local_attrs) {
161 4   33     10 my $jk = $local_attrs->{$attr}{json_key} // $attr;
162 4 50       8 if (!exists $schema_props->{$jk}) {
163 0         0 push @{$result{missing_in_schema}}, $jk;
  0         0  
164             }
165             }
166              
167 1         4 return \%result;
168             }
169              
170             sub _describe_local_type {
171 4     4   6 my ($info) = @_;
172 4 50       7 return 'string' if $info->{is_str};
173 4 50       7 return 'integer' if $info->{is_int};
174 4 50       6 return 'int-or-string' if $info->{is_int_or_string};
175 4 100       7 return 'quantity' if $info->{is_quantity};
176 3 50       6 return 'date-time' if $info->{is_time};
177 0 0       0 return 'boolean' if $info->{is_bool};
178 0 0       0 return 'array' if $info->{is_array_of_str};
179 0 0       0 return 'array' if $info->{is_array_of_int};
180 0 0       0 return 'array' if $info->{is_array_of_objects};
181 0 0       0 return 'hash' if $info->{is_hash_of_str};
182 0 0       0 return 'hash' if $info->{is_hash_of_objects};
183 0 0       0 return 'object' if $info->{is_object};
184 0         0 return 'unknown';
185             }
186              
187             sub _describe_schema_type {
188 4     4   5 my ($prop) = @_;
189 4 100       11 if (my $ref = $prop->{'$ref'}) {
190 3 50       6 return 'int-or-string' if $ref =~ /intstr\.IntOrString$/;
191 3 100       8 return 'quantity' if $ref =~ /resource\.Quantity$/;
192 2 50       11 return 'date-time' if $ref =~ /meta\.v1\.(Micro)?Time$/;
193 0         0 return 'object';
194             }
195 1   50     3 my $type = $prop->{type} // 'unknown';
196 1   50     3 my $format = $prop->{format} // '';
197 1 50       20 return 'int-or-string' if $format eq 'int-or-string';
198 1 50       6 return 'date-time' if $format eq 'date-time';
199 0 0         if ($type eq 'array') {
200 0   0       my $items = $prop->{items} // {};
201 0 0         if ($items->{'$ref'}) {
202 0           return 'array';
203             }
204 0   0       my $item_type = $items->{type} // 'unknown';
205 0           return "array<$item_type>";
206             }
207 0 0 0       if ($type eq 'object' && $prop->{additionalProperties}) {
208 0           my $add = $prop->{additionalProperties};
209 0 0         if ($add->{'$ref'}) {
210 0           return 'hash';
211             }
212 0   0       my $val_type = $add->{type} // 'unknown';
213 0           return "hash<$val_type>";
214             }
215 0           return $type;
216             }
217              
218             1;
219              
220             __END__