Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: z5labs/humus
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v0.5.0
Choose a base ref
...
head repository: z5labs/humus
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: v0.6.0
Choose a head ref
  • 1 commit
  • 5 files changed
  • 1 contributor

Commits on Feb 23, 2025

  1. story(issue-86): rpc implement json helpers (#90)

    Zaba505 authored Feb 23, 2025
    Copy the full SHA
    dbc6ee3 View commit details
Showing with 597 additions and 1 deletion.
  1. +1 −1 README.md
  2. +10 −0 rest/rpc/empty_example_test.go
  3. +149 −0 rest/rpc/json.go
  4. +86 −0 rest/rpc/json_example_test.go
  5. +351 −0 rest/rpc/json_test.go
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@

[![Go Reference](https://pkg.go.dev/badge/github.com/z5labs/humus.svg)](https://pkg.go.dev/github.com/z5labs/humus)
[![Go Report Card](https://goreportcard.com/badge/github.com/z5labs/humus)](https://goreportcard.com/report/github.com/z5labs/humus)
![Coverage](https://img.shields.io/badge/Coverage-71.5%25-brightgreen)
![Coverage](https://img.shields.io/badge/Coverage-76.3%25-brightgreen)
[![build](https://github.com/z5labs/humus/actions/workflows/build.yaml/badge.svg)](https://github.com/z5labs/humus/actions/workflows/build.yaml)

**humus one stop shop framework for all Z5Labs projects in Go.**
10 changes: 10 additions & 0 deletions rest/rpc/empty_example_test.go
Original file line number Diff line number Diff line change
@@ -53,6 +53,9 @@ func ExampleReturnNothing() {
fmt.Println("expected HTTP 200 status code but got", resp.StatusCode)
return
}
if resp.Header.Get("Content-Type") != "" {
return
}

// Output: hello world
}
@@ -66,6 +69,7 @@ func (*msgResponse) Spec() (int, *openapi3.Response, error) {
}

func (mr *msgResponse) WriteResponse(ctx context.Context, w http.ResponseWriter) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
enc := json.NewEncoder(w)
return enc.Encode(mr)
@@ -95,6 +99,12 @@ func ExampleConsumeNothing() {
return
}

contentType := resp.Header.Get("Content-Type")
if contentType != "application/json" {
fmt.Println("expected Content-Type to be set to application/json instead of:", contentType)
return
}

var mr msgResponse
dec := json.NewDecoder(resp.Body)
err = dec.Decode(&mr)
149 changes: 149 additions & 0 deletions rest/rpc/json.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
// Copyright (c) 2025 Z5Labs and Contributors
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT

package rpc

import (
"context"
"encoding/json"
"net/http"

"github.com/z5labs/humus/internal/ptr"
"github.com/z5labs/humus/internal/try"

"github.com/swaggest/jsonschema-go"
"github.com/swaggest/openapi-go/openapi3"
"go.opentelemetry.io/otel"
)

// ReturnJsonHandler
type ReturnJsonHandler[Req, Resp any] struct {
inner Handler[Req, Resp]
}

// ReturnJson initializes a [ReturnJsonHandler].
func ReturnJson[Req, Resp any](h Handler[Req, Resp]) *ReturnJsonHandler[Req, Resp] {
return &ReturnJsonHandler[Req, Resp]{
inner: h,
}
}

// JsonResponse
type JsonResponse[T any] struct {
inner *T
}

// Spec implements the [TypedResponse] interface.
func (*JsonResponse[T]) Spec() (int, *openapi3.Response, error) {
var t T
var reflector jsonschema.Reflector

jsonSchema, err := reflector.Reflect(t, jsonschema.InlineRefs)
if err != nil {
return 0, nil, err
}

var schemaOrRef openapi3.SchemaOrRef
schemaOrRef.FromJSONSchema(jsonSchema.ToSchemaOrBool())

spec := &openapi3.Response{
Content: map[string]openapi3.MediaType{
"application/json": {
Schema: &schemaOrRef,
},
},
}
return http.StatusOK, spec, nil
}

// WriteResponse implements the [ResponseWriter] interface.
func (jr *JsonResponse[T]) WriteResponse(ctx context.Context, w http.ResponseWriter) error {
_, span := otel.Tracer("rpc").Start(ctx, "JsonResponse.WriteResponse")
defer span.End()

w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)

enc := json.NewEncoder(w)
return enc.Encode(jr.inner)
}

// Handle implements the [Handler] interface.
func (h *ReturnJsonHandler[Req, Resp]) Handle(ctx context.Context, req *Req) (*JsonResponse[Resp], error) {
spanCtx, span := otel.Tracer("rpc").Start(ctx, "ReturnJsonHandler.Handle")
defer span.End()

resp, err := h.inner.Handle(spanCtx, req)
if err != nil {
return nil, err
}
return &JsonResponse[Resp]{inner: resp}, nil
}

// ConsumeJsonHandler
type ConsumeJsonHandler[Req, Resp any] struct {
inner Handler[Req, Resp]
}

// ConsumeJson initializes a [ConsumeJsonHandler].
func ConsumeJson[Req, Resp any](h Handler[Req, Resp]) *ConsumeJsonHandler[Req, Resp] {
return &ConsumeJsonHandler[Req, Resp]{
inner: h,
}
}

// JsonRequest
type JsonRequest[T any] struct {
inner T
}

// Spec implements the [TypedRequest] interface.
func (*JsonRequest[T]) Spec() (*openapi3.RequestBody, error) {
var t T
var reflector jsonschema.Reflector

jsonSchema, err := reflector.Reflect(t, jsonschema.InlineRefs)
if err != nil {
return nil, err
}

var schemaOrRef openapi3.SchemaOrRef
schemaOrRef.FromJSONSchema(jsonSchema.ToSchemaOrBool())

spec := &openapi3.RequestBody{
Required: ptr.Ref(true),
Content: map[string]openapi3.MediaType{
"application/json": {
Schema: &schemaOrRef,
},
},
}
return spec, nil
}

// ReadRequest implements the [RequestReader] interface.
func (jr *JsonRequest[T]) ReadRequest(ctx context.Context, r *http.Request) (err error) {
_, span := otel.Tracer("rpc").Start(ctx, "JsonRequest.ReadRequest")
defer span.End()
defer try.Close(&err, r.Body)

contentType := r.Header.Get("Content-Type")
if contentType != "application/json" {
return InvalidContentTypeError{
ContentType: contentType,
}
}

dec := json.NewDecoder(r.Body)
return dec.Decode(&jr.inner)
}

// Handle implements the [Handler] interface.
func (h *ConsumeJsonHandler[Req, Resp]) Handle(ctx context.Context, req *JsonRequest[Req]) (*Resp, error) {
spanCtx, span := otel.Tracer("rpc").Start(ctx, "ConsumeJsonHandler.Handle")
defer span.End()

return h.inner.Handle(spanCtx, &req.inner)
}
86 changes: 86 additions & 0 deletions rest/rpc/json_example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// Copyright (c) 2025 Z5Labs and Contributors
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT

package rpc

import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"strings"
)

func ExampleReturnJson() {
p := ProducerFunc[msgResponse](func(_ context.Context) (*msgResponse, error) {
return &msgResponse{Msg: "hello world"}, nil
})

h := ReturnJson(ConsumeNothing(p))

op := NewOperation(h)

srv := httptest.NewServer(op)
defer srv.Close()

resp, err := http.Get(srv.URL)
if err != nil {
fmt.Println(err)
return
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
fmt.Println("expected HTTP 200 status code but got", resp.StatusCode)
return
}

contentType := resp.Header.Get("Content-Type")
if contentType != "application/json" {
fmt.Println("expected Content-Type to be set to application/json instead of:", contentType)
return
}

var mr msgResponse
dec := json.NewDecoder(resp.Body)
err = dec.Decode(&mr)
if err != nil {
fmt.Println(err)
return
}

fmt.Println(mr.Msg)
// Output: hello world
}

func ExampleConsumeJson() {
c := ConsumerFunc[msgRequest](func(_ context.Context, req *msgRequest) error {
fmt.Println(req.Msg)
return nil
})

h := ConsumeJson(ReturnNothing(c))

op := NewOperation(h)

srv := httptest.NewServer(op)
defer srv.Close()

resp, err := http.Post(srv.URL, "application/json", strings.NewReader(`{"msg":"hello world"}`))
if err != nil {
fmt.Println(err)
return
}
if resp.StatusCode != http.StatusOK {
fmt.Println("expected HTTP 200 status code but got", resp.StatusCode)
return
}
if resp.Header.Get("Content-Type") != "" {
return
}

// Output: hello world
}
Loading