##---------------------------------------------------------------------------- ## Asynchronous HTTP Request and Promise - ~/lib/HTTP/Promise/Body/Form.pm ## Version v0.1.0 ## Copyright(c) 2022 DEGUEST Pte. Ltd. ## Author: Jacques Deguest ## Created 2022/05/18 ## Modified 2022/05/18 ## All rights reserved. ## ## ## This program is free software; you can redistribute it and/or modify it ## under the same terms as Perl itself. ##---------------------------------------------------------------------------- package HTTP::Promise::Body::Form; BEGIN { use strict; use warnings; use warnings::register; use parent qw( Module::Generic::Hash ); use vars qw( $VERSION ); use Nice::Try; use URL::Encode::XS (); our $VERSION = 'v0.1.0'; }; use strict; use warnings; sub new { my $this = shift( @_ ); if( @_ ) { my $data = shift( @_ ); if( ref( $data ) eq 'HASH' ) { return( $this->SUPER::new( $data, @_ ) ); } elsif( !ref( $data ) || ( ref( $data ) ne 'HASH' && overload::Method( $data => '""' ) ) ) { my $ref = $this->decode_to_hash( "${data}" ) || return( $this->pass_error ); return( $this->SUPER::new( $ref, @_ ) ); } else { return( $this->error( "Unsupported data type '", ref( $data ), "'." ) ); } } else { return( $this->SUPER::new ); } } sub init { my $self = shift( @_ ); $self->{_init_strict_use_sub} = 1; $self->SUPER::init( @_ ) || return( $self->pass_error ); return( $self ); } sub as_form_data { my $self = shift( @_ ); my $hash = {}; my $keys = $self->keys->sort; $self->_load_class( 'HHTP::Promise::Body::Form' ) || return( $self->pass_error ); my $form = HHTP::Promise::Body::Form->new; foreach my $n ( @$keys ) { my $v = $self->{ $n }; if( $self->_is_array( $v ) ) { foreach my $v2 ( @$v ) { my $e = $self->_is_a( $v2 => 'HTTP::Promise::Body::Form::Field' ) ? $v2 : $form->new_field( name => $n, body => $v2, ); if( exists( $form->{ $n } ) ) { $form->{ $n } = [$form->{ $n }] unless( $self->_is_array( $form->{ $n } ) ); push( @{$form->{ $n }}, $e ); } else { $form->{ $n } = $e; } } } else { my $e = $self->_is_a( $v => 'HTTP::Promise::Body::Form::Field' ) ? $v : $form->new_field( name => $n, body => $v, ); if( exists( $form->{ $n } ) ) { $form->{ $n } = [$form->{ $n }] unless( $self->_is_array( $form->{ $n } ) ); push( @{$form->{ $n }}, $e ); } else { $form->{ $n } = $e; } } } return( $form ); } sub as_string { my $self = shift( @_ ); my $keys = []; if( @_ && $self->_tie_object->_is_array( $_[0] ) ) { $keys = shift( @_ ); } else { $keys = $self->keys->sort; } my @pairs = (); try { $self->_tie_object->enable(1); foreach my $n ( @$keys ) { my $v = $self->{ $n }; if( ref( $v ) eq 'ARRAY' ) { foreach my $v2 ( @$v ) { if( $self->_is_a( $v2 => 'HTTP::Promise::Body::Form::Field' ) ) { $v2 = $v2->body->as_string( binmode => 'utf-8' ); } warn( "Found a value, within an array for item '$n', that is a reference, but does not stringifies.\n" ) if( ref( $v2 ) && !overload::Method( $v2 => '""' ) && $self->_is_warnings_enabled ); push( @pairs, join( '=', $n, URL::Encode::XS::url_encode_utf8( "$v2" ) ) ); } } else { if( $self->_is_a( $v => 'HTTP::Promise::Body::Form::Field' ) ) { $v = $v->body->as_string( binmode => 'utf-8' ); } warn( "Found a value, for item '$n', that is a reference, but does not stringifies.\n" ) if( ref( $v ) && !overload::Method( $v => '""' ) && $self->_is_warnings_enabled ); push( @pairs, join( '=', $n, URL::Encode::XS::url_encode_utf8( "$v" ) ) ); } } return( join( '&', @pairs ) ); } catch( $e ) { return( $self->error( "Error while Trying to url-encode ", scalar( @$keys ), " form elements: $e" ) ); } } sub decode { return( shift->decode_to_array( @_ ) ); } sub decode_string { my $self = shift( @_ ); my $data = shift( @_ ); warn( "No data to url-decode was provided.\n" ) if( ( !defined( $data ) || !length( "$data" ) ) && $self->_is_warnings_enabled ); return( $self->error( "Invalid parameter provided. You can only pass a string or an object that stringifies." ) ) if( ref( $data ) && !overload::Method( $data => '""' ) ); try { my $decoded = URL::Encode::XS::url_decode_utf8( "${data}" ); return( $decoded ); } catch( $e ) { return( $self->error( "Error while Trying to url-decode ", length( $data ), " bytes of data: $e" ) ); } } sub decode_to_array { my $self = shift( @_ ); my $data = shift( @_ ); # warn( "No data to url-decode was provided.\n" ) if( ( !defined( $data ) || !length( "$data" ) ) && $self->_is_warnings_enabled ); warn( "No data to url-decode was provided.\n" ) if( ( !defined( $data ) || !length( "$data" ) ) && $self->_is_warnings_enabled ); return( $self->error( "Invalid parameter provided. You can only pass a string or an object that stringifies." ) ) if( ref( $data ) && !overload::Method( $data => '""' ) ); try { my $ref = URL::Encode::XS::url_params_flat( "${data}" ); return( $ref ); } catch( $e ) { return( $self->error( "Error while Trying to url-decode ", length( $data ), " bytes of data: $e" ) ); } } sub decode_to_hash { my $self = shift( @_ ); my $ref = $self->_is_array( $_[0] ) ? shift( @_ ) : $self->decode_to_array( @_ ); return( $self->pass_error ) if( !defined( $ref ) ); my $hash = {}; while( my( $n, $v ) = splice( @$ref, 0, 2 ) ) { if( exists( $hash->{ $n } ) ) { $hash->{ $n } = [ $hash->{ $n } ] unless( ref( $hash->{ $n } ) eq 'ARRAY' ); push( @{$hash->{ $n }}, $v ); } else { $hash->{ $n } = $v; } } return( $hash ); } # TODO: This is redundant with code in as_string. as_string should be revamped to call encode() sub encode { my $self = shift( @_ ); my $ref = shift( @_ ); return( $self->error( "Invalid argument provided. I was expecting an array or an hash reference." ) ) if( ref( $ref ) ne 'ARRAY' && ref( $ref ) ne 'HASH' ); # Work on a copy my $this = ref( $ref ) eq 'ARRAY' ? [@$ref] : [%$ref]; return( '' ) if( !scalar( @$this ) ); my @pairs = (); try { while( my( $n, $v ) = splice( @$this, 0, 2 ) ) { if( ref( $v ) eq 'ARRAY' ) { foreach my $v2 ( @$v ) { if( $self->_is_a( $v2 => 'HTTP::Promise::Body::Form::Field' ) ) { $v2 = $v2->body->as_string( binmode => 'utf-8' ); } warn( "Found a value, within an array for item '$n', that is a reference, but does not stringifies.\n" ) if( ref( $v2 ) && !overload::Method( $v2 => '""' ) && $self->_is_warnings_enabled ); push( @pairs, join( '=', $n, URL::Encode::XS::url_encode_utf8( "$v2" ) ) ); } } else { if( $self->_is_a( $v => 'HTTP::Promise::Body::Form::Field' ) ) { $v = $v->body->as_string( binmode => 'utf-8' ); } warn( "Found a value, for item '$n', that is a reference, but does not stringifies.\n" ) if( ref( $v ) && !overload::Method( $v => '""' ) && $self->_is_warnings_enabled ); push( @pairs, join( '=', $n, URL::Encode::XS::url_encode_utf8( "$v" ) ) ); } } return( join( '&', @pairs ) ); } catch( $e ) { return( $self->error( "Error while Trying to url-encode ", scalar( @$this ), " elements provided: $e" ) ); } } sub encode_string { my $self = shift( @_ ); try { return( URL::Encode::XS::url_encode_utf8( shift( @_ ) ) ); } catch( $e ) { return( $self->error( "Error while trying to url-encode: $e" ) ); } } sub error { my $self = shift( @_ ); $self->_tie_object->enable(0); return( $self->SUPER::error( @_ ) ); } sub length { return( CORE::length( shift->as_string ) ); } sub open { my $self = shift( @_ ); my $encoded = $self->as_string; return( $self->pass_error ) if( !defined( $encoded ) ); my $s = $self->_tie_object->new_scalar( \$encoded ) || return( $self->pass_error ); my $io = $s->open( @_ ) || return( $self->pass_error( $s->error ) ); return( $io ); } sub pass_error { my $self = shift( @_ ); $self->_tie_object->enable(0); return( $self->SUPER::pass_error( @_ ) ); } sub print { my( $self, $fh ) = @_; my $nread; # Get output filehandle, and ensure that it's a printable object: $fh ||= select; return( $self->error( "Filehandle provided ($fh) is not a proper filehandle and its not a HTTP::Promise::IO object." ) ) if( !$self->_tie_object->_is_glob( $fh ) && !$self->_tie_object->_is_a( $fh => 'HTTP::Promise::IO' ) ); my $encoded = $self->as_string; return( $self->pass_error ) if( !defined( $encoded ) ); $fh->print( $encoded ) || return( $self->error( "Unable to print on given filehandle '$fh': $!" ) ); return(1); } sub _is_warnings_enabled { return( warnings::enabled( $_[0] ) ); } # NOTE: FREEZE is inherited # NOTE: STORABLE_freeze is inherited # NOTE: STORABLE_thaw is inherited # NOTE: THAW is inherited 1; # NOTE: POD __END__ =encoding utf-8 =head1 NAME HTTP::Promise::Body::Form - x-www-form-urlencoded Data Class =head1 SYNOPSIS use HTTP::Promise::Body::Form; my $form = HTTP::Promise::Body::Form->new; my $form = HTTP::Promise::Body::Form->new( $hash_ref ); my $form = HTTP::Promise::Body::Form->new( q{e%3Dmc2} ); die( HTTP::Promise::Body::Form->error, "\n" ) if( !defined( $form ) ); =head1 VERSION v0.1.0 =head1 DESCRIPTION This class represents C HTTP body. It inherits from L This is different from a C. For this, please check the module L =head1 CONSTRUCTOR =head2 new This takes an optional data, and some options and returns a new L object. Acceptable data are: =over 4 =item An hash reference =item An url encoded string =back If a string is provided, it will be automatically decoded into an hash of name-value pairs. When a name is found more than once, its values are added as an array reference. my $form = HTTP::Promise::Body->new( 'name=John+Doe&foo=bar&foo=baz&foo=' ); Would result in a C object containing: name => 'John Doe', foo => ['bar', 'baz', ''] As an historical note, C is not an rfc-defined standard, and differs from URI encoding defined by L in that it uses C<+> to represent whitespace. It was L as a non-standard way of encoding form data. This also L and this L. =head1 METHODS L inherits all the methods from L, and adds or override the following ones. =head2 as_form_data This returns a new L object based on the current data, or upon error, sets an L and returns C. =head2 as_string This returns a properly urlencoded representation of the name-value pairs stored in this hash object. Each value will be encoded into utf8 before being urlencoded. This is all done fast with L =head2 decode Provided with an C string and this will return a decoded string taking under account utf8 characters. my $params = $form->decode( 'tengu=%E5%A4%A9%E7%8B%97' ); # [ 'tengu', '天狗' ] If an error occurs, this will set an L and return C =head2 decode_string Provided with an url-encoded string, included utf-8 string, and this returns its corresponding decoded version. my $deity = $form->decode( '%E5%A4%A9%E7%8B%97' ); results in: C<天狗> =head2 decode_to_array Takes an C string and returns an array reference of name-value pairs. If a name is seen more than once, its value will be an array reference. If an error occurs, this will set an L and return C =head2 decode_to_hash Takes an C string or an array reference of name-value pairs and returns an hash reference of name-value pairs. If a name is seen more than once, its value will be an array reference. If an error occurs, this will set an L and return C =head2 encode Takes an array reference or an hash reference and this returns a properly url-encoded string representation. If an error occurs, this will set an L and return C =head2 encode_string Takes a string and returns an encoded string. UTF-8 strings are ok too as long as they are in L. If an error occurs, this will set an L and return C =head2 length Returns the number of keys currently set in this key-value pairs held in the object. =head2 open This encodes the key-pairs as C by calling L, which returns a new L, opens it, passing whatever arguments it received to L and return the resulting object upon success, or upon error, sets an L and returns C =for Pod::Coverage pass_error =head2 print Provided with a valid filehandle, and this print the C representation of the key-value pairs contained in this object, to the given filehandle, or upon error, sets an L and returns C =head1 AUTHOR Jacques Deguest EFE =head1 SEE ALSO L, L L L, L, L, L, L, L, L, L, L, L, L, L, L, L, L, L =head1 COPYRIGHT & LICENSE Copyright(c) 2022 DEGUEST Pte. Ltd. All rights reserved. This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =cut