github.com/bluenviron/gomavlib/v2@v2.2.1-0.20240308101627-2c07e3da629c/pkg/conversion/conversion.go (about) 1 // Package conversion contains functions to convert definitions from XML to Go. 2 package conversion 3 4 import ( 5 "bytes" 6 "fmt" 7 "io" 8 "net/http" 9 "net/url" 10 "os" 11 "path" 12 "path/filepath" 13 "regexp" 14 "sort" 15 "strconv" 16 "strings" 17 "text/template" 18 ) 19 20 var ( 21 reMsgName = regexp.MustCompile("^[A-Z0-9_]+$") 22 reTypeIsArray = regexp.MustCompile(`^(.+?)\[([0-9]+)\]$`) 23 ) 24 25 var tplDialect = template.Must(template.New("").Parse( 26 `// Package {{ .PkgName }} contains the {{ .PkgName }} dialect. 27 // 28 //autogenerated:yes 29 package {{ .PkgName }} 30 31 import ( 32 "github.com/bluenviron/gomavlib/v2/pkg/message" 33 "github.com/bluenviron/gomavlib/v2/pkg/dialect" 34 ) 35 36 // Dialect contains the dialect definition. 37 var Dialect = dial 38 39 // dial is not exposed directly in order not to display it in godoc. 40 var dial = &dialect.Dialect{ 41 Version: {{.Version}}, 42 Messages: []message.Message{ 43 {{- range .Defs }} 44 // {{ .Name }} 45 {{- range .Messages }} 46 &Message{{ .Name }}{}, 47 {{- end }} 48 {{- end }} 49 }, 50 } 51 `)) 52 53 var tplEnum = template.Must(template.New("").Parse( 54 `//autogenerated:yes 55 //nolint:revive,misspell,govet,lll,dupl,gocritic 56 package {{ .PkgName }} 57 58 {{- if .Link }} 59 60 import ( 61 "github.com/bluenviron/gomavlib/v2/pkg/dialects/{{ .Enum.DefName }}" 62 ) 63 64 {{- range .Enum.Description }} 65 // {{ . }} 66 {{- end }} 67 type {{ .Enum.Name }} = {{ .Enum.DefName }}.{{ .Enum.Name }} 68 69 const ( 70 {{- $en := .Enum }} 71 {{- range .Enum.Values }} 72 {{- range .Description }} 73 // {{ . }} 74 {{- end }} 75 {{ .Name }} {{ $en.Name }} = {{ $en.DefName }}.{{ .Name }} 76 {{- end }} 77 ) 78 79 {{- else }} 80 81 import ( 82 "strconv" 83 {{- if .Enum.Bitmask }} 84 "strings" 85 {{- end }} 86 "fmt" 87 ) 88 89 {{- range .Enum.Description }} 90 // {{ . }} 91 {{- end }} 92 type {{ .Enum.Name }} uint64 93 94 const ( 95 {{- $pn := .Enum.Name }} 96 {{- range .Enum.Values }} 97 {{- range .Description }} 98 // {{ . }} 99 {{- end }} 100 {{ .Name }} {{ $pn }} = {{ .Value }} 101 {{- end }} 102 ) 103 104 var labels_{{ .Enum.Name }} = map[{{ .Enum.Name }}]string{ 105 {{- range .Enum.Values }} 106 {{ .Name }}: "{{ .Name }}", 107 {{- end }} 108 } 109 110 var values_{{ .Enum.Name }} = map[string]{{ .Enum.Name }}{ 111 {{- range .Enum.Values }} 112 "{{ .Name }}": {{ .Name }}, 113 {{- end }} 114 } 115 116 // MarshalText implements the encoding.TextMarshaler interface. 117 func (e {{ .Enum.Name }}) MarshalText() ([]byte, error) { 118 {{- if .Enum.Bitmask }} 119 if e == 0 { 120 return []byte("0"), nil 121 } 122 var names []string 123 for i := 0; i < {{ len .Enum.Values }}; i++ { 124 mask := {{ .Enum.Name }}(1 << i) 125 if e&mask == mask { 126 names = append(names, labels_{{ .Enum.Name }}[mask]) 127 } 128 } 129 return []byte(strings.Join(names, " | ")), nil 130 {{- else }} 131 if name, ok := labels_{{ .Enum.Name }}[e]; ok { 132 return []byte(name), nil 133 } 134 return []byte(strconv.Itoa(int(e))), nil 135 {{- end }} 136 } 137 138 // UnmarshalText implements the encoding.TextUnmarshaler interface. 139 func (e *{{ .Enum.Name }}) UnmarshalText(text []byte) error { 140 {{- if .Enum.Bitmask }} 141 labels := strings.Split(string(text), " | ") 142 var mask {{ .Enum.Name }} 143 for _, label := range labels { 144 if value, ok := values_{{ .Enum.Name }}[label]; ok { 145 mask |= value 146 } else if value, err := strconv.Atoi(label); err == nil { 147 mask |= {{ .Enum.Name }}(value) 148 } else { 149 return fmt.Errorf("invalid label '%s'", label) 150 } 151 } 152 *e = mask 153 {{- else }} 154 if value, ok := values_{{ .Enum.Name }}[string(text)]; ok { 155 *e = value 156 } else if value, err := strconv.Atoi(string(text)); err == nil { 157 *e = {{ .Enum.Name }}(value) 158 } else { 159 return fmt.Errorf("invalid label '%s'", text) 160 } 161 {{- end }} 162 return nil 163 } 164 165 // String implements the fmt.Stringer interface. 166 func (e {{ .Enum.Name }}) String() string { 167 val, _ := e.MarshalText() 168 return string(val) 169 } 170 {{- end }} 171 `)) 172 173 var tplMessage = template.Must(template.New("").Parse( 174 `//autogenerated:yes 175 //nolint:revive,misspell,govet,lll 176 package {{ .PkgName }} 177 178 {{- if .Link }} 179 180 import ( 181 "github.com/bluenviron/gomavlib/v2/pkg/dialects/{{ .Msg.DefName }}" 182 ) 183 184 {{- range .Msg.Description }} 185 // {{ . }} 186 {{- end }} 187 type Message{{ .Msg.Name }} = {{ .Msg.DefName }}.Message{{ .Msg.Name }} 188 189 {{- else }} 190 191 {{- range .Msg.Description }} 192 // {{ . }} 193 {{- end }} 194 type Message{{ .Msg.Name }} struct { 195 {{- range .Msg.Fields }} 196 {{- range .Description }} 197 // {{ . }} 198 {{- end }} 199 {{ .Line }} 200 {{- end }} 201 } 202 203 // GetID implements the message.Message interface. 204 func (*Message{{ .Msg.Name }}) GetID() uint32 { 205 return {{ .Msg.ID }} 206 } 207 208 {{- end }} 209 `)) 210 211 var dialectTypeToGo = map[string]string{ 212 "double": "float64", 213 "uint64_t": "uint64", 214 "int64_t": "int64", 215 "float": "float32", 216 "uint32_t": "uint32", 217 "int32_t": "int32", 218 "uint16_t": "uint16", 219 "int16_t": "int16", 220 "uint8_t": "uint8", 221 "int8_t": "int8", 222 "char": "string", 223 } 224 225 func defAddrToName(pa string) string { 226 var b string 227 u, err := url.ParseRequestURI(pa) 228 if err == nil { 229 b = path.Base(u.Path) 230 } else { 231 b = path.Base(pa) 232 } 233 234 b = strings.TrimSuffix(b, path.Ext(b)) 235 return strings.ToLower(strings.ReplaceAll(b, "_", "")) 236 } 237 238 func dialectNameGoToDef(in string) string { 239 re := regexp.MustCompile("([A-Z])") 240 in = re.ReplaceAllString(in, "_${1}") 241 return strings.ToLower(in[1:]) 242 } 243 244 func dialectNameDefToGo(in string) string { 245 re := regexp.MustCompile("_[a-z]") 246 in = strings.ToLower(in) 247 in = re.ReplaceAllStringFunc(in, func(match string) string { 248 return strings.ToUpper(match[1:2]) 249 }) 250 return strings.ToUpper(in[:1]) + in[1:] 251 } 252 253 func parseDescription(in string) []string { 254 var lines []string 255 256 for _, line := range strings.Split(in, "\n") { 257 line = strings.TrimSpace(line) 258 if line != "" { 259 lines = append(lines, line) 260 } 261 } 262 263 return lines 264 } 265 266 func uintPow(base, exp uint64) uint64 { 267 result := uint64(1) 268 for { 269 if exp&1 == 1 { 270 result *= base 271 } 272 exp >>= 1 273 if exp == 0 { 274 break 275 } 276 base *= base 277 } 278 279 return result 280 } 281 282 type outEnumValue struct { 283 Value uint64 284 Name string 285 Description []string 286 } 287 288 type outEnum struct { 289 DefName string 290 Name string 291 Description []string 292 Values []*outEnumValue 293 Bitmask bool 294 } 295 296 type outField struct { 297 Description []string 298 Line string 299 } 300 301 type outMessage struct { 302 DefName string 303 OrigName string 304 Name string 305 Description []string 306 ID int 307 Fields []*outField 308 } 309 310 type outDefinition struct { 311 Name string 312 Enums []*outEnum 313 Messages []*outMessage 314 } 315 316 func processDefinition( 317 version *string, 318 processedDefs map[string]struct{}, 319 isRemote bool, 320 defAddr string, 321 ) ([]*outDefinition, error) { 322 // skip already processed 323 if _, ok := processedDefs[defAddr]; ok { 324 return nil, nil 325 } 326 processedDefs[defAddr] = struct{}{} 327 328 fmt.Fprintf(os.Stderr, "processing definition %s\n", defAddr) 329 330 content, err := getDefinition(isRemote, defAddr) 331 if err != nil { 332 return nil, err 333 } 334 335 def, err := definitionDecode(content) 336 if err != nil { 337 return nil, fmt.Errorf("unable to decode: %w", err) 338 } 339 340 addrPath, _ := filepath.Split(defAddr) 341 342 var outDefs []*outDefinition 343 344 // includes 345 for _, subDefAddr := range def.Includes { 346 // prepend url to remote address 347 if isRemote { 348 subDefAddr = addrPath + subDefAddr 349 } 350 subDefs, err := processDefinition(version, processedDefs, isRemote, subDefAddr) 351 if err != nil { 352 return nil, err 353 } 354 outDefs = append(outDefs, subDefs...) 355 } 356 357 // version (process it after includes, in order to allow overriding it) 358 if def.Version != "" { 359 *version = def.Version 360 } 361 362 outDef := &outDefinition{ 363 Name: defAddrToName(defAddr), 364 } 365 366 // enums 367 for _, enum := range def.Enums { 368 oute := &outEnum{ 369 DefName: outDef.Name, 370 Name: enum.Name, 371 Description: parseDescription(enum.Description), 372 Bitmask: enum.Bitmask, 373 } 374 375 for _, entry := range enum.Entries { 376 var v uint64 377 378 switch { 379 case strings.HasPrefix(entry.Value, "0b"): 380 tmp, err := strconv.ParseUint(entry.Value[2:], 2, 64) 381 if err != nil { 382 return nil, err 383 } 384 v = tmp 385 386 case strings.HasPrefix(entry.Value, "0x"): 387 tmp, err := strconv.ParseUint(entry.Value[2:], 16, 64) 388 if err != nil { 389 return nil, err 390 } 391 v = tmp 392 393 case strings.Contains(entry.Value, "**"): 394 parts := strings.SplitN(entry.Value, "**", 2) 395 396 x, err := strconv.ParseUint(parts[0], 10, 64) 397 if err != nil { 398 return nil, err 399 } 400 401 y, err := strconv.ParseUint(parts[1], 10, 64) 402 if err != nil { 403 return nil, err 404 } 405 406 v = uintPow(x, y) 407 408 default: 409 tmp, err := strconv.ParseUint(entry.Value, 10, 64) 410 if err != nil { 411 return nil, err 412 } 413 v = tmp 414 } 415 416 oute.Values = append(oute.Values, &outEnumValue{ 417 Value: v, 418 Name: entry.Name, 419 Description: parseDescription(entry.Description), 420 }) 421 } 422 423 outDef.Enums = append(outDef.Enums, oute) 424 } 425 426 // messages 427 for _, msg := range def.Messages { 428 outMsg, err := processMessage(outDef.Name, msg) 429 if err != nil { 430 return nil, err 431 } 432 outDef.Messages = append(outDef.Messages, outMsg) 433 } 434 435 outDefs = append(outDefs, outDef) 436 return outDefs, nil 437 } 438 439 func getDefinition(isRemote bool, defAddr string) ([]byte, error) { 440 if isRemote { 441 byt, err := download(defAddr) 442 if err != nil { 443 return nil, fmt.Errorf("unable to download: %w", err) 444 } 445 return byt, nil 446 } 447 448 byt, err := os.ReadFile(defAddr) 449 if err != nil { 450 return nil, fmt.Errorf("unable to open: %w", err) 451 } 452 return byt, nil 453 } 454 455 func download(addr string) ([]byte, error) { 456 res, err := http.Get(addr) 457 if err != nil { 458 return nil, err 459 } 460 defer res.Body.Close() 461 462 if res.StatusCode != http.StatusOK { 463 return nil, fmt.Errorf("bad return code: %v", res.StatusCode) 464 } 465 466 byt, err := io.ReadAll(res.Body) 467 if err != nil { 468 return nil, err 469 } 470 return byt, nil 471 } 472 473 func processMessage(defName string, msgDef *definitionMessage) (*outMessage, error) { 474 if m := reMsgName.FindStringSubmatch(msgDef.Name); m == nil { 475 return nil, fmt.Errorf("unsupported message name: %s", msgDef.Name) 476 } 477 478 outMsg := &outMessage{ 479 DefName: defName, 480 OrigName: msgDef.Name, 481 Name: dialectNameDefToGo(msgDef.Name), 482 Description: parseDescription(msgDef.Description), 483 ID: msgDef.ID, 484 } 485 486 for _, f := range msgDef.Fields { 487 outField, err := processField(f) 488 if err != nil { 489 return nil, err 490 } 491 outMsg.Fields = append(outMsg.Fields, outField) 492 } 493 494 return outMsg, nil 495 } 496 497 func processField(fieldDef *dialectField) (*outField, error) { 498 outF := &outField{ 499 Description: parseDescription(fieldDef.Description), 500 } 501 tags := make(map[string]string) 502 503 newname := dialectNameDefToGo(fieldDef.Name) 504 505 // name conversion is not univoque: add tag 506 if dialectNameGoToDef(newname) != fieldDef.Name { 507 tags["mavname"] = fieldDef.Name 508 } 509 510 outF.Line += newname 511 512 typ := fieldDef.Type 513 arrayLen := "" 514 515 if typ == "uint8_t_mavlink_version" { 516 typ = "uint8_t" 517 } 518 519 // string or array 520 if matches := reTypeIsArray.FindStringSubmatch(typ); matches != nil { 521 // string 522 if matches[1] == "char" { 523 tags["mavlen"] = matches[2] 524 typ = "char" 525 // array 526 } else { 527 arrayLen = matches[2] 528 typ = matches[1] 529 } 530 } 531 532 // extension 533 if fieldDef.Extension { 534 tags["mavext"] = "true" 535 } 536 537 typ = dialectTypeToGo[typ] 538 if typ == "" { 539 return nil, fmt.Errorf("unknown type: %s", typ) 540 } 541 542 outF.Line += " " 543 if arrayLen != "" { 544 outF.Line += "[" + arrayLen + "]" 545 } 546 if fieldDef.Enum != "" { 547 outF.Line += fieldDef.Enum 548 tags["mavenum"] = typ 549 } else { 550 outF.Line += typ 551 } 552 553 if len(tags) > 0 { 554 var tmp []string 555 for k, v := range tags { 556 tmp = append(tmp, fmt.Sprintf("%s:\"%s\"", k, v)) 557 } 558 sort.Strings(tmp) 559 outF.Line += " `" + strings.Join(tmp, " ") + "`" 560 } 561 return outF, nil 562 } 563 564 func writeDialect( 565 dir string, 566 defName string, 567 version string, 568 outDefs []*outDefinition, 569 enums map[string]*outEnum, 570 ) error { 571 var buf bytes.Buffer 572 err := tplDialect.Execute(&buf, map[string]interface{}{ 573 "PkgName": defName, 574 "Version": func() int { 575 ret, _ := strconv.Atoi(version) 576 return ret 577 }(), 578 "Defs": outDefs, 579 "Enums": enums, 580 }) 581 if err != nil { 582 return err 583 } 584 585 return os.WriteFile(filepath.Join(dir, "dialect.go"), buf.Bytes(), 0o644) 586 } 587 588 func writeEnum( 589 dir string, 590 defName string, 591 enum *outEnum, 592 link bool, 593 ) error { 594 var buf bytes.Buffer 595 err := tplEnum.Execute(&buf, map[string]interface{}{ 596 "PkgName": defName, 597 "Enum": enum, 598 "Link": link && defName != enum.DefName, 599 }) 600 if err != nil { 601 return err 602 } 603 604 return os.WriteFile(filepath.Join(dir, "enum_"+strings.ToLower(enum.Name)+".go"), buf.Bytes(), 0o644) 605 } 606 607 func writeMessage( 608 dir string, 609 defName string, 610 msg *outMessage, 611 link bool, 612 ) error { 613 var buf bytes.Buffer 614 err := tplMessage.Execute(&buf, map[string]interface{}{ 615 "PkgName": defName, 616 "Msg": msg, 617 "Link": link && defName != msg.DefName, 618 }) 619 if err != nil { 620 return err 621 } 622 623 return os.WriteFile(filepath.Join(dir, "message_"+strings.ToLower(msg.OrigName)+".go"), buf.Bytes(), 0o644) 624 } 625 626 // Convert converts a XML definition into a Golang definition. 627 func Convert(path string, link bool) error { 628 version := "" 629 processedDefs := make(map[string]struct{}) 630 _, err := url.ParseRequestURI(path) 631 isRemote := (err == nil) 632 defName := defAddrToName(path) 633 634 if _, err := os.Stat(defName); !os.IsNotExist(err) { 635 return fmt.Errorf("directory '%s' already exists", defName) 636 } 637 638 os.Mkdir(defName, 0o755) 639 640 // parse all definitions recursively 641 outDefs, err := processDefinition(&version, processedDefs, isRemote, path) 642 if err != nil { 643 return err 644 } 645 646 // merge enums together 647 enums := make(map[string]*outEnum) 648 for _, def := range outDefs { 649 for _, defEnum := range def.Enums { 650 if _, ok := enums[defEnum.Name]; !ok { 651 enums[defEnum.Name] = defEnum 652 } else { 653 enums[defEnum.Name].DefName = defName 654 enums[defEnum.Name].Values = append(enums[defEnum.Name].Values, defEnum.Values...) 655 } 656 } 657 } 658 659 err = writeDialect(defName, defName, version, outDefs, enums) 660 if err != nil { 661 return err 662 } 663 664 for _, enum := range enums { 665 err := writeEnum(defName, defName, enum, link) 666 if err != nil { 667 return err 668 } 669 } 670 671 for _, def := range outDefs { 672 for _, msg := range def.Messages { 673 err := writeMessage(defName, defName, msg, link) 674 if err != nil { 675 return err 676 } 677 } 678 } 679 680 return nil 681 }