diff --git a/acceptance/cmd_test.go b/acceptance/cmd_test.go index 429acc517..458193e00 100644 --- a/acceptance/cmd_test.go +++ b/acceptance/cmd_test.go @@ -37,10 +37,10 @@ func Start(cfg *static.Config) (*Cmd, error) { feature.Enable(cfg.Features) registerDynamicTypes() - app := runtime.New(cfg) - generator.SetConfig(cfg.DataGen) watcher := server.NewConfigWatcher(cfg) + app := runtime.New(cfg, watcher) + generator.SetConfig(cfg.DataGen) certStore, err := cert.NewStore(cfg) if err != nil { diff --git a/api/handler_events_test.go b/api/handler_events_test.go index e05f8d85c..a85dd3450 100644 --- a/api/handler_events_test.go +++ b/api/handler_events_test.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "mokapi/config/dynamic/dynamictest" "mokapi/config/static" "mokapi/providers/asyncapi3/kafka/store" "mokapi/providers/openapi" @@ -202,7 +203,7 @@ func TestHandler_Events(t *testing.T) { tc := tc t.Run(tc.name, func(t *testing.T) { cfg := &static.Config{} - app := runtime.New(cfg) + app := runtime.New(cfg, &dynamictest.Reader{}) h := New(app, static.Api{}) tc.fn(t, h, app.Events) @@ -330,7 +331,7 @@ func TestHandler_KafkaEvents(t *testing.T) { tc := tc t.Run(tc.name, func(t *testing.T) { cfg := &static.Config{} - app := runtime.New(cfg) + app := runtime.New(cfg, &dynamictest.Reader{}) h := New(app, static.Api{}) tc.fn(t, h, app.Events) diff --git a/api/handler_fileserver_test.go b/api/handler_fileserver_test.go index 323ee6feb..8fc9033eb 100644 --- a/api/handler_fileserver_test.go +++ b/api/handler_fileserver_test.go @@ -4,6 +4,7 @@ import ( "fmt" "mokapi/api" "mokapi/config/dynamic" + "mokapi/config/dynamic/dynamictest" "mokapi/config/static" "mokapi/providers/openapi/openapitest" "mokapi/runtime" @@ -131,7 +132,7 @@ func TestHandler_FileServer(t *testing.T) { t.Parallel() cfg := &static.Config{} - h := api.New(runtime.New(cfg), tc.config) + h := api.New(runtime.New(cfg, &dynamictest.Reader{}), tc.config) tc.fn(t, h) }) } @@ -146,7 +147,7 @@ func TestOpenGraphInDashboard(t *testing.T) { name: "http service", test: func(t *testing.T) { cfg := &static.Config{} - app := runtime.New(cfg) + app := runtime.New(cfg, &dynamictest.Reader{}) app.AddHttp(&dynamic.Config{Info: dynamic.ConfigInfo{Url: mustParse("https://foo.bar")}, Data: openapitest.NewConfig("3.0", openapitest.WithInfo("Swagger Petstore", "1.0", "This is a sample server Petstore server."))}) h := api.New(app, static.Api{Path: "/mokapi", Dashboard: true}) try.Handler(t, @@ -166,7 +167,7 @@ func TestOpenGraphInDashboard(t *testing.T) { name: "http service path without summary and description", test: func(t *testing.T) { cfg := &static.Config{} - app := runtime.New(cfg) + app := runtime.New(cfg, &dynamictest.Reader{}) app.AddHttp(&dynamic.Config{Info: dynamic.ConfigInfo{Url: mustParse("https://foo.bar")}, Data: openapitest.NewConfig("3.0", openapitest.WithInfo("Swagger Petstore", "1.0", "This is a sample server Petstore server."), openapitest.WithPath("/pet/{petId}"), @@ -190,7 +191,7 @@ func TestOpenGraphInDashboard(t *testing.T) { name: "http service path with summary and description", test: func(t *testing.T) { cfg := &static.Config{} - app := runtime.New(cfg) + app := runtime.New(cfg, &dynamictest.Reader{}) app.AddHttp(&dynamic.Config{Info: dynamic.ConfigInfo{Url: mustParse("https://foo.bar")}, Data: openapitest.NewConfig("3.0", openapitest.WithInfo("Swagger Petstore", "1.0", "This is a sample server Petstore server."), openapitest.WithPath("/pet/{petId}", @@ -216,7 +217,7 @@ func TestOpenGraphInDashboard(t *testing.T) { name: "http service path with no summary but description", test: func(t *testing.T) { cfg := &static.Config{} - app := runtime.New(cfg) + app := runtime.New(cfg, &dynamictest.Reader{}) app.AddHttp(&dynamic.Config{Info: dynamic.ConfigInfo{Url: mustParse("https://foo.bar")}, Data: openapitest.NewConfig("3.0", openapitest.WithInfo("Swagger Petstore", "1.0", "This is a sample server Petstore server."), openapitest.WithPath("/pet/{petId}", @@ -241,7 +242,7 @@ func TestOpenGraphInDashboard(t *testing.T) { name: "http service endpoint no summary and no description", test: func(t *testing.T) { cfg := &static.Config{} - app := runtime.New(cfg) + app := runtime.New(cfg, &dynamictest.Reader{}) app.AddHttp(&dynamic.Config{Info: dynamic.ConfigInfo{Url: mustParse("https://foo.bar")}, Data: openapitest.NewConfig("3.0", openapitest.WithInfo("Swagger Petstore", "1.0", "This is a sample server Petstore server."), openapitest.WithPath("/pet/{petId}", @@ -266,7 +267,7 @@ func TestOpenGraphInDashboard(t *testing.T) { name: "http service endpoint get right path", test: func(t *testing.T) { cfg := &static.Config{} - app := runtime.New(cfg) + app := runtime.New(cfg, &dynamictest.Reader{}) app.AddHttp(&dynamic.Config{Info: dynamic.ConfigInfo{Url: mustParse("https://foo.bar")}, Data: openapitest.NewConfig("3.0", openapitest.WithInfo("Swagger Petstore", "1.0", "This is a sample server Petstore server."), openapitest.WithPath("/pet/{petId}", diff --git a/api/handler_http_test.go b/api/handler_http_test.go index 460636508..d303d9774 100644 --- a/api/handler_http_test.go +++ b/api/handler_http_test.go @@ -66,7 +66,7 @@ func TestHandler_Http(t *testing.T) { { name: "get http service info", app: func() *runtime.App { - app := runtime.New(&static.Config{}) + app := runtime.New(&static.Config{}, &dynamictest.Reader{}) cfg := &dynamic.Config{ Info: dynamictest.NewConfigInfo(), Data: openapitest.NewConfig("3.0.0", openapitest.WithInfo("foo", "1.0", "bar"), @@ -328,7 +328,7 @@ func TestHandler_Http(t *testing.T) { func TestHandler_Http_NotFound(t *testing.T) { cfg := &static.Config{} - h := New(runtime.New(cfg), static.Api{}) + h := New(runtime.New(cfg, &dynamictest.Reader{}), static.Api{}) try.Handler(t, http.MethodGet, diff --git a/api/handler_kafka.go b/api/handler_kafka.go index 4904b6082..94e8426db 100644 --- a/api/handler_kafka.go +++ b/api/handler_kafka.go @@ -17,6 +17,7 @@ import ( "time" "github.com/pkg/errors" + log "github.com/sirupsen/logrus" ) type kafkaSummary struct { @@ -564,10 +565,26 @@ func newTopic(t *store.Topic, ch *asyncapi3.Channel, cfg *asyncapi3.Config) topi } if msg.Payload != nil && msg.Payload.Value != nil { - m.Payload = &schemaInfo{Schema: msg.Payload.Value.Schema, Format: msg.Payload.Value.Format} + format := "" + if msf, ok := msg.Payload.Value.(*asyncapi3.MultiSchemaFormat); ok { + format = msf.Format + } + s, err := msg.Payload.GetSchema() + if err != nil { + log.Errorf("failed to get schema for message in topic '%s': %v", t.Name, err) + } + m.Payload = &schemaInfo{Schema: s, Format: format} } if msg.Headers != nil && msg.Headers.Value != nil { - m.Header = &schemaInfo{Schema: msg.Headers.Value.Schema, Format: msg.Headers.Value.Format} + format := "" + if msf, ok := msg.Headers.Value.(*asyncapi3.MultiSchemaFormat); ok { + format = msf.Format + } + s, err := msg.Headers.GetSchema() + if err != nil { + log.Errorf("failed to get schema for headers in topic '%s': %v", t.Name, err) + } + m.Header = &schemaInfo{Schema: s, Format: format} } if m.ContentType == "" { @@ -575,7 +592,11 @@ func newTopic(t *store.Topic, ch *asyncapi3.Channel, cfg *asyncapi3.Config) topi } if msg.Bindings.Kafka.Key != nil { - m.Key = &schemaInfo{Schema: msg.Bindings.Kafka.Key.Value.Schema} + s, err := msg.Bindings.Kafka.Key.GetSchema() + if err != nil { + log.Errorf("failed to get schema for key in topic '%s': %v", t.Name, err) + } + m.Key = &schemaInfo{Schema: s} } if result.Messages == nil { result.Messages = map[string]messageConfig{} diff --git a/api/handler_kafka_test.go b/api/handler_kafka_test.go index 9d158e3bf..e358f4b29 100644 --- a/api/handler_kafka_test.go +++ b/api/handler_kafka_test.go @@ -4,6 +4,7 @@ import ( "encoding/json" "mokapi/api" "mokapi/config/dynamic" + "mokapi/config/dynamic/asyncApi" "mokapi/config/dynamic/dynamictest" "mokapi/config/static" "mokapi/engine/enginetest" @@ -57,7 +58,7 @@ func TestHandler_Kafka(t *testing.T) { { name: "get kafka services", app: func() *runtime.App { - app := runtime.New(&static.Config{}) + app := runtime.New(&static.Config{}, &dynamictest.Reader{}) _, _ = app.Kafka.Add(&dynamic.Config{ Info: dynamic.ConfigInfo{Url: try.MustUrl("kafka.yaml")}, Data: asyncapi3test.NewConfig( @@ -91,7 +92,7 @@ func TestHandler_Kafka(t *testing.T) { { name: "get specific", app: func() *runtime.App { - app := runtime.New(&static.Config{}) + app := runtime.New(&static.Config{}, &dynamictest.Reader{}) cfg := &dynamic.Config{ Info: dynamictest.NewConfigInfo(), Data: asyncapi3test.NewConfig( @@ -221,7 +222,7 @@ func TestHandler_Kafka(t *testing.T) { { name: "get specific with group", app: func() *runtime.App { - app := runtime.New(&static.Config{}) + app := runtime.New(&static.Config{}, &dynamictest.Reader{}) app.Kafka.Set("foo", getKafkaInfoWithGroup(asyncapi3test.NewConfig( asyncapi3test.WithInfo("foo", "bar", "1.0"), asyncapi3test.WithServer("foo", "kafka", "foo.bar"), @@ -246,7 +247,7 @@ func TestHandler_Kafka(t *testing.T) { { name: "get specific with group no generation", app: func() *runtime.App { - app := runtime.New(&static.Config{}) + app := runtime.New(&static.Config{}, &dynamictest.Reader{}) app.Kafka.Set("foo", getKafkaInfoWithGroup(asyncapi3test.NewConfig( asyncapi3test.WithInfo("foo", "bar", "1.0"), asyncapi3test.WithServer("foo", "kafka", "foo.bar"), @@ -273,7 +274,7 @@ func TestHandler_Kafka(t *testing.T) { return t1 } - app := runtime.New(&static.Config{}) + app := runtime.New(&static.Config{}, &dynamictest.Reader{}) app.Kafka.Set("foo", getKafkaInfoWithGroup(asyncapi3test.NewConfig( asyncapi3test.WithInfo("foo", "bar", "1.0"), asyncapi3test.WithServer("foo", "kafka", "foo.bar"), @@ -321,7 +322,7 @@ func TestHandler_Kafka(t *testing.T) { { name: "get specific with topic and openapi schema", app: func() *runtime.App { - app := runtime.New(&static.Config{}) + app := runtime.New(&static.Config{}, &dynamictest.Reader{}) app.Kafka.Set("foo", getKafkaInfo(asyncapi3test.NewConfig( asyncapi3test.WithInfo("foo", "bar", "1.0"), asyncapi3test.WithChannel("foo", @@ -369,7 +370,7 @@ func TestHandler_KafkaAPI(t *testing.T) { { name: "get kafka topics but empty", app: func() *runtime.App { - app := runtime.New(&static.Config{}) + app := runtime.New(&static.Config{}, &dynamictest.Reader{}) _, _ = app.Kafka.Add(&dynamic.Config{ Info: dynamic.ConfigInfo{Url: try.MustUrl("kafka.yaml")}, Data: asyncapi3test.NewConfig( @@ -394,7 +395,7 @@ func TestHandler_KafkaAPI(t *testing.T) { { name: "get kafka topics with one topic", app: func() *runtime.App { - app := runtime.New(&static.Config{}) + app := runtime.New(&static.Config{}, &dynamictest.Reader{}) _, _ = app.Kafka.Add(&dynamic.Config{ Info: dynamic.ConfigInfo{Url: try.MustUrl("kafka.yaml")}, Data: asyncapi3test.NewConfig( @@ -424,7 +425,7 @@ func TestHandler_KafkaAPI(t *testing.T) { { name: "get specific kafka topic", app: func() *runtime.App { - app := runtime.New(&static.Config{}) + app := runtime.New(&static.Config{}, &dynamictest.Reader{}) _, _ = app.Kafka.Add(&dynamic.Config{ Info: dynamic.ConfigInfo{Url: try.MustUrl("kafka.yaml")}, Data: asyncapi3test.NewConfig( @@ -454,7 +455,7 @@ func TestHandler_KafkaAPI(t *testing.T) { { name: "get specific kafka topic but not found", app: func() *runtime.App { - app := runtime.New(&static.Config{}) + app := runtime.New(&static.Config{}, &dynamictest.Reader{}) _, _ = app.Kafka.Add(&dynamic.Config{ Info: dynamic.ConfigInfo{Url: try.MustUrl("kafka.yaml")}, Data: asyncapi3test.NewConfig( @@ -482,7 +483,7 @@ func TestHandler_KafkaAPI(t *testing.T) { { name: "produce kafka message into topic", app: func() *runtime.App { - app := runtime.New(&static.Config{}) + app := runtime.New(&static.Config{}, &dynamictest.Reader{}) msg := asyncapi3test.NewMessage( asyncapi3test.WithContentType("application/json"), @@ -532,7 +533,7 @@ func TestHandler_KafkaAPI(t *testing.T) { { name: "produce kafka message into topic using binary", app: func() *runtime.App { - app := runtime.New(&static.Config{}) + app := runtime.New(&static.Config{}, &dynamictest.Reader{}) _, _ = app.Kafka.Add(&dynamic.Config{ Info: dynamic.ConfigInfo{Url: try.MustUrl("kafka.yaml")}, Data: asyncapi3test.NewConfig( @@ -571,7 +572,7 @@ func TestHandler_KafkaAPI(t *testing.T) { { name: "produce invalid kafka message into topic", app: func() *runtime.App { - app := runtime.New(&static.Config{}) + app := runtime.New(&static.Config{}, &dynamictest.Reader{}) msg := asyncapi3test.NewMessage( asyncapi3test.WithContentType("application/json"), @@ -627,7 +628,7 @@ func TestHandler_KafkaAPI(t *testing.T) { { name: "get kafka partitions", app: func() *runtime.App { - app := runtime.New(&static.Config{}) + app := runtime.New(&static.Config{}, &dynamictest.Reader{}) _, _ = app.Kafka.Add(&dynamic.Config{ Info: dynamic.ConfigInfo{Url: try.MustUrl("kafka.yaml")}, Data: asyncapi3test.NewConfig( @@ -656,7 +657,7 @@ func TestHandler_KafkaAPI(t *testing.T) { { name: "get specific kafka partition", app: func() *runtime.App { - app := runtime.New(&static.Config{}) + app := runtime.New(&static.Config{}, &dynamictest.Reader{}) _, _ = app.Kafka.Add(&dynamic.Config{ Info: dynamic.ConfigInfo{Url: try.MustUrl("kafka.yaml")}, Data: asyncapi3test.NewConfig( @@ -685,7 +686,7 @@ func TestHandler_KafkaAPI(t *testing.T) { { name: "produce kafka message into specific partition", app: func() *runtime.App { - app := runtime.New(&static.Config{}) + app := runtime.New(&static.Config{}, &dynamictest.Reader{}) msg := asyncapi3test.NewMessage( asyncapi3test.WithContentType("application/json"), @@ -739,7 +740,7 @@ func TestHandler_KafkaAPI(t *testing.T) { { name: "produce kafka message into specific partition using XML", app: func() *runtime.App { - app := runtime.New(&static.Config{}) + app := runtime.New(&static.Config{}, &dynamictest.Reader{}) msg := asyncapi3test.NewMessage( asyncapi3test.WithContentType("application/xml"), @@ -801,7 +802,7 @@ func TestHandler_KafkaAPI(t *testing.T) { { name: "produce kafka message into specific partition using plain XML string", app: func() *runtime.App { - app := runtime.New(&static.Config{}) + app := runtime.New(&static.Config{}, &dynamictest.Reader{}) msg := asyncapi3test.NewMessage( asyncapi3test.WithContentType("application/xml"), @@ -853,7 +854,7 @@ func TestHandler_KafkaAPI(t *testing.T) { { name: "produce invalid kafka message into specific partition using plain XML string", app: func() *runtime.App { - app := runtime.New(&static.Config{}) + app := runtime.New(&static.Config{}, &dynamictest.Reader{}) msg := asyncapi3test.NewMessage( asyncapi3test.WithContentType("application/xml"), @@ -912,7 +913,7 @@ func TestHandler_KafkaAPI(t *testing.T) { { name: "get records", app: func() *runtime.App { - app := runtime.New(&static.Config{}) + app := runtime.New(&static.Config{}, &dynamictest.Reader{}) msg := asyncapi3test.NewMessage( asyncapi3test.WithContentType("application/json"), @@ -970,7 +971,7 @@ func TestHandler_KafkaAPI(t *testing.T) { { name: "get specific record", app: func() *runtime.App { - app := runtime.New(&static.Config{}) + app := runtime.New(&static.Config{}, &dynamictest.Reader{}) msg := asyncapi3test.NewMessage( asyncapi3test.WithContentType("application/json"), @@ -1035,7 +1036,7 @@ func TestHandler_KafkaAPI(t *testing.T) { } func TestHandler_Kafka_NotFound(t *testing.T) { - h := api.New(runtime.New(&static.Config{}), static.Api{}) + h := api.New(runtime.New(&static.Config{}, &dynamictest.Reader{}), static.Api{}) try.Handler(t, http.MethodGet, @@ -1096,6 +1097,13 @@ func TestHandler_Kafka_Metrics(t *testing.T) { } } +func Test_IsAsyncApiConfig(t *testing.T) { + v2 := &asyncApi.Config{Info: asyncApi.Info{Name: "foo"}} + _, ok := runtime.IsAsyncApiConfig(&dynamic.Config{Data: v2}) + require.True(t, ok) + +} + func getKafkaInfo(config *asyncapi3.Config) *runtime.KafkaInfo { return &runtime.KafkaInfo{ Config: config, diff --git a/api/handler_ldap_test.go b/api/handler_ldap_test.go index 7f3c1d2c0..3dc3b9049 100644 --- a/api/handler_ldap_test.go +++ b/api/handler_ldap_test.go @@ -32,7 +32,7 @@ func TestHandler_Ldap(t *testing.T) { { name: "get ldap services", app: func() *runtime.App { - app := runtime.New(&static.Config{}) + app := runtime.New(&static.Config{}, &dynamictest.Reader{}) app.Ldap.Set("foo", &runtime.LdapInfo{ Config: &directory.Config{Info: directory.Info{Name: "foo", Description: "bar", Version: "1.0"}}, }) @@ -45,7 +45,7 @@ func TestHandler_Ldap(t *testing.T) { { name: "get ldap service", app: func() *runtime.App { - app := runtime.New(&static.Config{}) + app := runtime.New(&static.Config{}, &dynamictest.Reader{}) cfg := &dynamic.Config{ Info: dynamictest.NewConfigInfo(), Data: &directory.Config{ diff --git a/api/handler_mail_test.go b/api/handler_mail_test.go index c99de6db7..0edfc85ce 100644 --- a/api/handler_mail_test.go +++ b/api/handler_mail_test.go @@ -34,7 +34,7 @@ func TestHandler_Smtp(t *testing.T) { { name: "get smtp services", app: func() *runtime.App { - app := runtime.New(&static.Config{}) + app := runtime.New(&static.Config{}, &dynamictest.Reader{}) app.Mail.Set("foo", &runtime.MailInfo{ Config: &mail.Config{Info: mail.Info{Name: "foo", Description: "bar", Version: "2.1"}}, Store: &mail.Store{}, @@ -48,7 +48,7 @@ func TestHandler_Smtp(t *testing.T) { { name: "get /api/services/mail", app: func() *runtime.App { - app := runtime.New(&static.Config{}) + app := runtime.New(&static.Config{}, &dynamictest.Reader{}) app.Mail.Set("foo", &runtime.MailInfo{ Config: &mail.Config{Info: mail.Info{Name: "foo"}}, Store: &mail.Store{}, @@ -62,7 +62,7 @@ func TestHandler_Smtp(t *testing.T) { { name: "get smtp service", app: func() *runtime.App { - app := runtime.New(&static.Config{}) + app := runtime.New(&static.Config{}, &dynamictest.Reader{}) cfg := &dynamic.Config{ Info: dynamictest.NewConfigInfo(), Data: &mail.Config{ @@ -82,7 +82,7 @@ func TestHandler_Smtp(t *testing.T) { { name: "get smtp service with mailbox", app: func() *runtime.App { - app := runtime.New(&static.Config{}) + app := runtime.New(&static.Config{}, &dynamictest.Reader{}) app.Mail.Set("foo", &runtime.MailInfo{ Config: &mail.Config{ Info: mail.Info{Name: "foo"}, @@ -106,7 +106,7 @@ func TestHandler_Smtp(t *testing.T) { { name: "get smtp service with rules", app: func() *runtime.App { - app := runtime.New(&static.Config{}) + app := runtime.New(&static.Config{}, &dynamictest.Reader{}) app.Mail.Set("foo", &runtime.MailInfo{ Config: &mail.Config{ Info: mail.Info{Name: "foo"}, @@ -130,7 +130,7 @@ func TestHandler_Smtp(t *testing.T) { { name: "get smtp mailboxes", app: func() *runtime.App { - app := runtime.New(&static.Config{}) + app := runtime.New(&static.Config{}, &dynamictest.Reader{}) app.Mail.Set("foo", &runtime.MailInfo{ Store: &mail.Store{ Mailboxes: map[string]*mail.Mailbox{ @@ -152,7 +152,7 @@ func TestHandler_Smtp(t *testing.T) { { name: "get smtp mailbox", app: func() *runtime.App { - app := runtime.New(&static.Config{}) + app := runtime.New(&static.Config{}, &dynamictest.Reader{}) app.Mail.Set("foo", &runtime.MailInfo{ Store: &mail.Store{ Mailboxes: map[string]*mail.Mailbox{ @@ -173,7 +173,7 @@ func TestHandler_Smtp(t *testing.T) { { name: "get smtp mailbox folder", app: func() *runtime.App { - app := runtime.New(&static.Config{}) + app := runtime.New(&static.Config{}, &dynamictest.Reader{}) app.Mail.Set("foo", &runtime.MailInfo{ Store: &mail.Store{ Mailboxes: map[string]*mail.Mailbox{ @@ -208,7 +208,7 @@ func TestHandler_Smtp(t *testing.T) { { name: "get messages in mailbox", app: func() *runtime.App { - app := runtime.New(&static.Config{}) + app := runtime.New(&static.Config{}, &dynamictest.Reader{}) app.Mail.Set("foo", &runtime.MailInfo{ Store: &mail.Store{ Mailboxes: map[string]*mail.Mailbox{ @@ -254,7 +254,7 @@ func TestHandler_Smtp(t *testing.T) { { name: "get smtp mail", app: func() *runtime.App { - app := runtime.New(&static.Config{}) + app := runtime.New(&static.Config{}, &dynamictest.Reader{}) app.Mail.Set("foo", &runtime.MailInfo{ Config: &mail.Config{}, Store: &mail.Store{ @@ -293,7 +293,7 @@ func TestHandler_Smtp(t *testing.T) { { name: "get smtp mail attachment content", app: func() *runtime.App { - app := runtime.New(&static.Config{}) + app := runtime.New(&static.Config{}, &dynamictest.Reader{}) app.Mail.Set("foo", &runtime.MailInfo{ Store: &mail.Store{ Mailboxes: map[string]*mail.Mailbox{ @@ -333,7 +333,7 @@ func TestHandler_Smtp(t *testing.T) { { name: "get SMTP mail with encodings", app: func() *runtime.App { - app := runtime.New(&static.Config{}) + app := runtime.New(&static.Config{}, &dynamictest.Reader{}) app.Mail.Set("foo", &runtime.MailInfo{ Config: &mail.Config{}, Store: &mail.Store{ diff --git a/api/handler_schema_asyncapi.go b/api/handler_schema_asyncapi.go index 7b00a5d79..e77a39829 100644 --- a/api/handler_schema_asyncapi.go +++ b/api/handler_schema_asyncapi.go @@ -55,7 +55,7 @@ func getAsyncApiExample(w http.ResponseWriter, r *http.Request, cfg *asyncapi3.C } var s *jsonSchema.Schema - switch t := msg.Payload.Value.Schema.(type) { + switch t := msg.Payload.Value.(type) { case *openApiSchema.Schema: s = openApiSchema.ConvertToJsonSchema(t) case *avro.Schema: @@ -79,7 +79,7 @@ func getAsyncApiExample(w http.ResponseWriter, r *http.Request, cfg *asyncapi3.C ct := media.ParseContentType(msg.ContentType) var data []byte - switch t := msg.Payload.Value.Schema.(type) { + switch t := msg.Payload.Value.(type) { case *openApiSchema.Schema: data, err = t.Marshal(rnd, ct) case *avro.Schema: diff --git a/api/handler_search_test.go b/api/handler_search_test.go index cc7905489..c98370e12 100644 --- a/api/handler_search_test.go +++ b/api/handler_search_test.go @@ -47,7 +47,7 @@ func TestHandler_SearchQuery(t *testing.T) { app := runtime.New(&static.Config{Api: static.Api{Search: static.Search{ Enabled: true, InMemory: true, - }}}) + }}}, &dynamictest.Reader{}) cfg := openapitest.NewConfig("3.0", openapitest.WithInfo("foo", "", "")) app.AddHttp(toConfig(cfg)) @@ -67,7 +67,7 @@ func TestHandler_SearchQuery(t *testing.T) { app := runtime.New(&static.Config{Api: static.Api{Search: static.Search{ Enabled: true, InMemory: true, - }}}) + }}}, &dynamictest.Reader{}) cfg := openapitest.NewConfig("3.0", openapitest.WithInfo("foo", "", "")) app.AddHttp(toConfig(cfg)) @@ -87,7 +87,7 @@ func TestHandler_SearchQuery(t *testing.T) { app := runtime.New(&static.Config{Api: static.Api{Search: static.Search{ Enabled: true, InMemory: true, - }}}) + }}}, &dynamictest.Reader{}) cfg := openapitest.NewConfig("3.0", openapitest.WithInfo("foo", "", "")) app.AddHttp(toConfig(cfg)) @@ -109,7 +109,7 @@ func TestHandler_SearchQuery(t *testing.T) { app := runtime.New(&static.Config{Api: static.Api{Search: static.Search{ Enabled: true, InMemory: true, - }}}) + }}}, &dynamictest.Reader{}) cfg := openapitest.NewConfig("3.0", openapitest.WithInfo("foo", "", "")) app.AddHttp(toConfig(cfg)) @@ -139,7 +139,7 @@ func TestHandler_SearchQuery(t *testing.T) { app := runtime.New(&static.Config{Api: static.Api{Search: static.Search{ Enabled: true, InMemory: true, - }}}) + }}}, &dynamictest.Reader{}) h := openapitest.NewConfig("3.0", openapitest.WithInfo("foo", "", "")) app.AddHttp(toConfig(h)) @@ -169,7 +169,7 @@ func TestHandler_SearchQuery(t *testing.T) { app := runtime.New(&static.Config{Api: static.Api{Search: static.Search{ Enabled: true, InMemory: true, - }}}) + }}}, &dynamictest.Reader{}) h := openapitest.NewConfig("3.0", openapitest.WithInfo("foo", "", "")) app.AddHttp(toConfig(h)) diff --git a/api/handler_system_test.go b/api/handler_system_test.go index fc35fbd9a..204c5e425 100644 --- a/api/handler_system_test.go +++ b/api/handler_system_test.go @@ -1,6 +1,7 @@ package api import ( + "mokapi/config/dynamic/dynamictest" "mokapi/config/static" "mokapi/runtime" "mokapi/runtime/events" @@ -84,7 +85,7 @@ func TestHandler_System(t *testing.T) { tc := tc t.Run(tc.name, func(t *testing.T) { cfg := &static.Config{} - app := runtime.New(cfg) + app := runtime.New(cfg, &dynamictest.Reader{}) h := New(app, static.Api{}) tc.fn(t, h, app.Events) diff --git a/api/handler_test.go b/api/handler_test.go index 52040c65d..2d80ec935 100644 --- a/api/handler_test.go +++ b/api/handler_test.go @@ -2,6 +2,7 @@ package api_test import ( "mokapi/api" + "mokapi/config/dynamic/dynamictest" "mokapi/config/static" "mokapi/health" "mokapi/providers/openapi" @@ -83,7 +84,7 @@ func TestHandler_ServeHTTP(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() - h := api.New(runtime.New(&static.Config{}), static.Api{Dashboard: true}) + h := api.New(runtime.New(&static.Config{}, &dynamictest.Reader{}), static.Api{Dashboard: true}) tc.test(t, h) }) } @@ -184,7 +185,7 @@ func TestHandler_Api_Info(t *testing.T) { } func TestHandler_NoDashboard(t *testing.T) { - h := api.New(runtime.New(&static.Config{}), static.Api{Dashboard: false}) + h := api.New(runtime.New(&static.Config{}, &dynamictest.Reader{}), static.Api{Dashboard: false}) try.Handler(t, http.MethodGet, "http://foo.api", @@ -197,7 +198,7 @@ func TestHandler_NoDashboard(t *testing.T) { } func TestHandler_SearchEnabled(t *testing.T) { - h := api.New(runtime.New(&static.Config{}), static.Api{Dashboard: true, Search: static.Search{Enabled: true, InMemory: true}}) + h := api.New(runtime.New(&static.Config{}, &dynamictest.Reader{}), static.Api{Dashboard: true, Search: static.Search{Enabled: true, InMemory: true}}) try.Handler(t, http.MethodGet, "http://foo.api/api/info", @@ -255,7 +256,7 @@ func TestHandler_Health(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() - h := api.New(runtime.New(tc.cfg), tc.cfg.Api) + h := api.New(runtime.New(tc.cfg, &dynamictest.Reader{}), tc.cfg.Api) h.RegisterHealthHandler("/health", health.New(static.Health{})) tc.test(t, h) }) diff --git a/config/dynamic/asyncApi/asyncapitest/config.go b/config/dynamic/asyncApi/asyncapitest/config.go index 5f00e73fa..cc1aeac7e 100644 --- a/config/dynamic/asyncApi/asyncapitest/config.go +++ b/config/dynamic/asyncApi/asyncapitest/config.go @@ -66,7 +66,7 @@ func WithSchemas(name string, s *schema.Schema) ConfigOptions { c.Components.Schemas = map[string]*asyncapi3.SchemaRef{} } c.Components.Schemas[name] = &asyncapi3.SchemaRef{ - Value: &asyncapi3.MultiSchemaFormat{Schema: s}, + Value: s, } } } @@ -77,9 +77,9 @@ func WithMessages(name string, message *asyncApi.Message) ConfigOptions { c.Components = &asyncApi.Components{} } if c.Components.Messages == nil { - c.Components.Messages = map[string]*asyncApi.Message{} + c.Components.Messages = map[string]*asyncApi.MessageRef{} } - c.Components.Messages[name] = message + c.Components.Messages[name] = &asyncApi.MessageRef{Value: message} } } diff --git a/config/dynamic/asyncApi/asyncapitest/message.go b/config/dynamic/asyncApi/asyncapitest/message.go index 25b9d0aee..2596bc6ad 100644 --- a/config/dynamic/asyncApi/asyncapitest/message.go +++ b/config/dynamic/asyncApi/asyncapitest/message.go @@ -18,7 +18,7 @@ func NewMessage(opts ...MessageOptions) *asyncApi.Message { func WithPayload(s *schema.Schema) MessageOptions { return func(m *asyncApi.Message) { - m.Payload = &asyncapi3.SchemaRef{Value: &asyncapi3.MultiSchemaFormat{Schema: s}} + m.Payload = &asyncapi3.SchemaRef{Value: s} } } diff --git a/config/dynamic/asyncApi/config.go b/config/dynamic/asyncApi/config.go index 5b2e777e2..1f1e0607a 100644 --- a/config/dynamic/asyncApi/config.go +++ b/config/dynamic/asyncApi/config.go @@ -2,13 +2,14 @@ package asyncApi import ( "fmt" - log "github.com/sirupsen/logrus" "mokapi/config/dynamic" "mokapi/providers/asyncapi3" "mokapi/schema/json/schema" "mokapi/version" "net/url" "strconv" + + log "github.com/sirupsen/logrus" ) var supportedVersions = []version.Version{ @@ -131,12 +132,12 @@ type MessageBinding struct { } type Components struct { - Servers map[string]*Server `yaml:"servers" json:"servers"` - Channels map[string]*Channel `yaml:"channels" json:"channels"` + Servers map[string]*ServerRef `yaml:"servers" json:"servers"` + Channels map[string]*ChannelRef `yaml:"channels" json:"channels"` Schemas map[string]*asyncapi3.SchemaRef `yaml:"schemas" json:"schemas"` - Messages map[string]*Message `yaml:"messages" json:"messages"` + Messages map[string]*MessageRef `yaml:"messages" json:"messages"` Parameters map[string]*ParameterRef `yaml:"parameters" json:"parameters"` - MessageTraits map[string]MessageTraitRef `yaml:"messageTraits" json:"messageTraits"` + MessageTraits map[string]*MessageTraitRef `yaml:"messageTraits" json:"messageTraits"` } type ParameterRef struct { diff --git a/config/dynamic/asyncApi/convert.go b/config/dynamic/asyncApi/convert.go index 3b6b8d667..497d1eedc 100644 --- a/config/dynamic/asyncApi/convert.go +++ b/config/dynamic/asyncApi/convert.go @@ -70,7 +70,7 @@ func convertChannels(cfg *asyncapi3.Config, channels map[string]*ChannelRef) err cfg.Channels[name] = &asyncapi3.ChannelRef{Reference: dynamic.Reference{Ref: orig.Ref}} } if orig.Value != nil { - ch, err := convertChannel(name, orig.Value, cfg) + ch, err := convertChannel(name, orig, cfg) if err != nil { return err } @@ -82,55 +82,61 @@ func convertChannels(cfg *asyncapi3.Config, channels map[string]*ChannelRef) err return nil } -func convertChannel(name string, c *Channel, config *asyncapi3.Config) (*asyncapi3.ChannelRef, error) { - target := &asyncapi3.Channel{ - Address: name, - Summary: "", - Description: c.Description, - Messages: map[string]*asyncapi3.MessageRef{}, - Bindings: convertChannelBinding(c.Bindings), - } - ref := &asyncapi3.ChannelRef{Value: target} +func convertChannel(name string, c *ChannelRef, config *asyncapi3.Config) (*asyncapi3.ChannelRef, error) { + result := &asyncapi3.ChannelRef{Reference: dynamic.Reference{Ref: c.Ref}} - for _, server := range c.Servers { - target.Servers = append( - target.Servers, - &asyncapi3.ServerRef{Reference: dynamic.Reference{ - Ref: fmt.Sprintf("#/servers/%s", server), - }}) - } + if c.Value != nil { + target := &asyncapi3.Channel{ + Address: name, + Summary: "", + Description: c.Value.Description, + Messages: map[string]*asyncapi3.MessageRef{}, + Bindings: convertChannelBinding(c.Value.Bindings), + } + ref := &asyncapi3.ChannelRef{Value: target} - if err := convertParameters(target, c.Parameters); err != nil { - return nil, err - } + for _, server := range c.Value.Servers { + target.Servers = append( + target.Servers, + &asyncapi3.ServerRef{Reference: dynamic.Reference{ + Ref: fmt.Sprintf("#/servers/%s", server), + }}) + } - if c.Publish != nil && c.Subscribe != nil && c.Publish.Message != nil && c.Subscribe.Message != nil { - if c.Publish.Message.Ref == c.Subscribe.Message.Ref { - msg := convertMessage(c.Publish.Message) - msgName := addMessage(target, msg, "", "", c.Publish.Message.Ref) - if msgName != "" { - addOperation(msgName, "send", c.Publish, ref, msg, config) - addOperation(msgName, "receive", c.Subscribe, ref, msg, config) - } + if err := convertParameters(target, c.Value.Parameters); err != nil { + return nil, err } - } else { - if c.Publish != nil { - msg := convertMessage(c.Publish.Message) - msgName := addMessage(target, msg, c.Publish.OperationId, c.Publish.Message.Ref, "publish") - if msgName != "" { - addOperation(msgName, "send", c.Publish, ref, msg, config) + + if c.Value.Publish != nil && c.Value.Subscribe != nil && c.Value.Publish.Message != nil && c.Value.Subscribe.Message != nil { + if c.Value.Publish.Message.Ref == c.Value.Subscribe.Message.Ref { + msg := convertMessage(c.Value.Publish.Message) + msgName := addMessage(target, msg, "", "", c.Value.Publish.Message.Ref) + if msgName != "" { + addOperation(msgName, "send", c.Value.Publish, ref, msg, config) + addOperation(msgName, "receive", c.Value.Subscribe, ref, msg, config) + } } - } - if c.Subscribe != nil { - msg := convertMessage(c.Subscribe.Message) - msgName := addMessage(target, msg, c.Subscribe.OperationId, c.Subscribe.Message.Ref, "subscribe") - if msgName != "" { - addOperation(msgName, "receive", c.Subscribe, ref, msg, config) + } else { + if c.Value.Publish != nil { + msg := convertMessage(c.Value.Publish.Message) + msgName := addMessage(target, msg, c.Value.Publish.OperationId, c.Value.Publish.Message.Ref, "publish") + if msgName != "" { + addOperation(msgName, "send", c.Value.Publish, ref, msg, config) + } + } + if c.Value.Subscribe != nil { + msg := convertMessage(c.Value.Subscribe.Message) + msgName := addMessage(target, msg, c.Value.Subscribe.OperationId, c.Value.Subscribe.Message.Ref, "subscribe") + if msgName != "" { + addOperation(msgName, "receive", c.Value.Subscribe, ref, msg, config) + } } } + + result.Value = target } - return &asyncapi3.ChannelRef{Value: target}, nil + return result, nil } func addOperation(msgName, action string, op *Operation, ch *asyncapi3.ChannelRef, msg *asyncapi3.MessageRef, config *asyncapi3.Config) { @@ -200,16 +206,10 @@ func convertMessage(msg *MessageRef) *asyncapi3.MessageRef { } if msg.Value.Payload != nil && msg.Value.Payload.Value != nil { - target.Value.Payload = &asyncapi3.SchemaRef{Value: &asyncapi3.MultiSchemaFormat{ - Format: msg.Value.Payload.Value.Format, - Schema: msg.Value.Payload.Value.Schema, - }} + target.Value.Payload = msg.Value.Payload } if msg.Value.Headers != nil && msg.Value.Headers.Value != nil { - target.Value.Headers = &asyncapi3.SchemaRef{Value: &asyncapi3.MultiSchemaFormat{ - Format: msg.Value.Headers.Value.Format, - Schema: msg.Value.Headers.Value.Schema, - }} + target.Value.Headers = msg.Value.Headers } for _, orig := range msg.Value.Traits { trait := &asyncapi3.MessageTraitRef{} @@ -236,24 +236,11 @@ func convertMessageTrait(trait *MessageTrait) *asyncapi3.MessageTraitRef { } if trait.Headers != nil && trait.Headers.Value != nil { - target.Headers = &asyncapi3.SchemaRef{ - Value: &asyncapi3.MultiSchemaFormat{Schema: trait.Headers.Value.Schema, Format: trait.Headers.Value.Format}, - } + target.Headers = trait.Headers } return &asyncapi3.MessageTraitRef{Value: target} } -func convertSchema(s *asyncapi3.SchemaRef) *asyncapi3.SchemaRef { - if s == nil || s.Value == nil { - return nil - } - - target := &asyncapi3.SchemaRef{Reference: dynamic.Reference{Ref: s.Ref}} - target.Value = &asyncapi3.MultiSchemaFormat{Schema: s.Value.Schema, Format: s.Value.Format} - - return target -} - func convertParameters(channel *asyncapi3.Channel, params map[string]*ParameterRef) error { if len(params) == 0 { return nil @@ -325,27 +312,33 @@ func convertServers(cfg *asyncapi3.Config, servers map[string]*ServerRef) { cfg.Servers.Set(name, &asyncapi3.ServerRef{Reference: dynamic.Reference{Ref: orig.Ref}}) } if orig.Value != nil { - cfg.Servers.Set(name, convertServer(orig.Value)) + cfg.Servers.Set(name, convertServer(orig)) } } } -func convertServer(s *Server) *asyncapi3.ServerRef { - target := &asyncapi3.Server{ - Protocol: s.Protocol, - Description: s.Description, - ProtocolVersion: s.ProtocolVersion, - Bindings: convertServerBinding(s.Bindings), - } +func convertServer(ref *ServerRef) *asyncapi3.ServerRef { + result := &asyncapi3.ServerRef{Reference: dynamic.Reference{Ref: ref.Ref}} + + if ref.Value != nil { + target := &asyncapi3.Server{ + Protocol: ref.Value.Protocol, + Description: ref.Value.Description, + ProtocolVersion: ref.Value.ProtocolVersion, + Bindings: convertServerBinding(ref.Value.Bindings), + } + + protocol, host, p := resolveServerUrl(ref.Value.Url) + if target.Protocol == "" { + target.Protocol = protocol + } + target.Host = host + target.Pathname = p - protocol, host, p := resolveServerUrl(s.Url) - if target.Protocol == "" { - target.Protocol = protocol + result.Value = target } - target.Host = host - target.Pathname = p - return &asyncapi3.ServerRef{Value: target} + return result } func resolveServerUrl(s string) (protocol, host, path string) { @@ -380,7 +373,7 @@ func convertServerBinding(b ServerBindings) asyncapi3.ServerBindings { func convertMessageBinding(b MessageBinding) asyncapi3.MessageBinding { return asyncapi3.MessageBinding{ Kafka: asyncapi3.KafkaMessageBinding{ - Key: convertSchema(b.Kafka.Key), + Key: b.Kafka.Key, }, } } @@ -430,7 +423,7 @@ func convertComponents(c *Components, config *asyncapi3.Config) (*asyncapi3.Comp if target.Messages == nil { target.Messages = map[string]*asyncapi3.MessageRef{} } - target.Messages[name] = convertMessage(&MessageRef{Value: orig}) + target.Messages[name] = convertMessage(orig) } if c.Schemas != nil { @@ -438,7 +431,7 @@ func convertComponents(c *Components, config *asyncapi3.Config) (*asyncapi3.Comp if target.Schemas == nil { target.Schemas = map[string]*asyncapi3.SchemaRef{} } - target.Schemas[name] = convertSchema(orig) + target.Schemas[name] = orig } } diff --git a/config/dynamic/asyncApi/convert_test.go b/config/dynamic/asyncApi/convert_test.go index 7fce80b25..7a77de1ab 100644 --- a/config/dynamic/asyncApi/convert_test.go +++ b/config/dynamic/asyncApi/convert_test.go @@ -61,15 +61,17 @@ func TestConfig_Convert(t *testing.T) { // traits require.Len(t, msg.Traits, 1) - require.IsType(t, &schema.Schema{}, msg.Traits[0].Value.Headers.Value.Schema) - headers := msg.Traits[0].Value.Headers.Value.Schema.(*schema.Schema) + s, err := msg.Traits[0].Value.Headers.GetSchema() + require.NoError(t, err) + require.IsType(t, &schema.Schema{}, s) + headers := s.(*schema.Schema) require.Equal(t, float64(100), *headers.Properties.Get("my-app-header").Maximum) // payload - payload := msg.Payload.Value - require.IsType(t, &schema.Schema{}, payload.Schema) - s := payload.Schema.(*schema.Schema) - require.Equal(t, "object", s.Type[0]) + s, err = msg.Payload.GetSchema() + require.NoError(t, err) + require.IsType(t, &schema.Schema{}, s) + require.Equal(t, "object", s.(*schema.Schema).Type[0]) // operations require.Len(t, cfg3.Operations, 4) diff --git a/config/dynamic/asyncApi/parsing.go b/config/dynamic/asyncApi/parsing.go index 61fdb6974..4390cf703 100644 --- a/config/dynamic/asyncApi/parsing.go +++ b/config/dynamic/asyncApi/parsing.go @@ -1,8 +1,9 @@ package asyncApi import ( - log "github.com/sirupsen/logrus" "mokapi/config/dynamic" + + log "github.com/sirupsen/logrus" ) func (c *Config) Parse(config *dynamic.Config, reader dynamic.Reader) error { @@ -10,9 +11,11 @@ func (c *Config) Parse(config *dynamic.Config, reader dynamic.Reader) error { if server == nil || len(server.Ref) == 0 { continue } - if err := dynamic.Resolve(server.Ref, &server.Value, config, reader); err != nil { + var resolved *ServerRef + if err := dynamic.Resolve(server.Ref, &resolved, config, reader); err != nil { return err } + server.Value = resolved.Value } for _, ch := range c.Channels { @@ -29,7 +32,12 @@ 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 { - return dynamic.Resolve(r.Ref, &r.Value, config, reader) + var resolved *ChannelRef + if err := dynamic.Resolve(r.Ref, &resolved, config, reader); err != nil { + return err + } + r.Value = resolved.Value + return nil } if r.Value == nil { @@ -66,9 +74,12 @@ 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 { - if err := dynamic.Resolve(r.Ref, &r.Value, config, reader); err != nil { + var resolved *MessageRef + if err := dynamic.Resolve(r.Ref, &resolved, config, reader); err != nil { return err } + r.Value = resolved.Value + return nil } if r.Value == nil { @@ -104,9 +115,12 @@ 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 { - if err := dynamic.Resolve(r.Ref, &r.Value, config, reader); err != nil { + var resolved *MessageTraitRef + if err := dynamic.Resolve(r.Ref, &resolved, config, reader); err != nil { return err } + r.Value = resolved.Value + return nil } if r.Value == nil { @@ -151,7 +165,11 @@ func (m *Message) applyTrait(trait *MessageTrait) { func (r *ParameterRef) Parse(config *dynamic.Config, reader dynamic.Reader) error { if len(r.Ref) > 0 { - return dynamic.Resolve(r.Ref, &r.Value, config, reader) + var resolved *ParameterRef + if err := dynamic.Resolve(r.Ref, &resolved, config, reader); err != nil { + return err + } + r.Value = resolved.Value } return nil } diff --git a/config/dynamic/asyncApi/parsing_test.go b/config/dynamic/asyncApi/parsing_test.go index ec2556f18..d8d2251b1 100644 --- a/config/dynamic/asyncApi/parsing_test.go +++ b/config/dynamic/asyncApi/parsing_test.go @@ -18,7 +18,7 @@ type testReader struct { readFunc readFunc } -func (tr *testReader) Read(u *url.URL, v any) (*dynamic.Config, error) { +func (tr *testReader) Read(u *url.URL, _ any) (*dynamic.Config, error) { cfg := &dynamic.Config{Info: dynamic.ConfigInfo{Url: u}} if err := tr.readFunc(cfg); err != nil { return cfg, err @@ -57,7 +57,7 @@ func TestServerResolve(t *testing.T) { Servers: map[string]*asyncApi.ServerRef{ "foo": {Ref: "#/components/servers/foo"}, }, - Components: &asyncApi.Components{Servers: map[string]*asyncApi.Server{"foo": {Description: "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) { require.NoError(t, err) @@ -86,7 +86,7 @@ func TestServerResolve(t *testing.T) { name: "server with reference components", read: func(cfg *dynamic.Config) error { require.Equal(t, "/foo.yml", cfg.Info.Url.String()) - config := &asyncApi.Config{Components: &asyncApi.Components{Servers: map[string]*asyncApi.Server{"foo": {Description: "foo"}}}} + config := &asyncApi.Config{Components: &asyncApi.Components{Servers: map[string]*asyncApi.ServerRef{"foo": {Value: &asyncApi.Server{Description: "foo"}}}}} cfg.Data = config return nil }, @@ -247,7 +247,7 @@ func TestMessage(t *testing.T) { cfg: &asyncApi.Config{Channels: map[string]*asyncApi.ChannelRef{ "foo": {Value: &asyncApi.Channel{Subscribe: &asyncApi.Operation{Message: &asyncApi.MessageRef{Ref: "#/components/messages/foo"}}}}, }, Components: &asyncApi.Components{ - Messages: map[string]*asyncApi.Message{"foo": {Description: "foo"}}, + Messages: map[string]*asyncApi.MessageRef{"foo": {Value: &asyncApi.Message{Description: "foo"}}}, }}, test: func(t *testing.T, cfg *asyncApi.Config, err error) { require.NoError(t, err) @@ -262,7 +262,7 @@ func TestMessage(t *testing.T) { cfg: &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.Message{"foo": {Description: "foo"}}, + Messages: map[string]*asyncApi.MessageRef{"foo": {Value: &asyncApi.Message{Description: "foo"}}}, }}, test: func(t *testing.T, cfg *asyncApi.Config, err error) { require.NoError(t, err) @@ -274,7 +274,7 @@ func TestMessage(t *testing.T) { read: func(cfg *dynamic.Config) error { require.Equal(t, "/foo.yml", cfg.Info.Url.String()) config := &asyncApi.Config{Components: &asyncApi.Components{ - Messages: map[string]*asyncApi.Message{"foo": {Description: "foo"}}, + Messages: map[string]*asyncApi.MessageRef{"foo": {Value: &asyncApi.Message{Description: "foo"}}}, }} cfg.Data = config return nil @@ -292,7 +292,7 @@ func TestMessage(t *testing.T) { read: func(cfg *dynamic.Config) error { require.Equal(t, "/foo.yml", cfg.Info.Url.String()) config := &asyncApi.Config{Components: &asyncApi.Components{ - Messages: map[string]*asyncApi.Message{"foo": {Description: "foo"}}, + Messages: map[string]*asyncApi.MessageRef{"foo": {Value: &asyncApi.Message{Description: "foo"}}}, }} cfg.Data = config return nil @@ -361,13 +361,13 @@ func TestMessage(t *testing.T) { 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.Message{"foo": {}}, + 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"] = target + file.Data.(*asyncApi.Config).Components.Messages["foo"].Value = target require.NoError(t, err) require.Equal(t, target, config.Channels["foo"].Value.Publish.Message.Value) @@ -421,14 +421,16 @@ func TestSchema(t *testing.T) { t.Run("reference inside", func(t *testing.T) { target := &schema.Schema{} schemas := map[string]*asyncapi3.SchemaRef{} - schemas["foo"] = &asyncapi3.SchemaRef{Value: &asyncapi3.MultiSchemaFormat{Schema: target}} + 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"}} reader := &testReader{readFunc: func(cfg *dynamic.Config) error { return nil }} err := config.Parse(&dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: config}, reader) require.NoError(t, err) - require.Equal(t, target, message.Payload.Value.Schema.(*schema.Schema)) + s, err := message.Payload.GetSchema() + require.NoError(t, err) + require.Equal(t, target, s) }) t.Run("file reference direct", func(t *testing.T) { target := &schema.Schema{} @@ -440,11 +442,11 @@ func TestSchema(t *testing.T) { err := config.Parse(&dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: config}, reader) require.NoError(t, err) - require.Equal(t, target, message.Payload.Value.Schema) + require.Equal(t, target, message.Payload.Value) }) t.Run("modify file reference direct", func(t *testing.T) { target := &schema.Schema{} - message.Payload = &asyncapi3.SchemaRef{Value: &asyncapi3.MultiSchemaFormat{Schema: &schema.Schema{Ref: "foo.yml"}}} + message.Payload = &asyncapi3.SchemaRef{Value: &asyncapi3.MultiSchemaFormat{Schema: &asyncapi3.SchemaRef{Reference: dynamic.Reference{Ref: "foo.yml"}}}} reader := &testReader{readFunc: func(file *dynamic.Config) error { file.Data = target return nil @@ -458,6 +460,7 @@ func TestSchema(t *testing.T) { err = config.Parse(&dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: config}, reader) require.NoError(t, err) - require.Equal(t, "TARGET", message.Payload.Value.Schema.(*schema.Schema).Description) + s, err := message.Payload.GetSchema() + require.Equal(t, "TARGET", s.(*schema.Schema).Description) }) } diff --git a/config/dynamic/asyncApi/patch_test.go b/config/dynamic/asyncApi/patch_test.go index caecc786d..6aa7e3170 100644 --- a/config/dynamic/asyncApi/patch_test.go +++ b/config/dynamic/asyncApi/patch_test.go @@ -1,12 +1,13 @@ package asyncApi_test import ( - "github.com/stretchr/testify/require" "mokapi/config/dynamic/asyncApi" "mokapi/config/dynamic/asyncApi/asyncapitest" "mokapi/schema/json/schema" "mokapi/schema/json/schema/schematest" "testing" + + "github.com/stretchr/testify/require" ) func TestConfig_Patch_Info(t *testing.T) { @@ -400,7 +401,7 @@ func TestConfig_Patch_Message(t *testing.T) { }, test: func(t *testing.T, result *asyncApi.Config) { msg := result.Channels["foo"].Value.Publish.Message.Value - s := msg.Payload.Value.Schema.(*schema.Schema) + s := msg.Payload.Value.(*schema.Schema) require.Equal(t, "string", s.Type.String()) }, }, @@ -428,25 +429,25 @@ func TestConfig_Patch_Components(t *testing.T) { name: "patch add server", configs: []*asyncApi.Config{ asyncapitest.NewConfig(), - {Components: &asyncApi.Components{Servers: map[string]*asyncApi.Server{"foo": {Protocol: "kafka"}}}}, + {Components: &asyncApi.Components{Servers: map[string]*asyncApi.ServerRef{"foo": {Value: &asyncApi.Server{Protocol: "kafka"}}}}}, }, test: func(t *testing.T, result *asyncApi.Config) { require.Len(t, result.Components.Servers, 1) s := result.Components.Servers["foo"] - require.Equal(t, "kafka", s.Protocol) + require.Equal(t, "kafka", s.Value.Protocol) }, }, { name: "patch server", configs: []*asyncApi.Config{ - {Components: &asyncApi.Components{Servers: map[string]*asyncApi.Server{"foo": {Protocol: "kafka"}}}}, - {Components: &asyncApi.Components{Servers: map[string]*asyncApi.Server{"foo": {Protocol: "kafka", ProtocolVersion: "1.0"}}}}, + {Components: &asyncApi.Components{Servers: map[string]*asyncApi.ServerRef{"foo": {Value: &asyncApi.Server{Protocol: "kafka"}}}}}, + {Components: &asyncApi.Components{Servers: map[string]*asyncApi.ServerRef{"foo": {Value: &asyncApi.Server{Protocol: "kafka", ProtocolVersion: "1.0"}}}}}, }, test: func(t *testing.T, result *asyncApi.Config) { require.Len(t, result.Components.Servers, 1) s := result.Components.Servers["foo"] - require.Equal(t, "kafka", s.Protocol) - require.Equal(t, "1.0", s.ProtocolVersion) + require.Equal(t, "kafka", s.Value.Protocol) + require.Equal(t, "1.0", s.Value.ProtocolVersion) }, }, { @@ -457,7 +458,7 @@ func TestConfig_Patch_Components(t *testing.T) { }, test: func(t *testing.T, result *asyncApi.Config) { require.Len(t, result.Components.Schemas, 1) - s := result.Components.Schemas["foo"].Value.Schema.(*schema.Schema) + s := result.Components.Schemas["foo"].Value.(*schema.Schema) require.Equal(t, "number", s.Type.String()) }, }, @@ -469,9 +470,9 @@ func TestConfig_Patch_Components(t *testing.T) { }, test: func(t *testing.T, result *asyncApi.Config) { require.Len(t, result.Components.Schemas, 2) - s := result.Components.Schemas["foo"].Value.Schema.(*schema.Schema) + s := result.Components.Schemas["foo"].Value.(*schema.Schema) require.Equal(t, "number", s.Type.String()) - s = result.Components.Schemas["bar"].Value.Schema.(*schema.Schema) + s = result.Components.Schemas["bar"].Value.(*schema.Schema) require.Equal(t, "string", s.Type.String()) }, }, @@ -483,7 +484,7 @@ func TestConfig_Patch_Components(t *testing.T) { }, test: func(t *testing.T, result *asyncApi.Config) { require.Len(t, result.Components.Schemas, 1) - s := result.Components.Schemas["foo"].Value.Schema.(*schema.Schema) + s := result.Components.Schemas["foo"].Value.(*schema.Schema) require.Equal(t, "number", s.Type.String()) require.Equal(t, "double", s.Format) }, @@ -497,7 +498,7 @@ func TestConfig_Patch_Components(t *testing.T) { test: func(t *testing.T, result *asyncApi.Config) { require.Len(t, result.Components.Messages, 1) msg := result.Components.Messages["foo"] - require.Equal(t, "name", msg.Name) + require.Equal(t, "name", msg.Value.Name) }, }, { @@ -509,7 +510,7 @@ func TestConfig_Patch_Components(t *testing.T) { test: func(t *testing.T, result *asyncApi.Config) { require.Len(t, result.Components.Messages, 1) msg := result.Components.Messages["foo"] - require.Equal(t, "title", msg.Title) + require.Equal(t, "title", msg.Value.Title) }, }, } diff --git a/config/dynamic/asyncApi/unmarshal_yaml_test.go b/config/dynamic/asyncApi/unmarshal_yaml_test.go index d32f21f27..f39dd1953 100644 --- a/config/dynamic/asyncApi/unmarshal_yaml_test.go +++ b/config/dynamic/asyncApi/unmarshal_yaml_test.go @@ -1,9 +1,10 @@ package asyncApi import ( + "testing" + "github.com/stretchr/testify/require" "gopkg.in/yaml.v3" - "testing" ) func TestKafkaBinding(t *testing.T) { @@ -87,8 +88,8 @@ components: `, test: func(t *testing.T, c *Config, err error) { require.NoError(t, err) - require.Equal(t, "payload", c.Components.Messages["foo"].Bindings.Kafka.SchemaIdLocation) - require.Equal(t, "4", c.Components.Messages["foo"].Bindings.Kafka.SchemaIdPayloadEncoding) + require.Equal(t, "payload", c.Components.Messages["foo"].Value.Bindings.Kafka.SchemaIdLocation) + require.Equal(t, "4", c.Components.Messages["foo"].Value.Bindings.Kafka.SchemaIdPayloadEncoding) }, }, } diff --git a/config/dynamic/common.go b/config/dynamic/common.go index 69b773294..9dfb61531 100644 --- a/config/dynamic/common.go +++ b/config/dynamic/common.go @@ -33,3 +33,7 @@ func Register(header string, checkVersion func(v version.Version) bool, c interf checkVersion: checkVersion, configType: val.Type()}) } + +func Reset() { + configTypes = nil +} diff --git a/config/dynamic/parse.go b/config/dynamic/parse.go index 80b83d34b..6145d5818 100644 --- a/config/dynamic/parse.go +++ b/config/dynamic/parse.go @@ -72,22 +72,17 @@ func parse(c *Config) (interface{}, error) { case ".lua", ".js", ".cjs", ".mjs", ".ts": result = string(b) default: - if result != nil { - rv := reflect.ValueOf(result) - if rv.Kind() == reflect.Pointer || rv.Kind() == reflect.Struct || rv.Kind() == reflect.Slice || rv.Kind() == reflect.Array || rv.Kind() == reflect.Map { - // try parse from json and yaml - var v interface{} - v, err = parseJson(b, result) - if err == nil { - return v, nil - } - v, err = parseYaml(b, result) - if err == nil { - return v, nil - } - err = nil - } + // try parse from JSON and YAML + var v interface{} + v, err = parseJson(b, result) + if err == nil { + return v, nil + } + v, err = parseYaml(b, result) + if v != nil && err == nil { + return v, nil } + err = nil result = string(b) } @@ -135,6 +130,9 @@ func (d *dynamicObject) UnmarshalJSON(b []byte) error { rt = rt.Elem() } rv := reflect.ValueOf(d.data) + if rv.Kind() != reflect.Ptr { + return fmt.Errorf("dynamic data must have a pointer to a struct") + } if rv.Elem().CanSet() { rv.Elem().Set(reflect.New(rt).Elem()) } else { @@ -168,6 +166,9 @@ func (d *dynamicObject) UnmarshalYAML(unmarshal func(interface{}) error) error { rt = rt.Elem() } rv := reflect.ValueOf(d.data) + if rv.Kind() != reflect.Ptr { + return fmt.Errorf("dynamic data must have a pointer to a struct") + } if rv.Elem().CanSet() { rv.Elem().Set(reflect.New(rt).Elem()) } else { diff --git a/config/dynamic/parse_test.go b/config/dynamic/parse_test.go index 7638dc7e8..4d80224c0 100644 --- a/config/dynamic/parse_test.go +++ b/config/dynamic/parse_test.go @@ -95,6 +95,20 @@ func TestParse(t *testing.T) { require.Equal(t, "foo", c.Data.(*data).User) }, }, + { + name: "known json without extension", + test: func(t *testing.T) { + c := &dynamic.Config{ + Info: dynamic.ConfigInfo{Url: mustUrl("foo")}, + Raw: []byte(`{"user": "foo"}`), + } + dynamic.Register("user", nil, &data{}) + + err := dynamic.Parse(c, &dynamictest.Reader{}) + require.NoError(t, err) + require.Equal(t, "foo", c.Data.(*data).User) + }, + }, { name: "unknown yaml", test: func(t *testing.T) { @@ -135,6 +149,20 @@ func TestParse(t *testing.T) { require.Equal(t, "foo", c.Data.(*data).User) }, }, + { + name: "known yaml without extension", + test: func(t *testing.T) { + c := &dynamic.Config{ + Info: dynamic.ConfigInfo{Url: mustUrl("foo")}, + Raw: []byte(`user: foo`), + } + dynamic.Register("user", nil, &data{}) + + err := dynamic.Parse(c, &dynamictest.Reader{}) + require.NoError(t, err) + require.Equal(t, "foo", c.Data.(*data).User) + }, + }, { name: "lua", test: func(t *testing.T) { @@ -161,6 +189,19 @@ func TestParse(t *testing.T) { require.Equal(t, `console.log('Hello World')`, c.Data) }, }, + { + name: "javascript without extension", + test: func(t *testing.T) { + c := &dynamic.Config{ + Info: dynamic.ConfigInfo{Url: mustUrl("foo")}, + Raw: []byte(`console.log('Hello World')`), + } + + err := dynamic.Parse(c, &dynamictest.Reader{}) + require.NoError(t, err) + require.Equal(t, `console.log('Hello World')`, c.Data) + }, + }, { name: "update javascript", test: func(t *testing.T) { @@ -321,6 +362,7 @@ func TestParse(t *testing.T) { tc := tc t.Run(tc.name, func(t *testing.T) { tc.test(t) + dynamic.Reset() }) } } diff --git a/config/dynamic/resolve.go b/config/dynamic/resolve.go index b15211f32..4aab048a2 100644 --- a/config/dynamic/resolve.go +++ b/config/dynamic/resolve.go @@ -136,13 +136,6 @@ func get(token string, node interface{}) (interface{}, error) { case reflect.Map: mv := rValue.MapIndex(reflect.ValueOf(token)) if mv.IsValid() { - v := reflect.Indirect(mv) - // if map value is a "ref wrapper" like SchemaRef - if v.Kind() == reflect.Struct { - if f := v.FieldByName("Value"); f.IsValid() { - return f.Interface(), nil - } - } return mv.Interface(), nil } default: @@ -272,16 +265,11 @@ func resolveResource(ref string, element interface{}, config *Config, reader Rea 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 v.Kind() == reflect.Struct { - fRef := v.FieldByName("Ref") - fValue := v.FieldByName("Value") - if fRef.IsValid() && fValue.IsValid() { - val = fValue.Interface() - } - } if val == nil { return fmt.Errorf("value is null") @@ -299,10 +287,9 @@ func setResolved(element interface{}, val interface{}) (err error) { return } - v2 := reflect.Indirect(reflect.ValueOf(element)) - if !vCursor.Type().AssignableTo(v2.Type()) && vCursor.Kind() == reflect.Ptr { + if !vCursor.Type().AssignableTo(vElement.Type()) && vCursor.Kind() == reflect.Ptr { if c, ok := val.(Converter); ok { - if converted, err := c.ConvertTo(v2.Interface()); err == nil { + if converted, err := c.ConvertTo(vElement.Interface()); err == nil { vCursor = reflect.ValueOf(converted) } else { vCursor = vCursor.Elem() @@ -312,11 +299,11 @@ func setResolved(element interface{}, val interface{}) (err error) { } } - if !vCursor.Type().AssignableTo(v2.Type()) { - return fmt.Errorf("expected type %v, got %v", v2.Type(), vCursor.Type()) + if !vCursor.Type().AssignableTo(vElement.Type()) { + return fmt.Errorf("expected type %v, got %v", vElement.Type(), vCursor.Type()) } - v2.Set(vCursor) + vElement.Set(vCursor) return } diff --git a/config/dynamic/resolve_test.go b/config/dynamic/resolve_test.go index 13ab13da4..0381ea22e 100644 --- a/config/dynamic/resolve_test.go +++ b/config/dynamic/resolve_test.go @@ -2,11 +2,12 @@ package dynamic_test import ( "fmt" - "github.com/stretchr/testify/require" "mokapi/config/dynamic" "mokapi/config/dynamic/dynamictest" "net/url" "testing" + + "github.com/stretchr/testify/require" ) func TestResolve(t *testing.T) { @@ -112,12 +113,12 @@ func TestResolve(t *testing.T) { Value string } s := map[string]ref{"foo": {Value: "foo"}} - result := "" - err := dynamic.Resolve("#/foo", &result, &dynamic.Config{Data: s}, &dynamictest.Reader{}) + var resolved ref + err := dynamic.Resolve("#/foo", &resolved, &dynamic.Config{Data: s}, &dynamictest.Reader{}) require.NoError(t, err) - require.Equal(t, "foo", result) + require.Equal(t, "foo", resolved.Value) }, }, { @@ -177,12 +178,12 @@ func TestResolve(t *testing.T) { s := struct { Foo ref }{Foo: ref{Value: "foo"}} - result := "" - err := dynamic.Resolve("#/foo", &result, &dynamic.Config{Data: s}, &dynamictest.Reader{}) + var resolved ref + err := dynamic.Resolve("#/foo", &resolved, &dynamic.Config{Data: s}, &dynamictest.Reader{}) require.NoError(t, err) - require.Equal(t, "foo", result) + require.Equal(t, "foo", resolved.Value) }, }, { @@ -195,11 +196,11 @@ func TestResolve(t *testing.T) { s := struct { Foo ref }{Foo: ref{Value: nil}} - result := "" - err := dynamic.Resolve("#/foo", &result, &dynamic.Config{Data: s}, &dynamictest.Reader{}) - require.Error(t, err) - require.EqualError(t, err, "resolve reference '#/foo' failed: value is null") + var resolved ref + err := dynamic.Resolve("#/foo", &resolved, &dynamic.Config{Data: s}, &dynamictest.Reader{}) + require.NoError(t, err) + require.Nil(t, resolved.Value) }, }, { @@ -213,12 +214,12 @@ func TestResolve(t *testing.T) { s := struct { Foo ref }{Foo: ref{Value: &v}} - result := "" - err := dynamic.Resolve("#/foo", &result, &dynamic.Config{Data: s}, &dynamictest.Reader{}) + var resolved ref + err := dynamic.Resolve("#/foo", &resolved, &dynamic.Config{Data: s}, &dynamictest.Reader{}) require.NoError(t, err) - require.Equal(t, "foo", result) + require.Equal(t, "foo", *resolved.Value.(*string)) }, }, { diff --git a/docs/get-started/test-data.md b/docs/get-started/test-data.md index cb2c8ce23..b1506d444 100644 --- a/docs/get-started/test-data.md +++ b/docs/get-started/test-data.md @@ -1,22 +1,52 @@ --- -title: Mocking APIs with Realistic Test Data +title: Generate Realistic Test Data description: Mokapi generates random realistic test data or lets you customize responses with JavaScript to match your specific use case and scenarios. +subtitle: Mokapi automatically generates realistic test data from your API specifications. Customize responses with JavaScript or use declarative formats to match your exact testing needs. +cards: + items: + - title: Dashboard Guide + href: docs/get-started/dashboard + description: Learn how to validate and visualize your mock data + - title: Write Scripts + href: /docs/javascript-api/overview + description: Add dynamic behavior to your mocks with JavaScript + - title: Configure Mokapi + href: /docs/configuration/overview + description: Customize ports, providers, and other settings + - title: Explore Tutorials + href: /resources + description: Follow step-by-step guides for REST, Kafka, LDAP, and SMTP --- -# Mocking APIs with Realistic Test Data +# Generate Realistic Test Data Creating reliable test data is essential for accurate API testing and development. Mokapi provides a powerful data generation engine that creates realistic test data -based on API specifications. You can customize and control the generated data using -JavaScript scripts or declarative configurations to adapt it to real-world scenarios. +based on your API specifications. + +You have multiple options for controlling generated data: + +- **Automatic Generation** + Mokapi analyzes your schema and generates context-aware, realistic data automatically +- **Declarative Formats** + Use standard formats like `date`, `email`, `uuid` in your schema +- **JavaScript Customization** + Extend the generator with custom logic for specific fields or patterns +- **Full Response Control** + Write complete response handlers with conditional logic and dynamic behavior ## Automatic Test Data Generation -By default, Mokapi analyzes your API’s data structure and types to generate meaningful responses. -Additionally, Mokapi tailors responses based on request parameters, ensuring relevant data is returned. -For example, if a request filters for available pets, the response will include only pets with the status *available*. +By default, Mokapi analyzes your API's data structure and types to generate +meaningful responses. It also tailors responses based on request parameters, +ensuring relevant data is returned. + +### Context-Aware Generation + +For example, if a request filters for available pets, the response includes +only pets with `status: "available": -```bash tab=/pet/4 +```bash tab=Single Pet GET http://localhost/api/v3/pet/4 HTTP/1.1 200 OK Content-Type: application/json @@ -39,7 +69,7 @@ Content-Type: application/json "status": "pending" } ``` -```bash tab=/pet/findByStatus +```bash tab=Filtered List GET http://localhost/api/v3/pet/findByStatus?status=available HTTP/1.1 200 OK Content-Type: application/json @@ -54,17 +84,57 @@ Content-Type: application/json ] ``` -## Extending Data Generator with JavaScript +Notice how the status field in the second example matches the query parameter. +Mokapi understands the request context and generates appropriate data. + +## Declarative Formats in Schema + +Use standard OpenAPI formats to generate specific data types automatically. +Mokapi recognizes these formats and produces realistic values: + +| Format | Type | Example Output | +|---------------|----------|-----------------------------------------------------------| +| date | string | 2017-07-21 | +| time | string | 17:32:28Z | +| date-time | string | 2017-07-21T17:32:28Z | +| email | string | shanellewehner@cruickshank.biz | +| uuid | string | dd5742d1-82ad-4d42-8960-cb21bd02f3e7 | +| uri | string | http://www.regionale-enable.info/virtual/portals/redefine | +| password | string | w*YoR94jFL@X | +| ipv4 | string | 68.244.174.93 | +| {zip} {city} | string | 82910 San Jose | -Mokapi’s test data engine is highly extensible using JavaScript, allowing you to customize responses -based on specific attributes. In the example below, a random value from a predefined list is assigned to the frequency attribute. +### Example Schema -``` box=info -It’s recommended to define possible values using an enum in your API specification. This allows Mokapi to randomly -select a value from the list and clearly document all available options for your API users. This example demonstrates -the flexibility Mokapi offers. +```yaml +schema: + type: object + properties: + date: + type: string + format: date # 2017-07-21 + time: + type: string + format: date-time # 2017-07-21T17:32:28Z + email: + type: string + format: email # demetrisdach@yost.org + guid: + type: string + format: uuid # dd5742d1-82ad-4d42-8960-cb21bd02f3e7 ``` +This declarative approach requires no JavaScript, just add the `format` field to your schema. + +## Customize with JavaScript + +For more control, extend Mokapi's data generator with JavaScript. This is useful +when you need values from a specific list, complex patterns, or custom logic. + +### Example: Custom Enum Values + +Generate random values from a predefined list: + ```javascript import { fake, findByName, ROOT_NAME } from 'mokapi/faker'; @@ -81,12 +151,25 @@ export default function() { }; ``` -## Scripting API Behavior +Now any field named `frequency` will randomly return one of these values. + +``` box=tip title="Best Practice: Use Enums in Spec" +It's recommended to define possible values using `enum` in your OpenAPI +specification. This allows Mokapi to randomly select from the list automatically +and clearly documents all available options for API users. The JavaScript +approach shown here demonstrates Mokapi's flexibility for cases where +enum isn't suitable. +``` + +## Full Response Control with Scripts + +For complete control over API behavior, use Mokapi Scripts to customize +responses, simulate errors, add delays, or implement complex logic. + +### Example: Dynamic Time API -Mokapi Script allows you to customize API responses and simulate various behaviors, such as returning specific HTTP -statuses or producing Kafka messages. +Create an API that returns the current timestamp: -Example: A simple time API returning the current timestamp: ```javascript tab=time.js import {on} from 'mokapi' @@ -123,34 +206,22 @@ paths: format: date-time ``` -## Declarative Test Data Definition +This script intercepts requests to the `time` operation and returns the current +timestamp instead of a static mock value. -If you prefer a declarative approach, Mokapi uses formats like dates, emails, or UUIDs directly in your schema: +## Validate in the Dashboard -```yaml -schema: - type: object - properties: - date: - type: string - format: date # 2017-07-21 - time: - type: string - format: date-time # 2017-07-21T17:32:28Z - email: - type: string - format: email # demetrisdach@yost.org - guid: - type: string - format: uuid # dd5742d1-82ad-4d42-8960-cb21bd02f3e7 -``` +The Mokapi dashboard lets you: + +- **Generate sample data:** See what Mokapi produces for your schemas +- **Validate against spec:** Ensure mock data matches your OpenAPI definition +- **Inspect requests:** View generated responses for different request parameters +- **Test edge cases:** Verify your custom generators work as expected + +Access the dashboard at `http://localhost:8080` when Mokapi is running. -## Test and Validate in the Dashboard -In the dashboard, you can easily generate sample data and validate it against your API’s specification. -This allows you to ensure your mock data matches the expected structure and behavior. The following chapter will -guide you through the process in more detail. +## What's Next? -## Next Steps +Explore related topics to get more out of Mokapi's test data generation: -- [Using Mokapi's Dashboard](dashboard.md) -- [Explore Mokapi Scripts](../javascript-api/overview.md) +{{ card-grid key="cards" }} diff --git a/engine/enginetest/engine.go b/engine/enginetest/engine.go index 8229572ef..bb015f179 100644 --- a/engine/enginetest/engine.go +++ b/engine/enginetest/engine.go @@ -2,6 +2,7 @@ package enginetest import ( "mokapi/config/dynamic" + "mokapi/config/dynamic/dynamictest" "mokapi/config/static" "mokapi/engine" "mokapi/engine/common" @@ -22,7 +23,7 @@ func NewEngine(opts ...engine.Options) *engine.Engine { return loader.Load(file, host) })), engine.WithLogger(&Logger{}), - engine.WithApp(runtime.New(&static.Config{})), + engine.WithApp(runtime.New(&static.Config{}, &dynamictest.Reader{})), }, opts...) return engine.NewEngine(opts...) } diff --git a/engine/kafka.go b/engine/kafka.go index 71961a027..0a4d9a59d 100644 --- a/engine/kafka.go +++ b/engine/kafka.go @@ -50,9 +50,7 @@ func (c *KafkaClient) Produce(args *common.KafkaProduceArgs) (*common.KafkaProdu } keySchema := &asyncapi3.SchemaRef{ - Value: &asyncapi3.MultiSchemaFormat{ - Schema: &schema.Schema{Type: schema.Types{"string"}, Pattern: "[a-z]{9}"}, - }, + Value: &schema.Schema{Type: schema.Types{"string"}, Pattern: "[a-z]{9}"}, } // if m.Value is not used then select Kafka message config by the data which must be valid @@ -203,11 +201,14 @@ func (c *KafkaClient) getPartition(t *store.Topic, partition int) (*store.Partit func createValue(r *asyncapi3.SchemaRef) (value interface{}, err error) { var s asyncapi3.Schema - if r != nil && r.Value != nil && r.Value.Schema != nil { - s = r.Value.Schema + if r != nil && r.Value != nil { + s, err = r.GetSchema() } else { s = &schema.Schema{} } + if err != nil { + return + } switch v := s.(type) { case *schema.Schema: @@ -218,7 +219,7 @@ func createValue(r *asyncapi3.SchemaRef) (value interface{}, err error) { jsSchema := avro.ConvertToJsonSchema(v) value, err = generator.New(&generator.Request{Schema: jsSchema}) default: - err = fmt.Errorf("schema format not supported: %v", r.Value.Format) + err = fmt.Errorf("schema format not supported: %T", r.Value) } return @@ -308,7 +309,7 @@ func valueMatchMessagePayload(value any, msg *asyncapi3.Message) error { } ct := media.ParseContentType(msg.ContentType) - switch v := msg.Payload.Value.Schema.(type) { + switch v := msg.Payload.Value.(type) { case *schema.Schema: _, err := encoding.NewEncoder(v).Write(value, ct) return err diff --git a/engine/kafka_test.go b/engine/kafka_test.go index 37fa7f2ac..05ba77f8e 100644 --- a/engine/kafka_test.go +++ b/engine/kafka_test.go @@ -778,7 +778,7 @@ func TestKafkaClient(t *testing.T) { t.Run(tc.name, func(t *testing.T) { generator.Seed(11) - app := runtime.New(&static.Config{}) + app := runtime.New(&static.Config{}, &dynamictest.Reader{}) e := enginetest.NewEngine( engine.WithKafkaClient(engine.NewKafkaClient(app)), engine.WithDefaultLogger(), diff --git a/go.mod b/go.mod index 3ebbbb4fa..de5f5711b 100644 --- a/go.mod +++ b/go.mod @@ -6,10 +6,10 @@ 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.2 - github.com/bradleyfalzon/ghinstallation/v2 v2.17.0 + 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.3 + github.com/evanw/esbuild v0.27.4 github.com/fsnotify/fsnotify v1.9.0 github.com/go-co-op/gocron v1.37.0 github.com/go-git/go-git/v5 v5.17.0 @@ -20,8 +20,8 @@ require ( 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.51.0 - golang.org/x/text v0.34.0 + golang.org/x/net v0.52.0 + golang.org/x/text v0.35.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 @@ -61,8 +61,8 @@ require ( github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/golang/snappy v0.0.4 // indirect - github.com/google/go-github/v75 v75.0.0 // indirect - github.com/google/go-querystring v1.1.0 // indirect + github.com/google/go-github/v84 v84.0.0 // indirect + github.com/google/go-querystring v1.2.0 // indirect github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // indirect github.com/huandu/xstrings v1.3.2 // indirect github.com/imdario/mergo v0.3.14 // indirect @@ -80,8 +80,8 @@ require ( github.com/xanzy/ssh-agent v0.3.3 // indirect go.etcd.io/bbolt v1.4.0 // indirect go.uber.org/atomic v1.9.0 // indirect - golang.org/x/crypto v0.48.0 // indirect - golang.org/x/sys v0.41.0 // indirect + golang.org/x/crypto v0.49.0 // indirect + golang.org/x/sys v0.42.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 36ec8e37e..73df5d192 100644 --- a/go.sum +++ b/go.sum @@ -58,8 +58,8 @@ github.com/blevesearch/zapx/v15 v15.4.2 h1:sWxpDE0QQOTjyxYbAVjt3+0ieu8NCE0fDRaFx github.com/blevesearch/zapx/v15 v15.4.2/go.mod h1:1pssev/59FsuWcgSnTa0OeEpOzmhtmr/0/11H0Z8+Nw= github.com/blevesearch/zapx/v16 v16.2.8 h1:SlnzF0YGtSlrsOE3oE7EgEX6BIepGpeqxs1IjMbHLQI= github.com/blevesearch/zapx/v16 v16.2.8/go.mod h1:murSoCJPCk25MqURrcJaBQ1RekuqSCSfMjXH4rHyA14= -github.com/bradleyfalzon/ghinstallation/v2 v2.17.0 h1:SmbUK/GxpAspRjSQbB6ARvH+ArzlNzTtHydNyXUQ6zg= -github.com/bradleyfalzon/ghinstallation/v2 v2.17.0/go.mod h1:vuD/xvJT9Y+ZVZRv4HQ42cMyPFIYqpc7AbB4Gvt/DlY= +github.com/bradleyfalzon/ghinstallation/v2 v2.18.0 h1:WPqnN6NS9XvYlOgZQAIseN7Z1uAiE+UxgDKlW7FvFuU= +github.com/bradleyfalzon/ghinstallation/v2 v2.18.0/go.mod h1:gpoSwwWc4biE49F7n+roCcpkEkZ1Qr9soZ2ESvMiouU= github.com/brianvoe/gofakeit/v6 v6.28.0 h1:Xib46XXuQfmlLS2EXRuJpqcw8St6qSZz75OUo0tgAW4= github.com/brianvoe/gofakeit/v6 v6.28.0/go.mod h1:Xj58BMSnFqcn/fAQeSK+/PLtC5kSb7FJIq4JyGa8vEs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= @@ -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.3 h1:dH/to9tBKybig6hl25hg4SKIWP7U8COdJKbGEwnUkmU= -github.com/evanw/esbuild v0.27.3/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48= +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/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= @@ -105,13 +105,13 @@ github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8J github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/go-github/v75 v75.0.0 h1:k7q8Bvg+W5KxRl9Tjq16a9XEgVY1pwuiG5sIL7435Ic= -github.com/google/go-github/v75 v75.0.0/go.mod h1:H3LUJEA1TCrzuUqtdAQniBNwuKiQIqdGKgBo1/M/uqI= -github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= -github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/google/go-github/v84 v84.0.0 h1:I/0Xn5IuChMe8TdmI2bbim5nyhaRFJ7DEdzmD2w+yVA= +github.com/google/go-github/v84 v84.0.0/go.mod h1:WwYL1z1ajRdlaPszjVu/47x1L0PXukJBn73xsiYrRRQ= +github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0= +github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20230207041349-798e818bf904 h1:4/hN5RUoecvl+RmJRE2YxKWtnnQls6rQjjW5oV7qg2U= @@ -190,15 +190,15 @@ 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.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= -golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= +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/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.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= -golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= -golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= -golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +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/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= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -207,16 +207,15 @@ 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.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= -golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +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/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= -golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= +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/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= -golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +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/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 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/js/script.go b/js/script.go index ab02626ce..13080c8ad 100644 --- a/js/script.go +++ b/js/script.go @@ -148,6 +148,17 @@ func (s *Script) ensureRuntime() error { if s.runtime != nil { return nil } + + defer func() { + r := recover() + if r != nil { + // Closing the script may cause errors. runtime = nil => closing + if s.runtime != nil { + log.Errorf("js: recovered from panic %v: %v", s.file.Info.Path(), r) + } + } + }() + s.runtime = goja.New() s.loop = eventloop.New(s.runtime, s.host) diff --git a/pkg/cmd/mokapi/mokapi.go b/pkg/cmd/mokapi/mokapi.go index 65e24ae91..33a48ea62 100644 --- a/pkg/cmd/mokapi/mokapi.go +++ b/pkg/cmd/mokapi/mokapi.go @@ -129,9 +129,10 @@ func runRoot(cmd *cli.Command, cfg *static.Config) error { func createServer(cfg *static.Config) (*server.Server, error) { pool := safe.NewPool(context.Background()) - app := runtime.New(cfg) watcher := server.NewConfigWatcher(cfg) + app := runtime.New(cfg, watcher) + scriptEngine := engine.New(watcher, app, cfg, true) certStore, err := cert.NewStore(cfg) if err != nil { diff --git a/providers/asyncapi3/asyncapi3test/components.go b/providers/asyncapi3/asyncapi3test/components.go index 94d6d6fe7..f9c85a041 100644 --- a/providers/asyncapi3/asyncapi3test/components.go +++ b/providers/asyncapi3/asyncapi3test/components.go @@ -53,7 +53,7 @@ func WithComponentSchema(name string, s asyncapi3.Schema) ConfigOptions { c.Components.Schemas = map[string]*asyncapi3.SchemaRef{} } c.Components.Schemas[name] = &asyncapi3.SchemaRef{ - Value: &asyncapi3.MultiSchemaFormat{Schema: s}, + Value: s, } } } diff --git a/providers/asyncapi3/asyncapi3test/message.go b/providers/asyncapi3/asyncapi3test/message.go index 274ec0ccc..3a1b1cd81 100644 --- a/providers/asyncapi3/asyncapi3test/message.go +++ b/providers/asyncapi3/asyncapi3test/message.go @@ -18,14 +18,14 @@ func NewMessage(opts ...MessageOptions) *asyncapi3.Message { func WithPayload(s *schema.Schema) MessageOptions { return func(m *asyncapi3.Message) { - m.Payload = &asyncapi3.SchemaRef{Value: &asyncapi3.MultiSchemaFormat{Schema: s}} + m.Payload = &asyncapi3.SchemaRef{Value: s} } } func WithPayloadOpenAPI(s *openapi.Schema) MessageOptions { return func(m *asyncapi3.Message) { m.Payload = &asyncapi3.SchemaRef{Value: &asyncapi3.MultiSchemaFormat{ - Schema: s, + Schema: &asyncapi3.SchemaRef{Value: s}, Format: "application/vnd.oai.openapi+json;version=3.0.0", }} } @@ -35,7 +35,7 @@ func WithPayloadMulti(format string, schema asyncapi3.Schema) MessageOptions { return func(m *asyncapi3.Message) { m.Payload = &asyncapi3.SchemaRef{Value: &asyncapi3.MultiSchemaFormat{ Format: format, - Schema: schema, + Schema: &asyncapi3.SchemaRef{Value: schema}, }} } } @@ -48,7 +48,7 @@ func WithContentType(s string) MessageOptions { func WithKey(s *schema.Schema) MessageOptions { return func(m *asyncapi3.Message) { - m.Bindings.Kafka.Key = &asyncapi3.SchemaRef{Value: &asyncapi3.MultiSchemaFormat{Schema: s}} + m.Bindings.Kafka.Key = &asyncapi3.SchemaRef{Value: s} } } @@ -69,6 +69,6 @@ func WithKafkaMessageBinding(b asyncapi3.KafkaMessageBinding) MessageOptions { func WithHeaders(s *schema.Schema) MessageOptions { return func(m *asyncapi3.Message) { - m.Headers = &asyncapi3.SchemaRef{Value: &asyncapi3.MultiSchemaFormat{Schema: s}} + m.Headers = &asyncapi3.SchemaRef{Value: s} } } diff --git a/providers/asyncapi3/channel.go b/providers/asyncapi3/channel.go index 06a98a0a1..bebfcd89b 100644 --- a/providers/asyncapi3/channel.go +++ b/providers/asyncapi3/channel.go @@ -35,29 +35,40 @@ func (r *ChannelRef) UnmarshalJSON(b []byte) error { return r.Reference.UnmarshalJson(b, &r.Value) } -func (r *ChannelRef) parse(config *dynamic.Config, reader dynamic.Reader) error { +func (r *ChannelRef) Parse(config *dynamic.Config, reader dynamic.Reader) error { + if r == nil { + return nil + } if len(r.Ref) > 0 { - return dynamic.Resolve(r.Ref, &r.Value, config, reader) + var resolved *ChannelRef + if err := dynamic.Resolve(r.Ref, &resolved, config, reader); err != nil { + return err + } + r.Value = resolved.Value + return nil } + return r.Value.Parse(config, reader) +} - if r.Value == nil { +func (c *Channel) Parse(config *dynamic.Config, reader dynamic.Reader) error { + if c == nil { return nil } - for _, s := range r.Value.Servers { - if err := s.parse(config, reader); err != nil { + for _, s := range c.Servers { + if err := s.Parse(config, reader); err != nil { return err } } - for _, msg := range r.Value.Messages { - if err := msg.parse(config, reader); err != nil { + for _, msg := range c.Messages { + if err := msg.Parse(config, reader); err != nil { return err } } - for _, p := range r.Value.Parameters { - if err := p.parse(config, reader); err != nil { + for _, p := range c.Parameters { + if err := p.Parse(config, reader); err != nil { return err } } diff --git a/providers/asyncapi3/components.go b/providers/asyncapi3/components.go index 8dce2ad1f..38d48ff38 100644 --- a/providers/asyncapi3/components.go +++ b/providers/asyncapi3/components.go @@ -21,74 +21,11 @@ func (c *Components) parse(config *dynamic.Config, reader dynamic.Reader) error return nil } - for _, s := range c.Servers { - if err := s.parse(config, reader); err != nil { - return err - } - } - for _, t := range c.Tags { if err := t.parse(config, reader); err != nil { return err } } - for name, ch := range c.Channels { - if err := ch.parse(config, reader); err != nil { - return err - } - if ch.Value != nil { - ch.Value.Name = name - } - } - - for _, s := range c.Schemas { - if err := s.Parse(config, reader); err != nil { - return err - } - } - - for _, m := range c.Messages { - if err := m.parse(config, reader); err != nil { - return err - } - } - - for _, o := range c.Operations { - if err := o.parse(config, reader); err != nil { - return err - } - } - - for _, p := range c.Parameters { - if err := p.parse(config, reader); err != nil { - return err - } - } - - for _, cId := range c.CorrelationIds { - if err := cId.parse(config, reader); err != nil { - return err - } - } - - for _, d := range c.ExternalDocs { - if err := d.parse(config, reader); err != nil { - return err - } - } - - for _, trait := range c.OperationTraits { - if err := trait.parse(config, reader); err != nil { - return err - } - } - - for _, trait := range c.MessageTraits { - if err := trait.parse(config, reader); err != nil { - return err - } - } - return nil } diff --git a/providers/asyncapi3/components_test.go b/providers/asyncapi3/components_test.go index 174cea0b5..96523509f 100644 --- a/providers/asyncapi3/components_test.go +++ b/providers/asyncapi3/components_test.go @@ -1,8 +1,6 @@ package asyncapi3_test import ( - "github.com/stretchr/testify/require" - "gopkg.in/yaml.v3" "mokapi/config/dynamic" "mokapi/config/dynamic/dynamictest" "mokapi/providers/asyncapi3" @@ -12,6 +10,9 @@ import ( "mokapi/try" "net/url" "testing" + + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" ) func TestComponents_Ref(t *testing.T) { @@ -23,6 +24,9 @@ func TestComponents_Ref(t *testing.T) { { name: "servers", config: ` +servers: + foo: + $ref: '#/components/servers/foo' components: servers: foo: @@ -32,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 '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()) @@ -67,10 +71,9 @@ components: { name: "channels", config: ` -components: - channels: - foo: - $ref: 'test.yaml#/components/channels/foo' +channels: + foo: + $ref: 'test.yaml#/components/channels/foo' `, test: func(cfg *asyncapi3.Config) { c := &dynamic.Config{Data: cfg, Info: dynamic.ConfigInfo{Url: try.MustUrl("/foo")}} @@ -83,12 +86,18 @@ components: return &dynamic.Config{Data: asyncapi3test.NewConfig(asyncapi3test.WithComponentChannel("foo", &asyncapi3.Channel{Title: "FOO"}))}, nil })) require.NoError(t, err) - require.Equal(t, "FOO", cfg.Components.Channels["foo"].Value.Title) + require.Equal(t, "FOO", cfg.Channels["foo"].Value.Title) }, }, { name: "schemas", config: ` +channels: + foo: + messages: + msg: + payload: + $ref: '#/components/schemas/foo' components: schemas: foo: @@ -98,19 +107,24 @@ 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/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()) return &dynamic.Config{Data: asyncapi3test.NewConfig(asyncapi3test.WithComponentSchema("foo", schematest.New("string")))}, nil })) require.NoError(t, err) - require.Equal(t, "string", cfg.Components.Schemas["foo"].Value.Schema.(*schema.Schema).Type[0]) + require.Equal(t, "string", cfg.Components.Schemas["foo"].Value.(*schema.Schema).Type[0]) }, }, { name: "messages", config: ` +channels: + foo: + messages: + msg: + $ref: '#/components/messages/foo' components: messages: foo: @@ -120,7 +134,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/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()) @@ -133,6 +147,9 @@ components: { name: "operations", config: ` +operations: + foo: + $ref: '#/components/operations/foo' components: operations: foo: @@ -142,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 '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()) @@ -155,6 +172,11 @@ components: { name: "parameters", config: ` +channels: + foo: + parameters: + foo: + $ref: '#/components/parameters/foo' components: parameters: foo: @@ -164,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 '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()) @@ -177,6 +199,12 @@ components: { name: "correlationIds", config: ` +channels: + foo: + messages: + msg: + correlationId: + $ref: '#/components/correlationIds/foo' components: correlationIds: foo: @@ -186,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 '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()) @@ -199,6 +227,12 @@ components: { name: "externalDocs", config: ` +channels: + foo: + messages: + msg: + externalDocs: + - $ref: '#/components/externalDocs/foo' components: externalDocs: foo: @@ -208,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 '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()) @@ -221,6 +255,10 @@ components: { name: "operationTraits", config: ` +operations: + foo: + traits: + - $ref: '#/components/operationTraits/foo' components: operationTraits: foo: @@ -230,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 '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()) @@ -243,6 +281,12 @@ components: { name: "messageTraits", config: ` +channels: + foo: + messages: + msg: + traits: + - $ref: '#/components/messageTraits/foo' components: messageTraits: foo: @@ -252,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 '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.go b/providers/asyncapi3/config.go index 7ba0adb17..575aa9597 100644 --- a/providers/asyncapi3/config.go +++ b/providers/asyncapi3/config.go @@ -51,20 +51,14 @@ func (c *Config) Parse(config *dynamic.Config, reader dynamic.Reader) error { if c.Servers != nil { for it := c.Servers.Iter(); it.Next(); { server := it.Value() - if len(server.Ref) > 0 { - return dynamic.Resolve(server.Ref, &server.Value, config, reader) - } - if server.Value == nil { - return nil - } - if err := server.parse(config, reader); err != nil { + if err := server.Parse(config, reader); err != nil { return err } } } for name, ch := range c.Channels { - if err := ch.parse(config, reader); err != nil { + if err := ch.Parse(config, reader); err != nil { return err } if ch.Value != nil { @@ -74,7 +68,7 @@ func (c *Config) Parse(config *dynamic.Config, reader dynamic.Reader) error { } for _, op := range c.Operations { - if err := op.parse(config, reader); err != nil { + if err := op.Parse(config, reader); err != nil { return err } } diff --git a/providers/asyncapi3/config_test.go b/providers/asyncapi3/config_test.go index db6df0d94..d817e5905 100644 --- a/providers/asyncapi3/config_test.go +++ b/providers/asyncapi3/config_test.go @@ -37,17 +37,16 @@ components: err := yaml.Unmarshal(b, &cfg) require.NoError(t, err) - multi := cfg.Components.Schemas["Foo"].Value + multi := cfg.Components.Schemas["Foo"].Value.(*asyncapi3.MultiSchemaFormat) require.Equal(t, "application/vnd.apache.avro;version=1.9.0", multi.Format) - avroSchema := multi.Schema.(*asyncapi3.AvroRef) + avroSchema := multi.Schema.Value.(*asyncapi3.AvroRef) require.Equal(t, "record", avroSchema.Type[0]) - multi = cfg.Components.Schemas["FooRef"].Value + multi = cfg.Components.Schemas["FooRef"].Value.(*asyncapi3.MultiSchemaFormat) require.Equal(t, "application/vnd.apache.avro;version=1.9.0", multi.Format) - avroSchema = multi.Schema.(*asyncapi3.AvroRef) - require.Equal(t, "npm://foo.bar", avroSchema.Ref) + require.Equal(t, "npm://foo.bar", multi.Schema.Ref) - jsSchema := cfg.Components.Schemas["Bar"].Value.Schema.(*jsonSchema.Schema) + jsSchema := cfg.Components.Schemas["Bar"].Value.(*jsonSchema.Schema) require.Equal(t, "object", jsSchema.Type.String()) require.Equal(t, "application/json", cfg.DefaultContentType) @@ -103,16 +102,16 @@ func TestStreetlightKafka(t *testing.T) { require.True(t, strings.HasPrefix(message.Value.Summary, "Inform about environmental")) require.Equal(t, "application/json", message.Value.ContentType) // header from message trait should be applied - s := message.Value.Headers.Value.Schema.(*jsonSchema.Schema) + s := message.Value.Headers.Value.(*jsonSchema.Schema) require.Equal(t, "integer", s.Properties.Get("my-app-header").Type[0]) - payload := message.Value.Payload.Value.Schema.(*jsonSchema.Schema) + payload := message.Value.Payload.Value.(*jsonSchema.Schema) require.Equal(t, "Light intensity measured in lumens.", payload.Properties.Get("lumens").Description) // message trait require.Equal(t, "#/components/messageTraits/commonHeaders", message.Value.Traits[0].Ref) trait := message.Value.Traits[0].Value - s = trait.Headers.Value.Schema.(*jsonSchema.Schema) + s = trait.Headers.Value.(*jsonSchema.Schema) require.Equal(t, "integer", s.Properties.Get("my-app-header").Type[0]) param := channel.Value.Parameters["streetlightId"] @@ -146,63 +145,127 @@ func TestConfig_Payload_YAML(t *testing.T) { { name: "just a schema", cfg: `asyncapi: 3.0.0 +channels: + foo: + messages: + msg: + $ref: '#/components/messages/msg' components: + messages: + msg: + payload: + $ref: '#/components/schemas/foo' schemas: foo: type: string`, test: func(t *testing.T, cfg *asyncapi3.Config) { - s := cfg.Components.Schemas["foo"].Value.Schema.(*jsonSchema.Schema) + ch := cfg.Channels["foo"] + require.NotNil(t, ch) + msg := ch.Value.Messages["msg"] + require.NotNil(t, msg) + s := msg.Value.Payload.Value.(*jsonSchema.Schema) require.Equal(t, "string", s.Type.String()) }, }, { name: "MultiSchema: format and schema", cfg: `asyncapi: 3.0.0 +channels: + foo: + messages: + msg: + $ref: '#/components/messages/msg' components: + messages: + msg: + payload: + $ref: '#/components/schemas/foo' schemas: foo: schemaFormat: application/vnd.aai.asyncapi;version=3.0.0 schema: type: string`, test: func(t *testing.T, cfg *asyncapi3.Config) { - require.Equal(t, "application/vnd.aai.asyncapi;version=3.0.0", cfg.Components.Schemas["foo"].Value.Format) - s := cfg.Components.Schemas["foo"].Value.Schema.(*jsonSchema.Schema) + ch := cfg.Channels["foo"] + require.NotNil(t, ch) + msg := ch.Value.Messages["msg"] + require.NotNil(t, msg) + ms := msg.Value.Payload.Value.(*asyncapi3.MultiSchemaFormat) + require.NotNil(t, ms) + require.Equal(t, "application/vnd.aai.asyncapi;version=3.0.0", ms.Format) + s := ms.Schema.Value.(*jsonSchema.Schema) require.Equal(t, "string", s.Type.String()) }, }, { name: "MultiSchema: no format", cfg: `asyncapi: 3.0.0 +channels: + foo: + messages: + msg: + $ref: '#/components/messages/msg' components: + messages: + msg: + payload: + $ref: '#/components/schemas/foo' schemas: foo: schema: type: string`, test: func(t *testing.T, cfg *asyncapi3.Config) { - require.Equal(t, "", cfg.Components.Schemas["foo"].Value.Format) - s := cfg.Components.Schemas["foo"].Value.Schema.(*jsonSchema.Schema) + ch := cfg.Channels["foo"] + require.NotNil(t, ch) + msg := ch.Value.Messages["msg"] + require.NotNil(t, msg) + ms := msg.Value.Payload.Value.(*asyncapi3.MultiSchemaFormat) + require.NotNil(t, ms) + require.Equal(t, "", ms.Format) + s := ms.Schema.Value.(*jsonSchema.Schema) require.Equal(t, "string", s.Type.String()) }, }, { name: "ref", cfg: `asyncapi: 3.0.0 +channels: + foo: + messages: + msg: + $ref: '#/components/messages/msg' components: + messages: + msg: + payload: + $ref: '#/components/schemas/foo' schemas: foo: $ref: '#/components/schemas/bar' bar: type: string`, test: func(t *testing.T, cfg *asyncapi3.Config) { - require.Equal(t, "", cfg.Components.Schemas["foo"].Value.Format) - s := cfg.Components.Schemas["foo"].Value.Schema.(*jsonSchema.Schema) + ch := cfg.Channels["foo"] + require.NotNil(t, ch) + msg := ch.Value.Messages["msg"] + require.NotNil(t, msg) + s := msg.Value.Payload.Value.(*jsonSchema.Schema) require.Equal(t, "string", s.Type.String()) }, }, { name: "MultiSchema: ref", cfg: `asyncapi: 3.0.0 +channels: + foo: + messages: + msg: + $ref: '#/components/messages/msg' components: + messages: + msg: + payload: + $ref: '#/components/schemas/foo' schemas: foo: schemaFormat: application/vnd.aai.asyncapi;version=3.0.0 @@ -211,15 +274,30 @@ components: bar: type: string`, test: func(t *testing.T, cfg *asyncapi3.Config) { - require.Equal(t, "application/vnd.aai.asyncapi;version=3.0.0", cfg.Components.Schemas["foo"].Value.Format) - s := cfg.Components.Schemas["foo"].Value.Schema.(*jsonSchema.Schema) + ch := cfg.Channels["foo"] + require.NotNil(t, ch) + msg := ch.Value.Messages["msg"] + require.NotNil(t, msg) + ms := msg.Value.Payload.Value.(*asyncapi3.MultiSchemaFormat) + require.NotNil(t, ms) + require.Equal(t, "application/vnd.aai.asyncapi;version=3.0.0", ms.Format) + s := ms.Schema.Value.(*jsonSchema.Schema) require.Equal(t, "string", s.Type.String()) }, }, { name: "ref to swagger file", cfg: `asyncapi: 3.0.0 +channels: + foo: + messages: + msg: + $ref: '#/components/messages/msg' components: + messages: + msg: + payload: + $ref: '#/components/schemas/foo' schemas: foo: $ref: 'swagger.json#/definitions/foo'`, @@ -227,15 +305,27 @@ components: return &dynamic.Config{Data: &swagger.Config{Definitions: map[string]*schema.Schema{"foo": schematest.New("string")}}}, nil }), test: func(t *testing.T, cfg *asyncapi3.Config) { - require.Equal(t, "", cfg.Components.Schemas["foo"].Value.Format) - s := cfg.Components.Schemas["foo"].Value.Schema.(*jsonSchema.Schema) + ch := cfg.Channels["foo"] + require.NotNil(t, ch) + msg := ch.Value.Messages["msg"] + require.NotNil(t, msg) + s := msg.Value.Payload.Value.(*schema.Schema) require.Equal(t, "string", s.Type.String()) }, }, { name: "MultiSchema: ref to swagger file", cfg: `asyncapi: 3.0.0 +channels: + foo: + messages: + msg: + $ref: '#/components/messages/msg' components: + messages: + msg: + payload: + $ref: '#/components/schemas/foo' schemas: foo: schema: @@ -244,26 +334,83 @@ components: return &dynamic.Config{Data: &swagger.Config{Definitions: map[string]*schema.Schema{"foo": schematest.New("string")}}}, nil }), test: func(t *testing.T, cfg *asyncapi3.Config) { - require.Equal(t, "", cfg.Components.Schemas["foo"].Value.Format) - s := cfg.Components.Schemas["foo"].Value.Schema.(*jsonSchema.Schema) + ch := cfg.Channels["foo"] + require.NotNil(t, ch) + msg := ch.Value.Messages["msg"] + require.NotNil(t, msg) + msf := msg.Value.Payload.Value.(*asyncapi3.MultiSchemaFormat) + + require.Equal(t, "", msf.Format) + s := msf.Schema.Value.(*schema.Schema) require.Equal(t, "string", s.Type.String()) }, }, { name: "MultiSchema: OpenAPI", cfg: `asyncapi: 3.0.0 +channels: + foo: + messages: + msg: + $ref: '#/components/messages/msg' components: + messages: + msg: + payload: + $ref: '#/components/schemas/foo' schemas: foo: schemaFormat: application/vnd.oai.openapi;version=3.0.0 schema: type: string`, test: func(t *testing.T, cfg *asyncapi3.Config) { - require.Equal(t, "application/vnd.oai.openapi;version=3.0.0", cfg.Components.Schemas["foo"].Value.Format) - s := cfg.Components.Schemas["foo"].Value.Schema.(*schema.Schema) + ch := cfg.Channels["foo"] + require.NotNil(t, ch) + msg := ch.Value.Messages["msg"] + require.NotNil(t, msg) + msf := msg.Value.Payload.Value.(*asyncapi3.MultiSchemaFormat) + require.Equal(t, "application/vnd.oai.openapi;version=3.0.0", msf.Format) + s := msf.Schema.Value.(*schema.Schema) require.Equal(t, "string", s.Type.String()) }, }, + { + name: "multiple refs", + cfg: `asyncapi: 3.0.0 +channels: + foo: + messages: + msg: + payload: + $ref: '#/components/schemas/foo' +components: + schemas: + foo: + schemaFormat: application/vnd.oai.openapi;version=3.0.0 + schema: + $ref: '#/components/schemas/bar' + bar: + $ref: '#/components/schemas/zzz' + zzz: + $ref: '#/components/schemas/yuh' + yuh: + items: + $ref: '#/components/schemas/items' + items: + type: string +`, + test: func(t *testing.T, cfg *asyncapi3.Config) { + ch := cfg.Channels["foo"] + require.NotNil(t, ch) + msg := ch.Value.Messages["msg"] + require.NotNil(t, msg) + msf := msg.Value.Payload.Value.(*asyncapi3.MultiSchemaFormat) + require.Equal(t, "application/vnd.oai.openapi;version=3.0.0", msf.Format) + // referenced is schema defines no format => default JSON schema + s := msf.Schema.Value.(*jsonSchema.Schema) + require.Equal(t, "string", s.Items.Type.String()) + }, + }, } t.Parallel() @@ -294,7 +441,23 @@ func TestConfig_Payload_JSON(t *testing.T) { { name: "just a schema", cfg: `{"asyncapi": "3.0.0", +"channels": { + "foo": { + "messages": { + "msg": { + "$ref": "#/components/messages/msg" + } + } + } +}, "components": { + "messages": { + "msg": { + "payload": { + "$ref": "#/components/schemas/foo" + } + } + }, "schemas": { "foo": { "type": "string" @@ -302,14 +465,35 @@ func TestConfig_Payload_JSON(t *testing.T) { } }}`, test: func(t *testing.T, cfg *asyncapi3.Config) { - s := cfg.Components.Schemas["foo"].Value.Schema.(*jsonSchema.Schema) + ch := cfg.Channels["foo"] + require.NotNil(t, ch) + msg := ch.Value.Messages["msg"] + require.NotNil(t, msg) + s := msg.Value.Payload.Value.(*jsonSchema.Schema) + require.NotNil(t, s) require.Equal(t, "string", s.Type.String()) }, }, { name: "MultiSchema: format and schema", cfg: `{"asyncapi": "3.0.0", +"channels": { + "foo": { + "messages": { + "msg": { + "$ref": "#/components/messages/msg" + } + } + } +}, "components": { + "messages": { + "msg": { + "payload": { + "$ref": "#/components/schemas/foo" + } + } + }, "schemas": { "foo": { "schemaFormat": "application/vnd.aai.asyncapi;version=3.0.0", @@ -320,15 +504,37 @@ func TestConfig_Payload_JSON(t *testing.T) { } }}`, test: func(t *testing.T, cfg *asyncapi3.Config) { - require.Equal(t, "application/vnd.aai.asyncapi;version=3.0.0", cfg.Components.Schemas["foo"].Value.Format) - s := cfg.Components.Schemas["foo"].Value.Schema.(*jsonSchema.Schema) + ch := cfg.Channels["foo"] + require.NotNil(t, ch) + msg := ch.Value.Messages["msg"] + require.NotNil(t, msg) + msf := msg.Value.Payload.Value.(*asyncapi3.MultiSchemaFormat) + require.NotNil(t, msf) + require.Equal(t, "application/vnd.aai.asyncapi;version=3.0.0", msf.Format) + s := msf.Schema.Value.(*jsonSchema.Schema) require.Equal(t, "string", s.Type.String()) }, }, { name: "MultiSchema: no format", cfg: `{"asyncapi": "3.0.0", +"channels": { + "foo": { + "messages": { + "msg": { + "$ref": "#/components/messages/msg" + } + } + } +}, "components": { + "messages": { + "msg": { + "payload": { + "$ref": "#/components/schemas/foo" + } + } + }, "schemas": { "foo": { "schema": { @@ -338,15 +544,37 @@ func TestConfig_Payload_JSON(t *testing.T) { } }}`, test: func(t *testing.T, cfg *asyncapi3.Config) { - require.Equal(t, "", cfg.Components.Schemas["foo"].Value.Format) - s := cfg.Components.Schemas["foo"].Value.Schema.(*jsonSchema.Schema) + ch := cfg.Channels["foo"] + require.NotNil(t, ch) + msg := ch.Value.Messages["msg"] + require.NotNil(t, msg) + msf := msg.Value.Payload.Value.(*asyncapi3.MultiSchemaFormat) + require.NotNil(t, msf) + require.Equal(t, "", msf.Format) + s := msf.Schema.Value.(*jsonSchema.Schema) require.Equal(t, "string", s.Type.String()) }, }, { name: "ref", cfg: `{"asyncapi": "3.0.0", +"channels": { + "foo": { + "messages": { + "msg": { + "$ref": "#/components/messages/msg" + } + } + } +}, "components": { + "messages": { + "msg": { + "payload": { + "$ref": "#/components/schemas/foo" + } + } + }, "schemas": { "foo": { "$ref": "#/components/schemas/bar" @@ -357,15 +585,35 @@ func TestConfig_Payload_JSON(t *testing.T) { } }}`, test: func(t *testing.T, cfg *asyncapi3.Config) { - require.Equal(t, "", cfg.Components.Schemas["foo"].Value.Format) - s := cfg.Components.Schemas["foo"].Value.Schema.(*jsonSchema.Schema) + ch := cfg.Channels["foo"] + require.NotNil(t, ch) + msg := ch.Value.Messages["msg"] + require.NotNil(t, msg) + s := msg.Value.Payload.Value.(*jsonSchema.Schema) + require.NotNil(t, s) require.Equal(t, "string", s.Type.String()) }, }, { name: "MultiSchema: ref", cfg: `{"asyncapi": "3.0.0", +"channels": { + "foo": { + "messages": { + "msg": { + "$ref": "#/components/messages/msg" + } + } + } +}, "components": { + "messages": { + "msg": { + "payload": { + "$ref": "#/components/schemas/foo" + } + } + }, "schemas": { "foo": { "schemaFormat": "application/vnd.aai.asyncapi;version=3.0.0", @@ -379,15 +627,36 @@ func TestConfig_Payload_JSON(t *testing.T) { } }}`, test: func(t *testing.T, cfg *asyncapi3.Config) { - require.Equal(t, "application/vnd.aai.asyncapi;version=3.0.0", cfg.Components.Schemas["foo"].Value.Format) - s := cfg.Components.Schemas["foo"].Value.Schema.(*jsonSchema.Schema) + ch := cfg.Channels["foo"] + require.NotNil(t, ch) + msg := ch.Value.Messages["msg"] + require.NotNil(t, msg) + msf := msg.Value.Payload.Value.(*asyncapi3.MultiSchemaFormat) + require.Equal(t, "application/vnd.aai.asyncapi;version=3.0.0", msf.Format) + s := msf.Schema.Value.(*jsonSchema.Schema) require.Equal(t, "string", s.Type.String()) }, }, { name: "ref to swagger file", cfg: `{"asyncapi": "3.0.0", +"channels": { + "foo": { + "messages": { + "msg": { + "$ref": "#/components/messages/msg" + } + } + } +}, "components": { + "messages": { + "msg": { + "payload": { + "$ref": "#/components/schemas/foo" + } + } + }, "schemas": { "foo": { "$ref": "swagger.json#/definitions/foo" @@ -398,15 +667,34 @@ func TestConfig_Payload_JSON(t *testing.T) { return &dynamic.Config{Data: &swagger.Config{Definitions: map[string]*schema.Schema{"foo": schematest.New("string")}}}, nil }), test: func(t *testing.T, cfg *asyncapi3.Config) { - require.Equal(t, "", cfg.Components.Schemas["foo"].Value.Format) - s := cfg.Components.Schemas["foo"].Value.Schema.(*jsonSchema.Schema) + ch := cfg.Channels["foo"] + require.NotNil(t, ch) + msg := ch.Value.Messages["msg"] + require.NotNil(t, msg) + s := msg.Value.Payload.Value.(*schema.Schema) require.Equal(t, "string", s.Type.String()) }, }, { name: "MultiSchema: ref to swagger file", cfg: `{"asyncapi": "3.0.0", +"channels": { + "foo": { + "messages": { + "msg": { + "$ref": "#/components/messages/msg" + } + } + } +}, "components": { + "messages": { + "msg": { + "payload": { + "$ref": "#/components/schemas/foo" + } + } + }, "schemas": { "foo": { "schema": { @@ -419,15 +707,36 @@ func TestConfig_Payload_JSON(t *testing.T) { return &dynamic.Config{Data: &swagger.Config{Definitions: map[string]*schema.Schema{"foo": schematest.New("string")}}}, nil }), test: func(t *testing.T, cfg *asyncapi3.Config) { - require.Equal(t, "", cfg.Components.Schemas["foo"].Value.Format) - s := cfg.Components.Schemas["foo"].Value.Schema.(*jsonSchema.Schema) + ch := cfg.Channels["foo"] + require.NotNil(t, ch) + msg := ch.Value.Messages["msg"] + require.NotNil(t, msg) + msf := msg.Value.Payload.Value.(*asyncapi3.MultiSchemaFormat) + require.NotNil(t, msf) + s := msf.Schema.Value.(*schema.Schema) require.Equal(t, "string", s.Type.String()) }, }, { name: "MultiSchema: OpenAPI", cfg: `{"asyncapi": "3.0.0", +"channels": { + "foo": { + "messages": { + "msg": { + "$ref": "#/components/messages/msg" + } + } + } +}, "components": { + "messages": { + "msg": { + "payload": { + "$ref": "#/components/schemas/foo" + } + } + }, "schemas": { "foo": { "schemaFormat": "application/vnd.oai.openapi;version=3.0.0", @@ -438,8 +747,14 @@ func TestConfig_Payload_JSON(t *testing.T) { } }}`, test: func(t *testing.T, cfg *asyncapi3.Config) { - require.Equal(t, "application/vnd.oai.openapi;version=3.0.0", cfg.Components.Schemas["foo"].Value.Format) - s := cfg.Components.Schemas["foo"].Value.Schema.(*schema.Schema) + ch := cfg.Channels["foo"] + require.NotNil(t, ch) + msg := ch.Value.Messages["msg"] + require.NotNil(t, msg) + msf := msg.Value.Payload.Value.(*asyncapi3.MultiSchemaFormat) + require.NotNil(t, msf) + require.Equal(t, "application/vnd.oai.openapi;version=3.0.0", msf.Format) + s := msf.Schema.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 40ea6433d..e04110885 100644 --- a/providers/asyncapi3/correlation_id.go +++ b/providers/asyncapi3/correlation_id.go @@ -1,8 +1,9 @@ package asyncapi3 import ( - "gopkg.in/yaml.v3" "mokapi/config/dynamic" + + "gopkg.in/yaml.v3" ) type CorrelationIdRef struct { @@ -15,9 +16,13 @@ type CorrelationId struct { Location string `yaml:"location" json:"location"` } -func (r *CorrelationIdRef) parse(config *dynamic.Config, reader dynamic.Reader) error { +func (r *CorrelationIdRef) Parse(config *dynamic.Config, reader dynamic.Reader) error { if len(r.Ref) > 0 { - return dynamic.Resolve(r.Ref, &r.Value, config, reader) + var resolved *CorrelationIdRef + if err := dynamic.Resolve(r.Ref, &resolved, config, reader); err != nil { + return err + } + r.Value = resolved.Value } return nil } diff --git a/providers/asyncapi3/external_doc.go b/providers/asyncapi3/external_doc.go index b515f1959..d140cf27b 100644 --- a/providers/asyncapi3/external_doc.go +++ b/providers/asyncapi3/external_doc.go @@ -1,8 +1,9 @@ package asyncapi3 import ( - "gopkg.in/yaml.v3" "mokapi/config/dynamic" + + "gopkg.in/yaml.v3" ) type ExternalDocRef struct { @@ -23,9 +24,13 @@ func (r *ExternalDocRef) UnmarshalJSON(b []byte) error { return r.Reference.UnmarshalJson(b, &r.Value) } -func (r *ExternalDocRef) parse(config *dynamic.Config, reader dynamic.Reader) error { +func (r *ExternalDocRef) Parse(config *dynamic.Config, reader dynamic.Reader) error { if len(r.Ref) > 0 { - return dynamic.Resolve(r.Ref, &r.Value, config, reader) + var resolved *ExternalDocRef + if err := dynamic.Resolve(r.Ref, &resolved, config, reader); err != nil { + return err + } + r.Value = resolved.Value } return nil diff --git a/providers/asyncapi3/kafka/store/client.go b/providers/asyncapi3/kafka/store/client.go index 9032cbedc..b41131b98 100644 --- a/providers/asyncapi3/kafka/store/client.go +++ b/providers/asyncapi3/kafka/store/client.go @@ -257,7 +257,7 @@ func (c *Client) parse(v any, ct media.ContentType, topic *asyncapi3.Channel) ([ return nil, err } if msg != nil && msg.Payload != nil { - return msg.Payload.Value.Marshal(v, media.ParseContentType(msg.ContentType)) + return msg.Payload.Marshal(v, media.ParseContentType(msg.ContentType)) } b, _ := json.Marshal(v) return b, nil @@ -267,7 +267,7 @@ func (c *Client) parse(v any, ct media.ContentType, topic *asyncapi3.Channel) ([ return nil, err } if msg != nil && msg.Payload != nil { - return msg.Payload.Value.Marshal(v, media.ParseContentType(msg.ContentType)) + return msg.Payload.Marshal(v, media.ParseContentType(msg.ContentType)) } switch vt := v.(type) { @@ -391,8 +391,12 @@ func valueMatchMessagePayload(value any, msg *asyncapi3.Message) error { return nil } ct := media.ParseContentType(msg.ContentType) + s, err := msg.Payload.GetSchema() + if err != nil { + return err + } - switch v := msg.Payload.Value.Schema.(type) { + switch v := s.(type) { case *schema.Schema: _, err := encoding.NewEncoder(v).Write(value, ct) return err diff --git a/providers/asyncapi3/kafka/store/produce_test.go b/providers/asyncapi3/kafka/store/produce_test.go index 2641f3094..8cf6dbbab 100644 --- a/providers/asyncapi3/kafka/store/produce_test.go +++ b/providers/asyncapi3/kafka/store/produce_test.go @@ -218,6 +218,7 @@ func TestProduce(t *testing.T) { require.True(t, ok) require.Equal(t, "foo", res.Topics[0].Name) require.Equal(t, kafka.InvalidRecord, res.Topics[0].Partitions[0].ErrorCode) + require.Equal(t, "invalid message: error count 1:\n\t- #/type: invalid type, expected integer but got string", res.Topics[0].Partitions[0].ErrorMessage) require.Equal(t, int64(0), res.Topics[0].Partitions[0].BaseOffset) }, }, diff --git a/providers/asyncapi3/kafka/store/validation.go b/providers/asyncapi3/kafka/store/validation.go index 327b23010..59b67449f 100644 --- a/providers/asyncapi3/kafka/store/validation.go +++ b/providers/asyncapi3/kafka/store/validation.go @@ -68,22 +68,11 @@ func newMessageValidator(messageId string, msg *asyncapi3.Message, channel *asyn var msgParser encoding.Parser if msg.Payload != nil && channel.Bindings.Kafka.ValueSchemaValidation { - switch s := msg.Payload.Value.Schema.(type) { - case *schema.Schema: - msgParser = &parser.Parser{Schema: s, ConvertToSortedMap: true} - case *openapi.Schema: - mt := media.ParseContentType(msg.ContentType) - if mt.IsXml() { - msgParser = openapi.NewXmlParser(s) - } else { - msgParser = &parser.Parser{Schema: openapi.ConvertToJsonSchema(s), ConvertToSortedMap: true} - } - case *asyncapi3.AvroRef: - msgParser = &avro.Parser{Schema: s.Schema} - default: + var err error + msgParser, err = getParser(msg.Payload, msg.ContentType) + if err != nil { log.Errorf("unsupported payload type: %T", msg.Payload.Value) } - if msgParser != nil { v.payload = &schemaValidator{ parser: msgParser, @@ -94,7 +83,7 @@ func newMessageValidator(messageId string, msg *asyncapi3.Message, channel *asyn if msg.Bindings.Kafka.Key != nil && channel.Bindings.Kafka.KeySchemaValidation { var keyParser encoding.Parser - switch s := msg.Bindings.Kafka.Key.Value.Schema.(type) { + switch s := msg.Bindings.Kafka.Key.Value.(type) { case *schema.Schema: keyParser = &parser.Parser{Schema: s} case *asyncapi3.AvroRef: @@ -112,7 +101,7 @@ func newMessageValidator(messageId string, msg *asyncapi3.Message, channel *asyn if msg.Headers != nil { var headerParser encoding.Parser - switch s := msg.Headers.Value.Schema.(type) { + switch s := msg.Headers.Value.(type) { case *schema.Schema: headerParser = &parser.Parser{Schema: s} case *asyncapi3.AvroRef: @@ -252,7 +241,7 @@ func convertHeader(headers []kafka.RecordHeader) map[string]LogValue { } func parseHeader(headers []kafka.RecordHeader, sr *asyncapi3.SchemaRef) (map[string]LogValue, error) { - if sr == nil || sr.Value == nil || sr.Value.Schema == nil { + if sr == nil || sr.Value == nil { return convertHeader(headers), nil } m := map[string][]byte{} @@ -261,7 +250,7 @@ func parseHeader(headers []kafka.RecordHeader, sr *asyncapi3.SchemaRef) (map[str } result := map[string]LogValue{} - switch s := sr.Value.Schema.(type) { + switch s := sr.Value.(type) { case *schema.Schema: if s.Properties == nil { return result, fmt.Errorf("invalid header definition: expected object with properties") @@ -347,3 +336,22 @@ func parseHeader(headers []kafka.RecordHeader, sr *asyncapi3.SchemaRef) (map[str } return result, nil } + +func getParser(ref *asyncapi3.SchemaRef, contentType string) (encoding.Parser, error) { + switch s := ref.Value.(type) { + case *schema.Schema: + return &parser.Parser{Schema: s, ConvertToSortedMap: true}, nil + case *openapi.Schema: + mt := media.ParseContentType(contentType) + if mt.IsXml() { + return openapi.NewXmlParser(s), nil + } + return &parser.Parser{Schema: openapi.ConvertToJsonSchema(s), ConvertToSortedMap: true}, nil + case *asyncapi3.AvroRef: + return &avro.Parser{Schema: s.Schema}, nil + case *asyncapi3.MultiSchemaFormat: + return getParser(s.Schema, contentType) + default: + return nil, fmt.Errorf("unsupported payload type: %T", s) + } +} diff --git a/providers/asyncapi3/kafka/store/validation_test.go b/providers/asyncapi3/kafka/store/validation_test.go index 875f42263..0473bd7d1 100644 --- a/providers/asyncapi3/kafka/store/validation_test.go +++ b/providers/asyncapi3/kafka/store/validation_test.go @@ -284,6 +284,7 @@ func TestValidation(t *testing.T) { }, }) require.NoError(t, err) + require.Len(t, wr.Records, 1) require.Equal(t, "invalid message: error count 1:\n\t- #/required: required properties are missing: bar", wr.Records[0].BatchIndexErrorMessage) }, }, diff --git a/providers/asyncapi3/message.go b/providers/asyncapi3/message.go index ef6ab3bc3..a626b2d0b 100644 --- a/providers/asyncapi3/message.go +++ b/providers/asyncapi3/message.go @@ -70,64 +70,86 @@ func (r *MessageTraitRef) UnmarshalJSON(b []byte) error { return r.Reference.UnmarshalJson(b, &r.Value) } -func (r *MessageRef) parse(config *dynamic.Config, reader dynamic.Reader) error { - if len(r.Ref) > 0 { - return dynamic.Resolve(r.Ref, &r.Value, config, reader) +func (r *MessageRef) Parse(config *dynamic.Config, reader dynamic.Reader) error { + if r == nil { + return nil } + if r.Ref != "" { + var resolved *MessageRef + if err := dynamic.Resolve(r.Ref, &resolved, config, reader); err != nil { + return err + } + r.Value = resolved.Value + return nil + } + return r.Value.Parse(config, reader) +} - if r.Value == nil { +func (m *Message) Parse(config *dynamic.Config, reader dynamic.Reader) error { + if m == nil { return nil } - if r.Value.Payload != nil { - if err := r.Value.Payload.Parse(config, reader); err != nil { + if m.Payload != nil { + if err := m.Payload.Parse(config, reader); err != nil { return err } } - if r.Value.Headers != nil { - if err := r.Value.Headers.Parse(config, reader); err != nil { + if m.Headers != nil { + if err := m.Headers.Parse(config, reader); err != nil { return err } } - if r.Value.CorrelationId != nil { - if err := r.Value.CorrelationId.parse(config, reader); err != nil { + if m.CorrelationId != nil { + if err := m.CorrelationId.Parse(config, reader); err != nil { return err } } - for _, trait := range r.Value.Traits { - if err := trait.parse(config, reader); err != nil { + for _, trait := range m.Traits { + if err := trait.Parse(config, reader); err != nil { return err } - r.Value.applyTrait(trait.Value) + m.applyTrait(trait.Value) } - if r.Value.ContentType == "" { + if m.ContentType == "" { cfg, ok := config.Data.(*Config) if ok { - r.Value.ContentType = cfg.DefaultContentType + m.ContentType = cfg.DefaultContentType } - if r.Value.ContentType == "" { + if m.ContentType == "" { log.Warnf("content type is missing, using default %s", DefaultContentType) - r.Value.ContentType = DefaultContentType + m.ContentType = DefaultContentType } } - if r.Value.Bindings.Kafka.Key != nil { - err := r.Value.Bindings.Kafka.Key.Parse(config, reader) + if m.Bindings.Kafka.Key != nil { + err := m.Bindings.Kafka.Key.Parse(config, reader) if err != nil { return err } } + for _, doc := range m.ExternalDocs { + if err := doc.Parse(config, reader); err != nil { + return err + } + } + return nil } -func (r *MessageTraitRef) parse(config *dynamic.Config, reader dynamic.Reader) error { +func (r *MessageTraitRef) Parse(config *dynamic.Config, reader dynamic.Reader) error { if len(r.Ref) > 0 { - return dynamic.Resolve(r.Ref, &r.Value, config, reader) + var resolved *MessageTraitRef + if err := dynamic.Resolve(r.Ref, &resolved, config, reader); err != nil { + return err + } + r.Value = resolved.Value + return nil } if r.Value == nil { diff --git a/providers/asyncapi3/message_test.go b/providers/asyncapi3/message_test.go index 181c25c18..581b29738 100644 --- a/providers/asyncapi3/message_test.go +++ b/providers/asyncapi3/message_test.go @@ -2,14 +2,14 @@ package asyncapi3_test import ( "encoding/json" - "github.com/stretchr/testify/require" - "gopkg.in/yaml.v3" "mokapi/config/dynamic" "mokapi/config/dynamic/dynamictest" "mokapi/providers/asyncapi3" - "mokapi/providers/openapi/schema" jsonSchema "mokapi/schema/json/schema" "testing" + + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" ) func TestMessage_UnmarshalJSON(t *testing.T) { @@ -23,10 +23,9 @@ func TestMessage_UnmarshalJSON(t *testing.T) { data: `{ "payload": { "schemaFormat": "application/vnd.oai.openapi;version=3.0.0", "schema": { "$ref": "foo.json#foo" } }}`, test: func(t *testing.T, cfg *asyncapi3.Message, err error) { require.NoError(t, err) - require.Equal(t, "application/vnd.oai.openapi;version=3.0.0", cfg.Payload.Value.Format) - s := cfg.Payload.Value.Schema.(*schema.Schema) - require.NotNil(t, s) - require.Equal(t, "foo.json#foo", s.Ref) + msf := cfg.Payload.Value.(*asyncapi3.MultiSchemaFormat) + require.Equal(t, "application/vnd.oai.openapi;version=3.0.0", msf.Format) + require.Equal(t, "foo.json#foo", msf.Schema.Ref) }, }, { @@ -34,8 +33,7 @@ func TestMessage_UnmarshalJSON(t *testing.T) { data: `{ "payload": { "type": "string" }}`, test: func(t *testing.T, cfg *asyncapi3.Message, err error) { require.NoError(t, err) - require.Equal(t, "", cfg.Payload.Value.Format) - require.Equal(t, "string", cfg.Payload.Value.Schema.(*jsonSchema.Schema).Type.String()) + require.Equal(t, "string", cfg.Payload.Value.(*jsonSchema.Schema).Type.String()) }, }, } @@ -69,10 +67,10 @@ payload: `, test: func(t *testing.T, cfg *asyncapi3.Message, err error) { require.NoError(t, err) - require.Equal(t, "application/vnd.oai.openapi;version=3.0.0", cfg.Payload.Value.Format) - s := cfg.Payload.Value.Schema.(*schema.Schema) - require.NotNil(t, s) - require.Equal(t, "foo.json#foo", s.Ref) + msf := cfg.Payload.Value.(*asyncapi3.MultiSchemaFormat) + require.Equal(t, "application/vnd.oai.openapi;version=3.0.0", msf.Format) + require.NotNil(t, msf.Schema) + require.Equal(t, "foo.json#foo", msf.Schema.Ref) }, }, { @@ -83,8 +81,7 @@ payload: `, test: func(t *testing.T, cfg *asyncapi3.Message, err error) { require.NoError(t, err) - require.Equal(t, "", cfg.Payload.Value.Format) - require.Equal(t, "string", cfg.Payload.Value.Schema.(*jsonSchema.Schema).Type.String()) + require.Equal(t, "string", cfg.Payload.Value.(*jsonSchema.Schema).Type.String()) }, }, } @@ -111,26 +108,40 @@ func TestMessage_Parse(t *testing.T) { { name: "no content type set", cfg: &asyncapi3.Config{ - Components: &asyncapi3.Components{Messages: map[string]*asyncapi3.MessageRef{ - "foo": {Value: &asyncapi3.Message{}}, - }}, + Operations: map[string]*asyncapi3.OperationRef{ + "foo": {Value: &asyncapi3.Operation{ + Messages: []*asyncapi3.MessageRef{ + {Value: &asyncapi3.Message{}}, + }, + }}, + }, }, test: func(t *testing.T, cfg *asyncapi3.Config, err error) { require.NoError(t, err) - require.Equal(t, "application/json", cfg.Components.Messages["foo"].Value.ContentType) + op := cfg.Operations["foo"] + require.NotNil(t, op) + require.Len(t, op.Value.Messages, 1) + require.Equal(t, "application/json", op.Value.Messages[0].Value.ContentType) }, }, { name: "use defaultContentType", cfg: &asyncapi3.Config{ DefaultContentType: "avro/binary", - Components: &asyncapi3.Components{Messages: map[string]*asyncapi3.MessageRef{ - "foo": {Value: &asyncapi3.Message{}}, - }}, + Operations: map[string]*asyncapi3.OperationRef{ + "foo": {Value: &asyncapi3.Operation{ + Messages: []*asyncapi3.MessageRef{ + {Value: &asyncapi3.Message{}}, + }, + }}, + }, }, test: func(t *testing.T, cfg *asyncapi3.Config, err error) { require.NoError(t, err) - require.Equal(t, "avro/binary", cfg.Components.Messages["foo"].Value.ContentType) + op := cfg.Operations["foo"] + require.NotNil(t, op) + require.Len(t, op.Value.Messages, 1) + require.Equal(t, "avro/binary", op.Value.Messages[0].Value.ContentType) }, }, } diff --git a/providers/asyncapi3/operation.go b/providers/asyncapi3/operation.go index a72929297..09ada4a33 100644 --- a/providers/asyncapi3/operation.go +++ b/providers/asyncapi3/operation.go @@ -55,50 +55,60 @@ func (r *OperationTraitRef) UnmarshalJSON(b []byte) error { return r.Reference.UnmarshalJson(b, &r.Value) } -func (r *OperationRef) parse(config *dynamic.Config, reader dynamic.Reader) error { +func (r *OperationRef) Parse(config *dynamic.Config, reader dynamic.Reader) error { if r == nil { return nil } if len(r.Ref) > 0 { - return dynamic.Resolve(r.Ref, &r.Value, config, reader) + var resolved *OperationRef + if err := dynamic.Resolve(r.Ref, &resolved, config, reader); err != nil { + return err + } + r.Value = resolved.Value + return nil } + return r.Value.Parse(config, reader) +} - if r.Value == nil { +func (o *Operation) Parse(config *dynamic.Config, reader dynamic.Reader) error { + if o == nil { return nil } - if len(r.Value.Channel.Ref) > 0 { - if err := dynamic.Resolve(r.Value.Channel.Ref, &r.Value.Channel.Value, config, reader); err != nil { + if len(o.Channel.Ref) > 0 { + var resolved *ChannelRef + if err := dynamic.Resolve(o.Channel.Ref, &resolved, config, reader); err != nil { return err } + o.Channel.Value = resolved.Value } - for _, msg := range r.Value.Messages { - if err := msg.parse(config, reader); err != nil { + for _, msg := range o.Messages { + if err := msg.Parse(config, reader); err != nil { return err } } - for _, trait := range r.Value.Traits { - if err := trait.parse(config, reader); err != nil { + for _, trait := range o.Traits { + if err := trait.Parse(config, reader); err != nil { return err } - r.Value.applyTrait(trait.Value) + o.applyTrait(trait.Value) } return nil } -func (r *OperationTraitRef) parse(config *dynamic.Config, reader dynamic.Reader) error { +func (r *OperationTraitRef) Parse(config *dynamic.Config, reader dynamic.Reader) error { if len(r.Ref) > 0 { - return dynamic.Resolve(r.Ref, &r.Value, config, reader) - } - - if r.Value == nil { + var resolved *OperationTraitRef + if err := dynamic.Resolve(r.Ref, &resolved, config, reader); err != nil { + return err + } + r.Value = resolved.Value return nil } - return nil } diff --git a/providers/asyncapi3/operation_test.go b/providers/asyncapi3/operation_test.go index f471688ab..d83ab002f 100644 --- a/providers/asyncapi3/operation_test.go +++ b/providers/asyncapi3/operation_test.go @@ -1,13 +1,14 @@ package asyncapi3_test import ( - "github.com/stretchr/testify/require" - "gopkg.in/yaml.v3" "mokapi/config/dynamic" "mokapi/config/dynamic/dynamictest" "mokapi/providers/asyncapi3" json "mokapi/schema/json/schema" "testing" + + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" ) func TestOperation(t *testing.T) { @@ -40,7 +41,7 @@ operations: require.NotNil(t, op.Value) require.Equal(t, "send", op.Value.Action) require.Equal(t, "userSignedUp", op.Value.Channel.Value.Name) - require.IsType(t, &json.Schema{}, op.Value.Messages[0].Value.Payload.Value.Schema) - js := op.Value.Messages[0].Value.Payload.Value.Schema.(*json.Schema) + require.IsType(t, &json.Schema{}, op.Value.Messages[0].Value.Payload.Value) + js := op.Value.Messages[0].Value.Payload.Value.(*json.Schema) require.Equal(t, "string", js.Type.String()) } diff --git a/providers/asyncapi3/parameter.go b/providers/asyncapi3/parameter.go index 83d5fe218..df41dc09e 100644 --- a/providers/asyncapi3/parameter.go +++ b/providers/asyncapi3/parameter.go @@ -1,8 +1,9 @@ package asyncapi3 import ( - "gopkg.in/yaml.v3" "mokapi/config/dynamic" + + "gopkg.in/yaml.v3" ) type ParameterRef struct { @@ -26,9 +27,13 @@ func (r *ParameterRef) UnmarshalJSON(b []byte) error { return r.Reference.UnmarshalJson(b, &r.Value) } -func (r *ParameterRef) parse(config *dynamic.Config, reader dynamic.Reader) error { +func (r *ParameterRef) Parse(config *dynamic.Config, reader dynamic.Reader) error { if len(r.Ref) > 0 { - return dynamic.Resolve(r.Ref, &r.Value, config, reader) + var resolved *ParameterRef + if err := dynamic.Resolve(r.Ref, &resolved, config, reader); err != nil { + return err + } + r.Value = resolved.Value } return nil diff --git a/providers/asyncapi3/patch_test.go b/providers/asyncapi3/patch_test.go index b126c895e..78817c137 100644 --- a/providers/asyncapi3/patch_test.go +++ b/providers/asyncapi3/patch_test.go @@ -635,7 +635,7 @@ func TestConfig_Patch_Message(t *testing.T) { }, test: func(t *testing.T, result *asyncapi3.Config) { msg := result.Channels["foo"].Value.Messages["foo"].Value - r := msg.Payload.Value.Schema.(*schema.Schema) + r := msg.Payload.Value.(*schema.Schema) require.Equal(t, "string", r.Type.String()) }, }, @@ -692,7 +692,7 @@ func TestConfig_Patch_Components(t *testing.T) { }, test: func(t *testing.T, result *asyncapi3.Config) { require.Len(t, result.Components.Schemas, 1) - s := result.Components.Schemas["foo"].Value.Schema.(*schema.Schema) + s := result.Components.Schemas["foo"].Value.(*schema.Schema) require.Equal(t, "number", s.Type.String()) }, }, @@ -704,9 +704,9 @@ func TestConfig_Patch_Components(t *testing.T) { }, test: func(t *testing.T, result *asyncapi3.Config) { require.Len(t, result.Components.Schemas, 2) - s := result.Components.Schemas["foo"].Value.Schema.(*schema.Schema) + s := result.Components.Schemas["foo"].Value.(*schema.Schema) require.Equal(t, "number", s.Type.String()) - s = result.Components.Schemas["bar"].Value.Schema.(*schema.Schema) + s = result.Components.Schemas["bar"].Value.(*schema.Schema) require.Equal(t, "string", s.Type.String()) }, }, @@ -718,7 +718,7 @@ func TestConfig_Patch_Components(t *testing.T) { }, test: func(t *testing.T, result *asyncapi3.Config) { require.Len(t, result.Components.Schemas, 1) - s := result.Components.Schemas["foo"].Value.Schema.(*schema.Schema) + s := result.Components.Schemas["foo"].Value.(*schema.Schema) require.Equal(t, "number", s.Type.String()) require.Equal(t, "double", s.Format) }, diff --git a/providers/asyncapi3/schema.go b/providers/asyncapi3/schema.go index d863891ad..1f5f2bee3 100644 --- a/providers/asyncapi3/schema.go +++ b/providers/asyncapi3/schema.go @@ -12,12 +12,13 @@ import ( jsonSchema "mokapi/schema/json/schema" "reflect" + log "github.com/sirupsen/logrus" "gopkg.in/yaml.v3" ) type SchemaRef struct { dynamic.Reference - Value *MultiSchemaFormat + Value Schema } type Schema interface { @@ -25,8 +26,8 @@ type Schema interface { } type MultiSchemaFormat struct { - Format string `yaml:"schemaFormat,omitempty" json:"schemaFormat,omitempty"` - Schema Schema `yaml:"schema" json:"schema"` + Format string `yaml:"schemaFormat,omitempty" json:"schemaFormat,omitempty"` + Schema *SchemaRef `yaml:"schema" json:"schema"` } func (r *SchemaRef) UnmarshalYAML(node *yaml.Node) error { @@ -45,7 +46,7 @@ func (r *SchemaRef) UnmarshalYAML(node *yaml.Node) error { var s *jsonSchema.Schema err = node.Decode(&s) if err == nil { - r.Value = &MultiSchemaFormat{Schema: s} + r.Value = s } return err } @@ -53,8 +54,8 @@ func (r *SchemaRef) UnmarshalYAML(node *yaml.Node) error { func (r *SchemaRef) UnmarshalJSON(b []byte) error { d := json.NewDecoder(bytes.NewReader(b)) - err := d.Decode(&r.Ref) - if err == nil { + err := d.Decode(&r.Reference) + if err == nil && len(r.Ref) > 0 { return nil } @@ -70,33 +71,39 @@ func (r *SchemaRef) UnmarshalJSON(b []byte) error { var s *jsonSchema.Schema err = d.Decode(&s) if err == nil { - r.Value = &MultiSchemaFormat{Schema: s} + r.Value = s } return err } func (r *SchemaRef) Parse(config *dynamic.Config, reader dynamic.Reader) error { if len(r.Ref) > 0 { - err := dynamic.Resolve(r.Ref, &r.Value, config, reader) + var resolved *SchemaRef + err := dynamic.Resolve(r.Ref, &resolved, config, reader) if err != nil { - type t struct { - s *jsonSchema.Schema - } - s := &t{} - err = dynamic.Resolve(r.Ref, &s.s, config, reader) + s := &SchemaRef{Value: &jsonSchema.Schema{}} + err = dynamic.Resolve(r.Ref, &s.Value, config, reader) if err != nil { return err } - r.Value = &MultiSchemaFormat{Schema: s.s} + r.Value = s.Value + } else { + r.Value = resolved.Value } return nil } + return r.Value.Parse(config, reader) +} - return r.Value.parse(config, reader) +func (r *SchemaRef) ConvertTo(i interface{}) (interface{}, error) { + if s, ok := r.Value.(*jsonSchema.Schema); ok { + return s, nil + } + return nil, fmt.Errorf("unsupported schema convert %T: %T", r.Value, i) } -func (m *MultiSchemaFormat) Marshal(v any, ct media.ContentType) ([]byte, error) { - switch s := m.Schema.(type) { +func (r *SchemaRef) Marshal(v any, ct media.ContentType) ([]byte, error) { + switch s := r.Value.(type) { case *jsonSchema.Schema: e := encoding.NewEncoder(s) return e.Write(v, ct) @@ -104,12 +111,36 @@ func (m *MultiSchemaFormat) Marshal(v any, ct media.ContentType) ([]byte, error) return s.Marshal(v) case *openapi.Schema: return s.Marshal(v, ct) + case *SchemaRef: + return s.Marshal(v, ct) + case *MultiSchemaFormat: + return s.Schema.Marshal(v, ct) default: return nil, fmt.Errorf("unsupported schema type: %T", v) } } -func (m *MultiSchemaFormat) parse(config *dynamic.Config, reader dynamic.Reader) error { +func (r *SchemaRef) GetSchema() (Schema, error) { + if r.Value == nil { + return nil, nil + } + + switch s := r.Value.(type) { + case *jsonSchema.Schema, *avro.Schema, *openapi.Schema: + return s, nil + case *SchemaRef: + return r.Value, nil + case *MultiSchemaFormat: + return s.Schema.GetSchema() + default: + return nil, fmt.Errorf("unsupported schema type: %T", s) + } +} + +func (m *MultiSchemaFormat) Parse(config *dynamic.Config, reader dynamic.Reader) error { + if m == nil { + return nil + } if m.Schema != nil { return m.Schema.Parse(config, reader) } @@ -119,7 +150,7 @@ func (m *MultiSchemaFormat) parse(config *dynamic.Config, reader dynamic.Reader) func (m *MultiSchemaFormat) ConvertTo(i interface{}) (interface{}, error) { switch i.(type) { case *jsonSchema.Schema: - switch s := m.Schema.(type) { + switch s := m.Schema.Value.(type) { case *jsonSchema.Schema: return m.Schema, nil case *openapi.Schema: @@ -128,11 +159,11 @@ func (m *MultiSchemaFormat) ConvertTo(i interface{}) (interface{}, error) { return avro.ConvertToJsonSchema(s), nil } case *openapi.Schema: - if _, ok := m.Schema.(*openapi.Schema); ok { + if _, ok := m.Schema.Value.(*openapi.Schema); ok { return m.Schema, nil } case *avro.Schema: - if _, ok := m.Schema.(*avro.Schema); ok { + if _, ok := m.Schema.Value.(*avro.Schema); ok { return m.Schema, nil } } @@ -177,9 +208,22 @@ func (m *MultiSchemaFormat) UnmarshalJSON(b []byte) error { } } - m.Schema, err = unmarshal(raw, m.Format) + ref := &SchemaRef{} + err = json.Unmarshal(raw, &ref) + if err == nil && ref.Ref != "" { + m.Schema = ref + return nil + } - return err + var s Schema + s, err = unmarshal(raw, m.Format) + if err != nil { + return err + } + if s != nil { + m.Schema = &SchemaRef{Value: s} + } + return nil } func (m *MultiSchemaFormat) UnmarshalYAML(node *yaml.Node) error { @@ -202,28 +246,39 @@ func (m *MultiSchemaFormat) UnmarshalYAML(node *yaml.Node) error { return nil } + if schemaNode.Kind != yaml.MappingNode { + return fmt.Errorf("unexpected yaml node kind: %v", node.Kind) + } + + ref := &SchemaRef{} + err := schemaNode.Decode(&ref) + if err == nil && ref.Ref != "" { + m.Schema = ref + return nil + } + switch { case isOpenApi(format): var s *openapi.Schema - err := schemaNode.Decode(&s) + err = schemaNode.Decode(&s) if err != nil { return err } - m.Schema = s + m.Schema = &SchemaRef{Value: s} case isAvro(format): var ref *AvroRef - err := schemaNode.Decode(&ref) + err = schemaNode.Decode(&ref) if err != nil { return err } - m.Schema = ref + m.Schema = &SchemaRef{Value: ref} default: var s *jsonSchema.Schema - err := schemaNode.Decode(&s) + err = schemaNode.Decode(&s) if err != nil { return err } - m.Schema = s + m.Schema = &SchemaRef{Value: s} } return nil @@ -236,11 +291,36 @@ func (r *SchemaRef) Patch(patch *SchemaRef) { if r.Value == nil { r.Value = patch.Value } else { - r.Value.patch(patch.Value) + v1 := reflect.ValueOf(r.Value) + v2 := reflect.ValueOf(patch.Value) + + // if patch has different schema type then simple overwrite + if v1.Type() != v2.Type() { + r.Value = patch.Value + } else { + switch s := r.Value.(type) { + case *openapi.Schema: + p, ok := patch.Value.(*openapi.Schema) + if !ok { + log.Errorf("unexpected patch type: %T", patch.Value) + } else { + s.Patch(p) + } + case *avro.Schema: + log.Errorf("patch not supported for Avro schema") + case *jsonSchema.Schema: + p, ok := patch.Value.(*jsonSchema.Schema) + if !ok { + log.Errorf("unexpected patch type: %T", patch.Value) + } else { + s.Patch(p) + } + } + } } } -func (m *MultiSchemaFormat) patch(patch *MultiSchemaFormat) { +func (m *MultiSchemaFormat) Patch(patch *MultiSchemaFormat) { if patch == nil { return } @@ -254,19 +334,7 @@ func (m *MultiSchemaFormat) patch(patch *MultiSchemaFormat) { if m.Schema == nil { m.Schema = patch.Schema } else { - v1 := reflect.ValueOf(m.Schema) - v2 := reflect.ValueOf(patch.Schema) - - // if patch has different schema type then simple overwrite - if v1.Type() != v2.Type() { - m.Schema = patch.Schema - } else { - switch s := m.Schema.(type) { - case *avro.Schema: - case *jsonSchema.Schema: - s.Patch(patch.Schema.(*jsonSchema.Schema)) - } - } + m.Schema.Patch(patch.Schema) } } diff --git a/providers/asyncapi3/server.go b/providers/asyncapi3/server.go index ffd59fd56..e68ab2de7 100644 --- a/providers/asyncapi3/server.go +++ b/providers/asyncapi3/server.go @@ -1,8 +1,9 @@ package asyncapi3 import ( - "gopkg.in/yaml.v3" "mokapi/config/dynamic" + + "gopkg.in/yaml.v3" ) type ServerRef struct { @@ -36,9 +37,14 @@ type ServerVariable struct { Examples []string `yaml:"examples" json:"examples"` } -func (r *ServerRef) parse(config *dynamic.Config, reader dynamic.Reader) error { +func (r *ServerRef) Parse(config *dynamic.Config, reader dynamic.Reader) error { if len(r.Ref) > 0 { - return dynamic.Resolve(r.Ref, &r.Value, config, reader) + var resolved *ServerRef + if err := dynamic.Resolve(r.Ref, &resolved, config, reader); err != nil { + return err + } + r.Value = resolved.Value + return nil } if r.Value == nil { @@ -58,7 +64,7 @@ func (r *ServerRef) parse(config *dynamic.Config, reader dynamic.Reader) error { } for _, v := range r.Value.ExternalDocs { - if err := v.parse(config, reader); err != nil { + if err := v.Parse(config, reader); err != nil { return err } } diff --git a/providers/asyncapi3/tags.go b/providers/asyncapi3/tags.go index e9a3cd55f..252722bf3 100644 --- a/providers/asyncapi3/tags.go +++ b/providers/asyncapi3/tags.go @@ -1,8 +1,9 @@ package asyncapi3 import ( - "gopkg.in/yaml.v3" "mokapi/config/dynamic" + + "gopkg.in/yaml.v3" ) type TagRef struct { @@ -26,7 +27,12 @@ func (r *TagRef) UnmarshalJSON(b []byte) error { func (r *TagRef) parse(config *dynamic.Config, reader dynamic.Reader) error { if len(r.Ref) > 0 { - return dynamic.Resolve(r.Ref, &r.Value, config, reader) + var resolved *TagRef + if err := dynamic.Resolve(r.Ref, &resolved, config, reader); err != nil { + return err + } + r.Value = resolved.Value + return nil } if r.Value == nil { @@ -34,7 +40,7 @@ func (r *TagRef) parse(config *dynamic.Config, reader dynamic.Reader) error { } for _, v := range r.Value.ExternalDocs { - if err := v.parse(config, reader); err != nil { + if err := v.Parse(config, reader); err != nil { return err } } diff --git a/providers/openapi/components.go b/providers/openapi/components.go index b45d1aeb0..bbac81f50 100644 --- a/providers/openapi/components.go +++ b/providers/openapi/components.go @@ -19,7 +19,7 @@ type Components struct { type ComponentParameters map[string]*ParameterRef -func (p ComponentParameters) parse(config *dynamic.Config, reader dynamic.Reader) error { +func (p ComponentParameters) Parse(config *dynamic.Config, reader dynamic.Reader) error { for name, param := range p { if err := param.Parse(config, reader); err != nil { return fmt.Errorf("parse parameter '%v' failed: %w", name, err) diff --git a/providers/openapi/config.go b/providers/openapi/config.go index fc9213bb9..7dfc9f248 100644 --- a/providers/openapi/config.go +++ b/providers/openapi/config.go @@ -116,7 +116,7 @@ func (c *Config) Parse(config *dynamic.Config, reader dynamic.Reader) error { config.Scope.OpenIfNeeded(config.Info.Path()) - return c.Paths.parse(config, reader) + return c.Paths.Parse(config, reader) } func (c *Config) Patch(patch *Config) { diff --git a/providers/openapi/config_test.go b/providers/openapi/config_test.go index aa8b08765..03788b1dc 100644 --- a/providers/openapi/config_test.go +++ b/providers/openapi/config_test.go @@ -146,6 +146,45 @@ paths: } } +func TestConfig_Parse(t *testing.T) { + testcases := []struct { + name string + config *openapi.Config + test func(t *testing.T, config *openapi.Config, err error) + }{ + { + // empty server is handled in runtime after patching + name: "empty server", + config: &openapi.Config{}, + test: func(t *testing.T, config *openapi.Config, err error) { + require.NoError(t, err) + require.Len(t, config.Servers, 0) + }, + }, + { + name: "one server", + config: openapitest.NewConfig("3.1.0", openapitest.WithServer("/foo", "")), + test: func(t *testing.T, config *openapi.Config, err error) { + require.NoError(t, err) + require.Len(t, config.Servers, 1) + require.Equal(t, "/foo", config.Servers[0].Url) + }, + }, + } + + for _, tc := range testcases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + err := tc.config.Parse(&dynamic.Config{ + Data: tc.config, + Info: dynamictest.NewConfigInfo(), + }, &dynamictest.Reader{}) + tc.test(t, tc.config, err) + }) + } +} + func TestResponses(t *testing.T) { testdata := []struct { name string @@ -358,6 +397,7 @@ func TestConfig_PetStore_Path(t *testing.T) { require.True(t, put.Responses.Len() == 3) r := put.Responses.GetResponse(http.StatusBadRequest) + require.NotNil(t, r) require.Len(t, r.Content, 0) } @@ -380,7 +420,7 @@ func TestPetStore_Response(t *testing.T) { require.Nil(t, m) } -func TestPetStore_Paramters(t *testing.T) { +func TestPetStore_Parameters(t *testing.T) { config := &openapi.Config{} err := yaml.Unmarshal([]byte(petstore), &config) require.NoError(t, err) @@ -457,6 +497,10 @@ func TestConfig_Patch(t *testing.T) { tc := tc t.Run(tc.name, func(t *testing.T) { c := tc.configs[0] + for _, p := range tc.configs { + err := p.Parse(&dynamic.Config{Data: p, Info: dynamictest.NewConfigInfo()}, &dynamictest.Reader{}) + require.NoError(t, err) + } for _, p := range tc.configs[1:] { c.Patch(p) } diff --git a/providers/openapi/content.go b/providers/openapi/content.go index 1ac236a1a..96def51cb 100644 --- a/providers/openapi/content.go +++ b/providers/openapi/content.go @@ -4,9 +4,10 @@ import ( "bytes" "encoding/json" "fmt" - "gopkg.in/yaml.v3" "mokapi/config/dynamic" "mokapi/media" + + "gopkg.in/yaml.v3" ) type Content map[string]*MediaType @@ -75,9 +76,9 @@ func (c *Content) UnmarshalYAML(value *yaml.Node) error { } //goland:noinspection GoMixedReceiverTypes -func (c Content) parse(config *dynamic.Config, reader dynamic.Reader) error { +func (c Content) Parse(config *dynamic.Config, reader dynamic.Reader) error { for name, mediaType := range c { - if err := mediaType.parse(config, reader); err != nil { + if err := mediaType.Parse(config, reader); err != nil { return fmt.Errorf("parse content '%v' failed: %w", name, err) } } diff --git a/providers/openapi/example.go b/providers/openapi/example.go index 837c80060..a7f691fc2 100644 --- a/providers/openapi/example.go +++ b/providers/openapi/example.go @@ -3,8 +3,9 @@ package openapi import ( "encoding/json" "fmt" - "gopkg.in/yaml.v3" "mokapi/config/dynamic" + + "gopkg.in/yaml.v3" ) type ExampleValue struct { @@ -74,7 +75,7 @@ func (e *ExampleValue) UnmarshalYAML(node *yaml.Node) error { func (e Examples) parse(config *dynamic.Config, reader dynamic.Reader) error { for name, ex := range e { - if err := ex.parse(config, reader); err != nil { + if err := ex.Parse(config, reader); err != nil { return fmt.Errorf("parse example '%v' failed: %w", name, err) } } @@ -82,19 +83,24 @@ func (e Examples) parse(config *dynamic.Config, reader dynamic.Reader) error { return nil } -func (r *ExampleRef) parse(config *dynamic.Config, reader dynamic.Reader) error { +func (r *ExampleRef) Parse(config *dynamic.Config, reader dynamic.Reader) error { if r == nil { return nil } if len(r.Ref) > 0 { - return dynamic.Resolve(r.Ref, &r.Value, config, reader) + var resolved *ExampleRef + if err := dynamic.Resolve(r.Ref, &resolved, config, reader); err != nil { + return err + } + r.Value = resolved.Value + return nil } - return r.Value.parse(config, reader) + return r.Value.Parse(config, reader) } -func (e *Example) parse(config *dynamic.Config, reader dynamic.Reader) error { +func (e *Example) Parse(config *dynamic.Config, reader dynamic.Reader) error { if e == nil { return nil } diff --git a/providers/openapi/header.go b/providers/openapi/header.go index 109ca8820..76c8808e8 100644 --- a/providers/openapi/header.go +++ b/providers/openapi/header.go @@ -55,9 +55,9 @@ func (h *Header) UnmarshalYAML(node *yaml.Node) error { return nil } -func (h Headers) parse(config *dynamic.Config, reader dynamic.Reader) error { +func (h Headers) Parse(config *dynamic.Config, reader dynamic.Reader) error { for name, header := range h { - if err := header.parse(config, reader); err != nil { + if err := header.Parse(config, reader); err != nil { return fmt.Errorf("parse header '%v' failed: %w", name, err) } } @@ -65,13 +65,18 @@ func (h Headers) parse(config *dynamic.Config, reader dynamic.Reader) error { return nil } -func (r *HeaderRef) parse(config *dynamic.Config, reader dynamic.Reader) error { +func (r *HeaderRef) Parse(config *dynamic.Config, reader dynamic.Reader) error { if r == nil { return nil } if len(r.Ref) > 0 { - return dynamic.Resolve(r.Ref, &r.Value, config, reader) + var resolved *HeaderRef + if err := dynamic.Resolve(r.Ref, &resolved, config, reader); err != nil { + return err + } + r.Value = resolved.Value + return nil } return r.Value.Parse(config, reader) } diff --git a/providers/openapi/media_type.go b/providers/openapi/media_type.go index f7c05bdc6..3a4199a16 100644 --- a/providers/openapi/media_type.go +++ b/providers/openapi/media_type.go @@ -19,7 +19,7 @@ type MediaType struct { Encoding map[string]*Encoding `yaml:"encoding,omitempty" json:"encoding,omitempty"` } -func (m *MediaType) parse(config *dynamic.Config, reader dynamic.Reader) error { +func (m *MediaType) Parse(config *dynamic.Config, reader dynamic.Reader) error { if m == nil { return nil } @@ -62,7 +62,7 @@ func (m *MediaType) patch(patch *MediaType) { } } -func (m *MediaType) Parse(b []byte, contentType media.ContentType) (interface{}, error) { +func (m *MediaType) ParseData(b []byte, contentType media.ContentType) (interface{}, error) { if !isDefaultContentType(m.ContentType) { if !contentType.IsDerivedFrom(m.ContentType) { return nil, fmt.Errorf("content type '%v' does not match: %v", m.ContentType, contentType) diff --git a/providers/openapi/media_type_parse_test.go b/providers/openapi/media_type_parse_test.go index 6eafcd077..5f0ebc800 100644 --- a/providers/openapi/media_type_parse_test.go +++ b/providers/openapi/media_type_parse_test.go @@ -140,7 +140,7 @@ func TestRef_Unmarshal_Json(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() mt := &openapi.MediaType{Schema: tc.schema} - i, err := mt.Parse([]byte(tc.data), media.ParseContentType("application/json")) + i, err := mt.ParseData([]byte(tc.data), media.ParseContentType("application/json")) tc.test(t, i, err) }) } @@ -253,7 +253,7 @@ func TestRef_Unmarshal_Json_Any(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() mt := &openapi.MediaType{Schema: tc.schema} - i, err := mt.Parse([]byte(tc.s), media.ParseContentType("application/json")) + i, err := mt.ParseData([]byte(tc.s), media.ParseContentType("application/json")) tc.test(t, i, err) }) } @@ -507,7 +507,7 @@ func TestRef_Unmarshal_Json_String(t *testing.T) { t.Parallel() mt := openapi.MediaType{Schema: tc.schema} - i, err := mt.Parse([]byte(tc.s), media.ParseContentType("application/json")) + i, err := mt.ParseData([]byte(tc.s), media.ParseContentType("application/json")) tc.test(t, i, err) }) } @@ -568,7 +568,7 @@ func TestRef_Unmarshal_Json_OneOf(t *testing.T) { t.Run(tc.s, func(t *testing.T) { t.Parallel() mt := &openapi.MediaType{Schema: tc.schema} - i, err := mt.Parse([]byte(tc.s), media.ParseContentType("application/json")) + i, err := mt.ParseData([]byte(tc.s), media.ParseContentType("application/json")) tc.test(t, i, err) }) } @@ -617,7 +617,7 @@ func TestRef_Unmarshal_Json_AllOf(t *testing.T) { t.Parallel() mt := &openapi.MediaType{Schema: tc.schema} - i, err := mt.Parse([]byte(tc.s), media.ParseContentType("application/json")) + i, err := mt.ParseData([]byte(tc.s), media.ParseContentType("application/json")) tc.test(t, i, err) }) } @@ -759,7 +759,7 @@ func TestRef_Unmarshal_Json_Integer(t *testing.T) { t.Parallel() mt := &openapi.MediaType{Schema: tc.schema} - i, err := mt.Parse([]byte(tc.s), media.ParseContentType("application/json")) + i, err := mt.ParseData([]byte(tc.s), media.ParseContentType("application/json")) tc.test(t, i, err) }) } @@ -893,7 +893,7 @@ func TestParse_Number(t *testing.T) { t.Parallel() mt := &openapi.MediaType{Schema: tc.schema} - i, err := mt.Parse([]byte(tc.s), media.ParseContentType("application/json")) + i, err := mt.ParseData([]byte(tc.s), media.ParseContentType("application/json")) tc.test(t, i, err) }) } @@ -1089,7 +1089,7 @@ func TestRef_Unmarshal_Json_Object(t *testing.T) { t.Parallel() mt := &openapi.MediaType{Schema: tc.schema} - i, err := mt.Parse([]byte(tc.s), media.ParseContentType("application/json")) + i, err := mt.ParseData([]byte(tc.s), media.ParseContentType("application/json")) tc.test(t, i, err) }) } @@ -1242,7 +1242,7 @@ func TestRef_Unmarshal_Json_Array(t *testing.T) { t.Parallel() mt := &openapi.MediaType{Schema: tc.schema} - i, err := mt.Parse([]byte(tc.s), media.ParseContentType("application/json")) + i, err := mt.ParseData([]byte(tc.s), media.ParseContentType("application/json")) tc.test(t, i, err) }) } @@ -1292,7 +1292,7 @@ func TestRef_Unmarshal_Json_Bool(t *testing.T) { t.Parallel() mt := &openapi.MediaType{Schema: tc.schema} - i, err := mt.Parse([]byte(tc.s), media.ParseContentType("application/json")) + i, err := mt.ParseData([]byte(tc.s), media.ParseContentType("application/json")) tc.test(t, i, err) }) } @@ -1330,7 +1330,7 @@ func TestRef_Unmarshal_Json_Errors(t *testing.T) { t.Parallel() mt := &openapi.MediaType{Schema: tc.schema} - _, err := mt.Parse([]byte(tc.s), media.ParseContentType("application/json")) + _, err := mt.ParseData([]byte(tc.s), media.ParseContentType("application/json")) tc.test(t, err) }) } @@ -1363,7 +1363,7 @@ func TestRef_Unmarshal_Json_Dictionary(t *testing.T) { t.Parallel() mt := &openapi.MediaType{Schema: tc.schema} - _, err := mt.Parse([]byte(tc.s), media.ParseContentType("application/json")) + _, err := mt.ParseData([]byte(tc.s), media.ParseContentType("application/json")) require.Equal(t, tc.err, err) }) } @@ -1394,7 +1394,7 @@ func TestRef_Unmarshal_Json_SpecialNames(t *testing.T) { t.Parallel() mt := &openapi.MediaType{Schema: tc.schema} - i, err := mt.Parse([]byte(tc.s), media.ParseContentType("application/json")) + i, err := mt.ParseData([]byte(tc.s), media.ParseContentType("application/json")) tc.test(t, i, err) }) } diff --git a/providers/openapi/operation.go b/providers/openapi/operation.go index 6fc03a010..6ecac2aa1 100644 --- a/providers/openapi/operation.go +++ b/providers/openapi/operation.go @@ -84,7 +84,7 @@ func (o *Operation) getResponse(statusCode int) *Response { return o.Responses.GetResponse(statusCode) } -func (o *Operation) parse(p *Path, config *dynamic.Config, reader dynamic.Reader) error { +func (o *Operation) Parse(p *Path, config *dynamic.Config, reader dynamic.Reader) error { if o == nil { return nil } @@ -95,7 +95,7 @@ func (o *Operation) parse(p *Path, config *dynamic.Config, reader dynamic.Reader return err } - if err := o.RequestBody.parse(config, reader); err != nil { + if err := o.RequestBody.Parse(config, reader); err != nil { return fmt.Errorf("parse request body failed: %w", err) } diff --git a/providers/openapi/parameter.go b/providers/openapi/parameter.go index 5d04ad66e..43288ce21 100644 --- a/providers/openapi/parameter.go +++ b/providers/openapi/parameter.go @@ -102,7 +102,12 @@ func (r *ParameterRef) Parse(config *dynamic.Config, reader dynamic.Reader) erro } if len(r.Ref) > 0 && r.Value == nil { - return dynamic.Resolve(r.Ref, &r.Value, config, reader) + var resolved *ParameterRef + if err := dynamic.Resolve(r.Ref, &resolved, config, reader); err != nil { + return err + } + r.Value = resolved.Value + return nil } if r.Value == nil { diff --git a/providers/openapi/parameter_test.go b/providers/openapi/parameter_test.go index 3e1207ca8..f99aaa505 100644 --- a/providers/openapi/parameter_test.go +++ b/providers/openapi/parameter_test.go @@ -3,8 +3,6 @@ package openapi_test import ( "encoding/json" "fmt" - "github.com/stretchr/testify/require" - "gopkg.in/yaml.v3" "mokapi/config/dynamic" "mokapi/config/dynamic/dynamictest" "mokapi/providers/openapi" @@ -12,6 +10,9 @@ import ( "mokapi/providers/openapi/schema/schematest" "net/url" "testing" + + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" ) func TestParameterHeader_UnmarshalJSON(t *testing.T) { @@ -309,7 +310,7 @@ func TestParameterHeader_Parse(t *testing.T) { name: "reference", test: func(t *testing.T) { reader := dynamictest.ReaderFunc(func(u *url.URL, _ any) (*dynamic.Config, error) { - cfg := &dynamic.Config{Info: dynamic.ConfigInfo{Url: u}, Data: &openapi.Parameter{Description: "foo"}} + 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"}}} diff --git a/providers/openapi/path.go b/providers/openapi/path.go index 9114ce9a6..a7d17ff9b 100644 --- a/providers/openapi/path.go +++ b/providers/openapi/path.go @@ -146,16 +146,16 @@ func (p PathItems) Resolve(token string) (interface{}, error) { return nil, nil } -func (p PathItems) parse(config *dynamic.Config, reader dynamic.Reader) error { +func (p PathItems) Parse(config *dynamic.Config, reader dynamic.Reader) error { for name, e := range p { - if err := e.parse(name, config, reader); err != nil { + if err := e.Parse(name, config, reader); err != nil { return fmt.Errorf("parse path '%v' failed: %w", name, err) } } return nil } -func (r *PathRef) parse(name string, config *dynamic.Config, reader dynamic.Reader) error { +func (r *PathRef) Parse(name string, config *dynamic.Config, reader dynamic.Reader) error { if r == nil { return nil } @@ -166,13 +166,18 @@ func (r *PathRef) parse(name string, config *dynamic.Config, reader dynamic.Read }() if len(r.Ref) > 0 { - return dynamic.Resolve(r.Ref, &r.Value, config, reader) + var resolved *PathRef + if err := dynamic.Resolve(r.Ref, &resolved, config, reader); err != nil { + return err + } + r.Value = resolved.Value + return nil } - return r.Value.parse(config, reader) + return r.Value.Parse(config, reader) } -func (p *Path) parse(config *dynamic.Config, reader dynamic.Reader) error { +func (p *Path) Parse(config *dynamic.Config, reader dynamic.Reader) error { if p == nil { return nil } @@ -183,35 +188,35 @@ func (p *Path) parse(config *dynamic.Config, reader dynamic.Reader) error { } } - if err := p.Get.parse(p, config, reader); err != nil { + if err := p.Get.Parse(p, config, reader); err != nil { return fmt.Errorf("parse operation 'GET' failed: %w", err) } - if err := p.Post.parse(p, config, reader); err != nil { + if err := p.Post.Parse(p, config, reader); err != nil { return fmt.Errorf("parse operation 'POST' failed: %w", err) } - if err := p.Put.parse(p, config, reader); err != nil { + if err := p.Put.Parse(p, config, reader); err != nil { return fmt.Errorf("parse operation 'PUT' failed: %w", err) } - if err := p.Patch.parse(p, config, reader); err != nil { + if err := p.Patch.Parse(p, config, reader); err != nil { return fmt.Errorf("parse operation 'PATCH' failed: %w", err) } - if err := p.Delete.parse(p, config, reader); err != nil { + if err := p.Delete.Parse(p, config, reader); err != nil { return fmt.Errorf("parse operation 'DELETE' failed: %w", err) } - if err := p.Head.parse(p, config, reader); err != nil { + if err := p.Head.Parse(p, config, reader); err != nil { return fmt.Errorf("parse operation 'HEAD' failed: %w", err) } - if err := p.Options.parse(p, config, reader); err != nil { + if err := p.Options.Parse(p, config, reader); err != nil { return fmt.Errorf("parse operation 'OPTIONS' failed: %w", err) } - if err := p.Trace.parse(p, config, reader); err != nil { + if err := p.Trace.Parse(p, config, reader); err != nil { return fmt.Errorf("parse operation 'TRACE' failed: %w", err) } - if err := p.Query.parse(p, config, reader); err != nil { + if err := p.Query.Parse(p, config, reader); err != nil { return fmt.Errorf("parse operation 'QUERY' failed: %w", err) } for name, op := range p.AdditionalOperations { - if err := op.parse(p, config, reader); err != nil { + if err := op.Parse(p, config, reader); err != nil { return fmt.Errorf("parse operation '%s' failed: %w", name, err) } } diff --git a/providers/openapi/request_body.go b/providers/openapi/request_body.go index 45e51c9ea..36fcd3b61 100644 --- a/providers/openapi/request_body.go +++ b/providers/openapi/request_body.go @@ -132,7 +132,7 @@ func readBody(r *http.Request, contentType media.ContentType, mt *MediaType) (*B } func parseBody(body []byte, contentType media.ContentType, mt *MediaType) (*Body, error) { - v, err := mt.Parse(body, contentType) + v, err := mt.ParseData(body, contentType) return &Body{Value: v, Raw: string(body)}, err } @@ -172,7 +172,7 @@ func (r RequestBodies) parse(config *dynamic.Config, reader dynamic.Reader) erro } for name, body := range r { - if err := body.parse(config, reader); err != nil { + if err := body.Parse(config, reader); err != nil { return fmt.Errorf("parse request body '%v' failed: %w", name, err) } } @@ -180,16 +180,21 @@ func (r RequestBodies) parse(config *dynamic.Config, reader dynamic.Reader) erro return nil } -func (r *RequestBodyRef) parse(config *dynamic.Config, reader dynamic.Reader) error { +func (r *RequestBodyRef) Parse(config *dynamic.Config, reader dynamic.Reader) error { if r == nil { return nil } if len(r.Ref) > 0 { - return dynamic.Resolve(r.Ref, &r.Value, config, reader) + var resolved *RequestBodyRef + if err := dynamic.Resolve(r.Ref, &resolved, config, reader); err != nil { + return err + } + r.Value = resolved.Value + return nil } - return r.Value.Content.parse(config, reader) + return r.Value.Content.Parse(config, reader) } func (r RequestBodies) patch(patch RequestBodies) { diff --git a/providers/openapi/response.go b/providers/openapi/response.go index 08acce787..6add45a78 100644 --- a/providers/openapi/response.go +++ b/providers/openapi/response.go @@ -187,7 +187,7 @@ func (r *Responses) parse(config *dynamic.Config, reader dynamic.Reader) error { for it := r.Iter(); it.Next(); { res := it.Value() - if err := res.parse(config, reader); err != nil { + if err := res.Parse(config, reader); err != nil { return fmt.Errorf("parse response '%v' failed: %w", it.Key(), err) } } @@ -201,7 +201,7 @@ func (r ResponseBodies) parse(config *dynamic.Config, reader dynamic.Reader) err } for k, res := range r { - if err := res.parse(config, reader); err != nil { + if err := res.Parse(config, reader); err != nil { return fmt.Errorf("parse response '%v' failed: %w", k, err) } } @@ -209,13 +209,18 @@ func (r ResponseBodies) parse(config *dynamic.Config, reader dynamic.Reader) err return nil } -func (r *ResponseRef) parse(config *dynamic.Config, reader dynamic.Reader) error { +func (r *ResponseRef) Parse(config *dynamic.Config, reader dynamic.Reader) error { if r == nil { return nil } if len(r.Ref) > 0 { - return dynamic.Resolve(r.Ref, &r.Value, config, reader) + var resolved *ResponseRef + if err := dynamic.Resolve(r.Ref, &resolved, config, reader); err != nil { + return err + } + r.Value = resolved.Value + return nil } return r.Value.Parse(config, reader) @@ -226,11 +231,11 @@ func (r *Response) Parse(config *dynamic.Config, reader dynamic.Reader) error { return nil } - if err := r.Headers.parse(config, reader); err != nil { + if err := r.Headers.Parse(config, reader); err != nil { return err } - return r.Content.parse(config, reader) + return r.Content.Parse(config, reader) } func (r *Responses) patch(patch *Responses) { diff --git a/providers/openapi/schema/schema.go b/providers/openapi/schema/schema.go index 4fae84909..f45299731 100644 --- a/providers/openapi/schema/schema.go +++ b/providers/openapi/schema/schema.go @@ -5,7 +5,6 @@ import ( "fmt" "mokapi/config/dynamic" "mokapi/schema/json/schema" - "strings" "gopkg.in/yaml.v3" ) @@ -98,80 +97,64 @@ func (s *Schema) HasProperties() bool { } func (s *Schema) Parse(config *dynamic.Config, reader dynamic.Reader) error { - p := &schemaParser{visited: map[*Schema]bool{}, config: config, reader: reader} - return p.parse(s) -} - -type schemaParser struct { - visited map[*Schema]bool - config *dynamic.Config - reader dynamic.Reader -} - -func (p *schemaParser) parse(s *Schema) error { if s == nil { return nil } - if p.visited[s] { - return nil - } - p.visited[s] = true - if s.Id != "" { - p.config.OpenScope(s.Id) - defer p.config.CloseScope() + config.OpenScope(s.Id) + defer config.CloseScope() } else { - p.config.Scope.OpenIfNeeded(p.config.Info.Path()) + config.Scope.OpenIfNeeded(config.Info.Path()) } for _, d := range s.Definitions { - if err := p.parse(d); err != nil { + if err := d.Parse(config, reader); err != nil { return err } } for _, d := range s.Defs { - if err := p.parse(d); err != nil { + if err := d.Parse(config, reader); err != nil { return err } } if s.Anchor != "" { - if err := p.config.Scope.SetLexical(s.Anchor, s); err != nil { + if err := config.Scope.SetLexical(s.Anchor, s); err != nil { return err } } if s.DynamicAnchor != "" { - if err := p.config.Scope.SetDynamic(s.DynamicAnchor, s); err != nil { + if err := config.Scope.SetDynamic(s.DynamicAnchor, s); err != nil { return err } } if !s.skipParse("items") { - if err := p.parse(s.Items); err != nil { + if err := s.Items.Parse(config, reader); err != nil { return err } } if s.Properties != nil && !s.skipParse("properties") { for it := s.Properties.Iter(); it.Next(); { - if err := p.parse(it.Value()); err != nil { + if err := it.Value().Parse(config, reader); err != nil { return fmt.Errorf("parse schema '%v' failed: %w", it.Key(), err) } } } if !s.skipParse("additionalProperties") { - if err := p.parse(s.AdditionalProperties); err != nil { + if err := s.AdditionalProperties.Parse(config, reader); err != nil { return err } } if !s.skipParse("anyOf") { for _, r := range s.AnyOf { - if err := p.parse(r); err != nil { + if err := r.Parse(config, reader); err != nil { return err } } @@ -179,7 +162,7 @@ func (p *schemaParser) parse(s *Schema) error { if !s.skipParse("allOf") { for _, r := range s.AllOf { - if err := p.parse(r); err != nil { + if err := r.Parse(config, reader); err != nil { return err } } @@ -187,14 +170,14 @@ func (p *schemaParser) parse(s *Schema) error { if !s.skipParse("oneOf") { for _, r := range s.OneOf { - if err := p.parse(r); err != nil { + if err := r.Parse(config, reader); err != nil { return err } } } if s.Ref != "" { - err := dynamic.Resolve(s.Ref, &s.Sub, p.config, p.reader) + err := dynamic.Resolve(s.Ref, &s.Sub, config, reader) if err != nil { return err } @@ -208,7 +191,7 @@ func (p *schemaParser) parse(s *Schema) error { } if s.DynamicRef != "" { - err := dynamic.ResolveDynamic(s.DynamicRef, &s.Sub, p.config, p.reader) + err := dynamic.ResolveDynamic(s.DynamicRef, &s.Sub, config, reader) if err != nil { return err } @@ -219,125 +202,7 @@ func (p *schemaParser) parse(s *Schema) error { } func (s *Schema) String() string { - var sb strings.Builder - - if s.Boolean != nil { - return fmt.Sprintf("%v", s.Boolean) - } - - if len(s.AnyOf) > 0 { - sb.WriteString("any of ") - for _, i := range s.AnyOf { - if sb.Len() > 7 { - sb.WriteString(", ") - } - sb.WriteString(i.String()) - } - return sb.String() - } - if len(s.AllOf) > 0 { - sb.WriteString("all of ") - for _, i := range s.AllOf { - if sb.Len() > 7 { - sb.WriteString(", ") - } - sb.WriteString(i.String()) - } - return sb.String() - } - if len(s.OneOf) > 0 { - sb.WriteString("one of ") - for _, i := range s.OneOf { - if sb.Len() > 7 { - sb.WriteString(", ") - } - sb.WriteString(i.String()) - } - return sb.String() - } - - if len(s.Type) > 0 { - sb.WriteString(fmt.Sprintf("schema type=%v", s.Type.String())) - } - - if len(s.Format) > 0 { - sb.WriteString(fmt.Sprintf(" format=%v", s.Format)) - } - if len(s.Pattern) > 0 { - sb.WriteString(fmt.Sprintf(" pattern=%v", s.Pattern)) - } - if s.MinLength != nil { - sb.WriteString(fmt.Sprintf(" minLength=%v", *s.MinLength)) - } - if s.MaxLength != nil { - sb.WriteString(fmt.Sprintf(" maxLength=%v", *s.MaxLength)) - } - - if s.ExclusiveMinimum != nil { - if s.ExclusiveMinimum.IsA() { - sb.WriteString(fmt.Sprintf(" exclusiveMinimum=%v", s.ExclusiveMinimum.Value())) - } else if s.ExclusiveMinimum.B { - sb.WriteString(fmt.Sprintf(" exclusiveMinimum=%v", *s.Minimum)) - } - } else if s.Minimum != nil { - sb.WriteString(fmt.Sprintf(" minimum=%v", *s.Minimum)) - } - - if s.ExclusiveMaximum != nil { - if s.ExclusiveMaximum.IsA() { - sb.WriteString(fmt.Sprintf(" exclusiveMaximum=%v", s.ExclusiveMaximum.Value())) - } else if s.ExclusiveMaximum.B { - sb.WriteString(fmt.Sprintf(" exclusiveMaximum=%v", *s.Maximum)) - } - } else if s.Maximum != nil { - sb.WriteString(fmt.Sprintf(" maximum=%v", *s.Maximum)) - } - - if s.MinItems != nil { - sb.WriteString(fmt.Sprintf(" minItems=%v", *s.MinItems)) - } - if s.MaxItems != nil { - sb.WriteString(fmt.Sprintf(" maxItems=%v", *s.MaxItems)) - } - if s.MinProperties != nil { - sb.WriteString(fmt.Sprintf(" minProperties=%v", *s.MinProperties)) - } - if s.MaxProperties != nil { - sb.WriteString(fmt.Sprintf(" maxProperties=%v", *s.MaxProperties)) - } - if s.UniqueItems != nil && *s.UniqueItems { - sb.WriteString(" unique-items") - } - - if s.Type.Includes("object") && s.Properties != nil { - var sbProp strings.Builder - for _, p := range s.Properties.Keys() { - if sbProp.Len() > 0 { - sbProp.WriteString(", ") - } - sbProp.WriteString(fmt.Sprintf("%v", p)) - } - sb.WriteString(fmt.Sprintf(" properties=[%v]", sbProp.String())) - } - if len(s.Required) > 0 { - sb.WriteString(fmt.Sprintf(" required=%v", s.Required)) - } - if s.Type.Includes("object") && !s.IsFreeForm() { - sb.WriteString(" free-form=false") - } - - if s.Type.Includes("array") && s.Items != nil { - sb.WriteString(" items=") - sb.WriteString(s.Items.String()) - } - - if len(s.Title) > 0 { - sb.WriteString(fmt.Sprintf(" title=%v", s.Title)) - } else if len(s.Description) > 0 { - sb.WriteString(fmt.Sprintf(" description=%v", s.Description)) - } - - return strings.TrimSpace(sb.String()) + return ConvertToJsonSchema(s).String() } func (s *Schema) IsFreeForm() bool { diff --git a/providers/openapi/schema/schema_parse_test.go b/providers/openapi/schema/schema_parse_test.go index 4a1da99e5..ce548180c 100644 --- a/providers/openapi/schema/schema_parse_test.go +++ b/providers/openapi/schema/schema_parse_test.go @@ -215,7 +215,27 @@ $ref: '#/$defs/a' err = s.Parse(&dynamic.Config{Data: s}, &dynamictest.Reader{}) require.NoError(t, err) - require.Equal(t, "", s.String()) + require.Equal(t, "empty schema", s.String()) + }, + }, + { + name: "self-recursion", + test: func(t *testing.T) { + data := ` +$defs: + a: + properties: + part: + $ref: '#/$defs/a' +$ref: '#/$defs/a' +` + var s *schema.Schema + err := yaml.Unmarshal([]byte(data), &s) + require.NoError(t, err) + + err = s.Parse(&dynamic.Config{Data: s}, &dynamictest.Reader{}) + require.NoError(t, err) + require.Equal(t, "schema properties=[part]", s.String()) }, }, { diff --git a/providers/swagger/convert.go b/providers/swagger/convert.go index 2a9aef143..d142f354d 100644 --- a/providers/swagger/convert.go +++ b/providers/swagger/convert.go @@ -30,7 +30,9 @@ func (c *converter) Convert() (*openapi.Config, error) { if len(c.config.Schemes) == 0 { if len(c.config.Host) == 0 { - result.Servers = append(result.Servers, &openapi.Server{Url: c.config.BasePath}) + if c.config.BasePath != "" { + result.Servers = append(result.Servers, &openapi.Server{Url: c.config.BasePath}) + } } else { result.Servers = append(result.Servers, &openapi.Server{Url: fmt.Sprintf("http://%v%v", c.config.Host, c.config.BasePath)}) } diff --git a/providers/swagger/convert_test.go b/providers/swagger/convert_test.go index eb35b839a..07ae1da7c 100644 --- a/providers/swagger/convert_test.go +++ b/providers/swagger/convert_test.go @@ -109,6 +109,14 @@ func TestConvert(t *testing.T) { require.Equal(t, "https://foo/bar", config.Servers[0].Url) }, }, + { + // empty server is handled in runtime after patching + name: "scheme, host and basePath is empty", + config: `{"swagger": "2.0"}`, + test: func(t *testing.T, config *openapi.Config) { + require.Len(t, config.Servers, 0) + }, + }, { name: "path ref", config: `{"swagger": "2.0","paths":{"/foo":{"$ref":"./foo.json"}}}`, diff --git a/runtime/events/index_test.go b/runtime/events/index_test.go index 6211a1d1a..df46cc77d 100644 --- a/runtime/events/index_test.go +++ b/runtime/events/index_test.go @@ -2,6 +2,7 @@ package events_test import ( "context" + "mokapi/config/dynamic/dynamictest" "mokapi/config/static" "mokapi/runtime" "mokapi/runtime/events" @@ -143,7 +144,7 @@ func TestIndex_Http(t *testing.T) { InMemory: true, }, }, - }) + }, &dynamictest.Reader{}) pool := safe.NewPool(context.Background()) app.Start(pool) diff --git a/runtime/index_test.go b/runtime/index_test.go index 01e5e816c..eadffe8d2 100644 --- a/runtime/index_test.go +++ b/runtime/index_test.go @@ -3,6 +3,7 @@ package runtime_test import ( "context" "mokapi/config/dynamic" + "mokapi/config/dynamic/dynamictest" "mokapi/config/static" "mokapi/providers/openapi/openapitest" "mokapi/runtime" @@ -19,7 +20,7 @@ import ( func TestIndex(t *testing.T) { cfg := &static.Config{} cfg.Api.Search.Enabled = true - app := runtime.New(cfg) + app := runtime.New(cfg, &dynamictest.Reader{}) pool := safe.NewPool(context.Background()) app.Start(pool) diff --git a/runtime/runtime.go b/runtime/runtime.go index 70741a62a..754514f4e 100644 --- a/runtime/runtime.go +++ b/runtime/runtime.go @@ -27,10 +27,12 @@ type App struct { cfg *static.Config searchIndex *SearchIndex + reader dynamic.Reader + Configs map[string]*dynamic.Config } -func New(cfg *static.Config) *App { +func New(cfg *static.Config, reader dynamic.Reader) *App { m := monitor.New() index := newSearchIndex(cfg.Api.Search) @@ -48,13 +50,14 @@ func New(cfg *static.Config) *App { Monitor: m, Events: em, Configs: map[string]*dynamic.Config{}, - http: NewHttpStore(cfg, index, em), + http: &HttpStore{cfg: cfg, index: index, events: em, reader: reader}, Kafka: &KafkaStore{monitor: m, cfg: cfg, index: index, events: em}, Mqtt: &MqttStore{monitor: m, cfg: cfg, sm: em}, Ldap: &LdapStore{cfg: cfg, events: em, index: index}, Mail: &MailStore{cfg: cfg, sm: em, index: index}, cfg: cfg, searchIndex: index, + reader: reader, } return app diff --git a/runtime/runtime_http.go b/runtime/runtime_http.go index bd55b3c3b..33c5908dc 100644 --- a/runtime/runtime_http.go +++ b/runtime/runtime_http.go @@ -22,6 +22,7 @@ type HttpStore struct { m sync.RWMutex index search.Index events *events.StoreManager + reader dynamic.Reader } type HttpInfo struct { @@ -35,15 +36,6 @@ type httpHandler struct { next openapi.Handler } -func NewHttpStore(cfg *static.Config, index search.Index, em *events.StoreManager) *HttpStore { - s := &HttpStore{ - cfg: cfg, - index: index, - events: em, - } - return s -} - func (s *HttpStore) Get(name string) *HttpInfo { s.m.RLock() defer s.m.RUnlock() @@ -96,7 +88,7 @@ func (s *HttpStore) Add(c *dynamic.Config) *HttpInfo { if s.cfg.Api.Search.Enabled { s.removeFromIndex(cfg) } - patchHttp(hc) + patchHttp(hc, s.reader) for path := range cfg.Paths { if _, ok := hc.seenPaths[path]; ok { @@ -125,7 +117,7 @@ func (s *HttpStore) Remove(c *dynamic.Config) { } delete(hc.configs, c.Info.Url.String()) - patchHttp(hc) + patchHttp(hc, s.reader) if s.cfg.Api.Search.Enabled { s.addToIndex(hc.Config) } @@ -147,7 +139,7 @@ func (c *HttpInfo) Handler(http *monitor.Http, emitter common.EventEmitter, eh e return &httpHandler{http: http, next: h} } -func patchHttp(c *HttpInfo) { +func patchHttp(c *HttpInfo, reader dynamic.Reader) { if len(c.configs) == 0 { c.Config = nil return @@ -172,6 +164,13 @@ func patchHttp(c *HttpInfo) { r.Patch(p) } + if len(c.configs) > 1 { + err := r.Parse(&dynamic.Config{Data: r}, reader) + if err != nil { + log.Errorf("failed to parse config: %s", err) + } + } + if len(r.Servers) == 0 { r.Servers = append(r.Servers, &openapi.Server{Url: "/"}) } diff --git a/runtime/runtime_http_search_test.go b/runtime/runtime_http_search_test.go index 365afeab4..71cfc150b 100644 --- a/runtime/runtime_http_search_test.go +++ b/runtime/runtime_http_search_test.go @@ -398,7 +398,7 @@ func TestIndex_Http(t *testing.T) { InMemory: true, }, }, - }) + }, &dynamictest.Reader{}) pool := safe.NewPool(context.Background()) app.Start(pool) diff --git a/runtime/runtime_http_test.go b/runtime/runtime_http_test.go index bac8a8f6c..9dc729b09 100644 --- a/runtime/runtime_http_test.go +++ b/runtime/runtime_http_test.go @@ -2,10 +2,12 @@ package runtime_test import ( "mokapi/config/dynamic" + "mokapi/config/dynamic/dynamictest" "mokapi/config/static" "mokapi/engine/enginetest" "mokapi/providers/openapi" "mokapi/providers/openapi/openapitest" + "mokapi/providers/openapi/schema/schematest" "mokapi/runtime" "mokapi/runtime/events" "mokapi/runtime/events/eventstest" @@ -58,7 +60,8 @@ func TestApp_AddHttp(t *testing.T) { r := httptest.NewRequest(http.MethodGet, "https://mokapi.io/foo", nil) rr := httptest.NewRecorder() - h.ServeHTTP(rr, r) + err := h.ServeHTTP(rr, r) + require.Nil(t, err) require.Equal(t, float64(1), m.RequestCounter.Sum()) }, @@ -79,7 +82,7 @@ func TestApp_AddHttp(t *testing.T) { for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { cfg := &static.Config{} - app := runtime.New(cfg) + app := runtime.New(cfg, &dynamictest.Reader{}) tc.test(t, app) }) } @@ -169,6 +172,44 @@ func TestApp_AddHttp_Patching(t *testing.T) { require.Len(t, e, 104) }, }, + { + name: "patch schema in response using $ref", + configs: []*dynamic.Config{ + newConfig("https://a.io/a", openapitest.NewConfig("3.1.0", + openapitest.WithInfo("foo", "", "foo"), + openapitest.WithPath("/foo", + openapitest.WithOperation(http.MethodGet, + openapitest.WithResponse(http.StatusOK, + openapitest.WithContent("application/json", + openapitest.WithSchema( + schematest.NewTypes(nil, schematest.WithRef("#/components/schemas/Foo")), + ), + ), + ), + ), + ), + openapitest.WithComponentSchema("Foo", schematest.New("string")), + ), + ), + newConfig("https://a.io/b", openapitest.NewConfig("3.1.0", + openapitest.WithInfo("foo", "", "foo"), + openapitest.WithComponentSchema("Foo", schematest.New("string", schematest.WithFormat("date-time"))), + ), + ), + }, + test: func(t *testing.T, app *runtime.App) { + info := app.GetHttp("foo") + p := info.Paths["/foo"] + require.NotNil(t, p) + op := p.Value.Get + require.NotNil(t, op) + res := op.Responses.GetResponse(http.StatusOK) + require.NotNil(t, res) + mt := res.Content["application/json"] + require.NotNil(t, mt) + require.Equal(t, "date-time", mt.Schema.Format) + }, + }, } for _, tc := range testcases { tc := tc @@ -177,7 +218,7 @@ func TestApp_AddHttp_Patching(t *testing.T) { if cfg == nil { cfg = &static.Config{} } - app := runtime.New(cfg) + app := runtime.New(cfg, &dynamictest.Reader{}) for _, c := range tc.configs { app.AddHttp(c) } diff --git a/runtime/runtime_kafka.go b/runtime/runtime_kafka.go index 821979851..5d6aff83c 100644 --- a/runtime/runtime_kafka.go +++ b/runtime/runtime_kafka.go @@ -26,6 +26,7 @@ type KafkaStore struct { events *events.StoreManager index search.Index m sync.RWMutex + reader dynamic.Reader } type KafkaInfo struct { @@ -41,14 +42,13 @@ type KafkaHandler struct { next kafka.Handler } -func NewKafkaInfo(c *dynamic.Config, store *store.Store, updateEventAndMetrics func(info *KafkaInfo)) *KafkaInfo { +func newKafkaInfo(store *store.Store, updateEventAndMetrics func(info *KafkaInfo)) *KafkaInfo { hc := &KafkaInfo{ configs: map[string]*dynamic.Config{}, Store: store, seenTopics: map[string]bool{}, updateEventAndMetrics: updateEventAndMetrics, } - hc.AddConfig(c) return hc } @@ -98,11 +98,10 @@ func (s *KafkaStore) Add(c *dynamic.Config, emitter common.EventEmitter) (*Kafka s.events.ResetStores(events.NewTraits().WithNamespace("kafka").WithName(cfg.Info.Name)) s.events.SetStore(int(eventStore.Size), events.NewTraits().WithNamespace("kafka").WithName(cfg.Info.Name)) - ki = NewKafkaInfo(c, store.NewEmpty(emitter, s.events, s.monitor.Kafka), s.updateEventStore) + ki = newKafkaInfo(store.NewEmpty(emitter, s.events, s.monitor.Kafka), s.updateEventStore) s.infos[cfg.Info.Name] = ki - } else { - ki.AddConfig(c) } + ki.addConfig(c, s.reader) if s.cfg.Api.Search.Enabled { s.addToIndex(ki.Config) @@ -137,7 +136,7 @@ func (s *KafkaStore) Remove(c *dynamic.Config) { s.removeFromIndex(ki.Config) } delete(ki.configs, c.Info.Url.String()) - ki.update() + ki.update(s.reader) if len(ki.configs) == 0 { s.m.RUnlock() @@ -150,13 +149,13 @@ func (s *KafkaStore) Remove(c *dynamic.Config) { } } -func (c *KafkaInfo) AddConfig(config *dynamic.Config) { +func (c *KafkaInfo) addConfig(config *dynamic.Config, reader dynamic.Reader) { key := config.Info.Url.String() c.configs[key] = config - c.update() + c.update(reader) } -func (c *KafkaInfo) update() { +func (c *KafkaInfo) update(reader dynamic.Reader) { if len(c.configs) == 0 { c.Config = nil c.Store = nil @@ -188,6 +187,13 @@ func (c *KafkaInfo) update() { } } + if len(c.configs) > 1 { + err := cfg.Parse(&dynamic.Config{Data: cfg}, reader) + if err != nil { + log.Errorf("failed to parse config: %s", err) + } + } + if cfg.Servers.Len() == 0 { log.Infof("no servers defined in AsyncAPI spec — using default Mokapi broker for cluster '%s'", cfg.Info.Name) if cfg.Servers == nil { diff --git a/runtime/runtime_kafka_search.go b/runtime/runtime_kafka_search.go index e787c9c0c..03a960b64 100644 --- a/runtime/runtime_kafka_search.go +++ b/runtime/runtime_kafka_search.go @@ -167,7 +167,7 @@ func getSchema(s *asyncapi3.SchemaRef) (*schema.IndexData, error) { if s == nil || s.Value == nil { return nil, nil } - switch v := s.Value.Schema.(type) { + switch v := s.Value.(type) { case *schema.Schema: return schema.NewIndexData(v), nil default: diff --git a/runtime/runtime_kafka_search_test.go b/runtime/runtime_kafka_search_test.go index a647ee840..4b24bd866 100644 --- a/runtime/runtime_kafka_search_test.go +++ b/runtime/runtime_kafka_search_test.go @@ -128,7 +128,7 @@ func TestIndex_Kafka(t *testing.T) { InMemory: true, }, }, - }) + }, &dynamictest.Reader{}) pool := safe.NewPool(context.Background()) app.Start(pool) diff --git a/runtime/runtime_kafka_test.go b/runtime/runtime_kafka_test.go index c431c0952..5f90b3376 100644 --- a/runtime/runtime_kafka_test.go +++ b/runtime/runtime_kafka_test.go @@ -2,6 +2,7 @@ package runtime_test import ( "mokapi/config/dynamic" + "mokapi/config/dynamic/dynamictest" "mokapi/config/static" "mokapi/engine/enginetest" "mokapi/kafka" @@ -52,10 +53,11 @@ func TestApp_AddKafka(t *testing.T) { name: "event store available", test: func(t *testing.T, app *runtime.App) { c := asyncapi3test.NewConfig(asyncapi3test.WithInfo("foo", "", "")) - app.Kafka.Add(getConfig("foo.bar", c), enginetest.NewEngine()) + _, err := app.Kafka.Add(getConfig("foo.bar", c), enginetest.NewEngine()) + require.NoError(t, err) require.NotNil(t, app.Kafka.Get("foo")) - err := app.Events.Push(&eventstest.Event{Name: "bar"}, events.NewTraits().WithNamespace("kafka").WithName("foo")) + err = app.Events.Push(&eventstest.Event{Name: "bar"}, events.NewTraits().WithNamespace("kafka").WithName("foo")) require.NoError(t, err, "event store should be available") }, }, @@ -63,10 +65,11 @@ func TestApp_AddKafka(t *testing.T) { name: "event store for topic available", test: func(t *testing.T, app *runtime.App) { c := asyncapi3test.NewConfig(asyncapi3test.WithInfo("foo", "", ""), asyncapi3test.WithChannel("bar")) - app.Kafka.Add(getConfig("foo.bar", c), enginetest.NewEngine()) + _, err := app.Kafka.Add(getConfig("foo.bar", c), enginetest.NewEngine()) + require.NoError(t, err) require.NotNil(t, app.Kafka.Get("foo")) - err := app.Events.Push(&eventstest.Event{Name: "bar"}, events.NewTraits().WithNamespace("kafka").WithName("foo").With("path", "bar")) + err = app.Events.Push(&eventstest.Event{Name: "bar"}, events.NewTraits().WithNamespace("kafka").WithName("foo").With("path", "bar")) require.NoError(t, err, "event store should be available") }, }, @@ -74,11 +77,13 @@ func TestApp_AddKafka(t *testing.T) { name: "event store for topic available after patching", test: func(t *testing.T, app *runtime.App) { c := asyncapi3test.NewConfig(asyncapi3test.WithInfo("foo", "", ""), asyncapi3test.WithChannel("foo")) - app.Kafka.Add(getConfig("foo.bar", c), enginetest.NewEngine()) + eng := enginetest.NewEngine() + _, err := app.Kafka.Add(getConfig("foo.bar", c), eng) + require.NoError(t, err) patch := asyncapi3test.NewConfig(asyncapi3test.WithInfo("foo", "", ""), asyncapi3test.WithChannel("bar")) - ki := app.Kafka.Get(patch.Info.Name) - ki.AddConfig(getConfig("foo.patch", patch)) + _, err = app.Kafka.Add(getConfig("foo.patch", patch), eng) + require.NoError(t, err) require.NotNil(t, app.Kafka.Get("foo")) @@ -133,7 +138,7 @@ func TestApp_AddKafka(t *testing.T) { for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { cfg := &static.Config{} - app := runtime.New(cfg) + app := runtime.New(cfg, &dynamictest.Reader{}) tc.test(t, app) }) } @@ -211,14 +216,43 @@ func TestApp_AddKafka_Patching(t *testing.T) { require.Equal(t, float64(1), v) }, }, + { + name: "patching a referenced object", + 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{Ref: "#/components/messages/bar"}}), + ), + asyncapi3test.WithComponentMessage("bar", &asyncapi3.Message{ + Summary: "original", + }), + )), + getConfig("https://mokapi.io/b", asyncapi3test.NewConfig( + asyncapi3test.WithInfo("foo", "bar", ""), + asyncapi3test.WithComponentMessage("bar", &asyncapi3.Message{ + Summary: "patch", + }), + )), + }, + 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, "patch", msg.Value.Summary) + }, + }, } for _, tc := range testcases { tc := tc t.Run(tc.name, func(t *testing.T) { cfg := &static.Config{} - app := runtime.New(cfg) + app := runtime.New(cfg, &dynamictest.Reader{}) for _, c := range tc.configs { - app.Kafka.Add(c, enginetest.NewEngine()) + _, err := app.Kafka.Add(c, enginetest.NewEngine()) + require.NoError(t, err) } tc.test(t, app) }) diff --git a/runtime/runtime_ldap_search_test.go b/runtime/runtime_ldap_search_test.go index 4a488d842..b42d6265c 100644 --- a/runtime/runtime_ldap_search_test.go +++ b/runtime/runtime_ldap_search_test.go @@ -141,7 +141,7 @@ func TestIndex_Ldap(t *testing.T) { InMemory: true, }, }, - }) + }, &dynamictest.Reader{}) pool := safe.NewPool(context.Background()) app.Start(pool) diff --git a/runtime/runtime_ldap_test.go b/runtime/runtime_ldap_test.go index f680dc62a..ff4510534 100644 --- a/runtime/runtime_ldap_test.go +++ b/runtime/runtime_ldap_test.go @@ -1,8 +1,8 @@ package runtime_test import ( - "github.com/stretchr/testify/require" "mokapi/config/dynamic" + "mokapi/config/dynamic/dynamictest" "mokapi/config/static" "mokapi/engine/enginetest" "mokapi/ldap" @@ -14,6 +14,8 @@ import ( "mokapi/runtime/monitor" "net/url" "testing" + + "github.com/stretchr/testify/require" ) func TestApp_AddLdap(t *testing.T) { @@ -60,7 +62,7 @@ func TestApp_AddLdap(t *testing.T) { for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { cfg := &static.Config{} - app := runtime.New(cfg) + app := runtime.New(cfg, &dynamictest.Reader{}) tc.test(t, app) }) } @@ -127,7 +129,7 @@ func TestApp_AddLdap_Patching(t *testing.T) { tc := tc t.Run(tc.name, func(t *testing.T) { cfg := &static.Config{} - app := runtime.New(cfg) + app := runtime.New(cfg, &dynamictest.Reader{}) for _, c := range tc.configs { app.Ldap.Add(c, enginetest.NewEngine()) } diff --git a/runtime/runtime_mail_search_test.go b/runtime/runtime_mail_search_test.go index dc08e0035..3487a3d27 100644 --- a/runtime/runtime_mail_search_test.go +++ b/runtime/runtime_mail_search_test.go @@ -161,7 +161,7 @@ func TestIndex_Mail(t *testing.T) { InMemory: true, }, }, - }) + }, &dynamictest.Reader{}) pool := safe.NewPool(context.Background()) app.Start(pool) diff --git a/runtime/runtime_mail_test.go b/runtime/runtime_mail_test.go index 71398c092..2cb448e71 100644 --- a/runtime/runtime_mail_test.go +++ b/runtime/runtime_mail_test.go @@ -2,8 +2,8 @@ package runtime_test import ( "context" - "github.com/stretchr/testify/require" "mokapi/config/dynamic" + "mokapi/config/dynamic/dynamictest" "mokapi/config/static" "mokapi/engine/enginetest" "mokapi/providers/mail" @@ -15,6 +15,8 @@ import ( "mokapi/smtp/smtptest" "net/url" "testing" + + "github.com/stretchr/testify/require" ) func TestApp_AddSmtp(t *testing.T) { @@ -61,7 +63,7 @@ func TestApp_AddSmtp(t *testing.T) { for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { cfg := &static.Config{} - app := runtime.New(cfg) + app := runtime.New(cfg, &dynamictest.Reader{}) tc.test(t, app) }) } @@ -128,7 +130,7 @@ func TestApp_AddSmtp_Patching(t *testing.T) { tc := tc t.Run(tc.name, func(t *testing.T) { cfg := &static.Config{} - app := runtime.New(cfg) + app := runtime.New(cfg, &dynamictest.Reader{}) for _, c := range tc.configs { app.Mail.Add(c) } diff --git a/runtime/runtime_search_test.go b/runtime/runtime_search_test.go index bf1d0670c..2a48adf04 100644 --- a/runtime/runtime_search_test.go +++ b/runtime/runtime_search_test.go @@ -96,7 +96,7 @@ func TestIndex_Config(t *testing.T) { Search: static.Search{ Enabled: true, InMemory: true, - }}}) + }}}, &dynamictest.Reader{}) pool := safe.NewPool(context.Background()) app.Start(pool) diff --git a/runtime/runtime_test.go b/runtime/runtime_test.go index 56e2e7565..0a6b17182 100644 --- a/runtime/runtime_test.go +++ b/runtime/runtime_test.go @@ -1,11 +1,13 @@ package runtime_test import ( - "github.com/stretchr/testify/require" + "mokapi/config/dynamic/dynamictest" "mokapi/config/static" "mokapi/runtime" "mokapi/version" "testing" + + "github.com/stretchr/testify/require" ) func TestNew(t *testing.T) { @@ -13,7 +15,7 @@ func TestNew(t *testing.T) { defer func() { version.BuildVersion = "" }() - app := runtime.New(&static.Config{}) + app := runtime.New(&static.Config{}, &dynamictest.Reader{}) require.NotNil(t, app.Monitor) require.Equal(t, "1.0", app.Version) require.Len(t, app.ListHttp(), 0) diff --git a/runtime/runtimetest/app.go b/runtime/runtimetest/app.go index 88b633d97..5a7b12140 100644 --- a/runtime/runtimetest/app.go +++ b/runtime/runtimetest/app.go @@ -13,7 +13,7 @@ import ( func NewHttpApp(configs ...*openapi.Config) *runtime.App { cfg := &static.Config{} - app := runtime.New(cfg) + app := runtime.New(cfg, &dynamictest.Reader{}) for i, cfg := range configs { app.AddHttp(&dynamic.Config{ Info: dynamictest.NewConfigInfo(dynamictest.WithUrl(fmt.Sprintf("%d", i))), @@ -31,7 +31,7 @@ type KafkaInfoOptions func(ki *runtime.KafkaInfo) func NewKafkaApp(configs ...*asyncapi3.Config) *runtime.App { cfg := &static.Config{} - app := runtime.New(cfg) + app := runtime.New(cfg, &dynamictest.Reader{}) for i, cfg := range configs { _, _ = app.Kafka.Add(&dynamic.Config{ Info: dynamictest.NewConfigInfo(dynamictest.WithUrl(fmt.Sprintf("%d", i))), @@ -43,7 +43,7 @@ func NewKafkaApp(configs ...*asyncapi3.Config) *runtime.App { func NewApp(opts ...Options) *runtime.App { cfg := &static.Config{} - app := runtime.New(cfg) + app := runtime.New(cfg, &dynamictest.Reader{}) for _, opt := range opts { opt(app) } diff --git a/schema/avro/schema/schema.go b/schema/avro/schema/schema.go index 88a6d4265..0431abd6a 100644 --- a/schema/avro/schema/schema.go +++ b/schema/avro/schema/schema.go @@ -3,9 +3,10 @@ package schema import ( "encoding/json" "fmt" - "gopkg.in/yaml.v3" "mokapi/config/dynamic" "strings" + + "gopkg.in/yaml.v3" ) var table = map[string]*Schema{} diff --git a/schema/json/schema/parse_test.go b/schema/json/schema/parse_test.go new file mode 100644 index 000000000..89a928283 --- /dev/null +++ b/schema/json/schema/parse_test.go @@ -0,0 +1,118 @@ +package schema_test + +import ( + "mokapi/config/dynamic" + "mokapi/config/dynamic/dynamictest" + "mokapi/schema/json/schema" + "mokapi/schema/json/schema/schematest" + "testing" + + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" +) + +func TestParse(t *testing.T) { + testcases := []struct { + name string + test func(t *testing.T) + }{ + { + name: "recursion", + test: func(t *testing.T) { + s := schematest.New("object", + schematest.WithDescription("root"), + schematest.WithProperty("name", schematest.New("string")), + schematest.WithProperty("children", + schematest.New("array", schematest.WithItemsRef("#")), + ), + ) + + err := s.Parse(&dynamic.Config{Data: s}, &dynamictest.Reader{}) + + require.NoError(t, err) + children := s.Properties.Get("children") + require.Equal(t, s.Description, children.Items.Description) + }, + }, + { + name: "recursion using $def", + test: func(t *testing.T) { + data := ` +$defs: + a: + $ref: #/$defs/b + b: + $ref: '#' +$ref: '#/$defs/a' +` + var s *schema.Schema + err := yaml.Unmarshal([]byte(data), &s) + require.NoError(t, err) + + err = s.Parse(&dynamic.Config{Data: s}, &dynamictest.Reader{}) + require.NoError(t, err) + require.Equal(t, "empty schema", s.String()) + }, + }, + { + name: "self-recursion", + test: func(t *testing.T) { + data := ` +$defs: + a: + properties: + part: + $ref: '#/$defs/a' +$ref: '#/$defs/a' +` + var s *schema.Schema + err := yaml.Unmarshal([]byte(data), &s) + require.NoError(t, err) + + err = s.Parse(&dynamic.Config{Data: s}, &dynamictest.Reader{}) + require.NoError(t, err) + require.Equal(t, "schema properties=[part]", s.String()) + }, + }, + { + name: "parsing twice with $refs", + test: func(t *testing.T) { + reader := &dynamictest.Reader{ + Data: map[string]*dynamic.Config{ + "https://example.com/schemas/foo": { + Info: dynamictest.NewConfigInfo(dynamictest.WithUrl("https://example.com/schemas/foo")), + Raw: []byte(`{ +"$defs": { "items": { "type": "integer" } }, +"type": "array", +"items": { "$ref": "#/$defs/items" }, +}`), + }, + }, + } + + person := &dynamic.Config{ + Info: dynamictest.NewConfigInfo(dynamictest.WithUrl("https://example.com/schemas/bar")), + Data: &schema.Schema{ + Ref: "https://example.com/schemas/foo", + }, + } + + // 1. parsing + err := person.Data.(*schema.Schema).Parse(person, reader) + require.NoError(t, err) + + // 2. parsing + err = person.Data.(*schema.Schema).Parse(person, reader) + + require.NoError(t, err) + require.Equal(t, "integer", person.Data.(*schema.Schema).Items.Type.String()) + }, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + tc.test(t) + }) + } +} diff --git a/schema/json/schema/schema.go b/schema/json/schema/schema.go index c4767d069..d073d0c8e 100644 --- a/schema/json/schema/schema.go +++ b/schema/json/schema/schema.go @@ -254,33 +254,45 @@ func (s *Schema) Parse(config *dynamic.Config, reader dynamic.Reader) error { return err } - if err := s.Items.Parse(config, reader); err != nil { - return err + if !s.skipParse("items") { + if err := s.Items.Parse(config, reader); err != nil { + return err + } } - if err := s.Properties.parse(config, reader); err != nil { - return err + if !s.skipParse("properties") { + if err := s.Properties.parse(config, reader); err != nil { + return err + } } - if err := s.AdditionalProperties.Parse(config, reader); err != nil { - return err + if !s.skipParse("additionalProperties") { + if err := s.AdditionalProperties.Parse(config, reader); err != nil { + return err + } } - for _, r := range s.AnyOf { - if err := r.Parse(config, reader); err != nil { - return err + if !s.skipParse("anyOf") { + for _, r := range s.AnyOf { + if err := r.Parse(config, reader); err != nil { + return err + } } } - for _, r := range s.AllOf { - if err := r.Parse(config, reader); err != nil { - return err + if !s.skipParse("allOf") { + for _, r := range s.AllOf { + if err := r.Parse(config, reader); err != nil { + return err + } } } - for _, r := range s.OneOf { - if err := r.Parse(config, reader); err != nil { - return err + if !s.skipParse("oneOf") { + for _, r := range s.OneOf { + if err := r.Parse(config, reader); err != nil { + return err + } } } @@ -290,6 +302,12 @@ func (s *Schema) Parse(config *dynamic.Config, reader dynamic.Reader) error { if err != nil { return err } + + // Apply the resolved schema as an overlay onto the current schema. + // The referenced schema is cloned to preserve the immutability of + // 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) } @@ -304,3 +322,11 @@ func (s *Schema) Parse(config *dynamic.Config, reader dynamic.Reader) error { return nil } + +func (s *Schema) skipParse(name string) bool { + if s.Ref != "" { + _, ok := s.m[name] + return !ok + } + return false +} diff --git a/schema/json/schema/schematest/schema.go b/schema/json/schema/schematest/schema.go index 3062296c2..21a88fa2b 100644 --- a/schema/json/schema/schematest/schema.go +++ b/schema/json/schema/schematest/schema.go @@ -30,6 +30,12 @@ func NewBool(b bool) *schema.Schema { return s } +func WithDescription(desc string) SchemaOptions { + return func(s *schema.Schema) { + s.Description = desc + } +} + func WithProperty(name string, ps *schema.Schema) SchemaOptions { return func(s *schema.Schema) { if s.Properties == nil { @@ -63,6 +69,12 @@ func WithItems(typeName string, opts ...SchemaOptions) SchemaOptions { } } +func WithItemsRef(ref string) SchemaOptions { + return func(s *schema.Schema) { + s.Items = &schema.Schema{Ref: ref} + } +} + func WithItemsNew(items *schema.Schema) SchemaOptions { return func(s *schema.Schema) { s.Items = items diff --git a/server/httpmanager_test.go b/server/httpmanager_test.go index eb27c84bb..baf1a744d 100644 --- a/server/httpmanager_test.go +++ b/server/httpmanager_test.go @@ -4,6 +4,7 @@ import ( "fmt" "io" "mokapi/config/dynamic" + "mokapi/config/dynamic/dynamictest" "mokapi/config/static" "mokapi/engine" "mokapi/providers/openapi" @@ -28,7 +29,7 @@ func TestHttpServers_Monitor(t *testing.T) { store, err := cert.NewStore(cfg) require.NoError(t, err) - app := runtime.New(cfg) + app := runtime.New(cfg, &dynamictest.Reader{}) m := NewHttpManager(&engine.Engine{}, store, app) defer m.Stop() @@ -172,7 +173,7 @@ func TestHttpManager_Update(t *testing.T) { require.NoError(t, err) cfg := &static.Config{} - m := NewHttpManager(&engine.Engine{}, store, runtime.New(cfg)) + m := NewHttpManager(&engine.Engine{}, store, runtime.New(cfg, &dynamictest.Reader{})) defer m.Stop() data.test(t, m, hook) diff --git a/server/server_http_test.go b/server/server_http_test.go index 119de3717..15c480b52 100644 --- a/server/server_http_test.go +++ b/server/server_http_test.go @@ -269,7 +269,7 @@ func TestHttp(t *testing.T) { require.NoError(t, err) cfg := &static.Config{} - m := server.NewHttpManager(enginetest.NewEngine(), certStore, runtime.New(cfg)) + m := server.NewHttpManager(enginetest.NewEngine(), certStore, runtime.New(cfg, &dynamictest.Reader{})) defer m.Stop() tc.test(t, m) diff --git a/server/server_kafka_test.go b/server/server_kafka_test.go index c36648555..329e8b362 100644 --- a/server/server_kafka_test.go +++ b/server/server_kafka_test.go @@ -3,6 +3,7 @@ package server import ( "fmt" "mokapi/config/dynamic" + "mokapi/config/dynamic/dynamictest" "mokapi/config/static" "mokapi/kafka/kafkatest" "mokapi/kafka/metaData" @@ -33,7 +34,7 @@ func TestKafkaServer(t *testing.T) { ) cfg := &static.Config{} - m := NewKafkaManager(nil, runtime.New(cfg)) + m := NewKafkaManager(nil, runtime.New(cfg, &dynamictest.Reader{})) defer m.Stop() m.UpdateConfig(dynamic.ConfigEvent{Config: &dynamic.Config{Info: dynamic.ConfigInfo{Url: MustParseUrl("foo.yml")}, Data: c}}) @@ -351,7 +352,7 @@ func TestKafkaServer_Update(t *testing.T) { tc := tc t.Run(tc.name, func(t *testing.T) { cfg := &static.Config{} - m := NewKafkaManager(nil, runtime.New(cfg)) + m := NewKafkaManager(nil, runtime.New(cfg, &dynamictest.Reader{})) defer m.Stop() tc.fn(t, m) diff --git a/server/server_ldap_test.go b/server/server_ldap_test.go index 7d363c9bd..01fdc070c 100644 --- a/server/server_ldap_test.go +++ b/server/server_ldap_test.go @@ -3,6 +3,7 @@ package server import ( "fmt" "mokapi/config/dynamic" + "mokapi/config/dynamic/dynamictest" "mokapi/config/static" "mokapi/engine/enginetest" "mokapi/ldap" @@ -117,7 +118,7 @@ func TestLdapDirectory(t *testing.T) { tc := tc t.Run(tc.name, func(t *testing.T) { cfg := &static.Config{} - app := runtime.New(cfg) + app := runtime.New(cfg, &dynamictest.Reader{}) m := NewLdapDirectoryManager(enginetest.NewEngine(), nil, app) defer m.Stop() diff --git a/server/server_mail_test.go b/server/server_mail_test.go index 11c0aff04..c5830b6aa 100644 --- a/server/server_mail_test.go +++ b/server/server_mail_test.go @@ -205,7 +205,7 @@ func TestSmtp(t *testing.T) { require.NoError(t, err) cfg := &static.Config{} - m := server.NewMailManager(runtime.New(cfg), enginetest.NewEngine(), certStore) + m := server.NewMailManager(runtime.New(cfg, &dynamictest.Reader{}), enginetest.NewEngine(), certStore) defer m.Stop() tc.test(t, m) diff --git a/server/server_mqtt_test.go b/server/server_mqtt_test.go index d721002be..9c4a8a218 100644 --- a/server/server_mqtt_test.go +++ b/server/server_mqtt_test.go @@ -2,8 +2,8 @@ package server import ( "fmt" - "github.com/stretchr/testify/require" "mokapi/config/dynamic" + "mokapi/config/dynamic/dynamictest" "mokapi/config/static" "mokapi/providers/asyncapi3/asyncapi3test" "mokapi/runtime" @@ -11,6 +11,8 @@ import ( "mokapi/try" "testing" "time" + + "github.com/stretchr/testify/require" ) func TestMqttServer(t *testing.T) { @@ -29,7 +31,7 @@ func TestMqttServer(t *testing.T) { ) cfg := &static.Config{} - m := NewMqttManager(nil, runtime.New(cfg)) + m := NewMqttManager(nil, runtime.New(cfg, &dynamictest.Reader{})) defer m.Stop() m.UpdateConfig(dynamic.ConfigEvent{Config: &dynamic.Config{Info: dynamic.ConfigInfo{Url: MustParseUrl("foo.yml")}, Data: c}}) diff --git a/server/server_test.go b/server/server_test.go index d4b289f2f..1642d5bc4 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -2,6 +2,7 @@ package server_test import ( "context" + "mokapi/config/dynamic/dynamictest" "mokapi/config/static" "mokapi/engine" "mokapi/runtime" @@ -19,7 +20,7 @@ func TestServer(t *testing.T) { pool := safe.NewPool(context.Background()) cfg := &static.Config{} - app := runtime.New(cfg) + app := runtime.New(cfg, &dynamictest.Reader{}) watcher := server.NewConfigWatcher(cfg) kafka := &server.KafkaManager{} http := &server.HttpManager{} diff --git a/webui/package-lock.json b/webui/package-lock.json index 48f2f2d30..ed1f7302c 100644 --- a/webui/package-lock.json +++ b/webui/package-lock.json @@ -18,20 +18,20 @@ "bootstrap": "^5.3.8", "bootstrap-icons": "^1.13.1", "cors": "^2.8.6", - "dayjs": "^1.11.19", + "dayjs": "^1.11.20", "del-cli": "^7.0.0", "express": "^5.2.1", "fuse.js": "^7.1.0", "http-status-codes": "^2.3.0", "js-yaml": "^4.1.1", "kafkajs": "^2.2.4", - "ldapts": "^8.1.6", + "ldapts": "^8.1.7", "markdown-it-container": "^4.0.0", "mime-types": "^3.0.2", "ncp": "^2.0.0", - "nodemailer": "^8.0.1", - "vue": "^3.5.28", - "vue-router": "^5.0.3", + "nodemailer": "^8.0.2", + "vue": "^3.5.30", + "vue-router": "^5.0.4", "vue3-ace-editor": "^2.2.4", "vue3-highlightjs": "^1.0.5", "vue3-markdown-it": "^1.0.10", @@ -43,18 +43,18 @@ "@rushstack/eslint-patch": "^1.16.1", "@types/js-yaml": "^4.0.9", "@types/markdown-it-container": "^4.0.0", - "@types/node": "^25.3.2", + "@types/node": "^25.5.0", "@vitejs/plugin-vue": "^6.0.4", "@vue/eslint-config-prettier": "^10.2.0", "@vue/eslint-config-typescript": "^14.7.0", "@vue/tsconfig": "^0.9.0", - "eslint": "^10.0.2", + "eslint": "^10.0.3", "eslint-plugin-vue": "^10.8.0", "npm-run-all": "^4.1.5", "prettier": "^3.8.1", "typescript": "~5.9.3", - "vite": "^7.3.1", - "vue-tsc": "^3.2.5", + "vite": "^8.0.1", + "vue-tsc": "^3.2.6", "xml2js": "^0.6.2" } }, @@ -120,446 +120,38 @@ "node": ">=6.9.0" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", - "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", - "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", - "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", - "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", - "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", - "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", - "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", - "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", - "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", - "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", - "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", - "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", - "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", - "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", - "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", - "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", - "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", - "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", - "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", - "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", - "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", - "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", - "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", - "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", - "cpu": [ - "arm64" - ], + "node_modules/@emnapi/core": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz", + "integrity": "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==", "dev": true, "license": "MIT", "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" + "dependencies": { + "@emnapi/wasi-threads": "1.2.0", + "tslib": "^2.4.0" } }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", - "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", - "cpu": [ - "ia32" - ], + "node_modules/@emnapi/runtime": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz", + "integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==", "dev": true, "license": "MIT", "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" + "dependencies": { + "tslib": "^2.4.0" } }, - "node_modules/@esbuild/win32-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", - "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", - "cpu": [ - "x64" - ], + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz", + "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==", "dev": true, "license": "MIT", "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" + "dependencies": { + "tslib": "^2.4.0" } }, "node_modules/@eslint-community/eslint-utils": { @@ -592,15 +184,15 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.2.tgz", - "integrity": "sha512-YF+fE6LV4v5MGWRGj7G404/OZzGNepVF8fxk7jqmqo3lrza7a0uUcDnROGRBG1WFC1omYUS/Wp1f42i0M+3Q3A==", + "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==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^3.0.2", + "@eslint/object-schema": "^3.0.3", "debug": "^4.3.1", - "minimatch": "^10.2.1" + "minimatch": "^10.2.4" }, "engines": { "node": "^20.19.0 || ^22.13.0 || >=24" @@ -659,9 +251,9 @@ } }, "node_modules/@eslint/core": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.1.0.tgz", - "integrity": "sha512-/nr9K9wkr3P1EzFTdFdMoLuo1PmIxjmwvPozwoSodjNBdefGujXQUF93u1DDZpEaTuDvMsIQddsd35BwtrW9Xw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.1.1.tgz", + "integrity": "sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -672,9 +264,9 @@ } }, "node_modules/@eslint/object-schema": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.2.tgz", - "integrity": "sha512-HOy56KJt48Bx8KmJ+XGQNSUMT/6dZee/M54XyUyuvTvPXJmsERRvBchsUVx1UMe1WwIH49XLAczNC7V2INsuUw==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.3.tgz", + "integrity": "sha512-iM869Pugn9Nsxbh/YHRqYiqd23AmIbxJOcpUMOuWCVNdoQJ5ZtwL6h3t0bcZzJUlC3Dq9jCFCESBZnX0GTv7iQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -682,13 +274,13 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.6.0.tgz", - "integrity": "sha512-bIZEUzOI1jkhviX2cp5vNyXQc6olzb2ohewQubuYlMXZ2Q/XjBO0x0XhGPvc9fjSIiUN0vw+0hq53BJ4eQSJKQ==", + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.6.1.tgz", + "integrity": "sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^1.1.0", + "@eslint/core": "^1.1.1", "levn": "^0.4.1" }, "engines": { @@ -792,6 +384,23 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", + "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -827,6 +436,16 @@ "node": ">= 8" } }, + "node_modules/@oxc-project/types": { + "version": "0.120.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.120.0.tgz", + "integrity": "sha512-k1YNu55DuvAip/MGE1FTsIuU3FUCn6v/ujG9V7Nq5Df/kX2CWb13hhwD0lmJGMGqE+bE1MXvv9SZVnMzEXlWcg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, "node_modules/@pkgr/core": { "version": "0.2.9", "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", @@ -866,31 +485,10 @@ "url": "https://opencollective.com/popperjs" } }, - "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==", - "dev": true, - "license": "MIT" - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", - "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", - "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.10.tgz", + "integrity": "sha512-jOHxwXhxmFKuXztiu1ORieJeTbx5vrTkcOkkkn2d35726+iwhrY1w/+nYY/AGgF12thg33qC3R1LMBF5tHTZHg==", "cpu": [ "arm64" ], @@ -899,12 +497,15 @@ "optional": true, "os": [ "android" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", - "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.10.tgz", + "integrity": "sha512-gED05Teg/vtTZbIJBc4VNMAxAFDUPkuO/rAIyyxZjTj1a1/s6z5TII/5yMGZ0uLRCifEtwUQn8OlYzuYc0m70w==", "cpu": [ "arm64" ], @@ -913,12 +514,15 @@ "optional": true, "os": [ "darwin" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", - "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.10.tgz", + "integrity": "sha512-rI15NcM1mA48lqrIxVkHfAqcyFLcQwyXWThy+BQ5+mkKKPvSO26ir+ZDp36AgYoYVkqvMcdS8zOE6SeBsR9e8A==", "cpu": [ "x64" ], @@ -927,26 +531,15 @@ "optional": true, "os": [ "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", - "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", - "cpu": [ - "arm64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", - "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.10.tgz", + "integrity": "sha512-XZRXHdTa+4ME1MuDVp021+doQ+z6Ei4CCFmNc5/sKbqb8YmkiJdj8QKlV3rCI0AJtAeSB5n0WGPuJWNL9p/L2w==", "cpu": [ "x64" ], @@ -955,26 +548,15 @@ "optional": true, "os": [ "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", - "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", - "cpu": [ - "arm" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", - "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.10.tgz", + "integrity": "sha512-R0SQMRluISSLzFE20sPWYHVmJdDQnRyc/FzSCN72BqQmh2SOZUFG+N3/vBZpR4C6WpEUVYJLrYUXaj43sJsNLA==", "cpu": [ "arm" ], @@ -983,26 +565,15 @@ "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", - "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", - "cpu": [ - "arm64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", - "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.10.tgz", + "integrity": "sha512-Y1reMrV/o+cwpduYhJuOE3OMKx32RMYCidf14y+HssARRmhDuWXJ4yVguDg2R/8SyyGNo+auzz64LnPK9Hq6jg==", "cpu": [ "arm64" ], @@ -1011,54 +582,32 @@ "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", - "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", - "cpu": [ - "loong64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", - "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.10.tgz", + "integrity": "sha512-vELN+HNb2IzuzSBUOD4NHmP9yrGwl1DVM29wlQvx1OLSclL0NgVWnVDKl/8tEks79EFek/kebQKnNJkIAA4W2g==", "cpu": [ - "loong64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", - "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", - "cpu": [ - "ppc64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", - "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.10.tgz", + "integrity": "sha512-ZqrufYTgzxbHwpqOjzSsb0UV/aV2TFIY5rP8HdsiPTv/CuAgCRjM6s9cYFwQ4CNH+hf9Y4erHW1GjZuZ7WoI7w==", "cpu": [ "ppc64" ], @@ -1067,40 +616,15 @@ "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", - "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", - "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", - "cpu": [ - "riscv64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", - "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.10.tgz", + "integrity": "sha512-gSlmVS1FZJSRicA6IyjoRoKAFK7IIHBs7xJuHRSmjImqk3mPPWbR7RhbnfH2G6bcmMEllCt2vQ/7u9e6bBnByg==", "cpu": [ "s390x" ], @@ -1109,12 +633,15 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", - "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.10.tgz", + "integrity": "sha512-eOCKUpluKgfObT2pHjztnaWEIbUabWzk3qPZ5PuacuPmr4+JtQG4k2vGTY0H15edaTnicgU428XW/IH6AimcQw==", "cpu": [ "x64" ], @@ -1123,12 +650,15 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", - "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.10.tgz", + "integrity": "sha512-Xdf2jQbfQowJnLcgYfD/m0Uu0Qj5OdxKallD78/IPPfzaiaI4KRAwZzHcKQ4ig1gtg1SuzC7jovNiM2TzQsBXA==", "cpu": [ "x64" ], @@ -1137,26 +667,15 @@ "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", - "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", - "cpu": [ - "x64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", - "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.10.tgz", + "integrity": "sha512-o1hYe8hLi1EY6jgPFyxQgQ1wcycX+qz8eEbVmot2hFkgUzPxy9+kF0u0NIQBeDq+Mko47AkaFFaChcvZa9UX9Q==", "cpu": [ "arm64" ], @@ -1165,40 +684,49 @@ "optional": true, "os": [ "openharmony" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", - "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.10.tgz", + "integrity": "sha512-Ugv9o7qYJudqQO5Y5y2N2SOo6S4WiqiNOpuQyoPInnhVzCY+wi/GHltcLHypG9DEUYMB0iTB/huJrpadiAcNcA==", "cpu": [ - "arm64" + "wasm32" ], "dev": true, "license": "MIT", "optional": true, - "os": [ - "win32" - ] + "dependencies": { + "@napi-rs/wasm-runtime": "^1.1.1" + }, + "engines": { + "node": ">=14.0.0" + } }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", - "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.10.tgz", + "integrity": "sha512-7UODQb4fQUNT/vmgDZBl3XOBAIOutP5R3O/rkxg0aLfEGQ4opbCgU5vOw/scPe4xOqBwL9fw7/RP1vAMZ6QlAQ==", "cpu": [ - "ia32" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "win32" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", - "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.10.tgz", + "integrity": "sha512-PYxKHMVHOb5NJuDL53vBUl1VwUjymDcYI6rzpIni0C9+9mTiJedvUxSk7/RPp7OOAm3v+EjgMu9bIy3N6b408w==", "cpu": [ "x64" ], @@ -1207,21 +735,17 @@ "optional": true, "os": [ "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", - "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", - "cpu": [ - "x64" ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "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==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "license": "MIT" }, "node_modules/@rushstack/eslint-patch": { "version": "1.16.1", @@ -1259,6 +783,17 @@ "@ssthouse/tree-chart-core": "^1.2.0" } }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@types/bootstrap": { "version": "5.2.10", "resolved": "https://registry.npmjs.org/@types/bootstrap/-/bootstrap-5.2.10.tgz", @@ -1335,9 +870,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "25.3.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.2.tgz", - "integrity": "sha512-RpV6r/ij22zRRdyBPcxDeKAzH43phWVKEjL2iksqo1Vz3CuBUrgmPpPhALKiRfU7OMCmeeO9vECBMsV0hMTG8Q==", + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", + "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", "license": "MIT", "dependencies": { "undici-types": "~7.18.0" @@ -1675,53 +1210,53 @@ } }, "node_modules/@vue/compiler-core": { - "version": "3.5.28", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.28.tgz", - "integrity": "sha512-kviccYxTgoE8n6OCw96BNdYlBg2GOWfBuOW4Vqwrt7mSKWKwFVvI8egdTltqRgITGPsTFYtKYfxIG8ptX2PJHQ==", + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.30.tgz", + "integrity": "sha512-s3DfdZkcu/qExZ+td75015ljzHc6vE+30cFMGRPROYjqkroYI5NV2X1yAMX9UeyBNWB9MxCfPcsjpLS11nzkkw==", "license": "MIT", "dependencies": { "@babel/parser": "^7.29.0", - "@vue/shared": "3.5.28", + "@vue/shared": "3.5.30", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "node_modules/@vue/compiler-dom": { - "version": "3.5.28", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.28.tgz", - "integrity": "sha512-/1ZepxAb159jKR1btkefDP+J2xuWL5V3WtleRmxaT+K2Aqiek/Ab/+Ebrw2pPj0sdHO8ViAyyJWfhXXOP/+LQA==", + "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==", "license": "MIT", "dependencies": { - "@vue/compiler-core": "3.5.28", - "@vue/shared": "3.5.28" + "@vue/compiler-core": "3.5.30", + "@vue/shared": "3.5.30" } }, "node_modules/@vue/compiler-sfc": { - "version": "3.5.28", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.28.tgz", - "integrity": "sha512-6TnKMiNkd6u6VeVDhZn/07KhEZuBSn43Wd2No5zaP5s3xm8IqFTHBj84HJah4UepSUJTro5SoqqlOY22FKY96g==", + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.30.tgz", + "integrity": "sha512-LqmFPDn89dtU9vI3wHJnwaV6GfTRD87AjWpTWpyrdVOObVtjIuSeZr181z5C4PmVx/V3j2p+0f7edFKGRMpQ5A==", "license": "MIT", "dependencies": { "@babel/parser": "^7.29.0", - "@vue/compiler-core": "3.5.28", - "@vue/compiler-dom": "3.5.28", - "@vue/compiler-ssr": "3.5.28", - "@vue/shared": "3.5.28", + "@vue/compiler-core": "3.5.30", + "@vue/compiler-dom": "3.5.30", + "@vue/compiler-ssr": "3.5.30", + "@vue/shared": "3.5.30", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", - "postcss": "^8.5.6", + "postcss": "^8.5.8", "source-map-js": "^1.2.1" } }, "node_modules/@vue/compiler-ssr": { - "version": "3.5.28", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.28.tgz", - "integrity": "sha512-JCq//9w1qmC6UGLWJX7RXzrGpKkroubey/ZFqTpvEIDJEKGgntuDMqkuWiZvzTzTA5h2qZvFBFHY7fAAa9475g==", + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.30.tgz", + "integrity": "sha512-NsYK6OMTnx109PSL2IAyf62JP6EUdk4Dmj6AkWcJGBvN0dQoMYtVekAmdqgTtWQgEJo+Okstbf/1p7qZr5H+bA==", "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.28", - "@vue/shared": "3.5.28" + "@vue/compiler-dom": "3.5.30", + "@vue/shared": "3.5.30" } }, "node_modules/@vue/devtools-api": { @@ -1799,9 +1334,9 @@ } }, "node_modules/@vue/language-core": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.2.5.tgz", - "integrity": "sha512-d3OIxN/+KRedeM5wQ6H6NIpwS3P5gC9nmyaHgBk+rO6dIsjY+tOh4UlPpiZbAh3YtLdCGEX4M16RmsBqPmJV+g==", + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.2.6.tgz", + "integrity": "sha512-xYYYX3/aVup576tP/23sEUpgiEnujrENaoNRbaozC1/MA9I6EGFQRJb4xrt/MmUCAGlxTKL2RmT8JLTPqagCkg==", "dev": true, "license": "MIT", "dependencies": { @@ -1828,53 +1363,53 @@ } }, "node_modules/@vue/reactivity": { - "version": "3.5.28", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.28.tgz", - "integrity": "sha512-gr5hEsxvn+RNyu9/9o1WtdYdwDjg5FgjUSBEkZWqgTKlo/fvwZ2+8W6AfKsc9YN2k/+iHYdS9vZYAhpi10kNaw==", + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.30.tgz", + "integrity": "sha512-179YNgKATuwj9gB+66snskRDOitDiuOZqkYia7mHKJaidOMo/WJxHKF8DuGc4V4XbYTJANlfEKb0yxTQotnx4Q==", "license": "MIT", "dependencies": { - "@vue/shared": "3.5.28" + "@vue/shared": "3.5.30" } }, "node_modules/@vue/runtime-core": { - "version": "3.5.28", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.28.tgz", - "integrity": "sha512-POVHTdbgnrBBIpnbYU4y7pOMNlPn2QVxVzkvEA2pEgvzbelQq4ZOUxbp2oiyo+BOtiYlm8Q44wShHJoBvDPAjQ==", + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.30.tgz", + "integrity": "sha512-e0Z+8PQsUTdwV8TtEsLzUM7SzC7lQwYKePydb7K2ZnmS6jjND+WJXkmmfh/swYzRyfP1EY3fpdesyYoymCzYfg==", "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.28", - "@vue/shared": "3.5.28" + "@vue/reactivity": "3.5.30", + "@vue/shared": "3.5.30" } }, "node_modules/@vue/runtime-dom": { - "version": "3.5.28", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.28.tgz", - "integrity": "sha512-4SXxSF8SXYMuhAIkT+eBRqOkWEfPu6nhccrzrkioA6l0boiq7sp18HCOov9qWJA5HML61kW8p/cB4MmBiG9dSA==", + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.30.tgz", + "integrity": "sha512-2UIGakjU4WSQ0T4iwDEW0W7vQj6n7AFn7taqZ9Cvm0Q/RA2FFOziLESrDL4GmtI1wV3jXg5nMoJSYO66egDUBw==", "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.28", - "@vue/runtime-core": "3.5.28", - "@vue/shared": "3.5.28", + "@vue/reactivity": "3.5.30", + "@vue/runtime-core": "3.5.30", + "@vue/shared": "3.5.30", "csstype": "^3.2.3" } }, "node_modules/@vue/server-renderer": { - "version": "3.5.28", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.28.tgz", - "integrity": "sha512-pf+5ECKGj8fX95bNincbzJ6yp6nyzuLDhYZCeFxUNp8EBrQpPpQaLX3nNCp49+UbgbPun3CeVE+5CXVV1Xydfg==", + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.30.tgz", + "integrity": "sha512-v+R34icapydRwbZRD0sXwtHqrQJv38JuMB4JxbOxd8NEpGLny7cncMp53W9UH/zo4j8eDHjQ1dEJXwzFQknjtQ==", "license": "MIT", "dependencies": { - "@vue/compiler-ssr": "3.5.28", - "@vue/shared": "3.5.28" + "@vue/compiler-ssr": "3.5.30", + "@vue/shared": "3.5.30" }, "peerDependencies": { - "vue": "3.5.28" + "vue": "3.5.30" } }, "node_modules/@vue/shared": { - "version": "3.5.28", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.28.tgz", - "integrity": "sha512-cfWa1fCGBxrvaHRhvV3Is0MgmrbSCxYTXCSCau2I0a1Xw1N1pHAvkWCiXPRAqjvToILvguNyEwjevUqAuBQWvQ==", + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.30.tgz", + "integrity": "sha512-YXgQ7JjaO18NeK2K9VTbDHaFy62WrObMa6XERNfNOkAhD1F1oDSf3ZJ7K6GqabZ0BvSDHajp8qfS5Sa2I9n8uQ==", "license": "MIT" }, "node_modules/@vue/tsconfig": { @@ -2840,9 +2375,9 @@ } }, "node_modules/dayjs": { - "version": "1.11.19", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", - "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", + "version": "1.11.20", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz", + "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==", "license": "MIT" }, "node_modules/debug": { @@ -2965,6 +2500,16 @@ "node": ">= 0.8" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -3149,48 +2694,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/esbuild": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", - "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.2", - "@esbuild/android-arm": "0.27.2", - "@esbuild/android-arm64": "0.27.2", - "@esbuild/android-x64": "0.27.2", - "@esbuild/darwin-arm64": "0.27.2", - "@esbuild/darwin-x64": "0.27.2", - "@esbuild/freebsd-arm64": "0.27.2", - "@esbuild/freebsd-x64": "0.27.2", - "@esbuild/linux-arm": "0.27.2", - "@esbuild/linux-arm64": "0.27.2", - "@esbuild/linux-ia32": "0.27.2", - "@esbuild/linux-loong64": "0.27.2", - "@esbuild/linux-mips64el": "0.27.2", - "@esbuild/linux-ppc64": "0.27.2", - "@esbuild/linux-riscv64": "0.27.2", - "@esbuild/linux-s390x": "0.27.2", - "@esbuild/linux-x64": "0.27.2", - "@esbuild/netbsd-arm64": "0.27.2", - "@esbuild/netbsd-x64": "0.27.2", - "@esbuild/openbsd-arm64": "0.27.2", - "@esbuild/openbsd-x64": "0.27.2", - "@esbuild/openharmony-arm64": "0.27.2", - "@esbuild/sunos-x64": "0.27.2", - "@esbuild/win32-arm64": "0.27.2", - "@esbuild/win32-ia32": "0.27.2", - "@esbuild/win32-x64": "0.27.2" - } - }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -3211,18 +2714,18 @@ } }, "node_modules/eslint": { - "version": "10.0.2", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.0.2.tgz", - "integrity": "sha512-uYixubwmqJZH+KLVYIVKY1JQt7tysXhtj21WSvjcSmU5SVNzMus1bgLe+pAt816yQ8opKfheVVoPLqvVMGejYw==", + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.0.3.tgz", + "integrity": "sha512-COV33RzXZkqhG9P2rZCFl9ZmJ7WL+gQSCRzE7RhkbclbQPtLAWReL7ysA0Sh4c8Im2U9ynybdR56PV0XcKvqaQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", - "@eslint/config-array": "^0.23.2", + "@eslint/config-array": "^0.23.3", "@eslint/config-helpers": "^0.5.2", - "@eslint/core": "^1.1.0", - "@eslint/plugin-kit": "^0.6.0", + "@eslint/core": "^1.1.1", + "@eslint/plugin-kit": "^0.6.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", @@ -3231,7 +2734,7 @@ "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^9.1.1", + "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.1.1", "esquery": "^1.7.0", @@ -3244,7 +2747,7 @@ "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", - "minimatch": "^10.2.1", + "minimatch": "^10.2.4", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, @@ -3346,9 +2849,9 @@ } }, "node_modules/eslint-scope": { - "version": "9.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.1.tgz", - "integrity": "sha512-GaUN0sWim5qc8KVErfPBWmc31LEsOkrUJbvJZV+xuL3u2phMUK4HIvXlWAakfC8W4nzlK+chPEAkYOYb5ZScIw==", + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -4697,9 +4200,9 @@ } }, "node_modules/ldapts": { - "version": "8.1.6", - "resolved": "https://registry.npmjs.org/ldapts/-/ldapts-8.1.6.tgz", - "integrity": "sha512-sofxzGEPRBvubSrdmly0mmUwjXHPfTbO51KLAUzuO4sHWwy+r0G6FwaLWWDwTPRpjJFkMdLId5BeRUHksUH4yA==", + "version": "8.1.7", + "resolved": "https://registry.npmjs.org/ldapts/-/ldapts-8.1.7.tgz", + "integrity": "sha512-TJl6T92eIwMf/OJ0hDfKVa6ISwzo+lqCWCI5Mf//ARlKa3LKQZaSrme/H2rCRBhW0DZCQlrsV+fgoW5YHRNLUw==", "license": "MIT", "dependencies": { "strict-event-emitter-types": "2.0.0" @@ -4722,6 +4225,267 @@ "node": ">= 0.8.0" } }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/linkify-it": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz", @@ -5148,9 +4912,9 @@ "license": "MIT" }, "node_modules/nodemailer": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.1.tgz", - "integrity": "sha512-5kcldIXmaEjZcHR6F28IKGSgpmZHaF1IXLWFTG+Xh3S+Cce4MiakLtWY+PlBU69fLbRa8HlaGIrC/QolUpHkhg==", + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.2.tgz", + "integrity": "sha512-zbj002pZAIkWQFxyAaqoxvn+zoIwRnS40hgjqTXudKOOJkiFFgBeNqjgD3/YCR12sZnrghWYBY+yP1ZucdDRpw==", "license": "MIT-0", "engines": { "node": ">=6.0.0" @@ -5732,9 +5496,9 @@ } }, "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", "funding": [ { "type": "opencollective", @@ -6072,50 +5836,46 @@ "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", "license": "Unlicense" }, - "node_modules/rollup": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", - "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "node_modules/rolldown": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.10.tgz", + "integrity": "sha512-q7j6vvarRFmKpgJUT8HCAUljkgzEp4LAhPlJUvQhA5LA1SUL36s5QCysMutErzL3EbNOZOkoziSx9iZC4FddKA==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.8" + "@oxc-project/types": "=0.120.0", + "@rolldown/pluginutils": "1.0.0-rc.10" }, "bin": { - "rollup": "dist/bin/rollup" + "rolldown": "bin/cli.mjs" }, "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" + "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.59.0", - "@rollup/rollup-android-arm64": "4.59.0", - "@rollup/rollup-darwin-arm64": "4.59.0", - "@rollup/rollup-darwin-x64": "4.59.0", - "@rollup/rollup-freebsd-arm64": "4.59.0", - "@rollup/rollup-freebsd-x64": "4.59.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", - "@rollup/rollup-linux-arm-musleabihf": "4.59.0", - "@rollup/rollup-linux-arm64-gnu": "4.59.0", - "@rollup/rollup-linux-arm64-musl": "4.59.0", - "@rollup/rollup-linux-loong64-gnu": "4.59.0", - "@rollup/rollup-linux-loong64-musl": "4.59.0", - "@rollup/rollup-linux-ppc64-gnu": "4.59.0", - "@rollup/rollup-linux-ppc64-musl": "4.59.0", - "@rollup/rollup-linux-riscv64-gnu": "4.59.0", - "@rollup/rollup-linux-riscv64-musl": "4.59.0", - "@rollup/rollup-linux-s390x-gnu": "4.59.0", - "@rollup/rollup-linux-x64-gnu": "4.59.0", - "@rollup/rollup-linux-x64-musl": "4.59.0", - "@rollup/rollup-openbsd-x64": "4.59.0", - "@rollup/rollup-openharmony-arm64": "4.59.0", - "@rollup/rollup-win32-arm64-msvc": "4.59.0", - "@rollup/rollup-win32-ia32-msvc": "4.59.0", - "@rollup/rollup-win32-x64-gnu": "4.59.0", - "@rollup/rollup-win32-x64-msvc": "4.59.0", - "fsevents": "~2.3.2" - } + "@rolldown/binding-android-arm64": "1.0.0-rc.10", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.10", + "@rolldown/binding-darwin-x64": "1.0.0-rc.10", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.10", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.10", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.10", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.10", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.10", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.10", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.10", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.10", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.10", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.10", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.10", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.10" + } + }, + "node_modules/rolldown/node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.10.tgz", + "integrity": "sha512-UkVDEFk1w3mveXeKgaTuYfKWtPbvgck1dT8TUG3bnccrH0XtLTuAyfCoks4Q/M5ZGToSVJTIQYCzy2g/atAOeg==", + "dev": true, + "license": "MIT" }, "node_modules/router": { "version": "2.2.0", @@ -6763,6 +6523,14 @@ "typescript": ">=4.8.4" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -7056,17 +6824,16 @@ } }, "node_modules/vite": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", - "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.1.tgz", + "integrity": "sha512-wt+Z2qIhfFt85uiyRt5LPU4oVEJBXj8hZNWKeqFG4gRG/0RaRGJ7njQCwzFVjO+v4+Ipmf5CY7VdmZRAYYBPHw==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.27.0", - "fdir": "^6.5.0", + "lightningcss": "^1.32.0", "picomatch": "^4.0.3", - "postcss": "^8.5.6", - "rollup": "^4.43.0", + "postcss": "^8.5.8", + "rolldown": "1.0.0-rc.10", "tinyglobby": "^0.2.15" }, "bin": { @@ -7083,9 +6850,10 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.0", + "esbuild": "^0.27.0", "jiti": ">=1.21.0", "less": "^4.0.0", - "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", @@ -7098,13 +6866,16 @@ "@types/node": { "optional": true }, - "jiti": { + "@vitejs/devtools": { "optional": true }, - "less": { + "esbuild": { + "optional": true + }, + "jiti": { "optional": true }, - "lightningcss": { + "less": { "optional": true }, "sass": { @@ -7130,24 +6901,6 @@ } } }, - "node_modules/vite/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, "node_modules/vite/node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -7184,16 +6937,16 @@ "license": "MIT" }, "node_modules/vue": { - "version": "3.5.28", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.28.tgz", - "integrity": "sha512-BRdrNfeoccSoIZeIhyPBfvWSLFP4q8J3u8Ju8Ug5vu3LdD+yTM13Sg4sKtljxozbnuMu1NB1X5HBHRYUzFocKg==", + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.30.tgz", + "integrity": "sha512-hTHLc6VNZyzzEH/l7PFGjpcTvUgiaPK5mdLkbjrTeWSRcEfxFrv56g/XckIYlE9ckuobsdwqd5mk2g1sBkMewg==", "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.28", - "@vue/compiler-sfc": "3.5.28", - "@vue/runtime-dom": "3.5.28", - "@vue/server-renderer": "3.5.28", - "@vue/shared": "3.5.28" + "@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" }, "peerDependencies": { "typescript": "*" @@ -7242,9 +6995,9 @@ } }, "node_modules/vue-router": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-5.0.3.tgz", - "integrity": "sha512-nG1c7aAFac7NYj8Hluo68WyWfc41xkEjaR0ViLHCa3oDvTQ/nIuLJlXJX1NUPw/DXzx/8+OKMng045HHQKQKWw==", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-5.0.4.tgz", + "integrity": "sha512-lCqDLCI2+fKVRl2OzXuzdSWmxXFLQRxQbmHugnRpTMyYiT+hNaycV0faqG5FBHDXoYrZ6MQcX87BvbY8mQ20Bg==", "license": "MIT", "dependencies": { "@babel/generator": "^7.28.6", @@ -7299,14 +7052,14 @@ } }, "node_modules/vue-tsc": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.2.5.tgz", - "integrity": "sha512-/htfTCMluQ+P2FISGAooul8kO4JMheOTCbCy4M6dYnYYjqLe3BExZudAua6MSIKSFYQtFOYAll7XobYwcpokGA==", + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.2.6.tgz", + "integrity": "sha512-gYW/kWI0XrwGzd0PKc7tVB/qpdeAkIZLNZb10/InizkQjHjnT8weZ/vBarZoj4kHKbUTZT/bAVgoOr8x4NsQ/Q==", "dev": true, "license": "MIT", "dependencies": { "@volar/typescript": "2.4.28", - "@vue/language-core": "3.2.5" + "@vue/language-core": "3.2.6" }, "bin": { "vue-tsc": "bin/vue-tsc.js" diff --git a/webui/package.json b/webui/package.json index 806c44b20..ce3cd6bbe 100644 --- a/webui/package.json +++ b/webui/package.json @@ -27,20 +27,20 @@ "bootstrap": "^5.3.8", "bootstrap-icons": "^1.13.1", "cors": "^2.8.6", - "dayjs": "^1.11.19", + "dayjs": "^1.11.20", "del-cli": "^7.0.0", "express": "^5.2.1", "fuse.js": "^7.1.0", "http-status-codes": "^2.3.0", "js-yaml": "^4.1.1", "kafkajs": "^2.2.4", - "ldapts": "^8.1.6", + "ldapts": "^8.1.7", "markdown-it-container": "^4.0.0", "mime-types": "^3.0.2", "ncp": "^2.0.0", - "nodemailer": "^8.0.1", - "vue": "^3.5.28", - "vue-router": "^5.0.3", + "nodemailer": "^8.0.2", + "vue": "^3.5.30", + "vue-router": "^5.0.4", "vue3-ace-editor": "^2.2.4", "vue3-highlightjs": "^1.0.5", "vue3-markdown-it": "^1.0.10", @@ -52,18 +52,18 @@ "@rushstack/eslint-patch": "^1.16.1", "@types/js-yaml": "^4.0.9", "@types/markdown-it-container": "^4.0.0", - "@types/node": "^25.3.2", + "@types/node": "^25.5.0", "@vitejs/plugin-vue": "^6.0.4", "@vue/eslint-config-prettier": "^10.2.0", "@vue/eslint-config-typescript": "^14.7.0", "@vue/tsconfig": "^0.9.0", - "eslint": "^10.0.2", + "eslint": "^10.0.3", "eslint-plugin-vue": "^10.8.0", "npm-run-all": "^4.1.5", "prettier": "^3.8.1", "typescript": "~5.9.3", - "vite": "^7.3.1", - "vue-tsc": "^3.2.5", + "vite": "^8.0.1", + "vue-tsc": "^3.2.6", "xml2js": "^0.6.2" } } diff --git a/webui/src/main.ts b/webui/src/main.ts index a187efd7c..b0634f8b5 100644 --- a/webui/src/main.ts +++ b/webui/src/main.ts @@ -16,9 +16,17 @@ router.isReady().then(() =>{ app.mount('#app') }) -const config = import.meta.glob('/src/assets/docs/config.json', {as: 'raw', eager: true}) -const nav: DocConfig = JSON.parse(config['/src/assets/docs/config.json']!) +const config = import.meta.glob('/src/assets/docs/config.json', { + query: '?raw', + import: 'default', + eager: true, +}) +const nav: DocConfig = JSON.parse(config['/src/assets/docs/config.json']! as string) app.provide('nav', nav) -const files = import.meta.glob('/src/assets/docs/**/*.md', {as: 'raw', eager: true}) +const files = import.meta.glob('/src/assets/docs/**/*.md', { + query: '?raw', + import: 'default', + eager: true, +}) app.provide('files', files) \ No newline at end of file diff --git a/webui/src/views/DocsView.vue b/webui/src/views/DocsView.vue index 2bb2231d1..012a2f521 100644 --- a/webui/src/views/DocsView.vue +++ b/webui/src/views/DocsView.vue @@ -568,7 +568,9 @@ a[name] { .content p.subtitle { margin-bottom: 1.6rem; font-size: 1.2rem; - font-weight: 400; + font-weight: 550; + line-height: 1.6; + color: rgba(var(--bs-secondary-rgb)) } .tags {