diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index cfc5bd0..9066969 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -12,6 +12,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 + with: + submodules: recursive - name: Install mise uses: jdx/mise-action@v2 @@ -46,6 +48,8 @@ jobs: steps: - uses: actions/checkout@v5 + with: + submodules: recursive - name: Install mise uses: jdx/mise-action@v2 @@ -56,6 +60,14 @@ jobs: go-version-file: "go.mod" cache: true + # Verify Docker is available for testcontainers (Ubuntu only) + - name: Verify Docker availability + if: matrix.os == 'ubuntu-latest' + run: | + docker --version + docker info + echo "Docker is available for testcontainers" + - name: Get current date id: date run: echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT @@ -78,7 +90,7 @@ jobs: if: matrix.os == 'windows-latest' env: ARAZZO_CACHE_DIR: ${{ runner.temp }} - run: gotestsum --format testname -- -race ./... + run: gotestsum --format testname -- -race $(go list ./... | grep -v 'jsonschema/oas3/tests') - name: Calculate coverage if: matrix.os == 'ubuntu-latest' @@ -101,6 +113,11 @@ jobs: # Checkout main branch in a temporary directory git worktree add /tmp/main-branch main + # Initialize submodules in the main branch worktree + cd /tmp/main-branch + git submodule update --init --recursive || echo "Submodule initialization failed, continuing without submodules" + cd "$CURRENT_DIR" + # Run tests on main branch to get coverage (with timeout) cd /tmp/main-branch timeout 300 go test -coverprofile=main-coverage.out -covermode=atomic ./... > /dev/null 2>&1 || echo "Main branch tests failed or timed out" diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..6a149dd --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "jsonschema/oas3/tests/testsuite"] + path = jsonschema/oas3/tests/testsuite + url = https://github.com/json-schema-org/JSON-Schema-Test-Suite.git diff --git a/go.mod b/go.mod index d514e02..e36a209 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/speakeasy-api/jsonpath v0.6.2 github.com/spf13/cobra v1.7.0 github.com/stretchr/testify v1.10.0 + github.com/testcontainers/testcontainers-go v0.38.0 github.com/vmware-labs/yaml-jsonpath v0.3.2 golang.org/x/sync v0.16.0 golang.org/x/text v0.28.0 @@ -14,10 +15,61 @@ require ( ) require ( + dario.cat/mergo v1.0.1 // indirect + github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/cenkalti/backoff/v4 v4.2.1 // indirect + github.com/containerd/errdefs v1.0.0 // indirect + github.com/containerd/errdefs/pkg v0.3.0 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/containerd/platforms v0.2.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/docker v28.2.2+incompatible // indirect + github.com/docker/go-connections v0.5.0 // indirect + github.com/docker/go-units v0.5.0 // indirect github.com/dprotaso/go-yit v0.0.0-20191028211022-135eb7262960 // indirect + github.com/ebitengine/purego v0.8.4 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/kr/text v0.2.0 // indirect + github.com/klauspost/compress v1.18.0 // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/magiconair/properties v1.8.10 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/go-archive v0.1.0 // indirect + github.com/moby/patternmatcher v0.6.0 // indirect + github.com/moby/sys/sequential v0.6.0 // indirect + github.com/moby/sys/user v0.4.0 // indirect + github.com/moby/sys/userns v0.1.0 // indirect + github.com/moby/term v0.5.0 // indirect + github.com/morikuni/aec v1.0.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect + github.com/shirou/gopsutil/v4 v4.25.5 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/tklauser/go-sysconf v0.3.12 // indirect + github.com/tklauser/numcpus v0.6.1 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect + go.opentelemetry.io/otel v1.37.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect + go.opentelemetry.io/otel/metric v1.37.0 // indirect + go.opentelemetry.io/otel/sdk v1.37.0 // indirect + go.opentelemetry.io/otel/trace v1.37.0 // indirect + go.opentelemetry.io/proto/otlp v1.7.1 // indirect + golang.org/x/crypto v0.40.0 // indirect + golang.org/x/net v0.42.0 // indirect + golang.org/x/sys v0.34.0 // indirect + google.golang.org/protobuf v1.36.6 // indirect ) diff --git a/go.sum b/go.sum index 2f3c538..8c34bcc 100644 --- a/go.sum +++ b/go.sum @@ -1,38 +1,128 @@ +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= +github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/docker/docker v28.2.2+incompatible h1:CjwRSksz8Yo4+RmQ339Dp/D2tGO5JxwYeqtMOEe0LDw= +github.com/docker/docker v28.2.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/dprotaso/go-yit v0.0.0-20191028211022-135eb7262960 h1:aRd8M7HJVZOqn/vhOzrGcQH0lNAMkqMn+pXUYkatmcA= github.com/dprotaso/go-yit v0.0.0-20191028211022-135eb7262960/go.mod h1:9HQzr9D/0PGwMEbC3d5AB7oi67+h4TsQqItC1GVYG58= +github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw= +github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90= github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= +github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ= +github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo= +github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= +github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= +github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= +github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= +github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= +github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= +github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.10.2 h1:uqH7bpe+ERSiDa34FDOF7RikN6RzXgduUF8yarlZp94= github.com/onsi/ginkgo v1.10.2/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME= github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/shirou/gopsutil/v4 v4.25.5 h1:rtd9piuSMGeU8g1RMXjZs9y9luK5BwtnG7dZaQUJAsc= +github.com/shirou/gopsutil/v4 v4.25.5/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/speakeasy-api/jsonpath v0.6.2 h1:Mys71yd6u8kuowNCR0gCVPlVAHCmKtoGXYoAtcEbqXQ= github.com/speakeasy-api/jsonpath v0.6.2/go.mod h1:ymb2iSkyOycmzKwbEAYPJV/yi2rSmvBCLZJcyD+VVWw= github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= @@ -40,25 +130,103 @@ github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRM github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/testcontainers/testcontainers-go v0.38.0 h1:d7uEapLcv2P8AvH8ahLqDMMxda2W9gQN1nRbHS28HBw= +github.com/testcontainers/testcontainers-go v0.38.0/go.mod h1:C52c9MoHpWO+C4aqmgSU+hxlR5jlEayWtgYrb8Pzz1w= +github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= +github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= github.com/vmware-labs/yaml-jsonpath v0.3.2 h1:/5QKeCBGdsInyDCyVNLbXyilb61MXGi9NP674f9Hobk= github.com/vmware-labs/yaml-jsonpath v0.3.2/go.mod h1:U6whw1z03QyqgWdgXxvVnQ90zN1BWz5V+51Ewf8k+rQ= -golang.org/x/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9+7a3s8RBNOZ3eYZzJA= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= +go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= +go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU= +go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= +go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= +go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= +go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= +go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= +go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4= +go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= +golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= +golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= -golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e h1:o3PsSEY8E4eXWkXrIP9YJALUkVZqzHJT5DOasTyn8Vs= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= +golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg= +golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= +golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto/googleapis/api v0.0.0-20250728155136-f173205681a0 h1:0UOBWO4dC+e51ui0NFKSPbkHHiQ4TmrEfEZMLDyRmY8= +google.golang.org/genproto/googleapis/api v0.0.0-20250728155136-f173205681a0/go.mod h1:8ytArBbtOy2xfht+y2fqKd5DRDJRUQhqbyEnQ4bDChs= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0 h1:MAKi5q709QWfnkkpNQ0M12hYJ1+e8qYVDyowc4U1XZM= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4= +google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM= +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= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= @@ -71,3 +239,5 @@ gopkg.in/yaml.v3 v3.0.0-20191026110619-0b21df46bc1d/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= +gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= diff --git a/jsonpointer/jsonpointer_test.go b/jsonpointer/jsonpointer_test.go index 91cf06c..6cea6aa 100644 --- a/jsonpointer/jsonpointer_test.go +++ b/jsonpointer/jsonpointer_test.go @@ -50,6 +50,12 @@ func TestJSONPointer_Validate_Success(t *testing.T) { j: JSONPointer("/paths/~1special-events~1{eventId}/get"), }, }, + { + name: "empty tokens (consecutive slashes)", + args: args{ + j: JSONPointer("/some//path"), + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -94,11 +100,11 @@ func TestJSONPointer_Validate_Error(t *testing.T) { wantErr: errors.New("validation error -- jsonpointer part must be a valid token [^(?:[\x00-.0-}\x7f-\uffff]|~[01])+$]: /~/some~path"), }, { - name: "empty part in path", + name: "invalid path with unescaped tilde in middle", args: args{ - j: JSONPointer("/some//path"), + j: JSONPointer("/some/~invalid/path"), }, - wantErr: errors.New("validation error -- jsonpointer part must not be empty: /some//path"), + wantErr: errors.New("validation error -- jsonpointer part must be a valid token [^(?:[\x00-.0-}\x7f-\uffff]|~[01])+$]: /some/~invalid/path"), }, } for _, tt := range tests { @@ -273,6 +279,18 @@ func TestGetTarget_Success(t *testing.T) { }, want: 4, }, + { + name: "empty tokens in JSON pointer", + args: args{ + source: map[string]any{ + "": map[string]any{ + "": "value", + }, + }, + pointer: JSONPointer("//"), + }, + want: "value", + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/jsonpointer/models.go b/jsonpointer/models.go index eb34acc..a21d31d 100644 --- a/jsonpointer/models.go +++ b/jsonpointer/models.go @@ -5,6 +5,7 @@ import ( "reflect" "github.com/speakeasy-api/openapi/internal/interfaces" + "github.com/speakeasy-api/openapi/marshaller" ) type model interface { @@ -110,7 +111,20 @@ func navigateModel(sourceVal reflect.Value, currentPart navigationPart, stack [] } if coreFieldIndex == -1 { - return nil, nil, ErrNotFound.Wrap(fmt.Errorf("key %s not found in core model at %s", currentPart.Value, currentPath)) + // Field not found in core model, try searching the associated YAML node + // Check if the model implements CoreModeler interface (which has GetRootNode) + if coreModeler, ok := coreAny.(marshaller.CoreModeler); ok { + rootNode := coreModeler.GetRootNode() + if rootNode != nil { + // Use the existing YAML node navigation logic to search for the key + result, newStack, err := getYamlNodeTarget(rootNode, currentPart, stack, currentPath, o) + if err == nil { + return result, newStack, nil + } + } + } + + return nil, nil, ErrNotFound.Wrap(fmt.Errorf("key %s not found in core model or YAML node at %s", currentPart.Value, currentPath)) } // Find the corresponding field in the high-level model diff --git a/jsonpointer/models_yaml_fallback_test.go b/jsonpointer/models_yaml_fallback_test.go new file mode 100644 index 0000000..7c1950e --- /dev/null +++ b/jsonpointer/models_yaml_fallback_test.go @@ -0,0 +1,197 @@ +package jsonpointer + +import ( + "reflect" + "testing" + + "github.com/speakeasy-api/openapi/marshaller" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" +) + +// TestModel represents a simple model for testing YAML fallback +type TestModel struct { + marshaller.Model[TestModelCore] + + KnownField string +} + +type TestModelCore struct { + marshaller.CoreModel `model:"testModelCore"` + + KnownField marshaller.Node[string] `key:"knownField"` +} + +func TestNavigateModel_YAMLFallback_Success(t *testing.T) { + t.Parallel() + tests := []struct { + name string + yamlContent string + jsonPointer string + expectedType string + expectedVal interface{} + }{ + { + name: "unknown field in YAML root", + yamlContent: ` +knownField: "known value" +unknownField: "unknown value" +`, + jsonPointer: "/unknownField", + expectedType: "*yaml.Node", + expectedVal: "unknown value", + }, + { + name: "nested unknown field", + yamlContent: ` +knownField: "known value" +unknownObject: + nestedField: "nested value" +`, + jsonPointer: "/unknownObject/nestedField", + expectedType: "*yaml.Node", + expectedVal: "nested value", + }, + { + name: "unknown array field", + yamlContent: ` +knownField: "known value" +unknownArray: + - "item1" + - "item2" +`, + jsonPointer: "/unknownArray/1", + expectedType: "*yaml.Node", + expectedVal: "item2", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + // Parse YAML content + var rootNode yaml.Node + err := yaml.Unmarshal([]byte(tt.yamlContent), &rootNode) + require.NoError(t, err, "failed to parse YAML") + + // Create test model with YAML node + model := &TestModel{} + model.GetCore().SetRootNode(&rootNode) + + // Test navigation + pointer := JSONPointer(tt.jsonPointer) + result, err := GetTarget(model, pointer) + + require.NoError(t, err, "navigation should succeed") + require.NotNil(t, result, "result should not be nil") + + // Verify result type + assert.Equal(t, tt.expectedType, getTypeName(result), "result type should match expected") + + // For YAML nodes, check the value + if yamlNode, ok := result.(*yaml.Node); ok { + assert.Equal(t, tt.expectedVal, yamlNode.Value, "YAML node value should match expected") + } + }) + } +} + +func TestNavigateModel_YAMLFallback_KnownFieldStillWorks(t *testing.T) { + t.Parallel() + yamlContent := ` +knownField: "known value" +unknownField: "unknown value" +` + + // Parse YAML content + var rootNode yaml.Node + err := yaml.Unmarshal([]byte(yamlContent), &rootNode) + require.NoError(t, err, "failed to parse YAML") + + // Create test model with YAML node + model := &TestModel{ + KnownField: "known value", + } + model.GetCore().SetRootNode(&rootNode) + + // Test navigation to known field (should use struct field, not YAML fallback) + pointer := JSONPointer("/knownField") + result, err := GetTarget(model, pointer) + + require.NoError(t, err, "navigation should succeed") + require.NotNil(t, result, "result should not be nil") + + // Should return the string field from the high-level model, not a YAML node + assert.Equal(t, "string", getTypeName(result), "should return struct field, not YAML fallback") +} + +func TestNavigateModel_YAMLFallback_Error(t *testing.T) { + t.Parallel() + tests := []struct { + name string + yamlContent string + jsonPointer string + }{ + { + name: "field not found anywhere", + yamlContent: ` +knownField: "known value" +`, + jsonPointer: "/nonExistentField", + }, + { + name: "nested field not found", + yamlContent: ` +knownField: "known value" +existingObject: + someField: "value" +`, + jsonPointer: "/existingObject/nonExistentField", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + // Parse YAML content + var rootNode yaml.Node + err := yaml.Unmarshal([]byte(tt.yamlContent), &rootNode) + require.NoError(t, err, "failed to parse YAML") + + // Create test model with YAML node + model := &TestModel{} + model.GetCore().SetRootNode(&rootNode) + + // Test navigation + pointer := JSONPointer(tt.jsonPointer) + result, err := GetTarget(model, pointer) + + require.Error(t, err, "navigation should fail") + assert.Nil(t, result, "result should be nil") + assert.Contains(t, err.Error(), "not found", "error should indicate field not found") + }) + } +} + +func TestNavigateModel_YAMLFallback_NoYAMLNode(t *testing.T) { + t.Parallel() + // Create test model without YAML node + model := &TestModel{} + + // Test navigation to unknown field + pointer := JSONPointer("/unknownField") + result, err := GetTarget(model, pointer) + + require.Error(t, err, "navigation should fail when no YAML node available") + assert.Nil(t, result, "result should be nil") + assert.Contains(t, err.Error(), "not found", "error should indicate field not found") +} + +// Helper function to get type name for assertions +func getTypeName(v interface{}) string { + if v == nil { + return "nil" + } + return reflect.TypeOf(v).String() +} diff --git a/jsonpointer/navigation.go b/jsonpointer/navigation.go index 47477c6..b6eea04 100644 --- a/jsonpointer/navigation.go +++ b/jsonpointer/navigation.go @@ -54,15 +54,12 @@ func (j JSONPointer) getNavigationStack() ([]navigationPart, error) { strParts := strings.Split(strings.TrimPrefix(string(j), "/"), "/") for _, part := range strParts { - if len(part) == 0 { - return nil, fmt.Errorf("jsonpointer part must not be empty: %s", string(j)) - } - - if !tokenRegex.MatchString(part) { + // Empty parts are valid according to RFC 6901 - they represent empty string keys + if len(part) > 0 && !tokenRegex.MatchString(part) { return nil, fmt.Errorf("jsonpointer part must be a valid token [%s]: %s", tokenRegex.String(), string(j)) } - if digitOnlyRegex.MatchString(part) && (len(part) == 1 || part[0] != '0') { + if len(part) > 0 && digitOnlyRegex.MatchString(part) && (len(part) == 1 || part[0] != '0') { stack = append(stack, navigationPart{ Type: partTypeIndex, Value: part, diff --git a/jsonschema/oas3/jsonschema.go b/jsonschema/oas3/jsonschema.go index d65ef09..ca0af73 100644 --- a/jsonschema/oas3/jsonschema.go +++ b/jsonschema/oas3/jsonschema.go @@ -251,3 +251,26 @@ func (j *JSONSchema[T]) ShallowCopy() *JSONSchema[T] { return result } + +// PopulateWithParent implements the ParentAwarePopulator interface to establish parent relationships during population +func (j *JSONSchema[T]) PopulateWithParent(source any, parent any) error { + // If we have a parent that is also a JSONSchema, establish the parent relationship + if parent != nil { + if parentSchema, ok := parent.(*Schema); ok { + j.SetParent(parentSchema.GetParent()) + // If the parent has a top-level parent, inherit it; otherwise, the parent is the top-level + if parentSchema.GetParent().GetTopLevelParent() != nil { + j.SetTopLevelParent(parentSchema.GetParent().GetTopLevelParent()) + } else { + j.SetTopLevelParent(parentSchema.GetParent()) + } + } + } + + // First, perform the standard population + if err := j.EitherValue.PopulateWithParent(source, j); err != nil { + return err + } + + return nil +} diff --git a/jsonschema/oas3/resolution.go b/jsonschema/oas3/resolution.go index bcda84d..9f369ec 100644 --- a/jsonschema/oas3/resolution.go +++ b/jsonschema/oas3/resolution.go @@ -153,9 +153,14 @@ func (s *JSONSchema[Referenceable]) resolve(ctx context.Context, opts references // Special case: detect self-referencing schemas (references to root document) // This catches cases like "#" which reference the root document itself + // Only consider it circular if this schema has no parent (i.e., it's at the root level) if ref.GetURI() == "" && ref.GetJSONPointer() == "" { - s.circularErrorFound = true - return nil, nil, errors.New("circular reference detected: self-referencing schema") + // Check if this schema has a parent - if it does, then referencing "#" is legitimate + // If it has no parent, then it's the root schema referencing itself, which is circular + if s.GetParent() == nil && s.GetTopLevelParent() == nil { + s.circularErrorFound = true + return nil, nil, errors.New("circular reference detected: self-referencing schema") + } } // Check for circular reference by looking for the current reference in the chain diff --git a/jsonschema/oas3/schema.go b/jsonschema/oas3/schema.go index c04ebaa..334441d 100644 --- a/jsonschema/oas3/schema.go +++ b/jsonschema/oas3/schema.go @@ -3,6 +3,7 @@ package oas3 import ( _ "embed" + "fmt" "reflect" "github.com/speakeasy-api/openapi/extensions" @@ -72,6 +73,11 @@ type Schema struct { Schema *string XML *XML Extensions *extensions.Extensions + + // Parent reference links - private fields to avoid serialization + // These are set when the schema was populated as a child of another schema. + // Used for circular reference detection during resolution. + parent *JSONSchema[Referenceable] // Immediate parent schema in the hierarchy } // ShallowCopy creates a shallow copy of the Schema. @@ -127,6 +133,7 @@ func (s *Schema) ShallowCopy() *Schema { Schema: s.Schema, XML: s.XML, Extensions: s.Extensions, + parent: s.parent, } // Shallow copy slices - create new slice but reference same elements @@ -869,6 +876,53 @@ func (s *Schema) IsEqual(other *Schema) bool { return true } +// GetParent returns the immediate parent JSONSchema if this schema was populated as a child of another schema. +// Returns nil if this schema has no parent or was not populated via parent-aware population. +func (s *Schema) GetParent() *JSONSchema[Referenceable] { + if s == nil { + return nil + } + return s.parent +} + +// SetParent sets the immediate parent JSONSchema for this schema. +// This is used during parent-aware population to establish parent relationships. +func (s *Schema) SetParent(parent *JSONSchema[Referenceable]) { + if s == nil { + return + } + s.parent = parent +} + +// PopulateWithParent implements the ParentAwarePopulator interface to establish parent relationships during population +func (s *Schema) PopulateWithParent(source any, parent any) error { + // If we have a parent that is a JSONSchema, establish the parent relationship + if parent != nil { + if parentSchema, ok := parent.(*JSONSchema[Referenceable]); ok { + s.SetParent(parentSchema) + } + } + + var coreSchema *core.Schema + switch src := source.(type) { + case *core.Schema: + coreSchema = src + case core.Schema: + coreSchema = &src + default: + return fmt.Errorf("expected *core.Reference[C] or core.Reference[C], got %T", source) + } + + // First, perform the standard population + if err := marshaller.PopulateModel(source, s); err != nil { + return err + } + + s.SetCore(coreSchema) + + return nil +} + // Helper functions for equality comparison func equalJSONSchemas(a, b *JSONSchema[Referenceable]) bool { diff --git a/jsonschema/oas3/tests/remote_server.go b/jsonschema/oas3/tests/remote_server.go new file mode 100644 index 0000000..b8c5f7c --- /dev/null +++ b/jsonschema/oas3/tests/remote_server.go @@ -0,0 +1,218 @@ +package tests + +import ( + "context" + "errors" + "fmt" + "net/http" + "net/url" + "os" + "path/filepath" + "strings" + "time" + + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/wait" +) + +// RemoteServer manages a testcontainer that serves the JSON Schema Test Suite remote files +type RemoteServer struct { + container testcontainers.Container + baseURL string +} + +// startRemoteServer starts a container serving the remote files at localhost:1234 +func startRemoteServer() (*RemoteServer, error) { + ctx := context.Background() + + // Get the absolute path to the remotes directory + remotesPath, err := filepath.Abs("testsuite/remotes") + if err != nil { + return nil, fmt.Errorf("failed to get absolute path to remotes directory: %w", err) + } + + // Check if remotes directory exists + if _, err := os.Stat(remotesPath); os.IsNotExist(err) { + return nil, fmt.Errorf("remotes directory does not exist: %s", remotesPath) + } + + // Create nginx configuration that enables directory indexing and proper file serving + nginxConfig := ` +server { + listen 80; + server_name localhost; + root /remotes; + + # Enable directory indexing + autoindex on; + autoindex_exact_size off; + autoindex_localtime on; + + # Serve files with proper MIME types + location / { + try_files $uri $uri/ =404; + add_header Access-Control-Allow-Origin *; + add_header Access-Control-Allow-Methods "GET, POST, OPTIONS"; + add_header Access-Control-Allow-Headers "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range"; + } + + # Specific handling for JSON files + location ~* \.json$ { + add_header Content-Type application/json; + add_header Access-Control-Allow-Origin *; + } +} +` + + // Create container request with fixed port binding to 1234 + req := testcontainers.ContainerRequest{ + Image: "nginx:alpine", + ExposedPorts: []string{"80/tcp"}, + Files: []testcontainers.ContainerFile{ + { + HostFilePath: remotesPath, + ContainerFilePath: "/remotes", + FileMode: 0755, + }, + { + HostFilePath: "", + ContainerFilePath: "/etc/nginx/conf.d/default.conf", + FileMode: 0644, + Reader: strings.NewReader(nginxConfig), + }, + }, + WaitingFor: wait.ForHTTP("/draft2020-12/integer.json").WithPort("80/tcp").WithStartupTimeout(30 * time.Second), + } + + // Start the container with port binding + container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ContainerRequest: req, + Started: true, + }) + if err != nil { + return nil, fmt.Errorf("failed to start remote server container: %w", err) + } + + // Get the mapped port + mappedPort, err := container.MappedPort(ctx, "80") + if err != nil { + if termErr := container.Terminate(ctx); termErr != nil { + return nil, fmt.Errorf("failed to get mapped port: %w (cleanup error: %w)", err, termErr) + } + return nil, fmt.Errorf("failed to get mapped port: %w", err) + } + + // Get the host + host, err := container.Host(ctx) + if err != nil { + if termErr := container.Terminate(ctx); termErr != nil { + return nil, fmt.Errorf("failed to get container host: %w (cleanup error: %w)", err, termErr) + } + return nil, fmt.Errorf("failed to get container host: %w", err) + } + + baseURL := fmt.Sprintf("http://%s:%s", host, mappedPort.Port()) + + // Verify the server is working by checking a known file + client := &http.Client{Timeout: 5 * time.Second} + resp, err := client.Get(baseURL + "/draft2020-12/integer.json") + if err != nil { + if termErr := container.Terminate(ctx); termErr != nil { + return nil, fmt.Errorf("failed to verify remote server is working: %w (cleanup error: %w)", err, termErr) + } + return nil, fmt.Errorf("failed to verify remote server is working: %w", err) + } + if resp == nil { + if termErr := container.Terminate(ctx); termErr != nil { + return nil, fmt.Errorf("received nil response from remote server (cleanup error: %w)", termErr) + } + return nil, errors.New("received nil response from remote server") + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + if termErr := container.Terminate(ctx); termErr != nil { + return nil, fmt.Errorf("remote server health check failed with status: %d (cleanup error: %w)", resp.StatusCode, termErr) + } + return nil, fmt.Errorf("remote server health check failed with status: %d", resp.StatusCode) + } + + return &RemoteServer{ + container: container, + baseURL: baseURL, + }, nil +} + +// GetBaseURL returns the base URL where the remote files are served +func (rs *RemoteServer) GetBaseURL() string { + return rs.baseURL +} + +// Stop stops and removes the container +func (rs *RemoteServer) Stop() { + if rs.container != nil { + ctx := context.Background() + err := rs.container.Terminate(ctx) + if err != nil { + // Use fmt.Printf since we can't access log in this context + fmt.Printf("Warning: failed to terminate remote server container: %v\n", err) + } + } +} + +// GetExpectedURL returns the URL that the test suite expects for a given path +// Since we're binding to localhost:1234, this is the same as GetActualURL +func (rs *RemoteServer) GetExpectedURL(path string) string { + return "http://localhost:1234/" + path +} + +// GetActualURL returns the actual URL where the file is served +func (rs *RemoteServer) GetActualURL(path string) string { + return rs.baseURL + "/" + path +} + +// GetHTTPClient returns an HTTP client that redirects localhost:1234 requests to the actual container +func (rs *RemoteServer) GetHTTPClient() *http.Client { + return &http.Client{ + Transport: &redirectTransport{ + baseURL: rs.baseURL, + base: http.DefaultTransport, + }, + Timeout: 30 * time.Second, + } +} + +// redirectTransport is an HTTP transport that redirects localhost:1234 requests to the actual container URL +type redirectTransport struct { + baseURL string + base http.RoundTripper +} + +func (rt *redirectTransport) RoundTrip(req *http.Request) (*http.Response, error) { + // Check if this is a localhost:1234 request + if req.URL.Host == "localhost:1234" { + // Parse the container base URL + containerURL, err := url.Parse(rt.baseURL) + if err != nil { + return nil, fmt.Errorf("failed to parse container URL: %w", err) + } + + // Create a new URL with the container host/port but keep the original path + newURL := &url.URL{ + Scheme: containerURL.Scheme, + Host: containerURL.Host, + Path: req.URL.Path, + RawQuery: req.URL.RawQuery, + Fragment: req.URL.Fragment, + } + + // Clone the request with the new URL + newReq := req.Clone(req.Context()) + newReq.URL = newURL + + return rt.base.RoundTrip(newReq) + } + + // For all other requests, use the base transport + return rt.base.RoundTrip(req) +} diff --git a/jsonschema/oas3/tests/testsuite b/jsonschema/oas3/tests/testsuite new file mode 160000 index 0000000..15e4505 --- /dev/null +++ b/jsonschema/oas3/tests/testsuite @@ -0,0 +1 @@ +Subproject commit 15e4505bf689de5d30c29d50782bb48fa465c93f diff --git a/jsonschema/oas3/tests/testsuite_test.go b/jsonschema/oas3/tests/testsuite_test.go new file mode 100644 index 0000000..a79a7e5 --- /dev/null +++ b/jsonschema/oas3/tests/testsuite_test.go @@ -0,0 +1,451 @@ +package tests + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io/fs" + "log" + "os" + "path/filepath" + "strings" + "sync" + "sync/atomic" + "testing" + + "github.com/speakeasy-api/openapi/jsonschema/oas3" + "github.com/speakeasy-api/openapi/marshaller" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestCase represents a single test case from the JSON Schema Test Suite +type TestCase struct { + Description string `json:"description"` + Comment string `json:"comment,omitempty"` + Schema json.RawMessage `json:"schema"` + Tests []Test `json:"tests"` +} + +// Test represents a single test within a test case +type Test struct { + Description string `json:"description"` + Comment string `json:"comment,omitempty"` + Data interface{} `json:"data"` + Valid bool `json:"valid"` +} + +// Blacklisted test files that we don't support or want to skip +// TODO work on improving support for these files +var blacklistedFiles = map[string]string{ + // Anchor resolution edge cases + "optional/anchor.json": "contains edge cases for anchor resolution", + "anchor.json": "contains edge cases for anchor resolution", + + // Unknown keyword and ID edge cases + "optional/unknownKeyword.json": "contains edge cases for unknown keyword handling", + "optional/id.json": "contains edge cases for ID resolution", +} + +// Blacklisted test cases within specific files +// Key format: "filename:case_number" +// TODO work on improving support for these test cases +var blacklistedTestCases = map[string]string{ + // Remote reference tests that require $id base URI change support + "refRemote.json:2": "requires anchor resolution support", + "refRemote.json:4": "requires $id base URI change support", + "refRemote.json:5": "requires $id base URI change support", + "refRemote.json:6": "requires $id base URI change support", + "refRemote.json:7": "requires $id base URI change support", + "refRemote.json:8": "requires external reference resolution", + "refRemote.json:9": "requires anchor resolution support", + "refRemote.json:10": "requires $id base URI change support", + "refRemote.json:13": "requires nested absolute reference resolution", + "refRemote.json:14": "requires detached anchor resolution support", + + // ref.json tests that require advanced reference resolution features + "ref.json:11": "requires external reference resolution with $id", + "ref.json:15": "requires relative URI resolution with $id", + "ref.json:16": "requires absolute URI resolution with $id", + "ref.json:17": "requires complex $id resolution chain", + "ref.json:18": "requires $id evaluation before $ref", + "ref.json:19": "requires $id and $anchor evaluation before $ref", + "ref.json:20": "requires URN scheme support", + "ref.json:25": "requires URN scheme with JSON pointer", + "ref.json:26": "requires URN scheme with anchor", + "ref.json:27": "requires URN scheme with nested references", + "ref.json:28": "requires conditional schema reference resolution", + "ref.json:29": "requires conditional schema reference resolution", + "ref.json:30": "requires conditional schema reference resolution", + "ref.json:31": "requires absolute path reference resolution", + + // dynamicRef.json tests - all failing due to lack of dynamic reference support + "dynamicRef.json:0": "requires dynamic reference resolution support", + "dynamicRef.json:2": "requires dynamic reference resolution support", + "dynamicRef.json:3": "requires dynamic reference resolution support", + "dynamicRef.json:4": "requires dynamic reference resolution support", + "dynamicRef.json:5": "requires dynamic reference resolution support", + "dynamicRef.json:6": "requires dynamic reference resolution support", + "dynamicRef.json:7": "requires dynamic reference resolution support", + "dynamicRef.json:8": "requires dynamic reference resolution support", + "dynamicRef.json:9": "requires dynamic reference resolution support", + "dynamicRef.json:10": "requires dynamic reference resolution support", + "dynamicRef.json:11": "requires dynamic reference resolution support", + "dynamicRef.json:12": "requires dynamic reference resolution support", + "dynamicRef.json:13": "requires dynamic reference resolution support", + "dynamicRef.json:14": "requires dynamic reference resolution support", + "dynamicRef.json:15": "requires dynamic reference resolution support", + "dynamicRef.json:16": "requires dynamic reference resolution support", + "dynamicRef.json:19": "requires dynamic reference resolution support", + + // optional/dynamicRef.json tests + "optional/dynamicRef.json:0": "requires dynamic reference resolution support", + + // unevaluatedItems.json tests with dynamicRef + "unevaluatedItems.json:18": "requires dynamic reference resolution support", + + // unevaluatedProperties.json tests with dynamicRef + "unevaluatedProperties.json:21": "requires dynamic reference resolution support", +} + +const testSuiteDir = "testsuite/tests/draft2020-12" + +// Global variable to hold the remote server instance +var remoteServer *RemoteServer + +// Thread-safe coverage tracking +type CoverageTracker struct { + totalFiles int64 + skippedFiles int64 + totalCases int64 + skippedCases int64 + passedCases int64 +} + +func (c *CoverageTracker) AddFile() { + atomic.AddInt64(&c.totalFiles, 1) +} + +func (c *CoverageTracker) AddSkippedFile() { + atomic.AddInt64(&c.skippedFiles, 1) +} + +func (c *CoverageTracker) AddCases(total, skipped, passed int64) { + atomic.AddInt64(&c.totalCases, total) + atomic.AddInt64(&c.skippedCases, skipped) + atomic.AddInt64(&c.passedCases, passed) +} + +func (c *CoverageTracker) GetStats() (int, int, int, int, int) { + return int(atomic.LoadInt64(&c.totalFiles)), + int(atomic.LoadInt64(&c.skippedFiles)), + int(atomic.LoadInt64(&c.totalCases)), + int(atomic.LoadInt64(&c.skippedCases)), + int(atomic.LoadInt64(&c.passedCases)) +} + +func TestMain(m *testing.M) { + // Check if the git submodule is initialized + if !isSubmoduleInitialized(testSuiteDir) { + log.Println("JSON Schema Test Suite submodule not initialized. Run 'git submodule update --init --recursive' to enable these tests.") + return + } + + // Start the remote server for remote reference tests + var err error + remoteServer, err = startRemoteServer() + if err != nil { + log.Printf("Warning: Failed to start remote server for remote reference tests: %v", err) + log.Println("Remote reference tests will be skipped.") + } else { + log.Printf("Remote server started at %s", remoteServer.GetBaseURL()) + } + + // Run tests + exitCode := m.Run() + + // Clean up the remote server + if remoteServer != nil { + remoteServer.Stop() + } + + os.Exit(exitCode) +} + +// TestJSONSchemaTestSuite_RoundTrip runs the JSON Schema Test Suite tests +// focusing on schema parsing, validation, reference resolution, and round-trip marshalling +func TestJSONSchemaTestSuite_RoundTrip(t *testing.T) { + t.Parallel() + + // Get all test files + testFiles := getAllTestFiles(t, testSuiteDir) + + // Thread-safe coverage tracking + tracker := &CoverageTracker{} + var wg sync.WaitGroup + + for _, testFile := range testFiles { + tracker.AddFile() + + // Check if this file is blacklisted + if reason, isBlacklisted := blacklistedFiles[testFile]; isBlacklisted { + tracker.AddSkippedFile() + t.Run(testFile, func(t *testing.T) { + t.Skipf("Skipping blacklisted file: %s", reason) + }) + continue + } + + wg.Add(1) + t.Run(testFile, func(t *testing.T) { + defer wg.Done() + t.Parallel() + fileCases, fileSkipped, filePassed := runRoundTripTestFile(t, filepath.Join(testSuiteDir, testFile)) + tracker.AddCases(int64(fileCases), int64(fileSkipped), int64(filePassed)) + }) + } + + // Print coverage summary after all tests complete + t.Cleanup(func() { + wg.Wait() // Wait for all parallel tests to complete + totalFiles, skippedFiles, totalCases, skippedCases, passedCases := tracker.GetStats() + printCoverageSummary(t, totalFiles, skippedFiles, totalCases, skippedCases, passedCases) + }) +} + +// getAllTestFiles returns all JSON test files in the test suite directory +func getAllTestFiles(t *testing.T, testSuiteDir string) []string { + t.Helper() + var testFiles []string + + err := filepath.WalkDir(testSuiteDir, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + + if d.IsDir() { + return nil + } + + if strings.HasSuffix(path, ".json") { + // Get relative path from testSuiteDir + relPath, err := filepath.Rel(testSuiteDir, path) + if err != nil { + return err + } + testFiles = append(testFiles, relPath) + } + + return nil + }) + + require.NoError(t, err) + return testFiles +} + +// runRoundTripTestFile runs all test cases in a single test file with round-trip testing +// Returns: totalCases, skippedCases, passedCases +func runRoundTripTestFile(t *testing.T, testFilePath string) (int, int, int) { + t.Helper() + // Read the test file + data, err := os.ReadFile(testFilePath) + require.NoError(t, err, "failed to read test file: %s", testFilePath) + + // Parse the test cases + var testCases []TestCase + err = json.Unmarshal(data, &testCases) + require.NoError(t, err, "failed to parse test file: %s", testFilePath) + + var skippedCases, passedCases int + + // Run each test case + for i, testCase := range testCases { + t.Run(fmt.Sprintf("case_%d_%s", i, sanitizeTestName(testCase.Description)), func(t *testing.T) { + // Check if this specific test case is blacklisted + fileName := filepath.Base(testFilePath) + testCaseKey := fmt.Sprintf("%s:%d", fileName, i) + if reason, isBlacklisted := blacklistedTestCases[testCaseKey]; isBlacklisted { + skippedCases++ + t.Skipf("Skipping blacklisted test case: %s", reason) + return + } + + runRoundTripTestCase(t, testCase, testFilePath) + passedCases++ + }) + } + + return len(testCases), skippedCases, passedCases +} + +// runRoundTripTestCase runs a single test case with round-trip testing +func runRoundTripTestCase(t *testing.T, testCase TestCase, testFilePath string) { + t.Helper() + ctx := t.Context() + + // Step 1: Unmarshal the schema + var schema oas3.JSONSchema[oas3.Referenceable] + validationErrs, err := marshaller.Unmarshal(ctx, bytes.NewReader(testCase.Schema), &schema) + require.NoError(t, err, "failed to unmarshal schema for test case: %s", testCase.Description) + require.Empty(t, validationErrs, "schema validation errors for test case: %s", testCase.Description) + + // Step 2: Run validation on the schema + schemaValidationErrs := schema.Validate(ctx) + require.Empty(t, schemaValidationErrs, "schema validation failed for test case: %s", testCase.Description) + + // Step 4: Use the Walk API to walk through the schema and resolve any references + err = walkAndResolveReferences(ctx, t, &schema, testFilePath) + require.NoError(t, err, "failed to walk and resolve references for test case: %s", testCase.Description) + + // Step 5: Marshal the schema back to JSON + var buf bytes.Buffer + err = marshaller.Marshal(ctx, &schema, &buf) + require.NoError(t, err, "failed to marshal schema for test case: %s", testCase.Description) + + // Step 6: Use assert.JSONEq to check the schema makes a successful round trip + originalJSON := string(testCase.Schema) + roundTripJSON := buf.String() + + // For round-trip comparison, we need to normalize both JSONs since the order might differ + // and some fields might be added/removed during the process + assert.JSONEq(t, originalJSON, roundTripJSON, "schema round-trip failed for test case: %s", testCase.Description) + + // Log success information + t.Logf("โœ… Round-trip successful for test case: %s", testCase.Description) + t.Logf(" Original schema size: %d bytes", len(originalJSON)) + t.Logf(" Round-trip schema size: %d bytes", len(roundTripJSON)) +} + +// walkAndResolveReferences walks through the schema using the Walk API and resolves any references +func walkAndResolveReferences(ctx context.Context, t *testing.T, schema *oas3.JSONSchema[oas3.Referenceable], testFilePath string) error { + t.Helper() + if schema == nil { + return nil + } + + // Walk through the schema and resolve any references we find + for item := range oas3.Walk(ctx, schema) { + err := item.Match(oas3.SchemaMatcher{ + Schema: func(s *oas3.JSONSchema[oas3.Referenceable]) error { + // If this is a reference, try to resolve it + if s.IsReference() { + // Create resolve options + resolveOpts := oas3.ResolveOptions{ + TargetLocation: testFilePath, + RootDocument: schema, + } + + // If we have a remote server running, use its custom HTTP client + if remoteServer != nil { + resolveOpts.HTTPClient = remoteServer.GetHTTPClient() + } + + // Attempt to resolve the reference + // Most test suite schemas should have resolvable references within the schema + vErrs, resolveErr := s.Resolve(ctx, resolveOpts) + + assert.NoError(t, resolveErr) + assert.Empty(t, vErrs) + } + return nil + }, + }) + if err != nil { + return fmt.Errorf("failed to process schema during walk: %w", err) + } + } + + return nil +} + +// sanitizeTestName sanitizes a test name for use as a Go test name +func sanitizeTestName(name string) string { + // Replace spaces and special characters with underscores + name = strings.ReplaceAll(name, " ", "_") + name = strings.ReplaceAll(name, "-", "_") + name = strings.ReplaceAll(name, "(", "_") + name = strings.ReplaceAll(name, ")", "_") + name = strings.ReplaceAll(name, "[", "_") + name = strings.ReplaceAll(name, "]", "_") + name = strings.ReplaceAll(name, "{", "_") + name = strings.ReplaceAll(name, "}", "_") + name = strings.ReplaceAll(name, ".", "_") + name = strings.ReplaceAll(name, ",", "_") + name = strings.ReplaceAll(name, ":", "_") + name = strings.ReplaceAll(name, ";", "_") + name = strings.ReplaceAll(name, "!", "_") + name = strings.ReplaceAll(name, "?", "_") + name = strings.ReplaceAll(name, "'", "_") + name = strings.ReplaceAll(name, "\"", "_") + name = strings.ReplaceAll(name, "/", "_") + name = strings.ReplaceAll(name, "\\", "_") + name = strings.ReplaceAll(name, "<", "_") + name = strings.ReplaceAll(name, ">", "_") + name = strings.ReplaceAll(name, "=", "_") + name = strings.ReplaceAll(name, "+", "_") + name = strings.ReplaceAll(name, "*", "_") + name = strings.ReplaceAll(name, "&", "_") + name = strings.ReplaceAll(name, "%", "_") + name = strings.ReplaceAll(name, "$", "_") + name = strings.ReplaceAll(name, "#", "_") + name = strings.ReplaceAll(name, "@", "_") + + // Remove multiple consecutive underscores + for strings.Contains(name, "__") { + name = strings.ReplaceAll(name, "__", "_") + } + + // Trim leading and trailing underscores + name = strings.Trim(name, "_") + + // Ensure the name is not empty + if name == "" { + name = "unnamed_test" + } + + return name +} + +// isSubmoduleInitialized checks if the git submodule is properly initialized +func isSubmoduleInitialized(testSuiteDir string) bool { + // Check if the directory exists and contains expected files + if _, err := os.Stat(testSuiteDir); os.IsNotExist(err) { + return false + } + + // Check if the directory contains test files (should have .json files) + entries, err := os.ReadDir(testSuiteDir) + if err != nil { + return false + } + + // Look for at least one .json file to confirm the submodule is initialized + for _, entry := range entries { + if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".json") { + return true + } + } + + return false +} + +// printCoverageSummary prints a summary of test coverage statistics +func printCoverageSummary(t *testing.T, totalFiles, skippedFiles, totalCases, skippedCases, passedCases int) { + t.Helper() + + runFiles := totalFiles - skippedFiles + filesCoverage := float64(runFiles) / float64(totalFiles) * 100 + + casesCoverage := float64(passedCases) / float64(totalCases) * 100 + + t.Logf("\n"+ + "๐Ÿ“Š JSON Schema Test Suite Coverage Summary\n"+ + "==========================================\n"+ + "Files: %d/%d (%.1f%%) - %d skipped\n"+ + "Test Cases: %d/%d (%.1f%%) - %d skipped\n"+ + "Status: %d passed, %d skipped, %d total\n", + runFiles, totalFiles, filesCoverage, skippedFiles, + passedCases, totalCases, casesCoverage, skippedCases, + passedCases, skippedCases, totalCases) +} diff --git a/marshaller/populator.go b/marshaller/populator.go index cf79fad..ce97bd5 100644 --- a/marshaller/populator.go +++ b/marshaller/populator.go @@ -10,21 +10,26 @@ import ( // Pre-computed reflection types for performance var ( - nodeAccessorType = reflect.TypeOf((*NodeAccessor)(nil)).Elem() - populatorType = reflect.TypeOf((*Populator)(nil)).Elem() - sequencedMapType = reflect.TypeOf((*interfaces.SequencedMapInterface)(nil)).Elem() - coreModelerType = reflect.TypeOf((*CoreModeler)(nil)).Elem() - yamlNodePtrType = reflect.TypeOf((*yaml.Node)(nil)) - yamlNodeType = reflect.TypeOf(yaml.Node{}) - yamlNodePtrPtrType = reflect.TypeOf((**yaml.Node)(nil)) - populatorValueTag = "populatorValue" - populatorValueTrue = "true" + nodeAccessorType = reflect.TypeOf((*NodeAccessor)(nil)).Elem() + populatorType = reflect.TypeOf((*Populator)(nil)).Elem() + parentAwarePopulatorType = reflect.TypeOf((*ParentAwarePopulator)(nil)).Elem() + sequencedMapType = reflect.TypeOf((*interfaces.SequencedMapInterface)(nil)).Elem() + coreModelerType = reflect.TypeOf((*CoreModeler)(nil)).Elem() + yamlNodePtrType = reflect.TypeOf((*yaml.Node)(nil)) + yamlNodeType = reflect.TypeOf(yaml.Node{}) + yamlNodePtrPtrType = reflect.TypeOf((**yaml.Node)(nil)) + populatorValueTag = "populatorValue" + populatorValueTrue = "true" ) type Populator interface { Populate(source any) error } +type ParentAwarePopulator interface { + PopulateWithParent(source any, parent any) error +} + func Populate(source any, target any) error { t := reflect.ValueOf(target) @@ -59,10 +64,47 @@ func Populate(source any, target any) error { } } - return populateValue(source, t) + return populateValueWithParent(source, t, nil) +} + +func PopulateWithParent(source any, target any, parent any) error { + t := reflect.ValueOf(target) + + if t.Kind() == reflect.Ptr && t.IsNil() { + t.Set(CreateInstance(t.Type().Elem())) + } + if t.Kind() == reflect.Ptr { + t = t.Elem() + } + + s := reflect.ValueOf(source) + if s.Type().Implements(nodeAccessorType) { + source = source.(NodeAccessor).GetValue() + } + + // Special case for yaml.Node conversion (similar to unmarshaller.go:216-223) + switch { + case t.Type() == yamlNodePtrType: + if node, ok := source.(yaml.Node); ok { + t.Set(reflect.ValueOf(&node)) + return nil + } + case t.Type() == yamlNodeType: + if node, ok := source.(*yaml.Node); ok { + t.Set(reflect.ValueOf(*node)) + return nil + } + case t.Type() == yamlNodePtrPtrType: + if node, ok := source.(*yaml.Node); ok { + t.Set(reflect.ValueOf(&node)) + return nil + } + } + + return populateValueWithParent(source, t, parent) } -func populateModel(source any, target any) error { +func PopulateModel(source any, target any) error { s := reflect.ValueOf(source) t := reflect.ValueOf(target) @@ -113,7 +155,7 @@ func populateModel(source any, target any) error { if field.Anonymous { if targetSeqMap := getSequencedMapInterface(tField); targetSeqMap != nil { sourceForPopulation := getSourceForPopulation(s.Field(i), fieldInt) - if err := populateSequencedMap(sourceForPopulation, targetSeqMap); err != nil { + if err := populateSequencedMap(sourceForPopulation, targetSeqMap, target); err != nil { return err } } @@ -160,7 +202,7 @@ func populateModel(source any, target any) error { nodeValue = nodeAccessor.GetValue() } - if err := populateValue(nodeValue, tField); err != nil { + if err := populateValueWithParent(nodeValue, tField, target); err != nil { return err } } @@ -168,7 +210,7 @@ func populateModel(source any, target any) error { return nil } -func populateValue(source any, target reflect.Value) error { +func populateValueWithParent(source any, target reflect.Value, parent any) error { value := reflect.ValueOf(source) // Handle nil source early - when source is nil, reflect.ValueOf returns a zero Value @@ -208,18 +250,21 @@ func populateValue(source any, target reflect.Value) error { } targetType := target.Type() + if targetType.Implements(parentAwarePopulatorType) { + return target.Interface().(ParentAwarePopulator).PopulateWithParent(value.Interface(), parent) + } if targetType.Implements(populatorType) { return target.Interface().(Populator).Populate(value.Interface()) } // Check if target is a sequenced map and handle it specially if targetType.Implements(sequencedMapType) && !isEmbeddedSequencedMapType(value.Type()) { - return populateSequencedMap(value.Interface(), target.Interface().(interfaces.SequencedMapInterface)) + return populateSequencedMap(value.Interface(), target.Interface().(interfaces.SequencedMapInterface), parent) } // Check if target implements CoreSetter interface if coreSetter, ok := target.Interface().(CoreSetter); ok { - if err := populateModel(value.Interface(), target.Interface()); err != nil { + if err := PopulateModel(value.Interface(), target.Interface()); err != nil { return err } @@ -252,7 +297,7 @@ func populateValue(source any, target reflect.Value) error { } } - if err := populateValue(elementValue, target.Index(i)); err != nil { + if err := populateValueWithParent(elementValue, target.Index(i), target.Interface()); err != nil { return err } } diff --git a/marshaller/sequencedmap.go b/marshaller/sequencedmap.go index 36eaa44..24f4f21 100644 --- a/marshaller/sequencedmap.go +++ b/marshaller/sequencedmap.go @@ -112,7 +112,7 @@ func unmarshalSequencedMap(ctx context.Context, parentName string, node *yaml.No } // populateSequencedMap populates a target sequenced map from a source sequenced map -func populateSequencedMap(source any, target interfaces.SequencedMapInterface) error { +func populateSequencedMap(source any, target interfaces.SequencedMapInterface, parent any) error { if source == nil { return nil } @@ -154,7 +154,7 @@ func populateSequencedMap(source any, target interfaces.SequencedMapInterface) e targetValue = CreateInstance(valueType).Interface() } - if err := Populate(value, targetValue); err != nil { + if err := PopulateWithParent(value, targetValue, parent); err != nil { return err } diff --git a/mise-tasks/setup-submodules b/mise-tasks/setup-submodules new file mode 100755 index 0000000..cea9d5f --- /dev/null +++ b/mise-tasks/setup-submodules @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +set -euo pipefail + +echo "๐Ÿ”ง Setting up git submodules..." + +# Check if we're in a git repository +if [ ! -d ".git" ]; then + echo "โŒ Error: Not in a git repository" + exit 1 +fi + +# Initialize and update submodules +echo "๐Ÿ“ฅ Initializing and updating git submodules..." +git submodule update --init --recursive + +# Verify the JSON Schema Test Suite submodule is properly set up +if [ -d "jsonschema/oas3/tests/testsuite/tests/draft2020-12" ] && [ -n "$(ls -A jsonschema/oas3/tests/testsuite/tests/draft2020-12 2>/dev/null)" ]; then + echo "โœ… JSON Schema Test Suite submodule successfully initialized!" + echo " Location: jsonschema/oas3/tests/testsuite" + echo " Test files: $(find jsonschema/oas3/tests/testsuite/tests/draft2020-12 -name "*.json" | wc -l) JSON test files available" +else + echo "โš ๏ธ Warning: JSON Schema Test Suite submodule may not be fully initialized" + echo " Expected location: jsonschema/oas3/tests/testsuite/tests/draft2020-12" +fi + +echo "" +echo "๐Ÿงช You can now run 'mise run test' to execute all tests including JSON Schema Test Suite tests." \ No newline at end of file diff --git a/mise-tasks/test b/mise-tasks/test index df9daf2..8da773f 100755 --- a/mise-tasks/test +++ b/mise-tasks/test @@ -1,6 +1,13 @@ #!/usr/bin/env bash set -euo pipefail +# Check if JSON Schema Test Suite submodule is initialized +if [ ! -d "jsonschema/oas3/tests/testsuite/tests/draft2020-12" ] || [ -z "$(ls -A jsonschema/oas3/tests/testsuite/tests/draft2020-12 2>/dev/null)" ]; then + echo "โš ๏ธ JSON Schema Test Suite submodule not initialized." + echo " Some tests will be skipped. Run 'mise run setup-submodules' to enable all tests." + echo "" +fi + echo "๐Ÿงช Running tests with gotestsum..." gotestsum --format testname -- -race ./... echo "โœ… All tests passed!" \ No newline at end of file diff --git a/references/reference.go b/references/reference.go index 805646f..9fde75a 100644 --- a/references/reference.go +++ b/references/reference.go @@ -31,7 +31,16 @@ func (r Reference) GetJSONPointer() jsonpointer.JSONPointer { if len(parts) < 2 { return "" } - return jsonpointer.JSONPointer(strings.TrimSpace(parts[1])) + + pointer := strings.TrimSpace(parts[1]) + + // URL decode the JSON pointer to handle percent-encoded characters + // like %25 (which represents %) + if decoded, err := url.QueryUnescape(pointer); err == nil { + pointer = decoded + } + + return jsonpointer.JSONPointer(pointer) } func (r Reference) Validate() error { diff --git a/references/reference_test.go b/references/reference_test.go index cb0650e..3407cf0 100644 --- a/references/reference_test.go +++ b/references/reference_test.go @@ -103,11 +103,6 @@ func TestReference_Validate_Error(t *testing.T) { ref: "https://example .com/api.yaml#/User", expectError: "invalid reference URI", }, - { - name: "invalid JSON pointer - empty token after slash", - ref: "#/components//User", - expectError: "invalid reference JSON pointer", - }, } for _, tt := range tests { diff --git a/values/eithervalue.go b/values/eithervalue.go index 68e6f95..809bb8e 100644 --- a/values/eithervalue.go +++ b/values/eithervalue.go @@ -92,7 +92,7 @@ func (e *EitherValue[L, LCore, R, RCore]) RightValue() R { return *e.Right } -func (e *EitherValue[L, LCore, R, RCore]) Populate(source any) error { +func (e *EitherValue[L, LCore, R, RCore]) PopulateWithParent(source any, parent any) error { var ec *core.EitherValue[LCore, RCore] switch v := source.(type) { case *core.EitherValue[LCore, RCore]: @@ -107,7 +107,7 @@ func (e *EitherValue[L, LCore, R, RCore]) Populate(source any) error { e.SetCoreAny(ec) if ec.IsLeft { - if err := marshaller.Populate(ec.Left, &e.Left); err != nil { + if err := marshaller.PopulateWithParent(ec.Left, &e.Left, parent); err != nil { return fmt.Errorf("failed to populate left: %w", err) } diff --git a/yml/yml.go b/yml/yml.go index b4c0a3f..ee1962f 100644 --- a/yml/yml.go +++ b/yml/yml.go @@ -38,6 +38,7 @@ func CreateOrUpdateScalarNode(ctx context.Context, value any, valueNode *yaml.No if resolvedValueNode != nil { resolvedValueNode.Value = convNode.Value + resolvedValueNode.Tag = convNode.Tag // Also update the tag to match the new value type return valueNode }