Skip to content

Commit 1004749

Browse files
committed
feat(javascript): add HttpResponse.rebuild()
1 parent 4e8f1f0 commit 1004749

8 files changed

Lines changed: 473 additions & 31 deletions

File tree

docs/javascript-api/mokapi/eventhandler/httpresponse.md

Lines changed: 86 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,42 @@ description: HttpResponse is an object that is used to construct the HTTP respon
99
It is used to define the outgoing HTTP response, including status code, headers,
1010
and response body.
1111

12+
## Properties
13+
1214
| Name | Type | Description |
1315
|------------|--------|------------------------------------------------------------------------------|
1416
| statusCode | number | HTTP status code used to select the OpenAPI response definition |
1517
| headers | object | Response headers defined by the OpenAPI response header parameters |
1618
| body | string | Raw response body. Takes precedence over `data` |
1719
| data | any | Response data that will be encoded according to the OpenAPI response schema |
1820

21+
## Methods
22+
23+
| Name | Description |
24+
|----------------------------------|------------------------------|
25+
| rebuild(statusCode, contentType) | Rebuilds the HTTP response |
26+
27+
## Example
28+
29+
The following example demonstrates how to construct an HTTP response inside an
30+
HTTP event handler.
31+
32+
```javascript
33+
import { on } from 'mokapi'
34+
35+
export default function() {
36+
on('http', (request, response) => {
37+
response.statusCode = 200
38+
response.headers = {
39+
'Content-Type': 'application/json'
40+
}
41+
response.data = {
42+
message: 'Hello World'
43+
}
44+
})
45+
}
46+
```
47+
1948
## Default Response Generation
2049

2150
Mokapi automatically generates a valid HTTP response based on the OpenAPI
@@ -35,32 +64,75 @@ based on the schema defined for that status code.
3564
This behavior ensures that every response is valid according to the OpenAPI
3665
definition, even if the event handler does not explicitly modify the response.
3766

38-
## Example
67+
## Body vs Data
3968

40-
The following example demonstrates how to construct an HTTP response inside an
41-
HTTP event handler.
69+
Use body to return a raw response body without OpenAPI encoding and validating.
70+
71+
Use data to return structured data that should be validated and encoded
72+
according to the OpenAPI response definition
73+
74+
If both body and data are set, body takes precedence
75+
76+
## Rebuilding a Response
77+
78+
When changing the HTTP status code or content type, the existing response data
79+
and headers may no longer match the OpenAPI specification.
80+
81+
To address this, `HttpResponse` provides a helper function:
82+
83+
```typescript title=Definition
84+
function rebuild(statusCode?: number, contentType?: string): void {}
85+
```
86+
87+
This function rebuilds the entire HTTP response using the OpenAPI response
88+
definition for the given status code and content type.
89+
90+
### What rebuild does
91+
92+
Calling rebuild will:
93+
- Select the matching response definition from the OpenAPI specification
94+
- Set response.statusCode to the provided value
95+
- Generate valid response data based on the response schema
96+
- Generate valid response headers defined in the specification
97+
- Replace previously generated response data and headers
98+
99+
This ensures the response remains valid after changing the status code.
100+
101+
### When to Use rebuild
102+
103+
Use rebuild when:
104+
- You change the response status code
105+
- You want to switch to a different response definition
106+
- You want Mokapi to regenerate valid example data and headers
107+
108+
You do not need to call rebuild if you only modify fields within the
109+
already generated response.data.
110+
111+
### Example: Changing the Status Code Safely
42112

43113
```javascript
44114
import { on } from 'mokapi'
45115

46116
export default function() {
47117
on('http', (request, response) => {
48-
response.statusCode = 200
49-
response.headers = {
50-
'Content-Type': 'application/json'
51-
}
52-
response.data = {
53-
message: 'Hello World'
118+
if (request.path.petId === 10) {
119+
// Switch to a different OpenAPI response
120+
response.rebuild(404, 'application/json')
121+
122+
// Modify only what matters
123+
response.data.message = 'Pet not found'
54124
}
55125
})
56126
}
57127
```
58128

59-
## Body vs Data
129+
### Parameter Defaults
60130

61-
Use body to return a raw response body without OpenAPI encoding and validating.
131+
- If `statusCode` is not provided, Mokapi selects the OpenAPI `default` response.
132+
- If `contentType` is not provided, Mokapi selects the first content type
133+
defined for the selected status code in the OpenAPI specification.
62134

63-
Use data to return structured data that should be validated and encoded
64-
according to the OpenAPI response definition
135+
### Error handling
65136

66-
If both body and data are set, body takes precedence
137+
If `response.rebuild()` throws an error, and it is not caught, the current event
138+
handler is skipped and no response modifications from that handler are applied.

engine/common/http.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ type HttpEventResponse struct {
1111
StatusCode int `json:"statusCode"`
1212
Body string `json:"body"`
1313
Data any `json:"data"`
14+
15+
Rebuild func(statusCode int, contentType string) `json:"-"`
1416
}
1517

1618
type HttpEventRequest struct {

js/mokapi/on.go

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ func getHashes(args ...any) ([][]byte, error) {
122122
for _, arg := range args {
123123
b, err := json.Marshal(arg)
124124
if err != nil {
125-
return nil, fmt.Errorf("unable to marshal arg")
125+
return nil, fmt.Errorf("failed to marshal arg: %v", err)
126126
}
127127
result = append(result, b)
128128
}
@@ -151,6 +151,8 @@ func ArgToJs(arg any, vm *goja.Runtime) goja.Value {
151151
switch key {
152152
case "headers":
153153
p.KeyNormalizer = http.CanonicalHeaderKey
154+
case "rebuild":
155+
return rebuild(vm, v)
154156
}
155157

156158
switch val.(type) {
@@ -165,3 +167,28 @@ func ArgToJs(arg any, vm *goja.Runtime) goja.Value {
165167
return vm.ToValue(v)
166168
}
167169
}
170+
171+
func rebuild(vm *goja.Runtime, res *common.HttpEventResponse) goja.Value {
172+
if res.Rebuild == nil {
173+
return vm.ToValue(func() {})
174+
}
175+
return vm.ToValue(func(statusCode goja.Value, contentType goja.Value) {
176+
s := int64(0)
177+
c := ""
178+
if statusCode != nil {
179+
if statusCode.ExportType().Kind() != reflect.Int64 {
180+
panic(fmt.Sprintf("response.rebuild failed: statusCode must be a number: got %v", util.JsType(statusCode.Export())))
181+
} else {
182+
s = statusCode.ToInteger()
183+
}
184+
}
185+
if contentType != nil {
186+
if contentType.ExportType().Kind() != reflect.String {
187+
panic(fmt.Sprintf("response.rebuild failed: contentType must be a string: got %v", util.JsType(contentType.Export())))
188+
} else {
189+
c = contentType.String()
190+
}
191+
}
192+
res.Rebuild(int(s), c)
193+
})
194+
}

js/mokapi/on_test.go

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -490,6 +490,118 @@ m.on('http', (req, res) => {
490490
r.Equal(t, map[string]any{"foo": "yuh"}, res.Data)
491491
},
492492
},
493+
{
494+
name: "rebuild function not defined",
495+
script: `
496+
const m = require('mokapi')
497+
m.on('http', (req, res) => {
498+
res.rebuild();
499+
})
500+
`,
501+
run: func(evt common.EventEmitter) []*common.Action {
502+
res := &common.HttpEventResponse{Data: map[string]any{"foo": "bar"}}
503+
return evt.Emit("http", &common.HttpEventRequest{}, res)
504+
},
505+
test: func(t *testing.T, actions []*common.Action, err error) {
506+
r.NoError(t, err)
507+
508+
r.Nil(t, actions[0].Error)
509+
510+
var res *common.HttpEventResponse
511+
err = json.Unmarshal([]byte(actions[0].Parameters[1].(string)), &res)
512+
r.Equal(t, map[string]any{"foo": "bar"}, res.Data)
513+
},
514+
},
515+
{
516+
name: "rebuild function updates data",
517+
script: `
518+
const m = require('mokapi')
519+
m.on('http', (req, res) => {
520+
res.rebuild();
521+
})
522+
`,
523+
run: func(evt common.EventEmitter) []*common.Action {
524+
res := &common.HttpEventResponse{Data: map[string]any{"foo": "bar"}}
525+
res.Rebuild = func(statusCode int, contentType string) {
526+
res.Data = map[string]any{"foo": "yuh"}
527+
}
528+
return evt.Emit("http", &common.HttpEventRequest{}, res)
529+
},
530+
test: func(t *testing.T, actions []*common.Action, err error) {
531+
r.NoError(t, err)
532+
533+
r.Nil(t, actions[0].Error)
534+
535+
var res *common.HttpEventResponse
536+
err = json.Unmarshal([]byte(actions[0].Parameters[1].(string)), &res)
537+
r.Equal(t, map[string]any{"foo": "yuh"}, res.Data)
538+
},
539+
},
540+
{
541+
name: "rebuild function with parameters",
542+
script: `
543+
const m = require('mokapi')
544+
m.on('http', (req, res) => {
545+
res.rebuild(200, 'application/json');
546+
})
547+
`,
548+
run: func(evt common.EventEmitter) []*common.Action {
549+
res := &common.HttpEventResponse{Data: map[string]any{"foo": "bar"}}
550+
res.Rebuild = func(statusCode int, contentType string) {
551+
res.Data = map[string]any{"statusCode": statusCode, "contentType": contentType}
552+
}
553+
return evt.Emit("http", &common.HttpEventRequest{}, res)
554+
},
555+
test: func(t *testing.T, actions []*common.Action, err error) {
556+
r.NoError(t, err)
557+
558+
r.Nil(t, actions[0].Error)
559+
560+
var res *common.HttpEventResponse
561+
err = json.Unmarshal([]byte(actions[0].Parameters[1].(string)), &res)
562+
r.Equal(t, map[string]any{"statusCode": float64(200), "contentType": "application/json"}, res.Data)
563+
},
564+
},
565+
{
566+
name: "rebuild function wrong type statusCode",
567+
script: `
568+
const m = require('mokapi')
569+
m.on('http', (req, res) => {
570+
res.rebuild({ }, 'application/json');
571+
})
572+
`,
573+
run: func(evt common.EventEmitter) []*common.Action {
574+
res := &common.HttpEventResponse{Data: map[string]any{"foo": "bar"}}
575+
res.Rebuild = func(statusCode int, contentType string) {}
576+
return evt.Emit("http", &common.HttpEventRequest{}, res)
577+
},
578+
test: func(t *testing.T, actions []*common.Action, err error) {
579+
r.NoError(t, err)
580+
581+
r.NotNil(t, actions[0].Error)
582+
r.Equal(t, "response.rebuild failed: statusCode must be a number: got Object", actions[0].Error.Message)
583+
},
584+
},
585+
{
586+
name: "rebuild function wrong type contentType",
587+
script: `
588+
const m = require('mokapi')
589+
m.on('http', (req, res) => {
590+
res.rebuild(100, 200);
591+
})
592+
`,
593+
run: func(evt common.EventEmitter) []*common.Action {
594+
res := &common.HttpEventResponse{Data: map[string]any{"foo": "bar"}}
595+
res.Rebuild = func(statusCode int, contentType string) {}
596+
return evt.Emit("http", &common.HttpEventRequest{}, res)
597+
},
598+
test: func(t *testing.T, actions []*common.Action, err error) {
599+
r.NoError(t, err)
600+
601+
r.NotNil(t, actions[0].Error)
602+
r.Equal(t, "response.rebuild failed: contentType must be a string: got Integer", actions[0].Error.Message)
603+
},
604+
},
493605
}
494606

495607
for _, tc := range testcases {

js/mokapi/proxy.go

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,10 @@ func (p *Proxy) Get(key string) goja.Value {
5454
v := target.MapIndex(reflect.ValueOf(key))
5555
return p.toJSValue(key, v)
5656
case reflect.Struct:
57-
f := getFieldByTag(target, key, "json")
58-
return p.toJSValue(key, f)
57+
f := getField(target, key, "json")
58+
if f.IsValid() {
59+
return p.toJSValue(key, f)
60+
}
5961
case reflect.Slice:
6062
switch key {
6163
case "length":
@@ -116,9 +118,8 @@ func (p *Proxy) Get(key string) goja.Value {
116118
}
117119
return goja.Undefined()
118120
}
119-
default:
120-
return goja.Undefined()
121121
}
122+
panic(fmt.Sprintf("%s is not defined", key))
122123
}
123124

124125
func (p *Proxy) Has(key string) bool {
@@ -133,7 +134,7 @@ func (p *Proxy) Has(key string) bool {
133134
k := target.MapIndex(reflect.ValueOf(key))
134135
return k.IsValid()
135136
case reflect.Struct:
136-
f := getFieldByTag(target, key, "json")
137+
f := getField(target, key, "json")
137138
return f.IsValid()
138139
default:
139140
return false
@@ -155,7 +156,7 @@ func (p *Proxy) Set(key string, value goja.Value) bool {
155156
target.SetMapIndex(reflect.ValueOf(key), v)
156157
return true
157158
case reflect.Struct:
158-
f := getFieldByTag(target, key, "json")
159+
f := getField(target, key, "json")
159160
err := assignValue(f, value.Export(), key)
160161
if err != nil {
161162
panic(p.vm.ToValue(err))
@@ -216,6 +217,10 @@ func (p *Proxy) normalizeKey(key string) string {
216217
}
217218

218219
func (p *Proxy) toJSValue(key string, v reflect.Value) goja.Value {
220+
if !v.IsValid() {
221+
return goja.Undefined()
222+
}
223+
219224
if p.ToJSValue != nil {
220225
return p.ToJSValue(p.vm, key, v.Interface())
221226
}
@@ -242,10 +247,15 @@ func (p *Proxy) Export() any {
242247
return Export(v)
243248
}
244249

245-
func getFieldByTag(structValue reflect.Value, name, tag string) reflect.Value {
250+
func getField(structValue reflect.Value, name, tag string) reflect.Value {
251+
name = capitalize(name)
246252
for i := 0; i < structValue.NumField(); i++ {
247-
v := structValue.Type().Field(i).Tag.Get(tag)
248-
tagValues := strings.Split(v, ",")
253+
f := structValue.Type().Field(i)
254+
if f.Name == name {
255+
return structValue.Field(i)
256+
}
257+
t := f.Tag.Get(tag)
258+
tagValues := strings.Split(t, ",")
249259
for _, tagValue := range tagValues {
250260
if tagValue == name {
251261
return structValue.Field(i)
@@ -362,3 +372,7 @@ func unwrap(v reflect.Value) reflect.Value {
362372
}
363373
}
364374
}
375+
376+
func capitalize(s string) string {
377+
return strings.ToUpper(s[0:1]) + s[1:]
378+
}

0 commit comments

Comments
 (0)