commit cc835b36927bd3352605f46271484de42d6b606b Author: Maxime Renou Date: Tue Jul 9 11:31:28 2024 +0200 first commit diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..96ef9c2 --- /dev/null +++ b/composer.json @@ -0,0 +1,35 @@ +{ + "name": "bluesquare/pilot-sdk", + "description": "Pilot peer implementation.", + "keywords": [ + "package", + "bluesquare", + "pilot" + ], + "homepage": "https://git.bluesquare.io/bluesquare/pilot-sdk", + "license": "proprietary", + "authors": [ + { + "name": "Maxime Renou", + "email": "maxime@bluesquare.io", + "homepage": "https://bluesquare.io/" + } + ], + "minimum-stability": "dev", + "autoload": { + "psr-4": { + "Bluesquare\\Pilot\\": "src/" + } + }, + "extra": { + "laravel": { + "providers": [ + "Bluesquare\\Pilot\\Laravel\\PilotServiceProvider" + ] + } + }, + "require": { + "php": "^7.3|^8.0" + }, + "prefer-stable": true +} diff --git a/src/Entity/Action.php b/src/Entity/Action.php new file mode 100644 index 0000000..4aaba59 --- /dev/null +++ b/src/Entity/Action.php @@ -0,0 +1,42 @@ + [ + 'type' => 'action', + 'action' => compact('type', 'title', 'message', 'button', 'url', 'files'), + ], + ]; + } + + public function file( + $contents, + $name = '', + $type = '' + ) { + return [ + 'file' => compact('contents', 'name', 'type'), + ]; + } + + public function error($message) + { + return [ + 'json' => [ + 'type' => 'error', + 'error' => $message, + ], + ]; + } +} diff --git a/src/Entity/Entity.php b/src/Entity/Entity.php new file mode 100644 index 0000000..bbf87aa --- /dev/null +++ b/src/Entity/Entity.php @@ -0,0 +1,16 @@ +slug; + } +} diff --git a/src/Entity/Metric.php b/src/Entity/Metric.php new file mode 100644 index 0000000..c5d8ef0 --- /dev/null +++ b/src/Entity/Metric.php @@ -0,0 +1,116 @@ +data = $data; + + $cache_key = 'pilot-metric-'.$this->slug.'-'.json_encode($data); + + if ($this->cache !== false && cache()->has($cache_key)) { + $computed = cache()->get($cache_key); + } + else { + $computed = $this->compute( + $data['entry'] ?? null + ); + + if ($this->cache !== false) { + cache()->put($cache_key, $computed, $this->cache); + } + } + + return $this->output($computed); + } + + public function output($data) + { + return [ + 'json' => [ + 'metric' => $data + ] + ]; + } + + protected function resolveFilter($filters) + { + $filter = collect($filters)->firstWhere('value', $this->data['filter'] ?? null) ?? collect($filters)->first(); + + $computed = is_array($filter) ? $filter['callback']($this->data) : []; + + return [ + 'filter' => $filter['value'], + 'filters' => collect($filters)->map(function ($filter) { + return [ + 'label' => $filter['label'], + 'value' => $filter['value'], + ]; + })->values()->all(), + ...$computed + ]; + } + + public function filter($label, $value, callable $callback) + { + return ['label' => $label, 'value' => $value, 'callback' => $callback]; + } + + public function filters($filters) + { + return $this->resolveFilter($filters); + } + + public function progress($current, $target) + { + return $this->output([ + 'type' => 'progress', + 'value' => $current, + 'target' => $target, + ]); + } + + public function trend($value, $previous = null) + { + return [ + 'type' => 'trend', + 'value' => $value, + 'previous' => $previous, + ]; + } + + public function partition($values) + { + return [ + 'type' => 'partition', + 'values' => $values, + ]; + } + + public function value($value) + { + return [ + 'type' => 'value', + 'value' => $value, + ]; + } + + public function monitor($available) + { + return [ + 'type' => 'monitor', + 'value' => $available, + ]; + } +} diff --git a/src/Entity/Widget.php b/src/Entity/Widget.php new file mode 100644 index 0000000..6a83141 --- /dev/null +++ b/src/Entity/Widget.php @@ -0,0 +1,8 @@ +mergeConfigFrom( + __DIR__.'/config/pilot.php', 'pilot' + ); + } + + /** + * Bootstrap services. + */ + public function boot(): void + { + $this->publishes([ + __DIR__.'/config/pilot.php' => config_path('pilot.php'), + ], 'config'); + + $this->registerPilot(); + + Route::any('api/pilot', function (Request $request) { + return $this->handleRequest($request); + }); + } + + protected function registerPilot() + { + $this->pilot = new Pilot; + + try { + $actions = app('files')->allFiles(app_path('Pilot/Actions')); + + foreach ($actions as $action) { + $class = 'App\\Pilot\\Actions\\' . $action->getBasename('.php'); + + if (class_exists($class)) { + $action = new $class; + $this->pilot->action($action->slug(), [$action, 'handle']); + } + } + } + catch (\Exception $e) {} + + try { + $metrics = app('files')->allFiles(app_path('Pilot/Metrics')); + + foreach ($metrics as $metric) { + $class = 'App\\Pilot\\Metrics\\' . $metric->getBasename('.php'); + + if (class_exists($class)) { + $metric = new $class; + $this->pilot->metric($metric->slug(), [$metric, 'handle']); + } + } + } + catch (\Exception $e) {} + } + + protected function handleRequest(Request $request) + { + if (empty(config('pilot.key')) || config('pilot.key') != $request->bearerToken()) { + abort(403); + } + + $output = $this->pilot->handle($request->all(), $request->files->all()); + + if (isset($output['json'])) { + return response()->json($output['json']); + } + + if (isset($output['file'])) { + return response()->download( + $output['file']['content'], + $output['file']['name'], + [ + 'Content-Type' => $output['file']['type'], + ] + ); + } + + return response(); + } +} diff --git a/src/Laravel/config/pilot.php b/src/Laravel/config/pilot.php new file mode 100644 index 0000000..9485121 --- /dev/null +++ b/src/Laravel/config/pilot.php @@ -0,0 +1,7 @@ + env('PILOT_KEY', null), + +]; diff --git a/src/Pilot.php b/src/Pilot.php new file mode 100644 index 0000000..024ead3 --- /dev/null +++ b/src/Pilot.php @@ -0,0 +1,98 @@ + [], + 'metric' => [], + ]; + + public function __construct() + { + + } + + public function authorize($token, $user_token) + { + if ($token !== $user_token) { + throw new \Exception('Unauthorized Pilot request'); + } + } + + public function action($slug, callable $function) + { + $this->register('action', $slug, $function); + } + + public function metric($slug, callable $function) + { + $this->register('metric', $slug, $function); + } + + protected function register($type, $slug, callable $function) + { + $this->registry[$type][$slug] = $function; + } + + public function handle($data = [], $files = []) + { + $type = $data['type'] ?? null; + $slug = $data['slug'] ?? null; + + if (isset($this->registry[$type]) && isset($this->registry[$type][$slug])) { + $params = $this->resolveDependencies($slug, $this->registry[$type][$slug], $data, $files); + + return call_user_func_array($this->registry[$type][$slug], $params); + } + + return Response::error("Unknown {$type} {$slug}"); + } + + public function resolveDependencies($slug, $function, $data, $files) + { + $entries = isset($data['entries']) && is_array($data['entries']) ? $data['entries'] : []; + + $input = [ + 'data' => $data, + 'filter' => $data['filter'] ?? null, + 'files' => $files, + 'entries' => $entries, + 'entry' => $entries[0] ?? null, + ]; + + $output = []; + + $ref = is_array($function) ? new \ReflectionMethod($function[0], $function[1]) : new \ReflectionFunction($function); + + foreach ($ref->getParameters() as $parameter) { + if (isset($input[$parameter->getName()])) { + $output[$parameter->getName()] = $input[$parameter->getName()]; + } elseif (! $parameter->isOptional()) { + throw new \Exception("Pilot[$slug]: unknown required parameter '{$parameter->getName()}'"); + } + } + + return $output; + } + + public function serve($data) + { + if (isset($data['json'])) { + header('content-type: application/json'); + echo json_encode($data['json']); + } elseif (isset($data['file'])) { + header('content-type: '.$data['file']['type']); + + if (! empty($data['file']['name'])) { + header('content-disposition: attachment; filename="'.$data['file']['name'].'"'); + } + + echo $data['file']['content']; + } + } +} diff --git a/src/Response.php b/src/Response.php new file mode 100644 index 0000000..0fb2544 --- /dev/null +++ b/src/Response.php @@ -0,0 +1,42 @@ + [ + 'type' => 'action', + 'action' => compact('type', 'title', 'message', 'button', 'url', 'files'), + ], + ]; + } + + public static function error($message) + { + return [ + 'json' => [ + 'type' => 'error', + 'error' => $message, + ], + ]; + } + + public static function file( + $contents, + $name = '', + $type = '' + ) { + return [ + 'file' => compact('contents', 'name', 'type'), + ]; + } +}