Repository: go-swagger/go-swagger
Pull request #2745 (2022-03-23 11:25:51)

Add support for template plugins
This PR adds the ability to use a [go plugin](https://pkg.go.dev/plugin) to add custom functions to the templating library when using custom templates.

In #2741 I added the sprig library to provide some templating functionality we needed for a project, but it may be valuable to have the ability for users to inject their own template functions without modifications to the go-swagger codebase.

Haven't checked that this compiles on Windows yet, and I'd like to write some tests around the plugin functionality itself but wanted to gauge interest and see if something like this could be a valuable contribution to the project. 

Note that plugins are not supported on Windows hence the splitting of the code into files with `windows` and `!windows` build tags.

casualjim: I think this is an interesting idea, I always wanted to do plugins for adding extra string formats.

What kind of uses would this unlock beyond what you can achieve with the template config file? I'm not against it, just curious what you have in mind
kevinbarbour: As far as I'm aware the template config file doesn't let you alter the way in which templates are processed, it just lets you configure which templates are used, the output filename, etc. But if I am missing something let me know.

With this plugin setup you are able to add additional functions to the funcmap for template processing. For a silly example, let's say you needed to use a `rot13` function in your templates.

You can write the following plugin:

```go
package main

import "text/template"

func rot13(s string) string {
	// implement me
	return s
}

func AddFuncs(f template.FuncMap) {
	f["rot13"] = rot13
}
```

Build it as a plugin, which will create `plugin.so`:
`go build -buildmode=plugin plugin.go`

Use your plugin function within one of your custom templates:
`{{ rot13 .Item }}`

And then run swagger generate using the plugin:
`swagger generate cli -t gen --spec=spec.json -T templates -p plugin.so`


I do not actually have a specific use-case that needs this at the moment, but the idea is that in the future if you need some additional templating functionality you don't need to add it to the go-swagger codebase like I did with #2741, you can just pull in your own plugins and modify them as needed.
kevinbarbour: Fixed up an issue with tests and got the windows build fixed. The failing AppVeyor tests are due to an unrelated issue (#2762).

@casualjim Curious if you have any further feedback on this, and my explained use case above. 

Patch:
diff --git a/cmd/swagger/commands/generate/shared.go b/cmd/swagger/commands/generate/shared.go
index 24c3b345..99cab6f9 100644
--- a/cmd/swagger/commands/generate/shared.go
+++ b/cmd/swagger/commands/generate/shared.go
@@ -112,7 +112,7 @@ func (w WithShared) getConfigFile() string {
 	return string(w.Shared.ConfigFile)
 }
 
-type sharedOptions struct {
+type sharedOptionsCommon struct {
 	Spec                  flags.Filename `long:"spec" short:"f" description:"the spec file to use (default swagger.{json,yml,yaml})" group:"shared"`
 	Target                flags.Filename `long:"target" short:"t" default:"./" description:"the base directory for generating the files" group:"shared"`
 	Template              string         `long:"template" description:"load contributed templates" choice:"stratoscale" group:"shared"`
@@ -127,7 +127,7 @@ type sharedOptions struct {
 	FlattenCmdOptions
 }
 
-func (s sharedOptions) apply(opts *generator.GenOpts) {
+func (s sharedOptionsCommon) apply(opts *generator.GenOpts) {
 	opts.Spec = string(s.Spec)
 	opts.Target = string(s.Target)
 	opts.Template = s.Template
diff --git a/cmd/swagger/commands/generate/sharedopts_nonwin.go b/cmd/swagger/commands/generate/sharedopts_nonwin.go
new file mode 100644
index 00000000..7f7c2518
--- /dev/null
+++ b/cmd/swagger/commands/generate/sharedopts_nonwin.go
@@ -0,0 +1,19 @@
+//go:build !windows
+// +build !windows
+
+package generate
+
+import (
+	"github.com/go-swagger/go-swagger/generator"
+	"github.com/jessevdk/go-flags"
+)
+
+type sharedOptions struct {
+	sharedOptionsCommon
+	TemplatePlugin flags.Filename `long:"template-plugin" short:"p" description:"the template plugin to use" group:"shared"`
+}
+
+func (s sharedOptions) apply(opts *generator.GenOpts) {
+	opts.TemplatePlugin = string(s.TemplatePlugin)
+	s.sharedOptionsCommon.apply(opts)
+}
diff --git a/cmd/swagger/commands/generate/sharedopts_win.go b/cmd/swagger/commands/generate/sharedopts_win.go
new file mode 100644
index 00000000..b2cf00f9
--- /dev/null
+++ b/cmd/swagger/commands/generate/sharedopts_win.go
@@ -0,0 +1,8 @@
+//go:build windows
+// +build windows
+
+package generate
+
+type sharedOptions struct {
+	sharedOptionsCommon
+}
diff --git a/generator/genopts_nonwin.go b/generator/genopts_nonwin.go
new file mode 100644
index 00000000..7e2a4f1c
--- /dev/null
+++ b/generator/genopts_nonwin.go
@@ -0,0 +1,50 @@
+//go:build !windows
+// +build !windows
+
+package generator
+
+import (
+	"log"
+	"plugin"
+	"text/template"
+)
+
+type GenOpts struct {
+	GenOptsCommon
+	TemplatePlugin string
+}
+
+func (g *GenOpts) setTemplates() error {
+	if g.TemplatePlugin != "" {
+		if err := g.templates.LoadPlugin(g.TemplatePlugin); err != nil {
+			return err
+		}
+	}
+
+	return g.GenOptsCommon.setTemplates()
+}
+
+// LoadPlugin will load the named plugin and inject its functions into the funcMap
+//
+// The plugin must implement a function matching the signature:
+// `func AddFuncs(f template.FuncMap)`
+// which can add any number of functions to the template repository funcMap.
+// Any existing sprig or go-swagger templates with the same name will be overridden.
+func (t *Repository) LoadPlugin(pluginPath string) error {
+	log.Printf("Attempting to load template plugin: %s", pluginPath)
+
+	p, err := plugin.Open(pluginPath)
+
+	if err != nil {
+		return err
+	}
+
+	f, err := p.Lookup("AddFuncs")
+
+	if err != nil {
+		return err
+	}
+
+	f.(func(template.FuncMap))(t.funcs)
+	return nil
+}
diff --git a/generator/genopts_win.go b/generator/genopts_win.go
new file mode 100644
index 00000000..6dfbc1b2
--- /dev/null
+++ b/generator/genopts_win.go
@@ -0,0 +1,12 @@
+//go:build windows
+// +build windows
+
+package generator
+
+type GenOpts struct {
+	GenOptsCommon
+}
+
+func (g *GenOpts) setTemplates() error {
+	return g.GenOptsCommon.setTemplates()
+}
diff --git a/generator/shared.go b/generator/shared.go
index 43cf883e..8c5138de 100644
--- a/generator/shared.go
+++ b/generator/shared.go
@@ -287,8 +287,8 @@ type SectionOpts struct {
 	Models          []TemplateOpts `mapstructure:"models"`
 }
 
-// GenOpts the options for the generator
-type GenOpts struct {
+// GenOptsCommon the options for the generator
+type GenOptsCommon struct {
 	IncludeModel               bool
 	IncludeValidator           bool
 	IncludeHandler             bool
@@ -763,7 +763,7 @@ func (g *GenOpts) renderDefinition(gg *GenDefinition) error {
 	return nil
 }
 
-func (g *GenOpts) setTemplates() error {
+func (g *GenOptsCommon) setTemplates() error {
 	if g.Template != "" {
 		// set contrib templates
 		if err := g.templates.LoadContrib(g.Template); err != nil {


Test patch:
diff --git a/generator/operation_test.go b/generator/operation_test.go
index edfc46ed..c45489bf 100644
--- a/generator/operation_test.go
+++ b/generator/operation_test.go
@@ -502,17 +502,19 @@ func TestBuilder_Issue1703(t *testing.T) {
 	dr := testCwd(t)
 
 	opts := &GenOpts{
-		Spec:              filepath.FromSlash("../fixtures/codegen/existing-model.yml"),
-		IncludeModel:      true,
-		IncludeHandler:    true,
-		IncludeParameters: true,
-		IncludeResponses:  true,
-		IncludeMain:       true,
-		APIPackage:        "restapi",
-		ModelPackage:      "model",
-		ServerPackage:     "server",
-		ClientPackage:     "client",
-		Target:            dr,
+		GenOptsCommon: GenOptsCommon{
+			Spec:              filepath.FromSlash("../fixtures/codegen/existing-model.yml"),
+			IncludeModel:      true,
+			IncludeHandler:    true,
+			IncludeParameters: true,
+			IncludeResponses:  true,
+			IncludeMain:       true,
+			APIPackage:        "restapi",
+			ModelPackage:      "model",
+			ServerPackage:     "server",
+			ClientPackage:     "client",
+			Target:            dr,
+		},
 	}
 	require.NoError(t, opts.EnsureDefaults())
 
@@ -539,17 +541,19 @@ func TestBuilder_Issue287(t *testing.T) {
 	dr := testCwd(t)
 
 	opts := &GenOpts{
-		Spec:              filepath.FromSlash("../fixtures/bugs/287/swagger.yml"),
-		IncludeModel:      true,
-		IncludeHandler:    true,
-		IncludeParameters: true,
-		IncludeResponses:  true,
-		IncludeMain:       true,
-		APIPackage:        "restapi",
-		ModelPackage:      "model",
-		ServerPackage:     "server",
-		ClientPackage:     "client",
-		Target:            dr,
+		GenOptsCommon: GenOptsCommon{
+			Spec:              filepath.FromSlash("../fixtures/bugs/287/swagger.yml"),
+			IncludeModel:      true,
+			IncludeHandler:    true,
+			IncludeParameters: true,
+			IncludeResponses:  true,
+			IncludeMain:       true,
+			APIPackage:        "restapi",
+			ModelPackage:      "model",
+			ServerPackage:     "server",
+			ClientPackage:     "client",
+			Target:            dr,
+		},
 	}
 	require.NoError(t, opts.EnsureDefaults())
 
@@ -574,18 +578,20 @@ func TestBuilder_Issue465(t *testing.T) {
 	dr := testCwd(t)
 
 	opts := &GenOpts{
-		Spec:              filepath.FromSlash("../fixtures/bugs/465/swagger.yml"),
-		IncludeModel:      true,
-		IncludeHandler:    true,
-		IncludeParameters: true,
-		IncludeResponses:  true,
-		IncludeMain:       true,
-		APIPackage:        "restapi",
-		ModelPackage:      "model",
-		ServerPackage:     "server",
-		ClientPackage:     "client",
-		Target:            dr,
-		IsClient:          true,
+		GenOptsCommon: GenOptsCommon{
+			Spec:              filepath.FromSlash("../fixtures/bugs/465/swagger.yml"),
+			IncludeModel:      true,
+			IncludeHandler:    true,
+			IncludeParameters: true,
+			IncludeResponses:  true,
+			IncludeMain:       true,
+			APIPackage:        "restapi",
+			ModelPackage:      "model",
+			ServerPackage:     "server",
+			ClientPackage:     "client",
+			Target:            dr,
+			IsClient:          true,
+		},
 	}
 	require.NoError(t, opts.EnsureDefaults())
 
@@ -610,17 +616,19 @@ func TestBuilder_Issue500(t *testing.T) {
 	dr := testCwd(t)
 
 	opts := &GenOpts{
-		Spec:              filepath.FromSlash("../fixtures/bugs/500/swagger.yml"),
-		IncludeModel:      true,
-		IncludeHandler:    true,
-		IncludeParameters: true,
-		IncludeResponses:  true,
-		IncludeMain:       true,
-		APIPackage:        "restapi",
-		ModelPackage:      "model",
-		ServerPackage:     "server",
-		ClientPackage:     "client",
-		Target:            dr,
+		GenOptsCommon: GenOptsCommon{
+			Spec:              filepath.FromSlash("../fixtures/bugs/500/swagger.yml"),
+			IncludeModel:      true,
+			IncludeHandler:    true,
+			IncludeParameters: true,
+			IncludeResponses:  true,
+			IncludeMain:       true,
+			APIPackage:        "restapi",
+			ModelPackage:      "model",
+			ServerPackage:     "server",
+			ClientPackage:     "client",
+			Target:            dr,
+		},
 	}
 	require.NoError(t, opts.EnsureDefaults())
 
@@ -699,19 +707,21 @@ func TestGenServerIssue890_ValidationTrueFlatteningTrue(t *testing.T) {
 	dr := testCwd(t)
 
 	opts := &GenOpts{
-		Spec:              filepath.FromSlash("../fixtures/bugs/890/swagger.yaml"),
-		IncludeModel:      true,
-		IncludeHandler:    true,
-		IncludeParameters: true,
-		IncludeResponses:  true,
-		IncludeMain:       true,
-		ValidateSpec:      true,
-		APIPackage:        "restapi",
-		ModelPackage:      "model",
-		ServerPackage:     "server",
-		ClientPackage:     "client",
-		Target:            dr,
-		IsClient:          true,
+		GenOptsCommon: GenOptsCommon{
+			Spec:              filepath.FromSlash("../fixtures/bugs/890/swagger.yaml"),
+			IncludeModel:      true,
+			IncludeHandler:    true,
+			IncludeParameters: true,
+			IncludeResponses:  true,
+			IncludeMain:       true,
+			ValidateSpec:      true,
+			APIPackage:        "restapi",
+			ModelPackage:      "model",
+			ServerPackage:     "server",
+			ClientPackage:     "client",
+			Target:            dr,
+			IsClient:          true,
+		},
 	}
 
 	// Testing Server Generation
@@ -760,18 +770,20 @@ func TestGenServerIssue890_ValidationFalseFlattenTrue(t *testing.T) {
 	dr := testCwd(t)
 
 	opts := &GenOpts{
-		Spec:              filepath.FromSlash("../fixtures/bugs/890/swagger.yaml"),
-		IncludeModel:      true,
-		IncludeHandler:    true,
-		IncludeParameters: true,
-		IncludeResponses:  true,
-		IncludeMain:       true,
-		APIPackage:        "restapi",
-		ModelPackage:      "model",
-		ServerPackage:     "server",
-		ClientPackage:     "client",
-		Target:            dr,
-		IsClient:          true,
+		GenOptsCommon: GenOptsCommon{
+			Spec:              filepath.FromSlash("../fixtures/bugs/890/swagger.yaml"),
+			IncludeModel:      true,
+			IncludeHandler:    true,
+			IncludeParameters: true,
+			IncludeResponses:  true,
+			IncludeMain:       true,
+			APIPackage:        "restapi",
+			ModelPackage:      "model",
+			ServerPackage:     "server",
+			ClientPackage:     "client",
+			Target:            dr,
+			IsClient:          true,
+		},
 	}
 
 	// Testing Server Generation
@@ -820,19 +832,21 @@ func TestGenServerIssue890_ValidationFalseFlattenFalse(t *testing.T) {
 	dr := testCwd(t)
 
 	opts := &GenOpts{
-		Spec:              filepath.FromSlash("../fixtures/bugs/890/swagger.yaml"),
-		IncludeModel:      true,
-		IncludeHandler:    true,
-		IncludeParameters: true,
-		IncludeResponses:  true,
-		IncludeMain:       true,
-		ValidateSpec:      false,
-		APIPackage:        "restapi",
-		ModelPackage:      "model",
-		ServerPackage:     "server",
-		ClientPackage:     "client",
-		Target:            dr,
-		IsClient:          true,
+		GenOptsCommon: GenOptsCommon{
+			Spec:              filepath.FromSlash("../fixtures/bugs/890/swagger.yaml"),
+			IncludeModel:      true,
+			IncludeHandler:    true,
+			IncludeParameters: true,
+			IncludeResponses:  true,
+			IncludeMain:       true,
+			ValidateSpec:      false,
+			APIPackage:        "restapi",
+			ModelPackage:      "model",
+			ServerPackage:     "server",
+			ClientPackage:     "client",
+			Target:            dr,
+			IsClient:          true,
+		},
 	}
 
 	// Testing Server Generation
@@ -870,19 +884,21 @@ func TestGenServerIssue890_ValidationTrueFlattenFalse(t *testing.T) {
 	dr := testCwd(t)
 
 	opts := &GenOpts{
-		Spec:              filepath.FromSlash("../fixtures/bugs/890/swagger.yaml"),
-		IncludeModel:      true,
-		IncludeHandler:    true,
-		IncludeParameters: true,
-		IncludeResponses:  true,
-		IncludeMain:       true,
-		ValidateSpec:      true,
-		APIPackage:        "restapi",
-		ModelPackage:      "model",
-		ServerPackage:     "server",
-		ClientPackage:     "client",
-		Target:            dr,
-		IsClient:          true,
+		GenOptsCommon: GenOptsCommon{
+			Spec:              filepath.FromSlash("../fixtures/bugs/890/swagger.yaml"),
+			IncludeModel:      true,
+			IncludeHandler:    true,
+			IncludeParameters: true,
+			IncludeResponses:  true,
+			IncludeMain:       true,
+			ValidateSpec:      true,
+			APIPackage:        "restapi",
+			ModelPackage:      "model",
+			ServerPackage:     "server",
+			ClientPackage:     "client",
+			Target:            dr,
+			IsClient:          true,
+		},
 	}
 
 	// Testing Server Generation
@@ -909,40 +925,44 @@ func TestGenServerWithTemplate(t *testing.T) {
 		{
 			name: "None_existing_contributor_template",
 			opts: &GenOpts{
-				Spec:              filepath.FromSlash("../fixtures/bugs/890/swagger.yaml"),
-				IncludeModel:      true,
-				IncludeHandler:    true,
-				IncludeParameters: true,
-				IncludeResponses:  true,
-				IncludeMain:       true,
-				ValidateSpec:      true,
-				APIPackage:        "restapi",
-				ModelPackage:      "model",
-				ServerPackage:     "server",
-				ClientPackage:     "client",
-				Target:            dr,
-				IsClient:          true,
-				Template:          "InvalidTemplate",
+				GenOptsCommon: GenOptsCommon{
+					Spec:              filepath.FromSlash("../fixtures/bugs/890/swagger.yaml"),
+					IncludeModel:      true,
+					IncludeHandler:    true,
+					IncludeParameters: true,
+					IncludeResponses:  true,
+					IncludeMain:       true,
+					ValidateSpec:      true,
+					APIPackage:        "restapi",
+					ModelPackage:      "model",
+					ServerPackage:     "server",
+					ClientPackage:     "client",
+					Target:            dr,
+					IsClient:          true,
+					Template:          "InvalidTemplate",
+				},
 			},
 			wantError: true,
 		},
 		{
 			name: "Existing_contributor",
 			opts: &GenOpts{
-				Spec:              filepath.FromSlash("../fixtures/bugs/890/swagger.yaml"),
-				IncludeModel:      true,
-				IncludeHandler:    true,
-				IncludeParameters: true,
-				IncludeResponses:  true,
-				IncludeMain:       true,
-				ValidateSpec:      true,
-				APIPackage:        "restapi",
-				ModelPackage:      "model",
-				ServerPackage:     "server",
-				ClientPackage:     "client",
-				Target:            dr,
-				IsClient:          true,
-				Template:          "stratoscale",
+				GenOptsCommon: GenOptsCommon{
+					Spec:              filepath.FromSlash("../fixtures/bugs/890/swagger.yaml"),
+					IncludeModel:      true,
+					IncludeHandler:    true,
+					IncludeParameters: true,
+					IncludeResponses:  true,
+					IncludeMain:       true,
+					ValidateSpec:      true,
+					APIPackage:        "restapi",
+					ModelPackage:      "model",
+					ServerPackage:     "server",
+					ClientPackage:     "client",
+					Target:            dr,
+					IsClient:          true,
+					Template:          "stratoscale",
+				},
 			},
 			wantError: false,
 		},
@@ -995,18 +1015,20 @@ func TestBuilder_Issue1214(t *testing.T) {
 	const any = `(.|\n)+`
 
 	opts := &GenOpts{
-		Spec:              filepath.FromSlash("../fixtures/bugs/1214/fixture-1214.yaml"),
-		IncludeModel:      true,
-		IncludeHandler:    true,
-		IncludeParameters: true,
-		IncludeResponses:  true,
-		IncludeMain:       true,
-		APIPackage:        "restapi",
-		ModelPackage:      "model",
-		ServerPackage:     "server",
-		ClientPackage:     "client",
-		Target:            dr,
-		IsClient:          false,
+		GenOptsCommon: GenOptsCommon{
+			Spec:              filepath.FromSlash("../fixtures/bugs/1214/fixture-1214.yaml"),
+			IncludeModel:      true,
+			IncludeHandler:    true,
+			IncludeParameters: true,
+			IncludeResponses:  true,
+			IncludeMain:       true,
+			APIPackage:        "restapi",
+			ModelPackage:      "model",
+			ServerPackage:     "server",
+			ClientPackage:     "client",
+			Target:            dr,
+			IsClient:          false,
+		},
 	}
 	require.NoError(t, opts.EnsureDefaults())
 
@@ -1170,14 +1192,16 @@ func TestGenerateServerOperation(t *testing.T) {
 		_ = os.RemoveAll(tgt)
 	}()
 	o := &GenOpts{
-		ValidateSpec:      false,
-		IncludeModel:      true,
-		IncludeHandler:    true,
-		IncludeParameters: true,
-		IncludeResponses:  true,
-		ModelPackage:      "models",
-		Spec:              fname,
-		Target:            tgt,
+		GenOptsCommon: GenOptsCommon{
+			ValidateSpec:      false,
+			IncludeModel:      true,
+			IncludeHandler:    true,
+			IncludeParameters: true,
+			IncludeResponses:  true,
+			ModelPackage:      "models",
+			Spec:              fname,
+			Target:            tgt,
+		},
 	}
 	require.NoError(t, o.EnsureDefaults())
 
@@ -1225,18 +1249,20 @@ func TestBuilder_Issue1646(t *testing.T) {
 	dr := testCwd(t)
 
 	opts := &GenOpts{
-		Spec:              filepath.FromSlash("../fixtures/bugs/1646/fixture-1646.yaml"),
-		IncludeModel:      true,
-		IncludeHandler:    true,
-		IncludeParameters: true,
-		IncludeResponses:  true,
-		IncludeMain:       true,
-		APIPackage:        "restapi",
-		ModelPackage:      "model",
-		ServerPackage:     "server",
-		ClientPackage:     "client",
-		Target:            dr,
-		IsClient:          false,
+		GenOptsCommon: GenOptsCommon{
+			Spec:              filepath.FromSlash("../fixtures/bugs/1646/fixture-1646.yaml"),
+			IncludeModel:      true,
+			IncludeHandler:    true,
+			IncludeParameters: true,
+			IncludeResponses:  true,
+			IncludeMain:       true,
+			APIPackage:        "restapi",
+			ModelPackage:      "model",
+			ServerPackage:     "server",
+			ClientPackage:     "client",
+			Target:            dr,
+			IsClient:          false,
+		},
 	}
 	err := opts.EnsureDefaults()
 	require.NoError(t, err)
@@ -1265,18 +1291,20 @@ func TestGenServer_StrictAdditionalProperties(t *testing.T) {
 	dr := testCwd(t)
 
 	opts := &GenOpts{
-		Spec:              filepath.FromSlash("../fixtures/codegen/strict-additional-properties.yml"),
-		IncludeModel:      true,
-		IncludeHandler:    true,
-		IncludeParameters: true,
-		IncludeResponses:  true,
-		IncludeMain:       true,
-		APIPackage:        "restapi",
-		ModelPackage:      "model",
-		ServerPackage:     "server",
-		ClientPackage:     "client",
-		Target:            dr,
-		IsClient:          false,
+		GenOptsCommon: GenOptsCommon{
+			Spec:              filepath.FromSlash("../fixtures/codegen/strict-additional-properties.yml"),
+			IncludeModel:      true,
+			IncludeHandler:    true,
+			IncludeParameters: true,
+			IncludeResponses:  true,
+			IncludeMain:       true,
+			APIPackage:        "restapi",
+			ModelPackage:      "model",
+			ServerPackage:     "server",
+			ClientPackage:     "client",
+			Target:            dr,
+			IsClient:          false,
+		},
 	}
 	err := opts.EnsureDefaults()
 	require.NoError(t, err)
@@ -1433,19 +1461,21 @@ func TestGenServer_2161_panic(t *testing.T) {
 	}()
 
 	opts := &GenOpts{
-		Spec:                       filepath.FromSlash("../fixtures/bugs/2161/fixture-2161-panic.json"),
-		IncludeModel:               true,
-		IncludeHandler:             true,
-		IncludeParameters:          true,
-		IncludeResponses:           true,
-		IncludeMain:                true,
-		APIPackage:                 "restapi",
-		ModelPackage:               "model",
-		ServerPackage:              "server",
-		ClientPackage:              "client",
-		Target:                     generated,
-		IsClient:                   false,
-		StrictAdditionalProperties: true,
+		GenOptsCommon: GenOptsCommon{
+			Spec:                       filepath.FromSlash("../fixtures/bugs/2161/fixture-2161-panic.json"),
+			IncludeModel:               true,
+			IncludeHandler:             true,
+			IncludeParameters:          true,
+			IncludeResponses:           true,
+			IncludeMain:                true,
+			APIPackage:                 "restapi",
+			ModelPackage:               "model",
+			ServerPackage:              "server",
+			ClientPackage:              "client",
+			Target:                     generated,
+			IsClient:                   false,
+			StrictAdditionalProperties: true,
+		},
 	}
 	require.NoError(t, opts.EnsureDefaults())
 
@@ -1486,17 +1516,19 @@ func TestGenServer_1659_Principal(t *testing.T) {
 		{
 			Title: "default",
 			Opts: &GenOpts{
-				Spec:              filepath.FromSlash("../fixtures/enhancements/1659/fixture-1659.yaml"),
-				IncludeHandler:    true,
-				IncludeParameters: true,
-				IncludeResponses:  true,
-				IncludeMain:       false,
-				APIPackage:        "restapi",
-				ModelPackage:      "models",
-				ServerPackage:     "server",
-				ClientPackage:     "client",
-				Target:            dr,
-				IsClient:          false,
+				GenOptsCommon: GenOptsCommon{
+					Spec:              filepath.FromSlash("../fixtures/enhancements/1659/fixture-1659.yaml"),
+					IncludeHandler:    true,
+					IncludeParameters: true,
+					IncludeResponses:  true,
+					IncludeMain:       false,
+					APIPackage:        "restapi",
+					ModelPackage:      "models",
+					ServerPackage:     "server",
+					ClientPackage:     "client",
+					Target:            dr,
+					IsClient:          false,
+				},
 			},
 			Expected: map[string][]string{
 				"configure": {
@@ -1538,18 +1570,20 @@ func TestGenServer_1659_Principal(t *testing.T) {
 		{
 			Title: "principal is struct",
 			Opts: &GenOpts{
-				Spec:              filepath.FromSlash("../fixtures/enhancements/1659/fixture-1659.yaml"),
-				IncludeHandler:    true,
-				IncludeParameters: true,
-				IncludeResponses:  true,
-				IncludeMain:       false,
-				APIPackage:        "restapi",
-				ModelPackage:      "models",
-				ServerPackage:     "server",
-				ClientPackage:     "client",
-				Target:            dr,
-				Principal:         "github.com/example/security.Principal",
-				IsClient:          false,
+				GenOptsCommon: GenOptsCommon{
+					Spec:              filepath.FromSlash("../fixtures/enhancements/1659/fixture-1659.yaml"),
+					IncludeHandler:    true,
+					IncludeParameters: true,
+					IncludeResponses:  true,
+					IncludeMain:       false,
+					APIPackage:        "restapi",
+					ModelPackage:      "models",
+					ServerPackage:     "server",
+					ClientPackage:     "client",
+					Target:            dr,
+					Principal:         "github.com/example/security.Principal",
+					IsClient:          false,
+				},
 			},
 			Expected: map[string][]string{
 				"configure": {
@@ -1592,19 +1626,21 @@ func TestGenServer_1659_Principal(t *testing.T) {
 		{
 			Title: "principal is interface",
 			Opts: &GenOpts{
-				Spec:                 filepath.FromSlash("../fixtures/enhancements/1659/fixture-1659.yaml"),
-				IncludeHandler:       true,
-				IncludeParameters:    true,
-				IncludeResponses:     true,
-				IncludeMain:          false,
-				APIPackage:           "restapi",
-				ModelPackage:         "models",
-				ServerPackage:        "server",
-				ClientPackage:        "client",
-				Target:               dr,
-				Principal:            "github.com/example/security.PrincipalIface",
-				IsClient:             false,
-				PrincipalCustomIface: true,
+				GenOptsCommon: GenOptsCommon{
+					Spec:                 filepath.FromSlash("../fixtures/enhancements/1659/fixture-1659.yaml"),
+					IncludeHandler:       true,
+					IncludeParameters:    true,
+					IncludeResponses:     true,
+					IncludeMain:          false,
+					APIPackage:           "restapi",
+					ModelPackage:         "models",
+					ServerPackage:        "server",
+					ClientPackage:        "client",
+					Target:               dr,
+					Principal:            "github.com/example/security.PrincipalIface",
+					IsClient:             false,
+					PrincipalCustomIface: true,
+				},
 			},
 			Expected: map[string][]string{
 				"configure": {
@@ -1647,19 +1683,21 @@ func TestGenServer_1659_Principal(t *testing.T) {
 		{
 			Title: "stratoscale: principal is struct",
 			Opts: &GenOpts{
-				Spec:              filepath.FromSlash("../fixtures/enhancements/1659/fixture-1659.yaml"),
-				IncludeHandler:    true,
-				IncludeParameters: true,
-				IncludeResponses:  true,
-				IncludeMain:       false,
-				APIPackage:        "restapi",
-				ModelPackage:      "models",
-				ServerPackage:     "server",
-				ClientPackage:     "client",
-				Target:            dr,
-				Principal:         "github.com/example/security.Principal",
-				IsClient:          false,
-				Template:          "stratoscale",
+				GenOptsCommon: GenOptsCommon{
+					Spec:              filepath.FromSlash("../fixtures/enhancements/1659/fixture-1659.yaml"),
+					IncludeHandler:    true,
+					IncludeParameters: true,
+					IncludeResponses:  true,
+					IncludeMain:       false,
+					APIPackage:        "restapi",
+					ModelPackage:      "models",
+					ServerPackage:     "server",
+					ClientPackage:     "client",
+					Target:            dr,
+					Principal:         "github.com/example/security.Principal",
+					IsClient:          false,
+					Template:          "stratoscale",
+				},
 			},
 			Expected: map[string][]string{
 				"configure": {
@@ -1694,20 +1732,22 @@ func TestGenServer_1659_Principal(t *testing.T) {
 		{
 			Title: "stratoscale: principal is interface",
 			Opts: &GenOpts{
-				Spec:                 filepath.FromSlash("../fixtures/enhancements/1659/fixture-1659.yaml"),
-				IncludeHandler:       true,
-				IncludeParameters:    true,
-				IncludeResponses:     true,
-				IncludeMain:          false,
-				APIPackage:           "restapi",
-				ModelPackage:         "models",
-				ServerPackage:        "server",
-				ClientPackage:        "client",
-				Target:               dr,
-				Principal:            "github.com/example/security.PrincipalIface",
-				IsClient:             false,
-				PrincipalCustomIface: true,
-				Template:             "stratoscale",
+				GenOptsCommon: GenOptsCommon{
+					Spec:                 filepath.FromSlash("../fixtures/enhancements/1659/fixture-1659.yaml"),
+					IncludeHandler:       true,
+					IncludeParameters:    true,
+					IncludeResponses:     true,
+					IncludeMain:          false,
+					APIPackage:           "restapi",
+					ModelPackage:         "models",
+					ServerPackage:        "server",
+					ClientPackage:        "client",
+					Target:               dr,
+					Principal:            "github.com/example/security.PrincipalIface",
+					IsClient:             false,
+					PrincipalCustomIface: true,
+					Template:             "stratoscale",
+				},
 			},
 			Expected: map[string][]string{
 				"configure": {
diff --git a/generator/shared_test.go b/generator/shared_test.go
index 815f3bf2..f3735a27 100644
--- a/generator/shared_test.go
+++ b/generator/shared_test.go
@@ -622,7 +622,7 @@ func TestResolvePrincipal(t *testing.T) {
 		fixture := toPin
 		t.Run(fixture.Title, func(t *testing.T) {
 			t.Parallel()
-			opts := &GenOpts{Principal: fixture.Principal}
+			opts := &GenOpts{GenOptsCommon: GenOptsCommon{Principal: fixture.Principal}}
 			err := opts.EnsureDefaults()
 			require.NoError(t, err)
 			alias, principal, target := opts.resolvePrincipal()
@@ -649,7 +649,9 @@ func TestDefaultImports(t *testing.T) {
 		{
 			Title: "with base import",
 			Opts: &GenOpts{
-				Principal: "ext.Principal",
+				GenOptsCommon: GenOptsCommon{
+					Principal: "ext.Principal",
+				},
 			},
 			Expected: map[string]string{
 				"ext":    "github.com/go-swagger/go-swagger/generator/ext",
@@ -659,7 +661,9 @@ func TestDefaultImports(t *testing.T) {
 		{
 			Title: "with full import",
 			Opts: &GenOpts{
-				Principal: "github.com/myproject/identity.Principal",
+				GenOptsCommon: GenOptsCommon{
+					Principal: "github.com/myproject/identity.Principal",
+				},
 			},
 			Expected: map[string]string{
 				"identity": "github.com/myproject/identity",
@@ -669,7 +673,9 @@ func TestDefaultImports(t *testing.T) {
 		{
 			Title: "with name conflict",
 			Opts: &GenOpts{
-				Principal: "github.com/myproject/middleware.Principal",
+				GenOptsCommon: GenOptsCommon{
+					Principal: "github.com/myproject/middleware.Principal",
+				},
 			},
 			Expected: map[string]string{
 				"auth":   "github.com/myproject/middleware",
@@ -679,7 +685,9 @@ func TestDefaultImports(t *testing.T) {
 		{
 			Title: "with name conflict (2)",
 			Opts: &GenOpts{
-				Principal: "github.com/myproject/principal.Principal",
+				GenOptsCommon: GenOptsCommon{
+					Principal: "github.com/myproject/principal.Principal",
+				},
 			},
 			Expected: map[string]string{
 				"auth":   "github.com/myproject/principal",
@@ -689,7 +697,9 @@ func TestDefaultImports(t *testing.T) {
 		{
 			Title: "alternate target for models",
 			Opts: &GenOpts{
-				ModelPackage: "target/bespoke",
+				GenOptsCommon: GenOptsCommon{
+					ModelPackage: "target/bespoke",
+				},
 			},
 			Expected: map[string]string{
 				"bespoke": "github.com/go-swagger/go-swagger/generator/target/bespoke",
@@ -698,7 +708,9 @@ func TestDefaultImports(t *testing.T) {
 		{
 			Title: "with existing models",
 			Opts: &GenOpts{
-				ExistingModels: "github.com/myproject/target/bespoke",
+				GenOptsCommon: GenOptsCommon{
+					ExistingModels: "github.com/myproject/target/bespoke",
+				},
 			},
 			Expected: map[string]string{
 				"models": "github.com/myproject/target/bespoke",
@@ -708,8 +720,10 @@ func TestDefaultImports(t *testing.T) {
 		{
 			Title: "relative principal, in dedicated package under generated target",
 			Opts: &GenOpts{
-				Principal:    "auth.Principal",
-				ModelPackage: "target/bespoke",
+				GenOptsCommon: GenOptsCommon{
+					Principal:    "auth.Principal",
+					ModelPackage: "target/bespoke",
+				},
 			},
 			Expected: map[string]string{
 				"bespoke": "github.com/go-swagger/go-swagger/generator/target/bespoke",
@@ -719,8 +733,10 @@ func TestDefaultImports(t *testing.T) {
 		{
 			Title: "relative principal in models (1)",
 			Opts: &GenOpts{
-				Principal:    "bespoke.Principal",
-				ModelPackage: "target/bespoke",
+				GenOptsCommon: GenOptsCommon{
+					Principal:    "bespoke.Principal",
+					ModelPackage: "target/bespoke",
+				},
 			},
 			Expected: map[string]string{
 				"bespoke": "github.com/go-swagger/go-swagger/generator/target/bespoke",
@@ -729,8 +745,10 @@ func TestDefaultImports(t *testing.T) {
 		{
 			Title: "relative principal in models (2)",
 			Opts: &GenOpts{
-				Principal:    "target/bespoke.Principal",
-				ModelPackage: "target/bespoke",
+				GenOptsCommon: GenOptsCommon{
+					Principal:    "target/bespoke.Principal",
+					ModelPackage: "target/bespoke",
+				},
 			},
 			Expected: map[string]string{
 				"bespoke": "github.com/go-swagger/go-swagger/generator/target/bespoke",
@@ -740,8 +758,10 @@ func TestDefaultImports(t *testing.T) {
 			Title: "relative principal: not detected",
 			// NOTE: this case will probably not build: no way to determine the user intent
 			Opts: &GenOpts{
-				Principal:    "target/auth.Principal",
-				ModelPackage: "target/models",
+				GenOptsCommon: GenOptsCommon{
+					Principal:    "target/auth.Principal",
+					ModelPackage: "target/models",
+				},
 			},
 			Expected: map[string]string{
 				"models": "github.com/go-swagger/go-swagger/generator/target/models",