Open5

zod インスパイヤーの phel-schema を実装していく

しめじ(smeghead)しめじ(smeghead)

最近、phel-lang という言語を触ってます。https://phel-lang.org/
PHPにコンパイルされるphel-langという言語の特性上、PHPの機能を呼び出すことが簡単にできるようになっています。
https://github.com/smeghead/phel-old-school-guestbook
試しに簡単な古いタイプのWebアプリケーションを作ってみました。基本的な機能は言語コアの機能を使って実装できる状態にはなっていますが、細かい便利ライブラリがまだ揃っていないという状況です。

しめじ(smeghead)しめじ(smeghead)

zod

バリデーションライブラリも存在していません。TypeScriptでzodを使っている訳ではないのですが、zodというライブラリがバリデーションライブラリの枠を超えた形で大活躍しているらしいというのを知って、zodに影響を受けた、phel用のスキーマ定義、バリデーションライブラリを作ってみようと思いました。

https://github.com/smeghead/phel-schema
まずは、基本的な利用方法を、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}
しめじ(smeghead)しめじ(smeghead)

まずは最初の例を 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を使うところかな?

しめじ(smeghead)しめじ(smeghead)

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)))))

これをベースに、複数の型を扱える抽象度の高い設計を考えることにします。

しめじ(smeghead)しめじ(smeghead)

だいぶ、構造含めて変更しました。
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)))))

https://github.com/smeghead/phel-schema/tree/main