public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: Dominik Csapak <d.csapak@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [pve-devel] [PATCH common 3/4] section config: allow separated property lists for plugins
Date: Tue, 14 Nov 2023 11:33:38 +0100	[thread overview]
Message-ID: <20231114103340.2850162-4-d.csapak@proxmox.com> (raw)
In-Reply-To: <20231114103340.2850162-1-d.csapak@proxmox.com>

when using 'init(1)'. This saves the property lists per type instead of
a big one, and using create/updateSchema creates a new schema with the
options as 'oneOf' and/or 'instance-types' (depending if the schemas
match).

for that to work there are a few changes in how to use the 'options' hash:
* when wanting to use a property that is not defined in either the local
  list or the global one, the 'type' property has to be given to specify
  which plugin type this should be taken from
* if it's desired to always have some global properties in the schema,
  they have to be given in the 'global' options section now (the perl
  package where the base plugin is defined)

for now, we then save the global options in the section '__global',
which is now a forbidden plugin type, but that should be fine as we
don't use that

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
 src/PVE/SectionConfig.pm | 256 ++++++++++++++++++++++++++++++---------
 1 file changed, 198 insertions(+), 58 deletions(-)

diff --git a/src/PVE/SectionConfig.pm b/src/PVE/SectionConfig.pm
index 5690476..1e44c3d 100644
--- a/src/PVE/SectionConfig.pm
+++ b/src/PVE/SectionConfig.pm
@@ -30,6 +30,9 @@ sub register {
     die "duplicate plugin registration (type = $type)"
 	if defined($pdata->{plugins}->{$type});
 
+    die "invalid plugin type (type = '__global')"
+	if $type eq '__global';
+
     my $plugindata = $class->plugindata();
     $pdata->{plugindata}->{$type} = $plugindata;
     $pdata->{plugins}->{$type} = $class;
@@ -51,6 +54,67 @@ sub plugindata {
     return {};
 }
 
+my sub cmp_property {
+    my ($src, $tgt, $skip_opts) = @_;
+
+    my $merged = {$src->%*, $tgt->%*};
+    for my $opt ($skip_opts->@*) {
+	delete $merged->{$opt};
+    }
+    for my $opt (keys $merged->%*) {
+	return 0 if !defined($src->{$opt}) || !defined($tgt->{$opt}) || $tgt->{$opt} ne $src->{$opt};
+    }
+
+    return 1;
+}
+
+my sub copy_property {
+    my ($src) = @_;
+
+    my $res = {};
+    foreach my $k (keys %$src) {
+	$res->{$k} = $src->{$k};
+    }
+
+    return $res;
+}
+
+
+my sub add_property {
+    my ($props, $key, $prop, $type) = @_;
+
+    if (!defined($props->{$key})) {
+	$props->{$key} = $prop;
+	return;
+    }
+
+    if (!defined($props->{$key}->{oneOf})) {
+	if (cmp_property($props->{$key}, $prop, ['instance-types'])) {
+	    push $props->{$key}->{'instance-types'}->@*, $type;
+	} else {
+	    my $new_prop = delete $props->{$key};
+	    delete $new_prop->{'type-property'};
+	    delete $prop->{'type-property'};
+	    $props->{$key} = {
+		'type-property' => 'type',
+		oneOf => [
+		    $new_prop,
+		    $prop,
+		],
+	    };
+	}
+    } else {
+	for my $existing_prop ($props->{$key}->{oneOf}->@*) {
+	    if (cmp_property($existing_prop, $prop, ['instance-types', 'type-property'])) {
+		push $existing_prop->{'instance-types'}->@*, $type;
+		return;
+	    }
+	}
+
+	push $props->{$key}->{oneOf}->@*, $prop;
+    }
+}
+
 sub createSchema {
     my ($class, $skip_type, $base) = @_;
 
@@ -58,44 +122,55 @@ sub createSchema {
     my $propertyList = $pdata->{propertyList};
     my $plugins = $pdata->{plugins};
 
-    my $props = $base || {};
-
-    my $copy_property = sub {
-	my ($src) = @_;
+    my $global_options = $pdata->{options}->{__global};
+    my $propertyListSeparated = $pdata->{propertyListSeparated};
 
-	my $res = {};
-	foreach my $k (keys %$src) {
-	    $res->{$k} = $src->{$k};
-	}
+    my $props = $base || {};
 
-	return $res;
-    };
+    if (!defined($global_options)) {
+	foreach my $p (keys %$propertyList) {
+	    next if $skip_type && $p eq 'type';
 
-    foreach my $p (keys %$propertyList) {
-	next if $skip_type && $p eq 'type';
+	    if (!$propertyList->{$p}->{optional}) {
+		$props->{$p} = $propertyList->{$p};
+		next;
+	    }
 
-	if (!$propertyList->{$p}->{optional}) {
-	    $props->{$p} = $propertyList->{$p};
-	    next;
-	}
+	    my $required = 1;
 
-	my $required = 1;
+	    my $copts = $class->options();
+	    $required = 0 if defined($copts->{$p}) && $copts->{$p}->{optional};
 
-	my $copts = $class->options();
-	$required = 0 if defined($copts->{$p}) && $copts->{$p}->{optional};
+	    foreach my $t (keys %$plugins) {
+		my $opts = $pdata->{options}->{$t} || {};
+		$required = 0 if !defined($opts->{$p}) || $opts->{$p}->{optional};
+	    }
 
-	foreach my $t (keys %$plugins) {
-	    my $opts = $pdata->{options}->{$t} || {};
-	    $required = 0 if !defined($opts->{$p}) || $opts->{$p}->{optional};
+	    if ($required) {
+		# make a copy, because we modify the optional property
+		my $res = copy_property($propertyList->{$p});
+		$res->{optional} = 0;
+		$props->{$p} = $res;
+	    } else {
+		$props->{$p} = $propertyList->{$p};
+	    }
 	}
-
-	if ($required) {
-	    # make a copy, because we modify the optional property
-	    my $res = &$copy_property($propertyList->{$p});
-	    $res->{optional} = 0;
-	    $props->{$p} = $res;
-	} else {
-	    $props->{$p} = $propertyList->{$p};
+    } else {
+	for my $type (sort keys %$plugins) {
+	    my $opts = $pdata->{options}->{$type} || {};
+	    for my $key (sort keys $opts->%*) {
+		my $schema = $class->get_property_schema($type, $key);
+		my $prop = copy_property($schema);
+		$prop->{'instance-types'} = [$type];
+		$prop->{'type-property'} = 'type';
+		$prop->{optional} = 1 if $opts->{$key}->{optional};
+
+		add_property($props, $key, $prop, $type);
+	    }
+	}
+	for my $opt (keys $global_options->%*) {
+	    $props->{$opt} = copy_property($propertyList->{$opt});
+	    $props->{$opt}->{optional} = 1 if $global_options->{$opt}->{optional};
 	}
     }
 
@@ -113,34 +188,61 @@ sub updateSchema {
     my $propertyList = $pdata->{propertyList};
     my $plugins = $pdata->{plugins};
 
+    my $global_options = $pdata->{options}->{__global};
+    my $propertyListSeparated = $pdata->{propertyListSeparated};
+
     my $props = $base || {};
 
     my $filter_type = $single_class ? $class->type() : undef;
 
-    foreach my $p (keys %$propertyList) {
-	next if $p eq 'type';
+    if (!defined($global_options)) {
+	foreach my $p (keys %$propertyList) {
+	    next if $p eq 'type';
 
-	my $copts = $class->options();
+	    my $copts = $class->options();
 
-	next if defined($filter_type) && !defined($copts->{$p});
+	    next if defined($filter_type) && !defined($copts->{$p});
 
-	if (!$propertyList->{$p}->{optional}) {
-	    $props->{$p} = $propertyList->{$p};
-	    next;
-	}
+	    if (!$propertyList->{$p}->{optional}) {
+		$props->{$p} = $propertyList->{$p};
+		next;
+	    }
 
-	my $modifyable = 0;
+	    my $modifyable = 0;
 
-	$modifyable = 1 if defined($copts->{$p}) && !$copts->{$p}->{fixed};
+	    $modifyable = 1 if defined($copts->{$p}) && !$copts->{$p}->{fixed};
+
+	    foreach my $t (keys %$plugins) {
+		my $opts = $pdata->{options}->{$t} || {};
+		next if !defined($opts->{$p});
+		$modifyable = 1 if !$opts->{$p}->{fixed};
+	    }
+	    next if !$modifyable;
 
-	foreach my $t (keys %$plugins) {
-	    my $opts = $pdata->{options}->{$t} || {};
-	    next if !defined($opts->{$p});
-	    $modifyable = 1 if !$opts->{$p}->{fixed};
+	    $props->{$p} = $propertyList->{$p};
+	}
+    } else {
+	for my $type (sort keys %$plugins) {
+	    my $opts = $pdata->{options}->{$type} || {};
+	    for my $key (sort keys $opts->%*) {
+		next if $opts->{$key}->{fixed};
+
+		my $schema = $class->get_property_schema($type, $key);
+		my $prop = copy_property($schema);
+		$prop->{'instance-types'} = [$type];
+		$prop->{'type-property'} = 'type';
+		$prop->{optional} = 1;
+
+		add_property($props, $key, $prop, $type);
+	    }
 	}
-	next if !$modifyable;
 
-	$props->{$p} = $propertyList->{$p};
+	for my $opt (keys $global_options->%*) {
+	    next if $global_options->{$opt}->{fixed};
+
+	    $props->{$opt} = copy_property($propertyList->{$opt});
+	    $props->{$opt}->{optional} = 1 if $global_options->{$opt}->{optional};
+	}
     }
 
     $props->{digest} = get_standard_option('pve-config-digest');
@@ -160,22 +262,33 @@ sub updateSchema {
 }
 
 sub init {
-    my ($class) = @_;
+    my ($class, $separate_properties) = @_;
 
     my $pdata = $class->private();
 
-    foreach my $k (qw(options plugins plugindata propertyList)) {
+    foreach my $k (qw(options plugins plugindata propertyList propertyListSeparated)) {
 	$pdata->{$k} = {} if !$pdata->{$k};
     }
 
+    if ($separate_properties) {
+	$pdata->{options}->{__global} = delete $pdata->{options};
+    }
+
     my $plugins = $pdata->{plugins};
     my $propertyList = $pdata->{propertyList};
+    my $propertyListSeparated = $pdata->{propertyListSeparated};
 
     foreach my $type (keys %$plugins) {
 	my $props = $plugins->{$type}->properties();
 	foreach my $p (keys %$props) {
-	    die "duplicate property '$p'" if defined($propertyList->{$p});
-	    my $res = $propertyList->{$p} = {};
+	    my $res;
+	    if ($separate_properties) {
+		die "duplicate property '$p'" if defined($propertyListSeparated->{$type}->{$p});
+		$res = $propertyListSeparated->{$type}->{$p} = {};
+	    } else {
+		die "duplicate property '$p'" if defined($propertyList->{$p});
+		$res = $propertyList->{$p} = {};
+	    }
 	    my $data = $props->{$p};
 	    for my $a (keys %$data) {
 		$res->{$a} = $data->{$a};
@@ -187,7 +300,13 @@ sub init {
     foreach my $type (keys %$plugins) {
 	my $opts = $plugins->{$type}->options();
 	foreach my $p (keys %$opts) {
-	    die "undefined property '$p'" if !$propertyList->{$p};
+	    my $prop;
+	    if ($separate_properties) {
+		my $opt_type = $opts->{$p}->{type} // $type;
+		$prop = $propertyListSeparated->{$opt_type}->{$p};
+	    }
+	    $prop //= $propertyList->{$p};
+	    die "undefined property '$p'" if !$prop;
 	}
 	$pdata->{options}->{$type} = $opts;
     }
@@ -237,11 +356,13 @@ sub check_value {
     return $value if $key eq 'type' && $type eq $value;
 
     my $opts = $pdata->{options}->{$type};
+    my $global_opts = $pdata->{options}->{__global};
+
     die "unknown section type '$type'\n" if !$opts;
 
-    die "unexpected property '$key'\n" if !defined($opts->{$key});
+    die "unexpected property '$key'\n" if !defined($opts->{$key}) && !defined($global_opts->{$key});
 
-    my $schema = $pdata->{propertyList}->{$key};
+    my $schema = $class->get_property_schema($type, $key);
     die "unknown property type\n" if !$schema;
 
     my $ct = $schema->{type};
@@ -295,6 +416,23 @@ sub format_section_header {
     return "$type: $sectionId\n";
 }
 
+sub get_property_schema {
+    my ($class, $type, $key) = @_;
+
+    my $pdata = $class->private();
+    my $opts = $pdata->{options}->{$type};
+
+    my $separated = defined($pdata->{options}->{__global});
+
+    my $schema;
+    if ($separated) {
+	my $opt_type = $opts->{$key}->{type} // $type;
+	$schema = $pdata->{propertyListSeparated}->{$opt_type}->{$key};
+    }
+    $schema //= $pdata->{propertyList}->{$key};
+
+    return $schema;
+}
 
 sub parse_config {
     my ($class, $filename, $raw, $allow_unknown) = @_;
@@ -322,7 +460,7 @@ sub parse_config {
     my $is_array = sub {
 	my ($type, $key) = @_;
 
-	my $schema = $pdata->{propertyList}->{$key};
+	my $schema = $class->get_property_schema($type, $key);
 	die "unknown property type\n" if !$schema;
 
 	return $schema->{type} eq 'array';
@@ -494,7 +632,6 @@ sub write_config {
     my ($class, $filename, $cfg, $allow_unknown) = @_;
 
     my $pdata = $class->private();
-    my $propertyList = $pdata->{propertyList};
 
     my $out = '';
 
@@ -545,7 +682,8 @@ sub write_config {
 	if ($scfg->{comment} && !$done_hash->{comment}) {
 	    my $k = 'comment';
 	    my $v = $class->encode_value($type, $k, $scfg->{$k});
-	    $data .= &$format_config_line($propertyList->{$k}, $k, $v);
+	    my $prop = $class->get_property_schema($type, $k);
+	    $data .= &$format_config_line($prop, $k, $v);
 	}
 
 	$data .= "\tdisable\n" if $scfg->{disable} && !$done_hash->{disable};
@@ -562,7 +700,8 @@ sub write_config {
 	    die "section '$sectionId' - missing value for required option '$k'\n"
 		if !defined ($v);
 	    $v = $class->encode_value($type, $k, $v);
-	    $data .= &$format_config_line($propertyList->{$k}, $k, $v);
+	    my $prop = $class->get_property_schema($type, $k);
+	    $data .= &$format_config_line($prop, $k, $v);
 	}
 
 	foreach my $k (@option_keys) {
@@ -570,7 +709,8 @@ sub write_config {
 	    my $v = $scfg->{$k};
 	    next if !defined($v);
 	    $v = $class->encode_value($type, $k, $v);
-	    $data .= &$format_config_line($propertyList->{$k}, $k, $v);
+	    my $prop = $class->get_property_schema($type, $k);
+	    $data .= &$format_config_line($prop, $k, $v);
 	}
 
 	$out .= "$data\n";
-- 
2.30.2





  parent reply	other threads:[~2023-11-14 10:34 UTC|newest]

Thread overview: 9+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2023-11-14 10:33 [pve-devel] [PATCH common/widget-toolkit] implement oneOf schema Dominik Csapak
2023-11-14 10:33 ` [pve-devel] [PATCH common 1/4] section config: add test for the schemas Dominik Csapak
2023-11-14 10:33 ` [pve-devel] [PATCH common 2/4] json schema: implement 'oneOf' schema Dominik Csapak
2023-11-14 13:44   ` Wolfgang Bumiller
2023-11-14 10:33 ` Dominik Csapak [this message]
2023-11-15  9:36   ` [pve-devel] [PATCH common 3/4] section config: allow separated property lists for plugins Wolfgang Bumiller
2023-11-14 10:33 ` [pve-devel] [PATCH common 4/4] section config: add tests for separated property lists Dominik Csapak
2023-11-15  9:44   ` Wolfgang Bumiller
2023-11-14 10:33 ` [pve-devel] [PATCH widget-toolkit 1/1] api-viewer: implement basic oneOf support Dominik Csapak

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20231114103340.2850162-4-d.csapak@proxmox.com \
    --to=d.csapak@proxmox.com \
    --cc=pve-devel@lists.proxmox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal