# Copyright (c) 2025 Yuki Kimoto
# MIT License
class HTTP::Tiny::Content::Single extends HTTP::Tiny::Content {
version_from HTTP::Tiny;
allow HTTP::Tiny::Content::MultiPart;
use HTTP::Tiny::Asset::Memory;
use HTTP::Tiny::Content::MultiPart;
# Fields
has asset : rw HTTP::Tiny::Asset;
has auto_upgrade : ro byte;
# Undocumented Fields
has read : HTTP::Tiny::EventEmitter::Callback;
has body_size : int;
static method new : HTTP::Tiny::Content::Single ($options : object[] = undef) {
my $self = new HTTP::Tiny::Content::Single;
$self->{auto_upgrade} = 1;
$self->{asset} = HTTP::Tiny::Asset::Memory->new({auto_upgrade => 1});
$self->init($options);
$self->{read} = method : void ($that : HTTP::Tiny::Content::Single, $args : object[]){
my $chunk = $args->[0]->(string);
$that->set_asset($that->asset->add_chunk($chunk));
};
$self->on(read => $self->{read});
return $self;
}
method body_contains : int ($chunk : string) {
return $self->asset->contains($chunk) >= 0;
}
method body_size : int () {
if ($self->is_dynamic) {
my $content_length = $self->headers->content_length;
my $body_size = 0;
eval { $body_size = Fn->to_int($content_length); };
return $body_size;
}
$self->{body_size} = (int)$self->asset->size;
return $self->{body_size};
}
method clone : HTTP::Tiny::Content::Single () {
my $clone = (HTTP::Tiny::Content::Single)$self->SUPER::clone;
unless ($clone) {
return undef;
}
$clone->{asset} = $self->asset;
return $clone;
}
method get_body_chunk : string ($offset : int) {
if ($self->is_dynamic) {
return $self->generate_body_chunk($offset) ;
}
return $self->asset->get_chunk($offset);
}
method parse : HTTP::Tiny::Content ($chunk : string) {
# Parse headers
$self->_parse_until_body($chunk);
# Parse body
unless ($self->auto_upgrade && $self->boundary) {
$self->SUPER::parse(undef);
return (HTTP::Tiny::Content)$self;
}
# Content needs to be upgraded to multipart
$self->unsubscribe(read => $self->{read});
my $multi = HTTP::Tiny::Content::MultiPart->new_from_single($self);
$self->emit(upgrade => [(object)$multi]);
$multi->parse(undef);
return (HTTP::Tiny::Content)$multi;
}
method is_multipart : int () {
return 0;
}
}
__END__
TODO
package Mojo::Content::Single;
use Mojo::Base 'Mojo::Content';
use Mojo::Asset::Memory;
use Mojo::Content::MultiPart;
has asset => sub { Mojo::Asset::Memory->new(auto_upgrade => 1) };
has auto_upgrade => 1;
sub body_contains { shift->asset->contains(shift) >= 0 }
sub body_size {
my $self = shift;
return ($self->headers->content_length || 0) if $self->is_dynamic;
return $self->{body_size} //= $self->asset->size;
}
sub clone {
my $self = shift;
return undef unless my $clone = $self->SUPER::clone();
return $clone->asset($self->asset);
}
sub get_body_chunk {
my ($self, $offset) = @_;
return $self->generate_body_chunk($offset) if $self->is_dynamic;
return $self->asset->get_chunk($offset);
}
sub new {
my $self = shift->SUPER::new(@_);
$self->{read} = $self->on(read => sub { $_[0]->asset($_[0]->asset->add_chunk($_[1])) });
return $self;
}
sub parse {
my $self = shift;
# Parse headers
$self->_parse_until_body(@_);
# Parse body
return $self->SUPER::parse unless $self->auto_upgrade && defined $self->boundary;
# Content needs to be upgraded to multipart
$self->unsubscribe(read => $self->{read});
my $multi = Mojo::Content::MultiPart->new(%$self);
$self->emit(upgrade => $multi);
return $multi->parse;
}
1;
=encoding utf8
=head1 NAME
Mojo::Content::Single - HTTP content
=head1 SYNOPSIS
use Mojo::Content::Single;
my $single = Mojo::Content::Single->new;
$single->parse("Content-Length: 12\x0d\x0a\x0d\x0aHello World!");
say $single->headers->content_length;
=head1 DESCRIPTION
=head1 EVENTS
L<Mojo::Content::Single> inherits all events from L<Mojo::Content> and can emit the following new ones.
=head2 upgrade
$single->on(upgrade => sub ($single, $multi) {...});
Emitted when content gets upgraded to a L<Mojo::Content::MultiPart> object.
$single->on(upgrade => sub ($single, $multi) {
return unless $multi->headers->content_type =~ /multipart\/([^;]+)/i;
say "Multipart: $1";
});
=head1 ATTRIBUTES
L<Mojo::Content::Single> inherits all attributes from L<Mojo::Content> and implements the following new ones.
=head2 asset
my $asset = $single->asset;
$single = $single->asset(Mojo::Asset::Memory->new);
The actual content, defaults to a L<Mojo::Asset::Memory> object with L<Mojo::Asset::Memory/"auto_upgrade"> enabled.
=head2 auto_upgrade
my $bool = $single->auto_upgrade;
$single = $single->auto_upgrade($bool);
Try to detect multipart content and automatically upgrade to a L<Mojo::Content::MultiPart> object, defaults to a true
value.
=head1 METHODS
L<Mojo::Content::Single> inherits all methods from L<Mojo::Content> and implements the following new ones.
=head2 body_contains
my $bool = $single->body_contains('1234567');
Check if content contains a specific string.
=head2 body_size
my $size = $single->body_size;
Content size in bytes.
=head2 clone
my $clone = $single->clone;
Return a new L<Mojo::Content::Single> object cloned from this content if possible, otherwise return C<undef>.
=head2 get_body_chunk
my $bytes = $single->get_body_chunk(0);
Get a chunk of content starting from a specific position. Note that it might not be possible to get the same chunk
twice if content was generated dynamically.
=head2 new
my $single = Mojo::Content::Single->new;
my $single = Mojo::Content::Single->new(asset => Mojo::Asset::File->new);
my $single = Mojo::Content::Single->new({asset => Mojo::Asset::File->new});
Construct a new L<Mojo::Content::Single> object and subscribe to event L<Mojo::Content/"read"> with default content
parser.
=head2 parse
$single = $single->parse("Content-Length: 12\x0d\x0a\x0d\x0aHello World!");
my $multi = $single->parse("Content-Type: multipart/form-data\x0d\x0a\x0d\x0a");
Parse content chunk and upgrade to L<Mojo::Content::MultiPart> object if necessary.
=head1 SEE ALSO
=cut