Conform

バリデーションライブラリGo構造体タグデータ変換サニタイゼーション文字列処理

GitHub概要

leebenson/conform

Trims, sanitizes & scrubs data based on struct tags (go, golang)

スター326
ウォッチ4
フォーク38
作成日:2016年1月5日
言語:Go
ライセンス:MIT License

トピックス

なし

スター履歴

leebenson/conform Star History
データ取得日時: 2025/10/22 08:07

ライブラリ

Conform

概要

Conformは、Go言語向けの構造体タグベースのデータ変換・サニタイゼーションライブラリです。ユーザー入力の文字列を、構造体タグで指定したルールに基づいて自動的にトリミング、正規化、フォーマット処理を行います。バリデーションではなくデータの整形に特化しており、Gorilla Schemaなどのフォーム処理ライブラリと組み合わせて、ユーザー入力を適切な形式に変換します。

詳細

Conform("keep user input in check")は、2016年にリリースされたGo言語のデータ処理ライブラリです。構造体のフィールドにconformタグを追加するだけで、文字列データの様々な変換処理を自動化できます。名前、メールアドレス、URLスラッグなど、フォーマットが重要なフィールドの「第一段階のクリーンアップ」を提供。埋め込み構造体、スライス、マップなどの複雑なデータ構造にも対応し、外部依存なしで動作する軽量設計が特徴です。

主な特徴

  • タグベースの宣言的API: 構造体タグで変換ルールを簡潔に定義
  • 豊富な変換機能: トリミング、大文字小文字変換、スネークケース変換など多数
  • in-place変換: 元の構造体を直接変更する効率的な処理
  • 複雑なデータ構造対応: 埋め込み構造体、スライス、マップの再帰的処理
  • ゼロ依存: 外部ライブラリに依存しない軽量実装
  • フレームワーク統合: Gorilla Schema等との簡単な統合

メリット・デメリット

メリット

  • シンプルで直感的なタグベースのAPI
  • ユーザー入力の前処理を自動化
  • 複数の変換を組み合わせ可能(カンマ区切り)
  • 実行時のオーバーヘッドが最小限
  • フォームバリデーションライブラリとの相性が良い
  • 保守が容易で学習コストが低い

デメリット

  • バリデーション機能は提供しない(別ライブラリが必要)
  • 文字列型フィールドのみに適用
  • 型変換は行わない(文字列のまま)
  • エラーハンドリングがない(常に成功)
  • カスタム変換関数の定義が困難
  • 国際化対応が限定的

参考ページ

書き方の例

インストールと基本セットアップ

# Conformのインストール
go get github.com/leebenson/conform

# go.modへの追加
go mod tidy

基本的な構造体タグの使い方

package main

import (
    "fmt"
    "github.com/leebenson/conform"
)

// 基本的な構造体定義
type Person struct {
    FirstName string `conform:"name"`
    LastName  string `conform:"ucfirst,trim"`
    Email     string `conform:"email"`
    CamelCase string `conform:"camel"`
    UserName  string `conform:"snake"`
    Slug      string `conform:"slug"`
    Blurb     string `conform:"title"`
    Left      string `conform:"ltrim"`
    Right     string `conform:"rtrim"`
}

func main() {
    p := Person{
        FirstName: " LEE ",
        LastName:  "     Benson",
        Email:     "   [email protected]  ",
        CamelCase: "I love new york city",
        UserName:  "lee benson",
        Slug:      "LeeBensonWasHere",
        Blurb:     "this is a little bit about me...",
        Left:      "    Left trim   ",
        Right:     "    Right trim  ",
    }

    // 構造体の変換実行
    conform.Strings(&p) // ポインタを渡す

    fmt.Printf("FirstName: '%s'\n", p.FirstName) // 'Lee'
    fmt.Printf("LastName: '%s'\n", p.LastName)   // 'Benson'
    fmt.Printf("Email: '%s'\n", p.Email)         // '[email protected]'
    fmt.Printf("CamelCase: '%s'\n", p.CamelCase) // 'ILoveNewYorkCity'
    fmt.Printf("UserName: '%s'\n", p.UserName)   // 'lee_benson'
    fmt.Printf("Slug: '%s'\n", p.Slug)           // 'lee-benson-was-here'
    fmt.Printf("Blurb: '%s'\n", p.Blurb)         // 'This Is A Little Bit About Me...'
    fmt.Printf("Left: '%s'\n", p.Left)           // 'Left trim   '
    fmt.Printf("Right: '%s'\n", p.Right)         // '    Right trim'
}

複雑なデータ構造での使用

// スライスとマップを含む構造体
type ComplexData struct {
    // スライスの各要素に変換を適用
    Skills   []string          `conform:"upper"`
    // マップの値に変換を適用(キーは変更されない)
    Examples map[string]string `conform:"!html"`
    // 埋め込み構造体も処理される
    User
}

type User struct {
    Name     string `conform:"name"`
    Username string `conform:"snake"`
}

func main() {
    data := ComplexData{
        Skills: []string{"HtmL", "Yaml", "GoLang"},
        Examples: map[string]string{
            "<best>": "<body><p>I know this & that.</p></body>",
        },
        User: User{
            Name:     " john DOE ",
            Username: "John Doe",
        },
    }

    conform.Strings(&data)

    fmt.Println(data.Skills)   // ["HTML", "YAML", "GOLANG"]
    fmt.Println(data.Examples) // {"<best>": "&lt;body&gt;&lt;p&gt;I know this &amp; that.&lt;/p&gt;&lt;/body&gt;"}
    fmt.Println(data.Name)     // "John Doe"
    fmt.Println(data.Username) // "john_doe"
}

利用可能な変換タグ一覧

// トリミング系
type TrimExample struct {
    Trim  string `conform:"trim"`   // 前後の空白を削除
    LTrim string `conform:"ltrim"`  // 先頭の空白を削除
    RTrim string `conform:"rtrim"`  // 末尾の空白を削除
}

// 大文字小文字変換
type CaseExample struct {
    Lower   string `conform:"lower"`   // 小文字に変換
    Upper   string `conform:"upper"`   // 大文字に変換
    Title   string `conform:"title"`   // タイトルケース
    UCFirst string `conform:"ucfirst"` // 最初の文字を大文字に
}

// 命名規則変換
type NamingExample struct {
    Camel string `conform:"camel"` // キャメルケース(thisIsIt)
    Snake string `conform:"snake"` // スネークケース(this_is_it)
    Slug  string `conform:"slug"`  // URLスラッグ(this-is-it)
}

// 特殊変換
type SpecialExample struct {
    Name  string `conform:"name"`  // 名前用の処理(数字・特殊文字削除、タイトルケース)
    Email string `conform:"email"` // メールアドレス(ドメイン部分を小文字に)
    Num   string `conform:"num"`   // 数字以外を削除
    NoNum string `conform:"!num"`  // 数字を削除
    Alpha string `conform:"alpha"` // アルファベット以外を削除
    NoAlpha string `conform:"!alpha"` // アルファベットを削除
}

// エスケープ処理
type EscapeExample struct {
    HTML string `conform:"!html"` // HTMLエスケープ
    JS   string `conform:"!js"`   // JavaScriptエスケープ
}

// 複数タグの組み合わせ
type MultipleTagsExample struct {
    // カンマ区切りで複数のタグを適用(左から順に処理)
    FullName  string `conform:"trim,name"`
    Email     string `conform:"trim,lower"`
    Username  string `conform:"trim,lower,snake"`
    CleanText string `conform:"trim,!html,title"`
}

func demonstrateTags() {
    // 名前処理の例
    nameExample := struct {
        Name string `conform:"name"`
    }{
        Name: "3493€848Jo-s$%£@Ann ",
    }
    conform.Strings(&nameExample)
    fmt.Println(nameExample.Name) // "Jo-Ann"

    // メール処理の例
    emailExample := struct {
        Email string `conform:"email"`
    }{
        Email: "   [email protected]  ",
    }
    conform.Strings(&emailExample)
    fmt.Println(emailExample.Email) // "[email protected]"

    // 数値抽出の例
    numExample := struct {
        Price string `conform:"num"`
    }{
        Price: "価格は€30,38です",
    }
    conform.Strings(&numExample)
    fmt.Println(numExample.Price) // "3038"
}

Gorilla Schemaとの統合

package main

import (
    "net/http"
    "github.com/gorilla/schema"
    "github.com/leebenson/conform"
)

// フォーム構造体の定義
type RegistrationForm struct {
    FirstName string    `schema:"firstName" conform:"trim,name"`
    LastName  string    `schema:"lastName" conform:"trim,name"`
    Email     string    `schema:"email" conform:"trim,email"`
    Username  string    `schema:"username" conform:"trim,lower,snake"`
    Bio       string    `schema:"bio" conform:"trim"`
    Age       int       `schema:"age"` // 非文字列フィールドは無視される
}

func RegisterHandler(w http.ResponseWriter, r *http.Request) {
    // フォームデータのパース
    if err := r.ParseForm(); err != nil {
        http.Error(w, "フォームパースエラー", http.StatusBadRequest)
        return
    }

    // 構造体へのデコード
    form := new(RegistrationForm)
    decoder := schema.NewDecoder()
    if err := decoder.Decode(form, r.PostForm); err != nil {
        http.Error(w, "デコードエラー", http.StatusBadRequest)
        return
    }

    // Conformで文字列フィールドを自動整形
    conform.Strings(form)

    // この時点で form のデータは整形済み
    // 例: "  JOHN DOE  " -> "John Doe"
    // 例: "  [email protected]  " -> "[email protected]"
    
    // バリデーション処理(別途実装)
    if err := validateForm(form); err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }

    // データベースへの保存など
    saveUser(form)
}

実践的な使用例

// ユーザー入力処理のベストプラクティス
type UserInput struct {
    // 個人情報
    FirstName string `conform:"trim,name" json:"first_name"`
    LastName  string `conform:"trim,name" json:"last_name"`
    
    // 連絡先
    Email    string `conform:"trim,email" json:"email"`
    Phone    string `conform:"trim,num" json:"phone"`
    
    // アカウント情報
    Username string `conform:"trim,lower,snake" json:"username"`
    Slug     string `conform:"trim,slug" json:"slug"`
    
    // プロフィール
    Bio      string `conform:"trim" json:"bio"`
    Website  string `conform:"trim,lower" json:"website"`
    
    // セキュリティ
    DisplayName string `conform:"trim,!html" json:"display_name"`
}

// APIエンドポイントでの使用
func CreateUserAPI(w http.ResponseWriter, r *http.Request) {
    var input UserInput
    
    // JSONのデコード
    if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
        http.Error(w, "無効なJSON", http.StatusBadRequest)
        return
    }
    
    // データの正規化
    conform.Strings(&input)
    
    // 正規化の結果例:
    // FirstName: "  john  " -> "John"
    // Email: "  [email protected]  " -> "[email protected]"
    // Username: "John Doe" -> "john_doe"
    // Phone: "(123) 456-7890" -> "1234567890"
    
    // バリデーション(govalidatorとの組み合わせ)
    if !govalidator.IsEmail(input.Email) {
        http.Error(w, "無効なメールアドレス", http.StatusBadRequest)
        return
    }
    
    // レスポンス
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(input)
}

// カスタムヘルパー関数
func NormalizeUserData(data interface{}) error {
    // Conformの実行
    conform.Strings(data)
    
    // 追加の処理が必要な場合
    if user, ok := data.(*UserInput); ok {
        // 電話番号の国番号を追加など
        if user.Phone != "" && !strings.HasPrefix(user.Phone, "+") {
            user.Phone = "+81" + user.Phone
        }
    }
    
    return nil
}

テストコードの例

package main

import (
    "testing"
    "github.com/leebenson/conform"
)

func TestConformTags(t *testing.T) {
    tests := []struct {
        name     string
        input    interface{}
        expected interface{}
    }{
        {
            name: "名前の正規化",
            input: &struct {
                Name string `conform:"name"`
            }{Name: "  john-DOE  "},
            expected: &struct {
                Name string `conform:"name"`
            }{Name: "John-Doe"},
        },
        {
            name: "メールアドレスの正規化",
            input: &struct {
                Email string `conform:"email"`
            }{Email: "  [email protected]  "},
            expected: &struct {
                Email string `conform:"email"`
            }{Email: "[email protected]"},
        },
        {
            name: "複数タグの組み合わせ",
            input: &struct {
                Text string `conform:"trim,lower,snake"`
            }{Text: "  Hello World  "},
            expected: &struct {
                Text string `conform:"trim,lower,snake"`
            }{Text: "hello_world"},
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            conform.Strings(tt.input)
            // 実際のテストでは reflect.DeepEqual などを使用
        })
    }
}

// ベンチマークテスト
func BenchmarkConform(b *testing.B) {
    type LargeStruct struct {
        Field1  string `conform:"trim,name"`
        Field2  string `conform:"email"`
        Field3  string `conform:"snake"`
        Field4  string `conform:"slug"`
        Field5  string `conform:"!html"`
        Field6  string `conform:"upper"`
        Field7  string `conform:"title"`
        Field8  string `conform:"num"`
        Field9  string `conform:"alpha"`
        Field10 string `conform:"trim,lower"`
    }

    data := &LargeStruct{
        Field1:  "  john DOE  ",
        Field2:  "[email protected]",
        Field3:  "CamelCase String",
        Field4:  "This Is A Title",
        Field5:  "<script>alert('xss')</script>",
        Field6:  "lowercase text",
        Field7:  "this needs title case",
        Field8:  "abc123def456",
        Field9:  "test123!@#",
        Field10: "  UPPERCASE TEXT  ",
    }

    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        conform.Strings(data)
    }
}