Update module github.com/go-swagger/go-swagger to v0.36.0

Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
This commit is contained in:
renovate[bot] 2026-08-01 08:54:15 +00:00 committed by GitHub
parent 9b2de6085b
commit ea1679488f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
43 changed files with 1958 additions and 481 deletions

View file

@ -8,7 +8,7 @@ go 1.25.0
require (
github.com/cpuguy83/go-md2man/v2 v2.0.7
github.com/go-swagger/go-swagger v0.35.3
github.com/go-swagger/go-swagger v0.36.0
)
require (
@ -21,26 +21,26 @@ require (
github.com/felixge/httpsnoop v1.1.0 // indirect
github.com/fsnotify/fsnotify v1.10.1 // indirect
github.com/go-openapi/analysis v0.25.5 // indirect
github.com/go-openapi/codescan v0.35.1 // indirect
github.com/go-openapi/codescan v0.36.0 // indirect
github.com/go-openapi/errors v0.22.8 // indirect
github.com/go-openapi/inflect v0.21.6 // indirect
github.com/go-openapi/inflect v1.0.0 // indirect
github.com/go-openapi/jsonpointer v1.0.0 // indirect
github.com/go-openapi/jsonreference v1.0.0 // indirect
github.com/go-openapi/loads v0.25.0 // indirect
github.com/go-openapi/runtime v0.32.6 // indirect
github.com/go-openapi/runtime/server-middleware v0.32.6 // indirect
github.com/go-openapi/runtime v0.33.0 // indirect
github.com/go-openapi/runtime/server-middleware v0.33.0 // indirect
github.com/go-openapi/spec v0.22.9 // indirect
github.com/go-openapi/strfmt v0.27.0 // indirect
github.com/go-openapi/swag/conv v0.27.3 // indirect
github.com/go-openapi/swag/fileutils v0.27.3 // indirect
github.com/go-openapi/swag/jsonutils v0.27.3 // indirect
github.com/go-openapi/swag/loading v0.27.3 // indirect
github.com/go-openapi/swag/mangling v0.27.3 // indirect
github.com/go-openapi/swag/netutils v0.27.3 // indirect
github.com/go-openapi/swag/pools v0.27.3 // indirect
github.com/go-openapi/swag/stringutils v0.27.3 // indirect
github.com/go-openapi/swag/typeutils v0.27.3 // indirect
github.com/go-openapi/swag/yamlutils v0.27.3 // indirect
github.com/go-openapi/swag/conv v0.28.0 // indirect
github.com/go-openapi/swag/fileutils v0.28.0 // indirect
github.com/go-openapi/swag/jsonutils v0.28.0 // indirect
github.com/go-openapi/swag/loading v0.28.0 // indirect
github.com/go-openapi/swag/mangling v0.28.0 // indirect
github.com/go-openapi/swag/netutils v0.28.0 // indirect
github.com/go-openapi/swag/pools v0.28.0 // indirect
github.com/go-openapi/swag/stringutils v0.28.0 // indirect
github.com/go-openapi/swag/typeutils v0.28.0 // indirect
github.com/go-openapi/swag/yamlutils v0.28.0 // indirect
github.com/go-openapi/validate v0.26.1 // indirect
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
github.com/google/uuid v1.6.0 // indirect
@ -50,10 +50,10 @@ require (
github.com/kr/pretty v0.3.1 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/mattn/go-colorable v0.1.15 // indirect
github.com/mattn/go-isatty v0.0.23 // indirect
github.com/mattn/go-isatty v0.0.24 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/oklog/ulid/v2 v2.1.1 // indirect
github.com/oklog/ulid/v2 v2.1.2 // indirect
github.com/pelletier/go-toml/v2 v2.4.3 // indirect
github.com/rogpeppe/go-internal v1.15.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
@ -65,7 +65,7 @@ require (
github.com/spf13/viper v1.21.0 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/toqueteos/webbrowser v1.2.1 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
go.yaml.in/yaml/v3 v3.0.5 // indirect
golang.org/x/crypto v0.54.0 // indirect
golang.org/x/mod v0.38.0 // indirect
golang.org/x/net v0.57.0 // indirect

View file

@ -23,57 +23,57 @@ github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx5
github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo=
github.com/go-openapi/analysis v0.25.5 h1:xPYEvTb90o1y0epuiOPAoG4QqahjP3cdp5xNlHeKJRI=
github.com/go-openapi/analysis v0.25.5/go.mod h1:d3UGtQC5uq5Kqqqis2VH09Km/v3vwsWrYkbp4gdm+Rc=
github.com/go-openapi/codescan v0.35.1 h1:ajmiS6ha1cA6S/V4uSF1KLEEi5/DqhM7Bef1polLl14=
github.com/go-openapi/codescan v0.35.1/go.mod h1:2mzXphT7B2mM6IsMcP8pi01t2apd3LlIOuemfz4KLqI=
github.com/go-openapi/codescan v0.36.0 h1:sp2RVgGp5IUlYRU3kwlQK6cUiQ9a1GNbgKbaAjdeSoI=
github.com/go-openapi/codescan v0.36.0/go.mod h1:n0z3IJDm3ysnon+bLfBbHoy0WrRzlBE0LfeuAxGEnuM=
github.com/go-openapi/errors v0.22.8 h1:oP7sW7TWc3wFFjrzzj0nI83H2qMBkNjNfSd+XRejk/I=
github.com/go-openapi/errors v0.22.8/go.mod h1:BuUoHcYrU6E7V9gfj1I5wLQqgtIHnup/alXZ8KdgQ0w=
github.com/go-openapi/inflect v0.21.6 h1:0Se5BlyDT4hnV9JQQKA63W9TIUU6SC4hPJiY3qJOEzQ=
github.com/go-openapi/inflect v0.21.6/go.mod h1:ksYcnLD7j24H79hdqOMmWaLXjFXd0LTkRoBA0UazLW8=
github.com/go-openapi/inflect v1.0.0 h1:IzG7K5YBu97odaCXhjODGGt25WaNtEYLJc9NfUcW4AI=
github.com/go-openapi/inflect v1.0.0/go.mod h1:ksYcnLD7j24H79hdqOMmWaLXjFXd0LTkRoBA0UazLW8=
github.com/go-openapi/jsonpointer v1.0.0 h1:kR9tHqY0CtZaOPVFm622dPVNhrvYpwr4uCxgL3h1H8s=
github.com/go-openapi/jsonpointer v1.0.0/go.mod h1:Z3rw7dWu1p9IgitXCFamSlA5lmDiklEB6vkaxcNZW5Y=
github.com/go-openapi/jsonreference v1.0.0 h1:jlmTr6torcd1YgDQvSfNmRtKzYDO4FGBkrAdlAVWnpY=
github.com/go-openapi/jsonreference v1.0.0/go.mod h1:jtwdyGbJk0Xhe5Y+rwtglQP6Sb1WZST4rT32LWB+sv0=
github.com/go-openapi/loads v0.25.0 h1:74Bc2snfaVlsHzwdQj/3gsA9XJz3daXTJVs+4ZaK7jI=
github.com/go-openapi/loads v0.25.0/go.mod h1:JFBw4SIB9+PTIFHDfcXuSSy5h6aWzjtUCrPYyx3qWU8=
github.com/go-openapi/runtime v0.32.6 h1:hrcTTF8P7ZZr2Majzq11I65QtL/s85o7Q+zJf+AvFN4=
github.com/go-openapi/runtime v0.32.6/go.mod h1:+rsupH3+TFKqmFysqkmgBOTxpVJV8eV+j9myvvea2Xw=
github.com/go-openapi/runtime/server-middleware v0.32.6 h1:IGTYzybyFrUeSqQEwwO1y/9KnOk4QsabFNtAQtIHxDE=
github.com/go-openapi/runtime/server-middleware v0.32.6/go.mod h1:OQHTBqMGquJShXhPYQ62yAqDMtC1rYpsEwldNWjYKhA=
github.com/go-openapi/runtime v0.33.0 h1:Dd3Oj2ig+WH8ckK95l0Wn2V8a4bH/UqWPRZVT0vc8yU=
github.com/go-openapi/runtime v0.33.0/go.mod h1:+rsupH3+TFKqmFysqkmgBOTxpVJV8eV+j9myvvea2Xw=
github.com/go-openapi/runtime/server-middleware v0.33.0 h1:ZFUNyaa2eUs9DhLd6MTe/QRsuxeYn4Lq0C8iAoY13XE=
github.com/go-openapi/runtime/server-middleware v0.33.0/go.mod h1:OQHTBqMGquJShXhPYQ62yAqDMtC1rYpsEwldNWjYKhA=
github.com/go-openapi/spec v0.22.9 h1:/vKIFDcGKp0ktZWGbym/tJEWbk6/XOEmAVU0kqKMH+w=
github.com/go-openapi/spec v0.22.9/go.mod h1:b/mNUYIOQOyIiUzUzXEE8xzyZqf93KvM9hQGP91yfl0=
github.com/go-openapi/strfmt v0.27.0 h1:kbcTeaD9TXuXD0hhMXzuYa1sdTo6+dWGvwjW93E80IM=
github.com/go-openapi/strfmt v0.27.0/go.mod h1:s/qhDqfY72irigXUGJmtgid2Rm+3tnz3k8hZaRmvWYc=
github.com/go-openapi/swag v0.27.3 h1:i6oVKkGZeFgETHMiBHGtj9gIQ1aLtWDdJnT/SRZeets=
github.com/go-openapi/swag/conv v0.27.3 h1:iqJFmGEjmX3AY0lSszABFqRVqOSt99XS0LzNIMJYuhU=
github.com/go-openapi/swag/conv v0.27.3/go.mod h1:nPRmN6jgNme99hpf+nM0auDZGALWIqlwhisKPK/bQhQ=
github.com/go-openapi/swag/fileutils v0.27.3 h1:3UVoZ2RLaIs1lt+2jcKzL8RM3Yk0rmsDE9FLA/HGxFE=
github.com/go-openapi/swag/fileutils v0.27.3/go.mod h1:VvJFZLTZS0AI854gEQz5tk7dBESdLjiNUMSZ/th2ry8=
github.com/go-openapi/swag/jsonutils v0.27.3 h1:1DEz+O82frtSMBcos/7XIn1GnpNTbsD4Bru4Dc/uhRc=
github.com/go-openapi/swag/jsonutils v0.27.3/go.mod h1:qiDCoQvzkMxrV3G8FLEdIU5L+EFYc0zcDOHWT3Yofvo=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.27.3 h1:h/eT9kmGCDdFLJF29lOhzLtF0FmP1AX2MhLJWVebsb8=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.27.3/go.mod h1:mofwUWx70wvskwESqRJ//k/9kURmCgyJl5m5Ppoh5kY=
github.com/go-openapi/swag/loading v0.27.3 h1:L9nQkEgzU7QgFQL+pLEMfGUKxeM4pWwGwbET9Z3weW0=
github.com/go-openapi/swag/loading v0.27.3/go.mod h1:rJ0NeaKsF4CVPnMGjPQl7JlSHzvD0bc2DKXLss1hiuE=
github.com/go-openapi/swag/mangling v0.27.3 h1:gRzzD1PAUoLTtGMgI3KpBmCSOlTuLTFWnviLxLcTnyg=
github.com/go-openapi/swag/mangling v0.27.3/go.mod h1:jtBE2+V+3pILxOR7Vgce+Cwp6A2PgZbvVqfNntbVs0w=
github.com/go-openapi/swag/netutils v0.27.3 h1:IoBvfCoprsE6E87kAIm9basnISqDDqB79mJ8MN+f5PU=
github.com/go-openapi/swag/netutils v0.27.3/go.mod h1:J+WYyFMLtvtCGqa6jLv+YNUmIKI3ZRQRrvfNDMoQoEQ=
github.com/go-openapi/swag/pools v0.27.3 h1:gXjImP3F6/56wRRcFgEPld084Y6u2gs21ikPBt8NKBk=
github.com/go-openapi/swag/pools v0.27.3/go.mod h1:kVQefhSK5RWuRe7BXsL8htgBPAMpN7HDGpGEknqugeE=
github.com/go-openapi/swag/stringutils v0.27.3 h1:Ru28hnbAvN5wycALQYy8IobHvASq+FUFMlp1QzLM0JI=
github.com/go-openapi/swag/stringutils v0.27.3/go.mod h1:lzRN95CxXmA03XcDWHLOb6nOMcxCqR5rGY0lOgsfRoM=
github.com/go-openapi/swag/typeutils v0.27.3 h1:l6SSrx5eR5/WVwrGNzN6bQ9WqL04mrxNBl9YgQ3rcJ4=
github.com/go-openapi/swag/typeutils v0.27.3/go.mod h1:Srm0xFNRZ1Y+vCxJclo5qzx8aj+1pAKda/YfFPrG0dQ=
github.com/go-openapi/swag/yamlutils v0.27.3 h1:cRFCAoYtslYn9L9T0xWryHy1t7c1MACC+DMj3CLvwvs=
github.com/go-openapi/swag/yamlutils v0.27.3/go.mod h1:6JYBGj8sw/NawMllyZY+cTA8Mzk2etS3ZBASdcyPsiU=
github.com/go-openapi/swag v0.28.0 h1:xkgbOSKj6DZziNpyqRRAOt3GJGtgjgsd2RoyT30VWuw=
github.com/go-openapi/swag/conv v0.28.0 h1:GtqqbyFe7vR5Y7ehxG9W6/OvrSFdf1OLeTGp40TqxH8=
github.com/go-openapi/swag/conv v0.28.0/go.mod h1:mbUE+mzctnhxi864m0Q07SpN8OowD9JhxmxuYvZZD/k=
github.com/go-openapi/swag/fileutils v0.28.0 h1:Z04XWQD7R8Eq+7GnOrjovBxPPmZzsS4gt2H2GPGIViU=
github.com/go-openapi/swag/fileutils v0.28.0/go.mod h1:VvJFZLTZS0AI854gEQz5tk7dBESdLjiNUMSZ/th2ry8=
github.com/go-openapi/swag/jsonutils v0.28.0 h1:YIch6FwO7RXzeAnbO8Tu7dWBZeUEH+4nA0HXltVTnv4=
github.com/go-openapi/swag/jsonutils v0.28.0/go.mod h1:CYM3WlTUcagR2ZoHdz54di/cbBqt82tuxuXgAjxw+mg=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.28.0 h1:qV+VVUAx5Oro8WjVWpZeql7YReTKhT4smR4zhcOQZr0=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.28.0/go.mod h1:mofwUWx70wvskwESqRJ//k/9kURmCgyJl5m5Ppoh5kY=
github.com/go-openapi/swag/loading v0.28.0 h1:td8QZdZC9MIYGGSnSPKShKiK22I2tU5UQvuUhIBPRLU=
github.com/go-openapi/swag/loading v0.28.0/go.mod h1:rXB0QiQX5mMveXEA7ouM4KiiM9jVJe4K6BVbwhD1M4k=
github.com/go-openapi/swag/mangling v0.28.0 h1:pH8eyeNO9SLYsTMWJrurnNfKmDa28XrlA+HePVD53VM=
github.com/go-openapi/swag/mangling v0.28.0/go.mod h1:jtBE2+V+3pILxOR7Vgce+Cwp6A2PgZbvVqfNntbVs0w=
github.com/go-openapi/swag/netutils v0.28.0 h1:YXN6TALEi2pzts8/8GNm6T61HTAZsieukGZidap989k=
github.com/go-openapi/swag/netutils v0.28.0/go.mod h1:J+WYyFMLtvtCGqa6jLv+YNUmIKI3ZRQRrvfNDMoQoEQ=
github.com/go-openapi/swag/pools v0.28.0 h1:HPMZWSAfce3rdVTFcjFiCIBtDg9h4x2QlRrHipwhxeU=
github.com/go-openapi/swag/pools v0.28.0/go.mod h1:kVQefhSK5RWuRe7BXsL8htgBPAMpN7HDGpGEknqugeE=
github.com/go-openapi/swag/stringutils v0.28.0 h1:ixsc9iYgDPubHL/8nSkbnryEHpD2VRlBMLKpQyPXcDU=
github.com/go-openapi/swag/stringutils v0.28.0/go.mod h1:lzRN95CxXmA03XcDWHLOb6nOMcxCqR5rGY0lOgsfRoM=
github.com/go-openapi/swag/typeutils v0.28.0 h1:nRBKSBXjDgf01VDPB3fWeD9nQuhCOVeIYAkUx2tbkyY=
github.com/go-openapi/swag/typeutils v0.28.0/go.mod h1:Srm0xFNRZ1Y+vCxJclo5qzx8aj+1pAKda/YfFPrG0dQ=
github.com/go-openapi/swag/yamlutils v0.28.0 h1:TV3JXH6DS46KUroDtMLAYHGkdWf5VDq3wVWFirmzROY=
github.com/go-openapi/swag/yamlutils v0.28.0/go.mod h1:x0q/yndZHEgk9Rx3DyDqzFUmHy55KTvIZldvF2dTJXs=
github.com/go-openapi/testify/enable/yaml/v2 v2.6.0 h1:gGHwAJ0R/5jU8BEGDbfRNR3hL68dAVi84WuOApp29B0=
github.com/go-openapi/testify/enable/yaml/v2 v2.6.0/go.mod h1:tY+St1SGq4NFl0QIqdTY4aEdbChAHxhyB77XQi9iJCo=
github.com/go-openapi/testify/v2 v2.6.0 h1:5PKH2HE7YJ/LuRPQGvSxBRlFXNQhSetBLlGAgUEu3ug=
github.com/go-openapi/testify/v2 v2.6.0/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw=
github.com/go-openapi/validate v0.26.1 h1:pZSbvtRO8G2R2FpWTYRn3w8LrsNwbtaVhP2dWiBa0Us=
github.com/go-openapi/validate v0.26.1/go.mod h1:B8UMgXiQiwwQWIbmuROlwJZDPGlikPuh7iHV1vPX9Oo=
github.com/go-swagger/go-swagger v0.35.3 h1:TjesBnknEKhd5y1KOngCAYuySA+SN42UmSy4kYtTLNU=
github.com/go-swagger/go-swagger v0.35.3/go.mod h1:qvb30RtfTWVMAQKI0f4dNyHsulfVapjDfobkGP6ZbWc=
github.com/go-swagger/go-swagger v0.36.0 h1:YDdoOZ6QNzRWZwoRNLT9hOKN8xdvlSzjXM9JMRE9Gvc=
github.com/go-swagger/go-swagger v0.36.0/go.mod h1:zGi9Fhr7htWfJwNtjhEE93A1ITPDoeCulv4ASB46Gg8=
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
@ -92,14 +92,14 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY=
github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.23 h1:cYwCQTQf3HB6xUC+BtyCLZNr7IzbOmoZbmssVNzSyiQ=
github.com/mattn/go-isatty v0.0.23/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A=
github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI=
github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s=
github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
github.com/oklog/ulid/v2 v2.1.2 h1:IEclFb9JNvzYA6MW2SCxbLzcHTVsfqm3PrqGQJH5zec=
github.com/oklog/ulid/v2 v2.1.2/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=
github.com/pelletier/go-toml/v2 v2.4.3 h1:GTRvJQutkOSftxIFD5xw9aepkYNuPWmVJpffdDPYVpY=
github.com/pelletier/go-toml/v2 v2.4.3/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
@ -129,8 +129,8 @@ github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/toqueteos/webbrowser v1.2.1 h1:O7IsnnU7XQyJ1nHMRfAktUUJOAZD3aQyUVnxzhWphCg=
github.com/toqueteos/webbrowser v1.2.1/go.mod h1:XWoZq4cyp9WeUeak7w7LXRUQf1F1ATJMir8RTqb4ayM=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk=

View file

@ -1,8 +1,9 @@
codecov:
notify:
after_n_builds: 2
coverage:
status:
patch:
default:
target: 80%
ignore:
- internal/scantest
- fixtures

View file

@ -2,5 +2,6 @@
# so byte-level comparison doesn't trip on Windows checkouts that
# default to core.autocrlf=true.
*.json text eol=lf
*.txt text eol=lf
internal/parsers/grammar/grammar_test/testdata/golden/* text eol=lf
fixtures/integration/golden/* text eol=lf

View file

@ -20,6 +20,9 @@ profile.cov
# Dependency directories (remove the comment below to include it)
# vendor/
# Go workspace: commit go.work, ignore the generated checksum file
go.work.sum
# env file
.env

View file

@ -7,6 +7,7 @@ linters:
- gomoddirectives # mono-repo, multi-modules (docs/examples): local replace directives are needed for proper releasing
- goconst # disabled, perhaps temporarily as this linter has become way too pick and noisy
- godox
- gomoddirectives
- gomodguard
- gomodguard_v2
- exhaustruct
@ -54,6 +55,25 @@ linters:
- third_party$
- builtin$
- examples$
rules:
# cmd/genspec-tui is a bubbletea front-end. These four disagree with the
# shape of TUI code rather than with the code itself, and each fires often
# enough (102 between them) that //nolint would be the scattering this
# repo's linting rule tells us to avoid. Scoped by path so the library
# keeps all four.
- path: cmd/genspec-tui/
linters:
# Layout arithmetic, where the number IS the explanation: w-2 is the
# two border columns, h-3 is border plus title row.
- mnd
# Key, mouse and message switches read open-ended INPUT, not a closed
# domain. Falling through to the code after the switch is the
# behaviour, so a required `default:` would be an empty statement
# asserting nothing. The scanner's go/types switches still get this.
- exhaustive
# clamp/clampInt keep a general (v, lo, hi) signature though every
# call site happens to pass lo=0.
- unparam
formatters:
enable:
- gofmt

View file

@ -18,7 +18,9 @@ Supports Go modules (since go1.11).
## Announcements
* **2025-04-19** : large package layout reshuffle
* **2026-07-31** : landed a new spec generation TUI tool
* **2026-04-19** : large package layout reshuffle
* the entire project is being refactored to restore a reasonable level of maintenability
* the only exposed API is Run() and Options.
@ -32,7 +34,7 @@ API is stable.
go get github.com/go-openapi/codescan
```
## Basic usage
## Basic usage as a library
```go
import (
@ -44,6 +46,22 @@ swaggerSpec, err := codescan.Run(&codescan.Options{
})
```
## Work with the TUI
This project comes with a terminal UI to quickly render a Swagger spec from source
and navigate your code annotations. It shows diagnostics and you may test the impact
of the various available options.
```cmd
go install github.com/go-openapi/codescan/cmd/genspec-tui@latest
```
```cmd
genspec-tui -workdir [my source location]
```
![tui_screenshot](docs/genspec-tui.png)
## Change log
See <https://github.com/go-openapi/codescan/releases>

View file

@ -0,0 +1,20 @@
go 1.25.0
// Workspace for the codescan monorepo: the main library module (.) and the
// genspec-tui front-end module (./cmd/genspec-tui), kept in separate go.mod
// files so the TUI's bubbletea dependency tree never pollutes the lean library.
//
// The workspace is what lets CI test every module in one pass (`go test work
// ./...`, via the shared go-test-monorepo workflow) — the TUI has no CI of its
// own. `go install .../cmd/genspec-tui@latest` ignores this file, so the TUI
// module's own go.mod carries the real `require` on the library.
//
// Keep the `go` directive above in step with the modules (all 1.25.0): raising
// it past them imposes a toolchain floor the code does not need, and would take
// the oldstable CI job out.
use (
.
./cmd/genspec-tui
./docs/examples
./fixtures
)

View file

@ -20,6 +20,7 @@ trade-offs, and known quirks live here.
- [§allof](#allof) — `buildAllOf`, `buildNamedAllOf`, `scanEmbeddedFields`
- [§embedded](#embedded) — embed routing, struct/interface specials asymmetry
- [§embed-depth](#embed-depth) — ambiguous-embed diagnostic mechanism
- [§omit](#omit) — `swagger:omit` — the author's pre-filter on promoted fields
- [§method-mangler](#method-mangler) — interface-method JSON-name derivation
- [§user-overrides](#user-overrides) — explicit user-driven type/format overrides at decl-site and field-site
- [§traceability](#traceability) — `x-go-name` / `x-go-package` / `x-go-type` origin extensions and `EmitXGoType`
@ -587,6 +588,90 @@ contribute an `{}` entry to the outer schema.
---
## <a id="omit"></a>§omit — `swagger:omit`, the author's pre-filter on promoted fields
Promoting an embed can produce a schema the author never meant, and codescan must not guess which
one they meant. The two cases that motivated the annotation (go-swagger#1992 plus the override
defects found while auditing `DefaultAllOfForEmbeds`):
| shape | Go marshals | inlined | composed (`allOf`) |
|---|---|---|---|
| outer `ID` re-declared to add `readOnly` | one `ID` | one, decorated | `ID` in BOTH members |
| outer `ID string` over `ID int64` | one, string | string | integer AND string — unsatisfiable |
| a shared type embedded in a request body | every field | every field | every field |
Inlining **resolves** an override (it applies Go's depth rule and emits the winner); `allOf`
**accumulates** — members conjoin, and conjunction can only narrow, never replace. So an override
that replaces is not expressible as composition, and no amount of tidying the members fixes it.
`swagger:omit` is the escape hatch: the author states which promoted fields do not belong, and the
scanner keeps mirroring the code otherwise.
### It is a pre-filter, not a post-hoc delete
`swagger:omit` means *"do not promote this field when walking the embed"*. One rule then covers both
renderings — the field is simply never written:
- inlined: an outer re-declaration wins as it already did (so omitting
its promoted twin is correctly a no-op there);
- composed: the base member is built without the field, which removes
the duplicate and the unsatisfiable pair above.
No mode-awareness, and no Go-name↔JSON-name reconciliation, because filtering happens *before* names
are computed — which is why targets are Go field names and why the annotation is indifferent to json
tags and to `NameFromTags`.
The filter itself is one condition in `processStructField` (`struct.go`); everything else lives in
`omit.go`.
### Resolution runs against the type, not the walk
Each target is resolved once, at the annotation site, with `types.LookupFieldOrMethod` — Go's own
promoted-field lookup, so depth and ambiguity rules come for free. The scope then carries the
resulting `*types.Var` objects and the filter is pointer identity.
Consequences worth keeping: there is no "what was actually omitted" bookkeeping (a target either
names a field of the type or it does not, known before the walk starts); a target already excluded
for another reason resolves fine and is silently redundant; and two same-named fields at different
depths can never be confused, because distinct fields are distinct objects.
### Placement
- **on the embed** (ergonomic): targets are plain field names of that
embedded type;
- **on the declaration** (power form): a dotted path names the embed
chain (`Base.ID`, head consumed per level), a bare name is offered to
each embed and resolved against its promoted set.
Every path segment but the last must name an **embedded** field: `omit` removes promoted content,
the only thing the enclosing schema owns. A segment naming a regular field stops the walk and is
reported as unresolved — reaching through one would edit either another type's inlined copy or a
`$ref`'d definition.
### Diagnostics (all Hints)
| code | fires when |
|---|---|
| `scan.omit-unresolved` | the target names no field of the embedded type — a typo, or a rename upstream |
| `scan.omit-behind-ref` | the embed is composed as a `$ref` (an annotated model), where OAS2 cannot subtract a property; the omission is dropped rather than silently forking the definition |
| `scan.shadowed-embed-field` | a field re-declared with `json:"-"` carries the Go name of a promoted field (see below) |
`swagger:omit` is the only construct whose output depends on a hand-written name the compiler never
checks — everything else is derived from types. `scan.omit-unresolved` is what stops it rotting
silently when a field is renamed upstream.
### The `json:"-"` shadow is not what authors think
`fields.go` deletes a promoted property when an outer field re-declares it with `json:"-"`. That is
**unfaithful to `encoding/json`**, which ignores a `-` field entirely: it never enters the name set,
so it cannot shadow the promoted one and Go keeps marshalling the embedded field. The behaviour is
locked by `TestOverridingOneIgnore` and left in place for now; the Hint points at `swagger:omit`,
which removes the field for real. Removing the eviction is a separate decision — see
`.claude/plans/swagger-omit.md` §7.
Fixtures: `fixtures/enhancements/swagger-omit` (the annotation, both renderings, all three Hints,
plus the go-swagger#1992 shape verbatim) and `fixtures/enhancements/default-allof-embeds-override`
(the same overrides *without* the annotation — the documented limit).
## <a id="embed-depth"></a>§embed-depth — ambiguous-embed diagnostic mechanism
`Builder.embedDepth` (incremented around `buildNamedEmbedded`'s
@ -1456,70 +1541,36 @@ edges fixture covers a different (strfmt-tagged) shape already.
---
### ✅ Named-strfmt + `swagger:model` combo (was 🟡 deferred)
A type carrying both `swagger:strfmt phone` and `swagger:model` used to emit an
inconsistent pair: `{type: string, format: phone}` at the field site, but a
struct walk (`{type: object, properties: …}`) for the top-level definition. The
decl-level strfmt now wins and the field `$ref`s it — verified in
`enhancements_named_struct_tags-ref.json`: `PhoneNumber` is
`{type: string, format: phone}` and `Contact.phone` is a `$ref` to it.
Fixed by the F-series pass (`8e20d2f`, quirk F1), not by the attempt described
in the original entry — which was reverted. History:
`.claude/plans/archive/deferred-quirks.md` D3.
### ✅ Cross-package definition-name collisions (was 🟡 silently overwrite)
Two packages declaring the same identifier (`pkg/a.User`, `pkg/b.User`) both
mapped to `definitions["User"]`, and the second build silently overwrote the
first — one `User` in the output, no record of the collision.
Fixed by the name-identity / cyclic-`$ref` work: every definition is keyed by a
compiler-unique `DefKey` (`<pkgpath>/<name>`) while building, and a final reduce
stage projects each back to the shortest unique name, deconflicting collisions
(`AWidget` / `BWidget`) and raising a `scan.renamed-definition` Hint. See
[§discovery](#discovery) and `.claude/plans/name-identity-cyclic-ref.md`.
## <a id="quirks-open"></a>§quirks-open — still open
### 🟡 Named-strfmt + `swagger:model` combo (deferred)
When the author combines `swagger:strfmt` with `swagger:model`
on the same type, the FIELD reference inlines as `{string, format}`
(via the strfmt classifier) but the TOP-LEVEL definition body is
still emitted from walking the underlying struct.
**Reproduction.** Fixture `fixtures/enhancements/named-struct-tags-ref/types.go`
declares `PhoneNumber` with both `swagger:strfmt phone` and
`swagger:model`, used by `Contact.Phone`. The golden
`enhancements_named_struct_tags-ref.json` captures the observable
inconsistency:
- Field site: `{type: "string", format: "phone"}` — strfmt wins.
- Top-level definition: `{type: "object", properties: {CountryCode, Number}}`
the struct walk wins; the strfmt annotation is ignored at decl time.
The author asked for "named strfmt" (a reusable `PhoneNumber`
definition rendered as a formatted string) but gets an inconsistent
pair: the field says string, the definition says object.
**Attempted fix and reasons it reverted.** The first attempt
(referred to as "Option 1") would have:
1. Detected `swagger:strfmt` on the decl in `buildDeclNamed` and
emitted `{string, fmt}` instead of walking the struct body.
2. In `buildNamedStruct`, when the target also has `swagger:model`,
emitted `$ref` instead of inlining the strfmt.
This was reverted before merge because:
- Pre-existing fixtures in
`fixtures/goparsing/classification/transitive/mods/aliases.go` use
the same `swagger:strfmt + swagger:model` combination on
defined-from-`time.Time` types (e.g. `SomeTimeType time.Time`).
The existing tests (`TestAliasedTypes`, `TestAliasedModels`)
assert the *inline* baseline (`scantest.AssertProperty(..., "string", ...)`)
rather than a `$ref`. Option 1 flips these to `$ref`, requiring
coordinated test updates.
- The decl-level `StrfmtName` check also over-fires on slice / array /
map underlyings: `type SomeTimesType []time.Time` with
`swagger:strfmt date-time` should emit
`{array, items: {string, date-time}}`, not flatten to `{string}`.
A correct fix would gate the check on struct-underlying first,
then symmetrically consider whether `buildNamedSlice` /
`buildNamedArray` / `buildNamedMap` should also route through
`$ref` under the `swagger:model` combination.
The surface area is wider than the Option 1 code change suggested,
and the existing test coverage of the combination is entangled with
the inconsistency itself.
**Why deferred.** The combination is niche, the footgun is narrow
(you get what you asked for on one side of the indirection, not
both), and v2's annotation redesign can reshape the contract without
carrying this legacy. A focused decision on "named strfmt" semantics
belongs in the v2 design, not a bug-fix pass.
The `named-struct-tags-ref` fixture and its golden are checked in as
a deliberate marker — the golden captures the observable
inconsistency (inline field + struct-body definition) so future work
on this decision has a failing test to anchor against.
> **Where open quirks live.** This section documents caveats *of this package*.
> The project-wide register of what is actually open — verified, with the stale
> historical registers called out — is `.claude/plans/quirks-open.md`.
### 🟦 `interface{}` literals (documented behaviour)
@ -1542,155 +1593,18 @@ the substituted underlying via the `TypeArgs` short-circuit
without a concrete instantiation simply have no representable
schema.
### 🟡 Cross-package definition-name collisions silently overwrite
### 🟡 A field-level enum override discards the type's per-value docs, silently
`buildFromDecl` writes the top-level schema as
`s.definitions[s.Name] = schema`, keyed only by the Go identifier
(`decl.Names()[0]`). When two packages in a single scan declare a type
with the same identifier — `pkg/a.User` and `pkg/b.User` — both map
to `definitions["User"]` and the second build silently overwrites the
first. The output spec carries only one `User`, with no record of the
collision and no signal of which package won.
When a field whose type is marked `swagger:enum TypeName` carries its own
`enum:` override, the inherited `x-go-enum-desc` is stripped along with the
replaced values — and **no diagnostic is raised**, so the per-value docs
`TypeName` contributed vanish without a trace. Reproduced by
`fixtures/enhancements/enum-overrides` case E (`NotificationE`).
The existing `nameByJSON` (`propOwner`) map in field emission is **not**
a defense against this case: it tracks JSON property names within a
single struct's field set plus its embeds (for the ambiguous-embed
diagnostic), not type-level identifier conflicts across packages.
Whether the docs should be filtered through (when the override *subsets* the
type's values) or dropped with a Hint (when it narrows to *different* ones) is a
design call that belongs with the enum feature, not with this package: see
`.claude/plans/features/enum-richer-values.md` §1.2b.
#### Target shape
A proper fix needs three pieces:
1. **Detection** — at write time, recognise the case "definition key
already exists with non-empty schema and originates from a different
package" (use `x-go-package`, or stash origin in the `Builder`).
2. **Diagnostic** — emit `CodeNameConflict` (severity
`SeverityWarning` minimum, possibly `SeverityError` under strict
mode) carrying both `(pkg, name)` pairs.
3. **Policy** — open design call:
- **a. Rename** — prefix loser(s) with a stable short-package
(e.g. `a_User`, `b_User`). Stable but ugly; needs all `$ref`s
to follow the rename — cross-cutting.
- **b. Skip + warn** — keep the first writer, drop subsequent
ones, emit a warning. Predictable but lossy.
- **c. Fail the build** — under strict mode, treat as an error.
Forces the author to rename in source. Cleanest semantics,
most disruptive.
#### Why deferred
Each policy choice changes the contract for downstream code generators
(go-swagger, oapi-codegen, …) — they have assumptions about
`definitions` keys matching exported Go names. The "rename" path
additionally requires every `$ref` writer in the builders to consult a
rename map; the surface is wide.
For multi-package scans where the author controls both packages, the
workaround today is to scope scans to one package per spec, or to
rename one of the colliding types at the source. A future strict-mode
flag (e.g. `Options.StrictNameConflicts`) could enable option (c)
without breaking existing scans.
### 🟡 Stale `x-go-enum-desc` after a field-level enum override
When a field uses a type marked `swagger:enum TypeName` **and** carries
its own `enum: …` override, v1 mutates the schema in place: it replaces
`Enum`, strips the inherited `x-go-enum-desc`, and trims the matching
description suffix. This is **lossy** — the per-value docs contributed
by `TypeName` are silently discarded.
Concretely, given (fixture `fixtures/enhancements/enum-overrides/`,
case E):
```go
// swagger:enum PriorityE
type PriorityE string
const (
PriorityELow PriorityE = "low" // low-priority requests
PriorityEMed PriorityE = "medium" // medium-priority requests
PriorityEHigh PriorityE = "high" // high-priority requests
)
type NotificationE struct {
// Inline enum provides a narrower set than the const block.
//
// enum: urgent, normal
Priority PriorityE `json:"priority"`
}
```
v1 emits:
```yaml
priority:
type: string
enum: [urgent, normal] # the override wins
description: "Inline enum provides a narrower set than the const block."
# x-go-enum-desc removed by clearStaleEnumDesc
# PriorityE's per-value doc lines silently dropped from description
```
The cleanup runs reactively from `schemaValidations.SetEnum`
([typable.go](typable.go#L128)) via `clearStaleEnumDesc`
([extensions.go](extensions.go#L42)). It treats any
`x-go-enum-desc` present at `SetEnum` time as inherited (and therefore
stale once `Enum` is replaced), deletes it, and trims the matching
suffix off `Description`. The `TrimSuffix` dance is fragile — it
relies on the enum-desc pipeline having appended the doc lines as a
literal suffix — but it works under v1's emission discipline.
#### Target shape (allOf composition)
OpenAPI 2.0 supports `allOf` for schema composition, so the cleaner
model does not have to wait for OAS 3. The replacement shape is:
```yaml
# PriorityE promoted to a top-level definition:
definitions:
PriorityE:
type: string
enum: [low, medium, high]
description: |
low: low-priority requests
medium: medium-priority requests
high: high-priority requests
x-go-enum-desc: |
low: low-priority requests
medium: medium-priority requests
high: high-priority requests
NotificationE:
type: object
properties:
priority:
description: "Inline enum provides a narrower set than the const block."
allOf:
- $ref: '#/definitions/PriorityE' # inherited enum + per-value docs
- enum: [urgent, normal] # the override
```
Each branch keeps its own concern:
- the `$ref` branch carries `PriorityE`'s full schema (values + docs +
`x-go-enum-desc`), untouched and reusable by every field that
references `PriorityE`;
- the inline branch carries the narrowing override only.
No mutation of the inherited schema, no `TrimSuffix` dance. Validator
semantics for enum-narrowing `allOf` aren't perfectly uniform across
tools, but for the documentation / code-gen use cases codescan feeds
(go-swagger, oapi-codegen, redoc, …) this composition preserves both
layers cleanly.
#### Prerequisites for the migration (both currently missing)
1. **Promote unannotated `swagger:enum` types to top-level definitions**
so the `$ref` branch has a target. Today they exist only as inlined
fragments on each referring field.
2. **Move override detection from `SetEnum` (validation hook) to the
field-emission path**, so the override is composed alongside the
inherited schema instead of mutating it after the fact.
Until both land, `clearStaleEnumDesc` stays in place. The TODO in
`extensions.go` flags it as the replacement target.
The stripping itself lives in `handlers/dispatch_schema.go:clearStaleEnumDesc`,
which is where either resolution would land.

View file

@ -28,6 +28,10 @@ import (
func (s *Builder) scanEmbeddedFields(
decl *scanner.EntityDecl, st *types.Struct, schema *oaispec.Schema, nameByJSON map[string]propOwner,
) (target *oaispec.Schema, hasAllOf bool, err error) {
// `swagger:omit` written on the DECLARATION (the power form) applies to the embeds walked below;
// the ergonomic form lives on each embed and is read from its fieldDoc.
declOmits := s.declOmitTargets(decl.Comments)
for fld := range st.Fields() {
if !fld.Anonymous() {
continue
@ -60,8 +64,18 @@ func (s *Builder) scanEmbeddedFields(
isAllOf = true
}
// `swagger:omit` is a PRE-filter: the listed fields are dropped before the embed is walked, so
// nothing is ever written for them. That makes the annotation mean the same thing whether the
// embed is inlined or composed into an allOf member — see omit.go.
// An embed emitted as a $ref cannot have a property subtracted; embedOmitTargets reports that
// and filters nothing.
restoreOmits := s.pushOmitted(
s.embedOmitTargets(fld, afld, fd, declOmits, isAllOf && s.isModelEmbed(fld.Type())),
)
if !isAllOf {
target, err = s.buildPlainEmbed(fld, afld, fd, isString, omitEmpty, schema, target, nameByJSON)
restoreOmits()
if err != nil {
return nil, false, err
}
@ -73,8 +87,10 @@ func (s *Builder) scanEmbeddedFields(
target = &oaispec.Schema{}
}
var newSch oaispec.Schema
if err := s.buildAllOf(fld.Type(), &newSch); err != nil {
return nil, false, err
buildErr := s.buildAllOf(fld.Type(), &newSch)
restoreOmits()
if buildErr != nil {
return nil, false, buildErr
}
if fd.AllOfClass != "" {

View file

@ -270,6 +270,11 @@ func (s *Builder) structFieldCarrier(fld *types.Var, decl *scanner.EntityDecl, t
return fieldCarrier{}, false, err
}
if ignore {
// A `json:"-"` re-declaration does NOT shadow a promoted field in Go — encoding/json ignores the
// field entirely, so the embedded one keeps marshalling. Report it and point at swagger:omit,
// which drops it for real.
s.warnShadowedByJSONDash(fld, afld, target, nameByJSON)
for jsonName, prior := range nameByJSON {
if prior.goName == fld.Name() {
delete(target.Properties, jsonName)

View file

@ -0,0 +1,307 @@
// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers
// SPDX-License-Identifier: Apache-2.0
package schema
import (
"go/ast"
"go/token"
"go/types"
"strings"
"github.com/go-openapi/codescan/internal/parsers/grammar"
oaispec "github.com/go-openapi/spec"
)
// omitPathSep separates the segments of a qualified `swagger:omit` target (`Base.ID`).
const omitPathSep = "."
// parseOmitTargets splits a `swagger:omit` argument into its targets.
//
// The lexer captures the whole remainder verbatim (the list may carry spaces after commas), so the
// split happens here: comma-separated, each target trimmed, empties dropped.
func parseOmitTargets(arg string) []string {
var out []string
for raw := range strings.SplitSeq(arg, ",") {
if t := strings.TrimSpace(raw); t != "" {
out = append(out, t)
}
}
return out
}
// resolveOmitTargets resolves each target against the embedded type, returning the field objects to
// filter out of the promotion walk.
//
// Resolution runs against the TYPE, not the walk: a target either names a (promoted) field of the
// embedded type or it does not, and that is known before a single property is written. The scope
// then carries field OBJECTS, so the pre-filter is pointer identity — no name matching, no
// bookkeeping of what was "actually omitted", and no collision between same-named fields at
// different depths.
//
// [types.LookupFieldOrMethod] is Go's own promoted-field lookup, so depth and ambiguity rules come
// for free: a target that Go itself could not resolve unambiguously resolves to nothing here and is
// reported.
//
// Unresolved targets raise a scan.omit-unresolved Hint (see [grammar.CodeOmitUnresolved]) — this is
// what stops the annotation rotting silently when a field is renamed upstream.
func (s *Builder) resolveOmitTargets(embedded types.Type, targets []string, pos token.Position) []*types.Var {
if len(targets) == 0 {
return nil
}
out := make([]*types.Var, 0, len(targets))
for _, target := range targets {
fld, ok := lookupOmitPath(embedded, strings.Split(target, omitPathSep))
if !ok {
s.hintf(pos, grammar.CodeOmitUnresolved,
"swagger:omit: %s has no field %q; the target is ignored", typeLabel(embedded), target)
continue
}
out = append(out, fld)
}
return out
}
// lookupOmitPath walks a dotted target (`Inner.Deep`) through the embed chain of tpe and returns the
// field the last segment names.
//
// Every segment but the last must name an EMBEDDED field: `swagger:omit` removes promoted content,
// which is the only thing the enclosing schema owns. A segment naming a regular field stops the walk
// (reported as unresolved by the caller) — reaching through a regular field would either edit
// another type's inlined copy or, worse, a `$ref`'d definition.
func lookupOmitPath(tpe types.Type, path []string) (*types.Var, bool) {
cur := tpe
for i, seg := range path {
obj, _, _ := types.LookupFieldOrMethod(cur, true, pkgOf(cur), seg)
fld, isField := obj.(*types.Var)
if !isField {
return nil, false
}
if i == len(path)-1 {
return fld, true
}
if !fld.Embedded() {
return nil, false
}
cur = fld.Type()
}
return nil, false
}
// pkgOf returns the package a named type belongs to, or nil for an unnamed one.
//
// [types.LookupFieldOrMethod] needs it only to decide whether unexported names are visible; a nil
// package restricts the lookup to exported fields, which is the right default for an unnamed type.
func pkgOf(tpe types.Type) *types.Package {
switch t := types.Unalias(tpe).(type) {
case *types.Pointer:
return pkgOf(t.Elem())
case *types.Named:
if obj := t.Obj(); obj != nil {
return obj.Pkg()
}
}
return nil
}
// typeLabel names a type for a diagnostic — the declared name when there is one, else the type
// string.
func typeLabel(tpe types.Type) string {
if named, ok := types.Unalias(tpe).(*types.Named); ok {
return named.Obj().Name()
}
return tpe.String()
}
// pushOmitted adds fields to the active omit scope and returns the function restoring it.
//
// The scope is a set of field objects in force for the subtree currently being walked; nesting
// accumulates, and each level removes only what it added, so sibling embeds never leak into one
// another.
func (s *Builder) pushOmitted(fields []*types.Var) func() {
if len(fields) == 0 {
return func() {}
}
if s.omitted == nil {
s.omitted = make(map[*types.Var]struct{}, len(fields))
}
added := make([]*types.Var, 0, len(fields))
for _, fld := range fields {
if _, dup := s.omitted[fld]; dup {
continue
}
s.omitted[fld] = struct{}{}
added = append(added, fld)
}
return func() {
for _, fld := range added {
delete(s.omitted, fld)
}
}
}
// isOmitted reports whether fld is filtered out by the active `swagger:omit` scope.
//
// This is the whole enforcement: a pre-filter on the promotion walk, so the property is never
// written. Because it runs before any name is computed, it is indifferent to json tags, to
// `NameFromTags`, and to whether the enclosing schema is inlined or composed with allOf.
func (s *Builder) isOmitted(fld *types.Var) bool {
if len(s.omitted) == 0 {
return false
}
_, omitted := s.omitted[fld]
return omitted
}
// embedOmitTargets collects the `swagger:omit` targets that apply to one embed and resolves them
// against the embedded type.
//
// Sources: the embed's own annotation (the ergonomic form — targets are plain field names of that
// type) and the enclosing declaration's annotation (the power form — a bare name resolved against
// the promoted set, or a dotted path whose head names this embed).
//
// isAllOfRef marks an embed that will be emitted as a `$ref` rather than inlined; the omission
// cannot be expressed there, so it is dropped with a scan.omit-behind-ref Hint instead of silently
// forking the referenced definition.
func (s *Builder) embedOmitTargets(
fld *types.Var, afld *ast.Field, fd fieldDoc, declOmits []string, isAllOfRef bool,
) []*types.Var {
targets := append([]string(nil), fd.OmitTargets...)
targets = append(targets, declOmitsFor(fld, declOmits)...)
if len(targets) == 0 {
return nil
}
pos := s.Ctx.PosOf(afld.Pos())
if isAllOfRef {
s.hintf(pos, grammar.CodeOmitBehindRef,
"swagger:omit: %s is composed as a $ref; a referenced definition cannot have a property "+
"subtracted, so the omission is dropped", typeLabel(fld.Type()))
return nil
}
return s.resolveOmitTargets(fld.Type(), targets, pos)
}
// declOmitsFor selects the declaration-level targets that apply to one embed: a dotted path whose
// head names this embed (with the head consumed), and every bare name — a bare name is resolved
// against the promoted set of each embed in turn, exactly as Go resolves a promoted field.
//
// A bare name that matches no embed is reported once per embed it was tried against; that is the
// intended behaviour for a typo and is silent for the (redundant, harmless) case where the field was
// already excluded for another reason.
func declOmitsFor(fld *types.Var, declOmits []string) []string {
if len(declOmits) == 0 {
return nil
}
var out []string
for _, target := range declOmits {
head, tail, qualified := strings.Cut(target, omitPathSep)
if !qualified {
out = append(out, target)
continue
}
if head == fld.Name() {
out = append(out, tail)
}
}
return out
}
// hintf emits an informational diagnostic through the builder's sink.
func (s *Builder) hintf(pos token.Position, code grammar.Code, format string, args ...any) {
if onDiag := s.Ctx.OnDiagnostic(); onDiag != nil {
onDiag(grammar.Hintf(pos, code, format, args...))
}
}
// declOmitTargets reads the declaration-level `swagger:omit` targets off a type's doc comment.
//
// These are the power form, for embeds the author cannot annotate directly (a type they do not own,
// or one nested deeper than their own source).
func (s *Builder) declOmitTargets(cg *ast.CommentGroup) []string {
if cg == nil {
return nil
}
var out []string
for _, b := range s.ParseBlocks(cg) {
if b.AnnotationKind() != grammar.AnnOmit {
continue
}
if arg, ok := b.AnnotationArg(); ok {
out = append(out, parseOmitTargets(arg)...)
}
}
return out
}
// isModelEmbed reports whether an embedded type is a `swagger:model`, i.e. whether composing it into
// an allOf member emits a `$ref` rather than an inline copy (see buildNamedAllOf).
//
// A `$ref` member is where `swagger:omit` cannot be expressed at all.
func (s *Builder) isModelEmbed(tpe types.Type) bool {
named, ok := types.Unalias(derefType(tpe)).(*types.Named)
if !ok {
return false
}
obj := named.Obj()
if obj == nil || obj.Pkg() == nil {
return false
}
decl, found := s.Ctx.GetModel(obj.Pkg().Path(), obj.Name())
return found && decl.HasModelAnnotation()
}
// derefType peels a pointer, so `*Base` classifies exactly like `Base`.
func derefType(tpe types.Type) types.Type {
if ptr, ok := types.Unalias(tpe).(*types.Pointer); ok {
return ptr.Elem()
}
return tpe
}
// warnShadowedByJSONDash reports a field re-declared with `json:"-"` that carries the same Go name as
// a field promoted from an embed.
//
// encoding/json ignores a `-` field ENTIRELY — it never enters the name set, so it cannot shadow the
// promoted field and Go keeps marshalling the embedded one. The author almost certainly meant
// `swagger:omit`, which removes it from the schema for real.
//
// See [grammar.CodeShadowedEmbedField].
func (s *Builder) warnShadowedByJSONDash(fld *types.Var, afld *ast.Field, target *oaispec.Schema, nameByJSON map[string]propOwner) {
if afld == nil || len(nameByJSON) == 0 {
return
}
for jsonName, prior := range nameByJSON {
if prior.goName != fld.Name() || prior.depth == 0 {
continue
}
if _, written := target.Properties[jsonName]; !written {
continue
}
s.hintf(s.Ctx.PosOf(afld.Pos()), grammar.CodeShadowedEmbedField,
"field %s is re-declared with `json:\"-\"`, but encoding/json ignores such a field entirely: "+
"it does not shadow the %q promoted from an embed, which Go still marshals. "+
"Use swagger:omit on the embed to drop it from the schema", fld.Name(), jsonName)
return
}
}

View file

@ -48,6 +48,13 @@ type Builder struct {
// The mechanism is shared with the parameters and responses builders via common.EmbedInheritance;
// the schema builder consumes only Required (it has no `in:` location concept).
embedInherited common.EmbedInheritance
// omitted is the set of struct fields the active `swagger:omit` scope filters out of the
// promotion walk, keyed by field object (see omit.go).
//
// Pushed around each embed walk and popped after it, so sibling embeds never leak into one
// another.
omitted map[*types.Var]struct{}
}
// NewBuilder constructs an initialized [Builder].

View file

@ -74,6 +74,12 @@ func (s *Builder) buildStructFields(decl *scanner.EntityDecl, st *types.Struct,
}
func (s *Builder) processStructField(fld *types.Var, decl *scanner.EntityDecl, target *oaispec.Schema, nameByJSON map[string]propOwner) error {
// `swagger:omit` pre-filter: the field is not promoted, so no property is written for it and no
// name is ever computed. See omit.go.
if s.isOmitted(fld) {
return nil
}
c, ok, err := s.structFieldCarrier(fld, decl, target, nameByJSON)
if err != nil || !ok {
return err

View file

@ -20,7 +20,7 @@ import (
// (never a $ref).
//
// It is the single resolution point for the keyword consumed at every swagger:type site (the F3
// reconciliation — see .claude/plans/quirks-F-series-fix.md).
// reconciliation — see .claude/plans/archive/quirks-F-series-fix.md).
//
// - ownType is the annotated field/decl's Go type, consumed by the
// `inline` / `array` keywords (which expand that type in place). May be

View file

@ -81,7 +81,7 @@ func (s *Builder) classifierTextMarshal(tpe types.Type, tgt ifaces.SwaggerTypabl
// It is the single resolution point for `swagger:type` on a named type: it routes the argument
// through resolveTypeOverride (always inlining — keyword scalars / Go builtins / `[]T` / `inline`
// / `array` / type-name refs), and applies a co-present `swagger:strfmt` as a supplementary format
// only when compatible with the resolved type (F3 — see .claude/plans/quirks-F-series-fix.md).
// only when compatible with the resolved type (F3 — see .claude/plans/archive/quirks-F-series-fix.md).
// ownType is the named Go type (consumed by the `inline`/`array` keywords); pos drives diagnostics.
//
// Reports back via the (handled, fallthrough) tuple:
@ -291,6 +291,11 @@ type fieldDoc struct {
//
// Empty for bare `swagger:allOf`.
AllOfClass string
// OmitTargets — arguments of `swagger:omit <name>[,<name>…]` written ON AN EMBED: the Go field
// names not to promote out of the embedded type.
//
// Nil when the annotation is absent. See omit.go.
OmitTargets []string
}
// scanFieldDoc inspects afld's docstring through the ParseBlocks cache and returns every
@ -306,7 +311,7 @@ func (s *Builder) scanFieldDoc(afld *ast.Field) fieldDoc {
}
var nameKeyword string
for _, b := range s.ParseBlocks(afld.Doc) {
switch b.AnnotationKind() { //nolint:exhaustive // field-level walker only consumes these five kinds
switch b.AnnotationKind() { //nolint:exhaustive // field-level walker only consumes these six kinds
case grammar.AnnIgnore:
fd.Ignored = true
case grammar.AnnName:
@ -326,6 +331,10 @@ func (s *Builder) scanFieldDoc(afld *ast.Field) fieldDoc {
if name, ok := b.AnnotationArg(); ok {
fd.AllOfClass = name
}
case grammar.AnnOmit:
if arg, ok := b.AnnotationArg(); ok {
fd.OmitTargets = append(fd.OmitTargets, parseOmitTargets(arg)...)
}
}
// The `name:` keyword is the canonical field-naming keyword, honoured uniformly across schema /
// param / header (doc-quirk G2).

View file

@ -96,6 +96,16 @@ func (s *Builder) pruneUnusedModels() {
queue = queue[1:]
sch := s.input.Definitions[cur]
collectDefRefs(&sch, mark)
// A reachable DISCRIMINATED base keeps its whole polymorphic family: the subtypes reference the
// base, never the reverse, so the $ref walk above cannot see them and would prune the family down
// to its base alone.
// This is the discriminator reachability rule deferred here by §12 (go-swagger#1913 / §15).
if isDiscriminated(&sch) {
for _, sub := range s.subtypeKeysOf(cur) {
mark(sub)
}
}
}
// Prune the unreachable, deterministically.

View file

@ -55,6 +55,15 @@ type Builder struct {
// DefKey, so the prune (scan.pruned-unused) and rename (scan.renamed-definition) Hints can be
// located at the originating Go type even though the spec node may by then be gone or renamed.
declPos map[string]token.Position
// declIdentity records the Go type identity (typeIdentity) behind each discovered definition,
// keyed by DefKey — the bridge from definition-key space back to the reverse `swagger:allOf` index
// used by the discriminator-subtype pull (see subtypes.go).
declIdentity map[string]string
// subtypeIdx is the lazily-built reverse `swagger:allOf` index (base type identity → subtype
// declarations).
//
// Built once per Build, from the model index, which is independent of ScanModels.
subtypeIdx subtypeIndex
// sharedParamPos / sharedRespPos record the source position of each scanned shared parameter /
// response, keyed by its registered name, so the shared prune (scan.pruned-unused, C4) can locate
// its Hint at the originating Go declaration.
@ -110,6 +119,7 @@ func NewBuilder(input *oaispec.Swagger, sc *scanner.ScanCtx, scanModels bool) *B
responses: input.Responses,
parameters: input.Parameters,
declPos: make(map[string]token.Position),
declIdentity: make(map[string]string),
sharedParamPos: make(map[string]token.Position),
sharedRespPos: make(map[string]token.Position),
pinnedParams: pinnedParams,
@ -362,6 +372,10 @@ func (s *Builder) buildDiscoveredSchema(decl *scanner.EntityDecl) error {
// node may have been dropped or renamed.
s.declPos[decl.DefKey()] = s.ctx.PosOf(decl.Ident.Pos())
// Stash the Go type behind this definition so the prune reachability walk can ask the reverse
// `swagger:allOf` index for the subtypes of a discriminated base (see subtypeKeysOf).
s.declIdentity[decl.DefKey()] = typeIdentity(decl.Obj())
// Cross-ref linkage: initiate the base pointer for this definition so the schema builder
// path-joins its members (properties, …) under it, and anchor the definition node itself to its
// type declaration.
@ -392,6 +406,13 @@ func (s *Builder) buildDiscoveredSchema(decl *scanner.EntityDecl) error {
s.discovered = append(s.discovered, sb.PostDeclarations()...)
// Reverse discovery: if this definition turned out to be a discriminated base, its subtypes belong
// to the emitted document too — nothing $refs them, so top-down discovery can never reach them
// (go-swagger#1913).
// Queued like any other discovery, so the loop above keeps pulling their own dependencies, and a
// subtype that is itself a discriminated base cascades.
s.discovered = append(s.discovered, s.discriminatedSubtypesOf(decl)...)
return nil
}

View file

@ -0,0 +1,293 @@
// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers
// SPDX-License-Identifier: Apache-2.0
package spec
import (
"go/ast"
"go/types"
"sort"
"github.com/go-openapi/codescan/internal/parsers/grammar"
"github.com/go-openapi/codescan/internal/scanner"
oaispec "github.com/go-openapi/spec"
)
// subtypeIndex is the reverse `swagger:allOf` index: Go type identity of a base (see typeIdentity)
// mapped to every `swagger:model` declaration that declares that base as an `allOf` member.
//
// It is the inverse of the reference direction the spec carries: a subtype `$ref`s its base, never
// the other way round, so a discriminated base entering the reachable closure can only pull its
// family in by looking the relation up backwards.
// §15 / go-swagger#1913.
type subtypeIndex map[string][]*scanner.EntityDecl
// subtypes returns the reverse `swagger:allOf` index, building it on first use.
//
// The source is ScanCtx.Models() — populated by classification whether or not ScanModels is set, so
// the index sees every annotated subtype even when none of them would be built (the no-`-m` case
// this feature exists for).
//
// Entries are ordered by definition key so the pull order — and hence the order of the emitted
// scan.discovered-subtype Hints — does not depend on map iteration.
func (s *Builder) subtypes() subtypeIndex {
if s.subtypeIdx != nil {
return s.subtypeIdx
}
idx := make(subtypeIndex)
parser := grammar.NewParser(s.ctx.FileSet())
for _, decl := range s.ctx.Models() {
for _, base := range allOfBases(decl, parser) {
idx[base] = append(idx[base], decl)
}
}
for base := range idx {
decls := idx[base]
sort.Slice(decls, func(i, j int) bool { return decls[i].DefKey() < decls[j].DefKey() })
}
s.subtypeIdx = idx
return idx
}
// discriminatedSubtypesOf returns the declarations to pull into discovery because decl — whose
// definition has just been built — is a discriminated base.
//
// The gate is the built definition's own `discriminator`, not a source-level re-derivation: it holds
// uniformly for an interface base with a `discriminator: true` member and for a struct base with a
// `discriminator: true` field, and it is the same fact the emitted document exposes. A base with no
// discriminator pulls nothing: its `allOf` users are ordinary compositions, not a polymorphic
// family, and inventing definitions for them would over-generate.
//
// Under ScanModels the pass is a no-op: buildModels already builds every annotated model, so there
// is nothing to pull — and pulling anyway would emit Hints whose order follows the model index's map
// iteration. The `-m` case is served by the prune reachability rule instead (subtypeKeysOf), which is
// where a discriminated family can actually be lost.
//
// Subtypes already emitted are skipped; the discovery loop dedups queued ones, and only genuinely new
// pulls are reported as Hints.
func (s *Builder) discriminatedSubtypesOf(decl *scanner.EntityDecl) []*scanner.EntityDecl {
if s.scanModels {
return nil
}
sch, built := s.definitions[decl.DefKey()]
if !built || !isDiscriminated(&sch) {
return nil
}
candidates := s.subtypes()[typeIdentity(decl.Obj())]
if len(candidates) == 0 {
return nil
}
onDiag := s.ctx.OnDiagnostic()
out := make([]*scanner.EntityDecl, 0, len(candidates))
for _, sub := range candidates {
if _, already := s.definitions[sub.DefKey()]; already {
continue
}
out = append(out, sub)
if onDiag != nil {
onDiag(grammar.Hintf(s.ctx.PosOf(sub.Ident.Pos()), grammar.CodeDiscoveredSubtype,
"definition %q discovered as a subtype of discriminated base %q",
leafName(sub.DefKey()), leafName(decl.DefKey())))
}
}
return out
}
// isDiscriminated reports whether a definition DECLARES a discriminator of its own — the gate both
// hooks share.
//
// Two shapes carry one, depending on how the type is written:
//
// - a plain base puts it at the top level ({type: object, properties,
// discriminator});
// - a MID-LEVEL base — a subtype that is itself a base — renders as
// `allOf: [{$ref parent}, {own props, discriminator}]`, so its
// discriminator sits in its own compound member, not at the top.
// Multi-level hierarchies would otherwise stop at the first level.
//
// Only an inline discriminator counts. A leaf subtype's `$ref` member is not followed, so it does not
// inherit its base's discriminator and does not masquerade as a base itself.
func isDiscriminated(sch *oaispec.Schema) bool {
if sch == nil {
return false
}
if sch.Discriminator != "" {
return true
}
for i := range sch.AllOf {
if isDiscriminated(&sch.AllOf[i]) {
return true
}
}
return false
}
// subtypeKeysOf returns the definition keys of the subtypes of the base emitted under defKey.
//
// Used by the prune reachability walk, which works in definition-key space; the bridge back to Go
// type identity is the declIdentity side table, recorded for every definition as it is built.
// Keys are returned whether or not they are (still) present in the definitions map — marking is
// membership-checked by the caller.
func (s *Builder) subtypeKeysOf(defKey string) []string {
identity, known := s.declIdentity[defKey]
if !known {
return nil
}
subs := s.subtypes()[identity]
if len(subs) == 0 {
return nil
}
out := make([]string, 0, len(subs))
for _, sub := range subs {
out = append(out, sub.DefKey())
}
return out
}
// allOfBases returns the Go type identity of every base that decl declares as an `allOf` member,
// i.e. each embedded field carrying `swagger:allOf`.
//
// Only explicit `swagger:allOf` counts. DefaultAllOfForEmbeds — which renders a plain embed AS allOf
// composition — is deliberately not honoured: it is a rendering knob, and letting it decide which
// definitions exist would make the emitted set depend on an unrelated option. The polymorphic idiom
// is the explicit annotation.
//
// `swagger:ignore` on the embed drops it here exactly as it does in the schema builder, so the index
// never claims a relation the document does not carry.
// Declarations with no embeddable members (an alias, a named basic type, …) yield nothing.
func allOfBases(decl *scanner.EntityDecl, parser grammar.Parser) []string {
if decl.Spec == nil || decl.Pkg == nil || decl.Pkg.TypesInfo == nil {
return nil
}
var out []string
seen := make(map[string]struct{})
for _, afld := range embeddableMembers(decl.Spec) {
if len(afld.Names) != 0 { // named field: never an embed
continue
}
if !isAllOfEmbed(afld, parser) {
continue
}
tv, known := decl.Pkg.TypesInfo.Types[afld.Type]
if !known {
continue
}
// Deduped: a struct can reach one base through two embeds (the type and an alias of it), and the
// index must not list the same subtype twice under one base.
for _, id := range baseIdentities(tv.Type) {
if _, dup := seen[id]; dup {
continue
}
seen[id] = struct{}{}
out = append(out, id)
}
}
return out
}
// embeddableMembers returns the member list of a declaration that can hold an embed: a struct's
// fields or an interface's method list.
//
// Both matter: a struct embeds its base as an anonymous field, and an INTERFACE embeds its base as an
// anonymous interface — which is how a mid-level type in a multi-level hierarchy is written (a
// subtype of the root that is itself a discriminated base). In both AST shapes an embed is the entry
// with no Names.
// Returns nothing for any other type shape.
func embeddableMembers(spec *ast.TypeSpec) []*ast.Field {
switch tpe := spec.Type.(type) {
case *ast.StructType:
if tpe.Fields == nil {
return nil
}
return tpe.Fields.List
case *ast.InterfaceType:
if tpe.Methods == nil {
return nil
}
return tpe.Methods.List
default:
return nil
}
}
// isAllOfEmbed reports whether an embedded field's doc marks it as an `allOf` member.
//
// This mirrors the `IsAllOfMember` / `Ignored` half of the schema builder's field-doc classifier
// (schema.scanFieldDoc) — the index needs those two signals only, and the spec builder owns no
// field-level walker to borrow them from.
func isAllOfEmbed(afld *ast.Field, parser grammar.Parser) bool {
if afld.Doc == nil {
return false
}
var isAllOf bool
for _, b := range parser.ParseAll(afld.Doc) {
switch b.AnnotationKind() { //nolint:exhaustive // only these two signals matter to the index
case grammar.AnnIgnore:
return false
case grammar.AnnAllOf:
isAllOf = true
}
}
return isAllOf
}
// baseIdentities returns every type identity an embedded base may be indexed under: the pointer is
// unwrapped (`*Base` composes exactly like `Base`), and an alias contributes BOTH its own identity
// and that of the type it names.
//
// Both alias identities are kept because which one the emitted `allOf` member `$ref`s depends on
// RefAliases / TransparentAliases: indexing both means the relation is found either way, and since
// identities are unique per declaration there is no risk of matching an unrelated base.
//
// Returns nothing for an unnamed type — an anonymous struct / interface embed has no declaration to
// index against.
func baseIdentities(t types.Type) []string {
if ptr, isPtr := t.(*types.Pointer); isPtr {
t = ptr.Elem()
}
var out []string
if alias, isAlias := t.(*types.Alias); isAlias {
out = append(out, typeIdentity(alias.Obj()))
}
if named, isNamed := types.Unalias(t).(*types.Named); isNamed {
if id := typeIdentity(named.Obj()); len(out) == 0 || out[0] != id {
out = append(out, id)
}
}
return out
}
// typeIdentity is the index key for a base: "<pkgpath>.<Name>", the compiler's identity for the
// declared type.
//
// The Go type — not the swagger definition name — is what both ends of the relation can compute
// without knowing the other's annotations: the embed site resolves a type, the base declaration owns
// one, while either may carry a `swagger:model <name>` override.
// A package-less (universe) type falls back to its bare name.
func typeIdentity(obj *types.TypeName) string {
if obj == nil {
return ""
}
if pkg := obj.Pkg(); pkg != nil {
return pkg.Path() + "." + obj.Name()
}
return obj.Name()
}

View file

@ -161,7 +161,7 @@ once a concrete consumer asks for them.
the `swagger:type` + `swagger:strfmt` combination, where `swagger:type` wins on
the type axis and the strfmt format is applied as a **supplementary hint only
when it is consistent with that type** (the F3 reconciliation — see
`.claude/plans/quirks-F-series-fix.md`). It is **not** used for the
`.claude/plans/archive/quirks-F-series-fix.md`). It is **not** used for the
strfmt-alone path, where strfmt still forces `{type: string, format: X}`
(go-swagger#1512).

View file

@ -15,18 +15,27 @@ type AnnotationKind int
const (
AnnUnknown AnnotationKind = iota
AnnModel // swagger:model
AnnResponse // swagger:response
AnnParameters // swagger:parameters
AnnRoute // swagger:route
AnnOperation // swagger:operation
AnnMeta // swagger:meta
AnnStrfmt // swagger:strfmt
AnnAlias // swagger:alias
AnnName // swagger:name
AnnAllOf // swagger:allOf
AnnEnum // swagger:enum
AnnIgnore // swagger:ignore
AnnModel // swagger:model
AnnResponse // swagger:response
AnnParameters // swagger:parameters
AnnRoute // swagger:route
AnnOperation // swagger:operation
AnnMeta // swagger:meta
AnnStrfmt // swagger:strfmt
AnnAlias // swagger:alias
AnnName // swagger:name
AnnAllOf // swagger:allOf
AnnEnum // swagger:enum
AnnIgnore // swagger:ignore
// AnnOmit — swagger:omit <name>[,<name>…].
//
// An embed-level / type-level escape hatch: the listed Go field names are not promoted out of the
// embedded type, so the author resolves an embed conflict the scanner must not decide for them
// (go-swagger#1992).
//
// The whole remainder is captured as one raw arg token; the schema builder splits the list and
// resolves each name against the embedded type.
AnnOmit
AnnDefaultName // swagger:default — value-only classifier annotation
AnnType // swagger:type
AnnFile // swagger:file
@ -68,6 +77,7 @@ const (
labelAllOf = "allOf"
labelEnum = "enum"
labelIgnore = "ignore"
labelOmit = "omit"
labelDefault = "default"
labelType = "type"
labelFile = "file"
@ -105,6 +115,8 @@ func (a AnnotationKind) String() string {
return labelEnum
case AnnIgnore:
return labelIgnore
case AnnOmit:
return labelOmit
case AnnDefaultName:
return labelDefault
case AnnType:
@ -155,6 +167,8 @@ func AnnotationKindFromName(name string) AnnotationKind {
return AnnEnum
case labelIgnore:
return AnnIgnore
case labelOmit:
return AnnOmit
case labelDefault:
return AnnDefaultName
case labelType:
@ -208,7 +222,7 @@ func (a AnnotationKind) family() annotationFamily {
case AnnMeta:
return familyMeta
case AnnStrfmt, AnnAlias, AnnAllOf, AnnEnum,
AnnIgnore, AnnDefaultName, AnnType, AnnFile,
AnnIgnore, AnnOmit, AnnDefaultName, AnnType, AnnFile,
AnnAdditionalProperties, AnnPatternProperties:
return familyClassifier
case AnnUnknown:

View file

@ -89,7 +89,7 @@ const (
// is not valid (e.g. `inline`/`array` as an array element).
//
// The override is dropped and the subject falls through to its Go type.
// See the F3 reconciliation in .claude/plans/quirks-F-series-fix.md.
// See the F3 reconciliation in .claude/plans/archive/quirks-F-series-fix.md.
CodeUnsupportedType Code = "validate.unsupported-type"
// CodeDeprecated fires when an accepted-but-deprecated annotation or keyword value is used (the
@ -191,6 +191,41 @@ const (
// See scanner.Options PruneUnusedModels.
CodePrunedUnused Code = "scan.pruned-unused"
// CodeDiscoveredSubtype fires when a definition is emitted because it is a subtype of a
// discriminated base that entered the reachable set — a `swagger:model` declaring that base as an
// `allOf` member (go-swagger#1913).
//
// Such a subtype is unreachable top-down (it references the base, nothing references it), so it is
// pulled in by the reverse `swagger:allOf` index rather than by any $ref in the document.
// Informational (Hint); carries the subtype's own source position, so a definition that appears
// without ScanModels can be traced to the family that pulled it in.
CodeDiscoveredSubtype Code = "scan.discovered-subtype"
// CodeOmitUnresolved fires when a `swagger:omit` target names no field of the embedded type it is
// applied to — a typo, or a field renamed upstream.
//
// `swagger:omit` is the only construct whose output depends on a hand-written name the compiler
// never checks (everything else is derived from types), so an unresolved target is reported rather
// than silently ignored: otherwise a rename upstream would make the omitted field quietly reappear.
// Informational (Hint); located at the annotation.
CodeOmitUnresolved Code = "scan.omit-unresolved"
// CodeOmitBehindRef fires when a `swagger:omit` target resolves, but the embed carrying it is
// emitted as a `$ref` (the embedded type is a `swagger:model` composed with allOf).
//
// Swagger 2.0 cannot subtract a property from a `$ref`, so the omission is dropped rather than
// silently forking the referenced definition.
// Informational (Hint); located at the annotation.
CodeOmitBehindRef Code = "scan.omit-behind-ref"
// CodeShadowedEmbedField fires when a struct field re-declared with `json:"-"` carries the same Go
// name as a field promoted from an embed.
//
// encoding/json ignores a `-` field entirely, so it never shadows the promoted one: Go keeps
// marshalling the embedded field. The author most likely meant `swagger:omit`.
// Informational (Hint); located at the re-declared field.
CodeShadowedEmbedField Code = "scan.shadowed-embed-field"
// CodeRenamedDefinition fires when the reduce stage renames a definition to deconflict a
// cross-package name collision (e.g. b.Test / c.Test -> BTest / CTest), so a consumer that tracks
// source <-> spec links (the genspec TUI) learns the final name a Go type landed under.

View file

@ -184,7 +184,7 @@ func classifyHTTPMethod(s string) (string, bool) {
// It is a LEXICAL check only: the grammar no longer owns the closed type vocabulary.
// Semantic validity (is it a known keyword / scanned type? is the format compatible?) is resolved
// by the builder, which alone knows the scanned definitions and the annotated Go type (the F3
// reconciliation — see .claude/plans/quirks-F-series-fix.md).
// reconciliation — see .claude/plans/archive/quirks-F-series-fix.md).
//
// The lexer only rejects structurally malformed tokens (empty, embedded spaces, a bare `[]`, a
// leading digit, illegal characters), which the parser flags.

View file

@ -348,6 +348,10 @@ func classifyAnnotationArgs(kind AnnotationKind, rest string, linePos token.Posi
// type-name).
// The builder does the semantic resolution.
return []Token{argTypeRef(rest, pos)}
case AnnOmit:
// The arg is a comma-separated list of Go field names / dotted embed paths, which may carry
// spaces after the commas — capture the whole remainder verbatim; the builder splits it.
return []Token{{Kind: TokenRawValue, Pos: pos, Text: strings.TrimSpace(rest)}}
case AnnPatternProperties:
// The arg is a `"<re>": <spec>, …` pair list that may contain spaces/colons/commas inside
// quoted regexes — capture the whole remainder verbatim; the builder parses the pairs.

View file

@ -615,7 +615,7 @@ func (s *parseState) parseClassifierBlock(annIdx int, annTok Token, kind Annotat
s.emit(Errorf(annTok.Args[0].Pos, CodeInvalidTypeRef,
"swagger:type: %q is not a well-formed type reference", annTok.Args[0].Text))
}
case AnnAllOf, AnnIgnore, AnnAlias, AnnFile:
case AnnAllOf, AnnIgnore, AnnAlias, AnnFile, AnnOmit:
// Optional / no args.
default:
// ignored annotation

View file

@ -151,7 +151,14 @@ orchestrator level.
## <a id="quirks-open"></a>§quirks-open — deferred follow-ups
- **Column tracking.** routebody does not track per-line column
> **Where open quirks live.** This section documents caveats *of this package*.
> The project-wide register of what is actually open — verified, with the stale
> historical registers called out — is `.claude/plans/quirks-open.md`.
- **Column tracking** (owned by
`.claude/plans/features/column-precision-unicode.md` — an LSP prerequisite,
paired there with the multi-byte column question since both touch the same
`Line.Pos` contract). routebody does not track per-line column
information; diagnostics inherit `basePos.Column`. If the LSP
integration needs per-token positions on body sub-language
diagnostics, the body parser will need to track lex state more

View file

@ -20,6 +20,8 @@ parameters, responses) consumed by the builder layer.
experimental-API caveat
- [§prune](#prune) — `PruneUnusedModels` reachability and why it
runs before name reduction
- [§subtypes](#subtypes) — discriminator subtype discovery — the
reverse `swagger:allOf` index
- [§model-lookup](#model-lookup) — `GetModel` vs `FindModel`
pure read vs implicit registration
- [§classifier](#classifier) — `detectNodes` bitmask semantics and
@ -155,10 +157,93 @@ provenance for a pruned node is dropped so no anchor dangles. The
collision renames the reduce stage *does* perform are surfaced as
`scan.renamed-definition` Hints (located at the Go type).
**Known limitation.** A discriminator base references its subtypes by
mapping string, not by `$ref`, so a subtype reachable only through a
discriminator could be pruned. codescan does not auto-wire discriminator
subtypes today; revisit if it ever does (forthcoming-features §15).
**Discriminated families are kept whole.** A subtype `$ref`s its base,
never the reverse, so the walk above cannot see the subtypes of a
discriminated base and would prune a polymorphic family down to its base
alone. A reachable definition carrying `discriminator` therefore also
marks its subtypes reachable, via the reverse `swagger:allOf` index
(`spec/subtypes.go`, `subtypeKeysOf`). Note the rule keeps a *reached*
base's family — it does not make bases roots: a discriminated base
nothing references is still pruned, together with its subtypes. See
[§subtypes](#subtypes).
## <a id="subtypes"></a>§subtypes — discriminator subtype discovery
Interface-based polymorphism emits a base definition carrying
`discriminator` plus one `allOf: [{$ref base}, {own props}]` definition
per subtype — a struct embedding the base under `swagger:allOf`.
The reference direction is the problem: **a subtype `$ref`s its base;
nothing `$ref`s a subtype.** So a route that references only the base
reaches the base and stops, and the emitted family is the base alone —
useless to a consumer that has to unmarshal the polymorphic payload
(go-swagger/go-swagger#1913). Before v0.37 the only way to get the
subtypes was `ScanModels`, which emits every annotated model whether it
belongs to the family or not.
**Reverse index.** `spec/subtypes.go` builds, once per scan, a
`base Go type identity → subtype declarations` map from the model index
(`TypeIndex.Models`, which classification populates whether or not
`ScanModels` is set — that independence is what makes the pull possible).
A subtype relation is an *embedded member carrying `swagger:allOf`* — a
struct's anonymous field **or an interface's anonymous interface**, which
is how a mid-level type in a multi-level hierarchy is written:
- the pointer is unwrapped (`*Base` composes like `Base`), and an alias
embed is indexed under both its own and the aliased type's identity, so
the relation is found whichever definition the `allOf` member ends up
`$ref`ing under `RefAliases` / `TransparentAliases`;
- `swagger:ignore` on the embed drops it, exactly as in the schema
builder — the index never claims a relation the document lacks;
- a *plain* (unannotated) embed is not a subtype: it inlines the base's
properties, no `allOf` member. `DefaultAllOfForEmbeds` is deliberately
**not** honoured here — it is a rendering knob, and letting it decide
which definitions exist would be a surprising coupling.
The index is keyed by **Go type identity** (`<pkgpath>.<TypeName>`), not
by swagger name: it is the one fact both ends can compute without knowing
the other's `swagger:model` override. Entries are ordered by definition
key so the pull order — and hence the Hint order — does not follow map
iteration.
**Two hooks, one per hole.**
1. *Discovery* (`spec.go`, `buildDiscoveredSchema`
`discriminatedSubtypesOf`): when a definition has just been built and
carries a `discriminator`, its subtypes are appended to `s.discovered`,
so the existing fixpoint loop builds them, discovers *their*
dependencies, and cascades if a subtype is itself a discriminated
base. Each genuinely new pull raises a located
`scan.discovered-subtype` Hint. A no-op under `ScanModels`, where every
model is built up front anyway.
2. *Prune reachability* (`prune.go`): see
[§prune](#prune) — required because under `ScanModels` the family is
built and then lost, not never-built.
**The gate is the built definition's own `discriminator`**
(`isDiscriminated`), not a source-level re-derivation. It reads
identically for an interface base with a `discriminator: true` member and
a struct base with a `discriminator: true` property, and it is the same
fact the emitted document exposes. A base with no discriminator pulls
nothing: its `allOf` users are ordinary compositions, not a polymorphic
family.
**Multi-level hierarchies.** A mid-level type — a subtype that is itself a
base — renders as `allOf: [{$ref parent}, {own props, discriminator}]`:
its properties, and therefore its discriminator, sit in its own compound
member, *not* at the top level. So the gate looks for an inline
discriminator anywhere in the definition's own schema. The `$ref` member
is deliberately **not** followed: a leaf must not inherit its base's
discriminator, or every subtype would pull in its own siblings. Because
hook A feeds the discovery fixpoint, the levels cascade — the root pulls
the mid-level, and the mid-level (only just pulled in itself) pulls the
leaves on the next round.
Fixtures: `fixtures/enhancements/discriminated-subtypes` (`edges/` holds
the embed-shape corner cases, in a family no route references — which
also locks the other half of the gate: an unreached base pulls nothing)
and `fixtures/enhancements/discriminated-subtypes-nested` (two-level
hierarchy: `Shape``Polygon``Square`/`Triangle`).
## <a id="model-lookup"></a>§model-lookup — `GetModel` vs `FindModel`
@ -297,6 +382,10 @@ prose** — author-written overrides (harvested separately) are never filtered.
## <a id="quirks-open"></a>§quirks-open — deferred follow-ups
> **Where open quirks live.** This section documents caveats *of this package*.
> The project-wide register of what is actually open — verified, with the stale
> historical registers called out — is `.claude/plans/quirks-open.md`.
- **`FindModel` deprecation.** The deprecated alias is still on the
`ScanCtx` surface for in-tree callers. Once every builder has been
audited and migrated to the `GetModel` + `AddDiscoveredModel` pair,

View file

@ -521,7 +521,7 @@ func (a *TypeIndex) detectNodes(file *ast.File) (node, error) {
}
case "strfmt", "name", "discriminated", "file", "enum", "default", "alias", "type", "additionalProperties", "patternProperties", "title", "description":
// Proposal for enhancement: perhaps collect these and pass along to avoid lookups later on
case "allOf":
case "allOf", "omit":
case "ignore":
default:
return 0, fmt.Errorf("classifier: unknown swagger annotation %q: %w", annotation, ErrScanner)

View file

@ -4,11 +4,11 @@
| Total Contributors | Total Contributions |
| --- | --- |
| 5 | 47 |
| 5 | 52 |
| Username | All Time Contribution Count | All Commits |
| --- | --- | --- |
| @fredbi | 26 | <https://github.com/go-openapi/inflect/commits?author=fredbi> |
| @fredbi | 31 | <https://github.com/go-openapi/inflect/commits?author=fredbi> |
| @chrisfarms | 15 | <https://github.com/go-openapi/inflect/commits?author=chrisfarms> |
| @hirochachacha | 4 | <https://github.com/go-openapi/inflect/commits?author=hirochachacha> |
| @casualjim | 1 | <https://github.com/go-openapi/inflect/commits?author=casualjim> |

View file

@ -20,12 +20,9 @@ Originally forked from https://bitbucket.org/pkg/inflect under a MIT License.
* **2025-12-19** : new community chat on discord
* a new discord community channel is available to be notified of changes and support users
* our venerable Slack channel remains open, and will be eventually discontinued on **2026-03-31**
You may join the discord community by clicking the invite link on the discord badge (also above). [![Discord Channel][discord-badge]][discord-url]
Or join our Slack channel: [![Slack Channel][slack-logo]![slack-badge]][slack-url]
## Status
API is stable.
@ -73,11 +70,9 @@ on top of which it has been built.
## Other documentation
* [All-time contributors](./CONTRIBUTORS.md)
* [Contributing guidelines](.github/CONTRIBUTING.md)
<!--
* [Maintainers documentation](docs/MAINTAINERS.md)
* [Code style](docs/STYLE.md)
-->
* [Contributing guidelines][contributing-doc-site]
* [Maintainers documentation][maintainers-doc-site]
* [Code style][style-doc-site]
## Cutting a new release
@ -112,9 +107,6 @@ Maintainers can cut a new release by either:
[doc-url]: https://goswagger.io/go-openapi
[godoc-badge]: https://pkg.go.dev/badge/github.com/go-openapi/inflect
[godoc-url]: http://pkg.go.dev/github.com/go-openapi/inflect
[slack-logo]: https://a.slack-edge.com/e6a93c1/img/icons/favicon-32.png
[slack-badge]: https://img.shields.io/badge/slack-blue?link=https%3A%2F%2Fgoswagger.slack.com%2Farchives%2FC04R30YM
[slack-url]: https://goswagger.slack.com/archives/C04R30YMU
[discord-badge]: https://img.shields.io/discord/1446918742398341256?logo=discord&label=discord&color=blue
[discord-url]: https://discord.gg/FfnFYaC3k5
@ -126,3 +118,7 @@ Maintainers can cut a new release by either:
[goversion-url]: https://github.com/go-openapi/inflect/blob/master/go.mod
[top-badge]: https://img.shields.io/github/languages/top/go-openapi/inflect
[commits-badge]: https://img.shields.io/github/commits-since/go-openapi/inflect/latest
<!-- Organization docs -->
[contributing-doc-site]: https://go-openapi.github.io/doc-site/contributing/contributing/index.html
[maintainers-doc-site]: https://go-openapi.github.io/doc-site/maintainers/index.html
[style-doc-site]: https://go-openapi.github.io/doc-site/contributing/style/index.html

View file

@ -71,11 +71,16 @@ type FileBinder func(file multipart.File, header *multipart.FileHeader) error
// behaviour) be added without breaking the signature.
type BindOption func(*bindConfig)
type bindConfig struct {
maxParseMemory int64
type multipartFormLimits struct {
maxBody int64
maxFiles int
maxFilenameLen int
}
type bindConfig struct {
multipartFormLimits
maxParseMemory int64
files []formFileSpec
}
@ -198,7 +203,9 @@ func BindFormFile(name string, required bool, bind FileBinder) BindOption {
// it directly as a filesystem path.
func BindForm(r *http.Request, opts ...BindOption) (fatal bool, err error) {
cfg := bindConfig{
maxFilenameLen: DefaultMaxUploadFilenameLength,
multipartFormLimits: multipartFormLimits{
maxFilenameLen: DefaultMaxUploadFilenameLength,
},
}
for _, opt := range opts {
opt(&cfg)

View file

@ -0,0 +1,556 @@
// SPDX-FileCopyrightText: Copyright 2015-2026 go-swagger maintainers
// SPDX-License-Identifier: Apache-2.0
package runtime
import (
"context"
stderrors "errors"
"fmt"
"io"
"mime"
"mime/multipart"
"net/http"
"net/textproto"
"net/url"
"github.com/go-openapi/errors"
)
// MultipartFormStreamOption configures [NewMultipartFormStream].
type MultipartFormStreamOption func(*multipartFormStreamConfig)
const defaultMultipartFormStreamMaxParts = 1000
type multipartFormStreamConfig struct {
multipartFormLimits
maxParts int
}
// MultipartFormStreamMaxBody caps the total number of request-body bytes read
// by a [MultipartFormStream].
//
// A value of 0 applies [DefaultMaxUploadBodySize]. A negative value disables
// the cap when the caller has already limited the request body upstream.
func MultipartFormStreamMaxBody(n int64) MultipartFormStreamOption {
return func(c *multipartFormStreamConfig) { c.maxBody = n }
}
// MultipartFormStreamMaxFiles rejects a multipart stream after more than n
// file parts have been encountered. A value of 0 means no file-count cap.
func MultipartFormStreamMaxFiles(n int) MultipartFormStreamOption {
return func(c *multipartFormStreamConfig) { c.maxFiles = n }
}
// MultipartFormStreamMaxParts rejects a multipart stream after more than n
// total parts have been encountered. The default is 1000, matching
// [multipart.Reader.ReadForm]. A value of 0 disables the limit.
func MultipartFormStreamMaxParts(n int) MultipartFormStreamOption {
return func(c *multipartFormStreamConfig) { c.maxParts = n }
}
// MultipartFormStreamMaxFilenameLen rejects file parts whose filename exceeds
// n bytes. A value of 0 disables the limit. When this option is not supplied,
// [DefaultMaxUploadFilenameLength] is used.
func MultipartFormStreamMaxFilenameLen(n int) MultipartFormStreamOption {
return func(c *multipartFormStreamConfig) { c.maxFilenameLen = n }
}
// StreamedFile exposes a file part directly from the multipart request body.
//
// Reads block until bytes arrive from the client. StreamedFile is not seekable
// and is not safe for concurrent use. Its form name, filename and MIME headers
// are available before the payload is consumed. The underlying [multipart.Part]
// remains private so callers cannot bypass Close and its error-preserving drain
// semantics; Header exposes the part metadata without exposing that lifecycle.
//
// Closing a StreamedFile drains only the unread remainder of that file part.
// Close may therefore block while the client is still uploading the current
// part. The owning [MultipartFormStream] may then advance to the next part.
type StreamedFile struct {
FieldName string
Filename string
Header textproto.MIMEHeader
part *multipart.Part
closeErr error
}
// MultipartFileInfo describes a file part discovered by [MultipartFormStream].
//
// The payload reader is intentionally omitted. File parts remain sequential and
// are consumed through [MultipartFormStream.NextFile]. Header is a snapshot of
// the client-supplied MIME headers and must be treated as untrusted input.
type MultipartFileInfo struct {
FieldName string
Filename string
Header textproto.MIMEHeader
}
// Read reads file payload bytes directly from the request body.
func (f *StreamedFile) Read(p []byte) (int, error) {
if f == nil || f.part == nil {
return 0, io.ErrClosedPipe
}
return f.part.Read(p)
}
// Close discards the unread remainder of this file part.
//
// Close does not close the underlying HTTP request body and does not consume
// subsequent multipart parts. After Close returns successfully, the parent
// MultipartFormStream may advance to the next part.
//
// Any error encountered while discarding the unread payload is returned.
func (f *StreamedFile) Close() error {
if f == nil {
return nil
}
if f.part == nil {
return f.closeErr
}
part := f.part
f.part = nil
// multipart.Part.Close drains with io.Copy but intentionally discards the
// resulting error, so drain explicitly to preserve error propagation.
_, f.closeErr = io.Copy(io.Discard, part)
return f.closeErr
}
// MultipartFormStream reads multipart/form-data sequentially without parsing
// the complete request body before exposing file payloads.
//
// [MultipartFormStream.NextFile] consumes ordinary form fields until it reaches
// the next file part. Fields are appended to request.PostForm and request.Form
// as they are encountered. Consequently, fields after a file become visible
// only after the caller consumes or closes that file and advances the stream.
//
// A stream and its returned files are not safe for concurrent use. There is at
// most one active file part. Calling [MultipartFormStream.NextFile] closes and
// drains an unread active file before advancing, and may therefore block until
// the current part finishes arriving. No background goroutines are started.
//
// The caller owns the stream. Call [MultipartFormStream.Drain] to consume the
// remaining body, collect trailing fields and allow HTTP connection reuse when
// possible. Call [MultipartFormStream.Close] to stop multipart processing
// without explicitly draining the remaining parts.
//
// [MultipartFormStream.Fields] and [MultipartFormStream.Files] expose snapshots
// of the multipart fields and file metadata discovered so far. They never read
// ahead: fields or files after the active file become visible only after the
// stream advances.
type MultipartFormStream struct {
multipartFormStreamConfig
request *http.Request
reader *multipart.Reader
current *StreamedFile
fields url.Values
fileInfos []MultipartFileInfo
parts int
closed bool
done bool
}
// NewMultipartFormStream creates a sequential multipart/form-data stream over
// r.Body.
//
// For POST, PUT and PATCH requests, the constructor accepts only
// multipart/form-data, validates that a non-empty boundary is present, but does
// not consume multipart parts. For other methods, it returns an empty stream
// whose NextFile method reports io.EOF without reading the request body.
//
// The constructor initializes request.Form and request.PostForm in the same way
// as [http.Request.ParseForm], then populates multipart values incrementally as
// [MultipartFormStream.NextFile] advances.
//
// For POST, PUT and PATCH requests, NewMultipartFormStream marks the request
// as handled by MultipartReader. Callers must not subsequently call
// [http.Request.ParseMultipartForm] or [BindForm] for the same request.
//
// File payloads are exposed directly from the request body and are not buffered
// in memory or temporary files. Ordinary form values are read into memory as
// they are encountered. Parts are processed in wire order.
//
// At most one StreamedFile may be active at a time. Calling
// [MultipartFormStream.NextFile] automatically closes and drains an unread
// current file before advancing. No background goroutines are started.
//
// MultipartFormStream is not safe for concurrent use.
//
// File names and MIME headers are supplied by the client and remain untrusted.
func NewMultipartFormStream(r *http.Request, opts ...MultipartFormStreamOption) (*MultipartFormStream, error) {
cfg := multipartFormStreamConfig{
multipartFormLimits: multipartFormLimits{
maxFilenameLen: DefaultMaxUploadFilenameLength,
},
maxParts: defaultMultipartFormStreamMaxParts,
}
for _, opt := range opts {
opt(&cfg)
}
if r == nil {
return nil, errors.NewParseError("body", "formData", "", stderrors.New("nil request"))
}
if !supportsMultipartFormStream(r.Method) {
if err := r.ParseForm(); err != nil {
return nil, errors.NewParseError("body", "formData", "", err)
}
return &MultipartFormStream{
request: r,
multipartFormStreamConfig: cfg,
fields: make(url.Values),
done: true,
}, nil
}
if r.Body == nil {
return nil, errors.NewParseError("body", "formData", "", stderrors.New("nil request body"))
}
contentType := r.Header.Get(HeaderContentType)
mediaType, params, err := mime.ParseMediaType(contentType)
if err != nil {
return nil, errors.NewParseError(HeaderContentType, "header", contentType, err)
}
if mediaType != MultipartFormMime {
return nil, errors.NewParseError(HeaderContentType, "header", mediaType, http.ErrNotMultipart)
}
if params["boundary"] == "" {
return nil, errors.NewParseError(HeaderContentType, "header", contentType, http.ErrMissingBoundary)
}
if err = r.ParseForm(); err != nil {
return nil, errors.NewParseError("body", "formData", "", err)
}
body := r.Body
if cfg.maxBody >= 0 {
maxBody := cfg.maxBody
if maxBody == 0 {
maxBody = DefaultMaxUploadBodySize
}
body = http.MaxBytesReader(nil, body, maxBody)
}
body = &contextReadCloser{ctx: r.Context(), ReadCloser: body}
r.Body = body
reader, err := r.MultipartReader()
if err != nil {
return nil, errors.NewParseError("body", "formData", "", err)
}
return &MultipartFormStream{
request: r,
reader: reader,
multipartFormStreamConfig: cfg,
fields: make(url.Values),
}, nil
}
// Fields returns a snapshot of ordinary multipart form fields discovered so far.
//
// URL query values are not included. Repeated multipart fields preserve their
// encounter order. Fields after the active file are not visible until that file
// is consumed or closed and the stream advances. Mutating the returned values
// does not affect the stream or the request.
func (s *MultipartFormStream) Fields() url.Values {
if s == nil {
return nil
}
return cloneMultipartValues(s.fields)
}
// Files returns snapshots of file metadata discovered so far in wire order.
//
// The currently active file is included as soon as NextFile returns it. Payload
// readers are not retained in the index. Mutating the returned slice or MIME
// headers does not affect the stream.
func (s *MultipartFormStream) Files() []MultipartFileInfo {
if s == nil {
return nil
}
files := make([]MultipartFileInfo, len(s.fileInfos))
for i, file := range s.fileInfos {
files[i] = MultipartFileInfo{
FieldName: file.FieldName,
Filename: file.Filename,
Header: cloneMultipartMIMEHeader(file.Header),
}
}
return files
}
// NextFile advances through the multipart body and returns the next file part.
// Ordinary form fields encountered before that file are added to request.Form
// and request.PostForm.
//
// If the previously returned file is still open, NextFile closes and drains it
// before advancing. This may block while the client is still uploading that
// part. Any drain error is returned and the stream is aborted.
//
// NextFile returns io.EOF when no file parts remain. At that point all trailing
// ordinary fields have been collected.
func (s *MultipartFormStream) NextFile() (*StreamedFile, error) {
if err := s.prepareNextFile(); err != nil {
return nil, err
}
return s.readNextFile()
}
// Drain consumes the rest of the multipart body.
//
// Unread file payloads are discarded. Non-file form fields encountered while
// draining are collected in the request form values.
//
// Drain closes the underlying request body after reaching EOF. Subsequent calls
// to NextFile return io.EOF. Drain returns any multipart parsing, payload drain
// or request-body close error.
func (s *MultipartFormStream) Drain() error {
if s == nil || s.closed {
return nil
}
for {
file, err := s.NextFile()
if stderrors.Is(err, io.EOF) {
return s.Close()
}
if err != nil {
return stderrors.Join(err, s.Close())
}
if err = file.Close(); err != nil {
return stderrors.Join(err, s.Close())
}
}
}
// Close stops multipart processing and closes the underlying HTTP request body
// without explicitly draining the remaining multipart parts.
//
// The concrete request body may perform its own work during Close. In
// particular, a net/http server request body may discard a limited amount of
// unread data to allow connection reuse, so Close is not guaranteed to return
// immediately. Call Drain when trailing form fields must be collected.
func (s *MultipartFormStream) Close() error {
if s == nil || s.closed {
return nil
}
s.closed = true
if s.current != nil {
s.current.part = nil
s.current = nil
}
if s.request == nil || s.request.Body == nil {
return nil
}
return s.request.Body.Close()
}
func supportsMultipartFormStream(method string) bool {
switch method {
case http.MethodPost, http.MethodPut, http.MethodPatch:
return true
default:
return false
}
}
func (s *MultipartFormStream) abort(err error) error {
return stderrors.Join(err, s.Close())
}
func (s *MultipartFormStream) closeCurrent() error {
if s.current == nil {
return nil
}
err := s.current.Close()
s.current = nil
return err
}
func (s *MultipartFormStream) bindValue(part *multipart.Part, name string) error {
value, err := io.ReadAll(part)
if err != nil {
return err
}
s.fields.Add(name, string(value))
s.request.PostForm.Add(name, string(value))
// Match net/http.ParseMultipartForm: query values already present in Form
// keep precedence over multipart body values, which are appended.
s.request.Form.Add(name, string(value))
return nil
}
func discardPart(part *multipart.Part) error {
_, err := io.Copy(io.Discard, part)
return err
}
func cloneMultipartValues(values url.Values) url.Values {
cloned := make(url.Values, len(values))
for name, entries := range values {
cloned[name] = append([]string(nil), entries...)
}
return cloned
}
func cloneMultipartMIMEHeader(header textproto.MIMEHeader) textproto.MIMEHeader {
cloned := make(textproto.MIMEHeader, len(header))
for name, entries := range header {
cloned[name] = append([]string(nil), entries...)
}
return cloned
}
type contextReadCloser struct {
io.ReadCloser
ctx context.Context //nolint:containedctx // Read has no context parameter, so the wrapper must retain it
}
func (r *contextReadCloser) Read(p []byte) (int, error) {
if err := r.ctx.Err(); err != nil {
return 0, err
}
return r.ReadCloser.Read(p)
}
func (s *MultipartFormStream) prepareNextFile() error {
if s == nil {
return io.ErrClosedPipe
}
if s.done {
return io.EOF
}
if s.closed {
return io.ErrClosedPipe
}
if err := s.closeCurrent(); err != nil {
return s.abort(err)
}
return nil
}
func (s *MultipartFormStream) readNextFile() (*StreamedFile, error) {
for {
part, err := s.reader.NextPart()
if err != nil {
return nil, s.handleNextPartError(err)
}
s.parts++
if s.maxParts > 0 && s.parts > s.maxParts {
err := errors.NewParseError(
"body",
"formData",
"",
fmt.Errorf(
"multipart form contains %d parts, exceeds limit %d",
s.parts,
s.maxParts,
),
)
return nil, s.abort(err)
}
fieldName := part.FormName()
if fieldName == "" {
if err := discardPart(part); err != nil {
return nil, s.abort(err)
}
continue
}
filename := part.FileName()
if filename == "" {
if err := s.bindValue(part, fieldName); err != nil {
return nil, s.abort(err)
}
continue
}
return s.openFile(part, fieldName, filename)
}
}
func (s *MultipartFormStream) handleNextPartError(err error) error {
if stderrors.Is(err, io.EOF) {
s.done = true
return io.EOF
}
return s.abort(err)
}
func (s *MultipartFormStream) openFile(
part *multipart.Part,
fieldName string,
filename string,
) (*StreamedFile, error) {
fileCount := len(s.fileInfos) + 1
if s.maxFiles > 0 && fileCount > s.maxFiles {
err := errors.NewParseError(
"body",
"formData",
"",
fmt.Errorf(
"multipart form contains %d file parts, exceeds limit %d",
fileCount,
s.maxFiles,
),
)
return nil, s.abort(err)
}
if err := ValidateFilenameLength(
fieldName,
"formData",
filename,
s.maxFilenameLen,
); err != nil {
return nil, s.abort(err)
}
file := &StreamedFile{
FieldName: fieldName,
Filename: filename,
Header: part.Header,
part: part,
}
s.fileInfos = append(s.fileInfos, MultipartFileInfo{
FieldName: fieldName,
Filename: filename,
Header: cloneMultipartMIMEHeader(part.Header),
})
s.current = file
return file, nil
}

View file

@ -7,14 +7,16 @@ import (
"strconv"
)
const baseDecimal = 10
// FormatInteger turns an integer type into a string.
func FormatInteger[T Signed](value T) string {
return strconv.FormatInt(int64(value), 10)
return strconv.FormatInt(int64(value), baseDecimal)
}
// FormatUinteger turns an unsigned integer type into a string.
func FormatUinteger[T Unsigned](value T) string {
return strconv.FormatUint(uint64(value), 10)
return strconv.FormatUint(uint64(value), baseDecimal)
}
// FormatFloat turns a floating point numerical value into a string.
@ -26,3 +28,23 @@ func FormatFloat[T Float](value T) string {
func FormatBool(value bool) string {
return strconv.FormatBool(value)
}
// AppendInteger appends the decimal representation of an integer to a slice of bytes.
func AppendInteger[T Signed](dst []byte, value T) []byte {
return strconv.AppendInt(dst, int64(value), baseDecimal)
}
// AppendUinteger appends the decimal representation of an unsigned integer to a slice of bytes.
func AppendUinteger[T Unsigned](dst []byte, value T) []byte {
return strconv.AppendUint(dst, uint64(value), baseDecimal)
}
// AppendFloat appends the decimal representation of a floating point number to a slice of bytes.
func AppendFloat[T Float](dst []byte, value T) []byte {
return strconv.AppendFloat(dst, float64(value), 'g', -1, bitsize(value))
}
// AppendBool appends the text representation of a boolean to a slice of bytes.
func AppendBool(dst []byte, value bool) []byte {
return strconv.AppendBool(dst, value)
}

View file

@ -260,6 +260,11 @@ func (b *codeGenOpBuilder) MakeOperation() (GenOperation, error) {
var hasQueryParams, hasPathParams, hasHeaderParams, hasFormParams, hasFileParams, hasFormValueParams, hasBodyParams bool
paramsForOperation := b.Analyzed.ParamsFor(b.Method, b.Path)
hasStreamingForm, err := streamingFormEnabled(paramsForOperation, b.Method, b.Path)
if err != nil {
return GenOperation{}, err
}
idMapping, timeoutName, ctxName, err := b.paramMappings(paramsForOperation)
if err != nil {
return GenOperation{}, err
@ -303,6 +308,12 @@ func (b *codeGenOpBuilder) MakeOperation() (GenOperation, error) {
sort.Sort(hp)
sort.Sort(fp)
serverParams := serverParameters(params, hasStreamingForm)
multipartFormName := ""
if hasStreamingForm {
multipartFormName = deconflictMultipartFormName(serverParams)
}
var srs responses
if operation.Responses != nil {
srs = sortedResponses(operation.Responses.StatusCodeResponses)
@ -415,6 +426,7 @@ func (b *codeGenOpBuilder) MakeOperation() (GenOperation, error) {
DefaultImports: b.DefaultImports,
Imports: b.Imports,
Params: params,
ServerParams: serverParams,
Summary: trimBOM(operation.Summary),
QueryParams: qp,
PathParams: pp,
@ -427,7 +439,9 @@ func (b *codeGenOpBuilder) MakeOperation() (GenOperation, error) {
HasFormValueParams: hasFormValueParams,
HasFileParams: hasFileParams,
HasBodyParams: hasBodyParams,
HasStreamingForm: hasStreamingForm,
HasStreamingResponse: hasStreamingResponse,
MultipartFormName: multipartFormName,
Authorized: b.Authed,
Security: b.makeSecurityRequirements(receiver), // resolved security requirements, for codegen
SecurityDefinitions: b.makeSecuritySchemes(receiver),
@ -874,6 +888,57 @@ func (b *codeGenOpBuilder) MakeBodyParameterItemsAndMaps(res *GenParameter, it *
return items
}
func streamingFormEnabled(params map[string]spec.Parameter, method, path string) (bool, error) {
enabled := false
for _, param := range params {
raw, found := param.Extensions[xGoServerStreaming]
if !found {
continue
}
streaming, ok := raw.(bool)
if !ok {
return false, fmt.Errorf(`%s %s, parameter %q: %q must be a boolean, not a %T`,
method, path, param.Name, xGoServerStreaming, raw)
}
if !streaming {
continue
}
if param.In != formData || param.Type != file {
return false, fmt.Errorf(`%s %s, parameter %q: %q may only be enabled on a formData file parameter`,
method, path, param.Name, xGoServerStreaming)
}
enabled = true
}
return enabled, nil
}
func serverParameters(params GenParameters, streamingForm bool) GenParameters {
if !streamingForm {
return params
}
serverParams := make(GenParameters, 0, len(params))
for _, param := range params {
if param.IsFormParam() {
continue
}
serverParams = append(serverParams, param)
}
return serverParams
}
func deconflictMultipartFormName(params GenParameters) string {
seenIDs := make(map[string]any, len(params))
for _, param := range params {
seenIDs[strings.ToLower(param.ID)] = struct{}{}
}
return rename(multipartFormNamePreferences)(seenIDs, multipartFormNamePreferences[0], 0)
}
// paramMappings yields a map of safe parameter names for an operation.
func (b *codeGenOpBuilder) paramMappings(params map[string]spec.Parameter) (map[string]map[string]string, string, string, error) {
idMapping := map[string]map[string]string{
@ -964,6 +1029,15 @@ var (
"opContext",
"operContext",
}
multipartFormNamePreferences = []string{
"MultipartForm",
"RequestMultipartForm",
"HTTPMultipartForm",
"SwaggerMultipartForm",
"OperationMultipartForm",
"OpMultipartForm",
}
)
// rename the variable in use by client template to avoid conflicting

View file

@ -639,6 +639,7 @@ type GenOperation struct {
DefaultResponse *GenResponse
Params GenParameters
ServerParams GenParameters
QueryParams GenParameters
PathParams GenParameters
HeaderParams GenParameters
@ -650,7 +651,9 @@ type GenOperation struct {
HasFormValueParams bool
HasFileParams bool
HasBodyParams bool
HasStreamingForm bool
HasStreamingResponse bool
MultipartFormName string
Schemes []string
ExtraSchemes []string

View file

@ -128,7 +128,10 @@ func configureAPI(api *{{.APIPackageAlias}}.{{ pascalize .Name }}API) http.Handl
{{- $package := .Package }}
{{- $apipackagealias := .APIPackageAlias }}
{{- range .Operations }}
{{- if .HasFormParams }}
{{- if .HasStreamingForm }}
// You may change here the maximum body size for this streaming multipart form. Below is the default (32 MB).
// {{ if ne .Package $package }}{{ .PackageAlias }}{{ else }}{{ $apipackagealias }}{{ end }}.{{ pascalize .Name }}MaxBodySize = 32 << 20
{{- else if .HasFormParams }}
// You may change here the memory limit for this multipart form parser. Below is the default (32 MB).
// {{ if ne .Package $package }}{{ .PackageAlias }}{{ else }}{{ $apipackagealias }}{{ end }}.{{ pascalize .Name }}MaxParseMemory = 32 << 20
{{- end }}

View file

@ -319,6 +319,7 @@ import (
)
{{- if .HasFormParams }}
{{- if not .HasStreamingForm }}
// {{ pascalize .Name }}MaxParseMemory sets the maximum size in bytes for
// the multipart form parser for this operation.
@ -326,6 +327,7 @@ import (
// The default value is 32 MB.
// The multipart parser stores up to this + 10MB.
var {{ pascalize .Name }}MaxParseMemory int64 = 32 << 20
{{- end }}
// {{ pascalize .Name }}MaxBodySize caps the size of the form body.
//
@ -334,17 +336,17 @@ var {{ pascalize .Name }}MaxBodySize int64 = 32 << 20
{{- end }}
// New{{ pascalize .Name }}Params creates a new {{ pascalize .Name }}Params object
{{- if .Params.HasSomeDefaults }}
{{- if .ServerParams.HasSomeDefaults }}
// with the default values initialized.
{{- else }}
//
// There are no default values defined in the spec.
{{- end }}
func New{{ pascalize .Name }}Params() {{ pascalize .Name }}Params {
{{ if .Params.HasSomeDefaults }}
{{ if .ServerParams.HasSomeDefaults }}
var (
// initialize parameters with default values
{{ range .Params }}
{{ range .ServerParams }}
{{ if .HasDefault -}}
{{ if not .IsFileParam }}{{ varname .ID}}Default =
{{- if and .IsPrimitive .IsCustomFormatter (not (stringContains .Zero "(\"" )) }}{{ .Zero }}{{/* strfmt type initializer requires UnmarshalText(), e.g. Date, Datetime, Duration */}}
@ -363,7 +365,7 @@ func New{{ pascalize .Name }}Params() {{ pascalize .Name }}Params {
{{- end }}
)
{{ range .Params }}{{ if .HasDefault -}}{{- /* carry out UnmarshalText initialization strategy */ -}}
{{ range .ServerParams }}{{ if .HasDefault -}}{{- /* carry out UnmarshalText initialization strategy */ -}}
{{ if and .IsPrimitive .IsCustomFormatter (not (stringContains .Zero "(\"")) }}{{ varname .ID}}Default.UnmarshalText([]byte({{ printf "%q" .Default }}))
{{ else if .IsArray -}}
{{ if or ( and .Child.IsPrimitive .Child.IsCustomFormatter ) .Child.IsArray -}}
@ -377,7 +379,7 @@ func New{{ pascalize .Name }}Params() {{ pascalize .Name }}Params {
{{ end -}}
{{- end }}
{{ end }}
return {{ pascalize .Name }}Params{ {{ range .Params }}{{ if .HasDefault }}
return {{ pascalize .Name }}Params{ {{ range .ServerParams }}{{ if .HasDefault }}
{{ .ID}}: {{ if and (not .IsArray) (not .HasDiscriminator) (not .IsInterface) (not .IsStream) .IsNullable }}&{{ end }}{{ varname .ID }}Default,
{{ end }}{{ end }} }
}
@ -389,7 +391,14 @@ func New{{ pascalize .Name }}Params() {{ pascalize .Name }}Params {
type {{ pascalize .Name }}Params struct {
// HTTP Request Object
HTTPRequest *http.Request `json:"-"`
{{- range .Params }}
{{- if .HasStreamingForm }}
// {{ .MultipartFormName }} is the lazily consumed multipart form stream.
//
// The handler owns traversal, validation and closing or draining this stream.
{{ .MultipartFormName }} *runtime.MultipartFormStream `json:"-"`
{{- end }}
{{- range .ServerParams }}
{{- if .Description }}
{{ lineComment .Description }}
@ -422,11 +431,11 @@ func ({{ .ReceiverName }} *{{ pascalize .Name }}Params) BindRequest(r *http.Requ
{{- if .HasQueryParams }}
qs := runtime.Values(r.URL.Query())
{{- end }}
{{- if .HasFormParams }}
{{- if and .HasFormParams (not .HasStreamingForm) }}
isBlocking, err := runtime.BindForm(r,
runtime.BindFormMaxParseMemory({{ pascalize .Name }}MaxParseMemory),
runtime.BindFormMaxBody({{ pascalize .Name }}MaxBodySize),
{{- range .Params }}
{{- range .ServerParams }}
{{- if .IsFileParam }}
runtime.BindFormFile({{ .Path }}, {{ or .Required (not .IsNullable) }}, {{ .ReceiverName }}.bind{{ .ID }}),
{{- end }}
@ -445,7 +454,7 @@ func ({{ .ReceiverName }} *{{ pascalize .Name }}Params) BindRequest(r *http.Requ
{{- end }}
{{- end }}
{{ range .Params }}
{{ range .ServerParams }}
{{- if not .IsArray }}
{{- if .IsQueryParam }}
@ -542,11 +551,22 @@ func ({{ .ReceiverName }} *{{ pascalize .Name }}Params) BindRequest(r *http.Requ
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
{{- if .HasStreamingForm }}
multipartForm, err := runtime.NewMultipartFormStream(
r,
runtime.MultipartFormStreamMaxBody({{ pascalize .Name }}MaxBodySize),
)
if err != nil {
return err
}
{{ .ReceiverName }}.{{ .MultipartFormName }} = multipartForm
{{- end }}
return nil
}
{{- $className := (pascalize .Name) }}
{{ range .Params }}
{{ range .ServerParams }}
{{- if .IsFileParam }}
// bind{{ .ID }} validates file parameter File1 and assigns it as a *runtime.File on success.
//

View file

@ -36,17 +36,18 @@ const (
// Extensions supported by go-swagger.
const (
xClass = "x-class" // class name used by discriminator
xGoCustomTag = "x-go-custom-tag" // additional tag for serializers on struct fields
xGoName = "x-go-name" // name of the generated go variable
xGoType = "x-go-type" // reuse existing type (do not generate)
xIsNullable = "x-isnullable"
xNullable = "x-nullable" // turns the schema into a pointer
xOmitEmpty = "x-omitempty"
xSchemes = "x-schemes" // additional schemes supported for operations (server generation)
xOrder = "x-order" // sort order for properties (or any schema)
xGoJSONString = "x-go-json-string"
xGoEnumCI = "x-go-enum-ci" // make string enumeration case-insensitive
xClass = "x-class" // class name used by discriminator
xGoCustomTag = "x-go-custom-tag" // additional tag for serializers on struct fields
xGoName = "x-go-name" // name of the generated go variable
xGoType = "x-go-type" // reuse existing type (do not generate)
xIsNullable = "x-isnullable"
xNullable = "x-nullable" // turns the schema into a pointer
xOmitEmpty = "x-omitempty"
xSchemes = "x-schemes" // additional schemes supported for operations (server generation)
xOrder = "x-order" // sort order for properties (or any schema)
xGoJSONString = "x-go-json-string"
xGoEnumCI = "x-go-enum-ci" // make string enumeration case-insensitive
xGoServerStreaming = "x-go-server-streaming" // stream multipart form payloads directly to server handlers
xGoOperationTag = "x-go-operation-tag" // additional tag to override generation in operation groups
)

View file

@ -8,8 +8,12 @@ package isatty
import "golang.org/x/sys/unix"
// IsTerminal return true if the file descriptor is terminal.
// TIOCGWINSZ is used instead of TCGETS because TCGETS shares its ioctl
// number with SNDCTL_TMR_TIMEBASE of the OSS sound API, so it may succeed
// (and even change the device mode) on non-tty devices. musl's isatty does
// the same.
func IsTerminal(fd uintptr) bool {
_, err := unix.IoctlGetTermios(int(fd), unix.TCGETS)
_, err := unix.IoctlGetWinsize(int(fd), unix.TIOCGWINSZ)
return err == nil
}

View file

@ -505,7 +505,16 @@ func (id *ULID) Scan(src interface{}) error {
case string:
return id.UnmarshalText([]byte(x))
case []byte:
return id.UnmarshalBinary(x)
// Drivers often return text/varchar columns as []byte. Accept both
// the 16-byte binary form and the 26-character text encoding.
switch len(x) {
case len(*id):
return id.UnmarshalBinary(x)
case EncodedSize:
return id.UnmarshalText(x)
default:
return ErrDataSize
}
}
return ErrScanValue

View file

@ -226,9 +226,9 @@ func yaml_parser_state_machine(parser *yaml_parser_t, event *yaml_event_t) bool
}
// Parse the production:
// stream ::= STREAM-START implicit_document? explicit_document* STREAM-END
//
// ************
// stream ::= STREAM-START implicit_document? explicit_document* STREAM-END
// ************
func yaml_parser_parse_stream_start(parser *yaml_parser_t, event *yaml_event_t) bool {
token := peek_token(parser)
if token == nil {
@ -249,13 +249,11 @@ func yaml_parser_parse_stream_start(parser *yaml_parser_t, event *yaml_event_t)
}
// Parse the productions:
// implicit_document ::= block_node DOCUMENT-END*
//
// *
//
// explicit_document ::= DIRECTIVE* DOCUMENT-START block_node? DOCUMENT-END*
//
// *************************
// implicit_document ::= block_node DOCUMENT-END*
// *
// explicit_document ::= DIRECTIVE* DOCUMENT-START block_node? DOCUMENT-END*
// *************************
func yaml_parser_parse_document_start(parser *yaml_parser_t, event *yaml_event_t, implicit bool) bool {
token := peek_token(parser)
@ -359,9 +357,9 @@ func yaml_parser_parse_document_start(parser *yaml_parser_t, event *yaml_event_t
}
// Parse the productions:
// explicit_document ::= DIRECTIVE* DOCUMENT-START block_node? DOCUMENT-END*
//
// ***********
// explicit_document ::= DIRECTIVE* DOCUMENT-START block_node? DOCUMENT-END*
// ***********
func yaml_parser_parse_document_content(parser *yaml_parser_t, event *yaml_event_t) bool {
token := peek_token(parser)
if token == nil {
@ -382,11 +380,10 @@ func yaml_parser_parse_document_content(parser *yaml_parser_t, event *yaml_event
}
// Parse the productions:
// implicit_document ::= block_node DOCUMENT-END*
//
// *************
//
// explicit_document ::= DIRECTIVE* DOCUMENT-START block_node? DOCUMENT-END*
// implicit_document ::= block_node DOCUMENT-END*
// *************
// explicit_document ::= DIRECTIVE* DOCUMENT-START block_node? DOCUMENT-END*
func yaml_parser_parse_document_end(parser *yaml_parser_t, event *yaml_event_t) bool {
token := peek_token(parser)
if token == nil {
@ -432,42 +429,32 @@ func yaml_parser_set_event_comments(parser *yaml_parser_t, event *yaml_event_t)
}
// Parse the productions:
// block_node_or_indentless_sequence ::=
//
// ALIAS
// *****
// | properties (block_content | indentless_block_sequence)?
// ********** *
// | block_content | indentless_block_sequence
// *
//
// block_node ::= ALIAS
//
// *****
// | properties block_content?
// ********** *
// | block_content
// *
//
// flow_node ::= ALIAS
//
// *****
// | properties flow_content?
// ********** *
// | flow_content
// *
//
// properties ::= TAG ANCHOR? | ANCHOR TAG?
//
// *************************
//
// block_content ::= block_collection | flow_collection | SCALAR
//
// ******
//
// flow_content ::= flow_collection | SCALAR
//
// ******
// block_node_or_indentless_sequence ::=
// ALIAS
// *****
// | properties (block_content | indentless_block_sequence)?
// ********** *
// | block_content | indentless_block_sequence
// *
// block_node ::= ALIAS
// *****
// | properties block_content?
// ********** *
// | block_content
// *
// flow_node ::= ALIAS
// *****
// | properties flow_content?
// ********** *
// | flow_content
// *
// properties ::= TAG ANCHOR? | ANCHOR TAG?
// *************************
// block_content ::= block_collection | flow_collection | SCALAR
// ******
// flow_content ::= flow_collection | SCALAR
// ******
func yaml_parser_parse_node(parser *yaml_parser_t, event *yaml_event_t, block, indentless_sequence bool) bool {
//defer trace("yaml_parser_parse_node", "block:", block, "indentless_sequence:", indentless_sequence)()
@ -697,9 +684,9 @@ func yaml_parser_parse_node(parser *yaml_parser_t, event *yaml_event_t, block, i
}
// Parse the productions:
// block_sequence ::= BLOCK-SEQUENCE-START (BLOCK-ENTRY block_node?)* BLOCK-END
//
// ******************** *********** * *********
// block_sequence ::= BLOCK-SEQUENCE-START (BLOCK-ENTRY block_node?)* BLOCK-END
// ******************** *********** * *********
func yaml_parser_parse_block_sequence_entry(parser *yaml_parser_t, event *yaml_event_t, first bool) bool {
if first {
token := peek_token(parser)
@ -755,9 +742,9 @@ func yaml_parser_parse_block_sequence_entry(parser *yaml_parser_t, event *yaml_e
}
// Parse the productions:
// indentless_sequence ::= (BLOCK-ENTRY block_node?)+
//
// *********** *
// indentless_sequence ::= (BLOCK-ENTRY block_node?)+
// *********** *
func yaml_parser_parse_indentless_sequence_entry(parser *yaml_parser_t, event *yaml_event_t) bool {
token := peek_token(parser)
if token == nil {
@ -821,15 +808,15 @@ func yaml_parser_split_stem_comment(parser *yaml_parser_t, stem_len int) {
}
// Parse the productions:
// block_mapping ::= BLOCK-MAPPING_START
//
// *******************
// ((KEY block_node_or_indentless_sequence?)?
// *** *
// (VALUE block_node_or_indentless_sequence?)?)*
// block_mapping ::= BLOCK-MAPPING_START
// *******************
// ((KEY block_node_or_indentless_sequence?)?
// *** *
// (VALUE block_node_or_indentless_sequence?)?)*
//
// BLOCK-END
// *********
// BLOCK-END
// *********
func yaml_parser_parse_block_mapping_key(parser *yaml_parser_t, event *yaml_event_t, first bool) bool {
if first {
token := peek_token(parser)
@ -896,13 +883,14 @@ func yaml_parser_parse_block_mapping_key(parser *yaml_parser_t, event *yaml_even
}
// Parse the productions:
// block_mapping ::= BLOCK-MAPPING_START
//
// ((KEY block_node_or_indentless_sequence?)?
// block_mapping ::= BLOCK-MAPPING_START
//
// (VALUE block_node_or_indentless_sequence?)?)*
// ***** *
// BLOCK-END
// ((KEY block_node_or_indentless_sequence?)?
//
// (VALUE block_node_or_indentless_sequence?)?)*
// ***** *
// BLOCK-END
func yaml_parser_parse_block_mapping_value(parser *yaml_parser_t, event *yaml_event_t) bool {
token := peek_token(parser)
if token == nil {
@ -929,19 +917,17 @@ func yaml_parser_parse_block_mapping_value(parser *yaml_parser_t, event *yaml_ev
}
// Parse the productions:
// flow_sequence ::= FLOW-SEQUENCE-START
//
// *******************
// (flow_sequence_entry FLOW-ENTRY)*
// * **********
// flow_sequence_entry?
// *
// FLOW-SEQUENCE-END
// *****************
//
// flow_sequence_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)?
//
// *
// flow_sequence ::= FLOW-SEQUENCE-START
// *******************
// (flow_sequence_entry FLOW-ENTRY)*
// * **********
// flow_sequence_entry?
// *
// FLOW-SEQUENCE-END
// *****************
// flow_sequence_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)?
// *
func yaml_parser_parse_flow_sequence_entry(parser *yaml_parser_t, event *yaml_event_t, first bool) bool {
if first {
token := peek_token(parser)
@ -1005,9 +991,9 @@ func yaml_parser_parse_flow_sequence_entry(parser *yaml_parser_t, event *yaml_ev
}
// Parse the productions:
// flow_sequence_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)?
//
// *** *
// flow_sequence_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)?
// *** *
func yaml_parser_parse_flow_sequence_entry_mapping_key(parser *yaml_parser_t, event *yaml_event_t) bool {
token := peek_token(parser)
if token == nil {
@ -1026,9 +1012,9 @@ func yaml_parser_parse_flow_sequence_entry_mapping_key(parser *yaml_parser_t, ev
}
// Parse the productions:
// flow_sequence_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)?
//
// ***** *
// flow_sequence_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)?
// ***** *
func yaml_parser_parse_flow_sequence_entry_mapping_value(parser *yaml_parser_t, event *yaml_event_t) bool {
token := peek_token(parser)
if token == nil {
@ -1050,9 +1036,9 @@ func yaml_parser_parse_flow_sequence_entry_mapping_value(parser *yaml_parser_t,
}
// Parse the productions:
// flow_sequence_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)?
//
// *
// flow_sequence_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)?
// *
func yaml_parser_parse_flow_sequence_entry_mapping_end(parser *yaml_parser_t, event *yaml_event_t) bool {
token := peek_token(parser)
if token == nil {
@ -1068,18 +1054,17 @@ func yaml_parser_parse_flow_sequence_entry_mapping_end(parser *yaml_parser_t, ev
}
// Parse the productions:
// flow_mapping ::= FLOW-MAPPING-START
//
// ******************
// (flow_mapping_entry FLOW-ENTRY)*
// * **********
// flow_mapping_entry?
// ******************
// FLOW-MAPPING-END
// ****************
//
// flow_mapping_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)?
// - *** *
// flow_mapping ::= FLOW-MAPPING-START
// ******************
// (flow_mapping_entry FLOW-ENTRY)*
// * **********
// flow_mapping_entry?
// ******************
// FLOW-MAPPING-END
// ****************
// flow_mapping_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)?
// * *** *
func yaml_parser_parse_flow_mapping_key(parser *yaml_parser_t, event *yaml_event_t, first bool) bool {
if first {
token := peek_token(parser)
@ -1144,8 +1129,9 @@ func yaml_parser_parse_flow_mapping_key(parser *yaml_parser_t, event *yaml_event
}
// Parse the productions:
// flow_mapping_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)?
// - ***** *
//
// flow_mapping_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)?
// * ***** *
func yaml_parser_parse_flow_mapping_value(parser *yaml_parser_t, event *yaml_event_t, empty bool) bool {
token := peek_token(parser)
if token == nil {

View file

@ -433,21 +433,19 @@ type yaml_document_t struct {
// The prototype of a read handler.
//
// The read handler is called when the parser needs to read more bytes from the
// source. The handler should write not more than size bytes to the buffer.
// The number of written bytes should be set to the size_read variable.
// The read handler is called when the parser needs to read more bytes from the
// source. The handler should write not more than size bytes to the buffer.
// The number of written bytes should be set to the size_read variable.
//
// [in,out] data A pointer to an application data specified by
// [in,out] data A pointer to an application data specified by
// yaml_parser_set_input().
// [out] buffer The buffer to write the data from the source.
// [in] size The size of the buffer.
// [out] size_read The actual number of bytes read from the source.
//
// yaml_parser_set_input().
//
// [out] buffer The buffer to write the data from the source.
// [in] size The size of the buffer.
// [out] size_read The actual number of bytes read from the source.
//
// On success, the handler should return 1. If the handler failed,
// the returned value should be 0. On EOF, the handler should set the
// size_read to 0 and return 1.
// On success, the handler should return 1. If the handler failed,
// the returned value should be 0. On EOF, the handler should set the
// size_read to 0 and return 1.
type yaml_read_handler_t func(parser *yaml_parser_t, buffer []byte) (n int, err error)
// This structure holds information about a potential simple key.
@ -655,19 +653,17 @@ type yaml_comment_t struct {
// The prototype of a write handler.
//
// The write handler is called when the emitter needs to flush the accumulated
// characters to the output. The handler should write @a size bytes of the
// @a buffer to the output.
// The write handler is called when the emitter needs to flush the accumulated
// characters to the output. The handler should write @a size bytes of the
// @a buffer to the output.
//
// @param[in,out] data A pointer to an application data specified by
// @param[in,out] data A pointer to an application data specified by
// yaml_emitter_set_output().
// @param[in] buffer The buffer with bytes to be written.
// @param[in] size The size of the buffer.
//
// yaml_emitter_set_output().
//
// @param[in] buffer The buffer with bytes to be written.
// @param[in] size The size of the buffer.
//
// @returns On success, the handler should return @c 1. If the handler failed,
// the returned value should be @c 0.
// @returns On success, the handler should return @c 1. If the handler failed,
// the returned value should be @c 0.
type yaml_write_handler_t func(emitter *yaml_emitter_t, buffer []byte) error
type yaml_emitter_state_t int

View file

@ -37,7 +37,7 @@ github.com/go-openapi/analysis/internal/flatten/operations
github.com/go-openapi/analysis/internal/flatten/replace
github.com/go-openapi/analysis/internal/flatten/schutils
github.com/go-openapi/analysis/internal/flatten/sortref
# github.com/go-openapi/codescan v0.35.1
# github.com/go-openapi/codescan v0.36.0
## explicit; go 1.25.0
github.com/go-openapi/codescan
github.com/go-openapi/codescan/internal/builders/common
@ -62,7 +62,7 @@ github.com/go-openapi/codescan/internal/scanner/classify
# github.com/go-openapi/errors v0.22.8
## explicit; go 1.25.0
github.com/go-openapi/errors
# github.com/go-openapi/inflect v0.21.6
# github.com/go-openapi/inflect v1.0.0
## explicit; go 1.25.0
github.com/go-openapi/inflect
# github.com/go-openapi/jsonpointer v1.0.0
@ -76,10 +76,10 @@ github.com/go-openapi/jsonreference/internal
# github.com/go-openapi/loads v0.25.0
## explicit; go 1.25.0
github.com/go-openapi/loads
# github.com/go-openapi/runtime v0.32.6
# github.com/go-openapi/runtime v0.33.0
## explicit; go 1.25.0
github.com/go-openapi/runtime
# github.com/go-openapi/runtime/server-middleware v0.32.6
# github.com/go-openapi/runtime/server-middleware v0.33.0
## explicit; go 1.25.0
github.com/go-openapi/runtime/server-middleware/docui
# github.com/go-openapi/spec v0.22.9
@ -90,43 +90,43 @@ github.com/go-openapi/spec
github.com/go-openapi/strfmt
github.com/go-openapi/strfmt/internal/bsonlite
github.com/go-openapi/strfmt/internal/countries
# github.com/go-openapi/swag/conv v0.27.3
# github.com/go-openapi/swag/conv v0.28.0
## explicit; go 1.25.0
github.com/go-openapi/swag/conv
# github.com/go-openapi/swag/fileutils v0.27.3
# github.com/go-openapi/swag/fileutils v0.28.0
## explicit; go 1.25.0
github.com/go-openapi/swag/fileutils
# github.com/go-openapi/swag/jsonutils v0.27.3
# github.com/go-openapi/swag/jsonutils v0.28.0
## explicit; go 1.25.0
github.com/go-openapi/swag/jsonutils
github.com/go-openapi/swag/jsonutils/adapters
github.com/go-openapi/swag/jsonutils/adapters/ifaces
github.com/go-openapi/swag/jsonutils/adapters/stdlib/json
# github.com/go-openapi/swag/loading v0.27.3
# github.com/go-openapi/swag/loading v0.28.0
## explicit; go 1.25.0
github.com/go-openapi/swag/loading
# github.com/go-openapi/swag/mangling v0.27.3
# github.com/go-openapi/swag/mangling v0.28.0
## explicit; go 1.25.0
github.com/go-openapi/swag/mangling
# github.com/go-openapi/swag/netutils v0.27.3
# github.com/go-openapi/swag/netutils v0.28.0
## explicit; go 1.25.0
github.com/go-openapi/swag/netutils
# github.com/go-openapi/swag/pools v0.27.3
# github.com/go-openapi/swag/pools v0.28.0
## explicit; go 1.25.0
github.com/go-openapi/swag/pools
# github.com/go-openapi/swag/stringutils v0.27.3
# github.com/go-openapi/swag/stringutils v0.28.0
## explicit; go 1.25.0
github.com/go-openapi/swag/stringutils
# github.com/go-openapi/swag/typeutils v0.27.3
# github.com/go-openapi/swag/typeutils v0.28.0
## explicit; go 1.25.0
github.com/go-openapi/swag/typeutils
# github.com/go-openapi/swag/yamlutils v0.27.3
# github.com/go-openapi/swag/yamlutils v0.28.0
## explicit; go 1.25.0
github.com/go-openapi/swag/yamlutils
# github.com/go-openapi/validate v0.26.1
## explicit; go 1.25.0
github.com/go-openapi/validate
# github.com/go-swagger/go-swagger v0.35.3
# github.com/go-swagger/go-swagger v0.36.0
## explicit; go 1.25.0
github.com/go-swagger/go-swagger/cmd/swagger
github.com/go-swagger/go-swagger/cmd/swagger/commands
@ -161,7 +161,7 @@ github.com/kr/text
# github.com/mattn/go-colorable v0.1.15
## explicit; go 1.18
github.com/mattn/go-colorable
# github.com/mattn/go-isatty v0.0.23
# github.com/mattn/go-isatty v0.0.24
## explicit; go 1.20
github.com/mattn/go-isatty
# github.com/mitchellh/copystructure v1.2.0
@ -170,7 +170,7 @@ github.com/mitchellh/copystructure
# github.com/mitchellh/reflectwalk v1.0.2
## explicit
github.com/mitchellh/reflectwalk
# github.com/oklog/ulid/v2 v2.1.1
# github.com/oklog/ulid/v2 v2.1.2
## explicit; go 1.15
github.com/oklog/ulid/v2
# github.com/pelletier/go-toml/v2 v2.4.3
@ -218,7 +218,7 @@ github.com/subosito/gotenv
# github.com/toqueteos/webbrowser v1.2.1
## explicit; go 1.12
github.com/toqueteos/webbrowser
# go.yaml.in/yaml/v3 v3.0.4
# go.yaml.in/yaml/v3 v3.0.5
## explicit; go 1.16
go.yaml.in/yaml/v3
# golang.org/x/crypto v0.54.0