package Class::Meta::Express; # $Id: Express.pm 3690 2008-05-02 02:11:45Z david $ use strict; use vars qw($VERSION); use Class::Meta; $VERSION = '0.05'; my %meta_for; sub import { my $pkg = shift; my $caller = caller; no strict 'refs'; return shift if defined &{"$caller\::meta"}; *{"$caller\::$_"} = $pkg->can($_) for qw(class meta ctor has method build); return shift; } sub class (&) { my $code = shift; goto sub { $code->(); goto &build; }; } sub meta { my $caller = caller; my $key = shift; my $args = ref $_[0] eq 'HASH' ? $_[0] : { @_ }; $args->{key} = $key; _export(delete $args->{reexport}, $caller, $args) if $args->{reexport}; my $meta_class = delete $args->{meta_class} || 'Class::Meta'; my $def_type = delete $args->{default_type}; my $meta = $meta_class->new( package => $caller, %{ $args } ); $meta_for{$caller} = [ $meta, $def_type ]; return $meta; } sub ctor { unshift @_, 'constructor'; goto &_meth; } sub has { my ($meta, $def_type) = @{ $meta_for{ scalar caller } }; unshift @_, $meta, 'name'; splice @_, 3, 1, %{ $_[3] } if ref $_[3] eq 'HASH'; splice @_, 3, 0, type => $def_type if $def_type; goto &{ $meta->can('add_attribute') }; } sub method { unshift @_, 'method'; goto &_meth; } sub build { my $meta = delete $meta_for{ my $caller = caller }->[0]; # Remove exported functions. _unimport($caller); # Build the class. unshift @_, $meta; goto &{ $meta->can('build') }; } sub _meth { my $method = 'add_' . shift; my $meta = $meta_for{ scalar caller }->[0]; unshift @_, $meta, 'name'; if (my $ref = ref $_[3]) { if ($ref eq 'CODE') { splice @_, 3, 0, 'code'; } else { splice @_, 3, 1, %{ $_[3] } if $ref eq 'HASH'; } } goto &{ $meta->can($method) }; } sub _unimport { my $caller = shift; for my $fn (qw(class meta ctor has method build)) { no strict 'refs'; my $name = "$caller\::$fn"; # Copy the current glob contents, excluding CODE. my %things = map { $_ => *{$name}{$_} } grep { defined *{$name}{$_} } qw(SCALAR ARRAY HASH IO FORMAT); # Undefine the glob and reinstall the contents. undef *{$name}; *{$name} = $things{$_} for keys %things; } } sub _export { my ($export, $pkg, $args) = @_; my @args = map { $_ => $args->{$_} } grep { $args->{$_} } qw(meta_class default_type); my $meta = !@args ? \&meta : sub { splice @_, 1, 0, @args; goto &Class::Meta::Express::meta; }; $export = 0 unless ref $export eq 'CODE'; no strict 'refs'; *{"$pkg\::import"} = sub { my $caller = caller; no strict 'refs'; unless (defined &{"$caller\::meta"}) { *{"$caller\::meta"} = $meta; *{"$caller\::$_"} = \&{__PACKAGE__ . "::$_"} for qw(class ctor has method build); } goto $export if $export; return shift; }; } 1; __END__ ############################################################################## =begin comment Fake-out Module::Build. Delete if it ever changes to support =head1 headers other than all uppercase. =head1 NAME Class::Meta::Express - Concise, expressive creation of Class::Meta classes =end comment =head1 Name Class::Meta::Express - Concise, expressive creation of Class::Meta classes =head1 Synopsis package My::Contact; use Class::Meta::Express; class { meta contact => ( default_type => 'string' ); has 'name'; has contact => ( required => 1 ); } =head1 Description This module provides an interface to concisely yet expressively create classes with L. Although I am of course fond of L, I've never been overly thrilled with its interface for creating classes: package My::Thingy; use Class::Meta; BEGIN { # Create a Class::Meta object for this class. my $cm = Class::Meta->new( key => 'thingy' ); # Add a constructor. $cm->add_constructor( name => 'new' ); # Add a couple of attributes with generated accessors. $cm->add_attribute( name => 'id', is => 'integer', required => 1, ); $cm->add_attribute( name => 'name', is => 'string', required => 1, ); $cm->add_attribute( name => 'age', is => 'integer', ); # Add a custom method. $cm->add_method( name => 'chk_pass', code => sub { return 'code' }, ); $cm->build; } This example is relatively simple; it can get a lot more verbose. But even still, all of the method calls were annoying. I mean, whoever thought of using an object oriented interface for I a class? (Oh yeah: I did.) I wasn't alone in wanting a more declarative interface; Curtis Poe, with my blessing, created L, which would use this syntax to create the same class: package My::Thingy; use Class::Meta::Declare ':all'; Class::Meta::Declare->new( # Create a Class::Meta object for this class. meta => [ key => 'thingy', ], # Add a constructor. constructors => [ new => { } ], # Add a couple of attributes with generated accessors. attributes => [ id => { type => $TYPE_INTEGER, required => 1, }, name => { required => 1, type => $TYPE_STRING, }, age => { type => $TYPE_INTEGER, }, ], # Add a custom method. methods => [ chk_pass => { code => sub { return 'code' }, } ] ); This approach has the advantage of being a bit more concise, and it I declarative, but I find all of the indentation levels annoying; it's hard for me to figure out where I am, especially if I have to define a lot of attributes. And finally, I is a string with this syntax, except for those ugly read-only scalars such as C<$TYPE_INTEGER>. So I can't easily tell where one attribute ends and the next one starts. Bleh. What I wanted was an interface with the visual distinctiveness of the original Class::Meta syntax but with the declarative approach and intelligent defaults of Class::Meta::Declare, while adding B to the mix. The solution I've come up with is the use of temporary functions imported into a class only until the end of the class declaration: package My::Thingy; use Class::Meta::Express; class { # Create a Class::Meta object for this class. meta 'thingy'; # Add a constructor. ctor new => ( ); # Add a couple of attributes with generated accessors. has id => ( is => 'integer', required => 1 ); has name => ( is => 'string', required => 1 ); has age => ( is => 'integer' ); # Add a custom method. method chk_pass => sub { return 'code' }; } That's much better, isn't it? In fact, we can simplify it even more by setting a default data type and eliminating the empty lists: package My::Thingy; use Class::Meta::Express; class { # Create a Class::Meta object for this class. meta thingy => ( default_type => 'integer' ); # Add a constructor. ctor 'new'; # Add a couple of attributes with generated accessors. has id => ( required => 1 ); has name => ( is => 'string', required => 1 ); has 'age'; # Add a custom method. method chk_pass => sub { return 'code' }; } Not bad, eh? I have to be honest: I borrowed the syntax from L. Thanks for the idea, Stevan! =head1 Interface Class::Meta::Express exports the following functions into any package that Cs it. But beware! The functions are temporary! Once the class is declared, the functions are all removed from the calling package, thereby avoiding name space pollution I allowing you to create your own functions or methods with the same names, if you like, after declaring the class. =head2 Functions =head3 meta meta 'thingy'; This function creates and returns the C object that creates the class. The first argument must be the key to use for the class, which will be passed as the C parameter to C<< Class::Meta->new >>. Otherwise, it takes the same parameters as C<< Class::Meta->new >>, as well as the following additions: =over =item meta_class If you've subclassed Class::Meta and want to use your subclass to define your classes instead of Class::Meta itself, specify the subclass with this parameter. =item default_type The name of a data type that you'd like to be the default for all attributes created with C that don't specify their own data types. =item reexport Installs an C method into the calling name space that exports the express functions. The trick is that, if you've specified values for the C and/or C parameters, they will be used in the C function exported by your class! For example: package My::Base; use Class::Meta::Express; meta base => ( meta_class => 'My::Meta', default_type => 'string', reexport => 1, ); build; And now other classes can use My::Base instead of Class::Meta::Express and get the same defaults. Say that you want My::Contact to inherit from My::Base and use its defaults. Just do this: package My::Contact; use My::Base; # Forces import() to be called. use base 'My::Base'; meta 'contact'; # Uses My::Meta has 'name' # Will be a string. build; If you need your own C method to export stuff, just pass it to the reexport parameter: meta base => ( meta_class => 'My::Meta', default_type => 'string', reexport => sub { ... }, ); Class::Meta::Express will do the right thing by shifting execution to your import method after it finishes its dirty work. =back The parameters may be passed as either a list, as above, or as a hash reference: meta base => { meta_class => 'My::Meta', default_type => 'string', reexport => 1, }; =head3 ctor ctor 'new'; Calls C on the Class::Meta object created by C, passing the first argument as the C parameter. All other arguments can be any of the parameters supported by C: ctor new => ( label => 'Foo' ); Or, a if you have Class::Meta 0.53 or later, the second argument can be a code reference that will be passed as the C parameter to C: ctor new => sub { bless {} => shift }; If you want to specify other parameters I the code parameter, do so explicitly: ctor new => ( label => 'Foo', code => sub { bless {} => shift }, ); The parameters may be passed as either a list, as above, or as a hash reference: ctor new => { label => 'Foo', code => sub { bless {} => shift }, }; =head3 has has name => ( is => 'string' ); Calls C on the Class::Meta object created by C, passing the first argument as the C parameter. All other arguments can be any of the parameters supported by C, as in the example above. If the C parameter was specified in the call to C, then the type (or C if you have Class::Meta 0.53 or later and prefer it) can be omitted unless you need a different type: meta thingy => ( default_type => 'string' ); has 'name'; # Will be a string. has id => ( is => 'integer' ); # ... The parameters may be passed as either a list, as above, or as a hash reference: has id => { is => 'integer' }; =head3 method method 'say'; Calls C on the Class::Meta object created by C, passing the first argument as the C parameter. An optional second argument can be used to define the method itself (if you have Class::Meta 0.51 or later): method say => sub { shift; print @_, $/; } Otherwise, you'll have to define the method in the class itself (as was required in Class::Meta 0.50 and earlier). If you want to specify other parameters to C, just pass them after the method name and explicitly mix in the C parameter if you need it: method say => ( view => Class::Meta::Protected, code => sub { shift; print @_, $/; }, ); All other arguments can be any of the parameters supported by C The parameters may be passed as either a list, as above, or as a hash reference: method say => { view => Class::Meta::Protected, code => sub { shift; print @_, $/; }, }; =head3 class class { # Declare class. } Yes, the C keyword is secretly a function. It takes a single argument, a code reference, for which may omit the C keyword. Cute, eh?. It simply executes the code refernce passed as its sole argument, removes the C, C, C, C, C, and C functions from the calling name space, and then calls C on the Class::Meta object created by C. =head3 build build; This function is a deprecated holdover from before version 0.05. It used to be that there was no C keyword and you had to just call the rest of the above functions and then call C when you're done. But who liked I? It was actually a bitter pill among all this sweet, sweet sugar. But no more; C will likely be removed in a future version. =head1 Overriding Functions It is possible to override the functions exported by this module by subclassing it (after a fashion). Say that you wanted to change the C function so that it forces all attributes to default to a the type "string". Just override the function like so: package My::Express; use base 'Class::Meta::Express'; sub meta { splice @_, 1, 0, default_type => 'string'; goto &Class::Meta::Express::meta; } The trick here is to set C<@_> and then C. This is so that the package that calls this function will be seen as the caller and therefore the Class::Meta object will be properly created for that package. Why would you want to do all this? Well, perhaps you're building a I of classes and don't want to have to repeat yourself so much. So now all you have to do is use your My::Express module instead of Class::Meta::Express: package My::Person; use My::Express; class { meta person => (); has name => (); } And now you've created a new class with the string type attribute "name". =head1 See Also =over =item L This is the module that's actually doing all the work. Class::Meta::Express just offers a sweeter interface for creating new classes with Class::Meta. You'll still want to know all about Class::Meta's introspection capabilities, type constraints, and more. Check it out! =item L Curtis Poe's declarative inteface to Class::Meta. Deprecated in favor of this module. =back =head1 To Do =over =item * Make it so that the C parameter can work with an C method that's already installed in a module. =back =head1 Bugs Please send bug reports to . =head1 Author =begin comment Fake-out Module::Build. Delete if it ever changes to support =head1 headers other than all uppercase. =head1 AUTHOR =end comment David Wheeler =head1 Copyright and License Copyright (c) 2006 Kineticode, Inc. All Rights Reserved. This module is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =cut