-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathresponseheader.go
More file actions
215 lines (193 loc) · 5.86 KB
/
responseheader.go
File metadata and controls
215 lines (193 loc) · 5.86 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
package shiftapi
import (
"encoding/json"
"fmt"
"net/http"
"reflect"
"github.com/getkin/kin-openapi/openapi3"
)
var jsonMarshalerType = reflect.TypeFor[json.Marshaler]()
// hasRespHeaderFields reports whether the type has any exported fields with a `header` tag.
// This is used for response types to determine if response headers need to be written.
func hasRespHeaderFields(t reflect.Type) bool {
for t != nil && t.Kind() == reflect.Pointer {
t = t.Elem()
}
if t == nil || t.Kind() != reflect.Struct {
return false
}
for f := range t.Fields() {
if f.IsExported() && hasHeaderTag(f) {
return true
}
}
return false
}
// writeResponseHeaders extracts header-tagged fields from a response value
// and sets them on the ResponseWriter. Fields are formatted as their string
// representation. Only scalar types and pointer-to-scalar types are supported.
func writeResponseHeaders(w http.ResponseWriter, resp any) {
rv := reflect.ValueOf(resp)
for rv.Kind() == reflect.Pointer {
if rv.IsNil() {
return
}
rv = rv.Elem()
}
if rv.Kind() != reflect.Struct {
return
}
rt := rv.Type()
for i := range rt.NumField() {
field := rt.Field(i)
if !field.IsExported() || !hasHeaderTag(field) {
continue
}
name := http.CanonicalHeaderKey(headerFieldName(field))
fv := rv.Field(i)
// Pointer fields are optional — skip when nil.
if fv.Kind() == reflect.Pointer {
if fv.IsNil() {
continue
}
fv = fv.Elem()
}
// Non-pointer fields are always sent (consistent with required
// semantics elsewhere in the framework).
w.Header().Set(name, fmt.Sprint(fv.Interface()))
}
}
// respEncoder strips header-tagged fields from a response before JSON encoding.
// The derived struct type is built once at registration time so that runtime
// encoding uses a real struct — preserving omitempty, custom marshalers on
// field types, embedded structs, and all other encoding/json behavior.
//
// When the response type itself implements json.Marshaler, the encoder falls
// back to encoding the original value (customJSON mode) so the custom method
// is preserved. Header fields may appear in the JSON body in this case — the
// user controls their own encoding.
type respEncoder struct {
derivedType reflect.Type // struct type without header fields (nil when customJSON)
fieldMapping []int // derivedType field i ← original field fieldMapping[i]
customJSON bool // true when the original type implements json.Marshaler
}
// newRespEncoder builds a derived struct type from t that excludes all
// header-tagged fields. Returns nil if t is not a struct.
//
// If t (or *t) implements json.Marshaler, the encoder is returned with
// customJSON set to true — the original value is encoded as-is so the
// custom MarshalJSON is preserved, and only header extraction is performed.
// Unexported fields are excluded from the derived type — they don't affect
// JSON output and cannot be copied via reflect.
func newRespEncoder(t reflect.Type) *respEncoder {
for t.Kind() == reflect.Pointer {
t = t.Elem()
}
if t.Kind() != reflect.Struct {
return nil
}
// If the type has a custom MarshalJSON, we can't use a derived struct
// (it would lose the method). Fall back to encoding the original value.
if t.Implements(jsonMarshalerType) || reflect.PointerTo(t).Implements(jsonMarshalerType) {
return &respEncoder{customJSON: true}
}
var fields []reflect.StructField
var mapping []int
for i := range t.NumField() {
f := t.Field(i)
if !f.IsExported() {
continue // skip unexported — can't copy via reflect, invisible to encoding/json
}
if hasHeaderTag(f) {
continue
}
fields = append(fields, f)
mapping = append(mapping, i)
}
return &respEncoder{
derivedType: reflect.StructOf(fields),
fieldMapping: mapping,
}
}
// encode returns a value suitable for JSON encoding. When the response type
// has a custom MarshalJSON, it returns the original value unchanged. Otherwise
// it copies non-header fields into the derived struct type.
func (e *respEncoder) encode(resp any) any {
if e.customJSON {
return resp
}
rv := reflect.ValueOf(resp)
for rv.Kind() == reflect.Pointer {
if rv.IsNil() {
return resp
}
rv = rv.Elem()
}
if rv.Kind() != reflect.Struct {
return resp
}
derived := reflect.New(e.derivedType).Elem()
for i, origIdx := range e.fieldMapping {
derived.Field(i).Set(rv.Field(origIdx))
}
return derived.Interface()
}
// generateRespHeaders builds OpenAPI header definitions from header-tagged
// fields on a response struct type.
func generateRespHeaders(t reflect.Type) openapi3.Headers {
for t.Kind() == reflect.Pointer {
t = t.Elem()
}
if t.Kind() != reflect.Struct {
return nil
}
headers := make(openapi3.Headers)
for f := range t.Fields() {
if !f.IsExported() || !hasHeaderTag(f) {
continue
}
name := http.CanonicalHeaderKey(headerFieldName(f))
schema := scalarToOpenAPISchema(f.Type)
required := f.Type.Kind() != reflect.Pointer
headers[name] = &openapi3.HeaderRef{
Value: &openapi3.Header{
Parameter: openapi3.Parameter{
Name: name,
In: "header",
Required: required,
Schema: schema,
},
},
}
}
if len(headers) == 0 {
return nil
}
return headers
}
// stripRespHeaderFields removes header-tagged fields from a response body
// schema's Properties and Required slices so they don't appear in the JSON body.
func stripRespHeaderFields(t reflect.Type, schema *openapi3.Schema) {
for t.Kind() == reflect.Pointer {
t = t.Elem()
}
if t.Kind() != reflect.Struct || schema == nil {
return
}
for f := range t.Fields() {
if !f.IsExported() || !hasHeaderTag(f) {
continue
}
jname := jsonFieldName(f)
if jname == "" || jname == "-" {
continue
}
delete(schema.Properties, jname)
for j, req := range schema.Required {
if req == jname {
schema.Required = append(schema.Required[:j], schema.Required[j+1:]...)
break
}
}
}
}