rosavox/lib/alveolata/api/functions.php
2025-05-23 07:33:29 +00:00

448 lines
12 KiB
PHP

<?php
namespace alveolata\api;
// require_once(DIR_ALVEOLATA . '/definitions.php');
require_once(DIR_ALVEOLATA . '/log/functions.php');
require_once(DIR_ALVEOLATA . '/file/functions.php');
require_once(DIR_ALVEOLATA . '/type/functions.php');
require_once(DIR_ALVEOLATA . '/string/functions.php');
require_once(DIR_ALVEOLATA . '/markdown/functions.php');
require_once(DIR_ALVEOLATA . '/api/types.php');
/**
* @author Christian Fraß <frass@greenscale.de>
* @return struct_subject
*/
function make(
string $name = 'API',
array $infoparagraphs = []
) : struct_subject
{
return (new struct_subject($name, [], $infoparagraphs));
}
/**
* @param struct_subject $subject
* @param string $name
* @param array $options {
* record<
* ?active:function<(null|string),boolean>,
* ?title:(null|string),
* ?execution:function<(null|string),any,any,any>,
* ?restriction:function<(null|string),any,any>,
* ?input_type:function<(null|string),any>,
* ?output_type:function<(null|string),any>,
* ?description:string,
* ?custom:any,
* >
* }
* @author Christian Fraß <frass@greenscale.de>
*/
function register(
struct_subject $subject,
string $name,
array $options = []
) : void
{
$options = array_merge(
[
'active' => function ($version) {
return true;
},
'execution' => function ($version, $environment, $input) {
throw (new \Exception('not implemented'));
},
'restriction' => function ($version, $environment) {
return true;
},
'input_type' => function ($version) {
return ['kind' => 'any'];
},
'output_type' => function ($version) {
return ['kind' => 'any'];
},
'title' => null,
'description' => null,
'custom' => null,
],
$options
);
if (array_key_exists($name, $subject->actions)) {
throw (new exception_action_already_registered($name));
}
else {
$action = new struct_action(
$name,
$options['active'],
$options['execution'],
$options['restriction'],
fn($version) => \alveolata\type\make($options['input_type']($version)),
fn($version) => \alveolata\type\make($options['output_type']($version)),
$options['title'],
$options['description'],
$options['custom']
);
$subject->actions[$action->name] = $action;
}
}
/**
* @param struct_subject $subject
* @param string $action_name
* @param ?array $options {
* record<
* ?version:(null|string),
* ?input:any,
* ?environment:map<(null|string),any>,
* ?checklevel_restriction:&enum_checklevel,
* ?checklevel_input:&enum_checklevel,
* ?checklevel_output:&enum_checklevel,
* >
* }
* @return array {record<custom:any,output:any>}
* @author Christian Fraß <frass@greenscale.de>
*/
function call_with_custom(
struct_subject $subject,
string $action_name,
array $options = []
) : array
{
$options = array_merge(
[
'version' => null,
'input' => null,
'environment' => [],
'checklevel_restriction' => enum_checklevel::hard,
'checklevel_input' => enum_checklevel::soft,
'checklevel_output' => enum_checklevel::soft,
],
$options
);
if (! array_key_exists($action_name, $subject->actions)) {
throw (new exception_action_missing($action_name));
}
else {
$action = $subject->actions[$action_name];
if (! ($action->active)($options['version'])) {
throw (new exception_action_missing($action_name));
}
else {
switch ($options['checklevel_restriction']) {
case enum_checklevel::none: {
// do nothing
break;
}
case enum_checklevel::soft: {
if (! ($action->restriction)($options['version'], $options['environment'])) {
\alveolata\log\warning(
'api_permission_denied',
[
'version' => $options['version'],
'environment' => $options['environment'],
'action_name' => $action->name,
]
);
}
break;
}
default:
case enum_checklevel::hard: {
if (! ($action->restriction)($options['version'], $options['environment'])) {
throw (new exception_denied());
}
break;
}
}
switch ($options['checklevel_input']) {
case enum_checklevel::none: {
// do nothing
break;
}
default:
case enum_checklevel::soft: {
$input_type = ($action->input_type)($options['version']);
$flaws = \alveolata\type\investigate($input_type, $options['input']);
if (! empty($flaws)) {
\alveolata\log\warning(
'api_input_flawed',
[
'type_expected' => \alveolata\type\to_string($input_type),
// 'type_derived' => \alveolata\type\derive($options['input']),
'value' => $options['input'],
'flaws' => $flaws,
]
);
}
break;
}
case enum_checklevel::hard: {
$input_type = ($action->input_type)($options['version']);
$flaws = \alveolata\type\investigate($input_type, $options['input']);
if (! empty($flaws)) {
throw (new exception_invalid_input($input_type, $options['input'], $flaws));
}
break;
}
}
$output = ($action->execution)($options['version'], $options['environment'], $options['input']);
switch ($options['checklevel_output']) {
case enum_checklevel::none: {
// do nothing
break;
}
default:
case enum_checklevel::soft: {
$output_type = ($action->output_type)($options['version']);
$flaws = \alveolata\type\investigate($output_type, $output);
if (! empty($flaws)) {
\alveolata\log\warning(
'api_output_flawed',
[
'type_expected' => \alveolata\type\to_string($output_type),
// 'type_derived' => \alveolata\type\derive($output_type),
'value' => $output,
'flaws' => $flaws,
]
);
}
break;
}
case enum_checklevel::hard: {
$output_type = ($action->output_type)($options['version']);
$flaws = \alveolata\type\investigate($output_type, $output);
if (! empty($flaws)) {
throw (new exception_invalid_output($output_type, $output, $flaws));
}
break;
}
}
return [
'custom' => $action->custom,
'output' => $output,
];
}
}
}
/**
* @param struct_subject $subject
* @param string $action_name
* @param ?array $options {
* record<
* ?version:(null|string),
* ?input:any,
* ?environment:map<string,any>,
* ?checklevel_restriction:&enum_checklevel,
* ?checklevel_input:&enum_checklevel,
* ?checklevel_output:&enum_checklevel,
* >
* }
* @return mixed {any}
* @author Christian Fraß <frass@greenscale.de>
*/
function call(
struct_subject $subject,
string $action_name,
array $options = []
)
{
$options = array_merge(
[
'version' => null,
'input' => null,
'environment' => [],
'checklevel_restriction' => enum_checklevel::hard,
'checklevel_input' => enum_checklevel::soft,
'checklevel_output' => enum_checklevel::soft,
],
$options
);
return call_with_custom(
$subject,
$action_name,
[
'version' => $options['version'],
'input' => $options['input'],
'environment' => $options['environment'],
'checklevel_restriction' => $options['checklevel_restriction'],
'checklevel_input' => $options['checklevel_input'],
'checklevel_output' => $options['checklevel_output'],
]
)['output'];
}
/**
* @param struct_subject $subject
* @param array $options {
* record<
* ?version:(null|string),
* >
* }
* @return string
* @author Christian Fraß <frass@greenscale.de>
*/
function doc_markdown(
struct_subject $subject,
array $options = []
) : string
{
$options = array_merge(
[
'version' => null,
],
$options
);
$markdown = '';
$markdown .= \alveolata\markdown\headline(1, $subject->name);
{
{
$markdown .= \alveolata\markdown\headline(2, 'Infos');
foreach ($subject->infoparagraphs as $infoparagraph) {
$markdown .= \alveolata\markdown\paragraph($infoparagraph);
}
$markdown .= \alveolata\markdown\sectionend();
}
{
$markdown .= \alveolata\markdown\headline(2, 'Methods');
foreach ($subject->actions as $name => $action) {
if (! ($action->active)($options['version'])) {
// do nothing
}
else {
$markdown .= \alveolata\markdown\headline(3, $action->name);
// description
{
$markdown .= \alveolata\markdown\headline(4, 'Description');
$markdown .= \alveolata\markdown\paragraph($action->description);
$markdown .= \alveolata\markdown\sectionend();
}
// input type
{
$markdown .= \alveolata\markdown\headline(4, 'Input Type');
$markdown .= \alveolata\markdown\code(\alveolata\type\to_string(($action->input_type)($options['version']), true));
$markdown .= \alveolata\markdown\sectionend();
}
// output type
{
$markdown .= \alveolata\markdown\headline(4, 'Output Type');
$markdown .= \alveolata\markdown\code(\alveolata\type\to_string(($action->output_type)($options['version']), true));
$markdown .= \alveolata\markdown\sectionend();
}
$markdown .= \alveolata\markdown\sectionend();
$markdown .= \alveolata\markdown\rule();
}
}
$markdown .= \alveolata\markdown\sectionend();
}
$markdown .= \alveolata\markdown\sectionend();
}
return $markdown;
}
/**
* @param struct_subject $subject
* @param array $options {
* record<
* ?version:(null|string),
* ?hue:integer,
* >
* }
* @return string
* @author Christian Fraß <frass@greenscale.de>
*/
function doc_html(
struct_subject $subject,
array $options = []
) : string
{
$options = array_merge(
[
'version' => null,
'hue' => 150,
],
$options,
);
$expose_type = (fn($type) => htmlentities(
str_replace(
"\t",
' ',
\alveolata\type\to_string($type, true)
)
));
$template_stylesheet = \alveolata\file\read(__DIR__ . '/template-stylesheet.css.tpl');
$template_doc = \alveolata\file\read(__DIR__ . '/template-doc.html.tpl');
$template_infoparagraph = \alveolata\file\read(__DIR__ . '/template-infoparagraph.html.tpl');
$template_tocentry = \alveolata\file\read(__DIR__ . '/template-tocentry.html.tpl');
$template_action = \alveolata\file\read(__DIR__ . '/template-action.html.tpl');
return \alveolata\string\coin(
$template_doc,
[
'style' => \alveolata\string\coin(
$template_stylesheet,
[
'hue' => strval($options['hue']),
]
),
'name' => $subject->name,
'version' => ($options['version'] ?? '-'),
'infoparagraphs' => \alveolata\string\join(
\alveolata\list_\map(
$subject->infoparagraphs,
fn($infoparagraph) => \alveolata\string\coin(
$template_infoparagraph,
[
'content' => $infoparagraph,
]
)
),
"\n"
),
'tocentries' => \alveolata\string\join(
\alveolata\list_\map(
$subject->actions,
fn($action) => \alveolata\string\coin(
$template_tocentry,
[
'name' => $action->name,
'title' => ($action->title ?? $action->name),
'href' => sprintf('#%s', $action->name),
]
)
),
"\n"
),
'actions' => \alveolata\string\join(
\alveolata\list_\map(
\alveolata\list_\filter(
$subject->actions,
fn($action) => ($action->active)($options['version'])
),
fn($action) => \alveolata\string\coin(
$template_action,
[
'name' => $action->name,
'anchor' => sprintf('%s', $action->name),
'href' => sprintf('#%s', $action->name),
'description' => $action->description,
'input_type' => $expose_type(($action->input_type)($options['version'])),
'output_type' => $expose_type(($action->output_type)($options['version'])),
]
)
),
"\n"
),
]
);
}
?>