From 2904a2978c2c95823ba4586faaa8568e70426651 Mon Sep 17 00:00:00 2001 From: Sam Lown Date: Tue, 30 Jan 2024 23:03:54 +0000 Subject: [PATCH 1/2] Basic templating support example --- pkg/template/examples/invoice.yaml | 34 ++++++++++++++ pkg/template/helpers.go | 34 ++++++++++++++ pkg/template/template.go | 72 ++++++++++++++++++++++++++++++ pkg/template/template_test.go | 54 ++++++++++++++++++++++ 4 files changed, 194 insertions(+) create mode 100644 pkg/template/examples/invoice.yaml create mode 100644 pkg/template/helpers.go create mode 100644 pkg/template/template.go create mode 100644 pkg/template/template_test.go diff --git a/pkg/template/examples/invoice.yaml b/pkg/template/examples/invoice.yaml new file mode 100644 index 00000000..41075ae0 --- /dev/null +++ b/pkg/template/examples/invoice.yaml @@ -0,0 +1,34 @@ +$schema: "https://gobl.org/draft-0/bill/invoice" +series: "{{ .series | optional }}" +code: "{{ .code | optional }}" +supplier: + tax_id: + country: "ES" + code: "B98602642" # random + name: "Provide One S.L." + emails: + - addr: "billing@example.com" + addresses: + - num: "42" + street: "Calle Pradillo" + locality: "Madrid" + region: "Madrid" + code: "28002" + country: "ES" + +customer: + name: "{{ .customer_name }}" + tax_id: + country: "{{ .customer_country }}" + code: "{{ .customer_tax_code }}" + +lines: + {{ range .lines }} + - quantity: {{ .quantity }} + item: + name: "{{ .item_name }}" + price: "{{ .item_price }}" + taxes: + - cat: "VAT" + rate: "standard" + {{ end }} \ No newline at end of file diff --git a/pkg/template/helpers.go b/pkg/template/helpers.go new file mode 100644 index 00000000..06ffedea --- /dev/null +++ b/pkg/template/helpers.go @@ -0,0 +1,34 @@ +package template + +import ( + "strings" +) + +// Indent takes the text, finds all matching `\n`, and adds +// *two* spaces immediately after for each of the provided counts. +// This is useful for indenting variables as blocks of text to +// be correctly presented in YAML files. +// +// Example YAML block: +// +// rsa_key: |- +// {{ .Key | indent 1 }} +func Indent(count int, text string) string { + spaces := "" + for i := 0; i < count; i++ { + spaces = spaces + " " + } + return strings.ReplaceAll(text, "\n", "\n"+spaces) +} + +// Optional is useful when outputting strings to ensure that +// empty values are outputted correctly. +func Optional(in any) string { + if in == nil { + return "" + } + if s, ok := in.(string); ok { + return s + } + return "" +} diff --git a/pkg/template/template.go b/pkg/template/template.go new file mode 100644 index 00000000..131f0deb --- /dev/null +++ b/pkg/template/template.go @@ -0,0 +1,72 @@ +// Package template provides a common set of tools around Go templates +// that help with converting data in other formats to GOBL +// documents. +package template + +import ( + "fmt" + "strings" + "text/template" + + "github.com/invopop/gobl" + "github.com/invopop/yaml" +) + +// Template contains a GOBL template document prepared for interpolating +// with incoming rows or objects of data. +type Template struct { + tmpl *template.Template +} + +// New defines a new template with the given name and data. +func New(name, data string) (*Template, error) { + t := new(Template) + t.tmpl = template.New(name). + Option("missingkey=zero"). + Funcs(template.FuncMap{ + "indent": Indent, + "optional": Optional, + }) + + var err error + t.tmpl, err = t.tmpl.Parse(data) + if err != nil { + return nil, err + } + + return t, nil +} + +// Must is a helper function that wraps a call to a function returning +// (*Template, error) and panics if the error is non-nil. It is intended +// for use in variable initializations such as +// +// var t = template.Must(template.New("name", "..data..")) +func Must(t *Template, err error) *Template { + if err != nil { + panic(err) + } + return t +} + +// Execute takes the given data and interpolates it into the +// template to generate a GOBL Envelope or Schema Object according +// to the schema defined in the template. +func (t *Template) Execute(data any) (any, error) { + buf := new(strings.Builder) + if err := t.tmpl.Execute(buf, data); err != nil { + return nil, err + } + + out, err := yaml.YAMLToJSON([]byte(buf.String())) + if err != nil { + return nil, fmt.Errorf("parsing input: %w", err) + } + + res, err := gobl.Parse(out) + if err != nil { + return nil, fmt.Errorf("parsing GOBL: %w", err) + } + + return res, nil +} diff --git a/pkg/template/template_test.go b/pkg/template/template_test.go new file mode 100644 index 00000000..35c9f8e1 --- /dev/null +++ b/pkg/template/template_test.go @@ -0,0 +1,54 @@ +package template_test + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/invopop/gobl/bill" + "github.com/invopop/gobl/pkg/template" +) + +func TestTemplateExecute(t *testing.T) { + data, err := os.ReadFile("./examples/invoice.yaml") + require.NoError(t, err) + + row := map[string]any{ + "code": "1234", + "customer_country": "ES", + "customer_tax_code": "A27425347", + "customer_name": "ACME S.L.", + "lines": []map[string]any{ + { + "quantity": "1", + "item_name": "Widgets", + "item_price": "100.00", + }, + { + "quantity": "12", + "item_name": "Gadgets", + "item_price": "5.23", + }, + }, + } + + tmpl, err := template.New("invoice", string(data)) + require.NoError(t, err) + + out, err := tmpl.Execute(row) + require.NoError(t, err) + require.NotNil(t, out) + + inv, ok := out.(*bill.Invoice) + require.True(t, ok) + + require.NoError(t, inv.Calculate()) + require.NoError(t, inv.Validate()) + + assert.Equal(t, "ACME S.L.", inv.Customer.Name) + assert.Equal(t, "", inv.Series) + assert.Equal(t, "196.94", inv.Totals.Payable.String()) + assert.Equal(t, "34.18", inv.Totals.Tax.String()) +} From 43b0563a8fb24ca457053f1d747315f913a55a9f Mon Sep 17 00:00:00 2001 From: Sam Lown Date: Tue, 30 Jan 2024 23:07:02 +0000 Subject: [PATCH 2/2] Avoid using yaml as example --- pkg/template/examples/{invoice.yaml => invoice.yaml.tmpl} | 0 pkg/template/template_test.go | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename pkg/template/examples/{invoice.yaml => invoice.yaml.tmpl} (100%) diff --git a/pkg/template/examples/invoice.yaml b/pkg/template/examples/invoice.yaml.tmpl similarity index 100% rename from pkg/template/examples/invoice.yaml rename to pkg/template/examples/invoice.yaml.tmpl diff --git a/pkg/template/template_test.go b/pkg/template/template_test.go index 35c9f8e1..abb21d10 100644 --- a/pkg/template/template_test.go +++ b/pkg/template/template_test.go @@ -12,7 +12,7 @@ import ( ) func TestTemplateExecute(t *testing.T) { - data, err := os.ReadFile("./examples/invoice.yaml") + data, err := os.ReadFile("./examples/invoice.yaml.tmpl") require.NoError(t, err) row := map[string]any{