Skip to content

Commit

Permalink
Type api/*
Browse files Browse the repository at this point in the history
The first step was to rework the ApiRouter to
clearly state that it was using a trie and add
an appropriate TrieNode. This helped clean up
the types a bit.

Typed the handlers/validators though their
return value is anything that `json_encode`
can consume (so anything effectively :-)).
  • Loading branch information
jchaffraix committed Dec 25, 2024
1 parent 15d9310 commit 3b03367
Show file tree
Hide file tree
Showing 6 changed files with 87 additions and 64 deletions.
12 changes: 6 additions & 6 deletions SETUP/tests/unittests/ApiTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ public function test_get_invalid_round_stats(): void
$this->expectExceptionCode(103);

$path = "v1/stats/site/rounds/P4";
$query_params = "";
$query_params = [];
$router = ApiRouter::get_router();
$_SERVER["REQUEST_METHOD"] = "GET";
$router->route($path, $query_params);
Expand All @@ -128,7 +128,7 @@ public function test_get_invalid_page_data(): void

$project = $this->_create_project();
$path = "v1/projects/$project->projectid/pages/999.png/pagerounds/P1";
$query_params = "";
$query_params = [];
$router = ApiRouter::get_router();
$_SERVER["REQUEST_METHOD"] = "GET";
$router->route($path, $query_params);
Expand All @@ -142,7 +142,7 @@ public function test_get_invalid_pageround_data(): void
$this->add_page($project, "001");
// P0 is not a valid round
$path = "v1/projects/$project->projectid/pages/001.png/pagerounds/P0";
$query_params = "";
$query_params = [];
$router = ApiRouter::get_router();
$_SERVER["REQUEST_METHOD"] = "GET";
$router->route($path, $query_params);
Expand All @@ -153,7 +153,7 @@ public function test_get_valid_pageround_data(): void
$project = $this->_create_project();
$this->add_page($project, "001");
$path = "v1/projects/$project->projectid/pages/001.png/pagerounds/OCR";
$query_params = "";
$query_params = [];
$router = ApiRouter::get_router();
$_SERVER["REQUEST_METHOD"] = "GET";
$result = $router->route($path, $query_params);
Expand All @@ -168,7 +168,7 @@ public function test_create_project_unauthorised(): void
$this->expectExceptionCode(3);

$path = "v1/projects";
$query_params = "";
$query_params = [];
$router = ApiRouter::get_router();
$_SERVER["REQUEST_METHOD"] = "POST";
$router->route($path, $query_params);
Expand All @@ -181,7 +181,7 @@ public function test_create_project_no_data(): void

$pguser = $this->TEST_USERNAME_PM;
$path = "v1/projects";
$query_params = "";
$query_params = [];
$router = ApiRouter::get_router();
$_SERVER["REQUEST_METHOD"] = "POST";
$router->route($path, $query_params);
Expand Down
69 changes: 45 additions & 24 deletions api/ApiRouter.inc
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,34 @@ include_once("exceptions.inc");
// Raise exceptions on assert failures
ini_set("assert.exception", 1);

/**
* We use a trie to match the path to its handler(s).
*
* Handlers contains the individual handlers, keyed by the method.
*/
class TrieNode
{
/** @var array<string, TrieNode> */
public array $children;
/** @var array<string, callable> */
public array $handlers;
}

class ApiRouter
{
private $_url_map = [];
private $_validators = [];
private TrieNode $root;
/** @var array<string, callable> */
private $_validators;

public function add_route($method, $url, $function)
public function __construct()
{
// Confirm the function is defined or raise an assert exception
assert(function_exists($function), "$function not defined");
$this->root = new TrieNode();
$this->_validators = [];
}

$url_map = &$this->_url_map;
public function add_route(string $method, string $url, callable $function): void
{
$node = $this->root;
$parts = explode("/", $url);
foreach ($parts as $part) {
// If this is a param placeholder, confirm there is a validator
Expand All @@ -25,56 +42,60 @@ class ApiRouter
"No validator specified for $part"
);
}
if (!isset($url_map[$part])) {
$url_map[$part] = [];
if (!isset($node->children[$part])) {
$node->children[$part] = new TrieNode();
}
$url_map = &$url_map[$part];
$node = $node->children[$part];
}
$url_map["endpoint"][$method] = $function;
$node->handlers[$method] = $function;
}

public function route($url, $query_params)
/** @return mixed */
public function route(string $url, array $query_params)
{
$url_map = &$this->_url_map;
$node = $this->root;
$data = [];
$parts = explode("/", $url);
foreach ($parts as $part) {
if (isset($url_map[$part])) {
$url_map = &$url_map[$part];
$next_node = $node->children[$part] ?? null;
if ($next_node) {
$node = $next_node;
} else {
[$param_name, $validator] = $this->get_validator($url_map);
$url_map = &$url_map[$param_name];
[$param_name, $validator] = $this->get_validator($node);
$node = $node->children[$param_name];
$data[$param_name] = $validator($part, $data);
}
}
if (!isset($url_map["endpoint"])) {
if (empty($node->handlers)) {
throw new InvalidAPI();
}
$method = $_SERVER["REQUEST_METHOD"];
if (!isset($url_map["endpoint"][$method])) {
$handler = $node->handlers[$method] ?? null;
if (!$handler) {
throw new MethodNotAllowed();
}
$function = $url_map["endpoint"][$method];
return $function($method, $data, $query_params);
return $handler($method, $data, $query_params);
}

public function add_validator($label, $function)
public function add_validator(string $label, callable $function): void
{
$this->_validators[$label] = $function;
}

private function get_validator($url_map)
/** @return array{0: string, 1: callable} */
private function get_validator(TrieNode $node): array
{
foreach (array_keys($url_map) as $route) {
foreach (array_keys($node->children) as $route) {
if (startswith($route, ":")) {
return [$route, $this->_validators[$route]];
}
}
throw new InvalidAPI();
}

public static function get_router()
public static function get_router(): ApiRouter
{
/** @var ?ApiRouter */
static $router = null;
if (!$router) {
$router = new ApiRouter();
Expand Down
50 changes: 26 additions & 24 deletions api/v1_projects.inc
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ function get_return_fields(?array $query_params, ?Project $project)
return $return_fields;
}

function api_v1_projects($method, $data, $query_params)
function api_v1_projects(string $method, array $data, array $query_params)
{
// set which fields are queryable and their column names
$valid_fields = get_project_fields_with_attr("queryable", null);
Expand Down Expand Up @@ -306,7 +306,7 @@ function create_or_update_project(Project $project)
return $project;
}

function api_v1_project($method, $data, $query_params)
function api_v1_project(string $method, array $data, array $query_params)
{
if ($method == "GET") {
// restrict to list of desired fields, if set
Expand Down Expand Up @@ -373,7 +373,7 @@ function render_project_json(Project $project, ?array $return_fields = null)
//---------------------------------------------------------------------------
// projects/:projectID/wordlists/:type

function api_v1_project_wordlists($method, $data, $query_params)
function api_v1_project_wordlists(string $method, array $data, array $query_params)
{
// get the project this is for and the type of word list
$project = $data[":projectid"];
Expand Down Expand Up @@ -406,7 +406,7 @@ function api_v1_project_wordlists($method, $data, $query_params)
//---------------------------------------------------------------------------
// projects/:projectID/holdstates

function api_v1_project_holdstates($method, $data, $query_params)
function api_v1_project_holdstates(string $method, array $data, array $query_params)
{
$project = $data[":projectid"];

Expand Down Expand Up @@ -446,7 +446,7 @@ function api_v1_project_holdstates($method, $data, $query_params)
//---------------------------------------------------------------------------
// projects/:projectid/pages

function api_v1_project_pages($method, $data, $query_params)
function api_v1_project_pages(string $method, array $data, array $query_params)
{
$project = $data[":projectid"];

Expand All @@ -468,7 +468,7 @@ function api_v1_project_pages($method, $data, $query_params)
//---------------------------------------------------------------------------
// projects/:projectid/pagedetails

function api_v1_project_pagedetails($method, $data, $query_params)
function api_v1_project_pagedetails(string $method, array $data, array $query_params)
{
// optional page round IDs (one or more) to filter down to
$only_rounds = null;
Expand All @@ -479,7 +479,7 @@ function api_v1_project_pagedetails($method, $data, $query_params)
$pageroundids = [$pageroundids];
}
foreach ($pageroundids as $pageroundid) {
validate_page_round($pageroundid, null);
validate_page_round($pageroundid, []);
if ($pageroundid === "OCR") {
$only_rounds[] = "OCR";
} else {
Expand Down Expand Up @@ -524,7 +524,7 @@ function api_v1_project_pagedetails($method, $data, $query_params)
//---------------------------------------------------------------------------
// projects/:projectid/pages/:pagename/pagerounds/:pageroundid

function api_v1_project_page_round($method, $data, $query_params)
function api_v1_project_page_round(string $method, array $data, array $query_params)
{
if ($data[":pageroundid"] == "OCR") {
$text_column = "master_text";
Expand Down Expand Up @@ -583,7 +583,7 @@ function render_project_page_json($row)
//---------------------------------------------------------------------------
// projects/:projectid/transitions

function api_v1_project_transitions($method, $data, $query_params)
function api_v1_project_transitions(string $method, array $data, array $query_params)
{
$sql = sprintf(
"
Expand All @@ -610,7 +610,7 @@ function api_v1_project_transitions($method, $data, $query_params)
//---------------------------------------------------------------------------
// projects/difficulties

function api_v1_projects_difficulties($method, $data, $query_params)
function api_v1_projects_difficulties(string $method, array $data, array $query_params)
{
$difficulties = get_project_difficulties();
return array_keys($difficulties);
Expand All @@ -619,7 +619,7 @@ function api_v1_projects_difficulties($method, $data, $query_params)
//---------------------------------------------------------------------------
// projects/genres

function api_v1_projects_genres($method, $data, $query_params)
function api_v1_projects_genres(string $method, array $data, array $query_params)
{
$genres = ProjectSearchForm::genre_options();
unset($genres['']);
Expand All @@ -629,7 +629,7 @@ function api_v1_projects_genres($method, $data, $query_params)
//---------------------------------------------------------------------------
// projects/languages

function api_v1_projects_languages($method, $data, $query_params)
function api_v1_projects_languages(string $method, array $data, array $query_params)
{
$languages = ProjectSearchForm::language_options();
unset($languages['']);
Expand All @@ -639,7 +639,7 @@ function api_v1_projects_languages($method, $data, $query_params)
//---------------------------------------------------------------------------
// projects/states

function api_v1_projects_states($method, $data, $query_params)
function api_v1_projects_states(string $method, array $data, array $query_params)
{
$states = ProjectSearchForm::state_options();
unset($states['']);
Expand All @@ -649,15 +649,15 @@ function api_v1_projects_states($method, $data, $query_params)
//---------------------------------------------------------------------------
// projects/pagerounds

function api_v1_projects_pagerounds($method, $data, $query_params)
function api_v1_projects_pagerounds(string $method, array $data, array $query_params)
{
return array_merge(["OCR"], Rounds::get_ids());
}

//---------------------------------------------------------------------------
// projects/charsuites

function api_v1_projects_charsuites($method, $data, $query_params)
function api_v1_projects_charsuites(string $method, array $data, array $query_params)
{
$enabled_filter = _get_enabled_filter($query_params);
if ($enabled_filter === null) {
Expand Down Expand Up @@ -686,7 +686,7 @@ function api_v1_projects_charsuites($method, $data, $query_params)
//---------------------------------------------------------------------------
// projects/specialdays

function api_v1_projects_specialdays($method, $data, $query_params)
function api_v1_projects_specialdays(string $method, array $data, array $query_params)
{
$return_data = [];

Expand Down Expand Up @@ -720,7 +720,7 @@ function api_v1_projects_specialdays($method, $data, $query_params)
//---------------------------------------------------------------------------
// projects/imagesources

function api_v1_projects_imagesources($method, $data, $query_params)
function api_v1_projects_imagesources(string $method, array $data, array $query_params): array
{
$return_data = [];

Expand Down Expand Up @@ -755,7 +755,7 @@ function api_v1_projects_imagesources($method, $data, $query_params)
//---------------------------------------------------------------------------
// projects/holdstates

function api_v1_projects_holdstates($method, $data, $query_params)
function api_v1_projects_holdstates(string $method, array $data, array $query_params): array
{
return Project::get_holdable_states();
}
Expand All @@ -780,7 +780,7 @@ function _get_enabled_filter($query_params)
// Proofreading functions

// checkout a page
function api_v1_project_checkout($method, array $data, array $query_params)
function api_v1_project_checkout(string $method, array $data, array $query_params): array
{
try {
$project = $data[":projectid"];
Expand All @@ -795,14 +795,14 @@ function api_v1_project_checkout($method, array $data, array $query_params)
}
}

function api_v1_project_validatetext(string $method, array $data, array $query_params)
function api_v1_project_validatetext(string $method, array $data, array $query_params): array
{
$project = $data[":projectid"];
$invalid_characters = $project->find_invalid_characters(receive_project_text_from_request_body('text'));
return ["invalid_chars" => $invalid_characters];
}

function api_v1_project_wordcheck($method, $data, array $query_params)
function api_v1_project_wordcheck(string $method, array $data, array $query_params): array
{
$project = $data[":projectid"];
$accepted_words = receive_data_from_request_body("accepted_words") ?? [];
Expand Down Expand Up @@ -832,6 +832,8 @@ function api_v1_project_pickersets(string $method, array $data, array $query_par
return $verbose_pickersets;
}

// TODO(jchaffraix): Refine this return once all callees have been typed.
/** @return mixed */
function api_v1_project_page(string $method, array $data, array $query_params)
{
global $pguser;
Expand Down Expand Up @@ -894,7 +896,7 @@ function api_v1_project_page_wordcheck(string $method, array $data, array $query
}
}

function validate_project_state($project, $state)
function validate_project_state(Project $project, ?string $state): void
{
if (null === $state) {
throw new InvalidValue("No project state found in request.");
Expand All @@ -913,7 +915,7 @@ function validate_project_state($project, $state)
}
}

function validate_page_state($project_page, $page_state)
function validate_page_state(ProjectPage $project_page, ?string $page_state): void
{
if (null === $page_state) {
throw new InvalidValue("No page state found in request.");
Expand Down Expand Up @@ -941,7 +943,7 @@ function receive_project_text_from_request_body(): string
return $page_text;
}

function receive_data_from_request_body($field)
function receive_data_from_request_body(string $field)
{
$request_data = api_get_request_body();
return $request_data[$field] ?? null;
Expand Down
Loading

0 comments on commit 3b03367

Please sign in to comment.