Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: Expressions anywhere in the configuration #930

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
9 changes: 8 additions & 1 deletion internal/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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")
}
}
Expand Down
130 changes: 130 additions & 0 deletions internal/app/app_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}
36 changes: 36 additions & 0 deletions pkg/expr/expr.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package expr

import (
"bytes"
"encoding/json"
"fmt"
"html/template"
"io"
"net/http"
"regexp"
Expand Down Expand Up @@ -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
}