diff --git a/.github/actions/run-frontend-tests/action.yml b/.github/actions/run-frontend-tests/action.yml index 5c8f568e2..98aaa1cb8 100644 --- a/.github/actions/run-frontend-tests/action.yml +++ b/.github/actions/run-frontend-tests/action.yml @@ -23,7 +23,7 @@ runs: run: docker load --input /tmp/mokapi.tar shell: bash - name: Run mokapi image - run: docker run --name mokapi --rm -d -p 8090:8090 -p 8091:8091 --mount type=bind,source=$(pwd)/examples/mokapi,target=/data --env MOKAPI_Log_Level=Debug --env MOKAPI_Api_Port=8091 --env MOKAPI_Api_Path=/mokapi --env MOKAPI_Providers_File_Directory=/data ${{ inputs.image-name }} + run: docker run --name mokapi --rm -d -p 8090:8090 -p 8091:8091 --mount type=bind,source=$(pwd)/webui/e2e/mocks,target=/data --env MOKAPI_Log_Level=Debug --env MOKAPI_Api_Port=8091 --env MOKAPI_Api_Path=/mokapi --env MOKAPI_Providers_File_Directory=/data ${{ inputs.image-name }} shell: bash - uses: actions/setup-node@v4 with: @@ -49,7 +49,7 @@ runs: uses: actions/upload-artifact@v4 with: name: ${{ inputs.artifact-test-report }} - path: webui/playwright-report + path: ./webui/playwright-report - name: Save Mokapi logs if: always() run: docker logs mokapi > /var/tmp/mokapi.log 2>&1 diff --git a/api/handler_http.go b/api/handler_http.go index f6134df5a..f6adb8646 100644 --- a/api/handler_http.go +++ b/api/handler_http.go @@ -112,6 +112,10 @@ func getHttpServices(list []*runtime.HttpInfo, m *monitor.Monitor) []interface{} Type: ServiceHttp, } + if hs.Info.Summary != "" { + s.Description = hs.Info.Summary + } + if m != nil { s.Metrics = m.FindAll(metrics.ByNamespace("http"), metrics.ByLabel("service", hs.Info.Name)) } diff --git a/api/handler_http_test.go b/api/handler_http_test.go index d303d9774..93f015f2a 100644 --- a/api/handler_http_test.go +++ b/api/handler_http_test.go @@ -41,6 +41,19 @@ func TestHandler_Http(t *testing.T) { requestUrl: "http://foo.api/api/services", responseBody: `[{"name":"foo","description":"bar","version":"1.0","type":"http"}`, }, + { + name: "summary takes precedence over description", + app: func() *runtime.App { + return runtimetest.NewHttpApp( + openapitest.NewConfig("3.0.0", + openapitest.WithInfo("foo", "1.0", "bar"), + openapitest.WithSummary("summary"), + ), + ) + }, + requestUrl: "http://foo.api/api/services", + responseBody: `[{"name":"foo","description":"summary","version":"1.0","type":"http"}`, + }, { name: "get http services with contact", app: func() *runtime.App { @@ -210,7 +223,7 @@ func TestHandler_Http(t *testing.T) { c := openapitest.NewConfig("3.0.0", openapitest.WithInfo("foo", "", ""), openapitest.WithPathRef("/foo/{bar}", &openapi.PathRef{ - Reference: dynamic.Reference{ + Reference: dynamic.Reference[*openapi.PathRef]{ Ref: "#/components/pathItems/foo", Summary: "Summary", Description: "Description", @@ -220,7 +233,7 @@ func TestHandler_Http(t *testing.T) { openapitest.WithOperation("get", openapitest.UseResponseRef(http.StatusOK, &openapi.ResponseRef{ - Reference: dynamic.Reference{ + Reference: dynamic.Reference[*openapi.ResponseRef]{ Ref: "#/components/pathItems/foo", Description: "Description", }, diff --git a/api/handler_kafka_test.go b/api/handler_kafka_test.go index e358f4b29..d1be7baf0 100644 --- a/api/handler_kafka_test.go +++ b/api/handler_kafka_test.go @@ -4,7 +4,6 @@ import ( "encoding/json" "mokapi/api" "mokapi/config/dynamic" - "mokapi/config/dynamic/asyncApi" "mokapi/config/dynamic/dynamictest" "mokapi/config/static" "mokapi/engine/enginetest" @@ -1098,7 +1097,7 @@ func TestHandler_Kafka_Metrics(t *testing.T) { } func Test_IsAsyncApiConfig(t *testing.T) { - v2 := &asyncApi.Config{Info: asyncApi.Info{Name: "foo"}} + v2 := &asyncapi3.Config{Info: asyncapi3.Info{Name: "foo"}} _, ok := runtime.IsAsyncApiConfig(&dynamic.Config{Data: v2}) require.True(t, ok) diff --git a/api/handler_schema_test.go b/api/handler_schema_test.go index 3f07355fb..9edb633b4 100644 --- a/api/handler_schema_test.go +++ b/api/handler_schema_test.go @@ -3,6 +3,7 @@ package api import ( "encoding/base64" "encoding/json" + "mokapi/config/dynamic" "mokapi/config/static" "mokapi/providers/asyncapi3/asyncapi3test" "mokapi/providers/openapi/openapitest" @@ -540,7 +541,7 @@ func TestSchemaInfo_MarshalJSON(t *testing.T) { }, { name: "json schema only ref", - s: &schemaInfo{Schema: &jsonSchema.Schema{Ref: "foo/bar"}}, + s: &schemaInfo{Schema: &jsonSchema.Schema{Reference: dynamic.Reference[*jsonSchema.Schema]{Ref: "foo/bar"}}}, test: func(t *testing.T, s string, err error) { require.NoError(t, err) require.Equal(t, `{"schema":{"$ref":"foo/bar"}}`, s) @@ -549,8 +550,8 @@ func TestSchemaInfo_MarshalJSON(t *testing.T) { { name: "json schema ref and value", s: &schemaInfo{Schema: &jsonSchema.Schema{ - Ref: "foo/bar", - Type: jsonSchema.Types{"string"}, + Reference: dynamic.Reference[*jsonSchema.Schema]{Ref: "foo/bar"}, + Type: jsonSchema.Types{"string"}, }}, test: func(t *testing.T, s string, err error) { require.NoError(t, err) diff --git a/api/handler_search_test.go b/api/handler_search_test.go index c98370e12..4c81473df 100644 --- a/api/handler_search_test.go +++ b/api/handler_search_test.go @@ -4,10 +4,10 @@ import ( "context" "encoding/json" "mokapi/config/dynamic" - "mokapi/config/dynamic/asyncApi/asyncapitest" "mokapi/config/dynamic/dynamictest" "mokapi/config/static" "mokapi/engine/enginetest" + "mokapi/providers/asyncapi3/asyncapi3test" "mokapi/providers/openapi/openapitest" "mokapi/runtime" "mokapi/runtime/search" @@ -143,7 +143,7 @@ func TestHandler_SearchQuery(t *testing.T) { h := openapitest.NewConfig("3.0", openapitest.WithInfo("foo", "", "")) app.AddHttp(toConfig(h)) - k := asyncapitest.NewConfig(asyncapitest.WithInfo("foo", "", "")) + k := asyncapi3test.NewConfig(asyncapi3test.WithInfo("foo", "", "")) _, err := app.Kafka.Add(toConfig(k), enginetest.NewEngine()) require.NoError(t, err) @@ -173,7 +173,7 @@ func TestHandler_SearchQuery(t *testing.T) { h := openapitest.NewConfig("3.0", openapitest.WithInfo("foo", "", "")) app.AddHttp(toConfig(h)) - k := asyncapitest.NewConfig(asyncapitest.WithInfo("foo", "", "")) + k := asyncapi3test.NewConfig(asyncapi3test.WithInfo("foo", "", "")) _, err := app.Kafka.Add(toConfig(k), enginetest.NewEngine()) require.NoError(t, err) diff --git a/config/dynamic/asyncApi/config.go b/config/dynamic/asyncApi/config.go index 1f1e0607a..88bace64b 100644 --- a/config/dynamic/asyncApi/config.go +++ b/config/dynamic/asyncApi/config.go @@ -70,7 +70,7 @@ type ServerTag struct { } type ServerRef struct { - Ref string + dynamic.Reference[*ServerRef] Value *Server } @@ -79,7 +79,7 @@ type ServerBindings struct { } type ChannelRef struct { - Ref string + dynamic.Reference[*ChannelRef] Value *Channel } @@ -109,7 +109,7 @@ type OperationBindings struct { } type MessageRef struct { - Ref string + dynamic.Reference[*MessageRef] Value *Message } @@ -141,7 +141,7 @@ type Components struct { } type ParameterRef struct { - dynamic.Reference + dynamic.Reference[*ParameterRef] Value *Parameter } @@ -152,7 +152,7 @@ type Parameter struct { } type MessageTraitRef struct { - dynamic.Reference + dynamic.Reference[*MessageTraitRef] Value *MessageTrait } diff --git a/config/dynamic/asyncApi/convert.go b/config/dynamic/asyncApi/convert.go index 497d1eedc..55cf5194b 100644 --- a/config/dynamic/asyncApi/convert.go +++ b/config/dynamic/asyncApi/convert.go @@ -66,24 +66,21 @@ func convertChannels(cfg *asyncapi3.Config, channels map[string]*ChannelRef) err if orig == nil { continue } - if len(orig.Ref) > 0 { - cfg.Channels[name] = &asyncapi3.ChannelRef{Reference: dynamic.Reference{Ref: orig.Ref}} + ch, err := convertChannel(name, orig, cfg) + if err != nil { + return err } - if orig.Value != nil { - ch, err := convertChannel(name, orig, cfg) - if err != nil { - return err - } + if ch.Value != nil { ch.Value.Config = cfg - cfg.Channels[name] = ch } + cfg.Channels[name] = ch } return nil } func convertChannel(name string, c *ChannelRef, config *asyncapi3.Config) (*asyncapi3.ChannelRef, error) { - result := &asyncapi3.ChannelRef{Reference: dynamic.Reference{Ref: c.Ref}} + result := &asyncapi3.ChannelRef{Reference: dynamic.Reference[*asyncapi3.ChannelRef]{Ref: c.Ref}} if c.Value != nil { target := &asyncapi3.Channel{ @@ -98,7 +95,7 @@ func convertChannel(name string, c *ChannelRef, config *asyncapi3.Config) (*asyn for _, server := range c.Value.Servers { target.Servers = append( target.Servers, - &asyncapi3.ServerRef{Reference: dynamic.Reference{ + &asyncapi3.ServerRef{Reference: dynamic.Reference[*asyncapi3.ServerRef]{ Ref: fmt.Sprintf("#/servers/%s", server), }}) } @@ -192,7 +189,7 @@ func convertMessage(msg *MessageRef) *asyncapi3.MessageRef { return nil } - target := &asyncapi3.MessageRef{Reference: dynamic.Reference{Ref: msg.Ref}} + target := &asyncapi3.MessageRef{Reference: dynamic.Reference[*asyncapi3.MessageRef]{Ref: msg.Ref}} if msg.Value != nil { target.Value = &asyncapi3.Message{ Title: msg.Value.Title, @@ -200,45 +197,42 @@ func convertMessage(msg *MessageRef) *asyncapi3.MessageRef { Summary: msg.Value.Summary, Description: msg.Value.Description, ContentType: msg.Value.ContentType, - Payload: nil, + Payload: msg.Value.Payload, Bindings: convertMessageBinding(msg.Value.Bindings), + Headers: msg.Value.Headers, ExternalDocs: nil, } - if msg.Value.Payload != nil && msg.Value.Payload.Value != nil { - target.Value.Payload = msg.Value.Payload - } - if msg.Value.Headers != nil && msg.Value.Headers.Value != nil { - target.Value.Headers = msg.Value.Headers - } for _, orig := range msg.Value.Traits { - trait := &asyncapi3.MessageTraitRef{} - if len(orig.Ref) > 0 { - trait.Reference = dynamic.Reference{Ref: trait.Ref} - } - if orig.Value != nil { - trait.Value = convertMessageTrait(orig.Value).Value + trait := convertMessageTrait(orig) + if trait != nil { + target.Value.Traits = append(target.Value.Traits, trait) } - target.Value.Traits = append(target.Value.Traits, trait) } } return target } -func convertMessageTrait(trait *MessageTrait) *asyncapi3.MessageTraitRef { - target := &asyncapi3.MessageTrait{ - Name: trait.Name, - Title: trait.Title, - Summary: trait.Summary, - Description: trait.Description, - ContentType: trait.ContentType, - Bindings: convertMessageBinding(trait.Bindings), +func convertMessageTrait(trait *MessageTraitRef) *asyncapi3.MessageTraitRef { + target := &asyncapi3.MessageTraitRef{ + Reference: dynamic.Reference[*asyncapi3.MessageTraitRef]{Ref: trait.Ref}, } - if trait.Headers != nil && trait.Headers.Value != nil { - target.Headers = trait.Headers + if trait.Value != nil { + target.Value = &asyncapi3.MessageTrait{ + Name: trait.Value.Name, + Title: trait.Value.Title, + Summary: trait.Value.Summary, + Description: trait.Value.Description, + ContentType: trait.Value.ContentType, + Bindings: convertMessageBinding(trait.Value.Bindings), + } + if trait.Value.Headers != nil && trait.Value.Headers.Value != nil { + target.Value.Headers = trait.Value.Headers + } } - return &asyncapi3.MessageTraitRef{Value: target} + + return target } func convertParameters(channel *asyncapi3.Channel, params map[string]*ParameterRef) error { @@ -251,7 +245,7 @@ func convertParameters(channel *asyncapi3.Channel, params map[string]*ParameterR for name, orig := range params { if len(orig.Ref) > 0 { - channel.Parameters[name] = &asyncapi3.ParameterRef{Reference: dynamic.Reference{Ref: orig.Ref}} + channel.Parameters[name] = &asyncapi3.ParameterRef{Reference: dynamic.Reference[*asyncapi3.ParameterRef]{Ref: orig.Ref}} } if orig.Value != nil { p, err := convertParameter(name, orig.Value) @@ -308,17 +302,15 @@ func convertServers(cfg *asyncapi3.Config, servers map[string]*ServerRef) { } for name, orig := range servers { - if len(orig.Ref) > 0 { - cfg.Servers.Set(name, &asyncapi3.ServerRef{Reference: dynamic.Reference{Ref: orig.Ref}}) - } - if orig.Value != nil { - cfg.Servers.Set(name, convertServer(orig)) + if orig == nil { + continue } + cfg.Servers.Set(name, convertServer(orig)) } } func convertServer(ref *ServerRef) *asyncapi3.ServerRef { - result := &asyncapi3.ServerRef{Reference: dynamic.Reference{Ref: ref.Ref}} + result := &asyncapi3.ServerRef{Reference: dynamic.Reference[*asyncapi3.ServerRef]{Ref: ref.Ref}} if ref.Value != nil { target := &asyncapi3.Server{ @@ -440,7 +432,7 @@ func convertComponents(c *Components, config *asyncapi3.Config) (*asyncapi3.Comp target.Parameters = map[string]*asyncapi3.ParameterRef{} } if len(orig.Ref) > 0 { - target.Parameters[name] = &asyncapi3.ParameterRef{Reference: dynamic.Reference{Ref: orig.Ref}} + target.Parameters[name] = &asyncapi3.ParameterRef{Reference: dynamic.Reference[*asyncapi3.ParameterRef]{Ref: orig.Ref}} } if orig.Value != nil { p, err := convertParameter(name, orig.Value) @@ -455,12 +447,7 @@ func convertComponents(c *Components, config *asyncapi3.Config) (*asyncapi3.Comp if target.MessageTraits == nil { target.MessageTraits = map[string]*asyncapi3.MessageTraitRef{} } - if len(orig.Ref) > 0 { - target.MessageTraits[name] = &asyncapi3.MessageTraitRef{Reference: dynamic.Reference{Ref: orig.Ref}} - } - if orig.Value != nil { - target.MessageTraits[name] = convertMessageTrait(orig.Value) - } + target.MessageTraits[name] = convertMessageTrait(orig) } return target, nil diff --git a/config/dynamic/asyncApi/convert_test.go b/config/dynamic/asyncApi/convert_test.go index 7a77de1ab..92c868ac6 100644 --- a/config/dynamic/asyncApi/convert_test.go +++ b/config/dynamic/asyncApi/convert_test.go @@ -23,10 +23,11 @@ func TestConfig_Convert(t *testing.T) { err = yaml.Unmarshal(b, &cfg) require.NoError(t, err) - err = cfg.Parse(&dynamic.Config{Data: cfg}, &dynamictest.Reader{}) + c := &dynamic.Config{Data: cfg} + err = cfg.Parse(c, &dynamictest.Reader{}) require.NoError(t, err) - cfg3, err := cfg.Convert() + cfg3, err := c.Data.(*asyncApi.Config).Convert() require.NoError(t, err) require.Equal(t, "3.0.0", cfg3.Version) @@ -150,3 +151,70 @@ channels: require.Len(t, cfg.Operations["bar_send_publish"].Value.Messages, 1) require.Equal(t, cfg.Channels["bar"].Value.Messages["publish"], cfg.Operations["bar_send_publish"].Value.Messages[0]) } + +func TestConvert(t *testing.T) { + testcases := []struct { + name string + cfg *asyncApi.Config + test func(t *testing.T, config *asyncapi3.Config, err error) + }{ + { + name: "server ref", + cfg: &asyncApi.Config{ + Servers: map[string]*asyncApi.ServerRef{ + "foo": {Reference: dynamic.Reference[*asyncApi.ServerRef]{Ref: "#/components/servers/foo"}}, + }, + Components: &asyncApi.Components{ + Servers: map[string]*asyncApi.ServerRef{ + "foo": {Value: &asyncApi.Server{Url: "foo.bar"}}, + }, + }, + }, + test: func(t *testing.T, config *asyncapi3.Config, err error) { + require.Equal(t, 1, config.Servers.Len()) + s, _ := config.Servers.Get("foo") + require.NotNil(t, s) + require.NotNil(t, s.Value) + require.Equal(t, "foo.bar", s.Value.Host) + require.Equal(t, "#/components/servers/foo", s.Ref) + }, + }, + { + name: "channel ref", + cfg: &asyncApi.Config{ + Channels: map[string]*asyncApi.ChannelRef{ + "foo": {Reference: dynamic.Reference[*asyncApi.ChannelRef]{Ref: "#/components/channels/foo"}}, + }, + Components: &asyncApi.Components{ + Channels: map[string]*asyncApi.ChannelRef{ + "foo": {Value: &asyncApi.Channel{ + Description: "foo", + }}, + }, + }, + }, + test: func(t *testing.T, config *asyncapi3.Config, err error) { + require.Len(t, config.Channels, 1) + c := config.Channels["foo"] + require.NotNil(t, c) + require.NotNil(t, c.Value) + require.Equal(t, "foo", c.Value.Description) + require.Equal(t, "#/components/channels/foo", c.Ref) + }, + }, + } + + t.Parallel() + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + c := &dynamic.Config{Data: tc.cfg} + err := tc.cfg.Parse(c, &dynamictest.Reader{}) + require.NoError(t, err) + + cfg, err := tc.cfg.Convert() + tc.test(t, cfg, err) + }) + } +} diff --git a/config/dynamic/asyncApi/parsing.go b/config/dynamic/asyncApi/parsing.go index 4390cf703..dc93f500a 100644 --- a/config/dynamic/asyncApi/parsing.go +++ b/config/dynamic/asyncApi/parsing.go @@ -7,12 +7,16 @@ import ( ) func (c *Config) Parse(config *dynamic.Config, reader dynamic.Reader) error { + if c == nil { + return nil + } + for _, server := range c.Servers { if server == nil || len(server.Ref) == 0 { continue } - var resolved *ServerRef - if err := dynamic.Resolve(server.Ref, &resolved, config, reader); err != nil { + resolved, err := server.Resolve(config, reader) + if err != nil { return err } server.Value = resolved.Value @@ -32,8 +36,8 @@ func (c *Config) Parse(config *dynamic.Config, reader dynamic.Reader) error { func (r *ChannelRef) Parse(config *dynamic.Config, reader dynamic.Reader) error { if len(r.Ref) > 0 { - var resolved *ChannelRef - if err := dynamic.Resolve(r.Ref, &resolved, config, reader); err != nil { + resolved, err := r.Resolve(config, reader) + if err != nil { return err } r.Value = resolved.Value @@ -74,8 +78,8 @@ func (o *Operation) Parse(config *dynamic.Config, reader dynamic.Reader) error { func (r *MessageRef) Parse(config *dynamic.Config, reader dynamic.Reader) error { if len(r.Ref) > 0 { - var resolved *MessageRef - if err := dynamic.Resolve(r.Ref, &resolved, config, reader); err != nil { + resolved, err := r.Resolve(config, reader) + if err != nil { return err } r.Value = resolved.Value @@ -115,8 +119,8 @@ func (r *MessageRef) Parse(config *dynamic.Config, reader dynamic.Reader) error func (r *MessageTraitRef) parse(config *dynamic.Config, reader dynamic.Reader) error { if len(r.Ref) > 0 { - var resolved *MessageTraitRef - if err := dynamic.Resolve(r.Ref, &resolved, config, reader); err != nil { + resolved, err := r.Resolve(config, reader) + if err != nil { return err } r.Value = resolved.Value @@ -165,11 +169,12 @@ func (m *Message) applyTrait(trait *MessageTrait) { func (r *ParameterRef) Parse(config *dynamic.Config, reader dynamic.Reader) error { if len(r.Ref) > 0 { - var resolved *ParameterRef - if err := dynamic.Resolve(r.Ref, &resolved, config, reader); err != nil { + resolved, err := r.Resolve(config, reader) + if err != nil { return err } r.Value = resolved.Value + return nil } return nil } diff --git a/config/dynamic/asyncApi/parsing_test.go b/config/dynamic/asyncApi/parsing_test.go index d8d2251b1..c92015dcc 100644 --- a/config/dynamic/asyncApi/parsing_test.go +++ b/config/dynamic/asyncApi/parsing_test.go @@ -36,7 +36,7 @@ func TestServerResolve(t *testing.T) { name string cfg *asyncApi.Config read readFunc - test func(t *testing.T, cfg *asyncApi.Config, err error) + test func(t *testing.T, cfg *asyncapi3.Config, err error) }{ { name: "no error when server value is nil", @@ -44,7 +44,7 @@ func TestServerResolve(t *testing.T) { return nil }, cfg: &asyncApi.Config{Servers: map[string]*asyncApi.ServerRef{"foo": nil}}, - test: func(t *testing.T, cfg *asyncApi.Config, err error) { + test: func(t *testing.T, cfg *asyncapi3.Config, err error) { require.NoError(t, err) }, }, @@ -55,13 +55,15 @@ func TestServerResolve(t *testing.T) { }, cfg: &asyncApi.Config{ Servers: map[string]*asyncApi.ServerRef{ - "foo": {Ref: "#/components/servers/foo"}, + "foo": {Reference: dynamic.Reference[*asyncApi.ServerRef]{Ref: "#/components/servers/foo"}}, }, Components: &asyncApi.Components{Servers: map[string]*asyncApi.ServerRef{"foo": {Value: &asyncApi.Server{Description: "foo"}}}}, }, - test: func(t *testing.T, cfg *asyncApi.Config, err error) { + test: func(t *testing.T, cfg *asyncapi3.Config, err error) { require.NoError(t, err) - require.Equal(t, "foo", cfg.Servers["foo"].Value.Description) + s, ok := cfg.Servers.Get("foo") + require.True(t, ok) + require.Equal(t, "foo", s.Value.Description) }, }, { @@ -75,11 +77,13 @@ func TestServerResolve(t *testing.T) { return nil }, cfg: &asyncApi.Config{Servers: map[string]*asyncApi.ServerRef{ - "foo": {Ref: "foo.yml#/servers/foo"}, + "foo": {Reference: dynamic.Reference[*asyncApi.ServerRef]{Ref: "foo.yml#/servers/foo"}}, }}, - test: func(t *testing.T, cfg *asyncApi.Config, err error) { + test: func(t *testing.T, cfg *asyncapi3.Config, err error) { require.NoError(t, err) - require.Equal(t, "foo", cfg.Servers["foo"].Value.Description) + s, ok := cfg.Servers.Get("foo") + require.True(t, ok) + require.Equal(t, "foo", s.Value.Description) }, }, { @@ -91,11 +95,13 @@ func TestServerResolve(t *testing.T) { return nil }, cfg: &asyncApi.Config{Servers: map[string]*asyncApi.ServerRef{ - "foo": {Ref: "foo.yml#/components/servers/foo"}, + "foo": {Reference: dynamic.Reference[*asyncApi.ServerRef]{Ref: "foo.yml#/components/servers/foo"}}, }}, - test: func(t *testing.T, cfg *asyncApi.Config, err error) { + test: func(t *testing.T, cfg *asyncapi3.Config, err error) { require.NoError(t, err) - require.Equal(t, "foo", cfg.Servers["foo"].Value.Description) + s, ok := cfg.Servers.Get("foo") + require.True(t, ok) + require.Equal(t, "foo", s.Value.Description) }, }, { @@ -109,11 +115,13 @@ func TestServerResolve(t *testing.T) { return nil }, cfg: &asyncApi.Config{Servers: map[string]*asyncApi.ServerRef{ - "foo": {Ref: "foo.yml#/servers/foo"}, + "foo": {Reference: dynamic.Reference[*asyncApi.ServerRef]{Ref: "foo.yml#/servers/foo"}}, }}, - test: func(t *testing.T, cfg *asyncApi.Config, err error) { + test: func(t *testing.T, cfg *asyncapi3.Config, err error) { require.NoError(t, err) - require.Nil(t, cfg.Servers["foo"].Value) + s, ok := cfg.Servers.Get("foo") + require.True(t, ok) + require.Nil(t, s.Value) }, }, { @@ -122,10 +130,10 @@ func TestServerResolve(t *testing.T) { return fmt.Errorf("TEST ERROR") }, cfg: &asyncApi.Config{Servers: map[string]*asyncApi.ServerRef{ - "foo": {Ref: "foo.yml#/servers/foo"}, + "foo": {Reference: dynamic.Reference[*asyncApi.ServerRef]{Ref: "foo.yml#/servers/foo"}}, }}, - test: func(t *testing.T, cfg *asyncApi.Config, err error) { - require.EqualError(t, err, "resolve reference 'foo.yml#/servers/foo' failed: TEST ERROR") + test: func(t *testing.T, cfg *asyncapi3.Config, err error) { + require.EqualError(t, err, "resolve reference '/foo.yml#/servers/foo' failed: TEST ERROR") }, }, } @@ -136,8 +144,14 @@ func TestServerResolve(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() reader := &testReader{readFunc: tc.read} - err := tc.cfg.Parse(&dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: tc.cfg}, reader) - tc.test(t, tc.cfg, err) + c := &dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: tc.cfg} + err := tc.cfg.Parse(c, reader) + if err != nil { + tc.test(t, nil, err) + } else { + cfg, err := tc.cfg.Convert() + tc.test(t, cfg, err) + } }) } } @@ -147,7 +161,7 @@ func TestChannelResolve(t *testing.T) { name string cfg *asyncApi.Config read readFunc - test func(t *testing.T, cfg *asyncApi.Config, err error) + test func(t *testing.T, cfg *asyncapi3.Config, err error) }{ { name: "empty should not error", @@ -155,7 +169,7 @@ func TestChannelResolve(t *testing.T) { return nil }, cfg: &asyncApi.Config{}, - test: func(t *testing.T, cfg *asyncApi.Config, err error) { + test: func(t *testing.T, cfg *asyncapi3.Config, err error) { require.NoError(t, err) }, }, @@ -165,7 +179,7 @@ func TestChannelResolve(t *testing.T) { return nil }, cfg: &asyncApi.Config{Channels: map[string]*asyncApi.ChannelRef{"foo": nil}}, - test: func(t *testing.T, cfg *asyncApi.Config, err error) { + test: func(t *testing.T, cfg *asyncapi3.Config, err error) { require.NoError(t, err) }, }, @@ -181,9 +195,9 @@ func TestChannelResolve(t *testing.T) { return nil }, cfg: &asyncApi.Config{Channels: map[string]*asyncApi.ChannelRef{ - "foo": {Ref: "foo.yml#/channels/foo"}, + "foo": {Reference: dynamic.Reference[*asyncApi.ChannelRef]{Ref: "foo.yml#/channels/foo"}}, }}, - test: func(t *testing.T, cfg *asyncApi.Config, err error) { + test: func(t *testing.T, cfg *asyncapi3.Config, err error) { require.NoError(t, err) require.Equal(t, "reference", cfg.Channels["foo"].Value.Description) }, @@ -199,9 +213,9 @@ func TestChannelResolve(t *testing.T) { return nil }, cfg: &asyncApi.Config{Channels: map[string]*asyncApi.ChannelRef{ - "foo": {Ref: "foo.yml#/channels/foo"}, + "foo": {Reference: dynamic.Reference[*asyncApi.ChannelRef]{Ref: "foo.yml#/channels/foo"}}, }}, - test: func(t *testing.T, cfg *asyncApi.Config, err error) { + test: func(t *testing.T, cfg *asyncapi3.Config, err error) { require.NoError(t, err) require.Nil(t, cfg.Channels["foo"].Value) }, @@ -212,10 +226,10 @@ func TestChannelResolve(t *testing.T) { return fmt.Errorf("TEST ERROR") }, cfg: &asyncApi.Config{Channels: map[string]*asyncApi.ChannelRef{ - "foo": {Ref: "foo.yml#/channels/foo"}, + "foo": {Reference: dynamic.Reference[*asyncApi.ChannelRef]{Ref: "foo.yml#/channels/foo"}}, }}, - test: func(t *testing.T, cfg *asyncApi.Config, err error) { - require.EqualError(t, err, "resolve reference 'foo.yml#/channels/foo' failed: TEST ERROR") + test: func(t *testing.T, cfg *asyncapi3.Config, err error) { + require.EqualError(t, err, "resolve reference '/foo.yml#/channels/foo' failed: TEST ERROR") }, }, } @@ -226,8 +240,14 @@ func TestChannelResolve(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() reader := &testReader{readFunc: tc.read} - err := tc.cfg.Parse(&dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: tc.cfg}, reader) - tc.test(t, tc.cfg, err) + c := &dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: tc.cfg} + err := tc.cfg.Parse(c, reader) + if err != nil { + tc.test(t, nil, err) + } else { + cfg, err := tc.cfg.Convert() + tc.test(t, cfg, err) + } }) } } @@ -237,7 +257,7 @@ func TestMessage(t *testing.T) { name string cfg *asyncApi.Config read readFunc - test func(t *testing.T, cfg *asyncApi.Config, err error) + test func(t *testing.T, cfg *asyncapi3.Config, err error) }{ { name: "local subscribe message reference", @@ -245,13 +265,14 @@ func TestMessage(t *testing.T) { return nil }, cfg: &asyncApi.Config{Channels: map[string]*asyncApi.ChannelRef{ - "foo": {Value: &asyncApi.Channel{Subscribe: &asyncApi.Operation{Message: &asyncApi.MessageRef{Ref: "#/components/messages/foo"}}}}, + "foo": {Value: &asyncApi.Channel{Subscribe: &asyncApi.Operation{Message: &asyncApi.MessageRef{Reference: dynamic.Reference[*asyncApi.MessageRef]{Ref: "#/components/messages/foo"}}}}}, }, Components: &asyncApi.Components{ Messages: map[string]*asyncApi.MessageRef{"foo": {Value: &asyncApi.Message{Description: "foo"}}}, }}, - test: func(t *testing.T, cfg *asyncApi.Config, err error) { + test: func(t *testing.T, cfg *asyncapi3.Config, err error) { require.NoError(t, err) - require.Equal(t, "foo", cfg.Channels["foo"].Value.Subscribe.Message.Value.Description) + require.Contains(t, cfg.Channels["foo"].Value.Messages, "foo") + require.Equal(t, "foo", cfg.Channels["foo"].Value.Messages["foo"].Value.Description) }, }, { @@ -260,13 +281,13 @@ func TestMessage(t *testing.T) { return nil }, cfg: &asyncApi.Config{Channels: map[string]*asyncApi.ChannelRef{ - "foo": {Value: &asyncApi.Channel{Publish: &asyncApi.Operation{Message: &asyncApi.MessageRef{Ref: "#/components/messages/foo"}}}}, + "foo": {Value: &asyncApi.Channel{Publish: &asyncApi.Operation{Message: &asyncApi.MessageRef{Reference: dynamic.Reference[*asyncApi.MessageRef]{Ref: "#/components/messages/foo"}}}}}, }, Components: &asyncApi.Components{ Messages: map[string]*asyncApi.MessageRef{"foo": {Value: &asyncApi.Message{Description: "foo"}}}, }}, - test: func(t *testing.T, cfg *asyncApi.Config, err error) { + test: func(t *testing.T, cfg *asyncapi3.Config, err error) { require.NoError(t, err) - require.Equal(t, "foo", cfg.Channels["foo"].Value.Publish.Message.Value.Description) + require.Equal(t, "foo", cfg.Channels["foo"].Value.Messages["foo"].Value.Description) }, }, { @@ -280,11 +301,11 @@ func TestMessage(t *testing.T) { return nil }, cfg: &asyncApi.Config{Channels: map[string]*asyncApi.ChannelRef{ - "foo": {Value: &asyncApi.Channel{Subscribe: &asyncApi.Operation{Message: &asyncApi.MessageRef{Ref: "foo.yml#/components/messages/foo"}}}}, + "foo": {Value: &asyncApi.Channel{Subscribe: &asyncApi.Operation{Message: &asyncApi.MessageRef{Reference: dynamic.Reference[*asyncApi.MessageRef]{Ref: "foo.yml#/components/messages/foo"}}}}}, }}, - test: func(t *testing.T, cfg *asyncApi.Config, err error) { + test: func(t *testing.T, cfg *asyncapi3.Config, err error) { require.NoError(t, err) - require.Equal(t, "foo", cfg.Channels["foo"].Value.Subscribe.Message.Value.Description) + require.Equal(t, "foo", cfg.Channels["foo"].Value.Messages["foo"].Value.Description) }, }, { @@ -298,11 +319,11 @@ func TestMessage(t *testing.T) { return nil }, cfg: &asyncApi.Config{Channels: map[string]*asyncApi.ChannelRef{ - "foo": {Value: &asyncApi.Channel{Publish: &asyncApi.Operation{Message: &asyncApi.MessageRef{Ref: "foo.yml#/components/messages/foo"}}}}, + "foo": {Value: &asyncApi.Channel{Publish: &asyncApi.Operation{Message: &asyncApi.MessageRef{Reference: dynamic.Reference[*asyncApi.MessageRef]{Ref: "foo.yml#/components/messages/foo"}}}}}, }}, - test: func(t *testing.T, cfg *asyncApi.Config, err error) { + test: func(t *testing.T, cfg *asyncapi3.Config, err error) { require.NoError(t, err) - require.Equal(t, "foo", cfg.Channels["foo"].Value.Publish.Message.Value.Description) + require.Equal(t, "foo", cfg.Channels["foo"].Value.Messages["foo"].Value.Description) }, }, { @@ -311,10 +332,10 @@ func TestMessage(t *testing.T) { return fmt.Errorf("TEST ERROR") }, cfg: &asyncApi.Config{Channels: map[string]*asyncApi.ChannelRef{ - "foo": {Value: &asyncApi.Channel{Subscribe: &asyncApi.Operation{Message: &asyncApi.MessageRef{Ref: "foo.yml#/components/messages/foo"}}}}, + "foo": {Value: &asyncApi.Channel{Subscribe: &asyncApi.Operation{Message: &asyncApi.MessageRef{Reference: dynamic.Reference[*asyncApi.MessageRef]{Ref: "foo.yml#/components/messages/foo"}}}}}, }}, - test: func(t *testing.T, cfg *asyncApi.Config, err error) { - require.EqualError(t, err, "resolve reference 'foo.yml#/components/messages/foo' failed: TEST ERROR") + test: func(t *testing.T, cfg *asyncapi3.Config, err error) { + require.EqualError(t, err, "resolve reference '/foo.yml#/components/messages/foo' failed: TEST ERROR") }, }, { @@ -325,9 +346,9 @@ func TestMessage(t *testing.T) { "foo": {Value: &asyncApi.Channel{Publish: &asyncApi.Operation{Message: &asyncApi.MessageRef{Value: &asyncApi.Message{}}}}}, }, }, - test: func(t *testing.T, cfg *asyncApi.Config, err error) { + test: func(t *testing.T, cfg *asyncapi3.Config, err error) { require.NoError(t, err) - require.Equal(t, "text/plain", cfg.Channels["foo"].Value.Publish.Message.Value.ContentType) + require.Equal(t, "text/plain", cfg.Channels["foo"].Value.Messages["publish"].Value.ContentType) }, }, { @@ -337,9 +358,9 @@ func TestMessage(t *testing.T) { "foo": {Value: &asyncApi.Channel{Publish: &asyncApi.Operation{Message: &asyncApi.MessageRef{Value: &asyncApi.Message{}}}}}, }, }, - test: func(t *testing.T, cfg *asyncApi.Config, err error) { + test: func(t *testing.T, cfg *asyncapi3.Config, err error) { require.NoError(t, err) - require.Equal(t, "application/json", cfg.Channels["foo"].Value.Publish.Message.Value.ContentType) + require.Equal(t, "application/json", cfg.Channels["foo"].Value.Messages["publish"].Value.ContentType) }, }, } @@ -350,53 +371,16 @@ func TestMessage(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() reader := &testReader{readFunc: tc.read} - err := tc.cfg.Parse(&dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: tc.cfg}, reader) - tc.test(t, tc.cfg, err) + c := &dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: tc.cfg} + err := tc.cfg.Parse(c, reader) + if err != nil { + tc.test(t, nil, err) + } else { + cfg, err := tc.cfg.Convert() + tc.test(t, cfg, err) + } }) } - - t.Run("modify file", func(t *testing.T) { - target := &asyncApi.Message{ContentType: "application/json"} - reader := &testReader{readFunc: func(cfg *dynamic.Config) error { return nil }} - config := &asyncApi.Config{Channels: map[string]*asyncApi.ChannelRef{ - "foo": {Value: &asyncApi.Channel{Publish: &asyncApi.Operation{Message: &asyncApi.MessageRef{Ref: "#/components/messages/foo"}}}}, - }, Components: &asyncApi.Components{ - Messages: map[string]*asyncApi.MessageRef{"foo": {Value: &asyncApi.Message{}}}, - }} - file := &dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: config} - err := config.Parse(file, reader) - - // modify file - file.Data.(*asyncApi.Config).Components.Messages["foo"].Value = target - - require.NoError(t, err) - require.Equal(t, target, config.Channels["foo"].Value.Publish.Message.Value) - }) -} - -func TestModifyFileResolve(t *testing.T) { - target := &asyncApi.Channel{} - var fooConfig *dynamic.Config - reader := &testReader{readFunc: func(cfg *dynamic.Config) error { - require.Equal(t, "/foo.yml", cfg.Info.Url.String()) - config := &asyncApi.Config{Channels: map[string]*asyncApi.ChannelRef{ - "foo": {Value: &asyncApi.Channel{}}, - }} - cfg.Data = config - fooConfig = cfg - return nil - }} - config := &asyncApi.Config{Channels: map[string]*asyncApi.ChannelRef{ - "foo": {Ref: "foo.yml#/channels/foo"}, - }} - err := config.Parse(&dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: config}, reader) - require.NoError(t, err) - - fooConfig.Data.(*asyncApi.Config).Channels["foo"].Value = target - err = fooConfig.Data.(dynamic.Parser).Parse(fooConfig, reader) - - require.NoError(t, err) - require.Equal(t, target, config.Channels["foo"].Value) } func TestSchema(t *testing.T) { @@ -423,18 +407,20 @@ func TestSchema(t *testing.T) { schemas := map[string]*asyncapi3.SchemaRef{} schemas["foo"] = &asyncapi3.SchemaRef{Value: &asyncapi3.MultiSchemaFormat{Schema: &asyncapi3.SchemaRef{Value: target}}} config.Components = &asyncApi.Components{Schemas: schemas} - message.Payload = &asyncapi3.SchemaRef{Reference: dynamic.Reference{Ref: "#/components/schemas/foo"}} + message.Payload = &asyncapi3.SchemaRef{Reference: dynamic.Reference[*asyncapi3.SchemaRef]{Ref: "#/components/schemas/foo"}} reader := &testReader{readFunc: func(cfg *dynamic.Config) error { return nil }} - err := config.Parse(&dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: config}, reader) + c := &dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: config} + err := config.Parse(c, reader) require.NoError(t, err) - s, err := message.Payload.GetSchema() + cfg, err := config.Convert() + s, err := cfg.Channels["foo"].Value.Messages["publish"].Value.Payload.GetSchema() require.NoError(t, err) require.Equal(t, target, s) }) t.Run("file reference direct", func(t *testing.T) { target := &schema.Schema{} - message.Payload = &asyncapi3.SchemaRef{Reference: dynamic.Reference{Ref: "foo.yml"}} + message.Payload = &asyncapi3.SchemaRef{Reference: dynamic.Reference[*asyncapi3.SchemaRef]{Ref: "foo.yml"}} reader := &testReader{readFunc: func(cfg *dynamic.Config) error { cfg.Data = target return nil @@ -446,7 +432,7 @@ func TestSchema(t *testing.T) { }) t.Run("modify file reference direct", func(t *testing.T) { target := &schema.Schema{} - message.Payload = &asyncapi3.SchemaRef{Value: &asyncapi3.MultiSchemaFormat{Schema: &asyncapi3.SchemaRef{Reference: dynamic.Reference{Ref: "foo.yml"}}}} + message.Payload = &asyncapi3.SchemaRef{Value: &asyncapi3.MultiSchemaFormat{Schema: &asyncapi3.SchemaRef{Reference: dynamic.Reference[*asyncapi3.SchemaRef]{Ref: "foo.yml"}}}} reader := &testReader{readFunc: func(file *dynamic.Config) error { file.Data = target return nil diff --git a/config/dynamic/asyncApi/unmarshal_json_test.go b/config/dynamic/asyncApi/unmarshal_json_test.go index f553bdf9a..816d30603 100644 --- a/config/dynamic/asyncApi/unmarshal_json_test.go +++ b/config/dynamic/asyncApi/unmarshal_json_test.go @@ -2,10 +2,11 @@ package asyncApi_test import ( "encoding/json" - "github.com/stretchr/testify/require" "mokapi/config/dynamic" "mokapi/config/dynamic/asyncApi" "testing" + + "github.com/stretchr/testify/require" ) func Test_UnmarshalJSON(t *testing.T) { @@ -19,7 +20,7 @@ func Test_UnmarshalJSON(t *testing.T) { name: "ParameterRef $ref", input: `{"$ref":"foo"}`, target: &asyncApi.ParameterRef{}, - expected: &asyncApi.ParameterRef{Reference: dynamic.Reference{Ref: "foo"}}, + expected: &asyncApi.ParameterRef{Reference: dynamic.Reference[*asyncApi.ParameterRef]{Ref: "foo"}}, }, { name: "ParameterRef value", @@ -31,7 +32,7 @@ func Test_UnmarshalJSON(t *testing.T) { name: "MessageRef ref", input: `{"$ref":"foo"}`, target: &asyncApi.MessageRef{}, - expected: &asyncApi.MessageRef{Ref: "foo"}, + expected: &asyncApi.MessageRef{Reference: dynamic.Reference[*asyncApi.MessageRef]{Ref: "foo"}}, }, { name: "MessageRef value", @@ -43,7 +44,7 @@ func Test_UnmarshalJSON(t *testing.T) { name: "ChannelRef ref", input: `{"$ref":"foo"}`, target: &asyncApi.ChannelRef{}, - expected: &asyncApi.ChannelRef{Ref: "foo"}, + expected: &asyncApi.ChannelRef{Reference: dynamic.Reference[*asyncApi.ChannelRef]{Ref: "foo"}}, }, { name: "ChannelRef value", @@ -66,7 +67,7 @@ func Test_UnmarshalJSON(t *testing.T) { name: "ServerRef ref", input: `{"$ref":"foo"}`, target: &asyncApi.ServerRef{}, - expected: &asyncApi.ServerRef{Ref: "foo"}, + expected: &asyncApi.ServerRef{Reference: dynamic.Reference[*asyncApi.ServerRef]{Ref: "foo"}}, }, { name: "ServerRef value", diff --git a/config/dynamic/config.go b/config/dynamic/config.go index 64024fedf..3047806f5 100644 --- a/config/dynamic/config.go +++ b/config/dynamic/config.go @@ -75,6 +75,9 @@ func AddRef(parent, ref *Config) { if !added { return } + if parent.Info.Url == nil { + return + } checksum := ref.Info.Checksum ref.Listeners.Add(parent.Info.Url.String(), func(e ConfigEvent) { // event Create is used for reading first time diff --git a/config/dynamic/json.go b/config/dynamic/json.go index 92218e905..488497c0e 100644 --- a/config/dynamic/json.go +++ b/config/dynamic/json.go @@ -148,6 +148,9 @@ func object(d *decoder, v reflect.Value) error { continue } + i := v.Interface() + _ = i + offset = d.d.InputOffset() err = unmarshalJSON(d, field) if err != nil { diff --git a/config/dynamic/ref.go b/config/dynamic/ref.go index 16fcb70ea..6e274641c 100644 --- a/config/dynamic/ref.go +++ b/config/dynamic/ref.go @@ -2,16 +2,23 @@ package dynamic import ( "encoding/json" + "fmt" + "strings" + "gopkg.in/yaml.v3" ) -type Reference struct { - Ref string `yaml:"$ref" json:"$ref"` - Summary string `yaml:"summary" json:"summary"` - Description string `yaml:"description" json:"description"` +type Reference[T any] struct { + Ref string `yaml:"$ref,omitempty" json:"$ref,omitempty"` + DynamicRef string `yaml:"$dynamicRef,omitempty" json:"$dynamicRef,omitempty"` + + Summary string `yaml:"summary,omitempty" json:"summary,omitempty"` + Description string `yaml:"description,omitempty" json:"description,omitempty"` + + origin *Config } -func (r *Reference) UnmarshalYaml(node *yaml.Node, val interface{}) error { +func (r *Reference[T]) UnmarshalYaml(node *yaml.Node, val interface{}) error { err := node.Decode(r) if err == nil && len(r.Ref) > 0 { return nil @@ -20,7 +27,7 @@ func (r *Reference) UnmarshalYaml(node *yaml.Node, val interface{}) error { return node.Decode(val) } -func (r *Reference) UnmarshalJson(b []byte, val interface{}) error { +func (r *Reference[T]) UnmarshalJson(b []byte, val interface{}) error { var m map[string]string _ = json.Unmarshal(b, &m) if _, ok := m["$ref"]; ok { @@ -30,3 +37,41 @@ func (r *Reference) UnmarshalJson(b []byte, val interface{}) error { err := UnmarshalJSON(b, val) return err } + +func (r *Reference[T]) Parse(config *Config, _ Reader) error { + if r.Ref == "" || r.origin != nil { + return nil + } + r.origin = config + return nil +} + +func (r *Reference[T]) HasRef() bool { + return r.Ref != "" || r.DynamicRef != "" +} + +func (r *Reference[T]) Resolve(config *Config, reader Reader) (T, error) { + var err error + var result T + + if err := r.Parse(config, reader); err != nil { + return result, err + } + + if r.Ref != "" { + ref := r.Ref + if !strings.HasPrefix(ref, "#") { + u, err := resolveUrl(r.Ref, r.origin) + if err != nil { + return result, fmt.Errorf("resolve reference '%s' failed: %v", r.Ref, err) + } + ref = u.String() + } + + result, err = resolve[T](ref, config, reader) + return result, err + } + + result, err = ResolveDynamic[T](r.DynamicRef, config, reader) + return result, err +} diff --git a/config/dynamic/ref_test.go b/config/dynamic/ref_test.go index e08e1c33e..983765a8b 100644 --- a/config/dynamic/ref_test.go +++ b/config/dynamic/ref_test.go @@ -2,17 +2,18 @@ package dynamic_test import ( "encoding/json" - "github.com/stretchr/testify/require" - "gopkg.in/yaml.v3" "mokapi/config/dynamic" "testing" + + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" ) type foo struct { Foo string `yaml:"foo"` } type ref struct { - dynamic.Reference + dynamic.Reference[*ref] Value *foo } @@ -29,7 +30,7 @@ func TestReference_Unmarshal(t *testing.T) { err := json.Unmarshal([]byte(`{"$ref":"foo","summary":"summary","description":"description"}`), &r) require.NoError(t, err) require.Equal(t, &ref{ - Reference: dynamic.Reference{ + Reference: dynamic.Reference[*ref]{ Ref: "foo", Summary: "summary", Description: "description", @@ -59,7 +60,7 @@ description: description `), &r) require.NoError(t, err) require.Equal(t, &ref{ - Reference: dynamic.Reference{ + Reference: dynamic.Reference[*ref]{ Ref: "foo", Summary: "summary", Description: "description", diff --git a/config/dynamic/resolve.go b/config/dynamic/resolve.go index 4aab048a2..fb93f8167 100644 --- a/config/dynamic/resolve.go +++ b/config/dynamic/resolve.go @@ -16,34 +16,37 @@ type PathResolver interface { } type Converter interface { - ConvertTo(i interface{}) (interface{}, error) + ConvertTo(i any) (any, error) } -func Resolve(ref string, element interface{}, config *Config, reader Reader) error { +func resolve[T any](ref string, config *Config, reader Reader) (T, error) { var err error + var result T - fragment := ref[1:] + var fragment string isLocal := true parent := config - if !strings.HasPrefix(ref, "#") { - fragment, config, err = resolveResource(ref, element, config, reader) + if len(ref) > 0 && !strings.HasPrefix(ref, "#") { + fragment, config, err = resolveResource[T](ref, config, reader) if err != nil { - return fmt.Errorf("resolve reference '%v' failed: %w", ref, err) + return result, fmt.Errorf("resolve reference '%v' failed: %w", ref, err) } isLocal = false + } else if len(ref) > 0 { + fragment = ref[1:] } - err = resolveFragment(fragment, element, config, false) + result, err = resolveFragment[T](fragment, config, false) if err != nil { - return fmt.Errorf("resolve reference '%v' failed: %w", ref, err) + return result, fmt.Errorf("resolve reference '%v' failed: %w", ref, err) } // Parse the referenced schema again in the current context. // This ensures nested $ref and $dynamicRef are resolved relative // to the correct dynamic scope. - // element is **struct - p, ok := reflect.ValueOf(element).Elem().Interface().(Parser) + v := reflect.ValueOf(result) + p, ok := v.Interface().(Parser) if ok { if !isLocal { // set parent scope hierarchy @@ -51,39 +54,40 @@ func Resolve(ref string, element interface{}, config *Config, reader Reader) err config.Scope.SetParent(parent.Scope) } if !config.EnterRef(ref) { - return nil + return result, nil } defer config.LeaveRef(ref) err = p.Parse(config, reader) if err != nil { - return fmt.Errorf("resolve reference '%v' failed: %w", ref, err) + return result, fmt.Errorf("resolve reference '%v' failed: %w", ref, err) } } - return nil + return result, nil } -func ResolveDynamic(ref string, element interface{}, config *Config, reader Reader) error { +func ResolveDynamic[T any](ref string, config *Config, reader Reader) (T, error) { var err error + var result T fragment := ref[1:] if !strings.HasPrefix(ref, "#") { - fragment, config, err = resolveResource(ref, element, config, reader) + fragment, config, err = resolveResource[T](ref, config, reader) if err != nil { - return fmt.Errorf("resolve reference '%v' failed: %w", ref, err) + return result, fmt.Errorf("resolve reference '%v' failed: %w", ref, err) } } - err = resolveFragment(fragment, element, config, true) + result, err = resolveFragment[T](fragment, config, true) if err != nil { - return fmt.Errorf("resolve reference '%v' failed: %w", ref, err) + return result, fmt.Errorf("resolve reference '%v' failed: %w", ref, err) } - return nil + return result, nil } -func resolveFragment(fragment string, resolved interface{}, config *Config, dynamic bool) (err error) { +func resolveFragment[T any](fragment string, config *Config, dynamic bool) (result T, err error) { val := config.Data if fragment == "" { // resolve to current (root) element @@ -95,9 +99,53 @@ func resolveFragment(fragment string, resolved interface{}, config *Config, dyna val, err = config.Scope.GetLexical(fragment) } if err != nil { - return err + return } - return setResolved(resolved, val) + + result, err = convertTo[T](val) + return +} + +func convertTo[T any](val any) (T, error) { + if val == nil { + return *new(T), fmt.Errorf("value is null") + } + + if p, ok := val.(PathResolver); ok { + var err error + val, err = p.Resolve("") + if err != nil { + return *new(T), err + } + } + + val = convert[T](val) + + // types are identical + if v, ok := val.(T); ok { + return v, nil + } + + valType := reflect.TypeOf(val) + targetType := reflect.TypeOf((*T)(nil)).Elem() + + // val is pointer but T not + if valType != nil && valType.Kind() == reflect.Ptr && valType.Elem() == targetType { + v := reflect.ValueOf(val) + if !v.IsNil() { + return v.Elem().Interface().(T), nil + } + } + + // T is pointer but val not + if valType != nil && reflect.PointerTo(valType) == targetType { + vp := reflect.New(valType) + vp.Elem().Set(reflect.ValueOf(val)) + return vp.Interface().(T), nil + } + + var result T + return result, fmt.Errorf("expected type %T, got %T", result, val) } func get(token string, node interface{}) (interface{}, error) { @@ -204,6 +252,10 @@ func resolveUrl(ref string, cfg *Config) (*url.URL, error) { } info := cfg.Info.Kernel() + if info.Url == nil { + return u, nil + } + if len(info.Url.Opaque) > 0 { p := filepath.Join(filepath.Dir(info.Url.Opaque), u.Path) p = fmt.Sprintf("file:%v", p) @@ -211,9 +263,13 @@ func resolveUrl(ref string, cfg *Config) (*url.URL, error) { p = fmt.Sprintf("%v#%v", p, u.Fragment) } return url.Parse(p) - } else { - return info.Url.Parse(ref) } + + refURL := info.Url.ResolveReference(u) + if u.Fragment != "" { + refURL.Fragment = u.Fragment + } + return refURL, nil } func getId(v interface{}) string { @@ -242,7 +298,7 @@ func getId(v interface{}) string { return "" } -func resolveResource(ref string, element interface{}, config *Config, reader Reader) (string, *Config, error) { +func resolveResource[T any](ref string, config *Config, reader Reader) (string, *Config, error) { u, err := resolveUrl(ref, config) if err != nil { return "", nil, err @@ -253,7 +309,8 @@ func resolveResource(ref string, element interface{}, config *Config, reader Rea val := reflect.ValueOf(config.Data).Elem() data = reflect.New(val.Type()).Interface() } else { - data = reflect.ValueOf(element).Elem().Interface() + var result T + data = result } sub, err := reader.Read(removeFragment(u), data) @@ -263,51 +320,6 @@ func resolveResource(ref string, element interface{}, config *Config, reader Rea return u.Fragment, sub, err } -func setResolved(element interface{}, val interface{}) (err error) { - v := reflect.ValueOf(val) - vElement := reflect.Indirect(reflect.ValueOf(element)) - - if v.Kind() == reflect.Ptr { - v = v.Elem() - } - - if val == nil { - return fmt.Errorf("value is null") - } - - if r, ok := val.(PathResolver); ok { - if val, err = r.Resolve(""); err != nil { - return - } - } - - vCursor := reflect.ValueOf(val) - if reflect.Indirect(vCursor).Kind() == reflect.Map { - reflect.Indirect(reflect.ValueOf(element)).Set(reflect.Indirect(vCursor)) - return - } - - if !vCursor.Type().AssignableTo(vElement.Type()) && vCursor.Kind() == reflect.Ptr { - if c, ok := val.(Converter); ok { - if converted, err := c.ConvertTo(vElement.Interface()); err == nil { - vCursor = reflect.ValueOf(converted) - } else { - vCursor = vCursor.Elem() - } - } else { - vCursor = vCursor.Elem() - } - } - - if !vCursor.Type().AssignableTo(vElement.Type()) { - return fmt.Errorf("expected type %v, got %v", vElement.Type(), vCursor.Type()) - } - - vElement.Set(vCursor) - - return -} - func copyData(input interface{}) interface{} { val := reflect.ValueOf(input) @@ -324,3 +336,16 @@ func copyData(input interface{}) interface{} { return c.Interface() } + +func convert[T any](val any) any { + var target T + + if c, ok := val.(Converter); ok { + result, err := c.ConvertTo(target) + if err == nil { + return result + } + } + + return val +} diff --git a/config/dynamic/resolve_test.go b/config/dynamic/resolve_test.go index 0381ea22e..35b971f8b 100644 --- a/config/dynamic/resolve_test.go +++ b/config/dynamic/resolve_test.go @@ -18,7 +18,8 @@ func TestResolve(t *testing.T) { { name: "invalid ref", test: func(t *testing.T) { - err := dynamic.Resolve(":80", "", &dynamic.Config{}, &dynamictest.Reader{}) + r := dynamic.Reference[*ref]{Ref: ":80"} + _, err := r.Resolve(&dynamic.Config{}, &dynamictest.Reader{}) require.Error(t, err) require.EqualError(t, err, "resolve reference ':80' failed: parse \":80\": missing protocol scheme") }, @@ -30,9 +31,9 @@ func TestResolve(t *testing.T) { Foo string } s := v{Foo: "foo"} - var result v - err := dynamic.Resolve("#/", &result, &dynamic.Config{Data: s}, &dynamictest.Reader{}) + r := dynamic.Reference[v]{Ref: "#/"} + result, err := r.Resolve(&dynamic.Config{Data: s}, &dynamictest.Reader{}) require.NoError(t, err) require.Equal(t, s, result) @@ -44,9 +45,9 @@ func TestResolve(t *testing.T) { s := struct { Foo string }{Foo: "foo"} - result := "" - err := dynamic.Resolve("#/foo", &result, &dynamic.Config{Data: s}, &dynamictest.Reader{}) + r := dynamic.Reference[string]{Ref: "#/foo"} + result, err := r.Resolve(&dynamic.Config{Data: s}, &dynamictest.Reader{}) require.NoError(t, err) require.Equal(t, "foo", result) @@ -58,9 +59,9 @@ func TestResolve(t *testing.T) { s := struct { Foo map[string]string }{Foo: map[string]string{"bar": "bar"}} - result := "" - err := dynamic.Resolve("#/foo/bar", &result, &dynamic.Config{Data: s}, &dynamictest.Reader{}) + r := dynamic.Reference[string]{Ref: "#/foo/bar"} + result, err := r.Resolve(&dynamic.Config{Data: s}, &dynamictest.Reader{}) require.NoError(t, err) require.Equal(t, "bar", result) @@ -72,9 +73,9 @@ func TestResolve(t *testing.T) { s := struct { Foo map[string]string }{Foo: map[string]string{"/bar": "bar"}} - result := "" - err := dynamic.Resolve("#/foo/~1bar", &result, &dynamic.Config{Data: s}, &dynamictest.Reader{}) + r := dynamic.Reference[string]{Ref: "#/foo/~1bar"} + result, err := r.Resolve(&dynamic.Config{Data: s}, &dynamictest.Reader{}) require.NoError(t, err) require.Equal(t, "bar", result) @@ -86,9 +87,9 @@ func TestResolve(t *testing.T) { s := struct { Foo map[string]string }{Foo: map[string]string{"~bar": "bar"}} - result := "" - err := dynamic.Resolve("#/foo/~0bar", &result, &dynamic.Config{Data: s}, &dynamictest.Reader{}) + r := dynamic.Reference[string]{Ref: "#/foo/~0bar"} + result, err := r.Resolve(&dynamic.Config{Data: s}, &dynamictest.Reader{}) require.NoError(t, err) require.Equal(t, "bar", result) @@ -98,9 +99,9 @@ func TestResolve(t *testing.T) { name: "resolve local map entry not found", test: func(t *testing.T) { s := map[string]string{} - result := "" - err := dynamic.Resolve("#/foo", &result, &dynamic.Config{Data: s}, &dynamictest.Reader{}) + r := dynamic.Reference[string]{Ref: "#/foo"} + _, err := r.Resolve(&dynamic.Config{Data: s}, &dynamictest.Reader{}) require.Error(t, err) require.EqualError(t, err, "resolve reference '#/foo' failed: path element 'foo' not found") @@ -114,8 +115,8 @@ func TestResolve(t *testing.T) { } s := map[string]ref{"foo": {Value: "foo"}} - var resolved ref - err := dynamic.Resolve("#/foo", &resolved, &dynamic.Config{Data: s}, &dynamictest.Reader{}) + r := dynamic.Reference[ref]{Ref: "#/foo"} + resolved, err := r.Resolve(&dynamic.Config{Data: s}, &dynamictest.Reader{}) require.NoError(t, err) require.Equal(t, "foo", resolved.Value) @@ -130,9 +131,9 @@ func TestResolve(t *testing.T) { s := struct { Foo n }{Foo: n{Bar: "bar"}} - result := "" - err := dynamic.Resolve("#/foo/bar", &result, &dynamic.Config{Data: s}, &dynamictest.Reader{}) + r := dynamic.Reference[string]{Ref: "#/foo/bar"} + result, err := r.Resolve(&dynamic.Config{Data: s}, &dynamictest.Reader{}) require.NoError(t, err) require.Equal(t, "bar", result) @@ -148,9 +149,9 @@ func TestResolve(t *testing.T) { s := struct { Foo n }{Foo: n{Bar: &v}} - result := "" - err := dynamic.Resolve("#/foo/bar", &result, &dynamic.Config{Data: s}, &dynamictest.Reader{}) + r := dynamic.Reference[string]{Ref: "#/foo/bar"} + result, err := r.Resolve(&dynamic.Config{Data: s}, &dynamictest.Reader{}) require.NoError(t, err) require.Equal(t, "bar", result) @@ -160,9 +161,9 @@ func TestResolve(t *testing.T) { name: "resolve local struct field not found", test: func(t *testing.T) { s := struct{}{} - result := "" - err := dynamic.Resolve("#/foo", &result, &dynamic.Config{Data: s}, &dynamictest.Reader{}) + r := dynamic.Reference[string]{Ref: "#/foo"} + _, err := r.Resolve(&dynamic.Config{Data: s}, &dynamictest.Reader{}) require.Error(t, err) require.EqualError(t, err, "resolve reference '#/foo' failed: path element 'foo' not found") @@ -179,11 +180,11 @@ func TestResolve(t *testing.T) { Foo ref }{Foo: ref{Value: "foo"}} - var resolved ref - err := dynamic.Resolve("#/foo", &resolved, &dynamic.Config{Data: s}, &dynamictest.Reader{}) + r := dynamic.Reference[*ref]{Ref: "#/foo"} + result, err := r.Resolve(&dynamic.Config{Data: s}, &dynamictest.Reader{}) require.NoError(t, err) - require.Equal(t, "foo", resolved.Value) + require.Equal(t, "foo", result.Value) }, }, { @@ -197,10 +198,10 @@ func TestResolve(t *testing.T) { Foo ref }{Foo: ref{Value: nil}} - var resolved ref - err := dynamic.Resolve("#/foo", &resolved, &dynamic.Config{Data: s}, &dynamictest.Reader{}) + r := dynamic.Reference[*ref]{Ref: "#/foo"} + result, err := r.Resolve(&dynamic.Config{Data: s}, &dynamictest.Reader{}) require.NoError(t, err) - require.Nil(t, resolved.Value) + require.Nil(t, result.Value) }, }, { @@ -215,11 +216,11 @@ func TestResolve(t *testing.T) { Foo ref }{Foo: ref{Value: &v}} - var resolved ref - err := dynamic.Resolve("#/foo", &resolved, &dynamic.Config{Data: s}, &dynamictest.Reader{}) + r := dynamic.Reference[*ref]{Ref: "#/foo"} + result, err := r.Resolve(&dynamic.Config{Data: s}, &dynamictest.Reader{}) require.NoError(t, err) - require.Equal(t, "foo", *resolved.Value.(*string)) + require.Equal(t, "foo", *result.Value.(*string)) }, }, { @@ -231,9 +232,9 @@ func TestResolve(t *testing.T) { s := struct { Foo ref }{Foo: ref{Value: map[string]string{"bar": "bar"}}} - result := "" - err := dynamic.Resolve("#/foo/bar", &result, &dynamic.Config{Data: s}, &dynamictest.Reader{}) + r := dynamic.Reference[string]{Ref: "#/foo/bar"} + result, err := r.Resolve(&dynamic.Config{Data: s}, &dynamictest.Reader{}) require.NoError(t, err) require.Equal(t, "bar", result) @@ -245,9 +246,9 @@ func TestResolve(t *testing.T) { s := struct { Foo int }{Foo: 12} - var result float32 - err := dynamic.Resolve("#/foo", &result, &dynamic.Config{Data: s}, &dynamictest.Reader{}) + r := dynamic.Reference[float32]{Ref: "#/foo"} + _, err := r.Resolve(&dynamic.Config{Data: s}, &dynamictest.Reader{}) require.Error(t, err) require.EqualError(t, err, "resolve reference '#/foo' failed: expected type float32, got int") @@ -262,9 +263,9 @@ func TestResolve(t *testing.T) { s := struct { Foo *v }{Foo: &v{Value: "foo"}} - var result v - err := dynamic.Resolve("#/foo", &result, &dynamic.Config{Data: s}, &dynamictest.Reader{}) + r := dynamic.Reference[v]{Ref: "#/foo"} + result, err := r.Resolve(&dynamic.Config{Data: s}, &dynamictest.Reader{}) require.NoError(t, err) require.Equal(t, "foo", result.Value) @@ -276,9 +277,9 @@ func TestResolve(t *testing.T) { s := struct { Foo map[string]string }{Foo: map[string]string{"foo": "foo"}} - var result map[string]string - err := dynamic.Resolve("#/foo", &result, &dynamic.Config{Data: s}, &dynamictest.Reader{}) + r := dynamic.Reference[map[string]string]{Ref: "#/foo"} + result, err := r.Resolve(&dynamic.Config{Data: s}, &dynamictest.Reader{}) require.NoError(t, err) require.Contains(t, result, "foo") @@ -292,9 +293,9 @@ func TestResolve(t *testing.T) { }{Foo: &pathResolver{resolve: func(token string) (interface{}, error) { return "foo", nil }}} - var result string - err := dynamic.Resolve("#/foo", &result, &dynamic.Config{Data: s}, &dynamictest.Reader{}) + r := dynamic.Reference[string]{Ref: "#/foo"} + result, err := r.Resolve(&dynamic.Config{Data: s}, &dynamictest.Reader{}) require.NoError(t, err) require.Contains(t, result, "foo") @@ -308,9 +309,9 @@ func TestResolve(t *testing.T) { }{Foo: &pathResolver{resolve: func(token string) (interface{}, error) { return nil, fmt.Errorf("TEST ERROR") }}} - var result string - err := dynamic.Resolve("#/foo", &result, &dynamic.Config{Data: s}, &dynamictest.Reader{}) + r := dynamic.Reference[string]{Ref: "#/foo"} + _, err := r.Resolve(&dynamic.Config{Data: s}, &dynamictest.Reader{}) require.Error(t, err) require.EqualError(t, err, "resolve reference '#/foo' failed: TEST ERROR") @@ -325,9 +326,9 @@ func TestResolve(t *testing.T) { require.Equal(t, "bar", token) return "foo", nil }}} - var result string - err := dynamic.Resolve("#/foo/bar", &result, &dynamic.Config{Data: s}, &dynamictest.Reader{}) + r := dynamic.Reference[string]{Ref: "#/foo/bar"} + result, err := r.Resolve(&dynamic.Config{Data: s}, &dynamictest.Reader{}) require.NoError(t, err) require.Contains(t, result, "foo") @@ -341,14 +342,34 @@ func TestResolve(t *testing.T) { }{Foo: &pathResolver{resolve: func(token string) (interface{}, error) { return nil, fmt.Errorf("TEST ERROR") }}} - var result string - err := dynamic.Resolve("#/foo/bar", &result, &dynamic.Config{Data: s}, &dynamictest.Reader{}) + r := dynamic.Reference[string]{Ref: "#/foo/bar"} + _, err := r.Resolve(&dynamic.Config{Data: s}, &dynamictest.Reader{}) require.Error(t, err) require.EqualError(t, err, "resolve reference '#/foo/bar' failed: TEST ERROR") }, }, + { + name: "resolve local reference nested with Converter", + test: func(t *testing.T) { + s := struct { + Foo *converter + }{ + Foo: &converter{ + convert: func(interface{}) (interface{}, error) { + return "foo", nil + }, + }, + } + + r := dynamic.Reference[string]{Ref: "#/foo"} + result, err := r.Resolve(&dynamic.Config{Data: s}, &dynamictest.Reader{}) + + require.NoError(t, err) + require.Contains(t, result, "foo") + }, + }, { name: "resolve global reference", test: func(t *testing.T) { @@ -356,9 +377,9 @@ func TestResolve(t *testing.T) { require.Equal(t, "https://foo.bar", u.String()) return &dynamic.Config{Data: "foo"}, nil }) - result := "" - err := dynamic.Resolve("https://foo.bar", &result, &dynamic.Config{Info: dynamictest.NewConfigInfo()}, reader) + r := dynamic.Reference[string]{Ref: "https://foo.bar"} + result, err := r.Resolve(&dynamic.Config{Info: dynamictest.NewConfigInfo()}, reader) require.NoError(t, err) require.Equal(t, "foo", result) @@ -370,9 +391,9 @@ func TestResolve(t *testing.T) { reader := dynamictest.ReaderFunc(func(u *url.URL, v any) (*dynamic.Config, error) { return nil, fmt.Errorf("TESTING ERROR") }) - result := "" - err := dynamic.Resolve("https://foo.bar", &result, &dynamic.Config{Info: dynamictest.NewConfigInfo()}, reader) + r := dynamic.Reference[string]{Ref: "https://foo.bar"} + _, err := r.Resolve(&dynamic.Config{Info: dynamictest.NewConfigInfo()}, reader) require.Error(t, err) require.EqualError(t, err, "resolve reference 'https://foo.bar' failed: TESTING ERROR") @@ -388,10 +409,10 @@ func TestResolve(t *testing.T) { reader := dynamictest.ReaderFunc(func(u *url.URL, v any) (*dynamic.Config, error) { return &dynamic.Config{Data: value{Foo: "foo"}}, nil }) - result := "" - cfg := &dynamic.Config{Info: dynamictest.NewConfigInfo(), Data: &value{}} - err := dynamic.Resolve("https://foo.bar#/foo", &result, cfg, reader) + + r := dynamic.Reference[string]{Ref: "https://foo.bar#/foo"} + result, err := r.Resolve(cfg, reader) require.NoError(t, err) require.Equal(t, "foo", result) @@ -407,10 +428,10 @@ func TestResolve(t *testing.T) { reader := dynamictest.ReaderFunc(func(u *url.URL, v any) (*dynamic.Config, error) { return &dynamic.Config{Data: value{Foo: "foo"}}, nil }) - result := "" - cfg := &dynamic.Config{Info: dynamictest.NewConfigInfo(), Data: &value{}} - err := dynamic.Resolve("https://foo.bar#/bar", &result, cfg, reader) + + r := dynamic.Reference[string]{Ref: "https://foo.bar#/bar"} + _, err := r.Resolve(cfg, reader) require.Error(t, err) require.EqualError(t, err, "resolve reference 'https://foo.bar#/bar' failed: path element 'bar' not found") @@ -435,3 +456,11 @@ type pathResolver struct { func (p *pathResolver) Resolve(token string) (interface{}, error) { return p.resolve(token) } + +type converter struct { + convert func(i interface{}) (interface{}, error) +} + +func (c *converter) ConvertTo(i interface{}) (interface{}, error) { + return c.convert(i) +} diff --git a/go.mod b/go.mod index 8825d7e92..9a52f7d01 100644 --- a/go.mod +++ b/go.mod @@ -5,24 +5,24 @@ go 1.25.5 require ( github.com/Masterminds/sprig v2.22.0+incompatible github.com/blevesearch/bleve/v2 v2.5.7 - github.com/blevesearch/bleve_index_api v1.3.6 + github.com/blevesearch/bleve_index_api v1.3.10 github.com/bradleyfalzon/ghinstallation/v2 v2.18.0 github.com/brianvoe/gofakeit/v6 v6.28.0 github.com/dop251/goja v0.0.0-20250309171923-bcd7cc6bf64c - github.com/evanw/esbuild v0.27.4 + github.com/evanw/esbuild v0.28.0 github.com/fsnotify/fsnotify v1.9.0 github.com/go-co-op/gocron v1.37.0 github.com/go-git/go-git/v5 v5.18.0 github.com/golang-jwt/jwt/v4 v4.5.2 github.com/google/uuid v1.6.0 github.com/jinzhu/inflection v1.0.0 - github.com/modelcontextprotocol/go-sdk v1.4.1 + github.com/modelcontextprotocol/go-sdk v1.5.0 github.com/pkg/errors v0.9.1 github.com/sirupsen/logrus v1.9.4 github.com/stretchr/testify v1.11.1 - github.com/yuin/gopher-lua v1.1.1 - golang.org/x/net v0.52.0 - golang.org/x/text v0.35.0 + github.com/yuin/gopher-lua v1.1.2 + golang.org/x/net v0.53.0 + golang.org/x/text v0.36.0 gopkg.in/go-asn1-ber/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d gopkg.in/yaml.v3 v3.0.1 layeh.com/gopher-luar v1.0.11 @@ -85,9 +85,9 @@ require ( github.com/yosida95/uritemplate/v3 v3.0.2 // indirect go.etcd.io/bbolt v1.4.0 // indirect go.uber.org/atomic v1.9.0 // indirect - golang.org/x/crypto v0.49.0 // indirect - golang.org/x/oauth2 v0.34.0 // indirect - golang.org/x/sys v0.42.0 // indirect + golang.org/x/crypto v0.50.0 // indirect + golang.org/x/oauth2 v0.35.0 // indirect + golang.org/x/sys v0.43.0 // indirect google.golang.org/protobuf v1.36.6 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect ) diff --git a/go.sum b/go.sum index 686a8b67a..5fc4bf025 100644 --- a/go.sum +++ b/go.sum @@ -24,8 +24,8 @@ github.com/bits-and-blooms/bitset v1.22.0 h1:Tquv9S8+SGaS3EhyA+up3FXzmkhxPGjQQCk github.com/bits-and-blooms/bitset v1.22.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/blevesearch/bleve/v2 v2.5.7 h1:2d9YrL5zrX5EBBW++GOaEKjE+NPWeZGaX77IM26m1Z8= github.com/blevesearch/bleve/v2 v2.5.7/go.mod h1:yj0NlS7ocGC4VOSAedqDDMktdh2935v2CSWOCDMHdSA= -github.com/blevesearch/bleve_index_api v1.3.6 h1:Cp67NjekrlHh2KTDGpKM0hqMnBTBIBJVQVtEEzDnIaI= -github.com/blevesearch/bleve_index_api v1.3.6/go.mod h1:xvd48t5XMeeioWQ5/jZvgLrV98flT2rdvEJ3l/ki4Ko= +github.com/blevesearch/bleve_index_api v1.3.10 h1:a7G+IOMa2xuO6f8vtutbTsqjVLpLuCuH3uoTZHkGiYg= +github.com/blevesearch/bleve_index_api v1.3.10/go.mod h1:xvd48t5XMeeioWQ5/jZvgLrV98flT2rdvEJ3l/ki4Ko= github.com/blevesearch/geo v0.2.4 h1:ECIGQhw+QALCZaDcogRTNSJYQXRtC8/m8IKiA706cqk= github.com/blevesearch/geo v0.2.4/go.mod h1:K56Q33AzXt2YExVHGObtmRSFYZKYGv0JEN5mdacJJR8= github.com/blevesearch/go-faiss v1.0.26 h1:4dRLolFgjPyjkaXwff4NfbZFdE/dfywbzDqporeQvXI= @@ -81,8 +81,8 @@ github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= -github.com/evanw/esbuild v0.27.4 h1:8opEixKkH9EDsdjxC/aPmpk1KPwQOcyknDo5m5xIFxI= -github.com/evanw/esbuild v0.27.4/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48= +github.com/evanw/esbuild v0.28.0 h1:V96ghtc5p5JnNUQIUsc5H3kr+AcFcMqOJll2ZmJW6Lo= +github.com/evanw/esbuild v0.28.0/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= @@ -101,8 +101,8 @@ github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyL github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= -github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= -github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= +github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= @@ -148,8 +148,8 @@ github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMK github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY= github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= -github.com/modelcontextprotocol/go-sdk v1.4.1 h1:M4x9GyIPj+HoIlHNGpK2hq5o3BFhC+78PkEaldQRphc= -github.com/modelcontextprotocol/go-sdk v1.4.1/go.mod h1:Bo/mS87hPQqHSRkMv4dQq1XCu6zv4INdXnFZabkNU6s= +github.com/modelcontextprotocol/go-sdk v1.5.0 h1:CHU0FIX9kpueNkxuYtfYQn1Z0slhFzBZuq+x6IiblIU= +github.com/modelcontextprotocol/go-sdk v1.5.0/go.mod h1:gggDIhoemhWs3BGkGwd1umzEXCEMMvAnhTrnbXJKKKA= github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM= github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw= github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= @@ -195,22 +195,22 @@ github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= github.com/yuin/gopher-lua v0.0.0-20190206043414-8bfc7677f583/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ= -github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= -github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= +github.com/yuin/gopher-lua v1.1.2 h1:yF/FjE3hD65tBbt0VXLE13HWS9h34fdzJmrWRXwobGA= +github.com/yuin/gopher-lua v1.1.2/go.mod h1:7aRmXIWl37SqRf0koeyylBEzJ+aPt8A+mmkQ4f1ntR8= go.etcd.io/bbolt v1.4.0 h1:TU77id3TnN/zKr7CO/uk+fBCwF2jGcMuw2B/FMAzYIk= go.etcd.io/bbolt v1.4.0/go.mod h1:AsD+OCi/qPN1giOX1aiLAha3o1U8rAz65bvN4j0sRuk= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= -golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= +golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= +golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= -golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= -golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= -golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= +golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= +golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ= +golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -221,17 +221,17 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= -golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= +golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= -golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= +golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY= +golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= -golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= +golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= +golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= -golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= +golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= +golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/imap/idle_test.go b/imap/idle_test.go index 5be684545..7b758d9ec 100644 --- a/imap/idle_test.go +++ b/imap/idle_test.go @@ -176,7 +176,8 @@ func TestIdle(t *testing.T) { } } -func TestSendUpdatesWhileIdle(t *testing.T) { +// Test is not running stable on GitHub +func _TestSendUpdatesWhileIdle(t *testing.T) { p := try.GetFreePort() sent := make(chan bool) s := &imap.Server{ @@ -240,9 +241,9 @@ func TestSendUpdatesWhileIdle(t *testing.T) { require.NoError(t, err) require.Equal(t, "* 1 EXPUNGE", res) - res, err = c.SendRaw("A01 FINISHED") + res, err = c.SendRaw("DONE") require.NoError(t, err) - require.Equal(t, "A01 BAD Expected DONE to end IDLE", res) + require.Equal(t, "A01 OK IDLE terminated", res) } func TestIdle_DisconnectWithoutDone(t *testing.T) { diff --git a/mcp/data/automation-core.md b/mcp/data/automation-core.md new file mode 100644 index 000000000..7e35fc17c --- /dev/null +++ b/mcp/data/automation-core.md @@ -0,0 +1,204 @@ +# Core Discovery API + +Access to the global mokapi object for infrastructure metadata. + +```typescript +/** + * GLOBAL: The 'mokapi' object is globally available in the Automation API. + * Use it directly in your script. + */ +declare const mokapi: Mokapi; + +interface Mokapi { + /** + * Returns all mocked APIs (lightweight, no schemas). + */ + getApis(): ApiSummary[]; + + /** + * Returns a specific API by name. + * @example + * getApi('Swagger Petstore') + */ + getApi(name: string): OpenApi | Kafka; + + /** + * Generate a random value from a JSON Schema. + * The generated data strictly matches the schema, including all required fields and correct types. + * Use this function to create complex random data or writing HTTP mock scripts + * @param schema The JSON scheme for which a random value is generated. + * @example + * fake({ type: 'string', format: 'email' }) + */ + fake(schema: Schema): any; + + /** + * Returns recorded events from Mokapi + * Use this function when the user asks: + * - What requests were made? + * - Why did my request fail? + * - Show recent API activity + * @param traits Filter events by traits + * @param limit Maximum number of events to return, default is 10 + * @example + * getEvents({ apiType: 'http', name: 'Swagger Petstore', path: '/pets' }) + */ + getEvents(traits: HttpTraits | KafkaTraits, limit?: number): Event[]; +} + +type ApiType = 'http' | 'kafka' | 'ldap' | 'mail' + +interface ApiSummary { + name: string; + type: ApiType; +} + +/** + * JSON Schema defines a JSON-based format for describing the structure of JSON data + * @example + * { + * "type": "string", + * "format": "email" + * } + */ +interface Schema { + /** + * Specifies the data type for a schema. + */ + type?: SchemaType | SchemaType[]; + + /** + * The enum keyword is used to restrict a value to a fixed set of values. + */ + enum?: any[]; + + /** + * The const keyword is used to restrict a value to a single value. + */ + const?: any; + + /** + * Contains a list of valid examples. + */ + examples?: any[]; + + /** + * Specifies a default value. + */ + default?: any; + + // Numbers + /** + * Restricts the number to a multiple of the given number + */ + multipleOf?: number; + + /** + * Restricts the number to a maximum number + */ + maximum?: number; + + /** + * Restricts the number to an exclusive maximum number + */ + exclusiveMaximum?: number; + + /** + * Restricts the number to a minimum number + */ + minimum?: number; + + /** + * Restricts the number to an exclusive minimum number + */ + exclusiveMinimum?: number; + + // Strings + /** + * Restricts the string to a maximum length + */ + maxLength?: number; + + /** + * Restricts the string to a minimum length + */ + minLength?: number; + + /** + * The pattern keyword is used to restrict a string to a particular regular expression. + */ + pattern?: string; + + /** + * The format keyword allows for basic semantic identification of certain kinds of string values that are commonly used. + */ + format?: string; + + // Arrays + /** + * Specifies the schema of the items in the array. + */ + items?: Schema; + + /** + * Restricts the array to have a maximum length + */ + maxItems?: number; + + /** + * Restricts the array to have a minimum length + */ + minItems?: number; + + /** + * Restricts the array to have unique items + */ + uniqueItems?: boolean; + + // Objects + /** + * Specifies the properties of an object + */ + properties?: { [name: string]: Schema }; + + /** + * Restricts the object to have a maximum of properties + */ + maxProperties?: number; + + /** + * Restricts the object to have a minimum of properties + */ + minProperties?: number; + + /** + * Specifies the required properties for an object + */ + required?: string[]; + + /** + * The additionalProperties keyword is used to control the handling of extra stuff, + * that is, properties whose names are not listed in the properties keyword or match + * any of the regular expressions in the patternProperties keyword. By default, any + * additional properties are allowed. + */ + additionalProperties?: boolean | Schema; + + /** + * A value must be valid against all the schemas + */ + allOf?: Schema[]; + + /** + * A value must be valid against any the schemas + */ + anyOf?: Schema[]; + + /** + * A value must be valid against exactly one the schemas + */ + oneOf?: Schema[]; +} + +type SchemaType = "object" | "array" | "number" | "integer" | "string" | "boolean" | "null"; +``` \ No newline at end of file diff --git a/mcp/data/automation-event.md b/mcp/data/automation-event.md new file mode 100644 index 000000000..8059710bd --- /dev/null +++ b/mcp/data/automation-event.md @@ -0,0 +1,67 @@ +# Events + +Definitions for debugging activity via getEvents() and traits. + +```typescript +interface Traits { + type?: ApiType; + name?: string; +} + +interface HttpTraits extends Traits { + /** + * Path value specified by the OpenAPI path + * @example /pet/{petId} + */ + path?: string + + /** + * Request method. + * @example GET + */ + method?: string +} + +interface KafkaTraits extends Traits { + /** + * Topic name specified by the AsyncAPI channel + * @example user.signedup + */ + topic?: string + + /** + * Partition in which message was written + */ + partition?: number + + /** + * Which client produced the message + * The value 'mokapi-script' indicates the message was produced by a Mokapi Script + * The value 'mokapi-mcp' indicates the messages was produced by MCP server + */ + clientId?: string +} + +interface Event { + /** + * ID of the event + */ + id: string; + + /** + * List of traits + */ + traits: Traits; + + /** + * The data of the event + */ + data: any + + /** + * Time of the event in the format RFC3339 + * @example 2026-07-21T17:32:28Z + */ + time: string +} +``` \ No newline at end of file diff --git a/mcp/data/automation-http.md b/mcp/data/automation-http.md new file mode 100644 index 000000000..6f901244f --- /dev/null +++ b/mcp/data/automation-http.md @@ -0,0 +1,90 @@ +# HTTP (OpenAPI) + +Interfaces for exploring HTTP endpoints, parameters, and status codes. + +```typescript +interface Http extends ApiSummary { + servers: { url: string, description: string } + + /** + * Returns all operations of this API. + */ + getOperations(): HttpOperationSummary[]; + + /** + * Returns details about specific operation + * Use id from the operation summary list + * @param id The id of the operation + */ + getOperation(id: string): HttpOperation +} + +interface HttpOperationSummary { + /** generated from method and path if missing in spec */ + id: string + method: string; + path: string; + summary: string; + /** Names of required parameters to help decide if this is the right endpoint */ + parameters?: string[] +} + +interface HttpOperation { + id: string; + path: string + method: string + summary: string + description: string; + parameters: HttpRequestParameter[] + requestBody: HttpRequestBody + /** + * List of allowed responses. + * IMPORTANT: You must only use these status codes for this operation! + */ + responses: HttpResponse[] + + /** + * Invoke this operation against the mocked API. + * @example operation.invoke({ path: { id: 1 }, body: JSON.stringify({ name: "test" }) }) + */ + invoke(request?: HttpInvokeRequest): HttpInvokeResponse; +} + +interface HttpRequestParameter { + name: string + in: 'path' | 'query' | 'headers' + required: boolean + schema: Schema + description?: string +} + +interface HttpRequestBody { + description: string + required: boolean + contents: Content[] +} + +interface HttpContent { + contentType: string + schema: Schema +} + +interface HttpResponse { + statusCode: number + description: string + contents: HttpContent[] +} + +interface HttpInvokeRequest { + path?: Record; + query?: Record; + header?: Record; + body?: string; +} + +interface HttpInvokeResponse { + statusCode: number; + headers: Record; + body: string +} +``` \ No newline at end of file diff --git a/mcp/data/automation-kafka.md b/mcp/data/automation-kafka.md new file mode 100644 index 000000000..46e75d23b --- /dev/null +++ b/mcp/data/automation-kafka.md @@ -0,0 +1,114 @@ +# Kafka (AsyncAPI) + +Interfaces for inspecting topics, partitions, and message history. + +```typescript +interface Kafka extends ApiSummary { + brokers: { name: string, host: string, description?: string } + + getTopics(): KafkaTopicSummary[] + getTopic(topicName: string): KafkaTopic +} + +interface KafkaTopicSummary { + /** + * The unique name of the topic. + */ + name: string + title?: string + summary?: string +} + +interface KafkaTopic extends KafkaTopicSummary { + description: string + /** + * List of current partitions and their maximum offsets. + * Use this to determine the range for the 'consume' method. + * The 'offset' represents the NEXT available position. + * (e.g., if offset is 10, messages 0-9 exist; the next message will be 10). + */ + partitions: { index: number, offset: number } + + operations: KafkaOperation[]; + + /** + * Use 'produce' to send a message to this topic. + * Check 'operations' with action 'send' for valid payloads. + * @param partition The target partition index. MUST be one of the indices listed in the 'partitions' array. + * @param value The message payload. If the operation specifies a JSON schema, provide this as a stringified object. + * @param key Optional message key. + * @param headers Optional metadata headers. + */ + produce(partition: number, value: string, key?: string, headers?: KafkaHeader): void + + /** + * INSPECT: Retrieves a specific record for analysis or verification. + * Use this to check if the mock has received or produced a specific message. + * @param partition The partition index (see 'partitions' list). + * @param startOffset The offset to start reading from (see 'partitions' for max offset). + * @param limit The maximum number of records to return in this call. + * @returns An array of records found starting from the startOffset. + */ + consume(partition: number, startOffset: number, limit: number): KafkaRecord[] +} + +interface KafkaOperation { + action: 'send' | 'receive' + title: string + summary: string; + description: string + messages: KafkaMessage[]; +} + +interface KafkaMessage { + name: string; + title: string; + summary: string + description: string + contentType: string + payload: Schema; + key: Schema + headers?: Schema; +} + +interface KafkaHeader { + [name: string]: string +} + +interface KafkaRecord { + offset: number + key: string + value: string + headers: KafkaHeader +} +``` + +## Examples + +Read the very last existing message from a specific partition. Since offset is the NEXT position, we read at offset - 1. +```typescript +const kafka = mokapi.getApi("..."); +const topic = kafka.getTopic("..."); +const p0 = topic.partitions.find(p => p.index === 0); +let lastRecord = null +// Check if partition exists and contains at least one message +if (p0 && p0.offset > 0) { + lastRecord = topic.consume(0, p0.offset - 1, 1); +} +lastRecord +``` + +Write a message +```typescript +const kafka = mokapi.getApi("..."); +const topic = kafka.getTopic("..."); +topic.produce(0, JSON.stringify({ foo: "123" }), "key-123"); +``` + +Determine last offset. The last offset represents the NEXT available position. +```typescript +const kafka = mokapi.getApi("Kafka Server"); +const topic = kafka.getTopic("topic-name"); +const partition = topic.partitions.find(p => p.index === 0); +partition.offset; +``` \ No newline at end of file diff --git a/mcp/data/automation.md b/mcp/data/automation.md new file mode 100644 index 000000000..9be899ad3 --- /dev/null +++ b/mcp/data/automation.md @@ -0,0 +1,42 @@ +# Mokapi Automation API + +Technical reference for exploring the current environment, APIs, and Kafka clusters. + +## Category + +To see detailed TypeScript interfaces, call this tool with one of the following values for the category parameter: + +| Category | Description | Parameter Name | +|--------------------|-------------------------------------------------------------------------|-----------------| +| Core Discovery API | Access to the global mokapi object for infrastructure metadata. | `core` | +| HTTP (OpenAPI) | Interfaces for exploring HTTP endpoints, parameters, and status codes. | `http` | +| Kafka (AsyncAPI) | Interfaces for inspecting topics, partitions, and message history. | `kafka` | +| Events | Definitions for debugging activity via getEvents() and traits. | `event` | + +## Examples + +Get all available APIs +```typescript +// The last expression is returned as the tool result +mokapi.getApis() +``` + +Get all available Kafka APIs +```typescript +mokapi.getApis().filter(x => x.type === 'kafka') +``` + +Get specific API to understand the contract +```typescript +mokapi.getApi('API_NAME') +``` + +Get all recorded events from Mokapi, if user asks "Why did my request fail?" +```typescript +mokapi.getEvents() +``` + +Get latest HTTP events for a specific API +```typescript +mokapi.getEvents({ type: 'http', name: 'Petstore' }) +``` \ No newline at end of file diff --git a/mcp/data/conditional-response.md b/mcp/data/conditional-response.md new file mode 100644 index 000000000..497843849 --- /dev/null +++ b/mcp/data/conditional-response.md @@ -0,0 +1,68 @@ +# Scenario conditional-response + +HTTP mock handler for terminals. +Demonstrates how to: +- Access request parameters +- Apply custom logic (e.g., lookup, filtering, updates) + +IMPORTANT +Strict Specification Enforcement: +Mokapi will throw an error if you use a status code NOT defined in the specification. +Always verify the available status codes for each operation before calling response.rebuild() or setting response.statusCode. + +```typescript +import { on } from "mokapi" + +interface Terminal { + id: string + compartments: { + id: string + doorState: 'open' | 'closed' + }[] +} + +const terminals: Terminal[] = [] + +export default function () { + on('http', (request, response) => { + switch(request.key) { + case '/terminals/{id}': { + const terminal = terminals.find(x => x.id === request.path.id) + if (!terminal) { + // CHECK SPEC: Is 404 defined for this path and HTTP method? + response.rebuild(404) + response.data = { error: 'terminal not found' } + return + } + + if (request.method === 'GET') { + response.data = terminal + } else if (request.method === 'POST') { + // update the terminal + Object.assign(terminal, request.body) + // mokapi already set the success response, nothing to do + } + // do not raise an error if different method is used, + // maybe there is another event handler in a different file defined + return + } + case '/terminals': { + if (request.method === 'GET') { + response.data = terminals + } else if (request.method === 'POST') { + const terminal = terminals.find(x => x.id === request.path.id) + if (terminal) { + // console output will be displayed in the Mokapi's' dashboard + console.log('terminal already exists', request.body) + // CHECK SPEC: Is 400 defined for this path and HTTP method? + response.rebuild(400) + } else { + terminals.push(request.body) + } + } + return + } + } + }) +} +``` \ No newline at end of file diff --git a/mcp/data/delay-latency.md b/mcp/data/delay-latency.md new file mode 100644 index 000000000..58ad5e69b --- /dev/null +++ b/mcp/data/delay-latency.md @@ -0,0 +1,30 @@ +# Scenario delay-latency + +Simulate server latency by delaying the response. Useful to test scenarios: +- frontend loading states +- timeouts +- high-load + +```typescript +import { on } from "mokapi" + +let pets = [ + { id: 1, name: 'Fluffy', status: 'available', category: { id: 1, name: 'Dogs' }, photoUrls: [], tags: [] }, + { id: 3, name: 'Hedgie', status: 'pending', category: { id: 2, name: 'Small Animals' }, photoUrls: [], tags: [] } +]; + +export default function () { + on('http', async (request, response) => { + switch(request.key) { + case '/pets': { + if (request.method !== 'GET') return + + // simulate network latency (e.g., 2 seconds) + sleep('2s') + + response.data = pets + } + } + }) +} +``` \ No newline at end of file diff --git a/mcp/data/dynamic-error-simulation.md b/mcp/data/dynamic-error-simulation.md new file mode 100644 index 000000000..180f749e1 --- /dev/null +++ b/mcp/data/dynamic-error-simulation.md @@ -0,0 +1,45 @@ +# Scenario dynamic-error-simulation + +Return error responses based on runtime conditions, such as missing resources, validation failures, or conflicting state. + +```typescript +import { on } from "mokapi" + +const hotels = [] + +export default function () { + on('http', (request, response) => { + switch(request.key) { + case '/bookings': { + const hotel = hotels.find(x => x.code === request.body?.hotel?.code) + + if (!hotel) { + console.log('hotel not found') + response.rebuild(404) + response.data = { error: 'hotel not found' } + return + } + + // simulate dynamic errors based on hotel simulation config + const type = hotel.simulation?.responseType + switch (type) { + case 'bad-request': + response.rebuild(400) + return + case 'unauthorized': + response.rebuild(401) + return + case 'forbidden': + response.rebuild(403) + return + case 'internal-server-error': + response.rebuild(500) + return + } + // success path: generate valid response + // ... + } + } + }) +} +``` \ No newline at end of file diff --git a/mcp/data/dynamic-path-params.md b/mcp/data/dynamic-path-params.md new file mode 100644 index 000000000..7d1e0b162 --- /dev/null +++ b/mcp/data/dynamic-path-params.md @@ -0,0 +1,33 @@ +# Scenario dynamic-path-params + +HTTP mock handler to get a pet stored in an array list. +Demonstrates how to: +- Access request parameters +- Apply custom logic (e.g., lookup, filtering) + +```typescript +import { on } from "mokapi" + +let pets = [ + { id: 1, name: 'Fluffy', status: 'available', category: { id: 1, name: 'Dogs' }, photoUrls: [], tags: [] }, + { id: 3, name: 'Hedgie', status: 'pending', category: { id: 2, name: 'Small Animals' }, photoUrls: [], tags: [] } +]; + +export default function () { + on('http', async (request, response) => { + switch(request.key) { + case '/pets/{id}': + if (request.method !== 'GET') { + return + } + const pet = pets.find(x => x.id === request.path.id) + if (pet) { + response.data = pet + } else { + console.log('pet not found', request) + response.rebuild(404) + } + } + }) +} +``` \ No newline at end of file diff --git a/mcp/data/forward-request-to-real-backend.md b/mcp/data/forward-request-to-real-backend.md new file mode 100644 index 000000000..5fef825eb --- /dev/null +++ b/mcp/data/forward-request-to-real-backend.md @@ -0,0 +1,70 @@ +# Scenario forward-request-to-real-backend + +Stop API drift in its tracks. Use Mokapi as a validation layer to enforce OpenAPI contracts between +clients and backends, regardless of who's calling or what they're building. This scenario forwards +incoming requests to real backend services while validating both requests and responses against the +OpenAPI specification. + +```typescript +import { on } from 'mokapi'; +import { fetch } from 'mokapi/http'; + +export default async function () { + + on('http', async (request, response) => { + + // Map request to backend URL based on OpenAPI spec name + const url = getForwardUrl(request) + + // If no URL could be determined, return an error immediately + if (!url) { + response.statusCode = 500; + response.body = 'Failed to forward request: unknown backend'; + return; + } + + try { + // Forward the request to the backend + const res = await fetch(url, { + method: request.method, + body: request.body, + headers: request.header, + timeout: '30s' + }); + + // Copy status code and headers + response.statusCode = res.statusCode; + response.headers = res.headers + + // Check the content type to decide whether to validate the response + const contentType = res.headers['Content-Type']?.[0] || ''; + + if (contentType.includes('application/json')) { + // Mokapi can validate JSON responses automatically + response.data = res.json(); + } else { + // For other content types, skip validation + response.body = res.body; + } + + } catch (e) { + // Handle any errors that occur while forwarding + response.statusCode = 500; + response.body = e.toString(); + } + }); + + function getForwardUrl(request: HttpRequest): string | undefined { + switch (request.api) { + case 'backend-1': { + return 'https://backend1.example.com' + request.url.path + '?' + request.url.query; + } + case 'backend-2': { + return 'https://backend2.example.com' + request.url.path + '?' + request.url.query; + } + default: + return undefined; + } + } +} +``` \ No newline at end of file diff --git a/mcp/data/mock_reference.md b/mcp/data/mock_reference.md new file mode 100644 index 000000000..775b1aaa2 --- /dev/null +++ b/mcp/data/mock_reference.md @@ -0,0 +1,36 @@ +# Mock Reference + +Retrieves technical reference material for writing mock scripts. +You can request either 'types' (API definitions) or 'scenarios' (example blueprints). + +To read a specific definition or scenario, call this tool again +with the corresponding category and name (e.g., category='types', name='http') + +## Types + +Overview of TypeScript definitions for mock event handlers. +- Use these types to ensure correct syntax for "import { ... } from 'mokapi/...'". + +| Type | Description | Import Statement | Parameter Name | +|--------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------|----------------------| +| **Core** | Exposes the core scripting API for Mokapi. It allows you to intercept and manipulate protocol events (HTTP, Kafka, LDAP, SMTP), schedule jobs, generate mock data, and share state between scripts. | `import {...} from 'mokapi'` | mokapi | +| **HTTP** | Exposes functions to invoke HTTP request | `import {...} from 'mokapi/http'` | http | +| **Kafka** | Exposes functions to produce Kafka message | `import {...} from 'mokapi/kafka'` | kafka | +| **Faker** | Exposes functions to generate random data based on JSON schema | `import {...} from 'mokapi/faker'` | faker | +| **Mustache** | Exposes to render output based on mustache template | `import {...} from 'mokapi/mustache'` | mustache | +| **Yaml** | Exposes functions to parse or stringify YAML files | `import {...} from 'mokapi/yaml'` | yaml | + +## Scenarios + +A list of code examples and boilerplates. +Use these to understand how to implement specific use cases like latency, error simulation or conditional behavior. +Always check a scenario before writing a script from scratch to ensure you follow best practices. + +| Scenario | Description | +|---------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| dynamic-path-params | Access and use path parameters (e.g., /pets/{petId}) to retrieve or process specific resources based on request.path values. | +| conditional-response | Return different responses based on request data (path, query, headers, or body), such as selecting resources, updating state, or handling different HTTP methods. | +| static-error-simulation | Return predefined error responses (e.g., 400, 404, 500) for specific endpoints or conditions without dynamic logic. | +| dynamic-error-simulation | Return error responses based on runtime conditions, such as missing resources, validation failures, or conflicting state. | +| delay-latency | Simulate network latency or slow backend processing by delaying the response before returning data or errors. | +| forward-request-to-real-backend | Stop API drift in its tracks. Use Mokapi as a validation layer to enforce OpenAPI contracts between clients and backends, regardless of who's calling or what they're building. This scenario forwards incoming requests to real backend services while validating both requests and responses against the OpenAPI specification. | \ No newline at end of file diff --git a/mcp/data/mocking-types.md b/mcp/data/mocking-types.md new file mode 100644 index 000000000..e30ce14ca --- /dev/null +++ b/mcp/data/mocking-types.md @@ -0,0 +1,16 @@ +# Available Mocking Libraries + +Overview of TypeScript definitions for mock event handlers. +Provides URIs to technical definitions (d.ts) for 'mokapi', 'http', 'kafka', etc. +- Use these types to ensure correct syntax for "import { ... } from 'mokapi/...'". +- Refer to mokapi://lib/mocking/scenarios for boilerplate examples and usage patterns. + Mandatory for generating valid mock scripts. + +| Library | Resource URI | Import Statement | +|--------------|---------------------------------------|---------------------------------------| +| **Core** | `mokapi://lib/mocking/types/mokapi` | `import {...} from 'mokapi'` | +| **HTTP** | `mokapi://lib/mocking/types/http` | `import {...} from 'mokapi/http'` | +| **Kafka** | `mokapi://lib/mocking/types/kafka` | `import {...} from 'mokapi/kafka'` | +| **Faker** | `mokapi://lib/mocking/types/faker` | `import {...} from 'mokapi/faker'` | +| **Mustache** | `mokapi://lib/mocking/types/mustache` | `import {...} from 'mokapi/mustache'` | +| **Yaml** | `mokapi://lib/mocking/types/yaml` | `import {...} from 'mokapi/yaml'` | \ No newline at end of file diff --git a/mcp/data/scenarios.md b/mcp/data/scenarios.md new file mode 100644 index 000000000..2406fd7b5 --- /dev/null +++ b/mcp/data/scenarios.md @@ -0,0 +1,14 @@ +# Available Mocking Scenarios + +A directory of ready-to-use code examples and boilerplates. +Use these to understand how to implement specific use cases like latency, error simulation or conditional behavior. +Always check a scenario before writing a script from scratch to ensure you follow best practices. + +| Scenario | Scenario URI | Description | +|----------------------------------|------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| dynamic-path-params | `mokapi://lib/mocking/scenarios/dynamic-path-params` | Access and use path parameters (e.g., /pets/{petId}) to retrieve or process specific resources based on request.path values. | +| conditional-response | `mokapi://lib/mocking/scenarios/conditional-response` | Return different responses based on request data (path, query, headers, or body), such as selecting resources, updating state, or handling different HTTP methods. | +| static-error-simulation | `mokapi://lib/mocking/scenarios/static-error-simulation` | Return predefined error responses (e.g., 400, 404, 500) for specific endpoints or conditions without dynamic logic. | +| dynamic-error-simulation | `mokapi://lib/mocking/scenarios/dynamic-error-simulation` | Return error responses based on runtime conditions, such as missing resources, validation failures, or conflicting state. | +| delay-latency | `mokapi://lib/mocking/scenarios/delay-latency` | Simulate network latency or slow backend processing by delaying the response before returning data or errors. | +| forward-request-to-real-backend | `mokapi://lib/mocking/scenarios/forward-request-to-real-backend` | Stop API drift in its tracks. Use Mokapi as a validation layer to enforce OpenAPI contracts between clients and backends, regardless of who's calling or what they're building. This scenario forwards incoming requests to real backend services while validating both requests and responses against the OpenAPI specification. | \ No newline at end of file diff --git a/mcp/data/static-error-simulation.md b/mcp/data/static-error-simulation.md new file mode 100644 index 000000000..7a5d27b80 --- /dev/null +++ b/mcp/data/static-error-simulation.md @@ -0,0 +1,34 @@ +# Scenario static-error-simulation + +Return predefined error responses (e.g., 400, 404, 500) for specific endpoints or conditions without dynamic logic. + +```typescript +import { on } from "mokapi" + +export default function () { + on('http', (request, response) => { + switch(request.key) { + case '/bookings': { + if (request.method === 'POST') { + if (request.header['Api-Key'] === 'invalid') { + // console output will be displayed in the Mokapi's' dashboard + console.log('api-key is not valid') + response.rebuild(401) + return + } + if (request.body?.hotel?.code === 'NOT_FOUND') { + console.log('hotel not found') + response.rebuild(404) + return + } + if (request.body.hotel.name === 'INVALID') { + console.log('hotel name is not valid') + response.rebuild(400) + return + } + } + } + } + }) +} +``` \ No newline at end of file diff --git a/mcp/generate_http_mock_response.go b/mcp/generate_http_mock_response.go index 78a8aac1a..fc7fc91bd 100644 --- a/mcp/generate_http_mock_response.go +++ b/mcp/generate_http_mock_response.go @@ -32,7 +32,7 @@ func (s *Service) registerGenerateHttpMockResponseTool(server *mcp.Server) { "properties": map[string]any{ "apiName": map[string]any{ "type": "string", - "description": "The exact name of the API as returned by 'get_api_list'", + "description": "The exact name of the API as returned by 'mokapi_get_api_spec'", }, "path": map[string]any{ "type": "string", diff --git a/mcp/get_api_spec.go b/mcp/get_api_spec.go index 9c5b6a271..3f455d8df 100644 --- a/mcp/get_api_spec.go +++ b/mcp/get_api_spec.go @@ -20,7 +20,7 @@ type GetApiSpecOutput struct { type ApiSpec struct { Name string `json:"name"` Type string `json:"type"` - Spec any `json:"spec"` + Spec any `json:"spec,omitempty"` } func (s *Service) registerGetSpecTool(server *mcp.Server) { @@ -60,10 +60,11 @@ func (s *Service) registerGetSpecTool(server *mcp.Server) { "enum": []string{"http", "kafka", "ldap", "mail"}, }, "spec": map[string]any{ - "type": "any", + "type": "object", "description": "The specification of the API (e.g. OpenAPI or AsyncAPI", }, }, + "required": []any{"name", "type"}, }, }, }, @@ -96,7 +97,7 @@ func (s *Service) GetApiSpec(_ context.Context, in GetApiSpecInput) (GetApiSpecO if in.Type == "http" || len(in.Type) == 0 { for _, api := range s.app.ListHttp() { if api.Info.Name == "" { - log.Warnf("mcp tool get_api_list: skip empty HTTTP API name") + log.Warnf("mcp tool mokapi_get_api_spec: skip empty HTTTP API name") continue } result = append(result, ApiSpec{ @@ -109,7 +110,7 @@ func (s *Service) GetApiSpec(_ context.Context, in GetApiSpecInput) (GetApiSpecO if in.Type == "kafka" || len(in.Type) == 0 { for _, api := range s.app.Kafka.List() { if api.Info.Name == "" { - log.Warnf("mcp tool get_api_list: skip empty Kafka API name") + log.Warnf("mcp tool mokapi_get_api_spec: skip empty Kafka API name") continue } result = append(result, ApiSpec{ @@ -122,7 +123,7 @@ func (s *Service) GetApiSpec(_ context.Context, in GetApiSpecInput) (GetApiSpecO if in.Type == "ldap" || len(in.Type) == 0 { for _, api := range s.app.Ldap.List() { if api.Info.Name == "" { - log.Warnf("mcp tool get_api_list: skip empty LDAP API name") + log.Warnf("mcp tool mokapi_get_api_spec: skip empty LDAP API name") continue } result = append(result, ApiSpec{ @@ -135,7 +136,7 @@ func (s *Service) GetApiSpec(_ context.Context, in GetApiSpecInput) (GetApiSpecO if in.Type == "mail" || len(in.Type) == 0 { for _, api := range s.app.Mail.List() { if api.Info.Name == "" { - log.Warnf("mcp tool get_api_list: skip empty Mail API name") + log.Warnf("mcp tool mokapi_get_api_spec: skip empty Mail API name") continue } result = append(result, ApiSpec{ diff --git a/mcp/get_automation_definitions.go b/mcp/get_automation_definitions.go new file mode 100644 index 000000000..fa1259545 --- /dev/null +++ b/mcp/get_automation_definitions.go @@ -0,0 +1,76 @@ +package mcp + +import ( + "context" + _ "embed" + + "github.com/modelcontextprotocol/go-sdk/mcp" +) + +//go:embed data/automation.md +var automation string + +//go:embed data/automation-core.md +var automationCore string + +//go:embed data/automation-http.md +var automationHttp string + +//go:embed data/automation-kafka.md +var automationKafka string + +//go:embed data/automation-event.md +var automationEvent string + +type AutomationDefinitionsInput struct { + Category string `json:"category"` +} + +type AutomationDefinitionsOutput struct { + Text string `json:"text"` +} + +func (s *Service) registerGetAutomationDefinitions(server *mcp.Server) { + inputSchema := map[string]any{ + "type": "object", + "properties": map[string]any{ + "category": map[string]any{ + "type": "string", + "description": "The category of type definition to retrieve. If omitted, a general overview is returned.", + "enum": []string{"core", "http", "kafka", "event"}, + }, + }, + "required": []string{}, + } + + registerTool(server, &mcp.Tool{ + Name: "mokapi_get_automation_definitions", + Description: `Returns the required TypeScript definitions ONLY for the Mokapi Automation API (tool mokapi_execute_code). + +MANDATORY: Call this tool BEFORE using 'mokapi_execute_code'. If you are unsure which category you need, call this tool without any parameters to receive a general overview of all available categories. +- Learn how to query API specifications (OpenAPI, AsyncAPI) +- Access methods for inspecting live logs and events +- Get correct syntax for the global 'mokapi' object + +This ensures your generated code is valid and uses the correct library methods.`, + InputSchema: inputSchema, + }, s.GetAutomationDefinitions) +} + +func (s *Service) GetAutomationDefinitions(_ context.Context, in AutomationDefinitionsInput) (AutomationDefinitionsOutput, error) { + var text string + switch in.Category { + case "core": + text = automationCore + case "http": + text = automationHttp + case "kafka": + text = automationKafka + case "event": + text = automationEvent + default: + text = automation + } + + return AutomationDefinitionsOutput{Text: text}, nil +} diff --git a/mcp/get_http_response_schema.go b/mcp/get_http_response_schema.go index 11e616e3f..7173c3448 100644 --- a/mcp/get_http_response_schema.go +++ b/mcp/get_http_response_schema.go @@ -22,7 +22,7 @@ func (s *Service) registerGetHttpResponseSchemaTool(server *mcp.Server) { "properties": map[string]any{ "apiName": map[string]any{ "type": "string", - "description": "The exact name of the API as returned by 'get_api_list'", + "description": "The exact name of the API as returned by 'mokapi_get_api_spec'", }, "path": map[string]any{ "type": "string", diff --git a/mcp/get_mock_reference.go b/mcp/get_mock_reference.go new file mode 100644 index 000000000..fd9d67f57 --- /dev/null +++ b/mcp/get_mock_reference.go @@ -0,0 +1,87 @@ +package mcp + +import ( + "context" + _ "embed" + "fmt" + + "github.com/modelcontextprotocol/go-sdk/mcp" +) + +//go:embed data/mock_reference.md +var mockReference string + +type GetMockReferenceInput struct { + Category string `json:"category"` + Name string `json:"name"` +} + +type GetMockReferenceOutput struct { + Category string `json:"category"` + Name string `json:"name"` + Text string +} + +func (s *Service) registerGetMockReference(server *mcp.Server) { + inputSchema := map[string]any{ + "type": "object", + "properties": map[string]any{ + "category": map[string]any{ + "type": "string", + "description": "The category of reference material to retrieve.", + "enum": []string{"types", "scenarios"}, + }, + "name": map[string]interface{}{ + "type": "string", + "description": "The specific library or scenario name (e.g., 'http', 'kafka', 'delay-latency').", + "enum": []string{ + // Types + "mokapi", "http", "kafka", "faker", "mustache", "yaml", + // Scenarios + "dynamic-path-params", + "conditional-response", + "static-error-simulation", + "dynamic-error-simulation", + "delay-latency", + "forward-request-to-real-backend", + }, + }, + }, + "required": []string{}, + } + + registerTool(server, &mcp.Tool{ + Name: "mokapi_get_mock_reference", + Description: `Retrieves technical reference material for writing mock scripts. +You can request either 'types' (API definitions) or 'scenarios' (example blueprints). + +Use this tool to: +- See how to import and use 'mokapi', 'mokapi/http', or 'mokapi/kafka'. +- Get boilerplate code for specific use cases (e.g., 'rest-auth'). + +MANDATORY: Use this before generating a new mock script to ensure correct syntax. +Do NOT use this reference material with tool mokapi_execute_code +`, + InputSchema: inputSchema, + }, s.GetMockReference) +} + +func (s *Service) GetMockReference(_ context.Context, in *GetMockReferenceInput) (GetMockReferenceOutput, error) { + if in.Name == "" || in.Category == "" { + return GetMockReferenceOutput{Text: mockReference}, nil + } + + if in.Category == "types" { + text, ok := mockTypes[in.Name] + if !ok { + return GetMockReferenceOutput{}, fmt.Errorf("mock reference type not found: %s", in.Name) + } + return GetMockReferenceOutput{Text: text, Category: in.Category, Name: in.Name}, nil + } + + text, ok := scenarios[in.Name] + if !ok { + return GetMockReferenceOutput{}, fmt.Errorf("mock reference scenario not found: %s", in.Name) + } + return GetMockReferenceOutput{Text: text, Category: in.Category, Name: in.Name}, nil +} diff --git a/mcp/get_mokapi_typescript_api.go b/mcp/get_mokapi_typescript_api.go index fd64162b5..6bd876e5b 100644 --- a/mcp/get_mokapi_typescript_api.go +++ b/mcp/get_mokapi_typescript_api.go @@ -2,6 +2,8 @@ package mcp import ( "context" + _ "embed" + "strings" "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -20,7 +22,13 @@ type Package struct { Types string `json:"types"` } +func extractName(uri string) string { + parts := strings.Split(uri, "/") + return parts[len(parts)-1] +} + func (s *Service) registerGetMokapiTypeScriptApi(server *mcp.Server) { + inputSchema := map[string]any{ "type": "object", "properties": map[string]any{ diff --git a/mcp/list_apis.go b/mcp/list_apis.go index dc40f0368..4082352eb 100644 --- a/mcp/list_apis.go +++ b/mcp/list_apis.go @@ -1,5 +1,6 @@ package mcp +/* import ( "context" @@ -58,7 +59,7 @@ func (s *Service) registerListApiTool(server *mcp.Server) { registerTool(server, &mcp.Tool{ Name: "get_api_list", - Description: `Returns all available APIs with their name and type. + Description: `Returns all available APIs with their name and type. Use this to discover APIs before calling 'get_api_spec' to retrieve detailed specifications.`, InputSchema: inputSchema, OutputSchema: outputSchema, @@ -122,3 +123,4 @@ func (s *Service) ListApis(_ context.Context, in ListApisInput) (*ListApiRespons return &ListApiResponse{Apis: result}, nil } +*/ diff --git a/mcp/produce_kafka_message.go b/mcp/produce_kafka_message.go index da25da8bc..af51ba1e8 100644 --- a/mcp/produce_kafka_message.go +++ b/mcp/produce_kafka_message.go @@ -29,7 +29,7 @@ func (s *Service) registerProduceKafkaMessage(server *mcp.Server) { "properties": map[string]any{ "apiName": map[string]any{ "type": "string", - "description": "The name of the Kafka API as returned by 'get_api_list'", + "description": "The name of the Kafka API as returned by 'mokapi_get_api_spec'", }, "topic": map[string]any{ "type": "string", diff --git a/mcp/resources.go b/mcp/resources.go new file mode 100644 index 000000000..1c0aedb9f --- /dev/null +++ b/mcp/resources.go @@ -0,0 +1,154 @@ +package mcp + +import ( + "context" + _ "embed" + "mokapi/npm/go-mokapi/types" + + "github.com/modelcontextprotocol/go-sdk/mcp" +) + +//go:embed data/mocking-types.md +var overview string + +var mockTypes = map[string]string{ + "mokapi": types.Mokapi, + "faker": types.Faker, + "http": types.Http, + "kafka": types.Kafka, + "mustache": types.Mustache, + "yaml": types.Yaml, +} + +func addResources(server *mcp.Server) { + server.AddResource(&mcp.Resource{ + URI: "mokapi://lib/automation", + Name: "Automation API Reference", + Description: "Types for querying Mokapi specs, logs, and events via Code Mode", + }, func(ctx context.Context, request *mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) { + return &mcp.ReadResourceResult{ + Contents: []*mcp.ResourceContents{ + { + URI: "mokapi://lib/automation", + MIMEType: "application/typescript", + Text: automation, + }, + }, + }, nil + }) + + server.AddResource(&mcp.Resource{ + URI: "mokapi://lib/mocking/types", + Name: "Mokapi Script API Overview", + Description: `Overview of TypeScript definitions for mock event handlers. +Provides URIs to technical definitions (d.ts) for 'mokapi', 'http', 'kafka', etc. +- Use these types to ensure correct syntax for 'import { ... } from "mokapi/..."'. +- Refer to mokapi://lib/mocking/scenarios for boilerplate examples and usage patterns. +Mandatory for generating valid mock scripts.`, + }, func(ctx context.Context, request *mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) { + return &mcp.ReadResourceResult{ + Contents: []*mcp.ResourceContents{ + { + URI: "mokapi://lib/mocking/types", + MIMEType: "text/markdown", + Text: overview, + }, + }, + }, nil + }) + + server.AddResourceTemplate(&mcp.ResourceTemplate{ + URITemplate: "mokapi://lib/mocking/types/{name}", + Name: "Mocking Script API Reference", + Description: `Types for writing event handlers and mock logic inside Mokapi. +Use mokapi://lib/mocking/types to get an overview of all available APIs`, + }, func(ctx context.Context, request *mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) { + name := extractName(request.Params.URI) + text, ok := mockTypes[name] + if !ok { + return nil, mcp.ResourceNotFoundError(request.Params.URI) + } + return &mcp.ReadResourceResult{ + Contents: []*mcp.ResourceContents{ + { + URI: "mokapi://lib/mocking/types/" + name, + MIMEType: "application/typescript", + Text: text, + }, + }, + }, nil + }) + + addMockScenarios(server) +} + +//go:embed data/scenarios.md +var scenarioOverview string + +//go:embed data/dynamic-path-params.md +var dynamicPathParams string + +//go:embed data/conditional-response.md +var conditionalResponse string + +//go:embed data/static-error-simulation.md +var staticErrorSimulation string + +//go:embed data/dynamic-error-simulation.md +var dynamicErrorSimulation string + +//go:embed data/delay-latency.md +var delayLatency string + +//go:embed data/forward-request-to-real-backend.md +var forwardRequestToRealBackend string + +var scenarios = map[string]string{ + "dynamic-path-params": dynamicPathParams, + "conditional-response": conditionalResponse, + "static-error-simulation": staticErrorSimulation, + "dynamic-error-simulation": dynamicErrorSimulation, + "delay-latency": delayLatency, + "forward-request-to-real-backend": forwardRequestToRealBackend, +} + +func addMockScenarios(server *mcp.Server) { + server.AddResource(&mcp.Resource{ + URI: "mokapi://lib/mocking/scenarios", + Name: "Mokapi Script Blueprints & Scenarios", + Description: `A directory of ready-to-use code examples and boilerplates. +Use these to understand how to implement specific use cases like latency, error simulation or conditional behavior. +Always check a scenario before writing a script from scratch to ensure you follow best practices.`, + }, func(ctx context.Context, request *mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) { + return &mcp.ReadResourceResult{ + Contents: []*mcp.ResourceContents{ + { + URI: "mokapi://lib/mocking/scenarios", + MIMEType: "text/markdown", + Text: scenarioOverview, + }, + }, + }, nil + }) + + server.AddResourceTemplate(&mcp.ResourceTemplate{ + URITemplate: "mokapi://lib/mocking/scenarios/{name}", + Name: "Mocking Scenario Detail", + Description: "Full script template for a specific scenario. Use mokapi://lib/mocking/scenarios get an overview of all scenarios", + }, func(ctx context.Context, request *mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) { + name := extractName(request.Params.URI) + text, ok := scenarios[name] + if !ok { + return nil, mcp.ResourceNotFoundError(request.Params.URI) + } + return &mcp.ReadResourceResult{ + Contents: []*mcp.ResourceContents{ + { + URI: "mokapi://lib/mocking/scenarios/" + name, + MIMEType: "text/markdown", + Text: text, + }, + }, + }, nil + }) +} diff --git a/mcp/run.go b/mcp/run.go new file mode 100644 index 000000000..3479b7847 --- /dev/null +++ b/mcp/run.go @@ -0,0 +1,218 @@ +package mcp + +import ( + "context" + _ "embed" + "errors" + "fmt" + "mokapi/js/compiler" + "mokapi/js/faker" + "mokapi/providers/openapi" + "mokapi/runtime" + "mokapi/schema/json/generator" + "reflect" + "slices" + "strings" + + "github.com/dop251/goja" + "github.com/modelcontextprotocol/go-sdk/mcp" + log "github.com/sirupsen/logrus" +) + +const errorHint = `Tip for Correction: +It seems there is a syntax error or a misunderstanding of the API. +To ensure you are using the correct global variables and methods: +1. Call 'mokapi_get_automation_definitions' without parameters to see the general overview. +2. Check 'category="core"' to verify the syntax of the global 'mokapi' object.` + +type RunInput struct { + Code string `json:"code"` +} + +type RunOutput struct { + Result any `json:"result"` +} + +func (s *Service) registerRunTool(server *mcp.Server) { + inputSchema := map[string]any{ + "type": "object", + "properties": map[string]any{ + "code": map[string]any{ + "type": "string", + "description": "JavaScript code to execute in the Mokapi runtime. The last expression is returned as the result.", + }, + }, + "required": []string{"code"}, + } + + outputSchema := map[string]any{ + "type": "object", + "properties": map[string]any{ + "result": map[string]any{ + "description": "The result of the executed code.", + "nullable": true, + }, + }, + "required": []string{"result"}, + } + + registerTool(server, &mcp.Tool{ + Name: "mokapi_execute_code", + Description: `Executes JavaScript code in a sandboxed Mokapi runtime. +The last expression in the code is returned as the result. +Do not guess the API. Always call mokapi_get_automation_definitions first. + +MANDATORY WORKFLOW: +1. FIRST: Call 'mokapi_get_automation_definitions' to get the latest API types. +2. SECOND: Use this tool to query live API data (endpoints, schemas, events). + +Important for Object Returns: +JavaScript interprets {} at the start of a line as a block, not an object. To return an object literal, wrap it in parentheses ({ ... }) or assign it to a variable and put the variable name in the last line. +Example: const result = { a: 1 }; result + +Use this tool to: +- Explore mocked APIs (OpenAPI, AsyncAPI, LDAP, Mail) +- Inspect operations and schemas +- Invoke API operations directly`, + InputSchema: inputSchema, + OutputSchema: outputSchema, + }, s.GetRunResponse) + +} + +func (s *Service) GetRunResponse(_ context.Context, in RunInput) (RunOutput, error) { + m := newMokapi(s.app) + r, err := m.run(in.Code) + if err != nil { + return RunOutput{}, err + } + + return RunOutput{Result: r}, nil +} + +type mokapi struct { + app *runtime.App + vm *goja.Runtime + compiler *compiler.Compiler +} + +func newMokapi(app *runtime.App) *mokapi { + vm := goja.New() + vm.SetFieldNameMapper(&customFieldNameMapper{}) + c, _ := compiler.New() + return &mokapi{app: app, vm: vm, compiler: c} +} + +func (m *mokapi) run(code string) (any, error) { + obj := m.vm.NewObject() + m.init(obj) + _ = m.vm.Set("mokapi", obj) + p, err := m.compiler.Compile("mokapi_execute_code.js", code) + if err != nil { + return nil, err + } + v, err := m.vm.RunProgram(p) + if err != nil { + var ex *goja.Exception + if errors.As(err, &ex) { + ue := ex.Unwrap() + if ue == nil { + return nil, fmt.Errorf("%w\n\n%s", err, errorHint) + } + return nil, ue + } + return nil, err + } + return v.Export(), nil +} + +type ApiSummary struct { + Name string `json:"name"` + Type string `json:"type"` +} + +func (m *mokapi) init(obj *goja.Object) { + _ = obj.Set("getApis", m.getApis) + _ = obj.Set("getApi", m.getApi) + _ = obj.Set("fake", m.fake) + _ = obj.Set("getEvents", m.getEvents) +} + +func (m *mokapi) getApis() []ApiSummary { + var result []ApiSummary + for _, api := range m.app.ListHttp() { + if api.Info.Name == "" { + log.Warnf("mcp tool mokapi_execute_code: skip empty HTTTP API name") + continue + } + result = append(result, ApiSummary{ + Name: api.Info.Name, + Type: "http", + }) + } + for _, api := range m.app.Kafka.List() { + if api.Info.Name == "" { + log.Warnf("mcp tool mokapi_execute_code: skip empty Kafka API name") + continue + } + result = append(result, ApiSummary{ + Name: api.Info.Name, + Type: "kafka", + }) + } + slices.SortStableFunc(result, func(a, b ApiSummary) int { + return strings.Compare(a.Name, b.Name) + }) + return result +} + +func (m *mokapi) getApi(name string) any { + var api any + api = m.getHttpApi(name) + if api != nil { + return api + } + api = m.getKafkaApi(name) + return api +} + +func (m *mokapi) fake(v goja.Value) (any, error) { + js, err := faker.ToJsonSchema(v, m.vm) + if err != nil { + return nil, err + } + return generator.New(&generator.Request{Schema: js}) +} + +type customFieldNameMapper struct { +} + +func (cfm customFieldNameMapper) FieldName(_ reflect.Type, f reflect.StructField) string { + tag := f.Tag.Get("json") + if len(tag) == 0 { + return uncapitalize(f.Name) + } + if idx := strings.IndexByte(tag, ','); idx != -1 { + tag = tag[:idx] + } + + return tag +} + +func (cfm customFieldNameMapper) MethodName(_ reflect.Type, m reflect.Method) string { + return uncapitalize(m.Name) +} + +func uncapitalize(s string) string { + return strings.ToLower(s[0:1]) + s[1:] +} + +func getOperationId(method string, op *openapi.Operation) string { + if op == nil { + return "" + } + if op.OperationId != "" { + return op.OperationId + } + return strings.ToLower(fmt.Sprintf("%s-%s", method, op.Path.Path)) +} diff --git a/mcp/run_events.go b/mcp/run_events.go new file mode 100644 index 000000000..f926f4b23 --- /dev/null +++ b/mcp/run_events.go @@ -0,0 +1,77 @@ +package mcp + +import ( + "fmt" + "mokapi/js/util" + "mokapi/runtime/events" + "reflect" + "strings" + + "github.com/dop251/goja" +) + +func (m *mokapi) getEvents(vTraits goja.Value, vLimit goja.Value) ([]events.Event, error) { + traits, err := parseTraits(vTraits, m.vm) + if err != nil { + return nil, err + } + + evts := m.app.Events.GetEvents(traits) + + limit := 10 + if vLimit != nil { + if vLimit.ExportType().Kind() != reflect.Float64 { + return nil, fmt.Errorf("unexpected type for apiType: %s", util.JsType(vLimit.ExportType())) + } + limit = int(vLimit.ToInteger()) + } + if len(evts) > limit { + return evts[0:limit], nil + } else { + return evts, nil + } +} + +func parseTraits(v goja.Value, vm *goja.Runtime) (events.Traits, error) { + traits := events.Traits{} + + if v == nil { + return traits, nil + } + + if v.ExportType().Kind() != reflect.Map { + return nil, fmt.Errorf("expect object but got: %v", util.JsType(v.Export())) + } + + obj := v.ToObject(vm) + for _, k := range obj.Keys() { + switch k { + case "type": + val := obj.Get(k) + if val.ExportType().Kind() != reflect.String { + return nil, fmt.Errorf("unexpected type for type: %s", util.JsType(val.ExportType())) + } + traits.WithNamespace(val.String()) + case "name": + val := obj.Get(k) + if val.ExportType().Kind() != reflect.String { + return nil, fmt.Errorf("unexpected type for name: %s", util.JsType(val.ExportType())) + } + traits.WithName(val.String()) + case "method": + val := obj.Get(k) + if val.ExportType().Kind() != reflect.String { + return nil, fmt.Errorf("unexpected type for method: %s", util.JsType(val.ExportType())) + } + traits.With("method", strings.ToUpper(val.String())) + default: + val := obj.Get(k) + if val.ExportType().Kind() != reflect.String { + return nil, fmt.Errorf("unexpected type for %s: %s", k, util.JsType(val.ExportType())) + } + traits.With(k, val.String()) + } + } + + return traits, nil +} diff --git a/mcp/run_events_test.go b/mcp/run_events_test.go new file mode 100644 index 000000000..c1f222439 --- /dev/null +++ b/mcp/run_events_test.go @@ -0,0 +1,108 @@ +package mcp_test + +import ( + "context" + "mokapi/mcp" + "mokapi/runtime" + "mokapi/runtime/events" + "mokapi/runtime/runtimetest" + "testing" + + "github.com/stretchr/testify/require" +) + +type testEvent struct { + Name string +} + +func (t *testEvent) Title() string { + return t.Name +} + +func TestEvents(t *testing.T) { + testcases := []struct { + name string + app *runtime.App + code string + test func(t *testing.T, evts []events.Event, err error) + }{ + { + name: "without params should not error", + code: "mokapi.getEvents()", + app: runtimetest.NewApp( + runtimetest.WithEvent(events.NewTraits().WithNamespace("http"), &testEvent{Name: "test-1"}), + ), + test: func(t *testing.T, evts []events.Event, err error) { + require.NoError(t, err) + require.Len(t, evts, 1) + require.Equal(t, &testEvent{Name: "test-1"}, evts[0].Data) + }, + }, + { + name: "filter by API type", + code: "mokapi.getEvents({ type: 'http' })", + app: runtimetest.NewApp( + runtimetest.WithEvent(events.NewTraits().WithNamespace("kafka"), &testEvent{Name: "test-1"}), + runtimetest.WithEvent(events.NewTraits().WithNamespace("http"), &testEvent{Name: "test-2"}), + ), + test: func(t *testing.T, evts []events.Event, err error) { + require.NoError(t, err) + require.Len(t, evts, 1) + require.Equal(t, &testEvent{Name: "test-2"}, evts[0].Data) + }, + }, + { + name: "filter by API name", + code: "mokapi.getEvents({ name: 'bar' })", + app: runtimetest.NewApp( + runtimetest.WithEvent(events.NewTraits().WithNamespace("http").WithName("foo"), &testEvent{Name: "test-1"}), + runtimetest.WithEvent(events.NewTraits().WithNamespace("http").WithName("bar"), &testEvent{Name: "test-2"}), + ), + test: func(t *testing.T, evts []events.Event, err error) { + require.NoError(t, err) + require.Len(t, evts, 1) + require.Equal(t, &testEvent{Name: "test-2"}, evts[0].Data) + }, + }, + { + name: "filter by path", + code: "mokapi.getEvents({ path: '/pets' })", + app: runtimetest.NewApp( + runtimetest.WithEvent(events.NewTraits().WithNamespace("http").With("path", "/users"), &testEvent{Name: "test-1"}), + runtimetest.WithEvent(events.NewTraits().WithNamespace("http").With("path", "/pets"), &testEvent{Name: "test-2"}), + ), + test: func(t *testing.T, evts []events.Event, err error) { + require.NoError(t, err) + require.Len(t, evts, 1) + require.Equal(t, &testEvent{Name: "test-2"}, evts[0].Data) + }, + }, + { + name: "filter by method", + code: "mokapi.getEvents({ method: 'post' })", + app: runtimetest.NewApp( + runtimetest.WithEvent(events.NewTraits().WithNamespace("http").With("method", "GET"), &testEvent{Name: "test-1"}), + runtimetest.WithEvent(events.NewTraits().WithNamespace("http").With("method", "POST"), &testEvent{Name: "test-2"}), + ), + test: func(t *testing.T, evts []events.Event, err error) { + require.NoError(t, err) + require.Len(t, evts, 1) + require.Equal(t, &testEvent{Name: "test-2"}, evts[0].Data) + }, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + s := mcp.NewService(tc.app) + + r, err := s.GetRunResponse( + context.Background(), + mcp.RunInput{Code: tc.code}, + ) + require.IsType(t, []events.Event{}, r.Result) + + tc.test(t, r.Result.([]events.Event), err) + }) + } +} diff --git a/mcp/run_http.go b/mcp/run_http.go new file mode 100644 index 000000000..3616dbf8c --- /dev/null +++ b/mcp/run_http.go @@ -0,0 +1,321 @@ +package mcp + +import ( + "fmt" + "io" + "mokapi/providers/openapi" + "mokapi/providers/openapi/schema" + "mokapi/runtime" + "net/http" + "net/textproto" + "slices" + "strconv" + "strings" +) + +type OpenAPI struct { + Name string `json:"name"` + Type string `json:"type"` + Servers []OpenAPIServer `json:"servers"` + + info *runtime.HttpInfo + handler openapi.Handler +} + +type OpenAPIServer struct { + Url string `json:"url"` + Description string `json:"description"` +} + +type OperationSummary struct { + Id string `json:"id"` + Method string `json:"method"` + Path string `json:"path"` + Summary string `json:"summary"` + Parameters []string `json:"parameters"` +} + +type Operation struct { + OperationId string `json:"operationId"` + Method string `json:"method"` + Path string `json:"path"` + Summary string `json:"summary"` + Description string `json:"description,omitempty"` + Parameters []RequestParameters `json:"parameters,omitempty"` + RequestBody RequestBody `json:"requestBody,omitempty"` + Responses []Response `json:"responses,omitempty"` + + spec *openapi.Operation + handler openapi.Handler +} + +type RequestParameters struct { + Name string `json:"name"` + In string `json:"in"` + Required bool `json:"required"` + Schema *schema.Schema + Description string `json:"description,omitempty"` +} + +type RequestBody struct { + Description string `json:"description,omitempty"` + Required bool `json:"required"` + Contents []Content `json:"contents"` +} + +type Content struct { + ContentType string `json:"contentType"` + Schema *schema.Schema `json:"schema"` +} + +type Response struct { + StatusCode int `json:"statusCode"` + Description string `json:"description,omitempty"` + Content []Content `json:"content"` +} + +func (m *mokapi) getHttpApi(name string) any { + for _, api := range m.app.ListHttp() { + if api.Info.Name == name { + result := &OpenAPI{ + Name: name, + Type: "http", + info: api, + handler: api.Handler(m.app.Monitor.Http, m.app.Engine, m.app.Events), + } + for _, server := range api.Servers { + result.Servers = append(result.Servers, OpenAPIServer{ + Url: server.Url, + Description: server.Description, + }) + } + + return result + } + } + return nil +} +func (o *OpenAPI) GetOperations() []OperationSummary { + var result []OperationSummary + for _, p := range o.info.Paths { + if p.Value == nil { + continue + } + for method, op := range p.Value.Operations() { + os := OperationSummary{ + Id: getOperationId(method, op), + Method: method, + Path: p.Value.Path, + Summary: op.Summary, + } + + if os.Summary == "" { + os.Summary = p.Value.Summary + } + + params := append(op.Path.Parameters, op.Parameters...) + for _, param := range params { + if param.Value == nil { + continue + } + os.Parameters = append(os.Parameters, param.Value.Name) + } + slices.SortStableFunc(os.Parameters, func(a, b string) int { + return strings.Compare(a, b) + }) + + result = append(result, os) + } + } + + slices.SortStableFunc(result, func(a, b OperationSummary) int { + c := strings.Compare(a.Path, b.Path) + if c != 0 { + return c + } + return strings.Compare(a.Method, b.Method) + }) + + return result +} + +func (o *OpenAPI) GetOperation(id string) (*Operation, error) { + for _, p := range o.info.Paths { + if p.Value == nil { + continue + } + for method, op := range p.Value.Operations() { + + operationId := getOperationId(method, op) + if id != operationId { + continue + } + + r := &Operation{ + OperationId: operationId, + Method: method, + Path: p.Value.Path, + Summary: op.Summary, + Description: op.Description, + spec: op, + handler: o.handler, + } + for _, param := range op.Parameters { + if param.Value == nil { + continue + } + r.Parameters = append(r.Parameters, RequestParameters{ + Name: param.Value.Name, + In: param.Value.Type.String(), + Required: param.Value.Required, + Schema: param.Value.Schema, + Description: param.Value.Description, + }) + } + slices.SortStableFunc(r.Parameters, func(a, b RequestParameters) int { + return strings.Compare(a.Name, b.Name) + }) + + if op.RequestBody != nil && op.RequestBody.Value != nil { + r.RequestBody = RequestBody{ + Description: op.RequestBody.Value.Description, + Required: op.RequestBody.Value.Required, + } + for ct, content := range op.RequestBody.Value.Content { + r.RequestBody.Contents = append(r.RequestBody.Contents, Content{ + ContentType: ct, + Schema: content.Schema, + }) + } + } + for it := op.Responses.Iter(); it.Next(); { + status, err := strconv.Atoi(it.Key()) + if err != nil { + continue + } + + res := it.Value() + if res.Value == nil { + continue + } + var contents []Content + for ct, content := range res.Value.Content { + contents = append(contents, Content{ + ContentType: ct, + Schema: content.Schema, + }) + } + r.Responses = append(r.Responses, Response{ + StatusCode: status, + Description: res.Value.Description, + Content: contents, + }) + } + + return r, nil + } + } + return nil, fmt.Errorf("operation with ID '%s' not found. Hint: Use getOperations() to see the full list of valid IDs", id) +} + +type InvokeRequest struct { + Path map[string]string `json:"path"` + Query map[string]string `json:"query"` + Header map[string][]string `json:"header"` + Body string `json:"body"` +} + +type InvokeResponse struct { + StatusCode int `json:"statusCode"` + Headers map[string][]string `json:"headers"` + Body string `json:"body"` +} + +func (op *Operation) Invoke(req InvokeRequest) (InvokeResponse, error) { + result := InvokeResponse{Headers: make(map[string][]string)} + + var body io.Reader + if req.Body != "" { + body = strings.NewReader(req.Body) + } + + path := op.Path + query := "" + params := append(op.spec.Path.Parameters, op.spec.Parameters...) + for _, p := range params { + if p.Value == nil { + continue + } + switch p.Value.Type { + case openapi.ParameterPath: + if req.Path == nil { + return result, fmt.Errorf("invoke request %s %s failed: missing path parameter '%s'", op.Method, op.Path, p.Value.Name) + } + val, ok := req.Path[p.Value.Name] + if !ok { + return result, fmt.Errorf("invoke request %s %s failed: missing path parameter '%s'", op.Method, op.Path, p.Value.Name) + } + path = strings.ReplaceAll(path, fmt.Sprintf("{%s}", p.Value.Name), val) + case openapi.ParameterQuery: + if req.Query == nil && p.Value.Required { + return result, fmt.Errorf("invoke request %s %s failed: missing query parameter '%s'", op.Method, op.Path, p.Value.Name) + } + val, ok := req.Query[p.Value.Name] + if !ok { + if !p.Value.Required { + continue + } + return result, fmt.Errorf("invoke request %s %s failed: missing query parameter '%s'", op.Method, op.Path, p.Value.Name) + } + if query != "" { + query += "&" + } + query += fmt.Sprintf("%s=%s", p.Value.Name, val) + } + } + + if query != "" { + path += "?" + query + } + + r, err := http.NewRequest(op.Method, path, body) + if err != nil { + return result, fmt.Errorf("error creating request: %w", err) + } + for _, p := range params { + if p.Value == nil || p.Value.Type != openapi.ParameterHeader { + continue + } + if req.Header == nil && p.Value.Required { + return result, fmt.Errorf("invoke request %s %s failed: missing header parameter '%s'", op.Method, op.Path, p.Value.Name) + } + val, ok := req.Header[p.Value.Name] + if !ok { + if !p.Value.Required { + continue + } + return result, fmt.Errorf("invoke request %s %s failed: missing header parameter '%s'", op.Method, op.Path, p.Value.Name) + } + r.Header[textproto.CanonicalMIMEHeaderKey(p.Value.Name)] = val + } + + he := op.handler.ServeHTTP(&result, r) + if he != nil { + result.StatusCode = he.StatusCode + result.Body = he.Message + } + return result, nil +} + +func (r *InvokeResponse) Header() http.Header { + return r.Headers +} + +func (r *InvokeResponse) WriteHeader(statusCode int) { + r.StatusCode = statusCode +} + +func (r *InvokeResponse) Write(body []byte) (int, error) { + r.Body = string(body) + return len(body), nil +} diff --git a/mcp/run_http_test.go b/mcp/run_http_test.go new file mode 100644 index 000000000..2dc4708d9 --- /dev/null +++ b/mcp/run_http_test.go @@ -0,0 +1,405 @@ +package mcp_test + +import ( + "context" + "mokapi/mcp" + "mokapi/providers/openapi" + "mokapi/providers/openapi/openapitest" + "mokapi/providers/openapi/schema/schematest" + "mokapi/runtime" + "mokapi/runtime/runtimetest" + "mokapi/schema/json/generator" + "net/http" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestService_Run_Http(t *testing.T) { + testcases := []struct { + name string + app *runtime.App + test func(t *testing.T, s *mcp.Service) + }{ + { + name: "get HTTP APIs", + app: runtimetest.NewHttpApp( + openapitest.NewConfig("3.1.0", + openapitest.WithInfo("foo", "", ""), + ), + openapitest.NewConfig("3.1.0", + openapitest.WithInfo("bar", "", ""), + ), + ), + test: func(t *testing.T, s *mcp.Service) { + r, err := s.GetRunResponse( + context.Background(), + mcp.RunInput{ + Code: `mokapi.getApis()`, + }, + ) + require.NoError(t, err) + require.Equal(t, []mcp.ApiSummary{ + {Name: "bar", Type: "http"}, + {Name: "foo", Type: "http"}, + }, r.Result) + }, + }, + { + name: "get specific HTTP API", + app: runtimetest.NewHttpApp( + openapitest.NewConfig("3.1.0", + openapitest.WithInfo("foo", "", ""), + openapitest.WithServer("http://localhost/foo", "server description"), + ), + openapitest.NewConfig("3.1.0", + openapitest.WithInfo("bar", "", ""), + ), + ), + test: func(t *testing.T, s *mcp.Service) { + r, err := s.GetRunResponse( + context.Background(), + mcp.RunInput{ + Code: `mokapi.getApi('foo')`, + }, + ) + require.NoError(t, err) + require.IsType(t, &mcp.OpenAPI{}, r.Result) + api := r.Result.(*mcp.OpenAPI) + require.Equal(t, "foo", api.Name) + require.Equal(t, "http", api.Type) + require.Equal(t, []mcp.OpenAPIServer{{Url: "http://localhost/foo", Description: "server description"}}, api.Servers) + }, + }, + { + name: "get API's operation list", + app: runtimetest.NewHttpApp( + openapitest.NewConfig("3.1.0", + openapitest.WithInfo("foo", "", ""), + openapitest.WithPath("/pets", + openapitest.WithOperation(http.MethodGet, + openapitest.WithOperationId("pets"), + openapitest.WithOperationSummary("GET summary"), + ), + openapitest.WithOperation(http.MethodPut, + openapitest.WithOperationSummary("PUT summary"), + ), + ), + openapitest.WithPath("/users", + openapitest.WithPathInfo("path summary", ""), + openapitest.WithPathParam("foo"), + openapitest.WithOperation(http.MethodPost, openapitest.WithOperationParam("bar", false)), + ), + ), + ), + test: func(t *testing.T, s *mcp.Service) { + r, err := s.GetRunResponse( + context.Background(), + mcp.RunInput{ + Code: `mokapi.getApi('foo').getOperations()`, + }, + ) + require.NoError(t, err) + require.Equal(t, []mcp.OperationSummary{ + {Id: "pets", Method: "GET", Path: "/pets", Summary: "GET summary"}, + {Id: "put-/pets", Method: "PUT", Path: "/pets", Summary: "PUT summary"}, + {Id: "post-/users", Method: "POST", Path: "/users", Summary: "path summary", Parameters: []string{"bar", "foo"}}}, + r.Result) + }, + }, + { + name: "get API's operation details", + app: runtimetest.NewHttpApp( + openapitest.NewConfig("3.1.0", + openapitest.WithInfo("foo", "", ""), + openapitest.WithPath("/pets", + openapitest.WithOperation(http.MethodGet, + openapitest.WithOperationSummary("GET summary"), + openapitest.WithHeaderParam("foo", false, openapitest.WithParamSchema(schematest.New("string"))), + openapitest.WithRequestBody( + "request body description", + true, + openapitest.WithRequestContent( + "application/json", + &openapi.MediaType{ + Schema: schematest.New("string"), + }, + ), + ), + openapitest.WithResponse(200, + openapitest.WithResponseDescription("response description"), + openapitest.WithContent("application/json", openapitest.WithSchema( + schematest.New("string"), + )), + ), + ), + openapitest.WithOperation(http.MethodPut, + openapitest.WithOperationSummary("PUT summary"), + ), + ), + ), + ), + test: func(t *testing.T, s *mcp.Service) { + r, err := s.GetRunResponse( + context.Background(), + mcp.RunInput{ + Code: `mokapi.getApi('foo').getOperation('get-/pets')`, + }, + ) + require.NoError(t, err) + require.IsType(t, &mcp.Operation{}, r.Result) + op := r.Result.(*mcp.Operation) + require.Equal(t, "get-/pets", op.OperationId) + require.Equal(t, "GET", op.Method) + require.Equal(t, "/pets", op.Path) + require.Equal(t, "GET summary", op.Summary) + require.Equal(t, "", op.Description) + require.Equal(t, "foo", op.Parameters[0].Name) + require.Equal(t, "header", op.Parameters[0].In) + require.Equal(t, false, op.Parameters[0].Required) + require.Equal(t, "string", op.Parameters[0].Schema.Type[0]) + require.Equal(t, "", op.Parameters[0].Description) + require.Equal(t, "request body description", op.RequestBody.Description) + require.Equal(t, true, op.RequestBody.Required) + require.Equal(t, "application/json", op.RequestBody.Contents[0].ContentType) + require.Equal(t, "string", op.RequestBody.Contents[0].Schema.Type[0]) + + require.Len(t, op.Responses, 1) + require.Equal(t, http.StatusOK, op.Responses[0].StatusCode) + require.Equal(t, "response description", op.Responses[0].Description) + require.Len(t, op.Responses[0].Content, 1) + require.Equal(t, "application/json", op.Responses[0].Content[0].ContentType) + require.Equal(t, "string", op.Responses[0].Content[0].Schema.Type[0]) + }, + }, + { + name: "get API's operation using projection", + app: runtimetest.NewHttpApp( + openapitest.NewConfig("3.1.0", + openapitest.WithInfo("foo", "", ""), + openapitest.WithPath("/pets", + openapitest.WithOperation(http.MethodGet, + openapitest.WithOperationSummary("GET summary"), + openapitest.WithHeaderParam("foo", false, openapitest.WithParamSchema(schematest.New("string"))), + openapitest.WithRequestBody( + "request body description", + true, + openapitest.WithRequestContent( + "application/json", + &openapi.MediaType{ + Schema: schematest.New("string"), + }, + ), + ), + openapitest.WithResponse(200, + openapitest.WithContent("application/json", openapitest.WithSchema( + schematest.New("string"), + )), + ), + ), + openapitest.WithOperation(http.MethodPut, + openapitest.WithOperationSummary("PUT summary"), + ), + ), + ), + ), + test: func(t *testing.T, s *mcp.Service) { + r, err := s.GetRunResponse( + context.Background(), + mcp.RunInput{ + Code: `const op = mokapi.getApi('foo').getOperation('get-/pets') +const result = { path: op.path, method: op.method }; result`, + }, + ) + require.NoError(t, err) + require.Equal(t, map[string]any{"method": "GET", "path": "/pets"}, r.Result) + }, + }, + { + name: "invoke request", + app: runtimetest.NewHttpApp( + openapitest.NewConfig("3.1.0", + openapitest.WithInfo("foo", "", ""), + openapitest.WithPath("/pets", + openapitest.WithOperation(http.MethodGet, + openapitest.WithResponse(200, + openapitest.WithResponseDescription("response description"), + openapitest.WithContent("application/json", openapitest.WithSchema( + schematest.New("object", + schematest.WithProperty("foo", schematest.New("string")), + schematest.WithProperty("bar", schematest.New("integer")), + schematest.WithRequired("foo", "bar"), + ), + )), + ), + ), + ), + ), + ), + test: func(t *testing.T, s *mcp.Service) { + r, err := s.GetRunResponse( + context.Background(), + mcp.RunInput{ + Code: `const op = mokapi.getApi('foo').getOperation('get-/pets'); +op.invoke()`, + }, + ) + require.NoError(t, err) + require.IsType(t, mcp.InvokeResponse{}, r.Result) + res := r.Result.(mcp.InvokeResponse) + require.Equal(t, 200, res.StatusCode) + require.Equal(t, `{"foo":"P8","bar":-804702}`, res.Body) + require.Equal(t, map[string][]string{ + "Content-Type": {"application/json"}, + }, res.Headers) + }, + }, + { + name: "invoke request with path parameter", + app: runtimetest.NewHttpApp( + openapitest.NewConfig("3.1.0", + openapitest.WithInfo("foo", "", ""), + openapitest.WithPath("/{foo}/{bar}/pets", + openapitest.WithOperation(http.MethodGet, + openapitest.WithOperationParam("foo", true, openapitest.WithParamSchema(schematest.New("string"))), + openapitest.WithOperationParam("bar", true, openapitest.WithParamSchema(schematest.New("string"))), + openapitest.WithResponse(200, + openapitest.WithResponseDescription("response description"), + openapitest.WithContent("application/json", openapitest.WithSchema( + schematest.New("string"), + )), + ), + ), + ), + ), + ), + test: func(t *testing.T, s *mcp.Service) { + r, err := s.GetRunResponse( + context.Background(), + mcp.RunInput{ + Code: `const op = mokapi.getApi('foo').getOperation('get-/{foo}/{bar}/pets'); +op.invoke({ path: { foo: 'val1', 'bar': 'val2' }})`, + }, + ) + require.NoError(t, err) + res := r.Result.(mcp.InvokeResponse) + require.Equal(t, http.StatusOK, res.StatusCode, res.Body) + }, + }, + { + name: "invoke request with path parameter but not specified", + app: runtimetest.NewHttpApp( + openapitest.NewConfig("3.1.0", + openapitest.WithInfo("foo", "", ""), + openapitest.WithPath("/{foo}/{bar}/pets", + openapitest.WithOperation(http.MethodGet, + openapitest.WithOperationParam("foo", true, openapitest.WithParamSchema(schematest.New("string"))), + openapitest.WithOperationParam("bar", true, openapitest.WithParamSchema(schematest.New("string"))), + openapitest.WithResponse(200, + openapitest.WithResponseDescription("response description"), + openapitest.WithContent("application/json", openapitest.WithSchema( + schematest.New("string"), + )), + ), + ), + ), + ), + ), + test: func(t *testing.T, s *mcp.Service) { + _, err := s.GetRunResponse( + context.Background(), + mcp.RunInput{ + Code: `const op = mokapi.getApi('foo').getOperation('get-/{foo}/{bar}/pets'); +op.invoke()`, + }, + ) + require.EqualError(t, err, "invoke request GET /{foo}/{bar}/pets failed: missing path parameter 'foo'") + }, + }, + { + name: "invoke request with query parameter", + app: runtimetest.NewHttpApp( + openapitest.NewConfig("3.1.0", + openapitest.WithInfo("foo", "", ""), + openapitest.WithPath("/pets", + openapitest.WithOperation(http.MethodGet, + openapitest.WithQueryParam("foo", true, openapitest.WithParamSchema(schematest.New("string"))), + openapitest.WithResponse(200, + openapitest.WithResponseDescription("response description"), + openapitest.WithContent("application/json", openapitest.WithSchema( + schematest.New("string"), + )), + ), + ), + ), + ), + ), + test: func(t *testing.T, s *mcp.Service) { + r, err := s.GetRunResponse( + context.Background(), + mcp.RunInput{ + Code: `const op = mokapi.getApi('foo').getOperation('get-/pets'); +op.invoke({ query: { foo: 'val1' }})`, + }, + ) + require.NoError(t, err) + res := r.Result.(mcp.InvokeResponse) + require.Equal(t, http.StatusOK, res.StatusCode, res.Body) + }, + }, + { + name: "invoke request with header parameter", + app: runtimetest.NewHttpApp( + openapitest.NewConfig("3.1.0", + openapitest.WithInfo("foo", "", ""), + openapitest.WithPath("/pets", + openapitest.WithOperation(http.MethodGet, + openapitest.WithHeaderParam("foo", true, openapitest.WithParamSchema(schematest.New("string"))), + openapitest.WithResponse(200, + openapitest.WithResponseDescription("response description"), + openapitest.WithContent("application/json", openapitest.WithSchema( + schematest.New("string"), + )), + ), + ), + ), + ), + ), + test: func(t *testing.T, s *mcp.Service) { + r, err := s.GetRunResponse( + context.Background(), + mcp.RunInput{ + Code: `const op = mokapi.getApi('foo').getOperation('get-/pets'); +op.invoke({ header: { foo: ['val1'] }})`, + }, + ) + require.NoError(t, err) + res := r.Result.(mcp.InvokeResponse) + require.Equal(t, http.StatusOK, res.StatusCode, res.Body) + }, + }, + { + name: "fake", + app: runtimetest.NewApp(), + test: func(t *testing.T, s *mcp.Service) { + r, err := s.GetRunResponse( + context.Background(), + mcp.RunInput{ + Code: `mokapi.fake({ type: 'string', format: 'email' })`, + }, + ) + require.NoError(t, err) + require.Equal(t, "ivyjones@ziemann.com", r.Result) + }, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + generator.Seed(123456) + + s := mcp.NewService(tc.app) + tc.test(t, s) + }) + } +} diff --git a/mcp/run_kafka.go b/mcp/run_kafka.go new file mode 100644 index 000000000..ca2474e89 --- /dev/null +++ b/mcp/run_kafka.go @@ -0,0 +1,290 @@ +package mcp + +import ( + "fmt" + "mokapi/engine" + "mokapi/engine/common" + "mokapi/kafka" + "mokapi/runtime" + "slices" + "strings" + "time" +) + +type Kafka struct { + Name string `json:"name"` + Type string `json:"type"` + Brokers []Broker `json:"brokers"` + + info *runtime.KafkaInfo + client *engine.KafkaClient +} + +type Broker struct { + Name string `json:"name"` + Host string `json:"url"` + Description string `json:"description,omitempty"` +} + +type TopicSummary struct { + Name string `json:"name"` + Title string `json:"title,omitempty"` + Summary string `json:"description,omitempty"` +} + +type Topic struct { + TopicSummary + Description string `json:"description,omitempty"` + Partitions []*KafkaPartition `json:"partitions"` + + Operations []KafkaOperation `json:"operations,omitempty"` + + info *runtime.KafkaInfo + client *engine.KafkaClient +} + +type KafkaPartition struct { + Index int `json:"index"` + Offset int64 `json:"offset"` +} + +type KafkaOperation struct { + Action string `json:"action"` + Title string `json:"title"` + Summary string `json:"summary,omitempty"` + Description string `json:"description,omitempty"` + Messages []KafkaMessage `json:"messages,omitempty"` +} + +type KafkaMessage struct { + Name string `json:"name"` + Title string `json:"title"` + Summary string `json:"summary,omitempty"` + Description string `json:"description,omitempty"` + ContentType string `json:"contentType"` + Payload any `json:"payload,omitempty"` + Key any `json:"key,omitempty"` + Headers any `json:"headers,omitempty"` +} + +type KafkaRecord struct { + Offset int64 `json:"offset"` + Key string `json:"key"` + Value string `json:"value"` + Headers map[string]string `json:"headers,omitempty"` +} + +func (m *mokapi) getKafkaApi(name string) any { + for _, api := range m.app.Kafka.List() { + if api.Info.Name == name { + result := &Kafka{ + Name: name, + Type: "kafka", + info: api, + client: engine.NewKafkaClient(m.app), + } + for it := api.Servers.Iter(); it.Next(); { + b := it.Value() + if b.Value == nil { + continue + } + result.Brokers = append(result.Brokers, Broker{ + Name: it.Key(), + Host: b.Value.Host, + Description: b.Value.Description, + }) + } + + return result + } + } + return nil +} + +func (k *Kafka) GetTopics() []TopicSummary { + var topics []TopicSummary + for name, c := range k.info.Channels { + if c.Value == nil { + continue + } + topics = append(topics, TopicSummary{ + Name: name, + Title: c.Value.Title, + Summary: c.Value.Summary, + }) + } + slices.SortStableFunc(topics, func(a, b TopicSummary) int { + return strings.Compare(a.Name, b.Name) + }) + return topics +} + +func (k *Kafka) GetTopic(name string) (Topic, error) { + ch, ok := k.info.Channels[name] + if !ok || ch.Value == nil { + return Topic{}, fmt.Errorf("topic '%s' not found", name) + } + + t := Topic{ + TopicSummary: TopicSummary{ + Name: name, + Title: ch.Value.Title, + Summary: ch.Value.Summary, + }, + Description: ch.Value.Description, + info: k.info, + client: k.client, + } + + topic := k.info.Store.Topic(name) + if topic == nil { + return Topic{}, fmt.Errorf("topic '%s' not found", name) + } + for _, p := range topic.Partitions { + t.Partitions = append(t.Partitions, &KafkaPartition{ + Index: p.Index, + Offset: p.Offset(), + }) + } + + for _, op := range k.info.Operations { + if op.Value == nil { + continue + } + if op.Value.Channel.Value != ch.Value { + continue + } + + result := KafkaOperation{ + Action: op.Value.Action, + Title: op.Value.Title, + Summary: op.Value.Summary, + Description: op.Value.Description, + } + + for _, msg := range op.Value.Messages { + if msg.Value == nil { + continue + } + m := KafkaMessage{ + Name: msg.Value.Name, + Title: msg.Value.Title, + Summary: msg.Value.Summary, + Description: msg.Value.Description, + ContentType: msg.Value.ContentType, + Headers: msg.Value.Headers, + } + if msg.Value.Payload != nil { + m.Payload = msg.Value.Payload.Value + } + if msg.Value.Bindings.Kafka.Key != nil { + m.Key = msg.Value.Bindings.Kafka.Key + } + result.Messages = append(result.Messages, m) + } + + t.Operations = append(t.Operations, result) + } + + slices.SortStableFunc(t.Operations, func(a, b KafkaOperation) int { + r := strings.Compare(a.Action, b.Action) + if r != 0 { + return r + } + return strings.Compare(a.Title, b.Title) + }) + + return t, nil +} + +func (t *Topic) Produce(partition int, value string, key string, headers map[string]string) error { + msg := common.KafkaMessage{ + Value: []byte(value), + Data: nil, + Headers: headers, + Partition: partition, + } + if key != "" { + msg.Key = []byte(key) + } + + _, err := t.client.Produce(&common.KafkaProduceArgs{ + Cluster: t.info.Info.Name, + Topic: t.Name, + Messages: []common.KafkaMessage{msg}, + Retry: common.KafkaProduceRetry{ + MaxRetryTime: 3 * time.Minute, + InitialRetryTime: 500 * time.Millisecond, + Retries: 10, + Factor: 2, + }, + ClientId: "mokapi-mcp", + }) + if err != nil { + return err + } + + topic := t.info.Store.Topic(t.Name) + if topic == nil { + return fmt.Errorf("topic '%s' not found", t.Name) + } + p := topic.Partition(partition) + if p == nil { + return fmt.Errorf("partition '%s' not found", t.Name) + } + for _, pt := range t.Partitions { + if pt.Index == p.Index { + pt.Offset = p.Offset() + } + } + return nil +} + +func (t *Topic) Consume(partition int, startOffset int64, limit int) ([]KafkaRecord, error) { + topic := t.info.Store.Topic(t.Name) + if topic == nil { + return nil, fmt.Errorf("topic '%s' not found", t.Name) + } + p := topic.Partition(partition) + if p == nil { + return nil, fmt.Errorf("partition '%d' not found", partition) + } + + var records []KafkaRecord + offset := startOffset + n := 0 + for { + if offset >= p.Tail || n >= limit { + return records, nil + } + seg := p.GetSegment(offset) + if seg == nil { + return records, nil + } + + for seg.Contains(offset) { + r := seg.Record(offset) + + result := KafkaRecord{ + Offset: offset, + Key: kafka.BytesToString(r.Key), + Value: kafka.BytesToString(r.Value), + } + if r.Headers != nil { + result.Headers = make(map[string]string) + for _, h := range r.Headers { + result.Headers[h.Key] = string(h.Value) + } + } + + records = append(records, result) + + n++ + offset++ + + if n >= limit { + return records, nil + } + } + } +} diff --git a/mcp/run_kafka_test.go b/mcp/run_kafka_test.go new file mode 100644 index 000000000..1ca471b24 --- /dev/null +++ b/mcp/run_kafka_test.go @@ -0,0 +1,354 @@ +package mcp_test + +import ( + "context" + "mokapi/kafka" + "mokapi/mcp" + "mokapi/providers/asyncapi3" + "mokapi/providers/asyncapi3/asyncapi3test" + "mokapi/runtime" + "mokapi/runtime/runtimetest" + "mokapi/schema/json/generator" + jsonSchema "mokapi/schema/json/schema" + "mokapi/schema/json/schema/schematest" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestService_Run_Kafka(t *testing.T) { + testcases := []struct { + name string + app *runtime.App + test func(t *testing.T, s *mcp.Service) + }{ + { + name: "get Kafka APIs", + app: runtimetest.NewKafkaApp( + asyncapi3test.NewConfig( + asyncapi3test.WithInfo("foo", "", ""), + ), + asyncapi3test.NewConfig( + asyncapi3test.WithInfo("bar", "", ""), + ), + ), + test: func(t *testing.T, s *mcp.Service) { + r, err := s.GetRunResponse( + context.Background(), + mcp.RunInput{ + Code: `mokapi.getApis()`, + }, + ) + require.NoError(t, err) + require.Equal(t, []mcp.ApiSummary{ + {Name: "bar", Type: "kafka"}, + {Name: "foo", Type: "kafka"}, + }, r.Result) + }, + }, + { + name: "get Kafka API", + app: runtimetest.NewKafkaApp( + asyncapi3test.NewConfig( + asyncapi3test.WithInfo("foo", "", ""), + asyncapi3test.WithServer("bar", "kafka", "foo.bar", asyncapi3test.WithServerDescription("server description")), + ), + asyncapi3test.NewConfig( + asyncapi3test.WithInfo("bar", "", ""), + ), + ), + test: func(t *testing.T, s *mcp.Service) { + r, err := s.GetRunResponse( + context.Background(), + mcp.RunInput{ + Code: `mokapi.getApi('foo')`, + }, + ) + require.NoError(t, err) + require.IsType(t, &mcp.Kafka{}, r.Result) + kafka := r.Result.(*mcp.Kafka) + require.Equal(t, "foo", kafka.Name) + require.Equal(t, "kafka", kafka.Type) + require.Equal(t, []mcp.Broker{{Name: "bar", Host: "foo.bar", Description: "server description"}}, kafka.Brokers) + }, + }, + { + name: "get topics", + app: runtimetest.NewKafkaApp( + asyncapi3test.NewConfig( + asyncapi3test.WithInfo("foo", "", ""), + asyncapi3test.WithChannel("channel-1", + asyncapi3test.WithChannelTitle("title-1"), + asyncapi3test.WithChannelSummary("channel-1 summary"), + ), + asyncapi3test.WithChannel("channel-2", + asyncapi3test.WithChannelTitle("title-2"), + ), + ), + asyncapi3test.NewConfig( + asyncapi3test.WithInfo("bar", "", ""), + ), + ), + test: func(t *testing.T, s *mcp.Service) { + r, err := s.GetRunResponse( + context.Background(), + mcp.RunInput{ + Code: `mokapi.getApi('foo').getTopics()`, + }, + ) + require.NoError(t, err) + require.IsType(t, []mcp.TopicSummary{}, r.Result) + topics := r.Result.([]mcp.TopicSummary) + require.Len(t, topics, 2) + require.Equal(t, "channel-1", topics[0].Name) + require.Equal(t, "title-1", topics[0].Title) + require.Equal(t, "channel-1 summary", topics[0].Summary) + require.Equal(t, "channel-2", topics[1].Name) + require.Equal(t, "title-2", topics[1].Title) + require.Equal(t, "", topics[1].Summary) + }, + }, + { + name: "get topic", + app: func() *runtime.App { + msg := asyncapi3test.NewMessage( + asyncapi3test.WithMessageName("msg-name-1"), + asyncapi3test.WithMessageTitle("msg-title-1"), + asyncapi3test.WithMessageSummary("msg-summary-1"), + asyncapi3test.WithMessageDescription("msg-description-1"), + asyncapi3test.WithContentType("application/json"), + asyncapi3test.WithPayload( + schematest.New("object", + schematest.WithProperty("foo", schematest.New("string")), + ), + ), + ) + + ch := asyncapi3test.NewChannel( + asyncapi3test.WithChannelTitle("title-1"), + asyncapi3test.WithChannelSummary("channel-1 summary"), + asyncapi3test.WithChannelDescription("description"), + asyncapi3test.UseMessage("foo", &asyncapi3.MessageRef{Value: msg}), + ) + + return runtimetest.NewKafkaApp( + asyncapi3test.NewConfig( + asyncapi3test.WithInfo("foo", "", ""), + asyncapi3test.AddChannel("channel-1", ch), + asyncapi3test.WithOperation("publish", + asyncapi3test.WithOperationAction("send"), + asyncapi3test.WithOperationTitle("op-title-1"), + asyncapi3test.WithOperationSummary("op-summary-1"), + asyncapi3test.WithOperationDescription("op-description-1"), + asyncapi3test.WithOperationChannel(ch), + asyncapi3test.UseOperationMessage(msg), + ), + asyncapi3test.WithOperation("consume", + asyncapi3test.WithOperationAction("receive"), + asyncapi3test.WithOperationChannel(ch), + asyncapi3test.UseOperationMessage(msg), + ), + ), + ) + }(), + test: func(t *testing.T, s *mcp.Service) { + r, err := s.GetRunResponse( + context.Background(), + mcp.RunInput{ + Code: `mokapi.getApi('foo').getTopic('channel-1')`, + }, + ) + require.NoError(t, err) + require.IsType(t, mcp.Topic{}, r.Result) + topic := r.Result.(mcp.Topic) + require.Equal(t, "channel-1", topic.Name) + require.Equal(t, "title-1", topic.Title) + require.Equal(t, "channel-1 summary", topic.Summary) + require.Equal(t, "description", topic.Description) + require.Len(t, topic.Operations, 2) + + require.Equal(t, "receive", topic.Operations[0].Action) + + require.Equal(t, "send", topic.Operations[1].Action) + require.Equal(t, "op-title-1", topic.Operations[1].Title) + require.Equal(t, "op-summary-1", topic.Operations[1].Summary) + require.Equal(t, "op-description-1", topic.Operations[1].Description) + require.Len(t, topic.Operations[1].Messages, 1) + require.Equal(t, "msg-name-1", topic.Operations[1].Messages[0].Name) + require.Equal(t, "msg-title-1", topic.Operations[1].Messages[0].Title) + require.Equal(t, "msg-summary-1", topic.Operations[1].Messages[0].Summary) + require.Equal(t, "msg-description-1", topic.Operations[1].Messages[0].Description) + require.Equal(t, "application/json", topic.Operations[1].Messages[0].ContentType) + require.IsType(t, &jsonSchema.Schema{}, topic.Operations[1].Messages[0].Payload) + payload := topic.Operations[1].Messages[0].Payload.(*jsonSchema.Schema) + require.Equal(t, "object", payload.Type.String()) + }, + }, + { + name: "consume from topic", + app: func() *runtime.App { + msg := asyncapi3test.NewMessage( + asyncapi3test.WithMessageName("msg-name-1"), + asyncapi3test.WithMessageTitle("msg-title-1"), + asyncapi3test.WithMessageSummary("msg-summary-1"), + asyncapi3test.WithMessageDescription("msg-description-1"), + asyncapi3test.WithContentType("application/json"), + asyncapi3test.WithPayload( + schematest.New("object", + schematest.WithProperty("foo", schematest.New("string")), + ), + ), + ) + + ch := asyncapi3test.NewChannel( + asyncapi3test.WithChannelTitle("title-1"), + asyncapi3test.WithChannelSummary("channel-1 summary"), + asyncapi3test.WithChannelDescription("description"), + asyncapi3test.UseMessage("foo", &asyncapi3.MessageRef{Value: msg}), + ) + + app := runtimetest.NewKafkaApp( + asyncapi3test.NewConfig( + asyncapi3test.WithInfo("foo", "", ""), + asyncapi3test.AddChannel("channel-1", ch), + asyncapi3test.WithOperation("publish", + asyncapi3test.WithOperationAction("send"), + asyncapi3test.WithOperationTitle("op-title-1"), + asyncapi3test.WithOperationSummary("op-summary-1"), + asyncapi3test.WithOperationDescription("op-description-1"), + asyncapi3test.WithOperationChannel(ch), + asyncapi3test.UseOperationMessage(msg), + ), + asyncapi3test.WithOperation("consume", + asyncapi3test.WithOperationAction("receive"), + asyncapi3test.WithOperationChannel(ch), + asyncapi3test.UseOperationMessage(msg), + ), + ), + ) + + _, err := app.Kafka.Get("foo").Store.Topic("channel-1").Partition(0).Write( + kafka.RecordBatch{ + Records: []*kafka.Record{ + { + Offset: 0, + Time: time.Time{}, + Key: kafka.NewBytes([]byte("foo")), + Value: kafka.NewBytes([]byte(`{"foo":"bar"}`)), + Headers: nil, + }, + }, + }, + ) + require.NoError(t, err) + + return app + }(), + test: func(t *testing.T, s *mcp.Service) { + r, err := s.GetRunResponse( + context.Background(), + mcp.RunInput{ + Code: `mokapi.getApi('foo').getTopic('channel-1').consume(0, 0, 10)`, + }, + ) + require.NoError(t, err) + require.IsType(t, []mcp.KafkaRecord{}, r.Result) + records := r.Result.([]mcp.KafkaRecord) + require.Len(t, records, 1) + require.Equal(t, int64(0), records[0].Offset) + require.Equal(t, "foo", records[0].Key) + require.Equal(t, `{"foo":"bar"}`, records[0].Value) + + r, err = s.GetRunResponse( + context.Background(), + mcp.RunInput{ + Code: `const topic = mokapi.getApi('foo').getTopic('channel-1') +const p0 = topic.partitions.find(p => p.index === 0); +let lastMessage = null +if (p0 && p0.offset > 0) { + lastMessage = topic.consume(0, p0.offset - 1, 1); +} +lastMessage +`, + }, + ) + require.NoError(t, err) + require.IsType(t, []mcp.KafkaRecord{}, r.Result) + records = r.Result.([]mcp.KafkaRecord) + require.Len(t, records, 1) + require.Equal(t, int64(0), records[0].Offset) + require.Equal(t, "foo", records[0].Key) + require.Equal(t, `{"foo":"bar"}`, records[0].Value) + }, + }, + { + name: "produce into topic", + app: func() *runtime.App { + msg := asyncapi3test.NewMessage( + asyncapi3test.WithMessageName("msg-name-1"), + asyncapi3test.WithMessageTitle("msg-title-1"), + asyncapi3test.WithMessageSummary("msg-summary-1"), + asyncapi3test.WithMessageDescription("msg-description-1"), + asyncapi3test.WithContentType("application/json"), + asyncapi3test.WithPayload( + schematest.New("object", + schematest.WithProperty("foo", schematest.New("string")), + ), + ), + ) + + ch := asyncapi3test.NewChannel( + asyncapi3test.WithChannelTitle("title-1"), + asyncapi3test.WithChannelSummary("channel-1 summary"), + asyncapi3test.WithChannelDescription("description"), + asyncapi3test.UseMessage("foo", &asyncapi3.MessageRef{Value: msg}), + ) + + return runtimetest.NewKafkaApp( + asyncapi3test.NewConfig( + asyncapi3test.WithInfo("foo", "", ""), + asyncapi3test.AddChannel("channel-1", ch), + asyncapi3test.WithOperation("publish", + asyncapi3test.WithOperationAction("send"), + asyncapi3test.WithOperationTitle("op-title-1"), + asyncapi3test.WithOperationSummary("op-summary-1"), + asyncapi3test.WithOperationDescription("op-description-1"), + asyncapi3test.WithOperationChannel(ch), + asyncapi3test.UseOperationMessage(msg), + ), + asyncapi3test.WithOperation("consume", + asyncapi3test.WithOperationAction("receive"), + asyncapi3test.WithOperationChannel(ch), + asyncapi3test.UseOperationMessage(msg), + ), + ), + ) + }(), + test: func(t *testing.T, s *mcp.Service) { + r, err := s.GetRunResponse( + context.Background(), + mcp.RunInput{ + Code: `const t = mokapi.getApi('foo').getTopic('channel-1') +t.produce(0, '{"foo":"bar"}', 'foo'); +t`, + }, + ) + require.NoError(t, err) + require.IsType(t, mcp.Topic{}, r.Result) + topic := r.Result.(mcp.Topic) + require.Len(t, topic.Partitions, 1) + require.Equal(t, int64(1), topic.Partitions[0].Offset) + }, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + generator.Seed(123456) + + s := mcp.NewService(tc.app) + tc.test(t, s) + }) + } +} diff --git a/mcp/run_test.go b/mcp/run_test.go new file mode 100644 index 000000000..2a84478eb --- /dev/null +++ b/mcp/run_test.go @@ -0,0 +1,94 @@ +package mcp_test + +import ( + "context" + "mokapi/mcp" + "mokapi/providers/openapi/openapitest" + "mokapi/runtime" + "mokapi/runtime/runtimetest" + "mokapi/schema/json/generator" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestService_Run(t *testing.T) { + testcases := []struct { + name string + app *runtime.App + test func(t *testing.T, s *mcp.Service) + }{ + { + name: "run JavaScript code", + app: runtimetest.NewApp(), + test: func(t *testing.T, s *mcp.Service) { + r, err := s.GetRunResponse( + context.Background(), + mcp.RunInput{ + Code: `1+1`, + }, + ) + require.NoError(t, err) + require.Equal(t, int64(2), r.Result) + }, + }, + { + name: "JSON.parse()", + app: runtimetest.NewApp(), + test: func(t *testing.T, s *mcp.Service) { + r, err := s.GetRunResponse( + context.Background(), + mcp.RunInput{ + Code: `JSON.parse('{"foo":"bar"}')`, + }, + ) + require.NoError(t, err) + require.Equal(t, map[string]any{"foo": "bar"}, r.Result) + }, + }, + { + name: "List APIs skip empty name", + app: runtimetest.NewHttpApp( + openapitest.NewConfig("3.1.0"), + ), + test: func(t *testing.T, s *mcp.Service) { + r, err := s.GetRunResponse( + context.Background(), + mcp.RunInput{ + Code: `mokapi.getApis()`, + }, + ) + require.NoError(t, err) + require.Len(t, r.Result, 0) + }, + }, + { + name: "script error", + app: runtimetest.NewApp(), + test: func(t *testing.T, s *mcp.Service) { + _, err := s.GetRunResponse( + context.Background(), + mcp.RunInput{ + Code: `okapi.getApis()`, + }, + ) + require.EqualError(t, err, `ReferenceError: okapi is not defined at mokapi_execute_code.js:1:1(0) + +Tip for Correction: +It seems there is a syntax error or a misunderstanding of the API. +To ensure you are using the correct global variables and methods: +1. Call 'mokapi_get_automation_definitions' without parameters to see the general overview. +2. Check 'category="core"' to verify the syntax of the global 'mokapi' object.`) + }, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + generator.Seed(123456) + + s := mcp.NewService(tc.app) + tc.test(t, s) + }) + } +} diff --git a/mcp/send_http_request.go b/mcp/send_http_request.go index 0ef86af8f..ec621a60c 100644 --- a/mcp/send_http_request.go +++ b/mcp/send_http_request.go @@ -32,7 +32,7 @@ func (s *Service) registerSendHttpRequest(server *mcp.Server) { "properties": map[string]any{ "apiName": map[string]any{ "type": "string", - "description": "The name of the API as returned by 'get_api_list'", + "description": "The name of the API as returned by 'mokapi_get_api_spec'", }, "method": map[string]any{ "type": "string", diff --git a/mcp/server.go b/mcp/server.go index 8945930ea..1c3f651ce 100644 --- a/mcp/server.go +++ b/mcp/server.go @@ -26,22 +26,28 @@ func NewServer(app *runtime.App) http.Handler { }, nil) svc := NewService(app) - svc.registerGetSpecTool(server) + //svc.registerGetSpecTool(server) - svc.registerGenerateHttpMockResponseTool(server) + //svc.registerGenerateHttpMockResponseTool(server) - svc.registerSendHttpRequest(server) - svc.registerProduceKafkaMessage(server) + //svc.registerSendHttpRequest(server) + //svc.registerProduceKafkaMessage(server) - svc.registerGetEvents(server) + //svc.registerGetEvents(server) - svc.registerGetMokapiTypeScriptApi(server) - svc.registerGetScenarios(server) - svc.registerGetHttpMockTemplate(server) + //svc.registerGetMokapiTypeScriptApi(server) + //svc.registerGetScenarios(server) + //svc.registerGetHttpMockTemplate(server) + + svc.registerRunTool(server) + svc.registerGetAutomationDefinitions(server) + svc.registerGetMockReference(server) + + addResources(server) return mcp.NewStreamableHTTPHandler( func(*http.Request) *mcp.Server { return server }, - &mcp.StreamableHTTPOptions{}, + &mcp.StreamableHTTPOptions{Stateless: true}, ) } diff --git a/mcp/server_test.go b/mcp/server_test.go index 30611f6a3..2365d04af 100644 --- a/mcp/server_test.go +++ b/mcp/server_test.go @@ -29,16 +29,71 @@ func TestServer(t *testing.T) { require.NotNil(t, session) defer func() { _ = session.Close() }() - list, err := session.ListTools(ctx, &gomcp.ListToolsParams{}) - require.NoError(t, err) - require.Len(t, list.Tools, 8) - // alphabetical order - require.Equal(t, "mokapi_generate_http_mock_response", list.Tools[0].Name) - require.Equal(t, "mokapi_get_api_spec", list.Tools[1].Name) - require.Equal(t, "mokapi_get_events", list.Tools[2].Name) - require.Equal(t, "mokapi_get_http_mock_template", list.Tools[3].Name) - require.Equal(t, "mokapi_get_scenarios", list.Tools[4].Name) - require.Equal(t, "mokapi_get_typescript_api", list.Tools[5].Name) - require.Equal(t, "mokapi_produce_kafka_message", list.Tools[6].Name) - require.Equal(t, "mokapi_send_http_request", list.Tools[7].Name) + testcases := []struct { + name string + test func(t *testing.T) + }{ + { + name: "test tool list", + test: func(t *testing.T) { + list, err := session.ListTools(ctx, &gomcp.ListToolsParams{}) + require.NoError(t, err) + require.Len(t, list.Tools, 3) + // alphabetical order + require.Equal(t, "mokapi_execute_code", list.Tools[0].Name) + require.Equal(t, "mokapi_get_automation_definitions", list.Tools[1].Name) + require.Equal(t, "mokapi_get_mock_reference", list.Tools[2].Name) + /*require.Equal(t, "mokapi_generate_http_mock_response", list.Tools[1].Name) + require.Equal(t, "mokapi_get_api_spec", list.Tools[2].Name) + require.Equal(t, "mokapi_get_events", list.Tools[3].Name) + require.Equal(t, "mokapi_get_http_mock_template", list.Tools[4].Name) + require.Equal(t, "mokapi_get_scenarios", list.Tools[5].Name) + require.Equal(t, "mokapi_get_typescript_api", list.Tools[6].Name) + require.Equal(t, "mokapi_produce_kafka_message", list.Tools[7].Name) + require.Equal(t, "mokapi_send_http_request", list.Tools[8].Name)*/ + }, + }, + { + name: "run code", + test: func(t *testing.T) { + r, err := session.CallTool(ctx, &gomcp.CallToolParams{ + Name: "mokapi_execute_code", + Arguments: mcp.RunInput{ + Code: "1+1", + }, + }) + require.NoError(t, err) + require.IsType(t, &gomcp.TextContent{}, r.Content[0]) + tc := r.Content[0].(*gomcp.TextContent) + require.Equal(t, `{"result":2}`, tc.Text) + }, + }, + { + name: "get mokapi_execute_code tool information", + test: func(t *testing.T) { + list, err := session.ListTools(ctx, &gomcp.ListToolsParams{ + Meta: nil, + Cursor: "", + }) + require.NoError(t, err) + require.Contains(t, list.Tools[0].Description, "mokapi_get_automation_definitions") + }, + }, + { + name: "get resource execute-types", + test: func(t *testing.T) { + list, err := session.ListResources(ctx, &gomcp.ListResourcesParams{}) + require.NoError(t, err) + require.Equal(t, "Automation API Reference", list.Resources[0].Name) + require.Equal(t, "mokapi://lib/automation", list.Resources[0].URI) + }, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + tc.test(t) + }) + } + } diff --git a/npm/go-mokapi/types/types.go b/npm/go-mokapi/types/types.go new file mode 100644 index 000000000..3f462e1c8 --- /dev/null +++ b/npm/go-mokapi/types/types.go @@ -0,0 +1,28 @@ +package types + +import ( + _ "embed" +) + +//go:embed index.d.ts +var index string + +//go:embed global.d.ts +var global string + +var Mokapi = global + "\n" + index + +//go:embed faker.d.ts +var Faker string + +//go:embed http.d.ts +var Http string + +//go:embed kafka.d.ts +var Kafka string + +//go:embed mustache.d.ts +var Mustache string + +//go:embed yaml.d.ts +var Yaml string diff --git a/pkg/cmd/mokapi/mokapi.go b/pkg/cmd/mokapi/mokapi.go index 630062c7c..fc392d6af 100644 --- a/pkg/cmd/mokapi/mokapi.go +++ b/pkg/cmd/mokapi/mokapi.go @@ -175,6 +175,7 @@ func createServer(cfg *static.Config) (*server.Server, error) { return nil, err } } + log.Infof("MCP server runs on binding :%d on path %s", cfg.Mcp.Server.Port, cfg.Mcp.Server.Path) } } diff --git a/pkg/cmd/mokapi/sample-data.go b/pkg/cmd/mokapi/sample-data.go index 017967aa2..2f6e5499f 100644 --- a/pkg/cmd/mokapi/sample-data.go +++ b/pkg/cmd/mokapi/sample-data.go @@ -223,7 +223,7 @@ func readFromConfig(c *dynamic.Config, inputType *string, r dynamic.Reader) (sch *inputType = "openapi" } - err = dynamic.Resolve(fmt.Sprintf("#%s", c.Info.Url.Fragment), &schema, c, r) + schema, err = (&dynamic.Reference[any]{Ref: fmt.Sprintf("#%s", c.Info.Url.Fragment)}).Resolve(c, r) if err != nil { return nil, nil, fmt.Errorf("failed to resolve fragment %s: %v", c.Info.Url.Fragment, err) } diff --git a/providers/asyncapi3/asyncapi3test/channel.go b/providers/asyncapi3/asyncapi3test/channel.go index bcdfc94b4..194f46e3b 100644 --- a/providers/asyncapi3/asyncapi3test/channel.go +++ b/providers/asyncapi3/asyncapi3test/channel.go @@ -43,6 +43,18 @@ func WithKafkaChannelBinding(bindings asyncapi3.TopicBindings) ChannelOptions { } } +func WithChannelTitle(title string) ChannelOptions { + return func(c *asyncapi3.Channel) { + c.Title = title + } +} + +func WithChannelSummary(summary string) ChannelOptions { + return func(c *asyncapi3.Channel) { + c.Summary = summary + } +} + func WithChannelDescription(desc string) ChannelOptions { return func(c *asyncapi3.Channel) { c.Description = desc @@ -51,7 +63,7 @@ func WithChannelDescription(desc string) ChannelOptions { func AssignToServer(ref string) ChannelOptions { return func(c *asyncapi3.Channel) { - c.Servers = append(c.Servers, &asyncapi3.ServerRef{Reference: dynamic.Reference{Ref: ref}}) + c.Servers = append(c.Servers, &asyncapi3.ServerRef{Reference: dynamic.Reference[*asyncapi3.ServerRef]{Ref: ref}}) } } diff --git a/providers/asyncapi3/asyncapi3test/message.go b/providers/asyncapi3/asyncapi3test/message.go index 3a1b1cd81..c5df43c55 100644 --- a/providers/asyncapi3/asyncapi3test/message.go +++ b/providers/asyncapi3/asyncapi3test/message.go @@ -72,3 +72,27 @@ func WithHeaders(s *schema.Schema) MessageOptions { m.Headers = &asyncapi3.SchemaRef{Value: s} } } + +func WithMessageName(s string) MessageOptions { + return func(m *asyncapi3.Message) { + m.Name = s + } +} + +func WithMessageTitle(s string) MessageOptions { + return func(m *asyncapi3.Message) { + m.Title = s + } +} + +func WithMessageSummary(s string) MessageOptions { + return func(m *asyncapi3.Message) { + m.Summary = s + } +} + +func WithMessageDescription(s string) MessageOptions { + return func(m *asyncapi3.Message) { + m.Description = s + } +} diff --git a/providers/asyncapi3/asyncapi3test/operation.go b/providers/asyncapi3/asyncapi3test/operation.go index 412693e77..d8dbf6222 100644 --- a/providers/asyncapi3/asyncapi3test/operation.go +++ b/providers/asyncapi3/asyncapi3test/operation.go @@ -20,6 +20,24 @@ func WithOperationAction(action string) OperationOptions { } } +func WithOperationTitle(title string) OperationOptions { + return func(op *asyncapi3.Operation) { + op.Title = title + } +} + +func WithOperationSummary(summary string) OperationOptions { + return func(op *asyncapi3.Operation) { + op.Summary = summary + } +} + +func WithOperationDescription(description string) OperationOptions { + return func(op *asyncapi3.Operation) { + op.Description = description + } +} + func UseOperationMessage(msg *asyncapi3.Message) OperationOptions { return func(o *asyncapi3.Operation) { o.Messages = append(o.Messages, &asyncapi3.MessageRef{Value: msg}) diff --git a/providers/asyncapi3/channel.go b/providers/asyncapi3/channel.go index bebfcd89b..ca0f69f30 100644 --- a/providers/asyncapi3/channel.go +++ b/providers/asyncapi3/channel.go @@ -7,7 +7,7 @@ import ( ) type ChannelRef struct { - dynamic.Reference + dynamic.Reference[*ChannelRef] Value *Channel } @@ -40,8 +40,8 @@ func (r *ChannelRef) Parse(config *dynamic.Config, reader dynamic.Reader) error return nil } if len(r.Ref) > 0 { - var resolved *ChannelRef - if err := dynamic.Resolve(r.Ref, &resolved, config, reader); err != nil { + resolved, err := r.Resolve(config, reader) + if err != nil { return err } r.Value = resolved.Value diff --git a/providers/asyncapi3/components_test.go b/providers/asyncapi3/components_test.go index 96523509f..b310c57ee 100644 --- a/providers/asyncapi3/components_test.go +++ b/providers/asyncapi3/components_test.go @@ -36,7 +36,7 @@ components: c := &dynamic.Config{Data: cfg, Info: dynamic.ConfigInfo{Url: try.MustUrl("/foo")}} err := cfg.Parse(c, &dynamictest.Reader{}) - require.EqualError(t, err, "resolve reference '#/components/servers/foo' failed: resolve reference 'test.yaml#/components/servers/foo' failed: TestReader: config not found") + require.EqualError(t, err, "resolve reference '#/components/servers/foo' failed: resolve reference '/test.yaml#/components/servers/foo' failed: TestReader: config not found") err = cfg.Parse(c, dynamictest.ReaderFunc(func(u *url.URL, v any) (*dynamic.Config, error) { require.Equal(t, "/test.yaml", u.String()) @@ -58,7 +58,7 @@ components: c := &dynamic.Config{Data: cfg, Info: dynamic.ConfigInfo{Url: try.MustUrl("/foo")}} err := cfg.Parse(c, &dynamictest.Reader{}) - require.EqualError(t, err, `resolve reference 'test.yaml#/components/tags/foo' failed: TestReader: config not found`) + require.EqualError(t, err, `resolve reference '/test.yaml#/components/tags/foo' failed: TestReader: config not found`) err = cfg.Parse(c, dynamictest.ReaderFunc(func(u *url.URL, v any) (*dynamic.Config, error) { require.Equal(t, "/test.yaml", u.String()) @@ -79,7 +79,7 @@ channels: c := &dynamic.Config{Data: cfg, Info: dynamic.ConfigInfo{Url: try.MustUrl("/foo")}} err := cfg.Parse(c, &dynamictest.Reader{}) - require.EqualError(t, err, `resolve reference 'test.yaml#/components/channels/foo' failed: TestReader: config not found`) + require.EqualError(t, err, `resolve reference '/test.yaml#/components/channels/foo' failed: TestReader: config not found`) err = cfg.Parse(c, dynamictest.ReaderFunc(func(u *url.URL, v any) (*dynamic.Config, error) { require.Equal(t, "/test.yaml", u.String()) @@ -107,7 +107,7 @@ components: c := &dynamic.Config{Data: cfg, Info: dynamic.ConfigInfo{Url: try.MustUrl("/foo")}} err := cfg.Parse(c, &dynamictest.Reader{}) - require.EqualError(t, err, "resolve reference '#/components/schemas/foo' failed: resolve reference 'test.yaml#/components/schemas/foo' failed: TestReader: config not found") + require.EqualError(t, err, "resolve reference '#/components/schemas/foo' failed: resolve reference '/test.yaml#/components/schemas/foo' failed: TestReader: config not found") err = cfg.Parse(c, dynamictest.ReaderFunc(func(u *url.URL, v any) (*dynamic.Config, error) { require.Equal(t, "/test.yaml", u.String()) @@ -134,14 +134,14 @@ components: c := &dynamic.Config{Data: cfg, Info: dynamic.ConfigInfo{Url: try.MustUrl("/foo")}} err := cfg.Parse(c, &dynamictest.Reader{}) - require.EqualError(t, err, "resolve reference '#/components/messages/foo' failed: resolve reference 'test.yaml#/components/messages/foo' failed: TestReader: config not found") + require.EqualError(t, err, "resolve reference '#/components/messages/foo' failed: resolve reference '/test.yaml#/components/messages/foo' failed: TestReader: config not found") err = cfg.Parse(c, dynamictest.ReaderFunc(func(u *url.URL, v any) (*dynamic.Config, error) { require.Equal(t, "/test.yaml", u.String()) return &dynamic.Config{Data: asyncapi3test.NewConfig(asyncapi3test.WithComponentMessage("foo", &asyncapi3.Message{Title: "FOO"}))}, nil })) require.NoError(t, err) - require.Equal(t, "FOO", cfg.Components.Messages["foo"].Value.Title) + require.Equal(t, "FOO", cfg.Channels["foo"].Value.Messages["msg"].Value.Title) }, }, { @@ -159,7 +159,7 @@ components: c := &dynamic.Config{Data: cfg, Info: dynamic.ConfigInfo{Url: try.MustUrl("/foo")}} err := cfg.Parse(c, &dynamictest.Reader{}) - require.EqualError(t, err, "resolve reference '#/components/operations/foo' failed: resolve reference 'test.yaml#/components/operations/foo' failed: TestReader: config not found") + require.EqualError(t, err, "resolve reference '#/components/operations/foo' failed: resolve reference '/test.yaml#/components/operations/foo' failed: TestReader: config not found") err = cfg.Parse(c, dynamictest.ReaderFunc(func(u *url.URL, v any) (*dynamic.Config, error) { require.Equal(t, "/test.yaml", u.String()) @@ -186,7 +186,7 @@ components: c := &dynamic.Config{Data: cfg, Info: dynamic.ConfigInfo{Url: try.MustUrl("/foo")}} err := cfg.Parse(c, &dynamictest.Reader{}) - require.EqualError(t, err, "resolve reference '#/components/parameters/foo' failed: resolve reference 'test.yaml#/components/parameters/foo' failed: TestReader: config not found") + require.EqualError(t, err, "resolve reference '#/components/parameters/foo' failed: resolve reference '/test.yaml#/components/parameters/foo' failed: TestReader: config not found") err = cfg.Parse(c, dynamictest.ReaderFunc(func(u *url.URL, v any) (*dynamic.Config, error) { require.Equal(t, "/test.yaml", u.String()) @@ -214,7 +214,7 @@ components: c := &dynamic.Config{Data: cfg, Info: dynamic.ConfigInfo{Url: try.MustUrl("/foo")}} err := cfg.Parse(c, &dynamictest.Reader{}) - require.EqualError(t, err, "resolve reference '#/components/correlationIds/foo' failed: resolve reference 'test.yaml#/components/correlationIds/foo' failed: TestReader: config not found") + require.EqualError(t, err, "resolve reference '#/components/correlationIds/foo' failed: resolve reference '/test.yaml#/components/correlationIds/foo' failed: TestReader: config not found") err = cfg.Parse(c, dynamictest.ReaderFunc(func(u *url.URL, v any) (*dynamic.Config, error) { require.Equal(t, "/test.yaml", u.String()) @@ -242,7 +242,7 @@ components: c := &dynamic.Config{Data: cfg, Info: dynamic.ConfigInfo{Url: try.MustUrl("/foo")}} err := cfg.Parse(c, &dynamictest.Reader{}) - require.EqualError(t, err, "resolve reference '#/components/externalDocs/foo' failed: resolve reference 'test.yaml#/components/externalDocs/foo' failed: TestReader: config not found") + require.EqualError(t, err, "resolve reference '#/components/externalDocs/foo' failed: resolve reference '/test.yaml#/components/externalDocs/foo' failed: TestReader: config not found") err = cfg.Parse(c, dynamictest.ReaderFunc(func(u *url.URL, v any) (*dynamic.Config, error) { require.Equal(t, "/test.yaml", u.String()) @@ -268,7 +268,7 @@ components: c := &dynamic.Config{Data: cfg, Info: dynamic.ConfigInfo{Url: try.MustUrl("/foo")}} err := cfg.Parse(c, &dynamictest.Reader{}) - require.EqualError(t, err, "resolve reference '#/components/operationTraits/foo' failed: resolve reference 'test.yaml#/components/operationTraits/foo' failed: TestReader: config not found") + require.EqualError(t, err, "resolve reference '#/components/operationTraits/foo' failed: resolve reference '/test.yaml#/components/operationTraits/foo' failed: TestReader: config not found") err = cfg.Parse(c, dynamictest.ReaderFunc(func(u *url.URL, v any) (*dynamic.Config, error) { require.Equal(t, "/test.yaml", u.String()) @@ -296,7 +296,7 @@ components: c := &dynamic.Config{Data: cfg, Info: dynamic.ConfigInfo{Url: try.MustUrl("/foo")}} err := cfg.Parse(c, &dynamictest.Reader{}) - require.EqualError(t, err, "resolve reference '#/components/messageTraits/foo' failed: resolve reference 'test.yaml#/components/messageTraits/foo' failed: TestReader: config not found") + require.EqualError(t, err, "resolve reference '#/components/messageTraits/foo' failed: resolve reference '/test.yaml#/components/messageTraits/foo' failed: TestReader: config not found") err = cfg.Parse(c, dynamictest.ReaderFunc(func(u *url.URL, v any) (*dynamic.Config, error) { require.Equal(t, "/test.yaml", u.String()) diff --git a/providers/asyncapi3/config_test.go b/providers/asyncapi3/config_test.go index d817e5905..51c9923b1 100644 --- a/providers/asyncapi3/config_test.go +++ b/providers/asyncapi3/config_test.go @@ -309,6 +309,7 @@ components: require.NotNil(t, ch) msg := ch.Value.Messages["msg"] require.NotNil(t, msg) + require.IsType(t, &schema.Schema{}, msg.Value.Payload.Value) s := msg.Value.Payload.Value.(*schema.Schema) require.Equal(t, "string", s.Type.String()) }, diff --git a/providers/asyncapi3/correlation_id.go b/providers/asyncapi3/correlation_id.go index e04110885..722f4a1da 100644 --- a/providers/asyncapi3/correlation_id.go +++ b/providers/asyncapi3/correlation_id.go @@ -7,7 +7,7 @@ import ( ) type CorrelationIdRef struct { - dynamic.Reference + dynamic.Reference[*CorrelationIdRef] Value *CorrelationId } @@ -18,11 +18,12 @@ type CorrelationId struct { func (r *CorrelationIdRef) Parse(config *dynamic.Config, reader dynamic.Reader) error { if len(r.Ref) > 0 { - var resolved *CorrelationIdRef - if err := dynamic.Resolve(r.Ref, &resolved, config, reader); err != nil { + resolved, err := r.Resolve(config, reader) + if err != nil { return err } r.Value = resolved.Value + return nil } return nil } diff --git a/providers/asyncapi3/external_doc.go b/providers/asyncapi3/external_doc.go index d140cf27b..ae41dd8d5 100644 --- a/providers/asyncapi3/external_doc.go +++ b/providers/asyncapi3/external_doc.go @@ -7,7 +7,7 @@ import ( ) type ExternalDocRef struct { - dynamic.Reference + dynamic.Reference[*ExternalDocRef] Value *ExternalDoc } @@ -26,11 +26,12 @@ func (r *ExternalDocRef) UnmarshalJSON(b []byte) error { func (r *ExternalDocRef) Parse(config *dynamic.Config, reader dynamic.Reader) error { if len(r.Ref) > 0 { - var resolved *ExternalDocRef - if err := dynamic.Resolve(r.Ref, &resolved, config, reader); err != nil { + resolved, err := r.Resolve(config, reader) + if err != nil { return err } r.Value = resolved.Value + return nil } return nil diff --git a/providers/asyncapi3/kafka/store/partition.go b/providers/asyncapi3/kafka/store/partition.go index 83a13bc47..6a90b65b0 100644 --- a/providers/asyncapi3/kafka/store/partition.go +++ b/providers/asyncapi3/kafka/store/partition.go @@ -115,8 +115,8 @@ func (p *Partition) Read(offset int64, maxBytes int) (kafka.RecordBatch, kafka.E return batch, kafka.None } - for seg.contains(offset) { - r := seg.record(offset) + for seg.Contains(offset) { + r := seg.Record(offset) if baseOffset == 0 { baseOffset = r.Offset @@ -281,7 +281,7 @@ func (p *Partition) OffsetTimestamp(offset int64) int64 { if s == nil { return -1 } - r := s.record(offset) + r := s.Record(offset) if r == nil { return -1 } @@ -356,11 +356,11 @@ func newSegment(offset int64) *Segment { } } -func (s *Segment) contains(offset int64) bool { +func (s *Segment) Contains(offset int64) bool { return offset >= s.Head && offset < s.Tail } -func (s *Segment) record(offset int64) *kafka.Record { +func (s *Segment) Record(offset int64) *kafka.Record { index := int(offset - s.Head) if index < 0 || index >= len(s.Log) { return nil diff --git a/providers/asyncapi3/kafka/store/partition_test.go b/providers/asyncapi3/kafka/store/partition_test.go index b43cb61db..cad87a87a 100644 --- a/providers/asyncapi3/kafka/store/partition_test.go +++ b/providers/asyncapi3/kafka/store/partition_test.go @@ -210,7 +210,7 @@ func TestPartition_Write_Value_Validator(t *testing.T) { require.Equal(t, int64(0), wr.BaseOffset) require.Equal(t, int64(1), p.Offset()) require.Equal(t, int64(0), p.StartOffset()) - r := p.Segments[p.ActiveSegment].record(0) + r := p.Segments[p.ActiveSegment].Record(0) require.NotNil(t, r) require.Len(t, r.Headers, 1) require.Equal(t, "bar-1", r.Headers[0].Key) diff --git a/providers/asyncapi3/message.go b/providers/asyncapi3/message.go index a626b2d0b..8b57972b7 100644 --- a/providers/asyncapi3/message.go +++ b/providers/asyncapi3/message.go @@ -8,7 +8,7 @@ import ( ) type MessageRef struct { - dynamic.Reference + dynamic.Reference[*MessageRef] Value *Message } @@ -33,7 +33,7 @@ type Message struct { } type MessageTraitRef struct { - dynamic.Reference + dynamic.Reference[*MessageTraitRef] Value *MessageTrait } @@ -75,8 +75,8 @@ func (r *MessageRef) Parse(config *dynamic.Config, reader dynamic.Reader) error return nil } if r.Ref != "" { - var resolved *MessageRef - if err := dynamic.Resolve(r.Ref, &resolved, config, reader); err != nil { + resolved, err := r.Resolve(config, reader) + if err != nil { return err } r.Value = resolved.Value @@ -144,8 +144,8 @@ func (m *Message) Parse(config *dynamic.Config, reader dynamic.Reader) error { func (r *MessageTraitRef) Parse(config *dynamic.Config, reader dynamic.Reader) error { if len(r.Ref) > 0 { - var resolved *MessageTraitRef - if err := dynamic.Resolve(r.Ref, &resolved, config, reader); err != nil { + resolved, err := r.Resolve(config, reader) + if err != nil { return err } r.Value = resolved.Value diff --git a/providers/asyncapi3/operation.go b/providers/asyncapi3/operation.go index 09ada4a33..9599e9ee2 100644 --- a/providers/asyncapi3/operation.go +++ b/providers/asyncapi3/operation.go @@ -7,7 +7,7 @@ import ( ) type OperationRef struct { - dynamic.Reference + dynamic.Reference[*OperationRef] Value *Operation } @@ -25,7 +25,7 @@ type Operation struct { } type OperationTraitRef struct { - dynamic.Reference + dynamic.Reference[*OperationTraitRef] Value *OperationTrait } @@ -61,8 +61,8 @@ func (r *OperationRef) Parse(config *dynamic.Config, reader dynamic.Reader) erro } if len(r.Ref) > 0 { - var resolved *OperationRef - if err := dynamic.Resolve(r.Ref, &resolved, config, reader); err != nil { + resolved, err := r.Resolve(config, reader) + if err != nil { return err } r.Value = resolved.Value @@ -77,8 +77,9 @@ func (o *Operation) Parse(config *dynamic.Config, reader dynamic.Reader) error { } if len(o.Channel.Ref) > 0 { - var resolved *ChannelRef - if err := dynamic.Resolve(o.Channel.Ref, &resolved, config, reader); err != nil { + r := dynamic.Reference[ChannelRef]{Ref: o.Channel.Ref} + resolved, err := r.Resolve(config, reader) + if err != nil { return err } o.Channel.Value = resolved.Value @@ -102,8 +103,8 @@ func (o *Operation) Parse(config *dynamic.Config, reader dynamic.Reader) error { func (r *OperationTraitRef) Parse(config *dynamic.Config, reader dynamic.Reader) error { if len(r.Ref) > 0 { - var resolved *OperationTraitRef - if err := dynamic.Resolve(r.Ref, &resolved, config, reader); err != nil { + resolved, err := r.Resolve(config, reader) + if err != nil { return err } r.Value = resolved.Value diff --git a/providers/asyncapi3/parameter.go b/providers/asyncapi3/parameter.go index df41dc09e..ade8cab25 100644 --- a/providers/asyncapi3/parameter.go +++ b/providers/asyncapi3/parameter.go @@ -7,7 +7,7 @@ import ( ) type ParameterRef struct { - dynamic.Reference + dynamic.Reference[*ParameterRef] Value *Parameter } @@ -29,11 +29,12 @@ func (r *ParameterRef) UnmarshalJSON(b []byte) error { func (r *ParameterRef) Parse(config *dynamic.Config, reader dynamic.Reader) error { if len(r.Ref) > 0 { - var resolved *ParameterRef - if err := dynamic.Resolve(r.Ref, &resolved, config, reader); err != nil { + resolved, err := r.Resolve(config, reader) + if err != nil { return err } r.Value = resolved.Value + return nil } return nil diff --git a/providers/asyncapi3/schema.go b/providers/asyncapi3/schema.go index 1f5f2bee3..f66c3e568 100644 --- a/providers/asyncapi3/schema.go +++ b/providers/asyncapi3/schema.go @@ -17,7 +17,7 @@ import ( ) type SchemaRef struct { - dynamic.Reference + dynamic.Reference[*SchemaRef] Value Schema } @@ -78,15 +78,13 @@ func (r *SchemaRef) UnmarshalJSON(b []byte) error { func (r *SchemaRef) Parse(config *dynamic.Config, reader dynamic.Reader) error { if len(r.Ref) > 0 { - var resolved *SchemaRef - err := dynamic.Resolve(r.Ref, &resolved, config, reader) + resolved, err := r.Resolve(config, reader) if err != nil { - s := &SchemaRef{Value: &jsonSchema.Schema{}} - err = dynamic.Resolve(r.Ref, &s.Value, config, reader) + ra := dynamic.Reference[Schema]{Ref: r.Ref} + r.Value, err = ra.Resolve(config, reader) if err != nil { return err } - r.Value = s.Value } else { r.Value = resolved.Value } @@ -129,7 +127,7 @@ func (r *SchemaRef) GetSchema() (Schema, error) { case *jsonSchema.Schema, *avro.Schema, *openapi.Schema: return s, nil case *SchemaRef: - return r.Value, nil + return s.GetSchema() case *MultiSchemaFormat: return s.Schema.GetSchema() default: @@ -380,12 +378,16 @@ func isOpenApi(format string) bool { type AvroRef struct { *avro.Schema - dynamic.Reference + dynamic.Reference[*AvroRef] } func (r *AvroRef) Parse(config *dynamic.Config, reader dynamic.Reader) error { if r.Ref != "" { - return dynamic.Resolve(r.Ref, &r.Schema, config, reader) + resolved, err := r.Resolve(config, reader) + if err != nil { + return err + } + r.Schema = resolved.Schema } return nil } diff --git a/providers/asyncapi3/server.go b/providers/asyncapi3/server.go index e68ab2de7..60539969b 100644 --- a/providers/asyncapi3/server.go +++ b/providers/asyncapi3/server.go @@ -7,7 +7,7 @@ import ( ) type ServerRef struct { - dynamic.Reference + dynamic.Reference[*ServerRef] Value *Server } @@ -26,7 +26,7 @@ type Server struct { } type ServerVariableRef struct { - dynamic.Reference + dynamic.Reference[ServerVariableRef] Value *ServerVariable } @@ -39,8 +39,8 @@ type ServerVariable struct { func (r *ServerRef) Parse(config *dynamic.Config, reader dynamic.Reader) error { if len(r.Ref) > 0 { - var resolved *ServerRef - if err := dynamic.Resolve(r.Ref, &resolved, config, reader); err != nil { + resolved, err := r.Resolve(config, reader) + if err != nil { return err } r.Value = resolved.Value @@ -80,7 +80,12 @@ func (r *ServerRef) Parse(config *dynamic.Config, reader dynamic.Reader) error { func (r *ServerVariableRef) parse(config *dynamic.Config, reader dynamic.Reader) error { if len(r.Ref) > 0 { - return dynamic.Resolve(r.Ref, &r.Value, config, reader) + resolved, err := r.Resolve(config, reader) + if err != nil { + return err + } + r.Value = resolved.Value + return nil } return nil diff --git a/providers/asyncapi3/tags.go b/providers/asyncapi3/tags.go index 252722bf3..4c5067d68 100644 --- a/providers/asyncapi3/tags.go +++ b/providers/asyncapi3/tags.go @@ -7,7 +7,7 @@ import ( ) type TagRef struct { - dynamic.Reference + dynamic.Reference[*TagRef] Value *Tag } @@ -27,8 +27,8 @@ func (r *TagRef) UnmarshalJSON(b []byte) error { func (r *TagRef) parse(config *dynamic.Config, reader dynamic.Reader) error { if len(r.Ref) > 0 { - var resolved *TagRef - if err := dynamic.Resolve(r.Ref, &resolved, config, reader); err != nil { + resolved, err := r.Resolve(config, reader) + if err != nil { return err } r.Value = resolved.Value diff --git a/providers/directory/config_ldif_test.go b/providers/directory/config_ldif_test.go index d11b5dbe0..0b3e9c365 100644 --- a/providers/directory/config_ldif_test.go +++ b/providers/directory/config_ldif_test.go @@ -110,6 +110,18 @@ func TestLdif_Parse(t *testing.T) { }, ld.Records[0]) }, }, + { + name: "comment", + input: "dn: dc=mokapi, dc=io\n# line 1\n line 2\nfoo: bar", + test: func(t *testing.T, ld *Ldif, err error) { + require.NoError(t, err) + require.Len(t, ld.Records, 1) + require.Equal(t, &AddRecord{ + Dn: "dc=mokapi, dc=io", + Attributes: map[string][]string{"foo": {"bar"}}, + }, ld.Records[0]) + }, + }, { name: "version set", input: "version: 1\ndn: dc=mokapi, dc=io", diff --git a/providers/directory/schema_test.go b/providers/directory/schema_test.go index 37f860f08..3e70d581e 100644 --- a/providers/directory/schema_test.go +++ b/providers/directory/schema_test.go @@ -1,8 +1,9 @@ package directory import ( - "github.com/stretchr/testify/require" "testing" + + "github.com/stretchr/testify/require" ) func TestAttributeType(t *testing.T) { @@ -111,6 +112,20 @@ func TestObjectClass(t *testing.T) { require.Equal(t, []string{"description"}, class.May) }, }, + { + name: "parentheses are not mandatory when MUST and MAY are only followed by one oid for objectClasses", + input: "( 1.3.6.1.4.1.99999.1.1 NAME 'customPerson' SUP ( inetOrgPerson $ device ) STRUCTURAL MUST customID MAY description )", + test: func(t *testing.T, class *ObjectClass, err error) { + require.NoError(t, err) + require.Equal(t, "1.3.6.1.4.1.99999.1.1", class.Id) + require.Equal(t, []string{"customPerson"}, class.Name) + require.Equal(t, "", class.Description) + require.Equal(t, []string{"inetOrgPerson", "device"}, class.SuperClass) + require.Equal(t, "STRUCTURAL", class.Type) + require.Equal(t, []string{"customID"}, class.Must) + require.Equal(t, []string{"description"}, class.May) + }, + }, } t.Parallel() diff --git a/providers/directory/search_test.go b/providers/directory/search_test.go index 614f16483..807fd4881 100644 --- a/providers/directory/search_test.go +++ b/providers/directory/search_test.go @@ -431,6 +431,32 @@ attributeTypes: ( 1.2.3.4.5.6.7.8 NAME 'objectSid' DESC 'objectSid' EQUALITY act require.Equal(t, "ldap: filter syntax error: invalid SID 'S-1-5-21-foo-1234567890-1234567890-1001': invalid uint value 'foo' at position: 3", log.Entries[1].Message) }, }, + { + name: "no EQUALITY specified", + input: `{ "files": [ "./schema.ldif", "./users.ldif" ] }`, + reader: &dynamictest.Reader{Data: map[string]*dynamic.Config{ + "file:/schema.ldif": {Raw: []byte(` +dn: +subschemaSubentry: cn=schema + +dn: cn=schema +attributeTypes: ( 2.5.4.3 NAME 'cn' DESC 'Common Name' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE ) +`)}, + "file:/users.ldif": {Raw: []byte("dn: cn=user\ncn: UsEr")}, + }}, + test: func(t *testing.T, h ldap.Handler, _ *test.Hook, err error) { + require.NoError(t, err) + + rr := ldaptest.NewRecorder() + h.ServeLDAP(rr, ldaptest.NewRequest(0, &ldap.SearchRequest{ + Scope: ldap.ScopeWholeSubtree, + Filter: "(cn=user)", + })) + res := rr.Message.(*ldap.SearchResponse) + + require.Len(t, res.Results, 1) + }, + }, } for _, tc := range testcases { @@ -609,6 +635,37 @@ func TestSearch(t *testing.T) { require.Len(t, res.Results, 1) }, }, + { + name: "scope whole subtree", + input: `{ "files": [ "./users.ldif" ] }`, + reader: &dynamictest.Reader{Data: map[string]*dynamic.Config{ + "file:/users.ldif": {Raw: []byte(` +dn: cn=user + +dn: id=user1,ou=Sales,dc=example,dc=com +foo: bar + +dn: id=user2,ou=Sales,dc=example,dc=com +foo: bar + +dn: id=user3,ou=Accounting,dc=example,dc=com +foo: bar +`)}, + }}, + test: func(t *testing.T, h ldap.Handler, err error) { + require.NoError(t, err) + + rr := ldaptest.NewRecorder() + h.ServeLDAP(rr, ldaptest.NewRequest(0, &ldap.SearchRequest{ + Scope: ldap.ScopeWholeSubtree, + BaseDN: "ou=Sales,dc=example,dc=com", + Filter: "(foo=bar)", + })) + res := rr.Message.(*ldap.SearchResponse) + + require.Len(t, res.Results, 2) + }, + }, } t.Parallel() diff --git a/providers/openapi/components_test.go b/providers/openapi/components_test.go index 752353a41..4a8c9d932 100644 --- a/providers/openapi/components_test.go +++ b/providers/openapi/components_test.go @@ -249,7 +249,7 @@ func TestComponents_Parse(t *testing.T) { ), ) err := config.Parse(&dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: config}, reader) - require.EqualError(t, err, "parse path '/foo' failed: parse operation 'GET' failed: parse response '200' failed: parse content '' failed: parse schema failed: resolve reference 'foo.yml#/components/schemas/foo' failed: TESTING ERROR") + require.EqualError(t, err, "parse path '/foo' failed: parse operation 'GET' failed: parse response '200' failed: parse content '' failed: parse schema failed: resolve reference '/foo.yml#/components/schemas/foo' failed: TESTING ERROR") }, }, { @@ -301,7 +301,7 @@ func TestComponents_Parse(t *testing.T) { ), ) err := config.Parse(&dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: config}, reader) - require.EqualError(t, err, "parse path '/foo' failed: parse operation 'GET' failed: parse response '200' failed: resolve reference 'foo.yml#/components/responses/foo' failed: TESTING ERROR") + require.EqualError(t, err, "parse path '/foo' failed: parse operation 'GET' failed: parse response '200' failed: resolve reference '/foo.yml#/components/responses/foo' failed: TESTING ERROR") }, }, { @@ -347,7 +347,7 @@ func TestComponents_Parse(t *testing.T) { ), ) err := config.Parse(&dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: config}, reader) - require.EqualError(t, err, "parse path '/foo' failed: parse operation 'GET' failed: parse request body failed: resolve reference 'foo.yml#/components/requestBodies/foo' failed: TESTING ERROR") + require.EqualError(t, err, "parse path '/foo' failed: parse operation 'GET' failed: parse request body failed: resolve reference '/foo.yml#/components/requestBodies/foo' failed: TESTING ERROR") }, }, { @@ -390,7 +390,7 @@ func TestComponents_Parse(t *testing.T) { ), ) err := config.Parse(&dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: config}, reader) - require.EqualError(t, err, "parse path '/foo' failed: parse parameter '0' failed: resolve reference 'foo.yml#/components/parameters/foo' failed: TESTING ERROR") + require.EqualError(t, err, "parse path '/foo' failed: parse parameter '0' failed: resolve reference '/foo.yml#/components/parameters/foo' failed: TESTING ERROR") }, }, { @@ -446,7 +446,7 @@ func TestComponents_Parse(t *testing.T) { ), ) err := config.Parse(&dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: config}, reader) - require.EqualError(t, err, "parse path '/foo' failed: parse operation 'GET' failed: parse response '200' failed: parse content 'application/json' failed: parse example 'foo' failed: resolve reference 'foo.yml#/components/parameters/foo' failed: TESTING ERROR") + require.EqualError(t, err, "parse path '/foo' failed: parse operation 'GET' failed: parse response '200' failed: parse content 'application/json' failed: parse example 'foo' failed: resolve reference '/foo.yml#/components/parameters/foo' failed: TESTING ERROR") }, }, { @@ -497,7 +497,7 @@ func TestComponents_Parse(t *testing.T) { ), ) err := config.Parse(&dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: config}, reader) - require.EqualError(t, err, "parse path '/foo' failed: parse operation 'GET' failed: parse response '200' failed: parse header 'foo' failed: resolve reference 'foo.yml#/components/headers/foo' failed: TESTING ERROR") + require.EqualError(t, err, "parse path '/foo' failed: parse operation 'GET' failed: parse response '200' failed: parse header 'foo' failed: resolve reference '/foo.yml#/components/headers/foo' failed: TESTING ERROR") }, }, } diff --git a/providers/openapi/example.go b/providers/openapi/example.go index a7f691fc2..03680515b 100644 --- a/providers/openapi/example.go +++ b/providers/openapi/example.go @@ -15,7 +15,7 @@ type ExampleValue struct { type Examples map[string]*ExampleRef type ExampleRef struct { - dynamic.Reference + dynamic.Reference[*ExampleRef] Value *Example } @@ -89,8 +89,8 @@ func (r *ExampleRef) Parse(config *dynamic.Config, reader dynamic.Reader) error } if len(r.Ref) > 0 { - var resolved *ExampleRef - if err := dynamic.Resolve(r.Ref, &resolved, config, reader); err != nil { + resolved, err := r.Resolve(config, reader) + if err != nil { return err } r.Value = resolved.Value @@ -106,7 +106,13 @@ func (e *Example) Parse(config *dynamic.Config, reader dynamic.Reader) error { } if e.ExternalValue != "" { - return dynamic.Resolve(e.ExternalValue, &e.Value, config, reader) + r := dynamic.Reference[string]{Ref: e.ExternalValue} + resolved, err := r.Resolve(config, reader) + if err != nil { + return err + } + e.Value = resolved + return nil } return nil diff --git a/providers/openapi/example_test.go b/providers/openapi/example_test.go index be4f329d9..ab9f6d492 100644 --- a/providers/openapi/example_test.go +++ b/providers/openapi/example_test.go @@ -195,14 +195,14 @@ func TestExample_Parse(t *testing.T) { openapitest.WithOperation(http.MethodGet, openapitest.WithResponse(http.StatusOK, openapitest.UseContent("application/json", &openapi.MediaType{ - Examples: map[string]*openapi.ExampleRef{"foo": {Reference: dynamic.Reference{Ref: "foo.yml"}}}, + Examples: map[string]*openapi.ExampleRef{"foo": {Reference: dynamic.Reference[*openapi.ExampleRef]{Ref: "foo.yml"}}}, }), ), ), ), ) err := config.Parse(&dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: config}, reader) - require.EqualError(t, err, "parse path '/foo' failed: parse operation 'GET' failed: parse response '200' failed: parse content 'application/json' failed: parse example 'foo' failed: resolve reference 'foo.yml' failed: TEST ERROR") + require.EqualError(t, err, "parse path '/foo' failed: parse operation 'GET' failed: parse response '200' failed: parse content 'application/json' failed: parse example 'foo' failed: resolve reference '/foo.yml' failed: TEST ERROR") }, }, { diff --git a/providers/openapi/header.go b/providers/openapi/header.go index 76c8808e8..26419973a 100644 --- a/providers/openapi/header.go +++ b/providers/openapi/header.go @@ -15,7 +15,7 @@ import ( type Headers map[string]*HeaderRef type HeaderRef struct { - dynamic.Reference + dynamic.Reference[*HeaderRef] Value *Header } @@ -71,8 +71,8 @@ func (r *HeaderRef) Parse(config *dynamic.Config, reader dynamic.Reader) error { } if len(r.Ref) > 0 { - var resolved *HeaderRef - if err := dynamic.Resolve(r.Ref, &resolved, config, reader); err != nil { + resolved, err := r.Resolve(config, reader) + if err != nil { return err } r.Value = resolved.Value diff --git a/providers/openapi/header_test.go b/providers/openapi/header_test.go index e6b15162c..79f662d1c 100644 --- a/providers/openapi/header_test.go +++ b/providers/openapi/header_test.go @@ -288,7 +288,7 @@ func TestHeader_Parse(t *testing.T) { ), ) err := config.Parse(&dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: config}, reader) - require.EqualError(t, err, "parse path '/foo' failed: parse operation 'GET' failed: parse response '200' failed: parse header 'foo' failed: resolve reference 'foo' failed: TEST ERROR") + require.EqualError(t, err, "parse path '/foo' failed: parse operation 'GET' failed: parse response '200' failed: parse header 'foo' failed: resolve reference '/foo' failed: TEST ERROR") }, }, } diff --git a/providers/openapi/info.go b/providers/openapi/info.go index c00d999e1..ece13edfd 100644 --- a/providers/openapi/info.go +++ b/providers/openapi/info.go @@ -4,6 +4,8 @@ type Info struct { // The title of the service Name string `yaml:"title" json:"title"` + Summary string `yaml:"summary" json:"summary"` + // A short description of the API. CommonMark syntax MAY be // used for rich text representation. Description string `yaml:"description,omitempty" json:"description,omitempty"` diff --git a/providers/openapi/media_type_test.go b/providers/openapi/media_type_test.go index c86974cec..6770e4771 100644 --- a/providers/openapi/media_type_test.go +++ b/providers/openapi/media_type_test.go @@ -158,7 +158,7 @@ func TestMediaType_Parse(t *testing.T) { ), ) err := config.Parse(&dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: config}, reader) - require.EqualError(t, err, "parse path '/foo' failed: parse operation 'GET' failed: parse response '200' failed: parse content 'application/json' failed: parse schema failed: resolve reference 'foo.yml' failed: TEST ERROR") + require.EqualError(t, err, "parse path '/foo' failed: parse operation 'GET' failed: parse response '200' failed: parse content 'application/json' failed: parse schema failed: resolve reference '/foo.yml' failed: TEST ERROR") }, }, { @@ -172,14 +172,14 @@ func TestMediaType_Parse(t *testing.T) { openapitest.WithOperation(http.MethodGet, openapitest.WithResponse(http.StatusOK, openapitest.UseContent("application/json", - &openapi.MediaType{Examples: map[string]*openapi.ExampleRef{"foo": {Reference: dynamic.Reference{Ref: "foo.yml"}}}}, + &openapi.MediaType{Examples: map[string]*openapi.ExampleRef{"foo": {Reference: dynamic.Reference[*openapi.ExampleRef]{Ref: "foo.yml"}}}}, ), ), ), ), ) err := config.Parse(&dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: config}, reader) - require.EqualError(t, err, "parse path '/foo' failed: parse operation 'GET' failed: parse response '200' failed: parse content 'application/json' failed: parse example 'foo' failed: resolve reference 'foo.yml' failed: TEST ERROR") + require.EqualError(t, err, "parse path '/foo' failed: parse operation 'GET' failed: parse response '200' failed: parse content 'application/json' failed: parse example 'foo' failed: resolve reference '/foo.yml' failed: TEST ERROR") }, }, } diff --git a/providers/openapi/openapitest/config.go b/providers/openapi/openapitest/config.go index 7e6bd5493..01867547d 100644 --- a/providers/openapi/openapitest/config.go +++ b/providers/openapi/openapitest/config.go @@ -29,6 +29,12 @@ func WithInfo(name, version, description string) ConfigOptions { } } +func WithSummary(summary string) ConfigOptions { + return func(c *openapi.Config) { + c.Info.Summary = summary + } +} + func WithContact(name, url, email string) ConfigOptions { return func(c *openapi.Config) { c.Info.Contact = &openapi.Contact{ diff --git a/providers/openapi/openapitest/operation.go b/providers/openapi/openapitest/operation.go index 8defe7640..5e11eaa6c 100644 --- a/providers/openapi/openapitest/operation.go +++ b/providers/openapi/openapitest/operation.go @@ -24,6 +24,12 @@ func WithOperationId(id string) OperationOptions { } } +func WithOperationSummary(summary string) OperationOptions { + return func(o *openapi.Operation) { + o.Summary = summary + } +} + func WithResponse(status int, opts ...ResponseOptions) OperationOptions { return func(o *openapi.Operation) { r := &openapi.Response{ @@ -46,7 +52,7 @@ func UseResponse(status int, r *openapi.Response) OperationOptions { func WithResponseRef(status int, ref string) OperationOptions { return func(o *openapi.Operation) { o.Responses.Set(strconv.Itoa(status), &openapi.ResponseRef{ - Reference: dynamic.Reference{Ref: ref}, + Reference: dynamic.Reference[*openapi.ResponseRef]{Ref: ref}, }) } } @@ -70,7 +76,7 @@ func WithOperationParam(name string, required bool, opts ...ParamOptions) Operat func WithOperationParamRef(ref string) OperationOptions { return func(o *openapi.Operation) { - o.Parameters = append(o.Parameters, &openapi.ParameterRef{Reference: dynamic.Reference{Ref: ref}}) + o.Parameters = append(o.Parameters, &openapi.ParameterRef{Reference: dynamic.Reference[*openapi.ParameterRef]{Ref: ref}}) } } @@ -128,7 +134,7 @@ func WithRequestBody(description string, required bool, opts ...RequestBodyOptio func WithRequestBodyRef(ref string) OperationOptions { return func(o *openapi.Operation) { o.RequestBody = &openapi.RequestBodyRef{ - Reference: dynamic.Reference{Ref: ref}, + Reference: dynamic.Reference[*openapi.RequestBodyRef]{Ref: ref}, } } } diff --git a/providers/openapi/openapitest/path.go b/providers/openapi/openapitest/path.go index 7eade336b..b266dd730 100644 --- a/providers/openapi/openapitest/path.go +++ b/providers/openapi/openapitest/path.go @@ -83,7 +83,7 @@ func WithPathParam(name string, opts ...ParamOptions) PathOptions { func WithPathParamRef(ref string) PathOptions { return func(e *openapi.Path) { e.Parameters = append(e.Parameters, &openapi.ParameterRef{ - Reference: dynamic.Reference{Ref: ref}, + Reference: dynamic.Reference[*openapi.ParameterRef]{Ref: ref}, }) } } diff --git a/providers/openapi/openapitest/response.go b/providers/openapi/openapitest/response.go index 8781a5be8..108b7c042 100644 --- a/providers/openapi/openapitest/response.go +++ b/providers/openapi/openapitest/response.go @@ -41,7 +41,7 @@ func WithResponseHeaderRef(name string, ref string) ResponseOptions { if o.Headers == nil { o.Headers = map[string]*openapi.HeaderRef{} } - o.Headers[name] = &openapi.HeaderRef{Reference: dynamic.Reference{Ref: ref}} + o.Headers[name] = &openapi.HeaderRef{Reference: dynamic.Reference[*openapi.HeaderRef]{Ref: ref}} } } @@ -112,7 +112,7 @@ func WithExampleRef(name, ref string) ContentOptions { if c.Examples == nil { c.Examples = map[string]*openapi.ExampleRef{} } - c.Examples[name] = &openapi.ExampleRef{Reference: dynamic.Reference{Ref: ref}} + c.Examples[name] = &openapi.ExampleRef{Reference: dynamic.Reference[*openapi.ExampleRef]{Ref: ref}} } } @@ -122,8 +122,8 @@ func WithSchema(s *schema.Schema) ContentOptions { } } -func WithSchemaRef(r string) ContentOptions { +func WithSchemaRef(ref string) ContentOptions { return func(c *openapi.MediaType) { - c.Schema = &schema.Schema{Ref: r} + c.Schema = &schema.Schema{Reference: dynamic.Reference[*schema.Schema]{Ref: ref}} } } diff --git a/providers/openapi/operation_test.go b/providers/openapi/operation_test.go index e4eb51271..7a2312c92 100644 --- a/providers/openapi/operation_test.go +++ b/providers/openapi/operation_test.go @@ -232,7 +232,7 @@ func TestOperation_Parse(t *testing.T) { openapitest.WithOperationParamRef("foo.yml"))), ) err := config.Parse(&dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: config}, reader) - require.EqualError(t, err, "parse path '/foo' failed: parse operation 'GET' failed: parse parameter index '0' failed: resolve reference 'foo.yml' failed: TEST ERROR") + require.EqualError(t, err, "parse path '/foo' failed: parse operation 'GET' failed: parse parameter index '0' failed: resolve reference '/foo.yml' failed: TEST ERROR") }, }, { @@ -264,7 +264,7 @@ func TestOperation_Parse(t *testing.T) { openapitest.WithOperationParamRef("foo.yml"))), ) err := config.Parse(&dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: config}, reader) - require.EqualError(t, err, "parse path '/foo' failed: parse operation 'POST' failed: parse parameter index '0' failed: resolve reference 'foo.yml' failed: TEST ERROR") + require.EqualError(t, err, "parse path '/foo' failed: parse operation 'POST' failed: parse parameter index '0' failed: resolve reference '/foo.yml' failed: TEST ERROR") }, }, { @@ -296,7 +296,7 @@ func TestOperation_Parse(t *testing.T) { openapitest.WithOperationParamRef("foo.yml"))), ) err := config.Parse(&dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: config}, reader) - require.EqualError(t, err, "parse path '/foo' failed: parse operation 'PUT' failed: parse parameter index '0' failed: resolve reference 'foo.yml' failed: TEST ERROR") + require.EqualError(t, err, "parse path '/foo' failed: parse operation 'PUT' failed: parse parameter index '0' failed: resolve reference '/foo.yml' failed: TEST ERROR") }, }, { @@ -328,7 +328,7 @@ func TestOperation_Parse(t *testing.T) { openapitest.WithOperationParamRef("foo.yml"))), ) err := config.Parse(&dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: config}, reader) - require.EqualError(t, err, "parse path '/foo' failed: parse operation 'PATCH' failed: parse parameter index '0' failed: resolve reference 'foo.yml' failed: TEST ERROR") + require.EqualError(t, err, "parse path '/foo' failed: parse operation 'PATCH' failed: parse parameter index '0' failed: resolve reference '/foo.yml' failed: TEST ERROR") }, }, { @@ -360,7 +360,7 @@ func TestOperation_Parse(t *testing.T) { openapitest.WithOperationParamRef("foo.yml"))), ) err := config.Parse(&dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: config}, reader) - require.EqualError(t, err, "parse path '/foo' failed: parse operation 'DELETE' failed: parse parameter index '0' failed: resolve reference 'foo.yml' failed: TEST ERROR") + require.EqualError(t, err, "parse path '/foo' failed: parse operation 'DELETE' failed: parse parameter index '0' failed: resolve reference '/foo.yml' failed: TEST ERROR") }, }, { @@ -392,7 +392,7 @@ func TestOperation_Parse(t *testing.T) { openapitest.WithOperationParamRef("foo.yml"))), ) err := config.Parse(&dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: config}, reader) - require.EqualError(t, err, "parse path '/foo' failed: parse operation 'HEAD' failed: parse parameter index '0' failed: resolve reference 'foo.yml' failed: TEST ERROR") + require.EqualError(t, err, "parse path '/foo' failed: parse operation 'HEAD' failed: parse parameter index '0' failed: resolve reference '/foo.yml' failed: TEST ERROR") }, }, { @@ -424,7 +424,7 @@ func TestOperation_Parse(t *testing.T) { openapitest.WithOperationParamRef("foo.yml"))), ) err := config.Parse(&dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: config}, reader) - require.EqualError(t, err, "parse path '/foo' failed: parse operation 'OPTIONS' failed: parse parameter index '0' failed: resolve reference 'foo.yml' failed: TEST ERROR") + require.EqualError(t, err, "parse path '/foo' failed: parse operation 'OPTIONS' failed: parse parameter index '0' failed: resolve reference '/foo.yml' failed: TEST ERROR") }, }, { @@ -456,7 +456,7 @@ func TestOperation_Parse(t *testing.T) { openapitest.WithOperationParamRef("foo.yml"))), ) err := config.Parse(&dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: config}, reader) - require.EqualError(t, err, "parse path '/foo' failed: parse operation 'TRACE' failed: parse parameter index '0' failed: resolve reference 'foo.yml' failed: TEST ERROR") + require.EqualError(t, err, "parse path '/foo' failed: parse operation 'TRACE' failed: parse parameter index '0' failed: resolve reference '/foo.yml' failed: TEST ERROR") }, }, { @@ -488,7 +488,7 @@ func TestOperation_Parse(t *testing.T) { openapitest.WithOperationParamRef("foo.yml"))), ) err := config.Parse(&dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: config}, reader) - require.EqualError(t, err, "parse path '/foo' failed: parse operation 'QUERY' failed: parse parameter index '0' failed: resolve reference 'foo.yml' failed: TEST ERROR") + require.EqualError(t, err, "parse path '/foo' failed: parse operation 'QUERY' failed: parse parameter index '0' failed: resolve reference '/foo.yml' failed: TEST ERROR") }, }, { @@ -520,7 +520,7 @@ func TestOperation_Parse(t *testing.T) { openapitest.WithOperationParamRef("foo.yml"))), ) err := config.Parse(&dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: config}, reader) - require.EqualError(t, err, "parse path '/foo' failed: parse operation 'LINK' failed: parse parameter index '0' failed: resolve reference 'foo.yml' failed: TEST ERROR") + require.EqualError(t, err, "parse path '/foo' failed: parse operation 'LINK' failed: parse parameter index '0' failed: resolve reference '/foo.yml' failed: TEST ERROR") }, }, { @@ -556,7 +556,7 @@ func TestOperation_Parse(t *testing.T) { config := openapitest.NewConfig("3.0", openapitest.WithPath("/foo", openapitest.UseOperation(http.MethodTrace, &openapi.Operation{ - RequestBody: &openapi.RequestBodyRef{Reference: dynamic.Reference{Ref: "foo.yml#/components/requestBodies/foo"}}, + RequestBody: &openapi.RequestBodyRef{Reference: dynamic.Reference[*openapi.RequestBodyRef]{Ref: "foo.yml#/components/requestBodies/foo"}}, }), ), ) @@ -575,12 +575,12 @@ func TestOperation_Parse(t *testing.T) { config := openapitest.NewConfig("3.0", openapitest.WithPath("/foo", openapitest.UseOperation(http.MethodTrace, &openapi.Operation{ - RequestBody: &openapi.RequestBodyRef{Reference: dynamic.Reference{Ref: "foo.yml#/components/requestBodies/foo"}}, + RequestBody: &openapi.RequestBodyRef{Reference: dynamic.Reference[*openapi.RequestBodyRef]{Ref: "foo.yml#/components/requestBodies/foo"}}, }), ), ) err := config.Parse(&dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: config}, reader) - require.EqualError(t, err, "parse path '/foo' failed: parse operation 'TRACE' failed: parse request body failed: resolve reference 'foo.yml#/components/requestBodies/foo' failed: TEST ERROR") + require.EqualError(t, err, "parse path '/foo' failed: parse operation 'TRACE' failed: parse request body failed: resolve reference '/foo.yml#/components/requestBodies/foo' failed: TEST ERROR") }, }, } diff --git a/providers/openapi/parameter.go b/providers/openapi/parameter.go index 43288ce21..6fc4fc3c4 100644 --- a/providers/openapi/parameter.go +++ b/providers/openapi/parameter.go @@ -15,7 +15,7 @@ const ( ) type ParameterRef struct { - dynamic.Reference + dynamic.Reference[*ParameterRef] Value *Parameter } @@ -102,8 +102,8 @@ func (r *ParameterRef) Parse(config *dynamic.Config, reader dynamic.Reader) erro } if len(r.Ref) > 0 && r.Value == nil { - var resolved *ParameterRef - if err := dynamic.Resolve(r.Ref, &resolved, config, reader); err != nil { + resolved, err := r.Resolve(config, reader) + if err != nil { return err } r.Value = resolved.Value diff --git a/providers/openapi/parameter_test.go b/providers/openapi/parameter_test.go index f99aaa505..0fe7e8c69 100644 --- a/providers/openapi/parameter_test.go +++ b/providers/openapi/parameter_test.go @@ -313,7 +313,7 @@ func TestParameterHeader_Parse(t *testing.T) { cfg := &dynamic.Config{Info: dynamic.ConfigInfo{Url: u}, Data: &openapi.ParameterRef{Value: &openapi.Parameter{Description: "foo"}}} return cfg, nil }) - param := openapi.Parameters{&openapi.ParameterRef{Reference: dynamic.Reference{Ref: "foo.yml"}}} + param := openapi.Parameters{&openapi.ParameterRef{Reference: dynamic.Reference[*openapi.ParameterRef]{Ref: "foo.yml"}}} err := param.Parse(&dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: param}, reader) require.NoError(t, err) require.Equal(t, "foo", param[0].Value.Description) @@ -326,7 +326,7 @@ func TestParameterHeader_Parse(t *testing.T) { cfg := &dynamic.Config{Info: dynamic.ConfigInfo{Url: u}, Data: schematest.New("string")} return cfg, nil }) - param := openapi.Parameters{&openapi.ParameterRef{Value: &openapi.Parameter{Schema: &schema.Schema{Ref: "foo.yml"}}}} + param := openapi.Parameters{&openapi.ParameterRef{Value: &openapi.Parameter{Schema: &schema.Schema{Reference: dynamic.Reference[*schema.Schema]{Ref: "foo.yml"}}}}} err := param.Parse(&dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: param}, reader) require.NoError(t, err) require.Equal(t, "string", param[0].Value.Schema.Type.String()) @@ -338,9 +338,9 @@ func TestParameterHeader_Parse(t *testing.T) { reader := dynamictest.ReaderFunc(func(_ *url.URL, _ any) (*dynamic.Config, error) { return nil, fmt.Errorf("TEST ERROR") }) - param := openapi.Parameters{&openapi.ParameterRef{Reference: dynamic.Reference{Ref: "foo.yml"}}} + param := openapi.Parameters{&openapi.ParameterRef{Reference: dynamic.Reference[*openapi.ParameterRef]{Ref: "foo.yml"}}} err := param.Parse(&dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: param}, reader) - require.EqualError(t, err, "parse parameter index '0' failed: resolve reference 'foo.yml' failed: TEST ERROR") + require.EqualError(t, err, "parse parameter index '0' failed: resolve reference '/foo.yml' failed: TEST ERROR") }, }, { @@ -349,9 +349,9 @@ func TestParameterHeader_Parse(t *testing.T) { reader := dynamictest.ReaderFunc(func(_ *url.URL, _ any) (*dynamic.Config, error) { return nil, fmt.Errorf("TEST ERROR") }) - param := openapi.Parameters{&openapi.ParameterRef{Value: &openapi.Parameter{Schema: &schema.Schema{Ref: "foo.yml"}}}} + param := openapi.Parameters{&openapi.ParameterRef{Value: &openapi.Parameter{Schema: &schema.Schema{Reference: dynamic.Reference[*schema.Schema]{Ref: "foo.yml"}}}}} err := param.Parse(&dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: param}, reader) - require.EqualError(t, err, "parse parameter index '0' failed: parse schema failed: resolve reference 'foo.yml' failed: TEST ERROR") + require.EqualError(t, err, "parse parameter index '0' failed: parse schema failed: resolve reference '/foo.yml' failed: TEST ERROR") }, }, } diff --git a/providers/openapi/parse_test.go b/providers/openapi/parse_test.go index 2c9ae2a10..6b6820663 100644 --- a/providers/openapi/parse_test.go +++ b/providers/openapi/parse_test.go @@ -25,7 +25,7 @@ func Test_Parse(t *testing.T) { openapitest.WithOperation("get", openapitest.WithResponse(200, openapitest.UseContent("application/json", &openapi.MediaType{ - Schema: &schema.Schema{Ref: "#/components/schemas/Foo"}, + Schema: &schema.Schema{Reference: dynamic.Reference[*schema.Schema]{Ref: "#/components/schemas/Foo"}}, }, ), ), @@ -74,7 +74,7 @@ func Test_ParseAndPatch(t *testing.T) { openapitest.WithOperation("get", openapitest.WithResponse(200, openapitest.UseContent("application/json", &openapi.MediaType{ - Schema: &schema.Schema{Ref: "#/components/schemas/Foo"}, + Schema: &schema.Schema{Reference: dynamic.Reference[*schema.Schema]{Ref: "#/components/schemas/Foo"}}, }, ), ), diff --git a/providers/openapi/path.go b/providers/openapi/path.go index 31b629e0e..c1ceaaf8a 100644 --- a/providers/openapi/path.go +++ b/providers/openapi/path.go @@ -13,7 +13,7 @@ import ( type PathItems map[string]*PathRef type PathRef struct { - dynamic.Reference + dynamic.Reference[*PathRef] Value *Path } @@ -175,8 +175,8 @@ func (r *PathRef) Parse(name string, config *dynamic.Config, reader dynamic.Read }() if len(r.Ref) > 0 { - var resolved *PathRef - if err := dynamic.Resolve(r.Ref, &resolved, config, reader); err != nil { + resolved, err := r.Resolve(config, reader) + if err != nil { return err } r.Value = resolved.Value diff --git a/providers/openapi/path_test.go b/providers/openapi/path_test.go index f8b4454da..4fedb4f72 100644 --- a/providers/openapi/path_test.go +++ b/providers/openapi/path_test.go @@ -429,9 +429,9 @@ func TestPath_Parse(t *testing.T) { }) config := openapitest.NewConfig("3.0", openapitest.WithPathRef("foo", - &openapi.PathRef{Reference: dynamic.Reference{Ref: "foo.yml#/paths/foo"}})) + &openapi.PathRef{Reference: dynamic.Reference[*openapi.PathRef]{Ref: "foo.yml#/paths/foo"}})) err := config.Parse(&dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: config}, reader) - require.EqualError(t, err, "parse path 'foo' failed: resolve reference 'foo.yml#/paths/foo' failed: TEST ERROR") + require.EqualError(t, err, "parse path 'foo' failed: resolve reference '/foo.yml#/paths/foo' failed: TEST ERROR") }, }, { @@ -460,7 +460,7 @@ func TestPath_Parse(t *testing.T) { }) config := openapitest.NewConfig("3.0", openapitest.WithPathRef("/foo", - &openapi.PathRef{Reference: dynamic.Reference{Ref: "foo.yml#/paths/foo"}})) + &openapi.PathRef{Reference: dynamic.Reference[*openapi.PathRef]{Ref: "foo.yml#/paths/foo"}})) err := config.Parse(&dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: config}, reader) require.NoError(t, err) require.Equal(t, target, config.Paths["/foo"].Value) @@ -479,7 +479,7 @@ func TestPath_Parse(t *testing.T) { }) config := openapitest.NewConfig("3.0", openapitest.WithPathRef("/foo", - &openapi.PathRef{Reference: dynamic.Reference{Ref: "foo.yml#/paths/foo"}})) + &openapi.PathRef{Reference: dynamic.Reference[*openapi.PathRef]{Ref: "foo.yml#/paths/foo"}})) err := config.Parse(&dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: config}, reader) require.NoError(t, err) require.Nil(t, config.Paths["/foo"].Value) @@ -499,9 +499,9 @@ func TestPath_Parse(t *testing.T) { }) config := openapitest.NewConfig("3.0", openapitest.WithPathRef("/foo", - &openapi.PathRef{Reference: dynamic.Reference{Ref: "foo.yml#/paths/foo"}})) + &openapi.PathRef{Reference: dynamic.Reference[*openapi.PathRef]{Ref: "foo.yml#/paths/foo"}})) err := config.Parse(&dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: config}, reader) - require.EqualError(t, err, "parse path '/foo' failed: resolve reference 'foo.yml#/paths/foo' failed: value is null") + require.EqualError(t, err, "parse path '/foo' failed: resolve reference '/foo.yml#/paths/foo' failed: value is null") }, }, { @@ -516,7 +516,7 @@ func TestPath_Parse(t *testing.T) { ), ) err := config.Parse(&dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: config}, reader) - require.EqualError(t, err, "parse path '/foo' failed: parse parameter '0' failed: resolve reference 'foo.yml' failed: TEST ERROR") + require.EqualError(t, err, "parse path '/foo' failed: parse parameter '0' failed: resolve reference '/foo.yml' failed: TEST ERROR") }, }, { @@ -527,7 +527,7 @@ func TestPath_Parse(t *testing.T) { }) config := openapitest.NewConfig("3.0", openapitest.WithPathRef("foo", - &openapi.PathRef{Reference: dynamic.Reference{Ref: "#/components/pathItems/foo"}}, + &openapi.PathRef{Reference: dynamic.Reference[*openapi.PathRef]{Ref: "#/components/pathItems/foo"}}, ), openapitest.WithComponentPathItem("foo", openapitest.NewPath()), ) diff --git a/providers/openapi/request_body.go b/providers/openapi/request_body.go index 36fcd3b61..e91c8cf9f 100644 --- a/providers/openapi/request_body.go +++ b/providers/openapi/request_body.go @@ -22,7 +22,7 @@ var defaultContentType = media.ParseContentType("application/octet-stream") type RequestBodies map[string]*RequestBodyRef type RequestBodyRef struct { - dynamic.Reference + dynamic.Reference[*RequestBodyRef] Value *RequestBody } @@ -186,8 +186,8 @@ func (r *RequestBodyRef) Parse(config *dynamic.Config, reader dynamic.Reader) er } if len(r.Ref) > 0 { - var resolved *RequestBodyRef - if err := dynamic.Resolve(r.Ref, &resolved, config, reader); err != nil { + resolved, err := r.Resolve(config, reader) + if err != nil { return err } r.Value = resolved.Value diff --git a/providers/openapi/response.go b/providers/openapi/response.go index c33ea62b7..4a8a0eab9 100644 --- a/providers/openapi/response.go +++ b/providers/openapi/response.go @@ -20,7 +20,7 @@ type Responses struct { type ResponseBodies map[string]*ResponseRef type ResponseRef struct { - dynamic.Reference + dynamic.Reference[*ResponseRef] Value *Response } @@ -223,8 +223,8 @@ func (r *ResponseRef) Parse(config *dynamic.Config, reader dynamic.Reader) error } if len(r.Ref) > 0 { - var resolved *ResponseRef - if err := dynamic.Resolve(r.Ref, &resolved, config, reader); err != nil { + resolved, err := r.Resolve(config, reader) + if err != nil { return err } r.Value = resolved.Value diff --git a/providers/openapi/response_test.go b/providers/openapi/response_test.go index 62a3ca9a7..34e609fd4 100644 --- a/providers/openapi/response_test.go +++ b/providers/openapi/response_test.go @@ -365,7 +365,7 @@ func TestResponse_Parse(t *testing.T) { ), ) err := config.Parse(&dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: config}, reader) - require.EqualError(t, err, "parse path '/foo' failed: parse operation 'GET' failed: parse response '200' failed: resolve reference 'foo.yml' failed: TEST ERROR") + require.EqualError(t, err, "parse path '/foo' failed: parse operation 'GET' failed: parse response '200' failed: resolve reference '/foo.yml' failed: TEST ERROR") }, }, { @@ -382,7 +382,7 @@ func TestResponse_Parse(t *testing.T) { ), ) err := config.Parse(&dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: config}, reader) - require.EqualError(t, err, "parse path '/foo' failed: parse operation 'GET' failed: parse response '200' failed: parse header 'foo' failed: resolve reference 'foo.yml' failed: TEST ERROR") + require.EqualError(t, err, "parse path '/foo' failed: parse operation 'GET' failed: parse response '200' failed: parse header 'foo' failed: resolve reference '/foo.yml' failed: TEST ERROR") }, }, } diff --git a/providers/openapi/schema/convert.go b/providers/openapi/schema/convert.go index 7c351de25..43d4e90a3 100644 --- a/providers/openapi/schema/convert.go +++ b/providers/openapi/schema/convert.go @@ -37,8 +37,6 @@ func (c *JsonSchemaConverter) Convert(s *Schema) *schema.Schema { js := &schema.Schema{ Id: s.Id, Anchor: s.Anchor, - Ref: s.Ref, - DynamicRef: s.DynamicRef, Boolean: s.Boolean, Type: s.Type, Schema: s.Schema, @@ -74,6 +72,8 @@ func (c *JsonSchemaConverter) Convert(s *Schema) *schema.Schema { Deprecated: s.Deprecated, } c.history[s] = js + js.Ref = s.Ref + js.DynamicRef = s.DynamicRef js.Minimum = s.Minimum js.ExclusiveMinimum = s.ExclusiveMinimum diff --git a/providers/openapi/schema/convert_test.go b/providers/openapi/schema/convert_test.go index f92969a07..4ce9dd77c 100644 --- a/providers/openapi/schema/convert_test.go +++ b/providers/openapi/schema/convert_test.go @@ -1,6 +1,7 @@ package schema_test import ( + "mokapi/config/dynamic" "mokapi/providers/openapi/schema" "mokapi/providers/openapi/schema/schematest" jsonSchema "mokapi/schema/json/schema" @@ -24,7 +25,7 @@ func TestConvert(t *testing.T) { }, { name: "ref", - s: &schema.Schema{Ref: "foo.yaml"}, + s: &schema.Schema{Reference: dynamic.Reference[*schema.Schema]{Ref: "foo.yaml"}}, test: func(t *testing.T, s *jsonSchema.Schema) { require.Equal(t, "foo.yaml", s.Ref) }, @@ -45,7 +46,7 @@ func TestConvert(t *testing.T) { }, { name: "dynamic ref", - s: &schema.Schema{DynamicRef: "foo"}, + s: &schema.Schema{Reference: dynamic.Reference[*schema.Schema]{DynamicRef: "foo"}}, test: func(t *testing.T, s *jsonSchema.Schema) { require.Equal(t, "foo", s.DynamicRef) }, diff --git a/providers/openapi/schema/marshal.go b/providers/openapi/schema/marshal.go index a13892804..adfc13ad7 100644 --- a/providers/openapi/schema/marshal.go +++ b/providers/openapi/schema/marshal.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/json" "fmt" + "mokapi/config/dynamic" "mokapi/media" "mokapi/schema/encoding" "mokapi/schema/json/parser" @@ -120,6 +121,10 @@ func (e *encoder) encode(s *Schema) ([]byte, error) { } else { bVal, err = json.Marshal(val.B) } + case dynamic.Reference[*Schema]: + bVal, err = json.Marshal(val) + b.WriteString(strings.Trim(string(bVal), "{}")) + continue default: bVal, err = json.Marshal(val) } diff --git a/providers/openapi/schema/marshal_schema_test.go b/providers/openapi/schema/marshal_schema_test.go index 1fcc11082..dee1e3ab6 100644 --- a/providers/openapi/schema/marshal_schema_test.go +++ b/providers/openapi/schema/marshal_schema_test.go @@ -2,6 +2,7 @@ package schema_test import ( "encoding/json" + "mokapi/config/dynamic" "mokapi/providers/openapi/schema" "mokapi/providers/openapi/schema/schematest" jsonSchema "mokapi/schema/json/schema" @@ -28,7 +29,7 @@ func TestSchema_Marshal(t *testing.T) { }, { name: "$ref", - schema: &schema.Schema{Ref: "#/components/schemas/Foo"}, + schema: &schema.Schema{Reference: dynamic.Reference[*schema.Schema]{Ref: "#/components/schemas/Foo"}}, exp: `{"$ref":"#/components/schemas/Foo"}`, }, { diff --git a/providers/openapi/schema/marshal_xml_test.go b/providers/openapi/schema/marshal_xml_test.go index b8ec81156..1e54a4171 100644 --- a/providers/openapi/schema/marshal_xml_test.go +++ b/providers/openapi/schema/marshal_xml_test.go @@ -1,6 +1,7 @@ package schema_test import ( + "mokapi/config/dynamic" "mokapi/media" "mokapi/providers/openapi/schema" "mokapi/providers/openapi/schema/schematest" @@ -55,7 +56,7 @@ func TestMarshal_Xml(t *testing.T) { data: func() interface{} { return 4 }, - schema: &schema.Schema{Ref: "#/components/schemas/foo", Sub: schematest.New("integer")}, + schema: &schema.Schema{Reference: dynamic.Reference[*schema.Schema]{Ref: "#/components/schemas/foo"}, Sub: schematest.New("integer")}, test: func(t *testing.T, s string, err error) { require.NoError(t, err) require.Equal(t, "4", s) diff --git a/providers/openapi/schema/schema.go b/providers/openapi/schema/schema.go index f45299731..6f3acaf68 100644 --- a/providers/openapi/schema/schema.go +++ b/providers/openapi/schema/schema.go @@ -10,9 +10,8 @@ import ( ) type Schema struct { - Id string `yaml:"$id,omitempty" json:"$id,omitempty"` - Ref string `yaml:"$ref,omitempty" json:"$ref,omitempty"` - DynamicRef string `yaml:"$dynamicRef,omitempty" json:"$dynamicRef,omitempty"` + Id string `yaml:"$id,omitempty" json:"$id,omitempty"` + dynamic.Reference[*Schema] Schema string `yaml:"$schema,omitempty" json:"$schema,omitempty"` Boolean *bool `yaml:"-" json:"-"` @@ -176,8 +175,9 @@ func (s *Schema) Parse(config *dynamic.Config, reader dynamic.Reader) error { } } - if s.Ref != "" { - err := dynamic.Resolve(s.Ref, &s.Sub, config, reader) + if s.Reference.HasRef() { + var err error + s.Sub, err = s.Reference.Resolve(config, reader) if err != nil { return err } @@ -190,14 +190,6 @@ func (s *Schema) Parse(config *dynamic.Config, reader dynamic.Reader) error { s.apply(s.Sub) } - if s.DynamicRef != "" { - err := dynamic.ResolveDynamic(s.DynamicRef, &s.Sub, config, reader) - if err != nil { - return err - } - s.apply(s.Sub) - } - return nil } @@ -253,13 +245,20 @@ func (s *Schema) UnmarshalJSON(b []byte) error { return nil } + r := dynamic.Reference[*Schema]{} + err := dynamic.UnmarshalJSON(b, &r) + if err != nil { + return err + } + type alias Schema a := alias{} - err := dynamic.UnmarshalJSON(b, &a) + err = dynamic.UnmarshalJSON(b, &a) if err != nil { return err } a.m = s.m + a.Reference = r *s = Schema(a) return nil } @@ -280,13 +279,20 @@ func (s *Schema) UnmarshalYAML(node *yaml.Node) error { return nil } + r := dynamic.Reference[*Schema]{} + err := node.Decode(&r) + if err != nil { + return err + } + type alias Schema a := alias{} - err := node.Decode(&a) + err = node.Decode(&a) if err != nil { return err } a.m = s.m + a.Reference = r *s = Schema(a) return nil } diff --git a/providers/openapi/schema/schema_parse_test.go b/providers/openapi/schema/schema_parse_test.go index ce548180c..61e22b8f0 100644 --- a/providers/openapi/schema/schema_parse_test.go +++ b/providers/openapi/schema/schema_parse_test.go @@ -29,10 +29,10 @@ func TestJson_Structuring(t *testing.T) { }, }, } - r := &schema.Schema{} - err := dynamic.Resolve("https://example.com/schemas/address#/properties/street_address", &r, &dynamic.Config{Data: &schema.Schema{}}, reader) + r := &dynamic.Reference[schema.Schema]{Ref: "https://example.com/schemas/address#/properties/street_address"} + s, err := r.Resolve(&dynamic.Config{Data: &schema.Schema{}}, reader) require.NoError(t, err) - require.Equal(t, "string", r.Type.String()) + require.Equal(t, "string", s.Type.String()) }, }, { @@ -52,7 +52,7 @@ func TestJson_Structuring(t *testing.T) { person := &dynamic.Config{ Info: dynamictest.NewConfigInfo(dynamictest.WithUrl("https://example.com/schemas/person")), - Data: &schema.Schema{Ref: "https://example.com/schemas/address#street_address"}, + Data: &schema.Schema{Reference: dynamic.Reference[*schema.Schema]{Ref: "https://example.com/schemas/address#street_address"}}, } person.OpenScope("") @@ -81,7 +81,7 @@ func TestJson_Structuring(t *testing.T) { person := &dynamic.Config{ Info: dynamictest.NewConfigInfo(dynamictest.WithUrl("https://example.com/schema/billing-address")), - Data: &schema.Schema{Ref: "https://example.com/schema/billing-address#street_address"}, + Data: &schema.Schema{Reference: dynamic.Reference[*schema.Schema]{Ref: "https://example.com/schema/billing-address#street_address"}}, } err := person.Data.(*schema.Schema).Parse(person, reader) @@ -130,11 +130,11 @@ func TestJson_Structuring(t *testing.T) { cfg := &dynamic.Config{Data: &schema.Schema{Id: "https://example.com/schemas/customer"}} - r := &schema.Schema{} - err := dynamic.Resolve("/schemas/address", &r, cfg, reader) + r := &dynamic.Reference[schema.Schema]{Ref: "/schemas/address"} + s, err := r.Resolve(cfg, reader) require.NoError(t, err) require.NotNil(t, r) - require.Equal(t, "object", r.Type.String()) + require.Equal(t, "object", s.Type.String()) }, }, { @@ -170,7 +170,7 @@ func TestJson_Structuring(t *testing.T) { foo := &dynamic.Config{ Info: dynamictest.NewConfigInfo(dynamictest.WithUrl("/foo.json")), Data: &schema.Schema{ - Ref: "/bar.json", + Reference: dynamic.Reference[*schema.Schema]{Ref: "/bar.json"}, }, } @@ -263,7 +263,7 @@ $ref: '#/$defs/a' Type: jsonSchema.Types{"string"}, }, }, - Ref: "https://example.com/schemas/list-of-t", + Reference: dynamic.Reference[*schema.Schema]{Ref: "https://example.com/schemas/list-of-t"}, }, } @@ -293,7 +293,7 @@ $ref: '#/$defs/a' person := &dynamic.Config{ Info: dynamictest.NewConfigInfo(dynamictest.WithUrl("https://example.com/schemas/bar")), Data: &schema.Schema{ - Ref: "https://example.com/schemas/foo", + Reference: dynamic.Reference[*schema.Schema]{Ref: "https://example.com/schemas/foo"}, }, } diff --git a/providers/openapi/schema/schematest/schema.go b/providers/openapi/schema/schematest/schema.go index 474904c2f..fc612c09d 100644 --- a/providers/openapi/schema/schematest/schema.go +++ b/providers/openapi/schema/schematest/schema.go @@ -1,6 +1,7 @@ package schematest import ( + "mokapi/config/dynamic" "mokapi/providers/openapi/schema" jsonSchema "mokapi/schema/json/schema" ) @@ -47,7 +48,7 @@ func WithItems(typeName string, opts ...SchemaOptions) SchemaOptions { func WithItemsRef(ref string) SchemaOptions { return func(s *schema.Schema) { - s.Items = &schema.Schema{Ref: ref} + s.Items = &schema.Schema{Reference: dynamic.Reference[*schema.Schema]{Ref: ref}} } } @@ -104,12 +105,12 @@ func WithProperty(name string, ps *schema.Schema) SchemaOptions { } } -func WithPropertyRef(name string, r string) SchemaOptions { +func WithPropertyRef(name string, ref string) SchemaOptions { return func(s *schema.Schema) { if s.Properties == nil { s.Properties = &schema.Schemas{} } - s.Properties.Set(name, &schema.Schema{Ref: r}) + s.Properties.Set(name, &schema.Schema{Reference: dynamic.Reference[*schema.Schema]{Ref: ref}}) } } diff --git a/providers/swagger/convert.go b/providers/swagger/convert.go index d142f354d..13bccdcd3 100644 --- a/providers/swagger/convert.go +++ b/providers/swagger/convert.go @@ -50,6 +50,25 @@ func (c *converter) Convert() (*openapi.Config, error) { result.Paths[path] = converted } + if len(c.config.Responses) > 0 { + result.Components.Responses = map[string]*openapi.ResponseRef{} + for name, res := range c.config.Responses { + r, err := c.convertResponse(res, c.config.Produces) + if err != nil { + return nil, err + } + result.Components.Responses[name] = r + } + } + + if len(c.config.Parameters) > 0 { + result.Components.Parameters = map[string]*openapi.ParameterRef{} + for name, param := range c.config.Parameters { + p := convertParameter(param) + result.Components.Parameters[name] = p + } + } + if len(c.config.Definitions) > 0 { result.Components.Schemas = &schema.Schemas{} for k, v := range c.config.Definitions { @@ -86,7 +105,7 @@ func (c *converter) Convert() (*openapi.Config, error) { func (c *converter) convertPath(p *PathItem) (*openapi.PathRef, error) { if len(p.Ref) > 0 { - return &openapi.PathRef{Reference: dynamic.Reference{Ref: convertRef(p.Ref)}}, nil + return &openapi.PathRef{Reference: dynamic.Reference[*openapi.PathRef]{Ref: convertRef(p.Ref)}}, nil } result := &openapi.Path{} @@ -206,7 +225,7 @@ func (c *converter) convertOperation(o *Operation) (*openapi.Operation, error) { func (c *converter) convertResponse(r *Response, produces []string) (*openapi.ResponseRef, error) { if len(r.Ref) > 0 { - return &openapi.ResponseRef{Reference: dynamic.Reference{Ref: convertRef(r.Ref)}}, nil + return &openapi.ResponseRef{Reference: dynamic.Reference[*openapi.ResponseRef]{Ref: convertRef(r.Ref)}}, nil } result := &openapi.Response{ Description: r.Description, @@ -230,7 +249,7 @@ func (c *converter) convertSchema(s *schema.Schema) *schema.Schema { } if len(s.Ref) > 0 { - return &schema.Schema{Ref: convertRef(s.Ref)} + return &schema.Schema{Reference: dynamic.Reference[*schema.Schema]{Ref: convertRef(s.Ref)}} } if s.Type.IsInteger() && s.Format == "" { diff --git a/providers/swagger/convert_test.go b/providers/swagger/convert_test.go index 07ae1da7c..fd7732c51 100644 --- a/providers/swagger/convert_test.go +++ b/providers/swagger/convert_test.go @@ -8,6 +8,7 @@ import ( "testing" "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" ) func TestConvert(t *testing.T) { @@ -548,3 +549,65 @@ func TestConvert(t *testing.T) { }) } } + +func TestConvert_YAML(t *testing.T) { + testcases := []struct { + name string + config string + test func(t *testing.T, config *openapi.Config) + }{ + { + name: "description", + config: `swagger: '2.0' +info: + description: foo +`, + test: func(t *testing.T, config *openapi.Config) { + require.Equal(t, "foo", config.Info.Description) + }, + }, + { + name: "response reference", + config: `swagger: '2.0' +responses: + 401: + description: foo +`, + test: func(t *testing.T, config *openapi.Config) { + res, found := config.Components.Responses["401"] + require.True(t, found) + require.NotNil(t, res.Value) + require.Equal(t, "foo", res.Value.Description) + }, + }, + { + name: "parameters reference", + config: `swagger: '2.0' +parameters: + foo: + name: skip +`, + test: func(t *testing.T, config *openapi.Config) { + p, found := config.Components.Parameters["foo"] + require.True(t, found) + require.NotNil(t, p.Value) + require.Equal(t, "skip", p.Value.Name) + }, + }, + } + + t.Parallel() + for _, tc := range testcases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + c := &Config{} + err := yaml.Unmarshal([]byte(tc.config), &c) + require.NoError(t, err) + converted, err := Convert(c) + require.NoError(t, err) + tc.test(t, converted) + }) + } +} diff --git a/runtime/runtime.go b/runtime/runtime.go index cbb5ae2a4..29f9c70c4 100644 --- a/runtime/runtime.go +++ b/runtime/runtime.go @@ -53,7 +53,7 @@ func New(cfg *static.Config, reader dynamic.Reader) *App { Events: em, Configs: map[string]*dynamic.Config{}, http: &HttpStore{cfg: cfg, index: index, events: em, reader: reader}, - Kafka: &KafkaStore{monitor: m, cfg: cfg, index: index, events: em}, + Kafka: &KafkaStore{monitor: m, cfg: cfg, index: index, events: em, reader: reader}, Mqtt: &MqttStore{monitor: m, cfg: cfg, sm: em}, Ldap: &LdapStore{cfg: cfg, events: em, index: index}, Mail: &MailStore{cfg: cfg, sm: em, index: index}, diff --git a/runtime/runtime_kafka.go b/runtime/runtime_kafka.go index 5d6aff83c..e3c8738a1 100644 --- a/runtime/runtime_kafka.go +++ b/runtime/runtime_kafka.go @@ -81,11 +81,7 @@ func (s *KafkaStore) Add(c *dynamic.Config, emitter common.EventEmitter) (*Kafka if len(s.infos) == 0 { s.infos = make(map[string]*KafkaInfo) } - cfg, err := getKafkaConfig(c) - if err != nil { - return nil, err - } - + cfg := getKafkaConfig(c) name := cfg.Info.Name ki, ok := s.infos[name] @@ -124,11 +120,7 @@ func (s *KafkaStore) Set(name string, ki *KafkaInfo) { func (s *KafkaStore) Remove(c *dynamic.Config) { s.m.RLock() - cfg, err := getKafkaConfig(c) - if err != nil { - return - } - + cfg := getKafkaConfig(c) name := cfg.Info.Name ki := s.infos[name] @@ -175,10 +167,7 @@ func (c *KafkaInfo) update(reader dynamic.Reader) { cfg := &asyncapi3.Config{} for i, k := range keys { - p, err := getKafkaConfig(c.configs[k]) - if err != nil { - log.Errorf("patch %v failed: %v", c.configs[k].Info.Url, err) - } + p := getKafkaConfig(c.configs[k]) if i == 0 { *cfg = *p } else { @@ -234,30 +223,27 @@ func (h *KafkaHandler) ServeMessage(rw kafka.ResponseWriter, req *kafka.Request) } func IsAsyncApiConfig(c *dynamic.Config) (*asyncapi3.Config, bool) { - var cfg *asyncapi3.Config - if old, ok := c.Data.(*asyncApi.Config); ok { - var err error - cfg, err = old.Convert() + switch v := c.Data.(type) { + case *asyncapi3.Config: + return v, true + case *asyncApi.Config: + conv, err := v.Convert() if err != nil { + log.Errorf("failed to convert asyncapi 2.0 config: %s", err) return nil, false } - } else { - cfg, ok = c.Data.(*asyncapi3.Config) - if !ok { - return nil, false - } + return conv, true + default: + return nil, false } - - return cfg, true } -func getKafkaConfig(c *dynamic.Config) (*asyncapi3.Config, error) { - if _, ok := c.Data.(*asyncapi3.Config); ok { - return c.Data.(*asyncapi3.Config), nil - } else { - old := c.Data.(*asyncApi.Config) - return old.Convert() +func getKafkaConfig(c *dynamic.Config) *asyncapi3.Config { + cfg, ok := IsAsyncApiConfig(c) + if !ok { + return nil } + return cfg } func (s *KafkaStore) updateEventStore(k *KafkaInfo) { diff --git a/runtime/runtime_kafka_test.go b/runtime/runtime_kafka_test.go index 5f90b3376..d253e153d 100644 --- a/runtime/runtime_kafka_test.go +++ b/runtime/runtime_kafka_test.go @@ -3,6 +3,7 @@ package runtime_test import ( "mokapi/config/dynamic" "mokapi/config/dynamic/dynamictest" + "mokapi/config/dynamic/provider/file" "mokapi/config/static" "mokapi/engine/enginetest" "mokapi/kafka" @@ -15,6 +16,7 @@ import ( "mokapi/runtime/events/eventstest" "mokapi/runtime/monitor" "net/url" + "strings" "testing" "time" @@ -148,6 +150,7 @@ func TestApp_AddKafka_Patching(t *testing.T) { testcases := []struct { name string configs []*dynamic.Config + reader dynamic.Reader test func(t *testing.T, app *runtime.App) }{ { @@ -222,7 +225,7 @@ func TestApp_AddKafka_Patching(t *testing.T) { getConfig("https://a.io/a", asyncapi3test.NewConfig( asyncapi3test.WithInfo("foo", "foo", ""), asyncapi3test.WithChannel("bar", - asyncapi3test.UseMessage("bar", &asyncapi3.MessageRef{Reference: dynamic.Reference{Ref: "#/components/messages/bar"}}), + asyncapi3test.UseMessage("bar", &asyncapi3.MessageRef{Reference: dynamic.Reference[*asyncapi3.MessageRef]{Ref: "#/components/messages/bar"}}), ), asyncapi3test.WithComponentMessage("bar", &asyncapi3.Message{ Summary: "original", @@ -244,14 +247,87 @@ func TestApp_AddKafka_Patching(t *testing.T) { require.Equal(t, "patch", msg.Value.Summary) }, }, + { + name: "using relative file path", + reader: dynamictest.ReaderFunc(func(u *url.URL, v any) (*dynamic.Config, error) { + require.True(t, strings.HasSuffix(u.Path, "path/ref.yaml")) + return &dynamic.Config{Data: &asyncapi3.MessageRef{ + Value: asyncapi3test.NewMessage( + asyncapi3test.WithMessageTitle("original"), + ), + }, + }, nil + }), + configs: []*dynamic.Config{ + getFileConfig("path/foo.yaml", asyncapi3test.NewConfig( + asyncapi3test.WithInfo("foo", "foo", ""), + asyncapi3test.WithChannel("bar", + asyncapi3test.UseMessage("bar", + &asyncapi3.MessageRef{Reference: dynamic.Reference[*asyncapi3.MessageRef]{Ref: "ref.yaml"}}, + ), + ), + )), + getFileConfig("path/bar.yaml", asyncapi3test.NewConfig( + asyncapi3test.WithInfo("foo", "bar", ""), + )), + }, + test: func(t *testing.T, app *runtime.App) { + info := app.Kafka.Get("foo") + ch := info.Channels["bar"] + require.NotNil(t, ch) + msg := ch.Value.Messages["bar"] + require.NotNil(t, msg) + require.NotNil(t, msg.Value) + require.Equal(t, "original", msg.Value.Title) + }, + }, + { + name: "using relative URL path", + reader: dynamictest.ReaderFunc(func(u *url.URL, v any) (*dynamic.Config, error) { + require.Equal(t, "https://a.io/bar.yaml", u.String()) + return &dynamic.Config{Data: &asyncapi3.MessageRef{ + Value: asyncapi3test.NewMessage( + asyncapi3test.WithMessageTitle("original"), + ), + }, + }, nil + }), + configs: []*dynamic.Config{ + getConfig("https://a.io/a", asyncapi3test.NewConfig( + asyncapi3test.WithInfo("foo", "foo", ""), + asyncapi3test.WithChannel("bar", + asyncapi3test.UseMessage("bar", + &asyncapi3.MessageRef{Reference: dynamic.Reference[*asyncapi3.MessageRef]{Ref: "bar.yaml"}}, + ), + ), + )), + getConfig("https://mokapi.io/b", asyncapi3test.NewConfig( + asyncapi3test.WithInfo("foo", "bar", ""), + )), + }, + test: func(t *testing.T, app *runtime.App) { + info := app.Kafka.Get("foo") + ch := info.Channels["bar"] + require.NotNil(t, ch) + msg := ch.Value.Messages["bar"] + require.NotNil(t, msg) + require.Equal(t, "original", msg.Value.Title) + }, + }, } for _, tc := range testcases { tc := tc t.Run(tc.name, func(t *testing.T) { + r := tc.reader + if r == nil { + r = &dynamictest.Reader{} + } cfg := &static.Config{} - app := runtime.New(cfg, &dynamictest.Reader{}) + app := runtime.New(cfg, r) for _, c := range tc.configs { - _, err := app.Kafka.Add(c, enginetest.NewEngine()) + err := c.Data.(dynamic.Parser).Parse(c, r) + require.NoError(t, err) + _, err = app.Kafka.Add(c, enginetest.NewEngine()) require.NoError(t, err) } tc.test(t, app) @@ -273,6 +349,13 @@ func getConfig(name string, c *asyncapi3.Config) *dynamic.Config { return cfg } +func getFileConfig(name string, c *asyncapi3.Config) *dynamic.Config { + u, _ := file.ParseUrl(name) + cfg := &dynamic.Config{Data: c} + cfg.Info.Url = u + return cfg +} + func newProduceMessage(topic string) *kafka.Request { return kafkatest.NewRequest("kafkatest", 3, &produce.Request{ Topics: []produce.RequestTopic{ diff --git a/runtime/runtime_search_test.go b/runtime/runtime_search_test.go index 2a48adf04..fbe7608b2 100644 --- a/runtime/runtime_search_test.go +++ b/runtime/runtime_search_test.go @@ -3,10 +3,10 @@ package runtime_test import ( "context" "mokapi/config/dynamic" - "mokapi/config/dynamic/asyncApi/asyncapitest" "mokapi/config/dynamic/dynamictest" "mokapi/config/static" "mokapi/engine/enginetest" + "mokapi/providers/asyncapi3/asyncapi3test" "mokapi/providers/openapi/openapitest" "mokapi/runtime" "mokapi/runtime/search" @@ -72,7 +72,7 @@ func TestIndex_Config(t *testing.T) { test: func(t *testing.T, app *runtime.App) { h := openapitest.NewConfig("3.0", openapitest.WithInfo("foo", "", "")) app.AddHttp(toConfig(h)) - k := asyncapitest.NewConfig(asyncapitest.WithInfo("foo", "", "")) + k := asyncapi3test.NewConfig(asyncapi3test.WithInfo("foo", "", "")) _, err := app.Kafka.Add(toConfig(k), enginetest.NewEngine()) require.NoError(t, err) diff --git a/runtime/runtimetest/app.go b/runtime/runtimetest/app.go index de44f4faa..f3fbaf70a 100644 --- a/runtime/runtimetest/app.go +++ b/runtime/runtimetest/app.go @@ -11,6 +11,7 @@ import ( "mokapi/providers/mail" "mokapi/providers/openapi" "mokapi/runtime" + "mokapi/runtime/events" ) func NewHttpApp(configs ...*openapi.Config) *runtime.App { @@ -107,3 +108,12 @@ func WithMailInfo(name string, mi *runtime.MailInfo) Options { app.Mail.Set(name, mi) } } + +func WithEvent(traits events.Traits, data events.EventData) Options { + return func(app *runtime.App) { + err := app.Events.Push(data, traits) + if err != nil { + panic(err) + } + } +} diff --git a/schema/json/generator/pet_test.go b/schema/json/generator/pet_test.go index 96c3e169e..d2061ecca 100644 --- a/schema/json/generator/pet_test.go +++ b/schema/json/generator/pet_test.go @@ -1,11 +1,13 @@ package generator import ( - "github.com/brianvoe/gofakeit/v6" - "github.com/stretchr/testify/require" + "mokapi/config/dynamic" "mokapi/schema/json/schema" "mokapi/schema/json/schema/schematest" "testing" + + "github.com/brianvoe/gofakeit/v6" + "github.com/stretchr/testify/require" ) func TestPet(t *testing.T) { @@ -47,7 +49,7 @@ func TestPet(t *testing.T) { req: &Request{ Path: []string{"pet"}, Schema: schematest.New("array", schematest.WithItemsNew( - &schema.Schema{Ref: "#/components/schemas/Pet", Type: schema.Types{"string"}}, + &schema.Schema{Reference: dynamic.Reference[*schema.Schema]{Ref: "#/components/schemas/Pet"}, Type: schema.Types{"string"}}, )), }, test: func(t *testing.T, v interface{}, err error) { @@ -109,7 +111,7 @@ func TestPet(t *testing.T) { req: &Request{ Path: []string{"pet"}, Schema: schematest.New("array", schematest.WithItemsNew( - &schema.Schema{Ref: "#/components/schemas/Category", Type: schema.Types{"string"}}, + &schema.Schema{Reference: dynamic.Reference[*schema.Schema]{Ref: "#/components/schemas/Category"}, Type: schema.Types{"string"}}, )), }, test: func(t *testing.T, v interface{}, err error) { diff --git a/schema/json/schema/clone.go b/schema/json/schema/clone.go index fcc4288ba..66ef9e25e 100644 --- a/schema/json/schema/clone.go +++ b/schema/json/schema/clone.go @@ -7,8 +7,7 @@ func (s *Schema) Clone() *Schema { clone := &Schema{ Id: s.Id, - Ref: s.Ref, - DynamicRef: s.DynamicRef, + Reference: s.Reference, Schema: s.Schema, Boolean: s.Boolean, Anchor: s.Anchor, diff --git a/schema/json/schema/clone_test.go b/schema/json/schema/clone_test.go index eb5380f5d..07d680ca1 100644 --- a/schema/json/schema/clone_test.go +++ b/schema/json/schema/clone_test.go @@ -1,6 +1,7 @@ package schema_test import ( + "mokapi/config/dynamic" "mokapi/schema/json/schema" "mokapi/schema/json/schema/schematest" "testing" @@ -17,14 +18,13 @@ func TestSchema_Clone(t *testing.T) { name: "base", test: func(t *testing.T) { s := &schema.Schema{ - Id: "id", - Ref: "ref", - DynamicRef: "dynamicRef", - Schema: "schema", - Boolean: toBoolP(true), - Anchor: "anchor", - Type: schema.Types{"object"}, - Enum: []any{"one", "two", "three"}, + Id: "id", + Reference: dynamic.Reference[*schema.Schema]{Ref: "ref", DynamicRef: "dynamicRef"}, + Schema: "schema", + Boolean: toBoolP(true), + Anchor: "anchor", + Type: schema.Types{"object"}, + Enum: []any{"one", "two", "three"}, Const: func() *any { var v any v = "const" diff --git a/schema/json/schema/marshal.go b/schema/json/schema/marshal.go index e252ca3af..d3f2a43e0 100644 --- a/schema/json/schema/marshal.go +++ b/schema/json/schema/marshal.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/json" "fmt" + "mokapi/config/dynamic" "reflect" "strings" ) @@ -80,6 +81,10 @@ func (e *encoder) encode(r *Schema) ([]byte, error) { bVal = fields.Bytes() case *Schema: bVal, err = e.encode(val) + case dynamic.Reference[*Schema]: + bVal, err = json.Marshal(val) + b.WriteString(strings.Trim(string(bVal), "{}")) + continue default: bVal, err = json.Marshal(val) } diff --git a/schema/json/schema/marshal_test.go b/schema/json/schema/marshal_test.go index 6cde09963..f74a8f671 100644 --- a/schema/json/schema/marshal_test.go +++ b/schema/json/schema/marshal_test.go @@ -2,8 +2,10 @@ package schema import ( "encoding/json" - "github.com/stretchr/testify/require" + "mokapi/config/dynamic" "testing" + + "github.com/stretchr/testify/require" ) func TestRef_MarshalJSON(t *testing.T) { @@ -51,7 +53,7 @@ func TestSchema_MarshalJSON_Recursion(t *testing.T) { name: "recursion in property", s: func() *Schema { s := &Schema{ - Ref: "foo", + Reference: dynamic.Reference[*Schema]{Ref: "foo"}, Properties: &Schemas{}, } s.Properties.Set("foo", s) @@ -66,7 +68,7 @@ func TestSchema_MarshalJSON_Recursion(t *testing.T) { name: "recursion in array", s: func() *Schema { s := &Schema{ - Ref: "foo", + Reference: dynamic.Reference[*Schema]{Ref: "foo"}, } s.Items = s return s @@ -80,7 +82,7 @@ func TestSchema_MarshalJSON_Recursion(t *testing.T) { name: "recursion in contains", s: func() *Schema { s := &Schema{ - Ref: "foo", + Reference: dynamic.Reference[*Schema]{Ref: "foo"}, } s.Contains = s return s diff --git a/schema/json/schema/parse_test.go b/schema/json/schema/parse_test.go index 89a928283..0ce202a2a 100644 --- a/schema/json/schema/parse_test.go +++ b/schema/json/schema/parse_test.go @@ -93,7 +93,7 @@ $ref: '#/$defs/a' person := &dynamic.Config{ Info: dynamictest.NewConfigInfo(dynamictest.WithUrl("https://example.com/schemas/bar")), Data: &schema.Schema{ - Ref: "https://example.com/schemas/foo", + Reference: dynamic.Reference[*schema.Schema]{Ref: "https://example.com/schemas/foo"}, }, } diff --git a/schema/json/schema/schema.go b/schema/json/schema/schema.go index d073d0c8e..9507bf545 100644 --- a/schema/json/schema/schema.go +++ b/schema/json/schema/schema.go @@ -11,9 +11,8 @@ import ( type Schema struct { m map[string]bool - Id string `yaml:"$id,omitempty" json:"$id,omitempty"` - Ref string `yaml:"$ref,omitempty" json:"$ref,omitempty"` - DynamicRef string `yaml:"$dynamicRef,omitempty" json:"$dynamicRef,omitempty"` + Id string `yaml:"$id,omitempty" json:"$id,omitempty"` + dynamic.Reference[*Schema] Schema string `yaml:"$schema,omitempty" json:"$schema,omitempty"` Boolean *bool `yaml:"-" json:"-"` @@ -167,9 +166,15 @@ func (s *Schema) UnmarshalJSON(b []byte) error { return nil } + r := dynamic.Reference[*Schema]{} + err := json.Unmarshal(b, &r) + if err != nil { + return err + } + type alias Schema a := alias{} - err := json.Unmarshal(b, &a) + err = json.Unmarshal(b, &a) if typeErr, ok := err.(*json.UnmarshalTypeError); ok { return &UnmarshalError{ Value: typeErr.Value, @@ -179,6 +184,7 @@ func (s *Schema) UnmarshalJSON(b []byte) error { return err } a.m = s.m + a.Reference = r *s = Schema(a) return nil } @@ -199,21 +205,24 @@ func (s *Schema) UnmarshalYAML(node *yaml.Node) error { return nil } + r := dynamic.Reference[*Schema]{} + err := node.Decode(&r) + if err != nil { + return err + } + type alias Schema a := alias{} - err := node.Decode(&a) + err = node.Decode(&a) if err != nil { return err } a.m = s.m + a.Reference = r *s = Schema(a) return nil } -type ref struct { - Schema *Schema -} - func (s *Schema) Parse(config *dynamic.Config, reader dynamic.Reader) error { if s == nil { return nil @@ -296,9 +305,8 @@ func (s *Schema) Parse(config *dynamic.Config, reader dynamic.Reader) error { } } - if s.Ref != "" { - r := &ref{} - err := dynamic.Resolve(s.Ref, &r.Schema, config, reader) + if s.Reference.HasRef() { + r, err := s.Reference.Resolve(config, reader) if err != nil { return err } @@ -308,16 +316,7 @@ func (s *Schema) Parse(config *dynamic.Config, reader dynamic.Reader) error { // the parsed schema graph. Dynamic references may resolve differently // depending on the evaluation context, so shared schema nodes must // never be mutated. - s.apply(r.Schema) - } - - if s.DynamicRef != "" { - r := &ref{} - err := dynamic.ResolveDynamic(s.DynamicRef, &r.Schema, config, reader) - if err != nil { - return err - } - s.apply(r.Schema) + s.apply(r) } return nil diff --git a/schema/json/schema/schema_json_test.go b/schema/json/schema/schema_json_test.go index 70ee2df00..741ed26a4 100644 --- a/schema/json/schema/schema_json_test.go +++ b/schema/json/schema/schema_json_test.go @@ -351,10 +351,10 @@ func TestJson_Structuring(t *testing.T) { }, }, } - r := &schema.Schema{} - err := dynamic.Resolve("https://example.com/schemas/address#/properties/street_address", &r, &dynamic.Config{Data: &schema.Schema{}}, reader) + s := &schema.Schema{Reference: dynamic.Reference[*schema.Schema]{Ref: "https://example.com/schemas/address#/properties/street_address"}} + err := s.Parse(&dynamic.Config{Data: &schema.Schema{}}, reader) require.NoError(t, err) - require.Equal(t, "string", r.Type.String()) + require.Equal(t, "string", s.Type.String()) }, }, { @@ -374,7 +374,7 @@ func TestJson_Structuring(t *testing.T) { person := &dynamic.Config{ Info: dynamictest.NewConfigInfo(dynamictest.WithUrl("https://example.com/schemas/person")), - Data: &schema.Schema{Ref: "https://example.com/schemas/address#street_address"}, + Data: &schema.Schema{Reference: dynamic.Reference[*schema.Schema]{Ref: "https://example.com/schemas/address#street_address"}}, } person.OpenScope("") @@ -403,7 +403,7 @@ func TestJson_Structuring(t *testing.T) { person := &dynamic.Config{ Info: dynamictest.NewConfigInfo(dynamictest.WithUrl("https://example.com/schema/billing-address")), - Data: &schema.Schema{Ref: "https://example.com/schema/billing-address#street_address"}, + Data: &schema.Schema{Reference: dynamic.Reference[*schema.Schema]{Ref: "https://example.com/schema/billing-address#street_address"}}, } err := person.Data.(*schema.Schema).Parse(person, reader) @@ -452,11 +452,11 @@ func TestJson_Structuring(t *testing.T) { cfg := &dynamic.Config{Data: &schema.Schema{Id: "https://example.com/schemas/customer"}} - r := &schema.Schema{} - err := dynamic.Resolve("/schemas/address", &r, cfg, reader) + r := &dynamic.Reference[schema.Schema]{Ref: "/schemas/address"} + s, err := r.Resolve(cfg, reader) require.NoError(t, err) require.NotNil(t, r) - require.Equal(t, "object", r.Type.String()) + require.Equal(t, "object", s.Type.String()) }, }, { @@ -519,15 +519,17 @@ func TestJson_Structuring(t *testing.T) { Type: schema.Types{"string"}, }, }, - Ref: "https://example.com/schemas/list-of-t", + Reference: dynamic.Reference[*schema.Schema]{Ref: "https://example.com/schemas/list-of-t"}, }, } - err := person.Data.(*schema.Schema).Parse(person, reader) + s := person.Data.(*schema.Schema) + err := s.Parse(person, reader) require.NoError(t, err) require.NoError(t, err) - require.Equal(t, "string", person.Data.(*schema.Schema).Items.Type.String()) + require.Nil(t, s.Items.Not, "resolves to the base generic schema") + require.Equal(t, "string", s.Items.Type.String()) }, }, } diff --git a/schema/json/schema/schema_yaml_test.go b/schema/json/schema/schema_yaml_test.go index 28d7abfcb..d4c2ce4ed 100644 --- a/schema/json/schema/schema_yaml_test.go +++ b/schema/json/schema/schema_yaml_test.go @@ -336,7 +336,7 @@ items: Type: schema.Types{"string"}, }, }, - Ref: "https://example.com/schemas/list-of-t", + Reference: dynamic.Reference[*schema.Schema]{Ref: "https://example.com/schemas/list-of-t"}, }, } diff --git a/schema/json/schema/schematest/schema.go b/schema/json/schema/schematest/schema.go index 21a88fa2b..d60c243fc 100644 --- a/schema/json/schema/schematest/schema.go +++ b/schema/json/schema/schematest/schema.go @@ -1,6 +1,7 @@ package schematest import ( + "mokapi/config/dynamic" "mokapi/schema/json/schema" ) @@ -45,12 +46,12 @@ func WithProperty(name string, ps *schema.Schema) SchemaOptions { } } -func WithPropertyRef(name string, r string) SchemaOptions { +func WithPropertyRef(name string, ref string) SchemaOptions { return func(s *schema.Schema) { if s.Properties == nil { s.Properties = &schema.Schemas{} } - s.Properties.Set(name, &schema.Schema{Ref: r}) + s.Properties.Set(name, &schema.Schema{Reference: dynamic.Reference[*schema.Schema]{Ref: ref}}) } } @@ -71,7 +72,7 @@ func WithItems(typeName string, opts ...SchemaOptions) SchemaOptions { func WithItemsRef(ref string) SchemaOptions { return func(s *schema.Schema) { - s.Items = &schema.Schema{Ref: ref} + s.Items = &schema.Schema{Reference: dynamic.Reference[*schema.Schema]{Ref: ref}} } } @@ -81,9 +82,9 @@ func WithItemsNew(items *schema.Schema) SchemaOptions { } } -func WithItemsRefString(r string) SchemaOptions { +func WithItemsRefString(ref string) SchemaOptions { return func(s *schema.Schema) { - s.Items = &schema.Schema{Ref: r} + s.Items = &schema.Schema{Reference: dynamic.Reference[*schema.Schema]{Ref: ref}} } } diff --git a/schema/json/schema/string.go b/schema/json/schema/string.go index 6a2aba7f2..91597e62f 100644 --- a/schema/json/schema/string.go +++ b/schema/json/schema/string.go @@ -6,6 +6,10 @@ import ( ) func (s *Schema) String() string { + if s == nil { + return "" + } + var sb strings.Builder if s.Boolean != nil { diff --git a/server/configwatcher_openapi_test.go b/server/configwatcher_openapi_test.go index 296fb55ba..d2c715ba4 100644 --- a/server/configwatcher_openapi_test.go +++ b/server/configwatcher_openapi_test.go @@ -3,9 +3,6 @@ package server import ( "context" "fmt" - "github.com/sirupsen/logrus" - logtest "github.com/sirupsen/logrus/hooks/test" - "github.com/stretchr/testify/require" "io" "mokapi/config/dynamic" "mokapi/config/static" @@ -15,6 +12,10 @@ import ( "strings" "testing" "time" + + "github.com/sirupsen/logrus" + logtest "github.com/sirupsen/logrus/hooks/test" + "github.com/stretchr/testify/require" ) func TestConfigWatcher_Openapi(t *testing.T) { @@ -147,7 +148,7 @@ paths: time.Sleep(1 * time.Second) - require.Equal(t, "parse error /root.yml: parsing file /root.yml: parse path '/users' failed: resolve reference 'paths.yml#/paths/users' failed: parsing file /paths.yml: parse path '/users' failed: parse operation 'GET' failed: parse request body failed: resolve reference 'not_found.yaml' failed: not found: /not_found.yaml", hook.LastEntry().Message) + require.Equal(t, "parse error /root.yml: parsing file /root.yml: parse path '/users' failed: resolve reference '/paths.yml#/paths/users' failed: parsing file /paths.yml: parse path '/users' failed: parse operation 'GET' failed: parse request body failed: resolve reference '/not_found.yaml' failed: not found: /not_found.yaml", hook.LastEntry().Message) }, }, } diff --git a/webui/.gitignore b/webui/.gitignore index 7382fc04c..c0c29250d 100644 --- a/webui/.gitignore +++ b/webui/.gitignore @@ -14,9 +14,6 @@ dist-ssr coverage *.local -/cypress/videos/ -/cypress/screenshots/ - # Editor directories and files .vscode/* !.vscode/extensions.json @@ -27,8 +24,9 @@ coverage *.sln *.sw? -test-results/ -playwright-report/ src/assets/docs public/sitemap.xml public/demo/ + +test-results/ +playwright-report/ diff --git a/webui/README.md b/webui/README.md index 963afa186..5cee247f7 100644 --- a/webui/README.md +++ b/webui/README.md @@ -41,23 +41,6 @@ npm run build ### Run End-to-End Tests with [Playwright](https://playwright.dev) -```sh -# Install browsers for the first run -npx playwright install - -# When testing on CI, must build the project first -npm run build - -# Runs the end-to-end tests -npm run test:e2e -# Runs the tests only on Chromium -npm run test:e2e -- --project=chromium -# Runs the tests of a specific file -npm run test:e2e -- tests/example.spec.ts -# Runs the tests in debug mode -npm run test:e2e -- --debug -``` - ### Lint with [ESLint](https://eslint.org/) ```sh diff --git a/examples/mokapi/common.yml b/webui/e2e/mocks/common.yml similarity index 100% rename from examples/mokapi/common.yml rename to webui/e2e/mocks/common.yml diff --git a/examples/mokapi/dashboard.yml b/webui/e2e/mocks/dashboard.yml similarity index 100% rename from examples/mokapi/dashboard.yml rename to webui/e2e/mocks/dashboard.yml diff --git a/examples/mokapi/frequency.js b/webui/e2e/mocks/frequency.js similarity index 100% rename from examples/mokapi/frequency.js rename to webui/e2e/mocks/frequency.js diff --git a/examples/mokapi/http.yml b/webui/e2e/mocks/http.yml similarity index 100% rename from examples/mokapi/http.yml rename to webui/e2e/mocks/http.yml diff --git a/examples/mokapi/http_handler.js b/webui/e2e/mocks/http_handler.js similarity index 100% rename from examples/mokapi/http_handler.js rename to webui/e2e/mocks/http_handler.js diff --git a/examples/mokapi/icon.png b/webui/e2e/mocks/icon.png similarity index 100% rename from examples/mokapi/icon.png rename to webui/e2e/mocks/icon.png diff --git a/examples/mokapi/kafka.js b/webui/e2e/mocks/kafka.js similarity index 100% rename from examples/mokapi/kafka.js rename to webui/e2e/mocks/kafka.js diff --git a/examples/mokapi/kafka.yml b/webui/e2e/mocks/kafka.yml similarity index 100% rename from examples/mokapi/kafka.yml rename to webui/e2e/mocks/kafka.yml diff --git a/examples/mokapi/ldap.js b/webui/e2e/mocks/ldap.js similarity index 100% rename from examples/mokapi/ldap.js rename to webui/e2e/mocks/ldap.js diff --git a/examples/mokapi/ldap.yml b/webui/e2e/mocks/ldap.yml similarity index 100% rename from examples/mokapi/ldap.yml rename to webui/e2e/mocks/ldap.yml diff --git a/examples/mokapi/mail.js b/webui/e2e/mocks/mail.js similarity index 100% rename from examples/mokapi/mail.js rename to webui/e2e/mocks/mail.js diff --git a/examples/mokapi/mail.yml b/webui/e2e/mocks/mail.yml similarity index 100% rename from examples/mokapi/mail.yml rename to webui/e2e/mocks/mail.yml diff --git a/examples/mokapi/metrics.js b/webui/e2e/mocks/metrics.js similarity index 100% rename from examples/mokapi/metrics.js rename to webui/e2e/mocks/metrics.js diff --git a/examples/mokapi/schema.yml b/webui/e2e/mocks/schema.yml similarity index 100% rename from examples/mokapi/schema.yml rename to webui/e2e/mocks/schema.yml diff --git a/examples/mokapi/services_http.js b/webui/e2e/mocks/services_http.js similarity index 100% rename from examples/mokapi/services_http.js rename to webui/e2e/mocks/services_http.js diff --git a/webui/e2e/components/dashboard.ts b/webui/e2e/tests/components/dashboard.ts similarity index 100% rename from webui/e2e/components/dashboard.ts rename to webui/e2e/tests/components/dashboard.ts diff --git a/webui/e2e/components/kafka.ts b/webui/e2e/tests/components/kafka.ts similarity index 100% rename from webui/e2e/components/kafka.ts rename to webui/e2e/tests/components/kafka.ts diff --git a/webui/e2e/components/source.ts b/webui/e2e/tests/components/source.ts similarity index 100% rename from webui/e2e/components/source.ts rename to webui/e2e/tests/components/source.ts diff --git a/webui/e2e/components/table.ts b/webui/e2e/tests/components/table.ts similarity index 100% rename from webui/e2e/components/table.ts rename to webui/e2e/tests/components/table.ts diff --git a/webui/e2e/dashboard-demo/dashboard.spec.ts b/webui/e2e/tests/dashboard-demo/dashboard.spec.ts similarity index 100% rename from webui/e2e/dashboard-demo/dashboard.spec.ts rename to webui/e2e/tests/dashboard-demo/dashboard.spec.ts diff --git a/webui/e2e/dashboard-demo/kafka.spec.ts b/webui/e2e/tests/dashboard-demo/kafka.spec.ts similarity index 100% rename from webui/e2e/dashboard-demo/kafka.spec.ts rename to webui/e2e/tests/dashboard-demo/kafka.spec.ts diff --git a/webui/e2e/dashboard-demo/ldap.spec.ts b/webui/e2e/tests/dashboard-demo/ldap.spec.ts similarity index 100% rename from webui/e2e/dashboard-demo/ldap.spec.ts rename to webui/e2e/tests/dashboard-demo/ldap.spec.ts diff --git a/webui/e2e/dashboard-demo/mail.spec.ts b/webui/e2e/tests/dashboard-demo/mail.spec.ts similarity index 100% rename from webui/e2e/dashboard-demo/mail.spec.ts rename to webui/e2e/tests/dashboard-demo/mail.spec.ts diff --git a/webui/e2e/dashboard-demo/petstore.spec.ts b/webui/e2e/tests/dashboard-demo/petstore.spec.ts similarity index 100% rename from webui/e2e/dashboard-demo/petstore.spec.ts rename to webui/e2e/tests/dashboard-demo/petstore.spec.ts diff --git a/webui/e2e/Dashboard/dashboard.spec.ts b/webui/e2e/tests/dashboard/dashboard.spec.ts similarity index 100% rename from webui/e2e/Dashboard/dashboard.spec.ts rename to webui/e2e/tests/dashboard/dashboard.spec.ts diff --git a/webui/e2e/Dashboard/http/books.spec.ts b/webui/e2e/tests/dashboard/http/books.spec.ts similarity index 100% rename from webui/e2e/Dashboard/http/books.spec.ts rename to webui/e2e/tests/dashboard/http/books.spec.ts diff --git a/webui/e2e/Dashboard/kafka/cluster.spec.ts b/webui/e2e/tests/dashboard/kafka/cluster.spec.ts similarity index 100% rename from webui/e2e/Dashboard/kafka/cluster.spec.ts rename to webui/e2e/tests/dashboard/kafka/cluster.spec.ts diff --git a/webui/e2e/Dashboard/kafka/cluster.ts b/webui/e2e/tests/dashboard/kafka/cluster.ts similarity index 100% rename from webui/e2e/Dashboard/kafka/cluster.ts rename to webui/e2e/tests/dashboard/kafka/cluster.ts diff --git a/webui/e2e/Dashboard/kafka/overview.spec.ts b/webui/e2e/tests/dashboard/kafka/overview.spec.ts similarity index 100% rename from webui/e2e/Dashboard/kafka/overview.spec.ts rename to webui/e2e/tests/dashboard/kafka/overview.spec.ts diff --git a/webui/e2e/Dashboard/kafka/topic.order.spec.ts b/webui/e2e/tests/dashboard/kafka/topic.order.spec.ts similarity index 97% rename from webui/e2e/Dashboard/kafka/topic.order.spec.ts rename to webui/e2e/tests/dashboard/kafka/topic.order.spec.ts index f027ff07f..8d903c818 100644 --- a/webui/e2e/Dashboard/kafka/topic.order.spec.ts +++ b/webui/e2e/tests/dashboard/kafka/topic.order.spec.ts @@ -55,7 +55,7 @@ test('Visit Kafka topic mokapi.shop.products', async ({ page, context }) => { await test.step('Check config', async () => { await tabList.getByRole('tab', { name: 'Configs' }).click() const configs = page.getByRole('tabpanel', { name: 'Configs' }) - await expect(configs.getByLabel('Title')).toHaveText(topic.messageConfigs[0].title) + await expect(configs.getByLabel('Title')).toHaveText(topic.messageConfigs[0].title!) await expect(configs.getByLabel('Name')).toHaveText(topic.messageConfigs[0].name) await expect(configs.getByLabel('Summary')).toHaveText(topic.messageConfigs[0].summary) await expect(configs.getByLabel('Description')).toHaveText(topic.messageConfigs[0].description) @@ -93,6 +93,9 @@ test('Visit Kafka topic mokapi.shop.products', async ({ page, context }) => { await test.step('Check schema example', async () => { await configs.getByRole('button', { name: 'Example & Validate' }).click() const dialog = page.getByRole('dialog', { name: 'Value Validator - mokapi.shop.products' }) + + await expect(dialog.getByRole('region', { name: 'Content' })).toBeVisible() + await dialog.getByRole('button', { name: 'Example' }).click() const { test: testSourceView } = useSourceView(dialog) await testSourceView({ @@ -108,6 +111,9 @@ test('Visit Kafka topic mokapi.shop.products', async ({ page, context }) => { await test.step('Check data validation', async () =>{ await configs.getByRole('button', { name: 'Example & Validate' }).click() const dialog = page.getByRole('dialog', { name: 'Value Validator - mokapi.shop.products' }) + + await expect(dialog.getByRole('region', { name: 'Content' })).toBeVisible() + await dialog.getByRole('button', { name: 'Example' }).click() // first we try data that are wrong against the schema const id = await dialog.locator('.modal-dialog .ace_editor').getAttribute('id') diff --git a/webui/e2e/Dashboard/kafka/topic.userSignedUp.spec.ts b/webui/e2e/tests/dashboard/kafka/topic.userSignedUp.spec.ts similarity index 100% rename from webui/e2e/Dashboard/kafka/topic.userSignedUp.spec.ts rename to webui/e2e/tests/dashboard/kafka/topic.userSignedUp.spec.ts diff --git a/webui/e2e/Dashboard/mail/testserver.spec.ts b/webui/e2e/tests/dashboard/mail/testserver.spec.ts similarity index 100% rename from webui/e2e/Dashboard/mail/testserver.spec.ts rename to webui/e2e/tests/dashboard/mail/testserver.spec.ts diff --git a/webui/e2e/documentation/config.ts b/webui/e2e/tests/documentation/config.ts similarity index 100% rename from webui/e2e/documentation/config.ts rename to webui/e2e/tests/documentation/config.ts diff --git a/webui/e2e/documentation/configuration.spec.ts b/webui/e2e/tests/documentation/configuration.spec.ts similarity index 100% rename from webui/e2e/documentation/configuration.spec.ts rename to webui/e2e/tests/documentation/configuration.spec.ts diff --git a/webui/e2e/documentation/guides.spec.ts b/webui/e2e/tests/documentation/guides.spec.ts similarity index 100% rename from webui/e2e/documentation/guides.spec.ts rename to webui/e2e/tests/documentation/guides.spec.ts diff --git a/webui/e2e/documentation/resources.spec.ts b/webui/e2e/tests/documentation/resources.spec.ts similarity index 100% rename from webui/e2e/documentation/resources.spec.ts rename to webui/e2e/tests/documentation/resources.spec.ts diff --git a/webui/e2e/header.dashboard.spec.ts b/webui/e2e/tests/header.dashboard.spec.ts similarity index 100% rename from webui/e2e/header.dashboard.spec.ts rename to webui/e2e/tests/header.dashboard.spec.ts diff --git a/webui/e2e/header.website.spec.ts b/webui/e2e/tests/header.website.spec.ts similarity index 100% rename from webui/e2e/header.website.spec.ts rename to webui/e2e/tests/header.website.spec.ts diff --git a/webui/e2e/helpers/format.ts b/webui/e2e/tests/helpers/format.ts similarity index 100% rename from webui/e2e/helpers/format.ts rename to webui/e2e/tests/helpers/format.ts diff --git a/webui/e2e/helpers/table.ts b/webui/e2e/tests/helpers/table.ts similarity index 100% rename from webui/e2e/helpers/table.ts rename to webui/e2e/tests/helpers/table.ts diff --git a/webui/e2e/home.spec.ts b/webui/e2e/tests/home.spec.ts similarity index 100% rename from webui/e2e/home.spec.ts rename to webui/e2e/tests/home.spec.ts diff --git a/webui/e2e/models/dashboard.ts b/webui/e2e/tests/models/dashboard.ts similarity index 100% rename from webui/e2e/models/dashboard.ts rename to webui/e2e/tests/models/dashboard.ts diff --git a/webui/e2e/models/fixture-dashboard.ts b/webui/e2e/tests/models/fixture-dashboard.ts similarity index 100% rename from webui/e2e/models/fixture-dashboard.ts rename to webui/e2e/tests/models/fixture-dashboard.ts diff --git a/webui/e2e/models/fixture-website.ts b/webui/e2e/tests/models/fixture-website.ts similarity index 100% rename from webui/e2e/models/fixture-website.ts rename to webui/e2e/tests/models/fixture-website.ts diff --git a/webui/e2e/models/home.ts b/webui/e2e/tests/models/home.ts similarity index 100% rename from webui/e2e/models/home.ts rename to webui/e2e/tests/models/home.ts diff --git a/webui/e2e/models/http.ts b/webui/e2e/tests/models/http.ts similarity index 100% rename from webui/e2e/models/http.ts rename to webui/e2e/tests/models/http.ts diff --git a/webui/e2e/models/kafka.ts b/webui/e2e/tests/models/kafka.ts similarity index 100% rename from webui/e2e/models/kafka.ts rename to webui/e2e/tests/models/kafka.ts diff --git a/webui/e2e/models/mail.ts b/webui/e2e/tests/models/mail.ts similarity index 100% rename from webui/e2e/models/mail.ts rename to webui/e2e/tests/models/mail.ts diff --git a/webui/e2e/models/metric.ts b/webui/e2e/tests/models/metric.ts similarity index 100% rename from webui/e2e/models/metric.ts rename to webui/e2e/tests/models/metric.ts diff --git a/webui/e2e/models/mokapi.ts b/webui/e2e/tests/models/mokapi.ts similarity index 100% rename from webui/e2e/models/mokapi.ts rename to webui/e2e/tests/models/mokapi.ts diff --git a/webui/e2e/models/service-info.ts b/webui/e2e/tests/models/service-info.ts similarity index 100% rename from webui/e2e/models/service-info.ts rename to webui/e2e/tests/models/service-info.ts diff --git a/webui/e2e/sitemap.spec.ts b/webui/e2e/tests/sitemap.spec.ts similarity index 100% rename from webui/e2e/sitemap.spec.ts rename to webui/e2e/tests/sitemap.spec.ts diff --git a/webui/e2e/tsconfig.json b/webui/e2e/tests/tsconfig.json similarity index 100% rename from webui/e2e/tsconfig.json rename to webui/e2e/tests/tsconfig.json diff --git a/webui/e2e/types/types.ts b/webui/e2e/tests/types/types.ts similarity index 100% rename from webui/e2e/types/types.ts rename to webui/e2e/tests/types/types.ts diff --git a/webui/e2e/vue.spec.ts b/webui/e2e/vue.spec.ts deleted file mode 100644 index 644bcab71..000000000 --- a/webui/e2e/vue.spec.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { test, expect } from '@playwright/test'; - -// See here how to get started: -// https://playwright.dev/docs/intro -// test('visits the app root url', async ({ page }) => { -// await page.goto('/'); -// await expect(page.locator('div.greetings > h1')).toHaveText('You did it!'); -// }) diff --git a/webui/package-lock.json b/webui/package-lock.json index 0b1c72b5c..3f5b200fc 100644 --- a/webui/package-lock.json +++ b/webui/package-lock.json @@ -12,7 +12,7 @@ "@ssthouse/vue3-tree-chart": "^0.3.0", "@types/bootstrap": "^5.2.10", "@types/mokapi": "^0.35.0", - "@types/nodemailer": "^7.0.11", + "@types/nodemailer": "^8.0.0", "@types/whatwg-mimetype": "^5.0.0", "ace-builds": "^1.43.5", "bootstrap": "^5.3.8", @@ -21,7 +21,7 @@ "dayjs": "^1.11.20", "del-cli": "^7.0.0", "express": "^5.2.1", - "fuse.js": "^7.1.0", + "fuse.js": "^7.3.0", "highlight.js": "^11.11.1", "http-status-codes": "^2.3.0", "js-yaml": "^4.1.1", @@ -32,28 +32,28 @@ "mime-types": "^3.0.2", "ncp": "^2.0.0", "nodemailer": "^8.0.5", - "vue": "^3.5.30", + "vue": "^3.5.32", "vue-router": "^5.0.4", "vue3-ace-editor": "^2.2.4", "whatwg-mimetype": "^5.0.0", "xml-formatter": "^3.7.0" }, "devDependencies": { - "@playwright/test": "^1.58.2", + "@playwright/test": "^1.59.1", "@rushstack/eslint-patch": "^1.16.1", "@types/js-yaml": "^4.0.9", "@types/markdown-it-container": "^4.0.0", - "@types/node": "^25.5.0", - "@vitejs/plugin-vue": "^6.0.5", + "@types/node": "^25.6.0", + "@vitejs/plugin-vue": "^6.0.6", "@vue/eslint-config-prettier": "^10.2.0", "@vue/eslint-config-typescript": "^14.7.0", "@vue/tsconfig": "^0.9.1", - "eslint": "^10.1.0", + "eslint": "^10.2.0", "eslint-plugin-vue": "^10.8.0", "npm-run-all": "^4.1.5", - "prettier": "^3.8.1", - "typescript": "~5.9.3", - "vite": "^8.0.5", + "prettier": "^3.8.3", + "typescript": "~6.0.3", + "vite": "^8.0.8", "vue-tsc": "^3.2.6", "xml2js": "^0.6.2" } @@ -93,9 +93,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", - "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", "license": "MIT", "dependencies": { "@babel/types": "^7.29.0" @@ -127,7 +127,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" @@ -140,7 +139,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "tslib": "^2.4.0" } @@ -152,7 +150,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "tslib": "^2.4.0" } @@ -187,13 +184,13 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.23.3", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.3.tgz", - "integrity": "sha512-j+eEWmB6YYLwcNOdlwQ6L2OsptI/LO6lNBuLIqe5R7RetD658HLoF+Mn7LzYmAWWNNzdC6cqP+L6r8ujeYXWLw==", + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^3.0.3", + "@eslint/object-schema": "^3.0.5", "debug": "^4.3.1", "minimatch": "^10.2.4" }, @@ -201,62 +198,23 @@ "node": "^20.19.0 || ^22.13.0 || >=24" } }, - "node_modules/@eslint/config-array/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/@eslint/config-array/node_modules/brace-expansion": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/@eslint/config-array/node_modules/minimatch": { - "version": "10.2.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/@eslint/config-helpers": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.3.tgz", - "integrity": "sha512-lzGN0onllOZCGroKJmRwY6QcEHxbjBw1gwB8SgRSqK8YbbtEXMvKynsXc3553ckIEBxsbMBU7oOZXKIPGZNeZw==", + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.5.tgz", + "integrity": "sha512-eIJYKTCECbP/nsKaaruF6LW967mtbQbsw4JTtSVkUQc9MneSkbrgPJAbKl9nWr0ZeowV8BfsarBmPpBzGelA2w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^1.1.1" + "@eslint/core": "^1.2.1" }, "engines": { "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@eslint/core": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.1.1.tgz", - "integrity": "sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -267,9 +225,9 @@ } }, "node_modules/@eslint/object-schema": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.3.tgz", - "integrity": "sha512-iM869Pugn9Nsxbh/YHRqYiqd23AmIbxJOcpUMOuWCVNdoQJ5ZtwL6h3t0bcZzJUlC3Dq9jCFCESBZnX0GTv7iQ==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", "dev": true, "license": "Apache-2.0", "engines": { @@ -277,13 +235,13 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.6.1.tgz", - "integrity": "sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.1.tgz", + "integrity": "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^1.1.1", + "@eslint/core": "^1.2.1", "levn": "^0.4.1" }, "engines": { @@ -388,9 +346,9 @@ } }, "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.2.tgz", - "integrity": "sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.3.tgz", + "integrity": "sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ==", "dev": true, "license": "MIT", "optional": true, @@ -442,9 +400,9 @@ } }, "node_modules/@oxc-project/types": { - "version": "0.122.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.122.0.tgz", - "integrity": "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==", + "version": "0.124.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.124.0.tgz", + "integrity": "sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==", "dev": true, "license": "MIT", "funding": { @@ -465,13 +423,13 @@ } }, "node_modules/@playwright/test": { - "version": "1.58.2", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz", - "integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==", + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.1.tgz", + "integrity": "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright": "1.58.2" + "playwright": "1.59.1" }, "bin": { "playwright": "cli.js" @@ -491,9 +449,9 @@ } }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.12.tgz", - "integrity": "sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.15.tgz", + "integrity": "sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA==", "cpu": [ "arm64" ], @@ -508,9 +466,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.12.tgz", - "integrity": "sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.15.tgz", + "integrity": "sha512-oArR/ig8wNTPYsXL+Mzhs0oxhxfuHRfG7Ikw7jXsw8mYOtk71W0OkF2VEVh699pdmzjPQsTjlD1JIOoHkLP1Fg==", "cpu": [ "arm64" ], @@ -525,9 +483,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.12.tgz", - "integrity": "sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.15.tgz", + "integrity": "sha512-YzeVqOqjPYvUbJSWJ4EDL8ahbmsIXQpgL3JVipmN+MX0XnXMeWomLN3Fb+nwCmP/jfyqte5I3XRSm7OfQrbyxw==", "cpu": [ "x64" ], @@ -542,9 +500,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.12.tgz", - "integrity": "sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.15.tgz", + "integrity": "sha512-9Erhx956jeQ0nNTyif1+QWAXDRD38ZNjr//bSHrt6wDwB+QkAfl2q6Mn1k6OBPerznjRmbM10lgRb1Pli4xZPw==", "cpu": [ "x64" ], @@ -559,9 +517,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.12.tgz", - "integrity": "sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.15.tgz", + "integrity": "sha512-cVwk0w8QbZJGTnP/AHQBs5yNwmpgGYStL88t4UIaqcvYJWBfS0s3oqVLZPwsPU6M0zlW4GqjP0Zq5MnAGwFeGA==", "cpu": [ "arm" ], @@ -576,9 +534,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.12.tgz", - "integrity": "sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.15.tgz", + "integrity": "sha512-eBZ/u8iAK9SoHGanqe/jrPnY0JvBN6iXbVOsbO38mbz+ZJsaobExAm1Iu+rxa4S1l2FjG0qEZn4Rc6X8n+9M+w==", "cpu": [ "arm64" ], @@ -593,9 +551,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.12.tgz", - "integrity": "sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.15.tgz", + "integrity": "sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ==", "cpu": [ "arm64" ], @@ -610,9 +568,9 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.12.tgz", - "integrity": "sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.15.tgz", + "integrity": "sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ==", "cpu": [ "ppc64" ], @@ -627,9 +585,9 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.12.tgz", - "integrity": "sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.15.tgz", + "integrity": "sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ==", "cpu": [ "s390x" ], @@ -644,9 +602,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.12.tgz", - "integrity": "sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.15.tgz", + "integrity": "sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA==", "cpu": [ "x64" ], @@ -661,9 +619,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.12.tgz", - "integrity": "sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.15.tgz", + "integrity": "sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw==", "cpu": [ "x64" ], @@ -678,9 +636,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.12.tgz", - "integrity": "sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.15.tgz", + "integrity": "sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg==", "cpu": [ "arm64" ], @@ -695,9 +653,9 @@ } }, "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.12.tgz", - "integrity": "sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.15.tgz", + "integrity": "sha512-ApLruZq/ig+nhaE7OJm4lDjayUnOHVUa77zGeqnqZ9pn0ovdVbbNPerVibLXDmWeUZXjIYIT8V3xkT58Rm9u5Q==", "cpu": [ "wasm32" ], @@ -705,16 +663,18 @@ "license": "MIT", "optional": true, "dependencies": { - "@napi-rs/wasm-runtime": "^1.1.1" + "@emnapi/core": "1.9.2", + "@emnapi/runtime": "1.9.2", + "@napi-rs/wasm-runtime": "^1.1.3" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.12.tgz", - "integrity": "sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.15.tgz", + "integrity": "sha512-KmoUoU7HnN+Si5YWJigfTws1jz1bKBYDQKdbLspz0UaqjjFkddHsqorgiW1mxcAj88lYUE6NC/zJNwT+SloqtA==", "cpu": [ "arm64" ], @@ -729,9 +689,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.12.tgz", - "integrity": "sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.15.tgz", + "integrity": "sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g==", "cpu": [ "x64" ], @@ -746,9 +706,9 @@ } }, "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-rc.2", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.2.tgz", - "integrity": "sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw==", + "version": "1.0.0-rc.13", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.13.tgz", + "integrity": "sha512-3ngTAv6F/Py35BsYbeeLeecvhMKdsKm4AoOETVhAA+Qc8nrA2I0kF7oa93mE9qnIurngOSpMnQ0x2nQY2FPviA==", "dev": true, "license": "MIT" }, @@ -878,18 +838,18 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", - "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", + "version": "25.6.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", + "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", "license": "MIT", "dependencies": { - "undici-types": "~7.18.0" + "undici-types": "~7.19.0" } }, "node_modules/@types/nodemailer": { - "version": "7.0.11", - "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-7.0.11.tgz", - "integrity": "sha512-E+U4RzR2dKrx+u3N4DlsmLaDC6mMZOM/TPROxA0UAPiTgI0y4CEFBmZE+coGWTjakDriRsXG368lNk1u9Q0a2g==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-8.0.0.tgz", + "integrity": "sha512-fyf8jWULsCo0d0BuoQ75i6IeoHs47qcqxWc7yUdUcV0pOZGjUTTOvwdG1PRXUDqN/8A64yQdQdnA2pZgcdi+cA==", "license": "MIT", "dependencies": { "@types/node": "*" @@ -902,20 +862,20 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.0.tgz", - "integrity": "sha512-lRyPDLzNCuae71A3t9NEINBiTn7swyOhvUj3MyUOxb8x6g6vPEFoOU+ZRmGMusNC3X3YMhqMIX7i8ShqhT74Pw==", + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.0.tgz", + "integrity": "sha512-RLkVSiNuUP1C2ROIWfqX+YcUfLaSnxGE/8M+Y57lopVwg9VTYYfhuz15Yf1IzCKgZj6/rIbYTmJCUSqr76r0Wg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.56.0", - "@typescript-eslint/type-utils": "8.56.0", - "@typescript-eslint/utils": "8.56.0", - "@typescript-eslint/visitor-keys": "8.56.0", + "@typescript-eslint/scope-manager": "8.58.0", + "@typescript-eslint/type-utils": "8.58.0", + "@typescript-eslint/utils": "8.58.0", + "@typescript-eslint/visitor-keys": "8.58.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", - "ts-api-utils": "^2.4.0" + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -925,9 +885,9 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.56.0", + "@typescript-eslint/parser": "^8.58.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { @@ -941,16 +901,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.0.tgz", - "integrity": "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==", + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.58.0.tgz", + "integrity": "sha512-rLoGZIf9afaRBYsPUMtvkDWykwXwUPL60HebR4JgTI8mxfFe2cQTu3AGitANp4b9B2QlVru6WzjgB2IzJKiCSA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.56.0", - "@typescript-eslint/types": "8.56.0", - "@typescript-eslint/typescript-estree": "8.56.0", - "@typescript-eslint/visitor-keys": "8.56.0", + "@typescript-eslint/scope-manager": "8.58.0", + "@typescript-eslint/types": "8.58.0", + "@typescript-eslint/typescript-estree": "8.58.0", + "@typescript-eslint/visitor-keys": "8.58.0", "debug": "^4.4.3" }, "engines": { @@ -962,18 +922,18 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.0.tgz", - "integrity": "sha512-M3rnyL1vIQOMeWxTWIW096/TtVP+8W3p/XnaFflhmcFp+U4zlxUxWj4XwNs6HbDeTtN4yun0GNTTDBw/SvufKg==", + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.58.0.tgz", + "integrity": "sha512-8Q/wBPWLQP1j16NxoPNIKpDZFMaxl7yWIoqXWYeWO+Bbd2mjgvoF0dxP2jKZg5+x49rgKdf7Ck473M8PC3V9lg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.56.0", - "@typescript-eslint/types": "^8.56.0", + "@typescript-eslint/tsconfig-utils": "^8.58.0", + "@typescript-eslint/types": "^8.58.0", "debug": "^4.4.3" }, "engines": { @@ -984,18 +944,18 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.0.tgz", - "integrity": "sha512-7UiO/XwMHquH+ZzfVCfUNkIXlp/yQjjnlYUyYz7pfvlK3/EyyN6BK+emDmGNyQLBtLGaYrTAI6KOw8tFucWL2w==", + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.58.0.tgz", + "integrity": "sha512-W1Lur1oF50FxSnNdGp3Vs6P+yBRSmZiw4IIjEeYxd8UQJwhUF0gDgDD/W/Tgmh73mxgEU3qX0Bzdl/NGuSPEpQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.56.0", - "@typescript-eslint/visitor-keys": "8.56.0" + "@typescript-eslint/types": "8.58.0", + "@typescript-eslint/visitor-keys": "8.58.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1006,9 +966,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.0.tgz", - "integrity": "sha512-bSJoIIt4o3lKXD3xmDh9chZcjCz5Lk8xS7Rxn+6l5/pKrDpkCwtQNQQwZ2qRPk7TkUYhrq3WPIHXOXlbXP0itg==", + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.58.0.tgz", + "integrity": "sha512-doNSZEVJsWEu4htiVC+PR6NpM+pa+a4ClH9INRWOWCUzMst/VA9c4gXq92F8GUD1rwhNvRLkgjfYtFXegXQF7A==", "dev": true, "license": "MIT", "engines": { @@ -1019,21 +979,21 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.56.0.tgz", - "integrity": "sha512-qX2L3HWOU2nuDs6GzglBeuFXviDODreS58tLY/BALPC7iu3Fa+J7EOTwnX9PdNBxUI7Uh0ntP0YWGnxCkXzmfA==", + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.58.0.tgz", + "integrity": "sha512-aGsCQImkDIqMyx1u4PrVlbi/krmDsQUs4zAcCV6M7yPcPev+RqVlndsJy9kJ8TLihW9TZ0kbDAzctpLn5o+lOg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.56.0", - "@typescript-eslint/typescript-estree": "8.56.0", - "@typescript-eslint/utils": "8.56.0", + "@typescript-eslint/types": "8.58.0", + "@typescript-eslint/typescript-estree": "8.58.0", + "@typescript-eslint/utils": "8.58.0", "debug": "^4.4.3", - "ts-api-utils": "^2.4.0" + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1044,13 +1004,13 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.0.tgz", - "integrity": "sha512-DBsLPs3GsWhX5HylbP9HNG15U0bnwut55Lx12bHB9MpXxQ+R5GC8MwQe+N1UFXxAeQDvEsEDY6ZYwX03K7Z6HQ==", + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.58.0.tgz", + "integrity": "sha512-O9CjxypDT89fbHxRfETNoAnHj/i6IpRK0CvbVN3qibxlLdo5p5hcLmUuCCrHMpxiWSwKyI8mCP7qRNYuOJ0Uww==", "dev": true, "license": "MIT", "engines": { @@ -1062,21 +1022,21 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.0.tgz", - "integrity": "sha512-ex1nTUMWrseMltXUHmR2GAQ4d+WjkZCT4f+4bVsps8QEdh0vlBsaCokKTPlnqBFqqGaxilDNJG7b8dolW2m43Q==", + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.58.0.tgz", + "integrity": "sha512-7vv5UWbHqew/dvs+D3e1RvLv1v2eeZ9txRHPnEEBUgSNLx5ghdzjHa0sgLWYVKssH+lYmV0JaWdoubo0ncGYLA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.56.0", - "@typescript-eslint/tsconfig-utils": "8.56.0", - "@typescript-eslint/types": "8.56.0", - "@typescript-eslint/visitor-keys": "8.56.0", + "@typescript-eslint/project-service": "8.58.0", + "@typescript-eslint/tsconfig-utils": "8.58.0", + "@typescript-eslint/types": "8.58.0", + "@typescript-eslint/visitor-keys": "8.58.0", "debug": "^4.4.3", - "minimatch": "^9.0.5", + "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.4.0" + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1086,20 +1046,20 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/utils": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.0.tgz", - "integrity": "sha512-RZ3Qsmi2nFGsS+n+kjLAYDPVlrzf7UhTffrDIKr+h2yzAlYP/y5ZulU0yeDEPItos2Ph46JAL5P/On3pe7kDIQ==", + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.58.0.tgz", + "integrity": "sha512-RfeSqcFeHMHlAWzt4TBjWOAtoW9lnsAGiP3GbaX9uVgTYYrMbVnGONEfUCiSss+xMHFl+eHZiipmA8WkQ7FuNA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.56.0", - "@typescript-eslint/types": "8.56.0", - "@typescript-eslint/typescript-estree": "8.56.0" + "@typescript-eslint/scope-manager": "8.58.0", + "@typescript-eslint/types": "8.58.0", + "@typescript-eslint/typescript-estree": "8.58.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1110,17 +1070,17 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.0.tgz", - "integrity": "sha512-q+SL+b+05Ud6LbEE35qe4A99P+htKTKVbyiNEe45eCbJFyh/HVK9QXwlrbz+Q4L8SOW4roxSVwXYj4DMBT7Ieg==", + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.0.tgz", + "integrity": "sha512-XJ9UD9+bbDo4a4epraTwG3TsNPeiB9aShrUneAVXy8q4LuwowN+qu89/6ByLMINqvIMeI9H9hOHQtg/ijrYXzQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/types": "8.58.0", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -1132,9 +1092,9 @@ } }, "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.0.tgz", - "integrity": "sha512-A0XeIi7CXU7nPlfHS9loMYEKxUaONu/hTEzHTGba9Huu94Cq1hPivf+DE5erJozZOky0LfvXAyrV/tcswpLI0Q==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1145,13 +1105,13 @@ } }, "node_modules/@vitejs/plugin-vue": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.5.tgz", - "integrity": "sha512-bL3AxKuQySfk1iGcBsQnoRVexTPJq0Z/ixFVM8OhVJAP6ZXXXLtM7NFKWhLl30Kg7uTBqIaPXbh+nuQCuBDedg==", + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.6.tgz", + "integrity": "sha512-u9HHgfrq3AjXlysn0eINFnWQOJQLO9WN6VprZ8FXl7A2bYisv3Hui9Ij+7QZ41F/WYWarHjwBbXtD7dKg3uxbg==", "dev": true, "license": "MIT", "dependencies": { - "@rolldown/pluginutils": "1.0.0-rc.2" + "@rolldown/pluginutils": "1.0.0-rc.13" }, "engines": { "node": "^20.19.0 || >=22.12.0" @@ -1218,39 +1178,39 @@ } }, "node_modules/@vue/compiler-core": { - "version": "3.5.30", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.30.tgz", - "integrity": "sha512-s3DfdZkcu/qExZ+td75015ljzHc6vE+30cFMGRPROYjqkroYI5NV2X1yAMX9UeyBNWB9MxCfPcsjpLS11nzkkw==", + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.32.tgz", + "integrity": "sha512-4x74Tbtqnda8s/NSD6e1Dr5p1c8HdMU5RWSjMSUzb8RTcUQqevDCxVAitcLBKT+ie3o0Dl9crc/S/opJM7qBGQ==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.29.0", - "@vue/shared": "3.5.30", + "@babel/parser": "^7.29.2", + "@vue/shared": "3.5.32", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "node_modules/@vue/compiler-dom": { - "version": "3.5.30", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.30.tgz", - "integrity": "sha512-eCFYESUEVYHhiMuK4SQTldO3RYxyMR/UQL4KdGD1Yrkfdx4m/HYuZ9jSfPdA+nWJY34VWndiYdW/wZXyiPEB9g==", + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.32.tgz", + "integrity": "sha512-ybHAu70NtiEI1fvAUz3oXZqkUYEe5J98GjMDpTGl5iHb0T15wQYLR4wE3h9xfuTNA+Cm2f4czfe8B4s+CCH57Q==", "license": "MIT", "dependencies": { - "@vue/compiler-core": "3.5.30", - "@vue/shared": "3.5.30" + "@vue/compiler-core": "3.5.32", + "@vue/shared": "3.5.32" } }, "node_modules/@vue/compiler-sfc": { - "version": "3.5.30", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.30.tgz", - "integrity": "sha512-LqmFPDn89dtU9vI3wHJnwaV6GfTRD87AjWpTWpyrdVOObVtjIuSeZr181z5C4PmVx/V3j2p+0f7edFKGRMpQ5A==", + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.32.tgz", + "integrity": "sha512-8UYUYo71cP/0YHMO814TRZlPuUUw3oifHuMR7Wp9SNoRSrxRQnhMLNlCeaODNn6kNTJsjFoQ/kqIj4qGvya4Xg==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.29.0", - "@vue/compiler-core": "3.5.30", - "@vue/compiler-dom": "3.5.30", - "@vue/compiler-ssr": "3.5.30", - "@vue/shared": "3.5.30", + "@babel/parser": "^7.29.2", + "@vue/compiler-core": "3.5.32", + "@vue/compiler-dom": "3.5.32", + "@vue/compiler-ssr": "3.5.32", + "@vue/shared": "3.5.32", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", "postcss": "^8.5.8", @@ -1258,13 +1218,13 @@ } }, "node_modules/@vue/compiler-ssr": { - "version": "3.5.30", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.30.tgz", - "integrity": "sha512-NsYK6OMTnx109PSL2IAyf62JP6EUdk4Dmj6AkWcJGBvN0dQoMYtVekAmdqgTtWQgEJo+Okstbf/1p7qZr5H+bA==", + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.32.tgz", + "integrity": "sha512-Gp4gTs22T3DgRotZ8aA/6m2jMR+GMztvBXUBEUOYOcST+giyGWJ4WvFd7QLHBkzTxkfOt8IELKNdpzITLbA2rw==", "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.30", - "@vue/shared": "3.5.30" + "@vue/compiler-dom": "3.5.32", + "@vue/shared": "3.5.32" } }, "node_modules/@vue/devtools-api": { @@ -1358,9 +1318,9 @@ } }, "node_modules/@vue/language-core/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", "engines": { @@ -1371,53 +1331,53 @@ } }, "node_modules/@vue/reactivity": { - "version": "3.5.30", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.30.tgz", - "integrity": "sha512-179YNgKATuwj9gB+66snskRDOitDiuOZqkYia7mHKJaidOMo/WJxHKF8DuGc4V4XbYTJANlfEKb0yxTQotnx4Q==", + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.32.tgz", + "integrity": "sha512-/ORasxSGvZ6MN5gc+uE364SxFdJ0+WqVG0CENXaGW58TOCdrAW76WWaplDtECeS1qphvtBZtR+3/o1g1zL4xPQ==", "license": "MIT", "dependencies": { - "@vue/shared": "3.5.30" + "@vue/shared": "3.5.32" } }, "node_modules/@vue/runtime-core": { - "version": "3.5.30", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.30.tgz", - "integrity": "sha512-e0Z+8PQsUTdwV8TtEsLzUM7SzC7lQwYKePydb7K2ZnmS6jjND+WJXkmmfh/swYzRyfP1EY3fpdesyYoymCzYfg==", + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.32.tgz", + "integrity": "sha512-pDrXCejn4UpFDFmMd27AcJEbHaLemaE5o4pbb7sLk79SRIhc6/t34BQA7SGNgYtbMnvbF/HHOftYBgFJtUoJUQ==", "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.30", - "@vue/shared": "3.5.30" + "@vue/reactivity": "3.5.32", + "@vue/shared": "3.5.32" } }, "node_modules/@vue/runtime-dom": { - "version": "3.5.30", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.30.tgz", - "integrity": "sha512-2UIGakjU4WSQ0T4iwDEW0W7vQj6n7AFn7taqZ9Cvm0Q/RA2FFOziLESrDL4GmtI1wV3jXg5nMoJSYO66egDUBw==", + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.32.tgz", + "integrity": "sha512-1CDVv7tv/IV13V8Nip1k/aaObVbWqRlVCVezTwx3K07p7Vxossp5JU1dcPNhJk3w347gonIUT9jQOGutyJrSVQ==", "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.30", - "@vue/runtime-core": "3.5.30", - "@vue/shared": "3.5.30", + "@vue/reactivity": "3.5.32", + "@vue/runtime-core": "3.5.32", + "@vue/shared": "3.5.32", "csstype": "^3.2.3" } }, "node_modules/@vue/server-renderer": { - "version": "3.5.30", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.30.tgz", - "integrity": "sha512-v+R34icapydRwbZRD0sXwtHqrQJv38JuMB4JxbOxd8NEpGLny7cncMp53W9UH/zo4j8eDHjQ1dEJXwzFQknjtQ==", + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.32.tgz", + "integrity": "sha512-IOjm2+JQwRFS7W28HNuJeXQle9KdZbODFY7hFGVtnnghF51ta20EWAZJHX+zLGtsHhaU6uC9BGPV52KVpYryMQ==", "license": "MIT", "dependencies": { - "@vue/compiler-ssr": "3.5.30", - "@vue/shared": "3.5.30" + "@vue/compiler-ssr": "3.5.32", + "@vue/shared": "3.5.32" }, "peerDependencies": { - "vue": "3.5.30" + "vue": "3.5.32" } }, "node_modules/@vue/shared": { - "version": "3.5.30", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.30.tgz", - "integrity": "sha512-YXgQ7JjaO18NeK2K9VTbDHaFy62WrObMa6XERNfNOkAhD1F1oDSf3ZJ7K6GqabZ0BvSDHajp8qfS5Sa2I9n8uQ==", + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.32.tgz", + "integrity": "sha512-ksNyrmRQzWJJ8n3cRDuSF7zNNontuJg1YHnmWRJd2AMu8Ij2bqwiiri2lH5rHtYPZjj4STkNcgcmiQqlOjiYGg==", "license": "MIT" }, "node_modules/@vue/tsconfig": { @@ -1706,13 +1666,26 @@ "license": "MIT" }, "node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/brace-expansion/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" } }, "node_modules/braces": { @@ -2722,18 +2695,18 @@ } }, "node_modules/eslint": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.1.0.tgz", - "integrity": "sha512-S9jlY/ELKEUwwQnqWDO+f+m6sercqOPSqXM5Go94l7DOmxHVDgmSFGWEzeE/gwgTAr0W103BWt0QLe/7mabIvA==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.2.0.tgz", + "integrity": "sha512-+L0vBFYGIpSNIt/KWTpFonPrqYvgKw1eUI5Vn7mEogrQcWtWYtNQ7dNqC+px/J0idT3BAkiWrhfS7k+Tum8TUA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", - "@eslint/config-array": "^0.23.3", - "@eslint/config-helpers": "^0.5.3", - "@eslint/core": "^1.1.1", - "@eslint/plugin-kit": "^0.6.1", + "@eslint/config-array": "^0.23.4", + "@eslint/config-helpers": "^0.5.4", + "@eslint/core": "^1.2.0", + "@eslint/plugin-kit": "^0.7.0", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", @@ -2888,29 +2861,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/eslint/node_modules/brace-expansion": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, "node_modules/eslint/node_modules/eslint-visitor-keys": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", @@ -2924,22 +2874,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/minimatch": { - "version": "10.2.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/espree": { "version": "11.2.0", "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", @@ -3320,12 +3254,16 @@ } }, "node_modules/fuse.js": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.1.0.tgz", - "integrity": "sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.3.0.tgz", + "integrity": "sha512-plz8RVjfcDedTGfVngWH1jmJvBvAwi1v2jecfDerbEnMcmOYUEEwKFTHbNoCiYyzaK2Ws8lABkTCcRSqCY1q4w==", "license": "Apache-2.0", "engines": { "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/krisk" } }, "node_modules/generator-function": { @@ -4716,16 +4654,16 @@ } }, "node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^2.0.2" + "brace-expansion": "^5.0.5" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -5325,9 +5263,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "license": "MIT", "engines": { "node": ">=8.6" @@ -5371,13 +5309,13 @@ } }, "node_modules/playwright": { - "version": "1.58.2", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", - "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==", + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz", + "integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.58.2" + "playwright-core": "1.59.1" }, "bin": { "playwright": "cli.js" @@ -5390,9 +5328,9 @@ } }, "node_modules/playwright-core": { - "version": "1.58.2", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", - "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz", + "integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==", "dev": true, "license": "Apache-2.0", "bin": { @@ -5477,9 +5415,9 @@ } }, "node_modules/prettier": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", - "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", + "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", "dev": true, "license": "MIT", "bin": { @@ -5763,14 +5701,14 @@ "license": "Unlicense" }, "node_modules/rolldown": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.12.tgz", - "integrity": "sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.15.tgz", + "integrity": "sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.122.0", - "@rolldown/pluginutils": "1.0.0-rc.12" + "@oxc-project/types": "=0.124.0", + "@rolldown/pluginutils": "1.0.0-rc.15" }, "bin": { "rolldown": "bin/cli.mjs" @@ -5779,27 +5717,27 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.0-rc.12", - "@rolldown/binding-darwin-arm64": "1.0.0-rc.12", - "@rolldown/binding-darwin-x64": "1.0.0-rc.12", - "@rolldown/binding-freebsd-x64": "1.0.0-rc.12", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.12", - "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.12", - "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.12", - "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.12", - "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.12", - "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.12", - "@rolldown/binding-linux-x64-musl": "1.0.0-rc.12", - "@rolldown/binding-openharmony-arm64": "1.0.0-rc.12", - "@rolldown/binding-wasm32-wasi": "1.0.0-rc.12", - "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.12", - "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.12" + "@rolldown/binding-android-arm64": "1.0.0-rc.15", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.15", + "@rolldown/binding-darwin-x64": "1.0.0-rc.15", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.15", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.15", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.15", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.15", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.15", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.15", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.15", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.15", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.15", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.15", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.15", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.15" } }, "node_modules/rolldown/node_modules/@rolldown/pluginutils": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.12.tgz", - "integrity": "sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.15.tgz", + "integrity": "sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==", "dev": true, "license": "MIT" }, @@ -5910,9 +5848,9 @@ "license": "MIT" }, "node_modules/sax": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.4.tgz", - "integrity": "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -6404,9 +6342,9 @@ } }, "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", "engines": { "node": ">=12" @@ -6437,9 +6375,9 @@ } }, "node_modules/ts-api-utils": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", - "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", "dev": true, "license": "MIT", "engines": { @@ -6563,9 +6501,9 @@ } }, "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "devOptional": true, "license": "Apache-2.0", "bin": { @@ -6577,16 +6515,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.56.0.tgz", - "integrity": "sha512-c7toRLrotJ9oixgdW7liukZpsnq5CZ7PuKztubGYlNppuTqhIoWfhgHo/7EU0v06gS2l/x0i2NEFK1qMIf0rIg==", + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.58.0.tgz", + "integrity": "sha512-e2TQzKfaI85fO+F3QywtX+tCTsu/D3WW5LVU6nz8hTFKFZ8yBJ6mSYRpXqdR3mFjPWmO0eWsTa5f+UpAOe/FMA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.56.0", - "@typescript-eslint/parser": "8.56.0", - "@typescript-eslint/typescript-estree": "8.56.0", - "@typescript-eslint/utils": "8.56.0" + "@typescript-eslint/eslint-plugin": "8.58.0", + "@typescript-eslint/parser": "8.58.0", + "@typescript-eslint/typescript-estree": "8.58.0", + "@typescript-eslint/utils": "8.58.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6597,7 +6535,7 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/uc.micro": { @@ -6632,9 +6570,9 @@ } }, "node_modules/undici-types": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", - "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "version": "7.19.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", + "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", "license": "MIT" }, "node_modules/unicorn-magic": { @@ -6689,9 +6627,9 @@ } }, "node_modules/unplugin-utils/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", "engines": { "node": ">=12" @@ -6701,9 +6639,9 @@ } }, "node_modules/unplugin/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", "engines": { "node": ">=12" @@ -6750,16 +6688,16 @@ } }, "node_modules/vite": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.5.tgz", - "integrity": "sha512-nmu43Qvq9UopTRfMx2jOYW5l16pb3iDC1JH6yMuPkpVbzK0k+L7dfsEDH4jRgYFmsg0sTAqkojoZgzLMlwHsCQ==", + "version": "8.0.8", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.8.tgz", + "integrity": "sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==", "dev": true, "license": "MIT", "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.8", - "rolldown": "1.0.0-rc.12", + "rolldown": "1.0.0-rc.15", "tinyglobby": "^0.2.15" }, "bin": { @@ -6863,16 +6801,16 @@ "license": "MIT" }, "node_modules/vue": { - "version": "3.5.30", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.30.tgz", - "integrity": "sha512-hTHLc6VNZyzzEH/l7PFGjpcTvUgiaPK5mdLkbjrTeWSRcEfxFrv56g/XckIYlE9ckuobsdwqd5mk2g1sBkMewg==", + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.32.tgz", + "integrity": "sha512-vM4z4Q9tTafVfMAK7IVzmxg34rSzTFMyIe0UUEijUCkn9+23lj0WRfA83dg7eQZIUlgOSGrkViIaCfqSAUXsMw==", "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.30", - "@vue/compiler-sfc": "3.5.30", - "@vue/runtime-dom": "3.5.30", - "@vue/server-renderer": "3.5.30", - "@vue/shared": "3.5.30" + "@vue/compiler-dom": "3.5.32", + "@vue/compiler-sfc": "3.5.32", + "@vue/runtime-dom": "3.5.32", + "@vue/server-renderer": "3.5.32", + "@vue/shared": "3.5.32" }, "peerDependencies": { "typescript": "*" @@ -6966,9 +6904,9 @@ } }, "node_modules/vue-router/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", "engines": { "node": ">=12" diff --git a/webui/package.json b/webui/package.json index 033d4f8a5..1f9f2d3ed 100644 --- a/webui/package.json +++ b/webui/package.json @@ -21,7 +21,7 @@ "@ssthouse/vue3-tree-chart": "^0.3.0", "@types/bootstrap": "^5.2.10", "@types/mokapi": "^0.35.0", - "@types/nodemailer": "^7.0.11", + "@types/nodemailer": "^8.0.0", "@types/whatwg-mimetype": "^5.0.0", "ace-builds": "^1.43.5", "bootstrap": "^5.3.8", @@ -30,7 +30,7 @@ "dayjs": "^1.11.20", "del-cli": "^7.0.0", "express": "^5.2.1", - "fuse.js": "^7.1.0", + "fuse.js": "^7.3.0", "highlight.js": "^11.11.1", "http-status-codes": "^2.3.0", "js-yaml": "^4.1.1", @@ -41,28 +41,28 @@ "mime-types": "^3.0.2", "ncp": "^2.0.0", "nodemailer": "^8.0.5", - "vue": "^3.5.30", + "vue": "^3.5.32", "vue-router": "^5.0.4", "vue3-ace-editor": "^2.2.4", "whatwg-mimetype": "^5.0.0", "xml-formatter": "^3.7.0" }, "devDependencies": { - "@playwright/test": "^1.58.2", + "@playwright/test": "^1.59.1", "@rushstack/eslint-patch": "^1.16.1", "@types/js-yaml": "^4.0.9", "@types/markdown-it-container": "^4.0.0", - "@types/node": "^25.5.0", - "@vitejs/plugin-vue": "^6.0.5", + "@types/node": "^25.6.0", + "@vitejs/plugin-vue": "^6.0.6", "@vue/eslint-config-prettier": "^10.2.0", "@vue/eslint-config-typescript": "^14.7.0", "@vue/tsconfig": "^0.9.1", - "eslint": "^10.1.0", + "eslint": "^10.2.0", "eslint-plugin-vue": "^10.8.0", "npm-run-all": "^4.1.5", - "prettier": "^3.8.1", - "typescript": "~5.9.3", - "vite": "^8.0.5", + "prettier": "^3.8.3", + "typescript": "~6.0.3", + "vite": "^8.0.8", "vue-tsc": "^3.2.6", "xml2js": "^0.6.2" } diff --git a/webui/playwright.config.ts b/webui/playwright.config.ts index 6158261f9..f367bd38f 100644 --- a/webui/playwright.config.ts +++ b/webui/playwright.config.ts @@ -11,7 +11,7 @@ import { devices } from '@playwright/test' * See https://playwright.dev/docs/test-configuration. */ const config: PlaywrightTestConfig = { - testDir: './e2e', + testDir: './e2e/tests', /* Maximum time one test can run for. */ timeout: 30 * 1000, expect: { @@ -61,7 +61,7 @@ const config: PlaywrightTestConfig = { ] }, }, - testIgnore: ["/e2e/**/*.website.spec.ts"], + testIgnore: ["/e2e/tests/**/*.website.spec.ts"], }, { name: 'dashboard', @@ -81,7 +81,7 @@ const config: PlaywrightTestConfig = { ] }, }, - testIgnore: ["/e2e/**/*.website.spec.ts", "/e2e/dashboard-demo/**/*.spec.ts"], + testIgnore: ["/e2e/tests/**/*.website.spec.ts", "/e2e/tests/dashboard-demo/**/*.spec.ts"], }, { name: 'website', @@ -101,7 +101,7 @@ const config: PlaywrightTestConfig = { ] }, }, - testIgnore: ["/e2e/**/*.dashboard.spec.ts", "/e2e/dashboard/**/*.spec.ts"], + testIgnore: ["/e2e/tests/**/*.dashboard.spec.ts", "/e2e/tests/dashboard/**/*.spec.ts"], }, // { // name: 'firefox', diff --git a/webui/src/assets/dashboard.css b/webui/src/assets/dashboard.css index c4f7c4d3b..92326061f 100644 --- a/webui/src/assets/dashboard.css +++ b/webui/src/assets/dashboard.css @@ -188,4 +188,46 @@ .dashboard-tabs.demo { margin-top: 0.8rem; } +} + +.table-markdown { + display: -webkit-box; + -webkit-line-clamp: 3; + line-clamp: 3; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.table-markdown * { + margin: 0; + padding: 0; +} + +.table-markdown h1, +.table-markdown h2, +.table-markdown h3, +.table-markdown h4 { + font-size: inherit; + font-weight: 600; +} + +.table-markdown p { + display: inline; +} + +.table-markdown ul, +.table-markdown ol { + display: inline; + list-style: none; +} + +.table-markdown li::before { + content: "• "; +} + +.table-markdown code { + font-size: 0.9em; + background: #f5f5f5; + padding: 1px 4px; + border-radius: 3px; } \ No newline at end of file diff --git a/webui/src/components/dashboard/SchemaValidate.vue b/webui/src/components/dashboard/SchemaValidate.vue index 2178aed52..335cdc503 100644 --- a/webui/src/components/dashboard/SchemaValidate.vue +++ b/webui/src/components/dashboard/SchemaValidate.vue @@ -35,6 +35,7 @@ const props = withDefaults(defineProps<{ }>(), { title: 'Data Validator' }) +console.log(props) const { createGuid } = useGuid() const { formatLanguage } = usePrettyLanguage() diff --git a/webui/src/components/dashboard/SourceView.vue b/webui/src/components/dashboard/SourceView.vue index fc9aa0f4f..437a941e5 100644 --- a/webui/src/components/dashboard/SourceView.vue +++ b/webui/src/components/dashboard/SourceView.vue @@ -46,6 +46,7 @@ if (props.source.preview) { } else { throw new Error('preview and binary not defined') } +console.log(current.value) watch(() => props.source, (source) => { if (!current.value) { return @@ -247,7 +248,7 @@ watch( -
+
-
- +
+
diff --git a/webui/src/components/dashboard/http/HttpServicesCard.vue b/webui/src/components/dashboard/http/HttpServicesCard.vue index 7ae91c159..424efe870 100644 --- a/webui/src/components/dashboard/http/HttpServicesCard.vue +++ b/webui/src/components/dashboard/http/HttpServicesCard.vue @@ -2,7 +2,7 @@ import { useMetrics } from '@/composables/metrics'; import { usePrettyDates } from '@/composables/usePrettyDate'; import { useRoute } from '@/router'; -import { computed, onUnmounted } from 'vue'; +import { onUnmounted } from 'vue'; import { useDashboard } from '@/composables/dashboard'; import { useMarkdown } from '@/composables/markdown'; @@ -12,19 +12,19 @@ const { service: serviceRoute, router } = useRoute() const { dashboard } = useDashboard() const { services, close } = dashboard.value.getServices('http') -function lastRequest(s: Service){ +function lastRequest(s: Service) { const n = max(s.metrics, 'http_request_timestamp') - if (n == 0){ + if (n == 0) { return '-' } return format(n) } -function requests(s: Service){ +function requests(s: Service) { return sum(s.metrics, 'http_requests_total') } -function errors(s: Service){ +function errors(s: Service) { return sum(s.metrics, 'http_requests_errors_total') } @@ -62,18 +62,21 @@ onUnmounted(() => { - + - {{ service.name }} + {{ service.name }} -
+ +
+ {{ lastRequest(service) }} {{ requests(service) }} / - {{ errors(service) }} + {{ errors(service) }} diff --git a/webui/src/components/dashboard/kafka/KafkaServicesCard.vue b/webui/src/components/dashboard/kafka/KafkaServicesCard.vue index bd727cb3a..08c446785 100644 --- a/webui/src/components/dashboard/kafka/KafkaServicesCard.vue +++ b/webui/src/components/dashboard/kafka/KafkaServicesCard.vue @@ -67,7 +67,7 @@ onUnmounted(() => { {{ service.name }} -
+
{{ lastMessage(service) }} {{ messages(service) }} diff --git a/webui/src/components/dashboard/kafka/KafkaTopics.vue b/webui/src/components/dashboard/kafka/KafkaTopics.vue index 727d7f4bf..28dd9b85f 100644 --- a/webui/src/components/dashboard/kafka/KafkaTopics.vue +++ b/webui/src/components/dashboard/kafka/KafkaTopics.vue @@ -167,7 +167,7 @@ function toggleTag(name: string) { -
+
{{ lastMessage(service, topic) }} {{ messages(service, topic) }} diff --git a/webui/src/components/dashboard/ldap/LdapServicesCard.vue b/webui/src/components/dashboard/ldap/LdapServicesCard.vue index 4a8afd7d6..73380144b 100644 --- a/webui/src/components/dashboard/ldap/LdapServicesCard.vue +++ b/webui/src/components/dashboard/ldap/LdapServicesCard.vue @@ -66,7 +66,7 @@ onUnmounted(() => { {{ service.name }} -
+
{{ lastRequest(service) }} {{ requests(service) }} diff --git a/webui/src/components/dashboard/mail/MailServicesCard.vue b/webui/src/components/dashboard/mail/MailServicesCard.vue index 9c95148c2..561ca0aae 100644 --- a/webui/src/components/dashboard/mail/MailServicesCard.vue +++ b/webui/src/components/dashboard/mail/MailServicesCard.vue @@ -67,7 +67,7 @@ onUnmounted(() => { {{ service.name }} -
+
{{ lastMail(service) }} {{ messages(service) }} diff --git a/webui/src/components/dashboard/mail/Mailboxes.vue b/webui/src/components/dashboard/mail/Mailboxes.vue index d5431ac9f..903805b84 100644 --- a/webui/src/components/dashboard/mail/Mailboxes.vue +++ b/webui/src/components/dashboard/mail/Mailboxes.vue @@ -73,7 +73,7 @@ function goToMailbox(mb: SmtpMailbox, openInNewTab = false){ {{ mb.username }} {{ mb.password }} -
+
{{ mb.numMessages }} diff --git a/webui/src/router/index.ts b/webui/src/router/index.ts index 5160b3f9f..46e65ab7c 100644 --- a/webui/src/router/index.ts +++ b/webui/src/router/index.ts @@ -362,23 +362,27 @@ const router = createRouter({ ] }) -router.afterEach((to, from) => { +// add refresh query parameter if clicked link does not have it but current route has it +router.beforeEach((to, from, next) => { if (!to.path.startsWith('/dashboard') || to.path.startsWith('/dashboard-demo')) { - return + return next() } const hadRefresh = !!from.query.refresh; const hasRefresh = !!to.query.refresh; if (hadRefresh && !hasRefresh) { - router.replace({ - ...to, + return next({ + path: to.path, query: { ...to.query, refresh: from.query.refresh - } - }); + }, + hash: to.hash, + }) } + + next() }); if (import.meta.env.VITE_DASHBOARD === 'true') { diff --git a/webui/tsconfig.json b/webui/tsconfig.json index 7ffcc799f..496acdca1 100644 --- a/webui/tsconfig.json +++ b/webui/tsconfig.json @@ -2,7 +2,6 @@ "extends": "@vue/tsconfig/tsconfig.dom.json", "include": ["env.d.ts", "src/**/*", "src/**/*.vue"], "compilerOptions": { - "baseUrl": ".", "paths": { "@/*": ["./src/*"] },