From 993f06cb184bb99ece18dd14fd800991a891fb9f Mon Sep 17 00:00:00 2001 From: xuthus5 Date: Tue, 19 Mar 2024 22:49:04 +0800 Subject: [PATCH] first commit --- .gitignore | 1 + aligned.go | 51 ++++++++++ columns.go | 206 ++++++++++++++++++++++++++++++++++++++++ extensions.go | 47 ++++++++++ formatter.go | 233 ++++++++++++++++++++++++++++++++++++++++++++++ formatter_test.go | 22 +++++ go.mod | 14 +++ go.sum | 12 +++ inline_comment.go | 17 ++++ reflector.go | 35 +++++++ testdata.proto | 139 +++++++++++++++++++++++++++ utils.go | 190 +++++++++++++++++++++++++++++++++++++ 12 files changed, 967 insertions(+) create mode 100644 .gitignore create mode 100644 aligned.go create mode 100644 columns.go create mode 100644 extensions.go create mode 100644 formatter.go create mode 100644 formatter_test.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 inline_comment.go create mode 100644 reflector.go create mode 100644 testdata.proto create mode 100644 utils.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..485dee6 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea diff --git a/aligned.go b/aligned.go new file mode 100644 index 0000000..ef41611 --- /dev/null +++ b/aligned.go @@ -0,0 +1,51 @@ +package protofmt + +import "strings" +import "bytes" + +type aligned struct { + source string + left bool + padding bool +} + +var ( + alignedEquals = leftAligned(" = ") + alignedShortEquals = leftAligned("=") + alignedSpace = leftAligned(" ") + alignedComma = leftAligned(", ") + alignedEmpty = leftAligned("") + alignedSemicolon = notAligned(";") + alignedColon = notAligned(":") +) + +func leftAligned(src string) aligned { return aligned{src, true, true} } +func rightAligned(src string) aligned { return aligned{src, false, true} } +func notAligned(src string) aligned { return aligned{src, false, false} } + +func (a aligned) preferredWidth() int { + if !a.hasAlignment() { + return 0 // means do not force padding because of this source + } + return len(a.source) +} + +func (a aligned) formatted(indentSeparator string, indentLevel, width int) string { + if !a.padding { + // if the source has newlines then make sure the correct indent level is applied + buf := new(bytes.Buffer) + for _, each := range a.source { + buf.WriteRune(each) + if '\n' == each { + buf.WriteString(strings.Repeat(indentSeparator, indentLevel)) + } + } + return buf.String() + } + if a.left { + return a.source + strings.Repeat(" ", width-len(a.source)) + } + return strings.Repeat(" ", width-len(a.source)) + a.source +} + +func (a aligned) hasAlignment() bool { return a.left || a.padding } diff --git a/columns.go b/columns.go new file mode 100644 index 0000000..e02b2c9 --- /dev/null +++ b/columns.go @@ -0,0 +1,206 @@ +package protofmt + +import ( + "bytes" + "fmt" + "io" + "strconv" + + "github.com/emicklei/proto" +) + +func columns(v proto.Visitee) []aligned { + return asColumnsPrintable(v).columns() +} + +// columnsPrintable is for elements that can be printed in aligned columns. +type columnsPrintable interface { + columns() (cols []aligned) +} + +type columnsPrinter struct { + cols []aligned + visitee proto.Visitee +} + +func asColumnsPrintable(v proto.Visitee) *columnsPrinter { + p := new(columnsPrinter) + p.visitee = v + return p +} + +// columns is part of columnsPrintable +func (p *columnsPrinter) columns() []aligned { + p.visitee.Accept(p) + return p.cols +} + +func (p *columnsPrinter) VisitMessage(m *proto.Message) {} +func (p *columnsPrinter) VisitService(v *proto.Service) {} +func (p *columnsPrinter) VisitSyntax(s *proto.Syntax) {} +func (p *columnsPrinter) VisitPackage(pkg *proto.Package) { + p.cols = append(p.cols, notAligned("package "), notAligned(pkg.Name), alignedSemicolon) + if pkg.InlineComment != nil { + p.cols = append(p.cols, notAligned(" //"), notAligned(pkg.InlineComment.Message())) + } +} +func (p *columnsPrinter) VisitOption(o *proto.Option) { + if !o.IsEmbedded { + p.cols = append(p.cols, leftAligned("option ")) + } else { + p.cols = append(p.cols, leftAligned(" [")) + } + p.cols = append(p.cols, keyValuePair(o, o.IsEmbedded)...) + if o.IsEmbedded { + p.cols = append(p.cols, leftAligned("]")) + } + if !o.IsEmbedded { + p.cols = append(p.cols, alignedSemicolon) + if o.InlineComment != nil { + p.cols = append(p.cols, notAligned(" //"), notAligned(o.InlineComment.Message())) + } + } +} +func (p *columnsPrinter) VisitImport(i *proto.Import) { + p.cols = append(p.cols, leftAligned("import"), alignedSpace) + if len(i.Kind) > 0 { + p.cols = append(p.cols, leftAligned(i.Kind), alignedSpace) + } + p.cols = append(p.cols, notAligned(fmt.Sprintf("%q", i.Filename)), alignedSemicolon) + if i.InlineComment != nil { + p.cols = append(p.cols, notAligned(" //"), notAligned(i.InlineComment.Message())) + } +} + +// VisitNormalField +// [|repeated][|optional][space][name][equals][sequence][|option] +func (p *columnsPrinter) VisitNormalField(f *proto.NormalField) { + if f.Repeated { + p.cols = append(p.cols, leftAligned("repeated ")) + } else if f.Optional { + p.cols = append(p.cols, leftAligned("optional ")) + } else if f.Required { + p.cols = append(p.cols, leftAligned("required ")) + } else { + p.cols = append(p.cols, alignedEmpty) + } + p.cols = append(p.cols, leftAligned(f.Type), alignedSpace, leftAligned(f.Name), alignedEquals, rightAligned(strconv.Itoa(f.Sequence))) + if len(f.Options) > 0 { + p.cols = append(p.cols, leftAligned(" [")) + for i, each := range f.Options { + if i > 0 { + p.cols = append(p.cols, alignedComma) + } + p.cols = append(p.cols, keyValuePair(each, true)...) + } + p.cols = append(p.cols, leftAligned("]")) + } + p.cols = append(p.cols, alignedSemicolon) + if f.InlineComment != nil { + p.cols = append(p.cols, alignedInlinePrefix(f.InlineComment), notAligned(f.InlineComment.Message())) + } +} + +func (p *columnsPrinter) VisitEnumField(f *proto.EnumField) { + p.cols = append(p.cols, leftAligned(f.Name), alignedEquals, rightAligned(strconv.Itoa(f.Integer))) + if f.ValueOption != nil { + p.cols = append(p.cols, columns(f.ValueOption)...) + } + p.cols = append(p.cols, alignedSemicolon) + if f.InlineComment != nil { + p.cols = append(p.cols, alignedInlinePrefix(f.InlineComment), notAligned(f.InlineComment.Message())) + } +} +func (p *columnsPrinter) VisitEnum(e *proto.Enum) {} +func (p *columnsPrinter) VisitComment(e *proto.Comment) {} +func (p *columnsPrinter) VisitOneof(o *proto.Oneof) {} +func (p *columnsPrinter) VisitOneofField(o *proto.OneOfField) { + p.cols = append(p.cols, + leftAligned(o.Type), + alignedSpace, + leftAligned(o.Name), + alignedEquals, + rightAligned(strconv.Itoa(o.Sequence))) + if len(o.Options) > 0 { + p.cols = append(p.cols, leftAligned(" [")) + for i, each := range o.Options { + if i > 0 { + p.cols = append(p.cols, alignedComma) + } + p.cols = append(p.cols, keyValuePair(each, true)...) + } + p.cols = append(p.cols, leftAligned("]")) + } + p.cols = append(p.cols, alignedSemicolon) + if o.InlineComment != nil { + p.cols = append(p.cols, notAligned(" //"), notAligned(o.InlineComment.Message())) + } +} +func (p *columnsPrinter) VisitReserved(rs *proto.Reserved) {} +func (p *columnsPrinter) VisitRPC(r *proto.RPC) { + p.cols = append(p.cols, + leftAligned("rpc "), + leftAligned(r.Name), + leftAligned(" (")) + if r.StreamsRequest { + p.cols = append(p.cols, leftAligned("stream ")) + } else { + p.cols = append(p.cols, alignedEmpty) + } + p.cols = append(p.cols, + leftAligned(r.RequestType), + leftAligned(") "), + leftAligned("returns"), + leftAligned(" (")) + if r.StreamsReturns { + p.cols = append(p.cols, leftAligned("stream ")) + } else { + p.cols = append(p.cols, alignedEmpty) + } + p.cols = append(p.cols, + leftAligned(r.ReturnsType), + leftAligned(")")) + if len(r.Elements) > 0 { + buf := new(bytes.Buffer) + io.WriteString(buf, " {\n") + f := NewFormatter(buf, " ") // TODO get separator, now 2 spaces + f.level(1) + for _, each := range r.Elements { + each.Accept(f) + io.WriteString(buf, "\n") + } + f.indent(-1) + io.WriteString(buf, "}") + p.cols = append(p.cols, notAligned(buf.String())) + } else { + p.cols = append(p.cols, alignedSemicolon) + } + if r.InlineComment != nil { + p.cols = append(p.cols, notAligned(" //"), notAligned(r.InlineComment.Message())) + } +} +func (p *columnsPrinter) VisitMapField(f *proto.MapField) { + p.cols = append(p.cols, + alignedEmpty, // no repeated no optional + leftAligned(fmt.Sprintf("map <%s,%s>", f.KeyType, f.Type)), + alignedSpace, + leftAligned(f.Name), + alignedEquals, + rightAligned(strconv.Itoa(f.Sequence))) + if len(f.Options) > 0 { + p.cols = append(p.cols, leftAligned(" [")) + for i, each := range f.Options { + if i > 0 { + p.cols = append(p.cols, alignedComma) + } + p.cols = append(p.cols, keyValuePair(each, true)...) + } + p.cols = append(p.cols, leftAligned("]")) + } + p.cols = append(p.cols, alignedSemicolon) + if f.InlineComment != nil { + p.cols = append(p.cols, alignedInlinePrefix(f.InlineComment), notAligned(f.InlineComment.Message())) + } +} +func (p *columnsPrinter) VisitGroup(g *proto.Group) {} +func (p *columnsPrinter) VisitExtensions(e *proto.Extensions) {} diff --git a/extensions.go b/extensions.go new file mode 100644 index 0000000..1a7a8de --- /dev/null +++ b/extensions.go @@ -0,0 +1,47 @@ +package protofmt + +import "github.com/emicklei/proto" + +// keyValuePair returns key = value or "value" +func keyValuePair(o *proto.Option, embedded bool) (cols []aligned) { + equals := alignedEquals + name := o.Name + if len(o.Constant.OrderedMap) > 0 { + cols = append(cols, leftAligned(name), equals) + cols = append(cols, columnsPrintablesFromMap(o.Constant.OrderedMap)...) + return + } + if embedded { + return append(cols, leftAligned(name), equals, leftAligned(o.Constant.SourceRepresentation())) // numbers right, strings left? TODO + } + return append(cols, rightAligned(name), equals, rightAligned(o.Constant.SourceRepresentation())) +} + +func alignedInlinePrefix(c *proto.Comment) aligned { + prefix := " //" + if c.ExtraSlash { + prefix = " ///" + } + return notAligned(prefix) +} + +func columnsPrintables(c *proto.Comment) (list []columnsPrintable) { + for _, each := range c.Lines { + list = append(list, inlineComment{each, c.ExtraSlash}) + } + return +} + +func typeAssertColumnsPrintable(v proto.Visitee) (columnsPrintable, bool) { + return asColumnsPrintable(v), len(asColumnsPrintable(v).columns()) > 0 +} + +func columnsPrintablesFromMap(m proto.LiteralMap) (cols []aligned) { + cols = append(cols, leftAligned("{"), alignedSpace) + for _, each := range m { + // TODO only works for simple constants + cols = append(cols, leftAligned(each.Name), alignedColon, leftAligned(each.SourceRepresentation())) + } + cols = append(cols, alignedSpace, leftAligned("}")) + return +} diff --git a/formatter.go b/formatter.go new file mode 100644 index 0000000..d9de352 --- /dev/null +++ b/formatter.go @@ -0,0 +1,233 @@ +package protofmt + +import ( + "fmt" + "io" + + "github.com/emicklei/proto" +) + +// Formatter visits a Proto and writes formatted source. +type Formatter struct { + w io.Writer + indentSeparator string + indentLevel int + lastStmt string + lastLevel int +} + +// NewFormatter returns a new Formatter. Only the indentation separator is configurable. +func NewFormatter(writer io.Writer, indentSeparator string) *Formatter { + return &Formatter{w: writer, indentSeparator: indentSeparator} +} + +// Format visits all proto elements and writes formatted source. +func (f *Formatter) Format(p *proto.Proto) { + for _, each := range p.Elements { + each.Accept(f) + } +} + +// VisitComment formats a Comment and writes enclosing newlines. +func (f *Formatter) VisitComment(c *proto.Comment) { + f.printComment(c) + f.nl() +} + +// VisitEnum formats a Enum. +func (f *Formatter) VisitEnum(e *proto.Enum) { + f.begin("enum", e) + if _, ok := e.Parent.(*proto.Message); !ok && (e.Comment == nil || len(e.Comment.Lines) == 0) { + f.nl() + } + fmt.Fprintf(f.w, "enum %s {", e.Name) + if len(e.Elements) > 0 { + f.nl() + f.level(1) + f.printAsGroups(e.Elements) + f.indent(-1) + } + io.WriteString(f.w, "}\n") + f.end("enum") +} + +// VisitEnumField formats a EnumField. +func (f *Formatter) VisitEnumField(e *proto.EnumField) { + f.printAsGroups([]proto.Visitee{e}) +} + +// VisitImport formats a Import. +func (f *Formatter) VisitImport(i *proto.Import) { + f.beginNoDoc("import", i) + f.printAsGroups([]proto.Visitee{i}) + f.end("import") +} + +// VisitMessage formats a Message. +func (f *Formatter) VisitMessage(m *proto.Message) { + f.begin("message", m) + if _, ok := m.Parent.(*proto.Message); !ok && (m.Comment == nil || len(m.Comment.Lines) == 0) { + f.nl() + } + if m.IsExtend { + fmt.Fprintf(f.w, "extend ") + } else { + fmt.Fprintf(f.w, "message ") + } + fmt.Fprintf(f.w, "%s {", m.Name) + if len(m.Elements) > 0 { + f.nl() + f.level(1) + f.printAsGroups(m.Elements) + f.indent(-1) + } + io.WriteString(f.w, "}\n") + f.end("message") +} + +// VisitOption formats a Option. +func (f *Formatter) VisitOption(o *proto.Option) { + f.begin("option", o) + fmt.Fprintf(f.w, "option %s = ", o.Name) + f.formatLiteral(&o.Constant) + fmt.Fprintf(f.w, ";\n") + if o.InlineComment != nil { + fmt.Fprintf(f.w, " //%s", o.InlineComment.Message()) + } + f.end("option") +} + +func (f *Formatter) formatLiteral(l *proto.Literal) { + if len(l.OrderedMap) == 0 && len(l.Array) == 0 { + fmt.Fprintf(f.w, "%s", l.SourceRepresentation()) + return + } + fmt.Fprintf(f.w, "{\n") + for _, other := range l.OrderedMap { + f.indent(1) + fmt.Fprintf(f.w, "%s", other.Name) + if other.PrintsColon { + fmt.Fprintf(f.w, ": ") + } + f.formatLiteral(other.Literal) + f.level(-1) + f.nl() + } + f.indent(0) + fmt.Fprintf(f.w, "}") +} + +// VisitPackage formats a Package. +func (f *Formatter) VisitPackage(p *proto.Package) { + f.begin("package", p) + f.printAsGroups([]proto.Visitee{p}) + f.end("package") +} + +// VisitService formats a Service. +func (f *Formatter) VisitService(s *proto.Service) { + f.begin("service", s) + fmt.Fprintf(f.w, "service %s {", s.Name) + if len(s.Elements) > 0 { + f.nl() + f.level(1) + f.printAsGroups(s.Elements) + f.indent(-1) + } + io.WriteString(f.w, "}\n\n") + f.end("service") +} + +// VisitSyntax formats a Syntax. +func (f *Formatter) VisitSyntax(s *proto.Syntax) { + f.begin("syntax", s) + fmt.Fprintf(f.w, "syntax = %q", s.Value) + f.endWithComment(s.InlineComment) + f.end("syntax") +} + +// VisitOneof formats a Oneof. +func (f *Formatter) VisitOneof(o *proto.Oneof) { + f.begin("oneof", o) + fmt.Fprintf(f.w, "oneof %s {", o.Name) + if len(o.Elements) > 0 { + f.nl() + f.level(1) + f.printAsGroups(o.Elements) + f.indent(-1) + } + io.WriteString(f.w, "}\n") + f.end("oneof") +} + +// VisitOneofField formats a OneofField. +func (f *Formatter) VisitOneofField(o *proto.OneOfField) { + f.printAsGroups([]proto.Visitee{o}) +} + +// VisitReserved formats a Reserved. +func (f *Formatter) VisitReserved(r *proto.Reserved) { + f.begin("reserved", r) + io.WriteString(f.w, "reserved ") + if len(r.Ranges) > 0 { + for i, each := range r.Ranges { + if i > 0 { + io.WriteString(f.w, ", ") + } + fmt.Fprintf(f.w, "%s", each.SourceRepresentation()) + } + } else { + for i, each := range r.FieldNames { + if i > 0 { + io.WriteString(f.w, ", ") + } + fmt.Fprintf(f.w, "%q", each) + } + } + f.endWithComment(r.InlineComment) +} + +// VisitRPC formats a RPC. +func (f *Formatter) VisitRPC(r *proto.RPC) { + f.printAsGroups([]proto.Visitee{r}) +} + +// VisitMapField formats a MapField. +func (f *Formatter) VisitMapField(m *proto.MapField) { + f.printAsGroups([]proto.Visitee{m}) +} + +// VisitNormalField formats a NormalField. +func (f *Formatter) VisitNormalField(f1 *proto.NormalField) { + f.printAsGroups([]proto.Visitee{f1}) +} + +// VisitGroup formats a proto2 Group. +func (f *Formatter) VisitGroup(g *proto.Group) { + f.begin("group", g) + if g.Optional { + io.WriteString(f.w, "optional ") + } + fmt.Fprintf(f.w, "group %s = %d {", g.Name, g.Sequence) + if len(g.Elements) > 0 { + f.nl() + f.level(1) + f.printAsGroups(g.Elements) + f.indent(-1) + } + io.WriteString(f.w, "}\n") + f.end("group") +} + +// VisitExtensions formats a proto2 Extensions. +func (f *Formatter) VisitExtensions(e *proto.Extensions) { + f.begin("extensions", e) + io.WriteString(f.w, "extensions ") + for i, each := range e.Ranges { + if i > 0 { + io.WriteString(f.w, ", ") + } + fmt.Fprintf(f.w, "%s", each.SourceRepresentation()) + } + f.endWithComment(e.InlineComment) +} diff --git a/formatter_test.go b/formatter_test.go new file mode 100644 index 0000000..b19d9b6 --- /dev/null +++ b/formatter_test.go @@ -0,0 +1,22 @@ +package protofmt + +import ( + "bytes" + "github.com/stretchr/testify/assert" + "os" + "testing" + + "github.com/emicklei/proto" +) + +func TestPrint(t *testing.T) { + def, err := os.ReadFile("testdata.proto") + assert.NoError(t, err) + parser := proto.NewParser(bytes.NewReader(def)) + parse, err := parser.Parse() + assert.NoError(t, err) + var buf = new(bytes.Buffer) + NewFormatter(buf, " ").Format(parse) + err = os.WriteFile("testdata.proto", buf.Bytes(), os.ModePerm) + assert.NoError(t, err) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..d3126fc --- /dev/null +++ b/go.mod @@ -0,0 +1,14 @@ +module gitter.top/common/protofmt + +go 1.21 + +require ( + github.com/emicklei/proto v1.13.2 + github.com/stretchr/testify v1.9.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..a916ece --- /dev/null +++ b/go.sum @@ -0,0 +1,12 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/emicklei/proto v1.13.2 h1:z/etSFO3uyXeuEsVPzfl56WNgzcvIr42aQazXaQmFZY= +github.com/emicklei/proto v1.13.2/go.mod h1:rn1FgRS/FANiZdD2djyH7TMA9jdRDcYQ9IEN9yvjX0A= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/inline_comment.go b/inline_comment.go new file mode 100644 index 0000000..a81fd81 --- /dev/null +++ b/inline_comment.go @@ -0,0 +1,17 @@ +package protofmt + +type inlineComment struct { + line string + extraSlash bool +} + +func (i inlineComment) columns() (list []aligned) { + if len(i.line) == 0 { + return append(list, notAligned("")) + } + prefix := "//" + if i.extraSlash { + prefix = "///" + } + return append(list, notAligned(prefix+i.line)) +} diff --git a/reflector.go b/reflector.go new file mode 100644 index 0000000..a9fa205 --- /dev/null +++ b/reflector.go @@ -0,0 +1,35 @@ +package protofmt + +import "github.com/emicklei/proto" + +// reflector is a Visitor that can tell the short type name of a Visitee. +type reflector struct { + name string +} + +// sole instance of reflector +var namer = new(reflector) + +func (r *reflector) VisitMessage(m *proto.Message) { r.name = "Message" } +func (r *reflector) VisitService(v *proto.Service) { r.name = "Service" } +func (r *reflector) VisitSyntax(s *proto.Syntax) { r.name = "Syntax" } +func (r *reflector) VisitPackage(p *proto.Package) { r.name = "Package" } +func (r *reflector) VisitOption(o *proto.Option) { r.name = "Option" } +func (r *reflector) VisitImport(i *proto.Import) { r.name = "Import" } +func (r *reflector) VisitNormalField(i *proto.NormalField) { r.name = "NormalField" } +func (r *reflector) VisitEnumField(i *proto.EnumField) { r.name = "EnumField" } +func (r *reflector) VisitEnum(e *proto.Enum) { r.name = "Enum" } +func (r *reflector) VisitComment(e *proto.Comment) { r.name = "Comment" } +func (r *reflector) VisitOneof(o *proto.Oneof) { r.name = "Oneof" } +func (r *reflector) VisitOneofField(o *proto.OneOfField) { r.name = "OneOfField" } +func (r *reflector) VisitReserved(rs *proto.Reserved) { r.name = "Reserved" } +func (r *reflector) VisitRPC(rpc *proto.RPC) { r.name = "RPC" } +func (r *reflector) VisitMapField(f *proto.MapField) { r.name = "NormalField" } // handle as +func (r *reflector) VisitGroup(g *proto.Group) { r.name = "Group" } +func (r *reflector) VisitExtensions(e *proto.Extensions) { r.name = "Extensions" } + +// nameOfVisitee returns the short type name of a Visitee. +func nameOfVisitee(e proto.Visitee) string { + e.Accept(namer) + return namer.name +} diff --git a/testdata.proto b/testdata.proto new file mode 100644 index 0000000..269ee27 --- /dev/null +++ b/testdata.proto @@ -0,0 +1,139 @@ +syntax = "proto3"; + +package api_services.v1; + +option go_package = "filesaver/gen;genv1"; + + +// @route_group: true +// @base_url: /v1/file +// @gen_to: ./services/controller/v1/file_controller.go +service FileService { + // @desc: 列表 + // @author: Young Xu + // @method: GET + // @api: /list + rpc List (FileServiceListRequest) returns (FileServiceListResponse); + // @desc: 上传 + // @author: Young Xu + // @method: POST + // @api: /upload + rpc Upload (FileServiceUploadRequest) returns (FileServiceUploadResponse); + // @desc: 删除 + // @author: Young Xu + // @method: DELETE + // @api: /delete + rpc Delete (FileServiceDeleteRequest) returns (FileServiceDeleteResponse); + // @desc: 下载 + // @author: Young Xu + // @method: GET + // @api: /download + rpc Download (FileServiceDownloadRequest) returns (FileServiceDownloadResponse); +} + + +message FileServiceListRequest { + string dirname = 1; // 目录 +} + +message FileServiceListResponse { + message Metadata { + string domain = 1; // 域名 + } + message Item { + string file_id = 1; // 文件ID + string filename = 2; // 文件名 + string file_size = 3; // 文件大小 + string created_at = 4; // 上传时间 + string dirname = 5; // 文件路径 + bool is_directory = 6; // 是否是目录 + } + repeated Item items = 1; // 列表 + Metadata metadata = 2; // 元数据 +} + +message FileServiceUploadRequest {} + +message FileServiceUploadResponse {} + +message FileServiceDeleteRequest { + string file_id = 1; // 文件ID + string dirname = 2; // 文件夹 + string filename = 3; // 文件名 +} + +message FileServiceDeleteResponse {} + +message FileServiceDownloadRequest { + string file_id = 1; // 文件ID + string dirname = 2; // 文件夹 + string filename = 3; // 文件名 +} + +message FileServiceDownloadResponse {} + + +// @route_group: true +// @base_url: /v1/setting +// @gen_to: ./services/controller/v1/setting_controller.go +service SettingService { + // @desc: 改变存储适配器 + // @author: Young Xu + // @method: POST + // @api: /adapter + rpc SettingServiceChangeAdapter (SettingServiceChangeAdapterReq) returns (SettingServiceChangeAdapterResp); + // @desc: 改变存储适配器 + // @author: Young Xu + // @method: POST + // @api: /cf_r2 + rpc ConfigCloudFlareR2 (SettingServiceConfigCloudFlareR2Req) returns (SettingServiceConfigCloudFlareR2Resp); + // @desc: 获取cloudflare r2配置 + // @author: Young Xu + // @method: GET + // @api: /cf_r2 + rpc GetCloudFlareR2 (SettingServiceGetCloudFlareR2Req) returns (SettingServiceGetCloudFlareR2Resp); + // @desc: 获取适配器配置 + // @author: Young Xu + // @method: GET + // @api: /adapter + rpc GetAdapter (SettingServiceGetAdapterReq) returns (SettingServiceGetAdapterResp); +} + + +message SettingServiceChangeAdapterReq { + string adapter = 1; +} + +message SettingServiceChangeAdapterResp {} + +message SettingServiceConfigCloudFlareR2Req { + string api_token = 1; + string user_email = 2; + string user_id = 3; + string bucket = 4; + string s3_api = 5; + string domain = 6; + string secret_id = 7; + string secret_key = 8; +} + +message SettingServiceConfigCloudFlareR2Resp {} + +message SettingServiceGetCloudFlareR2Req {} + +message SettingServiceGetCloudFlareR2Resp { + string api_token = 1; + string user_email = 2; + string user_id = 3; + string bucket = 4; + string s3_api = 5; + string domain = 6; + string secret_id = 7; + string secret_key = 8; +} + +message SettingServiceGetAdapterReq {} + +message SettingServiceGetAdapterResp { + string adapter = 1; +} diff --git a/utils.go b/utils.go new file mode 100644 index 0000000..5041672 --- /dev/null +++ b/utils.go @@ -0,0 +1,190 @@ +package protofmt + +import ( + "fmt" + "io" + "strings" + + "github.com/emicklei/proto" +) + +// printDoc writes documentation is available +func (f *Formatter) printDoc(v proto.Visitee) { + if hasDoc, ok := v.(proto.Documented); ok { + if doc := hasDoc.Doc(); doc != nil { + f.printComment(doc) + } + } +} + +// printComment formats a Comment. +func (f *Formatter) printComment(c *proto.Comment) { + f.nl() + if c.Cstyle { + f.indent(0) + fmt.Fprintln(f.w, "/*") + } + for i, each := range c.Lines { + each = strings.TrimRight(each, " ") + f.indent(0) + if c.Cstyle { + // only skip first and last empty lines + skip := (i == 0 && len(each) == 0) || + (i == len(c.Lines)-1 && len(each) == 0) + if !skip { + fmt.Fprintf(f.w, "%s\n", each) + } + } else { + if c.ExtraSlash { + fmt.Fprint(f.w, "/") + } + fmt.Fprintf(f.w, "//%s\n", each) + } + } + if c.Cstyle { + fmt.Fprintf(f.w, "*/\n") + } +} + +// begin writes a newline if the last statement kind is different. always indents. +// if the Visitee has comment then print it. +func (f *Formatter) begin(stmt string, v proto.Visitee) { + // if not the first statement and different from last and on same indent level. + if len(f.lastStmt) > 0 && f.lastStmt != stmt && f.lastLevel == f.indentLevel { + f.nl() + } + f.lastStmt = stmt + f.printDoc(v) + f.indent(0) +} + +// beginNoDoc writes a newline if the last statement kind is different. always indents. +func (f *Formatter) beginNoDoc(stmt string, v proto.Visitee) { + // if not the first statement and different from last and on same indent level. + if len(f.lastStmt) > 0 && f.lastStmt != stmt && f.lastLevel == f.indentLevel { + f.nl() + } + f.lastStmt = stmt + f.indent(0) +} + +func (f *Formatter) end(stmt string) { + f.lastStmt = stmt +} + +// indent changes the indent level and writes indentation. +func (f *Formatter) indent(diff int) { + f.level(diff) + for i := 0; i < f.indentLevel; i++ { + io.WriteString(f.w, f.indentSeparator) + } +} + +func (f *Formatter) printListOfColumns(list []columnsPrintable) { + if len(list) == 0 { + return + } + // collect all column values + values := [][]aligned{} + widths := map[int]int{} + for _, each := range list { + cols := each.columns() + values = append(values, cols) + // update max widths per column + for i, other := range cols { + pw := other.preferredWidth() + w, ok := widths[i] + if ok { + if pw > w { + widths[i] = pw + } + } else { + widths[i] = pw + } + } + } + // now print all values + for _, each := range values { + f.indent(0) + for c := 0; c < len(widths); c++ { + pw := widths[c] + // only print if there is a value + if c < len(each) { + // using space padding to match the max width + io.WriteString(f.w, each[c].formatted(f.indentSeparator, f.indentLevel, pw)) + } + } + f.nl() + } +} + +// nl writes a newline. +func (f *Formatter) nl() { + io.WriteString(f.w, "\n") +} + +// level changes the current indentLevel but does not print indentation. +func (f *Formatter) level(diff int) { + f.lastLevel = f.indentLevel + f.indentLevel += diff +} + +// printAsGroups prints the list in groups of the same element type. +func (f *Formatter) printAsGroups(list []proto.Visitee) { + group := []columnsPrintable{} + lastGroupName := "" + for _, each := range list { + groupName := nameOfVisitee(each) + printable, isColumnsPrintable := typeAssertColumnsPrintable(each) + if isColumnsPrintable { + if lastGroupName != groupName { + lastGroupName = groupName + // print current group + if len(group) > 0 { + f.printListOfColumns(group) + // begin new group + group = []columnsPrintable{} + } + } + // comment as a group entity + if hasDoc, ok := each.(proto.Documented); ok { + if doc := hasDoc.Doc(); doc != nil { + f.printListOfColumns(group) + // begin new group + //group = []columnsPrintable{} + //if len(doc.Lines) > 0 { // if comment then add newline before it + // group = append(group, inlineComment{line: "", extraSlash: false}) + //} + group = append([]columnsPrintable{}, columnsPrintables(doc)...) + } + } + group = append(group, printable) + } else { + // not printable in group + lastGroupName = groupName + // print current group + if len(group) > 0 { + f.printListOfColumns(group) + // begin new group + group = []columnsPrintable{} + } + each.Accept(f) + } + } + // print last group + f.printListOfColumns(group) +} + +// endWithComment writes a statement end (;) followed by inline comment if present. +func (f *Formatter) endWithComment(commentOrNil *proto.Comment) { + io.WriteString(f.w, ";") + if commentOrNil != nil { + if commentOrNil.ExtraSlash { + io.WriteString(f.w, " ///") + } else { + io.WriteString(f.w, " //") + } + io.WriteString(f.w, commentOrNil.Message()) + } + io.WriteString(f.w, "\n") +}