mirror of
https://github.com/Bornholm/formidable.git
synced 2025-07-21 13:01:33 +02:00
feat: url based data loading system
This commit is contained in:
165
internal/data/format/hcl/decoder_handler.go
Normal file
165
internal/data/format/hcl/decoder_handler.go
Normal 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())
|
||||
}
|
78
internal/data/format/hcl/decoder_handler_test.go
Normal file
78
internal/data/format/hcl/decoder_handler_test.go
Normal 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)
|
||||
}
|
||||
}
|
19
internal/data/format/hcl/testdata/dummy.hcl
vendored
Normal file
19
internal/data/format/hcl/testdata/dummy.hcl
vendored
Normal 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
|
||||
]
|
||||
}
|
1
internal/data/format/hcl/testdata/dummy_no_ext
vendored
Normal file
1
internal/data/format/hcl/testdata/dummy_no_ext
vendored
Normal file
@ -0,0 +1 @@
|
||||
foo = "bar"
|
Reference in New Issue
Block a user