package Open::API;

use 5.008003;
use strict;
use warnings;
use Carp ();

our $VERSION = '0.05';

require XSLoader;
XSLoader::load('Open::API', $VERSION);

Carp::croak(
	"Open::API requires JSON::Schema::Fast built with its C ABI "
	. "(JSON::Schema::Fast 0.04 or later); please upgrade or reinstall it"
) unless _abi_ok();

1;

__END__

=head1 NAME

Open::API - OpenAPI 3.1 server and client

=head1 VERSION

Version 0.05

=head1 SYNOPSIS

    use Open::API;

    # a spec: hashref, JSON text, YAML text, or a filename
    my $api = Open::API->new(spec => 'openapi.json');

    # the compiled spec drives a PSGI app (see Open::API::Plack) ...
    my $app = Open::API::Plack->new(
        api      => $api,
        handlers => {
            listPets => 'MyApp::Pets::list',
            getPet   => sub { ... },
        },
    )->to_app;

    # ... and an HTTP client (see Open::API::Client)
    my $client = Open::API::Client->new(
        api => $api,
        base_url => 'http://127.0.0.1:5000'
    );
    my $res = $client->getPet(petId => 42)->get;

=head1 DESCRIPTION

C<Open::API> loads an OpenAPI 3.1 document once and compiles every parameter,
header, cookie and body schema through L<JSON::Schema::Fast> at
startup. Each request is then routed and validated on a C hot path.

The compiled object is the shared core of two consumers: L<Open::API::Plack>
serves it as a PSGI app (routing, validation, security, CSRF, CORS - all in
C before a handler runs), and L<Open::API::Client> is a spec-driven HTTP
client on L<Fetch>'s C ABI, so one document defines both sides of the wire.
The L</match> and L</validate_request> methods below expose the router and
validator directly for any other framework adapter.

OpenAPI B<3.1 only>: 3.1 schemas are native JSON Schema 2020-12, which is what
JSON::Schema::Fast validates. A document with any other C<openapi> version
croaks at load.

=head1 CONSTRUCTOR

=head2 new

    my $api = Open::API->new(spec => $spec);

C<spec> is required and may be:

=over 4

=item * a hashref (an already-decoded document),

=item * a string of JSON text,

=item * a string of YAML text (decoded with L<YAML::XS>, loaded lazily -
only YAML specs need it), or

=item * a filename (C<.json>, C<.yaml> or C<.yml>; anything else is sniffed by
content).

=back

Compilation walks C<paths> once: every operation needs a unique
C<operationId> (it is the dispatch key), path templates are pre-split,
path-item and operation C<parameters> are merged (operation wins), and every
schema is compiled through the JSF ABI - parameters with coercion enabled
(string sources satisfy typed schemas, exactly what OpenAPI parameters need)
and C<default>-filling on. C<$ref>s to C<#/components/schemas/...> are
resolved at compile time. A malformed document, a missing or duplicate
operationId, or an unresolvable reference croaks here, at startup.

=head1 METHODS

=head2 spec

The decoded OpenAPI document.

=head2 operations

    my $ops = $api->operations;   # [ { operationId, method, path }, ... ]

=head2 operation

    my $info = $api->operation('getPet');

A description of one operation: C<params> by location (name + required),
C<body> (required flag + content types) and C<responses> (statuses with
compiled schemas). C<undef> for an unknown id.

=head2 match

    my ($opId, $captures) = $api->match($method => $path);

The router alone: C<($operationId, \%raw_path_captures)> on a match; an empty
list for a 404; C<(undef, \@allow)> when the path exists but the method does
not (a 405 and its Allow list). For framework adapters.

=head2 validate_request

    my ($ok, $result) = $api->validate_request($opId, {
        path   => \%raw_captures,
        query  => $query_string_or_hashref,
        header => \%lowercased_headers,
        body   => $raw_body_or_decoded_ref,
    });

The validator alone: C<(1, \%params)> or C<(0, \@errors)>. Header names must
be lowercased by the caller; cookies are parsed from the C<cookie> header when
the operation declares cookie parameters. For framework adapters - together
with L</match> this is the complete integration surface, everything heavy
stays in C.

=head1 ERRORS

Validation errors are hashrefs: the L<JSON::Schema::Fast> output fields
(C<instanceLocation>, C<keyword>, C<schemaLocation>, C<message>) augmented
with C<in> (path / query / header / cookie / body / response) and C<name>
(the parameter name). Missing required inputs use C<< keyword => 'required' >>;
an undecodable JSON body uses C<< keyword => 'json' >>.

=head1 ARCHITECTURE

Open::API is a consumer of two runtime-resolved C ABIs, the same DBI-style
versioned function-pointer tables used across the Semantic stack:

=over 4

=item * L<JSON::Schema::Fast> (required) - schemas are compiled once through
C<jsf_abi.h> and every validation on the request path is a direct C call.
Resolved and version-checked at load; absence is a hard error.

=item * L<Fetch> (optional) - L<Open::API::Client> fires requests through
C<fetch_abi.h>. Resolved lazily on first client construction; the server side
never loads it.

=back

It is also a B<provider> of one: C<include/oa_abi.h> exposes the router and
validator to another XS module's own C dispatcher, so an operation is routed
and validated with no Perl frame between the socket and the controller. See
L</C ABI> below.

There is no link-time coupling: each distribution builds and upgrades
independently, and a version skew fails cleanly at boot (t/12-abi-guard.t on
the consumer side, t/25-abi.t for the provider table).

=head1 C ABI

C<include/oa_abi.h> is the C form of L</match> and L</validate_request>: a
versioned function-pointer table resolved at B<runtime>, in the DBI style used
across the Semantic stack. There is no link-time symbol coupling, so provider
and consumer build and upgrade independently. L<Punk> is the first consumer -
its C dispatcher routes, validates and hands the typed parameters to a
controller without unwinding into Perl.

=head2 Getting the header

Open::API installs C<oa_abi.h> through L<ExtUtils::Depends>, so a consumer's
F<Makefile.PL> picks up the include path with no vendoring:

    use ExtUtils::Depends;
    my $pkg = ExtUtils::Depends->new('My::Consumer', 'Open::API');
    WriteMakefile(
        ...
        $pkg->get_makefile_vars,   # INC reaches oa_abi.h
        PREREQ_PM => { 'Open::API' => '0.04' },
    );

The alternative is to vendor a copy of the header pinned at a known
C<OA_ABI_VERSION>. Either way the header only declares the table; nothing is
linked.

Perl's own headers (F<EXTERN.h>, F<perl.h>, F<XSUB.h>) must be included before
C<oa_abi.h>, which needs C<SV>, C<AV>, C<HV>, C<STRLEN> and C<pTHX>.

=head2 Resolving the table

C<Open::API::_abi_ptr> returns the address of the process-wide table as an IV.
It is not a Perl-level API - it is the ABI entry point, and the only supported
thing to do with it is C<INT2PTR> it back to a C<< const oa_abi * >> and check
C<abi_version> before the first call:

    #include "oa_abi.h"

    static const oa_abi *OA = NULL;

    static const oa_abi *my_oa(pTHX) {
        if (!OA) {
            IV p = 0;
            dSP;
            eval_pv("require Open::API;", FALSE);
            SPAGAIN;                    /* the require may have moved the stack */
            if (!SvTRUE(ERRSV)) {
                ENTER; SAVETMPS; PUSHMARK(SP); PUTBACK;
                if (call_pv("Open::API::_abi_ptr", G_SCALAR | G_EVAL) > 0) {
                    SPAGAIN;
                    p = POPi;
                }
                PUTBACK; FREETMPS; LEAVE;
            }
            if (p) {
                const oa_abi *a = INT2PTR(const oa_abi *, p);
                if (a && a->abi_version == OA_ABI_VERSION) OA = a;
            }
        }
        return OA;   /* NULL means: use the Perl-visible methods instead */
    }

The address is static for the life of the process, so resolve it once at boot
and cache it. The IV may legitimately be negative on platforms that map shared
objects high in the address space; C<INT2PTR> round-trips the bits either way,
so test for non-zero, not for positive.

A NULL result means Open::API is absent, too old, or built without the table.
That is not an error in itself - the documented answer is to fall back to
L</match> and L</validate_request>, which do the same work with Perl frames.
A consumer that hard-requires the ABI should croak at boot, not per request.

=head2 The table

    typedef struct oa_abi {
        int abi_version;
        void *(*api_of)   (pTHX_ SV *api_sv);
        void *(*route)    (pTHX_ void *api, const char *method, STRLEN mlen,
                           const char *path, STRLEN plen, HV *caps, AV *allow);
        void *(*op_by_id) (pTHX_ void *api, SV *op_id);
        int   (*validate) (pTHX_ void *api, void *op, HV *raw, HV *typed,
                           AV *errors);
        SV   *(*op_id)    (pTHX_ void *op);
    } oa_abi;

=over 4

=item * C<abi_version> - equals C<OA_ABI_VERSION> in the header the provider
was built with. Compare it before calling anything else.

=item * C<api_of> - the opaque API handle behind a blessed Open::API SV (what
L</new> returns). Returns NULL when the SV is not a reference; it never
croaks, so it is safe on the fall-back path. The handle lives as long as the
SV does and is not freed by the caller.

=item * C<route> - the C form of L</match>. C<method> and C<path> are given
with explicit lengths, and C<path> is the request path only, already
prefix-stripped by the caller (a mount strips its own prefix). On a match it
returns the opaque operation handle and fills C<caps> with the raw captured
path segments (name =E<gt> borrowed value SV). On a 405 - the path exists but
not for this method - it returns NULL and fills C<allow> with the upper-case
method names, an AV of SVs. On a 404 it returns NULL and leaves C<allow>
empty. C<caps> and C<allow> are caller-owned and must be empty on entry;
either may be NULL to discard that output.

=item * C<op_by_id> - the opaque operation handle for an operationId, for a
consumer that dispatches by name rather than by path. NULL when the id is
unknown.

=item * C<validate> - the C form of L</validate_request>. C<raw> is the same
hash that method documents:

    { path   => \%captures,           # e.g. the caps from route()
      query  => $query_string | \%hash,
      header => \%lowercased_headers, # cookies ride the "cookie" header
      body   => $raw_bytes | $decoded_ref }

On success it fills C<typed> with the validated, coerced, percent-decoded
parameters (the path/query/header/cookie hashes and the body, exactly the
shape L</validate_request> returns) and returns 1. On failure it pushes error
hashrefs - the L</ERRORS> shape - into C<errors> and returns 0. Both
containers are caller-owned and should be empty on entry; either may be NULL
to discard it, and a NULL C<typed> validates without materialising the
parameters.

=item * C<op_id> - the operationId SV of a routed operation. It is borrowed:
do not free it, and copy it if it must outlive the API object.

=back

Every call takes C<pTHX_> and must be made on the interpreter that owns the
API SV. The table itself is C<const> and shared; the handles it hands back are
owned by the Open::API object, so keep that object alive for as long as the
dispatcher uses them.

=head2 Versioning

The table only ever grows at the end. C<OA_ABI_VERSION> bumps on any append,
and a consumer requires C<abi_version> to equal the version it was written
against, or to be greater than it if it treats a later table as a superset and
uses only the prefix it knows. Fields are never reordered, retyped or removed
within a major line, so a consumer built against version I<N> keeps working
against any provider that still reports I<N>.

The provider table is exercised end to end by F<t/25-abi.t>, which drives
C<api_of>, C<route>, C<op_id> and C<validate> through the pointers and asserts
the results match the Perl-visible L</match> and L</validate_request> - the
ABI is the same behaviour with the Perl frames removed.

=head1 SEE ALSO

L<Open::API::Plack>, L<Open::API::Client>, L<JSON::Schema::Fast>, L<Fetch>,
L<Hyperman>, L<Punk>.

=head1 AUTHOR

LNATION <email@lnation.org>

=head1 BUGS

Please report any bugs or feature requests to C<bug-open-api at rt.cpan.org>, or through
the web interface at L<https://rt.cpan.org/NoAuth/ReportBug.html?Queue=Open-API>.  I will be notified, and then you'll
automatically be notified of progress on your bug as I make changes.

=head1 SUPPORT

You can find documentation for this module with the perldoc command.

    perldoc Open::API

You can also look for information at:

=over 4

=item * RT: CPAN's request tracker (report bugs here)

L<https://rt.cpan.org/NoAuth/Bugs.html?Dist=Open-API>

=item * Search CPAN

L<https://metacpan.org/release/Open-API>

=back

=head1 LICENSE AND COPYRIGHT

This software is Copyright (c) 2026 by LNATION <email@lnation.org>.

This is free software, licensed under:

  The Artistic License 2.0 (GPL Compatible)

=cut
