-
Notifications
You must be signed in to change notification settings - Fork 13
/
Copy pathmain.go
199 lines (171 loc) · 6.36 KB
/
main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
/*
If this program is on the path of your machine you can invoke it in the following way:
protoc --plugin protoc-gen-goexample --goexample_out=output example.proto
Note that the `goexample` term is both the last portion of the binary build and the first portion of the out argument.
If you named your plugin `protoc-gen-poodle` then you would need to invoke that plugin by:
protoc --plugin protoc-gen-poodle --poodle_out=output example.proto
Parameters may be set for additional information
protoc --plugin protoc-gen-goexample --goexample_out=param1=value1,param2=value2:output example.proto
I believe an equivalent, cleaner, way to do this would be using the opt argument
protoc --plugin ./protoc-gen-goexample --goexample_out=output --goexample_opt=param1=value1,param2=value2 example.proto
Parameters shall apply to multiple files. See an example in generateCode for applying settings to individual message types using annotations.
*/
package main
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"os"
"strings"
"text/tabwriter"
"github.com/golang/protobuf/proto"
descriptor "github.com/golang/protobuf/protoc-gen-go/descriptor"
plugin "github.com/golang/protobuf/protoc-gen-go/plugin"
)
type GoExample struct {
Request *plugin.CodeGeneratorRequest
Response *plugin.CodeGeneratorResponse
Parameters map[string]string
}
type LocationMessage struct {
Location *descriptor.SourceCodeInfo_Location
Message *descriptor.DescriptorProto
LeadingComments []string
}
func (runner *GoExample) PrintParameters(w io.Writer) {
const padding = 3
tw := tabwriter.NewWriter(w, 0, 0, padding, ' ', tabwriter.TabIndent)
fmt.Fprintf(tw, "Parameters:\n")
for k, v := range runner.Parameters {
fmt.Fprintf(tw, "%s:\t%s\n", k, v)
}
fmt.Fprintln(tw, "")
tw.Flush()
}
func (runner *GoExample) getLocationMessage() map[string][]*LocationMessage {
ret := make(map[string][]*LocationMessage)
for index, filename := range runner.Request.FileToGenerate {
locationMessages := make([]*LocationMessage, 0)
proto := runner.Request.ProtoFile[index]
desc := proto.GetSourceCodeInfo()
locations := desc.GetLocation()
for _, location := range locations {
// I would encourage developers to read the documentation about paths as I might have misunderstood this
// I am trying to process message types which I understand to be `4` and only at the root level which I understand
// to be path len == 2
if len(location.GetPath()) > 2 {
continue
}
leadingComments := strings.Split(location.GetLeadingComments(), "\n")
if len(location.GetPath()) > 1 && location.GetPath()[0] == int32(4) {
message := proto.GetMessageType()[location.GetPath()[1]]
println(message.GetName())
locationMessages = append(locationMessages, &LocationMessage{
Message: message,
Location: location,
// Because we are only parsing messages here at the root level we will not get field comments
LeadingComments: leadingComments[:len(leadingComments)-1],
})
}
}
ret[filename] = locationMessages
}
return ret
}
func (runner *GoExample) CreateMarkdownFile(filename string, messages []*LocationMessage) error {
// Create a file and append it to the output files
var outfileName string
var content string
outfileName = strings.Replace(filename, ".proto", ".md", -1)
var mdFile plugin.CodeGeneratorResponse_File
mdFile.Name = &outfileName
var buf bytes.Buffer
buf.WriteString(fmt.Sprintf("# %s\n", outfileName))
for _, locationMessage := range messages {
buf.WriteString(fmt.Sprintf("\n## %s\n", locationMessage.Message.GetName()))
buf.WriteString(fmt.Sprintf("### %s\n", "Leading Comments"))
for _, comment := range locationMessage.LeadingComments {
buf.WriteString(fmt.Sprintf("%s\n", comment))
}
if len(locationMessage.Message.NestedType) > 0 {
buf.WriteString(fmt.Sprintf("### %s\n", "Nested Messages"))
for _, nestedMessage := range locationMessage.Message.NestedType {
buf.WriteString(fmt.Sprintf("#### %s\n", nestedMessage.GetName()))
buf.WriteString(fmt.Sprintf("#### %s\n", "Fields"))
for _, field := range nestedMessage.Field {
buf.WriteString(fmt.Sprintf("%s - %s\n", field.GetName(), field.GetLabel()))
}
}
}
for _, field := range locationMessage.Message.Field {
buf.WriteString(fmt.Sprintf("%s - %s\n", field.GetName(), field.GetLabel()))
}
}
content = buf.String()
mdFile.Content = &content
runner.Response.File = append(runner.Response.File, &mdFile)
os.Stderr.WriteString(fmt.Sprintf("Created File: %s", filename))
return nil
}
func (runner *GoExample) generateMessageMarkdown() error {
// This convenience method will return a structure of some types that I use
fileLocationMessageMap := runner.getLocationMessage()
for filename, locationMessages := range fileLocationMessageMap {
runner.CreateMarkdownFile(filename, locationMessages)
}
return nil
}
func (runner *GoExample) generateCode() error {
// Initialize the output file slice
files := make([]*plugin.CodeGeneratorResponse_File, 0)
runner.Response.File = files
{
err := runner.generateMessageMarkdown()
if err != nil {
return err
}
}
return nil
}
func main() {
// os.Stdin will contain data which will unmarshal into the following object:
// https://godoc.org/github.com/golang/protobuf/protoc-gen-go/plugin#CodeGeneratorRequest
req := &plugin.CodeGeneratorRequest{}
resp := &plugin.CodeGeneratorResponse{}
data, err := ioutil.ReadAll(os.Stdin)
if err != nil {
panic(err)
}
// You must use the requests unmarshal method to handle this type
if err := proto.Unmarshal(data, req); err != nil {
panic(err)
}
// You may require more data than what is in the proto files alone. There are a couple ways in which to do this.
// The first is by parameters. Another may be using leading comments in the proto files which I will cover in generateCode.
parameters := req.GetParameter()
// =grpc,import_path=mypackage:.
exampleRunner := &GoExample{
Request: req,
Response: resp,
Parameters: make(map[string]string),
}
groupkv := strings.Split(parameters, ",")
for _, element := range groupkv {
kv := strings.Split(element, "=")
if len(kv) > 1 {
exampleRunner.Parameters[kv[0]] = kv[1]
}
}
// Print the parameters for example
exampleRunner.PrintParameters(os.Stderr)
err = exampleRunner.generateCode()
if err != nil {
panic(err)
}
marshalled, err := proto.Marshal(resp)
if err != nil {
panic(err)
}
os.Stdout.Write(marshalled)
}