Repository: ovn-org/libovsdb
Pull request #143 (2021-06-02 15:53:41)

Refactor code generator
To make it easier to test and use externally, refactor modelgen into it's own package, and make it accept an arbitrary template text so multiple code generators can be potentially implemented.

coveralls: ## Pull Request Test Coverage Report for [Build 899975466](https://coveralls.io/builds/40246500)

* **36** of **76**   **(47.37%)**  changed or added relevant lines in **3** files are covered.
* No unchanged relevant lines lost coverage.
* Overall coverage increased (+**1.1%**) to **70.671%**

---

|  Changes Missing Coverage | Covered Lines | Changed/Added Lines | % |
| :-----|--------------|--------|---: |
| [modelgen/dbmodel.go](https://coveralls.io/builds/40246500/source?filename=modelgen%2Fdbmodel.go#L25) | 0 | 18 | 0.0%
| [modelgen/table.go](https://coveralls.io/builds/40246500/source?filename=modelgen%2Ftable.go#L47) | 35 | 57 | 61.4%
<!-- | **Total:** | **36** | **76** | **47.37%** | -->


|  Totals | [![Coverage Status](https://coveralls.io/builds/40246500/badge)](https://coveralls.io/builds/40246500) |
| :-- | --: |
| Change from base [Build 882729464](https://coveralls.io/builds/40088700): |  1.1% |
| Covered Lines: | 1853 |
| Relevant Lines: | 2622 |

---
##### 💛  - [Coveralls](https://coveralls.io)

amorenoz: > Thinking on this some more, I wonder whether it's wise to allow the user to replace the entire template...
> 
> Maybe we should flip the API around so the caller is in control of the template execution.
> 
> E.g
> 
>     1. Moving the `const` templates from `cmd/modelgen/main.go` to the `modelgen` package. These should probably be registered (i.e via template.New) when the package is imported, or alternatively the user would call `New*Template` first - that might allow us to provide options to override bits of the base template e.g omit package name, binary name, or the comment about the code being auto-generated.
> 
>     2. Change `New*Generator` in to `New*Data` which would return a `map[string]interface{}` with the necessary key/values for the base template to parse correctly
> 
>     3. Encourage the use of [Nested Templates](https://golang.org/pkg/text/template/#hdr-Nested_template_definitions) for overriding template behaviour.
> 
> 
> Then all the user needs to do is calling `template.Execute` in whichever way they like.
> 
> A user defined table template could be something like this, where `table_base` was the template we registered earlier...
> 
> ```
> {{ template "table_base" }}
> 
> func (o *{{index . "table_name"}}) Foo() string {
>    return "foo"
> }
> ```

I thought of something similar at the beginning but started to get complicated when I had to add extra fields and extra tags to the table struct: see my usecase here:
https://github.com/skydive-project/skydive/blob/52c9d935ffb8924c13ec15907ddb9d8d96322093/topology/probes/ovn/ovnmetagen/table.go#L10

Resulting in files like:

https://github.com/skydive-project/skydive/blob/52c9d935ffb8924c13ec15907ddb9d8d96322093/topology/probes/ovn/ovnmodel/load_balancer.go

Can Nested Templates work for lags and additional fields?

amorenoz: So maybe I should just expose the `FieldName` `FieldType`, et al functions and leave the template generation as it is...
dave-tucker: > Can Nested Templates work for lags and additional fields?

Yes!

For example, what if we register a couple of blank templates called `"additional_struct_fields"` and one called `"after_struct_definition"`

your table template is something like this:

```
// {{ .StructName }} defines an object in {{ .TableName }} table
type {{ .StructName }} struct {
   {{ range .Fields }} {{ .Name }}  {{ .Type }}   {{ .Tag }}
   {{ end }}
   {{ template "additional_struct_fields" }}
}

{{ template "after_struct_definition" }}
```

You could replace those empty templates with ones that acutally have content.
https://golang.org/pkg/text/template/#Template.AddParseTree

Patch:
diff --git a/Makefile b/Makefile
index e33711d..7945ea6 100644
--- a/Makefile
+++ b/Makefile
@@ -8,7 +8,7 @@ prebuild-local:
 	@echo "+ $@"
 	@mkdir -p bin
 	@go build -v -o ./bin ./cmd/modelgen
-	@curl -o example/play_with_ovs/ovs.ovsschema https://raw.githubusercontent.com/openvswitch/ovs/v2.15.0/vswitchd/vswitch.ovsschema
+	@[ -f example/play_with_ovs/ovs.ovsschema ] || curl -o example/play_with_ovs/ovs.ovsschema https://raw.githubusercontent.com/openvswitch/ovs/v2.15.0/vswitchd/vswitch.ovsschema
 	@go generate -v ./...
 
 build-local: prebuild-local
diff --git a/README.md b/README.md
index 83662c2..ece8a74 100644
--- a/README.md
+++ b/README.md
@@ -80,6 +80,7 @@ This package is divided into several subpackages. Documentation for each subpack
 * **model**: model and database model used for mapping [![godoc for libovsdb/model][modelbadge]][modeldoc]
 * **ovsdb**: low level OVS types [![godoc for libovsdb/ovsdb][ovsdbbadge]][ovsdbdoc]
 * **cache**: model-based cache [![godoc for libovsdb/cache][cachebadge]][cachedoc]
+* **modelgen**: common code-generator functions  [![godoc for libovsdb/modelgen][genbadge]][gendoc]
 
 [doc]: https://pkg.go.dev/
 [clientbadge]: https://pkg.go.dev/badge/github.com/ovn-org/libovsdb/client
@@ -87,11 +88,13 @@ This package is divided into several subpackages. Documentation for each subpack
 [modelbadge]: https://pkg.go.dev/badge/github.com/ovn-org/libovsdb/model
 [ovsdbbadge]: https://pkg.go.dev/badge/github.com/ovn-org/libovsdb/ovsdb
 [cachebadge]: https://pkg.go.dev/badge/github.com/ovn-org/libovsdb/cache
+[genbadge]: https://pkg.go.dev/badge/github.com/ovn-org/libovsdb/modelgen
 [clientdoc]: https://pkg.go.dev/github.com/ovn-org/libovsdb/client
 [mapperdoc]: https://pkg.go.dev/github.com/ovn-org/libovsdb/mapper
 [modeldoc]: https://pkg.go.dev/github.com/ovn-org/libovsdb/model
 [ovsdbdoc]: https://pkg.go.dev/github.com/ovn-org/libovsdb/ovsdb
 [cachedoc]: https://pkg.go.dev/github.com/ovn-org/libovsdb/cache
+[gendoc]: https://pkg.go.dev/github.com/ovn-org/libovsdb/modelgen
 
 ## Quick API Examples
 
diff --git a/client/client.go b/client/client.go
index 2131f36..2a51f8b 100644
--- a/client/client.go
+++ b/client/client.go
@@ -1,7 +1,6 @@
 package client
 
 import (
-	"context"
 	"crypto/tls"
 	"encoding/json"
 	"fmt"
@@ -49,9 +48,8 @@ const (
 
 // Connect to ovn, using endpoint in format ovsdb Connection Methods
 // If address is empty, use default address for specified protocol
-func Connect(ctx context.Context, endpoints string, database *model.DBModel, tlsConfig *tls.Config) (*OvsdbClient, error) {
+func Connect(endpoints string, database *model.DBModel, tlsConfig *tls.Config) (*OvsdbClient, error) {
 	var c net.Conn
-	var dialer net.Dialer
 	var err error
 	var u *url.URL
 
@@ -71,14 +69,11 @@ func Connect(ctx context.Context, endpoints string, database *model.DBModel, tls
 			if len(path) == 0 {
 				path = defaultUnixAddress
 			}
-			c, err = dialer.DialContext(ctx, u.Scheme, path)
+			c, err = net.Dial(u.Scheme, path)
 		case TCP:
-			c, err = dialer.DialContext(ctx, u.Scheme, host)
+			c, err = net.Dial(u.Scheme, host)
 		case SSL:
-			dialer := tls.Dialer{
-				Config: tlsConfig,
-			}
-			c, err = dialer.DialContext(ctx, "tcp", host)
+			c, err = tls.Dial("tcp", host, tlsConfig)
 		default:
 			err = fmt.Errorf("unknown network protocol %s", u.Scheme)
 		}
diff --git a/cmd/modelgen/dbmodel.go b/cmd/modelgen/dbmodel.go
deleted file mode 100644
index 430618b..0000000
--- a/cmd/modelgen/dbmodel.go
+++ /dev/null
@@ -1,47 +0,0 @@
-package main
-
-import (
-	"text/template"
-
-	"github.com/ovn-org/libovsdb/ovsdb"
-)
-
-const MODEL_TEMPLATE = `
-// Code generated by "libovsdb.modelgen"
-// DO NOT EDIT.
-
-package {{ .PackageName }}
-
-import (
-	"github.com/ovn-org/libovsdb/model"
-)
-
-// FullDatabaseModel returns the DatabaseModel object to be used in libovsdb
-func FullDatabaseModel() (*model.DBModel, error) {
-	return model.NewDBModel("{{ .DatabaseName }}", map[string]model.Model{
-    {{ range $tableName, $structName := .Tables }} "{{ $tableName }}" : &{{ $structName }}{}, 
-    {{ end }}
-	})
-}
-`
-
-//DBModelTemplateData is the data needed for template processing
-type DBModelTemplateData struct {
-	PackageName  string
-	DatabaseName string
-	Tables       map[string]string
-}
-
-//NewDBModelGenerator returns a new DBModel generator
-func NewDBModelGenerator(pkg string, schema *ovsdb.DatabaseSchema) Generator {
-	templateData := DBModelTemplateData{
-		PackageName:  pkg,
-		DatabaseName: schema.Name,
-		Tables:       map[string]string{},
-	}
-	for tableName := range schema.Tables {
-		templateData.Tables[tableName] = StructName(tableName)
-	}
-	modelTemplate := template.Must(template.New("DBModel").Parse(MODEL_TEMPLATE))
-	return newGenerator("model.go", modelTemplate, templateData)
-}
diff --git a/cmd/modelgen/generator.go b/cmd/modelgen/generator.go
deleted file mode 100644
index fef8183..0000000
--- a/cmd/modelgen/generator.go
+++ /dev/null
@@ -1,39 +0,0 @@
-package main
-
-import (
-	"bytes"
-	"go/format"
-	"text/template"
-)
-
-type Generator interface {
-	FileName() string
-	Format() ([]byte, error)
-}
-
-type generator struct {
-	filename string
-	template *template.Template
-	data     interface{}
-}
-
-func (g *generator) Format() ([]byte, error) {
-	buffer := bytes.Buffer{}
-	err := g.template.Execute(&buffer, g.data)
-	if err != nil {
-		return nil, err
-	}
-	return format.Source(buffer.Bytes())
-}
-
-func (g *generator) FileName() string {
-	return g.filename
-}
-
-func newGenerator(filename string, template *template.Template, data interface{}) Generator {
-	return &generator{
-		filename: filename,
-		template: template,
-		data:     data,
-	}
-}
diff --git a/cmd/modelgen/main.go b/cmd/modelgen/main.go
index 4f99049..4b12815 100644
--- a/cmd/modelgen/main.go
+++ b/cmd/modelgen/main.go
@@ -9,6 +9,7 @@ import (
 	"os"
 	"path/filepath"
 
+	"github.com/ovn-org/libovsdb/modelgen"
 	"github.com/ovn-org/libovsdb/ovsdb"
 )
 
@@ -25,17 +26,6 @@ var (
 	dryRun   = flag.Bool("d", false, "Dry run")
 )
 
-func writeFile(filename string, src []byte) error {
-	if *dryRun {
-		fmt.Printf("---- Content of file %s ----\n", filename)
-		fmt.Print(string(src))
-		fmt.Print("\n")
-		return nil
-	} else {
-		return ioutil.WriteFile(filename, src, 0644)
-	}
-}
-
 func main() {
 	log.SetFlags(0)
 	log.SetPrefix("modelgen: ")
@@ -75,20 +65,15 @@ func main() {
 		log.Fatal(err)
 	}
 
-	generators := []Generator{}
+	gen := modelgen.NewGenerator(*dryRun)
 	for name, table := range dbSchema.Tables {
-		generators = append(generators, NewTableGenerator(pkgName, name, &table))
-	}
-	generators = append(generators, NewDBModelGenerator(pkgName, &dbSchema))
-
-	for _, gen := range generators {
-		code, err := gen.Format()
-		if err != nil {
-			log.Fatal(err)
-		}
-		outFile := filepath.Join(outDir, gen.FileName())
-		if err := writeFile(outFile, code); err != nil {
+		template, args := modelgen.NewTableTemplate(pkgName, name, &table)
+		if err := gen.Generate(modelgen.FileName(name), template, args); err != nil {
 			log.Fatal(err)
 		}
 	}
+	dbTemplate, dbArgs := modelgen.NewDBTemplate(pkgName, &dbSchema)
+	if err := gen.Generate("model.go", dbTemplate, dbArgs); err != nil {
+		log.Fatal(err)
+	}
 }
diff --git a/cmd/modelgen/table.go b/cmd/modelgen/table.go
deleted file mode 100644
index 97b796c..0000000
--- a/cmd/modelgen/table.go
+++ /dev/null
@@ -1,185 +0,0 @@
-package main
-
-import (
-	"fmt"
-	"sort"
-	"strings"
-	"text/template"
-
-	"github.com/ovn-org/libovsdb/ovsdb"
-)
-
-const TABLE_TEMPLATE = `
-// Code generated by "ovsdb.modelgen"
-// DO NOT EDIT.
-
-package {{ .PackageName }}
-
-// {{ .StructName }} defines an object in {{ .TableName }} table
-type {{ .StructName }} struct {
-    {{ range .Fields }} {{ .Name }}  {{ .Type }}   {{ .Tag }}
-    {{ end }}
-}
-`
-
-// TableTemplateData is the data needed for template processing
-type TableTemplateData struct {
-	TableName   string
-	PackageName string
-	StructName  string
-	Fields      []Field
-}
-
-// Field represents the field information
-type Field struct {
-	Name string
-	Type string
-	Tag  string
-}
-
-// NewTableGenerator returns a table code generator
-func NewTableGenerator(pkg string, name string, table *ovsdb.TableSchema) Generator {
-	templateData := TableTemplateData{
-		TableName:   name,
-		PackageName: pkg,
-		StructName:  StructName(name),
-		Fields:      []Field{},
-	}
-	// First, add UUID
-	templateData.Fields = append(templateData.Fields,
-		Field{
-			Name: "UUID",
-			Type: "string",
-			Tag:  Tag("_uuid"),
-		})
-
-	// Map iteration order is random, so for predictable generation
-	// lets sort fields by name
-	var order sort.StringSlice
-	for columnName := range table.Columns {
-		order = append(order, columnName)
-	}
-	order.Sort()
-
-	for _, columnName := range order {
-		columnSchema := table.Columns[columnName]
-		templateData.Fields = append(templateData.Fields, Field{
-			Name: FieldName(columnName),
-			Type: FieldType(columnSchema),
-			Tag:  Tag(columnName),
-		})
-	}
-
-	tableTemplate := template.Must(template.New(name).Parse(TABLE_TEMPLATE))
-	return newGenerator(FileName(name), tableTemplate, templateData)
-}
-
-// FieldName returns the name of a column field
-func FieldName(column string) string {
-	return camelCase(column)
-}
-
-// StructName returns the name of the table struct
-func StructName(tableName string) string {
-	return strings.ReplaceAll(tableName, "_", "")
-}
-
-// FieldType returns the string representation of a column type
-func FieldType(column *ovsdb.ColumnSchema) string {
-	switch column.Type {
-	case ovsdb.TypeEnum:
-		return AtomicType(column.TypeObj.Key.Type)
-	case ovsdb.TypeMap:
-		return fmt.Sprintf("map[%s]%s", AtomicType(column.TypeObj.Key.Type),
-			AtomicType(column.TypeObj.Value.Type))
-	case ovsdb.TypeSet:
-		return fmt.Sprintf("[]%s", AtomicType(column.TypeObj.Key.Type))
-	default:
-		return AtomicType(column.Type)
-	}
-}
-
-// BasicType returns the string type of an AtomicType
-func AtomicType(atype string) string {
-	switch atype {
-	case ovsdb.TypeInteger:
-		return "int"
-	case ovsdb.TypeReal:
-		return "float64"
-	case ovsdb.TypeBoolean:
-		return "bool"
-	case ovsdb.TypeString:
-		return "string"
-	case ovsdb.TypeUUID:
-		return "string"
-	}
-	return ""
-}
-
-// Tag returns the Tag string of a column
-func Tag(column string) string {
-	return fmt.Sprintf("`ovs:\"%s\"`", column)
-}
-
-// Filename returns the filename of a table
-func FileName(table string) string {
-	return fmt.Sprintf("%s.go", strings.ToLower(table))
-}
-
-// common initialisms used in ovsdb schemas
-var initialisms = map[string]bool{
-	"ACL":   true,
-	"BFD":   true,
-	"CFM":   true,
-	"CT":    true,
-	"CVLAN": true,
-	"DNS":   true,
-	"DSCP":  true,
-	"ID":    true,
-	"IP":    true,
-	"IPFIX": true,
-	"LACP":  true,
-	"LLDP":  true,
-	"MAC":   true,
-	"MTU":   true,
-	"OVS":   true,
-	"QOS":   true,
-	"RSTP":  true,
-	"SSL":   true,
-	"STP":   true,
-	"TCP":   true,
-	"UDP":   true,
-	"UUID":  true,
-	"VLAN":  true,
-}
-
-func camelCase(field string) string {
-	s := strings.ToLower(field)
-	parts := strings.FieldsFunc(s, func(r rune) bool {
-		return r == '_' || r == '-'
-	})
-	if len(parts) > 1 {
-		s = ""
-		for _, p := range parts {
-			s += strings.Title(expandInitilaisms(p))
-		}
-	} else {
-		s = strings.Title(expandInitilaisms(s))
-	}
-	return s
-}
-
-func expandInitilaisms(s string) string {
-	// check initialisms
-	if u := strings.ToUpper(s); initialisms[u] {
-		return strings.ToUpper(s)
-	}
-	// check for plurals too
-	if strings.HasSuffix(s, "s") {
-		sub := s[:len(s)-1]
-		if u := strings.ToUpper(sub); initialisms[u] {
-			return strings.ToUpper(sub) + "s"
-		}
-	}
-	return s
-}
diff --git a/cmd/stress/stress.go b/cmd/stress/stress.go
index 0f6173e..34c6b72 100644
--- a/cmd/stress/stress.go
+++ b/cmd/stress/stress.go
@@ -1,7 +1,6 @@
 package main
 
 import (
-	"context"
 	"flag"
 	"fmt"
 	"log"
@@ -46,7 +45,7 @@ var (
 )
 
 func run() {
-	ovs, err := client.Connect(context.Background(), *connection, dbModel, nil)
+	ovs, err := client.Connect(*connection, dbModel, nil)
 	if err != nil {
 		log.Fatal(err)
 	}
diff --git a/example/play_with_ovs/play_with_ovs.go b/example/play_with_ovs/play_with_ovs.go
index 54c8bb5..74753dd 100644
--- a/example/play_with_ovs/play_with_ovs.go
+++ b/example/play_with_ovs/play_with_ovs.go
@@ -1,7 +1,6 @@
 package main
 
 import (
-	"context"
 	"flag"
 	"fmt"
 	"log"
@@ -102,7 +101,7 @@ func main() {
 		log.Fatal("Unable to create DB model ", err)
 	}
 	// By default libovsdb connects to 127.0.0.0:6400.
-	ovs, err := client.Connect(context.Background(), *connection, dbmodel, nil)
+	ovs, err := client.Connect(*connection, dbmodel, nil)
 
 	// If you prefer to connect to OVS in a specific location :
 	// ovs, err := client.Connect("tcp:192.168.56.101:6640", nil)
diff --git a/modelgen/dbmodel.go b/modelgen/dbmodel.go
new file mode 100644
index 0000000..3a94552
--- /dev/null
+++ b/modelgen/dbmodel.go
@@ -0,0 +1,96 @@
+package modelgen
+
+import (
+	"sort"
+	"text/template"
+
+	"github.com/ovn-org/libovsdb/ovsdb"
+)
+
+// BASE_DB_TEMPLATE is the base DBModel template
+// It includes the following other templates that can be overriden to customize the generated file
+// "header"
+// "preDBDefinitions"
+// "postDBDefinitions"
+// It is design to be used with a map[string] interface and some defined keys (see GetDBTemplateData)
+const BASE_DB_TEMPLATE = `{{ template "header" . }}
+
+package {{ index . "PackageName" }}
+
+{{ template "preDBDefinitions" }}
+
+// FullDatabaseModel returns the DatabaseModel object to be used in libovsdb
+func FullDatabaseModel() (*model.DBModel, error) {
+	return model.NewDBModel("{{ index . "DatabaseName" }}", map[string]model.Model{
+    {{ range index . "Tables" }} "{{ .TableName }}" : &{{ .StructName }}{}, 
+    {{ end }}
+	})
+}
+
+{{ template "postDBDefinitions" . }}
+
+`
+
+// DEFAULT_PRE_DB_TEMPLATE is the default template for "preDBDefinitions"
+const DEFAULT_PRE_DB_TEMPLATE = `{{ define "preDBDefinitions" }} import (
+	"github.com/ovn-org/libovsdb/model"
+) {{ end }}
+`
+
+// DEFAULT_POST_DB_TEMPLATE is the default template for "postDBDefinitions"
+const DEFAULT_POST_DB_TEMPLATE = `{{ define "postDBDefinitions" }}{{ end }}`
+
+//TableInfo represents the information of a table needed by the Model template
+type TableInfo struct {
+	TableName  string
+	StructName string
+}
+
+// GetDBTemplateData returns the map needed to execute the DBTemplate. It has the following keys:
+// DatabaseName: (string) the database name
+// PackageName : (string) the package name
+// Tables: []Table list of Tables that form the Model
+func GetDBTemplateData(pkg string, schema *ovsdb.DatabaseSchema) map[string]interface{} {
+	data := map[string]interface{}{}
+	data["DatabaseName"] = schema.Name
+	data["PackageName"] = pkg
+	tables := []TableInfo{}
+
+	var order sort.StringSlice
+	for tableName := range schema.Tables {
+		order = append(order, tableName)
+	}
+	order.Sort()
+
+	for _, tableName := range order {
+		tables = append(tables, TableInfo{
+			TableName:  tableName,
+			StructName: StructName(tableName),
+		})
+	}
+	data["Tables"] = tables
+	return data
+}
+
+// NewDBTemplate returns a new DBTemplate and the DBTemplate data map
+// See BASE_DB_TEMPLATE to a detailed explanation of the possible ways this template can be expanded
+func NewDBTemplate(pkg string, db *ovsdb.DatabaseSchema) (*template.Template, map[string]interface{}) {
+	main, err := template.New("DB").Parse(BASE_DB_TEMPLATE)
+	if err != nil {
+		panic(err)
+	}
+	tmpl, err := main.Parse(DEFAULT_HEADER_TEMPLATE)
+	if err != nil {
+		panic(err)
+	}
+	tmpl, err = tmpl.Parse(DEFAULT_PRE_DB_TEMPLATE)
+	if err != nil {
+		panic(err)
+	}
+	_, err = tmpl.Parse(DEFAULT_POST_DB_TEMPLATE)
+	if err != nil {
+		panic(err)
+	}
+
+	return main, GetDBTemplateData(pkg, db)
+}
diff --git a/modelgen/doc.go b/modelgen/doc.go
new file mode 100644
index 0000000..26dd868
--- /dev/null
+++ b/modelgen/doc.go
@@ -0,0 +1,7 @@
+/*
+Package modelgen provides core functionality to implement Model code generators based on a schema.
+
+It allows to create and customize a text/template that can generate the Model types that libovsdb can work with.
+
+*/
+package modelgen
diff --git a/modelgen/generator.go b/modelgen/generator.go
new file mode 100644
index 0000000..9247b96
--- /dev/null
+++ b/modelgen/generator.go
@@ -0,0 +1,64 @@
+package modelgen
+
+import (
+	"bytes"
+	"fmt"
+	"go/format"
+	"io/ioutil"
+	"log"
+	"text/template"
+)
+
+// Generator is an interface that allows to format code from a template and write it to a file
+type Generator interface {
+	Generate(string, *template.Template, interface{}) error
+	Format(*template.Template, interface{}) ([]byte, error)
+}
+
+type generator struct {
+	dryRun bool
+}
+
+// Format returns a formatted byte slice by executing the template with the given args
+func (g *generator) Format(tmpl *template.Template, args interface{}) ([]byte, error) {
+	buffer := bytes.Buffer{}
+	err := tmpl.Execute(&buffer, args)
+	if err != nil {
+		return nil, err
+	}
+
+	src, err := format.Source(buffer.Bytes())
+	if err != nil {
+		return nil, err
+	}
+	return src, nil
+}
+
+// Generate generates the code and writes it to specified file path
+func (g *generator) Generate(filename string, tmpl *template.Template, args interface{}) error {
+	src, err := g.Format(tmpl, args)
+	if err != nil {
+		return err
+	}
+
+	if g.dryRun {
+		log.Printf("---- Content of file %s ----\n", filename)
+		log.Print(string(src))
+		fmt.Print("\n")
+		return nil
+	} else {
+		content, err := ioutil.ReadFile(filename)
+		if err == nil && bytes.Equal(content, src) {
+			return nil
+		} else {
+			return ioutil.WriteFile(filename, src, 0644)
+		}
+	}
+}
+
+// NewGenerator returns a new Generator
+func NewGenerator(dryRun bool) Generator {
+	return &generator{
+		dryRun: dryRun,
+	}
+}
diff --git a/modelgen/table.go b/modelgen/table.go
new file mode 100644
index 0000000..679d023
--- /dev/null
+++ b/modelgen/table.go
@@ -0,0 +1,248 @@
+package modelgen
+
+import (
+	"fmt"
+	"sort"
+	"strings"
+	"text/template"
+
+	"github.com/ovn-org/libovsdb/ovsdb"
+)
+
+// BASE_TABLE_TEMPLATE is the base table template
+// It includes the following other templates that can be overriden to customize the generated file
+// "header"
+// "preStructDefinitions"
+// "structComment"
+// "extraFields"
+// "extraTags"
+// "postStructDefinitions"
+// It is design to be used with a map[string] interface and some defined keys (see GetTableTemplateData)
+const BASE_TABLE_TEMPLATE = `{{ template "header" . }}
+
+package {{ index . "PackageName" }}
+
+{{ template "preStructDefinitions" }}
+{{ template "structComment" . }}
+type {{ index . "StructName" }} struct {
+{{ range index . "Fields" }}	{{ .Name }}  {{ .Type }} ` + "`" + `{{ .Tag }}{{ template "extraTags" . }}` + "`" + `
+{{ end }}
+{{ template "extraFields" . }}
+}
+
+{{ template "postStructDefinitions" . }}
+`
+
+// DEFAULT_HEADER_TEMPLATE is the default template for "header"
+const DEFAULT_HEADER_TEMPLATE = `{{ define "header" }}// Code generated by "libovsdb.modelgen"
+// DO NOT EDIT.{{ end }}
+`
+
+// DEFAULT_PRE_TABLE_TEMPLATE is the default template for "preStructDefinitions"
+const DEFAULT_PRE_TABLE_TEMPLATE = `{{ define "preStructDefinitions" }}{{ end }}`
+
+// DEFAULT_COMMENT_TEMPLATE is the default template for "structComment"
+const DEFAULT_COMMENT_TEMPLATE = `{{ define "structComment" }}// {{ index . "StructName" }} defines an object in {{ index . "TableName" }} table {{ end }} `
+
+// DEFAULT_EXTRA_TAGS_TEMPLATE is the default template for "extraTags"
+const DEFAULT_EXTRA_TAGS_TEMPLATE = `{{ define "extraTags" }}{{ end }}`
+
+// DEFAULT_EXTRA_FIELDS_TEMPLATE is the default template for "extraFields"
+const DEFAULT_EXTRA_FIELDS_TEMPLATE = `{{ define "extraFields" }}{{ end }}`
+
+// DEFAULT_POST_TABLE_TEMPLATE is the default template for "postStructDefinitions"
+const DEFAULT_POST_TABLE_TEMPLATE = `{{ define "postStructDefinitions" }}{{ end }}`
+
+// Field represents the field information
+type Field struct {
+	Column string
+	Name   string
+	Type   string
+	Tag    string
+}
+
+// GetTableTemplateData returns the TableTemplateData map. It has the following keys:
+// TableName: (string) the table name
+// PackageName : (string) the package name
+// StructName: (string) the struct name
+// Fields: []Field a list of Fields that the struct has
+func GetTableTemplateData(pkg, name string, table *ovsdb.TableSchema) map[string]interface{} {
+	data := map[string]interface{}{}
+	data["TableName"] = name
+	data["PackageName"] = pkg
+	data["StructName"] = StructName(name)
+	Fields := []Field{}
+
+	// First, add UUID
+	Fields = append(Fields,
+		Field{
+			Column: "_uuid",
+			Name:   "UUID",
+			Type:   "string",
+			Tag:    Tag("_uuid"),
+		})
+
+	// Map iteration order is random, so for predictable generation
+	// lets sort fields by name
+	var order sort.StringSlice
+	for columnName := range table.Columns {
+		order = append(order, columnName)
+	}
+	order.Sort()
+
+	for _, columnName := range order {
+		columnSchema := table.Columns[columnName]
+		Fields = append(Fields, Field{
+			Column: columnName,
+			Name:   FieldName(columnName),
+			Type:   FieldType(columnSchema),
+			Tag:    Tag(columnName),
+		})
+	}
+	data["Fields"] = Fields
+	return data
+}
+
+// NewTableTemplate returns a new TableTemplate and the TableTemplate data map
+// See BASE_TABLE_TEMPLATE to a detailed explanation of the possible ways this template can be expanded
+func NewTableTemplate(pkg string, name string, table *ovsdb.TableSchema) (*template.Template, map[string]interface{}) {
+	main, err := template.New(name).Parse(BASE_TABLE_TEMPLATE)
+	if err != nil {
+		panic(err)
+	}
+	tmpl, err := main.Parse(DEFAULT_HEADER_TEMPLATE)
+	if err != nil {
+		panic(err)
+	}
+	tmpl, err = tmpl.Parse(DEFAULT_PRE_TABLE_TEMPLATE)
+	if err != nil {
+		panic(err)
+	}
+	tmpl, err = tmpl.Parse(DEFAULT_POST_TABLE_TEMPLATE)
+	if err != nil {
+		panic(err)
+	}
+	tmpl, err = tmpl.Parse(DEFAULT_COMMENT_TEMPLATE)
+	if err != nil {
+		panic(err)
+	}
+	tmpl, err = tmpl.Parse(DEFAULT_EXTRA_FIELDS_TEMPLATE)
+	if err != nil {
+		panic(err)
+	}
+	_, err = tmpl.Parse(DEFAULT_EXTRA_TAGS_TEMPLATE)
+	if err != nil {
+		panic(err)
+	}
+	return main, GetTableTemplateData(pkg, name, table)
+}
+
+// FieldName returns the name of a column field
+func FieldName(column string) string {
+	return camelCase(column)
+}
+
+// StructName returns the name of the table struct
+func StructName(tableName string) string {
+	return strings.ReplaceAll(tableName, "_", "")
+}
+
+// FieldType returns the string representation of a column type
+func FieldType(column *ovsdb.ColumnSchema) string {
+	switch column.Type {
+	case ovsdb.TypeEnum:
+		return AtomicType(column.TypeObj.Key.Type)
+	case ovsdb.TypeMap:
+		return fmt.Sprintf("map[%s]%s", AtomicType(column.TypeObj.Key.Type),
+			AtomicType(column.TypeObj.Value.Type))
+	case ovsdb.TypeSet:
+		return fmt.Sprintf("[]%s", AtomicType(column.TypeObj.Key.Type))
+	default:
+		return AtomicType(column.Type)
+	}
+}
+
+// BasicType returns the string type of an AtomicType
+func AtomicType(atype string) string {
+	switch atype {
+	case ovsdb.TypeInteger:
+		return "int"
+	case ovsdb.TypeReal:
+		return "float64"
+	case ovsdb.TypeBoolean:
+		return "bool"
+	case ovsdb.TypeString:
+		return "string"
+	case ovsdb.TypeUUID:
+		return "string"
+	}
+	return ""
+}
+
+// Tag returns the Tag string of a column
+func Tag(column string) string {
+	return fmt.Sprintf("ovs:\"%s\"", column)
+}
+
+// Filename returns the filename of a table
+func FileName(table string) string {
+	return fmt.Sprintf("%s.go", strings.ToLower(table))
+}
+
+// common initialisms used in ovsdb schemas
+var initialisms = map[string]bool{
+	"ACL":   true,
+	"BFD":   true,
+	"CFM":   true,
+	"CT":    true,
+	"CVLAN": true,
+	"DNS":   true,
+	"DSCP":  true,
+	"ID":    true,
+	"IP":    true,
+	"IPFIX": true,
+	"LACP":  true,
+	"LLDP":  true,
+	"MAC":   true,
+	"MTU":   true,
+	"OVS":   true,
+	"QOS":   true,
+	"RSTP":  true,
+	"SSL":   true,
+	"STP":   true,
+	"TCP":   true,
+	"UDP":   true,
+	"UUID":  true,
+	"VLAN":  true,
+}
+
+func camelCase(field string) string {
+	s := strings.ToLower(field)
+	parts := strings.FieldsFunc(s, func(r rune) bool {
+		return r == '_' || r == '-'
+	})
+	if len(parts) > 1 {
+		s = ""
+		for _, p := range parts {
+			s += strings.Title(expandInitilaisms(p))
+		}
+	} else {
+		s = strings.Title(expandInitilaisms(s))
+	}
+	return s
+}
+
+func expandInitilaisms(s string) string {
+	// check initialisms
+	if u := strings.ToUpper(s); initialisms[u] {
+		return strings.ToUpper(s)
+	}
+	// check for plurals too
+	if strings.HasSuffix(s, "s") {
+		sub := s[:len(s)-1]
+		if u := strings.ToUpper(sub); initialisms[u] {
+			return strings.ToUpper(sub) + "s"
+		}
+	}
+	return s
+}


Test patch:
diff --git a/client/ovs_integration_test.go b/client/ovs_integration_test.go
index 5160528..66f3f85 100644
--- a/client/ovs_integration_test.go
+++ b/client/ovs_integration_test.go
@@ -2,7 +2,6 @@ package client
 
 import (
 	"bytes"
-	"context"
 	"log"
 	"os"
 	"testing"
@@ -72,14 +71,14 @@ func TestConnectIntegration(t *testing.T) {
 	go func() {
 		// Use Convenience params. Ignore failure even if any
 
-		_, err := Connect(context.Background(), cfg.Addr, defDB, nil)
+		_, err := Connect(cfg.Addr, defDB, nil)
 		if err != nil {
 			log.Println("Couldnt establish OVSDB connection with Defult params. No big deal")
 		}
 	}()
 
 	go func() {
-		ovs, err := Connect(context.Background(), cfg.Addr, defDB, nil)
+		ovs, err := Connect(cfg.Addr, defDB, nil)
 		if err != nil {
 			connected <- false
 		} else {
@@ -107,7 +106,7 @@ func TestListDbsIntegration(t *testing.T) {
 		t.Skip()
 	}
 
-	ovs, err := Connect(context.Background(), cfg.Addr, defDB, nil)
+	ovs, err := Connect(cfg.Addr, defDB, nil)
 	if err != nil {
 		t.Fatalf("Failed to Connect. error: %s", err)
 	}
@@ -142,7 +141,7 @@ func TestGetSchemasIntegration(t *testing.T) {
 		t.Skip()
 	}
 
-	ovs, err := Connect(context.Background(), cfg.Addr, defDB, nil)
+	ovs, err := Connect(cfg.Addr, defDB, nil)
 	if err != nil {
 		t.Fatalf("Failed to Connect. error: %s", err)
 	}
@@ -170,7 +169,7 @@ func TestInsertTransactIntegration(t *testing.T) {
 	}
 	SetConfig()
 
-	ovs, err := Connect(context.Background(), cfg.Addr, defDB, nil)
+	ovs, err := Connect(cfg.Addr, defDB, nil)
 	if err != nil {
 		t.Fatalf("Failed to Connect. error: %s", err)
 	}
@@ -228,7 +227,7 @@ func TestDeleteTransactIntegration(t *testing.T) {
 		t.Skip()
 	}
 
-	ovs, err := Connect(context.Background(), cfg.Addr, defDB, nil)
+	ovs, err := Connect(cfg.Addr, defDB, nil)
 	if err != nil {
 		t.Fatalf("Failed to Connect. error: %s", err)
 	}
@@ -273,7 +272,7 @@ func TestMonitorIntegration(t *testing.T) {
 		t.Skip()
 	}
 
-	ovs, err := Connect(context.Background(), cfg.Addr, defDB, nil)
+	ovs, err := Connect(cfg.Addr, defDB, nil)
 	if err != nil {
 		t.Fatalf("Failed to Connect. error: %s", err)
 	}
@@ -291,7 +290,7 @@ func TestNotifyIntegration(t *testing.T) {
 	}
 	SetConfig()
 
-	ovs, err := Connect(context.Background(), cfg.Addr, defDB, nil)
+	ovs, err := Connect(cfg.Addr, defDB, nil)
 	if err != nil {
 		t.Fatalf("Failed to Connect. error: %s", err)
 	}
@@ -322,7 +321,7 @@ func TestRemoveNotifyIntegration(t *testing.T) {
 	}
 	SetConfig()
 
-	ovs, err := Connect(context.Background(), cfg.Addr, defDB, nil)
+	ovs, err := Connect(cfg.Addr, defDB, nil)
 	if err != nil {
 		t.Fatalf("Failed to Connect. error: %s", err)
 	}
@@ -365,7 +364,7 @@ func TestTableSchemaValidationIntegration(t *testing.T) {
 	}
 	SetConfig()
 
-	ovs, err := Connect(context.Background(), cfg.Addr, defDB, nil)
+	ovs, err := Connect(cfg.Addr, defDB, nil)
 	if err != nil {
 		t.Fatalf("Failed to Connect. error: %s", err)
 	}
@@ -390,7 +389,7 @@ func TestColumnSchemaInRowValidationIntegration(t *testing.T) {
 	}
 	SetConfig()
 
-	ovs, err := Connect(context.Background(), cfg.Addr, defDB, nil)
+	ovs, err := Connect(cfg.Addr, defDB, nil)
 	if err != nil {
 		t.Fatalf("Failed to Connect. error: %s", err)
 	}
@@ -416,7 +415,7 @@ func TestColumnSchemaInMultipleRowsValidationIntegration(t *testing.T) {
 	}
 	SetConfig()
 
-	ovs, err := Connect(context.Background(), cfg.Addr, defDB, nil)
+	ovs, err := Connect(cfg.Addr, defDB, nil)
 	if err != nil {
 		t.Fatalf("Failed to Connect. error: %s", err)
 	}
@@ -448,7 +447,7 @@ func TestColumnSchemaValidationIntegration(t *testing.T) {
 		t.Skip()
 	}
 
-	ovs, err := Connect(context.Background(), cfg.Addr, defDB, nil)
+	ovs, err := Connect(cfg.Addr, defDB, nil)
 	if err != nil {
 		t.Fatalf("Failed to Connect. error: %s", err)
 	}
@@ -473,7 +472,7 @@ func TestMonitorCancelIntegration(t *testing.T) {
 	}
 	SetConfig()
 
-	ovs, err := Connect(context.Background(), cfg.Addr, defDB, nil)
+	ovs, err := Connect(cfg.Addr, defDB, nil)
 	if err != nil {
 		t.Fatalf("Failed to Connect. error: %s", err)
 	}
@@ -505,7 +504,7 @@ func TestInsertDuplicateTransactIntegration(t *testing.T) {
 	}
 	SetConfig()
 
-	ovs, err := Connect(context.Background(), cfg.Addr, defDB, nil)
+	ovs, err := Connect(cfg.Addr, defDB, nil)
 	if err != nil {
 		t.Fatalf("Failed to Connect. error: %s", err)
 	}
diff --git a/cmd/modelgen/table_test.go b/cmd/modelgen/table_test.go
deleted file mode 100644
index cdcd7cb..0000000
--- a/cmd/modelgen/table_test.go
+++ /dev/null
@@ -1,157 +0,0 @@
-package main
-
-import (
-	"encoding/json"
-	"testing"
-
-	"github.com/ovn-org/libovsdb/ovsdb"
-	"github.com/stretchr/testify/assert"
-)
-
-func TestNewTableGenerator(t *testing.T) {
-	rawSchema := []byte(`
-	{
-		"name": "AtomicDB",
-		"version": "0.0.0",
-		"tables": {
-			"atomicTable": {
-				"columns": {
-					"str": {
-						"type": "string"
-					},
-					"int": {
-						"type": "integer"
-					},
-					"float": {
-						"type": "real"
-					}
-				}
-			}
-		}
-	}`)
-
-	expected := `// Code generated by "ovsdb.modelgen"
-// DO NOT EDIT.
-
-package test
-
-// test defines an object in test table
-type test struct {
-	UUID  string  ` + "`" + `ovs:"_uuid"` + "`" + `
-	Float float64 ` + "`" + `ovs:"float"` + "`" + `
-	Int   int     ` + "`" + `ovs:"int"` + "`" + `
-	Str   string  ` + "`" + `ovs:"str"` + "`" + `
-}
-`
-
-	var schema ovsdb.DatabaseSchema
-	err := json.Unmarshal(rawSchema, &schema)
-	if err != nil {
-		t.Fatal(err)
-	}
-	table := schema.Tables["atomicTable"]
-	gen := NewTableGenerator(
-		"test",
-		"test",
-		&table,
-	)
-
-	for i := 0; i < 3; i++ {
-		b, err := gen.Format()
-		if err != nil {
-			t.Fatal(err)
-		}
-		assert.Equal(t, expected, string(b))
-	}
-}
-
-func TestFieldName(t *testing.T) {
-	cases := []struct {
-		in       string
-		expected string
-	}{
-		{"foo", "Foo"},
-	}
-	for _, tt := range cases {
-		if s := FieldName(tt.in); s != tt.expected {
-			t.Fatalf("got %s, wanted %s", s, tt.expected)
-		}
-	}
-
-}
-
-func TestStructName(t *testing.T) {
-	if s := StructName("Foo_Bar"); s != "FooBar" {
-		t.Fatalf("got %s, wanted FooBar", s)
-	}
-}
-
-/*
-TODO: Write Test
-func TestFieldType(t *testing.T) {
-	tests := []struct {
-		name string
-		in   *ovsdb.ColumnSchema
-		out  string
-	}
-	if got := FieldType(tt.args.column); got != tt.want {
-		t.Errorf("FieldType() = %v, want %v", got, tt.want)
-	}
-}
-*/
-
-func TestAtomicType(t *testing.T) {
-	tests := []struct {
-		name string
-		in   string
-		out  string
-	}{
-		{"IntegerToInt", ovsdb.TypeInteger, "int"},
-		{"RealToFloat", ovsdb.TypeReal, "float64"},
-		{"BooleanToBool", ovsdb.TypeBoolean, "bool"},
-		{"StringToString", ovsdb.TypeString, "string"},
-		{"UUIDToString", ovsdb.TypeUUID, "string"},
-		{"Invalid", "notAType", ""},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			if got := AtomicType(tt.in); got != tt.out {
-				t.Errorf("got %s, wanted %s", got, tt.out)
-			}
-		})
-	}
-}
-
-func TestTag(t *testing.T) {
-	if s := Tag("Foo_Bar"); s != "`ovs:\"Foo_Bar\"`" {
-		t.Fatalf("got %s, wanted `ovs:\"Foo_Bar\"`", s)
-	}
-}
-
-func TestFileName(t *testing.T) {
-	if s := FileName("foo"); s != "foo.go" {
-		t.Fatalf("got %s, wanted foo.go", s)
-	}
-}
-
-func TestCamelCase(t *testing.T) {
-	cases := []struct {
-		in       string
-		expected string
-	}{
-		{"foo_bar_baz", "FooBarBaz"},
-		{"foo-bar-baz", "FooBarBaz"},
-		{"foos-bars-bazs", "FoosBarsBazs"},
-		{"ip_port_mappings", "IPPortMappings"},
-		{"external_ids", "ExternalIDs"},
-		{"ip_prefix", "IPPrefix"},
-		{"dns_records", "DNSRecords"},
-		{"logical_ip", "LogicalIP"},
-		{"ip", "IP"},
-	}
-	for _, tt := range cases {
-		if s := camelCase(tt.in); s != tt.expected {
-			t.Fatalf("got %s, wanted %s", s, tt.expected)
-		}
-	}
-}
diff --git a/modelgen/table_test.go b/modelgen/table_test.go
new file mode 100644
index 0000000..c82f245
--- /dev/null
+++ b/modelgen/table_test.go
@@ -0,0 +1,295 @@
+package modelgen
+
+import (
+	"encoding/json"
+	"fmt"
+	"testing"
+	"text/template"
+
+	"github.com/ovn-org/libovsdb/ovsdb"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestNewTableTemplate(t *testing.T) {
+	rawSchema := []byte(`
+	{
+		"name": "AtomicDB",
+		"version": "0.0.0",
+		"tables": {
+			"atomicTable": {
+				"columns": {
+					"str": {
+						"type": "string"
+					},
+					"int": {
+						"type": "integer"
+					},
+					"float": {
+						"type": "real"
+					}
+				}
+			}
+		}
+	}`)
+
+	test := []struct {
+		name      string
+		extend    func(tmpl *template.Template, data map[string]interface{})
+		expected  string
+		err       bool
+		formatErr bool
+	}{
+		{
+			name: "normal",
+			expected: `// Code generated by "libovsdb.modelgen"
+// DO NOT EDIT.
+
+package test
+
+// test defines an object in test table
+type test struct {
+	UUID  string  ` + "`" + `ovs:"_uuid"` + "`" + `
+	Float float64 ` + "`" + `ovs:"float"` + "`" + `
+	Int   int     ` + "`" + `ovs:"int"` + "`" + `
+	Str   string  ` + "`" + `ovs:"str"` + "`" + `
+}
+`,
+		},
+		{
+			name: "add fields using same data",
+			extend: func(tmpl *template.Template, data map[string]interface{}) {
+				extra := `{{ define "extraFields" }} {{ range index . "Fields" }}	Other{{ .Name }}  {{ .Type }}
+{{end}} {{end}}`
+				_, err := tmpl.Parse(extra)
+				if err != nil {
+					panic(err)
+				}
+			},
+			expected: `// Code generated by "libovsdb.modelgen"
+// DO NOT EDIT.
+
+package test
+
+// test defines an object in test table
+type test struct {
+	UUID  string  ` + "`" + `ovs:"_uuid"` + "`" + `
+	Float float64 ` + "`" + `ovs:"float"` + "`" + `
+	Int   int     ` + "`" + `ovs:"int"` + "`" + `
+	Str   string  ` + "`" + `ovs:"str"` + "`" + `
+
+	OtherUUID  string
+	OtherFloat float64
+	OtherInt   int
+	OtherStr   string
+}
+`,
+		},
+		{
+			name: "add extra functions using extra data",
+			extend: func(tmpl *template.Template, data map[string]interface{}) {
+				extra := `{{ define "postStructDefinitions" }}
+func {{ index . "TestName" }} () string {
+    return "{{ index . "StructName" }}"
+} {{ end }}
+`
+				_, err := tmpl.Parse(extra)
+				if err != nil {
+					panic(err)
+				}
+				data["TestName"] = "TestFunc"
+			},
+			expected: `// Code generated by "libovsdb.modelgen"
+// DO NOT EDIT.
+
+package test
+
+// test defines an object in test table
+type test struct {
+	UUID  string  ` + "`" + `ovs:"_uuid"` + "`" + `
+	Float float64 ` + "`" + `ovs:"float"` + "`" + `
+	Int   int     ` + "`" + `ovs:"int"` + "`" + `
+	Str   string  ` + "`" + `ovs:"str"` + "`" + `
+}
+
+func TestFunc() string {
+	return "test"
+}
+`,
+		},
+		{
+			name:      "add bad code",
+			formatErr: true,
+			extend: func(tmpl *template.Template, data map[string]interface{}) {
+				extra := `{{ define "preStructDefinitions" }}
+WRONG FORMAT
+{{ end }}
+`
+				_, err := tmpl.Parse(extra)
+				if err != nil {
+					panic(err)
+				}
+			},
+		},
+	}
+
+	var schema ovsdb.DatabaseSchema
+	err := json.Unmarshal(rawSchema, &schema)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	for _, tt := range test {
+		t.Run(fmt.Sprintf("Table Test: %s", tt.name), func(t *testing.T) {
+			table := schema.Tables["atomicTable"]
+			templ, data := NewTableTemplate(
+				"test",
+				"test",
+				&table,
+			)
+			if tt.err {
+				assert.NotNil(t, err)
+			} else {
+				if tt.extend != nil {
+					tt.extend(templ, data)
+				}
+
+				for i := 0; i < 3; i++ {
+					g := NewGenerator(false)
+					b, err := g.Format(templ, data)
+					if tt.formatErr {
+						assert.NotNil(t, err)
+					} else {
+						assert.Nil(t, err)
+						assert.Equal(t, tt.expected, string(b))
+					}
+				}
+			}
+		})
+	}
+}
+
+func TestFieldName(t *testing.T) {
+	cases := []struct {
+		in       string
+		expected string
+	}{
+		{"foo", "Foo"},
+	}
+	for _, tt := range cases {
+		if s := FieldName(tt.in); s != tt.expected {
+			t.Fatalf("got %s, wanted %s", s, tt.expected)
+		}
+	}
+
+}
+
+func TestStructName(t *testing.T) {
+	if s := StructName("Foo_Bar"); s != "FooBar" {
+		t.Fatalf("got %s, wanted FooBar", s)
+	}
+}
+
+/*
+TODO: Write Test
+func TestFieldType(t *testing.T) {
+	tests := []struct {
+		name string
+		in   *ovsdb.ColumnSchema
+		out  string
+	}
+	if got := FieldType(tt.args.column); got != tt.want {
+		t.Errorf("FieldType() = %v, want %v", got, tt.want)
+	}
+}
+*/
+
+func TestAtomicType(t *testing.T) {
+	tests := []struct {
+		name string
+		in   string
+		out  string
+	}{
+		{"IntegerToInt", ovsdb.TypeInteger, "int"},
+		{"RealToFloat", ovsdb.TypeReal, "float64"},
+		{"BooleanToBool", ovsdb.TypeBoolean, "bool"},
+		{"StringToString", ovsdb.TypeString, "string"},
+		{"UUIDToString", ovsdb.TypeUUID, "string"},
+		{"Invalid", "notAType", ""},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := AtomicType(tt.in); got != tt.out {
+				t.Errorf("got %s, wanted %s", got, tt.out)
+			}
+		})
+	}
+}
+
+func TestTag(t *testing.T) {
+	if s := Tag("Foo_Bar"); s != "ovs:\"Foo_Bar\"" {
+		t.Fatalf("got %s, wanted ovs:\"Foo_Bar\"", s)
+	}
+}
+
+func TestFileName(t *testing.T) {
+	if s := FileName("foo"); s != "foo.go" {
+		t.Fatalf("got %s, wanted foo.go", s)
+	}
+}
+
+func TestCamelCase(t *testing.T) {
+	cases := []struct {
+		in       string
+		expected string
+	}{
+		{"foo_bar_baz", "FooBarBaz"},
+		{"foo-bar-baz", "FooBarBaz"},
+		{"foos-bars-bazs", "FoosBarsBazs"},
+		{"ip_port_mappings", "IPPortMappings"},
+		{"external_ids", "ExternalIDs"},
+		{"ip_prefix", "IPPrefix"},
+		{"dns_records", "DNSRecords"},
+		{"logical_ip", "LogicalIP"},
+		{"ip", "IP"},
+	}
+	for _, tt := range cases {
+		if s := camelCase(tt.in); s != tt.expected {
+			t.Fatalf("got %s, wanted %s", s, tt.expected)
+		}
+	}
+}
+
+func ExampleNewTableTemplate() {
+	schemaString := []byte(`
+	{
+		"name": "MyDB",
+		"version": "0.0.0",
+		"tables": {
+			"table1": {
+				"columns": {
+					"string_column": {
+						"type": "string"
+					},
+					"some_integer": {
+						"type": "integer"
+					},
+				}
+			}
+		}
+	}`)
+	var schema ovsdb.DatabaseSchema
+	_ = json.Unmarshal(schemaString, &schema)
+
+	base, data := NewTableTemplate("mypackage", "table1", schema.Table("table1"))
+
+	// Add a function at after the struct definition
+	// It can access the default data values plus any extra field that is added to data
+	_, _ = base.Parse(`{{define "postStructDefinitions"}}
+func (t {{ index . "StructName" }} {{ index . FuncName}}() string {
+    return "bar"
+}{{end}}`)
+	data["FuncName"] = "TestFunc"
+
+	gen := NewGenerator(false)
+	_ = gen.Generate("generated.go", base, data)
+}