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  }