zod インスパイヤーの phel-schema を実装していく
最近、phel-lang という言語を触ってます。https://phel-lang.org/
PHPにコンパイルされるphel-langという言語の特性上、PHPの機能を呼び出すことが簡単にできるようになっています。
試しに簡単な古いタイプのWebアプリケーションを作ってみました。基本的な機能は言語コアの機能を使って実装できる状態にはなっていますが、細かい便利ライブラリがまだ揃っていないという状況です。
zod
バリデーションライブラリも存在していません。TypeScriptでzodを使っている訳ではないのですが、zodというライブラリがバリデーションライブラリの枠を超えた形で大活躍しているらしいというのを知って、zodに影響を受けた、phel用のスキーマ定義、バリデーションライブラリを作ってみようと思いました。
まずは、基本的な利用方法を、zodの利用方法のページのTypeScriptコードをphel-langに翻訳する形で、こんな感じで使えればよいかな。というREADMEページを用意してみました。
(ns app
(:require smeghead\schema :as z))
# creating a schema for strings
(def my-schema (z/string))
# parsing
(z/parse my-schema "tuna") # => "tuna"
(z/parse my-schema 12) # => throws ZodError
# "safe" parsing (doesn't throw error if validation fails)
(z/safe-parse my-schema "tuna") # => {:success true :data "tuna"}
(z/safe-parse my-schema 12) # => {:success false :error ZodError}
まずは最初の例を phel
のテストコードを用意しました。
tests/unit/schema.phel
(ns smeghead\tests\unit\schema
(:require phel\test :refer [deftest is])
(:require smeghead\schema :as z))
(deftest test-string-parse-ok
(let [my-schema (z/string)]
(is (= "tuna" (z/parse my-schema "tuna")))))
(deftest test-string-parse-ng
(let [my-schema (z/string)]
(is (thrown? \Exception (z/parse my-schema 12)))))
これを満たす最小限の実装を書いてみました。
src/schema.phel
(ns smeghead\schema)
(defstruct schema [type message])
(defn string []
(schema :string ""))
(defn parse [schema target]
(cond
(= (schema :type) :string)
(if (string? target)
target
(throw (php/new \Exception "zod error")))))
最初の例は、string
だけだけど、型の種類が増えていくので、型の定義と型チェック関数を纏めて定義できるようにしないと、管理できなくなる事が目に見えている。phelのInterfaceを使うところかな?
parse に似た safe-parse という関数があります。
tests/unit/schema.phel
(deftest test-string-safe-parse-ok
(let [my-schema (z/string)
result (z/safe-parse my-schema "tuna")]
(is (= true (result :success)))
(is (= "tuna" (result :data)))))
(deftest test-string-safe-parse-ng
(let [my-schema (z/string)
result (z/safe-parse my-schema 12)]
(is (= false (result :success)))
(is (= "Exception" (php/get_class (result :error))))))
safe-parse を使って、parseを実装する形に変更しました。
src/schema.phel
(ns smeghead\schema)
(defstruct schema [type message])
(defn string []
(schema :string ""))
(defn safe-parse [schema target]
(cond
(= (schema :type) :string)
(if (string? target)
{:success true :data target}
{:success false :error (php/new \Exception "zod error")})))
(defn parse [schema target]
(let [result (safe-parse schema target)]
(if (result :success)
(result :data)
(throw (result :error)))))
これをベースに、複数の型を扱える抽象度の高い設計を考えることにします。
だいぶ、構造含めて変更しました。
phelで例外クラスを定義することができず、phpのソースコードも追加しました。
src/phel/schema.phel
型については、src/phel/type/*.phel
に定義するようにしました。
(z/string)
で2つのバリデーションルールが適応される事に気がついて、複数のルールを持つschema
という構造体をベースに管理することにしました。
ルールは、phelのinterfaceの機能を使って、追加していくことにしました。
(ns smeghead\schema
(:use \Smeghead\PhelSchema\ZodError)
(:require smeghead\schema\interfaces :refer [valid message])
(:require smeghead\schema\type\number)
(:require smeghead\schema\type\string))
(defn string []
(string/string))
(defn number []
(number/number))
(defn- validate [schema target]
(loop [rules (schema :rules)
acc []]
(if (empty? rules)
acc
(let [rule (first rules)]
(if (valid rule target)
(recur (rest rules) acc)
(recur (rest rules) (push acc {:message (message rule)})))))))
(defn safe-parse [schema target]
(let [issues (validate schema target)]
(if (empty? issues)
{:success true :data target}
{:success false :error (php/:: ZodError (create "Zod Error" (to-php-array issues)))})))
(defn parse [schema target]
(let [result (safe-parse schema target)]
(if (result :success)
(result :data)
(throw (result :error)))))