]>
git.proxmox.com Git - pve-common.git/blob - src/PVE/JSONSchema.pm
a144d5516b2ab698679a01c378a0b42059c79d57
1 package PVE
:: JSONSchema
;
5 use Storable
; # for dclone
9 use Devel
:: Cycle
- quiet
; # todo: remove?
10 use PVE
:: Tools
qw(split_list $IPV6RE $IPV4RE ) ;
11 use PVE
:: Exception
qw(raise) ;
12 use HTTP
:: Status
qw(:constants) ;
13 use Net
:: IP
qw(:PROC) ;
19 register_standard_option
23 # Note: This class implements something similar to JSON schema, but it is not 100% complete.
24 # see: http://tools.ietf.org/html/draft-zyp-json-schema-02
25 # see: http://json-schema.org/
27 # the code is similar to the javascript parser from http://code.google.com/p/jsonschema/
29 my $standard_options = {};
30 sub register_standard_option
{
31 my ( $name, $schema ) = @_ ;
33 die "standard option ' $name ' already registered \n "
34 if $standard_options ->{ $name };
36 $standard_options ->{ $name } = $schema ;
39 sub get_standard_option
{
40 my ( $name, $base ) = @_ ;
42 my $std = $standard_options ->{ $name };
43 die "no such standard option ' $name ' \n " if ! $std ;
45 my $res = $base || {};
47 foreach my $opt ( keys %$std ) {
48 next if defined ( $res ->{ $opt });
49 $res ->{ $opt } = $std ->{ $opt };
55 register_standard_option
( 'pve-vmid' , {
56 description
=> "The (unique) ID of the VM." ,
57 type
=> 'integer' , format
=> 'pve-vmid' ,
61 register_standard_option
( 'pve-node' , {
62 description
=> "The cluster node name." ,
63 type
=> 'string' , format
=> 'pve-node' ,
66 register_standard_option
( 'pve-node-list' , {
67 description
=> "List of cluster node names." ,
68 type
=> 'string' , format
=> 'pve-node-list' ,
71 register_standard_option
( 'pve-iface' , {
72 description
=> "Network interface name." ,
73 type
=> 'string' , format
=> 'pve-iface' ,
74 minLength
=> 2 , maxLength
=> 20 ,
77 register_standard_option
( 'pve-storage-id' , {
78 description
=> "The storage identifier." ,
79 type
=> 'string' , format
=> 'pve-storage-id' ,
82 register_standard_option
( 'pve-config-digest' , {
83 description
=> 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.' ,
86 maxLength
=> 40 , # sha1 hex digest length is 40
89 register_standard_option
( 'skiplock' , {
90 description
=> "Ignore locks - only root is allowed to use this option." ,
95 register_standard_option
( 'extra-args' , {
96 description
=> "Extra arguments as array" ,
98 items
=> { type
=> 'string' },
102 register_standard_option
( 'fingerprint-sha256' , {
103 description
=> "Certificate SHA 256 fingerprint." ,
105 pattern
=> '([A-Fa-f0-9]{2}:){31}[A-Fa-f0-9]{2}' ,
108 register_standard_option
( 'pve-output-format' , {
110 description
=> 'Output format.' ,
111 enum
=> [ 'text' , 'json' , 'json-pretty' , 'yaml' ],
116 register_standard_option
( 'pve-snapshot-name' , {
117 description
=> "The name of the snapshot." ,
118 type
=> 'string' , format
=> 'pve-configid' ,
122 my $format_list = {};
124 sub register_format
{
125 my ( $format, $code ) = @_ ;
127 die "JSON schema format ' $format ' already registered \n "
128 if $format_list ->{ $format };
130 $format_list ->{ $format } = $code ;
135 return $format_list ->{ $format };
138 my $renderer_hash = {};
140 sub register_renderer
{
141 my ( $name, $code ) = @_ ;
143 die "renderer ' $name ' already registered \n "
144 if $renderer_hash ->{ $name };
146 $renderer_hash ->{ $name } = $code ;
151 return $renderer_hash ->{ $name };
154 # register some common type for pve
156 register_format
( 'string' , sub {}); # allow format => 'string-list'
158 register_format
( 'urlencoded' , \
& pve_verify_urlencoded
);
159 sub pve_verify_urlencoded
{
160 my ( $text, $noerr ) = @_ ;
161 if ( $text !~ /^[-%a-zA-Z0-9_.!~*'()]*$/ ) {
162 return undef if $noerr ;
163 die "invalid urlencoded string: $text\n " ;
168 register_format
( 'pve-configid' , \
& pve_verify_configid
);
169 sub pve_verify_configid
{
170 my ( $id, $noerr ) = @_ ;
172 if ( $id !~ m/^[a-z][a-z0-9_]+$/i ) {
173 return undef if $noerr ;
174 die "invalid configuration ID ' $id ' \n " ;
179 PVE
:: JSONSchema
:: register_format
( 'pve-storage-id' , \
& parse_storage_id
);
180 sub parse_storage_id
{
181 my ( $storeid, $noerr ) = @_ ;
183 if ( $storeid !~ m/^[a-z][a-z0-9\-\_\.]*[a-z0-9]$/i ) {
184 return undef if $noerr ;
185 die "storage ID ' $storeid ' contains illegal characters \n " ;
191 register_format
( 'pve-vmid' , \
& pve_verify_vmid
);
192 sub pve_verify_vmid
{
193 my ( $vmid, $noerr ) = @_ ;
195 if ( $vmid !~ m/^[1-9][0-9]{2,8}$/ ) {
196 return undef if $noerr ;
197 die "value does not look like a valid VM ID \n " ;
202 register_format
( 'pve-node' , \
& pve_verify_node_name
);
203 sub pve_verify_node_name
{
204 my ( $node, $noerr ) = @_ ;
206 if ( $node !~ m/^([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)$/ ) {
207 return undef if $noerr ;
208 die "value does not look like a valid node name \n " ;
213 register_format
( 'mac-addr' , \
& pve_verify_mac_addr
);
214 sub pve_verify_mac_addr
{
215 my ( $mac_addr, $noerr ) = @_ ;
217 # don't allow I/G bit to be set, most of the time it breaks things, see:
218 # https://pve.proxmox.com/pipermail/pve-devel/2019-March/035998.html
219 if ( $mac_addr !~ m/^[a-f0-9][02468ace](?::[a-f0-9]{2}){5}$/i ) {
220 return undef if $noerr ;
221 die "value does not look like a valid unicast MAC address \n " ;
226 register_standard_option
( 'mac-addr' , {
228 description
=> 'Unicast MAC address.' ,
229 verbose_description
=> 'A common MAC address with the I/G (Individual/Group) bit not set.' ,
230 format_description
=> "XX:XX:XX:XX:XX:XX" ,
232 format
=> 'mac-addr' ,
235 register_format
( 'ipv4' , \
& pve_verify_ipv4
);
236 sub pve_verify_ipv4
{
237 my ( $ipv4, $noerr ) = @_ ;
239 if ( $ipv4 !~ m/^(?:$IPV4RE)$/ ) {
240 return undef if $noerr ;
241 die "value does not look like a valid IPv4 address \n " ;
246 register_format
( 'ipv6' , \
& pve_verify_ipv6
);
247 sub pve_verify_ipv6
{
248 my ( $ipv6, $noerr ) = @_ ;
250 if ( $ipv6 !~ m/^(?:$IPV6RE)$/ ) {
251 return undef if $noerr ;
252 die "value does not look like a valid IPv6 address \n " ;
257 register_format
( 'ip' , \
& pve_verify_ip
);
259 my ( $ip, $noerr ) = @_ ;
261 if ( $ip !~ m/^(?:(?:$IPV4RE)|(?:$IPV6RE))$/ ) {
262 return undef if $noerr ;
263 die "value does not look like a valid IP address \n " ;
268 my $ipv4_mask_hash = {
286 '255.255.128.0' => 17 ,
287 '255.255.192.0' => 18 ,
288 '255.255.224.0' => 19 ,
289 '255.255.240.0' => 20 ,
290 '255.255.248.0' => 21 ,
291 '255.255.252.0' => 22 ,
292 '255.255.254.0' => 23 ,
293 '255.255.255.0' => 24 ,
294 '255.255.255.128' => 25 ,
295 '255.255.255.192' => 26 ,
296 '255.255.255.224' => 27 ,
297 '255.255.255.240' => 28 ,
298 '255.255.255.248' => 29 ,
299 '255.255.255.252' => 30 ,
300 '255.255.255.254' => 31 ,
301 '255.255.255.255' => 32 ,
304 sub get_netmask_bits
{
306 return $ipv4_mask_hash ->{ $mask };
309 register_format
( 'ipv4mask' , \
& pve_verify_ipv4mask
);
310 sub pve_verify_ipv4mask
{
311 my ( $mask, $noerr ) = @_ ;
313 if (! defined ( $ipv4_mask_hash ->{ $mask })) {
314 return undef if $noerr ;
315 die "value does not look like a valid IP netmask \n " ;
320 register_format
( 'CIDRv6' , \
& pve_verify_cidrv6
);
321 sub pve_verify_cidrv6
{
322 my ( $cidr, $noerr ) = @_ ;
324 if ( $cidr =~ m!^(?:$IPV6RE)(?:/(\d+))$! && ( $1 > 7 ) && ( $1 <= 128 )) {
328 return undef if $noerr ;
329 die "value does not look like a valid IPv6 CIDR network \n " ;
332 register_format
( 'CIDRv4' , \
& pve_verify_cidrv4
);
333 sub pve_verify_cidrv4
{
334 my ( $cidr, $noerr ) = @_ ;
336 if ( $cidr =~ m!^(?:$IPV4RE)(?:/(\d+))$! && ( $1 > 7 ) && ( $1 <= 32 )) {
340 return undef if $noerr ;
341 die "value does not look like a valid IPv4 CIDR network \n " ;
344 register_format
( 'CIDR' , \
& pve_verify_cidr
);
345 sub pve_verify_cidr
{
346 my ( $cidr, $noerr ) = @_ ;
348 if (!( pve_verify_cidrv4
( $cidr, 1 ) ||
349 pve_verify_cidrv6
( $cidr, 1 )))
351 return undef if $noerr ;
352 die "value does not look like a valid CIDR network \n " ;
358 register_format
( 'pve-ipv4-config' , \
& pve_verify_ipv4_config
);
359 sub pve_verify_ipv4_config
{
360 my ( $config, $noerr ) = @_ ;
362 return $config if $config =~ /^(?:dhcp|manual)$/ ||
363 pve_verify_cidrv4
( $config, 1 );
364 return undef if $noerr ;
365 die "value does not look like a valid ipv4 network configuration \n " ;
368 register_format
( 'pve-ipv6-config' , \
& pve_verify_ipv6_config
);
369 sub pve_verify_ipv6_config
{
370 my ( $config, $noerr ) = @_ ;
372 return $config if $config =~ /^(?:auto|dhcp|manual)$/ ||
373 pve_verify_cidrv6
( $config, 1 );
374 return undef if $noerr ;
375 die "value does not look like a valid ipv6 network configuration \n " ;
378 register_format
( 'email' , \
& pve_verify_email
);
379 sub pve_verify_email
{
380 my ( $email, $noerr ) = @_ ;
382 if ( $email !~ /^[\w\+\-\~]+(\.[\w\+\-\~]+)*@[a-zA-Z0-9\-]+(\.[a-zA-Z0-9\-]+)*$/ ) {
383 return undef if $noerr ;
384 die "value does not look like a valid email address \n " ;
389 register_format
( 'dns-name' , \
& pve_verify_dns_name
);
390 sub pve_verify_dns_name
{
391 my ( $name, $noerr ) = @_ ;
393 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)" ;
395 if ( $name !~ /^(${namere}\.)*${namere}$/ ) {
396 return undef if $noerr ;
397 die "value does not look like a valid DNS name \n " ;
402 # network interface name
403 register_format
( 'pve-iface' , \
& pve_verify_iface
);
404 sub pve_verify_iface
{
405 my ( $id, $noerr ) = @_ ;
407 if ( $id !~ m/^[a-z][a-z0-9_]{1,20}([:\.]\d+)?$/i ) {
408 return undef if $noerr ;
409 die "invalid network interface name ' $id ' \n " ;
414 # general addresses by name or IP
415 register_format
( 'address' , \
& pve_verify_address
);
416 sub pve_verify_address
{
417 my ( $addr, $noerr ) = @_ ;
419 if (!( pve_verify_ip
( $addr, 1 ) ||
420 pve_verify_dns_name
( $addr, 1 )))
422 return undef if $noerr ;
423 die "value does not look like a valid address: $addr\n " ;
428 register_format
( 'disk-size' , \
& pve_verify_disk_size
);
429 sub pve_verify_disk_size
{
430 my ( $size, $noerr ) = @_ ;
431 if (! defined ( parse_size
( $size ))) {
432 return undef if $noerr ;
433 die "value does not look like a valid disk size: $size\n " ;
438 register_standard_option
( 'spice-proxy' , {
439 description
=> "SPICE proxy server. This can be used by the client to specify the proxy server. All nodes in a cluster runs 'spiceproxy', so it is up to the client to choose one. By default, we return the node where the VM is currently running. As reasonable setting is to use same node you use to connect to the API (This is window.location.hostname for the JS GUI)." ,
440 type
=> 'string' , format
=> 'address' ,
443 register_standard_option
( 'remote-viewer-config' , {
444 description
=> "Returned values can be directly passed to the 'remote-viewer' application." ,
445 additionalProperties
=> 1 ,
447 type
=> { type
=> 'string' },
448 password
=> { type
=> 'string' },
449 proxy
=> { type
=> 'string' },
450 host
=> { type
=> 'string' },
451 'tls-port' => { type
=> 'integer' },
455 register_format
( 'pve-startup-order' , \
& pve_verify_startup_order
);
456 sub pve_verify_startup_order
{
457 my ( $value, $noerr ) = @_ ;
459 return $value if pve_parse_startup_order
( $value );
461 return undef if $noerr ;
463 die "unable to parse startup options \n " ;
468 type
=> 'number' , minimum
=> '0' ,
469 format_description
=> 'LIMIT' ,
472 my $bwlimit_format = {
475 description
=> 'default bandwidth limit in KiB/s' ,
479 description
=> 'bandwidth limit in KiB/s for restoring guests from backups' ,
483 description
=> 'bandwidth limit in KiB/s for migrating guests (including moving local disks)' ,
487 description
=> 'bandwidth limit in KiB/s for cloning disks' ,
491 description
=> 'bandwidth limit in KiB/s for moving disks' ,
494 register_format
( 'bwlimit' , $bwlimit_format );
495 register_standard_option
( 'bwlimit' , {
496 description
=> "Set bandwidth/io limits various operations." ,
499 format
=> $bwlimit_format,
502 sub pve_parse_startup_order
{
505 return undef if ! $value ;
509 foreach my $p ( split ( /,/ , $value )) {
510 next if $p =~ m/^\s*$/ ;
512 if ( $p =~ m/^(order=)?(\d+)$/ ) {
514 } elsif ( $p =~ m/^up=(\d+)$/ ) {
516 } elsif ( $p =~ m/^down=(\d+)$/ ) {
526 PVE
:: JSONSchema
:: register_standard_option
( 'pve-startup-order' , {
527 description
=> "Startup and shutdown behavior. Order is a non-negative number defining the general startup order. Shutdown in done with reverse ordering. Additionally you can set the 'up' or 'down' delay in seconds, which specifies a delay to wait before the next VM is started or stopped." ,
529 type
=> 'string' , format
=> 'pve-startup-order' ,
530 typetext
=> '[[order=]\d+] [,up=\d+] [,down=\d+] ' ,
533 register_format
( 'pve-tfa-secret' , \
& pve_verify_tfa_secret
);
534 sub pve_verify_tfa_secret
{
535 my ( $key, $noerr ) = @_ ;
537 # The old format used 16 base32 chars or 40 hex digits. Since they have a common subset it's
538 # hard to distinguish them without the our previous length constraints, so add a 'v2' of the
539 # format to support arbitrary lengths properly:
540 if ( $key =~ /^v2-0x[0-9a-fA-F]{16,128}$/ || # hex
541 $key =~ /^v2-[A-Z2-7=]{16,128}$/ || # base32
542 $key =~ /^(?:[A-Z2-7=]{16}|[A-Fa-f0-9]{40})$/ ) # and the old pattern copy&pasted
547 return undef if $noerr ;
549 die "unable to decode TFA secret \n " ;
553 my ( $format, $value, $path ) = @_ ;
555 return parse_property_string
( $format, $value, $path ) if ref ( $format ) eq 'HASH' ;
556 return if $format eq 'regex' ;
558 if ( $format =~ m/^(.*)-a?list$/ ) {
560 my $code = $format_list ->{ $1 };
562 die "undefined format ' $format ' \n " if ! $code ;
564 # Note: we allow empty lists
565 foreach my $v ( split_list
( $value )) {
569 } elsif ( $format =~ m/^(.*)-opt$/ ) {
571 my $code = $format_list ->{ $1 };
573 die "undefined format ' $format ' \n " if ! $code ;
575 return if ! $value ; # allow empty string
581 my $code = $format_list ->{ $format };
583 die "undefined format ' $format ' \n " if ! $code ;
585 return parse_property_string
( $code, $value, $path ) if ref ( $code ) eq 'HASH' ;
593 return undef if $value !~ m/^(\d+(\.\d+)?)([KMGT])?$/ ;
594 my ( $size, $unit ) = ( $1, $3 );
597 $size = $size * 1024 ;
598 } elsif ( $unit eq 'M' ) {
599 $size = $size * 1024 * 1024 ;
600 } elsif ( $unit eq 'G' ) {
601 $size = $size * 1024 * 1024 * 1024 ;
602 } elsif ( $unit eq 'T' ) {
603 $size = $size * 1024 * 1024 * 1024 * 1024 ;
614 my $kb = int ( $size/1024 );
615 return $size if $kb*1024 != $size ;
617 my $mb = int ( $kb/1024 );
618 return "${kb}K" if $mb*1024 != $kb ;
620 my $gb = int ( $mb/1024 );
621 return "${mb}M" if $gb*1024 != $mb ;
623 my $tb = int ( $gb/1024 );
624 return "${gb}G" if $tb*1024 != $gb ;
631 return 1 if $bool =~ m/^(1|on|yes|true)$/i ;
632 return 0 if $bool =~ m/^(0|off|no|false)$/i ;
636 sub parse_property_string
{
637 my ( $format, $data, $path, $additional_properties ) = @_ ;
639 # In property strings we default to not allowing additional properties
640 $additional_properties = 0 if ! defined ( $additional_properties );
642 # Support named formats here, too:
644 if ( my $desc = $format_list ->{ $format }) {
647 die "unknown format: $format\n " ;
649 } elsif ( ref ( $format ) ne 'HASH' ) {
650 die "unexpected format value of type " . ref ( $format ). " \n " ;
656 foreach my $part ( split ( /,/ , $data )) {
657 next if $part =~ /^\s*$/ ;
659 if ( $part =~ /^([^=]+)=(.+)$/ ) {
660 my ( $k, $v ) = ( $1, $2 );
661 die "duplicate key in comma-separated list property: $k\n " if defined ( $res ->{ $k });
662 my $schema = $format ->{ $k };
663 if ( my $alias = $schema ->{ alias
}) {
664 if ( my $key_alias = $schema ->{ keyAlias
}) {
665 die "key alias ' $key_alias ' is already defined \n " if defined ( $res ->{ $key_alias });
666 $res ->{ $key_alias } = $k ;
669 $schema = $format ->{ $k };
672 die "invalid key in comma-separated list property: $k\n " if ! $schema ;
673 if ( $schema ->{ type
} && $schema ->{ type
} eq 'boolean' ) {
674 $v = parse_boolean
( $v ) // $v ;
677 } elsif ( $part !~ /=/ ) {
678 die "duplicate key in comma-separated list property: $default_key\n " if $default_key ;
679 foreach my $key ( keys %$format ) {
680 if ( $format ->{ $key }->{ default_key
}) {
682 if (! $res ->{ $default_key }) {
683 $res ->{ $default_key } = $part ;
686 die "duplicate key in comma-separated list property: $default_key\n " ;
689 die "value without key, but schema does not define a default key \n " if ! $default_key ;
691 die "missing key in comma-separated list property \n " ;
696 check_object
( $path, $format, $res, $additional_properties, $errors );
697 if ( scalar ( %$errors )) {
698 raise
"format error \n " , errors
=> $errors ;
705 my ( $errors, $path, $msg ) = @_ ;
707 $path = '_root' if ! $path ;
709 if ( $errors ->{ $path }) {
710 $errors ->{ $path } = join ( ' \n ' , $errors ->{ $path }, $msg );
712 $errors ->{ $path } = $msg ;
719 # see 'man perlretut'
720 return $value =~ /^[+-]?(\d+\.\d+|\d+\.|\.\d+|\d+)([eE][+-]?\d+)?$/ ;
726 return $value =~ m/^[+-]?\d+$/ ;
730 my ( $path, $type, $value, $errors ) = @_ ;
734 if (! defined ( $value )) {
735 return 1 if $type eq 'null' ;
739 if ( my $tt = ref ( $type )) {
740 if ( $tt eq 'ARRAY' ) {
741 foreach my $t ( @$type ) {
743 check_type
( $path, $t, $value, $tmperr );
744 return 1 if ! scalar ( %$tmperr );
746 my $ttext = join ( '|' , @$type );
747 add_error
( $errors, $path, "type check (' $ttext ') failed" );
749 } elsif ( $tt eq 'HASH' ) {
751 check_prop
( $value, $type, $path, $tmperr );
752 return 1 if ! scalar ( %$tmperr );
753 add_error
( $errors, $path, "type check failed" );
756 die "internal error - got reference type ' $tt '" ;
761 return 1 if $type eq 'any' ;
763 if ( $type eq 'null' ) {
764 if ( defined ( $value )) {
765 add_error
( $errors, $path, "type check (' $type ') failed - value is not null" );
771 my $vt = ref ( $value );
773 if ( $type eq 'array' ) {
774 if (! $vt || $vt ne 'ARRAY' ) {
775 add_error
( $errors, $path, "type check (' $type ') failed" );
779 } elsif ( $type eq 'object' ) {
780 if (! $vt || $vt ne 'HASH' ) {
781 add_error
( $errors, $path, "type check (' $type ') failed" );
785 } elsif ( $type eq 'coderef' ) {
786 if (! $vt || $vt ne 'CODE' ) {
787 add_error
( $errors, $path, "type check (' $type ') failed" );
791 } elsif ( $type eq 'string' && $vt eq 'Regexp' ) {
792 # qr// regexes can be used as strings and make sense for format=regex
796 add_error
( $errors, $path, "type check (' $type ') failed - got $vt " );
799 if ( $type eq 'string' ) {
800 return 1 ; # nothing to check ?
801 } elsif ( $type eq 'boolean' ) {
802 #if ($value =~ m/^(1|true|yes|on)$/i) {
805 #} elsif ($value =~ m/^(0|false|no|off)$/i) {
806 } elsif ( $value eq '0' ) {
807 return 1 ; # return success (not value)
809 add_error
( $errors, $path, "type check (' $type ') failed - got ' $value '" );
812 } elsif ( $type eq 'integer' ) {
813 if (! is_integer
( $value )) {
814 add_error
( $errors, $path, "type check (' $type ') failed - got ' $value '" );
818 } elsif ( $type eq 'number' ) {
819 if (! is_number
( $value )) {
820 add_error
( $errors, $path, "type check (' $type ') failed - got ' $value '" );
825 return 1 ; # no need to verify unknown types
835 my ( $path, $schema, $value, $additional_properties, $errors ) = @_ ;
837 # print "Check Object " . Dumper($value) . "\nSchema: " . Dumper($schema);
839 my $st = ref ( $schema );
840 if (! $st || $st ne 'HASH' ) {
841 add_error
( $errors, $path, "Invalid schema definition." );
845 my $vt = ref ( $value );
846 if (! $vt || $vt ne 'HASH' ) {
847 add_error
( $errors, $path, "an object is required" );
851 foreach my $k ( keys %$schema ) {
852 check_prop
( $value ->{ $k }, $schema ->{ $k }, $path ?
" $path . $k " : $k, $errors );
855 foreach my $k ( keys %$value ) {
857 my $newpath = $path ?
" $path . $k " : $k ;
859 if ( my $subschema = $schema ->{ $k }) {
860 if ( my $requires = $subschema ->{ requires
}) {
861 if ( ref ( $requires )) {
862 #print "TEST: " . Dumper($value) . "\n", Dumper($requires) ;
863 check_prop
( $value, $requires, $path, $errors );
864 } elsif (! defined ( $value ->{ $requires })) {
865 add_error
( $errors, $path ?
" $path . $requires " : $requires,
866 "missing property - ' $newpath ' requires this property" );
870 next ; # value is already checked above
873 if ( defined ( $additional_properties ) && ! $additional_properties ) {
874 add_error
( $errors, $newpath, "property is not defined in schema " .
875 "and the schema does not allow additional properties" );
878 check_prop
( $value ->{ $k }, $additional_properties, $newpath, $errors )
879 if ref ( $additional_properties );
883 sub check_object_warn
{
884 my ( $path, $schema, $value, $additional_properties ) = @_ ;
886 check_object
( $path, $schema, $value, $additional_properties, $errors );
887 if ( scalar ( %$errors )) {
888 foreach my $k ( keys %$errors ) {
889 warn "parse error: $k : $errors ->{ $k } \n " ;
897 my ( $value, $schema, $path, $errors ) = @_ ;
899 die "internal error - no schema" if ! $schema ;
900 die "internal error" if ! $errors ;
902 #print "check_prop $path\n" if $value;
904 my $st = ref ( $schema );
905 if (! $st || $st ne 'HASH' ) {
906 add_error
( $errors, $path, "Invalid schema definition." );
910 # if it extends another schema, it must pass that schema as well
911 if ( $schema ->{ extends
}) {
912 check_prop
( $value, $schema ->{ extends
}, $path, $errors );
915 if (! defined ( $value )) {
916 return if $schema ->{ type
} && $schema ->{ type
} eq 'null' ;
917 if (! $schema ->{ optional
} && ! $schema ->{ alias
} && ! $schema ->{ group
}) {
918 add_error
( $errors, $path, "property is missing and it is not optional" );
923 return if ! check_type
( $path, $schema ->{ type
}, $value, $errors );
925 if ( $schema ->{ disallow
}) {
927 if ( check_type
( $path, $schema ->{ disallow
}, $value, $tmperr )) {
928 add_error
( $errors, $path, "disallowed value was matched" );
933 if ( my $vt = ref ( $value )) {
935 if ( $vt eq 'ARRAY' ) {
936 if ( $schema ->{ items
}) {
937 my $it = ref ( $schema ->{ items
});
938 if ( $it && $it eq 'ARRAY' ) {
939 #die "implement me $path: $vt " . Dumper($schema) ."\n". Dumper($value);
940 die "not implemented" ;
943 foreach my $el ( @$value ) {
944 check_prop
( $el, $schema ->{ items
}, "${path}[ $ind ]" , $errors );
950 } elsif ( $schema ->{ properties
} || $schema ->{ additionalProperties
}) {
951 check_object
( $path, defined ( $schema ->{ properties
}) ?
$schema ->{ properties
} : {},
952 $value, $schema ->{ additionalProperties
}, $errors );
958 if ( my $format = $schema ->{ format
}) {
959 eval { check_format
( $format, $value, $path ); };
961 add_error
( $errors, $path, "invalid format - $@ " );
966 if ( my $pattern = $schema ->{ pattern
}) {
967 if ( $value !~ m/^$pattern$/ ) {
968 add_error
( $errors, $path, "value does not match the regex pattern" );
973 if ( defined ( my $max = $schema ->{ maxLength
})) {
974 if ( length ( $value ) > $max ) {
975 add_error
( $errors, $path, "value may only be $max characters long" );
980 if ( defined ( my $min = $schema ->{ minLength
})) {
981 if ( length ( $value ) < $min ) {
982 add_error
( $errors, $path, "value must be at least $min characters long" );
987 if ( is_number
( $value )) {
988 if ( defined ( my $max = $schema ->{ maximum
})) {
990 add_error
( $errors, $path, "value must have a maximum value of $max " );
995 if ( defined ( my $min = $schema ->{ minimum
})) {
997 add_error
( $errors, $path, "value must have a minimum value of $min " );
1003 if ( my $ea = $schema ->{ enum
}) {
1006 foreach my $ev ( @$ea ) {
1007 if ( $ev eq $value ) {
1013 add_error
( $errors, $path, "value ' $value ' does not have a value in the enumeration '" .
1014 join ( ", " , @$ea ) . "'" );
1021 my ( $instance, $schema, $errmsg ) = @_ ;
1024 $errmsg = "Parameter verification failed. \n " if ! $errmsg ;
1026 # todo: cycle detection is only needed for debugging, I guess
1027 # we can disable that in the final release
1028 # todo: is there a better/faster way to detect cycles?
1030 find_cycle
( $instance, sub { $cycles = 1 });
1032 add_error
( $errors, undef , "data structure contains recursive cycles" );
1034 check_prop
( $instance, $schema, '' , $errors );
1037 if ( scalar ( %$errors )) {
1038 raise
$errmsg, code
=> HTTP_BAD_REQUEST
, errors
=> $errors ;
1044 my $schema_valid_types = [ "string" , "object" , "coderef" , "array" , "boolean" , "number" , "integer" , "null" , "any" ];
1045 my $default_schema_noref = {
1046 description
=> "This is the JSON Schema for JSON Schemas." ,
1047 type
=> [ "object" ],
1048 additionalProperties
=> 0 ,
1051 type
=> [ "string" , "array" ],
1052 description
=> "This is a type definition value. This can be a simple type, or a union type" ,
1057 enum
=> $schema_valid_types,
1059 enum
=> $schema_valid_types,
1063 description
=> "This indicates that the instance property in the instance object is not required." ,
1069 description
=> "This is a definition for the properties of an object value" ,
1075 description
=> "When the value is an array, this indicates the schema to use to validate each item in an array" ,
1079 additionalProperties
=> {
1080 type
=> [ "boolean" , "object" ],
1081 description
=> "This provides a default property definition for all properties that are not explicitly defined in an object type definition." ,
1088 description
=> "This indicates the minimum value for the instance property when the type of the instance value is a number." ,
1093 description
=> "This indicates the maximum value for the instance property when the type of the instance value is a number." ,
1097 description
=> "When the instance value is a string, this indicates minimum length of the string" ,
1104 description
=> "When the instance value is a string, this indicates maximum length of the string." ,
1110 description
=> "A text representation of the type (used to generate documentation)." ,
1115 description
=> "When the instance value is a string, this provides a regular expression that a instance string value should match in order to be valid." ,
1122 description
=> "This provides an enumeration of possible values that are valid for the instance property." ,
1127 description
=> "This provides a description of the purpose the instance property. The value can be a string or it can be an object with properties corresponding to various different instance languages (with an optional default property indicating the default description)." ,
1129 verbose_description
=> {
1132 description
=> "This provides a more verbose description." ,
1134 format_description
=> {
1137 description
=> "This provides a shorter (usually just one word) description for a property used to generate descriptions for comma separated list property strings." ,
1142 description
=> "This provides the title of the property" ,
1147 description
=> "This is used to provide rendering hints to format cli command output." ,
1150 type
=> [ "string" , "object" ],
1152 description
=> "indicates a required property or a schema that must be validated if this property is present" ,
1155 type
=> [ "string" , "object" ],
1157 description
=> "This indicates what format the data is among some predefined formats which may include: \n\n date - a string following the ISO format \n address \n schema - a schema definition object \n person \n page \n html - a string representing HTML" ,
1162 description
=> "Whether this is the default key in a comma separated list property string." ,
1167 description
=> "When a key represents the same property as another it can be an alias to it, causing the parsed datastructure to use the other key to store the current value under." ,
1172 description
=> "Allows to store the current 'key' as value of another property. Only valid if used together with 'alias'." ,
1173 requires
=> 'alias' ,
1178 description
=> "This indicates the default for the instance property."
1182 description
=> "Bash completion function. This function should return a list of possible values." ,
1188 description
=> "This attribute may take the same values as the \" type \" attribute, however if the instance matches the type or if this value is an array and the instance matches any type or schema in the array, then this instance is not valid." ,
1193 description
=> "This indicates the schema extends the given schema. All instances of this schema must be valid to by the extended schema also." ,
1196 # this is from hyper schema
1199 description
=> "This defines the link relations of the instance objects" ,
1206 description
=> "This defines the target URL for the relation and can be parameterized using {propertyName} notation. It should be resolved as a URI-reference relative to the URI that was used to retrieve the instance document" ,
1210 description
=> "This is the name of the link relation" ,
1216 description
=> "For submission links, this defines the method that should be used to access the target resource" ,
1225 description
=> "For CLI context, this defines the maximal width to print before truncating" ,
1231 my $default_schema = Storable
:: dclone
( $default_schema_noref );
1233 $default_schema ->{ properties
}->{ properties
}->{ additionalProperties
} = $default_schema ;
1234 $default_schema ->{ properties
}->{ additionalProperties
}->{ properties
} = $default_schema ->{ properties
};
1236 $default_schema ->{ properties
}->{ items
}->{ properties
} = $default_schema ->{ properties
};
1237 $default_schema ->{ properties
}->{ items
}->{ additionalProperties
} = 0 ;
1239 $default_schema ->{ properties
}->{ disallow
}->{ properties
} = $default_schema ->{ properties
};
1240 $default_schema ->{ properties
}->{ disallow
}->{ additionalProperties
} = 0 ;
1242 $default_schema ->{ properties
}->{ requires
}->{ properties
} = $default_schema ->{ properties
};
1243 $default_schema ->{ properties
}->{ requires
}->{ additionalProperties
} = 0 ;
1245 $default_schema ->{ properties
}->{ extends
}->{ properties
} = $default_schema ->{ properties
};
1246 $default_schema ->{ properties
}->{ extends
}->{ additionalProperties
} = 0 ;
1248 my $method_schema = {
1250 additionalProperties
=> 0 ,
1253 description
=> "This a description of the method" ,
1258 description
=> "This indicates the name of the function to call." ,
1261 additionalProperties
=> 1 ,
1276 description
=> "The HTTP method name." ,
1277 enum
=> [ 'GET' , 'POST' , 'PUT' , 'DELETE' ],
1282 description
=> "Method needs special privileges - only pvedaemon can execute it" ,
1287 description
=> "Method downloads the file content (filename is the return value of the method)." ,
1292 description
=> "A parameter name. If specified, all calls to this method are proxied to the host contained in that parameter." ,
1295 proxyto_callback
=> {
1297 description
=> "A function which is called to resolve the proxyto attribute. The default implementation returns the value of the 'proxyto' parameter." ,
1302 description
=> "Required access permissions. By default only 'root' is allowed to access this method." ,
1304 additionalProperties
=> 0 ,
1307 description
=> "Describe access permissions." ,
1311 description
=> "A simply way to allow access for 'all' authenticated users. Value 'world' is used to allow access without credentials." ,
1313 enum
=> [ 'all' , 'world' ],
1317 description
=> "Array of permission checks (prefix notation)." ,
1324 description
=> "Used internally" ,
1328 description
=> "Used internally" ,
1333 description
=> "path for URL matching (uri template)" ,
1335 fragmentDelimiter
=> {
1337 description
=> "A way to override the default fragment delimiter '/'. This only works on a whole sub-class. You can set this to the empty string to match the whole rest of the URI." ,
1342 description
=> "JSON Schema for parameters." ,
1347 description
=> "JSON Schema for return value." ,
1352 description
=> "method implementation (code reference)" ,
1357 description
=> "Delegate call to this class (perl class string)." ,
1360 additionalProperties
=> 0 ,
1366 fragmentDelimiter
=> { optional
=> 1 }
1374 sub validate_schema
{
1377 my $errmsg = "internal error - unable to verify schema \n " ;
1378 validate
( $schema, $default_schema, $errmsg );
1381 sub validate_method_info
{
1384 my $errmsg = "internal error - unable to verify method info \n " ;
1385 validate
( $info, $method_schema, $errmsg );
1387 validate_schema
( $info ->{ parameters
}) if $info ->{ parameters
};
1388 validate_schema
( $info ->{ returns
}) if $info ->{ returns
};
1391 # run a self test on load
1392 # make sure we can verify the default schema
1393 validate_schema
( $default_schema_noref );
1394 validate_schema
( $method_schema );
1396 # and now some utility methods (used by pve api)
1397 sub method_get_child_link
{
1400 return undef if ! $info ;
1402 my $schema = $info ->{ returns
};
1403 return undef if ! $schema || ! $schema ->{ type
} || $schema ->{ type
} ne 'array' ;
1405 my $links = $schema ->{ links
};
1406 return undef if ! $links ;
1409 foreach my $lnk ( @$links ) {
1410 if ( $lnk ->{ href
} && $lnk ->{ rel
} && ( $lnk ->{ rel
} eq 'child' )) {
1419 # a way to parse command line parameters, using a
1420 # schema to configure Getopt::Long
1422 my ( $schema, $args, $arg_param, $fixed_param, $param_mapping_hash ) = @_ ;
1424 if (! $schema || ! $schema ->{ properties
}) {
1425 raise
( "too many arguments \n " , code
=> HTTP_BAD_REQUEST
)
1426 if scalar ( @$args ) != 0 ;
1431 if ( $arg_param && ! ref ( $arg_param )) {
1432 my $pd = $schema ->{ properties
}->{ $arg_param };
1433 die "expected list format $pd ->{format}"
1434 if !( $pd && $pd ->{ format
} && $pd ->{ format
} =~ m/-list/ );
1435 $list_param = $arg_param ;
1438 my @interactive = ();
1440 foreach my $prop ( keys %{ $schema ->{ properties
}}) {
1441 my $pd = $schema ->{ properties
}->{ $prop };
1442 next if $list_param && $prop eq $list_param ;
1443 next if defined ( $fixed_param ->{ $prop });
1445 my $mapping = $param_mapping_hash ->{ $prop };
1446 if ( $mapping && $mapping ->{ interactive
}) {
1447 # interactive parameters such as passwords: make the argument
1448 # optional and call the mapping function afterwards.
1449 push @getopt, " $prop :s" ;
1450 push @interactive, [ $prop, $mapping ->{ func
}];
1451 } elsif ( $pd ->{ type
} eq 'boolean' ) {
1452 push @getopt, " $prop :s" ;
1454 if ( $pd ->{ format
} && $pd ->{ format
} =~ m/-a?list/ ) {
1455 push @getopt, " $prop =s@" ;
1457 push @getopt, " $prop =s" ;
1462 Getopt
:: Long
:: Configure
( 'prefix_pattern=(--|-)' );
1465 raise
( "unable to parse option \n " , code
=> HTTP_BAD_REQUEST
)
1466 if ! Getopt
:: Long
:: GetOptionsFromArray
( $args, $opts, @getopt );
1470 $opts ->{ $list_param } = $args ;
1472 } elsif ( ref ( $arg_param )) {
1473 foreach my $arg_name ( @$arg_param ) {
1474 if ( $opts ->{ 'extra-args' }) {
1475 raise
( "internal error: extra-args must be the last argument \n " , code
=> HTTP_BAD_REQUEST
);
1477 if ( $arg_name eq 'extra-args' ) {
1478 $opts ->{ 'extra-args' } = $args ;
1482 raise
( "not enough arguments \n " , code
=> HTTP_BAD_REQUEST
) if ! @$args ;
1483 $opts ->{ $arg_name } = shift @$args ;
1485 raise
( "too many arguments \n " , code
=> HTTP_BAD_REQUEST
) if @$args ;
1487 raise
( "too many arguments \n " , code
=> HTTP_BAD_REQUEST
)
1488 if scalar ( @$args ) != 0 ;
1491 if ( ref ( $arg_param )) {
1492 foreach my $arg_name ( @$arg_param ) {
1493 if ( $arg_name eq 'extra-args' ) {
1494 $opts ->{ 'extra-args' } = [];
1496 raise
( "not enough arguments \n " , code
=> HTTP_BAD_REQUEST
);
1502 foreach my $entry ( @interactive ) {
1503 my ( $opt, $func ) = @$entry ;
1504 my $pd = $schema ->{ properties
}->{ $opt };
1505 my $value = $opts ->{ $opt };
1506 if ( defined ( $value ) || ! $pd ->{ optional
}) {
1507 $opts ->{ $opt } = $func ->( $value );
1511 # decode after Getopt as we are not sure how well it handles unicode
1512 foreach my $p ( keys %$opts ) {
1513 if (! ref ( $opts ->{ $p })) {
1514 $opts ->{ $p } = decode
( 'locale' , $opts ->{ $p });
1515 } elsif ( ref ( $opts ->{ $p }) eq 'ARRAY' ) {
1517 foreach my $v (@{ $opts ->{ $p }}) {
1518 push @$tmp, decode
( 'locale' , $v );
1521 } elsif ( ref ( $opts ->{ $p }) eq 'SCALAR' ) {
1522 $opts ->{ $p } = decode
( 'locale' , $$opts ->{ $p });
1524 raise
( "decoding options failed, unknown reference \n " , code
=> HTTP_BAD_REQUEST
);
1528 foreach my $p ( keys %$opts ) {
1529 if ( my $pd = $schema ->{ properties
}->{ $p }) {
1530 if ( $pd ->{ type
} eq 'boolean' ) {
1531 if ( $opts ->{ $p } eq '' ) {
1533 } elsif ( defined ( my $bool = parse_boolean
( $opts ->{ $p }))) {
1534 $opts ->{ $p } = $bool ;
1536 raise
( "unable to parse boolean option \n " , code
=> HTTP_BAD_REQUEST
);
1538 } elsif ( $pd ->{ format
}) {
1540 if ( $pd ->{ format
} =~ m/-list/ ) {
1541 # allow --vmid 100 --vmid 101 and --vmid 100,101
1542 # allow --dow mon --dow fri and --dow mon,fri
1543 $opts ->{ $p } = join ( "," , @{ $opts ->{ $p }}) if ref ( $opts ->{ $p }) eq 'ARRAY' ;
1544 } elsif ( $pd ->{ format
} =~ m/-alist/ ) {
1545 # we encode array as \0 separated strings
1546 # Note: CGI.pm also use this encoding
1547 if ( scalar (@{ $opts ->{ $p }}) != 1 ) {
1548 $opts ->{ $p } = join ( "\0" , @{ $opts ->{ $p }});
1550 # st that split_list knows it is \0 terminated
1551 my $v = $opts ->{ $p }->[ 0 ];
1552 $opts ->{ $p } = " $v\0 " ;
1559 foreach my $p ( keys %$fixed_param ) {
1560 $opts ->{ $p } = $fixed_param ->{ $p };
1566 # A way to parse configuration data by giving a json schema
1568 my ( $schema, $filename, $raw ) = @_ ;
1570 # do fast check (avoid validate_schema($schema))
1571 die "got strange schema" if ! $schema ->{ type
} ||
1572 ! $schema ->{ properties
} || $schema ->{ type
} ne 'object' ;
1576 while ( $raw =~ /^\s*(.+?)\s*$/gm ) {
1579 next if $line =~ /^#/ ;
1581 if ( $line =~ m/^(\S+?):\s*(.*)$/ ) {
1584 if ( $schema ->{ properties
}->{ $key } &&
1585 $schema ->{ properties
}->{ $key }->{ type
} eq 'boolean' ) {
1587 $value = parse_boolean
( $value ) // $value ;
1589 $cfg ->{ $key } = $value ;
1591 warn "ignore config line: $line\n "
1596 check_prop
( $cfg, $schema, '' , $errors );
1598 foreach my $k ( keys %$errors ) {
1599 warn "parse error in ' $filename ' - ' $k ': $errors ->{ $k } \n " ;
1606 # generate simple key/value file
1608 my ( $schema, $filename, $cfg ) = @_ ;
1610 # do fast check (avoid validate_schema($schema))
1611 die "got strange schema" if ! $schema ->{ type
} ||
1612 ! $schema ->{ properties
} || $schema ->{ type
} ne 'object' ;
1614 validate
( $cfg, $schema, "validation error in ' $filename ' \n " );
1618 foreach my $k ( sort keys %$cfg ) {
1619 $data .= " $k : $cfg ->{ $k } \n " ;
1625 # helpers used to generate our manual pages
1627 my $find_schema_default_key = sub {
1631 my $keyAliasProps = {};
1633 foreach my $key ( keys %$format ) {
1634 my $phash = $format ->{ $key };
1635 if ( $phash ->{ default_key
}) {
1636 die "multiple default keys in schema ( $default_key, $key ) \n "
1637 if defined ( $default_key );
1638 die "default key ' $key ' is an alias - this is not allowed \n "
1639 if defined ( $phash ->{ alias
});
1640 die "default key ' $key ' with keyAlias attribute is not allowed \n "
1641 if $phash ->{ keyAlias
};
1642 $default_key = $key ;
1644 my $key_alias = $phash ->{ keyAlias
};
1645 die "found keyAlias without 'alias definition for ' $key ' \n "
1646 if $key_alias && ! $phash ->{ alias
};
1648 if ( $phash ->{ alias
} && $key_alias ) {
1649 die "inconsistent keyAlias ' $key_alias ' definition"
1650 if defined ( $keyAliasProps ->{ $key_alias }) &&
1651 $keyAliasProps ->{ $key_alias } ne $phash ->{ alias
};
1652 $keyAliasProps ->{ $key_alias } = $phash ->{ alias
};
1656 return wantarray ?
( $default_key, $keyAliasProps ) : $default_key ;
1659 sub generate_typetext
{
1660 my ( $format, $list_enums ) = @_ ;
1662 my ( $default_key, $keyAliasProps ) = & $find_schema_default_key ( $format );
1667 my $add_option_string = sub {
1668 my ( $text, $optional ) = @_ ;
1674 $text = "[ $text ]" if $optional ;
1679 my $format_key_value = sub {
1680 my ( $key, $phash ) = @_ ;
1682 die "internal error" if defined ( $phash ->{ alias
});
1688 if ( my $desc = $phash ->{ format_description
}) {
1689 $typetext .= "< $desc >" ;
1690 } elsif ( my $text = $phash ->{ typetext
}) {
1692 } elsif ( my $enum = $phash ->{ enum
}) {
1693 if ( $list_enums || ( scalar ( @$enum ) <= 3 )) {
1694 $typetext .= '<' . join ( '|' , @$enum ) . '>' ;
1696 $typetext .= '<enum>' ;
1698 } elsif ( $phash ->{ type
} eq 'boolean' ) {
1699 $typetext .= '<1|0>' ;
1700 } elsif ( $phash ->{ type
} eq 'integer' ) {
1701 $typetext .= '<integer>' ;
1702 } elsif ( $phash ->{ type
} eq 'number' ) {
1703 $typetext .= '<number>' ;
1705 die "internal error: neither format_description nor typetext found for option ' $key '" ;
1708 if ( defined ( $default_key ) && ( $default_key eq $key )) {
1709 & $add_option_string ( "[ $keytext =] $typetext " , $phash ->{ optional
});
1711 & $add_option_string ( " $keytext = $typetext " , $phash ->{ optional
});
1717 my $cond_add_key = sub {
1720 return if $done ->{ $key }; # avoid duplicates
1724 my $phash = $format ->{ $key };
1726 return if ! $phash ; # should not happen
1728 return if $phash ->{ alias
};
1730 & $format_key_value ( $key, $phash );
1734 & $cond_add_key ( $default_key ) if defined ( $default_key );
1736 # add required keys first
1737 foreach my $key ( sort keys %$format ) {
1738 my $phash = $format ->{ $key };
1739 & $cond_add_key ( $key ) if $phash && ! $phash ->{ optional
};
1743 foreach my $key ( sort keys %$format ) {
1744 & $cond_add_key ( $key );
1747 foreach my $keyAlias ( sort keys %$keyAliasProps ) {
1748 & $add_option_string ( "< $keyAlias >=< $keyAliasProps ->{ $keyAlias }>" , 1 );
1754 sub print_property_string
{
1755 my ( $data, $format, $skip, $path ) = @_ ;
1757 if ( ref ( $format ) ne 'HASH' ) {
1758 my $schema = get_format
( $format );
1759 die "not a valid format: $format\n " if ! $schema ;
1764 check_object
( $path, $format, $data, undef , $errors );
1765 if ( scalar ( %$errors )) {
1766 raise
"format error" , errors
=> $errors ;
1769 my ( $default_key, $keyAliasProps ) = & $find_schema_default_key ( $format );
1774 my $add_option_string = sub {
1777 $res .= ',' if $add_sep ;
1782 my $format_value = sub {
1783 my ( $key, $value, $format ) = @_ ;
1785 if ( defined ( $format ) && ( $format eq 'disk-size' )) {
1786 return format_size
( $value );
1788 die "illegal value with commas for $key\n " if $value =~ /,/ ;
1793 my $done = { map { $_ => 1 } @$skip };
1795 my $cond_add_key = sub {
1796 my ( $key, $isdefault ) = @_ ;
1798 return if $done ->{ $key }; # avoid duplicates
1802 my $value = $data ->{ $key };
1804 return if ! defined ( $value );
1806 my $phash = $format ->{ $key };
1808 # try to combine values if we have key aliases
1809 if ( my $combine = $keyAliasProps ->{ $key }) {
1810 if ( defined ( my $combine_value = $data ->{ $combine })) {
1811 my $combine_format = $format ->{ $combine }->{ format
};
1812 my $value_str = & $format_value ( $key, $value, $phash ->{ format
});
1813 my $combine_str = & $format_value ( $combine, $combine_value, $combine_format );
1814 & $add_option_string ( "${value_str}=${combine_str}" );
1815 $done ->{ $combine } = 1 ;
1820 if ( $phash && $phash ->{ alias
}) {
1821 $phash = $format ->{ $phash ->{ alias
}};
1824 die "invalid key ' $key ' \n " if ! $phash ;
1825 die "internal error" if defined ( $phash ->{ alias
});
1827 my $value_str = & $format_value ( $key, $value, $phash ->{ format
});
1829 & $add_option_string ( $value_str );
1831 & $add_option_string ( " $key =${value_str}" );
1835 # add default key first
1836 & $cond_add_key ( $default_key, 1 ) if defined ( $default_key );
1838 # add required keys first
1839 foreach my $key ( sort keys %$data ) {
1840 my $phash = $format ->{ $key };
1841 & $cond_add_key ( $key ) if $phash && ! $phash ->{ optional
};
1845 foreach my $key ( sort keys %$data ) {
1846 & $cond_add_key ( $key );
1852 sub schema_get_type_text
{
1853 my ( $phash, $style ) = @_ ;
1855 my $type = $phash ->{ type
} || 'string' ;
1857 if ( $phash ->{ typetext
}) {
1858 return $phash ->{ typetext
};
1859 } elsif ( $phash ->{ format_description
}) {
1860 return "< $phash ->{format_description}>" ;
1861 } elsif ( $phash ->{ enum
}) {
1862 return "<" . join ( ' | ' , sort @{ $phash ->{ enum
}}) . ">" ;
1863 } elsif ( $phash ->{ pattern
}) {
1864 return $phash ->{ pattern
};
1865 } elsif ( $type eq 'integer' || $type eq 'number' ) {
1866 # NOTE: always access values as number (avoid converion to string)
1867 if ( defined ( $phash ->{ minimum
}) && defined ( $phash ->{ maximum
})) {
1868 return "< $type > (" . ( $phash ->{ minimum
} + 0 ) . " - " .
1869 ( $phash ->{ maximum
} + 0 ) . ")" ;
1870 } elsif ( defined ( $phash ->{ minimum
})) {
1871 return "< $type > (" . ( $phash ->{ minimum
} + 0 ) . " - N)" ;
1872 } elsif ( defined ( $phash ->{ maximum
})) {
1873 return "< $type > (-N - " . ( $phash ->{ maximum
} + 0 ) . ")" ;
1875 } elsif ( $type eq 'string' ) {
1876 if ( my $format = $phash ->{ format
}) {
1877 $format = get_format
( $format ) if ref ( $format ) ne 'HASH' ;
1878 if ( ref ( $format ) eq 'HASH' ) {
1880 $list_enums = 1 if $style && $style eq 'config-sub' ;
1881 return generate_typetext
( $format, $list_enums );