diff --git a/lib/alveolata/.gitignore b/lib/alveolata/.gitignore new file mode 100644 index 0000000..ad05c1d --- /dev/null +++ b/lib/alveolata/.gitignore @@ -0,0 +1,2 @@ +.geany + diff --git a/lib/alveolata/accesscontrol/functions.php b/lib/alveolata/accesscontrol/functions.php new file mode 100644 index 0000000..ef3e091 --- /dev/null +++ b/lib/alveolata/accesscontrol/functions.php @@ -0,0 +1,87 @@ +,bool>>} + */ + public $getters; + + + /** + * @var array {record>>,concrete:map>>>>} + */ + public $acl; + + + /** + */ + public function __construct( + array $getters, + array $acl + ) + { + $this->getters = $getters; + $this->acl = $acl; + } + +} + + +/** + */ +function make( + array $getters, + array $acl +) +{ + return ( + new struct_subject( + $getters, + $acl + ) + ); +} + + +/** + */ +function check( + struct_subject $subject, + string $action, + $state = null +) : bool +{ + $acl_section = ( + $subject->acl['concrete'][$action] + ?? $subject->acl['default'] + ?? [] + ); + return \alveolata\list_\some( + $acl_section, + function (array $acl_section_sub) use ($subject, $state) : bool { + return \alveolata\list_\every( + $acl_section_sub, + function (array $entry) use ($subject, $state) : bool { + if (! array_key_exists($entry['type'], $subject->getters)) { + throw (new \Exception('unhandled ACL check type: ' . $entry['type'])); + } + else { + $getter = $subject->getters[$entry['type']]; + return $getter($entry['parameters'] ?? null, $state); + } + } + ); + } + ); +} + + ?> diff --git a/lib/alveolata/accesscontrol/test.spec.php b/lib/alveolata/accesscontrol/test.spec.php new file mode 100644 index 0000000..30d67eb --- /dev/null +++ b/lib/alveolata/accesscontrol/test.spec.php @@ -0,0 +1,52 @@ + 'alveolata', + 'sections' => [ + [ + 'name' => 'accesscontrol', + 'sections' => [ + [ + 'name' => 'check', + 'setup' => function (&$environment) use ($data) { + $environment['subject'] = \alveolata\accesscontrol\make( + [ + 'password' => function ($parameters, $state) { + return ($state['password'] === $parameters['value']); + }, + ], + $data['check']['parameters']['acl'] + ); + }, + 'cases' => array_map( + function (array $case_raw) : array { + return [ + 'name' => $case_raw['name'], + 'procedure' => function ($assert, &$environment) use ($case_raw) { + // execution + $resultActual = \alveolata\accesscontrol\check( + $environment['subject'], + $case_raw['input']['action'], + $case_raw['input']['state'], + ); + // assertions + $resultExpected = $case_raw['output']; + $assert->equal($resultActual, $resultExpected); + }, + ]; + }, + $data['check']['cases'] + ), + ], + ] + ] + ] + ] +); + + ?> diff --git a/lib/alveolata/accesscontrol/testdata.json b/lib/alveolata/accesscontrol/testdata.json new file mode 100644 index 0000000..cd0f2e4 --- /dev/null +++ b/lib/alveolata/accesscontrol/testdata.json @@ -0,0 +1,61 @@ +{ + "check": { + "parameters": { + "acl": { + "default": [ + ], + "concrete": { + "bar": [ + ], + "baz": [ + [ + {"type": "password", "parameters": {"value": "correct"}} + ] + ] + } + } + }, + "cases": [ + { + "name": "should fall back to default if the action is unknown", + "input": { + "action": "qux", + "state": {} + }, + "output": false + }, + { + "name": "should forbid access if the state is bad and no checker is given", + "input": { + "action": "bar", + "state": {"password": "wrong"} + }, + "output": false + }, + { + "name": "should forbid access if the state is good but no checker is given", + "input": { + "action": "bar", + "state": {"password": "correct"} + }, + "output": false + }, + { + "name": "should forbid access if the state is bad but does not match the checker", + "input": { + "action": "baz", + "state": {"password": "wrong"} + }, + "output": false + }, + { + "name": "should grant access if the state is good and matches the checker", + "input": { + "action": "baz", + "state": {"password": "correct"} + }, + "output": true + } + ] + } +} diff --git a/lib/alveolata/algorithm/functions.php b/lib/alveolata/algorithm/functions.php new file mode 100644 index 0000000..ee3a463 --- /dev/null +++ b/lib/alveolata/algorithm/functions.php @@ -0,0 +1,79 @@ +>} + * @return array {list<§x>} + * @throw \Exception if not possible + */ +function topsort( + array $order +) : array +{ + if (empty($order)) { + return []; + } + else { + foreach ($order as $member => $dependencies) { + if (count($dependencies) <= 0) { + $order_ = []; + foreach ($order as $member_ => $dependencies_) { + if ($member === $member_) { + // do nothing + } + else { + $order_[$member_] = \alveolata\list_\filter/*<§x,bool>*/( + $dependencies_, + function ($dependency_) use ($member) : bool { + return (! ($member === $dependency_)); + } + ); + } + } + return array_merge([$member], topsort($order_)); + } + } + throw (new \Exception('not sortable')); + } +} + + +/** + * calculates the levenshtein distance between two strings + */ +function levenshtein(string $x, string $y) : int +{ + $u = str_split($x); + $v = str_split($y); + + // init + $matrix = array_map( + fn($i) => array_map(fn($j) => null, \alveolata\list_\sequence(count($u)+1)), + \alveolata\list_\sequence(count($v)+1) + ); + foreach (\alveolata\list_\sequence(count($u)+1) as $i) {$matrix[$i][0] = $i;} + foreach (\alveolata\list_\sequence(count($v)+1) as $j) {$matrix[0][$j] = $j;} + + // feed + foreach (\alveolata\list_\sequence(count($u)) as $i) { + foreach (\alveolata\list_\sequence(count($v)) as $j) { + $matrix[$i+1][$j+1] = min( + min( + ($matrix[$i+1][$j+0] + 1), + ($matrix[$i+0][$j+1] + 1) + ), + ($matrix[$i+0][$j+0] + (($u[$i] === $v[$j]) ? 0 : 1)) + ); + } + } + + // return + return $matrix[count($u)][count($v)]; +} + + + ?> diff --git a/lib/alveolata/algorithm/test.spec.php b/lib/alveolata/algorithm/test.spec.php new file mode 100644 index 0000000..de618e4 --- /dev/null +++ b/lib/alveolata/algorithm/test.spec.php @@ -0,0 +1,74 @@ + 'alveolata', + 'sections' => [ + [ + 'name' => 'algorithm', + 'sections' => [ + [ + 'name' => 'topsort', + 'cases' => [ + [ + 'name' => 'test', + 'procedure' => function ($assert) { + $order = [ + 'qux' => ['bar','baz'], + 'bar' => ['foo'], + 'foo' => [], + 'baz' => ['foo'], + ]; + $result_actual = \alveolata\algorithm\topsort($order); + $results_possible = [ + ['foo','bar','baz','qux'], + ['foo','baz','bar','qux'], + ]; + $found = false; + foreach ($results_possible as $result_expected) { + if ($result_actual === $result_expected) { + $found = true; + break; + } + } + $assert->equal($found, true); + } + ], + ] + ], + [ + 'name' => 'levenshtein', + 'cases' => [ + [ + 'name' => 'test1', + 'procedure' => function ($assert) { + $result = \alveolata\algorithm\levenshtein('', ''); + $assert->equal($result, 0); + } + ], + [ + 'name' => 'test2', + 'procedure' => function ($assert) { + $result = \alveolata\algorithm\levenshtein('tier', 'tor'); + $assert->equal($result, 2); + } + ], + [ + 'name' => 'test3', + 'procedure' => function ($assert) { + $result = \alveolata\algorithm\levenshtein('tier', 'vier'); + $assert->equal($result, 1); + } + ] + ] + ], + ] + ], + ] + ] +); + + ?> diff --git a/lib/alveolata/api/functions.php b/lib/alveolata/api/functions.php new file mode 100644 index 0000000..d061210 --- /dev/null +++ b/lib/alveolata/api/functions.php @@ -0,0 +1,448 @@ + + * @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ß + */ +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} + * @author Christian Fraß + */ +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, + * ?checklevel_restriction:&enum_checklevel, + * ?checklevel_input:&enum_checklevel, + * ?checklevel_output:&enum_checklevel, + * > + * } + * @return mixed {any} + * @author Christian Fraß + */ +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ß + */ +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ß + */ +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" + ), + ] + ); +} + + ?> diff --git a/lib/alveolata/api/template-action.html.tpl b/lib/alveolata/api/template-action.html.tpl new file mode 100644 index 0000000..715bf41 --- /dev/null +++ b/lib/alveolata/api/template-action.html.tpl @@ -0,0 +1,23 @@ +
  • + +
    +
    +
    Description
    +
    {{description}}
    +
    +
    +
    Input
    +
    {{input_type}}
    +
    +
    +
    Output
    +
    {{output_type}}
    +
    +
    +
  • + diff --git a/lib/alveolata/api/template-doc.html.tpl b/lib/alveolata/api/template-doc.html.tpl new file mode 100644 index 0000000..a78f6c4 --- /dev/null +++ b/lib/alveolata/api/template-doc.html.tpl @@ -0,0 +1,32 @@ + + + + + + + {{name}} — API Documentation + + + +
    +

    {{name}} — API Documentation

    +
    + version: {{version}} +
    +
    + {{infoparagraphs}} +
    +
    +
      + {{tocentries}} +
    +
    +
      + {{actions}} +
    +
    + + + diff --git a/lib/alveolata/api/template-infoparagraph.html.tpl b/lib/alveolata/api/template-infoparagraph.html.tpl new file mode 100644 index 0000000..de30ef3 --- /dev/null +++ b/lib/alveolata/api/template-infoparagraph.html.tpl @@ -0,0 +1 @@ +

    {{content}}

    diff --git a/lib/alveolata/api/template-stylesheet.css.tpl b/lib/alveolata/api/template-stylesheet.css.tpl new file mode 100644 index 0000000..756629c --- /dev/null +++ b/lib/alveolata/api/template-stylesheet.css.tpl @@ -0,0 +1,107 @@ +html { + padding: 0; + margin: 0; + font-family: sans; + background-color: hsl({{hue}},0%,0%); + color: hsl({{hue}},0%,100%); +} + +body { + max-width: 960px; + padding: 8px; + margin: auto; + background-color: hsl({{hue}},0%,5%); + color: hsl({{hue}},0%,75%); +} + +a { + text-decoration: none; + color: hsl({{hue}},100%,25%); +} + +a:hover { + text-decoration: none; + color: hsl({{hue}},100%,50%); +} + +.api { + padding: 8px; +} + +.api_tocentry { + list-style-type: "» "; +} + +.api_actionlist { + padding: 0; + margin: 0; + list-style-type: none; +} + +.api_action { + margin: 24px; + padding: 12px; + border-radius: 4px; + background-color: hsl({{hue}},0%,10%); +} + +.api_action_head { + display: flex; + flex-direction: row; + flex-wrap: nowrap; +} + +.api_action_head_left { + flex-basis: 90%; + flex-grow: 1; + flex-shrink: 0; +} + +.api_action_head_right { + flex-basis: 10%; + flex-grow: 0; + flex-shrink: 0; + text-align: right; +} + +.api_action_stuff { + display: flex; + flex-direction: row; + flex-wrap: wrap; +} + +.api_action_snippet { + margin: 8px; + padding: 8px; + border-radius: 4px; + background-color: hsl({{hue}},0%,15%); +} + +.api_action_snippet_head { + font-weight: bold; + margin-bottom: 4px; + color: hsl({{hue}},0%,10%); + text-transform: uppercase; +} + +.api_action_description { + flex-shrink: 1; + flex-basis: 90%; + flex-grow: 1; +} + +.api_action_input { + flex-shrink: 1; + flex-basis: 45%; + flex-grow: 1; +} + +.api_action_output { + flex-shrink: 1; + flex-basis: 45%; + flex-grow: 1; +} + +.api_version { + text-align: right; +} diff --git a/lib/alveolata/api/template-tocentry.html.tpl b/lib/alveolata/api/template-tocentry.html.tpl new file mode 100644 index 0000000..6e7fa49 --- /dev/null +++ b/lib/alveolata/api/template-tocentry.html.tpl @@ -0,0 +1,4 @@ +
  • + {{title}} ({{name}}) +
  • + diff --git a/lib/alveolata/api/test.spec.php b/lib/alveolata/api/test.spec.php new file mode 100644 index 0000000..15f8f85 --- /dev/null +++ b/lib/alveolata/api/test.spec.php @@ -0,0 +1,124 @@ + 'alveolata', + 'sections' => [ + [ + 'name' => 'api', + 'sections' => [ + [ + 'name' => 'register', + 'cases' => [ + [ + 'name' => 'throw exception on multiple registration for same name', + 'procedure' => function ($assert) { + $subject = \alveolata\api\make(); + $assert->crashes( + function () use (&$subject) { + \alveolata\api\register( + $subject, + 'foo' + ); + \alveolata\api\register( + $subject, + 'foo' + ); + } + ); + } + ], + ] + ], + [ + 'name' => 'call', + 'setup' => function (&$env) { + $subject = \alveolata\api\make(); + \alveolata\api\register( + $subject, + 'increment', + [ + 'execution' => function ($version, $environment, $input) { + $output = ($input + 1); + return $output; + }, + 'restriction' => function ($version, $environment) { + return true; + }, + 'input_type' => function ($version) { + return [ + 'kind' => 'integer', + ]; + }, + 'output_type' => function ($version) { + return [ + 'kind' => 'integer', + ]; + }, + ] + ); + $env['subject'] = $subject; + }, + 'cases' => [ + [ + 'name' => 'throw exception on malformed input', + 'procedure' => function ($assert, &$env) { + // execution & assertions + { + $input = '42'; + $assert->crashes( + function () use ($env, $input) { + $output_actual = \alveolata\api\call( + $env['subject'], + 'increment', + [ + 'input' => $input, + 'environment' => [], + 'checklevel_restriction' => \alveolata\api\enum_checklevel::hard, + 'checklevel_input' => \alveolata\api\enum_checklevel::hard, + 'checklevel_output' => \alveolata\api\enum_checklevel::hard + ] + ); + }, + \alveolata\api\exception_invalid_input::class + ); + } + }, + ], + [ + 'name' => 'all_good', + 'procedure' => function ($assert, &$env) { + // execution + { + $input = 42; + $output_actual = \alveolata\api\call( + $env['subject'], + 'increment', + [ + 'input' => $input, + 'environment' => [], + 'checklevel_restriction' => \alveolata\api\enum_checklevel::hard, + 'checklevel_input' => \alveolata\api\enum_checklevel::hard, + 'checklevel_output' => \alveolata\api\enum_checklevel::hard, + ] + ); + } + // assertions + { + $output_expected = 43; + $assert->equal($output_actual, $output_expected); + } + }, + ], + ] + ], + ] + ] + ] + ] +); + diff --git a/lib/alveolata/api/types.php b/lib/alveolata/api/types.php new file mode 100644 index 0000000..d952a49 --- /dev/null +++ b/lib/alveolata/api/types.php @@ -0,0 +1,318 @@ + + */ +class exception_action_already_registered extends \Exception { + + /** + */ + public function __construct( + string $action_name + ) + { + parent::__construct( + \sprintf('action "%s" already registered', $action_name) + ); + } + +} + + +/** + * @author Christian Fraß + */ +class exception_action_missing extends \Exception { + + /** + */ + public function __construct( + string $action_name + ) + { + parent::__construct( + \sprintf('no action "%s"', $action_name) + ); + } + +} + + +/** + * @author Christian Fraß + */ +class exception_denied extends \Exception { + + /** + */ + public function __construct( + ) + { + parent::__construct( + \sprintf('permission denied') + ); + } + +} + + +/** + * @author Christian Fraß + */ +class exception_flawed_value extends \Exception { + + /** + * @var \alveolata\type\interface_type + */ + private $type; + + + /** + * @var string + */ + private $value; + + + /** + * @var array {list} + */ + private $flaws; + + + /** + * @param mixed $input {any} + */ + public function __construct( + string $name, + \alveolata\type\interface_type $type_expected, + $value, + array $flaws + ) + { + parent::__construct( + \alveolata\string\coin( + 'malformed {{name}} | {{details}}', + [ + 'name' => $name, + 'details' => \json_encode( + [ + 'type_expected' => \alveolata\type\to_string($type_expected), + 'value' => $value, + 'flaws' => $flaws, + ] + ), + ] + ) + ); + $this->type_expected = $type_expected; + $this->value = $value; + $this->flaws = $flaws; + } + + + /** + * @return array {list} + */ + public function get_flaws( + ) : array + { + return $this->flaws; + } + +} + + +/** + * @author Christian Fraß + */ +class exception_invalid_input extends exception_flawed_value { + + /** + * @param mixed $input {any} + */ + public function __construct( + \alveolata\type\interface_type $type_expected, + $input, + array $flaws + ) + { + parent::__construct( + 'input', + $type_expected, + $input, + $flaws + ); + } + +} + + +/** + * @author Christian Fraß + */ +class exception_invalid_output extends exception_flawed_value { + + /** + * @param mixed $output {any} + */ + public function __construct( + \alveolata\type\interface_type $type_expected, + $output, + array $flaws + ) + { + parent::__construct( + 'output', + $type_expected, + $output, + $flaws + ); + } + +} + + +/** + * @author Christian Fraß + */ +class enum_checklevel { + public const none = 'none'; + public const soft = 'soft'; + public const hard = 'hard'; +} + + +/** + * @author Christian Fraß + */ +class struct_action { + + /** + * @var string supposed to be unique + */ + public $name; + + + /** + * @var \Closure {function} + */ + public $active; + + + /** + * @var \Closure {function,any>} + */ + public $execution; + + + /** + * @var \Closure {function} + */ + public $restriction; + + + /** + * @var \Closure {function} + */ + public $input_type; + + + /** + * @var \Closure {function} + */ + public $output_type; + + + /** + * @var ?string supposed to be human readable + */ + public $title; + + + /** + * @var ?string + */ + public $description; + + + /** + * @var mixed + */ + public $custom; + + + /** + */ + public function __construct( + string $name, + \Closure $active, + \Closure $execution, + \Closure $restriction, + \Closure $input_type, + \Closure $output_type, + ?string $title, + ?string $description, + $custom + ) + { + $this->name = $name; + $this->active = $active; + $this->execution = $execution; + $this->restriction = $restriction; + $this->input_type = $input_type; + $this->output_type = $output_type; + $this->title = $title; + $this->description = $description; + $this->custom = $custom; + } + +} + + +/** + * @author Christian Fraß + */ +class struct_subject { + + /** + * @var string + */ + public $name; + + + /** + * @var array {map} + * @author Christian Fraß + */ + public $actions; + + + /** + * @var array {list} + */ + public $infoparagraphs; + + + /** + * @author Christian Fraß + */ + public function __construct( + string $name, + array $actions, + array $infoparagraphs = [] + ) + { + $this->name = $name; + $this->actions = $actions; + $this->infoparagraphs = $infoparagraphs; + } + +} + + ?> diff --git a/lib/alveolata/api/wrapper-class.php b/lib/alveolata/api/wrapper-class.php new file mode 100644 index 0000000..89687c2 --- /dev/null +++ b/lib/alveolata/api/wrapper-class.php @@ -0,0 +1,47 @@ + + */ +class class_api +{ + + /** + * @var struct_subject + * @author Christian Fraß + */ + private $subject; + + + /** + * @author Christian Fraß + */ + private function __construct(struct_subject $subject) {$this->subject = $subject;} + + + /** + * @author Christian Fraß + */ + public static function make() : class_api {return (new class_api(make()));} + + + /** + * implementations + * + * @author Christian Fraß + */ + public function register(array $data_given) {register($this->subject, $data_given);} + public function has(string $action_name) : bool {return has($this->subject, $action_name);} + public function may(string $action_name, array $options = []) : array {return may($this->subject, $action_name, $options);} + public function call(string $action_name, array $options = []) {return call($this->subject, $action_name, $options);} + public function doc(array $options = []) : string {return doc($this->subject, $options);} + +} + + ?> diff --git a/lib/alveolata/args/functions.php b/lib/alveolata/args/functions.php new file mode 100644 index 0000000..f7c3762 --- /dev/null +++ b/lib/alveolata/args/functions.php @@ -0,0 +1,226 @@ + + */ +function _regexp_match( + string $pattern, + string $subject +) +{ + $matches = []; + $pattern_ = sprintf('/%s/', $pattern); + $result = preg_match($pattern_, $subject, $matches); + if ($result === 0) { + return null; + } + else { + return array_slice($matches, 1); + } +} + + +/** + * @param list<§x> $list + * @param function<§x,§y> $function + * @return list<§y> + */ +function _list_map( + array $list, + \Closure $function +) : array +{ + $list_ = []; + for ($index = 0; $index < count($list); $index += 1) { + $value = $list[$index]; + $value_ = $function($value, $index); + array_push($list_, $value_); + } + return $list_; +} + + +/** + * @param map $map + * @param function<§x,§y> $function + * @return map + */ +function _map_map( + array $map, + \Closure $function +) : array +{ + $map_ = []; + foreach ($map as $key => $value) { + $key_ = $key; + $value_ = $function($value, $key); + $map_[$key_] = $value_; + } + return $map_; +} + + +/** + * @param record>>,named:map>>> $spec_raw + * @param list $args_raw + * @return map + * @author Christian Fraß + */ +function parse( + array $spec_raw, + array $args_raw, + array $settings_given = [] +) : array +{ + $settings_default = [ + 'supress_warnings' => false, + ]; + $settings = array_merge( + $settings_default, + $settings_given + ); + // spec refinement + { + $spec = [ + 'positioned_mandatory' => _list_map( + $spec_raw['positioned_mandatory'], + function ($entry_raw, $index) { + return [ + 'target' => ( + array_key_exists('target', $entry_raw) + ? $entry_raw['target'] + : sprintf('_arg_%d', $index) + ), + 'processing' => ( + array_key_exists('processing', $entry_raw) + ? $entry_raw['processing'] + : (function ($value_raw) {return $value_raw;}) + ), + ]; + } + ), + 'positioned_optional' => _list_map( + $spec_raw['positioned_optional'], + function ($entry_raw, $index) use (&$spec_raw) { + return [ + 'target' => ( + array_key_exists('target', $entry_raw) + ? $entry_raw['target'] + : sprintf('_arg_%d', $index + count($spec_raw['positioned_mandatory'])) + ), + 'processing' => ( + array_key_exists('processing', $entry_raw) + ? $entry_raw['processing'] + : (function ($value_raw) {return $value_raw;}) + ), + 'default' => ( + array_key_exists('default', $entry_raw) + ? $entry_raw['default'] + : null + ), + ]; + } + ), + 'named' => _map_map( + $spec_raw['named'], + function ($entry_raw, $key) { + return [ + 'target' => ( + array_key_exists('target', $entry_raw) + ? $entry_raw['target'] + : $key + ), + 'processing' => ( + array_key_exists('processing', $entry_raw) + ? $entry_raw['processing'] + : (function ($value_raw) {return $value_raw;}) + ), + 'default' => ( + array_key_exists('default', $entry_raw) + ? $entry_raw['default'] + : null + ), + ]; + } + ), + ]; + } + $args = []; + // default values + { + foreach ($spec['positioned_optional'] as $entry) { + $args[$entry['target']] = $entry['default']; + } + foreach ($spec['named'] as $entry) { + $args[$entry['target']] = $entry['default']; + } + } + // parsing + { + $position = 0; + foreach ($args_raw as $arg_raw) { + $result = _regexp_match('--([^=]+)=([^=]+)', $arg_raw); + if ($result === null) { + if ($position < count($spec['positioned_mandatory'])) { + $entry = $spec['positioned_mandatory'][$position]; + $value_raw = $entry['processing']($arg_raw); + $value = $value_raw; + $args[$entry['target']] = $value; + } + else if ($position < count($spec['positioned_mandatory']) + count($spec['positioned_optional'])) { + $entry = $spec['positioned_optional'][$position - count($spec['positioned_optional'])]; + $value_raw = $entry['processing']($arg_raw); + $value = $value_raw; + $args[$entry['target']] = $value; + } + else { + if (! $settings['supress_warnings']) { + \alveolata\log\warning( + 'unrecognized positional argument', + [ + 'position' => $position, + 'value' => $arg_raw, + ] + ); + } + } + $position += 1; + } + else { + $name = $result[0]; + $value_raw = $result[1]; + if (array_key_exists($name, $spec['named'])) { + $entry = $spec['named'][$name]; + $value = $entry['processing']($value_raw); + $args[$entry['target']] = $value; + } + else { + if (! $settings['supress_warnings']) { + \alveolata\log\warning( + 'unrecognized named argument', + [ + 'name' => $name, + 'value' => $value_raw, + ] + ); + } + } + } + } + $left = array_slice($spec['positioned_mandatory'], $position); + if (count($left) > 0) { + throw (new \Exception(sprintf('%d missing mandatory positioned arguments', count($left)))); + } + } + return $args; +} + + diff --git a/lib/alveolata/args/test.spec.php b/lib/alveolata/args/test.spec.php new file mode 100644 index 0000000..e5f14bd --- /dev/null +++ b/lib/alveolata/args/test.spec.php @@ -0,0 +1,225 @@ + 'alveolata', + 'sections' => [ + [ + 'name' => 'args', + 'setup' => function (&$environment) { + $spec_raw = [ + 'positioned_mandatory' => [ + [ + 'target' => 'hagalaz', + ], + ], + 'positioned_optional' => [ + [ + 'target' => 'naudhiz', + 'default' => 'hau_mich_blau', + ], + ], + 'named' => [ + 'fehuz' => [ + // 'target' => 'fehuz', + 'default' => false, + 'processing' => function ($x) {return ($x === 'yes');}, + ], + 'uruz' => [ + // 'target' => 'uruz', + 'default' => 42, + 'processing' => function ($x) {return intval($x);}, + ], + 'thurisaz' => [ + // 'target' => 'thurisaz', + 'default' => 2.7182, + 'processing' => function ($x) {return floatval($x);}, + ], + 'ansuz' => [ + // 'target' => 'ansuz', + 'default' => 'math', + 'processing' => function ($x) {return ($x);}, + ], + ], + ]; + $environment['call'] = function ($args_raw) use (&$spec_raw) { + return \alveolata\args\parse( + $spec_raw, + $args_raw, + [ + 'supress_warnings' => false, + ] + ); + }; + }, + 'cleanup' => function (&$environment) { + }, + 'sections' => [ + [ + 'name' => 'parse', + 'cases' => [ + [ + 'name' => 'positined_mandatory_missing', + 'procedure' => function ($assert, $environment) { + // execution + $assert->crashes( + function () use (&$environment) { + $args = $environment['call']([]); + } + ); + } + ], + [ + 'name' => 'positined_mandatory_present', + 'procedure' => function ($assert, $environment) { + // execution + $args = $environment['call'](['foo']); + + // assertions + $assert->equal($args['hagalaz'], 'foo'); + } + ], + [ + 'name' => 'positined_optional_missing', + 'procedure' => function ($assert, $environment) { + // execution + $args = $environment['call'](['foo']); + + // assertions + $assert->equal($args['naudhiz'], 'hau_mich_blau'); + } + ], + [ + 'name' => 'positined_optional_present', + 'procedure' => function ($assert, $environment) { + // execution + $args = $environment['call'](['foo', 'bar']); + + // assertions + $assert->equal($args['naudhiz'], 'bar'); + } + ], + [ + 'name' => 'named_boolean_missing', + 'procedure' => function ($assert, $environment) { + // execution + $args = $environment['call'](['foo']); + + // assertions + $assert->equal($args['fehuz'], false); + } + ], + [ + 'name' => 'named_boolean_present_negative', + 'procedure' => function ($assert, $environment) { + // execution + $args = $environment['call'](['foo', '--fehuz=no']); + + // assertions + $assert->equal($args['fehuz'], false); + } + ], + [ + 'name' => 'named_boolean_present_positive', + 'procedure' => function ($assert, $environment) { + // execution + $args = $environment['call'](['foo', '--fehuz=yes']); + + // assertions + $assert->equal($args['fehuz'], true); + } + ], + [ + 'name' => 'named_integer_missing', + 'procedure' => function ($assert, $environment) { + // execution + $args = $environment['call'](['foo']); + + // assertions + $assert->equal($args['uruz'], 42); + } + ], + [ + 'name' => 'named_integer_present', + 'procedure' => function ($assert, $environment) { + // execution + $args = $environment['call'](['foo', '--uruz=7']); + + // assertions + $assert->equal($args['uruz'], 7); + } + ], + [ + 'name' => 'named_float_missing', + 'procedure' => function ($assert, $environment) { + // execution + $args = $environment['call'](['foo']); + + // assertions + $assert->equal($args['thurisaz'], 2.7182); + } + ], + [ + 'name' => 'named_float_present', + 'procedure' => function ($assert, $environment) { + // execution + $args = $environment['call'](['foo', '--thurisaz=3.1415']); + + // assertions + $assert->equal($args['thurisaz'], 3.1415); + } + ], + [ + 'name' => 'named_string_missing', + 'procedure' => function ($assert, $environment) { + // execution + $args = $environment['call'](['foo']); + + // assertions + $assert->equal($args['ansuz'], 'math'); + } + ], + [ + 'name' => 'named_string_present', + 'procedure' => function ($assert, $environment) { + // execution + $args = $environment['call'](['foo', '--ansuz=algebra']); + + // assertions + $assert->equal($args['ansuz'], 'algebra'); + } + ], + [ + 'name' => 'named_malformed', + 'procedure' => function ($assert, $environment) { + // execution & assertions + $assert->runs( + function () use (&$environment) { + $args = $environment['call'](['foo', '--raid==ho=odin']); + } + ); + } + ], + [ + 'name' => 'named_unspecified', + 'procedure' => function ($assert, $environment) { + // execution & assertions + $assert->runs( + function () use (&$environment) { + $args = $environment['call'](['foo', '--raidho=odin']); + } + ); + } + ], + ] + ], + ] + ] + ] + ] +); + diff --git a/lib/alveolata/auth/abstract/interface-client.php b/lib/alveolata/auth/abstract/interface-client.php new file mode 100644 index 0000000..dd2c080 --- /dev/null +++ b/lib/alveolata/auth/abstract/interface-client.php @@ -0,0 +1,65 @@ + + */ +interface interface_client +{ + + /** + * @param string $username + * @param string $password + * @return boolean + * @author Christian Fraß + */ + public function register( + string $username, + string $password + ) : bool + ; + + + /** + * @param string $password + * @return boolean + * @author Christian Fraß + */ + public function passwordchange( + string $password + ) : bool + ; + + + /** + * @param string $username + * @param string $password + * @param string $key + * @return boolean + * @author Christian Fraß + */ + public function passwordreset( + string $username, + string $password, + string $key + ) : bool + ; + + + /** + * @param string $username + * @param string $passwrd + * @return boolean + * @author Christian Fraß + */ + public function login( + string $username, + string $password + ) : bool + ; + +} + + ?> diff --git a/lib/alveolata/auth/abstract/interface-server.php b/lib/alveolata/auth/abstract/interface-server.php new file mode 100644 index 0000000..5f72ce1 --- /dev/null +++ b/lib/alveolata/auth/abstract/interface-server.php @@ -0,0 +1,33 @@ + + */ +interface interface_server +{ + + /** + * @author Christian Fraß + */ + public function register( + string $username, + string $password + ) : bool + ; + + + /** + * @author Christian Fraß + */ + public function login( + string $kind, + $parameters + ) : bool + ; + +} + + ?> diff --git a/lib/alveolata/auth/implementation-srp/core.php b/lib/alveolata/auth/implementation-srp/core.php new file mode 100644 index 0000000..f0fc9a5 --- /dev/null +++ b/lib/alveolata/auth/implementation-srp/core.php @@ -0,0 +1,880 @@ + + */ +class struct_srp_toolset/**/ +{ + + /** + * @var function,string> + */ + public $encode; + + + /** + * @var function,type_number> + */ + public $decode; + + + /** + * @var function,type_number> + */ + public $make; + + + /** + * @var function,type_number> + */ + public $zero; + + + /** + * @todo check if really needed + * @var function,type_number> + */ + public $one; + + + /** + * @var function,string> + */ + public $to_string; + + + /** + * @var function,boolean> + */ + public $equal; + + + /** + * @var function,type_number> + */ + public $add; + + + /** + * @var function,type_number> + */ + public $subtract; + + + /** + * @var function,type_number> + */ + public $multiply; + + + /** + * @var function,type_number> + */ + public $xor; + + + /** + * @var function,type_number> + */ + public $mod; + + + /** + * @var function,type_number> + */ + public $modpow; + + + /** + * @var function,string> + */ + public $join; + + + /** + * @var function<> + */ + public $compute_hash; + + + /** + * @var function,string> + */ + public $compute_password_hash; + + + /** + * @var function,type_number> + */ + public $compute_random; + + + /** + * @param array $tools { + * record< + * encode:function,string>, + * decode:function,type_number>, + * make:function,type_number>, + * zero:function,type_number>, + * one:function,type_number>, + * to_string:function,string>, + * equal:function,boolean>, + * add:function,type_number>, + * subtract:function,type_number>, + * multiply:function,type_number>, + * xor:function,type_number>, + * mod:function,type_number>, + * modpow:function,type_number>, + * join:function,string>, + * compute_hash:function<>, + * compute_password_hash:function,string>, + * compute_random:function,type_number>, + * > + * } + */ + public function __construct( + array $tools + ) + { + $this->encode = $tools['encode']; + $this->decode = $tools['decode']; + $this->make = $tools['make']; + $this->zero = $tools['zero']; + $this->one = $tools['one']; + $this->to_string = $tools['to_string']; + $this->equal = $tools['equal']; + $this->add = $tools['add']; + $this->subtract = $tools['subtract']; + $this->multiply = $tools['multiply']; + $this->xor = $tools['xor']; + $this->mod = $tools['mod']; + $this->modpow = $tools['modpow']; + $this->join = $tools['join']; + $this->compute_hash = $tools['compute_hash']; + $this->compute_password_hash = $tools['compute_password_hash']; + $this->compute_random = $tools['compute_random']; + } + +} + + +/** + * @author Christian Fraß + */ +function _srp_toolset_simple( +) : struct_srp_toolset/**/ +{ + return (new struct_srp_toolset/**/( + [ + 'encode' => (fn($number) => \sprintf('%X', $number)), + 'decode' => (fn($string) => \intval($string, 16)), + 'make' => (fn($base, $digits) => \intval($digits, $base)), + 'zero' => (fn() => 0), + 'one' => (fn() => 1), + 'to_string' => (fn($number) => \sprintf('%X', $number)), + 'equal' => (fn($x, $y) => ($x === $y)), + 'add' => (fn($x, $y) => ($x + $y)), + 'subtract' => (fn($x, $y) => ($x - $y)), + 'multiply' => (fn($x, $y) => ($x * $y)), + 'xor' => (fn($x, $y) => ($x ^ $y)), + // 'mod' => (fn($x, $y) => ($x % $y)), + 'mod' => (fn($x, $y) => \alveolata\math\mod($x, $y)), + // 'modpow' => (fn($base, $exponent, $modulus) => (($base ** $exponent) % $modulus)), + 'modpow' => (fn($base, $exponent, $modulus) => \alveolata\math\modpow($base, $exponent, $modulus)), + 'join' => (fn($parts) => \implode('', $parts)), + 'compute_hash' => (fn($string) => $string), + 'compute_password_hash' => (fn($salt, $password) => \sprintf( + '%04X', + \alveolata\list_\reduce( + \alveolata\list_\map( + \str_split($password . $salt), + (fn($character) => \ord($character)) + ), + 0, + (fn($x, $y) => (($x + $y) & 0xFFFF)) + ) + )), + 'compute_random' => (fn() => \rand(1, 7)), + ] + )); +} + + +/** + * requires composer package phpseclib/phpseclib + * + * @see https://api.phpseclib.com/master/phpseclib3/Math/BigInteger.html + * @author Christian Fraß + */ +function _srp_toolset_biginteger( +) : struct_srp_toolset/*<\phpseclib3\Math\BigInteger>*/ +{ + return (new struct_srp_toolset/*<\phpseclib3\Math\BigInteger>*/( + [ + 'encode' => (fn($number) => $number->toHex()), + 'decode' => (fn($string) => (new \phpseclib3\Math\BigInteger($string, 16))), + 'make' => (fn($base, $digits) => (new \phpseclib3\Math\BigInteger($digits, $base))), + 'zero' => (fn() => (new \phpseclib3\Math\BigInteger('0', 16))), + 'one' => (fn() => (new \phpseclib3\Math\BigInteger('1', 16))), + 'to_string' => (fn($number) => $number->toHex()), + 'equal' => (fn($x, $y) => $x->equals($y)), + 'add' => (fn($x, $y) => $x->add($y)), + 'subtract' => (fn($x, $y) => $x->subtract($y)), + 'multiply' => (fn($x, $y) => $x->multiply($y)), + 'xor' => (fn($x, $y) => $x->bitwise_xor($y)), + 'mod' => (fn($x, $y) => $x->modPow(new \phpseclib3\Math\BigInteger('1', 16), $y)), + 'modpow' => (fn($base, $exponent, $modulus) => $base->modPow($exponent, $modulus)), + 'join' => (fn($parts) => \implode('', $parts)), + 'compute_hash' => (fn($string) => $string), + /* + 'compute_password_hash' => (fn($salt, $password) => \Vinsaj9\Crypto\Scrypt\scrypt( + $password, + $salt, + 16384, // TODO: wadd is dadd? + 8, // TODO: wadd is dadd? + 1, // TODO: wadd is dadd? + 64 // TODO: wadd is dadd? + )), + */ + /* + 'compute_password_hash' => (fn($salt, $password) => \password_hash( + $password, + \PASSWORD_BCRYPT, + [ + 'salt' => $salt, + ] + )), + */ + 'compute_password_hash' => (fn($salt, $password) => \hash( + 'sha256', + ($password . $salt) + )), + 'compute_random' => (fn() => \phpseclib3\Math\BigInteger::random(24)), + ] + )); +} + + +/** + * @author Christian Fraß + */ +class struct_srp_subject/**/ +{ + + /** + * @var struct_srp_toolset + */ + public $toolset; + + + /** + * @var type_number + */ + public $base; + + + /** + * @var type_number + */ + public $modulus; + + + /** + */ + public function __construct( + struct_srp_toolset/**/ $toolset, + /*type_number */$base, + /*type_number */$modulus + ) + { + $this->toolset = $toolset; + $this->base = $base; + $this->modulus = $modulus; + } + +} + + +/** + */ +function srp_subject_make/**/( + struct_srp_toolset/**/ $toolset, + /*type_number */$base, + /*type_number */$modulus +) +{ + return ( + new struct_srp_subject( + $toolset, + $base, + $modulus + ) + ); +} + + +/** + * @return struct_srp_subject + */ +function srp_subject_test( +) : struct_srp_subject/**/ +{ + $toolset = _srp_toolset_simple(); + // <2>_19 = {2,4,8,16,13,7,14,9,18,17,15,11,3,6,12,5,10,1} + return (new struct_srp_subject/**/( + $toolset, + ($toolset->decode)('2'), + ($toolset->decode)('1323') // wollte eigentlich 19 nehmen, aber das ist zu klein :/ + )); +} + + +/** + * sind die Werte, die bytegod immer nimmt + * requires composer package phpseclib/phpseclib + * + * @return struct_srp_subject<\phpseclib3\Math\BigInteger> + */ +function srp_subject_biginteger_1( +) : struct_srp_subject/*<\phpseclib3\Math\BigInteger>*/ +{ + $toolset = _srp_toolset_biginteger(); + return (new struct_srp_subject/*<\phpseclib3\Math\BigInteger>*/( + $toolset, + ($toolset->decode)('2'), + ($toolset->decode)('AC6BDB41324A9A9BF166DE5E1389582FAF72B6651987EE07FC3192943DB56050A37329CBB4A099ED8193E0757767A13DD52312AB4B03310DCD7F48A9DA04FD50E8083969EDB767B0CF6095179A163AB3661A05FBD5FAAAE82918A9962F0B93B855F97993EC975EEAA80D740ADBF4FF747359D041D5C33EA71D281E446B14773BCA97B43A23FB801676BD207A436C6481F1D2B9078717461A5B9D32E688F87748544523B524B0D57D5EA77A2775D2ECFA032CFBDBF52FB3786160279004E57AE6AF874E7303CE53299CCC041C7BC308D82A5698F3A8D0C38271AE35F8E9DBFBB694B5C803D89F7AE435DE236D525F54759B65E372FCD68EF20FA7111F9E4AFF73') + )); +} + + +/** + * @author Christian Fraß + */ +class _state +{ + public static $subject_default = null; +} + + +/** + * @author Christian Fraß + */ +function srp_set_default_subject( + \Closure $factory +) : void +{ + _state::$subject_default = $factory; +} + + +/** + * @return struct_srp_subject + * @author Christian Fraß + */ +function srp_subject_default( +) : struct_srp_subject +{ + if (_state::$subject_default === null) { + srp_set_default_subject(fn() => srp_subject_biginteger_1()); + } + return (_state::$subject_default)(); +} + + +/** + * @param string $salt + * @param string $password + * @return array { + * record< + * x:type_number, + * v:type_number + * > + * } + */ +function srp_compute_verifier/**/( + struct_srp_subject/**/ $subject, + string $salt, + string $password +) : array +{ + $x = ($subject->toolset->decode)( + ($subject->toolset->compute_password_hash)($salt, $password) + ); + // g^x + $verifier = ($subject->toolset->modpow)( + $subject->base, + $x, + $subject->modulus + ); + return [ + 'x' => $x, + 'verifier' => ($verifier), + ]; +} + + +/** + * compute A + * + * @return array { + * record< + * k:type_number, + * a_exponent:type_number, + * a_value:type_number + * > + * } + */ +function srp_compute_a/**/( + struct_srp_subject $subject +) : array +{ + // set asymmetry factor + $k = ($subject->toolset->decode)( + ($subject->toolset->compute_hash)( + ($subject->toolset->join)([ + ($subject->toolset->to_string)($subject->modulus), + ($subject->toolset->to_string)($subject->base), + ]) + ) + ); + // compute random until it fits requirements by specification + while (true) { + $a_exponent = ($subject->toolset->compute_random)(); + // g^a % N + $a_value = ($subject->toolset->modpow)($subject->base, $a_exponent, $subject->modulus); + // (g^a % N) % N == 0 + // if (! ($subject->toolset->equal)(($subject->toolset->mod)($a_value, $subject->modulus), ($subject->toolset->zero)())) { + if (! ($subject->toolset->equal)($a_value, ($subject->toolset->zero)())) { + break; + } + } + return [ + 'k' => $k, + 'a_exponent' => $a_exponent, + 'a_value' => $a_value, + ]; +} + + +/** + * compute B + * + * @param type_number $verifier + * @return array { + * record< + * k:type_number, + * b_exponent:type_number, + * b_value:type_number + * > + * } + */ +function srp_compute_b/**/( + struct_srp_subject/**/ $subject, + /**/ $verifier +) : array +{ + // asymmetry factor + $k = ($subject->toolset->decode)( + ($subject->toolset->compute_hash)( + ($subject->toolset->join)([ + ($subject->toolset->to_string)($subject->modulus), + ($subject->toolset->to_string)($subject->base), + ]) + ) + ); + $b_exponent = ($subject->toolset->compute_random)(); + // k*v + $Bl = ($subject->toolset->multiply)($k, $verifier); + // g^b + $Br = ($subject->toolset->modpow)($subject->base, $b_exponent, $subject->modulus); + // k*v + g^b + $Bn = ($subject->toolset->add)($Bl, $Br); + // (k*v + g^b) % N + $b_value = ($subject->toolset->mod)($Bn, $subject->modulus); + return [ + 'k' => $k, + 'b_exponent' => $b_exponent, + 'b_value' => $b_value, + ]; +} + + +/** + * @return array { + * record< + * k_client:string + * > + * } + */ +function srp_compute_k_client/**/( + struct_srp_subject/**/ $subject, + /*type_number */$a_exponent, + /*type_number */$a_value, + /*type_number */$b_value, + /*type_number */$x, + /*type_number */$k +) : array +{ + $u = ($subject->toolset->decode)( + ($subject->toolset->compute_hash)( + ($subject->toolset->join)([ + ($subject->toolset->to_string)($a_value), + ($subject->toolset->to_string)($b_value), + ]) + ) + ); + // B - k*g^x + $Sl = ($subject->toolset->subtract)( + $b_value, + ($subject->toolset->multiply)( + $k, + ($subject->toolset->modpow)( + $subject->base, + $x, + $subject->modulus + ) + ) + ); + // a + u*x + $Sr = ($subject->toolset->add)( + $a_exponent, + ($subject->toolset->multiply)( + $u, + $x + ) + ); + // (B - k*g^x) ^ (a + u*x) + $S = ($subject->toolset->modpow)( + $Sl, + $Sr, + $subject->modulus + ); + $k_client = ($subject->toolset->compute_hash)(($subject->toolset->to_string)($S)); + return [ + 'k_client' => $k_client, + ]; +} + + +/** + * @return array { + * record< + * k_server:string + * > + * } + */ +function srp_compute_k_server/**/( + struct_srp_subject/**/ $subject, + /*type_number */$a_value, + /*type_number */$b_exponent, + /*type_number */$b_value, + /*type_number */$verifier +) : array +{ + $u = ($subject->toolset->decode)( + ($subject->toolset->compute_hash)( + ($subject->toolset->join)([ + ($subject->toolset->to_string)($a_value), + ($subject->toolset->to_string)($b_value), + ]) + ) + ); + // A*v^u + $Sl = ($subject->toolset->multiply)( + $a_value, + ($subject->toolset->modpow)( + $verifier, + $u, + $subject->modulus + ) + ); + // (A*v^u)^b + $S = ($subject->toolset->modpow)( + $Sl, + $b_exponent, + $subject->modulus + ); + $k_server = ($subject->toolset->compute_hash)(($subject->toolset->to_string)($S)); + return [ + 'k_server' => $k_server, + ]; +} + + +/** + * @return array { + * record< + * m1:string + * > + * } + */ +function srp_compute_m1_generic/**/( + struct_srp_subject/**/ $subject, + string $s, + /*type_number */$a_value, + /*type_number */$b_value, + string $identifier, + string $k +) : array +{ + $HN = ($subject->toolset->decode)( + ($subject->toolset->compute_hash)( + ($subject->toolset->to_string)($subject->modulus) + ) + ); + $Hg = ($subject->toolset->decode)( + ($subject->toolset->compute_hash)( + ($subject->toolset->to_string)($subject->base) + ) + ); + $HI = ($subject->toolset->decode)( + ($subject->toolset->compute_hash)( + $identifier + ) + ); + // H(H(N) XOR H(g), H(I), s, A, B, k) + $m1 = ($subject->toolset->compute_hash)( + ($subject->toolset->join)([ + ($subject->toolset->to_string)(($subject->toolset->xor)($HN, $Hg)), + ($subject->toolset->to_string)($HI), + $s, + ($subject->toolset->to_string)($a_value), + ($subject->toolset->to_string)($b_value), + $k, + ]) + ) ; + return [ + 'm1' => $m1, + ]; +} + + +/** + * @return array { + * record< + * m1_client:string, + * > + * } + */ +function srp_compute_m1_client/**/( + struct_srp_subject/**/ $subject, + string $s, + /*type_number */$a_value, + /*type_number */$b_value, + string $identifier, + string $k_client +) : array +{ + return ['m1_client' => srp_compute_m1_generic( + $subject, + $s, + $a_value, + $b_value, + $identifier, + $k_client + )['m1']]; +} + + +/** + * @return array { + * record< + * m1_server:string + * > + * } + */ +function srp_compute_m1_server/**/( + struct_srp_subject/**/ $subject, + string $s, + /*type_number */$a_value, + /*type_number */$b_value, + string $identifier, + string $k_server +) : array +{ + return ['m1_server' => srp_compute_m1_generic( + $subject, + $s, + $a_value, + $b_value, + $identifier, + $k_server + )['m1']]; +} + + +/** + * @return array { + * record< + * M2:string + * > + * } + */ +function srp_compute_m2_generic/**/( + struct_srp_subject/**/ $subject, + /*type_number */$a_value, + string $m1, + string $k +) : array +{ + $M2 = ($subject->toolset->compute_hash)( + ($subject->toolset->join)([ + ($subject->toolset->to_string)($a_value), + $m1, + $k, + ]) + ); + return [ + 'M2' => $M2, + ]; +} + + +/** + * @return array { + * record< + * m2_client:string + * > + * } + */ +function srp_compute_m2_client/**/( + struct_srp_subject/**/ $subject, + /*type_number */$a_value, + string $m1_client, + string $k_client +) : array +{ + return ['m2_client' => srp_compute_m2_generic( + $subject, + $a_value, + $m1_client, + $k_client + )['M2']]; +} + + +/** + * @return array { + * record< + * m2_server:string + * > + * } + */ +function srp_compute_m2_server/**/( + struct_srp_subject/**/ $subject, + /*type_number */$a_value, + string $m1_server, + string $k_server +) : array +{ + return ['m2_server' => srp_compute_m2_generic( + $subject, + $a_value, + $m1_server, + $k_server + )['M2']]; +} + + +/** + * @return boolean + */ +function srp_verify_a/**/( + struct_srp_subject/**/ $subject, + /*type_number */$a_value +) : bool +{ + if (! ($subject->toolset->equal)($a_value, ($subject->toolset->zero)())) { + if (! ($subject->toolset->equal)(($subject->toolset->mod)($a_value, $subject->modulus), ($subject->toolset->zero)())) { + return true; + } + else { + return false; + } + } + else { + return false; + } +} + + +/** + * @return boolean + */ +function srp_verify_b/**/( + struct_srp_subject/**/ $subject, + /*type_number */$b_value +) : bool +{ + if (! ($subject->toolset->equal)($b_value, ($subject->toolset->zero)())) { + if (! ($subject->toolset->equal)(($subject->toolset->mod)($b_value, $subject->modulus), ($subject->toolset->zero)())) { + return true; + } + else { + return false; + } + } + else { + return false; + } +} + + +/** + * @return boolean + */ +function srp_verify_hab/**/( + struct_srp_subject/**/ $subject, + /*type_number */$a_value, + /*type_number */$b_value +) : bool +{ + $u = ($subject->toolset->decode)( + ($subject->toolset->compute_hash)( + ($subject->toolset->join)([ + ($subject->toolset->to_string)($a_value), + ($subject->toolset->to_string)($b_value), + ]) + ) + ); + if (! ($subject->toolset->equal)($u, ($subject->toolset->zero)())) { + return true; + } + else { + return false; + } +} + + +/** + * @return boolean + */ +function srp_verify_k( + struct_srp_subject/**/ $subject, + string $k_client, + string $k_server +) : bool +{ + if (($k_client != null) && ($k_server != null)) { + if ($k_client === $k_server) { + return true; + } + else { + return false; + } + } + else { + return false; + } +} + +?> diff --git a/lib/alveolata/auth/implementation-srp/functions-client.php b/lib/alveolata/auth/implementation-srp/functions-client.php new file mode 100644 index 0000000..33d5da2 --- /dev/null +++ b/lib/alveolata/auth/implementation-srp/functions-client.php @@ -0,0 +1,96 @@ + $subject + * @param function,string> $generate_salt + * @param function,void> $handle + */ +function srp_client_register/**/( + struct_srp_subject/**/ $subject, + \Closure $generate_salt, + \Closure $handle +) : \Closure +{ + return ( + function ($username, $password) use (&$subject, &$generate_salt, &$handle) { + $salt = $generate_salt(); + $compute_verifier_result = srp_compute_verifier( + $subject, + $salt, + $password + ); + $verifier = $compute_verifier_result['verifier']; + $response1 = $handle( + $username, + $salt, + ($subject->toolset->encode)($verifier) + ); + return true; + } + ); +} + + +/** + * @param function,record> $handle1 + * @param function,record> $handle2 + * @return function,boolean> + */ +function srp_client_login/**/( + struct_srp_subject/**/ $subject, + \Closure $handle1, + \Closure $handle2 +) : \Closure +{ + return ( + function (string $username, string $password) use (&$subject, &$handle1, &$handle2) { + $computeAResult = srp_compute_a($subject); + $a_exponent = $computeAResult['a_exponent']; + $a_value = $computeAResult['a_value']; + $k = $computeAResult['k']; + $response1 = $handle1($username, ($subject->toolset->encode)($a_value)); + $salt = $response1['salt']; + $b_value = ($subject->toolset->decode)($response1['b_value']); + if (! $response1['passed']) { + return false; + } + else { + $verifiedB = srp_verify_b($subject, $b_value); + $verifiedHAB = srp_verify_hab($subject, $a_exponent, $b_value); + if (! ($verifiedB && $verifiedHAB)) { + throw (new \Exception('connection security fault')); + } + else { + $computeVerifierResult = srp_compute_verifier($subject, $salt, $password); + $x = $computeVerifierResult['x']; + $computeClientKResult = srp_compute_k_client($subject, $a_exponent, $a_value, $b_value, $x, $k); + $k_client = $computeClientKResult['k_client']; + $computeClientM1Result = srp_compute_m1_client($subject, $salt, $a_value, $b_value, $username, $k_client); + $m1_client = $computeClientM1Result['m1_client']; + $response2 = $handle2($m1_client); + $m2_server = $response2['m2_server']; + if (! $response2['passed']) { + return false; + } + else { + $computeClientM2Result = srp_compute_m2_client($subject, $a_value, $m1_client, $k_client); + $m2_client = $computeClientM2Result['m2_client']; + if (! ($m2_server === $m2_client)) { + return false; + } + else { + return true; + } + } + } + } + } + ); +} + + ?> diff --git a/lib/alveolata/auth/implementation-srp/functions-server.php b/lib/alveolata/auth/implementation-srp/functions-server.php new file mode 100644 index 0000000..20ec378 --- /dev/null +++ b/lib/alveolata/auth/implementation-srp/functions-server.php @@ -0,0 +1,194 @@ +, + * void + * > + * } procedure that shall store the data (username, salt, verifier) (e.g. by creating a new database entry) + * @return \Closure { + * function< + * tuple< + * string, + * string, + * type_number, + * >, + * void + * > + * } + */ +function srp_server_register/**/( + struct_srp_subject/**/ $subject, + \Closure $handler +) : \Closure +{ + return ( + function ($username, $salt, $verifier_encoded) use (&$subject, &$handler) { + $handler( + $username, + $salt, + /*($subject->toolset->encode)($verifier)*/$verifier_encoded + ); + return true; + } + ); +} + + +/** + * @param \Closure $get_salt_and_verifier { + * function< + * tuple< + * >, + * record< + * found:boolean, + * salt:string, + * verifier:string, + * > + * > + * } + * @param \Closure $state_save { + * function< + * tuple< + * any, + * >, + * void + * > + * } + * @return \Closure { + * function< + * tuple< + * string, + * string, + * >, + * record< + * passed:boolean, + * salt:string, + * b_value:string, + * > + * > + * } + */ +function srp_server_login_1/**/( + struct_srp_subject/**/ $subject, + \Closure $get_salt_and_verifier, + \Closure $state_save +) : \Closure +{ + return ( + function (string $username, string $a_value_encoded) use (&$subject, &$get_salt_and_verifier, &$state_save) { + $a_value = ($subject->toolset->decode)($a_value_encoded); + $salt_and_verifier = $get_salt_and_verifier($username); + $found = $salt_and_verifier['found']; + if (! $found) { + return [ + 'passed' => false, + 'salt' => null, + 'b_value' => null, + ]; + } + else { + $salt = $salt_and_verifier['salt']; + $verifier = ($subject->toolset->decode)($salt_and_verifier['verifier']); + $verified_a = srp_verify_a($subject, $a_value); + if (! $verified_a) { + return [ + 'passed' => false, + 'salt' => null, + 'b_value' => null, + ]; + } + else { + $computeBResult = srp_compute_b( + $subject, + $verifier + ); + $b_value = $computeBResult['b_value']; + $b_exponent = $computeBResult['b_exponent']; + $state_save( + [ + 'salt' => $salt, + 'identifier' => $username, + 'a_value' => ($subject->toolset->encode)($a_value), + 'verifier' => ($subject->toolset->encode)($verifier), + 'b_exponent' => ($subject->toolset->encode)($b_exponent), + 'b_value' => ($subject->toolset->encode)($b_value), + ] + ); + return [ + 'passed' => true, + 'salt' => $salt, + 'b_value' => ($subject->toolset->encode)($b_value), + ]; + } + } + } + ); +} + + +/** + * @param \Closure $state_load { + * function< + * tuple< + * >, + * state:any + * > + * } + * @return \Closure { + * function< + * tuple< + * type_number + * >, + * record< + * passed:boolean, + * m2_server:type_number, + * > + * > + * } + */ +function srp_server_login_2/**/( + struct_srp_subject/**/ $subject, + \Closure $state_load +) : \Closure +{ + return ( + function (string $m1_client) use (&$subject, &$state_load) { + $state = $state_load(); + $salt = $state['salt']; + $a_value = ($subject->toolset->decode)($state['a_value']); + $b_exponent = ($subject->toolset->decode)($state['b_exponent']); + $b_value = ($subject->toolset->decode)($state['b_value']); + $verifier = ($subject->toolset->decode)($state['verifier']); + $identifier = $state['identifier']; + $compute_k_server_result = srp_compute_k_server($subject, $a_value, $b_exponent, $b_value, $verifier); + $k_server = $compute_k_server_result['k_server']; + $compute_m1_server_result = srp_compute_m1_server($subject, $salt, $a_value, $b_value, $identifier, $k_server); + $m1_server = $compute_m1_server_result['m1_server']; + if (! ($m1_client === $m1_server)) { + return [ + 'passed' => false, + 'm2_server' => null, + ]; + } + else { + $compute_m2_server_result = srp_compute_m2_server($subject, $a_value, $m1_server, $k_server); + return [ + 'passed' => true, + 'm2_server' => $compute_m2_server_result['m2_server'], + ]; + } + } + ); +} + diff --git a/lib/alveolata/auth/implementation-srp/wrapper-class-client.php b/lib/alveolata/auth/implementation-srp/wrapper-class-client.php new file mode 100644 index 0000000..aef1505 --- /dev/null +++ b/lib/alveolata/auth/implementation-srp/wrapper-class-client.php @@ -0,0 +1,101 @@ + + */ +class implementation_client_srp implements interface_client +{ + + /** + * @param function,string> $generate_salt + * @param function,void> $registration_send + * @param function,record> $login_handle1 + * @param function,record> $login_handle2 + * @author Christian Fraß + */ + public function __construct( + struct_srp_subject $subject, + \Closure $generate_salt, + \Closure $register_handle, + \Closure $login_handle1, + \Closure $login_handle2 + ) + { + $this->subject = $subject; + $this->generate_salt = $generate_salt; + $this->register_handle = $register_handle; + $this->login_handle1 = $login_handle1; + $this->login_handle2 = $login_handle2; + } + + + /** + * @implementation + * @author Christian Fraß + */ + public function register( + string $username, + string $password + ) : bool + { + $proc = srp_client_register( + $this->subject, + $this->generate_salt, + $this->register_handle + ); + return $proc($username, $password); + } + + + /** + * @implementation + * @author Christian Fraß + */ + public function passwordchange( + string $password + ) : bool + { + throw (new Exception('not implemented')); + } + + + /** + * @implementation + * @author Christian Fraß + */ + public function passwordreset( + string $username, + string $password, + string $key + ) : bool + { + throw (new Exception('not implemented')); + } + + + /** + * @implementation + * @author Christian Fraß + */ + public function login( + string $username, + string $password + ) : bool + { + $proc = srp_client_login( + $this->subject, + $this->login_handle1, + $this->login_handle2 + ); + return $proc($username, $password); + } + +} + + ?> diff --git a/lib/alveolata/auth/implementation-srp/wrapper-class-server.php b/lib/alveolata/auth/implementation-srp/wrapper-class-server.php new file mode 100644 index 0000000..88e0ed0 --- /dev/null +++ b/lib/alveolata/auth/implementation-srp/wrapper-class-server.php @@ -0,0 +1,86 @@ + + */ +class implementation_server_srp implements interface_server +{ + + /** + * @author Christian Fraß + */ + public function __construct( + struct_srp_subject $subject, + \Closure $registration_handle, + \Closure $get_salt_and_verifier, + \Closure $state_save, + \Closure $state_load + ) + { + $this->subject = $subject; + $this->registration_handle = $registration_handle; + $this->get_salt_and_verifier = $get_salt_and_verifier; + $this->state_save = $state_save; + $this->state_load = $state_load; + } + + + /** + * @implementation + * @author Christian Fraß + */ + public function register( + string $username, + string $password + ) : bool + { + $proc = srp_server_register( + $this->registration_handle + ); + return $proc($username, $password); + } + + + /** + * @implementation + * @author Christian Fraß + */ + public function login( + string $kind, + $parameters + ) : bool + { + switch ($kind) { + default: { + throw (new \Exception('unhandled')); + break; + } + case 'step1': { + $proc = srp_server_login_1( + $this->subject, + $this->state_save, + $this->get_salt_and_verifier + ); + return $proc($username, $password); + break; + } + case 'step2': { + $proc = srp_server_login_1( + $this->subject, + $this->state_load + ); + return $proc($username, $password); + break; + } + } + } + +} + +?> diff --git a/lib/alveolata/auth/policy.php b/lib/alveolata/auth/policy.php new file mode 100644 index 0000000..5dccd6d --- /dev/null +++ b/lib/alveolata/auth/policy.php @@ -0,0 +1,298 @@ + + */ +class class_exception_policyreport extends \Exception +{ + + /** + * @author Christian Fraß + */ + public function __construct( + $messages + ) + { + parent::__construct( + ' | '.join($messages) + ); + } + +} + + +/** + * @author Christian Fraß + */ +abstract class class_policy +{ + + /** + * @author Christian Fraß + */ + public function __construct( + ) + { + } + + + /** + * @param record $environment + * @return list + * @author Christian Fraß + */ + abstract public function check( + array $environment + ) : array + ; + +} + + +/** + * @author Christian Fraß + */ +class class_policy_none extends class_policy +{ + + /** + * @author Christian Fraß + */ + public function __construct( + ) + { + parent::__construct(); + } + + + /** + * @implementation + * @author Christian Fraß + */ + public function check( + array $environment + ) : array + { + return []; + } + +} + + +/** + * @author Christian Fraß + */ +class class_policy_conjunction extends class_policy +{ + + /** + * @author Christian Fraß + */ + public function __construct( + array $subpolicies + ) + { + parent::__construct(); + $this->subpolicies = $subpolicies; + } + + + /** + * @implementation + * @author Christian Fraß + */ + public function check( + array $environment + ) : array + { + return ( + \alveolata\list_\reduce( + \alveolata\list_\map( + $this->subpolicies, + function ($policy) use ($environment) { + return $policy->check($environment); + } + ), + [], + function ($x, $y) {return \alveolata\list_\concat($x, $y);} + ) + ); + } + +} + + +/** + * @author Christian Fraß + */ +class class_policy_disjunction extends class_policy +{ + + /** + * @author Christian Fraß + */ + public function __construct( + array $subpolicies + ) + { + parent::__construct(); + $this->subpolicies = $subpolicies; + } + + + /** + * @implementation + * @author Christian Fraß + */ + public function check( + array $environment + ) : array + { + $some = \alveolata\list_\some( + $this->subpolicies, + function ($policy) use ($environment) { + return $policy->check($environment); + } + ); + return ($some ? [] : ['all alternatives failed']); + } + +} + + +/** + * @author Christian Fraß + */ +class class_policy_logged_in extends class_policy +{ + + /** + * @author Christian Fraß + */ + public function __construct( + ) + { + parent::__construct(); + } + + + /** + * @implementation + * @author Christian Fraß + */ + public function check( + array $environment + ) : array + { + if (! array_key_exists('session_id', $environment)) { + return ['session id not set']; + } + else { + if (! \alveolata\session\has($environment['session_id'])) { + return ['no session present']; + } + else { + return []; + } + } + } + +} + + +/** + * @param record> + * @return class_policy + * @author Christian Fraß + */ +function make( + array $description +) : class_policy +{ + switch ($description['kind']) { + default: { + throw ( + new \Exception( + \alveolata\string\coin('unhandled policy kind "{{kind}}"', ['kind' => $description['kind']]) + ) + ); + break; + } + case 'none': { + return ( + new class_policy_none( + ) + ); + break; + } + case 'logged_in': { + return ( + new class_policy_logged_in( + ) + ); + break; + } + case 'conjunction': { + return ( + new class_policy_conjunction( + \helper\list_\map( + $description['parameters']['sub'] ?? [], + function ($description_) { + return make($description_); + } + ) + ) + ); + break; + } + case 'disjunction': { + return ( + new class_policy_disjunction( + \helper\list_\map( + $description['parameters']['sub'] ?? [], + function ($description_) { + return make($description_); + } + ) + ) + ); + break; + } + } +} + + +/** + * @param class_policy $policy + * @param function $function + * @param record $environment + * @return type_result + * @throw \Exception if the policy check fails + * @author Christian Fraß + */ +function wrap/**/( + class_policy $policy, + array $environment, + \Closure $function +)/* : type_result*/ +{ + $messages = $policy->check($environment); + if (count($messages) === 0) { + $result = $function(); + return $result; + } + else { + throw (new class_exception_policyreport($messages)); + } +} + + diff --git a/lib/alveolata/auth/test.spec.php b/lib/alveolata/auth/test.spec.php new file mode 100644 index 0000000..b45a65a --- /dev/null +++ b/lib/alveolata/auth/test.spec.php @@ -0,0 +1,139 @@ + 'alveolata', + 'sections' => [ + [ + 'name' => 'auth', + 'sections' => [ + [ + 'name' => 'test', + 'setup' => function (&$environment) { + // vars + { + $environment['users'] = []; + $environment['state'] = null; + } + // constants + { + $srp_subject_client = \alveolata\auth\srp_subject_test(); + $srp_subject_server = \alveolata\auth\srp_subject_test(); + $proc_server_register = \alveolata\auth\srp_server_register( + $srp_subject_server, + function ($username, $salt, $verifier_encoded) use (&$environment) { + $environment['users'][$username] = [ + 'salt' => $salt, + 'verifier' => $verifier_encoded, + ]; + } + ); + $proc_client_register = \alveolata\auth\srp_client_register( + $srp_subject_client, + fn() => 'salt', + fn($username, $salt, $verifier_encoded) => $proc_server_register($username, $salt, $verifier_encoded) + ); + $proc_server_login_1 = \alveolata\auth\srp_server_login_1( + $srp_subject_server, + function ($username) use (&$environment) { + if (! array_key_exists($username, $environment['users'])) { + return [ + // 'found' => true, + 'found' => false, + 'salt' => '', + 'verifier' => '', + ]; + } + else { + $user = $environment['users'][$username]; + return [ + 'found' => true, + 'salt' => $user['salt'], + 'verifier' => $user['verifier'], + ]; + } + }, + function ($state) use (&$environment) { + $environment['state'] = $state; + } + ); + $proc_server_login_2 = \alveolata\auth\srp_server_login_2( + $srp_subject_server, + function () use (&$environment) { + return $environment['state']; + } + ); + $environment['proc_client_login'] = \alveolata\auth\srp_client_login( + $srp_subject_client, + (fn($username, $a_value) => $proc_server_login_1($username, $a_value)), + (fn($m1_client) => $proc_server_login_2($m1_client)) + ); + $environment['user_name'] = 'hans'; + $environment['user_password'] = '1234'; + $proc_client_register($environment['user_name'], $environment['user_password']); + } + }, + 'cases' => [ + [ + 'name' => 'user_missing', + 'procedure' => function ($assert, $environment) { + // execution + { + $success = $environment['proc_client_login']( + $environment['user_name'] . '_some_bogus', + $environment['user_password'] + ); + } + // assertions + { + $assert->is(! $success); + } + }, + ], + [ + 'name' => 'user_existing_and_password_wrong', + 'procedure' => function ($assert, $environment) { + // execution + { + $success = $environment['proc_client_login']( + $environment['user_name'], + $environment['user_password'] . '_some_bogus' + ); + } + // assertions + { + $assert->is(! $success); + } + }, + ], + [ + 'name' => 'user_existing_and_password_correct', + 'procedure' => function ($assert, $environment) { + // execution + { + $success = $environment['proc_client_login']( + $environment['user_name'], + $environment['user_password'] + ); + } + // assertions + { + $assert->is($success); + } + }, + ], + ], + 'cleanup' => function (&$environment) { + }, + ], + ] + ] + ] + ] +); + diff --git a/lib/alveolata/cache/abstract/interface.php b/lib/alveolata/cache/abstract/interface.php new file mode 100644 index 0000000..1e0af9d --- /dev/null +++ b/lib/alveolata/cache/abstract/interface.php @@ -0,0 +1,64 @@ + + */ +interface interface_cache +{ + + /** + * shell store a value + * + * @param string $id + * @param mixed $value + * @author Christian Fraß + */ + function set( + string $id, + $value + ) : void + ; + + + /** + * shall tell if a value is set + * + * @param string $id + * @return bool + * @author Christian Fraß + */ + function has( + string $id + ) : bool + ; + + + /** + * shall fetch a stored value + * + * @param string $id + * @return mixed + * @author Christian Fraß + */ + function fetch( + string $id + ) + ; + + + /** + * shall remove all stored values + * + * @author Christian Fraß + */ + function clear( + ) : void + ; + +} + diff --git a/lib/alveolata/cache/functions.php b/lib/alveolata/cache/functions.php new file mode 100644 index 0000000..ba57625 --- /dev/null +++ b/lib/alveolata/cache/functions.php @@ -0,0 +1,103 @@ + + * @author Christian Fraß + */ +function get_with_info( + interface_cache $cache, + string $id, + \Closure $retrieve +) +{ + if (! $cache->has($id)) { + $value = $retrieve(); + $cache->set($id, $value); + return ['fetched' => false, 'value' => $value]; + } + else { + $value = $cache->fetch($id); + return ['fetched' => true, 'value' => $value]; + } +} + + +/** + * @param string $id + * @param Closure $retrieve + * @return mixed + * @author Christian Fraß + */ +function get( + interface_cache $cache, + string $id, + \Closure $retrieve +) +{ + return get_with_info($cache, $id, $retrieve)['value']; +} + + +/** + * @param string $id + * @param Closure $retrieve + * @return mixed + * @author Christian Fraß + */ +function clear( + interface_cache $cache +) +{ + return $cache->clear(); +} + + +/** + * @author Christian Fraß + */ +function make( + string $kind, + array $parameters = [] +) : interface_cache +{ + switch ($kind) { + default: { + throw (new \Exception(sprintf('invalid cache kind "%s"', $kind))); + break; + } + case 'none': { + return ( + implementation_none::make( + ) + ); + break; + } + case 'memory': { + return ( + implementation_memory::make( + ) + ); + break; + } + case 'apc': { + return ( + implementation_apc::make( + $parameters['section'] ?? UNSET_STRING + ) + ); + break; + } + } +} + diff --git a/lib/alveolata/cache/implementation-apc/functions.php b/lib/alveolata/cache/implementation-apc/functions.php new file mode 100644 index 0000000..da29678 --- /dev/null +++ b/lib/alveolata/cache/implementation-apc/functions.php @@ -0,0 +1,163 @@ +} + */ + public static $sections = []; + +} + + +/** + * @author Christian Fraß + */ +class struct_subject_apc { + + /** + * @var string + * @author Christian Fraß + */ + public $section; + + + /** + * @var list + * @author Christian Fraß + */ + public $ids; + + + /** + * @param string $section + * @author Christian Fraß + */ + public function __construct( + string $section + ) + { + $this->section = $section; + $this->ids = []; + } + +} + + +/** + * @param struct_subject_apc $subject + * @param string id + * @return string + * @author Christian Fraß + */ +function _apc_id( + struct_subject_apc $subject, + string $id +) : string +{ + return ( + ($subject->section === UNSET_STRING) + ? $id + : sprintf('%s_%s', $subject->section, $id) + ); +} + + +/** + * @param string $section + * @return struct_subject_apc + * @author Christian Fraß + */ +function apc_make( + string $section = UNSET_STRING +) : struct_subject_apc +{ + if ($section === UNSET_STRING) { + $section = sprintf('alveolata_%d', count(_state_apc::$sections)); + } + if (in_array($section, _state_apc::$sections)) { + throw (new \Exception(sprintf('APC section "%s" already in use', $section))); + } + else { + array_push(_state_apc::$sections, $section); + $subject = (new struct_subject_apc($section)); + return $subject; + } +} + + +/** + * @param struct_subject_apc $subject + * @param string $id + * @param mixed $value + * @author Christian Fraß + */ +function apc_set( + struct_subject_apc $subject, + string $id, + $value +) : void +{ + $id_ = _apc_id($subject, $id); + array_push($subject->ids, $id_); + \apc_store($id_, $value); +} + + +/** + * @param struct_subject_apc $subject + * @param string $id + * @return bool + * @author Christian Fraß + */ +function apc_has( + struct_subject_apc $subject, + string $id +) : bool +{ + return \apc_exists(_apc_id($subject, $id)); +} + + +/** + * @param struct_subject_apc $subject + * @param string $id + * @return mixed + * @author Christian Fraß + */ +function apc_fetch( + struct_subject_apc $subject, + string $id +) +{ + return \apc_fetch(_apc_id($subject, $id)); +} + + +/** + * @param struct_subject_apc $subject + * @author Christian Fraß + */ +function apc_clear( + struct_subject_apc $subject +) : void +{ + if ($subject->section === UNSET_STRING) { + \apc_clear_cache(); + } + else { + foreach ($subject->ids as $id_) { + \apc_delete($id_); + } + $subject->ids = []; + } +} + + ?> diff --git a/lib/alveolata/cache/implementation-apc/wrapper-class.php b/lib/alveolata/cache/implementation-apc/wrapper-class.php new file mode 100644 index 0000000..b301a89 --- /dev/null +++ b/lib/alveolata/cache/implementation-apc/wrapper-class.php @@ -0,0 +1,48 @@ + + */ +class implementation_apc implements interface_cache +{ + + /** + * @var struct_subject_apc $subject + * @author Christian Fraß + */ + private $subject; + + + /** + * @author Christian Fraß + */ + private function __construct(struct_subject_apc $subject) {$this->subject = $subject;} + + + /** + * @param string $section + * @return implementation_apc + * @author Christian Fraß + */ + public static function make(string $section = UNSET_STRING) : implementation_apc {return (new static(apc_make($section)));} + + + /** + * @implementations + * + * @author Christian Fraß + */ + public function set(string $id, $value) : void {apc_set($this->subject, $id, $value);} + public function has(string $id) : bool {return apc_has($this->subject, $id);} + public function fetch(string $id) {return apc_fetch($this->subject, $id);} + public function clear() : void {apc_clear($this->subject);} + +} + diff --git a/lib/alveolata/cache/implementation-memory/functions.php b/lib/alveolata/cache/implementation-memory/functions.php new file mode 100644 index 0000000..ef5af26 --- /dev/null +++ b/lib/alveolata/cache/implementation-memory/functions.php @@ -0,0 +1,98 @@ + + */ +class struct_subject_memory { + + /** + * @author Christian Fraß + */ + public $data; + + + /** + * @author Christian Fraß + */ + public function __construct( + ) + { + $this->data = []; + } + +} + + +/** + * @author Christian Fraß + * @return struct_subject_memory + */ +function memory_make( +) : struct_subject_memory +{ + return (new struct_subject_memory()); +} + + +/** + * @param struct_subject_memory $subject + * @param string $id + * @param mixed $value + * @author Christian Fraß + */ +function memory_set( + struct_subject_memory $subject, + string $id, + $value +) : void +{ + $subject->data[$id] = $value; +} + + +/** + * @param struct_subject_memory $subject + * @param string $id + * @return bool + * @author Christian Fraß + */ +function memory_has( + struct_subject_memory $subject, + string $id +) : bool +{ + return array_key_exists($id, $subject->data); +} + + +/** + * @param struct_subject_memory $subject + * @param string $id + * @return mixed + * @author Christian Fraß + */ +function memory_fetch( + struct_subject_memory $subject, + string $id +) +{ + return $subject->data[$id]; +} + + +/** + * @param struct_subject_memory $subject + * @author Christian Fraß + */ +function memory_clear( + struct_subject_memory $subject +) : void +{ + $subject->data = []; +} + diff --git a/lib/alveolata/cache/implementation-memory/wrapper-class.php b/lib/alveolata/cache/implementation-memory/wrapper-class.php new file mode 100644 index 0000000..b4c9ffe --- /dev/null +++ b/lib/alveolata/cache/implementation-memory/wrapper-class.php @@ -0,0 +1,48 @@ + + */ +class implementation_memory implements interface_cache +{ + + /** + * @var struct_subject_memory + * @author Christian Fraß + */ + private $subject; + + + /** + * @var struct_subject_memory $subject + * @author Christian Fraß + */ + private function __construct(struct_subject_memory $subject) {$this->subject = $subject;} + + + /** + * @return implementation_memory + * @author Christian Fraß + */ + public static function make() : implementation_memory {return (new static(memory_make()));} + + + /** + * @implementations + * + * @author Christian Fraß + */ + public function set(string $id, $value) : void {memory_set($this->subject, $id, $value);} + public function has(string $id) : bool {return memory_has($this->subject, $id);} + public function fetch(string $id) {return memory_fetch($this->subject, $id);} + public function clear() : void {memory_clear($this->subject);} + +} + diff --git a/lib/alveolata/cache/implementation-none/functions.php b/lib/alveolata/cache/implementation-none/functions.php new file mode 100644 index 0000000..9ca72fe --- /dev/null +++ b/lib/alveolata/cache/implementation-none/functions.php @@ -0,0 +1,54 @@ + + */ +function none_set( + string $id, + $value +) : void +{ +} + + +/** + * @param string $id + * @return bool + * @author Christian Fraß + */ +function none_has( + string $id +) : bool +{ + return false; +} + + +/** + * @param string $id + * @return mixed + * @author Christian Fraß + */ +function none_fetch( + string $id +) +{ + throw (new \Exception('not available')); +} + + +/** + * @author Christian Fraß + */ +function none_clear( +) : void +{ +} + diff --git a/lib/alveolata/cache/implementation-none/wrapper-class.php b/lib/alveolata/cache/implementation-none/wrapper-class.php new file mode 100644 index 0000000..c6ab110 --- /dev/null +++ b/lib/alveolata/cache/implementation-none/wrapper-class.php @@ -0,0 +1,39 @@ + + */ +class implementation_none implements interface_cache +{ + + /** + * @author Christian Fraß + */ + private function __construct() {} + + + /** + * @author Christian Fraß + */ + public static function make() : implementation_none {return (new static());} + + + /** + * @implementations + * + * @author Christian Fraß + */ + public function set(string $id, $value) : void {none_set($id, $value);} + public function has(string $id) : bool {return none_has($id);} + public function fetch(string $id) {return none_fetch($id);} + public function clear() : void {none_clear();} + +} + diff --git a/lib/alveolata/cache/test.spec.php b/lib/alveolata/cache/test.spec.php new file mode 100644 index 0000000..0ac59c5 --- /dev/null +++ b/lib/alveolata/cache/test.spec.php @@ -0,0 +1,205 @@ + 'alveolata', + 'sections' => [ + [ + 'name' => 'cache', + 'sections' => [ + [ + 'name' => 'none', + 'setup' => function (&$environment) { + $cache = \alveolata\cache\make('none', []); + \alveolata\cache\clear($cache); + $environment['cache'] = $cache; + }, + 'cleanup' => function (&$environment) { + }, + 'cases' => [ + [ + 'name' => 'retrieve at first access', + 'procedure' => function ($assert, $environment) { + // execution + $result = \alveolata\cache\get_with_info( + $environment['cache'], + 'foo', + function () { + return 'fehuz'; + } + ); + + // assertions + $assert->equal($result['fetched'], false); + $assert->equal($result['value'], 'fehuz'); + } + ], + [ + 'name' => 'retrieve at second access', + 'procedure' => function ($assert, $environment) { + // consts + $id = 'bar'; + $retrieve = function () {return 'uruz';}; + + // execution + $result1 = \alveolata\cache\get_with_info($environment['cache'], $id, $retrieve); + $result2 = \alveolata\cache\get_with_info($environment['cache'], $id, $retrieve); + + // assertions + $assert->equal($result1['fetched'], false); + $assert->equal($result1['value'], 'uruz'); + $assert->equal($result2['fetched'], false); + $assert->equal($result2['value'], 'uruz'); + } + ], + ] + ], + [ + 'name' => 'memory', + 'setup' => function (&$environment) { + $cache = \alveolata\cache\make('memory', []); + \alveolata\cache\clear($cache); + $environment['cache'] = $cache; + }, + 'cleanup' => function (&$environment) { + }, + 'cases' => [ + [ + 'name' => 'retrieve at first access', + 'procedure' => function ($assert, $environment) { + // execution + $result = \alveolata\cache\get_with_info( + $environment['cache'], + 'foo', + function () { + return 'fehuz'; + } + ); + + // assertions + $assert->equal($result['fetched'], false); + $assert->equal($result['value'], 'fehuz'); + } + ], + [ + 'name' => 'fetch at second access', + 'procedure' => function ($assert, $environment) { + // consts + $id = 'bar'; + $retrieve = function () {return 'uruz';}; + + // execution + $result1 = \alveolata\cache\get_with_info($environment['cache'], $id, $retrieve); + $result2 = \alveolata\cache\get_with_info($environment['cache'], $id, $retrieve); + + // assertions + $assert->equal($result1['fetched'], false); + $assert->equal($result1['value'], 'uruz'); + $assert->equal($result2['fetched'], true); + $assert->equal($result2['value'], 'uruz'); + } + ], + [ + 'name' => 'retrieve again after clear', + 'procedure' => function ($assert, $environment) { + // consts + $id = 'baz'; + $retrieve = function () {return 'thurisaz';}; + + // execution + $result1 = \alveolata\cache\get_with_info($environment['cache'], $id, $retrieve); + $result2 = \alveolata\cache\get_with_info($environment['cache'], $id, $retrieve); + \alveolata\cache\clear($environment['cache']); + $result3 = \alveolata\cache\get_with_info($environment['cache'], $id, $retrieve); + + // assertions + $assert->equal($result1['fetched'], false); + $assert->equal($result1['value'], 'thurisaz'); + $assert->equal($result2['fetched'], true); + $assert->equal($result2['value'], 'thurisaz'); + $assert->equal($result3['fetched'], false); + $assert->equal($result3['value'], 'thurisaz'); + } + ], + ] + ], + [ + 'name' => 'apc', + 'active' => false, + 'setup' => function (&$environment) { + $cache = \alveolata\cache\make('apc', []); + \alveolata\cache\clear($cache); + $environment['cache'] = $cache; + }, + 'cleanup' => function (&$environment) { + }, + 'cases' => [ + [ + 'name' => 'retrieve at first access', + 'procedure' => function ($assert, $environment) { + // execution + $result = \alveolata\cache\get_with_info( + $environment['cache'], + 'foo', + function () { + return 'fehuz'; + } + ); + + // assertions + $assert->equal($result['fetched'], false); + $assert->equal($result['value'], 'fehuz'); + } + ], + [ + 'name' => 'fetch at second access', + 'procedure' => function ($assert, $environment) { + // consts + $id = 'bar'; + $retrieve = function () {return 'uruz';}; + + // execution + $result1 = \alveolata\cache\get_with_info($environment['cache'], $id, $retrieve); + $result2 = \alveolata\cache\get_with_info($environment['cache'], $id, $retrieve); + + // assertions + $assert->equal($result1['fetched'], false); + $assert->equal($result1['value'], 'uruz'); + $assert->equal($result2['fetched'], true); + $assert->equal($result2['value'], 'uruz'); + } + ], + [ + 'name' => 'retrieve again after clear', + 'procedure' => function ($assert, $environment) { + // consts + $id = 'baz'; + $retrieve = function () {return 'thurisaz';}; + + // execution + $result1 = \alveolata\cache\get_with_info($environment['cache'], $id, $retrieve); + $result2 = \alveolata\cache\get_with_info($environment['cache'], $id, $retrieve); + \alveolata\cache\clear($environment['cache']); + $result3 = \alveolata\cache\get_with_info($environment['cache'], $id, $retrieve); + + // assertions + $assert->equal($result1['fetched'], false); + $assert->equal($result1['value'], 'thurisaz'); + $assert->equal($result2['fetched'], true); + $assert->equal($result2['value'], 'thurisaz'); + $assert->equal($result3['fetched'], false); + $assert->equal($result3['value'], 'thurisaz'); + } + ], + ] + ], + ], + ] + ] + ] +); + diff --git a/lib/alveolata/call/functions.php b/lib/alveolata/call/functions.php new file mode 100644 index 0000000..90a571e --- /dev/null +++ b/lib/alveolata/call/functions.php @@ -0,0 +1,36 @@ + + */ +function convey( + $value, + array $functions +) +{ + $result = $value; + foreach ($functions as $function) { + $result = $function($result); + } + return $result; +} + + +/** + * @author Christian Fraß + * @see https://www.php.net/manual/en/function.usleep.php + */ +function pause( + float $seconds +) : void +{ + $seconds_whole = \intval(\floor($seconds)); + $seconds_rest = ($seconds - $seconds_whole); + \sleep($seconds_whole); + \usleep(\intval($seconds_rest * 1000000)); +} + + ?> diff --git a/lib/alveolata/cgi/functions.php b/lib/alveolata/cgi/functions.php new file mode 100644 index 0000000..56e7361 --- /dev/null +++ b/lib/alveolata/cgi/functions.php @@ -0,0 +1,79 @@ + + */ +function get_http_request( +): \alveolata\http\struct_request +{ + /* + \alveolata\log\debug( + 'superglobals', + [ + 'server' => $_SERVER, + 'cookie' => \json_encode($_COOKIE), + ] + ); + */ + $cookiedata = ( + isset($_SERVER['HTTP_COOKIE']) + ? \alveolata\cookie\decode($_SERVER['HTTP_COOKIE']) + : null + ); + $request = new \alveolata\http\struct_request( + $_SERVER['SERVER_PROTOCOL'], + ($_SERVER['REQUEST_METHOD'] ?? 'POST'), + $_SERVER['REQUEST_URI'], + \array_merge( + \getallheaders(), + [ + 'Cookie' => ($_SERVER['HTTP_COOKIE'] ?? null), + 'Content-Type' => ($_SERVER['CONTENT_TYPE'] ?? 'application/json'), + ], + ), + \file_get_contents('php://input') + ); + return $request; +} + + +/** + * @author Christian Fraß + */ +function setup( +): void +{ + while (ob_get_level() >= 1) { + ob_end_clean(); + } + ob_start(); +} + + +/** + * @author Christian Fraß + */ +function put_http_response( + \alveolata\http\struct_response $response +): void +{ + while (ob_get_level() >= 1) { + ob_end_clean(); + } + ob_start(); + foreach ($response->headers as $key => $value) { + \header(\sprintf('%s: %s', $key, $value)); + } + \http_response_code($response->statuscode); + \file_put_contents('php://output', $response->body); + ob_end_flush(); +} + + ?> diff --git a/lib/alveolata/cgi/setup.php b/lib/alveolata/cgi/setup.php new file mode 100644 index 0000000..9981093 --- /dev/null +++ b/lib/alveolata/cgi/setup.php @@ -0,0 +1,8 @@ += 1) { + ob_end_clean(); +} +ob_start(); + diff --git a/lib/alveolata/composer.json b/lib/alveolata/composer.json new file mode 100644 index 0000000..bea3b8c --- /dev/null +++ b/lib/alveolata/composer.json @@ -0,0 +1,20 @@ +{ + "name": "greenscale/alveolata", + "description": "", + "keywords": [], + "homepage": "https://greenscale.de", + "license": ["proprietary"], + "authors": [ + { + "name": "Christian Fraß" + } + ], + "require": { + }, + "autoload": { + "files": [ + "./definitions.php" + ] + } +} + diff --git a/lib/alveolata/conf/functions.php b/lib/alveolata/conf/functions.php new file mode 100644 index 0000000..7ae5fe8 --- /dev/null +++ b/lib/alveolata/conf/functions.php @@ -0,0 +1,103 @@ + + */ +class _state +{ + public static $path = 'conf.json'; + public static $cache_kind = 'memory'; + public static $cache_parameters = []; + public static $cache_instance = null; +} + + +/** + * @author Christian Fraß + */ +function set_cache( + string $kind, + array $parameters = [] +) : void +{ + _state::$cache_kind = $kind; + _state::$cache_parameters = $parameters; +} + + +/** + * @author Christian Fraß + */ +function set_path( + string $path +) : void +{ + _state::$path = $path; +} + + +/** + * @param string $path + * @param any [$fallback] value to return in case no conf entry was found for the given path + * @param boolean [$throw_exception] if true, do not return the fallback in case of a missing entry, but throw an exception + * @return mixed + * @throw \Exception + * @author Christian Fraß + */ +function get( + string $path, + $fallback = null, + $throw_exception = false +) +{ + // prepare cache + if (_state::$cache_instance === null) { + _state::$cache_instance = \alveolata\cache\make(_state::$cache_kind, _state::$cache_parameters); + } + // get raw data + $data = \alveolata\cache\get( + _state::$cache_instance, + 'data', + function () { + $content = \alveolata\file\read(_state::$path); + $data = \alveolata\json\decode($content); + return $data; + } + ); + // get specific piece of information + $steps = (($path === '') ? [] : explode('.', $path)); + $value = $data; + foreach ($steps as $step) { + if (! array_key_exists($step, $value)) { + $report = \alveolata\report\make( + 'conf entry not set', + [ + 'path' => $path, + 'fallback' => $fallback, + ] + ); + if ($throw_exception) { + throw (\alveolata\report_as_exception($report)); + } + else { + \alveolata\log\warning_($report); + return $fallback; + } + } + else { + $value = $value[$step]; + } + } + return $value; +} + + ?> diff --git a/lib/alveolata/cookie/functions.php b/lib/alveolata/cookie/functions.php new file mode 100644 index 0000000..57648df --- /dev/null +++ b/lib/alveolata/cookie/functions.php @@ -0,0 +1,42 @@ + + * @author Christian Fraß + */ +function decode( + $string +) : array +{ + $parts = explode('; ', $string); + $stuff = []; + foreach ($parts as $part) { + $parts2 = explode('=', $part); + $stuff[$parts2[0]] = $parts2[1]; + } + return $stuff; +} + + +/** + * @param map $stuff + * @return string + * @author Christian Fraß + */ +function encode( + array $stuff +) : string +{ + $parts = []; + foreach ($stuff as $key => $value) { + $part = sprintf('%s=%s', $key, $value); + } + $string = implode('; ', $parts); + return $string; +} + + ?> diff --git a/lib/alveolata/csv/functions.php b/lib/alveolata/csv/functions.php new file mode 100644 index 0000000..740d0cb --- /dev/null +++ b/lib/alveolata/csv/functions.php @@ -0,0 +1,101 @@ +>} + */ +function encode( + array $data, + ?array $settings = null +) : string +{ + $settings = array_merge( + [ + 'delimiter' => ";", + 'linebreak' => "\n", + 'quote' => "\"", + 'prepend_byte_order_mark' => false, + ], + ($settings ?? []) + ); + $csv = \alveolata\string\join( + \alveolata\list_\map( + $data, + function (array $row) use ($settings) : string { + return \alveolata\string\join( + \alveolata\list_\map( + $row, + function ($field) use ($settings) : string { + return sprintf( + '%s%s%s', + $settings['quote'], + str_replace($settings['quote'], '\\' . $settings['quote'], $field), + $settings['quote'] + ); + } + ), + $settings['delimiter'] + ); + } + ), + $settings['linebreak'] + ); + if ($settings['prepend_byte_order_mark']) { + $csv = (chr(0xEF) . chr(0xBB) . chr(0xBF) . $csv); + } + return $csv; +} + + +/** + */ +function decode( + string $csv, + array $settings_given = [] +) : array +{ + $settings_default = [ + 'delimiter' => ";", + 'linebreak' => "\n", + 'quote' => "\"", + ]; + $settings = array_merge($settings_default, $settings_given); + return \alveolata\list_\map( + \alveolata\list_\filter( + \alveolata\string\split( + $csv, + $settings['linebreak'] + ), + function (string $line) : bool { + return (! empty(trim($line))); + } + ), + function (string $line) use ($settings) : array { + return \alveolata\list_\map( + \alveolata\string\split( + $line, + $settings['delimiter'] + ), + function (string $field) use ($settings) { + if ( + \alveolata\string\starts_with($field, $settings['quote']) + and + \alveolata\string\end_with($field, $settings['quote']) + ) { + return substr($field, 1, strlen($field)-2); + } + else { + return $field; + } + } + ); + } + ); +} + + ?> diff --git a/lib/alveolata/database/abstract/interface.php b/lib/alveolata/database/abstract/interface.php new file mode 100644 index 0000000..656296a --- /dev/null +++ b/lib/alveolata/database/abstract/interface.php @@ -0,0 +1,59 @@ + + */ +interface interface_database +{ + + /** + * shall return the terminal symbol used for indicating auto incrementation for a column + * + * @return string + * @author Christian Fraß + */ + public function terminal_autoincrement( + ) : string + ; + + + /** + * shall return the definition for a field, which is meant to be an integer typed primary key with auto increment + * setting + * + * @return string + * @author Christian Fraß + */ + public function boilerplate_field_definition_for_integer_primary_key_with_auto_increment( + ) : string + ; + + + /** + * shall send a query to the database + * + * @param string $template an SQL query string with placeholders of the form ":name" + * @param array $arguments record values to insert for the placeholders + * @return array { + * record< + * rows:list>, + * id:(null|integer), + * affected:integer + * > + * } + * @author Christian Fraß + */ + public function query( + string $template, + array $arguments = [] + ) : array + ; + +} + + ?> diff --git a/lib/alveolata/database/functions.php b/lib/alveolata/database/functions.php new file mode 100644 index 0000000..b973cf9 --- /dev/null +++ b/lib/alveolata/database/functions.php @@ -0,0 +1,61 @@ + + */ +function make( + string $kind, + array $parameters +) : interface_database +{ + switch ($kind) { + default: { + throw (new \Exception(sprintf('invalid database kind "%s"', $kind))); + break; + } + case 'sqlite': { + return ( + implementation_sqlite::make( + $parameters['path'], + $parameters['verbosity'] ?? 0 + ) + ); + break; + } + case 'mysql': { + return ( + implementation_mysql::make( + $parameters['host'], + $parameters['port'], + $parameters['schema'], + $parameters['username'], + $parameters['password'] + ) + ); + break; + } + case 'postgresql': { + return ( + implementation_postgresql::make( + $parameters['host'], + $parameters['port'], + $parameters['schema'], + $parameters['username'], + $parameters['password'] + ) + ); + break; + } + } +} + + ?> diff --git a/lib/alveolata/database/implementation-mysql/functions.php b/lib/alveolata/database/implementation-mysql/functions.php new file mode 100644 index 0000000..0f22e4d --- /dev/null +++ b/lib/alveolata/database/implementation-mysql/functions.php @@ -0,0 +1,188 @@ + + */ +class struct_subject_mysql { + + /** + * @var string + * @author Christian Fraß + */ + public $host; + + + /** + * @var int + * @author Christian Fraß + */ + public $port; + + + /** + * @var string + * @author Christian Fraß + */ + public $schema; + + + /** + * @var string + * @author Christian Fraß + */ + public $username; + + + /** + * @var string + * @author Christian Fraß + */ + public $password; + + + + /** + * @author Christian Fraß + */ + public function __construct( + string $host, + int $port, + string $schema, + string $username, + string $password + ) + { + $this->host = $host; + $this->port = $port; + $this->schema = $schema; + $this->username = $username; + $this->password = $password; + } + +} + + +/** + * @author Christian Fraß + */ +function mysql_make( + string $host, + int $port, + string $schema, + string $username, + string $password +) : struct_subject_mysql +{ + return ( + new struct_subject_mysql( + $host, + $port, + $schema, + $username, + $password + ) + ); +} + + +/** + * @return string + * @author Christian Fraß + */ +function mysql_terminal_autoincrement( +) : string +{ + return 'AUTO_INCREMENT'; +} + + +/** + * @return string + * @author Christian Fraß + */ +function mysql_boilerplate_field_definition_for_integer_primary_key_with_auto_increment( +) : string +{ + return 'INTEGER PRIMARY KEY AUTO_INCREMENT'; +} + + +/** + * @param struct_subject_mysql $subject + * @param string $template + * @param array $arguments + * @return array + * @author Christian Fraß + */ +function mysql_query( + struct_subject_mysql $subject, + string $template, + array $arguments +) : array +{ + $connection = \mysqli_connect( + $subject->host, + $subject->username, + $subject->password, + $subject->schema, + $subject->port + ); + \mysqli_options($connection, MYSQLI_OPT_INT_AND_FLOAT_NATIVE, true); + \mysqli_set_charset($connection, 'utf8'); + \mysqli_query($connection, 'set names \'utf8\''); + $query = $template; + foreach ($arguments as $key => $value) { + $pattern = sprintf(':%s', $key); + $replacement = \alveolata\sql\format( + $value, + function ($x) use ($connection) { + return \mysqli_real_escape_string($connection, $x); + } + ); + $query = str_replace($pattern, $replacement, $query); + } + $report = \alveolata\report\make( + 'query', + [ + 'query' => $query, + ] + ); + \alveolata\log\debug_($report); + $result = \mysqli_query($connection, $query); + if ($result === false) { + $report = \alveolata\report\make( + 'query failed', + [ + 'query' => $query, + 'reason' => \mysqli_error($connection), + ] + ); + throw (\alveolata\report\as_exception($report)); + } + else if ($result === true) { + $output = [ + 'rows' => [], + 'id' => mysqli_insert_id($connection), + 'affected' => \mysqli_affected_rows($connection), + ]; + return $output; + } + else { + $output = [ + 'rows' => $result->fetch_all(MYSQLI_ASSOC), + 'id' => 0, + 'affected' => 0, + ]; + return $output; + } +} + +?> diff --git a/lib/alveolata/database/implementation-mysql/wrapper-class.php b/lib/alveolata/database/implementation-mysql/wrapper-class.php new file mode 100644 index 0000000..6fb1fc1 --- /dev/null +++ b/lib/alveolata/database/implementation-mysql/wrapper-class.php @@ -0,0 +1,56 @@ + + */ +class implementation_mysql implements interface_database +{ + + /** + * @var struct_subject_mysql + * @author Christian Fraß + */ + private $subject; + + + /** + * @author Christian Fraß + */ + private function __construct(struct_subject_mysql $subject) {$this->subject = $subject;} + + + /** + * @author Christian Fraß + */ + public static function make( + string $host, + int $port, + string $schema, + string $username, + string $password + ) : implementation_mysql + { + $subject = mysql_make($host, $port, $schema, $username, $password); + return (new implementation_mysql($subject)); + } + + + /** + * implementations + * + * @author Christian Fraß + */ + public function terminal_autoincrement() : string {return mysql_terminal_autoincrement();} + public function boilerplate_field_definition_for_integer_primary_key_with_auto_increment() : string {return mysql_boilerplate_field_definition_for_integer_primary_key_with_auto_increment();} + public function query(string $template, array $arguments = []) : array {return mysql_query($this->subject, $template, $arguments);} + +} + + ?> diff --git a/lib/alveolata/database/implementation-postgresql/functions.php b/lib/alveolata/database/implementation-postgresql/functions.php new file mode 100644 index 0000000..8bb5ff5 --- /dev/null +++ b/lib/alveolata/database/implementation-postgresql/functions.php @@ -0,0 +1,203 @@ + + * requires PHP module "pgsql" (Debian package name "php-pgsql") + */ +class struct_subject_postgresql { + + /** + * @var string + * @author Christian Fraß + */ + public $host; + + + /** + * @var int + * @author Christian Fraß + */ + public $port; + + + /** + * @var string + * @author Christian Fraß + */ + public $schema; + + + /** + * @var string + * @author Christian Fraß + */ + public $username; + + + /** + * @var string + * @author Christian Fraß + */ + public $password; + + + + /** + * @author Christian Fraß + */ + public function __construct( + string $host, + int $port, + string $schema, + string $username, + string $password + ) + { + $this->host = $host; + $this->port = $port; + $this->schema = $schema; + $this->username = $username; + $this->password = $password; + } + +} + + +/** + * @author Christian Fraß + */ +function postgresql_make( + string $host, + int $port, + string $schema, + string $username, + string $password +) : struct_subject_postgresql +{ + return ( + new struct_subject_postgresql( + $host, + $port, + $schema, + $username, + $password + ) + ); +} + + +/** + * @return string + * @author Christian Fraß + */ +function postgresql_terminal_autoincrement( +) : string +{ + return 'AUTO_INCREMENT'; +} + + +/** + * @return string + * @author Christian Fraß + */ +function postgresql_boilerplate_field_definition_for_integer_primary_key_with_auto_increment( +) : string +{ + return 'SERIAL PRIMARY KEY'; +} + + +/** + * @param struct_subject_postgresql $subject + * @param string $template + * @param array $arguments + * @return array + * @author Christian Fraß + */ +function postgresql_query( + struct_subject_postgresql $subject, + string $template, + array $arguments +) : array +{ + $connection = \pg_connect( + sprintf( + 'host=%s port=%d user=%s password=%s dbname=%s', + $subject->host, + $subject->port, + $subject->username, + $subject->password, + $subject->schema + ) + ); + // \pg_set_client_encoding($connection, \UNICODE); + \pg_query($connection, "SET client_encoding TO 'UNICODE'"); + $template_adjusted = $template; + $arguments_adjusted = []; + $counter = 0; + foreach ($arguments as $key => $value) { + $pattern = \sprintf(':%s', $key); + $replacement = \sprintf('$%d', $counter+1); + $counter += 1; + $template_adjusted = str_replace($pattern, $replacement, $template_adjusted); + \array_push($arguments_adjusted, $value); + } + $report = \alveolata\report\make( + 'postgresl_query', + [ + 'template' => $template_adjusted, + 'arguments' => $arguments, + ] + ); + \alveolata\log\debug_($report); + $result = \pg_query_params($connection, $template_adjusted, $arguments_adjusted); + if ($result === false) { + $report = \alveolata\report\make( + 'postgresl_query_failed', + [ + 'template' => $template_adjusted, + 'arguments' => $arguments_adjusted, + ] + ); + throw (\alveolata\report\as_exception($report)); + } + else { + // $status = \pg_result_status($result, \PGSQL_STATUS_STRING); + $status = \pg_result_status($result, \PGSQL_STATUS_LONG); + switch ($status) { + default: { + $report = \alveolata\report\make( + 'postgresl_query_bad_result', + [ + 'template' => $template_adjusted, + 'arguments' => $arguments_adjusted, + 'status' => $status, + ] + ); + throw (\alveolata\report\as_exception($report)); + break; + } + case \PGSQL_COMMAND_OK: + case \PGSQL_TUPLES_OK: { + $id_raw = \pg_last_oid($result); + return [ + 'rows' => \pg_fetch_all($result, \PGSQL_ASSOC), + 'id' => (($id_raw === false) ? null : $id_raw), + 'affected' => \pg_affected_rows($result), + ]; + break; + } + } + } +} + +?> diff --git a/lib/alveolata/database/implementation-postgresql/wrapper-class.php b/lib/alveolata/database/implementation-postgresql/wrapper-class.php new file mode 100644 index 0000000..e55825f --- /dev/null +++ b/lib/alveolata/database/implementation-postgresql/wrapper-class.php @@ -0,0 +1,56 @@ + + */ +class implementation_postgresql implements interface_database +{ + + /** + * @var struct_subject_postgresql + * @author Christian Fraß + */ + private $subject; + + + /** + * @author Christian Fraß + */ + private function __construct(struct_subject_postgresql $subject) {$this->subject = $subject;} + + + /** + * @author Christian Fraß + */ + public static function make( + string $host, + int $port, + string $schema, + string $username, + string $password + ) : implementation_postgresql + { + $subject = postgresql_make($host, $port, $schema, $username, $password); + return (new implementation_postgresql($subject)); + } + + + /** + * implementations + * + * @author Christian Fraß + */ + public function terminal_autoincrement() : string {return postgresql_terminal_autoincrement();} + public function boilerplate_field_definition_for_integer_primary_key_with_auto_increment() : string {return postgresql_boilerplate_field_definition_for_integer_primary_key_with_auto_increment();} + public function query(string $template, array $arguments = []) : array {return postgresql_query($this->subject, $template, $arguments);} + +} + + ?> diff --git a/lib/alveolata/database/implementation-sqlite/functions.php b/lib/alveolata/database/implementation-sqlite/functions.php new file mode 100644 index 0000000..f322538 --- /dev/null +++ b/lib/alveolata/database/implementation-sqlite/functions.php @@ -0,0 +1,144 @@ + + */ +class struct_subject_sqlite { + + /** + * @var string + * @author Christian Fraß + */ + public $path; + + + /** + * @var int + * @author Christian Fraß + */ + public $verbosity; + + + /** + * @author Christian Fraß + */ + public function __construct( + string $path, + int $verbosity + ) + { + $this->path = $path; + $this->verbosity = $verbosity; + } + +} + + + +/** + * @author Christian Fraß + */ +function sqlite_make( + string $path, + int $verbosity = 0 +) +{ + return ( + new struct_subject_sqlite( + $path, + $verbosity + ) + ); +} + + +/** + * @return string + * @author Christian Fraß + */ +function sqlite_terminal_autoincrement( +) : string +{ + return 'AUTOINCREMENT'; +} + + +/** + * @return string + * @author Christian Fraß + */ +function sqlite_boilerplate_field_definition_for_integer_primary_key_with_auto_increment( +) : string +{ + return 'INTEGER PRIMARY KEY AUTOINCREMENT'; +} + + +/** + * @param struct_subject_sqlite $subject + * @param string $template + * @param array $arguments + * @return array + * @author Christian Fraß + */ +function sqlite_query( + struct_subject_sqlite $subject, + string $template, + array $arguments +) : array +{ + $sqlite3 = new \SQLite3($subject->path); + $query = $template; + foreach ($arguments as $key => $value) { + $pattern = sprintf(':%s', $key); + $replacement = \alveolata\sql\format($value, function ($x) {return \SQLite3::escapeString($x);}); + $query = str_replace($pattern, $replacement, $query); + } + $report = \alveolata\report\make( + 'query', + [ + 'query' => \alveolata\string\replace( + $query, + [ + "\n" => ' ', + "\t" => ' ', + ] + ), + ] + ); + \alveolata\log\debug_($report); + $result = $sqlite3->query($query); + if ($result === false) { + $report = \alveolata\report\make( + 'query failed', + [ + 'query' => $query, + ] + ); + throw (\alveolata\report\as_exception($report)); + } + else { + $output = [ + 'rows' => [], + 'id' => $sqlite3->lastInsertRowId(), + 'affected' => $sqlite3->changes(), + ]; + if ($result->numColumns() > 0) { + while ($row = $result->fetchArray(SQLITE3_ASSOC)) { + array_push($output['rows'], $row); + } + } + return $output; + } +} + +?> diff --git a/lib/alveolata/database/implementation-sqlite/wrapper-class.php b/lib/alveolata/database/implementation-sqlite/wrapper-class.php new file mode 100644 index 0000000..b32e352 --- /dev/null +++ b/lib/alveolata/database/implementation-sqlite/wrapper-class.php @@ -0,0 +1,53 @@ + + */ +class implementation_sqlite implements interface_database +{ + + /** + * @var struct_subject_sqlite + * @author Christian Fraß + */ + private $subject; + + + /** + * @author Christian Fraß + */ + private function __construct(struct_subject_sqlite $subject) {$this->subject = $subject;} + + + /** + * @author Christian Fraß + */ + public static function make( + string $path, + int $verbosity = 0 + ) : implementation_sqlite + { + $subject = sqlite_make($path, $verbosity); + return (new implementation_sqlite($subject)); + } + + + /** + * implementations + * + * @author Christian Fraß + */ + public function terminal_autoincrement() : string {return sqlite_terminal_autoincrement();} + public function boilerplate_field_definition_for_integer_primary_key_with_auto_increment() : string {return sqlite_boilerplate_field_definition_for_integer_primary_key_with_auto_increment();} + public function query(string $template, array $arguments = []) : array {return sqlite_query($this->subject, $template, $arguments);} + +} + + ?> diff --git a/lib/alveolata/definitions.php b/lib/alveolata/definitions.php new file mode 100644 index 0000000..b60922a --- /dev/null +++ b/lib/alveolata/definitions.php @@ -0,0 +1,14 @@ + diff --git a/lib/alveolata/email/abstract/interface.php b/lib/alveolata/email/abstract/interface.php new file mode 100644 index 0000000..7ead1fd --- /dev/null +++ b/lib/alveolata/email/abstract/interface.php @@ -0,0 +1,22 @@ + diff --git a/lib/alveolata/email/base.php b/lib/alveolata/email/base.php new file mode 100644 index 0000000..1385d53 --- /dev/null +++ b/lib/alveolata/email/base.php @@ -0,0 +1,244 @@ + 'host', + 'default' => \alveolata\pod\class_pod::toom(), + ], + [ + 'name' => 'port', + 'default' => \alveolata\pod\class_pod::full(587), + ], + [ + 'name' => 'negotiation_type', + 'default' => \alveolata\pod\class_pod::full(enum_negotiation_type::forced), + ], + [ + 'name' => 'auth_type', + 'default' => \alveolata\pod\class_pod::full(enum_auth_type::login), + ], + [ + 'name' => 'username', + 'default' => \alveolata\pod\class_pod::toom(), + ], + [ + 'name' => 'password', + 'default' => \alveolata\pod\class_pod::toom(), + ], + ]; + foreach ($fields as $field) { + if ( + (! $field['default']->has()) + && + (! array_key_exists($field['name'], $raw)) + ) { + throw (new \Exception(sprintf('mandatory parameter "%s" missing', $field['name']))); + } + else { + $this->{$field['name']} = ($raw[$field['name']] ?? $field['default']->get()); + } + } + } + +} + + +/** + */ +class struct_data { + + /** + * @param array {list} + */ + public $to; + + + /** + * @param ?string {(null | string)} + */ + public $subject; + + + /** + * @param ?string {(null | string)} + */ + public $body_plain; + + + /** + * @param ?string {(null | string)} + */ + public $body_html; + + + /** + * @param ?string {(null | string)} + */ + public $from; + + + /** + * @param array {list} + */ + public $cc; + + + /** + * @param array {list} + */ + public $bcc; + + + /** + * @param ?string (null | string) + */ + public $reply_to; + + + /** + * @param array {list>} + */ + public $attachments; + + + /** + */ + public function __construct( + array $raw + ) + { + $fields = [ + [ + 'name' => 'to', + 'default' => \alveolata\pod\class_pod::toom(), + ], + [ + 'name' => 'subject', + 'default' => \alveolata\pod\class_pod::toom(), + ], + [ + 'name' => 'body_plain', + 'default' => \alveolata\pod\class_pod::full(null), + ], + [ + 'name' => 'body_html', + 'default' => \alveolata\pod\class_pod::full(null), + ], + [ + 'name' => 'from', + 'default' => \alveolata\pod\class_pod::full(null), + ], + [ + 'name' => 'cc', + 'default' => \alveolata\pod\class_pod::full([]), + ], + [ + 'name' => 'bcc', + 'default' => \alveolata\pod\class_pod::full([]), + ], + [ + 'name' => 'reply_to', + 'default' => \alveolata\pod\class_pod::full(null), + ], + [ + 'name' => 'attachments', + 'default' => \alveolata\pod\class_pod::full([]), + ], + ]; + foreach ($fields as $field) { + if ( + (! $field['default']->has()) + && + (! array_key_exists($field['name'], $raw)) + ) { + throw (new \Exception(sprintf('mandatory parameter "%s" missing', $field['name']))); + } + else { + $this->{$field['name']} = ($raw[$field['name']] ?? $field['default']->get()); + } + } + } + +} + + ?> diff --git a/lib/alveolata/email/functions.php b/lib/alveolata/email/functions.php new file mode 100644 index 0000000..2f84da5 --- /dev/null +++ b/lib/alveolata/email/functions.php @@ -0,0 +1,75 @@ + + * } + * @param array $data { + * record< + * to:list, + * subject:string, + * ?body_plain:string, + * ?body_html:string, + * ?from:string, + * ?cc:list, + * ?bcc:list, + * ?reply_to:string, + * ?attachments:list> + * > + * } + * @throws Exception if mail can not be sent + * @author Christian Fraß + */ +function send( + array $credentials_raw, + array $data_raw, + array $options = [] +) : void +{ + $options = \array_merge( + [ + 'implementation' => 'swift', + ], + $options + ); + + $credentials = new struct_credentials($credentials_raw); + $data = new struct_data($data_raw); + + switch ($options['implementation']) { + default: + case 'legacy': { + implementations\legacy\send($credentials, $data); + break; + } + case 'pear': { + implementations\pear\send($credentials, $data); + break; + } + case 'swift': { + implementations\swift\send($credentials, $data); + break; + } + case 'phpmailer': { + implementations\phpmailer\send($credentials, $data); + break; + } + } +} + + ?> diff --git a/lib/alveolata/email/implementation-legacy/functions.php b/lib/alveolata/email/implementation-legacy/functions.php new file mode 100644 index 0000000..a58588b --- /dev/null +++ b/lib/alveolata/email/implementation-legacy/functions.php @@ -0,0 +1,96 @@ + + */ +function send( + \alveolata\email\struct_credentials $credentials, + \alveolata\email\struct_data $data +) : void +{ + // constants + $separator = \md5(\time()); + + // helpers + $headerEntry = function ($key, $value, $args = []) { + return \sprintf( + "%s: %s%s\r\n", + $key, + $value, + \implode( + '', + \array_map( + fn($x) => \sprintf('; %s=%s', $x, $args[$x]), + \array_keys($args) + ) + ) + ); + }; + $separatorEntry = function ($final) use ($separator) { + return ( + $final + ? \sprintf("--%s--\r\n", $separator) + : \sprintf("--%s\r\n", $separator) + ); + }; + $textEntry = function ($content) { + return \sprintf("\r\n%s\r\n", $content); + }; + + // vars + $message = ''; + $additional_headers = ''; + $additional_headers .= $headerEntry('To', \implode(',', $data->to)); + $additional_headers .= $headerEntry('Subject', $data->subject); + if (! empty($data->from)) { + $additional_headers .= $headerEntry('From', $data->from); + } + if (! empty($data->reply_to)) { + $additional_headers .= $headerEntry('Reply-To', $data->reply_to); + } + if (! empty($data->cc)) { + $additional_headers .= $headerEntry('Cc', \implode(',', $data->cc)); + } + if (! empty($data->bcc)) { + $additional_headers .= $headerEntry('Bcc', \implode(',', $data->bcc)); + } + if (empty($data->attachments)) { + $additional_headers .= $headerEntry('Content-Type', 'text/plain', ['charset' => 'utf-8']); + $message .= $textEntry(\wordwrap($data->body_plain)); + } + else { + $additional_headers .= $headerEntry('MIME-Version', '1.0'); + $additional_headers .= $headerEntry('Content-Type', 'multipart/mixed', ['boundary' => $separator]); + $message .= $separatorEntry(false); + $message .= $headerEntry('Content-Type', 'text/plain', ['charset' => 'utf-8']); + $message .= $headerEntry('Content-Transfer-Encoding', '7bit'); + $message .= $textEntry(\wordwrap($data->body_plain)); + foreach ($data->attachments as $attachment) { + $message .= $separatorEntry(false); + $message .= $headerEntry('Content-Transfer-Encoding', 'base64'); + $message .= $headerEntry('Content-Type', $attachment['mime'] ?? 'application/octet-stream'); + $message .= $headerEntry('Content-Disposition', 'attachment', ['filename' => ($attachment['name'] ?? \basename($attachment['path']))]); + $message .= $textEntry(\chunk_split(\base64_encode(\file_get_contents($attachment['path'])))); + } + $message .= $separatorEntry(true); + } + + // exec + $successful = \mail( + \implode(',', $data->to), + $data->subject, + $message, + $additional_headers + ); + if (! $successful) { + throw (new \Exception(\sprintf('mail_could_not_be_sent'))); + } +} + + ?> diff --git a/lib/alveolata/email/implementation-legacy/wrapper-class.php b/lib/alveolata/email/implementation-legacy/wrapper-class.php new file mode 100644 index 0000000..5279a38 --- /dev/null +++ b/lib/alveolata/email/implementation-legacy/wrapper-class.php @@ -0,0 +1,26 @@ + diff --git a/lib/alveolata/email/implementation-pear/functions.php b/lib/alveolata/email/implementation-pear/functions.php new file mode 100644 index 0000000..8b774c0 --- /dev/null +++ b/lib/alveolata/email/implementation-pear/functions.php @@ -0,0 +1,99 @@ + false, + \alveolata\email\enum_auth_type::plain => 'PLAIN', + \alveolata\email\enum_auth_type::login => 'LOGIN', + \alveolata\email\enum_auth_type::gssapi => 'GSSAPI', + \alveolata\email\enum_auth_type::digest_md5 => 'DIGEST-MD5', + \alveolata\email\enum_auth_type::cram_md5 => 'CRAM-MD5', + ]; + if (! \array_key_exists($credentials->auth_type, $auth_type_map)) { + throw (new \Exception(\sprintf('unsupported auth method "%s"', $credentials->auth_type))); + } + else { + $auth = $auth_type_map[$credentials->auth_type]; + + /** + * @see https://pear.php.net/manual/en/package.mail.mail.factory.php + */ + $sender = \Mail::factory( + 'smtp', + [ + 'host' => $credentials->host, + 'port' => \sprintf('%u', $credentials->port), + 'auth' => $auth, + 'username' => $credentials->username, + 'password' => $credentials->password, + // 'localhost' => null, + // 'timeout' => null, + // 'verp' => null, + // 'debug' => null, + // 'persist' => null, + // 'pipelining' => null, + // 'socket_options' => null, + ] + ); + + // headers & body + $headers_raw = []; + $headers_raw['To'] = \implode(',', $data->to); + $headers_raw['Subject'] = $data->subject; + if (! empty($data->from)) { + $headers_raw['From'] = $data->from; + } + if (! empty($data->reply_to)) { + $headers_raw['Reply-To'] = $data->reply_to; + } + if (empty($data->attachments)) { + $headers = $headers_raw; + $body = $data->body_plain; + } + else { + $mime = new \Mail_mime(); + if (! empty($data->body_plain)) { + $mime->setTXTBody($data->body_plain); + } + if (! empty($data->body_html)) { + $mime->setHTMLBody($data->body_html); + } + foreach ($data->attachments as $attachment) { + $mime->addAttachment( + $attachment['path'], + ($attachment['mime'] ?? 'application/octet-stream'), + ($attachment['name'] ?? \basename($attachment['path'])) + ); + } + $headers = $mime->headers($headers_raw); + $body = $mime->get(); + } + + // exec + $mail = $sender->send( + \implode(',', $data->to), + $headers, + $body + ); + if (\PEAR::isError($mail)) { + throw (new \Exception(\sprintf('mail_could_not_be_sent: %s', $mail->getMessage()))); + } + } +} + + ?> diff --git a/lib/alveolata/email/implementation-pear/wrapper-class.php b/lib/alveolata/email/implementation-pear/wrapper-class.php new file mode 100644 index 0000000..45e4c56 --- /dev/null +++ b/lib/alveolata/email/implementation-pear/wrapper-class.php @@ -0,0 +1,26 @@ + diff --git a/lib/alveolata/email/implementation-phpmailer/functions.php b/lib/alveolata/email/implementation-phpmailer/functions.php new file mode 100644 index 0000000..90fb01b --- /dev/null +++ b/lib/alveolata/email/implementation-phpmailer/functions.php @@ -0,0 +1,73 @@ + + */ +function send( + \alveolata\email\struct_credentials $credentials, + \alveolata\email\struct_data $data +) : void +{ + $phpmailer = new \PHPMailer\PHPMailer\PHPMailer(true); + + // auth + $phpmailer->Host = $credentials->host; + $phpmailer->Port = \sprintf('%u', $credentials->port); + $phpmailer->Username = $credentials->username; + $phpmailer->Password = $credentials->password; + $phpmailer->isSMTP(); + // $phpmailer->SMTPDebug = \PHPMailer\PHPMailer\SMTP::DEBUG_SERVER; + $phpmailer->SMTPAuth = ($credentials->negotiation_type !== \alveolata\email\enum_negotiation_type::none); + $phpmailer->SMTPSecure = ([ + \alveolata\email\enum_negotiation_type::none => null, + \alveolata\email\enum_negotiation_type::opportunistic => \PHPMailer\PHPMailer\PHPMailer::ENCRYPTION_STARTTLS, + \alveolata\email\enum_negotiation_type::forced => \PHPMailer\PHPMailer\PHPMailer::ENCRYPTION_SMTPS, + ][$credentials->negotiation_type]); + + // data + foreach ($data->to as $address) { + $phpmailer->addAddress($address); + } + $phpmailer->Subject = $data->subject; + if (! empty($data->from)) { + $phpmailer->setFrom($data->from); + } + if (! empty($data->reply_to)) { + $phpmailer->addReplyTo($data->reply_to); + } + foreach ($data->cc as $address) { + $phpmailer->addCC($address); + } + foreach ($data->bcc as $address) { + $phpmailer->addBCC($address); + } + foreach ($data->attachments as $attachment) { + $phpmailer->addAttachment($attachment); + } + + // body + if (! empty($data->body_html)) { + $phpmailer->isHTML(true); + $phpmailer->Body = $data->body_html; + if (! empty($data->body_plain)) { + $phpmailer->AltBody = $data->body_plain; + } + } + else { + $phpmailer->Body = $data->body_plain; + } + + // send + $phpmailer->send(); +} + + ?> diff --git a/lib/alveolata/email/implementation-phpmailer/wrapper-class.php b/lib/alveolata/email/implementation-phpmailer/wrapper-class.php new file mode 100644 index 0000000..7c2354f --- /dev/null +++ b/lib/alveolata/email/implementation-phpmailer/wrapper-class.php @@ -0,0 +1,26 @@ + diff --git a/lib/alveolata/email/implementation-swift/functions.php b/lib/alveolata/email/implementation-swift/functions.php new file mode 100644 index 0000000..f68e91b --- /dev/null +++ b/lib/alveolata/email/implementation-swift/functions.php @@ -0,0 +1,68 @@ + + */ +function send( + \alveolata\email\struct_credentials $credentials, + \alveolata\email\struct_data $data +) : void +{ + // Create the Transport + $transport = new \Swift_SmtpTransport( + $credentials->host, + $credentials->port, + [ + \alveolata\email\enum_negotiation_type::none => null, + \alveolata\email\enum_negotiation_type::opportunistic => 'tls', + \alveolata\email\enum_negotiation_type::forced => 'ssl', + ][$credentials->negotiation_type] + ); + $transport->setUsername($credentials->username); + $transport->setPassword($credentials->password); + + // Create the Mailer using your created Transport + $mailer = new \Swift_Mailer($transport); + + // Create a message + $message = new \Swift_Message(); + if (! empty($data->from)) { + $message->setFrom([$data->from]); + } + $message->setTo($data->to); + if (! empty($data->cc)) { + $message->setCc($data->cc); + } + if (! empty($data->bcc)) { + $message->setBcc($data->bcc); + } + $message->setSubject($data->subject); + + if (! empty($data->body_html)) { + $message->setBody($data->body_html, 'text/html'); + if (! empty($data->body_plain)) { + $message->addPart($data->body_plain, 'text/plain'); + } + } + else { + $message->setBody($data->body_plain, 'text/plain'); + } + + foreach ($data->attachments as $attachment) { + $attachment_ = \Swift_Attachment::fromPath($attachment['path'], $attachment['mime']); + $attachment_->setFilename($attachment['name']); + $message->attach($attachment_); + } + + // Send the message + $result = $mailer->send($message); +} diff --git a/lib/alveolata/email/implementation-swift/wrapper-class.php b/lib/alveolata/email/implementation-swift/wrapper-class.php new file mode 100644 index 0000000..d74d71c --- /dev/null +++ b/lib/alveolata/email/implementation-swift/wrapper-class.php @@ -0,0 +1,26 @@ + diff --git a/lib/alveolata/file/functions.php b/lib/alveolata/file/functions.php new file mode 100644 index 0000000..a879869 --- /dev/null +++ b/lib/alveolata/file/functions.php @@ -0,0 +1,81 @@ + + */ +function exists( + string $path +) : bool +{ + return file_exists($path); +} + + +/** + * @param string $path + * @return string + * @author Christian Fraß + */ +function read( + string $path +) : string +{ + if (! exists($path)) { + throw (new \Exception(sprintf('file not found: "%s"', $path))); + } + else { + $content = file_get_contents($path); + if ($content === false) { + throw (new \Exception('could not read file')); + } + else { + return $content; + } + } +} + + +/** + * @param string $path + * @param string $content + * @author Christian Fraß + */ +function write( + string $path, + string $content, + bool $create_directory_if_missing = false +) +{ + if ($create_directory_if_missing) { + $directory = implode('/', array_slice(explode('/', $path), 0, -1)); + if (! file_exists($directory)) { + mkdir($directory); + } + } + file_put_contents($path, $content); +} + + +/** + * @param string $path + * @author Christian Fraß + */ +function remove( + string $path +) +{ + if (! exists($path)) { + throw (new \Exception(sprintf('file not found: "%s"', $path))); + } + else { + unlink($path); + } +} + diff --git a/lib/alveolata/html/functions.php b/lib/alveolata/html/functions.php new file mode 100644 index 0000000..95d60af --- /dev/null +++ b/lib/alveolata/html/functions.php @@ -0,0 +1,61 @@ +} + * @param array $data {list>} + */ +function table( + array $titles, + array $data +) : string +{ + $html = ''; + $html .= ("\n"); + if (! is_null($titles)) { + $html .= ("\n"); + $html .= sprintf( + "%s\n", + implode( + '', + array_map( + function (string $title) : string { + return sprintf( + "", + $title + ); + }, + $titles + ) + ) + ); + $html .= ("\n"); + } + { + $html .= ("\n"); + foreach ($data as $line) { + $html .= sprintf( + "%s\n", + implode( + '', + array_map( + function (string $field) : string { + return sprintf( + "", + $field + ); + }, + $line + ) + ) + ); + } + $html .= ("\n"); + } + $html .= ('
    %s
    %s
    ' . "\n"); + return $html; +} + + ?> diff --git a/lib/alveolata/html/test.spec.php b/lib/alveolata/html/test.spec.php new file mode 100644 index 0000000..554e428 --- /dev/null +++ b/lib/alveolata/html/test.spec.php @@ -0,0 +1,167 @@ + 'alveolata', + 'sections' => [ + [ + 'name' => 'html', + 'setup' => function (&$environment) { + }, + 'sections' => [ + [ + 'name' => 'parse', + 'cases' => [ + [ + 'name' => 'oblique', + 'procedure' => function ($assert, &$environment) { + $html = 'foo bar baz qux'; + $xmlnode = \alveolata\html\parse($html); + $output_actual = $xmlnode->to_raw(); + $output_expected = [ + 'kind' => 'group', + 'data' => [ + 'members' => [ + [ + 'kind' => 'complex', + 'data' => [ + 'name' => 'html', + 'attributes' => [], + 'subnode' => [ + 'kind' => 'group', + 'data' => [ + 'members' => [ + [ + 'kind' => 'complex', + 'data' => [ + 'name' => 'head', + 'attributes' => [], + 'subnode' => [ + 'kind' => 'group', + 'data' => [ + 'members' => [ + [ + 'kind' => 'complex', + 'data' => [ + 'name' => 'meta', + 'attributes' => [ + 'charset' => 'utf-8' + ], + 'subnode' => [ + 'kind' => 'group', + 'data' => [ + 'members' => [] + ] + ] + ] + ] + ] + ] + ] + ] + ], + [ + 'kind' => 'complex', + 'data' => [ + 'name' => 'body', + 'attributes' => [], + 'subnode' => [ + 'kind' => 'group', + 'data' => [ + 'members' => [ + [ + 'kind' => 'complex', + 'data' => [ + 'name' => 'span', + 'attributes' => [ + 'class' => 'abc def' + ], + 'subnode' => [ + 'kind' => 'group', + 'data' => [ + 'members' => [ + [ + 'kind' => 'text', + 'data' => [ + 'content' => 'foo' + ] + ], + [ + 'kind' => 'complex', + 'data' => [ + 'name' => 'b', + 'attributes' => [], + 'subnode' => [ + 'kind' => 'group', + 'data' => [ + 'members' => [ + [ + 'kind' => 'text', + 'data' => [ + 'content' => 'bar' + ] + ] + ] + ] + ] + ] + ], + [ + 'kind' => 'text', + 'data' => [ + 'content' => 'baz' + ] + ], + [ + 'kind' => 'complex', + 'data' => [ + 'name' => 'i', + 'attributes' => [], + 'subnode' => [ + 'kind' => 'group', + 'data' => [ + 'members' => [ + [ + 'kind' => 'text', + 'data' => [ + 'content' => 'qux' + ] + ] + ] + ] + ] + ] + ] + ] + ] + ] + ] + ] + ] + ] + ] + ] + ] + ] + ] + ] + ] + ] + ] + ] + ]; + $assert->equal($output_actual, $output_expected); + }, + ], + ], + ], + ], + ] + ] + ] +); + + ?> diff --git a/lib/alveolata/http/functions.php b/lib/alveolata/http/functions.php new file mode 100644 index 0000000..3beb129 --- /dev/null +++ b/lib/alveolata/http/functions.php @@ -0,0 +1,309 @@ + + */ +function method_to_oas( + string $http_method +) : string +{ + return \strtolower($http_method); +} + + +/** + * @param string $input + * @return \alveolata\http\struct_request + * @author Christian Fraß + */ +function request_decode( + string $input +) : \alveolata\http\struct_request +{ + $input_ = str_replace("\r\n", "\n", $input); + $parts = explode("\n", $input_); + $chunks = []; + $request_method = null; + $request_path = null; + $request_protocol = null; + $request_headers = []; + $request_body = ''; + foreach ($parts as $part) { + if (preg_match('/^([A-Z]+) ([\S]*) (.*)$/', $part, $matches) === 1) { + $request_method = strtoupper($matches[1]); + $request_path = $matches[2]; + $request_protocol = $matches[3]; + } + else if (preg_match('/^([^:]+): (.*)$/', $part, $matches) === 1) { + $request_headers[$matches[1]] = $matches[2]; + } + else if ($part !== '') { + array_push($chunks, $part); + } + else { + // ignore + } + } + $request_body .= implode("\n", $chunks); + $request = new \alveolata\http\struct_request( + $request_protocol, + $request_method, + $request_path, + $request_headers, + $request_body + ); + return $request; +} + + +/** + * @param \alveolata\http\struct_response $response + * @return string + * @author Christian Fraß + */ +function response_encode( + \alveolata\http\struct_response $response +) : string +{ + $code_text_map = [ + '100' => 'Continue', + '101' => 'Switching Protocols', + '103' => 'Early Hints', + '200' => 'OK', + '201' => 'Created', + '202' => 'Accepted', + '203' => 'Non-Authoritative Information', + '204' => 'No Content', + '205' => 'Reset Content', + '206' => 'Partial Content', + '300' => 'Multiple Choices', + '301' => 'Moved Permanently', + '302' => 'Found', + '303' => 'See Other', + '304' => 'Not Modified', + '307' => 'Temporary Redirect', + '308' => 'Permanent Redirect', + '400' => 'Bad Request', + '401' => 'Unauthorized', + '402' => 'Payment Required', + '403' => 'Forbidden', + '404' => 'Not Found', + '405' => 'Method Not Allowed', + '406' => 'Not Acceptable', + '407' => 'Proxy Authentication Required', + '408' => 'Request Timeout', + '409' => 'Conflict', + '410' => 'Gone', + '411' => 'Length Required', + '412' => 'Precondition Failed', + '413' => 'Payload Too Large', + '414' => 'URI Too Long', + '415' => 'Unsupported Media Type', + '416' => 'Range Not Satisfiable', + '417' => 'Expectation Failed', + '418' => 'I\'m a teapot', + '422' => 'Unprocessable Entity', + '424' => 'Failed Dependency', + '425' => 'Too Early', + '426' => 'Upgrade Required', + '428' => 'Precondition Required', + '429' => 'Too Many Requests', + '431' => 'Request Header Fields Too Large', + '451' => 'Unavailable For Legal Reasons', + '500' => 'Internal Server Error', + '501' => 'Not Implemented', + '502' => 'Bad Gateway', + '503' => 'Service Unavailable', + '504' => 'Gateway Timeout', + '505' => 'HTTP Version Not Supported', + '506' => 'Variant Also Negotiates', + '507' => 'Insufficient Storage', + '508' => 'Loop Detected', + '510' => 'Not Extended', + '511' => 'Network Authentication Required', + ]; + $output = implode( + "\r\n", + array_merge( + [ + sprintf('HTTP/1.1 %d %s', $response->statuscode, $code_text_map[strval($response->statuscode)]), + ], + array_map( + function ($key) use (&$response) { + $value = $response->headers[$key]; + return sprintf('%s: %s', $key, $value); + }, + array_keys($response->headers) + ), + [ + '', + $response->body, + ] + ) + ); + return $output; +} + +/** + * sends an HTTP request and returns the response + * requires CURL plugin + * + * @param array $options { + * record< + * timeout ?: float, + * > + * } + */ +function call( + \alveolata\http\struct_request $request, + array $options = [] +): \alveolata\http\struct_response +{ + $options = \array_merge( + [ + 'timeout' => 5.0 + ], + $options + ); + + \alveolata\log\debug( + 'http_call_request', + [ + 'request' => [ + 'protocol' => $request->protocol, + 'method' => $request->method, + 'target' => $request->target, + 'headers' => $request->headers, + 'body' => $request->body, + ], + ] + ); + + $request_headers_transformed = []; + foreach ($request->headers as $key => $value) { + \array_push($request_headers_transformed, \sprintf('%s: %s', $key, $value)); + } + + $curl_object = \curl_init(); + + // set common options + \curl_setopt($curl_object, \CURLOPT_CONNECTTIMEOUT, $options['timeout']); + \curl_setopt($curl_object, \CURLOPT_TIMEOUT, $options['timeout']); + \curl_setopt($curl_object, \CURLOPT_FOLLOWLOCATION, true); + + // set request options + \curl_setopt($curl_object, \CURLOPT_URL, $request->target); + \curl_setopt($curl_object, \CURLOPT_CUSTOMREQUEST, $request->method); + \curl_setopt($curl_object, \CURLINFO_HEADER_OUT, true); + \curl_setopt($curl_object, \CURLOPT_HTTPHEADER, $request_headers_transformed); + \curl_setopt($curl_object, \CURLOPT_SSL_VERIFYPEER, /*$options['sslVerifyPeer']*/1); + \curl_setopt($curl_object, \CURLOPT_SSL_VERIFYHOST, /*$options['sslVerifyHost']*/2); + + if ( + \in_array( + $request->method, + [ + \alveolata\http\enum_method::post, + \alveolata\http\enum_method::put, + \alveolata\http\enum_method::patch, + ] + ) + ) { + \curl_setopt($curl_object, \CURLOPT_POSTFIELDS, $request->body); + } + + // set response options + \curl_setopt($curl_object, \CURLOPT_RETURNTRANSFER, true); + \curl_setopt($curl_object, \CURLOPT_HEADER, 1); + + $response_raw = \curl_exec($curl_object); + if ($response_raw === false) { + $error_number = \curl_errno($curl_object); + $error_message = \curl_error($curl_object); + \curl_close($curl_object); + throw (new \Exception( + \sprintf( + '%s | %s', + 'http_call_failed', + \sprintf( + \json_encode( + [ + 'error_number' => $error_number, + 'error_message' => $error_message, + 'method' => $request->method, + 'target' => $request->target, + ] + ) + ) + ) + )); + } + else { + $header_size = \curl_getinfo($curl_object, \CURLINFO_HEADER_SIZE); + // statuscode + { + $statuscode = \intval(\curl_getinfo($curl_object, \CURLINFO_HTTP_CODE)); + } + // headers + { + $headers_raw = \substr($response_raw, 0, $header_size); + $header_parts = \explode("\r\n", $headers_raw); + // throw away first part, containing the status code information + \array_shift($header_parts); + $headers = []; + foreach ($header_parts as $part) { + if ($part === '') { + // do nothing + } + else if (\preg_match('/^HTTP\/.* [0-9]{3} .*$/', $part) === 1) { + // e.g. "HTTP\/1.1 200 OK" + // do nothing + } + else { + $funds = null; + $matching = (\preg_match('/([^:]*): (.*)/', $part, $funds) === 1); + if (! $matching) { + \alveolata\log\warning( + 'http_call_malformed_header', + [ + 'header' => $part, + ] + ); + } + else { + $key = $funds[1]; + $value = $funds[2]; + $headers[$key] = $value; + } + } + } + } + // body + { + $body = \substr($response_raw, $header_size); + } + \curl_close($curl_object); + $response = new \alveolata\http\struct_response($statuscode, $headers, $body); + + \alveolata\log\debug( + 'http_call_response', + [ + 'response' => [ + 'statuscode' => $response->statuscode, + 'headers' => $response->headers, + 'body' => $response->body, + ], + ] + ); + + return $response; + } +} + + ?> diff --git a/lib/alveolata/http/test.spec.php b/lib/alveolata/http/test.spec.php new file mode 100644 index 0000000..f3796b3 --- /dev/null +++ b/lib/alveolata/http/test.spec.php @@ -0,0 +1,59 @@ + 'alveolata', + 'sections' => [ + [ + 'name' => 'http', + 'sections' => [ + [ + 'name' => 'request_decode', + 'cases' => [ + [ + 'name' => 'test1', + 'procedure' => function ($assert, &$environment) { + $input = 'OPTIONS /server/index.php/session HTTP/1.1 +Host: localhost:1919 +User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:72.0) Gecko/20100101 Firefox/72.0 +Accept: */* +Accept-Language: de,en-US;q=0.7,en;q=0.3 +Accept-Encoding: gzip, deflate +Access-Control-Request-Method: POST +Access-Control-Request-Headers: content-type +Referer: http://localhost:8888/ +Origin: http://localhost:8888 +Connection: keep-alive + +foo +bar +'; + // execution + { + $request = \alveolata\http\request_decode($input); + } + // assertions + { + $assert->equal($request->protocol, 'HTTP/1.1'); + $assert->equal($request->method, 'OPTIONS'); + $assert->equal($request->target, '/server/index.php/session'); + $assert->equal($request->headers['Host'], 'localhost:1919'); + $assert->equal($request->headers['User-Agent'], 'Mozilla/5.0 (X11; Linux x86_64; rv:72.0) Gecko/20100101 Firefox/72.0'); + $assert->equal($request->headers['Accept'], '*/*'); + $assert->equal($request->body, "foo\nbar"); + } + }, + ], + ] + ], + ] + ] + ] + ] +); + + ?> diff --git a/lib/alveolata/http/types.php b/lib/alveolata/http/types.php new file mode 100644 index 0000000..c8a7080 --- /dev/null +++ b/lib/alveolata/http/types.php @@ -0,0 +1,115 @@ +} + */ + public $headers; + + + /** + * @var string + */ + public $body; + + + /** + * constructor + */ + public function __construct( + string $protocol, + string $method, + string $target, + array $headers, + string $body + ) + { + $this->protocol = $protocol; + $this->method = $method; + $this->target = $target; + $this->headers = $headers; + $this->body = $body; + } + +} + + +/** + */ +class struct_response +{ + + /** + * @var int + */ + public $statuscode; + + + /** + * @var array {map} + */ + public $headers; + + + /** + * @var string + */ + public $body; + + + /** + * constructor + */ + public function __construct( + int $statuscode, + array $headers, + string $body + ) + { + $this->statuscode = $statuscode; + $this->headers = $headers; + $this->body = $body; + } + +} + + ?> diff --git a/lib/alveolata/json/functions.php b/lib/alveolata/json/functions.php new file mode 100644 index 0000000..a04284f --- /dev/null +++ b/lib/alveolata/json/functions.php @@ -0,0 +1,46 @@ + + */ +function encode( + $value, + bool $formatted = false +) : string +{ + $string = json_encode($value, $formatted ? JSON_PRETTY_PRINT : 0); + if (json_last_error() !== JSON_ERROR_NONE) { + throw (new \Exception('json not encodable: ' . json_last_error_msg())); + } + else { + return $string; + } +} + + +/** + * @param string $string + * @return mixed + * @throws \Exception if not decodable + * @author Christian Fraß + */ +function decode( + string $string +) +{ + $value = json_decode($string, true); + if (json_last_error() !== JSON_ERROR_NONE) { + throw (new \Exception('json not decodable: ' . json_last_error_msg())); + } + else { + return $value; + } +} + diff --git a/lib/alveolata/list/functions.php b/lib/alveolata/list/functions.php new file mode 100644 index 0000000..bf32ee6 --- /dev/null +++ b/lib/alveolata/list/functions.php @@ -0,0 +1,317 @@ +} + * @author Christian Fraß + */ +function sequence( + int $length +) : array +{ + return ( + ($length <= 0) + ? [] + : array_merge(sequence($length-1), [$length-1]) + ); +} + + +/** + * @param array $list1 {list<§x>} + * @param array $list2 {list<§x>} + * @return array {list<§x>} + * @author Christian Fraß + */ +function concat( + array $list1, + array $list2 +) : array +{ + return array_merge($list1, $list2); +} + + +/** + * @param array $list {list<§x>} + * @param \Closure $predicate {function<§x,boolean>} + * @return array {list<§x>} + * @author Christian Fraß + */ +function filter( + array $list, + \Closure $predicate +) : array +{ + $list_ = []; + foreach ($list as $value) { + if ($predicate($value)) { + array_push($list_, $value); + } + } + return $list_; +} + + +/** + * @param array $list {list<§x>} + * @param \Closure $transformator {function<§x,§y>} + * @return array {list<§y>} + * @author Christian Fraß + */ +function map( + array $list, + \Closure $transformator +) : array +{ + $list_ = []; + foreach ($list as $value) { + $value_ = $transformator($value); + array_push($list_, $value_); + } + return $list_; +} + + +/** + * @param array $list {list<§x>} + * @param mixed $start {§y} + * @param \Closure $aggregator {function<§y,§x,§y>} + * @return mixed {§y} + * @author Christian Fraß + */ +function reduce_left( + array $list, + $start, + \Closure $aggregator +) +{ + $result = $start; + foreach ($list as $current) { + $result = $aggregator($result, $current); + } + return $result; +} + + +/** + * @param array $list {list<§x>} + * @param mixed $start {§y} + * @param \Closure $aggregator {function<§x,§y,§y>} + * @return mixed {§y} + * @author Christian Fraß + */ +function reduce_right( + array $list, + $start, + \Closure $aggregator +) +{ + $result = $start; + foreach (array_reverse($list) as $current) { + $result = $aggregator($current, $result); + } + return $result; +} + + +/** + * @param array $list {list<§x>} + * @param mixed $start {§y} + * @param \Closure $aggregator {function<§y,§x,§y>} + * @return mixed {§y} + * @author Christian Fraß + */ +function reduce( + array $list, + $start, + \Closure $aggregator +) +{ + return reduce_left($list, $start, $aggregator); +} + + +/** + * @param array $list {list<§x>} + * @param \Closure $predicate {function<§x,bool>} + * @return bool {boolean} + * @author Christian Fraß + */ +function some( + array $list, + \Closure $predicate +) : bool +{ + foreach ($list as $current) { + if ($predicate($current)) { + return true; + } + } + return false; +} + + +/** + * @param array $list {list<§x>} + * @param \Closure $predicate {function<§x,bool>} + * @return bool {boolean} + * @author Christian Fraß + */ +function every( + array $list, + \Closure $predicate +) : bool +{ + foreach ($list as $current) { + if (! $predicate($current)) { + return false; + } + } + return true; +} + + +/** + * @param array $list1 {list<§x>} + * @param array $list2 {list<§y>} + * @param bool $cut {boolean} whether to take the least length in case the lists have different lengths + * @return array {list>} + * @throw \Exception if lists have different lengths and $cut = false + * @author Christian Fraß + */ +function zip( + array $list1, + array $list2, + bool $cut = true +) : array +{ + $l1 = count($list1); + $l2 = count($list2); + if (! ($l1 === $l2)) { + if (! $cut) { + throw (new \Exception('lists have different lengths')); + } + else { + $length = $l1; + } + } + else { + $length = min($l1, $l2); + } + $list3 = []; + for ($index = 0; $index < $length; $index += 1) { + $pair = [ + 'first' => $list1[$index], + 'second' => $list2[$index], + ]; + array_push($list3, $pair); + } + return $list3; +} + + +/** + * @param array $list {list<§x>} + * @param \Closure $order {function<§x,§x,boolean>} + * @return array {list<§x>} + * @throws \Exception if the sorting fails + * @author Christian Fraß + */ +function sort( + array $list, + \Closure $order = null +) : array +{ + if (is_null($order)) { + $order = (function ($x, $y) {return ($x <= $y);}); + } + $copy = array_map(function ($x) {return $x;}, $list); + $successful = usort( + $copy, + function ($x, $y) use ($order) : int { + return ($order($x, $y) ? (-1) : (+1)); + } + ); + if (! $successful) { + throw (new \Exception('alveolata_list_sort_failed')); + } + else { + return $copy; + } +} + + +/** + * @param array $list {list<§x>} + * @param \Closure $collation {function<§x,§x,boolean>} + * @return array {list>} + */ +function group( + array $list, + \Closure $collation +) : array +{ + $groups = []; + foreach ($list as $element) { + $found = false; + foreach ($groups as &$group) { + if ($collation($group[0], $element)) { + array_push($group, $element); + $found = true; + break; + } + } + unset($group); + if (! $found) { + $group = [$element]; + array_push($groups, $group); + } + } + return $groups; +} + + +/** + * @param array $x {list} + * @param array $y {list} + * @param ?array $options { + * ( + * null + * | + * record< + * ?combinator:function,any>, + * > + * ) + * } + * @return array {list>} + */ +function product( + array $x, + array $y, + ?array $options = null +) : array +{ + $options = \array_merge( + [ + 'combinator' => (fn($u, $v) => [$u, $v]) + ], + ($options ?? []) + ); + $z = []; + foreach ($x as $u) { + foreach ($y as $v) { + \array_push($z, $options['combinator']($u, $v)); + } + } + return $z; +} + + ?> diff --git a/lib/alveolata/list/test.spec.json b/lib/alveolata/list/test.spec.json new file mode 100644 index 0000000..bdc573f --- /dev/null +++ b/lib/alveolata/list/test.spec.json @@ -0,0 +1,247 @@ +{ + "active": true, + "sections": [ + { + "name": "sequence", + "active": true, + "execution": { + "call": "\\alveolata\\list_\\sequence({{length}})" + }, + "cases": [ + { + "name": "empty", + "active": true, + "input": { + "length": 0 + }, + "output": { + "kind": "regular", + "value": [] + } + }, + { + "name": "non-empty", + "active": true, + "input": { + "length": 5 + }, + "output": { + "kind": "regular", + "value": [0,1,2,3,4] + } + } + ] + }, + { + "name": "map", + "active": true, + "execution": { + "call": "\\alveolata\\list_\\map({{list}}, function ($x) {return ($x*2);})" + }, + "cases": [ + { + "name": "test", + "active": true, + "input": { + "list": [0,1,2] + }, + "output": { + "kind": "regular", + "value": [0,2,4] + } + } + ] + }, + { + "name": "reduce", + "active": true, + "execution": { + "call": "\\alveolata\\list_\\reduce({{list}}, {{start}}, function ($x, $y) {return ($x-$y);})" + }, + "cases": [ + { + "name": "empty list", + "active": true, + "input": { + "start": 7, + "list": [] + }, + "output": { + "kind": "regular", + "value": 7 + } + }, + { + "name": "non empty list", + "active": true, + "input": { + "start": 7, + "list": [0,1,2] + }, + "output": { + "kind": "regular", + "value": 4 + } + } + ] + }, + { + "name": "some", + "active": true, + "execution": { + "call": "\\alveolata\\list_\\some({{list}}, function ($x) {return (($x % 3) === 0);})" + }, + "cases": [ + { + "name": "empty list", + "active": true, + "input": { + "list": [] + }, + "output": { + "kind": "regular", + "value": false + } + }, + { + "name": "non empty list; negative", + "active": true, + "input": { + "list": [2,4,5,7] + }, + "output": { + "kind": "regular", + "value": false + } + }, + { + "name": "non empty list; positive", + "active": true, + "input": { + "list": [2,3,5,7] + }, + "output": { + "kind": "regular", + "value": true + } + } + ] + }, + { + "name": "every", + "active": true, + "execution": { + "call": "\\alveolata\\list_\\every({{list}}, function ($x) {return (($x % 3) === 0);})" + }, + "cases": [ + { + "name": "empty list", + "active": true, + "input": { + "list": [] + }, + "output": { + "kind": "regular", + "value": true + } + }, + { + "name": "non empty list; negative", + "active": true, + "input": { + "list": [0,3,7,9] + }, + "output": { + "kind": "regular", + "value": false + } + }, + { + "name": "non empty list; positive", + "active": true, + "input": { + "list": [0,3,6,9] + }, + "output": { + "kind": "regular", + "value": true + } + } + ] + }, + { + "name": "sort", + "active": true, + "execution": { + "call": "\\alveolata\\list_\\sort({{list}})" + }, + "cases": [ + { + "name": "empty", + "active": true, + "input": { + "list": [] + }, + "output": { + "kind": "regular", + "value": [] + } + }, + { + "name": "non empty, positive", + "active": true, + "input": { + "list": [5,2,3] + }, + "output": { + "kind": "regular", + "value": [2,3,5] + } + }, + { + "name": "non empty, negative", + "active": true, + "input": { + "list": [-13,-7,-11] + }, + "output": { + "kind": "regular", + "value": [-13,-11,-7] + } + } + ] + }, + { + "name": "group", + "active": true, + "execution": { + "call": "\\alveolata\\list_\\group({{list}}, function ($x, $y) {return (($x % 3) === ($y % 3));})" + }, + "cases": [ + { + "name": "empty", + "active": true, + "input": { + "list": [] + }, + "output": { + "kind": "regular", + "value": [] + } + }, + { + "name": "non empty", + "active": true, + "input": { + "list": [0,1,2,3,4,5] + }, + "output": { + "kind": "regular", + "value": [[0,3],[1,4],[2,5]] + } + } + ] + } + ] +} + diff --git a/lib/alveolata/localization/functions.php b/lib/alveolata/localization/functions.php new file mode 100644 index 0000000..1dca863 --- /dev/null +++ b/lib/alveolata/localization/functions.php @@ -0,0 +1,147 @@ + + */ +class _state { + + /** + * @var list + * @author Christian Fraß + */ + public static $order = []; + + + /** + * @var map + * @author Christian Fraß + */ + public static $data = []; + + + /** + * @var boolean + * @author Christian Fraß + */ + public static $suppress_messages = false; + +} + + +/** + * @author Christian Fraß + */ +function _add( + string $language, + string $key, + string $value +) : void +{ + _state::$data; + if (! array_key_exists($language, _state::$data)) { + _state::$data[$language] = []; + } + _state::$data[$language][$key] = $value; +} + + +/** + * @author Christian Fraß + */ +function feed( + string $language, + array $data +) : void +{ + array_unshift(_state::$order, $language); + foreach ($data as $key => $value) { + _add($language, $key, $value); + } +} + + +/** + * @param string $language + * @param string $key + * @return string + * @author Christian Fraß + */ +function _fetch( + string $language, + string $key +) : string +{ + if (! array_key_exists($language, _state::$data)) { + if (! _state::$suppress_messages) { + \alveolata\log\notice( + 'no localization data for language', + [ + 'language' => $language, + ] + ); + } + return UNSET_STRING; + } + else { + if (! array_key_exists($key, _state::$data[$language])) { + if (! _state::$suppress_messages) { + \alveolata\log\notice( + 'no translation for key', + [ + 'language' => $language, + 'key' => $key, + ] + ); + } + return UNSET_STRING; + } + else { + return _state::$data[$language][$key]; + } + } +} + + +/** + * @author Christian Fraß + */ +function get( + string $key, + string $fallback = UNSET_STRING +) : string +{ + if ($fallback === UNSET_STRING) { + $fallback = sprintf('{%s}', $key); + } + $found = false; + foreach (_state::$order as $language) { + $value = _fetch($language, $key); + if (! ($value === UNSET_STRING)) { + $found = true; + break; + } + } + if (! $found) { + if (! _state::$suppress_messages) { + \alveolata\log\warning( + 'using fallback translation', + [ + 'key' => $key, + 'languages_tried' => _state::$order, + ] + ); + } + return $fallback; + } + else { + return $value; + } +} + + ?> diff --git a/lib/alveolata/localization/test.spec.php b/lib/alveolata/localization/test.spec.php new file mode 100644 index 0000000..3bbaf0b --- /dev/null +++ b/lib/alveolata/localization/test.spec.php @@ -0,0 +1,61 @@ + 'alveolata', + 'sections' => [ + [ + 'name' => 'localization', + 'sections' => [ + [ + 'name' => 'get', + 'setup' => function (&$environment) { + \alveolata\localization\_state::$suppress_messages = true; + \alveolata\localization\feed( + 'eng', + [ + 'foo' => 'roses are red', + 'bar' => 'grass is green', + ] + ); + \alveolata\localization\feed( + 'deu', + [ + 'foo' => 'Rosen sind rot', + ] + ); + }, + 'cases' => [ + [ + 'name' => 'found at first', + 'procedure' => function ($assert, &$environment) { + $value = \alveolata\localization\get('foo'); + $assert->equal($value, 'Rosen sind rot'); + }, + ], + [ + 'name' => 'found at second', + 'procedure' => function ($assert, &$environment) { + $value = \alveolata\localization\get('bar'); + $assert->equal($value, 'grass is green'); + }, + ], + [ + 'name' => 'not found', + 'procedure' => function ($assert, &$environment) { + $value = \alveolata\localization\get('baz'); + $assert->equal($value, '{baz}'); + }, + ], + ] + ] + ] + ] + ] + ] +); + + ?> diff --git a/lib/alveolata/log/base.php b/lib/alveolata/log/base.php new file mode 100644 index 0000000..793ab0e --- /dev/null +++ b/lib/alveolata/log/base.php @@ -0,0 +1,75 @@ + + */ +function level_decode( + string $level +) : int +{ + $map = [ + 'error' => 0, + 'warning' => 1, + 'notice' => 2, + 'info' => 3, + 'debug' => 4, + ]; + if (! \array_key_exists($level, $map)) { + throw (new \Exception(\sprintf('unhandled log level "%s"', $level))); + } + else { + // return \alveolata\string\pad_right(\alveolata\string\case_upper($map[$level]), 7, ' '); + return $map[$level]; + } +} + + +/** + * @param int $level + * @return string + * @author Christian Fraß + */ +function level_encode( + int $level +) : string +{ + $map = [ + 0 => 'error', + 1 => 'warning', + 2 => 'notice', + 3 => 'info', + 4 => 'debug', + ]; + if (! \array_key_exists($level, $map)) { + throw (new \Exception(sprintf('unhandled log output level %u', $level))); + } + else { + // return \alveolata\string\pad_right(\alveolata\string\case_upper($map[$level]), 7, ' '); + return $map[$level]; + } +} + + +/** + * @deprecated + */ +function output_translate_level($level) { + return level_encode($level); +} + + ?> diff --git a/lib/alveolata/log/functions.php b/lib/alveolata/log/functions.php new file mode 100644 index 0000000..20dde4b --- /dev/null +++ b/lib/alveolata/log/functions.php @@ -0,0 +1,245 @@ + + */ +class _state +{ + public static $outputs = []; +} + + +/** + * @author Christian Fraß + */ +function add_output( + interface_output $output +) : void +{ + array_push(_state::$outputs, $output); +} + + +/** + * @author Christian Fraß + */ +function _submit( + int $level, + \alveolata\report\struct_report $report +) : void +{ + foreach (_state::$outputs as $output) { + $output->process($level, $report); + } +} + + +/** + * @param \alveolata\report\type $report + * @author Christian Fraß + */ +function error_( + \alveolata\report\struct_report $report +) : void +{ + _submit(enum_level::error, $report); +} + + +/** + * @param string $incident + * @param map [$details] + * @author Christian Fraß + */ +function error( + string $incident, + array $details = [] +) : void +{ + error_(\alveolata\report\make($incident, $details)); +} + + +/** + * @param \alveolata\report\type $report + * @author Christian Fraß + */ +function warning_( + \alveolata\report\struct_report $report +) : void +{ + _submit(enum_level::warning, $report); +} + + +/** + * @param string $incident + * @param map [$details] + * @author Christian Fraß + */ +function warning( + string $incident, + array $details = [] +) : void +{ + warning_(\alveolata\report\make($incident, $details)); +} + + +/** + * @param \alveolata\report\type $report + * @author Christian Fraß + */ +function notice_( + \alveolata\report\struct_report $report +) : void +{ + _submit(enum_level::notice, $report); +} + + +/** + * @param string $incident + * @param map [$details] + * @author Christian Fraß + */ +function notice( + string $incident, + array $details = [] +) : void +{ + notice_(\alveolata\report\make($incident, $details)); +} + + +/** + * @param \alveolata\report\type $report + * @author Christian Fraß + */ +function info_( + \alveolata\report\struct_report $report +) : void +{ + _submit(enum_level::info, $report); +} + + +/** + * @param string $incident + * @param map [$details] + * @author Christian Fraß + */ +function info( + string $incident, + array $details = [] +) : void +{ + info_(\alveolata\report\make($incident, $details)); +} + + +/** + * @param \alveolata\report\type $report + * @author Christian Fraß + */ +function debug_( + \alveolata\report\struct_report $report +) : void +{ + _submit(enum_level::debug, $report); +} + + +/** + * @param string $incident + * @param map [$details] + * @author Christian Fraß + */ +function debug( + string $incident, + array $details = [] +) : void +{ + debug_(\alveolata\report\make($incident, $details)); +} + + +/** + * @author Christian Fraß + */ +function make_output( + string $kind, + array $parameters +) : interface_output +{ + switch ($kind) { + case 'console': { + return ( + new implementation_restricted( + new implementation_console(), + level_decode($parameters['level_threshold'] ?? 'notice') + ) + ); + break; + } + case 'file': { + return ( + new implementation_restricted( + new implementation_file( + $parameters['path'], + [ + 'human_readable' => ($parameters['human_readable'] ?? false), + ] + ), + level_decode($parameters['level_threshold'] ?? 'notice') + ) + ); + break; + } + case 'email': { + return ( + new implementation_restricted( + new implementation_email( + $parameters['auth'], + $parameters['receivers'], + $parameters['sender'], + $parameters['tags'], + $parameters['implementation'] + ), + level_decode($parameters['level_threshold'] ?? 'notice') + ) + ); + break; + } + case 'libnotify': { + return ( + new implementation_restricted( + new implementation_libnotify(), + level_decode($parameters['level_threshold'] ?? 'notice') + ) + ); + break; + } + default: { + throw (new \Exception(sprintf('invalid logoutput kind "%s"', $kind))); + break; + } + } +} + + ?> diff --git a/lib/alveolata/log/output-implementation-console.php b/lib/alveolata/log/output-implementation-console.php new file mode 100644 index 0000000..82ecb86 --- /dev/null +++ b/lib/alveolata/log/output-implementation-console.php @@ -0,0 +1,55 @@ + + */ +class implementation_console implements interface_output +{ + + /** + * @param int $level_threshold + * @author Christian Fraß + */ + public function __construct( + ) + { + } + + + /** + * @implementation + * @author Christian Fraß + */ + public function process( + int $level, + \alveolata\report\struct_report $report + ) : void + { + $message = \alveolata\string\coin( + ( + empty($report->details) + ? '<{{datetime}}> [{{level}}] {{incident}}' + : '<{{datetime}}> [{{level}}] {{incident}} | {{details}}' + ), + [ + 'datetime' => date('Y-m-d|H:i:s', $report->timestamp), + 'level' => output_translate_level($level), + 'incident' => $report->incident, + 'details' => \alveolata\json\encode($report->details), + ] + ); + error_log($message); + } + +} + + ?> diff --git a/lib/alveolata/log/output-implementation-email.php b/lib/alveolata/log/output-implementation-email.php new file mode 100644 index 0000000..d90449d --- /dev/null +++ b/lib/alveolata/log/output-implementation-email.php @@ -0,0 +1,139 @@ + + */ +class implementation_email implements interface_output +{ + + /** + * @var $auth { + * record< + * host:string, + * port:integer, + * authtype:string, + * username:string, + * password:string + * > + * } + */ + private $auth; + + + /** + * @var array {list} + */ + private $receivers; + + + /** + * @var string + */ + private $sender; + + + /** + * @var array {list} + */ + private $tags; + + + /** + * @var string + */ + private $implementation; + + + /** + * @author Christian Fraß + */ + public function __construct( + array $auth, + array $receivers, + string $sender, + array $tags, + string $implementation + ) + { + $this->auth = $auth; + $this->receivers = $receivers; + $this->sender = $sender; + $this->tags = $tags; + $this->implementation = $implementation; + } + + + /** + * @implementation + * @author Christian Fraß + */ + public function process( + int $level, + \alveolata\report\struct_report $report + ) : void + { + $subject = \alveolata\string\coin( + '{{tags}} {{level}}: {{incident}}', + [ + 'tags' => \alveolata\string\join( + \alveolata\list_\map( + $this->tags, + function (string $tag): string { + return \alveolata\string\coin('[{{tag}}]', ['tag' => $tag]); + } + ), + ' ' + ), + 'level' => output_translate_level($level), + 'incident' => $report->incident, + ] + ); + $body_plain = \alveolata\string\coin( + "<{{timestamp}}>\n{{details}}", + [ + 'timestamp' => date('Y-m-d|H:i:s', $report->timestamp), + 'details' => implode( + "\n", + \alveolata\list_\map( + array_keys($report->details), + function ($key) use ($report) { + $value = $report->details[$key]; + return \alveolata\string\coin( + '{{key}}: {{value}}', + [ + 'key' => $key, + 'value' => json_encode($value), + ] + ); + } + ) + ), + ] + ); + \alveolata\email\send( + $this->auth, + [ + 'to' => $this->receivers, + 'from' => $this->sender, + 'subject' => $subject, + 'body_plain' => $body_plain, + ], + [ + 'implementation' => $this->implementation + ] + ); + } + +} + + ?> diff --git a/lib/alveolata/log/output-implementation-file.php b/lib/alveolata/log/output-implementation-file.php new file mode 100644 index 0000000..c399bd3 --- /dev/null +++ b/lib/alveolata/log/output-implementation-file.php @@ -0,0 +1,99 @@ + + */ +class implementation_file implements interface_output +{ + + /** + * @var string + * @author Christian Fraß + */ + protected $path; + + + /** + * @var boolean + */ + protected $human_readable; + + + /** + * @param int $level_threshold + * @author Christian Fraß + */ + public function __construct( + string $path, + array $options = [] + ) + { + $options = \array_merge( + [ + 'human_readable' => false, + ], + $options + ); + $this->path = $path; + $this->human_readable = $options['human_readable']; + } + + + /** + * @implementation + * @author Christian Fraß + */ + public function process( + int $level, + \alveolata\report\struct_report $report + ) : void + { + $content = ( + $this->human_readable + ? \alveolata\string\coin( + ( + empty($report->details) + ? "<{{datetime}}> [{{level}}] {{incident}}\n" + : "<{{datetime}}> [{{level}}] {{incident}} | {{details}}\n" + ), + [ + 'datetime' => date('Y-m-d|H:i:s', $report->timestamp), + 'level' => output_translate_level($level), + 'incident' => $report->incident, + 'details' => \alveolata\json\encode($report->details), + ] + ) + : ( + \alveolata\json\encode( + [ + 'timestamp' => $report->timestamp, + 'datetime' => date('c', $report->timestamp), + 'level_value' => $level, + 'level_name' => output_translate_level($level), + 'incident' => $report->incident, + 'details' => $report->details, + ] + ) + . + "\n" + ) + ); + file_put_contents( + $this->path, + $content, + FILE_APPEND + ); + } + +} + + ?> diff --git a/lib/alveolata/log/output-implementation-libnotify.php b/lib/alveolata/log/output-implementation-libnotify.php new file mode 100644 index 0000000..c8839d8 --- /dev/null +++ b/lib/alveolata/log/output-implementation-libnotify.php @@ -0,0 +1,63 @@ + + */ +class implementation_libnotify implements interface_output +{ + + /** + * @param int $level_threshold + * @author Christian Fraß + */ + public function __construct( + ) + { + } + + + /** + * @implementation + * @author Christian Fraß + */ + public function process( + int $level, + \alveolata\report\struct_report $report + ) : void + { + $command = \alveolata\string\coin( + 'notify-send \'[{{level}}] {{incident}}\' \'{{details}}\'', + [ + 'level' => output_translate_level($level), + 'incident' => $report->incident, + 'details' => implode( + "\n", + array_map( + function ($key) use (&$report) { + $value = $report->details[$key]; + return \alveolata\string\coin( + '{{key}}: {{value}}', + [ + 'key' => $key, + 'value' => json_encode($value), + ] + ); + }, + array_keys($report->details) + ) + ), + ] + ); + exec($command); + } + +} + diff --git a/lib/alveolata/log/output-implementation-restricted.php b/lib/alveolata/log/output-implementation-restricted.php new file mode 100644 index 0000000..ecc199f --- /dev/null +++ b/lib/alveolata/log/output-implementation-restricted.php @@ -0,0 +1,65 @@ + + */ +class implementation_restricted implements interface_output +{ + + /** + * @var interface_output + * @author Christian Fraß + */ + protected $core; + + + /** + * @var int + * @author Christian Fraß + */ + protected $level_threshold; + + + /** + * @param int $level_threshold + * @author Christian Fraß + */ + public function __construct( + interface_output $core, + int $level_threshold + ) + { + $this->core = $core; + $this->level_threshold = $level_threshold; + } + + + /** + * @implementation + * @author Christian Fraß + */ + public function process( + int $level, + \alveolata\report\struct_report $report + ) : void + { + if ($level <= $this->level_threshold) { + $this->core->process($level, $report); + } + else { + // do nothing + } + } + +} + + ?> diff --git a/lib/alveolata/log/output-interface.php b/lib/alveolata/log/output-interface.php new file mode 100644 index 0000000..cad89e7 --- /dev/null +++ b/lib/alveolata/log/output-interface.php @@ -0,0 +1,25 @@ + + */ +interface interface_output +{ + + /** + * @author Christian Fraß + */ + function process( + int $level, + \alveolata\report\struct_report $report + ) : void + ; + +} diff --git a/lib/alveolata/main.php b/lib/alveolata/main.php new file mode 100644 index 0000000..0a0fa21 --- /dev/null +++ b/lib/alveolata/main.php @@ -0,0 +1,11 @@ + 4])); +// \alveolata\log\add_output(\alveolata\log\make_output('libnotify', ['level_threshold' => 2])); +\alveolata\test\run(isset($argv[1]) ? explode('.', $argv[1]) : []); + diff --git a/lib/alveolata/map/functions.php b/lib/alveolata/map/functions.php new file mode 100644 index 0000000..0b5e5d3 --- /dev/null +++ b/lib/alveolata/map/functions.php @@ -0,0 +1,61 @@ +} + * @param \Closure $transformator {function<§x,string,§y>} + * @return array + * @author Christian Fraß + */ +function map/*<§x,§y>*/( + array $map, + \Closure $transformator +) : array +{ + $map_ = []; + foreach ($map as $key => $value) { + $value_ = $transformator($value, $key); + $map_[$key] = $value_; + } + return $map_; +} + + +/** + * @param array $pairs {list>} + * @return array {map} + * @author Christian Fraß + */ +function from_pairs/*<§x>*/( + array $pairs +) : array +{ + $map = []; + foreach ($pairs as $pair) { + $map[$pair['key']] = $pair['value']; + } + return $map; +} + + +/** + * @param array $map {map} + * @return array {list>} + * @author Christian Fraß + */ +function to_pairs/*<§x>*/( + array $map +) : array +{ + $pairs = []; + foreach ($map as $key => $value) { + \array_push($pairs, ['key' => $key, 'value' => $value]); + } + return $pairs; +} + + ?> diff --git a/lib/alveolata/markdown/functions.php b/lib/alveolata/markdown/functions.php new file mode 100644 index 0000000..9cce381 --- /dev/null +++ b/lib/alveolata/markdown/functions.php @@ -0,0 +1,156 @@ + "/\n([ ]*)-/", 'replacement' => "\n$1\\-"], + ['pattern' => "/_/", 'replacement' => "\\_"], + ['pattern' => "/\|/", 'replacement' => "\\\|"], + ['pattern' => "/\*/", 'replacement' => "\\\*"], + ['pattern' => "/\[/", 'replacement' => "\\\["], + ['pattern' => "/\]/", 'replacement' => "\\\]"], + ]; + $result = $string; + foreach ($replacements as $replacement) { + $result = \preg_replace($replacement['pattern'], $replacement['replacement'], $result); + } + return $result; +} + + +/** + */ +function bold( + string $piece +) : string +{ + return sprintf('__%s__', $piece); +} + + +/** + */ +function link( + string $label, + string $url +) : string +{ + return sprintf('[%s](%s)', $label, $url); +} + + +/** + */ +function image( + string $label, + string $url +) : string +{ + return sprintf('![%s](%s)', $label, $url); +} + + +/** + */ +function headline( + int $level, + string $string +) : string +{ + if ($level < 1) { + throw (new \Exception(sprintf('invalid level: %d', $level))); + } + return sprintf("%s %s\n\n", str_repeat('#', $level), $string); +} + + +/** + */ +function sectionend( +) : string +{ + return sprintf("\n\n"); +} + + +/** + */ +function paragraph( + string $string +) : string +{ + return sprintf("%s\n\n", $string); +} + + +/** + */ +function code( + string $string +) : string +{ + return sprintf("```\n%s\n```", $string); +} + + +/** + */ +function item( + int $level, + string $string +) : string +{ + if ($level < 1) { + throw (new \Exception(sprintf('invalid level: %d', $level))); + } + return sprintf("%s- %s\n", str_repeat(' ', $level-1), $string); +} + + +/** + */ +function rule( +) : string +{ + return sprintf("---\n\n"); +} + + +/** + * @param null|array $titles {null|list} + * @param array $data {list>} + */ +function table( + array $titles, + array $data +) : string +{ + $md = ''; + if (! is_null($titles)) { + $md .= sprintf( + "| %s |\n", + implode(' | ', $titles) + ); + $md .= sprintf( + "| %s |\n", + implode(' | ', array_map(function (string $title) {return ':--';}, $titles)) + ); + } + foreach ($data as $line) { + $md .= sprintf( + "| %s |\n", + implode(' | ', $line) + ); + } + return $md; +} + + ?> diff --git a/lib/alveolata/markdown/test.spec.php b/lib/alveolata/markdown/test.spec.php new file mode 100644 index 0000000..72b0fc8 --- /dev/null +++ b/lib/alveolata/markdown/test.spec.php @@ -0,0 +1,34 @@ + 'alveolata', + 'sections' => [ + [ + 'name' => 'markdown', + 'sections' => [ + [ + 'name' => 'escape', + 'cases' => \array_map( + fn($case) => [ + 'name' => $case['name'], + 'procedure' => function ($assert) use ($case) { + $result_actual = \alveolata\markdown\escape($case['input']); + $result_expected = $case['output']; + $assert->equal($result_actual, $result_expected); + }, + ], + $data['escape']['cases'] + ), + ], + ] + ] + ] + ] +); + + ?> diff --git a/lib/alveolata/markdown/testdata.json b/lib/alveolata/markdown/testdata.json new file mode 100644 index 0000000..4a14cb5 --- /dev/null +++ b/lib/alveolata/markdown/testdata.json @@ -0,0 +1,37 @@ +{ + "escape": { + "cases": [ + { + "name": "list item 1", + "input": "foo-bar", + "output": "foo-bar" + }, + { + "name": "list item 2", + "input": "foo\n- bar", + "output": "foo\n\\- bar" + }, + { + "name": "list item 3", + "input": "foo\n- bar\n - baz", + "output": "foo\n\\- bar\n \\- baz" + }, + { + "name": "underscore 1", + "input": "foo_bar", + "output": "foo\\_bar" + }, + { + "name": "pipe 1", + "input": "foo|bar", + "output": "foo\\|bar" + }, + { + "name": "oblique", + "input": "jean-claude: twink]e, twink]e, little *, how | w{}nder, wha+ you are!", + "output": "jean-claude: twink\\]e, twink\\]e, little \\*, how \\| w{}nder, wha+ you are!" + } + ] + } +} + diff --git a/lib/alveolata/math/functions.php b/lib/alveolata/math/functions.php new file mode 100644 index 0000000..7ae8e79 --- /dev/null +++ b/lib/alveolata/math/functions.php @@ -0,0 +1,233 @@ + + */ +function mod( + int $number, + int $modulus +) : int +{ + // return (x - (div(x, y) * y)); + if ($modulus <= 0) { + throw (new \Exception('invalid divisor')); + } + else { + return ( + ($number >= 0) + ? ($number%$modulus) + : (($modulus-((-$number)%$modulus))%$modulus) + ); + } +} + + +/** + * computes the power of a number in a residue class ring via square and multiply + * + * @author Christian Fraß + */ +function modpow( + int $base, + int $exponent, + int $modulus +) : int +{ + if ($exponent === 0) { + return 1; + } + else { + $a = modpow($base, $exponent >> 1, $modulus); + $a = mod(($a * $a), $modulus); + if (($exponent & 1) != 0) { + $a = mod(($a * $base), $modulus); + } + return $a; + } +} + + +/** + * @template TypeElement + * @param \Closure {function,boolean>} + * @param array $set1 {list} + * @param array $set2 {list} + * @return array {list} + * @author Christian Fraß + */ +function set_sub/**/( + \Closure $collation, + array $set1, + array $set2 +) : bool +{ + foreach ($set1 as $element1) { + $present = false; + foreach ($set2 as $element2) { + if ($collation($element1, $element2)) { + $present = true; + break; + } + } + if (! $present) { + return false; + break; + } + } + return true; +} + + +/** + * @param \Closure function,boolean> + * @param array $set1 {list} + * @param array $set2 {list} + * @return array {list} + * @author Christian Fraß + */ +function set_union/**/( + \Closure $collation, + array $set1, + array $set2 +) : array +{ + $set3 = []; + foreach ([$set1, $set2] as $source) { + foreach ($source as $element) { + $add = true; + foreach ($set3 as $element_) { + if ($collation($element, $element_)) { + $add = false; + break; + } + } + if ($add) { + array_push($set3, $element); + } + } + } + return $set3; +} + + +/** + * @param \Closure function,boolean> + * @param array $set1 {list} + * @param array $set2 {list} + * @return array {list} + * @author Christian Fraß + */ +function set_intersection/**/( + \Closure $collation, + array $set1, + array $set2 +) : array +{ + $set3 = []; + foreach ($set1 as $element1) { + $add = false; + foreach ($set2 as $element2) { + if ($collation($element1, $element2)) { + $add = true; + break; + } + } + if ($add) { + array_push($set3, $element1); + } + } + return $set3; +} + + +/** + * @param \Closure function,boolean> + * @param array $set1 {list} + * @param array $set2 {list} + * @return array {list} + * @author Christian Fraß + */ +function set_difference/**/( + \Closure $collation, + array $set1, + array $set2 +) : array +{ + $set3 = []; + foreach ($set1 as $element1) { + $add = true; + foreach ($set2 as $element2) { + if ($collation($element1, $element2)) { + $add = false; + break; + } + } + if ($add) { + array_push($set3, $element1); + } + } + return $set3; +} + + +/** + * applies the lexicographic order + * + * @template type_element + * @param order Closure {function} + * @param array $list1 {list} + * @param array $list2 {list} + * @return boolean + * @author Christian Fraß + */ +function order_lexicographic( + \Closure $order, + array $list1, + array $list2 +) : bool +{ + if (empty($list1)) { + if (empty($list2)) { + return true; + } + else { + return true; + } + } + else { + if (empty($list2)) { + return false; + } + else { + $le = $order($list1[0], $list2[0]); + $ge = $order($list2[0], $list1[0]); + if (! $le) { + if (! $ge) { + // impossible: badly defined order + } + else { + return false; + } + } + else { + if (! $ge) { + return true; + } + else { + return order_lexicographic( + $order, + array_slice($list1, 1), + array_slice($list2, 1) + ); + } + } + } + } +} + + ?> diff --git a/lib/alveolata/math/test.spec.php b/lib/alveolata/math/test.spec.php new file mode 100644 index 0000000..0241a44 --- /dev/null +++ b/lib/alveolata/math/test.spec.php @@ -0,0 +1,139 @@ + 'alveolata', + 'sections' => [ + [ + 'name' => 'math', + 'sections' => [ + [ + 'name' => 'mod', + 'cases' => array_map( + function (array $case) : array { + return [ + 'name' => $case['name'], + 'procedure' => function ($assert) use ($case) { + $result_actual = \alveolata\math\mod( + $case['input']['divident'], + $case['input']['divisor'] + ); + $result_expected = $case['output']; + $assert->equal($result_actual, $result_expected); + }, + ]; + }, + $data['mod']['cases'] + ), + ], + [ + 'name' => 'modpow', + 'cases' => array_map( + function (array $case) : array { + return [ + 'name' => $case['name'], + 'procedure' => function ($assert) use ($case) { + $result_actual = \alveolata\math\modpow( + $case['input']['base'], + $case['input']['exponent'], + $case['input']['modulus'] + ); + $result_expected = $case['output']; + $assert->equal($result_actual, $result_expected); + }, + ]; + }, + $data['modpow']['cases'] + ), + ], + [ + 'name' => 'set_union', + 'cases' => array_map( + function (array $case) : array { + return [ + 'name' => $case['name'], + 'procedure' => function ($assert) use ($case) { + $result_actual = \alveolata\math\set_union( + function ($x, $y) {return ($x === $y);}, + $case['input']['set1'], + $case['input']['set2'] + ); + $result_expected = $case['output']; + $assert->equal($result_actual, $result_expected); + }, + ]; + }, + $data['set_union']['cases'] + ) + ], + [ + 'name' => 'set_intersection', + 'cases' => array_map( + function (array $case) : array { + return [ + 'name' => $case['name'], + 'procedure' => function ($assert) use ($case) { + $result_actual = \alveolata\math\set_intersection( + function ($x, $y) {return ($x === $y);}, + $case['input']['set1'], + $case['input']['set2'] + ); + $result_expected = $case['output']; + $assert->equal($result_actual, $result_expected); + }, + ]; + }, + $data['set_intersection']['cases'] + ) + ], + [ + 'name' => 'set_difference', + 'cases' => array_map( + function (array $case) : array { + return [ + 'name' => $case['name'], + 'procedure' => function ($assert) use ($case) { + $result_actual = \alveolata\math\set_difference( + function ($x, $y) {return ($x === $y);}, + $case['input']['set1'], + $case['input']['set2'] + ); + $result_expected = $case['output']; + $assert->equal($result_actual, $result_expected); + }, + ]; + }, + $data['set_difference']['cases'] + ) + ], + [ + 'name' => 'order_lexicographic', + 'cases' => array_map( + function (array $case) : array { + return [ + 'name' => $case['name'], + 'procedure' => function ($assert) use ($case) { + $result_actual = \alveolata\math\order_lexicographic( + (function ($x, $y) {return ($x <= $y);}), + $case['input']['list1'], + $case['input']['list2'] + ); + $result_expected = $case['output']; + $assert->equal($result_actual, $result_expected); + }, + ]; + }, + $data['order_lexicographic']['cases'] + ) + ], + ], + ] + ] + ] +); + + ?> diff --git a/lib/alveolata/math/testdata.json b/lib/alveolata/math/testdata.json new file mode 100644 index 0000000..a8fa33b --- /dev/null +++ b/lib/alveolata/math/testdata.json @@ -0,0 +1,188 @@ +{ + "mod": { + "cases": [ + { + "name": "positive divident, regular", + "input": { + "divident": 13, + "divisor": 5 + }, + "output": 3 + }, + { + "name": "positive divident, lower edge case", + "input": { + "divident": 0, + "divisor": 5 + }, + "output": 0 + }, + { + "name": "positive divident, upper edge case", + "input": { + "divident": 5, + "divisor": 5 + }, + "output": 0 + }, + { + "name": "negative divident", + "input": { + "divident": -13, + "divisor": 5 + }, + "output": 2 + } + ] + }, + "modpow": { + "cases": [ + { + "name": "test", + "input": { + "base": 23, + "exponent": 5, + "modulus": 31 + }, + "output": 30 + } + ] + }, + "set_union": { + "cases": [ + { + "name": "test1", + "input": { + "set1": [0,1], + "set2": [0,1] + }, + "output": [0,1] + }, + { + "name": "test2", + "input": { + "set1": [0,1], + "set2": [1,2] + }, + "output": [0,1,2] + }, + { + "name": "test3", + "input": { + "set1": [0,1], + "set2": [2,3] + }, + "output": [0,1,2,3] + } + ] + }, + "set_intersection": { + "cases": [ + { + "name": "test1", + "input": { + "set1": [0,1], + "set2": [0,1] + }, + "output": [0,1] + }, + { + "name": "test2", + "input": { + "set1": [0,1], + "set2": [1,2] + }, + "output": [1] + }, + { + "name": "test3", + "input": { + "set1": [0,1], + "set2": [2,3] + }, + "output": [] + } + ] + }, + "set_difference": { + "cases": [ + { + "name": "test1", + "input": { + "set1": [0,1], + "set2": [0,1] + }, + "output": [] + }, + { + "name": "test2", + "input": { + "set1": [0,1], + "set2": [1,2] + }, + "output": [0] + }, + { + "name": "test3", + "input": { + "set1": [0,1], + "set2": [2,3] + }, + "output": [0,1] + } + ] + }, + "order_lexicographic": { + "cases": [ + { + "name": "test1", + "input": { + "list1": [], + "list2": [] + }, + "output": true + }, + { + "name": "test2", + "input": { + "list1": [2], + "list2": [] + }, + "output": false + }, + { + "name": "test3", + "input": { + "list1": [], + "list2": [2] + }, + "output": true + }, + { + "name": "test4", + "input": { + "list1": [2], + "list2": [2] + }, + "output": true + }, + { + "name": "test5", + "input": { + "list1": [2], + "list2": [3] + }, + "output": true + }, + { + "name": "test6", + "input": { + "list1": [2], + "list2": [1] + }, + "output": false + } + ] + } +} + diff --git a/lib/alveolata/module/implementation-system.php b/lib/alveolata/module/implementation-system.php new file mode 100644 index 0000000..d74bf8b --- /dev/null +++ b/lib/alveolata/module/implementation-system.php @@ -0,0 +1,67 @@ +>>} + */ + private $members; + + + /** + * @var null|array {union>} + */ + private $order; + + + /** + * @param array $members {map>>} + */ + public function __construct( + array $members + ) + { + $this->members = $members; + $dependencies = []; + foreach ($this->members as $name => $stuff) { + $dependencies[$name] = $stuff['dependencies']; + } + $this->order = \alveolata\algorithm\topsort($dependencies); + } + + + /** + * [implementation] + * @author Christian Fraß + */ + function setup( + ) : void + { + foreach ($this->order as $name) { + $this->members[$name]['module']->setup(); + } + } + + + /** + * [implementation] + * @author Christian Fraß + */ + function teardown( + ) : void + { + foreach (array_reverse($this->order) as $name) { + $this->members[$name]['module']->teardown(); + } + } + +} + + ?> diff --git a/lib/alveolata/module/interface.php b/lib/alveolata/module/interface.php new file mode 100644 index 0000000..f64ff50 --- /dev/null +++ b/lib/alveolata/module/interface.php @@ -0,0 +1,31 @@ + + */ + function setup( + ) : void + ; + + + /** + * shall close/remove/terminate the module + * + * @author Christian Fraß + */ + function teardown( + ) : void + ; + +} + + ?> diff --git a/lib/alveolata/observer/functions.php b/lib/alveolata/observer/functions.php new file mode 100644 index 0000000..5d945a0 --- /dev/null +++ b/lib/alveolata/observer/functions.php @@ -0,0 +1,73 @@ +} + */ + public $listeners; + + + /** + * @array {map} + */ + public function __construct( + array $listeners + ) + { + $this->listeners = $listeners; + } + +} + + +/** + * @return struct + */ +function make( +) : struct +{ + return ( + new struct([]) + ); +} + + +/** + * @param struct $subject + * @param \Closure $procedure {function} + */ +function register( + struct $subject, + \Closure $procedure +) : string +{ + $count = count($subject->listeners); + $id = sprintf('listener_%d', $count); + $subject->listeners[$id] = $procedure; + return $id; +} + + +/** + * @param struct $subject + * @param mixed $data {any} + */ +function notify( + struct $subject, + $data +)/* : void*/ +{ + $result = []; + foreach ($subject->listeners as $id => $procedure) { + $result[$id] = $procedure($data); + } + return $result; +} + + ?> diff --git a/lib/alveolata/observer/test.spec.php b/lib/alveolata/observer/test.spec.php new file mode 100644 index 0000000..5d35b49 --- /dev/null +++ b/lib/alveolata/observer/test.spec.php @@ -0,0 +1,48 @@ + 'alveolata', + 'sections' => [ + [ + 'name' => 'observer', + 'sections' => [ + [ + 'name' => 'register_notify', + 'cases' => [ + [ + 'name' => 'test', + 'procedure' => function ($assert) { + $subject = \alveolata\observer\make(); + $value = [ + 'own' => null, + 'other' => null, + ]; + $id = \alveolata\observer\register( + $subject, + function ($data) use (&$value) : void { + $value = [ + 'own' => 7, + 'other' => $data, + ]; + } + ); + \alveolata\observer\notify( + $subject, + 11 + ); + $assert->equal($value, ['own' => 7, 'other' => 11]); + } + ], + ] + ] + ] + ], + ] + ] +); + + ?> diff --git a/lib/alveolata/observer/wrapper-class.php b/lib/alveolata/observer/wrapper-class.php new file mode 100644 index 0000000..5ad0744 --- /dev/null +++ b/lib/alveolata/observer/wrapper-class.php @@ -0,0 +1,42 @@ + + */ + private $subject; + + + /** + * @author Christian Fraß + */ + private function __construct(struct $subject) {$this->subject = $subject;} + + + /** + * @author Christian Fraß + */ + public static function make() : class_observer {return (new class_observer(make()));} + + + /** + * implementations + * + * @author Christian Fraß + */ + public function register(\Closure $procedure) {return register($this->subject, $procedure);} + public function notify($data) {notify($this->subject, $data);} + +} + + ?> diff --git a/lib/alveolata/pod/functions.php b/lib/alveolata/pod/functions.php new file mode 100644 index 0000000..518d384 --- /dev/null +++ b/lib/alveolata/pod/functions.php @@ -0,0 +1,124 @@ + + */ +class struct_pod +{ + + /** + * @var bool + */ + public $full; + + + /** + * @var null|TypeValue + */ + public $value; + + + /** + * @param bool $full + * @param null|TypeValue $value + */ + public function __construct( + bool $full, + $value + ) + { + $this->full = $full; + $this->value = $value; + } + +} + + +/** + * @template TypeValue + * @return struct_pod + * @author Christian Fraß + */ +function make_toom( +) : struct_pod +{ + return (new struct_pod(false, null)); +} + + +/** + * @template TypeValue + * @param TypeValue $value + * @return struct_pod + * @author Christian Fraß + */ +function make_full( + $value +) : struct_pod +{ + return (new struct_pod(true, $value)); +} + + +/** + * @template TypeValue + * @param struct_pod + * @return bool + * @author Christian Fraß + */ +function has( + struct_pod $pod +): bool +{ + return $pod->full; +} + + +/** + * @template TypeValue + * @param struct_pod + * @return TypeValue + * @author Christian Fraß + */ +function get( + struct_pod $pod +) +{ + if (! $pod->full) { + throw (new \Exception('empty')); + } + else { + return $pod->value; + } +} + + +/** + * creates a pod on base of the input pod; i.e. if the input pod is empty, the output put is too; if the input pod is + * full, the function is applied to its value to make up a new full pod with the result value + * + * @template TypeValueFrom + * @template TypeValueTo + * @param struct_pod + * @param \Closure {function} + * @return struct_pod + * @author Christian Fraß + */ +function brook( + struct_pod $pod, + \Closure $function +) : struct_pod +{ + if (! is_something($pod)) { + return make_empty(); + } + else { + return make_full($function($pod->value)); + } +} + + ?> diff --git a/lib/alveolata/pod/test.spec.php b/lib/alveolata/pod/test.spec.php new file mode 100644 index 0000000..0acc550 --- /dev/null +++ b/lib/alveolata/pod/test.spec.php @@ -0,0 +1,50 @@ + 'alveolata', + 'sections' => [ + [ + 'name' => 'pod', + 'sections' => [ + [ + 'active' => true, + 'name' => 'has_get', + 'setup' => function (&$env) { + $env['safe_division'] = function (int $divident, int $divisor) : pod { + if ($divisor === 0) { + return pod::toom(); + } + else { + return pod::full(intval(floor($divident/$divisor))); + } + }; + }, + 'cases' => [ + [ + 'name' => 'fail', + 'procedure' => function ($assert, &$env) { + $result_actual = $env['safe_division'](42, 0); + $assert->equal($result_actual->has(), false); + }, + ], + [ + 'name' => 'success', + 'procedure' => function ($assert, &$env) { + $result_actual = $env['safe_division'](42, 7); + $assert->equal($result_actual->has(), true); + $assert->equal($result_actual->get(), 6); + }, + ], + ] + ], + ] + ] + ] + ] +); + + ?> diff --git a/lib/alveolata/pod/wrapper-class.php b/lib/alveolata/pod/wrapper-class.php new file mode 100644 index 0000000..008a0ec --- /dev/null +++ b/lib/alveolata/pod/wrapper-class.php @@ -0,0 +1,61 @@ + + */ +class class_pod +{ + + /** + * @var struct_pod + */ + private $subject; + + + /** + * [constructor] + * + * @param struct_pod $subject + * @author Christian Fraß + */ + private function __construct(struct_pod $subject) {$this->subject = $subject;} + + + /** + * @template TypeValue + * @return struct_pod + * @author Christian Fraß + */ + public static function toom() : class_pod {return (new class_pod(make_toom()));} + + + /** + * @template TypeValue + * @param TypeValue $value + * @return struct_pod + * @author Christian Fraß + */ + public static function full($value) : class_pod {return (new class_pod(make_full($value)));} + + + /** + * implementations + * + * @author Christian Fraß + */ + public function has() : bool {return has($this->subject);} + public function get() {return get($this->subject);} + public function brook(\Closure $function) : class_pod {return (new class_pod(brook($this->subject, $function)));} + // public function __isset() : bool {return $this->has();} + +} + +class_alias('\alveolata\pod\class_pod', 'pod'); + + ?> diff --git a/lib/alveolata/random/functions.php b/lib/alveolata/random/functions.php new file mode 100644 index 0000000..b3a1b06 --- /dev/null +++ b/lib/alveolata/random/functions.php @@ -0,0 +1,304 @@ +} + * @return type_element + */ +function choose_uniformly( + array $list +) +{ + $index = generate_integer(0, count($list)-1); + return $list[$index]; +} + + +/** + * chooses a value randomly from a list of values with weights (a higher weight means a higher probability to be chosen) + * + * @template type_element + * @param array $list {list>} + * @return type_element + */ +function choose_weighted( + array $sets +) +{ + $sum = array_reduce( + $sets, + function (float $sum, array $entry) : float {return ($sum + $entry['weight']);}, + 0 + ); + if ($sum === 0) { + throw (new \Exception('weights sum up to zero; are all zero or are negative weights included?')); + } + else { + $position = generate_unit(); + return array_reduce( + $sets, + function ($current, $set) use ($sum, $position) { + $next = ['index' => null, 'value' => null]; + $next['index'] = ($current['index'] + ($set['weight'] / $sum)); + $next['value'] = ( + (($current['index'] <= $position) && ($position < $next['index'])) + ? $set['value'] + : $current['value'] + ); + return $next; + }, + ['index' => 0, 'value' => null] + )['value']; + } +} + + +/** + * @return string + */ +function generate_vowel( +) : string +{ + return choose_weighted([ + ['value' => 'i', 'weight' => 1], + ['value' => 'e', 'weight' => 2], + ['value' => 'a', 'weight' => 3], + ['value' => 'o', 'weight' => 2], + ['value' => 'u', 'weight' => 1], + ]); +} + + +/** + * @return string + */ +function generate_semivowel( +) : string +{ + return choose_weighted([ + ['value' => 'y', 'weight' => 2], + ['value' => 'w', 'weight' => 1], + ]); +} + + +/** + * @return string + */ +function generate_lateral_approximant( +) : string +{ + return choose_weighted([ + ['value' => 'l', 'weight' => 1], + ]); +} + + +/** + * @return string + */ +function generate_nasal( +) : string +{ + return choose_weighted([ + ['value' => 'n', 'weight' => 4], + ['value' => 'm', 'weight' => 2], + ['value' => 'q', 'weight' => 1], + ]); +} + + +/** + * @return string + */ +function generate_plosive( +) : string +{ + return choose_weighted([ + ['value' => 'b', 'weight' => 1], + ['value' => 'p', 'weight' => 2], + ['value' => 'd', 'weight' => 1], + ['value' => 't', 'weight' => 2], + ['value' => 'g', 'weight' => 1], + ['value' => 'k', 'weight' => 2], + ]); +} + + +/** + * @return string + */ +function generate_fricative( +) : string +{ + return choose_weighted([ + ['value' => 'r', 'weight' => 2], + ['value' => 'h', 'weight' => 1], + ['value' => 'x', 'weight' => 1], + ['value' => 'j', 'weight' => 1], + ['value' => 'c', 'weight' => 1], + ['value' => 'v', 'weight' => 2], + ['value' => 'f', 'weight' => 2], + ['value' => 's', 'weight' => 2], + ['value' => 'z', 'weight' => 2], + ]); +} + + +/** + * @return string + */ +function generate_consonant( +) : string +{ + return choose_weighted([ + ['value' => generate_lateral_approximant(), 'weight' => 1], + ['value' => generate_nasal(), 'weight' => 1], + ['value' => generate_plosive(), 'weight' => 1], + ['value' => generate_fricative(), 'weight' => 1], + ]); +} + + +/** + * @return string + */ +function generate_syllable( +) : string +{ + /* + +---+---+---+---+---+---+---+ + | | L | N | P | F | S | V | + +---+---+---+---+---+---+---+ + | L | | o | x | x | o | x | + +---+---+---+---+---+---+---+ + | N | | | o | x | | x | + +---+---+---+---+---+---+---+ + | P | x | o | o | x | x | x | + +---+---+---+---+---+---+---+ + | F | x | o | | o | x | x | + +---+---+---+---+---+---+---+ + | S | x | x | x | x | | x | + +---+---+---+---+---+---+---+ + | V | x | x | x | x | x | o | + +---+---+---+---+---+---+---+ + + {L,N,P,F} + {LP,LF,NF,NP,PL,PF,FL} + {LPF,NPL,NPF,PFL} + + pflantze + glas + crayb-tic + + + */ + $length1 = choose_weighted([ + ['value' => 1, 'weight' => 3], + ['value' => 2, 'weight' => 4], + ['value' => 3, 'weight' => 1], + ]); + + return sprintf( + '%s%s%s', + generate_consonant(), + generate_vowel(), + (generate_boolean(0.125) ? generate_semivowel() : '') + ); +} + + +/** + * @return string + */ +function generate_word( +) : string +{ + $length = generate_integer(1, 4); + $syllables = []; + for ($index = 0; $index < $length; $index += 1) { + $syllables[] = generate_syllable(); + } + return implode('', $syllables); +} + + +/** + * @return string + */ +function generate_text( +) : string +{ + $length = generate_integer(8, 32); + $words = []; + for ($index = 0; $index < $length; $index += 1) { + $words[] = generate_word(); + } + return implode(' ', $words); +} + + ?> diff --git a/lib/alveolata/random/test.php b/lib/alveolata/random/test.php new file mode 100644 index 0000000..32bdd5e --- /dev/null +++ b/lib/alveolata/random/test.php @@ -0,0 +1,8 @@ + diff --git a/lib/alveolata/readme.md b/lib/alveolata/readme.md new file mode 100644 index 0000000..2f30235 --- /dev/null +++ b/lib/alveolata/readme.md @@ -0,0 +1,12 @@ +## Requirements + +### PHP Modules + +- `SQLite3` +- `mysqli` + + +## Testing + +- `php main.php []` (e.g. `php main.php cache.memory`) + diff --git a/lib/alveolata/report/functions.php b/lib/alveolata/report/functions.php new file mode 100644 index 0000000..204c03e --- /dev/null +++ b/lib/alveolata/report/functions.php @@ -0,0 +1,99 @@ + + */ +class struct_report +{ + + /** + * @var string + * @author Christian Fraß + */ + public $incident; + + + /** + * @var array record + * @author Christian Fraß + */ + public $details; + + + /** + * @var int $timestamp UNIX timestamp + * @author Christian Fraß + */ + public $timestamp; + + + /** + * @param string $incident + * @param array $details + * @author Christian Fraß + */ + public function __construct( + string $incident, + array $details, + int $timestamp + ) + { + $this->incident = $incident; + $this->details = $details; + $this->timestamp = $timestamp; + } + +} + + +/** + * @param string $incident + * @param array $details + * @author Christian Fraß + */ +function make( + string $incident, + array $details = [], + int $timestamp = -1 +) : struct_report +{ + if ($timestamp < 0) { + $timestamp = time(); + } + return ( + new struct_report( + $incident, + $details, + $timestamp + ) + ); +} + + +/** + * @param \alveolata\report\type $report + * @return Exception + * @author Christian Fraß + */ +function as_exception( + struct_report $report +) : \Exception +{ + $message = \alveolata\string\coin( + '{{incident}} | {{details}}', + [ + 'incident' => $report->incident, + 'details' => \alveolata\json\encode($report->details), + ] + ); + return (new \Exception($message)); +} + + ?> diff --git a/lib/alveolata/rest/functions.php b/lib/alveolata/rest/functions.php new file mode 100644 index 0000000..364498e --- /dev/null +++ b/lib/alveolata/rest/functions.php @@ -0,0 +1,715 @@ +sub_branch)) { + foreach ($node->sub_branch as $key => $sub) { + route_node_iterate($sub, $procedure, \alveolata\list_\concat($steps, [$key])); + } + } + if (! \is_null($node->sub_wildcard)) { + route_node_iterate( + $node->sub_wildcard['node'], + $procedure, + \alveolata\list_\concat($steps, [wildcard_step_encode($node->sub_wildcard['name'])]) + ); + } +} + + +/** + */ +function route_node_flatten( + struct_route_node $node +) : array +{ + $list = []; + route_node_iterate( + $node, + function ($node_, $steps) use (&$list) { + \array_push($list, ['steps' => $steps, 'node' => $node_]); + }, + [] + ); + return $list; +} + + +/** + * @param string $http_method {&\alveolata\http\enum_method} + */ +function route_node_spawn( + array $steps, + string $http_method, + string $action_name +) : ?struct_route_node +{ + if (\count($steps) <= 0) { + $node = (new struct_route_node([$http_method => $action_name], null, null)); + } + else { + $steps_head = $steps[0]; + $steps_tail = \array_slice($steps, 1); + $sub = route_node_spawn($steps_tail, $http_method, $action_name); + $wildcard_name = wildcard_step_decode($steps_head); + if (\is_null($wildcard_name)) { + // branch + $node = (new struct_route_node([], [$steps_head => $sub], null)); + } + else { + // wildcard + $node = (new struct_route_node([], null, [$wildcard_name => $sub])); + } + } + return $node; +} + + +/** + * @return record< + * steps:list, + * rest:list, + * node:struct_route_node, + * parameters:list<(null|string)>, + * > + */ +function route_node_path_read( + struct_route_node $node, + array $steps, + ?array $options = null +) : array +{ + $options = \array_merge( + [ + ], + ($options ?? []) + ); + + if (\count($steps) <= 0) { + return [ + 'steps' => [], + 'rest' => [], + 'node' => $node, + 'parameters' => [], + ]; + } + else { + $path_head = $steps[0]; + $path_tail = \array_slice($steps, 1); + if (\is_null($node->sub_branch) && \is_null($node->sub_wildcard)) { + throw (new \Exception('uncertain route node kind')); + } + else { + if (! \is_null($node->sub_branch)) { + // branch + if (! \array_key_exists($path_head, $node->sub_branch)) { + return [ + 'steps' => [$path_head], + 'rest' => $path_tail, + 'node' => $node, + 'parameters' => [], + ]; + } + else { + $result = route_node_path_read($node->sub_branch[$path_head], $path_tail); + return [ + 'steps' => \alveolata\list_\concat([$path_head], $result['steps']), + 'rest' => $result['rest'], + 'node' => $result['node'], + 'parameters' => $result['parameters'], + ]; + } + } + else if (! \is_null($node->sub_wildcard)) { + // wildcard + $result = route_node_path_read($node->sub_wildcard['node'], $path_tail); + if (! \array_key_exists($node->sub_wildcard['name'], $result['parameters'])) { + // do nothing + } + else { + \alveolata\log\warning( + '[alveolata:rest] overwriting path parameter', + [ + 'key' => $node->sub_wildcard['name'], + 'value_old' => $result['parameters'][$node->sub_wildcard['name']], + 'value_new' => $path_head, + ] + ); + } + return [ + 'steps' => \alveolata\list_\concat([$path_head], $result['steps']), + 'rest' => $result['rest'], + 'node' => $result['node'], + 'parameters' => \alveolata\list_\concat([$node->sub_wildcard['name'] => $path_head], $result['parameters']), + ]; + } + else { + throw (new \Exception('inconsistent route node kind')); + } + } + } +} + + +/** + * @param array $steps {list} + */ +function route_node_path_write( + struct_route_node $node, + array $steps, + string $http_method, + string $action_name, + ?array $options = null +) : void +{ + $options = \array_merge( + [ + 'create' => false, + ], + ($options ?? []) + ); + + if (\count($steps) <= 0) { + if (! \array_key_exists($http_method, $node->operations)) { + // do nothing + } + else { + \alveolata\log\warning( + '[alveolata:rest] overwriting action', + [ + 'http_method' => $http_method, + 'steps' => $step, + ] + ); + } + $node->operations[$http_method] = $action_name; + } + else { + $steps_head = $steps[0]; + $steps_tail = \array_slice($steps, 1); + + if (\is_null($node->sub_branch) && \is_null($node->sub_wildcard)) { + if (! $options['create']) { + throw (new \Exception('may not create missing route')); + } + else { + $wildcard_name = wildcard_step_decode($steps_head); + if (\is_null($wildcard_name)) { + // branch + $node->sub_branch[$steps_head] = route_node_spawn($steps_tail, $http_method, $action_name); + } + else { + // wildcard + $node->sub_wildcard = [ + 'name' => $wildcard_name, + 'node' => route_node_spawn($steps_tail, $http_method, $action_name), + ]; + } + } + } + else { + if (! \is_null($node->sub_branch)) { + // branch + if (! \array_key_exists($steps_head, $node->sub_branch)) { + if (! $options['create']) { + throw (new \Exception('no such route: ' . \alveolata\string\join(\alveolata\list_\concat([''], $steps), '/'))); + } + else { + $node->sub_branch[$steps_head] = route_node_spawn($steps_tail, $http_method, $action_name); + } + } + else { + route_node_path_write($node->sub_branch[$steps_head], $steps_tail, $http_method, $action_name, $options); + } + } + else if (! \is_null($node->sub_wildcard)) { + // wildcard + $wildcard_name = wildcard_step_decode($steps_head); + if (\is_null($wildcard_name)) { + throw (new \Exception('expected brace enclosed name step on wildcard node')); + } + else { + route_node_path_write($node->sub_wildcard['node'], $steps_tail, $http_method, $action_name, $options); + } + } + else { + throw (new \Exception('inconsistent route node kind')); + } + } + } +} + + +/** + * @param ?array { + * ( + * null + * | + * record< + * ?name:string, + * ?versioning_method:string, + * ?versioning_header_name:(null|string), + * ?versioning_query_key:(null|string), + * ?response_body_encode:function, + * > + * ) + * } + */ +function make( + ?array $options = null +) : struct_subject +{ + $options = \array_merge( + [ + 'name' => 'REST-API', + 'versioning_method' => 'path', + 'versioning_header_name' => 'X-Api-Version', + 'versioning_query_key' => 'version', + 'request_body_decode' => (fn($data) => \json_decode($data, true)), + 'request_body_mimetype' => 'application/json', + 'response_body_encode' => (fn($data) => \json_encode($data)), + 'response_body_mimetype' => 'application/json', + ], + ($options ?? []) + ); + return ( + new struct_subject( + \alveolata\api\make( + $options['name'] + ), + new struct_route_node( + [], + null, + null + ), + $options['versioning_method'], + $options['versioning_header_name'], + $options['versioning_query_key'], + $options['request_body_decode'], + $options['request_body_mimetype'], + $options['response_body_encode'], + $options['response_body_mimetype'] + ) + ); +} + + +/** + * @param ?array $options { + * ( + * null + * | + * record< + * ?active:function, + * ?execution:function,record,any,any>, + * ?title:(null|string), + * ?description:(null|string), + * ?input_type:function, + * ?output_type:function, + * > + * ) + * } + * @todo use "custom" for documenting the different responses? + */ +function register( + struct_subject $subject, + string $http_method, + string $path, + ?array $options = null +) : void +{ + $options = \array_merge( + [ + 'active' => function ($version) { + return true; + }, + 'execution' => function ($version, $path_parameters, $headers, $input) { + // throw (new \Exception(\sprintf('not implemented: "%s %s"', $http_method, $path))); + return [ + 'statuscode' => 501, + 'data' => 'not implemented', + ]; + }, + 'title' => null, + 'description' => null, + 'input_type' => function ($version) { + return ['kind' => 'any']; + }, + 'output_type' => function ($version) { + return ['kind' => 'any']; + }, + ], + ($options ?? []) + ); + + $steps = \array_slice(\alveolata\string\split($path, '/'), 1); + $steps_enriched = ( + ($subject->versioning_method === 'path') + ? \array_merge(['{version}'], $steps) + : $steps + ); + $action_name = \alveolata\string\join( + \alveolata\list_\concat( + $steps, + [\strtolower($http_method)] + ), + '_' + ); + route_node_path_write( + $subject->route_tree, + $steps_enriched, + $http_method, + $action_name, + [ + 'create' => true, + ] + ); + \alveolata\api\register( + $subject->api_subject, + $action_name, + [ + 'active' => $options['active'], + 'execution' => function ($version, $environment, $input) use ($options) { + return $options['execution']( + $version, + $environment['path_parameters'], + $environment['headers'], + $input + ); + }, + 'title' => $options['title'], + 'description' => $options['description'], + 'input_type' => $options['input_type'], + 'output_type' => $options['output_type'], + ] + ); + \alveolata\log\info( + '[alveolata:rest] route added', + [ + 'route_tree' => $subject->route_tree + ] + ); +} + + +/** + */ +function call( + struct_subject $subject, + \alveolata\http\struct_request $http_request +) : \alveolata\http\struct_response +{ + // parse target + $target_parts = \parse_url($http_request->target); + $path = $target_parts['path']; + + // parse query + $query_parameters = null; \parse_str($target_parts['query'] ?? '', $query_parameters); + + $steps = \array_slice(\alveolata\string\split($path, '/'), 1); + if (\count($steps) <= 0) { + throw (new \Exception('empty path')); + } + else { + // resolve + $stuff = route_node_path_read($subject->route_tree, $steps, []); + if (\count($stuff['rest']) > 0) { + throw (new \Exception(\sprintf('path not found: %s', $path))); + } + else { + if (! \array_key_exists($http_request->method, $stuff['node']->operations)) { + throw (new \Exception(\sprintf('no route "%s %s"', $http_request->method, $path))); + } + else { + $action_name = $stuff['node']->operations[$http_request->method]; + + // get version + switch ($subject->versioning_method) { + case 'path': { + $version = $stuff['parameters']['version']; + // unset($stuff['parameters']['version']); + break; + } + case 'header': { + $version = $http_request->headers[$subject->versioning_header_name]; + // unset($http_request->headers[$subject->versioning_header_name]); + break; + } + case 'query': { + $version = $query_parameters[$subject->versioning_query_key]; + // unset($query_parameters[$subject->versioning_query_key]); + break; + } + default: { + throw (new \Exception('unhandled versioning method: ' . $subject->versioning_method)); + break; + } + } + + // call + $output = \alveolata\api\call( + $subject->api_subject, + $action_name, + [ + 'version' => $version, + 'environment' => [ + 'headers' => $http_request->headers, + 'path_parameters' => $stuff['parameters'], + 'query_parameters' => $query_parameters, + ], + 'input' => ($subject->request_body_decode)($http_request->body), + ] + ); + + // encode + return ( + new \alveolata\http\struct_response( + $output['status_code'], + [], + ($subject->response_body_encode)($output['data']) + ) + ); + } + } + } +} + + +/** + * @return mixed + * @see https://swagger.io/specification/#openapi-object + */ +function to_oas( + struct_subject $subject, + ?array $options = null +) : array +{ + $options = \array_merge( + [ + 'version' => null, + 'servers' => [], + ], + ($options ?? []) + ); + $version = ($options['version'] ?? '-'); + + return [ + 'openapi' => '3.0.2', + 'info' => [ + 'title' => $subject->api_subject->name, + 'version' => $version, + ], + 'servers' => $options['servers'], + 'paths' => \alveolata\call\convey( + $subject->route_tree, + [ + fn($x) => route_node_flatten($x), + fn($x) => \alveolata\list_\map( + $x, + function ($entry) use ($subject, $options) { + $steps_ = ( + ($subject->versioning_method === 'path') + ? \array_merge([$options['version'] ?? '-'], \array_slice($entry['steps'], 1)) + : $entry['steps'] + ); + $path = ( + (\count($steps_) <= 0) + ? '/' + : \alveolata\string\join( + \alveolata\list_\concat([''], $steps_), + '/' + ) + ); + $key = ( + (\count($steps_) <= 0) + ? '/' + : \alveolata\string\join( + \alveolata\list_\concat([''], $steps_), + '/' + ) + ); + return [ + 'key' => $key, + 'value' => \alveolata\call\convey( + $entry['node']->operations, + [ + fn($x) => \alveolata\map\to_pairs($x), + fn($x) => \alveolata\list_\map( + $x, + fn($pair) => [ + 'key' => \alveolata\http\method_to_oas($pair['key']), + 'value' => $pair['value'], + ] + ), + fn($x) => \alveolata\map\from_pairs($x), + fn($x) => \alveolata\map\map( + $x, + fn($action_name, $http_method) => [ + 'operationId' => $action_name, + 'summary' => ( + $subject->api_subject->actions[$action_name]->title + ?? + \alveolata\call\convey( + \alveolata\list_\concat( + [\strtolower($http_method)], + ( + ($subject->versioning_method === 'path') + ? \array_slice($steps_, 1) + : $steps_ + ) + ), + [ + fn($x) => \alveolata\list_\map( + $x, + fn($step) => ( + \is_null(wildcard_step_decode($step)) + ? $step + : '(specific)' + ) + ), + fn($x) => \alveolata\string\join($x, ' '), + ] + ) + ), + 'description' => $subject->api_subject->actions[$action_name]->description, + 'parameters' => \array_merge( + [], + ( + ($subject->versioning_method === 'query') + ? [ + [ + 'name' => $subject->versioning_query_key, + 'in' => 'query', + 'required' => true, + 'schema' => [ + 'type' => 'string', + 'enum' => [$options['version'] ?? '-'], + ], + ] + ] + : [] + ), + ( + ($subject->versioning_method === 'header') + ? [ + [ + 'name' => $subject->versioning_header_name, + 'in' => 'header', + 'required' => true, + 'schema' => [ + 'type' => 'string', + 'enum' => [$options['version'] ?? '-'], + ], + ] + ] + : [] + ), + \alveolata\call\convey( + $steps_, + [ + fn($x) => \alveolata\list_\map($x, fn($y) => wildcard_step_decode($y)), + fn($x) => \alveolata\list_\filter($x, fn($y) => (! \is_null($y))), + fn($x) => \alveolata\list_\map( + $x, + fn($y) => [ + 'name' => $y, + 'in' => 'path', + 'required' => true, + 'schema' => [ + 'type' => 'string', + ], + ] + ), + ] + ), + ), + 'requestBody' => [ + 'content' => [ + $subject->request_body_mimetype => [ + 'schema' => ($subject->api_subject->actions[$action_name]->input_type)($options['version'])->to_oas(), + ] + ] + ], + 'responses' => [ + 'default' => [ + 'content' => [ + $subject->response_body_mimetype => [ + 'schema' => ($subject->api_subject->actions[$action_name]->output_type)($options['version'])->to_oas(), + ] + ] + ] + ], + ] + ), + ] + ), + ]; + } + ), + fn($x) => \alveolata\list_\filter( + $x, + fn($y) => (\count($y['value']) > 0) + ), + fn($x) => \alveolata\map\from_pairs($x), + ] + ) + ]; +} + + ?> diff --git a/lib/alveolata/rest/test.spec.php b/lib/alveolata/rest/test.spec.php new file mode 100644 index 0000000..f953ad9 --- /dev/null +++ b/lib/alveolata/rest/test.spec.php @@ -0,0 +1,387 @@ + 'alveolata', + 'sections' => [ + [ + 'name' => 'rest', + 'sections' => [ + [ + 'name' => 'oblique', + 'setup' => function (&$env) { + $env['reset_data'] = function () use (&$env) { + $env['data'] = [ + 'foo' => [ + 2 => [ + 'de' => 'zwei', + 'en' => null, + ], + 3 => [ + 'de' => 'drei', + 'en' => null, + ], + 5 => [ + 'de' => 'fünf', + 'en' => null, + ], + 7 => [ + 'de' => 'sieben', + 'en' => null, + ], + ] + ]; + }; + $env['data'] = null; + $env['reset_data'](); + $type_description_key = [ + 'kind' => 'integer', + ]; + $type_description_value = [ + 'kind' => 'record', + 'data' => [ + 'fields' => [ + [ + 'name' => 'de', + 'type' => [ + 'kind' => 'union', + 'data' => [ + 'members' => [ + [ + 'kind' => 'null' + ], + [ + 'kind' => 'string' + ], + ] + ] + ], + ], + [ + 'name' => 'en', + 'type' => [ + 'kind' => 'union', + 'data' => [ + 'members' => [ + [ + 'kind' => 'null' + ], + [ + 'kind' => 'string' + ], + ] + ] + ], + ], + ], + ] + ]; + + /* + GET /{version}/foo + POST /{version}/foo + GET /{version}/foo/{id} + PUT /{version}/foo/{id} + DELETE /{version}/foo/{id} + */ + $env['subject'] = \alveolata\rest\make( + [ + 'name' => 'Alveolata-Test', + 'versioning_method' => 'path', + 'request_body_mimetype' => 'application/json', + 'request_body_decode' => (fn($x) => \json_decode($x)), + 'response_body_encode' => (fn($x) => \json_encode($x)), + 'response_body_mimetype' => 'application/json', + ] + ); + \alveolata\rest\register( + $env['subject'], + \alveolata\http\enum_method::get, + '/foo', + [ + 'execution' => function ($version, $path_parameters, $headers, $input) use (&$env) { + return [ + 'status_code' => 200, + 'data' => \array_keys($env['data']['foo']), + ]; + }, + 'title' => 'list foos', + 'description' => 'lists the elements of the foo domain', + 'input_type' => (fn($version) => ['kind' => 'null']), + 'output_type' => (fn($version) => ['kind' => 'list', 'data' => ['type_element' => $type_description_value]]), + ] + ); + \alveolata\rest\register( + $env['subject'], + \alveolata\http\enum_method::post, + '/foo', + [ + 'execution' => function ($version, $path_parameters, $headers, $input) use (&$env) { + $prod = \array_reduce( + \array_keys($env['data']['foo']), + fn($x, $y) => ($x * $y), + 1 + ); + $key = ($prod + 1); + $env['data']['foo'][$key] = $input; + return [ + 'status_code' => 201, + 'data' => $key, + ]; + }, + 'title' => 'create foo', + 'description' => 'adds an element to the foo domain', + 'input_type' => (fn($version) => $type_description_key), + 'output_type' => (fn($version) => $type_description_value), + ] + ); + \alveolata\rest\register( + $env['subject'], + \alveolata\http\enum_method::get, + '/foo/{id}', + [ + 'execution' => function ($version, $path_parameters, $headers, $input) use (&$env) { + $key = \intval($path_parameters['id']); + return [ + 'status_code' => 200, + 'data' => $env['data']['foo'][$key], + ]; + }, + 'title' => 'read foo', + 'description' => 'reads an element from the foo domain', + 'input_type' => (fn($version) => $type_description_key), + 'output_type' => (fn($version) => $type_description_value), + ] + ); + \alveolata\rest\register( + $env['subject'], + \alveolata\http\enum_method::put, + '/foo/{id}', + [ + 'execution' => function ($version, $path_parameters, $headers, $input) use (&$env) { + $key = \intval($path_parameters['id']); + $env['data'][$key] = $input; + return [ + 'status_code' => 204, + 'data' => null, + ]; + }, + 'title' => 'update foo', + 'description' => 'modifies an element of the foo domain', + 'input_type' => (fn($version) => $type_description_key), + 'output_type' => (fn($version) => $type_description_value), + ] + ); + \alveolata\rest\register( + $env['subject'], + \alveolata\http\enum_method::delete, + '/foo/{id}', + [ + 'execution' => function ($version, $path_parameters, $headers, $input) use (&$env) { + $key = \intval($path_parameters['id']); + unset($env['data'][$key]); + return [ + 'status_code' => 204, + 'data' => null, + ]; + }, + 'title' => 'delete foo', + 'description' => 'deletes an element from the foo domain', + 'input_type' => (fn($version) => ['kind' => 'null']), + 'output_type' => (fn($version) => ['kind' => 'null']), + ] + ); + }, + 'cases' => [ + [ + 'active' => true, + 'name' => 'list', + 'procedure' => function ($assert, $env) { + // setup + $env['reset_data'](); + + // execution + $http_request = new \alveolata\http\struct_request( + 'HTTP/1.1', + \alveolata\http\enum_method::get, + '/v1/foo', + [], + \json_encode(null) + ); + $http_response = \alveolata\rest\call( + $env['subject'], + $http_request + ); + + // assertions + $assert->equal($http_response->statuscode, 200); + $assert->equal($http_response->body, \json_encode([2,3,5,7])); + } + ], + [ + 'active' => true, + 'name' => 'create', + 'procedure' => function ($assert, $env) { + // setup + $env['reset_data'](); + + // execution + $http_request = new \alveolata\http\struct_request( + 'HTTP/1.1', + \alveolata\http\enum_method::post, + '/v1/foo', + [], + \json_encode('zweihundertundelf') + ); + $http_response = \alveolata\rest\call( + $env['subject'], + $http_request + ); + + // assertions + $assert->equal($http_response->statuscode, 201); + $assert->equal($http_response->body, \json_encode(211)); + } + ], + [ + 'active' => true, + 'name' => 'update', + 'procedure' => function ($assert, $env) { + // setup + $env['reset_data'](); + + // execution + $http_request = new \alveolata\http\struct_request( + 'HTTP/1.1', + \alveolata\http\enum_method::put, + '/v1/foo/3', + [], + \json_encode('three') + ); + $http_response = \alveolata\rest\call( + $env['subject'], + $http_request + ); + + // assertions + $assert->equal($http_response->statuscode, 204); + $assert->equal($http_response->body, \json_encode(null)); + } + ], + [ + 'active' => true, + 'name' => 'delete', + 'procedure' => function ($assert, $env) { + // setup + $env['reset_data'](); + + // execution + $http_request = new \alveolata\http\struct_request( + 'HTTP/1.1', + \alveolata\http\enum_method::delete, + '/v1/foo/3', + [], + \json_encode(null) + ); + $http_response = \alveolata\rest\call( + $env['subject'], + $http_request + ); + + // assertions + $assert->equal($http_response->statuscode, 204); + $assert->equal($http_response->body, \json_encode(null)); + } + ], + [ + 'active' => true, + 'name' => 'read', + 'procedure' => function ($assert, $env) { + // setup + $env['reset_data'](); + + // execution + $http_request = new \alveolata\http\struct_request( + 'HTTP/1.1', + \alveolata\http\enum_method::get, + '/v1/foo/3', + [], + \json_encode(null) + ); + $http_response = \alveolata\rest\call( + $env['subject'], + $http_request + ); + + // assertions + $assert->equal($http_response->statuscode, 200); + $assert->equal($http_response->body, \json_encode(['de' => 'drei', 'en' => null])); + } + ], + [ + 'active' => true, + 'name' => 'oas', + 'procedure' => function ($assert, $env) { + // exec + $spec = \alveolata\rest\to_oas( + $env['subject'], + [ + 'version' => 'v2', + 'servers' => [ + [ + 'url' => 'http://localhost:8888/', + ], + ] + ] + ); + + // assertions + // print(\json_encode($spec, \JSON_PRETTY_PRINT) . "\n"); + + $assert->equal( + \array_keys($spec['paths']), + [ + '/v2/foo', + '/v2/foo/{id}', + ] + ); + $assert->equal( + \array_keys($spec['paths']['/v2/foo']), + [ + 'get', + 'post', + ] + ); + $assert->equal( + \array_keys($spec['paths']['/v2/foo/{id}']), + [ + 'get', + 'put', + 'delete', + ] + ); + /* + $assert->equal( + \array_keys($spec['paths']['/v2/foo/{id}']['delete']['responses']), + [ + 204, + ] + ); + */ + }, + ], + ] + ], + ] + ] + ] + ] +); + + ?> diff --git a/lib/alveolata/rest/types.php b/lib/alveolata/rest/types.php new file mode 100644 index 0000000..1b19621 --- /dev/null +++ b/lib/alveolata/rest/types.php @@ -0,0 +1,125 @@ +} + */ + public array $operations; + + + /** + * @var ?array $sub_branch {(null | list<&struct_route_node>)} + */ + public ?array $sub_branch; + + + /** + * @var ?array $sub_wildcard {(null | record)} + */ + public ?array $sub_wildcard; + + + /** + * [constructor] + */ + public function __construct( + array $operations, + ?array $sub_branch, + ?array $sub_wildcard + ) + { + $this->operations = $operations; + $this->sub_branch = $sub_branch; + $this->sub_wildcard = $sub_wildcard; + } + +} + + +/** + */ +class struct_subject +{ + + /** + */ + public \alveolata\api\struct_subject $api_subject; + + + /** + */ + public struct_route_node $route_tree; + + + /** + */ + public string $versioning_method; + + + /** + */ + public ?string $versioning_header_name; + + + /** + */ + public ?string $versioning_query_key; + + + /** + */ + public \Closure $request_body_decode; + + + /** + */ + public string $request_body_mimetype; + + + /** + */ + public \Closure $response_body_encode; + + + /** + */ + public string $response_body_mimetype; + + + /** + */ + public function __construct( + \alveolata\api\struct_subject $api_subject, + struct_route_node $route_tree, + string $versioning_method, + ?string $versioning_header_name, + ?string $versioning_query_key, + \Closure $request_body_decode, + string $request_body_mimetype, + \Closure $response_body_encode, + string $response_body_mimetype + ) + { + $this->api_subject = $api_subject; + $this->route_tree = $route_tree; + $this->versioning_method = $versioning_method; + $this->versioning_header_name = $versioning_header_name; + $this->versioning_query_key = $versioning_query_key; + $this->request_body_decode = $request_body_decode; + $this->request_body_mimetype = $request_body_mimetype; + $this->response_body_encode = $response_body_encode; + $this->response_body_mimetype = $response_body_mimetype; + } + +} + + ?> diff --git a/lib/alveolata/server/functions.php b/lib/alveolata/server/functions.php new file mode 100644 index 0000000..ed5071d --- /dev/null +++ b/lib/alveolata/server/functions.php @@ -0,0 +1,276 @@ + + */ +class struct_subject/**/ +{ + + /** + * @var int + * @author Christian Fraß + */ + public $port; + + + /** + * @var function + * @author Christian Fraß + */ + public $handler; + + + /** + * whether a global handler for the operating systems interrupt signal (SIGINT) shall be installed for stopping the + * server gracfully + * + * @var boolean + * @author Christian Fraß + */ + public $install_killhandler; + + + /** + * procedure to be executed on start + * + * @var function,void> + * @author Christian Fraß + */ + public $on_starting; + + + /** + * procedure to be executed on stop + * + * @var function,void> + * @author Christian Fraß + */ + public $on_stopping; + + + /** + * procedure to be executed on start + * + * @var function,void> + * @author Christian Fraß + */ + public $on_started; + + + /** + * procedure to be executed on stop + * + * @var function,void> + * @author Christian Fraß + */ + public $on_stopped; + + + /** + * @var any + * @author Christian Fraß + */ + public $socket; + + + /** + * whether the server is supposed to shutdown + * + * @var boolean + * @author Christian Fraß + */ + public $run; + + + /** + * @author Christian Fraß + */ + public function __construct( + int $port, + \Closure $handler, + bool $install_killhandler, + \Closure $on_starting, + \Closure $on_stopping, + \Closure $on_started, + \Closure $on_stopped + ) + { + $this->port = $port; + $this->handler = $handler; + $this->install_killhandler = $install_killhandler; + $this->on_starting = $on_starting; + $this->on_stopping = $on_stopping; + $this->on_started = $on_started; + $this->on_stopped = $on_stopped; + $this->socket = null; + $this->run = false; + } + +} + + +/** + * @param int $port + * @param function $handler + * @param boolean $install_killhandler + * @param function,void> [$on_starting] + * @param function,void> [$on_stopping] + * @param function,void> [$on_started] + * @param function,void> [$on_stopped] + * @return struct_subject + * @author Christian Fraß + */ +function make( + int $port, + \Closure $handler, + bool $install_killhandler = false, + \Closure $on_starting = null, + \Closure $on_stopping = null, + \Closure $on_started = null, + \Closure $on_stopped = null +) : struct_subject +{ + if ($on_starting === null) { + $on_starting = function () { + \alveolata\log\info('starting server …'); + }; + } + if ($on_stopping === null) { + $on_stopping = function () { + \alveolata\log\info('stopping server …'); + }; + } + if ($on_started === null) { + $on_started = function () { + \alveolata\log\info('server started'); + }; + } + if ($on_stopped === null) { + $on_stopped = function () { + \alveolata\log\info('server stopped'); + }; + } + return ( + new struct_subject( + $port, + $handler, + $install_killhandler, + $on_starting, + $on_stopping, + $on_started, + $on_stopped + ) + ); +} + + +/** + * @param struct_subject $subject + * @author Christian Fraß + */ +function killhandler( + struct_subject/**/ $subject +) : void +{ + \alveolata\log\warning( + 'killhandler for server won\'t work' + ); + /* + \pcntl_signal( + SIGINT, + function ($signo) use (&$subject) {stop($subject);} + ); + */ +} + + +/** + * @param struct_subject $subject + * @author Christian Fraß + */ +function stop/**/( + struct_subject/**/ $subject +) : void +{ + ($subject->on_stopping)(); + if (! ($subject->socket === null)) { + \socket_shutdown($subject->socket); + } + $subject->run = false; +} + + +/** + * @param struct_subject $subject + * @author Christian Fraß + */ +function start/**/( + struct_subject/**/ $subject +) : void +{ + $conf = [ + 'host' => 'localhost', + 'max_connections' => 7, + 'buffer_length' => 2048, + ]; + ($subject->on_starting)(); + if ($subject->install_killhandler) { + killhandler($subject); + } + $subject->run = true; + $socket = \socket_create( + AF_INET, + SOCK_STREAM, + SOL_TCP + ); + // \socket_set_nonblock($socket); + \socket_bind( + $socket, + $conf['host'], + $subject->port + ); + \socket_listen( + $socket, + $conf['max_connections'] + ); + ($subject->on_started)(); + while ($subject->run) { + $subject->socket = $request = \socket_accept( + $socket + ); + /*if ($request === false) { + usleep(125000); + } + else {*/ + $input = \socket_read( + $request, + $conf['buffer_length'] + ); + $output = ($subject->handler)($input); + $bytes = \socket_write( + $request, + $output, + strlen($output) + ); + $successful = ($bytes !== false); + if (! $successful) { + \error_log('could not write to socket; shutting down server'); + break; + } + /*}*/ + } + \socket_close( + $request + ); + \socket_close( + $socket + ); + ($subject->on_stopped)(); +} + + ?> diff --git a/lib/alveolata/server/test.spec.php b/lib/alveolata/server/test.spec.php new file mode 100644 index 0000000..9cbed03 --- /dev/null +++ b/lib/alveolata/server/test.spec.php @@ -0,0 +1,46 @@ + 'alveolata', + 'sections' => [ + [ + 'name' => 'server', + 'sections' => [ + [ + 'active' => false, + 'name' => 'test', + 'cases' => [ + [ + 'name' => 'should be killable', + 'procedure' => function ($assert, &$environment) { + class thread1 extends \Thread { + public function __construct($server) {$this->server = $server;} + public function run() {$this->server->start();} + } + class thread2 extends \Thread { + public function __construct($server) {$this->server = $server;} + public function run() {sleep(1); $this->server->stop();} + } + $server = \alveolata\server\make( + 7777, + function () {}, + true + ); + (new thread1($server))->run(); + (new thread2($server))->run(); + }, + ], + ] + ], + ] + ] + ] + ] +); + + ?> diff --git a/lib/alveolata/server/wrapper-class.php b/lib/alveolata/server/wrapper-class.php new file mode 100644 index 0000000..c85b869 --- /dev/null +++ b/lib/alveolata/server/wrapper-class.php @@ -0,0 +1,55 @@ + + */ +class class_server/**/ { + + /* + * @var struct_server + */ + private $subject; + + + /** + * @var struct_server $subject + * @author Christian Fraß + */ + private function __construct( + struct_server/**/ $subject + ) + { + $this->subject = $subject; + } + + + /** + * @author Christian Fraß + */ + public static function make/**/( + int $port, + \Closure $handler, + bool $install_killhandler = false, + \Closure $on_starting = null, + \Closure $on_stopping = null, + \Closure $on_started = null, + \Closure $on_stopped = null + ) : class_server/**/ + { + $subject = make($port, $handler, $install_killhandler, $on_starting, $on_stopping, $on_started, $on_stopped); + return (new class_server($subject)); + } + + + /** + * @author Christian Fraß + */ + function start() : void {start($this->subject);} + function stop() : void {stop($this->subject);} + +} + + ?> diff --git a/lib/alveolata/session/functions.php b/lib/alveolata/session/functions.php new file mode 100644 index 0000000..1bc1495 --- /dev/null +++ b/lib/alveolata/session/functions.php @@ -0,0 +1,81 @@ + + */ +function begin( + string $kind, + array $parameters = [] +) : interface_session +{ + switch ($kind) { + default: { + throw (new \Exception(sprintf('invalid session kind "%s"', $kind))); + break; + } + case 'memory': { + return ( + implementation_memory::begin() + ); + break; + } + case 'file': { + return ( + implementation_file::begin() + ); + break; + } + case 'cgi': { + return ( + implementation_cgi::begin() + ); + break; + } + } +} + + +/** + * @author Christian Fraß + */ +function get( + string $kind, + string $id +) : interface_session +{ + switch ($kind) { + default: { + throw (new \Exception(sprintf('invalid session kind "%s"', $kind))); + break; + } + case 'memory': { + return ( + implementation_memory::get($id) + ); + break; + } + case 'file': { + return ( + implementation_file::get($id) + ); + break; + } + case 'cgi': { + return ( + implementation_cgi::get($id) + ); + break; + } + } +} + + ?> diff --git a/lib/alveolata/session/implementation-cgi.php b/lib/alveolata/session/implementation-cgi.php new file mode 100644 index 0000000..1a46fe1 --- /dev/null +++ b/lib/alveolata/session/implementation-cgi.php @@ -0,0 +1,195 @@ + + */ +class struct_subject_cgi { + + /** + * @var string + * @author Christian Fraß + */ + public $id; + + + /** + * @param string $id + * @author Christian Fraß + */ + public function __construct( + string $id + ) + { + $this->id = $id; + } + +} + + +/** + * @param struct_subject_cgi $subject + * @param string $key + * @throw \Exception if no such key + * @author Christian Fraß + */ +function _cgi_keycheck( + struct_subject_cgi $subject, + string $key +) : void +{ + // \session_id($subject->id); + if (! array_key_exists($key, $_SESSION)) { + $message = sprintf('no key "%s"', $key); + throw (new \Exception($message)); + } +} + + +/** + * @author Christian Fraß + */ +function cgi_set_dir( + string $path +) : void +{ + ini_set('session.save_path', $path); +} + + +/** + * @author Christian Fraß + */ +function cgi_begin( +) : struct_subject_cgi +{ + \session_start(); + $id = \session_id(); + $subject = new struct_subject_cgi($id); + return $subject; +} + + +/** + * @author Christian Fraß + */ +function cgi_get( + string $id +) : struct_subject_cgi +{ + $subject = (new struct_subject_cgi($id)); + \session_id($id); + \session_start(); + return $subject; +} + + +/** + * @author Christian Fraß + */ +function cgi_id( + struct_subject_cgi $subject +) : string +{ + return $subject->id; +} + + +/** + * @implementation + * @author Christian Fraß + */ +function cgi_read( + struct_subject_cgi $subject, + string $key +) : string +{ + _cgi_keycheck($subject, $key); + // \session_id($subject->id); + $value = $_SESSION[$key]; + return $value; +} + + +/** + * @implementation + * @author Christian Fraß + */ +function cgi_write( + struct_subject_cgi $subject, + string $key, + string $value +) : void +{ + // \session_id($subject->id); + $_SESSION[$key] = $value; +} + + +/** + * @implementation + * @author Christian Fraß + */ +function cgi_remove( + struct_subject_cgi $subject, + string $key +) : void +{ + _cgi_keycheck($subject, $key); + // \session_id($subject->id); + unset($_SESSION[$key]); +} + + +/** + * @implementation + * @author Christian Fraß + */ +function cgi_end( + struct_subject_cgi $subject +) : void +{ + // \session_id($subject->id); + \session_destroy(); +} + + +/** + * @author Christian Fraß + */ +class implementation_cgi implements interface_session { + + /** + * @var struct_subject_cgi + * @author Christian Fraß + */ + private $subject; + + + /** + * @author Christian Fraß + */ + private function __construct(struct_subject_cgi $subject) {$this->subject = $subject;} + + + /** + * implementations + * + * @author Christian Fraß + */ + public static function begin() : interface_session {return (new implementation_cgi(cgi_begin()));} + public static function get(string $id) : interface_session {return (new implementation_cgi(cgi_get($id)));} + public function id() : string {return cgi_id($this->subject);} + public function read(string $key) : string {return cgi_read($this->subject, $key);} + public function write(string $key, string $value) : void {cgi_write($this->subject, $key, $value);} + public function remove(string $key) : void {cgi_remove($this->subject, $key);} + public function end() : void {cgi_end($this->subject);} + +} + + ?> diff --git a/lib/alveolata/session/implementation-file.php b/lib/alveolata/session/implementation-file.php new file mode 100644 index 0000000..76ab4b3 --- /dev/null +++ b/lib/alveolata/session/implementation-file.php @@ -0,0 +1,260 @@ + + */ +class _state_file +{ + + /** + * @var string + * @author Christian Fraß + */ + public static $directory = '/tmp/sessions'; + + + /** + * @var int + * @author Christian Fraß + */ + public static $time_to_live_in_seconds = 3600; + +} + + +/** + * @author Christian Fraß + */ +class struct_subject_file { + + /** + * @var string + * @author Christian Fraß + */ + public $id; + + + /** + * @author Christian Fraß + */ + public function __construct( + string $id + ) + { + $this->id = $id; + } + +} + + +/** + * @author Christian Fraß + */ +function file_set_directory( + string $directory +) : void +{ + _state_file::$directory = $directory; +} + + +/** + * @author Christian Fraß + */ +function file_set_time_to_live_in_seconds( + int $time_to_live_in_seconds +) : void +{ + _state_file::$time_to_live_in_seconds = $time_to_live_in_seconds; +} + + +/** + * @param string $id + * @return string + * @author Christian Fraß + */ +function _file_get_path( + string $id +) : string +{ + return \alveolata\string\coin( + '{{directory}}/{{id}}.json', + [ + 'directory' => _state_file::$directory, + 'id' => $id, + ] + ); +} + + +/** + * @param struct_subject_file $subject + * @return map + * @author Christian Fraß + */ +function _file_get_data( + struct_subject_file $subject +) : array +{ + $path = _file_get_path($subject->id); + try { + $content = \alveolata\file\read($path); + $data = \alveolata\json\decode($content); + } + catch (\Exception $exception) { + $data = null; + } + if ($data === null) { + throw (new \Exception('session does not exist')); + } + else { + $now = time(); + if (($data['timestamp'] + _state_file::$time_to_live_in_seconds) < $now) { + throw (new \Exception('session timed out')); + } + else { + return $data['data']; + } + } +} + + +/** + * @return struct_subject_file + * @author Christian Fraß + */ +function file_begin( +) : struct_subject_file +{ + $id = hash('sha256', strval(time())); + $subject = (new struct_subject_file($id)); + $path = _file_get_path($id); + $data = [ + 'timestamp' => time(), + 'data' => [], + ]; + $content = \alveolata\json\encode($data); + \alveolata\file\write($path, $content, true); + return $subject; +} + + +/** + * @param string $id + * @return struct_subject_file + * @author Christian Fraß + */ +function file_get( + string $id +) : struct_subject_file +{ + $subject = (new struct_subject_file($id)); + return $subject; +} + + +/** + * @param string $id + * @return struct_subject_file + * @author Christian Fraß + */ +function file_id( + struct_subject_file $subject +) : string +{ + return $subject->id; +} + + +/** + * @param struct_subject_file $subject + * @return string + * @author Christian Fraß + */ +function file_read( + struct_subject_file $subject, + string $key +) : string +{ + $data = _file_get_data($subject); + return $data[$key]; +} + + +/** + * @param struct_subject_file $subject + * @param string $key + * @return string $value + * @author Christian Fraß + */ +function file_write( + struct_subject_file $subject, + string $key, + string $value +) : void +{ + $data = _file_get_data($subject); + $data['timestamp'] = time(); + $data['data'][$key] = $value; + $content = \alveolata\json\encode($data); + $path = _file_get_path($subject->id); + \alveolata\file\write($path, $content, true); +} + + +/** + * @param struct_subject_file $subject + * @author Christian Fraß + */ +function file_end( + struct_subject_file $subject +) : void +{ + $path = _file_get_path($subject->id); + \alveolata\file\remove($path); +} + + +/** + * @author Christian Fraß + */ +class implementation_file implements interface_session { + + /** + * @var struct_subject_file + * @author Christian Fraß + */ + private $subject; + + + /** + * @author Christian Fraß + */ + private function __construct(struct_subject_file $subject) {$this->subject = $subject;} + + + /** + * implementations + * + * @author Christian Fraß + */ + public static function begin() : interface_session {return (new implementation_file(file_begin()));} + public static function get(string $id) : interface_session {return (new implementation_file(file_get($id)));} + public function id() : string {return file_id($this->subject);} + public function read(string $key) : string {return file_read($this->subject, $key);} + public function write(string $key, string $value) : void {file_write($this->subject, $key, $value);} + public function remove(string $key) : void {file_remove($this->subject, $key);} + public function end() : void {file_end($this->subject);} + +} + + ?> diff --git a/lib/alveolata/session/implementation-memory.php b/lib/alveolata/session/implementation-memory.php new file mode 100644 index 0000000..883c8b5 --- /dev/null +++ b/lib/alveolata/session/implementation-memory.php @@ -0,0 +1,255 @@ + + */ +class struct_subject_memory { + + /** + * @var string + * @author Christian Fraß + */ + public $id; + + + /** + * @var integar + * @author Christian Fraß + */ + public $timestamp; + + + /** + * @var map + * @author Christian Fraß + */ + public $data; + + + /** + * @author Christian Fraß + */ + public function __construct( + string $id, + int $timestamp = -1, + array $data = [] + ) + { + if ($timestamp < 0) $timestamp = time(); + $this->id = $id; + $this->timestamp = $timestamp; + $this->data = $data; + } + +} + + +/** + * @author Christian Fraß + */ +class _state_memory { + + /** + * @var map + * @author Christian Fraß + */ + public static $pool = []; + + + /** + * @var int + * @author Christian Fraß + */ + public static $time_to_live_in_seconds = 3600; // ~1h + +} + + +/** + * @throw \Exception if timed out + * @author Christian Fraß + */ +function _memory_ttlcheck( + struct_subject_memory $subject +) : void +{ + $now = time(); + if (($subject->timestamp + _state_memory::$time_to_live_in_seconds) < $now) { + throw (new \Exception('session timed out')); + } +} + + +/** + * @param struct_subject_memory $subject + * @param string $key + * @throw \Exception if no such key + * @author Christian Fraß + */ +function _memory_keycheck( + struct_subject_memory $subject, + string $key +) : void +{ + if (! array_key_exists($key, $subject->data)) { + $message = sprintf('no key "%s"', $key); + throw (new \Exception($message)); + } +} + + +/** + * @param struct_subject_memory $subject + * @author Christian Fraß + */ +function _memory_update( + struct_subject_memory $subject +) : void +{ + $subject->timestamp = time(); +} + + +/** + * @author Christian Fraß + */ +function memory_begin( +) : struct_subject_memory +{ + $id = hash('sha256', strval(time())); + $subject = (new struct_subject_memory($id)); + _state_memory::$pool[$id] = $subject; + return $subject; +} + + +/** + * @author Christian Fraß + */ +function memory_get( + string $id +) : struct_subject_memory +{ + if (! array_key_exists($id, _state_memory::$pool)) { + $message = sprintf('no session "%s"', $id); + throw (new \Exception($message)); + } + else { + $subject = _state_memory::$pool[$id]; + _memory_ttlcheck($subject); + return $subject; + } +} + + +/** + * @author Christian Fraß + */ +function memory_id( + struct_subject_memory $subject +) : string +{ + return $subject->id; +} + + +/** + * @author Christian Fraß + */ +function memory_read( + struct_subject_memory $subject, + string $key +) : string +{ + _memory_ttlcheck($subject); + _memory_keycheck($subject, $key); + $value = $subject->data[$key]; + _memory_update($subject); + return $value; +} + + +/** + * @author Christian Fraß + */ +function memory_write( + struct_subject_memory $subject, + string $key, + string $value +) : void +{ + _memory_ttlcheck($subject); + $subject->data[$key] = $value; + _memory_update($subject); +} + + +/** + * @author Christian Fraß + */ +function memory_remove( + struct_subject_memory $subject, + string $key +) : void +{ + _memory_ttlcheck($subject); + _memory_keycheck($subject, $key); + unset($subject->data[$key]); + _memory_update($subject); +} + + +/** + * @author Christian Fraß + * @todo check for existence and ttl + */ +function memory_end( + struct_subject_memory $subject +) : void +{ + $subject->id = 0; + $subject->timestamp = 0; + $subject->data = []; + unset(_state_memory::$pool[$subject->id]); +} + + +/** + * @author Christian Fraß + */ +class implementation_memory implements interface_session { + + /** + * @var struct_subject_memory + * @author Christian Fraß + */ + private $subject; + + + /** + * @author Christian Fraß + */ + private function __construct(struct_subject_memory $subject) {$this->subject = $subject;} + + + /** + * implementations + * + * @author Christian Fraß + */ + public static function begin() : interface_session {return (new implementation_memory(memory_begin()));} + public static function get(string $id) : interface_session {return (new implementation_memory(memory_get($id)));} + public function id() : string {return memory_id($this->subject);} + public function read(string $key) : string {return memory_read($this->subject, $key);} + public function write(string $key, string $value) : void {memory_write($this->subject, $key, $value);} + public function remove(string $key) : void {memory_remove($this->subject, $key);} + public function end() : void {memory_end($this->subject);} + +} + + ?> diff --git a/lib/alveolata/session/interface.php b/lib/alveolata/session/interface.php new file mode 100644 index 0000000..f0bc18c --- /dev/null +++ b/lib/alveolata/session/interface.php @@ -0,0 +1,91 @@ + + * @todo consider to allow "any" keys and offer an option to specify the encoder and decoder + */ +interface interface_session { + + /** + * shall create a new session instance, i.e. starting a new session + * + * @return interface_session + */ + public static function begin( + ) : interface_session + ; + + + /** + * shall recover the session instance by a given ID + * + * @param string $id + * @return interface_session + */ + public static function get( + string $id + ) : interface_session + ; + + + /** + * shall return the generated ID, which has been assigned to the session + * + * @return string + */ + public function id( + ) : string + ; + + + /** + * shall read a specific value from the session data + * + * @param string $key + * @return string + */ + public function read( + string $key + ) : string + ; + + + /** + * shall write a specific value to the session data + * + * @param string $key + * @param string $value + */ + public function write( + string $key, + string $value + ) : void + ; + + + /** + * shall remove a specific value from the session data + * @param string $key + */ + public function remove( + string $key + ) : void + ; + + + /** + * shall terminate the session + * + */ + public function end( + ) : void + ; + +} + + ?> diff --git a/lib/alveolata/sql/functions.php b/lib/alveolata/sql/functions.php new file mode 100644 index 0000000..b0d18b8 --- /dev/null +++ b/lib/alveolata/sql/functions.php @@ -0,0 +1,150 @@ +} + * @return string + * @author Christian Fraß + */ +function conjunction( + array $expressions +) : string +{ + return ( + implode( + ' AND ', + array_map( + function (string $expression) : string { + return sprintf('(%s)', $expression); + }, + $expressions + ) + ) + ); +} + + +/** + * @param array $expressions {list} + * @return string + * @author Christian Fraß + */ +function disjunction( + array $expressions +) : string +{ + return ( + implode( + ' OR ', + array_map( + function (string $expression) : string { + return sprintf('(%s)', $expression); + }, + $expressions + ) + ) + ); +} + + +/** + * expression to check if two sets (~ 1 column tables) are equal (in their elements, not in their order) + * warning: won't work for empty sets + * + * @author Christian Fraß + */ +function set_equal( + string $set1, + string $set2 +) : string +{ + // (x \ y) ∪ (y \ x) = {} + return sprintf( + '(NOT EXISTS (SELECT value FROM (%s) WHERE NOT (value IN (%s)) UNION SELECT value FROM (%s) WHERE NOT (value IN (%s))))', + $set1, + $set2, + $set2, + $set1 + ); +} + + +/** + * @param mixed $value + * @param \Closure $escape + * @return string + * @author Christian Fraß + */ +function format( + $value, + \Closure $escape = null +) : string +{ + if ($escape === null) { + $escape = ( + function ($x) { + $replacements = [ + '\'' => '\'\'', + ';' => '\\;', + ]; + $y = $x; + foreach ($replacements as $from => $to) { + $y = str_replace($from, $to, $y); + } + return $y; + } + ); + } + if ($value === null) { + return 'NULL'; + } + else { + $type = gettype($value); + switch ($type) { + case 'boolean': { + return ($value ? 'TRUE' : 'FALSE'); + break; + } + case 'integer': { + return sprintf('%d', $value); + break; + } + case 'float': + case 'double': { + return sprintf('%.4f', $value); + break; + } + case 'string': { + return sprintf('\'%s\'', $escape($value)); + break; + } + case 'array': { + return sprintf( + '(%s)', + implode( + ',', + array_map( + function ($element) : string { + return format($element); + }, + ( + empty($value) + ? ['___DUMMY_VALUE___'] + : $value + ) + ) + ) + ); + } + default: { + throw (new \Exception(sprintf('unhandled type "%s" of value %s', $type, json_encode($value)))); + break; + } + } + } +} + diff --git a/lib/alveolata/sql/test.spec.json b/lib/alveolata/sql/test.spec.json new file mode 100644 index 0000000..8a4a273 --- /dev/null +++ b/lib/alveolata/sql/test.spec.json @@ -0,0 +1,103 @@ +{ + "active": true, + "sections": [ + { + "name": "format", + "active": true, + "execution": { + "call": "\\alveolata\\sql\\format({{value}})" + }, + "cases": [ + { + "name": "null", + "active": true, + "input": { + "value": null + }, + "output": { + "kind": "regular", + "value": "NULL" + } + }, + { + "name": "int", + "active": true, + "input": { + "value": 42 + }, + "output": { + "kind": "regular", + "value": "42" + } + }, + { + "name": "float", + "active": true, + "input": { + "value": 2.718218 + }, + "output": { + "kind": "regular", + "value": "2.7182" + } + }, + { + "name": "string_without_quotes", + "active": true, + "input": { + "value": "foo" + }, + "output": { + "kind": "regular", + "value": "'foo'" + } + }, + { + "name": "string_with_quotes", + "active": true, + "input": { + "value": "f'o" + }, + "output": { + "kind": "regular", + "value": "'f''o'" + } + }, + { + "name": "string_with_semicolon", + "active": true, + "input": { + "value": "f;o" + }, + "output": { + "kind": "regular", + "value": "'f\\;o'" + } + }, + { + "name": "array_empty", + "active": true, + "input": { + "value": [] + }, + "output": { + "kind": "regular", + "value": "('___DUMMY_VALUE___')" + } + }, + { + "name": "array_nonempty", + "active": true, + "input": { + "value": [2,3,5] + }, + "output": { + "kind": "regular", + "value": "(2,3,5)" + } + } + ] + } + ] +} + diff --git a/lib/alveolata/storage/abstract/interface.php b/lib/alveolata/storage/abstract/interface.php new file mode 100644 index 0000000..5bf1a98 --- /dev/null +++ b/lib/alveolata/storage/abstract/interface.php @@ -0,0 +1,74 @@ + + */ +interface interface_storage/**/ +{ + + // type_key ~ string + // type_value ~ map + + + /** + * @param type_value $value + * @return type_key key + * @author Christian Fraß + */ + function create( + /*type_value */$value + )/* : type_key*/ + ; + + + /** + * @param type_key $key + * @param type_value $value + * @author Christian Fraß + */ + function update( + /*type_key */$key, + /*type_value */$value + ) : void + ; + + + /** + * @param type_key $key + * @author Christian Fraß + */ + function delete( + /*type_key */$key + ) : void + ; + + + /** + * @param type_key $key + * @return type_value + * @author Christian Fraß + */ + function read( + /*type_key*/ $key + )/* : type_value*/ + ; + + + /** + * @param map $parameters + * @return list + * @author Christian Fraß + */ + function search( + array $parameters + ) : array + ; + +} + + ?> diff --git a/lib/alveolata/storage/implementation-sqltable/functions.php b/lib/alveolata/storage/implementation-sqltable/functions.php new file mode 100644 index 0000000..c3d01b4 --- /dev/null +++ b/lib/alveolata/storage/implementation-sqltable/functions.php @@ -0,0 +1,418 @@ + + */ +class struct_sqltable +{ + + /** + * @var \alveolata\database\interface_database + * @author Christian Fraß + */ + public $database; + + + /** + * @var string + * @author Christian Fraß + */ + public $name; + + + /** + * @var list> + * @author Christian Fraß + */ + public $fields; + + + /** + * @var \alveolata\observer\class_observer + */ + public $observer_teardown; + + + /** + * @var \alveolata\observer\class_observer + */ + public $observer_delete; + + + /** + * @author Christian Fraß + */ + public function __construct( + \alveolata\database\interface_database $database, + string $name, + array $fields + ) + { + $this->database = $database; + $this->name = $name; + $this->fields = $fields; + + $this->observer_teardown = \alveolata\observer\class_observer::make(); + $this->observer_delete = \alveolata\observer\class_observer::make(); + } + +} + + +/** + * @author Christian Fraß + */ +function sqltable_make( + \alveolata\database\interface_database $database, + string $name, + array $fields = UNSET_ARRAY +) : struct_sqltable +{ + return ( + new struct_sqltable( + $database, + $name, + $fields + ) + ); +} + + +/** + * @author Christian Fraß + */ +function sqltable_hook_delete( + struct_sqltable $subject, + \Closure $procedure +) : string +{ + return $subject->observer_delete->register($procedure); +} + + +/** + * @author Christian Fraß + */ +function sqltable_teardown( + struct_sqltable $subject +) : void +{ + $subject->observer_teardown->notify(null); + $template = \alveolata\string\coin( + 'DROP TABLE IF EXISTS `{{tablename}}`', + [ + 'tablename' => $subject->name, + ] + ); + $subject->database->query( + $template + ); +} + + +/** + * @author Christian Fraß + */ +function sqltable_setup( + struct_sqltable $subject +) : void +{ + if ($subject->fields === UNSET_ARRAY) { + $report = \alveolata\report\make( + 'SQL table setup not possible since fields have not been specified', + [ + 'tablename' => $subject->name, + ] + ); + throw (\alveolata\report\as_exception($report)); + } + else { + $arguments = []; + $fielddescriptions = []; + // id + { + $fielddescription = \alveolata\string\coin( + '`id` {{definition}}', + [ + 'definition' => $subject->database->boilerplate_field_definition_for_integer_primary_key_with_auto_increment(), + ] + ); + array_push($fielddescriptions, $fielddescription); + } + // regular fields + { + } + foreach ($subject->fields as $field) { + $key = \alveolata\string\coin( + 'defaultvalue_{{columnname}}', + [ + 'columnname' => $field['name'], + ] + ); + $fielddescription = \alveolata\string\coin( + '`{{columnname}}` {{type}} {{nullability}} DEFAULT {{defaultvalue}}', + [ + 'columnname' => $field['name'], + 'type' => $field['type'], + 'nullability' => ($field['null_allowed'] ? 'NULL' : 'NOT NULL'), + // 'defaultvalue' => $field['default'], + 'defaultvalue' => sprintf(':%s', $key), + ] + ); + array_push($fielddescriptions, $fielddescription); + $value = ($field['default'] ?? 'NULL'); + $arguments[$key] = $value; + } + $template = \alveolata\string\coin( + 'CREATE TABLE IF NOT EXISTS `{{tablename}}`({{fields}});', + [ + 'tablename' => $subject->name, + 'fields' => implode(', ', $fielddescriptions), + ] + ); + $subject->database->query( + $template, + $arguments + ); + } +} + + +/** + * @author Christian Fraß + */ +function sqltable_create( + struct_sqltable $subject, + /*map */$value +)/* : int*/ +{ + $fields = array_keys($value); + $template = \alveolata\string\coin( + 'INSERT INTO `{{tablename}}`({{fields}}) VALUES ({{values}})', + [ + 'tablename' => $subject->name, + 'fields' => implode( + ',', + \alveolata\list_\map( + $fields, + function ($field) { + return \alveolata\string\coin('`{{field}}`', ['field' => $field]); + } + ) + ), + 'values' => implode( + ',', + \alveolata\list_\map( + $fields, + function ($field) { + return \alveolata\string\coin(':{{field}}', ['field' => $field]); + } + ) + ), + ] + ); + $arguments = $value; + $result = $subject->database->query( + $template, + $arguments + ); + return $result['id']; +} + + +/** + * @author Christian Fraß + */ +function sqltable_update( + struct_sqltable $subject, + /*int */$key, + /*map */$value +) : void +{ + $fields = array_keys($value); + if (empty($fields)) { + } + else { + $template = \alveolata\string\coin( + 'UPDATE `{{tablename}}` SET {{sets}} WHERE (`id` = :id)', + [ + 'tablename' => $subject->name, + 'sets' => implode( + ',', + \alveolata\list_\map( + $fields, + function ($field) { + return \alveolata\string\coin('`{{field}}` = :{{field}}', ['field' => $field]); + } + ) + ), + ] + ); + $arguments = array_merge( + [ + 'id' => $key, + ], + $value, + ); + $result = $subject->database->query( + $template, + $arguments + ); + /* + if (! ($result['affected'] === 1)) { + $report = \alveolata\report\make( + 'could not update dataset', + [ + 'tablename' => $subject->name, + 'key' => $key + ] + ); + throw (\alveolata\report\as_exception($report)); + } + else { + // do nothing + } + */ + } +} + + +/** + * @author Christian Fraß + */ +function sqltable_delete( + struct_sqltable $subject, + /*int */$key +) : void +{ + $subject->observer_delete->notify($key); + $template = \alveolata\string\coin( + 'DELETE FROM `{{tablename}}` WHERE (`id` = :id)', + [ + 'tablename' => $subject->name, + ] + ); + $arguments = [ + 'id' => $key, + ]; + $result = $subject->database->query( + $template, + $arguments + ); + if (! ($result['affected'] === 1)) { + $report = \alveolata\report\make( + 'could not delete dataset', + [ + 'tablename' => $subject->name, + 'key' => $key + ] + ); + throw (\alveolata\report\as_exception($report)); + } + else { + // do nothing + } +} + + +/** + * @author Christian Fraß + */ +function sqltable_read( + struct_sqltable $subject, + /*int */$key +)/* : map*/ +{ + $template = \alveolata\string\coin( + 'SELECT * FROM `{{tablename}}` WHERE (`id` = :id)', + [ + 'tablename' => $subject->name, + ] + ); + $result = $subject->database->query( + $template, + [ + 'id' => $key + ] + ); + if (count($result['rows']) !== 1) { + $report = \alveolata\report\make( + 'none or ambiguous result on read', + [ + 'tablename' => $subject->name, + 'id' => $key, + ] + ); + throw (\alveolata\report\as_exception($report)); + } + else { + $row = $result['rows'][0]; + unset($row['id']); + return $row; + } +} + + +/** + * @author Christian Fraß + */ +function sqltable_search( + struct_sqltable $subject, + array $parameters = [] +) : array +{ + $condition = ( + (count($parameters) === 0) + // ? 'TRUE' + ? '(0 = 0)' + : \alveolata\string\coin( + '({{clauses}})', + [ + 'clauses' => \alveolata\sql\conjunction( + \alveolata\list_\map( + array_keys($parameters), + function ($key) { + return \alveolata\string\coin( + '`{{key}}` = :{{key}}', + [ + 'key' => $key, + ] + ); + } + ) + ), + ] + ) + ); + $template = \alveolata\string\coin( + 'SELECT `id` FROM `{{tablename}}` WHERE {{condition}}', + [ + 'tablename' => $subject->name, + 'condition' => $condition, + ] + ); + $arguments = $parameters; + $result = $subject->database->query( + $template, + $arguments + ); + return \alveolata\list_\map( + $result['rows'], + function ($row) { + return $row['id']; + } + ); +} + + ?> diff --git a/lib/alveolata/storage/implementation-sqltable/test.spec.php b/lib/alveolata/storage/implementation-sqltable/test.spec.php new file mode 100644 index 0000000..43605b5 --- /dev/null +++ b/lib/alveolata/storage/implementation-sqltable/test.spec.php @@ -0,0 +1,161 @@ + 'alveolata', + 'sections' => [ + [ + 'name' => 'storage', + 'sections' => [ + [ + 'name' => 'sqltable', + 'setup' => function (&$environment) { + $environment['path_database'] = '/tmp/localdb.sqlite'; + exec(sprintf('echo "" > %s', $environment['path_database'])); + $database = \alveolata\database\make( + 'sqlite', + [ + 'path' => $environment['path_database'], + ] + ); + $tablename = '_testtable_'; + $fields = [ + [ + 'name' => 'name', + 'type' => 'VARCHAR(64)', + 'null_allowed' => false, + 'default' => '', + ], + ]; + $environment['storage'] = \alveolata\storage\implementation_sqltable::make( + $database, + $tablename, + $fields + ); + $environment['storage']->setup(); + }, + 'sections' => [ + [ + 'name' => 'create_delete', + 'cases' => [ + [ + 'name' => 'test', + 'procedure' => function ($assert, $environment) { + // constants + $value = [ + 'name' => 'foox', + ]; + + // execution & assertions + $assert->runs( + function () use (&$environment, &$value) { + $key = $environment['storage']->create($value); + $environment['storage']->delete($key); + } + ); + } + ], + ] + ], + [ + 'name' => 'read', + 'cases' => [ + [ + 'name' => 'test', + 'procedure' => function ($assert, $environment) { + // constants + $value = [ + 'name' => 'foox', + ]; + + // setup + $key = $environment['storage']->create($value); + + // execution + $value_read = $environment['storage']->read($key); + + // assertions + $assert->equal($value_read, $value); + + // cleanup + $environment['storage']->delete($key); + } + ], + ] + ], + [ + 'name' => 'search', + 'cases' => [ + [ + 'name' => 'test', + 'procedure' => function ($assert, $environment) { + // constants + $value = [ + 'name' => 'foox', + ]; + + // setup + $key = $environment['storage']->create($value); + + // execution + $keys = $environment['storage']->search($value); + + // assertions + $assert->equal($keys, [$key]); + + // cleanup + $environment['storage']->delete($key); + } + ], + ] + ], + [ + 'name' => 'update', + 'cases' => [ + [ + 'name' => 'test', + 'procedure' => function ($assert, $environment) { + // constants + $value1 = [ + 'name' => 'foox', + ]; + $value2 = [ + 'name' => 'barx', + ]; + + // setup + $key = $environment['storage']->create($value1); + + // execution + $environment['storage']->update($key, $value2); + $value_read = $environment['storage']->read($key); + + // assertions + $assert->equal($value_read, $value2); + + // cleanup + $environment['storage']->delete($key); + } + ], + ] + ], + ], + 'cleanup' => function (&$environment) { + $environment['storage']->teardown(); + exec(sprintf('rm -f %s', $environment['path_database'])); + } + ] + ] + ], + ] + ] +); + diff --git a/lib/alveolata/storage/implementation-sqltable/wrapper-class.php b/lib/alveolata/storage/implementation-sqltable/wrapper-class.php new file mode 100644 index 0000000..861d9ff --- /dev/null +++ b/lib/alveolata/storage/implementation-sqltable/wrapper-class.php @@ -0,0 +1,65 @@ + + */ +class implementation_sqltable implements interface_storage/**/ { + + /** + * @var struct_sqltable $subject + * @author Christian Fraß + */ + private $subject; + + + /** + * @param struct_sqltable $subject + * @author Christian Fraß + */ + private function __construct(struct_sqltable $subject) {$this->subject = $subject;} + + + /** + * @author Christian Fraß + */ + public function get_subject() {return $this->subject;} + + + /** + * @return implementation_sqltable + * @author Christian Fraß + */ + public static function make( + \alveolata\database\interface_database $database, + string $name, + array $fields = UNSET_ARRAY + ) : implementation_sqltable + { + $subject = sqltable_make($database, $name, $fields); + return (new implementation_sqltable($subject)); + } + + + /** + * implementations + * + * @author Christian Fraß + */ + public function teardown() : void {sqltable_teardown($this->subject);} + public function setup() : void {sqltable_setup($this->subject);} + public function create($value) {return sqltable_create($this->subject, $value);} + public function update($key, $value) : void {sqltable_update($this->subject, $key, $value);} + public function delete($key) : void {sqltable_delete($this->subject, $key);} + public function read($key) {return sqltable_read($this->subject, $key);} + public function search(array $parameters) : array {return sqltable_search($this->subject, $parameters);} + +} + +?> diff --git a/lib/alveolata/storage/implementation-sqltablecluster/functions.php b/lib/alveolata/storage/implementation-sqltablecluster/functions.php new file mode 100644 index 0000000..2952a2e --- /dev/null +++ b/lib/alveolata/storage/implementation-sqltablecluster/functions.php @@ -0,0 +1,535 @@ +>" in a relational + * database, one would create a core table for the primitive record fields (foo) and separate tables for all + * non-primitive fields (bar), which are linked to the core table + * + * @template type_value + * @template type_element + * @author Christian Fraß + */ +class struct_sqltablecluster_tight_supplement +{ + + /** + * @var struct_sqltable + */ + public $target_table; + + + /** + * @var string + */ + public $core_id_column; + + + /** + * @var boolean + */ + public $include_own_id; + + + /** + * @var boolean + */ + public $exclude_core_id; + + + /** + */ + public function __construct( + struct_sqltable $target_table, + string $core_id_column, + bool $include_own_id, + bool $exclude_core_id + ) + { + $this->target_table = $target_table; + $this->core_id_column = $core_id_column; + $this->include_own_id = $include_own_id; + $this->exclude_core_id = $exclude_core_id; + } + +} + + +/** + * exemplary structure: + * »CREATE TABLE core(id);« + * »CREATE TABLE target(id);« + * »CREATE TABLE edge(core_id,target_id);« + * + * typical use case: domain B depends on domain A; elements of A can exist in ignorance of B + * + * + * @template type_value + * @template type_thing + * @author Christian Fraß + */ +class struct_sqltablecluster_loose_supplement +{ + + /** + * @var struct_sqltable + * + * will only be used to add a deletion hook + */ + public $target_table; + + + /** + * @var struct_sqltable + */ + public $edge_table; + + + /** + * @var string + * + * name of the column in the edge table, which references the id in the core table + */ + public $core_id_column; + + + /** + * @var string + * + * name of the column in the edge table, which references the id in the target table + */ + public $target_id_column; + + + /** + */ + public function __construct( + struct_sqltable $target_table, + struct_sqltable $edge_table, + string $core_id_column, + string $target_id_column + ) + { + $this->target_table = $target_table; + $this->edge_table = $edge_table; + $this->core_id_column = $core_id_column; + $this->target_id_column = $target_id_column; + } + +} + + +/** + * @author Christian Fraß + */ +class struct_sqltablecluster +{ + + /** + * @var struct_sqltable + */ + public $core; + + + /** + * @var array {map>} + */ + public $tight_supplements; + + + /** + * @var array {map>} + */ + public $loose_supplements; + + + /** + * @var \Closure { + * function< + * record< + * core_row:map, + * tight_supplement_values:map>, + * loose_supplement_values:map> + * >, + * type_value + * > + * } + */ + public $assemble; + + + /** + * @var \Closure { + * function< + * type_value, + * record< + * core_row:map, + * tight_supplement_values:map>, + * loose_supplement_values:map> + * > + * > + * } + */ + public $disperse; + + + /** + * @var \alveolata\observer\class_observer + */ + public $observer_delete; + + + /** + */ + public function __construct( + struct_sqltable $core, + array $tight_supplements, + array $loose_supplements, + \Closure $assemble, + \Closure $disperse + ) + { + $this->core = $core; + $this->tight_supplements = $tight_supplements; + $this->loose_supplements = $loose_supplements; + $this->assemble = $assemble; + $this->disperse = $disperse; + + $this->observer_delete = \alveolata\observer\class_observer::make(); + } + +} + + +/** + * @author Christian Fraß + */ +function sqltablecluster_make( + struct_sqltable $core, + array $tight_supplements, + array $loose_supplements, + \Closure $assemble, + \Closure $disperse +) : struct_sqltablecluster +{ + $subject = ( + new struct_sqltablecluster( + $core, + $tight_supplements, + $loose_supplements, + $assemble, + $disperse + ) + ); + // hooks + { + // remove corresponding edges, when a supplemental dataset is deleted + { + foreach ($subject->loose_supplements as $loose_supplement_name => $loose_supplement) { + sqltable_hook_delete( + $loose_supplement->target_table, + function ($target_id) use ($loose_supplement) : void { + $loose_supplement_ids = sqltable_search( + $loose_supplement->edge_table, + [$loose_supplement->target_id_column => $target_id] + ); + \alveolata\list_\map/**/( + $loose_supplement_ids, + function (int $loose_supplement_id) use ($loose_supplement) : void { + sqltable_delete($loose_supplement->edge_table, $loose_supplement_id); + } + ); + } + ); + } + } + } + return $subject; +} + + +/** + * @author Christian Fraß + */ +function sqltablecluster_hook_delete( + struct_sqltable $subject, + \Closure $procedure +) : string +{ + return $subject->observer_delete->register($procedure); +} + + +/** + * @author Christian Fraß + */ +function sqltablecluster_create( + struct_sqltablecluster $subject, + /*map */$value +)/* : int*/ +{ + $dispersal = ($subject->disperse)($value); + $core_row = $dispersal['core_row']; + // core + { + $core_id = sqltable_create($subject->core, $core_row); + } + // tight supplements + { + $tight_supplement_ids = \alveolata\map\map/**/( + $subject->tight_supplements, + function ($tight_supplement, $tight_supplement_name) use ($dispersal, $core_id) { + return \alveolata\list_\map( + $dispersal['tight_supplement_values'][$tight_supplement_name], + function ($tight_supplement_row) use ($core_id, $tight_supplement) { + $tight_supplement_row[$tight_supplement->core_id_column] = $core_id; + return sqltable_create($tight_supplement->target_table, $tight_supplement_row); + } + ); + } + ); + } + // loose supplements + { + $loose_supplement_ids = \alveolata\map\map/**/( + $subject->loose_supplements, + function ($loose_supplement, $loose_supplement_name) use ($dispersal, $core_id) { + return \alveolata\list_\map( + $dispersal['loose_supplement_values'][$loose_supplement_name], + function (int $target_id) use ($core_id, $loose_supplement) { + $loose_supplement_row = [ + $loose_supplement->core_id_column => $core_id, + $loose_supplement->target_id_column => $target_id, + ]; + return sqltable_create($loose_supplement->edge_table, $loose_supplement_row); + } + ); + } + ); + } + $key = $core_id; + return $key; +} + + +/** + * @author Christian Fraß + * @todo improve update of satellites + */ +function sqltablecluster_update( + struct_sqltablecluster $subject, + /*int */$key, + /*map */$value +) : void +{ + $core_id = $key; + $dispersal = ($subject->disperse)($value); + $core_row = $dispersal['core_row']; + // core + { + sqltale_update($subject->core, $core_id, $core_row); + } + // tight supplements + { + \alveolata\map\map( + $subject->tight_supplements, + function ($tight_supplement, $tight_supplement_name) use ($core_id, $value, $dispersal) { + // remove old + { + $tight_supplement_ids_old = sqltable_search($tight_supplement->target_table, [$tight_supplement->core_id_column => $core_id]); + \alveolata\list_\map/**/( + $tight_supplement_ids_old, + function (int $tight_supplement_id_old) use ($tight_supplement) : void { + sqltable_delete($tight_supplement->target_table, $tight_supplement_id_old); + } + ); + } + // insert new + { + $tight_supplement_ids_new = \alveolata\list_\map( + $dispersal['tight_supplement_values'][$tight_supplement_name], + function ($tight_supplement_row) use ($core_id, $tight_supplement) { + $tight_supplement_row[$tight_supplement->core_id_column] = $core_id; + return sqltable_create($tight_supplement->target_table, $tight_supplement_row); + } + ); + } + } + ); + } + // loose supplements + { + \alveolata\map\map( + $subject->loose_supplements, + function ($loose_supplement, $loose_supplement_name) use ($core_id, $value, $dispersal) { + // remove old + { + $loose_supplement_ids_old = sqltable_search($loose_supplement->edge_table, [$loose_supplement->core_id_column => $core_id]); + \alveolata\list_\map/**/( + $loose_supplement_ids_old, + function (int $loose_supplement_id_old) use ($loose_supplement) : void { + sqltable_delete($loose_supplement->edge_table, $loose_supplement_id_old); + } + ); + } + // insert new + { + $loose_supplement_ids_new = \alveolata\list_\map( + $dispersal['loose_supplement_values'][$loose_supplement_name], + function (int $target_id) use ($core_id, $loose_supplement) { + $loose_supplement_row = [ + $loose_supplement->core_id_column => $core_id, + $loose_supplement->target_id_column => $target_id, + ]; + return sqltable_create($loose_supplement->edge_table, $loose_supplement_row); + } + ); + } + } + ); + } +} + + +/** + * @author Christian Fraß + */ +function sqltablecluster_delete( + struct_sqltablecluster $subject, + /*int */$key +) : void +{ + // delete depedent data + { + $observation = $subject->observer_delete->notify($key); + } + $core_id = $key; + $core_row = sqltable_read($subject->core, $core_id); + // loose_supplements + { + \alveolata\map\map( + $subject->loose_supplements, + function (struct_sqltablecluster_loose_supplement $loose_supplement) use ($core_id) { + $loose_supplement_ids = sqltable_search($loose_supplement->edge_table, [$loose_supplement->core_id_column => $core_id]); + return \alveolata\list_\map/**/( + $loose_supplement_ids, + function (int $loose_supplement_id) use ($loose_supplement) : void { + sqltable_delete($loose_supplement->edge_table, $loose_supplement_id); + } + ); + } + ); + } + // tight_supplements + { + \alveolata\map\map( + $subject->tight_supplements, + function (struct_sqltablecluster_tight_supplement $tight_supplement) use ($core_id) { + $tight_supplement_ids = sqltable_search($tight_supplement->target_table, [$tight_supplement->core_id_column => $core_id]); + return \alveolata\list_\map/**/( + $tight_supplement_ids, + function (int $tight_supplement_id) use ($tight_supplement) : void { + sqltable_delete($tight_supplement->target_table, $tight_supplement_id); + } + ); + } + ); + } + // core + { + sqltable_delete($subject->core, $core_id); + } +} + + +/** + * @author Christian Fraß + */ +function sqltablecluster_read( + struct_sqltablecluster $subject, + /*int */$key +)/* : map*/ +{ + $core_id = $key; + // core + { + $core_row = sqltable_read($subject->core, $core_id); + } + // tight supplements + { + $tight_supplement_values = \alveolata\map\map( + $subject->tight_supplements, + function (struct_sqltablecluster_tight_supplement $tight_supplement) use ($core_id) { + $tight_supplement_ids = sqltable_search($tight_supplement->target_table, [$tight_supplement->core_id_column => $core_id]); + sort($tight_supplement_ids); + $value = \alveolata\list_\map/**/( + $tight_supplement_ids, + function (int $tight_supplement_id) use ($tight_supplement) { + $tight_supplement_row = sqltable_read($tight_supplement->target_table, $tight_supplement_id); + if ($tight_supplement->include_own_id) { + $tight_supplement_row['id'] = $tight_supplement_id; + } + if ($tight_supplement->exclude_core_id) { + unset($tight_supplement_row[$tight_supplement->core_id_column]); + } + return $tight_supplement_row; + } + ); + return $value; + } + ); + } + // loose supplements + { + $loose_supplement_values = \alveolata\map\map( + $subject->loose_supplements, + function (struct_sqltablecluster_loose_supplement $loose_supplement) use ($core_id) { + $loose_supplement_ids = sqltable_search($loose_supplement->edge_table, [$loose_supplement->core_id_column => $core_id]); + sort($loose_supplement_ids); + $value = \alveolata\list_\map/**/( + $loose_supplement_ids, + function (int $loose_supplement_id) use ($loose_supplement) { + $loose_supplement_row = sqltable_read($loose_supplement->edge_table, $loose_supplement_id); + $target_id = $loose_supplement_row[$loose_supplement->target_id_column]; + return $target_id; + } + ); + return $value; + } + ); + } + $stuff = [ + 'core_row' => $core_row, + 'tight_supplement_values' => $tight_supplement_values, + 'loose_supplement_values' => $loose_supplement_values, + ]; + $value = ($subject->assemble)($stuff); + return $value; +} + + +/** + * @author Christian Fraß + * @todo search in satellites + */ +function sqltablecluster_search( + struct_sqltablecluster $subject, + array $parameters = [] +) : array +{ + return sqltable_search($subject->core, $parameters); +} + + ?> diff --git a/lib/alveolata/storage/implementation-sqltablecluster/test.spec.php b/lib/alveolata/storage/implementation-sqltablecluster/test.spec.php new file mode 100644 index 0000000..4279790 --- /dev/null +++ b/lib/alveolata/storage/implementation-sqltablecluster/test.spec.php @@ -0,0 +1,276 @@ + 'alveolata', + 'sections' => [ + [ + 'name' => 'storage', + 'sections' => [ + [ + 'name' => 'sqltablecluster', + 'setup' => function (&$environment) { + $environment['path_database'] = '/tmp/localdb.sqlite'; + exec(sprintf('echo "" > %s', $environment['path_database'])); + $database = \alveolata\database\make( + 'sqlite', + [ + 'path' => $environment['path_database'], + ] + ); + $database->query( + 'CREATE TABLE fehuz( + id INTEGER PRIMARY KEY AUTOINCREMENT, + value TEXT + );', + [] + ); + $database->query( + 'CREATE TABLE uruz( + id INTEGER PRIMARY KEY AUTOINCREMENT, + fehuz_id INTEGER NOT NULL, + value TEXT + );', + [] + ); + $database->query( + 'CREATE TABLE thurisaz( + id INTEGER PRIMARY KEY AUTOINCREMENT, + value TEXT + );', + [] + ); + $database->query( + 'CREATE TABLE ansuz( + id INTEGER PRIMARY KEY AUTOINCREMENT, + fehuz_id INTEGER NOT NULL, + thurisaz_id INTEGER NOT NULL, + FOREIGN KEY (fehuz_id) REFERENCES fehuz(id), + FOREIGN KEY (thurisaz_id) REFERENCES thurisaz(id) + );', + [] + ); + $sqltable_thurisaz = \alveolata\storage\implementation_sqltable::make( + $database, + 'thurisaz' + ); + $sqltablecluster = \alveolata\storage\implementation_sqltablecluster::make( + \alveolata\storage\implementation_sqltable::make( + $database, + 'fehuz' + ), + [ + 'uruz' => [ + 'target_table' => \alveolata\storage\implementation_sqltable::make( + $database, + 'uruz' + ), + 'core_id_column' => 'fehuz_id', + 'include_own_id' => false, + 'exclude_core_id' => false, + ] + ], + [ + 'thurisaz' => [ + 'target_table' => $sqltable_thurisaz, + 'edge_table' => \alveolata\storage\implementation_sqltable::make( + $database, + 'ansuz' + ), + 'core_id_column' => 'fehuz_id', + 'target_id_column' => 'thurisaz_id', + ] + ], + function ($stuff) { + return [ + 'fehuz' => $stuff['core_row']['value'], + 'uruz' => \alveolata\list_\map( + $stuff['tight_supplement_values']['uruz'], + function ($x) {return $x['value'];} + ), + 'thurisaz' => \alveolata\list_\map( + $stuff['loose_supplement_values']['thurisaz'], + function ($x) {return $x['value'];} + ), + ]; + }, + function ($value) { + return [ + 'core_row' => [ + 'value' => $value['fehuz'], + ], + 'tight_supplement_values' => [ + 'uruz' => \alveolata\list_\map( + $value['uruz'], + function ($x) {return ['value' => $x];} + ), + ], + 'loose_supplement_values' => [ + 'thurisaz' => \alveolata\list_\map( + $value['thurisaz'], + function ($x) {return ['value' => $x];} + ), + ], + ]; + } + ); + $environment['sqltable_thurisaz'] = $sqltable_thurisaz; + $environment['storage'] = $sqltablecluster; + }, + 'sections' => [ + [ + 'name' => 'create_delete', + 'cases' => [ + [ + 'name' => 'test', + 'procedure' => function ($assert, $environment) { + // constants + $value = [ + 'fehuz' => 'fehuz', + 'uruz' => [ + 'uruz1', + 'uruz2', + ], + 'thurisaz' => [ + // 'thurisaz1', + // 'thurisaz2', + ], + ]; + + // execution & assertions + $assert->runs( + function () use (&$environment, &$value) { + $key = $environment['storage']->create($value); + $environment['storage']->delete($key); + } + ); + } + ], + ] + ], + [ + 'name' => 'read', + 'cases' => [ + [ + 'name' => 'test', + 'procedure' => function ($assert, $environment) { + // constants + $value = [ + 'fehuz' => 'fehuz', + 'uruz' => [ + 'uruz1', + 'uruz2', + ], + 'thurisaz' => [ + // 'thurisaz1', + // 'thurisaz2', + ], + ]; + + // setup + $key = $environment['storage']->create($value); + + // execution + $value_read = $environment['storage']->read($key); + + // assertions + $assert->equal($value_read, $value); + + // cleanup + $environment['storage']->delete($key); + } + ], + ] + ], + [ + 'name' => 'search', + 'cases' => [ + [ + 'name' => 'test', + 'procedure' => function ($assert, $environment) { + // constants + $value = [ + 'fehuz' => 'fehuz', + 'uruz' => [ + 'uruz1', + 'uruz2', + ], + 'thurisaz' => [ + // 'thurisaz1', + // 'thurisaz2', + ], + ]; + + // setup + $key = $environment['storage']->create($value); + + // execution + $keys = $environment['storage']->search(['value' => 'fehuz']); + + // assertions + $assert->equal($keys, [$key]); + + // cleanup + $environment['storage']->delete($key); + } + ], + ] + ], + /* + [ + 'name' => 'update', + 'cases' => [ + [ + 'name' => 'test', + 'procedure' => function ($assert, $environment) { + // constants + $value1 = [ + 'foo' => 'Foo', + 'bar' => [['val' => 0], ['val' => 2]], + 'baz' => [['val' => 1], ['val' => 3]], + ]; + $value2 = [ + 'foo' => 'Fox', + 'bar' => [['val' => 4], ['val' => 6]], + 'baz' => [['val' => 5], ['val' => 7]], + ]; + + // setup + $key = $environment['storage']->create($value1); + + // execution + $environment['storage']->update($key, $value2); + $value_read = $environment['storage']->read($key); + + // assertions + $assert->equal($value_read, $value2); + + // cleanup + $environment['storage']->delete($key); + } + ], + ] + ], + */ + ], + 'cleanup' => function (&$environment) { + // $environment['storage']->teardown(); + exec(sprintf('rm -f %s', $environment['path_database'])); + } + ] + ] + ], + ] + ] +); + diff --git a/lib/alveolata/storage/implementation-sqltablecluster/wrapper-class.php b/lib/alveolata/storage/implementation-sqltablecluster/wrapper-class.php new file mode 100644 index 0000000..f716dca --- /dev/null +++ b/lib/alveolata/storage/implementation-sqltablecluster/wrapper-class.php @@ -0,0 +1,88 @@ + + */ +class implementation_sqltablecluster implements interface_storage/**/ { + + /** + * @var struct_sqltablecluster $subject + * @author Christian Fraß + */ + private $subject; + + + /** + * @param struct_sqltable $subject + * @author Christian Fraß + */ + private function __construct(struct_sqltablecluster $subject) {$this->subject = $subject;} + + + /** + * @return implementation_sqltablecluster + * @author Christian Fraß + */ + public static function make( + \alveolata\storage\implementation_sqltable $core, + array $tight_supplements, + array $loose_supplements, + \Closure $assemble, + \Closure $disperse + ) : implementation_sqltablecluster + { + $subject = sqltablecluster_make( + $core->get_subject(), + \alveolata\map\map( + $tight_supplements, + function ($value) { + return (new struct_sqltablecluster_tight_supplement( + $value['target_table']->get_subject(), + $value['core_id_column'], + $value['include_own_id'], + $value['exclude_core_id'] + )); + } + ), + \alveolata\map\map( + $loose_supplements, + function ($value) { + return (new struct_sqltablecluster_loose_supplement( + $value['target_table']->get_subject(), + $value['edge_table']->get_subject(), + $value['core_id_column'], + $value['target_id_column'], + )); + } + ), + $assemble, + $disperse + ); + return (new implementation_sqltablecluster($subject)); + } + + + /** + * implementations + * + * @author Christian Fraß + */ + public function teardown() : void {sqltablecluster_teardown($this->subject);} + public function setup() : void {sqltablecluster_setup($this->subject);} + public function create($value) {return sqltablecluster_create($this->subject, $value);} + public function update($key, $value) : void {sqltablecluster_update($this->subject, $key, $value);} + public function delete($key) : void {sqltablecluster_delete($this->subject, $key);} + public function read($key) {return sqltablecluster_read($this->subject, $key);} + public function search(array $parameters) : array {return sqltablecluster_search($this->subject, $parameters);} + +} + +?> diff --git a/lib/alveolata/storage/implementation-sqltablegroup/functions.php b/lib/alveolata/storage/implementation-sqltablegroup/functions.php new file mode 100644 index 0000000..9845aa4 --- /dev/null +++ b/lib/alveolata/storage/implementation-sqltablegroup/functions.php @@ -0,0 +1,202 @@ + + */ +class struct_sqltablegroup +{ + + /** + * @var struct_sqltable + * @author Christian Fraß + */ + public $core; + + + /** + * @var list> + * @author Christian Fraß + */ + public $satellites; + + + /** + * @author Christian Fraß + */ + public function __construct( + \alveolata\storage\struct_sqltable $core, + array $satellites + ) + { + $this->core = $core; + $this->satellites = $satellites; + } + +} + + +/** + * @author Christian Fraß + */ +function sqltablegroup_make( + \alveolata\storage\struct_sqltable $core, + array $satellites +) +{ + return ( + new struct_sqltablegroup( + $core, + $satellites + ) + ); +} + + +/** + * @author Christian Fraß + */ +function sqltablegroup_teardown( + struct_sqltablegroup $subject +) : void +{ + foreach ($subject->satellites as $satellite) { + \alveolata\storage\sqltable_teardown($satellite['sqltable']); + } + \alveolata\storage\sqltable_teardown($subject->core); +} + + +/** + * @author Christian Fraß + */ +function sqltablegroup_setup( + struct_sqltablegroup $subject +) : void +{ + \alveolata\storage\sqltable_setup($subject->core); + foreach ($subject->satellites as $satellite) { + \alveolata\storage\sqltable_setup($satellite['sqltable']); + } +} + + +/** + * @author Christian Fraß + */ +function sqltablegroup_create( + struct_sqltablegroup $subject, + /*map */$value +)/* : int*/ +{ + $value_stripped = $value; + foreach ($subject->satellites as $satellite) { + unset($value_stripped[$satellite['target']]); + } + $key = \alveolata\storage\sqltable_create($subject->core, $value_stripped); + foreach ($subject->satellites as $satellite) { + foreach ($value[$satellite['target']] as $value_orbit) { + $value_orbit[$satellite['key']] = $key; + \alveolata\storage\sqltable_create($satellite['sqltable'], $value_orbit); + } + } + return $key; +} + + +/** + * @author Christian Fraß + * @todo improve update of satellites + */ +function sqltablegroup_update( + struct_sqltablegroup $subject, + /*int */$key, + /*map */$value +) : void +{ + $value_stripped = $value; + foreach ($subject->satellites as $satellite) { + unset($value_stripped[$satellite['target']]); + } + \alveolata\storage\sqltable_update($subject->core, $key, $value_stripped); + foreach ($subject->satellites as $satellite) { + // delete old + $keys_satellites = \alveolata\storage\sqltable_search($satellite['sqltable'], [$satellite['key'] => $key]); + foreach ($keys_satellites as $key_satellite) { + \alveolata\storage\sqltable_delete($satellite['sqltable'], $key_satellite); + } + // create new + foreach ($value[$satellite['target']] as $value_orbit) { + $value_orbit[$satellite['key']] = $key; + \alveolata\storage\sqltable_create($satellite['sqltable'], $value_orbit); + } + } +} + + +/** + * @author Christian Fraß + */ +function sqltablegroup_delete( + struct_sqltablegroup $subject, + /*int */$key +) : void +{ + foreach ($subject->satellites as $satellite) { + $key_satellites = \alveolata\storage\sqltable_search($satellite['sqltable'], [$satellite['key'] => $key]); + foreach ($key_satellites as $key_satellite) { + \alveolata\storage\sqltable_delete($satellite['sqltable'], $key_satellite); + } + } + \alveolata\storage\sqltable_delete($subject->core, $key); +} + + +/** + * @author Christian Fraß + */ +function sqltablegroup_read( + struct_sqltablegroup $subject, + /*int */$key +)/* : map*/ +{ + $value = \alveolata\storage\sqltable_read($subject->core, $key); + foreach ($subject->satellites as $satellite) { + $keys_satellites = \alveolata\storage\sqltable_search($satellite['sqltable'], [$satellite['key'] => $key]); + $value_orbit = []; + foreach ($keys_satellites as $key_satellite) { + $value_orbit_element = \alveolata\storage\sqltable_read($satellite['sqltable'], $key_satellite); + unset($value_orbit_element[$satellite['key']]); + array_push($value_orbit, $value_orbit_element); + } + $value[$satellite['target']] = $value_orbit; + } + return $value; +} + + +/** + * @author Christian Fraß + * @todo search in satellites + */ +function sqltablegroup_search( + struct_sqltablegroup $subject, + array $parameters = [] +) : array +{ + $parameters_stripped = $parameters; + foreach ($subject->satellites as $satellite) { + unset($parameters_stripped[$satellite['target']]); + } + return \alveolata\storage\sqltable_search($subject->core, $parameters_stripped); +} + + ?> diff --git a/lib/alveolata/storage/implementation-sqltablegroup/test.spec.php b/lib/alveolata/storage/implementation-sqltablegroup/test.spec.php new file mode 100644 index 0000000..3ffc38a --- /dev/null +++ b/lib/alveolata/storage/implementation-sqltablegroup/test.spec.php @@ -0,0 +1,216 @@ + 'alveolata', + 'sections' => [ + [ + 'name' => 'storage', + 'sections' => [ + [ + 'name' => 'sqltablegroup', + 'setup' => function (&$environment) { + $environment['path_database'] = '/tmp/localdb.sqlite'; + exec(sprintf('echo "" > %s', $environment['path_database'])); + $database = \alveolata\database\make( + 'sqlite', + [ + 'path' => $environment['path_database'], + ] + ); + $environment['storage'] = \alveolata\storage\implementation_sqltablegroup::make( + \alveolata\storage\implementation_sqltable::make( + $database, + '_testtable_core_', + [ + [ + 'name' => 'foo', + 'type' => 'VARCHAR(64)', + 'null_allowed' => false, + 'default' => '', + ], + ] + ), + [ + [ + 'sqltable' => \alveolata\storage\sqltable_make( + $database, + '_testtable_satellite1_', + [ + [ + 'name' => 'foo_id', + 'type' => 'INTEGER', + 'null_allowed' => false, + ], + [ + 'name' => 'val', + 'type' => 'INTEGER', + 'null_allowed' => false, + 'default' => 0, + ], + ] + ), + 'key' => 'foo_id', + 'target' => 'bar', + ], + [ + 'sqltable' => \alveolata\storage\sqltable_make( + $database, + '_testtable_satellite2_', + [ + [ + 'name' => 'foo_id', + 'type' => 'INTEGER', + 'null_allowed' => false, + ], + [ + 'name' => 'val', + 'type' => 'INTEGER', + 'null_allowed' => false, + 'default' => 0, + ], + ] + ), + 'key' => 'foo_id', + 'target' => 'baz', + ], + ] + ); + $environment['storage']->setup(); + }, + 'sections' => [ + [ + 'name' => 'create_delete', + 'cases' => [ + [ + 'name' => 'test', + 'procedure' => function ($assert, $environment) { + // constants + $value = [ + 'foo' => 'Foo', + 'bar' => [['val' => 0], ['val' => 2]], + 'baz' => [['val' => 1], ['val' => 3]], + ]; + + // execution & assertions + $assert->runs( + function () use (&$environment, &$value) { + $key = $environment['storage']->create($value); + $environment['storage']->delete($key); + } + ); + } + ], + ] + ], + [ + 'name' => 'read', + 'cases' => [ + [ + 'name' => 'test', + 'procedure' => function ($assert, $environment) { + // constants + $value = [ + 'foo' => 'Foo', + 'bar' => [['val' => 0], ['val' => 2]], + 'baz' => [['val' => 1], ['val' => 3]], + ]; + + // setup + $key = $environment['storage']->create($value); + + // execution + $value_read = $environment['storage']->read($key); + + // assertions + $assert->equal($value_read, $value); + + // cleanup + $environment['storage']->delete($key); + } + ], + ] + ], + [ + 'name' => 'search', + 'cases' => [ + [ + 'name' => 'test', + 'procedure' => function ($assert, $environment) { + // constants + $value = [ + 'foo' => 'Foo', + 'bar' => [['val' => 0], ['val' => 2]], + 'baz' => [['val' => 1], ['val' => 3]], + ]; + + // setup + $key = $environment['storage']->create($value); + + // execution + $keys = $environment['storage']->search(['bar' => ['val' => 2]]); + + // assertions + $assert->equal($keys, [$key]); + + // cleanup + $environment['storage']->delete($key); + } + ], + ] + ], + [ + 'name' => 'update', + 'cases' => [ + [ + 'name' => 'test', + 'procedure' => function ($assert, $environment) { + // constants + $value1 = [ + 'foo' => 'Foo', + 'bar' => [['val' => 0], ['val' => 2]], + 'baz' => [['val' => 1], ['val' => 3]], + ]; + $value2 = [ + 'foo' => 'Fox', + 'bar' => [['val' => 4], ['val' => 6]], + 'baz' => [['val' => 5], ['val' => 7]], + ]; + + // setup + $key = $environment['storage']->create($value1); + + // execution + $environment['storage']->update($key, $value2); + $value_read = $environment['storage']->read($key); + + // assertions + $assert->equal($value_read, $value2); + + // cleanup + $environment['storage']->delete($key); + } + ], + ] + ], + ], + 'cleanup' => function (&$environment) { + $environment['storage']->teardown(); + exec(sprintf('rm -f %s', $environment['path_database'])); + } + ] + ] + ], + ] + ] +); + diff --git a/lib/alveolata/storage/implementation-sqltablegroup/wrapper-class.php b/lib/alveolata/storage/implementation-sqltablegroup/wrapper-class.php new file mode 100644 index 0000000..63b7929 --- /dev/null +++ b/lib/alveolata/storage/implementation-sqltablegroup/wrapper-class.php @@ -0,0 +1,58 @@ + + */ +class implementation_sqltablegroup implements interface_storage/**/ { + + /** + * @var struct_sqltablegroup $subject + * @author Christian Fraß + */ + private $subject; + + + /** + * @param struct_sqltable $subject + * @author Christian Fraß + */ + private function __construct(struct_sqltablegroup $subject) {$this->subject = $subject;} + + + /** + * @return implementation_sqltable + * @author Christian Fraß + */ + public static function make( + \alveolata\storage\implementation_sqltable $core, + array $satellites + ) : implementation_sqltablegroup + { + $subject = sqltablegroup_make($core->get_subject(), $satellites); + return (new implementation_sqltablegroup($subject)); + } + + + /** + * implementations + * + * @author Christian Fraß + */ + public function teardown() : void {sqltablegroup_teardown($this->subject);} + public function setup() : void {sqltablegroup_setup($this->subject);} + public function create($value) {return sqltablegroup_create($this->subject, $value);} + public function update($key, $value) : void {sqltablegroup_update($this->subject, $key, $value);} + public function delete($key) : void {sqltablegroup_delete($this->subject, $key);} + public function read($key) {return sqltablegroup_read($this->subject, $key);} + public function search(array $parameters) : array {return sqltablegroup_search($this->subject, $parameters);} + +} + +?> diff --git a/lib/alveolata/storage/implementation-wrapper/functions.php b/lib/alveolata/storage/implementation-wrapper/functions.php new file mode 100644 index 0000000..72663bb --- /dev/null +++ b/lib/alveolata/storage/implementation-wrapper/functions.php @@ -0,0 +1,208 @@ + + */ +class struct_wrapper/**/ +{ + + /** + * @var \Closure {function} + * @author Christian Fraß + */ + public $convert_key_in; + + + /** + * @var \Closure {function} + * @author Christian Fraß + */ + public $convert_key_out; + + + /** + * @var \Closure {function} + * @author Christian Fraß + */ + public $convert_value_in; + + + /** + * @var \Closure {function} + * @author Christian Fraß + */ + public $convert_value_out; + + + /** + * @author Christian Fraß + */ + public function __construct( + \Closure $core_setup, + \Closure $core_teardown, + \Closure $core_create, + \Closure $core_update, + \Closure $core_delete, + \Closure $core_read, + \Closure $core_search, + \Closure $convert_key_in, + \Closure $convert_key_out, + \Closure $convert_value_in, + \Closure $convert_value_out + ) + { + $this->core_setup = $core_setup; + $this->core_teardown = $core_teardown; + $this->core_create = $core_create; + $this->core_update = $core_update; + $this->core_delete = $core_delete; + $this->core_read = $core_read; + $this->core_search = $core_search; + $this->convert_key_in = $convert_key_in; + $this->convert_key_out = $convert_key_out; + $this->convert_value_in = $convert_value_in; + $this->convert_value_out = $convert_value_out; + } + +} + + +/** + * @author Christian Fraß + */ +function wrapper_make/**/( + \Closure $core_setup, + \Closure $core_teardown, + \Closure $core_create, + \Closure $core_update, + \Closure $core_delete, + \Closure $core_read, + \Closure $core_search, + \Closure $convert_key_in, + \Closure $convert_key_out, + \Closure $convert_value_in, + \Closure $convert_value_out +) : struct_wrapper/**/ +{ + return ( + new struct_wrapper/**/( + $core_setup, + $core_teardown, + $core_create, + $core_update, + $core_delete, + $core_read, + $core_search, + $convert_key_in, + $convert_key_out, + $convert_value_in, + $convert_value_out + ) + ); +} + + +/** + * @author Christian Fraß + */ +function wrapper_teardown/**/( + struct_wrapper/**/ $subject +) : void +{ + ($subject->core_teardown)(); +} + + +/** + * @author Christian Fraß + */ +function wrapper_setup( + struct_wrapper/**/ $subject +) : void +{ + ($subject->core_setup)(); +} + + +/** + * @author Christian Fraß + */ +function wrapper_create( + struct_wrapper/**/ $subject, + /*type_value_outer */$value_outer +)/* : type_key_outer*/ +{ + $value_inner = ($subject->convert_value_in)($value_outer); + $key_inner = ($subject->core_create)($value_inner); + $key_outer = ($subject->convert_key_out)($key_inner); + return $key_outer; +} + + +/** + * @author Christian Fraß + */ +function wrapper_update( + struct_wrapper/**/ $subject, + /*type_key_outer */$key_outer, + /*type_value_outer */$value_outer +) : void +{ + $key_inner = ($subject->convert_key_in)($key_outer); + $value_inner = ($subject->convert_value_in)($value_outer); + ($subject->core_update)($key_inner, $value_inner); +} + + +/** + * @author Christian Fraß + */ +function wrapper_delete( + struct_wrapper/**/ $subject, + /*type_key_outer */$key_outer +) : void +{ + ($subject->core_delete)($key_outer); +} + + +/** + * @author Christian Fraß + */ +function wrapper_read( + struct_wrapper/**/ $subject, + /*type_key_outer */$key_outer +)/* : type_value_outer*/ +{ + $key_inner = ($subject->convert_key_in)($key_outer); + $value_inner = ($subject->core_read)($key_inner); + $value_outer = ($subject->convert_value_out)($value_inner); + return $value_outer; +} + + +/** + * @author Christian Fraß + */ +function wrapper_search( + struct_wrapper/**/ $subject, + array $parameters = [] +) : array +{ + $keys_inner = ($subject->core_search)($parameters); + $keys_outer = \alveolata\list_\map/**/( + $keys_inner, + function (/*type_key_inner */$key_inner)/* : type_key_outer*/ use ($subject) { + return ($subject->convert_key_out)($key_inner); + } + ); + return $keys_outer; +} + + ?> diff --git a/lib/alveolata/storage/implementation-wrapper/test.spec.php b/lib/alveolata/storage/implementation-wrapper/test.spec.php new file mode 100644 index 0000000..1a0546b --- /dev/null +++ b/lib/alveolata/storage/implementation-wrapper/test.spec.php @@ -0,0 +1,174 @@ + 'alveolata', + 'sections' => [ + [ + 'name' => 'storage', + 'sections' => [ + [ + 'name' => 'wrapper', + 'setup' => function (&$environment) { + $environment['path_database'] = '/tmp/localdb.sqlite'; + exec(sprintf('echo "" > %s', $environment['path_database'])); + $database = \alveolata\database\make( + 'sqlite', + [ + 'path' => $environment['path_database'], + ] + ); + $tablename = '_testtable_'; + $fields = [ + [ + 'name' => 'state', + 'type' => 'VARCHAR(64)', + 'null_allowed' => false, + 'default' => '', + ], + ]; + $core = \alveolata\storage\implementation_sqltable::make( + $database, + $tablename, + $fields + ); + $environment['storage'] = \alveolata\storage\implementation_wrapper::make( + $core, + function (string $key_outer) : int { + return intval($key_outer); + }, + function (int $key_inner) : string { + return strval($key_inner); + }, + function (bool $value_outer) : array { + return [ + 'state' => ( + $value_outer + ? 'true' + : 'false' + ), + ]; + }, + function (array $value_inner) : bool { + return ($value_inner['state'] === 'true'); + } + ); + $environment['storage']->setup(); + }, + 'sections' => [ + [ + 'name' => 'create_delete', + 'cases' => [ + [ + 'name' => 'test', + 'procedure' => function ($assert, $environment) { + // constants + $state = true; + + // execution & assertions + $assert->runs( + function () use (&$environment, &$state) { + $key = $environment['storage']->create($state); + $environment['storage']->delete($key); + } + ); + } + ], + ] + ], + [ + 'name' => 'read', + 'cases' => [ + [ + 'name' => 'test', + 'procedure' => function ($assert, $environment) { + // constants + $state = true; + + // setup + $key = $environment['storage']->create($state); + + // execution + $value = $environment['storage']->read($key); + + // assertions + $assert->equal($value, $state); + + // cleanup + $environment['storage']->delete($key); + } + ], + ] + ], + [ + 'name' => 'search', + 'cases' => [ + [ + 'name' => 'test', + 'procedure' => function ($assert, $environment) { + // constants + $state = true; + + // setup + $key = $environment['storage']->create($state); + + // execution + $keys = $environment['storage']->search([]); + + // assertions + $assert->equal($keys, [$key]); + + // cleanup + $environment['storage']->delete($key); + } + ], + ] + ], + [ + 'name' => 'update', + 'cases' => [ + [ + 'name' => 'test', + 'procedure' => function ($assert, $environment) { + // constants + $state1 = true; + $state2 = false; + + // setup + $key = $environment['storage']->create($state1); + + // execution + $environment['storage']->update($key, $state2); + $value = $environment['storage']->read($key); + + // assertions + $assert->equal($value, $state2); + + // cleanup + $environment['storage']->delete($key); + } + ], + ] + ], + ], + 'cleanup' => function (&$environment) { + $environment['storage']->teardown(); + exec(sprintf('rm -f %s', $environment['path_database'])); + } + ] + ] + ], + ] + ] +); + diff --git a/lib/alveolata/storage/implementation-wrapper/wrapper-class.php b/lib/alveolata/storage/implementation-wrapper/wrapper-class.php new file mode 100644 index 0000000..2fb51c1 --- /dev/null +++ b/lib/alveolata/storage/implementation-wrapper/wrapper-class.php @@ -0,0 +1,73 @@ + + */ +class implementation_wrapper/**/ implements interface_storage/**/ { + + /** + * @var struct_wrapper $subject + * @author Christian Fraß + */ + private $subject; + + + /** + * @param struct_wrapper $subject + * @author Christian Fraß + */ + protected function __construct(struct_wrapper $subject) {$this->subject = $subject;} + + + /** + * @return implementation_wrapper + * @author Christian Fraß + */ + public static function make( + \alveolata\storage\interface_storage/**/ $core, + \Closure $convert_key_in, + \Closure $convert_key_out, + \Closure $convert_value_in, + \Closure $convert_value_out + ) : implementation_wrapper + { + $subject = wrapper_make( + function () use ($core) {$core->setup();}, + function () use ($core) {$core->teardown();}, + function ($value) use ($core) {return $core->create($value);}, + function ($key, $value) use ($core) {$core->update($key, $value);}, + function ($key) use ($core) {$core->delete($key);}, + function ($key) use ($core) {return $core->read($key);}, + function ($parameters) use ($core) {return $core->search($parameters);}, + $convert_key_in, + $convert_key_out, + $convert_value_in, + $convert_value_out + ); + return (new implementation_wrapper($subject)); + } + + + /** + * implementations + * + * @author Christian Fraß + */ + public function teardown() : void {wrapper_teardown($this->subject);} + public function setup() : void {wrapper_setup($this->subject);} + public function create($value) {return wrapper_create($this->subject, $value);} + public function update($key, $value) : void {wrapper_update($this->subject, $key, $value);} + public function delete($key) : void {wrapper_delete($this->subject, $key);} + public function read($key) {return wrapper_read($this->subject, $key);} + public function search(array $parameters) : array {return wrapper_search($this->subject, $parameters);} + +} + +?> diff --git a/lib/alveolata/string/functions.php b/lib/alveolata/string/functions.php new file mode 100644 index 0000000..ae743b0 --- /dev/null +++ b/lib/alveolata/string/functions.php @@ -0,0 +1,250 @@ + + */ +class _state { + + /** + * @var int + */ + public static $generateCounters = []; + + + /** + * @var string + */ + public static $generatePattern = '{{context}}-{{number}}'; + +} + + +/** + * splits a string into pieces according to a given delimiter + * + * @param string $subject + * @param string $delimiter + * @param int $limit + * @return array {list} + * @author Christian Fraß + */ +function split( + string $subject, + string $delimiter, + int $limit = UNSET_INTEGER +) : array +{ + if (empty($subject)) { + return []; + } + else { + if ($limit === UNSET_INTEGER) { + return \explode($delimiter, $subject); + } + else { + return \explode($delimiter, $subject, $limit); + } + } +} + + +/** + * connects string pieces together according to a given delimiter + * + * @param array $parts {list} + * @param string $delimiter + * @return string + * @author Christian Fraß + */ +function join( + array $parts, + string $delimiter +) : string +{ + return implode($delimiter, $parts); +} + + +/** + */ +function starts_with( + string $subject, + string $part +) : bool +{ + if (empty($part)) { + return true; + } + else { + return (strpos($subject, $part) === 0); + } +} + + +/** + */ +function ends_with( + string $subject, + string $part +) : bool +{ + if (empty($part)) { + return true; + } + else { + return (strrpos($subject, $part) === (strlen($subject) - strlen($part))); + } +} + + +/** + * @see https://www.php.net/manual/en/function.str-contains.php + */ +function contains( + string $subject, + string $part +) : bool +{ + return \str_contains($subject, $part); +} + + +/** + * @param array $replacments {map} + * @author Christian Fraß + */ +function replace( + string $string, + array $replacements +) : string +{ + $result = $string; + foreach ($replacements as $from => $to) { + str_replace($result, $from, $to); + } + return $result; +} + + +/** + * @param string $template + * @param map [$arguments] + * @param string $open left delimiter for placeholder + * @param string $close right delimiter for placeholder + * @return string + * @author Christian Fraß + */ +function coin( + string $template, + array $arguments = [], + string $open = '{{', + string $close = '}}' +) : string +{ + $result = $template; + foreach ($arguments as $key => $value) { + $pattern = ($open . $key . $close); + $replacement = strval($value); + $result = str_replace($pattern, $replacement, $result); + } + return $result; +} + + +/** + * @param string $core + * @param int $length + * @param string $filler + * @return string + * @author Christian Fraß + */ +function pad_right( + string $core, + int $length, + string $filler +) : string +{ + return str_pad($core, $length, $filler, STR_PAD_RIGHT); +} + + +/** + * @param string $string + * @return string + * @author Christian Fraß + */ +function case_upper( + string $string +) : string +{ + return strtoupper($string); +} + + +/** + * @param string $string + * @param int $length the maximum length + * @param string $ellipsis how to finish a too long string + * @return string + * @author Christian Fraß + */ +function limit( + string $string, + int $length, + string $ellipsis = ' …' +) : string +{ + $use_mb = (function_exists('mb_strlen') && function_exists('mb_substr')); + $length_string = ($use_mb ? \mb_strlen($string) : \strlen($string)); + $length_ellipsis = ($use_mb ? \mb_strlen($ellipsis) : \strlen($ellipsis)); + return ( + ($length_string <= $length) + ? $string + : ( + $use_mb + ? (\mb_substr($string, 0, $length - $length_ellipsis) . $ellipsis) + : (\substr($string, 0, $length - $length_ellipsis) . $ellipsis) + ) + ); +} + + +/** + * @return string + * @author Christian Fraß + */ +function generate( + string $context = 'common' +) : string +{ + if (! array_key_exists($context, _state::$generateCounters)) { + _state::$generateCounters[$context] = 0; + } + $string = _state::coin( + _state::$generatePattern, + [ + 'context' => $context, + 'number' => sprintf('%d', _state::$generateCounters[$context]), + ] + ); + _state::$generateCounters[$context] += 1; + return $string; +} + + +/** + * removes the UTF-8 byte order mark from a string + */ +function remove_bom( + string $input +) : string +{ + return \preg_replace('/x{EF}x{BB}x{BF}/', '', $input); +} + + ?> diff --git a/lib/alveolata/string/test.spec.json b/lib/alveolata/string/test.spec.json new file mode 100644 index 0000000..880602b --- /dev/null +++ b/lib/alveolata/string/test.spec.json @@ -0,0 +1,380 @@ +{ + "active": true, + "sections": [ + { + "name": "split", + "active": true, + "execution": { + "call": "\\alveolata\\string\\split({{subject}}, {{delimiter}})" + }, + "cases": [ + { + "name": "empty", + "active": true, + "input": { + "subject": "", + "delimiter": "," + }, + "output": { + "kind": "regular", + "value": [] + } + }, + { + "name": "non-empty", + "active": true, + "input": { + "subject": "foo,bar,baz,qux", + "delimiter": "," + }, + "output": { + "kind": "regular", + "value": ["foo","bar","baz","qux"] + } + } + ] + }, + { + "name": "join", + "active": true, + "execution": { + "call": "\\alveolata\\string\\join({{parts}}, {{delimiter}})" + }, + "cases": [ + { + "name": "empty", + "active": true, + "input": { + "parts": [], + "delimiter": "," + }, + "output": { + "kind": "regular", + "value": "" + } + }, + { + "name": "non-empty", + "active": true, + "input": { + "parts": ["foo","bar","baz","qux"], + "delimiter": "," + }, + "output": { + "kind": "regular", + "value": "foo,bar,baz,qux" + } + } + ] + }, + { + "name": "starts_with", + "active": true, + "execution": { + "call": "\\alveolata\\string\\starts_with({{subject}}, {{part}})" + }, + "cases": [ + { + "name": "test1", + "active": true, + "input": { + "subject": "foobar", + "part": "foo" + }, + "output": { + "kind": "regular", + "value": true + } + }, + { + "name": "test2", + "active": true, + "input": { + "subject": "foobar", + "part": "fox" + }, + "output": { + "kind": "regular", + "value": false + } + }, + { + "name": "test3", + "active": true, + "input": { + "subject": "foobar", + "part": "" + }, + "output": { + "kind": "regular", + "value": true + } + }, + { + "name": "test4", + "active": true, + "input": { + "subject": "foobar", + "part": "foobar_" + }, + "output": { + "kind": "regular", + "value": false + } + } + ] + }, + { + "name": "ends_with", + "active": true, + "execution": { + "call": "\\alveolata\\string\\ends_with({{subject}}, {{part}})" + }, + "cases": [ + { + "name": "test1", + "active": true, + "input": { + "subject": "foobar", + "part": "bar" + }, + "output": { + "kind": "regular", + "value": true + } + }, + { + "name": "test2", + "active": true, + "input": { + "subject": "foobar", + "part": "baz" + }, + "output": { + "kind": "regular", + "value": false + } + }, + { + "name": "test3", + "active": true, + "input": { + "subject": "foobar", + "part": "" + }, + "output": { + "kind": "regular", + "value": true + } + }, + { + "name": "test4", + "active": true, + "input": { + "subject": "foobar", + "part": "_foobar" + }, + "output": { + "kind": "regular", + "value": false + } + }, + { + "name": "test5", + "active": true, + "input": { + "subject": "foobar", + "part": "foo" + }, + "output": { + "kind": "regular", + "value": false + } + } + ] + }, + { + "name": "contains", + "active": true, + "execution": { + "call": "\\alveolata\\string\\contains({{subject}}, {{part}})" + }, + "cases": [ + { + "name": "empty_subject", + "active": true, + "input": { + "subject": "", + "part": "x" + }, + "output": { + "kind": "regular", + "value": false + } + }, + { + "name": "empty_part", + "active": true, + "input": { + "subject": "whatever", + "part": "" + }, + "output": { + "kind": "regular", + "value": true + } + }, + { + "name": "regular_negative", + "active": true, + "input": { + "subject": "whatever", + "part": "ex" + }, + "output": { + "kind": "regular", + "value": false + } + }, + { + "name": "regular_positiv", + "active": true, + "input": { + "subject": "whatever", + "part": "ev" + }, + "output": { + "kind": "regular", + "value": true + } + } + ] + }, + { + "name": "coin", + "active": true, + "execution": { + "call": "\\alveolata\\string\\coin({{template}}, {{arguments}})" + }, + "cases": [ + { + "name": "regular", + "active": true, + "input": { + "template": "{{blumen}} sind {{farbe}}", + "arguments": { + "blumen": "rosen", + "farbe": "rot" + } + }, + "output": { + "kind": "regular", + "value": "rosen sind rot" + } + }, + { + "name": "missing_argument", + "active": true, + "input": { + "template": "{{blumen}} sind {{farbe}}", + "arguments": { + "farbe": "rot" + } + }, + "output": { + "kind": "regular", + "value": "{{blumen}} sind rot" + } + } + ] + }, + { + "name": "limit", + "active": true, + "execution": { + "call": "\\alveolata\\string\\limit({{string}}, {{length}}, {{ellipsis}})" + }, + "cases": [ + { + "name": "without unicode | short enough", + "active": true, + "input": { + "string": "foo", + "length": 5, + "ellipsis": "." + }, + "output": { + "kind": "regular", + "value": "foo" + } + }, + { + "name": "without unicode | exact length", + "active": true, + "input": { + "string": "foo b", + "length": 5, + "ellipsis": "." + }, + "output": { + "kind": "regular", + "value": "foo b" + } + }, + { + "name": "without unicode | too long", + "active": true, + "input": { + "string": "foo bar", + "length": 5, + "ellipsis": "." + }, + "output": { + "kind": "regular", + "value": "foo ." + } + }, + { + "name": "with unicode | short enough", + "active": true, + "input": { + "string": "foo", + "length": 5, + "ellipsis": " …" + }, + "output": { + "kind": "regular", + "value": "foo" + } + }, + { + "name": "with unicode | exact length", + "active": true, + "input": { + "string": "foo b", + "length": 5, + "ellipsis": " …" + }, + "output": { + "kind": "regular", + "value": "foo b" + } + }, + { + "name": "with unicode | too long", + "active": true, + "input": { + "string": "foo bar", + "length": 5, + "ellipsis": " …" + }, + "output": { + "kind": "regular", + "value": "foo …" + } + } + ] + } + ] +} + diff --git a/lib/alveolata/structures/list/functions.php b/lib/alveolata/structures/list/functions.php new file mode 100644 index 0000000..96e9035 --- /dev/null +++ b/lib/alveolata/structures/list/functions.php @@ -0,0 +1,209 @@ +} + */ + public $elements; + + + /** + * @param array $elements {list<§element>} + */ + public function __construct( + array $elements + ) + { + $this->elements = $elements; + } + +} + + +/** + * @template type_element + * @param array $elements {list<§element>} + * @return struct_subject_list + */ +function list_make( + array $elements = [] +) : struct_subject_list +{ + return (new struct_subject_list($elements)); +} + + +/** + * @template type_element + * @param struct_subject_list $subject + * @return int + */ +function list_length( + struct_subject_list $subject +) : int +{ + return count($subject->elements); +} + + +/** + * @template type_element + * @param struct_subject_list $subject + * @param int $index + * @return type_element + * @throws \Exception if index out of range + */ +function list_get( + struct_subject_list $subject, + int $index +) +{ + if (! (($index >= 0) && ($index < list_lengt($subject)))) { + throw (new \Exception('index out of range')); + } + else { + return $subject->elements[$index]; + } +} + + +/** + * @template type_element + * @param struct_subject_list $subject + * @param type_element $element + */ +function list_add( + struct_subject_list $subject, + $element +) : void +{ + array_push($subject->elements, $element); +} + + +/** + * @template type_element + * @param struct_subject_list $subject + * @param \Closure $procedure {function<§type_element,void>} + */ +function list_iterate( + struct_subject_list $subject, + \Closure $procedure +) : void +{ + foreach ($subject->elements as $element) { + $procedure($element); + } +} + + +/** + * @template type_element + * @param struct_subject_list $subject + * @param \Closure $predicate {function<§type_element,boolean>} + * @return struct_subject_list + */ +function list_filter( + struct_subject_list $subject, + \Closure $predicate +) : struct_subject_list +{ + return ( + new struct_subject_list( + array_values( + array_filter( + $subject->elements, + $predicate + ) + ) + ) + ); +} + + +/** + * @template type_element_from + * @template type_element_to + * @param struct_subject_list $subject + * @param \Closure $transformation {function<§type_element_from,§type_element_to>} + * @return struct_subject_list + */ +function list_map( + struct_subject_list $subject, + \Closure $transformation +) : struct_subject_list +{ + return ( + new struct_subject_list( + array_map( + $transformation, + $subject->elements + ) + ) + ); +} + + +/** + * @template type_element + * @template type_result + * @param struct_subject_list $subject + * @param type_result $start + * @param \Closure $aggregation function<§type_result,§type_element,§type_result> + * @return type_result + */ +function list_reduce( + struct_subject_list $subject, + $start, + \Closure $aggregation +) +{ + return array_reduce( + $subject->element, + $aggregation, + $start + ); +} + + +/** + * @template type_element_1 + * @template type_element_2 + * @param \Closure $collate_element {function} + * @param struct_subject_list + * @param struct_subject_list + * @return bool + */ +function list_collate( + \Closure $collate_element, + struct_subject_list $list_1, + struct_subject_list $list_2 +) : bool +{ + $length_1 = list_length($list_1); + $length_2 = list_length($list_2); + if (! ($length_1 === $length_2)) { + return false; + } + else { + for ($index = 0; $index < $length_1; $index += 1) { + $element_1 = $list_1->elements[$index]; + $element_2 = $list_2->elements[$index]; + if (! $collate_element($element_1, $element_2)) { + return false; + } + } + return true; + } +} + + ?> diff --git a/lib/alveolata/structures/list/test.spec.php b/lib/alveolata/structures/list/test.spec.php new file mode 100644 index 0000000..503808b --- /dev/null +++ b/lib/alveolata/structures/list/test.spec.php @@ -0,0 +1,78 @@ + 'alveolata', + 'sections' => [ + [ + 'name' => 'structures', + 'sections' => [ + [ + 'name' => 'list', + 'sections' => [ + [ + 'name' => 'map', + 'cases' => [ + [ + 'name' => 'positive', + 'procedure' => function ($assert) { + $subject = \alveolata\structures\list_make([2,3,5,7]); + $result_actual = \alveolata\structures\list_map($subject, function ($x) {return ($x * 2);}); + $result_expected = \alveolata\structures\list_make([4,6,10,14]); + $assert->equal( + \alveolata\structures\list_collate( + function ($x, $y) {return ($x === $y);}, + $result_actual, + $result_expected + ), + true + ); + } + ], + [ + 'name' => 'negative_length', + 'procedure' => function ($assert) { + $subject = \alveolata\structures\list_make([2,3,5,7]); + $result_actual = \alveolata\structures\list_map($subject, function ($x) {return ($x * 2);}); + $result_expected = \alveolata\structures\list_make([4,6,10]); + $assert->equal( + \alveolata\structures\list_collate( + function ($x, $y) {return ($x === $y);}, + $result_actual, + $result_expected + ), + false + ); + } + ], + [ + 'name' => 'negative_element', + 'procedure' => function ($assert) { + $subject = \alveolata\structures\list_make([2,3,5,7]); + $result_actual = \alveolata\structures\list_map($subject, function ($x) {return ($x * 2);}); + $result_expected = \alveolata\structures\list_make([4,6,9,14]); + $assert->equal( + \alveolata\structures\list_collate( + function ($x, $y) {return ($x === $y);}, + $result_actual, + $result_expected + ), + false + ); + } + ], + ] + ], + ] + ], + ] + ] + ] + ] +); + diff --git a/lib/alveolata/structures/list/wrapper-class.php b/lib/alveolata/structures/list/wrapper-class.php new file mode 100644 index 0000000..bfe8934 --- /dev/null +++ b/lib/alveolata/structures/list/wrapper-class.php @@ -0,0 +1,43 @@ + + */ + private $subject; + + + /** + * @param struct_subject_list + */ + public function __construct(struct_subject_list $subject) {$this->subject = $subject;} + + + /** + * @param array $elements {list<§element>} + */ + public static function make(array $elements = []) {return (new class_list(list_make($elements)));} + + + public function length() : int {return list_length($this->subject);} + public function get($index) {return list_get($this->subject, $index);} + public function add($element) : void {list_add($this->subject, $element);} + public function iterate(\Closure $procedure) : void {list_iterate($this->subject, $procedure);} + public function filter(\Closure $predicate) : class_list {return (new class_list(list_filter($this->subject, $predicate)));} + public function map(\Closure $transformation) : class_list {return (new class_list(list_map($this->subject, $transformation)));} + public function reduce($start, \Closure $aggregation) {return list_reduce($this->subject, $start, $aggregation);} + public function collate(\Closure $collate_element, class_list $other) {return list_collate($collate_element, $this->subject, $other);} + +} + + ?> diff --git a/lib/alveolata/structures/map/functions.php b/lib/alveolata/structures/map/functions.php new file mode 100644 index 0000000..fbeea23 --- /dev/null +++ b/lib/alveolata/structures/map/functions.php @@ -0,0 +1,141 @@ +>} + */ + public $pairs; + + + /** + * @param array {list<&struct_subject_pair<§type_key,§type_value>>} + */ + public function __construct( + array $pairs + ) + { + $this->pairs = $pairs; + } + +} + + +/** + * @template type_key + * @template type_value + * @param array {list>} + * @return struct_subject_map + */ +function map_make( + array $pairs = [] +) +{ + return (new struct_subject_map($pairs)); +} + + + +/** + * @template type_key + * @template type_value + * @param struct_subject_map $subject + * @param type_key $key + * @return bool + */ +function map_has( + \Closure $collate_key, + struct_subject_map $subject, + $key +) : bool +{ + foreach ($subject->pairs as $pair) { + if ($collate_key($key, $pair->first)) { + return true; + } + } + return false; +} + + +/** + * @template type_key + * @template type_value + * @param struct_subject_map $subject + * @param type_key $key + * @return type_value + * @throws \Exception if not found + */ +function map_get( + \Closure $collate_key, + struct_subject_map $subject, + $key +) +{ + foreach ($subject->pairs as $pair) { + if ($collate_key($key, $pair->first)) { + return $pair->second; + } + } + throw (new \Exception('not found')); +} + + +/** + * @template type_key + * @template type_value + * @param struct_subject_map $subject + * @param type_key $key + * @param type_value $value + */ +function map_set( + \Closure $collate_key, + struct_subject_map $subject, + $key, + $value +) : void +{ + $found = false; + foreach ($subject->pairs as &$pair) { + if ($collate_key($key, $pair->first)) { + $pair->second = $value; + $found = true; + break; + } + } + unset($pair); + if (! $found) { + $pair = \alveolata\structures\pair_make($key, $value); + array_push($subject->pairs, $pair); + } +} + + +/** + * @template type_key + * @template type_value + * @param struct_subject_map $subject + * @param \Closure $procedure {function<§type_key,§type_value,void>} + */ +function map_iterate( + struct_subject_map $subject, + \Closure $procedure +) : void +{ + foreach ($subject->pairs as $pair) { + $procedure($pair->first, $pair->second); + } +} + + ?> diff --git a/lib/alveolata/structures/map/test.spec.php b/lib/alveolata/structures/map/test.spec.php new file mode 100644 index 0000000..476a109 --- /dev/null +++ b/lib/alveolata/structures/map/test.spec.php @@ -0,0 +1,43 @@ + 'alveolata', + 'sections' => [ + [ + 'name' => 'structures', + 'sections' => [ + [ + 'name' => 'map', + 'sections' => [ + [ + 'name' => 'set_get', + 'cases' => [ + [ + 'name' => 'test', + 'procedure' => function ($assert) { + $map = \alveolata\structures\class_map::make( + function ($x, $y) {return ($x === $y);} + ); + $map->set(2, 'zwei'); + $map->set(3, 'drei'); + $map->set(5, 'fünf'); + $assert->runs(function () use ($map) {return $map->get(3);}); + $assert->equal($map->get(3), 'drei'); + $assert->crashes(function () use ($map) {return $map->get(4);}); + } + ], + ] + ], + ] + ], + ] + ] + ] + ] +); + diff --git a/lib/alveolata/structures/map/wrapper-class.php b/lib/alveolata/structures/map/wrapper-class.php new file mode 100644 index 0000000..e734a11 --- /dev/null +++ b/lib/alveolata/structures/map/wrapper-class.php @@ -0,0 +1,58 @@ +} + */ + private $collate_key; + + + /** + * @var struct_subject_map + */ + private $subject; + + + /** + * @param struct_subject_map + */ + public function __construct( + \Closure $collate_key, + struct_subject_map $subject + ) + { + $this->collate_key = $collate_key; + $this->subject = $subject; + } + + + /** + * @param array $pairs {list<&struct_subject_pair<§type_key,§type_value>>} + */ + public static function make( + \Closure $collate_key, + array $pairs = [] + ) : class_map + { + return (new class_map($collate_key, map_make($pairs))); + } + + + public function get($key) {return map_get($this->collate_key, $this->subject, $key);} + public function set($key, $value) : void {map_set($this->collate_key, $this->subject, $key, $value);} + public function iterate(\Closure $procedure) : void {map_iterate($this->subject, $procedure);} + +} + + ?> diff --git a/lib/alveolata/structures/pair/functions.php b/lib/alveolata/structures/pair/functions.php new file mode 100644 index 0000000..cec0dd5 --- /dev/null +++ b/lib/alveolata/structures/pair/functions.php @@ -0,0 +1,57 @@ +first = $first; + $this->second = $second; + } + +} + + +/** + * @template type_first + * @template type_second + * @param type_first $first + * @param type_second $second + * @return struct_subject_pair + */ +function pair_make( + $first, + $second +) { + return ( + new struct_subject_pair( + $first, + $second + ) + ); +} + + + ?> diff --git a/lib/alveolata/term/functions.php b/lib/alveolata/term/functions.php new file mode 100644 index 0000000..baa5850 --- /dev/null +++ b/lib/alveolata/term/functions.php @@ -0,0 +1,129 @@ + + */ +function to_string( + interface_term $term +) : string +{ + if ($term instanceof class_variable) { + $variable = $term; + return sprintf('$%s', $variable->name); + } + else if ($term instanceof class_function) { + $function = $term; + return sprintf( + '%s(%s)', + $function->head, + implode( + ',', + \alveolata\list_\map( + $function->arguments, + function ($argument) { + return to_string($argument); + } + ) + ) + ); + } + else { + throw (new \Exception('unhandled')); + } +} + + +/** + * @param interface_term $term1 + * @param interface_term $term2 + * @return bool + * @author Christian Fraß + */ +function equal( + interface_term $term1, + interface_term $term2 +) : bool +{ + if ($term1 instanceof class_variable) { + if ($term2 instanceof class_variable) { + return ($term1->name === $term2->name); + } + else if ($term2 instanceof class_function) { + return false; + } + else { + throw (new \Exception('unhandled')); + } + } + else if ($term1 instanceof class_function) { + if ($term2 instanceof class_variable) { + return false; + } + else if ($term2 instanceof class_function) { + return ( + ($term1->head === $term2->head) + && + (count($term1->arguments) === count($term2->arguments)) + && + \alveolata\list_\every( + \alveolata\list_\sequence(count($term1->arguments)), + function ($index) use (&$term1, &$term2) { + return equal($term1->arguments[$index], $term2->arguments[$index]); + } + ) + ); + } + else { + throw (new \Exception('unhandled')); + } + } + else { + throw (new \Exception('unhandled')); + } +} + + +/** + * @param interface_term $term1 + * @param interface_term $term2 + * @return bool + * @author Christian Fraß + */ +function contains( + interface_term $term1, + interface_term $term2 +) : bool +{ + if (equal($term1, $term2)) { + return true; + } + else { + if ($term1 instanceof class_variable) { + return false; + } + else if ($term1 instanceof class_function) { + return \alveolata\list_\some( + $term1->arguments, + function ($argument) use (&$term2) { + return contains($argument, $term2); + } + ); + } + else { + throw (new \Exception('unhandled')); + } + } +} + + ?> diff --git a/lib/alveolata/term/implementation-function.php b/lib/alveolata/term/implementation-function.php new file mode 100644 index 0000000..5ca0965 --- /dev/null +++ b/lib/alveolata/term/implementation-function.php @@ -0,0 +1,64 @@ + + */ +class class_function implements interface_term +{ + + /** + * @var string + * @author Christian Fraß + */ + public $head; + + + /** + * @var list + * @author Christian Fraß + */ + public $arguments; + + + /** + * @param string $head + * @param list $arguments + * @author Christian Fraß + */ + public function __construct( + string $head, + array $arguments = [] + ) + { + $this->head = $head; + $this->arguments = $arguments; + } + + + /** + * @implementation + * @author Christian Fraß + */ + public function clone_( + ) : interface_term + { + return ( + new class_function( + $this->head, + \alveolata\list_\map( + $this->arguments, + function ($argument) {return $argument->clone_();} + ) + ) + ); + } + +} + diff --git a/lib/alveolata/term/implementation-variable.php b/lib/alveolata/term/implementation-variable.php new file mode 100644 index 0000000..af72660 --- /dev/null +++ b/lib/alveolata/term/implementation-variable.php @@ -0,0 +1,49 @@ + + */ +class class_variable implements interface_term +{ + + /** + * @var string + * @author Christian Fraß + */ + public $name; + + + /** + * @param string $name + * @author Christian Fraß + */ + public function __construct( + string $name + ) + { + $this->name = $name; + } + + + /** + * @implementation + * @author Christian Fraß + */ + public function clone_( + ) : interface_term + { + return ( + new class_variable( + $this->name + ) + ); + } + +} + diff --git a/lib/alveolata/term/interface.php b/lib/alveolata/term/interface.php new file mode 100644 index 0000000..3250c12 --- /dev/null +++ b/lib/alveolata/term/interface.php @@ -0,0 +1,20 @@ + + */ +interface interface_term +{ + + /** + * @author Christian Fraß + */ + function clone_( + ) : interface_term + ; + +} + diff --git a/lib/alveolata/term/substitution.php b/lib/alveolata/term/substitution.php new file mode 100644 index 0000000..567e52b --- /dev/null +++ b/lib/alveolata/term/substitution.php @@ -0,0 +1,354 @@ + + * @author Christian Fraß + */ + public $mapping; + + + /** + * @param map + * @author Christian Fraß + */ + public function __construct( + array $mapping + ) + { + $this->mapping = $mapping; + } + +} + + +/** + * @author Christian Fraß + */ +function _substitution_clone( + struct_substitution $substitution +) : struct_substitution +{ + $mapping_ = []; + foreach ($substitution->mapping as $name => $term) { + $mapping[$name] = $term->clone_(); + } + return (new struct_substitution($mapping_)); +} + + +/** + * @return set + * @author Christian Fraß + */ +function _substitution_domain( + struct_substitution $substitution +) : array +{ + return array_keys($substitution->mapping); +} + + +/** + * @param set $names + * @author Christian Fraß + */ +function _substitution_restricted( + struct_substitution $substitution, + array $set +) : struct_substitution +{ + $mapping = []; + foreach ($substitution->mapping as $name => $term) { + if (in_array($name, $set)) { + $mapping[$name] = $term; + } + } + return (new struct_substitution($mapping)); +} + + +/** + * @author Christian Fraß + */ +function substitution_instance( + struct_substitution $substitution, + interface_term $term +) : interface_term +{ + if ($term instanceof class_variable) { + $variable = $term; + return ( + array_key_exists($variable->name, $substitution->mapping) + ? $substitution->mapping[$variable->name] + : $term->clone_() + ); + } + else if ($term instanceof class_function) { + $function = $term; + return ( + new class_function( + $function->head, + \alveolata\list_\map( + $function->arguments, + function ($argument) use (&$substitution) { + return substitution_instance($substitution, $argument); + } + ) + ) + ); + } + else { + throw (new \Exception('invalid term')); + } +} + + +/** + * @author Christian Fraß + */ +function _substitution_compose( + struct_substitution $substitution1, + struct_substitution $substitution2 +) : struct_substitution +{ + $substitution3 = _substitution_clone($substitution1); + $domain1 = _substitution_domain($substitution1); + // überschreiben der vorhandenen Zuordnungen und entfernen der Identitäten + { + foreach ($domain1 as $name) { + $term = substitution_instance($substitution2, $substitution1->mapping[$name]); + if (equal(new class_variable($name), $term)) { + unset($substitution3->mapping[$name]); + } + else { + $substitution3->mapping[$name] = $term; + } + } + } + // hinzufügen der neuen Ersetzungen + { + $domain2 = _substitution_domain($substitution2); + foreach ($domain2 as $name) { + if (! in_array($name, $domain1)) { + $substitution3->mapping[$name] = $substitution2->mapping[$name]; + } + } + } + return $substitution3; +} + + +/** + * @author Christian Fraß + */ +function _substitution_empty( +) : struct_substitution +{ + return (new struct_substitution([])); +} + + +/** + * ermittelt die anzuwendenen Substiutionen und die neuen Term-Paare für ein gegebenes Term-Paar + * + * @param record $pair + * @return record,pairs:list>> + */ +function _unificationstep( + array $pair +) : array +{ + $nothing = [ + 'substitutions' => [], + 'pairs' => [], + ]; + if ($pair['first'] instanceof class_variable) { + $x_variable = $pair['first']; + if ($pair['second'] instanceof class_variable) { + $y_variable = $pair['second']; + return [ + 'substitutions' => \alveolata\list_\map( + [ + ['from' => $x_variable, 'to' => $y_variable], + ['from' => $y_variable, 'to' => $x_variable], + ], + function (array $satz) : struct_substitution { + $mapping = []; + if (! ($satz['from']->name === $satz['to']->name)) { + $mapping[$satz['from']->name] = $satz['to']; + } + else { + // do nothing + } + return (new struct_substitution($mapping)); + } + ), + 'pairs' => [], + ]; + } + else { + if (contains($pair['second'], $x_variable)) { + return $nothing; + } + else { + return [ + 'substitutions' => [ + new struct_substitution([$x_variable->name => $pair['second']]), + ], + 'pairs' => [], + ]; + } + } + } + else if ($pair['first'] instanceof class_function) { + $x_function = $pair['first']; + if ($pair['second'] instanceof class_variable) { + return [ + 'substitutions' => [_substitution_empty()], + 'pairs' => ['first' => $pair['second'], 'second' => $pair['first']], + ]; + } + else if ($pair['second'] instanceof class_function) { + $y_function = $pair['second']; + if ($x_function->head === $y_function->head) { + if (count($x_function->arguments) === count($y_function->arguments)) { + return [ + 'substitutions' => [_substitution_empty()], + 'pairs' => \alveolata\list_\zip( + $x_function->arguments, + $y_function->arguments, + false + ), + ]; + } + else { + throw (new \Exception('ungleiche Anzahl an Argumenten bei Funktionen mit gleichem Kopf')); + } + } + else { + return $nothing; + } + } + else { + throw (new \Exception('ungültiger Term')); + } + } + else { + throw (new \Exception('ungültiger Term')); + } +} + + +/** + * ermittelt die Menge aller allgemeinsten unifiers + * + * @param list> $problem + * @param int [$depth] + * @return list + * @author Christian Fraß + */ +function unifiers( + array $problem, + int $depth = 0 +) : array +{ + $solution = null; + if (empty($problem)) { + $solution = [_substitution_empty()]; + } + else { + $components = [ + 'head' => $problem[0], + 'rest' => array_slice($problem, 1), + ]; + $step = _unificationstep($components['head']); + $solution = \alveolata\list_\reduce( + $step['substitutions'], + [], + function (array $solution_, struct_substitution $substitution) use ($depth, &$components, &$step) { + $rest = \alveolata\list_\map( + $components['rest'], + function ($pair) use (&$substitution) { + return [ + 'first' => substitution_instance($substitution, $pair['first']), + 'second' => substitution_instance($substitution, $pair['second']), + ]; + } + ); + $subsolution = unifiers( + \alveolata\list_\concat($step['pairs'], $rest), + $depth+1 + ); + return \alveolata\list_\concat( + $solution_, + \alveolata\list_\map( + $subsolution, + function ($substitution_) use (&$substitution) { + return _substitution_compose($substitution, $substitution_); + } + ) + ); + } + ); + } + \alveolata\log\debug( + 'unification', + [ + 'problem' => implode( + ' + ', + \alveolata\list_\map( + $problem, + function ($pair) { + return sprintf( + '[%s ~ %s]', + to_string($pair['first']), + to_string($pair['second']) + ); + } + ) + ), + 'solution' => $solution, + ] + ); + return $solution; +} + + +/** + * @author Christian Fraß + */ +function unifier( + interface_term $x, + interface_term $y +) : struct_substitution +{ + return ( + unifiers( + [ + [ + 'first' => $x, + 'second' => $y + ] + ] + )[0] + ); +} + + diff --git a/lib/alveolata/term/test.spec.php b/lib/alveolata/term/test.spec.php new file mode 100644 index 0000000..d2b9c98 --- /dev/null +++ b/lib/alveolata/term/test.spec.php @@ -0,0 +1,106 @@ + 'alveolata', + 'sections' => [ + [ + 'name' => 'term', + 'setup' => function (&$environment) { + // foo(x,y) + $environment['x'] = new \alveolata\term\class_function( + 'foo', + [ + new \alveolata\term\class_variable('x'), + new \alveolata\term\class_variable('y'), + ] + ); + // foo(bar(),z) + $environment['y'] = new \alveolata\term\class_function( + 'foo', + [ + new \alveolata\term\class_function('bar', []), + // new \alveolata\term\class_function('baz', []), + new \alveolata\term\class_variable('z'), + ] + ); + }, + 'sections' => [ + [ + 'name' => 'substitution-instance', + 'cases' => [ + [ + 'name' => 'test', + 'procedure' => function ($assert, &$environment) { + $substitution = new \alveolata\term\struct_substitution( + [ + 'x' => new \alveolata\term\class_function('qux', []), + ] + ); + $x_actual = \alveolata\term\substitution_instance($substitution, $environment['x']); + $x_expected = new \alveolata\term\class_function( + 'foo', + [ + new \alveolata\term\class_function('qux', []), + new \alveolata\term\class_variable('y'), + ] + ); + $assert->is(\alveolata\term\equal($x_expected, $x_actual)); + }, + ], + ], + ], + [ + 'name' => 'unifiers', + 'cases' => [ + [ + 'name' => 'test', + 'procedure' => function ($assert, &$environment) { + $substitutions = \alveolata\term\unifiers( + [ + [ + 'first' => $environment['x'], + 'second' => $environment['y'], + ] + ] + ); + $assert->equal(count($substitutions), 2); + foreach ($substitutions as $substitution) { + $assert->equal(count($substitution->mapping), 2); + $assert->is( + \alveolata\term\equal( + $substitution->mapping['x'], + new \alveolata\term\class_function('bar', []), + ) + ); + if (array_key_exists('y', $substitution->mapping)) { + $assert->is( + \alveolata\term\equal( + $substitution->mapping['y'], + new \alveolata\term\class_variable('z', []), + ) + ); + } + if (array_key_exists('z', $substitution->mapping)) { + $assert->is( + \alveolata\term\equal( + $substitution->mapping['z'], + new \alveolata\term\class_variable('y', []), + ) + ); + } + } + }, + ], + ], + ], + ], + ] + ] + ] +); + diff --git a/lib/alveolata/test.php b/lib/alveolata/test.php new file mode 100644 index 0000000..6ba12ee --- /dev/null +++ b/lib/alveolata/test.php @@ -0,0 +1,985 @@ + + */ +function _fatal_error( + string $message +) : void +{ + // throw (new \Exception($message)); + error_log(sprintf('FATAL ERROR: %s', $message)); + exit(1); +} + + +/** + * @author Christian Fraß + */ +function _string_coin( + string $template, + array $arguments = [] +) : string +{ + $result = $template; + foreach ($arguments as $key => $value) { + $pattern = sprintf('{{%s}}', $key); + $replacement = strval($value); + $result = str_replace($pattern, $replacement, $result); + } + return $result; +} + + +/** + * @author Christian Fraß + */ +function _list_filter( + array $list, + \Closure $predicate +) : array +{ + return array_values(array_filter($list, $predicate)); +} + + +/** + * @author Christian Fraß + */ +function _colorize( + string $string, + string $color, + string $mode = 'foreground' +) : string +{ + $map = [ + 'foreground' => [ + 'blue' => '34', + 'green' => '32', + 'purple' => '35', + 'red' => '31', + 'white' => '37', + 'yellow' => '33', + ], + 'background' => [ + 'blue' => '44', + 'green' => '42', + 'purple' => '45', + 'red' => '41', + 'white' => '47', + 'yellow' => '43', + ], + ]; + return _string_coin( + "\033[{{colorcode}}m{{string}}\033[0m", + [ + 'colorcode' => $map[$mode][$color], + 'string' => $string, + ] + ); +} + + +/** + * @author Christian Fraß + */ +function _log( + int $level, + string $message +) : void +{ + error_log( + _string_coin( + '{{indentation}}{{message}}', + [ + 'indentation' => str_repeat(" ", $level-1), + 'message' => $message, + ] + ) + ); +} + + +/** + * @author Christian Fraß + */ +class struct_case +{ + public $name; + public $active; + public $procedure; + public function __construct( + string $name, + bool $active, + \Closure $procedure + ) + { + $this->name = $name; + $this->active = $active; + $this->procedure = $procedure; + } +} + + +/** + * @author Christian Fraß + */ +class struct_section +{ + public $name; + public $active; + public $setup; + public $cleanup; + public $subsections; + public $cases; + public function __construct( + string $name, + bool $active, + \Closure $setup, + \Closure $cleanup, + array $subsections, + array $cases + ) + { + $this->name = $name; + $this->active = $active; + $this->setup = $setup; + $this->cleanup = $cleanup; + $this->subsections = $subsections; + $this->cases = $cases; + } +} + + +/** + * @author Christian Fraß + */ +class struct_state +{ + public $name; + public $sign; + public $color; + public function __construct( + $name, + $sign, + $color + ) + { + $this->name = $name; + $this->sign = $sign; + $this->color = $color; + } + public static $skipped = null; + public static $aborted = null; + public static $failed = null; + public static $passed = null; + public static function init( + ) : void + { + self::$skipped = new static('skipped', 'o', 'yellow'); + self::$aborted = new static('aborted', '!', 'purple'); + self::$failed = new static('failed', 'x', 'red'); + self::$passed = new static('passed', '+', 'green'); + } +} +struct_state::init(); + + +/** + * @author Christian Fraß + */ +class struct_report +{ + public /*array */$path; + public /*struct_state */$state; + public /*string */$messages; + public function __construct( + array $path, + struct_state $state, + array $messages + ) + { + $this->path = $path; + $this->state = $state; + $this->messages = $messages; + } +} + + +/** + * @author Christian Fraß + */ +class class_assertion +{ + + /** + * @var function<~string,void> + * @author Christian Fraß + */ + private $finalize; + + + /** + * @author Christian Fraß + */ + public function __construct( + \Closure $finalize + ) + { + $this->finalize = $finalize; + } + + + /** + * @author Christian Fraß + */ + public function not( + ) : class_assertion + { + return ( + new class_assertion( + function (/*?string */$message) : void { + if (is_null($message)) { + ($this->finalize)('should NOT be'); + } + else { + ($this->finalize)(null); + } + } + ) + ); + } + + + /** + * unspecific explicit fail + * + * @author Christian Fraß + */ + public function fail( + $reason = null + ) + { + $message = ($reason ?: '(unexplained fail)'); + ($this->finalize)($message); + } + + + /** + * unspecific generic test + * + * @author Christian Fraß + */ + public function is( + bool $value + ) + { + $message = ($value ? null : 'condition not met'); + ($this->finalize)($message); + } + + + /** + * tests whether the piece of code runs without throwing an error + * + * @param \Closure $procedure + * @author Christian Fraß + */ + public function runs( + \Closure $procedure + ) + { + try { + $procedure(); + $message = null; + } + catch (\Throwable $throwable) { + $message = _string_coin( + 'does not run; reason: {{reason}}', + [ + 'reason' => $throwable->getMessage(), + ] + ); + } + ($this->finalize)($message); + } + + + /** + * tests whether the piece of code runs with throwing an error + * + * @param \Closure $procedure + * @author Christian Fraß + */ + public function crashes( + \Closure $procedure, + ?string $throwable_class = null + ) + { + try { + $procedure(); + $message = _string_coin( + 'does run though it is not supposed to do', + [ + ] + ); + } + catch (\Throwable $throwable) { + if ( + (is_null($throwable_class)) + || + (get_class($throwable) === $throwable_class) + ) { + $message = null; + } + else { + $message = _string_coin( + 'wrong throwable class: [actual] {{actual}}; [expected] {{expected}}', + [ + 'actual' => get_class($throwable), + 'expected' => $throwable_class, + ] + ); + } + } + ($this->finalize)($message); + } + + + /** + * tests whether two values are equal + * + * @param any $value_actual + * @param any $value_expected + * @author Christian Fraß + */ + public function equal( + $value_actual, + $value_expected + ) + { + if ($value_actual === $value_expected) { + $message = null; + } + else { + $message = _string_coin( + '[actual] {{actual}}; [expected] {{expected}}', + [ + 'actual' => json_encode($value_actual), + 'expected' => json_encode($value_expected), + ] + ); + } + ($this->finalize)($message); + } + +} + + +/** + * @var struct_section + * @author Christian Fraß + */ +$_root = null; + + + +/** + * merges two sections + * + * @author Christian Fraß + */ +function _merge( + struct_section $section1, + struct_section $section2 +) : struct_section +{ + if (! ($section1->name === $section2->name)) { + throw (new \Exception('sections with different names may not be merged')); + } + else { + if (! ($section1->active === $section2->active)) { + throw (new \Exception('sections with different active states may not be merged')); + } + else { + $subsections = []; + foreach ([$section1, $section2] as $section) { + foreach ($section->subsections as $subsection_name => $subsection) { + if (! array_key_exists($subsection_name, $subsections)) { + $subsections[$subsection_name] = $subsection; + } + else { + $subsections[$subsection_name] = _merge( + $subsections[$subsection_name], + $subsection + ); + } + } + } + $section_merged = ( + new struct_section( + $section1->name, + $section1->active, + // TODO: subenvironments? + function (&$environment) use ($section1, $section2) : void { + ($section1->setup)($environment); + ($section2->setup)($environment); + }, + function (&$environment) use ($section1, $section2) : void { + ($section1->cleanup)($environment); + ($section2->cleanup)($environment); + }, + $subsections, + array_merge( + $section1->cases, + $section2->cases + ) + ) + ); + return $section_merged; + } + } +} + + +/** + * adds a regular test definition + * + * @author Christian Fraß + */ +function _add( + struct_section &$section_parent, + array $section_raw_given +) +{ + $section_raw_default = [ + 'name' => '?', + 'active' => true, + 'setup' => (function (&$environment) {}), + 'cleanup' => (function (&$environment) {}), + 'sections' => [], + 'cases' => [], + ]; + $section_raw = array_merge( + $section_raw_default, + $section_raw_given + ); + $section = new struct_section( + $section_raw['name'], + $section_raw['active'], + $section_raw['setup'], + $section_raw['cleanup'], + [], + [] + ); + // sections + { + foreach ($section_raw['sections'] as $section_sub_raw) { + _add($section, $section_sub_raw); + } + } + // cases + { + foreach ($section_raw['cases'] as $case_raw_given) { + $case_raw_default = [ + 'name' => '?', + 'active' => true, + 'procedure' => function ($assert, &$environment) { + $assert->fail('not implemented'); + }, + ]; + $case_raw = array_merge($case_raw_default, $case_raw_given); + $case = new struct_case( + $case_raw['name'], + $case_raw['active'], + $case_raw['procedure'] + ); + array_push($section->cases, $case); + } + } + // array_push($section_parent->subsections, $section); + { + if (array_key_exists($section->name, $section_parent->subsections)) { + $section_parent->subsections[$section->name] = _merge( + $section_parent->subsections[$section->name], + $section + ); + } + else { + $section_parent->subsections[$section->name] = $section; + } + } +} + + +/** + * adds a json based test definition + * + * @author Christian Fraß + */ +function _adapt( + string $spec_path +) +{ + global $_root; + $steps = explode('/', $spec_path); + $paths_to_try = [ + sprintf('%s/%s.php', implode('/', array_slice($steps, 0, count($steps)-1)), $steps[count($steps)-1]), + sprintf('%s/functions.php', implode('/', array_slice($steps, 0, count($steps)-1))), + ]; + foreach ($paths_to_try as $path) { + if (file_exists($path)) { + require_once($path); + break; + } + } + $spec = json_decode(file_get_contents(sprintf('%s.spec.json', $spec_path)), true); + if (empty($spec)) { + $message = _string_coin( + 'json based test definition "{{path}}" malformed', + [ + 'path' => $spec_path, + ] + ); + throw (new \Exception($message)); + } + _add( + $_root, + [ + 'name' => $spec_path, + 'active' => $spec['active'], + 'sections' => array_map( + function ($section_raw) { + return [ + 'name' => $section_raw['name'], + 'active' => $section_raw['active'], + 'cases' => array_map( + function ($case_raw) use ($section_raw) { + return [ + 'name' => $case_raw['name'], + 'active' => $case_raw['active'], + 'procedure' => function ($assert, $environment) use ($section_raw, $case_raw) { + $expression = _string_coin( + $section_raw['execution']['call'], + array_map( + function ($value) { + return var_export($value, true); + }, + $case_raw['input'] + ) + ); + switch ($case_raw['output']['kind']) { + case 'runs': { + $assert->runs( + function () use (&$expression) { + eval(sprintf('return %s;', $expression)); + } + ); + break; + } + case 'crashes': { + $assert->crashes( + function () use (&$expression) { + eval(sprintf('return %s;', $expression)); + } + ); + break; + } + case 'regular': { + $result_actual = eval(sprintf('return %s;', $expression)); + $result_expected = $case_raw['output']['value']; + $assert->equal($result_actual, $result_expected); + break; + } + default: { + $message = _string_coin( + 'unhadled output kind "{{kind}}"', + [ + 'kind' => $case['output']['kind'] + ] + ); + throw (new \Exception($message)); + break; + } + } + } + ]; + }, + $section_raw['cases'] + ) + ]; + }, + $spec['sections'] + ), + ] + ); +} + + +/** + * @param array $path + * @return struct_section section + * @author Christian Fraß + */ +function _get( + array $path, + struct_section $section = null, + array $path_complete = null +) : ?struct_section +{ + global $_root; + if ($section === null) { + $section = $_root; + } + if ($path_complete === null) { + $path_complete = $path; + } + if (count($path) === 0) { + return $section; + } + else { + $name = $path[0]; + foreach ($section->subsections as $section_sub_name => $section_sub) { + if ($section_sub->name === $name) { + return _get( + array_slice($path, 1), + $section_sub, + $path_complete + ); + } + } + $message = _string_coin( + 'no test section "{{path}}"', + [ + 'path' => implode('.', $path_complete), + ] + ); + throw (new \Exception($message)); + } +} + + +/** + * @param struct_section $section + * @param bool $log + * @return list + * @author Christian Fraß + */ +function _execute( + struct_section $section = null, + bool $log = true, + array $path = [], + &$environment = [], + $active = true +) : array +{ + global $_root; + if ($section === null) { + $section = $_root; + } + if (! $active) { + return []; + } + else { + $level = count($path); + $reports = []; + if (count($path) > 0) { + if ($log) { + _log( + $level, + _string_coin( + '<{{name}}>', + [ + 'name' => $section->name, + ] + ) + ); + } + } + ($section->setup)($environment); + foreach ($section->subsections as $section_sub_name => &$section_sub) { + $reports_sub = _execute( + $section_sub, + $log, + array_merge($path, [$section_sub->name]), + $environment, + $active && $section_sub->active + ); + $reports = array_merge($reports, $reports_sub); + } + unset($section_sub); + foreach ($section->cases as &$case) { + $messages = []; + if (!$active || !$case->active) { + $state = struct_state::$skipped; + $messages = []; + } + else { + $state = struct_state::$passed; + $assert = new class_assertion( + function ($message) use (&$state, &$messages) { + if ($message === null) { + // do nothing + } + else { + array_push($messages, $message); + $state = struct_state::$failed; + } + } + ); + try { + ($case->procedure)($assert, $environment); + } + catch (\Throwable $throwable) { + $state = struct_state::$aborted; + $message = _string_coin( + "{{description}} in {{file}}:{{line}}", + [ + 'description' => $throwable->getMessage(), + 'file' => $throwable->getFile(), + 'line' => $throwable->getLine(), + 'stacktrace' => $throwable->getTraceAsString(), + ] + ); + array_push($messages, $message); + } + } + $report = new struct_report( + array_merge($path, [$case->name]), + $state, + $messages + ); + if ($log) { + _log( + $level+1, + _colorize( + _string_coin( + '[{{indicator}}] {{name}}', + [ + 'indicator' => $state->sign, + // 'indicator' => _colorize($state->sign, $state->color), + // 'indicator' => _colorize('x', $state->color, 'background'), + // 'indicator' => _colorize('x', $state->color, 'foreground'), + 'name' => $case->name, + ] + ), + $state->color + ) + ); + } + array_push($reports, $report); + } + unset($case); + ($section->cleanup)($environment); + return $reports; + } +} + + +/** + * searching and adding test definitions + * + * @author Christian Fraß + */ +function _gather( +) : void +{ + global $_root; + // .spec.php + { + exec( + 'find . -name "*.spec.php" | cut -d "/" -f "2-"', + $paths + ); + foreach ($paths as $path) { + include($path); + } + } + // .spec.json + { + exec( + 'find . -name "*.spec.json" | sed -e "s:.spec.json::g" | cut -d "/" -f "2-"', + $spec_paths + ); + foreach ($spec_paths as $spec_path) { + _adapt($spec_path); + } + } +} + + +/** + * @param list + * @return record + * @author Christian Fraß + */ +function _evaluation( + array $reports +) : array +{ + $counts = [ + struct_state::$skipped->name => 0, + struct_state::$aborted->name => 0, + struct_state::$failed->name => 0, + struct_state::$passed->name => 0, + ]; + foreach ($reports as $report) { + $counts[$report->state->name] += 1; + } + $count_total = count($reports); + $apt = ($counts[struct_state::$skipped->name] + $counts[struct_state::$passed->name] === $count_total); + $script = ''; + $script .= ('--------' . "\n"); + { + $script .= ('# summary' . "\n"); + $script .= ('' . "\n"); + if ($count_total === 0) { + $script .= ('(no tests found)' . "\n"); + } + else { + $name = (struct_state::$passed)->name; + $share = (1.0 * $counts[$name] / $count_total); + $script .= ( + _string_coin( + '{{count_passed}}/{{count_total}} passed ({{percentage}}%)' . "\n", + [ + 'count_passed' => sprintf('%d', $counts[struct_state::$passed->name]), + 'count_total' => sprintf('%d', $count_total), + 'percentage' => sprintf('%.2f', $share*100), + ] + ) + ); + } + $script .= ('' . "\n"); + $script .= ('' . "\n"); + } + { + $states = [ + struct_state::$skipped, + struct_state::$failed, + struct_state::$aborted, + ]; + foreach ($states as $state) { + $reports_filtered = _list_filter( + $reports, + function ($report) use (&$state) { + return ($report->state === $state); + } + ); + if (count($reports_filtered) > 0) { + $script .= ( + _string_coin( + '# {{name}}' . "\n", + [ + 'name' => $state->name + ] + ) + ); + $script .= ('' . "\n"); + foreach ($reports_filtered as $report) { + $script .= ( + _string_coin( + '- `{{path}}`' . "\n", + [ + 'path' => implode('.', $report->path), + ] + ) + ); + // $script .= ('' . "\n"); + foreach ($report->messages as $message) { + $script .= ( + _string_coin( + "\t" . '- {{message}}' . "\n", + [ + 'message' => $message, + ] + ) + ); + } + } + $script .= ('' . "\n"); + $script .= ('' . "\n"); + } + } + } + return [ + 'apt' => $apt, + 'script' => $script, + ]; +} + + +/** + * @author Christian Fraß + */ +function get_data( + string $path = null +) : array +{ + if (is_null($path)) { + $directory = dirname(debug_backtrace()[0]['file']); + $path = ($directory . '/testdata.json'); + } + $content = file_get_contents($path); + if ($content === false) { + $message = sprintf('testdata file not readable: %s', $path); + _fatal_error($message); + } + else { + $data = json_decode($content, true); + if (json_last_error() !== JSON_ERROR_NONE) { + $message = sprintf('testdata json not decodable: %s', $path); + _fatal_error($message); + } + else { + return $data; + } + } +} + + +/** + * adds a regular test definition + * + * @author Christian Fraß + */ +function add( + array $section_raw +) : void +{ + global $_root; + _add($_root, $section_raw); +} + + +/** + * searches for test definitions, executes all the tests the selected branch and writes the summary to stderr + * + * @return true if all non skipped tests have passed; false if there are aborted or failing tests + * @author Christian Fraß + */ +function run( + array $path = [], + bool $log = true +) : bool +{ + global $_root; + $_root = new struct_section( + 'root', + true, + function () {}, + function () {}, + [], + [] + ); + _gather(); + $section = _get($path); + $reports = _execute($section, $log); + $evaluation = _evaluation($reports); + error_log($evaluation['script']); + return $evaluation['apt']; +} + diff --git a/lib/alveolata/tools/functions.php b/lib/alveolata/tools/functions.php new file mode 100644 index 0000000..ad79386 --- /dev/null +++ b/lib/alveolata/tools/functions.php @@ -0,0 +1,65 @@ + + */ +function input( + string $message +) +{ + error_log($message); + $handle = fopen('php://stdin','r'); + $input = fgets($handle); + return $input; +} + + +/** + * @author Christian Fraß + */ +function confirm( + string $message +) +{ + return (trim(input($message . ' [y/n]')) === 'y'); +} + + +/** + * halts the excution until ENTER is pressed + * + * @author Christian Fraß + */ +function pause( +) +{ + error_log('-- PAUSED -- press to continue'); + $handle = fopen('php://stdin','r'); + fgets($handle); +} + + +/** + * wraps a procedure with try-catch + * + * @author Christian Fraß + */ +function soften( + \Closure $procedure, + bool $silent = false +) : void +{ + try { + $procedure(); + } + catch (\Throwable $throwable) { + if (! $silent) { + error_log(strval($throwable)); + } + } +} + + ?> diff --git a/lib/alveolata/type/functions.php b/lib/alveolata/type/functions.php new file mode 100644 index 0000000..a43bde2 --- /dev/null +++ b/lib/alveolata/type/functions.php @@ -0,0 +1,539 @@ + $field_raw['name'], + 'type' => make($field_raw['type']), + 'mandatory' => ( + array_key_exists('mandatory', $field_raw) + ? $field_raw['mandatory'] + : true + ) + ]; + }, + $description['data']['fields'] + ) + )); + break; + } + default: { + throw (new \Exception(sprintf('unhandled type kind "%s"', $description['kind']))); + break; + } + } +} + + +/** + * @param interface_type $type + * @param mixed $value {any} + * @return array {list} + */ +function investigate( + interface_type $type, + $value +) : array +{ + return array_map( + function (struct_flaw $flaw) : string { + return flaw_to_string($flaw); + }, + $type->investigate($value) + ); +} + + +/** + * returns whether a given value has the type + * + * @param interface_type $type + * @param mixed $value {any} + * @return bool {boolean} + */ +function check( + interface_type $type, + $value +) : bool +{ + return empty($type->investigate($value)); +} + + +/** + * shall return a representation of the type as term + * + * @return \alveolata\term\interface_term + */ +function to_term( + interface_type $type +) : \alveolata\term\interface_term +{ + if ($type instanceof class_var) { + $var = $type; + return ( + new \alveolata\term\class_variable( + $var->name + ) + ); + } + else if ($type instanceof class_boolean) { + return ( + new \alveolata\term\class_function( + 'boolean' + ) + ); + } + else if ($type instanceof class_integer) { + return ( + new \alveolata\term\class_function( + 'integer' + ) + ); + } + else if ($type instanceof class_float) { + return ( + new \alveolata\term\class_function( + 'float' + ) + ); + } + else if ($type instanceof class_string) { + return ( + new \alveolata\term\class_function( + 'string' + ) + ); + } + else if ($type instanceof class_list) { + return ( + new \alveolata\term\class_function( + 'list', + [ + to_term($type->type_element), + ] + ) + ); + } + else if ($type instanceof class_map) { + return ( + new \alveolata\term\class_function( + 'map', + [ + to_term($type->type_key), + to_term($type->type_value), + ] + ) + ); + } + else if ($type instanceof class_record) { + $record = $type; + return ( + new \alveolata\term\class_function( + 'record', + \alveolata\list_\map( + $record->fields, + function ($field) { + return ( + new \alveolata\term\class_function( + 'field', + [ + new \alveolata\term\class_function( + $field['name'] + ), + to_term($field['type']) + ] + ) + ); + } + ) + ) + ); + } + else { + // error_log(json_encode($type)); + throw (new \Exception('unhandled')); + } +} + + +/** + * shall return a conversion from a term to a type + * + * @return inteface_type + */ +function from_term( + \alveolata\term\interface_term $term +) : interface_type +{ + if ($term instanceof \alveolata\term\class_variable) { + $variable = $term; + return ( + new class_var( + $variable->name + ) + ); + } + else if ($term instanceof \alveolata\term\class_function) { + $function = $term; + switch ($function->head) { + default: { + throw (new \Exception('unhandled')); + break; + } + case 'boolean': { + return ( + new class_boolean( + ) + ); + break; + } + case 'integer': { + return ( + new class_integer( + ) + ); + break; + } + case 'float': { + return ( + new class_float( + ) + ); + break; + } + case 'string': { + return ( + new class_string( + ) + ); + break; + } + case 'list': { + return ( + new class_list( + from_term($function->arguments[0]) + ) + ); + break; + } + case 'map': { + return ( + new class_map( + from_term($function->arguments[0]), + from_term($function->arguments[1]) + ) + ); + break; + } + case 'record': { + return ( + new class_record( + \alveolata\list_\map( + $function->arguments, + function ($argument) { + if ($argument instanceof \alveolata\term\class_variable) { + throw (new \Exception('unhandled')); + } + else if ($argument instanceof \alveolata\term\class_function) { + if ($argument->head === 'field') { + return [ + 'name' => $argument->arguments[0]->head, + 'type' => from_term($argument->arguments[1]), + ]; + } + else { + throw (new \Exception('unhandled')); + } + } + else { + throw (new \Exception('unhandled')); + } + } + ) + ) + ); + break; + } + } + } + else { + throw (new \Exception('unhandled')); + } +} + + +/** + * @param interface_type $type + * @return string + */ +function to_string( + interface_type $type, + bool $formatted = false +) : string +{ + $string_original = $type->to_string(0); + if (! $formatted) { + $string = str_replace( + ["\n", "\t"], + '', + $string_original + ); + } + else { + $string = $string_original; + } + return $string; +} + + +/** + * @param any $value + * @return interface_type + */ +function derive( + $value +) : interface_type +{ + $unify_terms = function ($terms) { + if (count($terms) === 0) { + throw (new \Exception('nothing to unify')); + } + else { + $head = $terms[0]; + $rest = array_slice($terms, 1); + $unifiers = \alveolata\term\unifiers( + \alveolata\list_\map( + $rest, + function ($term) use (&$head) { + return [ + 'first' => $head, + 'second' => $term, + ]; + } + ) + ); + switch (count($unifiers)) { + case 0: { + throw (new \Exception('not unifiyable')); + } + case 1: { + return \alveolata\term\substitution_instance( + $unifiers[0], + $terms[0] + ); + break; + } + case 2: { + \alveolata\log\warning( + 'multiple unifiers', + [ + 'unifiers' => $unifiers, + ] + ); + return \alveolata\term\substitution_instance( + $unifiers[0], + $terms[0] + ); + break; + } + } + } + }; + $unify_types = function ($types) use (&$unify_terms) { + return from_term( + $unify_terms( + \alveolata\list_\map( + $types, + function ($type) {return to_term($type);} + ) + ) + ); + }; + if ($value === null) { + return (new class_null()); + } + else { + if (gettype($value) === 'boolean') { + return (new class_boolean()); + } + else if (gettype($value) === 'integer') { + return (new class_integer()); + } + else if ((gettype($value) === 'float') or (gettype($value) === 'double')) { + return (new class_float()); + } + else if (gettype($value) === 'string') { + return (new class_string()); + } + else if (gettype($value) === 'array') { + $keys = array_keys($value); + $values = array_values($value); + $empty = (count($keys) === 0); + if ($empty) { + return (new class_var('x')); + } + else { + $all_numeric = \alveolata\list_\every($keys, function ($key) {return is_numeric($key);}); + if ($all_numeric) { + $types_values = \alveolata\list_\map($values, function ($value_) {return derive($value_);}); + try { + $type_values_master = $unify_types($types_values); + return ( + new class_list( + $type_values_master + ) + ); + } + catch (\Exception $exception) { + \alveolata\log\info( + 'types of array elements not unifyable', + [ + 'types_values' => \alveolata\list_\map( + $types_values, + function ($type) {return $type->to_string(0);} + ), + ] + ); + return ( + new class_list( + new class_var('x') + ) + ); + } + } + else { + $types_keys = \alveolata\list_\map($keys, function ($key) {return derive($key);}); + $types_values = \alveolata\list_\map($values, function ($value_) {return derive($value_);}); + try { + $type_keys_master = $unify_types($types_keys); + $type_values_master = $unify_types($types_values); + return ( + new class_map( + $type_keys_master, + $type_values_master + ) + ); + } + catch (\Exception $exception) { + \alveolata\log\info( + 'types of array keys or values not unifyable', + [ + 'types_keys' => \alveolata\list_\map( + $types_keys, + function ($type) {return $type->to_string(0);} + ), + 'types_values' => \alveolata\list_\map( + $types_values, + function ($type) {return $type->to_string(0);} + ), + ] + ); + return ( + new class_record( + \alveolata\list_\map( + $keys, + function ($key) use ($value) { + return [ + 'name' => $key, + 'type' => derive($value[$key]), + 'mandatory' => true, + ]; + } + ) + ) + ); + } + } + } + } + } +} + + ?> diff --git a/lib/alveolata/type/implementation-any.php b/lib/alveolata/type/implementation-any.php new file mode 100644 index 0000000..b1b2abb --- /dev/null +++ b/lib/alveolata/type/implementation-any.php @@ -0,0 +1,59 @@ + + */ +class class_any implements interface_type +{ + + /** + * [implementation] + */ + public function investigate( + $value + ) : array + { + return []; + } + + + /** + * [implementation] + */ + public function check( + $value + ) : bool + { + return true; + } + + + /** + * [implementation] + */ + public function to_string( + int $depth + ) : string + { + return sprintf( + "any" + ); + } + + + /** + * [implementation] + */ + public function to_oas( + ) : array + { + return [ + ]; + } + +} + + ?> diff --git a/lib/alveolata/type/implementation-boolean.php b/lib/alveolata/type/implementation-boolean.php new file mode 100644 index 0000000..48b835d --- /dev/null +++ b/lib/alveolata/type/implementation-boolean.php @@ -0,0 +1,61 @@ + + */ +class class_boolean implements interface_type +{ + + /** + * [implementation] + */ + public function investigate( + $value + ) : array + { + return investigate_primitive(['boolean'], $value); + } + + + /** + * @implementation + */ + public function check( + $value + ) : bool + { + return (gettype($value) === 'boolean'); + } + + + /** + * @implementation + */ + public function to_string( + int $depth + ) : string + { + return sprintf( + "boolean" + ); + } + + + /** + * [implementation] + */ + public function to_oas( + ) : array + { + return [ + 'nullable' => false, + 'type' => 'boolean', + ]; + } + +} + + ?> diff --git a/lib/alveolata/type/implementation-float.php b/lib/alveolata/type/implementation-float.php new file mode 100644 index 0000000..7d079f4 --- /dev/null +++ b/lib/alveolata/type/implementation-float.php @@ -0,0 +1,67 @@ + + */ +class class_float implements interface_type +{ + + /** + * [implementation] + */ + public function investigate( + $value + ) : array + { + return investigate_primitive(['integer','float','double'], $value); + } + + + /** + * @implementation + */ + public function check( + $value + ) : bool + { + return ( + (gettype($value) === 'integer') + || + (gettype($value) === 'float') + || + (gettype($value) === 'double') + ); + } + + + /** + * @implementation + */ + public function to_string( + int $depth + ) : string + { + return sprintf( + "float" + ); + } + + + /** + * [implementation] + */ + public function to_oas( + ) : array + { + return [ + 'nullable' => false, + 'type' => 'number', + ]; + } + +} + + ?> diff --git a/lib/alveolata/type/implementation-integer.php b/lib/alveolata/type/implementation-integer.php new file mode 100644 index 0000000..55d6f61 --- /dev/null +++ b/lib/alveolata/type/implementation-integer.php @@ -0,0 +1,61 @@ + + */ +class class_integer implements interface_type +{ + + /** + * [implementation] + */ + public function investigate( + $value + ) : array + { + return investigate_primitive(['integer'], $value); + } + + + /** + * @implementation + */ + public function check( + $value + ) : bool + { + return (gettype($value) === 'integer'); + } + + + /** + * @implementation + */ + public function to_string( + int $depth + ) : string + { + return sprintf( + "integer" + ); + } + + + /** + * [implementation] + */ + public function to_oas( + ) : array + { + return [ + 'nullable' => false, + 'type' => 'integer', + ]; + } + +} + + ?> diff --git a/lib/alveolata/type/implementation-list.php b/lib/alveolata/type/implementation-list.php new file mode 100644 index 0000000..8863c11 --- /dev/null +++ b/lib/alveolata/type/implementation-list.php @@ -0,0 +1,115 @@ + + */ +class class_list implements interface_type +{ + + /** + * @var interface_type + */ + public $type_element; + + + /** + * @param interface_type $type_element + * @author Christian Fraß + */ + public function __construct( + interface_type $type_element + ) + { + $this->type_element = $type_element; + } + + + /** + * [implementation] + */ + public function investigate( + $value + ) : array + { + $flaws = []; + if (! (gettype($value) === 'array')) { + $flaw = (new struct_flaw([], 'not an array')); + array_push($flaws, $flaw); + } + else { + $index = 0; + foreach ($value as $element) { + $flaws_sub = $this->type_element->investigate($element); + $flaws = array_merge( + $flaws, + \alveolata\list_\map/**/( + $flaws_sub, + function (struct_flaw $flaw_sub) use ($index) : struct_flaw { + return flaw_extend($flaw_sub, sprintf('element:#%d', $index)); + } + ) + ); + $index += 1; + } + } + return $flaws; + } + + + /** + * @implementation + */ + public function check( + $value + ) : bool + { + return ( + (gettype($value) === 'array') + && + \alveolata\list_\every( + $value, + function ($element) { + return $this->type_element->check($element); + } + ) + ); + } + + + /** + * @implementation + */ + public function to_string( + int $depth + ) : string + { + return sprintf( + "list<\n%s%s\n%s>", + str_repeat("\t", $depth+1), + $this->type_element->to_string($depth+1), + str_repeat("\t", $depth) + ); + } + + + /** + * [implementation] + */ + public function to_oas( + ) : array + { + return [ + 'nullable' => false, + 'type' => 'array', + 'items' => $this->type_element->to_oas(), + ]; + } + +} + + ?> diff --git a/lib/alveolata/type/implementation-map.php b/lib/alveolata/type/implementation-map.php new file mode 100644 index 0000000..f3b5ef1 --- /dev/null +++ b/lib/alveolata/type/implementation-map.php @@ -0,0 +1,151 @@ + + */ +class class_map implements interface_type +{ + + /** + * @var interface_type + */ + public $type_key; + + + /** + * @var interface_type + */ + public $type_value; + + + /** + * @param interface_type $type_element + * @param interface_type $type_value + * @author Christian Fraß + */ + public function __construct( + interface_type $type_key, + interface_type $type_value + ) + { + $this->type_key = $type_key; + $this->type_value = $type_value; + } + + + /** + * [implementation] + */ + public function investigate( + $value + ) : array + { + $flaws = []; + if (! (gettype($value) === 'array')) { + $flaw = (new struct_flaw([], 'not an array')); + array_push($flaws, $flaw); + } + else { + $index = 0; + foreach ($value as $key => $value_) { + // key + { + $flaws_sub = $this->type_key->investigate($key); + $flaws = array_merge( + $flaws, + \alveolata\list_\map/**/( + $flaws_sub, + function (struct_flaw $flaw_sub) use ($index) : struct_flaw { + return flaw_extend($flaw_sub, sprintf('key:#%d', $index)); + } + ) + ); + } + // value + { + $flaws_sub = $this->type_value->investigate($value_); + $flaws = array_merge( + $flaws, + \alveolata\list_\map/**/( + $flaws_sub, + function (struct_flaw $flaw_sub) use ($index) : struct_flaw { + return flaw_extend($flaw_sub, sprintf('value:#%d', $index)); + } + ) + ); + } + $index += 1; + } + } + return $flaws; + } + + + /** + * @implementation + */ + public function check( + $value + ) : bool + { + return ( + (gettype($value) === 'array') + && + \alveolata\list_\every( + array_keys($value), + function ($key) { + return $this->type_key->check($key); + } + ) + && + \alveolata\list_\every( + array_values($value), + function ($value_) { + return $this->type_value->check($value_); + } + ) + ); + } + + + /** + * @implementation + */ + function to_string( + int $depth + ) : string + { + return sprintf( + "map<\n%s%s,\n%s%s\n%s>", + str_repeat("\t", $depth+1), + $this->type_key->to_string($depth+1), + str_repeat("\t", $depth+1), + $this->type_value->to_string($depth+1), + str_repeat("\t", $depth) + ); + } + + + /** + * [implementation] + */ + public function to_oas( + ) : array + { + return [ + 'nullable' => false, + 'type' => 'object', + 'additionalProperties' => $this->type_value->to_oas(), + 'properties' => [], + 'required' => [], + ]; + } + +} + + ?> diff --git a/lib/alveolata/type/implementation-null.php b/lib/alveolata/type/implementation-null.php new file mode 100644 index 0000000..7132699 --- /dev/null +++ b/lib/alveolata/type/implementation-null.php @@ -0,0 +1,68 @@ + + */ +class class_null implements interface_type +{ + + /** + * [implementation] + */ + public function investigate( + $value + ) : array + { + $flaws = []; + if (! ($value === null)) { + $flaw = (new struct_flaw( + [], + sprintf('%s is not null', json_encode($value)) + )); + array_push($flaws, $flaw); + } + return $flaws; + } + + + /** + * @implementation + */ + public function check( + $value + ) : bool + { + return ($value === null); + } + + + /** + * @implementation + */ + public function to_string( + int $depth + ) : string + { + return sprintf( + "null" + ); + } + + + /** + * [implementation] + */ + public function to_oas( + ) : array + { + return [ + 'nullable' => true, + ]; + } + +} + + ?> diff --git a/lib/alveolata/type/implementation-record.php b/lib/alveolata/type/implementation-record.php new file mode 100644 index 0000000..f3ac54b --- /dev/null +++ b/lib/alveolata/type/implementation-record.php @@ -0,0 +1,163 @@ + + */ +class class_record implements interface_type +{ + + /** + * @var array {list>} + */ + public $fields; + + + /** + * @param array $fields {list>} + * @author Christian Fraß + */ + public function __construct( + array $fields + ) + { + $this->fields = $fields; + } + + + /** + * [implementation] + */ + public function investigate( + $value + ) : array + { + $flaws = []; + // core type + if (! (gettype($value) === 'array')) { + $flaw = (new struct_flaw([], 'not an array')); + array_push($flaws, $flaw); + } + else { + // expected fields + { + foreach ($this->fields as $field) { + if (! array_key_exists($field['name'], $value)) { + if ($field['mandatory']) { + $flaw = (new struct_flaw( + [], + sprintf('missing field "%s"', $field['name']) + )); + array_push($flaws, $flaw); + } + else { + // do nothing + } + } + else { + $value_sub = $value[$field['name']]; + $type_sub = $field['type']; + $flaws_sub = $type_sub->investigate($value_sub); + $flaws = array_merge( + $flaws, + \alveolata\list_\map/**/( + $flaws_sub, + function (struct_flaw $flaw_sub) use ($field) : struct_flaw { + return flaw_extend($flaw_sub, sprintf('field:%s', $field['name'])); + } + ) + ); + } + } + } + // spare fields + { + $spare_field_names = array_diff( + array_keys($value), + \alveolata\list_\map( + $this->fields, + function (array $field) : string { + return $field['name']; + } + ) + ); + $flaws = array_merge( + $flaws, + \alveolata\list_\map/**/( + $spare_field_names, + function (string $spare_field_names) : struct_flaw { + return (new struct_flaw( + [], + sprintf('spare field "%s"', $spare_field_names) + )); + } + ) + ); + } + } + return $flaws; + } + + + /** + * @implementation + */ + public function to_string( + int $depth + ) : string + { + return sprintf( + "record<\n%s\n%s>", + implode( + ",\n", + array_map( + function ($field) use ($depth) { + return sprintf( + "%s%s%s:%s", + str_repeat("\t", $depth+1), + ($field['mandatory'] ? '' : '?'), + $field['name'], + $field['type']->to_string($depth+1) + ); + }, + $this->fields + ) + ), + str_repeat("\t", $depth) + ); + } + + + /** + * [implementation] + */ + public function to_oas( + ) : array + { + $properties = []; + $required = []; + foreach ($this->fields as $field) { + $properties[$field['name']] = $field['type']->to_oas(); + if ($field['mandatory']) { + \array_push($required, $field['name']); + } + else { + // do nothing + } + }; + return [ + 'nullable' => false, + 'type' => 'object', + 'additionalProperties' => false, + 'properties' => $properties, + 'required' => $required, + ]; + } + +} + + ?> diff --git a/lib/alveolata/type/implementation-string.php b/lib/alveolata/type/implementation-string.php new file mode 100644 index 0000000..5c5ccde --- /dev/null +++ b/lib/alveolata/type/implementation-string.php @@ -0,0 +1,61 @@ + + */ +class class_string implements interface_type +{ + + /** + * [implementation] + */ + public function investigate( + $value + ) : array + { + return investigate_primitive(['string'], $value); + } + + + /** + * @implementation + */ + public function check( + $value + ) : bool + { + return (gettype($value) === 'string'); + } + + + /** + * @implementation + */ + function to_string( + int $depth + ) : string + { + return sprintf( + "string" + ); + } + + + /** + * [implementation] + */ + public function to_oas( + ) : array + { + return [ + 'nullable' => false, + 'type' => 'string', + ]; + } + +} + + ?> diff --git a/lib/alveolata/type/implementation-union.php b/lib/alveolata/type/implementation-union.php new file mode 100644 index 0000000..c5a8c55 --- /dev/null +++ b/lib/alveolata/type/implementation-union.php @@ -0,0 +1,126 @@ + + */ +class class_union implements interface_type +{ + + /** + * @var list + */ + public $options; + + + /** + * @param list + * @author Christian Fraß + */ + public function __construct( + array $options + ) + { + $this->options = $options; + } + + + /** + * [implementation] + */ + public function investigate( + $value + ) : array + { + $valid = false; + $index = 0; + $flaws = []; + foreach ($this->options as $option) { + $flaws_sub = $option->investigate($value); + if (empty($flaws_sub)) { + $valid = true; + } + $flaws = array_merge( + $flaws, + \alveolata\list_\map/**/( + $flaws_sub, + function (struct_flaw $flaw_sub) use ($index) : struct_flaw { + return flaw_extend($flaw_sub, sprintf('option:#%d', $index)); + } + ) + ); + $index += 1; + } + if ($valid) { + return []; + } + else { + return $flaws; + } + } + + + /** + * @implementation + */ + public function check( + $value + ) : bool + { + return \alveolata\list_\some( + $this->options, + function (interface_type $option) use ($value) { + return $option->check($value); + } + ); + } + + + /** + * @implementation + */ + public function to_string( + int $depth + ) : string + { + return sprintf( + "union<\n%s\n%s>", + implode( + ",\n", + array_map( + function ($option) use ($depth) { + return sprintf( + "%s%s", + str_repeat("\t", $depth+1), + $option->to_string($depth+1) + ); + }, + $this->options + ) + ), + str_repeat("\t", $depth) + ); + } + + + /** + * [implementation] + */ + public function to_oas( + ) : array + { + return [ + 'anyOf' => \array_map( + fn($option) => $option->to_oas(), + $this->options + ), + ]; + } + +} + + ?> diff --git a/lib/alveolata/type/implementation-var.php b/lib/alveolata/type/implementation-var.php new file mode 100644 index 0000000..ecf3180 --- /dev/null +++ b/lib/alveolata/type/implementation-var.php @@ -0,0 +1,78 @@ + + */ +class class_var implements interface_type +{ + + /** + * @var string + * @author Christian Fraß + */ + public $name; + + + /** + * @param string + * @author Christian Fraß + */ + public function __construct( + string $name + ) + { + $this->name = $name; + } + + + /** + * [implementation] + */ + public function investigate( + $value + ) : array + { + return []; + } + + + /** + * @implementation + */ + public function check( + $value + ) : bool + { + return true; + } + + + /** + * @implementation + */ + function to_string( + int $depth + ) : string + { + return sprintf( + '$%s', $this->name + ); + } + + + /** + * [implementation] + */ + public function to_oas( + ) : array + { + return [ + ]; + } + +} + + ?> diff --git a/lib/alveolata/type/interface.php b/lib/alveolata/type/interface.php new file mode 100644 index 0000000..23239e0 --- /dev/null +++ b/lib/alveolata/type/interface.php @@ -0,0 +1,145 @@ + + */ +class struct_flaw +{ + + /** + * @var array {list} + */ + public $context; + + + /** + * @var string + */ + public $message; + + + /** + * constructor + */ + public function __construct( + array $context, + string $message + ) + { + $this->context = $context; + $this->message = $message; + } + +} + + +/** + */ +function flaw_extend( + struct_flaw $flaw, + string $step +) : struct_flaw +{ + return (new struct_flaw( + array_merge([$step], $flaw->context), + $flaw->message + )); +} + + +/** + */ +function flaw_to_string( + struct_flaw $flaw +) : string +{ + return ( + empty($flaw->context) + ? $flaw->message + : sprintf( + '%s %s', + implode( + ' ', + array_map( + function (string $step) : string { + return sprintf('[%s]', $step); + }, + $flaw->context + ) + ), + $flaw->message + ) + ); +} + + +/** + */ +function investigate_primitive( + array $typenames, + $value +) : array +{ + $typename = gettype($value); + $flaws = []; + if (! in_array($typename, $typenames)) { + $flaw = (new struct_flaw( + [], + sprintf( + '%s : %s <> %s', + json_encode($value), + $typename, + implode('|', $typenames) + ) + )); + array_push($flaws, $flaw); + } + return $flaws; +} + + +/** + * @author Christian Fraß + */ +interface interface_type +{ + + /** + * shall return a list of flaws; empty result means, that everything is proper + * + * @param mixed $value {any} + * @return array {list<&struct_flaw>} + */ + function investigate( + $value + ) : array + ; + + + /** + * shall return a textual representation of the type + * + * @return string + */ + function to_string( + int $depth + ) : string + ; + + + /** + * shall return an OpenAPI schema node, representing the type + * + * @see https://swagger.io/specification/#schema-object + * @return array + */ + function to_oas( + ) : array + ; + +} + + ?> diff --git a/lib/alveolata/type/test.spec.php b/lib/alveolata/type/test.spec.php new file mode 100644 index 0000000..5617a86 --- /dev/null +++ b/lib/alveolata/type/test.spec.php @@ -0,0 +1,688 @@ + 'alveolata', + 'sections' => [ + [ + 'name' => 'type', + 'setup' => function (&$environment) { + }, + 'sections' => [ + [ + 'name' => 'investigate', + 'sections' => [ + [ + 'name' => 'any', + 'cases' => [ + [ + 'name' => 'test1', + 'procedure' => function ($assert) { + $type = new \alveolata\type\class_any(); + $result = $type->investigate(null); + $assert->equal(count($result), 0); + } + ], + [ + 'name' => 'test2', + 'procedure' => function ($assert) { + $type = new \alveolata\type\class_any(); + $result = $type->investigate(false); + $assert->equal(count($result), 0); + } + ], + [ + 'name' => 'test3', + 'procedure' => function ($assert) { + $type = new \alveolata\type\class_any(); + $result = $type->investigate('foobar'); + $assert->equal(count($result), 0); + } + ], + ] + ], + [ + 'name' => 'boolean', + 'cases' => [ + [ + 'name' => 'test1', + 'procedure' => function ($assert) { + $type = new \alveolata\type\class_boolean(); + $result = $type->investigate(null); + $assert->equal(count($result), 1); + $assert->equal($result[0]->context, []); + $assert->equal($result[0]->message,'null : NULL <> boolean'); + } + ], + [ + 'name' => 'test2', + 'procedure' => function ($assert) { + $type = new \alveolata\type\class_boolean(); + $result = $type->investigate(false); + $assert->equal(count($result), 0); + } + ], + [ + 'name' => 'test3', + 'procedure' => function ($assert) { + $type = new \alveolata\type\class_boolean(); + $result = $type->investigate(true); + $assert->equal(count($result), 0); + } + ], + [ + 'name' => 'test4', + 'procedure' => function ($assert) { + $type = new \alveolata\type\class_boolean(); + $result = $type->investigate('foobar'); + $assert->equal(count($result), 1); + $assert->equal($result[0]->context, []); + $assert->equal($result[0]->message,'"foobar" : string <> boolean'); + } + ], + ] + ], + [ + 'name' => 'union', + 'cases' => [ + [ + 'name' => 'test1', + 'procedure' => function ($assert) { + $type = new \alveolata\type\class_union([ + new \alveolata\type\class_boolean(), + new \alveolata\type\class_integer(), + ]); + $result = $type->investigate(false); + $assert->equal(count($result), 0); + } + ], + [ + 'name' => 'test2', + 'procedure' => function ($assert) { + $type = new \alveolata\type\class_union([ + new \alveolata\type\class_boolean(), + new \alveolata\type\class_integer(), + ]); + $result = $type->investigate(42); + $assert->equal(count($result), 0); + } + ], + [ + 'name' => 'test3', + 'procedure' => function ($assert) { + $type = new \alveolata\type\class_union([ + new \alveolata\type\class_boolean(), + new \alveolata\type\class_integer(), + ]); + $result = $type->investigate('foobar'); + $assert->equal(count($result), 2); + $assert->equal($result[0]->context, ['option:#0']); + $assert->equal($result[0]->message,'"foobar" : string <> boolean'); + $assert->equal($result[1]->context, ['option:#1']); + $assert->equal($result[1]->message,'"foobar" : string <> integer'); + } + ], + ] + ], + [ + 'name' => 'list', + 'cases' => [ + [ + 'name' => 'test1', + 'procedure' => function ($assert) { + $type = new \alveolata\type\class_list( + new \alveolata\type\class_boolean() + ); + $result = $type->investigate([]); + $assert->equal(count($result), 0); + } + ], + [ + 'name' => 'test2', + 'procedure' => function ($assert) { + $type = new \alveolata\type\class_list( + new \alveolata\type\class_boolean() + ); + $result = $type->investigate([false]); + $assert->equal(count($result), 0); + } + ], + [ + 'name' => 'test3', + 'procedure' => function ($assert) { + $type = new \alveolata\type\class_list( + new \alveolata\type\class_boolean() + ); + $result = $type->investigate([false, true]); + $assert->equal(count($result), 0); + } + ], + [ + 'name' => 'test4', + 'procedure' => function ($assert) { + $type = new \alveolata\type\class_list( + new \alveolata\type\class_boolean() + ); + $result = $type->investigate([false, 42, true, 'foobar', false]); + $assert->equal(count($result), 2); + $assert->equal($result[0]->context, ['element:#1']); + $assert->equal($result[0]->message,'42 : integer <> boolean'); + $assert->equal($result[1]->context, ['element:#3']); + $assert->equal($result[1]->message,'"foobar" : string <> boolean'); + } + ], + ] + ], + [ + 'name' => 'map', + 'cases' => [ + [ + 'name' => 'test1', + 'procedure' => function ($assert) { + $type = new \alveolata\type\class_map( + new \alveolata\type\class_string(), + new \alveolata\type\class_boolean() + ); + $result = $type->investigate([]); + $assert->equal(count($result), 0); + } + ], + [ + 'name' => 'test2', + 'procedure' => function ($assert) { + $type = new \alveolata\type\class_map( + new \alveolata\type\class_string(), + new \alveolata\type\class_boolean() + ); + $result = $type->investigate(['foo' => false]); + $assert->equal(count($result), 0); + } + ], + [ + 'name' => 'test3', + 'procedure' => function ($assert) { + $type = new \alveolata\type\class_map( + new \alveolata\type\class_string(), + new \alveolata\type\class_boolean() + ); + $result = $type->investigate(['foo' => 42]); + $assert->equal(count($result), 1); + $assert->equal($result[0]->context, ['value:#0']); + $assert->equal($result[0]->message,'42 : integer <> boolean'); + } + ], + [ + 'name' => 'test4', + 'procedure' => function ($assert) { + $type = new \alveolata\type\class_map( + new \alveolata\type\class_string(), + new \alveolata\type\class_boolean() + ); + $result = $type->investigate([42 => true]); + $assert->equal(count($result), 1); + $assert->equal($result[0]->context, ['key:#0']); + $assert->equal($result[0]->message,'42 : integer <> string'); + } + ], + [ + 'name' => 'test5', + 'procedure' => function ($assert) { + $type = new \alveolata\type\class_map( + new \alveolata\type\class_string(), + new \alveolata\type\class_boolean() + ); + $result = $type->investigate(['foo' => false, 'bar' => true, 'baz' => 'qux']); + $assert->equal(count($result), 1); + $assert->equal($result[0]->context, ['value:#2']); + $assert->equal($result[0]->message,'"qux" : string <> boolean'); + } + ], + ] + ], + [ + 'name' => 'record', + 'cases' => [ + [ + 'name' => 'test1', + 'procedure' => function ($assert) { + $type = new \alveolata\type\class_record([ + [ + 'name' => 'foo', + 'type' => new \alveolata\type\class_boolean(), + 'mandatory' => true + ], + ]); + $result = $type->investigate(['foo' => false]); + $assert->equal(count($result), 0); + } + ], + [ + 'name' => 'test2', + 'procedure' => function ($assert) { + $type = new \alveolata\type\class_record([ + [ + 'name' => 'foo', + 'type' => new \alveolata\type\class_boolean(), + 'mandatory' => true, + ], + ]); + $result = $type->investigate(['foo' => 42]); + $assert->equal(count($result), 1); + $assert->equal($result[0]->context, ['field:foo']); + $assert->equal($result[0]->message,'42 : integer <> boolean'); + } + ], + [ + 'name' => 'test3', + 'procedure' => function ($assert) { + $type = new \alveolata\type\class_record([ + [ + 'name' => 'foo', + 'type' => new \alveolata\type\class_boolean(), + 'mandatory' => true + ], + ]); + $result = $type->investigate(['foo' => false, 'bar' => 0]); + $assert->equal(count($result), 1); + $assert->equal($result[0]->context, []); + $assert->equal($result[0]->message,'spare field "bar"'); + } + ], + [ + 'name' => 'test4', + 'procedure' => function ($assert) { + $type = new \alveolata\type\class_record([ + [ + 'name' => 'foo', + 'type' => new \alveolata\type\class_record([ + [ + 'name' => 'bar', + 'type' => new \alveolata\type\class_boolean(), + 'mandatory' => true + ], + ]), + 'mandatory' => true + ], + ]); + $result = $type->investigate(['foo' => ['bar' => false]]); + $assert->equal(count($result), 0); + } + ], + [ + 'name' => 'test5', + 'procedure' => function ($assert) { + $type = new \alveolata\type\class_record([ + [ + 'name' => 'foo', + 'type' => new \alveolata\type\class_record([ + [ + 'name' => 'bar', + 'type' => new \alveolata\type\class_boolean(), + 'mandatory' => true + ], + ]), + 'mandatory' => true + ], + ]); + $result = $type->investigate(['foo' => ['bar' => 42]]); + $assert->equal(count($result), 1); + $assert->equal($result[0]->context, ['field:foo', 'field:bar']); + $assert->equal($result[0]->message,'42 : integer <> boolean'); + } + ], + [ + 'name' => 'test6', + 'procedure' => function ($assert) { + $type = new \alveolata\type\class_record([ + [ + 'name' => 'foo', + 'type' => new \alveolata\type\class_boolean(), + 'mandatory' => false + ], + ]); + $result = $type->investigate([]); + $assert->equal(count($result), 0); + } + ], + ] + ], + ] + ], + [ + 'name' => 'derive', + 'cases' => [ + [ + 'name' => 'boolean', + 'procedure' => function ($assert, &$environment) { + // constants + { + $value = false; + $result_expected = 'boolean'; + } + // execution & assertions + { + $type = \alveolata\type\derive($value); + $result_actual = \alveolata\type\to_string($type); + } + // assertions + { + $assert->equal($result_actual, $result_expected); + } + }, + ], + [ + 'name' => 'empty list', + 'procedure' => function ($assert, &$environment) { + // constants + { + $value = []; + $result_expected = '$x'; + } + // execution & assertions + { + $type = \alveolata\type\derive($value); + $result_actual = \alveolata\type\to_string($type); + } + // assertions + { + $assert->equal($result_actual, $result_expected); + } + }, + ], + [ + 'name' => 'simple list', + 'procedure' => function ($assert, &$environment) { + // constants + { + $value = [2,3,5,7]; + $result_expected = 'list'; + } + // execution & assertions + { + $type = \alveolata\type\derive($value); + $result_actual = \alveolata\type\to_string($type); + } + // assertions + { + $assert->equal($result_actual, $result_expected); + } + }, + ], + [ + 'name' => 'map', + 'procedure' => function ($assert, &$environment) { + // constants + { + $value = [ + 'foo' => 2, + 'bar' => 3, + 'baz' => 5, + 'qux' => 7, + ]; + $result_expected = 'map'; + } + // execution & assertions + { + $type = \alveolata\type\derive($value); + $result_actual = \alveolata\type\to_string($type); + } + // assertions + { + $assert->equal($result_actual, $result_expected); + } + }, + ], + [ + 'name' => 'record', + 'procedure' => function ($assert, &$environment) { + // constants + { + $value = [ + 'foo' => false, + 'bar' => 42, + 'baz' => 2.7182, + 'qux' => 'mathe', + ]; + $result_expected = 'record'; + } + // execution & assertions + { + $type = \alveolata\type\derive($value); + $result_actual = \alveolata\type\to_string($type); + } + // assertions + { + $assert->equal($result_actual, $result_expected); + } + }, + ], + [ + 'name' => 'unification necessary', + 'procedure' => function ($assert, &$environment) { + // constants + { + $value = [[],[1]]; + $result_expected = 'list>'; + } + // execution & assertions + { + $type = \alveolata\type\derive($value); + $result_actual = \alveolata\type\to_string($type); + } + // assertions + { + $assert->equal($result_actual, $result_expected); + } + }, + ], + ], + ], + [ + 'name' => 'to_string', + 'setup' => function (&$env) { + $env['type'] = new \alveolata\type\class_record( + [ + [ + 'name' => 'fehuz', + 'type' => new \alveolata\type\class_any(), + 'mandatory' => true, + ], + [ + 'name' => 'uruz', + 'type' => new \alveolata\type\class_boolean(), + 'mandatory' => true, + ], + [ + 'name' => 'thurisaz', + 'type' => new \alveolata\type\class_integer(), + 'mandatory' => false, + ], + [ + 'name' => 'ansuz', + 'type' => new \alveolata\type\class_float(), + 'mandatory' => true, + ], + [ + 'name' => 'raidho', + 'type' => new \alveolata\type\class_string(), + 'mandatory' => true, + ], + [ + 'name' => 'kenaz', + 'type' => new \alveolata\type\class_list( + new \alveolata\type\class_any() + ), + 'mandatory' => true, + ], + [ + 'name' => 'gebo', + 'type' => new \alveolata\type\class_map( + new \alveolata\type\class_any(), + new \alveolata\type\class_integer() + ), + 'mandatory' => true, + ], + [ + 'name' => 'wunjo', + 'type' => new \alveolata\type\class_union( + [ + new \alveolata\type\class_any(), + new \alveolata\type\class_integer(), + ] + ), + 'mandatory' => true, + ], + [ + 'name' => 'hagalaz', + 'type' => new \alveolata\type\class_var('VAR'), + 'mandatory' => true, + ], + ] + ); + }, + 'cases' => [ + [ + 'name' => 'oblique, formatted', + 'procedure' => function ($assert, $env) { + $result_actual = \alveolata\type\to_string($env['type'], true); + $result_expected = "record<\n\tfehuz:any,\n\turuz:boolean,\n\t?thurisaz:integer,\n\tansuz:float,\n\traidho:string,\n\tkenaz:list<\n\t\tany\n\t>,\n\tgebo:map<\n\t\tany,\n\t\tinteger\n\t>,\n\twunjo:union<\n\t\tany,\n\t\tinteger\n\t>,\n\thagalaz:\$VAR\n>"; + $assert->equal($result_actual, $result_expected); + } + ], + [ + 'name' => 'oblique, unformatted', + 'procedure' => function ($assert, $env) { + $result_actual = \alveolata\type\to_string($env['type'], false); + $result_expected = 'record,gebo:map,wunjo:union,hagalaz:$VAR>'; + $assert->equal($result_actual, $result_expected); + } + ], + ] + ], + [ + 'name' => 'to_oas', + 'cases' => [ + [ + 'name' => 'boolean', + 'procedure' => function ($assert, $env) { + // setup + $type = (new \alveolata\type\class_boolean()); + + // exec + $result_actual = $type->to_oas(); + + // assertions + $result_expected = [ + 'nullable' => false, + 'type' => 'boolean', + ]; + $assert->equal($result_actual, $result_expected); + }, + ], + [ + 'name' => 'list', + 'procedure' => function ($assert, $env) { + // setup + $type = (new \alveolata\type\class_list(new \alveolata\type\class_integer())); + + // exec + $result_actual = $type->to_oas(); + + // assertions + $result_expected = [ + 'nullable' => false, + 'type' => 'array', + 'items' => [ + 'nullable' => false, + 'type' => 'integer', + ] + ]; + $assert->equal($result_actual, $result_expected); + }, + ], + [ + 'name' => 'record', + 'procedure' => function ($assert, $env) { + // setup + $type = (new \alveolata\type\class_record( + [ + [ + 'name' => 'foo', + 'type' => (new \alveolata\type\class_boolean()), + 'mandatory' => true, + ], + [ + 'name' => 'bar', + 'type' => (new \alveolata\type\class_integer()), + 'mandatory' => false, + ], + ] + )); + + // exec + $result_actual = $type->to_oas(); + + // assertions + $result_expected = [ + 'nullable' => false, + 'type' => 'object', + 'additionalProperties' => false, + 'properties' => [ + 'foo' => [ + 'nullable' => false, + 'type' => 'boolean', + ], + 'bar' => [ + 'nullable' => false, + 'type' => 'integer', + ] + ], + 'required' => [ + 'foo', + ], + ]; + $assert->equal($result_actual, $result_expected); + }, + ], + [ + 'name' => 'union', + 'procedure' => function ($assert, $env) { + // setup + $type = (new \alveolata\type\class_union( + [ + (new \alveolata\type\class_null()), + (new \alveolata\type\class_string()), + ] + )); + + // exec + $result_actual = $type->to_oas(); + + // assertions + $result_expected = [ + 'anyOf' => [ + [ + 'nullable' => true, + ], + [ + 'nullable' => false, + 'type' => 'string', + ] + ], + ]; + $assert->equal($result_actual, $result_expected); + }, + ], + ], + ], + ], + ] + ] + ] +); + + ?> diff --git a/lib/alveolata/url/functions.php b/lib/alveolata/url/functions.php new file mode 100644 index 0000000..fb81f99 --- /dev/null +++ b/lib/alveolata/url/functions.php @@ -0,0 +1,130 @@ +)} + */ +function form_encode( + ?array $data +) : ?string +{ + if ($data === null) { + return $data; + } + else { + $parts = []; + foreach ($data as $key => $value) { + \array_push( + $parts, + \sprintf( + '%s%s', + $key, + ( + ($value === null) + ? '' + : ('=' . $value) + ) + ) + ); + } + return \implode('&', $parts); + } +} + + +/** + * @todo prüfen ob PHP-interne Funktion verwendbar ist + * @return array {(null|map)} + */ +function form_decode( + ?string $data_encoded +) : array +{ + if ($data_encoded === null) { + return null; + } + else { + throw (new \Exception('not implemented')); + } +} + + +/** + * @param array $parts { + * record< + * ?scheme:(null|string), + * ?host:(null|string), + * ?path:(null|list), + * ?query:(null|string), + * ?hash:(null|string), + * > + * } + */ +function encode( + array $parts +) : string +{ + $parts = \array_merge( + [ + 'scheme' => null, + 'host' => null, + 'path' => null, + 'query' => null, + 'hash' => null, + ], + $parts + ); + if ( + ($parts['scheme'] === null) + && + ($parts['host'] === null) + && + ($parts['path'] === null) + && + ($parts['query'] === null) + && + ($parts['hash'] === null) + ) { + throw (new \Exception('url.encode: at least one part has to be defined')); + } + else { + if ( + ($parts['scheme'] !== null) + && + ($parts['host'] === null) + && + ($parts['path'] === null) + && + ($parts['query'] === null) + && + ($parts['hash'] === null) + ) { + throw (new \Exception('url.encode: a sole scheme is insufficient to form a URL')); + } + else { + return \sprintf( + '%s%s%s%s%s', + (($parts['scheme'] === null) ? '' : ($parts['scheme'] . '://')), + (($parts['host'] === null) ? '' : ($parts['host'])), + (($parts['path'] === null) ? '' : ('/' . \implode('/', $parts['path']))), + (($parts['query'] === null) ? '' : ('?' . $parts['query'])), + (($parts['hash'] === null) ? '' : ('#' . $parts['hash'])) + ); + } + } +} + + +/** + */ +function decode( + string $url_encoded +) : array +{ + throw (new \Exception('not implemented')); +} + + ?> diff --git a/lib/alveolata/url/test.spec.json b/lib/alveolata/url/test.spec.json new file mode 100644 index 0000000..2f72ae7 --- /dev/null +++ b/lib/alveolata/url/test.spec.json @@ -0,0 +1,179 @@ +{ + "active": true, + "sections": [ + { + "name": "query_encode", + "active": true, + "execution": { + "call": "\\alveolata\\url\\form_encode({{data}})" + }, + "cases": [ + { + "name": "null", + "active": true, + "input": { + "data": null + }, + "output": { + "kind": "regular", + "value": null + } + }, + { + "name": "empty", + "active": true, + "input": { + "data": { + } + }, + "output": { + "kind": "regular", + "value": "" + } + }, + { + "name": "single_entry_with_null_value", + "active": true, + "input": { + "data": { + "foo": null + } + }, + "output": { + "kind": "regular", + "value": "foo" + } + }, + { + "name": "single_entry_with_string_value", + "active": true, + "input": { + "data": { + "foo": "bar" + } + }, + "output": { + "kind": "regular", + "value": "foo=bar" + } + }, + { + "name": "complex", + "active": true, + "input": { + "data": { + "foo": null, + "baz": "qux" + } + }, + "output": { + "kind": "regular", + "value": "foo&baz=qux" + } + } + ] + }, + { + "name": "encode", + "active": true, + "execution": { + "call": "\\alveolata\\url\\encode({{parts}})" + }, + "cases": [ + { + "name": "empty", + "active": true, + "input": { + "parts": { + } + }, + "output": { + "kind": "crashes" + } + }, + { + "name": "scheme_only", + "active": true, + "input": { + "parts": { + "scheme": "http" + } + }, + "output": { + "kind": "crashes" + } + }, + { + "name": "host_only", + "active": true, + "input": { + "parts": { + "host": "example.org" + } + }, + "output": { + "kind": "regular", + "value": "example.org" + } + }, + { + "name": "path_only", + "active": true, + "input": { + "parts": { + "path": ["foo", "bar"] + } + }, + "output": { + "kind": "regular", + "value": "/foo/bar" + } + }, + { + "name": "query_only", + "active": true, + "input": { + "parts": { + "query": "baz=2&qux=3" + } + }, + "output": { + "kind": "regular", + "value": "?baz=2&qux=3" + } + }, + { + "name": "hash_only", + "active": true, + "input": { + "parts": { + "hash": "blub" + } + }, + "output": { + "kind": "regular", + "value": "#blub" + } + }, + { + "name": "complex", + "active": true, + "input": { + "parts": { + "scheme": "git", + "host": "example.org", + "path": ["foo", "bar"], + "query": "baz=2&qux=3", + "hash": "blub" + } + }, + "output": { + "kind": "regular", + "value": "git://example.org/foo/bar?baz=2&qux=3#blub" + } + } + ] + } + ] +} + diff --git a/lib/alveolata/xml/abstract/interface.php b/lib/alveolata/xml/abstract/interface.php new file mode 100644 index 0000000..45dffa1 --- /dev/null +++ b/lib/alveolata/xml/abstract/interface.php @@ -0,0 +1,56 @@ +} + */ + function find( + \Closure $sub, + \Closure $predicate, + ?int $max_depth + ) : array + ; + + + /** + * shall replace nodes + */ + function transform( + \Closure $sub, + \Closure $function + ) + ; + + + /** + * shall convert the node to an array tree + */ + function to_raw( + \Closure $sub + ) + ; + + + /** + * shall convert the node to its string representation + */ + function to_string( + \Closure $sub, + int $depth + ) : string + ; + +} + + ?> diff --git a/lib/alveolata/xml/concrete/comment/implementation.php b/lib/alveolata/xml/concrete/comment/implementation.php new file mode 100644 index 0000000..b1423db --- /dev/null +++ b/lib/alveolata/xml/concrete/comment/implementation.php @@ -0,0 +1,93 @@ + + */ +class implementation_comment implements interface_node +{ + + /** + * @var \alveolata\xml\struct_comment + */ + private $subject; + + + /** + */ + public function __construct( + struct_comment $subject + ) + { + $this->subject = $subject; + } + + + /** + * @implementation + */ + public function find( + \Closure $sub, + \Closure $predicate, + ?int $max_depth + ) : array + { + return ( + ((! \is_null($max_depth)) && ($max_depth <= 0)) + ? [] + : ( + ($predicate)($this->subject) + ? [$this->subject] + : [] + ) + ); + } + + + /** + * @implementation + */ + public function transform( + \Closure $sub, + \Closure $function + ) + { + return ($function)($this->subject); + } + + + /** + * @implementation + */ + public function to_raw( + \Closure $sub + ) + { + return [ + 'kind' => 'comment', + 'data' => [ + 'content' => $this->subject->content, + ], + ]; + } + + + /** + * @implementation + */ + public function to_string( + \Closure $sub, + int $depth + ) : string + { + return (''); + } + +} + + ?> diff --git a/lib/alveolata/xml/concrete/comment/type.php b/lib/alveolata/xml/concrete/comment/type.php new file mode 100644 index 0000000..b09c724 --- /dev/null +++ b/lib/alveolata/xml/concrete/comment/type.php @@ -0,0 +1,29 @@ + + */ + public function __construct( + string $content + ) + { + $this->content = $content; + } + +} + + ?> diff --git a/lib/alveolata/xml/concrete/complex/implementation.php b/lib/alveolata/xml/concrete/complex/implementation.php new file mode 100644 index 0000000..3576dfc --- /dev/null +++ b/lib/alveolata/xml/concrete/complex/implementation.php @@ -0,0 +1,164 @@ + + */ +class implementation_complex implements interface_node +{ + + /** + * @var \alveolata\xml\struct_complex + */ + private $subject; + + + /** + */ + public function __construct( + struct_complex $subject + ) + { + $this->subject = $subject; + } + + + /** + * @implementation + */ + public function find( + \Closure $sub, + \Closure $predicate, + ?int $max_depth + ) : array + { + return ( + ((! \is_null($max_depth)) && ($max_depth <= 0)) + ? [] + : \alveolata\list_\reduce( + ( + \is_null($this->subject->children) + ? [] + : \alveolata\list_\map( + $this->subject->children, + fn($child) => ($sub)( + $child, + $predicate, + (\is_null($max_depth) ? null : ($max_depth - 1)) + ) + ) + ), + ( + ($predicate)($this->subject) + ? [$this->subject] + : [] + ), + fn($x, $y) => \array_merge($x, $y) + ) + ); + } + + + /** + * @implementation + */ + public function transform( + \Closure $sub, + \Closure $function + ) + { + return ($function)( + new struct_complex( + $this->subject->name, + $this->subject->attributes, + ( + \is_null($this->subject->children) + ? null + : \alveolata\list_\map( + $this->subject->children, + fn($child) => ($sub)($child, $function) + ) + ) + ) + ); + } + + + /** + * @implementation + */ + public function to_raw( + \Closure $sub + ) + { + return [ + 'kind' => 'complex', + 'data' => [ + 'name' => $this->subject->name, + 'attributes' => $this->subject->attributes, + 'children' => ( + \is_null($this->subject->children) + ? null + : \alveolata\list_\map( + $this->subject->children, + fn($child) => ($sub)($child) + ) + ), + ], + ]; + } + + + /** + * @implementation + */ + public function to_string( + \Closure $sub, + int $depth + ) : string + { + return \alveolata\string\coin( + ( + \is_null($this->subject->children) + ? '<{{name}}{{attributes}}/>' + : '<{{name}}{{attributes}}>{{children}}' + ), + [ + 'name' => $this->subject->name, + 'attributes' => \implode( + '', + \alveolata\list_\map( + \alveolata\list_\filter( + \array_keys($this->subject->attributes), + fn($key) => (! \is_null($this->subject->attributes[$key])) + ), + fn($key) => \alveolata\string\coin( + ' {{key}}="{{value}}"', + [ + 'key' => $key, + 'value' => $this->subject->attributes[$key], + ] + ) + ) + ), + 'children' => \implode( + '', + \alveolata\list_\map( + $this->subject->children, + fn($child) => ($sub)($child, ['depth' => ($depth + 1)]) + ) + ), + ] + ); + } + +} + + ?> diff --git a/lib/alveolata/xml/concrete/complex/type.php b/lib/alveolata/xml/concrete/complex/type.php new file mode 100644 index 0000000..36c2fc0 --- /dev/null +++ b/lib/alveolata/xml/concrete/complex/type.php @@ -0,0 +1,44 @@ +} + */ + public array $attributes; + + + /** + * @var ?array $children + */ + public ?array $children; + + + /** + */ + public function __construct( + string $name, + array $attributes, + ?array $children + ) + { + $this->name = $name; + $this->attributes = $attributes; + $this->children = $children; + } + +} + + ?> diff --git a/lib/alveolata/xml/concrete/document/implementation.php b/lib/alveolata/xml/concrete/document/implementation.php new file mode 100644 index 0000000..0dd6417 --- /dev/null +++ b/lib/alveolata/xml/concrete/document/implementation.php @@ -0,0 +1,125 @@ + + */ +class implementation_document implements interface_node +{ + + /** + * @var \alveolata\xml\struct_document + */ + private $subject; + + + /** + */ + public function __construct( + struct_document $subject + ) + { + $this->subject = $subject; + } + + + /** + * @implementation + */ + public function find( + \Closure $sub, + \Closure $predicate, + ?int $max_depth + ) : array + { + return ( + ((! \is_null($max_depth)) && ($max_depth <= 0)) + ? [] + : \alveolata\list_\reduce( + \alveolata\list_\map( + $this->subject->children, + fn($child) => ($sub)( + $child, + $predicate, + (\is_null($max_depth) ? null : ($max_depth - 1)) + ) + ), + ( + ($predicate)($this->subject) + ? [$this->subject] + : [] + ), + fn($x, $y) => \array_merge($x, $y) + ) + ); + } + + + /** + * @implementation + */ + public function transform( + \Closure $sub, + \Closure $function + ) + { + return ($function)( + new struct_document( + ( + \is_null($this->subject->children) + ? null + : \alveolata\list_\map( + $this->subject->children, + fn($child) => ($sub)($child, $function) + ) + ) + ) + ); + } + + + /** + * @implementation + */ + public function to_raw( + \Closure $sub + ) + { + return [ + 'kind' => 'document', + 'data' => [ + 'children' => \alveolata\list_\map( + $this->subject->children, + fn($child) => ($sub)($child) + ) + ], + ]; + } + + + /** + * @implementation + */ + public function to_string( + \Closure $sub, + int $depth + ) : string + { + return \implode( + '', + \alveolata\list_\map( + $this->subject->children, + fn($child) => ($sub)($child, ['depth' => ($depth + 1)]) + ) + ); + } + +} + + ?> diff --git a/lib/alveolata/xml/concrete/document/type.php b/lib/alveolata/xml/concrete/document/type.php new file mode 100644 index 0000000..fd9fc08 --- /dev/null +++ b/lib/alveolata/xml/concrete/document/type.php @@ -0,0 +1,29 @@ + + */ + public function __construct( + array $children + ) + { + $this->children = $children; + } + +} + + ?> diff --git a/lib/alveolata/xml/concrete/text/implementation.php b/lib/alveolata/xml/concrete/text/implementation.php new file mode 100644 index 0000000..d9581ce --- /dev/null +++ b/lib/alveolata/xml/concrete/text/implementation.php @@ -0,0 +1,93 @@ + + */ +class implementation_text implements interface_node +{ + + /** + * @var \alveolata\xml\struct_text + */ + private $subject; + + + /** + */ + public function __construct( + struct_text $subject + ) + { + $this->subject = $subject; + } + + + /** + * @implementation + */ + public function find( + \Closure $sub, + \Closure $predicate, + ?int $max_depth + ) : array + { + return ( + ((! \is_null($max_depth)) && ($max_depth <= 0)) + ? [] + : ( + ($predicate)($this->subject) + ? [$this->subject] + : [] + ) + ); + } + + + /** + * @implementation + */ + public function transform( + \Closure $sub, + \Closure $function + ) + { + return ($function)($this->subject); + } + + + /** + * @implementation + */ + public function to_raw( + \Closure $sub + ) + { + return [ + 'kind' => 'text', + 'data' => [ + 'content' => $this->subject->content, + ], + ]; + } + + + /** + * @implementation + */ + public function to_string( + \Closure $sub, + int $depth + ) : string + { + return $this->subject->content; + } + +} + + ?> diff --git a/lib/alveolata/xml/concrete/text/type.php b/lib/alveolata/xml/concrete/text/type.php new file mode 100644 index 0000000..42da60e --- /dev/null +++ b/lib/alveolata/xml/concrete/text/type.php @@ -0,0 +1,29 @@ + + */ + public function __construct( + string $content + ) + { + $this->content = $content; + } + +} + + ?> diff --git a/lib/alveolata/xml/functions.php b/lib/alveolata/xml/functions.php new file mode 100644 index 0000000..9b325c5 --- /dev/null +++ b/lib/alveolata/xml/functions.php @@ -0,0 +1,285 @@ + [], + 'children' => null, + ], + ($options ?? []) + ); + return ( + new struct_complex( + $name, + $options['attributes'], + $options['children'] + ) + ); +} + + +/** + */ +function make_document( + array $children +) : struct_document +{ + return ( + new struct_document( + $children + ) + ); +} + + +/** + */ +function _get_logic( + $node +) +{ + if ($node instanceof struct_text) { + return (new implementation_text($node)); + } + else if ($node instanceof struct_comment) { + return (new implementation_comment($node)); + } + else if ($node instanceof struct_complex) { + return (new implementation_complex($node)); + } + else if ($node instanceof struct_document) { + return (new implementation_document($node)); + } + else { + var_dump($node); + throw (new \Exception('unhandled struct')); + } +} + + +/** + */ +function find( + $node, + \Closure $predicate, + $options = null +) : array +{ + $options = \array_merge( + [ + 'max_depth' => null, + ], + ($options ?? []) + ); + return _get_logic($node)->find(fn($x, $y, $z) => find($x, $y, $z), $predicate, $options['max_depth']); +} + + +/** + * shortcut for xpath/css like navigation + * + * @param array $path {list} + */ +function walk( + $node, + array $path +) +{ + if (\count($path) <= 0) { + return $node; + } + else { + $hits = find( + $node, + fn($x) => ( + ($x instanceof struct_complex) + && + ($x->name === $path[0]) + ), + [ + 'max_depth' => 1, + ] + ); + if (\count($hits) <= 0) { + throw (new \Exception('not found: ' . $path[0])); + } + else if (\count($hits) >= 2) { + throw (new \Exception('ambiguous: ' . $path[0])); + } + else { + return walk( + $hits[0], + \array_slice($path, 1) + ); + } + } +} + + +/** + */ +function transform( + $node, + \Closure $function +) +{ + return _get_logic($node)->transform(fn($x, $y) => transform($x, $y), $function); +} + + +/** + */ +function to_raw( + $node +) +{ + return _get_logic($node)->to_raw(fn($x) => to_raw($x)); +} + + +/** + */ +function to_string( + $node, + $options = null +) +{ + $options = \array_merge( + [ + 'depth' => 0, + ], + ($options ?? []) + ); + return _get_logic($node)->to_string(fn($x, $y) => to_string($x, $y), $options['depth']); +} + + +/** + */ +function _convert( + \DOMNode $doc +) +{ + if ($doc instanceof \DOMComment) { + return make_comment($doc->data); + } + else if ($doc instanceof \DOMText) { + $content = \trim($doc->nodeValue); + return ( + empty($content) + ? null + : make_text($content) + ); + } + else if ($doc instanceof \DOMElement) { + $attributes = []; + foreach ($doc->attributes as $attribute_raw) { + $attributes[$attribute_raw->name] = $attribute_raw->value; + } + $children = []; + foreach ($doc->childNodes as $child_raw) { + $child = _convert($child_raw); + if (\is_null($child)) { + // do nothing + } + else { + \array_push($children, $child); + } + } + return make_complex($doc->nodeName, ['attributes' => $attributes, 'children' => $children]); + } + else if ($doc instanceof \DOMDocument) { + $children = []; + foreach ($doc->childNodes as $child_raw) { + $child = _convert($child_raw); + if (\is_null($child)) { + // do nothing + } + else { + \array_push($children, $child); + } + } + return make_document($children); + } + else { + var_dump($doc); + throw (new \Exception('unhandled node type: ' . \strval($doc))); + } +} + + +/** + * @see https://www.php.net/manual/en/class.domdocument.php + * @see https://www.php.net/manual/en/domdocument.loadxml.php + * @see https://www.php.net/manual/en/libxml.constants.php + */ +function parse( + string $xml +) +{ + $doc = new \DOMDocument(); + $doc->loadXML(\sprintf('<_dummy>%s', $xml), 0); + $out = _convert($doc); + return ( + (\count($out->children[0]->children) === 1) + ? $out->children[0]->children[0] + : make_document($out->children[0]->children) + ); +} + + +/** + * just a shortcut + */ +function parse_raw( + string $xml +) +{ + $node = parse($xml); + return to_raw($node); +} + + ?> diff --git a/lib/alveolata/xml/test.spec.php b/lib/alveolata/xml/test.spec.php new file mode 100644 index 0000000..944ac25 --- /dev/null +++ b/lib/alveolata/xml/test.spec.php @@ -0,0 +1,408 @@ + 'alveolata', + 'sections' => [ + [ + 'name' => 'xml', + 'setup' => function (&$environment) { + }, + 'sections' => [ + [ + 'name' => 'to_raw', + 'cases' => [ + [ + 'name' => 'comment', + 'procedure' => function ($assert, &$environment) { + $xmlnode = \alveolata\xml\make_comment('foo'); + $output_actual = \alveolata\xml\to_raw($xmlnode); + $output_expected = [ + 'kind' => 'comment', + 'data' => ['content' => 'foo'], + ]; + $assert->equal($output_expected, $output_actual); + }, + ], + [ + 'name' => 'text', + 'procedure' => function ($assert, &$environment) { + $xmlnode = \alveolata\xml\make_text('foo'); + $output_actual = \alveolata\xml\to_raw($xmlnode); + $output_expected = [ + 'kind' => 'text', + 'data' => ['content' => 'foo'], + ]; + $assert->equal($output_expected, $output_actual); + }, + ], + [ + 'name' => 'complex', + 'procedure' => function ($assert, &$environment) { + $xmlnode = \alveolata\xml\make_complex( + 'foo', + [ + 'attributes' => [ + 'bar' => '0', + 'baz' => '1', + ], + 'children' => [ + \alveolata\xml\make_text('qux'), + ] + ] + ); + $output_actual = \alveolata\xml\to_raw($xmlnode); + $output_expected = [ + 'kind' => 'complex', + 'data' => [ + 'name' => 'foo', + 'attributes' => [ + 'bar' => '0', + 'baz' => '1', + ], + 'children' => [ + [ + 'kind' => 'text', + 'data' => [ + 'content' => 'qux', + ] + ], + ] + ] + ]; + $assert->equal($output_expected, $output_actual); + }, + ], + [ + 'name' => 'document', + 'procedure' => function ($assert, &$environment) { + $xmlnode = \alveolata\xml\make_document( + [ + \alveolata\xml\make_text('foo'), + \alveolata\xml\make_text('bar'), + ] + ); + $output_actual = \alveolata\xml\to_raw($xmlnode); + $output_expected = [ + 'kind' => 'document', + 'data' => [ + 'children' => [ + [ + 'kind' => 'text', + 'data' => [ + 'content' => 'foo', + ] + ], + [ + 'kind' => 'text', + 'data' => [ + 'content' => 'bar', + ] + ], + ] + ] + ]; + $assert->equal($output_expected, $output_actual); + }, + ], + ], + ], + [ + 'name' => 'to_string', + 'cases' => [ + [ + 'name' => 'comment', + 'procedure' => function ($assert, &$environment) { + $xmlnode = \alveolata\xml\make_comment('foo'); + $output_actual = \alveolata\xml\to_string($xmlnode); + $output_expected = ''; + $assert->equal($output_expected, $output_actual); + }, + ], + [ + 'name' => 'text', + 'procedure' => function ($assert, &$environment) { + $xmlnode = \alveolata\xml\make_text('foo'); + $output_actual = \alveolata\xml\to_string($xmlnode); + $output_expected = 'foo'; + $assert->equal($output_expected, $output_actual); + }, + ], + [ + 'name' => 'complex', + 'procedure' => function ($assert, &$environment) { + $xmlnode = \alveolata\xml\make_complex( + 'foo', + [ + 'attributes' => [ + 'bar' => '0', + 'baz' => '1', + ], + 'children' => [ + alveolata\xml\make_text('qux'), + ] + ] + ); + $output_actual = \alveolata\xml\to_string($xmlnode); + $output_expected = 'qux'; + $assert->equal($output_expected, $output_actual); + }, + ], + [ + 'name' => 'document', + 'procedure' => function ($assert, &$environment) { + $xmlnode = \alveolata\xml\make_document( + [ + \alveolata\xml\make_text('foo'), + \alveolata\xml\make_text('bar'), + ] + ); + $output_actual = \alveolata\xml\to_string($xmlnode); + $output_expected = 'foobar'; + $assert->equal($output_expected, $output_actual); + }, + ], + ], + ], + [ + 'name' => 'find', + 'cases' => [ + [ + 'name' => 'oblique', + 'procedure' => function ($assert) { + $node = \alveolata\xml\make_complex( + 'foo', + [ + 'attributes' => [ + 'one' => '1', + 'two' => '2', + ], + 'children' => [ + \alveolata\xml\make_complex( + 'bar', + [ + 'attributes' => [ + 'three' => 3, + 'four' => 4, + ], + 'subnode' => null + ] + ), + \alveolata\xml\make_complex( + 'baz', + [ + 'attributes' => [ + 'two' => 2, + 'five' => 5, + ], + 'subnode' => null + ] + ), + ] + ] + ); + + // exec + $result = \alveolata\xml\find( + $node, + fn($x) => ( + ($x instanceof \alveolata\xml\struct_complex) + && + \array_key_exists('two', $x->attributes) + ) + ); + + // assertions + $assert->equal( + ( + (\count($result) === 2) + && + ( + ( + ($result[0]->name === 'foo') + && + ($result[1]->name === 'baz') + ) + || + ( + ($result[1]->name === 'foo') + && + ($result[0]->name === 'baz') + ) + ) + ), + true + ); + } + ], + ] + ], + [ + 'name' => 'transform', + 'cases' => [ + [ + 'name' => 'oblique', + 'procedure' => function ($assert) { + $node = \alveolata\xml\make_complex( + 'foo', + [ + 'attributes' => [ + 'one' => '1', + 'two' => '2', + ], + 'children' => [ + \alveolata\xml\make_complex( + 'bar', + [ + 'attributes' => [ + 'three' => 3, + 'four' => 4, + ], + 'subnode' => \alveolata\xml\make_text('test') + ] + ), + ] + ] + ); + + // exec + $result_actual = \alveolata\xml\transform( + $node, + fn($x) => ( + ( + ($x instanceof \alveolata\xml\struct_complex) + && + ($x->name === 'bar') + ) + ? \alveolata\xml\make_complex( + 'baz', + [ + 'attributes' => $x->attributes, + 'children' => $x->children, + ] + ) + : $x + ) + ); + + // assertions + $result_expected = \alveolata\xml\make_complex( + 'foo', + [ + 'attributes' => [ + 'one' => '1', + 'two' => '2', + ], + 'children' => [ + \alveolata\xml\make_complex( + 'baz', + [ + 'attributes' => [ + 'three' => 3, + 'four' => 4, + ], + 'subnode' => \alveolata\xml\make_text('test') + ] + ), + ] + ] + ); + $assert->equal(\alveolata\xml\to_raw($result_actual), \alveolata\xml\to_raw($result_expected)); + } + ], + ] + ], + [ + 'name' => 'parse', + 'cases' => \alveolata\list_\map( + [ + [ + 'name' => 'comment', + 'input' => '', + 'output' => [ + 'kind' => 'comment', + 'data' => [ + 'content' => 'foo' + ] + ] + ], + [ + 'name' => 'text', + 'input' => 'foo', + 'output' => [ + 'kind' => 'text', + 'data' => [ + 'content' => 'foo' + ] + ] + ], + [ + 'name' => 'complex', + 'input' => 'bla', + 'output' => [ + 'kind' => 'complex', + 'data' => [ + 'name' => 'foo', + 'attributes' => [ + 'bar' => '0', + 'baz' => '1', + ], + 'children' => [ + [ + 'kind' => 'text', + 'data' => [ + 'content' => 'bla' + ] + ], + ] + ] + ] + ], + [ + 'name' => 'document', + 'input' => 'bar', + 'output' => [ + 'kind' => 'document', + 'data' => [ + 'children' => [ + [ + 'kind' => 'comment', + 'data' => [ + 'content' => 'foo' + ] + ], + [ + 'kind' => 'text', + 'data' => [ + 'content' => 'bar' + ] + ], + ] + ] + ] + ], + ], + fn($case) => [ + 'name' => $case['name'], + 'procedure' => function ($assert) use ($case) { + $assert->equal( + \alveolata\xml\parse_raw( + $case['input'] + ), + $case['output'] + ); + } + ] + ), + ], + ], + ] + ] + ] +); + + ?> diff --git a/source/entities/doc.php b/source/backend/entities/doc.php similarity index 100% rename from source/entities/doc.php rename to source/backend/entities/doc.php diff --git a/source/backend/main.php b/source/backend/main.php new file mode 100644 index 0000000..ac30d99 --- /dev/null +++ b/source/backend/main.php @@ -0,0 +1,103 @@ + 'rosavox', + ] + ); + \alveolata\rest\register( + $rest, + \alveolata\http\enum_method::get, + 'meta/ping', + [ + 'execution' => function ($version, $path_parameters, $headers, $input) { + return [ + 'status_code' => 200, + 'data' => 'pong', + ]; + }, + ] + ); + \alveolata\rest\register( + $rest, + \alveolata\http\enum_method::get, + 'doc', + [ + 'execution' => function ($version, $path_parameters, $headers, $input) { + return [ + 'status_code' => 200, + 'data' => \alveolata\list_\map( + \rosavox\services\doc\dump(), + function ($entry) { + return [ + 'id' => $entry['id'], + 'doc' => [ + 'title' => $entry['value']->title + ] + ]; + } + ), + ]; + }, + ] + ); + \alveolata\rest\register( + $rest, + \alveolata\http\enum_method::get, + 'doc/{id}', + [ + 'execution' => function ($version, $path_parameters, $headers, $input) { +\alveolata\log\info('req', ['path_parameters' => $path_parameters]); + $id = \intval($path_parameters['id']); + $doc = \rosavox\services\doc\get($id); + return [ + 'status_code' => 200, + 'data' => [ + 'title' => $doc->title, + ], + ]; + }, + ] + ); + + // exec + $http_request = \alveolata\cgi\get_http_request(); + \alveolata\log\info( + 'http_request', + [ + 'http_request' => $http_request, + ] + ); + $http_response = \alveolata\rest\call($rest, $http_request); + \alveolata\log\info( + 'http_response', + [ + 'http_response' => $http_response, + ] + ); + \alveolata\cgi\put_http_response($http_response); +} + + +main(); + + ?> diff --git a/source/repositories/doc.php b/source/backend/repositories/doc.php similarity index 100% rename from source/repositories/doc.php rename to source/backend/repositories/doc.php diff --git a/source/scripts/generate-audio b/source/backend/scripts/generate-audio similarity index 100% rename from source/scripts/generate-audio rename to source/backend/scripts/generate-audio diff --git a/source/services/doc.php b/source/backend/services/doc.php similarity index 100% rename from source/services/doc.php rename to source/backend/services/doc.php diff --git a/source/index.html.php b/source/frontend/index.html.php similarity index 100% rename from source/index.html.php rename to source/frontend/index.html.php diff --git a/source/main.php b/source/frontend/main.php similarity index 100% rename from source/main.php rename to source/frontend/main.php diff --git a/source/nav.php b/source/frontend/nav.php similarity index 100% rename from source/nav.php rename to source/frontend/nav.php diff --git a/source/renderings.php b/source/frontend/renderings.php similarity index 100% rename from source/renderings.php rename to source/frontend/renderings.php diff --git a/source/style.css b/source/frontend/style.css similarity index 100% rename from source/style.css rename to source/frontend/style.css diff --git a/source/templates/docs-edit.html.tpl b/source/frontend/templates/docs-edit.html.tpl similarity index 100% rename from source/templates/docs-edit.html.tpl rename to source/frontend/templates/docs-edit.html.tpl diff --git a/source/templates/docs-list-entry.html.tpl b/source/frontend/templates/docs-list-entry.html.tpl similarity index 100% rename from source/templates/docs-list-entry.html.tpl rename to source/frontend/templates/docs-list-entry.html.tpl diff --git a/source/templates/docs-list.html.tpl b/source/frontend/templates/docs-list.html.tpl similarity index 100% rename from source/templates/docs-list.html.tpl rename to source/frontend/templates/docs-list.html.tpl diff --git a/source/templates/download.html.tpl b/source/frontend/templates/download.html.tpl similarity index 100% rename from source/templates/download.html.tpl rename to source/frontend/templates/download.html.tpl diff --git a/source/templates/player.html.tpl b/source/frontend/templates/player.html.tpl similarity index 100% rename from source/templates/player.html.tpl rename to source/frontend/templates/player.html.tpl diff --git a/source/templates/state-waiting.html.tpl b/source/frontend/templates/state-waiting.html.tpl similarity index 100% rename from source/templates/state-waiting.html.tpl rename to source/frontend/templates/state-waiting.html.tpl diff --git a/tools/build b/tools/build index 0e215d7..7faf8dc 100755 --- a/tools/build +++ b/tools/build @@ -8,8 +8,38 @@ dir_source="source" ## exec -mkdir -p ${dir_build} -cp -r -u lib/* ${dir_build}/ -cp -r -u -v ${dir_source}/* ${dir_build}/ -cd ${dir_build} && ln -f -s index.html.php index.php ; cd - -mkdir -p ${dir_build}/docs +### exec:backend + +dir_build_backend="${dir_build}/backend" + +mkdir -p ${dir_build_backend}/lib/alveolata +cp -r -u lib/alveolata/* ${dir_build_backend}/lib/alveolata/ + +mkdir -p ${dir_build_backend}/lib/piper +cp -r -u lib/piper/* ${dir_build_backend}/lib/piper/ + +mkdir -p ${dir_build_backend}/helpers +cp -r -u -v ${dir_source}/helpers/* ${dir_build_backend}/helpers/ +cp -r -u -v ${dir_source}/strings.json ${dir_build_backend}/ + +mkdir -p ${dir_build_backend} +cp -r -u -v ${dir_source}/backend/* ${dir_build}/backend/ +cd ${dir_build_backend} && ln -f -s main.php index.php ; cd - + +mkdir -p ${dir_build_backend}/docs + + +### exec:frontend + +dir_build_frontend="${dir_build}/frontend" + +# mkdir -p ${dir_build_frontend}/lib/alveolata +# cp -r -u lib/alveolata/* ${dir_build_frontend}/lib/alveolata/ + +mkdir -p ${dir_build_frontend}/helpers +cp -r -u -v ${dir_source}/helpers/* ${dir_build_frontend}/helpers/ +cp -r -u -v ${dir_source}/strings.json ${dir_build_frontend}/ + +mkdir -p ${dir_build_frontend} +cp -r -u -v ${dir_source}/frontend/* ${dir_build_frontend}/ +cd ${dir_build_frontend} && ln -f -s index.html.php index.php ; cd - diff --git a/tools/update-alveolata b/tools/update-alveolata new file mode 100755 index 0000000..549ec22 --- /dev/null +++ b/tools/update-alveolata @@ -0,0 +1,24 @@ +#!/usr/bin/env bash + +## consts + +dir_lib="lib" +dir_source="${HOME}/projekte/greenscale/libs/alveolata" + + +## vars + +dir_target="${dir_lib}/alveolata" + + +## exec + +mkdir -p ${dir_target} +rsync \ + --recursive \ + --update \ + --delete \ + --exclude=".git" \ + --verbose \ + ${dir_source}/ \ + ${dir_target}