Templates: support class-local include files (#32871)
This commit is contained in:
parent
9034f45355
commit
de3f8995cd
116 changed files with 285 additions and 212 deletions
|
|
@ -31,7 +31,7 @@ func TestRequiredLargeNumber(t *testing.T) {
|
|||
Params: []Param{{Name: "serial", Type: TypeInt, Required: true}},
|
||||
}
|
||||
|
||||
_, _, err := tmpl.RenderResult(RenderModeUnitTest, map[string]any{
|
||||
_, _, err := tmpl.RenderResult(Meter, RenderModeUnitTest, map[string]any{
|
||||
"serial": float64(3493601102),
|
||||
})
|
||||
assert.NoError(t, err, "large serial supplied as JSON number")
|
||||
|
|
|
|||
|
|
@ -1,19 +0,0 @@
|
|||
{{ define "battery-capacity" }}
|
||||
capacity: {{ .capacity }} # kWh
|
||||
{{- end }}
|
||||
|
||||
{{ define "battery-minmaxsoc" }}
|
||||
minsoc: {{ .minsoc }} # %
|
||||
maxsoc: {{ .maxsoc }} # %
|
||||
{{- end }}
|
||||
|
||||
{{ define "battery-power" }}
|
||||
maxchargepower: {{ .maxchargepower }} # W
|
||||
maxdischargepower: {{ .maxdischargepower }} # W
|
||||
{{- end }}
|
||||
|
||||
{{ define "battery-params" }}
|
||||
{{- include "battery-capacity" . }}
|
||||
{{- include "battery-minmaxsoc" . }}
|
||||
{{- include "battery-power" . }}
|
||||
{{- end }}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
{{ define "charger-features" }}
|
||||
{{- if or (eq .heating "true") (eq .integrateddevice "true") }}
|
||||
features:
|
||||
{{- if eq .heating "true" }}
|
||||
- heating
|
||||
{{- end }}
|
||||
{{- if eq .integrateddevice "true" }}
|
||||
- integrateddevice
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
{{ define "heatpumpswitch" }}
|
||||
features:
|
||||
- continuous
|
||||
- heating
|
||||
- integrateddevice
|
||||
- switchdevice
|
||||
{{- end }}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
{{ define "ocpp" }}
|
||||
type: ocpp
|
||||
{{- if .stationid }}
|
||||
stationid: {{ .stationid }}
|
||||
{{- end }}
|
||||
{{- if ne .connector "1" }}
|
||||
connector: {{ .connector }}
|
||||
{{- end }}
|
||||
{{- if .idtag }}
|
||||
idtag: {{ .idtag }}
|
||||
{{- end }}
|
||||
{{- if and .remotestart (ne .remotestart "false") }}
|
||||
remotestart: {{ .remotestart }}
|
||||
{{- end }}
|
||||
{{- if .metervalues }}
|
||||
metervalues: {{ .metervalues }}
|
||||
{{- end }}
|
||||
{{- if and .meterinterval (ne .meterinterval "10s") }}
|
||||
meterinterval: {{ .meterinterval }}
|
||||
{{- end }}
|
||||
{{- if ne .connecttimeout "5m" }}
|
||||
connecttimeout: {{ .connecttimeout }}
|
||||
{{- end }}
|
||||
{{- if and .timeout (ne .timeout "30s") }}
|
||||
timeout: {{ .timeout }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
{{ define "switchsocket" }}
|
||||
standbypower: {{ .standbypower }}
|
||||
features:
|
||||
- switchdevice
|
||||
{{- if and .integrateddevice (ne .integrateddevice "false") }}
|
||||
- integrateddevice
|
||||
{{- end }}
|
||||
{{- if and .heating (ne .heating "false") }}
|
||||
- heating
|
||||
{{- end }}
|
||||
{{- if .icon }}
|
||||
icon: {{ .icon }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
{{ define "tariff-base" }}
|
||||
{{- if .charges }}
|
||||
charges: {{ .charges }}
|
||||
{{- end }}
|
||||
{{- if .chargesZones }}
|
||||
chargesZones:
|
||||
{{- range .chargesZones }}
|
||||
- charges: {{ .price }}
|
||||
{{- if .days }}
|
||||
days: {{ .days }}
|
||||
{{- end }}
|
||||
{{- if .hours }}
|
||||
hours: {{ .hours }}
|
||||
{{- end }}
|
||||
{{- if .months }}
|
||||
months: {{ .months }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if .tax }}
|
||||
tax: {{ .tax }}
|
||||
{{- end }}
|
||||
{{- if .formula }}
|
||||
formula: {{ .formula }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
{{ define "tariff-features" }}
|
||||
{{- if eq .average "true" }}
|
||||
features: ["average"]
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
{{ define "vehicle-base" }}
|
||||
user: {{ .user }}
|
||||
password: {{ .password }}
|
||||
vin: {{ .vin }}
|
||||
{{ template "vehicle-common" . }}
|
||||
{{- if .cache }}
|
||||
cache: {{ .cache }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
{{ define "vehicle-common" }}
|
||||
{{- if .title }}
|
||||
title: {{ .title }}
|
||||
{{- end }}
|
||||
{{- if .icon }}
|
||||
icon: {{ .icon }}
|
||||
{{- end }}
|
||||
{{- if .capacity }}
|
||||
capacity: {{ .capacity }}
|
||||
{{- end }}
|
||||
{{- if .phases }}
|
||||
phases: {{ .phases }}
|
||||
{{- end }}
|
||||
|
||||
{{- if or .mode .minCurrent .maxCurrent .maxPower .priority }}
|
||||
onIdentify:
|
||||
{{- if .mode }}
|
||||
mode: {{ .mode }}
|
||||
{{- end }}
|
||||
{{- if .minCurrent }}
|
||||
minCurrent: {{ .minCurrent }}
|
||||
{{- end }}
|
||||
{{- if .maxCurrent }}
|
||||
maxCurrent: {{ .maxCurrent }}
|
||||
{{- end }}
|
||||
{{- if .maxPower }}
|
||||
maxPower: {{ .maxPower }}
|
||||
{{- end }}
|
||||
{{- if .priority }}
|
||||
priority: {{ .priority }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- if len .identifiers }}
|
||||
identifiers:
|
||||
{{- range .identifiers }}
|
||||
- {{ quote . }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- end }}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
{{ define "vehicle-features" }}
|
||||
{{- if or .basefeatures (eq .coarsecurrent "true") (eq .welcomecharge "true") (eq .streaming "true") (eq .climaterdisabled "true") (eq .autodetectdisabled "true") (eq .wakeupdisabled "true") }}
|
||||
features:
|
||||
{{- range .basefeatures }}
|
||||
- {{ . }}
|
||||
{{- end }}
|
||||
{{- if eq .coarsecurrent "true" }}
|
||||
- coarsecurrent
|
||||
{{- end }}
|
||||
{{- if eq .welcomecharge "true" }}
|
||||
- welcomecharge
|
||||
{{- end }}
|
||||
{{- if eq .streaming "true" }}
|
||||
- streaming
|
||||
{{- end }}
|
||||
{{- if eq .climaterdisabled "true" }}
|
||||
- climaterdisabled
|
||||
{{- end }}
|
||||
{{- if eq .autodetectdisabled "true" }}
|
||||
- autodetectdisabled
|
||||
{{- end }}
|
||||
{{- if eq .wakeupdisabled "true" }}
|
||||
- wakeupdisabled
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
{{ define "vehicle-language" }}
|
||||
language: {{ .language }}
|
||||
{{- end }}
|
||||
|
|
@ -15,6 +15,9 @@ import (
|
|||
"go.yaml.in/yaml/v4"
|
||||
)
|
||||
|
||||
// IncludeExt is the file extension of class-local, include-only templates
|
||||
const IncludeExt = ".tpl"
|
||||
|
||||
var (
|
||||
//go:embed includes/*.tpl
|
||||
includeFS embed.FS
|
||||
|
|
@ -22,6 +25,9 @@ var (
|
|||
// baseTmpl holds all included template definitions
|
||||
baseTmpl *template.Template
|
||||
|
||||
// classTmpl holds baseTmpl plus the class-local include definitions
|
||||
classTmpl = make(map[Class]*template.Template)
|
||||
|
||||
templates = make(map[Class][]Template)
|
||||
ConfigDefaults configDefaults
|
||||
mu sync.Mutex
|
||||
|
|
@ -31,13 +37,35 @@ var (
|
|||
func init() {
|
||||
ConfigDefaults.Load()
|
||||
|
||||
baseTmpl = template.Must(FuncMap(template.New("base")).ParseFS(includeFS, "includes/*.tpl"))
|
||||
// the root template is replaced by the rendered template's body, so its name
|
||||
// must not collide with an include name (`$` cannot appear in a file name)
|
||||
baseTmpl = template.Must(FuncMap(template.New("$root")).ParseFS(includeFS, "includes/*.tpl"))
|
||||
|
||||
for _, class := range []Class{Charger, Meter, Vehicle, Tariff, Messenger, Circuit, Hems} {
|
||||
if err := loadIncludes(definition.TemplateIncludes, class); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
load(class)
|
||||
}
|
||||
}
|
||||
|
||||
// loadIncludes parses the class-local include-only templates
|
||||
func loadIncludes(fsys fs.FS, class Class) error {
|
||||
files, err := fs.Glob(fsys, class.String()+"/*"+IncludeExt)
|
||||
if err != nil || len(files) == 0 {
|
||||
return err
|
||||
}
|
||||
|
||||
tmpl, err := FuncMap(template.Must(baseTmpl.Clone())).ParseFS(fsys, files...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("processing %s includes failed: %w", class, err)
|
||||
}
|
||||
|
||||
classTmpl[class] = tmpl
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Register adds a template file to the registry
|
||||
func Register(class Class, filepath string) error {
|
||||
b, err := os.ReadFile(filepath)
|
||||
|
|
|
|||
36
util/templates/init_test.go
Normal file
36
util/templates/init_test.go
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
package templates
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestClassLocalIncludes(t *testing.T) {
|
||||
fsys := fstest.MapFS{
|
||||
"loadpoint/local" + IncludeExt: &fstest.MapFile{
|
||||
Data: []byte(`{{ define "local" }}power: {{ .foo }}{{ end }}`),
|
||||
},
|
||||
}
|
||||
|
||||
require.NoError(t, loadIncludes(fsys, Loadpoint))
|
||||
require.Contains(t, classTmpl, Loadpoint)
|
||||
|
||||
// class without include files
|
||||
require.NoError(t, loadIncludes(fsys, Circuit))
|
||||
require.NotContains(t, classTmpl, Circuit)
|
||||
|
||||
tmpl := Template{
|
||||
Params: []Param{{Name: "foo", Default: "bar"}},
|
||||
Render: "type: custom\n{{ include \"local\" . }}",
|
||||
}
|
||||
|
||||
b, _, err := tmpl.RenderResult(Loadpoint, RenderModeInstance, map[string]any{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "type: custom\npower: bar", string(b))
|
||||
|
||||
// class-local includes are not visible to other classes
|
||||
_, _, err = tmpl.RenderResult(Circuit, RenderModeInstance, map[string]any{})
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
|
@ -31,7 +31,7 @@ func RenderInstance(class Class, other map[string]any) (*Instance, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
b, _, err := tmpl.RenderResult(RenderModeInstance, other)
|
||||
b, _, err := tmpl.RenderResult(class, RenderModeInstance, other)
|
||||
if err != nil {
|
||||
return nil, util.NewConfigError(err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,10 +13,10 @@ import (
|
|||
)
|
||||
|
||||
// test renders and instantiates plus yaml-parses the template per usage
|
||||
func test(t *testing.T, tmpl Template, values map[string]any, cb func(values map[string]any)) {
|
||||
func test(t *testing.T, class Class, tmpl Template, values map[string]any, cb func(values map[string]any)) {
|
||||
t.Helper()
|
||||
|
||||
b, _, err := tmpl.RenderResult(RenderModeInstance, values)
|
||||
b, _, err := tmpl.RenderResult(class, RenderModeInstance, values)
|
||||
if err != nil {
|
||||
t.Log(string(b))
|
||||
t.Error(err)
|
||||
|
|
@ -108,7 +108,7 @@ func TestClass(t *testing.T, class Class, instantiate func(t *testing.T, values
|
|||
t.Run(tmpl.Template, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
test(t, tmpl, values, func(values map[string]any) {
|
||||
test(t, class, tmpl, values, func(values map[string]any) {
|
||||
instantiate(t, values)
|
||||
})
|
||||
})
|
||||
|
|
@ -125,7 +125,7 @@ func TestClass(t *testing.T, class Class, instantiate func(t *testing.T, values
|
|||
t.Run(tmpl.Template+"/"+u, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
test(t, tmpl, usageValues, func(values map[string]any) {
|
||||
test(t, class, tmpl, usageValues, func(values map[string]any) {
|
||||
instantiate(t, values)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -325,7 +325,7 @@ func formatValue(val any) string {
|
|||
}
|
||||
|
||||
// RenderResult renders the result template to instantiate the proxy
|
||||
func (t *Template) RenderResult(renderMode int, other map[string]any) ([]byte, map[string]any, error) {
|
||||
func (t *Template) RenderResult(class Class, renderMode int, other map[string]any) ([]byte, map[string]any, error) {
|
||||
values := t.Defaults(renderMode)
|
||||
if err := mergeMaps(other, values); err != nil {
|
||||
return nil, values, err
|
||||
|
|
@ -422,7 +422,13 @@ func (t *Template) RenderResult(renderMode int, other map[string]any) ([]byte, m
|
|||
}
|
||||
}
|
||||
|
||||
tmpl, err := FuncMap(template.Must(baseTmpl.Clone())).Parse(t.Render)
|
||||
// class-local includes take precedence over the global ones
|
||||
base, ok := classTmpl[class]
|
||||
if !ok {
|
||||
base = baseTmpl
|
||||
}
|
||||
|
||||
tmpl, err := FuncMap(template.Must(base.Clone())).Parse(t.Render)
|
||||
if err != nil {
|
||||
return nil, res, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ func TestModbusTemplateDefaultID(t *testing.T) {
|
|||
tmpl, err := ByName(Charger, "phoenix-ev-eth")
|
||||
require.NoError(t, err)
|
||||
|
||||
_, values, err := tmpl.RenderResult(mode, map[string]any{
|
||||
_, values, err := tmpl.RenderResult(Meter, mode, map[string]any{
|
||||
"host": "192.168.0.8",
|
||||
"port": 502,
|
||||
})
|
||||
|
|
@ -40,7 +40,7 @@ func TestModbusTemplateUserIDOverridesTemplate(t *testing.T) {
|
|||
tmpl, err := ByName(Charger, "phoenix-ev-eth")
|
||||
require.NoError(t, err)
|
||||
|
||||
_, values, err := tmpl.RenderResult(mode, map[string]any{
|
||||
_, values, err := tmpl.RenderResult(Meter, mode, map[string]any{
|
||||
"host": "192.168.0.8",
|
||||
"port": 502,
|
||||
"id": 42,
|
||||
|
|
@ -62,7 +62,7 @@ func TestWallbeTemplateCoveredByPhoenix(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
assert.Equal(t, "phoenix-ev-eth", tmpl.Template)
|
||||
|
||||
_, values, err := tmpl.RenderResult(RenderModeInstance, map[string]any{
|
||||
_, values, err := tmpl.RenderResult(Meter, RenderModeInstance, map[string]any{
|
||||
"host": "192.168.0.8",
|
||||
"port": 502,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -42,22 +42,22 @@ func TestRequiredString(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
_, _, err := tmpl.RenderResult(RenderModeUnitTest, map[string]any{
|
||||
_, _, err := tmpl.RenderResult(Meter, RenderModeUnitTest, map[string]any{
|
||||
"Param": "foo",
|
||||
})
|
||||
assert.NoError(t, err, "test: required present")
|
||||
|
||||
_, _, err = tmpl.RenderResult(RenderModeUnitTest, map[string]any{
|
||||
_, _, err = tmpl.RenderResult(Meter, RenderModeUnitTest, map[string]any{
|
||||
"Param": "",
|
||||
})
|
||||
assert.Error(t, err, "test: required present but empty")
|
||||
|
||||
_, _, err = tmpl.RenderResult(RenderModeUnitTest, map[string]any{
|
||||
_, _, err = tmpl.RenderResult(Meter, RenderModeUnitTest, map[string]any{
|
||||
"Param": nil,
|
||||
})
|
||||
assert.Error(t, err, "test: required present but nil")
|
||||
|
||||
_, _, err = tmpl.RenderResult(RenderModeDocs, map[string]any{
|
||||
_, _, err = tmpl.RenderResult(Meter, RenderModeDocs, map[string]any{
|
||||
"Param": nil,
|
||||
})
|
||||
assert.NoError(t, err, "docs: required present but nil")
|
||||
|
|
@ -74,27 +74,27 @@ func TestRequiredNumber(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
_, _, err := tmpl.RenderResult(RenderModeUnitTest, map[string]any{
|
||||
_, _, err := tmpl.RenderResult(Meter, RenderModeUnitTest, map[string]any{
|
||||
"Param": "1",
|
||||
})
|
||||
assert.NoError(t, err, "test: required present")
|
||||
|
||||
_, _, err = tmpl.RenderResult(RenderModeUnitTest, map[string]any{
|
||||
_, _, err = tmpl.RenderResult(Meter, RenderModeUnitTest, map[string]any{
|
||||
"Param": "",
|
||||
})
|
||||
assert.Error(t, err, "test: required present but empty")
|
||||
|
||||
_, _, err = tmpl.RenderResult(RenderModeUnitTest, map[string]any{
|
||||
_, _, err = tmpl.RenderResult(Meter, RenderModeUnitTest, map[string]any{
|
||||
"Param": "0",
|
||||
})
|
||||
assert.Error(t, err, "test: required present but zero value")
|
||||
|
||||
_, _, err = tmpl.RenderResult(RenderModeUnitTest, map[string]any{
|
||||
_, _, err = tmpl.RenderResult(Meter, RenderModeUnitTest, map[string]any{
|
||||
"Param": nil,
|
||||
})
|
||||
assert.Error(t, err, "test: required present but nil")
|
||||
|
||||
_, _, err = tmpl.RenderResult(RenderModeDocs, map[string]any{
|
||||
_, _, err = tmpl.RenderResult(Meter, RenderModeDocs, map[string]any{
|
||||
"Param": nil,
|
||||
})
|
||||
assert.NoError(t, err, "docs: required present but nil")
|
||||
|
|
@ -111,22 +111,22 @@ func TestRequiredDeprecated(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
_, _, err := tmpl.RenderResult(RenderModeUnitTest, map[string]any{
|
||||
_, _, err := tmpl.RenderResult(Meter, RenderModeUnitTest, map[string]any{
|
||||
"Param": "foo",
|
||||
})
|
||||
assert.NoError(t, err, "test: required present")
|
||||
|
||||
_, _, err = tmpl.RenderResult(RenderModeUnitTest, map[string]any{
|
||||
_, _, err = tmpl.RenderResult(Meter, RenderModeUnitTest, map[string]any{
|
||||
"Param": "",
|
||||
})
|
||||
assert.NoError(t, err, "test: required present but empty")
|
||||
|
||||
_, _, err = tmpl.RenderResult(RenderModeUnitTest, map[string]any{
|
||||
_, _, err = tmpl.RenderResult(Meter, RenderModeUnitTest, map[string]any{
|
||||
"Param": nil,
|
||||
})
|
||||
assert.NoError(t, err, "test: required present but nil")
|
||||
|
||||
_, _, err = tmpl.RenderResult(RenderModeDocs, map[string]any{
|
||||
_, _, err = tmpl.RenderResult(Meter, RenderModeDocs, map[string]any{
|
||||
"Param": nil,
|
||||
})
|
||||
assert.NoError(t, err, "docs: required present but nil")
|
||||
|
|
@ -146,25 +146,25 @@ func TestRequiredPerUsage(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
_, _, err := tmpl.RenderResult(RenderModeUnitTest, map[string]any{
|
||||
_, _, err := tmpl.RenderResult(Meter, RenderModeUnitTest, map[string]any{
|
||||
"Param": nil,
|
||||
"Usage": nil,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, _, err = tmpl.RenderResult(RenderModeUnitTest, map[string]any{
|
||||
_, _, err = tmpl.RenderResult(Meter, RenderModeUnitTest, map[string]any{
|
||||
"Param": nil,
|
||||
"Usage": "pv",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, _, err = tmpl.RenderResult(RenderModeUnitTest, map[string]any{
|
||||
_, _, err = tmpl.RenderResult(Meter, RenderModeUnitTest, map[string]any{
|
||||
"Param": nil,
|
||||
"Usage": "battery",
|
||||
})
|
||||
require.Error(t, err)
|
||||
|
||||
_, _, err = tmpl.RenderResult(RenderModeUnitTest, map[string]any{
|
||||
_, _, err = tmpl.RenderResult(Meter, RenderModeUnitTest, map[string]any{
|
||||
"Param": "foo",
|
||||
"Usage": "battery",
|
||||
})
|
||||
|
|
@ -190,7 +190,7 @@ func TestValidatePattern(t *testing.T) {
|
|||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.host, func(t *testing.T) {
|
||||
_, _, err := tmpl.RenderResult(RenderModeInstance, map[string]any{"host": tt.host})
|
||||
_, _, err := tmpl.RenderResult(Meter, RenderModeInstance, map[string]any{"host": tt.host})
|
||||
if tt.valid {
|
||||
require.NoError(t, err)
|
||||
} else {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue