blob: 70b7b46910642c8d291947469a1bd8375bead8a7 [file] [log] [blame]
Sergiusz Bazanskic881cf32020-04-08 20:03:12 +02001package config
2
3import (
4 "fmt"
5 "html/template"
6 "strings"
7
8 "github.com/BurntSushi/toml"
9 "github.com/golang/glog"
10
11 "code.hackerspace.pl/hscloud/devtools/hackdoc/source"
12)
13
14// Config is a configuration concerning a given path of the source. It is built
15// from files present in the source, and from global configuration.
16type Config struct {
17 // DefaultIndex is the filenames that should attempt to be rendered if no exact path is given
18 DefaultIndex []string
19 // Templates are the templates available to render markdown files, keyed by template name.
20 Templates map[string]*template.Template
21
22 // Errors that occured while building this config (due to config file errors, etc).
23 Errors map[string]error
24}
25
26type configToml struct {
27 DefaultIndex []string `toml:"default_index"`
28 Templates map[string]*configTomlTemplate `toml:"template"`
29}
30
31type configTomlTemplate struct {
32 Sources []string `toml:"sources"`
33}
34
35func parseToml(data []byte) (*configToml, error) {
36 var c configToml
37 err := toml.Unmarshal(data, &c)
38 if err != nil {
39 return nil, err
40 }
41 if c.Templates == nil {
42 c.Templates = make(map[string]*configTomlTemplate)
43 }
44 return &c, nil
45}
46
47func configFileLocations(path string) []string {
48 // Support for unix-style filesystem prefix (/foo/bar/baz) and
49 // perforce-depot-style prefix (//foo/bar/baz).
50 // Also support relative paths.
51 pathTrimmed := strings.TrimLeft(path, "/")
52 prefixLen := len(path) - len(pathTrimmed)
53 prefix := path[:prefixLen]
54 path = pathTrimmed
55 if len(prefix) > 2 {
56 return nil
57 }
58
59 // Turn path into possible directory names, including root.
60 path = strings.Trim(path, "/")
61 parts := strings.Split(path, "/")
62 if parts[0] != "" {
63 parts = append([]string{""}, parts...)
64 }
65
66 locations := []string{}
67 for i, _ := range parts {
68 p := strings.Join(parts[:i+1], "/")
69 p += "/hackdoc.toml"
70 p = prefix + strings.Trim(p, "/")
71 locations = append(locations, p)
72 }
73 return locations
74}
75
76func ForPath(s source.Source, path string) (*Config, error) {
77 if path != "//" {
78 path = strings.TrimRight(path, "/")
79 }
80
81 // Try cache.
82 cacheKey := fmt.Sprintf("config:%s", path)
83 if v := s.CacheGet(cacheKey); v != nil {
84 cfg, ok := v.(*Config)
85 if !ok {
86 glog.Errorf("Cache key %q corrupted, deleting", cacheKey)
87 s.CacheSet([]string{}, cacheKey, nil)
88 } else {
89 return cfg, nil
90 }
91 }
92
93 // Feed cache.
94 cfg := &Config{
95 Templates: make(map[string]*template.Template),
96 Errors: make(map[string]error),
97 }
98
99 tomlPaths := configFileLocations(path)
100 for _, p := range tomlPaths {
101 file, err := s.IsFile(p)
102 if err != nil {
103 return nil, fmt.Errorf("IsFile(%q): %w", path, err)
104 }
105 if !file {
106 continue
107 }
108 data, err := s.ReadFile(p)
109 if err != nil {
110 return nil, fmt.Errorf("ReadFile(%q): %w", path, err)
111 }
112
113 c, err := parseToml(data)
114 if err != nil {
115 cfg.Errors[p] = err
116 continue
117 }
118
119 err = cfg.updateFromToml(p, s, c)
120 if err != nil {
121 return nil, fmt.Errorf("updating from %q: %w", p, err)
122 }
123 }
124
125 return cfg, nil
126}
127
128func (c *Config) updateFromToml(p string, s source.Source, t *configToml) error {
129 if t.DefaultIndex != nil {
130 c.DefaultIndex = t.DefaultIndex
131 }
132
133 for k, v := range t.Templates {
134 tmpl := template.New(k)
135
136 for _, source := range v.Sources {
137 data, err := s.ReadFile(source)
138 if err != nil {
139 c.Errors[p] = fmt.Errorf("reading template file %q: %w", source, err)
140 return nil
141 }
142 tmpl, err = tmpl.Parse(string(data))
143 if err != nil {
144 c.Errors[p] = fmt.Errorf("parsing template file %q: %w", source, err)
145 return nil
146 }
147 }
148 c.Templates[k] = tmpl
149 }
150
151 return nil
152}