snivilised / cobrass

🐲 Assistant for cli applications using cobra
https://pkg.go.dev/github.com/snivilised/cobrass
MIT License
1 stars 0 forks source link

convert powershell code generators to use go generate #55

Open plastikfan opened 2 years ago

plastikfan commented 2 years ago

See: go generate design

The powershell script is a bit of a fly in the ointment for this project as it requires any dev to know powershell pretty well in order to update the generators. These 2 skill sets are not particularly correlated, so code generation should be done in the Golang domain

This is not really that important, but would be a good task to work on to learn another aspect of Go development.

For a code generation reference, see: go-i18n codegen

Template resources:

Tasks:

To help in viewing template files (*.tmpl), set the language of these files to go, set in vscode settings (this sets the colour highlighting according to go and also sets the file icon type):

  "files.associations": {
    "**/*.tmpl": "go"
  }
plastikfan commented 1 year ago

watched video: Generating code with go generate - Daniel Milde

Actually, this might be a better video: GopherCon 2021: Alan Shreve - Becoming the Metaprogrammer Real World Code Generation

plastikfan commented 1 year ago

Use the generate command in a comment. When you run go generate, it will search all the source files that contains the go generate command and run them

//go:generate <then some executable and args ....>

You can create aliases

//go:generate -command foo echo msg:

creates an alias named foo which runs echo on the parameter msg, eg

//go:generate foo hello

go generate ./...

msg: hello-world
plastikfan commented 1 year ago

I think we'll also need to combine templates with generate, ie the existing powershell script generate-options-validators.ps1 will be used as a reference of how to define the templates. There should also be some config too (not sure yet what it'll contain). Once we have the templates, we will also define a go based generator that will use the templates and generate the code.

Actually, one such piece of config data will be the output directory of where to generate the output. Initially, we will definitely need this to check what we generate matches the existing generated code auto files.

The go program we create, will be the program we reference from the go:generate comment statement.

plastikfan commented 1 year ago

Can use to generate mocks (gomock)

Some examples of code generation:

plastikfan commented 1 year ago

Actually, to implement this, we need to define a new project whose purpose is to generate the boilerplate source code and the accompanying tests. This separate program is then installed locally. We then add appropriate go:generate comments to invoke our program. So really, go:generate doesnt do that much for us other than to provide the plumbing to stich the parts together, there is no magic here! Look at stringer for an example.

plastikfan commented 1 year ago

a template is essentially map[string]*Template.

🎈 To define a template:

{{ template "footer }}

<p>
<div>my blog</div>
    {{ template "copyright" }}
</p>

{{ define "copyright" }}
Copyright 2020. All RIghts Reserved
{{ end }}

🎯 so when you execute {{ template "footer" }}

you are performing a map[string]*Template for "footer"

map[string]*Template{
    "hello.html": &Template{
        name: "hello.html"
        Tree: /* template body*/
    },
    "footer": &Template{
        name: "footer"
        Tree: /* template body*/
    },
    copyright": &Template{
        name: "copyright"
        Tree: /* template body*/
    },
}

🎈 There are 6 keywords: define, template, block, if/else, with, range

🎯 Variables

each template has access to 1 variable known as the . the . variable is whatever you pass into Execute/ExecuteTemplate

struct{
    Params: Params{
        Author: Author{
            LastName: "John",
            LastName: "Doe",
        },
    },
}
{{ . }}
{{ .Params.Author.LastName }}

the . refers to the struct above

map[string]interface{}{
    "Params": map[string]interface{}{
        "Author": map[string]interface{}{
            "FirstName": "John",
            "LastName": "Doe",
        }
    }
}
{{ . }}
{{ .Params.Author.LastName }}

the . refers to the map above ie, if you pass in the map into the Execute function

🧿 this all works via reflection

You can define your own variable but there are all relative to .

{{ define "greet" }}
    {{ $firstName := .Params.User.FirstName }} // declaration & assignment
    <p>hello, name is {{ $firstName }}</p>
    {{ firstName = "Joe" }} // reassignment
    <p>actually, my name is {{ $firstName }}</p>
{{ end }}

scope of a variable extends to the matching {{ end }} of the current block

🎈 dot and dollar

. refers to the current template variable, but so does $

--> the difference is that the dot changes depending on context

the dot changes inside {{ with }} and {{ range }} bocks

the dollar always points to the toplevel template variable, it never changes, where as the dot starts off at the top level, but can change

🎈 with

a bit like {{ if }}

usage:

{{ with }} {{ end }}

if is falsy, the block is not executed the dot '.' variable is set to inside the block

🎈 range

loops over a variable

usage:

{{ range <var> }} {{ else }} {{ end }}

\<var> must be array, slice, map or channel

if there are 0 element, the else block is executed

you can also specify an index variable in the range statement (assuming you pass in a slice/array/chan) (for map, $i would be the key):

{{ range $i, $user := .Users }}
... template content

{{ else }}
... template content
{{ end }}

🎯 This allows our template to generate dynamic content

🎈 functions methods and pipes

example functions:

(where var is string|slice|array|map|channel)

{{ index <slice> <num> ... }}
{{ index <map> <key> ... }}

🎈 templates can call user defined functions .Funcs(map[string]interface{})

template.
    New("t").
    Funcs(map[string]interface{}){
        "greet": func(name string) string { return "hello" + name}
    }).
    Parse(`{{ greet "bob" }}`)

funcs take either of these forms:

func() string
func(s string) string
func(args ...interface{}) interface{}

you can also return an error, when error occurs template engine will stop executing

func() (string, error)
func(s string) (string, error)
func(args ...interface{}) (string, interface{}) // !! this looks fishy, should the return be?: (error, interface{})

🎈 templates can call methods

type User struct { greeting string }

func (u User) Greet(name string) string {
    return u.greeting + " " + name
}

t, _ := template.New("t"),Parse(`{{ .User.Greet "bob" }}`)
t.Execute(os.Stdout, map[string]interface{}){
    "User": User{greeting: "hello"},
}

if a member of a struct is variable that is a func, then the template can't invoke it directory. It has to be invoked using the 'call' keyword

---> the results of 1 function can be piped into another using |

plastikfan commented 1 year ago

Another potential useful reference: Go Templates - Simple and Powerful

plastikfan commented 1 year ago

to print a value that appears inside quotes, you can use printf go template function with %q format specifier. The %q, is a placeholder of a value that will be quoted.