ComponentFactory API
ComponentFactory は Loader と Parser が共有するコンポーネントを作成・管理し、明確なライフサイクル管理を提供します。
型定義
type ComponentFactory struct {
// プライベートフィールドを含む
}コア責務:
- 共有のバリデーター、監査機能、変数エキスパンダーを作成
- コンポーネントのライフサイクルを管理
- カスタムパーサーからの内部コンポーネントアクセスをサポート
スレッドセーフ: ComponentFactory のすべてのメソッドはスレッドセーフです。
メソッド
Validator
func (f *ComponentFactory) Validator() Validatorバリデーターコンポーネントを返します。キー名と値の検証に使用します。
// カスタムパーサーで使用
validator := factory.Validator()
if err := validator.ValidateKey("MY_KEY"); err != nil {
// キー名が無効
}
if err := validator.ValidateValue("some value"); err != nil {
// 値に不正な内容が含まれる(ヌルバイト、制御文字など)
}Auditor
func (f *ComponentFactory) Auditor() FullAuditLogger監査ログコンポーネントを返します。完全な監査ログ機能を提供します。
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
func (f *ComponentFactory) Expander() VariableExpander変数エキスパンダーコンポーネントを返します。${VAR} 構文の変数展開に使用します。
expander := factory.Expander()
expanded, err := expander.Expand("${BASE_URL}/api")Close
func (f *ComponentFactory) Close() errorファクトリーが保持するリソースを解放します。クローズ後はファクトリーおよびそれ経由で作成したコンポーネントを使用してはなりません。
動作:
- 安全なクローズ、複数回呼び出しで nil を返す
- 監査機能のリソースを解放
- アトミック操作でスレッドセーフを保証
// 通常は Loader が自動管理
loader, _ := env.New(cfg)
defer loader.Close() // ComponentFactory を自動クローズIsClosed
func (f *ComponentFactory) IsClosed() boolファクトリーがクローズ済みかチェックします。
if factory.IsClosed() {
// ファクトリーはクローズ済み、使用不可
}作成方法
自動作成(推奨)
Loader 作成時に ComponentFactory が自動的に作成・管理されます:
cfg := env.DefaultConfig()
loader, _ := env.New(cfg)
// Loader が内部で ComponentFactory を自動作成
defer loader.Close() // ファクトリーを自動クローズカスタムパーサーで使用
カスタムパーサーを登録する際、ComponentFactory 経由でバリデーターと監査機能を取得します:
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
})ライフサイクル管理
Config 作成
↓
env.New(cfg)
↓
ComponentFactory を自動作成
↓
┌───────┼───────┐
↓ ↓ ↓
Validator Auditor Expander
↓ ↓ ↓
└───────┼───────┘
↓
Loader/Parser
↓
Close() で解放注意
- 各 Loader は通常独自の ComponentFactory を持ちます
- Close() 呼び出し後、そのファクトリー経由で作成したすべてのコンポーネントを使用してはなりません
- ファクトリーはスレッドセーフで、並行アクセス可能です
監査ハンドラーファクトリー
NewJSONAuditHandler
func NewJSONAuditHandler(w io.Writer) *JSONAuditHandlerJSON フォーマットの監査ハンドラーを作成します。構造化ログを出力します。
パラメータ:
w- 出力先(os.Stdout、ファイルなど)
cfg := env.ProductionConfig()
cfg.AuditEnabled = true
cfg.AuditHandler = env.NewJSONAuditHandler(os.Stdout)出力例:
{"timestamp":"2024-01-15T10:30:00Z","action":"load","file":".env","success":true,"duration_ns":1234567}NewLogAuditHandler
func NewLogAuditHandler(logger *log.Logger) *LogAuditHandler標準ログフォーマットの監査ハンドラーを作成します。
パラメータ:
logger- 標準 log.Logger インスタンス
import "log"
logger := log.New(os.Stderr, "[AUDIT] ", log.LstdFlags)
cfg.AuditHandler = env.NewLogAuditHandler(logger)出力例:
[AUDIT] 2024/01/15 10:30:00 load .env success (1.23ms)NewChannelAuditHandler
func NewChannelAuditHandler(ch chan<- AuditEvent) *ChannelAuditHandlerチャネル監査ハンドラーを作成し、監査イベントを非同期処理に使用します。
パラメータ:
ch- 監査イベントチャネル
チャネルの所有権
ChannelAuditHandler はチャネルを所有しません、Close() は基盤チャネルをクローズしません。呼び出し側がチャネルをクローズして受信側に終了を通知する必要があります。また、チャネルバッファが満杯の場合、Log() はブロックします——バッファ付きチャネルの使用を推奨します。
ch := make(chan env.AuditEvent, 100)
cfg.AuditHandler = env.NewChannelAuditHandler(ch)
// 監査イベントを非同期処理
go func() {
for event := range ch {
fmt.Printf("Audit: %+v\n", event)
}
}()NewNopAuditHandler
func NewNopAuditHandler() *NopAuditHandler何もしない監査ハンドラーを作成し、監査ログを無効化します。
cfg.AuditEnabled = true
cfg.AuditHandler = env.NewNopAuditHandler() // 何も記録しないNewCloseableChannelHandler
func NewCloseableChannelHandler(bufferSize int) *CloseableChannelHandler独自のバッファチャネルを持つクローズ可能な監査ハンドラーを作成します。ChannelAuditHandler が外部チャネルを受け取るのとは異なり、CloseableChannelHandler は独自のバッファチャネルを作成して所有します。Close() を呼び出すとハンドラーをクローズしチャネルもクローズします。Channel() でイベントを受信します。
パラメータ:
bufferSize- バッファチャネルのサイズ(負数は 0 として扱われます)
handler := env.NewCloseableChannelHandler(64)
defer handler.Close()
go func() {
for event := range handler.Channel() {
fmt.Printf("Audit: %+v\n", event)
}
}()CloseableChannelHandler メソッド
CloseableChannelHandler は AuditHandler インターフェース(Log / Close)の実装に加え、以下の固有メソッドを提供します:
func (h *CloseableChannelHandler) Channel() <-chan AuditEvent
func (h *CloseableChannelHandler) IsClosed() boolメソッドの説明:
| メソッド | シグネチャ | 用途 |
|---|---|---|
Channel | func (h *CloseableChannelHandler) Channel() <-chan AuditEvent | 監査イベントを消費するための読み取り専用内部チャネルを返します。Close() 呼び出し後このチャネルはクローズされ、range ループが終了します |
IsClosed | func (h *CloseableChannelHandler) IsClosed() bool | ハンドラーがクローズ済みかチェック(スレッドセーフ、並行呼び出し可能) |
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 ファイル操作をカプセル化:
type OSFileSystem struct{}実装インターフェース: FileSystem
// メソッドリスト
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
var DefaultFileSystem FileSystem = OSFileSystem{}グローバルなデフォルトファイルシステムインスタンス。
カスタムファイルシステムの使用
テストでファイルシステムをモックします:
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
func DetectFormat(filename string) FileFormatファイル拡張子に基づいてフォーマットを検出します。
パラメータ:
filename- ファイル名またはパス
戻り値:
FileFormat- 検出されたフォーマット
検出ルール:
| 拡張子 | 戻り値フォーマット |
|---|---|
.env | FormatEnv |
.json | FormatJSON |
.yaml, .yml | FormatYAML |
| その他 | FormatAuto |
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") // FormatAutoLoadFiles での応用:
loader.LoadFiles("config.env", "settings.json", "secrets.yaml")
// 各ファイルのフォーマットを自動検出し、対応するパーサーを使用FileFormat 定数
const (
FormatAuto FileFormat = iota // 自動検出
FormatEnv // .env フォーマット
FormatJSON // JSON フォーマット
FormatYAML // YAML フォーマット
)カスタムフォーマット:
// カスタムフォーマット定数を定義(衝突を避けるため 100+ の値の使用を推奨)
const (
FormatTOML env.FileFormat = 100
FormatINI env.FileFormat = 101
FormatXML env.FileFormat = 102
)FileFormat.String
func (f FileFormat) String() stringフォーマットの文字列表現を返します。
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
func RegisterParser(format FileFormat, factory ParserFactory) errorカスタムフォーマットパーサーを登録します。
パラメータ:
format- ファイルフォーマット定数factory- パーサーファクトリー関数
戻り値:
error- 登録失敗時にエラーを返す
エラーケース:
- 組み込みフォーマット(FormatEnv、FormatJSON、FormatYAML)は上書き不可
- フォーマットが登録済み
注意事項:
env.New()呼び出し前に登録する必要がある- 組み込みフォーマットとの衝突を避けるため 100+ のフォーマット値の使用を推奨
- ファクトリー関数はスレッドセーフなパーサーを返すべき
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
func ForceRegisterParser(format FileFormat, factory ParserFactory) errorパーサーを強制登録し、組み込みパーサーの上書きを許可します。
パラメータ:
format- ファイルフォーマット定数factory- パーサーファクトリー関数
戻り値:
error- 登録失敗時にエラーを返す(factoryが nil の場合)
危険
慎重に使用してください。置換パーサーが同じセキュリティチェック(キー検証、値検証、サイズ制限など)を実装していない場合、組み込みパーサーの上書きはセキュリティ脆弱性を導入する可能性があります。
以下の高度なシーンに適用:
- 組み込みパーサーへのカスタムセキュリティチェック追加
- フォーマット拡張の実装(HEREDOC、複数行値など)
- テスト用モックパーサーの使用
// デフォルト .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 型
type ParserFactory func(cfg Config, factory *ComponentFactory) (EnvParser, error)パーサーファクトリー関数のシグネチャ。
パラメータ:
cfg- 設定オブジェクト。制限とセキュリティ設定を含むfactory- コンポーネントファクトリー。バリデーターと監査機能を取得可能
戻り値:
EnvParser- パーサーインスタンスerror- 作成エラー
EnvParser インターフェース
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 フォーマットパーサー。サポート:
- キーと値のペア
- ネスト構造(フラット化処理)
- 複数のスカラー型
- リスト(インデックスキーにフラット化)
完全な例
カスタムパーサーの登録
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")
}カスタムファイルシステム
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
}