ETL-Yertl

 view release on metacpan or  search on metacpan

lib/ETL/Yertl/Transform.pm  view on Meta::CPAN

#pod
#pod Transformations can be simple subroutines or full classes (inheriting
#pod from this class).
#pod
#pod =head2 Transform Object
#pod
#pod Create ad-hoc transform objects by passing in a C<transform_doc>
#pod callback. The callback receives two arguments: The transform object, and
#pod the document to transform.  The callback should return the transformed
#pod document (whether or not it is the same document modified in-place).
#pod
#pod =head2 Transform Class
#pod
#pod Create transform classes by inheriting from C<ETL::Yertl::Transform>.
#pod Subclasses can override the C<transform_doc> method to transform
#pod documents. This method receives the same arguments, returns the same
#pod values, sets C<$_>, and behaves exactly like the C<transform_doc>
#pod callback.
#pod
#pod =head2 Overloaded Operators
#pod
#pod Transforms can be chained together using the pipe (C<|>) operator. The
#pod result of the expression is the transform on the right side, for
#pod continued chaining.
#pod
#pod     my $xform1 = ETL::Yertl::Transform->new(
#pod         transform_doc => sub { ... },
#pod     );
#pod     my $xform2 = ETL::Yertl::Transform->new(
#pod         transform_doc => sub { ... },
#pod     );
#pod     my $xform3 = $xform1 | $xform2 | ETL::Yertl::Transform->new(
#pod         transform_doc => sub { ... },
#pod     );
#pod
#pod Transforms can receive sources using the C<< << >> operator with
#pod a L<ETL::Yertl::FormatStream> object. The result of the expression is
#pod the transform object, for continued chaining.
#pod
#pod     my $input = ETL::Yertl::FormatStream->new_for_stdin;
#pod     my $xform = ETL::Yertl::Transform->new(
#pod         transform_doc => sub { ... },
#pod     ) << $input;
#pod
#pod Transforms can receive destinations using the C<< >> >> operator with
#pod a L<ETL::Yertl::FormatStream> object. The result of the expression is
#pod the transform object, for continued chaining.
#pod
#pod     my $output = ETL::Yertl::FormatStream->new_for_stdout;
#pod     my $xform = ETL::Yertl::Transform->new(
#pod         transform_doc => sub { ... },
#pod     ) >> $output;
#pod
#pod =head1 SEE ALSO
#pod
#pod L<ETL::Yertl>, L<ETL::Yertl::FormatStream>
#pod
#pod =cut

use ETL::Yertl;
use Scalar::Util qw( weaken );
use Carp qw( croak );

use base 'IO::Async::Notifier';

# override pipe, <<, and >> to set on_read_doc and on_write_doc
# handlers appropriately
use overload
    '>>' => \&_set_output,
    '<<' => \&_set_input,
    '|' => \&_pipe,
    'fallback' => 1,
    ;

sub _set_output {
    my ( $self, $output ) = @_;
    # TODO: Allow Path::Tiny objects as output
    # TODO: Allow arrayrefs as output
    $self->configure( destination => $output );
    return $self;
}

sub _set_input {
    my ( $self, $input ) = @_;
    # TODO: Allow Path::Tiny objects as input
    # TODO: Allow arrayrefs as input
    $self->configure( source => $input );
    return $self;
}

sub _pipe {
    my ( $self, $other, $swap ) = @_;
    if ( $swap ) {
        ( $self, $other ) = ( $other, $self );
    }
    # TODO: Allow CODE ref as pipe to make up transform
    $other->configure( source => $self );
    return $other;
}

#pod =method new
#pod
#pod     my $xform = ETL::Yertl::Transform->new( %args );
#pod
#pod Create a new transform object. C<%args> is a hash with the following keys:
#pod
#pod =over
#pod
#pod =item source
#pod
#pod The source for documents. Can be a L<ETL::Yertl::FormatStream> or
#pod a L<ETL::Yertl::Transform> object. You do not need to specify this right
#pod away, but it is required for the transform to do useful work.
#pod
#pod =item destination
#pod
#pod (optional) A L<ETL::Yertl::FormatStream> object to write the documents
#pod to.  This can be an intermediate destination or the ultimate
#pod destination.  The last transform in a stream should have a destination.
#pod
#pod =item transform_doc
#pod
#pod A subref to transform the documents read from the source. The subref
#pod will receive two arguments: The transform object and the document to
#pod transform. It should return the transformed document.  The document to
#pod transform is also set as C<$_> for simpler transforms.
#pod
#pod =back
#pod
#pod =method configure
#pod
#pod     $xform->configure( %args );
#pod
#pod Configure this object. Takes the same arguments as the constructor,
#pod L</new>. This method allows updating any of the transform attributes
#pod later, so that transforms can be given new sources/destinations.
#pod
#pod =cut

sub configure {
    my ( $self, %args ) = @_;

    if ( $args{source} ) {
        # Register ourselves with the source
        my $source = $self->{source} = delete $args{source};
        weaken $self;
        $source->configure(
            on_doc => sub {
                my ( $source, $doc, $eof ) = @_;
                local $_ = $doc;
                my @docs = $self->invoke_event( transform_doc => $doc );
                # ; say "Writing docs from return: " . join ", ", @docs;
                # ; use Data::Dumper;
                # ; say STDERR Dumper( \@docs );
                $self->write( $_ ) for grep { $_ } @docs;
                return;
            },
            # XXX This probably needs to be done better:
            # * Users can't add their own handler to this event at all,
            #   making it more difficult to add Yertl streams to larger
            #   programs
            # * This requires on_read_eof to be called after all
            #   transforms are complete, which prevents cooperative
            #   multitasking by using `$self->loop->later` to defer
            #   execution of the transform_doc method/callback
            on_read_eof => sub {
                if ( my $dest = $self->{destination} ) {
                    if ( $dest->{write_handle} != \*STDOUT ) {
                        # Gracefully close the destination and then let
                        # anyone using us as a source know we're finished
                        $dest->configure( on_closed => sub {
                            $self->maybe_invoke_event( 'on_read_eof' );
                        } );
                        $dest->close_when_empty;
                        return;
                    }
                }
                # We emit our own on_read_eof event so downstream things
                # can clean up
                $self->maybe_invoke_event( 'on_read_eof' );
            },
        );
    }
    elsif ( !$self->{source} ) {
        # If we remove this requirement, we can configure objects and
        # add sources to them later, which could enable a bunch of fun
        # things like a pipe-based stream.
        # Without a source, this thing does nothing useful anyway, so
        # it'll be pretty obvious that something is broken.
        # croak "Expected a source";
    }

    if ( $args{destination} ) {
        $self->{destination} = delete $args{destination};
    }

    for my $event ( qw( transform_doc on_doc on_read_eof ) ) {
        $self->{ $event } = delete $args{ $event } if exists $args{ $event };
    }
    croak "Expected either a transform_doc callback or to be able to ->transform_doc"
        unless $self->can_event( 'transform_doc' );
    $self->{on_doc} ||= sub { }; # Default on_doc does nothing

    $self->SUPER::configure( %args );
}



( run in 5.769 seconds using v1.01-cache-2.11-cpan-c221a9de4ec )