diff --git a/src/php/access/AbstractAccess.php b/src/php/access/AbstractAccess.php index fb51207..d90ad7f 100644 --- a/src/php/access/AbstractAccess.php +++ b/src/php/access/AbstractAccess.php @@ -49,7 +49,10 @@ abstract class AbstractAccess implements IAccess { function ensureAssoc(array $keys, ?array $params=null): void { } - function ensureKeys(array $defaults, ?array $params=null): void { + function ensureKeys(array $defaults, ?array $missings, ?array $params=null): void { + } + + function deleteMissings(array $missings, ?array $params=null): void { } function ensureOrder(array $keys, ?array $params=null): void { diff --git a/src/php/access/ChainAccess.php b/src/php/access/ChainAccess.php index 3ed339e..6395d99 100644 --- a/src/php/access/ChainAccess.php +++ b/src/php/access/ChainAccess.php @@ -170,7 +170,7 @@ class ChainAccess extends AbstractAccess { #$this->access->ensureAssoc($keys, $params); } - function ensureKeys(array $defaults, ?array $params=null): void { + function ensureKeys(array $defaults, ?array $missings, ?array $params=null): void { #XXX fonction de $accessType? #$this->access->ensureKeys($defaults, $params); } diff --git a/src/php/access/IAccess.php b/src/php/access/IAccess.php index b81692b..71d8631 100644 --- a/src/php/access/IAccess.php +++ b/src/php/access/IAccess.php @@ -39,10 +39,16 @@ interface IAccess extends IGetter, ISetter, IDeleter { /** * s'assurer que toutes les clés mentionnées dans le tableau $defaults - * existent. si elles n'existent pas, leur donner la valeur du tableau - * $defaults + * existent. si elles n'existent pas, ou si elles ont la valeur correspondante + * du tableau $missings, leur donner la valeur du tableau $defaults */ - function ensureKeys(array $defaults, ?array $params=null): void; + function ensureKeys(array $defaults, ?array $missings, ?array $params=null): void; + + /** + * supprimer toutes les clés dont la valeur est celle mentionnée dans le + * tableau $missings + */ + function deleteMissings(array $missings, ?array $params=null): void; /** * s'assure que les clés de la destination sont dans l'ordre mentionné dans le diff --git a/src/php/access/KeyAccess.php b/src/php/access/KeyAccess.php index 47a8291..fbc9781 100644 --- a/src/php/access/KeyAccess.php +++ b/src/php/access/KeyAccess.php @@ -142,15 +142,30 @@ class KeyAccess extends AbstractAccess { } } - function ensureKeys(array $defaults, ?array $params=null): void { + function ensureKeys(array $defaults, ?array $missings, ?array $params=null): void { $dest =& $this->dest; $keys = array_keys($defaults); $prefix = $params["key_prefix"] ?? null; $suffix = $params["key_suffix"] ?? null; foreach ($keys as $key) { $destKey = "$prefix$key$suffix"; + $haveMissing = $missings !== null && array_key_exists($key, $missings); if ($dest === null || !array_key_exists($destKey, $dest)) { $dest[$destKey] = $defaults[$key]; + } elseif ($haveMissing && $dest[$destKey] === $missings[$key]) { + $dest[$destKey] = $defaults[$key]; + } + } + } + + function deleteMissings(array $missings, ?array $params=null): void { + $dest =& $this->dest; + $prefix = $params["key_prefix"] ?? null; + $suffix = $params["key_suffix"] ?? null; + foreach ($missings as $key => $missing) { + $destKey = "$prefix$key$suffix"; + if (array_key_exists($destKey, $dest) && $dest[$destKey] === $missing) { + unset($dest[$destKey]); } } } diff --git a/src/php/access/PropertyAccess.php b/src/php/access/PropertyAccess.php index c91529b..b0bc011 100644 --- a/src/php/access/PropertyAccess.php +++ b/src/php/access/PropertyAccess.php @@ -144,7 +144,7 @@ class PropertyAccess extends AbstractAccess { return new ChainAccess($this, $key); } - function ensureKeys(array $defaults, ?array $params=null): void { + function ensureKeys(array $defaults, ?array $missings, ?array $params=null): void { $dest = $this->dest; if ($dest === null) { # comme ne connait pas la classe de l'objet destination, on n'essaie pas diff --git a/src/php/access/ShadowAccess.php b/src/php/access/ShadowAccess.php index 639b311..accb960 100644 --- a/src/php/access/ShadowAccess.php +++ b/src/php/access/ShadowAccess.php @@ -62,8 +62,8 @@ class ShadowAccess extends AbstractAccess { $this->writer->ensureAssoc($keys, $params); } - function ensureKeys(array $defaults, ?array $params=null): void { - $this->writer->ensureKeys($defaults, $params); + function ensureKeys(array $defaults, ?array $missings, ?array $params=null): void { + $this->writer->ensureKeys($defaults, $missings, $params); } function ensureOrder(array $keys, ?array $params=null): void { diff --git a/src/schema/Wrapper.php b/src/schema/Wrapper.php index 425e0f6..12049c0 100644 --- a/src/schema/Wrapper.php +++ b/src/schema/Wrapper.php @@ -149,6 +149,11 @@ abstract class Wrapper implements ArrayAccess, IteratorAggregate { return $this->getResult($key)->available; } + /** retourner true si la valeur est nulle */ + function isNull($key=false): bool { + return $this->getResult($key)->null; + } + /** retourner true si la valeur est valide */ function isValid($key=false): bool { return $this->getResult($key)->valid; diff --git a/src/schema/_assoc/AssocWrapper.php b/src/schema/_assoc/AssocWrapper.php index 9d33c7f..2b77804 100644 --- a/src/schema/_assoc/AssocWrapper.php +++ b/src/schema/_assoc/AssocWrapper.php @@ -102,16 +102,39 @@ class AssocWrapper extends Wrapper { $result = $context->result; if (!$result->valid) return $what; + $schema = $context->schema; + $keys = $schema->getKeys(); + $defaults = []; + $missings = null; + foreach ($keys as $key) { + $type = $wrapper->getType($key); + $default = $schema->getSchema($key)->default; + if ($default === null) $default = $type->getNullValue(); + $defaults[$key] = $default; + $missing = $type->getMissingValue($valid); + if ($valid) $missings[$key] = $missing; + } + foreach ($context->keyWrappers as $keyWrapper) { $keyWrapper->analyze($params); - if (!$keyWrapper->isValid()) { - #XXX distinguer MISSING, UNAVAILABLE, NULL et !VALID - $what = ref_analyze::INVALID; + if ($keyWrapper->isValid()) continue; + $what = ref_analyze::INVALID; + if (!$keyWrapper->isPresent()) { + $result->addMissingMessage($keyWrapper); + } elseif (!$keyWrapper->isAvailable()) { + $result->addUnavailableMessage($keyWrapper); + } elseif ($keyWrapper->isNull()) { + $result->addNullMessage($keyWrapper); + } else { $result->addInvalidMessage($keyWrapper); } } + if ($params["ensure_keys"] ?? $context->ensureKeys) { + $context->input->ensureKeys($defaults, $missings, $params); + } else { + $context->input->deleteMissings($missings, $params); + } - #XXX supprimer les clés "missing" ou "unavailable" sauf si $ensureKeys return $what; } @@ -120,23 +143,11 @@ class AssocWrapper extends Wrapper { * @param AssocWrapper $wrapper */ static function _normalize(WrapperContext $context, Wrapper $wrapper, ?array $params): bool { - $ensureKeys = $params["ensure_keys"] ?? $context->ensureKeys; $ensureOrder = $params["ensure_order"] ?? $context->ensureOrder; - if ($ensureKeys || $ensureOrder) { + if ($ensureOrder) { $schema = $context->schema; $keys = $schema->getKeys(); - if ($ensureKeys) { - $defaults = []; - foreach ($keys as $key) { - $default = $schema->getSchema($key)->default; - if ($default === null) { - $default = $wrapper->getType($key)->getNullValue(); - } - $defaults[$key] = $default; - } - } - if ($ensureKeys) $context->input->ensureKeys($defaults, $params); - if ($ensureOrder) $context->input->ensureOrder($keys, $params); + $context->input->ensureOrder($keys, $params); } $modified = ScalarWrapper::_normalize($context, $wrapper, $params); diff --git a/src/schema/_scalar/ScalarResult.php b/src/schema/_scalar/ScalarResult.php index c442bdb..b538444 100644 --- a/src/schema/_scalar/ScalarResult.php +++ b/src/schema/_scalar/ScalarResult.php @@ -67,6 +67,29 @@ class ScalarResult extends Result { } } + function addMissingMessage(Wrapper $wrapper): void { + $this->resultAvailable = true; + $this->present = false; + $this->available = false; + $this->null = false; + $this->valid = false; + $this->messageKey = "missing"; + $result = $wrapper->getResult(); + $resultException = $result->exception; + $resultMessage = $result->message; + if ($resultException !== null) { + $tmessage = ValueException::get_message($resultException); + if ($tmessage) { + if ($resultMessage !== null) $resultMessage .= ": "; + $resultMessage .= $tmessage; + } + } + $message = $this->message; + if ($message) $message .= "\n"; + $message .= $resultMessage; + $this->message = $message; + } + function setUnavailable( Schema $schema): int { $this->resultAvailable = true; $this->present = true; @@ -83,6 +106,29 @@ class ScalarResult extends Result { } } + function addUnavailableMessage(Wrapper $wrapper): void { + $this->resultAvailable = true; + $this->present = true; + $this->available = false; + $this->null = false; + $this->valid = false; + $this->messageKey = "unavailable"; + $result = $wrapper->getResult(); + $resultException = $result->exception; + $resultMessage = $result->message; + if ($resultException !== null) { + $tmessage = ValueException::get_message($resultException); + if ($tmessage) { + if ($resultMessage !== null) $resultMessage .= ": "; + $resultMessage .= $tmessage; + } + } + $message = $this->message; + if ($message) $message .= "\n"; + $message .= $resultMessage; + $this->message = $message; + } + function setNull( Schema $schema): int { $this->resultAvailable = true; $this->present = true; @@ -99,6 +145,29 @@ class ScalarResult extends Result { } } + function addNullMessage(Wrapper $wrapper): void { + $this->resultAvailable = true; + $this->present = true; + $this->available = true; + $this->null = true; + $this->valid = false; + $this->messageKey = "null"; + $result = $wrapper->getResult(); + $resultException = $result->exception; + $resultMessage = $result->message; + if ($resultException !== null) { + $tmessage = ValueException::get_message($resultException); + if ($tmessage) { + if ($resultMessage !== null) $resultMessage .= ": "; + $resultMessage .= $tmessage; + } + } + $message = $this->message; + if ($message) $message .= "\n"; + $message .= $resultMessage; + $this->message = $message; + } + function setInvalid($value, Schema $schema, ?Throwable $exception=null): int { $this->resultAvailable = true; $this->present = true; diff --git a/src/schema/_scalar/ScalarWrapper.php b/src/schema/_scalar/ScalarWrapper.php index 1f43f4b..72f1e40 100644 --- a/src/schema/_scalar/ScalarWrapper.php +++ b/src/schema/_scalar/ScalarWrapper.php @@ -124,6 +124,16 @@ class ScalarWrapper extends Wrapper { } $value = $input->get($valueKey); + $missing = $type->getMissingValue($haveMissing); + if ($haveMissing && $value === $missing) { + if ($default !== null) { + $input->set($default, $valueKey); + return $result->setNormalized(); + } else { + return $result->setMissing($schema); + } + } + $context->origValue = $context->value = $value; if ($type->isNull($value)) { return $result->setNull($schema); diff --git a/src/schema/input/Input.php b/src/schema/input/Input.php index 59eefaa..4935bd6 100644 --- a/src/schema/input/Input.php +++ b/src/schema/input/Input.php @@ -76,8 +76,12 @@ class Input { $this->access->ensureAssoc($keys, $params); } - function ensureKeys(array $defaults, ?array $params=null): void { - $this->access->ensureKeys($defaults, $params); + function ensureKeys(array $defaults, ?array $missings, ?array $params=null): void { + $this->access->ensureKeys($defaults, $missings, $params); + } + + function deleteMissings(array $missings, ?array $params=null): void { + $this->access->deleteMissings($missings, $params); } function ensureOrder(array $keys, ?array $params=null): void { diff --git a/src/schema/types/IType.php b/src/schema/types/IType.php index e94fb80..c5fd8e3 100644 --- a/src/schema/types/IType.php +++ b/src/schema/types/IType.php @@ -47,30 +47,26 @@ interface IType { */ function getPhpType(bool $allowNullable=true): ?string; + /** + * obtenir la valeur "inexistante" pour les objets de ce type + * + * si $valid reçoit la valeur false, il faut ignorer la valeur de retour: + * cela veut dire qu'il n'y a pas de valeur "inexistant" pour les valeurs de + * ce type + */ + function getMissingValue(?bool &$valid=null); + /** obtenir la valeur "nulle" pour les objets de ce type */ function getNullValue(); /** - * indiquer si c'est le type d'une valeur qui ne peut prendre que 2 états: une - * "vraie" et une "fausse" + * si c'est le type d'une valeur qui ne prendre qu'une liste prédéterminée + * d'états spécifiques, retourner le nombre d'états possibles, et mettre à + * jour $states avec les valeurs possibles + * + * sinon, retourner 0 et ne pas mettre $states à jour */ - function is2States(): bool; - - /** - * Si {@link is2States()} est vrai, retourner les deux valeurs [faux, vrai] - */ - function get2States(): array; - - /** - * indiquer si c'est le type d'une valeur qui ne peut prendre que 3 états: une - * "vraie", une "fausse", et une "indéterminée" - */ - function is3States(): bool; - - /** - * Si {@link is3States()} est vrai, retourner les 3 valeurs [faux, vrai, undef] - */ - function get3States(): array; + function getNbStates(?array &$states=null): int; /** la donnée $input($valueKey) est-elle disponible? */ function isAvailable(Input $input, $valueKey): bool; diff --git a/src/schema/types/_tsimple.php b/src/schema/types/_tsimple.php index 94ab013..fd8174a 100644 --- a/src/schema/types/_tsimple.php +++ b/src/schema/types/_tsimple.php @@ -43,20 +43,13 @@ abstract class _tsimple implements IType { return $phpType; } - function is2States(): bool { + function getMissingValue(?bool &$valid=null) { + $valid = true; return false; } - function get2States(): array { - throw StateException::not_implemented(); - } - - function is3States(): bool { - return false; - } - - function get3States(): array { - throw StateException::not_implemented(); + function getNbStates(?array &$states=null): int { + return 0; } function isAvailable(Input $input, $valueKey): bool { diff --git a/src/schema/types/tbool.php b/src/schema/types/tbool.php index 1d62eef..a9b7854 100644 --- a/src/schema/types/tbool.php +++ b/src/schema/types/tbool.php @@ -60,26 +60,25 @@ class tbool extends _tformatable { return "bool"; } - function is2States(): bool { - return !$this->nullable; - } - - function get2States(): array { - return [false, true]; - } - - function is3States(): bool { - return $this->nullable; - } - - function get3States(): array { - return [false, true, null]; + function getMissingValue(?bool &$valid=null) { + $valid = !$this->nullable; + return null; } function getNullValue() { return $this->nullable? null: false; } + public function getNbStates(?array &$states=null): int { + if ($this->nullable) { + $states = [false, true, null]; + return 3; + } else { + $states = [false, true]; + return 2; + } + } + function isAvailable(Input $input, $valueKey): bool { return $input->isAvailable($valueKey); } diff --git a/src/schema/types/tgeneric.php b/src/schema/types/tgeneric.php index 9d948a6..498a16b 100644 --- a/src/schema/types/tgeneric.php +++ b/src/schema/types/tgeneric.php @@ -24,10 +24,6 @@ class tgeneric extends _tsimple { return null; } - function isAvailable(Input $input, $valueKey): bool { - return $input->isAvailable($valueKey); - } - public function isNull($value): bool { return $value === null; } diff --git a/src/schema/types/tmixed.php b/src/schema/types/tmixed.php index 045cebf..3d64620 100644 --- a/src/schema/types/tmixed.php +++ b/src/schema/types/tmixed.php @@ -14,6 +14,11 @@ class tmixed extends _tsimple { return "mixed"; } + function getMissingValue(?bool &$valid=null) { + $valid = false; + return null; + } + function getNullValue() { return null; } diff --git a/tests/php/access/KeyAccessTest.php b/tests/php/access/KeyAccessTest.php index b654050..6c17fef 100644 --- a/tests/php/access/KeyAccessTest.php +++ b/tests/php/access/KeyAccessTest.php @@ -152,7 +152,7 @@ class KeyAccessTest extends TestCase { private function _ensureKeys(?array $orig, ?array $expected, array $defaults, ?array $params=null) { $v = $orig; $a = new KeyAccess($v); - $a->ensureKeys($defaults, $params); + $a->ensureKeys($defaults, $missings, $params); self::assertSame($expected, $v); } function testEnsureKeys() { @@ -187,7 +187,7 @@ class KeyAccessTest extends TestCase { $v = $orig; $a = new KeyAccess($v); $keys = array_keys($defaults); $a->ensureAssoc($keys, $params); - $a->ensureKeys($defaults, $params); + $a->ensureKeys($defaults, $missings, $params); $a->ensureOrder($keys, $params); self::assertSame($expected, $v); } diff --git a/tests/schema/_assoc/AssocSchemaTest.php b/tests/schema/_assoc/AssocSchemaTest.php index c8e6c0d..d3314e1 100644 --- a/tests/schema/_assoc/AssocSchemaTest.php +++ b/tests/schema/_assoc/AssocSchemaTest.php @@ -50,54 +50,54 @@ class AssocSchemaTest extends TestCase { self::assertSame(self::schema([ "type" => ["array"], "nullable" => true, ], [ - "a" => [ + "s" => [ "type" => ["string"], "nullable" => false, - "name" => "a", "pkey" => "a", "header" => "a", + "name" => "s", "pkey" => "s", "header" => "s", ], - ]), AssocSchema::normalize_definition(["a" => "string"])); + ]), AssocSchema::normalize_definition(["s" => "string"])); self::assertSame(self::schema([ "type" => ["array"], "nullable" => true, ], [ - "a" => [ + "s" => [ "type" => ["string"], "nullable" => false, - "name" => "a", "pkey" => "a", "header" => "a", + "name" => "s", "pkey" => "s", "header" => "s", + ], + "i" => [ + "type" => ["int"], "nullable" => false, + "name" => "i", "pkey" => "i", "header" => "i", ], "b" => [ - "type" => ["int"], "nullable" => false, + "type" => ["bool"], "nullable" => false, "name" => "b", "pkey" => "b", "header" => "b", ], - "c" => [ - "type" => ["bool"], "nullable" => false, - "name" => "c", "pkey" => "c", "header" => "c", - ], ]), AssocSchema::normalize_definition([ - "a" => "string", - "b" => "int", - "c" => "bool", + "s" => "string", + "i" => "int", + "b" => "bool", ])); } function testConstructor() { $schema = new AssocSchema([ - "a" => "string", - "b" => "int", - "c" => "bool", + "s" => "string", + "i" => "int", + "b" => "bool", ]); self::assertSame(self::schema([ "type" => ["array"], "nullable" => true, ], [ - "a" => [ + "s" => [ "type" => ["string"], "nullable" => false, - "name" => "a", "pkey" => "a", "header" => "a", + "name" => "s", "pkey" => "s", "header" => "s", + ], + "i" => [ + "type" => ["int"], "nullable" => false, + "name" => "i", "pkey" => "i", "header" => "i", ], "b" => [ - "type" => ["int"], "nullable" => false, - "name" => "b", "pkey" => "b", "header" => "b", - ], - "c" => [ "type" => ["bool"], "nullable" => false, - "name" => "c", "pkey" => "c", "header" => "c", + "name" => "b", "pkey" => "b", "header" => "b", ], ]), $schema->getDefinition()); //yaml::dump($schema->getDefinition()); @@ -105,69 +105,82 @@ class AssocSchemaTest extends TestCase { function testWrapper() { $schema = new AssocSchema([ - "a" => "?string", - "b" => "?int", - "c" => "?bool", + "s" => "?string", + "i" => "?int", + "b" => "?bool", ]); - $array = ["a" => " string ", "b" => " 42 ", "c" => false]; + $array = ["s" => " string ", "i" => " 42 ", "b" => false]; $schema->getWrapper($array); self::assertSame([ - "a" => "string", - "b" => 42, - "c" => false, + "s" => "string", + "i" => 42, + "b" => false, ], $array); ########################################################################### $schema = new AssocSchema([ - "a" => "string", - "b" => "int", - "c" => "bool", + "s" => "string", + "i" => "int", + "b" => "bool", ]); - $array = ["a" => " string "]; + $array = ["s" => " string "]; $schema->getWrapper($array); self::assertSame([ - "a" => "string", - "b" => 0, - "c" => false, + "s" => "string", + "i" => 0, + "b" => false, ], $array); - $array = ["c" => false, "a" => " string "]; + $array = ["b" => false, "s" => " string "]; $schema->getWrapper($array); self::assertSame([ - "a" => "string", - "b" => 0, - "c" => false, + "s" => "string", + "i" => 0, + "b" => false, ], $array); - $array = ["a" => " string "]; + $array = ["s" => " string "]; $schema->getWrapper($array, null, ["ensure_order" => false]); self::assertSame([ - "a" => "string", - "b" => 0, - "c" => false, + "s" => "string", + "i" => 0, + "b" => false, ], $array); - $array = ["c" => false, "a" => " string "]; + $array = ["b" => false, "s" => " string "]; $schema->getWrapper($array, null, ["ensure_order" => false]); self::assertSame([ - "c" => false, - "a" => "string", - "b" => 0, + "b" => false, + "s" => "string", + "i" => 0, ], $array); - $array = ["a" => " string "]; + $array = ["s" => " string "]; $schema->getWrapper($array, null, ["ensure_keys" => false]); self::assertSame([ - "a" => "string", + "s" => "string", ], $array); - $array = ["c" => false, "a" => " string "]; + $array = ["b" => false, "s" => " string "]; $schema->getWrapper($array, null, ["ensure_keys" => false]); self::assertSame([ - "a" => "string", - "c" => false, + "s" => "string", + "b" => false, ], $array); + + // false équivaut à absent + $array = ["s" => false, "i" => false, "b" => null]; + $schema->getWrapper($array, null, ["ensure_keys" => true]); + self::assertSame([ + "s" => "", + "i" => 0, + "b" => false, + ], $array); + + $array = ["s" => false, "i" => false, "b" => null]; + $schema->getWrapper($array, null, ["ensure_keys" => false]); + self::assertSame([], $array); } const STRING_SCHEMA = [ @@ -203,7 +216,7 @@ class AssocSchemaTest extends TestCase { $array = self::STRINGS; $wrapper = $schema->getWrapper($array, null, ["throw" => false]); - self::assertSame(["s" => "string", "f" => false, "m" => ""], $array); + self::assertSame(["s" => "string", "f" => "", "m" => ""], $array); $result = $wrapper->getResult("s"); self::assertTrue($result->normalized); $result = $wrapper->getResult("f"); @@ -241,7 +254,7 @@ class AssocSchemaTest extends TestCase { $array = self::STRINGS; $wrapper = $schema->getWrapper($array, null, ["throw" => false]); - self::assertSame(["s" => "string", "f" => false, "m" => null], $array); + self::assertSame(["s" => "string", "f" => null, "m" => null], $array); $result = $wrapper->getResult("s"); self::assertTrue($result->normalized); $result = $wrapper->getResult("f"); @@ -277,7 +290,7 @@ class AssocSchemaTest extends TestCase { $array = self::STRINGS; $wrapper = $schema->getWrapper($array, null, ["throw" => false]); - self::assertSame(["s" => "string", "f" => false, "m" => ""], $array); + self::assertSame(["s" => "string", "f" => "", "m" => ""], $array); $result = $wrapper->getResult("s"); self::assertTrue($result->normalized); $result = $wrapper->getResult("f"); @@ -318,7 +331,7 @@ class AssocSchemaTest extends TestCase { $array = self::STRINGS; $wrapper = $schema->getWrapper($array, null, ["throw" => false]); - self::assertSame(["s" => "string", "f" => false, "m" => null], $array); + self::assertSame(["s" => "string", "f" => null, "m" => null], $array); $result = $wrapper->getResult("s"); self::assertTrue($result->normalized); $result = $wrapper->getResult("f");