From e402484f914b234c1ee06334d5c75788acf53404 Mon Sep 17 00:00:00 2001 From: Fernando Bevilacqua Date: Mon, 13 Sep 2021 07:56:07 -0300 Subject: [PATCH] feat: add config entry `attr_validation` It is now possible to use PollFromText::ATTR_AS_* to validate attributes. Currently there are two options: STRIC_JSON (forces attribute string to be a valid json) and TEXT (allow anything). --- CHANGELOG.md | 4 + README.md | 12 + src/PollFromText.php | 84 +++-- ...st.php => AttributesJsonInOptionsTest.php} | 68 ++-- ....php => AttributesJsonInQuestionsTest.php} | 28 +- tests/Unit/AttributesTextInOptionsTest.php | 317 ++++++++++++++++++ tests/Unit/AttributesTextInQuestionsTest.php | 74 ++++ tests/Unit/SimpleQuestionsTest.php | 4 +- 8 files changed, 527 insertions(+), 64 deletions(-) rename tests/Unit/{AttributesInOptionsTest.php => AttributesJsonInOptionsTest.php} (90%) rename tests/Unit/{AttributesInQuestionsTest.php => AttributesJsonInQuestionsTest.php} (78%) create mode 100644 tests/Unit/AttributesTextInOptionsTest.php create mode 100644 tests/Unit/AttributesTextInQuestionsTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 22777f3..a3c5c8b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ All notable changes to this project are documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and the project follows [semantic versioning](http://semver.org/spec/v2.0.0.html). +## [1.1.0] (https://github.com/ccuffs/poll-from-text/releases/tag/v.1.1.0) - 2021-09-13 +### Added +- Config entry `attr_validation` to control how attributes are validated. + ## [1.0.1] (https://github.com/ccuffs/poll-from-text/releases/tag/v.1.0.1) - 2021-06-23 ### Added - Static method `CCUFFS\Text\PollFromText::make()` for convinente parsing. diff --git a/README.md b/README.md index 3fe7987..5b37726 100644 --- a/README.md +++ b/README.md @@ -220,6 +220,18 @@ array(1) { } ``` +### 3.1 Specific configuration + +Both `parse()` and `make()` accept a `$config` which is an array of configuration to consider when generating the questionnaire. Below is a complete list of all available configuration options: + +```php +$config = [ + 'multiline_question' => false, // if `true`, questions are allowed to have `\n` in their text. + 'attr_validation' => PollFromText::ATTR_AS_TEXT, // allow any text a attribute (no validation) +]; +``` + + ### 4. Testing (related to the package development) If you plan on changing how the package works, be sure to clone it first: diff --git a/src/PollFromText.php b/src/PollFromText.php index 784589f..366a92f 100644 --- a/src/PollFromText.php +++ b/src/PollFromText.php @@ -12,6 +12,24 @@ */ class PollFromText { + /** + * Allow question/answer attributes to be any text. + */ + public const ATTR_AS_TEXT = 1; + + /** + * Force question/answer attributes to be a valid JSON string. + */ + public const ATTR_AS_STRICT_JSON = 2; + + /** + * Lista of available attribute configurations. + */ + private array $attrConfigs = [ + self::ATTR_AS_TEXT, + self::ATTR_AS_STRICT_JSON + ]; + /** * @param mixed $text text to be parsed into a questionnaire * @param array $config specific configuration for this parsing, e.g. allow multiline questions. @@ -27,8 +45,8 @@ protected function split($text) return preg_split('/\R+/', $text, 0, PREG_SPLIT_NO_EMPTY); } - protected function findJsonStringStartingAt($startIndex, $text) { - $json = ''; + protected function findDataStringStartingAt($startIndex, $text) { + $data = ''; $chars = 0; $braces = 0; @@ -38,13 +56,13 @@ protected function findJsonStringStartingAt($startIndex, $text) { do { $currentChar = $text[$startIndex]; - $json .= $currentChar; + $data .= $currentChar; if ($currentChar == '{') { $braces++; } if ($currentChar == '}') { $braces--; } if ($braces == 0) { - return $json; + return $data; } } while ($startIndex++ < strlen($text)); @@ -52,7 +70,7 @@ protected function findJsonStringStartingAt($startIndex, $text) { return $text; } - protected function fillStructureWithDataAttribute(& $structure) { + protected function fillStructureWithDataAttribute(& $structure, array $config = []) { $text = trim($structure['text']); $textFirstChar = $text[0]; @@ -64,7 +82,7 @@ protected function fillStructureWithDataAttribute(& $structure) { // {...} text here // {...} text here { this should be text } again - $data = $this->findJsonStringStartingAt(0, $text); + $data = $this->findDataStringStartingAt(0, $text); if (empty($data)) { return false; @@ -73,12 +91,12 @@ protected function fillStructureWithDataAttribute(& $structure) { $textWithoutAttr = str_replace($data, '', $text); $structure['text'] = trim($textWithoutAttr); - $structure['data'] = $this->decodeAttribute($textWithoutAttr, $data); + $structure['data'] = $this->decodeAttribute($textWithoutAttr, $data, $config); return true; } - protected function fillStructureWithOption(& $structure) { + protected function fillStructureWithOption(& $structure, array $config = []) { $text = trim($structure['text']); $firstChar = $text[0]; $optionChars = ['-', '*']; // TODO: make a config? @@ -120,7 +138,7 @@ protected function fillStructureWithOption(& $structure) { return true; } - protected function extractStructure($text) { + protected function extractStructure($text, array $config = []) { $text = trim($text); $structure = [ 'type' => 'question', @@ -134,7 +152,7 @@ protected function extractStructure($text) { // Something like the following: // {...} text here // {...} text here { this should be text } again - $this->fillStructureWithDataAttribute($structure); + $this->fillStructureWithDataAttribute($structure, $config); // Something like the following: // a) option text @@ -142,18 +160,39 @@ protected function extractStructure($text) { // no_space_here) option text // - option text // * option text - $this->fillStructureWithOption($structure); + $this->fillStructureWithOption($structure, $config); return $structure; } - protected function decodeAttribute($text, $dataAttr) { - try { - $flags = JSON_THROW_ON_ERROR | JSON_NUMERIC_CHECK; - $data = json_decode($dataAttr, true, 512, $flags); - return $data; - } catch (\JsonException $e) { - throw new \UnexpectedValueException("Data attribute '$dataAttr' is not valid JSON in use at text '$text'.", 1, $e); + protected function removeDataGuardChars($text) { + $text = trim($text); + return trim(substr($text, 1, -1)); + } + + protected function decodeAttribute($text, $dataAttr, array $config = []) { + $requestedAttrValidation = isset($config['attr_validation']) ? $config['attr_validation'] : self::ATTR_AS_TEXT; + + foreach($this->attrConfigs as $availableAttrConfig) { + $requestedThisAttrValidation = ($requestedAttrValidation & $availableAttrConfig) != 0; + + if (!$requestedThisAttrValidation) { + continue; + } + + if ($availableAttrConfig == self::ATTR_AS_TEXT) { + return $this->removeDataGuardChars($dataAttr); + + } else if ($availableAttrConfig == self::ATTR_AS_STRICT_JSON) { + try { + $flags = JSON_THROW_ON_ERROR | JSON_NUMERIC_CHECK; + $data = json_decode($dataAttr, true, 512, $flags); + return $data; + + } catch (\JsonException $e) { + throw new \UnexpectedValueException("Data attribute '$dataAttr' is not valid JSON in use at text '$text'.", 1, $e); + } + } } } @@ -209,7 +248,12 @@ protected function addOptionToPreviousQuestion(& $questions, array $structure) { } /** + * Parse the given text and return an array of questions. * + * @param mixed $text text to be parsed into a questionnaire + * @param array $config specific configuration for this parsing, e.g. allow multiline questions. + * + * @return array associative array containing the structured questionnaire. */ public function parse($text, array $config = []) { @@ -233,11 +277,11 @@ public function parse($text, array $config = []) continue; } - $structure = $this->extractStructure($currentLine); + $structure = $this->extractStructure($currentLine, $config); $currentType = $structure['type']; if ($currentType == 'question') { - if($previousType == 'question' && @$config['mutliline_question']) { + if($previousType == 'question' && @$config['multiline_question']) { $this->amendPreviousQuestion($questions, $structure); } else { $questions[] = $this->createQuestion($structure); diff --git a/tests/Unit/AttributesInOptionsTest.php b/tests/Unit/AttributesJsonInOptionsTest.php similarity index 90% rename from tests/Unit/AttributesInOptionsTest.php rename to tests/Unit/AttributesJsonInOptionsTest.php index e7f60cd..693d133 100644 --- a/tests/Unit/AttributesInOptionsTest.php +++ b/tests/Unit/AttributesJsonInOptionsTest.php @@ -1,12 +1,18 @@ PollFromText::ATTR_AS_STRICT_JSON +]; + +test('attribute in select option (star)', function() use ($poller, $config) { $poll = $poller->parse(' Choose favorite color {"attr":true} * Green - '); + ', $config); $this->assertEquals([ [ @@ -22,11 +28,11 @@ ], $poll); }); -test('attribute in select option (no spaces, star)', function() use ($poller) { +test('attribute in select option (no spaces, star)', function() use ($poller, $config) { $poll = $poller->parse(' Choose favorite color {"attr":true}*Green - '); + ', $config); $this->assertEquals([ [ @@ -42,12 +48,12 @@ ], $poll); }); -test('attribute in select option (crazy format, star)', function() use ($poller) { +test('attribute in select option (crazy format, star)', function() use ($poller, $config) { $poll = $poller->parse(' Choose favorite color { "attr" : true }*Green { "attr" : false } * Blue - '); + ', $config); $this->assertEquals([ [ @@ -67,11 +73,11 @@ ], $poll); }); -test('option with attribute char (start)', function() use ($poller) { +test('option with attribute char (start)', function() use ($poller, $config) { $poll = $poller->parse(' Choose favorite color {"attr":true} * Green is { my } favorite - '); + ', $config); $this->assertEquals([ [ @@ -88,19 +94,19 @@ }); -test('throw exception on wrong attribute format (star)', function() use ($poller) { +test('throw exception on wrong attribute format (star)', function() use ($poller, $config) { $poll = $poller->parse(' Choose favorite color {attr = "value" attr2 = "value"} * Green - '); + ', $config); })->throws(UnexpectedValueException::class); -test('attribute in select option (dash)', function() use ($poller) { +test('attribute in select option (dash)', function() use ($poller, $config) { $poll = $poller->parse(' Choose favorite color {"attr":true} - Green - '); + ', $config); $this->assertEquals([ [ @@ -116,11 +122,11 @@ ], $poll); }); -test('attribute in select option (no spaces, dash)', function() use ($poller) { +test('attribute in select option (no spaces, dash)', function() use ($poller, $config) { $poll = $poller->parse(' Choose favorite color {"attr":true}-Green - '); + ', $config); $this->assertEquals([ [ @@ -136,12 +142,12 @@ ], $poll); }); -test('attribute in select option (crazy format, dash)', function() use ($poller) { +test('attribute in select option (crazy format, dash)', function() use ($poller, $config) { $poll = $poller->parse(' Choose favorite color { "attr" : true }-Green { "attr" : false } - Blue - '); + ', $config); $this->assertEquals([ [ @@ -161,11 +167,11 @@ ], $poll); }); -test('option with attribute char (dash)', function() use ($poller) { +test('option with attribute char (dash)', function() use ($poller, $config) { $poll = $poller->parse(' Choose favorite color {"attr":true} - Green is { my } favorite - '); + ', $config); $this->assertEquals([ [ @@ -182,19 +188,19 @@ }); -test('throw exception on wrong attribute format (dash)', function() use ($poller) { +test('throw exception on wrong attribute format (dash)', function() use ($poller, $config) { $poll = $poller->parse(' Choose favorite color {attr = "value" attr2 = "value"} - Green - '); + ', $config); })->throws(UnexpectedValueException::class); -test('attribute in select option (parentheses)', function() use ($poller) { +test('attribute in select option (parentheses)', function() use ($poller, $config) { $poll = $poller->parse(' Choose favorite color {"attr":true} a) Green - '); + ', $config); $this->assertEquals([ [ @@ -210,11 +216,11 @@ ], $poll); }); -test('attribute in select option (no spaces, parentheses)', function() use ($poller) { +test('attribute in select option (no spaces, parentheses)', function() use ($poller, $config) { $poll = $poller->parse(' Choose favorite color {"attr":true}a)Green - '); + ', $config); $this->assertEquals([ [ @@ -230,12 +236,12 @@ ], $poll); }); -test('attribute in select option (crazy format, parentheses)', function() use ($poller) { +test('attribute in select option (crazy format, parentheses)', function() use ($poller, $config) { $poll = $poller->parse(' Choose favorite color { "attr" : true }a)Green { "attr" : false } b) Blue - '); + ', $config); $this->assertEquals([ [ @@ -255,11 +261,11 @@ ], $poll); }); -test('option with attribute char (parentheses)', function() use ($poller) { +test('option with attribute char (parentheses)', function() use ($poller, $config) { $poll = $poller->parse(' Choose favorite color {"attr":"string", "field": 20} a) Green is { my } favorite - '); + ', $config); $this->assertEquals([ [ @@ -276,9 +282,9 @@ }); -test('throw exception on wrong attribute format (parentheses)', function() use ($poller) { +test('throw exception on wrong attribute format (parentheses)', function() use ($poller, $config) { $poll = $poller->parse(' Choose favorite color {attr = "value" attr2 = "value"} a) Green - '); + ', $config); })->throws(UnexpectedValueException::class); diff --git a/tests/Unit/AttributesInQuestionsTest.php b/tests/Unit/AttributesJsonInQuestionsTest.php similarity index 78% rename from tests/Unit/AttributesInQuestionsTest.php rename to tests/Unit/AttributesJsonInQuestionsTest.php index 876342e..409a817 100644 --- a/tests/Unit/AttributesInQuestionsTest.php +++ b/tests/Unit/AttributesJsonInQuestionsTest.php @@ -1,11 +1,17 @@ PollFromText::ATTR_AS_STRICT_JSON +]; + +test('recognize attribute simple question', function() use ($poller, $config) { $poll = $poller->parse(' {"attr":true} Choose favorite color - '); + ', $config); $this->assertEquals([ [ @@ -16,11 +22,11 @@ ], $poll); }); -test('recognize attribute select question', function() use ($poller) { +test('recognize attribute select question', function() use ($poller, $config) { $poll = $poller->parse(' {"attr":true} Choose favorite color * Green - '); + ', $config); $this->assertEquals([ [ @@ -32,10 +38,10 @@ ], $poll); }); -test('complext attribute list', function() use ($poller) { +test('complext attribute list', function() use ($poller, $config) { $poll = $poller->parse(' {"attr":"value", "attr2":"value"} Type favorite color - '); + ', $config); $this->assertEquals([ [ @@ -46,10 +52,10 @@ ], $poll); }); -test('question with attribute char', function() use ($poller) { +test('question with attribute char', function() use ($poller, $config) { $poll = $poller->parse(' {"attr":"value"} Choose { favorite } color - '); + ', $config); $this->assertEquals([ [ @@ -60,8 +66,8 @@ ], $poll); }); -test('throw exception on wrong attribute format', function() use ($poller) { +test('throw exception on wrong attribute format', function() use ($poller, $config) { $poll = $poller->parse(' {attr = "value" attr2 = "value"} Choose favorite color - '); + ', $config); })->throws(UnexpectedValueException::class); \ No newline at end of file diff --git a/tests/Unit/AttributesTextInOptionsTest.php b/tests/Unit/AttributesTextInOptionsTest.php new file mode 100644 index 0000000..18d813b --- /dev/null +++ b/tests/Unit/AttributesTextInOptionsTest.php @@ -0,0 +1,317 @@ + PollFromText::ATTR_AS_TEXT, +]; + +test('attribute in select option (star)', function() use ($poller, $config) { + $poll = $poller->parse(' + Choose favorite color + {attr} * Green + ', $config); + + $this->assertEquals([ + [ + 'text' => 'Choose favorite color', + 'type' => 'select', + 'options' => [ + [ + 'text' => 'Green', + 'data' => 'attr' + ] + ] + ], + ], $poll); +}); + +test('attribute in select option (no spaces, star)', function() use ($poller, $config) { + $poll = $poller->parse(' + Choose favorite color + {attr}*Green + ', $config); + + $this->assertEquals([ + [ + 'text' => 'Choose favorite color', + 'type' => 'select', + 'options' => [ + [ + 'text' => 'Green', + 'data' => 'attr' + ] + ] + ], + ], $poll); +}); + +test('attribute in select option (crazy format, star)', function() use ($poller, $config) { + $poll = $poller->parse(' + Choose favorite color + { attr_true }*Green + { attr_false } * Blue + ', $config); + + $this->assertEquals([ + [ + 'text' => 'Choose favorite color', + 'type' => 'select', + 'options' => [ + [ + 'text' => 'Green', + 'data' => 'attr_true' + ], + [ + 'text' => 'Blue', + 'data' => 'attr_false' + ] + ] + ], + ], $poll); +}); + +test('option with attribute char (start)', function() use ($poller, $config) { + $poll = $poller->parse(' + Choose favorite color + {attr} * Green is { my } favorite + ', $config); + + $this->assertEquals([ + [ + 'text' => 'Choose favorite color', + 'type' => 'select', + 'options' => [ + [ + 'text' => 'Green is { my } favorite', + 'data' => 'attr' + ] + ] + ], + ], $poll); +}); + + +test('throw exception on wrong attribute format (star)', function() use ($poller, $config) { + expect( + $poll = $poller->parse(' + Choose favorite color + {attr = "value" attr2 = "value"} * Green + ', $config) + )->toBeArray(); +}); + + +test('attribute in select option (dash)', function() use ($poller, $config) { + $poll = $poller->parse(' + Choose favorite color + {attr} - Green + ', $config); + + $this->assertEquals([ + [ + 'text' => 'Choose favorite color', + 'type' => 'select', + 'options' => [ + [ + 'text' => 'Green', + 'data' => 'attr' + ] + ] + ], + ], $poll); +}); + +test('attribute in select option (no spaces, dash)', function() use ($poller, $config) { + $poll = $poller->parse(' + Choose favorite color + {attr}-Green + ', $config); + + $this->assertEquals([ + [ + 'text' => 'Choose favorite color', + 'type' => 'select', + 'options' => [ + [ + 'text' => 'Green', + 'data' => 'attr' + ] + ] + ], + ], $poll); +}); + +test('attribute in select option (crazy format, dash)', function() use ($poller, $config) { + $poll = $poller->parse(' + Choose favorite color + { attr_true }-Green + { attr_false } - Blue + ', $config); + + $this->assertEquals([ + [ + 'text' => 'Choose favorite color', + 'type' => 'select', + 'options' => [ + [ + 'text' => 'Green', + 'data' => 'attr_true' + ], + [ + 'text' => 'Blue', + 'data' => 'attr_false' + ] + ] + ], + ], $poll); +}); + +test('option with attribute char (dash)', function() use ($poller, $config) { + $poll = $poller->parse(' + Choose favorite color + {attr} - Green is { my } favorite + ', $config); + + $this->assertEquals([ + [ + 'text' => 'Choose favorite color', + 'type' => 'select', + 'options' => [ + [ + 'text' => 'Green is { my } favorite', + 'data' => 'attr' + ] + ] + ], + ], $poll); +}); + + +test('not throw exception on wrong attribute format (dash)', function() use ($poller, $config) { + expect( + $poll = $poller->parse(' + Choose favorite color + {attr = "value" attr2 = "value"} - Green + ', $config) + )->toBeArray(); +}); + + +test('attribute in select option (parentheses)', function() use ($poller, $config) { + $poll = $poller->parse(' + Choose favorite color + {attr} a) Green + ', $config); + + $this->assertEquals([ + [ + 'text' => 'Choose favorite color', + 'type' => 'select', + 'options' => [ + 'a' => [ + 'text' => 'Green', + 'data' => 'attr' + ] + ] + ], + ], $poll); +}); + +test('attribute in select option (no spaces, parentheses)', function() use ($poller, $config) { + $poll = $poller->parse(' + Choose favorite color + {attr}a)Green + ', $config); + + $this->assertEquals([ + [ + 'text' => 'Choose favorite color', + 'type' => 'select', + 'options' => [ + 'a' => [ + 'text' => 'Green', + 'data' => 'attr' + ] + ] + ], + ], $poll); +}); + +test('attribute in select option (crazy format, parentheses)', function() use ($poller, $config) { + $poll = $poller->parse(' + Choose favorite color + { attr_true }a)Green + { attr_false } b) Blue + ', $config); + + $this->assertEquals([ + [ + 'text' => 'Choose favorite color', + 'type' => 'select', + 'options' => [ + 'a' => [ + 'text' => 'Green', + 'data' => 'attr_true' + ], + 'b' => [ + 'text' => 'Blue', + 'data' => 'attr_false' + ] + ] + ], + ], $poll); +}); + +test('option with attribute char (parentheses)', function() use ($poller, $config) { + $poll = $poller->parse(' + Choose favorite color + {attr_string field_20} a) Green is { my } favorite + ', $config); + + $this->assertEquals([ + [ + 'text' => 'Choose favorite color', + 'type' => 'select', + 'options' => [ + 'a' => [ + 'text' => 'Green is { my } favorite', + 'data' => 'attr_string field_20' + ] + ] + ], + ], $poll); +}); + + +test('not throw exception on wrong attribute format (parentheses)', function() use ($poller, $config) { + expect( + $poll = $poller->parse(' + Choose favorite color + {attr = "value" attr2 = "value"} a) Green + ', $config) + )->toBeArray(); +}); + + +test('attribute in select option (star, quotes)', function() use ($poller, $config) { + $poll = $poller->parse(' + Choose favorite color + {"attr"} * Green + ', $config); + + $this->assertEquals([ + [ + 'text' => 'Choose favorite color', + 'type' => 'select', + 'options' => [ + [ + 'text' => 'Green', + 'data' => '"attr"' + ] + ] + ], + ], $poll); +}); \ No newline at end of file diff --git a/tests/Unit/AttributesTextInQuestionsTest.php b/tests/Unit/AttributesTextInQuestionsTest.php new file mode 100644 index 0000000..f3ccde7 --- /dev/null +++ b/tests/Unit/AttributesTextInQuestionsTest.php @@ -0,0 +1,74 @@ + PollFromText::ATTR_AS_TEXT +]; + +test('recognize attribute simple question', function() use ($poller, $config) { + $poll = $poller->parse(' + {attr} Choose favorite color + ', $config); + + $this->assertEquals([ + [ + 'text' => 'Choose favorite color', + 'type' => 'input', + 'data' => 'attr' + ], + ], $poll); +}); + +test('recognize attribute select question', function() use ($poller, $config) { + $poll = $poller->parse(' + {attr} Choose favorite color + * Green + ', $config); + + $this->assertEquals([ + [ + 'text' => 'Choose favorite color', + 'type' => 'select', + 'options' => ['Green'], + 'data' => 'attr' + ], + ], $poll); +}); + +test('complext attribute list', function() use ($poller, $config) { + $poll = $poller->parse(' + {"attr":"value", "attr2":"value"} Type favorite color + ', $config); + + $this->assertEquals([ + [ + 'text' => 'Type favorite color', + 'type' => 'input', + 'data' => '"attr":"value", "attr2":"value"' + ], + ], $poll); +}); + +test('question with attribute char', function() use ($poller, $config) { + $poll = $poller->parse(' + {attr_value} Choose { favorite } color + ', $config); + + $this->assertEquals([ + [ + 'text' => 'Choose { favorite } color', + 'type' => 'input', + 'data' => 'attr_value' + ], + ], $poll); +}); + +test('not throw exception on wrong attribute format', function() use ($poller, $config) { + expect($poller->parse(' + {attr = "value" attr2 = "value"} Choose favorite color + ', $config) + )->toBeArray(); +}); \ No newline at end of file diff --git a/tests/Unit/SimpleQuestionsTest.php b/tests/Unit/SimpleQuestionsTest.php index 81b621e..253e0a8 100644 --- a/tests/Unit/SimpleQuestionsTest.php +++ b/tests/Unit/SimpleQuestionsTest.php @@ -46,7 +46,7 @@ multiline question? '; $poll = $poller->parse($question, [ - 'mutliline_question' => true + 'multiline_question' => true ]); $this->assertEquals([[ 'text' => 'Is this a multiline question?', @@ -60,7 +60,7 @@ This is another '; $poll = $poller->parse($question, [ - 'mutliline_question' => false + 'multiline_question' => false ]); $this->assertEquals([ [