Catalyst Model #2: Moon phase data

Published · Tuesday, 14 July 2009 (Updated · 26 December 2011)

Again, this example is light. It’s bigger, with more features, than our last model—Random quotes—but it’s still not doing anything but abstracting the calls and wrapping the usage of another module; in this case, Astro::MoonPhase with date magick help from Date::Manip.

The model does no serious exception handling or error feedback. A production version would need to. For example, dates before epoch 0 are not supported and will cause a fatal error.

Install the required modules (if not already present)

cpan Astro::MoonPhase Date::Manip

Create the new model

./script/myapp_create.pl model MoonPhase
 exists "/Users/jinx/depot/sites/MyApp/script/../lib/MyApp/Model"
 exists "/Users/jinx/depot/sites/MyApp/script/../t"
created "/Users/jinx/depot/sites/MyApp/script/../lib/MyApp/Model/MoonPhase.pm"
created "/Users/jinx/depot/sites/MyApp/script/../t/model_MoonPhase.t"

Fill the model up with goodies

Notice we are using Date::Manip to parse date arguments so we can use a variety of natural language, such as “tomorrow at 10am,” and have it correctly interpreted, converted to epoch seconds, and given to Astro::MoonPhase::phase(). I like Date::Manip and as far as I know it’s still the best at this trick but it’s not something I’d put in a production application. If you want date handling look at DateTime. When not doing human date string parsing, it has the most comprehensive, correct date handling out there.

emacs lib/MyApp/Model/MoonPhase.pm
package MyApp::Model::MoonPhase;
use strict;
use warnings;
use parent 'Catalyst::Model';
use Astro::MoonPhase ();
use Date::Manip qw( ParseDate ParseDateString UnixDate );
use Carp;

sub phase {
    my ( $self, $raw_timish ) = @_;
    Astro::MoonPhase::phase( _helper_time($raw_timish) );
}

sub illumination {
    [ +shift->phase(@_) ]->[1]
}

sub age {
    [ +shift->phase(@_) ]->[2]
}

sub is_waxing {
    +shift->age(@_) < ( 29.53 / 2 );
}

sub is_waning {
    ! +shift->is_waxing(@_);
}

sub is_gibbous {
    +shift->illlumination(@_) > .5;
}

sub _helper_time {
    my $raw_date = shift || time();
    # If it's not a YYYY looking thing, call it an epoch stamp.
    my $parsed = $raw_date =~ /^(?!19|20)\d{9,10}$/ ?
        ParseDate( ParseDateString("epoch $raw_date") )
        :
        ParseDate($raw_date);
    my $time = eval { UnixDate($parsed,"%s") };
    croak "Sorry, bad date: $@; got $parsed parsing $raw_date" if $@;
    return $time;
}

1;

Make a controller to consume the model’s data

/script/myapp_create.pl controller MoonPhase
 exists "/Users/jinx/MyApp/script/../lib/MyApp/Controller"
 exists "/Users/jinx/MyApp/script/../t"
created "/Users/jinx/MyApp/script/../lib/MyApp/Controller/MoonPhase.pm"
created "/Users/jinx/MyApp/script/../t/controller_MoonPhase.t"
emacs lib/MyApp/Controller/MoonPhase.pm
package MyApp::Controller::MoonPhase;
use strict;
use warnings;
use parent 'Catalyst::Controller';

sub index :Path Args(0) {
    my ( $self, $c ) = @_;
    $c->detach("with_arg", ["now"]);
}

sub with_arg :Path Args(1) {
    my ( $self, $c, $when ) = @_;
    $c->response->content_type("text/plain; charset=utf-8");
    my $phase = sprintf("The moon $when: %.1f%% full and %s.",
                        $c->model("MoonPhase")->illumination($when) * 100,
                        $c->model("MoonPhase")->is_waxing($when) ?
                            "waxing" : "waning",
                        );
    $c->response->body( $phase );
}

1;

Note that the with_arg method accepts one argument in its path. This is the time string, natural language or otherwise, to lookup. If called without an argument, you get index instead which sets the argument to “now” and detaches to with_arg. The model has a similar safety/default. Notice that MyApp::Model->_helper_time in the model fills in the argument if not provided.

my $raw_date = shift || time();

Also of note is the content type we set for the output.

$c->response->content_type("text/plain; charset=utf-8");

We are accepting user input in the controller. It is crucial that you never, ever, ever, ever, ever make a song about the Sibbie. It’s also somewhat important that you do not ever echo user input back to the browser. If you do, and your content is HTML or any medium which supports executables, you have just made your site insecure.

We set the output as plain text. This means JavaScript is neutered and XSS attacks are less likely. You can test it yourself with a “time” argument like-

%3Cscript%3Ealert(%22I%20can%20haz%20cookies%22)%3C%2Fscript%3E

To see the difference try swapping out the content type line with-

$c->response->content_type("text/html; charset=utf-8");

–and see how it goes. It’s worth noting that Internet Explorer sometimes ignores the content type header in favor of its own content based heuristics.

Now hit your test server and try it out…

./script/myapp_server.pl -d -r -p 3000
  1. No arguments in the path—http://localhost:3000/moonphase
  2. http://localhost:3000/moonphase/today
  3. http://localhost:3000/moonphase/tomorrow at 11:59pm
  4. http://localhost:3000/moonphase/100 days ago
  5. http://localhost:3000/moonphase/February 16 1984 4:36pm

Tomorrow we try Catalyst Model #3: Cover images via Amazon.com’s APA.



digg stumbleupon del.icio.us reddit Fark Technorati Faves

« Catalyst Model #1: Random quotes · Catalyst Model #3: Cover images via Amazon.com’s APA »
« 10 Catalyst models in 10 days1 »