error-pages/internal/config/config.go
2022-11-02 01:13:03 +04:00

256 lines
6.0 KiB
Go

package config
import (
"os"
"path"
"path/filepath"
"strconv"
"strings"
"github.com/a8m/envsubst"
"github.com/pkg/errors"
"gopkg.in/yaml.v3"
)
// Config is a main (exportable) config struct.
type Config struct {
Templates []Template
Pages map[string]Page // map key is a page code
Formats map[string]Format // map key is a format name
}
// Template returns a Template with the passes name.
func (c *Config) Template(name string) (*Template, bool) {
for i := 0; i < len(c.Templates); i++ {
if c.Templates[i].name == name {
return &c.Templates[i], true
}
}
return &Template{}, false
}
func (c *Config) JSONFormat() (*Format, bool) { return c.format("json") }
func (c *Config) XMLFormat() (*Format, bool) { return c.format("xml") }
func (c *Config) format(name string) (*Format, bool) {
if f, ok := c.Formats[name]; ok {
if len(f.content) > 0 {
return &f, true
}
}
return &Format{}, false
}
// TemplateNames returns all template names.
func (c *Config) TemplateNames() []string {
n := make([]string, len(c.Templates))
for i, t := range c.Templates {
n[i] = t.name
}
return n
}
// Template describes HTTP error page template.
type Template struct {
name string
content []byte
}
// Name returns the name of the template.
func (t Template) Name() string { return t.name }
// Content returns the template content.
func (t Template) Content() []byte { return t.content }
func (t *Template) loadContentFromFile(filePath string) (err error) {
if t.content, err = os.ReadFile(filePath); err != nil {
return errors.Wrap(err, "cannot load content for the template "+t.Name()+" from file "+filePath)
}
return
}
// Page describes error page.
type Page struct {
code string
message string
description string
}
// Code returns the code of the Page.
func (p Page) Code() string { return p.code }
// Message returns the message of the Page.
func (p Page) Message() string { return p.message }
// Description returns the description of the Page.
func (p Page) Description() string { return p.description }
// Format describes different response formats.
type Format struct {
name string
content []byte
}
// Name returns the name of the format.
func (f Format) Name() string { return f.name }
// Content returns the format content.
func (f Format) Content() []byte { return f.content }
// config is internal struct for marshaling/unmarshaling configuration file content.
type config struct {
Templates []struct {
Path string `yaml:"path"`
Name string `yaml:"name"`
Content string `yaml:"content"`
} `yaml:"templates"`
Formats map[string]struct {
Content string `yaml:"content"`
} `yaml:"formats"`
Pages map[string]struct {
Message string `yaml:"message"`
Description string `yaml:"description"`
} `yaml:"pages"`
}
// Validate the config struct and return an error if something is wrong.
func (c config) Validate() error {
if len(c.Templates) == 0 {
return errors.New("empty templates list")
} else {
for i := 0; i < len(c.Templates); i++ {
if c.Templates[i].Name == "" && c.Templates[i].Path == "" {
return errors.New("empty path and name with index " + strconv.Itoa(i))
}
if c.Templates[i].Path == "" && c.Templates[i].Content == "" {
return errors.New("empty path and template content with index " + strconv.Itoa(i))
}
}
}
if len(c.Pages) == 0 {
return errors.New("empty pages list")
} else {
for code := range c.Pages {
if code == "" {
return errors.New("empty page code")
}
if strings.ContainsRune(code, ' ') {
return errors.New("code should not contain whitespaces")
}
}
}
if len(c.Formats) > 0 {
for name := range c.Formats {
if name == "" {
return errors.New("empty format name")
}
if strings.ContainsRune(name, ' ') {
return errors.New("format should not contain whitespaces")
}
}
}
return nil
}
// Export the config struct into Config.
func (c *config) Export() (*Config, error) {
cfg := &Config{}
cfg.Templates = make([]Template, 0, len(c.Templates))
for i := 0; i < len(c.Templates); i++ {
tpl := Template{name: c.Templates[i].Name}
if c.Templates[i].Content == "" {
if c.Templates[i].Path == "" {
return nil, errors.New("path to the template " + c.Templates[i].Name + " not provided")
}
if err := tpl.loadContentFromFile(c.Templates[i].Path); err != nil {
return nil, err
}
} else {
tpl.content = []byte(c.Templates[i].Content)
}
cfg.Templates = append(cfg.Templates, tpl)
}
cfg.Pages = make(map[string]Page, len(c.Pages))
for code, p := range c.Pages {
cfg.Pages[code] = Page{code: code, message: p.Message, description: p.Description}
}
cfg.Formats = make(map[string]Format, len(c.Formats))
for name, f := range c.Formats {
cfg.Formats[name] = Format{name: name, content: []byte(strings.TrimSpace(f.Content))}
}
return cfg, nil
}
// FromYaml creates new Config instance using YAML-structured content.
func FromYaml(in []byte) (_ *Config, err error) {
in, err = envsubst.Bytes(in)
if err != nil {
return nil, err
}
c := &config{}
if err = yaml.Unmarshal(in, c); err != nil {
return nil, errors.Wrap(err, "cannot parse configuration file")
}
var basename string
for i := 0; i < len(c.Templates); i++ {
if c.Templates[i].Name == "" { // set the template name from file path
basename = filepath.Base(c.Templates[i].Path)
c.Templates[i].Name = strings.TrimSuffix(basename, filepath.Ext(basename))
}
}
if err = c.Validate(); err != nil {
return nil, err
}
return c.Export()
}
// FromYamlFile creates new Config instance using YAML file.
func FromYamlFile(filepath string) (*Config, error) {
bytes, err := os.ReadFile(filepath)
if err != nil {
return nil, errors.Wrap(err, "cannot read configuration file")
}
// the following code makes it possible to use the relative links in the config file (`.` means "directory with
// the config file")
cwd, err := os.Getwd()
if err == nil {
if err = os.Chdir(path.Dir(filepath)); err != nil {
return nil, err
}
defer func() { _ = os.Chdir(cwd) }()
}
return FromYaml(bytes)
}