Skip to content

Commit 7eaea1d

Browse files
Code Generation, Pt. 3: extension code generation (#259)
* add code generation for extensions * updated README to demonstrate extension building * fixup extension generation to allow multiple extensions in one pkg
1 parent ce7939b commit 7eaea1d

File tree

10 files changed

+519
-19
lines changed

10 files changed

+519
-19
lines changed

README.md

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ $ mkdir -p modules/test
154154
$ cd modules/test
155155
<create module.yaml>
156156
$ kraken module generate
157-
INFO[0000] module "test" generated at modules/test
157+
INFO[0000] module "test" generated at "."
158158
```
159159

160160
If we selected `with_config: true`, we will need to generate the protobuf code from the provided `proto` file. You can add some variables to `test.config.proto` first, then:
@@ -180,7 +180,27 @@ This will *only* update `test.mod.go`. Note: you may need to make manual change
180180

181181
### Generating an extension
182182

183-
Extension generation is not yet supported.
183+
Extensions are the least complicated to generate. The definition file for an extension looks like:
184+
185+
```yaml
186+
---
187+
package_url: github.com/kraken-hpc/kraken/test
188+
name: TestMessage
189+
custom_types:
190+
- "MySpecialType"
191+
```
192+
193+
This will generate an extension that will be referenced as `Test.TestMessage`. Note that we support generating multiple extensions in the same proto package using multiple definition files. E.g., you could also have `Test.AnotherMessage` defined in another file.
194+
195+
The procedure is similar to the others. The default file name for extensions is `extension.yaml`:
196+
197+
```bash
198+
$ mkdir -p extensions/test
199+
$ cd extensions/test
200+
<create extension.yaml>
201+
$ kraken extension generate
202+
INFO[0000] extension "Test.TestMessage" generated at "."
203+
```
184204

185205
## I want to get involved...
186206

generators/extension.go

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
/* extension.go: generators for making kraken extensions
2+
*
3+
* Author: J. Lowell Wofford <lowell@lanl.gov>
4+
*
5+
* This software is open source software available under the BSD-3 license.
6+
* Copyright (c) 2021, Triad National Security, LLC
7+
* See LICENSE file for details.
8+
*/
9+
10+
package generators
11+
12+
import (
13+
"flag"
14+
"fmt"
15+
"io/ioutil"
16+
"os"
17+
"path/filepath"
18+
"strings"
19+
"text/template"
20+
21+
"github.com/kraken-hpc/kraken/generators/templates"
22+
"github.com/kraken-hpc/kraken/lib/util"
23+
"gopkg.in/yaml.v2"
24+
)
25+
26+
type ExtensionConfig struct {
27+
// Global should not be specified on config; it will get overwritten regardless.
28+
Global *GlobalConfigType `yaml:""`
29+
// URL of package, e.g. "github.com/kraken-hpc/kraken/extensions/ipv4" (required)
30+
PackageUrl string `yaml:"package_url"`
31+
// Go package name (default: last element of PackageUrl)
32+
PackageName string `yaml:"package_name"`
33+
// Proto package name (default: camel case of PackageName)
34+
ProtoPackage string `yaml:"proto_name"`
35+
// Extension object name (required)
36+
// More than one extension object can be declared in the same package
37+
// Objects are referenced as ProtoName.Name, e.g. IPv4.IPv4OverEthernet
38+
Name string `yaml:"name"`
39+
// CustomTypes are an advanced feature and most people won't use them
40+
// Declaring a custom type will create a stub to develop a gogo customtype on
41+
// It will also include a commented example of linking a customtype in the proto
42+
CustomTypes []string `yaml:"custom_types"`
43+
// LowerName is intended for internal use only
44+
LowerName string `yaml:""`
45+
}
46+
47+
type CustomTypeConfig struct {
48+
Global *GlobalConfigType `yaml:"__global,omitempty"`
49+
Name string
50+
}
51+
52+
func extensionReadConfig(file string) (cfg *ExtensionConfig, err error) {
53+
cfgData, err := ioutil.ReadFile(file)
54+
if err != nil {
55+
return nil, fmt.Errorf("could not read config file %s: %v", file, err)
56+
}
57+
cfg = &ExtensionConfig{}
58+
if err = yaml.Unmarshal(cfgData, cfg); err != nil {
59+
return nil, fmt.Errorf("failed to parse config file %s: %v", file, err)
60+
}
61+
// now, apply sanity checks & defaults
62+
if cfg.PackageUrl == "" {
63+
return nil, fmt.Errorf("package_url must be specified")
64+
}
65+
if cfg.Name == "" {
66+
return nil, fmt.Errorf("name must be specified")
67+
}
68+
urlParts := util.URLToSlice(cfg.PackageUrl)
69+
name := urlParts[len(urlParts)-1]
70+
if cfg.PackageName == "" {
71+
cfg.PackageName = name
72+
}
73+
if cfg.ProtoPackage == "" {
74+
parts := strings.Split(cfg.PackageName, "_") // in the off chance _ is used
75+
cname := ""
76+
for _, s := range parts {
77+
cname += strings.Title(s)
78+
}
79+
cfg.ProtoPackage = cname
80+
}
81+
cfg.LowerName = strings.ToLower(cfg.Name)
82+
return
83+
}
84+
85+
func extensionCompileTemplate(tplFile, outDir string, cfg *ExtensionConfig) (target string, err error) {
86+
var tpl *template.Template
87+
var out *os.File
88+
parts := strings.Split(filepath.Base(tplFile), ".")
89+
if parts[0] == "template" {
90+
parts = append([]string{cfg.LowerName}, parts[1:len(parts)-1]...)
91+
} else {
92+
parts = parts[:len(parts)-1]
93+
}
94+
target = strings.Join(parts, ".")
95+
dest := filepath.Join(outDir, target)
96+
if _, err := os.Stat(dest); err == nil {
97+
if !Global.Force {
98+
return "", fmt.Errorf("refusing to overwrite file: %s (force not specified)", dest)
99+
}
100+
}
101+
tpl = template.New(tplFile)
102+
data, err := templates.Asset(tplFile)
103+
if err != nil {
104+
return
105+
}
106+
if tpl, err = tpl.Parse(string(data)); err != nil {
107+
return
108+
}
109+
if out, err = os.Create(dest); err != nil {
110+
return
111+
}
112+
defer out.Close()
113+
err = tpl.Execute(out, cfg)
114+
return
115+
}
116+
117+
func extensionCompileCustomtype(outDir string, cfg *CustomTypeConfig) (target string, err error) {
118+
var tpl *template.Template
119+
var out *os.File
120+
target = cfg.Name + ".type.go"
121+
dest := filepath.Join(outDir, target)
122+
if _, err := os.Stat(dest); err == nil {
123+
if !Global.Force {
124+
return "", fmt.Errorf("refusing to overwrite file: %s (force not specified)", dest)
125+
}
126+
}
127+
tpl = template.New("extension/customtype.type.go.tpl")
128+
data, err := templates.Asset("extension/customtype.type.go.tpl")
129+
if err != nil {
130+
return
131+
}
132+
if tpl, err = tpl.Parse(string(data)); err != nil {
133+
return
134+
}
135+
if out, err = os.Create(dest); err != nil {
136+
return
137+
}
138+
defer out.Close()
139+
err = tpl.Execute(out, cfg)
140+
return
141+
}
142+
143+
func ExtensionGenerate(global *GlobalConfigType, args []string) {
144+
Global = global
145+
Log = global.Log
146+
var configFile string
147+
var outDir string
148+
var help bool
149+
fs := flag.NewFlagSet("extension generate", flag.ExitOnError)
150+
fs.StringVar(&configFile, "c", "extension.yaml", "name of extension config file to use")
151+
fs.StringVar(&outDir, "o", ".", "output directory for extension")
152+
fs.BoolVar(&help, "h", false, "print this usage")
153+
fs.Usage = func() {
154+
fmt.Println("extension [gen]erate will generate a kraken extension based on an extension config.")
155+
fmt.Println("Usage: kraken <opts> extension generate [-h] [-c <config_file>] [-o <out_dir>]")
156+
fs.PrintDefaults()
157+
}
158+
fs.Parse(args)
159+
if help {
160+
fs.Usage()
161+
os.Exit(0)
162+
}
163+
if len(fs.Args()) != 0 {
164+
Log.Fatalf("unknown option: %s", fs.Args()[0])
165+
}
166+
stat, err := os.Stat(outDir)
167+
if err == nil && !stat.IsDir() {
168+
Log.Fatalf("output directory %s exists, but is not a directory", outDir)
169+
}
170+
if err != nil {
171+
// create the dir
172+
if err = os.MkdirAll(outDir, 0777); err != nil {
173+
Log.Fatalf("failed to create output directory %s: %v", outDir, err)
174+
}
175+
}
176+
cfg, err := extensionReadConfig(configFile)
177+
if err != nil {
178+
Log.Fatalf("failed to read config file: %v", err)
179+
}
180+
Log.Debugf("generating %s.%s with %d custom types",
181+
cfg.ProtoPackage,
182+
cfg.Name,
183+
len(cfg.CustomTypes))
184+
cfg.Global = global
185+
// Ok, that's all the prep, now fill/write the templates
186+
common := []string{
187+
"extension/template.proto.tpl",
188+
"extension/template.ext.go.tpl",
189+
"extension/template.go.tpl",
190+
}
191+
for _, f := range common {
192+
written, err := extensionCompileTemplate(f, outDir, cfg)
193+
if err != nil {
194+
Log.Fatalf("failed to write template: %v", err)
195+
}
196+
Log.Debugf("wrote file: %s", written)
197+
}
198+
if len(cfg.CustomTypes) > 0 {
199+
// generate customtypes
200+
ctypeDir := filepath.Join(outDir, "customtypes")
201+
stat, err := os.Stat(ctypeDir)
202+
if err == nil && !stat.IsDir() {
203+
Log.Fatalf("customtypes directory %s exists, but is not a directory", outDir)
204+
}
205+
if err != nil {
206+
// create the dir
207+
if err = os.MkdirAll(ctypeDir, 0777); err != nil {
208+
Log.Fatalf("failed to create customtypes directory %s: %v", outDir, err)
209+
}
210+
}
211+
for _, name := range cfg.CustomTypes {
212+
ctCfg := &CustomTypeConfig{
213+
Global: global,
214+
Name: name,
215+
}
216+
written, err := extensionCompileCustomtype(ctypeDir, ctCfg)
217+
if err != nil {
218+
Log.Fatalf("failed to write template: %v", err)
219+
}
220+
Log.Debugf("wrote file: %s", written)
221+
}
222+
}
223+
Log.Infof("extension \"%s.%s\" generated at %s", cfg.ProtoPackage, cfg.Name, outDir)
224+
}

generators/global.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
* See LICENSE file for details.
88
*/
99

10-
//go:generate go-bindata -o templates/templates.go -fs -pkg templates -prefix templates templates/app templates/module
10+
//go:generate go-bindata -o templates/templates.go -fs -pkg templates -prefix templates templates/app templates/module templates/extension
1111

1212
package generators
1313

generators/module.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,8 +142,8 @@ func ModuleGenerate(global *GlobalConfigType, args []string) {
142142
var outDir string
143143
var help bool
144144
fs := flag.NewFlagSet("module generate", flag.ExitOnError)
145-
fs.StringVar(&configFile, "c", "module.yaml", "name of app config file to use")
146-
fs.StringVar(&outDir, "o", ".", "output directory for app")
145+
fs.StringVar(&configFile, "c", "module.yaml", "name of module config file to use")
146+
fs.StringVar(&outDir, "o", ".", "output directory for module")
147147
fs.BoolVar(&help, "h", false, "print this usage")
148148
fs.Usage = func() {
149149
fmt.Println("module [gen]erate will generate a kraken module based on a module config.")
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// One-time generated by kraken {{ .Global.Version }}.
2+
// You probably want to edit this file to add values you need.
3+
// `kraken extension update` *will not* replace this file.
4+
// `kraken -f extension generate` *will* replace this file.
5+
6+
// The generated template is supposed to act more as an example than something useable out-of-the-box
7+
// You'll probably need to update most of these methods to make something functional.
8+
9+
package customtypes
10+
11+
import (
12+
"strconv"
13+
"strings"
14+
15+
"github.com/kraken-hpc/kraken/lib/types"
16+
)
17+
18+
var _ (types.ExtensionCustomType) = {{ .Name }}{}
19+
var _ (types.ExtensionCustomTypePtr) = (*{{ .Name }})(nil)
20+
21+
type {{ .Name }} struct {
22+
// TODO: you might want this type to be based on some other kind of data
23+
// if you change this, all of the methods will need to change appropriately
24+
s string
25+
}
26+
27+
// Marshal controls how the type is converted to binary
28+
func (t {{ .Name }}) Marshal() ([]byte, error) {
29+
return []byte(t.s), nil
30+
}
31+
32+
// MarshalTo is like Marshal, but writes the result to `data`
33+
func (t *{{ .Name }}) MarshalTo(data []byte) (int, error) {
34+
copy(data, []byte(t.s))
35+
return len(t.s), nil
36+
}
37+
38+
// Unmarshal converts an encoded []byte to populate the type
39+
func (t *{{ .Name }}) Unmarshal(data []byte) error {
40+
t.s = string(data)
41+
return nil
42+
}
43+
44+
// Size returns the size (encoded) of the type
45+
func (t *{{ .Name }}) Size() int {
46+
return len(t.s)
47+
}
48+
49+
// MarshalJSON writes the type to JSON format in the form of a byte string
50+
func (t {{ .Name }}) MarshalJSON() (j []byte, e error) {
51+
return []byte(strconv.Quote(t.s)), nil
52+
}
53+
54+
// UnmarshalJSON takes byte string JSON to populate the type
55+
func (t *{{ .Name }}) UnmarshalJSON(data []byte) error {
56+
t.s = strings.Trim(string(data), "\"'")
57+
return nil
58+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Generated by kraken {{- .Global.Version }}. DO NOT EDIT
2+
// To update, run `kraken extension update`
3+
package {{ .PackageName }}
4+
5+
import (
6+
"github.com/kraken-hpc/kraken/core"
7+
)
8+
9+
//////////////////////////
10+
// Boilerplate Methods //
11+
////////////////////////
12+
13+
14+
func (*{{ .Name }}) Name() string {
15+
return "type.googleapis.com/{{ .ProtoPackage }}.{{ .Name }}"
16+
}
17+
18+
func init() {
19+
core.Registry.RegisterExtension(&{{ .Name }}{})
20+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// One-time generated by kraken {{ .Global.Version }}.
2+
// You probably want to edit this file to add values you need.
3+
// `kraken extension update` *will not* replace this file.
4+
// `kraken -f extension generate` *will* replace this file.
5+
// To generate your config protobuf run `go generate` (requires `protoc` and `gogo-protobuf`)
6+
7+
//go:generate protoc -I $GOPATH/src -I . --gogo_out=plugins=grpc:. {{ .LowerName }}.proto
8+
9+
package {{ .PackageName }}
10+
11+
import (
12+
"github.com/kraken-hpc/kraken/lib/types"
13+
)
14+
15+
/////////////////////////
16+
// {{ .Name }} Object //
17+
///////////////////////
18+
19+
// This null declaration ensures that we adhere to the interface
20+
var _ types.Extension = (*{{ .Name }})(nil)
21+
22+
// New creates a new initialized instance of an extension
23+
func (i *{{ .Name }}) New() (m types.Message) {
24+
m = &{{ .Name }}{}
25+
// TODO: you can add extra actions that should happen when a new instance is created here
26+
// for instance, you could set default values.
27+
return
28+
}

0 commit comments

Comments
 (0)