marshmallow-formというもの作っています

marshmallow-formというもの作っています(wip)

marshmallow-form というもの作っています。これは、marshmallow をフォームライブラリっぽく使えるようにラッピングしたライブラリです。

フォームライブラリとは

フォームライブラリとはhtmlのフォーム要素を取り扱うのに使うライブラリです。 具体的には、フォーム要素の生成とフォームからpostされたデータのvalidationなどが入ります。 立場的にフォームライブラリとvalidationライブラリは似たようにも見え、djangoのフォームライブラリがたまにvalidationライブラリとして使われることがありますが。両者は微妙に異なるものだという立場を取っています。

フォームライブラリの特徴

フォームライブラリの特徴としては、html要素をレンダリングしなければいけないということがあります。 validationライブラリに関しては受け取るAPIのデータを適切にdeserialize/serializeできれば良いわけですが。 フォームライブラリはそれだけでは足りません。 具体的には以下の様なことができなければいけません。

  • フォームのレンダリングの際に各フィールドに対応する値を管理したい(e.g. label,placeholder,htmlのclassなどの属性)
  • フォームからPOSTされるデータはflatで全て文字列のデータ

前者からは、各フォームのフィールドには種々のmetadataを付加したいという要件が出てきます。 また、select要素について考えてみると分かるのですが、例えばvalidatonライブラリではOneOf(値がある特定の候補の中に含まれていればOKというvalidation)のようなvalidationを付与する際には、単に必要な値の候補をリストで与えれば良いですが、select要素に関しては人の目に触れるラベルのような情報も必要になります。また、同じ文字列を意味するフィールドであってもinput要素でレンダリングされるものあるいはtextareaでレンダリングされるものなど複数種類あります。

後者からは、受け取る値がjsonのようなネスト構造を保持できる構造を期待することができないということです。具体的にはネストした構造を扱う際には、ネスト状態をフラットな状態から復元する必要があります。また、全てが文字列なのでいきなりjsonschemaなど数値型は数値になっているということが前提になっているものなどを利用する事ができません。

実際に必要なフォームライブラリ

ただ、validationライブラリに対して幾つか機能の追加が必要だと評したフォームライブラリですが。 例えば、djangoのフォームライブラリのように直接html要素を生成する機能は付与しなくて良いように思います。 結局、想定しているフォームのhtml要素が固定であることは少なく、大抵テンプレート側に手書きすることになっているからです。 実際の所、widgetというようなメタデータを各フィールドが保持しておいて、それを見てテンプレートが生成の方法を変えるという形にするのが良いのではないかなと思っています。

本当は、さらにjsとの連携を加えられると良いのですが。今のところどのようにjsと連携すれば良いのかということに対する答えが出ていません。

marshmallowについて

marshmallowはvalidationライブラリの1つです。それなりに使いやすいのではないかと思います。

marshmallow-formの使い方

フォームの定義

以下の様な感じで定義します。

import marshmallow_form as mf


class PersonForm(mf.Form):
    name = mf.String(label="名前", placeholder="foo")
    age = mf.Integer(label="年齢", placeholder="0")


class ParentsForm(mf.Form):
    father = mf.Nested(PersonForm, label="父親")
    mother = mf.Nested(PersonForm, label="母親")

これらのフォームのlabel,placeholderはmetadataです。初期値として与えたデータ(initial)及びPOSTデータ(data)は、 各フィールドのvalueにアクセスすることで取得できます。

form = ParentsForm(initial={"father": {"name": "foo"}})
form.father.name["label"] # => 名前
form.father.name.value # => "foo"

フォーム(html要素)の生成

htmlの生成はテンプレート側で手書きするイメージです。 例えばmakoを使って以下の様に書きます。もちろん、普通は、直接手書きせずに関数に分けるんじゃないかと思います。

<form>
%for f in form:
  %if f.get("widget") == "anything":
    ## 何かwidgetのようなmetadataでrendering方法を変える
    do_something
  %else:
  <label> ${f["label"]}
    <input name="${f.name}" value="${f.value}" placeholder="${f['placeholder']}"/>
  </label>
  %endif
%endfor
</form>

POST/GETデータのvalidation

POSTされたデータはdesrialize()でvalidationします。成功失敗にかかわらずdeserializeされた結果が返ってきます。 validationが成功か失敗かはhas_errors()で調べてください。(この辺りのUIはまだ決まっていないです)

input_data = {
    "yagou": "bar",
    "father.name": "foo",
    "father.age": "10",
    "mother.name": "foo",
    "mother.age": "10",
}

form = ParentsForm(input_data)
result = form.deserialize()
print(result)
"""
{
    "yagou": "bar",
    "father": {
        "name": "foo",
        "age": 10
    },
    "mother": {
        "name": "foo",
        "age": 10
    }
}
"""

失敗した時は以下の様なメッセージが出ます。

input_data = {
    "yagou": "bar",
    "father.name": "foo",
    "father.age": "10",
}
form = ParentsForm(input_data)
result = form.deserialize()
print(form.errors)
# {'father': ['Missing data for required field.']}

(このエラーメッセージの修正は必要なTODOかもしれないです)

make_object

また事前にmake_objectのメソッドを定義しておくとdeserializeした段階で変換されます。

from collections import namedtuple

Person = namedtuple("Person", "name age")

class PersonForm(mf.Form):
    name = mf.String(label="名前", placeholder="foo")
    age = mf.Integer(label="年齢", placeholder="0")

    def make_object(self, **kwargs):  # ここのselfはformではなくmarshmallow.Schema
        return Person(**kwargs)

事前にmake_objectを定義しておいた場合には、以下の様にdictがPersonオブジェクトに変換されています。

form = ParentsForm(input_data)
result = form.deserialize()
print(result)
"""
{
    "yagou": "bar",
    "father": Person(name="foo", age=10),
    "mother": Person(name="foo", age=10)
}
"""