Templates



Templates are the implementation for your generator. They are parameterized files which are filled in with data from the schema.

  • Based on Go text/template with extra helpers
  • Support for partial templates for reuse and separate of concerns
  • Typically in files, can also be a string in your CUE

Generators have several kinds of files that end up in the output

  1. Once Templates - used to generate a single file, like main.go or index.js
  2. Repeated Templates - used in a loop to generate files per entry, for routes in this example
  3. Partial Templates - code snippets which are available in other templates
  4. Static Files - copied directly into the output, bypassing the template engine

Once Templates

These files are needed once for every server we generate. Some have minimal templating and others loop over values, like router.go.

templates/go.mod

module {{ .SERVER.GoModule }}

go 1.17

require (
	github.com/labstack/echo-contrib v0.11.0
	github.com/labstack/echo/v4 v4.6.1
	github.com/prometheus/client_golang v1.11.0
)

templates/server.go

package main

import (
	"context"
	"os"
	"os/signal"
	"time"

	"github.com/labstack/echo/v4"
)

var PORT string = ":8080"

func main() {
	// create echo server
	e := echo.New()
	e.HideBanner = true

	// add middleware
	err := setupMiddleware(e)
	if err != nil {
		panic(err)
	}

	// setup router
	err = setupRouter(e)
	if err != nil {
		panic(err)
	}

	//
	// code to run server and enable graceful shutdown
	//
	// Start server with background goroutine
	go func() {
		if err := e.Start(PORT); err != nil {
			e.Logger.Info("shutting down the server")
		}
	}()

	// Wait for interrupt signal to gracefully shutdown the server with
	// a timeout of 10 seconds.
	quit := make(chan os.Signal)
	signal.Notify(quit, os.Interrupt)

	// wait on a quit signal
	<-quit

	// start the shutdown process
	ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
	defer cancel()
	if err := e.Shutdown(ctx); err != nil {
		e.Logger.Fatal(err)
	}
}

templates/router.go

package main

import (
	"net/http"

	"github.com/labstack/echo/v4"
	"github.com/prometheus/client_golang/prometheus/promhttp"

	{{ if gt (len .SERVER.Routes ) 1 }}
	"{{ .SERVER.GoModule }}/routes"
	{{ end }}
)

func setupRouter(e *echo.Echo) error {

	// Internal routes
	e.GET("/internal/alive", func(c echo.Context) error {
		return c.NoContent(http.StatusOK)
	})

	{{ if .SERVER.Prometheus }}
	h := promhttp.Handler()
	e.GET("/internal/metrics", func(c echo.Context) error {
		h.ServeHTTP(c.Response(), c.Request())
		return nil
	})
	{{ end }}

	// Application routes group
	g := e.Group("")

	// Register the routes
	{{ range $ROUTE := .SERVER.Routes -}}
	routes.{{ $ROUTE.Name }}Routes(g)
	{{ end }}

	return nil
}

templates/middleware.go

package main

import (
	"github.com/labstack/echo/v4"
	"github.com/labstack/echo/v4/middleware"
	"github.com/labstack/echo-contrib/prometheus"
)

func setupMiddleware(e *echo.Echo) error {
	// setup recovery middleware
	e.Use(middleware.Recover())

	// setup logging middleware
	e.Use(middleware.Logger())

	{{ if .SERVER.Prometheus }}
	// Setup metrics middleware
	p := prometheus.NewPrometheus("{{ .SERVER.Name }}", nil)
	e.Use(p.HandlerFunc)
	{{ end }}

	return nil
}

Repeated and Partial Templates

We separate the handler into a template which uses the partial. This is for demonstration purpose here and will be more useful in the “full-example” section where the implementation is more complete.

templates/route.go

package routes

import (
	"net/http"

	"github.com/labstack/echo/v4"
)

// {{ .ROUTE.Name }}Routes sets up the routes in a router Group
func {{ .ROUTE.Name }}Routes(G *echo.Group) {
	g := G.Group("{{ .ROUTE.Path }}{{ range $PATH := .ROUTE.Params }}/:{{$PATH}}{{ end }}")
	g.{{.ROUTE.Method}}( "", {{.ROUTE.Name}}Handler)

	// we'll handle sub-routes here in "full-example"
}

// Handler implementation is in a partial template
{{ template "handler.go" .ROUTE }}

partials/handler.go

{{ $ROUTE := . }}
func {{ $ROUTE.Name }}Handler(c echo.Context) (err error) {

	// process any path and query params
	{{ range $P := $ROUTE.Params }}
	{{ $P }} := c.Param("{{ $P }}")
	{{ end }}

	{{ range $Q := $ROUTE.Query }}
	{{ $Q }} := c.QueryParam("{{ $Q }}")
	{{ end }}

	{{ if $ROUTE.Body }}
	// custom body
	{{ $ROUTE.Body }}
	{{ else }}
	// default body
	c.String(http.StatusNotImplemented, "Not Implemented")
	{{ end }}

	return nil
}

Rendered Output Files

Here we can see the result of code generation for a sample of the files. We will actually generate these in the next section. They are provided here so you can see the input / output pairs on a single page.

output/middleware.go

package main

import (
	"github.com/labstack/echo-contrib/prometheus"
	"github.com/labstack/echo/v4"
	"github.com/labstack/echo/v4/middleware"
)

func setupMiddleware(e *echo.Echo) error {
	// setup recovery middleware
	e.Use(middleware.Recover())

	// setup logging middleware
	e.Use(middleware.Logger())

	// Setup metrics middleware
	p := prometheus.NewPrometheus("Example", nil)
	e.Use(p.HandlerFunc)

	return nil
}

output/router.go

package main

import (
	"net/http"

	"github.com/labstack/echo/v4"
	"github.com/prometheus/client_golang/prometheus/promhttp"

	"hof.io/docs/first-example/routes"
)

func setupRouter(e *echo.Echo) error {

	// Internal routes
	e.GET("/internal/alive", func(c echo.Context) error {
		return c.NoContent(http.StatusOK)
	})

	h := promhttp.Handler()
	e.GET("/internal/metrics", func(c echo.Context) error {
		h.ServeHTTP(c.Response(), c.Request())
		return nil
	})

	// Application routes group
	g := e.Group("")

	// Register the routes
	routes.EchoQRoutes(g)
	routes.EchoPRoutes(g)
	routes.HelloRoutes(g)

	return nil
}

output/routes/Hello.go

package routes

import (
	"net/http"

	"github.com/labstack/echo/v4"
)

// HelloRoutes sets up the routes in a router Group
func HelloRoutes(G *echo.Group) {
	g := G.Group("/hello")
	g.GET("", HelloHandler)

	// we'll handle sub-routes here in "full-example"
}

// Handler implementation is in a partial template

func HelloHandler(c echo.Context) (err error) {

	// process any path and query params

	msg := c.QueryParam("msg")

	// custom body
	if msg == "" {
		msg = "hello world"
	}
	c.String(http.StatusOK, msg)

	return nil
}

2022 Hofstadter, Inc