[pve-devel] [PATCH v3 common 1/3] JSONSchema: add format validator support and cleanup check_format

Stefan Reiter s.reiter at proxmox.com
Thu Jun 25 13:35:39 CEST 2020


Adds a third, optional parameter to register_format that allows specifying
a function that will be called after parsing and can validate the parsed
data. A validator should die on failed validation, and can also change the
parsed object by returning a modified version of it.

This is useful so one can register a format with its hash, thus allowing
documentation to be generated automatically, while still enforcing certain
validation rules.

The validator only needs to be called in parse_property_string, since
check_format always calls parse_property_string if there is a
possibility of a validator existing at all. parse_property_string should
then be called with named formats for best effect, as only then can
validators be used.

Clean up 'check_format' as well (which pretty much amounts to a rewrite).
No existing functionality is intentionally changed.

Signed-off-by: Stefan Reiter <s.reiter at proxmox.com>
---
 src/PVE/JSONSchema.pm | 87 +++++++++++++++++++++++++++----------------
 1 file changed, 55 insertions(+), 32 deletions(-)

diff --git a/src/PVE/JSONSchema.pm b/src/PVE/JSONSchema.pm
index 84fb694..f987006 100644
--- a/src/PVE/JSONSchema.pm
+++ b/src/PVE/JSONSchema.pm
@@ -121,19 +121,26 @@ register_standard_option('pve-snapshot-name', {
 });
 
 my $format_list = {};
+my $format_validators = {};
 
 sub register_format {
-    my ($format, $code) = @_;
+    my ($name, $format, $validator) = @_;
 
-    die "JSON schema format '$format' already registered\n"
-	if $format_list->{$format};
+    die "JSON schema format '$name' already registered\n"
+	if $format_list->{$name};
 
-    $format_list->{$format} = $code;
+    if ($validator) {
+	die "A \$validator function can only be specified for hash-based formats\n"
+	    if ref($format) ne 'HASH';
+	$format_validators->{$name} = $validator;
+    }
+
+    $format_list->{$name} = $format;
 }
 
 sub get_format {
-    my ($format) = @_;
-    return $format_list->{$format};
+    my ($name) = @_;
+    return $format_list->{$name};
 }
 
 my $renderer_hash = {};
@@ -647,39 +654,47 @@ sub pve_verify_tfa_secret {
 sub check_format {
     my ($format, $value, $path) = @_;
 
-    return parse_property_string($format, $value, $path) if ref($format) eq 'HASH';
+    if (ref($format) eq 'HASH') {
+	# hash ref cannot have validator/list/opt handling attached
+	return parse_property_string($format, $value, $path);
+    }
+
+    if (ref($format) eq 'CODE') {
+	# we are the (sole, old-style) validator
+	return $format->($value);
+    }
+
     return if $format eq 'regex';
 
-    if ($format =~ m/^(.*)-a?list$/) {
+    my $parsed;
+    $format =~ m/^(.*?)(?:-a?(list|opt))?$/;
+    my ($format_name, $format_type) = ($1, $2 // 'none');
+    my $registered = get_format($format_name);
+    die "undefined format '$format'\n" if !$registered;
 
-	my $code = $format_list->{$1};
-
-	die "undefined format '$format'\n" if !$code;
+    die "'-$format_type' format must have code ref, not hash\n"
+	if $format_type ne 'none' && ref($registered) ne 'CODE';
 
+    if ($format_type eq 'list') {
 	# Note: we allow empty lists
 	foreach my $v (split_list($value)) {
-	    &$code($v);
+	    $parsed = $registered->($v);
 	}
-
-    } elsif ($format =~ m/^(.*)-opt$/) {
-
-	my $code = $format_list->{$1};
-
-	die "undefined format '$format'\n" if !$code;
-
-	return if !$value; # allow empty string
-
- 	&$code($value);
-
+    } elsif ($format_type eq 'opt') {
+	$parsed = $registered->($value) if $value;
    } else {
-
-	my $code = $format_list->{$format};
-
-	die "undefined format '$format'\n" if !$code;
-
-	return parse_property_string($code, $value, $path) if ref($code) eq 'HASH';
-	&$code($value);
+	if (ref($registered) eq 'HASH') {
+	    # Note: this is the only case where a validator function could be
+	    # attached, hence it's safe to handle that in parse_property_string.
+	    # We do however have to call it with $format_name instead of
+	    # $registered, so it knows about the name (and thus any validators).
+	    $parsed = parse_property_string($format, $value, $path);
+	} else {
+	    $parsed = $registered->($value);
+	}
     }
+
+    return $parsed;
 }
 
 sub parse_size {
@@ -735,9 +750,16 @@ sub parse_property_string {
     $additional_properties = 0 if !defined($additional_properties);
 
     # Support named formats here, too:
+    my $validator;
     if (!ref($format)) {
-	if (my $desc = $format_list->{$format}) {
-	    $format = $desc;
+	if (my $reg = get_format($format)) {
+	    die "parse_property_string only accepts hash based named formats\n"
+		if ref($reg) ne 'HASH';
+
+	    # named formats can have validators attached
+	    $validator = $format_validators->{$format};
+
+	    $format = $reg;
 	} else {
 	    die "unknown format: $format\n";
 	}
@@ -793,6 +815,7 @@ sub parse_property_string {
 	raise "format error\n", errors => $errors;
     }
 
+    return $validator->($res) if $validator;
     return $res;
 }
 
-- 
2.20.1





More information about the pve-devel mailing list