package Variable::Strongly::Typed; use version; $VERSION = qv('1.1.0'); use warnings; use strict; use Carp; # Tie any 'TYPE' attributes to me use Attribute::Handlers; # Our 3 friends for scalars & arrays & hashes use Variable::Strongly::Typed::Scalar; use Variable::Strongly::Typed::Array; use Variable::Strongly::Typed::Hash; use Variable::Strongly::Typed::Validators; use Variable::Strongly::Typed::Code; # Can't use Class::Std as it wants the 'ATTR' attribute # as does Attribute::Handlers... Bummer use Class::Std::Utils; { # class variables my %actual_object_of; # The actual object ref my %validator_sub_of; # Validate subroutine my %type_of; # Strong Type of this variable my %error_handler_of; # Error handling function to call when # assignment/return goes wrong do { no warnings 'redefine'; sub UNIVERSAL::TYPE : ATTR(SCALAR) { my ($package, $symbol, $referent, $attr, $data) = @_; tie $$referent, 'Variable::Strongly::Typed::Scalar', $data; return; } sub UNIVERSAL::TYPE : ATTR(ARRAY) { my ($package, $symbol, $referent, $attr, $data) = @_; tie @$referent, 'Variable::Strongly::Typed::Array', $data; return; } sub UNIVERSAL::TYPE : ATTR(HASH) { my ($package, $symbol, $referent, $attr, $data) = @_; tie %$referent, 'Variable::Strongly::Typed::Hash', $data; return; } sub UNIVERSAL::TYPE : ATTR(CODE) { my ($package, $symbol, $referent, $attr, $data) = @_; Variable::Strongly::Typed::Code->new($symbol, $referent, $data); return; } }; # Set up our class variables sub _init { my($self, $object, $type) = @_; my $ident = ident $self; my $error_handler; if (ref $type eq 'ARRAY') { ($type, $error_handler) = @$type; croak("Error handler must be a CODE ref!") if (ref $error_handler ne 'CODE'); } $actual_object_of {$ident} = $object; $validator_sub_of {$ident} = _make_validator_sub($type); $type_of {$ident} = $type; $error_handler_of {$ident} = $error_handler; return $self; } # Get actual object ref sub _get_object { my($self) = @_; return $actual_object_of{ident $self}; } sub _get_type { my($self) = @_; return $type_of{ident $self}; } sub _get_valid_sub { my($self) = @_; return $validator_sub_of{ident $self}; } sub _get_error_handler { my($self) = @_; return $error_handler_of{ident $self}; } sub _error { my($self, $val) = @_; my $eh = $self->_get_error_handler; if ($eh) { $eh->($val); } else { croak($self->_make_error_message($val)); } } # Check if a value is okay... # IF this returns w/o croaking then all is okay... sub _check_values { my ($self, @values) = @_; my($valid_sub) = $self->_get_valid_sub; # if valid or failed sub says it's okay then ok # otherwise undef # Note this works slightly differently then Tie::Constrained # as we have no notion of an 'original' or 'default' value # 'failed' sub will croak if not valid... foreach my $val (@values) { # This'll croak (by default) if a value is not valid $valid_sub->($val) || $self->_error($val); } # if we got this far all must be good return \@values; } sub _make_error_message { my($self, $val) = @_; # 'num', 'string', 'IO::File' or CODE ref if user defined my $valid_type = $self->_get_type; # Scalar, Array, or Hash my ($variable_type) = $self =~ /:: ([^:=]+) =/xms; $variable_type = lc $variable_type; # String-ify value if (ref $val) { $val = (ref $val) . ' reference'; } else { $val = "'$val'"; } my $ww = $variable_type eq 'code' ? 'return' : 'assign'; my $from_or_to = $variable_type eq 'code' ? 'from' : 'to'; $variable_type = 'function or method' if $variable_type eq 'code'; my $msg; if (ref $valid_type eq 'CODE') { $msg = "Cannot $ww $val $from_or_to a user-validated" . " $variable_type variable!"; } else { $msg = "Cannot $ww $val $from_or_to a" . ($variable_type eq 'array' ? 'n' : '') . " $variable_type of type $valid_type!"; } return $msg; } sub _make_validator_sub { my($val) = @_; # User supplied?? return $val if ($val && (ref($val) eq 'CODE')); my $condition; # Stuff like 'int', 'string', 'float' ... 'primitive' types $condition = $Variable::Strongly::Typed::Validators::conditions{$val}; unless ($condition) { # It's either SCALAR, ARRAY, HASH the exact object type # or an isa... A reference type basically $condition = 'ref && ((ref eq ' . "'$val'" . ')' . ' || (ref ne "SCALAR" && ref ne "ARRAY"' . ' && ref ne "HASH" && $_->isa(' . "'$val'" . ')))'; } # Concoct validate sub my $sub = eval 'sub { local $_ = shift;' . ' !$_ || (' . $condition . ') }'; if ($@) { croak("Unable to create a Strongly Typed $val!!"); } return $sub; } # Clean Up, Clean Up, Everybody's Doing Some, Clean Up # -Walter (26 months) sub DESTROY { my($self) = @_; my $ident = ident $self; my $obj = $self->_get_object; undef $obj; delete $actual_object_of{$ident}; delete $validator_sub_of{$ident}; delete $type_of {$ident}; } } 1; # Magic true value required at end of module __END__ =head1 NAME Variable::Strongly::Typed - Let some variables be strongly typed =head1 VERSION This document describes Variable::Strongly::Typed version 1.0.0 =head1 SYNOPSIS use Variable::Strongly::Typed; my $int :TYPE('int'); # must have an 'int' value my $float :TYPE('float'); # must have a 'float' value my $string :TYPE('string'); # must not be a reference my $file :TYPE('IO::File'); # must be an IO::File my @array_of_ints :TYPE('int'); # Each slot must contain # an int my %hash_of_floats :TYPE('float'); # Each value must be a float my $int_own_error :TYPE('int', \&my_own_error_handler); # Roll my own error handler my @array_of_rgb :TYPE(\&red_green_blue); # my enumerated type # For subs!! sub return_an_int :TYPE('int') { # .. do some stuff .. return $something; } # ... and later ... $int = 23; # All is well $int = 'howdy!'; # This line will croak with a good error message $float = 3.23; # All is well, nothing to see here $float = new XML::Parser; # croak! $array_of_ints[23] = 44; # Groovy $array_of_ints[12] = 'yah'; # croak! $hash_of_floats{pi} = 3.14159; # no problem $hash_of_floats{e} = new IO::File; # croak! # Return 1 if this val is RED, BLUE, or GREEN # 0 otherwise sub red_green_blue { local $_ = shift; /\A RED \z/xms || /\A BLUE \z/xms || /\A GREEN \z/xms; } $array_of_my_very_own_types[23] = 99; # croak! $array_of_my_very_own_types[2] = 'BLUE'; # OK! $int_own_error = 'lksdklwe'; # The sub 'my_own_error_hanlder' # will be # called with the # offending value my $got_it = return_an_int(); # Will 'croak' (or call your error # function) # if this sub doesn't # return an 'int' =head1 DESCRIPTION This modules allow you to strongly type your variables. Also known as the 'no fun' module - it can greatly enhance you code's quality and robustness. By enforcing types on some (or all) of your variables you will eliminate a large class of careless (& not so careless) errors. This could also aid an editor or code-browsing tools to verify code correctness without having to execute the script. =head1 INTERFACE Variable types are specified using the 'TYPE' attribute. There are currently 7 builtin types: =over 16 =item 'int' => /\A \d+ \z/xms; =item 'string' => anything that's not a reference (!ref) =item 'float' => standard float regex =item 'bool' => same as 'int' =item 'SCALAR' => unblessed scalar ref =item 'ARRAY' => unblessed array ref =item 'HASH' => unblessed hash ref =back You can also specify a class name like 'XML::Parser' - note this is NOT the same as 'HASH' which represents an unblessed HASH reference. Note when specifying 'XML::Parser' assigning 'XML::Parser' or any subclass of 'XML::Parser' will be valid - it does a $value->isa('XML::Parser') (or whatever the type you specified was) to test the value. If you try to assign an illegal value to your variable the assignment will croak with an informative error message. If you specify a second parameter in your 'TYPE' attribute it will be treated as a CODE ref to be called when the assignment will fail. All builtin types allow 'undef' or any false value as a valid value. Your own types (see section below) should probably do the same. =head1 ROLLING YOUR OWN TYPES NOTE: You do NOT have to roll your own type to just specify a blessed object - see the section above! Specifying a CODE ref in your TYPE() attribute allows you to define your own types. Your function will be call with 1 parameter - the value that's about to be assigned to any variable of that TYPE(). Returning 1 (or any true value) will allow the assignment to happen. Return 0 (or any false value) will cause to assignment to croak. I'm sure you can dream up of all sorts of interesting types - enumerated types, all sorts of specific string types, bounded numbers, words only from a specific language are the sorts of things that pop into my mind. Again I heartily expect you to surprise me with the types you come up with. Note also you must allow for 'undef' as that is the initial value your variable will have! Allowing for any false value may also be a good idea. =head1 FUNCTION RETURN VALUES By specifying a TYPE() attribute for a function you 'lock-down' that function to only return that type. If your function returns an array every element of that array must be of the specified type. =head1 ROLL YOUR OWN ERROR FUNCTION By default Variable::Strongly::Typed croaks when an assignment fails. You can change this behavior by supplying a code ref as a second parameter in your TYPE() attribute. If you do this, your function will be called instead of 'croak' with 1 parameter, the value to be assigned to the variable that failed. If your function does not 'croak' or 'die' the assignment will occur so be careful! =head1 DIAGNOSTICS croak'ed error messages for built-in types look like: Cannot assign to a [ scalar | hash | array ] of type croak'ed error messages for user-supplied types look like: Cannot assign to a user-validated [ scalar | hash | array ] variable croak'ed error messages from an erroneous subroutine return value look like: Cannot return from a function or method of type If you'ved typed your subroutine & call it in a void context you'll get a croak like: Throwing away return value of =head1 CONFIGURATION AND ENVIRONMENT Variable::Strongly::Typed requires no configuration files or environment variables. =head1 DEPENDENCIES Attribute::Handlers handle the attribute stuff Class::Std::Utils handle the basic 'inside-out' module work Test::More for testing version for versioning =head1 INCOMPATIBILITIES None reported. =head1 BUGS AND LIMITATIONS No bugs have been reported. Please report any bugs or feature requests to C, or through the web interface at L. =head1 SEE ALSO Attributes::Types - The main differences are you can type subroutines with this monster and you can specify a different error handler other than the default of 'croak'. Oh the code is a hella cleaner also. Other than that these two modules are very similar. =head1 AUTHOR Mark Ethan Trostler C<< >> =head1 LICENCE AND COPYRIGHT Copyright (c) 2005, Mark Ethan Trostler C<< >>. All rights reserved. This module is free software; you can redistribute it and/or modify it under the same terms as Perl itself. See L. =head1 DISCLAIMER OF WARRANTY BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR, OR CORRECTION. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.