=head1 NAME

Router::Ragel - High-performance URL router built on a Ragel-generated state machine

=head1 SYNOPSIS

    use Router::Ragel;

    my $router = Router::Ragel->new
        ->add('/users', 'users_list')
        ->add('/users/:id<int>', 'user_show')
        ->add('/blog/:year<int>/:month<int>/:slug', 'blog_post')
        ->compile;

    my ($handler, @captures) = $router->match('/users/42');
    # ('user_show', '42')

    my @no_match = $router->match('/nope');
    # ()

    # Function form, faster than method dispatch:
    my ($h, @c) = Router::Ragel::match($router, '/users/42');

=head1 DESCRIPTION

Router::Ragel compiles a set of URL patterns into a single Ragel finite-state
machine and emits it as C via L<Inline::C>. Matching a path is a fixed-cost
walk over a DFA: there is no per-route loop and no regex engine. For
applications with many routes or high request rates this typically beats
regex- and trie-based routers by a wide margin (see L</PERFORMANCE>).

The router supports:

=over 4

=item * Static segments (C</users>, C</products>)

=item * Named placeholders (C</users/:id>, C</blog/:year/:month>)

=item * Typed placeholders (C<< /users/:id<int> >>, C<< /code/:c<[0-9]{4}> >>)

=item * Inline placeholders mixed with literal text in a single segment
(C<< /v/:major<int>.:minor<int> >>)

=item * Multiple independent router instances in the same process

=back

=head1 METHODS

=head2 new

    my $router = Router::Ragel->new;

Constructs a new router. Takes no arguments.

=head2 add

    $router->add($pattern, $data);

Registers a route. C<$pattern> is a path string (see L</"ROUTE PATTERNS">);
C<$data> is the value returned by C<match> on a hit and may be any scalar.
Returns the router, so calls can be chained.

Adding a route after C<compile> invalidates the compiled state; the next
C<match> will C<croak> until C<compile> is called again.

=head2 compile

    $router->compile;

Builds and binds the Ragel state machine. Must be called before C<match>.
Returns the router. May be called more than once to incorporate routes added
between calls; see L</LIMITATIONS> for the cost of recompiling.

=head2 match

    my ($data, @captures) = $router->match($path);

Matches C<$path> against the compiled routes and returns the route data
followed by captured values, in pattern order. Returns the empty list on no
match.

For the lowest call overhead, invoke C<match> as a plain function and skip
Perl's method dispatch:

    my ($data, @captures) = Router::Ragel::match($router, $path);

Both forms run the same compiled state machine.

=head1 ROUTE PATTERNS

A pattern is a string starting with C</>. Each segment between slashes is
either literal text or contains one or more B<placeholders>. A placeholder is
C<:NAME> optionally followed by a type constraint C<< <TYPE> >>.

=head2 Placeholder names

The name is a run of word characters (C<\w+>): letters, digits, and
underscores. With no type, a placeholder matches any non-slash bytes
(C<[^/]+>).

The name is greedy, so to follow a placeholder with literal characters that
could otherwise extend the name, terminate the name with an explicit type:

    /:type_extra # one placeholder named "type_extra"
    /:type<string>_extra # placeholder "type" then literal "_extra"

=head2 Type constraints

Built-in aliases:

=over 4

=item * C<int> = C<[0-9]+>

=item * C<string> = C<[^/]+> (default; explicit form)

=item * C<hex> = C<[0-9a-fA-F]+>

=back

Anything else inside C<< <...> >> is passed verbatim to Ragel, so arbitrary
character classes and quantifiers work:

    /code/:c<[0-9]{4}> # exactly four digits
    /file/:name<[a-z0-9\-]+> # slug-like (escape '-' inside a class)

The dialect is B<Ragel's>, not Perl/PCRE. Available: character classes,
quantifiers (C<*>, C<+>, C<?>, C<{n}>, C<{n,m}>), alternation, grouping, and
Ragel keywords (C<digit>, C<alpha>, C<alnum>, C<xdigit>, C<lower>, C<upper>,
C<space>, C<punct>, C<print>, C<ascii>, C<any>). B<Not> available: Perl
shortcuts (C<\d>, C<\w>, C<\s>), anchors, lookaround, and backreferences.
Anchors are unnecessary anyway: segment boundaries are implicit.

A literal C<< > >> cannot appear inside a C<< <type> >> expression (the
parser closes the type at the first C<< > >>); a literal C<< < >> is
rejected at compile time. A literal C<< - >> inside a character class must
be escaped as C<< \- >> (Ragel parses an unescaped C<< - >> as a range
operator and errors out, even at the start or end of the class). For any of
these, match a permissive class and post-process in user code.

=head2 Captures

Captures are returned positionally by C<match>, in the order their
placeholders appear in the pattern. Placeholder names are not used at match
time.

=head2 Examples

    /users # static
    /users/:id # untyped placeholder, matches any non-slash
    /users/:id<int> # typed: digits only
    /blog/:year<int>/:month<int> # multiple typed placeholders
    /v/:major<int>.:minor<int> # multiple placeholders in one segment
    /file/:name<[a-z0-9\-]+>.:ext<[a-z]+> # inline + raw character classes
    /path/to_:type<string>/id_:id<int>/end # mixed literals and placeholders

=head2 Caveats

Any C<:> inside a segment introduces a placeholder; there is no escaping
mechanism. Avoid literal colons in path segments.

=head1 DEPLOYMENT

The compiled Ragel machine lives in a shared library that L<Inline::C>
C<dlopen>s into the process; the function pointer is stored on the router
object. To avoid every worker compiling its own copy, B<call C<compile>
once in the parent process before forking>:

    # in app.psgi or equivalent startup code
    my $router = MyApp->build_router; # calls Router::Ragel->compile
    # then exec the server with --preload-app or equivalent so children
    # inherit the loaded .so via copy-on-write

If the cache is cold and several workers reach C<compile> concurrently,
L<Inline::C> serializes them on a directory lock and only one process runs
the C compiler -- the rest C<dlopen> the resulting C<.so>. That avoids the
C<cc>/C<ragel> stampede but every worker still pays the wait. Compiling in
the parent before fork eliminates the wait too.

For deterministic startup, populate the L<Inline::C> cache at build/deploy
time and ship the warmed directory with the artifact (e.g., bake C<_Inline/>
into your Docker image). Pin the cache location for reproducibility:

    use Inline Config => DIRECTORY => '/opt/myapp/inline';
    use Router::Ragel;

The C<use Inline Config> line must be evaluated before C<Router::Ragel> is
loaded (place it in the same file above C<use Router::Ragel>, or in a
B<BEGIN> block that runs first). Once C<Router::Ragel> is loaded, the cache
location is fixed.

The compiled C<.so> is architecture- and Perl-version-specific; build the
cache on the same target as production.

=head1 PERFORMANCE

Indicative numbers from C<eg/bench.pl> (Linux x86_64, single core; matches
per second across seven mixed routes and six paths):

                       Rate   Mojo R3(method) R3(fun) XS(fun) UR(method) UR(fun) Ragel(method) Ragel(fun)
    Mojo             9166/s     --       -97%    -97%    -99%       -99%    -99%          -99%       -99%
    R3(method)     319956/s  3391%         --     -4%    -55%       -56%    -60%          -68%       -74%
    R3(fun)        332160/s  3524%         4%      --    -53%       -55%    -59%          -66%       -73%
    XS(fun)        713678/s  7686%       123%    115%      --        -3%    -11%          -28%       -42%
    UR(method)     735486/s  7924%       130%    121%      3%         --     -9%          -26%       -41%
    UR(fun)        804357/s  8675%       151%    142%     13%         9%      --          -19%       -35%
    Ragel(method)  988728/s 10687%       209%    198%     39%        34%     23%            --       -20%
    Ragel(fun)    1238754/s 13414%       287%    273%     74%        68%     54%           25%         --

Run C<eg/bench.pl> to reproduce locally; routers that aren't installed are
skipped.

=head1 LIMITATIONS

=over 4

=item *

Patterns and paths are byte strings. Wide-character (utf8-flagged) strings
are matched against their UTF-8 byte representation.

=item *

Patterns must start with C</> and must not be empty, contain a NUL byte,
contain consecutive slashes, use a bare C<:> placeholder name, or contain an
empty or unterminated C<< <type> >>. C<compile> C<croak>s on any of these.

=item *

Matching is exact: C</users> matches only C</users>, not C<//users>,
C</users/>, or C</users//>. C</users> and C</users/> are distinct routes.
Normalize input ahead of C<match> to fold repeated or trailing slashes.

=item *

Adding a route after C<compile> invalidates the compiled matcher; C<match>
C<croak>s until you re-run C<compile>. Each C<compile> C<dlopen>s a new
shared library; the previous one stays loaded for the lifetime of the
process. See L</DEPLOYMENT> for the implications under pre-forking servers.

=item *

Calling C<match> before C<compile> C<croak>s.

=back

=head1 SEE ALSO

=over 4

=item * L<Router::R3>

=item * L<Router::XS>

=item * L<URI::Router>

=item * L<Mojolicious::Routes>

=item * L<Inline::C>

=item * L<Inline::Filters::Ragel>

=back

=head1 AUTHOR

vividsnow

=head1 LICENSE AND COPYRIGHT

Copyright (c) 2026 vividsnow.

This is free software; you can redistribute it and/or modify it under the
same terms as the Perl 5 programming language system itself.

=cut
