|
| 1 | +// Copyright (c) 2025 Z5Labs and Contributors |
| 2 | +// |
| 3 | +// This software is released under the MIT License. |
| 4 | +// https://opensource.org/licenses/MIT |
| 5 | + |
| 6 | +package rpc |
| 7 | + |
| 8 | +import ( |
| 9 | + "context" |
| 10 | + "encoding/json" |
| 11 | + "net/http" |
| 12 | + |
| 13 | + "github.com/z5labs/humus/internal/ptr" |
| 14 | + "github.com/z5labs/humus/internal/try" |
| 15 | + |
| 16 | + "github.com/swaggest/jsonschema-go" |
| 17 | + "github.com/swaggest/openapi-go/openapi3" |
| 18 | + "go.opentelemetry.io/otel" |
| 19 | +) |
| 20 | + |
| 21 | +// ReturnJsonHandler |
| 22 | +type ReturnJsonHandler[Req, Resp any] struct { |
| 23 | + inner Handler[Req, Resp] |
| 24 | +} |
| 25 | + |
| 26 | +// ReturnJson initializes a [ReturnJsonHandler]. |
| 27 | +func ReturnJson[Req, Resp any](h Handler[Req, Resp]) *ReturnJsonHandler[Req, Resp] { |
| 28 | + return &ReturnJsonHandler[Req, Resp]{ |
| 29 | + inner: h, |
| 30 | + } |
| 31 | +} |
| 32 | + |
| 33 | +// JsonResponse |
| 34 | +type JsonResponse[T any] struct { |
| 35 | + inner *T |
| 36 | +} |
| 37 | + |
| 38 | +// Spec implements the [TypedResponse] interface. |
| 39 | +func (*JsonResponse[T]) Spec() (int, *openapi3.Response, error) { |
| 40 | + var t T |
| 41 | + var reflector jsonschema.Reflector |
| 42 | + |
| 43 | + jsonSchema, err := reflector.Reflect(t, jsonschema.InlineRefs) |
| 44 | + if err != nil { |
| 45 | + return 0, nil, err |
| 46 | + } |
| 47 | + |
| 48 | + var schemaOrRef openapi3.SchemaOrRef |
| 49 | + schemaOrRef.FromJSONSchema(jsonSchema.ToSchemaOrBool()) |
| 50 | + |
| 51 | + spec := &openapi3.Response{ |
| 52 | + Content: map[string]openapi3.MediaType{ |
| 53 | + "application/json": { |
| 54 | + Schema: &schemaOrRef, |
| 55 | + }, |
| 56 | + }, |
| 57 | + } |
| 58 | + return http.StatusOK, spec, nil |
| 59 | +} |
| 60 | + |
| 61 | +// WriteResponse implements the [ResponseWriter] interface. |
| 62 | +func (jr *JsonResponse[T]) WriteResponse(ctx context.Context, w http.ResponseWriter) error { |
| 63 | + _, span := otel.Tracer("rpc").Start(ctx, "JsonResponse.WriteResponse") |
| 64 | + defer span.End() |
| 65 | + |
| 66 | + w.Header().Set("Content-Type", "application/json") |
| 67 | + w.WriteHeader(http.StatusOK) |
| 68 | + |
| 69 | + enc := json.NewEncoder(w) |
| 70 | + return enc.Encode(jr.inner) |
| 71 | +} |
| 72 | + |
| 73 | +// Handle implements the [Handler] interface. |
| 74 | +func (h *ReturnJsonHandler[Req, Resp]) Handle(ctx context.Context, req *Req) (*JsonResponse[Resp], error) { |
| 75 | + spanCtx, span := otel.Tracer("rpc").Start(ctx, "ReturnJsonHandler.Handle") |
| 76 | + defer span.End() |
| 77 | + |
| 78 | + resp, err := h.inner.Handle(spanCtx, req) |
| 79 | + if err != nil { |
| 80 | + return nil, err |
| 81 | + } |
| 82 | + return &JsonResponse[Resp]{inner: resp}, nil |
| 83 | +} |
| 84 | + |
| 85 | +// ConsumeJsonHandler |
| 86 | +type ConsumeJsonHandler[Req, Resp any] struct { |
| 87 | + inner Handler[Req, Resp] |
| 88 | +} |
| 89 | + |
| 90 | +// ConsumeJson initializes a [ConsumeJsonHandler]. |
| 91 | +func ConsumeJson[Req, Resp any](h Handler[Req, Resp]) *ConsumeJsonHandler[Req, Resp] { |
| 92 | + return &ConsumeJsonHandler[Req, Resp]{ |
| 93 | + inner: h, |
| 94 | + } |
| 95 | +} |
| 96 | + |
| 97 | +// JsonRequest |
| 98 | +type JsonRequest[T any] struct { |
| 99 | + inner T |
| 100 | +} |
| 101 | + |
| 102 | +// Spec implements the [TypedRequest] interface. |
| 103 | +func (*JsonRequest[T]) Spec() (*openapi3.RequestBody, error) { |
| 104 | + var t T |
| 105 | + var reflector jsonschema.Reflector |
| 106 | + |
| 107 | + jsonSchema, err := reflector.Reflect(t, jsonschema.InlineRefs) |
| 108 | + if err != nil { |
| 109 | + return nil, err |
| 110 | + } |
| 111 | + |
| 112 | + var schemaOrRef openapi3.SchemaOrRef |
| 113 | + schemaOrRef.FromJSONSchema(jsonSchema.ToSchemaOrBool()) |
| 114 | + |
| 115 | + spec := &openapi3.RequestBody{ |
| 116 | + Required: ptr.Ref(true), |
| 117 | + Content: map[string]openapi3.MediaType{ |
| 118 | + "application/json": { |
| 119 | + Schema: &schemaOrRef, |
| 120 | + }, |
| 121 | + }, |
| 122 | + } |
| 123 | + return spec, nil |
| 124 | +} |
| 125 | + |
| 126 | +// ReadRequest implements the [RequestReader] interface. |
| 127 | +func (jr *JsonRequest[T]) ReadRequest(ctx context.Context, r *http.Request) (err error) { |
| 128 | + _, span := otel.Tracer("rpc").Start(ctx, "JsonRequest.ReadRequest") |
| 129 | + defer span.End() |
| 130 | + defer try.Close(&err, r.Body) |
| 131 | + |
| 132 | + contentType := r.Header.Get("Content-Type") |
| 133 | + if contentType != "application/json" { |
| 134 | + return InvalidContentTypeError{ |
| 135 | + ContentType: contentType, |
| 136 | + } |
| 137 | + } |
| 138 | + |
| 139 | + dec := json.NewDecoder(r.Body) |
| 140 | + return dec.Decode(&jr.inner) |
| 141 | +} |
| 142 | + |
| 143 | +// Handle implements the [Handler] interface. |
| 144 | +func (h *ConsumeJsonHandler[Req, Resp]) Handle(ctx context.Context, req *JsonRequest[Req]) (*Resp, error) { |
| 145 | + spanCtx, span := otel.Tracer("rpc").Start(ctx, "ConsumeJsonHandler.Handle") |
| 146 | + defer span.End() |
| 147 | + |
| 148 | + return h.inner.Handle(spanCtx, &req.inner) |
| 149 | +} |
0 commit comments