DataSource.pm 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. package Trog::DataSource;
  2. use v5.36;
  3. use re '/aa';
  4. use Trog::DataModule ();
  5. use Trog::Log qw{:all};
  6. =head1 Trog::DataSource
  7. What a datasource is, and the default answers for the parts of it a given
  8. datasource doesn't care to implement.
  9. A post type can say its posts come from somewhere other than the datastore --
  10. see Trog::DataSource::Virt, which builds them out of libvirt guests. The series
  11. is still an ordinary post; only its children are synthesized, and they are
  12. rebuilt on every view rather than stored.
  13. Datasources are plain packages under this namespace, called as functions rather
  14. than methods, and named by a post type's sidecar in x-tcms-datasource.
  15. =head1 THE CONTRACT
  16. posts($series, $query) = @posts required
  17. The children of $series, as post hashrefs. Enough of a post to survive being
  18. rendered like one -- see Trog::DataSource::Virt::_post_for for the shape.
  19. EDITABLE optional constant
  20. Whether the wizard should generate an editor for a type drawn from this source.
  21. A source which builds its posts from somewhere else has nothing to edit, and
  22. saying nothing means no.
  23. filter($query, @posts) = @posts optional
  24. Apply the viewer's search to the synthesized posts. Defaulted below; implement
  25. it to search the fields your posts actually carry.
  26. lang() = STRING optional
  27. help() = STRING optional
  28. What the search box is searching, and where to read about it, for a page drawn
  29. from this source. Defaulted below.
  30. =head1 FUNCTIONS
  31. =head2 filter($query, @posts) = @posts
  32. The default search over synthesized posts: the query's C<like>, C<author>,
  33. C<older> and C<newer>, with the same meanings they have for stored posts.
  34. Deliberately not the tag, acl or version filtering Trog::DataModule::filter()
  35. also does. Those are answers about the datastore, and they are already settled
  36. before a datasource is reached: the series carries the acls, the reader got to
  37. its page by holding them, and every synthesized post belongs to that series.
  38. Applying them again would at best be redundant, and at worst would blank the
  39. page for a source whose posts carry no tags -- which nothing requires them to.
  40. Nor does it paginate. A datasource runs before the paginator's own variables
  41. are worked out, so there is nothing yet to paginate against.
  42. A source with more to search than a title should say so by implementing filter()
  43. rather than by hoping this one covers it.
  44. =cut
  45. sub filter ( $query, @posts ) {
  46. $query //= {};
  47. if ( length( $query->{like} // '' ) ) {
  48. my $like = $query->{like};
  49. @posts = grep { searchable_match( $like, $_->{title}, $_->{data} ) } @posts;
  50. }
  51. @posts = grep { ( $_->{user} // '' ) eq $query->{author} } @posts if $query->{author};
  52. # Coerced to a number the same way Trog::DataModule::filter() does it, since
  53. # this arrives straight off a query string.
  54. if ( $query->{older} ) {
  55. ( my $older = $query->{older} ) =~ s/[^0-9]//g;
  56. @posts = grep { ( $_->{created} // 0 ) < $older } @posts if length $older;
  57. }
  58. if ( $query->{newer} ) {
  59. ( my $newer = $query->{newer} ) =~ s/[^0-9]//g;
  60. @posts = grep { ( $_->{created} // 0 ) > $newer } @posts if length $newer;
  61. }
  62. return @posts;
  63. }
  64. =head2 searchable_match($like, @values) = BOOL
  65. Whether any of @values contains $like, case insensitively.
  66. The same case insensitive substring test Trog::DataModule::filter() applies to a
  67. stored post's title and body, factored out so that a datasource can hand it the
  68. fields its own posts actually carry without having to restate what a search
  69. means. Quotemeta'd, so a search for '*' looks for an asterisk.
  70. Anything in @values which isn't a plain string is skipped, so an arrayref body
  71. or a nested structure is passed over rather than matched against its address.
  72. =cut
  73. sub searchable_match ( $like, @values ) {
  74. return 0 unless length( $like // '' );
  75. foreach my $value (@values) {
  76. next if !defined $value || ref $value;
  77. return 1 if $value =~ m/\Q$like\E/i;
  78. }
  79. return 0;
  80. }
  81. =head2 lang() = STRING
  82. What the search box searches on a page drawn from a datasource which hasn't said.
  83. Not the data model's answer: the model's query language describes what it can do
  84. to the datastore, and a datasource page is not being served out of the datastore.
  85. =cut
  86. sub lang { 'Case insensitive substring' }
  87. =head2 help() = STRING
  88. Where to read about it.
  89. =cut
  90. sub help { 'https://en.wikipedia.org/wiki/Substring' }
  91. =head2 for_type($form) = STRING or undef
  92. The datasource a post type names, or nothing when it names none.
  93. Checked against this namespace rather than requiring whatever we were told to:
  94. the name comes out of a sidecar, and a sidecar cannot be allowed to make us load
  95. an arbitrary module. Returns the module name without loading it; see load().
  96. =cut
  97. sub for_type ( $form = '' ) {
  98. return undef unless length( $form // '' );
  99. my $meta = Trog::DataModule::type_meta_for($form);
  100. my $source = $meta->{'x-tcms-datasource'};
  101. return undef unless $source && !ref $source;
  102. if ( $source !~ m/^Trog::DataSource::\w+$/ ) {
  103. WARN("Post type '$form' names a datasource '$source' which isn't one");
  104. return undef;
  105. }
  106. return $source;
  107. }
  108. =head2 load($source) = BOOL
  109. Load a datasource named by for_type(), returning whether it worked.
  110. =cut
  111. sub load ($source) {
  112. my $modpath = $source;
  113. $modpath =~ s{::}{/}g;
  114. $modpath .= '.pm';
  115. local $@;
  116. eval { require $modpath; 1 } or do {
  117. WARN("Datasource '$source' will not load: $@");
  118. return 0;
  119. };
  120. return 1;
  121. }
  122. 1;