Catalyst Model #5: Stock quotes

Published · Friday, 17 July 2009 (Updated · 7 March 2010)

At last we arrive at a real world—well, real world-ish—example. We’re doing a model for an external data source with an existing interface which means the game now is: don’t write any Catalyst code for the model.

Lucky for us the winningest swim-suit model in the Catalyst community, Jonathan Rockway, has made this a gimmie with his Catalyst::Model::Adaptor. All we will have to do is mix it with Finance::Quote. This can even be done in config!

We’re also to the point where it’s time to stop the nonsense of using controllers to write output, be it text or HTML. It’s time to start using a view which we have been able to avoid so far due to the simplicity of the examples. And now here we are going from–

$c->response->body("some string");

To two views: one for XHTML and one for Ajax. Scared? Once you get the hang of it, it’s easy. We do have a few things to install now. Including Catalyst::Action::RenderView. This is an action class we attach to a render or an end method in a controller. It makes smart choices about whether to forward the application’s execution chain to a view—configured with default_view: XYZ— or not, e.g., in the case of a redirect you do not want to render a page, only headers.

For our views we’re bringing in Template::Alloy and its view wrapper Catalyst::View::TT::Alloy.

So, why Alloy and not straight Template Toolkit (TT2)? Alloy is a really implementation of TT2 + some of TT3 so it’s more flexible. It does plenty more, covering several Perl templating kits. Also most of the examples here and there are for TT2 so let’s try something new.

The Ajax view will be JSON because XML is a really an impedance mismatch for JavaScript. For JSON we want the helper/view class Catalyst::View::JSON and the incomparable JSON::XS as its engine.

Install the required modules

cpan Catalyst::Model::Adaptor
cpan Catalyst::Action::RenderView
cpan Finance::Quote
cpan Template::Alloy
cpan Catalyst::View::TT::Alloy
cpan Catalyst::View::JSON
cpan JSON::XS

Don’t panic! Excepting Finance::Quote those are all modules you are going to learn to love and will use again and again. They are much better than Cats.

Create the new model

./script/myapp_create.pl model StockQuote
 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/StockQuote.pm"
created "/Users/jinx/depot/sites/MyApp/script/../t/model_StockQuote.t"

Glue access to the real model

The real model will be Finance::Quote. We don’t want to write a bunch of wrapping or Catalyst specific code. That destroys the separation and reusability of model classes. Imagine, for example, if DBI were an integral part of Catalyst. You could not do database interaction without starting a web app. That would be colossally stupid and untenable.

We just want to be able to get at all the goodies Finance::Quote provides from controllers without needing to–

  • use it over and over,
  • instantiate it over and over,
  • and feed it default arguments over and over.
emacs lib/MyApp/Model/StockQuote.pm
package MyApp::Model::StockQuote;
use strict;
use warnings;
use parent 'Catalyst::Model::Adaptor';
__PACKAGE__->config( class => 'Finance::Quote' );

sub mangle_arguments {}

1;

That’s it! We’re done—well, we could be but there’s more options we want to cover. Your Catalyst application now has full access to everything that a default Finance::Quote object can do.

The only reason it even has to contain the mangle_arguments is that Finance::Quote->new takes a plain list or array and the default for Catalyst::Model::Adaptor is the very sensible hash ref or if none is passed, depending on the version CMA, it sends undef which is an invalid argument for a large number of modules, including Finance::Quote.

This is fine but it does exclude using arguments to create a new Finance::Quote instance. It can let you choose the backend for the quotes for example.

# What we are doing now when the instance is created-
Finance::Quote->new;

# What we’d like to be able to do-
Finance::Quote->new("ASX");
Finance::Quote->new("-defaults", "CustomModule");
# et cetera.

So let’s start by configuring the arguments we’d like to use. Nothing fancy, we just want to choose the quote source/engine. To create a Finance::Quote object/instance for your application with arguments, you just use the configuration file again. You could ride it in in the model–

__PACKAGE__->config
    ( class => 'Finance::Quote',
      args  => [qw( Yahoo::USA )],
     );

–but that is generally a mistake. It makes quick changes and testing difficult–impossible.

Remember we’re looking for an array/list of args so we’ll just make one for that model. It is an array reference here–

cat myapp.yml
---
Model::StockQuote:
  args:
   - Yahoo::USA

# Remember me?
Model::Amazon::APA:
  service_uri: http://ecs.amazonaws.com/onca/xml
  service: AWSECommerceService
  operation: ItemSearch
  search_index: Books
  associate_tag: apv-20
  response_group: Large
  signature:
    key: YOU_MUST_GET_YOUR_OWN_FROM_AMAZON
    secret: ditto/thisizrlyasekretushudnotnaodig

Now the data is passed to MyApp::Model::StockQuote—via Catalyst::Model::Adaptor—in the $self->{args} hash value. We have to use prepare_arguments and mangle_arguments to get them out and turn them into a flat list to keep Finance::Quote’s new method from dying on us. The output of mangle_arguments goes into FQ’s new. Conceptually this is what’s going on–

Finance::Quote->new(MyApp::Model::StockQuote->mangle_arguments)

The model from the top with argument passing and configuration in mind–

package MyApp::Model::StockQuote;
use strict;
use warnings;
use parent 'Catalyst::Model::Adaptor';
__PACKAGE__->config( class => 'Finance::Quote' );

sub prepare_arguments {
    my ( $self, $c ) = @_;
    return $self->{args};
}

sub mangle_arguments {
    my ( $self, $args ) = @_;
    return @{$args||[]}; # Now the args are a plain list.
}

1;

With that you will get a Finance::Quote object driving your MyApp::Model::StockQuote created in the background like so–

Finance::Quote->new("Yahoo::USA");

Sidenote that could be accomplished without prepare_arguments this way–

sub mangle_arguments {
    my ( $self ) = @_;
    return @{$self->{args}||[]};
}

But don’t do it that way. Use the bigger-revised example above with both methods.

So, great. A nice new model all dressed up and no place to render. Since we have a fancier model for this example, we’re going whole swine and dispense with the in-line display stuff in the controllers. Time to do some views.

Set the application to use RenderView

emacs lib/MyApp/Controller/Root.pm

Add this to your Root.pm file (yeah, it’s this easy for most use cases)–

sub end :ActionClass("RenderView") {}

Make a Template::Alloy view

We will use the plain view helper. The Alloy stuff we’ll add by hand. We’d usually like to use a view helper like the one that comes with Catalyst::View::TT::Alloy. We will not use it this time. Its defaults are kinda of nasty, to me. So, note there is no third argument below to pick up the Alloy helper.

./script/myapp_create.pl view Alloy
 exists "/Users/jinx/depot/sites/MyApp/script/../lib/MyApp/View"
 exists "/Users/jinx/depot/sites/MyApp/script/../t"
created "/Users/jinx/depot/sites/MyApp/script/../lib/MyApp/View/Alloy.pm"
created "/Users/jinx/depot/sites/MyApp/script/../t/view_Alloy.t"
emacs lib/MyApp/View/Alloy.pm
package MyApp::View::Alloy;
use strict;
use warnings;
no warnings "uninitialized";
use parent "Catalyst::View::TT::Alloy";

__PACKAGE__->config
    (
     ENCODING => 'UTF-8',
     TEMPLATE_EXTENSION => ".tt",
     CATALYST_VAR => "c", # Default but let's be explicit.
     );

1;

We include a few things in the package which we never want to change. As you should be increasingly aware, we can include them in the config file too. We have a few things to set-up. Add this to myapp.yml–

View::Alloy:
  INCLUDE_PATH: __path_to(root/alloy)__
  WRAPPER: base_page.tt
  TRIM: 1
  COLLAPSE: 1

I hope you’ll be able to intuit what’s going on already. Our template files will be available in MyApp/root/alloy/ and we will need a master template called base_page.tt to be found in the file tree at MyApp/root/alloy/base_page.tt and automatically used with all Alloy-based rendering.

Our previous examples are really drearily boring for display. We won’t stop at an Alloy view. We’ll add a JSON view as well so we can put Ajax into this example. Ajax seems tough but it can be easier than the equivalent template-based display stuff since you have the DOM at your disposal and can sling it to and fro dynamically. We’re back to using the JSON specific helper this time. Note the third arg—the second JSON. That is what calls Catalyst::View::JSON’s helper to stub out the view.

./script/myapp_create.pl view JSON JSON
 exists "/Users/jinx/depot/sites/MyApp/script/../lib/MyApp/View"
 exists "/Users/jinx/depot/sites/MyApp/script/../t"
created "/Users/jinx/depot/sites/MyApp/script/../lib/MyApp/View/JSON.pm"
created "/Users/jinx/depot/sites/MyApp/script/../t/view_JSON.t"
emacs lib/MyApp/View/JSON.pm
package MyApp::View::JSON;
use strict;
use base 'Catalyst::View::JSON';

1;

So, with Pod redacted, that is exactly what we got from the helper. We don’t need to do anything else with it. It’s defaults are excellent and if JSON::XS is installed—you installed it right?—it will be the engine selected.

We do have a problem now though. We have two views. RenderView has no idea what to do about that if one isn’t specified in a controller which can be done but it’s tedious if you have a default view which is used most of the time. So we need to pick one with the sensibly named–

# Add this to your YAML config, note it is not in an M, V, or C
default_view: Alloy

Make a controller to dispatch the model’s data to the view

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

sub index :Path :Args(0) {}

sub ajax :Local {
    my ( $self, $c ) = @_;
    my @stocks = $c->request->param("stocks") =~ /(\w+)/g;
    $c->log->debug("Fetching stocks: " . join(", ", @stocks));
    my %raw_quotes = $c->model("StockQuote")->fetch("usa", @stocks);
    my $quotes = _normalize_hash(%raw_quotes);
    $c->stash( results => [ values %{$quotes} ] );
    $c->detach( $c->view("JSON") );
}

sub _normalize_hash {
    my %funky_hash = @_;
    my %hash;
    for my $compound_key ( keys %funky_hash )
    {
        my ( $name, $key ) = split /$;/, $compound_key;
        $hash{$name}{$key} = $funky_hash{$compound_key};
    }
    return \%hash;    
}

1;

The MyApp::Controller::Stock is a doing few things for us in conjunction with our other new pieces. This–

sub index :Path :Args(0) {}

Means that requests to /stock will arrive here. No path, no args. The controller sub, as you can see, has no code at all in it. Since we have set our default_view to Alloy, it will dispatch to the Alloy view and path/template it decides matches. Which we know from our Alloy config–

  INCLUDE_PATH: __path_to(root/alloy)__

–will be MyApp/root/alloy/stock/index.tt. The root/alloy because of the INCLUDE_PATH argument, the index because of the sub name, and the .tt extension from the TEMPLATE_EXTENSION, duh.

Sadly the original authors of the module we’re using as an engine—Finance::Quote—made some poor, legacy choices in their data handling. They used a multi-dimensional hash emulation which was a Perl 4 feature. So we have to do a little data massage on results to make them ready for presentation. We do this with _normalize_hash. Again, this is only necessary because we’re dealing with a 15 year old data, legacy structure.

In a production application, I’d put the normalization in the model—or better still, patch the original for the author so everyone wins—but we wanted it nice and squeaky clean for this example to show how Catalyst::Model::Adaptor shines. It is really a matter of shining through though and Finance::Quote is just a bit dingy.

Now blasting through the rest of it; sorry for the dearth of discussion–

The base_page.tt WRAPPER

emacs root/alloy/base_page.tt
[%-DEFAULT title = c.config.name-%]
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
  <head>
  <meta http-equiv="content-type" content="text/html; charset=UTF-8" />
  <title>[% title | html %]</title>
<style type="text/css" media="screen">
body { font-size: 12px; font-family: helvetica, sans-serif; }
a { text-decoration: none; }
a:hover { text-decoration: underline; }
img { margin: 25px; border: 1px solid #aaa; }
html, body {
 font-size: 12px; font-family: helvetica, sans-serif;
 margin: 0;
 padding: 0;
 text-align: center;
}

a[rel="external"],
a[href^="http://sedition.com"] {
  background:url(/img/external-link.jpg) 99% 40%
  no-repeat; padding-right: 15px;
}

#content {
 margin: 0 auto;
 text-align: left;
 width: 850px;
}

#footer {
 float:left;
 clear:both;
 line-height: 130%;
 margin-top: 1.5em;
 padding: 1ex 25px 25px 25px;
 border-top: 1px solid #ddd;
 font-size: 11px;
 text-align:center;
 width: 800px;
}

#nav {
  margin: 10px 0;
  text-align: right;
}
.stock {
 float: left;
 width: 15em;
 text-align: right;
 margin: 10px;
}
.stock h2, .stock h3, .stock h4, .stock p {
 padding:0;
 margin: .1ex 0;
}
.stock b {
  width:4em;
  text-align: left;
}
#body > ol > li {
 font-size: 130%;
 float: left;
 width: 43%;
 margin: 0 3%;
}
ul { list-style-type: square; }
li {
  font-size: 12px;
  line-height: 120%;
}
h3, h4 {
 margin: .5ex 0 .3ex 0;
 padding: 0;
 line-height:110%;
 font-weight: normal;
}
h3 { font-size: 15px }
h4 { font-size: 13px }
input[type="text"] {
  width: 25em;
}
#viewtrack div {
 border-top: 1px solid #999;
 float:left;
 margin:0;
 padding: 2px 1% 2px 0;
}
#viewtrack div.count {
 width: 12%;
 text-align: right;
}
#viewtrack div.path {
 width: 40%;
}
#viewtrack div.visit {
 width: 44%;
}
</style>
<script type="text/javascript"
  src="http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.js">
</script>
</head>

<body>
<div id="content">
  <div id="nav">
[%-IF c.action != "index" %]
  <a href="[% c.uri_for("/").path %]">[% c.config.name | html %]</a>
[% END %]
[%-IF c.action != "index" AND c.action != "src/index" %] &middot; [% END %]
[%-IF c.action != "src/index" %]
  <a href="[% c.uri_for("/src").path %]">Source browser</a>
[% END %]
  </div>

<div id="body">
[% content %]
</div>

<div id="footer">
Content and code in this demo application&mdash;[% c.config.name %]&mdash;&copy;
 Ashley Pond V, <a href="http://pangyresoft.org">PangyreSoft</a>.
<br />
Code is released under the
<a href="http://www.perlfoundation.org/artistic_license_2_0">Artistic License 2.0</a>.
<br style="clear:both" />
<a href="http://www.catalystframework.org/">
<img src="[% c.uri_for("/img/btn_120x50_built_shadow.png").path %]"
 alt="Powered by Catalyst" style="border:0;" /></a>
</div>

</div>

</body>
</html>

The wrapped template matching and dispatched by the controller

emacs root/alloy/stock/index.tt
<script type="text/javascript">//<![CDATA[
jQuery(function($) {
  $("input[name='stocks']").bind("change",function(){
      $(".stock:visible").remove();
      var stocks = $(this).val();
      $.ajax({
              type: "json"
              ,data: { stocks: stocks }
              ,dataType:"json"
              ,url: "[% c.uri_for("ajax").path %]"
              ,cache: false
              ,success: function(json){
                  for ( var i = 0; i < json.results.length; i++ )
                  {
                      var $div = $(".template").clone().removeClass("template");
                      $("#body").append( $div );
                      for ( var n in json.results[i] )
                      {
                          $div.find("."+n).append("<b>" + json.results[i][n] + "</b>");
                      }

                      if ( json.results[i].success != 1 )
                      {
                          $div.children().filter(":not(h2,h4)").remove();
                          $div.css({color:"red"});
                          $("h4", $div).text("No stock found");
                      }
                      $div.fadeIn();
                  }
              }
      });
  });

  var queryString = document.location.search.replace(/^\?/,"");
  if ( queryString )
      $("input[name='stocks']").val(queryString).change();
});
//]]> </script>

<p>
  Enter stock symbols and hit returns: <input name="stocks" type="text" />
<br style="clear:both" />
</p>

<div class="stock template" style="display:none">
  <h2 class="symbol"></h2>
  <h4 class="name"></h4>
  <p class="open">Open </p>
  <p class="close">Close </p>
  <p class="volume">Volume </p>
  <p class="eps">Earnings per Share </p>
  <p class="pe">P/E Ratio </p>
  <p class="cap">Market cap </p>
  <p class="low">Day&rsquo;s low </p>
  <p class="high">Day&rsquo;s high </p>
</div>

Screenshot

When it’s running you can hit /stock and enter symobls. You could start with this /stock?dna,goog,msft,amzn,vita,aapl,ge and see–

Stock model screenshot

The URI arguments are not necessary. The Catalyst doesn’t even use them (directly). The JS will pick them up for an Ajax request if you include them. This way you can bookmark pages or send links.

Fire it up and play away

./script/myapp_server.pl -d -r -p 3000

Come back Monday for #6: Log file model–Apache access log.



digg stumbleupon del.icio.us reddit Fark Technorati Faves

« Catalyst Model #4: Random numbers and really-o, truly-o random numbers · Catalyst Models Intermission—MyApp source code browser »
« 10 Catalyst models in 10 days1 »