# t/request.tusev5.38;useTest2::V0-target=>'JsonRpc::Request';useJsonRpc::Version;useJsonRpc::MethodName;subtest'constructor with required fields only'=>sub{my$req=JsonRpc::Request->new(jsonrpc=>JsonRpc::Version->new(value=>'2.0'),method=>JsonRpc::MethodName->new(value=>'getUser'),);ok$req,'Request created with required fields';isa_ok$req->jsonrpc,'JsonRpc::Version';isa_ok$req->method,'JsonRpc::MethodName';is$req->params,undef,'params is undef by default';is$req->id,undef,'id is undef by default';};subtest'constructor with all fields'=>sub{my$req=JsonRpc::Request->new(jsonrpc=>JsonRpc::Version->new(value=>'2.0'),method=>JsonRpc::MethodName->new(value=>'createUser'),params=>{name=>'Alice',age=>30},id=>'req-001',);ok$req,'Request created with all fields';is$req->params,{name=>'Alice',age=>30},'params is hash';is$req->id,'req-001','id is string';};subtest'constructor rejects invalid jsonrpc'=>sub{like(dies{JsonRpc::Request->new(jsonrpc=>"not a Version object",method=>JsonRpc::MethodName->new(value=>'test'),);},qr/type constraint|isa/i,'rejects non-Version jsonrpc');};subtest'constructor rejects invalid method'=>sub{like(dies{JsonRpc::Request->new(jsonrpc=>JsonRpc::Version->new(value=>'2.0'),method=>"not a MethodName object",);},qr/type constraint|isa/i,'rejects non-MethodName method');};subtest'params accepts array or hash or undef'=>sub{my$version=JsonRpc::Version->new(value=>'2.0');my$method=JsonRpc::MethodName->new(value=>'test');# ArrayRefok(lives{JsonRpc::Request->new(jsonrpc=>$version,method=>$method,params=>[1,2,3],);},'params accepts ArrayRef');# HashRefok(lives{JsonRpc::Request->new(jsonrpc=>$version,method=>$method,params=>{key=>'value'},);},'params accepts HashRef');# undefok(lives{JsonRpc::Request->new(jsonrpc=>$version,method=>$method,params=>undef,);},'params accepts undef');# String は拒否like(dies{JsonRpc::Request->new(jsonrpc=>$version,method=>$method,params=>"string",);},qr/type constraint/i,'params rejects string');};subtest'id accepts string, int, or undef'=>sub{my$version=JsonRpc::Version->new(value=>'2.0');my$method=JsonRpc::MethodName->new(value=>'test');ok(lives{JsonRpc::Request->new(jsonrpc=>$version,method=>$method,id=>'str-id');},'id accepts string');ok(lives{JsonRpc::Request->new(jsonrpc=>$version,method=>$method,id=>123);},'id accepts integer');ok(lives{JsonRpc::Request->new(jsonrpc=>$version,method=>$method,id=>undef);},'id accepts undef');like(dies{JsonRpc::Request->new(jsonrpc=>$version,method=>$method,id=>[]);},qr/type constraint/i,'id rejects array reference');};done_testing;
テストを実行すると、当然失敗します(Red)。
1
2
3
$ prove -lv t/request.t
Can't locate JsonRpc/Request.pm in @INC
...
# t/request.t に追加subtest'from_hash factory method'=>sub{subtest'creates request from hash'=>sub{my$req=JsonRpc::Request->from_hash({jsonrpc=>'2.0',method=>'getUser',params=>{user_id=>42},id=>'req-001',});isa_ok$req,['JsonRpc::Request'],'is a JsonRpc::Request';is$req->jsonrpc->value,'2.0','jsonrpc is correct';is$req->method->value,'getUser','method is correct';is$req->params,{user_id=>42},'params is correct';is$req->id,'req-001','id is correct';};subtest'from_hash rejects missing required fields'=>sub{like(dies{JsonRpc::Request->from_hash({method=>'test'});},qr/missing.*jsonrpc/i,'missing jsonrpc rejected');like(dies{JsonRpc::Request->from_hash({jsonrpc=>'2.0'});},qr/missing.*method/i,'missing method rejected');};subtest'from_hash rejects non-hash'=>sub{like(dies{JsonRpc::Request->from_hash("not a hash");},qr/hash reference/i,'string rejected');like(dies{JsonRpc::Request->from_hash([]);},qr/hash reference/i,'array rejected');};};
A Notification is a Request object without an “id” member. A Request object that is a Notification signifies the Client’s lack of interest in the corresponding Response object, and as such no Response object needs to be returned to the client. The Server MUST NOT reply to a Notification, including those that are within a batch request.
リクエストに id が存在しない場合は、 Notification という扱いになります。MUST NOTが使用されている重要な仕様です。
ところで、この場合 JsonRpc::Request は id を必須項目に変更する必要もあります。何故ならば、 id が存在しない JsonRpc::Request は JsonRpc::Notification になるので、結果的に JsonRpc::Request は id が必ず存在するという仕様に変わりました。
graph LR;
リクエスト-->idを持っているか;
idを持っているか-- Yes -->JsonRpc::Request;
idを持っているか-- No -->JsonRpc::Notification;
ちょっとした仕様追加のはずだったのに、かなり大ごとになりそうな予感です。
ですが、落ち着いてテストコードを変更していきましょう。
id が必須項目になるので、 id を省略していた正しく生成しているテストには、必須項目として id を追加する
新たなテストとして、 id を指定しないで new すると、エラー(required)を返すテストを追加する
ちなみに、 id は null を許容しているので、指定しないのとundefを渡すのとは明確に区別する必要があります。今回の仕様変更で JsonRpc::Request は id を指定しない場合はエラーになりますが、idとしてundefを渡すと正常に作成されます。
# t/request_factory.tusev5.38;useTest2::V0-target=>'JsonRpc::RequestFactory';subtest'constructor with required fields only'=>sub{subtest'from_hash creates JsonRpc::Request'=>sub{my$req=JsonRpc::RequestFactory->from_hash({jsonrpc=>'2.0',method=>'createUser',params=>{name=>'Bob'},id=>123,});isa_ok$req,['JsonRpc::Request'];isa_ok$req->jsonrpc,['JsonRpc::Version'];isa_ok$req->method,['JsonRpc::MethodName'];is$req->params,{name=>'Bob'},'params is hash';is$req->id,123,'id is integer';};subtest'from_hash creates JsonRpc::Notification'=>sub{my$req=JsonRpc::RequestFactory->from_hash({jsonrpc=>'2.0',method=>'notifyEvent',params=>['event1','event2'],});isa_ok$req,['JsonRpc::Notification'];isa_ok$req->jsonrpc,['JsonRpc::Version'];isa_ok$req->method,['JsonRpc::MethodName'];is$req->params,['event1','event2'],'params is array';};};done_testing;
# t/notification.tusev5.38;useTest2::V0-target=>'JsonRpc::Notification';useJsonRpc::Version;useJsonRpc::MethodName;subtest'constructor with required fields only'=>sub{my$req=JsonRpc::Notification->new(jsonrpc=>JsonRpc::Version->new(value=>'2.0'),method=>JsonRpc::MethodName->new(value=>'notify'),);isa_ok$req,['JsonRpc::Notification'];isa_ok$req->jsonrpc,['JsonRpc::Version'];isa_ok$req->method,['JsonRpc::MethodName'];is$req->method->value,'notify','method value is notify';is$req->params,undef,'params is undef by default';};subtest'constructor with all fields'=>sub{my$req=JsonRpc::Notification->new(jsonrpc=>JsonRpc::Version->new(value=>'2.0'),method=>JsonRpc::MethodName->new(value=>'notifyEvent'),params=>[{event=>'event1'},{event=>'event2'}],);isa_ok$req,['JsonRpc::Notification'];is$req->method->value,'notifyEvent','method value is notifyEvent';is$req->params,[{event=>'event1'},{event=>'event2'}],'params is array';};subtest'constructor rejects invalid jsonrpc'=>sub{like(dies{JsonRpc::Notification->new(jsonrpc=>"not a Version object",method=>JsonRpc::MethodName->new(value=>'test'),);},qr/type constraint|isa/i,'rejects non-Version jsonrpc');};subtest'constructor rejects invalid method'=>sub{like(dies{JsonRpc::Notification->new(jsonrpc=>JsonRpc::Version->new(value=>'2.0'),method=>"not a MethodName object",);},qr/type constraint|isa/i,'rejects non-MethodName method');};subtest'params accepts array or hash or undef'=>sub{my$version=JsonRpc::Version->new(value=>'2.0');my$method=JsonRpc::MethodName->new(value=>'testMethod');my$req_array=JsonRpc::Notification->new(jsonrpc=>$version,method=>$method,params=>['item1','item2'],);is$req_array->params,['item1','item2'],'params is array';my$req_hash=JsonRpc::Notification->new(jsonrpc=>$version,method=>$method,params=>{key1=>'value1',key2=>'value2'},);is$req_hash->params,{key1=>'value1',key2=>'value2'},'params is hash';my$req_undef=JsonRpc::Notification->new(jsonrpc=>$version,method=>$method,params=>undef,);is$req_undef->params,undef,'params is undef';# String は拒否like(dies{JsonRpc::Notification->new(jsonrpc=>$version,method=>$method,params=>"not an array or hash",);},qr/type constraint|isa/i,'rejects string params');};done_testing;
# t/request.tusev5.38;useTest2::V0-target=>'JsonRpc::Request';useJsonRpc::Version;useJsonRpc::MethodName;subtest'constructor with required fields only'=>sub{my$req=JsonRpc::Request->new(jsonrpc=>JsonRpc::Version->new(value=>'2.0'),method=>JsonRpc::MethodName->new(value=>'ping'),id=>undef,);isa_ok$req,['JsonRpc::Request'];isa_ok$req->jsonrpc,['JsonRpc::Version'];isa_ok$req->method,['JsonRpc::MethodName'];is$req->method->value,'ping','method value is ping';is$req->params,undef,'params is undef by default';is$req->id,undef,'id is undef';};subtest'constructor with all fields'=>sub{my$req=JsonRpc::Request->new(jsonrpc=>JsonRpc::Version->new(value=>'2.0'),method=>JsonRpc::MethodName->new(value=>'createUser'),params=>{name=>'Alice',age=>30},id=>'req-001',);isa_ok$req,['JsonRpc::Request'];is$req->method->value,'createUser','method value is createUser';is$req->params,{name=>'Alice',age=>30},'params is hash';is$req->id,'req-001','id is string';};subtest'constructor rejects invalid jsonrpc'=>sub{like(dies{JsonRpc::Request->new(jsonrpc=>"not a Version object",method=>JsonRpc::MethodName->new(value=>'test'),id=>undef,);},qr/type constraint|isa/i,'rejects non-Version jsonrpc');};subtest'constructor rejects invalid method'=>sub{like(dies{JsonRpc::Request->new(jsonrpc=>JsonRpc::Version->new(value=>'2.0'),method=>"not a MethodName object",id=>undef,);},qr/type constraint|isa/i,'rejects non-MethodName method');};subtest'constructor rejects missing id'=>sub{like(dies{JsonRpc::Request->new(jsonrpc=>JsonRpc::Version->new(value=>'2.0'),method=>"not a MethodName object",);},qr/required/i,'rejects missing id');};subtest'params accepts array or hash or undef'=>sub{my$version=JsonRpc::Version->new(value=>'2.0');my$method=JsonRpc::MethodName->new(value=>'test');# ArrayRefok(lives{JsonRpc::Request->new(jsonrpc=>$version,method=>$method,params=>[1,2,3],id=>1,);},'params accepts ArrayRef');# HashRefok(lives{JsonRpc::Request->new(jsonrpc=>$version,method=>$method,params=>{key=>'value'},id=>1,);},'params accepts HashRef');# undefok(lives{JsonRpc::Request->new(jsonrpc=>$version,method=>$method,params=>undef,id=>1,);},'params accepts undef');# String は拒否like(dies{JsonRpc::Request->new(jsonrpc=>$version,method=>$method,params=>"string",id=>1,);},qr/type constraint/i,'params rejects string');};subtest'id accepts string, int, or undef'=>sub{my$version=JsonRpc::Version->new(value=>'2.0');my$method=JsonRpc::MethodName->new(value=>'test');ok(lives{JsonRpc::Request->new(jsonrpc=>$version,method=>$method,id=>'str-id');},'id accepts string');ok(lives{JsonRpc::Request->new(jsonrpc=>$version,method=>$method,id=>123);},'id accepts integer');ok(lives{JsonRpc::Request->new(jsonrpc=>$version,method=>$method,id=>undef);},'id accepts undef');like(dies{JsonRpc::Request->new(jsonrpc=>$version,method=>$method,id=>[]);},qr/type constraint/i,'id rejects array reference');};done_testing;
# t/response.tusev5.38;useTest2::V0-target=>'JsonRpc::Response';useJsonRpc::Version;subtest'constructor with all required fields'=>sub{my$res=JsonRpc::Response->new(jsonrpc=>JsonRpc::Version->new,result=>{name=>'Alice',age=>30},id=>'req-001',);ok$res,'Response created';isa_ok$res->jsonrpc,['JsonRpc::Version'],'version';is$res->result,{name=>'Alice',age=>30},'result is hash';is$res->id,'req-001','id is string';# ここでは省略していますが id は `Maybe[Str|Int]` なので、数値やundefのテストも必要です。};subtest'result accepts any type'=>sub{my$version=JsonRpc::Version->new;# 文字列ok(lives{JsonRpc::Response->new(jsonrpc=>$version,result=>'success',id=>1);},'result accepts string');# 数値ok(lives{JsonRpc::Response->new(jsonrpc=>$version,result=>42,id=>2);},'result accepts number');# 配列ok(lives{JsonRpc::Response->new(jsonrpc=>$version,result=>[1,2,3],id=>3);},'result accepts array');# ハッシュok(lives{JsonRpc::Response->new(jsonrpc=>$version,result=>{ok=>1},id=>4);},'result accepts hash');# null/undefok(lives{JsonRpc::Response->new(jsonrpc=>$version,result=>undef,id=>5);},'result accepts undef');};subtest'id is required'=>sub{like(dies{JsonRpc::Response->new(jsonrpc=>JsonRpc::Version->new,result=>'ok',);},qr/required|missing/i,'id is required');};subtest'from_hash factory method'=>sub{my$res=JsonRpc::Response->from_hash({jsonrpc=>'2.0',result=>{status=>'ok'},id=>'test-id',});isa_ok$res,['JsonRpc::Response'],'Response created from hash';is$res->result,{status=>'ok'},'result is correct';is$res->id,'test-id','id is correct';};done_testing;
# lib/JsonRpc/Response.pmpackageJsonRpc::Response;usev5.38;useMoo;useTypes::Standardqw(InstanceOf Any Str Int);useJsonRpc::Version;usenamespace::clean;hasjsonrpc=>(is=>'ro',isa=>InstanceOf['JsonRpc::Version'],required=>1,);hasresult=>(is=>'ro',isa=>Any,# 任意の型を許容required=>1,);hasid=>(is=>'ro',isa=>Maybe[Str|Int],required=>1,);subfrom_hash{my($class,$hash)=@_;die"from_hash requires a hash reference"unlessref$hasheq'HASH';die"missing required field: jsonrpc"unlessexists$hash->{jsonrpc};die"missing required field: result"unlessexists$hash->{result};die"missing required field: id"unlessexists$hash->{id};return$class->new(jsonrpc=>JsonRpc::Version->new(value=>$hash->{jsonrpc}),result=>$hash->{result},id=>$hash->{id},);}1;__END__
=head1 NAME
JsonRpc::Response - JSON-RPC 2.0 successful Response object
=head1 SYNOPSIS
use JsonRpc::Response;
my $res = JsonRpc::Response->new(
jsonrpc => JsonRpc::Version->new(value => '2.0'),
result => { user => { id => 42, name => 'Alice' } },
id => 'req-123',
);
# From hash
my $res2 = JsonRpc::Response->from_hash({
jsonrpc => '2.0',
result => [1, 2, 3],
id => 999,
});
=head1 DESCRIPTION
Represents a JSON-RPC 2.0 successful Response object.
The C<result> field can be any value (string, number, array, hash, null).
=cut
# これはコンパイル時(ロード時)にエラーmy$req=JsonRpc::Request->new(jsonrpc=>"2.0",# NG: Version オブジェクトが必要method=>"test",);# Value "2.0" did not pass type constraint "InstanceOf['JsonRpc::Version']"