# Copyright (c) 2025 Yuki Kimoto
# MIT License
class Mojolicious::Routes::Route {
version_from Mojolicious;
use Mojo::Util;
use Mojolicious::Routes::Pattern;
# Class Variables
# Reserved stash values
our $RESERVED : Hash of string;
INIT {
my $reserved = [
"action",
"app",
"cb",
"controller",
"data",
"extends",
"format",
"handler",
"inline",
"json",
"layout",
"namespace",
"path",
"status",
"template",
"text",
"variant",
];
$RESERVED = Hash->new_from_keys($reserved => 1);
}
# Fields
has inline : rw byte;
has partial : rw byte;
has children : rw Mojolicious::Routes::Route[];
has parent : rw Mojolicious::Routes::Route
get {
unless (exists $self->{parent}) {
$self->{parent} = undef;
}
return $self->{parent};
}
set {
$self->{parent} = $_;
weaken $self->{parent};
}
;
has pattern : rw Mojolicious::Routes::Pattern
get {
unless (exists $self->{pattern}) {
$self->{pattern} = Mojolicious::Routes::Pattern->new;
}
return $self->{pattern};
}
;
has name : rw string
set {
$self->{name} = $_;
$self->{custom} = 1;
}
;
has has_websocket : ro int
get {
unless (exists $self->{has_websocket}) {
my $chain = $self->_chain;
my $has_websocket = 0;
for (my $i = 0; $i < $chain->length; $i++) {
my $child = $chain->get($i);
if ($child->is_websocket) {
$has_websocket = 1;
}
}
}
return $self->{has_websocket};
}
;
has methods : rw string[]
set ($_ : string|string[]) {
my $methods = (string[])undef;
if (!$_) {
# undef
}
elsif ($_ isa string) {
$methods = [(string)$_];
}
elsif ($_ isa string[]) {
$methods = (string[])$_;
}
else {
die "The methods field value must be a string, a string array.";
}
if ($methods) {
for (my $i = 0; $i < @$methods; $i++) {
$methods->[$i] = Fn->uc($methods->[$i]);
}
}
$self->{methods} = $methods;
}
;
has requires : rw Hash
get {
unless (exists $self->{requires}) {
$self->{requires} = Hash->new;
}
return $self->{requires};
}
set {
unless ($_) {
die "The requires field value must be defined.";
}
$self->{requires} = $_;
$self->root->cache->set_max_keys(0);
}
;
# Undocumented Fields
has websocket : byte;
has custom : string;
# Class Methods
static method new : Mojolicious::Routes::Route () {
my $self = new Mojolicious::Routes::Route;
$self->{children} = new Mojolicious::Routes::Route[0];
return $self;
}
# Instance Methods
method any : Mojolicious::Routes::Route ($args : object...) {
my $methods = (string[])undef;
my $shift = 0;
if ($args->[0] isa string[]) {
$methods = (string[])$args->[0];
$shift = 1;
}
else {
$methods = new string[0];
}
$args = $shift ? (object...)Array->copy_object_address($args, 1, @$args - 1) : $args;
return $self->_generate_route($methods, $args);
}
method delete : Mojolicious::Routes::Route ($args : object...) {
return $self->_generate_route(DELETE => $args);
}
method get : Mojolicious::Routes::Route ($args : object...) {
return $self->_generate_route(GET => $args);
}
method options : Mojolicious::Routes::Route ($args : object...) {
return $self->_generate_route(OPTIONS => $args);
}
method patch : Mojolicious::Routes::Route ($args : object...) {
return $self->_generate_route(PATCH => $args);
}
method post : Mojolicious::Routes::Route ($args : object...) {
return $self->_generate_route(POST => $args);
}
method put : Mojolicious::Routes::Route ($args : object...) {
return $self->_generate_route(PUT => $args);
}
method under : Mojolicious::Routes::Route ($args : object...) {
return $self->_generate_route(under => $args);
}
method websocket : Mojolicious::Routes::Route ($args : object...) {
my $route = $self->get($args);
$route->{websocket} = 1;
return $route;
}
method add_child : void ($route : Mojolicious::Routes::Route) {
$route->remove;
$route->set_parent($self);
my $children = List->new_ref($self->{children});
$children->push($route);
$route->pattern->set_types($self->root->types);
}
method remove : void () {
my $parent = $self->parent;
unless ($parent) {
return;
}
my $new_children = List->new(new Mojolicious::Routes::Route[0]);
for (my $i = 0; $i < @{$parent->children}; $i++) {
my $child = $parent->children->[$i];
unless ($child == $self) {
$new_children->push($child);
}
}
$parent->set_children($new_children->get_array->(Mojolicious::Routes::Route[]));
$self->set_parent(undef);
}
method root : Mojolicious::Routes () {
return (Mojolicious::Routes)$self->_chain->get(0);
}
method _chain : List of Mojolicious::Routes::Route () {
my $chain = List->new([$self]);
while ($self = $self->parent) {
$chain->unshift($self);
}
return $chain;
}
method find : Mojolicious::Routes::Route ($name : string) {
return $self->_index->get($name);
}
private method _index : Hash of Mojolicious::Routes::Route () {
my $auto = Hash->new;
my $custom = Hash->new;
my $children = (List of Mojolicious::Routes::Route)List->new_ref(Array->copy_object_address($self->children));
while (my $child = $children->shift) {
if ($child->has_custom_name) {
unless ($custom->get($child->name)) {
$custom->set($child->name, $child);
}
}
else {
unless ($auto->get($child->name)) {
$auto->set($child->name, $child);
}
}
$children->push_($child->children);
}
my $ret = Hash->new;
for my $key (@{$auto->keys}) {
$ret->set($key, $auto->get($key));
}
for my $key (@{$custom->keys}) {
$ret->set($key, $custom->get($key));
}
return $ret;
}
method has_custom_name : int () {
return !!$self->{custom};
}
method is_websocket : int () {
return !!$self->{websocket};
}
method is_reserved : int ($name : string) {
return !!$RESERVED->get($name);
}
method is_endpoint : int () {
return $self->inline ? 0 : !@{$self->children};
}
method parse : void ($args : object...) {
my $pattern = $self->pattern;
$pattern->parse($args);
my $name = (mutable string)copy($pattern->unparsed // "");
Re->s($name, ["\W+", "g"], "");
$self->{name} = $name;
}
method render : string ($values : Hash) {
my $chain = $self->_chain;
my $pattern_strings_list = StringList->new;
for (my $i = 0; $i < $chain->length; $i++) {
my $_ = $chain->get($i);
my $pattern_string = $_->pattern->render($values, !@{$_->children} && !$_->partial);
$pattern_strings_list->push($pattern_string);
}
my $path = Fn->join("", $pattern_strings_list->to_array);
return length $path ? $path : "/";
}
method suggested_method : string () {
die "TODO";
}
method to : void ($args : object...) {
my $pattern = $self->pattern;
unless (@$args > 0) {
die "Too few arguments.";
}
my $_ = Mojo::Util->_options($args);
my $shortcut = $_->[0];
my $default_h = Hash->new((object[])$_->[1]);
if ($shortcut) {
die "TODO";
=pod
# Application
if (ref $shortcut || $shortcut =~ /^[\w:]+$/) { $defaults{app} = $shortcut }
# Controller and action
elsif ($shortcut =~ /^([\w\-:]+)?\#(\w+)?$/) {
$defaults{controller} = $1 if defined $1;
$defaults{action} = $2 if defined $2;
}
=cut
}
for my $key (@{$default_h->keys}) {
my $value = $default_h->get($key);
$pattern->defaults->set($key, $value);
}
}
method to_string : string () {
my $chain = $self->_chain;
my $pattern_unparseds_list = StringList->new;
for (my $i = 0; $i < $chain->length; $i++) {
my $chain = $chain->get($i);
my $pattern_unparsed = $chain->pattern->unparsed // "";
$pattern_unparseds_list->push($pattern_unparsed);
}
return Fn->join("", $pattern_unparseds_list->to_array);
}
private method _generate_route : Mojolicious::Routes::Route ($methods : string|string[], $args : object...) {
my $conditions = Hash->new;
my $constraints = Hash->new;
my $defaults = Hash->new;
my $name = (string)undef;
my $pattern = (string)undef;
my $args_list = List->new($args);
while (my $arg = $args_list->shift) {
# First scalar is the pattern
if ($arg isa string && !$pattern) { $pattern = (string)$arg; }
# Scalar
elsif ($arg isa string && $args_list->length) { $conditions->set((string)$arg, $args_list->shift); }
# Last scalar is the route name
elsif ($arg isa string) { $name = (string)$arg; }
# Callback
elsif ($arg isa Mojo::Callback) { $defaults->set("cb", $arg); }
# Defaults
elsif (is_options $arg) {
$defaults = Hash->new(Fn->merge_options($defaults->to_array, (object[])$arg));
}
# Constraints
elsif ($arg isa object[]) {
$constraints = Hash->new(Fn->merge_options($constraints->to_array, (object[])$arg));
}
}
my $route = $self->_route($pattern, $constraints);
$route->set_requires($conditions);
$route->to($defaults);
if ($methods isa string && $methods->(string) eq "under") {
$route->set_inline(1);
}
else {
$route->set_methods((string[])$methods);
}
if ($name) {
$route->set_name($name);
}
return $route;
}
method _route : Mojolicious::Routes::Route ($pattern : string, $constraints_arg : Hash) {
my $r = Mojolicious::Routes::Route->new;
$r->parse($pattern, $constraints_arg);
$self->add_child($r);
my $children = $self->children;
my $route = $children->[-1];
my $new_pattern = $route->pattern;
my $has_reserved = 0;
for (my $i = 0; $i < @{$new_pattern->placeholders}; $i++) {
my $placeholder = $new_pattern->placeholders->[$i];
if ($self->is_reserved($placeholder)) {
$has_reserved = 1;
last;
}
}
if ($has_reserved) {
die "Route pattern " . $new_pattern->unparsed . " contains a reserved stash value.";
}
my $old_pattern = $self->pattern;
my $constraints = $old_pattern->constraints;
if ($constraints->exists("format")) {
unless ($new_pattern->constraints->get("format")) {
$new_pattern->constraints->set("format", $constraints->get("format"));
}
}
my $defaults = $old_pattern->defaults;
if ($defaults->exists("format")) {
unless ($new_pattern->defaults->get("format")) {
$new_pattern->defaults->set("format", $defaults->get("format"));
}
}
return $route;
}
}