Skip to content

Commit

Permalink
switch to generics & libopenapi
Browse files Browse the repository at this point in the history
fcjr committed Dec 31, 2024
1 parent d4eeaaf commit 7aa6a91
Showing 12 changed files with 505 additions and 340 deletions.
61 changes: 27 additions & 34 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,25 +1,18 @@

<p align="center">
<img src="assets/logo.svg" alt="ShiftAPI Logo">
<img src="assets/logo.svg" alt="ShiftAPI Logo">
</p>

# ShiftAPI

Quickly write RESTful APIs in go with automatic openapi schema generation.

Inspired by the simplicity of [FastAPI](https://github.com/tiangolo/fastapi).

<!-- [![GitHub release (latest by date)][release-img]][release] -->
[![GolangCI][golangci-lint-img]][golangci-lint]
[![Go Report Card][report-card-img]][report-card]

## NOTE: THIS IS AN EXPERIMENT

This project is highly experimental -- the API is likely to change (currently only _basic_ post requests are even implemented).
This is **in no way production ready**.

This project was inspired by the simplicity of [FastAPI](https://github.com/tiangolo/fastapi).

Due to limitations of typing in go this library will probably not be production ready pre go 1.18 as handlers must be passed as `interface{}`s and validated at _runtime_ (Scary I know! 😱). Once generics hit I hope to rewrite the handler implementation to restore compile time type checking & safety.

## Installation

```sh
@@ -32,47 +25,47 @@ go get github.com/fcjr/shiftapi
package main

import (
"log"
"net/http"
"log"
"net/http"

"github.com/fcjr/shiftapi"
"github.com/fcjr/shiftapi"
)

type Person struct {
Name string `json:"name"`
Name string `json:"name"`
}

type Greeting struct {
Hello string `json:"hello"`
Hello string `json:"hello"`
}

// This is your http handler!
// ShiftAPI is responsible for marshalling the request body
// and marshalling the return value.
func greeter(p *Person) (*Greeting, *shiftapi.Error) {
return &Greeting{
Hello: p.Name,
}, nil
return &Greeting{
Hello: p.Name,
}, nil
}

func main() {

api := shiftapi.New(&shiftapi.Params{
SchemaInfo: &shiftapi.SchemaParams{
Title: "Greeter Demo API",
},
})

err := api.POST("/greet", greeter, http.StatusOK, &shiftapi.HandlerOpts{
Summary: "Greeter Method",
Description: "It greets you by name.",
})
if err != nil {
log.Fatal(err)
}

log.Fatal(api.Serve())
// redoc will be served at http://localhost:8080/docs
api := shiftapi.New(&shiftapi.Params{
SchemaInfo: &shiftapi.SchemaParams{
Title: "Greeter Demo API",
},
})

err := api.POST("/greet", greeter, http.StatusOK, &shiftapi.HandlerOpts{
Summary: "Greeter Method",
Description: "It greets you by name.",
})
if err != nil {
log.Fatal(err)
}

log.Fatal(api.Serve())
// redoc will be served at http://localhost:8080/docs
}
```

148 changes: 0 additions & 148 deletions app.go

This file was deleted.

10 changes: 5 additions & 5 deletions docs.go
Original file line number Diff line number Diff line change
@@ -27,11 +27,11 @@ const redocTemplate string = `<!DOCTYPE html>
`

type redocData struct {
Title string
FaviconURL string
RedocURL string
SpecURL string
EnableGoogleFonts bool
Title string
FaviconURL string
RedocURL string
SpecURL string
// EnableGoogleFonts bool
}

func genRedocHTML(data redocData, out io.Writer) error {
6 changes: 0 additions & 6 deletions error.go

This file was deleted.

47 changes: 13 additions & 34 deletions examples/greeter/main.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
package main

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

@@ -16,46 +17,24 @@ type Greeting struct {
Hello string `json:"hello"`
}

func BadRequestError(code, msg string) *shiftapi.Error {
type err struct {
Code string `json:"code"`
Message string `json:"message"`
}
b, _ := json.Marshal(&err{
Code: code,
Message: msg,
})
return &shiftapi.Error{
Code: http.StatusBadRequest,
Body: b,
}
}

func greeter(p *Person) (*Greeting, *shiftapi.Error) {
if p.Name != "frank" {
return nil, BadRequestError("wrong_name", "I only greet frank.")
func greet(ctx context.Context, headers http.Header, person *Person) (*Greeting, error) {
if person.Name != "frank" {
return nil, errors.New("wrong name, I only greet frank")
}
return &Greeting{
Hello: p.Name,
Hello: person.Name,
}, nil
}

func main() {
ctx := context.Background()
server := shiftapi.New(ctx, shiftapi.WithInfo(shiftapi.Info{
Title: "Geeter Demo API",
}))

api := shiftapi.New(&shiftapi.Params{
SchemaInfo: &shiftapi.SchemaParams{
Title: "Greeter Demo API",
},
})

err := api.POST("/greet", greeter, http.StatusOK, &shiftapi.HandlerOpts{
Summary: "Greeter Method",
Description: "It greets anyone named 'frank'",
})
if err != nil {
log.Fatal(err)
}
handleGreet := shiftapi.Post("/greet", greet)
server.Register(handleGreet)

log.Fatal(api.Serve())
log.Fatal(server.ListenAndServe(":8080"))
// redoc will be served at http://localhost:8080/docs
}
21 changes: 11 additions & 10 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
module github.com/fcjr/shiftapi

go 1.17
go 1.23.0

require (
github.com/getkin/kin-openapi v0.80.0
github.com/julienschmidt/httprouter v1.3.0
)
toolchain go1.23.3

require github.com/pb33f/libopenapi v0.18.7

require (
github.com/ghodss/yaml v1.0.0 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/swag v0.19.5 // indirect
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e // indirect
gopkg.in/yaml.v2 v2.3.0 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/buger/jsonparser v1.1.1 // indirect
github.com/dprotaso/go-yit v0.0.0-20240618133044-5a0af90af097 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/vmware-labs/yaml-jsonpath v0.3.2 // indirect
github.com/wk8/go-ordered-map/v2 v2.1.9-0.20240815153524-6ea36470d1bd // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
145 changes: 126 additions & 19 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,34 +1,141 @@
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/getkin/kin-openapi v0.80.0 h1:W/s5/DNnDCR8P+pYyafEWlGk4S7/AfQUWXgrRSSAzf8=
github.com/getkin/kin-openapi v0.80.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/swag v0.19.5 h1:lTz6Ys4CmqqCQmZPBlbQENR1/GucA2bzYTE12Pw4tFY=
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/dprotaso/go-yit v0.0.0-20191028211022-135eb7262960/go.mod h1:9HQzr9D/0PGwMEbC3d5AB7oi67+h4TsQqItC1GVYG58=
github.com/dprotaso/go-yit v0.0.0-20240618133044-5a0af90af097 h1:f5nA5Ys8RXqFXtKc0XofVRiuwNTuJzPIwTmbjLz9vj8=
github.com/dprotaso/go-yit v0.0.0-20240618133044-5a0af90af097/go.mod h1:FTAVyH6t+SlS97rv6EXRVuBDLkQqcIe/xQw9f4IFUI4=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e h1:hB2xlXdHp/pmPZq0y3QnmWAArdw9PqbmotexnWx/FU8=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.10.2/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc=
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c=
github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw=
github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro=
github.com/pb33f/libopenapi v0.18.7 h1:gLD4gQ88zEqv7x13SDzk3AUdpHUp9gWrP1NDwrFTy+U=
github.com/pb33f/libopenapi v0.18.7/go.mod h1:qZRs2IHIcs9SjHPmQfSUCyeD3OY9JkLJQOuFxd0bYCY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/vmware-labs/yaml-jsonpath v0.3.2 h1:/5QKeCBGdsInyDCyVNLbXyilb61MXGi9NP674f9Hobk=
github.com/vmware-labs/yaml-jsonpath v0.3.2/go.mod h1:U6whw1z03QyqgWdgXxvVnQ90zN1BWz5V+51Ewf8k+rQ=
github.com/wk8/go-ordered-map/v2 v2.1.9-0.20240815153524-6ea36470d1bd h1:dLuIF2kX9c+KknGJUdJi1Il1SDiTSK158/BB9kdgAew=
github.com/wk8/go-ordered-map/v2 v2.1.9-0.20240815153524-6ea36470d1bd/go.mod h1:DbzwytT4g/odXquuOCqroKvtxxldI4nb3nuesHF/Exo=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f h1:oA4XRj0qtSt8Yo1Zms0CUlsT3KG69V2UGQWPBxujDmc=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20191026110619-0b21df46bc1d/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
85 changes: 85 additions & 0 deletions handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package shiftapi

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

type Handler interface {
register(server *ShiftAPI) error

// unimplementable is a method that should never be called.
// It is simply used to ensure that the Handler interface can only be implemented
// internally by the shiftapi package.
unimplementable()
}

type HandlerOption interface {
// unimplementable is a method that should never be called.
// It is simply used to ensure that the HandlerOption interface can only be implemented
// internally by the shiftapi package.
unimplementable()
}

type HandlerFunc[RequestBody any, ResponseBody any] func(
ctx context.Context,
headers http.Header,
requestBody RequestBody,
) (responseBody ResponseBody, err error)

// TODO pass status code
type handler[RequestBody any, ResponseBody any] struct {
method string
path string
handlerFunc HandlerFunc[RequestBody, ResponseBody]
options []func(HandlerFunc[RequestBody, ResponseBody]) HandlerFunc[RequestBody, ResponseBody]
}

// ensure handler implements Handler at compile time
var _ Handler = handler[any, any]{}

func (h handler[RequestBody, ResponseBody]) unimplementable() {
panic("unimplementable called")
}

func (h handler[RequestBody, ResponseBody]) register(server *ShiftAPI) error {
if err := h.updateSpec(server); err != nil {
return err
}

pattern := fmt.Sprintf("%s %s", h.method, h.path)
stdHandler := h.stdHandler(server.baseContext)
server.mux.HandleFunc(pattern, stdHandler)
return nil
}

func (h handler[RequestBody, ResponseBody]) updateSpec(server *ShiftAPI) error {
return nil
}

func (h handler[RequestBody, ResponseBody]) stdHandler(ctx context.Context) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// TODO: valdiate request body?
var requestBody RequestBody
if err := json.NewDecoder(r.Body).Decode(&requestBody); err != nil {
http.Error(w, "invalid request body", http.StatusBadRequest)
return
}
responseBody, err := h.handlerFunc(
ctx,
r.Header,
requestBody,
)
if err != nil {
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(responseBody); err != nil {
http.Error(w, "error encoding response", http.StatusInternalServerError)
return
}
}
}
88 changes: 88 additions & 0 deletions handlerCreate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package shiftapi

import "net/http"

func Get[RequestBody any, ResponseBody any](
path string,
handlerFunc HandlerFunc[RequestBody, ResponseBody],
options ...func(HandlerFunc[RequestBody, ResponseBody]) HandlerFunc[RequestBody, ResponseBody],
) Handler {
return &handler[RequestBody, ResponseBody]{
method: http.MethodGet,
path: path,
handlerFunc: handlerFunc,
options: options,
}
}

func Post[RequestBody any, ResponseBody any](
path string,
handlerFunc HandlerFunc[RequestBody, ResponseBody],
options ...func(HandlerFunc[RequestBody, ResponseBody]) HandlerFunc[RequestBody, ResponseBody],
) Handler {
return &handler[RequestBody, ResponseBody]{
method: http.MethodPost,
path: path,
handlerFunc: handlerFunc,
options: options,
}
}

func Put[RequestBody any, ResponseBody any](
path string,
handlerFunc HandlerFunc[RequestBody, ResponseBody],
options ...func(HandlerFunc[RequestBody, ResponseBody]) HandlerFunc[RequestBody, ResponseBody],
) Handler {
return &handler[RequestBody, ResponseBody]{
method: http.MethodPut,
path: path,
handlerFunc: handlerFunc,
}
}
func Patch[RequestBody any, ResponseBody any](
path string,
handlerFunc HandlerFunc[RequestBody, ResponseBody],
options ...func(HandlerFunc[RequestBody, ResponseBody]) HandlerFunc[RequestBody, ResponseBody],
) Handler {
return &handler[RequestBody, ResponseBody]{
method: http.MethodPatch,
path: path,
handlerFunc: handlerFunc,
}
}

func Delete[RequestBody any, ResponseBody any](
path string,
handlerFunc HandlerFunc[RequestBody, ResponseBody],
options ...func(HandlerFunc[RequestBody, ResponseBody]) HandlerFunc[RequestBody, ResponseBody],
) Handler {
return &handler[RequestBody, ResponseBody]{
method: http.MethodDelete,
path: path,
handlerFunc: handlerFunc,
}
}

func Head[RequestBody any, ResponseBody any](
path string,
handlerFunc HandlerFunc[RequestBody, ResponseBody],
options ...func(HandlerFunc[RequestBody, ResponseBody]) HandlerFunc[RequestBody, ResponseBody],
) Handler {
return &handler[RequestBody, ResponseBody]{
method: http.MethodHead,
path: path,
handlerFunc: handlerFunc,
}
}

func Options[RequestBody any, ResponseBody any](
path string,
handlerFunc HandlerFunc[RequestBody, ResponseBody],
options ...func(HandlerFunc[RequestBody, ResponseBody]) HandlerFunc[RequestBody, ResponseBody],
) Handler {
return &handler[RequestBody, ResponseBody]{
method: http.MethodOptions,
path: path,
handlerFunc: handlerFunc,
}
}
5 changes: 0 additions & 5 deletions internal/utils/utils.go

This file was deleted.

128 changes: 49 additions & 79 deletions schema.go
Original file line number Diff line number Diff line change
@@ -1,96 +1,66 @@
package shiftapi

import (
"fmt"
"net/http"
"reflect"

"github.com/fcjr/shiftapi/internal/utils"
"github.com/getkin/kin-openapi/openapi3"
"github.com/pb33f/libopenapi/datamodel/high/base"
)

func (s *ShiftAPI) updateSchema(method, path string, handlerFunc, in, out reflect.Type, status int, opts *HandlerOpts) error {
type Info struct {
Summary string
Title string
Description string
TermsOfService string
Contact *Contact
License *License
Version string
}

inSchema, err := s.generateSchemaRef(in)
if err != nil {
return err
}
outSchema, err := s.generateSchemaRef(out)
if err != nil {
return err
}
responses := make(openapi3.Responses)
responseContent := make(map[string]*openapi3.MediaType)
responseContent["application/json"] = &openapi3.MediaType{
Schema: &openapi3.SchemaRef{
Ref: fmt.Sprintf("#/components/schemas/%s", outSchema.Ref),
},
}
responses[fmt.Sprint(status)] = &openapi3.ResponseRef{
Value: &openapi3.Response{
Description: utils.String("Success"),
Content: responseContent,
},
}
type Contact struct {
Name string
URL string
Email string
}

requestContent := make(map[string]*openapi3.MediaType)
requestContent["application/json"] = &openapi3.MediaType{
Schema: &openapi3.SchemaRef{
Ref: fmt.Sprintf("#/components/schemas/%s", inSchema.Ref),
},
}
requestBody := &openapi3.RequestBodyRef{
Value: &openapi3.RequestBody{
Content: requestContent,
},
}
type License struct {
Name string
URL string
Identifier string
}

var oPath *openapi3.PathItem
switch method {
case http.MethodPost:
oPath = &openapi3.PathItem{
Post: &openapi3.Operation{
Summary: opts.Summary,
RequestBody: requestBody,
Description: opts.Description,
Responses: responses,
},
func WithInfo(info Info) func(*ShiftAPI) *ShiftAPI {
return func(api *ShiftAPI) *ShiftAPI {
api.spec.Info = &base.Info{
Title: info.Title,
Description: info.Description,
Version: info.Version,
}
if info.Contact != nil {
api.spec.Info.Contact = &base.Contact{
Name: info.Contact.Name,
URL: info.Contact.URL,
Email: info.Contact.Email,
}
}
if info.License != nil {
api.spec.Info.License = &base.License{
Name: info.License.Name,
URL: info.License.URL,
}
}
return api
}
if oPath == nil {
return fmt.Errorf("method '%s' not implemented", method)
}
s.schema.Paths[path] = oPath
s.schema.Components.Responses.Default()

s.schema.Components.Schemas[inSchema.Ref] = &openapi3.SchemaRef{
Value: inSchema.Value,
}
s.schema.Components.Schemas[outSchema.Ref] = &openapi3.SchemaRef{
Value: outSchema.Value,
}
return nil
}

func (s *ShiftAPI) generateSchemaRef(t reflect.Type) (*openapi3.SchemaRef, error) {
schema, err := s.schemaGen.GenerateSchemaRef(t)
if err != nil {
return nil, err
}

// TODO why tf does kin set ref values for basic types
scrubRefs(schema)

return schema, nil
type ExternalDocs struct {
Description string
URL string
}

func scrubRefs(s *openapi3.SchemaRef) {
if s.Value.Properties == nil || len(s.Value.Properties) <= 0 {
return
}
for _, p := range s.Value.Properties {
if p.Value.Type != "object" {
p.Ref = ""
func WithExternalDocs(externalDocs ExternalDocs) func(*ShiftAPI) *ShiftAPI {
return func(api *ShiftAPI) *ShiftAPI {
api.spec.ExternalDocs = &base.ExternalDoc{
Description: externalDocs.Description,
URL: externalDocs.URL,
}
return api
}
}
101 changes: 101 additions & 0 deletions server.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package shiftapi

import (
"context"
"fmt"
"net/http"

"github.com/pb33f/libopenapi/datamodel/high/base"
v3 "github.com/pb33f/libopenapi/datamodel/high/v3"
"github.com/pb33f/libopenapi/orderedmap"
)

type ShiftAPI struct {
baseContext context.Context
spec *v3.Document
mux *http.ServeMux
}

func New(
ctx context.Context,
options ...func(*ShiftAPI) *ShiftAPI,
) *ShiftAPI {
mux := http.NewServeMux()
spec := &v3.Document{
Version: "3.1",
Paths: &v3.Paths{
PathItems: orderedmap.New[string, *v3.PathItem](),
},
Components: &v3.Components{
Schemas: orderedmap.New[string, *base.SchemaProxy](),
Responses: orderedmap.New[string, *v3.Response](),
Parameters: orderedmap.New[string, *v3.Parameter](),
Examples: orderedmap.New[string, *base.Example](),
RequestBodies: orderedmap.New[string, *v3.RequestBody](),
Headers: orderedmap.New[string, *v3.Header](),
SecuritySchemes: orderedmap.New[string, *v3.SecurityScheme](),
Links: orderedmap.New[string, *v3.Link](),
Callbacks: orderedmap.New[string, *v3.Callback](),
PathItems: orderedmap.New[string, *v3.PathItem](),
},
}

api := &ShiftAPI{
baseContext: ctx,
spec: spec,
mux: mux,
}
for _, option := range options {
api = option(api)
}
mux.HandleFunc("GET /openapi.json", api.serveSchema)
mux.HandleFunc("GET /docs", api.serveDocs)
mux.HandleFunc("GET /", api.redirectTo("/docs"))
return api
}

// Register adds 1 or more handlers to the server.
// The handlers are expected to be created via the shiftapi.Post, shiftapi.Get,
// shiftapi.Put, shiftapi.Patch, and shiftapi.Delete functions.
func (s *ShiftAPI) Register(handlers ...Handler) {
for _, h := range handlers {
h.register(s)
}
}

func (s *ShiftAPI) redirectTo(path string) http.HandlerFunc {
return func(res http.ResponseWriter, req *http.Request) {
http.Redirect(res, req, path, http.StatusTemporaryRedirect)
}
}

func (s *ShiftAPI) serveSchema(res http.ResponseWriter, req *http.Request) {
b, err := s.spec.RenderJSON(" ")
if err != nil {
res.WriteHeader(http.StatusInternalServerError)
return
}
res.Header().Set("Content-Type", "application/json")
_, _ = res.Write(b)
}

func (s *ShiftAPI) serveDocs(res http.ResponseWriter, req *http.Request) {
title := ""
if s.spec.Info != nil {
title = s.spec.Info.Title
}
err := genRedocHTML(redocData{
Title: title,
SpecURL: "/openapi.json",
}, res)
if err != nil {
fmt.Println(err)
res.WriteHeader(http.StatusInternalServerError)
}
}

func (s *ShiftAPI) ListenAndServe(addr string) error {
// TODO add address to schema & create server separately http server
// maybe also pass ctx
return http.ListenAndServe(addr, s.mux)
}

0 comments on commit 7aa6a91

Please sign in to comment.