From 4c28f9cf9c882803cc810327b657437f815afa43 Mon Sep 17 00:00:00 2001
From: Philipp <admin@philipp.info>
Date: Sun, 15 Jan 2023 00:53:51 +0100
Subject: [PATCH] Config: Improve the node.config.php transformation - Add more
 types - Improvement for assoziative arrays and key-value arrays - Add a lot
 more tests

---
 .../Config/Util/ConfigFileTransformer.php     | 200 ++++++++++++++----
 tests/datasets/config/B.node.config.php       |   8 +-
 .../{ => transformer}/C.node.config.php       |   0
 .../config/transformer/D.node.config.php      |   8 +
 .../config/transformer/object.node.config.php |   7 +
 .../transformer/ressource.node.config.php     |   7 +
 .../transformer/small_types.node.config.php   |  12 ++
 .../config/transformer/types.node.config.php  |  75 +++++++
 .../Config/Util/ConfigFileTransformerTest.php |  25 ++-
 9 files changed, 300 insertions(+), 42 deletions(-)
 rename tests/datasets/config/{ => transformer}/C.node.config.php (100%)
 create mode 100644 tests/datasets/config/transformer/D.node.config.php
 create mode 100644 tests/datasets/config/transformer/object.node.config.php
 create mode 100644 tests/datasets/config/transformer/ressource.node.config.php
 create mode 100644 tests/datasets/config/transformer/small_types.node.config.php
 create mode 100644 tests/datasets/config/transformer/types.node.config.php

diff --git a/src/Core/Config/Util/ConfigFileTransformer.php b/src/Core/Config/Util/ConfigFileTransformer.php
index ac4990df13..7cd351ab8f 100644
--- a/src/Core/Config/Util/ConfigFileTransformer.php
+++ b/src/Core/Config/Util/ConfigFileTransformer.php
@@ -26,67 +26,191 @@ namespace Friendica\Core\Config\Util;
  */
 class ConfigFileTransformer
 {
+	/**
+	 * The public method to start the encoding
+	 *
+	 * @param array $data A full config array
+	 *
+	 * @return string The config stream, which can be saved
+	 */
 	public static function encode(array $data): string
 	{
+		// Add the typical header values
 		$dataString = '<?php' . PHP_EOL . PHP_EOL;
-		$dataString .= 'return [' . PHP_EOL;
+		$dataString .= 'return ';
 
-		$categories = array_keys($data);
+		$dataString .= static::extractArray($data);
 
-		foreach ($categories as $category) {
-			if (is_null($data[$category])) {
-				$dataString .= "\t'$category' => null," . PHP_EOL;
-				continue;
-			}
-
-			$dataString .= "\t'$category' => [" . PHP_EOL;
+		// the last array line, close it with a semicolon
+		$dataString .= ";" . PHP_EOL;
 
-			if (is_array($data[$category])) {
-				$keys = array_keys($data[$category]);
+		return $dataString;
+	}
 
-				foreach ($keys as $key) {
-					$dataString .= static::mapConfigValue($key, $data[$category][$key]);
-				}
-			}
-			$dataString .= "\t]," . PHP_EOL;
+	/**
+	 * Extracts an inner config array.
+	 * Either as a Key => Value pair array or as an assoziative array
+	 *
+	 * @param array $config             The config array which should get extracted
+	 * @param int   $level              The current level of recursion (necessary for tab-indentation calculation)
+	 * @param bool  $inAssoziativeArray If true, the current array resides inside another assoziative array. Different rules may be applicable
+	 *
+	 * @return string The config string
+	 */
+	protected static function extractArray(array $config, int $level = 0, bool $inAssoziativeArray = false): string
+	{
+		if (array_values($config) === $config) {
+			return self::extractAssoziativeArray($config, $level, $inAssoziativeArray);
+		} else {
+			return self::extractKeyValueArray($config, $level, $inAssoziativeArray);
 		}
-
-		$dataString .= "];" . PHP_EOL;
-
-		return $dataString;
 	}
 
-	protected static function extractArray(array $config, int $level = 0): string
+	/**
+	 * Extracts a key-value array and save it into a string
+	 * output:
+	 * [
+	 *    'key' => value,
+	 *    'key' => value,
+	 *    ...
+	 * ]
+	 *
+	 * @param array $config             The key-value array
+	 * @param int   $level              The current level of recursion (necessary for tab-indentation calculation)
+	 * @param bool  $inAssoziativeArray If true, the current array resides inside another assoziative array. Different rules may be applicable
+	 *
+	 * @return string The config string
+	 */
+	protected static function extractKeyValueArray(array $config, int $level = 0, bool $inAssoziativeArray = false): string
 	{
 		$string = '';
 
+		// Because we're in an assoziative array, we have to add a line-break first
+		if ($inAssoziativeArray) {
+			$string .= PHP_EOL . str_repeat("\t", $level);
+		}
+
+		// Add a typical Line break for a taxative list of key-value pairs
+		$string .= '[' . PHP_EOL;
+
 		foreach ($config as $configKey => $configValue) {
-			$string .= static::mapConfigValue($configKey, $configValue, $level);
+			$string .= str_repeat("\t", $level + 1) .
+					   "'$configKey' => " .
+					   static::transformConfigValue($configValue, $level) .
+					   ',' . PHP_EOL;
 		}
 
+		$string .= str_repeat("\t", $level) . ']';
+
 		return $string;
 	}
 
-	protected static function mapConfigValue(string $key, $value, $level = 0): string
+	/**
+	 * Extracts an assoziative array and save it into a string
+	 * output1 - simple:
+	 * [ value, value, value ]
+	 *
+	 * output2 - complex:
+	 * [
+	 *    [ value, value, value ],
+	 *    value,
+	 *    [
+	 *       key => value,
+	 *       key => value,
+	 *    ],
+	 * ]
+	 *
+	 * @param array $config             The assoziative array
+	 * @param int   $level              The current level of recursion (necessary for tab-indentation calculation)
+	 * @param bool  $inAssoziativeArray If true, the current array resides inside another assoziative array. Different rules may be applicable
+	 *
+	 * @return string The config string
+	 */
+	protected static function extractAssoziativeArray(array $config, int $level = 0, bool $inAssoziativeArray = false): string
 	{
-		$string = str_repeat("\t", $level + 2) . "'$key' => ";
-
-		if (is_null($value)) {
-			$string .= "null,";
-		} elseif (is_array($value)) {
-			$string .= "[" . PHP_EOL;
-			$string .= static::extractArray($value, ++$level);
-			$string .= str_repeat("\t", $level + 1) . '],';
-		} elseif (is_bool($value)) {
-			$string .= ($value ? 'true' : 'false') . ",";
-		} elseif (is_numeric($value)) {
-			$string .= $value . ",";
-		} else {
-			$string .= sprintf('\'%s\',', addcslashes($value, '\'\\'));
+		$string = '[';
+
+		$countConfigValues = count($config);
+		// multiline defines, if each entry uses a new line
+		$multiline = false;
+
+		// Search if any value is an array, because then other formatting rules are applicable
+		foreach ($config as $item) {
+			if (is_array($item)) {
+				$multiline = true;
+				break;
+			}
+		}
+
+		for ($i = 0; $i < $countConfigValues; $i++) {
+			$isArray = is_array($config[$i]);
+
+			/**
+			 * In case this is an array in an array, directly extract this array again and continue
+			 * Skip any other logic since this isn't applicable for an array in an array
+			 */
+			if ($isArray) {
+				$string   .= PHP_EOL . str_repeat("\t", $level + 1);
+				$string   .= static::extractArray($config[$i], $level + 1, $inAssoziativeArray) . ',';
+				continue;
+			}
+
+			if ($multiline) {
+				$string .= PHP_EOL . str_repeat("\t", $level + 1);
+			}
+
+			$string .= static::transformConfigValue($config[$i], $level, true);
+
+			// add trailing commas or whitespaces for certain config entries
+			if (($i < ($countConfigValues - 1))) {
+				$string .= ',';
+				if (!$multiline) {
+					$string .= ' ';
+				}
+			}
+		}
+
+		// Add a new line for the last bracket as well
+		if ($multiline) {
+			$string .= PHP_EOL . str_repeat("\t", $level);
 		}
 
-		$string .= PHP_EOL;
+		$string .= ']';
 
 		return $string;
 	}
+
+	/**
+	 * Transforms one config value and returns the corresponding text-representation
+	 *
+	 * @param mixed $value              Any value to transform
+	 * @param int   $level              The current level of recursion (necessary for tab-indentation calculation)
+	 * @param bool  $inAssoziativeArray If true, the current array resides inside another assoziative array. Different rules may be applicable
+	 *
+	 * @return string
+	 */
+	protected static function transformConfigValue($value, int $level = 0, bool $inAssoziativeArray = false): string
+	{
+		switch (gettype($value)) {
+			case "boolean":
+				return ($value ? 'true' : 'false');
+			case "integer":
+			case "double":
+				return $value;
+			case "string":
+				return sprintf('\'%s\'', addcslashes($value, '\'\\'));
+			case "array":
+				return static::extractArray($value, ++$level, $inAssoziativeArray);
+			case "NULL":
+				return "null";
+			case "object":
+			case "resource":
+			case "resource (closed)":
+				throw new \InvalidArgumentException(sprintf('%s in configs are not supported yet.', gettype($value)));
+			case "unknown type":
+				throw new \InvalidArgumentException(sprintf('%s is an unknown value', $value));
+			default:
+				throw new \InvalidArgumentException(sprintf('%s is currently unsupported', $value));
+		}
+	}
 }
diff --git a/tests/datasets/config/B.node.config.php b/tests/datasets/config/B.node.config.php
index 499e092a45..6b0f15ad1e 100644
--- a/tests/datasets/config/B.node.config.php
+++ b/tests/datasets/config/B.node.config.php
@@ -23,9 +23,13 @@ return [
 					'string2' => 'false',
 				],
 			],
-			'v' => true,
-			'v3' => 1,
+			'bool_true' => true,
+			'bool_false' => false,
+			'int_1_not_true' => 1,
+			'int_0_not_false' => 0,
 			'v4' => 5.6443,
+			'string_1_not_true' => '1',
+			'string_0_not_false' => '0',
 		],
 	],
 	'system' => [
diff --git a/tests/datasets/config/C.node.config.php b/tests/datasets/config/transformer/C.node.config.php
similarity index 100%
rename from tests/datasets/config/C.node.config.php
rename to tests/datasets/config/transformer/C.node.config.php
diff --git a/tests/datasets/config/transformer/D.node.config.php b/tests/datasets/config/transformer/D.node.config.php
new file mode 100644
index 0000000000..9e5707a9bb
--- /dev/null
+++ b/tests/datasets/config/transformer/D.node.config.php
@@ -0,0 +1,8 @@
+<?php
+
+return [
+	'string_int_values' => [
+		'string_1_not_true' => '1',
+		'string_0_not_false' => '0',
+	],
+];
diff --git a/tests/datasets/config/transformer/object.node.config.php b/tests/datasets/config/transformer/object.node.config.php
new file mode 100644
index 0000000000..f1807199bc
--- /dev/null
+++ b/tests/datasets/config/transformer/object.node.config.php
@@ -0,0 +1,7 @@
+<?php
+
+return [
+	'object' => [
+		'objects_not_supported' => new stdClass(),
+	],
+];
diff --git a/tests/datasets/config/transformer/ressource.node.config.php b/tests/datasets/config/transformer/ressource.node.config.php
new file mode 100644
index 0000000000..b83a139e34
--- /dev/null
+++ b/tests/datasets/config/transformer/ressource.node.config.php
@@ -0,0 +1,7 @@
+<?php
+
+return [
+	'ressource' => [
+		'ressources_not_allowed' => new \GuzzleHttp\Psr7\AppendStream(),
+	],
+];
diff --git a/tests/datasets/config/transformer/small_types.node.config.php b/tests/datasets/config/transformer/small_types.node.config.php
new file mode 100644
index 0000000000..4bf92e3b9b
--- /dev/null
+++ b/tests/datasets/config/transformer/small_types.node.config.php
@@ -0,0 +1,12 @@
+<?php
+
+return [
+	'small_cat' => [
+		[
+			'key' => 'value',
+		],
+		[
+			'key2' => 'value2',
+		],
+	],
+];
diff --git a/tests/datasets/config/transformer/types.node.config.php b/tests/datasets/config/transformer/types.node.config.php
new file mode 100644
index 0000000000..d2a7dfe57d
--- /dev/null
+++ b/tests/datasets/config/transformer/types.node.config.php
@@ -0,0 +1,75 @@
+<?php
+
+return [
+	'type_test' => [
+		'bool_true' => true,
+		'bool_false' => false,
+		'int_1' => 1,
+		'int_0' => 2,
+		'int_12345' => 12345,
+		'float' => 1.234,
+		'double_E+' => 1.24E+20,
+		'double_E-' => 7.0E-10,
+		'null' => null,
+		'array' => [1, '2', '3', 4.0E-10, 12345, 0, false, 'true', true],
+		'array_keys' => [
+			'int_1' => 1,
+			'string_2' => '2',
+			'string_3' => '3',
+			'double' => 4.0E-10,
+			'int' => 12345,
+			'int_0' => 0,
+			'false' => false,
+			'string_true' => 'true',
+			'true' => true,
+		],
+		'array_extended' => [
+			[
+				'key_1' => 'value_1',
+				'key_2' => 'value_2',
+				'key_3' => [
+					'inner_key' => 'inner_value',
+				],
+			],
+			[
+				'key_2' => false,
+				'0' => [
+					'is_that' => true,
+					'0' => [
+						'working' => '?',
+					],
+				],
+				'inner_array' => [
+					[
+						'key' => 'value',
+						'key2' => 12,
+					],
+				],
+				'key_3' => true,
+			],
+			['value', 'value2'],
+			[
+				[
+					'key' => 123,
+				],
+				'test',
+				'test52',
+				'test23',
+				[
+					'key' => 456,
+				],
+			],
+		],
+	],
+	'other_cat' => [
+		'key' => 'value',
+	],
+	'other_cat2' => [
+		[
+			'key' => 'value',
+		],
+		[
+			'key2' => 'value2',
+		],
+	],
+];
diff --git a/tests/src/Core/Config/Util/ConfigFileTransformerTest.php b/tests/src/Core/Config/Util/ConfigFileTransformerTest.php
index 5a0ab96a46..bb156cf282 100644
--- a/tests/src/Core/Config/Util/ConfigFileTransformerTest.php
+++ b/tests/src/Core/Config/Util/ConfigFileTransformerTest.php
@@ -36,8 +36,25 @@ class ConfigFileTransformerTest extends MockedTest
 				'configFile' => (dirname(__DIR__, 4) . '/datasets/config/B.node.config.php'),
 			],
 			'friendica.local' => [
-				'configFile' => (dirname(__DIR__, 4) . '/datasets/config/C.node.config.php'),
+				'configFile' => (dirname(__DIR__, 4) . '/datasets/config/transformer/C.node.config.php'),
 			],
+			'friendica.local.2' => [
+				'configFile' => (dirname(__DIR__, 4) . '/datasets/config/transformer/D.node.config.php'),
+			],
+			'object_invalid' => [
+				'configFile' => (dirname(__DIR__, 4) . '/datasets/config/transformer/object.node.config.php'),
+				'assertException' => true,
+			],
+			'ressource_invalid' => [
+				'configFile' => (dirname(__DIR__, 4) . '/datasets/config/transformer/ressource.node.config.php'),
+				'assertException' => true,
+			],
+			'test_types' => [
+				'configFile' => (dirname(__DIR__, 4) . '/datasets/config/transformer/types.node.config.php'),
+			],
+			'small_types' => [
+				'configFile' => (dirname(__DIR__, 4) . '/datasets/config/transformer/small_types.node.config.php'),
+			]
 		];
 	}
 
@@ -46,10 +63,14 @@ class ConfigFileTransformerTest extends MockedTest
 	 *
 	 * @dataProvider dataTests
 	 */
-	public function testConfigFile(string $configFile)
+	public function testConfigFile(string $configFile, bool $assertException = false)
 	{
 		$dataArray = include $configFile;
 
+		if ($assertException) {
+			self::expectException(\InvalidArgumentException::class);
+		}
+
 		$newConfig = ConfigFileTransformer::encode($dataArray);
 
 		self::assertEquals(file_get_contents($configFile), $newConfig);
-- 
2.39.5