feat: url based data loading system

This commit is contained in:
2022-05-05 16:22:52 +02:00
parent 2a7dc481b1
commit e05722cc2f
25 changed files with 774 additions and 33 deletions

View File

@ -0,0 +1,165 @@
package hcl
import (
"io"
"net/url"
"path"
"path/filepath"
"forge.cadoles.com/wpetit/formidable/internal/data/format"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/gohcl"
"github.com/hashicorp/hcl/v2/hclparse"
"github.com/pkg/errors"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/function"
"github.com/zclconf/go-cty/cty/gocty"
)
const (
ExtensionHCL = ".hcl"
FormatHCL = "hcl"
)
type DecoderHandler struct {
ctx *hcl.EvalContext
}
func (d *DecoderHandler) Match(url *url.URL) bool {
ext := filepath.Ext(path.Join(url.Host, url.Path))
return ext == ExtensionHCL ||
format.MatchURLQueryFormat(url, FormatHCL)
}
func (d *DecoderHandler) Decode(url *url.URL, reader io.Reader) (interface{}, error) {
data, err := io.ReadAll(reader)
if err != nil {
return nil, errors.WithStack(err)
}
ctx := d.ctx
if ctx == nil {
ctx = &hcl.EvalContext{
Variables: make(map[string]cty.Value),
Functions: make(map[string]function.Function),
}
}
parser := hclparse.NewParser()
file, diags := parser.ParseHCL(data, url.String())
if diags.HasErrors() {
return nil, errors.WithStack(diags)
}
var tree map[string]interface{}
diags = gohcl.DecodeBody(file.Body, ctx, &tree)
if diags.HasErrors() {
return nil, errors.WithStack(diags)
}
ctx = ctx.NewChild()
values, err := hclTreeToRawValues(ctx, tree)
if err != nil {
return nil, errors.WithStack(err)
}
return values, nil
}
func NewDecoderHandler(ctx *hcl.EvalContext) *DecoderHandler {
return &DecoderHandler{ctx}
}
func hclTreeToRawValues(ctx *hcl.EvalContext, tree map[string]interface{}) (map[string]interface{}, error) {
values := make(map[string]interface{})
for key, branch := range tree {
v, err := hclBranchToRawValue(ctx, branch)
if err != nil {
return nil, errors.WithStack(err)
}
values[key] = v
}
return values, nil
}
func hclBranchToRawValue(ctx *hcl.EvalContext, branch interface{}) (interface{}, error) {
switch typ := branch.(type) {
case *hcl.Attribute:
val, diags := typ.Expr.Value(ctx)
if diags.HasErrors() {
return nil, errors.WithStack(diags)
}
raw, err := ctyValueToRaw(ctx, val)
if err != nil {
return nil, errors.WithStack(err)
}
return raw, nil
default:
return nil, errors.Errorf("unexpected type '%T'", typ)
}
}
func ctyValueToRaw(ctx *hcl.EvalContext, val cty.Value) (interface{}, error) {
if val.Type().Equals(cty.Bool) {
var raw bool
if err := gocty.FromCtyValue(val, &raw); err != nil {
return nil, errors.WithStack(err)
}
return raw, nil
} else if val.Type().Equals(cty.Number) {
var raw float64
if err := gocty.FromCtyValue(val, &raw); err != nil {
return nil, errors.WithStack(err)
}
return raw, nil
} else if val.Type().Equals(cty.String) {
var raw string
if err := gocty.FromCtyValue(val, &raw); err != nil {
return nil, errors.WithStack(err)
}
return raw, nil
} else if val.Type().IsObjectType() {
obj := make(map[string]interface{})
for k, v := range val.AsValueMap() {
rv, err := ctyValueToRaw(ctx, v)
if err != nil {
return nil, errors.WithStack(err)
}
obj[k] = rv
}
return obj, nil
} else if val.Type().IsTupleType() {
sl := make([]interface{}, 0)
for _, v := range val.AsValueSlice() {
rv, err := ctyValueToRaw(ctx, v)
if err != nil {
return nil, errors.WithStack(err)
}
sl = append(sl, rv)
}
return sl, nil
}
return nil, errors.Errorf("unexpected cty.Type '%s'", val.Type().FriendlyName())
}

View File

@ -0,0 +1,78 @@
package hcl
import (
"fmt"
"net/url"
"os"
"path/filepath"
"testing"
"github.com/pkg/errors"
)
type parserHandlerTestCase struct {
Path string
ExpectMatch bool
ExpectParseError bool
}
var parserHandlerTestCases = []parserHandlerTestCase{
{
Path: "testdata/dummy.hcl",
ExpectMatch: true,
ExpectParseError: false,
},
{
Path: "file://testdata/dummy_no_ext?format=hcl",
ExpectMatch: true,
ExpectParseError: false,
},
}
func TestDecoderHandler(t *testing.T) {
t.Parallel()
handler := NewDecoderHandler(nil)
for _, tc := range parserHandlerTestCases {
func(tc parserHandlerTestCase) {
t.Run(fmt.Sprintf("Parse '%s'", tc.Path), func(t *testing.T) {
t.Parallel()
url, err := url.Parse(tc.Path)
if err != nil {
t.Fatal(errors.Wrapf(err, "could not parse url '%s'", tc.Path))
}
if e, g := tc.ExpectMatch, handler.Match(url); e != g {
t.Errorf("URL '%s': expected matching result '%v', got '%v'", url.String(), e, g)
}
if !tc.ExpectMatch {
return
}
cleanedPath := filepath.Join(url.Host, url.Path)
file, err := os.Open(cleanedPath)
if err != nil {
t.Fatal(errors.Wrapf(err, "could not open file '%s'", cleanedPath))
}
defer func() {
if err := file.Close(); err != nil {
t.Error(errors.Wrapf(err, "could not close file '%s'", cleanedPath))
}
}()
if _, err := handler.Decode(url, file); err != nil && !tc.ExpectParseError {
t.Fatal(errors.Wrapf(err, "could not parse file '%s'", tc.Path))
}
if tc.ExpectParseError {
t.Fatal(errors.Errorf("no error was returned as expected when opening url '%s'", url.String()))
}
})
}(tc)
}
}

View File

@ -0,0 +1,19 @@
test = 1
test1 = 2 + 1
foo = {
description = "Ça fait des trucs"
type = "object"
properties = {
type = "string"
minLength = 5
}
test = [
"foo",
{
test = "foo"
},
5 + 5.2
]
}

View File

@ -0,0 +1 @@
foo = "bar"