hclsimple: Simple one-shot parse/decode/evaluate API
For programs that don't need fine control over the process of decoding a configuration, this allow a one-shot decode into a value of a Go struct type.
This commit is contained in:
parent
b0134908b0
commit
34955ebf80
33
doc.go
33
doc.go
@ -1 +1,34 @@
|
|||||||
|
// Package hcl contains the main modelling types and general utility functions
|
||||||
|
// for HCL.
|
||||||
|
//
|
||||||
|
// For a simple entry point into HCL, see the package in the subdirectory
|
||||||
|
// "hclsimple", which has an opinionated function Decode that can decode HCL
|
||||||
|
// configurations in either native HCL syntax or JSON syntax into a Go struct
|
||||||
|
// type:
|
||||||
|
//
|
||||||
|
// package main
|
||||||
|
//
|
||||||
|
// import (
|
||||||
|
// "log"
|
||||||
|
// "github.com/hashicorp/hcl/v2/hclsimple"
|
||||||
|
// )
|
||||||
|
//
|
||||||
|
// type Config struct {
|
||||||
|
// LogLevel string `hcl:"log_level"`
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// func main() {
|
||||||
|
// var config Config
|
||||||
|
// err := hclsimple.DecodeFile("config.hcl", nil, &config)
|
||||||
|
// if err != nil {
|
||||||
|
// log.Fatalf("Failed to load configuration: %s", err)
|
||||||
|
// }
|
||||||
|
// log.Printf("Configuration is %#v", config)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// If your application needs more control over the evaluation of the
|
||||||
|
// configuration, you can use the functions in the subdirectories hclparse,
|
||||||
|
// gohcl, hcldec, etc. Splitting the handling of configuration into multiple
|
||||||
|
// phases allows for advanced patterns such as allowing expressions in one
|
||||||
|
// part of the configuration to refer to data defined in another part.
|
||||||
package hcl
|
package hcl
|
||||||
|
108
hclsimple/hclsimple.go
Normal file
108
hclsimple/hclsimple.go
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
// Package hclsimple is a higher-level entry point for loading HCL
|
||||||
|
// configuration files directly into Go struct values in a single step.
|
||||||
|
//
|
||||||
|
// This package is more opinionated than the rest of the HCL API. See the
|
||||||
|
// documentation for function Decode for more information.
|
||||||
|
package hclsimple
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/hashicorp/hcl/v2"
|
||||||
|
"github.com/hashicorp/hcl/v2/gohcl"
|
||||||
|
"github.com/hashicorp/hcl/v2/hclsyntax"
|
||||||
|
"github.com/hashicorp/hcl/v2/json"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Decode parses, decodes, and evaluates expressions in the given HCL source
|
||||||
|
// code, in a single step.
|
||||||
|
//
|
||||||
|
// The main HCL API is built to allow applications that need to decompose
|
||||||
|
// the processing steps into a pipeline, with different tasks done by
|
||||||
|
// different parts of the program: parsing the source code into an abstract
|
||||||
|
// representation, analysing the block structure, evaluating expressions,
|
||||||
|
// and then extracting the results into a form consumable by the rest of
|
||||||
|
// the program.
|
||||||
|
//
|
||||||
|
// This function does all of those steps in one call, going directly from
|
||||||
|
// source code to a populated Go struct value.
|
||||||
|
//
|
||||||
|
// The "filename" and "src" arguments describe the input configuration. The
|
||||||
|
// filename is used to add source location context to any returned error
|
||||||
|
// messages and its suffix will choose one of the two supported syntaxes:
|
||||||
|
// ".hcl" for native syntax, and ".json" for HCL JSON. The src must therefore
|
||||||
|
// contain a sequence of bytes that is valid for the selected syntax.
|
||||||
|
//
|
||||||
|
// The "ctx" argument provides variables and functions for use during
|
||||||
|
// expression evaluation. Applications that need no variables nor functions
|
||||||
|
// can just pass nil.
|
||||||
|
//
|
||||||
|
// The "target" argument must be a pointer to a value of a struct type,
|
||||||
|
// with struct tags as defined by the sibling package "gohcl".
|
||||||
|
//
|
||||||
|
// The return type is error but any non-nil error is guaranteed to be
|
||||||
|
// type-assertable to hcl.Diagnostics for applications that wish to access
|
||||||
|
// the full error details.
|
||||||
|
//
|
||||||
|
// This is a very opinionated function that is intended to serve the needs of
|
||||||
|
// applications that are just using HCL for simple configuration and don't
|
||||||
|
// need detailed control over the decoding process. Because this function is
|
||||||
|
// just wrapping functionality elsewhere, if it doesn't meet your needs then
|
||||||
|
// please consider copying it into your program and adapting it as needed.
|
||||||
|
func Decode(filename string, src []byte, ctx *hcl.EvalContext, target interface{}) error {
|
||||||
|
var file *hcl.File
|
||||||
|
var diags hcl.Diagnostics
|
||||||
|
|
||||||
|
switch suffix := strings.ToLower(filepath.Ext(filename)); suffix {
|
||||||
|
case ".hcl":
|
||||||
|
file, diags = hclsyntax.ParseConfig(src, filename, hcl.Pos{Line: 1, Column: 1})
|
||||||
|
case ".json":
|
||||||
|
file, diags = json.Parse(src, filename)
|
||||||
|
default:
|
||||||
|
diags = diags.Append(&hcl.Diagnostic{
|
||||||
|
Severity: hcl.DiagError,
|
||||||
|
Summary: "Unsupported file format",
|
||||||
|
Detail: fmt.Sprintf("Cannot read from %s: unrecognized file format suffix %q.", filename, suffix),
|
||||||
|
})
|
||||||
|
return diags
|
||||||
|
}
|
||||||
|
if diags.HasErrors() {
|
||||||
|
return diags
|
||||||
|
}
|
||||||
|
|
||||||
|
diags = gohcl.DecodeBody(file.Body, ctx, target)
|
||||||
|
if diags.HasErrors() {
|
||||||
|
return diags
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecodeFile is a wrapper around Decode that first reads the given filename
|
||||||
|
// from disk. See the Decode documentation for more information.
|
||||||
|
func DecodeFile(filename string, ctx *hcl.EvalContext, target interface{}) error {
|
||||||
|
src, err := ioutil.ReadFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return hcl.Diagnostics{
|
||||||
|
{
|
||||||
|
Severity: hcl.DiagError,
|
||||||
|
Summary: "Configuration file not found",
|
||||||
|
Detail: fmt.Sprintf("The configuration file %s does not exist.", filename),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return hcl.Diagnostics{
|
||||||
|
{
|
||||||
|
Severity: hcl.DiagError,
|
||||||
|
Summary: "Failed to read configuration",
|
||||||
|
Detail: fmt.Sprintf("Can't read %s: %s.", filename, err),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Decode(filename, src, ctx, target)
|
||||||
|
}
|
82
hclsimple/hclsimple_test.go
Normal file
82
hclsimple/hclsimple_test.go
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
package hclsimple_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hashicorp/hcl/v2/hclsimple"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Example_nativeSyntax() {
|
||||||
|
type Config struct {
|
||||||
|
Foo string `hcl:"foo"`
|
||||||
|
Baz string `hcl:"baz"`
|
||||||
|
}
|
||||||
|
|
||||||
|
const exampleConfig = `
|
||||||
|
foo = "bar"
|
||||||
|
baz = "boop"
|
||||||
|
`
|
||||||
|
|
||||||
|
var config Config
|
||||||
|
err := hclsimple.Decode(
|
||||||
|
"example.hcl", []byte(exampleConfig),
|
||||||
|
nil, &config,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to load configuration: %s", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Configuration is %v\n", config)
|
||||||
|
|
||||||
|
// Output:
|
||||||
|
// Configuration is {bar boop}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Example_jsonSyntax() {
|
||||||
|
type Config struct {
|
||||||
|
Foo string `hcl:"foo"`
|
||||||
|
Baz string `hcl:"baz"`
|
||||||
|
}
|
||||||
|
|
||||||
|
const exampleConfig = `
|
||||||
|
{
|
||||||
|
"foo": "bar",
|
||||||
|
"baz": "boop"
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
var config Config
|
||||||
|
err := hclsimple.Decode(
|
||||||
|
"example.json", []byte(exampleConfig),
|
||||||
|
nil, &config,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to load configuration: %s", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Configuration is %v\n", config)
|
||||||
|
|
||||||
|
// Output:
|
||||||
|
// Configuration is {bar boop}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecodeFile(t *testing.T) {
|
||||||
|
type Config struct {
|
||||||
|
Foo string `hcl:"foo"`
|
||||||
|
Baz string `hcl:"baz"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var got Config
|
||||||
|
err := hclsimple.DecodeFile("testdata/test.hcl", nil, &got)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error(s): %s", err)
|
||||||
|
}
|
||||||
|
want := Config{
|
||||||
|
Foo: "bar",
|
||||||
|
Baz: "boop",
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(got, want) {
|
||||||
|
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, want)
|
||||||
|
}
|
||||||
|
}
|
2
hclsimple/testdata/test.hcl
vendored
Normal file
2
hclsimple/testdata/test.hcl
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
foo = "bar"
|
||||||
|
baz = "boop"
|
Loading…
Reference in New Issue
Block a user