Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
f40d0ef
fix(codegen): use reflect.DeepEqual for ExclusiveMin/Max in MergeSchemas
mromaszewicz Apr 24, 2026
a8e4a66
feat(codegen): OpenAPI 3.1 foundation -- version awareness and nullable
mromaszewicz Apr 25, 2026
89b534f
feat(codegen): OpenAPI 3.1 enum-via-oneOf detection
mromaszewicz Apr 25, 2026
af16442
feat(codegen): OpenAPI 3.1 webhooks -- WebhookInitiator + WebhookRece…
mromaszewicz Apr 25, 2026
428023e
feat(codegen): OpenAPI callbacks -- CallbackInitiator + CallbackReceiver
mromaszewicz Apr 25, 2026
eda5048
feat(codegen): OpenAPI 3.1 polish -- examples in doc comments + const
mromaszewicz Apr 25, 2026
d60b9d8
refactor(codegen): unify stdhttp webhook+callback receiver template, …
mromaszewicz Apr 25, 2026
a80da23
chore(gen): regenerate gen files to pick up Phase 5 examples-in-doc-c…
mromaszewicz Apr 25, 2026
6bd2d55
feat(codegen): chi webhook+callback receiver template
mromaszewicz Apr 25, 2026
c2a69fa
feat(codegen): gorilla/mux webhook+callback receiver template
mromaszewicz Apr 25, 2026
82c8f89
feat(codegen): echo (v4) webhook+callback receiver template
mromaszewicz Apr 25, 2026
75c06a4
feat(codegen): echo (v5) webhook+callback receiver template
mromaszewicz Apr 25, 2026
fc880cd
feat(codegen): gin webhook+callback receiver template
mromaszewicz Apr 25, 2026
7ede50a
feat(codegen): fiber (v2) webhook+callback receiver template
mromaszewicz Apr 25, 2026
0c9bd7d
feat(codegen): iris webhook+callback receiver template
mromaszewicz Apr 25, 2026
6117d88
refactor(test): consolidate webhook tests into internal/test/webhooks…
mromaszewicz Apr 25, 2026
aa23540
fix(codegen): address greptile review findings on Phase 6 work
mromaszewicz Apr 25, 2026
2349d15
Pull in kin-openapi v0.136.0
mromaszewicz Apr 25, 2026
ef34d82
fix(test): address golangci-lint errcheck + ST1023 findings
mromaszewicz Apr 25, 2026
4ec93d4
Fix lint issues and CI versioning
mromaszewicz Apr 25, 2026
4c055d9
Rebase and downgrade Go
mromaszewicz Apr 30, 2026
23f1a63
Extend hoisting tests
mromaszewicz Apr 30, 2026
329414b
Fix issues due to rebasing onto main
mromaszewicz May 4, 2026
a307030
Merge remote-tracking branch 'upstream/main' into feat/kin-openapi-3.1
mromaszewicz May 18, 2026
9bb79e7
Replace kin Types.Is() usage with our own
mromaszewicz May 18, 2026
31fbe85
Merge upstream/main into feat/kin-openapi-3.1
mromaszewicz Jun 19, 2026
3bd0c88
Add test coverage for nullables
mromaszewicz Jun 19, 2026
103a51a
Merge upstream/main into feat/kin-openapi-3.1
mromaszewicz Jun 20, 2026
214cb59
codegen: hoist inline-object array items under generate-types-for-ano…
mromaszewicz Jun 20, 2026
2d6387e
codegen: treat `{"type": "null"}` branches in anyOf/oneOf as nullabil…
mromaszewicz Jun 20, 2026
2f9bfe2
codegen: compare discriminator mapping count against effective branch…
mromaszewicz Jun 20, 2026
87dafc7
codegen: handle 3.1 contentEncoding/contentMediaType file-upload keyw…
mromaszewicz Jun 20, 2026
d58ecd7
codegen: only map contentEncoding to []byte for standard base64
mromaszewicz Jun 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ jobs:
fail-fast: true
matrix:
version:
- "1.26"
- "1.25"
- "1.26"
steps:
- name: Check out source code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
Expand Down
4 changes: 0 additions & 4 deletions cmd/oapi-codegen/oapi-codegen.go
Original file line number Diff line number Diff line change
Expand Up @@ -323,10 +323,6 @@ func main() {
errExit("error loading swagger spec in %s\n: %s\n", flag.Arg(0), err)
}

if strings.HasPrefix(swagger.OpenAPI, "3.1.") {
fmt.Fprintln(os.Stderr, "WARNING: You are using an OpenAPI 3.1.x specification, which is not yet supported by oapi-codegen (https://github.com/oapi-codegen/oapi-codegen/issues/373) and so some functionality may not be available. Until oapi-codegen supports OpenAPI 3.1, it is recommended to downgrade your spec to 3.0.x")
}

if len(noVCSVersionOverride) > 0 {
opts.NoVCSVersionOverride = &noVCSVersionOverride
}
Expand Down
4 changes: 4 additions & 0 deletions configuration-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,10 @@
"type": "boolean",
"description": "Whether to skip generation of the Valid() method on enum types"
},
"skip-enum-via-oneof": {
"type": "boolean",
"description": "Disables detection of the OpenAPI 3.1 enum-via-oneOf idiom: a schema with `type: string|integer` and `oneOf:` members that each carry `const` + `title` will normally be emitted as a Go enum with named constants. Set this to true to fall through to the standard union generator instead."
},
"include-tags": {
"type": "array",
"description": "Only include operations that have one of these tags. Ignored when empty.",
Expand Down
144 changes: 144 additions & 0 deletions examples/callback/client/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package main

import (
"bytes"
"encoding/json"
"flag"
"fmt"
"log"
"net"
"net/http"
"sync"
"sync/atomic"

treefarm "github.com/oapi-codegen/oapi-codegen/v2/examples/callback"
)

// trees and cities for our planting requests
var treeKinds = []string{
"oak", "maple", "pine", "birch", "willow",
"cedar", "elm", "ash", "cherry", "walnut",
}

var cities = []string{
"Providence", "Austin", "Denver", "Seattle", "Chicago",
"Boston", "Miami", "Nashville", "Savannah", "Mountain View",
}

// CallbackReceiver implements treefarm.CallbackReceiverInterface.
type CallbackReceiver struct {
received atomic.Int32
total int
done chan struct{}
once sync.Once

mu sync.Mutex
ordinals map[string]int // UUID string -> 1-based planting order
}

var _ treefarm.CallbackReceiverInterface = (*CallbackReceiver)(nil)

func NewCallbackReceiver(total int) *CallbackReceiver {
return &CallbackReceiver{
total: total,
done: make(chan struct{}),
ordinals: make(map[string]int),
}
}

func (cr *CallbackReceiver) Register(id string, ordinal int) {
cr.mu.Lock()
cr.ordinals[id] = ordinal
cr.mu.Unlock()
}

func (cr *CallbackReceiver) HandleTreePlantedCallback(w http.ResponseWriter, r *http.Request) {
var result treefarm.TreePlantingResult
if err := json.NewDecoder(r.Body).Decode(&result); err != nil {
log.Printf("Error decoding callback: %v", err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}

cr.mu.Lock()
ordinal := cr.ordinals[result.ID.String()]
cr.mu.Unlock()

n := cr.received.Add(1)
log.Printf("Callback %d/%d received: tree #%d success=%v", n, cr.total, ordinal, result.Success)

w.WriteHeader(http.StatusOK)

if int(n) >= cr.total {
cr.once.Do(func() { close(cr.done) })
}
}

func main() {
serverAddr := flag.String("server", "http://localhost:8080", "Tree farm server address")
flag.Parse()

const numTrees = 10

// Start callback receiver on an ephemeral port.
receiver := NewCallbackReceiver(numTrees)

mux := http.NewServeMux()
mux.Handle("/tree_callback", treefarm.TreePlantedCallbackHandler(receiver, nil))

listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
log.Fatalf("Failed to listen: %v", err)
}
callbackPort := listener.Addr().(*net.TCPAddr).Port
callbackURL := fmt.Sprintf("http://localhost:%d/tree_callback", callbackPort)
log.Printf("Callback receiver listening on port %d", callbackPort)

go func() {
if err := http.Serve(listener, mux); err != nil {
log.Printf("Callback server stopped: %v", err)
}
}()

// Send 10 tree planting requests.
client := &http.Client{}
for i := range numTrees {
req := treefarm.TreePlantingRequest{
Kind: treeKinds[i],
Location: cities[i],
CallbackURL: callbackURL,
}

body, err := json.Marshal(req)
if err != nil {
log.Fatalf("Failed to marshal request: %v", err)
}

resp, err := client.Post(
*serverAddr+"/api/plant_tree",
"application/json",
bytes.NewReader(body),
)
if err != nil {
log.Fatalf("Failed to plant tree %d: %v", i+1, err)
}

var accepted treefarm.TreeWithID
if err := json.NewDecoder(resp.Body).Decode(&accepted); err != nil {
_ = resp.Body.Close()
log.Fatalf("Failed to decode response: %v", err)
}
_ = resp.Body.Close()

receiver.Register(accepted.ID.String(), i+1)
log.Printf("Planted tree %d/%d: id=%s kind=%q location=%q",
i+1, numTrees, accepted.ID, accepted.Kind, accepted.Location)
}

log.Printf("All %d trees planted, waiting for callbacks...", numTrees)

// Wait for all callbacks.
<-receiver.done

log.Printf("All %d callbacks received, done!", numTrees)
}
13 changes: 13 additions & 0 deletions examples/callback/config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# yaml-language-server: $schema=../../configuration-schema.json
package: treefarm
generate:
models: true
client: true
std-http-server: true
output-options:
skip-prune: true
# Use ToCamelCaseWithInitialisms so spec fields like `id` and
# `callbackUrl` generate as `ID` and `CallbackURL` (matching the
# example's Go code).
name-normalizer: ToCamelCaseWithInitialisms
output: treefarm.gen.go
14 changes: 14 additions & 0 deletions examples/callback/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
//go:generate go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen --config=config.yaml tree-farm.yaml

// Package treefarm provides an example of how to handle OpenAPI callbacks.
// We create a server which plants trees. The client asks the server to plant
// a tree and requests a callback when the planting is done.
//
// The server program will wait 1-5 seconds before notifying the client that a
// tree has been planted.
//
// You can run the example by running these two commands in parallel:
//
// go run ./server --port 8080
// go run ./client --server http://localhost:8080
package treefarm
116 changes: 116 additions & 0 deletions examples/callback/server/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package main

import (
"context"
"encoding/json"
"flag"
"log"
"math/rand/v2"
"net"
"net/http"
"time"

"github.com/google/uuid"

treefarm "github.com/oapi-codegen/oapi-codegen/v2/examples/callback"
)

// TreeFarm implements treefarm.ServerInterface.
type TreeFarm struct {
initiator *treefarm.CallbackInitiator
}

var _ treefarm.ServerInterface = (*TreeFarm)(nil)

func NewTreeFarm() *TreeFarm {
initiator, err := treefarm.NewCallbackInitiator()
if err != nil {
log.Fatalf("Failed to create callback initiator: %v", err)
}
return &TreeFarm{initiator: initiator}
}

func sendError(w http.ResponseWriter, code int, message string) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
_ = json.NewEncoder(w).Encode(treefarm.Error{
Code: int32(code),
Message: message,
})
}

func (tf *TreeFarm) PlantTree(w http.ResponseWriter, r *http.Request) {
log.Printf("Received PlantTree request from %s", r.RemoteAddr)

var req treefarm.TreePlantingRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
sendError(w, http.StatusBadRequest, "Invalid request body: "+err.Error())
return
}

if req.CallbackURL == "" {
sendError(w, http.StatusBadRequest, "callbackUrl is required")
return
}

id := uuid.New()

log.Printf("Accepted tree planting: id=%s kind=%q location=%q callbackUrl=%q",
id, req.Kind, req.Location, req.CallbackURL)

w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusAccepted)
_ = json.NewEncoder(w).Encode(treefarm.TreeWithID{
Location: req.Location,
Kind: req.Kind,
ID: id,
})

go tf.plantAndNotify(id, req)
}

func (tf *TreeFarm) plantAndNotify(id uuid.UUID, req treefarm.TreePlantingRequest) {
delay := time.Duration(1+rand.IntN(5)) * time.Second
log.Printf("Planting tree %s (kind=%q, location=%q) — will complete in %s",
id, req.Kind, req.Location, delay)

time.Sleep(delay)

result := treefarm.TreePlantedJSONRequestBody{
ID: id,
Kind: req.Kind,
Location: req.Location,
Success: true,
}

log.Printf("Tree %s planted, invoking callback at %s", id, req.CallbackURL)

resp, err := tf.initiator.TreePlanted(context.Background(), req.CallbackURL, result)
if err != nil {
log.Printf("Callback to %s failed: %v", req.CallbackURL, err)
return
}
defer func() { _ = resp.Body.Close() }()

log.Printf("Callback to %s returned status %d", req.CallbackURL, resp.StatusCode)
}

func main() {
port := flag.String("port", "8080", "Port for HTTP server")
flag.Parse()

farm := NewTreeFarm()

mux := http.NewServeMux()
treefarm.HandlerFromMux(farm, mux)

// Wrap with request logging.
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Printf("%s %s from %s", r.Method, r.URL.Path, r.RemoteAddr)
mux.ServeHTTP(w, r)
})

addr := net.JoinHostPort("0.0.0.0", *port)
log.Printf("Tree Farm server listening on %s", addr)
log.Fatal(http.ListenAndServe(addr, handler))
}
Loading