Multiple fixes
* Implemented ScriptRunner with environment expansion and cleaner code. * Added ApiClient plugin (com.noccy.apiclient) * Renamed CHANGELOG.md to VERSIONS.md * Shuffled buildtools * Added first unittests
This commit is contained in:
		
							
								
								
									
										84
									
								
								plugins/com.noccy.apiclient/Api/Catalog.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								plugins/com.noccy.apiclient/Api/Catalog.php
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,84 @@
 | 
			
		||||
<?php // "name":"Call on web APIs", "author":"Noccy"
 | 
			
		||||
 | 
			
		||||
namespace SparkPlug\Com\Noccy\ApiClient\Api;
 | 
			
		||||
 | 
			
		||||
use JsonSerializable;
 | 
			
		||||
 | 
			
		||||
class Catalog implements JsonSerializable
 | 
			
		||||
{
 | 
			
		||||
    private array $properties = [];
 | 
			
		||||
 | 
			
		||||
    private array $methods = [];
 | 
			
		||||
 | 
			
		||||
    private ?string $name;
 | 
			
		||||
 | 
			
		||||
    private ?string $info;
 | 
			
		||||
 | 
			
		||||
    public function __construct(array $catalog=[])
 | 
			
		||||
    {
 | 
			
		||||
        $catalog = $catalog['catalog']??[];
 | 
			
		||||
        $this->name = $catalog['name']??null;
 | 
			
		||||
        $this->info = $catalog['info']??null;
 | 
			
		||||
        foreach ($catalog['props']??[] as $k=>$v) {
 | 
			
		||||
            $this->properties[$k] = $v;
 | 
			
		||||
        }
 | 
			
		||||
        foreach ($catalog['methods']??[] as $k=>$v) {
 | 
			
		||||
            $this->methods[$k] = new Method($v);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static function createFromFile(string $filename): Catalog
 | 
			
		||||
    {
 | 
			
		||||
        $json = file_get_contents($filename);
 | 
			
		||||
        $catalog = json_decode($json, true);
 | 
			
		||||
        $catalog['name'] = basename($filename, ".json");
 | 
			
		||||
        return new Catalog($catalog);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getName(): ?string
 | 
			
		||||
    {
 | 
			
		||||
        return $this->name;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getInfo(): ?string
 | 
			
		||||
    {
 | 
			
		||||
        return $this->info;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getProperties(): array
 | 
			
		||||
    {
 | 
			
		||||
        return $this->properties;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function applyProperties(array $props)
 | 
			
		||||
    {
 | 
			
		||||
        $this->properties = array_merge($this->properties, $props);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function addMethod(string $name, Method $method)
 | 
			
		||||
    {
 | 
			
		||||
        $this->methods[$name] = $method;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getMethod(string $method): ?Method
 | 
			
		||||
    {
 | 
			
		||||
        return $this->methods[$method]??null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getMethods(): array
 | 
			
		||||
    {
 | 
			
		||||
        return $this->methods;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function jsonSerialize(): mixed
 | 
			
		||||
    {
 | 
			
		||||
        return [
 | 
			
		||||
            'catalog' => [
 | 
			
		||||
                'name' => $this->name,
 | 
			
		||||
                'info' => $this->info,
 | 
			
		||||
                'props' => $this->properties,
 | 
			
		||||
                'methods' => $this->methods,
 | 
			
		||||
            ]
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										36
									
								
								plugins/com.noccy.apiclient/Api/Method.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								plugins/com.noccy.apiclient/Api/Method.php
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,36 @@
 | 
			
		||||
<?php // "name":"Call on web APIs", "author":"Noccy"
 | 
			
		||||
 | 
			
		||||
namespace SparkPlug\Com\Noccy\ApiClient\Api;
 | 
			
		||||
 | 
			
		||||
use JsonSerializable;
 | 
			
		||||
 | 
			
		||||
class Method implements JsonSerializable
 | 
			
		||||
{
 | 
			
		||||
    private array $properties = [];
 | 
			
		||||
 | 
			
		||||
    private ?string $info;
 | 
			
		||||
 | 
			
		||||
    public function __construct(array $method)
 | 
			
		||||
    {
 | 
			
		||||
        $this->properties = $method['props']??[];
 | 
			
		||||
        $this->info = $method['info']??null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getProperties(): array
 | 
			
		||||
    {
 | 
			
		||||
        return $this->properties;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getInfo(): ?string
 | 
			
		||||
    {
 | 
			
		||||
        return $this->info;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function jsonSerialize(): mixed
 | 
			
		||||
    {
 | 
			
		||||
        return [
 | 
			
		||||
            'info' => $this->info,
 | 
			
		||||
            'props' => $this->properties,
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										13
									
								
								plugins/com.noccy.apiclient/Api/Profile.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								plugins/com.noccy.apiclient/Api/Profile.php
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,13 @@
 | 
			
		||||
<?php // "name":"Call on web APIs", "author":"Noccy"
 | 
			
		||||
 | 
			
		||||
namespace SparkPlug\Com\Noccy\ApiClient\Api;
 | 
			
		||||
 | 
			
		||||
class Profile
 | 
			
		||||
{
 | 
			
		||||
    private array $properties = [];
 | 
			
		||||
 | 
			
		||||
    public function getProperties(): array
 | 
			
		||||
    {
 | 
			
		||||
        return $this->properties;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										91
									
								
								plugins/com.noccy.apiclient/Commands/ApiCatalogCommand.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										91
									
								
								plugins/com.noccy.apiclient/Commands/ApiCatalogCommand.php
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,91 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
namespace SparkPlug\Com\Noccy\ApiClient\Commands;
 | 
			
		||||
 | 
			
		||||
use Spark\Commands\Command;
 | 
			
		||||
use SparkPlug;
 | 
			
		||||
use SparkPlug\Com\Noccy\ApiClient\Api\Catalog;
 | 
			
		||||
use Symfony\Component\Console\Input\InputArgument;
 | 
			
		||||
use Symfony\Component\Console\Input\InputInterface;
 | 
			
		||||
use Symfony\Component\Console\Input\InputOption;
 | 
			
		||||
use Symfony\Component\Console\Output\OutputInterface;
 | 
			
		||||
 | 
			
		||||
class ApiCatalogCommand extends Command
 | 
			
		||||
{
 | 
			
		||||
    protected function configure()
 | 
			
		||||
    {
 | 
			
		||||
        $this->setName("api:catalog")
 | 
			
		||||
            ->setDescription("Manage the API catalogs")
 | 
			
		||||
            ->addOption("create", "c", InputOption::VALUE_REQUIRED, "Create a new catalog")
 | 
			
		||||
            ->addOption("remove", "r", InputOption::VALUE_REQUIRED, "Remove a catalog")
 | 
			
		||||
            ->addOption("set-props", null, InputOption::VALUE_REQUIRED, "Apply properties to a catalog")
 | 
			
		||||
            ->addArgument("properties", InputArgument::IS_ARRAY, "Default properties for the catalog")
 | 
			
		||||
            ->addOption("list", null, InputOption::VALUE_NONE, "Only list catalogs, not methods")
 | 
			
		||||
            ;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected function execute(InputInterface $input, OutputInterface $output)
 | 
			
		||||
    {
 | 
			
		||||
        $api = get_plugin('com.noccy.apiclient');
 | 
			
		||||
        $list = $input->getOption("list");
 | 
			
		||||
        $dest = get_environment()->getConfigDirectory() . "/api/catalogs";
 | 
			
		||||
 | 
			
		||||
        if ($create = $input->getOption("create")) {
 | 
			
		||||
            if (file_exists($dest."/".$create.".json")) {
 | 
			
		||||
                $output->writeln("<error>Catalog {$create} already exists!</>");
 | 
			
		||||
                return Command::FAILURE;
 | 
			
		||||
            }
 | 
			
		||||
            $catalog = new Catalog([
 | 
			
		||||
                'catalog' => [
 | 
			
		||||
                    'name' => $create
 | 
			
		||||
                ]
 | 
			
		||||
            ]);
 | 
			
		||||
            file_put_contents($dest."/".$create.".json", json_encode($catalog, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES));
 | 
			
		||||
            $output->writeln("<info>Created new catalog {$create}</>");
 | 
			
		||||
            return Command::SUCCESS;
 | 
			
		||||
        } 
 | 
			
		||||
        if ($remove = $input->getOption("remove")) {
 | 
			
		||||
            if (!file_exists($dest."/".$remove.".json")) {
 | 
			
		||||
                $output->writeln("<error>Catalog {$remove} does not exist!</>");
 | 
			
		||||
                return Command::FAILURE;
 | 
			
		||||
            }
 | 
			
		||||
            unlink($dest."/".$remove.".json");
 | 
			
		||||
            $output->writeln("<info>Removed catalog {$remove}</>");
 | 
			
		||||
            return Command::SUCCESS;
 | 
			
		||||
        }
 | 
			
		||||
        if ($setprops = $input->getOption("set-props")) {
 | 
			
		||||
            $proparr = [];
 | 
			
		||||
            $props = $input->getArgument("properties");
 | 
			
		||||
            foreach ($props as $str) {
 | 
			
		||||
                if (!str_contains($str,"=")) {
 | 
			
		||||
                    $output->writeln("<error>Ignoring parameter argument '{$str}'</>");
 | 
			
		||||
                } else {
 | 
			
		||||
                    [$k,$v] = explode("=",$str,2);
 | 
			
		||||
                    $proparr[$k] = $v;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            $catalog = $api->getCatalog($setprops);
 | 
			
		||||
            $catalog->applyProperties($proparr);
 | 
			
		||||
            $api->saveCatalog($catalog);
 | 
			
		||||
            $output->writeln("<info>Updated properties on catalog {$setprops}</>");
 | 
			
		||||
            return Command::SUCCESS;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $catalogs = $api->getCatalogNames();
 | 
			
		||||
        foreach ($catalogs as $catalog) {
 | 
			
		||||
            $c = $api->getCatalog($catalog);
 | 
			
		||||
            if ($list) {
 | 
			
		||||
                $output->writeln($catalog);
 | 
			
		||||
            } else {
 | 
			
		||||
                $output->writeln("\u{25e9} <options=bold>{$catalog}</>: <fg=gray>{$c->getInfo()}</>");
 | 
			
		||||
                $ms = $c->getMethods();
 | 
			
		||||
                foreach ($ms as $name=>$m) {
 | 
			
		||||
                    $last = ($m === end($ms));
 | 
			
		||||
                    $output->writeln(($last?"\u{2514}\u{2500}":"\u{251c}\u{2500}")."\u{25a2} {$catalog}.{$name}: <info>{$m->getInfo()}</>");
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return Command::SUCCESS;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										35
									
								
								plugins/com.noccy.apiclient/Commands/ApiLogsCommand.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								plugins/com.noccy.apiclient/Commands/ApiLogsCommand.php
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,35 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
namespace SparkPlug\Com\Noccy\ApiClient\Commands;
 | 
			
		||||
 | 
			
		||||
use Spark\Commands\Command;
 | 
			
		||||
use SparkPlug;
 | 
			
		||||
use SparkPlug\Com\Noccy\ApiClient\Api\Method;
 | 
			
		||||
use SparkPlug\Com\Noccy\ApiClient\ApiClientPlugin;
 | 
			
		||||
use SparkPlug\Com\Noccy\ApiClient\Request\RequestBuilder;
 | 
			
		||||
use Symfony\Component\Console\Helper\Table;
 | 
			
		||||
use Symfony\Component\Console\Input\InputArgument;
 | 
			
		||||
use Symfony\Component\Console\Input\InputInterface;
 | 
			
		||||
use Symfony\Component\Console\Input\InputOption;
 | 
			
		||||
use Symfony\Component\Console\Output\OutputInterface;
 | 
			
		||||
 | 
			
		||||
class ApiLogsCommand extends Command
 | 
			
		||||
{
 | 
			
		||||
    protected function configure()
 | 
			
		||||
    {
 | 
			
		||||
        $this->setName("api:logs")
 | 
			
		||||
            ->setDescription("Show previous requests and manage the log")
 | 
			
		||||
            ->addOption("clear", null, InputOption::VALUE_NONE, "Clear the log")
 | 
			
		||||
            ->addOption("write", "w", InputOption::VALUE_REQUIRED, "Write the formatted entries to a file")
 | 
			
		||||
            ;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected function execute(InputInterface $input, OutputInterface $output)
 | 
			
		||||
    {
 | 
			
		||||
        /** @var ApiClientPlugin */
 | 
			
		||||
        $plugin = get_plugin('com.noccy.apiclient');
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        return Command::SUCCESS;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										22
									
								
								plugins/com.noccy.apiclient/Commands/ApiProfileCommand.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								plugins/com.noccy.apiclient/Commands/ApiProfileCommand.php
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,22 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
namespace SparkPlug\Com\Noccy\ApiClient\Commands;
 | 
			
		||||
 | 
			
		||||
use Spark\Commands\Command;
 | 
			
		||||
use SparkPlug;
 | 
			
		||||
use Symfony\Component\Console\Input\InputInterface;
 | 
			
		||||
use Symfony\Component\Console\Output\OutputInterface;
 | 
			
		||||
 | 
			
		||||
class ApiProfileCommand extends Command
 | 
			
		||||
{
 | 
			
		||||
    protected function configure()
 | 
			
		||||
    {
 | 
			
		||||
        $this->setName("api:profile")
 | 
			
		||||
            ->setDescription("Manage API profiles");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected function execute(InputInterface $input, OutputInterface $output)
 | 
			
		||||
    {
 | 
			
		||||
        return Command::SUCCESS;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										134
									
								
								plugins/com.noccy.apiclient/Commands/ApiRequestCommand.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										134
									
								
								plugins/com.noccy.apiclient/Commands/ApiRequestCommand.php
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,134 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
namespace SparkPlug\Com\Noccy\ApiClient\Commands;
 | 
			
		||||
 | 
			
		||||
use Spark\Commands\Command;
 | 
			
		||||
use SparkPlug;
 | 
			
		||||
use SparkPlug\Com\Noccy\ApiClient\Api\Method;
 | 
			
		||||
use SparkPlug\Com\Noccy\ApiClient\ApiClientPlugin;
 | 
			
		||||
use SparkPlug\Com\Noccy\ApiClient\Request\RequestBuilder;
 | 
			
		||||
use Symfony\Component\Console\Helper\Table;
 | 
			
		||||
use Symfony\Component\Console\Input\InputArgument;
 | 
			
		||||
use Symfony\Component\Console\Input\InputInterface;
 | 
			
		||||
use Symfony\Component\Console\Input\InputOption;
 | 
			
		||||
use Symfony\Component\Console\Output\OutputInterface;
 | 
			
		||||
 | 
			
		||||
class ApiRequestCommand extends Command
 | 
			
		||||
{
 | 
			
		||||
    protected function configure()
 | 
			
		||||
    {
 | 
			
		||||
        $this->setName("api:request")
 | 
			
		||||
            ->setDescription("Send a request")
 | 
			
		||||
            ->addOption("profile", "p", InputOption::VALUE_REQUIRED, "Use profile for request")
 | 
			
		||||
            ->addOption("save", "s", InputOption::VALUE_NONE, "Save to catalog")
 | 
			
		||||
            ->addArgument("method", InputArgument::OPTIONAL, "Request URL or catalog.method")
 | 
			
		||||
            ->addArgument("props", InputArgument::IS_ARRAY, "Parameter key=value pairs")
 | 
			
		||||
            ;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected function execute(InputInterface $input, OutputInterface $output)
 | 
			
		||||
    {
 | 
			
		||||
        /** @var ApiClientPlugin */
 | 
			
		||||
        $plugin = get_plugin('com.noccy.apiclient');
 | 
			
		||||
 | 
			
		||||
        $separator = str_repeat("\u{2500}", 40);
 | 
			
		||||
 | 
			
		||||
        $method = $input->getArgument("method");
 | 
			
		||||
        $builder = new RequestBuilder();
 | 
			
		||||
        if (str_contains($method, "://")) {
 | 
			
		||||
            $builder->setProperties([
 | 
			
		||||
                'url' => $method
 | 
			
		||||
            ]);
 | 
			
		||||
        } else {
 | 
			
		||||
            if (str_contains($method, '.')) {
 | 
			
		||||
                [$catalog,$method] = explode(".", $method, 2);
 | 
			
		||||
                $catalogObj = $plugin->getCatalog($catalog);
 | 
			
		||||
                // if (!$catalogObj) {
 | 
			
		||||
                //     $output->writeln("<error>No such catalog {$catalog}</>");
 | 
			
		||||
                //     return Command::FAILURE;
 | 
			
		||||
                // }
 | 
			
		||||
                $methodObj = $catalogObj->getMethod($method);
 | 
			
		||||
                // if (!$methodObj) {
 | 
			
		||||
                //     $output->writeln("<error>No such method {$method} in catalog {$catalog}</>");
 | 
			
		||||
                //     return Command::FAILURE;
 | 
			
		||||
                // }
 | 
			
		||||
                $builder->setCatalog($catalogObj);
 | 
			
		||||
                $builder->setMethod($methodObj);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        $props = [];
 | 
			
		||||
        $propstr = $input->getArgument("props");
 | 
			
		||||
        foreach ($propstr as $str) {
 | 
			
		||||
            if (!str_contains($str,"=")) {
 | 
			
		||||
                $output->writeln("<error>Ignoring parameter argument '{$str}'</>");
 | 
			
		||||
            } else {
 | 
			
		||||
                [$k,$v] = explode("=",$str,2);
 | 
			
		||||
                $props[$k] = $v;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        $builder->addProperties($props);
 | 
			
		||||
 | 
			
		||||
        if ($input->getOption("save")) {
 | 
			
		||||
            $catalogObj = $plugin->getCatalog($catalog);
 | 
			
		||||
            $methodObj = new Method([
 | 
			
		||||
                'name' => $method,
 | 
			
		||||
                'info' => $props['method.info']??null,
 | 
			
		||||
                'props' => $props
 | 
			
		||||
            ]);
 | 
			
		||||
            
 | 
			
		||||
            $catalogObj->addMethod($method, $methodObj);
 | 
			
		||||
            $plugin->saveCatalog($catalogObj);
 | 
			
		||||
            $output->writeln("<info>Saved method {$method} to catalog {$catalog}</>");
 | 
			
		||||
            return self::SUCCESS;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if ($profile = $input->getOption("profile")) {
 | 
			
		||||
            $profileObj = $plugin->getProfile($profile);
 | 
			
		||||
            $builder->setProfile($profileObj);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $request = $builder->getRequest();
 | 
			
		||||
 | 
			
		||||
        $table = new Table($output);
 | 
			
		||||
        $table->setStyle('compact');
 | 
			
		||||
        $table->setHeaders([ "Request Info", "" ]);
 | 
			
		||||
        foreach ($request->getInfo() as $i=>$v) {
 | 
			
		||||
            $table->addRow([$i,$v]);
 | 
			
		||||
        }
 | 
			
		||||
        $table->render();
 | 
			
		||||
 | 
			
		||||
        $table = new Table($output);
 | 
			
		||||
        $table->setStyle('compact');
 | 
			
		||||
        $table->setHeaders([ "Request Headers", "" ]);
 | 
			
		||||
        foreach ($request->getHeaders() as $i=>$v) {
 | 
			
		||||
            $table->addRow([$i,join("\n",$v)]);
 | 
			
		||||
        }
 | 
			
		||||
        $table->render();
 | 
			
		||||
 | 
			
		||||
        $output->writeln($separator);
 | 
			
		||||
        $response = $request->send();
 | 
			
		||||
        
 | 
			
		||||
        $rheaders = $response->getHeaders();
 | 
			
		||||
        $table = new Table($output);
 | 
			
		||||
        $table->setStyle('compact');
 | 
			
		||||
        $table->setHeaders([ "Response headers", "" ]);
 | 
			
		||||
        foreach ($rheaders as $h=>$v) {
 | 
			
		||||
            $table->addRow([$h,join("\n",$v)]);
 | 
			
		||||
        }
 | 
			
		||||
        $table->render();
 | 
			
		||||
        $body = (string)$response->getBody();
 | 
			
		||||
        $output->writeln($separator);
 | 
			
		||||
 | 
			
		||||
        $parseAs = $builder->getCalculatedProperty('response.parse');
 | 
			
		||||
        if ($parseAs == 'json') {
 | 
			
		||||
            dump(json_decode($body));
 | 
			
		||||
        } else {
 | 
			
		||||
            $output->writeln($body);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $output->writeln($separator);
 | 
			
		||||
        $output->writeln(strlen($body)." bytes");
 | 
			
		||||
 | 
			
		||||
        return Command::SUCCESS;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										73
									
								
								plugins/com.noccy.apiclient/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								plugins/com.noccy.apiclient/README.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,73 @@
 | 
			
		||||
# ApiClient for Spark
 | 
			
		||||
 | 
			
		||||
## Installation
 | 
			
		||||
 | 
			
		||||
To install, downlad and extract the plugin directory into your plugin directory.
 | 
			
		||||
 | 
			
		||||
## Usage
 | 
			
		||||
 | 
			
		||||
*Note: Profiles are not yet implemented*
 | 
			
		||||
 | 
			
		||||
You should make a catalog and a profile first. You don't have to, but this will
 | 
			
		||||
save you some time.
 | 
			
		||||
 | 
			
		||||
    $ spark api:catalog --create mysite \
 | 
			
		||||
        protocol=http \
 | 
			
		||||
        urlbase=http://127.0.0.1:80/api/
 | 
			
		||||
    $ spark api:profile --create apiuser \
 | 
			
		||||
        --catalog mysite \
 | 
			
		||||
        auth.username=apiuser \
 | 
			
		||||
        auth.token=APITOKEN
 | 
			
		||||
 | 
			
		||||
You can now add some requests:
 | 
			
		||||
 | 
			
		||||
    $ spark api:request --add mysite.info \
 | 
			
		||||
        url=v1/info \
 | 
			
		||||
        http.method=POST \
 | 
			
		||||
        response.parse=json
 | 
			
		||||
 | 
			
		||||
And send them:
 | 
			
		||||
 | 
			
		||||
    $ spark api:request -p apiuser mysite.info
 | 
			
		||||
 | 
			
		||||
## Internals
 | 
			
		||||
 | 
			
		||||
ApiClient works on a map of properties, populated with the defaults from the
 | 
			
		||||
catalog. The request properties are then appied, followed by the profile
 | 
			
		||||
properties.
 | 
			
		||||
 | 
			
		||||
### Properties
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
# Core properties
 | 
			
		||||
protocol={"http"|"websocket"|"xmlrpc"|"jsonrpc"}
 | 
			
		||||
# Final URL is [urlbase+]url
 | 
			
		||||
urlbase={url}
 | 
			
		||||
url={url}
 | 
			
		||||
 | 
			
		||||
# Authentication options
 | 
			
		||||
auth.username={username}
 | 
			
		||||
auth.password={password}
 | 
			
		||||
auth.token={token}
 | 
			
		||||
auth.type={"basic"|"bearer"}
 | 
			
		||||
 | 
			
		||||
# HTTP options
 | 
			
		||||
http.method={"GET"|"POST"|...}
 | 
			
		||||
http.version={"1.0"|"1.1"|"2.0"}
 | 
			
		||||
http.header.{name}={value}
 | 
			
		||||
http.query.{field}={value}
 | 
			
		||||
http.body={raw-body}
 | 
			
		||||
http.body.json={object}
 | 
			
		||||
 | 
			
		||||
# RPC options
 | 
			
		||||
rpc.method={string}
 | 
			
		||||
rpc.argument.{index}={value}
 | 
			
		||||
 | 
			
		||||
# Request handling
 | 
			
		||||
request.follow-redirecs={"auto"|"no"|"yes"}
 | 
			
		||||
request.max-redirects={number}
 | 
			
		||||
 | 
			
		||||
# Response handling
 | 
			
		||||
response.parse={"none"|"json"|"yaml"|"xml"}  
 | 
			
		||||
response.good="200,201,202,203,204,205,206,207,208"
 | 
			
		||||
```
 | 
			
		||||
							
								
								
									
										91
									
								
								plugins/com.noccy.apiclient/Request/HttpRequest.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										91
									
								
								plugins/com.noccy.apiclient/Request/HttpRequest.php
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,91 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
namespace SparkPlug\Com\Noccy\ApiClient\Request;
 | 
			
		||||
 | 
			
		||||
use GuzzleHttp\Client;
 | 
			
		||||
use GuzzleHttp\Psr7\Response;
 | 
			
		||||
 | 
			
		||||
class HttpRequest extends Request
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
    private string $method = 'GET';
 | 
			
		||||
 | 
			
		||||
    private string $version = '1.1';
 | 
			
		||||
 | 
			
		||||
    private ?string $url = null;
 | 
			
		||||
 | 
			
		||||
    private array $query = [];
 | 
			
		||||
 | 
			
		||||
    private array $headers = [];
 | 
			
		||||
 | 
			
		||||
    public function __construct(array $props)
 | 
			
		||||
    {
 | 
			
		||||
        foreach ($props as $prop=>$value) {
 | 
			
		||||
 | 
			
		||||
            if (str_starts_with($prop, 'http.')) {
 | 
			
		||||
                $this->handleHttpProp(substr($prop,5), $value);
 | 
			
		||||
            } elseif ($prop == 'url') {
 | 
			
		||||
                $this->url = $value;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function handleHttpProp(string $prop, $value)
 | 
			
		||||
    {
 | 
			
		||||
 | 
			
		||||
        if (str_starts_with($prop, 'query.')) {
 | 
			
		||||
            $this->query[substr($prop, 6)] = $value;
 | 
			
		||||
        } elseif (str_starts_with($prop, 'header.')) {
 | 
			
		||||
            $this->headers[substr($prop, 7)] = $value;
 | 
			
		||||
        } elseif ($prop === 'method') {
 | 
			
		||||
            $this->method = strtoupper($value);
 | 
			
		||||
        } elseif ($prop === 'version') {
 | 
			
		||||
            $this->version = $value;
 | 
			
		||||
        } else {
 | 
			
		||||
            fprintf(STDERR, "Warning: unhandled prop: http.%s (%s)\n", $prop, $value);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getInfo(): array
 | 
			
		||||
    {
 | 
			
		||||
        $query = http_build_query($this->query);
 | 
			
		||||
        $headers = [];
 | 
			
		||||
        foreach ($this->headers as $k=>$v) {
 | 
			
		||||
            // Convert to Proper-Case unless UPPERCASE
 | 
			
		||||
            if ($k !== strtoupper($k))
 | 
			
		||||
                $k = ucwords($k, '-');
 | 
			
		||||
            // Build the header
 | 
			
		||||
            $headers[] = sprintf("<options=bold>%s</>: %s", $k, $v);
 | 
			
		||||
        }
 | 
			
		||||
        return [
 | 
			
		||||
            'protocol' => sprintf("HTTP/%s %s", $this->version, $this->method),
 | 
			
		||||
            'query' => $this->url . "?" . $query,
 | 
			
		||||
            'body' => "Empty body"
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getHeaders(): array
 | 
			
		||||
    {
 | 
			
		||||
        $headers = [];
 | 
			
		||||
        foreach ($this->headers as $k=>$v) {
 | 
			
		||||
            // Convert to Proper-Case unless UPPERCASE
 | 
			
		||||
            if ($k !== strtoupper($k))
 | 
			
		||||
                $k = ucwords($k, '-');
 | 
			
		||||
            // Build the header
 | 
			
		||||
            $headers[$k] = (array)$v;
 | 
			
		||||
        }
 | 
			
		||||
        return $headers;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function send(): ?Response
 | 
			
		||||
    {
 | 
			
		||||
        $query = http_build_query($this->query);
 | 
			
		||||
        $url = $this->url . ($query?'?'.$query:''); 
 | 
			
		||||
        $config = [];
 | 
			
		||||
        $client = new Client($config);
 | 
			
		||||
        $options = [];
 | 
			
		||||
        $response = $client->request($this->method, $url, $options);
 | 
			
		||||
        return $response;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										26
									
								
								plugins/com.noccy.apiclient/Request/JsonRpcRequest.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								plugins/com.noccy.apiclient/Request/JsonRpcRequest.php
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,26 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
namespace SparkPlug\Com\Noccy\ApiClient\Request;
 | 
			
		||||
 | 
			
		||||
use GuzzleHttp\Psr7\Response;
 | 
			
		||||
 | 
			
		||||
class JsonRpcRequest extends Request
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
    public function getInfo(): array
 | 
			
		||||
    {
 | 
			
		||||
        return [
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function send(): ?Response
 | 
			
		||||
    {
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getHeaders(): array
 | 
			
		||||
    {
 | 
			
		||||
        return [];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										16
									
								
								plugins/com.noccy.apiclient/Request/Request.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								plugins/com.noccy.apiclient/Request/Request.php
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,16 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
namespace SparkPlug\Com\Noccy\ApiClient\Request;
 | 
			
		||||
 | 
			
		||||
use GuzzleHttp\Psr7\Response;
 | 
			
		||||
 | 
			
		||||
abstract class Request
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
    abstract public function send(): ?Response;
 | 
			
		||||
 | 
			
		||||
    abstract public function getInfo(): array;
 | 
			
		||||
 | 
			
		||||
    abstract public function getHeaders(): array;
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										100
									
								
								plugins/com.noccy.apiclient/Request/RequestBuilder.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								plugins/com.noccy.apiclient/Request/RequestBuilder.php
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,100 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
namespace SparkPlug\Com\Noccy\ApiClient\Request;
 | 
			
		||||
 | 
			
		||||
use SparkPlug\Com\Noccy\ApiClient\Api\Catalog;
 | 
			
		||||
use SparkPlug\Com\Noccy\ApiClient\Api\Method;
 | 
			
		||||
use SparkPlug\Com\Noccy\ApiClient\Api\Profile;
 | 
			
		||||
 | 
			
		||||
class RequestBuilder
 | 
			
		||||
{
 | 
			
		||||
    public static $Protocols = [
 | 
			
		||||
        'http' => HttpRequest::class,
 | 
			
		||||
        'websocket' => WebSocketRequest::class,
 | 
			
		||||
        'jsonrpc' => JsonRpcRequest::class,
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    private ?Catalog $catalog = null;
 | 
			
		||||
 | 
			
		||||
    private ?Method $method = null;
 | 
			
		||||
 | 
			
		||||
    private ?Profile $profile = null;
 | 
			
		||||
 | 
			
		||||
    private array $props = [];
 | 
			
		||||
 | 
			
		||||
    public function setCatalog(?Catalog $catalog)
 | 
			
		||||
    {
 | 
			
		||||
        $this->catalog = $catalog;
 | 
			
		||||
        return $this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function setMethod(?Method $method)
 | 
			
		||||
    {
 | 
			
		||||
        $this->method = $method;
 | 
			
		||||
        return $this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function setProfile(?Profile $profile)
 | 
			
		||||
    {
 | 
			
		||||
        $this->profile = $profile;
 | 
			
		||||
        return $this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function setProperties(array $properties)
 | 
			
		||||
    {
 | 
			
		||||
        $this->props = $properties;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function addProperties(array $properties)
 | 
			
		||||
    {
 | 
			
		||||
        $this->props = array_merge(
 | 
			
		||||
            $this->props,
 | 
			
		||||
            $properties
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function buildProperties()
 | 
			
		||||
    {
 | 
			
		||||
        $props = [];
 | 
			
		||||
        if ($this->catalog) {
 | 
			
		||||
            $add = $this->catalog->getProperties();
 | 
			
		||||
            $props = array_merge($props, $add);
 | 
			
		||||
        }
 | 
			
		||||
        if ($this->method) {
 | 
			
		||||
            $add = $this->method->getProperties();
 | 
			
		||||
            $props = array_merge($props, $add);
 | 
			
		||||
        }
 | 
			
		||||
        if ($this->profile) {
 | 
			
		||||
            $add = $this->profile->getProperties();
 | 
			
		||||
            $props = array_merge($props, $add);
 | 
			
		||||
        }
 | 
			
		||||
        $props = array_merge($props, $this->props);
 | 
			
		||||
        $props = array_filter($props);
 | 
			
		||||
        return $props;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getCalculatedProperty(string $name)
 | 
			
		||||
    {
 | 
			
		||||
        $props = $this->buildProperties();
 | 
			
		||||
        return $props[$name] ?? null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getRequest(): Request
 | 
			
		||||
    {
 | 
			
		||||
        $props = $this->buildProperties();
 | 
			
		||||
        $protocol = $props['protocol']??'http';
 | 
			
		||||
 | 
			
		||||
        if (!$handler = self::$Protocols[$protocol]??null) {
 | 
			
		||||
            throw new \Exception("Invalid protocol for request: {$protocol}");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $base = $props['urlbase']??null;
 | 
			
		||||
        $url = $props['url']??null;
 | 
			
		||||
        if ($base) {
 | 
			
		||||
            $props['url'] = $base . $url;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $request = new $handler($props);
 | 
			
		||||
        return $request;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										26
									
								
								plugins/com.noccy.apiclient/Request/WebsocketRequest.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								plugins/com.noccy.apiclient/Request/WebsocketRequest.php
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,26 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
namespace SparkPlug\Com\Noccy\ApiClient\Request;
 | 
			
		||||
 | 
			
		||||
use GuzzleHttp\Psr7\Response;
 | 
			
		||||
 | 
			
		||||
class WebsocketRequest extends Request
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
    public function getInfo(): array
 | 
			
		||||
    {
 | 
			
		||||
        return [
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function send(): ?Response
 | 
			
		||||
    {
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getHeaders(): array
 | 
			
		||||
    {
 | 
			
		||||
        return [];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										109
									
								
								plugins/com.noccy.apiclient/sparkplug.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										109
									
								
								plugins/com.noccy.apiclient/sparkplug.php
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,109 @@
 | 
			
		||||
<?php // "name":"Call on web APIs", "author":"Noccy"
 | 
			
		||||
 | 
			
		||||
namespace SparkPlug\Com\Noccy\ApiClient;
 | 
			
		||||
 | 
			
		||||
use SparkPlug;
 | 
			
		||||
 | 
			
		||||
class ApiClientPlugin extends SparkPlug
 | 
			
		||||
{
 | 
			
		||||
    private array $catalogs = [];
 | 
			
		||||
 | 
			
		||||
    private array $profiles = [];
 | 
			
		||||
 | 
			
		||||
    public function load()
 | 
			
		||||
    {
 | 
			
		||||
        register_command(new Commands\ApiCatalogCommand());
 | 
			
		||||
        register_command(new Commands\ApiRequestCommand());
 | 
			
		||||
        register_command(new Commands\ApiProfileCommand());
 | 
			
		||||
        register_command(new Commands\ApiLogsCommand());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function loadCatalogs()
 | 
			
		||||
    {
 | 
			
		||||
        $env = get_environment();
 | 
			
		||||
        $catalogDir = $env->getConfigDirectory() . "/api/catalogs";
 | 
			
		||||
        if (file_exists($catalogDir)) {
 | 
			
		||||
            $catalogFiles = glob($catalogDir."/*.json");
 | 
			
		||||
            foreach ($catalogFiles as $catalogFile) {
 | 
			
		||||
                $name = basename($catalogFile, ".json");
 | 
			
		||||
                $this->catalogs[$name] = Api\Catalog::createFromFile($catalogFile);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function loadProfiles()
 | 
			
		||||
    {
 | 
			
		||||
        $env = get_environment();
 | 
			
		||||
        $catalogDir = $env->getConfigDirectory() . "/api/profiles";
 | 
			
		||||
        if (file_exists($catalogDir)) {
 | 
			
		||||
            $catalogFiles = glob($catalogDir."/*.json");
 | 
			
		||||
            foreach ($catalogFiles as $catalogFile) {
 | 
			
		||||
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function createCatalog(string $name): ?Api\Catalog
 | 
			
		||||
    {
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function saveCatalog(Api\Catalog $catalog)
 | 
			
		||||
    {
 | 
			
		||||
        $env = get_environment();
 | 
			
		||||
        $catalogDir = $env->getConfigDirectory() . "/api/catalogs";
 | 
			
		||||
        $catalogFile = $catalogDir . "/" . $catalog->getName() . ".json";
 | 
			
		||||
 | 
			
		||||
        if (!is_dir($catalogDir)) {
 | 
			
		||||
            mkdir($catalogDir, 0777, true);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        file_put_contents($catalogFile."~", json_encode($catalog, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES));
 | 
			
		||||
        rename($catalogFile."~", $catalogFile);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function deleteCatalog(string $name)
 | 
			
		||||
    {
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getCatalog(string $name): ?Api\Catalog
 | 
			
		||||
    {
 | 
			
		||||
        if (empty($this->catalogs)) $this->loadCatalogs();
 | 
			
		||||
 | 
			
		||||
        return $this->catalogs[$name]??null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getCatalogNames(): array
 | 
			
		||||
    {
 | 
			
		||||
        if (empty($this->catalogs)) $this->loadCatalogs();
 | 
			
		||||
 | 
			
		||||
        return array_keys($this->catalogs);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function saveProfile(string $name, Api\Profile $profile)
 | 
			
		||||
    {
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function deleteProfile(string $name)
 | 
			
		||||
    {
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getProfile(string $name): ?Api\Profile
 | 
			
		||||
    {
 | 
			
		||||
        if (empty($this->profiles)) $this->loadProfiles();
 | 
			
		||||
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getProfileNames(): array
 | 
			
		||||
    {
 | 
			
		||||
        if (empty($this->profiles)) $this->loadProfiles();
 | 
			
		||||
 | 
			
		||||
        return array_keys($this->profiles);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
register_plugin("com.noccy.apiclient", new ApiClientPlugin);
 | 
			
		||||
@@ -65,7 +65,7 @@ class DockerDbExportCommand extends Command
 | 
			
		||||
                    $cmd = sprintf("mysqldump -u%s -p%s %s", $dbuser, $dbpass, $database);
 | 
			
		||||
                    break;
 | 
			
		||||
            }
 | 
			
		||||
            $this->exportFromService($service, $cmd, $output);
 | 
			
		||||
            $this->exportFromService($service, $cmd, $output, $input->getOption("output"));
 | 
			
		||||
        } elseif ($dsn) {
 | 
			
		||||
            $url = parse_url($dsn);
 | 
			
		||||
            if (empty($url)) {
 | 
			
		||||
@@ -77,9 +77,13 @@ class DockerDbExportCommand extends Command
 | 
			
		||||
        return Command::SUCCESS;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function exportFromService(string $service, string $command, OutputInterface $output)
 | 
			
		||||
    private function exportFromService(string $service, string $command, OutputInterface $output, ?string $outfile=null)
 | 
			
		||||
    {
 | 
			
		||||
        $cmd = sprintf("docker-compose exec -T %s %s", $service, $command);
 | 
			
		||||
        if ($outfile) {
 | 
			
		||||
            $cmd .= " > ".escapeshellarg($outfile);
 | 
			
		||||
        }
 | 
			
		||||
        $output->writeln(sprintf("→ <info>%s</>", $cmd));
 | 
			
		||||
        passthru($cmd);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user