diff options
Diffstat (limited to 'xC-gen-proto-go.awk')
-rw-r--r-- | xC-gen-proto-go.awk | 519 |
1 files changed, 519 insertions, 0 deletions
diff --git a/xC-gen-proto-go.awk b/xC-gen-proto-go.awk new file mode 100644 index 0000000..477a471 --- /dev/null +++ b/xC-gen-proto-go.awk @@ -0,0 +1,519 @@ +# xC-gen-proto-go.awk: Go backend for xC-gen-proto.awk. +# +# Copyright (c) 2022, Přemysl Eric Janouch <p@janouch.name> +# SPDX-License-Identifier: 0BSD +# +# This backend also enables proxying to other endpoints using JSON. + +function define_internal(name, gotype) { + Types[name] = "internal" + CodegenGoType[name] = gotype +} + +function define_sint(size, shortname, gotype) { + shortname = "i" size + gotype = "int" size + define_internal(shortname, gotype) + + CodegenAppendJSON[shortname] = \ + "\tb = strconv.AppendInt(b, int64(%s), 10)\n" + if (size == 8) { + CodegenSerialize[shortname] = "\tdata = append(data, uint8(%s))\n" + CodegenDeserialize[shortname] = \ + "\tif len(data) >= 1 {\n" \ + "\t\t%s, data = int8(data[0]), data[1:]\n" \ + "\t} else {\n" \ + "\t\treturn nil, false\n" \ + "\t}\n" + return + } + + CodegenSerialize[shortname] = \ + "\tdata = binary.BigEndian.AppendUint" size "(data, uint" size "(%s))\n" + CodegenDeserialize[shortname] = \ + "\tif len(data) >= " (size / 8) " {\n" \ + "\t\t%s = " gotype "(binary.BigEndian.Uint" size "(data))\n" \ + "\t\tdata = data[" (size / 8) ":]\n" \ + "\t} else {\n" \ + "\t\treturn nil, false\n" \ + "\t}\n" +} + +function define_uint(size, shortname, gotype) { + # Both []byte and []uint8 luckily marshal as base64-encoded JSON strings, + # so there's no need to rename the type as an exception. + shortname = "u" size + gotype = "uint" size + define_internal(shortname, gotype) + + CodegenAppendJSON[shortname] = \ + "\tb = strconv.AppendUint(b, uint64(%s), 10)\n" + if (size == 8) { + CodegenSerialize[shortname] = "\tdata = append(data, %s)\n" + CodegenDeserialize[shortname] = \ + "\tif len(data) >= 1 {\n" \ + "\t\t%s, data = data[0], data[1:]\n" \ + "\t} else {\n" \ + "\t\treturn nil, false\n" \ + "\t}\n" + return + } + + CodegenSerialize[shortname] = \ + "\tdata = binary.BigEndian.AppendUint" size "(data, %s)\n" + CodegenDeserialize[shortname] = \ + "\tif len(data) >= " (size / 8) " {\n" \ + "\t\t%s = binary.BigEndian.Uint" size "(data)\n" \ + "\t\tdata = data[" (size / 8) ":]\n" \ + "\t} else {\n" \ + "\t\treturn nil, false\n" \ + "\t}\n" +} + +function codegen_begin() { + define_sint("8") + define_sint("16") + define_sint("32") + define_sint("64") + define_uint("8") + define_uint("16") + define_uint("32") + define_uint("64") + + define_internal("bool", "bool") + CodegenAppendJSON["bool"] = \ + "\tb = strconv.AppendBool(b, %s)\n" + CodegenSerialize["bool"] = \ + "\tif %s {\n" \ + "\t\tdata = append(data, 1)\n" \ + "\t} else {\n" \ + "\t\tdata = append(data, 0)\n" \ + "\t}\n" + CodegenDeserialize["bool"] = \ + "\tif data, ok = protoConsumeBoolFrom(data, &%s); !ok {\n" \ + "\t\treturn nil, ok\n" \ + "\t}\n" + + define_internal("string", "string") + CodegenSerialize["string"] = \ + "\tif data, ok = protoAppendStringTo(data, %s); !ok {\n" \ + "\t\treturn nil, ok\n" \ + "\t}\n" + CodegenDeserialize["string"] = \ + "\tif data, ok = protoConsumeStringFrom(data, &%s); !ok {\n" \ + "\t\treturn nil, ok\n" \ + "\t}\n" + + print "package main" + print "" + print "import (" + print "\t`encoding/base64`" + print "\t`encoding/binary`" + print "\t`encoding/json`" + print "\t`errors`" + print "\t`math`" + print "\t`strconv`" + print "\t`unicode/utf8`" + print ")" + print "" + + print "// protoConsumeBoolFrom tries to deserialize a boolean value" + print "// from the beginning of a byte stream. When successful," + print "// it returns a subslice with any data that might follow." + print "func protoConsumeBoolFrom(data []byte, b *bool) ([]byte, bool) {" + print "\tif len(data) < 1 {" + print "\t\treturn nil, false" + print "\t}" + print "\tif data[0] != 0 {" + print "\t\t*b = true" + print "\t} else {" + print "\t\t*b = false" + print "\t}" + print "\treturn data[1:], true" + print "}" + print "" + + print "// protoAppendStringTo tries to serialize a string value," + print "// appending it to the end of a byte stream." + print "func protoAppendStringTo(data []byte, s string) ([]byte, bool) {" + print "\tif len(s) > math.MaxUint32 {" + print "\t\treturn nil, false" + print "\t}" + print "\tdata = binary.BigEndian.AppendUint32(data, uint32(len(s)))" + print "\treturn append(data, s...), true" + print "}" + print "" + + print "// protoConsumeStringFrom tries to deserialize a string value" + print "// from the beginning of a byte stream. When successful," + print "// it returns a subslice with any data that might follow." + print "func protoConsumeStringFrom(data []byte, s *string) ([]byte, bool) {" + print "\tif len(data) < 4 {" + print "\t\treturn nil, false" + print "\t}" + print "\tlength := binary.BigEndian.Uint32(data)" + print "\tif data = data[4:]; uint64(len(data)) < uint64(length) {" + print "\t\treturn nil, false" + print "\t}" + print "\t*s = string(data[:length])" + print "\tif !utf8.ValidString(*s) {" + print "\t\treturn nil, false" + print "\t}" + print "\treturn data[length:], true" + print "}" + print "" + + print "// protoUnmarshalEnumJSON converts a JSON fragment to an integer," + print "// ensuring that it's within the expected range of enum values." + print "func protoUnmarshalEnumJSON(data []byte) (int64, error) {" + print "\tvar n int64" + print "\tif err := json.Unmarshal(data, &n); err != nil {" + print "\t\treturn 0, err" + print "\t} else if n > math.MaxInt8 || n < math.MinInt8 {" + print "\t\treturn 0, errors.New(`integer out of range`)" + print "\t} else {" + print "\t\treturn n, nil" + print "\t}" + print "}" + print "" +} + +function codegen_constant(name, value) { + print "const " PrefixCamel snaketocamel(name) " = " value + print "" +} + +function codegen_enum_value(name, subname, value, cg, goname) { + goname = PrefixCamel name snaketocamel(subname) + append(cg, "fields", + "\t" goname " = " value "\n") + append(cg, "stringer", + "\tcase " goname ":\n" \ + "\t\treturn `" snaketocamel(subname) "`\n") + append(cg, "marshal", + goname ",\n") + append(cg, "unmarshal", + "\tcase `" snaketocamel(subname) "`:\n" \ + "\t\t*v = " goname "\n") +} + +function codegen_enum(name, cg, gotype, fields) { + gotype = PrefixCamel name + print "type " gotype " int8" + print "" + + print "const (" + print cg["fields"] ")" + print "" + + print "func (v " gotype ") String() string {" + print "\tswitch v {" + print cg["stringer"] "\tdefault:" + print "\t\treturn strconv.Itoa(int(v))" + print "\t}" + print "}" + print "" + + CodegenIsMarshaler[name] = 1 + fields = cg["marshal"] + sub(/,\n$/, ":", fields) + gsub(/\n/, "\n\t", fields) + print "func (v " gotype ") MarshalJSON() ([]byte, error) {" + print "\tswitch v {" + print indent("case " fields) + print "\t\treturn []byte(`\"` + v.String() + `\"`), nil" + print "\t}" + print "\treturn json.Marshal(int(v))" + print "}" + print "" + + print "func (v *" gotype ") UnmarshalJSON(data []byte) error {" + print "\tvar s string" + print "\tif json.Unmarshal(data, &s) == nil {" + print "\t\t// Handled below." + print "\t} else if n, err := protoUnmarshalEnumJSON(data); err != nil {" + print "\t\treturn err" + print "\t} else {" + print "\t\t*v = " gotype "(n)" + print "\t\treturn nil" + print "\t}" + print "" + print "\tswitch s {" + print cg["unmarshal"] "\tdefault:" + print "\t\treturn errors.New(`unrecognized value: ` + s)" + print "\t}" + print "\treturn nil" + print "}" + print "" + + # XXX: This should also check if it isn't out-of-range for any reason, + # but our usage of sprintf() stands in the way a bit. + CodegenSerialize[name] = "\tdata = append(data, uint8(%s))\n" + CodegenDeserialize[name] = \ + "\tif len(data) >= 1 {\n" \ + "\t\t%s, data = " gotype "(data[0]), data[1:]\n" \ + "\t} else {\n" \ + "\t\treturn nil, false\n" \ + "\t}\n" + + CodegenGoType[name] = gotype + for (i in cg) + delete cg[i] +} + +function codegen_marshal(type, f, marshal) { + if (CodegenAppendJSON[type]) + return sprintf(CodegenAppendJSON[type], f) + + # Complex types are json.Marshalers, there's no need to json.Marshal(&f). + if (CodegenIsMarshaler[type]) + marshal = f ".MarshalJSON()" + else + marshal = "json.Marshal(" f ")" + + return \ + "\tif j, err := " marshal "; err != nil {\n" \ + "\t\treturn nil, err\n" \ + "\t} else {\n" \ + "\t\tb = append(b, j...)\n" \ + "\t}\n" +} + +function codegen_struct_field_marshal(d, cg, camel, f, marshal) { + camel = snaketocamel(d["name"]) + f = "s." camel + if (!d["isarray"]) { + append(cg, "marshal", + "\tb = append(b, `,\"" decapitalize(camel) "\":`...)\n" \ + codegen_marshal(d["type"], f)) + return + } + + # Note that we do not produce `null` for nil slices, unlike encoding/json. + # And arrays never get deserialized as such. + if (d["type"] == "u8") { + append(cg, "marshal", + "\tb = append(b, `,\"" decapitalize(camel) "\":\"`...)\n" \ + "\tb = append(b, base64.StdEncoding.EncodeToString(" f ")...)\n" \ + "\tb = append(b, '\"')\n") + return + } + + append(cg, "marshal", + "\tb = append(b, `,\"" decapitalize(camel) "\":[`...)\n" \ + "\tfor i := 0; i < len(" f "); i++ {\n" \ + "\t\tif i > 0 {\n" \ + "\t\t\tb = append(b, ',')\n" \ + "\t\t}\n" \ + indent(codegen_marshal(d["type"], f "[i]")) \ + "\t}\n" \ + "\tb = append(b, ']')\n") +} + +function codegen_struct_field(d, cg, camel, f, serialize, deserialize) { + codegen_struct_field_marshal(d, cg) + + camel = snaketocamel(d["name"]) + f = "s." camel + serialize = CodegenSerialize[d["type"]] + deserialize = CodegenDeserialize[d["type"]] + if (!d["isarray"]) { + append(cg, "fields", "\t" camel " " CodegenGoType[d["type"]] \ + " `json:\"" decapitalize(camel) "\"`\n") + append(cg, "serialize", sprintf(serialize, f)) + append(cg, "deserialize", sprintf(deserialize, f)) + return + } + + append(cg, "fields", "\t" camel " []" CodegenGoType[d["type"]] \ + " `json:\"" decapitalize(camel) "\"`\n") + + # XXX: This should also check if it isn't out-of-range for any reason. + append(cg, "serialize", + sprintf(CodegenSerialize["u32"], "uint32(len(" f "))")) + if (d["type"] == "u8") { + append(cg, "serialize", + "\tdata = append(data, " f "...)\n") + } else { + append(cg, "serialize", + "\tfor i := 0; i < len(" f "); i++ {\n" \ + indent(sprintf(serialize, f "[i]")) \ + "\t}\n") + } + + append(cg, "deserialize", + "\t{\n" \ + "\t\tvar length uint32\n" \ + indent(sprintf(CodegenDeserialize["u32"], "length"))) + if (d["type"] == "u8") { + append(cg, "deserialize", + "\t\tif uint64(len(data)) < uint64(length) {\n" \ + "\t\t\treturn nil, false\n" \ + "\t\t}\n" \ + "\t\t" f ", data = data[:length], data[length:]\n" \ + "\t}\n") + } else { + append(cg, "deserialize", + "\t\t" f " = make([]" CodegenGoType[d["type"]] ", length)\n" \ + "\t}\n" \ + "\tfor i := 0; i < len(" f "); i++ {\n" \ + indent(sprintf(deserialize, f "[i]")) \ + "\t}\n") + } +} + +function codegen_struct_tag(d, cg, camel, f) { + codegen_struct_field_marshal(d, cg) + + camel = snaketocamel(d["name"]) + f = "s." camel + append(cg, "fields", "\t" camel " " CodegenGoType[d["type"]] \ + " `json:\"" decapitalize(camel) "\"`\n") + append(cg, "serialize", sprintf(CodegenSerialize[d["type"]], f)) + # Do not deserialize here, that is already done by the containing union. +} + +function codegen_struct(name, cg, gotype) { + gotype = PrefixCamel name + print "type " gotype " struct {\n" cg["fields"] "}\n" + + if (cg["marshal"]) { + CodegenIsMarshaler[name] = 1 + print "func (s *" gotype ") MarshalJSON() ([]byte, error) {" + print "\tb := []byte{}" + print cg["marshal"] "\tb[0] = '{'" + print "\treturn append(b, '}'), nil" + print "}" + print "" + } + + if (cg["serialize"]) { + print "func (s *" gotype ") AppendTo(data []byte) ([]byte, bool) {" + print "\tok := true" + print cg["serialize"] "\treturn data, ok" + print "}" + print "" + + CodegenSerialize[name] = \ + "\tif data, ok = %s.AppendTo(data); !ok {\n" \ + "\t\treturn nil, ok\n" \ + "\t}\n" + } + if (cg["deserialize"]) { + print "func (s *" gotype ") ConsumeFrom(data []byte) ([]byte, bool) {" + print "\tok := true" + print cg["deserialize"] "\treturn data, ok" + print "}" + print "" + + CodegenDeserialize[name] = \ + "\tif data, ok = %s.ConsumeFrom(data); !ok {\n" \ + "\t\treturn nil, ok\n" \ + "\t}\n" + } + + CodegenGoType[name] = gotype + for (i in cg) + delete cg[i] +} + +function codegen_union_tag(d, cg) { + cg["tagtype"] = d["type"] + cg["tagname"] = d["name"] + # The tag is implied from the type of struct stored in the interface. +} + +function codegen_union_struct(name, casename, cg, scg, structname, init) { + # And thus not all generated structs are present in Types. + structname = name snaketocamel(casename) + codegen_struct(structname, scg) + + init = CodegenGoType[structname] "{" snaketocamel(cg["tagname"]) \ + ": " decapitalize(snaketocamel(cg["tagname"])) "}" + append(cg, "unmarshal", + "\tcase " CodegenGoType[cg["tagtype"]] snaketocamel(casename) ":\n" \ + "\t\ts := " init "\n" \ + "\t\terr = json.Unmarshal(data, &s)\n" \ + "\t\tu.Interface = &s\n") + append(cg, "serialize", + "\tcase *" CodegenGoType[structname] ":\n" \ + indent(sprintf(CodegenSerialize[structname], "union"))) + append(cg, "deserialize", + "\tcase " CodegenGoType[cg["tagtype"]] snaketocamel(casename) ":\n" \ + "\t\ts := " init "\n" \ + indent(sprintf(CodegenDeserialize[structname], "s")) \ + "\t\tu.Interface = &s\n") +} + +function codegen_union(name, cg, gotype, tagfield, tagvar) { + gotype = PrefixCamel name + print "type " gotype " struct {" + print "\tInterface any" + print "}" + print "" + + # This cannot be a pointer method, it wouldn't work recursively. + CodegenIsMarshaler[name] = 1 + print "func (u " gotype ") MarshalJSON() ([]byte, error) {" + print "\treturn u.Interface.(json.Marshaler).MarshalJSON()" + print "}" + print "" + + tagfield = snaketocamel(cg["tagname"]) + tagvar = decapitalize(tagfield) + print "func (u *" gotype ") UnmarshalJSON(data []byte) (err error) {" + print "\tvar t struct {" + print "\t\t" tagfield " " CodegenGoType[cg["tagtype"]] \ + " `json:\"" tagvar "\"`" + print "\t}" + print "\tif err := json.Unmarshal(data, &t); err != nil {" + print "\t\treturn err" + print "\t}" + print "" + print "\tswitch " tagvar " := t." tagfield "; " tagvar " {" + print cg["unmarshal"] "\tdefault:" + print "\t\terr = errors.New(`unsupported value: ` + " tagvar ".String())" + print "\t}" + print "\treturn err" + print "}" + print "" + + # XXX: Consider changing the interface into an AppendTo/ConsumeFrom one, + # that would eliminate these type case switches entirely. + # On the other hand, it would make it possible to send unsuitable structs. + print "func (u *" gotype ") AppendTo(data []byte) ([]byte, bool) {" + print "\tok := true" + print "\tswitch union := u.Interface.(type) {" + print cg["serialize"] "\tdefault:" + print "\t\treturn nil, false" + print "\t}" + print "\treturn data, ok" + print "}" + print "" + + CodegenSerialize[name] = \ + "\tif data, ok = %s.AppendTo(data); !ok {\n" \ + "\t\treturn nil, ok\n" \ + "\t}\n" + + print "func (u *" gotype ") ConsumeFrom(data []byte) ([]byte, bool) {" + print "\tok := true" + print "\tvar " tagvar " " CodegenGoType[cg["tagtype"]] + print sprintf(CodegenDeserialize[cg["tagtype"]], tagvar) + print "\tswitch " tagvar " {" + print cg["deserialize"] "\tdefault:" + print "\t\treturn nil, false" + print "\t}" + print "\treturn data, ok" + print "}" + print "" + + CodegenDeserialize[name] = \ + "\tif data, ok = %s.ConsumeFrom(data); !ok {\n" \ + "\t\treturn nil, ok\n" \ + "\t}\n" + + CodegenGoType[name] = gotype + for (i in cg) + delete cg[i] +} |