NAME

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

VERSION

v0.13.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 almost entirely documentation. Its one exception is probe_component() (see "PROBING: CHEAP REACHABILITY CHECKS FOR 'defer' COMPONENTS"), a small shared helper used by build_desk() and open_desk() for deferred components; nothing else here is functional, and nothing inherits from this module. 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 -- and just as importantly, neither Concierge nor Concierge::Desk::Setup ever inherits from an added component. The relationship is compositional, not hierarchical: the application's concierge obtains component objects and hands them to the application, which composes its own capabilities from them, rather than a component's behavior becoming part of Concierge's own class hierarchy.

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.

PROBING: CHEAP REACHABILITY CHECKS FOR 'defer' COMPONENTS

probe

my $result = SomeComponent->probe($payload);
# { success => 1 }  or  { success => 0, message => '...' }

An optional, duck-typed class method (not an instance method -- deliberately decoupled from new() so it can do a genuinely cheap check, e.g. a raw socket connect, a file-exists check, a DNS resolution, without paying for whatever new() actually does: pool setup, ORM init, a full auth handshake).

probe is only ever consulted for a component marked defer => 1 in the desk's components config block; it is never called for a non-defer component. A component author is never required to implement probe just because defer is used elsewhere in the desk -- see probe_component() below for what happens when it's absent.

probe must be fast and self-bounded. Concierge does not enforce a timeout around it; see "Additional Components" in Concierge for the $max_wait_for_load convention and the blocking-retry warning.

probe_component

my $result = Concierge::Desk::Component::probe_component($class, $payload);
# { success => 1 }  or  { success => 0, message => '...' }

A plain package-qualified function -- not a method call, since this module is never instantiated and nothing inherits from it. Shared identically by Concierge::Desk::Setup::build_desk() (build-time probe, for every defer entry, immediately after setup() succeeds) and Concierge->open_desk() (open-time revalidation, for every defer entry), so the two call sites can never drift apart.

If $class implements probe, probe_component() simply calls and returns $class->probe($payload). If $class does not implement probe, it falls back to a default check equivalent to what already happens implicitly for every non-defer component: can the class even be required. This is the same default used at both the build-time and open-time call sites -- nothing is ever silently skipped for a defer component merely because it lacks a probe method.

Both Concierge.pm and Concierge::Desk::Setup declare their own explicit use Concierge::Desk::Component; to call this function, rather than relying on one loading it as a side effect of the other.

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.

FORWARD REFERENCE: A FUTURE compose()

This section is a flag for future editing, not documentation of existing behavior. No compose() method exists anywhere in Concierge today, nothing described below is implemented, and no API for it has been finalized or even fully designed.

The idea under consideration is some future affordance -- tentatively named compose() -- for combining multiple added components into a single, unified interface, rather than an application reaching each one individually through its own accessor as described above. Earlier drafts of this project assumed such a thing, if it were ever built, would belong on Concierge or Concierge::Desk::Setup. On reflection, this module -- Concierge::Desk::Component -- looks like the more logical home for whatever contract documentation a real compose() would eventually need, since this is already the fixed point for the single-added-component contract that compose() would presumably have to build on or generalize.

This note exists purely so that whoever eventually designs and implements compose() starts by looking here, not to commit to any particular design, timeline, or even the certainty that it will be built at all.

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.