Skip to content
Snippets Groups Projects
Commit 70454f00 authored by Semyon Krestyaninov's avatar Semyon Krestyaninov :dog2: Committed by Pavel Antonov
Browse files

feat: Добавлена возможность включить файл в YAML или использовать его...

feat: Добавлена возможность включить файл в YAML или использовать его содержимое в качестве значения

Close #PRXS-2865
parent 8d7ce977
No related branches found
No related tags found
No related merge requests found
package yaml
import (
"io"
"git.perx.ru/perxis/perxis-go/pkg/errors"
"gopkg.in/yaml.v3"
)
// FileResolver подключает содержимое целевого файла в качестве значения поля.
func FileResolver(tp TagProcessor, node *yaml.Node) (*yaml.Node, error) {
if node.Kind != yaml.ScalarNode {
return nil, errors.New("!include on a non-scalar node")
}
file, err := tp.FS().Open(node.Value)
if err != nil {
return nil, err
}
defer func() { _ = file.Close() }()
bytes, err := io.ReadAll(file)
if err != nil {
return nil, err
}
out := new(yaml.Node)
out.SetString(string(bytes))
return out, err
}
package yaml
import (
"path/filepath"
"git.perx.ru/perxis/perxis-go/pkg/errors"
"gopkg.in/yaml.v3"
)
// IncludeResolver включает содержимое целевого YAML-файла в поле.
func IncludeResolver(tp TagProcessor, node *yaml.Node) (*yaml.Node, error) {
if node.Kind != yaml.ScalarNode {
return nil, errors.New("!include on a non-scalar node")
}
if ext := filepath.Ext(node.Value); ext != ".yaml" && ext != ".yml" {
return nil, errors.New("!include on file with unknown extension")
}
file, err := tp.FS().Open(node.Value)
if err != nil {
return nil, err
}
defer func() { _ = file.Close() }()
out := new(yaml.Node)
err = yaml.NewDecoder(file).Decode(WithTagProcessor(tp.FS())(out))
if err != nil {
return nil, err
}
return out, nil
}
package yaml
import (
"io/fs"
"gopkg.in/yaml.v3"
)
type TagProcessor interface {
yaml.Unmarshaler
FS() fs.FS
}
// tagProcessor обёртка для декодирования целевого значения.
//
// Перед декодированием значения обрабатываются все теги узла.
type tagProcessor struct {
fsys fs.FS
target any
}
// WithTagProcessor возвращает функцию, которая оборачивает декодируемые значения для поддержки обработки тегов YAML.
//
// Для путей к файлам, используемых в качестве значений тегов, пути должны быть указаны относительно переданной файловой системы.
func WithTagProcessor(fsys fs.FS) func(any) *tagProcessor {
return func(v any) *tagProcessor {
return &tagProcessor{fsys: fsys, target: v}
}
}
func (tp *tagProcessor) FS() fs.FS {
return tp.fsys
}
func (tp *tagProcessor) UnmarshalYAML(value *yaml.Node) error {
resolved, err := resolveTags(tp, value)
if err != nil {
return err
}
return resolved.Decode(tp.target)
}
// resolveTags обрабатывает все теги YAML для переданного узла и возвращает исправленный узел.
// Если узел представляет собой последовательность или словарь, то обрабатываются теги для его дочерних элементов.
func resolveTags(tp TagProcessor, node *yaml.Node) (*yaml.Node, error) {
switch node.Kind {
case yaml.SequenceNode, yaml.MappingNode:
for i := range node.Content {
var err error
node.Content[i], err = resolveTags(tp, node.Content[i])
if err != nil {
return nil, err
}
}
default:
if resolver, ok := tagResolvers[node.Tag]; ok {
return resolver.Resolve(tp, node)
}
}
return node, nil
}
package yaml
import (
"testing"
"git.perx.ru/perxis/perxis-go/yaml/testdata"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
)
func TestTagProcessor(t *testing.T) {
t.Run("!file", func(t *testing.T) {
file, err := testdata.FS.Open("file/file_simple.yaml")
require.NoError(t, err)
defer file.Close()
var result any
decoder := yaml.NewDecoder(file)
err = decoder.Decode(WithTagProcessor(testdata.FS)(&result))
require.NoError(t, err)
assert.Equal(t, map[string]any{"config": `server {
listen 80;
server_name example.com;
location / {
root /var/www/example.com/html;
index index.html index.htm;
try_files $uri $uri/ =404;
}
}`}, result)
})
t.Run("!include", func(t *testing.T) {
file, err := testdata.FS.Open("include/include_simple.yaml")
require.NoError(t, err)
defer file.Close()
var result any
decoder := yaml.NewDecoder(file)
err = decoder.Decode(WithTagProcessor(testdata.FS)(&result))
require.NoError(t, err)
assert.Equal(t, map[string]any{"data": map[string]any{"text": "Hello, World!"}}, result)
})
}
config: !file file/nginx.conf
\ No newline at end of file
server {
listen 80;
server_name example.com;
location / {
root /var/www/example.com/html;
index index.html index.htm;
try_files $uri $uri/ =404;
}
}
\ No newline at end of file
data: !include include/simple_data.yaml
\ No newline at end of file
text: Hello, World!
\ No newline at end of file
package testdata
import "embed"
//go:embed *
var FS embed.FS
package yaml
import (
"gopkg.in/yaml.v3"
)
var (
NewDecoder = yaml.NewDecoder
NewEncoder = yaml.NewEncoder
Unmarshal = yaml.Unmarshal
Marshal = yaml.Marshal
)
var tagResolvers = make(map[string]Resolver)
func RegisterTagResolver(tag string, resolver Resolver) {
tagResolvers[tag] = resolver
}
// Resolver обрабатывает тег YAML и возвращает его обработанный вариант
type Resolver interface {
Resolve(tp TagProcessor, node *yaml.Node) (*yaml.Node, error)
}
type ResolverFunc func(TagProcessor, *yaml.Node) (*yaml.Node, error)
func (fn ResolverFunc) Resolve(tp TagProcessor, node *yaml.Node) (*yaml.Node, error) {
return fn(tp, node)
}
func init() {
RegisterTagResolver("!include", ResolverFunc(IncludeResolver))
RegisterTagResolver("!file", ResolverFunc(FileResolver))
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment