Skip Menu |

This queue is for tickets about the Types-ReadOnly CPAN distribution.

Report information
The Basics
Id: 87511
Status: resolved
Priority: 0/
Queue: Types-ReadOnly

People
Owner: Nobody in particular
Requestors: TIMB [...] cpan.org
Cc:
AdminCc:

Bug Information
Severity: Wishlist
Broken in: (no value)
Fixed in: (no value)



Subject: It would be nice if Type::Params could apply Hash::Util::lock_keys to Dict types
Type::Params does a great job of ensuring that the arguments to a sub are correct. It would be nice if it would take the next step of ensuring that access to the Dict fields are correct. For example: sub execute { my $self = shift; our $_execute_check ||= compile( slurpy Dict[ search_criteria => InstanceOf["foo"], mls_board_id => Id, sort_order => Str, rows_per_page => Int2_not_negative, page_number => Int4_not_negative, search_via => Maybe[StrMatch[qr{^(?:es|pg)$}]], inflate_via => Maybe[StrMatch[qr{^(?:es|pg)$}]], inflate => Maybe[HashRef], explain => Bool, ] ); my ($args) = $_execute_check->(@_); That's all fine, but I'd like to be sure that the code in the method doesn't miss-spell any of the key names. To do that I can call lock_keys but I have to duplicate the names of all the keys that I passed to compile(): lock_keys(%$args, qw( search_criteria mls_board_id sort_order rows_per_page page_number search_via inflate_via inflate explain )); Since compile() already has that information it could do that itself. I think this would be frequently used if made easy. As an interface, I see there's an (undocumented) initial \%options argument to compile(), So perhaps: compile( { lock_dict_keys => 1 }, ... )
On 2013-08-01T12:59:45+01:00, TIMB wrote: Show quoted text
> Since compile() already has that information it could do that itself. > I think this would be frequently used if made easy.
I agree with the idea of the feature, but not sure it belongs in Type::Params. It might be better off to to it in the type constraint/coercion, along the lines of: http://search.cpan.org/~ilmari/MooseX-Types-Ro-0.002/lib/MooseX/Types/Ro.pm So rather than using the options hash you'd do something like: compile(slurpy LockedDict[...]); That way, hashrefs with locked keys (and I'd also like to go further and offer fully read-only hashrefs/arrayrefs too) can be used not just with Type::Params, but also in Moo/Mouse/Moose attributes. I'd probably release that as a separate distribution though as I'm making an effort to get the Type::Tiny core distribution Perl 5.6 compatible. Lockable hash keys were introduced in Perl 5.8. I'll have a stab at this some time tomorrow.
Show quoted text
> So rather than using the options hash you'd do something like: > > compile(slurpy LockedDict[...]);
That seems like a nice approach, at least initially, but then I had second thoughts... I think it'll cause problems with type inheritance. Lets say I have a subtype of Dict called MyStruct and in some places I want it locked and in others I don't. I'd need to create two types: a MyStruct that's a subtype of Dict and a LockedMyStruct that's a subtype of LockedDict. These two types are equivalent yet incompatible. What I'd really want to do is create LockedMyStruct as a subtype of MyStruct. So locking seems like it's best implemented as a kind of attribute. Something like: declare LockedMyStruct, as MyStruct after_assign { lock_keys(%$_) } Here 'after_assign' is just a suggestion for the attribute name (and not a very good one). The key point is that it's a hook for code to be run after assignment. That has the great advantage of being completely general. No need for extra dependencies. (I've ignored inlining and other design issues just to keep the suggestion simple.) Show quoted text
> I'd probably release that as a separate distribution though as I'm > making an effort to get the Type::Tiny core distribution Perl 5.6 > compatible.
I'm puzzled by your interest in supporting 5.6. Now the toolchain-gang is requiring 5.8 it seems like the death of 5.6 will only accelerate. Show quoted text
> Lockable hash keys were introduced in Perl 5.8.
With the after_assign hook approach outlined above there no need for any extra dependencies. Show quoted text
> I'll have a stab at this some time tomorrow.
Great.
On 2013-08-02T09:50:57+01:00, TIMB wrote: Show quoted text
> I think it'll cause problems with type inheritance.
Point taken. I've taken a slightly different approach that makes it easy to recycle non-locked types into locked types. use Types::Standard qw( Dict Int ); use Types::ReadOnly qw( Locked ); my $dict = Dict[ n => Int ]; my $locked_dict = Locked[ $dict ]; Using is_a_type_of, $locked_dict appears to inherit from both $dict and Locked. Using is_strictly_a_type_of, it inherits from $dict. I haven't done coercions or inlining yet, but I think this approach seems reasonable.
Subject: locked-types.pl
use strict; use warnings; use Time::Limit; use Test::More; use Test::Fatal; use Test::TypeTiny; # Declared inline; don't try to load these thanks! no thanks 'Types::ReadOnly', 'Type::Tiny::Wrapped', 'Type::Tiny::Wrapper'; BEGIN { package Type::Tiny::Wrapped; use base 'Type::Tiny'; sub wrap { my $class = shift; my $type = Types::TypeTiny::to_TypeTiny($_[0]); my $wrapper = $_[1]; my $self = bless($type->create_child_type => $class); $self->{wrapper} = $wrapper; $self->{display_name} = sprintf('%s[%s]', $wrapper->display_name, $type->display_name); return $self; } sub wrapper { $_[0]{wrapper} } sub pre_check { $_[0]{wrapper}{pre_check} } sub pre_coerce { $_[0]{wrapper}{pre_coerce} } sub post_check { $_[0]{wrapper}{post_check} } sub post_coerce { $_[0]{wrapper}{post_coerce} } sub _build_compiled_check { my $self = shift; my $pre = $self->pre_check; my $orig = $self->parent->compiled_check(@_); my $post = $self->post_check; return $orig unless $pre || $post; return sub { local $_ = $_[0]; return if defined($pre) && !$pre->(@_); return if !$orig->(@_); return if defined($post) && !$post->(@_); return !!1; }; } sub _strict_check { my $self = shift; local $_ = $_[0]; my $pre = $self->pre_check; my $post = $self->post_check; return if defined($pre) && !$pre->(@_); return if !$self->parent->_strict_check(@_); return if defined($post) && !$post->(@_); return !!1; } sub is_subtype_of { my $self = shift; $self->wrapper->is_a_type_of(@_) or $self->SUPER::is_subtype_of(@_); } sub inline_check { ...; } sub can_be_inlined { 0; } }; BEGIN { package Type::Tiny::Wrapper; use base 'Type::Tiny'; use Scalar::Util 'weaken'; sub new { my $class = shift; my $self = $class->SUPER::new(@_); Type::Tiny::_croak("Type::Tiny::Wrapper types must not have a constraint!") unless $self->_is_null_constraint; return $self; } my @FIELDS = qw/ pre_check pre_coerce post_check post_coerce /; sub pre_check { $_[0]{'pre_check'} } sub pre_coerce { $_[0]{'pre_coerce'} } sub post_check { $_[0]{'post_check'} } sub post_coerce { $_[0]{'post_coerce'} } sub child_type_class { +__PACKAGE__ } sub create_child_type { my $self = shift; $self->SUPER::create_child_type( ( map { exists($self->{$_}) ? ($_ => $self->{$_}) : () } @FIELDS ), @_, ); } sub has_constraint_generator { 1 } sub constraint_generator { my $self = shift; weaken $self; return sub { 'Type::Tiny::Wrapped'->wrap(shift, $self) }; } }; BEGIN { package Types::ReadOnly; use Types::Standard qw(Any); use Type::Utils; use Type::Library -base, -declare => qw(Locked); use Hash::Util qw( hashref_locked lock_ref_keys ); declare Locked, bless => 'Type::Tiny::Wrapper', pre_check => sub { hashref_locked($_) }, ; }; use Data::Dumper; use Types::Standard -types; use Types::ReadOnly -types; use Hash::Util qw( hashref_locked lock_ref_keys ); my $my_hash = HashRef[ Undef ]; my $my_lock = Locked[ $my_hash ]; isa_ok(Locked, 'Type::Tiny::Wrapper'); isa_ok($my_lock, 'Type::Tiny::Wrapped'); ok( $my_lock->is_a_type_of(Locked), '$my_lock->is_a_type_of(Locked)'); ok( $my_lock->is_a_type_of($my_hash), '$my_lock->is_a_type_of($my_hash)'); ok(!$my_lock->is_strictly_a_type_of(Locked), '!$my_lock->is_strictly_a_type_of(Locked)'); ok( $my_lock->is_strictly_a_type_of($my_hash), '$my_lock->is_strictly_a_type_of($my_hash)'); my $hash1 = { foo => undef }; my $hash2 = { foo => undef }; lock_ref_keys($hash2); my $hash3 = { foo => "xxx" }; lock_ref_keys($hash3); like( exception { my $bar = $hash2->{bar} }, qr{^Attempt to access disallowed key 'bar' in a restricted hash}, 'hashes can be locked', ); should_pass($hash1, $my_hash); should_pass($hash2, $my_hash); should_fail($hash3, $my_hash); should_fail($hash1, $my_lock); should_pass($hash2, $my_lock); should_fail($hash3, $my_lock); done_testing;
Show quoted text
> my $dict = Dict[ n => Int ]; > my $locked_dict = Locked[ $dict ];
Nice. I wonder about the "Locked" name though, given the number of variants in https://metacpan.org/module/Hash::Util Perhaps LockedHashKeys[...] LockedHash[...] LockedHashRecurse[...] Also, I wonder if Dict[ ..., Optional[...] ] fields will need any special handling.
Posting progress to gist now because RT seems to show the entire attached script inline on the ticket. This supports inlining and coercions, but not inlined coercions yet. https://gist.github.com/tobyink/6147246
There is now a preview of Types::ReadOnly on CPAN: https://metacpan.org/release/TOBYINK/Types-ReadOnly-0.000_02 I'll close this ticket once I'm able to move it to the Types-ReadOnly RT queue, which won't be created until I've released a stable version.