Protobuf and Connect
protoc-gen-goalchemy is a protoc and buf plugin that turns .proto
files into Goalchemy-subset Go: one struct per message and proto3 JSON
encoding through std/encoding/protojson. The generated code is ordinary
source, so it compiles to every target with the rest of a program.
go install github.com/eugenioenko/goalchemy/cmd/protoc-gen-goalchemy@latestversion: v2plugins: - local: protoc-gen-goalchemy out: gen opt: - go_import_prefix=example.com/app/gen - include=policy.attributes.ListAttributesResponse;kas.RewrapRequestParameters
Section titled “Parameters”| Parameter | Effect |
|---|---|
go_import_prefix=<path> |
Places each file’s package at <path>/<proto directory> instead of its go_package. |
include=<full.Name>[;<full.Name>...] |
Generates only these messages, enums, services and methods (pkg.Service.Method) and the types they reference, across files. Without it every message and unary method in the requested files is generated. |
getters=false |
Omits the nil-safe GetX accessors. |
Generate only what a program uses: every message adds code, and Swift and Rust build time grows with the amount of code.
Generated code
Section titled “Generated code”Names follow protoc-gen-go: KeyAccess, AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_HIERARCHY,
EntityIdentifier_Token for a oneof member, and an isEntityIdentifier_Identifier
interface for the oneof field.
| Proto | Go |
|---|---|
| scalar | bool, int32, int64, uint32, uint64, float32, float64, string, []byte |
optional scalar or enum |
pointer to the scalar (*bool), encoded whenever set, even to zero; GetX returns the pointer |
optional message |
pointer to the generated struct, as without optional |
| enum | named int32 with _name and _value maps and String |
| message | pointer to the generated struct |
repeated T, map<K, V> |
[]T, map[K]V |
oneof |
interface field set to a wrapper struct per member |
google.protobuf.Timestamp, Duration, Empty, FieldMask, Any |
*protojson.Timestamp and so on |
google.protobuf.Struct, Value, ListValue |
jsonvalue.Value (zero means unset) |
wrappers such as BoolValue |
pointer to the scalar (*bool); BytesValue is []byte |
Every message has EncodeProtoJSON and DecodeProtoJSON (the
protojson.Message interface), MarshalJSON and UnmarshalJSON, and,
unless disabled, GetX accessors. Each generated file registers its
messages with protojson.Register so that Any payloads of those types are
decoded and re-encoded canonically.
attr := &policy.Attribute{Name: "classification", Rule: policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_HIERARCHY}data, err := protojson.Marshal(attr)
var resp attributes.ListAttributesResponseerr = protojson.Unmarshal(body, &resp)for _, a := range resp.GetAttributes() { println(a.GetFqn())}Services
Section titled “Services”Each service with unary methods gets a <file>.connect.go file holding a
client over std/connect, the Connect protocol with the
JSON codec. Streaming methods are skipped. Calls suspend on the host HTTP
client, so programs that make them compile with the cooperative gate.
client := connect.NewClient("https://platform.example.com")client.Headers = []string{"Authorization", "Bearer " + token}attrs := attributes.NewAttributesServiceClient(client)
resp, err := attrs.GetAttribute(ctx, &attributes.GetAttributeRequest{ Identifier: &attributes.GetAttributeRequest_Fqn{Fqn: "https://example.com/attr/classification"},})if connect.CodeOf(err) == connect.CodeNotFound { // ...}Each method also has a <Service><Method>Procedure constant, and
Client.CallUnary calls any procedure with per-call headers and returns the
response headers. Requests are POSTed as application/json with
Connect-Protocol-Version: 1. Failures are
*connect.Error values with the code, message, details (type URL, binary
value and JSON debug form) and response headers that connect-go reports for
the same response, including the HTTP-status fallbacks for non-Connect
error bodies. A context that is already cancelled or past its deadline
yields canceled or deadline_exceeded; other transport failures are
unavailable.
Client.TimeoutMillis (default 30 s) bounds each call locally, and a
context deadline can shorten it; Client.MaxResponseBytes (default 16 MiB)
bounds the reply. The timeout is not sent unless Client.SendTimeout is
true, in which case it goes to the server as Connect-Timeout-Ms.
connect-go sends that header only when the context has a deadline, and the
subset cannot read a context’s deadline, so it is off by default: a server
whose CORS allow-list lacks Connect-Timeout-Ms, such as a stock OpenTDF
platform, would otherwise fail every browser call at the preflight.
Client.HTTPClient replaces the transport, like connect-go’s HTTPClient
and interceptors: set it to add credentials, DPoP proofs or an allowlist, or
to record calls in tests. Its Do has the arguments and results of
lib/http.Do; it receives the method, the full URL, the Connect and client
headers, the JSON body and the client’s response bound and timeout, and a
non-2xx status is a result rather than an error. Its response is decoded
exactly as a lib/http response, so status and Connect error mapping is the
same either way. When it is nil, calls go through lib/http.DoWith with the
Client.Transport options.
type authTransport struct{ token string }
func (t *authTransport) Do(ctx context.Context, method, url string, headers []string, body []byte, maxResponseBytes int, timeoutMillis int64) (int, []string, []byte, error) { headers = append(append([]string(nil), headers...), "Authorization", "Bearer "+t.token) return http.Do(ctx, method, url, headers, body, maxResponseBytes, timeoutMillis)}
client.HTTPClient = &authTransport{token: token}Differences from google.golang.org/protobuf/encoding/protojson
Section titled “Differences from google.golang.org/protobuf/encoding/protojson”Output is protojson.Marshal’s with insignificant whitespace removed, and
input follows protojson.UnmarshalOptions{DiscardUnknown: true}, the options
Connect clients use. The proto_platform language fixture checks both
against protojson on every target, using the OpenTDF platform’s messages.
- Unregistered
Anypayloads are kept as received instead of failing with “unable to resolve”. Payloads of generated messages and of the well-known types are decoded and re-encoded as protojson does. - Error messages use protojson’s wording where practical but do not include byte offsets.
- Extensions, groups and proto2 defaults are not supported. proto3
optionalis supported, and the plugin declares it toprotoc.