2025-01-21 14:22:30 -07:00
|
|
|
package generate
|
2025-01-09 00:31:15 -07:00
|
|
|
|
|
|
|
import "io"
|
|
|
|
import "fmt"
|
|
|
|
import "errors"
|
2025-01-20 11:56:03 -07:00
|
|
|
import "strconv"
|
2025-01-09 00:31:15 -07:00
|
|
|
import "strings"
|
|
|
|
import "github.com/gomarkdown/markdown"
|
|
|
|
import "github.com/gomarkdown/markdown/ast"
|
|
|
|
import "github.com/gomarkdown/markdown/parser"
|
|
|
|
|
|
|
|
// Protocol describes a protocol.
|
|
|
|
type Protocol struct {
|
|
|
|
Messages []Message
|
|
|
|
}
|
|
|
|
|
|
|
|
// Message describes a protocol message.
|
|
|
|
type Message struct {
|
2025-01-20 11:56:03 -07:00
|
|
|
Doc string
|
|
|
|
Method uint16
|
|
|
|
Name string
|
2025-01-09 00:31:15 -07:00
|
|
|
Fields []Field
|
|
|
|
}
|
|
|
|
|
|
|
|
// Field describes a named value within a message.
|
|
|
|
type Field struct {
|
|
|
|
Doc string
|
2025-01-20 11:56:03 -07:00
|
|
|
Tag uint16
|
2025-01-09 00:31:15 -07:00
|
|
|
Name string
|
|
|
|
Optional bool
|
|
|
|
Type string
|
|
|
|
}
|
|
|
|
|
|
|
|
// ParseReader parses a protocol definition from a reader.
|
2025-01-20 11:56:03 -07:00
|
|
|
func ParseReader(reader io.Reader) (*Protocol, error) {
|
2025-01-09 00:31:15 -07:00
|
|
|
data, err := io.ReadAll(reader)
|
|
|
|
if err != nil { return nil, err }
|
|
|
|
protocol := new(Protocol)
|
|
|
|
err = protocol.UnmarshalText(data)
|
|
|
|
if err != nil { return nil, err }
|
|
|
|
return protocol, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// UnmarshalText unmarshals markdown-formatted text data into the protocol.
|
2025-01-20 11:56:03 -07:00
|
|
|
func (this *Protocol) UnmarshalText(text []byte) error {
|
2025-01-09 00:31:15 -07:00
|
|
|
var state int; const (
|
|
|
|
stateIdle = iota
|
|
|
|
stateMessage
|
|
|
|
stateMessageDoc
|
|
|
|
stateMessageField
|
|
|
|
)
|
|
|
|
|
|
|
|
var message *Message
|
2025-01-20 11:56:03 -07:00
|
|
|
addMessage := func(method uint16, name string) {
|
2025-01-09 00:31:15 -07:00
|
|
|
this.Messages = append(this.Messages, Message {
|
2025-01-20 11:56:03 -07:00
|
|
|
Method: method,
|
|
|
|
Name: name,
|
2025-01-09 00:31:15 -07:00
|
|
|
})
|
|
|
|
message = &this.Messages[len(this.Messages) - 1]
|
|
|
|
}
|
|
|
|
|
|
|
|
root := markdown.Parse(text, parser.New())
|
|
|
|
for _, node := range root.GetChildren() {
|
|
|
|
if node, ok := node.(*ast.Heading); ok {
|
|
|
|
if node.Level == 2 {
|
|
|
|
if removeBreaks(flatten(node)) == "Messages" {
|
|
|
|
state = stateMessage
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if node.Level > 3 {
|
|
|
|
state = stateIdle
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
if state != stateIdle && node.Level == 3 {
|
2025-01-20 11:56:03 -07:00
|
|
|
heading := removeBreaks(flatten(node))
|
|
|
|
method, name, err := splitMessageHeading(heading)
|
|
|
|
if err != nil { return err }
|
|
|
|
addMessage(method, name)
|
2025-01-09 00:31:15 -07:00
|
|
|
state = stateMessageDoc
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if state == stateIdle { continue }
|
|
|
|
if message == nil { continue }
|
|
|
|
|
2025-01-20 11:56:03 -07:00
|
|
|
// TODO when we are adding text content to the doc comment, it
|
|
|
|
// might be wise to do stuff like indent lists and quotes so
|
|
|
|
// that go doc renders them correctly
|
2025-01-09 00:31:15 -07:00
|
|
|
switch node := node.(type) {
|
|
|
|
case *ast.Paragraph:
|
|
|
|
if message.Doc != "" { message.Doc += "\n\n" }
|
|
|
|
message.Doc += removeBreaks(flatten(node))
|
|
|
|
case *ast.BlockQuote:
|
|
|
|
if message.Doc != "" { message.Doc += "\n\n> " }
|
|
|
|
message.Doc += removeBreaks(flatten(node))
|
|
|
|
case *ast.List:
|
2025-01-20 11:56:03 -07:00
|
|
|
// FIXME format the list
|
|
|
|
if message.Doc != "" { message.Doc += "\n\n" }
|
|
|
|
message.Doc += removeBreaks(flatten(node))
|
2025-01-09 00:31:15 -07:00
|
|
|
case *ast.Table:
|
|
|
|
fields, err := processFieldTable(node)
|
|
|
|
if err != nil { return err}
|
|
|
|
message.Fields = append(message.Fields, fields...)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2025-01-20 11:56:03 -07:00
|
|
|
func processFieldTable(node *ast.Table) ([]Field, error) {
|
2025-01-09 00:31:15 -07:00
|
|
|
fields := []Field { }
|
|
|
|
children := node.GetChildren()
|
|
|
|
if len(children) != 2 {
|
|
|
|
return nil, errors.New("malformed field table")
|
|
|
|
}
|
|
|
|
|
|
|
|
// get columns
|
|
|
|
columns := []string { }
|
|
|
|
if header, ok := children[0].(*ast.TableHeader); ok {
|
|
|
|
children := header.GetChildren()
|
|
|
|
if len(children) != 1 {
|
|
|
|
return nil, errors.New("malformed field table header")
|
|
|
|
}
|
|
|
|
if row, ok := header.Children[0].(*ast.TableRow); ok {
|
|
|
|
for _, cell := range row.GetChildren() {
|
|
|
|
if cell, ok := cell.(*ast.TableCell); ok {
|
|
|
|
columns = append(columns, flatten(cell))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
return nil, errors.New("malformed field table header")
|
|
|
|
}
|
|
|
|
for index, column := range columns {
|
|
|
|
columns[index] = strings.ToLower(column)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
return nil, errors.New("malformed field table: no header")
|
|
|
|
}
|
|
|
|
|
|
|
|
// get data
|
|
|
|
if body, ok := children[1].(*ast.TableBody); ok {
|
|
|
|
for _, node := range body.GetChildren() {
|
|
|
|
if row, ok := node.(*ast.TableRow); ok {
|
|
|
|
children := row.GetChildren()
|
|
|
|
if len(children) != len(columns) {
|
|
|
|
return nil, errors.New (
|
|
|
|
"malformed field table row: wrong " +
|
|
|
|
"number of columns")
|
|
|
|
}
|
|
|
|
|
|
|
|
field := Field { }
|
|
|
|
|
|
|
|
for index, node := range children {
|
|
|
|
if cell, ok := node.(*ast.TableCell); ok {
|
|
|
|
text := flatten(cell)
|
|
|
|
switch columns[index] {
|
2025-01-20 11:56:03 -07:00
|
|
|
case "tag":
|
|
|
|
tag, err := parseTag(text)
|
|
|
|
if err != nil { return nil, err }
|
|
|
|
field.Tag = tag
|
2025-01-09 00:31:15 -07:00
|
|
|
case "name":
|
|
|
|
field.Name = text
|
|
|
|
case "required":
|
|
|
|
field.Optional = !parseBool(text)
|
|
|
|
case "optional":
|
|
|
|
field.Optional = parseBool(text)
|
|
|
|
case "type":
|
|
|
|
field.Type = text
|
|
|
|
case "comment", "purpose", "documentation":
|
|
|
|
field.Doc = text
|
|
|
|
}
|
|
|
|
}}
|
|
|
|
|
|
|
|
fields = append(fields, field)
|
|
|
|
}}
|
|
|
|
} else {
|
|
|
|
return nil, errors.New("malformed field table: no body")
|
|
|
|
}
|
|
|
|
return fields, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
type nodeFlattener struct {
|
|
|
|
text string
|
|
|
|
}
|
2025-01-20 11:56:03 -07:00
|
|
|
func (this *nodeFlattener) String() string { return this.text }
|
|
|
|
func (this *nodeFlattener) Visit(node ast.Node, entering bool) ast.WalkStatus {
|
2025-01-09 00:31:15 -07:00
|
|
|
if entering {
|
|
|
|
if node := node.AsLeaf(); node != nil {
|
|
|
|
this.text += string(node.Literal)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return ast.GoToNext
|
|
|
|
}
|
2025-01-20 11:56:03 -07:00
|
|
|
func flatten(node ast.Node) string {
|
2025-01-09 00:31:15 -07:00
|
|
|
flattener := new(nodeFlattener)
|
|
|
|
ast.Walk(node, flattener)
|
|
|
|
return flattener.text
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2025-01-20 11:56:03 -07:00
|
|
|
func removeBreaks(text string) string {
|
2025-01-09 00:31:15 -07:00
|
|
|
text = strings.ReplaceAll(text, "\n", " ")
|
|
|
|
text = strings.ReplaceAll(text, "\r", "")
|
|
|
|
return text
|
|
|
|
}
|
|
|
|
|
2025-01-20 11:56:03 -07:00
|
|
|
func parseBool(text string) bool {
|
2025-01-09 00:31:15 -07:00
|
|
|
switch(strings.ToLower(text)) {
|
|
|
|
case "yes": return true
|
|
|
|
case "no": return false
|
|
|
|
case "true": return true
|
|
|
|
case "false": return false
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
2025-01-20 11:56:03 -07:00
|
|
|
|
|
|
|
func parseTag(text string) (uint16, error) {
|
|
|
|
tag, err := strconv.ParseUint(text, 10, 16)
|
|
|
|
if err != nil {
|
|
|
|
return 0, fmt.Errorf("malformed tag '%s': %w", text, err)
|
|
|
|
}
|
|
|
|
return uint16(tag), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func splitMessageHeading(text string) (uint16, string, error) {
|
|
|
|
text = strings.TrimSpace(text)
|
2025-01-21 14:22:30 -07:00
|
|
|
methodText, name, ok := strings.Cut(text, " ")
|
2025-01-20 11:56:03 -07:00
|
|
|
if !ok {
|
|
|
|
return 0, "", fmt.Errorf(
|
|
|
|
"malformed message heading '%s': no message name",
|
|
|
|
text)
|
|
|
|
}
|
2025-01-21 14:22:30 -07:00
|
|
|
method, err := strconv.ParseUint(methodText, 16, 16)
|
2025-01-20 11:56:03 -07:00
|
|
|
if err != nil {
|
|
|
|
return 0, "", fmt.Errorf(
|
|
|
|
"malformed method number '%s': %w",
|
|
|
|
methodText, err)
|
|
|
|
}
|
2025-01-21 14:22:30 -07:00
|
|
|
name = strings.TrimSpace(name)
|
2025-01-20 11:56:03 -07:00
|
|
|
return uint16(method), name, nil
|
|
|
|
}
|