Sfoglia il codice sorgente

Take perl's own booleans in a router's document

A router had to write Mojo::JSON::true, by its full name, because the
true that use 5.040 imports failed the check against the OpenAPI schema.
JSON::Validator 5.19 compares enum values by a checksum, and a native
true does not hash as the JSON true of required: true.

The router now checks a copy of each document in which perl's booleans
are Cpanel::JSON::XS booleans.  The callbacks in the copy are the same
subs, and the router's own document is left as it was.  The documents in
the POD and the tests say plain true.  Without the copy, t/TPSGI-Router.t
dies as it builds its router.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
troglodyne-bot 6 giorni fa
parent
commit
be155af285
6 ha cambiato i file con 78 aggiunte e 23 eliminazioni
  1. 1 0
      .pod_stopwords
  2. 8 9
      README.md
  3. 8 9
      lib/TPSGI.pm
  4. 40 1
      lib/TPSGI/Router.pm
  5. 19 2
      t/TPSGI-Router.t
  6. 2 2
      t/TPSGI.t

+ 1 - 0
.pod_stopwords

@@ -40,6 +40,7 @@ basename
 bashrc
 Baugh
 BOOL
+booleans
 bugtracker
 cetera
 cgi

+ 8 - 9
README.md

@@ -12,7 +12,7 @@ A router module, which `routers` in `~/.tpsgi.ini` names:
 
 ```perl
 package My::Routes;
-use Mojo::JSON ();
+use 5.040;
 
 our %openapi = (
     openapi => '3.0.3',
@@ -383,7 +383,7 @@ document that each one exports as `%openapi`.  A router can also export
 
 ```perl
 package My::Routes;
-use Mojo::JSON ();
+use 5.040;
 
 our %openapi = (
     openapi => '3.0.3',
@@ -392,12 +392,12 @@ our %openapi = (
         '/sitemap/{map}.xml' => {
             get => {
                 'x-tpsgi-callback' => \&sitemap,
-                'x-myapp-nomap'    => Mojo::JSON::true,
+                'x-myapp-nomap'    => true,
                 parameters         => [
                     {
                         name     => 'map',
                         in       => 'path',
-                        required => Mojo::JSON::true,
+                        required => true,
                         schema   => { type => 'integer' },
                     },
                 ],
@@ -412,7 +412,7 @@ our %openapi = (
                     {
                         name     => 'module+',
                         in       => 'path',
-                        required => Mojo::JSON::true,
+                        required => true,
                         schema   => { type => 'string' },
                     },
                 ],
@@ -423,10 +423,9 @@ our %openapi = (
 );
 ```
 
-The document is Perl data.  A boolean in it must be `Mojo::JSON::true` or
-`Mojo::JSON::false`.  `\1` and `builtin::true` fail the check against the
-OpenAPI schema.  Write the full name: `use 5.040` imports `builtin::true` as
-`true`, and that wins over an imported `Mojo::JSON::true`.
+The document is Perl data.  A boolean in it is perl's own `true` or `false`,
+which `use 5.040` imports, or a JSON boolean such as `Cpanel::JSON::XS::true`.
+`\1` is not a boolean, and fails the check against the OpenAPI schema.
 
 new() dies when a document is not valid, so a broken router fails as it loads,
 and not at the first request.  A router that still defines `@routes` and no

+ 8 - 9
lib/TPSGI.pm

@@ -1658,7 +1658,7 @@ __END__
 A router module, which C<routers> in F<~/.tpsgi.ini> names:
 
     package My::Routes;
-    use Mojo::JSON ();
+    use 5.040;
 
     our %openapi = (
         openapi => '3.0.3',
@@ -1725,7 +1725,7 @@ document that each one exports as C<%openapi>.  A router can also export
 C<%aliases>, C<%watches> and C<generic_route()>.
 
     package My::Routes;
-    use Mojo::JSON ();
+    use 5.040;
 
     our %openapi = (
         openapi => '3.0.3',
@@ -1734,12 +1734,12 @@ C<%aliases>, C<%watches> and C<generic_route()>.
             '/sitemap/{map}.xml' => {
                 get => {
                     'x-tpsgi-callback' => \&sitemap,
-                    'x-myapp-nomap'    => Mojo::JSON::true,
+                    'x-myapp-nomap'    => true,
                     parameters         => [
                         {
                             name     => 'map',
                             in       => 'path',
-                            required => Mojo::JSON::true,
+                            required => true,
                             schema   => { type => 'integer' },
                         },
                     ],
@@ -1754,7 +1754,7 @@ C<%aliases>, C<%watches> and C<generic_route()>.
                         {
                             name     => 'module+',
                             in       => 'path',
-                            required => Mojo::JSON::true,
+                            required => true,
                             schema   => { type => 'string' },
                         },
                     ],
@@ -1764,10 +1764,9 @@ C<%aliases>, C<%watches> and C<generic_route()>.
         },
     );
 
-The document is Perl data.  A boolean in it must be C<Mojo::JSON::true> or
-C<Mojo::JSON::false>.  C<\1> and C<builtin::true> fail the check against the
-OpenAPI schema.  Write the full name: C<use 5.040> imports C<builtin::true> as
-C<true>, and that wins over an imported C<Mojo::JSON::true>.
+The document is Perl data.  A boolean in it is perl's own C<true> or C<false>,
+which C<use 5.040> imports, or a JSON boolean such as C<Cpanel::JSON::XS::true>.
+C<\1> is not a boolean, and fails the check against the OpenAPI schema.
 
 new() dies when a document is not valid, so a broken router fails as it loads,
 and not at the first request.  A router that still defines C<@routes> and no

+ 40 - 1
lib/TPSGI/Router.pm

@@ -9,6 +9,7 @@ use re '/aasx';
 use List::Util ();
 use JSON::Validator::Schema::OpenAPIv3;
 use JSON::Validator::Util ();
+use Cpanel::JSON::XS      ();
 
 =head1 SYNOPSIS
 
@@ -102,6 +103,44 @@ sub new {
     return $self;
 }
 
+=head2 json_booleans($data) = copy
+
+A copy of $data in which each of perl's own booleans, such as the C<true> that
+C<use 5.040> gives a router, is a Cpanel::JSON::XS boolean.  JSON::Validator
+5.19 compares C<enum> values by a checksum, so a native true does not equal
+the true of the OpenAPI schema, and C<required =E<gt> true> fails.  Hashes and
+arrays are copied.  Every other reference is kept, so a callback stays the
+same sub.
+
+=cut
+
+sub json_booleans {
+    my ($data) = @_;
+    no warnings qw{experimental::builtin};
+
+    # Each job is the place a copy goes, and what to copy there.
+    my $copy;
+    my @todo = ( [ \$copy, $data ] );
+    while ( my $job = shift @todo ) {
+        my ( $slot, $value ) = @$job;
+        if ( ref $value eq 'HASH' ) {
+            $$slot = {};
+            push( @todo, map { [ \$$slot->{$_}, $value->{$_} ] } keys(%$value) );
+        }
+        elsif ( ref $value eq 'ARRAY' ) {
+            $$slot = [];
+            push( @todo, map { [ \$$slot->[$_], $value->[$_] ] } 0 .. $#$value );
+        }
+        elsif ( !ref $value && builtin::is_bool($value) ) {
+            $$slot = $value ? Cpanel::JSON::XS::true : Cpanel::JSON::XS::false;
+        }
+        else {
+            $$slot = $value;
+        }
+    }
+    return $copy;
+}
+
 =head2 add_document($name, $document)
 
 Check $document as OpenAPI 3, and add each of its operations.  new() calls it
@@ -114,7 +153,7 @@ sub add_document {
 
     die "$name: the router's %openapi is not a HASH reference\n" unless ref $document eq 'HASH';
 
-    my $schema = JSON::Validator::Schema::OpenAPIv3->new($document);
+    my $schema = JSON::Validator::Schema::OpenAPIv3->new( json_booleans($document) );
     my @errors = @{ $schema->errors };
     die "$name: %openapi is not a valid OpenAPI 3 document:\n" . join( "\n", map { "  $_" } @errors ) . "\n" if @errors;
 

+ 19 - 2
t/TPSGI-Router.t

@@ -8,6 +8,8 @@ use Test2::Plugin::NoWarnings;
 
 use Mojo::JSON ();
 
+use Cpanel::JSON::XS ();
+
 use FindBin::libs;
 
 use TPSGI::Router;
@@ -18,7 +20,7 @@ sub coderef { return 'coderef' }
 
 sub path_parameter {
     my ( $name, %schema ) = @_;
-    return { name => $name, in => 'path', required => Mojo::JSON::true, schema => { type => 'string', %schema } };
+    return { name => $name, in => 'path', required => true, schema => { type => 'string', %schema } };
 }
 
 sub operation {
@@ -48,7 +50,7 @@ my $router = TPSGI::Router->new(
                         'application/x-www-form-urlencoded' => {
                             schema => {
                                 type                 => 'object',
-                                additionalProperties => Mojo::JSON::false,
+                                additionalProperties => false,
                                 properties           => { count => { type => 'integer' } },
                             },
                         },
@@ -116,6 +118,21 @@ subtest 'callbacks' => sub {
     );
 };
 
+subtest 'booleans' => sub {
+    my $json = { %{ path_parameter('x') }, required => Cpanel::JSON::XS::true };
+    ok(
+        lives { TPSGI::Router->new( 'Json.pm' => document( '/j/{x}' => { get => operation( parameters => [$json] ) } ) ) },
+        'a JSON boolean is a boolean'
+    ) or note $@;
+
+    # $router's own document marks its path parameters required with perl's own true.
+    is( $router->match( 'GET', '/post/a' )->{template}, '/post/{slug}', "and so is perl's own, which every document above uses" );
+
+    my $document = document( '/n/{x}' => { get => operation( parameters => [ path_parameter('x') ] ) } );
+    TPSGI::Router->new( 'Native.pm' => $document );
+    ok( !ref $document->{paths}{'/n/{x}'}{get}{parameters}[0]{required}, 'and the router leaves the document it was given as it was' );
+};
+
 subtest 'documents that are refused' => sub {
     like(
         dies { TPSGI::Router->new( 'Bad.pm' => { openapi => '3.0.3', paths => {} } ) },

+ 2 - 2
t/TPSGI.t

@@ -144,7 +144,7 @@ subtest 'serve_ranges' => sub {
 # Writes a router module into $dir, and returns its name relative to $dir.
 sub write_router {
     my ( $package, $body ) = @_;
-    Mojo::File->new("$dir/$package.pm")->spew("package $package;\nuse 5.040;\nuse Mojo::JSON ();\n$body\n1;\n");
+    Mojo::File->new("$dir/$package.pm")->spew("package $package;\nuse 5.040;\n$body\n1;\n");
     return "$package.pm";
 }
 
@@ -173,7 +173,7 @@ subtest 'new' => sub {
                         'x-tpsgi-callback' => sub { },
                         'x-tpsgi-data'     => { from => 'data' },
                         parameters         => [
-                            { name => 'slug',  in => 'path',  required => Mojo::JSON::true, schema => { type => 'string' } },
+                            { name => 'slug',  in => 'path',  required => true, schema => { type => 'string' } },
                             { name => 'limit', in => 'query', schema   => { type => 'integer' } },
                         ],
                         responses => { 200 => { description => 'ok' } },