MojoliciousでCpanel::JSON::XS::Typeを組み合わせるワークログ
結論
検証3の方向性にした。理由は、Mojoliciousに入門したばかりなのであんまりレールが外れることはしたくないから。
具体的な実装はこんな感じ。
蛇足。上記の実装では、JSON::UnblessObjectで、blessされたオブジェクトを渡しても良いようにしている。specに基づいている分、TO_JSON
よりも柔軟性があると思う。
Mojoliciousでこんな具合に、Cpanel::JSON::XS::Typeを組み合わせたい
$c->render(
json => { hello => 'WORLD!!' },
spec => { hello => JSON_TYPE_STRING }
);
Mojolicious v9.31では、jsonをencodeする際、specは食わせられない。該当コードはこちら。
思いつく解決案
-
Mojolicious::Renderer#render
を上書きする -
Mojolicious::Renderer#render
を迂回する
検証1: Mojolicious::Renderer#renderを上書きする
まずは、renderを上書きしたRendererを用意する。テンプレートエンジンを利用なども上書きしてる
package MyApp::Renderer;
use Mojo::Base 'Mojolicious::Renderer';
use Cpanel::JSON::XS;
sub render {
my ($self, $c) = @_;
my $stash = $c->stash;
my $JSON_SELIALIZER = Cpanel::JSON::XS->new->ascii(0);
return $JSON_SELIALIZER->encode($stash->{json}, $stash->{spec}), 'json';
}
1;
そして、$app
のrendererを上書きしてみた。が、「Renderer生成時にhelpers以外にも食わせるべきものがあるんじゃないの?」とか脆い感じ。
sub startup($self) {
$self->app->renderer(Blog::Web::Renderer->new(
helpers => $self->app->renderer->helpers, # helpers以外にも食わせるべきものあるだろう・・
));
...
所感
- Rendererのインスタンスを作成際、何を食わせるべきか明確でない。脆そう。
検証1に関して。もしrenderer_classだけを食わせる形であれば、心配は減りそう?
$self->app->renderer_class('Blog::Web::Renderer')
検証2: before_renderで迂回する
Renderer#render
は、$stash->{data}
であれば、何もすることなく素通りするよう。ドキュメントはこちら。コードはこちら↓
before_renderは、文字通りrender処理前に処理をフックしてくれる。ドキュメントはこちら。コードはこちら↓
この2つを組み合わせて迂回する。具体的には、before_dispatch内で、json encodeして、encodeした値をdataに詰め込む。
package MyApp::Plugin::JSON;
use v5.36;
use Mojo::Base 'Mojolicious::Plugin';
use Cpanel::JSON::XS;
my $JSON_SELIALIZER = Cpanel::JSON::XS->new->ascii(0);
sub register($self, $app, $config) {
$app->hook(before_render => sub($c, $stash) {
if (exists $stash->{json} && exists $stash->{spec}) {
my $json = delete $stash->{json};
my $spec = delete $stash->{spec};
my $data = $JSON_SELIALIZER->encode($json, $spec);
$stash->{data} = $data;
}
});
}
所感
- Mojoliciousの仕様に沿って動かしているので検証1よりは脆くはなさそう
- だけど、before_renderでrenderの実処理をしているのは非直感的
検証3: json_encodeするhelperで迂回する
検証2の亜種。json_encodeするhelperを用意して、コントローラーのアクション内で、encodeされた値をdataに詰め込む。
sub startup($self) {
$self->app->helper(json_encode => sub($c, $json, $spec) {
my $JSON_SELIALIZER = Cpanel::JSON::XS->new->ascii(0);
my $data = $JSON_SELIALIZER->encode($json, $spec);
});
my $r = $self->routes;
$r->get('/' => sub ($c) {
$c->render(
status => HTTP_OK,
data => $c->json_encode({ foo => "HELLO WORLD" }, { foo => JSON_TYPE_STRING }),
);
});
}
所感
- dataのMojoliciousの仕様に沿って動かしているので検証2同様脆さは少なそう
- encodeをどこで行っているか直感的にはわかる。
- 強いて言えば、json encodeの生々しさが、コントローラーアクションに露出してるのが気になる?
検証4: render_jsonをhelperで生やして迂回する
検証3の生々しさを隠蔽するrender_json helperを用意した
sub startup($self) {
$self->app->helper(render_json => sub($c, $status, $json, $spec) {
my $stash = { status => $status, json => $json, spec => $spec };
my $plugins = $c->app->plugins->emit_hook(before_render => $self, $stash);
my $JSON_SELIALIZER = Cpanel::JSON::XS->new->ascii(0);
my $output = $JSON_SELIALIZER->encode($json, $spec);
my $format = 'json';
$plugins->emit_hook(after_render => $c, \$output, $format);
$c->app->renderer->respond($c, $output, $format, $status);
});
my $r = $self->routes;
$r->get('/' => sub ($c) {
$c->render_json(HTTP_OK, { foo => "HELLO WORLD" }, { foo => JSON_TYPE_STRING });
});
}
所感
-
render_json($status, $json, $spec)
のI/Fは慣れているので、気持ちは良い - が、Mojoliciousのrenderの内部処理が変化したとき、追従が面倒