Skip to content

Instantly share code, notes, and snippets.

@j1n3l0
Created March 17, 2021 11:10
Show Gist options
  • Save j1n3l0/349cb42fd3329fc6e672b4251b41cb7e to your computer and use it in GitHub Desktop.
Save j1n3l0/349cb42fd3329fc6e672b4251b41cb7e to your computer and use it in GitHub Desktop.
Trying to avoid mutating a readonly attribute
use Test2::V0 -target => 'Client';
subtest 'A class with a [readonly, class_type("URI")] attribute' => sub {
my $uri = 'https://example.com';
my $object = $CLASS->new( service_uri => $uri );
is(
$object,
object { call service_uri => $uri },
'should return the attribute unchanged',
);
$object->get('path');
is(
$object,
object { call service_uri => $uri },
'should not update the attribute at a distance',
);
};
done_testing();
package Client;
use Moo;
use Type::Utils qw< class_type >;
use Types::Standard -types;
use URI;
use experimental qw< signatures >;
has service_uri => (
coerce => 1,
is => 'ro',
isa => class_type('URI')->plus_constructors(Str, 'new'),
);
sub get ( $self, $path ) { $self->service_uri->clone->path_segments($path) }
1;
@j1n3l0
Copy link
Author

j1n3l0 commented Mar 23, 2021

Interestingly, the Object::Pad solution has the same limitations:

use Object::Pad 0.36;

package Client;
class Client;

use Type::Utils qw< class_type >;
use Types::Standard -types;
use URI;

my $Uri = class_type('URI')->plus_constructors(Str, 'new');

has $service_uri :reader;

BUILD (%args) { $service_uri = $Uri->coerce( $args{service_uri} ) };

method get($path) { $service_uri->clone->path_segments($path) }

1;

@j1n3l0
Copy link
Author

j1n3l0 commented Mar 24, 2021

You could use the around method modifier to ensure that you are only ever passed a clone on the original object:

package Client;

use Moo;
use Type::Utils qw< class_type >;
use Types::Standard -types;
use URI;

use experimental qw< signatures >;

has service_uri => (
    coerce => 1,
    is     => 'ro',
    isa    => class_type('URI')->plus_constructors(Str, 'new'),
);

around service_uri => sub ( $orig, $self ) {
    $self->$orig()->clone();
};

sub get ( $self, $path ) {
    $self->service_uri->path_segments($path);
}

1;

The same solution exists for Object::Pad:

use Object::Pad 0.36;

package Client;
class Client;

use Class::Method::Modifiers qw< around >;
use Type::Utils qw< class_type >;
use Types::Standard -types;
use URI;

my $Uri = class_type('URI')->plus_constructors(Str, 'new');

has $service_uri :reader;

BUILD (%args) {
    $service_uri = $Uri->coerce( $args{service_uri} );
};

around service_uri => sub ( $orig, $self ) {
    $self->$orig()->clone();
};

method get($path) {
    $self->service_uri->path_segments($path);
}

1;

But the correct way to solve this with Object::Pad (as of v0.51) is:

use Object::Pad 0.51;

package Client;
class Client;

use Type::Utils qw< class_type >;
use Types::Standard -types;
use URI;

my $Uri = class_type('URI')->plus_constructors(Str, 'new');

has $service_uri :param;

ADJUST { $service_uri = $Uri->assert_coerce($service_uri) };

method service_uri () { $service_uri->clone() }

method get ($path) { $self->service_uri->path_segments($path) }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment