Repository: ogen-go/ogen
Pull request #446 (2022-07-01 00:09:40)

feat(gen): support `text/plain` as simple stream


Patch:
diff --git a/examples/ex_github/oas_client_gen.go b/examples/ex_github/oas_client_gen.go
index f92ab179..68971632 100644
--- a/examples/ex_github/oas_client_gen.go
+++ b/examples/ex_github/oas_client_gen.go
@@ -27805,6 +27805,50 @@ func (c *Client) MetaGet(ctx context.Context) (res MetaGetRes, err error) {
 	return result, nil
 }
 
+// MetaGetZen invokes meta/get-zen operation.
+//
+// Get a random sentence from the Zen of GitHub.
+//
+// GET /zen
+func (c *Client) MetaGetZen(ctx context.Context) (res MetaGetZenOK, err error) {
+	startTime := time.Now()
+	otelAttrs := []attribute.KeyValue{
+		otelogen.OperationID("meta/get-zen"),
+	}
+	ctx, span := c.cfg.Tracer.Start(ctx, "MetaGetZen",
+		trace.WithAttributes(otelAttrs...),
+		trace.WithSpanKind(trace.SpanKindClient),
+	)
+	defer func() {
+		if err != nil {
+			span.RecordError(err)
+			c.errors.Add(ctx, 1, otelAttrs...)
+		} else {
+			elapsedDuration := time.Since(startTime)
+			c.duration.Record(ctx, elapsedDuration.Microseconds(), otelAttrs...)
+		}
+		span.End()
+	}()
+	c.requests.Add(ctx, 1, otelAttrs...)
+	u := uri.Clone(c.serverURL)
+	u.Path += "/zen"
+
+	r := ht.NewRequest(ctx, "GET", u, nil)
+
+	resp, err := c.cfg.Client.Do(r)
+	if err != nil {
+		return res, errors.Wrap(err, "do request")
+	}
+	defer resp.Body.Close()
+
+	result, err := decodeMetaGetZenResponse(resp, span)
+	if err != nil {
+		return res, errors.Wrap(err, "decode response")
+	}
+
+	return result, nil
+}
+
 // MetaRoot invokes meta/root operation.
 //
 // Get Hypermedia links to resources accessible in GitHub's REST API.
diff --git a/examples/ex_github/oas_handlers_gen.go b/examples/ex_github/oas_handlers_gen.go
index e1b32ded..c08cd096 100644
--- a/examples/ex_github/oas_handlers_gen.go
+++ b/examples/ex_github/oas_handlers_gen.go
@@ -13438,6 +13438,42 @@ func (s *Server) handleMetaGetRequest(args [0]string, w http.ResponseWriter, r *
 	s.duration.Record(ctx, elapsedDuration.Microseconds(), otelAttrs...)
 }
 
+// HandleMetaGetZenRequest handles meta/get-zen operation.
+//
+// GET /zen
+func (s *Server) handleMetaGetZenRequest(args [0]string, w http.ResponseWriter, r *http.Request) {
+	startTime := time.Now()
+	otelAttrs := []attribute.KeyValue{
+		otelogen.OperationID("meta/get-zen"),
+	}
+	ctx, span := s.cfg.Tracer.Start(r.Context(), "MetaGetZen",
+		trace.WithAttributes(otelAttrs...),
+		trace.WithSpanKind(trace.SpanKindServer),
+	)
+	s.requests.Add(ctx, 1, otelAttrs...)
+	defer span.End()
+
+	var err error
+
+	response, err := s.h.MetaGetZen(ctx)
+	if err != nil {
+		span.RecordError(err)
+		span.SetStatus(codes.Error, "Internal")
+		s.errors.Add(ctx, 1, otelAttrs...)
+		s.cfg.ErrorHandler(ctx, w, r, err)
+		return
+	}
+
+	if err := encodeMetaGetZenResponse(response, w, span); err != nil {
+		span.RecordError(err)
+		span.SetStatus(codes.Error, "Response")
+		s.errors.Add(ctx, 1, otelAttrs...)
+		return
+	}
+	elapsedDuration := time.Since(startTime)
+	s.duration.Record(ctx, elapsedDuration.Microseconds(), otelAttrs...)
+}
+
 // HandleMetaRootRequest handles meta/root operation.
 //
 // GET /
diff --git a/examples/ex_github/oas_response_decoders_gen.go b/examples/ex_github/oas_response_decoders_gen.go
index 6b936f4c..ad1ab150 100644
--- a/examples/ex_github/oas_response_decoders_gen.go
+++ b/examples/ex_github/oas_response_decoders_gen.go
@@ -3,6 +3,7 @@
 package api
 
 import (
+	"bytes"
 	"io"
 	"mime"
 	"net/http"
@@ -16245,6 +16246,29 @@ func decodeMetaGetResponse(resp *http.Response, span trace.Span) (res MetaGetRes
 		return res, validate.UnexpectedStatusCode(resp.StatusCode)
 	}
 }
+func decodeMetaGetZenResponse(resp *http.Response, span trace.Span) (res MetaGetZenOK, err error) {
+	switch resp.StatusCode {
+	case 200:
+		ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
+		if err != nil {
+			return res, errors.Wrap(err, "parse media type")
+		}
+		switch {
+		case ct == "text/plain":
+			b, err := io.ReadAll(resp.Body)
+			if err != nil {
+				return res, err
+			}
+
+			response := MetaGetZenOK{Data: bytes.NewReader(b)}
+			return response, nil
+		default:
+			return res, validate.InvalidContentType(ct)
+		}
+	default:
+		return res, validate.UnexpectedStatusCode(resp.StatusCode)
+	}
+}
 func decodeMetaRootResponse(resp *http.Response, span trace.Span) (res MetaRootOK, err error) {
 	switch resp.StatusCode {
 	case 200:
diff --git a/examples/ex_github/oas_response_encoders_gen.go b/examples/ex_github/oas_response_encoders_gen.go
index 8a1c6d96..ca8bbaa9 100644
--- a/examples/ex_github/oas_response_encoders_gen.go
+++ b/examples/ex_github/oas_response_encoders_gen.go
@@ -3,6 +3,7 @@
 package api
 
 import (
+	"io"
 	"net/http"
 
 	"github.com/go-faster/errors"
@@ -8463,6 +8464,16 @@ func encodeMetaGetResponse(response MetaGetRes, w http.ResponseWriter, span trac
 		return errors.Errorf("/meta"+`: unexpected response type: %T`, response)
 	}
 }
+func encodeMetaGetZenResponse(response MetaGetZenOK, w http.ResponseWriter, span trace.Span) error {
+	w.Header().Set("Content-Type", "text/plain")
+	w.WriteHeader(200)
+	span.SetStatus(codes.Ok, http.StatusText(200))
+	if _, err := io.Copy(w, response); err != nil {
+		return errors.Wrap(err, "write")
+	}
+	return nil
+
+}
 func encodeMetaRootResponse(response MetaRootOK, w http.ResponseWriter, span trace.Span) error {
 	w.Header().Set("Content-Type", "application/json")
 	w.WriteHeader(200)
diff --git a/examples/ex_github/oas_router_gen.go b/examples/ex_github/oas_router_gen.go
index 7d6e9c91..09c74fad 100644
--- a/examples/ex_github/oas_router_gen.go
+++ b/examples/ex_github/oas_router_gen.go
@@ -12122,6 +12122,19 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 						}
 					}
 				}
+			case 'z': // Prefix: "zen"
+				if l := len("zen"); len(elem) >= l && elem[0:l] == "zen" {
+					elem = elem[l:]
+				} else {
+					break
+				}
+
+				if len(elem) == 0 {
+					// Leaf: MetaGetZen
+					s.handleMetaGetZenRequest([0]string{}, w, r)
+
+					return
+				}
 			}
 		}
 	case "PATCH":
@@ -30007,6 +30020,20 @@ func (s *Server) FindRoute(method, path string) (r Route, _ bool) {
 						}
 					}
 				}
+			case 'z': // Prefix: "zen"
+				if l := len("zen"); len(elem) >= l && elem[0:l] == "zen" {
+					elem = elem[l:]
+				} else {
+					break
+				}
+
+				if len(elem) == 0 {
+					// Leaf: MetaGetZen
+					r.name = "MetaGetZen"
+					r.args = args
+					r.count = 0
+					return r, true
+				}
 			}
 		}
 	case "PATCH":
diff --git a/examples/ex_github/oas_schemas_gen.go b/examples/ex_github/oas_schemas_gen.go
index fd3a5de3..f5e01423 100644
--- a/examples/ex_github/oas_schemas_gen.go
+++ b/examples/ex_github/oas_schemas_gen.go
@@ -3,6 +3,7 @@
 package api
 
 import (
+	"io"
 	"net/url"
 	"time"
 
@@ -7267,6 +7268,14 @@ const (
 	MergedUpstreamMergeTypeNone             MergedUpstreamMergeType = "none"
 )
 
+type MetaGetZenOK struct {
+	Data io.Reader
+}
+
+func (s MetaGetZenOK) Read(p []byte) (n int, err error) {
+	return s.Data.Read(p)
+}
+
 type MetaRootOK struct {
 	CurrentUserURL                   string    "json:\"current_user_url\""
 	CurrentUserAuthorizationsHTMLURL string    "json:\"current_user_authorizations_html_url\""
diff --git a/examples/ex_github/oas_server_gen.go b/examples/ex_github/oas_server_gen.go
index 5723a480..8fcf278a 100644
--- a/examples/ex_github/oas_server_gen.go
+++ b/examples/ex_github/oas_server_gen.go
@@ -3044,6 +3044,12 @@ type Handler interface {
 	//
 	// GET /meta
 	MetaGet(ctx context.Context) (MetaGetRes, error)
+	// MetaGetZen implements meta/get-zen operation.
+	//
+	// Get a random sentence from the Zen of GitHub.
+	//
+	// GET /zen
+	MetaGetZen(ctx context.Context) (MetaGetZenOK, error)
 	// MetaRoot implements meta/root operation.
 	//
 	// Get Hypermedia links to resources accessible in GitHub's REST API.
diff --git a/examples/ex_github/oas_unimplemented_gen.go b/examples/ex_github/oas_unimplemented_gen.go
index 46f5e948..cd619b0a 100644
--- a/examples/ex_github/oas_unimplemented_gen.go
+++ b/examples/ex_github/oas_unimplemented_gen.go
@@ -3903,6 +3903,15 @@ func (UnimplementedHandler) MetaGet(ctx context.Context) (r MetaGetRes, _ error)
 	return r, ht.ErrNotImplemented
 }
 
+// MetaGetZen implements meta/get-zen operation.
+//
+// Get a random sentence from the Zen of GitHub.
+//
+// GET /zen
+func (UnimplementedHandler) MetaGetZen(ctx context.Context) (r MetaGetZenOK, _ error) {
+	return r, ht.ErrNotImplemented
+}
+
 // MetaRoot implements meta/root operation.
 //
 // Get Hypermedia links to resources accessible in GitHub's REST API.
diff --git a/examples/ex_k8s/oas_response_decoders_gen.go b/examples/ex_k8s/oas_response_decoders_gen.go
index e521d53f..4faf65c0 100644
--- a/examples/ex_k8s/oas_response_decoders_gen.go
+++ b/examples/ex_k8s/oas_response_decoders_gen.go
@@ -3,6 +3,7 @@
 package api
 
 import (
+	"bytes"
 	"io"
 	"mime"
 	"net/http"
@@ -6821,6 +6822,14 @@ func decodeReadCoreV1NamespacedPodLogResponse(resp *http.Response, span trace.Sp
 				return res, err
 			}
 			return &response, nil
+		case ct == "text/plain":
+			b, err := io.ReadAll(resp.Body)
+			if err != nil {
+				return res, err
+			}
+
+			response := ReadCoreV1NamespacedPodLogOKTextPlain{Data: bytes.NewReader(b)}
+			return &response, nil
 		default:
 			return res, validate.InvalidContentType(ct)
 		}
diff --git a/examples/ex_k8s/oas_response_encoders_gen.go b/examples/ex_k8s/oas_response_encoders_gen.go
index 55efa34b..c6fe6043 100644
--- a/examples/ex_k8s/oas_response_encoders_gen.go
+++ b/examples/ex_k8s/oas_response_encoders_gen.go
@@ -3,6 +3,7 @@
 package api
 
 import (
+	"io"
 	"net/http"
 
 	"github.com/go-faster/errors"
@@ -4614,6 +4615,15 @@ func encodeReadCoreV1NamespacedPodLogResponse(response ReadCoreV1NamespacedPodLo
 		}
 		return nil
 
+	case *ReadCoreV1NamespacedPodLogOKTextPlain:
+		w.Header().Set("Content-Type", "text/plain")
+		w.WriteHeader(200)
+		span.SetStatus(codes.Ok, http.StatusText(200))
+		if _, err := io.Copy(w, response); err != nil {
+			return errors.Wrap(err, "write")
+		}
+		return nil
+
 	case *ReadCoreV1NamespacedPodLogUnauthorized:
 		w.WriteHeader(401)
 		span.SetStatus(codes.Error, http.StatusText(401))
diff --git a/examples/ex_k8s/oas_schemas_gen.go b/examples/ex_k8s/oas_schemas_gen.go
index fb26d96c..806b6a91 100644
--- a/examples/ex_k8s/oas_schemas_gen.go
+++ b/examples/ex_k8s/oas_schemas_gen.go
@@ -3,6 +3,7 @@
 package api
 
 import (
+	"io"
 	"time"
 )
 
@@ -24940,6 +24941,16 @@ type ReadCoreV1NamespacedPodLogOKApplicationJSON string
 
 func (*ReadCoreV1NamespacedPodLogOKApplicationJSON) readCoreV1NamespacedPodLogRes() {}
 
+type ReadCoreV1NamespacedPodLogOKTextPlain struct {
+	Data io.Reader
+}
+
+func (s ReadCoreV1NamespacedPodLogOKTextPlain) Read(p []byte) (n int, err error) {
+	return s.Data.Read(p)
+}
+
+func (*ReadCoreV1NamespacedPodLogOKTextPlain) readCoreV1NamespacedPodLogRes() {}
+
 // ReadCoreV1NamespacedPodLogUnauthorized is response for ReadCoreV1NamespacedPodLog operation.
 type ReadCoreV1NamespacedPodLogUnauthorized struct{}
 
diff --git a/gen/gen_contents.go b/gen/gen_contents.go
index 2dc5e9d4..6dfeacef 100644
--- a/gen/gen_contents.go
+++ b/gen/gen_contents.go
@@ -252,14 +252,29 @@ func (g *Generator) generateContents(
 				return nil
 
 			case "application/octet-stream":
-				if media.Schema != nil && !isBinary(media.Schema) {
-					return errors.Errorf("octet stream with %q schema not supported", media.Schema.Type)
+				if s := media.Schema; s != nil && !isBinary(s) {
+					return errors.Wrapf(
+						&ErrNotImplemented{Name: "complex application/octet-stream"},
+						"generate %q", s.Type,
+					)
 				}
 
 				t := ir.Stream(typeName)
 				result[ir.ContentTypeOctetStream] = t
 				return ctx.saveType(t)
 
+			case "text/plain":
+				if s := media.Schema; s != nil && s.Type != "string" {
+					return errors.Wrapf(
+						&ErrNotImplemented{Name: "complex text/plain"},
+						"generate %q", s.Type,
+					)
+				}
+
+				t := ir.Stream(typeName)
+				result[ir.ContentTypeTextPlain] = t
+				return ctx.saveType(t)
+
 			default:
 				if isBinary(media.Schema) {
 					t := ir.Stream(typeName)
diff --git a/gen/ir/operation.go b/gen/ir/operation.go
index 69bd037c..cd746f3b 100644
--- a/gen/ir/operation.go
+++ b/gen/ir/operation.go
@@ -110,6 +110,8 @@ const (
 	ContentTypeMultipart ContentType = "multipart/form-data"
 	// ContentTypeOctetStream is ContentType for binary.
 	ContentTypeOctetStream ContentType = "application/octet-stream"
+	// ContentTypeTextPlain is ContentType for text.
+	ContentTypeTextPlain ContentType = "text/plain"
 )
 
 func (t ContentType) String() string { return string(t) }
@@ -124,11 +126,13 @@ func (t ContentType) MultipartForm() bool { return t == ContentTypeMultipart }
 
 func (t ContentType) OctetStream() bool { return t == ContentTypeOctetStream }
 
+func (t ContentType) TextPlain() bool { return t == ContentTypeTextPlain }
+
 func (t ContentType) EncodedDataTypeGo() string {
 	switch t {
 	case ContentTypeJSON:
 		return "*jx.Encoder"
-	case ContentTypeOctetStream:
+	case ContentTypeOctetStream, ContentTypeTextPlain:
 		return "io.Reader"
 	default:
 		return "io.Reader"


Test patch:
diff --git a/gen_test.go b/gen_test.go
index c15f22bc..0dfb04f1 100644
--- a/gen_test.go
+++ b/gen_test.go
@@ -71,10 +71,10 @@ func TestGenerate(t *testing.T) {
 		"autorest/ApiManagementClient-openapi.json": {
 			"oauth2 security",
 		},
-		"autorest/lro.json":     {},
-		"autorest/storage.json": {},
+		"autorest/lro.json":                 {},
+		"autorest/storage.json":             {},
 		"autorest/xms-error-responses.json": {},
-		"2ch.yml": {},
+		"2ch.yml":                           {},
 		"api.github.com.json": {
 			"complex anyOf",
 			"discriminator inference",
@@ -101,9 +101,7 @@ func TestGenerate(t *testing.T) {
 		"redoc/discriminator.json": {
 			"unsupported content types",
 		},
-		"superset.json": {
-			"unsupported content types",
-		},
+		"superset.json": {},
 	}
 
 	testDataPath := "_testdata/positive"