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 - Канал событий аудита
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

Помимо реализации интерфейса AuditHandler (Log / Close), CloseableChannelHandler предоставляет следующие специфичные методы:

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

Реализация файловой системы по умолчанию, обёртывающая файловые операции ОС:

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 - Ошибка разбора

Встроенные парсеры

Библиотека включает три встроенных парсера форматов:

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
}

Связанная документация