File Coverage

blib/lib/IO/K8s.pm
Criterion Covered Total %
statement 170 215 79.0
branch 78 126 61.9
condition 26 54 48.1
subroutine 19 23 82.6
pod 9 13 69.2
total 302 431 70.0


line stmt bran cond sub pod time code
1             package IO::K8s;
2             # ABSTRACT: Objects representing things found in the Kubernetes API
3              
4 18     18   3175059 use v5.10;
  18         68  
5 18     18   11875 use Moo;
  18         156782  
  18         126  
6 18     18   43125 use Module::Runtime qw(require_module);
  18         35822  
  18         117  
7 18     18   9368 use JSON::MaybeXS;
  18         235750  
  18         1518  
8 18     18   169 use Scalar::Util ();
  18         29  
  18         426  
9 18     18   9370 use namespace::clean;
  18         292612  
  18         159  
10              
11             our $VERSION = '1.008';
12              
13             # Track which classes we've auto-generated
14             my %_autogen_cache;
15              
16             # Default resource map - maps short names to class paths relative to IO::K8s
17             my %DEFAULT_RESOURCE_MAP = (
18             # Core API resources
19             Binding => 'Api::Core::V1::Binding',
20             ComponentStatus => 'Api::Core::V1::ComponentStatus',
21             ConfigMap => 'Api::Core::V1::ConfigMap',
22             Endpoints => 'Api::Core::V1::Endpoints',
23             Event => 'Api::Core::V1::Event',
24             LimitRange => 'Api::Core::V1::LimitRange',
25             Namespace => 'Api::Core::V1::Namespace',
26             Node => 'Api::Core::V1::Node',
27             PersistentVolume => 'Api::Core::V1::PersistentVolume',
28             PersistentVolumeClaim => 'Api::Core::V1::PersistentVolumeClaim',
29             Pod => 'Api::Core::V1::Pod',
30             PodTemplate => 'Api::Core::V1::PodTemplate',
31             ReplicationController => 'Api::Core::V1::ReplicationController',
32             ResourceQuota => 'Api::Core::V1::ResourceQuota',
33             Secret => 'Api::Core::V1::Secret',
34             Service => 'Api::Core::V1::Service',
35             ServiceAccount => 'Api::Core::V1::ServiceAccount',
36             # Apps
37             ControllerRevision => 'Api::Apps::V1::ControllerRevision',
38             DaemonSet => 'Api::Apps::V1::DaemonSet',
39             Deployment => 'Api::Apps::V1::Deployment',
40             ReplicaSet => 'Api::Apps::V1::ReplicaSet',
41             StatefulSet => 'Api::Apps::V1::StatefulSet',
42             # Batch
43             CronJob => 'Api::Batch::V1::CronJob',
44             Job => 'Api::Batch::V1::Job',
45             # Networking
46             Ingress => 'Api::Networking::V1::Ingress',
47             IngressClass => 'Api::Networking::V1::IngressClass',
48             NetworkPolicy => 'Api::Networking::V1::NetworkPolicy',
49             # Storage
50             CSIDriver => 'Api::Storage::V1::CSIDriver',
51             CSINode => 'Api::Storage::V1::CSINode',
52             CSIStorageCapacity => 'Api::Storage::V1::CSIStorageCapacity',
53             StorageClass => 'Api::Storage::V1::StorageClass',
54             VolumeAttachment => 'Api::Storage::V1::VolumeAttachment',
55             # Authorization
56             LocalSubjectAccessReview => 'Api::Authorization::V1::LocalSubjectAccessReview',
57             SelfSubjectAccessReview => 'Api::Authorization::V1::SelfSubjectAccessReview',
58             SelfSubjectRulesReview => 'Api::Authorization::V1::SelfSubjectRulesReview',
59             SubjectAccessReview => 'Api::Authorization::V1::SubjectAccessReview',
60             # Authentication
61             SelfSubjectReview => 'Api::Authentication::V1::SelfSubjectReview',
62             TokenRequest => 'Api::Authentication::V1::TokenRequest',
63             TokenReview => 'Api::Authentication::V1::TokenReview',
64             # RBAC
65             ClusterRole => 'Api::Rbac::V1::ClusterRole',
66             ClusterRoleBinding => 'Api::Rbac::V1::ClusterRoleBinding',
67             Role => 'Api::Rbac::V1::Role',
68             RoleBinding => 'Api::Rbac::V1::RoleBinding',
69             # Policy
70             Eviction => 'Api::Policy::V1::Eviction',
71             PodDisruptionBudget => 'Api::Policy::V1::PodDisruptionBudget',
72             # Autoscaling
73             HorizontalPodAutoscaler => 'Api::Autoscaling::V2::HorizontalPodAutoscaler',
74             Scale => 'Api::Autoscaling::V1::Scale',
75             # Certificates
76             CertificateSigningRequest => 'Api::Certificates::V1::CertificateSigningRequest',
77             # Coordination
78             Lease => 'Api::Coordination::V1::Lease',
79             # Discovery
80             EndpointSlice => 'Api::Discovery::V1::EndpointSlice',
81             # Scheduling
82             PriorityClass => 'Api::Scheduling::V1::PriorityClass',
83             # Node
84             RuntimeClass => 'Api::Node::V1::RuntimeClass',
85             # Flowcontrol
86             FlowSchema => 'Api::Flowcontrol::V1::FlowSchema',
87             PriorityLevelConfiguration => 'Api::Flowcontrol::V1::PriorityLevelConfiguration',
88             # Admissionregistration
89             MutatingWebhookConfiguration => 'Api::Admissionregistration::V1::MutatingWebhookConfiguration',
90             ValidatingAdmissionPolicy => 'Api::Admissionregistration::V1::ValidatingAdmissionPolicy',
91             ValidatingAdmissionPolicyBinding => 'Api::Admissionregistration::V1::ValidatingAdmissionPolicyBinding',
92             ValidatingWebhookConfiguration => 'Api::Admissionregistration::V1::ValidatingWebhookConfiguration',
93             # Extension APIs (different base paths)
94             CustomResourceDefinition => 'ApiextensionsApiserver::Pkg::Apis::Apiextensions::V1::CustomResourceDefinition',
95             APIService => 'KubeAggregator::Pkg::Apis::Apiregistration::V1::APIService',
96             );
97              
98             has json => (is => 'ro', default => sub {
99             return JSON::MaybeXS->new(utf8 => 1, canonical => 1);
100             });
101              
102             # Resource map - can be customized per instance
103             # Returns a copy so add() can safely mutate without affecting other instances
104             has resource_map => (
105             is => 'ro',
106             lazy => 1,
107             default => sub { +{ %DEFAULT_RESOURCE_MAP } },
108             );
109              
110             # OpenAPI spec for auto-generating unknown types
111             has openapi_spec => (
112             is => 'ro',
113             predicate => 1,
114             );
115              
116             # External resource map providers to merge at construction time
117             # e.g. with => ['IO::K8s::Cilium'] or with => [IO::K8s::Cilium->new]
118             has with => (
119             is => 'ro',
120             default => sub { [] },
121             );
122              
123             # User namespaces to search for pre-built classes (checked before IO::K8s::)
124             # e.g. ['MyProject::K8s'] will look for MyProject::K8s::HelmChart before IO::K8s::...
125             has class_namespaces => (
126             is => 'ro',
127             default => sub { [] },
128             );
129              
130             # Internal: unique autogen namespace for this instance (isolated, collision-free)
131             has _autogen_namespace => (
132             is => 'ro',
133             lazy => 1,
134             default => sub {
135             my $self = shift;
136             # Create unique identifier based on object address
137             my $id = sprintf('%x', 0 + $self);
138             return "IO::K8s::_AUTOGEN_$id";
139             },
140             );
141              
142             # Class method to get default resource map
143 0     0 0 0 sub default_resource_map { \%DEFAULT_RESOURCE_MAP }
144              
145             sub BUILD {
146 60     60 0 2395 my ($self) = @_;
147 60 100       130 $self->add(@{$self->with}) if @{$self->with};
  33         169  
  60         631  
148             }
149              
150             # Merge external resource maps into this instance
151             # Accepts: class names, objects with resource_map(), or plain hashrefs
152             sub add {
153 48     48 1 2628 my ($self, @providers) = @_;
154 48         1243 my $map = $self->resource_map;
155              
156 48         214 for my $provider (@providers) {
157 49         118 my $ext_map;
158 49 100       223 if (ref $provider eq 'HASH') {
159 5         10 $ext_map = $provider;
160             } else {
161 44 100       142 my $obj = ref $provider ? $provider : do {
162 34         220 require_module($provider); $provider->new;
  34         1773  
163             };
164 44         1129 $ext_map = $obj->resource_map;
165             }
166              
167 49         311 for my $kind (keys %$ext_map) {
168 299         1395 my $class_path = $ext_map->{$kind};
169              
170             # Resolve full class name for api_version lookup
171 299 100       929 my $full_class = $class_path =~ /^\+/
172             ? substr($class_path, 1) : "IO::K8s::$class_path";
173              
174             # Try to load the class and get its api_version
175 299         501 my $api_version;
176 299 50 33     595 if (_class_exists($full_class) && $full_class->can('api_version')) {
177 299         1198 $api_version = $full_class->api_version;
178             }
179              
180 299 100       777 if (exists $map->{$kind}) {
181             # COLLISION: short name already taken
182             # Ensure the original entry also has a domain-qualified key
183 17         36 my $orig_path = $map->{$kind};
184 17 50       78 my $orig_class = $orig_path =~ /^\+/
185             ? substr($orig_path, 1) : "IO::K8s::$orig_path";
186 17 50 33     44 if (_class_exists($orig_class) && $orig_class->can('api_version')) {
187 17         80 my $orig_av = $orig_class->api_version;
188 17 100 66     107 if ($orig_av && !exists $map->{"$orig_av/$kind"}) {
189 14         55 $map->{"$orig_av/$kind"} = $orig_path;
190             }
191             }
192             # New entry: domain-qualified only (no short name)
193 17 50       45 if ($api_version) {
194 17         89 $map->{"$api_version/$kind"} = $class_path;
195             }
196             } else {
197             # No collision: register short name
198 282         640 $map->{$kind} = $class_path;
199             # Also register domain-qualified
200 282 50       665 if ($api_version) {
201 282         1249 $map->{"$api_version/$kind"} = $class_path;
202             }
203             }
204             }
205             }
206 48         353 return $self;
207             }
208              
209             # Expand short class name to full class path
210             # Supports:
211             # 'Pod' -> lookup in resource_map -> IO::K8s::Api::Core::V1::Pod
212             # 'Api::Core::V1::Pod' -> IO::K8s::Api::Core::V1::Pod
213             # 'IO::K8s::...' -> returned as-is
214             # '+MyApp::K8s::Resource' -> MyApp::K8s::Resource (+ prefix = full class name)
215             #
216             # Search order:
217             # 1. User's class_namespaces (if class exists)
218             # 2. IO::K8s built-in (resource_map or relative path)
219             # 3. Auto-generate from openapi_spec (if available)
220             sub expand_class {
221 1894     1894 0 112008 my ($self, $class, $api_version) = @_;
222              
223             # +FullClassName - strip + and use as-is
224 1894 50       7004 return substr($class, 1) if $class =~ /^\+/;
225              
226             # Already a full IO::K8s class name - return as-is
227 1894 100       7779 return $class if $class =~ /^IO::K8s::/;
228              
229             # Already a loaded class (e.g. CRD class passed by ref) - return as-is
230 405 100       1317 return $class if _class_exists($class);
231              
232 394 50       17785 my $map = ref($self) ? $self->resource_map : \%DEFAULT_RESOURCE_MAP;
233              
234             # Domain-qualified string: 'cilium.io/v2/NetworkPolicy'
235 394 100       5102 if ($class =~ m{/}) {
236 71 100       327 if (my $mapped = $map->{$class}) {
237 70 100       672 return $mapped =~ /^\+/ ? substr($mapped, 1) : "IO::K8s::$mapped";
238             }
239 1         6 return undef;
240             }
241              
242             # Short name + api_version disambiguation: 'NetworkPolicy' + 'cilium.io/v2'
243 323 100       1172 if ($api_version) {
244 91         265 my $qualified = "$api_version/$class";
245 91 100       446 if (my $mapped = $map->{$qualified}) {
246 29 100       194 return $mapped =~ /^\+/ ? substr($mapped, 1) : "IO::K8s::$mapped";
247             }
248             }
249              
250             # Short name like "Pod" - look up in resource_map
251 294 50       1450 if (my $mapped = $map->{$class}) {
252             # Mapped value with + prefix = full class name
253 294 100       1148 return substr($mapped, 1) if $mapped =~ /^\+/;
254              
255 281         615 my $rel_path = $mapped;
256              
257             # 1. Check user's class_namespaces first
258 281 50       813 if (ref($self)) {
259 281         539 for my $ns (@{$self->class_namespaces}) {
  281         1448  
260 1         2 my $user_class = "${ns}::${rel_path}";
261 1 50       3 return $user_class if _class_exists($user_class);
262             }
263             }
264              
265             # 2. Check IO::K8s built-in
266 280         880 my $builtin_class = 'IO::K8s::' . $rel_path;
267 280 50       768 return $builtin_class if _class_exists($builtin_class);
268              
269             # 3. Try auto-generation if we have openapi_spec
270 0 0 0     0 if (ref($self) && $self->has_openapi_spec) {
271 0         0 my $autogen = $self->_autogen_class_for($class);
272 0 0       0 return $autogen if $autogen;
273             }
274              
275             # Fall back to IO::K8s:: path (might not exist, but let load_class handle error)
276 0         0 return $builtin_class;
277             }
278              
279             # Not in resource_map - might be a CRD or relative path
280             # 1. Check user's class_namespaces
281 0 0       0 if (ref($self)) {
282 0         0 for my $ns (@{$self->class_namespaces}) {
  0         0  
283 0         0 my $user_class = "${ns}::${class}";
284 0 0       0 return $user_class if _class_exists($user_class);
285             }
286             }
287              
288             # 2. Check IO::K8s relative path
289 0         0 my $builtin_class = 'IO::K8s::' . $class;
290 0 0       0 return $builtin_class if _class_exists($builtin_class);
291              
292             # 3. Try auto-generation for unknown types
293 0 0 0     0 if (ref($self) && $self->has_openapi_spec) {
294 0         0 my $autogen = $self->_autogen_class_for($class);
295 0 0       0 return $autogen if $autogen;
296             }
297              
298             # Fall back
299 0         0 return $builtin_class;
300             }
301              
302             # Check if a class exists (is loaded or can be loaded)
303             sub _class_exists {
304 1002     1002   2235 my ($class) = @_;
305             # Check if already loaded
306 1002 100       13535 return 1 if $class->can('new');
307             # Try to load it
308 486         1084 eval { require_module($class) };
  486         2187  
309 486         109275 return !$@;
310             }
311              
312             # Auto-generate a class from OpenAPI spec for unknown type
313             sub _autogen_class_for {
314 0     0   0 my ($self, $kind) = @_;
315              
316 0 0       0 return unless $self->has_openapi_spec;
317              
318 0         0 my $spec = $self->openapi_spec;
319 0   0     0 my $defs = $spec->{definitions} // {};
320              
321             # Find the definition for this kind
322 0         0 my $def_name = $self->_find_definition_for_kind($kind, $defs);
323 0 0       0 return unless $def_name;
324              
325             # Check cache first
326 0         0 my $cache_key = $self->_autogen_namespace . '::' . $def_name;
327 0 0       0 return $_autogen_cache{$cache_key} if $_autogen_cache{$cache_key};
328              
329             # Generate the class
330 0         0 require IO::K8s::AutoGen;
331             my $class = IO::K8s::AutoGen::get_or_generate(
332             $def_name,
333 0         0 $defs->{$def_name},
334             $defs,
335             $self->_autogen_namespace,
336             );
337              
338 0         0 $_autogen_cache{$cache_key} = $class;
339 0         0 return $class;
340             }
341              
342             # Find OpenAPI definition name for a given kind
343             sub _find_definition_for_kind {
344 0     0   0 my ($self, $kind, $defs) = @_;
345              
346             # Direct match by kind name at end of definition
347 0         0 for my $def_name (keys %$defs) {
348 0         0 my $def = $defs->{$def_name};
349             # Check x-kubernetes-group-version-kind
350 0 0       0 if (my $gvk_list = $def->{'x-kubernetes-group-version-kind'}) {
351 0         0 for my $gvk (@$gvk_list) {
352 0 0       0 if ($gvk->{kind} eq $kind) {
353 0         0 return $def_name;
354             }
355             }
356             }
357             # Also check if definition name ends with the kind
358 0 0       0 if ($def_name =~ /\.\Q$kind\E$/) {
359 0         0 return $def_name;
360             }
361             }
362              
363 0         0 return undef;
364             }
365              
366             sub load_class {
367 1564     1564 0 3468 my ($self, $class) = @_;
368 1564         5868 require_module $class;
369             }
370              
371             sub json_to_object {
372 9     9 1 19450 my ($self, $class_or_json, $json) = @_;
373              
374             # If only one argument, auto-detect class from kind
375 9 100       36 if (!defined $json) {
376 7         26 return $self->inflate($class_or_json);
377             }
378              
379             # Two arguments: class and JSON
380 2         9 my $class = $self->expand_class($class_or_json);
381 2         352 my $struct = $self->json->decode($json);
382 2         11 return $self->struct_to_object($class, $struct);
383             }
384              
385             sub struct_to_object {
386 1502     1502 1 163800 my ($self, $class_or_struct, $params) = @_;
387              
388             # If only one argument (a hashref), auto-detect class from kind
389 1502 50 33     5009 if (!defined $params && ref($class_or_struct) eq 'HASH') {
390 0         0 return $self->inflate($class_or_struct);
391             }
392              
393             # Two arguments: class and params
394 1502         4743 my $class = $self->expand_class($class_or_struct);
395              
396             # Already an object of the right class — pass through as-is
397 1502 100 66     4995 return $params if Scalar::Util::blessed($params) && $params->isa($class);
398              
399 1484         4412 $self->load_class($class);
400 1484         45667 my $inflated = $self->_inflate_struct($class, $params);
401 1478         42383 return $class->new(%$inflated);
402             }
403              
404             sub inflate {
405 80     80 1 2729176 my ($self, $data) = @_;
406              
407             # Accept both JSON string and hashref
408 80 100       1001 my $struct = ref($data) eq 'HASH' ? $data : $self->json->decode($data);
409              
410             my $kind = $struct->{kind}
411 80 50       457 or die "Cannot inflate: missing 'kind' field in data";
412 80         252 my $api_version = $struct->{apiVersion};
413              
414 80         444 my $class = $self->expand_class($kind, $api_version);
415 80         448 $self->load_class($class);
416 80         3156 my $inflated = $self->_inflate_struct($class, $struct);
417 77         2264 return $class->new(%$inflated);
418             }
419              
420             sub new_object {
421 155     155 1 1278553 my ($self, $short_class, @args) = @_;
422              
423             # Support:
424             # ->new_object('Pod', { ... })
425             # ->new_object('Pod', foo => 'bar')
426             # ->new_object('Pod', { ... }, 'cilium.io/v2') # with api_version
427 155         405 my ($params, $api_version);
428 155 100 100     1992 if (@args >= 2 && ref($args[0]) eq 'HASH' && !ref($args[1])) {
    100 66        
      66        
429 4         13 ($params, $api_version) = @args;
430             } elsif (@args == 1 && ref($args[0]) eq 'HASH') {
431 126         325 $params = $args[0];
432             } else {
433 25         108 $params = { @args };
434             }
435              
436 155         983 my $class = $self->expand_class($short_class, $api_version);
437 155         739 return $self->struct_to_object($class, $params);
438             }
439              
440             sub _inflate_struct {
441 1564     1564   4575 my ($self, $class, $params) = @_;
442              
443             # Blessed objects should be caught by struct_to_object before reaching
444             # here. If one does slip through (defensive), extract its data rather
445             # than silently returning {} which would create an empty object.
446 1564 50       4724 if (Scalar::Util::blessed($params)) {
447 0 0       0 return $params->TO_JSON if $params->can('TO_JSON');
448 0         0 return {};
449             }
450              
451 1564 50       5145 return {} unless ref $params eq 'HASH';
452              
453             # Opaque fields that should be passed through as-is (complex JSON structures)
454 1564         3680 my %opaque_fields = map { $_ => 1 } qw(fieldsV1 rawExtension raw);
  4692         13161  
455              
456             # Get attribute info from the registry (keyed by Perl attr name)
457 1564         11684 my $attr_info = $class->_k8s_attr_info;
458              
459             # Build reverse map: JSON key → Perl attr name (for sanitized names)
460 1564         2931 my %json_to_perl;
461 1564         11896 for my $perl_name (keys %$attr_info) {
462 15592   33     51291 my $json_key = $attr_info->{$perl_name}{json_key} // $perl_name;
463 15592         34165 $json_to_perl{$json_key} = $perl_name;
464             }
465              
466 1564         4984 my %args;
467              
468 1564         4797 for my $attr (keys %$params) {
469 3496         613056 my $value = $params->{$attr};
470 3496 50       8006 next unless defined $value;
471              
472             # Pass through opaque fields without type coercion
473 3496 50       7930 if ($opaque_fields{$attr}) {
474 0         0 $args{$attr} = $value;
475 0         0 next;
476             }
477              
478             # Look up by Perl attr name (handles sanitized JSON keys like x-kubernetes-*)
479 3496   66     8670 my $perl_name = $json_to_perl{$attr} // $attr;
480 3496   100     8495 my $info = $attr_info->{$perl_name} // {};
481              
482 3496 100       14008 if ($info->{is_array_of_objects}) {
    100          
    100          
    100          
483 299         877 my $inner_class = $info->{class};
484 299         874 $args{$attr} = [ map { $self->struct_to_object($inner_class, $_) } @$value ];
  419         204923  
485             } elsif ($info->{is_hash_of_objects}) {
486 2         6 my $inner_class = $info->{class};
487 2         6 $args{$attr} = { map { $_ => $self->struct_to_object($inner_class, $value->{$_}) } keys %$value };
  3         9  
488             } elsif ($info->{is_object}) {
489 892         3412 $args{$attr} = $self->struct_to_object($info->{class}, $value);
490             } elsif ($info->{is_bool}) {
491 79 100 66     698 $args{$attr} = (ref($value) eq '' && lc($value) eq 'true') || $value ? 1 : 0;
492             } else {
493 2224         6536 $args{$attr} = $value;
494             }
495             }
496              
497 1555         643732 return \%args;
498             }
499              
500             sub object_to_struct {
501 0     0 1 0 my ($self, $object) = @_;
502 0         0 return $object->TO_JSON;
503             }
504              
505             sub object_to_json {
506 24     24 1 95976 my ($self, $object) = @_;
507 24         187 return $object->to_json;
508             }
509              
510             sub load {
511 7     7 1 17129 my ($self, $file) = @_;
512              
513 7         8595 require IO::K8s::Manifest;
514              
515             # Set k8s instance for DSL functions
516 7         34 local $IO::K8s::Manifest::_k8s_instance = $self;
517              
518 7         35 return IO::K8s::Manifest->_load_file($file, $self);
519             }
520              
521             sub load_yaml {
522 6     6 1 57896 my ($self, $file_or_string, %opts) = @_;
523              
524 6         808 require YAML::PP;
525              
526 6         98797 my $content;
527 6 100 66     90 if ($file_or_string !~ /\n/ && -f $file_or_string) {
528             # It's a file path
529 1 50       46 open my $fh, '<', $file_or_string or die "Cannot open $file_or_string: $!";
530 1         4 $content = do { local $/; <$fh> };
  1         6  
  1         40  
531 1         15 close $fh;
532             } else {
533             # It's YAML content
534 5         16 $content = $file_or_string;
535             }
536              
537             # Parse multi-document YAML (Load returns all docs in list context)
538 6         72 my @docs = YAML::PP::Load($content);
539              
540 6         169197 my $collect_errors = $opts{collect_errors};
541 6         26 my @objects;
542             my @errors;
543              
544             # Inflate each document - this validates types!
545 6         36 for my $i (0 .. $#docs) {
546 11         873 my $doc = $docs[$i];
547 11 50 33     123 next unless $doc && ref($doc) eq 'HASH';
548              
549 11 100       44 if ($collect_errors) {
550 2         6 eval { push @objects, $self->inflate($doc) };
  2         14  
551 2 50       707 if ($@) {
552 2   33     22 my $name = $doc->{metadata}{name} // "document $i";
553 2   50     7 my $kind = $doc->{kind} // 'unknown';
554 2         14 push @errors, "$kind/$name: $@";
555             }
556             } else {
557 9         51 push @objects, $self->inflate($doc);
558             }
559             }
560              
561             # In collect_errors mode, return (objects, errors) in list context
562 5 100       8532 if ($collect_errors) {
563 1         18 return (\@objects, \@errors);
564             }
565              
566 4         67 return \@objects;
567             }
568              
569             1;
570              
571             __END__