@@ -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