[Catalyst] Best practices: XML output from static XML

J. Shirley jshirley at gmail.com
Sat Mar 6 22:33:40 GMT 2010


On Sat, Mar 6, 2010 at 2:20 PM, Bill Moseley <moseley at hank.org> wrote:
>
>
> On Sat, Mar 6, 2010 at 12:59 PM, J. Shirley <jshirley at gmail.com> wrote:
>>
>> I don't think the path taken with Catalyst::Action::REST is the best,
>> but it does work very very well in my opinion and I certainly can't
>> think of anything better.  Being able to send to a serialization
>> method based on the content-type solves a lot of these issues.  You
>> could just setup a content type for your feed in configuration and
>> write a custom serialize class and get exactly what you are asking
>> for.
>
> I agree, ::REST works well for what it does, but doesn't provide a framework
> for a View that is action-specific.  ::REST assumes that the actions set an
> entity in the "rest" stash that can be serialized.  But, if the same action
> is used for a TT View then probably want to just pass model objects in the
> stash instead of creating a "rest" structure in the stash.
> One could argue that actions should always result in the same data in the
> stash regardless of the View (the Controller has no idea about the final
> View).  The reality is that a request to /blob/recent_posts would return
> different data in a JSON response than would be use to render a web page
> with a TT view.
>

This is where I actually really love ::REST.  If you make a JSON call,
then whatever is in the stash key that ::REST is returned to the
client (via ->status_ok) -- but if you combine this with chained and
setup other data in the stash, the TT view can access all of that.

So I view it more that the JSON response is limited to showing *only*
the request result, where as TT can show everything else surrounding
it.

This allows me a great amount of flexibility in my templates, without
having any [% c.model('Schema::Whatever') %] in them.

>
> The "problem" I'm up against is we have an existing application written for
> the web -- so actions expect GET and POST requests and place objects in the
> stash.  Then TT uses the objects in the stash to render the markup.
> Now we need to expose these same methods (which means same URLs really) for
> two similar purposes.  New development for the web app is all client side.
>  Fat AJAX that talks to the application via JSON serialized (mostly)
> requests and responses.  Plus, we need to expose a REST API for third party
> customers.  So, really it's just an API for both.
> ::REST will work fine for new actions, but there's a lot of existing actions
> that need to work both for TT rendered pages and for JSON responses.
> I think the action's job should be to take a request, validate,
> authenticate, authorize, etc, then either generate error or place model
> objects in the stash.  Then pass off to the view to render/serialize.
> The problem is that a request coming from a browser may be slightly
> different than from an AJAX or API request.  (Request might come in a as a
> POST on the web and a PUT via the API and parameters might be slightly
> different.)  So, either need to dispatch to different actions for same
> request URL or have some kind of filtering code that runs before the action
> to "normalize" the request for the action depending on where it's coming
> from.
> Obviously, it makes sense to share the actions where possible.
> Likewise, the TT View passes control to an action-specific template to
> render the markup for the request, but the same action might need to return
> JSON  so in that case would need to also have action-specific code to build
> the json stash from the objects the action fetched.

Just curious, but if you apply the REST action class (and define
_GET/_POST methods) and keep stash population to your actions, what is
failing there?  I've done similar things to what you are discussing
(and will again in about a week or two from now) and hadn't run into
anything severe.

As long as the content-type (no problem for XmlHttpRequest) is set to
"application/json" I get back the data I expect.  I do have to modify
some of the actions to set the final stash result, but in most cases
this is just a single line of code.

>>
>> I don't want to evangelize ::REST too much, so to address your
>>
>> suggestions more directly I'd have to say that relying on $self->can
>>
>> seems a bit too limited for my tastes.
>
> I don't like it either.  Still, to me it seems the act of taking the model
> objects loaded by the controller and building the "json" stash is a View
> action -- not something that should happen in the controller action.
>
>> I'd lean on configuration more so than $self->can.  Then a call to
>> $self->get_serializer_for('JSON') that returns some serialization
>> object (or whatever handler you have) is simple, and coupling it with
>> Moose would work very well.  Then you can work out adding new
>> serialization calls just in config.
>
> So are you suggesting that $self->get_serializer_for('JSON') would return
> code that would be action-specific?  That is for a request to
> /blog/recent_posts would return code that would know what to put in the
> "json" stash for that specific request?
> I like the idea of leaning on the configuration implementation -- just not
> sure what that looks like. ;)
>

The very barest example:


__PACKAGE__->config(
    serializers => {
        'JSON' => sub { return "this is icky, but would work" }
    }
);
has 'serializers' => (
    is => 'rw',
    isa => 'HashRef',
    traits => [ 'Hash' ],
    handles => { 'get_serializer_for' => 'get' }
);

Then you could do something like:
$self->get_serializer_for('JSON')->($c);

(Untested, and I would use coercions to setup a secondary class to
handle, but again.. at this point just having Views and end actions
seems more sensible :))

>
>>
>> However, I'm having a hard time thinking about any valid use cases for
>> this, especially since ::REST does things fairly well (especially for
>> how old the code is) so I'd automatically use that for all the cases I
>> can think of. Anything else that doesn't fit, I'd just defer to having
>> separate views (and possibly a different RenderView+end action as
>> appropriate).
>
> If I was starting fresh I'd be tempted to write all the controller actions
> with ::REST.  I'd like it if the various _<METHOD> actions were real actions
> for dispatching (they aren't right?).  Real actions would mean could test
> things like Args:
>      sub blog_GET : Local Args(1) {  # GET requires an argument
>      sub blog_POST: Local Args(0)  { # POST creates and must not have an
> argument
>      sub blog_PUT : Local Args(1) {  # PUT requires and argument
>      sub blog_DELETE : Local Args(1) {

They aren't normal actions, and they shouldn't be because they're
handlers for a specific request type.  There isn't anything stopping
you from doing $controller->blog_GET($c) though.

If you have variable arguments, then you are pointing to a different
resource and thus what you described wouldn't be REST :)

> Then layer the web app on top -- especially if it's all client side.  But,
> again I still think the action's job would be to just place model objects in
> the stash -- not build a "json" structure as that is only needed when the
> response is actually json.
>



More information about the Catalyst mailing list