summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorYorhel <git@yorhel.nl>2020-10-25 12:02:35 +0100
committerYorhel <git@yorhel.nl>2020-10-28 09:32:45 +0100
commit960946ac90a8da32953a4a21128d993f2049f8d1 (patch)
treedcb9e0006c2afe79143141b1526d6ec7e11bc11a /lib
parent3e3f36d3459d0db851c09315fcd74155735d9859 (diff)
Advsearch: Initial experiments with a new advanced search
Doing this on the main branch to make it easier to get early testing and feedback. Not like I have anything worth testing now, but it's not like this code is getting in the way of anything else. (Unless the changes broke something unrelated, in which case it's extra good to get that early testing)
Diffstat (limited to 'lib')
-rw-r--r--lib/VNWeb/AdvSearch.pm133
-rw-r--r--lib/VNWeb/Releases/Lib.pm16
-rw-r--r--lib/VNWeb/VN/List.pm40
3 files changed, 182 insertions, 7 deletions
diff --git a/lib/VNWeb/AdvSearch.pm b/lib/VNWeb/AdvSearch.pm
new file mode 100644
index 00000000..c8efa87b
--- /dev/null
+++ b/lib/VNWeb/AdvSearch.pm
@@ -0,0 +1,133 @@
+package VNWeb::AdvSearch;
+
+use v5.26;
+use TUWF;
+use Exporter 'import';
+use VNWeb::DB;
+use VNWeb::Validation;
+use VNWeb::HTML;
+use VNDB::Types;
+
+our @EXPORT = qw/ as_tosql as_elm_ /;
+
+
+# Search query (JSON):
+#
+# $Query = $Combinator || $Predicate
+# $Combinator = [ 'and'||'or', $Query, .. ]
+# $Predicate = [ $Field, $Op, $Value ]
+# $Op = '=', '!=', '>=', '<='
+# $Value = $integer || $string || $Query
+#
+# Accepted values for $Op and $Value depend on $Field.
+#
+# e.g.
+#
+# [ 'and'
+# , [ 'or' # No support for array values, so IN() queries need explicit ORs.
+# , [ '=', 'lang', 'en' ]
+# , [ '=', 'lang', 'de' ]
+# , [ '=', 'lang', 'fr' ]
+# ]
+# , [ '!=', 'olang', 'ja' ]
+# , [ '=', 'char', [ 'and' # VN has a char that matches the given query
+# , [ '>=', 'bust', 40 ]
+# , [ '<=', 'bust', 100 ]
+# ]
+# ]
+# ]
+#
+# Search queries should be seen as some kind of low-level assembly for
+# generating complex queries, they're designed to be simple to implement,
+# powerful, extendable and stable. They're also a pain to work with, but that
+# comes with the trade-off.
+#
+# TODO: Compact search query encoding for in URLs. Passing around JSON is... ugly.
+
+
+# Define a $Field, args:
+# $type -> 'v', 'c', etc.
+# $name -> $Field name
+# $value -> TUWF::Validate schema for value validation, or $query_type to accept a nested query.
+# $op=>$sql -> Operator definitions and sql() generation functions.
+#
+# An implementation for the '!=' operator will be supplied automatically if it's not explicitely defined.
+my %fields;
+sub f {
+ my($t, $n, $v, %op) = @_;
+ my %f = (
+ value => ref $v eq 'HASH' ? tuwf->compile($v) : $v,
+ op => \%op,
+ );
+ $f{op}{'!='} = sub { sql 'NOT (', $f{op}{'='}->(@_), ')' } if $f{op}{'='} && !$f{op}{'!='};
+ $f{int} = $f{value} && ($f{value}->analyze->{type} eq 'int' || $f{value}->analyze->{type} eq 'bool');
+ $fields{$t}{$n} = \%f;
+}
+
+f 'v', 'lang', { enum => \%LANGUAGE }, '=' => sub { sql 'v.c_languages && ARRAY', \$_, '::language[]' };
+f 'v', 'olang', { enum => \%LANGUAGE }, '=' => sub { sql 'v.c_olang && ARRAY', \$_, '::language[]' };
+f 'v', 'plat', { enum => \%PLATFORM }, '=' => sub { sql 'v.c_platforms && ARRAY', \$_, '::platform[]' };
+
+
+
+sub validate {
+ my($t, $q) = @_;
+ return { msg => 'Invalid query' } if ref $q ne 'ARRAY' || @$q < 2 || !defined $q->[0] || ref $q->[0];
+
+ # combinator
+ if($q->[0] eq 'and' || $q->[0] eq 'or') {
+ for(@$q[1..$#$q]) {
+ my $r = validate($t, $_);
+ return $r if !$r || ref $r;
+ }
+ return 1;
+ }
+
+ # predicate
+ return { msg => 'Invalid predicate' } if @$q != 3 || !defined $q->[1] || ref $q->[1];
+ my $f = $fields{$t}{$q->[0]};
+ return { msg => 'Unknown field', field => $q->[0] } if !$f;
+ return { msg => 'Invalid operator', field => $q->[0], op => $q->[1] } if !$f->{op}{$q->[1]};
+ return validate($f->{value}, $q->[2]) if !ref $f->{value};
+ my $r = $f->{value}->validate($q->[2]);
+ return { msg => 'Invalid value', field => $q->[0], value => $q->[2], error => $r->err } if $r->err;
+ $q->[2] = $r->data;
+ 1
+}
+
+
+# 'advsearch' validation, accepts either a JSON representation or an already decoded array.
+TUWF::set('custom_validations')->{advsearch} = sub { my($t) = @_; +{ required => 0, type => 'any', func => sub {
+ return { msg => 'Invalid JSON', error => $@ =~ s{[\s\r\n]* at /[^ ]+ line.*$}{}smr } if !ref $_[0] && !eval { $_[0] = JSON::XS->new->decode($_[0]); 1 };
+ validate($t, @_)
+} } };
+
+
+sub as_tosql {
+ my($t, $q) = @_;
+ return sql_and map as_tosql($t, $_), @$q[1..$#$q] if $q->[0] eq 'and';
+ return sql_or map as_tosql($t, $_), @$q[1..$#$q] if $q->[0] eq 'or';
+
+ my $f = $fields{$t}{$q->[0]};
+ local $_ = ref $f->{value} ? $q->[2] : as_tosql($f->{value}, $q->[2]);
+ $f->{op}{$q->[1]}->();
+}
+
+
+sub coerce_for_json {
+ my($t, $q) = @_;
+ if($q->[0] eq 'and' || $q->[0] eq 'or') {
+ coerce_for_json($t, $_) for @$q[1..$#$q];
+ } else {
+ my $f = $fields{$t}{$q->[0]};
+ $f->{int} ? $q->[2]*1 : ref $f->{value} ? "$q->[2]" : coerce_for_json($t, $q->[2]);
+ }
+ $q
+}
+
+sub as_elm_ {
+ my($t, $q) = @_;
+ elm_ 'AdvSearch.Main', 'raw', $q && coerce_for_json($t, $q);
+}
+
+1;
diff --git a/lib/VNWeb/Releases/Lib.pm b/lib/VNWeb/Releases/Lib.pm
index 4aad7b50..5be64efd 100644
--- a/lib/VNWeb/Releases/Lib.pm
+++ b/lib/VNWeb/Releases/Lib.pm
@@ -52,13 +52,15 @@ sub release_extlinks_ {
abbr_ class => 'icons external', title => 'External link', '';
};
div_ sub {
- ul_ sub {
- li_ sub {
- a_ href => $_->[1], sub {
- span_ $_->[2] if length $_->[2];
- txt_ $_->[0];
- }
- } for $r->{extlinks}->@*;
+ div_ sub {
+ ul_ sub {
+ li_ sub {
+ a_ href => $_->[1], sub {
+ span_ $_->[2] if length $_->[2];
+ txt_ $_->[0];
+ }
+ } for $r->{extlinks}->@*;
+ }
}
}
}
diff --git a/lib/VNWeb/VN/List.pm b/lib/VNWeb/VN/List.pm
new file mode 100644
index 00000000..a9cf6fb4
--- /dev/null
+++ b/lib/VNWeb/VN/List.pm
@@ -0,0 +1,40 @@
+package VNWeb::VN::List;
+
+use VNWeb::Prelude;
+use VNWeb::AdvSearch;
+
+
+TUWF::get qr{/experimental/v}, sub {
+ my $opt = tuwf->validate(get =>
+ q => { onerror => '' },
+ p => { upage => 1 },
+ f => { advsearch => 'v' },
+ )->data;
+
+ my $where = sql_and
+ 'NOT v.hidden',
+ $opt->{q} ? map sql('v.c_search LIKE', \"%$_%"), normalize_query $opt->{q} : (),
+ $opt->{f} ? as_tosql(v => $opt->{f}) : ();
+
+ my $time = time;
+ my $count = tuwf->dbVali('SELECT count(*) FROM vn v WHERE', $where);
+ $time = time - $time;
+
+ framework_ title => 'Browse visual novels', sub {
+ div_ class => 'mainbox', sub {
+ h1_ 'Browse visual novels';
+ div_ class => 'warning', sub {
+ h2_ 'EXPERIMENTAL';
+ p_ "This is Yorhel's playground. Lots of functionality is missing, lots of stuff is or will be broken. Here be dragons. Etc.";
+ };
+ br_;
+ form_ action => '/experimental/v', method => 'get', sub {
+ searchbox_ v => $opt->{q};
+ as_elm_ v => $opt->{f};
+ };
+ p_ sprintf '%d results in %.3fs', $count, $time;
+ };
+ };
+};
+
+1;