Přemysl Eric Janouch
8cd94b30f6
Currently it only goes for the longest common prefix. Refactor WebSocket handling into an abstraction for our protocol. The Go code generater finally needed fixing.
449 lines
13 KiB
Awk
449 lines
13 KiB
Awk
# 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)
|
|
|
|
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) {
|
|
shortname = "u" size
|
|
gotype = "uint" size
|
|
define_internal(shortname, gotype)
|
|
|
|
# Both byte and uint8 luckily marshal as base64-encoded JSON strings.
|
|
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")
|
|
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/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.MaxInt32 || n < math.MinInt32 {"
|
|
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 " int"
|
|
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 ""
|
|
|
|
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 json.Marshal(v.String())"
|
|
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 = binary.BigEndian.AppendUint32(data, uint32(%s))\n"
|
|
CodegenDeserialize[name] = \
|
|
"\tif len(data) >= 4 {\n" \
|
|
"\t\t%s = " gotype "(int32(binary.BigEndian.Uint32(data)))\n" \
|
|
"\t\tdata = data[4:]\n" \
|
|
"\t} else {\n" \
|
|
"\t\treturn nil, false\n" \
|
|
"\t}\n"
|
|
|
|
CodegenGoType[name] = gotype
|
|
for (i in cg)
|
|
delete cg[i]
|
|
}
|
|
|
|
function codegen_struct_field(d, cg, camel, f, serialize, deserialize) {
|
|
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) {
|
|
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["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.
|
|
print "func (u " gotype ") MarshalJSON() ([]byte, error) {"
|
|
print "\treturn json.Marshal(u.Interface)"
|
|
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]
|
|
}
|