1package config
2
3import (
4	"fmt"
5	"io/fs"
6	"path/filepath"
7	"strings"
8
9	"github.com/hashicorp/hcl/v2"
10	"github.com/hashicorp/hcl/v2/hclparse"
11	"github.com/spf13/afero"
12	"github.com/zclconf/go-cty/cty"
13	"github.com/zclconf/go-cty/cty/function"
14)
15
16type SourceType string
17
18const (
19	SourceJSON = "json"
20	SourceHCL  = "hcl"
21)
22
23// EnvVarPrefix is a prefix for environment variable names to be exported for HCL substitution.
24const EnvVarPrefix = "CQ_VAR_"
25
26// Parser is the main interface to read configuration files and other related
27// files from disk.
28//
29// It retains a cache of all files that are loaded so that they can be used
30// to create source code snippets in diagnostics, etc.
31type Parser struct {
32	fs         afero.Afero
33	p          *hclparse.Parser
34	HCLContext hcl.EvalContext
35}
36
37type Option func(*Parser)
38
39func WithFS(fs afero.Fs) Option {
40	return func(p *Parser) {
41		p.fs = afero.Afero{Fs: fs}
42	}
43}
44
45// WithEnvironmentVariables fills hcl.Context with values of environment variables given in vars.
46// Only variables that start with given prefix are considered. Prefix is removed from the name and
47// the name is lower cased then.
48func WithEnvironmentVariables(prefix string, vars []string) Option {
49	return func(p *Parser) {
50		EnvToHCLContext(&p.HCLContext, prefix, vars)
51	}
52}
53
54// NewParser creates and returns a new Parser.
55func NewParser(options ...Option) *Parser {
56	p := Parser{
57		fs: afero.Afero{Fs: afero.OsFs{}},
58		p:  hclparse.NewParser(),
59		HCLContext: hcl.EvalContext{
60			Variables: make(map[string]cty.Value),
61			Functions: make(map[string]function.Function),
62		},
63	}
64
65	for _, opt := range options {
66		opt(&p)
67	}
68	return &p
69}
70
71// LoadHCLFile is a low-level method that reads the file at the given path,
72// parses it, and returns the hcl.Body representing its root. In many cases
73// it is better to use one of the other Load*File methods on this type,
74// which additionally decode the root body in some way and return a higher-level
75// construct.
76//
77// If the file cannot be read at all -- e.g. because it does not exist -- then
78// this method will return a nil body and error diagnostics. In this case
79// callers may wish to ignore the provided error diagnostics and produce
80// a more context-sensitive error instead.
81//
82// The file will be parsed using the HCL native syntax unless the filename
83// ends with ".json", in which case the HCL JSON syntax will be used.
84func (p *Parser) LoadHCLFile(path string) (hcl.Body, hcl.Diagnostics) {
85	src, err := p.fs.ReadFile(path)
86
87	if err != nil {
88		if e, ok := err.(*fs.PathError); ok {
89			err = fmt.Errorf(e.Err.Error())
90		}
91		return nil, hcl.Diagnostics{
92			{
93				Severity: hcl.DiagError,
94				Summary:  "Failed to read file",
95				Detail:   fmt.Sprintf("The file %q could not be read: %s", path, err),
96			},
97		}
98	}
99	return p.loadFromSource(path, src, SourceType(filepath.Ext(path)))
100}
101
102func (p *Parser) loadFromSource(name string, data []byte, ext SourceType) (hcl.Body, hcl.Diagnostics) {
103	var file *hcl.File
104	var diags hcl.Diagnostics
105	switch ext {
106	case SourceJSON:
107		file, diags = p.p.ParseJSON(data, name)
108	default:
109		file, diags = p.p.ParseHCL(data, name)
110	}
111	// If the returned file or body is nil, then we'll return a non-nil empty
112	// body so we'll meet our contract that nil means an error reading the file.
113	if file == nil || file.Body == nil {
114		return hcl.EmptyBody(), diags
115	}
116
117	return file.Body, diags
118}
119
120func (p *Parser) LoadFromSource(name string, data []byte, ext SourceType) (hcl.Body, hcl.Diagnostics) {
121	return p.loadFromSource(name, data, ext)
122}
123
124func EnvToHCLContext(evalContext *hcl.EvalContext, prefix string, vars []string) {
125	for _, e := range vars {
126		pair := strings.SplitN(e, "=", 2)
127		if strings.HasPrefix(pair[0], prefix) {
128			evalContext.Variables[strings.TrimPrefix(pair[0], prefix)] = cty.StringVal(pair[1])
129		}
130	}
131}
132