feat
This commit is contained in:
commit
ca3ea1900d
|
@ -0,0 +1,15 @@
|
||||||
|
# protoc-gen-markdown
|
||||||
|
|
||||||
|
## install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go install git.apinb.com/bsm-tools/protoc-gen-markdown
|
||||||
|
```
|
||||||
|
|
||||||
|
## generate markdown
|
||||||
|
|
||||||
|
```bash
|
||||||
|
protoc --markdown_out=Mhello.proto=./:. ./hello.proto
|
||||||
|
# set path prefix to /api
|
||||||
|
protoc --markdown_out=Mhello.proto=./,prefix=/api:. ./hello.proto
|
||||||
|
```
|
|
@ -0,0 +1,8 @@
|
||||||
|
module git.apinb.com/bsm-tools/protoc-gen-markdown
|
||||||
|
|
||||||
|
go 1.24
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/ditashi/jsbeautifier-go v0.0.0-20141206144643-2520a8026a9c
|
||||||
|
google.golang.org/protobuf v1.27.1
|
||||||
|
)
|
|
@ -0,0 +1,10 @@
|
||||||
|
github.com/ditashi/jsbeautifier-go v0.0.0-20141206144643-2520a8026a9c h1:+Zo5Ca9GH0RoeVZQKzFJcTLoAixx5s5Gq3pTIS+n354=
|
||||||
|
github.com/ditashi/jsbeautifier-go v0.0.0-20141206144643-2520a8026a9c/go.mod h1:HJGU9ULdREjOcVGZVPB5s6zYmHi1RxzT71l2wQyLmnE=
|
||||||
|
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||||
|
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||||
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||||
|
google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=
|
||||||
|
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
|
@ -0,0 +1,130 @@
|
||||||
|
# Demo
|
||||||
|
|
||||||
|
Service demo.
|
||||||
|
|
||||||
|
All leading comments will be copied to markdown.
|
||||||
|
|
||||||
|
- [/api/demo.Demo/Echo1](#apidemodemoecho1)
|
||||||
|
- [/api/demo.Demo/Echo2](#apidemodemoecho2)
|
||||||
|
|
||||||
|
## /api/demo.Demo/Echo1
|
||||||
|
|
||||||
|
Rpc demo
|
||||||
|
|
||||||
|
All leading comments will be copied to markdown.
|
||||||
|
|
||||||
|
|
||||||
|
### Request
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
// boolean value demo
|
||||||
|
a: false, // type<bool>
|
||||||
|
// 32 bit int value demo
|
||||||
|
b: 0, // type<int32>
|
||||||
|
// 64 bit int value demo
|
||||||
|
c: "0", // type<int64>, stored as string
|
||||||
|
// float value demo
|
||||||
|
d: 0.0, // type<double>
|
||||||
|
// string value demo
|
||||||
|
e: "", // type<string>
|
||||||
|
// bytes value demo
|
||||||
|
f: "", // type<bytes>, stored as base64 string
|
||||||
|
// message value demo
|
||||||
|
g: {
|
||||||
|
// string list value demo
|
||||||
|
a: [""], // list<string>
|
||||||
|
// map value demo
|
||||||
|
b: {
|
||||||
|
"0": ""
|
||||||
|
}, // map<int32,string>
|
||||||
|
// self reference value demo
|
||||||
|
c: {
|
||||||
|
a: {}, // type<Bar>, self-referenced message will be displayed as {}
|
||||||
|
}, // type<Baz>
|
||||||
|
// enum value demo
|
||||||
|
d: "Unknown", // enum<Unknown,Male,Female>
|
||||||
|
// message list value demo
|
||||||
|
e: [{
|
||||||
|
name: "", // type<string>
|
||||||
|
age: 0, // type<int32>
|
||||||
|
}], // list<Person>
|
||||||
|
}, // type<Bar>
|
||||||
|
// imported message value demo
|
||||||
|
h: {
|
||||||
|
// Represents seconds of UTC time since Unix epoch
|
||||||
|
// 1970-01-01T00:00:00Z. Must be from 0001-01-01T00:00:00Z to
|
||||||
|
// 9999-12-31T23:59:59Z inclusive.
|
||||||
|
seconds: "0", // type<int64>
|
||||||
|
// Non-negative fractions of a second at nanosecond resolution. Negative
|
||||||
|
// second values with fractions must still have non-negative nanos values
|
||||||
|
// that count forward in time. Must be from 0 to 999,999,999
|
||||||
|
// inclusive.
|
||||||
|
nanos: 0, // type<int32>
|
||||||
|
}, // type<Timestamp>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Reply
|
||||||
|
```javascript
|
||||||
|
{}
|
||||||
|
```
|
||||||
|
## /api/demo.Demo/Echo2
|
||||||
|
|
||||||
|
Another rpc demo
|
||||||
|
|
||||||
|
|
||||||
|
### Request
|
||||||
|
```javascript
|
||||||
|
{}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Reply
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
// boolean value demo
|
||||||
|
a: false, // type<bool>
|
||||||
|
// 32 bit int value demo
|
||||||
|
b: 0, // type<int32>
|
||||||
|
// 64 bit int value demo
|
||||||
|
c: "0", // type<int64>, stored as string
|
||||||
|
// float value demo
|
||||||
|
d: 0.0, // type<double>
|
||||||
|
// string value demo
|
||||||
|
e: "", // type<string>
|
||||||
|
// bytes value demo
|
||||||
|
f: "", // type<bytes>, stored as base64 string
|
||||||
|
// message value demo
|
||||||
|
g: {
|
||||||
|
// string list value demo
|
||||||
|
a: [""], // list<string>
|
||||||
|
// map value demo
|
||||||
|
b: {
|
||||||
|
"0": ""
|
||||||
|
}, // map<int32,string>
|
||||||
|
// self reference value demo
|
||||||
|
c: {
|
||||||
|
a: {}, // type<Bar>, self-referenced message will be displayed as {}
|
||||||
|
}, // type<Baz>
|
||||||
|
// enum value demo
|
||||||
|
d: "Unknown", // enum<Unknown,Male,Female>
|
||||||
|
// message list value demo
|
||||||
|
e: [{
|
||||||
|
name: "", // type<string>
|
||||||
|
age: 0, // type<int32>
|
||||||
|
}], // list<Person>
|
||||||
|
}, // type<Bar>
|
||||||
|
// imported message value demo
|
||||||
|
h: {
|
||||||
|
// Represents seconds of UTC time since Unix epoch
|
||||||
|
// 1970-01-01T00:00:00Z. Must be from 0001-01-01T00:00:00Z to
|
||||||
|
// 9999-12-31T23:59:59Z inclusive.
|
||||||
|
seconds: "0", // type<int64>
|
||||||
|
// Non-negative fractions of a second at nanosecond resolution. Negative
|
||||||
|
// second values with fractions must still have non-negative nanos values
|
||||||
|
// that count forward in time. Must be from 0 to 999,999,999
|
||||||
|
// inclusive.
|
||||||
|
nanos: 0, // type<int32>
|
||||||
|
}, // type<Timestamp>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
|
@ -0,0 +1,65 @@
|
||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package demo;
|
||||||
|
|
||||||
|
import "google/protobuf/empty.proto";
|
||||||
|
import "google/protobuf/timestamp.proto";
|
||||||
|
|
||||||
|
// Service demo.
|
||||||
|
//
|
||||||
|
// All leading comments will be copied to markdown.
|
||||||
|
service Demo {
|
||||||
|
// Rpc demo
|
||||||
|
//
|
||||||
|
// All leading comments will be copied to markdown.
|
||||||
|
rpc Echo1(Foo) returns (google.protobuf.Empty) {}
|
||||||
|
// Another rpc demo
|
||||||
|
rpc Echo2(google.protobuf.Empty) returns (Foo) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Leading comments of message will be ignored.
|
||||||
|
message Foo {
|
||||||
|
// boolean value demo
|
||||||
|
bool a = 1;
|
||||||
|
// 32 bit int value demo
|
||||||
|
int32 b = 2;
|
||||||
|
// 64 bit int value demo
|
||||||
|
int64 c = 3; // stored as string
|
||||||
|
// float value demo
|
||||||
|
double d = 4;
|
||||||
|
// string value demo
|
||||||
|
string e = 5;
|
||||||
|
// bytes value demo
|
||||||
|
bytes f = 6; // stored as base64 string
|
||||||
|
// message value demo
|
||||||
|
Bar g = 7;
|
||||||
|
// imported message value demo
|
||||||
|
google.protobuf.Timestamp h = 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Bar {
|
||||||
|
// string list value demo
|
||||||
|
repeated string a = 1;
|
||||||
|
// map value demo
|
||||||
|
map<int32,string> b = 2;
|
||||||
|
// self reference value demo
|
||||||
|
Baz c = 3;
|
||||||
|
enum Sex {
|
||||||
|
Unknown = 0;
|
||||||
|
Male = 1;
|
||||||
|
Female = 2;
|
||||||
|
}
|
||||||
|
// enum value demo
|
||||||
|
Sex d = 4;
|
||||||
|
// message list value demo
|
||||||
|
repeated Person e = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Baz {
|
||||||
|
Bar a = 1; // self-referenced message will be displayed as {}
|
||||||
|
}
|
||||||
|
|
||||||
|
message Person {
|
||||||
|
string name = 1;
|
||||||
|
int32 age = 2;
|
||||||
|
}
|
|
@ -0,0 +1,207 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/ditashi/jsbeautifier-go/jsbeautifier"
|
||||||
|
"google.golang.org/protobuf/compiler/protogen"
|
||||||
|
"google.golang.org/protobuf/reflect/protoreflect"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
g := markdown{}
|
||||||
|
|
||||||
|
var flags flag.FlagSet
|
||||||
|
|
||||||
|
flags.StringVar(&g.Prefix, "prefix", "/", "API path prefix")
|
||||||
|
|
||||||
|
protogen.Options{
|
||||||
|
ParamFunc: flags.Set,
|
||||||
|
}.Run(g.Generate)
|
||||||
|
}
|
||||||
|
|
||||||
|
type markdown struct {
|
||||||
|
Prefix string
|
||||||
|
|
||||||
|
msgs []protoreflect.FullName
|
||||||
|
}
|
||||||
|
|
||||||
|
func (md *markdown) in(m *protogen.Message) {
|
||||||
|
md.msgs = append(md.msgs, m.Desc.FullName())
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (md *markdown) out() {
|
||||||
|
md.msgs = md.msgs[0 : len(md.msgs)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (md *markdown) recursive(m *protogen.Message) bool {
|
||||||
|
for _, n := range md.msgs {
|
||||||
|
if n == m.Desc.FullName() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (md *markdown) Generate(plugin *protogen.Plugin) error {
|
||||||
|
// The service should be defined in the last file.
|
||||||
|
// All other files are imported by the service proto.
|
||||||
|
for _, f := range plugin.Files {
|
||||||
|
if len(f.Services) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
fname := f.GeneratedFilenamePrefix + ".md"
|
||||||
|
t := plugin.NewGeneratedFile(fname, f.GoImportPath)
|
||||||
|
|
||||||
|
for _, s := range f.Services {
|
||||||
|
t.P("# ", s.Desc.Name())
|
||||||
|
t.P()
|
||||||
|
t.P(string(s.Comments.Leading))
|
||||||
|
|
||||||
|
for _, m := range s.Methods {
|
||||||
|
name := string(m.Desc.FullName())
|
||||||
|
api := md.api(name)
|
||||||
|
anchor := md.anchor(api)
|
||||||
|
|
||||||
|
t.P(fmt.Sprintf("- [%s](#%s)", api, anchor))
|
||||||
|
}
|
||||||
|
t.P()
|
||||||
|
for _, m := range s.Methods {
|
||||||
|
n := string(m.Desc.FullName())
|
||||||
|
t.P("## ", md.api(n))
|
||||||
|
t.P()
|
||||||
|
t.P(string(m.Comments.Leading))
|
||||||
|
t.P()
|
||||||
|
t.P("### Request")
|
||||||
|
t.P("```javascript")
|
||||||
|
t.P(md.jsDocForMessage(m.Input))
|
||||||
|
t.P("```")
|
||||||
|
t.P()
|
||||||
|
t.P("### Reply")
|
||||||
|
t.P("```javascript")
|
||||||
|
t.P(md.jsDocForMessage(m.Output))
|
||||||
|
t.P("```")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.P()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (md *markdown) api(s string) string {
|
||||||
|
i := strings.LastIndex(s, ".")
|
||||||
|
|
||||||
|
prefix := strings.Trim(md.Prefix, "/")
|
||||||
|
if prefix != "" {
|
||||||
|
prefix = "/" + prefix
|
||||||
|
}
|
||||||
|
|
||||||
|
return prefix + "/" + s[:i] + "/" + s[i+1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (md *markdown) anchor(s string) string {
|
||||||
|
s = strings.ToLower(s)
|
||||||
|
s = strings.ReplaceAll(s, ".", "")
|
||||||
|
s = strings.ReplaceAll(s, "/", "")
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (md *markdown) scalarDefaultValue(field *protogen.Field) string {
|
||||||
|
switch field.Desc.Kind() {
|
||||||
|
case protoreflect.StringKind, protoreflect.BytesKind:
|
||||||
|
return `""`
|
||||||
|
case protoreflect.Fixed64Kind, protoreflect.Int64Kind,
|
||||||
|
protoreflect.Sfixed64Kind, protoreflect.Sint64Kind,
|
||||||
|
protoreflect.Uint64Kind:
|
||||||
|
return `"0"`
|
||||||
|
case protoreflect.DoubleKind, protoreflect.FloatKind:
|
||||||
|
return `0.0`
|
||||||
|
case protoreflect.BoolKind:
|
||||||
|
return "false"
|
||||||
|
default:
|
||||||
|
return "0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (md *markdown) jsDocForField(field *protogen.Field) string {
|
||||||
|
js := field.Comments.Leading.String()
|
||||||
|
js += string(field.Desc.Name()) + ":"
|
||||||
|
|
||||||
|
var vv string
|
||||||
|
var vt string
|
||||||
|
if field.Desc.IsMap() {
|
||||||
|
vf := field.Message.Fields[1]
|
||||||
|
if m := vf.Message; m != nil {
|
||||||
|
vv = md.jsDocForMessage(m)
|
||||||
|
vt = string(vf.Message.Desc.FullName())
|
||||||
|
} else {
|
||||||
|
vv = md.scalarDefaultValue(vf)
|
||||||
|
vt = vf.Desc.Kind().String()
|
||||||
|
}
|
||||||
|
kf := field.Desc.MapKey()
|
||||||
|
vv = fmt.Sprintf("{\n\"%s\":%s}", kf.Default().String(), vv)
|
||||||
|
vt = fmt.Sprintf("%s,%s", kf.Kind().String(), vt)
|
||||||
|
} else if field.Message != nil {
|
||||||
|
if md.recursive(field.Message) {
|
||||||
|
vv = "{}"
|
||||||
|
} else {
|
||||||
|
vv = md.jsDocForMessage(field.Message)
|
||||||
|
}
|
||||||
|
vt = string(field.Message.Desc.Name())
|
||||||
|
} else if field.Enum != nil {
|
||||||
|
vv = `"` + string(field.Enum.Values[0].Desc.Name()) + `"`
|
||||||
|
vt = ""
|
||||||
|
for i, v := range field.Enum.Values {
|
||||||
|
if i > 0 {
|
||||||
|
vt += ","
|
||||||
|
}
|
||||||
|
vt += string(v.Desc.Name())
|
||||||
|
}
|
||||||
|
} else if field.Oneof != nil {
|
||||||
|
vv = `"Does Not Support OneOf"`
|
||||||
|
} else {
|
||||||
|
vv = md.scalarDefaultValue(field)
|
||||||
|
vt = field.Desc.Kind().String()
|
||||||
|
}
|
||||||
|
|
||||||
|
if field.Desc.IsList() {
|
||||||
|
js += fmt.Sprintf("[%s], // list<%s>", vv, vt)
|
||||||
|
} else if field.Desc.IsMap() {
|
||||||
|
js += vv + fmt.Sprintf(", // map<%s>", vt)
|
||||||
|
} else if field.Enum != nil {
|
||||||
|
js += vv + fmt.Sprintf(", // enum<%s>", vt)
|
||||||
|
} else {
|
||||||
|
js += vv + fmt.Sprintf(", // type<%s>", vt)
|
||||||
|
}
|
||||||
|
|
||||||
|
if t := string(field.Comments.Trailing); len(t) > 0 {
|
||||||
|
js += ", " + strings.TrimLeft(t, " ")
|
||||||
|
} else {
|
||||||
|
js += "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
return js
|
||||||
|
}
|
||||||
|
|
||||||
|
func (md *markdown) jsDocForMessage(m *protogen.Message) string {
|
||||||
|
md.in(m)
|
||||||
|
defer md.out()
|
||||||
|
|
||||||
|
js := "{\n"
|
||||||
|
|
||||||
|
for _, field := range m.Fields {
|
||||||
|
js += md.jsDocForField(field)
|
||||||
|
}
|
||||||
|
|
||||||
|
js += "}"
|
||||||
|
options := jsbeautifier.DefaultOptions()
|
||||||
|
js, _ = jsbeautifier.Beautify(&js, options)
|
||||||
|
|
||||||
|
return js
|
||||||
|
}
|
Loading…
Reference in New Issue