pax_global_header00006660000000000000000000000064146472543750014533gustar00rootroot0000000000000052 comment=6a811aa9bbb04563eab52a510fa055243a702279 p5-Kwalify-1.24/000077500000000000000000000000001464725437500134115ustar00rootroot00000000000000p5-Kwalify-1.24/.github/000077500000000000000000000000001464725437500147515ustar00rootroot00000000000000p5-Kwalify-1.24/.github/workflows/000077500000000000000000000000001464725437500170065ustar00rootroot00000000000000p5-Kwalify-1.24/.github/workflows/test.yml000066400000000000000000000115761464725437500205220ustar00rootroot00000000000000name: CI on: push: branches-ignore: - '*travis*' - '*appveyor*' - '*doozer*' pull_request: workflow_dispatch: jobs: test: name: ${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: matrix: include: - os: 'ubuntu-20.04' - os: 'ubuntu-22.04' - os: 'ubuntu-24.04' steps: - name: apt hacks # removing non-needed apt repositories may speed up "apt-get update" # also, 3rd party repositories might be unreliable run: | sudo rm -f /etc/apt/sources.list.d/*.list sudo apt-get update -qq if: "startsWith(matrix.os, 'ubuntu-')" - uses: taiki-e/checkout-action@v1 - name: Preinstall, Configure, Build and Test env: HARNESS_TIMER: 1 HARNESS_OPTIONS: j8 run: | sudo apt-get install -qq --no-install-recommends cpanminus cpanm --sudo --quiet --installdeps --notest . || ( cat ~/.cpanm/work/*/build.log; false ) perl Makefile.PL make -j8 make test if: "!startsWith(matrix.os,'windows-')" test_in_container: name: Test with perl ${{ matrix.perlimage }} ${{ matrix.label }} runs-on: ubuntu-latest container: perl:${{ matrix.perlimage }} strategy: matrix: include: - perlimage: 5.8.9-threaded-stretch - perlimage: 5.10.1-buster - perlimage: 5.12.5-stretch - perlimage: 5.14.4-stretch - perlimage: 5.16.3-buster - perlimage: 5.18.4-buster - perlimage: 5.22.4-stretch - perlimage: 5.24.4-threaded-buster WITH_JSON_XS: 1 WITH_YAML_SYCK: 1 label: "with JSON::XS + YAML::Syck" - perlimage: 5.26.3-buster WITH_JSON_XS: 1 WITH_YAML_XS: 1 label: "with JSON::XS + YAML::XS" - perlimage: 5.28.3-buster WITH_JSON: 1 WITH_YAML: 1 label: "with JSON + YAML" - perlimage: 5.30.3-bullseye WITH_YAML_SYCK: 1 label: "with YAML::Syck" - perlimage: 5.32.1-bullseye WITH_YAML_XS: 1 label: "with YAML::XS" - perlimage: 5.34.3-bullseye WITH_YAML_PP: 1 label: "with YAML::PP" - perlimage: 5.36.3-bookworm WITH_YAML: 1 label: "with YAML" - perlimage: 5.38.2-bookworm WITH_JSON: 1 WITH_YAML: 1 WITH_IPC_RUN: 1 DO_COVERALLS: 1 label: "with JSON + YAML + IPC::Run + DO_COVERALLS" - perlimage: 5.40.0-slim-bookworm steps: - name: apt hacks run: | set -e CODENAME=$(perl -nle '/^VERSION_CODENAME="?([^"]+)/ and $codename=$1; /^VERSION="\d+ \((.*)\)/ and $maybe_codename=$1; END { print $codename || $maybe_codename }' /etc/os-release) case "$CODENAME" in wheezy|jessie|stretch) echo "APT::Get::AllowUnauthenticated 1;" > /etc/apt/apt.conf.d/02allow-unsigned echo "deb [check-valid-until=no] http://archive.debian.org/debian $CODENAME main" > /etc/apt/sources.list echo "deb [check-valid-until=no] http://archive.debian.org/debian-security/ $CODENAME/updates main" >> /etc/apt/sources.list ;; esac - uses: taiki-e/checkout-action@v1 - name: Preinstall, Configure, Build and Test env: HARNESS_TIMER: 1 HARNESS_OPTIONS: j8 DO_COVERALLS: ${{ matrix.DO_COVERALLS }} WITH_JSON_XS: ${{ matrix.WITH_JSON_XS }} WITH_JSON: ${{ matrix.WITH_JSON }} WITH_YAML_SYCK: ${{ matrix.WITH_YAML_SYCK }} WITH_YAML_XS: ${{ matrix.WITH_YAML_XS }} WITH_YAML_PP: ${{ matrix.WITH_YAML_PP }} WITH_YAML: ${{ matrix.WITH_YAML }} WITH_IPC_RUN: ${{ matrix.WITH_IPC_RUN }} run: | [ "$DO_COVERALLS" = 1 ] && cpanm --quiet --notest Devel::Cover::Report::Coveralls || true [ "$WITH_JSON_XS" = 1 ] && cpanm --quiet --notest JSON::XS || true [ "$WITH_JSON" = 1 ] && cpanm --quiet --notest JSON || true [ "$WITH_YAML_SYCK" = 1 ] && cpanm --quiet --notest YAML::Syck || true [ "$WITH_YAML_XS" = 1 ] && cpanm --quiet --notest YAML::XS || true [ "$WITH_YAML_PP" = 1 ] && cpanm --quiet --notest YAML::PP || true [ "$WITH_YAML" = 1 ] && cpanm --quiet --notest YAML || true [ "$WITH_IPC_RUN" = 1 ] && cpanm --quiet --notest IPC::Run || true cpanm --quiet --installdeps --notest . || ( cat ~/.cpanm/work/*/build.log; false ) perl Makefile.PL make -j8 make test [ "$DO_COVERALLS" = 1 ] && cover -test -report coveralls || true p5-Kwalify-1.24/.gitignore000066400000000000000000000001471464725437500154030ustar00rootroot00000000000000/.prove /Kwalify-*.tar.gz /MYMETA.json /MYMETA.yml /Makefile /Makefile.old /blib /cover_db /pm_to_blib p5-Kwalify-1.24/Changes000066400000000000000000000051031464725437500147030ustar00rootroot00000000000000Revision history for Perl extension Kwalify. 1.24 2024-07-21 - doc improvements (by guillemj) - ci changes (swich from travis-ci to github actions) - tests: allow cyclic_refs when YAML::PP is used 1.23 2020-02-21 - stable release with all changes in 1.22_90 1.22_90 2020-01-31 - change handling of required:no for existing undefined data. Now the behavior is like in the ruby implementation: hash entries with an undefined value are treated as missing, and thus pass the "required:no" rule. This addresses https://github.com/eserte/p5-Kwalify/issues/1 - more tests - enable travis-ci and coveralls - don't use YAML::Syck anymore in the tests because of https://github.com/toddr/YAML-Syck/issues/52 (use YAML::PP instead) 1.22 2013-04-06 - cease possible "unitinialized value" warnings 1.21 2009-10-23 - fixed RT #48800 (unique in mappings did not work correctly) 1.20 2009-08-14 - test failure fix - fixed RT #48714 (return early if not a hash was given) 1.19 2008-08-11 - allow the new "class" (previously "classname") - more documentation about unclear schema definition - -h/-help is not anymore "accidentaly" showing the usage 1.18 2008-07-16 - additional checks were not run with type 'any' - ceasing a warning if undef was used in enum - added warnings::compat prereq for 5.005 and older 1.17 2008-06-18 - documentation change: the URL of the schema user guide changed (thanks to Christoph Lamprecht for the pointer) 1.16 2007-09-22 - get rid of backquote in error and warning messages (after reading http://www.cl.cam.ac.uk/~mgk25/ucs/quotes.html) - protect from blib warnings (perl 5.6.x) in pkwalify.t test 1.15 (not released) - include experimental META.yml spec (version 1.3) to test directory - new -v version in pkwalify - new tests 1.14 2007-02-08 - stricter checks if schema itself is correct (croak on unhandled keys) 1.13 2007-01-10 - fixed bug in range/max test - fixed bug in Schema::Kwalify::validate - better test coverage 1.12 (not released) - added another test for older perls 1.11 2006-12-02 - fixed another test problem (occurred if IPC::Run was not installed) 1.10 2006-11-28 - just some test fixes 1.09 2006-11-28 - fixed endless recursion when using recursive schemas or data structures - implemented -s option in pkwalify (like in ruby's kwalify) - pkwalify output now looks closer to the original - pkwalify outputs most things to STDOUT instead of STDERR 1.08 2006-11-26 - fixed warnings in pkwalify - inc version in Kwalify.pm to force reinstalling 1.07 2006-11-23 first and maybe last version :-) p5-Kwalify-1.24/MANIFEST000066400000000000000000000003741464725437500145460ustar00rootroot00000000000000Changes Makefile.PL MANIFEST README pkwalify t/compat.t t/Kwalify.t t/pkwalify.t t/testdata/document05a.yaml t/testdata/document05b.yaml t/testdata/kwalify.yaml t/testdata/schema05.yaml t/testdata/META-spec-1.3.yml lib/Kwalify.pm lib/Schema/Kwalify.pm p5-Kwalify-1.24/Makefile.PL000066400000000000000000000023051464725437500153630ustar00rootroot00000000000000use 5.000; use ExtUtils::MakeMaker; $is_devel_host = defined $ENV{USER} && $ENV{USER} eq 'eserte' && ($^O =~ /bsd/i || $ENV{PERL_RELEASE_READY}) && -f "../../perl.release.mk"; $eumm_recent_enough = $ExtUtils::MakeMaker::VERSION >= 6.54; if (!$eumm_recent_enough) { *MY::dist_core = sub { <<'EOF'; dist : $(NOECHO) $(ECHO) "Sorry, use a newer EUMM!" EOF }; } WriteMakefile( NAME => 'Kwalify', VERSION_FROM => 'lib/Kwalify.pm', PREREQ_PM => { ($] < 5.006 ? ('warnings::compat' => 0) : ()), }, EXE_FILES => ['pkwalify'], LICENSE => 'perl', #NO_META => 1, ($] >= 5.005 ? ## Add these new keywords supported since 5.005 (ABSTRACT_FROM => 'lib/Kwalify.pm', # retrieve abstract from module AUTHOR => 'Slaven Rezic ') : ()), ($eumm_recent_enough ? (META_ADD => { resources => { repository => 'git://github.com/eserte/p5-Kwalify.git' } }) : ()), ); sub MY::postamble { my $postamble = ''; if ($is_devel_host) { $postamble .= <<'EOF'; PERL_TEST_DISTRIBUTION_OPTS=not podcover .include "../../perl.release.mk" .include "../../perl.git.mk" EOF } $postamble; } p5-Kwalify-1.24/README000066400000000000000000000015601464725437500142730ustar00rootroot00000000000000Kwalify ======= Kwalify is a Perl implementation for validating data structures against the Kwalify schema. For a schema definition, see http://www.kuwata-lab.com/kwalify/ruby/users-guide.01.html Note that there is no support for validator hooks (section 1-7 of the user guide document). INSTALLATION To install this module type the following: perl Makefile.PL make make test make install Or with modern CPAN.pm ($CPAN::VERSION >= 1.88_55) just: cpan . USAGE of the installed program: pkwalify -f schemafile datafile schemafile and datafile may be YAML or JSON files. COPYRIGHT AND LICENCE Copyright (C) 2006,2007,2008,2009 by Slaven Rezic This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself, either Perl version 5.8.8 or, at your option, any later version of Perl 5 you may have available. p5-Kwalify-1.24/appveyor.yml000066400000000000000000000013601464725437500160010ustar00rootroot00000000000000branches: except: - /travis/ - /github-actions/ - /doozer/ skip_tags: true #cache: # - C:\strawberry environment: matrix: ## ppm downloads do not work anymore, since beginning of June 2024 # - perl: activeperl # make: dmake - perl: strawberryperl make: gmake install: - if %perl%==strawberryperl ( if not exist "C:\strawberry" cinst strawberryperl ) - if %perl%==strawberryperl set PATH=C:\strawberry\perl\bin;C:\strawberry\perl\site\bin;C:\strawberry\c\bin;%PATH% - cd C:\projects\%APPVEYOR_PROJECT_NAME% - if %perl%==activeperl ppm install dmake YAML-LibYAML - if %perl%==strawberryperl ( cpanm --installdeps . && cpanm --notest YAML::XS ) build_script: - perl Makefile.PL - '%make% test' p5-Kwalify-1.24/lib/000077500000000000000000000000001464725437500141575ustar00rootroot00000000000000p5-Kwalify-1.24/lib/Kwalify.pm000066400000000000000000000410141464725437500161230ustar00rootroot00000000000000# -*- mode: cperl -*- # # Author: Slaven Rezic # # Copyright (C) 2006,2007,2008,2009,2010,2015,2020,2024 Slaven Rezic. All rights reserved. # This package is free software; you can redistribute it and/or # modify it under the same terms as Perl itself. # # Mail: srezic@cpan.org # WWW: https://github.com/eserte/p5-kwalify/ # package Kwalify; use strict; use warnings; use base qw(Exporter); use vars qw(@EXPORT_OK $VERSION); @EXPORT_OK = qw(validate); $VERSION = '1.24'; sub validate ($$) { my($schema, $data) = @_; my $self = Kwalify::Validator->new; $self->validate($schema, $data, "/"); if (@{$self->{errors}}) { die join("\n", map { " - $_" } @{$self->{errors}}) . "\n"; } else { 1; } } package Kwalify::Validator; use overload (); sub new { my($class) = @_; bless { errors => [] }, $class; } sub validate { my($self, $schema, $data, $path, $args) = @_; $self->{done} = {}; $self->_validate($schema, $data, $path, $args); } sub _validate { my($self, $schema, $data, $path, $args) = @_; $self->{path} = $path; if (!UNIVERSAL::isa($schema, "HASH")) { $self->_die("Schema structure must be a hash reference"); } my $type = $schema->{type}; if (!defined $type) { $type = 'str'; # default type; } my $type_check_method = "validate_" . $type; if (!$self->can($type_check_method)) { $self->_die("Invalid or unimplemented type '$type'"); } $self->$type_check_method($schema, $data, $path, $args); } sub _additional_rules { my($self, $schema, $data, $path) = @_; no warnings 'uninitialized'; # legal undef values may happen everywhere for my $schema_key (keys %$schema) { if (defined $schema->{$schema_key}) { if ($schema_key eq 'pattern') { (my $pattern = $schema->{pattern}) =~ s{^/(.*)/$}{$1}; if ($data !~ qr{$pattern}) { $self->_error("Non-valid data '$data' does not match /$pattern/"); } } elsif ($schema_key eq 'length') { if (!UNIVERSAL::isa($schema->{'length'}, "HASH")) { $self->_die("'length' must be a hash with keys max and/or min"); } my $length = length($data); for my $sub_schema_key (keys %{ $schema->{'length'} }) { if ($sub_schema_key eq 'min') { my $min = $schema->{'length'}->{min}; if ($length < $min) { $self->_error("'$data' is too short (length $length < min $min)"); } } elsif ($sub_schema_key eq 'min-ex') { my $min = $schema->{'length'}->{'min-ex'}; if ($length <= $min) { $self->_error("'$data' is too short (length $length <= min $min)"); } } elsif ($sub_schema_key eq 'max') { my $max = $schema->{'length'}->{max}; if ($length > $max) { $self->_error("'$data' is too long (length $length > max $max)"); } } elsif ($sub_schema_key eq 'max-ex') { my $max = $schema->{'length'}->{'max-ex'}; if ($length >= $max) { $self->_error("'$data' is too long (length $length >= max $max)"); } } else { $self->_die("Unexpected key '$sub_schema_key' in length specification, expected min, max, min-ex and/or max-ex"); } } } elsif ($schema_key eq 'enum') { if (!UNIVERSAL::isa($schema->{enum}, 'ARRAY')) { $self->_die("'enum' must be an array"); } my %valid = map { ($_,1) } @{ $schema->{enum} }; if (!exists $valid{$data}) { $self->_error("'$data': invalid " . _base_path($path) . " value"); } } elsif ($schema_key eq 'range') { if (!UNIVERSAL::isa($schema->{range}, "HASH")) { $self->_die("'range' must be a hash with keys max and/or min"); } my($lt, $le, $gt, $ge); ## yes? no? # if (eval { require Scalar::Util; defined &Scalar::Util::looks_like_number }) { # if (Scalar::Util::looks_like_number($data)) { # $lt = sub { $_[0] < $_[1] }; # $gt = sub { $_[0] > $_[1] }; # } else { # $lt = sub { $_[0] lt $_[1] }; # $gt = sub { $_[0] gt $_[1] }; # } # } else { # warn "Cannot determine whether $data is a number, assume so..."; # XXX show only once no warnings 'numeric'; $lt = sub { $_[0] < $_[1] }; $gt = sub { $_[0] > $_[1] }; $le = sub { $_[0] <= $_[1] }; $ge = sub { $_[0] >= $_[1] }; # } for my $sub_schema_key (keys %{ $schema->{range} }) { if ($sub_schema_key eq 'min') { my $min = $schema->{range}->{min}; if ($lt->($data, $min)) { $self->_error("'$data' is too small (< min $min)"); } } elsif ($sub_schema_key eq 'min-ex') { my $min = $schema->{range}->{'min-ex'}; if ($le->($data, $min)) { $self->_error("'$data' is too small (<= min $min)"); } } elsif ($sub_schema_key eq 'max') { my $max = $schema->{range}->{max}; if ($gt->($data, $max)) { $self->_error("'$data' is too large (> max $max)"); } } elsif ($sub_schema_key eq 'max-ex') { my $max = $schema->{range}->{'max-ex'}; if ($ge->($data, $max)) { $self->_error("'$data' is too large (>= max $max)"); } } else { $self->_die("Unexpected key '$sub_schema_key' in range specification, expected min, max, min-ex and/or max-ex"); } } } elsif ($schema_key eq 'assert') { $self->_die("'assert' is not yet implemented"); } elsif ($schema_key !~ m{^(type|required|unique|name|classname|class|desc)$}) { $self->_die("Unexpected key '$schema_key' in type specification"); } } } } sub validate_text { my($self, $schema, $data, $path) = @_; if (!defined $data || ref $data) { return $self->_error("Non-valid data '" . (defined $data ? $data : 'undef') . "', expected text"); } $self->_additional_rules($schema, $data, $path); } sub validate_str { my($self, $schema, $data, $path) = @_; if (!defined $data || ref $data || $data =~ m{^\d+(\.\d+)?$}) { return $self->_error("Non-valid data '" . (defined $data ? $data : 'undef') . "', expected a str"); } $self->_additional_rules($schema, $data, $path); } sub validate_int { my($self, $schema, $data, $path) = @_; if ($data !~ m{^[+-]?\d+$}) { # XXX what about scientific notation? $self->_error("Non-valid data '" . $data . "', expected an int"); } $self->_additional_rules($schema, $data, $path); } sub validate_float { my($self, $schema, $data, $path) = @_; if ($data !~ m{^[+-]?\d+\.\d+$}) { # XXX other values? $self->_error("Non-valid data '" . $data . "', expected a float"); } $self->_additional_rules($schema, $data, $path); } sub validate_number { my($self, $schema, $data, $path) = @_; if ($data !~ m{^[+-]?\d+(\.\d+)?$}) { # XXX combine int+float regexp! $self->_error("Non-valid data '" . $data . "', expected a number"); } $self->_additional_rules($schema, $data, $path); } sub validate_bool { my($self, $schema, $data, $path) = @_; if ($data !~ m{^(yes|true|1|no|false|0)$}) { # XXX correct? $self->_error("Non-valid data '" . $data . "', expected a boolean"); } $self->_additional_rules($schema, $data, $path); } # XXX is this correct? sub validate_scalar { shift->validate_text(@_); } sub validate_date { my($self, $schema, $data, $path) = @_; if ($data !~ m{^\d{4}-\d{2}-\d{2}$}) { $self->_error("Non-valid data '" . $data . "', expected a date (YYYY-MM-DD)"); } $self->_additional_rules($schema, $data, $path); } sub validate_time { my($self, $schema, $data, $path) = @_; if ($data !~ m{^\d{2}:\d{2}:\d{2}$}) { $self->_error("Non-valid data '" . $data . "', expected a time (HH:MM:SS)"); } $self->_additional_rules($schema, $data, $path); } sub validate_timestamp { my($self) = @_; $self->_error("timestamp validation NYI"); # XXX } sub validate_any { my($self, $schema, $data, $path) = @_; $self->_additional_rules($schema, $data, $path); } sub validate_seq { my($self, $schema, $data, $path) = @_; if (!exists $schema->{sequence}) { $self->_die("'sequence' missing with 'seq' type"); } my $sequence = $schema->{sequence}; if (!UNIVERSAL::isa($sequence, 'ARRAY')) { $self->_die("Expected array in 'sequence'"); } if (@$sequence != 1) { $self->_die("Expect exactly one element in sequence"); } if (!UNIVERSAL::isa($data, 'ARRAY')) { $self->_error("Non-valid data " . $data . ", expected sequence"); return; } return if ($self->{done}{overload::StrVal($data)}{overload::StrVal($schema)}); $self->{done}{overload::StrVal($data)}{overload::StrVal($schema)} = 1; my $subschema = $sequence->[0]; my $unique = _get_boolean($subschema->{unique}); my %unique_val; my %unique_mapping_val; my $index = 0; for my $elem (@$data) { my $subpath = _append_path($path, $index); $self->_validate($subschema, $elem, $subpath, { unique_mapping_val => \%unique_mapping_val}); if ($unique) { if (exists $unique_val{$elem}) { $self->_error("'$elem' is already used at '$unique_val{$elem}'"); } else { $unique_val{$elem} = $subpath; } } $index++; } } sub validate_map { my($self, $schema, $data, $path, $args) = @_; my $unique_mapping_val; if ($args && $args->{unique_mapping_val}) { $unique_mapping_val = $args->{unique_mapping_val}; } if (!exists $schema->{mapping}) { $self->_die("'mapping' missing with 'map' type"); } my $mapping = $schema->{mapping}; if (!UNIVERSAL::isa($mapping, 'HASH')) { $self->_die("Expected hash in 'mapping'"); } if (!defined $data) { $self->_error("Undefined data, expected mapping"); return; } if (!UNIVERSAL::isa($data, 'HASH')) { $self->_error("Non-valid data " . $data . ", expected mapping"); return; } return if ($self->{done}{overload::StrVal($data)}{overload::StrVal($schema)}); $self->{done}{overload::StrVal($data)}{overload::StrVal($schema)} = 1; my %seen_key; my $default_key_schema; ## Originally this was an each-loop, but this could lead into ## endless recursions, because mapping may be reused in Kwalify, ## thus the each iterator was shared between recursion levels. # while(my($key,$subschema) = each %$mapping) { for my $key (keys %$mapping) { my $subschema = $mapping->{$key}; if ($key eq '=') { # the "default" key $default_key_schema = $subschema; next; } my $subpath = _append_path($path, $key); $self->{path} = $subpath; if (!UNIVERSAL::isa($subschema, 'HASH')) { $self->_die("Expected subschema (a hash)"); } my $required = _get_boolean($subschema->{required}); if (!defined $data->{$key}) { if ($required) { $self->{path} = $path; $self->_error("Expected required key '$key'"); next; } else { $seen_key{$key}++; next; } } my $unique = _get_boolean($subschema->{unique}); if ($unique) { if (defined $unique_mapping_val->{$data->{$key}}->{val} && $unique_mapping_val->{$data->{$key}}->{val} eq $data->{$key}) { $self->_error("'$data->{$key}' is already used at '$unique_mapping_val->{$data->{$key}}->{path}'"); } else { $unique_mapping_val->{$data->{$key}} = { val => $data->{$key}, path => $subpath, }; } } $self->_validate($subschema, $data->{$key}, $subpath); $seen_key{$key}++; } # while(my($key,$val) = each %$data) { for my $key (keys %$data) { my $val = $data->{$key}; my $subpath = _append_path($path, $key); $self->{path} = $subpath; if (!$seen_key{$key}) { if ($default_key_schema) { $self->_validate($default_key_schema, $val, $subpath); } else { $self->_error("Unexpected key '$key'"); } } } } sub _die { my($self, $msg) = @_; $msg = "[$self->{path}] $msg"; die $msg."\n"; } sub _error { my($self, $msg) = @_; $msg = "[$self->{path}] $msg"; push @{$self->{errors}}, $msg; 0; } # Functions: sub _append_path { my($root, $leaf) = @_; $root . ($root !~ m{/$} ? "/" : "") . $leaf; } sub _base_path { my($path) = @_; my($base) = $path =~ m{([^/]+)$}; $base; } sub _get_boolean { my($val) = @_; defined $val && $val =~ m{^(yes|true|1)$}; # XXX check for all boolean trues } 1; __END__ =head1 NAME Kwalify - Kwalify schema for data structures =head1 SYNOPSIS use Kwalify qw(validate); validate($schema, $data); Typically used together with YAML or JSON: use YAML; validate(YAML::LoadFile($schema_file), YAML::LoadFile($data_file)); use JSON; validate(decode_json($schema_data), decode_json($data)); =head1 DESCRIPTION Kwalify is a Perl implementation for validating data structures against the Kwalify schema. For a schema definition, see L, but see also below L. =head2 validate($schema_data, $data) Validate I<$data> according to Kwalify schema specified in I<$schema_data>. Dies if the validation fails. B may be exported. =head1 SCHEMA DEFINITION The original schema definition document is not very specific about types and behaviour. Here's how B implements things: =over =item name The name of the schema. =item desc The description for the rule. It is not used for validation. =item pattern A pattern matching the valid values. Perl regular expressions are used for patterns. This may or may not be compatible with other Kwalify validators, so restrict to "simple" regular expression constructs to be compatible with other validators. =item enum A list of the valid values. =item range A hash with the valid value ranges for types other than B, B, B and B. =over =item max The maximum inclusive. =item min The minimum inclusive. =item max-ex The maximum exclusive. =item min-ex The minimum exclusive. =back =item length Like B but for B and B. =item required A constraint to denote the value is required when B. The default is B. =item type The default B if omitted is B. =over =item str Any defined value which is B a number. Most probably you will want to use B instead of B. =item int A possibly signed integer. Note that scientific notation is not supported, and it is also not clear whether it should be supported. =item float A possibly signed floating value with a mandatory decimal point. Note that scientific notation is also not supported here. =item number A possibly signed floating value with an optional decimal point (so either B or B). Note that scientific notation is also not supported here. =item text Any defined value which is either a B or a B. =item bool The values B, B, and B<1> for true values and the values B, B, and B<0> for false values are allowed. The ruby implementation possibly allows more values, but this is not documented. Note that this definition is problematic, because for example the string B is a true boolean value in Perl. So one should stick to B<0> and B<1> as data values, and probably define an additional B or B to ensure this: type: bool enum: [0, 1] =item date A string matching C<< /^\d{4}-\d{2}-\d{2}$/ >> (i.e. YYYY-MM-DD). Note that no date range checks are done (yet). =item time A string matching C<< /^\d{2}:\d{2}:\d{2}$/ >> (i.e. HH:MM:SS). Note that no time range checks are done (yet). =item timestamp Not supported --- it is not clear what this is supposed to be. =item seq A sequence (list) of rules. =item map A mapping (hash) of rules. The name "B<=>" can be used to apply rules to any key. =item scalar Currently the same as B, but it's not clear if this is correct. Originally defined as all but B and B. =item any Any data type. =back =item unique The value is unique for a B or a B. =item assert Currently not supported by the Perl implementation. =item classname Previously defined what is now B, see L. =item class Currently not used, as there's no genclass action. =item default Currently not used, as there's no genclass action. =back =head1 TECHNICAL NOTES As B is a pure validator and de-coupled from a parser (in fact, it does not need to deal with YAML at all, but just with pure perl data structures), there's no connection to the original validated document. This means that no line numbers are available to the validator. In case of validation errors the validator is only able to show a path-like expression to the data causing the error. =head1 AUTHOR Slaven ReziE, Esrezic@cpan.orgE =head1 COPYRIGHT AND LICENSE Copyright (C) 2006,2007,2008,2009,2010,2015 by Slaven ReziE This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =head1 SEE ALSO L, L. Other non-XML schema languages: L =cut p5-Kwalify-1.24/lib/Schema/000077500000000000000000000000001464725437500153575ustar00rootroot00000000000000p5-Kwalify-1.24/lib/Schema/Kwalify.pm000066400000000000000000000017651464725437500173340ustar00rootroot00000000000000# -*- mode: cperl; coding: latin-2 -*- # # Author: Slaven Rezic # # Copyright (C) 2006,2007,2020 Slaven Rezic. All rights reserved. # This package is free software; you can redistribute it and/or # modify it under the same terms as Perl itself. # # Mail: slaven@rezic.de # WWW: http://www.rezic.de/eserte/ # package Schema::Kwalify; use strict; use warnings; use vars qw($VERSION); $VERSION = '1.05'; use Kwalify qw(); sub new { bless {}, shift; } sub validate { my($self, $schema, $data) = @_; Kwalify::validate($schema, $data); } 1; __END__ =encoding iso-8859-2 =head1 NAME Schema::Kwalify - Kwalify implementation in the Schema:: namespace =head1 SYNOPSIS Not yet =head1 DESCRIPTION I expect that there will be other schema languages for data structures defined. It would be nice if the implementations would use the B namespace, and that these modules share a common interface. =head1 AUTHOR Slaven Reziæ, Esrezic@cpan.orgE =head1 SEE ALSO L. =cut p5-Kwalify-1.24/pkwalify000077500000000000000000000114041464725437500151650ustar00rootroot00000000000000#!/usr/bin/perl -w # -*- mode: cperl; coding: latin-2 -*- # # Author: Slaven Rezic # # Copyright (C) 2006,2007,2008,2009,2015 Slaven Rezic. All rights reserved. # This program is free software; you can redistribute it and/or # modify it under the same terms as Perl itself. # # Mail: slaven@rezic.de # WWW: http://www.rezic.de/eserte/ # use strict; use vars qw($VERSION); $VERSION = '1.13'; use Kwalify; use Getopt::Long; my $schema_file; my $parse_mod; my $silent; my $show_version; GetOptions("f=s" => \$schema_file, "m|module=s" => \$parse_mod, "s|silent" => \$silent, "v|version" => \$show_version, "h|help" => sub { print usage(); exit 0 }, ) or die usage(); if ($show_version) { version(); exit; } if (!defined $schema_file) { die usage("-f option is mandatory"); } my $data_file = shift @ARGV; if (!defined $data_file) { die usage("datafile is mandatory"); } my(@schema) = read_file($schema_file); if (@schema != 1) { print "<$schema_file> does not contain exactly one schema, cannot handle this."; exit 1; } my $schema = $schema[0]; my(@data) = read_file($data_file); my $errors = 0; my $document_index = 0; for my $data (@data) { my $document_label = $data_file . '#' . $document_index; eval { Kwalify::validate($schema, $data) }; if ($@) { print "$document_label: INVALID\n$@\n"; $errors++; } else { if (!$silent) { print "$document_label: valid.\n"; } } $document_index++; } exit $errors; sub read_file { my $file = shift; my @try_order; if (defined $parse_mod) { @try_order = ($parse_mod); } elsif ($file =~ m{\.json$}i) { @try_order = ('JSON::XS', 'JSON', 'YAML::Syck', 'YAML', 'YAML::XS'); } else { # yaml or don't know @try_order = ('YAML::Syck', 'YAML', 'YAML::XS', 'JSON::XS', 'JSON'); } my @errors; for my $try (@try_order) { if ($try eq 'YAML::Syck' && eval { require YAML::Syck; 1 }) { my @data = eval { YAML::Syck::LoadFile($file) }; return @data if !$@; push @errors, $@; } elsif ($try eq 'YAML::XS' && eval { require YAML::XS; 1 }) { my @data = eval { YAML::XS::LoadFile($file) }; return @data if !$@; push @errors, $@; } elsif ($try eq 'YAML' && eval { require YAML; 1 }) { my @data = eval { YAML::LoadFile($file) }; return @data if !$@; push @errors, $@; } elsif ($try eq 'JSON::XS' && eval { require JSON::XS; 1 }) { my @data = eval { JSON::XS::decode_json(slurp_file($file)) }; return @data if !$@; push @errors, $@; } elsif ($try eq 'JSON' && eval { require JSON; 1 }) { my $data = eval { my $json = slurp_file($file); if (defined &JSON::from_json) { JSON::from_json($json, {utf8 => 1}); } else { # old style JSON::jsonToObj($json); } }; return ($data) if $data && !$@; push @errors, $@; } else { push @errors, "Unsupported module $try"; } } if (!@errors) { die "Cannot parse <$file>. Try to install a YAML and/or JSON parsing module first.\n"; } else { die "Cannot parse <$file>. Cumulated errors:\n" . join("\n", @errors) . "\n"; } } sub slurp_file { my $file = shift; open FH, "< $file" or die "Can't open <$file>: $!"; local $/ = undef; my $json = ; close FH; $json; } sub usage { my($msg) = @_; if (defined $msg) { $msg .= "\n"; } else { $msg = ""; } < validates the data from I (which may be a L or L file) against a schema defined with I (which also may be a YAML or JSON file). It is required that either L, L or L is installed to parse YAML files, or either L or L for JSON files. Or the module specified on the command-line. The program returns the number of errors found in the datafile. An exit status 0 means no errors. =head2 OPTIONS =over =item -f I Specify a schema file, either as YAML or JSON. Required. =item -m I Specify the YAML or JSON Perl module to use. Valid modules are: L, L, L, L and L. =item -s Be silent if the document is valid. =item -v Show script and module versions and exit. =item -h --help Show summary of options. =back =head1 AUTHOR Slaven Reziæ, Esrezic@cpan.orgE =head1 SEE ALSO L, L, L, L, L, L, L. =cut p5-Kwalify-1.24/t/000077500000000000000000000000001464725437500136545ustar00rootroot00000000000000p5-Kwalify-1.24/t/Kwalify.t000077500000000000000000000455111464725437500154600ustar00rootroot00000000000000#!/usr/bin/perl -w # -*- perl -*- # # Author: Slaven Rezic # use strict; BEGIN { if (!eval q{ use Test::More; 1; }) { print "1..0 # skip: no Test::More module\n"; exit; } if ($] < 5.005) { print "1..0 # skip: test works only with perl 5.005 or better\n"; exit; } } my $yaml_mod_tests; BEGIN { $yaml_mod_tests = 38; plan tests => 2 + $yaml_mod_tests + 60; } BEGIN { use_ok('Kwalify', 'validate'); } my @w; $SIG{__WARN__} = sub { push @w, @_ }; use_ok('Schema::Kwalify'); my $use_yaml_module; for my $mod (qw(YAML::XS YAML::PP)) { # YAML::Syck currently does not work --- https://github.com/toddr/YAML-Syck/issues/52 if (eval qq{ require $mod; 1 }) { if ($mod eq 'YAML::PP') { no strict 'refs'; *YAML_Load = sub { YAML::PP->new(cyclic_refs => 'allow')->load_string($_[0]); }; } else { no strict 'refs'; *YAML_Load = \&{$mod . '::Load'}; } $use_yaml_module = $mod; last; } } sub is_valid_yaml { my($schema, $document, $testname) = @_; local $Test::Builder::Level = $Test::Builder::Level+1; ok(validate(YAML_Load($schema), YAML_Load($document)), $testname); } sub is_invalid_yaml { my($schema, $document, $errors, $testname) = @_; local $Test::Builder::Level = $Test::Builder::Level+1; ok(!eval { validate(YAML_Load($schema), YAML_Load($document)) }, $testname); for my $error (@$errors) { if (UNIVERSAL::isa($error, 'HASH')) { my($pattern, $testname) = @{$error}{qw(pattern testname)}; like($@, $pattern, $testname); } else { like($@, $error); } } } SKIP: { skip("Need a YAML loading module for tests", $yaml_mod_tests) if !$use_yaml_module; my $schema01 = <<'EOF'; type: seq sequence: - type: str EOF my $document01a = <<'EOF'; - foo - bar - baz EOF is_valid_yaml($schema01, $document01a, "sequence of str"); my $schema01b = <<'EOF'; type: seq sequence: [{}] EOF is_valid_yaml($schema01b, $document01a, "sequence with default type (str)"); my $document01b = <<'EOF'; - foo - 123 - baz EOF is_invalid_yaml($schema01,$document01b, [qr{\Q[/1] Non-valid data '123', expected a str}], "Non valid data, int in sequence of str"); my $schema02 = <<'EOF'; type: map mapping: name: type: str required: yes email: type: str pattern: /@/ age: type: int birth: type: date EOF my $document02a = <<'EOF'; name: foo email: foo@mail.com age: 20 birth: 1985-01-01 EOF is_valid_yaml($schema02, $document02a, "mapping"); my $document02b = <<'EOF'; name: foo email: foo(at)mail.com age: twenty birth: Jun 01, 1985 EOF is_invalid_yaml($schema02, $document02b, [qr{\Q[/birth] Non-valid data 'Jun 01, 1985', expected a date (YYYY-MM-DD)}, qr{\Q[/age] Non-valid data 'twenty', expected an int}, qr{\Q[/email] Non-valid data 'foo(at)mail.com' does not match /@/}, ], "invalid mapping"); my $schema03 = <<'EOF'; type: seq sequence: - type: map mapping: name: type: str required: true email: type: str EOF my $document03a = <<'EOF'; - name: foo email: foo@mail.com - name: bar email: bar@mail.net - name: baz email: baz@mail.org EOF is_valid_yaml($schema03, $document03a, "sequence of mapping"); my $document03b = <<'EOF'; - name: foo email: foo@mail.com - naem: bar email: bar@mail.net - name: baz mail: baz@mail.org EOF is_invalid_yaml($schema03, $document03b, [qr{\Q[/1] Expected required key 'name'}, qr{\Q[/1/naem] Unexpected key 'naem'}, qr{\Q[/2/mail] Unexpected key 'mail'}, ]); my $schema04 = <<'EOF'; type: map mapping: company: type: str required: yes email: type: str employees: type: seq sequence: - type: map mapping: code: type: int required: yes name: type: str required: yes email: type: str EOF my $document04a = <<'EOF'; company: Kuwata lab. email: webmaster@kuwata-lab.com employees: - code: 101 name: foo email: foo@kuwata-lab.com - code: 102 name: bar email: bar@kuwata-lab.com EOF is_valid_yaml($schema04, $document04a, "mapping of sequence"); my $document04b = <<'EOF'; company: Kuwata Lab. email: webmaster@kuwata-lab.com employees: - code: A101 name: foo email: foo@kuwata-lab.com - code: 102 name: bar mail: bar@kuwata-lab.com EOF is_invalid_yaml($schema04, $document04b, [qr{\Q[/employees/0/code] Non-valid data 'A101', expected an int}, qr{\Q[/employees/1/mail] Unexpected key 'mail'}, ]); my $schema05 = <<'EOF'; type: seq # new rule sequence: - type: map # new rule mapping: name: type: str # new rule required: yes email: type: str # new rule required: yes pattern: /@/ password: type: text # new rule length: { max: 16, min: 8 } age: type: int # new rule range: { max: 30, min: 18 } # or assert: 18 <= val && val <= 30 blood: type: str # new rule enum: - A - B - O - AB birth: type: date # new rule memo: type: any # new rule EOF my $document05a = <<'EOF'; - name: foo email: foo@mail.com password: xxx123456 age: 20 blood: A birth: 1985-01-01 - name: bar email: bar@mail.net age: 25 blood: AB birth: 1980-01-01 EOF is_valid_yaml($schema05, $document05a, "Many rules"); my $document05b = <<'EOF'; - name: foo email: foo(at)mail.com password: xxx123 age: twenty blood: a birth: 1985-01-01 - given-name: bar family-name: Bar email: bar@mail.net age: 15 blood: AB birth: 1980/01/01 EOF is_invalid_yaml($schema05, $document05b, [ qr{\Q[/0/blood] 'a': invalid blood value}, qr{\Q[/0/email] Non-valid data 'foo(at)mail.com' does not match /@/}, qr{\Q[/0/password] 'xxx123' is too short (length 6 < min 8)}, qr{\Q[/0/age] Non-valid data 'twenty', expected an int}, qr{\Q[/0/age] 'twenty' is too small (< min 18)}, qr{\Q[/1/birth] Non-valid data '1980/01/01', expected a date (YYYY-MM-DD)}, qr{\Q[/1] Expected required key 'name'}, qr{\Q[/1/age] '15' is too small (< min 18)}, qr{\Q[/1/given-name] Unexpected key 'given-name'}, qr{\Q[/1/family-name] Unexpected key 'family-name'}, ]); my $schema06 = <<'EOF'; type: seq sequence: - type: map required: yes mapping: name: type: str required: yes unique: yes email: type: str groups: type: seq sequence: - type: str unique: yes EOF my $document06a = <<'EOF'; - name: foo email: admin@mail.com groups: - users - foo - admin - name: bar email: admin@mail.com groups: - users - admin - name: baz email: baz@mail.com groups: - users EOF is_valid_yaml($schema06, $document06a, "unique"); my $document06b = <<'EOF'; - name: foo email: admin@mail.com groups: - foo - users - admin - foo - name: bar email: admin@mail.com groups: - admin - users - name: bar email: baz@mail.com groups: - users EOF is_invalid_yaml($schema06, $document06b, [qr{\Q[/0/groups/3] 'foo' is already used at '/0/groups/0'}, qr{\Q[/2/name] 'bar' is already used at '/1/name'}, ]); # testcase for RT #48800 my $document_unique = <<'EOF'; - name: foo - name: bar - name: barf - name: bar EOF is_invalid_yaml($schema06, $document_unique, [qr{\Q[/3/name] 'bar' is already used at '/1/name'}, ]); # Recursive mappings: my $recursive_schema = <<'EOF'; name: MAIN type: map required: yes mapping: &main-rule "type": type: str enum: - map - str "mapping": name: MAPPING type: map mapping: =: type: map mapping: *main-rule name: MAIN #required: yes EOF my $non_recursive_document = <<'EOF'; type: map mapping: recursive_hash: type: map mapping: bla: type: str foo: type: str another_key: type: str EOF my $recursive_maps = <<'EOF'; type: map mapping: recursive_hash: &recursive type: map mapping: bla: type: str foo: type: str bar: *recursive another_key: type: str EOF is_valid_yaml($recursive_schema, $non_recursive_document, "valid data against schema with recursive rules (no endless loop)"); is_valid_yaml($recursive_schema, $recursive_maps, "valid recursive data against schema with recursive rules (no endless loop)"); } { my $schema06_pl = { 'sequence' => [ { 'mapping' => { 'email' => { 'type' => 'str' }, 'groups' => { 'sequence' => [ { 'unique' => 'yes', 'type' => 'str' } ], 'type' => 'seq' }, 'name' => { 'unique' => 'yes', 'required' => 'yes', 'type' => 'str' } }, 'required' => 'yes', 'type' => 'map' } ], 'type' => 'seq' }; my $document06a_pl = [ { 'email' => 'admin@mail.com', 'groups' => [ 'users', 'foo', 'admin' ], 'name' => 'foo' }, { 'email' => 'admin@mail.com', 'groups' => [ 'users', 'admin' ], 'name' => 'bar' }, { 'email' => 'baz@mail.com', 'groups' => [ 'users' ], 'name' => 'baz' } ]; my $document06b_pl = [ { 'email' => 'admin@mail.com', 'groups' => [ 'foo', 'users', 'admin', 'foo' ], 'name' => 'foo' }, { 'email' => 'admin@mail.com', 'groups' => [ 'admin', 'users' ], 'name' => 'bar' }, { 'email' => 'baz@mail.com', 'groups' => [ 'users' ], 'name' => 'bar' } ]; ok(validate($schema06_pl, $document06a_pl), "valid data against perl schema"); eval { validate($schema06_pl, $document06b_pl) }; ok($@, "invalid data against perl schema"); } { # test length/range min/max-ex # (no tests in original document) my $schema_ex = { type => "map", mapping => { password => { type => "text", length => { 'max-ex' => 16, 'min-ex' => 8 }, }, age => { type => "int", range => { 'max-ex' => 30, 'min-ex' => 18 }, }, } }; my $document_length_min_ex_pass = { password => "123456789" }; ok(validate($schema_ex, $document_length_min_ex_pass), "min-ex length pass"); my $document_length_min_ex_fail = { password => "12345678" }; eval { validate($schema_ex, $document_length_min_ex_fail) }; like($@, qr{\Qis too short (length 8 <= min 8)}, "min-ex length fail"); my $document_length_max_ex_pass = { password => "123456789012345" }; ok(validate($schema_ex, $document_length_max_ex_pass), "max-ex length pass"); my $document_length_max_ex_fail = { password => "1234567890123456" }; eval { validate($schema_ex, $document_length_max_ex_fail) }; like($@, qr{\Qis too long (length 16 >= max 16)}, "max-ex length fail"); ###################################################################### my $document_range_min_ex_pass = { age => 19 }; ok(validate($schema_ex, $document_range_min_ex_pass), "min-ex range pass"); my $document_range_min_ex_fail = { age => 18 }; eval { validate($schema_ex, $document_range_min_ex_fail) }; like($@, qr{\Qis too small (<= min 18)}, "min-ex range fail"); my $document_range_max_ex_pass = { age => 29 }; ok(validate($schema_ex, $document_range_max_ex_pass), "max-ex range pass"); my $document_range_max_ex_fail = { age => 30 }; eval { validate($schema_ex, $document_range_max_ex_fail) }; like($@, qr{\Qis too large (>= max 30)}, "max-ex range fail"); } { # missing length/range max tests my $schema = { type => "map", mapping => { password => { type => "text", length => { 'max' => 16, 'min-ex' => 8 }, }, age => { type => "int", range => { 'max' => 16, 'min-ex' => 8 }, }, } }; my $document_length_max_pass = { password => "1234567890123456" }; ok(validate($schema, $document_length_max_pass), "max length pass"); my $document_length_max_fail = { password => "12345678901234567" }; eval { validate($schema, $document_length_max_fail) }; like($@, qr{\Qis too long (length 17 > max 16)}, "max length fail"); my $document_range_max_pass = { age => 16 }; ok(validate($schema, $document_range_max_pass), "max range pass"); my $document_range_max_fail = { age => 17 }; eval { validate($schema, $document_range_max_fail) }; like($@, qr{\Qis too large (> max 16)}, "max range fail"); } { ok(validate({type=>"text", name=>"A schema name", classname=>"TestClass", # the old now undocumented "classname" desc=>"Just testing the description.\nReally!", }, "foo"), "Passing name/classname/desc"); } { ok(validate({type=>"text", name=>"A schema name", class=>"TestClass", # the new "class" (instead of "classname") desc=>"Just testing the description.\nReally!", }, "foo"), "Passing name/class/desc"); } { # Some validation tests, negative eval { validate({type => "text"}, [qw(a ref is not a text)]) }; like($@, qr{Non-valid data}, "a ref is not a text"); eval { validate({type => "text"}, undef) }; like($@, qr{Non-valid data.*undef}, "undef is not a text"); eval { validate({type => "str"}, [qw(a ref is not a str)]) }; like($@, qr{Non-valid data}, "a str is not a text"); eval { validate({type => "str"}, undef) }; like($@, qr{Non-valid data.*undef}, "undef is not a str"); eval { validate({type => "str"}, 1.2) }; like($@, qr{Non-valid data}, "a number is not a str"); eval { validate({type => "float"}, "xyz") }; like($@, qr{Non-valid data}, "a non-float"); eval { validate({type => "number"}, "xyz") }; like($@, qr{Non-valid data}, "a non-number"); eval { validate({type => "bool"}, "fasle") }; like($@, qr{Non-valid data}, "a non-bool"); ## Not clear what a "time" is actually... #eval { validate({type => "time"}, "123:45:67") }; #like($@, qr{Non-valid data}, "a non-time"); } { # Some validation tests, positive for (0, 1, 'yes', 'no', 'true', 'false') { ok validate({type => 'bool'}, $_), "validate '$_' as bool"; } ok validate({type => 'float'}, 3.141592653), 'validate float'; ok validate({type => 'number'}, 3.141592653), 'validate number'; ok validate({type => 'time'}, '12:34:56'), 'validate time'; } { # Various schema error conditions eval { validate([qw(schema not a hash)], {}) }; like($@, qr{Schema structure must be a hash reference}, "schema must be hash"); eval { validate({type=>"unknown"},{}) }; like($@, qr{Invalid or unimplemented type .*unknown}, "unknown type"); eval { validate({type=>"text", length => "foo"}, "foo") }; like($@, qr{length.* must be a hash with keys max and/or min}, "invalid length spec"); eval { validate({type=>"text", enum=>"not an array"}, "foo") }; like($@, qr{must be an array}, "invalid enum spec"); eval { validate({type=>"text", range => "foo"}, "foo") }; like($@, qr{range.* must be a hash with keys max and/or min}, "invalid range spec"); eval { validate({type=>"text", unknown_key => "foo"}, "foo") }; like($@, qr{Unexpected key 'unknown_key' in type specification}, "unknown key in type"); eval { validate({type=>"int", range=>{foo => 1}}, "foo") }; like($@, qr{Unexpected key 'foo' in range specification}, "unknown key in range"); eval { validate({type=>"int", length=>{foo => 1}}, "foo") }; like($@, qr{Unexpected key 'foo' in length specification}, "unknown key in length"); eval { validate({type=>"map", mapping=> {foo=>{type=>"text"}} }, []) }; like($@, qr{Non-valid data .*, expected mapping}, "expected hash in data"); eval { validate({type=>'seq'}, []) }; like($@, qr{'sequence' missing with 'seq' type}, 'wrong seq in schema'); eval { validate({type=>'seq',sequence=>"this is not a sequence"}, []) }; like($@, qr{Expected array in 'sequence'}, 'wrong seq in schema'); eval { validate({type=>'seq',sequence=>['one','two']}, []) }; like($@, qr{Expect exactly one element in sequence}, 'wrong seq in schema'); eval { validate({type=>'seq',sequence=>[{type => 'any'}]}, 'no array') }; like($@, qr{Non-valid data .*, expected sequence}, 'wrong data, no sequence'); eval { validate({type=>'map'}, []) }; like($@, qr{mapping' missing with 'map' type}, 'wrong map in schema'); eval { validate({type=>'map',mapping=>"this is not a mapping"}, []) }; like($@, qr{Expected hash in 'mapping'}, 'wrong map in schema'); eval { validate({type=>'map',mapping=>{key => {type => 'any' }}}, undef) }; like($@, qr{Undefined data, expected mapping}, 'wrong data, undefined'); eval { validate({type=>'map',mapping=>{key => {type => 'any' }}}, 'something else') }; like($@, qr{Non-valid data .*, expected mapping}, 'wrong data, no mapping'); } { # Schema::Kwalify tests my $sk = Schema::Kwalify->new; isa_ok($sk, "Schema::Kwalify"); ok($sk->validate({type=>"text"},"foo"), "Simple Schema::Kwalify validation"); eval { $sk->validate({type=>"text"},[]) }; isnt($@, "", "Simple Schema::Kwalify failure"); } { # Test any with additional checks my $schema = { type => "any", pattern => "CODE", }; ok(validate($schema, "CODE"), "type any with additional check, successful"); eval { validate($schema, "CoDe"); }; like($@, qr{Non-valid data 'CoDe' does not match /CODE/}, "type any with additional check, failure"); } { my $schema = { type => "any", enum => [1,2,undef], }; ok(validate($schema, 1), "enum with defined value"); ok(validate($schema, undef), "enum with undefined value"); } { my $schema = { type => "any", pattern => '/^(|something)$/', }; ok(validate($schema, 'something'), "legally undefined pattern"); ok(validate($schema, undef), "legally undefined pattern"); } SKIP: { skip("Don't bother with warnings on old perls without warnings.pm", 1) if $] < 5.006; is("@w", "", "No warnings expected"); } __END__ p5-Kwalify-1.24/t/compat.t000077500000000000000000000033331464725437500153310ustar00rootroot00000000000000#!/usr/bin/perl -w # -*- perl -*- # # Author: Slaven Rezic # # This test is just for older perls (5.004, 5.005) # because the other test scripts in this suite have # too large prerequisites (Test::More, \Q in regexpes...) use strict; use Kwalify qw(validate); use Test; plan tests => 2; { my $schema06_pl = { 'sequence' => [ { 'mapping' => { 'email' => { 'type' => 'str' }, 'groups' => { 'sequence' => [ { 'unique' => 'yes', 'type' => 'str' } ], 'type' => 'seq' }, 'name' => { 'unique' => 'yes', 'required' => 'yes', 'type' => 'str' } }, 'required' => 'yes', 'type' => 'map' } ], 'type' => 'seq' }; my $document06a_pl = [ { 'email' => 'admin@mail.com', 'groups' => [ 'users', 'foo', 'admin' ], 'name' => 'foo' }, { 'email' => 'admin@mail.com', 'groups' => [ 'users', 'admin' ], 'name' => 'bar' }, { 'email' => 'baz@mail.com', 'groups' => [ 'users' ], 'name' => 'baz' } ]; my $document06b_pl = [ { 'email' => 'admin@mail.com', 'groups' => [ 'foo', 'users', 'admin', 'foo' ], 'name' => 'foo' }, { 'email' => 'admin@mail.com', 'groups' => [ 'admin', 'users' ], 'name' => 'bar' }, { 'email' => 'baz@mail.com', 'groups' => [ 'users' ], 'name' => 'bar' } ]; ok(validate($schema06_pl, $document06a_pl)); eval { validate($schema06_pl, $document06b_pl) }; ok($@); } __END__ p5-Kwalify-1.24/t/pkwalify.t000077500000000000000000000171051464725437500156760ustar00rootroot00000000000000#!/usr/bin/perl -w # -*- perl -*- # # Author: Slaven Rezic # use strict; use FindBin; use Getopt::Long; BEGIN { if (!eval q{ use Test::More; use File::Temp; use File::Spec 0.8; # rel2abs 1; }) { print "1..0 # skip: no Test::More, File::Spec and/or File::Temp modules\n"; exit; } } require blib; # just to get blib's VERSION my $skip_warnings_test = $blib::VERSION < 1.01; # Test cases with single documents my @yaml_mod_defs = ( ["schema05.yaml", "document05a.yaml", 1], ["schema05.yaml", "document05b.yaml", 0], ); # Test cases with multiple documents (by combining single documents) my %combined_document; { for my $def (["invalid_diff", "document05a.yaml", "document05b.yaml"], ["valid_same", "document05a.yaml", "document05a.yaml"], ["invalid_same", "document05b.yaml", "document05b.yaml"], ["invalid_schema", "schema05.yaml", "schema05.yaml"], ) { my($newname, @yaml) = @$def; my($fh,$outfile) = File::Temp::tempfile(SUFFIX => ".yaml", UNLINK => 1); if (!$fh) { die "Cannot create temporary file: $!"; } # fix possible problem if somebody sets TMPDIR=. $outfile = File::Spec->rel2abs($outfile) if !File::Spec->file_name_is_absolute($outfile); for my $document (@yaml) { print $fh "--- \n"; { open IN, "$FindBin::RealBin/testdata/$document" or die $!; local $/ = undef; print $fh ; close IN; } } $combined_document{$newname} = $outfile; close $fh; } } # Test cases for YAML/YAML::XS/YAML::Syck (schema+document combinations) push @yaml_mod_defs, ( [$combined_document{"invalid_schema"}, "document05a.yaml", 0], ["schema05.yaml", $combined_document{"invalid_diff"}, 0], ["schema05.yaml", $combined_document{"valid_same"}, 1], ["schema05.yaml", $combined_document{"invalid_same"}, 0], ); my $can_yaml = (eval { require YAML::Syck; 1 } || eval { require YAML::XS; 1 } || eval { require YAML; 1 }); if ($can_yaml) { *YAML_LoadFile = defined &YAML::Syck::LoadFile ? \&YAML::Syck::LoadFile : defined &YAML::XS::LoadFile ? \&YAML::XS::LoadFile : \&YAML::LoadFile; } my $can_json = (eval { require JSON::XS; 1 } || eval { require JSON; 1 }); if ($can_json) { *JSON_encode = defined &JSON::XS::encode_json ? \&JSON::XS::encode_json : \&JSON::encode_json; } # Test cases for JSON (generated from YAML documents) my @json_defs = (); if ($can_yaml && $can_json) { my %json_equivalent; for my $file ('schema05.yaml', 'document05a.yaml', 'document05b.yaml') { my($tmpfh,$tmpfile) = File::Temp::tempfile(SUFFIX => '.json', UNLINK => 1); my $data = YAML_LoadFile("$FindBin::RealBin/testdata/$file"); print $tmpfh JSON_encode($data); close $tmpfh or die "Can't write JSON data to $tmpfile: $!"; $json_equivalent{$file} = $tmpfile; } push @json_defs, ([$json_equivalent{'schema05.yaml'}, $json_equivalent{'document05a.yaml'}, 1], [$json_equivalent{'schema05.yaml'}, $json_equivalent{'document05b.yaml'}, 0], ); } my $v; GetOptions("v!") or die "usage: $0 [-v]"; my $tests_per_case = 3; plan tests => 13 + $tests_per_case*(scalar(@yaml_mod_defs) + scalar(@json_defs)); my $script = "$FindBin::RealBin/../blib/script/pkwalify"; my @cmd = ($^X, "-Mblib=$FindBin::RealBin/..", $script, "-s"); SKIP: { skip("Need YAML, YAML::XS or YAML::Syck for tests", $tests_per_case*scalar(@yaml_mod_defs)) if !$can_yaml; for my $def (@yaml_mod_defs) { any_test($def); } } SKIP: { skip("Need JSON or JSON::XS for tests", $tests_per_case*scalar(@json_defs)) if !$can_json; for my $def (@json_defs) { any_test($def); } } { my $result = run_pkwalify(); is($result->{success}, 0, "No success without options"); SKIP: { skip("Skip STDERR test", 1) if !$result->{can_capture}; like($result->{stderr}, qr{-f option is mandatory}, "usage -f"); } } { my $result = run_pkwalify("-xxx"); is($result->{success}, 0, "Invalid option"); SKIP: { skip("Skip STDERR test", 1) if !$result->{can_capture}; like($result->{stderr}, qr{usage}, "got usage"); } } { my $result = run_pkwalify("-f", "foo"); is($result->{success}, 0, "Missing data file"); SKIP: { skip("Skip STDERR test", 1) if !$result->{can_capture}; like($result->{stderr}, qr{datafile is mandatory}, "usage datafile"); } } { my $result = run_pkwalify("-f", $0, $0); is($result->{success}, 0, "No YAML/JSON file"); SKIP: { skip("Skip STDERR test", 1) if !$result->{can_capture}; like($result->{stderr}, qr{cannot parse}i, "cannot parse file"); } } SKIP: { skip("Need YAML, YAML::XS or YAML::Syck for tests", 4) if !$can_yaml; my $schema_file = "schema05.yaml"; my $data_file = "document05a.yaml"; for ($schema_file, $data_file) { if (!File::Spec->file_name_is_absolute($_)) { $_ = "$FindBin::RealBin/testdata/$_"; } } { my @args = ('-f', $schema_file, $data_file, '-s'); my $result = run_pkwalify_non_silent(@args); is($result->{success}, 1, "Success with -s"); SKIP: { skip("Skip STDOUT test", 1) if !$result->{can_capture}; is($result->{stdout}, "", "silent output"); } } { my @args = ('-f', $schema_file, $data_file); my $result = run_pkwalify_non_silent(@args); is($result->{success}, 1, "Success without -s"); SKIP: { skip("Skip STDOUT test", 1) if !$result->{can_capture}; like($result->{stdout}, qr{\Q: valid.}, "non-silent output for validity"); } } } sub any_test { my($def) = @_; local $Test::Builder::Level = $Test::Builder::Level+1; my($schema_file, $data_file, $expect_validity) = @$def; for ($schema_file, $data_file) { if (!File::Spec->file_name_is_absolute($_)) { $_ = "$FindBin::RealBin/testdata/$_"; } } my @args = ('-f' => $schema_file, $data_file); my $result = run_pkwalify(@args); my($valid, $stdin, $stdout, $stderr, $can_capture) = @{$result}{qw(success stdin stdout stderr can_capture)}; if ($can_capture) { if (!$valid) { diag "STDOUT=$stdout\nSTDERR=$stderr\n" if $v; } SKIP: { skip("Older blib versions write to STDERR", 2) if $skip_warnings_test; if ($valid) { is($stdout, "", "No warnings in @args"); } else { isnt($stdout, "", "There are warnings in @args"); } is($stderr, "", "Nothing in STDERR"); } } else { SKIP: { skip("No stdout/stderr tests without IPC::Run", 2) } } is($valid, $expect_validity, "@args") or diag("@args"); } sub _run_pkwalify { my(@cmd) = @_; my($success,$stdin,$stdout,$stderr,$can_capture); if (eval { require IPC::Run; 1 }) { $can_capture = 1; $success = IPC::Run::run(\@cmd, \$stdin, \$stdout, \$stderr) ? 1 : 0; } else { *OLDOUT = *OLDOUT; # cease -w *OLDERR = *OLDERR; # cease -w open(OLDOUT, ">&STDOUT") or die $!; open(OLDERR, ">&STDERR") or die $!; open(STDOUT, ">".File::Spec->devnull) or die $!; open(STDERR, ">".File::Spec->devnull) or die $!; system(@cmd); close STDERR; close STDOUT; open(STDERR, ">&OLDERR") or die $!; open(STDOUT, ">&OLDOUT") or die $!; $success = $? == 0 ? 1 : 0; } return { success => $success, stdin => $stdin, stdout => $stdout, stderr => $stderr, can_capture => $can_capture, }; } sub run_pkwalify { my(@args) = @_; my @cmd = (@cmd, @args); _run_pkwalify(@cmd); } sub run_pkwalify_non_silent { my(@args) = @_; my(@cmd) = grep { $_ ne '-s' } @cmd; push @cmd, @args; _run_pkwalify(@cmd); } # Should be last because of STDERR redirection { open(STDERR, ">" . File::Spec->devnull); system($^X, "-c", "-Mblib=$FindBin::RealBin/..", $script); ok($?==0, "$script compiles OK"); } __END__ p5-Kwalify-1.24/t/required-no.t000066400000000000000000000016501464725437500162750ustar00rootroot00000000000000#!/usr/bin/perl -w # -*- cperl -*- # # Author: Slaven Rezic # use strict; BEGIN { if (!eval q{ use Test::More; 1; }) { print "1..0 # skip: no Test::More module\n"; exit; } if ($] < 5.005) { print "1..0 # skip: test works only with perl 5.005 or better\n"; exit; } } use Kwalify qw(validate); plan tests => 1; # from https://github.com/eserte/p5-Kwalify/issues/1 # translated yaml to perl my $schema = { 'mapping' => { 'foo' => { 'mapping' => { 'bar' => { 'sequence' => [ { 'required' => 'no', 'type' => 'str' } ], 'required' => 'no', 'type' => 'seq' } }, 'required' => 'yes', 'type' => 'map' } }, 'desc' => 'test for tilde', 'name' => 'test', 'type' => 'map' }; my $data = { 'foo' => { 'bar' => undef } }; ok validate $schema, $data; __END__ p5-Kwalify-1.24/t/testdata/000077500000000000000000000000001464725437500154655ustar00rootroot00000000000000p5-Kwalify-1.24/t/testdata/META-spec-1.3.yml000066400000000000000000000053521464725437500201720ustar00rootroot00000000000000# META.yml specification 1.3 type: map mapping: meta-spec: type: map required: yes comment: exact specification missing mapping: version: &optional_version type: text pattern: /^[\d_]+(\.[\d_]+)*$/ required: no url: &required_url type: text pattern: /^((ftp|https?):\/\/|mailto:.*@)/ required: yes name: type: text required: yes version: &required_version type: text pattern: /^[\d_]+(\.[\d_]+)*$/ required: yes abstract: type: text required: yes author: type: seq comment: preferred form is "author-name " required: yes sequence: - type: text license: type: text required: yes enum: - perl - gpl - lgpl - artistic - bsd - open_source - unrestricted - restrictive distribution_type: type: text required: no enum: - module - script requires: &requires type: map required: no mapping: =: &required_version_range type: text pattern: "/^((>|>=|==|!=|<|<=)\s*)?[\d_]+(\.[\d_]+)*(,\s*((>|>=|==|!=|<|<=)\s*)?[\d_]+(\.[\d_]+)*)*$/" required: yes recommends: *requires optional_features: type: map mapping: =: type: map mapping: description: type: text requires: *requires build_requires: *requires conflicts: *requires requires_packages: comment: exact specification missing type: any requires_os: comment: exact specification missing type: any excludes_os: comment: exact specification missing type: any build_requires: *requires conflicts: *requires dynamic_config: type: bool required: no provides: type: map required: no mapping: =: type: map mapping: file: type: text version: *optional_version no_index: &no_index type: map required: no mapping: file: &no_index_element type: seq required: no sequence: - type: text directory: *no_index_element package: *no_index_element namespace: *no_index_element private: *no_index keywords: type: seq required: no sequence: - type: text resources: type: map required: no mapping: license: &optional_url type: text pattern: /^((ftp|https?|git):\/\/|mailto:.*@)/ required: no homepage: *optional_url bugtracker: *optional_url repository: *optional_url # missing in spec =: *optional_url generated_by: type: text required: yes p5-Kwalify-1.24/t/testdata/document05a.yaml000066400000000000000000000003211464725437500204710ustar00rootroot00000000000000- name: foo email: foo@mail.com password: xxx123456 age: 20 blood: A birth: 1985-01-01 - name: bar email: bar@mail.net age: 25 blood: AB birth: 1980-01-01 p5-Kwalify-1.24/t/testdata/document05b.yaml000066400000000000000000000003531464725437500204770ustar00rootroot00000000000000- name: foo email: foo(at)mail.com password: xxx123 age: twenty blood: a birth: 1985-01-01 - given-name: bar family-name: Bar email: bar@mail.net age: 15 blood: AB birth: 1980/01/01 p5-Kwalify-1.24/t/testdata/kwalify.yaml000066400000000000000000000030211464725437500200130ustar00rootroot00000000000000name: MAIN type: map required: yes mapping: &main-rule "name": type: str "desc": type: str "classname": type: str "type": type: str #required: yes enum: - seq #- sequence #- list - map #- mapping #- hash - str #- string - int #- integer - float - number #- numeric - bool #- boolean - text - date - time - timestamp #- object - any - scalar #- collection "required": type: bool "enum": type: seq sequence: - type: scalar unique: yes "pattern": type: str "assert": type: str pattern: /\bval\b/ "range": type: map mapping: "max": type: scalar "min": type: scalar "max-ex": type: scalar "min-ex": type: scalar "length": type: map mapping: "max": type: int "min": type: int "max-ex": type: int "min-ex": type: int "ident": type: bool "unique": type: bool "sequence": name: SEQUENCE type: seq sequence: - type: map mapping: *main-rule name: MAIN #required: yes "mapping": name: MAPPING type: map mapping: =: type: map mapping: *main-rule name: MAIN #required: yes p5-Kwalify-1.24/t/testdata/schema05.yaml000066400000000000000000000011151464725437500177540ustar00rootroot00000000000000type: seq sequence: - type: map mapping: name: type: str required: yes email: type: str required: yes pattern: /@/ password: type: text length: { max: 16, min: 8 } age: type: int range: { max: 30, min: 18 } # or assert: 18 <= val && val <= 30 blood: type: str enum: - A - B - O - AB birth: type: date memo: type: any