feat(gen): support SSE server response generation#1715
Conversation
|
Please consider using separate commits for generated files. 🙏 Line 24 in 961e0c4 |
e59dd96 to
e9ad4ef
Compare
Done :) |
|
Original author of the SSE client generation feature here. While working on SSE support, I came to the conclusion that it is much more about getting the behavior right than just adding code generation on top of it. A lot of the decisions here affect the public API and runtime semantics, so they need to be thought through in advance to avoid future breaking changes. That is also the main reason why I did not rush server generation earlier. Because of that, I think a PR like this needs feature-level discussion before merge, to make sure the generated API and runtime behavior match the actual requirements. I took a look at your implementation and found several concerns. Some of them are purely structural, but some are behavioral and should probably be discussed before merge. First, I actually like the idea of having an
So overall, I believe this feature should first go through a short design discussion, and only then move toward merge. |
| {{ define "response_encoders/sse_event" }} | ||
| {{- /*gotype: github.com/ogen-go/ogen/gen.TypeElem*/ -}}{{ $t := $.Type }} | ||
| {{- /*gotype: github.com/ogen-go/ogen/gen.TemplateConfig*/ -}}{{ $cfg := $.Config }} | ||
| {{- $sse := $t.SSE }} | ||
| // encode{{ $t.Name }}Event encodes {{ $sse.EventType.Go }} as a Server-Sent Event. | ||
| func encode{{ $t.Name }}Event(event {{ $sse.EventType.Go }}) (sse.Event, error) { | ||
| {{- if eq $sse.Shape "data-only" }} | ||
| raw := sse.Event{ | ||
| ID: event.ID, | ||
| Type: event.Type, | ||
| } | ||
| {{- if and $sse.DataType.NeedValidation $cfg.ResponseValidationEnabled }} | ||
| if err := func() error { | ||
| {{- template "validate" elem $sse.DataType "event.Data" }} | ||
| }(); err != nil { | ||
| return raw, errors.Wrap(err, "validate") | ||
| } | ||
| {{- end }} | ||
| if v, ok := event.Retry.Get(); ok { | ||
| retry := time.Duration(v) * time.Millisecond | ||
| raw.Retry = &retry | ||
| } | ||
| {{- if $sse.DataType.IsString }} | ||
| raw.Data = event.Data | ||
| {{- else }} | ||
| e := jx.GetEncoder() | ||
| defer jx.PutEncoder(e) | ||
| {{- template "json/enc" elem $sse.DataType "event.Data" }} | ||
| {{- if $sse.DataType.IsJSONString }} | ||
| // Data is a string, so it is sent as-is, without JSON quoting. | ||
| data, err := jx.DecodeBytes(e.Bytes()).Str() | ||
| if err != nil { | ||
| return raw, errors.Wrap(err, "encode data") | ||
| } | ||
| raw.Data = data | ||
| {{- else }} | ||
| raw.Data = string(e.Bytes()) | ||
| {{- end }} | ||
| {{- end }} | ||
| return raw, nil | ||
| {{- else if or (eq $sse.Shape "full") (eq $sse.Shape "full-array") }} | ||
| var raw sse.Event | ||
| {{- if and $sse.EventType.NeedValidation $cfg.ResponseValidationEnabled }} | ||
| if err := func() error { | ||
| {{- template "validate" elem $sse.EventType "event" }} | ||
| }(); err != nil { | ||
| return raw, errors.Wrap(err, "validate") | ||
| } | ||
| {{- end }} | ||
| e := jx.GetEncoder() | ||
| defer jx.PutEncoder(e) | ||
| {{- template "json/enc" elem $sse.EventType "event" }} | ||
|
|
||
| // Event schema describes the full event envelope, so the standard fields | ||
| // are taken from the encoded object. | ||
| if err := jx.DecodeBytes(e.Bytes()).ObjBytes(func(d *jx.Decoder, key []byte) error { | ||
| if d.Next() == jx.Null { | ||
| return d.Skip() | ||
| } | ||
| switch string(key) { | ||
| case "id": | ||
| v, err := d.Str() | ||
| if err != nil { | ||
| return err | ||
| } | ||
| raw.ID = v | ||
| case "event": | ||
| v, err := d.Str() | ||
| if err != nil { | ||
| return err | ||
| } | ||
| raw.Type = v | ||
| case "retry": | ||
| v, err := d.Int64() | ||
| if err != nil { | ||
| return err | ||
| } | ||
| retry := time.Duration(v) * time.Millisecond | ||
| raw.Retry = &retry | ||
| case "data": | ||
| v, err := d.Raw() | ||
| if err != nil { | ||
| return err | ||
| } | ||
| // String data is sent as-is, any other value is sent as JSON. | ||
| if v.Type() == jx.String { | ||
| data, err := jx.DecodeBytes(v).Str() | ||
| if err != nil { | ||
| return err | ||
| } | ||
| raw.Data = data | ||
| return nil | ||
| } | ||
| raw.Data = string(v) | ||
| default: | ||
| return d.Skip() | ||
| } | ||
| return nil | ||
| }); err != nil { | ||
| return raw, errors.Wrap(err, "encode event") | ||
| } | ||
| return raw, nil |
There was a problem hiding this comment.
This is actually strange. Based on the code, the logic is that depending on the SSE shape, the event is encoded differently.
But the thing is that SSE shape is spec representation only thing - it should not imply that some SSE fields are used and some are not. All represented SSE events should be encoded the same
Generate the server side of
text/event-streamresponses, which previously raised ErrNotImplemented("sse server response encoding").The generated response stream type gains an
Eventsfield, called by the server after the response headers are sent:Client and server sides of a stream type are now generated independently, so client-only output is unchanged and server-only output contains no stream reader.
data-only,fullandfull-arrayshapes, honoring server/response/validation. String data is written unquoted, so that the stream stays consumable by EventSource clients;