JSONのSpecをしっかり書く
JSONのSpecをしっかり書く:
APIの仕様をSwagger/OpenAPIで書くケースは増えてきました。これはAPI提供側と利用側の取り決めなので、これさえしっかり書いてメンテしていけば、独立して開発できるようになります。
というのはいささか理想が過ぎるようにも思えます。というのもSwagger/OpenAPIでは仕様、特にやりとりするJSONのデータの仕様を書ききるのは難しいためです。一応型としてきっちり定義できるように、スキーマ定義の部分は仕様が膨らんできていますが、その延長線上にはそれこそメンテ不可能な、過去人類が通ったスペックモンスターの規格を生み出してしまわれそうです。
もう少し上手いやり方は無いものでしょうか?
そこでJSON Specというものを作ってみました。どんなAPIでもサーバサイドでは、受け取るパラメータやJSONのバリデーションはしっかり書くはずです。これを利用して、APIのテストにも活用する方法をご紹介します。
ソース: https://github.com/kawasima/json-spec
ライブラリ: https://www.npmjs.com/org/json-spec
こんな感じでスペックを定義します。firstNameやlastNameなどは、ビルトインのスペックです。
バリデーションは、
バリデーションの詳細は、
オブジェクトとしても取れます。
spec`を使ってバリデーション用のファンクションをスペック化できます。
またスペックを組み合わせて複雑なスペックを生成できます。
ObjectとArrayには専用のスペック定義メソッドがあります。
オプションでは、以下のものが指定可能です。
Objectのスペックでは、キーが必須か任意かで定義を分けます。
JSON Specはイチから開発したわけではなく、clojure.specのJavaScript移植です。Clojureは動的型付け言語であり、またシンプルなマップやリストを使ってデータ構造を表現する設計指針もあいまって、既存のファンクションを利用する側が、ドキュメントを良く読んで使わなきゃいけない問題がありました。通常の言語のアプローチだと、型を導入してなんとかしようとするわけですが、Clojureではこのclojure.specを使って実行可能な仕様書を導入することで、この問題の解決を図っています。
https://clojure.org/about/spec
clojure.specのもう一つ面白いところは、バリデーションと同時にスペックに沿ったデータの生成ができるところです。
https://clojure.org/guides/spec#_generators
clojure.specと同じくJSON Specにもデータの生成機能を実装してあります。スペックには、その仕様に沿ったデータを生成するジェネレータファンクションを設定でき、ObjectスペックやArrayスペック、ビルトインのスペックには、ジェネレータファンクションがプリセットされているので、ただちにデータの生成が可能です。
JSON Specは、Open APIとともに用いることでその真価を発揮できると考えています。スペックに沿ったデータ生成ができるので、データを用意せずとも、Open APIの仕様を読み込んで、モックサーバやモッククライアントが出来上がるのです。
https://www.npmjs.com/package/@json-spec/openapi
OpenAPIで定義されているエンドポイントを、スペックに沿って一通り叩いてくれるコマンドが用意されています。
JSON Specは次のように、スペックをexportしたファイルを読み込ませます。
そして、Open APIのコンポーネント定義で、
これで、実行すると以下のような結果を得ます。
APIサーバのCIに仕込んでおけば、意図せず仕様違反となる修正をしてしまったことを検出できるようになります。
既存のAPIサーバがあって、それを使って新たにシステムを作るのがトラブル少なくよいことですが、それらを同時に作らなきゃいけないことも往々にしてあります。
そんなときに、JSON Specがあれば、Open API仕様を読み込んでAPIサーバを自動的に立ててくれます。
今後はJSON Specから`
当然ながらテストだけでなく、実際のAPIの処理に置いてバリデーションとしてJSON Specが使えますが、サーバサイドの言語がNode.jsに限られてしまうので、GraalVMのPolyglotでなんとかすることを考えています。
Open APIの仕様を書くのに疲れた方は、JSON Specとともに使うことで、そのありがたみをハッキリと再認識できると思いますので、ぜひお試しください。
はじめに
APIの仕様をSwagger/OpenAPIで書くケースは増えてきました。これはAPI提供側と利用側の取り決めなので、これさえしっかり書いてメンテしていけば、独立して開発できるようになります。というのはいささか理想が過ぎるようにも思えます。というのもSwagger/OpenAPIでは仕様、特にやりとりするJSONのデータの仕様を書ききるのは難しいためです。一応型としてきっちり定義できるように、スキーマ定義の部分は仕様が膨らんできていますが、その延長線上にはそれこそメンテ不可能な、過去人類が通ったスペックモンスターの規格を生み出してしまわれそうです。
もう少し上手いやり方は無いものでしょうか?
そこでJSON Specというものを作ってみました。どんなAPIでもサーバサイドでは、受け取るパラメータやJSONのバリデーションはしっかり書くはずです。これを利用して、APIのテストにも活用する方法をご紹介します。
JSON Spec
ソース: https://github.com/kawasima/json-specライブラリ: https://www.npmjs.com/org/json-spec
const s = require('@json-spec/core'); const gen = require('@json-spec/core/gen'); const sp = require('@json-spec/spec-profiles'); const sb = require('@json-spec/spec-basic'); const personSpec = s.object({ required: { firstName: sp.firstName({ size: 100, locale:"ja"}), lastName: sp.lastName({ size: 100, locale: "ja" }), languages: s.array([ "C", "C++", "Java" ], { distinct: true, maxCount: 3 }) }, optional: { birthDay: sp.birthDay, postalCd: sp.postalCode_JP } });
バリデーションは、
@json-spec/core
のisValid
を使います。const person = { firstName: "ピカチュウ" }; s.isValid(personSpec, person); // => return false
explain
を使うと取れます。> s.explain(personSpec, person); - failed: key lastName required in: lastName at: lastName - failed: key languages required in: languages at: languages
> s.explainData(personSpec, person); { problems: [ { path: [Array], via: [], pred: [Function: pred], reason: 'key lastName required', val: undefined, in: [Array] }, { path: [Array], via: [], pred: [Function: pred], reason: 'key languages required', val: undefined, in: [Array] } ], spec: ObjectSpec { gfn: undefined, name: null, required: { firstName: [AndSpec], lastName: [AndSpec], languages: [ArraySpec] }, optional: { birthDay: [AndSpec], postalCd: [ScalarSpec] }, keysPred: [Function: keysPred] }, value: { firstName: 'ピカチュウ' } }
スペックの定義
spec`を使ってバリデーション用のファンクションをスペック化できます。> const even = s.spec(x => x % 2 === 0); > s.isValid(even, 2) true > s.isValid(even, 3) false
> const intSpec = s.spec(x => typeof(x) === 'number' && isFinite(x) && Math.floor(x) === x) undefined > s.and(intSpec, even) > s.or(intSpec, even)
s.array(【Arrayの要素が満たすべきPredicateファンクション】, 【オプション】);
s.array(x => x % 2 === 0, { count: 3 });
オブション | 説明 |
---|---|
count | 要素の個数 |
minCount | 要素の最小個数 |
maxCount | 要素の最大個数 |
distinct | 重複を許すか? (デフォルト: false) |
> const objSpec = s.object({ required: { a: x => typeof(x) === 'string' }, optional: { b: x => typeof(x) === 'number' } }) > s.isValid(objSpec, { a: 'hoge' }) true > s.isValid(objSpec, { b: 1 }) false > s.isValid(objSpec, { a: 'hoge', b: 1 }) true
設計
JSON Specはイチから開発したわけではなく、clojure.specのJavaScript移植です。Clojureは動的型付け言語であり、またシンプルなマップやリストを使ってデータ構造を表現する設計指針もあいまって、既存のファンクションを利用する側が、ドキュメントを良く読んで使わなきゃいけない問題がありました。通常の言語のアプローチだと、型を導入してなんとかしようとするわけですが、Clojureではこのclojure.specを使って実行可能な仕様書を導入することで、この問題の解決を図っています。https://clojure.org/about/spec
clojure.specのもう一つ面白いところは、バリデーションと同時にスペックに沿ったデータの生成ができるところです。
(gen/generate (s/gen int?)) ;;=> -959 (gen/generate (s/gen nil?)) ;;=> nil (gen/sample (s/gen string?)) ;;=> ("" "" "" "" "8" "W" "" "G74SmCm" "K9sL9" "82vC") (gen/sample (s/gen #{:club :diamond :heart :spade})) ;;=> (:heart :diamond :heart :heart :heart :diamond :spade :spade :spade :club) (gen/sample (s/gen (s/cat :k keyword? :ns (s/+ number?)))) ;;=> ((:D -2.0) ;;=> (:q4/c 0.75 -1) ;;=> (:*!3/? 0) ;;=> (:+k_?.p*K.*o!d/*V -3) ;;=> (:i -1 -1 0.5 -0.5 -4) ;;=> (:?!/! 0.515625 -15 -8 0.5 0 0.75) ;;=> (:vv_z2.A??!377.+z1*gR.D9+G.l9+.t9/L34p -1.4375 -29 0.75 -1.25) ;;=> (:-.!pm8bS_+.Z2qB5cd.p.JI0?_2m.S8l.a_Xtu/+OM_34* -2.3125) ;;=> (:Ci 6.0 -30 -3 1.0) ;;=> (:s?cw*8.t+G.OS.xh_z2!.cF-b!PAQ_.E98H4_4lSo/?_m0T*7i 4.4375 -3.5 6.0 108 0.33203125 2 8 -0.517578125 -4))
データの生成
clojure.specと同じくJSON Specにもデータの生成機能を実装してあります。スペックには、その仕様に沿ったデータを生成するジェネレータファンクションを設定でき、ObjectスペックやArrayスペック、ビルトインのスペックには、ジェネレータファンクションがプリセットされているので、ただちにデータの生成が可能です。> gen.generate(s.gen(personSpec)) { firstName: '蓮', lastName: '高橋', languages: [ 'C', 'Java' ] } > gen.generate(s.gen(personSpec)) { firstName: '蒼空', lastName: '清水', languages: [], birthDay: 1983-06-13T15:00:00.000Z, postalCd: '8581401' }
sample
を使えば一気にたくさんのデータを生成可能です。> gen.sample(s.gen(person), 5) [ { firstName: '結衣', lastName: '鈴木', languages: [ 'C++' ], birthDay: 1977-03-05T15:00:00.000Z, postalCd: '6012240' }, { firstName: '杏', lastName: '井上', languages: [], postalCd: '6807950' }, { firstName: '颯太', lastName: '斎藤', languages: [], birthDay: 2015-11-08T15:00:00.000Z }, { firstName: '莉子', lastName: '小林', languages: [ 'Java', 'C' ] }, { firstName: '太一', lastName: '林', languages: [ 'C++', 'Java', 'C' ], postalCd: '5175024', birthDay: 1901-07-27T15:00:00.000Z } ]
Open APIとともに
JSON Specは、Open APIとともに用いることでその真価を発揮できると考えています。スペックに沿ったデータ生成ができるので、データを用意せずとも、Open APIの仕様を読み込んで、モックサーバやモッククライアントが出来上がるのです。https://www.npmjs.com/package/@json-spec/openapi
モッククライアントを使ったAPIサーバのテスト
OpenAPIで定義されているエンドポイントを、スペックに沿って一通り叩いてくれるコマンドが用意されています。json-spec-client --openapi=【OpenAPI定義のYAMLまたはJSON】 --jsonspec=【JSON Specの定義】--base-url=【APIサーバのベースのURL】
jsonspec.js
const s = require('@json-spec/core'); const gen = require('@json-spec/core/gen'); const sp = require('@json-spec/spec-profiles'); const sb = require('@json-spec/spec-basic'); module.exports = { Pet: s.object({ required: { id: sb.posInt, name: sp.name({}), tag: sb.enum(['dog','cat','lion']) } }) }
x-json-spec
を使って関連付けます。components: schemas: Pet: x-json-spec: Pet required: - id - name properties: id: type: integer format: int64 name: type: string tag: type: string
% node bin/client.js --openapi=examples/petstore/petstore.yaml --jsonspec=examples/petstore/jsonspec.js --base-url=http://localhost:3000 ✔ pass! GET /pets?limit=18 ✔ pass! POST /pets?limit=28 ✔ pass! GET /pets/E75bNb4h7p3637aEVU42Z6Ikc
APIクライアント開発で使うモックサーバ
既存のAPIサーバがあって、それを使って新たにシステムを作るのがトラブル少なくよいことですが、それらを同時に作らなきゃいけないことも往々にしてあります。json-spec-server --openapi=【OpenAPI定義のYAMLまたはJSON】 --jsonspec=【JSON Specの定義】--port=【モックサーバのListenするポート】
% node bin/server.js --openapi=examples/petstore/petstore.yaml --jsonspec=examples/petstore/jsonspec.js & started % curl http://localhost:3000/pets | jq [ { "id": 5, "name": "Dawson Pfeffer", "tag": "lion" }, { "id": 14, "name": "Ashleigh Farrell DVM", "tag": "dog" }, { "id": 26, "name": "Pietro Lockman", "tag": "cat" }, { "id": 2, "name": "Josue McClure", "tag": "dog" }, { "id": 11, "name": "Tomasa Prohaska", "tag": "lion" }, { "id": 21, "name": "Daren Fisher", "tag": "lion" }, { "id": 12, "name": "Damon Tremblay", "tag": "lion" } ]
おわりに
今後はJSON Specから`#/components/schemas
を生成する機能を開発予定です。これでJSON Specを書くのとOpen APIでのコンポーネント定義を書くので重複感がなくなります。当然ながらテストだけでなく、実際のAPIの処理に置いてバリデーションとしてJSON Specが使えますが、サーバサイドの言語がNode.jsに限られてしまうので、GraalVMのPolyglotでなんとかすることを考えています。
Open APIの仕様を書くのに疲れた方は、JSON Specとともに使うことで、そのありがたみをハッキリと再認識できると思いますので、ぜひお試しください。
コメント
コメントを投稿