From 25969b48b262dca1eb049eb1da604436d0d3ce00 Mon Sep 17 00:00:00 2001 From: Sean Molenaar Date: Wed, 5 Jun 2024 13:47:57 +0200 Subject: [PATCH] feat: generate OpenAPI JSON from APIB --- .gitignore | 2 +- phpdraft | 6 + src/PHPDraft/Model/HTTPRequest.php | 8 +- src/PHPDraft/Model/Tests/HTTPRequestTest.php | 2 +- src/PHPDraft/Model/Transition.php | 4 +- src/PHPDraft/Out/BaseTemplateRenderer.php | 64 ++-- ...eRenderer.php => HtmlTemplateRenderer.php} | 65 ++-- src/PHPDraft/Out/OpenAPI/OpenApiRenderer.php | 293 ++++++++++++++++++ .../Out/Tests/TemplateRendererTest.php | 42 +-- src/PHPDraft/Out/TwigFactory.php | 6 +- src/PHPDraft/Parse/BaseHtmlGenerator.php | 2 - src/PHPDraft/Parse/HtmlGenerator.php | 4 +- src/PHPDraft/Parse/ParserFactory.php | 11 + 13 files changed, 416 insertions(+), 93 deletions(-) rename src/PHPDraft/Out/{TemplateRenderer.php => HtmlTemplateRenderer.php} (85%) create mode 100644 src/PHPDraft/Out/OpenAPI/OpenApiRenderer.php diff --git a/.gitignore b/.gitignore index fb051901..cb9ab694 100644 --- a/.gitignore +++ b/.gitignore @@ -11,7 +11,6 @@ /build/out /build/*.phar /tests/statics/index.* -src/Michelf/* src/.gitignore vendor/** @@ -24,3 +23,4 @@ atlassian-ide-plugin.xml /coverage.xml /event.json +!/src/PHPDraft/Out/ diff --git a/phpdraft b/phpdraft index 90ea0f77..a1468992 100755 --- a/phpdraft +++ b/phpdraft @@ -28,6 +28,7 @@ try ->opt('help:h', 'This help text', false) ->opt('version:v', 'Print the version for PHPDraft.', false) ->opt('file:f', 'Specifies the file to parse.', false) + ->opt('openapi:a', 'Output location for an OpenAPI file.', false) ->opt('yes:y', 'Always accept using the online mode.', false, 'bool') ->opt('online:o', 'Always use the online mode.', false, 'bool') ->opt('template:t', 'Specifies the template to use. (defaults to \'default\').', false) @@ -90,6 +91,11 @@ try $data = json_decode($json_string); } + if (isset($args['openapi'])) { + $openapi = ParserFactory::getOpenAPI()->init($data); + $openapi->write($args['openapi']); + } + $html = ParserFactory::getJson()->init($data); $name = 'PHPD_SORT_' . strtoupper($args->getOpt('sort', '')); $html->sorting = Sorting::${$name} ?? Sorting::PHPD_SORT_NONE->value; diff --git a/src/PHPDraft/Model/HTTPRequest.php b/src/PHPDraft/Model/HTTPRequest.php index c551184f..b19b6745 100644 --- a/src/PHPDraft/Model/HTTPRequest.php +++ b/src/PHPDraft/Model/HTTPRequest.php @@ -12,6 +12,7 @@ namespace PHPDraft\Model; +use PHPDraft\Model\Elements\ObjectStructureElement; use PHPDraft\Model\Elements\RequestBodyElement; use PHPDraft\Model\Elements\StructureElement; @@ -43,7 +44,7 @@ class HTTPRequest implements Comparable * * @var string */ - public string $description; + public string $description = ''; /** * Parent class. @@ -68,9 +69,10 @@ class HTTPRequest implements Comparable /** * Structure of the request. * - * @var RequestBodyElement[]|RequestBodyElement + * @var RequestBodyElement[]|RequestBodyElement|ObjectStructureElement */ - public mixed $struct = []; + public RequestBodyElement|ObjectStructureElement|array|null $struct = []; + /** * Identifier for the request. * diff --git a/src/PHPDraft/Model/Tests/HTTPRequestTest.php b/src/PHPDraft/Model/Tests/HTTPRequestTest.php index 114e2fae..6558c9e6 100644 --- a/src/PHPDraft/Model/Tests/HTTPRequestTest.php +++ b/src/PHPDraft/Model/Tests/HTTPRequestTest.php @@ -205,7 +205,7 @@ public function testGetCurlCommandStructBodyFilled(): void $struct_ar->expects($this->once()) ->method('print_request') ->with(null) - ->will($this->returnValue('TEST')); + ->willReturn('TEST'); $struct->value = [ $struct_ar ]; $this->set_reflection_property_value('struct', $struct); diff --git a/src/PHPDraft/Model/Transition.php b/src/PHPDraft/Model/Transition.php index 8158f94e..2d488197 100644 --- a/src/PHPDraft/Model/Transition.php +++ b/src/PHPDraft/Model/Transition.php @@ -23,9 +23,9 @@ class Transition extends HierarchyElement /** * HTTP method used. * - * @var string + * @var string|null */ - public string $method; + public ?string $method = NULL; /** * URI. diff --git a/src/PHPDraft/Out/BaseTemplateRenderer.php b/src/PHPDraft/Out/BaseTemplateRenderer.php index 765e5ec9..b052ab94 100644 --- a/src/PHPDraft/Out/BaseTemplateRenderer.php +++ b/src/PHPDraft/Out/BaseTemplateRenderer.php @@ -24,36 +24,21 @@ abstract class BaseTemplateRenderer * @var int */ public int $sorting; + /** - * CSS Files to load. - * - * @var string[] - */ - public array $css = []; - /** - * JS Files to load. - * - * @var string[] - */ - public array $js = []; - /** - * The image to use as a logo. - * - * @var string|null - */ - protected ?string $image = null; - /** - * The template file to load. + * JSON representation of an API Blueprint. * - * @var string + * @var object */ - protected string $template; + protected object $object; + /** * The base data of the API. * * @var array */ - protected array $base_data; + protected array $base_data = []; + /** * JSON object of the API blueprint. * @@ -66,4 +51,39 @@ abstract class BaseTemplateRenderer * @var BasicStructureElement[] */ protected array $base_structures = []; + + /** + * Parse base data + * + * @param object $object + */ + protected function parse_base_data(object $object): void + { + //Prepare base data + if (!is_array($object->content[0]->content)) { + return; + } + + $this->base_data['TITLE'] = $object->content[0]->meta->title->content ?? ''; + + foreach ($object->content[0]->attributes->metadata->content as $meta) { + $this->base_data[$meta->content->key->content] = $meta->content->value->content; + } + + foreach ($object->content[0]->content as $value) { + if ($value->element === 'copy') { + $this->base_data['DESC'] = $value->content; + continue; + } + + $cat = new Category(); + $cat = $cat->parse($value); + + if (($value->meta->classes->content[0]->content ?? null) === 'dataStructures') { + $this->base_structures = array_merge($this->base_structures, $cat->structures); + } else { + $this->categories[] = $cat; + } + } + } } diff --git a/src/PHPDraft/Out/TemplateRenderer.php b/src/PHPDraft/Out/HtmlTemplateRenderer.php similarity index 85% rename from src/PHPDraft/Out/TemplateRenderer.php rename to src/PHPDraft/Out/HtmlTemplateRenderer.php index 7ad6a025..183462fd 100644 --- a/src/PHPDraft/Out/TemplateRenderer.php +++ b/src/PHPDraft/Out/HtmlTemplateRenderer.php @@ -28,8 +28,36 @@ use Twig\TwigFilter; use Twig\TwigTest; -class TemplateRenderer extends BaseTemplateRenderer +class HtmlTemplateRenderer extends BaseTemplateRenderer { + + + /** + * CSS Files to load. + * + * @var string[] + */ + public array $css = []; + + /** + * JS Files to load. + * + * @var string[] + */ + public array $js = []; + /** + * The image to use as a logo. + * + * @var string|null + */ + protected ?string $image = null; + /** + * The template file to load. + * + * @var string + */ + protected string $template; + /** * TemplateGenerator constructor. * @@ -106,41 +134,6 @@ public function get(object $object): string ]); } - /** - * Parse base data - * - * @param object $object - */ - private function parse_base_data(object $object): void - { - //Prepare base data - if (!is_array($object->content[0]->content)) { - return; - } - - $this->base_data['TITLE'] = $object->content[0]->meta->title->content ?? ''; - - foreach ($object->content[0]->attributes->metadata->content as $meta) { - $this->base_data[$meta->content->key->content] = $meta->content->value->content; - } - - foreach ($object->content[0]->content as $value) { - if ($value->element === 'copy') { - $this->base_data['DESC'] = $value->content; - continue; - } - - $cat = new Category(); - $cat = $cat->parse($value); - - if (($value->meta->classes->content[0]->content ?? null) === 'dataStructures') { - $this->base_structures = array_merge($this->base_structures, $cat->structures); - } else { - $this->categories[] = $cat; - } - } - } - /** * Get the path to a file to include. * diff --git a/src/PHPDraft/Out/OpenAPI/OpenApiRenderer.php b/src/PHPDraft/Out/OpenAPI/OpenApiRenderer.php new file mode 100644 index 00000000..1f10dd19 --- /dev/null +++ b/src/PHPDraft/Out/OpenAPI/OpenApiRenderer.php @@ -0,0 +1,293 @@ +object = $json; + + return $this; + } + + public function write(string $filename): void + { + $output = json_encode($this->toOpenApiObject(), JSON_PRETTY_PRINT); + file_put_contents($filename, $output); + } + + /** + * Get OpenAPI base structure. + * + * @return array|string[]> + */ + private function toOpenApiObject(): array + { + $this->parse_base_data($this->object); + + return [ + 'openapi' => '3.1.0', +// 'jsonSchemaDialect' => '', + 'info' => $this->getApiInfo(), + 'servers' => $this->getServers(), + 'paths' => $this->getPaths(), + 'webhooks' => $this->getWebhooks(), + 'components' => $this->getComponents(), + 'security' => $this->getSecurity(), + 'tags' => $this->getTags(), +// 'externalDocs' => $this->getDocs(), + ]; + } + + /** + * Get generic info for the API + * @return array + */ + private function getApiInfo(): array { + return [ + "title"=> $this->base_data['TITLE'], + "version"=> $this->base_data['VERSION'] ?? '1.0.0', + "summary"=> $this->base_data['TITLE'] . ' generated from API Blueprint', + "description"=> $this->base_data['DESC'], +// "termsOfService"=> "https://example.com/terms/", +// "contact"=> [ +// "name"=> "API Support", +// "url"=> "https://www.example.com/support", +// "email"=> "support@example.com" +// ], +// "license" => [ +// "name"=> "Apache 2.0", +// "url"=> "https://www.apache.org/licenses/LICENSE-2.0.html" +// ], + ]; + } + + /** + * Get information about the servers involved in the API. + * + * @return array> + */ + private function getServers(): array { + $return = []; + $return[] = ['url' => $this->base_data['HOST'], 'description' => 'Main host']; + + foreach (explode(',', $this->base_data['ALT_HOST'] ?? '') as $host) { + $return[] = ['url' => $host]; + } + + return $return; + } + + /** + * Get path information + * + * @return object + */ + private function getPaths(): object { + $return = []; + foreach ($this->categories as $category) { + foreach ($category->children ?? [] as $resource) { + foreach ($resource->children ?? [] as $transition) { + $transition_return = []; + $transition_return['parameters'] = []; + if ($transition->url_variables !== []) { + $transition_return['parameters'] = $this->toParameters($transition->url_variables, $transition->href); + } + + foreach ($transition->requests as $request) { + $request_return = [ + 'operationId' => $request->get_id(), + 'responses' => $this->toResponses($transition->responses), + ]; + if (isset($transition_return['parameters']) && $transition_return['parameters'] !== []) { + $request_return['parameters'] = $transition_return['parameters']; + } + if ($request->body !== NULL) { + $request_return['requestBody'] = $this->toBody($request); + } + + if ($request->title !== NULL) { + $request_return['summary'] = $request->title; + } + if ($request->description !== '') { + $request_return['description'] = $request->description; + } + + $transition_return[strtolower($request->method)] = (object) $request_return; + } + $return[$transition->href] = (object) $transition_return; + } + } + } + + return (object) $return; + } + + /** + * Convert objects into parameters. + * + * @param object[] $objects List of objects to convert + * @param string $href Base URL + * + * @return array> + */ + private function toParameters(array $objects, string $href): array { + $return = []; + + foreach ($objects as $variable) { + $return_tmp = [ + 'name' => $variable->key->value, + 'in' => str_contains($href, '{' . $variable->key->value . '}') ? 'path' : 'query', + 'required' => $variable->status === 'required', + 'schema' => [ + 'type' => $variable->type, + ], + ]; + + if (isset($variable->value)) + { + $return_tmp['example'] = $variable->value; + } + + if (isset($variable->description)) + { + $return_tmp['description'] = $variable->description; + } + $return[] = $return_tmp; + } + + return $return; + } + + /** + * Convert a HTTP Request into an OpenAPI body + * + * @param HTTPRequest $request Request to convert + * + * @return array> OpenAPI style body + */ + private function toBody(HTTPRequest $request): array + { + $return = []; + + if (!is_array($request->struct)) { + $return['description'] = $request->struct->description; + } + + $content_type = $request->headers['Content-Type'] ?? 'text/plain'; + if (isset($request->struct) && $request->struct !== []) + { + $return['content'] = [ + $content_type => [ + 'schema' => [ + 'type' => $request->struct->element, + 'properties' => array_map(fn($value) => [$value->key->value => ['type' => $value->type]], $request->struct->value), + ], + ], + ]; + } else { + $return['content'] = [ + $content_type => [ + 'schema' => [ + 'type' => 'string', + ], + ], + ]; + } + + if ($request->body !== NULL && $request->body !== []) { + $return['content'][$content_type]['example'] = $request->body[0]; + } + + return $return; + } + + /** + * Convert responses to the OpenAPI structure + * + * @param HTTPResponse[] $responses List of responses to parse + * + * @return array> List of status codes with the response + */ + private function toResponses(array $responses): array + { + $return = []; + + foreach ($responses as $response) { + $headers = []; + foreach ($response->headers as $header => $value) { + if ($header === 'Content-Type') { continue; } + $headers[$header] = [ + 'schema' => [ + 'type' => 'string', + 'example' => $value, + ] + ]; + } + + $content = []; + foreach ($response->content as $key => $contents) { + $content[$key] = [ + "schema"=> [ + "type"=> "string", + "example"=> $contents + ] + ]; + } + foreach ($response->structure as $structure) { + if ($structure->key === NULL) { continue; } + $content[$response->headers['Content-Type'] ?? 'text/plain'] = [ + "schema"=> [ + "type"=> "object", + "properties"=> [ + $structure->key->value => [ + "type" => $structure->type, + 'example' => $structure->value, + ] + ] + ] + ]; + } + $return[$response->statuscode] = [ + 'description' => $response->description ?? $response->title ?? '', + 'headers' => (object) $headers, + 'content' => (object) $content, + ]; + } + + return $return; + } + + /** + * Get webhook information for the API. + * @return object + */ + private function getWebhooks(): object { return (object) []; } + + /** + * Get component information for the API. + * @return object + */ + private function getComponents(): object { return (object) []; } + + /** + * Get security information for the API + * @return string[] + */ + private function getSecurity(): array { return []; } + + /** + * Get tags for the API + * @return string[] + */ + private function getTags(): array { return []; } + +// private function getDocs(): object { return new \stdClass(); } + +} \ No newline at end of file diff --git a/src/PHPDraft/Out/Tests/TemplateRendererTest.php b/src/PHPDraft/Out/Tests/TemplateRendererTest.php index 72863cad..c47fb1c5 100644 --- a/src/PHPDraft/Out/Tests/TemplateRendererTest.php +++ b/src/PHPDraft/Out/Tests/TemplateRendererTest.php @@ -10,17 +10,17 @@ namespace PHPDraft\Out\Tests; use Lunr\Halo\LunrBaseTest; -use PHPDraft\Out\TemplateRenderer; +use PHPDraft\Out\HtmlTemplateRenderer; /** * Class TemplateGeneratorTest * - * @covers \PHPDraft\Out\TemplateRenderer + * @covers \PHPDraft\Out\HtmlTemplateRenderer */ class TemplateRendererTest extends LunrBaseTest { /** - * @var TemplateRenderer + * @var HtmlTemplateRenderer */ protected $class; @@ -30,14 +30,14 @@ class TemplateRendererTest extends LunrBaseTest */ public function setUp(): void { - $this->class = new TemplateRenderer('default', 'none'); - $this->reflection = new \ReflectionClass('PHPDraft\Out\TemplateRenderer'); + $this->class = new HtmlTemplateRenderer('default', 'none'); + $this->reflection = new \ReflectionClass('PHPDraft\Out\HtmlTemplateRenderer'); } /** * Test if the value the class is initialized with is correct * - * @covers \PHPDraft\Out\TemplateRenderer + * @covers \PHPDraft\Out\HtmlTemplateRenderer */ public function testSetupCorrectly(): void { @@ -48,7 +48,7 @@ public function testSetupCorrectly(): void /** * Test if the value the class is initialized with is correct * - * @covers \PHPDraft\Out\TemplateRenderer::strip_link_spaces + * @covers \PHPDraft\Out\HtmlTemplateRenderer::strip_link_spaces */ public function testStripSpaces(): void { @@ -81,11 +81,11 @@ public static function responseStatusProvider(): array * @param int $code HTTP code * @param string $text Class to return * - * @covers \PHPDraft\Out\TemplateRenderer::get_response_status + * @covers \PHPDraft\Out\HtmlTemplateRenderer::get_response_status */ public function testResponseStatus(int $code, string $text): void { - $return = TemplateRenderer::get_response_status($code); + $return = HtmlTemplateRenderer::get_response_status($code); $this->assertEquals($text, $return); } @@ -120,18 +120,18 @@ public static function requestMethodProvider(): array * @param string $method HTTP Method * @param string $text Class to return * - * @covers \PHPDraft\Out\TemplateRenderer::get_method_icon + * @covers \PHPDraft\Out\HtmlTemplateRenderer::get_method_icon */ public function testRequestMethod(string $method, string $text): void { - $return = TemplateRenderer::get_method_icon($method); + $return = HtmlTemplateRenderer::get_method_icon($method); $this->assertEquals($text, $return); } /** * Test if the value the class is initialized with is correct * - * @covers \PHPDraft\Out\TemplateRenderer::find_include_file + * @covers \PHPDraft\Out\HtmlTemplateRenderer::find_include_file */ public function testIncludeFileDefault(): void { @@ -142,7 +142,7 @@ public function testIncludeFileDefault(): void /** * Test if the value the class is initialized with is correct * - * @covers \PHPDraft\Out\TemplateRenderer::find_include_file + * @covers \PHPDraft\Out\HtmlTemplateRenderer::find_include_file */ public function testIncludeFileFallback(): void { @@ -153,7 +153,7 @@ public function testIncludeFileFallback(): void /** * Test if the value the class is initialized with is correct * - * @covers \PHPDraft\Out\TemplateRenderer::find_include_file + * @covers \PHPDraft\Out\HtmlTemplateRenderer::find_include_file */ public function testIncludeFileNone(): void { @@ -164,7 +164,7 @@ public function testIncludeFileNone(): void /** * Test if the value the class is initialized with is correct * - * @covers \PHPDraft\Out\TemplateRenderer::find_include_file + * @covers \PHPDraft\Out\HtmlTemplateRenderer::find_include_file */ public function testIncludeFileSingle(): void { @@ -176,7 +176,7 @@ public function testIncludeFileSingle(): void /** * Test if the value the class is initialized with is correct * - * @covers \PHPDraft\Out\TemplateRenderer::find_include_file + * @covers \PHPDraft\Out\HtmlTemplateRenderer::find_include_file */ public function testIncludeFileMultiple(): void { @@ -192,7 +192,7 @@ public function testIncludeFileMultiple(): void } /** - * @covers \PHPDraft\Out\TemplateRenderer::get + * @covers \PHPDraft\Out\HtmlTemplateRenderer::get */ public function testGetTemplateFailsEmpty(): void { @@ -205,7 +205,7 @@ public function testGetTemplateFailsEmpty(): void } /** - * @covers \PHPDraft\Out\TemplateRenderer::get + * @covers \PHPDraft\Out\HtmlTemplateRenderer::get * @group twig */ public function testGetTemplate(): void @@ -216,7 +216,7 @@ public function testGetTemplate(): void } /** - * @covers \PHPDraft\Out\TemplateRenderer::get + * @covers \PHPDraft\Out\HtmlTemplateRenderer::get * @group twig */ public function testGetTemplateSorting(): void @@ -228,7 +228,7 @@ public function testGetTemplateSorting(): void } /** - * @covers \PHPDraft\Out\TemplateRenderer::get + * @covers \PHPDraft\Out\HtmlTemplateRenderer::get * @group twig */ public function testGetTemplateMetaData(): void @@ -247,7 +247,7 @@ public function testGetTemplateMetaData(): void } /** - * @covers \PHPDraft\Out\TemplateRenderer::get + * @covers \PHPDraft\Out\HtmlTemplateRenderer::get * @group twig */ public function testGetTemplateCategories(): void diff --git a/src/PHPDraft/Out/TwigFactory.php b/src/PHPDraft/Out/TwigFactory.php index 725cbe89..102ed100 100644 --- a/src/PHPDraft/Out/TwigFactory.php +++ b/src/PHPDraft/Out/TwigFactory.php @@ -25,9 +25,9 @@ public static function get(LoaderInterface $loader): Environment { $twig = new Environment($loader); - $twig->addFilter(new TwigFilter('method_icon', fn(string $string) => TemplateRenderer::get_method_icon($string))); - $twig->addFilter(new TwigFilter('strip_link_spaces', fn(string $string) => TemplateRenderer::strip_link_spaces($string))); - $twig->addFilter(new TwigFilter('response_status', fn(string $string) => TemplateRenderer::get_response_status((int) $string))); + $twig->addFilter(new TwigFilter('method_icon', fn(string $string) => HtmlTemplateRenderer::get_method_icon($string))); + $twig->addFilter(new TwigFilter('strip_link_spaces', fn(string $string) => HtmlTemplateRenderer::strip_link_spaces($string))); + $twig->addFilter(new TwigFilter('response_status', fn(string $string) => HtmlTemplateRenderer::get_response_status((int) $string))); $twig->addFilter(new TwigFilter('status_reason', fn(int $code) => (new Httpstatus())->getReasonPhrase($code))); $twig->addFilter(new TwigFilter('minify_css', function (string $string) { $minify = new Css(); diff --git a/src/PHPDraft/Parse/BaseHtmlGenerator.php b/src/PHPDraft/Parse/BaseHtmlGenerator.php index ab3e25b7..37d750cf 100644 --- a/src/PHPDraft/Parse/BaseHtmlGenerator.php +++ b/src/PHPDraft/Parse/BaseHtmlGenerator.php @@ -12,8 +12,6 @@ namespace PHPDraft\Parse; -use PHPDraft\Out\BaseTemplateRenderer; -use stdClass; use \Stringable; abstract class BaseHtmlGenerator implements Stringable diff --git a/src/PHPDraft/Parse/HtmlGenerator.php b/src/PHPDraft/Parse/HtmlGenerator.php index 3dc813ce..b8a1e6a5 100644 --- a/src/PHPDraft/Parse/HtmlGenerator.php +++ b/src/PHPDraft/Parse/HtmlGenerator.php @@ -12,7 +12,7 @@ namespace PHPDraft\Parse; -use PHPDraft\Out\TemplateRenderer; +use PHPDraft\Out\HtmlTemplateRenderer; /** * Class HtmlGenerator. @@ -36,7 +36,7 @@ class HtmlGenerator extends BaseHtmlGenerator */ public function build_html(string $template = 'default', ?string $image = null, ?string $css = null, ?string $js = null): void { - $gen = new TemplateRenderer($template, $image); + $gen = new HtmlTemplateRenderer($template, $image); if (!is_null($css)) { $gen->css = explode(',', $css); diff --git a/src/PHPDraft/Parse/ParserFactory.php b/src/PHPDraft/Parse/ParserFactory.php index 20271564..4a45dda3 100644 --- a/src/PHPDraft/Parse/ParserFactory.php +++ b/src/PHPDraft/Parse/ParserFactory.php @@ -4,6 +4,8 @@ namespace PHPDraft\Parse; +use PHPDraft\Out\OpenAPI\OpenApiRenderer; + /** * Class ParserFactory. */ @@ -39,4 +41,13 @@ public static function getJson(): BaseHtmlGenerator throw new ResourceException("Couldn't get a JSON parser", 255); } + + public static function getOpenAPI(): OpenApiRenderer + { + if (Drafter::available() || DrafterAPI::available()) { + return new OpenApiRenderer(); + } + + throw new ResourceException("Couldn't get a JSON parser", 255); + } }