Skip to content

Commit 98f72dc

Browse files
committed
wip: also make resources available via tool
1 parent c843966 commit 98f72dc

23 files changed

Lines changed: 1134 additions & 119 deletions

mcp/conditional-response.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# Scenario conditional-response
2+
3+
HTTP mock handler for terminals.
4+
Demonstrates how to:
5+
- Access request parameters
6+
- Apply custom logic (e.g., lookup, filtering, updates)
7+
8+
```typescript
9+
import { on } from "mokapi"
10+
11+
interface Terminal {
12+
id: string
13+
compartments: {
14+
id: string
15+
doorState: 'open' | 'closed'
16+
}[]
17+
}
18+
19+
const terminals: Terminal[] = []
20+
21+
export default function () {
22+
on('http', (request, response) => {
23+
switch(request.key) {
24+
case '/terminals/{id}': {
25+
const terminal = terminals.find(x => x.id === request.path.id)
26+
if (!terminal) {
27+
response.rebuild(404)
28+
response.data = { error: 'terminal not found' }
29+
return
30+
}
31+
32+
if (request.method === 'GET') {
33+
response.data = terminal
34+
} else if (request.method === 'POST') {
35+
// update the terminal
36+
Object.assign(terminal, request.body)
37+
// mokapi already set the success response, nothing to do
38+
}
39+
// do not raise an error if different method is used,
40+
// maybe there is another event handler in a different file defined
41+
return
42+
}
43+
case '/terminals': {
44+
if (request.method === 'GET') {
45+
response.data = terminals
46+
} else if (request.method === 'POST') {
47+
const terminal = terminals.find(x => x.id === request.path.id)
48+
if (terminal) {
49+
// console output will be displayed in the Mokapi's' dashboard
50+
console.log('terminal already exists', request.body)
51+
response.rebuild(400)
52+
} else {
53+
terminals.push(request.body)
54+
}
55+
}
56+
return
57+
}
58+
}
59+
})
60+
}
61+
```

mcp/delay-latency.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Scenario delay-latency
2+
3+
Simulate server latency by delaying the response. Useful to test scenarios:
4+
- frontend loading states
5+
- timeouts
6+
- high-load
7+
8+
```typescript
9+
import { on } from "mokapi"
10+
11+
let pets = [
12+
{ id: 1, name: 'Fluffy', status: 'available', category: { id: 1, name: 'Dogs' }, photoUrls: [], tags: [] },
13+
{ id: 3, name: 'Hedgie', status: 'pending', category: { id: 2, name: 'Small Animals' }, photoUrls: [], tags: [] }
14+
];
15+
16+
export default function () {
17+
on('http', async (request, response) => {
18+
switch(request.key) {
19+
case '/pets': {
20+
if (request.method !== 'GET') return
21+
22+
// simulate network latency (e.g., 2 seconds)
23+
sleep('2s')
24+
25+
response.data = pets
26+
}
27+
}
28+
})
29+
}
30+
```

mcp/dynamic-error-simulation.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Scenario dynamic-error-simulation
2+
3+
Return error responses based on runtime conditions, such as missing resources, validation failures, or conflicting state.
4+
5+
```typescript
6+
import { on } from "mokapi"
7+
8+
const hotels = []
9+
10+
export default function () {
11+
on('http', (request, response) => {
12+
switch(request.key) {
13+
case '/bookings': {
14+
const hotel = hotels.find(x => x.code === request.body?.hotel?.code)
15+
16+
if (!hotel) {
17+
console.log('hotel not found')
18+
response.rebuild(404)
19+
response.data = { error: 'hotel not found' }
20+
return
21+
}
22+
23+
// simulate dynamic errors based on hotel simulation config
24+
const type = hotel.simulation?.responseType
25+
switch (type) {
26+
case 'bad-request':
27+
response.rebuild(400)
28+
return
29+
case 'unauthorized':
30+
response.rebuild(401)
31+
return
32+
case 'forbidden':
33+
response.rebuild(403)
34+
return
35+
case 'internal-server-error':
36+
response.rebuild(500)
37+
return
38+
}
39+
// success path: generate valid response
40+
// ...
41+
}
42+
}
43+
})
44+
}
45+
```

mcp/dynamic-path-params.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Scenario dynamic-path-params
2+
3+
HTTP mock handler to get a pet stored in an array list.
4+
Demonstrates how to:
5+
- Access request parameters
6+
- Apply custom logic (e.g., lookup, filtering)
7+
8+
```typescript
9+
import { on } from "mokapi"
10+
11+
let pets = [
12+
{ id: 1, name: 'Fluffy', status: 'available', category: { id: 1, name: 'Dogs' }, photoUrls: [], tags: [] },
13+
{ id: 3, name: 'Hedgie', status: 'pending', category: { id: 2, name: 'Small Animals' }, photoUrls: [], tags: [] }
14+
];
15+
16+
export default function () {
17+
on('http', async (request, response) => {
18+
switch(request.key) {
19+
case '/pets/{id}':
20+
if (request.method !== 'GET') {
21+
return
22+
}
23+
const pet = pets.find(x => x.id === request.path.id)
24+
if (pet) {
25+
response.data = pet
26+
} else {
27+
console.log('pet not found', request)
28+
response.rebuild(404)
29+
}
30+
}
31+
})
32+
}
33+
```

mcp/events.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package mcp
2+
3+
import (
4+
"fmt"
5+
"mokapi/js/util"
6+
"mokapi/runtime/events"
7+
"reflect"
8+
"strings"
9+
10+
"github.com/dop251/goja"
11+
)
12+
13+
func (m *mokapi) getEvents(vTraits goja.Value, vLimit goja.Value) ([]events.Event, error) {
14+
traits, err := parseTraits(vTraits, m.vm)
15+
if err != nil {
16+
return nil, err
17+
}
18+
19+
evts := m.app.Events.GetEvents(traits)
20+
21+
limit := 10
22+
if vLimit != nil {
23+
if vLimit.ExportType().Kind() != reflect.Float64 {
24+
return nil, fmt.Errorf("unexpected type for apiType: %s", util.JsType(vLimit.ExportType()))
25+
}
26+
limit = int(vLimit.ToInteger())
27+
}
28+
if len(evts) > limit {
29+
return evts[0:limit], nil
30+
} else {
31+
return evts, nil
32+
}
33+
}
34+
35+
func parseTraits(v goja.Value, vm *goja.Runtime) (events.Traits, error) {
36+
traits := events.Traits{}
37+
38+
if v == nil {
39+
return traits, nil
40+
}
41+
42+
if v.ExportType().Kind() != reflect.Map {
43+
return nil, fmt.Errorf("expect object but got: %v", util.JsType(v.Export()))
44+
}
45+
46+
obj := v.ToObject(vm)
47+
for _, k := range obj.Keys() {
48+
switch k {
49+
case "type":
50+
val := obj.Get(k)
51+
if val.ExportType().Kind() != reflect.String {
52+
return nil, fmt.Errorf("unexpected type for type: %s", util.JsType(val.ExportType()))
53+
}
54+
traits.WithNamespace(val.String())
55+
case "name":
56+
val := obj.Get(k)
57+
if val.ExportType().Kind() != reflect.String {
58+
return nil, fmt.Errorf("unexpected type for name: %s", util.JsType(val.ExportType()))
59+
}
60+
traits.WithName(val.String())
61+
case "method":
62+
val := obj.Get(k)
63+
if val.ExportType().Kind() != reflect.String {
64+
return nil, fmt.Errorf("unexpected type for method: %s", util.JsType(val.ExportType()))
65+
}
66+
traits.With("method", strings.ToUpper(val.String()))
67+
default:
68+
val := obj.Get(k)
69+
if val.ExportType().Kind() != reflect.String {
70+
return nil, fmt.Errorf("unexpected type for %s: %s", k, util.JsType(val.ExportType()))
71+
}
72+
traits.With(k, val.String())
73+
}
74+
}
75+
76+
return traits, nil
77+
}

mcp/events_test.go

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
package mcp_test
2+
3+
import (
4+
"context"
5+
"mokapi/mcp"
6+
"mokapi/runtime"
7+
"mokapi/runtime/events"
8+
"mokapi/runtime/runtimetest"
9+
"testing"
10+
11+
"github.com/stretchr/testify/require"
12+
)
13+
14+
type testEvent struct {
15+
Name string
16+
}
17+
18+
func (t *testEvent) Title() string {
19+
return t.Name
20+
}
21+
22+
func TestEvents(t *testing.T) {
23+
testcases := []struct {
24+
name string
25+
app *runtime.App
26+
code string
27+
test func(t *testing.T, evts []events.Event, err error)
28+
}{
29+
{
30+
name: "without params should not error",
31+
code: "mokapi.getEvents()",
32+
app: runtimetest.NewApp(
33+
runtimetest.WithEvent(events.NewTraits().WithNamespace("http"), &testEvent{Name: "test-1"}),
34+
),
35+
test: func(t *testing.T, evts []events.Event, err error) {
36+
require.NoError(t, err)
37+
require.Len(t, evts, 1)
38+
require.Equal(t, &testEvent{Name: "test-1"}, evts[0].Data)
39+
},
40+
},
41+
{
42+
name: "filter by API type",
43+
code: "mokapi.getEvents({ type: 'http' })",
44+
app: runtimetest.NewApp(
45+
runtimetest.WithEvent(events.NewTraits().WithNamespace("kafka"), &testEvent{Name: "test-1"}),
46+
runtimetest.WithEvent(events.NewTraits().WithNamespace("http"), &testEvent{Name: "test-2"}),
47+
),
48+
test: func(t *testing.T, evts []events.Event, err error) {
49+
require.NoError(t, err)
50+
require.Len(t, evts, 1)
51+
require.Equal(t, &testEvent{Name: "test-2"}, evts[0].Data)
52+
},
53+
},
54+
{
55+
name: "filter by API name",
56+
code: "mokapi.getEvents({ name: 'bar' })",
57+
app: runtimetest.NewApp(
58+
runtimetest.WithEvent(events.NewTraits().WithNamespace("http").WithName("foo"), &testEvent{Name: "test-1"}),
59+
runtimetest.WithEvent(events.NewTraits().WithNamespace("http").WithName("bar"), &testEvent{Name: "test-2"}),
60+
),
61+
test: func(t *testing.T, evts []events.Event, err error) {
62+
require.NoError(t, err)
63+
require.Len(t, evts, 1)
64+
require.Equal(t, &testEvent{Name: "test-2"}, evts[0].Data)
65+
},
66+
},
67+
{
68+
name: "filter by path",
69+
code: "mokapi.getEvents({ path: '/pets' })",
70+
app: runtimetest.NewApp(
71+
runtimetest.WithEvent(events.NewTraits().WithNamespace("http").With("path", "/users"), &testEvent{Name: "test-1"}),
72+
runtimetest.WithEvent(events.NewTraits().WithNamespace("http").With("path", "/pets"), &testEvent{Name: "test-2"}),
73+
),
74+
test: func(t *testing.T, evts []events.Event, err error) {
75+
require.NoError(t, err)
76+
require.Len(t, evts, 1)
77+
require.Equal(t, &testEvent{Name: "test-2"}, evts[0].Data)
78+
},
79+
},
80+
{
81+
name: "filter by method",
82+
code: "mokapi.getEvents({ method: 'post' })",
83+
app: runtimetest.NewApp(
84+
runtimetest.WithEvent(events.NewTraits().WithNamespace("http").With("method", "GET"), &testEvent{Name: "test-1"}),
85+
runtimetest.WithEvent(events.NewTraits().WithNamespace("http").With("method", "POST"), &testEvent{Name: "test-2"}),
86+
),
87+
test: func(t *testing.T, evts []events.Event, err error) {
88+
require.NoError(t, err)
89+
require.Len(t, evts, 1)
90+
require.Equal(t, &testEvent{Name: "test-2"}, evts[0].Data)
91+
},
92+
},
93+
}
94+
95+
for _, tc := range testcases {
96+
t.Run(tc.name, func(t *testing.T) {
97+
s := mcp.NewService(tc.app)
98+
99+
r, err := s.GetRunResponse(
100+
context.Background(),
101+
mcp.RunInput{Code: tc.code},
102+
)
103+
require.IsType(t, []events.Event{}, r.Result)
104+
105+
tc.test(t, r.Result.([]events.Event), err)
106+
})
107+
}
108+
}

0 commit comments

Comments
 (0)