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

Basic templating support example #237

Draft
wants to merge 3 commits into
base: main
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions pkg/template/examples/invoice.yaml.tmpl
Original file line number Diff line number Diff line change
@@ -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: "[email protected]"
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 }}
34 changes: 34 additions & 0 deletions pkg/template/helpers.go
Original file line number Diff line number Diff line change
@@ -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 ""
}
72 changes: 72 additions & 0 deletions pkg/template/template.go
Original file line number Diff line number Diff line change
@@ -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
}
54 changes: 54 additions & 0 deletions pkg/template/template_test.go
Original file line number Diff line number Diff line change
@@ -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.tmpl")
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())
}
Loading