vitrine

config.go

package main

import (
	"fmt"
	"os"
	"path/filepath"

	"gopkg.in/yaml.v3"
)

// exhibit is one configured mount: a directory served under a URL prefix.
type exhibit struct {
	// Path is the directory to serve.
	// In the config file it may be relative (resolved against the config file's directory);
	// after loadConfig it is always absolute.
	Path string `yaml:"path"`
	// ExposeGit makes the .git directory list and serve like any other content
	// (it is hidden and unreachable by default).
	ExposeGit bool `yaml:"expose-git"`
}

// config is the top-level schema of the vitrine config file.
type config struct {
	// Exhibits maps a URL prefix (which may contain slashes) to what it serves.
	Exhibits map[string]exhibit `yaml:"exhibits"`
}

// loadConfig reads and validates the YAML config file at the given path.
// Relative exhibit paths are resolved against the config file's directory,
// so a config travels with the tree it describes.
// Any parse or validation problem is an error: vitrine refuses to start on a bad config.
func loadConfig(path string) (*config, error) {
	f, err := os.Open(path)
	if err != nil {
		return nil, fmt.Errorf("config: %w", err)
	}
	defer f.Close()

	var cfg config
	dec := yaml.NewDecoder(f)
	dec.KnownFields(true) // typo'd keys are errors, not silence
	if err := dec.Decode(&cfg); err != nil {
		return nil, fmt.Errorf("config %s: %w", path, err)
	}

	if len(cfg.Exhibits) == 0 {
		return nil, fmt.Errorf("config %s: no exhibits configured; a vitrine with nothing on display is a misconfiguration", path)
	}

	base, err := filepath.Abs(filepath.Dir(path))
	if err != nil {
		return nil, fmt.Errorf("config %s: resolving directory: %w", path, err)
	}
	for name, ex := range cfg.Exhibits {
		if ex.Path == "" {
			return nil, fmt.Errorf("config %s: exhibit %q: missing required field %q", path, name, "path")
		}
		if !filepath.IsAbs(ex.Path) {
			ex.Path = filepath.Join(base, ex.Path)
			cfg.Exhibits[name] = ex
		}
	}
	return &cfg, nil
}

config.go · 1.9 KiB · 3d0af8c57e