Skip to content

Formを作る

Formは、リソースの設定項目と、その設定が意味する振る舞いを記述するものです。 定義をパッケージにまとめると、公開元、クライアント、Hostが同じ内容を検証できます。 ここでは小さな定義からパッケージを組み立て、入力の検証と改変の検出まで試します。

まず動かす

所要時間の目安は5分です。GoとGit、初回のソース・依存取得にはネットワーク接続が必要です。 既にこのリポジトリを取得している場合は、そのルートで最後の2行を実行してください。

sh
git clone https://github.com/tako0614/takoform.git
cd takoform
go mod download
go test -v ./formpackage -run '^ExampleVerifyFS$' -count=1

テストは次の出力を検証し、PASS で終了します。

text
GreetingPolicy 1
changed payload rejected: true

例は架空の GreetingPolicy を使います。ローカルの一時データだけを扱い、 署名、パッケージの公開、Host上のリソース作成は行いません。

設定と意味を定義する

この例の設定は、40文字以内の挨拶の接頭辞です。作成時にそのまま保存し、 更新時に置き換え、削除時に取り除く方針を定義します。実行用のエンドポイントはありません。

go
const exampleDefinition = `{
  "apiVersion": "resources.publisher.example",
  "kind": "GreetingPolicy",
  "definitionVersion": "0.1.0",
  "title": "Greeting policy authoring example",
  "description": "Synthetic authoring fixture. The prefix is stored exactly as supplied; updating replaces it and deleting removes the policy. No runtime endpoint is provided.",
  "role": "policy",
  "requiresHostApi": "forms.takoform.com/v1",
  "desiredSchema": {
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "type": "object",
    "additionalProperties": false,
    "properties": {"prefix": {"type": "string", "minLength": 1, "maxLength": 40}},
    "required": ["prefix"]
  },
  "lifecycleCapabilities": ["create", "read", "update", "delete", "observe"]
}`

desiredSchema は入力の形を検証します。それだけでは「更新で何が変わるか」や 「失敗後に再試行できるか」は決まりません。実用のFormでは、ライフサイクル、失敗時の扱い、 必要なInterfaceやBindingまで定義します。同じFormとして提供できる振る舞いの範囲は Form Definition移植性の範囲 を参照してください。

パッケージを組み立てて検証する

次のコードは上の定義と入力を検証し、definition.jsonpackage-index.json を持つ 仮想ファイルシステムを組み立てます。VerifyFS は一時ディレクトリを使って内容を検証し、 処理後にそのディレクトリを削除します。

go
func ExampleVerifyFS() {
	raw := []byte(exampleDefinition)
	definition, err := formpackage.ValidateDefinition(raw)
	if err != nil {
		panic(err)
	}
	if err := formpackage.ValidateDesiredInstance(definition.DesiredSchema, map[string]any{"prefix": "Hello"}); err != nil {
		panic(err)
	}
	schemaDigest, err := formpackage.DigestCanonicalJSON(raw)
	if err != nil {
		panic(err)
	}
	index, err := json.Marshal(map[string]any{
		"apiVersion": formpackage.VersionlessFamilyPackageAPIVersion,
		"kind":       "FormPackage",
		"formRef": formpackage.FormRef{
			APIVersion: definition.APIVersion, Kind: definition.Kind,
			DefinitionVersion: definition.DefinitionVersion, SchemaDigest: schemaDigest,
		},
		"definitionPath": "definition.json",
		"files": []map[string]any{{
			"path": "definition.json", "mediaType": formpackage.DefinitionMediaType,
			"size": len(raw), "digest": formpackage.DigestBytes(raw),
		}},
	})
	if err != nil {
		panic(err)
	}
	files := fstest.MapFS{
		"definition.json":    &fstest.MapFile{Data: raw},
		"package-index.json": &fstest.MapFile{Data: index},
	}
	report, err := formpackage.VerifyFS(files, ".")
	if err != nil {
		panic(err)
	}
	fmt.Println(report.FormRef.Kind, report.FileCount)

	// A stale index must not validate a changed payload.
	files["definition.json"].Data = append(append([]byte{}, raw...), '\n')
	_, err = formpackage.VerifyFS(files, ".")
	fmt.Println("changed payload rejected:", err != nil)
	// Output:
	// GreetingPolicy 1
	// changed payload rejected: true
}

schemaDigest は正規化した定義を識別します。一方、索引の各ファイルの digestsize は収録したバイト列そのものを検証します。例の最後では定義ファイルに改行を足し、 索引を更新しないまま再検証して、改変が拒否されることを確認しています。

ファイルとして配布するときも同じ定義と索引を保存し、すべての収録ファイルと参照先を揃えます。 索引の形式と計算規則は Form Package が基準です。

公開前に揃えるもの

  1. 公開元が管理する名前空間と、Formの用途・振る舞いを決める。
  2. 設定、更新・削除、エラー、再試行の規則と、それを確認するテストを用意する。
  3. 互換性の規則 に従ってバージョンを決め、公開済みの内容を上書きしない。
  4. 署名と失効の仕様 に沿った来歴・署名・失効情報と、利用例・制約の説明を公開元で用意する。
  5. 利用するHostが、その正確なFormRefを実装し、対象の利用者に許可しているかを確認する。

署名の検証に成功することと、その公開元を信頼するかは別です。信頼ポリシーは利用者・運用者が 決めます。Coreへの中央カタログ登録で利用可能になる仕組みではありません。

次は 共通モデル でSnapshotへの参照のまとめ方を確認するか、 GoからHostを使う でAPIの呼び出しを試してください。

仕様とソースコードは GitHub で公開しています。