NAME

IO::K8s::Role::SpecBuilder - Role for deep-path spec manipulation on CRD objects

VERSION

version 1.108

SYNOPSIS

package My::App;
use Moo;
with 'IO::K8s::Role::SpecBuilder';

has spec => ( is => 'rw' );

sub BUILD { $_[0]->spec({ routes => [] }) }

package main;
my $app = My::App->new;
$app->spec_push('routes', { match => 'Host(`x`)' });
$app->spec_set('routes.0.match', 'Host(`y`)');
print $app->spec_get('routes.0.match');

DESCRIPTION

This role provides dotted-path get/set/push/merge/delete operations against a consumer class's spec attribute. It exists so callers building arbitrary CRDs (IngressRoute, HTTPRoute, Gateway listeners, ...) can reach deeply-nested fields without writing the indexing by hand each time; since 1.108 the built-in Kinds reach theirs the same way.

Path syntax: dot-separated segments. On a plain hashref/arrayref node each segment is a hash key or an array index; on an object node -- a typed spec, or any nested typed field reached along the way -- a segment is instead the JSON field name mapped to its attribute through the registry, or, for a key the class does not declare, read and written through the object's _unknown_fields bag (D1) exactly as it would be on a plain hash. A purely numeric segment is always an array index: it counts from the end when negative, so -1 addresses the last element, or (on an empty array) the one spec_set/spec_array/spec_push create there; any other index that resolves outside the array croaks rather than autovivifying a hole.

spec_set, spec_array, spec_hash and spec_push vivify missing intermediate structure as they walk: a plain hashref/arrayref on an untyped node, or, on a typed node, whatever the attribute registry declares for that field (an inline struct or referenced class, an array/hash container), with a hashref handed to a typed slot inflated the same way FROM_HASH does. spec_get and spec_delete never vivify.

Every failure raised while walking or vivifying begins with the spec path and carries no internal file or line (k101) -- spec path 'PATH': in front of the reason, or, for a path that is itself empty, spec path 'PATH' is empty on its own. A Moo/Type::Tiny failure hit while vivifying or writing a declared field is re-raised with that prefix, such as:

spec path 'PATH': cannot set 'SEG': ORIGINAL MESSAGE
spec path 'PATH': cannot create CLASS for 'SEG': ORIGINAL MESSAGE
spec path 'PATH': cannot clear 'SEG': ORIGINAL MESSAGE

The walk's own checks use the same spec path 'PATH': prefix for the failures they detect directly, too: an invalid or out-of-range array index, a scalar blocking further descent (whether hit while walking or while storing into it), and spec_array/spec_hash finding a non-array or scalar value already at the path. spec_merge bypasses the path machinery entirely and shallow-merges into the top level only.

IO::K8s::Role::APIObject composes this role, so every top-level Kind has it: the built-in Kubernetes kinds, CRD classes declared via use IO::K8s::APIObject api_version => ..., ..., and the classes IO::K8s::AutoGen builds at runtime, which compose IO::K8s::Role::APIObject directly. Before 1.108 only CRD classes got it, and composing one of the builder roles (IO::K8s::Role::CertManaged, IO::K8s::Role::Routable, ...) onto a class without it failed at the first spec_* call rather than at composition time; those roles now require the spec_* methods they use, which is only correct because every APIObject has them (k103).

A Kind that declares no spec field at all -- 32 of the shipped ones, carrying data/rules/subjects instead: ConfigMap, Secret, Endpoints, the four RBAC kinds, ... -- still has the methods, and every one of them croaks

IO::K8s::Api::Core::V1::ConfigMap has no spec field: the spec_* methods need one to read or build

naming the class, at the caller's line. Composition does not fail for those Kinds: this role is composed before the class's own k8s spec => ... line runs, so a requires 'spec' would reject every consumer, including the ones that do declare one.

spec_get

my $value = $obj->spec_get($path);

Reads a value from the object's spec at the dotted path $path. Each segment is a hash key, an array index (a purely numeric segment, -1 for the last element), or a JSON field name on a typed spec node -- an inline struct, a referenced class, or an array/hash of either. A terminal that is itself typed comes back as that object, not a hashref -- spec_get never serializes what it finds. Returns undef if any segment along the way is missing, spec itself is unset, or the terminal value is not defined. Never vivifies.

my $match = $ir->spec_get('routes.0.match');

spec_set

$obj->spec_set($path, $value);

Writes $value into the object's spec at the dotted path $path. Vivifies missing intermediate structure along the way: on a plain hash spec as a hashref (or arrayref, for a numeric segment), and on a typed spec as whatever the attribute registry declares for that field -- an inline struct or referenced class, an array/hash container, or (via the _unknown_fields bag, D1) a plain hash for a field the class does not declare. A hashref handed to a declared object/array/hash-of-objects slot is inflated through the registry the same way FROM_HASH would, and the final write goes through the target's ordinary accessor, so a declared field's own type constraint validates the value -- the wrong type croaks the same way a direct ->attr($value) call would. Returns $self for chaining.

Vivifying a typed intermediate constructs the declared class with no arguments; a class with required attributes (k101) cannot be built that way, and the call croaks naming the spec path instead -- build that object yourself and hand it to spec_set as the value.

$ir->spec_set('tls.secretName', 'my-cert');

spec_array

my $arrayref = $obj->spec_array($path);

Vivifies and returns the arrayref at the dotted path $path -- the same intermediate vivification as spec_set, but returning the container itself rather than storing a value into it, so the caller can push, splice or iterate in place. Croaks if the path already holds a defined, non-array value. Vivifies intermediates the same way spec_set does, including the required-attribute croak described there.

push @{ $ir->spec_array('entryPoints') }, 'websecure';

spec_hash

my $hashref = $obj->spec_hash($path);

Vivifies and returns the container at the dotted path $path, so the caller can read or write it directly: a plain hashref on an untyped node or an opaque map field, or the struct/object itself when the declared field is a typed struct or referenced class. Croaks if the path already holds a defined scalar. Vivifies intermediates the same way spec_set does, including the required-attribute croak described there.

$ir->spec_hash('tls')->{secretName} = 'my-cert';

spec_push

$obj->spec_push($path, @values);

Appends @values onto the arrayref located at the dotted path $path, vivifying it (and its parent structure, including the required-attribute croak described under spec_set) as needed via spec_array; pushing onto a path that already holds a defined, non-array value croaks rather than replacing it. A hashref value handed to an array-of-objects slot is inflated to the element class, same as spec_set; an already-blessed value is kept as is. Returns $self for chaining.

$ir->spec_push('routes', { match => 'Host(`api.example.com`)' });

spec_merge

$obj->spec_merge(key1 => $value1, key2 => $value2, ...);

Shallow-merges the given key/value pairs into the top-level spec, creating it first if the object has none. On a typed spec each key is written through its declared accessor (with inflation, as spec_set does) when the class declares it, and into the _unknown_fields bag (D1) otherwise. Existing keys are overwritten; keys not mentioned in the merge are left alone. Returns $self for chaining.

$ir->spec_merge(entryPoints => ['web', 'websecure']);

spec_delete

$obj->spec_delete($path);

Removes the value at the dotted path $path. For a hash parent the key is deleted; for a declared field on a typed node there is nothing to remove, so it is cleared to undef through its accessor instead -- which croaks on a required field, because its type constraint is not Maybe-wrapped and rejects undef the same as any other bad value. For an array parent the indexed element is spliced out. If the path does not resolve -- spec is unset, a parent is missing, or the terminal is undefined -- the call is a no-op. Returns $self for chaining.

$ir->spec_delete('tls');

SEE ALSO

IO::K8s::APIObject, IO::K8s::Role::ResourceMap

SUPPORT

Issues

Please report bugs and feature requests on GitHub at https://github.com/pplu/io-k8s-p5/issues.

CONTRIBUTING

Contributions are welcome! Please fork the repository and submit a pull request.

AUTHORS

  • Torsten Raudssus <getty@cpan.org>

  • Jose Luis Martinez Torres <jlmartin@cpan.org>

COPYRIGHT AND LICENSE

This software is Copyright (c) 2018-2026 by Jose Luis Martinez Torres <jlmartin@cpan.org>.

This is free software, licensed under:

The Apache License, Version 2.0, January 2004