From 1620619d5c8dd5764f09e27865fca22f8dc57228 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 3 Apr 2026 07:43:36 +0000 Subject: [PATCH 01/46] build(deps): bump github.com/go-git/go-git/v5 from 5.17.1 to 5.17.2 Bumps [github.com/go-git/go-git/v5](https://github.com/go-git/go-git) from 5.17.1 to 5.17.2. - [Release notes](https://github.com/go-git/go-git/releases) - [Commits](https://github.com/go-git/go-git/compare/v5.17.1...v5.17.2) --- updated-dependencies: - dependency-name: github.com/go-git/go-git/v5 dependency-version: 5.17.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 4 ++-- go.sum | 8 ++++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index 4a1d62586..0934ea988 100644 --- a/go.mod +++ b/go.mod @@ -12,10 +12,11 @@ require ( 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.1 + github.com/go-git/go-git/v5 v5.17.2 github.com/golang-jwt/jwt/v4 v4.5.2 github.com/google/uuid v1.6.0 github.com/jinzhu/inflection v1.0.0 + github.com/modelcontextprotocol/go-sdk v1.4.1 github.com/pkg/errors v0.9.1 github.com/sirupsen/logrus v1.9.4 github.com/stretchr/testify v1.11.1 @@ -72,7 +73,6 @@ require ( github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/mitchellh/copystructure v1.0.0 // indirect github.com/mitchellh/reflectwalk v1.0.0 // indirect - github.com/modelcontextprotocol/go-sdk v1.4.1 // indirect github.com/mschoch/smat v0.2.0 // indirect github.com/pjbgf/sha1cd v0.3.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect diff --git a/go.sum b/go.sum index 58c3709bf..bb777fa9d 100644 --- a/go.sum +++ b/go.sum @@ -95,12 +95,14 @@ github.com/go-git/go-billy/v5 v5.8.0 h1:I8hjc3LbBlXTtVuFNJuwYuMiHvQJDq1AT6u4DwDz github.com/go-git/go-billy/v5 v5.8.0/go.mod h1:RpvI/rw4Vr5QA+Z60c6d6LXH0rYJo0uD5SqfmrrheCY= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= -github.com/go-git/go-git/v5 v5.17.1 h1:WnljyxIzSj9BRRUlnmAU35ohDsjRK0EKmL0evDqi5Jk= -github.com/go-git/go-git/v5 v5.17.1/go.mod h1:pW/VmeqkanRFqR6AljLcs7EA7FbZaN5MQqO7oZADXpo= +github.com/go-git/go-git/v5 v5.17.2 h1:B+nkdlxdYrvyFK4GPXVU8w1U+YkbsgciIR7f2sZJ104= +github.com/go-git/go-git/v5 v5.17.2/go.mod h1:pW/VmeqkanRFqR6AljLcs7EA7FbZaN5MQqO7oZADXpo= github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU= github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= @@ -228,6 +230,8 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= +golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= 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= From 46fbd9fe4f016cbbb2541abe37d0408fca2ddcf6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 3 Apr 2026 07:51:35 +0000 Subject: [PATCH 02/46] build(deps): bump github.com/yuin/gopher-lua from 1.1.1 to 1.1.2 Bumps [github.com/yuin/gopher-lua](https://github.com/yuin/gopher-lua) from 1.1.1 to 1.1.2. - [Release notes](https://github.com/yuin/gopher-lua/releases) - [Commits](https://github.com/yuin/gopher-lua/compare/v1.1.1...v1.1.2) --- updated-dependencies: - dependency-name: github.com/yuin/gopher-lua dependency-version: 1.1.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 0934ea988..b8102dc11 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,7 @@ require ( github.com/pkg/errors v0.9.1 github.com/sirupsen/logrus v1.9.4 github.com/stretchr/testify v1.11.1 - github.com/yuin/gopher-lua v1.1.1 + github.com/yuin/gopher-lua v1.1.2 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 diff --git a/go.sum b/go.sum index bb777fa9d..8f6396bdd 100644 --- a/go.sum +++ b/go.sum @@ -195,8 +195,8 @@ github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= github.com/yuin/gopher-lua v0.0.0-20190206043414-8bfc7677f583/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ= -github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= -github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= +github.com/yuin/gopher-lua v1.1.2 h1:yF/FjE3hD65tBbt0VXLE13HWS9h34fdzJmrWRXwobGA= +github.com/yuin/gopher-lua v1.1.2/go.mod h1:7aRmXIWl37SqRf0koeyylBEzJ+aPt8A+mmkQ4f1ntR8= go.etcd.io/bbolt v1.4.0 h1:TU77id3TnN/zKr7CO/uk+fBCwF2jGcMuw2B/FMAzYIk= go.etcd.io/bbolt v1.4.0/go.mod h1:AsD+OCi/qPN1giOX1aiLAha3o1U8rAz65bvN4j0sRuk= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= From 55b8da061f1947fac5fcb000769a48fdddeda25c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 3 Apr 2026 08:00:09 +0000 Subject: [PATCH 03/46] build(deps): bump github.com/blevesearch/bleve_index_api Bumps [github.com/blevesearch/bleve_index_api](https://github.com/blevesearch/bleve_index_api) from 1.3.6 to 1.3.9. - [Commits](https://github.com/blevesearch/bleve_index_api/compare/v1.3.6...v1.3.9) --- updated-dependencies: - dependency-name: github.com/blevesearch/bleve_index_api dependency-version: 1.3.9 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index b8102dc11..9cc3c06d9 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.25.5 require ( github.com/Masterminds/sprig v2.22.0+incompatible github.com/blevesearch/bleve/v2 v2.5.7 - github.com/blevesearch/bleve_index_api v1.3.6 + github.com/blevesearch/bleve_index_api v1.3.9 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 diff --git a/go.sum b/go.sum index 8f6396bdd..3e675d8a3 100644 --- a/go.sum +++ b/go.sum @@ -24,8 +24,8 @@ github.com/bits-and-blooms/bitset v1.22.0 h1:Tquv9S8+SGaS3EhyA+up3FXzmkhxPGjQQCk github.com/bits-and-blooms/bitset v1.22.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/blevesearch/bleve/v2 v2.5.7 h1:2d9YrL5zrX5EBBW++GOaEKjE+NPWeZGaX77IM26m1Z8= github.com/blevesearch/bleve/v2 v2.5.7/go.mod h1:yj0NlS7ocGC4VOSAedqDDMktdh2935v2CSWOCDMHdSA= -github.com/blevesearch/bleve_index_api v1.3.6 h1:Cp67NjekrlHh2KTDGpKM0hqMnBTBIBJVQVtEEzDnIaI= -github.com/blevesearch/bleve_index_api v1.3.6/go.mod h1:xvd48t5XMeeioWQ5/jZvgLrV98flT2rdvEJ3l/ki4Ko= +github.com/blevesearch/bleve_index_api v1.3.9 h1:TLoiBaqcfWGfI1Il0+zzky452uYCPoSMosDSltkCfKs= +github.com/blevesearch/bleve_index_api v1.3.9/go.mod h1:xvd48t5XMeeioWQ5/jZvgLrV98flT2rdvEJ3l/ki4Ko= github.com/blevesearch/geo v0.2.4 h1:ECIGQhw+QALCZaDcogRTNSJYQXRtC8/m8IKiA706cqk= github.com/blevesearch/geo v0.2.4/go.mod h1:K56Q33AzXt2YExVHGObtmRSFYZKYGv0JEN5mdacJJR8= github.com/blevesearch/go-faiss v1.0.26 h1:4dRLolFgjPyjkaXwff4NfbZFdE/dfywbzDqporeQvXI= From 4aa9dfb041dd67b42b1e397023cf92b4c4477fcf Mon Sep 17 00:00:00 2001 From: maesi Date: Fri, 3 Apr 2026 10:01:58 +0200 Subject: [PATCH 04/46] chore(test): move tests into one folder --- .github/actions/publish-website/action.yml | 4 +- .github/actions/run-frontend-tests/action.yml | 4 +- .../acceptance}/acceptance_test.go | 0 {acceptance => tests/acceptance}/cmd_test.go | 0 {acceptance => tests/acceptance}/ldap/amy.png | Bin .../acceptance}/ldap/farnsworth.png | Bin {acceptance => tests/acceptance}/ldap/ldap.js | 0 .../acceptance}/ldap/ldap.yml | 0 .../acceptance}/ldap/leela.png | Bin .../acceptance}/ldap/philip.png | Bin {acceptance => tests/acceptance}/ldap_test.go | 0 .../acceptance}/mail/config.yml | 0 .../acceptance}/mail/config_old.yml | 0 .../acceptance}/mail/mail.mokapi.local.pem | 0 {acceptance => tests/acceptance}/mail_test.go | 0 .../acceptance}/petstore/asyncapi.yml | 0 .../acceptance}/petstore/openapi.yml | 0 .../acceptance}/petstore/petstore-3.0.yaml | 0 .../acceptance}/petstore/petstore.js | 0 .../acceptance}/petstore/petstore.lua | 0 .../acceptance}/petstore/store.lua | 0 .../acceptance}/petstore_test.go | 0 tests/e2e/.gitignore | 5 + .../mokapi => tests/e2e/mocks}/common.yml | 0 .../mokapi => tests/e2e/mocks}/dashboard.yml | 0 .../mokapi => tests/e2e/mocks}/frequency.js | 0 {examples/mokapi => tests/e2e/mocks}/http.yml | 0 .../e2e/mocks}/http_handler.js | 0 {examples/mokapi => tests/e2e/mocks}/icon.png | Bin {examples/mokapi => tests/e2e/mocks}/kafka.js | 0 .../mokapi => tests/e2e/mocks}/kafka.yml | 0 {examples/mokapi => tests/e2e/mocks}/ldap.js | 0 {examples/mokapi => tests/e2e/mocks}/ldap.yml | 0 {examples/mokapi => tests/e2e/mocks}/mail.js | 0 {examples/mokapi => tests/e2e/mocks}/mail.yml | 0 .../mokapi => tests/e2e/mocks}/metrics.js | 0 .../mokapi => tests/e2e/mocks}/schema.yml | 0 .../e2e/mocks}/services_http.js | 0 {webui => tests/e2e}/playwright.config.ts | 2 +- .../e2e/tests}/components/dashboard.ts | 0 .../e2e/tests}/components/kafka.ts | 0 .../e2e/tests}/components/source.ts | 0 .../e2e/tests}/components/table.ts | 0 .../tests}/dashboard-demo/dashboard.spec.ts | 0 .../e2e/tests}/dashboard-demo/kafka.spec.ts | 0 .../e2e/tests}/dashboard-demo/ldap.spec.ts | 0 .../e2e/tests}/dashboard-demo/mail.spec.ts | 0 .../tests}/dashboard-demo/petstore.spec.ts | 0 .../e2e/tests/dashboard}/dashboard.spec.ts | 0 .../e2e/tests/dashboard}/http/books.spec.ts | 0 .../tests/dashboard}/kafka/cluster.spec.ts | 0 .../e2e/tests/dashboard}/kafka/cluster.ts | 0 .../tests/dashboard}/kafka/overview.spec.ts | 0 .../dashboard}/kafka/topic.order.spec.ts | 0 .../kafka/topic.userSignedUp.spec.ts | 0 .../tests/dashboard}/mail/testserver.spec.ts | 0 .../e2e/tests}/documentation/config.ts | 0 .../documentation/configuration.spec.ts | 0 .../e2e/tests}/documentation/guides.spec.ts | 0 .../tests}/documentation/resources.spec.ts | 0 .../e2e/tests}/header.dashboard.spec.ts | 0 .../e2e/tests}/header.website.spec.ts | 0 .../e2e => tests/e2e/tests}/helpers/format.ts | 0 .../e2e => tests/e2e/tests}/helpers/table.ts | 0 {webui/e2e => tests/e2e/tests}/home.spec.ts | 0 .../e2e/tests}/models/dashboard.ts | 0 .../e2e/tests}/models/fixture-dashboard.ts | 0 .../e2e/tests}/models/fixture-website.ts | 0 {webui/e2e => tests/e2e/tests}/models/home.ts | 0 {webui/e2e => tests/e2e/tests}/models/http.ts | 0 .../e2e => tests/e2e/tests}/models/kafka.ts | 0 {webui/e2e => tests/e2e/tests}/models/mail.ts | 0 .../e2e => tests/e2e/tests}/models/metric.ts | 0 .../e2e => tests/e2e/tests}/models/mokapi.ts | 0 .../e2e/tests}/models/service-info.ts | 0 .../e2e => tests/e2e/tests}/sitemap.spec.ts | 0 {webui/e2e => tests/e2e/tests}/tsconfig.json | 0 {webui/e2e => tests/e2e/tests}/types/types.ts | 0 webui/.gitignore | 5 - webui/README.md | 17 --- webui/e2e/vue.spec.ts | 8 -- webui/package-lock.json | 101 +----------------- webui/package.json | 4 +- 83 files changed, 12 insertions(+), 138 deletions(-) rename {acceptance => tests/acceptance}/acceptance_test.go (100%) rename {acceptance => tests/acceptance}/cmd_test.go (100%) rename {acceptance => tests/acceptance}/ldap/amy.png (100%) rename {acceptance => tests/acceptance}/ldap/farnsworth.png (100%) rename {acceptance => tests/acceptance}/ldap/ldap.js (100%) rename {acceptance => tests/acceptance}/ldap/ldap.yml (100%) rename {acceptance => tests/acceptance}/ldap/leela.png (100%) rename {acceptance => tests/acceptance}/ldap/philip.png (100%) rename {acceptance => tests/acceptance}/ldap_test.go (100%) rename {acceptance => tests/acceptance}/mail/config.yml (100%) rename {acceptance => tests/acceptance}/mail/config_old.yml (100%) rename {acceptance => tests/acceptance}/mail/mail.mokapi.local.pem (100%) rename {acceptance => tests/acceptance}/mail_test.go (100%) rename {acceptance => tests/acceptance}/petstore/asyncapi.yml (100%) rename {acceptance => tests/acceptance}/petstore/openapi.yml (100%) rename {acceptance => tests/acceptance}/petstore/petstore-3.0.yaml (100%) rename {acceptance => tests/acceptance}/petstore/petstore.js (100%) rename {acceptance => tests/acceptance}/petstore/petstore.lua (100%) rename {acceptance => tests/acceptance}/petstore/store.lua (100%) rename {acceptance => tests/acceptance}/petstore_test.go (100%) create mode 100644 tests/e2e/.gitignore rename {examples/mokapi => tests/e2e/mocks}/common.yml (100%) rename {examples/mokapi => tests/e2e/mocks}/dashboard.yml (100%) rename {examples/mokapi => tests/e2e/mocks}/frequency.js (100%) rename {examples/mokapi => tests/e2e/mocks}/http.yml (100%) rename {examples/mokapi => tests/e2e/mocks}/http_handler.js (100%) rename {examples/mokapi => tests/e2e/mocks}/icon.png (100%) rename {examples/mokapi => tests/e2e/mocks}/kafka.js (100%) rename {examples/mokapi => tests/e2e/mocks}/kafka.yml (100%) rename {examples/mokapi => tests/e2e/mocks}/ldap.js (100%) rename {examples/mokapi => tests/e2e/mocks}/ldap.yml (100%) rename {examples/mokapi => tests/e2e/mocks}/mail.js (100%) rename {examples/mokapi => tests/e2e/mocks}/mail.yml (100%) rename {examples/mokapi => tests/e2e/mocks}/metrics.js (100%) rename {examples/mokapi => tests/e2e/mocks}/schema.yml (100%) rename {examples/mokapi => tests/e2e/mocks}/services_http.js (100%) rename {webui => tests/e2e}/playwright.config.ts (99%) rename {webui/e2e => tests/e2e/tests}/components/dashboard.ts (100%) rename {webui/e2e => tests/e2e/tests}/components/kafka.ts (100%) rename {webui/e2e => tests/e2e/tests}/components/source.ts (100%) rename {webui/e2e => tests/e2e/tests}/components/table.ts (100%) rename {webui/e2e => tests/e2e/tests}/dashboard-demo/dashboard.spec.ts (100%) rename {webui/e2e => tests/e2e/tests}/dashboard-demo/kafka.spec.ts (100%) rename {webui/e2e => tests/e2e/tests}/dashboard-demo/ldap.spec.ts (100%) rename {webui/e2e => tests/e2e/tests}/dashboard-demo/mail.spec.ts (100%) rename {webui/e2e => tests/e2e/tests}/dashboard-demo/petstore.spec.ts (100%) rename {webui/e2e/Dashboard => tests/e2e/tests/dashboard}/dashboard.spec.ts (100%) rename {webui/e2e/Dashboard => tests/e2e/tests/dashboard}/http/books.spec.ts (100%) rename {webui/e2e/Dashboard => tests/e2e/tests/dashboard}/kafka/cluster.spec.ts (100%) rename {webui/e2e/Dashboard => tests/e2e/tests/dashboard}/kafka/cluster.ts (100%) rename {webui/e2e/Dashboard => tests/e2e/tests/dashboard}/kafka/overview.spec.ts (100%) rename {webui/e2e/Dashboard => tests/e2e/tests/dashboard}/kafka/topic.order.spec.ts (100%) rename {webui/e2e/Dashboard => tests/e2e/tests/dashboard}/kafka/topic.userSignedUp.spec.ts (100%) rename {webui/e2e/Dashboard => tests/e2e/tests/dashboard}/mail/testserver.spec.ts (100%) rename {webui/e2e => tests/e2e/tests}/documentation/config.ts (100%) rename {webui/e2e => tests/e2e/tests}/documentation/configuration.spec.ts (100%) rename {webui/e2e => tests/e2e/tests}/documentation/guides.spec.ts (100%) rename {webui/e2e => tests/e2e/tests}/documentation/resources.spec.ts (100%) rename {webui/e2e => tests/e2e/tests}/header.dashboard.spec.ts (100%) rename {webui/e2e => tests/e2e/tests}/header.website.spec.ts (100%) rename {webui/e2e => tests/e2e/tests}/helpers/format.ts (100%) rename {webui/e2e => tests/e2e/tests}/helpers/table.ts (100%) rename {webui/e2e => tests/e2e/tests}/home.spec.ts (100%) rename {webui/e2e => tests/e2e/tests}/models/dashboard.ts (100%) rename {webui/e2e => tests/e2e/tests}/models/fixture-dashboard.ts (100%) rename {webui/e2e => tests/e2e/tests}/models/fixture-website.ts (100%) rename {webui/e2e => tests/e2e/tests}/models/home.ts (100%) rename {webui/e2e => tests/e2e/tests}/models/http.ts (100%) rename {webui/e2e => tests/e2e/tests}/models/kafka.ts (100%) rename {webui/e2e => tests/e2e/tests}/models/mail.ts (100%) rename {webui/e2e => tests/e2e/tests}/models/metric.ts (100%) rename {webui/e2e => tests/e2e/tests}/models/mokapi.ts (100%) rename {webui/e2e => tests/e2e/tests}/models/service-info.ts (100%) rename {webui/e2e => tests/e2e/tests}/sitemap.spec.ts (100%) rename {webui/e2e => tests/e2e/tests}/tsconfig.json (100%) rename {webui/e2e => tests/e2e/tests}/types/types.ts (100%) delete mode 100644 webui/e2e/vue.spec.ts diff --git a/.github/actions/publish-website/action.yml b/.github/actions/publish-website/action.yml index baf650b94..faad64a0d 100644 --- a/.github/actions/publish-website/action.yml +++ b/.github/actions/publish-website/action.yml @@ -62,11 +62,11 @@ runs: npm run build-website shell: bash - name: Install Playwright - working-directory: ./webui + working-directory: ./tests/e2e run: npx playwright install --with-deps shell: bash - name: test website - working-directory: ./webui + working-directory: ./tests/e2e run: npx playwright test --project=website shell: bash - name: Upload test results diff --git a/.github/actions/run-frontend-tests/action.yml b/.github/actions/run-frontend-tests/action.yml index 5c8f568e2..1fdcabdec 100644 --- a/.github/actions/run-frontend-tests/action.yml +++ b/.github/actions/run-frontend-tests/action.yml @@ -37,11 +37,11 @@ runs: npm run build-dashboard shell: bash - name: Install Playwright - working-directory: ./webui + working-directory: ./tests/e2e run: npx playwright install --with-deps shell: bash - name: Run your tests - working-directory: ./webui + working-directory: ./tests/e2e run: npx playwright test --project=dashboard shell: bash - name: Upload test results diff --git a/acceptance/acceptance_test.go b/tests/acceptance/acceptance_test.go similarity index 100% rename from acceptance/acceptance_test.go rename to tests/acceptance/acceptance_test.go diff --git a/acceptance/cmd_test.go b/tests/acceptance/cmd_test.go similarity index 100% rename from acceptance/cmd_test.go rename to tests/acceptance/cmd_test.go diff --git a/acceptance/ldap/amy.png b/tests/acceptance/ldap/amy.png similarity index 100% rename from acceptance/ldap/amy.png rename to tests/acceptance/ldap/amy.png diff --git a/acceptance/ldap/farnsworth.png b/tests/acceptance/ldap/farnsworth.png similarity index 100% rename from acceptance/ldap/farnsworth.png rename to tests/acceptance/ldap/farnsworth.png diff --git a/acceptance/ldap/ldap.js b/tests/acceptance/ldap/ldap.js similarity index 100% rename from acceptance/ldap/ldap.js rename to tests/acceptance/ldap/ldap.js diff --git a/acceptance/ldap/ldap.yml b/tests/acceptance/ldap/ldap.yml similarity index 100% rename from acceptance/ldap/ldap.yml rename to tests/acceptance/ldap/ldap.yml diff --git a/acceptance/ldap/leela.png b/tests/acceptance/ldap/leela.png similarity index 100% rename from acceptance/ldap/leela.png rename to tests/acceptance/ldap/leela.png diff --git a/acceptance/ldap/philip.png b/tests/acceptance/ldap/philip.png similarity index 100% rename from acceptance/ldap/philip.png rename to tests/acceptance/ldap/philip.png diff --git a/acceptance/ldap_test.go b/tests/acceptance/ldap_test.go similarity index 100% rename from acceptance/ldap_test.go rename to tests/acceptance/ldap_test.go diff --git a/acceptance/mail/config.yml b/tests/acceptance/mail/config.yml similarity index 100% rename from acceptance/mail/config.yml rename to tests/acceptance/mail/config.yml diff --git a/acceptance/mail/config_old.yml b/tests/acceptance/mail/config_old.yml similarity index 100% rename from acceptance/mail/config_old.yml rename to tests/acceptance/mail/config_old.yml diff --git a/acceptance/mail/mail.mokapi.local.pem b/tests/acceptance/mail/mail.mokapi.local.pem similarity index 100% rename from acceptance/mail/mail.mokapi.local.pem rename to tests/acceptance/mail/mail.mokapi.local.pem diff --git a/acceptance/mail_test.go b/tests/acceptance/mail_test.go similarity index 100% rename from acceptance/mail_test.go rename to tests/acceptance/mail_test.go diff --git a/acceptance/petstore/asyncapi.yml b/tests/acceptance/petstore/asyncapi.yml similarity index 100% rename from acceptance/petstore/asyncapi.yml rename to tests/acceptance/petstore/asyncapi.yml diff --git a/acceptance/petstore/openapi.yml b/tests/acceptance/petstore/openapi.yml similarity index 100% rename from acceptance/petstore/openapi.yml rename to tests/acceptance/petstore/openapi.yml diff --git a/acceptance/petstore/petstore-3.0.yaml b/tests/acceptance/petstore/petstore-3.0.yaml similarity index 100% rename from acceptance/petstore/petstore-3.0.yaml rename to tests/acceptance/petstore/petstore-3.0.yaml diff --git a/acceptance/petstore/petstore.js b/tests/acceptance/petstore/petstore.js similarity index 100% rename from acceptance/petstore/petstore.js rename to tests/acceptance/petstore/petstore.js diff --git a/acceptance/petstore/petstore.lua b/tests/acceptance/petstore/petstore.lua similarity index 100% rename from acceptance/petstore/petstore.lua rename to tests/acceptance/petstore/petstore.lua diff --git a/acceptance/petstore/store.lua b/tests/acceptance/petstore/store.lua similarity index 100% rename from acceptance/petstore/store.lua rename to tests/acceptance/petstore/store.lua diff --git a/acceptance/petstore_test.go b/tests/acceptance/petstore_test.go similarity index 100% rename from acceptance/petstore_test.go rename to tests/acceptance/petstore_test.go diff --git a/tests/e2e/.gitignore b/tests/e2e/.gitignore new file mode 100644 index 000000000..137d9041b --- /dev/null +++ b/tests/e2e/.gitignore @@ -0,0 +1,5 @@ +node_modules +.DS_Store + +test-results/ +playwright-report/ diff --git a/examples/mokapi/common.yml b/tests/e2e/mocks/common.yml similarity index 100% rename from examples/mokapi/common.yml rename to tests/e2e/mocks/common.yml diff --git a/examples/mokapi/dashboard.yml b/tests/e2e/mocks/dashboard.yml similarity index 100% rename from examples/mokapi/dashboard.yml rename to tests/e2e/mocks/dashboard.yml diff --git a/examples/mokapi/frequency.js b/tests/e2e/mocks/frequency.js similarity index 100% rename from examples/mokapi/frequency.js rename to tests/e2e/mocks/frequency.js diff --git a/examples/mokapi/http.yml b/tests/e2e/mocks/http.yml similarity index 100% rename from examples/mokapi/http.yml rename to tests/e2e/mocks/http.yml diff --git a/examples/mokapi/http_handler.js b/tests/e2e/mocks/http_handler.js similarity index 100% rename from examples/mokapi/http_handler.js rename to tests/e2e/mocks/http_handler.js diff --git a/examples/mokapi/icon.png b/tests/e2e/mocks/icon.png similarity index 100% rename from examples/mokapi/icon.png rename to tests/e2e/mocks/icon.png diff --git a/examples/mokapi/kafka.js b/tests/e2e/mocks/kafka.js similarity index 100% rename from examples/mokapi/kafka.js rename to tests/e2e/mocks/kafka.js diff --git a/examples/mokapi/kafka.yml b/tests/e2e/mocks/kafka.yml similarity index 100% rename from examples/mokapi/kafka.yml rename to tests/e2e/mocks/kafka.yml diff --git a/examples/mokapi/ldap.js b/tests/e2e/mocks/ldap.js similarity index 100% rename from examples/mokapi/ldap.js rename to tests/e2e/mocks/ldap.js diff --git a/examples/mokapi/ldap.yml b/tests/e2e/mocks/ldap.yml similarity index 100% rename from examples/mokapi/ldap.yml rename to tests/e2e/mocks/ldap.yml diff --git a/examples/mokapi/mail.js b/tests/e2e/mocks/mail.js similarity index 100% rename from examples/mokapi/mail.js rename to tests/e2e/mocks/mail.js diff --git a/examples/mokapi/mail.yml b/tests/e2e/mocks/mail.yml similarity index 100% rename from examples/mokapi/mail.yml rename to tests/e2e/mocks/mail.yml diff --git a/examples/mokapi/metrics.js b/tests/e2e/mocks/metrics.js similarity index 100% rename from examples/mokapi/metrics.js rename to tests/e2e/mocks/metrics.js diff --git a/examples/mokapi/schema.yml b/tests/e2e/mocks/schema.yml similarity index 100% rename from examples/mokapi/schema.yml rename to tests/e2e/mocks/schema.yml diff --git a/examples/mokapi/services_http.js b/tests/e2e/mocks/services_http.js similarity index 100% rename from examples/mokapi/services_http.js rename to tests/e2e/mocks/services_http.js diff --git a/webui/playwright.config.ts b/tests/e2e/playwright.config.ts similarity index 99% rename from webui/playwright.config.ts rename to tests/e2e/playwright.config.ts index 6158261f9..0ca90caca 100644 --- a/webui/playwright.config.ts +++ b/tests/e2e/playwright.config.ts @@ -11,7 +11,7 @@ import { devices } from '@playwright/test' * See https://playwright.dev/docs/test-configuration. */ const config: PlaywrightTestConfig = { - testDir: './e2e', + testDir: './tests', /* Maximum time one test can run for. */ timeout: 30 * 1000, expect: { diff --git a/webui/e2e/components/dashboard.ts b/tests/e2e/tests/components/dashboard.ts similarity index 100% rename from webui/e2e/components/dashboard.ts rename to tests/e2e/tests/components/dashboard.ts diff --git a/webui/e2e/components/kafka.ts b/tests/e2e/tests/components/kafka.ts similarity index 100% rename from webui/e2e/components/kafka.ts rename to tests/e2e/tests/components/kafka.ts diff --git a/webui/e2e/components/source.ts b/tests/e2e/tests/components/source.ts similarity index 100% rename from webui/e2e/components/source.ts rename to tests/e2e/tests/components/source.ts diff --git a/webui/e2e/components/table.ts b/tests/e2e/tests/components/table.ts similarity index 100% rename from webui/e2e/components/table.ts rename to tests/e2e/tests/components/table.ts diff --git a/webui/e2e/dashboard-demo/dashboard.spec.ts b/tests/e2e/tests/dashboard-demo/dashboard.spec.ts similarity index 100% rename from webui/e2e/dashboard-demo/dashboard.spec.ts rename to tests/e2e/tests/dashboard-demo/dashboard.spec.ts diff --git a/webui/e2e/dashboard-demo/kafka.spec.ts b/tests/e2e/tests/dashboard-demo/kafka.spec.ts similarity index 100% rename from webui/e2e/dashboard-demo/kafka.spec.ts rename to tests/e2e/tests/dashboard-demo/kafka.spec.ts diff --git a/webui/e2e/dashboard-demo/ldap.spec.ts b/tests/e2e/tests/dashboard-demo/ldap.spec.ts similarity index 100% rename from webui/e2e/dashboard-demo/ldap.spec.ts rename to tests/e2e/tests/dashboard-demo/ldap.spec.ts diff --git a/webui/e2e/dashboard-demo/mail.spec.ts b/tests/e2e/tests/dashboard-demo/mail.spec.ts similarity index 100% rename from webui/e2e/dashboard-demo/mail.spec.ts rename to tests/e2e/tests/dashboard-demo/mail.spec.ts diff --git a/webui/e2e/dashboard-demo/petstore.spec.ts b/tests/e2e/tests/dashboard-demo/petstore.spec.ts similarity index 100% rename from webui/e2e/dashboard-demo/petstore.spec.ts rename to tests/e2e/tests/dashboard-demo/petstore.spec.ts diff --git a/webui/e2e/Dashboard/dashboard.spec.ts b/tests/e2e/tests/dashboard/dashboard.spec.ts similarity index 100% rename from webui/e2e/Dashboard/dashboard.spec.ts rename to tests/e2e/tests/dashboard/dashboard.spec.ts diff --git a/webui/e2e/Dashboard/http/books.spec.ts b/tests/e2e/tests/dashboard/http/books.spec.ts similarity index 100% rename from webui/e2e/Dashboard/http/books.spec.ts rename to tests/e2e/tests/dashboard/http/books.spec.ts diff --git a/webui/e2e/Dashboard/kafka/cluster.spec.ts b/tests/e2e/tests/dashboard/kafka/cluster.spec.ts similarity index 100% rename from webui/e2e/Dashboard/kafka/cluster.spec.ts rename to tests/e2e/tests/dashboard/kafka/cluster.spec.ts diff --git a/webui/e2e/Dashboard/kafka/cluster.ts b/tests/e2e/tests/dashboard/kafka/cluster.ts similarity index 100% rename from webui/e2e/Dashboard/kafka/cluster.ts rename to tests/e2e/tests/dashboard/kafka/cluster.ts diff --git a/webui/e2e/Dashboard/kafka/overview.spec.ts b/tests/e2e/tests/dashboard/kafka/overview.spec.ts similarity index 100% rename from webui/e2e/Dashboard/kafka/overview.spec.ts rename to tests/e2e/tests/dashboard/kafka/overview.spec.ts diff --git a/webui/e2e/Dashboard/kafka/topic.order.spec.ts b/tests/e2e/tests/dashboard/kafka/topic.order.spec.ts similarity index 100% rename from webui/e2e/Dashboard/kafka/topic.order.spec.ts rename to tests/e2e/tests/dashboard/kafka/topic.order.spec.ts diff --git a/webui/e2e/Dashboard/kafka/topic.userSignedUp.spec.ts b/tests/e2e/tests/dashboard/kafka/topic.userSignedUp.spec.ts similarity index 100% rename from webui/e2e/Dashboard/kafka/topic.userSignedUp.spec.ts rename to tests/e2e/tests/dashboard/kafka/topic.userSignedUp.spec.ts diff --git a/webui/e2e/Dashboard/mail/testserver.spec.ts b/tests/e2e/tests/dashboard/mail/testserver.spec.ts similarity index 100% rename from webui/e2e/Dashboard/mail/testserver.spec.ts rename to tests/e2e/tests/dashboard/mail/testserver.spec.ts diff --git a/webui/e2e/documentation/config.ts b/tests/e2e/tests/documentation/config.ts similarity index 100% rename from webui/e2e/documentation/config.ts rename to tests/e2e/tests/documentation/config.ts diff --git a/webui/e2e/documentation/configuration.spec.ts b/tests/e2e/tests/documentation/configuration.spec.ts similarity index 100% rename from webui/e2e/documentation/configuration.spec.ts rename to tests/e2e/tests/documentation/configuration.spec.ts diff --git a/webui/e2e/documentation/guides.spec.ts b/tests/e2e/tests/documentation/guides.spec.ts similarity index 100% rename from webui/e2e/documentation/guides.spec.ts rename to tests/e2e/tests/documentation/guides.spec.ts diff --git a/webui/e2e/documentation/resources.spec.ts b/tests/e2e/tests/documentation/resources.spec.ts similarity index 100% rename from webui/e2e/documentation/resources.spec.ts rename to tests/e2e/tests/documentation/resources.spec.ts diff --git a/webui/e2e/header.dashboard.spec.ts b/tests/e2e/tests/header.dashboard.spec.ts similarity index 100% rename from webui/e2e/header.dashboard.spec.ts rename to tests/e2e/tests/header.dashboard.spec.ts diff --git a/webui/e2e/header.website.spec.ts b/tests/e2e/tests/header.website.spec.ts similarity index 100% rename from webui/e2e/header.website.spec.ts rename to tests/e2e/tests/header.website.spec.ts diff --git a/webui/e2e/helpers/format.ts b/tests/e2e/tests/helpers/format.ts similarity index 100% rename from webui/e2e/helpers/format.ts rename to tests/e2e/tests/helpers/format.ts diff --git a/webui/e2e/helpers/table.ts b/tests/e2e/tests/helpers/table.ts similarity index 100% rename from webui/e2e/helpers/table.ts rename to tests/e2e/tests/helpers/table.ts diff --git a/webui/e2e/home.spec.ts b/tests/e2e/tests/home.spec.ts similarity index 100% rename from webui/e2e/home.spec.ts rename to tests/e2e/tests/home.spec.ts diff --git a/webui/e2e/models/dashboard.ts b/tests/e2e/tests/models/dashboard.ts similarity index 100% rename from webui/e2e/models/dashboard.ts rename to tests/e2e/tests/models/dashboard.ts diff --git a/webui/e2e/models/fixture-dashboard.ts b/tests/e2e/tests/models/fixture-dashboard.ts similarity index 100% rename from webui/e2e/models/fixture-dashboard.ts rename to tests/e2e/tests/models/fixture-dashboard.ts diff --git a/webui/e2e/models/fixture-website.ts b/tests/e2e/tests/models/fixture-website.ts similarity index 100% rename from webui/e2e/models/fixture-website.ts rename to tests/e2e/tests/models/fixture-website.ts diff --git a/webui/e2e/models/home.ts b/tests/e2e/tests/models/home.ts similarity index 100% rename from webui/e2e/models/home.ts rename to tests/e2e/tests/models/home.ts diff --git a/webui/e2e/models/http.ts b/tests/e2e/tests/models/http.ts similarity index 100% rename from webui/e2e/models/http.ts rename to tests/e2e/tests/models/http.ts diff --git a/webui/e2e/models/kafka.ts b/tests/e2e/tests/models/kafka.ts similarity index 100% rename from webui/e2e/models/kafka.ts rename to tests/e2e/tests/models/kafka.ts diff --git a/webui/e2e/models/mail.ts b/tests/e2e/tests/models/mail.ts similarity index 100% rename from webui/e2e/models/mail.ts rename to tests/e2e/tests/models/mail.ts diff --git a/webui/e2e/models/metric.ts b/tests/e2e/tests/models/metric.ts similarity index 100% rename from webui/e2e/models/metric.ts rename to tests/e2e/tests/models/metric.ts diff --git a/webui/e2e/models/mokapi.ts b/tests/e2e/tests/models/mokapi.ts similarity index 100% rename from webui/e2e/models/mokapi.ts rename to tests/e2e/tests/models/mokapi.ts diff --git a/webui/e2e/models/service-info.ts b/tests/e2e/tests/models/service-info.ts similarity index 100% rename from webui/e2e/models/service-info.ts rename to tests/e2e/tests/models/service-info.ts diff --git a/webui/e2e/sitemap.spec.ts b/tests/e2e/tests/sitemap.spec.ts similarity index 100% rename from webui/e2e/sitemap.spec.ts rename to tests/e2e/tests/sitemap.spec.ts diff --git a/webui/e2e/tsconfig.json b/tests/e2e/tests/tsconfig.json similarity index 100% rename from webui/e2e/tsconfig.json rename to tests/e2e/tests/tsconfig.json diff --git a/webui/e2e/types/types.ts b/tests/e2e/tests/types/types.ts similarity index 100% rename from webui/e2e/types/types.ts rename to tests/e2e/tests/types/types.ts diff --git a/webui/.gitignore b/webui/.gitignore index 7382fc04c..8a4a1fb27 100644 --- a/webui/.gitignore +++ b/webui/.gitignore @@ -14,9 +14,6 @@ dist-ssr coverage *.local -/cypress/videos/ -/cypress/screenshots/ - # Editor directories and files .vscode/* !.vscode/extensions.json @@ -27,8 +24,6 @@ coverage *.sln *.sw? -test-results/ -playwright-report/ src/assets/docs public/sitemap.xml public/demo/ diff --git a/webui/README.md b/webui/README.md index 963afa186..5cee247f7 100644 --- a/webui/README.md +++ b/webui/README.md @@ -41,23 +41,6 @@ npm run build ### Run End-to-End Tests with [Playwright](https://playwright.dev) -```sh -# Install browsers for the first run -npx playwright install - -# When testing on CI, must build the project first -npm run build - -# Runs the end-to-end tests -npm run test:e2e -# Runs the tests only on Chromium -npm run test:e2e -- --project=chromium -# Runs the tests of a specific file -npm run test:e2e -- tests/example.spec.ts -# Runs the tests in debug mode -npm run test:e2e -- --debug -``` - ### Lint with [ESLint](https://eslint.org/) ```sh diff --git a/webui/e2e/vue.spec.ts b/webui/e2e/vue.spec.ts deleted file mode 100644 index 644bcab71..000000000 --- a/webui/e2e/vue.spec.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { test, expect } from '@playwright/test'; - -// See here how to get started: -// https://playwright.dev/docs/intro -// test('visits the app root url', async ({ page }) => { -// await page.goto('/'); -// await expect(page.locator('div.greetings > h1')).toHaveText('You did it!'); -// }) diff --git a/webui/package-lock.json b/webui/package-lock.json index d0e36a221..c69fbcb24 100644 --- a/webui/package-lock.json +++ b/webui/package-lock.json @@ -39,7 +39,6 @@ "xml-formatter": "^3.7.0" }, "devDependencies": { - "@playwright/test": "^1.58.2", "@rushstack/eslint-patch": "^1.16.1", "@types/js-yaml": "^4.0.9", "@types/markdown-it-container": "^4.0.0", @@ -54,8 +53,7 @@ "prettier": "^3.8.1", "typescript": "~5.9.3", "vite": "^8.0.1", - "vue-tsc": "^3.2.6", - "xml2js": "^0.6.2" + "vue-tsc": "^3.2.6" } }, "node_modules/@babel/generator": { @@ -459,22 +457,6 @@ "url": "https://opencollective.com/pkgr" } }, - "node_modules/@playwright/test": { - "version": "1.58.2", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz", - "integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "playwright": "1.58.2" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/@popperjs/core": { "version": "2.11.8", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", @@ -3259,21 +3241,6 @@ "node": ">= 0.8" } }, - "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -5365,38 +5332,6 @@ "pathe": "^2.0.3" } }, - "node_modules/playwright": { - "version": "1.58.2", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", - "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "playwright-core": "1.58.2" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "fsevents": "2.3.2" - } - }, - "node_modules/playwright-core": { - "version": "1.58.2", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", - "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "playwright-core": "cli.js" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -5904,16 +5839,6 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, - "node_modules/sax": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.4.tgz", - "integrity": "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=11.0.0" - } - }, "node_modules/scule": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/scule/-/scule-1.3.0.tgz", @@ -7169,30 +7094,6 @@ "node": ">= 16" } }, - "node_modules/xml2js": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", - "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", - "dev": true, - "license": "MIT", - "dependencies": { - "sax": ">=0.6.0", - "xmlbuilder": "~11.0.0" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/xmlbuilder": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", - "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4.0" - } - }, "node_modules/yaml": { "version": "2.8.2", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", diff --git a/webui/package.json b/webui/package.json index 825cfd99e..299d643ed 100644 --- a/webui/package.json +++ b/webui/package.json @@ -48,7 +48,6 @@ "xml-formatter": "^3.7.0" }, "devDependencies": { - "@playwright/test": "^1.58.2", "@rushstack/eslint-patch": "^1.16.1", "@types/js-yaml": "^4.0.9", "@types/markdown-it-container": "^4.0.0", @@ -63,7 +62,6 @@ "prettier": "^3.8.1", "typescript": "~5.9.3", "vite": "^8.0.1", - "vue-tsc": "^3.2.6", - "xml2js": "^0.6.2" + "vue-tsc": "^3.2.6" } } From 9d65fbe3265641aac5bf74b6e0e0d8d04164203b Mon Sep 17 00:00:00 2001 From: maesi Date: Fri, 3 Apr 2026 11:08:20 +0200 Subject: [PATCH 05/46] chore(test): update test --- imap/idle_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/imap/idle_test.go b/imap/idle_test.go index 5be684545..0822383a3 100644 --- a/imap/idle_test.go +++ b/imap/idle_test.go @@ -240,9 +240,9 @@ func TestSendUpdatesWhileIdle(t *testing.T) { require.NoError(t, err) require.Equal(t, "* 1 EXPUNGE", res) - res, err = c.SendRaw("A01 FINISHED") + res, err = c.SendRaw("DONE") require.NoError(t, err) - require.Equal(t, "A01 BAD Expected DONE to end IDLE", res) + require.Equal(t, "A01 OK IDLE terminated", res) } func TestIdle_DisconnectWithoutDone(t *testing.T) { From 23f390455891b81b15d4b7e318a88f2247f69ea2 Mon Sep 17 00:00:00 2001 From: maesi Date: Fri, 3 Apr 2026 11:15:55 +0200 Subject: [PATCH 06/46] chore(test): fix test --- pkg/cmd/mokapi/sample_data_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/mokapi/sample_data_test.go b/pkg/cmd/mokapi/sample_data_test.go index be149c874..cf6def028 100644 --- a/pkg/cmd/mokapi/sample_data_test.go +++ b/pkg/cmd/mokapi/sample_data_test.go @@ -54,7 +54,7 @@ func TestMain_SampleData(t *testing.T) { }, { name: "generate from openapi", - args: []string{"sample-data", "../../../acceptance/petstore/openapi.yml#/paths/~1pet/put/requestBody/content/application~1json/schema"}, + args: []string{"sample-data", "../../../tests/acceptance/petstore/openapi.yml#/paths/~1pet/put/requestBody/content/application~1json/schema"}, test: func(t *testing.T, out string) { require.Equal(t, `{"id":37727,"category":{"id":83580,"name":"rabbit"},"name":"Prince of Barkness","photoUrls":[],"tags":[{"id":57421,"name":"Prism"},{"id":69949,"name":"Sol"}],"status":"pending"}`, out) }, From 4d26593e4669a6373727fa4cee75d6bb1186b7fa Mon Sep 17 00:00:00 2001 From: maesi Date: Fri, 3 Apr 2026 11:44:36 +0200 Subject: [PATCH 07/46] chore(test): fix path to mocks --- .github/actions/run-frontend-tests/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/run-frontend-tests/action.yml b/.github/actions/run-frontend-tests/action.yml index 1fdcabdec..eddce283c 100644 --- a/.github/actions/run-frontend-tests/action.yml +++ b/.github/actions/run-frontend-tests/action.yml @@ -23,7 +23,7 @@ runs: run: docker load --input /tmp/mokapi.tar shell: bash - name: Run mokapi image - run: docker run --name mokapi --rm -d -p 8090:8090 -p 8091:8091 --mount type=bind,source=$(pwd)/examples/mokapi,target=/data --env MOKAPI_Log_Level=Debug --env MOKAPI_Api_Port=8091 --env MOKAPI_Api_Path=/mokapi --env MOKAPI_Providers_File_Directory=/data ${{ inputs.image-name }} + run: docker run --name mokapi --rm -d -p 8090:8090 -p 8091:8091 --mount type=bind,source=$(pwd)/tests/e2e/mocks,target=/data --env MOKAPI_Log_Level=Debug --env MOKAPI_Api_Port=8091 --env MOKAPI_Api_Path=/mokapi --env MOKAPI_Providers_File_Directory=/data ${{ inputs.image-name }} shell: bash - uses: actions/setup-node@v4 with: From b8a032e6ecc33f11e0d87b40933218c247714fcd Mon Sep 17 00:00:00 2001 From: maesi Date: Fri, 3 Apr 2026 13:54:53 +0200 Subject: [PATCH 08/46] chore(test): move tests --- .github/actions/publish-website/action.yml | 4 +- .github/actions/run-frontend-tests/action.yml | 8 +- .../acceptance_test.go | 0 {tests/acceptance => acceptance}/cmd_test.go | 0 {tests/acceptance => acceptance}/ldap/amy.png | Bin .../ldap/farnsworth.png | Bin {tests/acceptance => acceptance}/ldap/ldap.js | 0 .../acceptance => acceptance}/ldap/ldap.yml | 0 .../acceptance => acceptance}/ldap/leela.png | Bin .../acceptance => acceptance}/ldap/philip.png | Bin {tests/acceptance => acceptance}/ldap_test.go | 0 .../acceptance => acceptance}/mail/config.yml | 0 .../mail/config_old.yml | 0 .../mail/mail.mokapi.local.pem | 0 {tests/acceptance => acceptance}/mail_test.go | 0 .../petstore/asyncapi.yml | 0 .../petstore/openapi.yml | 0 .../petstore/petstore-3.0.yaml | 0 .../petstore/petstore.js | 0 .../petstore/petstore.lua | 0 .../petstore/store.lua | 0 .../petstore_test.go | 0 pkg/cmd/mokapi/sample_data_test.go | 2 +- tests/e2e/.gitignore | 5 - webui/.gitignore | 3 + {tests => webui}/e2e/mocks/common.yml | 0 {tests => webui}/e2e/mocks/dashboard.yml | 0 {tests => webui}/e2e/mocks/frequency.js | 0 {tests => webui}/e2e/mocks/http.yml | 0 {tests => webui}/e2e/mocks/http_handler.js | 0 {tests => webui}/e2e/mocks/icon.png | Bin {tests => webui}/e2e/mocks/kafka.js | 0 {tests => webui}/e2e/mocks/kafka.yml | 0 {tests => webui}/e2e/mocks/ldap.js | 0 {tests => webui}/e2e/mocks/ldap.yml | 0 {tests => webui}/e2e/mocks/mail.js | 0 {tests => webui}/e2e/mocks/mail.yml | 0 {tests => webui}/e2e/mocks/metrics.js | 0 {tests => webui}/e2e/mocks/schema.yml | 0 {tests => webui}/e2e/mocks/services_http.js | 0 .../e2e/tests/components/dashboard.ts | 0 .../e2e/tests/components/kafka.ts | 0 .../e2e/tests/components/source.ts | 0 .../e2e/tests/components/table.ts | 0 .../tests/dashboard-demo/dashboard.spec.ts | 0 .../e2e/tests/dashboard-demo/kafka.spec.ts | 0 .../e2e/tests/dashboard-demo/ldap.spec.ts | 0 .../e2e/tests/dashboard-demo/mail.spec.ts | 0 .../e2e/tests/dashboard-demo/petstore.spec.ts | 0 .../e2e/tests/dashboard/dashboard.spec.ts | 0 .../e2e/tests/dashboard/http/books.spec.ts | 0 .../e2e/tests/dashboard/kafka/cluster.spec.ts | 0 .../e2e/tests/dashboard/kafka/cluster.ts | 0 .../tests/dashboard/kafka/overview.spec.ts | 0 .../tests/dashboard/kafka/topic.order.spec.ts | 0 .../kafka/topic.userSignedUp.spec.ts | 0 .../tests/dashboard/mail/testserver.spec.ts | 0 .../e2e/tests/documentation/config.ts | 0 .../tests/documentation/configuration.spec.ts | 0 .../e2e/tests/documentation/guides.spec.ts | 0 .../e2e/tests/documentation/resources.spec.ts | 0 .../e2e/tests/header.dashboard.spec.ts | 0 .../e2e/tests/header.website.spec.ts | 0 {tests => webui}/e2e/tests/helpers/format.ts | 0 {tests => webui}/e2e/tests/helpers/table.ts | 0 {tests => webui}/e2e/tests/home.spec.ts | 0 .../e2e/tests/models/dashboard.ts | 0 .../e2e/tests/models/fixture-dashboard.ts | 0 .../e2e/tests/models/fixture-website.ts | 0 {tests => webui}/e2e/tests/models/home.ts | 0 {tests => webui}/e2e/tests/models/http.ts | 0 {tests => webui}/e2e/tests/models/kafka.ts | 0 {tests => webui}/e2e/tests/models/mail.ts | 0 {tests => webui}/e2e/tests/models/metric.ts | 0 {tests => webui}/e2e/tests/models/mokapi.ts | 0 .../e2e/tests/models/service-info.ts | 0 {tests => webui}/e2e/tests/sitemap.spec.ts | 0 {tests => webui}/e2e/tests/tsconfig.json | 0 {tests => webui}/e2e/tests/types/types.ts | 0 webui/package-lock.json | 109 +++++++++++++++++- webui/package.json | 6 +- {tests/e2e => webui}/playwright.config.ts | 2 +- 82 files changed, 119 insertions(+), 20 deletions(-) rename {tests/acceptance => acceptance}/acceptance_test.go (100%) rename {tests/acceptance => acceptance}/cmd_test.go (100%) rename {tests/acceptance => acceptance}/ldap/amy.png (100%) rename {tests/acceptance => acceptance}/ldap/farnsworth.png (100%) rename {tests/acceptance => acceptance}/ldap/ldap.js (100%) rename {tests/acceptance => acceptance}/ldap/ldap.yml (100%) rename {tests/acceptance => acceptance}/ldap/leela.png (100%) rename {tests/acceptance => acceptance}/ldap/philip.png (100%) rename {tests/acceptance => acceptance}/ldap_test.go (100%) rename {tests/acceptance => acceptance}/mail/config.yml (100%) rename {tests/acceptance => acceptance}/mail/config_old.yml (100%) rename {tests/acceptance => acceptance}/mail/mail.mokapi.local.pem (100%) rename {tests/acceptance => acceptance}/mail_test.go (100%) rename {tests/acceptance => acceptance}/petstore/asyncapi.yml (100%) rename {tests/acceptance => acceptance}/petstore/openapi.yml (100%) rename {tests/acceptance => acceptance}/petstore/petstore-3.0.yaml (100%) rename {tests/acceptance => acceptance}/petstore/petstore.js (100%) rename {tests/acceptance => acceptance}/petstore/petstore.lua (100%) rename {tests/acceptance => acceptance}/petstore/store.lua (100%) rename {tests/acceptance => acceptance}/petstore_test.go (100%) delete mode 100644 tests/e2e/.gitignore rename {tests => webui}/e2e/mocks/common.yml (100%) rename {tests => webui}/e2e/mocks/dashboard.yml (100%) rename {tests => webui}/e2e/mocks/frequency.js (100%) rename {tests => webui}/e2e/mocks/http.yml (100%) rename {tests => webui}/e2e/mocks/http_handler.js (100%) rename {tests => webui}/e2e/mocks/icon.png (100%) rename {tests => webui}/e2e/mocks/kafka.js (100%) rename {tests => webui}/e2e/mocks/kafka.yml (100%) rename {tests => webui}/e2e/mocks/ldap.js (100%) rename {tests => webui}/e2e/mocks/ldap.yml (100%) rename {tests => webui}/e2e/mocks/mail.js (100%) rename {tests => webui}/e2e/mocks/mail.yml (100%) rename {tests => webui}/e2e/mocks/metrics.js (100%) rename {tests => webui}/e2e/mocks/schema.yml (100%) rename {tests => webui}/e2e/mocks/services_http.js (100%) rename {tests => webui}/e2e/tests/components/dashboard.ts (100%) rename {tests => webui}/e2e/tests/components/kafka.ts (100%) rename {tests => webui}/e2e/tests/components/source.ts (100%) rename {tests => webui}/e2e/tests/components/table.ts (100%) rename {tests => webui}/e2e/tests/dashboard-demo/dashboard.spec.ts (100%) rename {tests => webui}/e2e/tests/dashboard-demo/kafka.spec.ts (100%) rename {tests => webui}/e2e/tests/dashboard-demo/ldap.spec.ts (100%) rename {tests => webui}/e2e/tests/dashboard-demo/mail.spec.ts (100%) rename {tests => webui}/e2e/tests/dashboard-demo/petstore.spec.ts (100%) rename {tests => webui}/e2e/tests/dashboard/dashboard.spec.ts (100%) rename {tests => webui}/e2e/tests/dashboard/http/books.spec.ts (100%) rename {tests => webui}/e2e/tests/dashboard/kafka/cluster.spec.ts (100%) rename {tests => webui}/e2e/tests/dashboard/kafka/cluster.ts (100%) rename {tests => webui}/e2e/tests/dashboard/kafka/overview.spec.ts (100%) rename {tests => webui}/e2e/tests/dashboard/kafka/topic.order.spec.ts (100%) rename {tests => webui}/e2e/tests/dashboard/kafka/topic.userSignedUp.spec.ts (100%) rename {tests => webui}/e2e/tests/dashboard/mail/testserver.spec.ts (100%) rename {tests => webui}/e2e/tests/documentation/config.ts (100%) rename {tests => webui}/e2e/tests/documentation/configuration.spec.ts (100%) rename {tests => webui}/e2e/tests/documentation/guides.spec.ts (100%) rename {tests => webui}/e2e/tests/documentation/resources.spec.ts (100%) rename {tests => webui}/e2e/tests/header.dashboard.spec.ts (100%) rename {tests => webui}/e2e/tests/header.website.spec.ts (100%) rename {tests => webui}/e2e/tests/helpers/format.ts (100%) rename {tests => webui}/e2e/tests/helpers/table.ts (100%) rename {tests => webui}/e2e/tests/home.spec.ts (100%) rename {tests => webui}/e2e/tests/models/dashboard.ts (100%) rename {tests => webui}/e2e/tests/models/fixture-dashboard.ts (100%) rename {tests => webui}/e2e/tests/models/fixture-website.ts (100%) rename {tests => webui}/e2e/tests/models/home.ts (100%) rename {tests => webui}/e2e/tests/models/http.ts (100%) rename {tests => webui}/e2e/tests/models/kafka.ts (100%) rename {tests => webui}/e2e/tests/models/mail.ts (100%) rename {tests => webui}/e2e/tests/models/metric.ts (100%) rename {tests => webui}/e2e/tests/models/mokapi.ts (100%) rename {tests => webui}/e2e/tests/models/service-info.ts (100%) rename {tests => webui}/e2e/tests/sitemap.spec.ts (100%) rename {tests => webui}/e2e/tests/tsconfig.json (100%) rename {tests => webui}/e2e/tests/types/types.ts (100%) rename {tests/e2e => webui}/playwright.config.ts (99%) diff --git a/.github/actions/publish-website/action.yml b/.github/actions/publish-website/action.yml index faad64a0d..baf650b94 100644 --- a/.github/actions/publish-website/action.yml +++ b/.github/actions/publish-website/action.yml @@ -62,11 +62,11 @@ runs: npm run build-website shell: bash - name: Install Playwright - working-directory: ./tests/e2e + working-directory: ./webui run: npx playwright install --with-deps shell: bash - name: test website - working-directory: ./tests/e2e + working-directory: ./webui run: npx playwright test --project=website shell: bash - name: Upload test results diff --git a/.github/actions/run-frontend-tests/action.yml b/.github/actions/run-frontend-tests/action.yml index eddce283c..98aaa1cb8 100644 --- a/.github/actions/run-frontend-tests/action.yml +++ b/.github/actions/run-frontend-tests/action.yml @@ -23,7 +23,7 @@ runs: run: docker load --input /tmp/mokapi.tar shell: bash - name: Run mokapi image - run: docker run --name mokapi --rm -d -p 8090:8090 -p 8091:8091 --mount type=bind,source=$(pwd)/tests/e2e/mocks,target=/data --env MOKAPI_Log_Level=Debug --env MOKAPI_Api_Port=8091 --env MOKAPI_Api_Path=/mokapi --env MOKAPI_Providers_File_Directory=/data ${{ inputs.image-name }} + run: docker run --name mokapi --rm -d -p 8090:8090 -p 8091:8091 --mount type=bind,source=$(pwd)/webui/e2e/mocks,target=/data --env MOKAPI_Log_Level=Debug --env MOKAPI_Api_Port=8091 --env MOKAPI_Api_Path=/mokapi --env MOKAPI_Providers_File_Directory=/data ${{ inputs.image-name }} shell: bash - uses: actions/setup-node@v4 with: @@ -37,11 +37,11 @@ runs: npm run build-dashboard shell: bash - name: Install Playwright - working-directory: ./tests/e2e + working-directory: ./webui run: npx playwright install --with-deps shell: bash - name: Run your tests - working-directory: ./tests/e2e + working-directory: ./webui run: npx playwright test --project=dashboard shell: bash - name: Upload test results @@ -49,7 +49,7 @@ runs: uses: actions/upload-artifact@v4 with: name: ${{ inputs.artifact-test-report }} - path: webui/playwright-report + path: ./webui/playwright-report - name: Save Mokapi logs if: always() run: docker logs mokapi > /var/tmp/mokapi.log 2>&1 diff --git a/tests/acceptance/acceptance_test.go b/acceptance/acceptance_test.go similarity index 100% rename from tests/acceptance/acceptance_test.go rename to acceptance/acceptance_test.go diff --git a/tests/acceptance/cmd_test.go b/acceptance/cmd_test.go similarity index 100% rename from tests/acceptance/cmd_test.go rename to acceptance/cmd_test.go diff --git a/tests/acceptance/ldap/amy.png b/acceptance/ldap/amy.png similarity index 100% rename from tests/acceptance/ldap/amy.png rename to acceptance/ldap/amy.png diff --git a/tests/acceptance/ldap/farnsworth.png b/acceptance/ldap/farnsworth.png similarity index 100% rename from tests/acceptance/ldap/farnsworth.png rename to acceptance/ldap/farnsworth.png diff --git a/tests/acceptance/ldap/ldap.js b/acceptance/ldap/ldap.js similarity index 100% rename from tests/acceptance/ldap/ldap.js rename to acceptance/ldap/ldap.js diff --git a/tests/acceptance/ldap/ldap.yml b/acceptance/ldap/ldap.yml similarity index 100% rename from tests/acceptance/ldap/ldap.yml rename to acceptance/ldap/ldap.yml diff --git a/tests/acceptance/ldap/leela.png b/acceptance/ldap/leela.png similarity index 100% rename from tests/acceptance/ldap/leela.png rename to acceptance/ldap/leela.png diff --git a/tests/acceptance/ldap/philip.png b/acceptance/ldap/philip.png similarity index 100% rename from tests/acceptance/ldap/philip.png rename to acceptance/ldap/philip.png diff --git a/tests/acceptance/ldap_test.go b/acceptance/ldap_test.go similarity index 100% rename from tests/acceptance/ldap_test.go rename to acceptance/ldap_test.go diff --git a/tests/acceptance/mail/config.yml b/acceptance/mail/config.yml similarity index 100% rename from tests/acceptance/mail/config.yml rename to acceptance/mail/config.yml diff --git a/tests/acceptance/mail/config_old.yml b/acceptance/mail/config_old.yml similarity index 100% rename from tests/acceptance/mail/config_old.yml rename to acceptance/mail/config_old.yml diff --git a/tests/acceptance/mail/mail.mokapi.local.pem b/acceptance/mail/mail.mokapi.local.pem similarity index 100% rename from tests/acceptance/mail/mail.mokapi.local.pem rename to acceptance/mail/mail.mokapi.local.pem diff --git a/tests/acceptance/mail_test.go b/acceptance/mail_test.go similarity index 100% rename from tests/acceptance/mail_test.go rename to acceptance/mail_test.go diff --git a/tests/acceptance/petstore/asyncapi.yml b/acceptance/petstore/asyncapi.yml similarity index 100% rename from tests/acceptance/petstore/asyncapi.yml rename to acceptance/petstore/asyncapi.yml diff --git a/tests/acceptance/petstore/openapi.yml b/acceptance/petstore/openapi.yml similarity index 100% rename from tests/acceptance/petstore/openapi.yml rename to acceptance/petstore/openapi.yml diff --git a/tests/acceptance/petstore/petstore-3.0.yaml b/acceptance/petstore/petstore-3.0.yaml similarity index 100% rename from tests/acceptance/petstore/petstore-3.0.yaml rename to acceptance/petstore/petstore-3.0.yaml diff --git a/tests/acceptance/petstore/petstore.js b/acceptance/petstore/petstore.js similarity index 100% rename from tests/acceptance/petstore/petstore.js rename to acceptance/petstore/petstore.js diff --git a/tests/acceptance/petstore/petstore.lua b/acceptance/petstore/petstore.lua similarity index 100% rename from tests/acceptance/petstore/petstore.lua rename to acceptance/petstore/petstore.lua diff --git a/tests/acceptance/petstore/store.lua b/acceptance/petstore/store.lua similarity index 100% rename from tests/acceptance/petstore/store.lua rename to acceptance/petstore/store.lua diff --git a/tests/acceptance/petstore_test.go b/acceptance/petstore_test.go similarity index 100% rename from tests/acceptance/petstore_test.go rename to acceptance/petstore_test.go diff --git a/pkg/cmd/mokapi/sample_data_test.go b/pkg/cmd/mokapi/sample_data_test.go index cf6def028..be149c874 100644 --- a/pkg/cmd/mokapi/sample_data_test.go +++ b/pkg/cmd/mokapi/sample_data_test.go @@ -54,7 +54,7 @@ func TestMain_SampleData(t *testing.T) { }, { name: "generate from openapi", - args: []string{"sample-data", "../../../tests/acceptance/petstore/openapi.yml#/paths/~1pet/put/requestBody/content/application~1json/schema"}, + args: []string{"sample-data", "../../../acceptance/petstore/openapi.yml#/paths/~1pet/put/requestBody/content/application~1json/schema"}, test: func(t *testing.T, out string) { require.Equal(t, `{"id":37727,"category":{"id":83580,"name":"rabbit"},"name":"Prince of Barkness","photoUrls":[],"tags":[{"id":57421,"name":"Prism"},{"id":69949,"name":"Sol"}],"status":"pending"}`, out) }, diff --git a/tests/e2e/.gitignore b/tests/e2e/.gitignore deleted file mode 100644 index 137d9041b..000000000 --- a/tests/e2e/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -node_modules -.DS_Store - -test-results/ -playwright-report/ diff --git a/webui/.gitignore b/webui/.gitignore index 8a4a1fb27..c0c29250d 100644 --- a/webui/.gitignore +++ b/webui/.gitignore @@ -27,3 +27,6 @@ coverage src/assets/docs public/sitemap.xml public/demo/ + +test-results/ +playwright-report/ diff --git a/tests/e2e/mocks/common.yml b/webui/e2e/mocks/common.yml similarity index 100% rename from tests/e2e/mocks/common.yml rename to webui/e2e/mocks/common.yml diff --git a/tests/e2e/mocks/dashboard.yml b/webui/e2e/mocks/dashboard.yml similarity index 100% rename from tests/e2e/mocks/dashboard.yml rename to webui/e2e/mocks/dashboard.yml diff --git a/tests/e2e/mocks/frequency.js b/webui/e2e/mocks/frequency.js similarity index 100% rename from tests/e2e/mocks/frequency.js rename to webui/e2e/mocks/frequency.js diff --git a/tests/e2e/mocks/http.yml b/webui/e2e/mocks/http.yml similarity index 100% rename from tests/e2e/mocks/http.yml rename to webui/e2e/mocks/http.yml diff --git a/tests/e2e/mocks/http_handler.js b/webui/e2e/mocks/http_handler.js similarity index 100% rename from tests/e2e/mocks/http_handler.js rename to webui/e2e/mocks/http_handler.js diff --git a/tests/e2e/mocks/icon.png b/webui/e2e/mocks/icon.png similarity index 100% rename from tests/e2e/mocks/icon.png rename to webui/e2e/mocks/icon.png diff --git a/tests/e2e/mocks/kafka.js b/webui/e2e/mocks/kafka.js similarity index 100% rename from tests/e2e/mocks/kafka.js rename to webui/e2e/mocks/kafka.js diff --git a/tests/e2e/mocks/kafka.yml b/webui/e2e/mocks/kafka.yml similarity index 100% rename from tests/e2e/mocks/kafka.yml rename to webui/e2e/mocks/kafka.yml diff --git a/tests/e2e/mocks/ldap.js b/webui/e2e/mocks/ldap.js similarity index 100% rename from tests/e2e/mocks/ldap.js rename to webui/e2e/mocks/ldap.js diff --git a/tests/e2e/mocks/ldap.yml b/webui/e2e/mocks/ldap.yml similarity index 100% rename from tests/e2e/mocks/ldap.yml rename to webui/e2e/mocks/ldap.yml diff --git a/tests/e2e/mocks/mail.js b/webui/e2e/mocks/mail.js similarity index 100% rename from tests/e2e/mocks/mail.js rename to webui/e2e/mocks/mail.js diff --git a/tests/e2e/mocks/mail.yml b/webui/e2e/mocks/mail.yml similarity index 100% rename from tests/e2e/mocks/mail.yml rename to webui/e2e/mocks/mail.yml diff --git a/tests/e2e/mocks/metrics.js b/webui/e2e/mocks/metrics.js similarity index 100% rename from tests/e2e/mocks/metrics.js rename to webui/e2e/mocks/metrics.js diff --git a/tests/e2e/mocks/schema.yml b/webui/e2e/mocks/schema.yml similarity index 100% rename from tests/e2e/mocks/schema.yml rename to webui/e2e/mocks/schema.yml diff --git a/tests/e2e/mocks/services_http.js b/webui/e2e/mocks/services_http.js similarity index 100% rename from tests/e2e/mocks/services_http.js rename to webui/e2e/mocks/services_http.js diff --git a/tests/e2e/tests/components/dashboard.ts b/webui/e2e/tests/components/dashboard.ts similarity index 100% rename from tests/e2e/tests/components/dashboard.ts rename to webui/e2e/tests/components/dashboard.ts diff --git a/tests/e2e/tests/components/kafka.ts b/webui/e2e/tests/components/kafka.ts similarity index 100% rename from tests/e2e/tests/components/kafka.ts rename to webui/e2e/tests/components/kafka.ts diff --git a/tests/e2e/tests/components/source.ts b/webui/e2e/tests/components/source.ts similarity index 100% rename from tests/e2e/tests/components/source.ts rename to webui/e2e/tests/components/source.ts diff --git a/tests/e2e/tests/components/table.ts b/webui/e2e/tests/components/table.ts similarity index 100% rename from tests/e2e/tests/components/table.ts rename to webui/e2e/tests/components/table.ts diff --git a/tests/e2e/tests/dashboard-demo/dashboard.spec.ts b/webui/e2e/tests/dashboard-demo/dashboard.spec.ts similarity index 100% rename from tests/e2e/tests/dashboard-demo/dashboard.spec.ts rename to webui/e2e/tests/dashboard-demo/dashboard.spec.ts diff --git a/tests/e2e/tests/dashboard-demo/kafka.spec.ts b/webui/e2e/tests/dashboard-demo/kafka.spec.ts similarity index 100% rename from tests/e2e/tests/dashboard-demo/kafka.spec.ts rename to webui/e2e/tests/dashboard-demo/kafka.spec.ts diff --git a/tests/e2e/tests/dashboard-demo/ldap.spec.ts b/webui/e2e/tests/dashboard-demo/ldap.spec.ts similarity index 100% rename from tests/e2e/tests/dashboard-demo/ldap.spec.ts rename to webui/e2e/tests/dashboard-demo/ldap.spec.ts diff --git a/tests/e2e/tests/dashboard-demo/mail.spec.ts b/webui/e2e/tests/dashboard-demo/mail.spec.ts similarity index 100% rename from tests/e2e/tests/dashboard-demo/mail.spec.ts rename to webui/e2e/tests/dashboard-demo/mail.spec.ts diff --git a/tests/e2e/tests/dashboard-demo/petstore.spec.ts b/webui/e2e/tests/dashboard-demo/petstore.spec.ts similarity index 100% rename from tests/e2e/tests/dashboard-demo/petstore.spec.ts rename to webui/e2e/tests/dashboard-demo/petstore.spec.ts diff --git a/tests/e2e/tests/dashboard/dashboard.spec.ts b/webui/e2e/tests/dashboard/dashboard.spec.ts similarity index 100% rename from tests/e2e/tests/dashboard/dashboard.spec.ts rename to webui/e2e/tests/dashboard/dashboard.spec.ts diff --git a/tests/e2e/tests/dashboard/http/books.spec.ts b/webui/e2e/tests/dashboard/http/books.spec.ts similarity index 100% rename from tests/e2e/tests/dashboard/http/books.spec.ts rename to webui/e2e/tests/dashboard/http/books.spec.ts diff --git a/tests/e2e/tests/dashboard/kafka/cluster.spec.ts b/webui/e2e/tests/dashboard/kafka/cluster.spec.ts similarity index 100% rename from tests/e2e/tests/dashboard/kafka/cluster.spec.ts rename to webui/e2e/tests/dashboard/kafka/cluster.spec.ts diff --git a/tests/e2e/tests/dashboard/kafka/cluster.ts b/webui/e2e/tests/dashboard/kafka/cluster.ts similarity index 100% rename from tests/e2e/tests/dashboard/kafka/cluster.ts rename to webui/e2e/tests/dashboard/kafka/cluster.ts diff --git a/tests/e2e/tests/dashboard/kafka/overview.spec.ts b/webui/e2e/tests/dashboard/kafka/overview.spec.ts similarity index 100% rename from tests/e2e/tests/dashboard/kafka/overview.spec.ts rename to webui/e2e/tests/dashboard/kafka/overview.spec.ts diff --git a/tests/e2e/tests/dashboard/kafka/topic.order.spec.ts b/webui/e2e/tests/dashboard/kafka/topic.order.spec.ts similarity index 100% rename from tests/e2e/tests/dashboard/kafka/topic.order.spec.ts rename to webui/e2e/tests/dashboard/kafka/topic.order.spec.ts diff --git a/tests/e2e/tests/dashboard/kafka/topic.userSignedUp.spec.ts b/webui/e2e/tests/dashboard/kafka/topic.userSignedUp.spec.ts similarity index 100% rename from tests/e2e/tests/dashboard/kafka/topic.userSignedUp.spec.ts rename to webui/e2e/tests/dashboard/kafka/topic.userSignedUp.spec.ts diff --git a/tests/e2e/tests/dashboard/mail/testserver.spec.ts b/webui/e2e/tests/dashboard/mail/testserver.spec.ts similarity index 100% rename from tests/e2e/tests/dashboard/mail/testserver.spec.ts rename to webui/e2e/tests/dashboard/mail/testserver.spec.ts diff --git a/tests/e2e/tests/documentation/config.ts b/webui/e2e/tests/documentation/config.ts similarity index 100% rename from tests/e2e/tests/documentation/config.ts rename to webui/e2e/tests/documentation/config.ts diff --git a/tests/e2e/tests/documentation/configuration.spec.ts b/webui/e2e/tests/documentation/configuration.spec.ts similarity index 100% rename from tests/e2e/tests/documentation/configuration.spec.ts rename to webui/e2e/tests/documentation/configuration.spec.ts diff --git a/tests/e2e/tests/documentation/guides.spec.ts b/webui/e2e/tests/documentation/guides.spec.ts similarity index 100% rename from tests/e2e/tests/documentation/guides.spec.ts rename to webui/e2e/tests/documentation/guides.spec.ts diff --git a/tests/e2e/tests/documentation/resources.spec.ts b/webui/e2e/tests/documentation/resources.spec.ts similarity index 100% rename from tests/e2e/tests/documentation/resources.spec.ts rename to webui/e2e/tests/documentation/resources.spec.ts diff --git a/tests/e2e/tests/header.dashboard.spec.ts b/webui/e2e/tests/header.dashboard.spec.ts similarity index 100% rename from tests/e2e/tests/header.dashboard.spec.ts rename to webui/e2e/tests/header.dashboard.spec.ts diff --git a/tests/e2e/tests/header.website.spec.ts b/webui/e2e/tests/header.website.spec.ts similarity index 100% rename from tests/e2e/tests/header.website.spec.ts rename to webui/e2e/tests/header.website.spec.ts diff --git a/tests/e2e/tests/helpers/format.ts b/webui/e2e/tests/helpers/format.ts similarity index 100% rename from tests/e2e/tests/helpers/format.ts rename to webui/e2e/tests/helpers/format.ts diff --git a/tests/e2e/tests/helpers/table.ts b/webui/e2e/tests/helpers/table.ts similarity index 100% rename from tests/e2e/tests/helpers/table.ts rename to webui/e2e/tests/helpers/table.ts diff --git a/tests/e2e/tests/home.spec.ts b/webui/e2e/tests/home.spec.ts similarity index 100% rename from tests/e2e/tests/home.spec.ts rename to webui/e2e/tests/home.spec.ts diff --git a/tests/e2e/tests/models/dashboard.ts b/webui/e2e/tests/models/dashboard.ts similarity index 100% rename from tests/e2e/tests/models/dashboard.ts rename to webui/e2e/tests/models/dashboard.ts diff --git a/tests/e2e/tests/models/fixture-dashboard.ts b/webui/e2e/tests/models/fixture-dashboard.ts similarity index 100% rename from tests/e2e/tests/models/fixture-dashboard.ts rename to webui/e2e/tests/models/fixture-dashboard.ts diff --git a/tests/e2e/tests/models/fixture-website.ts b/webui/e2e/tests/models/fixture-website.ts similarity index 100% rename from tests/e2e/tests/models/fixture-website.ts rename to webui/e2e/tests/models/fixture-website.ts diff --git a/tests/e2e/tests/models/home.ts b/webui/e2e/tests/models/home.ts similarity index 100% rename from tests/e2e/tests/models/home.ts rename to webui/e2e/tests/models/home.ts diff --git a/tests/e2e/tests/models/http.ts b/webui/e2e/tests/models/http.ts similarity index 100% rename from tests/e2e/tests/models/http.ts rename to webui/e2e/tests/models/http.ts diff --git a/tests/e2e/tests/models/kafka.ts b/webui/e2e/tests/models/kafka.ts similarity index 100% rename from tests/e2e/tests/models/kafka.ts rename to webui/e2e/tests/models/kafka.ts diff --git a/tests/e2e/tests/models/mail.ts b/webui/e2e/tests/models/mail.ts similarity index 100% rename from tests/e2e/tests/models/mail.ts rename to webui/e2e/tests/models/mail.ts diff --git a/tests/e2e/tests/models/metric.ts b/webui/e2e/tests/models/metric.ts similarity index 100% rename from tests/e2e/tests/models/metric.ts rename to webui/e2e/tests/models/metric.ts diff --git a/tests/e2e/tests/models/mokapi.ts b/webui/e2e/tests/models/mokapi.ts similarity index 100% rename from tests/e2e/tests/models/mokapi.ts rename to webui/e2e/tests/models/mokapi.ts diff --git a/tests/e2e/tests/models/service-info.ts b/webui/e2e/tests/models/service-info.ts similarity index 100% rename from tests/e2e/tests/models/service-info.ts rename to webui/e2e/tests/models/service-info.ts diff --git a/tests/e2e/tests/sitemap.spec.ts b/webui/e2e/tests/sitemap.spec.ts similarity index 100% rename from tests/e2e/tests/sitemap.spec.ts rename to webui/e2e/tests/sitemap.spec.ts diff --git a/tests/e2e/tests/tsconfig.json b/webui/e2e/tests/tsconfig.json similarity index 100% rename from tests/e2e/tests/tsconfig.json rename to webui/e2e/tests/tsconfig.json diff --git a/tests/e2e/tests/types/types.ts b/webui/e2e/tests/types/types.ts similarity index 100% rename from tests/e2e/tests/types/types.ts rename to webui/e2e/tests/types/types.ts diff --git a/webui/package-lock.json b/webui/package-lock.json index e5a4eb5a1..1653e2bc0 100644 --- a/webui/package-lock.json +++ b/webui/package-lock.json @@ -39,10 +39,11 @@ "xml-formatter": "^3.7.0" }, "devDependencies": { + "@playwright/test": "^1.59.1", "@rushstack/eslint-patch": "^1.16.1", "@types/js-yaml": "^4.0.9", "@types/markdown-it-container": "^4.0.0", - "@types/node": "^25.5.0", + "@types/node": "^25.5.1", "@vitejs/plugin-vue": "^6.0.5", "@vue/eslint-config-prettier": "^10.2.0", "@vue/eslint-config-typescript": "^14.7.0", @@ -53,7 +54,8 @@ "prettier": "^3.8.1", "typescript": "~5.9.3", "vite": "^8.0.1", - "vue-tsc": "^3.2.6" + "vue-tsc": "^3.2.6", + "xml2js": "^0.6.2" } }, "node_modules/@babel/generator": { @@ -457,6 +459,22 @@ "url": "https://opencollective.com/pkgr" } }, + "node_modules/@playwright/test": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.1.tgz", + "integrity": "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.59.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@popperjs/core": { "version": "2.11.8", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", @@ -855,9 +873,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", - "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", + "version": "25.5.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.1.tgz", + "integrity": "sha512-lgrR3HRNQdTEeeXBnLURFO4JIIbpcVcMlLM9IG0jsNRTRNSbMkm9S2hyhxhnokke1NM25Dr9QghgeB5PQKolrw==", "license": "MIT", "dependencies": { "undici-types": "~7.18.0" @@ -3241,6 +3259,21 @@ "node": ">= 0.8" } }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -5332,6 +5365,38 @@ "pathe": "^2.0.3" } }, + "node_modules/playwright": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz", + "integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.59.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz", + "integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -5839,6 +5904,16 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/sax": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, "node_modules/scule": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/scule/-/scule-1.3.0.tgz", @@ -7094,6 +7169,30 @@ "node": ">= 16" } }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "dev": true, + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, "node_modules/yaml": { "version": "2.8.3", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", diff --git a/webui/package.json b/webui/package.json index 299d643ed..ae877e18b 100644 --- a/webui/package.json +++ b/webui/package.json @@ -48,10 +48,11 @@ "xml-formatter": "^3.7.0" }, "devDependencies": { + "@playwright/test": "^1.59.1", "@rushstack/eslint-patch": "^1.16.1", "@types/js-yaml": "^4.0.9", "@types/markdown-it-container": "^4.0.0", - "@types/node": "^25.5.0", + "@types/node": "^25.5.1", "@vitejs/plugin-vue": "^6.0.5", "@vue/eslint-config-prettier": "^10.2.0", "@vue/eslint-config-typescript": "^14.7.0", @@ -62,6 +63,7 @@ "prettier": "^3.8.1", "typescript": "~5.9.3", "vite": "^8.0.1", - "vue-tsc": "^3.2.6" + "vue-tsc": "^3.2.6", + "xml2js": "^0.6.2" } } diff --git a/tests/e2e/playwright.config.ts b/webui/playwright.config.ts similarity index 99% rename from tests/e2e/playwright.config.ts rename to webui/playwright.config.ts index 0ca90caca..afeba4961 100644 --- a/tests/e2e/playwright.config.ts +++ b/webui/playwright.config.ts @@ -11,7 +11,7 @@ import { devices } from '@playwright/test' * See https://playwright.dev/docs/test-configuration. */ const config: PlaywrightTestConfig = { - testDir: './tests', + testDir: './e2e/tests', /* Maximum time one test can run for. */ timeout: 30 * 1000, expect: { From 635f238b09cbb4c0091b36d44102adb952bc931d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 3 Apr 2026 11:56:05 +0000 Subject: [PATCH 09/46] build(deps): bump fuse.js from 7.1.0 to 7.2.0 in /webui Bumps [fuse.js](https://github.com/krisk/Fuse) from 7.1.0 to 7.2.0. - [Release notes](https://github.com/krisk/Fuse/releases) - [Changelog](https://github.com/krisk/Fuse/blob/main/CHANGELOG.md) - [Commits](https://github.com/krisk/Fuse/compare/v7.1.0...v7.2.0) --- updated-dependencies: - dependency-name: fuse.js dependency-version: 7.2.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- webui/package-lock.json | 8 ++++---- webui/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/webui/package-lock.json b/webui/package-lock.json index 1653e2bc0..b640d35e9 100644 --- a/webui/package-lock.json +++ b/webui/package-lock.json @@ -21,7 +21,7 @@ "dayjs": "^1.11.20", "del-cli": "^7.0.0", "express": "^5.2.1", - "fuse.js": "^7.1.0", + "fuse.js": "^7.2.0", "highlight.js": "^11.11.1", "http-status-codes": "^2.3.0", "js-yaml": "^4.1.1", @@ -3315,9 +3315,9 @@ } }, "node_modules/fuse.js": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.1.0.tgz", - "integrity": "sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.2.0.tgz", + "integrity": "sha512-zf4vdcIGpjNKTuXwug33Hm2okqX6a0t2ZEbez+o9oBJQSNhVJ5AqERfeiRD3r8HcLqP66MrjdkmzxrncbAOTUQ==", "license": "Apache-2.0", "engines": { "node": ">=10" diff --git a/webui/package.json b/webui/package.json index ae877e18b..2ed1d47ec 100644 --- a/webui/package.json +++ b/webui/package.json @@ -30,7 +30,7 @@ "dayjs": "^1.11.20", "del-cli": "^7.0.0", "express": "^5.2.1", - "fuse.js": "^7.1.0", + "fuse.js": "^7.2.0", "highlight.js": "^11.11.1", "http-status-codes": "^2.3.0", "js-yaml": "^4.1.1", From 438c6e727374010dde02b5d6ed77705e5c34849d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 3 Apr 2026 11:56:13 +0000 Subject: [PATCH 10/46] build(deps-dev): bump typescript from 5.9.3 to 6.0.2 in /webui Bumps [typescript](https://github.com/microsoft/TypeScript) from 5.9.3 to 6.0.2. - [Release notes](https://github.com/microsoft/TypeScript/releases) - [Commits](https://github.com/microsoft/TypeScript/compare/v5.9.3...v6.0.2) --- updated-dependencies: - dependency-name: typescript dependency-version: 6.0.2 dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- webui/package-lock.json | 277 +++++++++++++++------------------------- webui/package.json | 2 +- 2 files changed, 107 insertions(+), 172 deletions(-) diff --git a/webui/package-lock.json b/webui/package-lock.json index 1653e2bc0..8a4b2e4c8 100644 --- a/webui/package-lock.json +++ b/webui/package-lock.json @@ -52,7 +52,7 @@ "eslint-plugin-vue": "^10.8.0", "npm-run-all": "^4.1.5", "prettier": "^3.8.1", - "typescript": "~5.9.3", + "typescript": "~6.0.2", "vite": "^8.0.1", "vue-tsc": "^3.2.6", "xml2js": "^0.6.2" @@ -198,45 +198,6 @@ "node": "^20.19.0 || ^22.13.0 || >=24" } }, - "node_modules/@eslint/config-array/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/@eslint/config-array/node_modules/brace-expansion": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/@eslint/config-array/node_modules/minimatch": { - "version": "10.2.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/@eslint/config-helpers": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.3.tgz", @@ -897,20 +858,20 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.0.tgz", - "integrity": "sha512-lRyPDLzNCuae71A3t9NEINBiTn7swyOhvUj3MyUOxb8x6g6vPEFoOU+ZRmGMusNC3X3YMhqMIX7i8ShqhT74Pw==", + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.0.tgz", + "integrity": "sha512-RLkVSiNuUP1C2ROIWfqX+YcUfLaSnxGE/8M+Y57lopVwg9VTYYfhuz15Yf1IzCKgZj6/rIbYTmJCUSqr76r0Wg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.56.0", - "@typescript-eslint/type-utils": "8.56.0", - "@typescript-eslint/utils": "8.56.0", - "@typescript-eslint/visitor-keys": "8.56.0", + "@typescript-eslint/scope-manager": "8.58.0", + "@typescript-eslint/type-utils": "8.58.0", + "@typescript-eslint/utils": "8.58.0", + "@typescript-eslint/visitor-keys": "8.58.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", - "ts-api-utils": "^2.4.0" + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -920,9 +881,9 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.56.0", + "@typescript-eslint/parser": "^8.58.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { @@ -936,16 +897,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.0.tgz", - "integrity": "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==", + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.58.0.tgz", + "integrity": "sha512-rLoGZIf9afaRBYsPUMtvkDWykwXwUPL60HebR4JgTI8mxfFe2cQTu3AGitANp4b9B2QlVru6WzjgB2IzJKiCSA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.56.0", - "@typescript-eslint/types": "8.56.0", - "@typescript-eslint/typescript-estree": "8.56.0", - "@typescript-eslint/visitor-keys": "8.56.0", + "@typescript-eslint/scope-manager": "8.58.0", + "@typescript-eslint/types": "8.58.0", + "@typescript-eslint/typescript-estree": "8.58.0", + "@typescript-eslint/visitor-keys": "8.58.0", "debug": "^4.4.3" }, "engines": { @@ -957,18 +918,18 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.0.tgz", - "integrity": "sha512-M3rnyL1vIQOMeWxTWIW096/TtVP+8W3p/XnaFflhmcFp+U4zlxUxWj4XwNs6HbDeTtN4yun0GNTTDBw/SvufKg==", + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.58.0.tgz", + "integrity": "sha512-8Q/wBPWLQP1j16NxoPNIKpDZFMaxl7yWIoqXWYeWO+Bbd2mjgvoF0dxP2jKZg5+x49rgKdf7Ck473M8PC3V9lg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.56.0", - "@typescript-eslint/types": "^8.56.0", + "@typescript-eslint/tsconfig-utils": "^8.58.0", + "@typescript-eslint/types": "^8.58.0", "debug": "^4.4.3" }, "engines": { @@ -979,18 +940,18 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.0.tgz", - "integrity": "sha512-7UiO/XwMHquH+ZzfVCfUNkIXlp/yQjjnlYUyYz7pfvlK3/EyyN6BK+emDmGNyQLBtLGaYrTAI6KOw8tFucWL2w==", + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.58.0.tgz", + "integrity": "sha512-W1Lur1oF50FxSnNdGp3Vs6P+yBRSmZiw4IIjEeYxd8UQJwhUF0gDgDD/W/Tgmh73mxgEU3qX0Bzdl/NGuSPEpQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.56.0", - "@typescript-eslint/visitor-keys": "8.56.0" + "@typescript-eslint/types": "8.58.0", + "@typescript-eslint/visitor-keys": "8.58.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1001,9 +962,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.0.tgz", - "integrity": "sha512-bSJoIIt4o3lKXD3xmDh9chZcjCz5Lk8xS7Rxn+6l5/pKrDpkCwtQNQQwZ2qRPk7TkUYhrq3WPIHXOXlbXP0itg==", + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.58.0.tgz", + "integrity": "sha512-doNSZEVJsWEu4htiVC+PR6NpM+pa+a4ClH9INRWOWCUzMst/VA9c4gXq92F8GUD1rwhNvRLkgjfYtFXegXQF7A==", "dev": true, "license": "MIT", "engines": { @@ -1014,21 +975,21 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.56.0.tgz", - "integrity": "sha512-qX2L3HWOU2nuDs6GzglBeuFXviDODreS58tLY/BALPC7iu3Fa+J7EOTwnX9PdNBxUI7Uh0ntP0YWGnxCkXzmfA==", + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.58.0.tgz", + "integrity": "sha512-aGsCQImkDIqMyx1u4PrVlbi/krmDsQUs4zAcCV6M7yPcPev+RqVlndsJy9kJ8TLihW9TZ0kbDAzctpLn5o+lOg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.56.0", - "@typescript-eslint/typescript-estree": "8.56.0", - "@typescript-eslint/utils": "8.56.0", + "@typescript-eslint/types": "8.58.0", + "@typescript-eslint/typescript-estree": "8.58.0", + "@typescript-eslint/utils": "8.58.0", "debug": "^4.4.3", - "ts-api-utils": "^2.4.0" + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1039,13 +1000,13 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.0.tgz", - "integrity": "sha512-DBsLPs3GsWhX5HylbP9HNG15U0bnwut55Lx12bHB9MpXxQ+R5GC8MwQe+N1UFXxAeQDvEsEDY6ZYwX03K7Z6HQ==", + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.58.0.tgz", + "integrity": "sha512-O9CjxypDT89fbHxRfETNoAnHj/i6IpRK0CvbVN3qibxlLdo5p5hcLmUuCCrHMpxiWSwKyI8mCP7qRNYuOJ0Uww==", "dev": true, "license": "MIT", "engines": { @@ -1057,21 +1018,21 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.0.tgz", - "integrity": "sha512-ex1nTUMWrseMltXUHmR2GAQ4d+WjkZCT4f+4bVsps8QEdh0vlBsaCokKTPlnqBFqqGaxilDNJG7b8dolW2m43Q==", + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.58.0.tgz", + "integrity": "sha512-7vv5UWbHqew/dvs+D3e1RvLv1v2eeZ9txRHPnEEBUgSNLx5ghdzjHa0sgLWYVKssH+lYmV0JaWdoubo0ncGYLA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.56.0", - "@typescript-eslint/tsconfig-utils": "8.56.0", - "@typescript-eslint/types": "8.56.0", - "@typescript-eslint/visitor-keys": "8.56.0", + "@typescript-eslint/project-service": "8.58.0", + "@typescript-eslint/tsconfig-utils": "8.58.0", + "@typescript-eslint/types": "8.58.0", + "@typescript-eslint/visitor-keys": "8.58.0", "debug": "^4.4.3", - "minimatch": "^9.0.5", + "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.4.0" + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1081,20 +1042,20 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/utils": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.0.tgz", - "integrity": "sha512-RZ3Qsmi2nFGsS+n+kjLAYDPVlrzf7UhTffrDIKr+h2yzAlYP/y5ZulU0yeDEPItos2Ph46JAL5P/On3pe7kDIQ==", + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.58.0.tgz", + "integrity": "sha512-RfeSqcFeHMHlAWzt4TBjWOAtoW9lnsAGiP3GbaX9uVgTYYrMbVnGONEfUCiSss+xMHFl+eHZiipmA8WkQ7FuNA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.56.0", - "@typescript-eslint/types": "8.56.0", - "@typescript-eslint/typescript-estree": "8.56.0" + "@typescript-eslint/scope-manager": "8.58.0", + "@typescript-eslint/types": "8.58.0", + "@typescript-eslint/typescript-estree": "8.58.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1105,17 +1066,17 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.0.tgz", - "integrity": "sha512-q+SL+b+05Ud6LbEE35qe4A99P+htKTKVbyiNEe45eCbJFyh/HVK9QXwlrbz+Q4L8SOW4roxSVwXYj4DMBT7Ieg==", + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.0.tgz", + "integrity": "sha512-XJ9UD9+bbDo4a4epraTwG3TsNPeiB9aShrUneAVXy8q4LuwowN+qu89/6ByLMINqvIMeI9H9hOHQtg/ijrYXzQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/types": "8.58.0", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -1127,9 +1088,9 @@ } }, "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.0.tgz", - "integrity": "sha512-A0XeIi7CXU7nPlfHS9loMYEKxUaONu/hTEzHTGba9Huu94Cq1hPivf+DE5erJozZOky0LfvXAyrV/tcswpLI0Q==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1701,13 +1662,26 @@ "license": "MIT" }, "node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/brace-expansion/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" } }, "node_modules/braces": { @@ -2883,29 +2857,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/eslint/node_modules/brace-expansion": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, "node_modules/eslint/node_modules/eslint-visitor-keys": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", @@ -2919,22 +2870,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/minimatch": { - "version": "10.2.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/espree": { "version": "11.2.0", "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", @@ -4711,16 +4646,16 @@ } }, "node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^2.0.2" + "brace-expansion": "^5.0.5" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -6432,9 +6367,9 @@ } }, "node_modules/ts-api-utils": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", - "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", "dev": true, "license": "MIT", "engines": { @@ -6558,9 +6493,9 @@ } }, "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz", + "integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==", "devOptional": true, "license": "Apache-2.0", "bin": { @@ -6572,16 +6507,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.56.0.tgz", - "integrity": "sha512-c7toRLrotJ9oixgdW7liukZpsnq5CZ7PuKztubGYlNppuTqhIoWfhgHo/7EU0v06gS2l/x0i2NEFK1qMIf0rIg==", + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.58.0.tgz", + "integrity": "sha512-e2TQzKfaI85fO+F3QywtX+tCTsu/D3WW5LVU6nz8hTFKFZ8yBJ6mSYRpXqdR3mFjPWmO0eWsTa5f+UpAOe/FMA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.56.0", - "@typescript-eslint/parser": "8.56.0", - "@typescript-eslint/typescript-estree": "8.56.0", - "@typescript-eslint/utils": "8.56.0" + "@typescript-eslint/eslint-plugin": "8.58.0", + "@typescript-eslint/parser": "8.58.0", + "@typescript-eslint/typescript-estree": "8.58.0", + "@typescript-eslint/utils": "8.58.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6592,7 +6527,7 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/uc.micro": { diff --git a/webui/package.json b/webui/package.json index ae877e18b..9773ad359 100644 --- a/webui/package.json +++ b/webui/package.json @@ -61,7 +61,7 @@ "eslint-plugin-vue": "^10.8.0", "npm-run-all": "^4.1.5", "prettier": "^3.8.1", - "typescript": "~5.9.3", + "typescript": "~6.0.2", "vite": "^8.0.1", "vue-tsc": "^3.2.6", "xml2js": "^0.6.2" From f82db9846e157aa238df42aa35165f556d5c6d46 Mon Sep 17 00:00:00 2001 From: maesi Date: Fri, 3 Apr 2026 15:23:15 +0200 Subject: [PATCH 11/46] test: fix paths --- webui/playwright.config.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/webui/playwright.config.ts b/webui/playwright.config.ts index afeba4961..f367bd38f 100644 --- a/webui/playwright.config.ts +++ b/webui/playwright.config.ts @@ -61,7 +61,7 @@ const config: PlaywrightTestConfig = { ] }, }, - testIgnore: ["/e2e/**/*.website.spec.ts"], + testIgnore: ["/e2e/tests/**/*.website.spec.ts"], }, { name: 'dashboard', @@ -81,7 +81,7 @@ const config: PlaywrightTestConfig = { ] }, }, - testIgnore: ["/e2e/**/*.website.spec.ts", "/e2e/dashboard-demo/**/*.spec.ts"], + testIgnore: ["/e2e/tests/**/*.website.spec.ts", "/e2e/tests/dashboard-demo/**/*.spec.ts"], }, { name: 'website', @@ -101,7 +101,7 @@ const config: PlaywrightTestConfig = { ] }, }, - testIgnore: ["/e2e/**/*.dashboard.spec.ts", "/e2e/dashboard/**/*.spec.ts"], + testIgnore: ["/e2e/tests/**/*.dashboard.spec.ts", "/e2e/tests/dashboard/**/*.spec.ts"], }, // { // name: 'firefox', From d906e809eaba812112000fa1fba8a36c30cfd7d8 Mon Sep 17 00:00:00 2001 From: maesi Date: Fri, 3 Apr 2026 15:56:04 +0200 Subject: [PATCH 12/46] chore(webui): remove deprecated baseUrl --- webui/tsconfig.json | 1 - 1 file changed, 1 deletion(-) diff --git a/webui/tsconfig.json b/webui/tsconfig.json index 7ffcc799f..496acdca1 100644 --- a/webui/tsconfig.json +++ b/webui/tsconfig.json @@ -2,7 +2,6 @@ "extends": "@vue/tsconfig/tsconfig.dom.json", "include": ["env.d.ts", "src/**/*", "src/**/*.vue"], "compilerOptions": { - "baseUrl": ".", "paths": { "@/*": ["./src/*"] }, From 0cc71781da0f0ee50b961b90d841204a748cd285 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 3 Apr 2026 15:03:16 +0000 Subject: [PATCH 13/46] build(deps): bump vue from 3.5.30 to 3.5.32 in /webui Bumps [vue](https://github.com/vuejs/core) from 3.5.30 to 3.5.32. - [Release notes](https://github.com/vuejs/core/releases) - [Changelog](https://github.com/vuejs/core/blob/main/CHANGELOG.md) - [Commits](https://github.com/vuejs/core/compare/v3.5.30...v3.5.32) --- updated-dependencies: - dependency-name: vue dependency-version: 3.5.32 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- webui/package-lock.json | 118 ++++++++++++++++++++-------------------- webui/package.json | 2 +- 2 files changed, 60 insertions(+), 60 deletions(-) diff --git a/webui/package-lock.json b/webui/package-lock.json index 4dfdc8d67..307befa38 100644 --- a/webui/package-lock.json +++ b/webui/package-lock.json @@ -32,7 +32,7 @@ "mime-types": "^3.0.2", "ncp": "^2.0.0", "nodemailer": "^8.0.4", - "vue": "^3.5.30", + "vue": "^3.5.32", "vue-router": "^5.0.4", "vue3-ace-editor": "^2.2.4", "whatwg-mimetype": "^5.0.0", @@ -93,9 +93,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", - "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", "license": "MIT", "dependencies": { "@babel/types": "^7.29.0" @@ -1174,39 +1174,39 @@ } }, "node_modules/@vue/compiler-core": { - "version": "3.5.30", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.30.tgz", - "integrity": "sha512-s3DfdZkcu/qExZ+td75015ljzHc6vE+30cFMGRPROYjqkroYI5NV2X1yAMX9UeyBNWB9MxCfPcsjpLS11nzkkw==", + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.32.tgz", + "integrity": "sha512-4x74Tbtqnda8s/NSD6e1Dr5p1c8HdMU5RWSjMSUzb8RTcUQqevDCxVAitcLBKT+ie3o0Dl9crc/S/opJM7qBGQ==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.29.0", - "@vue/shared": "3.5.30", + "@babel/parser": "^7.29.2", + "@vue/shared": "3.5.32", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "node_modules/@vue/compiler-dom": { - "version": "3.5.30", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.30.tgz", - "integrity": "sha512-eCFYESUEVYHhiMuK4SQTldO3RYxyMR/UQL4KdGD1Yrkfdx4m/HYuZ9jSfPdA+nWJY34VWndiYdW/wZXyiPEB9g==", + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.32.tgz", + "integrity": "sha512-ybHAu70NtiEI1fvAUz3oXZqkUYEe5J98GjMDpTGl5iHb0T15wQYLR4wE3h9xfuTNA+Cm2f4czfe8B4s+CCH57Q==", "license": "MIT", "dependencies": { - "@vue/compiler-core": "3.5.30", - "@vue/shared": "3.5.30" + "@vue/compiler-core": "3.5.32", + "@vue/shared": "3.5.32" } }, "node_modules/@vue/compiler-sfc": { - "version": "3.5.30", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.30.tgz", - "integrity": "sha512-LqmFPDn89dtU9vI3wHJnwaV6GfTRD87AjWpTWpyrdVOObVtjIuSeZr181z5C4PmVx/V3j2p+0f7edFKGRMpQ5A==", + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.32.tgz", + "integrity": "sha512-8UYUYo71cP/0YHMO814TRZlPuUUw3oifHuMR7Wp9SNoRSrxRQnhMLNlCeaODNn6kNTJsjFoQ/kqIj4qGvya4Xg==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.29.0", - "@vue/compiler-core": "3.5.30", - "@vue/compiler-dom": "3.5.30", - "@vue/compiler-ssr": "3.5.30", - "@vue/shared": "3.5.30", + "@babel/parser": "^7.29.2", + "@vue/compiler-core": "3.5.32", + "@vue/compiler-dom": "3.5.32", + "@vue/compiler-ssr": "3.5.32", + "@vue/shared": "3.5.32", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", "postcss": "^8.5.8", @@ -1214,13 +1214,13 @@ } }, "node_modules/@vue/compiler-ssr": { - "version": "3.5.30", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.30.tgz", - "integrity": "sha512-NsYK6OMTnx109PSL2IAyf62JP6EUdk4Dmj6AkWcJGBvN0dQoMYtVekAmdqgTtWQgEJo+Okstbf/1p7qZr5H+bA==", + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.32.tgz", + "integrity": "sha512-Gp4gTs22T3DgRotZ8aA/6m2jMR+GMztvBXUBEUOYOcST+giyGWJ4WvFd7QLHBkzTxkfOt8IELKNdpzITLbA2rw==", "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.30", - "@vue/shared": "3.5.30" + "@vue/compiler-dom": "3.5.32", + "@vue/shared": "3.5.32" } }, "node_modules/@vue/devtools-api": { @@ -1327,53 +1327,53 @@ } }, "node_modules/@vue/reactivity": { - "version": "3.5.30", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.30.tgz", - "integrity": "sha512-179YNgKATuwj9gB+66snskRDOitDiuOZqkYia7mHKJaidOMo/WJxHKF8DuGc4V4XbYTJANlfEKb0yxTQotnx4Q==", + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.32.tgz", + "integrity": "sha512-/ORasxSGvZ6MN5gc+uE364SxFdJ0+WqVG0CENXaGW58TOCdrAW76WWaplDtECeS1qphvtBZtR+3/o1g1zL4xPQ==", "license": "MIT", "dependencies": { - "@vue/shared": "3.5.30" + "@vue/shared": "3.5.32" } }, "node_modules/@vue/runtime-core": { - "version": "3.5.30", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.30.tgz", - "integrity": "sha512-e0Z+8PQsUTdwV8TtEsLzUM7SzC7lQwYKePydb7K2ZnmS6jjND+WJXkmmfh/swYzRyfP1EY3fpdesyYoymCzYfg==", + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.32.tgz", + "integrity": "sha512-pDrXCejn4UpFDFmMd27AcJEbHaLemaE5o4pbb7sLk79SRIhc6/t34BQA7SGNgYtbMnvbF/HHOftYBgFJtUoJUQ==", "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.30", - "@vue/shared": "3.5.30" + "@vue/reactivity": "3.5.32", + "@vue/shared": "3.5.32" } }, "node_modules/@vue/runtime-dom": { - "version": "3.5.30", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.30.tgz", - "integrity": "sha512-2UIGakjU4WSQ0T4iwDEW0W7vQj6n7AFn7taqZ9Cvm0Q/RA2FFOziLESrDL4GmtI1wV3jXg5nMoJSYO66egDUBw==", + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.32.tgz", + "integrity": "sha512-1CDVv7tv/IV13V8Nip1k/aaObVbWqRlVCVezTwx3K07p7Vxossp5JU1dcPNhJk3w347gonIUT9jQOGutyJrSVQ==", "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.30", - "@vue/runtime-core": "3.5.30", - "@vue/shared": "3.5.30", + "@vue/reactivity": "3.5.32", + "@vue/runtime-core": "3.5.32", + "@vue/shared": "3.5.32", "csstype": "^3.2.3" } }, "node_modules/@vue/server-renderer": { - "version": "3.5.30", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.30.tgz", - "integrity": "sha512-v+R34icapydRwbZRD0sXwtHqrQJv38JuMB4JxbOxd8NEpGLny7cncMp53W9UH/zo4j8eDHjQ1dEJXwzFQknjtQ==", + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.32.tgz", + "integrity": "sha512-IOjm2+JQwRFS7W28HNuJeXQle9KdZbODFY7hFGVtnnghF51ta20EWAZJHX+zLGtsHhaU6uC9BGPV52KVpYryMQ==", "license": "MIT", "dependencies": { - "@vue/compiler-ssr": "3.5.30", - "@vue/shared": "3.5.30" + "@vue/compiler-ssr": "3.5.32", + "@vue/shared": "3.5.32" }, "peerDependencies": { - "vue": "3.5.30" + "vue": "3.5.32" } }, "node_modules/@vue/shared": { - "version": "3.5.30", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.30.tgz", - "integrity": "sha512-YXgQ7JjaO18NeK2K9VTbDHaFy62WrObMa6XERNfNOkAhD1F1oDSf3ZJ7K6GqabZ0BvSDHajp8qfS5Sa2I9n8uQ==", + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.32.tgz", + "integrity": "sha512-ksNyrmRQzWJJ8n3cRDuSF7zNNontuJg1YHnmWRJd2AMu8Ij2bqwiiri2lH5rHtYPZjj4STkNcgcmiQqlOjiYGg==", "license": "MIT" }, "node_modules/@vue/tsconfig": { @@ -6793,16 +6793,16 @@ "license": "MIT" }, "node_modules/vue": { - "version": "3.5.30", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.30.tgz", - "integrity": "sha512-hTHLc6VNZyzzEH/l7PFGjpcTvUgiaPK5mdLkbjrTeWSRcEfxFrv56g/XckIYlE9ckuobsdwqd5mk2g1sBkMewg==", + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.32.tgz", + "integrity": "sha512-vM4z4Q9tTafVfMAK7IVzmxg34rSzTFMyIe0UUEijUCkn9+23lj0WRfA83dg7eQZIUlgOSGrkViIaCfqSAUXsMw==", "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.30", - "@vue/compiler-sfc": "3.5.30", - "@vue/runtime-dom": "3.5.30", - "@vue/server-renderer": "3.5.30", - "@vue/shared": "3.5.30" + "@vue/compiler-dom": "3.5.32", + "@vue/compiler-sfc": "3.5.32", + "@vue/runtime-dom": "3.5.32", + "@vue/server-renderer": "3.5.32", + "@vue/shared": "3.5.32" }, "peerDependencies": { "typescript": "*" diff --git a/webui/package.json b/webui/package.json index 1f4c6c4d3..3ca4e8d4b 100644 --- a/webui/package.json +++ b/webui/package.json @@ -41,7 +41,7 @@ "mime-types": "^3.0.2", "ncp": "^2.0.0", "nodemailer": "^8.0.4", - "vue": "^3.5.30", + "vue": "^3.5.32", "vue-router": "^5.0.4", "vue3-ace-editor": "^2.2.4", "whatwg-mimetype": "^5.0.0", From 7ec9a91395013f1c46e4810e38a5ef889b5333d9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 3 Apr 2026 15:10:54 +0000 Subject: [PATCH 14/46] build(deps-dev): bump vite from 8.0.1 to 8.0.3 in /webui Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 8.0.1 to 8.0.3. - [Release notes](https://github.com/vitejs/vite/releases) - [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite/commits/create-vite@8.0.3/packages/vite) --- updated-dependencies: - dependency-name: vite dependency-version: 8.0.3 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- webui/package-lock.json | 195 ++++++++++++++++++++-------------------- webui/package.json | 2 +- 2 files changed, 101 insertions(+), 96 deletions(-) diff --git a/webui/package-lock.json b/webui/package-lock.json index 307befa38..a7ba123ad 100644 --- a/webui/package-lock.json +++ b/webui/package-lock.json @@ -53,7 +53,7 @@ "npm-run-all": "^4.1.5", "prettier": "^3.8.1", "typescript": "~6.0.2", - "vite": "^8.0.1", + "vite": "^8.0.3", "vue-tsc": "^3.2.6", "xml2js": "^0.6.2" } @@ -121,35 +121,38 @@ } }, "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==", + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", + "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==", "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { - "@emnapi/wasi-threads": "1.2.0", + "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "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==", + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", + "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "tslib": "^2.4.0" } }, "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==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "tslib": "^2.4.0" } @@ -346,20 +349,22 @@ } }, "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==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.2.tgz", + "integrity": "sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw==", "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" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" } }, "node_modules/@nodelib/fs.scandir": { @@ -398,9 +403,9 @@ } }, "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==", + "version": "0.122.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.122.0.tgz", + "integrity": "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==", "dev": true, "license": "MIT", "funding": { @@ -447,9 +452,9 @@ } }, "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==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.12.tgz", + "integrity": "sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==", "cpu": [ "arm64" ], @@ -464,9 +469,9 @@ } }, "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==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.12.tgz", + "integrity": "sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==", "cpu": [ "arm64" ], @@ -481,9 +486,9 @@ } }, "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==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.12.tgz", + "integrity": "sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==", "cpu": [ "x64" ], @@ -498,9 +503,9 @@ } }, "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==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.12.tgz", + "integrity": "sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==", "cpu": [ "x64" ], @@ -515,9 +520,9 @@ } }, "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==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.12.tgz", + "integrity": "sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==", "cpu": [ "arm" ], @@ -532,9 +537,9 @@ } }, "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==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==", "cpu": [ "arm64" ], @@ -549,9 +554,9 @@ } }, "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==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.12.tgz", + "integrity": "sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==", "cpu": [ "arm64" ], @@ -566,9 +571,9 @@ } }, "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==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==", "cpu": [ "ppc64" ], @@ -583,9 +588,9 @@ } }, "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==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==", "cpu": [ "s390x" ], @@ -600,9 +605,9 @@ } }, "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==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==", "cpu": [ "x64" ], @@ -617,9 +622,9 @@ } }, "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==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.12.tgz", + "integrity": "sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==", "cpu": [ "x64" ], @@ -634,9 +639,9 @@ } }, "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==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.12.tgz", + "integrity": "sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==", "cpu": [ "arm64" ], @@ -651,9 +656,9 @@ } }, "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==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.12.tgz", + "integrity": "sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==", "cpu": [ "wasm32" ], @@ -668,9 +673,9 @@ } }, "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==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.12.tgz", + "integrity": "sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==", "cpu": [ "arm64" ], @@ -685,9 +690,9 @@ } }, "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==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.12.tgz", + "integrity": "sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==", "cpu": [ "x64" ], @@ -5693,14 +5698,14 @@ "license": "Unlicense" }, "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==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.12.tgz", + "integrity": "sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.120.0", - "@rolldown/pluginutils": "1.0.0-rc.10" + "@oxc-project/types": "=0.122.0", + "@rolldown/pluginutils": "1.0.0-rc.12" }, "bin": { "rolldown": "bin/cli.mjs" @@ -5709,27 +5714,27 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@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" + "@rolldown/binding-android-arm64": "1.0.0-rc.12", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.12", + "@rolldown/binding-darwin-x64": "1.0.0-rc.12", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.12", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.12", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.12", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.12", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.12", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.12", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.12", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.12" } }, "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==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.12.tgz", + "integrity": "sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==", "dev": true, "license": "MIT" }, @@ -6680,16 +6685,16 @@ } }, "node_modules/vite": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.1.tgz", - "integrity": "sha512-wt+Z2qIhfFt85uiyRt5LPU4oVEJBXj8hZNWKeqFG4gRG/0RaRGJ7njQCwzFVjO+v4+Ipmf5CY7VdmZRAYYBPHw==", + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.3.tgz", + "integrity": "sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==", "dev": true, "license": "MIT", "dependencies": { "lightningcss": "^1.32.0", - "picomatch": "^4.0.3", + "picomatch": "^4.0.4", "postcss": "^8.5.8", - "rolldown": "1.0.0-rc.10", + "rolldown": "1.0.0-rc.12", "tinyglobby": "^0.2.15" }, "bin": { @@ -6773,9 +6778,9 @@ } }, "node_modules/vite/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { diff --git a/webui/package.json b/webui/package.json index 3ca4e8d4b..72efdc60e 100644 --- a/webui/package.json +++ b/webui/package.json @@ -62,7 +62,7 @@ "npm-run-all": "^4.1.5", "prettier": "^3.8.1", "typescript": "~6.0.2", - "vite": "^8.0.1", + "vite": "^8.0.3", "vue-tsc": "^3.2.6", "xml2js": "^0.6.2" } From 4c23f56b936c9b55abd62899ffc69ea2fde7b179 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 3 Apr 2026 15:41:11 +0000 Subject: [PATCH 15/46] build(deps): bump github.com/evanw/esbuild from 0.27.4 to 0.28.0 Bumps [github.com/evanw/esbuild](https://github.com/evanw/esbuild) from 0.27.4 to 0.28.0. - [Release notes](https://github.com/evanw/esbuild/releases) - [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md) - [Commits](https://github.com/evanw/esbuild/compare/v0.27.4...v0.28.0) --- updated-dependencies: - dependency-name: github.com/evanw/esbuild dependency-version: 0.28.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 9cc3c06d9..749882473 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/bradleyfalzon/ghinstallation/v2 v2.18.0 github.com/brianvoe/gofakeit/v6 v6.28.0 github.com/dop251/goja v0.0.0-20250309171923-bcd7cc6bf64c - github.com/evanw/esbuild v0.27.4 + github.com/evanw/esbuild v0.28.0 github.com/fsnotify/fsnotify v1.9.0 github.com/go-co-op/gocron v1.37.0 github.com/go-git/go-git/v5 v5.17.2 diff --git a/go.sum b/go.sum index 3e675d8a3..d475c825b 100644 --- a/go.sum +++ b/go.sum @@ -81,8 +81,8 @@ github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= -github.com/evanw/esbuild v0.27.4 h1:8opEixKkH9EDsdjxC/aPmpk1KPwQOcyknDo5m5xIFxI= -github.com/evanw/esbuild v0.27.4/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48= +github.com/evanw/esbuild v0.28.0 h1:V96ghtc5p5JnNUQIUsc5H3kr+AcFcMqOJll2ZmJW6Lo= +github.com/evanw/esbuild v0.28.0/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= From 1df7705d877b33f5d0f5e82febf71f862485f464 Mon Sep 17 00:00:00 2001 From: maesi Date: Sun, 5 Apr 2026 12:02:48 +0200 Subject: [PATCH 16/46] fix(swagger): convert responses and parameters style(dashboard): improve display markdown in HTTP tables feat(dashboard): summary takes precedence over description in HTTP service view --- api/handler_http.go | 4 ++ api/handler_http_test.go | 13 ++++ providers/openapi/info.go | 2 + providers/openapi/openapitest/config.go | 6 ++ providers/swagger/convert.go | 19 ++++++ providers/swagger/convert_test.go | 63 +++++++++++++++++++ webui/src/assets/dashboard.css | 42 +++++++++++++ .../dashboard/http/HttpServicesCard.vue | 21 ++++--- .../dashboard/kafka/KafkaServicesCard.vue | 2 +- .../dashboard/kafka/KafkaTopics.vue | 2 +- .../dashboard/ldap/LdapServicesCard.vue | 2 +- .../dashboard/mail/MailServicesCard.vue | 2 +- .../components/dashboard/mail/Mailboxes.vue | 2 +- 13 files changed, 166 insertions(+), 14 deletions(-) diff --git a/api/handler_http.go b/api/handler_http.go index f6134df5a..f6adb8646 100644 --- a/api/handler_http.go +++ b/api/handler_http.go @@ -112,6 +112,10 @@ func getHttpServices(list []*runtime.HttpInfo, m *monitor.Monitor) []interface{} Type: ServiceHttp, } + if hs.Info.Summary != "" { + s.Description = hs.Info.Summary + } + if m != nil { s.Metrics = m.FindAll(metrics.ByNamespace("http"), metrics.ByLabel("service", hs.Info.Name)) } diff --git a/api/handler_http_test.go b/api/handler_http_test.go index d303d9774..0b752a6b7 100644 --- a/api/handler_http_test.go +++ b/api/handler_http_test.go @@ -41,6 +41,19 @@ func TestHandler_Http(t *testing.T) { requestUrl: "http://foo.api/api/services", responseBody: `[{"name":"foo","description":"bar","version":"1.0","type":"http"}`, }, + { + name: "summary takes precedence over description", + app: func() *runtime.App { + return runtimetest.NewHttpApp( + openapitest.NewConfig("3.0.0", + openapitest.WithInfo("foo", "1.0", "bar"), + openapitest.WithSummary("summary"), + ), + ) + }, + requestUrl: "http://foo.api/api/services", + responseBody: `[{"name":"foo","description":"summary","version":"1.0","type":"http"}`, + }, { name: "get http services with contact", app: func() *runtime.App { diff --git a/providers/openapi/info.go b/providers/openapi/info.go index c00d999e1..ece13edfd 100644 --- a/providers/openapi/info.go +++ b/providers/openapi/info.go @@ -4,6 +4,8 @@ type Info struct { // The title of the service Name string `yaml:"title" json:"title"` + Summary string `yaml:"summary" json:"summary"` + // A short description of the API. CommonMark syntax MAY be // used for rich text representation. Description string `yaml:"description,omitempty" json:"description,omitempty"` diff --git a/providers/openapi/openapitest/config.go b/providers/openapi/openapitest/config.go index 7e6bd5493..01867547d 100644 --- a/providers/openapi/openapitest/config.go +++ b/providers/openapi/openapitest/config.go @@ -29,6 +29,12 @@ func WithInfo(name, version, description string) ConfigOptions { } } +func WithSummary(summary string) ConfigOptions { + return func(c *openapi.Config) { + c.Info.Summary = summary + } +} + func WithContact(name, url, email string) ConfigOptions { return func(c *openapi.Config) { c.Info.Contact = &openapi.Contact{ diff --git a/providers/swagger/convert.go b/providers/swagger/convert.go index d142f354d..2738a9743 100644 --- a/providers/swagger/convert.go +++ b/providers/swagger/convert.go @@ -50,6 +50,25 @@ func (c *converter) Convert() (*openapi.Config, error) { result.Paths[path] = converted } + if len(c.config.Responses) > 0 { + result.Components.Responses = map[string]*openapi.ResponseRef{} + for name, res := range c.config.Responses { + r, err := c.convertResponse(res, c.config.Produces) + if err != nil { + return nil, err + } + result.Components.Responses[name] = r + } + } + + if len(c.config.Parameters) > 0 { + result.Components.Parameters = map[string]*openapi.ParameterRef{} + for name, param := range c.config.Parameters { + p := convertParameter(param) + result.Components.Parameters[name] = p + } + } + if len(c.config.Definitions) > 0 { result.Components.Schemas = &schema.Schemas{} for k, v := range c.config.Definitions { diff --git a/providers/swagger/convert_test.go b/providers/swagger/convert_test.go index 07ae1da7c..fd7732c51 100644 --- a/providers/swagger/convert_test.go +++ b/providers/swagger/convert_test.go @@ -8,6 +8,7 @@ import ( "testing" "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" ) func TestConvert(t *testing.T) { @@ -548,3 +549,65 @@ func TestConvert(t *testing.T) { }) } } + +func TestConvert_YAML(t *testing.T) { + testcases := []struct { + name string + config string + test func(t *testing.T, config *openapi.Config) + }{ + { + name: "description", + config: `swagger: '2.0' +info: + description: foo +`, + test: func(t *testing.T, config *openapi.Config) { + require.Equal(t, "foo", config.Info.Description) + }, + }, + { + name: "response reference", + config: `swagger: '2.0' +responses: + 401: + description: foo +`, + test: func(t *testing.T, config *openapi.Config) { + res, found := config.Components.Responses["401"] + require.True(t, found) + require.NotNil(t, res.Value) + require.Equal(t, "foo", res.Value.Description) + }, + }, + { + name: "parameters reference", + config: `swagger: '2.0' +parameters: + foo: + name: skip +`, + test: func(t *testing.T, config *openapi.Config) { + p, found := config.Components.Parameters["foo"] + require.True(t, found) + require.NotNil(t, p.Value) + require.Equal(t, "skip", p.Value.Name) + }, + }, + } + + t.Parallel() + for _, tc := range testcases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + c := &Config{} + err := yaml.Unmarshal([]byte(tc.config), &c) + require.NoError(t, err) + converted, err := Convert(c) + require.NoError(t, err) + tc.test(t, converted) + }) + } +} diff --git a/webui/src/assets/dashboard.css b/webui/src/assets/dashboard.css index c4f7c4d3b..92326061f 100644 --- a/webui/src/assets/dashboard.css +++ b/webui/src/assets/dashboard.css @@ -188,4 +188,46 @@ .dashboard-tabs.demo { margin-top: 0.8rem; } +} + +.table-markdown { + display: -webkit-box; + -webkit-line-clamp: 3; + line-clamp: 3; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.table-markdown * { + margin: 0; + padding: 0; +} + +.table-markdown h1, +.table-markdown h2, +.table-markdown h3, +.table-markdown h4 { + font-size: inherit; + font-weight: 600; +} + +.table-markdown p { + display: inline; +} + +.table-markdown ul, +.table-markdown ol { + display: inline; + list-style: none; +} + +.table-markdown li::before { + content: "• "; +} + +.table-markdown code { + font-size: 0.9em; + background: #f5f5f5; + padding: 1px 4px; + border-radius: 3px; } \ No newline at end of file diff --git a/webui/src/components/dashboard/http/HttpServicesCard.vue b/webui/src/components/dashboard/http/HttpServicesCard.vue index 7ae91c159..424efe870 100644 --- a/webui/src/components/dashboard/http/HttpServicesCard.vue +++ b/webui/src/components/dashboard/http/HttpServicesCard.vue @@ -2,7 +2,7 @@ import { useMetrics } from '@/composables/metrics'; import { usePrettyDates } from '@/composables/usePrettyDate'; import { useRoute } from '@/router'; -import { computed, onUnmounted } from 'vue'; +import { onUnmounted } from 'vue'; import { useDashboard } from '@/composables/dashboard'; import { useMarkdown } from '@/composables/markdown'; @@ -12,19 +12,19 @@ const { service: serviceRoute, router } = useRoute() const { dashboard } = useDashboard() const { services, close } = dashboard.value.getServices('http') -function lastRequest(s: Service){ +function lastRequest(s: Service) { const n = max(s.metrics, 'http_request_timestamp') - if (n == 0){ + if (n == 0) { return '-' } return format(n) } -function requests(s: Service){ +function requests(s: Service) { return sum(s.metrics, 'http_requests_total') } -function errors(s: Service){ +function errors(s: Service) { return sum(s.metrics, 'http_requests_errors_total') } @@ -62,18 +62,21 @@ onUnmounted(() => { - + - {{ service.name }} + {{ service.name }} -
+ +
+ {{ lastRequest(service) }} {{ requests(service) }} / - {{ errors(service) }} + {{ errors(service) }} diff --git a/webui/src/components/dashboard/kafka/KafkaServicesCard.vue b/webui/src/components/dashboard/kafka/KafkaServicesCard.vue index bd727cb3a..08c446785 100644 --- a/webui/src/components/dashboard/kafka/KafkaServicesCard.vue +++ b/webui/src/components/dashboard/kafka/KafkaServicesCard.vue @@ -67,7 +67,7 @@ onUnmounted(() => { {{ service.name }} -
+
{{ lastMessage(service) }} {{ messages(service) }} diff --git a/webui/src/components/dashboard/kafka/KafkaTopics.vue b/webui/src/components/dashboard/kafka/KafkaTopics.vue index 727d7f4bf..28dd9b85f 100644 --- a/webui/src/components/dashboard/kafka/KafkaTopics.vue +++ b/webui/src/components/dashboard/kafka/KafkaTopics.vue @@ -167,7 +167,7 @@ function toggleTag(name: string) { -
+
{{ lastMessage(service, topic) }} {{ messages(service, topic) }} diff --git a/webui/src/components/dashboard/ldap/LdapServicesCard.vue b/webui/src/components/dashboard/ldap/LdapServicesCard.vue index 4a8afd7d6..73380144b 100644 --- a/webui/src/components/dashboard/ldap/LdapServicesCard.vue +++ b/webui/src/components/dashboard/ldap/LdapServicesCard.vue @@ -66,7 +66,7 @@ onUnmounted(() => { {{ service.name }} -
+
{{ lastRequest(service) }} {{ requests(service) }} diff --git a/webui/src/components/dashboard/mail/MailServicesCard.vue b/webui/src/components/dashboard/mail/MailServicesCard.vue index 9c95148c2..561ca0aae 100644 --- a/webui/src/components/dashboard/mail/MailServicesCard.vue +++ b/webui/src/components/dashboard/mail/MailServicesCard.vue @@ -67,7 +67,7 @@ onUnmounted(() => { {{ service.name }} -
+
{{ lastMail(service) }} {{ messages(service) }} diff --git a/webui/src/components/dashboard/mail/Mailboxes.vue b/webui/src/components/dashboard/mail/Mailboxes.vue index d5431ac9f..903805b84 100644 --- a/webui/src/components/dashboard/mail/Mailboxes.vue +++ b/webui/src/components/dashboard/mail/Mailboxes.vue @@ -73,7 +73,7 @@ function goToMailbox(mb: SmtpMailbox, openInNewTab = false){ {{ mb.username }} {{ mb.password }} -
+
{{ mb.numMessages }} From 20cc96d17198e6c0188d23d426f51a69e45a0678 Mon Sep 17 00:00:00 2001 From: marle3003 Date: Tue, 7 Apr 2026 22:11:14 +0200 Subject: [PATCH 17/46] fix(json reference): fix resolving json reference after patching which caused a nil pointer panic --- config/dynamic/resolve.go | 4 + providers/asyncapi3/asyncapi3test/message.go | 12 +++ runtime/runtime.go | 2 +- runtime/runtime_kafka_test.go | 77 +++++++++++++++++++- 4 files changed, 93 insertions(+), 2 deletions(-) diff --git a/config/dynamic/resolve.go b/config/dynamic/resolve.go index 4aab048a2..f9b8df6c9 100644 --- a/config/dynamic/resolve.go +++ b/config/dynamic/resolve.go @@ -204,6 +204,10 @@ func resolveUrl(ref string, cfg *Config) (*url.URL, error) { } info := cfg.Info.Kernel() + if info.Url == nil { + return u, nil + } + if len(info.Url.Opaque) > 0 { p := filepath.Join(filepath.Dir(info.Url.Opaque), u.Path) p = fmt.Sprintf("file:%v", p) diff --git a/providers/asyncapi3/asyncapi3test/message.go b/providers/asyncapi3/asyncapi3test/message.go index 3a1b1cd81..ca58b35a4 100644 --- a/providers/asyncapi3/asyncapi3test/message.go +++ b/providers/asyncapi3/asyncapi3test/message.go @@ -72,3 +72,15 @@ func WithHeaders(s *schema.Schema) MessageOptions { m.Headers = &asyncapi3.SchemaRef{Value: s} } } + +func WithMessageTitle(s string) MessageOptions { + return func(m *asyncapi3.Message) { + m.Title = s + } +} + +func WithMessageSummary(s string) MessageOptions { + return func(m *asyncapi3.Message) { + m.Summary = s + } +} diff --git a/runtime/runtime.go b/runtime/runtime.go index cbb5ae2a4..29f9c70c4 100644 --- a/runtime/runtime.go +++ b/runtime/runtime.go @@ -53,7 +53,7 @@ func New(cfg *static.Config, reader dynamic.Reader) *App { Events: em, Configs: map[string]*dynamic.Config{}, http: &HttpStore{cfg: cfg, index: index, events: em, reader: reader}, - Kafka: &KafkaStore{monitor: m, cfg: cfg, index: index, events: em}, + Kafka: &KafkaStore{monitor: m, cfg: cfg, index: index, events: em, reader: reader}, Mqtt: &MqttStore{monitor: m, cfg: cfg, sm: em}, Ldap: &LdapStore{cfg: cfg, events: em, index: index}, Mail: &MailStore{cfg: cfg, sm: em, index: index}, diff --git a/runtime/runtime_kafka_test.go b/runtime/runtime_kafka_test.go index 5f90b3376..911a276f2 100644 --- a/runtime/runtime_kafka_test.go +++ b/runtime/runtime_kafka_test.go @@ -148,6 +148,7 @@ func TestApp_AddKafka_Patching(t *testing.T) { testcases := []struct { name string configs []*dynamic.Config + reader dynamic.Reader test func(t *testing.T, app *runtime.App) }{ { @@ -244,12 +245,86 @@ func TestApp_AddKafka_Patching(t *testing.T) { require.Equal(t, "patch", msg.Value.Summary) }, }, + { + name: "using relative file path", + reader: &dynamictest.Reader{ + Data: map[string]*dynamic.Config{ + "bar.yaml": { + Data: &asyncapi3.MessageRef{ + Value: asyncapi3test.NewMessage( + asyncapi3test.WithMessageTitle("original"), + ), + }, + }, + }, + }, + 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: "bar.yaml"}}, + ), + ), + )), + getConfig("https://mokapi.io/b", asyncapi3test.NewConfig( + asyncapi3test.WithInfo("foo", "bar", ""), + )), + }, + test: func(t *testing.T, app *runtime.App) { + info := app.Kafka.Get("foo") + ch := info.Channels["bar"] + require.NotNil(t, ch) + msg := ch.Value.Messages["bar"] + require.NotNil(t, msg) + require.Equal(t, "original", msg.Value.Title) + }, + }, + { + name: "using relative u path", + reader: &dynamictest.Reader{ + Data: map[string]*dynamic.Config{ + "bar.yaml": { + Data: &asyncapi3.MessageRef{ + Value: asyncapi3test.NewMessage( + asyncapi3test.WithMessageTitle("original"), + ), + }, + }, + }, + }, + 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: "bar.yaml"}}, + ), + ), + )), + getConfig("https://mokapi.io/b", asyncapi3test.NewConfig( + asyncapi3test.WithInfo("foo", "bar", ""), + )), + }, + test: func(t *testing.T, app *runtime.App) { + info := app.Kafka.Get("foo") + ch := info.Channels["bar"] + require.NotNil(t, ch) + msg := ch.Value.Messages["bar"] + require.NotNil(t, msg) + require.Equal(t, "original", msg.Value.Title) + }, + }, } for _, tc := range testcases { tc := tc t.Run(tc.name, func(t *testing.T) { + r := tc.reader + if r == nil { + r = &dynamictest.Reader{} + } cfg := &static.Config{} - app := runtime.New(cfg, &dynamictest.Reader{}) + app := runtime.New(cfg, r) for _, c := range tc.configs { _, err := app.Kafka.Add(c, enginetest.NewEngine()) require.NoError(t, err) From 68ee1485c1dc5b8ba972643b52e85eb288bf0c4f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 Apr 2026 07:44:10 +0000 Subject: [PATCH 18/46] build(deps): bump fuse.js from 7.2.0 to 7.3.0 in /webui Bumps [fuse.js](https://github.com/krisk/Fuse) from 7.2.0 to 7.3.0. - [Release notes](https://github.com/krisk/Fuse/releases) - [Changelog](https://github.com/krisk/Fuse/blob/main/CHANGELOG.md) - [Commits](https://github.com/krisk/Fuse/compare/v7.2.0...v7.3.0) --- updated-dependencies: - dependency-name: fuse.js dependency-version: 7.3.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- webui/package-lock.json | 12 ++++++++---- webui/package.json | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/webui/package-lock.json b/webui/package-lock.json index a7ba123ad..60be58af1 100644 --- a/webui/package-lock.json +++ b/webui/package-lock.json @@ -21,7 +21,7 @@ "dayjs": "^1.11.20", "del-cli": "^7.0.0", "express": "^5.2.1", - "fuse.js": "^7.2.0", + "fuse.js": "^7.3.0", "highlight.js": "^11.11.1", "http-status-codes": "^2.3.0", "js-yaml": "^4.1.1", @@ -3255,12 +3255,16 @@ } }, "node_modules/fuse.js": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.2.0.tgz", - "integrity": "sha512-zf4vdcIGpjNKTuXwug33Hm2okqX6a0t2ZEbez+o9oBJQSNhVJ5AqERfeiRD3r8HcLqP66MrjdkmzxrncbAOTUQ==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.3.0.tgz", + "integrity": "sha512-plz8RVjfcDedTGfVngWH1jmJvBvAwi1v2jecfDerbEnMcmOYUEEwKFTHbNoCiYyzaK2Ws8lABkTCcRSqCY1q4w==", "license": "Apache-2.0", "engines": { "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/krisk" } }, "node_modules/generator-function": { diff --git a/webui/package.json b/webui/package.json index 72efdc60e..e5b20b8d6 100644 --- a/webui/package.json +++ b/webui/package.json @@ -30,7 +30,7 @@ "dayjs": "^1.11.20", "del-cli": "^7.0.0", "express": "^5.2.1", - "fuse.js": "^7.2.0", + "fuse.js": "^7.3.0", "highlight.js": "^11.11.1", "http-status-codes": "^2.3.0", "js-yaml": "^4.1.1", From e0f641719145a78d55dff4f3820da7646da92b73 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 Apr 2026 07:51:57 +0000 Subject: [PATCH 19/46] build(deps-dev): bump eslint from 10.1.0 to 10.2.0 in /webui Bumps [eslint](https://github.com/eslint/eslint) from 10.1.0 to 10.2.0. - [Release notes](https://github.com/eslint/eslint/releases) - [Commits](https://github.com/eslint/eslint/compare/v10.1.0...v10.2.0) --- updated-dependencies: - dependency-name: eslint dependency-version: 10.2.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- webui/package-lock.json | 52 ++++++++++++++++++++--------------------- webui/package.json | 2 +- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/webui/package-lock.json b/webui/package-lock.json index 60be58af1..0ccc8e7bd 100644 --- a/webui/package-lock.json +++ b/webui/package-lock.json @@ -48,7 +48,7 @@ "@vue/eslint-config-prettier": "^10.2.0", "@vue/eslint-config-typescript": "^14.7.0", "@vue/tsconfig": "^0.9.1", - "eslint": "^10.1.0", + "eslint": "^10.2.0", "eslint-plugin-vue": "^10.8.0", "npm-run-all": "^4.1.5", "prettier": "^3.8.1", @@ -187,13 +187,13 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.23.3", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.3.tgz", - "integrity": "sha512-j+eEWmB6YYLwcNOdlwQ6L2OsptI/LO6lNBuLIqe5R7RetD658HLoF+Mn7LzYmAWWNNzdC6cqP+L6r8ujeYXWLw==", + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^3.0.3", + "@eslint/object-schema": "^3.0.5", "debug": "^4.3.1", "minimatch": "^10.2.4" }, @@ -202,22 +202,22 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.3.tgz", - "integrity": "sha512-lzGN0onllOZCGroKJmRwY6QcEHxbjBw1gwB8SgRSqK8YbbtEXMvKynsXc3553ckIEBxsbMBU7oOZXKIPGZNeZw==", + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.5.tgz", + "integrity": "sha512-eIJYKTCECbP/nsKaaruF6LW967mtbQbsw4JTtSVkUQc9MneSkbrgPJAbKl9nWr0ZeowV8BfsarBmPpBzGelA2w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^1.1.1" + "@eslint/core": "^1.2.1" }, "engines": { "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@eslint/core": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.1.1.tgz", - "integrity": "sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -228,9 +228,9 @@ } }, "node_modules/@eslint/object-schema": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.3.tgz", - "integrity": "sha512-iM869Pugn9Nsxbh/YHRqYiqd23AmIbxJOcpUMOuWCVNdoQJ5ZtwL6h3t0bcZzJUlC3Dq9jCFCESBZnX0GTv7iQ==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", "dev": true, "license": "Apache-2.0", "engines": { @@ -238,13 +238,13 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.6.1.tgz", - "integrity": "sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.1.tgz", + "integrity": "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^1.1.1", + "@eslint/core": "^1.2.1", "levn": "^0.4.1" }, "engines": { @@ -2696,18 +2696,18 @@ } }, "node_modules/eslint": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.1.0.tgz", - "integrity": "sha512-S9jlY/ELKEUwwQnqWDO+f+m6sercqOPSqXM5Go94l7DOmxHVDgmSFGWEzeE/gwgTAr0W103BWt0QLe/7mabIvA==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.2.0.tgz", + "integrity": "sha512-+L0vBFYGIpSNIt/KWTpFonPrqYvgKw1eUI5Vn7mEogrQcWtWYtNQ7dNqC+px/J0idT3BAkiWrhfS7k+Tum8TUA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", - "@eslint/config-array": "^0.23.3", - "@eslint/config-helpers": "^0.5.3", - "@eslint/core": "^1.1.1", - "@eslint/plugin-kit": "^0.6.1", + "@eslint/config-array": "^0.23.4", + "@eslint/config-helpers": "^0.5.4", + "@eslint/core": "^1.2.0", + "@eslint/plugin-kit": "^0.7.0", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", diff --git a/webui/package.json b/webui/package.json index e5b20b8d6..a6f93beaa 100644 --- a/webui/package.json +++ b/webui/package.json @@ -57,7 +57,7 @@ "@vue/eslint-config-prettier": "^10.2.0", "@vue/eslint-config-typescript": "^14.7.0", "@vue/tsconfig": "^0.9.1", - "eslint": "^10.1.0", + "eslint": "^10.2.0", "eslint-plugin-vue": "^10.8.0", "npm-run-all": "^4.1.5", "prettier": "^3.8.1", From 51281021e30396a5bc32b27acdfd28e553192a0d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 Apr 2026 07:59:41 +0000 Subject: [PATCH 20/46] build(deps-dev): bump prettier from 3.8.1 to 3.8.2 in /webui Bumps [prettier](https://github.com/prettier/prettier) from 3.8.1 to 3.8.2. - [Release notes](https://github.com/prettier/prettier/releases) - [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md) - [Commits](https://github.com/prettier/prettier/compare/3.8.1...3.8.2) --- updated-dependencies: - dependency-name: prettier dependency-version: 3.8.2 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- webui/package-lock.json | 8 ++++---- webui/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/webui/package-lock.json b/webui/package-lock.json index 0ccc8e7bd..9741fa69f 100644 --- a/webui/package-lock.json +++ b/webui/package-lock.json @@ -51,7 +51,7 @@ "eslint": "^10.2.0", "eslint-plugin-vue": "^10.8.0", "npm-run-all": "^4.1.5", - "prettier": "^3.8.1", + "prettier": "^3.8.2", "typescript": "~6.0.2", "vite": "^8.0.3", "vue-tsc": "^3.2.6", @@ -5416,9 +5416,9 @@ } }, "node_modules/prettier": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", - "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.2.tgz", + "integrity": "sha512-8c3mgTe0ASwWAJK+78dpviD+A8EqhndQPUBpNUIPt6+xWlIigCwfN01lWr9MAede4uqXGTEKeQWTvzb3vjia0Q==", "dev": true, "license": "MIT", "bin": { diff --git a/webui/package.json b/webui/package.json index a6f93beaa..bf4cbd94e 100644 --- a/webui/package.json +++ b/webui/package.json @@ -60,7 +60,7 @@ "eslint": "^10.2.0", "eslint-plugin-vue": "^10.8.0", "npm-run-all": "^4.1.5", - "prettier": "^3.8.1", + "prettier": "^3.8.2", "typescript": "~6.0.2", "vite": "^8.0.3", "vue-tsc": "^3.2.6", From b7dd16ea2eb0dc6751e520042e36f2ed99564f5e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 Apr 2026 08:07:29 +0000 Subject: [PATCH 21/46] build(deps): bump nodemailer and @types/nodemailer in /webui Bumps [nodemailer](https://github.com/nodemailer/nodemailer) and [@types/nodemailer](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/nodemailer). These dependencies needed to be updated together. Updates `nodemailer` from 8.0.4 to 8.0.5 - [Release notes](https://github.com/nodemailer/nodemailer/releases) - [Changelog](https://github.com/nodemailer/nodemailer/blob/master/CHANGELOG.md) - [Commits](https://github.com/nodemailer/nodemailer/compare/v8.0.4...v8.0.5) Updates `@types/nodemailer` from 7.0.11 to 8.0.0 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/nodemailer) --- updated-dependencies: - dependency-name: nodemailer dependency-version: 8.0.5 dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: "@types/nodemailer" dependency-version: 8.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- webui/package-lock.json | 16 ++++++++-------- webui/package.json | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/webui/package-lock.json b/webui/package-lock.json index 9741fa69f..d89abdfae 100644 --- a/webui/package-lock.json +++ b/webui/package-lock.json @@ -12,7 +12,7 @@ "@ssthouse/vue3-tree-chart": "^0.3.0", "@types/bootstrap": "^5.2.10", "@types/mokapi": "^0.35.0", - "@types/nodemailer": "^7.0.11", + "@types/nodemailer": "^8.0.0", "@types/whatwg-mimetype": "^5.0.0", "ace-builds": "^1.43.5", "bootstrap": "^5.3.8", @@ -31,7 +31,7 @@ "markdown-it-container": "^4.0.0", "mime-types": "^3.0.2", "ncp": "^2.0.0", - "nodemailer": "^8.0.4", + "nodemailer": "^8.0.5", "vue": "^3.5.32", "vue-router": "^5.0.4", "vue3-ace-editor": "^2.2.4", @@ -848,9 +848,9 @@ } }, "node_modules/@types/nodemailer": { - "version": "7.0.11", - "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-7.0.11.tgz", - "integrity": "sha512-E+U4RzR2dKrx+u3N4DlsmLaDC6mMZOM/TPROxA0UAPiTgI0y4CEFBmZE+coGWTjakDriRsXG368lNk1u9Q0a2g==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-8.0.0.tgz", + "integrity": "sha512-fyf8jWULsCo0d0BuoQ75i6IeoHs47qcqxWc7yUdUcV0pOZGjUTTOvwdG1PRXUDqN/8A64yQdQdnA2pZgcdi+cA==", "license": "MIT", "dependencies": { "@types/node": "*" @@ -4768,9 +4768,9 @@ "license": "MIT" }, "node_modules/nodemailer": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.4.tgz", - "integrity": "sha512-k+jf6N8PfQJ0Fe8ZhJlgqU5qJU44Lpvp2yvidH3vp1lPnVQMgi4yEEMPXg5eJS1gFIJTVq1NHBk7Ia9ARdSBdQ==", + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.5.tgz", + "integrity": "sha512-0PF8Yb1yZuQfQbq+5/pZJrtF6WQcjTd5/S4JOHs9PGFxuTqoB/icwuB44pOdURHJbRKX1PPoJZtY7R4VUoCC8w==", "license": "MIT-0", "engines": { "node": ">=6.0.0" diff --git a/webui/package.json b/webui/package.json index bf4cbd94e..a9a8a5fd2 100644 --- a/webui/package.json +++ b/webui/package.json @@ -21,7 +21,7 @@ "@ssthouse/vue3-tree-chart": "^0.3.0", "@types/bootstrap": "^5.2.10", "@types/mokapi": "^0.35.0", - "@types/nodemailer": "^7.0.11", + "@types/nodemailer": "^8.0.0", "@types/whatwg-mimetype": "^5.0.0", "ace-builds": "^1.43.5", "bootstrap": "^5.3.8", @@ -40,7 +40,7 @@ "markdown-it-container": "^4.0.0", "mime-types": "^3.0.2", "ncp": "^2.0.0", - "nodemailer": "^8.0.4", + "nodemailer": "^8.0.5", "vue": "^3.5.32", "vue-router": "^5.0.4", "vue3-ace-editor": "^2.2.4", From 04a02c24772feb26b536f6b9923037ecd666736c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 Apr 2026 08:15:38 +0000 Subject: [PATCH 22/46] build(deps-dev): bump vite from 8.0.3 to 8.0.8 in /webui Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 8.0.3 to 8.0.8. - [Release notes](https://github.com/vitejs/vite/releases) - [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite/commits/v8.0.8/packages/vite) --- updated-dependencies: - dependency-name: vite dependency-version: 8.0.8 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- webui/package-lock.json | 167 ++++++++++++++++++++-------------------- webui/package.json | 2 +- 2 files changed, 84 insertions(+), 85 deletions(-) diff --git a/webui/package-lock.json b/webui/package-lock.json index d89abdfae..f2f023c1d 100644 --- a/webui/package-lock.json +++ b/webui/package-lock.json @@ -53,7 +53,7 @@ "npm-run-all": "^4.1.5", "prettier": "^3.8.2", "typescript": "~6.0.2", - "vite": "^8.0.3", + "vite": "^8.0.8", "vue-tsc": "^3.2.6", "xml2js": "^0.6.2" } @@ -127,7 +127,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" @@ -140,7 +139,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "tslib": "^2.4.0" } @@ -152,7 +150,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "tslib": "^2.4.0" } @@ -349,9 +346,9 @@ } }, "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.2.tgz", - "integrity": "sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.3.tgz", + "integrity": "sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ==", "dev": true, "license": "MIT", "optional": true, @@ -403,9 +400,9 @@ } }, "node_modules/@oxc-project/types": { - "version": "0.122.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.122.0.tgz", - "integrity": "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==", + "version": "0.124.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.124.0.tgz", + "integrity": "sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==", "dev": true, "license": "MIT", "funding": { @@ -452,9 +449,9 @@ } }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.12.tgz", - "integrity": "sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.15.tgz", + "integrity": "sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA==", "cpu": [ "arm64" ], @@ -469,9 +466,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.12.tgz", - "integrity": "sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.15.tgz", + "integrity": "sha512-oArR/ig8wNTPYsXL+Mzhs0oxhxfuHRfG7Ikw7jXsw8mYOtk71W0OkF2VEVh699pdmzjPQsTjlD1JIOoHkLP1Fg==", "cpu": [ "arm64" ], @@ -486,9 +483,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.12.tgz", - "integrity": "sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.15.tgz", + "integrity": "sha512-YzeVqOqjPYvUbJSWJ4EDL8ahbmsIXQpgL3JVipmN+MX0XnXMeWomLN3Fb+nwCmP/jfyqte5I3XRSm7OfQrbyxw==", "cpu": [ "x64" ], @@ -503,9 +500,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.12.tgz", - "integrity": "sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.15.tgz", + "integrity": "sha512-9Erhx956jeQ0nNTyif1+QWAXDRD38ZNjr//bSHrt6wDwB+QkAfl2q6Mn1k6OBPerznjRmbM10lgRb1Pli4xZPw==", "cpu": [ "x64" ], @@ -520,9 +517,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.12.tgz", - "integrity": "sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.15.tgz", + "integrity": "sha512-cVwk0w8QbZJGTnP/AHQBs5yNwmpgGYStL88t4UIaqcvYJWBfS0s3oqVLZPwsPU6M0zlW4GqjP0Zq5MnAGwFeGA==", "cpu": [ "arm" ], @@ -537,9 +534,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.12.tgz", - "integrity": "sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.15.tgz", + "integrity": "sha512-eBZ/u8iAK9SoHGanqe/jrPnY0JvBN6iXbVOsbO38mbz+ZJsaobExAm1Iu+rxa4S1l2FjG0qEZn4Rc6X8n+9M+w==", "cpu": [ "arm64" ], @@ -554,9 +551,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.12.tgz", - "integrity": "sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.15.tgz", + "integrity": "sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ==", "cpu": [ "arm64" ], @@ -571,9 +568,9 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.12.tgz", - "integrity": "sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.15.tgz", + "integrity": "sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ==", "cpu": [ "ppc64" ], @@ -588,9 +585,9 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.12.tgz", - "integrity": "sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.15.tgz", + "integrity": "sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ==", "cpu": [ "s390x" ], @@ -605,9 +602,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.12.tgz", - "integrity": "sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.15.tgz", + "integrity": "sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA==", "cpu": [ "x64" ], @@ -622,9 +619,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.12.tgz", - "integrity": "sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.15.tgz", + "integrity": "sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw==", "cpu": [ "x64" ], @@ -639,9 +636,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.12.tgz", - "integrity": "sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.15.tgz", + "integrity": "sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg==", "cpu": [ "arm64" ], @@ -656,9 +653,9 @@ } }, "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.12.tgz", - "integrity": "sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.15.tgz", + "integrity": "sha512-ApLruZq/ig+nhaE7OJm4lDjayUnOHVUa77zGeqnqZ9pn0ovdVbbNPerVibLXDmWeUZXjIYIT8V3xkT58Rm9u5Q==", "cpu": [ "wasm32" ], @@ -666,16 +663,18 @@ "license": "MIT", "optional": true, "dependencies": { - "@napi-rs/wasm-runtime": "^1.1.1" + "@emnapi/core": "1.9.2", + "@emnapi/runtime": "1.9.2", + "@napi-rs/wasm-runtime": "^1.1.3" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.12.tgz", - "integrity": "sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.15.tgz", + "integrity": "sha512-KmoUoU7HnN+Si5YWJigfTws1jz1bKBYDQKdbLspz0UaqjjFkddHsqorgiW1mxcAj88lYUE6NC/zJNwT+SloqtA==", "cpu": [ "arm64" ], @@ -690,9 +689,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.12.tgz", - "integrity": "sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.15.tgz", + "integrity": "sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g==", "cpu": [ "x64" ], @@ -5702,14 +5701,14 @@ "license": "Unlicense" }, "node_modules/rolldown": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.12.tgz", - "integrity": "sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.15.tgz", + "integrity": "sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.122.0", - "@rolldown/pluginutils": "1.0.0-rc.12" + "@oxc-project/types": "=0.124.0", + "@rolldown/pluginutils": "1.0.0-rc.15" }, "bin": { "rolldown": "bin/cli.mjs" @@ -5718,27 +5717,27 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.0-rc.12", - "@rolldown/binding-darwin-arm64": "1.0.0-rc.12", - "@rolldown/binding-darwin-x64": "1.0.0-rc.12", - "@rolldown/binding-freebsd-x64": "1.0.0-rc.12", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.12", - "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.12", - "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.12", - "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.12", - "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.12", - "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.12", - "@rolldown/binding-linux-x64-musl": "1.0.0-rc.12", - "@rolldown/binding-openharmony-arm64": "1.0.0-rc.12", - "@rolldown/binding-wasm32-wasi": "1.0.0-rc.12", - "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.12", - "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.12" + "@rolldown/binding-android-arm64": "1.0.0-rc.15", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.15", + "@rolldown/binding-darwin-x64": "1.0.0-rc.15", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.15", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.15", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.15", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.15", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.15", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.15", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.15", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.15", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.15", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.15", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.15", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.15" } }, "node_modules/rolldown/node_modules/@rolldown/pluginutils": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.12.tgz", - "integrity": "sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.15.tgz", + "integrity": "sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==", "dev": true, "license": "MIT" }, @@ -6689,16 +6688,16 @@ } }, "node_modules/vite": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.3.tgz", - "integrity": "sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==", + "version": "8.0.8", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.8.tgz", + "integrity": "sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==", "dev": true, "license": "MIT", "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.8", - "rolldown": "1.0.0-rc.12", + "rolldown": "1.0.0-rc.15", "tinyglobby": "^0.2.15" }, "bin": { @@ -6716,7 +6715,7 @@ "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.0", - "esbuild": "^0.27.0", + "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", diff --git a/webui/package.json b/webui/package.json index a9a8a5fd2..27b775b1e 100644 --- a/webui/package.json +++ b/webui/package.json @@ -62,7 +62,7 @@ "npm-run-all": "^4.1.5", "prettier": "^3.8.2", "typescript": "~6.0.2", - "vite": "^8.0.3", + "vite": "^8.0.8", "vue-tsc": "^3.2.6", "xml2js": "^0.6.2" } From fa18fc839b6b33bfec8f1a3c127e01d23cf77e8a Mon Sep 17 00:00:00 2001 From: marle3003 Date: Sun, 12 Apr 2026 19:06:11 +0200 Subject: [PATCH 23/46] fix(json reference): fix resolving json reference after patching which caused a nil pointer panic --- api/handler_http_test.go | 4 +- api/handler_kafka_test.go | 3 +- api/handler_schema_test.go | 7 +- api/handler_search_test.go | 6 +- config/dynamic/asyncApi/config.go | 10 +- config/dynamic/asyncApi/convert.go | 87 ++++----- config/dynamic/asyncApi/convert_test.go | 73 ++++++- config/dynamic/asyncApi/parsing.go | 42 ++-- config/dynamic/asyncApi/parsing_test.go | 177 ++++++++--------- .../dynamic/asyncApi/unmarshal_json_test.go | 11 +- config/dynamic/config.go | 3 + config/dynamic/json.go | 3 + config/dynamic/ref.go | 57 +++++- config/dynamic/ref_test.go | 11 +- config/dynamic/resolve.go | 179 +++++++++++------- config/dynamic/resolve_test.go | 141 ++++++++------ pkg/cmd/mokapi/sample-data.go | 2 +- providers/asyncapi3/asyncapi3test/channel.go | 2 +- providers/asyncapi3/channel.go | 6 +- providers/asyncapi3/components_test.go | 24 +-- providers/asyncapi3/config_test.go | 1 + providers/asyncapi3/correlation_id.go | 7 +- providers/asyncapi3/external_doc.go | 7 +- providers/asyncapi3/message.go | 12 +- providers/asyncapi3/operation.go | 17 +- providers/asyncapi3/parameter.go | 7 +- providers/asyncapi3/schema.go | 25 ++- providers/asyncapi3/server.go | 15 +- providers/asyncapi3/tags.go | 6 +- providers/openapi/components_test.go | 12 +- providers/openapi/example.go | 14 +- providers/openapi/example_test.go | 4 +- providers/openapi/header.go | 6 +- providers/openapi/header_test.go | 2 +- providers/openapi/media_type_test.go | 6 +- providers/openapi/openapitest/operation.go | 6 +- providers/openapi/openapitest/path.go | 2 +- providers/openapi/openapitest/response.go | 8 +- providers/openapi/operation_test.go | 26 +-- providers/openapi/parameter.go | 6 +- providers/openapi/parameter_test.go | 12 +- providers/openapi/parse_test.go | 4 +- providers/openapi/path.go | 6 +- providers/openapi/path_test.go | 16 +- providers/openapi/request_body.go | 6 +- providers/openapi/response.go | 6 +- providers/openapi/response_test.go | 4 +- providers/openapi/schema/convert.go | 4 +- providers/openapi/schema/convert_test.go | 5 +- providers/openapi/schema/marshal.go | 5 + .../openapi/schema/marshal_schema_test.go | 3 +- providers/openapi/schema/marshal_xml_test.go | 3 +- providers/openapi/schema/schema.go | 36 ++-- providers/openapi/schema/schema_parse_test.go | 22 +-- providers/openapi/schema/schematest/schema.go | 7 +- providers/swagger/convert.go | 6 +- runtime/runtime_kafka.go | 46 +---- runtime/runtime_kafka_test.go | 62 +++--- runtime/runtime_search_test.go | 4 +- schema/json/generator/pet_test.go | 10 +- schema/json/schema/clone.go | 3 +- schema/json/schema/clone_test.go | 16 +- schema/json/schema/marshal.go | 5 + schema/json/schema/marshal_test.go | 10 +- schema/json/schema/parse_test.go | 2 +- schema/json/schema/schema.go | 43 ++--- schema/json/schema/schema_json_test.go | 24 +-- schema/json/schema/schema_yaml_test.go | 2 +- schema/json/schema/schematest/schema.go | 11 +- server/configwatcher_openapi_test.go | 9 +- 70 files changed, 800 insertions(+), 629 deletions(-) diff --git a/api/handler_http_test.go b/api/handler_http_test.go index 0b752a6b7..93f015f2a 100644 --- a/api/handler_http_test.go +++ b/api/handler_http_test.go @@ -223,7 +223,7 @@ func TestHandler_Http(t *testing.T) { c := openapitest.NewConfig("3.0.0", openapitest.WithInfo("foo", "", ""), openapitest.WithPathRef("/foo/{bar}", &openapi.PathRef{ - Reference: dynamic.Reference{ + Reference: dynamic.Reference[*openapi.PathRef]{ Ref: "#/components/pathItems/foo", Summary: "Summary", Description: "Description", @@ -233,7 +233,7 @@ func TestHandler_Http(t *testing.T) { openapitest.WithOperation("get", openapitest.UseResponseRef(http.StatusOK, &openapi.ResponseRef{ - Reference: dynamic.Reference{ + Reference: dynamic.Reference[*openapi.ResponseRef]{ Ref: "#/components/pathItems/foo", Description: "Description", }, diff --git a/api/handler_kafka_test.go b/api/handler_kafka_test.go index e358f4b29..d1be7baf0 100644 --- a/api/handler_kafka_test.go +++ b/api/handler_kafka_test.go @@ -4,7 +4,6 @@ import ( "encoding/json" "mokapi/api" "mokapi/config/dynamic" - "mokapi/config/dynamic/asyncApi" "mokapi/config/dynamic/dynamictest" "mokapi/config/static" "mokapi/engine/enginetest" @@ -1098,7 +1097,7 @@ func TestHandler_Kafka_Metrics(t *testing.T) { } func Test_IsAsyncApiConfig(t *testing.T) { - v2 := &asyncApi.Config{Info: asyncApi.Info{Name: "foo"}} + v2 := &asyncapi3.Config{Info: asyncapi3.Info{Name: "foo"}} _, ok := runtime.IsAsyncApiConfig(&dynamic.Config{Data: v2}) require.True(t, ok) diff --git a/api/handler_schema_test.go b/api/handler_schema_test.go index 3f07355fb..9edb633b4 100644 --- a/api/handler_schema_test.go +++ b/api/handler_schema_test.go @@ -3,6 +3,7 @@ package api import ( "encoding/base64" "encoding/json" + "mokapi/config/dynamic" "mokapi/config/static" "mokapi/providers/asyncapi3/asyncapi3test" "mokapi/providers/openapi/openapitest" @@ -540,7 +541,7 @@ func TestSchemaInfo_MarshalJSON(t *testing.T) { }, { name: "json schema only ref", - s: &schemaInfo{Schema: &jsonSchema.Schema{Ref: "foo/bar"}}, + s: &schemaInfo{Schema: &jsonSchema.Schema{Reference: dynamic.Reference[*jsonSchema.Schema]{Ref: "foo/bar"}}}, test: func(t *testing.T, s string, err error) { require.NoError(t, err) require.Equal(t, `{"schema":{"$ref":"foo/bar"}}`, s) @@ -549,8 +550,8 @@ func TestSchemaInfo_MarshalJSON(t *testing.T) { { name: "json schema ref and value", s: &schemaInfo{Schema: &jsonSchema.Schema{ - Ref: "foo/bar", - Type: jsonSchema.Types{"string"}, + Reference: dynamic.Reference[*jsonSchema.Schema]{Ref: "foo/bar"}, + Type: jsonSchema.Types{"string"}, }}, test: func(t *testing.T, s string, err error) { require.NoError(t, err) diff --git a/api/handler_search_test.go b/api/handler_search_test.go index c98370e12..4c81473df 100644 --- a/api/handler_search_test.go +++ b/api/handler_search_test.go @@ -4,10 +4,10 @@ import ( "context" "encoding/json" "mokapi/config/dynamic" - "mokapi/config/dynamic/asyncApi/asyncapitest" "mokapi/config/dynamic/dynamictest" "mokapi/config/static" "mokapi/engine/enginetest" + "mokapi/providers/asyncapi3/asyncapi3test" "mokapi/providers/openapi/openapitest" "mokapi/runtime" "mokapi/runtime/search" @@ -143,7 +143,7 @@ func TestHandler_SearchQuery(t *testing.T) { h := openapitest.NewConfig("3.0", openapitest.WithInfo("foo", "", "")) app.AddHttp(toConfig(h)) - k := asyncapitest.NewConfig(asyncapitest.WithInfo("foo", "", "")) + k := asyncapi3test.NewConfig(asyncapi3test.WithInfo("foo", "", "")) _, err := app.Kafka.Add(toConfig(k), enginetest.NewEngine()) require.NoError(t, err) @@ -173,7 +173,7 @@ func TestHandler_SearchQuery(t *testing.T) { h := openapitest.NewConfig("3.0", openapitest.WithInfo("foo", "", "")) app.AddHttp(toConfig(h)) - k := asyncapitest.NewConfig(asyncapitest.WithInfo("foo", "", "")) + k := asyncapi3test.NewConfig(asyncapi3test.WithInfo("foo", "", "")) _, err := app.Kafka.Add(toConfig(k), enginetest.NewEngine()) require.NoError(t, err) diff --git a/config/dynamic/asyncApi/config.go b/config/dynamic/asyncApi/config.go index 1f1e0607a..88bace64b 100644 --- a/config/dynamic/asyncApi/config.go +++ b/config/dynamic/asyncApi/config.go @@ -70,7 +70,7 @@ type ServerTag struct { } type ServerRef struct { - Ref string + dynamic.Reference[*ServerRef] Value *Server } @@ -79,7 +79,7 @@ type ServerBindings struct { } type ChannelRef struct { - Ref string + dynamic.Reference[*ChannelRef] Value *Channel } @@ -109,7 +109,7 @@ type OperationBindings struct { } type MessageRef struct { - Ref string + dynamic.Reference[*MessageRef] Value *Message } @@ -141,7 +141,7 @@ type Components struct { } type ParameterRef struct { - dynamic.Reference + dynamic.Reference[*ParameterRef] Value *Parameter } @@ -152,7 +152,7 @@ type Parameter struct { } type MessageTraitRef struct { - dynamic.Reference + dynamic.Reference[*MessageTraitRef] Value *MessageTrait } diff --git a/config/dynamic/asyncApi/convert.go b/config/dynamic/asyncApi/convert.go index 497d1eedc..55cf5194b 100644 --- a/config/dynamic/asyncApi/convert.go +++ b/config/dynamic/asyncApi/convert.go @@ -66,24 +66,21 @@ func convertChannels(cfg *asyncapi3.Config, channels map[string]*ChannelRef) err if orig == nil { continue } - if len(orig.Ref) > 0 { - cfg.Channels[name] = &asyncapi3.ChannelRef{Reference: dynamic.Reference{Ref: orig.Ref}} + ch, err := convertChannel(name, orig, cfg) + if err != nil { + return err } - if orig.Value != nil { - ch, err := convertChannel(name, orig, cfg) - if err != nil { - return err - } + if ch.Value != nil { ch.Value.Config = cfg - cfg.Channels[name] = ch } + cfg.Channels[name] = ch } return nil } func convertChannel(name string, c *ChannelRef, config *asyncapi3.Config) (*asyncapi3.ChannelRef, error) { - result := &asyncapi3.ChannelRef{Reference: dynamic.Reference{Ref: c.Ref}} + result := &asyncapi3.ChannelRef{Reference: dynamic.Reference[*asyncapi3.ChannelRef]{Ref: c.Ref}} if c.Value != nil { target := &asyncapi3.Channel{ @@ -98,7 +95,7 @@ func convertChannel(name string, c *ChannelRef, config *asyncapi3.Config) (*asyn for _, server := range c.Value.Servers { target.Servers = append( target.Servers, - &asyncapi3.ServerRef{Reference: dynamic.Reference{ + &asyncapi3.ServerRef{Reference: dynamic.Reference[*asyncapi3.ServerRef]{ Ref: fmt.Sprintf("#/servers/%s", server), }}) } @@ -192,7 +189,7 @@ func convertMessage(msg *MessageRef) *asyncapi3.MessageRef { return nil } - target := &asyncapi3.MessageRef{Reference: dynamic.Reference{Ref: msg.Ref}} + target := &asyncapi3.MessageRef{Reference: dynamic.Reference[*asyncapi3.MessageRef]{Ref: msg.Ref}} if msg.Value != nil { target.Value = &asyncapi3.Message{ Title: msg.Value.Title, @@ -200,45 +197,42 @@ func convertMessage(msg *MessageRef) *asyncapi3.MessageRef { Summary: msg.Value.Summary, Description: msg.Value.Description, ContentType: msg.Value.ContentType, - Payload: nil, + Payload: msg.Value.Payload, Bindings: convertMessageBinding(msg.Value.Bindings), + Headers: msg.Value.Headers, ExternalDocs: nil, } - if msg.Value.Payload != nil && msg.Value.Payload.Value != nil { - target.Value.Payload = msg.Value.Payload - } - if msg.Value.Headers != nil && msg.Value.Headers.Value != nil { - target.Value.Headers = msg.Value.Headers - } for _, orig := range msg.Value.Traits { - trait := &asyncapi3.MessageTraitRef{} - if len(orig.Ref) > 0 { - trait.Reference = dynamic.Reference{Ref: trait.Ref} - } - if orig.Value != nil { - trait.Value = convertMessageTrait(orig.Value).Value + trait := convertMessageTrait(orig) + if trait != nil { + target.Value.Traits = append(target.Value.Traits, trait) } - target.Value.Traits = append(target.Value.Traits, trait) } } return target } -func convertMessageTrait(trait *MessageTrait) *asyncapi3.MessageTraitRef { - target := &asyncapi3.MessageTrait{ - Name: trait.Name, - Title: trait.Title, - Summary: trait.Summary, - Description: trait.Description, - ContentType: trait.ContentType, - Bindings: convertMessageBinding(trait.Bindings), +func convertMessageTrait(trait *MessageTraitRef) *asyncapi3.MessageTraitRef { + target := &asyncapi3.MessageTraitRef{ + Reference: dynamic.Reference[*asyncapi3.MessageTraitRef]{Ref: trait.Ref}, } - if trait.Headers != nil && trait.Headers.Value != nil { - target.Headers = trait.Headers + if trait.Value != nil { + target.Value = &asyncapi3.MessageTrait{ + Name: trait.Value.Name, + Title: trait.Value.Title, + Summary: trait.Value.Summary, + Description: trait.Value.Description, + ContentType: trait.Value.ContentType, + Bindings: convertMessageBinding(trait.Value.Bindings), + } + if trait.Value.Headers != nil && trait.Value.Headers.Value != nil { + target.Value.Headers = trait.Value.Headers + } } - return &asyncapi3.MessageTraitRef{Value: target} + + return target } func convertParameters(channel *asyncapi3.Channel, params map[string]*ParameterRef) error { @@ -251,7 +245,7 @@ func convertParameters(channel *asyncapi3.Channel, params map[string]*ParameterR for name, orig := range params { if len(orig.Ref) > 0 { - channel.Parameters[name] = &asyncapi3.ParameterRef{Reference: dynamic.Reference{Ref: orig.Ref}} + channel.Parameters[name] = &asyncapi3.ParameterRef{Reference: dynamic.Reference[*asyncapi3.ParameterRef]{Ref: orig.Ref}} } if orig.Value != nil { p, err := convertParameter(name, orig.Value) @@ -308,17 +302,15 @@ func convertServers(cfg *asyncapi3.Config, servers map[string]*ServerRef) { } for name, orig := range servers { - if len(orig.Ref) > 0 { - cfg.Servers.Set(name, &asyncapi3.ServerRef{Reference: dynamic.Reference{Ref: orig.Ref}}) - } - if orig.Value != nil { - cfg.Servers.Set(name, convertServer(orig)) + if orig == nil { + continue } + cfg.Servers.Set(name, convertServer(orig)) } } func convertServer(ref *ServerRef) *asyncapi3.ServerRef { - result := &asyncapi3.ServerRef{Reference: dynamic.Reference{Ref: ref.Ref}} + result := &asyncapi3.ServerRef{Reference: dynamic.Reference[*asyncapi3.ServerRef]{Ref: ref.Ref}} if ref.Value != nil { target := &asyncapi3.Server{ @@ -440,7 +432,7 @@ func convertComponents(c *Components, config *asyncapi3.Config) (*asyncapi3.Comp target.Parameters = map[string]*asyncapi3.ParameterRef{} } if len(orig.Ref) > 0 { - target.Parameters[name] = &asyncapi3.ParameterRef{Reference: dynamic.Reference{Ref: orig.Ref}} + target.Parameters[name] = &asyncapi3.ParameterRef{Reference: dynamic.Reference[*asyncapi3.ParameterRef]{Ref: orig.Ref}} } if orig.Value != nil { p, err := convertParameter(name, orig.Value) @@ -455,12 +447,7 @@ func convertComponents(c *Components, config *asyncapi3.Config) (*asyncapi3.Comp if target.MessageTraits == nil { target.MessageTraits = map[string]*asyncapi3.MessageTraitRef{} } - if len(orig.Ref) > 0 { - target.MessageTraits[name] = &asyncapi3.MessageTraitRef{Reference: dynamic.Reference{Ref: orig.Ref}} - } - if orig.Value != nil { - target.MessageTraits[name] = convertMessageTrait(orig.Value) - } + target.MessageTraits[name] = convertMessageTrait(orig) } return target, nil diff --git a/config/dynamic/asyncApi/convert_test.go b/config/dynamic/asyncApi/convert_test.go index 7a77de1ab..4a960eb3e 100644 --- a/config/dynamic/asyncApi/convert_test.go +++ b/config/dynamic/asyncApi/convert_test.go @@ -23,11 +23,11 @@ func TestConfig_Convert(t *testing.T) { err = yaml.Unmarshal(b, &cfg) require.NoError(t, err) - err = cfg.Parse(&dynamic.Config{Data: cfg}, &dynamictest.Reader{}) + c := &dynamic.Config{Data: cfg} + err = cfg.Parse(c, &dynamictest.Reader{}) require.NoError(t, err) - cfg3, err := cfg.Convert() - require.NoError(t, err) + cfg3 := c.Data.(*asyncapi3.Config) require.Equal(t, "3.0.0", cfg3.Version) require.Equal(t, "urn:example:com:smartylighting:streetlights:server", cfg3.Id) @@ -150,3 +150,70 @@ channels: require.Len(t, cfg.Operations["bar_send_publish"].Value.Messages, 1) require.Equal(t, cfg.Channels["bar"].Value.Messages["publish"], cfg.Operations["bar_send_publish"].Value.Messages[0]) } + +func TestConvert(t *testing.T) { + testcases := []struct { + name string + cfg *asyncApi.Config + test func(t *testing.T, config *asyncapi3.Config, err error) + }{ + { + name: "server ref", + cfg: &asyncApi.Config{ + Servers: map[string]*asyncApi.ServerRef{ + "foo": {Reference: dynamic.Reference[*asyncApi.ServerRef]{Ref: "#/components/servers/foo"}}, + }, + Components: &asyncApi.Components{ + Servers: map[string]*asyncApi.ServerRef{ + "foo": {Value: &asyncApi.Server{Url: "foo.bar"}}, + }, + }, + }, + test: func(t *testing.T, config *asyncapi3.Config, err error) { + require.Equal(t, 1, config.Servers.Len()) + s, _ := config.Servers.Get("foo") + require.NotNil(t, s) + require.NotNil(t, s.Value) + require.Equal(t, "foo.bar", s.Value.Host) + require.Equal(t, "#/components/servers/foo", s.Ref) + }, + }, + { + name: "channel ref", + cfg: &asyncApi.Config{ + Channels: map[string]*asyncApi.ChannelRef{ + "foo": {Reference: dynamic.Reference[*asyncApi.ChannelRef]{Ref: "#/components/channels/foo"}}, + }, + Components: &asyncApi.Components{ + Channels: map[string]*asyncApi.ChannelRef{ + "foo": {Value: &asyncApi.Channel{ + Description: "foo", + }}, + }, + }, + }, + test: func(t *testing.T, config *asyncapi3.Config, err error) { + require.Len(t, config.Channels, 1) + c := config.Channels["foo"] + require.NotNil(t, c) + require.NotNil(t, c.Value) + require.Equal(t, "foo", c.Value.Description) + require.Equal(t, "#/components/channels/foo", c.Ref) + }, + }, + } + + t.Parallel() + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + c := &dynamic.Config{Data: tc.cfg} + err := tc.cfg.Parse(c, &dynamictest.Reader{}) + require.NoError(t, err) + require.IsType(t, &asyncapi3.Config{}, c.Data) + + tc.test(t, c.Data.(*asyncapi3.Config), err) + }) + } +} diff --git a/config/dynamic/asyncApi/parsing.go b/config/dynamic/asyncApi/parsing.go index 4390cf703..838f0136e 100644 --- a/config/dynamic/asyncApi/parsing.go +++ b/config/dynamic/asyncApi/parsing.go @@ -7,33 +7,22 @@ import ( ) func (c *Config) Parse(config *dynamic.Config, reader dynamic.Reader) error { - for _, server := range c.Servers { - if server == nil || len(server.Ref) == 0 { - continue - } - var resolved *ServerRef - if err := dynamic.Resolve(server.Ref, &resolved, config, reader); err != nil { - return err - } - server.Value = resolved.Value + if c == nil { + return nil } - for _, ch := range c.Channels { - if ch == nil { - continue - } - if err := ch.Parse(config, reader); err != nil { - return err - } + converted, err := c.Convert() + if err != nil { + return err } - - return nil + config.Data = converted + return converted.Parse(config, reader) } func (r *ChannelRef) Parse(config *dynamic.Config, reader dynamic.Reader) error { if len(r.Ref) > 0 { - var resolved *ChannelRef - if err := dynamic.Resolve(r.Ref, &resolved, config, reader); err != nil { + resolved, err := r.Resolve(config, reader) + if err != nil { return err } r.Value = resolved.Value @@ -74,8 +63,8 @@ func (o *Operation) Parse(config *dynamic.Config, reader dynamic.Reader) error { func (r *MessageRef) Parse(config *dynamic.Config, reader dynamic.Reader) error { if len(r.Ref) > 0 { - var resolved *MessageRef - if err := dynamic.Resolve(r.Ref, &resolved, config, reader); err != nil { + resolved, err := r.Resolve(config, reader) + if err != nil { return err } r.Value = resolved.Value @@ -115,8 +104,8 @@ func (r *MessageRef) Parse(config *dynamic.Config, reader dynamic.Reader) error func (r *MessageTraitRef) parse(config *dynamic.Config, reader dynamic.Reader) error { if len(r.Ref) > 0 { - var resolved *MessageTraitRef - if err := dynamic.Resolve(r.Ref, &resolved, config, reader); err != nil { + resolved, err := r.Resolve(config, reader) + if err != nil { return err } r.Value = resolved.Value @@ -165,11 +154,12 @@ func (m *Message) applyTrait(trait *MessageTrait) { func (r *ParameterRef) Parse(config *dynamic.Config, reader dynamic.Reader) error { if len(r.Ref) > 0 { - var resolved *ParameterRef - if err := dynamic.Resolve(r.Ref, &resolved, config, reader); err != nil { + resolved, err := r.Resolve(config, reader) + if err != nil { return err } r.Value = resolved.Value + return nil } return nil } diff --git a/config/dynamic/asyncApi/parsing_test.go b/config/dynamic/asyncApi/parsing_test.go index d8d2251b1..af22e2f97 100644 --- a/config/dynamic/asyncApi/parsing_test.go +++ b/config/dynamic/asyncApi/parsing_test.go @@ -36,7 +36,7 @@ func TestServerResolve(t *testing.T) { name string cfg *asyncApi.Config read readFunc - test func(t *testing.T, cfg *asyncApi.Config, err error) + test func(t *testing.T, cfg *asyncapi3.Config, err error) }{ { name: "no error when server value is nil", @@ -44,7 +44,7 @@ func TestServerResolve(t *testing.T) { return nil }, cfg: &asyncApi.Config{Servers: map[string]*asyncApi.ServerRef{"foo": nil}}, - test: func(t *testing.T, cfg *asyncApi.Config, err error) { + test: func(t *testing.T, cfg *asyncapi3.Config, err error) { require.NoError(t, err) }, }, @@ -55,13 +55,15 @@ func TestServerResolve(t *testing.T) { }, cfg: &asyncApi.Config{ Servers: map[string]*asyncApi.ServerRef{ - "foo": {Ref: "#/components/servers/foo"}, + "foo": {Reference: dynamic.Reference[*asyncApi.ServerRef]{Ref: "#/components/servers/foo"}}, }, Components: &asyncApi.Components{Servers: map[string]*asyncApi.ServerRef{"foo": {Value: &asyncApi.Server{Description: "foo"}}}}, }, - test: func(t *testing.T, cfg *asyncApi.Config, err error) { + test: func(t *testing.T, cfg *asyncapi3.Config, err error) { require.NoError(t, err) - require.Equal(t, "foo", cfg.Servers["foo"].Value.Description) + s, ok := cfg.Servers.Get("foo") + require.True(t, ok) + require.Equal(t, "foo", s.Value.Description) }, }, { @@ -75,11 +77,13 @@ func TestServerResolve(t *testing.T) { return nil }, cfg: &asyncApi.Config{Servers: map[string]*asyncApi.ServerRef{ - "foo": {Ref: "foo.yml#/servers/foo"}, + "foo": {Reference: dynamic.Reference[*asyncApi.ServerRef]{Ref: "foo.yml#/servers/foo"}}, }}, - test: func(t *testing.T, cfg *asyncApi.Config, err error) { + test: func(t *testing.T, cfg *asyncapi3.Config, err error) { require.NoError(t, err) - require.Equal(t, "foo", cfg.Servers["foo"].Value.Description) + s, ok := cfg.Servers.Get("foo") + require.True(t, ok) + require.Equal(t, "foo", s.Value.Description) }, }, { @@ -91,11 +95,13 @@ func TestServerResolve(t *testing.T) { return nil }, cfg: &asyncApi.Config{Servers: map[string]*asyncApi.ServerRef{ - "foo": {Ref: "foo.yml#/components/servers/foo"}, + "foo": {Reference: dynamic.Reference[*asyncApi.ServerRef]{Ref: "foo.yml#/components/servers/foo"}}, }}, - test: func(t *testing.T, cfg *asyncApi.Config, err error) { + test: func(t *testing.T, cfg *asyncapi3.Config, err error) { require.NoError(t, err) - require.Equal(t, "foo", cfg.Servers["foo"].Value.Description) + s, ok := cfg.Servers.Get("foo") + require.True(t, ok) + require.Equal(t, "foo", s.Value.Description) }, }, { @@ -109,11 +115,13 @@ func TestServerResolve(t *testing.T) { return nil }, cfg: &asyncApi.Config{Servers: map[string]*asyncApi.ServerRef{ - "foo": {Ref: "foo.yml#/servers/foo"}, + "foo": {Reference: dynamic.Reference[*asyncApi.ServerRef]{Ref: "foo.yml#/servers/foo"}}, }}, - test: func(t *testing.T, cfg *asyncApi.Config, err error) { + test: func(t *testing.T, cfg *asyncapi3.Config, err error) { require.NoError(t, err) - require.Nil(t, cfg.Servers["foo"].Value) + s, ok := cfg.Servers.Get("foo") + require.True(t, ok) + require.Nil(t, s.Value) }, }, { @@ -122,10 +130,10 @@ func TestServerResolve(t *testing.T) { return fmt.Errorf("TEST ERROR") }, cfg: &asyncApi.Config{Servers: map[string]*asyncApi.ServerRef{ - "foo": {Ref: "foo.yml#/servers/foo"}, + "foo": {Reference: dynamic.Reference[*asyncApi.ServerRef]{Ref: "foo.yml#/servers/foo"}}, }}, - test: func(t *testing.T, cfg *asyncApi.Config, err error) { - require.EqualError(t, err, "resolve reference 'foo.yml#/servers/foo' failed: TEST ERROR") + test: func(t *testing.T, cfg *asyncapi3.Config, err error) { + require.EqualError(t, err, "resolve reference '/foo.yml#/servers/foo' failed: TEST ERROR") }, }, } @@ -136,8 +144,10 @@ func TestServerResolve(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() reader := &testReader{readFunc: tc.read} - err := tc.cfg.Parse(&dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: tc.cfg}, reader) - tc.test(t, tc.cfg, err) + c := &dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: tc.cfg} + err := tc.cfg.Parse(c, reader) + require.IsType(t, &asyncapi3.Config{}, c.Data) + tc.test(t, c.Data.(*asyncapi3.Config), err) }) } } @@ -147,7 +157,7 @@ func TestChannelResolve(t *testing.T) { name string cfg *asyncApi.Config read readFunc - test func(t *testing.T, cfg *asyncApi.Config, err error) + test func(t *testing.T, cfg *asyncapi3.Config, err error) }{ { name: "empty should not error", @@ -155,7 +165,7 @@ func TestChannelResolve(t *testing.T) { return nil }, cfg: &asyncApi.Config{}, - test: func(t *testing.T, cfg *asyncApi.Config, err error) { + test: func(t *testing.T, cfg *asyncapi3.Config, err error) { require.NoError(t, err) }, }, @@ -165,7 +175,7 @@ func TestChannelResolve(t *testing.T) { return nil }, cfg: &asyncApi.Config{Channels: map[string]*asyncApi.ChannelRef{"foo": nil}}, - test: func(t *testing.T, cfg *asyncApi.Config, err error) { + test: func(t *testing.T, cfg *asyncapi3.Config, err error) { require.NoError(t, err) }, }, @@ -181,9 +191,9 @@ func TestChannelResolve(t *testing.T) { return nil }, cfg: &asyncApi.Config{Channels: map[string]*asyncApi.ChannelRef{ - "foo": {Ref: "foo.yml#/channels/foo"}, + "foo": {Reference: dynamic.Reference[*asyncApi.ChannelRef]{Ref: "foo.yml#/channels/foo"}}, }}, - test: func(t *testing.T, cfg *asyncApi.Config, err error) { + test: func(t *testing.T, cfg *asyncapi3.Config, err error) { require.NoError(t, err) require.Equal(t, "reference", cfg.Channels["foo"].Value.Description) }, @@ -199,9 +209,9 @@ func TestChannelResolve(t *testing.T) { return nil }, cfg: &asyncApi.Config{Channels: map[string]*asyncApi.ChannelRef{ - "foo": {Ref: "foo.yml#/channels/foo"}, + "foo": {Reference: dynamic.Reference[*asyncApi.ChannelRef]{Ref: "foo.yml#/channels/foo"}}, }}, - test: func(t *testing.T, cfg *asyncApi.Config, err error) { + test: func(t *testing.T, cfg *asyncapi3.Config, err error) { require.NoError(t, err) require.Nil(t, cfg.Channels["foo"].Value) }, @@ -212,10 +222,10 @@ func TestChannelResolve(t *testing.T) { return fmt.Errorf("TEST ERROR") }, cfg: &asyncApi.Config{Channels: map[string]*asyncApi.ChannelRef{ - "foo": {Ref: "foo.yml#/channels/foo"}, + "foo": {Reference: dynamic.Reference[*asyncApi.ChannelRef]{Ref: "foo.yml#/channels/foo"}}, }}, - test: func(t *testing.T, cfg *asyncApi.Config, err error) { - require.EqualError(t, err, "resolve reference 'foo.yml#/channels/foo' failed: TEST ERROR") + test: func(t *testing.T, cfg *asyncapi3.Config, err error) { + require.EqualError(t, err, "resolve reference '/foo.yml#/channels/foo' failed: TEST ERROR") }, }, } @@ -226,8 +236,11 @@ func TestChannelResolve(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() reader := &testReader{readFunc: tc.read} - err := tc.cfg.Parse(&dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: tc.cfg}, reader) - tc.test(t, tc.cfg, err) + c := &dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: tc.cfg} + err := tc.cfg.Parse(c, reader) + require.IsType(t, &asyncapi3.Config{}, c.Data) + + tc.test(t, c.Data.(*asyncapi3.Config), err) }) } } @@ -237,7 +250,7 @@ func TestMessage(t *testing.T) { name string cfg *asyncApi.Config read readFunc - test func(t *testing.T, cfg *asyncApi.Config, err error) + test func(t *testing.T, cfg *asyncapi3.Config, err error) }{ { name: "local subscribe message reference", @@ -245,13 +258,14 @@ func TestMessage(t *testing.T) { return nil }, cfg: &asyncApi.Config{Channels: map[string]*asyncApi.ChannelRef{ - "foo": {Value: &asyncApi.Channel{Subscribe: &asyncApi.Operation{Message: &asyncApi.MessageRef{Ref: "#/components/messages/foo"}}}}, + "foo": {Value: &asyncApi.Channel{Subscribe: &asyncApi.Operation{Message: &asyncApi.MessageRef{Reference: dynamic.Reference[*asyncApi.MessageRef]{Ref: "#/components/messages/foo"}}}}}, }, Components: &asyncApi.Components{ Messages: map[string]*asyncApi.MessageRef{"foo": {Value: &asyncApi.Message{Description: "foo"}}}, }}, - test: func(t *testing.T, cfg *asyncApi.Config, err error) { + test: func(t *testing.T, cfg *asyncapi3.Config, err error) { require.NoError(t, err) - require.Equal(t, "foo", cfg.Channels["foo"].Value.Subscribe.Message.Value.Description) + require.Contains(t, cfg.Channels["foo"].Value.Messages, "foo") + require.Equal(t, "foo", cfg.Channels["foo"].Value.Messages["foo"].Value.Description) }, }, { @@ -260,13 +274,13 @@ func TestMessage(t *testing.T) { return nil }, cfg: &asyncApi.Config{Channels: map[string]*asyncApi.ChannelRef{ - "foo": {Value: &asyncApi.Channel{Publish: &asyncApi.Operation{Message: &asyncApi.MessageRef{Ref: "#/components/messages/foo"}}}}, + "foo": {Value: &asyncApi.Channel{Publish: &asyncApi.Operation{Message: &asyncApi.MessageRef{Reference: dynamic.Reference[*asyncApi.MessageRef]{Ref: "#/components/messages/foo"}}}}}, }, Components: &asyncApi.Components{ Messages: map[string]*asyncApi.MessageRef{"foo": {Value: &asyncApi.Message{Description: "foo"}}}, }}, - test: func(t *testing.T, cfg *asyncApi.Config, err error) { + test: func(t *testing.T, cfg *asyncapi3.Config, err error) { require.NoError(t, err) - require.Equal(t, "foo", cfg.Channels["foo"].Value.Publish.Message.Value.Description) + require.Equal(t, "foo", cfg.Channels["foo"].Value.Messages["foo"].Value.Description) }, }, { @@ -280,11 +294,11 @@ func TestMessage(t *testing.T) { return nil }, cfg: &asyncApi.Config{Channels: map[string]*asyncApi.ChannelRef{ - "foo": {Value: &asyncApi.Channel{Subscribe: &asyncApi.Operation{Message: &asyncApi.MessageRef{Ref: "foo.yml#/components/messages/foo"}}}}, + "foo": {Value: &asyncApi.Channel{Subscribe: &asyncApi.Operation{Message: &asyncApi.MessageRef{Reference: dynamic.Reference[*asyncApi.MessageRef]{Ref: "foo.yml#/components/messages/foo"}}}}}, }}, - test: func(t *testing.T, cfg *asyncApi.Config, err error) { + test: func(t *testing.T, cfg *asyncapi3.Config, err error) { require.NoError(t, err) - require.Equal(t, "foo", cfg.Channels["foo"].Value.Subscribe.Message.Value.Description) + require.Equal(t, "foo", cfg.Channels["foo"].Value.Messages["foo"].Value.Description) }, }, { @@ -298,11 +312,11 @@ func TestMessage(t *testing.T) { return nil }, cfg: &asyncApi.Config{Channels: map[string]*asyncApi.ChannelRef{ - "foo": {Value: &asyncApi.Channel{Publish: &asyncApi.Operation{Message: &asyncApi.MessageRef{Ref: "foo.yml#/components/messages/foo"}}}}, + "foo": {Value: &asyncApi.Channel{Publish: &asyncApi.Operation{Message: &asyncApi.MessageRef{Reference: dynamic.Reference[*asyncApi.MessageRef]{Ref: "foo.yml#/components/messages/foo"}}}}}, }}, - test: func(t *testing.T, cfg *asyncApi.Config, err error) { + test: func(t *testing.T, cfg *asyncapi3.Config, err error) { require.NoError(t, err) - require.Equal(t, "foo", cfg.Channels["foo"].Value.Publish.Message.Value.Description) + require.Equal(t, "foo", cfg.Channels["foo"].Value.Messages["foo"].Value.Description) }, }, { @@ -311,10 +325,10 @@ func TestMessage(t *testing.T) { return fmt.Errorf("TEST ERROR") }, cfg: &asyncApi.Config{Channels: map[string]*asyncApi.ChannelRef{ - "foo": {Value: &asyncApi.Channel{Subscribe: &asyncApi.Operation{Message: &asyncApi.MessageRef{Ref: "foo.yml#/components/messages/foo"}}}}, + "foo": {Value: &asyncApi.Channel{Subscribe: &asyncApi.Operation{Message: &asyncApi.MessageRef{Reference: dynamic.Reference[*asyncApi.MessageRef]{Ref: "foo.yml#/components/messages/foo"}}}}}, }}, - test: func(t *testing.T, cfg *asyncApi.Config, err error) { - require.EqualError(t, err, "resolve reference 'foo.yml#/components/messages/foo' failed: TEST ERROR") + test: func(t *testing.T, cfg *asyncapi3.Config, err error) { + require.EqualError(t, err, "resolve reference '/foo.yml#/components/messages/foo' failed: TEST ERROR") }, }, { @@ -325,9 +339,9 @@ func TestMessage(t *testing.T) { "foo": {Value: &asyncApi.Channel{Publish: &asyncApi.Operation{Message: &asyncApi.MessageRef{Value: &asyncApi.Message{}}}}}, }, }, - test: func(t *testing.T, cfg *asyncApi.Config, err error) { + test: func(t *testing.T, cfg *asyncapi3.Config, err error) { require.NoError(t, err) - require.Equal(t, "text/plain", cfg.Channels["foo"].Value.Publish.Message.Value.ContentType) + require.Equal(t, "text/plain", cfg.Channels["foo"].Value.Messages["publish"].Value.ContentType) }, }, { @@ -337,9 +351,9 @@ func TestMessage(t *testing.T) { "foo": {Value: &asyncApi.Channel{Publish: &asyncApi.Operation{Message: &asyncApi.MessageRef{Value: &asyncApi.Message{}}}}}, }, }, - test: func(t *testing.T, cfg *asyncApi.Config, err error) { + test: func(t *testing.T, cfg *asyncapi3.Config, err error) { require.NoError(t, err) - require.Equal(t, "application/json", cfg.Channels["foo"].Value.Publish.Message.Value.ContentType) + require.Equal(t, "application/json", cfg.Channels["foo"].Value.Messages["publish"].Value.ContentType) }, }, } @@ -350,53 +364,12 @@ func TestMessage(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() reader := &testReader{readFunc: tc.read} - err := tc.cfg.Parse(&dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: tc.cfg}, reader) - tc.test(t, tc.cfg, err) + c := &dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: tc.cfg} + err := tc.cfg.Parse(c, reader) + require.IsType(t, &asyncapi3.Config{}, c.Data) + tc.test(t, c.Data.(*asyncapi3.Config), err) }) } - - t.Run("modify file", func(t *testing.T) { - target := &asyncApi.Message{ContentType: "application/json"} - reader := &testReader{readFunc: func(cfg *dynamic.Config) error { return nil }} - config := &asyncApi.Config{Channels: map[string]*asyncApi.ChannelRef{ - "foo": {Value: &asyncApi.Channel{Publish: &asyncApi.Operation{Message: &asyncApi.MessageRef{Ref: "#/components/messages/foo"}}}}, - }, Components: &asyncApi.Components{ - Messages: map[string]*asyncApi.MessageRef{"foo": {Value: &asyncApi.Message{}}}, - }} - file := &dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: config} - err := config.Parse(file, reader) - - // modify file - file.Data.(*asyncApi.Config).Components.Messages["foo"].Value = target - - require.NoError(t, err) - require.Equal(t, target, config.Channels["foo"].Value.Publish.Message.Value) - }) -} - -func TestModifyFileResolve(t *testing.T) { - target := &asyncApi.Channel{} - var fooConfig *dynamic.Config - reader := &testReader{readFunc: func(cfg *dynamic.Config) error { - require.Equal(t, "/foo.yml", cfg.Info.Url.String()) - config := &asyncApi.Config{Channels: map[string]*asyncApi.ChannelRef{ - "foo": {Value: &asyncApi.Channel{}}, - }} - cfg.Data = config - fooConfig = cfg - return nil - }} - config := &asyncApi.Config{Channels: map[string]*asyncApi.ChannelRef{ - "foo": {Ref: "foo.yml#/channels/foo"}, - }} - err := config.Parse(&dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: config}, reader) - require.NoError(t, err) - - fooConfig.Data.(*asyncApi.Config).Channels["foo"].Value = target - err = fooConfig.Data.(dynamic.Parser).Parse(fooConfig, reader) - - require.NoError(t, err) - require.Equal(t, target, config.Channels["foo"].Value) } func TestSchema(t *testing.T) { @@ -423,18 +396,20 @@ func TestSchema(t *testing.T) { schemas := map[string]*asyncapi3.SchemaRef{} schemas["foo"] = &asyncapi3.SchemaRef{Value: &asyncapi3.MultiSchemaFormat{Schema: &asyncapi3.SchemaRef{Value: target}}} config.Components = &asyncApi.Components{Schemas: schemas} - message.Payload = &asyncapi3.SchemaRef{Reference: dynamic.Reference{Ref: "#/components/schemas/foo"}} + message.Payload = &asyncapi3.SchemaRef{Reference: dynamic.Reference[*asyncapi3.SchemaRef]{Ref: "#/components/schemas/foo"}} reader := &testReader{readFunc: func(cfg *dynamic.Config) error { return nil }} - err := config.Parse(&dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: config}, reader) + c := &dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: config} + err := config.Parse(c, reader) require.NoError(t, err) - s, err := message.Payload.GetSchema() + require.IsType(t, &asyncapi3.Config{}, c.Data) + s, err := c.Data.(*asyncapi3.Config).Channels["foo"].Value.Messages["publish"].Value.Payload.GetSchema() require.NoError(t, err) require.Equal(t, target, s) }) t.Run("file reference direct", func(t *testing.T) { target := &schema.Schema{} - message.Payload = &asyncapi3.SchemaRef{Reference: dynamic.Reference{Ref: "foo.yml"}} + message.Payload = &asyncapi3.SchemaRef{Reference: dynamic.Reference[*asyncapi3.SchemaRef]{Ref: "foo.yml"}} reader := &testReader{readFunc: func(cfg *dynamic.Config) error { cfg.Data = target return nil @@ -446,7 +421,7 @@ func TestSchema(t *testing.T) { }) t.Run("modify file reference direct", func(t *testing.T) { target := &schema.Schema{} - message.Payload = &asyncapi3.SchemaRef{Value: &asyncapi3.MultiSchemaFormat{Schema: &asyncapi3.SchemaRef{Reference: dynamic.Reference{Ref: "foo.yml"}}}} + message.Payload = &asyncapi3.SchemaRef{Value: &asyncapi3.MultiSchemaFormat{Schema: &asyncapi3.SchemaRef{Reference: dynamic.Reference[*asyncapi3.SchemaRef]{Ref: "foo.yml"}}}} reader := &testReader{readFunc: func(file *dynamic.Config) error { file.Data = target return nil diff --git a/config/dynamic/asyncApi/unmarshal_json_test.go b/config/dynamic/asyncApi/unmarshal_json_test.go index f553bdf9a..816d30603 100644 --- a/config/dynamic/asyncApi/unmarshal_json_test.go +++ b/config/dynamic/asyncApi/unmarshal_json_test.go @@ -2,10 +2,11 @@ package asyncApi_test import ( "encoding/json" - "github.com/stretchr/testify/require" "mokapi/config/dynamic" "mokapi/config/dynamic/asyncApi" "testing" + + "github.com/stretchr/testify/require" ) func Test_UnmarshalJSON(t *testing.T) { @@ -19,7 +20,7 @@ func Test_UnmarshalJSON(t *testing.T) { name: "ParameterRef $ref", input: `{"$ref":"foo"}`, target: &asyncApi.ParameterRef{}, - expected: &asyncApi.ParameterRef{Reference: dynamic.Reference{Ref: "foo"}}, + expected: &asyncApi.ParameterRef{Reference: dynamic.Reference[*asyncApi.ParameterRef]{Ref: "foo"}}, }, { name: "ParameterRef value", @@ -31,7 +32,7 @@ func Test_UnmarshalJSON(t *testing.T) { name: "MessageRef ref", input: `{"$ref":"foo"}`, target: &asyncApi.MessageRef{}, - expected: &asyncApi.MessageRef{Ref: "foo"}, + expected: &asyncApi.MessageRef{Reference: dynamic.Reference[*asyncApi.MessageRef]{Ref: "foo"}}, }, { name: "MessageRef value", @@ -43,7 +44,7 @@ func Test_UnmarshalJSON(t *testing.T) { name: "ChannelRef ref", input: `{"$ref":"foo"}`, target: &asyncApi.ChannelRef{}, - expected: &asyncApi.ChannelRef{Ref: "foo"}, + expected: &asyncApi.ChannelRef{Reference: dynamic.Reference[*asyncApi.ChannelRef]{Ref: "foo"}}, }, { name: "ChannelRef value", @@ -66,7 +67,7 @@ func Test_UnmarshalJSON(t *testing.T) { name: "ServerRef ref", input: `{"$ref":"foo"}`, target: &asyncApi.ServerRef{}, - expected: &asyncApi.ServerRef{Ref: "foo"}, + expected: &asyncApi.ServerRef{Reference: dynamic.Reference[*asyncApi.ServerRef]{Ref: "foo"}}, }, { name: "ServerRef value", diff --git a/config/dynamic/config.go b/config/dynamic/config.go index 64024fedf..3047806f5 100644 --- a/config/dynamic/config.go +++ b/config/dynamic/config.go @@ -75,6 +75,9 @@ func AddRef(parent, ref *Config) { if !added { return } + if parent.Info.Url == nil { + return + } checksum := ref.Info.Checksum ref.Listeners.Add(parent.Info.Url.String(), func(e ConfigEvent) { // event Create is used for reading first time diff --git a/config/dynamic/json.go b/config/dynamic/json.go index 92218e905..488497c0e 100644 --- a/config/dynamic/json.go +++ b/config/dynamic/json.go @@ -148,6 +148,9 @@ func object(d *decoder, v reflect.Value) error { continue } + i := v.Interface() + _ = i + offset = d.d.InputOffset() err = unmarshalJSON(d, field) if err != nil { diff --git a/config/dynamic/ref.go b/config/dynamic/ref.go index 16fcb70ea..6e274641c 100644 --- a/config/dynamic/ref.go +++ b/config/dynamic/ref.go @@ -2,16 +2,23 @@ package dynamic import ( "encoding/json" + "fmt" + "strings" + "gopkg.in/yaml.v3" ) -type Reference struct { - Ref string `yaml:"$ref" json:"$ref"` - Summary string `yaml:"summary" json:"summary"` - Description string `yaml:"description" json:"description"` +type Reference[T any] struct { + Ref string `yaml:"$ref,omitempty" json:"$ref,omitempty"` + DynamicRef string `yaml:"$dynamicRef,omitempty" json:"$dynamicRef,omitempty"` + + Summary string `yaml:"summary,omitempty" json:"summary,omitempty"` + Description string `yaml:"description,omitempty" json:"description,omitempty"` + + origin *Config } -func (r *Reference) UnmarshalYaml(node *yaml.Node, val interface{}) error { +func (r *Reference[T]) UnmarshalYaml(node *yaml.Node, val interface{}) error { err := node.Decode(r) if err == nil && len(r.Ref) > 0 { return nil @@ -20,7 +27,7 @@ func (r *Reference) UnmarshalYaml(node *yaml.Node, val interface{}) error { return node.Decode(val) } -func (r *Reference) UnmarshalJson(b []byte, val interface{}) error { +func (r *Reference[T]) UnmarshalJson(b []byte, val interface{}) error { var m map[string]string _ = json.Unmarshal(b, &m) if _, ok := m["$ref"]; ok { @@ -30,3 +37,41 @@ func (r *Reference) UnmarshalJson(b []byte, val interface{}) error { err := UnmarshalJSON(b, val) return err } + +func (r *Reference[T]) Parse(config *Config, _ Reader) error { + if r.Ref == "" || r.origin != nil { + return nil + } + r.origin = config + return nil +} + +func (r *Reference[T]) HasRef() bool { + return r.Ref != "" || r.DynamicRef != "" +} + +func (r *Reference[T]) Resolve(config *Config, reader Reader) (T, error) { + var err error + var result T + + if err := r.Parse(config, reader); err != nil { + return result, err + } + + if r.Ref != "" { + ref := r.Ref + if !strings.HasPrefix(ref, "#") { + u, err := resolveUrl(r.Ref, r.origin) + if err != nil { + return result, fmt.Errorf("resolve reference '%s' failed: %v", r.Ref, err) + } + ref = u.String() + } + + result, err = resolve[T](ref, config, reader) + return result, err + } + + result, err = ResolveDynamic[T](r.DynamicRef, config, reader) + return result, err +} diff --git a/config/dynamic/ref_test.go b/config/dynamic/ref_test.go index e08e1c33e..983765a8b 100644 --- a/config/dynamic/ref_test.go +++ b/config/dynamic/ref_test.go @@ -2,17 +2,18 @@ package dynamic_test import ( "encoding/json" - "github.com/stretchr/testify/require" - "gopkg.in/yaml.v3" "mokapi/config/dynamic" "testing" + + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" ) type foo struct { Foo string `yaml:"foo"` } type ref struct { - dynamic.Reference + dynamic.Reference[*ref] Value *foo } @@ -29,7 +30,7 @@ func TestReference_Unmarshal(t *testing.T) { err := json.Unmarshal([]byte(`{"$ref":"foo","summary":"summary","description":"description"}`), &r) require.NoError(t, err) require.Equal(t, &ref{ - Reference: dynamic.Reference{ + Reference: dynamic.Reference[*ref]{ Ref: "foo", Summary: "summary", Description: "description", @@ -59,7 +60,7 @@ description: description `), &r) require.NoError(t, err) require.Equal(t, &ref{ - Reference: dynamic.Reference{ + Reference: dynamic.Reference[*ref]{ Ref: "foo", Summary: "summary", Description: "description", diff --git a/config/dynamic/resolve.go b/config/dynamic/resolve.go index f9b8df6c9..239248f89 100644 --- a/config/dynamic/resolve.go +++ b/config/dynamic/resolve.go @@ -16,34 +16,41 @@ type PathResolver interface { } type Converter interface { - ConvertTo(i interface{}) (interface{}, error) + ConvertTo(i any) (any, error) } -func Resolve(ref string, element interface{}, config *Config, reader Reader) error { +type FromConverter interface { + ConvertFrom(i any) (any, error) +} + +func resolve[T any](ref string, config *Config, reader Reader) (T, error) { var err error + var result T - fragment := ref[1:] + var fragment string isLocal := true parent := config - if !strings.HasPrefix(ref, "#") { - fragment, config, err = resolveResource(ref, element, config, reader) + if len(ref) > 0 && !strings.HasPrefix(ref, "#") { + fragment, config, err = resolveResource[T](ref, config, reader) if err != nil { - return fmt.Errorf("resolve reference '%v' failed: %w", ref, err) + return result, fmt.Errorf("resolve reference '%v' failed: %w", ref, err) } isLocal = false + } else if len(ref) > 0 { + fragment = ref[1:] } - err = resolveFragment(fragment, element, config, false) + result, err = resolveFragment[T](fragment, config, false) if err != nil { - return fmt.Errorf("resolve reference '%v' failed: %w", ref, err) + return result, fmt.Errorf("resolve reference '%v' failed: %w", ref, err) } // Parse the referenced schema again in the current context. // This ensures nested $ref and $dynamicRef are resolved relative // to the correct dynamic scope. - // element is **struct - p, ok := reflect.ValueOf(element).Elem().Interface().(Parser) + v := reflect.ValueOf(result) + p, ok := v.Interface().(Parser) if ok { if !isLocal { // set parent scope hierarchy @@ -51,39 +58,40 @@ func Resolve(ref string, element interface{}, config *Config, reader Reader) err config.Scope.SetParent(parent.Scope) } if !config.EnterRef(ref) { - return nil + return result, nil } defer config.LeaveRef(ref) err = p.Parse(config, reader) if err != nil { - return fmt.Errorf("resolve reference '%v' failed: %w", ref, err) + return result, fmt.Errorf("resolve reference '%v' failed: %w", ref, err) } } - return nil + return result, nil } -func ResolveDynamic(ref string, element interface{}, config *Config, reader Reader) error { +func ResolveDynamic[T any](ref string, config *Config, reader Reader) (T, error) { var err error + var result T fragment := ref[1:] if !strings.HasPrefix(ref, "#") { - fragment, config, err = resolveResource(ref, element, config, reader) + fragment, config, err = resolveResource[T](ref, config, reader) if err != nil { - return fmt.Errorf("resolve reference '%v' failed: %w", ref, err) + return result, fmt.Errorf("resolve reference '%v' failed: %w", ref, err) } } - err = resolveFragment(fragment, element, config, true) + result, err = resolveFragment[T](fragment, config, true) if err != nil { - return fmt.Errorf("resolve reference '%v' failed: %w", ref, err) + return result, fmt.Errorf("resolve reference '%v' failed: %w", ref, err) } - return nil + return result, nil } -func resolveFragment(fragment string, resolved interface{}, config *Config, dynamic bool) (err error) { +func resolveFragment[T any](fragment string, config *Config, dynamic bool) (result T, err error) { val := config.Data if fragment == "" { // resolve to current (root) element @@ -95,9 +103,53 @@ func resolveFragment(fragment string, resolved interface{}, config *Config, dyna val, err = config.Scope.GetLexical(fragment) } if err != nil { - return err + return + } + + result, err = convertTo[T](val) + return +} + +func convertTo[T any](val any) (T, error) { + if val == nil { + return *new(T), fmt.Errorf("value is null") + } + + if p, ok := val.(PathResolver); ok { + var err error + val, err = p.Resolve("") + if err != nil { + return *new(T), err + } } - return setResolved(resolved, val) + + val = convert[T](val) + + // types are identical + if v, ok := val.(T); ok { + return v, nil + } + + valType := reflect.TypeOf(val) + targetType := reflect.TypeOf((*T)(nil)).Elem() + + // val is pointer but T not + if valType != nil && valType.Kind() == reflect.Ptr && valType.Elem() == targetType { + v := reflect.ValueOf(val) + if !v.IsNil() { + return v.Elem().Interface().(T), nil + } + } + + // T is pointer but val not + if valType != nil && reflect.PointerTo(valType) == targetType { + vp := reflect.New(valType) + vp.Elem().Set(reflect.ValueOf(val)) + return vp.Interface().(T), nil + } + + var result T + return result, fmt.Errorf("expected type %T, got %T", result, val) } func get(token string, node interface{}) (interface{}, error) { @@ -215,9 +267,13 @@ func resolveUrl(ref string, cfg *Config) (*url.URL, error) { p = fmt.Sprintf("%v#%v", p, u.Fragment) } return url.Parse(p) - } else { - return info.Url.Parse(ref) } + + refURL := info.Url.ResolveReference(u) + if u.Fragment != "" { + refURL.Fragment = u.Fragment + } + return refURL, nil } func getId(v interface{}) string { @@ -246,7 +302,7 @@ func getId(v interface{}) string { return "" } -func resolveResource(ref string, element interface{}, config *Config, reader Reader) (string, *Config, error) { +func resolveResource[T any](ref string, config *Config, reader Reader) (string, *Config, error) { u, err := resolveUrl(ref, config) if err != nil { return "", nil, err @@ -257,7 +313,8 @@ func resolveResource(ref string, element interface{}, config *Config, reader Rea val := reflect.ValueOf(config.Data).Elem() data = reflect.New(val.Type()).Interface() } else { - data = reflect.ValueOf(element).Elem().Interface() + var result T + data = result } sub, err := reader.Read(removeFragment(u), data) @@ -267,51 +324,6 @@ func resolveResource(ref string, element interface{}, config *Config, reader Rea return u.Fragment, sub, err } -func setResolved(element interface{}, val interface{}) (err error) { - v := reflect.ValueOf(val) - vElement := reflect.Indirect(reflect.ValueOf(element)) - - if v.Kind() == reflect.Ptr { - v = v.Elem() - } - - if val == nil { - return fmt.Errorf("value is null") - } - - if r, ok := val.(PathResolver); ok { - if val, err = r.Resolve(""); err != nil { - return - } - } - - vCursor := reflect.ValueOf(val) - if reflect.Indirect(vCursor).Kind() == reflect.Map { - reflect.Indirect(reflect.ValueOf(element)).Set(reflect.Indirect(vCursor)) - return - } - - if !vCursor.Type().AssignableTo(vElement.Type()) && vCursor.Kind() == reflect.Ptr { - if c, ok := val.(Converter); ok { - if converted, err := c.ConvertTo(vElement.Interface()); err == nil { - vCursor = reflect.ValueOf(converted) - } else { - vCursor = vCursor.Elem() - } - } else { - vCursor = vCursor.Elem() - } - } - - if !vCursor.Type().AssignableTo(vElement.Type()) { - return fmt.Errorf("expected type %v, got %v", vElement.Type(), vCursor.Type()) - } - - vElement.Set(vCursor) - - return -} - func copyData(input interface{}) interface{} { val := reflect.ValueOf(input) @@ -328,3 +340,28 @@ func copyData(input interface{}) interface{} { return c.Interface() } + +func convert[T any](val any) any { + var target T + + if c, ok := val.(Converter); ok { + result, err := c.ConvertTo(target) + if err == nil { + return result + } + } + + v := reflect.ValueOf(target) + if !v.IsValid() || !v.CanInterface() { + return val + } + c, ok := v.Interface().(FromConverter) + if !ok { + return val + } + result, err := c.ConvertFrom(val) + if err == nil { + return result + } + return val +} diff --git a/config/dynamic/resolve_test.go b/config/dynamic/resolve_test.go index 0381ea22e..35b971f8b 100644 --- a/config/dynamic/resolve_test.go +++ b/config/dynamic/resolve_test.go @@ -18,7 +18,8 @@ func TestResolve(t *testing.T) { { name: "invalid ref", test: func(t *testing.T) { - err := dynamic.Resolve(":80", "", &dynamic.Config{}, &dynamictest.Reader{}) + r := dynamic.Reference[*ref]{Ref: ":80"} + _, err := r.Resolve(&dynamic.Config{}, &dynamictest.Reader{}) require.Error(t, err) require.EqualError(t, err, "resolve reference ':80' failed: parse \":80\": missing protocol scheme") }, @@ -30,9 +31,9 @@ func TestResolve(t *testing.T) { Foo string } s := v{Foo: "foo"} - var result v - err := dynamic.Resolve("#/", &result, &dynamic.Config{Data: s}, &dynamictest.Reader{}) + r := dynamic.Reference[v]{Ref: "#/"} + result, err := r.Resolve(&dynamic.Config{Data: s}, &dynamictest.Reader{}) require.NoError(t, err) require.Equal(t, s, result) @@ -44,9 +45,9 @@ func TestResolve(t *testing.T) { s := struct { Foo string }{Foo: "foo"} - result := "" - err := dynamic.Resolve("#/foo", &result, &dynamic.Config{Data: s}, &dynamictest.Reader{}) + r := dynamic.Reference[string]{Ref: "#/foo"} + result, err := r.Resolve(&dynamic.Config{Data: s}, &dynamictest.Reader{}) require.NoError(t, err) require.Equal(t, "foo", result) @@ -58,9 +59,9 @@ func TestResolve(t *testing.T) { s := struct { Foo map[string]string }{Foo: map[string]string{"bar": "bar"}} - result := "" - err := dynamic.Resolve("#/foo/bar", &result, &dynamic.Config{Data: s}, &dynamictest.Reader{}) + r := dynamic.Reference[string]{Ref: "#/foo/bar"} + result, err := r.Resolve(&dynamic.Config{Data: s}, &dynamictest.Reader{}) require.NoError(t, err) require.Equal(t, "bar", result) @@ -72,9 +73,9 @@ func TestResolve(t *testing.T) { s := struct { Foo map[string]string }{Foo: map[string]string{"/bar": "bar"}} - result := "" - err := dynamic.Resolve("#/foo/~1bar", &result, &dynamic.Config{Data: s}, &dynamictest.Reader{}) + r := dynamic.Reference[string]{Ref: "#/foo/~1bar"} + result, err := r.Resolve(&dynamic.Config{Data: s}, &dynamictest.Reader{}) require.NoError(t, err) require.Equal(t, "bar", result) @@ -86,9 +87,9 @@ func TestResolve(t *testing.T) { s := struct { Foo map[string]string }{Foo: map[string]string{"~bar": "bar"}} - result := "" - err := dynamic.Resolve("#/foo/~0bar", &result, &dynamic.Config{Data: s}, &dynamictest.Reader{}) + r := dynamic.Reference[string]{Ref: "#/foo/~0bar"} + result, err := r.Resolve(&dynamic.Config{Data: s}, &dynamictest.Reader{}) require.NoError(t, err) require.Equal(t, "bar", result) @@ -98,9 +99,9 @@ func TestResolve(t *testing.T) { name: "resolve local map entry not found", test: func(t *testing.T) { s := map[string]string{} - result := "" - err := dynamic.Resolve("#/foo", &result, &dynamic.Config{Data: s}, &dynamictest.Reader{}) + r := dynamic.Reference[string]{Ref: "#/foo"} + _, err := r.Resolve(&dynamic.Config{Data: s}, &dynamictest.Reader{}) require.Error(t, err) require.EqualError(t, err, "resolve reference '#/foo' failed: path element 'foo' not found") @@ -114,8 +115,8 @@ func TestResolve(t *testing.T) { } s := map[string]ref{"foo": {Value: "foo"}} - var resolved ref - err := dynamic.Resolve("#/foo", &resolved, &dynamic.Config{Data: s}, &dynamictest.Reader{}) + r := dynamic.Reference[ref]{Ref: "#/foo"} + resolved, err := r.Resolve(&dynamic.Config{Data: s}, &dynamictest.Reader{}) require.NoError(t, err) require.Equal(t, "foo", resolved.Value) @@ -130,9 +131,9 @@ func TestResolve(t *testing.T) { s := struct { Foo n }{Foo: n{Bar: "bar"}} - result := "" - err := dynamic.Resolve("#/foo/bar", &result, &dynamic.Config{Data: s}, &dynamictest.Reader{}) + r := dynamic.Reference[string]{Ref: "#/foo/bar"} + result, err := r.Resolve(&dynamic.Config{Data: s}, &dynamictest.Reader{}) require.NoError(t, err) require.Equal(t, "bar", result) @@ -148,9 +149,9 @@ func TestResolve(t *testing.T) { s := struct { Foo n }{Foo: n{Bar: &v}} - result := "" - err := dynamic.Resolve("#/foo/bar", &result, &dynamic.Config{Data: s}, &dynamictest.Reader{}) + r := dynamic.Reference[string]{Ref: "#/foo/bar"} + result, err := r.Resolve(&dynamic.Config{Data: s}, &dynamictest.Reader{}) require.NoError(t, err) require.Equal(t, "bar", result) @@ -160,9 +161,9 @@ func TestResolve(t *testing.T) { name: "resolve local struct field not found", test: func(t *testing.T) { s := struct{}{} - result := "" - err := dynamic.Resolve("#/foo", &result, &dynamic.Config{Data: s}, &dynamictest.Reader{}) + r := dynamic.Reference[string]{Ref: "#/foo"} + _, err := r.Resolve(&dynamic.Config{Data: s}, &dynamictest.Reader{}) require.Error(t, err) require.EqualError(t, err, "resolve reference '#/foo' failed: path element 'foo' not found") @@ -179,11 +180,11 @@ func TestResolve(t *testing.T) { Foo ref }{Foo: ref{Value: "foo"}} - var resolved ref - err := dynamic.Resolve("#/foo", &resolved, &dynamic.Config{Data: s}, &dynamictest.Reader{}) + r := dynamic.Reference[*ref]{Ref: "#/foo"} + result, err := r.Resolve(&dynamic.Config{Data: s}, &dynamictest.Reader{}) require.NoError(t, err) - require.Equal(t, "foo", resolved.Value) + require.Equal(t, "foo", result.Value) }, }, { @@ -197,10 +198,10 @@ func TestResolve(t *testing.T) { Foo ref }{Foo: ref{Value: nil}} - var resolved ref - err := dynamic.Resolve("#/foo", &resolved, &dynamic.Config{Data: s}, &dynamictest.Reader{}) + r := dynamic.Reference[*ref]{Ref: "#/foo"} + result, err := r.Resolve(&dynamic.Config{Data: s}, &dynamictest.Reader{}) require.NoError(t, err) - require.Nil(t, resolved.Value) + require.Nil(t, result.Value) }, }, { @@ -215,11 +216,11 @@ func TestResolve(t *testing.T) { Foo ref }{Foo: ref{Value: &v}} - var resolved ref - err := dynamic.Resolve("#/foo", &resolved, &dynamic.Config{Data: s}, &dynamictest.Reader{}) + r := dynamic.Reference[*ref]{Ref: "#/foo"} + result, err := r.Resolve(&dynamic.Config{Data: s}, &dynamictest.Reader{}) require.NoError(t, err) - require.Equal(t, "foo", *resolved.Value.(*string)) + require.Equal(t, "foo", *result.Value.(*string)) }, }, { @@ -231,9 +232,9 @@ func TestResolve(t *testing.T) { s := struct { Foo ref }{Foo: ref{Value: map[string]string{"bar": "bar"}}} - result := "" - err := dynamic.Resolve("#/foo/bar", &result, &dynamic.Config{Data: s}, &dynamictest.Reader{}) + r := dynamic.Reference[string]{Ref: "#/foo/bar"} + result, err := r.Resolve(&dynamic.Config{Data: s}, &dynamictest.Reader{}) require.NoError(t, err) require.Equal(t, "bar", result) @@ -245,9 +246,9 @@ func TestResolve(t *testing.T) { s := struct { Foo int }{Foo: 12} - var result float32 - err := dynamic.Resolve("#/foo", &result, &dynamic.Config{Data: s}, &dynamictest.Reader{}) + r := dynamic.Reference[float32]{Ref: "#/foo"} + _, err := r.Resolve(&dynamic.Config{Data: s}, &dynamictest.Reader{}) require.Error(t, err) require.EqualError(t, err, "resolve reference '#/foo' failed: expected type float32, got int") @@ -262,9 +263,9 @@ func TestResolve(t *testing.T) { s := struct { Foo *v }{Foo: &v{Value: "foo"}} - var result v - err := dynamic.Resolve("#/foo", &result, &dynamic.Config{Data: s}, &dynamictest.Reader{}) + r := dynamic.Reference[v]{Ref: "#/foo"} + result, err := r.Resolve(&dynamic.Config{Data: s}, &dynamictest.Reader{}) require.NoError(t, err) require.Equal(t, "foo", result.Value) @@ -276,9 +277,9 @@ func TestResolve(t *testing.T) { s := struct { Foo map[string]string }{Foo: map[string]string{"foo": "foo"}} - var result map[string]string - err := dynamic.Resolve("#/foo", &result, &dynamic.Config{Data: s}, &dynamictest.Reader{}) + r := dynamic.Reference[map[string]string]{Ref: "#/foo"} + result, err := r.Resolve(&dynamic.Config{Data: s}, &dynamictest.Reader{}) require.NoError(t, err) require.Contains(t, result, "foo") @@ -292,9 +293,9 @@ func TestResolve(t *testing.T) { }{Foo: &pathResolver{resolve: func(token string) (interface{}, error) { return "foo", nil }}} - var result string - err := dynamic.Resolve("#/foo", &result, &dynamic.Config{Data: s}, &dynamictest.Reader{}) + r := dynamic.Reference[string]{Ref: "#/foo"} + result, err := r.Resolve(&dynamic.Config{Data: s}, &dynamictest.Reader{}) require.NoError(t, err) require.Contains(t, result, "foo") @@ -308,9 +309,9 @@ func TestResolve(t *testing.T) { }{Foo: &pathResolver{resolve: func(token string) (interface{}, error) { return nil, fmt.Errorf("TEST ERROR") }}} - var result string - err := dynamic.Resolve("#/foo", &result, &dynamic.Config{Data: s}, &dynamictest.Reader{}) + r := dynamic.Reference[string]{Ref: "#/foo"} + _, err := r.Resolve(&dynamic.Config{Data: s}, &dynamictest.Reader{}) require.Error(t, err) require.EqualError(t, err, "resolve reference '#/foo' failed: TEST ERROR") @@ -325,9 +326,9 @@ func TestResolve(t *testing.T) { require.Equal(t, "bar", token) return "foo", nil }}} - var result string - err := dynamic.Resolve("#/foo/bar", &result, &dynamic.Config{Data: s}, &dynamictest.Reader{}) + r := dynamic.Reference[string]{Ref: "#/foo/bar"} + result, err := r.Resolve(&dynamic.Config{Data: s}, &dynamictest.Reader{}) require.NoError(t, err) require.Contains(t, result, "foo") @@ -341,14 +342,34 @@ func TestResolve(t *testing.T) { }{Foo: &pathResolver{resolve: func(token string) (interface{}, error) { return nil, fmt.Errorf("TEST ERROR") }}} - var result string - err := dynamic.Resolve("#/foo/bar", &result, &dynamic.Config{Data: s}, &dynamictest.Reader{}) + r := dynamic.Reference[string]{Ref: "#/foo/bar"} + _, err := r.Resolve(&dynamic.Config{Data: s}, &dynamictest.Reader{}) require.Error(t, err) require.EqualError(t, err, "resolve reference '#/foo/bar' failed: TEST ERROR") }, }, + { + name: "resolve local reference nested with Converter", + test: func(t *testing.T) { + s := struct { + Foo *converter + }{ + Foo: &converter{ + convert: func(interface{}) (interface{}, error) { + return "foo", nil + }, + }, + } + + r := dynamic.Reference[string]{Ref: "#/foo"} + result, err := r.Resolve(&dynamic.Config{Data: s}, &dynamictest.Reader{}) + + require.NoError(t, err) + require.Contains(t, result, "foo") + }, + }, { name: "resolve global reference", test: func(t *testing.T) { @@ -356,9 +377,9 @@ func TestResolve(t *testing.T) { require.Equal(t, "https://foo.bar", u.String()) return &dynamic.Config{Data: "foo"}, nil }) - result := "" - err := dynamic.Resolve("https://foo.bar", &result, &dynamic.Config{Info: dynamictest.NewConfigInfo()}, reader) + r := dynamic.Reference[string]{Ref: "https://foo.bar"} + result, err := r.Resolve(&dynamic.Config{Info: dynamictest.NewConfigInfo()}, reader) require.NoError(t, err) require.Equal(t, "foo", result) @@ -370,9 +391,9 @@ func TestResolve(t *testing.T) { reader := dynamictest.ReaderFunc(func(u *url.URL, v any) (*dynamic.Config, error) { return nil, fmt.Errorf("TESTING ERROR") }) - result := "" - err := dynamic.Resolve("https://foo.bar", &result, &dynamic.Config{Info: dynamictest.NewConfigInfo()}, reader) + r := dynamic.Reference[string]{Ref: "https://foo.bar"} + _, err := r.Resolve(&dynamic.Config{Info: dynamictest.NewConfigInfo()}, reader) require.Error(t, err) require.EqualError(t, err, "resolve reference 'https://foo.bar' failed: TESTING ERROR") @@ -388,10 +409,10 @@ func TestResolve(t *testing.T) { reader := dynamictest.ReaderFunc(func(u *url.URL, v any) (*dynamic.Config, error) { return &dynamic.Config{Data: value{Foo: "foo"}}, nil }) - result := "" - cfg := &dynamic.Config{Info: dynamictest.NewConfigInfo(), Data: &value{}} - err := dynamic.Resolve("https://foo.bar#/foo", &result, cfg, reader) + + r := dynamic.Reference[string]{Ref: "https://foo.bar#/foo"} + result, err := r.Resolve(cfg, reader) require.NoError(t, err) require.Equal(t, "foo", result) @@ -407,10 +428,10 @@ func TestResolve(t *testing.T) { reader := dynamictest.ReaderFunc(func(u *url.URL, v any) (*dynamic.Config, error) { return &dynamic.Config{Data: value{Foo: "foo"}}, nil }) - result := "" - cfg := &dynamic.Config{Info: dynamictest.NewConfigInfo(), Data: &value{}} - err := dynamic.Resolve("https://foo.bar#/bar", &result, cfg, reader) + + r := dynamic.Reference[string]{Ref: "https://foo.bar#/bar"} + _, err := r.Resolve(cfg, reader) require.Error(t, err) require.EqualError(t, err, "resolve reference 'https://foo.bar#/bar' failed: path element 'bar' not found") @@ -435,3 +456,11 @@ type pathResolver struct { func (p *pathResolver) Resolve(token string) (interface{}, error) { return p.resolve(token) } + +type converter struct { + convert func(i interface{}) (interface{}, error) +} + +func (c *converter) ConvertTo(i interface{}) (interface{}, error) { + return c.convert(i) +} diff --git a/pkg/cmd/mokapi/sample-data.go b/pkg/cmd/mokapi/sample-data.go index 017967aa2..2f6e5499f 100644 --- a/pkg/cmd/mokapi/sample-data.go +++ b/pkg/cmd/mokapi/sample-data.go @@ -223,7 +223,7 @@ func readFromConfig(c *dynamic.Config, inputType *string, r dynamic.Reader) (sch *inputType = "openapi" } - err = dynamic.Resolve(fmt.Sprintf("#%s", c.Info.Url.Fragment), &schema, c, r) + schema, err = (&dynamic.Reference[any]{Ref: fmt.Sprintf("#%s", c.Info.Url.Fragment)}).Resolve(c, r) if err != nil { return nil, nil, fmt.Errorf("failed to resolve fragment %s: %v", c.Info.Url.Fragment, err) } diff --git a/providers/asyncapi3/asyncapi3test/channel.go b/providers/asyncapi3/asyncapi3test/channel.go index bcdfc94b4..8636991bb 100644 --- a/providers/asyncapi3/asyncapi3test/channel.go +++ b/providers/asyncapi3/asyncapi3test/channel.go @@ -51,7 +51,7 @@ func WithChannelDescription(desc string) ChannelOptions { func AssignToServer(ref string) ChannelOptions { return func(c *asyncapi3.Channel) { - c.Servers = append(c.Servers, &asyncapi3.ServerRef{Reference: dynamic.Reference{Ref: ref}}) + c.Servers = append(c.Servers, &asyncapi3.ServerRef{Reference: dynamic.Reference[*asyncapi3.ServerRef]{Ref: ref}}) } } diff --git a/providers/asyncapi3/channel.go b/providers/asyncapi3/channel.go index bebfcd89b..ca0f69f30 100644 --- a/providers/asyncapi3/channel.go +++ b/providers/asyncapi3/channel.go @@ -7,7 +7,7 @@ import ( ) type ChannelRef struct { - dynamic.Reference + dynamic.Reference[*ChannelRef] Value *Channel } @@ -40,8 +40,8 @@ func (r *ChannelRef) Parse(config *dynamic.Config, reader dynamic.Reader) error return nil } if len(r.Ref) > 0 { - var resolved *ChannelRef - if err := dynamic.Resolve(r.Ref, &resolved, config, reader); err != nil { + resolved, err := r.Resolve(config, reader) + if err != nil { return err } r.Value = resolved.Value diff --git a/providers/asyncapi3/components_test.go b/providers/asyncapi3/components_test.go index 96523509f..b310c57ee 100644 --- a/providers/asyncapi3/components_test.go +++ b/providers/asyncapi3/components_test.go @@ -36,7 +36,7 @@ components: c := &dynamic.Config{Data: cfg, Info: dynamic.ConfigInfo{Url: try.MustUrl("/foo")}} err := cfg.Parse(c, &dynamictest.Reader{}) - require.EqualError(t, err, "resolve reference '#/components/servers/foo' failed: resolve reference 'test.yaml#/components/servers/foo' failed: TestReader: config not found") + require.EqualError(t, err, "resolve reference '#/components/servers/foo' failed: resolve reference '/test.yaml#/components/servers/foo' failed: TestReader: config not found") err = cfg.Parse(c, dynamictest.ReaderFunc(func(u *url.URL, v any) (*dynamic.Config, error) { require.Equal(t, "/test.yaml", u.String()) @@ -58,7 +58,7 @@ components: c := &dynamic.Config{Data: cfg, Info: dynamic.ConfigInfo{Url: try.MustUrl("/foo")}} err := cfg.Parse(c, &dynamictest.Reader{}) - require.EqualError(t, err, `resolve reference 'test.yaml#/components/tags/foo' failed: TestReader: config not found`) + require.EqualError(t, err, `resolve reference '/test.yaml#/components/tags/foo' failed: TestReader: config not found`) err = cfg.Parse(c, dynamictest.ReaderFunc(func(u *url.URL, v any) (*dynamic.Config, error) { require.Equal(t, "/test.yaml", u.String()) @@ -79,7 +79,7 @@ channels: c := &dynamic.Config{Data: cfg, Info: dynamic.ConfigInfo{Url: try.MustUrl("/foo")}} err := cfg.Parse(c, &dynamictest.Reader{}) - require.EqualError(t, err, `resolve reference 'test.yaml#/components/channels/foo' failed: TestReader: config not found`) + require.EqualError(t, err, `resolve reference '/test.yaml#/components/channels/foo' failed: TestReader: config not found`) err = cfg.Parse(c, dynamictest.ReaderFunc(func(u *url.URL, v any) (*dynamic.Config, error) { require.Equal(t, "/test.yaml", u.String()) @@ -107,7 +107,7 @@ components: c := &dynamic.Config{Data: cfg, Info: dynamic.ConfigInfo{Url: try.MustUrl("/foo")}} err := cfg.Parse(c, &dynamictest.Reader{}) - require.EqualError(t, err, "resolve reference '#/components/schemas/foo' failed: resolve reference 'test.yaml#/components/schemas/foo' failed: TestReader: config not found") + require.EqualError(t, err, "resolve reference '#/components/schemas/foo' failed: resolve reference '/test.yaml#/components/schemas/foo' failed: TestReader: config not found") err = cfg.Parse(c, dynamictest.ReaderFunc(func(u *url.URL, v any) (*dynamic.Config, error) { require.Equal(t, "/test.yaml", u.String()) @@ -134,14 +134,14 @@ components: c := &dynamic.Config{Data: cfg, Info: dynamic.ConfigInfo{Url: try.MustUrl("/foo")}} err := cfg.Parse(c, &dynamictest.Reader{}) - require.EqualError(t, err, "resolve reference '#/components/messages/foo' failed: resolve reference 'test.yaml#/components/messages/foo' failed: TestReader: config not found") + require.EqualError(t, err, "resolve reference '#/components/messages/foo' failed: resolve reference '/test.yaml#/components/messages/foo' failed: TestReader: config not found") err = cfg.Parse(c, dynamictest.ReaderFunc(func(u *url.URL, v any) (*dynamic.Config, error) { require.Equal(t, "/test.yaml", u.String()) return &dynamic.Config{Data: asyncapi3test.NewConfig(asyncapi3test.WithComponentMessage("foo", &asyncapi3.Message{Title: "FOO"}))}, nil })) require.NoError(t, err) - require.Equal(t, "FOO", cfg.Components.Messages["foo"].Value.Title) + require.Equal(t, "FOO", cfg.Channels["foo"].Value.Messages["msg"].Value.Title) }, }, { @@ -159,7 +159,7 @@ components: c := &dynamic.Config{Data: cfg, Info: dynamic.ConfigInfo{Url: try.MustUrl("/foo")}} err := cfg.Parse(c, &dynamictest.Reader{}) - require.EqualError(t, err, "resolve reference '#/components/operations/foo' failed: resolve reference 'test.yaml#/components/operations/foo' failed: TestReader: config not found") + require.EqualError(t, err, "resolve reference '#/components/operations/foo' failed: resolve reference '/test.yaml#/components/operations/foo' failed: TestReader: config not found") err = cfg.Parse(c, dynamictest.ReaderFunc(func(u *url.URL, v any) (*dynamic.Config, error) { require.Equal(t, "/test.yaml", u.String()) @@ -186,7 +186,7 @@ components: c := &dynamic.Config{Data: cfg, Info: dynamic.ConfigInfo{Url: try.MustUrl("/foo")}} err := cfg.Parse(c, &dynamictest.Reader{}) - require.EqualError(t, err, "resolve reference '#/components/parameters/foo' failed: resolve reference 'test.yaml#/components/parameters/foo' failed: TestReader: config not found") + require.EqualError(t, err, "resolve reference '#/components/parameters/foo' failed: resolve reference '/test.yaml#/components/parameters/foo' failed: TestReader: config not found") err = cfg.Parse(c, dynamictest.ReaderFunc(func(u *url.URL, v any) (*dynamic.Config, error) { require.Equal(t, "/test.yaml", u.String()) @@ -214,7 +214,7 @@ components: c := &dynamic.Config{Data: cfg, Info: dynamic.ConfigInfo{Url: try.MustUrl("/foo")}} err := cfg.Parse(c, &dynamictest.Reader{}) - require.EqualError(t, err, "resolve reference '#/components/correlationIds/foo' failed: resolve reference 'test.yaml#/components/correlationIds/foo' failed: TestReader: config not found") + require.EqualError(t, err, "resolve reference '#/components/correlationIds/foo' failed: resolve reference '/test.yaml#/components/correlationIds/foo' failed: TestReader: config not found") err = cfg.Parse(c, dynamictest.ReaderFunc(func(u *url.URL, v any) (*dynamic.Config, error) { require.Equal(t, "/test.yaml", u.String()) @@ -242,7 +242,7 @@ components: c := &dynamic.Config{Data: cfg, Info: dynamic.ConfigInfo{Url: try.MustUrl("/foo")}} err := cfg.Parse(c, &dynamictest.Reader{}) - require.EqualError(t, err, "resolve reference '#/components/externalDocs/foo' failed: resolve reference 'test.yaml#/components/externalDocs/foo' failed: TestReader: config not found") + require.EqualError(t, err, "resolve reference '#/components/externalDocs/foo' failed: resolve reference '/test.yaml#/components/externalDocs/foo' failed: TestReader: config not found") err = cfg.Parse(c, dynamictest.ReaderFunc(func(u *url.URL, v any) (*dynamic.Config, error) { require.Equal(t, "/test.yaml", u.String()) @@ -268,7 +268,7 @@ components: c := &dynamic.Config{Data: cfg, Info: dynamic.ConfigInfo{Url: try.MustUrl("/foo")}} err := cfg.Parse(c, &dynamictest.Reader{}) - require.EqualError(t, err, "resolve reference '#/components/operationTraits/foo' failed: resolve reference 'test.yaml#/components/operationTraits/foo' failed: TestReader: config not found") + require.EqualError(t, err, "resolve reference '#/components/operationTraits/foo' failed: resolve reference '/test.yaml#/components/operationTraits/foo' failed: TestReader: config not found") err = cfg.Parse(c, dynamictest.ReaderFunc(func(u *url.URL, v any) (*dynamic.Config, error) { require.Equal(t, "/test.yaml", u.String()) @@ -296,7 +296,7 @@ components: c := &dynamic.Config{Data: cfg, Info: dynamic.ConfigInfo{Url: try.MustUrl("/foo")}} err := cfg.Parse(c, &dynamictest.Reader{}) - require.EqualError(t, err, "resolve reference '#/components/messageTraits/foo' failed: resolve reference 'test.yaml#/components/messageTraits/foo' failed: TestReader: config not found") + require.EqualError(t, err, "resolve reference '#/components/messageTraits/foo' failed: resolve reference '/test.yaml#/components/messageTraits/foo' failed: TestReader: config not found") err = cfg.Parse(c, dynamictest.ReaderFunc(func(u *url.URL, v any) (*dynamic.Config, error) { require.Equal(t, "/test.yaml", u.String()) diff --git a/providers/asyncapi3/config_test.go b/providers/asyncapi3/config_test.go index d817e5905..51c9923b1 100644 --- a/providers/asyncapi3/config_test.go +++ b/providers/asyncapi3/config_test.go @@ -309,6 +309,7 @@ components: require.NotNil(t, ch) msg := ch.Value.Messages["msg"] require.NotNil(t, msg) + require.IsType(t, &schema.Schema{}, msg.Value.Payload.Value) s := msg.Value.Payload.Value.(*schema.Schema) require.Equal(t, "string", s.Type.String()) }, diff --git a/providers/asyncapi3/correlation_id.go b/providers/asyncapi3/correlation_id.go index e04110885..722f4a1da 100644 --- a/providers/asyncapi3/correlation_id.go +++ b/providers/asyncapi3/correlation_id.go @@ -7,7 +7,7 @@ import ( ) type CorrelationIdRef struct { - dynamic.Reference + dynamic.Reference[*CorrelationIdRef] Value *CorrelationId } @@ -18,11 +18,12 @@ type CorrelationId struct { func (r *CorrelationIdRef) Parse(config *dynamic.Config, reader dynamic.Reader) error { if len(r.Ref) > 0 { - var resolved *CorrelationIdRef - if err := dynamic.Resolve(r.Ref, &resolved, config, reader); err != nil { + resolved, err := r.Resolve(config, reader) + if err != nil { return err } r.Value = resolved.Value + return nil } return nil } diff --git a/providers/asyncapi3/external_doc.go b/providers/asyncapi3/external_doc.go index d140cf27b..ae41dd8d5 100644 --- a/providers/asyncapi3/external_doc.go +++ b/providers/asyncapi3/external_doc.go @@ -7,7 +7,7 @@ import ( ) type ExternalDocRef struct { - dynamic.Reference + dynamic.Reference[*ExternalDocRef] Value *ExternalDoc } @@ -26,11 +26,12 @@ func (r *ExternalDocRef) UnmarshalJSON(b []byte) error { func (r *ExternalDocRef) Parse(config *dynamic.Config, reader dynamic.Reader) error { if len(r.Ref) > 0 { - var resolved *ExternalDocRef - if err := dynamic.Resolve(r.Ref, &resolved, config, reader); err != nil { + resolved, err := r.Resolve(config, reader) + if err != nil { return err } r.Value = resolved.Value + return nil } return nil diff --git a/providers/asyncapi3/message.go b/providers/asyncapi3/message.go index a626b2d0b..8b57972b7 100644 --- a/providers/asyncapi3/message.go +++ b/providers/asyncapi3/message.go @@ -8,7 +8,7 @@ import ( ) type MessageRef struct { - dynamic.Reference + dynamic.Reference[*MessageRef] Value *Message } @@ -33,7 +33,7 @@ type Message struct { } type MessageTraitRef struct { - dynamic.Reference + dynamic.Reference[*MessageTraitRef] Value *MessageTrait } @@ -75,8 +75,8 @@ func (r *MessageRef) Parse(config *dynamic.Config, reader dynamic.Reader) error return nil } if r.Ref != "" { - var resolved *MessageRef - if err := dynamic.Resolve(r.Ref, &resolved, config, reader); err != nil { + resolved, err := r.Resolve(config, reader) + if err != nil { return err } r.Value = resolved.Value @@ -144,8 +144,8 @@ func (m *Message) Parse(config *dynamic.Config, reader dynamic.Reader) error { func (r *MessageTraitRef) Parse(config *dynamic.Config, reader dynamic.Reader) error { if len(r.Ref) > 0 { - var resolved *MessageTraitRef - if err := dynamic.Resolve(r.Ref, &resolved, config, reader); err != nil { + resolved, err := r.Resolve(config, reader) + if err != nil { return err } r.Value = resolved.Value diff --git a/providers/asyncapi3/operation.go b/providers/asyncapi3/operation.go index 09ada4a33..9599e9ee2 100644 --- a/providers/asyncapi3/operation.go +++ b/providers/asyncapi3/operation.go @@ -7,7 +7,7 @@ import ( ) type OperationRef struct { - dynamic.Reference + dynamic.Reference[*OperationRef] Value *Operation } @@ -25,7 +25,7 @@ type Operation struct { } type OperationTraitRef struct { - dynamic.Reference + dynamic.Reference[*OperationTraitRef] Value *OperationTrait } @@ -61,8 +61,8 @@ func (r *OperationRef) Parse(config *dynamic.Config, reader dynamic.Reader) erro } if len(r.Ref) > 0 { - var resolved *OperationRef - if err := dynamic.Resolve(r.Ref, &resolved, config, reader); err != nil { + resolved, err := r.Resolve(config, reader) + if err != nil { return err } r.Value = resolved.Value @@ -77,8 +77,9 @@ func (o *Operation) Parse(config *dynamic.Config, reader dynamic.Reader) error { } if len(o.Channel.Ref) > 0 { - var resolved *ChannelRef - if err := dynamic.Resolve(o.Channel.Ref, &resolved, config, reader); err != nil { + r := dynamic.Reference[ChannelRef]{Ref: o.Channel.Ref} + resolved, err := r.Resolve(config, reader) + if err != nil { return err } o.Channel.Value = resolved.Value @@ -102,8 +103,8 @@ func (o *Operation) Parse(config *dynamic.Config, reader dynamic.Reader) error { func (r *OperationTraitRef) Parse(config *dynamic.Config, reader dynamic.Reader) error { if len(r.Ref) > 0 { - var resolved *OperationTraitRef - if err := dynamic.Resolve(r.Ref, &resolved, config, reader); err != nil { + resolved, err := r.Resolve(config, reader) + if err != nil { return err } r.Value = resolved.Value diff --git a/providers/asyncapi3/parameter.go b/providers/asyncapi3/parameter.go index df41dc09e..ade8cab25 100644 --- a/providers/asyncapi3/parameter.go +++ b/providers/asyncapi3/parameter.go @@ -7,7 +7,7 @@ import ( ) type ParameterRef struct { - dynamic.Reference + dynamic.Reference[*ParameterRef] Value *Parameter } @@ -29,11 +29,12 @@ func (r *ParameterRef) UnmarshalJSON(b []byte) error { func (r *ParameterRef) Parse(config *dynamic.Config, reader dynamic.Reader) error { if len(r.Ref) > 0 { - var resolved *ParameterRef - if err := dynamic.Resolve(r.Ref, &resolved, config, reader); err != nil { + resolved, err := r.Resolve(config, reader) + if err != nil { return err } r.Value = resolved.Value + return nil } return nil diff --git a/providers/asyncapi3/schema.go b/providers/asyncapi3/schema.go index 1f5f2bee3..35cf212c2 100644 --- a/providers/asyncapi3/schema.go +++ b/providers/asyncapi3/schema.go @@ -17,7 +17,7 @@ import ( ) type SchemaRef struct { - dynamic.Reference + dynamic.Reference[*SchemaRef] Value Schema } @@ -78,15 +78,13 @@ func (r *SchemaRef) UnmarshalJSON(b []byte) error { func (r *SchemaRef) Parse(config *dynamic.Config, reader dynamic.Reader) error { if len(r.Ref) > 0 { - var resolved *SchemaRef - err := dynamic.Resolve(r.Ref, &resolved, config, reader) + resolved, err := r.Resolve(config, reader) if err != nil { - s := &SchemaRef{Value: &jsonSchema.Schema{}} - err = dynamic.Resolve(r.Ref, &s.Value, config, reader) + ra := dynamic.Reference[Schema]{Ref: r.Ref} + r.Value, err = ra.Resolve(config, reader) if err != nil { return err } - r.Value = s.Value } else { r.Value = resolved.Value } @@ -120,6 +118,13 @@ func (r *SchemaRef) Marshal(v any, ct media.ContentType) ([]byte, error) { } } +//func (r *SchemaRef) ConvertFrom(val any) (any, error) { +// if s, ok := val.(Schema); ok { +// return &SchemaRef{Value: s}, nil +// } +// return nil, fmt.Errorf("unsupported type: %T", val) +//} + func (r *SchemaRef) GetSchema() (Schema, error) { if r.Value == nil { return nil, nil @@ -380,12 +385,16 @@ func isOpenApi(format string) bool { type AvroRef struct { *avro.Schema - dynamic.Reference + dynamic.Reference[*AvroRef] } func (r *AvroRef) Parse(config *dynamic.Config, reader dynamic.Reader) error { if r.Ref != "" { - return dynamic.Resolve(r.Ref, &r.Schema, config, reader) + resolved, err := r.Resolve(config, reader) + if err != nil { + return err + } + r.Schema = resolved.Schema } return nil } diff --git a/providers/asyncapi3/server.go b/providers/asyncapi3/server.go index e68ab2de7..60539969b 100644 --- a/providers/asyncapi3/server.go +++ b/providers/asyncapi3/server.go @@ -7,7 +7,7 @@ import ( ) type ServerRef struct { - dynamic.Reference + dynamic.Reference[*ServerRef] Value *Server } @@ -26,7 +26,7 @@ type Server struct { } type ServerVariableRef struct { - dynamic.Reference + dynamic.Reference[ServerVariableRef] Value *ServerVariable } @@ -39,8 +39,8 @@ type ServerVariable struct { func (r *ServerRef) Parse(config *dynamic.Config, reader dynamic.Reader) error { if len(r.Ref) > 0 { - var resolved *ServerRef - if err := dynamic.Resolve(r.Ref, &resolved, config, reader); err != nil { + resolved, err := r.Resolve(config, reader) + if err != nil { return err } r.Value = resolved.Value @@ -80,7 +80,12 @@ func (r *ServerRef) Parse(config *dynamic.Config, reader dynamic.Reader) error { func (r *ServerVariableRef) parse(config *dynamic.Config, reader dynamic.Reader) error { if len(r.Ref) > 0 { - return dynamic.Resolve(r.Ref, &r.Value, config, reader) + resolved, err := r.Resolve(config, reader) + if err != nil { + return err + } + r.Value = resolved.Value + return nil } return nil diff --git a/providers/asyncapi3/tags.go b/providers/asyncapi3/tags.go index 252722bf3..4c5067d68 100644 --- a/providers/asyncapi3/tags.go +++ b/providers/asyncapi3/tags.go @@ -7,7 +7,7 @@ import ( ) type TagRef struct { - dynamic.Reference + dynamic.Reference[*TagRef] Value *Tag } @@ -27,8 +27,8 @@ func (r *TagRef) UnmarshalJSON(b []byte) error { func (r *TagRef) parse(config *dynamic.Config, reader dynamic.Reader) error { if len(r.Ref) > 0 { - var resolved *TagRef - if err := dynamic.Resolve(r.Ref, &resolved, config, reader); err != nil { + resolved, err := r.Resolve(config, reader) + if err != nil { return err } r.Value = resolved.Value diff --git a/providers/openapi/components_test.go b/providers/openapi/components_test.go index 752353a41..4a8c9d932 100644 --- a/providers/openapi/components_test.go +++ b/providers/openapi/components_test.go @@ -249,7 +249,7 @@ func TestComponents_Parse(t *testing.T) { ), ) err := config.Parse(&dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: config}, reader) - require.EqualError(t, err, "parse path '/foo' failed: parse operation 'GET' failed: parse response '200' failed: parse content '' failed: parse schema failed: resolve reference 'foo.yml#/components/schemas/foo' failed: TESTING ERROR") + require.EqualError(t, err, "parse path '/foo' failed: parse operation 'GET' failed: parse response '200' failed: parse content '' failed: parse schema failed: resolve reference '/foo.yml#/components/schemas/foo' failed: TESTING ERROR") }, }, { @@ -301,7 +301,7 @@ func TestComponents_Parse(t *testing.T) { ), ) err := config.Parse(&dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: config}, reader) - require.EqualError(t, err, "parse path '/foo' failed: parse operation 'GET' failed: parse response '200' failed: resolve reference 'foo.yml#/components/responses/foo' failed: TESTING ERROR") + require.EqualError(t, err, "parse path '/foo' failed: parse operation 'GET' failed: parse response '200' failed: resolve reference '/foo.yml#/components/responses/foo' failed: TESTING ERROR") }, }, { @@ -347,7 +347,7 @@ func TestComponents_Parse(t *testing.T) { ), ) err := config.Parse(&dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: config}, reader) - require.EqualError(t, err, "parse path '/foo' failed: parse operation 'GET' failed: parse request body failed: resolve reference 'foo.yml#/components/requestBodies/foo' failed: TESTING ERROR") + require.EqualError(t, err, "parse path '/foo' failed: parse operation 'GET' failed: parse request body failed: resolve reference '/foo.yml#/components/requestBodies/foo' failed: TESTING ERROR") }, }, { @@ -390,7 +390,7 @@ func TestComponents_Parse(t *testing.T) { ), ) err := config.Parse(&dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: config}, reader) - require.EqualError(t, err, "parse path '/foo' failed: parse parameter '0' failed: resolve reference 'foo.yml#/components/parameters/foo' failed: TESTING ERROR") + require.EqualError(t, err, "parse path '/foo' failed: parse parameter '0' failed: resolve reference '/foo.yml#/components/parameters/foo' failed: TESTING ERROR") }, }, { @@ -446,7 +446,7 @@ func TestComponents_Parse(t *testing.T) { ), ) err := config.Parse(&dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: config}, reader) - require.EqualError(t, err, "parse path '/foo' failed: parse operation 'GET' failed: parse response '200' failed: parse content 'application/json' failed: parse example 'foo' failed: resolve reference 'foo.yml#/components/parameters/foo' failed: TESTING ERROR") + require.EqualError(t, err, "parse path '/foo' failed: parse operation 'GET' failed: parse response '200' failed: parse content 'application/json' failed: parse example 'foo' failed: resolve reference '/foo.yml#/components/parameters/foo' failed: TESTING ERROR") }, }, { @@ -497,7 +497,7 @@ func TestComponents_Parse(t *testing.T) { ), ) err := config.Parse(&dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: config}, reader) - require.EqualError(t, err, "parse path '/foo' failed: parse operation 'GET' failed: parse response '200' failed: parse header 'foo' failed: resolve reference 'foo.yml#/components/headers/foo' failed: TESTING ERROR") + require.EqualError(t, err, "parse path '/foo' failed: parse operation 'GET' failed: parse response '200' failed: parse header 'foo' failed: resolve reference '/foo.yml#/components/headers/foo' failed: TESTING ERROR") }, }, } diff --git a/providers/openapi/example.go b/providers/openapi/example.go index a7f691fc2..03680515b 100644 --- a/providers/openapi/example.go +++ b/providers/openapi/example.go @@ -15,7 +15,7 @@ type ExampleValue struct { type Examples map[string]*ExampleRef type ExampleRef struct { - dynamic.Reference + dynamic.Reference[*ExampleRef] Value *Example } @@ -89,8 +89,8 @@ func (r *ExampleRef) Parse(config *dynamic.Config, reader dynamic.Reader) error } if len(r.Ref) > 0 { - var resolved *ExampleRef - if err := dynamic.Resolve(r.Ref, &resolved, config, reader); err != nil { + resolved, err := r.Resolve(config, reader) + if err != nil { return err } r.Value = resolved.Value @@ -106,7 +106,13 @@ func (e *Example) Parse(config *dynamic.Config, reader dynamic.Reader) error { } if e.ExternalValue != "" { - return dynamic.Resolve(e.ExternalValue, &e.Value, config, reader) + r := dynamic.Reference[string]{Ref: e.ExternalValue} + resolved, err := r.Resolve(config, reader) + if err != nil { + return err + } + e.Value = resolved + return nil } return nil diff --git a/providers/openapi/example_test.go b/providers/openapi/example_test.go index be4f329d9..ab9f6d492 100644 --- a/providers/openapi/example_test.go +++ b/providers/openapi/example_test.go @@ -195,14 +195,14 @@ func TestExample_Parse(t *testing.T) { openapitest.WithOperation(http.MethodGet, openapitest.WithResponse(http.StatusOK, openapitest.UseContent("application/json", &openapi.MediaType{ - Examples: map[string]*openapi.ExampleRef{"foo": {Reference: dynamic.Reference{Ref: "foo.yml"}}}, + Examples: map[string]*openapi.ExampleRef{"foo": {Reference: dynamic.Reference[*openapi.ExampleRef]{Ref: "foo.yml"}}}, }), ), ), ), ) err := config.Parse(&dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: config}, reader) - require.EqualError(t, err, "parse path '/foo' failed: parse operation 'GET' failed: parse response '200' failed: parse content 'application/json' failed: parse example 'foo' failed: resolve reference 'foo.yml' failed: TEST ERROR") + require.EqualError(t, err, "parse path '/foo' failed: parse operation 'GET' failed: parse response '200' failed: parse content 'application/json' failed: parse example 'foo' failed: resolve reference '/foo.yml' failed: TEST ERROR") }, }, { diff --git a/providers/openapi/header.go b/providers/openapi/header.go index 76c8808e8..26419973a 100644 --- a/providers/openapi/header.go +++ b/providers/openapi/header.go @@ -15,7 +15,7 @@ import ( type Headers map[string]*HeaderRef type HeaderRef struct { - dynamic.Reference + dynamic.Reference[*HeaderRef] Value *Header } @@ -71,8 +71,8 @@ func (r *HeaderRef) Parse(config *dynamic.Config, reader dynamic.Reader) error { } if len(r.Ref) > 0 { - var resolved *HeaderRef - if err := dynamic.Resolve(r.Ref, &resolved, config, reader); err != nil { + resolved, err := r.Resolve(config, reader) + if err != nil { return err } r.Value = resolved.Value diff --git a/providers/openapi/header_test.go b/providers/openapi/header_test.go index e6b15162c..79f662d1c 100644 --- a/providers/openapi/header_test.go +++ b/providers/openapi/header_test.go @@ -288,7 +288,7 @@ func TestHeader_Parse(t *testing.T) { ), ) err := config.Parse(&dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: config}, reader) - require.EqualError(t, err, "parse path '/foo' failed: parse operation 'GET' failed: parse response '200' failed: parse header 'foo' failed: resolve reference 'foo' failed: TEST ERROR") + require.EqualError(t, err, "parse path '/foo' failed: parse operation 'GET' failed: parse response '200' failed: parse header 'foo' failed: resolve reference '/foo' failed: TEST ERROR") }, }, } diff --git a/providers/openapi/media_type_test.go b/providers/openapi/media_type_test.go index c86974cec..6770e4771 100644 --- a/providers/openapi/media_type_test.go +++ b/providers/openapi/media_type_test.go @@ -158,7 +158,7 @@ func TestMediaType_Parse(t *testing.T) { ), ) err := config.Parse(&dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: config}, reader) - require.EqualError(t, err, "parse path '/foo' failed: parse operation 'GET' failed: parse response '200' failed: parse content 'application/json' failed: parse schema failed: resolve reference 'foo.yml' failed: TEST ERROR") + require.EqualError(t, err, "parse path '/foo' failed: parse operation 'GET' failed: parse response '200' failed: parse content 'application/json' failed: parse schema failed: resolve reference '/foo.yml' failed: TEST ERROR") }, }, { @@ -172,14 +172,14 @@ func TestMediaType_Parse(t *testing.T) { openapitest.WithOperation(http.MethodGet, openapitest.WithResponse(http.StatusOK, openapitest.UseContent("application/json", - &openapi.MediaType{Examples: map[string]*openapi.ExampleRef{"foo": {Reference: dynamic.Reference{Ref: "foo.yml"}}}}, + &openapi.MediaType{Examples: map[string]*openapi.ExampleRef{"foo": {Reference: dynamic.Reference[*openapi.ExampleRef]{Ref: "foo.yml"}}}}, ), ), ), ), ) err := config.Parse(&dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: config}, reader) - require.EqualError(t, err, "parse path '/foo' failed: parse operation 'GET' failed: parse response '200' failed: parse content 'application/json' failed: parse example 'foo' failed: resolve reference 'foo.yml' failed: TEST ERROR") + require.EqualError(t, err, "parse path '/foo' failed: parse operation 'GET' failed: parse response '200' failed: parse content 'application/json' failed: parse example 'foo' failed: resolve reference '/foo.yml' failed: TEST ERROR") }, }, } diff --git a/providers/openapi/openapitest/operation.go b/providers/openapi/openapitest/operation.go index 8defe7640..082aa2cae 100644 --- a/providers/openapi/openapitest/operation.go +++ b/providers/openapi/openapitest/operation.go @@ -46,7 +46,7 @@ func UseResponse(status int, r *openapi.Response) OperationOptions { func WithResponseRef(status int, ref string) OperationOptions { return func(o *openapi.Operation) { o.Responses.Set(strconv.Itoa(status), &openapi.ResponseRef{ - Reference: dynamic.Reference{Ref: ref}, + Reference: dynamic.Reference[*openapi.ResponseRef]{Ref: ref}, }) } } @@ -70,7 +70,7 @@ func WithOperationParam(name string, required bool, opts ...ParamOptions) Operat func WithOperationParamRef(ref string) OperationOptions { return func(o *openapi.Operation) { - o.Parameters = append(o.Parameters, &openapi.ParameterRef{Reference: dynamic.Reference{Ref: ref}}) + o.Parameters = append(o.Parameters, &openapi.ParameterRef{Reference: dynamic.Reference[*openapi.ParameterRef]{Ref: ref}}) } } @@ -128,7 +128,7 @@ func WithRequestBody(description string, required bool, opts ...RequestBodyOptio func WithRequestBodyRef(ref string) OperationOptions { return func(o *openapi.Operation) { o.RequestBody = &openapi.RequestBodyRef{ - Reference: dynamic.Reference{Ref: ref}, + Reference: dynamic.Reference[*openapi.RequestBodyRef]{Ref: ref}, } } } diff --git a/providers/openapi/openapitest/path.go b/providers/openapi/openapitest/path.go index 7eade336b..b266dd730 100644 --- a/providers/openapi/openapitest/path.go +++ b/providers/openapi/openapitest/path.go @@ -83,7 +83,7 @@ func WithPathParam(name string, opts ...ParamOptions) PathOptions { func WithPathParamRef(ref string) PathOptions { return func(e *openapi.Path) { e.Parameters = append(e.Parameters, &openapi.ParameterRef{ - Reference: dynamic.Reference{Ref: ref}, + Reference: dynamic.Reference[*openapi.ParameterRef]{Ref: ref}, }) } } diff --git a/providers/openapi/openapitest/response.go b/providers/openapi/openapitest/response.go index 8781a5be8..108b7c042 100644 --- a/providers/openapi/openapitest/response.go +++ b/providers/openapi/openapitest/response.go @@ -41,7 +41,7 @@ func WithResponseHeaderRef(name string, ref string) ResponseOptions { if o.Headers == nil { o.Headers = map[string]*openapi.HeaderRef{} } - o.Headers[name] = &openapi.HeaderRef{Reference: dynamic.Reference{Ref: ref}} + o.Headers[name] = &openapi.HeaderRef{Reference: dynamic.Reference[*openapi.HeaderRef]{Ref: ref}} } } @@ -112,7 +112,7 @@ func WithExampleRef(name, ref string) ContentOptions { if c.Examples == nil { c.Examples = map[string]*openapi.ExampleRef{} } - c.Examples[name] = &openapi.ExampleRef{Reference: dynamic.Reference{Ref: ref}} + c.Examples[name] = &openapi.ExampleRef{Reference: dynamic.Reference[*openapi.ExampleRef]{Ref: ref}} } } @@ -122,8 +122,8 @@ func WithSchema(s *schema.Schema) ContentOptions { } } -func WithSchemaRef(r string) ContentOptions { +func WithSchemaRef(ref string) ContentOptions { return func(c *openapi.MediaType) { - c.Schema = &schema.Schema{Ref: r} + c.Schema = &schema.Schema{Reference: dynamic.Reference[*schema.Schema]{Ref: ref}} } } diff --git a/providers/openapi/operation_test.go b/providers/openapi/operation_test.go index e4eb51271..7a2312c92 100644 --- a/providers/openapi/operation_test.go +++ b/providers/openapi/operation_test.go @@ -232,7 +232,7 @@ func TestOperation_Parse(t *testing.T) { openapitest.WithOperationParamRef("foo.yml"))), ) err := config.Parse(&dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: config}, reader) - require.EqualError(t, err, "parse path '/foo' failed: parse operation 'GET' failed: parse parameter index '0' failed: resolve reference 'foo.yml' failed: TEST ERROR") + require.EqualError(t, err, "parse path '/foo' failed: parse operation 'GET' failed: parse parameter index '0' failed: resolve reference '/foo.yml' failed: TEST ERROR") }, }, { @@ -264,7 +264,7 @@ func TestOperation_Parse(t *testing.T) { openapitest.WithOperationParamRef("foo.yml"))), ) err := config.Parse(&dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: config}, reader) - require.EqualError(t, err, "parse path '/foo' failed: parse operation 'POST' failed: parse parameter index '0' failed: resolve reference 'foo.yml' failed: TEST ERROR") + require.EqualError(t, err, "parse path '/foo' failed: parse operation 'POST' failed: parse parameter index '0' failed: resolve reference '/foo.yml' failed: TEST ERROR") }, }, { @@ -296,7 +296,7 @@ func TestOperation_Parse(t *testing.T) { openapitest.WithOperationParamRef("foo.yml"))), ) err := config.Parse(&dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: config}, reader) - require.EqualError(t, err, "parse path '/foo' failed: parse operation 'PUT' failed: parse parameter index '0' failed: resolve reference 'foo.yml' failed: TEST ERROR") + require.EqualError(t, err, "parse path '/foo' failed: parse operation 'PUT' failed: parse parameter index '0' failed: resolve reference '/foo.yml' failed: TEST ERROR") }, }, { @@ -328,7 +328,7 @@ func TestOperation_Parse(t *testing.T) { openapitest.WithOperationParamRef("foo.yml"))), ) err := config.Parse(&dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: config}, reader) - require.EqualError(t, err, "parse path '/foo' failed: parse operation 'PATCH' failed: parse parameter index '0' failed: resolve reference 'foo.yml' failed: TEST ERROR") + require.EqualError(t, err, "parse path '/foo' failed: parse operation 'PATCH' failed: parse parameter index '0' failed: resolve reference '/foo.yml' failed: TEST ERROR") }, }, { @@ -360,7 +360,7 @@ func TestOperation_Parse(t *testing.T) { openapitest.WithOperationParamRef("foo.yml"))), ) err := config.Parse(&dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: config}, reader) - require.EqualError(t, err, "parse path '/foo' failed: parse operation 'DELETE' failed: parse parameter index '0' failed: resolve reference 'foo.yml' failed: TEST ERROR") + require.EqualError(t, err, "parse path '/foo' failed: parse operation 'DELETE' failed: parse parameter index '0' failed: resolve reference '/foo.yml' failed: TEST ERROR") }, }, { @@ -392,7 +392,7 @@ func TestOperation_Parse(t *testing.T) { openapitest.WithOperationParamRef("foo.yml"))), ) err := config.Parse(&dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: config}, reader) - require.EqualError(t, err, "parse path '/foo' failed: parse operation 'HEAD' failed: parse parameter index '0' failed: resolve reference 'foo.yml' failed: TEST ERROR") + require.EqualError(t, err, "parse path '/foo' failed: parse operation 'HEAD' failed: parse parameter index '0' failed: resolve reference '/foo.yml' failed: TEST ERROR") }, }, { @@ -424,7 +424,7 @@ func TestOperation_Parse(t *testing.T) { openapitest.WithOperationParamRef("foo.yml"))), ) err := config.Parse(&dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: config}, reader) - require.EqualError(t, err, "parse path '/foo' failed: parse operation 'OPTIONS' failed: parse parameter index '0' failed: resolve reference 'foo.yml' failed: TEST ERROR") + require.EqualError(t, err, "parse path '/foo' failed: parse operation 'OPTIONS' failed: parse parameter index '0' failed: resolve reference '/foo.yml' failed: TEST ERROR") }, }, { @@ -456,7 +456,7 @@ func TestOperation_Parse(t *testing.T) { openapitest.WithOperationParamRef("foo.yml"))), ) err := config.Parse(&dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: config}, reader) - require.EqualError(t, err, "parse path '/foo' failed: parse operation 'TRACE' failed: parse parameter index '0' failed: resolve reference 'foo.yml' failed: TEST ERROR") + require.EqualError(t, err, "parse path '/foo' failed: parse operation 'TRACE' failed: parse parameter index '0' failed: resolve reference '/foo.yml' failed: TEST ERROR") }, }, { @@ -488,7 +488,7 @@ func TestOperation_Parse(t *testing.T) { openapitest.WithOperationParamRef("foo.yml"))), ) err := config.Parse(&dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: config}, reader) - require.EqualError(t, err, "parse path '/foo' failed: parse operation 'QUERY' failed: parse parameter index '0' failed: resolve reference 'foo.yml' failed: TEST ERROR") + require.EqualError(t, err, "parse path '/foo' failed: parse operation 'QUERY' failed: parse parameter index '0' failed: resolve reference '/foo.yml' failed: TEST ERROR") }, }, { @@ -520,7 +520,7 @@ func TestOperation_Parse(t *testing.T) { openapitest.WithOperationParamRef("foo.yml"))), ) err := config.Parse(&dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: config}, reader) - require.EqualError(t, err, "parse path '/foo' failed: parse operation 'LINK' failed: parse parameter index '0' failed: resolve reference 'foo.yml' failed: TEST ERROR") + require.EqualError(t, err, "parse path '/foo' failed: parse operation 'LINK' failed: parse parameter index '0' failed: resolve reference '/foo.yml' failed: TEST ERROR") }, }, { @@ -556,7 +556,7 @@ func TestOperation_Parse(t *testing.T) { config := openapitest.NewConfig("3.0", openapitest.WithPath("/foo", openapitest.UseOperation(http.MethodTrace, &openapi.Operation{ - RequestBody: &openapi.RequestBodyRef{Reference: dynamic.Reference{Ref: "foo.yml#/components/requestBodies/foo"}}, + RequestBody: &openapi.RequestBodyRef{Reference: dynamic.Reference[*openapi.RequestBodyRef]{Ref: "foo.yml#/components/requestBodies/foo"}}, }), ), ) @@ -575,12 +575,12 @@ func TestOperation_Parse(t *testing.T) { config := openapitest.NewConfig("3.0", openapitest.WithPath("/foo", openapitest.UseOperation(http.MethodTrace, &openapi.Operation{ - RequestBody: &openapi.RequestBodyRef{Reference: dynamic.Reference{Ref: "foo.yml#/components/requestBodies/foo"}}, + RequestBody: &openapi.RequestBodyRef{Reference: dynamic.Reference[*openapi.RequestBodyRef]{Ref: "foo.yml#/components/requestBodies/foo"}}, }), ), ) err := config.Parse(&dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: config}, reader) - require.EqualError(t, err, "parse path '/foo' failed: parse operation 'TRACE' failed: parse request body failed: resolve reference 'foo.yml#/components/requestBodies/foo' failed: TEST ERROR") + require.EqualError(t, err, "parse path '/foo' failed: parse operation 'TRACE' failed: parse request body failed: resolve reference '/foo.yml#/components/requestBodies/foo' failed: TEST ERROR") }, }, } diff --git a/providers/openapi/parameter.go b/providers/openapi/parameter.go index 43288ce21..6fc4fc3c4 100644 --- a/providers/openapi/parameter.go +++ b/providers/openapi/parameter.go @@ -15,7 +15,7 @@ const ( ) type ParameterRef struct { - dynamic.Reference + dynamic.Reference[*ParameterRef] Value *Parameter } @@ -102,8 +102,8 @@ func (r *ParameterRef) Parse(config *dynamic.Config, reader dynamic.Reader) erro } if len(r.Ref) > 0 && r.Value == nil { - var resolved *ParameterRef - if err := dynamic.Resolve(r.Ref, &resolved, config, reader); err != nil { + resolved, err := r.Resolve(config, reader) + if err != nil { return err } r.Value = resolved.Value diff --git a/providers/openapi/parameter_test.go b/providers/openapi/parameter_test.go index f99aaa505..0fe7e8c69 100644 --- a/providers/openapi/parameter_test.go +++ b/providers/openapi/parameter_test.go @@ -313,7 +313,7 @@ func TestParameterHeader_Parse(t *testing.T) { cfg := &dynamic.Config{Info: dynamic.ConfigInfo{Url: u}, Data: &openapi.ParameterRef{Value: &openapi.Parameter{Description: "foo"}}} return cfg, nil }) - param := openapi.Parameters{&openapi.ParameterRef{Reference: dynamic.Reference{Ref: "foo.yml"}}} + param := openapi.Parameters{&openapi.ParameterRef{Reference: dynamic.Reference[*openapi.ParameterRef]{Ref: "foo.yml"}}} err := param.Parse(&dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: param}, reader) require.NoError(t, err) require.Equal(t, "foo", param[0].Value.Description) @@ -326,7 +326,7 @@ func TestParameterHeader_Parse(t *testing.T) { cfg := &dynamic.Config{Info: dynamic.ConfigInfo{Url: u}, Data: schematest.New("string")} return cfg, nil }) - param := openapi.Parameters{&openapi.ParameterRef{Value: &openapi.Parameter{Schema: &schema.Schema{Ref: "foo.yml"}}}} + param := openapi.Parameters{&openapi.ParameterRef{Value: &openapi.Parameter{Schema: &schema.Schema{Reference: dynamic.Reference[*schema.Schema]{Ref: "foo.yml"}}}}} err := param.Parse(&dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: param}, reader) require.NoError(t, err) require.Equal(t, "string", param[0].Value.Schema.Type.String()) @@ -338,9 +338,9 @@ func TestParameterHeader_Parse(t *testing.T) { reader := dynamictest.ReaderFunc(func(_ *url.URL, _ any) (*dynamic.Config, error) { return nil, fmt.Errorf("TEST ERROR") }) - param := openapi.Parameters{&openapi.ParameterRef{Reference: dynamic.Reference{Ref: "foo.yml"}}} + param := openapi.Parameters{&openapi.ParameterRef{Reference: dynamic.Reference[*openapi.ParameterRef]{Ref: "foo.yml"}}} err := param.Parse(&dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: param}, reader) - require.EqualError(t, err, "parse parameter index '0' failed: resolve reference 'foo.yml' failed: TEST ERROR") + require.EqualError(t, err, "parse parameter index '0' failed: resolve reference '/foo.yml' failed: TEST ERROR") }, }, { @@ -349,9 +349,9 @@ func TestParameterHeader_Parse(t *testing.T) { reader := dynamictest.ReaderFunc(func(_ *url.URL, _ any) (*dynamic.Config, error) { return nil, fmt.Errorf("TEST ERROR") }) - param := openapi.Parameters{&openapi.ParameterRef{Value: &openapi.Parameter{Schema: &schema.Schema{Ref: "foo.yml"}}}} + param := openapi.Parameters{&openapi.ParameterRef{Value: &openapi.Parameter{Schema: &schema.Schema{Reference: dynamic.Reference[*schema.Schema]{Ref: "foo.yml"}}}}} err := param.Parse(&dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: param}, reader) - require.EqualError(t, err, "parse parameter index '0' failed: parse schema failed: resolve reference 'foo.yml' failed: TEST ERROR") + require.EqualError(t, err, "parse parameter index '0' failed: parse schema failed: resolve reference '/foo.yml' failed: TEST ERROR") }, }, } diff --git a/providers/openapi/parse_test.go b/providers/openapi/parse_test.go index 2c9ae2a10..6b6820663 100644 --- a/providers/openapi/parse_test.go +++ b/providers/openapi/parse_test.go @@ -25,7 +25,7 @@ func Test_Parse(t *testing.T) { openapitest.WithOperation("get", openapitest.WithResponse(200, openapitest.UseContent("application/json", &openapi.MediaType{ - Schema: &schema.Schema{Ref: "#/components/schemas/Foo"}, + Schema: &schema.Schema{Reference: dynamic.Reference[*schema.Schema]{Ref: "#/components/schemas/Foo"}}, }, ), ), @@ -74,7 +74,7 @@ func Test_ParseAndPatch(t *testing.T) { openapitest.WithOperation("get", openapitest.WithResponse(200, openapitest.UseContent("application/json", &openapi.MediaType{ - Schema: &schema.Schema{Ref: "#/components/schemas/Foo"}, + Schema: &schema.Schema{Reference: dynamic.Reference[*schema.Schema]{Ref: "#/components/schemas/Foo"}}, }, ), ), diff --git a/providers/openapi/path.go b/providers/openapi/path.go index 31b629e0e..c1ceaaf8a 100644 --- a/providers/openapi/path.go +++ b/providers/openapi/path.go @@ -13,7 +13,7 @@ import ( type PathItems map[string]*PathRef type PathRef struct { - dynamic.Reference + dynamic.Reference[*PathRef] Value *Path } @@ -175,8 +175,8 @@ func (r *PathRef) Parse(name string, config *dynamic.Config, reader dynamic.Read }() if len(r.Ref) > 0 { - var resolved *PathRef - if err := dynamic.Resolve(r.Ref, &resolved, config, reader); err != nil { + resolved, err := r.Resolve(config, reader) + if err != nil { return err } r.Value = resolved.Value diff --git a/providers/openapi/path_test.go b/providers/openapi/path_test.go index f8b4454da..4fedb4f72 100644 --- a/providers/openapi/path_test.go +++ b/providers/openapi/path_test.go @@ -429,9 +429,9 @@ func TestPath_Parse(t *testing.T) { }) config := openapitest.NewConfig("3.0", openapitest.WithPathRef("foo", - &openapi.PathRef{Reference: dynamic.Reference{Ref: "foo.yml#/paths/foo"}})) + &openapi.PathRef{Reference: dynamic.Reference[*openapi.PathRef]{Ref: "foo.yml#/paths/foo"}})) err := config.Parse(&dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: config}, reader) - require.EqualError(t, err, "parse path 'foo' failed: resolve reference 'foo.yml#/paths/foo' failed: TEST ERROR") + require.EqualError(t, err, "parse path 'foo' failed: resolve reference '/foo.yml#/paths/foo' failed: TEST ERROR") }, }, { @@ -460,7 +460,7 @@ func TestPath_Parse(t *testing.T) { }) config := openapitest.NewConfig("3.0", openapitest.WithPathRef("/foo", - &openapi.PathRef{Reference: dynamic.Reference{Ref: "foo.yml#/paths/foo"}})) + &openapi.PathRef{Reference: dynamic.Reference[*openapi.PathRef]{Ref: "foo.yml#/paths/foo"}})) err := config.Parse(&dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: config}, reader) require.NoError(t, err) require.Equal(t, target, config.Paths["/foo"].Value) @@ -479,7 +479,7 @@ func TestPath_Parse(t *testing.T) { }) config := openapitest.NewConfig("3.0", openapitest.WithPathRef("/foo", - &openapi.PathRef{Reference: dynamic.Reference{Ref: "foo.yml#/paths/foo"}})) + &openapi.PathRef{Reference: dynamic.Reference[*openapi.PathRef]{Ref: "foo.yml#/paths/foo"}})) err := config.Parse(&dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: config}, reader) require.NoError(t, err) require.Nil(t, config.Paths["/foo"].Value) @@ -499,9 +499,9 @@ func TestPath_Parse(t *testing.T) { }) config := openapitest.NewConfig("3.0", openapitest.WithPathRef("/foo", - &openapi.PathRef{Reference: dynamic.Reference{Ref: "foo.yml#/paths/foo"}})) + &openapi.PathRef{Reference: dynamic.Reference[*openapi.PathRef]{Ref: "foo.yml#/paths/foo"}})) err := config.Parse(&dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: config}, reader) - require.EqualError(t, err, "parse path '/foo' failed: resolve reference 'foo.yml#/paths/foo' failed: value is null") + require.EqualError(t, err, "parse path '/foo' failed: resolve reference '/foo.yml#/paths/foo' failed: value is null") }, }, { @@ -516,7 +516,7 @@ func TestPath_Parse(t *testing.T) { ), ) err := config.Parse(&dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: config}, reader) - require.EqualError(t, err, "parse path '/foo' failed: parse parameter '0' failed: resolve reference 'foo.yml' failed: TEST ERROR") + require.EqualError(t, err, "parse path '/foo' failed: parse parameter '0' failed: resolve reference '/foo.yml' failed: TEST ERROR") }, }, { @@ -527,7 +527,7 @@ func TestPath_Parse(t *testing.T) { }) config := openapitest.NewConfig("3.0", openapitest.WithPathRef("foo", - &openapi.PathRef{Reference: dynamic.Reference{Ref: "#/components/pathItems/foo"}}, + &openapi.PathRef{Reference: dynamic.Reference[*openapi.PathRef]{Ref: "#/components/pathItems/foo"}}, ), openapitest.WithComponentPathItem("foo", openapitest.NewPath()), ) diff --git a/providers/openapi/request_body.go b/providers/openapi/request_body.go index 36fcd3b61..e91c8cf9f 100644 --- a/providers/openapi/request_body.go +++ b/providers/openapi/request_body.go @@ -22,7 +22,7 @@ var defaultContentType = media.ParseContentType("application/octet-stream") type RequestBodies map[string]*RequestBodyRef type RequestBodyRef struct { - dynamic.Reference + dynamic.Reference[*RequestBodyRef] Value *RequestBody } @@ -186,8 +186,8 @@ func (r *RequestBodyRef) Parse(config *dynamic.Config, reader dynamic.Reader) er } if len(r.Ref) > 0 { - var resolved *RequestBodyRef - if err := dynamic.Resolve(r.Ref, &resolved, config, reader); err != nil { + resolved, err := r.Resolve(config, reader) + if err != nil { return err } r.Value = resolved.Value diff --git a/providers/openapi/response.go b/providers/openapi/response.go index c33ea62b7..4a8a0eab9 100644 --- a/providers/openapi/response.go +++ b/providers/openapi/response.go @@ -20,7 +20,7 @@ type Responses struct { type ResponseBodies map[string]*ResponseRef type ResponseRef struct { - dynamic.Reference + dynamic.Reference[*ResponseRef] Value *Response } @@ -223,8 +223,8 @@ func (r *ResponseRef) Parse(config *dynamic.Config, reader dynamic.Reader) error } if len(r.Ref) > 0 { - var resolved *ResponseRef - if err := dynamic.Resolve(r.Ref, &resolved, config, reader); err != nil { + resolved, err := r.Resolve(config, reader) + if err != nil { return err } r.Value = resolved.Value diff --git a/providers/openapi/response_test.go b/providers/openapi/response_test.go index 62a3ca9a7..34e609fd4 100644 --- a/providers/openapi/response_test.go +++ b/providers/openapi/response_test.go @@ -365,7 +365,7 @@ func TestResponse_Parse(t *testing.T) { ), ) err := config.Parse(&dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: config}, reader) - require.EqualError(t, err, "parse path '/foo' failed: parse operation 'GET' failed: parse response '200' failed: resolve reference 'foo.yml' failed: TEST ERROR") + require.EqualError(t, err, "parse path '/foo' failed: parse operation 'GET' failed: parse response '200' failed: resolve reference '/foo.yml' failed: TEST ERROR") }, }, { @@ -382,7 +382,7 @@ func TestResponse_Parse(t *testing.T) { ), ) err := config.Parse(&dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: config}, reader) - require.EqualError(t, err, "parse path '/foo' failed: parse operation 'GET' failed: parse response '200' failed: parse header 'foo' failed: resolve reference 'foo.yml' failed: TEST ERROR") + require.EqualError(t, err, "parse path '/foo' failed: parse operation 'GET' failed: parse response '200' failed: parse header 'foo' failed: resolve reference '/foo.yml' failed: TEST ERROR") }, }, } diff --git a/providers/openapi/schema/convert.go b/providers/openapi/schema/convert.go index 7c351de25..43d4e90a3 100644 --- a/providers/openapi/schema/convert.go +++ b/providers/openapi/schema/convert.go @@ -37,8 +37,6 @@ func (c *JsonSchemaConverter) Convert(s *Schema) *schema.Schema { js := &schema.Schema{ Id: s.Id, Anchor: s.Anchor, - Ref: s.Ref, - DynamicRef: s.DynamicRef, Boolean: s.Boolean, Type: s.Type, Schema: s.Schema, @@ -74,6 +72,8 @@ func (c *JsonSchemaConverter) Convert(s *Schema) *schema.Schema { Deprecated: s.Deprecated, } c.history[s] = js + js.Ref = s.Ref + js.DynamicRef = s.DynamicRef js.Minimum = s.Minimum js.ExclusiveMinimum = s.ExclusiveMinimum diff --git a/providers/openapi/schema/convert_test.go b/providers/openapi/schema/convert_test.go index f92969a07..4ce9dd77c 100644 --- a/providers/openapi/schema/convert_test.go +++ b/providers/openapi/schema/convert_test.go @@ -1,6 +1,7 @@ package schema_test import ( + "mokapi/config/dynamic" "mokapi/providers/openapi/schema" "mokapi/providers/openapi/schema/schematest" jsonSchema "mokapi/schema/json/schema" @@ -24,7 +25,7 @@ func TestConvert(t *testing.T) { }, { name: "ref", - s: &schema.Schema{Ref: "foo.yaml"}, + s: &schema.Schema{Reference: dynamic.Reference[*schema.Schema]{Ref: "foo.yaml"}}, test: func(t *testing.T, s *jsonSchema.Schema) { require.Equal(t, "foo.yaml", s.Ref) }, @@ -45,7 +46,7 @@ func TestConvert(t *testing.T) { }, { name: "dynamic ref", - s: &schema.Schema{DynamicRef: "foo"}, + s: &schema.Schema{Reference: dynamic.Reference[*schema.Schema]{DynamicRef: "foo"}}, test: func(t *testing.T, s *jsonSchema.Schema) { require.Equal(t, "foo", s.DynamicRef) }, diff --git a/providers/openapi/schema/marshal.go b/providers/openapi/schema/marshal.go index a13892804..adfc13ad7 100644 --- a/providers/openapi/schema/marshal.go +++ b/providers/openapi/schema/marshal.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/json" "fmt" + "mokapi/config/dynamic" "mokapi/media" "mokapi/schema/encoding" "mokapi/schema/json/parser" @@ -120,6 +121,10 @@ func (e *encoder) encode(s *Schema) ([]byte, error) { } else { bVal, err = json.Marshal(val.B) } + case dynamic.Reference[*Schema]: + bVal, err = json.Marshal(val) + b.WriteString(strings.Trim(string(bVal), "{}")) + continue default: bVal, err = json.Marshal(val) } diff --git a/providers/openapi/schema/marshal_schema_test.go b/providers/openapi/schema/marshal_schema_test.go index 1fcc11082..dee1e3ab6 100644 --- a/providers/openapi/schema/marshal_schema_test.go +++ b/providers/openapi/schema/marshal_schema_test.go @@ -2,6 +2,7 @@ package schema_test import ( "encoding/json" + "mokapi/config/dynamic" "mokapi/providers/openapi/schema" "mokapi/providers/openapi/schema/schematest" jsonSchema "mokapi/schema/json/schema" @@ -28,7 +29,7 @@ func TestSchema_Marshal(t *testing.T) { }, { name: "$ref", - schema: &schema.Schema{Ref: "#/components/schemas/Foo"}, + schema: &schema.Schema{Reference: dynamic.Reference[*schema.Schema]{Ref: "#/components/schemas/Foo"}}, exp: `{"$ref":"#/components/schemas/Foo"}`, }, { diff --git a/providers/openapi/schema/marshal_xml_test.go b/providers/openapi/schema/marshal_xml_test.go index b8ec81156..1e54a4171 100644 --- a/providers/openapi/schema/marshal_xml_test.go +++ b/providers/openapi/schema/marshal_xml_test.go @@ -1,6 +1,7 @@ package schema_test import ( + "mokapi/config/dynamic" "mokapi/media" "mokapi/providers/openapi/schema" "mokapi/providers/openapi/schema/schematest" @@ -55,7 +56,7 @@ func TestMarshal_Xml(t *testing.T) { data: func() interface{} { return 4 }, - schema: &schema.Schema{Ref: "#/components/schemas/foo", Sub: schematest.New("integer")}, + schema: &schema.Schema{Reference: dynamic.Reference[*schema.Schema]{Ref: "#/components/schemas/foo"}, Sub: schematest.New("integer")}, test: func(t *testing.T, s string, err error) { require.NoError(t, err) require.Equal(t, "4", s) diff --git a/providers/openapi/schema/schema.go b/providers/openapi/schema/schema.go index f45299731..6f3acaf68 100644 --- a/providers/openapi/schema/schema.go +++ b/providers/openapi/schema/schema.go @@ -10,9 +10,8 @@ import ( ) type Schema struct { - Id string `yaml:"$id,omitempty" json:"$id,omitempty"` - Ref string `yaml:"$ref,omitempty" json:"$ref,omitempty"` - DynamicRef string `yaml:"$dynamicRef,omitempty" json:"$dynamicRef,omitempty"` + Id string `yaml:"$id,omitempty" json:"$id,omitempty"` + dynamic.Reference[*Schema] Schema string `yaml:"$schema,omitempty" json:"$schema,omitempty"` Boolean *bool `yaml:"-" json:"-"` @@ -176,8 +175,9 @@ func (s *Schema) Parse(config *dynamic.Config, reader dynamic.Reader) error { } } - if s.Ref != "" { - err := dynamic.Resolve(s.Ref, &s.Sub, config, reader) + if s.Reference.HasRef() { + var err error + s.Sub, err = s.Reference.Resolve(config, reader) if err != nil { return err } @@ -190,14 +190,6 @@ func (s *Schema) Parse(config *dynamic.Config, reader dynamic.Reader) error { s.apply(s.Sub) } - if s.DynamicRef != "" { - err := dynamic.ResolveDynamic(s.DynamicRef, &s.Sub, config, reader) - if err != nil { - return err - } - s.apply(s.Sub) - } - return nil } @@ -253,13 +245,20 @@ func (s *Schema) UnmarshalJSON(b []byte) error { return nil } + r := dynamic.Reference[*Schema]{} + err := dynamic.UnmarshalJSON(b, &r) + if err != nil { + return err + } + type alias Schema a := alias{} - err := dynamic.UnmarshalJSON(b, &a) + err = dynamic.UnmarshalJSON(b, &a) if err != nil { return err } a.m = s.m + a.Reference = r *s = Schema(a) return nil } @@ -280,13 +279,20 @@ func (s *Schema) UnmarshalYAML(node *yaml.Node) error { return nil } + r := dynamic.Reference[*Schema]{} + err := node.Decode(&r) + if err != nil { + return err + } + type alias Schema a := alias{} - err := node.Decode(&a) + err = node.Decode(&a) if err != nil { return err } a.m = s.m + a.Reference = r *s = Schema(a) return nil } diff --git a/providers/openapi/schema/schema_parse_test.go b/providers/openapi/schema/schema_parse_test.go index ce548180c..61e22b8f0 100644 --- a/providers/openapi/schema/schema_parse_test.go +++ b/providers/openapi/schema/schema_parse_test.go @@ -29,10 +29,10 @@ func TestJson_Structuring(t *testing.T) { }, }, } - r := &schema.Schema{} - err := dynamic.Resolve("https://example.com/schemas/address#/properties/street_address", &r, &dynamic.Config{Data: &schema.Schema{}}, reader) + r := &dynamic.Reference[schema.Schema]{Ref: "https://example.com/schemas/address#/properties/street_address"} + s, err := r.Resolve(&dynamic.Config{Data: &schema.Schema{}}, reader) require.NoError(t, err) - require.Equal(t, "string", r.Type.String()) + require.Equal(t, "string", s.Type.String()) }, }, { @@ -52,7 +52,7 @@ func TestJson_Structuring(t *testing.T) { person := &dynamic.Config{ Info: dynamictest.NewConfigInfo(dynamictest.WithUrl("https://example.com/schemas/person")), - Data: &schema.Schema{Ref: "https://example.com/schemas/address#street_address"}, + Data: &schema.Schema{Reference: dynamic.Reference[*schema.Schema]{Ref: "https://example.com/schemas/address#street_address"}}, } person.OpenScope("") @@ -81,7 +81,7 @@ func TestJson_Structuring(t *testing.T) { person := &dynamic.Config{ Info: dynamictest.NewConfigInfo(dynamictest.WithUrl("https://example.com/schema/billing-address")), - Data: &schema.Schema{Ref: "https://example.com/schema/billing-address#street_address"}, + Data: &schema.Schema{Reference: dynamic.Reference[*schema.Schema]{Ref: "https://example.com/schema/billing-address#street_address"}}, } err := person.Data.(*schema.Schema).Parse(person, reader) @@ -130,11 +130,11 @@ func TestJson_Structuring(t *testing.T) { cfg := &dynamic.Config{Data: &schema.Schema{Id: "https://example.com/schemas/customer"}} - r := &schema.Schema{} - err := dynamic.Resolve("/schemas/address", &r, cfg, reader) + r := &dynamic.Reference[schema.Schema]{Ref: "/schemas/address"} + s, err := r.Resolve(cfg, reader) require.NoError(t, err) require.NotNil(t, r) - require.Equal(t, "object", r.Type.String()) + require.Equal(t, "object", s.Type.String()) }, }, { @@ -170,7 +170,7 @@ func TestJson_Structuring(t *testing.T) { foo := &dynamic.Config{ Info: dynamictest.NewConfigInfo(dynamictest.WithUrl("/foo.json")), Data: &schema.Schema{ - Ref: "/bar.json", + Reference: dynamic.Reference[*schema.Schema]{Ref: "/bar.json"}, }, } @@ -263,7 +263,7 @@ $ref: '#/$defs/a' Type: jsonSchema.Types{"string"}, }, }, - Ref: "https://example.com/schemas/list-of-t", + Reference: dynamic.Reference[*schema.Schema]{Ref: "https://example.com/schemas/list-of-t"}, }, } @@ -293,7 +293,7 @@ $ref: '#/$defs/a' person := &dynamic.Config{ Info: dynamictest.NewConfigInfo(dynamictest.WithUrl("https://example.com/schemas/bar")), Data: &schema.Schema{ - Ref: "https://example.com/schemas/foo", + Reference: dynamic.Reference[*schema.Schema]{Ref: "https://example.com/schemas/foo"}, }, } diff --git a/providers/openapi/schema/schematest/schema.go b/providers/openapi/schema/schematest/schema.go index 474904c2f..fc612c09d 100644 --- a/providers/openapi/schema/schematest/schema.go +++ b/providers/openapi/schema/schematest/schema.go @@ -1,6 +1,7 @@ package schematest import ( + "mokapi/config/dynamic" "mokapi/providers/openapi/schema" jsonSchema "mokapi/schema/json/schema" ) @@ -47,7 +48,7 @@ func WithItems(typeName string, opts ...SchemaOptions) SchemaOptions { func WithItemsRef(ref string) SchemaOptions { return func(s *schema.Schema) { - s.Items = &schema.Schema{Ref: ref} + s.Items = &schema.Schema{Reference: dynamic.Reference[*schema.Schema]{Ref: ref}} } } @@ -104,12 +105,12 @@ func WithProperty(name string, ps *schema.Schema) SchemaOptions { } } -func WithPropertyRef(name string, r string) SchemaOptions { +func WithPropertyRef(name string, ref string) SchemaOptions { return func(s *schema.Schema) { if s.Properties == nil { s.Properties = &schema.Schemas{} } - s.Properties.Set(name, &schema.Schema{Ref: r}) + s.Properties.Set(name, &schema.Schema{Reference: dynamic.Reference[*schema.Schema]{Ref: ref}}) } } diff --git a/providers/swagger/convert.go b/providers/swagger/convert.go index 2738a9743..13bccdcd3 100644 --- a/providers/swagger/convert.go +++ b/providers/swagger/convert.go @@ -105,7 +105,7 @@ func (c *converter) Convert() (*openapi.Config, error) { func (c *converter) convertPath(p *PathItem) (*openapi.PathRef, error) { if len(p.Ref) > 0 { - return &openapi.PathRef{Reference: dynamic.Reference{Ref: convertRef(p.Ref)}}, nil + return &openapi.PathRef{Reference: dynamic.Reference[*openapi.PathRef]{Ref: convertRef(p.Ref)}}, nil } result := &openapi.Path{} @@ -225,7 +225,7 @@ func (c *converter) convertOperation(o *Operation) (*openapi.Operation, error) { func (c *converter) convertResponse(r *Response, produces []string) (*openapi.ResponseRef, error) { if len(r.Ref) > 0 { - return &openapi.ResponseRef{Reference: dynamic.Reference{Ref: convertRef(r.Ref)}}, nil + return &openapi.ResponseRef{Reference: dynamic.Reference[*openapi.ResponseRef]{Ref: convertRef(r.Ref)}}, nil } result := &openapi.Response{ Description: r.Description, @@ -249,7 +249,7 @@ func (c *converter) convertSchema(s *schema.Schema) *schema.Schema { } if len(s.Ref) > 0 { - return &schema.Schema{Ref: convertRef(s.Ref)} + return &schema.Schema{Reference: dynamic.Reference[*schema.Schema]{Ref: convertRef(s.Ref)}} } if s.Type.IsInteger() && s.Format == "" { diff --git a/runtime/runtime_kafka.go b/runtime/runtime_kafka.go index 5d6aff83c..a4104a91c 100644 --- a/runtime/runtime_kafka.go +++ b/runtime/runtime_kafka.go @@ -2,7 +2,6 @@ package runtime import ( "mokapi/config/dynamic" - "mokapi/config/dynamic/asyncApi" "mokapi/config/static" "mokapi/engine/common" "mokapi/kafka" @@ -81,11 +80,7 @@ func (s *KafkaStore) Add(c *dynamic.Config, emitter common.EventEmitter) (*Kafka if len(s.infos) == 0 { s.infos = make(map[string]*KafkaInfo) } - cfg, err := getKafkaConfig(c) - if err != nil { - return nil, err - } - + cfg := getKafkaConfig(c) name := cfg.Info.Name ki, ok := s.infos[name] @@ -124,11 +119,7 @@ func (s *KafkaStore) Set(name string, ki *KafkaInfo) { func (s *KafkaStore) Remove(c *dynamic.Config) { s.m.RLock() - cfg, err := getKafkaConfig(c) - if err != nil { - return - } - + cfg := getKafkaConfig(c) name := cfg.Info.Name ki := s.infos[name] @@ -175,10 +166,7 @@ func (c *KafkaInfo) update(reader dynamic.Reader) { cfg := &asyncapi3.Config{} for i, k := range keys { - p, err := getKafkaConfig(c.configs[k]) - if err != nil { - log.Errorf("patch %v failed: %v", c.configs[k].Info.Url, err) - } + p := getKafkaConfig(c.configs[k]) if i == 0 { *cfg = *p } else { @@ -234,30 +222,16 @@ func (h *KafkaHandler) ServeMessage(rw kafka.ResponseWriter, req *kafka.Request) } func IsAsyncApiConfig(c *dynamic.Config) (*asyncapi3.Config, bool) { - var cfg *asyncapi3.Config - if old, ok := c.Data.(*asyncApi.Config); ok { - var err error - cfg, err = old.Convert() - if err != nil { - return nil, false - } - } else { - cfg, ok = c.Data.(*asyncapi3.Config) - if !ok { - return nil, false - } + switch v := c.Data.(type) { + case *asyncapi3.Config: + return v, true + default: + return nil, false } - - return cfg, true } -func getKafkaConfig(c *dynamic.Config) (*asyncapi3.Config, error) { - if _, ok := c.Data.(*asyncapi3.Config); ok { - return c.Data.(*asyncapi3.Config), nil - } else { - old := c.Data.(*asyncApi.Config) - return old.Convert() - } +func getKafkaConfig(c *dynamic.Config) *asyncapi3.Config { + return c.Data.(*asyncapi3.Config) } func (s *KafkaStore) updateEventStore(k *KafkaInfo) { diff --git a/runtime/runtime_kafka_test.go b/runtime/runtime_kafka_test.go index 911a276f2..d253e153d 100644 --- a/runtime/runtime_kafka_test.go +++ b/runtime/runtime_kafka_test.go @@ -3,6 +3,7 @@ package runtime_test import ( "mokapi/config/dynamic" "mokapi/config/dynamic/dynamictest" + "mokapi/config/dynamic/provider/file" "mokapi/config/static" "mokapi/engine/enginetest" "mokapi/kafka" @@ -15,6 +16,7 @@ import ( "mokapi/runtime/events/eventstest" "mokapi/runtime/monitor" "net/url" + "strings" "testing" "time" @@ -223,7 +225,7 @@ func TestApp_AddKafka_Patching(t *testing.T) { getConfig("https://a.io/a", asyncapi3test.NewConfig( asyncapi3test.WithInfo("foo", "foo", ""), asyncapi3test.WithChannel("bar", - asyncapi3test.UseMessage("bar", &asyncapi3.MessageRef{Reference: dynamic.Reference{Ref: "#/components/messages/bar"}}), + asyncapi3test.UseMessage("bar", &asyncapi3.MessageRef{Reference: dynamic.Reference[*asyncapi3.MessageRef]{Ref: "#/components/messages/bar"}}), ), asyncapi3test.WithComponentMessage("bar", &asyncapi3.Message{ Summary: "original", @@ -247,27 +249,25 @@ func TestApp_AddKafka_Patching(t *testing.T) { }, { name: "using relative file path", - reader: &dynamictest.Reader{ - Data: map[string]*dynamic.Config{ - "bar.yaml": { - Data: &asyncapi3.MessageRef{ - Value: asyncapi3test.NewMessage( - asyncapi3test.WithMessageTitle("original"), - ), - }, - }, + reader: dynamictest.ReaderFunc(func(u *url.URL, v any) (*dynamic.Config, error) { + require.True(t, strings.HasSuffix(u.Path, "path/ref.yaml")) + return &dynamic.Config{Data: &asyncapi3.MessageRef{ + Value: asyncapi3test.NewMessage( + asyncapi3test.WithMessageTitle("original"), + ), }, - }, + }, nil + }), configs: []*dynamic.Config{ - getConfig("https://a.io/a", asyncapi3test.NewConfig( + getFileConfig("path/foo.yaml", asyncapi3test.NewConfig( asyncapi3test.WithInfo("foo", "foo", ""), asyncapi3test.WithChannel("bar", asyncapi3test.UseMessage("bar", - &asyncapi3.MessageRef{Reference: dynamic.Reference{Ref: "bar.yaml"}}, + &asyncapi3.MessageRef{Reference: dynamic.Reference[*asyncapi3.MessageRef]{Ref: "ref.yaml"}}, ), ), )), - getConfig("https://mokapi.io/b", asyncapi3test.NewConfig( + getFileConfig("path/bar.yaml", asyncapi3test.NewConfig( asyncapi3test.WithInfo("foo", "bar", ""), )), }, @@ -277,28 +277,27 @@ func TestApp_AddKafka_Patching(t *testing.T) { require.NotNil(t, ch) msg := ch.Value.Messages["bar"] require.NotNil(t, msg) + require.NotNil(t, msg.Value) require.Equal(t, "original", msg.Value.Title) }, }, { - name: "using relative u path", - reader: &dynamictest.Reader{ - Data: map[string]*dynamic.Config{ - "bar.yaml": { - Data: &asyncapi3.MessageRef{ - Value: asyncapi3test.NewMessage( - asyncapi3test.WithMessageTitle("original"), - ), - }, - }, + name: "using relative URL path", + reader: dynamictest.ReaderFunc(func(u *url.URL, v any) (*dynamic.Config, error) { + require.Equal(t, "https://a.io/bar.yaml", u.String()) + return &dynamic.Config{Data: &asyncapi3.MessageRef{ + Value: asyncapi3test.NewMessage( + asyncapi3test.WithMessageTitle("original"), + ), }, - }, + }, nil + }), configs: []*dynamic.Config{ getConfig("https://a.io/a", asyncapi3test.NewConfig( asyncapi3test.WithInfo("foo", "foo", ""), asyncapi3test.WithChannel("bar", asyncapi3test.UseMessage("bar", - &asyncapi3.MessageRef{Reference: dynamic.Reference{Ref: "bar.yaml"}}, + &asyncapi3.MessageRef{Reference: dynamic.Reference[*asyncapi3.MessageRef]{Ref: "bar.yaml"}}, ), ), )), @@ -326,7 +325,9 @@ func TestApp_AddKafka_Patching(t *testing.T) { cfg := &static.Config{} app := runtime.New(cfg, r) for _, c := range tc.configs { - _, err := app.Kafka.Add(c, enginetest.NewEngine()) + err := c.Data.(dynamic.Parser).Parse(c, r) + require.NoError(t, err) + _, err = app.Kafka.Add(c, enginetest.NewEngine()) require.NoError(t, err) } tc.test(t, app) @@ -348,6 +349,13 @@ func getConfig(name string, c *asyncapi3.Config) *dynamic.Config { return cfg } +func getFileConfig(name string, c *asyncapi3.Config) *dynamic.Config { + u, _ := file.ParseUrl(name) + cfg := &dynamic.Config{Data: c} + cfg.Info.Url = u + return cfg +} + func newProduceMessage(topic string) *kafka.Request { return kafkatest.NewRequest("kafkatest", 3, &produce.Request{ Topics: []produce.RequestTopic{ diff --git a/runtime/runtime_search_test.go b/runtime/runtime_search_test.go index 2a48adf04..fbe7608b2 100644 --- a/runtime/runtime_search_test.go +++ b/runtime/runtime_search_test.go @@ -3,10 +3,10 @@ package runtime_test import ( "context" "mokapi/config/dynamic" - "mokapi/config/dynamic/asyncApi/asyncapitest" "mokapi/config/dynamic/dynamictest" "mokapi/config/static" "mokapi/engine/enginetest" + "mokapi/providers/asyncapi3/asyncapi3test" "mokapi/providers/openapi/openapitest" "mokapi/runtime" "mokapi/runtime/search" @@ -72,7 +72,7 @@ func TestIndex_Config(t *testing.T) { test: func(t *testing.T, app *runtime.App) { h := openapitest.NewConfig("3.0", openapitest.WithInfo("foo", "", "")) app.AddHttp(toConfig(h)) - k := asyncapitest.NewConfig(asyncapitest.WithInfo("foo", "", "")) + k := asyncapi3test.NewConfig(asyncapi3test.WithInfo("foo", "", "")) _, err := app.Kafka.Add(toConfig(k), enginetest.NewEngine()) require.NoError(t, err) diff --git a/schema/json/generator/pet_test.go b/schema/json/generator/pet_test.go index 96c3e169e..d2061ecca 100644 --- a/schema/json/generator/pet_test.go +++ b/schema/json/generator/pet_test.go @@ -1,11 +1,13 @@ package generator import ( - "github.com/brianvoe/gofakeit/v6" - "github.com/stretchr/testify/require" + "mokapi/config/dynamic" "mokapi/schema/json/schema" "mokapi/schema/json/schema/schematest" "testing" + + "github.com/brianvoe/gofakeit/v6" + "github.com/stretchr/testify/require" ) func TestPet(t *testing.T) { @@ -47,7 +49,7 @@ func TestPet(t *testing.T) { req: &Request{ Path: []string{"pet"}, Schema: schematest.New("array", schematest.WithItemsNew( - &schema.Schema{Ref: "#/components/schemas/Pet", Type: schema.Types{"string"}}, + &schema.Schema{Reference: dynamic.Reference[*schema.Schema]{Ref: "#/components/schemas/Pet"}, Type: schema.Types{"string"}}, )), }, test: func(t *testing.T, v interface{}, err error) { @@ -109,7 +111,7 @@ func TestPet(t *testing.T) { req: &Request{ Path: []string{"pet"}, Schema: schematest.New("array", schematest.WithItemsNew( - &schema.Schema{Ref: "#/components/schemas/Category", Type: schema.Types{"string"}}, + &schema.Schema{Reference: dynamic.Reference[*schema.Schema]{Ref: "#/components/schemas/Category"}, Type: schema.Types{"string"}}, )), }, test: func(t *testing.T, v interface{}, err error) { diff --git a/schema/json/schema/clone.go b/schema/json/schema/clone.go index fcc4288ba..66ef9e25e 100644 --- a/schema/json/schema/clone.go +++ b/schema/json/schema/clone.go @@ -7,8 +7,7 @@ func (s *Schema) Clone() *Schema { clone := &Schema{ Id: s.Id, - Ref: s.Ref, - DynamicRef: s.DynamicRef, + Reference: s.Reference, Schema: s.Schema, Boolean: s.Boolean, Anchor: s.Anchor, diff --git a/schema/json/schema/clone_test.go b/schema/json/schema/clone_test.go index eb5380f5d..07d680ca1 100644 --- a/schema/json/schema/clone_test.go +++ b/schema/json/schema/clone_test.go @@ -1,6 +1,7 @@ package schema_test import ( + "mokapi/config/dynamic" "mokapi/schema/json/schema" "mokapi/schema/json/schema/schematest" "testing" @@ -17,14 +18,13 @@ func TestSchema_Clone(t *testing.T) { name: "base", test: func(t *testing.T) { s := &schema.Schema{ - Id: "id", - Ref: "ref", - DynamicRef: "dynamicRef", - Schema: "schema", - Boolean: toBoolP(true), - Anchor: "anchor", - Type: schema.Types{"object"}, - Enum: []any{"one", "two", "three"}, + Id: "id", + Reference: dynamic.Reference[*schema.Schema]{Ref: "ref", DynamicRef: "dynamicRef"}, + Schema: "schema", + Boolean: toBoolP(true), + Anchor: "anchor", + Type: schema.Types{"object"}, + Enum: []any{"one", "two", "three"}, Const: func() *any { var v any v = "const" diff --git a/schema/json/schema/marshal.go b/schema/json/schema/marshal.go index e252ca3af..d3f2a43e0 100644 --- a/schema/json/schema/marshal.go +++ b/schema/json/schema/marshal.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/json" "fmt" + "mokapi/config/dynamic" "reflect" "strings" ) @@ -80,6 +81,10 @@ func (e *encoder) encode(r *Schema) ([]byte, error) { bVal = fields.Bytes() case *Schema: bVal, err = e.encode(val) + case dynamic.Reference[*Schema]: + bVal, err = json.Marshal(val) + b.WriteString(strings.Trim(string(bVal), "{}")) + continue default: bVal, err = json.Marshal(val) } diff --git a/schema/json/schema/marshal_test.go b/schema/json/schema/marshal_test.go index 6cde09963..f74a8f671 100644 --- a/schema/json/schema/marshal_test.go +++ b/schema/json/schema/marshal_test.go @@ -2,8 +2,10 @@ package schema import ( "encoding/json" - "github.com/stretchr/testify/require" + "mokapi/config/dynamic" "testing" + + "github.com/stretchr/testify/require" ) func TestRef_MarshalJSON(t *testing.T) { @@ -51,7 +53,7 @@ func TestSchema_MarshalJSON_Recursion(t *testing.T) { name: "recursion in property", s: func() *Schema { s := &Schema{ - Ref: "foo", + Reference: dynamic.Reference[*Schema]{Ref: "foo"}, Properties: &Schemas{}, } s.Properties.Set("foo", s) @@ -66,7 +68,7 @@ func TestSchema_MarshalJSON_Recursion(t *testing.T) { name: "recursion in array", s: func() *Schema { s := &Schema{ - Ref: "foo", + Reference: dynamic.Reference[*Schema]{Ref: "foo"}, } s.Items = s return s @@ -80,7 +82,7 @@ func TestSchema_MarshalJSON_Recursion(t *testing.T) { name: "recursion in contains", s: func() *Schema { s := &Schema{ - Ref: "foo", + Reference: dynamic.Reference[*Schema]{Ref: "foo"}, } s.Contains = s return s diff --git a/schema/json/schema/parse_test.go b/schema/json/schema/parse_test.go index 89a928283..0ce202a2a 100644 --- a/schema/json/schema/parse_test.go +++ b/schema/json/schema/parse_test.go @@ -93,7 +93,7 @@ $ref: '#/$defs/a' person := &dynamic.Config{ Info: dynamictest.NewConfigInfo(dynamictest.WithUrl("https://example.com/schemas/bar")), Data: &schema.Schema{ - Ref: "https://example.com/schemas/foo", + Reference: dynamic.Reference[*schema.Schema]{Ref: "https://example.com/schemas/foo"}, }, } diff --git a/schema/json/schema/schema.go b/schema/json/schema/schema.go index d073d0c8e..9507bf545 100644 --- a/schema/json/schema/schema.go +++ b/schema/json/schema/schema.go @@ -11,9 +11,8 @@ import ( type Schema struct { m map[string]bool - Id string `yaml:"$id,omitempty" json:"$id,omitempty"` - Ref string `yaml:"$ref,omitempty" json:"$ref,omitempty"` - DynamicRef string `yaml:"$dynamicRef,omitempty" json:"$dynamicRef,omitempty"` + Id string `yaml:"$id,omitempty" json:"$id,omitempty"` + dynamic.Reference[*Schema] Schema string `yaml:"$schema,omitempty" json:"$schema,omitempty"` Boolean *bool `yaml:"-" json:"-"` @@ -167,9 +166,15 @@ func (s *Schema) UnmarshalJSON(b []byte) error { return nil } + r := dynamic.Reference[*Schema]{} + err := json.Unmarshal(b, &r) + if err != nil { + return err + } + type alias Schema a := alias{} - err := json.Unmarshal(b, &a) + err = json.Unmarshal(b, &a) if typeErr, ok := err.(*json.UnmarshalTypeError); ok { return &UnmarshalError{ Value: typeErr.Value, @@ -179,6 +184,7 @@ func (s *Schema) UnmarshalJSON(b []byte) error { return err } a.m = s.m + a.Reference = r *s = Schema(a) return nil } @@ -199,21 +205,24 @@ func (s *Schema) UnmarshalYAML(node *yaml.Node) error { return nil } + r := dynamic.Reference[*Schema]{} + err := node.Decode(&r) + if err != nil { + return err + } + type alias Schema a := alias{} - err := node.Decode(&a) + err = node.Decode(&a) if err != nil { return err } a.m = s.m + a.Reference = r *s = Schema(a) return nil } -type ref struct { - Schema *Schema -} - func (s *Schema) Parse(config *dynamic.Config, reader dynamic.Reader) error { if s == nil { return nil @@ -296,9 +305,8 @@ func (s *Schema) Parse(config *dynamic.Config, reader dynamic.Reader) error { } } - if s.Ref != "" { - r := &ref{} - err := dynamic.Resolve(s.Ref, &r.Schema, config, reader) + if s.Reference.HasRef() { + r, err := s.Reference.Resolve(config, reader) if err != nil { return err } @@ -308,16 +316,7 @@ func (s *Schema) Parse(config *dynamic.Config, reader dynamic.Reader) error { // the parsed schema graph. Dynamic references may resolve differently // depending on the evaluation context, so shared schema nodes must // never be mutated. - s.apply(r.Schema) - } - - if s.DynamicRef != "" { - r := &ref{} - err := dynamic.ResolveDynamic(s.DynamicRef, &r.Schema, config, reader) - if err != nil { - return err - } - s.apply(r.Schema) + s.apply(r) } return nil diff --git a/schema/json/schema/schema_json_test.go b/schema/json/schema/schema_json_test.go index 70ee2df00..741ed26a4 100644 --- a/schema/json/schema/schema_json_test.go +++ b/schema/json/schema/schema_json_test.go @@ -351,10 +351,10 @@ func TestJson_Structuring(t *testing.T) { }, }, } - r := &schema.Schema{} - err := dynamic.Resolve("https://example.com/schemas/address#/properties/street_address", &r, &dynamic.Config{Data: &schema.Schema{}}, reader) + s := &schema.Schema{Reference: dynamic.Reference[*schema.Schema]{Ref: "https://example.com/schemas/address#/properties/street_address"}} + err := s.Parse(&dynamic.Config{Data: &schema.Schema{}}, reader) require.NoError(t, err) - require.Equal(t, "string", r.Type.String()) + require.Equal(t, "string", s.Type.String()) }, }, { @@ -374,7 +374,7 @@ func TestJson_Structuring(t *testing.T) { person := &dynamic.Config{ Info: dynamictest.NewConfigInfo(dynamictest.WithUrl("https://example.com/schemas/person")), - Data: &schema.Schema{Ref: "https://example.com/schemas/address#street_address"}, + Data: &schema.Schema{Reference: dynamic.Reference[*schema.Schema]{Ref: "https://example.com/schemas/address#street_address"}}, } person.OpenScope("") @@ -403,7 +403,7 @@ func TestJson_Structuring(t *testing.T) { person := &dynamic.Config{ Info: dynamictest.NewConfigInfo(dynamictest.WithUrl("https://example.com/schema/billing-address")), - Data: &schema.Schema{Ref: "https://example.com/schema/billing-address#street_address"}, + Data: &schema.Schema{Reference: dynamic.Reference[*schema.Schema]{Ref: "https://example.com/schema/billing-address#street_address"}}, } err := person.Data.(*schema.Schema).Parse(person, reader) @@ -452,11 +452,11 @@ func TestJson_Structuring(t *testing.T) { cfg := &dynamic.Config{Data: &schema.Schema{Id: "https://example.com/schemas/customer"}} - r := &schema.Schema{} - err := dynamic.Resolve("/schemas/address", &r, cfg, reader) + r := &dynamic.Reference[schema.Schema]{Ref: "/schemas/address"} + s, err := r.Resolve(cfg, reader) require.NoError(t, err) require.NotNil(t, r) - require.Equal(t, "object", r.Type.String()) + require.Equal(t, "object", s.Type.String()) }, }, { @@ -519,15 +519,17 @@ func TestJson_Structuring(t *testing.T) { Type: schema.Types{"string"}, }, }, - Ref: "https://example.com/schemas/list-of-t", + Reference: dynamic.Reference[*schema.Schema]{Ref: "https://example.com/schemas/list-of-t"}, }, } - err := person.Data.(*schema.Schema).Parse(person, reader) + s := person.Data.(*schema.Schema) + err := s.Parse(person, reader) require.NoError(t, err) require.NoError(t, err) - require.Equal(t, "string", person.Data.(*schema.Schema).Items.Type.String()) + require.Nil(t, s.Items.Not, "resolves to the base generic schema") + require.Equal(t, "string", s.Items.Type.String()) }, }, } diff --git a/schema/json/schema/schema_yaml_test.go b/schema/json/schema/schema_yaml_test.go index 28d7abfcb..d4c2ce4ed 100644 --- a/schema/json/schema/schema_yaml_test.go +++ b/schema/json/schema/schema_yaml_test.go @@ -336,7 +336,7 @@ items: Type: schema.Types{"string"}, }, }, - Ref: "https://example.com/schemas/list-of-t", + Reference: dynamic.Reference[*schema.Schema]{Ref: "https://example.com/schemas/list-of-t"}, }, } diff --git a/schema/json/schema/schematest/schema.go b/schema/json/schema/schematest/schema.go index 21a88fa2b..d60c243fc 100644 --- a/schema/json/schema/schematest/schema.go +++ b/schema/json/schema/schematest/schema.go @@ -1,6 +1,7 @@ package schematest import ( + "mokapi/config/dynamic" "mokapi/schema/json/schema" ) @@ -45,12 +46,12 @@ func WithProperty(name string, ps *schema.Schema) SchemaOptions { } } -func WithPropertyRef(name string, r string) SchemaOptions { +func WithPropertyRef(name string, ref string) SchemaOptions { return func(s *schema.Schema) { if s.Properties == nil { s.Properties = &schema.Schemas{} } - s.Properties.Set(name, &schema.Schema{Ref: r}) + s.Properties.Set(name, &schema.Schema{Reference: dynamic.Reference[*schema.Schema]{Ref: ref}}) } } @@ -71,7 +72,7 @@ func WithItems(typeName string, opts ...SchemaOptions) SchemaOptions { func WithItemsRef(ref string) SchemaOptions { return func(s *schema.Schema) { - s.Items = &schema.Schema{Ref: ref} + s.Items = &schema.Schema{Reference: dynamic.Reference[*schema.Schema]{Ref: ref}} } } @@ -81,9 +82,9 @@ func WithItemsNew(items *schema.Schema) SchemaOptions { } } -func WithItemsRefString(r string) SchemaOptions { +func WithItemsRefString(ref string) SchemaOptions { return func(s *schema.Schema) { - s.Items = &schema.Schema{Ref: r} + s.Items = &schema.Schema{Reference: dynamic.Reference[*schema.Schema]{Ref: ref}} } } diff --git a/server/configwatcher_openapi_test.go b/server/configwatcher_openapi_test.go index 296fb55ba..d2c715ba4 100644 --- a/server/configwatcher_openapi_test.go +++ b/server/configwatcher_openapi_test.go @@ -3,9 +3,6 @@ package server import ( "context" "fmt" - "github.com/sirupsen/logrus" - logtest "github.com/sirupsen/logrus/hooks/test" - "github.com/stretchr/testify/require" "io" "mokapi/config/dynamic" "mokapi/config/static" @@ -15,6 +12,10 @@ import ( "strings" "testing" "time" + + "github.com/sirupsen/logrus" + logtest "github.com/sirupsen/logrus/hooks/test" + "github.com/stretchr/testify/require" ) func TestConfigWatcher_Openapi(t *testing.T) { @@ -147,7 +148,7 @@ paths: time.Sleep(1 * time.Second) - require.Equal(t, "parse error /root.yml: parsing file /root.yml: parse path '/users' failed: resolve reference 'paths.yml#/paths/users' failed: parsing file /paths.yml: parse path '/users' failed: parse operation 'GET' failed: parse request body failed: resolve reference 'not_found.yaml' failed: not found: /not_found.yaml", hook.LastEntry().Message) + require.Equal(t, "parse error /root.yml: parsing file /root.yml: parse path '/users' failed: resolve reference '/paths.yml#/paths/users' failed: parsing file /paths.yml: parse path '/users' failed: parse operation 'GET' failed: parse request body failed: resolve reference '/not_found.yaml' failed: not found: /not_found.yaml", hook.LastEntry().Message) }, }, } From 94a24bde3ba71cc9f58cb789cde1ab4ec2f4487a Mon Sep 17 00:00:00 2001 From: maesi Date: Wed, 15 Apr 2026 08:01:12 +0200 Subject: [PATCH 24/46] wip: implement mcp server code mode --- config/dynamic/resolve.go | 10 + mcp/generate_http_mock_response.go | 2 +- mcp/get_api_spec.go | 13 +- mcp/get_http_response_schema.go | 2 +- mcp/list_apis.go | 4 +- mcp/produce_kafka_message.go | 2 +- mcp/run.go | 250 +++++++++++++++++++++ mcp/run.ts | 76 +++++++ mcp/run_test.go | 159 +++++++++++++ mcp/send_http_request.go | 2 +- mcp/server.go | 2 + mcp/server_test.go | 19 +- providers/openapi/openapitest/operation.go | 6 + 13 files changed, 527 insertions(+), 20 deletions(-) create mode 100644 mcp/run.go create mode 100644 mcp/run.ts create mode 100644 mcp/run_test.go diff --git a/config/dynamic/resolve.go b/config/dynamic/resolve.go index 4aab048a2..29f1119d5 100644 --- a/config/dynamic/resolve.go +++ b/config/dynamic/resolve.go @@ -266,6 +266,8 @@ 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)) + i := vElement.Interface() + _ = i if v.Kind() == reflect.Ptr { v = v.Elem() @@ -303,7 +305,15 @@ func setResolved(element interface{}, val interface{}) (err error) { return fmt.Errorf("expected type %v, got %v", vElement.Type(), vCursor.Type()) } + n1 := fmt.Sprintf("%s.%s", vCursor.Type().PkgPath(), vCursor.Type().Name()) + _ = n1 + n2 := fmt.Sprintf("%s.%s", vElement.Type().PkgPath(), vElement.Type().Name()) + _ = n2 + + b := vElement.Type() == vCursor.Type() + _ = b vElement.Set(vCursor) + i = vElement.Interface() return } diff --git a/mcp/generate_http_mock_response.go b/mcp/generate_http_mock_response.go index 78a8aac1a..fc7fc91bd 100644 --- a/mcp/generate_http_mock_response.go +++ b/mcp/generate_http_mock_response.go @@ -32,7 +32,7 @@ func (s *Service) registerGenerateHttpMockResponseTool(server *mcp.Server) { "properties": map[string]any{ "apiName": map[string]any{ "type": "string", - "description": "The exact name of the API as returned by 'get_api_list'", + "description": "The exact name of the API as returned by 'mokapi_get_api_spec'", }, "path": map[string]any{ "type": "string", diff --git a/mcp/get_api_spec.go b/mcp/get_api_spec.go index 9c5b6a271..3f455d8df 100644 --- a/mcp/get_api_spec.go +++ b/mcp/get_api_spec.go @@ -20,7 +20,7 @@ type GetApiSpecOutput struct { type ApiSpec struct { Name string `json:"name"` Type string `json:"type"` - Spec any `json:"spec"` + Spec any `json:"spec,omitempty"` } func (s *Service) registerGetSpecTool(server *mcp.Server) { @@ -60,10 +60,11 @@ func (s *Service) registerGetSpecTool(server *mcp.Server) { "enum": []string{"http", "kafka", "ldap", "mail"}, }, "spec": map[string]any{ - "type": "any", + "type": "object", "description": "The specification of the API (e.g. OpenAPI or AsyncAPI", }, }, + "required": []any{"name", "type"}, }, }, }, @@ -96,7 +97,7 @@ func (s *Service) GetApiSpec(_ context.Context, in GetApiSpecInput) (GetApiSpecO if in.Type == "http" || len(in.Type) == 0 { for _, api := range s.app.ListHttp() { if api.Info.Name == "" { - log.Warnf("mcp tool get_api_list: skip empty HTTTP API name") + log.Warnf("mcp tool mokapi_get_api_spec: skip empty HTTTP API name") continue } result = append(result, ApiSpec{ @@ -109,7 +110,7 @@ func (s *Service) GetApiSpec(_ context.Context, in GetApiSpecInput) (GetApiSpecO if in.Type == "kafka" || len(in.Type) == 0 { for _, api := range s.app.Kafka.List() { if api.Info.Name == "" { - log.Warnf("mcp tool get_api_list: skip empty Kafka API name") + log.Warnf("mcp tool mokapi_get_api_spec: skip empty Kafka API name") continue } result = append(result, ApiSpec{ @@ -122,7 +123,7 @@ func (s *Service) GetApiSpec(_ context.Context, in GetApiSpecInput) (GetApiSpecO if in.Type == "ldap" || len(in.Type) == 0 { for _, api := range s.app.Ldap.List() { if api.Info.Name == "" { - log.Warnf("mcp tool get_api_list: skip empty LDAP API name") + log.Warnf("mcp tool mokapi_get_api_spec: skip empty LDAP API name") continue } result = append(result, ApiSpec{ @@ -135,7 +136,7 @@ func (s *Service) GetApiSpec(_ context.Context, in GetApiSpecInput) (GetApiSpecO if in.Type == "mail" || len(in.Type) == 0 { for _, api := range s.app.Mail.List() { if api.Info.Name == "" { - log.Warnf("mcp tool get_api_list: skip empty Mail API name") + log.Warnf("mcp tool mokapi_get_api_spec: skip empty Mail API name") continue } result = append(result, ApiSpec{ diff --git a/mcp/get_http_response_schema.go b/mcp/get_http_response_schema.go index 11e616e3f..7173c3448 100644 --- a/mcp/get_http_response_schema.go +++ b/mcp/get_http_response_schema.go @@ -22,7 +22,7 @@ func (s *Service) registerGetHttpResponseSchemaTool(server *mcp.Server) { "properties": map[string]any{ "apiName": map[string]any{ "type": "string", - "description": "The exact name of the API as returned by 'get_api_list'", + "description": "The exact name of the API as returned by 'mokapi_get_api_spec'", }, "path": map[string]any{ "type": "string", diff --git a/mcp/list_apis.go b/mcp/list_apis.go index dc40f0368..4082352eb 100644 --- a/mcp/list_apis.go +++ b/mcp/list_apis.go @@ -1,5 +1,6 @@ package mcp +/* import ( "context" @@ -58,7 +59,7 @@ func (s *Service) registerListApiTool(server *mcp.Server) { registerTool(server, &mcp.Tool{ Name: "get_api_list", - Description: `Returns all available APIs with their name and type. + Description: `Returns all available APIs with their name and type. Use this to discover APIs before calling 'get_api_spec' to retrieve detailed specifications.`, InputSchema: inputSchema, OutputSchema: outputSchema, @@ -122,3 +123,4 @@ func (s *Service) ListApis(_ context.Context, in ListApisInput) (*ListApiRespons return &ListApiResponse{Apis: result}, nil } +*/ diff --git a/mcp/produce_kafka_message.go b/mcp/produce_kafka_message.go index da25da8bc..af51ba1e8 100644 --- a/mcp/produce_kafka_message.go +++ b/mcp/produce_kafka_message.go @@ -29,7 +29,7 @@ func (s *Service) registerProduceKafkaMessage(server *mcp.Server) { "properties": map[string]any{ "apiName": map[string]any{ "type": "string", - "description": "The name of the Kafka API as returned by 'get_api_list'", + "description": "The name of the Kafka API as returned by 'mokapi_get_api_spec'", }, "topic": map[string]any{ "type": "string", diff --git a/mcp/run.go b/mcp/run.go new file mode 100644 index 000000000..22cfa3749 --- /dev/null +++ b/mcp/run.go @@ -0,0 +1,250 @@ +package mcp + +import ( + "context" + _ "embed" + "mokapi/runtime" + "reflect" + "slices" + "strings" + + "github.com/dop251/goja" + "github.com/modelcontextprotocol/go-sdk/mcp" + log "github.com/sirupsen/logrus" +) + +//go:embed run.ts +var types string + +type RunInput struct { + Code string `json:"code"` +} + +type RunOutput struct { + Result any `json:"result"` +} + +func (s *Service) registerRunTool(server *mcp.Server) { + inputSchema := map[string]any{ + "type": "object", + "properties": map[string]any{ + "code": map[string]any{ + "type": "string", + "description": "JavaScript code to execute in the Mokapi runtime. The last expression is returned as the result.", + }, + }, + "required": []string{"code"}, + } + + outputSchema := map[string]any{ + "type": "object", + "properties": map[string]any{ + "result": map[string]any{ + "description": "The result of the executed code.", + "nullable": true, + }, + }, + "required": []string{"result"}, + } + + registerTool(server, &mcp.Tool{ + Name: "mokapi_execute_code", + Description: `Executes JavaScript code in a sandboxed Mokapi runtime. +The last expression in the code is returned as the result. + +Important: +Before writing any code, be sure to read the API definitions at api://execute-types to understand +the available global objects, functions, and types. + +Use this tool to: +- Explore mocked APIs (OpenAPI, AsyncAPI, LDAP, Mail) +- Inspect operations and schemas +- Invoke API operations directly + +Prefer this tool over retrieving full API specifications, as it returns only the computed result.`, + InputSchema: inputSchema, + OutputSchema: outputSchema, + }, s.GenerateHttpMockResponse) + + server.AddResource(&mcp.Resource{ + URI: "api://execute-types", + Name: "api-docs", + }, func(ctx context.Context, request *mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) { + return &mcp.ReadResourceResult{ + Contents: []*mcp.ResourceContents{ + { + URI: "api://types", + MIMEType: "application/typescript", + Text: types, + }, + }, + }, nil + }) +} + +func (s *Service) GetRunResponse(_ context.Context, in RunInput) (RunOutput, error) { + m := newMokapi(s.app) + r, err := m.run(in.Code) + if err != nil { + return RunOutput{}, err + } + + return RunOutput{Result: r}, nil +} + +type mokapi struct { + app *runtime.App + vm *goja.Runtime +} + +func newMokapi(app *runtime.App) *mokapi { + vm := goja.New() + vm.SetFieldNameMapper(&customFieldNameMapper{}) + return &mokapi{app: app, vm: vm} +} + +func (m *mokapi) run(code string) (any, error) { + obj := m.vm.NewObject() + m.init(obj) + _ = m.vm.Set("mokapi", obj) + v, err := m.vm.RunString(code) + if err != nil { + return nil, err + } + return v.Export(), nil +} + +type ApiSummary struct { + Name string `json:"name"` + Type string `json:"type"` +} + +func (m *mokapi) init(obj *goja.Object) { + _ = obj.Set("getApis", m.getApis) + _ = obj.Set("getApi", m.getApi) +} + +func (m *mokapi) getApis() []ApiSummary { + var result []ApiSummary + for _, api := range m.app.ListHttp() { + if api.Info.Name == "" { + log.Warnf("mcp tool mokapi_get_api_spec: skip empty HTTTP API name") + continue + } + result = append(result, ApiSummary{ + Name: api.Info.Name, + Type: "http", + }) + } + slices.SortStableFunc(result, func(a, b ApiSummary) int { + return strings.Compare(a.Name, b.Name) + }) + return result +} + +func (m *mokapi) getApi(name string) any { + for _, api := range m.app.ListHttp() { + if api.Info.Name == name { + return &OpenAPI{ + Name: name, + Type: "http", + info: api, + } + } + } + return nil +} + +type OpenAPI struct { + Name string `json:"name"` + Type string `json:"type"` + + info *runtime.HttpInfo +} + +type OperationSummary struct { + Method string `json:"method"` + Path string `json:"path"` + Summary string `json:"summary"` +} + +type OperationDetails struct { + OperationId string `json:"operationId"` + Method string `json:"method"` + Path string `json:"path"` + Summary string `json:"summary"` + Description string `json:"description"` +} + +func (o *OpenAPI) GetOperations() []OperationSummary { + var result []OperationSummary + for _, p := range o.info.Paths { + if p.Value == nil { + continue + } + for method, op := range p.Value.Operations() { + summary := op.Summary + if summary == "" { + summary = p.Value.Summary + } + result = append(result, OperationSummary{ + Method: method, + Path: p.Value.Path, + Summary: summary, + }) + } + } + + slices.SortStableFunc(result, func(a, b OperationSummary) int { + c := strings.Compare(a.Path, b.Path) + if c != 0 { + return c + } + return strings.Compare(a.Method, b.Method) + }) + + return result +} + +func (o *OpenAPI) GetOperationDetails(path, method string) *OperationDetails { + for _, p := range o.info.Paths { + if p.Value == nil || p.Value.Path != path { + continue + } + op := p.Value.Operation(method) + if op == nil { + continue + } + return &OperationDetails{ + OperationId: op.OperationId, + Method: method, + Path: p.Value.Path, + Summary: op.Summary, + Description: op.Description, + } + } + return nil +} + +type customFieldNameMapper struct { +} + +func (cfm customFieldNameMapper) FieldName(_ reflect.Type, f reflect.StructField) string { + tag := f.Tag.Get("json") + if len(tag) == 0 { + return uncapitalize(f.Name) + } + if idx := strings.IndexByte(tag, ','); idx != -1 { + tag = tag[:idx] + } + + return tag +} + +func (cfm customFieldNameMapper) MethodName(_ reflect.Type, m reflect.Method) string { + return uncapitalize(m.Name) +} + +func uncapitalize(s string) string { + return strings.ToLower(s[0:1]) + s[1:] +} diff --git a/mcp/run.ts b/mcp/run.ts new file mode 100644 index 000000000..35b39408d --- /dev/null +++ b/mcp/run.ts @@ -0,0 +1,76 @@ +declare const mokapi: Mokapi; + +interface Mokapi { + /** + * Returns all mocked APIs (lightweight, no schemas). + */ + getApis(): ApiSummary[]; + + /** + * Returns a specific API by name. + */ + getApi(name: string): Api; + + /** + * Directly invoke an API operation. + */ + invoke(apiName: string, operationId: string, request?: InvokeRequest): InvokeResponse; +} + +interface ApiSummary { + name: string; + type: 'openapi' | 'asyncapi' | 'ldap' | 'mail'; +} + +interface Api extends ApiSummary { + spec: OpenApi +} + +interface OpenApi { + /** + * Returns operations of this API. + * By default, only minimal fields are returned. + */ + getOperations(): OperationSummary[]; + + getOperationDetails(path: string, method: string): OperationDetails +} + +interface OperationSummary { + method: string; + path: string; + summary: string; +} + +interface OperationDetails { + operationId: string; + description: string; + + /** + * Invoke this operation against the mocked API. + */ + invoke(request?: InvokeRequest): InvokeResponse; + + /** + * Returns the response schema for a given status code. + */ + getResponseSchema(status: number): Schema; +} + +interface InvokeRequest { + path?: Record; + query?: Record; + headers?: Record; + body?: any; +} + +interface InvokeResponse { + status: number; + headers: Record; + body?: any; +} + +interface Schema { + type?: string; + properties?: Record; +} \ No newline at end of file diff --git a/mcp/run_test.go b/mcp/run_test.go new file mode 100644 index 000000000..17e105efb --- /dev/null +++ b/mcp/run_test.go @@ -0,0 +1,159 @@ +package mcp_test + +import ( + "context" + "encoding/json" + "mokapi/mcp" + "mokapi/providers/openapi/openapitest" + "mokapi/runtime" + "mokapi/runtime/runtimetest" + "net/http" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestService_Run(t *testing.T) { + testcases := []struct { + name string + app *runtime.App + test func(t *testing.T, s *mcp.Service) + }{ + { + name: "List APIs skip empty name", + app: runtimetest.NewHttpApp( + openapitest.NewConfig("3.1.0"), + ), + test: func(t *testing.T, s *mcp.Service) { + r, err := s.GetRunResponse( + context.Background(), + mcp.RunInput{ + Code: `mokapi.getApis()`, + }, + ) + require.NoError(t, err) + require.Len(t, r.Result, 0) + }, + }, + { + name: "get HTTP APIs", + app: runtimetest.NewHttpApp( + openapitest.NewConfig("3.1.0", + openapitest.WithInfo("foo", "", ""), + ), + openapitest.NewConfig("3.1.0", + openapitest.WithInfo("bar", "", ""), + ), + ), + test: func(t *testing.T, s *mcp.Service) { + r, err := s.GetRunResponse( + context.Background(), + mcp.RunInput{ + Code: `mokapi.getApis()`, + }, + ) + require.NoError(t, err) + require.Equal(t, []mcp.ApiSummary{ + {Name: "bar", Type: "http"}, + {Name: "foo", Type: "http"}, + }, r.Result) + }, + }, + { + name: "get specific HTTP API", + app: runtimetest.NewHttpApp( + openapitest.NewConfig("3.1.0", + openapitest.WithInfo("foo", "", ""), + ), + openapitest.NewConfig("3.1.0", + openapitest.WithInfo("bar", "", ""), + ), + ), + test: func(t *testing.T, s *mcp.Service) { + r, err := s.GetRunResponse( + context.Background(), + mcp.RunInput{ + Code: `mokapi.getApi('foo')`, + }, + ) + require.NoError(t, err) + b, err := json.Marshal(r.Result) + require.NoError(t, err) + require.Equal(t, `{"name":"foo","type":"http"}`, string(b)) + }, + }, + { + name: "get API's operation list", + app: runtimetest.NewHttpApp( + openapitest.NewConfig("3.1.0", + openapitest.WithInfo("foo", "", ""), + openapitest.WithPath("/pets", + openapitest.WithOperation(http.MethodGet, + openapitest.WithOperationSummary("GET summary"), + ), + openapitest.WithOperation(http.MethodPut, + openapitest.WithOperationSummary("PUT summary"), + ), + ), + openapitest.WithPath("/users", + openapitest.WithPathInfo("path summary", ""), + openapitest.WithOperation(http.MethodPost), + ), + ), + ), + test: func(t *testing.T, s *mcp.Service) { + r, err := s.GetRunResponse( + context.Background(), + mcp.RunInput{ + Code: `mokapi.getApi('foo').getOperations()`, + }, + ) + require.NoError(t, err) + require.Equal(t, []mcp.OperationSummary{ + {Method: "GET", Path: "/pets", Summary: "GET summary"}, + {Method: "PUT", Path: "/pets", Summary: "PUT summary"}, + {Method: "POST", Path: "/users", Summary: "path summary"}}, + r.Result) + }, + }, + { + name: "get API's operation details", + app: runtimetest.NewHttpApp( + openapitest.NewConfig("3.1.0", + openapitest.WithInfo("foo", "", ""), + openapitest.WithPath("/pets", + openapitest.WithOperation(http.MethodGet, + openapitest.WithOperationSummary("GET summary"), + ), + openapitest.WithOperation(http.MethodPut, + openapitest.WithOperationSummary("PUT summary"), + ), + ), + ), + ), + test: func(t *testing.T, s *mcp.Service) { + r, err := s.GetRunResponse( + context.Background(), + mcp.RunInput{ + Code: `mokapi.getApi('foo').getOperationDetails('/pets', 'GET')`, + }, + ) + require.NoError(t, err) + require.Equal(t, &mcp.OperationDetails{ + OperationId: "", + Method: "GET", + Path: "/pets", + Summary: "GET summary", + Description: ""}, + r.Result) + }, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + s := mcp.NewService(tc.app) + tc.test(t, s) + }) + } +} diff --git a/mcp/send_http_request.go b/mcp/send_http_request.go index 0ef86af8f..ec621a60c 100644 --- a/mcp/send_http_request.go +++ b/mcp/send_http_request.go @@ -32,7 +32,7 @@ func (s *Service) registerSendHttpRequest(server *mcp.Server) { "properties": map[string]any{ "apiName": map[string]any{ "type": "string", - "description": "The name of the API as returned by 'get_api_list'", + "description": "The name of the API as returned by 'mokapi_get_api_spec'", }, "method": map[string]any{ "type": "string", diff --git a/mcp/server.go b/mcp/server.go index 8945930ea..f7d25261d 100644 --- a/mcp/server.go +++ b/mcp/server.go @@ -39,6 +39,8 @@ func NewServer(app *runtime.App) http.Handler { svc.registerGetScenarios(server) svc.registerGetHttpMockTemplate(server) + svc.registerRunTool(server) + return mcp.NewStreamableHTTPHandler( func(*http.Request) *mcp.Server { return server }, &mcp.StreamableHTTPOptions{}, diff --git a/mcp/server_test.go b/mcp/server_test.go index 30611f6a3..edb36b615 100644 --- a/mcp/server_test.go +++ b/mcp/server_test.go @@ -31,14 +31,15 @@ func TestServer(t *testing.T) { list, err := session.ListTools(ctx, &gomcp.ListToolsParams{}) require.NoError(t, err) - require.Len(t, list.Tools, 8) + require.Len(t, list.Tools, 9) // alphabetical order - require.Equal(t, "mokapi_generate_http_mock_response", list.Tools[0].Name) - require.Equal(t, "mokapi_get_api_spec", list.Tools[1].Name) - require.Equal(t, "mokapi_get_events", list.Tools[2].Name) - require.Equal(t, "mokapi_get_http_mock_template", list.Tools[3].Name) - require.Equal(t, "mokapi_get_scenarios", list.Tools[4].Name) - require.Equal(t, "mokapi_get_typescript_api", list.Tools[5].Name) - require.Equal(t, "mokapi_produce_kafka_message", list.Tools[6].Name) - require.Equal(t, "mokapi_send_http_request", list.Tools[7].Name) + require.Equal(t, "mokapi_execute_code", list.Tools[0].Name) + require.Equal(t, "mokapi_generate_http_mock_response", list.Tools[1].Name) + require.Equal(t, "mokapi_get_api_spec", list.Tools[2].Name) + require.Equal(t, "mokapi_get_events", list.Tools[3].Name) + require.Equal(t, "mokapi_get_http_mock_template", list.Tools[4].Name) + require.Equal(t, "mokapi_get_scenarios", list.Tools[5].Name) + require.Equal(t, "mokapi_get_typescript_api", list.Tools[6].Name) + require.Equal(t, "mokapi_produce_kafka_message", list.Tools[7].Name) + require.Equal(t, "mokapi_send_http_request", list.Tools[8].Name) } diff --git a/providers/openapi/openapitest/operation.go b/providers/openapi/openapitest/operation.go index 8defe7640..97a51e2ad 100644 --- a/providers/openapi/openapitest/operation.go +++ b/providers/openapi/openapitest/operation.go @@ -24,6 +24,12 @@ func WithOperationId(id string) OperationOptions { } } +func WithOperationSummary(summary string) OperationOptions { + return func(o *openapi.Operation) { + o.Summary = summary + } +} + func WithResponse(status int, opts ...ResponseOptions) OperationOptions { return func(o *openapi.Operation) { r := &openapi.Response{ From f29b9b76e04b099ea15a2db7b527426c1e8ed718 Mon Sep 17 00:00:00 2001 From: marle3003 Date: Fri, 17 Apr 2026 08:07:26 +0200 Subject: [PATCH 25/46] wip: mcp server execute code --- mcp/run.go | 216 +++++++++++++++++++++++++++++++++++-- mcp/run.ts | 200 +++++++++++++++++++++++++++++++--- mcp/run_test.go | 280 ++++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 667 insertions(+), 29 deletions(-) diff --git a/mcp/run.go b/mcp/run.go index 22cfa3749..5fac95871 100644 --- a/mcp/run.go +++ b/mcp/run.go @@ -3,7 +3,15 @@ package mcp import ( "context" _ "embed" + "fmt" + "io" + "mokapi/js/faker" + "mokapi/providers/openapi" + "mokapi/providers/openapi/schema" "mokapi/runtime" + "mokapi/schema/json/generator" + "net/http" + "net/textproto" "reflect" "slices" "strings" @@ -122,6 +130,7 @@ type ApiSummary struct { func (m *mokapi) init(obj *goja.Object) { _ = obj.Set("getApis", m.getApis) _ = obj.Set("getApi", m.getApi) + _ = obj.Set("fake", m.fake) } func (m *mokapi) getApis() []ApiSummary { @@ -146,20 +155,30 @@ func (m *mokapi) getApi(name string) any { for _, api := range m.app.ListHttp() { if api.Info.Name == name { return &OpenAPI{ - Name: name, - Type: "http", - info: api, + Name: name, + Type: "http", + info: api, + handler: api.Handler(m.app.Monitor.Http, m.app.Engine, m.app.Events), } } } return nil } +func (m *mokapi) fake(v goja.Value) (any, error) { + js, err := faker.ToJsonSchema(v, m.vm) + if err != nil { + return nil, err + } + return generator.New(&generator.Request{Schema: js}) +} + type OpenAPI struct { Name string `json:"name"` Type string `json:"type"` - info *runtime.HttpInfo + info *runtime.HttpInfo + handler openapi.Handler } type OperationSummary struct { @@ -169,11 +188,41 @@ type OperationSummary struct { } type OperationDetails struct { - OperationId string `json:"operationId"` - Method string `json:"method"` - Path string `json:"path"` - Summary string `json:"summary"` - Description string `json:"description"` + OperationId string `json:"operationId"` + Method string `json:"method"` + Path string `json:"path"` + Summary string `json:"summary"` + Description string `json:"description,omitempty"` + Parameters []RequestParameters `json:"parameters,omitempty"` + RequestBody RequestBody `json:"requestBody,omitempty"` + + spec *openapi.Operation + handler openapi.Handler +} + +type RequestParameters struct { + Name string `json:"name"` + In string `json:"in"` + Required bool `json:"required"` + Schema *schema.Schema + Description string `json:"description,omitempty"` +} + +type RequestBody struct { + Description string `json:"description,omitempty"` + Required bool `json:"required"` + Contents []Content `json:"contents"` +} + +type Content struct { + ContentType string `json:"contentType"` + Schema *schema.Schema `json:"schema"` +} + +type Response struct { + StatusCode int `json:"statusCode"` + Description string `json:"description,omitempty"` + Content []Content `json:"content"` } func (o *OpenAPI) GetOperations() []OperationSummary { @@ -215,17 +264,164 @@ func (o *OpenAPI) GetOperationDetails(path, method string) *OperationDetails { if op == nil { continue } - return &OperationDetails{ + r := &OperationDetails{ OperationId: op.OperationId, Method: method, Path: p.Value.Path, Summary: op.Summary, Description: op.Description, + spec: op, + handler: o.handler, + } + for _, param := range op.Parameters { + if param.Value == nil { + continue + } + r.Parameters = append(r.Parameters, RequestParameters{ + Name: param.Value.Name, + In: param.Value.Type.String(), + Required: param.Value.Required, + Schema: param.Value.Schema, + Description: param.Value.Description, + }) } + if op.RequestBody != nil && op.RequestBody.Value != nil { + r.RequestBody = RequestBody{ + Description: op.RequestBody.Value.Description, + Required: op.RequestBody.Value.Required, + } + for ct, content := range op.RequestBody.Value.Content { + r.RequestBody.Contents = append(r.RequestBody.Contents, Content{ + ContentType: ct, + Schema: content.Schema, + }) + } + } + return r } return nil } +func (op *OperationDetails) GetResponseSchema(statusCode int) *Response { + r := op.spec.Responses.GetResponse(statusCode) + if r == nil { + return nil + } + result := &Response{ + StatusCode: statusCode, + Description: r.Description, + } + for ct, content := range r.Content { + result.Content = append(result.Content, Content{ + ContentType: ct, + Schema: content.Schema, + }) + } + return result +} + +type InvokeRequest struct { + Path map[string]string `json:"path"` + Query map[string]string `json:"query"` + Header map[string][]string `json:"header"` + Body string `json:"body"` +} + +type InvokeResponse struct { + StatusCode int `json:"statusCode"` + Headers map[string][]string `json:"headers"` + Body string `json:"body"` +} + +func (op *OperationDetails) Invoke(req InvokeRequest) (InvokeResponse, error) { + result := InvokeResponse{Headers: make(map[string][]string)} + + var body io.Reader + if req.Body != "" { + body = strings.NewReader(req.Body) + } + + path := op.Path + query := "" + params := append(op.spec.Path.Parameters, op.spec.Parameters...) + for _, p := range params { + if p.Value == nil { + continue + } + switch p.Value.Type { + case openapi.ParameterPath: + if req.Path == nil { + return result, fmt.Errorf("invoke request %s %s failed: missing path parameter '%s'", op.Method, op.Path, p.Value.Name) + } + val, ok := req.Path[p.Value.Name] + if !ok { + return result, fmt.Errorf("invoke request %s %s failed: missing path parameter '%s'", op.Method, op.Path, p.Value.Name) + } + path = strings.ReplaceAll(path, fmt.Sprintf("{%s}", p.Value.Name), val) + case openapi.ParameterQuery: + if req.Query == nil && p.Value.Required { + return result, fmt.Errorf("invoke request %s %s failed: missing query parameter '%s'", op.Method, op.Path, p.Value.Name) + } + val, ok := req.Query[p.Value.Name] + if !ok { + if !p.Value.Required { + continue + } + return result, fmt.Errorf("invoke request %s %s failed: missing query parameter '%s'", op.Method, op.Path, p.Value.Name) + } + if query != "" { + query += "&" + } + query += fmt.Sprintf("%s=%s", p.Value.Name, val) + } + } + + if query != "" { + path += "?" + query + } + + r, err := http.NewRequest(op.Method, path, body) + if err != nil { + return result, fmt.Errorf("error creating request: %w", err) + } + for _, p := range params { + if p.Value == nil || p.Value.Type != openapi.ParameterHeader { + continue + } + if req.Header == nil && p.Value.Required { + return result, fmt.Errorf("invoke request %s %s failed: missing header parameter '%s'", op.Method, op.Path, p.Value.Name) + } + val, ok := req.Header[p.Value.Name] + if !ok { + if !p.Value.Required { + continue + } + return result, fmt.Errorf("invoke request %s %s failed: missing header parameter '%s'", op.Method, op.Path, p.Value.Name) + } + r.Header[textproto.CanonicalMIMEHeaderKey(p.Value.Name)] = val + } + + he := op.handler.ServeHTTP(&result, r) + if he != nil { + result.StatusCode = he.StatusCode + result.Body = he.Message + } + return result, nil +} + +func (r *InvokeResponse) Header() http.Header { + return r.Headers +} + +func (r *InvokeResponse) WriteHeader(statusCode int) { + r.StatusCode = statusCode +} + +func (r *InvokeResponse) Write(body []byte) (int, error) { + r.Body = string(body) + return len(body), nil +} + type customFieldNameMapper struct { } diff --git a/mcp/run.ts b/mcp/run.ts index 35b39408d..a97768449 100644 --- a/mcp/run.ts +++ b/mcp/run.ts @@ -12,9 +12,11 @@ interface Mokapi { getApi(name: string): Api; /** - * Directly invoke an API operation. + * Generate a random value from a JSON Schema. + * @example + * fake({ type: 'string', format: 'email' }) */ - invoke(apiName: string, operationId: string, request?: InvokeRequest): InvokeResponse; + fake(schema: Schema): any; } interface ApiSummary { @@ -44,7 +46,12 @@ interface OperationSummary { interface OperationDetails { operationId: string; + method: string + path: string + summary: string description: string; + parameters: RequestParameter[] + requestBody: RequestBody /** * Invoke this operation against the mocked API. @@ -54,23 +61,192 @@ interface OperationDetails { /** * Returns the response schema for a given status code. */ - getResponseSchema(status: number): Schema; + getResponseSchema(statusCode: number): Response[]; +} + +interface RequestParameter { + name: string + in: 'path' | 'query' | 'headers' + required: boolean + schema: Schema + description?: string +} + +interface RequestBody { + description: string + required: boolean + contents: Content[] +} + +interface Content { + contentType: string + schema: Schema +} + +interface Response { + statusCode: number + description: string + contents: Content[] } interface InvokeRequest { - path?: Record; - query?: Record; - headers?: Record; - body?: any; + path?: Record; + query?: Record; + header?: Record; + body?: string; } interface InvokeResponse { - status: number; + statusCode: number; headers: Record; - body?: any; + body?: string; } +/** + * JSON Schema defines a JSON-based format for describing the structure of JSON data + * @example + * { + * "type": "string", + * "format": "email" + * } + */ interface Schema { - type?: string; - properties?: Record; -} \ No newline at end of file + /** + * Specifies the data type for a schema. + */ + type?: SchemaType | SchemaType[]; + + /** + * The enum keyword is used to restrict a value to a fixed set of values. + */ + enum?: any[]; + + /** + * The const keyword is used to restrict a value to a single value. + */ + const?: any; + + /** + * Contains a list of valid examples. + */ + examples?: any[]; + + /** + * Specifies a default value. + */ + default?: any; + + // Numbers + /** + * Restricts the number to a multiple of the given number + */ + multipleOf?: number; + + /** + * Restricts the number to a maximum number + */ + maximum?: number; + + /** + * Restricts the number to an exclusive maximum number + */ + exclusiveMaximum?: number; + + /** + * Restricts the number to a minimum number + */ + minimum?: number; + + /** + * Restricts the number to an exclusive minimum number + */ + exclusiveMinimum?: number; + + // Strings + /** + * Restricts the string to a maximum length + */ + maxLength?: number; + + /** + * Restricts the string to a minimum length + */ + minLength?: number; + + /** + * The pattern keyword is used to restrict a string to a particular regular expression. + */ + pattern?: string; + + /** + * The format keyword allows for basic semantic identification of certain kinds of string values that are commonly used. + */ + format?: string; + + // Arrays + /** + * Specifies the schema of the items in the array. + */ + items?: Schema; + + /** + * Restricts the array to have a maximum length + */ + maxItems?: number; + + /** + * Restricts the array to have a minimum length + */ + minItems?: number; + + /** + * Restricts the array to have unique items + */ + uniqueItems?: boolean; + + // Objects + /** + * Specifies the properties of an object + */ + properties?: { [name: string]: Schema }; + + /** + * Restricts the object to have a maximum of properties + */ + maxProperties?: number; + + /** + * Restricts the object to have a minimum of properties + */ + minProperties?: number; + + /** + * Specifies the required properties for an object + */ + required?: string[]; + + /** + * The additionalProperties keyword is used to control the handling of extra stuff, + * that is, properties whose names are not listed in the properties keyword or match + * any of the regular expressions in the patternProperties keyword. By default, any + * additional properties are allowed. + */ + additionalProperties?: boolean | Schema; + + /** + * A value must be valid against all the schemas + */ + allOf?: Schema[]; + + /** + * A value must be valid against any the schemas + */ + anyOf?: Schema[]; + + /** + * A value must be valid against exactly one the schemas + */ + oneOf?: Schema[]; +} + +type SchemaType = "object" | "array" | "number" | "integer" | "string" | "boolean" | "null"; \ No newline at end of file diff --git a/mcp/run_test.go b/mcp/run_test.go index 17e105efb..8e47eb6ad 100644 --- a/mcp/run_test.go +++ b/mcp/run_test.go @@ -4,9 +4,12 @@ import ( "context" "encoding/json" "mokapi/mcp" + "mokapi/providers/openapi" "mokapi/providers/openapi/openapitest" + "mokapi/providers/openapi/schema/schematest" "mokapi/runtime" "mokapi/runtime/runtimetest" + "mokapi/schema/json/generator" "net/http" "testing" @@ -124,6 +127,22 @@ func TestService_Run(t *testing.T) { openapitest.WithPath("/pets", openapitest.WithOperation(http.MethodGet, openapitest.WithOperationSummary("GET summary"), + openapitest.WithHeaderParam("foo", false, openapitest.WithParamSchema(schematest.New("string"))), + openapitest.WithRequestBody( + "request body description", + true, + openapitest.WithRequestContent( + "application/json", + &openapi.MediaType{ + Schema: schematest.New("string"), + }, + ), + ), + openapitest.WithResponse(200, + openapitest.WithContent("application/json", openapitest.WithSchema( + schematest.New("string"), + )), + ), ), openapitest.WithOperation(http.MethodPut, openapitest.WithOperationSummary("PUT summary"), @@ -139,19 +158,266 @@ func TestService_Run(t *testing.T) { }, ) require.NoError(t, err) - require.Equal(t, &mcp.OperationDetails{ - OperationId: "", - Method: "GET", - Path: "/pets", - Summary: "GET summary", - Description: ""}, - r.Result) + require.IsType(t, &mcp.OperationDetails{}, r.Result) + op := r.Result.(*mcp.OperationDetails) + require.Equal(t, "", op.OperationId) + require.Equal(t, "GET", op.Method) + require.Equal(t, "/pets", op.Path) + require.Equal(t, "GET summary", op.Summary) + require.Equal(t, "", op.Description) + require.Equal(t, "foo", op.Parameters[0].Name) + require.Equal(t, "header", op.Parameters[0].In) + require.Equal(t, false, op.Parameters[0].Required) + require.Equal(t, "string", op.Parameters[0].Schema.Type[0]) + require.Equal(t, "", op.Parameters[0].Description) + require.Equal(t, "request body description", op.RequestBody.Description) + require.Equal(t, true, op.RequestBody.Required) + require.Equal(t, "application/json", op.RequestBody.Contents[0].ContentType) + require.Equal(t, "string", op.RequestBody.Contents[0].Schema.Type[0]) + }, + }, + { + name: "getResponseSchema", + app: runtimetest.NewHttpApp( + openapitest.NewConfig("3.1.0", + openapitest.WithInfo("foo", "", ""), + openapitest.WithPath("/pets", + openapitest.WithOperation(http.MethodGet, + openapitest.WithResponse(200, + openapitest.WithResponseDescription("response description"), + openapitest.WithContent("application/json", openapitest.WithSchema( + schematest.New("string"), + )), + ), + ), + ), + ), + ), + test: func(t *testing.T, s *mcp.Service) { + r, err := s.GetRunResponse( + context.Background(), + mcp.RunInput{ + Code: `const op = mokapi.getApi('foo').getOperationDetails('/pets', 'GET') +op.getResponseSchema(200)`, + }, + ) + require.NoError(t, err) + require.IsType(t, &mcp.Response{}, r.Result) + res := r.Result.(*mcp.Response) + require.Equal(t, 200, res.StatusCode) + require.Equal(t, "response description", res.Description) + require.Equal(t, "application/json", res.Content[0].ContentType) + require.Equal(t, "string", res.Content[0].Schema.Type[0]) + }, + }, + { + name: "getResponseSchema not exists", + app: runtimetest.NewHttpApp( + openapitest.NewConfig("3.1.0", + openapitest.WithInfo("foo", "", ""), + openapitest.WithPath("/pets", + openapitest.WithOperation(http.MethodGet, + openapitest.WithResponse(200, + openapitest.WithResponseDescription("response description"), + openapitest.WithContent("application/json", openapitest.WithSchema( + schematest.New("string"), + )), + ), + ), + ), + ), + ), + test: func(t *testing.T, s *mcp.Service) { + r, err := s.GetRunResponse( + context.Background(), + mcp.RunInput{ + Code: `const op = mokapi.getApi('foo').getOperationDetails('/pets', 'GET') +op.getResponseSchema(404)`, + }, + ) + require.NoError(t, err) + require.Nil(t, r.Result) + }, + }, + { + name: "invoke request", + app: runtimetest.NewHttpApp( + openapitest.NewConfig("3.1.0", + openapitest.WithInfo("foo", "", ""), + openapitest.WithPath("/pets", + openapitest.WithOperation(http.MethodGet, + openapitest.WithResponse(200, + openapitest.WithResponseDescription("response description"), + openapitest.WithContent("application/json", openapitest.WithSchema( + schematest.New("string"), + )), + ), + ), + ), + ), + ), + test: func(t *testing.T, s *mcp.Service) { + r, err := s.GetRunResponse( + context.Background(), + mcp.RunInput{ + Code: `const op = mokapi.getApi('foo').getOperationDetails('/pets', 'GET'); +op.invoke()`, + }, + ) + require.NoError(t, err) + require.IsType(t, mcp.InvokeResponse{}, r.Result) + res := r.Result.(mcp.InvokeResponse) + require.Equal(t, 200, res.StatusCode) + require.Equal(t, `"P8"`, res.Body) + require.Equal(t, map[string][]string{ + "Content-Type": {"application/json"}, + }, res.Headers) + }, + }, + { + name: "invoke request with path parameter", + app: runtimetest.NewHttpApp( + openapitest.NewConfig("3.1.0", + openapitest.WithInfo("foo", "", ""), + openapitest.WithPath("/{foo}/{bar}/pets", + openapitest.WithOperation(http.MethodGet, + openapitest.WithOperationParam("foo", true, openapitest.WithParamSchema(schematest.New("string"))), + openapitest.WithOperationParam("bar", true, openapitest.WithParamSchema(schematest.New("string"))), + openapitest.WithResponse(200, + openapitest.WithResponseDescription("response description"), + openapitest.WithContent("application/json", openapitest.WithSchema( + schematest.New("string"), + )), + ), + ), + ), + ), + ), + test: func(t *testing.T, s *mcp.Service) { + r, err := s.GetRunResponse( + context.Background(), + mcp.RunInput{ + Code: `const op = mokapi.getApi('foo').getOperationDetails('/{foo}/{bar}/pets', 'GET'); +op.invoke({ path: { foo: 'val1', 'bar': 'val2' }})`, + }, + ) + require.NoError(t, err) + res := r.Result.(mcp.InvokeResponse) + require.Equal(t, http.StatusOK, res.StatusCode, res.Body) + }, + }, + { + name: "invoke request with path parameter but not specified", + app: runtimetest.NewHttpApp( + openapitest.NewConfig("3.1.0", + openapitest.WithInfo("foo", "", ""), + openapitest.WithPath("/{foo}/{bar}/pets", + openapitest.WithOperation(http.MethodGet, + openapitest.WithOperationParam("foo", true, openapitest.WithParamSchema(schematest.New("string"))), + openapitest.WithOperationParam("bar", true, openapitest.WithParamSchema(schematest.New("string"))), + openapitest.WithResponse(200, + openapitest.WithResponseDescription("response description"), + openapitest.WithContent("application/json", openapitest.WithSchema( + schematest.New("string"), + )), + ), + ), + ), + ), + ), + test: func(t *testing.T, s *mcp.Service) { + _, err := s.GetRunResponse( + context.Background(), + mcp.RunInput{ + Code: `const op = mokapi.getApi('foo').getOperationDetails('/{foo}/{bar}/pets', 'GET'); +op.invoke()`, + }, + ) + require.EqualError(t, err, "invoke request GET /{foo}/{bar}/pets failed: missing path parameter 'foo'") + }, + }, + { + name: "invoke request with query parameter", + app: runtimetest.NewHttpApp( + openapitest.NewConfig("3.1.0", + openapitest.WithInfo("foo", "", ""), + openapitest.WithPath("/pets", + openapitest.WithOperation(http.MethodGet, + openapitest.WithQueryParam("foo", true, openapitest.WithParamSchema(schematest.New("string"))), + openapitest.WithResponse(200, + openapitest.WithResponseDescription("response description"), + openapitest.WithContent("application/json", openapitest.WithSchema( + schematest.New("string"), + )), + ), + ), + ), + ), + ), + test: func(t *testing.T, s *mcp.Service) { + r, err := s.GetRunResponse( + context.Background(), + mcp.RunInput{ + Code: `const op = mokapi.getApi('foo').getOperationDetails('/pets', 'GET'); +op.invoke({ query: { foo: 'val1' }})`, + }, + ) + require.NoError(t, err) + res := r.Result.(mcp.InvokeResponse) + require.Equal(t, http.StatusOK, res.StatusCode, res.Body) + }, + }, + { + name: "invoke request with header parameter", + app: runtimetest.NewHttpApp( + openapitest.NewConfig("3.1.0", + openapitest.WithInfo("foo", "", ""), + openapitest.WithPath("/pets", + openapitest.WithOperation(http.MethodGet, + openapitest.WithHeaderParam("foo", true, openapitest.WithParamSchema(schematest.New("string"))), + openapitest.WithResponse(200, + openapitest.WithResponseDescription("response description"), + openapitest.WithContent("application/json", openapitest.WithSchema( + schematest.New("string"), + )), + ), + ), + ), + ), + ), + test: func(t *testing.T, s *mcp.Service) { + r, err := s.GetRunResponse( + context.Background(), + mcp.RunInput{ + Code: `const op = mokapi.getApi('foo').getOperationDetails('/pets', 'GET'); +op.invoke({ header: { foo: ['val1'] }})`, + }, + ) + require.NoError(t, err) + res := r.Result.(mcp.InvokeResponse) + require.Equal(t, http.StatusOK, res.StatusCode, res.Body) + }, + }, + { + name: "fake", + app: runtimetest.NewApp(), + test: func(t *testing.T, s *mcp.Service) { + r, err := s.GetRunResponse( + context.Background(), + mcp.RunInput{ + Code: `mokapi.fake({ type: 'string', format: 'email' })`, + }, + ) + require.NoError(t, err) + require.Equal(t, "ivyjones@ziemann.com", r.Result) }, }, } for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { + generator.Seed(123456) + s := mcp.NewService(tc.app) tc.test(t, s) }) From 65855cb5c42e5efd18a1048248e3f2a9eb92fb39 Mon Sep 17 00:00:00 2001 From: marle3003 Date: Fri, 17 Apr 2026 08:54:52 +0200 Subject: [PATCH 26/46] wip: mcp server execute code --- mcp/run_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mcp/run_test.go b/mcp/run_test.go index 8e47eb6ad..cdfeebc3e 100644 --- a/mcp/run_test.go +++ b/mcp/run_test.go @@ -333,7 +333,7 @@ op.invoke({ path: { foo: 'val1', 'bar': 'val2' }})`, op.invoke()`, }, ) - require.EqualError(t, err, "invoke request GET /{foo}/{bar}/pets failed: missing path parameter 'foo'") + require.EqualError(t, err, "GoError: invoke request GET /{foo}/{bar}/pets failed: missing path parameter 'foo' at reflect.methodValueCall (native)") }, }, { From 157567b6490a92c3482943a086cc99c9475704e9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 Apr 2026 07:43:53 +0000 Subject: [PATCH 27/46] build(deps): bump github.com/go-git/go-git/v5 from 5.17.2 to 5.18.0 Bumps [github.com/go-git/go-git/v5](https://github.com/go-git/go-git) from 5.17.2 to 5.18.0. - [Release notes](https://github.com/go-git/go-git/releases) - [Commits](https://github.com/go-git/go-git/compare/v5.17.2...v5.18.0) --- updated-dependencies: - dependency-name: github.com/go-git/go-git/v5 dependency-version: 5.18.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 749882473..e43eb2e51 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/evanw/esbuild v0.28.0 github.com/fsnotify/fsnotify v1.9.0 github.com/go-co-op/gocron v1.37.0 - github.com/go-git/go-git/v5 v5.17.2 + github.com/go-git/go-git/v5 v5.18.0 github.com/golang-jwt/jwt/v4 v4.5.2 github.com/google/uuid v1.6.0 github.com/jinzhu/inflection v1.0.0 diff --git a/go.sum b/go.sum index d475c825b..26ee2e297 100644 --- a/go.sum +++ b/go.sum @@ -95,8 +95,8 @@ github.com/go-git/go-billy/v5 v5.8.0 h1:I8hjc3LbBlXTtVuFNJuwYuMiHvQJDq1AT6u4DwDz github.com/go-git/go-billy/v5 v5.8.0/go.mod h1:RpvI/rw4Vr5QA+Z60c6d6LXH0rYJo0uD5SqfmrrheCY= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= -github.com/go-git/go-git/v5 v5.17.2 h1:B+nkdlxdYrvyFK4GPXVU8w1U+YkbsgciIR7f2sZJ104= -github.com/go-git/go-git/v5 v5.17.2/go.mod h1:pW/VmeqkanRFqR6AljLcs7EA7FbZaN5MQqO7oZADXpo= +github.com/go-git/go-git/v5 v5.18.0 h1:O831KI+0PR51hM2kep6T8k+w0/LIAD490gvqMCvL5hM= +github.com/go-git/go-git/v5 v5.18.0/go.mod h1:pW/VmeqkanRFqR6AljLcs7EA7FbZaN5MQqO7oZADXpo= github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU= github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= From 889113223c8bba02fa36cca8292f670a11742b15 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 Apr 2026 07:44:02 +0000 Subject: [PATCH 28/46] build(deps-dev): bump @types/node from 25.5.1 to 25.6.0 in /webui Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 25.5.1 to 25.6.0. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) --- updated-dependencies: - dependency-name: "@types/node" dependency-version: 25.6.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- webui/package-lock.json | 16 ++++++++-------- webui/package.json | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/webui/package-lock.json b/webui/package-lock.json index f2f023c1d..04190ecae 100644 --- a/webui/package-lock.json +++ b/webui/package-lock.json @@ -43,7 +43,7 @@ "@rushstack/eslint-patch": "^1.16.1", "@types/js-yaml": "^4.0.9", "@types/markdown-it-container": "^4.0.0", - "@types/node": "^25.5.1", + "@types/node": "^25.6.0", "@vitejs/plugin-vue": "^6.0.5", "@vue/eslint-config-prettier": "^10.2.0", "@vue/eslint-config-typescript": "^14.7.0", @@ -838,12 +838,12 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "25.5.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.1.tgz", - "integrity": "sha512-lgrR3HRNQdTEeeXBnLURFO4JIIbpcVcMlLM9IG0jsNRTRNSbMkm9S2hyhxhnokke1NM25Dr9QghgeB5PQKolrw==", + "version": "25.6.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", + "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", "license": "MIT", "dependencies": { - "undici-types": "~7.18.0" + "undici-types": "~7.19.0" } }, "node_modules/@types/nodemailer": { @@ -6570,9 +6570,9 @@ } }, "node_modules/undici-types": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", - "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "version": "7.19.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", + "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", "license": "MIT" }, "node_modules/unicorn-magic": { diff --git a/webui/package.json b/webui/package.json index 27b775b1e..3a782860f 100644 --- a/webui/package.json +++ b/webui/package.json @@ -52,7 +52,7 @@ "@rushstack/eslint-patch": "^1.16.1", "@types/js-yaml": "^4.0.9", "@types/markdown-it-container": "^4.0.0", - "@types/node": "^25.5.1", + "@types/node": "^25.6.0", "@vitejs/plugin-vue": "^6.0.5", "@vue/eslint-config-prettier": "^10.2.0", "@vue/eslint-config-typescript": "^14.7.0", From 082a437527252ae112a68f0ecedea946a532273f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 Apr 2026 07:52:30 +0000 Subject: [PATCH 29/46] build(deps-dev): bump prettier from 3.8.2 to 3.8.3 in /webui Bumps [prettier](https://github.com/prettier/prettier) from 3.8.2 to 3.8.3. - [Release notes](https://github.com/prettier/prettier/releases) - [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md) - [Commits](https://github.com/prettier/prettier/compare/3.8.2...3.8.3) --- updated-dependencies: - dependency-name: prettier dependency-version: 3.8.3 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- webui/package-lock.json | 8 ++++---- webui/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/webui/package-lock.json b/webui/package-lock.json index 04190ecae..78ef4b9c9 100644 --- a/webui/package-lock.json +++ b/webui/package-lock.json @@ -51,7 +51,7 @@ "eslint": "^10.2.0", "eslint-plugin-vue": "^10.8.0", "npm-run-all": "^4.1.5", - "prettier": "^3.8.2", + "prettier": "^3.8.3", "typescript": "~6.0.2", "vite": "^8.0.8", "vue-tsc": "^3.2.6", @@ -5415,9 +5415,9 @@ } }, "node_modules/prettier": { - "version": "3.8.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.2.tgz", - "integrity": "sha512-8c3mgTe0ASwWAJK+78dpviD+A8EqhndQPUBpNUIPt6+xWlIigCwfN01lWr9MAede4uqXGTEKeQWTvzb3vjia0Q==", + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", + "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", "dev": true, "license": "MIT", "bin": { diff --git a/webui/package.json b/webui/package.json index 3a782860f..0d48f655e 100644 --- a/webui/package.json +++ b/webui/package.json @@ -60,7 +60,7 @@ "eslint": "^10.2.0", "eslint-plugin-vue": "^10.8.0", "npm-run-all": "^4.1.5", - "prettier": "^3.8.2", + "prettier": "^3.8.3", "typescript": "~6.0.2", "vite": "^8.0.8", "vue-tsc": "^3.2.6", From 7322e279689d1222490e597d25585af171ed2991 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 Apr 2026 08:00:07 +0000 Subject: [PATCH 30/46] build(deps-dev): bump @vitejs/plugin-vue from 6.0.5 to 6.0.6 in /webui Bumps [@vitejs/plugin-vue](https://github.com/vitejs/vite-plugin-vue/tree/HEAD/packages/plugin-vue) from 6.0.5 to 6.0.6. - [Release notes](https://github.com/vitejs/vite-plugin-vue/releases) - [Changelog](https://github.com/vitejs/vite-plugin-vue/blob/main/packages/plugin-vue/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite-plugin-vue/commits/plugin-vue@6.0.6/packages/plugin-vue) --- updated-dependencies: - dependency-name: "@vitejs/plugin-vue" dependency-version: 6.0.6 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- webui/package-lock.json | 16 ++++++++-------- webui/package.json | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/webui/package-lock.json b/webui/package-lock.json index 78ef4b9c9..08b723287 100644 --- a/webui/package-lock.json +++ b/webui/package-lock.json @@ -44,7 +44,7 @@ "@types/js-yaml": "^4.0.9", "@types/markdown-it-container": "^4.0.0", "@types/node": "^25.6.0", - "@vitejs/plugin-vue": "^6.0.5", + "@vitejs/plugin-vue": "^6.0.6", "@vue/eslint-config-prettier": "^10.2.0", "@vue/eslint-config-typescript": "^14.7.0", "@vue/tsconfig": "^0.9.1", @@ -706,9 +706,9 @@ } }, "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-rc.2", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.2.tgz", - "integrity": "sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw==", + "version": "1.0.0-rc.13", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.13.tgz", + "integrity": "sha512-3ngTAv6F/Py35BsYbeeLeecvhMKdsKm4AoOETVhAA+Qc8nrA2I0kF7oa93mE9qnIurngOSpMnQ0x2nQY2FPviA==", "dev": true, "license": "MIT" }, @@ -1105,13 +1105,13 @@ } }, "node_modules/@vitejs/plugin-vue": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.5.tgz", - "integrity": "sha512-bL3AxKuQySfk1iGcBsQnoRVexTPJq0Z/ixFVM8OhVJAP6ZXXXLtM7NFKWhLl30Kg7uTBqIaPXbh+nuQCuBDedg==", + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.6.tgz", + "integrity": "sha512-u9HHgfrq3AjXlysn0eINFnWQOJQLO9WN6VprZ8FXl7A2bYisv3Hui9Ij+7QZ41F/WYWarHjwBbXtD7dKg3uxbg==", "dev": true, "license": "MIT", "dependencies": { - "@rolldown/pluginutils": "1.0.0-rc.2" + "@rolldown/pluginutils": "1.0.0-rc.13" }, "engines": { "node": "^20.19.0 || >=22.12.0" diff --git a/webui/package.json b/webui/package.json index 0d48f655e..f4024858a 100644 --- a/webui/package.json +++ b/webui/package.json @@ -53,7 +53,7 @@ "@types/js-yaml": "^4.0.9", "@types/markdown-it-container": "^4.0.0", "@types/node": "^25.6.0", - "@vitejs/plugin-vue": "^6.0.5", + "@vitejs/plugin-vue": "^6.0.6", "@vue/eslint-config-prettier": "^10.2.0", "@vue/eslint-config-typescript": "^14.7.0", "@vue/tsconfig": "^0.9.1", From 320cf77a675eae4560e2cedd67513e782cf3ad98 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 Apr 2026 08:08:02 +0000 Subject: [PATCH 31/46] build(deps-dev): bump typescript from 6.0.2 to 6.0.3 in /webui Bumps [typescript](https://github.com/microsoft/TypeScript) from 6.0.2 to 6.0.3. - [Release notes](https://github.com/microsoft/TypeScript/releases) - [Commits](https://github.com/microsoft/TypeScript/compare/v6.0.2...v6.0.3) --- updated-dependencies: - dependency-name: typescript dependency-version: 6.0.3 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- webui/package-lock.json | 8 ++++---- webui/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/webui/package-lock.json b/webui/package-lock.json index 08b723287..3f5b200fc 100644 --- a/webui/package-lock.json +++ b/webui/package-lock.json @@ -52,7 +52,7 @@ "eslint-plugin-vue": "^10.8.0", "npm-run-all": "^4.1.5", "prettier": "^3.8.3", - "typescript": "~6.0.2", + "typescript": "~6.0.3", "vite": "^8.0.8", "vue-tsc": "^3.2.6", "xml2js": "^0.6.2" @@ -6501,9 +6501,9 @@ } }, "node_modules/typescript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz", - "integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "devOptional": true, "license": "Apache-2.0", "bin": { diff --git a/webui/package.json b/webui/package.json index f4024858a..1f9f2d3ed 100644 --- a/webui/package.json +++ b/webui/package.json @@ -61,7 +61,7 @@ "eslint-plugin-vue": "^10.8.0", "npm-run-all": "^4.1.5", "prettier": "^3.8.3", - "typescript": "~6.0.2", + "typescript": "~6.0.3", "vite": "^8.0.8", "vue-tsc": "^3.2.6", "xml2js": "^0.6.2" From 1b032c277a76a1e482aab23b8116c8994cddeb8b Mon Sep 17 00:00:00 2001 From: maesi Date: Fri, 17 Apr 2026 19:06:10 +0200 Subject: [PATCH 32/46] test: add LDAP search with scope whole subtree --- providers/directory/search_test.go | 31 ++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/providers/directory/search_test.go b/providers/directory/search_test.go index 614f16483..ff47e87a9 100644 --- a/providers/directory/search_test.go +++ b/providers/directory/search_test.go @@ -609,6 +609,37 @@ func TestSearch(t *testing.T) { require.Len(t, res.Results, 1) }, }, + { + name: "scope whole subtree", + input: `{ "files": [ "./users.ldif" ] }`, + reader: &dynamictest.Reader{Data: map[string]*dynamic.Config{ + "file:/users.ldif": {Raw: []byte(` +dn: cn=user + +dn: id=user1,ou=Sales,dc=example,dc=com +foo: bar + +dn: id=user2,ou=Sales,dc=example,dc=com +foo: bar + +dn: id=user3,ou=Accounting,dc=example,dc=com +foo: bar +`)}, + }}, + test: func(t *testing.T, h ldap.Handler, err error) { + require.NoError(t, err) + + rr := ldaptest.NewRecorder() + h.ServeLDAP(rr, ldaptest.NewRequest(0, &ldap.SearchRequest{ + Scope: ldap.ScopeWholeSubtree, + BaseDN: "ou=Sales,dc=example,dc=com", + Filter: "(foo=bar)", + })) + res := rr.Message.(*ldap.SearchResponse) + + require.Len(t, res.Results, 2) + }, + }, } t.Parallel() From 4c0502cb207feba4fa3eed283b510cdeebe69d18 Mon Sep 17 00:00:00 2001 From: maesi Date: Fri, 17 Apr 2026 19:40:18 +0200 Subject: [PATCH 33/46] test: add test for LDAP search if no equality is specified in schema --- providers/directory/search_test.go | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/providers/directory/search_test.go b/providers/directory/search_test.go index ff47e87a9..807fd4881 100644 --- a/providers/directory/search_test.go +++ b/providers/directory/search_test.go @@ -431,6 +431,32 @@ attributeTypes: ( 1.2.3.4.5.6.7.8 NAME 'objectSid' DESC 'objectSid' EQUALITY act require.Equal(t, "ldap: filter syntax error: invalid SID 'S-1-5-21-foo-1234567890-1234567890-1001': invalid uint value 'foo' at position: 3", log.Entries[1].Message) }, }, + { + name: "no EQUALITY specified", + input: `{ "files": [ "./schema.ldif", "./users.ldif" ] }`, + reader: &dynamictest.Reader{Data: map[string]*dynamic.Config{ + "file:/schema.ldif": {Raw: []byte(` +dn: +subschemaSubentry: cn=schema + +dn: cn=schema +attributeTypes: ( 2.5.4.3 NAME 'cn' DESC 'Common Name' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE ) +`)}, + "file:/users.ldif": {Raw: []byte("dn: cn=user\ncn: UsEr")}, + }}, + test: func(t *testing.T, h ldap.Handler, _ *test.Hook, err error) { + require.NoError(t, err) + + rr := ldaptest.NewRecorder() + h.ServeLDAP(rr, ldaptest.NewRequest(0, &ldap.SearchRequest{ + Scope: ldap.ScopeWholeSubtree, + Filter: "(cn=user)", + })) + res := rr.Message.(*ldap.SearchResponse) + + require.Len(t, res.Results, 1) + }, + }, } for _, tc := range testcases { From 1a838662ffe54c1c63b6da5f24e43a43e6a22078 Mon Sep 17 00:00:00 2001 From: maesi Date: Fri, 17 Apr 2026 22:23:15 +0200 Subject: [PATCH 34/46] test: add test for LDIF folded comment --- providers/directory/config_ldif_test.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/providers/directory/config_ldif_test.go b/providers/directory/config_ldif_test.go index d11b5dbe0..0b3e9c365 100644 --- a/providers/directory/config_ldif_test.go +++ b/providers/directory/config_ldif_test.go @@ -110,6 +110,18 @@ func TestLdif_Parse(t *testing.T) { }, ld.Records[0]) }, }, + { + name: "comment", + input: "dn: dc=mokapi, dc=io\n# line 1\n line 2\nfoo: bar", + test: func(t *testing.T, ld *Ldif, err error) { + require.NoError(t, err) + require.Len(t, ld.Records, 1) + require.Equal(t, &AddRecord{ + Dn: "dc=mokapi, dc=io", + Attributes: map[string][]string{"foo": {"bar"}}, + }, ld.Records[0]) + }, + }, { name: "version set", input: "version: 1\ndn: dc=mokapi, dc=io", From 25d8c5209aab87c11832d211f9bb3cacb57bf659 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 Apr 2026 20:31:35 +0000 Subject: [PATCH 35/46] build(deps): bump golang.org/x/text from 0.35.0 to 0.36.0 Bumps [golang.org/x/text](https://github.com/golang/text) from 0.35.0 to 0.36.0. - [Release notes](https://github.com/golang/text/releases) - [Commits](https://github.com/golang/text/compare/v0.35.0...v0.36.0) --- updated-dependencies: - dependency-name: golang.org/x/text dependency-version: 0.36.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index e43eb2e51..f870b0c55 100644 --- a/go.mod +++ b/go.mod @@ -22,7 +22,7 @@ require ( github.com/stretchr/testify v1.11.1 github.com/yuin/gopher-lua v1.1.2 golang.org/x/net v0.52.0 - golang.org/x/text v0.35.0 + golang.org/x/text v0.36.0 gopkg.in/go-asn1-ber/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d gopkg.in/yaml.v3 v3.0.1 layeh.com/gopher-luar v1.0.11 diff --git a/go.sum b/go.sum index 26ee2e297..d3a60c776 100644 --- a/go.sum +++ b/go.sum @@ -227,11 +227,11 @@ golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9sn 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.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= -golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= +golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= +golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= -golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= +golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= +golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From 9eefc594dbc56fe58821fbcdf67d025e34ed9c81 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 Apr 2026 20:40:18 +0000 Subject: [PATCH 36/46] build(deps): bump golang.org/x/net from 0.52.0 to 0.53.0 Bumps [golang.org/x/net](https://github.com/golang/net) from 0.52.0 to 0.53.0. - [Commits](https://github.com/golang/net/compare/v0.52.0...v0.53.0) --- updated-dependencies: - dependency-name: golang.org/x/net dependency-version: 0.53.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 6 +++--- go.sum | 16 ++++++++-------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/go.mod b/go.mod index f870b0c55..039c9f153 100644 --- a/go.mod +++ b/go.mod @@ -21,7 +21,7 @@ require ( github.com/sirupsen/logrus v1.9.4 github.com/stretchr/testify v1.11.1 github.com/yuin/gopher-lua v1.1.2 - golang.org/x/net v0.52.0 + golang.org/x/net v0.53.0 golang.org/x/text v0.36.0 gopkg.in/go-asn1-ber/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d gopkg.in/yaml.v3 v3.0.1 @@ -85,9 +85,9 @@ require ( github.com/yosida95/uritemplate/v3 v3.0.2 // indirect go.etcd.io/bbolt v1.4.0 // indirect go.uber.org/atomic v1.9.0 // indirect - golang.org/x/crypto v0.49.0 // indirect + golang.org/x/crypto v0.50.0 // indirect golang.org/x/oauth2 v0.34.0 // indirect - golang.org/x/sys v0.42.0 // indirect + golang.org/x/sys v0.43.0 // indirect google.golang.org/protobuf v1.36.6 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect ) diff --git a/go.sum b/go.sum index d3a60c776..692e74c74 100644 --- a/go.sum +++ b/go.sum @@ -202,13 +202,13 @@ go.etcd.io/bbolt v1.4.0/go.mod h1:AsD+OCi/qPN1giOX1aiLAha3o1U8rAz65bvN4j0sRuk= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= -golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= +golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= +golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= -golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= +golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= +golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= @@ -221,11 +221,11 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= -golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= +golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= -golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= +golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY= +golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= From dd4fd4506eab1b854d7f0f889cfcca21f838a4a5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 Apr 2026 20:48:47 +0000 Subject: [PATCH 37/46] build(deps): bump github.com/modelcontextprotocol/go-sdk Bumps [github.com/modelcontextprotocol/go-sdk](https://github.com/modelcontextprotocol/go-sdk) from 1.4.1 to 1.5.0. - [Release notes](https://github.com/modelcontextprotocol/go-sdk/releases) - [Commits](https://github.com/modelcontextprotocol/go-sdk/compare/v1.4.1...v1.5.0) --- updated-dependencies: - dependency-name: github.com/modelcontextprotocol/go-sdk dependency-version: 1.5.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 4 ++-- go.sum | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/go.mod b/go.mod index 039c9f153..27ae7408e 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,7 @@ require ( github.com/golang-jwt/jwt/v4 v4.5.2 github.com/google/uuid v1.6.0 github.com/jinzhu/inflection v1.0.0 - github.com/modelcontextprotocol/go-sdk v1.4.1 + github.com/modelcontextprotocol/go-sdk v1.5.0 github.com/pkg/errors v0.9.1 github.com/sirupsen/logrus v1.9.4 github.com/stretchr/testify v1.11.1 @@ -86,7 +86,7 @@ require ( go.etcd.io/bbolt v1.4.0 // indirect go.uber.org/atomic v1.9.0 // indirect golang.org/x/crypto v0.50.0 // indirect - golang.org/x/oauth2 v0.34.0 // indirect + golang.org/x/oauth2 v0.35.0 // indirect golang.org/x/sys v0.43.0 // indirect google.golang.org/protobuf v1.36.6 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect diff --git a/go.sum b/go.sum index 692e74c74..961166648 100644 --- a/go.sum +++ b/go.sum @@ -101,8 +101,8 @@ github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyL github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= -github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= -github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= +github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= @@ -148,8 +148,8 @@ github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMK github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY= github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= -github.com/modelcontextprotocol/go-sdk v1.4.1 h1:M4x9GyIPj+HoIlHNGpK2hq5o3BFhC+78PkEaldQRphc= -github.com/modelcontextprotocol/go-sdk v1.4.1/go.mod h1:Bo/mS87hPQqHSRkMv4dQq1XCu6zv4INdXnFZabkNU6s= +github.com/modelcontextprotocol/go-sdk v1.5.0 h1:CHU0FIX9kpueNkxuYtfYQn1Z0slhFzBZuq+x6IiblIU= +github.com/modelcontextprotocol/go-sdk v1.5.0/go.mod h1:gggDIhoemhWs3BGkGwd1umzEXCEMMvAnhTrnbXJKKKA= github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM= github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw= github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= @@ -209,8 +209,8 @@ golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbR golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= -golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= -golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ= +golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= From 223b05adec413de1413c24fb4065a872de133bd6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 Apr 2026 20:57:25 +0000 Subject: [PATCH 38/46] build(deps): bump github.com/blevesearch/bleve_index_api Bumps [github.com/blevesearch/bleve_index_api](https://github.com/blevesearch/bleve_index_api) from 1.3.9 to 1.3.10. - [Commits](https://github.com/blevesearch/bleve_index_api/compare/v1.3.9...v1.3.10) --- updated-dependencies: - dependency-name: github.com/blevesearch/bleve_index_api dependency-version: 1.3.10 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 27ae7408e..9a52f7d01 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.25.5 require ( github.com/Masterminds/sprig v2.22.0+incompatible github.com/blevesearch/bleve/v2 v2.5.7 - github.com/blevesearch/bleve_index_api v1.3.9 + github.com/blevesearch/bleve_index_api v1.3.10 github.com/bradleyfalzon/ghinstallation/v2 v2.18.0 github.com/brianvoe/gofakeit/v6 v6.28.0 github.com/dop251/goja v0.0.0-20250309171923-bcd7cc6bf64c diff --git a/go.sum b/go.sum index 961166648..5fc4bf025 100644 --- a/go.sum +++ b/go.sum @@ -24,8 +24,8 @@ github.com/bits-and-blooms/bitset v1.22.0 h1:Tquv9S8+SGaS3EhyA+up3FXzmkhxPGjQQCk github.com/bits-and-blooms/bitset v1.22.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/blevesearch/bleve/v2 v2.5.7 h1:2d9YrL5zrX5EBBW++GOaEKjE+NPWeZGaX77IM26m1Z8= github.com/blevesearch/bleve/v2 v2.5.7/go.mod h1:yj0NlS7ocGC4VOSAedqDDMktdh2935v2CSWOCDMHdSA= -github.com/blevesearch/bleve_index_api v1.3.9 h1:TLoiBaqcfWGfI1Il0+zzky452uYCPoSMosDSltkCfKs= -github.com/blevesearch/bleve_index_api v1.3.9/go.mod h1:xvd48t5XMeeioWQ5/jZvgLrV98flT2rdvEJ3l/ki4Ko= +github.com/blevesearch/bleve_index_api v1.3.10 h1:a7G+IOMa2xuO6f8vtutbTsqjVLpLuCuH3uoTZHkGiYg= +github.com/blevesearch/bleve_index_api v1.3.10/go.mod h1:xvd48t5XMeeioWQ5/jZvgLrV98flT2rdvEJ3l/ki4Ko= github.com/blevesearch/geo v0.2.4 h1:ECIGQhw+QALCZaDcogRTNSJYQXRtC8/m8IKiA706cqk= github.com/blevesearch/geo v0.2.4/go.mod h1:K56Q33AzXt2YExVHGObtmRSFYZKYGv0JEN5mdacJJR8= github.com/blevesearch/go-faiss v1.0.26 h1:4dRLolFgjPyjkaXwff4NfbZFdE/dfywbzDqporeQvXI= From d4c9b6976291680ebcc6e53ed894031f680ad49f Mon Sep 17 00:00:00 2001 From: maesi Date: Sat, 18 Apr 2026 00:02:23 +0200 Subject: [PATCH 39/46] wip: fix setup for tool mokapi_execute_code --- mcp/run.go | 2 +- mcp/run_test.go | 16 ++++++++++ mcp/server_test.go | 78 ++++++++++++++++++++++++++++++++++++++-------- 3 files changed, 82 insertions(+), 14 deletions(-) diff --git a/mcp/run.go b/mcp/run.go index 5fac95871..23d1b89a8 100644 --- a/mcp/run.go +++ b/mcp/run.go @@ -72,7 +72,7 @@ Use this tool to: Prefer this tool over retrieving full API specifications, as it returns only the computed result.`, InputSchema: inputSchema, OutputSchema: outputSchema, - }, s.GenerateHttpMockResponse) + }, s.GetRunResponse) server.AddResource(&mcp.Resource{ URI: "api://execute-types", diff --git a/mcp/run_test.go b/mcp/run_test.go index cdfeebc3e..070a36472 100644 --- a/mcp/run_test.go +++ b/mcp/run_test.go @@ -22,6 +22,22 @@ func TestService_Run(t *testing.T) { app *runtime.App test func(t *testing.T, s *mcp.Service) }{ + { + name: "run JavaScript code", + app: runtimetest.NewHttpApp( + openapitest.NewConfig("3.1.0"), + ), + test: func(t *testing.T, s *mcp.Service) { + r, err := s.GetRunResponse( + context.Background(), + mcp.RunInput{ + Code: `1+1`, + }, + ) + require.NoError(t, err) + require.Equal(t, int64(2), r.Result) + }, + }, { name: "List APIs skip empty name", app: runtimetest.NewHttpApp( diff --git a/mcp/server_test.go b/mcp/server_test.go index edb36b615..1e5057292 100644 --- a/mcp/server_test.go +++ b/mcp/server_test.go @@ -29,17 +29,69 @@ func TestServer(t *testing.T) { require.NotNil(t, session) defer func() { _ = session.Close() }() - list, err := session.ListTools(ctx, &gomcp.ListToolsParams{}) - require.NoError(t, err) - require.Len(t, list.Tools, 9) - // alphabetical order - require.Equal(t, "mokapi_execute_code", list.Tools[0].Name) - require.Equal(t, "mokapi_generate_http_mock_response", list.Tools[1].Name) - require.Equal(t, "mokapi_get_api_spec", list.Tools[2].Name) - require.Equal(t, "mokapi_get_events", list.Tools[3].Name) - require.Equal(t, "mokapi_get_http_mock_template", list.Tools[4].Name) - require.Equal(t, "mokapi_get_scenarios", list.Tools[5].Name) - require.Equal(t, "mokapi_get_typescript_api", list.Tools[6].Name) - require.Equal(t, "mokapi_produce_kafka_message", list.Tools[7].Name) - require.Equal(t, "mokapi_send_http_request", list.Tools[8].Name) + testcases := []struct { + name string + test func(t *testing.T) + }{ + { + name: "test tool list", + test: func(t *testing.T) { + list, err := session.ListTools(ctx, &gomcp.ListToolsParams{}) + require.NoError(t, err) + require.Len(t, list.Tools, 9) + // alphabetical order + require.Equal(t, "mokapi_execute_code", list.Tools[0].Name) + require.Equal(t, "mokapi_generate_http_mock_response", list.Tools[1].Name) + require.Equal(t, "mokapi_get_api_spec", list.Tools[2].Name) + require.Equal(t, "mokapi_get_events", list.Tools[3].Name) + require.Equal(t, "mokapi_get_http_mock_template", list.Tools[4].Name) + require.Equal(t, "mokapi_get_scenarios", list.Tools[5].Name) + require.Equal(t, "mokapi_get_typescript_api", list.Tools[6].Name) + require.Equal(t, "mokapi_produce_kafka_message", list.Tools[7].Name) + require.Equal(t, "mokapi_send_http_request", list.Tools[8].Name) + }, + }, + { + name: "run code", + test: func(t *testing.T) { + r, err := session.CallTool(ctx, &gomcp.CallToolParams{ + Name: "mokapi_execute_code", + Arguments: mcp.RunInput{ + Code: "1+1", + }, + }) + require.NoError(t, err) + require.IsType(t, &gomcp.TextContent{}, r.Content[0]) + tc := r.Content[0].(*gomcp.TextContent) + require.Equal(t, `{"result":2}`, tc.Text) + }, + }, + { + name: "get mokapi_execute_code tool information", + test: func(t *testing.T) { + list, err := session.ListTools(ctx, &gomcp.ListToolsParams{ + Meta: nil, + Cursor: "", + }) + require.NoError(t, err) + require.Contains(t, list.Tools[0].Description, "api://execute-types") + }, + }, + { + name: "get resource execute-types", + test: func(t *testing.T) { + list, err := session.ListResources(ctx, &gomcp.ListResourcesParams{}) + require.NoError(t, err) + require.Equal(t, "api-docs", list.Resources[0].Name) + require.Equal(t, "api://execute-types", list.Resources[0].URI) + }, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + tc.test(t) + }) + } + } From 98f72dcc5f620956eb35a332a156cb0c491d59a4 Mon Sep 17 00:00:00 2001 From: maesi Date: Sun, 19 Apr 2026 23:04:36 +0200 Subject: [PATCH 40/46] wip: also make resources available via tool --- mcp/conditional-response.md | 61 +++++++++ mcp/delay-latency.md | 30 ++++ mcp/dynamic-error-simulation.md | 45 ++++++ mcp/dynamic-path-params.md | 33 +++++ mcp/events.go | 77 +++++++++++ mcp/events_test.go | 108 +++++++++++++++ mcp/forward-request-to-real-backend.md | 70 ++++++++++ mcp/get_automation_definitions.go | 33 +++++ mcp/get_mock_reference.go | 85 ++++++++++++ mcp/get_mokapi_typescript_api.go | 8 ++ mcp/mock_reference.md | 36 +++++ mcp/mocking-types.md | 16 +++ mcp/resources.go | 154 +++++++++++++++++++++ mcp/run.go | 182 +++++++++++++++---------- mcp/run.ts | 89 ++++++++++-- mcp/run_test.go | 103 +++++++++++--- mcp/scenarios.md | 14 ++ mcp/server.go | 22 +-- mcp/server_test.go | 14 +- mcp/static-error-simulation.md | 34 +++++ npm/go-mokapi/types/types.go | 28 ++++ pkg/cmd/mokapi/mokapi.go | 1 + runtime/runtimetest/app.go | 10 ++ 23 files changed, 1134 insertions(+), 119 deletions(-) create mode 100644 mcp/conditional-response.md create mode 100644 mcp/delay-latency.md create mode 100644 mcp/dynamic-error-simulation.md create mode 100644 mcp/dynamic-path-params.md create mode 100644 mcp/events.go create mode 100644 mcp/events_test.go create mode 100644 mcp/forward-request-to-real-backend.md create mode 100644 mcp/get_automation_definitions.go create mode 100644 mcp/get_mock_reference.go create mode 100644 mcp/mock_reference.md create mode 100644 mcp/mocking-types.md create mode 100644 mcp/resources.go create mode 100644 mcp/scenarios.md create mode 100644 mcp/static-error-simulation.md create mode 100644 npm/go-mokapi/types/types.go diff --git a/mcp/conditional-response.md b/mcp/conditional-response.md new file mode 100644 index 000000000..eeb76296f --- /dev/null +++ b/mcp/conditional-response.md @@ -0,0 +1,61 @@ +# Scenario conditional-response + +HTTP mock handler for terminals. +Demonstrates how to: +- Access request parameters +- Apply custom logic (e.g., lookup, filtering, updates) + +```typescript +import { on } from "mokapi" + +interface Terminal { + id: string + compartments: { + id: string + doorState: 'open' | 'closed' + }[] +} + +const terminals: Terminal[] = [] + +export default function () { + on('http', (request, response) => { + switch(request.key) { + case '/terminals/{id}': { + const terminal = terminals.find(x => x.id === request.path.id) + if (!terminal) { + response.rebuild(404) + response.data = { error: 'terminal not found' } + return + } + + if (request.method === 'GET') { + response.data = terminal + } else if (request.method === 'POST') { + // update the terminal + Object.assign(terminal, request.body) + // mokapi already set the success response, nothing to do + } + // do not raise an error if different method is used, + // maybe there is another event handler in a different file defined + return + } + case '/terminals': { + if (request.method === 'GET') { + response.data = terminals + } else if (request.method === 'POST') { + const terminal = terminals.find(x => x.id === request.path.id) + if (terminal) { + // console output will be displayed in the Mokapi's' dashboard + console.log('terminal already exists', request.body) + response.rebuild(400) + } else { + terminals.push(request.body) + } + } + return + } + } + }) +} +``` \ No newline at end of file diff --git a/mcp/delay-latency.md b/mcp/delay-latency.md new file mode 100644 index 000000000..58ad5e69b --- /dev/null +++ b/mcp/delay-latency.md @@ -0,0 +1,30 @@ +# Scenario delay-latency + +Simulate server latency by delaying the response. Useful to test scenarios: +- frontend loading states +- timeouts +- high-load + +```typescript +import { on } from "mokapi" + +let pets = [ + { id: 1, name: 'Fluffy', status: 'available', category: { id: 1, name: 'Dogs' }, photoUrls: [], tags: [] }, + { id: 3, name: 'Hedgie', status: 'pending', category: { id: 2, name: 'Small Animals' }, photoUrls: [], tags: [] } +]; + +export default function () { + on('http', async (request, response) => { + switch(request.key) { + case '/pets': { + if (request.method !== 'GET') return + + // simulate network latency (e.g., 2 seconds) + sleep('2s') + + response.data = pets + } + } + }) +} +``` \ No newline at end of file diff --git a/mcp/dynamic-error-simulation.md b/mcp/dynamic-error-simulation.md new file mode 100644 index 000000000..180f749e1 --- /dev/null +++ b/mcp/dynamic-error-simulation.md @@ -0,0 +1,45 @@ +# Scenario dynamic-error-simulation + +Return error responses based on runtime conditions, such as missing resources, validation failures, or conflicting state. + +```typescript +import { on } from "mokapi" + +const hotels = [] + +export default function () { + on('http', (request, response) => { + switch(request.key) { + case '/bookings': { + const hotel = hotels.find(x => x.code === request.body?.hotel?.code) + + if (!hotel) { + console.log('hotel not found') + response.rebuild(404) + response.data = { error: 'hotel not found' } + return + } + + // simulate dynamic errors based on hotel simulation config + const type = hotel.simulation?.responseType + switch (type) { + case 'bad-request': + response.rebuild(400) + return + case 'unauthorized': + response.rebuild(401) + return + case 'forbidden': + response.rebuild(403) + return + case 'internal-server-error': + response.rebuild(500) + return + } + // success path: generate valid response + // ... + } + } + }) +} +``` \ No newline at end of file diff --git a/mcp/dynamic-path-params.md b/mcp/dynamic-path-params.md new file mode 100644 index 000000000..7d1e0b162 --- /dev/null +++ b/mcp/dynamic-path-params.md @@ -0,0 +1,33 @@ +# Scenario dynamic-path-params + +HTTP mock handler to get a pet stored in an array list. +Demonstrates how to: +- Access request parameters +- Apply custom logic (e.g., lookup, filtering) + +```typescript +import { on } from "mokapi" + +let pets = [ + { id: 1, name: 'Fluffy', status: 'available', category: { id: 1, name: 'Dogs' }, photoUrls: [], tags: [] }, + { id: 3, name: 'Hedgie', status: 'pending', category: { id: 2, name: 'Small Animals' }, photoUrls: [], tags: [] } +]; + +export default function () { + on('http', async (request, response) => { + switch(request.key) { + case '/pets/{id}': + if (request.method !== 'GET') { + return + } + const pet = pets.find(x => x.id === request.path.id) + if (pet) { + response.data = pet + } else { + console.log('pet not found', request) + response.rebuild(404) + } + } + }) +} +``` \ No newline at end of file diff --git a/mcp/events.go b/mcp/events.go new file mode 100644 index 000000000..f926f4b23 --- /dev/null +++ b/mcp/events.go @@ -0,0 +1,77 @@ +package mcp + +import ( + "fmt" + "mokapi/js/util" + "mokapi/runtime/events" + "reflect" + "strings" + + "github.com/dop251/goja" +) + +func (m *mokapi) getEvents(vTraits goja.Value, vLimit goja.Value) ([]events.Event, error) { + traits, err := parseTraits(vTraits, m.vm) + if err != nil { + return nil, err + } + + evts := m.app.Events.GetEvents(traits) + + limit := 10 + if vLimit != nil { + if vLimit.ExportType().Kind() != reflect.Float64 { + return nil, fmt.Errorf("unexpected type for apiType: %s", util.JsType(vLimit.ExportType())) + } + limit = int(vLimit.ToInteger()) + } + if len(evts) > limit { + return evts[0:limit], nil + } else { + return evts, nil + } +} + +func parseTraits(v goja.Value, vm *goja.Runtime) (events.Traits, error) { + traits := events.Traits{} + + if v == nil { + return traits, nil + } + + if v.ExportType().Kind() != reflect.Map { + return nil, fmt.Errorf("expect object but got: %v", util.JsType(v.Export())) + } + + obj := v.ToObject(vm) + for _, k := range obj.Keys() { + switch k { + case "type": + val := obj.Get(k) + if val.ExportType().Kind() != reflect.String { + return nil, fmt.Errorf("unexpected type for type: %s", util.JsType(val.ExportType())) + } + traits.WithNamespace(val.String()) + case "name": + val := obj.Get(k) + if val.ExportType().Kind() != reflect.String { + return nil, fmt.Errorf("unexpected type for name: %s", util.JsType(val.ExportType())) + } + traits.WithName(val.String()) + case "method": + val := obj.Get(k) + if val.ExportType().Kind() != reflect.String { + return nil, fmt.Errorf("unexpected type for method: %s", util.JsType(val.ExportType())) + } + traits.With("method", strings.ToUpper(val.String())) + default: + val := obj.Get(k) + if val.ExportType().Kind() != reflect.String { + return nil, fmt.Errorf("unexpected type for %s: %s", k, util.JsType(val.ExportType())) + } + traits.With(k, val.String()) + } + } + + return traits, nil +} diff --git a/mcp/events_test.go b/mcp/events_test.go new file mode 100644 index 000000000..c1f222439 --- /dev/null +++ b/mcp/events_test.go @@ -0,0 +1,108 @@ +package mcp_test + +import ( + "context" + "mokapi/mcp" + "mokapi/runtime" + "mokapi/runtime/events" + "mokapi/runtime/runtimetest" + "testing" + + "github.com/stretchr/testify/require" +) + +type testEvent struct { + Name string +} + +func (t *testEvent) Title() string { + return t.Name +} + +func TestEvents(t *testing.T) { + testcases := []struct { + name string + app *runtime.App + code string + test func(t *testing.T, evts []events.Event, err error) + }{ + { + name: "without params should not error", + code: "mokapi.getEvents()", + app: runtimetest.NewApp( + runtimetest.WithEvent(events.NewTraits().WithNamespace("http"), &testEvent{Name: "test-1"}), + ), + test: func(t *testing.T, evts []events.Event, err error) { + require.NoError(t, err) + require.Len(t, evts, 1) + require.Equal(t, &testEvent{Name: "test-1"}, evts[0].Data) + }, + }, + { + name: "filter by API type", + code: "mokapi.getEvents({ type: 'http' })", + app: runtimetest.NewApp( + runtimetest.WithEvent(events.NewTraits().WithNamespace("kafka"), &testEvent{Name: "test-1"}), + runtimetest.WithEvent(events.NewTraits().WithNamespace("http"), &testEvent{Name: "test-2"}), + ), + test: func(t *testing.T, evts []events.Event, err error) { + require.NoError(t, err) + require.Len(t, evts, 1) + require.Equal(t, &testEvent{Name: "test-2"}, evts[0].Data) + }, + }, + { + name: "filter by API name", + code: "mokapi.getEvents({ name: 'bar' })", + app: runtimetest.NewApp( + runtimetest.WithEvent(events.NewTraits().WithNamespace("http").WithName("foo"), &testEvent{Name: "test-1"}), + runtimetest.WithEvent(events.NewTraits().WithNamespace("http").WithName("bar"), &testEvent{Name: "test-2"}), + ), + test: func(t *testing.T, evts []events.Event, err error) { + require.NoError(t, err) + require.Len(t, evts, 1) + require.Equal(t, &testEvent{Name: "test-2"}, evts[0].Data) + }, + }, + { + name: "filter by path", + code: "mokapi.getEvents({ path: '/pets' })", + app: runtimetest.NewApp( + runtimetest.WithEvent(events.NewTraits().WithNamespace("http").With("path", "/users"), &testEvent{Name: "test-1"}), + runtimetest.WithEvent(events.NewTraits().WithNamespace("http").With("path", "/pets"), &testEvent{Name: "test-2"}), + ), + test: func(t *testing.T, evts []events.Event, err error) { + require.NoError(t, err) + require.Len(t, evts, 1) + require.Equal(t, &testEvent{Name: "test-2"}, evts[0].Data) + }, + }, + { + name: "filter by method", + code: "mokapi.getEvents({ method: 'post' })", + app: runtimetest.NewApp( + runtimetest.WithEvent(events.NewTraits().WithNamespace("http").With("method", "GET"), &testEvent{Name: "test-1"}), + runtimetest.WithEvent(events.NewTraits().WithNamespace("http").With("method", "POST"), &testEvent{Name: "test-2"}), + ), + test: func(t *testing.T, evts []events.Event, err error) { + require.NoError(t, err) + require.Len(t, evts, 1) + require.Equal(t, &testEvent{Name: "test-2"}, evts[0].Data) + }, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + s := mcp.NewService(tc.app) + + r, err := s.GetRunResponse( + context.Background(), + mcp.RunInput{Code: tc.code}, + ) + require.IsType(t, []events.Event{}, r.Result) + + tc.test(t, r.Result.([]events.Event), err) + }) + } +} diff --git a/mcp/forward-request-to-real-backend.md b/mcp/forward-request-to-real-backend.md new file mode 100644 index 000000000..5fef825eb --- /dev/null +++ b/mcp/forward-request-to-real-backend.md @@ -0,0 +1,70 @@ +# Scenario forward-request-to-real-backend + +Stop API drift in its tracks. Use Mokapi as a validation layer to enforce OpenAPI contracts between +clients and backends, regardless of who's calling or what they're building. This scenario forwards +incoming requests to real backend services while validating both requests and responses against the +OpenAPI specification. + +```typescript +import { on } from 'mokapi'; +import { fetch } from 'mokapi/http'; + +export default async function () { + + on('http', async (request, response) => { + + // Map request to backend URL based on OpenAPI spec name + const url = getForwardUrl(request) + + // If no URL could be determined, return an error immediately + if (!url) { + response.statusCode = 500; + response.body = 'Failed to forward request: unknown backend'; + return; + } + + try { + // Forward the request to the backend + const res = await fetch(url, { + method: request.method, + body: request.body, + headers: request.header, + timeout: '30s' + }); + + // Copy status code and headers + response.statusCode = res.statusCode; + response.headers = res.headers + + // Check the content type to decide whether to validate the response + const contentType = res.headers['Content-Type']?.[0] || ''; + + if (contentType.includes('application/json')) { + // Mokapi can validate JSON responses automatically + response.data = res.json(); + } else { + // For other content types, skip validation + response.body = res.body; + } + + } catch (e) { + // Handle any errors that occur while forwarding + response.statusCode = 500; + response.body = e.toString(); + } + }); + + function getForwardUrl(request: HttpRequest): string | undefined { + switch (request.api) { + case 'backend-1': { + return 'https://backend1.example.com' + request.url.path + '?' + request.url.query; + } + case 'backend-2': { + return 'https://backend2.example.com' + request.url.path + '?' + request.url.query; + } + default: + return undefined; + } + } +} +``` \ No newline at end of file diff --git a/mcp/get_automation_definitions.go b/mcp/get_automation_definitions.go new file mode 100644 index 000000000..efdcc7872 --- /dev/null +++ b/mcp/get_automation_definitions.go @@ -0,0 +1,33 @@ +package mcp + +import ( + "context" + _ "embed" + + "github.com/modelcontextprotocol/go-sdk/mcp" +) + +//go:embed run.ts +var automationTypes string + +type AutomationDefinitions struct { + Code string +} + +func (s *Service) registerGetAutomationDefinitions(server *mcp.Server) { + registerTool(server, &mcp.Tool{ + Name: "mokapi_get_automation_definitions", + Description: `Returns the required TypeScript definitions for the Mokapi Automation API. + +MANDATORY: Call this tool BEFORE using 'mokapi_execute_code' to: +- Learn how to query API specifications (OpenAPI, AsyncAPI) +- Access methods for inspecting live logs and events +- Get correct syntax for the global 'mokapi' object + +This ensures your generated code is valid and uses the correct library methods.`, + }, s.GetAutomationDefinitions) +} + +func (s *Service) GetAutomationDefinitions(_ context.Context, _ any) (AutomationDefinitions, error) { + return AutomationDefinitions{Code: automationTypes}, nil +} diff --git a/mcp/get_mock_reference.go b/mcp/get_mock_reference.go new file mode 100644 index 000000000..a9e3fb280 --- /dev/null +++ b/mcp/get_mock_reference.go @@ -0,0 +1,85 @@ +package mcp + +import ( + "context" + _ "embed" + "fmt" + + "github.com/modelcontextprotocol/go-sdk/mcp" +) + +//go:embed mock_reference.md +var mockReference string + +type GetMockReferenceInput struct { + Category string `json:"category"` + Name string `json:"name"` +} + +type GetMockReferenceOutput struct { + Category string `json:"category"` + Name string `json:"name"` + Text string +} + +func (s *Service) registerGetMockReference(server *mcp.Server) { + inputSchema := map[string]any{ + "type": "object", + "properties": map[string]any{ + "category": map[string]any{ + "type": "string", + "description": "The category of reference material to retrieve.", + "enum": []string{"types", "scenarios"}, + }, + "name": map[string]interface{}{ + "type": "string", + "description": "The specific library or scenario name (e.g., 'http', 'kafka', 'delay-latency').", + "enum": []string{ + // Types + "mokapi", "http", "kafka", "faker", "mustache", "yaml", + // Scenarios + "dynamic-path-params", + "conditional-response", + "static-error-simulation", + "dynamic-error-simulation", + "delay-latency", + "forward-request-to-real-backend", + }, + }, + }, + "required": []string{}, + } + + registerTool(server, &mcp.Tool{ + Name: "mokapi_get_mock_reference", + Description: `Retrieves technical reference material for writing mock scripts. +You can request either 'types' (API definitions) or 'scenarios' (example blueprints). + +Use this tool to: +- See how to import and use 'mokapi', 'mokapi/http', or 'mokapi/kafka'. +- Get boilerplate code for specific use cases (e.g., 'rest-auth'). + +MANDATORY: Use this before generating a new mock script to ensure correct syntax.`, + InputSchema: inputSchema, + }, s.GetMockReference) +} + +func (s *Service) GetMockReference(_ context.Context, in *GetMockReferenceInput) (GetMockReferenceOutput, error) { + if in.Name == "" || in.Category == "" { + return GetMockReferenceOutput{Text: mockReference}, nil + } + + if in.Category == "types" { + text, ok := mockTypes[in.Name] + if !ok { + return GetMockReferenceOutput{}, fmt.Errorf("mock reference type not found: %s", in.Name) + } + return GetMockReferenceOutput{Text: text, Category: in.Category, Name: in.Name}, nil + } + + text, ok := scenarios[in.Name] + if !ok { + return GetMockReferenceOutput{}, fmt.Errorf("mock reference scenario not found: %s", in.Name) + } + return GetMockReferenceOutput{Text: text, Category: in.Category, Name: in.Name}, nil +} diff --git a/mcp/get_mokapi_typescript_api.go b/mcp/get_mokapi_typescript_api.go index fd64162b5..6bd876e5b 100644 --- a/mcp/get_mokapi_typescript_api.go +++ b/mcp/get_mokapi_typescript_api.go @@ -2,6 +2,8 @@ package mcp import ( "context" + _ "embed" + "strings" "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -20,7 +22,13 @@ type Package struct { Types string `json:"types"` } +func extractName(uri string) string { + parts := strings.Split(uri, "/") + return parts[len(parts)-1] +} + func (s *Service) registerGetMokapiTypeScriptApi(server *mcp.Server) { + inputSchema := map[string]any{ "type": "object", "properties": map[string]any{ diff --git a/mcp/mock_reference.md b/mcp/mock_reference.md new file mode 100644 index 000000000..7f29d5f8a --- /dev/null +++ b/mcp/mock_reference.md @@ -0,0 +1,36 @@ +# Mock Reference + +Retrieves technical reference material for writing mock scripts. +You can request either 'types' (API definitions) or 'scenarios' (example blueprints). + +To read a specific definition or scenario, call this tool again +with the corresponding category and name (e.g., category='types', name='http') + +## Types + +Overview of TypeScript definitions for mock event handlers. +- Use these types to ensure correct syntax for "import { ... } from 'mokapi/...'". + +| Type | Import Statement | Parameter Name | +|--------------------------------|---------------------------------------|----------------| +| **Core** | `import {...} from 'mokapi'` | mokapi | +| **HTTP** | `import {...} from 'mokapi/http'` | http | +| **Kafka** | `import {...} from 'mokapi/kafka'` | kafka | +| **Faker** | `import {...} from 'mokapi/faker'` | faker | +| **Mustache** | `import {...} from 'mokapi/mustache'` | mustache | +| **Yaml** | `import {...} from 'mokapi/yaml'` | yaml | + +## Scenarios + +A list of code examples and boilerplates. +Use these to understand how to implement specific use cases like latency, error simulation or conditional behavior. +Always check a scenario before writing a script from scratch to ensure you follow best practices. + +| Scenario | Description | +|---------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| dynamic-path-params | Access and use path parameters (e.g., /pets/{petId}) to retrieve or process specific resources based on request.path values. | +| conditional-response | Return different responses based on request data (path, query, headers, or body), such as selecting resources, updating state, or handling different HTTP methods. | +| static-error-simulation | Return predefined error responses (e.g., 400, 404, 500) for specific endpoints or conditions without dynamic logic. | +| dynamic-error-simulation | Return error responses based on runtime conditions, such as missing resources, validation failures, or conflicting state. | +| delay-latency | Simulate network latency or slow backend processing by delaying the response before returning data or errors. | +| forward-request-to-real-backend | Stop API drift in its tracks. Use Mokapi as a validation layer to enforce OpenAPI contracts between clients and backends, regardless of who's calling or what they're building. This scenario forwards incoming requests to real backend services while validating both requests and responses against the OpenAPI specification. | \ No newline at end of file diff --git a/mcp/mocking-types.md b/mcp/mocking-types.md new file mode 100644 index 000000000..e30ce14ca --- /dev/null +++ b/mcp/mocking-types.md @@ -0,0 +1,16 @@ +# Available Mocking Libraries + +Overview of TypeScript definitions for mock event handlers. +Provides URIs to technical definitions (d.ts) for 'mokapi', 'http', 'kafka', etc. +- Use these types to ensure correct syntax for "import { ... } from 'mokapi/...'". +- Refer to mokapi://lib/mocking/scenarios for boilerplate examples and usage patterns. + Mandatory for generating valid mock scripts. + +| Library | Resource URI | Import Statement | +|--------------|---------------------------------------|---------------------------------------| +| **Core** | `mokapi://lib/mocking/types/mokapi` | `import {...} from 'mokapi'` | +| **HTTP** | `mokapi://lib/mocking/types/http` | `import {...} from 'mokapi/http'` | +| **Kafka** | `mokapi://lib/mocking/types/kafka` | `import {...} from 'mokapi/kafka'` | +| **Faker** | `mokapi://lib/mocking/types/faker` | `import {...} from 'mokapi/faker'` | +| **Mustache** | `mokapi://lib/mocking/types/mustache` | `import {...} from 'mokapi/mustache'` | +| **Yaml** | `mokapi://lib/mocking/types/yaml` | `import {...} from 'mokapi/yaml'` | \ No newline at end of file diff --git a/mcp/resources.go b/mcp/resources.go new file mode 100644 index 000000000..b53c70a85 --- /dev/null +++ b/mcp/resources.go @@ -0,0 +1,154 @@ +package mcp + +import ( + "context" + _ "embed" + "mokapi/npm/go-mokapi/types" + + "github.com/modelcontextprotocol/go-sdk/mcp" +) + +//go:embed mocking-types.md +var overview string + +var mockTypes = map[string]string{ + "mokapi": types.Mokapi, + "faker": types.Faker, + "http": types.Http, + "kafka": types.Kafka, + "mustache": types.Mustache, + "yaml": types.Yaml, +} + +func addResources(server *mcp.Server) { + server.AddResource(&mcp.Resource{ + URI: "mokapi://lib/automation", + Name: "Automation API Reference", + Description: "Types for querying Mokapi specs, logs, and events via Code Mode", + }, func(ctx context.Context, request *mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) { + return &mcp.ReadResourceResult{ + Contents: []*mcp.ResourceContents{ + { + URI: "mokapi://lib/automation", + MIMEType: "application/typescript", + Text: automationTypes, + }, + }, + }, nil + }) + + server.AddResource(&mcp.Resource{ + URI: "mokapi://lib/mocking/types", + Name: "Mokapi Script API Overview", + Description: `Overview of TypeScript definitions for mock event handlers. +Provides URIs to technical definitions (d.ts) for 'mokapi', 'http', 'kafka', etc. +- Use these types to ensure correct syntax for 'import { ... } from "mokapi/..."'. +- Refer to mokapi://lib/mocking/scenarios for boilerplate examples and usage patterns. +Mandatory for generating valid mock scripts.`, + }, func(ctx context.Context, request *mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) { + return &mcp.ReadResourceResult{ + Contents: []*mcp.ResourceContents{ + { + URI: "mokapi://lib/mocking/types", + MIMEType: "text/markdown", + Text: overview, + }, + }, + }, nil + }) + + server.AddResourceTemplate(&mcp.ResourceTemplate{ + URITemplate: "mokapi://lib/mocking/types/{name}", + Name: "Mocking Script API Reference", + Description: `Types for writing event handlers and mock logic inside Mokapi. +Use mokapi://lib/mocking/types to get an overview of all available APIs`, + }, func(ctx context.Context, request *mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) { + name := extractName(request.Params.URI) + text, ok := mockTypes[name] + if !ok { + return nil, mcp.ResourceNotFoundError(request.Params.URI) + } + return &mcp.ReadResourceResult{ + Contents: []*mcp.ResourceContents{ + { + URI: "mokapi://lib/mocking/types/" + name, + MIMEType: "application/typescript", + Text: text, + }, + }, + }, nil + }) + + addMockScenarios(server) +} + +//go:embed scenarios.md +var scenarioOverview string + +//go:embed dynamic-path-params.md +var dynamicPathParams string + +//go:embed conditional-response.md +var conditionalResponse string + +//go:embed static-error-simulation.md +var staticErrorSimulation string + +//go:embed dynamic-error-simulation.md +var dynamicErrorSimulation string + +//go:embed delay-latency.md +var delayLatency string + +//go:embed forward-request-to-real-backend.md +var forwardRequestToRealBackend string + +var scenarios = map[string]string{ + "dynamic-path-params": dynamicPathParams, + "conditional-response": conditionalResponse, + "static-error-simulation": staticErrorSimulation, + "dynamic-error-simulation": dynamicErrorSimulation, + "delay-latency": delayLatency, + "forward-request-to-real-backend": forwardRequestToRealBackend, +} + +func addMockScenarios(server *mcp.Server) { + server.AddResource(&mcp.Resource{ + URI: "mokapi://lib/mocking/scenarios", + Name: "Mokapi Script Blueprints & Scenarios", + Description: `A directory of ready-to-use code examples and boilerplates. +Use these to understand how to implement specific use cases like latency, error simulation or conditional behavior. +Always check a scenario before writing a script from scratch to ensure you follow best practices.`, + }, func(ctx context.Context, request *mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) { + return &mcp.ReadResourceResult{ + Contents: []*mcp.ResourceContents{ + { + URI: "mokapi://lib/mocking/scenarios", + MIMEType: "text/markdown", + Text: scenarioOverview, + }, + }, + }, nil + }) + + server.AddResourceTemplate(&mcp.ResourceTemplate{ + URITemplate: "mokapi://lib/mocking/scenarios/{name}", + Name: "Mocking Scenario Detail", + Description: "Full script template for a specific scenario. Use mokapi://lib/mocking/scenarios get an overview of all scenarios", + }, func(ctx context.Context, request *mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) { + name := extractName(request.Params.URI) + text, ok := scenarios[name] + if !ok { + return nil, mcp.ResourceNotFoundError(request.Params.URI) + } + return &mcp.ReadResourceResult{ + Contents: []*mcp.ResourceContents{ + { + URI: "mokapi://lib/mocking/scenarios/" + name, + MIMEType: "text/markdown", + Text: text, + }, + }, + }, nil + }) +} diff --git a/mcp/run.go b/mcp/run.go index 23d1b89a8..7c4872b61 100644 --- a/mcp/run.go +++ b/mcp/run.go @@ -3,8 +3,10 @@ package mcp import ( "context" _ "embed" + "errors" "fmt" "io" + "mokapi/js/compiler" "mokapi/js/faker" "mokapi/providers/openapi" "mokapi/providers/openapi/schema" @@ -21,9 +23,6 @@ import ( log "github.com/sirupsen/logrus" ) -//go:embed run.ts -var types string - type RunInput struct { Code string `json:"code"` } @@ -60,34 +59,24 @@ func (s *Service) registerRunTool(server *mcp.Server) { Description: `Executes JavaScript code in a sandboxed Mokapi runtime. The last expression in the code is returned as the result. -Important: -Before writing any code, be sure to read the API definitions at api://execute-types to understand -the available global objects, functions, and types. +MANDATORY WORKFLOW: +1. FIRST: Call 'mokapi_get_automation_definitions' to get the latest API types. +2. SECOND: Use this tool to query live API data (endpoints, schemas, events). +NEVER guess the API structure; always use the definitions as a reference. + +Important for Object Returns: +JavaScript interprets {} at the start of a line as a block, not an object. To return an object literal, wrap it in parentheses ({ ... }) or assign it to a variable and put the variable name in the last line. +Example: const result = { a: 1 }; result + Use this tool to: - Explore mocked APIs (OpenAPI, AsyncAPI, LDAP, Mail) - Inspect operations and schemas -- Invoke API operations directly - -Prefer this tool over retrieving full API specifications, as it returns only the computed result.`, +- Invoke API operations directly`, InputSchema: inputSchema, OutputSchema: outputSchema, }, s.GetRunResponse) - server.AddResource(&mcp.Resource{ - URI: "api://execute-types", - Name: "api-docs", - }, func(ctx context.Context, request *mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) { - return &mcp.ReadResourceResult{ - Contents: []*mcp.ResourceContents{ - { - URI: "api://types", - MIMEType: "application/typescript", - Text: types, - }, - }, - }, nil - }) } func (s *Service) GetRunResponse(_ context.Context, in RunInput) (RunOutput, error) { @@ -101,24 +90,34 @@ func (s *Service) GetRunResponse(_ context.Context, in RunInput) (RunOutput, err } type mokapi struct { - app *runtime.App - vm *goja.Runtime + app *runtime.App + vm *goja.Runtime + compiler *compiler.Compiler } func newMokapi(app *runtime.App) *mokapi { vm := goja.New() vm.SetFieldNameMapper(&customFieldNameMapper{}) - return &mokapi{app: app, vm: vm} + c, _ := compiler.New() + return &mokapi{app: app, vm: vm, compiler: c} } func (m *mokapi) run(code string) (any, error) { obj := m.vm.NewObject() m.init(obj) _ = m.vm.Set("mokapi", obj) - v, err := m.vm.RunString(code) + p, err := m.compiler.Compile("mokapi_execute_code.js", code) if err != nil { return nil, err } + v, err := m.vm.RunProgram(p) + if err != nil { + var ex *goja.Exception + if errors.As(err, &ex) { + return nil, ex.Unwrap() + } + return nil, err + } return v.Export(), nil } @@ -131,6 +130,7 @@ func (m *mokapi) init(obj *goja.Object) { _ = obj.Set("getApis", m.getApis) _ = obj.Set("getApi", m.getApi) _ = obj.Set("fake", m.fake) + _ = obj.Set("getEvents", m.getEvents) } func (m *mokapi) getApis() []ApiSummary { @@ -182,12 +182,14 @@ type OpenAPI struct { } type OperationSummary struct { - Method string `json:"method"` - Path string `json:"path"` - Summary string `json:"summary"` + Id string `json:"id"` + Method string `json:"method"` + Path string `json:"path"` + Summary string `json:"summary"` + Parameters []string `json:"parameters"` } -type OperationDetails struct { +type Operation struct { OperationId string `json:"operationId"` Method string `json:"method"` Path string `json:"path"` @@ -232,15 +234,29 @@ func (o *OpenAPI) GetOperations() []OperationSummary { continue } for method, op := range p.Value.Operations() { - summary := op.Summary - if summary == "" { - summary = p.Value.Summary - } - result = append(result, OperationSummary{ + os := OperationSummary{ + Id: getOperationId(method, op), Method: method, Path: p.Value.Path, - Summary: summary, + Summary: op.Summary, + } + + if os.Summary == "" { + os.Summary = p.Value.Summary + } + + params := append(op.Path.Parameters, op.Parameters...) + for _, param := range params { + if param.Value == nil { + continue + } + os.Parameters = append(os.Parameters, param.Value.Name) + } + slices.SortStableFunc(os.Parameters, func(a, b string) int { + return strings.Compare(a, b) }) + + result = append(result, os) } } @@ -255,54 +271,62 @@ func (o *OpenAPI) GetOperations() []OperationSummary { return result } -func (o *OpenAPI) GetOperationDetails(path, method string) *OperationDetails { +func (o *OpenAPI) GetOperation(id string) (*Operation, error) { for _, p := range o.info.Paths { - if p.Value == nil || p.Value.Path != path { - continue - } - op := p.Value.Operation(method) - if op == nil { + if p.Value == nil { continue } - r := &OperationDetails{ - OperationId: op.OperationId, - Method: method, - Path: p.Value.Path, - Summary: op.Summary, - Description: op.Description, - spec: op, - handler: o.handler, - } - for _, param := range op.Parameters { - if param.Value == nil { + for method, op := range p.Value.Operations() { + + operationId := getOperationId(method, op) + if id != operationId { continue } - r.Parameters = append(r.Parameters, RequestParameters{ - Name: param.Value.Name, - In: param.Value.Type.String(), - Required: param.Value.Required, - Schema: param.Value.Schema, - Description: param.Value.Description, - }) - } - if op.RequestBody != nil && op.RequestBody.Value != nil { - r.RequestBody = RequestBody{ - Description: op.RequestBody.Value.Description, - Required: op.RequestBody.Value.Required, + + r := &Operation{ + OperationId: operationId, + Method: method, + Path: p.Value.Path, + Summary: op.Summary, + Description: op.Description, + spec: op, + handler: o.handler, } - for ct, content := range op.RequestBody.Value.Content { - r.RequestBody.Contents = append(r.RequestBody.Contents, Content{ - ContentType: ct, - Schema: content.Schema, + for _, param := range op.Parameters { + if param.Value == nil { + continue + } + r.Parameters = append(r.Parameters, RequestParameters{ + Name: param.Value.Name, + In: param.Value.Type.String(), + Required: param.Value.Required, + Schema: param.Value.Schema, + Description: param.Value.Description, }) } + slices.SortStableFunc(r.Parameters, func(a, b RequestParameters) int { + return strings.Compare(a.Name, b.Name) + }) + + if op.RequestBody != nil && op.RequestBody.Value != nil { + r.RequestBody = RequestBody{ + Description: op.RequestBody.Value.Description, + Required: op.RequestBody.Value.Required, + } + for ct, content := range op.RequestBody.Value.Content { + r.RequestBody.Contents = append(r.RequestBody.Contents, Content{ + ContentType: ct, + Schema: content.Schema, + }) + } + } + return r, nil } - return r } - return nil + return nil, fmt.Errorf("operation with ID '%s' not found. Hint: Use getOperations() to see the full list of valid IDs", id) } -func (op *OperationDetails) GetResponseSchema(statusCode int) *Response { +func (op *Operation) GetResponseSchema(statusCode int) *Response { r := op.spec.Responses.GetResponse(statusCode) if r == nil { return nil @@ -333,7 +357,7 @@ type InvokeResponse struct { Body string `json:"body"` } -func (op *OperationDetails) Invoke(req InvokeRequest) (InvokeResponse, error) { +func (op *Operation) Invoke(req InvokeRequest) (InvokeResponse, error) { result := InvokeResponse{Headers: make(map[string][]string)} var body io.Reader @@ -444,3 +468,13 @@ func (cfm customFieldNameMapper) MethodName(_ reflect.Type, m reflect.Method) st func uncapitalize(s string) string { return strings.ToLower(s[0:1]) + s[1:] } + +func getOperationId(method string, op *openapi.Operation) string { + if op == nil { + return "" + } + if op.OperationId != "" { + return op.OperationId + } + return strings.ToLower(fmt.Sprintf("%s-%s", method, op.Path.Path)) +} diff --git a/mcp/run.ts b/mcp/run.ts index a97768449..30cb38fef 100644 --- a/mcp/run.ts +++ b/mcp/run.ts @@ -8,20 +8,40 @@ interface Mokapi { /** * Returns a specific API by name. + * @example + * getApi('Swagger Petstore') */ getApi(name: string): Api; /** * Generate a random value from a JSON Schema. + * The generated data strictly matches the schema, including all required fields and correct types. + * Use this function to create complex random data or writing HTTP mock scripts + * @param schema The JSON scheme for which a random value is generated. * @example * fake({ type: 'string', format: 'email' }) */ fake(schema: Schema): any; + + /** + * Returns recorded events from Mokapi + * Use this function when the user asks: + * - What requests were made? + * - Why did my request fail? + * - Show recent API activity + * @param traits Filter events by traits + * @param limit Maximum number of events to return, default is 10 + * @example + * getEvents({ apiType: 'http' }) + */ + getEvents(traits: HttpTraits, limit?: number): Event[]; } +type ApiType = 'http' | 'kafka' | 'ldap' | 'mail' + interface ApiSummary { name: string; - type: 'openapi' | 'asyncapi' | 'ldap' | 'mail'; + type: ApiType; } interface Api extends ApiSummary { @@ -30,24 +50,32 @@ interface Api extends ApiSummary { interface OpenApi { /** - * Returns operations of this API. - * By default, only minimal fields are returned. + * Returns all operations of this API. */ getOperations(): OperationSummary[]; - getOperationDetails(path: string, method: string): OperationDetails + /** + * Returns details about specific operation + * Use id from the operation summary list + * @param id The id of the operation + */ + getOperation(id: string): OperationDetail } interface OperationSummary { + /** generated from method and path if missing in spec */ + id: string method: string; path: string; summary: string; + /** Names of required parameters to help decide if this is the right endpoint */ + parameters?: string[] } -interface OperationDetails { - operationId: string; - method: string +interface OperationDetail { + id: string; path: string + method: string summary: string description: string; parameters: RequestParameter[] @@ -55,6 +83,7 @@ interface OperationDetails { /** * Invoke this operation against the mocked API. + * @example operation.invoke({ path: { id: 1 }, body: JSON.stringify({ name: "test" }) }) */ invoke(request?: InvokeRequest): InvokeResponse; @@ -99,7 +128,7 @@ interface InvokeRequest { interface InvokeResponse { statusCode: number; headers: Record; - body?: string; + body: string } /** @@ -249,4 +278,46 @@ interface Schema { oneOf?: Schema[]; } -type SchemaType = "object" | "array" | "number" | "integer" | "string" | "boolean" | "null"; \ No newline at end of file +type SchemaType = "object" | "array" | "number" | "integer" | "string" | "boolean" | "null"; + +interface Traits { + type: ApiType; + name: string; +} + +interface HttpTraits extends Traits { + /** + * Path value specified by the OpenAPI path + * @example /pet/{petId} + */ + path: string + + /** + * Request method. + * @example GET + */ + method: string +} + +interface Event { + /** + * ID of the event + */ + id: string; + + /** + * List of traits + */ + traits: Traits; + + /** + * The data of the event + */ + data: any + + /** + * Time of the event in the format RFC3339 + * @example 2026-07-21T17:32:28Z + */ + time: string +} \ No newline at end of file diff --git a/mcp/run_test.go b/mcp/run_test.go index 070a36472..7bbf98d5a 100644 --- a/mcp/run_test.go +++ b/mcp/run_test.go @@ -24,9 +24,7 @@ func TestService_Run(t *testing.T) { }{ { name: "run JavaScript code", - app: runtimetest.NewHttpApp( - openapitest.NewConfig("3.1.0"), - ), + app: runtimetest.NewApp(), test: func(t *testing.T, s *mcp.Service) { r, err := s.GetRunResponse( context.Background(), @@ -38,6 +36,20 @@ func TestService_Run(t *testing.T) { require.Equal(t, int64(2), r.Result) }, }, + { + name: "JSON.parse()", + app: runtimetest.NewApp(), + test: func(t *testing.T, s *mcp.Service) { + r, err := s.GetRunResponse( + context.Background(), + mcp.RunInput{ + Code: `JSON.parse('{"foo":"bar"}')`, + }, + ) + require.NoError(t, err) + require.Equal(t, map[string]any{"foo": "bar"}, r.Result) + }, + }, { name: "List APIs skip empty name", app: runtimetest.NewHttpApp( @@ -108,6 +120,7 @@ func TestService_Run(t *testing.T) { openapitest.WithInfo("foo", "", ""), openapitest.WithPath("/pets", openapitest.WithOperation(http.MethodGet, + openapitest.WithOperationId("pets"), openapitest.WithOperationSummary("GET summary"), ), openapitest.WithOperation(http.MethodPut, @@ -116,7 +129,8 @@ func TestService_Run(t *testing.T) { ), openapitest.WithPath("/users", openapitest.WithPathInfo("path summary", ""), - openapitest.WithOperation(http.MethodPost), + openapitest.WithPathParam("foo"), + openapitest.WithOperation(http.MethodPost, openapitest.WithOperationParam("bar", false)), ), ), ), @@ -129,9 +143,9 @@ func TestService_Run(t *testing.T) { ) require.NoError(t, err) require.Equal(t, []mcp.OperationSummary{ - {Method: "GET", Path: "/pets", Summary: "GET summary"}, - {Method: "PUT", Path: "/pets", Summary: "PUT summary"}, - {Method: "POST", Path: "/users", Summary: "path summary"}}, + {Id: "pets", Method: "GET", Path: "/pets", Summary: "GET summary"}, + {Id: "put-/pets", Method: "PUT", Path: "/pets", Summary: "PUT summary"}, + {Id: "post-/users", Method: "POST", Path: "/users", Summary: "path summary", Parameters: []string{"bar", "foo"}}}, r.Result) }, }, @@ -170,13 +184,13 @@ func TestService_Run(t *testing.T) { r, err := s.GetRunResponse( context.Background(), mcp.RunInput{ - Code: `mokapi.getApi('foo').getOperationDetails('/pets', 'GET')`, + Code: `mokapi.getApi('foo').getOperation('get-/pets')`, }, ) require.NoError(t, err) - require.IsType(t, &mcp.OperationDetails{}, r.Result) - op := r.Result.(*mcp.OperationDetails) - require.Equal(t, "", op.OperationId) + require.IsType(t, &mcp.Operation{}, r.Result) + op := r.Result.(*mcp.Operation) + require.Equal(t, "get-/pets", op.OperationId) require.Equal(t, "GET", op.Method) require.Equal(t, "/pets", op.Path) require.Equal(t, "GET summary", op.Summary) @@ -192,6 +206,49 @@ func TestService_Run(t *testing.T) { require.Equal(t, "string", op.RequestBody.Contents[0].Schema.Type[0]) }, }, + { + name: "get API's operation using projection", + app: runtimetest.NewHttpApp( + openapitest.NewConfig("3.1.0", + openapitest.WithInfo("foo", "", ""), + openapitest.WithPath("/pets", + openapitest.WithOperation(http.MethodGet, + openapitest.WithOperationSummary("GET summary"), + openapitest.WithHeaderParam("foo", false, openapitest.WithParamSchema(schematest.New("string"))), + openapitest.WithRequestBody( + "request body description", + true, + openapitest.WithRequestContent( + "application/json", + &openapi.MediaType{ + Schema: schematest.New("string"), + }, + ), + ), + openapitest.WithResponse(200, + openapitest.WithContent("application/json", openapitest.WithSchema( + schematest.New("string"), + )), + ), + ), + openapitest.WithOperation(http.MethodPut, + openapitest.WithOperationSummary("PUT summary"), + ), + ), + ), + ), + test: func(t *testing.T, s *mcp.Service) { + r, err := s.GetRunResponse( + context.Background(), + mcp.RunInput{ + Code: `const op = mokapi.getApi('foo').getOperation('get-/pets') +const result = { path: op.path, method: op.method }; result`, + }, + ) + require.NoError(t, err) + require.Equal(t, map[string]any{"method": "GET", "path": "/pets"}, r.Result) + }, + }, { name: "getResponseSchema", app: runtimetest.NewHttpApp( @@ -213,7 +270,7 @@ func TestService_Run(t *testing.T) { r, err := s.GetRunResponse( context.Background(), mcp.RunInput{ - Code: `const op = mokapi.getApi('foo').getOperationDetails('/pets', 'GET') + Code: `const op = mokapi.getApi('foo').getOperation('get-/pets') op.getResponseSchema(200)`, }, ) @@ -247,7 +304,7 @@ op.getResponseSchema(200)`, r, err := s.GetRunResponse( context.Background(), mcp.RunInput{ - Code: `const op = mokapi.getApi('foo').getOperationDetails('/pets', 'GET') + Code: `const op = mokapi.getApi('foo').getOperation('get-/pets') op.getResponseSchema(404)`, }, ) @@ -265,7 +322,11 @@ op.getResponseSchema(404)`, openapitest.WithResponse(200, openapitest.WithResponseDescription("response description"), openapitest.WithContent("application/json", openapitest.WithSchema( - schematest.New("string"), + schematest.New("object", + schematest.WithProperty("foo", schematest.New("string")), + schematest.WithProperty("bar", schematest.New("integer")), + schematest.WithRequired("foo", "bar"), + ), )), ), ), @@ -276,7 +337,7 @@ op.getResponseSchema(404)`, r, err := s.GetRunResponse( context.Background(), mcp.RunInput{ - Code: `const op = mokapi.getApi('foo').getOperationDetails('/pets', 'GET'); + Code: `const op = mokapi.getApi('foo').getOperation('get-/pets'); op.invoke()`, }, ) @@ -284,7 +345,7 @@ op.invoke()`, require.IsType(t, mcp.InvokeResponse{}, r.Result) res := r.Result.(mcp.InvokeResponse) require.Equal(t, 200, res.StatusCode) - require.Equal(t, `"P8"`, res.Body) + require.Equal(t, `{"foo":"P8","bar":-804702}`, res.Body) require.Equal(t, map[string][]string{ "Content-Type": {"application/json"}, }, res.Headers) @@ -313,7 +374,7 @@ op.invoke()`, r, err := s.GetRunResponse( context.Background(), mcp.RunInput{ - Code: `const op = mokapi.getApi('foo').getOperationDetails('/{foo}/{bar}/pets', 'GET'); + Code: `const op = mokapi.getApi('foo').getOperation('get-/{foo}/{bar}/pets'); op.invoke({ path: { foo: 'val1', 'bar': 'val2' }})`, }, ) @@ -345,11 +406,11 @@ op.invoke({ path: { foo: 'val1', 'bar': 'val2' }})`, _, err := s.GetRunResponse( context.Background(), mcp.RunInput{ - Code: `const op = mokapi.getApi('foo').getOperationDetails('/{foo}/{bar}/pets', 'GET'); + Code: `const op = mokapi.getApi('foo').getOperation('get-/{foo}/{bar}/pets'); op.invoke()`, }, ) - require.EqualError(t, err, "GoError: invoke request GET /{foo}/{bar}/pets failed: missing path parameter 'foo' at reflect.methodValueCall (native)") + require.EqualError(t, err, "invoke request GET /{foo}/{bar}/pets failed: missing path parameter 'foo'") }, }, { @@ -374,7 +435,7 @@ op.invoke()`, r, err := s.GetRunResponse( context.Background(), mcp.RunInput{ - Code: `const op = mokapi.getApi('foo').getOperationDetails('/pets', 'GET'); + Code: `const op = mokapi.getApi('foo').getOperation('get-/pets'); op.invoke({ query: { foo: 'val1' }})`, }, ) @@ -405,7 +466,7 @@ op.invoke({ query: { foo: 'val1' }})`, r, err := s.GetRunResponse( context.Background(), mcp.RunInput{ - Code: `const op = mokapi.getApi('foo').getOperationDetails('/pets', 'GET'); + Code: `const op = mokapi.getApi('foo').getOperation('get-/pets'); op.invoke({ header: { foo: ['val1'] }})`, }, ) diff --git a/mcp/scenarios.md b/mcp/scenarios.md new file mode 100644 index 000000000..2406fd7b5 --- /dev/null +++ b/mcp/scenarios.md @@ -0,0 +1,14 @@ +# Available Mocking Scenarios + +A directory of ready-to-use code examples and boilerplates. +Use these to understand how to implement specific use cases like latency, error simulation or conditional behavior. +Always check a scenario before writing a script from scratch to ensure you follow best practices. + +| Scenario | Scenario URI | Description | +|----------------------------------|------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| dynamic-path-params | `mokapi://lib/mocking/scenarios/dynamic-path-params` | Access and use path parameters (e.g., /pets/{petId}) to retrieve or process specific resources based on request.path values. | +| conditional-response | `mokapi://lib/mocking/scenarios/conditional-response` | Return different responses based on request data (path, query, headers, or body), such as selecting resources, updating state, or handling different HTTP methods. | +| static-error-simulation | `mokapi://lib/mocking/scenarios/static-error-simulation` | Return predefined error responses (e.g., 400, 404, 500) for specific endpoints or conditions without dynamic logic. | +| dynamic-error-simulation | `mokapi://lib/mocking/scenarios/dynamic-error-simulation` | Return error responses based on runtime conditions, such as missing resources, validation failures, or conflicting state. | +| delay-latency | `mokapi://lib/mocking/scenarios/delay-latency` | Simulate network latency or slow backend processing by delaying the response before returning data or errors. | +| forward-request-to-real-backend | `mokapi://lib/mocking/scenarios/forward-request-to-real-backend` | Stop API drift in its tracks. Use Mokapi as a validation layer to enforce OpenAPI contracts between clients and backends, regardless of who's calling or what they're building. This scenario forwards incoming requests to real backend services while validating both requests and responses against the OpenAPI specification. | \ No newline at end of file diff --git a/mcp/server.go b/mcp/server.go index f7d25261d..1c3f651ce 100644 --- a/mcp/server.go +++ b/mcp/server.go @@ -26,24 +26,28 @@ func NewServer(app *runtime.App) http.Handler { }, nil) svc := NewService(app) - svc.registerGetSpecTool(server) + //svc.registerGetSpecTool(server) - svc.registerGenerateHttpMockResponseTool(server) + //svc.registerGenerateHttpMockResponseTool(server) - svc.registerSendHttpRequest(server) - svc.registerProduceKafkaMessage(server) + //svc.registerSendHttpRequest(server) + //svc.registerProduceKafkaMessage(server) - svc.registerGetEvents(server) + //svc.registerGetEvents(server) - svc.registerGetMokapiTypeScriptApi(server) - svc.registerGetScenarios(server) - svc.registerGetHttpMockTemplate(server) + //svc.registerGetMokapiTypeScriptApi(server) + //svc.registerGetScenarios(server) + //svc.registerGetHttpMockTemplate(server) svc.registerRunTool(server) + svc.registerGetAutomationDefinitions(server) + svc.registerGetMockReference(server) + + addResources(server) return mcp.NewStreamableHTTPHandler( func(*http.Request) *mcp.Server { return server }, - &mcp.StreamableHTTPOptions{}, + &mcp.StreamableHTTPOptions{Stateless: true}, ) } diff --git a/mcp/server_test.go b/mcp/server_test.go index 1e5057292..2365d04af 100644 --- a/mcp/server_test.go +++ b/mcp/server_test.go @@ -38,17 +38,19 @@ func TestServer(t *testing.T) { test: func(t *testing.T) { list, err := session.ListTools(ctx, &gomcp.ListToolsParams{}) require.NoError(t, err) - require.Len(t, list.Tools, 9) + require.Len(t, list.Tools, 3) // alphabetical order require.Equal(t, "mokapi_execute_code", list.Tools[0].Name) - require.Equal(t, "mokapi_generate_http_mock_response", list.Tools[1].Name) + require.Equal(t, "mokapi_get_automation_definitions", list.Tools[1].Name) + require.Equal(t, "mokapi_get_mock_reference", list.Tools[2].Name) + /*require.Equal(t, "mokapi_generate_http_mock_response", list.Tools[1].Name) require.Equal(t, "mokapi_get_api_spec", list.Tools[2].Name) require.Equal(t, "mokapi_get_events", list.Tools[3].Name) require.Equal(t, "mokapi_get_http_mock_template", list.Tools[4].Name) require.Equal(t, "mokapi_get_scenarios", list.Tools[5].Name) require.Equal(t, "mokapi_get_typescript_api", list.Tools[6].Name) require.Equal(t, "mokapi_produce_kafka_message", list.Tools[7].Name) - require.Equal(t, "mokapi_send_http_request", list.Tools[8].Name) + require.Equal(t, "mokapi_send_http_request", list.Tools[8].Name)*/ }, }, { @@ -74,7 +76,7 @@ func TestServer(t *testing.T) { Cursor: "", }) require.NoError(t, err) - require.Contains(t, list.Tools[0].Description, "api://execute-types") + require.Contains(t, list.Tools[0].Description, "mokapi_get_automation_definitions") }, }, { @@ -82,8 +84,8 @@ func TestServer(t *testing.T) { test: func(t *testing.T) { list, err := session.ListResources(ctx, &gomcp.ListResourcesParams{}) require.NoError(t, err) - require.Equal(t, "api-docs", list.Resources[0].Name) - require.Equal(t, "api://execute-types", list.Resources[0].URI) + require.Equal(t, "Automation API Reference", list.Resources[0].Name) + require.Equal(t, "mokapi://lib/automation", list.Resources[0].URI) }, }, } diff --git a/mcp/static-error-simulation.md b/mcp/static-error-simulation.md new file mode 100644 index 000000000..7a5d27b80 --- /dev/null +++ b/mcp/static-error-simulation.md @@ -0,0 +1,34 @@ +# Scenario static-error-simulation + +Return predefined error responses (e.g., 400, 404, 500) for specific endpoints or conditions without dynamic logic. + +```typescript +import { on } from "mokapi" + +export default function () { + on('http', (request, response) => { + switch(request.key) { + case '/bookings': { + if (request.method === 'POST') { + if (request.header['Api-Key'] === 'invalid') { + // console output will be displayed in the Mokapi's' dashboard + console.log('api-key is not valid') + response.rebuild(401) + return + } + if (request.body?.hotel?.code === 'NOT_FOUND') { + console.log('hotel not found') + response.rebuild(404) + return + } + if (request.body.hotel.name === 'INVALID') { + console.log('hotel name is not valid') + response.rebuild(400) + return + } + } + } + } + }) +} +``` \ No newline at end of file diff --git a/npm/go-mokapi/types/types.go b/npm/go-mokapi/types/types.go new file mode 100644 index 000000000..3f462e1c8 --- /dev/null +++ b/npm/go-mokapi/types/types.go @@ -0,0 +1,28 @@ +package types + +import ( + _ "embed" +) + +//go:embed index.d.ts +var index string + +//go:embed global.d.ts +var global string + +var Mokapi = global + "\n" + index + +//go:embed faker.d.ts +var Faker string + +//go:embed http.d.ts +var Http string + +//go:embed kafka.d.ts +var Kafka string + +//go:embed mustache.d.ts +var Mustache string + +//go:embed yaml.d.ts +var Yaml string diff --git a/pkg/cmd/mokapi/mokapi.go b/pkg/cmd/mokapi/mokapi.go index 630062c7c..c9f18d61a 100644 --- a/pkg/cmd/mokapi/mokapi.go +++ b/pkg/cmd/mokapi/mokapi.go @@ -168,6 +168,7 @@ func createServer(cfg *static.Config) (*server.Server, error) { if cfg.Mcp.Server.Enabled { if u, err := mcp.BuildUrl(cfg.Mcp.Server); err == nil { if cfg.Api.Port == cfg.Mcp.Server.Port { + log.Infof("MCP server runs on %s:%d", cfg.Mcp.Server.Path, cfg.Mcp.Server.Port) apiHandler.RegisterMcpHandler(u.Path, mcp.NewServer(app)) } else { err = http.AddInternalService("mcp", u, mcp.NewServer(app)) diff --git a/runtime/runtimetest/app.go b/runtime/runtimetest/app.go index de44f4faa..f3fbaf70a 100644 --- a/runtime/runtimetest/app.go +++ b/runtime/runtimetest/app.go @@ -11,6 +11,7 @@ import ( "mokapi/providers/mail" "mokapi/providers/openapi" "mokapi/runtime" + "mokapi/runtime/events" ) func NewHttpApp(configs ...*openapi.Config) *runtime.App { @@ -107,3 +108,12 @@ func WithMailInfo(name string, mi *runtime.MailInfo) Options { app.Mail.Set(name, mi) } } + +func WithEvent(traits events.Traits, data events.EventData) Options { + return func(app *runtime.App) { + err := app.Events.Push(data, traits) + if err != nil { + panic(err) + } + } +} From d52cf43b770a8f7c143763769dbd712000a64096 Mon Sep 17 00:00:00 2001 From: marle3003 Date: Mon, 20 Apr 2026 18:59:03 +0200 Subject: [PATCH 41/46] wip: mcp server execute code - add Kafka support - refactor run code --- mcp/conditional-response.md | 7 + mcp/mock_reference.md | 16 +- mcp/run.go | 305 +----------- mcp/run.ts | 113 ++++- mcp/{events.go => run_events.go} | 0 mcp/{events_test.go => run_events_test.go} | 0 mcp/run_http.go | 321 +++++++++++++ mcp/run_http_test.go | 434 ++++++++++++++++++ mcp/run_kafka.go | 274 +++++++++++ mcp/run_kafka_test.go | 332 ++++++++++++++ mcp/run_test.go | 427 ----------------- providers/asyncapi3/asyncapi3test/channel.go | 12 + providers/asyncapi3/asyncapi3test/message.go | 12 + .../asyncapi3/asyncapi3test/operation.go | 18 + providers/asyncapi3/kafka/store/partition.go | 10 +- 15 files changed, 1534 insertions(+), 747 deletions(-) rename mcp/{events.go => run_events.go} (100%) rename mcp/{events_test.go => run_events_test.go} (100%) create mode 100644 mcp/run_http.go create mode 100644 mcp/run_http_test.go create mode 100644 mcp/run_kafka.go create mode 100644 mcp/run_kafka_test.go diff --git a/mcp/conditional-response.md b/mcp/conditional-response.md index eeb76296f..497843849 100644 --- a/mcp/conditional-response.md +++ b/mcp/conditional-response.md @@ -5,6 +5,11 @@ Demonstrates how to: - Access request parameters - Apply custom logic (e.g., lookup, filtering, updates) +IMPORTANT +Strict Specification Enforcement: +Mokapi will throw an error if you use a status code NOT defined in the specification. +Always verify the available status codes for each operation before calling response.rebuild() or setting response.statusCode. + ```typescript import { on } from "mokapi" @@ -24,6 +29,7 @@ export default function () { case '/terminals/{id}': { const terminal = terminals.find(x => x.id === request.path.id) if (!terminal) { + // CHECK SPEC: Is 404 defined for this path and HTTP method? response.rebuild(404) response.data = { error: 'terminal not found' } return @@ -48,6 +54,7 @@ export default function () { if (terminal) { // console output will be displayed in the Mokapi's' dashboard console.log('terminal already exists', request.body) + // CHECK SPEC: Is 400 defined for this path and HTTP method? response.rebuild(400) } else { terminals.push(request.body) diff --git a/mcp/mock_reference.md b/mcp/mock_reference.md index 7f29d5f8a..775b1aaa2 100644 --- a/mcp/mock_reference.md +++ b/mcp/mock_reference.md @@ -11,14 +11,14 @@ with the corresponding category and name (e.g., category='types', name='http') Overview of TypeScript definitions for mock event handlers. - Use these types to ensure correct syntax for "import { ... } from 'mokapi/...'". -| Type | Import Statement | Parameter Name | -|--------------------------------|---------------------------------------|----------------| -| **Core** | `import {...} from 'mokapi'` | mokapi | -| **HTTP** | `import {...} from 'mokapi/http'` | http | -| **Kafka** | `import {...} from 'mokapi/kafka'` | kafka | -| **Faker** | `import {...} from 'mokapi/faker'` | faker | -| **Mustache** | `import {...} from 'mokapi/mustache'` | mustache | -| **Yaml** | `import {...} from 'mokapi/yaml'` | yaml | +| Type | Description | Import Statement | Parameter Name | +|--------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------|----------------------| +| **Core** | Exposes the core scripting API for Mokapi. It allows you to intercept and manipulate protocol events (HTTP, Kafka, LDAP, SMTP), schedule jobs, generate mock data, and share state between scripts. | `import {...} from 'mokapi'` | mokapi | +| **HTTP** | Exposes functions to invoke HTTP request | `import {...} from 'mokapi/http'` | http | +| **Kafka** | Exposes functions to produce Kafka message | `import {...} from 'mokapi/kafka'` | kafka | +| **Faker** | Exposes functions to generate random data based on JSON schema | `import {...} from 'mokapi/faker'` | faker | +| **Mustache** | Exposes to render output based on mustache template | `import {...} from 'mokapi/mustache'` | mustache | +| **Yaml** | Exposes functions to parse or stringify YAML files | `import {...} from 'mokapi/yaml'` | yaml | ## Scenarios diff --git a/mcp/run.go b/mcp/run.go index 7c4872b61..baaa71701 100644 --- a/mcp/run.go +++ b/mcp/run.go @@ -5,15 +5,11 @@ import ( _ "embed" "errors" "fmt" - "io" "mokapi/js/compiler" "mokapi/js/faker" "mokapi/providers/openapi" - "mokapi/providers/openapi/schema" "mokapi/runtime" "mokapi/schema/json/generator" - "net/http" - "net/textproto" "reflect" "slices" "strings" @@ -137,7 +133,7 @@ func (m *mokapi) getApis() []ApiSummary { var result []ApiSummary for _, api := range m.app.ListHttp() { if api.Info.Name == "" { - log.Warnf("mcp tool mokapi_get_api_spec: skip empty HTTTP API name") + log.Warnf("mcp tool mokapi_execute_code: skip empty HTTTP API name") continue } result = append(result, ApiSummary{ @@ -145,6 +141,16 @@ func (m *mokapi) getApis() []ApiSummary { Type: "http", }) } + for _, api := range m.app.Kafka.List() { + if api.Info.Name == "" { + log.Warnf("mcp tool mokapi_execute_code: skip empty Kafka API name") + continue + } + result = append(result, ApiSummary{ + Name: api.Info.Name, + Type: "kafka", + }) + } slices.SortStableFunc(result, func(a, b ApiSummary) int { return strings.Compare(a.Name, b.Name) }) @@ -152,17 +158,13 @@ func (m *mokapi) getApis() []ApiSummary { } func (m *mokapi) getApi(name string) any { - for _, api := range m.app.ListHttp() { - if api.Info.Name == name { - return &OpenAPI{ - Name: name, - Type: "http", - info: api, - handler: api.Handler(m.app.Monitor.Http, m.app.Engine, m.app.Events), - } - } + var api any + api = m.getHttpApi(name) + if api != nil { + return api } - return nil + api = m.getKafkaApi(name) + return api } func (m *mokapi) fake(v goja.Value) (any, error) { @@ -173,279 +175,6 @@ func (m *mokapi) fake(v goja.Value) (any, error) { return generator.New(&generator.Request{Schema: js}) } -type OpenAPI struct { - Name string `json:"name"` - Type string `json:"type"` - - info *runtime.HttpInfo - handler openapi.Handler -} - -type OperationSummary struct { - Id string `json:"id"` - Method string `json:"method"` - Path string `json:"path"` - Summary string `json:"summary"` - Parameters []string `json:"parameters"` -} - -type Operation struct { - OperationId string `json:"operationId"` - Method string `json:"method"` - Path string `json:"path"` - Summary string `json:"summary"` - Description string `json:"description,omitempty"` - Parameters []RequestParameters `json:"parameters,omitempty"` - RequestBody RequestBody `json:"requestBody,omitempty"` - - spec *openapi.Operation - handler openapi.Handler -} - -type RequestParameters struct { - Name string `json:"name"` - In string `json:"in"` - Required bool `json:"required"` - Schema *schema.Schema - Description string `json:"description,omitempty"` -} - -type RequestBody struct { - Description string `json:"description,omitempty"` - Required bool `json:"required"` - Contents []Content `json:"contents"` -} - -type Content struct { - ContentType string `json:"contentType"` - Schema *schema.Schema `json:"schema"` -} - -type Response struct { - StatusCode int `json:"statusCode"` - Description string `json:"description,omitempty"` - Content []Content `json:"content"` -} - -func (o *OpenAPI) GetOperations() []OperationSummary { - var result []OperationSummary - for _, p := range o.info.Paths { - if p.Value == nil { - continue - } - for method, op := range p.Value.Operations() { - os := OperationSummary{ - Id: getOperationId(method, op), - Method: method, - Path: p.Value.Path, - Summary: op.Summary, - } - - if os.Summary == "" { - os.Summary = p.Value.Summary - } - - params := append(op.Path.Parameters, op.Parameters...) - for _, param := range params { - if param.Value == nil { - continue - } - os.Parameters = append(os.Parameters, param.Value.Name) - } - slices.SortStableFunc(os.Parameters, func(a, b string) int { - return strings.Compare(a, b) - }) - - result = append(result, os) - } - } - - slices.SortStableFunc(result, func(a, b OperationSummary) int { - c := strings.Compare(a.Path, b.Path) - if c != 0 { - return c - } - return strings.Compare(a.Method, b.Method) - }) - - return result -} - -func (o *OpenAPI) GetOperation(id string) (*Operation, error) { - for _, p := range o.info.Paths { - if p.Value == nil { - continue - } - for method, op := range p.Value.Operations() { - - operationId := getOperationId(method, op) - if id != operationId { - continue - } - - r := &Operation{ - OperationId: operationId, - Method: method, - Path: p.Value.Path, - Summary: op.Summary, - Description: op.Description, - spec: op, - handler: o.handler, - } - for _, param := range op.Parameters { - if param.Value == nil { - continue - } - r.Parameters = append(r.Parameters, RequestParameters{ - Name: param.Value.Name, - In: param.Value.Type.String(), - Required: param.Value.Required, - Schema: param.Value.Schema, - Description: param.Value.Description, - }) - } - slices.SortStableFunc(r.Parameters, func(a, b RequestParameters) int { - return strings.Compare(a.Name, b.Name) - }) - - if op.RequestBody != nil && op.RequestBody.Value != nil { - r.RequestBody = RequestBody{ - Description: op.RequestBody.Value.Description, - Required: op.RequestBody.Value.Required, - } - for ct, content := range op.RequestBody.Value.Content { - r.RequestBody.Contents = append(r.RequestBody.Contents, Content{ - ContentType: ct, - Schema: content.Schema, - }) - } - } - return r, nil - } - } - return nil, fmt.Errorf("operation with ID '%s' not found. Hint: Use getOperations() to see the full list of valid IDs", id) -} - -func (op *Operation) GetResponseSchema(statusCode int) *Response { - r := op.spec.Responses.GetResponse(statusCode) - if r == nil { - return nil - } - result := &Response{ - StatusCode: statusCode, - Description: r.Description, - } - for ct, content := range r.Content { - result.Content = append(result.Content, Content{ - ContentType: ct, - Schema: content.Schema, - }) - } - return result -} - -type InvokeRequest struct { - Path map[string]string `json:"path"` - Query map[string]string `json:"query"` - Header map[string][]string `json:"header"` - Body string `json:"body"` -} - -type InvokeResponse struct { - StatusCode int `json:"statusCode"` - Headers map[string][]string `json:"headers"` - Body string `json:"body"` -} - -func (op *Operation) Invoke(req InvokeRequest) (InvokeResponse, error) { - result := InvokeResponse{Headers: make(map[string][]string)} - - var body io.Reader - if req.Body != "" { - body = strings.NewReader(req.Body) - } - - path := op.Path - query := "" - params := append(op.spec.Path.Parameters, op.spec.Parameters...) - for _, p := range params { - if p.Value == nil { - continue - } - switch p.Value.Type { - case openapi.ParameterPath: - if req.Path == nil { - return result, fmt.Errorf("invoke request %s %s failed: missing path parameter '%s'", op.Method, op.Path, p.Value.Name) - } - val, ok := req.Path[p.Value.Name] - if !ok { - return result, fmt.Errorf("invoke request %s %s failed: missing path parameter '%s'", op.Method, op.Path, p.Value.Name) - } - path = strings.ReplaceAll(path, fmt.Sprintf("{%s}", p.Value.Name), val) - case openapi.ParameterQuery: - if req.Query == nil && p.Value.Required { - return result, fmt.Errorf("invoke request %s %s failed: missing query parameter '%s'", op.Method, op.Path, p.Value.Name) - } - val, ok := req.Query[p.Value.Name] - if !ok { - if !p.Value.Required { - continue - } - return result, fmt.Errorf("invoke request %s %s failed: missing query parameter '%s'", op.Method, op.Path, p.Value.Name) - } - if query != "" { - query += "&" - } - query += fmt.Sprintf("%s=%s", p.Value.Name, val) - } - } - - if query != "" { - path += "?" + query - } - - r, err := http.NewRequest(op.Method, path, body) - if err != nil { - return result, fmt.Errorf("error creating request: %w", err) - } - for _, p := range params { - if p.Value == nil || p.Value.Type != openapi.ParameterHeader { - continue - } - if req.Header == nil && p.Value.Required { - return result, fmt.Errorf("invoke request %s %s failed: missing header parameter '%s'", op.Method, op.Path, p.Value.Name) - } - val, ok := req.Header[p.Value.Name] - if !ok { - if !p.Value.Required { - continue - } - return result, fmt.Errorf("invoke request %s %s failed: missing header parameter '%s'", op.Method, op.Path, p.Value.Name) - } - r.Header[textproto.CanonicalMIMEHeaderKey(p.Value.Name)] = val - } - - he := op.handler.ServeHTTP(&result, r) - if he != nil { - result.StatusCode = he.StatusCode - result.Body = he.Message - } - return result, nil -} - -func (r *InvokeResponse) Header() http.Header { - return r.Headers -} - -func (r *InvokeResponse) WriteHeader(statusCode int) { - r.StatusCode = statusCode -} - -func (r *InvokeResponse) Write(body []byte) (int, error) { - r.Body = string(body) - return len(body), nil -} - type customFieldNameMapper struct { } diff --git a/mcp/run.ts b/mcp/run.ts index 30cb38fef..335c1f7c8 100644 --- a/mcp/run.ts +++ b/mcp/run.ts @@ -11,7 +11,7 @@ interface Mokapi { * @example * getApi('Swagger Petstore') */ - getApi(name: string): Api; + getApi(name: string): OpenApi; /** * Generate a random value from a JSON Schema. @@ -32,7 +32,7 @@ interface Mokapi { * @param traits Filter events by traits * @param limit Maximum number of events to return, default is 10 * @example - * getEvents({ apiType: 'http' }) + * getEvents({ apiType: 'http', name: 'Swagger Petstore', path: '/pets' }) */ getEvents(traits: HttpTraits, limit?: number): Event[]; } @@ -44,25 +44,23 @@ interface ApiSummary { type: ApiType; } -interface Api extends ApiSummary { - spec: OpenApi -} +interface OpenApi extends ApiSummary { + servers: { url: string, description: string } -interface OpenApi { /** * Returns all operations of this API. */ - getOperations(): OperationSummary[]; + getOperations(): OpenApiOperationSummary[]; /** * Returns details about specific operation * Use id from the operation summary list * @param id The id of the operation */ - getOperation(id: string): OperationDetail + getOperation(id: string): OpenApiOperation } -interface OperationSummary { +interface OpenApiOperationSummary { /** generated from method and path if missing in spec */ id: string method: string; @@ -72,7 +70,7 @@ interface OperationSummary { parameters?: string[] } -interface OperationDetail { +interface OpenApiOperation { id: string; path: string method: string @@ -80,17 +78,17 @@ interface OperationDetail { description: string; parameters: RequestParameter[] requestBody: RequestBody + /** + * List of allowed responses. + * IMPORTANT: You must only use these status codes for this operation! + */ + responses: Response[] /** * Invoke this operation against the mocked API. * @example operation.invoke({ path: { id: 1 }, body: JSON.stringify({ name: "test" }) }) */ invoke(request?: InvokeRequest): InvokeResponse; - - /** - * Returns the response schema for a given status code. - */ - getResponseSchema(statusCode: number): Response[]; } interface RequestParameter { @@ -131,6 +129,83 @@ interface InvokeResponse { body: string } +interface Kafka extends ApiSummary { + brokers: { name: string, host: string, description?: string } + + getTopics(): KafkaTopicSummary[] + getTopic(topicName: string): KafkaTopic +} + +interface KafkaTopicSummary { + /** + * The unique name of the topic. + */ + name: string + title?: string + summary?: string +} + +interface KafkaTopic extends KafkaTopicSummary{ + description: string + /** + * List of current partitions and their maximum offsets. + * Use this to determine the range for the 'consume' method. + */ + partitions: { index: number, offset: number } + + operations: KafkaOperation[]; + + /** + * Use 'produce' to send a message to this topic. + * Check 'operations' with action 'send' for valid payloads. + * @param partition The target partition index. MUST be one of the indices listed in the 'partitions' array. + * @param value The message payload. If the operation specifies a JSON schema, provide this as a stringified object. + * @param key Optional message key. + * @param headers Optional metadata headers. + */ + produce(partition: number, value: string, key?: string, headers?: KafkaHeader): void + + /** + * INSPECT: Retrieves a specific record for analysis or verification. + * Use this to check if the mock has received or produced a specific message. + * @param partition The partition index (see 'partitions' list). + * @param startOffset The offset to start reading from (see 'partitions' for max offset). + * @param limit The maximum number of records to return in this call. + * @returns An array of records found starting from the startOffset. + */ + consume(partition: number, startOffset: number, limit: number): KafkaRecord[] +} + +interface KafkaOperation { + action: 'send' | 'receive' + title: string + summary: string; + description: string + messages: KafkaMessage[]; +} + +interface KafkaMessage { + name: string; + title: string; + summary: string + description: string + contentType: string + payload: Schema; + key: Schema + headers?: Schema; +} + +interface KafkaHeader { + [name: string]: string +} + +interface KafkaRecord { + offset: number + key: string + value: string + headers: KafkaHeader +} + /** * JSON Schema defines a JSON-based format for describing the structure of JSON data * @example @@ -281,8 +356,8 @@ interface Schema { type SchemaType = "object" | "array" | "number" | "integer" | "string" | "boolean" | "null"; interface Traits { - type: ApiType; - name: string; + type?: ApiType; + name?: string; } interface HttpTraits extends Traits { @@ -290,13 +365,13 @@ interface HttpTraits extends Traits { * Path value specified by the OpenAPI path * @example /pet/{petId} */ - path: string + path?: string /** * Request method. * @example GET */ - method: string + method?: string } interface Event { diff --git a/mcp/events.go b/mcp/run_events.go similarity index 100% rename from mcp/events.go rename to mcp/run_events.go diff --git a/mcp/events_test.go b/mcp/run_events_test.go similarity index 100% rename from mcp/events_test.go rename to mcp/run_events_test.go diff --git a/mcp/run_http.go b/mcp/run_http.go new file mode 100644 index 000000000..3616dbf8c --- /dev/null +++ b/mcp/run_http.go @@ -0,0 +1,321 @@ +package mcp + +import ( + "fmt" + "io" + "mokapi/providers/openapi" + "mokapi/providers/openapi/schema" + "mokapi/runtime" + "net/http" + "net/textproto" + "slices" + "strconv" + "strings" +) + +type OpenAPI struct { + Name string `json:"name"` + Type string `json:"type"` + Servers []OpenAPIServer `json:"servers"` + + info *runtime.HttpInfo + handler openapi.Handler +} + +type OpenAPIServer struct { + Url string `json:"url"` + Description string `json:"description"` +} + +type OperationSummary struct { + Id string `json:"id"` + Method string `json:"method"` + Path string `json:"path"` + Summary string `json:"summary"` + Parameters []string `json:"parameters"` +} + +type Operation struct { + OperationId string `json:"operationId"` + Method string `json:"method"` + Path string `json:"path"` + Summary string `json:"summary"` + Description string `json:"description,omitempty"` + Parameters []RequestParameters `json:"parameters,omitempty"` + RequestBody RequestBody `json:"requestBody,omitempty"` + Responses []Response `json:"responses,omitempty"` + + spec *openapi.Operation + handler openapi.Handler +} + +type RequestParameters struct { + Name string `json:"name"` + In string `json:"in"` + Required bool `json:"required"` + Schema *schema.Schema + Description string `json:"description,omitempty"` +} + +type RequestBody struct { + Description string `json:"description,omitempty"` + Required bool `json:"required"` + Contents []Content `json:"contents"` +} + +type Content struct { + ContentType string `json:"contentType"` + Schema *schema.Schema `json:"schema"` +} + +type Response struct { + StatusCode int `json:"statusCode"` + Description string `json:"description,omitempty"` + Content []Content `json:"content"` +} + +func (m *mokapi) getHttpApi(name string) any { + for _, api := range m.app.ListHttp() { + if api.Info.Name == name { + result := &OpenAPI{ + Name: name, + Type: "http", + info: api, + handler: api.Handler(m.app.Monitor.Http, m.app.Engine, m.app.Events), + } + for _, server := range api.Servers { + result.Servers = append(result.Servers, OpenAPIServer{ + Url: server.Url, + Description: server.Description, + }) + } + + return result + } + } + return nil +} +func (o *OpenAPI) GetOperations() []OperationSummary { + var result []OperationSummary + for _, p := range o.info.Paths { + if p.Value == nil { + continue + } + for method, op := range p.Value.Operations() { + os := OperationSummary{ + Id: getOperationId(method, op), + Method: method, + Path: p.Value.Path, + Summary: op.Summary, + } + + if os.Summary == "" { + os.Summary = p.Value.Summary + } + + params := append(op.Path.Parameters, op.Parameters...) + for _, param := range params { + if param.Value == nil { + continue + } + os.Parameters = append(os.Parameters, param.Value.Name) + } + slices.SortStableFunc(os.Parameters, func(a, b string) int { + return strings.Compare(a, b) + }) + + result = append(result, os) + } + } + + slices.SortStableFunc(result, func(a, b OperationSummary) int { + c := strings.Compare(a.Path, b.Path) + if c != 0 { + return c + } + return strings.Compare(a.Method, b.Method) + }) + + return result +} + +func (o *OpenAPI) GetOperation(id string) (*Operation, error) { + for _, p := range o.info.Paths { + if p.Value == nil { + continue + } + for method, op := range p.Value.Operations() { + + operationId := getOperationId(method, op) + if id != operationId { + continue + } + + r := &Operation{ + OperationId: operationId, + Method: method, + Path: p.Value.Path, + Summary: op.Summary, + Description: op.Description, + spec: op, + handler: o.handler, + } + for _, param := range op.Parameters { + if param.Value == nil { + continue + } + r.Parameters = append(r.Parameters, RequestParameters{ + Name: param.Value.Name, + In: param.Value.Type.String(), + Required: param.Value.Required, + Schema: param.Value.Schema, + Description: param.Value.Description, + }) + } + slices.SortStableFunc(r.Parameters, func(a, b RequestParameters) int { + return strings.Compare(a.Name, b.Name) + }) + + if op.RequestBody != nil && op.RequestBody.Value != nil { + r.RequestBody = RequestBody{ + Description: op.RequestBody.Value.Description, + Required: op.RequestBody.Value.Required, + } + for ct, content := range op.RequestBody.Value.Content { + r.RequestBody.Contents = append(r.RequestBody.Contents, Content{ + ContentType: ct, + Schema: content.Schema, + }) + } + } + for it := op.Responses.Iter(); it.Next(); { + status, err := strconv.Atoi(it.Key()) + if err != nil { + continue + } + + res := it.Value() + if res.Value == nil { + continue + } + var contents []Content + for ct, content := range res.Value.Content { + contents = append(contents, Content{ + ContentType: ct, + Schema: content.Schema, + }) + } + r.Responses = append(r.Responses, Response{ + StatusCode: status, + Description: res.Value.Description, + Content: contents, + }) + } + + return r, nil + } + } + return nil, fmt.Errorf("operation with ID '%s' not found. Hint: Use getOperations() to see the full list of valid IDs", id) +} + +type InvokeRequest struct { + Path map[string]string `json:"path"` + Query map[string]string `json:"query"` + Header map[string][]string `json:"header"` + Body string `json:"body"` +} + +type InvokeResponse struct { + StatusCode int `json:"statusCode"` + Headers map[string][]string `json:"headers"` + Body string `json:"body"` +} + +func (op *Operation) Invoke(req InvokeRequest) (InvokeResponse, error) { + result := InvokeResponse{Headers: make(map[string][]string)} + + var body io.Reader + if req.Body != "" { + body = strings.NewReader(req.Body) + } + + path := op.Path + query := "" + params := append(op.spec.Path.Parameters, op.spec.Parameters...) + for _, p := range params { + if p.Value == nil { + continue + } + switch p.Value.Type { + case openapi.ParameterPath: + if req.Path == nil { + return result, fmt.Errorf("invoke request %s %s failed: missing path parameter '%s'", op.Method, op.Path, p.Value.Name) + } + val, ok := req.Path[p.Value.Name] + if !ok { + return result, fmt.Errorf("invoke request %s %s failed: missing path parameter '%s'", op.Method, op.Path, p.Value.Name) + } + path = strings.ReplaceAll(path, fmt.Sprintf("{%s}", p.Value.Name), val) + case openapi.ParameterQuery: + if req.Query == nil && p.Value.Required { + return result, fmt.Errorf("invoke request %s %s failed: missing query parameter '%s'", op.Method, op.Path, p.Value.Name) + } + val, ok := req.Query[p.Value.Name] + if !ok { + if !p.Value.Required { + continue + } + return result, fmt.Errorf("invoke request %s %s failed: missing query parameter '%s'", op.Method, op.Path, p.Value.Name) + } + if query != "" { + query += "&" + } + query += fmt.Sprintf("%s=%s", p.Value.Name, val) + } + } + + if query != "" { + path += "?" + query + } + + r, err := http.NewRequest(op.Method, path, body) + if err != nil { + return result, fmt.Errorf("error creating request: %w", err) + } + for _, p := range params { + if p.Value == nil || p.Value.Type != openapi.ParameterHeader { + continue + } + if req.Header == nil && p.Value.Required { + return result, fmt.Errorf("invoke request %s %s failed: missing header parameter '%s'", op.Method, op.Path, p.Value.Name) + } + val, ok := req.Header[p.Value.Name] + if !ok { + if !p.Value.Required { + continue + } + return result, fmt.Errorf("invoke request %s %s failed: missing header parameter '%s'", op.Method, op.Path, p.Value.Name) + } + r.Header[textproto.CanonicalMIMEHeaderKey(p.Value.Name)] = val + } + + he := op.handler.ServeHTTP(&result, r) + if he != nil { + result.StatusCode = he.StatusCode + result.Body = he.Message + } + return result, nil +} + +func (r *InvokeResponse) Header() http.Header { + return r.Headers +} + +func (r *InvokeResponse) WriteHeader(statusCode int) { + r.StatusCode = statusCode +} + +func (r *InvokeResponse) Write(body []byte) (int, error) { + r.Body = string(body) + return len(body), nil +} diff --git a/mcp/run_http_test.go b/mcp/run_http_test.go new file mode 100644 index 000000000..2abe40959 --- /dev/null +++ b/mcp/run_http_test.go @@ -0,0 +1,434 @@ +package mcp_test + +import ( + "context" + "mokapi/mcp" + "mokapi/providers/openapi" + "mokapi/providers/openapi/openapitest" + "mokapi/providers/openapi/schema/schematest" + "mokapi/runtime" + "mokapi/runtime/runtimetest" + "mokapi/schema/json/generator" + "net/http" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestService_Run_Http(t *testing.T) { + testcases := []struct { + name string + app *runtime.App + test func(t *testing.T, s *mcp.Service) + }{ + { + name: "get HTTP APIs", + app: runtimetest.NewHttpApp( + openapitest.NewConfig("3.1.0", + openapitest.WithInfo("foo", "", ""), + ), + openapitest.NewConfig("3.1.0", + openapitest.WithInfo("bar", "", ""), + ), + ), + test: func(t *testing.T, s *mcp.Service) { + r, err := s.GetRunResponse( + context.Background(), + mcp.RunInput{ + Code: `mokapi.getApis()`, + }, + ) + require.NoError(t, err) + require.Equal(t, []mcp.ApiSummary{ + {Name: "bar", Type: "http"}, + {Name: "foo", Type: "http"}, + }, r.Result) + }, + }, + { + name: "get specific HTTP API", + app: runtimetest.NewHttpApp( + openapitest.NewConfig("3.1.0", + openapitest.WithInfo("foo", "", ""), + openapitest.WithServer("http://localhost/foo", "server description"), + ), + openapitest.NewConfig("3.1.0", + openapitest.WithInfo("bar", "", ""), + ), + ), + test: func(t *testing.T, s *mcp.Service) { + r, err := s.GetRunResponse( + context.Background(), + mcp.RunInput{ + Code: `mokapi.getApi('foo')`, + }, + ) + require.NoError(t, err) + require.IsType(t, &mcp.OpenAPI{}, r.Result) + api := r.Result.(*mcp.OpenAPI) + require.Equal(t, "foo", api.Name) + require.Equal(t, "http", api.Type) + require.Equal(t, []mcp.OpenAPIServer{{Url: "http://localhost/foo", Description: "server description"}}, api.Servers) + }, + }, + { + name: "get API's operation list", + app: runtimetest.NewHttpApp( + openapitest.NewConfig("3.1.0", + openapitest.WithInfo("foo", "", ""), + openapitest.WithPath("/pets", + openapitest.WithOperation(http.MethodGet, + openapitest.WithOperationId("pets"), + openapitest.WithOperationSummary("GET summary"), + ), + openapitest.WithOperation(http.MethodPut, + openapitest.WithOperationSummary("PUT summary"), + ), + ), + openapitest.WithPath("/users", + openapitest.WithPathInfo("path summary", ""), + openapitest.WithPathParam("foo"), + openapitest.WithOperation(http.MethodPost, openapitest.WithOperationParam("bar", false)), + ), + ), + ), + test: func(t *testing.T, s *mcp.Service) { + r, err := s.GetRunResponse( + context.Background(), + mcp.RunInput{ + Code: `mokapi.getApi('foo').getOperations()`, + }, + ) + require.NoError(t, err) + require.Equal(t, []mcp.OperationSummary{ + {Id: "pets", Method: "GET", Path: "/pets", Summary: "GET summary"}, + {Id: "put-/pets", Method: "PUT", Path: "/pets", Summary: "PUT summary"}, + {Id: "post-/users", Method: "POST", Path: "/users", Summary: "path summary", Parameters: []string{"bar", "foo"}}}, + r.Result) + }, + }, + { + name: "get API's operation details", + app: runtimetest.NewHttpApp( + openapitest.NewConfig("3.1.0", + openapitest.WithInfo("foo", "", ""), + openapitest.WithPath("/pets", + openapitest.WithOperation(http.MethodGet, + openapitest.WithOperationSummary("GET summary"), + openapitest.WithHeaderParam("foo", false, openapitest.WithParamSchema(schematest.New("string"))), + openapitest.WithRequestBody( + "request body description", + true, + openapitest.WithRequestContent( + "application/json", + &openapi.MediaType{ + Schema: schematest.New("string"), + }, + ), + ), + openapitest.WithResponse(200, + openapitest.WithResponseDescription("response description"), + openapitest.WithContent("application/json", openapitest.WithSchema( + schematest.New("string"), + )), + ), + ), + openapitest.WithOperation(http.MethodPut, + openapitest.WithOperationSummary("PUT summary"), + ), + ), + ), + ), + test: func(t *testing.T, s *mcp.Service) { + r, err := s.GetRunResponse( + context.Background(), + mcp.RunInput{ + Code: `mokapi.getApi('foo').getOperation('get-/pets')`, + }, + ) + require.NoError(t, err) + require.IsType(t, &mcp.Operation{}, r.Result) + op := r.Result.(*mcp.Operation) + require.Equal(t, "get-/pets", op.OperationId) + require.Equal(t, "GET", op.Method) + require.Equal(t, "/pets", op.Path) + require.Equal(t, "GET summary", op.Summary) + require.Equal(t, "", op.Description) + require.Equal(t, "foo", op.Parameters[0].Name) + require.Equal(t, "header", op.Parameters[0].In) + require.Equal(t, false, op.Parameters[0].Required) + require.Equal(t, "string", op.Parameters[0].Schema.Type[0]) + require.Equal(t, "", op.Parameters[0].Description) + require.Equal(t, "request body description", op.RequestBody.Description) + require.Equal(t, true, op.RequestBody.Required) + require.Equal(t, "application/json", op.RequestBody.Contents[0].ContentType) + require.Equal(t, "string", op.RequestBody.Contents[0].Schema.Type[0]) + + require.Len(t, op.Responses, 1) + require.Equal(t, http.StatusOK, op.Responses[0].StatusCode) + require.Equal(t, "response description", op.Responses[0].Description) + require.Len(t, op.Responses[0].Content, 1) + require.Equal(t, "application/json", op.Responses[0].Content[0].ContentType) + require.Equal(t, "string", op.Responses[0].Content[0].Schema.Type[0]) + }, + }, + { + name: "get API's operation using projection", + app: runtimetest.NewHttpApp( + openapitest.NewConfig("3.1.0", + openapitest.WithInfo("foo", "", ""), + openapitest.WithPath("/pets", + openapitest.WithOperation(http.MethodGet, + openapitest.WithOperationSummary("GET summary"), + openapitest.WithHeaderParam("foo", false, openapitest.WithParamSchema(schematest.New("string"))), + openapitest.WithRequestBody( + "request body description", + true, + openapitest.WithRequestContent( + "application/json", + &openapi.MediaType{ + Schema: schematest.New("string"), + }, + ), + ), + openapitest.WithResponse(200, + openapitest.WithContent("application/json", openapitest.WithSchema( + schematest.New("string"), + )), + ), + ), + openapitest.WithOperation(http.MethodPut, + openapitest.WithOperationSummary("PUT summary"), + ), + ), + ), + ), + test: func(t *testing.T, s *mcp.Service) { + r, err := s.GetRunResponse( + context.Background(), + mcp.RunInput{ + Code: `const op = mokapi.getApi('foo').getOperation('get-/pets') +const result = { path: op.path, method: op.method }; result`, + }, + ) + require.NoError(t, err) + require.Equal(t, map[string]any{"method": "GET", "path": "/pets"}, r.Result) + }, + }, + { + name: "getResponseSchema not exists", + app: runtimetest.NewHttpApp( + openapitest.NewConfig("3.1.0", + openapitest.WithInfo("foo", "", ""), + openapitest.WithPath("/pets", + openapitest.WithOperation(http.MethodGet, + openapitest.WithResponse(200, + openapitest.WithResponseDescription("response description"), + openapitest.WithContent("application/json", openapitest.WithSchema( + schematest.New("string"), + )), + ), + ), + ), + ), + ), + test: func(t *testing.T, s *mcp.Service) { + r, err := s.GetRunResponse( + context.Background(), + mcp.RunInput{ + Code: `const op = mokapi.getApi('foo').getOperation('get-/pets') +op.getResponseSchema(404)`, + }, + ) + require.NoError(t, err) + require.Nil(t, r.Result) + }, + }, + { + name: "invoke request", + app: runtimetest.NewHttpApp( + openapitest.NewConfig("3.1.0", + openapitest.WithInfo("foo", "", ""), + openapitest.WithPath("/pets", + openapitest.WithOperation(http.MethodGet, + openapitest.WithResponse(200, + openapitest.WithResponseDescription("response description"), + openapitest.WithContent("application/json", openapitest.WithSchema( + schematest.New("object", + schematest.WithProperty("foo", schematest.New("string")), + schematest.WithProperty("bar", schematest.New("integer")), + schematest.WithRequired("foo", "bar"), + ), + )), + ), + ), + ), + ), + ), + test: func(t *testing.T, s *mcp.Service) { + r, err := s.GetRunResponse( + context.Background(), + mcp.RunInput{ + Code: `const op = mokapi.getApi('foo').getOperation('get-/pets'); +op.invoke()`, + }, + ) + require.NoError(t, err) + require.IsType(t, mcp.InvokeResponse{}, r.Result) + res := r.Result.(mcp.InvokeResponse) + require.Equal(t, 200, res.StatusCode) + require.Equal(t, `{"foo":"P8","bar":-804702}`, res.Body) + require.Equal(t, map[string][]string{ + "Content-Type": {"application/json"}, + }, res.Headers) + }, + }, + { + name: "invoke request with path parameter", + app: runtimetest.NewHttpApp( + openapitest.NewConfig("3.1.0", + openapitest.WithInfo("foo", "", ""), + openapitest.WithPath("/{foo}/{bar}/pets", + openapitest.WithOperation(http.MethodGet, + openapitest.WithOperationParam("foo", true, openapitest.WithParamSchema(schematest.New("string"))), + openapitest.WithOperationParam("bar", true, openapitest.WithParamSchema(schematest.New("string"))), + openapitest.WithResponse(200, + openapitest.WithResponseDescription("response description"), + openapitest.WithContent("application/json", openapitest.WithSchema( + schematest.New("string"), + )), + ), + ), + ), + ), + ), + test: func(t *testing.T, s *mcp.Service) { + r, err := s.GetRunResponse( + context.Background(), + mcp.RunInput{ + Code: `const op = mokapi.getApi('foo').getOperation('get-/{foo}/{bar}/pets'); +op.invoke({ path: { foo: 'val1', 'bar': 'val2' }})`, + }, + ) + require.NoError(t, err) + res := r.Result.(mcp.InvokeResponse) + require.Equal(t, http.StatusOK, res.StatusCode, res.Body) + }, + }, + { + name: "invoke request with path parameter but not specified", + app: runtimetest.NewHttpApp( + openapitest.NewConfig("3.1.0", + openapitest.WithInfo("foo", "", ""), + openapitest.WithPath("/{foo}/{bar}/pets", + openapitest.WithOperation(http.MethodGet, + openapitest.WithOperationParam("foo", true, openapitest.WithParamSchema(schematest.New("string"))), + openapitest.WithOperationParam("bar", true, openapitest.WithParamSchema(schematest.New("string"))), + openapitest.WithResponse(200, + openapitest.WithResponseDescription("response description"), + openapitest.WithContent("application/json", openapitest.WithSchema( + schematest.New("string"), + )), + ), + ), + ), + ), + ), + test: func(t *testing.T, s *mcp.Service) { + _, err := s.GetRunResponse( + context.Background(), + mcp.RunInput{ + Code: `const op = mokapi.getApi('foo').getOperation('get-/{foo}/{bar}/pets'); +op.invoke()`, + }, + ) + require.EqualError(t, err, "invoke request GET /{foo}/{bar}/pets failed: missing path parameter 'foo'") + }, + }, + { + name: "invoke request with query parameter", + app: runtimetest.NewHttpApp( + openapitest.NewConfig("3.1.0", + openapitest.WithInfo("foo", "", ""), + openapitest.WithPath("/pets", + openapitest.WithOperation(http.MethodGet, + openapitest.WithQueryParam("foo", true, openapitest.WithParamSchema(schematest.New("string"))), + openapitest.WithResponse(200, + openapitest.WithResponseDescription("response description"), + openapitest.WithContent("application/json", openapitest.WithSchema( + schematest.New("string"), + )), + ), + ), + ), + ), + ), + test: func(t *testing.T, s *mcp.Service) { + r, err := s.GetRunResponse( + context.Background(), + mcp.RunInput{ + Code: `const op = mokapi.getApi('foo').getOperation('get-/pets'); +op.invoke({ query: { foo: 'val1' }})`, + }, + ) + require.NoError(t, err) + res := r.Result.(mcp.InvokeResponse) + require.Equal(t, http.StatusOK, res.StatusCode, res.Body) + }, + }, + { + name: "invoke request with header parameter", + app: runtimetest.NewHttpApp( + openapitest.NewConfig("3.1.0", + openapitest.WithInfo("foo", "", ""), + openapitest.WithPath("/pets", + openapitest.WithOperation(http.MethodGet, + openapitest.WithHeaderParam("foo", true, openapitest.WithParamSchema(schematest.New("string"))), + openapitest.WithResponse(200, + openapitest.WithResponseDescription("response description"), + openapitest.WithContent("application/json", openapitest.WithSchema( + schematest.New("string"), + )), + ), + ), + ), + ), + ), + test: func(t *testing.T, s *mcp.Service) { + r, err := s.GetRunResponse( + context.Background(), + mcp.RunInput{ + Code: `const op = mokapi.getApi('foo').getOperation('get-/pets'); +op.invoke({ header: { foo: ['val1'] }})`, + }, + ) + require.NoError(t, err) + res := r.Result.(mcp.InvokeResponse) + require.Equal(t, http.StatusOK, res.StatusCode, res.Body) + }, + }, + { + name: "fake", + app: runtimetest.NewApp(), + test: func(t *testing.T, s *mcp.Service) { + r, err := s.GetRunResponse( + context.Background(), + mcp.RunInput{ + Code: `mokapi.fake({ type: 'string', format: 'email' })`, + }, + ) + require.NoError(t, err) + require.Equal(t, "ivyjones@ziemann.com", r.Result) + }, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + generator.Seed(123456) + + s := mcp.NewService(tc.app) + tc.test(t, s) + }) + } +} diff --git a/mcp/run_kafka.go b/mcp/run_kafka.go new file mode 100644 index 000000000..1d31db39f --- /dev/null +++ b/mcp/run_kafka.go @@ -0,0 +1,274 @@ +package mcp + +import ( + "fmt" + "mokapi/kafka" + "mokapi/runtime" + "slices" + "strings" + "time" +) + +type Kafka struct { + Name string `json:"name"` + Type string `json:"type"` + Brokers []Broker `json:"brokers"` + + info *runtime.KafkaInfo +} + +type Broker struct { + Name string `json:"name"` + Host string `json:"url"` + Description string `json:"description,omitempty"` +} + +type TopicSummary struct { + Name string `json:"name"` + Title string `json:"title,omitempty"` + Summary string `json:"description,omitempty"` +} + +type Topic struct { + TopicSummary + Description string `json:"description,omitempty"` + Partitions []*KafkaPartition `json:"partitions"` + + Operations []KafkaOperation `json:"operations,omitempty"` + + info *runtime.KafkaInfo +} + +type KafkaPartition struct { + Index int `json:"index"` + Offset int64 `json:"offset"` +} + +type KafkaOperation struct { + Action string `json:"action"` + Title string `json:"title"` + Summary string `json:"summary,omitempty"` + Description string `json:"description,omitempty"` + Messages []KafkaMessage `json:"messages,omitempty"` +} + +type KafkaMessage struct { + Name string `json:"name"` + Title string `json:"title"` + Summary string `json:"summary,omitempty"` + Description string `json:"description,omitempty"` + ContentType string `json:"contentType"` + Payload any `json:"payload,omitempty"` + Key any `json:"key,omitempty"` + Headers any `json:"headers,omitempty"` +} + +type KafkaRecord struct { + Offset int64 `json:"offset"` + Key string `json:"key"` + Value string `json:"value"` + Headers map[string]string `json:"headers,omitempty"` +} + +func (m *mokapi) getKafkaApi(name string) any { + for _, api := range m.app.Kafka.List() { + if api.Info.Name == name { + result := &Kafka{ + Name: name, + Type: "kafka", + info: api, + } + for it := api.Servers.Iter(); it.Next(); { + b := it.Value() + if b.Value == nil { + continue + } + result.Brokers = append(result.Brokers, Broker{ + Name: it.Key(), + Host: b.Value.Host, + Description: b.Value.Description, + }) + } + + return result + } + } + return nil +} + +func (k *Kafka) GetTopics() []TopicSummary { + var topics []TopicSummary + for name, c := range k.info.Channels { + if c.Value == nil { + continue + } + topics = append(topics, TopicSummary{ + Name: name, + Title: c.Value.Title, + Summary: c.Value.Summary, + }) + } + slices.SortStableFunc(topics, func(a, b TopicSummary) int { + return strings.Compare(a.Name, b.Name) + }) + return topics +} + +func (k *Kafka) GetTopic(name string) (Topic, error) { + ch, ok := k.info.Channels[name] + if !ok || ch.Value == nil { + return Topic{}, fmt.Errorf("topic '%s' not found", name) + } + + t := Topic{ + TopicSummary: TopicSummary{ + Name: name, + Title: ch.Value.Title, + Summary: ch.Value.Summary, + }, + Description: ch.Value.Description, + info: k.info, + } + + topic := k.info.Store.Topic(name) + if topic == nil { + return Topic{}, fmt.Errorf("topic '%s' not found", name) + } + for _, p := range topic.Partitions { + t.Partitions = append(t.Partitions, &KafkaPartition{ + Index: p.Index, + Offset: p.Offset(), + }) + } + + for _, op := range k.info.Operations { + if op.Value == nil { + continue + } + if op.Value.Channel.Value != ch.Value { + continue + } + + result := KafkaOperation{ + Action: op.Value.Action, + Title: op.Value.Title, + Summary: op.Value.Summary, + Description: op.Value.Description, + } + + for _, msg := range op.Value.Messages { + if msg.Value == nil { + continue + } + m := KafkaMessage{ + Name: msg.Value.Name, + Title: msg.Value.Title, + Summary: msg.Value.Summary, + Description: msg.Value.Description, + ContentType: msg.Value.ContentType, + Headers: msg.Value.Headers, + } + if msg.Value.Payload != nil { + m.Payload = msg.Value.Payload.Value + } + if msg.Value.Bindings.Kafka.Key != nil { + m.Key = msg.Value.Bindings.Kafka.Key + } + result.Messages = append(result.Messages, m) + } + + t.Operations = append(t.Operations, result) + } + + return t, nil +} + +func (t *Topic) Produce(partition int, value string, key string, headers map[string]string) error { + topic := t.info.Store.Topic(t.Name) + if topic == nil { + return fmt.Errorf("topic '%s' not found", t.Name) + } + p := topic.Partition(partition) + if p == nil { + return fmt.Errorf("partition '%d' not found", partition) + } + + r := &kafka.Record{ + Time: time.Now(), + Key: kafka.NewBytes([]byte(key)), + Value: kafka.NewBytes([]byte(value)), + } + if headers != nil { + for k, v := range headers { + r.Headers = append(r.Headers, kafka.RecordHeader{ + Key: k, + Value: []byte(v), + }) + } + } + + result, err := p.Write(kafka.RecordBatch{ + Records: []*kafka.Record{r}, + }) + if err != nil { + return err + } + if result.ErrorCode != kafka.None { + return fmt.Errorf("%d: %s", result.ErrorCode, result.ErrorMessage) + } + for _, pt := range t.Partitions { + if pt.Index == p.Index { + pt.Offset += 1 + } + } + return nil +} + +func (t *Topic) Consume(partition int, startOffset int64, limit int) ([]KafkaRecord, error) { + topic := t.info.Store.Topic(t.Name) + if topic == nil { + return nil, fmt.Errorf("topic '%s' not found", t.Name) + } + p := topic.Partition(partition) + if p == nil { + return nil, fmt.Errorf("partition '%d' not found", partition) + } + + var records []KafkaRecord + offset := startOffset + n := 0 + for { + if offset >= p.Tail || n >= limit { + return records, nil + } + seg := p.GetSegment(offset) + if seg == nil { + return records, nil + } + + for seg.Contains(offset) { + r := seg.Record(offset) + + result := KafkaRecord{ + Offset: offset, + Key: kafka.BytesToString(r.Key), + Value: kafka.BytesToString(r.Value), + } + if r.Headers != nil { + result.Headers = make(map[string]string) + for _, h := range r.Headers { + result.Headers[h.Key] = string(h.Value) + } + } + + records = append(records, result) + + n++ + offset++ + + if n >= limit { + return records, nil + } + } + } +} diff --git a/mcp/run_kafka_test.go b/mcp/run_kafka_test.go new file mode 100644 index 000000000..87f2f0dfd --- /dev/null +++ b/mcp/run_kafka_test.go @@ -0,0 +1,332 @@ +package mcp_test + +import ( + "context" + "mokapi/kafka" + "mokapi/mcp" + "mokapi/providers/asyncapi3" + "mokapi/providers/asyncapi3/asyncapi3test" + "mokapi/runtime" + "mokapi/runtime/runtimetest" + "mokapi/schema/json/generator" + jsonSchema "mokapi/schema/json/schema" + "mokapi/schema/json/schema/schematest" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestService_Run_Kafka(t *testing.T) { + testcases := []struct { + name string + app *runtime.App + test func(t *testing.T, s *mcp.Service) + }{ + { + name: "get Kafka APIs", + app: runtimetest.NewKafkaApp( + asyncapi3test.NewConfig( + asyncapi3test.WithInfo("foo", "", ""), + ), + asyncapi3test.NewConfig( + asyncapi3test.WithInfo("bar", "", ""), + ), + ), + test: func(t *testing.T, s *mcp.Service) { + r, err := s.GetRunResponse( + context.Background(), + mcp.RunInput{ + Code: `mokapi.getApis()`, + }, + ) + require.NoError(t, err) + require.Equal(t, []mcp.ApiSummary{ + {Name: "bar", Type: "kafka"}, + {Name: "foo", Type: "kafka"}, + }, r.Result) + }, + }, + { + name: "get Kafka API", + app: runtimetest.NewKafkaApp( + asyncapi3test.NewConfig( + asyncapi3test.WithInfo("foo", "", ""), + asyncapi3test.WithServer("bar", "kafka", "foo.bar", asyncapi3test.WithServerDescription("server description")), + ), + asyncapi3test.NewConfig( + asyncapi3test.WithInfo("bar", "", ""), + ), + ), + test: func(t *testing.T, s *mcp.Service) { + r, err := s.GetRunResponse( + context.Background(), + mcp.RunInput{ + Code: `mokapi.getApi('foo')`, + }, + ) + require.NoError(t, err) + require.IsType(t, &mcp.Kafka{}, r.Result) + kafka := r.Result.(*mcp.Kafka) + require.Equal(t, "foo", kafka.Name) + require.Equal(t, "kafka", kafka.Type) + require.Equal(t, []mcp.Broker{{Name: "bar", Host: "foo.bar", Description: "server description"}}, kafka.Brokers) + }, + }, + { + name: "get topics", + app: runtimetest.NewKafkaApp( + asyncapi3test.NewConfig( + asyncapi3test.WithInfo("foo", "", ""), + asyncapi3test.WithChannel("channel-1", + asyncapi3test.WithChannelTitle("title-1"), + asyncapi3test.WithChannelSummary("channel-1 summary"), + ), + asyncapi3test.WithChannel("channel-2", + asyncapi3test.WithChannelTitle("title-2"), + ), + ), + asyncapi3test.NewConfig( + asyncapi3test.WithInfo("bar", "", ""), + ), + ), + test: func(t *testing.T, s *mcp.Service) { + r, err := s.GetRunResponse( + context.Background(), + mcp.RunInput{ + Code: `mokapi.getApi('foo').getTopics()`, + }, + ) + require.NoError(t, err) + require.IsType(t, []mcp.TopicSummary{}, r.Result) + topics := r.Result.([]mcp.TopicSummary) + require.Len(t, topics, 2) + require.Equal(t, "channel-1", topics[0].Name) + require.Equal(t, "title-1", topics[0].Title) + require.Equal(t, "channel-1 summary", topics[0].Summary) + require.Equal(t, "channel-2", topics[1].Name) + require.Equal(t, "title-2", topics[1].Title) + require.Equal(t, "", topics[1].Summary) + }, + }, + { + name: "get topic", + app: func() *runtime.App { + msg := asyncapi3test.NewMessage( + asyncapi3test.WithMessageName("msg-name-1"), + asyncapi3test.WithMessageTitle("msg-title-1"), + asyncapi3test.WithMessageSummary("msg-summary-1"), + asyncapi3test.WithMessageDescription("msg-description-1"), + asyncapi3test.WithContentType("application/json"), + asyncapi3test.WithPayload( + schematest.New("object", + schematest.WithProperty("foo", schematest.New("string")), + ), + ), + ) + + ch := asyncapi3test.NewChannel( + asyncapi3test.WithChannelTitle("title-1"), + asyncapi3test.WithChannelSummary("channel-1 summary"), + asyncapi3test.WithChannelDescription("description"), + asyncapi3test.UseMessage("foo", &asyncapi3.MessageRef{Value: msg}), + ) + + return runtimetest.NewKafkaApp( + asyncapi3test.NewConfig( + asyncapi3test.WithInfo("foo", "", ""), + asyncapi3test.AddChannel("channel-1", ch), + asyncapi3test.WithOperation("publish", + asyncapi3test.WithOperationAction("send"), + asyncapi3test.WithOperationTitle("op-title-1"), + asyncapi3test.WithOperationSummary("op-summary-1"), + asyncapi3test.WithOperationDescription("op-description-1"), + asyncapi3test.WithOperationChannel(ch), + asyncapi3test.UseOperationMessage(msg), + ), + asyncapi3test.WithOperation("consume", + asyncapi3test.WithOperationAction("receive"), + asyncapi3test.WithOperationChannel(ch), + asyncapi3test.UseOperationMessage(msg), + ), + ), + ) + }(), + test: func(t *testing.T, s *mcp.Service) { + r, err := s.GetRunResponse( + context.Background(), + mcp.RunInput{ + Code: `mokapi.getApi('foo').getTopic('channel-1')`, + }, + ) + require.NoError(t, err) + require.IsType(t, mcp.Topic{}, r.Result) + topic := r.Result.(mcp.Topic) + require.Equal(t, "channel-1", topic.Name) + require.Equal(t, "title-1", topic.Title) + require.Equal(t, "channel-1 summary", topic.Summary) + require.Equal(t, "description", topic.Description) + require.Len(t, topic.Operations, 2) + require.Equal(t, "send", topic.Operations[0].Action) + require.Equal(t, "op-title-1", topic.Operations[0].Title) + require.Equal(t, "op-summary-1", topic.Operations[0].Summary) + require.Equal(t, "op-description-1", topic.Operations[0].Description) + require.Len(t, topic.Operations[0].Messages, 1) + require.Equal(t, "msg-name-1", topic.Operations[0].Messages[0].Name) + require.Equal(t, "msg-title-1", topic.Operations[0].Messages[0].Title) + require.Equal(t, "msg-summary-1", topic.Operations[0].Messages[0].Summary) + require.Equal(t, "msg-description-1", topic.Operations[0].Messages[0].Description) + require.Equal(t, "application/json", topic.Operations[0].Messages[0].ContentType) + require.IsType(t, &jsonSchema.Schema{}, topic.Operations[0].Messages[0].Payload) + payload := topic.Operations[0].Messages[0].Payload.(*jsonSchema.Schema) + require.Equal(t, "object", payload.Type.String()) + + require.Equal(t, "receive", topic.Operations[1].Action) + }, + }, + { + name: "consume from topic", + app: func() *runtime.App { + msg := asyncapi3test.NewMessage( + asyncapi3test.WithMessageName("msg-name-1"), + asyncapi3test.WithMessageTitle("msg-title-1"), + asyncapi3test.WithMessageSummary("msg-summary-1"), + asyncapi3test.WithMessageDescription("msg-description-1"), + asyncapi3test.WithContentType("application/json"), + asyncapi3test.WithPayload( + schematest.New("object", + schematest.WithProperty("foo", schematest.New("string")), + ), + ), + ) + + ch := asyncapi3test.NewChannel( + asyncapi3test.WithChannelTitle("title-1"), + asyncapi3test.WithChannelSummary("channel-1 summary"), + asyncapi3test.WithChannelDescription("description"), + asyncapi3test.UseMessage("foo", &asyncapi3.MessageRef{Value: msg}), + ) + + app := runtimetest.NewKafkaApp( + asyncapi3test.NewConfig( + asyncapi3test.WithInfo("foo", "", ""), + asyncapi3test.AddChannel("channel-1", ch), + asyncapi3test.WithOperation("publish", + asyncapi3test.WithOperationAction("send"), + asyncapi3test.WithOperationTitle("op-title-1"), + asyncapi3test.WithOperationSummary("op-summary-1"), + asyncapi3test.WithOperationDescription("op-description-1"), + asyncapi3test.WithOperationChannel(ch), + asyncapi3test.UseOperationMessage(msg), + ), + asyncapi3test.WithOperation("consume", + asyncapi3test.WithOperationAction("receive"), + asyncapi3test.WithOperationChannel(ch), + asyncapi3test.UseOperationMessage(msg), + ), + ), + ) + + _, err := app.Kafka.Get("foo").Store.Topic("channel-1").Partition(0).Write( + kafka.RecordBatch{ + Records: []*kafka.Record{ + { + Offset: 0, + Time: time.Time{}, + Key: kafka.NewBytes([]byte("foo")), + Value: kafka.NewBytes([]byte(`{"foo":"bar"}`)), + Headers: nil, + }, + }, + }, + ) + require.NoError(t, err) + + return app + }(), + test: func(t *testing.T, s *mcp.Service) { + r, err := s.GetRunResponse( + context.Background(), + mcp.RunInput{ + Code: `mokapi.getApi('foo').getTopic('channel-1').consume(0, 0, 10)`, + }, + ) + require.NoError(t, err) + require.IsType(t, []mcp.KafkaRecord{}, r.Result) + records := r.Result.([]mcp.KafkaRecord) + require.Len(t, records, 1) + require.Equal(t, int64(0), records[0].Offset) + require.Equal(t, "foo", records[0].Key) + require.Equal(t, `{"foo":"bar"}`, records[0].Value) + }, + }, + { + name: "produce into topic", + app: func() *runtime.App { + msg := asyncapi3test.NewMessage( + asyncapi3test.WithMessageName("msg-name-1"), + asyncapi3test.WithMessageTitle("msg-title-1"), + asyncapi3test.WithMessageSummary("msg-summary-1"), + asyncapi3test.WithMessageDescription("msg-description-1"), + asyncapi3test.WithContentType("application/json"), + asyncapi3test.WithPayload( + schematest.New("object", + schematest.WithProperty("foo", schematest.New("string")), + ), + ), + ) + + ch := asyncapi3test.NewChannel( + asyncapi3test.WithChannelTitle("title-1"), + asyncapi3test.WithChannelSummary("channel-1 summary"), + asyncapi3test.WithChannelDescription("description"), + asyncapi3test.UseMessage("foo", &asyncapi3.MessageRef{Value: msg}), + ) + + return runtimetest.NewKafkaApp( + asyncapi3test.NewConfig( + asyncapi3test.WithInfo("foo", "", ""), + asyncapi3test.AddChannel("channel-1", ch), + asyncapi3test.WithOperation("publish", + asyncapi3test.WithOperationAction("send"), + asyncapi3test.WithOperationTitle("op-title-1"), + asyncapi3test.WithOperationSummary("op-summary-1"), + asyncapi3test.WithOperationDescription("op-description-1"), + asyncapi3test.WithOperationChannel(ch), + asyncapi3test.UseOperationMessage(msg), + ), + asyncapi3test.WithOperation("consume", + asyncapi3test.WithOperationAction("receive"), + asyncapi3test.WithOperationChannel(ch), + asyncapi3test.UseOperationMessage(msg), + ), + ), + ) + }(), + test: func(t *testing.T, s *mcp.Service) { + r, err := s.GetRunResponse( + context.Background(), + mcp.RunInput{ + Code: `const t = mokapi.getApi('foo').getTopic('channel-1') +t.produce(0, '{"foo":"bar"}', 'foo'); +t`, + }, + ) + require.NoError(t, err) + require.IsType(t, mcp.Topic{}, r.Result) + topic := r.Result.(mcp.Topic) + require.Len(t, topic.Partitions, 1) + require.Equal(t, int64(1), topic.Partitions[0].Offset) + }, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + generator.Seed(123456) + + s := mcp.NewService(tc.app) + tc.test(t, s) + }) + } +} diff --git a/mcp/run_test.go b/mcp/run_test.go index 7bbf98d5a..bf28489be 100644 --- a/mcp/run_test.go +++ b/mcp/run_test.go @@ -2,15 +2,11 @@ package mcp_test import ( "context" - "encoding/json" "mokapi/mcp" - "mokapi/providers/openapi" "mokapi/providers/openapi/openapitest" - "mokapi/providers/openapi/schema/schematest" "mokapi/runtime" "mokapi/runtime/runtimetest" "mokapi/schema/json/generator" - "net/http" "testing" "github.com/stretchr/testify/require" @@ -66,429 +62,6 @@ func TestService_Run(t *testing.T) { require.Len(t, r.Result, 0) }, }, - { - name: "get HTTP APIs", - app: runtimetest.NewHttpApp( - openapitest.NewConfig("3.1.0", - openapitest.WithInfo("foo", "", ""), - ), - openapitest.NewConfig("3.1.0", - openapitest.WithInfo("bar", "", ""), - ), - ), - test: func(t *testing.T, s *mcp.Service) { - r, err := s.GetRunResponse( - context.Background(), - mcp.RunInput{ - Code: `mokapi.getApis()`, - }, - ) - require.NoError(t, err) - require.Equal(t, []mcp.ApiSummary{ - {Name: "bar", Type: "http"}, - {Name: "foo", Type: "http"}, - }, r.Result) - }, - }, - { - name: "get specific HTTP API", - app: runtimetest.NewHttpApp( - openapitest.NewConfig("3.1.0", - openapitest.WithInfo("foo", "", ""), - ), - openapitest.NewConfig("3.1.0", - openapitest.WithInfo("bar", "", ""), - ), - ), - test: func(t *testing.T, s *mcp.Service) { - r, err := s.GetRunResponse( - context.Background(), - mcp.RunInput{ - Code: `mokapi.getApi('foo')`, - }, - ) - require.NoError(t, err) - b, err := json.Marshal(r.Result) - require.NoError(t, err) - require.Equal(t, `{"name":"foo","type":"http"}`, string(b)) - }, - }, - { - name: "get API's operation list", - app: runtimetest.NewHttpApp( - openapitest.NewConfig("3.1.0", - openapitest.WithInfo("foo", "", ""), - openapitest.WithPath("/pets", - openapitest.WithOperation(http.MethodGet, - openapitest.WithOperationId("pets"), - openapitest.WithOperationSummary("GET summary"), - ), - openapitest.WithOperation(http.MethodPut, - openapitest.WithOperationSummary("PUT summary"), - ), - ), - openapitest.WithPath("/users", - openapitest.WithPathInfo("path summary", ""), - openapitest.WithPathParam("foo"), - openapitest.WithOperation(http.MethodPost, openapitest.WithOperationParam("bar", false)), - ), - ), - ), - test: func(t *testing.T, s *mcp.Service) { - r, err := s.GetRunResponse( - context.Background(), - mcp.RunInput{ - Code: `mokapi.getApi('foo').getOperations()`, - }, - ) - require.NoError(t, err) - require.Equal(t, []mcp.OperationSummary{ - {Id: "pets", Method: "GET", Path: "/pets", Summary: "GET summary"}, - {Id: "put-/pets", Method: "PUT", Path: "/pets", Summary: "PUT summary"}, - {Id: "post-/users", Method: "POST", Path: "/users", Summary: "path summary", Parameters: []string{"bar", "foo"}}}, - r.Result) - }, - }, - { - name: "get API's operation details", - app: runtimetest.NewHttpApp( - openapitest.NewConfig("3.1.0", - openapitest.WithInfo("foo", "", ""), - openapitest.WithPath("/pets", - openapitest.WithOperation(http.MethodGet, - openapitest.WithOperationSummary("GET summary"), - openapitest.WithHeaderParam("foo", false, openapitest.WithParamSchema(schematest.New("string"))), - openapitest.WithRequestBody( - "request body description", - true, - openapitest.WithRequestContent( - "application/json", - &openapi.MediaType{ - Schema: schematest.New("string"), - }, - ), - ), - openapitest.WithResponse(200, - openapitest.WithContent("application/json", openapitest.WithSchema( - schematest.New("string"), - )), - ), - ), - openapitest.WithOperation(http.MethodPut, - openapitest.WithOperationSummary("PUT summary"), - ), - ), - ), - ), - test: func(t *testing.T, s *mcp.Service) { - r, err := s.GetRunResponse( - context.Background(), - mcp.RunInput{ - Code: `mokapi.getApi('foo').getOperation('get-/pets')`, - }, - ) - require.NoError(t, err) - require.IsType(t, &mcp.Operation{}, r.Result) - op := r.Result.(*mcp.Operation) - require.Equal(t, "get-/pets", op.OperationId) - require.Equal(t, "GET", op.Method) - require.Equal(t, "/pets", op.Path) - require.Equal(t, "GET summary", op.Summary) - require.Equal(t, "", op.Description) - require.Equal(t, "foo", op.Parameters[0].Name) - require.Equal(t, "header", op.Parameters[0].In) - require.Equal(t, false, op.Parameters[0].Required) - require.Equal(t, "string", op.Parameters[0].Schema.Type[0]) - require.Equal(t, "", op.Parameters[0].Description) - require.Equal(t, "request body description", op.RequestBody.Description) - require.Equal(t, true, op.RequestBody.Required) - require.Equal(t, "application/json", op.RequestBody.Contents[0].ContentType) - require.Equal(t, "string", op.RequestBody.Contents[0].Schema.Type[0]) - }, - }, - { - name: "get API's operation using projection", - app: runtimetest.NewHttpApp( - openapitest.NewConfig("3.1.0", - openapitest.WithInfo("foo", "", ""), - openapitest.WithPath("/pets", - openapitest.WithOperation(http.MethodGet, - openapitest.WithOperationSummary("GET summary"), - openapitest.WithHeaderParam("foo", false, openapitest.WithParamSchema(schematest.New("string"))), - openapitest.WithRequestBody( - "request body description", - true, - openapitest.WithRequestContent( - "application/json", - &openapi.MediaType{ - Schema: schematest.New("string"), - }, - ), - ), - openapitest.WithResponse(200, - openapitest.WithContent("application/json", openapitest.WithSchema( - schematest.New("string"), - )), - ), - ), - openapitest.WithOperation(http.MethodPut, - openapitest.WithOperationSummary("PUT summary"), - ), - ), - ), - ), - test: func(t *testing.T, s *mcp.Service) { - r, err := s.GetRunResponse( - context.Background(), - mcp.RunInput{ - Code: `const op = mokapi.getApi('foo').getOperation('get-/pets') -const result = { path: op.path, method: op.method }; result`, - }, - ) - require.NoError(t, err) - require.Equal(t, map[string]any{"method": "GET", "path": "/pets"}, r.Result) - }, - }, - { - name: "getResponseSchema", - app: runtimetest.NewHttpApp( - openapitest.NewConfig("3.1.0", - openapitest.WithInfo("foo", "", ""), - openapitest.WithPath("/pets", - openapitest.WithOperation(http.MethodGet, - openapitest.WithResponse(200, - openapitest.WithResponseDescription("response description"), - openapitest.WithContent("application/json", openapitest.WithSchema( - schematest.New("string"), - )), - ), - ), - ), - ), - ), - test: func(t *testing.T, s *mcp.Service) { - r, err := s.GetRunResponse( - context.Background(), - mcp.RunInput{ - Code: `const op = mokapi.getApi('foo').getOperation('get-/pets') -op.getResponseSchema(200)`, - }, - ) - require.NoError(t, err) - require.IsType(t, &mcp.Response{}, r.Result) - res := r.Result.(*mcp.Response) - require.Equal(t, 200, res.StatusCode) - require.Equal(t, "response description", res.Description) - require.Equal(t, "application/json", res.Content[0].ContentType) - require.Equal(t, "string", res.Content[0].Schema.Type[0]) - }, - }, - { - name: "getResponseSchema not exists", - app: runtimetest.NewHttpApp( - openapitest.NewConfig("3.1.0", - openapitest.WithInfo("foo", "", ""), - openapitest.WithPath("/pets", - openapitest.WithOperation(http.MethodGet, - openapitest.WithResponse(200, - openapitest.WithResponseDescription("response description"), - openapitest.WithContent("application/json", openapitest.WithSchema( - schematest.New("string"), - )), - ), - ), - ), - ), - ), - test: func(t *testing.T, s *mcp.Service) { - r, err := s.GetRunResponse( - context.Background(), - mcp.RunInput{ - Code: `const op = mokapi.getApi('foo').getOperation('get-/pets') -op.getResponseSchema(404)`, - }, - ) - require.NoError(t, err) - require.Nil(t, r.Result) - }, - }, - { - name: "invoke request", - app: runtimetest.NewHttpApp( - openapitest.NewConfig("3.1.0", - openapitest.WithInfo("foo", "", ""), - openapitest.WithPath("/pets", - openapitest.WithOperation(http.MethodGet, - openapitest.WithResponse(200, - openapitest.WithResponseDescription("response description"), - openapitest.WithContent("application/json", openapitest.WithSchema( - schematest.New("object", - schematest.WithProperty("foo", schematest.New("string")), - schematest.WithProperty("bar", schematest.New("integer")), - schematest.WithRequired("foo", "bar"), - ), - )), - ), - ), - ), - ), - ), - test: func(t *testing.T, s *mcp.Service) { - r, err := s.GetRunResponse( - context.Background(), - mcp.RunInput{ - Code: `const op = mokapi.getApi('foo').getOperation('get-/pets'); -op.invoke()`, - }, - ) - require.NoError(t, err) - require.IsType(t, mcp.InvokeResponse{}, r.Result) - res := r.Result.(mcp.InvokeResponse) - require.Equal(t, 200, res.StatusCode) - require.Equal(t, `{"foo":"P8","bar":-804702}`, res.Body) - require.Equal(t, map[string][]string{ - "Content-Type": {"application/json"}, - }, res.Headers) - }, - }, - { - name: "invoke request with path parameter", - app: runtimetest.NewHttpApp( - openapitest.NewConfig("3.1.0", - openapitest.WithInfo("foo", "", ""), - openapitest.WithPath("/{foo}/{bar}/pets", - openapitest.WithOperation(http.MethodGet, - openapitest.WithOperationParam("foo", true, openapitest.WithParamSchema(schematest.New("string"))), - openapitest.WithOperationParam("bar", true, openapitest.WithParamSchema(schematest.New("string"))), - openapitest.WithResponse(200, - openapitest.WithResponseDescription("response description"), - openapitest.WithContent("application/json", openapitest.WithSchema( - schematest.New("string"), - )), - ), - ), - ), - ), - ), - test: func(t *testing.T, s *mcp.Service) { - r, err := s.GetRunResponse( - context.Background(), - mcp.RunInput{ - Code: `const op = mokapi.getApi('foo').getOperation('get-/{foo}/{bar}/pets'); -op.invoke({ path: { foo: 'val1', 'bar': 'val2' }})`, - }, - ) - require.NoError(t, err) - res := r.Result.(mcp.InvokeResponse) - require.Equal(t, http.StatusOK, res.StatusCode, res.Body) - }, - }, - { - name: "invoke request with path parameter but not specified", - app: runtimetest.NewHttpApp( - openapitest.NewConfig("3.1.0", - openapitest.WithInfo("foo", "", ""), - openapitest.WithPath("/{foo}/{bar}/pets", - openapitest.WithOperation(http.MethodGet, - openapitest.WithOperationParam("foo", true, openapitest.WithParamSchema(schematest.New("string"))), - openapitest.WithOperationParam("bar", true, openapitest.WithParamSchema(schematest.New("string"))), - openapitest.WithResponse(200, - openapitest.WithResponseDescription("response description"), - openapitest.WithContent("application/json", openapitest.WithSchema( - schematest.New("string"), - )), - ), - ), - ), - ), - ), - test: func(t *testing.T, s *mcp.Service) { - _, err := s.GetRunResponse( - context.Background(), - mcp.RunInput{ - Code: `const op = mokapi.getApi('foo').getOperation('get-/{foo}/{bar}/pets'); -op.invoke()`, - }, - ) - require.EqualError(t, err, "invoke request GET /{foo}/{bar}/pets failed: missing path parameter 'foo'") - }, - }, - { - name: "invoke request with query parameter", - app: runtimetest.NewHttpApp( - openapitest.NewConfig("3.1.0", - openapitest.WithInfo("foo", "", ""), - openapitest.WithPath("/pets", - openapitest.WithOperation(http.MethodGet, - openapitest.WithQueryParam("foo", true, openapitest.WithParamSchema(schematest.New("string"))), - openapitest.WithResponse(200, - openapitest.WithResponseDescription("response description"), - openapitest.WithContent("application/json", openapitest.WithSchema( - schematest.New("string"), - )), - ), - ), - ), - ), - ), - test: func(t *testing.T, s *mcp.Service) { - r, err := s.GetRunResponse( - context.Background(), - mcp.RunInput{ - Code: `const op = mokapi.getApi('foo').getOperation('get-/pets'); -op.invoke({ query: { foo: 'val1' }})`, - }, - ) - require.NoError(t, err) - res := r.Result.(mcp.InvokeResponse) - require.Equal(t, http.StatusOK, res.StatusCode, res.Body) - }, - }, - { - name: "invoke request with header parameter", - app: runtimetest.NewHttpApp( - openapitest.NewConfig("3.1.0", - openapitest.WithInfo("foo", "", ""), - openapitest.WithPath("/pets", - openapitest.WithOperation(http.MethodGet, - openapitest.WithHeaderParam("foo", true, openapitest.WithParamSchema(schematest.New("string"))), - openapitest.WithResponse(200, - openapitest.WithResponseDescription("response description"), - openapitest.WithContent("application/json", openapitest.WithSchema( - schematest.New("string"), - )), - ), - ), - ), - ), - ), - test: func(t *testing.T, s *mcp.Service) { - r, err := s.GetRunResponse( - context.Background(), - mcp.RunInput{ - Code: `const op = mokapi.getApi('foo').getOperation('get-/pets'); -op.invoke({ header: { foo: ['val1'] }})`, - }, - ) - require.NoError(t, err) - res := r.Result.(mcp.InvokeResponse) - require.Equal(t, http.StatusOK, res.StatusCode, res.Body) - }, - }, - { - name: "fake", - app: runtimetest.NewApp(), - test: func(t *testing.T, s *mcp.Service) { - r, err := s.GetRunResponse( - context.Background(), - mcp.RunInput{ - Code: `mokapi.fake({ type: 'string', format: 'email' })`, - }, - ) - require.NoError(t, err) - require.Equal(t, "ivyjones@ziemann.com", r.Result) - }, - }, } for _, tc := range testcases { diff --git a/providers/asyncapi3/asyncapi3test/channel.go b/providers/asyncapi3/asyncapi3test/channel.go index 8636991bb..194f46e3b 100644 --- a/providers/asyncapi3/asyncapi3test/channel.go +++ b/providers/asyncapi3/asyncapi3test/channel.go @@ -43,6 +43,18 @@ func WithKafkaChannelBinding(bindings asyncapi3.TopicBindings) ChannelOptions { } } +func WithChannelTitle(title string) ChannelOptions { + return func(c *asyncapi3.Channel) { + c.Title = title + } +} + +func WithChannelSummary(summary string) ChannelOptions { + return func(c *asyncapi3.Channel) { + c.Summary = summary + } +} + func WithChannelDescription(desc string) ChannelOptions { return func(c *asyncapi3.Channel) { c.Description = desc diff --git a/providers/asyncapi3/asyncapi3test/message.go b/providers/asyncapi3/asyncapi3test/message.go index ca58b35a4..c5df43c55 100644 --- a/providers/asyncapi3/asyncapi3test/message.go +++ b/providers/asyncapi3/asyncapi3test/message.go @@ -73,6 +73,12 @@ func WithHeaders(s *schema.Schema) MessageOptions { } } +func WithMessageName(s string) MessageOptions { + return func(m *asyncapi3.Message) { + m.Name = s + } +} + func WithMessageTitle(s string) MessageOptions { return func(m *asyncapi3.Message) { m.Title = s @@ -84,3 +90,9 @@ func WithMessageSummary(s string) MessageOptions { m.Summary = s } } + +func WithMessageDescription(s string) MessageOptions { + return func(m *asyncapi3.Message) { + m.Description = s + } +} diff --git a/providers/asyncapi3/asyncapi3test/operation.go b/providers/asyncapi3/asyncapi3test/operation.go index 412693e77..d8dbf6222 100644 --- a/providers/asyncapi3/asyncapi3test/operation.go +++ b/providers/asyncapi3/asyncapi3test/operation.go @@ -20,6 +20,24 @@ func WithOperationAction(action string) OperationOptions { } } +func WithOperationTitle(title string) OperationOptions { + return func(op *asyncapi3.Operation) { + op.Title = title + } +} + +func WithOperationSummary(summary string) OperationOptions { + return func(op *asyncapi3.Operation) { + op.Summary = summary + } +} + +func WithOperationDescription(description string) OperationOptions { + return func(op *asyncapi3.Operation) { + op.Description = description + } +} + func UseOperationMessage(msg *asyncapi3.Message) OperationOptions { return func(o *asyncapi3.Operation) { o.Messages = append(o.Messages, &asyncapi3.MessageRef{Value: msg}) diff --git a/providers/asyncapi3/kafka/store/partition.go b/providers/asyncapi3/kafka/store/partition.go index 83a13bc47..6a90b65b0 100644 --- a/providers/asyncapi3/kafka/store/partition.go +++ b/providers/asyncapi3/kafka/store/partition.go @@ -115,8 +115,8 @@ func (p *Partition) Read(offset int64, maxBytes int) (kafka.RecordBatch, kafka.E return batch, kafka.None } - for seg.contains(offset) { - r := seg.record(offset) + for seg.Contains(offset) { + r := seg.Record(offset) if baseOffset == 0 { baseOffset = r.Offset @@ -281,7 +281,7 @@ func (p *Partition) OffsetTimestamp(offset int64) int64 { if s == nil { return -1 } - r := s.record(offset) + r := s.Record(offset) if r == nil { return -1 } @@ -356,11 +356,11 @@ func newSegment(offset int64) *Segment { } } -func (s *Segment) contains(offset int64) bool { +func (s *Segment) Contains(offset int64) bool { return offset >= s.Head && offset < s.Tail } -func (s *Segment) record(offset int64) *kafka.Record { +func (s *Segment) Record(offset int64) *kafka.Record { index := int(offset - s.Head) if index < 0 || index >= len(s.Log) { return nil From 6ccb2bef022c88f0f401ec8dacb94fcc36fdd88c Mon Sep 17 00:00:00 2001 From: marle3003 Date: Tue, 21 Apr 2026 14:40:38 +0200 Subject: [PATCH 42/46] fix(kafka): fix converting AsyncAPI 2.0 to 3.0 test(mcp): improve test stability (map is not ordered --- config/dynamic/asyncApi/convert_test.go | 7 +- config/dynamic/asyncApi/parsing.go | 25 ++++-- config/dynamic/asyncApi/parsing_test.go | 29 ++++--- config/dynamic/resolve.go | 16 ---- mcp/run_kafka.go | 76 +++++++++++-------- mcp/run_kafka_test.go | 29 +++---- .../asyncapi3/kafka/store/partition_test.go | 2 +- providers/asyncapi3/schema.go | 9 +-- runtime/runtime_kafka.go | 14 +++- schema/json/schema/string.go | 4 + 10 files changed, 124 insertions(+), 87 deletions(-) diff --git a/config/dynamic/asyncApi/convert_test.go b/config/dynamic/asyncApi/convert_test.go index 4a960eb3e..92c868ac6 100644 --- a/config/dynamic/asyncApi/convert_test.go +++ b/config/dynamic/asyncApi/convert_test.go @@ -27,7 +27,8 @@ func TestConfig_Convert(t *testing.T) { err = cfg.Parse(c, &dynamictest.Reader{}) require.NoError(t, err) - cfg3 := c.Data.(*asyncapi3.Config) + cfg3, err := c.Data.(*asyncApi.Config).Convert() + require.NoError(t, err) require.Equal(t, "3.0.0", cfg3.Version) require.Equal(t, "urn:example:com:smartylighting:streetlights:server", cfg3.Id) @@ -211,9 +212,9 @@ func TestConvert(t *testing.T) { c := &dynamic.Config{Data: tc.cfg} err := tc.cfg.Parse(c, &dynamictest.Reader{}) require.NoError(t, err) - require.IsType(t, &asyncapi3.Config{}, c.Data) - tc.test(t, c.Data.(*asyncapi3.Config), err) + cfg, err := tc.cfg.Convert() + tc.test(t, cfg, err) }) } } diff --git a/config/dynamic/asyncApi/parsing.go b/config/dynamic/asyncApi/parsing.go index 838f0136e..dc93f500a 100644 --- a/config/dynamic/asyncApi/parsing.go +++ b/config/dynamic/asyncApi/parsing.go @@ -11,12 +11,27 @@ func (c *Config) Parse(config *dynamic.Config, reader dynamic.Reader) error { return nil } - converted, err := c.Convert() - if err != nil { - return err + for _, server := range c.Servers { + if server == nil || len(server.Ref) == 0 { + continue + } + resolved, err := server.Resolve(config, reader) + if err != nil { + return err + } + server.Value = resolved.Value } - config.Data = converted - return converted.Parse(config, reader) + + for _, ch := range c.Channels { + if ch == nil { + continue + } + if err := ch.Parse(config, reader); err != nil { + return err + } + } + + return nil } func (r *ChannelRef) Parse(config *dynamic.Config, reader dynamic.Reader) error { diff --git a/config/dynamic/asyncApi/parsing_test.go b/config/dynamic/asyncApi/parsing_test.go index af22e2f97..c92015dcc 100644 --- a/config/dynamic/asyncApi/parsing_test.go +++ b/config/dynamic/asyncApi/parsing_test.go @@ -146,8 +146,12 @@ func TestServerResolve(t *testing.T) { reader := &testReader{readFunc: tc.read} c := &dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: tc.cfg} err := tc.cfg.Parse(c, reader) - require.IsType(t, &asyncapi3.Config{}, c.Data) - tc.test(t, c.Data.(*asyncapi3.Config), err) + if err != nil { + tc.test(t, nil, err) + } else { + cfg, err := tc.cfg.Convert() + tc.test(t, cfg, err) + } }) } } @@ -238,9 +242,12 @@ func TestChannelResolve(t *testing.T) { reader := &testReader{readFunc: tc.read} c := &dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: tc.cfg} err := tc.cfg.Parse(c, reader) - require.IsType(t, &asyncapi3.Config{}, c.Data) - - tc.test(t, c.Data.(*asyncapi3.Config), err) + if err != nil { + tc.test(t, nil, err) + } else { + cfg, err := tc.cfg.Convert() + tc.test(t, cfg, err) + } }) } } @@ -366,8 +373,12 @@ func TestMessage(t *testing.T) { reader := &testReader{readFunc: tc.read} c := &dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: tc.cfg} err := tc.cfg.Parse(c, reader) - require.IsType(t, &asyncapi3.Config{}, c.Data) - tc.test(t, c.Data.(*asyncapi3.Config), err) + if err != nil { + tc.test(t, nil, err) + } else { + cfg, err := tc.cfg.Convert() + tc.test(t, cfg, err) + } }) } } @@ -402,8 +413,8 @@ func TestSchema(t *testing.T) { c := &dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: config} err := config.Parse(c, reader) require.NoError(t, err) - require.IsType(t, &asyncapi3.Config{}, c.Data) - s, err := c.Data.(*asyncapi3.Config).Channels["foo"].Value.Messages["publish"].Value.Payload.GetSchema() + cfg, err := config.Convert() + s, err := cfg.Channels["foo"].Value.Messages["publish"].Value.Payload.GetSchema() require.NoError(t, err) require.Equal(t, target, s) }) diff --git a/config/dynamic/resolve.go b/config/dynamic/resolve.go index 239248f89..fb93f8167 100644 --- a/config/dynamic/resolve.go +++ b/config/dynamic/resolve.go @@ -19,10 +19,6 @@ type Converter interface { ConvertTo(i any) (any, error) } -type FromConverter interface { - ConvertFrom(i any) (any, error) -} - func resolve[T any](ref string, config *Config, reader Reader) (T, error) { var err error var result T @@ -351,17 +347,5 @@ func convert[T any](val any) any { } } - v := reflect.ValueOf(target) - if !v.IsValid() || !v.CanInterface() { - return val - } - c, ok := v.Interface().(FromConverter) - if !ok { - return val - } - result, err := c.ConvertFrom(val) - if err == nil { - return result - } return val } diff --git a/mcp/run_kafka.go b/mcp/run_kafka.go index 1d31db39f..ca2474e89 100644 --- a/mcp/run_kafka.go +++ b/mcp/run_kafka.go @@ -2,6 +2,8 @@ package mcp import ( "fmt" + "mokapi/engine" + "mokapi/engine/common" "mokapi/kafka" "mokapi/runtime" "slices" @@ -14,7 +16,8 @@ type Kafka struct { Type string `json:"type"` Brokers []Broker `json:"brokers"` - info *runtime.KafkaInfo + info *runtime.KafkaInfo + client *engine.KafkaClient } type Broker struct { @@ -36,7 +39,8 @@ type Topic struct { Operations []KafkaOperation `json:"operations,omitempty"` - info *runtime.KafkaInfo + info *runtime.KafkaInfo + client *engine.KafkaClient } type KafkaPartition struct { @@ -74,9 +78,10 @@ func (m *mokapi) getKafkaApi(name string) any { for _, api := range m.app.Kafka.List() { if api.Info.Name == name { result := &Kafka{ - Name: name, - Type: "kafka", - info: api, + Name: name, + Type: "kafka", + info: api, + client: engine.NewKafkaClient(m.app), } for it := api.Servers.Iter(); it.Next(); { b := it.Value() @@ -128,6 +133,7 @@ func (k *Kafka) GetTopic(name string) (Topic, error) { }, Description: ch.Value.Description, info: k.info, + client: k.client, } topic := k.info.Store.Topic(name) @@ -180,45 +186,55 @@ func (k *Kafka) GetTopic(name string) (Topic, error) { t.Operations = append(t.Operations, result) } + slices.SortStableFunc(t.Operations, func(a, b KafkaOperation) int { + r := strings.Compare(a.Action, b.Action) + if r != 0 { + return r + } + return strings.Compare(a.Title, b.Title) + }) + return t, nil } func (t *Topic) Produce(partition int, value string, key string, headers map[string]string) error { - topic := t.info.Store.Topic(t.Name) - if topic == nil { - return fmt.Errorf("topic '%s' not found", t.Name) - } - p := topic.Partition(partition) - if p == nil { - return fmt.Errorf("partition '%d' not found", partition) - } - - r := &kafka.Record{ - Time: time.Now(), - Key: kafka.NewBytes([]byte(key)), - Value: kafka.NewBytes([]byte(value)), + msg := common.KafkaMessage{ + Value: []byte(value), + Data: nil, + Headers: headers, + Partition: partition, } - if headers != nil { - for k, v := range headers { - r.Headers = append(r.Headers, kafka.RecordHeader{ - Key: k, - Value: []byte(v), - }) - } + if key != "" { + msg.Key = []byte(key) } - result, err := p.Write(kafka.RecordBatch{ - Records: []*kafka.Record{r}, + _, err := t.client.Produce(&common.KafkaProduceArgs{ + Cluster: t.info.Info.Name, + Topic: t.Name, + Messages: []common.KafkaMessage{msg}, + Retry: common.KafkaProduceRetry{ + MaxRetryTime: 3 * time.Minute, + InitialRetryTime: 500 * time.Millisecond, + Retries: 10, + Factor: 2, + }, + ClientId: "mokapi-mcp", }) if err != nil { return err } - if result.ErrorCode != kafka.None { - return fmt.Errorf("%d: %s", result.ErrorCode, result.ErrorMessage) + + topic := t.info.Store.Topic(t.Name) + if topic == nil { + return fmt.Errorf("topic '%s' not found", t.Name) + } + p := topic.Partition(partition) + if p == nil { + return fmt.Errorf("partition '%s' not found", t.Name) } for _, pt := range t.Partitions { if pt.Index == p.Index { - pt.Offset += 1 + pt.Offset = p.Offset() } } return nil diff --git a/mcp/run_kafka_test.go b/mcp/run_kafka_test.go index 87f2f0dfd..0584edfc3 100644 --- a/mcp/run_kafka_test.go +++ b/mcp/run_kafka_test.go @@ -167,21 +167,22 @@ func TestService_Run_Kafka(t *testing.T) { require.Equal(t, "channel-1 summary", topic.Summary) require.Equal(t, "description", topic.Description) require.Len(t, topic.Operations, 2) - require.Equal(t, "send", topic.Operations[0].Action) - require.Equal(t, "op-title-1", topic.Operations[0].Title) - require.Equal(t, "op-summary-1", topic.Operations[0].Summary) - require.Equal(t, "op-description-1", topic.Operations[0].Description) - require.Len(t, topic.Operations[0].Messages, 1) - require.Equal(t, "msg-name-1", topic.Operations[0].Messages[0].Name) - require.Equal(t, "msg-title-1", topic.Operations[0].Messages[0].Title) - require.Equal(t, "msg-summary-1", topic.Operations[0].Messages[0].Summary) - require.Equal(t, "msg-description-1", topic.Operations[0].Messages[0].Description) - require.Equal(t, "application/json", topic.Operations[0].Messages[0].ContentType) - require.IsType(t, &jsonSchema.Schema{}, topic.Operations[0].Messages[0].Payload) - payload := topic.Operations[0].Messages[0].Payload.(*jsonSchema.Schema) - require.Equal(t, "object", payload.Type.String()) - require.Equal(t, "receive", topic.Operations[1].Action) + require.Equal(t, "receive", topic.Operations[0].Action) + + require.Equal(t, "send", topic.Operations[1].Action) + require.Equal(t, "op-title-1", topic.Operations[1].Title) + require.Equal(t, "op-summary-1", topic.Operations[1].Summary) + require.Equal(t, "op-description-1", topic.Operations[1].Description) + require.Len(t, topic.Operations[1].Messages, 1) + require.Equal(t, "msg-name-1", topic.Operations[1].Messages[0].Name) + require.Equal(t, "msg-title-1", topic.Operations[1].Messages[0].Title) + require.Equal(t, "msg-summary-1", topic.Operations[1].Messages[0].Summary) + require.Equal(t, "msg-description-1", topic.Operations[1].Messages[0].Description) + require.Equal(t, "application/json", topic.Operations[1].Messages[0].ContentType) + require.IsType(t, &jsonSchema.Schema{}, topic.Operations[1].Messages[0].Payload) + payload := topic.Operations[1].Messages[0].Payload.(*jsonSchema.Schema) + require.Equal(t, "object", payload.Type.String()) }, }, { diff --git a/providers/asyncapi3/kafka/store/partition_test.go b/providers/asyncapi3/kafka/store/partition_test.go index b43cb61db..cad87a87a 100644 --- a/providers/asyncapi3/kafka/store/partition_test.go +++ b/providers/asyncapi3/kafka/store/partition_test.go @@ -210,7 +210,7 @@ func TestPartition_Write_Value_Validator(t *testing.T) { require.Equal(t, int64(0), wr.BaseOffset) require.Equal(t, int64(1), p.Offset()) require.Equal(t, int64(0), p.StartOffset()) - r := p.Segments[p.ActiveSegment].record(0) + r := p.Segments[p.ActiveSegment].Record(0) require.NotNil(t, r) require.Len(t, r.Headers, 1) require.Equal(t, "bar-1", r.Headers[0].Key) diff --git a/providers/asyncapi3/schema.go b/providers/asyncapi3/schema.go index 35cf212c2..f66c3e568 100644 --- a/providers/asyncapi3/schema.go +++ b/providers/asyncapi3/schema.go @@ -118,13 +118,6 @@ func (r *SchemaRef) Marshal(v any, ct media.ContentType) ([]byte, error) { } } -//func (r *SchemaRef) ConvertFrom(val any) (any, error) { -// if s, ok := val.(Schema); ok { -// return &SchemaRef{Value: s}, nil -// } -// return nil, fmt.Errorf("unsupported type: %T", val) -//} - func (r *SchemaRef) GetSchema() (Schema, error) { if r.Value == nil { return nil, nil @@ -134,7 +127,7 @@ func (r *SchemaRef) GetSchema() (Schema, error) { case *jsonSchema.Schema, *avro.Schema, *openapi.Schema: return s, nil case *SchemaRef: - return r.Value, nil + return s.GetSchema() case *MultiSchemaFormat: return s.Schema.GetSchema() default: diff --git a/runtime/runtime_kafka.go b/runtime/runtime_kafka.go index a4104a91c..e3c8738a1 100644 --- a/runtime/runtime_kafka.go +++ b/runtime/runtime_kafka.go @@ -2,6 +2,7 @@ package runtime import ( "mokapi/config/dynamic" + "mokapi/config/dynamic/asyncApi" "mokapi/config/static" "mokapi/engine/common" "mokapi/kafka" @@ -225,13 +226,24 @@ func IsAsyncApiConfig(c *dynamic.Config) (*asyncapi3.Config, bool) { switch v := c.Data.(type) { case *asyncapi3.Config: return v, true + case *asyncApi.Config: + conv, err := v.Convert() + if err != nil { + log.Errorf("failed to convert asyncapi 2.0 config: %s", err) + return nil, false + } + return conv, true default: return nil, false } } func getKafkaConfig(c *dynamic.Config) *asyncapi3.Config { - return c.Data.(*asyncapi3.Config) + cfg, ok := IsAsyncApiConfig(c) + if !ok { + return nil + } + return cfg } func (s *KafkaStore) updateEventStore(k *KafkaInfo) { diff --git a/schema/json/schema/string.go b/schema/json/schema/string.go index 6a2aba7f2..91597e62f 100644 --- a/schema/json/schema/string.go +++ b/schema/json/schema/string.go @@ -6,6 +6,10 @@ import ( ) func (s *Schema) String() string { + if s == nil { + return "" + } + var sb strings.Builder if s.Boolean != nil { From d082040f7193a952be7257829bac73a5b121d35b Mon Sep 17 00:00:00 2001 From: marle3003 Date: Tue, 21 Apr 2026 15:09:26 +0200 Subject: [PATCH 43/46] fix(mcp): improve log output test(imap): disable test which is unstable on GitHub --- imap/idle_test.go | 3 ++- pkg/cmd/mokapi/mokapi.go | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/imap/idle_test.go b/imap/idle_test.go index 0822383a3..7b758d9ec 100644 --- a/imap/idle_test.go +++ b/imap/idle_test.go @@ -176,7 +176,8 @@ func TestIdle(t *testing.T) { } } -func TestSendUpdatesWhileIdle(t *testing.T) { +// Test is not running stable on GitHub +func _TestSendUpdatesWhileIdle(t *testing.T) { p := try.GetFreePort() sent := make(chan bool) s := &imap.Server{ diff --git a/pkg/cmd/mokapi/mokapi.go b/pkg/cmd/mokapi/mokapi.go index c9f18d61a..fc392d6af 100644 --- a/pkg/cmd/mokapi/mokapi.go +++ b/pkg/cmd/mokapi/mokapi.go @@ -168,7 +168,6 @@ func createServer(cfg *static.Config) (*server.Server, error) { if cfg.Mcp.Server.Enabled { if u, err := mcp.BuildUrl(cfg.Mcp.Server); err == nil { if cfg.Api.Port == cfg.Mcp.Server.Port { - log.Infof("MCP server runs on %s:%d", cfg.Mcp.Server.Path, cfg.Mcp.Server.Port) apiHandler.RegisterMcpHandler(u.Path, mcp.NewServer(app)) } else { err = http.AddInternalService("mcp", u, mcp.NewServer(app)) @@ -176,6 +175,7 @@ func createServer(cfg *static.Config) (*server.Server, error) { return nil, err } } + log.Infof("MCP server runs on binding :%d on path %s", cfg.Mcp.Server.Port, cfg.Mcp.Server.Path) } } From 8dd338f916625ded27c3f05b7efb5b5e25798aa2 Mon Sep 17 00:00:00 2001 From: marle3003 Date: Tue, 21 Apr 2026 19:48:18 +0200 Subject: [PATCH 44/46] wip(mcp): improve agent guiding --- mcp/data/automation-core.md | 204 +++++++++ mcp/data/automation-event.md | 67 +++ mcp/data/automation-http.md | 90 ++++ mcp/data/automation-kafka.md | 114 +++++ mcp/data/automation.md | 42 ++ mcp/{ => data}/conditional-response.md | 0 mcp/{ => data}/delay-latency.md | 0 mcp/{ => data}/dynamic-error-simulation.md | 0 mcp/{ => data}/dynamic-path-params.md | 0 .../forward-request-to-real-backend.md | 0 mcp/{ => data}/mock_reference.md | 0 mcp/{ => data}/mocking-types.md | 0 mcp/{ => data}/scenarios.md | 0 mcp/{ => data}/static-error-simulation.md | 0 mcp/get_automation_definitions.go | 59 ++- mcp/get_mock_reference.go | 6 +- mcp/resources.go | 18 +- mcp/run.go | 15 +- mcp/run.ts | 398 ------------------ mcp/run_http_test.go | 29 -- mcp/run_kafka_test.go | 21 + mcp/run_test.go | 19 + 22 files changed, 633 insertions(+), 449 deletions(-) create mode 100644 mcp/data/automation-core.md create mode 100644 mcp/data/automation-event.md create mode 100644 mcp/data/automation-http.md create mode 100644 mcp/data/automation-kafka.md create mode 100644 mcp/data/automation.md rename mcp/{ => data}/conditional-response.md (100%) rename mcp/{ => data}/delay-latency.md (100%) rename mcp/{ => data}/dynamic-error-simulation.md (100%) rename mcp/{ => data}/dynamic-path-params.md (100%) rename mcp/{ => data}/forward-request-to-real-backend.md (100%) rename mcp/{ => data}/mock_reference.md (100%) rename mcp/{ => data}/mocking-types.md (100%) rename mcp/{ => data}/scenarios.md (100%) rename mcp/{ => data}/static-error-simulation.md (100%) delete mode 100644 mcp/run.ts diff --git a/mcp/data/automation-core.md b/mcp/data/automation-core.md new file mode 100644 index 000000000..7e35fc17c --- /dev/null +++ b/mcp/data/automation-core.md @@ -0,0 +1,204 @@ +# Core Discovery API + +Access to the global mokapi object for infrastructure metadata. + +```typescript +/** + * GLOBAL: The 'mokapi' object is globally available in the Automation API. + * Use it directly in your script. + */ +declare const mokapi: Mokapi; + +interface Mokapi { + /** + * Returns all mocked APIs (lightweight, no schemas). + */ + getApis(): ApiSummary[]; + + /** + * Returns a specific API by name. + * @example + * getApi('Swagger Petstore') + */ + getApi(name: string): OpenApi | Kafka; + + /** + * Generate a random value from a JSON Schema. + * The generated data strictly matches the schema, including all required fields and correct types. + * Use this function to create complex random data or writing HTTP mock scripts + * @param schema The JSON scheme for which a random value is generated. + * @example + * fake({ type: 'string', format: 'email' }) + */ + fake(schema: Schema): any; + + /** + * Returns recorded events from Mokapi + * Use this function when the user asks: + * - What requests were made? + * - Why did my request fail? + * - Show recent API activity + * @param traits Filter events by traits + * @param limit Maximum number of events to return, default is 10 + * @example + * getEvents({ apiType: 'http', name: 'Swagger Petstore', path: '/pets' }) + */ + getEvents(traits: HttpTraits | KafkaTraits, limit?: number): Event[]; +} + +type ApiType = 'http' | 'kafka' | 'ldap' | 'mail' + +interface ApiSummary { + name: string; + type: ApiType; +} + +/** + * JSON Schema defines a JSON-based format for describing the structure of JSON data + * @example + * { + * "type": "string", + * "format": "email" + * } + */ +interface Schema { + /** + * Specifies the data type for a schema. + */ + type?: SchemaType | SchemaType[]; + + /** + * The enum keyword is used to restrict a value to a fixed set of values. + */ + enum?: any[]; + + /** + * The const keyword is used to restrict a value to a single value. + */ + const?: any; + + /** + * Contains a list of valid examples. + */ + examples?: any[]; + + /** + * Specifies a default value. + */ + default?: any; + + // Numbers + /** + * Restricts the number to a multiple of the given number + */ + multipleOf?: number; + + /** + * Restricts the number to a maximum number + */ + maximum?: number; + + /** + * Restricts the number to an exclusive maximum number + */ + exclusiveMaximum?: number; + + /** + * Restricts the number to a minimum number + */ + minimum?: number; + + /** + * Restricts the number to an exclusive minimum number + */ + exclusiveMinimum?: number; + + // Strings + /** + * Restricts the string to a maximum length + */ + maxLength?: number; + + /** + * Restricts the string to a minimum length + */ + minLength?: number; + + /** + * The pattern keyword is used to restrict a string to a particular regular expression. + */ + pattern?: string; + + /** + * The format keyword allows for basic semantic identification of certain kinds of string values that are commonly used. + */ + format?: string; + + // Arrays + /** + * Specifies the schema of the items in the array. + */ + items?: Schema; + + /** + * Restricts the array to have a maximum length + */ + maxItems?: number; + + /** + * Restricts the array to have a minimum length + */ + minItems?: number; + + /** + * Restricts the array to have unique items + */ + uniqueItems?: boolean; + + // Objects + /** + * Specifies the properties of an object + */ + properties?: { [name: string]: Schema }; + + /** + * Restricts the object to have a maximum of properties + */ + maxProperties?: number; + + /** + * Restricts the object to have a minimum of properties + */ + minProperties?: number; + + /** + * Specifies the required properties for an object + */ + required?: string[]; + + /** + * The additionalProperties keyword is used to control the handling of extra stuff, + * that is, properties whose names are not listed in the properties keyword or match + * any of the regular expressions in the patternProperties keyword. By default, any + * additional properties are allowed. + */ + additionalProperties?: boolean | Schema; + + /** + * A value must be valid against all the schemas + */ + allOf?: Schema[]; + + /** + * A value must be valid against any the schemas + */ + anyOf?: Schema[]; + + /** + * A value must be valid against exactly one the schemas + */ + oneOf?: Schema[]; +} + +type SchemaType = "object" | "array" | "number" | "integer" | "string" | "boolean" | "null"; +``` \ No newline at end of file diff --git a/mcp/data/automation-event.md b/mcp/data/automation-event.md new file mode 100644 index 000000000..8059710bd --- /dev/null +++ b/mcp/data/automation-event.md @@ -0,0 +1,67 @@ +# Events + +Definitions for debugging activity via getEvents() and traits. + +```typescript +interface Traits { + type?: ApiType; + name?: string; +} + +interface HttpTraits extends Traits { + /** + * Path value specified by the OpenAPI path + * @example /pet/{petId} + */ + path?: string + + /** + * Request method. + * @example GET + */ + method?: string +} + +interface KafkaTraits extends Traits { + /** + * Topic name specified by the AsyncAPI channel + * @example user.signedup + */ + topic?: string + + /** + * Partition in which message was written + */ + partition?: number + + /** + * Which client produced the message + * The value 'mokapi-script' indicates the message was produced by a Mokapi Script + * The value 'mokapi-mcp' indicates the messages was produced by MCP server + */ + clientId?: string +} + +interface Event { + /** + * ID of the event + */ + id: string; + + /** + * List of traits + */ + traits: Traits; + + /** + * The data of the event + */ + data: any + + /** + * Time of the event in the format RFC3339 + * @example 2026-07-21T17:32:28Z + */ + time: string +} +``` \ No newline at end of file diff --git a/mcp/data/automation-http.md b/mcp/data/automation-http.md new file mode 100644 index 000000000..6f901244f --- /dev/null +++ b/mcp/data/automation-http.md @@ -0,0 +1,90 @@ +# HTTP (OpenAPI) + +Interfaces for exploring HTTP endpoints, parameters, and status codes. + +```typescript +interface Http extends ApiSummary { + servers: { url: string, description: string } + + /** + * Returns all operations of this API. + */ + getOperations(): HttpOperationSummary[]; + + /** + * Returns details about specific operation + * Use id from the operation summary list + * @param id The id of the operation + */ + getOperation(id: string): HttpOperation +} + +interface HttpOperationSummary { + /** generated from method and path if missing in spec */ + id: string + method: string; + path: string; + summary: string; + /** Names of required parameters to help decide if this is the right endpoint */ + parameters?: string[] +} + +interface HttpOperation { + id: string; + path: string + method: string + summary: string + description: string; + parameters: HttpRequestParameter[] + requestBody: HttpRequestBody + /** + * List of allowed responses. + * IMPORTANT: You must only use these status codes for this operation! + */ + responses: HttpResponse[] + + /** + * Invoke this operation against the mocked API. + * @example operation.invoke({ path: { id: 1 }, body: JSON.stringify({ name: "test" }) }) + */ + invoke(request?: HttpInvokeRequest): HttpInvokeResponse; +} + +interface HttpRequestParameter { + name: string + in: 'path' | 'query' | 'headers' + required: boolean + schema: Schema + description?: string +} + +interface HttpRequestBody { + description: string + required: boolean + contents: Content[] +} + +interface HttpContent { + contentType: string + schema: Schema +} + +interface HttpResponse { + statusCode: number + description: string + contents: HttpContent[] +} + +interface HttpInvokeRequest { + path?: Record; + query?: Record; + header?: Record; + body?: string; +} + +interface HttpInvokeResponse { + statusCode: number; + headers: Record; + body: string +} +``` \ No newline at end of file diff --git a/mcp/data/automation-kafka.md b/mcp/data/automation-kafka.md new file mode 100644 index 000000000..46e75d23b --- /dev/null +++ b/mcp/data/automation-kafka.md @@ -0,0 +1,114 @@ +# Kafka (AsyncAPI) + +Interfaces for inspecting topics, partitions, and message history. + +```typescript +interface Kafka extends ApiSummary { + brokers: { name: string, host: string, description?: string } + + getTopics(): KafkaTopicSummary[] + getTopic(topicName: string): KafkaTopic +} + +interface KafkaTopicSummary { + /** + * The unique name of the topic. + */ + name: string + title?: string + summary?: string +} + +interface KafkaTopic extends KafkaTopicSummary { + description: string + /** + * List of current partitions and their maximum offsets. + * Use this to determine the range for the 'consume' method. + * The 'offset' represents the NEXT available position. + * (e.g., if offset is 10, messages 0-9 exist; the next message will be 10). + */ + partitions: { index: number, offset: number } + + operations: KafkaOperation[]; + + /** + * Use 'produce' to send a message to this topic. + * Check 'operations' with action 'send' for valid payloads. + * @param partition The target partition index. MUST be one of the indices listed in the 'partitions' array. + * @param value The message payload. If the operation specifies a JSON schema, provide this as a stringified object. + * @param key Optional message key. + * @param headers Optional metadata headers. + */ + produce(partition: number, value: string, key?: string, headers?: KafkaHeader): void + + /** + * INSPECT: Retrieves a specific record for analysis or verification. + * Use this to check if the mock has received or produced a specific message. + * @param partition The partition index (see 'partitions' list). + * @param startOffset The offset to start reading from (see 'partitions' for max offset). + * @param limit The maximum number of records to return in this call. + * @returns An array of records found starting from the startOffset. + */ + consume(partition: number, startOffset: number, limit: number): KafkaRecord[] +} + +interface KafkaOperation { + action: 'send' | 'receive' + title: string + summary: string; + description: string + messages: KafkaMessage[]; +} + +interface KafkaMessage { + name: string; + title: string; + summary: string + description: string + contentType: string + payload: Schema; + key: Schema + headers?: Schema; +} + +interface KafkaHeader { + [name: string]: string +} + +interface KafkaRecord { + offset: number + key: string + value: string + headers: KafkaHeader +} +``` + +## Examples + +Read the very last existing message from a specific partition. Since offset is the NEXT position, we read at offset - 1. +```typescript +const kafka = mokapi.getApi("..."); +const topic = kafka.getTopic("..."); +const p0 = topic.partitions.find(p => p.index === 0); +let lastRecord = null +// Check if partition exists and contains at least one message +if (p0 && p0.offset > 0) { + lastRecord = topic.consume(0, p0.offset - 1, 1); +} +lastRecord +``` + +Write a message +```typescript +const kafka = mokapi.getApi("..."); +const topic = kafka.getTopic("..."); +topic.produce(0, JSON.stringify({ foo: "123" }), "key-123"); +``` + +Determine last offset. The last offset represents the NEXT available position. +```typescript +const kafka = mokapi.getApi("Kafka Server"); +const topic = kafka.getTopic("topic-name"); +const partition = topic.partitions.find(p => p.index === 0); +partition.offset; +``` \ No newline at end of file diff --git a/mcp/data/automation.md b/mcp/data/automation.md new file mode 100644 index 000000000..9be899ad3 --- /dev/null +++ b/mcp/data/automation.md @@ -0,0 +1,42 @@ +# Mokapi Automation API + +Technical reference for exploring the current environment, APIs, and Kafka clusters. + +## Category + +To see detailed TypeScript interfaces, call this tool with one of the following values for the category parameter: + +| Category | Description | Parameter Name | +|--------------------|-------------------------------------------------------------------------|-----------------| +| Core Discovery API | Access to the global mokapi object for infrastructure metadata. | `core` | +| HTTP (OpenAPI) | Interfaces for exploring HTTP endpoints, parameters, and status codes. | `http` | +| Kafka (AsyncAPI) | Interfaces for inspecting topics, partitions, and message history. | `kafka` | +| Events | Definitions for debugging activity via getEvents() and traits. | `event` | + +## Examples + +Get all available APIs +```typescript +// The last expression is returned as the tool result +mokapi.getApis() +``` + +Get all available Kafka APIs +```typescript +mokapi.getApis().filter(x => x.type === 'kafka') +``` + +Get specific API to understand the contract +```typescript +mokapi.getApi('API_NAME') +``` + +Get all recorded events from Mokapi, if user asks "Why did my request fail?" +```typescript +mokapi.getEvents() +``` + +Get latest HTTP events for a specific API +```typescript +mokapi.getEvents({ type: 'http', name: 'Petstore' }) +``` \ No newline at end of file diff --git a/mcp/conditional-response.md b/mcp/data/conditional-response.md similarity index 100% rename from mcp/conditional-response.md rename to mcp/data/conditional-response.md diff --git a/mcp/delay-latency.md b/mcp/data/delay-latency.md similarity index 100% rename from mcp/delay-latency.md rename to mcp/data/delay-latency.md diff --git a/mcp/dynamic-error-simulation.md b/mcp/data/dynamic-error-simulation.md similarity index 100% rename from mcp/dynamic-error-simulation.md rename to mcp/data/dynamic-error-simulation.md diff --git a/mcp/dynamic-path-params.md b/mcp/data/dynamic-path-params.md similarity index 100% rename from mcp/dynamic-path-params.md rename to mcp/data/dynamic-path-params.md diff --git a/mcp/forward-request-to-real-backend.md b/mcp/data/forward-request-to-real-backend.md similarity index 100% rename from mcp/forward-request-to-real-backend.md rename to mcp/data/forward-request-to-real-backend.md diff --git a/mcp/mock_reference.md b/mcp/data/mock_reference.md similarity index 100% rename from mcp/mock_reference.md rename to mcp/data/mock_reference.md diff --git a/mcp/mocking-types.md b/mcp/data/mocking-types.md similarity index 100% rename from mcp/mocking-types.md rename to mcp/data/mocking-types.md diff --git a/mcp/scenarios.md b/mcp/data/scenarios.md similarity index 100% rename from mcp/scenarios.md rename to mcp/data/scenarios.md diff --git a/mcp/static-error-simulation.md b/mcp/data/static-error-simulation.md similarity index 100% rename from mcp/static-error-simulation.md rename to mcp/data/static-error-simulation.md diff --git a/mcp/get_automation_definitions.go b/mcp/get_automation_definitions.go index efdcc7872..fa1259545 100644 --- a/mcp/get_automation_definitions.go +++ b/mcp/get_automation_definitions.go @@ -7,27 +7,70 @@ import ( "github.com/modelcontextprotocol/go-sdk/mcp" ) -//go:embed run.ts -var automationTypes string +//go:embed data/automation.md +var automation string -type AutomationDefinitions struct { - Code string +//go:embed data/automation-core.md +var automationCore string + +//go:embed data/automation-http.md +var automationHttp string + +//go:embed data/automation-kafka.md +var automationKafka string + +//go:embed data/automation-event.md +var automationEvent string + +type AutomationDefinitionsInput struct { + Category string `json:"category"` +} + +type AutomationDefinitionsOutput struct { + Text string `json:"text"` } func (s *Service) registerGetAutomationDefinitions(server *mcp.Server) { + inputSchema := map[string]any{ + "type": "object", + "properties": map[string]any{ + "category": map[string]any{ + "type": "string", + "description": "The category of type definition to retrieve. If omitted, a general overview is returned.", + "enum": []string{"core", "http", "kafka", "event"}, + }, + }, + "required": []string{}, + } + registerTool(server, &mcp.Tool{ Name: "mokapi_get_automation_definitions", - Description: `Returns the required TypeScript definitions for the Mokapi Automation API. + Description: `Returns the required TypeScript definitions ONLY for the Mokapi Automation API (tool mokapi_execute_code). -MANDATORY: Call this tool BEFORE using 'mokapi_execute_code' to: +MANDATORY: Call this tool BEFORE using 'mokapi_execute_code'. If you are unsure which category you need, call this tool without any parameters to receive a general overview of all available categories. - Learn how to query API specifications (OpenAPI, AsyncAPI) - Access methods for inspecting live logs and events - Get correct syntax for the global 'mokapi' object This ensures your generated code is valid and uses the correct library methods.`, + InputSchema: inputSchema, }, s.GetAutomationDefinitions) } -func (s *Service) GetAutomationDefinitions(_ context.Context, _ any) (AutomationDefinitions, error) { - return AutomationDefinitions{Code: automationTypes}, nil +func (s *Service) GetAutomationDefinitions(_ context.Context, in AutomationDefinitionsInput) (AutomationDefinitionsOutput, error) { + var text string + switch in.Category { + case "core": + text = automationCore + case "http": + text = automationHttp + case "kafka": + text = automationKafka + case "event": + text = automationEvent + default: + text = automation + } + + return AutomationDefinitionsOutput{Text: text}, nil } diff --git a/mcp/get_mock_reference.go b/mcp/get_mock_reference.go index a9e3fb280..fd9d67f57 100644 --- a/mcp/get_mock_reference.go +++ b/mcp/get_mock_reference.go @@ -8,7 +8,7 @@ import ( "github.com/modelcontextprotocol/go-sdk/mcp" ) -//go:embed mock_reference.md +//go:embed data/mock_reference.md var mockReference string type GetMockReferenceInput struct { @@ -59,7 +59,9 @@ Use this tool to: - See how to import and use 'mokapi', 'mokapi/http', or 'mokapi/kafka'. - Get boilerplate code for specific use cases (e.g., 'rest-auth'). -MANDATORY: Use this before generating a new mock script to ensure correct syntax.`, +MANDATORY: Use this before generating a new mock script to ensure correct syntax. +Do NOT use this reference material with tool mokapi_execute_code +`, InputSchema: inputSchema, }, s.GetMockReference) } diff --git a/mcp/resources.go b/mcp/resources.go index b53c70a85..1c0aedb9f 100644 --- a/mcp/resources.go +++ b/mcp/resources.go @@ -8,7 +8,7 @@ import ( "github.com/modelcontextprotocol/go-sdk/mcp" ) -//go:embed mocking-types.md +//go:embed data/mocking-types.md var overview string var mockTypes = map[string]string{ @@ -31,7 +31,7 @@ func addResources(server *mcp.Server) { { URI: "mokapi://lib/automation", MIMEType: "application/typescript", - Text: automationTypes, + Text: automation, }, }, }, nil @@ -82,25 +82,25 @@ Use mokapi://lib/mocking/types to get an overview of all available APIs`, addMockScenarios(server) } -//go:embed scenarios.md +//go:embed data/scenarios.md var scenarioOverview string -//go:embed dynamic-path-params.md +//go:embed data/dynamic-path-params.md var dynamicPathParams string -//go:embed conditional-response.md +//go:embed data/conditional-response.md var conditionalResponse string -//go:embed static-error-simulation.md +//go:embed data/static-error-simulation.md var staticErrorSimulation string -//go:embed dynamic-error-simulation.md +//go:embed data/dynamic-error-simulation.md var dynamicErrorSimulation string -//go:embed delay-latency.md +//go:embed data/delay-latency.md var delayLatency string -//go:embed forward-request-to-real-backend.md +//go:embed data/forward-request-to-real-backend.md var forwardRequestToRealBackend string var scenarios = map[string]string{ diff --git a/mcp/run.go b/mcp/run.go index baaa71701..3479b7847 100644 --- a/mcp/run.go +++ b/mcp/run.go @@ -19,6 +19,12 @@ import ( log "github.com/sirupsen/logrus" ) +const errorHint = `Tip for Correction: +It seems there is a syntax error or a misunderstanding of the API. +To ensure you are using the correct global variables and methods: +1. Call 'mokapi_get_automation_definitions' without parameters to see the general overview. +2. Check 'category="core"' to verify the syntax of the global 'mokapi' object.` + type RunInput struct { Code string `json:"code"` } @@ -54,17 +60,16 @@ func (s *Service) registerRunTool(server *mcp.Server) { Name: "mokapi_execute_code", Description: `Executes JavaScript code in a sandboxed Mokapi runtime. The last expression in the code is returned as the result. +Do not guess the API. Always call mokapi_get_automation_definitions first. MANDATORY WORKFLOW: 1. FIRST: Call 'mokapi_get_automation_definitions' to get the latest API types. 2. SECOND: Use this tool to query live API data (endpoints, schemas, events). -NEVER guess the API structure; always use the definitions as a reference. Important for Object Returns: JavaScript interprets {} at the start of a line as a block, not an object. To return an object literal, wrap it in parentheses ({ ... }) or assign it to a variable and put the variable name in the last line. Example: const result = { a: 1 }; result - Use this tool to: - Explore mocked APIs (OpenAPI, AsyncAPI, LDAP, Mail) - Inspect operations and schemas @@ -110,7 +115,11 @@ func (m *mokapi) run(code string) (any, error) { if err != nil { var ex *goja.Exception if errors.As(err, &ex) { - return nil, ex.Unwrap() + ue := ex.Unwrap() + if ue == nil { + return nil, fmt.Errorf("%w\n\n%s", err, errorHint) + } + return nil, ue } return nil, err } diff --git a/mcp/run.ts b/mcp/run.ts deleted file mode 100644 index 335c1f7c8..000000000 --- a/mcp/run.ts +++ /dev/null @@ -1,398 +0,0 @@ -declare const mokapi: Mokapi; - -interface Mokapi { - /** - * Returns all mocked APIs (lightweight, no schemas). - */ - getApis(): ApiSummary[]; - - /** - * Returns a specific API by name. - * @example - * getApi('Swagger Petstore') - */ - getApi(name: string): OpenApi; - - /** - * Generate a random value from a JSON Schema. - * The generated data strictly matches the schema, including all required fields and correct types. - * Use this function to create complex random data or writing HTTP mock scripts - * @param schema The JSON scheme for which a random value is generated. - * @example - * fake({ type: 'string', format: 'email' }) - */ - fake(schema: Schema): any; - - /** - * Returns recorded events from Mokapi - * Use this function when the user asks: - * - What requests were made? - * - Why did my request fail? - * - Show recent API activity - * @param traits Filter events by traits - * @param limit Maximum number of events to return, default is 10 - * @example - * getEvents({ apiType: 'http', name: 'Swagger Petstore', path: '/pets' }) - */ - getEvents(traits: HttpTraits, limit?: number): Event[]; -} - -type ApiType = 'http' | 'kafka' | 'ldap' | 'mail' - -interface ApiSummary { - name: string; - type: ApiType; -} - -interface OpenApi extends ApiSummary { - servers: { url: string, description: string } - - /** - * Returns all operations of this API. - */ - getOperations(): OpenApiOperationSummary[]; - - /** - * Returns details about specific operation - * Use id from the operation summary list - * @param id The id of the operation - */ - getOperation(id: string): OpenApiOperation -} - -interface OpenApiOperationSummary { - /** generated from method and path if missing in spec */ - id: string - method: string; - path: string; - summary: string; - /** Names of required parameters to help decide if this is the right endpoint */ - parameters?: string[] -} - -interface OpenApiOperation { - id: string; - path: string - method: string - summary: string - description: string; - parameters: RequestParameter[] - requestBody: RequestBody - /** - * List of allowed responses. - * IMPORTANT: You must only use these status codes for this operation! - */ - responses: Response[] - - /** - * Invoke this operation against the mocked API. - * @example operation.invoke({ path: { id: 1 }, body: JSON.stringify({ name: "test" }) }) - */ - invoke(request?: InvokeRequest): InvokeResponse; -} - -interface RequestParameter { - name: string - in: 'path' | 'query' | 'headers' - required: boolean - schema: Schema - description?: string -} - -interface RequestBody { - description: string - required: boolean - contents: Content[] -} - -interface Content { - contentType: string - schema: Schema -} - -interface Response { - statusCode: number - description: string - contents: Content[] -} - -interface InvokeRequest { - path?: Record; - query?: Record; - header?: Record; - body?: string; -} - -interface InvokeResponse { - statusCode: number; - headers: Record; - body: string -} - -interface Kafka extends ApiSummary { - brokers: { name: string, host: string, description?: string } - - getTopics(): KafkaTopicSummary[] - getTopic(topicName: string): KafkaTopic -} - -interface KafkaTopicSummary { - /** - * The unique name of the topic. - */ - name: string - title?: string - summary?: string -} - -interface KafkaTopic extends KafkaTopicSummary{ - description: string - /** - * List of current partitions and their maximum offsets. - * Use this to determine the range for the 'consume' method. - */ - partitions: { index: number, offset: number } - - operations: KafkaOperation[]; - - /** - * Use 'produce' to send a message to this topic. - * Check 'operations' with action 'send' for valid payloads. - * @param partition The target partition index. MUST be one of the indices listed in the 'partitions' array. - * @param value The message payload. If the operation specifies a JSON schema, provide this as a stringified object. - * @param key Optional message key. - * @param headers Optional metadata headers. - */ - produce(partition: number, value: string, key?: string, headers?: KafkaHeader): void - - /** - * INSPECT: Retrieves a specific record for analysis or verification. - * Use this to check if the mock has received or produced a specific message. - * @param partition The partition index (see 'partitions' list). - * @param startOffset The offset to start reading from (see 'partitions' for max offset). - * @param limit The maximum number of records to return in this call. - * @returns An array of records found starting from the startOffset. - */ - consume(partition: number, startOffset: number, limit: number): KafkaRecord[] -} - -interface KafkaOperation { - action: 'send' | 'receive' - title: string - summary: string; - description: string - messages: KafkaMessage[]; -} - -interface KafkaMessage { - name: string; - title: string; - summary: string - description: string - contentType: string - payload: Schema; - key: Schema - headers?: Schema; -} - -interface KafkaHeader { - [name: string]: string -} - -interface KafkaRecord { - offset: number - key: string - value: string - headers: KafkaHeader -} - -/** - * JSON Schema defines a JSON-based format for describing the structure of JSON data - * @example - * { - * "type": "string", - * "format": "email" - * } - */ -interface Schema { - /** - * Specifies the data type for a schema. - */ - type?: SchemaType | SchemaType[]; - - /** - * The enum keyword is used to restrict a value to a fixed set of values. - */ - enum?: any[]; - - /** - * The const keyword is used to restrict a value to a single value. - */ - const?: any; - - /** - * Contains a list of valid examples. - */ - examples?: any[]; - - /** - * Specifies a default value. - */ - default?: any; - - // Numbers - /** - * Restricts the number to a multiple of the given number - */ - multipleOf?: number; - - /** - * Restricts the number to a maximum number - */ - maximum?: number; - - /** - * Restricts the number to an exclusive maximum number - */ - exclusiveMaximum?: number; - - /** - * Restricts the number to a minimum number - */ - minimum?: number; - - /** - * Restricts the number to an exclusive minimum number - */ - exclusiveMinimum?: number; - - // Strings - /** - * Restricts the string to a maximum length - */ - maxLength?: number; - - /** - * Restricts the string to a minimum length - */ - minLength?: number; - - /** - * The pattern keyword is used to restrict a string to a particular regular expression. - */ - pattern?: string; - - /** - * The format keyword allows for basic semantic identification of certain kinds of string values that are commonly used. - */ - format?: string; - - // Arrays - /** - * Specifies the schema of the items in the array. - */ - items?: Schema; - - /** - * Restricts the array to have a maximum length - */ - maxItems?: number; - - /** - * Restricts the array to have a minimum length - */ - minItems?: number; - - /** - * Restricts the array to have unique items - */ - uniqueItems?: boolean; - - // Objects - /** - * Specifies the properties of an object - */ - properties?: { [name: string]: Schema }; - - /** - * Restricts the object to have a maximum of properties - */ - maxProperties?: number; - - /** - * Restricts the object to have a minimum of properties - */ - minProperties?: number; - - /** - * Specifies the required properties for an object - */ - required?: string[]; - - /** - * The additionalProperties keyword is used to control the handling of extra stuff, - * that is, properties whose names are not listed in the properties keyword or match - * any of the regular expressions in the patternProperties keyword. By default, any - * additional properties are allowed. - */ - additionalProperties?: boolean | Schema; - - /** - * A value must be valid against all the schemas - */ - allOf?: Schema[]; - - /** - * A value must be valid against any the schemas - */ - anyOf?: Schema[]; - - /** - * A value must be valid against exactly one the schemas - */ - oneOf?: Schema[]; -} - -type SchemaType = "object" | "array" | "number" | "integer" | "string" | "boolean" | "null"; - -interface Traits { - type?: ApiType; - name?: string; -} - -interface HttpTraits extends Traits { - /** - * Path value specified by the OpenAPI path - * @example /pet/{petId} - */ - path?: string - - /** - * Request method. - * @example GET - */ - method?: string -} - -interface Event { - /** - * ID of the event - */ - id: string; - - /** - * List of traits - */ - traits: Traits; - - /** - * The data of the event - */ - data: any - - /** - * Time of the event in the format RFC3339 - * @example 2026-07-21T17:32:28Z - */ - time: string -} \ No newline at end of file diff --git a/mcp/run_http_test.go b/mcp/run_http_test.go index 2abe40959..2dc4708d9 100644 --- a/mcp/run_http_test.go +++ b/mcp/run_http_test.go @@ -215,35 +215,6 @@ const result = { path: op.path, method: op.method }; result`, require.Equal(t, map[string]any{"method": "GET", "path": "/pets"}, r.Result) }, }, - { - name: "getResponseSchema not exists", - app: runtimetest.NewHttpApp( - openapitest.NewConfig("3.1.0", - openapitest.WithInfo("foo", "", ""), - openapitest.WithPath("/pets", - openapitest.WithOperation(http.MethodGet, - openapitest.WithResponse(200, - openapitest.WithResponseDescription("response description"), - openapitest.WithContent("application/json", openapitest.WithSchema( - schematest.New("string"), - )), - ), - ), - ), - ), - ), - test: func(t *testing.T, s *mcp.Service) { - r, err := s.GetRunResponse( - context.Background(), - mcp.RunInput{ - Code: `const op = mokapi.getApi('foo').getOperation('get-/pets') -op.getResponseSchema(404)`, - }, - ) - require.NoError(t, err) - require.Nil(t, r.Result) - }, - }, { name: "invoke request", app: runtimetest.NewHttpApp( diff --git a/mcp/run_kafka_test.go b/mcp/run_kafka_test.go index 0584edfc3..1ca471b24 100644 --- a/mcp/run_kafka_test.go +++ b/mcp/run_kafka_test.go @@ -259,6 +259,27 @@ func TestService_Run_Kafka(t *testing.T) { require.Equal(t, int64(0), records[0].Offset) require.Equal(t, "foo", records[0].Key) require.Equal(t, `{"foo":"bar"}`, records[0].Value) + + r, err = s.GetRunResponse( + context.Background(), + mcp.RunInput{ + Code: `const topic = mokapi.getApi('foo').getTopic('channel-1') +const p0 = topic.partitions.find(p => p.index === 0); +let lastMessage = null +if (p0 && p0.offset > 0) { + lastMessage = topic.consume(0, p0.offset - 1, 1); +} +lastMessage +`, + }, + ) + require.NoError(t, err) + require.IsType(t, []mcp.KafkaRecord{}, r.Result) + records = r.Result.([]mcp.KafkaRecord) + require.Len(t, records, 1) + require.Equal(t, int64(0), records[0].Offset) + require.Equal(t, "foo", records[0].Key) + require.Equal(t, `{"foo":"bar"}`, records[0].Value) }, }, { diff --git a/mcp/run_test.go b/mcp/run_test.go index bf28489be..2a84478eb 100644 --- a/mcp/run_test.go +++ b/mcp/run_test.go @@ -62,6 +62,25 @@ func TestService_Run(t *testing.T) { require.Len(t, r.Result, 0) }, }, + { + name: "script error", + app: runtimetest.NewApp(), + test: func(t *testing.T, s *mcp.Service) { + _, err := s.GetRunResponse( + context.Background(), + mcp.RunInput{ + Code: `okapi.getApis()`, + }, + ) + require.EqualError(t, err, `ReferenceError: okapi is not defined at mokapi_execute_code.js:1:1(0) + +Tip for Correction: +It seems there is a syntax error or a misunderstanding of the API. +To ensure you are using the correct global variables and methods: +1. Call 'mokapi_get_automation_definitions' without parameters to see the general overview. +2. Check 'category="core"' to verify the syntax of the global 'mokapi' object.`) + }, + }, } for _, tc := range testcases { From 99e6faa9c86520a879577d527dd36d63fc179286 Mon Sep 17 00:00:00 2001 From: marle3003 Date: Tue, 21 Apr 2026 19:51:51 +0200 Subject: [PATCH 45/46] fix(webui): fix navigation history back fix(webui): fix example & validate dialog view not displayed editor --- .../tests/dashboard/kafka/topic.order.spec.ts | 8 +++++++- .../src/components/dashboard/SchemaValidate.vue | 1 + webui/src/components/dashboard/SourceView.vue | 7 ++++--- webui/src/router/index.ts | 16 ++++++++++------ 4 files changed, 22 insertions(+), 10 deletions(-) diff --git a/webui/e2e/tests/dashboard/kafka/topic.order.spec.ts b/webui/e2e/tests/dashboard/kafka/topic.order.spec.ts index f027ff07f..8d903c818 100644 --- a/webui/e2e/tests/dashboard/kafka/topic.order.spec.ts +++ b/webui/e2e/tests/dashboard/kafka/topic.order.spec.ts @@ -55,7 +55,7 @@ test('Visit Kafka topic mokapi.shop.products', async ({ page, context }) => { await test.step('Check config', async () => { await tabList.getByRole('tab', { name: 'Configs' }).click() const configs = page.getByRole('tabpanel', { name: 'Configs' }) - await expect(configs.getByLabel('Title')).toHaveText(topic.messageConfigs[0].title) + await expect(configs.getByLabel('Title')).toHaveText(topic.messageConfigs[0].title!) await expect(configs.getByLabel('Name')).toHaveText(topic.messageConfigs[0].name) await expect(configs.getByLabel('Summary')).toHaveText(topic.messageConfigs[0].summary) await expect(configs.getByLabel('Description')).toHaveText(topic.messageConfigs[0].description) @@ -93,6 +93,9 @@ test('Visit Kafka topic mokapi.shop.products', async ({ page, context }) => { await test.step('Check schema example', async () => { await configs.getByRole('button', { name: 'Example & Validate' }).click() const dialog = page.getByRole('dialog', { name: 'Value Validator - mokapi.shop.products' }) + + await expect(dialog.getByRole('region', { name: 'Content' })).toBeVisible() + await dialog.getByRole('button', { name: 'Example' }).click() const { test: testSourceView } = useSourceView(dialog) await testSourceView({ @@ -108,6 +111,9 @@ test('Visit Kafka topic mokapi.shop.products', async ({ page, context }) => { await test.step('Check data validation', async () =>{ await configs.getByRole('button', { name: 'Example & Validate' }).click() const dialog = page.getByRole('dialog', { name: 'Value Validator - mokapi.shop.products' }) + + await expect(dialog.getByRole('region', { name: 'Content' })).toBeVisible() + await dialog.getByRole('button', { name: 'Example' }).click() // first we try data that are wrong against the schema const id = await dialog.locator('.modal-dialog .ace_editor').getAttribute('id') diff --git a/webui/src/components/dashboard/SchemaValidate.vue b/webui/src/components/dashboard/SchemaValidate.vue index 2178aed52..335cdc503 100644 --- a/webui/src/components/dashboard/SchemaValidate.vue +++ b/webui/src/components/dashboard/SchemaValidate.vue @@ -35,6 +35,7 @@ const props = withDefaults(defineProps<{ }>(), { title: 'Data Validator' }) +console.log(props) const { createGuid } = useGuid() const { formatLanguage } = usePrettyLanguage() diff --git a/webui/src/components/dashboard/SourceView.vue b/webui/src/components/dashboard/SourceView.vue index fc9aa0f4f..437a941e5 100644 --- a/webui/src/components/dashboard/SourceView.vue +++ b/webui/src/components/dashboard/SourceView.vue @@ -46,6 +46,7 @@ if (props.source.preview) { } else { throw new Error('preview and binary not defined') } +console.log(current.value) watch(() => props.source, (source) => { if (!current.value) { return @@ -247,7 +248,7 @@ watch( -
+
-
- +
+
diff --git a/webui/src/router/index.ts b/webui/src/router/index.ts index 5160b3f9f..46e65ab7c 100644 --- a/webui/src/router/index.ts +++ b/webui/src/router/index.ts @@ -362,23 +362,27 @@ const router = createRouter({ ] }) -router.afterEach((to, from) => { +// add refresh query parameter if clicked link does not have it but current route has it +router.beforeEach((to, from, next) => { if (!to.path.startsWith('/dashboard') || to.path.startsWith('/dashboard-demo')) { - return + return next() } const hadRefresh = !!from.query.refresh; const hasRefresh = !!to.query.refresh; if (hadRefresh && !hasRefresh) { - router.replace({ - ...to, + return next({ + path: to.path, query: { ...to.query, refresh: from.query.refresh - } - }); + }, + hash: to.hash, + }) } + + next() }); if (import.meta.env.VITE_DASHBOARD === 'true') { From c0bb34d146fc8224ccffb1ac620927805427c402 Mon Sep 17 00:00:00 2001 From: marle3003 Date: Tue, 21 Apr 2026 20:27:07 +0200 Subject: [PATCH 46/46] test(ldap): add test for parentheses are not mandatory when MUST and MAY are only followed by one oid for objectClasses --- providers/directory/schema_test.go | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/providers/directory/schema_test.go b/providers/directory/schema_test.go index 37f860f08..3e70d581e 100644 --- a/providers/directory/schema_test.go +++ b/providers/directory/schema_test.go @@ -1,8 +1,9 @@ package directory import ( - "github.com/stretchr/testify/require" "testing" + + "github.com/stretchr/testify/require" ) func TestAttributeType(t *testing.T) { @@ -111,6 +112,20 @@ func TestObjectClass(t *testing.T) { require.Equal(t, []string{"description"}, class.May) }, }, + { + name: "parentheses are not mandatory when MUST and MAY are only followed by one oid for objectClasses", + input: "( 1.3.6.1.4.1.99999.1.1 NAME 'customPerson' SUP ( inetOrgPerson $ device ) STRUCTURAL MUST customID MAY description )", + test: func(t *testing.T, class *ObjectClass, err error) { + require.NoError(t, err) + require.Equal(t, "1.3.6.1.4.1.99999.1.1", class.Id) + require.Equal(t, []string{"customPerson"}, class.Name) + require.Equal(t, "", class.Description) + require.Equal(t, []string{"inetOrgPerson", "device"}, class.SuperClass) + require.Equal(t, "STRUCTURAL", class.Type) + require.Equal(t, []string{"customID"}, class.Must) + require.Equal(t, []string{"description"}, class.May) + }, + }, } t.Parallel()