NAME

Concierge::Desk::Component - Contract documentation for additional Concierge desk components

VERSION

v0.10.0

DESCRIPTION

Concierge::Desk::Component documents the minimal contract a module must follow to be usable as an additional component in a Concierge desk (wired up via a components block in "build_desk" in Concierge::Desk::Setup).

This module is pure documentation. It has no functional subs, and nothing inherits from it. Concierge's component mechanism is duck-typed: any class satisfying the contract below works, regardless of what (if anything) it subclasses. There is no isa check anywhere in the loading path.

THE CONTRACT

new

my $component = Some::Component->new($payload);

Ordinary Perl constructor convention -- the sole exception to the hashref-return convention followed everywhere else in Concierge. new either returns a blessed reference or dies/croaks on failure. It does not return { success => 0, ... } on failure; Concierge's open_desk() wraps the call in eval and inspects $@, not a return value.

$payload is exactly whatever the component's own setup() returned at build time (see below) -- persisted verbatim into concierge.conf and handed back unchanged. new() is never called at build time with this payload; build time calls new() with no arguments (or whatever the component itself expects at that point in its own lifecycle) before calling setup(). The two calls to new() -- one at build time, one at open_desk() time -- are not required to take the same arguments; each component decides its own construction story for each phase.

setup

my $result = $component->setup($config);

Called exactly once, at desk build time (from build_desk()). Always returns a hashref:

{ success => 1, message => '...', ...payload keys... }
{ success => 0, message => '...' }

Whatever setup() returns is stored verbatim as the component's payload in concierge.conf, and is exactly what gets passed to new() at open_desk() time in every subsequent process that opens the desk. setup() is never re-run or re-consulted at runtime -- design accordingly. A setup() failure at build time always fails the entire desk build, regardless of whether the component was marked optional in the components config block; optional only affects behavior at open_desk() time (see Concierge::Desk::UnavailableComponent), not at build time.

Every other method

Every other method exposed by a conforming component should return a hashref following the same convention used throughout Concierge:

{ success => 1, message => '...', payload... }
{ success => 0, message => '...' }

Callers (including Concierge itself, for any core-affordance-adjacent component) should check $result->{success} rather than relying on exceptions.

PROMOTION: EXPOSING COMPONENT METHODS ON $concierge

A component's full API is always reachable through the bare accessor installed by open_desk() -- $concierge->{name}->method(...) or $concierge->name->method(...). This is the permanent escape hatch and requires no configuration; it works for every component, promoted or not.

promote is an additional, optional, purely-cosmetic layer on top of that: a curated allowlist of a component's methods forwarded directly onto $concierge itself, so $concierge->get_signal_report(...) works as sugar for $concierge->{reports}->get_signal_report(...). It is a convenience/clarity mechanism only, not access control -- every component method stays reachable via the escape hatch above regardless of whether it is promoted.

Config shape

Declared in the same components config block as class and optional, passed to Concierge::Desk::Setup::build_desk():

components => {
    reports => {
        class   => 'Concierge::Reports',
        promote => ['get_signal_report'],
        # or, to expose it under a different top-level name:
        promote => { get_signal_report => 'fetch_signal_report' },
    },
},

An arrayref promotes each listed method under its own name. A hashref promotes top_name => component_method pairs, letting the top-level name differ from the component's own method name.

my $c = Concierge->open_desk($desk_dir)->{concierge};
$c->get_signal_report(...);                  # promoted sugar
$c->{reports}->get_signal_report(...);        # escape hatch, always works

Validation happens entirely at build time

promote is a build-time-only decision. build_desk() validates it exhaustively -- shape (arrayref/hashref of plain strings), method-existence ($comp->can($method_name)), and name-collision detection (against core Concierge methods, every component's own bare-accessor name, and every other component's promoted names) -- and persists the already-validated promote entries verbatim into concierge.conf. Any failure here uses build_desk()'s standard { success => 0, message => '...' } return, exactly like a setup() failure -- never an exception.

open_desk() performs none of this validation. It trusts concierge.conf completely and simply replays each persisted promote entry as a forwarding sub. This is intentional: the validation already happened once, at build time; re-validating on every desk open would be redundant work for no added safety.

setup() never sees promote -- it is excluded from the config hash passed to $component->setup(), alongside class and optional.

Interaction with UnavailableComponent

If a promoted component is optional and its new() fails at open_desk() time (see "UNAVAILABLE COMPONENT SUBSTITUTION" below), its promoted forwarding subs are still installed -- with zero special casing. A forwarding sub's ->$method_name(@_) call resolves through Concierge::Desk::UnavailableComponent's AUTOLOAD exactly as it would through any other method call on the stand-in, returning the standard { success => 0, message => "Component '...' unavailable: ..." } failure hashref -- no die, no missing sub.

Process-lifetime uniqueness caveat

Promoted forwarding subs are installed the same way bare accessors are: as package-level globs on Concierge::, guarded by unless ($concierge->can($top_name)) so a later open_desk() call in the same process does not reinstall (or fight over) a name already claimed by an earlier desk that process opened. This is not new behavior -- it is the existing bare-accessor idiom, applied identically to promoted names.

Future direction (not implemented)

A component-advertised "suggested methods" discovery convention (e.g. an api_methods() method a component could implement to suggest its own promotion candidates) is a plausible future extension, but is out of scope today -- promote is entirely author-specified in the components config block.

UNAVAILABLE COMPONENT SUBSTITUTION

If a component is registered as optional in the desk's components config block and its new() dies at open_desk() time, Concierge substitutes a Concierge::Desk::UnavailableComponent object in its place rather than failing the whole desk open. Every method call on that stand-in -- via AUTOLOAD -- returns { success => 0, message => "Component '$name' unavailable: $reason" }.

Caveat: AUTOLOAD does not make can() or isa() report true for the methods it's standing in for. Application code that probes a component with $comp->can('some_method') before calling it will find the probe returns false on an UnavailableComponent stand-in, even though calling some_method directly works fine (via AUTOLOAD) and returns the expected failure hashref. The correct pattern is to call the method directly and check $result->{success} -- never probe with can/isa first:

my $result = $concierge->organizations->add_record($id, \%data);
unless ($result->{success}) {
    # handle $result->{message} -- this branch also
    # correctly handles an UnavailableComponent substitution
}

A required (non-optional) component's new() failure is not caught this way -- it propagates as an uncaught exception from open_desk(), since a desk must never open half-instantiated.

SEE ALSO

Concierge -- see its open_desk() and EXTENSIBILITY section for how the components config block is loaded.

Concierge::Desk::Setup -- see build_desk() for how a component's setup() result is resolved and persisted at build time.

Concierge::Desk::UnavailableComponent -- the stand-in substituted for a failed optional component.

Concierge::Users -- the identity core records-store component; not itself wired through the generic components mechanism (Users remains a hardcoded core affordance), but a production example of the new/ setup two-phase lifecycle this contract documents.

AUTHOR

Bruce Van Allen <bva@cruzio.com>

LICENSE

This module is free software; you can redistribute it and/or modify it under the terms of the Artistic License 2.0.