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.008';
4 28     28   19624 use v5.10;
  28         116  
5 28     28   167 use Moo::Role;
  28         51  
  28         276  
6 28     28   21203 use JSON::MaybeXS ();
  28         97764  
  28         822  
7 28     28   158 use Scalar::Util qw(blessed);
  28         57  
  28         5727  
8              
9             has json => (
10             is => 'ro',
11             lazy => 1,
12             builder => '_build_json',
13             );
14              
15             sub _build_json {
16 35     35   706 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 2894     2894   18101 my ($class) = @_;
22 2894 100       7429 $class = ref($class) if ref($class);
23 2894   100     11201 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   2689 my ($self) = @_;
29 1320   33     4116 my $class = ref($self) || $self;
30 28     28   204 no strict 'refs';
  28         58  
  28         43819  
31 1320         2174 return \@{"${class}::_k8s_attributes"};
  1320         9383  
32             }
33              
34             sub TO_JSON {
35 1320     1320 0 369238 my $self = shift;
36 1320         2215 my %data;
37 1320         3830 my $attrs = $self->_k8s_attributes;
38 1320         3276 my $info = _k8s_attr_info($self);
39              
40             # Add apiVersion, kind, and metadata for APIObjects (those with the role)
41 1320 100 66     8580 if ($self->can('_is_resource') && $self->_is_resource) {
42 203 50       898 $data{apiVersion} = $self->api_version if $self->api_version;
43 203 50       998 $data{kind} = $self->kind if $self->kind;
44             # metadata comes from the Role, not from k8s DSL
45 203 100 66     7186 if ($self->can('metadata') && $self->metadata) {
46 189         5787 $data{metadata} = $self->metadata->TO_JSON;
47             }
48             }
49              
50 1320         3449 for my $attr (@$attrs) {
51 13518         295191 my $value = $self->$attr;
52 13518 100       101409 next unless defined $value;
53              
54 2785   50     8163 my $attr_info = $info->{$attr} // {};
55             # Use json_key for output when attr name differs from JSON field name
56 2785   33     10146 my $key = $attr_info->{json_key} // $attr;
57              
58 2785 100 66     21262 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         673 $data{$key} = int($value);
62             } elsif ($attr_info->{is_int_or_string}) {
63 56 100       469 $data{$key} = ($value =~ /\A-?\d+\z/) ? int($value) : $value;
64             } elsif ($attr_info->{is_object} && blessed($value) && $value->can('TO_JSON')) {
65 646         1782 $data{$key} = $value->TO_JSON;
66             } elsif ($attr_info->{is_array_of_objects}) {
67 217         671 $data{$key} = [ map { $_->TO_JSON } @$value ];
  311         1283  
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         333 $data{$key} = $value;
74             } elsif (ref $value eq 'HASH') {
75 361         1507 $data{$key} = $value;
76             } else {
77 1158         4007 $data{$key} = $value;
78             }
79             }
80 1320         19954 return \%data;
81             }
82              
83             sub to_json {
84 36     36 0 18334 my $self = shift;
85 36         1388 return $self->json->encode($self->TO_JSON);
86             }
87              
88             sub TO_YAML {
89 18     18 0 2442 my $self = shift;
90 18         8425 require YAML::PP;
91 18         705400 my $yp = YAML::PP->new(schema => [qw/JSON/], boolean => 'JSON::PP');
92 18         25184 return $yp->dump_string($self->TO_JSON);
93             }
94              
95             sub to_yaml {
96 17     17 0 23624 my $self = shift;
97 17         102 return $self->TO_YAML;
98             }
99              
100             sub FROM_HASH {
101 1     1 0 4770 my ($class, $hash) = @_;
102 1         40 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 1006 my ($class, $schema) = @_;
118 1 50       4 $class = ref($class) if ref($class);
119              
120 1   50     81 my $local_attrs = $IO::K8s::Resource::_attr_registry{$class} // {};
121 1   50     8 my $schema_props = $schema->{properties} // {};
122              
123             # Build json_key -> attr_name mapping for lookup
124 1         2 my %json_to_attr;
125 1         4 for my $attr (keys %$local_attrs) {
126 4   33     10 my $jk = $local_attrs->{$attr}{json_key} // $attr;
127 4         10 $json_to_attr{$jk} = $attr;
128             }
129              
130 1         6 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       7 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         8 my $local_type = _describe_local_type($local_attrs->{$attr});
148 4         7 my $schema_type = _describe_schema_type($schema_props->{$prop});
149 4 50       9 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         2 for my $attr (keys %$local_attrs) {
161 4   33     11 my $jk = $local_attrs->{$attr}{json_key} // $attr;
162 4 50       7 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   5 my ($info) = @_;
172 4 50       7 return 'string' if $info->{is_str};
173 4 50       6 return 'integer' if $info->{is_int};
174 4 50       6 return 'int-or-string' if $info->{is_int_or_string};
175 4 100       8 return 'quantity' if $info->{is_quantity};
176 3 50       5 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       8 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     4 my $format = $prop->{format} // '';
197 1 50       4 return 'int-or-string' if $format eq 'int-or-string';
198 1 50       3 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__