Kaynağa Gözat

Search a post with no title, and each slide of a presentation

Trog::DataModule::filter() matched a search against $_->{title} and
$_->{data} as they were.  A post with no title is undef there, and under
fatal warnings the match died, so one such post stopped every search of the
site.  A presentation's body is a list, so its slides were searched as the
address of that list.

Trog::DataSource::searchable_match() already said what a search means, with
both of those cases handled, for datasources to use.  It moves into
Trog::DataModule, whose filter it describes, and filter() uses it on each
slide of a body that is a list.  Trog::DataSource keeps the name, as an
alias, because the datasources call it there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
troglodyne-bot 2 gün önce
ebeveyn
işleme
108f3badf2
3 değiştirilmiş dosya ile 63 ekleme ve 19 silme
  1. 24 1
      lib/Trog/DataModule.pm
  2. 4 18
      lib/Trog/DataSource.pm
  3. 35 0
      t/Trog-DataModule.t

+ 24 - 1
lib/Trog/DataModule.pm

@@ -196,6 +196,28 @@ my sub filter_acls ( $query, @filtered ) {
     } @filtered;
 }
 
+=head2 searchable_match($like, @values) = BOOL
+
+Whether any of @values contains $like, case insensitively.  This is what a
+search means: filter() applies it to a stored post's title and body, and
+Trog::DataSource gives it to a datasource by the same name.  Quotemeta'd, so a
+search for '*' looks for an asterisk.
+
+Anything in @values which isn't a plain string is skipped, so a missing title
+or a nested structure is passed over rather than matched against its address.
+
+=cut
+
+sub searchable_match ( $like, @values ) {
+    return 0 if ( $like // q{} ) eq q{};
+
+    foreach my $value (@values) {
+        next     if !defined $value || ref $value;
+        return 1 if $value =~ m/\Q$like\E/i;
+    }
+    return 0;
+}
+
 =head2 filter($query, @posts) = @filtered
 
 Apply a get() request's filters to a list of posts: tags, exclude_tags, acls,
@@ -255,7 +277,8 @@ sub filter ( $, $query, @filtered ) {
 
     @filtered = grep { ( $_->{form} || '' ) eq $query->{form} } @filtered if $query->{form};
 
-    @filtered = grep { $_->{title} =~ m/\Q$query->{like}\E/i || $_->{data} =~ m/\Q$query->{like}\E/i } @filtered if $query->{like};
+    # Each slide of a body that is a list, such as a presentation's.
+    @filtered = grep { searchable_match( $query->{like}, $_->{title}, Ref::Util::is_arrayref( $_->{data} ) ? @{ $_->{data} } : $_->{data} ) } @filtered if $query->{like};
 
     @filtered = grep { $_->{user} eq $query->{author} } @filtered if $query->{author};
 

+ 4 - 18
lib/Trog/DataSource.pm

@@ -125,27 +125,13 @@ sub filter ( $query, @posts ) {
 
 =head2 searchable_match($like, @values) = BOOL
 
-Whether any of @values contains $like, case insensitively.
-
-The same case insensitive substring test Trog::DataModule::filter() applies to a
-stored post's title and body, factored out so that a datasource can hand it the
-fields its own posts actually carry without having to restate what a search
-means.  Quotemeta'd, so a search for '*' looks for an asterisk.
-
-Anything in @values which isn't a plain string is skipped, so an arrayref body
-or a nested structure is passed over rather than matched against its address.
+Trog::DataModule::searchable_match(), which is what a search of stored posts
+means, so that a datasource can hand it the fields its own posts actually carry
+without having to restate it.
 
 =cut
 
-sub searchable_match ( $like, @values ) {
-    return 0 if ( $like // q{} ) eq q{};
-
-    foreach my $value (@values) {
-        next     if !defined $value || ref $value;
-        return 1 if $value =~ m/\Q$like\E/i;
-    }
-    return 0;
-}
+*searchable_match = \&Trog::DataModule::searchable_match;
 
 =head2 order($query, @posts) = @posts
 

+ 35 - 0
t/Trog-DataModule.t

@@ -0,0 +1,35 @@
+#!/usr/bin/env perl
+use 5.040;
+use re '/aasx';
+use warnings FATAL => 'all';
+
+use Test::More;
+use Test::Fatal qw{exception};
+
+use FindBin::libs;
+
+require_ok('Trog::DataModule') or BAIL_OUT("Can't find SUT");
+
+subtest 'filter, with a search' => sub {
+    my %stored = ( version => 0, created => 1, tags => ['public'] );
+    my @posts  = (
+        { %stored, id => 'titled',   title => 'A Spec Title', data => 'body' },
+        { %stored, id => 'untitled', data  => 'a spec body' },
+        { %stored, id => 'slides',   title => 'Deck',  data => [ 'first slide', 'a spec slide' ] },
+        { %stored, id => 'other',    title => 'Other', data => 'nothing here' },
+    );
+    my $search = sub ($like) {
+        my @found;
+        my $err = exception { @found = Trog::DataModule::filter( undef, { like => $like, acls => ['admin'] }, @posts ) };
+        return ( $err, [ map { $_->{id} } @found ] );
+    };
+
+    my ( $err, $found ) = $search->('spec');
+    is( $err, undef, 'a post with no title does not stop the search' );
+    is_deeply( $found, [qw{titled untitled slides}], 'which finds a title, a body, and a slide of a body that is a list' );
+
+    ( $err, $found ) = $search->('ARRAY');
+    is_deeply( $found, [], 'and never the address of that list' );
+};
+
+done_testing();