| line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
|
1
|
|
|
|
|
|
|
package IO::K8s::AutoGen; |
|
2
|
|
|
|
|
|
|
# ABSTRACT: Dynamically generate IO::K8s classes from OpenAPI schema |
|
3
|
|
|
|
|
|
|
our $VERSION = '1.008'; |
|
4
|
2
|
|
|
2
|
|
2212
|
use v5.10; |
|
|
2
|
|
|
|
|
8
|
|
|
5
|
2
|
|
|
2
|
|
10
|
use strict; |
|
|
2
|
|
|
|
|
4
|
|
|
|
2
|
|
|
|
|
48
|
|
|
6
|
2
|
|
|
2
|
|
9
|
use warnings; |
|
|
2
|
|
|
|
|
4
|
|
|
|
2
|
|
|
|
|
140
|
|
|
7
|
2
|
|
|
2
|
|
10
|
use Carp qw(croak); |
|
|
2
|
|
|
|
|
4
|
|
|
|
2
|
|
|
|
|
146
|
|
|
8
|
2
|
|
|
2
|
|
11
|
use Package::Stash; |
|
|
2
|
|
|
|
|
6
|
|
|
|
2
|
|
|
|
|
892
|
|
|
9
|
|
|
|
|
|
|
|
|
10
|
|
|
|
|
|
|
# Cache of generated classes |
|
11
|
|
|
|
|
|
|
my %_generated; |
|
12
|
|
|
|
|
|
|
|
|
13
|
|
|
|
|
|
|
# Default namespace for auto-generated classes |
|
14
|
|
|
|
|
|
|
our $DEFAULT_NAMESPACE = 'IO::K8s::_AUTOGEN'; |
|
15
|
|
|
|
|
|
|
|
|
16
|
|
|
|
|
|
|
# Convert OpenAPI definition name to Perl class name |
|
17
|
|
|
|
|
|
|
# With namespace 'MyProject::K8s': |
|
18
|
|
|
|
|
|
|
# helm.cattle.io.v1.HelmChart -> MyProject::K8s::helm::cattle::io::v1::HelmChart |
|
19
|
|
|
|
|
|
|
sub def_to_class { |
|
20
|
7
|
|
|
7
|
1
|
261439
|
my ($def_name, $namespace) = @_; |
|
21
|
7
|
|
66
|
|
|
23
|
$namespace //= $DEFAULT_NAMESPACE; |
|
22
|
7
|
|
|
|
|
13
|
my $class = $def_name; |
|
23
|
7
|
|
|
|
|
44
|
$class =~ s/\./::/g; |
|
24
|
7
|
|
|
|
|
28
|
return "${namespace}::$class"; |
|
25
|
|
|
|
|
|
|
} |
|
26
|
|
|
|
|
|
|
|
|
27
|
|
|
|
|
|
|
# Convert Perl class name back to OpenAPI definition name |
|
28
|
|
|
|
|
|
|
sub class_to_def { |
|
29
|
1
|
|
|
1
|
1
|
3
|
my ($class) = @_; |
|
30
|
|
|
|
|
|
|
# Strip any _AUTOGEN namespace prefix |
|
31
|
1
|
|
|
|
|
3
|
$class =~ s/^IO::K8s::_AUTOGEN_[^:]+:://; |
|
32
|
1
|
|
|
|
|
3
|
$class =~ s/^IO::K8s::_AUTOGEN:://; |
|
33
|
1
|
|
|
|
|
4
|
$class =~ s/::/./g; |
|
34
|
1
|
|
|
|
|
3
|
return $class; |
|
35
|
|
|
|
|
|
|
} |
|
36
|
|
|
|
|
|
|
|
|
37
|
|
|
|
|
|
|
# Check if a class was auto-generated |
|
38
|
|
|
|
|
|
|
sub is_autogen { |
|
39
|
3
|
|
|
3
|
1
|
8
|
my ($class) = @_; |
|
40
|
3
|
|
|
|
|
16
|
return $class =~ /^IO::K8s::_AUTOGEN/; |
|
41
|
|
|
|
|
|
|
} |
|
42
|
|
|
|
|
|
|
|
|
43
|
|
|
|
|
|
|
# Get or generate a class from schema |
|
44
|
|
|
|
|
|
|
# Options hash can include: |
|
45
|
|
|
|
|
|
|
# api_version => 'stable.example.com/v1' |
|
46
|
|
|
|
|
|
|
# kind => 'StaticWebSite' |
|
47
|
|
|
|
|
|
|
# resource_plural => 'staticwebsites' |
|
48
|
|
|
|
|
|
|
# is_namespaced => 1 |
|
49
|
|
|
|
|
|
|
sub get_or_generate { |
|
50
|
5
|
|
|
5
|
1
|
133
|
my ($def_name, $schema, $all_defs, $namespace, %opts) = @_; |
|
51
|
|
|
|
|
|
|
|
|
52
|
5
|
|
|
|
|
21
|
my $class = def_to_class($def_name, $namespace); |
|
53
|
5
|
50
|
|
|
|
35
|
return $class if $_generated{$class}; |
|
54
|
|
|
|
|
|
|
|
|
55
|
5
|
|
|
|
|
24
|
_generate_class($class, $def_name, $schema, $all_defs, $namespace, %opts); |
|
56
|
5
|
|
|
|
|
31
|
return $class; |
|
57
|
|
|
|
|
|
|
} |
|
58
|
|
|
|
|
|
|
|
|
59
|
|
|
|
|
|
|
# Generate a class from OpenAPI schema using IO::K8s::Resource |
|
60
|
|
|
|
|
|
|
sub _generate_class { |
|
61
|
5
|
|
|
5
|
|
16
|
my ($class, $def_name, $schema, $all_defs, $namespace, %opts) = @_; |
|
62
|
|
|
|
|
|
|
|
|
63
|
5
|
50
|
|
|
|
19
|
return if $_generated{$class}; |
|
64
|
5
|
|
|
|
|
13
|
$_generated{$class} = 1; # Mark early to prevent recursion |
|
65
|
|
|
|
|
|
|
|
|
66
|
|
|
|
|
|
|
# Ensure parent packages exist |
|
67
|
5
|
|
|
|
|
23
|
_ensure_package_exists($class); |
|
68
|
|
|
|
|
|
|
|
|
69
|
|
|
|
|
|
|
# Set up the class using IO::K8s::Resource's shared setup method |
|
70
|
|
|
|
|
|
|
{ |
|
71
|
2
|
|
|
2
|
|
14
|
no strict 'refs'; |
|
|
2
|
|
|
|
|
4
|
|
|
|
2
|
|
|
|
|
2475
|
|
|
|
5
|
|
|
|
|
10
|
|
|
72
|
5
|
|
|
|
|
11
|
@{"${class}::ISA"} = (); |
|
|
5
|
|
|
|
|
92
|
|
|
73
|
|
|
|
|
|
|
} |
|
74
|
|
|
|
|
|
|
|
|
75
|
5
|
|
|
|
|
655
|
require IO::K8s::Resource; |
|
76
|
5
|
|
|
|
|
51
|
IO::K8s::Resource->_setup_class($class); |
|
77
|
|
|
|
|
|
|
|
|
78
|
|
|
|
|
|
|
# Get the k8s function for this class |
|
79
|
5
|
50
|
|
|
|
55
|
my $k8s = $class->can('k8s') |
|
80
|
|
|
|
|
|
|
or croak "Failed to set up k8s DSL for $class"; |
|
81
|
|
|
|
|
|
|
|
|
82
|
5
|
|
50
|
|
|
19
|
my $properties = $schema->{properties} // {}; |
|
83
|
|
|
|
|
|
|
|
|
84
|
|
|
|
|
|
|
# Generate attributes using k8s DSL |
|
85
|
|
|
|
|
|
|
# Property names with special characters ($ref, x-kubernetes-*) are |
|
86
|
|
|
|
|
|
|
# automatically sanitized to valid Perl identifiers by _k8s(), with |
|
87
|
|
|
|
|
|
|
# init_arg mapping so constructors still accept the original JSON keys. |
|
88
|
5
|
|
|
|
|
35
|
for my $prop (sort keys %$properties) { |
|
89
|
25
|
|
|
|
|
25296
|
my $prop_schema = $properties->{$prop}; |
|
90
|
25
|
|
|
|
|
108
|
my $type_spec = _schema_to_type_spec($prop_schema, $all_defs, $namespace, $prop); |
|
91
|
25
|
100
|
|
|
|
74
|
next unless defined $type_spec; # Skip unsupported types |
|
92
|
|
|
|
|
|
|
|
|
93
|
24
|
|
|
|
|
69
|
$k8s->($prop, $type_spec); |
|
94
|
|
|
|
|
|
|
} |
|
95
|
|
|
|
|
|
|
|
|
96
|
|
|
|
|
|
|
# Determine api_version/kind from schema or explicit options |
|
97
|
5
|
|
|
|
|
6754
|
my ($api_ver, $kind_val, $res_plural, $is_namespaced); |
|
98
|
|
|
|
|
|
|
|
|
99
|
5
|
100
|
|
|
|
26
|
if (my $gvk = $schema->{'x-kubernetes-group-version-kind'}) { |
|
100
|
1
|
50
|
|
|
|
8
|
my $entry = ref($gvk) eq 'ARRAY' ? $gvk->[0] : $gvk; |
|
101
|
1
|
|
50
|
|
|
6
|
my $group = $entry->{group} // ''; |
|
102
|
1
|
|
50
|
|
|
4
|
my $version = $entry->{version} // ''; |
|
103
|
1
|
|
50
|
|
|
5
|
$kind_val = $entry->{kind} // ''; |
|
104
|
1
|
50
|
|
|
|
6
|
$api_ver = $group ? "$group/$version" : $version; |
|
105
|
|
|
|
|
|
|
} |
|
106
|
|
|
|
|
|
|
|
|
107
|
|
|
|
|
|
|
# Explicit options override schema-derived values |
|
108
|
5
|
100
|
|
|
|
26
|
$api_ver = $opts{api_version} if exists $opts{api_version}; |
|
109
|
5
|
100
|
|
|
|
18
|
$kind_val = $opts{kind} if exists $opts{kind}; |
|
110
|
5
|
100
|
|
|
|
19
|
$res_plural = $opts{resource_plural} if exists $opts{resource_plural}; |
|
111
|
5
|
100
|
|
|
|
20
|
$is_namespaced = $opts{is_namespaced} if exists $opts{is_namespaced}; |
|
112
|
|
|
|
|
|
|
|
|
113
|
|
|
|
|
|
|
# Install class methods if we have api_version/kind |
|
114
|
5
|
100
|
66
|
|
|
45
|
if (defined $api_ver && defined $kind_val) { |
|
115
|
2
|
|
|
|
|
34
|
my $stash = Package::Stash->new($class); |
|
116
|
|
|
|
|
|
|
|
|
117
|
2
|
|
|
7
|
|
45
|
$stash->add_symbol('&api_version', sub { $api_ver }); |
|
|
7
|
|
|
|
|
13243
|
|
|
118
|
2
|
|
|
10
|
|
19
|
$stash->add_symbol('&kind', sub { $kind_val }); |
|
|
10
|
|
|
|
|
5574
|
|
|
119
|
2
|
|
|
2
|
|
14
|
$stash->add_symbol('&resource_plural', sub { $res_plural }); |
|
|
2
|
|
|
|
|
12
|
|
|
120
|
|
|
|
|
|
|
|
|
121
|
|
|
|
|
|
|
# Apply Role::APIObject for metadata, to_yaml, save, etc. |
|
122
|
2
|
|
|
|
|
19
|
require Moo::Role; |
|
123
|
2
|
|
|
|
|
11
|
require IO::K8s::Role::APIObject; |
|
124
|
2
|
|
|
|
|
17
|
Moo::Role->apply_roles_to_package($class, 'IO::K8s::Role::APIObject'); |
|
125
|
|
|
|
|
|
|
|
|
126
|
|
|
|
|
|
|
# Register metadata attribute via k8s DSL so _inflate_struct knows the type |
|
127
|
|
|
|
|
|
|
# (same as IO::K8s::APIObject::import does for hand-written classes) |
|
128
|
2
|
|
|
|
|
3361
|
$k8s->('metadata', 'Meta::V1::ObjectMeta'); |
|
129
|
|
|
|
|
|
|
|
|
130
|
|
|
|
|
|
|
# Apply Namespaced role if requested or schema suggests it |
|
131
|
2
|
100
|
|
|
|
11
|
if ($is_namespaced) { |
|
132
|
1
|
|
|
|
|
8
|
require IO::K8s::Role::Namespaced; |
|
133
|
1
|
|
|
|
|
6
|
Moo::Role->apply_roles_to_package($class, 'IO::K8s::Role::Namespaced'); |
|
134
|
|
|
|
|
|
|
} |
|
135
|
|
|
|
|
|
|
} |
|
136
|
|
|
|
|
|
|
|
|
137
|
5
|
|
|
|
|
1394
|
return $class; |
|
138
|
|
|
|
|
|
|
} |
|
139
|
|
|
|
|
|
|
|
|
140
|
|
|
|
|
|
|
# Opaque type definitions that should be HashRef, not object references |
|
141
|
|
|
|
|
|
|
my %OPAQUE_TYPES = map { $_ => 1 } qw( |
|
142
|
|
|
|
|
|
|
io.k8s.apimachinery.pkg.apis.meta.v1.FieldsV1 |
|
143
|
|
|
|
|
|
|
io.k8s.apimachinery.pkg.runtime.RawExtension |
|
144
|
|
|
|
|
|
|
); |
|
145
|
|
|
|
|
|
|
|
|
146
|
|
|
|
|
|
|
# Convert OpenAPI schema to k8s() type spec |
|
147
|
|
|
|
|
|
|
sub _schema_to_type_spec { |
|
148
|
25
|
|
|
25
|
|
70
|
my ($schema, $all_defs, $namespace, $field_name) = @_; |
|
149
|
|
|
|
|
|
|
|
|
150
|
|
|
|
|
|
|
# Handle $ref |
|
151
|
25
|
100
|
|
|
|
92
|
if (my $ref = $schema->{'$ref'}) { |
|
152
|
5
|
|
|
|
|
33
|
$ref =~ s{^#/definitions/}{}; |
|
153
|
|
|
|
|
|
|
|
|
154
|
|
|
|
|
|
|
# Special apimachinery types - not object references |
|
155
|
5
|
100
|
|
|
|
28
|
if ($ref =~ /intstr\.IntOrString$/) { |
|
156
|
1
|
|
|
|
|
4
|
return 'IntOrStr'; |
|
157
|
|
|
|
|
|
|
} |
|
158
|
4
|
100
|
|
|
|
48
|
if ($ref =~ /resource\.Quantity$/) { |
|
159
|
1
|
|
|
|
|
6
|
return 'Quantity'; |
|
160
|
|
|
|
|
|
|
} |
|
161
|
3
|
100
|
|
|
|
22
|
if ($ref =~ /meta\.v1\.(Micro)?Time$/) { |
|
162
|
2
|
|
|
|
|
8
|
return 'Time'; |
|
163
|
|
|
|
|
|
|
} |
|
164
|
|
|
|
|
|
|
|
|
165
|
|
|
|
|
|
|
# Opaque types should be HashRef, not object references |
|
166
|
1
|
50
|
|
|
|
5
|
if ($OPAQUE_TYPES{$ref}) { |
|
167
|
0
|
|
|
|
|
0
|
return { Str => 1 }; # HashRef |
|
168
|
|
|
|
|
|
|
} |
|
169
|
|
|
|
|
|
|
|
|
170
|
|
|
|
|
|
|
# Generate referenced class if needed |
|
171
|
1
|
50
|
33
|
|
|
8
|
if ($all_defs && $all_defs->{$ref}) { |
|
172
|
0
|
|
|
|
|
0
|
my $ref_class = get_or_generate($ref, $all_defs->{$ref}, $all_defs, $namespace); |
|
173
|
0
|
|
|
|
|
0
|
return "+$ref_class"; # + prefix for full class name |
|
174
|
|
|
|
|
|
|
} |
|
175
|
1
|
|
|
|
|
4
|
return undef; # Can't resolve reference |
|
176
|
|
|
|
|
|
|
} |
|
177
|
|
|
|
|
|
|
|
|
178
|
20
|
|
50
|
|
|
63
|
my $type = $schema->{type} // ''; |
|
179
|
|
|
|
|
|
|
|
|
180
|
20
|
100
|
|
|
|
97
|
if ($type eq 'string') { |
|
|
|
100
|
|
|
|
|
|
|
|
|
50
|
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
|
|
|
50
|
|
|
|
|
|
|
181
|
11
|
|
100
|
|
|
50
|
my $format = $schema->{format} // ''; |
|
182
|
11
|
100
|
|
|
|
42
|
return 'IntOrStr' if $format eq 'int-or-string'; |
|
183
|
10
|
100
|
|
|
|
51
|
return 'Time' if $format eq 'date-time'; |
|
184
|
9
|
|
|
|
|
26
|
return 'Str'; |
|
185
|
|
|
|
|
|
|
} |
|
186
|
|
|
|
|
|
|
elsif ($type eq 'integer') { |
|
187
|
3
|
|
|
|
|
11
|
return 'Int'; |
|
188
|
|
|
|
|
|
|
} |
|
189
|
|
|
|
|
|
|
elsif ($type eq 'number') { |
|
190
|
0
|
|
|
|
|
0
|
return 'Str'; # Treat numbers as strings for now |
|
191
|
|
|
|
|
|
|
} |
|
192
|
|
|
|
|
|
|
elsif ($type eq 'boolean') { |
|
193
|
1
|
|
|
|
|
2
|
return 'Bool'; |
|
194
|
|
|
|
|
|
|
} |
|
195
|
|
|
|
|
|
|
elsif ($type eq 'array') { |
|
196
|
1
|
|
50
|
|
|
4
|
my $items = $schema->{items} // {}; |
|
197
|
1
|
50
|
|
|
|
4
|
if (my $ref = $items->{'$ref'}) { |
|
198
|
0
|
|
|
|
|
0
|
$ref =~ s{^#/definitions/}{}; |
|
199
|
0
|
0
|
0
|
|
|
0
|
if ($all_defs && $all_defs->{$ref}) { |
|
200
|
0
|
|
|
|
|
0
|
my $ref_class = get_or_generate($ref, $all_defs->{$ref}, $all_defs, $namespace); |
|
201
|
0
|
|
|
|
|
0
|
return ["+$ref_class"]; |
|
202
|
|
|
|
|
|
|
} |
|
203
|
0
|
|
|
|
|
0
|
return undef; |
|
204
|
|
|
|
|
|
|
} |
|
205
|
1
|
|
50
|
|
|
3
|
my $item_type = $items->{type} // 'string'; |
|
206
|
1
|
50
|
|
|
|
6
|
return ['Str'] if $item_type eq 'string'; |
|
207
|
0
|
0
|
|
|
|
0
|
return ['Int'] if $item_type eq 'integer'; |
|
208
|
0
|
|
|
|
|
0
|
return ['Str']; # Default |
|
209
|
|
|
|
|
|
|
} |
|
210
|
|
|
|
|
|
|
elsif ($type eq 'object') { |
|
211
|
4
|
100
|
|
|
|
16
|
if (my $addl = $schema->{additionalProperties}) { |
|
212
|
1
|
50
|
|
|
|
3
|
if (my $ref = $addl->{'$ref'}) { |
|
213
|
0
|
|
|
|
|
0
|
$ref =~ s{^#/definitions/}{}; |
|
214
|
0
|
0
|
0
|
|
|
0
|
if ($all_defs && $all_defs->{$ref}) { |
|
215
|
0
|
|
|
|
|
0
|
my $ref_class = get_or_generate($ref, $all_defs->{$ref}, $all_defs, $namespace); |
|
216
|
0
|
|
|
|
|
0
|
return { "+$ref_class" => 1 }; |
|
217
|
|
|
|
|
|
|
} |
|
218
|
0
|
|
|
|
|
0
|
return undef; |
|
219
|
|
|
|
|
|
|
} |
|
220
|
1
|
|
|
|
|
3
|
return { Str => 1 }; # Hash of strings |
|
221
|
|
|
|
|
|
|
} |
|
222
|
3
|
|
|
|
|
16
|
return { Str => 1 }; # Generic object -> hash of strings |
|
223
|
|
|
|
|
|
|
} |
|
224
|
|
|
|
|
|
|
|
|
225
|
|
|
|
|
|
|
# Unknown type |
|
226
|
0
|
|
|
|
|
0
|
return 'Str'; |
|
227
|
|
|
|
|
|
|
} |
|
228
|
|
|
|
|
|
|
|
|
229
|
|
|
|
|
|
|
# Ensure parent packages exist |
|
230
|
|
|
|
|
|
|
sub _ensure_package_exists { |
|
231
|
5
|
|
|
5
|
|
10
|
my ($class) = @_; |
|
232
|
5
|
|
|
|
|
29
|
my @parts = split /::/, $class; |
|
233
|
5
|
|
|
|
|
13
|
pop @parts; # Remove the final class name |
|
234
|
|
|
|
|
|
|
|
|
235
|
5
|
|
|
|
|
12
|
my $current = ''; |
|
236
|
5
|
|
|
|
|
13
|
for my $part (@parts) { |
|
237
|
32
|
100
|
|
|
|
70
|
$current .= '::' if $current; |
|
238
|
32
|
|
|
|
|
48
|
$current .= $part; |
|
239
|
2
|
|
|
2
|
|
16
|
no strict 'refs'; |
|
|
2
|
|
|
|
|
8
|
|
|
|
2
|
|
|
|
|
426
|
|
|
240
|
32
|
100
|
|
|
|
41
|
unless (%{"${current}::"}) { |
|
|
32
|
|
|
|
|
186
|
|
|
241
|
|
|
|
|
|
|
# Create empty package |
|
242
|
22
|
50
|
|
|
|
1464
|
eval "package $current; 1;" or warn "Could not create package $current: $@"; |
|
243
|
|
|
|
|
|
|
} |
|
244
|
|
|
|
|
|
|
} |
|
245
|
|
|
|
|
|
|
} |
|
246
|
|
|
|
|
|
|
|
|
247
|
|
|
|
|
|
|
# Clear generated class cache (mainly for testing) |
|
248
|
|
|
|
|
|
|
sub clear_cache { |
|
249
|
5
|
|
|
5
|
1
|
40608
|
%_generated = (); |
|
250
|
|
|
|
|
|
|
} |
|
251
|
|
|
|
|
|
|
|
|
252
|
|
|
|
|
|
|
# List all generated classes |
|
253
|
|
|
|
|
|
|
sub generated_classes { |
|
254
|
0
|
|
|
0
|
1
|
|
return keys %_generated; |
|
255
|
|
|
|
|
|
|
} |
|
256
|
|
|
|
|
|
|
|
|
257
|
|
|
|
|
|
|
1; |
|
258
|
|
|
|
|
|
|
|
|
259
|
|
|
|
|
|
|
__END__ |