Skip to content

ComponentFactory API

ComponentFactory は Loader と Parser が共有するコンポーネントを作成・管理し、明確なライフサイクル管理を提供します。

型定義

go
type ComponentFactory struct {
    // プライベートフィールドを含む
}

コア責務:

  • 共有のバリデーター、監査機能、変数エキスパンダーを作成
  • コンポーネントのライフサイクルを管理
  • カスタムパーサーからの内部コンポーネントアクセスをサポート

スレッドセーフ: ComponentFactory のすべてのメソッドはスレッドセーフです。


メソッド

Validator

go
func (f *ComponentFactory) Validator() Validator

バリデーターコンポーネントを返します。キー名と値の検証に使用します。

go
// カスタムパーサーで使用
validator := factory.Validator()

if err := validator.ValidateKey("MY_KEY"); err != nil {
    // キー名が無効
}

if err := validator.ValidateValue("some value"); err != nil {
    // 値に不正な内容が含まれる(ヌルバイト、制御文字など)
}

Auditor

go
func (f *ComponentFactory) Auditor() FullAuditLogger

監査ログコンポーネントを返します。完全な監査ログ機能を提供します。

go
auditor := factory.Auditor()
_ = auditor.Log(env.ActionSet, "KEY", "value set", true)
_ = auditor.LogError(env.ActionSet, "KEY", "validation failed")
_ = auditor.LogWithFile(env.ActionLoad, "KEY", ".env", "loaded", true)
_ = auditor.LogWithDuration(env.ActionParse, "", "parsed", true, time.Since(start))

Expander

go
func (f *ComponentFactory) Expander() VariableExpander

変数エキスパンダーコンポーネントを返します。${VAR} 構文の変数展開に使用します。

go
expander := factory.Expander()
expanded, err := expander.Expand("${BASE_URL}/api")

Close

go
func (f *ComponentFactory) Close() error

ファクトリーが保持するリソースを解放します。クローズ後はファクトリーおよびそれ経由で作成したコンポーネントを使用してはなりません。

動作:

  • 安全なクローズ、複数回呼び出しで nil を返す
  • 監査機能のリソースを解放
  • アトミック操作でスレッドセーフを保証
go
// 通常は Loader が自動管理
loader, _ := env.New(cfg)
defer loader.Close()  // ComponentFactory を自動クローズ

IsClosed

go
func (f *ComponentFactory) IsClosed() bool

ファクトリーがクローズ済みかチェックします。

go
if factory.IsClosed() {
    // ファクトリーはクローズ済み、使用不可
}

作成方法

自動作成(推奨)

Loader 作成時に ComponentFactory が自動的に作成・管理されます:

go
cfg := env.DefaultConfig()
loader, _ := env.New(cfg)
// Loader が内部で ComponentFactory を自動作成
defer loader.Close()  // ファクトリーを自動クローズ

カスタムパーサーで使用

カスタムパーサーを登録する際、ComponentFactory 経由でバリデーターと監査機能を取得します:

go
type CustomParser struct {
    cfg       env.Config
    validator env.Validator
    auditor   env.FullAuditLogger
}

func newCustomParser(cfg env.Config, factory *env.ComponentFactory) *CustomParser {
    return &CustomParser{
        cfg:       cfg,
        validator: factory.Validator(),
        auditor:   factory.Auditor(),
    }
}

// カスタムフォーマット定数を定義(衝突を避けるため 100+ の使用を推奨)
const FormatCustom env.FileFormat = 100

// パーサーを登録
env.RegisterParser(FormatCustom, func(cfg env.Config, factory *env.ComponentFactory) (env.EnvParser, error) {
    return newCustomParser(cfg, factory), nil
})

ライフサイクル管理

text
Config 作成

env.New(cfg)

ComponentFactory を自動作成

    ┌───────┼───────┐
    ↓       ↓       ↓
Validator  Auditor  Expander
    ↓       ↓       ↓
    └───────┼───────┘

      Loader/Parser

      Close() で解放

注意

  • 各 Loader は通常独自の ComponentFactory を持ちます
  • Close() 呼び出し後、そのファクトリー経由で作成したすべてのコンポーネントを使用してはなりません
  • ファクトリーはスレッドセーフで、並行アクセス可能です

監査ハンドラーファクトリー

NewJSONAuditHandler

go
func NewJSONAuditHandler(w io.Writer) *JSONAuditHandler

JSON フォーマットの監査ハンドラーを作成します。構造化ログを出力します。

パラメータ:

  • w - 出力先(os.Stdout、ファイルなど)
go
cfg := env.ProductionConfig()
cfg.AuditEnabled = true
cfg.AuditHandler = env.NewJSONAuditHandler(os.Stdout)

出力例:

json
{"timestamp":"2024-01-15T10:30:00Z","action":"load","file":".env","success":true,"duration_ns":1234567}

NewLogAuditHandler

go
func NewLogAuditHandler(logger *log.Logger) *LogAuditHandler

標準ログフォーマットの監査ハンドラーを作成します。

パラメータ:

  • logger - 標準 log.Logger インスタンス
go
import "log"

logger := log.New(os.Stderr, "[AUDIT] ", log.LstdFlags)
cfg.AuditHandler = env.NewLogAuditHandler(logger)

出力例:

text
[AUDIT] 2024/01/15 10:30:00 load .env success (1.23ms)

NewChannelAuditHandler

go
func NewChannelAuditHandler(ch chan<- AuditEvent) *ChannelAuditHandler

チャネル監査ハンドラーを作成し、監査イベントを非同期処理に使用します。

パラメータ:

  • ch - 監査イベントチャネル

チャネルの所有権

ChannelAuditHandler はチャネルを所有しませんClose() は基盤チャネルをクローズしません。呼び出し側がチャネルをクローズして受信側に終了を通知する必要があります。また、チャネルバッファが満杯の場合、Log() はブロックします——バッファ付きチャネルの使用を推奨します。

go
ch := make(chan env.AuditEvent, 100)
cfg.AuditHandler = env.NewChannelAuditHandler(ch)

// 監査イベントを非同期処理
go func() {
    for event := range ch {
        fmt.Printf("Audit: %+v\n", event)
    }
}()

NewNopAuditHandler

go
func NewNopAuditHandler() *NopAuditHandler

何もしない監査ハンドラーを作成し、監査ログを無効化します。

go
cfg.AuditEnabled = true
cfg.AuditHandler = env.NewNopAuditHandler() // 何も記録しない

NewCloseableChannelHandler

go
func NewCloseableChannelHandler(bufferSize int) *CloseableChannelHandler

独自のバッファチャネルを持つクローズ可能な監査ハンドラーを作成します。ChannelAuditHandler が外部チャネルを受け取るのとは異なり、CloseableChannelHandler は独自のバッファチャネルを作成して所有します。Close() を呼び出すとハンドラーをクローズしチャネルもクローズします。Channel() でイベントを受信します。

パラメータ:

  • bufferSize - バッファチャネルのサイズ(負数は 0 として扱われます)
go
handler := env.NewCloseableChannelHandler(64)
defer handler.Close()

go func() {
    for event := range handler.Channel() {
        fmt.Printf("Audit: %+v\n", event)
    }
}()

CloseableChannelHandler メソッド

CloseableChannelHandlerAuditHandler インターフェース(Log / Close)の実装に加え、以下の固有メソッドを提供します:

go
func (h *CloseableChannelHandler) Channel() <-chan AuditEvent
func (h *CloseableChannelHandler) IsClosed() bool

メソッドの説明:

メソッドシグネチャ用途
Channelfunc (h *CloseableChannelHandler) Channel() <-chan AuditEvent監査イベントを消費するための読み取り専用内部チャネルを返します。Close() 呼び出し後このチャネルはクローズされ、range ループが終了します
IsClosedfunc (h *CloseableChannelHandler) IsClosed() boolハンドラーがクローズ済みかチェック(スレッドセーフ、並行呼び出し可能)
go
handler := env.NewCloseableChannelHandler(64)
defer handler.Close()

// クローズ前に状態をチェック可能
if !handler.IsClosed() {
    // ハンドラーはまだ使用可能
}

// チャネルがクローズされるまでイベントを消費
go func() {
    for event := range handler.Channel() {
        fmt.Printf("Audit: %+v\n", event)
    }
    // handler.Close() 後にチャネルがクローズされ、ループ終了
}()

ファイルシステム

OSFileSystem

デフォルトのファイルシステム実装。OS ファイル操作をカプセル化:

go
type OSFileSystem struct{}

実装インターフェース: FileSystem

go
// メソッドリスト
func (fs OSFileSystem) Open(name string) (File, error)
func (fs OSFileSystem) OpenFile(name string, flag int, perm os.FileMode) (File, error)
func (fs OSFileSystem) Stat(name string) (os.FileInfo, error)
func (fs OSFileSystem) MkdirAll(path string, perm os.FileMode) error
func (fs OSFileSystem) Remove(name string) error
func (fs OSFileSystem) Rename(oldpath, newpath string) error
func (fs OSFileSystem) Getenv(key string) string
func (fs OSFileSystem) Setenv(key, value string) error
func (fs OSFileSystem) Unsetenv(key string) error
func (fs OSFileSystem) LookupEnv(key string) (string, bool)

DefaultFileSystem

go
var DefaultFileSystem FileSystem = OSFileSystem{}

グローバルなデフォルトファイルシステムインスタンス。


カスタムファイルシステムの使用

テストでファイルシステムをモックします:

go
type MockFileSystem struct {
    files map[string]string
    env   map[string]string
}

func (m *MockFileSystem) Open(name string) (env.File, error) {
    content, ok := m.files[name]
    if !ok {
        return nil, os.ErrNotExist
    }
    return &MockFile{content: content}, nil
}

func (m *MockFileSystem) Getenv(key string) string {
    return m.env[key]
}

func (m *MockFileSystem) Setenv(key, value string) error {
    m.env[key] = value
    return nil
}

func (m *MockFileSystem) Unsetenv(key string) error {
    delete(m.env, key)
    return nil
}

func (m *MockFileSystem) LookupEnv(key string) (string, bool) {
    val, ok := m.env[key]
    return val, ok
}

func (m *MockFileSystem) OpenFile(name string, flag int, perm os.FileMode) (env.File, error) {
    return m.Open(name)
}

func (m *MockFileSystem) Stat(name string) (os.FileInfo, error) {
    if _, ok := m.files[name]; !ok {
        return nil, os.ErrNotExist
    }
    return nil, nil
}

func (m *MockFileSystem) MkdirAll(path string, perm os.FileMode) error {
    return nil
}

func (m *MockFileSystem) Remove(name string) error {
    delete(m.files, name)
    return nil
}

func (m *MockFileSystem) Rename(oldpath, newpath string) error {
    m.files[newpath] = m.files[oldpath]
    delete(m.files, oldpath)
    return nil
}

// 使用
cfg := env.TestingConfig()
cfg.FileSystem = &MockFileSystem{
    files: map[string]string{".env": "KEY=value"},
    env:   make(map[string]string),
}

フォーマット検出

DetectFormat

go
func DetectFormat(filename string) FileFormat

ファイル拡張子に基づいてフォーマットを検出します。

パラメータ:

  • filename - ファイル名またはパス

戻り値:

  • FileFormat - 検出されたフォーマット

検出ルール:

拡張子戻り値フォーマット
.envFormatEnv
.jsonFormatJSON
.yaml, .ymlFormatYAML
その他FormatAuto
go
format := env.DetectFormat("config.json")   // FormatJSON
format := env.DetectFormat("settings.yaml") // FormatYAML
format := env.DetectFormat("app.yml")       // FormatYAML
format := env.DetectFormat(".env")          // FormatEnv
format := env.DetectFormat(".env.local")    // FormatAuto (実際は .env として処理)
format := env.DetectFormat("unknown.txt")   // FormatAuto

LoadFiles での応用:

go
loader.LoadFiles("config.env", "settings.json", "secrets.yaml")
// 各ファイルのフォーマットを自動検出し、対応するパーサーを使用

FileFormat 定数

go
const (
    FormatAuto  FileFormat = iota  // 自動検出
    FormatEnv                      // .env フォーマット
    FormatJSON                     // JSON フォーマット
    FormatYAML                     // YAML フォーマット
)

カスタムフォーマット:

go
// カスタムフォーマット定数を定義(衝突を避けるため 100+ の値の使用を推奨)
const (
    FormatTOML  env.FileFormat = 100
    FormatINI   env.FileFormat = 101
    FormatXML   env.FileFormat = 102
)

FileFormat.String

go
func (f FileFormat) String() string

フォーマットの文字列表現を返します。

go
fmt.Println(env.FormatJSON.String())  // "json"
fmt.Println(env.FormatYAML.String())  // "yaml"
fmt.Println(env.FormatEnv.String())   // "dotenv"
fmt.Println(env.FormatAuto.String())  // "auto"
fmt.Println(env.FileFormat(999).String())  // "unknown"

パーサー登録

RegisterParser

go
func RegisterParser(format FileFormat, factory ParserFactory) error

カスタムフォーマットパーサーを登録します。

パラメータ:

  • format - ファイルフォーマット定数
  • factory - パーサーファクトリー関数

戻り値:

  • error - 登録失敗時にエラーを返す

エラーケース:

  • 組み込みフォーマット(FormatEnv、FormatJSON、FormatYAML)は上書き不可
  • フォーマットが登録済み

注意事項:

  • env.New() 呼び出し前に登録する必要がある
  • 組み込みフォーマットとの衝突を避けるため 100+ のフォーマット値の使用を推奨
  • ファクトリー関数はスレッドセーフなパーサーを返すべき
go
package main

import (
    "io"

    "github.com/cybergodev/env"
)

// 1. カスタムフォーマット定数を定義
const FormatTOML env.FileFormat = 100

// 2. パーサーインターフェースを実装
type TOMLParser struct {
    cfg       env.Config
    validator env.Validator
    auditor   env.FullAuditLogger
}

func (p *TOMLParser) Parse(r io.Reader, filename string) (map[string]string, error) {
    // TOML 解析ロジックを実装
    result := make(map[string]string)
    // ... 解析コード
    return result, nil
}

// 3. パーサーを登録(init() で登録、使用前に完了を保証)
func init() {
    err := env.RegisterParser(FormatTOML, func(cfg env.Config, f *env.ComponentFactory) (env.EnvParser, error) {
        return &TOMLParser{
            cfg:       cfg,
            validator: f.Validator(),
            auditor:   f.Auditor(),
        }, nil
    })
    if err != nil {
        panic(err)
    }
}

// 4. カスタムフォーマットを使用
func main() {
    // 登録は init() で完了(main より先に実行)
    loader, _ := env.New(env.DefaultConfig())
    defer loader.Close()

    // .toml ファイルを読み込み可能
    loader.LoadFiles("config.toml")
}

ForceRegisterParser

go
func ForceRegisterParser(format FileFormat, factory ParserFactory) error

パーサーを強制登録し、組み込みパーサーの上書きを許可します。

パラメータ:

  • format - ファイルフォーマット定数
  • factory - パーサーファクトリー関数

戻り値:

  • error - 登録失敗時にエラーを返す(factory が nil の場合)

危険

慎重に使用してください。置換パーサーが同じセキュリティチェック(キー検証、値検証、サイズ制限など)を実装していない場合、組み込みパーサーの上書きはセキュリティ脆弱性を導入する可能性があります。

以下の高度なシーンに適用:

  • 組み込みパーサーへのカスタムセキュリティチェック追加
  • フォーマット拡張の実装(HEREDOC、複数行値など)
  • テスト用モックパーサーの使用
go
// デフォルト .env パーサーを上書き(高度な用途)
err := env.ForceRegisterParser(env.FormatEnv, func(cfg env.Config, f *env.ComponentFactory) (env.EnvParser, error) {
    return &MyCustomEnvParser{
        validator: f.Validator(),
        auditor:   f.Auditor(),
    }, nil
})

ParserFactory 型

go
type ParserFactory func(cfg Config, factory *ComponentFactory) (EnvParser, error)

パーサーファクトリー関数のシグネチャ。

パラメータ:

  • cfg - 設定オブジェクト。制限とセキュリティ設定を含む
  • factory - コンポーネントファクトリー。バリデーターと監査機能を取得可能

戻り値:

  • EnvParser - パーサーインスタンス
  • error - 作成エラー

EnvParser インターフェース

go
type EnvParser interface {
    Parse(r io.Reader, filename string) (map[string]string, error)
}

パーサーが実装しなければならないインターフェース。

パラメータ:

  • r - ファイル内容のリーダー
  • filename - ファイル名(エラー情報用)

戻り値:

  • map[string]string - 解析後のキーと値のペア
  • error - 解析エラー

組み込みパーサー

ライブラリは 3 種類のフォーマットパーサーを組み込みで提供します:

DotEnv Parser

.env フォーマットパーサー。サポート:

  • KEY=value 構文
  • export KEY=value 構文
  • 単一引用符 'value' と二重引用符 "value"
  • 変数展開 ${VAR}${VAR:-default}
  • コメント #

JSON Parser

JSON フォーマットパーサー。サポート:

  • キーと値のペアオブジェクト
  • ネスト構造(フラット化処理)
  • 数値、文字列、ブール値の変換
  • 配列(KEY_0, KEY_1... にフラット化)

YAML Parser

YAML フォーマットパーサー。サポート:

  • キーと値のペア
  • ネスト構造(フラット化処理)
  • 複数のスカラー型
  • リスト(インデックスキーにフラット化)

完全な例

カスタムパーサーの登録

go
package main

import (
    "fmt"
    "io"
    "strings"

    "github.com/cybergodev/env"
)

// カスタム INI パーサー
type INIParser struct {
    cfg       env.Config
    validator env.Validator
    auditor   env.FullAuditLogger
}

func (p *INIParser) Parse(r io.Reader, filename string) (map[string]string, error) {
    content, err := io.ReadAll(r)
    if err != nil {
        return nil, err
    }

    result := make(map[string]string)
    lines := strings.Split(string(content), "\n")
    var section string

    for lineNum, line := range lines {
        line = strings.TrimSpace(line)

        // 空行とコメントをスキップ
        if line == "" || strings.HasPrefix(line, ";") || strings.HasPrefix(line, "#") {
            continue
        }

        // Section [section]
        if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
            section = strings.Trim(line, "[]")
            continue
        }

        // Key=Value
        if idx := strings.Index(line, "="); idx > 0 {
            key := strings.TrimSpace(line[:idx])
            value := strings.TrimSpace(line[idx+1:])

            // section プレフィックスを追加
            if section != "" {
                key = section + "_" + key
            }

            // キーを検証
            if err := p.validator.ValidateKey(key); err != nil {
                _ = p.auditor.LogError(env.ActionParse, key, err.Error())
                return nil, fmt.Errorf("line %d: %w", lineNum+1, err)
            }

            result[strings.ToUpper(key)] = value
        }
    }

    _ = p.auditor.Log(env.ActionParse, "", fmt.Sprintf("parsed %d variables from %s", len(result), filename), true)
    return result, nil
}

func main() {
    // カスタムフォーマットを定義
    const FormatINI env.FileFormat = 101

    // パーサーを登録
    err := env.RegisterParser(FormatINI, func(cfg env.Config, f *env.ComponentFactory) (env.EnvParser, error) {
        return &INIParser{
            cfg:       cfg,
            validator: f.Validator(),
            auditor:   f.Auditor(),
        }, nil
    })
    if err != nil {
        panic(err)
    }

    // カスタムフォーマットを使用
    cfg := env.DefaultConfig()
    loader, _ := env.New(cfg)
    defer loader.Close()

    // .ini ファイルを読み込み可能
    // loader.LoadFiles("config.ini")

    fmt.Println("INI parser registered")
}

カスタムファイルシステム

go
package main

import (
    "errors"
    "fmt"
    "os"
    "strings"
    "time"

    "github.com/cybergodev/env"
)

// メモリファイルシステム(テスト用)
type MemoryFileSystem struct {
    files map[string]string
    env   map[string]string
}

func NewMemoryFileSystem() *MemoryFileSystem {
    return &MemoryFileSystem{
        files: make(map[string]string),
        env:   make(map[string]string),
    }
}

func (m *MemoryFileSystem) Open(name string) (env.File, error) {
    content, ok := m.files[name]
    if !ok {
        return nil, os.ErrNotExist
    }
    return &MemoryFile{reader: strings.NewReader(content)}, nil
}

func (m *MemoryFileSystem) OpenFile(name string, flag int, perm os.FileMode) (env.File, error) {
    return m.Open(name)
}

func (m *MemoryFileSystem) Stat(name string) (os.FileInfo, error) {
    content, ok := m.files[name]
    if !ok {
        return nil, os.ErrNotExist
    }
    return &MemoryFileInfo{name: name, size: int64(len(content))}, nil
}

func (m *MemoryFileSystem) MkdirAll(path string, perm os.FileMode) error {
    return nil
}

func (m *MemoryFileSystem) Remove(name string) error {
    delete(m.files, name)
    return nil
}

func (m *MemoryFileSystem) Rename(oldpath, newpath string) error {
    m.files[newpath] = m.files[oldpath]
    delete(m.files, oldpath)
    return nil
}

func (m *MemoryFileSystem) Getenv(key string) string {
    return m.env[key]
}

func (m *MemoryFileSystem) Setenv(key, value string) error {
    m.env[key] = value
    return nil
}

func (m *MemoryFileSystem) Unsetenv(key string) error {
    delete(m.env, key)
    return nil
}

func (m *MemoryFileSystem) LookupEnv(key string) (string, bool) {
    val, ok := m.env[key]
    return val, ok
}

// MemoryFile は env.File を実装
type MemoryFile struct {
    reader *strings.Reader
}

func (f *MemoryFile) Read(p []byte) (n int, err error)  { return f.reader.Read(p) }
func (f *MemoryFile) Write(p []byte) (n int, err error) { return 0, errors.ErrUnsupported }
func (f *MemoryFile) Close() error                      { return nil }
func (f *MemoryFile) Stat() (os.FileInfo, error)        { return nil, errors.ErrUnsupported }
func (f *MemoryFile) Sync() error                       { return nil }

// MemoryFileInfo は os.FileInfo を実装
type MemoryFileInfo struct {
    name string
    size int64
}

func (i *MemoryFileInfo) Name() string       { return i.name }
func (i *MemoryFileInfo) Size() int64        { return i.size }
func (i *MemoryFileInfo) Mode() os.FileMode  { return 0644 }
func (i *MemoryFileInfo) ModTime() time.Time { return time.Time{} }
func (i *MemoryFileInfo) IsDir() bool        { return false }
func (i *MemoryFileInfo) Sys() interface{}   { return nil }

// 使用例
func main() {
    // メモリファイルシステムを作成
    fs := NewMemoryFileSystem()
    fs.files[".env"] = "APP_NAME=myapp\nPORT=8080\n"

    // カスタムファイルシステムを使用するよう設定
    cfg := env.TestingConfig()
    cfg.FileSystem = fs

    loader, _ := env.New(cfg)
    defer loader.Close()

    loader.LoadFiles(".env")

    fmt.Println(loader.GetString("APP_NAME"))  // myapp
    fmt.Println(loader.GetInt("PORT"))         // 8080
}

関連ドキュメント