From 2ef71e606bcbf586eb56a79dd8196941176f4255 Mon Sep 17 00:00:00 2001 From: Luke Carrier Date: Fri, 15 Jun 2018 01:28:23 +0100 Subject: [PATCH] format: XML --- classes/format/xml_format.php | 185 ++++++++++++++++++++++++++++++++++ tests/xml_format_test.php | 145 ++++++++++++++++++++++++++ 2 files changed, 330 insertions(+) create mode 100644 classes/format/xml_format.php create mode 100644 tests/xml_format_test.php diff --git a/classes/format/xml_format.php b/classes/format/xml_format.php new file mode 100644 index 0000000..c3e33f1 --- /dev/null +++ b/classes/format/xml_format.php @@ -0,0 +1,185 @@ +. + +/** + * Loaded REST. + * + * @package webservice_loadedrest + * @author Luke Carrier + * @copyright 2018 Luke Carrier + */ + +namespace webservice_loadedrest\format; + +use coding_exception; +use Exception; +use external_description; +use external_multiple_structure; +use external_single_structure; +use external_value; +use invalid_parameter_exception; +use XMLWriter; + +defined('MOODLE_INTERNAL') || die; + +/** + * XML format. + */ +class xml_format extends abstract_format implements format { + /** + * XML version. + * + * @var string + */ + const VERSION = '1.0'; + + /** + * XML document encoding. + * + * @var string + */ + const ENCODING = 'utf-8'; + + /** + * @inheritdoc + */ + public function get_name() { + return 'xml'; + } + + /** + * @inheritdoc + */ + public function serialise($params) { + throw new coding_exception('unimplemented'); + } + + /** + * @inheritdoc + */ + public function deserialise($body) { + $oldvalue = libxml_use_internal_errors(true); + $data = simplexml_load_string($body); + $errors = libxml_get_errors(); + libxml_use_internal_errors($oldvalue); + + if ($data === false) { + throw new invalid_parameter_exception( + 'mangled and hideous though it was, request body could not' + . ' be parsed as valid xml'); + } + + return json_decode(json_encode($data), true); + } + + /** + * @inheritdoc + */ + public function send_headers() { + parent::send_headers(); + header('Content-Type: application/xml; charset=utf-8'); + header('Content-Disposition: inline; filename="response.xml"'); + } + + /** + * @inheritdoc + */ + public function send_error(Exception $exception) { + $doc = new XMLWriter(); + $doc->openMemory(); + $doc->startDocument(static::VERSION, static::ENCODING); + $doc->startElement('response'); + $doc->startElement('success'); + $doc->text('false'); + $doc->endElement(); + $doc->startElement('exception'); + $doc->startAttribute('class'); + $doc->text(get_class($exception)); + $doc->endAttribute(); + $doc->startAttribute('code'); + $doc->text($exception->getCode()); + $doc->endAttribute(); + $doc->startElement('message'); + $doc->text($exception->getMessage()); + $doc->endElement(); + + if (debugging() && property_exists($exception, 'debuginfo')) { + $doc->startElement('debug'); + /** @noinspection PhpUndefinedFieldInspection */ + $doc->text($exception->debuginfo); + $doc->endElement(); + } + + $doc->endElement(); + $doc->endDocument(); + echo $doc->outputMemory(); + } + + /** + * @inheritdoc + */ + public function send_response($result, external_description $description) { + $doc = new XMLWriter(); + $doc->openMemory(); + $doc->startDocument(static::VERSION, static::ENCODING); + $doc->startElement('response'); + $this->to_xml($doc, $result, $description); + $doc->endElement(); + $doc->endDocument(); + echo $doc->outputMemory(); + } + + /** + * Dump the result to XML. + * + * @param XMLWriter $doc + * @param $result + * @param external_description $description + * @param string|null $key + */ + protected function to_xml(XMLWriter $doc, $result, external_description $description, $key=null) { + $singlekey = $key ?? 'value'; + + if ($description instanceof external_value) { + switch ($description->type) { + case PARAM_BOOL: + $doc->startElement($singlekey); + $doc->text($result ? 'true' : 'false'); + $doc->endElement(); + break; + default: + $doc->startElement($singlekey); + $doc->text($result); + $doc->endElement(); + } + } elseif ($description instanceof external_single_structure) { + $doc->startElement($singlekey); + foreach ($description->keys as $singlekey => $keydescription) { + $this->to_xml($doc, $result[$singlekey], $keydescription, $singlekey); + } + $doc->endElement(); + } elseif ($description instanceof external_multiple_structure) { + $doc->startElement($singlekey); + foreach ($result as $resultitem) { + $this->to_xml($doc, $resultitem, $description->content, $singlekey); + } + $doc->endElement(); + } else { + throw new coding_exception(sprintf( + 'unknown external_description type %s', get_class($description))); + } + } +} diff --git a/tests/xml_format_test.php b/tests/xml_format_test.php new file mode 100644 index 0000000..27e2717 --- /dev/null +++ b/tests/xml_format_test.php @@ -0,0 +1,145 @@ +. + +/** + * Loaded REST. + * + * @package webservice_loadedrest + * @author Luke Carrier + * @copyright 2018 Luke Carrier + */ + +use webservice_loadedrest\format\xml_format; + +defined('MOODLE_INTERNAL') || die; + +// Data providers seem to execute before the PHPUnit bootstrap, so Moodle's +// classloader hasn't yet been enabled before we start instantiating external_* +// objects. +global $CFG; +require_once $CFG->libdir . '/externallib.php'; + +/** + * Loaded REST server test suite. + * + * @group webservice_loadedrest + */ +class webservice_loadedrest_xml_format_testcase extends advanced_testcase { + public function data_deserialise() { + return [ + [ + 'body' => 'text', + 'expect' => [ + 'some' => 'text', + ], + ], + [ + 'body' => '1', + 'expect' => [ + 'some' => 1, + ], + ], + ]; + } + + /** + * @dataProvider data_deserialise + */ + public function test_deserialise($body, $expect) { + $format = new xml_format(); + $this->assertEquals($expect, $format->deserialise($body)); + } + + + /** + * @expectedException invalid_parameter_exception + * @expectedExceptionMessage mangled and hideous though it was, request body could not be parsed as valid xml + */ + public function test_deserialise_throws() { + $format = new xml_format(); + $format->deserialise('send_error($exception); + $output = ob_get_contents(); + ob_end_clean(); + + $this->assertRegExp('%\.*\%', $output); + $this->assertRegExp('%\false\%', $output); + $this->assertRegExp('%\assertRegExp('%class="Exception"%', $output); + $this->assertRegExp('%code="1"%', $output); + $this->assertRegExp('%\message\%', $output); + } + + public function data_send_response() { + return [ + [ + 'result' => true, + 'description' => new external_value(PARAM_BOOL), + 'expect' => [ + '%true%', + ], + ], + [ + 'result' => [ + 'key' => 3.14, + ], + 'description' => new external_single_structure([ + 'key' => new external_value(PARAM_FLOAT), + ]), + 'expect' => [ + '%3.14%', + ], + ], + [ + 'result' => [ + [ + 'key' => 3.14, + ], + ], + 'description' => new external_multiple_structure(new external_single_structure([ + 'key' => new external_value(PARAM_FLOAT), + ])), + 'expect' => [ + '%3.14%', + ], + ], + ]; + } + + /** + * @dataProvider data_send_response + */ + public function test_send_response($result, external_description $description, $expect) { + $format = new xml_format(); + + ob_start(); + $format->send_response($result, $description); + $output = ob_get_contents(); + ob_end_clean(); + + $this->assertRegExp('%\.*\%', $output); + foreach ($expect as $regexp) { + $this->assertRegExp($regexp, $output); + } + } +}