diff --git a/.drone.yml b/.drone.yml index 21f4201..09bdf4f 100644 --- a/.drone.yml +++ b/.drone.yml @@ -35,3 +35,20 @@ steps: commands: - cd provider/json - golangci-lint run + +--- +kind: pipeline +name: yaml + +steps: +- name: test + image: golang + commands: + - cd provider/yaml + - go test ./... + +- name: golangci-lint + image: golangci/golangci-lint:v1.55 + commands: + - cd provider/yaml + - golangci-lint run diff --git a/provider/yaml/fixture/config.yaml b/provider/yaml/fixture/config.yaml new file mode 100644 index 0000000..0d99de8 --- /dev/null +++ b/provider/yaml/fixture/config.yaml @@ -0,0 +1,14 @@ +app: + title: yaml title + name: + var: + - test + bool_var: true +duration_var: 21m +empty_var: +url_var: "http://google.com/" +time_var: "2020-01-02T15:04:05Z" +cfg: + duration: 21m + enabled: true + type: yaml diff --git a/provider/yaml/go.mod b/provider/yaml/go.mod new file mode 100644 index 0000000..0102951 --- /dev/null +++ b/provider/yaml/go.mod @@ -0,0 +1,8 @@ +module gitoa.ru/go-4devs/config/provider/yaml + +go 1.21 + +require ( + gitoa.ru/go-4devs/config v0.0.1 + gopkg.in/yaml.v3 v3.0.1 +) diff --git a/provider/yaml/go.sum b/provider/yaml/go.sum new file mode 100644 index 0000000..efd4f67 --- /dev/null +++ b/provider/yaml/go.sum @@ -0,0 +1,6 @@ +gitoa.ru/go-4devs/config v0.0.1 h1:9KrOO09YbIMO8qL8aVn/G74DurGdOIW5y3O02bays4I= +gitoa.ru/go-4devs/config v0.0.1/go.mod h1:xfEC2Al9xnMLJUuekYs3KhJ5BIzWAseNwkMwbN6/xss= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/provider/yaml/provider.go b/provider/yaml/provider.go new file mode 100644 index 0000000..25716a4 --- /dev/null +++ b/provider/yaml/provider.go @@ -0,0 +1,100 @@ +package yaml + +import ( + "context" + "errors" + "fmt" + "os" + + "gitoa.ru/go-4devs/config" + "gitoa.ru/go-4devs/config/value" + "gopkg.in/yaml.v3" +) + +const ( + Name = "yaml" +) + +var _ config.Provider = (*Provider)(nil) + +func NewFile(name string, opts ...Option) (*Provider, error) { + in, err := os.ReadFile(name) + if err != nil { + return nil, fmt.Errorf("yaml_file: read error: %w", err) + } + + return New(in, opts...) +} + +func New(yml []byte, opts ...Option) (*Provider, error) { + var data yaml.Node + if err := yaml.Unmarshal(yml, &data); err != nil { + return nil, fmt.Errorf("yaml: unmarshal err: %w", err) + } + + return create(opts...).With(&data), nil +} + +func create(opts ...Option) *Provider { + prov := Provider{ + name: Name, + } + + for _, opt := range opts { + opt(&prov) + } + + return &prov +} + +type Option func(*Provider) + +type Provider struct { + data node + name string +} + +func (p *Provider) Name() string { + return p.name +} + +func (p *Provider) Value(_ context.Context, path ...string) (config.Value, error) { + return p.data.read(p.Name(), path) +} + +func (p *Provider) With(data *yaml.Node) *Provider { + return &Provider{ + data: node{Node: data}, + } +} + +type node struct { + *yaml.Node +} + +func (n *node) read(name string, keys []string) (config.Value, error) { + val, err := getData(n.Node.Content[0].Content, keys) + if err != nil { + if errors.Is(err, config.ErrValueNotFound) { + return nil, fmt.Errorf("%w: %s", config.ErrValueNotFound, name) + } + + return nil, fmt.Errorf("%w: %s", err, name) + } + + return value.Decode(val), nil +} + +func getData(node []*yaml.Node, keys []string) (func(interface{}) error, error) { + for idx := len(node) - 1; idx > 0; idx -= 2 { + if node[idx-1].Value == keys[0] { + if len(keys) > 1 { + return getData(node[idx].Content, keys[1:]) + } + + return node[idx].Decode, nil + } + } + + return nil, config.ErrValueNotFound +} diff --git a/provider/yaml/provider_test.go b/provider/yaml/provider_test.go new file mode 100644 index 0000000..ca040c2 --- /dev/null +++ b/provider/yaml/provider_test.go @@ -0,0 +1,32 @@ +package yaml_test + +import ( + "embed" + "testing" + "time" + + "gitoa.ru/go-4devs/config/provider/yaml" + "gitoa.ru/go-4devs/config/test" + "gitoa.ru/go-4devs/config/test/require" +) + +//go:embed fixture/* +var fixture embed.FS + +func TestProvider(t *testing.T) { + t.Parallel() + + data, err := fixture.ReadFile("fixture/config.yaml") + require.NoError(t, err) + prov, err := yaml.New(data) + require.NoError(t, err) + + read := []test.Read{ + test.NewRead(21*time.Minute, "duration_var"), + test.NewRead(true, "app", "name", "bool_var"), + test.NewRead(test.Time("2020-01-02T15:04:05Z"), "time_var"), + test.NewReadConfig("cfg"), + } + + test.Run(t, prov, read) +} diff --git a/provider/yaml/watch.go b/provider/yaml/watch.go new file mode 100644 index 0000000..5bd3f83 --- /dev/null +++ b/provider/yaml/watch.go @@ -0,0 +1,46 @@ +package yaml + +import ( + "context" + "fmt" + "os" + + "gitoa.ru/go-4devs/config" + "gopkg.in/yaml.v3" +) + +const NameWatch = "yaml_watch" + +func NewWatch(name string, opts ...Option) *Watch { + wath := Watch{ + file: name, + prov: create(opts...), + name: NameWatch, + } + + return &wath +} + +type Watch struct { + file string + prov *Provider + name string +} + +func (p *Watch) Name() string { + return p.name +} + +func (p *Watch) Value(ctx context.Context, path ...string) (config.Value, error) { + in, err := os.ReadFile(p.file) + if err != nil { + return nil, fmt.Errorf("yaml_file: read error: %w", err) + } + + var yNode yaml.Node + if err = yaml.Unmarshal(in, &yNode); err != nil { + return nil, fmt.Errorf("yaml_file: unmarshal error: %w", err) + } + + return p.prov.With(&yNode).Value(ctx, path...) +}