From db8bb97dfa2dacef002a1f349ea970d76fee4fc9 Mon Sep 17 00:00:00 2001 From: Alexander Neonxp Kiryukhin Date: Sun, 22 Feb 2026 20:15:50 +0300 Subject: Refactoring --- README.md | 211 ++++++++++++++++++++++++++++++++-------------- example/file2.conf | 7 ++ example/main.go | 15 +++- internal/ast/processor.go | 26 +++--- loader.go | 30 +++++-- loader_test.go | 35 ++++++-- model/assignment.go | 6 -- model/body.go | 28 ++++++ model/command.go | 9 -- model/directive.go | 9 ++ model/doc.go | 41 --------- model/setting.go | 6 ++ model/visitor.go | 6 ++ visitor/default.go | 63 ++++++++++++++ 14 files changed, 346 insertions(+), 146 deletions(-) create mode 100644 example/file2.conf delete mode 100644 model/assignment.go create mode 100644 model/body.go delete mode 100644 model/command.go create mode 100644 model/directive.go delete mode 100644 model/doc.go create mode 100644 model/setting.go create mode 100644 model/visitor.go create mode 100644 visitor/default.go diff --git a/README.md b/README.md index 1c0eb9e..267b226 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # conf -Go библиотека для чтения конфигурационных файлов в формате `.conf`. +Go библиотека для парсинга конфигурационных файлов `.conf`. ## Установка @@ -10,17 +10,15 @@ go get go.neonxp.ru/conf ## Особенности формата -- Простые присваивания: `key = value;` -- Строковые значения: `"text"` или `'text'` -- Многострочные строки: ``` `text` ``` -- Числовые значения: целые и дробные, включая отрицательные -- Булевы значения: `true` / `false` -- Директивы/команды: `directive arg1 arg2;` -- Групповые директивы с блоками кода: `directive { ... }` -- Комментарии от `#` до конца строки -- Поддержка кириллицы и UTF-8 +- **Присваивания**: `key = value;` +- **Типы значений**: строки (двойные/одинарные кавычки, backticks), числа (целые/дробные), булевы значения +- **Директивы**: `directive arg1 arg2;` +- **Блочные директивы**: `directive { ... }` +- **Комментарии**: `#` до конца строки +- **UTF-8**: включая кириллицу +- **Подстановка переменных окружения**: `$VAR` -## Пример использования +## Быстрый старт ```go package main @@ -28,86 +26,163 @@ package main import ( "fmt" "go.neonxp.ru/conf" + "go.neonxp.ru/conf/model" + "go.neonxp.ru/conf/visitor" ) func main() { - doc, err := conf.LoadFile("./config.conf") - if err != nil { + cfg := conf.New() + if err := cfg.LoadFile("config.conf"); err != nil { panic(err) } - // Получение значения по ключу - values := doc.Get("my_key") - fmt.Println(values) - - // Получение команд по имени - commands := doc.Commands("directivename") - fmt.Println(commands) + v := visitor.NewDefault() + if err := cfg.Process(v); err != nil { + panic(err) + } - // Все переменные - vars := doc.Vars() + // Доступ по пути с точечной нотацией + val, err := v.Get("server.host") + if err != nil { + panic(err) + } + fmt.Println(val.String()) // localhost - // Все элементы документа - items := doc.Items() + port, err := v.Get("server.port") + fmt.Println(port.Int()) // 8080 } ``` ## Пример конфигурационного файла ```conf -# Пример конфигурации +# Переменная окружения: db_file = $HOME "/app/data.db"; + +# Простые присваивания +rss = "https://neonxp.ru/feed/"; +host = "localhost"; +port = 8080; +debug = true; + +# Директивы с аргументами +telegram "bot123" "-1003888840756" { + token = "token_value"; + admin_chat = "@admin"; +} + +# Вложенные блоки +server { + host = "localhost"; + port = 8080; -# Простое присваивание -simple_key = value; + ssl { + enabled = true; + cert = "/etc/ssl/cert.pem"; + } -# Многострочное присваивание -string_key = - "value" - 'string'; + middleware "auth" { + enabled = true; + secret = "$JWT_SECRET"; + } +} -# Многострочные строки (backticks) -multiline_string = ` - multiline - string - 123 +# Многострочные строки +template = ` + + + Hello + `; +``` + +## API -# Числа и булевы значения -int_key = -123.456; -bool_key = true; +### Загрузка конфигурации -# Директивы -expression1 argument1 "argument2" 123; +```go +cfg := conf.New() -# Групповая директива -group_directive_without_arguments { - expression1 argument2 "string" 123 true; - expression2 argument3 "string111" 123321 false; +// Из файла +cfg.LoadFile("config.conf") - children_group "some argument" { - # Вложенная группа - } +// Из памяти +cfg.Load("inline", []byte("key = value;")) +``` + +### Обработка через Visitor + +Библиотека использует паттерн Visitor для обхода конфигурации: + +```go +type Visitor interface { + VisitDirective(ident string, args Values, body Body) error + VisitSetting(key string, values Values) error } +``` -# Групповая директива с аргументами -group_directive_with_argument "argument1" 'argument2' { - child_val = "children value"; +### Get-методы на Values + +| Метод | Описание | +|-------|----------| +| `String()` | Строковое представление через пробел | +| `Int()` | Преобразование в int (одно значение) | +| `BuildString(lookups...)` | Сборка строки с подстановками | + +### Подстановка переменных окружения + +```go +vals, _ := v.Get("db_file") +path := vals.BuildString(model.LookupEnv) +// $HOME → "/home/user", результат: "/home/user/app/data.db" +``` + +### Кастомные подстановки + +```go +substitutions := map[model.Word]string{ + "APP_DIR": "/opt/myapp", + "LOG_LEVEL": "debug", } + +path := vals.BuildString(model.LookupSubst(substitutions), model.Origin) ``` -## API +## Реализация собственного Visitor -### Функции +```go +type MyVisitor struct{} -- `LoadFile(filename string) (*model.Doc, error)` - загрузка конфигурации из файла -- `Load(name string, input []byte) (*model.Doc, error)` - парсинг конфигурации из байтов +func (m *MyVisitor) VisitDirective(ident string, args model.Values, body model.Body) error { + fmt.Printf("Directive: %s, args: %s\n", ident, args.String()) + return body.Execute(m) // Рекурсивный обход тела +} -### Методы `*model.Doc` +func (m *MyVisitor) VisitSetting(key string, values model.Values) error { + fmt.Printf("Setting: %s = %s\n", key, values.String()) + return nil +} +``` + +## Грамматика (EBNF) -- `Get(key string) Values` - получить значения по ключу -- `Commands(name string) Commands` - получить команды по имени -- `Vars() map[string]Values` - получить все переменные -- `Items() []any` - получить все элементы документа +``` +Config = Doc . +Doc = Stmt { Stmt } . +Stmt = Word ( Assignment | Command ) . + +Assignment = "=" Values br . +Command = [Values] ( Body | br ) . + +Values = Value { Value } . +Value = Word | String | Number | Boolean . +Body = "{" [ Doc ] "}" . + +Word = word (alpha | "$" | "_") {alpha | number | "$" | "_"} . +String = `"[^"]*"` | `'[^']*'` | '`' { `[^`]' } '`' . +Number = `-?[0-9]+(\.[0-9]+)?` . +Boolean = `true` | `false` . +br = ";" . +``` ## Требования @@ -115,4 +190,18 @@ group_directive_with_argument "argument1" 'argument2' { ## Лицензия -См. файл [LICENSE](LICENSE) +Этот проект лицензирован в соответствии с GNU General Public License версии 3 +(GPLv3). Подробности смотрите в файле [LICENSE](LICENSE). + +``` + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2026 Alexander NeonXP Kiryukhin + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. +``` + +## Автор + +- Александр Кирюхин diff --git a/example/file2.conf b/example/file2.conf new file mode 100644 index 0000000..b22f06b --- /dev/null +++ b/example/file2.conf @@ -0,0 +1,7 @@ +rss = "https://neonxp.ru/feed/"; +db_file = $XDG_DATA_HOME "/pose/pose.db"; + +telegram { + token = "279146841:AAsome_secret_token_M"; + groups = "-1003888840756"; +} diff --git a/example/main.go b/example/main.go index 6e0c8b8..c42ab3b 100644 --- a/example/main.go +++ b/example/main.go @@ -4,13 +4,24 @@ import ( "fmt" "go.neonxp.ru/conf" + "go.neonxp.ru/conf/visitor" ) func main() { - out, err := conf.LoadFile("./file.conf") + cfg := conf.New() + if err := cfg.LoadFile("./example/file2.conf"); err != nil { + panic(err) + } + + pr := visitor.NewDefault() + if err := cfg.Process(pr); err != nil { + panic(err) + } + + tok, err := pr.Get("telegram.token") if err != nil { panic(err) } - fmt.Println(out) + fmt.Println(tok.String()) } diff --git a/internal/ast/processor.go b/internal/ast/processor.go index 783dbbe..55dce59 100644 --- a/internal/ast/processor.go +++ b/internal/ast/processor.go @@ -8,7 +8,7 @@ import ( "go.neonxp.ru/conf/model" ) -func ToDoc(config *Node) (*model.Doc, error) { +func ToDoc(config *Node) (model.Body, error) { if len(config.Children) < 1 { return nil, fmt.Errorf("invalid ast tree") } @@ -18,31 +18,31 @@ func ToDoc(config *Node) (*model.Doc, error) { return processDoc(doc), nil } -func processDoc(docNode *Node) *model.Doc { - doc := model.New(len(docNode.Children)) +func processDoc(docNode *Node) model.Body { + doc := make(model.Body, 0, len(docNode.Children)) for _, stmt := range docNode.Children { - processStmt(doc, stmt) + processStmt(&doc, stmt) } return doc } -func processStmt(doc *model.Doc, stmt *Node) { +func processStmt(doc *model.Body, stmt *Node) { ident := extractIdent(stmt.Children[0]) nodeBody := stmt.Children[1] switch nodeBody.Symbol { case parser.Command: - doc.AppendCommand(processCommand(ident, nodeBody)) + *doc = append(*doc, processDirective(ident, nodeBody)) case parser.Assignment: - doc.AppendAssignment(processAssignment(ident, nodeBody)) + *doc = append(*doc, processSetting(ident, nodeBody)) } } -func processCommand(ident string, command *Node) *model.Command { - result := &model.Command{ +func processDirective(ident string, directive *Node) *model.Directive { + result := &model.Directive{ Name: ident, } - for _, child := range command.Children { + for _, child := range directive.Children { // Can be arguments OR body OR both switch child.Symbol { case parser.Values: @@ -56,10 +56,10 @@ func processCommand(ident string, command *Node) *model.Command { return result } -func processAssignment(ident string, assignment *Node) *model.Assignment { - result := &model.Assignment{ +func processSetting(ident string, setting *Node) *model.Setting { + result := &model.Setting{ Key: ident, - Value: extractValues(assignment.Children[1]), // Children[0] = '=', Children[1] = Values, Children[2] = ';' + Value: extractValues(setting.Children[1]), // Children[0] = '=', Children[1] = Values, Children[2] = ';' } return result diff --git a/loader.go b/loader.go index e9757a6..7f58237 100644 --- a/loader.go +++ b/loader.go @@ -9,28 +9,44 @@ import ( "go.neonxp.ru/conf/model" ) -func LoadFile(filename string) (*model.Doc, error) { +func New() *Conf { + return &Conf{ + root: model.Body{}, + } +} + +type Conf struct { + root model.Body +} + +func (c *Conf) LoadFile(filename string) error { content, err := os.ReadFile(filename) if err != nil { - return nil, fmt.Errorf("failed load file: %w", err) + return fmt.Errorf("failed load file: %w", err) } - return Load(filename, content) + return c.Load(filename, content) } -func Load(name string, input []byte) (*model.Doc, error) { +func (c *Conf) Load(name string, input []byte) error { p := &parser.Parser{} astSlice, err := p.Parse(name, input) if err != nil { - return nil, fmt.Errorf("failed parse conf content: %w", err) + return fmt.Errorf("failed parse conf content: %w", err) } astTree := ast.Parse(p, astSlice) doc, err := ast.ToDoc(astTree[0]) if err != nil { - return nil, fmt.Errorf("failed build Doc: %w", err) + return fmt.Errorf("failed build Doc: %w", err) } - return doc, nil + c.root = doc + + return nil +} + +func (c *Conf) Process(visitor model.Visitor) error { + return c.root.Execute(visitor) } diff --git a/loader_test.go b/loader_test.go index acd689d..3e16922 100644 --- a/loader_test.go +++ b/loader_test.go @@ -4,9 +4,10 @@ import ( "fmt" "go.neonxp.ru/conf" + "go.neonxp.ru/conf/visitor" ) -func ExampleLoad() { +func ExampleNew() { config := ` key = "value"; group "test" { @@ -14,17 +15,37 @@ func ExampleLoad() { } ` - cfg, err := conf.Load("example", []byte(config)) + cfg := conf.New() + + if err := cfg.Load("example", []byte(config)); err != nil { + panic(err) + } + + pr := visitor.NewDefault() + if err := cfg.Process(pr); err != nil { + panic(err) + } + + val1, err := pr.Get("key") + if err != nil { + panic(err) + } + val2, err := pr.Get("group.key") if err != nil { panic(err) } - fmt.Println("key =", cfg.Get("key")[0]) - group := cfg.Commands("group") - for _, gr := range group { - fmt.Println("key =", gr.Body.Get("key")[0]) + val3, err := pr.Get("group") + if err != nil { + panic(err) } + + fmt.Println("key =", val1.String()) + fmt.Println("group.key =", val2.String()) + fmt.Println("group args =", val3.String()) + // Output: // key = value - // key = 123 + // group.key = 123 + // group args = test } diff --git a/model/assignment.go b/model/assignment.go deleted file mode 100644 index 746e2b3..0000000 --- a/model/assignment.go +++ /dev/null @@ -1,6 +0,0 @@ -package model - -type Assignment struct { - Key string - Value Values -} diff --git a/model/body.go b/model/body.go new file mode 100644 index 0000000..b7b4a0c --- /dev/null +++ b/model/body.go @@ -0,0 +1,28 @@ +package model + +import ( + "errors" + "fmt" +) + +type Body []any + +var ErrInvalidType = errors.New("invalid type") + +func (d Body) Execute(v Visitor) error { + for _, it := range d { + switch it := it.(type) { + case *Setting: + if err := v.VisitSetting(it.Key, it.Value); err != nil { + return err + } + case *Directive: + if err := v.VisitDirective(it.Name, it.Arguments, it.Body); err != nil { + return err + } + default: + return fmt.Errorf("%w: %t", ErrInvalidType, it) + } + } + return nil +} diff --git a/model/command.go b/model/command.go deleted file mode 100644 index 237e94f..0000000 --- a/model/command.go +++ /dev/null @@ -1,9 +0,0 @@ -package model - -type Command struct { - Name string - Arguments Values - Body *Doc -} - -type Commands []*Command diff --git a/model/directive.go b/model/directive.go new file mode 100644 index 0000000..3852e72 --- /dev/null +++ b/model/directive.go @@ -0,0 +1,9 @@ +package model + +type Directive struct { + Name string + Arguments Values + Body Body +} + +type Directives []*Directive diff --git a/model/doc.go b/model/doc.go deleted file mode 100644 index 9c13a4c..0000000 --- a/model/doc.go +++ /dev/null @@ -1,41 +0,0 @@ -package model - -type Doc struct { - items []any - vars map[string]Values - commands map[string]Commands -} - -func New(cap int) *Doc { - return &Doc{ - items: make([]any, 0, cap), - vars: make(map[string]Values, cap), - commands: make(map[string]Commands, cap), - } -} - -func (d *Doc) AppendAssignment(e *Assignment) { - d.items = append(d.items, e) - d.vars[e.Key] = append(d.vars[e.Key], e.Value...) -} - -func (d *Doc) AppendCommand(c *Command) { - d.items = append(d.items, c) - d.commands[c.Name] = append(d.commands[c.Name], c) -} - -func (d *Doc) Vars() map[string]Values { - return d.vars -} - -func (d *Doc) Get(key string) Values { - return d.vars[key] -} - -func (d *Doc) Commands(name string) Commands { - return d.commands[name] -} - -func (d *Doc) Items() []any { - return d.items -} diff --git a/model/setting.go b/model/setting.go new file mode 100644 index 0000000..363a8a9 --- /dev/null +++ b/model/setting.go @@ -0,0 +1,6 @@ +package model + +type Setting struct { + Key string + Value Values +} diff --git a/model/visitor.go b/model/visitor.go new file mode 100644 index 0000000..3f290d3 --- /dev/null +++ b/model/visitor.go @@ -0,0 +1,6 @@ +package model + +type Visitor interface { + VisitDirective(ident string, args Values, body Body) error + VisitSetting(key string, values Values) error +} diff --git a/visitor/default.go b/visitor/default.go new file mode 100644 index 0000000..d8c8172 --- /dev/null +++ b/visitor/default.go @@ -0,0 +1,63 @@ +package visitor + +import ( + "errors" + "fmt" + "strings" + + "go.neonxp.ru/conf/model" +) + +var ( + ErrEmptyQuery = errors.New("empty query") + ErrNoChildKey = errors.New("no child key") +) + +func NewDefault() *Default { + return &Default{ + vars: map[string]model.Values{}, + children: map[string]*Default{}, + args: model.Values{}, + } +} + +// Default просто собирает рекурсивно все переменные в дерево. +// На самом деле, для большинства сценариев конфигов его должно хватить. +type Default struct { + vars map[string]model.Values + children map[string]*Default + args model.Values +} + +func (p *Default) VisitDirective(ident string, args model.Values, body model.Body) error { + p.children[ident] = NewDefault() + p.children[ident].args = args + return body.Execute(p.children[ident]) +} + +func (p *Default) VisitSetting(key string, values model.Values) error { + p.vars[key] = values + + return nil +} + +func (p *Default) Get(path string) (model.Values, error) { + splitPath := strings.SplitN(path, ".", 2) + switch len(splitPath) { + case 1: + if v, ok := p.vars[splitPath[0]]; ok { + return v, nil + } + if child, ok := p.children[splitPath[0]]; ok { + return child.args, nil + } + return nil, fmt.Errorf("%w: %s", ErrNoChildKey, splitPath[0]) + case 2: + if child, ok := p.children[splitPath[0]]; ok { + return child.Get(splitPath[1]) + } + return nil, fmt.Errorf("%w: %s", ErrNoChildKey, splitPath[0]) + default: + return nil, ErrEmptyQuery + } +} -- cgit v1.2.3