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() // Автоматическое закрытие ComponentFactoryIsClosed
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) *JSONAuditHandlerСоздаёт обработчик аудита в формате JSON, выводит структурированные логи.
Параметры:
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- Канал событий аудита
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
Помимо реализации интерфейса AuditHandler (Log / Close), CloseableChannelHandler предоставляет следующие специфичные методы:
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
Реализация файловой системы по умолчанию, обёртывающая файловые операции ОС:
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") // FormatAutoПрименение в LoadFiles:
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- Ошибка разбора
Встроенные парсеры
Библиотека включает три встроенных парсера форматов:
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
}Связанная документация
- Определения интерфейсов - Все определения интерфейсов
- Пользовательский парсер - Руководство по пользовательскому парсеру
- Сценарии тестирования - Тестирование с пользовательской файловой системой