Skip to content

Commit 190aed7

Browse files
committed
feat: Added hcl parsing
1 parent b7e4d16 commit 190aed7

6 files changed

Lines changed: 744 additions & 16 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ For comprehensive configuration documentation, see [docs/configuration.md](docs/
2323
### Format
2424

2525
- File extension determines format: `.json` for JSON, `.hcl` for HCL.
26-
- For HCL, use equivalent field names (`listen_address`, etc).
26+
- For HCL, use equivalent field names (`listen-address`, etc).
2727

2828
## Building with Docker
2929

flake.nix

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
inherit version src;
3131
go = gopkgs;
3232
goVersion = goVersion;
33-
vendorHash = "sha256-K1QkmS9Yfl6aeZCTdceUedlhwwa+wDOD+WwLqmY7X+4=";
33+
vendorHash = "sha256-l9rR7251xMz0bKOTQ0ER9r+TWsTWKudaUgPjGimPCtw=";
3434
subPackages = [ "." ];
3535
CGO_ENABLED = 0;
3636
ldflags = [ "-X main.version=${version} -s -w" ];

go.mod

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,28 @@ go 1.24.1
55
require golang.org/x/net v0.38.0
66

77
require (
8+
github.com/agext/levenshtein v1.2.1
9+
github.com/apparentlymart/go-textseg/v15 v15.0.0
810
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
911
github.com/gorilla/websocket v1.5.3
12+
github.com/hashicorp/hcl/v2 v2.24.0
13+
github.com/mitchellh/go-wordwrap v1.0.1
1014
github.com/quic-go/quic-go v0.52.0
15+
github.com/zclconf/go-cty v1.16.3
16+
golang.org/x/crypto v0.38.0
17+
golang.org/x/sync v0.14.0
18+
golang.org/x/sys v0.33.0
19+
golang.org/x/text v0.25.0
20+
)
21+
22+
require (
23+
github.com/agext/levenshtein v1.2.1 // indirect
24+
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
25+
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
26+
golang.org/x/crypto v0.38.0 // indirect
27+
golang.org/x/sync v0.14.0 // indirect
28+
golang.org/x/sys v0.33.0 // indirect
29+
golang.org/x/text v0.25.0 // indirect
1130
)
1231

1332
require (
@@ -17,11 +36,7 @@ require (
1736
github.com/onsi/ginkgo/v2 v2.9.5 // indirect
1837
github.com/quic-go/qpack v0.5.1 // indirect
1938
go.uber.org/mock v0.5.0 // indirect
20-
golang.org/x/crypto v0.36.0 // indirect
2139
golang.org/x/mod v0.18.0 // indirect
22-
golang.org/x/sync v0.12.0 // indirect
23-
golang.org/x/sys v0.31.0 // indirect
24-
golang.org/x/text v0.23.0 // indirect
2540
golang.org/x/tools v0.22.0 // indirect
2641
)
2742

go.sum

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
github.com/BobuSumisu/aho-corasick v1.0.3 h1:uuf+JHwU9CHP2Vx+wAy6jcksJThhJS9ehR8a+4nPE9g=
22
github.com/BobuSumisu/aho-corasick v1.0.3/go.mod h1:hm4jLcvZKI2vRF2WDU1N4p/jpWtpOzp3nLmi9AzX/XE=
3+
github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8=
4+
github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
5+
github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY=
6+
github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4=
37
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
48
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
59
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
@@ -12,6 +16,8 @@ github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
1216
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
1317
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
1418
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
19+
github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
20+
github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
1521
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
1622
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
1723
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
@@ -20,7 +26,11 @@ github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE
2026
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
2127
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
2228
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
29+
github.com/hashicorp/hcl/v2 v2.24.0 h1:2QJdZ454DSsYGoaE6QheQZjtKZSUs9Nh2izTWiwQxvE=
30+
github.com/hashicorp/hcl/v2 v2.24.0/go.mod h1:oGoO1FIQYfn/AgyOhlg9qLC6/nOJPX3qGbkZpYAcqfM=
2331
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
32+
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
33+
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
2434
github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q=
2535
github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k=
2636
github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
@@ -35,21 +45,25 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
3545
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
3646
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
3747
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
48+
github.com/zclconf/go-cty v1.16.3 h1:osr++gw2T61A8KVYHoQiFbFd1Lh3JOCXc/jFLJXKTxk=
49+
github.com/zclconf/go-cty v1.16.3/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE=
50+
github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo=
51+
github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM=
3852
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
3953
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
40-
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
41-
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
54+
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
55+
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
4256
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
4357
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
4458
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
4559
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
46-
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
47-
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
60+
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
61+
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
4862
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
49-
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
50-
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
51-
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
52-
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
63+
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
64+
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
65+
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
66+
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
5367
golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
5468
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
5569
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=

msgtausch-srv/config/config.go

Lines changed: 137 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ import (
1010
"strings"
1111

1212
"github.com/codefionn/msgtausch/msgtausch-srv/logger"
13+
"github.com/hashicorp/hcl/v2"
14+
"github.com/hashicorp/hcl/v2/hclparse"
15+
"github.com/zclconf/go-cty/cty"
16+
"github.com/zclconf/go-cty/cty/gocty"
1317
)
1418

1519
// ProxyType defines the type of proxy server
@@ -166,6 +170,8 @@ func LoadConfig(configPath string) (*Config, error) {
166170
switch strings.ToLower(ext) {
167171
case ".json":
168172
err = loadJSONConfig(configPath, cfg)
173+
case ".hcl":
174+
err = loadHCLConfig(configPath, cfg)
169175
default:
170176
return nil, fmt.Errorf("unsupported config file format: %s", ext)
171177
}
@@ -305,7 +311,111 @@ func loadJSONConfig(configPath string, cfg *Config) error {
305311
return err
306312
}
307313

308-
// Manually map the values from the map to the Config struct
314+
// Use the same parsing logic as HCL by manually mapping values from the map to Config struct
315+
return parseConfigData(data, cfg)
316+
}
317+
318+
func loadHCLConfig(configPath string, cfg *Config) error {
319+
cleanPath := filepath.Clean(configPath)
320+
if !filepath.IsAbs(cleanPath) {
321+
absPath, err := filepath.Abs(cleanPath)
322+
if err != nil {
323+
return fmt.Errorf("invalid config file path: %w", err)
324+
}
325+
cleanPath = absPath
326+
}
327+
328+
// Parse HCL file
329+
parser := hclparse.NewParser()
330+
file, diags := parser.ParseHCLFile(cleanPath)
331+
if diags.HasErrors() {
332+
return fmt.Errorf("failed to parse HCL config: %s", diags.Error())
333+
}
334+
335+
// Create evaluation context
336+
evalCtx := &hcl.EvalContext{}
337+
338+
// Get all attributes (we'll accept any attributes dynamically)
339+
attrs, diags := file.Body.JustAttributes()
340+
if diags.HasErrors() {
341+
return fmt.Errorf("failed to decode HCL config: %s", diags.Error())
342+
}
343+
344+
// Convert HCL attributes to a map[string]any
345+
data := make(map[string]any)
346+
for name, attr := range attrs {
347+
val, diags := attr.Expr.Value(evalCtx)
348+
if diags.HasErrors() {
349+
return fmt.Errorf("failed to evaluate HCL attribute %s: %s", name, diags.Error())
350+
}
351+
352+
// Convert cty.Value to Go types
353+
goVal, err := convertCtyValueToGo(val)
354+
if err != nil {
355+
return fmt.Errorf("failed to convert HCL value for %s: %w", name, err)
356+
}
357+
data[name] = goVal
358+
}
359+
360+
// Validate config keys and provide helpful error messages for underscore usage
361+
if err := validateConfigKeys(data); err != nil {
362+
return err
363+
}
364+
365+
// Use the same parsing logic as JSON by manually mapping values from the map to Config struct
366+
return parseConfigData(data, cfg)
367+
}
368+
369+
// convertCtyValueToGo converts a cty.Value to a Go value (map[string]any, []any, etc.)
370+
func convertCtyValueToGo(val cty.Value) (any, error) {
371+
if val.IsNull() {
372+
return nil, nil
373+
}
374+
375+
switch {
376+
case val.Type() == cty.String:
377+
return val.AsString(), nil
378+
case val.Type() == cty.Number:
379+
f, _ := val.AsBigFloat().Float64()
380+
// Try to convert to int if it's a whole number
381+
if f == float64(int64(f)) {
382+
return int64(f), nil
383+
}
384+
return f, nil
385+
case val.Type() == cty.Bool:
386+
return val.True(), nil
387+
case val.Type().IsListType() || val.Type().IsTupleType():
388+
var result []any
389+
for it := val.ElementIterator(); it.Next(); {
390+
_, elem := it.Element()
391+
converted, err := convertCtyValueToGo(elem)
392+
if err != nil {
393+
return nil, err
394+
}
395+
result = append(result, converted)
396+
}
397+
return result, nil
398+
case val.Type().IsMapType() || val.Type().IsObjectType():
399+
result := make(map[string]any)
400+
for it := val.ElementIterator(); it.Next(); {
401+
key, elem := it.Element()
402+
keyStr := key.AsString()
403+
converted, err := convertCtyValueToGo(elem)
404+
if err != nil {
405+
return nil, err
406+
}
407+
result[keyStr] = converted
408+
}
409+
return result, nil
410+
default:
411+
// Fallback: try to convert using gocty
412+
var result any
413+
err := gocty.FromCtyValue(val, &result)
414+
return result, err
415+
}
416+
}
417+
418+
func parseConfigData(data map[string]any, cfg *Config) error {
309419
// Handle servers configuration
310420
if val, exists := data["servers"]; exists {
311421
serverList, ok := val.([]any)
@@ -590,6 +700,26 @@ func parseValue[T any](value any) (*T, error) {
590700
default:
591701
return nil, fmt.Errorf("expected %T, got JSON number", zero)
592702
}
703+
case int64:
704+
// HCL number (integer)
705+
switch elem.Kind() {
706+
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
707+
elem.SetInt(v)
708+
case reflect.Float32, reflect.Float64:
709+
elem.SetFloat(float64(v))
710+
default:
711+
return nil, fmt.Errorf("expected %T, got int64", zero)
712+
}
713+
case int:
714+
// Go number
715+
switch elem.Kind() {
716+
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
717+
elem.SetInt(int64(v))
718+
case reflect.Float32, reflect.Float64:
719+
elem.SetFloat(float64(v))
720+
default:
721+
return nil, fmt.Errorf("expected %T, got int", zero)
722+
}
593723
case string:
594724
switch elem.Kind() {
595725
case reflect.String:
@@ -705,8 +835,13 @@ func parseClassifier(classifierMap map[string]any) (Classifier, error) {
705835
newClassifier = networkClassifier
706836
case "port":
707837
portClassifier := &ClassifierPort{}
708-
if port, ok := classifierMap["port"].(float64); ok {
838+
switch port := classifierMap["port"].(type) {
839+
case float64:
840+
portClassifier.Port = int(port)
841+
case int64:
709842
portClassifier.Port = int(port)
843+
case int:
844+
portClassifier.Port = port
710845
}
711846
newClassifier = portClassifier
712847
case "ref":

0 commit comments

Comments
 (0)