From d8e63f1ea46c74123cfef68e17bb343615fb8d7f Mon Sep 17 00:00:00 2001 From: Graham Ollis Date: Sat, 14 Dec 2024 18:04:53 -0700 Subject: [PATCH 1/2] Add format content plugin --- README.md | 42 ++++++- author.yml | 3 +- .../Pluggable/Role/FormatContentPlugin.pm | 14 +++ lib/Data/Section/Writer.pm | 108 +++++++++++++++++- t/00_diag.t | 3 + ...ction_pluggable_role_formatcontentplugin.t | 48 ++++++++ 6 files changed, 210 insertions(+), 8 deletions(-) create mode 100644 lib/Data/Section/Pluggable/Role/FormatContentPlugin.pm create mode 100644 t/data_section_pluggable_role_formatcontentplugin.t diff --git a/README.md b/README.md index 9f1d6d8..afc8980 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,7 @@ Starting with version 0.02, this method will not write to the file if the conten \[version 0.02\] ```perl -my $bool = $self->unchanged; +my $bool = $writer->unchanged; ``` Returns: @@ -103,6 +103,46 @@ Returns: If the last call to </update\_file> did not modify the file. +## add\_format + +```perl +$writer->add_format( $ext, sub ($writer, $content) { return ... } ); +``` + +Adds a content formatter to the given filename extension. The extension should be a filename extension without the `.`, for example `txt` or `json`. + +The callback takes the [Data::Section::Writable](https://metacpan.org/pod/Data::Section::Writable) instance as its first argument and the content to be processed as the second. +This callback should return the format content as a scalar. + +You can chain multiple content formatters to the same filename extension, and they will be called in the order that they were added. + +## add\_plugin + +``` +$writer->add_plugin( $name, %args ); +``` + +Applies the plugin with `$name`. If the plugin supports instance mode (that is: it has a constructor named new), then %args will be passed to the +constructor. For included plugins see ["CORE PLUGINS"](#core-plugins). To write your own see ["PLUGIN ROLES"](#plugin-roles). + +# CORE PLUGINS + +This module will work with some core [Data::Section::Pluggable](https://metacpan.org/pod/Data::Section::Pluggable) plugins listed here. + +## json + +Automatically encode json into Perl data structures. + +See [Data::Section::Pluggable::Plugin::Json](https://metacpan.org/pod/Data::Section::Pluggable::Plugin::Json). + +# PLUGIN ROLES + +## FormatContentPlugin + +Used for adding content formatting for specific formats. This +is essentially a way to wrap the [add\_format method](#add_format) +as a module. See [Data::Section::Pluggable::Role::FormatContentPlugin](https://metacpan.org/pod/Data::Section::Pluggable::Role::FormatContentPlugin). + # CAVEATS Added text files will get an added trailing new line if they do not already have diff --git a/author.yml b/author.yml index 16a62f0..4e35369 100644 --- a/author.yml +++ b/author.yml @@ -5,7 +5,8 @@ pod_spelling_system: # (regardless of what spell check thinks) # or stuff that I like to spell incorrectly # intentionally - stopwords: [] + stopwords: + - FormatContentPlugin pod_coverage: skip: 0 diff --git a/lib/Data/Section/Pluggable/Role/FormatContentPlugin.pm b/lib/Data/Section/Pluggable/Role/FormatContentPlugin.pm new file mode 100644 index 0000000..b086af4 --- /dev/null +++ b/lib/Data/Section/Pluggable/Role/FormatContentPlugin.pm @@ -0,0 +1,14 @@ +use warnings; +use 5.020; +use true; +use experimental qw( signatures ); + +package Data::Section::Pluggable::Role::FormatContentPlugin { + + # ABSTRACT: Plugin role for Data::Section::Writer + + use Role::Tiny; + requires 'extensions'; + requires 'format_content'; + +} diff --git a/lib/Data/Section/Writer.pm b/lib/Data/Section/Writer.pm index 3369f2c..28cf559 100644 --- a/lib/Data/Section/Writer.pm +++ b/lib/Data/Section/Writer.pm @@ -2,6 +2,7 @@ use warnings; use 5.020; use experimental qw( signatures ); use stable qw( postderef ); +use true; package Data::Section::Writer { @@ -38,8 +39,8 @@ The name of the Perl source file. If not provided then the source for the calle use Path::Tiny (); use Carp (); - use Class::Tiny qw( perl_filename _files _same ); - use Ref::Util qw( is_blessed_ref ); + use Class::Tiny qw( perl_filename _files _same _formats ); + use Ref::Util qw( is_coderef is_blessed_ref is_plain_arrayref ); use MIME::Base64 qw(encode_base64); sub BUILD ($self, $) { @@ -56,6 +57,7 @@ The name of the Perl source file. If not provided then the source for the calle } $self->_files({}); + $self->_formats({}); } @@ -81,10 +83,18 @@ only supported by L at the moment. my $text = "@@ $filename"; $text .= " (" . $data->[1] . ")" if defined $data->[1]; $text .= "\n"; + + my $content = $data->[0]; + + if($filename =~ /\.(.*?)\z/ && ($self->_formats->{$1} // [])->@*) { + my $ext = $1; + $content = $_->($self, $content) for $self->_formats->{$ext}->@*; + } + if(defined $data->[1] && $data->[1] eq 'base64') { - $text .= encode_base64($data->[0]); + $text .= encode_base64($data->[0]); } else { - $text .= $data->[0]; + $text .= $content; } chomp $text; return $text; @@ -161,7 +171,7 @@ Starting with version 0.02, this method will not write to the file if the conten [version 0.02] - my $bool = $self->unchanged; + my $bool = $writer->unchanged; Returns: @@ -187,9 +197,95 @@ If the last call to did not modify the file. return $self->_same; } +=head2 add_format + + $writer->add_format( $ext, sub ($writer, $content) { return ... } ); + +Adds a content formatter to the given filename extension. The extension should be a filename extension without the C<.>, for example C or C. + +The callback takes the L instance as its first argument and the content to be processed as the second. +This callback should return the format content as a scalar. + +You can chain multiple content formatters to the same filename extension, and they will be called in the order that they were added. + +=cut + + sub add_format ($self, $ext, $cb) { + Carp::croak("callback is not a code reference") unless is_coderef $cb; + push $self->_formats->{$ext}->@*, $cb; + return $self; + } + +=head2 add_plugin + + $writer->add_plugin( $name, %args ); + +Applies the plugin with C<$name>. If the plugin supports instance mode (that is: it has a constructor named new), then %args will be passed to the +constructor. For included plugins see L. To write your own see L. + +=cut + + sub add_plugin ($self, $name, %args) { + Carp::croak("plugin name must match [a-z][a-z0-9_]+, got $name") + unless $name =~ /^[a-z][a-z0-9_]+\z/; + + my $class = join '::', 'Data', 'Section', 'Pluggable', 'Plugin', ucfirst($name =~ s/_(.)/uc($1)/egr); + my $pm = ($class =~ s!::!/!gr) . ".pm"; + + require $pm unless $self->_valid_plugin($class); + + my $plugin; + if($class->can("new")) { + $plugin = $class->new(%args); + } else { + if(%args) { + Carp::croak("extra arguments are not allowed for class plugins (hint create constructor)"); + } + $plugin = $class; + } + + Carp::croak("$class is not a valid Data::Section::Pluggable plugin") + unless $self->_valid_plugin($plugin); + + if($plugin->does('Data::Section::Pluggable::Role::FormatContentPlugin')) { + + my @extensions = $plugin->extensions; + @extensions = $extensions[0]->@* if is_plain_arrayref $extensions[0]; + + die "extensions method for $class returned no extensions" unless @extensions; + + my $cb = sub ($self, $content) { + return $plugin->format_content($self, $content); + }; + + $self->add_format($_, $cb) for @extensions; + } + + return $self; + } + + sub _valid_plugin ($self, $plugin) { + $plugin->can('does') && $plugin->does('Data::Section::Pluggable::Role::FormatContentPlugin'); + } } -1; +=head1 CORE PLUGINS + +This module will work with some core L plugins listed here. + +=head2 json + +Automatically encode json into Perl data structures. + +See L. + +=head1 PLUGIN ROLES + +=head2 FormatContentPlugin + +Used for adding content formatting for specific formats. This +is essentially a way to wrap the L +as a module. See L. =head1 CAVEATS diff --git a/t/00_diag.t b/t/00_diag.t index 27863c2..cf3a619 100644 --- a/t/00_diag.t +++ b/t/00_diag.t @@ -21,11 +21,14 @@ $modules{$_} = $_ for qw( Mojolicious Path::Tiny Ref::Util + Role::Tiny + Role::Tiny::With Test2::API Test2::Require::Module Test2::V0 Test::Differences stable + true ); diff --git a/t/data_section_pluggable_role_formatcontentplugin.t b/t/data_section_pluggable_role_formatcontentplugin.t new file mode 100644 index 0000000..a61a7b1 --- /dev/null +++ b/t/data_section_pluggable_role_formatcontentplugin.t @@ -0,0 +1,48 @@ +use Test2::V0 -no_srand => 1; +use experimental qw( signatures ); +use Data::Section::Writer; + +plan 3; + +package Data::Section::Pluggable::Plugin::MyPlugin1 { + use Role::Tiny::With; + with 'Data::Section::Pluggable::Role::FormatContentPlugin'; + + sub extensions ($class) { + package main; + is( + [$class], + ['Data::Section::Pluggable::Plugin::MyPlugin1'], + 'arguments to ->extensions' + ); + return ('txt') + } + + sub format_content ($class, $writer, $content) { + package main; + is( + [ $class, $writer, $content ], + [ 'Data::Section::Pluggable::Plugin::MyPlugin1', object { prop isa => 'Data::Section::Writer' }, "hello world" ], + 'arguments to ->process_content', + ); + return "[$content]" + } + +} + +is( + Data::Section::Writer->new->add_plugin('my_plugin1'), + object { + prop isa => 'Data::Section::Writer'; + call [add_file => "hello.txt", "hello world"] => object {}; + call [add_file => "hello.t2", "hello world"] => object {}; + call render_section => "__DATA__\n" . + "\@\@ hello.t2\n" . + "hello world\n" . + "\@\@ hello.txt\n" . + "[hello world]\n"; + }, + 'simple', +); + +done_testing; From 4396df8fcf818c5c98aeb0f0bf98d42f1356f89a Mon Sep 17 00:00:00 2001 From: Graham Ollis Date: Sat, 14 Dec 2024 18:07:19 -0700 Subject: [PATCH 2/2] update changes --- Changes | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Changes b/Changes index 9ee8778..9e843a3 100644 --- a/Changes +++ b/Changes @@ -2,8 +2,11 @@ Revision history for {{$dist->name}} {{$NEXT}} - Don't write file if __DATA__ hasn't changed (gh#2) - - Add ->unchanged method (gh#2) - - Add integration tests and documentation re: Data::Section::Pluggable (gh#3) + - Added ->unchanged method (gh#2) + - Added integration tests and documentation re: Data::Section::Pluggable (gh#3) + - Added Data::Section::Pluggable::FormatContentPlugin (gh#4) + - Added ->add_format method (gh#2) + - Added ->add_plugin method (gh#2) 0.01 2024-12-03 17:15:21 -0700 - initial version