From a0f4a313ef9d9f6cbab300cc72a03ad4d4b21941 Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Thu, 1 Feb 2018 19:07:02 -0800 Subject: [PATCH] configs: Parser type configs.Parser is the entry-point for this package, providing functions to load and parse HCL-based configuration files. We use the library "afero" to decouple the parser from the physical OS filesystem, which here allows us to easily use an in-memory filesystem for testing and will, in future, allow us to read files from more unusual places, such as configuration embedded in a plan file. --- configs/parser.go | 85 ++++++++++++++++++++++++++++++++++++++++++ configs/parser_test.go | 31 +++++++++++++++ 2 files changed, 116 insertions(+) create mode 100644 configs/parser.go create mode 100644 configs/parser_test.go diff --git a/configs/parser.go b/configs/parser.go new file mode 100644 index 0000000000..3d3f07ff18 --- /dev/null +++ b/configs/parser.go @@ -0,0 +1,85 @@ +package configs + +import ( + "fmt" + "strings" + + "github.com/hashicorp/hcl2/hcl" + "github.com/hashicorp/hcl2/hclparse" + "github.com/spf13/afero" +) + +// Parser is the main interface to read configuration files and other related +// files from disk. +// +// It retains a cache of all files that are loaded so that they can be used +// to create source code snippets in diagnostics, etc. +type Parser struct { + fs afero.Afero + p *hclparse.Parser +} + +// NewParser creates and returns a new Parser that reads files from the given +// filesystem. If a nil filesystem is passed then the system's "real" filesystem +// will be used, via afero.OsFs. +func NewParser(fs afero.Fs) *Parser { + if fs == nil { + fs = afero.OsFs{} + } + + return &Parser{ + fs: afero.Afero{Fs: fs}, + p: hclparse.NewParser(), + } +} + +// LoadHCLFile is a low-level method that reads the file at the given path, +// parses it, and returns the hcl.Body representing its root. In many cases +// it is better to use one of the other Load*File methods on this type, +// which additionally decode the root body in some way and return a higher-level +// construct. +// +// If the file cannot be read at all -- e.g. because it does not exist -- then +// this method will return a nil body and error diagnostics. In this case +// callers may wish to ignore the provided error diagnostics and produce +// a more context-sensitive error instead. +// +// The file will be parsed using the HCL native syntax unless the filename +// ends with ".json", in which case the HCL JSON syntax will be used. +func (p *Parser) LoadHCLFile(path string) (hcl.Body, hcl.Diagnostics) { + src, err := p.fs.ReadFile(path) + + if err != nil { + return nil, hcl.Diagnostics{ + { + Severity: hcl.DiagError, + Summary: "Failed to read file", + Detail: fmt.Sprintf("The file %q could not be read.", path), + }, + } + } + + var file *hcl.File + var diags hcl.Diagnostics + switch { + case strings.HasSuffix(path, ".json"): + file, diags = p.p.ParseJSON(src, path) + default: + file, diags = p.p.ParseHCL(src, path) + } + + // If the returned file or body is nil, then we'll return a non-nil empty + // body so we'll meet our contract that nil means an error reading the file. + if file == nil || file.Body == nil { + return hcl.EmptyBody(), diags + } + + return file.Body, diags +} + +// Sources returns a map of the cached source buffers for all files that +// have been loaded through this parser, with source filenames (as requested +// when each file was opened) as the keys. +func (p *Parser) Sources() map[string][]byte { + return p.p.Sources() +} diff --git a/configs/parser_test.go b/configs/parser_test.go new file mode 100644 index 0000000000..316a09e5d9 --- /dev/null +++ b/configs/parser_test.go @@ -0,0 +1,31 @@ +package configs + +import ( + "os" + "path" + + "github.com/spf13/afero" +) + +// testParser returns a parser that reads files from the given map, which +// is from paths to file contents. +// +// Since this function uses only in-memory objects, it should never fail. +// If any errors are encountered in practice, this function will panic. +func testParser(files map[string]string) *Parser { + fs := afero.Afero{Fs: afero.NewMemMapFs()} + + for filePath, contents := range files { + dirPath := path.Dir(filePath) + err := fs.MkdirAll(dirPath, os.ModePerm) + if err != nil { + panic(err) + } + err = fs.WriteFile(filePath, []byte(contents), os.ModePerm) + if err != nil { + panic(err) + } + } + + return NewParser(fs) +}