448 lines
12 KiB
PHP
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"
|
|
),
|
|
]
|
|
);
|
|
}
|
|
|
|
?>
|