mirror of
https://github.com/tarampampam/error-pages.git
synced 2024-08-30 18:22:40 +00:00
257 lines
6.0 KiB
Go
257 lines
6.0 KiB
Go
package config
|
|
|
|
import (
|
|
"io/ioutil"
|
|
"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 = ioutil.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 := ioutil.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)
|
|
}
|