diff --git a/internal/app/app.go b/internal/app/app.go index b27437db..208aad77 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -9,6 +9,7 @@ import ( "runtime" "strings" + "github.com/AlexxIT/go2rtc/pkg/expr" "github.com/AlexxIT/go2rtc/pkg/shell" "github.com/AlexxIT/go2rtc/pkg/yaml" "github.com/rs/zerolog/log" @@ -85,7 +86,13 @@ func Init() { func LoadConfig(v any) { for _, data := range configs { - if err := yaml.Unmarshal(data, v); err != nil { + processedData, err := expr.ProcessConfig(data) + if err != nil { + log.Warn().Err(err).Msg("[app] process config") + continue + } + + if err := yaml.Unmarshal(processedData, v); err != nil { log.Warn().Err(err).Msg("[app] read config") } } diff --git a/internal/app/app_test.go b/internal/app/app_test.go new file mode 100644 index 00000000..86822e93 --- /dev/null +++ b/internal/app/app_test.go @@ -0,0 +1,130 @@ +package app + +import ( + "bytes" + "github.com/AlexxIT/go2rtc/pkg/expr" + "log" + "os" + "reflect" + "testing" +) + +// TestLoadConfig tests the LoadConfig function. +func TestLoadConfig(t *testing.T) { + // Redirect log output to buffer for testing. + var buf bytes.Buffer + log.SetOutput(&buf) + defer func() { + log.SetOutput(os.Stderr) + }() + + type Config struct { + Key1 string `yaml:"key1"` + Key2 string `yaml:"key2"` + Key3 string `yaml:"key3"` + } + + tests := []struct { + name string + configs [][]byte + want Config + expectError bool + }{ + { + name: "Valid configs", + configs: [][]byte{ + []byte("key1: value1\nkey2: value2"), + []byte("key3: value3"), + }, + want: Config{ + Key1: "value1", + Key2: "value2", + Key3: "value3", + }, + expectError: false, + }, + { + name: "Invalid config", + configs: [][]byte{ + []byte("key1: value1"), + []byte("invalid_yaml"), + }, + want: Config{ + Key1: "value1", + }, + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Mock the configs with the test case's configs. + configs = tt.configs + + var got Config + LoadConfig(&got) + + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("%s: LoadConfig() got = %v, want %v", tt.name, got, tt.want) + } + + containsError := bytes.Contains(buf.Bytes(), []byte("read config")) + if containsError != tt.expectError { + t.Errorf("%s: LoadConfig() expected error = %v, but got = %v", tt.name, tt.expectError, containsError) + } + + // Clear buffer after each test case. + buf.Reset() + }) + } +} + +// Test_processConfig tests the processConfig function. +func Test_processConfig(t *testing.T) { + type args struct { + data []byte + } + tests := []struct { + name string + args args + want []byte + wantErr bool + }{ + { + name: "No expressions", + args: args{data: []byte("config: value")}, + want: []byte("config: value"), + wantErr: false, + }, + { + name: "Simple expression", + args: args{data: []byte(`config: ${{ "value" }}`)}, + want: []byte("config: value"), + wantErr: false, + }, + { + name: "Math expression", + args: args{data: []byte(`config: ${{ 2+2 }}`)}, + want: []byte("config: 4"), + wantErr: false, + }, + { + name: "Invalid expression", + args: args{data: []byte(`config: ${{ invalid }}`)}, + want: []byte(`config: ${{ invalid }}`), + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := expr.ProcessConfig(tt.args.data) + if (err != nil) != tt.wantErr { + t.Errorf("%s: processConfig() error = %v, wantErr %v", tt.name, err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("%s: processConfig() got = %v, want %v", tt.name, got, tt.want) + } + }) + } +} diff --git a/pkg/expr/expr.go b/pkg/expr/expr.go index 36719100..583082d7 100644 --- a/pkg/expr/expr.go +++ b/pkg/expr/expr.go @@ -1,8 +1,10 @@ package expr import ( + "bytes" "encoding/json" "fmt" + "html/template" "io" "net/http" "regexp" @@ -113,3 +115,37 @@ func Run(input string) (any, error) { return expr.Run(program, nil) } + +func ProcessConfig(data []byte) ([]byte, error) { + r := regexp.MustCompile(`\${{(.+?)}}`) + return r.ReplaceAllFunc(data, func(match []byte) []byte { + exprStr := match[3 : len(match)-2] // Extract the expression without `${{` and `}}`. + result, err := evalExpr(string(exprStr)) + if err != nil { + // log.Warn().Err(err).Msg("[app] eval expression") + return match + } + + return []byte(result) + }), nil +} + +func evalExpr(expression string) (string, error) { + result, err := expr.Eval(expression, nil) + if err != nil { + return "", err + } + + // Use a template to ensure proper conversion to string. + var tpl bytes.Buffer + tmpl, err := template.New("expr").Parse("{{ . }}") + if err != nil { + return "", err + } + err = tmpl.Execute(&tpl, result) + if err != nil { + return "", err + } + + return tpl.String(), nil +}