diff --git a/build/swag/main.go b/build/swag/main.go index 31403e0120..f994e44d0d 100644 --- a/build/swag/main.go +++ b/build/swag/main.go @@ -56,9 +56,9 @@ var initFlags = []cli.Flag{ Name: parseVendorFlag, Usage: "Parse go files in 'vendor' folder, disabled by default", }, - &cli.BoolFlag{ + &cli.IntFlag{ Name: parseDependencyFlag, - Usage: "Parse go files in outside dependency folder, disabled by default", + Usage: "ParseDependencies whether swag should be parse outside dependency folder: 0 none, 1 models, 2 operations, 3 all. 0 by default", }, &cli.StringFlag{ Name: markdownFilesFlag, @@ -92,7 +92,7 @@ func initAction(c *cli.Context) error { PropNamingStrategy: strategy, OutputDir: c.String(outputFlag), ParseVendor: c.Bool(parseVendorFlag), - ParseDependency: c.Bool(parseDependencyFlag), + ParseDependency: c.Int(parseDependencyFlag), MarkdownFilesDir: c.String(markdownFilesFlag), ParseInternal: c.Bool(parseInternal), GeneratedTime: c.Bool(generatedTimeFlag), diff --git a/go.mod b/go.mod index e93172a1b4..7da9092b30 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.19 require ( github.com/DATA-DOG/go-sqlmock v1.5.0 github.com/Masterminds/semver/v3 v3.1.1 - github.com/actiontech/dms v0.0.0-20240426094735-693e797b6293 + github.com/actiontech/dms v0.0.0-20240830025931-20d313b918e8 github.com/actiontech/java-sql-extractor v0.0.0-20231103015812-cdd5fc040f62 github.com/actiontech/mybatis-mapper-2-sql v0.5.1-0.20240806065717-29cde7000ef5 github.com/agiledragon/gomonkey v2.0.2+incompatible @@ -30,9 +30,9 @@ require ( github.com/github/gh-ost v1.1.3-0.20210727153850-e484824bbd68 github.com/go-git/go-git/v5 v5.9.0 github.com/go-ini/ini v1.63.2 - github.com/go-openapi/jsonreference v0.19.4 // indirect - github.com/go-openapi/spec v0.19.8 // indirect - github.com/go-openapi/swag v0.19.9 // indirect + github.com/go-openapi/jsonreference v0.19.6 // indirect + github.com/go-openapi/spec v0.20.4 // indirect + github.com/go-openapi/swag v0.19.15 // indirect github.com/go-playground/locales v0.14.1 github.com/go-playground/universal-translator v0.18.1 github.com/go-playground/validator/v10 v10.14.1 @@ -57,10 +57,10 @@ require ( github.com/sirupsen/logrus v1.9.0 github.com/spf13/cobra v1.1.1 github.com/stretchr/testify v1.8.4 - github.com/swaggo/swag v1.6.7 + github.com/swaggo/swag v1.16.3 github.com/ungerik/go-dry v0.0.0-20210209114055-a3e162a9e62e github.com/urfave/cli/v2 v2.8.1 - golang.org/x/net v0.15.0 + golang.org/x/net v0.17.0 google.golang.org/grpc v1.50.1 gopkg.in/natefinch/lumberjack.v2 v2.0.0 gopkg.in/yaml.v2 v2.4.0 @@ -101,13 +101,12 @@ require ( github.com/facebookgo/ensure v0.0.0-20200202191622-63f1cf65ac4c // indirect github.com/facebookgo/freeport v0.0.0-20150612182905-d4adf43b75b9 // indirect github.com/gabriel-vasile/mimetype v1.4.2 // indirect - github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.5.0 // indirect github.com/go-mysql-org/go-mysql v1.3.0 // indirect github.com/go-ole/go-ole v1.2.4 // indirect github.com/go-openapi/errors v0.20.3 // indirect - github.com/go-openapi/jsonpointer v0.19.3 // indirect + github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/strfmt v0.21.7 // indirect github.com/golang-jwt/jwt/v4 v4.5.0 // indirect github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe // indirect @@ -126,11 +125,12 @@ require ( github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect + github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/labstack/gommon v0.4.0 // indirect github.com/leodido/go-urn v1.2.4 // indirect - github.com/mailru/easyjson v0.7.1 // indirect + github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.17 // indirect github.com/mitchellh/go-testing-interface v1.14.0 // indirect @@ -164,7 +164,7 @@ require ( go.uber.org/atomic v1.7.0 // indirect go.uber.org/multierr v1.6.0 // indirect go.uber.org/zap v1.17.0 // indirect - golang.org/x/crypto v0.13.0 // indirect + golang.org/x/crypto v0.14.0 // indirect golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc // indirect golang.org/x/mod v0.12.0 // indirect golang.org/x/sys v0.15.0 // indirect @@ -175,6 +175,7 @@ require ( gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + sigs.k8s.io/yaml v1.3.0 // indirect ) replace ( diff --git a/go.sum b/go.sum index d50a17917a..cf16c4304b 100644 --- a/go.sum +++ b/go.sum @@ -46,8 +46,8 @@ github.com/VividCortex/ewma v1.1.1 h1:MnEK4VOv6n0RSY4vtRe3h11qjxL3+t0B8yOL8iMXdc github.com/VividCortex/ewma v1.1.1/go.mod h1:2Tkkvm3sRDVXaiyucHiACn4cqf7DpdyLvmxzcbUokwA= github.com/acomagu/bufpipe v1.0.4 h1:e3H4WUzM3npvo5uv95QuJM3cQspFNtFBzvJ2oNjKIDQ= github.com/acomagu/bufpipe v1.0.4/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4= -github.com/actiontech/dms v0.0.0-20240426094735-693e797b6293 h1:c4YKOC0VPeCQLxdDVCA1kNwG/Xxq+AO6bavDNAudesk= -github.com/actiontech/dms v0.0.0-20240426094735-693e797b6293/go.mod h1:xhu4id1GnOt5Gip+xa+EGX9/0aKXxWIAn35VHE76kQQ= +github.com/actiontech/dms v0.0.0-20240830025931-20d313b918e8 h1:l5arXsG/J+z6k72JMy3J2eOXG/S9oJ/saoEpnT5K5Vg= +github.com/actiontech/dms v0.0.0-20240830025931-20d313b918e8/go.mod h1:6puFXgN9RWM/28xTntH++kiNgmCOQbjiYt5K58qmSo0= github.com/actiontech/java-sql-extractor v0.0.0-20231103015812-cdd5fc040f62 h1:JM7WnLzlvXOGE90KKd+aigi+qUDS+U5dLwQMNpTKZxE= github.com/actiontech/java-sql-extractor v0.0.0-20231103015812-cdd5fc040f62/go.mod h1:adDZHhAf2LRMx2h0JzofPXn12x2XlyQjVE116KXquwo= github.com/actiontech/mybatis-mapper-2-sql v0.5.1-0.20240806065717-29cde7000ef5 h1:vyQVrkYPzUV9d7gSvOWoezwWMTiC4jc3f3Hpianefq0= @@ -227,7 +227,6 @@ github.com/fsouza/fake-gcs-server v1.17.0/go.mod h1:D1rTE4YCyHFNa99oyJJ5HyclvN/0 github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32 h1:Mn26/9ZMNWSw9C9ERFA1PUxfmGpolnw2v0bKOREu5ew= github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I= github.com/gin-contrib/gzip v0.0.1/go.mod h1:fGBJBCdt6qCZuCAOwWuFhBB4OOq9EFqlo5dEaFhhu5w= github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= @@ -268,27 +267,28 @@ github.com/go-openapi/errors v0.20.3 h1:rz6kiC84sqNQoqrtulzaL/VERgkoCyB6WdEkc2uj github.com/go-openapi/errors v0.20.3/go.mod h1:Z3FlZ4I8jEGxjUK+bugx3on2mIAk4txuAOhlsB1FSgk= github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= -github.com/go-openapi/jsonpointer v0.19.3 h1:gihV7YNZK1iK6Tgwwsxo2rJbD1GTbdm72325Bq8FI3w= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= github.com/go-openapi/jsonreference v0.19.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= -github.com/go-openapi/jsonreference v0.19.4 h1:3Vw+rh13uq2JFNxgnMTGE1rnoieU9FmyE1gvnyylsYg= -github.com/go-openapi/jsonreference v0.19.4/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg= +github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs= +github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= github.com/go-openapi/spec v0.19.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= github.com/go-openapi/spec v0.19.4/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= github.com/go-openapi/spec v0.19.7/go.mod h1:Hm2Jr4jv8G1ciIAo+frC/Ft+rR2kQDh8JHKHb3gWUSk= -github.com/go-openapi/spec v0.19.8 h1:qAdZLh1r6QF/hI/gTq+TJTvsQUodZsM7KLqkAJdiJNg= -github.com/go-openapi/spec v0.19.8/go.mod h1:Hm2Jr4jv8G1ciIAo+frC/Ft+rR2kQDh8JHKHb3gWUSk= +github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M= +github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I= github.com/go-openapi/strfmt v0.21.7 h1:rspiXgNWgeUzhjo1YU01do6qsahtJNByjLVbPLNHb8k= github.com/go-openapi/strfmt v0.21.7/go.mod h1:adeGTkxE44sPyLk0JV235VQAO/ZXUr8KAzYjclFs3ew= github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/swag v0.19.8/go.mod h1:ao+8BpOPyKdpQz3AOJfbeEVpLmWAvlT1IfTe5McPyhY= -github.com/go-openapi/swag v0.19.9 h1:1IxuqvBUU3S2Bi4YC7tlP9SJF1gVpCvqN0T2Qof4azE= -github.com/go-openapi/swag v0.19.9/go.mod h1:ao+8BpOPyKdpQz3AOJfbeEVpLmWAvlT1IfTe5McPyhY= +github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= +github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= @@ -500,6 +500,8 @@ github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqx github.com/jonboulle/clockwork v0.1.0 h1:VKV+ZcuP6l3yW9doeqz6ziZGgcynBVQO+obU0+0hcPo= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/joomcode/errorx v1.0.1/go.mod h1:kgco15ekB6cs+4Xjzo7SPeXzx38PbJzBwbnu9qfVNHQ= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= @@ -559,8 +561,10 @@ github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czP github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.7.1 h1:mdxE1MF9o53iCb2Ghj1VfWvh7ZOwHpnVG/xwXrV90U8= github.com/mailru/easyjson v0.7.1/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A= github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= @@ -842,8 +846,8 @@ github.com/swaggo/swag v1.5.1/go.mod h1:1Bl9F/ZBpVWh22nY0zmYyASPO1lI/zIwRDrpZU+t github.com/swaggo/swag v1.6.3/go.mod h1:wcc83tB4Mb2aNiL/HP4MFeQdpHUrca+Rp/DRNgWAUio= github.com/swaggo/swag v1.6.5/go.mod h1:Y7ZLSS0d0DdxhWGVhQdu+Bu1QhaF5k0RD7FKdiAykeY= github.com/swaggo/swag v1.6.6-0.20200323071853-8e21f4cefeea/go.mod h1:xDhTyuFIujYiN3DKWC/H/83xcfHp+UE/IzWWampG7Zc= -github.com/swaggo/swag v1.6.7 h1:e8GC2xDllJZr3omJkm9YfmK0Y56+rMO3cg0JBKNz09s= -github.com/swaggo/swag v1.6.7/go.mod h1:xDhTyuFIujYiN3DKWC/H/83xcfHp+UE/IzWWampG7Zc= +github.com/swaggo/swag v1.16.3 h1:PnCYjPCah8FK4I26l2F/KQ4yz3sILcVUN3cTlBFA9Pg= +github.com/swaggo/swag v1.16.3/go.mod h1:DImHIuOFXKpMFAQjcC7FG4m3Dg4+QuUgUzJmKjI/gRk= github.com/syndtr/goleveldb v0.0.0-20180815032940-ae2bd5eed72d/go.mod h1:Z4AUp2Km+PwemOoO/VB5AOx9XSsIItzFjoJlOSiYmn0= github.com/syndtr/goleveldb v1.0.1-0.20190625010220-02440ea7a285 h1:uSDYjYejelKyceA6DiCsngFof9jAyeaSyX9XC5a1a7Q= github.com/syndtr/goleveldb v1.0.1-0.20190625010220-02440ea7a285/go.mod h1:9OrXJhf154huy1nPWmuSrkgjPUtUNhA+Zmy+6AESzuA= @@ -973,8 +977,8 @@ golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0 golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I= -golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= -golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -1041,6 +1045,7 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20210224082022-3d97a244fca7/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= golang.org/x/net v0.0.0-20210913180222-943fd674d43e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= @@ -1048,8 +1053,8 @@ golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= -golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1104,6 +1109,7 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1131,7 +1137,7 @@ golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo= -golang.org/x/term v0.12.0 h1:/ZfYdc3zq+q02Rv9vGqTeSItdzZTSNDmfTi0mBAuidU= +golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= @@ -1289,6 +1295,7 @@ gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= @@ -1305,6 +1312,8 @@ honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9 k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= +sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= sourcegraph.com/sourcegraph/appdash v0.0.0-20180531100431-4c381bd170b4 h1:VO9oZbbkvTwqLimlQt15QNdOOBArT2dw/bvzsMZBiqQ= sourcegraph.com/sourcegraph/appdash v0.0.0-20180531100431-4c381bd170b4/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= sourcegraph.com/sourcegraph/appdash-data v0.0.0-20151005221446-73f23eafcf67/go.mod h1:L5q+DGLGOQFpo1snNEkLOJT2d1YTW66rWNzatr3He1k= diff --git a/sqle/api/app.go b/sqle/api/app.go index 1bd5f29563..9b257dc61e 100644 --- a/sqle/api/app.go +++ b/sqle/api/app.go @@ -319,6 +319,13 @@ func StartApi(net *gracenet.Net, exitChan chan struct{}, config *config.SqleOpti // report push v1ProjectAdminRouter.PUT("/:project_name/report_push_configs/:report_push_config_id/", v1.UpdateReportPushConfig) v1ProjectRouter.GET("/:project_name/report_push_configs", v1.GetReportPushConfigList) + + // pipeline + v1ProjectRouter.POST("/:project_name/pipelines", v1.CreatePipeline) + v1ProjectRouter.DELETE("/:project_name/pipelines/:pipeline_id/", v1.DeletePipeline) + v1ProjectRouter.PATCH("/:project_name/pipelines/:pipeline_id/", v1.UpdatePipeline) + v1ProjectRouter.GET("/:project_name/pipelines", v1.GetPipelines) + v1ProjectRouter.GET("/:project_name/pipelines/:pipeline_id/", v1.GetPipelineDetail) } // project member router diff --git a/sqle/api/controller/v1/instance.go b/sqle/api/controller/v1/instance.go index 05e5dca17a..4176ea5df1 100644 --- a/sqle/api/controller/v1/instance.go +++ b/sqle/api/controller/v1/instance.go @@ -331,6 +331,7 @@ const ( // InstanceTipReqV1.FunctionalModule Enums create_audit_plan = "create_audit_plan" create_workflow = "create_workflow" create_optimization = "create_optimization" + create_pipeline = "create_pipeline" ) type InstanceTipReqV1 struct { @@ -390,6 +391,8 @@ func GetInstanceTips(c echo.Context) error { operationType = v1.OpPermissionTypeCreateWorkflow case create_optimization: operationType = v1.OpPermissionTypeCreateOptimization + case create_pipeline: + operationType = v1.OpPermissionTypeCreatePipeline default: } dbServiceReq := &dmsV1.ListDBServiceReq{ diff --git a/sqle/api/controller/v1/pipeline.go b/sqle/api/controller/v1/pipeline.go index 7ba711dab3..6871f21712 100644 --- a/sqle/api/controller/v1/pipeline.go +++ b/sqle/api/controller/v1/pipeline.go @@ -1,7 +1,13 @@ package v1 import ( + "context" + "net/http" + "strconv" + "github.com/actiontech/sqle/sqle/api/controller" + "github.com/actiontech/sqle/sqle/dms" + "github.com/actiontech/sqle/sqle/server/pipeline" "github.com/labstack/echo/v4" ) @@ -12,6 +18,14 @@ type pipelineDetail struct { pipelineBase } +func (p *pipelineDetail) fillWith(pipe *pipeline.Pipeline) { + p.ID = pipe.ID + p.NodeCount = pipe.NodeCount() + p.Name = pipe.Name + p.Description = pipe.Description + p.Address = pipe.Address +} + // pipelineBase 流水线基础信息 type pipelineBase struct { Name string `json:"name" valid:"required"` // 流水线名称 @@ -19,6 +33,14 @@ type pipelineBase struct { Address string `json:"address"` // 关联流水线地址 } +func (pipe *pipelineBase) toSvcPipeline() *pipeline.Pipeline { + return &pipeline.Pipeline{ + Name: pipe.Name, + Description: pipe.Description, + Address: pipe.Address, + } +} + // pipelineNodeDetail 流水线节点的信息详情 type pipelineNodeDetail struct { ID uint `json:"id"` // 节点的唯一标识符 @@ -26,6 +48,22 @@ type pipelineNodeDetail struct { pipelineNodeBase } +func (p *pipelineNodeDetail) fillWith(node *pipeline.PipelineNode) { + if node == nil { + return + } + p.ID = node.ID + p.Name = node.Name + p.Type = node.NodeType + p.InstanceName = node.InstanceName + p.InstanceType = node.InstanceType + p.ObjectPath = node.ObjectPath + p.ObjectType = node.ObjectType + p.AuditMethod = node.AuditMethod + p.RuleTemplateName = node.RuleTemplateName + p.IntegrationInfo = node.IntegrationInfo() +} + // pipelineNodeBase 流水线节点基础信息 type pipelineNodeBase struct { Name string `json:"name" valid:"required"` // 节点名称,必填,支持中文、英文+数字+特殊字符 @@ -38,12 +76,35 @@ type pipelineNodeBase struct { RuleTemplateName string `json:"rule_template_name" valid:"required"` // 审核规则模板,必填 } +func (node *pipelineNodeBase) toSvcPipelineNode() *pipeline.PipelineNode { + return &pipeline.PipelineNode{ + Name: node.Name, + NodeType: node.Type, + InstanceName: node.InstanceName, + InstanceType: node.InstanceType, + ObjectPath: node.ObjectPath, + ObjectType: node.ObjectType, + AuditMethod: node.AuditMethod, + RuleTemplateName: node.RuleTemplateName, + } +} + // CreatePipelineReqV1 用于创建流水线的请求结构体 type CreatePipelineReqV1 struct { pipelineBase Nodes []pipelineNodeBase `json:"nodes" valid:"dive,required"` // 节点信息 } +func (req *CreatePipelineReqV1) convertToSvcPipeline(projectUID string) *pipeline.Pipeline { + pipe := req.pipelineBase.toSvcPipeline() + pipe.ProjectUID = projectUID + pipe.PipelineNodes = make([]*pipeline.PipelineNode, 0, len(req.Nodes)) + for _, node := range req.Nodes { + pipe.PipelineNodes = append(pipe.PipelineNodes, node.toSvcPipelineNode()) + } + return pipe +} + type CreatePipelineResV1 struct { controller.BaseRes Data createPipelineResData `json:"data"` @@ -53,6 +114,56 @@ type createPipelineResData struct { PipelineID uint `json:"pipeline_id"` // 流水线的唯一标识符 } +// @Summary 创建流水线 +// @Description create pipeline +// @Id createPipelineV1 +// @Tags pipeline +// @Security ApiKeyAuth +// @Accept json +// @Param project_name path string true "project name" +// @Param pipeline body v1.CreatePipelineReqV1 true "create pipeline" +// @Success 200 {object} v1.CreatePipelineResV1 +// @router /v1/projects/{project_name}/pipelines [post] +func CreatePipeline(c echo.Context) error { + // 1. 解析请求体中的数据 + req := new(CreatePipelineReqV1) + if err := controller.BindAndValidateReq(c, req); err != nil { + return controller.JSONBaseErrorReq(c, err) + } + + // 2. 获取项目 UID + projectUid, err := dms.GetPorjectUIDByName(context.TODO(), c.Param("project_name"), true) + if err != nil { + return controller.JSONBaseErrorReq(c, err) + } + + svcPipeline := req.convertToSvcPipeline(projectUid) + + var pipelineSvc pipeline.PipelineSvc + + // 3. 自动匹配与添加审核模板 + err = pipelineSvc.CheckRuleTemplate(svcPipeline) + if err != nil { + return controller.JSONBaseErrorReq(c, err) + } + err = pipelineSvc.CheckInstance(c.Request().Context(), svcPipeline) + if err != nil { + return controller.JSONBaseErrorReq(c, err) + } + // 4. 保存流水线 + err = pipelineSvc.CreatePipeline(svcPipeline, controller.GetUserID(c)) + if err != nil { + return controller.JSONBaseErrorReq(c, err) + } + // 5. 返回成功响应 + return c.JSON(http.StatusOK, &CreatePipelineResV1{ + BaseRes: controller.NewBaseReq(nil), + Data: createPipelineResData{ + PipelineID: svcPipeline.ID, + }, + }) +} + // GetPipelinesReqV1 用于请求获取流水线列表的结构体 type GetPipelinesReqV1 struct { PageIndex uint32 `json:"page_index" query:"page_index" valid:"required"` // 页码索引 @@ -67,6 +178,52 @@ type GetPipelinesResV1 struct { TotalNums uint64 `json:"total_nums"` // 流水线总数 } +// @Summary 获取流水线列表 +// @Description get pipeline list +// @Id getPipelinesV1 +// @Tags pipeline +// @Security ApiKeyAuth +// @Param project_name path string true "project name" +// @Param fuzzy_search_name_desc query string false "fuzzy search pipeline name and description" +// @Param page_index query uint32 true "page index" +// @Param page_size query uint32 true "size of per page" +// @Success 200 {object} v1.GetPipelinesResV1 +// @router /v1/projects/{project_name}/pipelines [get] +func GetPipelines(c echo.Context) error { + // 1. 解析请求体中的数据 + req := new(GetPipelinesReqV1) + if err := controller.BindAndValidateReq(c, req); err != nil { + return controller.JSONBaseErrorReq(c, err) + } + + // 2. 获取项目 UID + projectUid, err := dms.GetPorjectUIDByName(context.TODO(), c.Param("project_name")) + if err != nil { + return controller.JSONBaseErrorReq(c, err) + } + + // 3. 计算分页参数 + limit, offset := controller.GetLimitAndOffset(req.PageIndex, req.PageSize) + + // 4. 获取存储对象并查询流水线列表 + var pipelineSvc pipeline.PipelineSvc + count, pipelineList, err := pipelineSvc.GetPipelineList(limit, offset, req.FuzzySearchNameDesc, projectUid) + if err != nil { + return controller.JSONBaseErrorReq(c, err) + } + + data := make([]pipelineDetail, len(pipelineList)) + for idx, pipe := range pipelineList { + data[idx].fillWith(pipe) + } + // 5. 返回成功响应 + return c.JSON(http.StatusOK, &GetPipelinesResV1{ + BaseRes: controller.NewBaseReq(nil), + TotalNums: count, + Data: data, + }) +} + // GetPipelineDetailReqV1 用于请求获取流水线详情的结构体 type GetPipelineDetailReqV1 struct { PipelineID string `json:"pipeline_id" query:"pipeline_id" valid:"required"` // 流水线的唯一标识符 @@ -78,6 +235,48 @@ type GetPipelineDetailResV1 struct { Data pipelineDetailData `json:"data"` } +// @Summary 获取流水线详情 +// @Description get pipeline detail +// @Id getPipelineDetailV1 +// @Tags pipeline +// @Security ApiKeyAuth +// @Param project_name path string true "project name" +// @Param pipeline_id path string true "pipeline id" +// @Success 200 {object} v1.GetPipelineDetailResV1 +// @router /v1/projects/{project_name}/pipelines/{pipeline_id}/ [get] +func GetPipelineDetail(c echo.Context) error { + projectUid, err := dms.GetPorjectUIDByName(context.TODO(), c.Param("project_name")) + if err != nil { + return controller.JSONBaseErrorReq(c, err) + } + + pipelineID, err := strconv.Atoi(c.Param("pipeline_id")) + if err != nil { + return controller.JSONBaseErrorReq(c, err) + } + + var pipelineSvc pipeline.PipelineSvc + pipe, err := pipelineSvc.GetPipeline(projectUid, uint(pipelineID)) + if err != nil { + return controller.JSONBaseErrorReq(c, err) + } + + var pipelineDetail pipelineDetail + pipelineDetail.fillWith(pipe) + nodeDetails := make([]pipelineNodeDetail, len(pipe.PipelineNodes)) + for i, node := range pipe.PipelineNodes { + nodeDetails[i].fillWith(node) + } + + return c.JSON(http.StatusOK, &GetPipelineDetailResV1{ + BaseRes: controller.NewBaseReq(nil), + Data: pipelineDetailData{ + pipelineDetail: pipelineDetail, + Nodes: nodeDetails, + }, + }) +} + type pipelineDetailData struct { pipelineDetail Nodes []pipelineNodeDetail `json:"nodes"` // 流水线节点信息 @@ -86,60 +285,74 @@ type pipelineDetailData struct { // UpdatePipelineReqV1 用于更新流水线的请求结构体 type UpdatePipelineReqV1 struct { pipelineBase - Nodes []pipelineNodeToBeUpdated `json:"nodes,omitempty" valid:"dive,required"` // 节点信息 + Nodes []updatePipelineNode `json:"nodes,omitempty" valid:"dive,required"` // 节点信息 } -type pipelineNodeToBeUpdated struct { +type updatePipelineNode struct { ID uint `json:"id"` pipelineNodeBase } -// DeletePipelineReqV1 用于删除流水线的请求结构体 -type DeletePipelineReqV1 struct { - ProjectName string `json:"project_name" valid:"required"` // 项目名称,必填 - PipelineID string `json:"pipeline_id" valid:"required"` // 流水线 ID,必填 +func (req UpdatePipelineReqV1) convertToSvcPipeline(projectUID string, pipelineID uint) *pipeline.Pipeline { + svcPipeline := req.pipelineBase.toSvcPipeline() + svcPipeline.ID = pipelineID + svcPipeline.ProjectUID = projectUID + svcPipeline.PipelineNodes = make([]*pipeline.PipelineNode, 0, len(req.Nodes)) + for idx, node := range req.Nodes { + svcPipeline.PipelineNodes = append(svcPipeline.PipelineNodes, node.toSvcPipelineNode()) + svcPipeline.PipelineNodes[idx].ID = node.ID + } + return svcPipeline } -// @Summary 创建流水线 -// @Description create pipeline -// @Id createPipelineV1 +// @Summary 更新流水线 +// @Description update pipeline +// @Id updatePipelineV1 // @Tags pipeline // @Security ApiKeyAuth -// @Accept json // @Param project_name path string true "project name" -// @Param pipeline body v1.CreatePipelineReqV1 true "create pipeline" -// @Success 200 {object} v1.CreatePipelineResV1 -// @router /v1/projects/{project_name}/pipelines [post] -func CreatePipeline(c echo.Context) error { - return nil -} +// @Param pipeline_id path string true "pipeline id" +// @Param pipeline body v1.UpdatePipelineReqV1 true "update pipeline" +// @Success 200 {object} controller.BaseRes +// @router /v1/projects/{project_name}/pipelines/{pipeline_id}/ [patch] +func UpdatePipeline(c echo.Context) error { -// @Summary 获取流水线列表 -// @Description get pipeline list -// @Id getPipelinesV1 -// @Tags pipeline -// @Security ApiKeyAuth -// @Param project_name path string true "project name" -// @Param fuzzy_search_name_desc query string false "fuzzy search pipeline name and description" -// @Param page_index query uint32 true "page index" -// @Param page_size query uint32 true "size of per page" -// @Success 200 {object} v1.GetPipelinesResV1 -// @router /v1/projects/{project_name}/pipelines [get] -func GetPipelines(c echo.Context) error { - return nil + req := new(UpdatePipelineReqV1) + if err := controller.BindAndValidateReq(c, req); err != nil { + return controller.JSONBaseErrorReq(c, err) + } + projectUid, err := dms.GetPorjectUIDByName(context.TODO(), c.Param("project_name")) + if err != nil { + return controller.JSONBaseErrorReq(c, err) + } + pipelineID, err := strconv.Atoi(c.Param("pipeline_id")) + if err != nil { + return controller.JSONBaseErrorReq(c, err) + } + + var pipelineSvc pipeline.PipelineSvc + svcPipeline := req.convertToSvcPipeline(projectUid, uint(pipelineID)) + + err = pipelineSvc.CheckRuleTemplate(svcPipeline) + if err != nil { + return controller.JSONBaseErrorReq(c, err) + } + err = pipelineSvc.CheckInstance(c.Request().Context(), svcPipeline) + if err != nil { + return controller.JSONBaseErrorReq(c, err) + } + err = pipelineSvc.UpdatePipeline(svcPipeline, controller.GetUserID(c)) + if err != nil { + return controller.JSONBaseErrorReq(c, err) + } + + return c.JSON(http.StatusOK, controller.NewBaseReq(nil)) } -// @Summary 获取流水线详情 -// @Description get pipeline detail -// @Id getPipelineDetailV1 -// @Tags pipeline -// @Security ApiKeyAuth -// @Param project_name path string true "project name" -// @Param pipeline_id path string true "pipeline id" -// @Success 200 {object} v1.GetPipelineDetailResV1 -// @router /v1/projects/{project_name}/pipelines/{pipeline_id}/ [get] -func GetPipelineDetail(c echo.Context) error { - return nil +// DeletePipelineReqV1 用于删除流水线的请求结构体 +type DeletePipelineReqV1 struct { + ProjectName string `json:"project_name" valid:"required"` // 项目名称,必填 + PipelineID string `json:"pipeline_id" valid:"required"` // 流水线 ID,必填 } // @Summary 删除流水线 @@ -152,19 +365,20 @@ func GetPipelineDetail(c echo.Context) error { // @Success 200 {object} controller.BaseRes // @router /v1/projects/{project_name}/pipelines/{pipeline_id}/ [delete] func DeletePipeline(c echo.Context) error { - return nil -} + // 获取项目的 UID + projectUid, err := dms.GetPorjectUIDByName(context.TODO(), c.Param("project_name")) + if err != nil { + return controller.JSONBaseErrorReq(c, err) + } + pipelineID, err := strconv.Atoi(c.Param("pipeline_id")) + if err != nil { + return controller.JSONBaseErrorReq(c, err) + } + var pipelineSvc pipeline.PipelineSvc + err = pipelineSvc.DeletePipeline(projectUid, uint(pipelineID)) + if err != nil { + return controller.JSONBaseErrorReq(c, err) + } -// @Summary 更新流水线 -// @Description update pipeline -// @Id updatePipelineV1 -// @Tags pipeline -// @Security ApiKeyAuth -// @Param project_name path string true "project name" -// @Param pipeline_id path string true "pipeline id" -// @Param pipeline body v1.UpdatePipelineReqV1 true "update pipeline" -// @Success 200 {object} controller.BaseRes -// @router /v1/projects/{project_name}/pipelines/{pipeline_id}/ [patch] -func UpdatePipeline(c echo.Context) error { - return nil + return c.JSON(http.StatusOK, controller.NewBaseReq(nil)) } diff --git a/sqle/model/pipline.go b/sqle/model/pipline.go new file mode 100644 index 0000000000..78394718cc --- /dev/null +++ b/sqle/model/pipline.go @@ -0,0 +1,158 @@ +package model + +import ( + "fmt" + + "github.com/actiontech/sqle/sqle/errors" + "gorm.io/gorm" +) + +func init() { + autoMigrateList = append(autoMigrateList, &Pipeline{}) + autoMigrateList = append(autoMigrateList, &PipelineNode{}) +} + +// 定义节点类型 +type PipelineNodeType string + +const ( + NodeTypeAudit PipelineNodeType = "audit" + NodeTypeRelease PipelineNodeType = "release" +) + +// 定义审核对象类型 +type ObjectType string + +const ( + ObjectTypeSQL ObjectType = "sql" + ObjectTypeMyBatis ObjectType = "mybatis" +) + +// 定义审核方式 +type AuditMethod string + +const ( + AuditMethodOffline AuditMethod = "offline" + AuditMethodOnline AuditMethod = "online" +) + +type Pipeline struct { + Model + ProjectUid ProjectUID `gorm:"index; not null" json:"project_uid"` // 关联的流水线ID + Name string `gorm:"type:varchar(255);not null" json:"name"` // 流水线名称 + Description string `gorm:"type:varchar(512)" json:"description"` // 流水线描述 + Address string `gorm:"type:varchar(255)" json:"address"` // 关联流水线地址 +} + +type PipelineNode struct { + gorm.Model + PipelineID uint `gorm:"type:bigint;not null;index" json:"pipeline_id"` // 关联的流水线ID + UUID string `gorm:"type:varchar(255);not null" json:"uuid"` // 节点uuid + Name string `gorm:"type:varchar(255);not null" json:"name"` // 节点名称 + NodeType string `gorm:"type:varchar(20);not null" json:"node_type"` // 节点类型 + NodeVersion string `gorm:"type:varchar(255)" json:"node_version"` // 节点版本 + InstanceName string `gorm:"type:varchar(255)" json:"instance_name,omitempty"` // 数据源名称,在线审核时必填 + InstanceType string `gorm:"type:varchar(255)" json:"instance_type,omitempty"` // 数据源类型,离线审核时必填 + ObjectPath string `gorm:"type:varchar(512);not null" json:"object_path"` // 审核脚本路径 + ObjectType string `gorm:"type:varchar(20);not null" json:"object_type"` // 审核对象类型 + AuditMethod string `gorm:"type:varchar(20);not null" json:"audit_method"` // 审核方式 + RuleTemplateName string `gorm:"type:varchar(255);not null" json:"rule_template_name"` // 审核规则模板 + Token string `gorm:"type:varchar(512);not null" json:"token"` // token +} + +func (p *PipelineNode) BeforeSave(tx *gorm.DB) (err error) { + if !isValidPipelineNodeType(p.NodeType) { + return fmt.Errorf("invalid node type: %s", p.NodeType) + } + if !isValidObjectType(p.ObjectType) { + return fmt.Errorf("invalid object type: %s", p.ObjectType) + } + if !isValidAuditMethod(p.AuditMethod) { + return fmt.Errorf("invalid audit method: %s", p.AuditMethod) + } + return nil +} + +func isValidPipelineNodeType(t string) bool { + for _, validType := range []PipelineNodeType{NodeTypeAudit, NodeTypeRelease} { + if PipelineNodeType(t) == validType { + return true + } + } + return false +} + +func isValidObjectType(o string) bool { + for _, validObjectType := range []ObjectType{ObjectTypeSQL, ObjectTypeMyBatis} { + if ObjectType(o) == validObjectType { + return true + } + } + return false +} + +func isValidAuditMethod(a string) bool { + for _, validMethod := range []AuditMethod{AuditMethodOffline, AuditMethodOnline} { + if AuditMethod(a) == validMethod { + return true + } + } + return false +} + +func (s *Storage) GetPipelineList(projectID ProjectUID, fuzzySearchContent string, limit, offset uint32) ([]*Pipeline, uint64, error) { + var count int64 + var pipelines []*Pipeline + query := s.db.Model(&Pipeline{}).Where("project_uid = ?", projectID) + + if fuzzySearchContent != "" { + query = query.Where("name LIKE ? OR description LIKE ?", "%"+fuzzySearchContent+"%", "%"+fuzzySearchContent+"%") + } + + err := query.Count(&count).Error + if err != nil { + return pipelines, uint64(count), errors.New(errors.ConnectStorageError, err) + } + + if count == 0 { + return pipelines, uint64(count), nil + } + + err = query.Offset(int(offset)).Limit(int(limit)).Order("id desc").Find(&pipelines).Error + return pipelines, uint64(count), errors.New(errors.ConnectStorageError, err) +} + +func (s *Storage) GetPipelineDetail(projectID ProjectUID, pipelineID uint) (*Pipeline, error) { + pipeline := &Pipeline{} + err := s.db.Model(Pipeline{}).Where("project_uid = ? AND id = ?", projectID, pipelineID).First(pipeline).Error + if err != nil { + return pipeline, errors.New(errors.ConnectStorageError, err) + } + return pipeline, nil +} + +func (s *Storage) GetPipelineNodes(pipelineID uint) ([]*PipelineNode, error) { + var nodes []*PipelineNode + err := s.db.Model(PipelineNode{}).Where("pipeline_id = ?", pipelineID).Find(&nodes).Error + if err != nil { + return nodes, errors.New(errors.ConnectStorageError, err) + } + return nodes, nil +} + +func (s *Storage) CreatePipeline(pipeline *Pipeline, nodes []*PipelineNode) error { + return s.Tx(func(txDB *gorm.DB) error { + // 4.1 保存 Pipeline 到数据库 + if err := txDB.Create(pipeline).Error; err != nil { + return fmt.Errorf("failed to create pipeline: %w", err) + } + // 4.2 创建 PipelineNodes 并保存到数据库 + for _, node := range nodes { + node.PipelineID = pipeline.ID + if err := txDB.Create(node).Error; err != nil { + return fmt.Errorf("failed to create pipeline node: %w", err) + } + } + return nil + }) +} diff --git a/sqle/server/pipeline/pipeline.go b/sqle/server/pipeline/pipeline.go new file mode 100644 index 0000000000..a7bb43346b --- /dev/null +++ b/sqle/server/pipeline/pipeline.go @@ -0,0 +1,423 @@ +package pipeline + +import ( + "context" + "fmt" + "net" + "net/url" + "time" + + dmsCommonJwt "github.com/actiontech/dms/pkg/dms-common/api/jwt" + "github.com/actiontech/sqle/sqle/api/controller" + "github.com/actiontech/sqle/sqle/dms" + "github.com/actiontech/sqle/sqle/model" + "github.com/aliyun/credentials-go/credentials/utils" + + "gorm.io/gorm" +) + +type Pipeline struct { + ID uint // 流水线的唯一标识符 + ProjectUID string // 项目UID + Token string // token + Name string // 流水线名称 + Description string // 流水线描述 + Address string // 关联流水线地址 + PipelineNodes []*PipelineNode // 节点 +} + +func (pipe Pipeline) NodeCount() uint32 { + return uint32(len(pipe.PipelineNodes)) +} + +func (node PipelineNode) IntegrationInfo() string { + dmsAddr := controller.GetDMSServerAddress() + parsedURL, err := url.Parse(dmsAddr) + if err != nil { + return "" + } + ip, port, err := net.SplitHostPort(parsedURL.Host) + if err != nil { + return "" + } + + switch model.PipelineNodeType(node.NodeType) { + case model.NodeTypeAudit: + var cmdUsage = "#使用方法#\n1. 确保运行该命令的用户具有scannerd的执行权限。\n2. 在scannerd文件所在目录执行启动命令。\n#启动命令#\n" + baseCmd := "./scannerd %s --host=\"%s\" --port=\"%s\" --dir=\"%s\" --token=\"%s\"" + var extraArgs string + var cmdType string + if model.ObjectType(node.ObjectType) == model.ObjectTypeSQL { + cmdType = "sql_file" + } + if model.ObjectType(node.ObjectType) == model.ObjectTypeMyBatis { + cmdType = "mysql_mybatis" + } + if model.AuditMethod(node.AuditMethod) == model.AuditMethodOnline { + extraArgs = fmt.Sprintf(" --instance-name=\"%s\"", node.InstanceName) + } + if model.AuditMethod(node.AuditMethod) == model.AuditMethodOffline { + extraArgs = fmt.Sprintf(" --db-type=\"%s\"", node.InstanceType) + } + return fmt.Sprintf(cmdUsage+baseCmd+extraArgs, cmdType, ip, port, node.ObjectPath, node.Token) + case model.NodeTypeRelease: + return "" + default: + return "" + } +} + +type PipelineNode struct { + ID uint // 节点的唯一标识符,在更新时必填 + Version string // 节点版本ID + Name string // 节点名称,必填,支持中文、英文+数字+特殊字符 + NodeType string // 节点类型,必填,选项为“审核”或“上线” + InstanceName string // 数据源名称,在线审核时必填 + InstanceType string // 数据源类型,在线审核时必填 + ObjectPath string // 审核脚本路径,必填,用户填写文件路径 + ObjectType string // 审核对象类型,必填,可选项为SQL文件、MyBatis文件 + AuditMethod string // 审核方式,必选,可选项为离线审核、在线审核 + RuleTemplateName string // 审核规则模板,必填 + Token string // 节点Token +} + +type PipelineSvc struct{} + +func (svc PipelineSvc) CheckRuleTemplate(pipe *Pipeline) (err error) { + s := model.GetStorage() + for _, node := range pipe.PipelineNodes { + exist, err := s.IsRuleTemplateExist(node.RuleTemplateName, []string{ + pipe.ProjectUID, + model.ProjectIdForGlobalRuleTemplate, + }) + if err != nil { + return err + } + if !exist { + return fmt.Errorf("rule template does not exist") + } + } + return nil +} + +func (svc PipelineSvc) CheckInstance(ctx context.Context, pipe *Pipeline) (err error) { + for _, node := range pipe.PipelineNodes { + if node.InstanceName != "" { + instance, exist, err := dms.GetInstanceInProjectByName(ctx, pipe.ProjectUID, node.InstanceName) + if err != nil { + return err + } + if !exist { + return fmt.Errorf("instance does not exist") + } + node.InstanceType = instance.DbType + } + } + return nil +} + +func (svc PipelineSvc) CreatePipeline(pipe *Pipeline, userID string) error { + s := model.GetStorage() + modelPipeline := svc.toModelPipeline(pipe) + modelPipelineNodes := svc.toModelPipelineNodes(pipe, userID) + err := s.CreatePipeline(modelPipeline, modelPipelineNodes) + if err != nil { + return err + } + pipe.ID = modelPipeline.ID + return nil +} + +func (svc PipelineSvc) toModelPipeline(pipe *Pipeline) *model.Pipeline { + if pipe == nil { + return nil + } + + return &model.Pipeline{ + ProjectUid: model.ProjectUID(pipe.ProjectUID), + Name: pipe.Name, + Description: pipe.Description, + Address: pipe.Address, + } +} + +func (svc PipelineSvc) toModelPipelineNodes(pipe *Pipeline, userId string) []*model.PipelineNode { + if pipe == nil || len(pipe.PipelineNodes) == 0 { + return nil + } + nodeVersion := svc.newVersion() + modelNodes := make([]*model.PipelineNode, 0, len(pipe.PipelineNodes)) + for _, node := range pipe.PipelineNodes { + nodeUuid := utils.GetUUID() + token, err := svc.newToken(userId, nodeVersion, nodeUuid) + if err != nil { + return nil + } + modelNode := &model.PipelineNode{ + PipelineID: pipe.ID, // 需要将 Pipeline 的 ID 关联到 Node 上 + Name: node.Name, + NodeType: node.NodeType, + InstanceName: node.InstanceName, + InstanceType: node.InstanceType, + ObjectPath: node.ObjectPath, + ObjectType: node.ObjectType, + AuditMethod: node.AuditMethod, + RuleTemplateName: node.RuleTemplateName, + NodeVersion: nodeVersion, + UUID: nodeUuid, + Token: token, + } + modelNodes = append(modelNodes, modelNode) + } + + return modelNodes +} + +func (svc PipelineSvc) newVersion() string { + return utils.GetUUID() +} + +/* +1. token 由用户uid、node版本id、node版本下的uid和过期时间信息构成 +2. 因此同一版本每一个node都有一个唯一的token, token能够通过版本号+时间戳索引到唯一的node +3. 不同版本的node,当配置参数不变,可以通过继承node上一版本的token和版本下的uid和token,达到不需要启动命令的效果 +*/ +func (svc PipelineSvc) newToken(userId, version, uuid string) (string, error) { + token, err := dmsCommonJwt.GenJwtToken( + dmsCommonJwt.WithUserId(userId), + dmsCommonJwt.WithExpiredTime(365*24*time.Hour), + // TODO 带上版本和uuid信息 + ) + if err != nil { + return "", err + } + return token, nil +} + +func (svc PipelineSvc) GetPipeline(projectUID string, pipelineID uint) (*Pipeline, error) { + s := model.GetStorage() + modelPipeline, err := s.GetPipelineDetail(model.ProjectUID(projectUID), pipelineID) + if err != nil { + return nil, err + } + modelPiplineNodes, err := s.GetPipelineNodes(pipelineID) + if err != nil { + return nil, err + } + return svc.toPipeline(modelPipeline, modelPiplineNodes), nil +} + +func (svc PipelineSvc) GetPipelineList(limit, offset uint32, fuzzySearchNameDesc string, projectUID string) (count uint64, pipelines []*Pipeline, err error) { + s := model.GetStorage() + modelPipelines, count, err := s.GetPipelineList(model.ProjectUID(projectUID), fuzzySearchNameDesc, limit, offset) + if err != nil { + return 0, nil, err + } + pipelines = make([]*Pipeline, 0, len(modelPipelines)) + for _, modelPipeline := range modelPipelines { + modelPiplineNodes, err := s.GetPipelineNodes(modelPipeline.ID) + if err != nil { + return 0, nil, err + } + pipelines = append(pipelines, svc.toPipeline(modelPipeline, modelPiplineNodes)) + } + return count, pipelines, nil +} + +func (svc PipelineSvc) toPipeline(modelPipeline *model.Pipeline, modelPipelineNodes []*model.PipelineNode) *Pipeline { + if modelPipeline == nil { + return nil + } + pipeline := &Pipeline{ + ID: modelPipeline.ID, + ProjectUID: string(modelPipeline.ProjectUid), // 如果 ProjectUID 是字符串,可以直接转换 + Name: modelPipeline.Name, + Description: modelPipeline.Description, + Address: modelPipeline.Address, + } + if len(modelPipelineNodes) > 0 { + pipeline.PipelineNodes = make([]*PipelineNode, 0, len(modelPipelineNodes)) + for _, node := range modelPipelineNodes { + pipeline.PipelineNodes = append(pipeline.PipelineNodes, svc.toPipelineNode(node)) + } + } + return pipeline +} + +func (svc PipelineSvc) toPipelineNode(modelPipelineNode *model.PipelineNode) *PipelineNode { + if modelPipelineNode == nil { + return nil + } + return &PipelineNode{ + ID: modelPipelineNode.ID, + Name: modelPipelineNode.Name, + NodeType: modelPipelineNode.NodeType, + InstanceName: modelPipelineNode.InstanceName, + InstanceType: modelPipelineNode.InstanceType, + ObjectPath: modelPipelineNode.ObjectPath, + ObjectType: modelPipelineNode.ObjectType, + AuditMethod: modelPipelineNode.AuditMethod, + RuleTemplateName: modelPipelineNode.RuleTemplateName, + Token: modelPipelineNode.Token, + } +} + +/* +needUpdateToken比较旧节点(oldNode)和新节点(newNode)的属性 + + 若以下任意属性发生变化,则认为需要更新 token + 1. RuleTemplateName:规则模板名称 + 2. NodeType:节点类型 + 3. ObjectPath:对象路径 + 4. ObjectType:对象类型 + 5. AuditMethod:审计方法 + 6. InstanceName:实例名称 + 7. InstanceType:实例类型 +*/ +func (svc PipelineSvc) needUpdateToken(oldNode *model.PipelineNode, newNode *PipelineNode) bool { + return newNode.RuleTemplateName != oldNode.RuleTemplateName || + newNode.NodeType != oldNode.NodeType || + newNode.ObjectPath != oldNode.ObjectPath || + newNode.ObjectType != oldNode.ObjectType || + newNode.AuditMethod != oldNode.AuditMethod || + newNode.InstanceName != oldNode.InstanceName || + newNode.InstanceType != oldNode.InstanceType +} + +/* + *节点更新的逻辑有以下功能需求: + + 1. 流水线节点更新一次,对用户来说视为一个版本,数据存储上需要能够方便筛选出每一个版本的所有节点 + 2. 流水线的每一个节点,会生成一段执行脚本的命令,当节点信息修改可以不影响脚本命令时,不应改变脚本 + 3. Scannerd在客户的流水线中执行,请求SQLE时携带的token需要能够唯一确定SQLE上的一个流水线节点 + + *节点更新时,遵循以下逻辑: + + 1. 当用户没有修改会导致需要变更启动命令的配置时,节点version uuid token继承之前节点的数据 + 2. 当用户修改了节点配置,导致该节点要正确运行需要重新配置运行命令时,节点会使用新的version uuid token + +``` + + *在界面上的表示形态如下图所示: + 1. uv1表示的是用户理解的版本1 user version 1 + 2. node1 表示用户理解的节点1,uuid表示节点的唯一id + 3. n1-v1表示节点1的第一个版本 node 1 version 1 + 4. uv2表示用户理解的版本2,此时由于node1实际未修改,因此继承了之前节点的uuid version token,用户可以无需修改启动命令 + 5. 在用户修改的第三个版本中,即行uv3,节点1更新了参数(例如修改了审核路径,实际上用户也会知道需要修改命令),导致需要修改启动命令,因此这里的version token都更新了 + 版本 + ↓ + +-------+-------+-------+-------+ + | | node1 | node2 | node3 | <- 节点名称 + +-------+-------+-------+-------+ + | | uuid1 | uuid2 | uuid3 | <- 此列不展示 + +-------+-------+-------+-------+ + | uv1 | n1-v1 | n2-v1 | | + +-------+-------+-------+-------+ + | uv2 | n1-v1 | n2-v2 | | + +-------+-------+-------+-------+ + | uv3 | n1-v3 | n2-v2 | | + +-------+-------+-------+-------+ + | uv4 | n1-v3 | n2-v2 | n3-v4 | + +-------+-------+-------+-------+ + +``` +*/ +func (svc PipelineSvc) UpdatePipeline(pipe *Pipeline, userId string) error { + s := model.GetStorage() + return s.Tx(func(txDB *gorm.DB) error { + // 4.1 更新 pipeline + err := txDB.Model(&model.Pipeline{}). + Where("id = ? AND project_uid = ?", pipe.ID, pipe.ProjectUID). + Updates(model.Pipeline{ + Name: pipe.Name, + Description: pipe.Description, + Address: pipe.Address, + }).Error + if err != nil { + return fmt.Errorf("failed to update pipeline: %w", err) + } + nodes := []model.PipelineNode{} + if err := txDB.Where("pipeline_id = ?", pipe.ID).Find(&nodes).Error; err != nil { + return fmt.Errorf("failed to delete old pipeline nodes: %w", err) + } + oldNodeMap := make(map[uint] /* node id */ *model.PipelineNode, len(nodes)) + for idx, node := range nodes { + oldNodeMap[node.ID] = &nodes[idx] + } + // 4.2 删除旧的 pipeline nodes + if err := txDB.Where("pipeline_id = ?", pipe.ID).Delete(&model.PipelineNode{}).Error; err != nil { + return fmt.Errorf("failed to delete old pipeline nodes: %w", err) + } + v := svc.newVersion() + // 4.3 添加新的 pipeline nodes + var newToken string + var newUuid string + var newVersion string + for _, newNode := range pipe.PipelineNodes { + /* + 若节点命令不变更,则节点继承原有节点对应的token uuid和version + 若节点命令变更,则节点使用新的token uuid和version + */ + // 节点不变 + oldNode, exist := oldNodeMap[newNode.ID] + if exist { + newToken = oldNode.Token + newUuid = oldNode.UUID + newVersion = oldNode.NodeVersion + } + // 更新节点 + if exist && svc.needUpdateToken(oldNode, newNode) { + newVersion = v + newToken, err = svc.newToken(userId, newVersion, newUuid) + if err != nil { + return err + } + } + // 新建节点 + if !exist { + newVersion = v + newUuid = utils.GetUUID() + newToken, err = svc.newToken(userId, newVersion, newUuid) + if err != nil { + return err + } + } + node := model.PipelineNode{ + PipelineID: pipe.ID, + Name: newNode.Name, + NodeType: newNode.NodeType, + InstanceName: newNode.InstanceName, + InstanceType: newNode.InstanceType, + ObjectPath: newNode.ObjectPath, + ObjectType: newNode.ObjectType, + AuditMethod: newNode.AuditMethod, + RuleTemplateName: newNode.RuleTemplateName, + NodeVersion: newVersion, + Token: newToken, + UUID: newUuid, + } + + if err := txDB.Create(&node).Error; err != nil { + return fmt.Errorf("failed to update pipeline node: %w", err) + } + } + return nil + }) +} + +func (svc PipelineSvc) DeletePipeline(projectUID string, pipelineID uint) error { + s := model.GetStorage() + return s.Tx(func(txDB *gorm.DB) error { + // 删除 pipeline 相关的所有 nodes + if err := txDB.Model(&model.PipelineNode{}).Where("pipeline_id = ?", pipelineID).Delete(&model.PipelineNode{}).Error; err != nil { + return fmt.Errorf("failed to delete pipeline nodes: %w", err) + } + + // 删除 pipeline + if err := txDB.Model(&model.Pipeline{}).Where("id = ?", pipelineID).Delete(&model.Pipeline{}).Error; err != nil { + return fmt.Errorf("failed to delete pipeline: %w", err) + } + + return nil + }) +} diff --git a/vendor/github.com/actiontech/dms/pkg/dms-common/api/dms/v1/db_service.go b/vendor/github.com/actiontech/dms/pkg/dms-common/api/dms/v1/db_service.go index 8a08ff889f..4fca05ef0b 100644 --- a/vendor/github.com/actiontech/dms/pkg/dms-common/api/dms/v1/db_service.go +++ b/vendor/github.com/actiontech/dms/pkg/dms-common/api/dms/v1/db_service.go @@ -95,6 +95,12 @@ type Time struct { Minute int `json:"minute"` } +type AuditPlanTypes struct { + AuditPlanId uint `json:"audit_plan_id"` + AuditPlanType string `json:"type"` + AuditPlanTypeDesc string `json:"desc"` +} + // A dms db Service type ListDBService struct { // db service uid @@ -127,6 +133,10 @@ type ListDBService struct { AdditionalParams []*AdditionalParam `json:"additional_params"` // is enable masking IsEnableMasking bool `json:"is_enable_masking"` + // audit plan types + AuditPlanTypes []*AuditPlanTypes `json:"audit_plan_types"` + // instance audit plan id + InstanceAuditPlanID uint `json:"instance_audit_plan_id,omitempty"` } type SQLEConfig struct { diff --git a/vendor/github.com/actiontech/dms/pkg/dms-common/api/dms/v1/plugin.go b/vendor/github.com/actiontech/dms/pkg/dms-common/api/dms/v1/plugin.go index 0863638b31..e2d6f59086 100644 --- a/vendor/github.com/actiontech/dms/pkg/dms-common/api/dms/v1/plugin.go +++ b/vendor/github.com/actiontech/dms/pkg/dms-common/api/dms/v1/plugin.go @@ -52,10 +52,8 @@ type Plugin struct { OperateDataResourceHandleUrl string `json:"operate_data_resource_handle_url"` } -// swagger:parameters RegisterDMSPlugin +// swagger:model type RegisterDMSPluginReq struct { - // Register dms plugin - // in:body Plugin *Plugin `json:"plugin" validate:"required"` } diff --git a/vendor/github.com/actiontech/dms/pkg/dms-common/api/dms/v1/proxy.go b/vendor/github.com/actiontech/dms/pkg/dms-common/api/dms/v1/proxy.go index ba68826d7e..36dc6554b3 100644 --- a/vendor/github.com/actiontech/dms/pkg/dms-common/api/dms/v1/proxy.go +++ b/vendor/github.com/actiontech/dms/pkg/dms-common/api/dms/v1/proxy.go @@ -2,10 +2,8 @@ package v1 import base "github.com/actiontech/dms/pkg/dms-common/api/base/v1" -// swagger:parameters RegisterDMSProxyTarget +// swagger:model type RegisterDMSProxyTargetReq struct { - // register dms proxy - // in:body DMSProxyTarget *DMSProxyTarget `json:"dms_proxy_target" validate:"required"` } diff --git a/vendor/github.com/actiontech/dms/pkg/dms-common/api/dms/v1/user.go b/vendor/github.com/actiontech/dms/pkg/dms-common/api/dms/v1/user.go index c8670ee92d..64603c31cf 100644 --- a/vendor/github.com/actiontech/dms/pkg/dms-common/api/dms/v1/user.go +++ b/vendor/github.com/actiontech/dms/pkg/dms-common/api/dms/v1/user.go @@ -92,9 +92,13 @@ type GetUserOpPermissionReq struct { // user uid // in:path UserUid string `param:"user_uid" json:"user_uid" validate:"required"` + + // in:query + // uesr project uid + ProjectUid string `json:"project_uid" query:"project_uid"` // user op permission info // in:body - UserOpPermission *UserOpPermission `json:"user_op_permission" validate:"required"` + UserOpPermission *UserOpPermission `json:"user_op_permission" ` } type UserOpPermission struct { @@ -183,6 +187,8 @@ const ( OpPermissionTypeCreateOptimization OpPermissionType = "create_optimization" // 查看他人创建的智能调优 OpPermissionTypeViewOthersOptimization OpPermissionType = "view_others_optimization" + // 配置流水线 + OpPermissionTypeCreatePipeline OpPermissionType = "create_pipeline" ) func ParseOpPermissionType(typ string) (OpPermissionType, error) { @@ -215,6 +221,8 @@ func ParseOpPermissionType(typ string) (OpPermissionType, error) { return OpPermissionTypeCreateOptimization, nil case string(OpPermissionTypeViewOthersOptimization): return OpPermissionTypeViewOthersOptimization, nil + case string(OpPermissionTypeCreatePipeline): + return OpPermissionTypeCreatePipeline, nil default: return "", fmt.Errorf("invalid op permission type: %s", typ) } @@ -248,6 +256,8 @@ func GetOperationTypeDesc(opType OpPermissionType) string { return "创建智能调优" case OpPermissionTypeViewOthersOptimization: return "查看他人创建的智能调优" + case OpPermissionTypeCreatePipeline: + return "配置流水线" default: return "未知操作类型" } @@ -319,7 +329,7 @@ type ListUserReply struct { base.GenericResp } -// swagger:parameters GenAccessToken +// swagger:model type GenAccessToken struct { ExpirationDays string `param:"expiration_days" json:"expiration_days" validate:"required"` } diff --git a/vendor/github.com/actiontech/dms/pkg/dms-common/api/dms/v1/webhooks.go b/vendor/github.com/actiontech/dms/pkg/dms-common/api/dms/v1/webhooks.go index 99bbd2ac5c..9da77bd997 100644 --- a/vendor/github.com/actiontech/dms/pkg/dms-common/api/dms/v1/webhooks.go +++ b/vendor/github.com/actiontech/dms/pkg/dms-common/api/dms/v1/webhooks.go @@ -9,10 +9,8 @@ const ( TriggerEventAuditPlan TriggerEventType = "auditplan" ) -// swagger:parameters WebHookSendMessage +// swagger:model type WebHookSendMessageReq struct { - // webhooks - // in:body WebHookMessage *WebHooksMessage `json:"webhook_message" validate:"required"` } diff --git a/vendor/github.com/ghodss/yaml/.travis.yml b/vendor/github.com/ghodss/yaml/.travis.yml deleted file mode 100644 index 98ad417e22..0000000000 --- a/vendor/github.com/ghodss/yaml/.travis.yml +++ /dev/null @@ -1,8 +0,0 @@ -language: go -go: - - "1.9" - - "1.10" - - "1.11" -script: - - go test - - go build diff --git a/vendor/github.com/go-openapi/jsonpointer/.travis.yml b/vendor/github.com/go-openapi/jsonpointer/.travis.yml index 9aef9184e8..03a22fe06f 100644 --- a/vendor/github.com/go-openapi/jsonpointer/.travis.yml +++ b/vendor/github.com/go-openapi/jsonpointer/.travis.yml @@ -1,8 +1,8 @@ after_success: - bash <(curl -s https://codecov.io/bash) go: -- 1.11.x -- 1.12.x +- 1.14.x +- 1.15.x install: - GO111MODULE=off go get -u gotest.tools/gotestsum env: diff --git a/vendor/github.com/go-openapi/jsonpointer/pointer.go b/vendor/github.com/go-openapi/jsonpointer/pointer.go index b284eb77a6..7df9853def 100644 --- a/vendor/github.com/go-openapi/jsonpointer/pointer.go +++ b/vendor/github.com/go-openapi/jsonpointer/pointer.go @@ -114,16 +114,16 @@ func getSingleImpl(node interface{}, decodedToken string, nameProvider *swag.Nam rValue := reflect.Indirect(reflect.ValueOf(node)) kind := rValue.Kind() - switch kind { + if rValue.Type().Implements(jsonPointableType) { + r, err := node.(JSONPointable).JSONLookup(decodedToken) + if err != nil { + return nil, kind, err + } + return r, kind, nil + } + switch kind { case reflect.Struct: - if rValue.Type().Implements(jsonPointableType) { - r, err := node.(JSONPointable).JSONLookup(decodedToken) - if err != nil { - return nil, kind, err - } - return r, kind, nil - } nm, ok := nameProvider.GetGoNameForType(rValue.Type(), decodedToken) if !ok { return nil, kind, fmt.Errorf("object has no field %q", decodedToken) @@ -161,17 +161,17 @@ func getSingleImpl(node interface{}, decodedToken string, nameProvider *swag.Nam func setSingleImpl(node, data interface{}, decodedToken string, nameProvider *swag.NameProvider) error { rValue := reflect.Indirect(reflect.ValueOf(node)) - switch rValue.Kind() { - case reflect.Struct: - if ns, ok := node.(JSONSetable); ok { // pointer impl - return ns.JSONSet(decodedToken, data) - } + if ns, ok := node.(JSONSetable); ok { // pointer impl + return ns.JSONSet(decodedToken, data) + } - if rValue.Type().Implements(jsonSetableType) { - return node.(JSONSetable).JSONSet(decodedToken, data) - } + if rValue.Type().Implements(jsonSetableType) { + return node.(JSONSetable).JSONSet(decodedToken, data) + } + switch rValue.Kind() { + case reflect.Struct: nm, ok := nameProvider.GetGoNameForType(rValue.Type(), decodedToken) if !ok { return fmt.Errorf("object has no field %q", decodedToken) @@ -270,22 +270,22 @@ func (p *Pointer) set(node, data interface{}, nameProvider *swag.NameProvider) e rValue := reflect.Indirect(reflect.ValueOf(node)) kind := rValue.Kind() - switch kind { - - case reflect.Struct: - if rValue.Type().Implements(jsonPointableType) { - r, err := node.(JSONPointable).JSONLookup(decodedToken) - if err != nil { - return err - } - fld := reflect.ValueOf(r) - if fld.CanAddr() && fld.Kind() != reflect.Interface && fld.Kind() != reflect.Map && fld.Kind() != reflect.Slice && fld.Kind() != reflect.Ptr { - node = fld.Addr().Interface() - continue - } - node = r + if rValue.Type().Implements(jsonPointableType) { + r, err := node.(JSONPointable).JSONLookup(decodedToken) + if err != nil { + return err + } + fld := reflect.ValueOf(r) + if fld.CanAddr() && fld.Kind() != reflect.Interface && fld.Kind() != reflect.Map && fld.Kind() != reflect.Slice && fld.Kind() != reflect.Ptr { + node = fld.Addr().Interface() continue } + node = r + continue + } + + switch kind { + case reflect.Struct: nm, ok := nameProvider.GetGoNameForType(rValue.Type(), decodedToken) if !ok { return fmt.Errorf("object has no field %q", decodedToken) diff --git a/vendor/github.com/go-openapi/jsonreference/.golangci.yml b/vendor/github.com/go-openapi/jsonreference/.golangci.yml new file mode 100644 index 0000000000..f9381aee54 --- /dev/null +++ b/vendor/github.com/go-openapi/jsonreference/.golangci.yml @@ -0,0 +1,41 @@ +linters-settings: + govet: + check-shadowing: true + golint: + min-confidence: 0 + gocyclo: + min-complexity: 30 + maligned: + suggest-new: true + dupl: + threshold: 100 + goconst: + min-len: 2 + min-occurrences: 4 +linters: + enable-all: true + disable: + - maligned + - lll + - gochecknoglobals + - godox + - gocognit + - whitespace + - wsl + - funlen + - gochecknoglobals + - gochecknoinits + - scopelint + - wrapcheck + - exhaustivestruct + - exhaustive + - nlreturn + - testpackage + - gci + - gofumpt + - goerr113 + - gomnd + - tparallel + - nestif + - godot + - errorlint diff --git a/vendor/github.com/go-openapi/jsonreference/.travis.yml b/vendor/github.com/go-openapi/jsonreference/.travis.yml index 40b90757d8..05482f4b90 100644 --- a/vendor/github.com/go-openapi/jsonreference/.travis.yml +++ b/vendor/github.com/go-openapi/jsonreference/.travis.yml @@ -1,10 +1,19 @@ after_success: - bash <(curl -s https://codecov.io/bash) go: -- 1.11.x -- 1.12.x +- 1.14.x +- 1.x install: -- GO111MODULE=off go get -u gotest.tools/gotestsum +- go get gotest.tools/gotestsum +jobs: + include: + # include linting job, but only for latest go version and amd64 arch + - go: 1.x + arch: amd64 + install: + go get github.com/golangci/golangci-lint/cmd/golangci-lint + script: + - golangci-lint run --new-from-rev master env: - GO111MODULE=on language: go diff --git a/vendor/github.com/go-openapi/spec/.golangci.yml b/vendor/github.com/go-openapi/spec/.golangci.yml index 4e17ed4979..835d55e742 100644 --- a/vendor/github.com/go-openapi/spec/.golangci.yml +++ b/vendor/github.com/go-openapi/spec/.golangci.yml @@ -26,3 +26,17 @@ linters: - gocognit - whitespace - wsl + - wrapcheck + - testpackage + - nlreturn + - gomnd + - exhaustivestruct + - goerr113 + - errorlint + - nestif + - godot + - gofumpt + - paralleltest + - tparallel + - thelper + - ifshort diff --git a/vendor/github.com/go-openapi/spec/.travis.yml b/vendor/github.com/go-openapi/spec/.travis.yml index f1a3f80b35..2281a07b05 100644 --- a/vendor/github.com/go-openapi/spec/.travis.yml +++ b/vendor/github.com/go-openapi/spec/.travis.yml @@ -1,8 +1,26 @@ after_success: - bash <(curl -s https://codecov.io/bash) go: -- 1.13.x -- 1.14.x +- 1.16.x +- 1.x +arch: + - amd64 +jobs: + include: + # only run fast tests on ppc64le + - go: 1.x + arch: ppc64le + script: + - gotestsum -f short-verbose -- ./... + + # include linting job, but only for latest go version and amd64 arch + - go: 1.x + arch: amd64 + install: + go get github.com/golangci/golangci-lint/cmd/golangci-lint + script: + - golangci-lint run --new-from-rev master + install: - GO111MODULE=off go get -u gotest.tools/gotestsum language: go diff --git a/vendor/github.com/go-openapi/spec/README.md b/vendor/github.com/go-openapi/spec/README.md index 6354742cbf..18782c6daf 100644 --- a/vendor/github.com/go-openapi/spec/README.md +++ b/vendor/github.com/go-openapi/spec/README.md @@ -1,10 +1,34 @@ -# OAI object model [![Build Status](https://travis-ci.org/go-openapi/spec.svg?branch=master)](https://travis-ci.org/go-openapi/spec) [![codecov](https://codecov.io/gh/go-openapi/spec/branch/master/graph/badge.svg)](https://codecov.io/gh/go-openapi/spec) [![Slack Status](https://slackin.goswagger.io/badge.svg)](https://slackin.goswagger.io) +# OAI object model +[![Build Status](https://travis-ci.org/go-openapi/spec.svg?branch=master)](https://travis-ci.org/go-openapi/spec) + +[![codecov](https://codecov.io/gh/go-openapi/spec/branch/master/graph/badge.svg)](https://codecov.io/gh/go-openapi/spec) +[![Slack Status](https://slackin.goswagger.io/badge.svg)](https://slackin.goswagger.io) [![license](http://img.shields.io/badge/license-Apache%20v2-orange.svg)](https://raw.githubusercontent.com/go-openapi/spec/master/LICENSE) -[![GoDoc](https://godoc.org/github.com/go-openapi/spec?status.svg)](http://godoc.org/github.com/go-openapi/spec) -[![GolangCI](https://golangci.com/badges/github.com/go-openapi/spec.svg)](https://golangci.com) +[![Go Reference](https://pkg.go.dev/badge/github.com/go-openapi/spec.svg)](https://pkg.go.dev/github.com/go-openapi/spec) [![Go Report Card](https://goreportcard.com/badge/github.com/go-openapi/spec)](https://goreportcard.com/report/github.com/go-openapi/spec) The object model for OpenAPI specification documents. -Currently supports Swagger 2.0. +### FAQ + +* What does this do? + +> 1. This package knows how to marshal and unmarshal Swagger API specifications into a golang object model +> 2. It knows how to resolve $ref and expand them to make a single root document + +* How does it play with the rest of the go-openapi packages ? + +> 1. This package is at the core of the go-openapi suite of packages and [code generator](https://github.com/go-swagger/go-swagger) +> 2. There is a [spec loading package](https://github.com/go-openapi/loads) to fetch specs as JSON or YAML from local or remote locations +> 3. There is a [spec validation package](https://github.com/go-openapi/validate) built on top of it +> 4. There is a [spec analysis package](https://github.com/go-openapi/analysis) built on top of it, to analyze, flatten, fix and merge spec documents + +* Does this library support OpenAPI 3? + +> No. +> This package currently only supports OpenAPI 2.0 (aka Swagger 2.0). +> There is no plan to make it evolve toward supporting OpenAPI 3.x. +> This [discussion thread](https://github.com/go-openapi/spec/issues/21) relates the full story. +> +> An early attempt to support Swagger 3 may be found at: https://github.com/go-openapi/spec3 diff --git a/vendor/github.com/go-openapi/spec/appveyor.yml b/vendor/github.com/go-openapi/spec/appveyor.yml new file mode 100644 index 0000000000..0903593916 --- /dev/null +++ b/vendor/github.com/go-openapi/spec/appveyor.yml @@ -0,0 +1,32 @@ +version: "0.1.{build}" + +clone_folder: C:\go-openapi\spec +shallow_clone: true # for startup speed +pull_requests: + do_not_increment_build_number: true + +#skip_tags: true +#skip_branch_with_pr: true + +# appveyor.yml +build: off + +environment: + GOPATH: c:\gopath + +stack: go 1.15 + +test_script: + - go test -v -timeout 20m ./... + +deploy: off + +notifications: + - provider: Slack + incoming_webhook: https://hooks.slack.com/services/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 + auth_token: + secure: Sf7kZf7ZGbnwWUMpffHwMu5A0cHkLK2MYY32LNTPj4+/3qC3Ghl7+9v4TSLOqOlCwdRNjOGblAq7s+GDJed6/xgRQl1JtCi1klzZNrYX4q01pgTPvvGcwbBkIYgeMaPeIRcK9OZnud7sRXdttozgTOpytps2U6Js32ip7uj5mHSg2ub0FwoSJwlS6dbezZ8+eDhoha0F/guY99BEwx8Bd+zROrT2TFGsSGOFGN6wFc7moCqTHO/YkWib13a2QNXqOxCCVBy/lt76Wp+JkeFppjHlzs/2lP3EAk13RIUAaesdEUHvIHrzCyNJEd3/+KO2DzsWOYfpktd+KBCvgaYOsoo7ubdT3IROeAegZdCgo/6xgCEsmFc9ZcqCfN5yNx2A+BZ2Vwmpws+bQ1E1+B5HDzzaiLcYfG4X2O210QVGVDLWsv1jqD+uPYeHY2WRfh5ZsIUFvaqgUEnwHwrK44/8REAhQavt1QAj5uJpsRd7CkRVPWRNK+yIky+wgbVUFEchRNmS55E7QWf+W4+4QZkQi7vUTMc9nbTUu2Es9NfvfudOpM2wZbn98fjpb/qq/nRv6Bk+ca+7XD5/IgNLMbWp2ouDdzbiHLCOfDUiHiDJhLfFZx9Bwo7ZwfzeOlbrQX66bx7xRKYmOe4DLrXhNcpbsMa8qbfxlZRCmYbubB/Y8h4= + channel: bots + on_build_success: false + on_build_failure: true + on_build_status_changed: true diff --git a/vendor/github.com/go-openapi/spec/bindata.go b/vendor/github.com/go-openapi/spec/bindata.go index 66b1f32635..afc83850c2 100644 --- a/vendor/github.com/go-openapi/spec/bindata.go +++ b/vendor/github.com/go-openapi/spec/bindata.go @@ -247,9 +247,9 @@ type bintree struct { } var _bintree = &bintree{nil, map[string]*bintree{ - "jsonschema-draft-04.json": &bintree{jsonschemaDraft04Json, map[string]*bintree{}}, - "v2": &bintree{nil, map[string]*bintree{ - "schema.json": &bintree{v2SchemaJson, map[string]*bintree{}}, + "jsonschema-draft-04.json": {jsonschemaDraft04Json, map[string]*bintree{}}, + "v2": {nil, map[string]*bintree{ + "schema.json": {v2SchemaJson, map[string]*bintree{}}, }}, }} diff --git a/vendor/github.com/go-openapi/spec/cache.go b/vendor/github.com/go-openapi/spec/cache.go index 3fada0daef..122993b44b 100644 --- a/vendor/github.com/go-openapi/spec/cache.go +++ b/vendor/github.com/go-openapi/spec/cache.go @@ -14,7 +14,9 @@ package spec -import "sync" +import ( + "sync" +) // ResolutionCache a cache for resolving urls type ResolutionCache interface { @@ -27,12 +29,23 @@ type simpleCache struct { store map[string]interface{} } +func (s *simpleCache) ShallowClone() ResolutionCache { + store := make(map[string]interface{}, len(s.store)) + s.lock.RLock() + for k, v := range s.store { + store[k] = v + } + s.lock.RUnlock() + + return &simpleCache{ + store: store, + } +} + // Get retrieves a cached URI func (s *simpleCache) Get(uri string) (interface{}, bool) { - debugLog("getting %q from resolution cache", uri) s.lock.RLock() v, ok := s.store[uri] - debugLog("got %q from resolution cache: %t", uri, ok) s.lock.RUnlock() return v, ok @@ -45,16 +58,41 @@ func (s *simpleCache) Set(uri string, data interface{}) { s.lock.Unlock() } -var resCache ResolutionCache +var ( + // resCache is a package level cache for $ref resolution and expansion. + // It is initialized lazily by methods that have the need for it: no + // memory is allocated unless some expander methods are called. + // + // It is initialized with JSON schema and swagger schema, + // which do not mutate during normal operations. + // + // All subsequent utilizations of this cache are produced from a shallow + // clone of this initial version. + resCache *simpleCache + onceCache sync.Once -func init() { - resCache = initResolutionCache() + _ ResolutionCache = &simpleCache{} +) + +// initResolutionCache initializes the URI resolution cache. To be wrapped in a sync.Once.Do call. +func initResolutionCache() { + resCache = defaultResolutionCache() } -// initResolutionCache initializes the URI resolution cache -func initResolutionCache() ResolutionCache { +func defaultResolutionCache() *simpleCache { return &simpleCache{store: map[string]interface{}{ "http://swagger.io/v2/schema.json": MustLoadSwagger20Schema(), "http://json-schema.org/draft-04/schema": MustLoadJSONSchemaDraft04(), }} } + +func cacheOrDefault(cache ResolutionCache) ResolutionCache { + onceCache.Do(initResolutionCache) + + if cache != nil { + return cache + } + + // get a shallow clone of the base cache with swagger and json schema + return resCache.ShallowClone() +} diff --git a/vendor/github.com/go-openapi/spec/contact_info.go b/vendor/github.com/go-openapi/spec/contact_info.go index f9bf42e8dd..2f7bb219b5 100644 --- a/vendor/github.com/go-openapi/spec/contact_info.go +++ b/vendor/github.com/go-openapi/spec/contact_info.go @@ -28,12 +28,14 @@ type ContactInfo struct { VendorExtensible } +// ContactInfoProps hold the properties of a ContactInfo object type ContactInfoProps struct { Name string `json:"name,omitempty"` URL string `json:"url,omitempty"` Email string `json:"email,omitempty"` } +// UnmarshalJSON hydrates ContactInfo from json func (c *ContactInfo) UnmarshalJSON(data []byte) error { if err := json.Unmarshal(data, &c.ContactInfoProps); err != nil { return err @@ -41,6 +43,7 @@ func (c *ContactInfo) UnmarshalJSON(data []byte) error { return json.Unmarshal(data, &c.VendorExtensible) } +// MarshalJSON produces ContactInfo as json func (c ContactInfo) MarshalJSON() ([]byte, error) { b1, err := json.Marshal(c.ContactInfoProps) if err != nil { diff --git a/vendor/github.com/go-openapi/spec/debug.go b/vendor/github.com/go-openapi/spec/debug.go index 389c528ff6..fc889f6d0b 100644 --- a/vendor/github.com/go-openapi/spec/debug.go +++ b/vendor/github.com/go-openapi/spec/debug.go @@ -18,14 +18,16 @@ import ( "fmt" "log" "os" - "path/filepath" + "path" "runtime" ) +// Debug is true when the SWAGGER_DEBUG env var is not empty. +// +// It enables a more verbose logging of this package. +var Debug = os.Getenv("SWAGGER_DEBUG") != "" + var ( - // Debug is true when the SWAGGER_DEBUG env var is not empty. - // It enables a more verbose logging of this package. - Debug = os.Getenv("SWAGGER_DEBUG") != "" // specLogger is a debug logger for this package specLogger *log.Logger ) @@ -42,6 +44,6 @@ func debugLog(msg string, args ...interface{}) { // A private, trivial trace logger, based on go-openapi/spec/expander.go:debugLog() if Debug { _, file1, pos1, _ := runtime.Caller(1) - specLogger.Printf("%s:%d: %s", filepath.Base(file1), pos1, fmt.Sprintf(msg, args...)) + specLogger.Printf("%s:%d: %s", path.Base(file1), pos1, fmt.Sprintf(msg, args...)) } } diff --git a/vendor/github.com/go-openapi/spec/errors.go b/vendor/github.com/go-openapi/spec/errors.go new file mode 100644 index 0000000000..6992c7ba73 --- /dev/null +++ b/vendor/github.com/go-openapi/spec/errors.go @@ -0,0 +1,19 @@ +package spec + +import "errors" + +// Error codes +var ( + // ErrUnknownTypeForReference indicates that a resolved reference was found in an unsupported container type + ErrUnknownTypeForReference = errors.New("unknown type for the resolved reference") + + // ErrResolveRefNeedsAPointer indicates that a $ref target must be a valid JSON pointer + ErrResolveRefNeedsAPointer = errors.New("resolve ref: target needs to be a pointer") + + // ErrDerefUnsupportedType indicates that a resolved reference was found in an unsupported container type. + // At the moment, $ref are supported only inside: schemas, parameters, responses, path items + ErrDerefUnsupportedType = errors.New("deref: unsupported type") + + // ErrExpandUnsupportedType indicates that $ref expansion is attempted on some invalid type + ErrExpandUnsupportedType = errors.New("expand: unsupported type. Input should be of type *Parameter or *Response") +) diff --git a/vendor/github.com/go-openapi/spec/expander.go b/vendor/github.com/go-openapi/spec/expander.go index 1f30e7f8b1..d4ea889d44 100644 --- a/vendor/github.com/go-openapi/spec/expander.go +++ b/vendor/github.com/go-openapi/spec/expander.go @@ -17,152 +17,51 @@ package spec import ( "encoding/json" "fmt" - "strings" ) -// ExpandOptions provides options for spec expand -type ExpandOptions struct { - RelativeBase string - SkipSchemas bool - ContinueOnError bool - AbsoluteCircularRef bool -} - -// ResolveRefWithBase resolves a reference against a context root with preservation of base path -func ResolveRefWithBase(root interface{}, ref *Ref, opts *ExpandOptions) (*Schema, error) { - resolver, err := defaultSchemaLoader(root, opts, nil, nil) - if err != nil { - return nil, err - } - specBasePath := "" - if opts != nil && opts.RelativeBase != "" { - specBasePath, _ = absPath(opts.RelativeBase) - } - - result := new(Schema) - if err := resolver.Resolve(ref, result, specBasePath); err != nil { - return nil, err - } - return result, nil -} - -// ResolveRef resolves a reference against a context root -// ref is guaranteed to be in root (no need to go to external files) -// ResolveRef is ONLY called from the code generation module -func ResolveRef(root interface{}, ref *Ref) (*Schema, error) { - res, _, err := ref.GetPointer().Get(root) - if err != nil { - panic(err) - } - switch sch := res.(type) { - case Schema: - return &sch, nil - case *Schema: - return sch, nil - case map[string]interface{}: - b, _ := json.Marshal(sch) - newSch := new(Schema) - _ = json.Unmarshal(b, newSch) - return newSch, nil - default: - return nil, fmt.Errorf("unknown type for the resolved reference") - } -} - -// ResolveParameter resolves a parameter reference against a context root -func ResolveParameter(root interface{}, ref Ref) (*Parameter, error) { - return ResolveParameterWithBase(root, ref, nil) -} - -// ResolveParameterWithBase resolves a parameter reference against a context root and base path -func ResolveParameterWithBase(root interface{}, ref Ref, opts *ExpandOptions) (*Parameter, error) { - resolver, err := defaultSchemaLoader(root, opts, nil, nil) - if err != nil { - return nil, err - } - - result := new(Parameter) - if err := resolver.Resolve(&ref, result, ""); err != nil { - return nil, err - } - return result, nil -} - -// ResolveResponse resolves response a reference against a context root -func ResolveResponse(root interface{}, ref Ref) (*Response, error) { - return ResolveResponseWithBase(root, ref, nil) -} - -// ResolveResponseWithBase resolves response a reference against a context root and base path -func ResolveResponseWithBase(root interface{}, ref Ref, opts *ExpandOptions) (*Response, error) { - resolver, err := defaultSchemaLoader(root, opts, nil, nil) - if err != nil { - return nil, err - } - - result := new(Response) - if err := resolver.Resolve(&ref, result, ""); err != nil { - return nil, err - } - return result, nil -} - -// ResolveItems resolves parameter items reference against a context root and base path. +// ExpandOptions provides options for the spec expander. // -// NOTE: stricly speaking, this construct is not supported by Swagger 2.0. -// Similarly, $ref are forbidden in response headers. -func ResolveItems(root interface{}, ref Ref, opts *ExpandOptions) (*Items, error) { - resolver, err := defaultSchemaLoader(root, opts, nil, nil) - if err != nil { - return nil, err - } - basePath := "" - if opts.RelativeBase != "" { - basePath = opts.RelativeBase - } - result := new(Items) - if err := resolver.Resolve(&ref, result, basePath); err != nil { - return nil, err - } - return result, nil +// RelativeBase is the path to the root document. This can be a remote URL or a path to a local file. +// +// If left empty, the root document is assumed to be located in the current working directory: +// all relative $ref's will be resolved from there. +// +// PathLoader injects a document loading method. By default, this resolves to the function provided by the SpecLoader package variable. +// +type ExpandOptions struct { + RelativeBase string // the path to the root document to expand. This is a file, not a directory + SkipSchemas bool // do not expand schemas, just paths, parameters and responses + ContinueOnError bool // continue expanding even after and error is found + PathLoader func(string) (json.RawMessage, error) `json:"-"` // the document loading method that takes a path as input and yields a json document + AbsoluteCircularRef bool // circular $ref remaining after expansion remain absolute URLs } -// ResolvePathItem resolves response a path item against a context root and base path -func ResolvePathItem(root interface{}, ref Ref, opts *ExpandOptions) (*PathItem, error) { - resolver, err := defaultSchemaLoader(root, opts, nil, nil) - if err != nil { - return nil, err - } - basePath := "" - if opts.RelativeBase != "" { - basePath = opts.RelativeBase - } - result := new(PathItem) - if err := resolver.Resolve(&ref, result, basePath); err != nil { - return nil, err +func optionsOrDefault(opts *ExpandOptions) *ExpandOptions { + if opts != nil { + clone := *opts // shallow clone to avoid internal changes to be propagated to the caller + if clone.RelativeBase != "" { + clone.RelativeBase = normalizeBase(clone.RelativeBase) + } + // if the relative base is empty, let the schema loader choose a pseudo root document + return &clone } - return result, nil + return &ExpandOptions{} } // ExpandSpec expands the references in a swagger spec func ExpandSpec(spec *Swagger, options *ExpandOptions) error { - resolver, err := defaultSchemaLoader(spec, options, nil, nil) - // Just in case this ever returns an error. - if resolver.shouldStopOnError(err) { - return err - } + options = optionsOrDefault(options) + resolver := defaultSchemaLoader(spec, options, nil, nil) - // getting the base path of the spec to adjust all subsequent reference resolutions - specBasePath := "" - if options != nil && options.RelativeBase != "" { - specBasePath, _ = absPath(options.RelativeBase) - } + specBasePath := options.RelativeBase - if options == nil || !options.SkipSchemas { + if !options.SkipSchemas { for key, definition := range spec.Definitions { - var def *Schema - var err error - if def, err = expandSchema(definition, []string{fmt.Sprintf("#/definitions/%s", key)}, resolver, specBasePath); resolver.shouldStopOnError(err) { + parentRefs := make([]string, 0, 10) + parentRefs = append(parentRefs, fmt.Sprintf("#/definitions/%s", key)) + + def, err := expandSchema(definition, parentRefs, resolver, specBasePath) + if resolver.shouldStopOnError(err) { return err } if def != nil { @@ -189,157 +88,136 @@ func ExpandSpec(spec *Swagger, options *ExpandOptions) error { if spec.Paths != nil { for key := range spec.Paths.Paths { - path := spec.Paths.Paths[key] - if err := expandPathItem(&path, resolver, specBasePath); resolver.shouldStopOnError(err) { + pth := spec.Paths.Paths[key] + if err := expandPathItem(&pth, resolver, specBasePath); resolver.shouldStopOnError(err) { return err } - spec.Paths.Paths[key] = path + spec.Paths.Paths[key] = pth } } return nil } -const rootBase = "root" -// baseForRoot loads in the cache the root document and produces a fake "root" base path entry +const rootBase = ".root" + +// baseForRoot loads in the cache the root document and produces a fake ".root" base path entry // for further $ref resolution +// +// Setting the cache is optional and this parameter may safely be left to nil. func baseForRoot(root interface{}, cache ResolutionCache) string { - // cache the root document to resolve $ref's - if root != nil { - base, _ := absPath(rootBase) - normalizedBase := normalizeAbsPath(base) - debugLog("setting root doc in cache at: %s", normalizedBase) - if cache == nil { - cache = resCache - } - cache.Set(normalizedBase, root) - return rootBase + if root == nil { + return "" } - return "" + + // cache the root document to resolve $ref's + normalizedBase := normalizeBase(rootBase) + cache.Set(normalizedBase, root) + + return normalizedBase } -// ExpandSchema expands the refs in the schema object with reference to the root object -// go-openapi/validate uses this function -// notice that it is impossible to reference a json schema in a different file other than root +// ExpandSchema expands the refs in the schema object with reference to the root object. +// +// go-openapi/validate uses this function. +// +// Notice that it is impossible to reference a json schema in a different document other than root +// (use ExpandSchemaWithBasePath to resolve external references). +// +// Setting the cache is optional and this parameter may safely be left to nil. func ExpandSchema(schema *Schema, root interface{}, cache ResolutionCache) error { + cache = cacheOrDefault(cache) + if root == nil { + root = schema + } + opts := &ExpandOptions{ // when a root is specified, cache the root as an in-memory document for $ref retrieval RelativeBase: baseForRoot(root, cache), SkipSchemas: false, ContinueOnError: false, - // when no base path is specified, remaining $ref (circular) are rendered with an absolute path - AbsoluteCircularRef: true, } + return ExpandSchemaWithBasePath(schema, cache, opts) } -// ExpandSchemaWithBasePath expands the refs in the schema object, base path configured through expand options +// ExpandSchemaWithBasePath expands the refs in the schema object, base path configured through expand options. +// +// Setting the cache is optional and this parameter may safely be left to nil. func ExpandSchemaWithBasePath(schema *Schema, cache ResolutionCache, opts *ExpandOptions) error { if schema == nil { return nil } - var basePath string - if opts.RelativeBase != "" { - basePath, _ = absPath(opts.RelativeBase) - } + cache = cacheOrDefault(cache) + + opts = optionsOrDefault(opts) - resolver, err := defaultSchemaLoader(nil, opts, cache, nil) + resolver := defaultSchemaLoader(nil, opts, cache, nil) + + parentRefs := make([]string, 0, 10) + s, err := expandSchema(*schema, parentRefs, resolver, opts.RelativeBase) if err != nil { return err } - - refs := []string{""} - var s *Schema - if s, err = expandSchema(*schema, refs, resolver, basePath); err != nil { - return err + if s != nil { + // guard for when continuing on error + *schema = *s } - *schema = *s + return nil } func expandItems(target Schema, parentRefs []string, resolver *schemaLoader, basePath string) (*Schema, error) { - if target.Items != nil { - if target.Items.Schema != nil { - t, err := expandSchema(*target.Items.Schema, parentRefs, resolver, basePath) - if err != nil { - return nil, err - } - *target.Items.Schema = *t + if target.Items == nil { + return &target, nil + } + + // array + if target.Items.Schema != nil { + t, err := expandSchema(*target.Items.Schema, parentRefs, resolver, basePath) + if err != nil { + return nil, err } - for i := range target.Items.Schemas { - t, err := expandSchema(target.Items.Schemas[i], parentRefs, resolver, basePath) - if err != nil { - return nil, err - } - target.Items.Schemas[i] = *t + *target.Items.Schema = *t + } + + // tuple + for i := range target.Items.Schemas { + t, err := expandSchema(target.Items.Schemas[i], parentRefs, resolver, basePath) + if err != nil { + return nil, err } + target.Items.Schemas[i] = *t } + return &target, nil } func expandSchema(target Schema, parentRefs []string, resolver *schemaLoader, basePath string) (*Schema, error) { if target.Ref.String() == "" && target.Ref.IsRoot() { - // normalizing is important - newRef := normalizeFileRef(&target.Ref, basePath) + newRef := normalizeRef(&target.Ref, basePath) target.Ref = *newRef return &target, nil - } // change the base path of resolution when an ID is encountered // otherwise the basePath should inherit the parent's - // important: ID can be relative path if target.ID != "" { - debugLog("schema has ID: %s", target.ID) - // handling the case when id is a folder - // remember that basePath has to be a file - refPath := target.ID - if strings.HasSuffix(target.ID, "/") { - // path.Clean here would not work correctly if basepath is http - refPath = fmt.Sprintf("%s%s", refPath, "placeholder.json") - } - basePath = normalizePaths(refPath, basePath) + basePath, _ = resolver.setSchemaID(target, target.ID, basePath) } - var t *Schema - // if Ref is found, everything else doesn't matter - // Ref also changes the resolution scope of children expandSchema if target.Ref.String() != "" { - // here the resolution scope is changed because a $ref was encountered - normalizedRef := normalizeFileRef(&target.Ref, basePath) - normalizedBasePath := normalizedRef.RemoteURI() - - if resolver.isCircular(normalizedRef, basePath, parentRefs...) { - // this means there is a cycle in the recursion tree: return the Ref - // - circular refs cannot be expanded. We leave them as ref. - // - denormalization means that a new local file ref is set relative to the original basePath - debugLog("shortcut circular ref: basePath: %s, normalizedPath: %s, normalized ref: %s", - basePath, normalizedBasePath, normalizedRef.String()) - if !resolver.options.AbsoluteCircularRef { - target.Ref = *denormalizeFileRef(normalizedRef, normalizedBasePath, resolver.context.basePath) - } else { - target.Ref = *normalizedRef - } - return &target, nil - } + return expandSchemaRef(target, parentRefs, resolver, basePath) + } - debugLog("basePath: %s: calling Resolve with target: %#v", basePath, target) - if err := resolver.Resolve(&target.Ref, &t, basePath); resolver.shouldStopOnError(err) { - return nil, err + for k := range target.Definitions { + tt, err := expandSchema(target.Definitions[k], parentRefs, resolver, basePath) + if resolver.shouldStopOnError(err) { + return &target, err } - - if t != nil { - parentRefs = append(parentRefs, normalizedRef.String()) - var err error - transitiveResolver, err := resolver.transitiveResolver(basePath, target.Ref) - if transitiveResolver.shouldStopOnError(err) { - return nil, err - } - - basePath = resolver.updateBasePath(transitiveResolver, normalizedBasePath) - - return expandSchema(*t, parentRefs, transitiveResolver, basePath) + if tt != nil { + target.Definitions[k] = *tt } } @@ -356,15 +234,21 @@ func expandSchema(target Schema, parentRefs []string, resolver *schemaLoader, ba if resolver.shouldStopOnError(err) { return &target, err } - target.AllOf[i] = *t + if t != nil { + target.AllOf[i] = *t + } } + for i := range target.AnyOf { t, err := expandSchema(target.AnyOf[i], parentRefs, resolver, basePath) if resolver.shouldStopOnError(err) { return &target, err } - target.AnyOf[i] = *t + if t != nil { + target.AnyOf[i] = *t + } } + for i := range target.OneOf { t, err := expandSchema(target.OneOf[i], parentRefs, resolver, basePath) if resolver.shouldStopOnError(err) { @@ -374,6 +258,7 @@ func expandSchema(target Schema, parentRefs []string, resolver *schemaLoader, ba target.OneOf[i] = *t } } + if target.Not != nil { t, err := expandSchema(*target.Not, parentRefs, resolver, basePath) if resolver.shouldStopOnError(err) { @@ -383,6 +268,7 @@ func expandSchema(target Schema, parentRefs []string, resolver *schemaLoader, ba *target.Not = *t } } + for k := range target.Properties { t, err := expandSchema(target.Properties[k], parentRefs, resolver, basePath) if resolver.shouldStopOnError(err) { @@ -392,6 +278,7 @@ func expandSchema(target Schema, parentRefs []string, resolver *schemaLoader, ba target.Properties[k] = *t } } + if target.AdditionalProperties != nil && target.AdditionalProperties.Schema != nil { t, err := expandSchema(*target.AdditionalProperties.Schema, parentRefs, resolver, basePath) if resolver.shouldStopOnError(err) { @@ -401,6 +288,7 @@ func expandSchema(target Schema, parentRefs []string, resolver *schemaLoader, ba *target.AdditionalProperties.Schema = *t } } + for k := range target.PatternProperties { t, err := expandSchema(target.PatternProperties[k], parentRefs, resolver, basePath) if resolver.shouldStopOnError(err) { @@ -410,6 +298,7 @@ func expandSchema(target Schema, parentRefs []string, resolver *schemaLoader, ba target.PatternProperties[k] = *t } } + for k := range target.Dependencies { if target.Dependencies[k].Schema != nil { t, err := expandSchema(*target.Dependencies[k].Schema, parentRefs, resolver, basePath) @@ -421,6 +310,7 @@ func expandSchema(target Schema, parentRefs []string, resolver *schemaLoader, ba } } } + if target.AdditionalItems != nil && target.AdditionalItems.Schema != nil { t, err := expandSchema(*target.AdditionalItems.Schema, parentRefs, resolver, basePath) if resolver.shouldStopOnError(err) { @@ -430,16 +320,48 @@ func expandSchema(target Schema, parentRefs []string, resolver *schemaLoader, ba *target.AdditionalItems.Schema = *t } } - for k := range target.Definitions { - t, err := expandSchema(target.Definitions[k], parentRefs, resolver, basePath) - if resolver.shouldStopOnError(err) { - return &target, err - } - if t != nil { - target.Definitions[k] = *t + return &target, nil +} + +func expandSchemaRef(target Schema, parentRefs []string, resolver *schemaLoader, basePath string) (*Schema, error) { + // if a Ref is found, all sibling fields are skipped + // Ref also changes the resolution scope of children expandSchema + + // here the resolution scope is changed because a $ref was encountered + normalizedRef := normalizeRef(&target.Ref, basePath) + normalizedBasePath := normalizedRef.RemoteURI() + + if resolver.isCircular(normalizedRef, basePath, parentRefs...) { + // this means there is a cycle in the recursion tree: return the Ref + // - circular refs cannot be expanded. We leave them as ref. + // - denormalization means that a new local file ref is set relative to the original basePath + debugLog("short circuit circular ref: basePath: %s, normalizedPath: %s, normalized ref: %s", + basePath, normalizedBasePath, normalizedRef.String()) + if !resolver.options.AbsoluteCircularRef { + target.Ref = denormalizeRef(normalizedRef, resolver.context.basePath, resolver.context.rootID) + } else { + target.Ref = *normalizedRef } + return &target, nil } - return &target, nil + + var t *Schema + err := resolver.Resolve(&target.Ref, &t, basePath) + if resolver.shouldStopOnError(err) { + return nil, err + } + + if t == nil { + // guard for when continuing on error + return &target, nil + } + + parentRefs = append(parentRefs, normalizedRef.String()) + transitiveResolver := resolver.transitiveResolver(basePath, target.Ref) + + basePath = resolver.updateBasePath(transitiveResolver, normalizedBasePath) + + return expandSchema(*t, parentRefs, transitiveResolver, basePath) } func expandPathItem(pathItem *PathItem, resolver *schemaLoader, basePath string) error { @@ -447,25 +369,24 @@ func expandPathItem(pathItem *PathItem, resolver *schemaLoader, basePath string) return nil } - parentRefs := []string{} + parentRefs := make([]string, 0, 10) if err := resolver.deref(pathItem, parentRefs, basePath); resolver.shouldStopOnError(err) { return err } + if pathItem.Ref.String() != "" { - transitiveResolver, err := resolver.transitiveResolver(basePath, pathItem.Ref) - if transitiveResolver.shouldStopOnError(err) { - return err - } + transitiveResolver := resolver.transitiveResolver(basePath, pathItem.Ref) basePath = transitiveResolver.updateBasePath(resolver, basePath) resolver = transitiveResolver } - pathItem.Ref = Ref{} - for idx := range pathItem.Parameters { - if err := expandParameterOrResponse(&(pathItem.Parameters[idx]), resolver, basePath); resolver.shouldStopOnError(err) { + pathItem.Ref = Ref{} + for i := range pathItem.Parameters { + if err := expandParameterOrResponse(&(pathItem.Parameters[i]), resolver, basePath); resolver.shouldStopOnError(err) { return err } } + ops := []*Operation{ pathItem.Get, pathItem.Head, @@ -480,6 +401,7 @@ func expandPathItem(pathItem *PathItem, resolver *schemaLoader, basePath string) return err } } + return nil } @@ -496,71 +418,65 @@ func expandOperation(op *Operation, resolver *schemaLoader, basePath string) err op.Parameters[i] = param } - if op.Responses != nil { - responses := op.Responses - if err := expandParameterOrResponse(responses.Default, resolver, basePath); resolver.shouldStopOnError(err) { + if op.Responses == nil { + return nil + } + + responses := op.Responses + if err := expandParameterOrResponse(responses.Default, resolver, basePath); resolver.shouldStopOnError(err) { + return err + } + + for code := range responses.StatusCodeResponses { + response := responses.StatusCodeResponses[code] + if err := expandParameterOrResponse(&response, resolver, basePath); resolver.shouldStopOnError(err) { return err } - for code := range responses.StatusCodeResponses { - response := responses.StatusCodeResponses[code] - if err := expandParameterOrResponse(&response, resolver, basePath); resolver.shouldStopOnError(err) { - return err - } - responses.StatusCodeResponses[code] = response - } + responses.StatusCodeResponses[code] = response } + return nil } // ExpandResponseWithRoot expands a response based on a root document, not a fetchable document +// +// Notice that it is impossible to reference a json schema in a different document other than root +// (use ExpandResponse to resolve external references). +// +// Setting the cache is optional and this parameter may safely be left to nil. func ExpandResponseWithRoot(response *Response, root interface{}, cache ResolutionCache) error { + cache = cacheOrDefault(cache) opts := &ExpandOptions{ - RelativeBase: baseForRoot(root, cache), - SkipSchemas: false, - ContinueOnError: false, - // when no base path is specified, remaining $ref (circular) are rendered with an absolute path - AbsoluteCircularRef: true, - } - resolver, err := defaultSchemaLoader(root, opts, nil, nil) - if err != nil { - return err + RelativeBase: baseForRoot(root, cache), } + resolver := defaultSchemaLoader(root, opts, cache, nil) return expandParameterOrResponse(response, resolver, opts.RelativeBase) } // ExpandResponse expands a response based on a basepath -// This is the exported version of expandResponse -// all refs inside response will be resolved relative to basePath +// +// All refs inside response will be resolved relative to basePath func ExpandResponse(response *Response, basePath string) error { - var specBasePath string - if basePath != "" { - specBasePath, _ = absPath(basePath) - } - opts := &ExpandOptions{ - RelativeBase: specBasePath, - } - resolver, err := defaultSchemaLoader(nil, opts, nil, nil) - if err != nil { - return err - } + opts := optionsOrDefault(&ExpandOptions{ + RelativeBase: basePath, + }) + resolver := defaultSchemaLoader(nil, opts, nil, nil) return expandParameterOrResponse(response, resolver, opts.RelativeBase) } -// ExpandParameterWithRoot expands a parameter based on a root document, not a fetchable document +// ExpandParameterWithRoot expands a parameter based on a root document, not a fetchable document. +// +// Notice that it is impossible to reference a json schema in a different document other than root +// (use ExpandParameter to resolve external references). func ExpandParameterWithRoot(parameter *Parameter, root interface{}, cache ResolutionCache) error { + cache = cacheOrDefault(cache) + opts := &ExpandOptions{ - RelativeBase: baseForRoot(root, cache), - SkipSchemas: false, - ContinueOnError: false, - // when no base path is specified, remaining $ref (circular) are rendered with an absolute path - AbsoluteCircularRef: true, - } - resolver, err := defaultSchemaLoader(root, opts, nil, nil) - if err != nil { - return err + RelativeBase: baseForRoot(root, cache), } + resolver := defaultSchemaLoader(root, opts, cache, nil) return expandParameterOrResponse(parameter, resolver, opts.RelativeBase) } @@ -569,24 +485,20 @@ func ExpandParameterWithRoot(parameter *Parameter, root interface{}, cache Resol // This is the exported version of expandParameter // all refs inside parameter will be resolved relative to basePath func ExpandParameter(parameter *Parameter, basePath string) error { - var specBasePath string - if basePath != "" { - specBasePath, _ = absPath(basePath) - } - opts := &ExpandOptions{ - RelativeBase: specBasePath, - } - resolver, err := defaultSchemaLoader(nil, opts, nil, nil) - if err != nil { - return err - } + opts := optionsOrDefault(&ExpandOptions{ + RelativeBase: basePath, + }) + resolver := defaultSchemaLoader(nil, opts, nil, nil) return expandParameterOrResponse(parameter, resolver, opts.RelativeBase) } func getRefAndSchema(input interface{}) (*Ref, *Schema, error) { - var ref *Ref - var sch *Schema + var ( + ref *Ref + sch *Schema + ) + switch refable := input.(type) { case *Parameter: if refable == nil { @@ -601,8 +513,9 @@ func getRefAndSchema(input interface{}) (*Ref, *Schema, error) { ref = &refable.Ref sch = refable.Schema default: - return nil, nil, fmt.Errorf("expand: unsupported type %T. Input should be of type *Parameter or *Response", input) + return nil, nil, fmt.Errorf("unsupported type: %T: %w", input, ErrExpandUnsupportedType) } + return ref, sch, nil } @@ -611,41 +524,71 @@ func expandParameterOrResponse(input interface{}, resolver *schemaLoader, basePa if err != nil { return err } + if ref == nil { return nil } - parentRefs := []string{} - if err := resolver.deref(input, parentRefs, basePath); resolver.shouldStopOnError(err) { + + parentRefs := make([]string, 0, 10) + if err = resolver.deref(input, parentRefs, basePath); resolver.shouldStopOnError(err) { return err } + ref, sch, _ := getRefAndSchema(input) if ref.String() != "" { - transitiveResolver, err := resolver.transitiveResolver(basePath, *ref) - if transitiveResolver.shouldStopOnError(err) { - return err - } + transitiveResolver := resolver.transitiveResolver(basePath, *ref) basePath = resolver.updateBasePath(transitiveResolver, basePath) resolver = transitiveResolver } - if sch != nil && sch.Ref.String() != "" { - // schema expanded to a $ref in another root - var ern error - sch.Ref, ern = NewRef(normalizePaths(sch.Ref.String(), ref.RemoteURI())) + if sch == nil { + // nothing to be expanded + if ref != nil { + *ref = Ref{} + } + return nil + } + + if sch.Ref.String() != "" { + rebasedRef, ern := NewRef(normalizeURI(sch.Ref.String(), basePath)) if ern != nil { return ern } + + switch { + case resolver.isCircular(&rebasedRef, basePath, parentRefs...): + // this is a circular $ref: stop expansion + if !resolver.options.AbsoluteCircularRef { + sch.Ref = denormalizeRef(&rebasedRef, resolver.context.basePath, resolver.context.rootID) + } else { + sch.Ref = rebasedRef + } + case !resolver.options.SkipSchemas: + // schema expanded to a $ref in another root + sch.Ref = rebasedRef + debugLog("rebased to: %s", sch.Ref.String()) + default: + // skip schema expansion but rebase $ref to schema + sch.Ref = denormalizeRef(&rebasedRef, resolver.context.basePath, resolver.context.rootID) + } } + if ref != nil { *ref = Ref{} } - if !resolver.options.SkipSchemas && sch != nil { + // expand schema + if !resolver.options.SkipSchemas { s, err := expandSchema(*sch, parentRefs, resolver, basePath) if resolver.shouldStopOnError(err) { return err } + if s == nil { + // guard for when continuing on error + return nil + } *sch = *s } + return nil } diff --git a/vendor/github.com/go-openapi/spec/header.go b/vendor/github.com/go-openapi/spec/header.go index 39efe452bb..9dfd17b185 100644 --- a/vendor/github.com/go-openapi/spec/header.go +++ b/vendor/github.com/go-openapi/spec/header.go @@ -141,6 +141,12 @@ func (h *Header) AllowDuplicates() *Header { return h } +// WithValidations is a fluent method to set header validations +func (h *Header) WithValidations(val CommonValidations) *Header { + h.SetValidations(SchemaValidations{CommonValidations: val}) + return h +} + // MarshalJSON marshal this to JSON func (h Header) MarshalJSON() ([]byte, error) { b1, err := json.Marshal(h.CommonValidations) diff --git a/vendor/github.com/go-openapi/spec/items.go b/vendor/github.com/go-openapi/spec/items.go index 365d163158..e2afb2133b 100644 --- a/vendor/github.com/go-openapi/spec/items.go +++ b/vendor/github.com/go-openapi/spec/items.go @@ -53,22 +53,6 @@ func (s *SimpleSchema) ItemsTypeName() string { return s.Items.TypeName() } -// CommonValidations describe common JSON-schema validations -type CommonValidations struct { - Maximum *float64 `json:"maximum,omitempty"` - ExclusiveMaximum bool `json:"exclusiveMaximum,omitempty"` - Minimum *float64 `json:"minimum,omitempty"` - ExclusiveMinimum bool `json:"exclusiveMinimum,omitempty"` - MaxLength *int64 `json:"maxLength,omitempty"` - MinLength *int64 `json:"minLength,omitempty"` - Pattern string `json:"pattern,omitempty"` - MaxItems *int64 `json:"maxItems,omitempty"` - MinItems *int64 `json:"minItems,omitempty"` - UniqueItems bool `json:"uniqueItems,omitempty"` - MultipleOf *float64 `json:"multipleOf,omitempty"` - Enum []interface{} `json:"enum,omitempty"` -} - // Items a limited subset of JSON-Schema's items object. // It is used by parameter definitions that are not located in "body". // @@ -180,6 +164,12 @@ func (i *Items) AllowDuplicates() *Items { return i } +// WithValidations is a fluent method to set Items validations +func (i *Items) WithValidations(val CommonValidations) *Items { + i.SetValidations(SchemaValidations{CommonValidations: val}) + return i +} + // UnmarshalJSON hydrates this items instance with the data from JSON func (i *Items) UnmarshalJSON(data []byte) error { var validations CommonValidations diff --git a/vendor/github.com/go-openapi/spec/license.go b/vendor/github.com/go-openapi/spec/license.go index e1529b401c..b42f80368e 100644 --- a/vendor/github.com/go-openapi/spec/license.go +++ b/vendor/github.com/go-openapi/spec/license.go @@ -28,11 +28,13 @@ type License struct { VendorExtensible } +// LicenseProps holds the properties of a License object type LicenseProps struct { Name string `json:"name,omitempty"` URL string `json:"url,omitempty"` } +// UnmarshalJSON hydrates License from json func (l *License) UnmarshalJSON(data []byte) error { if err := json.Unmarshal(data, &l.LicenseProps); err != nil { return err @@ -40,6 +42,7 @@ func (l *License) UnmarshalJSON(data []byte) error { return json.Unmarshal(data, &l.VendorExtensible) } +// MarshalJSON produces License as json func (l License) MarshalJSON() ([]byte, error) { b1, err := json.Marshal(l.LicenseProps) if err != nil { diff --git a/vendor/github.com/go-openapi/spec/normalizer.go b/vendor/github.com/go-openapi/spec/normalizer.go index b8957e7c0c..d6c4839712 100644 --- a/vendor/github.com/go-openapi/spec/normalizer.go +++ b/vendor/github.com/go-openapi/spec/normalizer.go @@ -15,138 +15,189 @@ package spec import ( - "fmt" "net/url" - "os" "path" - "path/filepath" "strings" ) -// normalize absolute path for cache. -// on Windows, drive letters should be converted to lower as scheme in net/url.URL -func normalizeAbsPath(path string) string { - u, err := url.Parse(path) +const fileScheme = "file" + +// normalizeURI ensures that all $ref paths used internally by the expander are canonicalized. +// +// NOTE(windows): there is a tolerance over the strict URI format on windows. +// +// The normalizer accepts relative file URLs like 'Path\File.JSON' as well as absolute file URLs like +// 'C:\Path\file.Yaml'. +// +// Both are canonicalized with a "file://" scheme, slashes and a lower-cased path: +// 'file:///c:/path/file.yaml' +// +// URLs can be specified with a file scheme, like in 'file:///folder/file.json' or +// 'file:///c:\folder\File.json'. +// +// URLs like file://C:\folder are considered invalid (i.e. there is no host 'c:\folder') and a "repair" +// is attempted. +// +// The base path argument is assumed to be canonicalized (e.g. using normalizeBase()). +func normalizeURI(refPath, base string) string { + refURL, err := url.Parse(refPath) if err != nil { - debugLog("normalize absolute path failed: %s", err) - return path + specLogger.Printf("warning: invalid URI in $ref %q: %v", refPath, err) + refURL, refPath = repairURI(refPath) } - return u.String() -} -// base or refPath could be a file path or a URL -// given a base absolute path and a ref path, return the absolute path of refPath -// 1) if refPath is absolute, return it -// 2) if refPath is relative, join it with basePath keeping the scheme, hosts, and ports if exists -// base could be a directory or a full file path -func normalizePaths(refPath, base string) string { - refURL, _ := url.Parse(refPath) - if path.IsAbs(refURL.Path) || filepath.IsAbs(refPath) { - // refPath is actually absolute - if refURL.Host != "" { - return refPath - } - parts := strings.Split(refPath, "#") - result := filepath.FromSlash(parts[0]) - if len(parts) == 2 { - result += "#" + parts[1] - } - return result + fixWindowsURI(refURL, refPath) // noop on non-windows OS + + refURL.Path = path.Clean(refURL.Path) + if refURL.Path == "." { + refURL.Path = "" } - // relative refPath - baseURL, _ := url.Parse(base) - if !strings.HasPrefix(refPath, "#") { - // combining paths - if baseURL.Host != "" { - baseURL.Path = path.Join(path.Dir(baseURL.Path), refURL.Path) - } else { // base is a file - newBase := fmt.Sprintf("%s#%s", filepath.Join(filepath.Dir(base), filepath.FromSlash(refURL.Path)), refURL.Fragment) - return newBase - } + r := MustCreateRef(refURL.String()) + if r.IsCanonical() { + return refURL.String() + } + baseURL, _ := url.Parse(base) + if path.IsAbs(refURL.Path) { + baseURL.Path = refURL.Path + } else if refURL.Path != "" { + baseURL.Path = path.Join(path.Dir(baseURL.Path), refURL.Path) } // copying fragment from ref to base baseURL.Fragment = refURL.Fragment + return baseURL.String() } -// denormalizePaths returns to simplest notation on file $ref, -// i.e. strips the absolute path and sets a path relative to the base path. +// denormalizeRef returns the simplest notation for a normalized $ref, given the path of the original root document. +// +// When calling this, we assume that: +// * $ref is a canonical URI +// * originalRelativeBase is a canonical URI +// +// denormalizeRef is currently used when we rewrite a $ref after a circular $ref has been detected. +// In this case, expansion stops and normally renders the internal canonical $ref. +// +// This internal $ref is eventually rebased to the original RelativeBase used for the expansion. // -// This is currently used when we rewrite ref after a circular ref has been detected -func denormalizeFileRef(ref *Ref, relativeBase, originalRelativeBase string) *Ref { - debugLog("denormalizeFileRef for: %s", ref.String()) +// There is a special case for schemas that are anchored with an "id": +// in that case, the rebasing is performed // against the id only if this is an anchor for the initial root document. +// All other intermediate "id"'s found along the way are ignored for the purpose of rebasing. +// +func denormalizeRef(ref *Ref, originalRelativeBase, id string) Ref { + debugLog("denormalizeRef called:\n$ref: %q\noriginal: %s\nroot ID:%s", ref.String(), originalRelativeBase, id) if ref.String() == "" || ref.IsRoot() || ref.HasFragmentOnly { - return ref - } - // strip relativeBase from URI - relativeBaseURL, _ := url.Parse(relativeBase) - relativeBaseURL.Fragment = "" - - if relativeBaseURL.IsAbs() && strings.HasPrefix(ref.String(), relativeBase) { - // this should work for absolute URI (e.g. http://...): we have an exact match, just trim prefix - r, _ := NewRef(strings.TrimPrefix(ref.String(), relativeBase)) - return &r + // short circuit: $ref to current doc + return *ref } - if relativeBaseURL.IsAbs() { - // other absolute URL get unchanged (i.e. with a non-empty scheme) - return ref + if id != "" { + idBaseURL, err := url.Parse(id) + if err == nil { // if the schema id is not usable as a URI, ignore it + if ref, ok := rebase(ref, idBaseURL, true); ok { // rebase, but keep references to root unchaged (do not want $ref: "") + // $ref relative to the ID of the schema in the root document + return ref + } + } } - // for relative file URIs: originalRelativeBaseURL, _ := url.Parse(originalRelativeBase) - originalRelativeBaseURL.Fragment = "" - if strings.HasPrefix(ref.String(), originalRelativeBaseURL.String()) { - // the resulting ref is in the expanded spec: return a local ref - r, _ := NewRef(strings.TrimPrefix(ref.String(), originalRelativeBaseURL.String())) - return &r + + r, _ := rebase(ref, originalRelativeBaseURL, false) + + return r +} + +func rebase(ref *Ref, v *url.URL, notEqual bool) (Ref, bool) { + var newBase url.URL + + u := ref.GetURL() + + if u.Scheme != v.Scheme || u.Host != v.Host { + return *ref, false } - // check if we may set a relative path, considering the original base path for this spec. - // Example: - // spec is located at /mypath/spec.json - // my normalized ref points to: /mypath/item.json#/target - // expected result: item.json#/target - parts := strings.Split(ref.String(), "#") - relativePath, err := filepath.Rel(path.Dir(originalRelativeBaseURL.String()), parts[0]) - if err != nil { - // there is no common ancestor (e.g. different drives on windows) - // leaves the ref unchanged - return ref + docPath := v.Path + v.Path = path.Dir(v.Path) + + if v.Path == "." { + v.Path = "" + } else if !strings.HasSuffix(v.Path, "/") { + v.Path += "/" } - if len(parts) == 2 { - relativePath += "#" + parts[1] + + newBase.Fragment = u.Fragment + + if strings.HasPrefix(u.Path, docPath) { + newBase.Path = strings.TrimPrefix(u.Path, docPath) + } else { + newBase.Path = strings.TrimPrefix(u.Path, v.Path) + } + + if notEqual && newBase.Path == "" && newBase.Fragment == "" { + // do not want rebasing to end up in an empty $ref + return *ref, false } - r, _ := NewRef(relativePath) - return &r -} -// relativeBase could be an ABSOLUTE file path or an ABSOLUTE URL -func normalizeFileRef(ref *Ref, relativeBase string) *Ref { - // This is important for when the reference is pointing to the root schema - if ref.String() == "" { - r, _ := NewRef(relativeBase) - return &r + if path.IsAbs(newBase.Path) { + // whenever we end up with an absolute path, specify the scheme and host + newBase.Scheme = v.Scheme + newBase.Host = v.Host } - debugLog("normalizing %s against %s", ref.String(), relativeBase) + return MustCreateRef(newBase.String()), true +} - s := normalizePaths(ref.String(), relativeBase) - r, _ := NewRef(s) +// normalizeRef canonicalize a Ref, using a canonical relativeBase as its absolute anchor +func normalizeRef(ref *Ref, relativeBase string) *Ref { + r := MustCreateRef(normalizeURI(ref.String(), relativeBase)) return &r } -// absPath returns the absolute path of a file -func absPath(fname string) (string, error) { - if strings.HasPrefix(fname, "http") { - return fname, nil +// normalizeBase performs a normalization of the input base path. +// +// This always yields a canonical URI (absolute), usable for the document cache. +// +// It ensures that all further internal work on basePath may safely assume +// a non-empty, cross-platform, canonical URI (i.e. absolute). +// +// This normalization tolerates windows paths (e.g. C:\x\y\File.dat) and transform this +// in a file:// URL with lower cased drive letter and path. +// +// See also: https://en.wikipedia.org/wiki/File_URI_scheme +func normalizeBase(in string) string { + u, err := url.Parse(in) + if err != nil { + specLogger.Printf("warning: invalid URI in RelativeBase %q: %v", in, err) + u, in = repairURI(in) + } + + u.Fragment = "" // any fragment in the base is irrelevant + + fixWindowsURI(u, in) // noop on non-windows OS + + u.Path = path.Clean(u.Path) + if u.Path == "." { // empty after Clean() + u.Path = "" } - if filepath.IsAbs(fname) { - return fname, nil + + if u.Scheme != "" { + if path.IsAbs(u.Path) || u.Scheme != fileScheme { + // this is absolute or explicitly not a local file: we're good + return u.String() + } } - wd, err := os.Getwd() - return filepath.Join(wd, fname), err + + // no scheme or file scheme with relative path: assume file and make it absolute + // enforce scheme file://... with absolute path. + // + // If the input path is relative, we anchor the path to the current working directory. + // NOTE: we may end up with a host component. Leave it unchanged: e.g. file://host/folder/file.json + + u.Scheme = fileScheme + u.Path = absPath(u.Path) // platform-dependent + u.RawQuery = "" // any query component is irrelevant for a base + return u.String() } diff --git a/vendor/github.com/go-openapi/spec/normalizer_nonwindows.go b/vendor/github.com/go-openapi/spec/normalizer_nonwindows.go new file mode 100644 index 0000000000..c8a0645347 --- /dev/null +++ b/vendor/github.com/go-openapi/spec/normalizer_nonwindows.go @@ -0,0 +1,43 @@ +// +build !windows + +// Copyright 2015 go-swagger maintainers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package spec + +import ( + "net/url" + "path/filepath" +) + +// absPath makes a file path absolute and compatible with a URI path component. +// +// The parameter must be a path, not an URI. +func absPath(in string) string { + anchored, err := filepath.Abs(in) + if err != nil { + specLogger.Printf("warning: could not resolve current working directory: %v", err) + return in + } + return anchored +} + +func repairURI(in string) (*url.URL, string) { + u, _ := url.Parse("") + debugLog("repaired URI: original: %q, repaired: %q", in, "") + return u, "" +} + +func fixWindowsURI(u *url.URL, in string) { +} diff --git a/vendor/github.com/go-openapi/spec/normalizer_windows.go b/vendor/github.com/go-openapi/spec/normalizer_windows.go new file mode 100644 index 0000000000..fe2d1ecd43 --- /dev/null +++ b/vendor/github.com/go-openapi/spec/normalizer_windows.go @@ -0,0 +1,154 @@ +// -build windows + +// Copyright 2015 go-swagger maintainers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package spec + +import ( + "net/url" + "os" + "path" + "path/filepath" + "strings" +) + +// absPath makes a file path absolute and compatible with a URI path component +// +// The parameter must be a path, not an URI. +func absPath(in string) string { + // NOTE(windows): filepath.Abs exhibits a special behavior on windows for empty paths. + // See https://github.com/golang/go/issues/24441 + if in == "" { + in = "." + } + + anchored, err := filepath.Abs(in) + if err != nil { + specLogger.Printf("warning: could not resolve current working directory: %v", err) + return in + } + + pth := strings.ReplaceAll(strings.ToLower(anchored), `\`, `/`) + if !strings.HasPrefix(pth, "/") { + pth = "/" + pth + } + + return path.Clean(pth) +} + +// repairURI tolerates invalid file URIs with common typos +// such as 'file://E:\folder\file', that break the regular URL parser. +// +// Adopting the same defaults as for unixes (e.g. return an empty path) would +// result into a counter-intuitive result for that case (e.g. E:\folder\file is +// eventually resolved as the current directory). The repair will detect the missing "/". +// +// Note that this only works for the file scheme. +func repairURI(in string) (*url.URL, string) { + const prefix = fileScheme + "://" + if !strings.HasPrefix(in, prefix) { + // giving up: resolve to empty path + u, _ := url.Parse("") + + return u, "" + } + + // attempt the repair, stripping the scheme should be sufficient + u, _ := url.Parse(strings.TrimPrefix(in, prefix)) + debugLog("repaired URI: original: %q, repaired: %q", in, u.String()) + + return u, u.String() +} + +// fixWindowsURI tolerates an absolute file path on windows such as C:\Base\File.yaml or \\host\share\Base\File.yaml +// and makes it a canonical URI: file:///c:/base/file.yaml +// +// Catch 22 notes for Windows: +// +// * There may be a drive letter on windows (it is lower-cased) +// * There may be a share UNC, e.g. \\server\folder\data.xml +// * Paths are case insensitive +// * Paths may already contain slashes +// * Paths must be slashed +// +// NOTE: there is no escaping. "/" may be valid separators just like "\". +// We don't use ToSlash() (which escapes everything) because windows now also +// tolerates the use of "/". Hence, both C:\File.yaml and C:/File.yaml will work. +func fixWindowsURI(u *url.URL, in string) { + drive := filepath.VolumeName(in) + + if len(drive) > 0 { + if len(u.Scheme) == 1 && strings.EqualFold(u.Scheme, drive[:1]) { // a path with a drive letter + u.Scheme = fileScheme + u.Host = "" + u.Path = strings.Join([]string{drive, u.Opaque, u.Path}, `/`) // reconstruct the full path component (no fragment, no query) + } else if u.Host == "" && strings.HasPrefix(u.Path, drive) { // a path with a \\host volume + // NOTE: the special host@port syntax for UNC is not supported (yet) + u.Scheme = fileScheme + + // this is a modified version of filepath.Dir() to apply on the VolumeName itself + i := len(drive) - 1 + for i >= 0 && !os.IsPathSeparator(drive[i]) { + i-- + } + host := drive[:i] // \\host\share => host + + u.Path = strings.TrimPrefix(u.Path, host) + u.Host = strings.TrimPrefix(host, `\\`) + } + + u.Opaque = "" + u.Path = strings.ReplaceAll(strings.ToLower(u.Path), `\`, `/`) + + // ensure we form an absolute path + if !strings.HasPrefix(u.Path, "/") { + u.Path = "/" + u.Path + } + + u.Path = path.Clean(u.Path) + + return + } + + if u.Scheme == fileScheme { + // Handle dodgy cases for file://{...} URIs on windows. + // A canonical URI should always be followed by an absolute path. + // + // Examples: + // * file:///folder/file => valid, unchanged + // * file:///c:\folder\file => slashed + // * file:///./folder/file => valid, cleaned to remove the dot + // * file:///.\folder\file => remapped to cwd + // * file:///. => dodgy, remapped to / (consistent with the behavior on unix) + // * file:///.. => dodgy, remapped to / (consistent with the behavior on unix) + if (!path.IsAbs(u.Path) && !filepath.IsAbs(u.Path)) || (strings.HasPrefix(u.Path, `/.`) && strings.Contains(u.Path, `\`)) { + // ensure we form an absolute path + u.Path, _ = filepath.Abs(strings.TrimLeft(u.Path, `/`)) + if !strings.HasPrefix(u.Path, "/") { + u.Path = "/" + u.Path + } + } + u.Path = strings.ToLower(u.Path) + } + + // NOTE: lower case normalization does not propagate to inner resources, + // generated when rebasing: when joining a relative URI with a file to an absolute base, + // only the base is currently lower-cased. + // + // For now, we assume this is good enough for most use cases + // and try not to generate too many differences + // between the output produced on different platforms. + u.Path = path.Clean(strings.ReplaceAll(u.Path, `\`, `/`)) +} diff --git a/vendor/github.com/go-openapi/spec/operation.go b/vendor/github.com/go-openapi/spec/operation.go index b1ebd59945..995ce6acb1 100644 --- a/vendor/github.com/go-openapi/spec/operation.go +++ b/vendor/github.com/go-openapi/spec/operation.go @@ -25,7 +25,6 @@ import ( ) func init() { - //gob.Register(map[string][]interface{}{}) gob.Register(map[string]interface{}{}) gob.Register([]interface{}{}) } diff --git a/vendor/github.com/go-openapi/spec/parameter.go b/vendor/github.com/go-openapi/spec/parameter.go index cecdff5456..2b2b89b67b 100644 --- a/vendor/github.com/go-openapi/spec/parameter.go +++ b/vendor/github.com/go-openapi/spec/parameter.go @@ -39,8 +39,7 @@ func PathParam(name string) *Parameter { // BodyParam creates a body parameter func BodyParam(name string, schema *Schema) *Parameter { - return &Parameter{ParamProps: ParamProps{Name: name, In: "body", Schema: schema}, - SimpleSchema: SimpleSchema{Type: "object"}} + return &Parameter{ParamProps: ParamProps{Name: name, In: "body", Schema: schema}} } // FormDataParam creates a body parameter @@ -58,7 +57,7 @@ func FileParam(name string) *Parameter { func SimpleArrayParam(name, tpe, fmt string) *Parameter { return &Parameter{ParamProps: ParamProps{Name: name}, SimpleSchema: SimpleSchema{Type: jsonArray, CollectionFormat: "csv", - Items: &Items{SimpleSchema: SimpleSchema{Type: "string", Format: fmt}}}} + Items: &Items{SimpleSchema: SimpleSchema{Type: tpe, Format: fmt}}}} } // ParamRef creates a parameter that's a json reference @@ -278,6 +277,12 @@ func (p *Parameter) AllowDuplicates() *Parameter { return p } +// WithValidations is a fluent method to set parameter validations +func (p *Parameter) WithValidations(val CommonValidations) *Parameter { + p.SetValidations(SchemaValidations{CommonValidations: val}) + return p +} + // UnmarshalJSON hydrates this items instance with the data from JSON func (p *Parameter) UnmarshalJSON(data []byte) error { if err := json.Unmarshal(data, &p.CommonValidations); err != nil { diff --git a/vendor/github.com/go-openapi/spec/properties.go b/vendor/github.com/go-openapi/spec/properties.go new file mode 100644 index 0000000000..2af13787ab --- /dev/null +++ b/vendor/github.com/go-openapi/spec/properties.go @@ -0,0 +1,91 @@ +package spec + +import ( + "bytes" + "encoding/json" + "reflect" + "sort" +) + +// OrderSchemaItem holds a named schema (e.g. from a property of an object) +type OrderSchemaItem struct { + Name string + Schema +} + +// OrderSchemaItems is a sortable slice of named schemas. +// The ordering is defined by the x-order schema extension. +type OrderSchemaItems []OrderSchemaItem + +// MarshalJSON produces a json object with keys defined by the name schemas +// of the OrderSchemaItems slice, keeping the original order of the slice. +func (items OrderSchemaItems) MarshalJSON() ([]byte, error) { + buf := bytes.NewBuffer(nil) + buf.WriteString("{") + for i := range items { + if i > 0 { + buf.WriteString(",") + } + buf.WriteString("\"") + buf.WriteString(items[i].Name) + buf.WriteString("\":") + bs, err := json.Marshal(&items[i].Schema) + if err != nil { + return nil, err + } + buf.Write(bs) + } + buf.WriteString("}") + return buf.Bytes(), nil +} + +func (items OrderSchemaItems) Len() int { return len(items) } +func (items OrderSchemaItems) Swap(i, j int) { items[i], items[j] = items[j], items[i] } +func (items OrderSchemaItems) Less(i, j int) (ret bool) { + ii, oki := items[i].Extensions.GetString("x-order") + ij, okj := items[j].Extensions.GetString("x-order") + if oki { + if okj { + defer func() { + if err := recover(); err != nil { + defer func() { + if err = recover(); err != nil { + ret = items[i].Name < items[j].Name + } + }() + ret = reflect.ValueOf(ii).String() < reflect.ValueOf(ij).String() + } + }() + return reflect.ValueOf(ii).Int() < reflect.ValueOf(ij).Int() + } + return true + } else if okj { + return false + } + return items[i].Name < items[j].Name +} + +// SchemaProperties is a map representing the properties of a Schema object. +// It knows how to transform its keys into an ordered slice. +type SchemaProperties map[string]Schema + +// ToOrderedSchemaItems transforms the map of properties into a sortable slice +func (properties SchemaProperties) ToOrderedSchemaItems() OrderSchemaItems { + items := make(OrderSchemaItems, 0, len(properties)) + for k, v := range properties { + items = append(items, OrderSchemaItem{ + Name: k, + Schema: v, + }) + } + sort.Sort(items) + return items +} + +// MarshalJSON produces properties as json, keeping their order. +func (properties SchemaProperties) MarshalJSON() ([]byte, error) { + if properties == nil { + return []byte("null"), nil + } + return json.Marshal(properties.ToOrderedSchemaItems()) +} diff --git a/vendor/github.com/go-openapi/spec/ref.go b/vendor/github.com/go-openapi/spec/ref.go index 1f31a9ead0..b0ef9bd9c9 100644 --- a/vendor/github.com/go-openapi/spec/ref.go +++ b/vendor/github.com/go-openapi/spec/ref.go @@ -48,7 +48,7 @@ type Ref struct { // RemoteURI gets the remote uri part of the ref func (r *Ref) RemoteURI() string { if r.String() == "" { - return r.String() + return "" } u := *r.GetURL() @@ -68,7 +68,7 @@ func (r *Ref) IsValidURI(basepaths ...string) bool { } if r.HasFullURL { - //#nosec + //nolint:noctx,gosec rr, err := http.Get(v) if err != nil { return false diff --git a/vendor/github.com/go-openapi/spec/resolver.go b/vendor/github.com/go-openapi/spec/resolver.go new file mode 100644 index 0000000000..47d1ee13fc --- /dev/null +++ b/vendor/github.com/go-openapi/spec/resolver.go @@ -0,0 +1,127 @@ +package spec + +import ( + "fmt" + + "github.com/go-openapi/swag" +) + +func resolveAnyWithBase(root interface{}, ref *Ref, result interface{}, options *ExpandOptions) error { + options = optionsOrDefault(options) + resolver := defaultSchemaLoader(root, options, nil, nil) + + if err := resolver.Resolve(ref, result, options.RelativeBase); err != nil { + return err + } + + return nil +} + +// ResolveRefWithBase resolves a reference against a context root with preservation of base path +func ResolveRefWithBase(root interface{}, ref *Ref, options *ExpandOptions) (*Schema, error) { + result := new(Schema) + + if err := resolveAnyWithBase(root, ref, result, options); err != nil { + return nil, err + } + + return result, nil +} + +// ResolveRef resolves a reference for a schema against a context root +// ref is guaranteed to be in root (no need to go to external files) +// +// ResolveRef is ONLY called from the code generation module +func ResolveRef(root interface{}, ref *Ref) (*Schema, error) { + res, _, err := ref.GetPointer().Get(root) + if err != nil { + return nil, err + } + + switch sch := res.(type) { + case Schema: + return &sch, nil + case *Schema: + return sch, nil + case map[string]interface{}: + newSch := new(Schema) + if err = swag.DynamicJSONToStruct(sch, newSch); err != nil { + return nil, err + } + return newSch, nil + default: + return nil, fmt.Errorf("type: %T: %w", sch, ErrUnknownTypeForReference) + } +} + +// ResolveParameterWithBase resolves a parameter reference against a context root and base path +func ResolveParameterWithBase(root interface{}, ref Ref, options *ExpandOptions) (*Parameter, error) { + result := new(Parameter) + + if err := resolveAnyWithBase(root, &ref, result, options); err != nil { + return nil, err + } + + return result, nil +} + +// ResolveParameter resolves a parameter reference against a context root +func ResolveParameter(root interface{}, ref Ref) (*Parameter, error) { + return ResolveParameterWithBase(root, ref, nil) +} + +// ResolveResponseWithBase resolves response a reference against a context root and base path +func ResolveResponseWithBase(root interface{}, ref Ref, options *ExpandOptions) (*Response, error) { + result := new(Response) + + err := resolveAnyWithBase(root, &ref, result, options) + if err != nil { + return nil, err + } + + return result, nil +} + +// ResolveResponse resolves response a reference against a context root +func ResolveResponse(root interface{}, ref Ref) (*Response, error) { + return ResolveResponseWithBase(root, ref, nil) +} + +// ResolvePathItemWithBase resolves response a path item against a context root and base path +func ResolvePathItemWithBase(root interface{}, ref Ref, options *ExpandOptions) (*PathItem, error) { + result := new(PathItem) + + if err := resolveAnyWithBase(root, &ref, result, options); err != nil { + return nil, err + } + + return result, nil +} + +// ResolvePathItem resolves response a path item against a context root and base path +// +// Deprecated: use ResolvePathItemWithBase instead +func ResolvePathItem(root interface{}, ref Ref, options *ExpandOptions) (*PathItem, error) { + return ResolvePathItemWithBase(root, ref, options) +} + +// ResolveItemsWithBase resolves parameter items reference against a context root and base path. +// +// NOTE: stricly speaking, this construct is not supported by Swagger 2.0. +// Similarly, $ref are forbidden in response headers. +func ResolveItemsWithBase(root interface{}, ref Ref, options *ExpandOptions) (*Items, error) { + result := new(Items) + + if err := resolveAnyWithBase(root, &ref, result, options); err != nil { + return nil, err + } + + return result, nil +} + +// ResolveItems resolves parameter items reference against a context root and base path. +// +// Deprecated: use ResolveItemsWithBase instead +func ResolveItems(root interface{}, ref Ref, options *ExpandOptions) (*Items, error) { + return ResolveItemsWithBase(root, ref, options) +} diff --git a/vendor/github.com/go-openapi/spec/response.go b/vendor/github.com/go-openapi/spec/response.go index 27729c1d93..0340b60d84 100644 --- a/vendor/github.com/go-openapi/spec/response.go +++ b/vendor/github.com/go-openapi/spec/response.go @@ -23,7 +23,7 @@ import ( // ResponseProps properties specific to a response type ResponseProps struct { - Description string `json:"description,omitempty"` + Description string `json:"description"` Schema *Schema `json:"schema,omitempty"` Headers map[string]Header `json:"headers,omitempty"` Examples map[string]interface{} `json:"examples,omitempty"` @@ -63,10 +63,31 @@ func (r *Response) UnmarshalJSON(data []byte) error { // MarshalJSON converts this items object to JSON func (r Response) MarshalJSON() ([]byte, error) { - b1, err := json.Marshal(r.ResponseProps) + var ( + b1 []byte + err error + ) + + if r.Ref.String() == "" { + // when there is no $ref, empty description is rendered as an empty string + b1, err = json.Marshal(r.ResponseProps) + } else { + // when there is $ref inside the schema, description should be omitempty-ied + b1, err = json.Marshal(struct { + Description string `json:"description,omitempty"` + Schema *Schema `json:"schema,omitempty"` + Headers map[string]Header `json:"headers,omitempty"` + Examples map[string]interface{} `json:"examples,omitempty"` + }{ + Description: r.ResponseProps.Description, + Schema: r.ResponseProps.Schema, + Examples: r.ResponseProps.Examples, + }) + } if err != nil { return nil, err } + b2, err := json.Marshal(r.Refable) if err != nil { return nil, err diff --git a/vendor/github.com/go-openapi/spec/schema.go b/vendor/github.com/go-openapi/spec/schema.go index 37858ece90..a8d0f737a7 100644 --- a/vendor/github.com/go-openapi/spec/schema.go +++ b/vendor/github.com/go-openapi/spec/schema.go @@ -158,41 +158,41 @@ func (r *SchemaURL) fromMap(v map[string]interface{}) error { // SchemaProps describes a JSON schema (draft 4) type SchemaProps struct { - ID string `json:"id,omitempty"` - Ref Ref `json:"-"` - Schema SchemaURL `json:"-"` - Description string `json:"description,omitempty"` - Type StringOrArray `json:"type,omitempty"` - Nullable bool `json:"nullable,omitempty"` - Format string `json:"format,omitempty"` - Title string `json:"title,omitempty"` - Default interface{} `json:"default,omitempty"` - Maximum *float64 `json:"maximum,omitempty"` - ExclusiveMaximum bool `json:"exclusiveMaximum,omitempty"` - Minimum *float64 `json:"minimum,omitempty"` - ExclusiveMinimum bool `json:"exclusiveMinimum,omitempty"` - MaxLength *int64 `json:"maxLength,omitempty"` - MinLength *int64 `json:"minLength,omitempty"` - Pattern string `json:"pattern,omitempty"` - MaxItems *int64 `json:"maxItems,omitempty"` - MinItems *int64 `json:"minItems,omitempty"` - UniqueItems bool `json:"uniqueItems,omitempty"` - MultipleOf *float64 `json:"multipleOf,omitempty"` - Enum []interface{} `json:"enum,omitempty"` - MaxProperties *int64 `json:"maxProperties,omitempty"` - MinProperties *int64 `json:"minProperties,omitempty"` - Required []string `json:"required,omitempty"` - Items *SchemaOrArray `json:"items,omitempty"` - AllOf []Schema `json:"allOf,omitempty"` - OneOf []Schema `json:"oneOf,omitempty"` - AnyOf []Schema `json:"anyOf,omitempty"` - Not *Schema `json:"not,omitempty"` - Properties map[string]Schema `json:"properties,omitempty"` - AdditionalProperties *SchemaOrBool `json:"additionalProperties,omitempty"` - PatternProperties map[string]Schema `json:"patternProperties,omitempty"` - Dependencies Dependencies `json:"dependencies,omitempty"` - AdditionalItems *SchemaOrBool `json:"additionalItems,omitempty"` - Definitions Definitions `json:"definitions,omitempty"` + ID string `json:"id,omitempty"` + Ref Ref `json:"-"` + Schema SchemaURL `json:"-"` + Description string `json:"description,omitempty"` + Type StringOrArray `json:"type,omitempty"` + Nullable bool `json:"nullable,omitempty"` + Format string `json:"format,omitempty"` + Title string `json:"title,omitempty"` + Default interface{} `json:"default,omitempty"` + Maximum *float64 `json:"maximum,omitempty"` + ExclusiveMaximum bool `json:"exclusiveMaximum,omitempty"` + Minimum *float64 `json:"minimum,omitempty"` + ExclusiveMinimum bool `json:"exclusiveMinimum,omitempty"` + MaxLength *int64 `json:"maxLength,omitempty"` + MinLength *int64 `json:"minLength,omitempty"` + Pattern string `json:"pattern,omitempty"` + MaxItems *int64 `json:"maxItems,omitempty"` + MinItems *int64 `json:"minItems,omitempty"` + UniqueItems bool `json:"uniqueItems,omitempty"` + MultipleOf *float64 `json:"multipleOf,omitempty"` + Enum []interface{} `json:"enum,omitempty"` + MaxProperties *int64 `json:"maxProperties,omitempty"` + MinProperties *int64 `json:"minProperties,omitempty"` + Required []string `json:"required,omitempty"` + Items *SchemaOrArray `json:"items,omitempty"` + AllOf []Schema `json:"allOf,omitempty"` + OneOf []Schema `json:"oneOf,omitempty"` + AnyOf []Schema `json:"anyOf,omitempty"` + Not *Schema `json:"not,omitempty"` + Properties SchemaProperties `json:"properties,omitempty"` + AdditionalProperties *SchemaOrBool `json:"additionalProperties,omitempty"` + PatternProperties SchemaProperties `json:"patternProperties,omitempty"` + Dependencies Dependencies `json:"dependencies,omitempty"` + AdditionalItems *SchemaOrBool `json:"additionalItems,omitempty"` + Definitions Definitions `json:"definitions,omitempty"` } // SwaggerSchemaProps are additional properties supported by swagger schemas, but not JSON-schema (draft 4) @@ -513,6 +513,56 @@ func (s *Schema) AsUnwrappedXML() *Schema { return s } +// SetValidations defines all schema validations. +// +// NOTE: Required, ReadOnly, AllOf, AnyOf, OneOf and Not are not considered. +func (s *Schema) SetValidations(val SchemaValidations) { + s.Maximum = val.Maximum + s.ExclusiveMaximum = val.ExclusiveMaximum + s.Minimum = val.Minimum + s.ExclusiveMinimum = val.ExclusiveMinimum + s.MaxLength = val.MaxLength + s.MinLength = val.MinLength + s.Pattern = val.Pattern + s.MaxItems = val.MaxItems + s.MinItems = val.MinItems + s.UniqueItems = val.UniqueItems + s.MultipleOf = val.MultipleOf + s.Enum = val.Enum + s.MinProperties = val.MinProperties + s.MaxProperties = val.MaxProperties + s.PatternProperties = val.PatternProperties +} + +// WithValidations is a fluent method to set schema validations +func (s *Schema) WithValidations(val SchemaValidations) *Schema { + s.SetValidations(val) + return s +} + +// Validations returns a clone of the validations for this schema +func (s Schema) Validations() SchemaValidations { + return SchemaValidations{ + CommonValidations: CommonValidations{ + Maximum: s.Maximum, + ExclusiveMaximum: s.ExclusiveMaximum, + Minimum: s.Minimum, + ExclusiveMinimum: s.ExclusiveMinimum, + MaxLength: s.MaxLength, + MinLength: s.MinLength, + Pattern: s.Pattern, + MaxItems: s.MaxItems, + MinItems: s.MinItems, + UniqueItems: s.UniqueItems, + MultipleOf: s.MultipleOf, + Enum: s.Enum, + }, + MinProperties: s.MinProperties, + MaxProperties: s.MaxProperties, + PatternProperties: s.PatternProperties, + } +} + // MarshalJSON marshal this to JSON func (s Schema) MarshalJSON() ([]byte, error) { b1, err := json.Marshal(s.SchemaProps) diff --git a/vendor/github.com/go-openapi/spec/schema_loader.go b/vendor/github.com/go-openapi/spec/schema_loader.go index 02d9966c1a..b81175afdf 100644 --- a/vendor/github.com/go-openapi/spec/schema_loader.go +++ b/vendor/github.com/go-openapi/spec/schema_loader.go @@ -25,35 +25,50 @@ import ( "github.com/go-openapi/swag" ) -// PathLoader function to use when loading remote refs -var PathLoader func(string) (json.RawMessage, error) - -func init() { - PathLoader = func(path string) (json.RawMessage, error) { - data, err := swag.LoadFromFileOrHTTP(path) - if err != nil { - return nil, err - } - return json.RawMessage(data), nil +// PathLoader is a function to use when loading remote refs. +// +// This is a package level default. It may be overridden or bypassed by +// specifying the loader in ExpandOptions. +// +// NOTE: if you are using the go-openapi/loads package, it will override +// this value with its own default (a loader to retrieve YAML documents as +// well as JSON ones). +var PathLoader = func(pth string) (json.RawMessage, error) { + data, err := swag.LoadFromFileOrHTTP(pth) + if err != nil { + return nil, err } + return json.RawMessage(data), nil } // resolverContext allows to share a context during spec processing. // At the moment, it just holds the index of circular references found. type resolverContext struct { - // circulars holds all visited circular references, which allows shortcuts. - // NOTE: this is not just a performance improvement: it is required to figure out - // circular references which participate several cycles. + // circulars holds all visited circular references, to shortcircuit $ref resolution. + // // This structure is privately instantiated and needs not be locked against // concurrent access, unless we chose to implement a parallel spec walking. circulars map[string]bool basePath string + loadDoc func(string) (json.RawMessage, error) + rootID string } -func newResolverContext(originalBasePath string) *resolverContext { +func newResolverContext(options *ExpandOptions) *resolverContext { + expandOptions := optionsOrDefault(options) + + // path loader may be overridden by options + var loader func(string) (json.RawMessage, error) + if expandOptions.PathLoader == nil { + loader = PathLoader + } else { + loader = expandOptions.PathLoader + } + return &resolverContext{ circulars: make(map[string]bool), - basePath: originalBasePath, // keep the root base path in context + basePath: expandOptions.RelativeBase, // keep the root base path in context + loadDoc: loader, } } @@ -62,21 +77,20 @@ type schemaLoader struct { options *ExpandOptions cache ResolutionCache context *resolverContext - loadDoc func(string) (json.RawMessage, error) } -func (r *schemaLoader) transitiveResolver(basePath string, ref Ref) (*schemaLoader, error) { +func (r *schemaLoader) transitiveResolver(basePath string, ref Ref) *schemaLoader { if ref.IsRoot() || ref.HasFragmentOnly { - return r, nil + return r } - baseRef, _ := NewRef(basePath) - currentRef := normalizeFileRef(&ref, basePath) + baseRef := MustCreateRef(basePath) + currentRef := normalizeRef(&ref, basePath) if strings.HasPrefix(currentRef.String(), baseRef.String()) { - return r, nil + return r } - // Set a new root to resolve against + // set a new root against which to resolve rootURL := currentRef.GetURL() rootURL.Fragment = "" root, _ := r.cache.Get(rootURL.String()) @@ -85,35 +99,36 @@ func (r *schemaLoader) transitiveResolver(basePath string, ref Ref) (*schemaLoad // traversing multiple documents newOptions := r.options newOptions.RelativeBase = rootURL.String() - debugLog("setting new root: %s", newOptions.RelativeBase) + return defaultSchemaLoader(root, newOptions, r.cache, r.context) } func (r *schemaLoader) updateBasePath(transitive *schemaLoader, basePath string) string { if transitive != r { - debugLog("got a new resolver") if transitive.options != nil && transitive.options.RelativeBase != "" { - basePath, _ = absPath(transitive.options.RelativeBase) - debugLog("new basePath = %s", basePath) + return normalizeBase(transitive.options.RelativeBase) } } + return basePath } func (r *schemaLoader) resolveRef(ref *Ref, target interface{}, basePath string) error { tgt := reflect.ValueOf(target) if tgt.Kind() != reflect.Ptr { - return fmt.Errorf("resolve ref: target needs to be a pointer") + return ErrResolveRefNeedsAPointer } - refURL := ref.GetURL() - if refURL == nil { + if ref.GetURL() == nil { return nil } - var res interface{} - var data interface{} - var err error + var ( + res interface{} + data interface{} + err error + ) + // Resolve against the root if it isn't nil, and if ref is pointing at the root, or has a fragment only which means // it is pointing somewhere in the root. root := r.root @@ -122,12 +137,11 @@ func (r *schemaLoader) resolveRef(ref *Ref, target interface{}, basePath string) root, _, _, _ = r.load(baseRef.GetURL()) } } + if (ref.IsRoot() || ref.HasFragmentOnly) && root != nil { data = root } else { - baseRef := normalizeFileRef(ref, basePath) - debugLog("current ref is: %s", ref.String()) - debugLog("current ref normalized file: %s", baseRef.String()) + baseRef := normalizeRef(ref, basePath) data, _, _, err = r.load(baseRef.GetURL()) if err != nil { return err @@ -150,52 +164,60 @@ func (r *schemaLoader) load(refURL *url.URL) (interface{}, url.URL, bool, error) toFetch.Fragment = "" var err error - path := toFetch.String() - if path == rootBase { - path, err = absPath(rootBase) - if err != nil { - return nil, url.URL{}, false, err - } + pth := toFetch.String() + normalized := normalizeBase(pth) + debugLog("loading doc from: %s", normalized) + + unescaped, err := url.PathUnescape(normalized) + if err != nil { + return nil, url.URL{}, false, err } - normalized := normalizeAbsPath(path) - data, fromCache := r.cache.Get(normalized) - if !fromCache { - b, err := r.loadDoc(normalized) - if err != nil { - debugLog("unable to load the document: %v", err) - return nil, url.URL{}, false, err - } + u := url.URL{Path: unescaped} - if err := json.Unmarshal(b, &data); err != nil { - return nil, url.URL{}, false, err - } - r.cache.Set(normalized, data) + data, fromCache := r.cache.Get(u.RequestURI()) + if fromCache { + return data, toFetch, fromCache, nil } - return data, toFetch, fromCache, nil + b, err := r.context.loadDoc(normalized) + if err != nil { + return nil, url.URL{}, false, err + } + + var doc interface{} + if err := json.Unmarshal(b, &doc); err != nil { + return nil, url.URL{}, false, err + } + r.cache.Set(normalized, doc) + + return doc, toFetch, fromCache, nil } // isCircular detects cycles in sequences of $ref. +// // It relies on a private context (which needs not be locked). func (r *schemaLoader) isCircular(ref *Ref, basePath string, parentRefs ...string) (foundCycle bool) { - normalizedRef := normalizePaths(ref.String(), basePath) + normalizedRef := normalizeURI(ref.String(), basePath) if _, ok := r.context.circulars[normalizedRef]; ok { // circular $ref has been already detected in another explored cycle foundCycle = true return } - foundCycle = swag.ContainsStringsCI(parentRefs, normalizedRef) + foundCycle = swag.ContainsStrings(parentRefs, normalizedRef) // normalized windows url's are lower cased if foundCycle { r.context.circulars[normalizedRef] = true } return } -// Resolve resolves a reference against basePath and stores the result in target -// Resolve is not in charge of following references, it only resolves ref by following its URL -// if the schema that ref is referring to has more refs in it. Resolve doesn't resolve them -// if basePath is an empty string, ref is resolved against the root schema stored in the schemaLoader struct +// Resolve resolves a reference against basePath and stores the result in target. +// +// Resolve is not in charge of following references: it only resolves ref by following its URL. +// +// If the schema the ref is referring to holds nested refs, Resolve doesn't resolve them. +// +// If basePath is an empty string, ref is resolved against the root schema stored in the schemaLoader struct func (r *schemaLoader) Resolve(ref *Ref, target interface{}, basePath string) error { return r.resolveRef(ref, target, basePath) } @@ -212,30 +234,32 @@ func (r *schemaLoader) deref(input interface{}, parentRefs []string, basePath st case *PathItem: ref = &refable.Ref default: - return fmt.Errorf("deref: unsupported type %T", input) + return fmt.Errorf("unsupported type: %T: %w", input, ErrDerefUnsupportedType) } curRef := ref.String() - if curRef != "" { - normalizedRef := normalizeFileRef(ref, basePath) - normalizedBasePath := normalizedRef.RemoteURI() + if curRef == "" { + return nil + } - if r.isCircular(normalizedRef, basePath, parentRefs...) { - return nil - } + normalizedRef := normalizeRef(ref, basePath) + normalizedBasePath := normalizedRef.RemoteURI() - if err := r.resolveRef(ref, input, basePath); r.shouldStopOnError(err) { - return err - } + if r.isCircular(normalizedRef, basePath, parentRefs...) { + return nil + } - // NOTE(fredbi): removed basePath check => needs more testing - if ref.String() != "" && ref.String() != curRef { - parentRefs = append(parentRefs, normalizedRef.String()) - return r.deref(input, parentRefs, normalizedBasePath) - } + if err := r.resolveRef(ref, input, basePath); r.shouldStopOnError(err) { + return err + } + + if ref.String() == "" || ref.String() == curRef { + // done with rereferencing + return nil } - return nil + parentRefs = append(parentRefs, normalizedRef.String()) + return r.deref(input, parentRefs, normalizedBasePath) } func (r *schemaLoader) shouldStopOnError(err error) bool { @@ -250,30 +274,65 @@ func (r *schemaLoader) shouldStopOnError(err error) bool { return false } +func (r *schemaLoader) setSchemaID(target interface{}, id, basePath string) (string, string) { + debugLog("schema has ID: %s", id) + + // handling the case when id is a folder + // remember that basePath has to point to a file + var refPath string + if strings.HasSuffix(id, "/") { + // ensure this is detected as a file, not a folder + refPath = fmt.Sprintf("%s%s", id, "placeholder.json") + } else { + refPath = id + } + + // updates the current base path + // * important: ID can be a relative path + // * registers target to be fetchable from the new base proposed by this id + newBasePath := normalizeURI(refPath, basePath) + + // store found IDs for possible future reuse in $ref + r.cache.Set(newBasePath, target) + + // the root document has an ID: all $ref relative to that ID may + // be rebased relative to the root document + if basePath == r.context.basePath { + debugLog("root document is a schema with ID: %s (normalized as:%s)", id, newBasePath) + r.context.rootID = newBasePath + } + + return newBasePath, refPath +} + func defaultSchemaLoader( root interface{}, expandOptions *ExpandOptions, cache ResolutionCache, - context *resolverContext) (*schemaLoader, error) { + context *resolverContext) *schemaLoader { - if cache == nil { - cache = resCache - } if expandOptions == nil { expandOptions = &ExpandOptions{} } - absBase, _ := absPath(expandOptions.RelativeBase) + + cache = cacheOrDefault(cache) + + if expandOptions.RelativeBase == "" { + // if no relative base is provided, assume the root document + // contains all $ref, or at least, that the relative documents + // may be resolved from the current working directory. + expandOptions.RelativeBase = baseForRoot(root, cache) + } + debugLog("effective expander options: %#v", expandOptions) + if context == nil { - context = newResolverContext(absBase) + context = newResolverContext(expandOptions) } + return &schemaLoader{ root: root, options: expandOptions, cache: cache, context: context, - loadDoc: func(path string) (json.RawMessage, error) { - debugLog("fetching document at %q", path) - return PathLoader(path) - }, - }, nil + } } diff --git a/vendor/github.com/go-openapi/spec/security_scheme.go b/vendor/github.com/go-openapi/spec/security_scheme.go index fe353842a6..9d0bdae908 100644 --- a/vendor/github.com/go-openapi/spec/security_scheme.go +++ b/vendor/github.com/go-openapi/spec/security_scheme.go @@ -82,12 +82,12 @@ func OAuth2AccessToken(authorizationURL, tokenURL string) *SecurityScheme { type SecuritySchemeProps struct { Description string `json:"description,omitempty"` Type string `json:"type"` - Name string `json:"name,omitempty"` // api key - In string `json:"in,omitempty"` // api key - Flow string `json:"flow,omitempty"` // oauth2 - AuthorizationURL string `json:"authorizationUrl,omitempty"` // oauth2 - TokenURL string `json:"tokenUrl,omitempty"` // oauth2 - Scopes map[string]string `json:"scopes,omitempty"` // oauth2 + Name string `json:"name,omitempty"` // api key + In string `json:"in,omitempty"` // api key + Flow string `json:"flow,omitempty"` // oauth2 + AuthorizationURL string `json:"authorizationUrl"` // oauth2 + TokenURL string `json:"tokenUrl,omitempty"` // oauth2 + Scopes map[string]string `json:"scopes,omitempty"` // oauth2 } // AddScope adds a scope to this security scheme @@ -120,10 +120,40 @@ func (s SecurityScheme) JSONLookup(token string) (interface{}, error) { // MarshalJSON marshal this to JSON func (s SecurityScheme) MarshalJSON() ([]byte, error) { - b1, err := json.Marshal(s.SecuritySchemeProps) + var ( + b1 []byte + err error + ) + + if s.Type == oauth2 && (s.Flow == "implicit" || s.Flow == "accessCode") { + // when oauth2 for implicit or accessCode flows, empty AuthorizationURL is added as empty string + b1, err = json.Marshal(s.SecuritySchemeProps) + } else { + // when not oauth2, empty AuthorizationURL should be omitted + b1, err = json.Marshal(struct { + Description string `json:"description,omitempty"` + Type string `json:"type"` + Name string `json:"name,omitempty"` // api key + In string `json:"in,omitempty"` // api key + Flow string `json:"flow,omitempty"` // oauth2 + AuthorizationURL string `json:"authorizationUrl,omitempty"` // oauth2 + TokenURL string `json:"tokenUrl,omitempty"` // oauth2 + Scopes map[string]string `json:"scopes,omitempty"` // oauth2 + }{ + Description: s.Description, + Type: s.Type, + Name: s.Name, + In: s.In, + Flow: s.Flow, + AuthorizationURL: s.AuthorizationURL, + TokenURL: s.TokenURL, + Scopes: s.Scopes, + }) + } if err != nil { return nil, err } + b2, err := json.Marshal(s.VendorExtensible) if err != nil { return nil, err diff --git a/vendor/github.com/go-openapi/spec/spec.go b/vendor/github.com/go-openapi/spec/spec.go index 0bb045bc06..7d38b6e625 100644 --- a/vendor/github.com/go-openapi/spec/spec.go +++ b/vendor/github.com/go-openapi/spec/spec.go @@ -14,7 +14,9 @@ package spec -import "encoding/json" +import ( + "encoding/json" +) //go:generate curl -L --progress -o ./schemas/v2/schema.json http://swagger.io/v2/schema.json //go:generate curl -L --progress -o ./schemas/jsonschema-draft-04.json http://json-schema.org/draft-04/schema @@ -28,16 +30,6 @@ const ( JSONSchemaURL = "http://json-schema.org/draft-04/schema#" ) -var ( - jsonSchema *Schema - swaggerSchema *Schema -) - -func init() { - jsonSchema = MustLoadJSONSchemaDraft04() - swaggerSchema = MustLoadSwagger20Schema() -} - // MustLoadJSONSchemaDraft04 panics when Swagger20Schema returns an error func MustLoadJSONSchemaDraft04() *Schema { d, e := JSONSchemaDraft04() diff --git a/vendor/github.com/go-openapi/spec/unused.go b/vendor/github.com/go-openapi/spec/unused.go deleted file mode 100644 index aa12b56f6e..0000000000 --- a/vendor/github.com/go-openapi/spec/unused.go +++ /dev/null @@ -1,174 +0,0 @@ -// Copyright 2015 go-swagger maintainers -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package spec - -/* - -import ( - "net/url" - "os" - "path" - "path/filepath" - - "github.com/go-openapi/jsonpointer" -) - - // Some currently unused functions and definitions that - // used to be part of the expander. - - // Moved here for the record and possible future reuse - -var ( - idPtr, _ = jsonpointer.New("/id") - refPtr, _ = jsonpointer.New("/$ref") -) - -func idFromNode(node interface{}) (*Ref, error) { - if idValue, _, err := idPtr.Get(node); err == nil { - if refStr, ok := idValue.(string); ok && refStr != "" { - idRef, err := NewRef(refStr) - if err != nil { - return nil, err - } - return &idRef, nil - } - } - return nil, nil -} - -func nextRef(startingNode interface{}, startingRef *Ref, ptr *jsonpointer.Pointer) *Ref { - if startingRef == nil { - return nil - } - - if ptr == nil { - return startingRef - } - - ret := startingRef - var idRef *Ref - node := startingNode - - for _, tok := range ptr.DecodedTokens() { - node, _, _ = jsonpointer.GetForToken(node, tok) - if node == nil { - break - } - - idRef, _ = idFromNode(node) - if idRef != nil { - nw, err := ret.Inherits(*idRef) - if err != nil { - break - } - ret = nw - } - - refRef, _, _ := refPtr.Get(node) - if refRef != nil { - var rf Ref - switch value := refRef.(type) { - case string: - rf, _ = NewRef(value) - } - nw, err := ret.Inherits(rf) - if err != nil { - break - } - nwURL := nw.GetURL() - if nwURL.Scheme == "file" || (nwURL.Scheme == "" && nwURL.Host == "") { - nwpt := filepath.ToSlash(nwURL.Path) - if filepath.IsAbs(nwpt) { - _, err := os.Stat(nwpt) - if err != nil { - nwURL.Path = filepath.Join(".", nwpt) - } - } - } - - ret = nw - } - - } - - return ret -} - -// basePathFromSchemaID returns a new basePath based on an existing basePath and a schema ID -func basePathFromSchemaID(oldBasePath, id string) string { - u, err := url.Parse(oldBasePath) - if err != nil { - panic(err) - } - uid, err := url.Parse(id) - if err != nil { - panic(err) - } - - if path.IsAbs(uid.Path) { - return id - } - u.Path = path.Join(path.Dir(u.Path), uid.Path) - return u.String() -} -*/ - -// type ExtraSchemaProps map[string]interface{} - -// // JSONSchema represents a structure that is a json schema draft 04 -// type JSONSchema struct { -// SchemaProps -// ExtraSchemaProps -// } - -// // MarshalJSON marshal this to JSON -// func (s JSONSchema) MarshalJSON() ([]byte, error) { -// b1, err := json.Marshal(s.SchemaProps) -// if err != nil { -// return nil, err -// } -// b2, err := s.Ref.MarshalJSON() -// if err != nil { -// return nil, err -// } -// b3, err := s.Schema.MarshalJSON() -// if err != nil { -// return nil, err -// } -// b4, err := json.Marshal(s.ExtraSchemaProps) -// if err != nil { -// return nil, err -// } -// return swag.ConcatJSON(b1, b2, b3, b4), nil -// } - -// // UnmarshalJSON marshal this from JSON -// func (s *JSONSchema) UnmarshalJSON(data []byte) error { -// var sch JSONSchema -// if err := json.Unmarshal(data, &sch.SchemaProps); err != nil { -// return err -// } -// if err := json.Unmarshal(data, &sch.Ref); err != nil { -// return err -// } -// if err := json.Unmarshal(data, &sch.Schema); err != nil { -// return err -// } -// if err := json.Unmarshal(data, &sch.ExtraSchemaProps); err != nil { -// return err -// } -// *s = sch -// return nil -// } diff --git a/vendor/github.com/go-openapi/spec/validations.go b/vendor/github.com/go-openapi/spec/validations.go new file mode 100644 index 0000000000..6360a8ea77 --- /dev/null +++ b/vendor/github.com/go-openapi/spec/validations.go @@ -0,0 +1,215 @@ +package spec + +// CommonValidations describe common JSON-schema validations +type CommonValidations struct { + Maximum *float64 `json:"maximum,omitempty"` + ExclusiveMaximum bool `json:"exclusiveMaximum,omitempty"` + Minimum *float64 `json:"minimum,omitempty"` + ExclusiveMinimum bool `json:"exclusiveMinimum,omitempty"` + MaxLength *int64 `json:"maxLength,omitempty"` + MinLength *int64 `json:"minLength,omitempty"` + Pattern string `json:"pattern,omitempty"` + MaxItems *int64 `json:"maxItems,omitempty"` + MinItems *int64 `json:"minItems,omitempty"` + UniqueItems bool `json:"uniqueItems,omitempty"` + MultipleOf *float64 `json:"multipleOf,omitempty"` + Enum []interface{} `json:"enum,omitempty"` +} + +// SetValidations defines all validations for a simple schema. +// +// NOTE: the input is the larger set of validations available for schemas. +// For simple schemas, MinProperties and MaxProperties are ignored. +func (v *CommonValidations) SetValidations(val SchemaValidations) { + v.Maximum = val.Maximum + v.ExclusiveMaximum = val.ExclusiveMaximum + v.Minimum = val.Minimum + v.ExclusiveMinimum = val.ExclusiveMinimum + v.MaxLength = val.MaxLength + v.MinLength = val.MinLength + v.Pattern = val.Pattern + v.MaxItems = val.MaxItems + v.MinItems = val.MinItems + v.UniqueItems = val.UniqueItems + v.MultipleOf = val.MultipleOf + v.Enum = val.Enum +} + +type clearedValidation struct { + Validation string + Value interface{} +} + +type clearedValidations []clearedValidation + +func (c clearedValidations) apply(cbs []func(string, interface{})) { + for _, cb := range cbs { + for _, cleared := range c { + cb(cleared.Validation, cleared.Value) + } + } +} + +// ClearNumberValidations clears all number validations. +// +// Some callbacks may be set by the caller to capture changed values. +func (v *CommonValidations) ClearNumberValidations(cbs ...func(string, interface{})) { + done := make(clearedValidations, 0, 5) + defer func() { + done.apply(cbs) + }() + + if v.Minimum != nil { + done = append(done, clearedValidation{Validation: "minimum", Value: v.Minimum}) + v.Minimum = nil + } + if v.Maximum != nil { + done = append(done, clearedValidation{Validation: "maximum", Value: v.Maximum}) + v.Maximum = nil + } + if v.ExclusiveMaximum { + done = append(done, clearedValidation{Validation: "exclusiveMaximum", Value: v.ExclusiveMaximum}) + v.ExclusiveMaximum = false + } + if v.ExclusiveMinimum { + done = append(done, clearedValidation{Validation: "exclusiveMinimum", Value: v.ExclusiveMinimum}) + v.ExclusiveMinimum = false + } + if v.MultipleOf != nil { + done = append(done, clearedValidation{Validation: "multipleOf", Value: v.MultipleOf}) + v.MultipleOf = nil + } +} + +// ClearStringValidations clears all string validations. +// +// Some callbacks may be set by the caller to capture changed values. +func (v *CommonValidations) ClearStringValidations(cbs ...func(string, interface{})) { + done := make(clearedValidations, 0, 3) + defer func() { + done.apply(cbs) + }() + + if v.Pattern != "" { + done = append(done, clearedValidation{Validation: "pattern", Value: v.Pattern}) + v.Pattern = "" + } + if v.MinLength != nil { + done = append(done, clearedValidation{Validation: "minLength", Value: v.MinLength}) + v.MinLength = nil + } + if v.MaxLength != nil { + done = append(done, clearedValidation{Validation: "maxLength", Value: v.MaxLength}) + v.MaxLength = nil + } +} + +// ClearArrayValidations clears all array validations. +// +// Some callbacks may be set by the caller to capture changed values. +func (v *CommonValidations) ClearArrayValidations(cbs ...func(string, interface{})) { + done := make(clearedValidations, 0, 3) + defer func() { + done.apply(cbs) + }() + + if v.MaxItems != nil { + done = append(done, clearedValidation{Validation: "maxItems", Value: v.MaxItems}) + v.MaxItems = nil + } + if v.MinItems != nil { + done = append(done, clearedValidation{Validation: "minItems", Value: v.MinItems}) + v.MinItems = nil + } + if v.UniqueItems { + done = append(done, clearedValidation{Validation: "uniqueItems", Value: v.UniqueItems}) + v.UniqueItems = false + } +} + +// Validations returns a clone of the validations for a simple schema. +// +// NOTE: in the context of simple schema objects, MinProperties, MaxProperties +// and PatternProperties remain unset. +func (v CommonValidations) Validations() SchemaValidations { + return SchemaValidations{ + CommonValidations: v, + } +} + +// HasNumberValidations indicates if the validations are for numbers or integers +func (v CommonValidations) HasNumberValidations() bool { + return v.Maximum != nil || v.Minimum != nil || v.MultipleOf != nil +} + +// HasStringValidations indicates if the validations are for strings +func (v CommonValidations) HasStringValidations() bool { + return v.MaxLength != nil || v.MinLength != nil || v.Pattern != "" +} + +// HasArrayValidations indicates if the validations are for arrays +func (v CommonValidations) HasArrayValidations() bool { + return v.MaxItems != nil || v.MinItems != nil || v.UniqueItems +} + +// HasEnum indicates if the validation includes some enum constraint +func (v CommonValidations) HasEnum() bool { + return len(v.Enum) > 0 +} + +// SchemaValidations describes the validation properties of a schema +// +// NOTE: at this moment, this is not embedded in SchemaProps because this would induce a breaking change +// in the exported members: all initializers using litterals would fail. +type SchemaValidations struct { + CommonValidations + + PatternProperties SchemaProperties `json:"patternProperties,omitempty"` + MaxProperties *int64 `json:"maxProperties,omitempty"` + MinProperties *int64 `json:"minProperties,omitempty"` +} + +// HasObjectValidations indicates if the validations are for objects +func (v SchemaValidations) HasObjectValidations() bool { + return v.MaxProperties != nil || v.MinProperties != nil || v.PatternProperties != nil +} + +// SetValidations for schema validations +func (v *SchemaValidations) SetValidations(val SchemaValidations) { + v.CommonValidations.SetValidations(val) + v.PatternProperties = val.PatternProperties + v.MaxProperties = val.MaxProperties + v.MinProperties = val.MinProperties +} + +// Validations for a schema +func (v SchemaValidations) Validations() SchemaValidations { + val := v.CommonValidations.Validations() + val.PatternProperties = v.PatternProperties + val.MinProperties = v.MinProperties + val.MaxProperties = v.MaxProperties + return val +} + +// ClearObjectValidations returns a clone of the validations with all object validations cleared. +// +// Some callbacks may be set by the caller to capture changed values. +func (v *SchemaValidations) ClearObjectValidations(cbs ...func(string, interface{})) { + done := make(clearedValidations, 0, 3) + defer func() { + done.apply(cbs) + }() + + if v.MaxProperties != nil { + done = append(done, clearedValidation{Validation: "maxProperties", Value: v.MaxProperties}) + v.MaxProperties = nil + } + if v.MinProperties != nil { + done = append(done, clearedValidation{Validation: "minProperties", Value: v.MinProperties}) + v.MinProperties = nil + } + if v.PatternProperties != nil { + done = append(done, clearedValidation{Validation: "patternProperties", Value: v.PatternProperties}) + v.PatternProperties = nil + } +} diff --git a/vendor/github.com/go-openapi/swag/.golangci.yml b/vendor/github.com/go-openapi/swag/.golangci.yml index 625c3d6aff..842ac1c095 100644 --- a/vendor/github.com/go-openapi/swag/.golangci.yml +++ b/vendor/github.com/go-openapi/swag/.golangci.yml @@ -20,3 +20,22 @@ linters: - lll - gochecknoinits - gochecknoglobals + - nlreturn + - testpackage + - wrapcheck + - gomnd + - exhaustive + - exhaustivestruct + - goerr113 + - wsl + - whitespace + - gofumpt + - godot + - nestif + - godox + - funlen + - gci + - gocognit + - paralleltest + - thelper + - ifshort diff --git a/vendor/github.com/go-openapi/swag/.travis.yml b/vendor/github.com/go-openapi/swag/.travis.yml index f1a3f80b35..fc25a88728 100644 --- a/vendor/github.com/go-openapi/swag/.travis.yml +++ b/vendor/github.com/go-openapi/swag/.travis.yml @@ -1,8 +1,32 @@ after_success: - bash <(curl -s https://codecov.io/bash) go: -- 1.13.x - 1.14.x +- 1.x +arch: +- amd64 +jobs: + include: + # include arch ppc, but only for latest go version - skip testing for race + - go: 1.x + arch: ppc64le + install: ~ + script: + - go test -v + + #- go: 1.x + # arch: arm + # install: ~ + # script: + # - go test -v + + # include linting job, but only for latest go version and amd64 arch + - go: 1.x + arch: amd64 + install: + go get github.com/golangci/golangci-lint/cmd/golangci-lint + script: + - golangci-lint run --new-from-rev master install: - GO111MODULE=off go get -u gotest.tools/gotestsum language: go diff --git a/vendor/github.com/go-openapi/swag/README.md b/vendor/github.com/go-openapi/swag/README.md index eb60ae80ab..217f6fa505 100644 --- a/vendor/github.com/go-openapi/swag/README.md +++ b/vendor/github.com/go-openapi/swag/README.md @@ -2,7 +2,6 @@ [![license](http://img.shields.io/badge/license-Apache%20v2-orange.svg)](https://raw.githubusercontent.com/go-openapi/swag/master/LICENSE) [![GoDoc](https://godoc.org/github.com/go-openapi/swag?status.svg)](http://godoc.org/github.com/go-openapi/swag) -[![GolangCI](https://golangci.com/badges/github.com/go-openapi/swag.svg)](https://golangci.com) [![Go Report Card](https://goreportcard.com/badge/github.com/go-openapi/swag)](https://goreportcard.com/report/github.com/go-openapi/swag) Contains a bunch of helper functions for go-openapi and go-swagger projects. diff --git a/vendor/github.com/go-openapi/swag/loading.go b/vendor/github.com/go-openapi/swag/loading.go index 04160b89ba..9a60409725 100644 --- a/vendor/github.com/go-openapi/swag/loading.go +++ b/vendor/github.com/go-openapi/swag/loading.go @@ -19,7 +19,9 @@ import ( "io/ioutil" "log" "net/http" + "net/url" "path/filepath" + "runtime" "strings" "time" ) @@ -57,6 +59,26 @@ func LoadStrategy(path string, local, remote func(string) ([]byte, error)) func( if err != nil { return nil, err } + + if strings.HasPrefix(pth, `file://`) { + if runtime.GOOS == "windows" { + // support for canonical file URIs on windows. + // Zero tolerance here for dodgy URIs. + u, _ := url.Parse(upth) + if u.Host != "" { + // assume UNC name (volume share) + // file://host/share/folder\... ==> \\host\share\path\folder + // NOTE: UNC port not yet supported + upth = strings.Join([]string{`\`, u.Host, u.Path}, `\`) + } else { + // file:///c:/folder/... ==> just remove the leading slash + upth = strings.TrimPrefix(upth, `file:///`) + } + } else { + upth = strings.TrimPrefix(upth, `file://`) + } + } + return local(filepath.FromSlash(upth)) } } @@ -64,7 +86,7 @@ func LoadStrategy(path string, local, remote func(string) ([]byte, error)) func( func loadHTTPBytes(timeout time.Duration) func(path string) ([]byte, error) { return func(path string) ([]byte, error) { client := &http.Client{Timeout: timeout} - req, err := http.NewRequest("GET", path, nil) + req, err := http.NewRequest("GET", path, nil) // nolint: noctx if err != nil { return nil, err } diff --git a/vendor/github.com/go-openapi/swag/util.go b/vendor/github.com/go-openapi/swag/util.go index 9eac16afb2..193702f2ce 100644 --- a/vendor/github.com/go-openapi/swag/util.go +++ b/vendor/github.com/go-openapi/swag/util.go @@ -31,7 +31,7 @@ var isInitialism func(string) bool // GoNamePrefixFunc sets an optional rule to prefix go names // which do not start with a letter. // -// e.g. to help converting "123" into "{prefix}123" +// e.g. to help convert "123" into "{prefix}123" // // The default is to prefix with "X" var GoNamePrefixFunc func(string) string @@ -91,7 +91,7 @@ func init() { } const ( - //collectionFormatComma = "csv" + // collectionFormatComma = "csv" collectionFormatSpace = "ssv" collectionFormatTab = "tsv" collectionFormatPipe = "pipes" @@ -370,7 +370,7 @@ func IsZero(data interface{}) bool { // AddInitialisms add additional initialisms func AddInitialisms(words ...string) { for _, word := range words { - //commonInitialisms[upper(word)] = true + // commonInitialisms[upper(word)] = true commonInitialisms.add(upper(word)) } // sort again diff --git a/vendor/github.com/josharian/intern/README.md b/vendor/github.com/josharian/intern/README.md new file mode 100644 index 0000000000..ffc44b219b --- /dev/null +++ b/vendor/github.com/josharian/intern/README.md @@ -0,0 +1,5 @@ +Docs: https://godoc.org/github.com/josharian/intern + +See also [Go issue 5160](https://golang.org/issue/5160). + +License: MIT diff --git a/vendor/github.com/josharian/intern/intern.go b/vendor/github.com/josharian/intern/intern.go new file mode 100644 index 0000000000..7acb1fe90a --- /dev/null +++ b/vendor/github.com/josharian/intern/intern.go @@ -0,0 +1,44 @@ +// Package intern interns strings. +// Interning is best effort only. +// Interned strings may be removed automatically +// at any time without notification. +// All functions may be called concurrently +// with themselves and each other. +package intern + +import "sync" + +var ( + pool sync.Pool = sync.Pool{ + New: func() interface{} { + return make(map[string]string) + }, + } +) + +// String returns s, interned. +func String(s string) string { + m := pool.Get().(map[string]string) + c, ok := m[s] + if ok { + pool.Put(m) + return c + } + m[s] = s + pool.Put(m) + return s +} + +// Bytes returns b converted to a string, interned. +func Bytes(b []byte) string { + m := pool.Get().(map[string]string) + c, ok := m[string(b)] + if ok { + pool.Put(m) + return c + } + s := string(b) + m[s] = s + pool.Put(m) + return s +} diff --git a/vendor/github.com/josharian/intern/license.md b/vendor/github.com/josharian/intern/license.md new file mode 100644 index 0000000000..353d3055f0 --- /dev/null +++ b/vendor/github.com/josharian/intern/license.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Josh Bleecher Snyder + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/mailru/easyjson/buffer/pool.go b/vendor/github.com/mailru/easyjson/buffer/pool.go index 07fb4bc1f7..598a54af9d 100644 --- a/vendor/github.com/mailru/easyjson/buffer/pool.go +++ b/vendor/github.com/mailru/easyjson/buffer/pool.go @@ -4,6 +4,7 @@ package buffer import ( "io" + "net" "sync" ) @@ -52,14 +53,12 @@ func putBuf(buf []byte) { // getBuf gets a chunk from reuse pool or creates a new one if reuse failed. func getBuf(size int) []byte { - if size < config.PooledSize { - return make([]byte, 0, size) - } - - if c := buffers[size]; c != nil { - v := c.Get() - if v != nil { - return v.([]byte) + if size >= config.PooledSize { + if c := buffers[size]; c != nil { + v := c.Get() + if v != nil { + return v.([]byte) + } } } return make([]byte, 0, size) @@ -78,9 +77,12 @@ type Buffer struct { // EnsureSpace makes sure that the current chunk contains at least s free bytes, // possibly creating a new chunk. func (b *Buffer) EnsureSpace(s int) { - if cap(b.Buf)-len(b.Buf) >= s { - return + if cap(b.Buf)-len(b.Buf) < s { + b.ensureSpaceSlow(s) } +} + +func (b *Buffer) ensureSpaceSlow(s int) { l := len(b.Buf) if l > 0 { if cap(b.toPool) != cap(b.Buf) { @@ -105,18 +107,22 @@ func (b *Buffer) EnsureSpace(s int) { // AppendByte appends a single byte to buffer. func (b *Buffer) AppendByte(data byte) { - if cap(b.Buf) == len(b.Buf) { // EnsureSpace won't be inlined. - b.EnsureSpace(1) - } + b.EnsureSpace(1) b.Buf = append(b.Buf, data) } // AppendBytes appends a byte slice to buffer. func (b *Buffer) AppendBytes(data []byte) { + if len(data) <= cap(b.Buf)-len(b.Buf) { + b.Buf = append(b.Buf, data...) // fast path + } else { + b.appendBytesSlow(data) + } +} + +func (b *Buffer) appendBytesSlow(data []byte) { for len(data) > 0 { - if cap(b.Buf) == len(b.Buf) { // EnsureSpace won't be inlined. - b.EnsureSpace(1) - } + b.EnsureSpace(1) sz := cap(b.Buf) - len(b.Buf) if sz > len(data) { @@ -128,12 +134,18 @@ func (b *Buffer) AppendBytes(data []byte) { } } -// AppendBytes appends a string to buffer. +// AppendString appends a string to buffer. func (b *Buffer) AppendString(data string) { + if len(data) <= cap(b.Buf)-len(b.Buf) { + b.Buf = append(b.Buf, data...) // fast path + } else { + b.appendStringSlow(data) + } +} + +func (b *Buffer) appendStringSlow(data string) { for len(data) > 0 { - if cap(b.Buf) == len(b.Buf) { // EnsureSpace won't be inlined. - b.EnsureSpace(1) - } + b.EnsureSpace(1) sz := cap(b.Buf) - len(b.Buf) if sz > len(data) { @@ -156,18 +168,14 @@ func (b *Buffer) Size() int { // DumpTo outputs the contents of a buffer to a writer and resets the buffer. func (b *Buffer) DumpTo(w io.Writer) (written int, err error) { - var n int - for _, buf := range b.bufs { - if err == nil { - n, err = w.Write(buf) - written += n - } - putBuf(buf) + bufs := net.Buffers(b.bufs) + if len(b.Buf) > 0 { + bufs = append(bufs, b.Buf) } + n, err := bufs.WriteTo(w) - if err == nil { - n, err = w.Write(b.Buf) - written += n + for _, buf := range b.bufs { + putBuf(buf) } putBuf(b.toPool) @@ -175,7 +183,7 @@ func (b *Buffer) DumpTo(w io.Writer) (written int, err error) { b.Buf = nil b.toPool = nil - return + return int(n), err } // BuildBytes creates a single byte slice with all the contents of the buffer. Data is @@ -192,7 +200,7 @@ func (b *Buffer) BuildBytes(reuse ...[]byte) []byte { var ret []byte size := b.Size() - // If we got a buffer as argument and it is big enought, reuse it. + // If we got a buffer as argument and it is big enough, reuse it. if len(reuse) == 1 && cap(reuse[0]) >= size { ret = reuse[0][:0] } else { diff --git a/vendor/github.com/mailru/easyjson/jlexer/lexer.go b/vendor/github.com/mailru/easyjson/jlexer/lexer.go index ddd376b844..b5f5e26132 100644 --- a/vendor/github.com/mailru/easyjson/jlexer/lexer.go +++ b/vendor/github.com/mailru/easyjson/jlexer/lexer.go @@ -5,6 +5,7 @@ package jlexer import ( + "bytes" "encoding/base64" "encoding/json" "errors" @@ -14,6 +15,8 @@ import ( "unicode" "unicode/utf16" "unicode/utf8" + + "github.com/josharian/intern" ) // tokenKind determines type of a token. @@ -32,9 +35,10 @@ const ( type token struct { kind tokenKind // Type of a token. - boolValue bool // Value if a boolean literal token. - byteValue []byte // Raw value of a token. - delimValue byte + boolValue bool // Value if a boolean literal token. + byteValueCloned bool // true if byteValue was allocated and does not refer to original json body + byteValue []byte // Raw value of a token. + delimValue byte } // Lexer is a JSON lexer: it iterates over JSON tokens in a byte slice. @@ -240,23 +244,65 @@ func (r *Lexer) fetchNumber() { // findStringLen tries to scan into the string literal for ending quote char to determine required size. // The size will be exact if no escapes are present and may be inexact if there are escaped chars. -func findStringLen(data []byte) (isValid, hasEscapes bool, length int) { - delta := 0 - - for i := 0; i < len(data); i++ { - switch data[i] { - case '\\': - i++ - delta++ - if i < len(data) && data[i] == 'u' { - delta++ - } - case '"': - return true, (delta > 0), (i - delta) +func findStringLen(data []byte) (isValid bool, length int) { + for { + idx := bytes.IndexByte(data, '"') + if idx == -1 { + return false, len(data) + } + if idx == 0 || (idx > 0 && data[idx-1] != '\\') { + return true, length + idx + } + + // count \\\\\\\ sequences. even number of slashes means quote is not really escaped + cnt := 1 + for idx-cnt-1 >= 0 && data[idx-cnt-1] == '\\' { + cnt++ + } + if cnt%2 == 0 { + return true, length + idx + } + + length += idx + 1 + data = data[idx+1:] + } +} + +// unescapeStringToken performs unescaping of string token. +// if no escaping is needed, original string is returned, otherwise - a new one allocated +func (r *Lexer) unescapeStringToken() (err error) { + data := r.token.byteValue + var unescapedData []byte + + for { + i := bytes.IndexByte(data, '\\') + if i == -1 { + break + } + + escapedRune, escapedBytes, err := decodeEscape(data[i:]) + if err != nil { + r.errParse(err.Error()) + return err + } + + if unescapedData == nil { + unescapedData = make([]byte, 0, len(r.token.byteValue)) } + + var d [4]byte + s := utf8.EncodeRune(d[:], escapedRune) + unescapedData = append(unescapedData, data[:i]...) + unescapedData = append(unescapedData, d[:s]...) + + data = data[i+escapedBytes:] } - return false, false, len(data) + if unescapedData != nil { + r.token.byteValue = append(unescapedData, data...) + r.token.byteValueCloned = true + } + return } // getu4 decodes \uXXXX from the beginning of s, returning the hex value, @@ -286,36 +332,30 @@ func getu4(s []byte) rune { return val } -// processEscape processes a single escape sequence and returns number of bytes processed. -func (r *Lexer) processEscape(data []byte) (int, error) { +// decodeEscape processes a single escape sequence and returns number of bytes processed. +func decodeEscape(data []byte) (decoded rune, bytesProcessed int, err error) { if len(data) < 2 { - return 0, fmt.Errorf("syntax error at %v", string(data)) + return 0, 0, errors.New("incorrect escape symbol \\ at the end of token") } c := data[1] switch c { case '"', '/', '\\': - r.token.byteValue = append(r.token.byteValue, c) - return 2, nil + return rune(c), 2, nil case 'b': - r.token.byteValue = append(r.token.byteValue, '\b') - return 2, nil + return '\b', 2, nil case 'f': - r.token.byteValue = append(r.token.byteValue, '\f') - return 2, nil + return '\f', 2, nil case 'n': - r.token.byteValue = append(r.token.byteValue, '\n') - return 2, nil + return '\n', 2, nil case 'r': - r.token.byteValue = append(r.token.byteValue, '\r') - return 2, nil + return '\r', 2, nil case 't': - r.token.byteValue = append(r.token.byteValue, '\t') - return 2, nil + return '\t', 2, nil case 'u': rr := getu4(data) if rr < 0 { - return 0, errors.New("syntax error") + return 0, 0, errors.New("incorrectly escaped \\uXXXX sequence") } read := 6 @@ -328,13 +368,10 @@ func (r *Lexer) processEscape(data []byte) (int, error) { rr = unicode.ReplacementChar } } - var d [4]byte - s := utf8.EncodeRune(d[:], rr) - r.token.byteValue = append(r.token.byteValue, d[:s]...) - return read, nil + return rr, read, nil } - return 0, errors.New("syntax error") + return 0, 0, errors.New("incorrectly escaped bytes") } // fetchString scans a string literal token. @@ -342,43 +379,14 @@ func (r *Lexer) fetchString() { r.pos++ data := r.Data[r.pos:] - isValid, hasEscapes, length := findStringLen(data) + isValid, length := findStringLen(data) if !isValid { r.pos += length r.errParse("unterminated string literal") return } - if !hasEscapes { - r.token.byteValue = data[:length] - r.pos += length + 1 - return - } - - r.token.byteValue = make([]byte, 0, length) - p := 0 - for i := 0; i < len(data); { - switch data[i] { - case '"': - r.pos += i + 1 - r.token.byteValue = append(r.token.byteValue, data[p:i]...) - i++ - return - - case '\\': - r.token.byteValue = append(r.token.byteValue, data[p:i]...) - off, err := r.processEscape(data[i:]) - if err != nil { - r.errParse(err.Error()) - return - } - i += off - p = i - - default: - i++ - } - } - r.errParse("unterminated string literal") + r.token.byteValue = data[:length] + r.pos += length + 1 // skip closing '"' as well } // scanToken scans the next token if no token is currently available in the lexer. @@ -393,6 +401,7 @@ func (r *Lexer) scanToken() { // consume resets the current token to allow scanning the next one. func (r *Lexer) consume() { r.token.kind = tokenUndef + r.token.byteValueCloned = false r.token.delimValue = 0 } @@ -520,6 +529,7 @@ func (r *Lexer) Skip() { func (r *Lexer) SkipRecursive() { r.scanToken() var start, end byte + startPos := r.start switch r.token.delimValue { case '{': @@ -545,6 +555,14 @@ func (r *Lexer) SkipRecursive() { level-- if level == 0 { r.pos += i + 1 + if !json.Valid(r.Data[startPos:r.pos]) { + r.pos = len(r.Data) + r.fatalError = &LexerError{ + Reason: "skipped array/object json value is invalid", + Offset: r.pos, + Data: string(r.Data[r.pos:]), + } + } return } case c == '\\' && inQuotes: @@ -602,7 +620,7 @@ func (r *Lexer) Consumed() { } } -func (r *Lexer) unsafeString() (string, []byte) { +func (r *Lexer) unsafeString(skipUnescape bool) (string, []byte) { if r.token.kind == tokenUndef && r.Ok() { r.FetchToken() } @@ -610,6 +628,13 @@ func (r *Lexer) unsafeString() (string, []byte) { r.errInvalidToken("string") return "", nil } + if !skipUnescape { + if err := r.unescapeStringToken(); err != nil { + r.errInvalidToken("string") + return "", nil + } + } + bytes := r.token.byteValue ret := bytesToStr(r.token.byteValue) r.consume() @@ -621,13 +646,19 @@ func (r *Lexer) unsafeString() (string, []byte) { // Warning: returned string may point to the input buffer, so the string should not outlive // the input buffer. Intended pattern of usage is as an argument to a switch statement. func (r *Lexer) UnsafeString() string { - ret, _ := r.unsafeString() + ret, _ := r.unsafeString(false) return ret } // UnsafeBytes returns the byte slice if the token is a string literal. func (r *Lexer) UnsafeBytes() []byte { - _, ret := r.unsafeString() + _, ret := r.unsafeString(false) + return ret +} + +// UnsafeFieldName returns current member name string token +func (r *Lexer) UnsafeFieldName(skipUnescape bool) string { + ret, _ := r.unsafeString(skipUnescape) return ret } @@ -640,7 +671,34 @@ func (r *Lexer) String() string { r.errInvalidToken("string") return "" } - ret := string(r.token.byteValue) + if err := r.unescapeStringToken(); err != nil { + r.errInvalidToken("string") + return "" + } + var ret string + if r.token.byteValueCloned { + ret = bytesToStr(r.token.byteValue) + } else { + ret = string(r.token.byteValue) + } + r.consume() + return ret +} + +// StringIntern reads a string literal, and performs string interning on it. +func (r *Lexer) StringIntern() string { + if r.token.kind == tokenUndef && r.Ok() { + r.FetchToken() + } + if !r.Ok() || r.token.kind != tokenString { + r.errInvalidToken("string") + return "" + } + if err := r.unescapeStringToken(); err != nil { + r.errInvalidToken("string") + return "" + } + ret := intern.Bytes(r.token.byteValue) r.consume() return ret } @@ -654,6 +712,10 @@ func (r *Lexer) Bytes() []byte { r.errInvalidToken("string") return nil } + if err := r.unescapeStringToken(); err != nil { + r.errInvalidToken("string") + return nil + } ret := make([]byte, base64.StdEncoding.DecodedLen(len(r.token.byteValue))) n, err := base64.StdEncoding.Decode(ret, r.token.byteValue) if err != nil { @@ -839,7 +901,7 @@ func (r *Lexer) Int() int { } func (r *Lexer) Uint8Str() uint8 { - s, b := r.unsafeString() + s, b := r.unsafeString(false) if !r.Ok() { return 0 } @@ -856,7 +918,7 @@ func (r *Lexer) Uint8Str() uint8 { } func (r *Lexer) Uint16Str() uint16 { - s, b := r.unsafeString() + s, b := r.unsafeString(false) if !r.Ok() { return 0 } @@ -873,7 +935,7 @@ func (r *Lexer) Uint16Str() uint16 { } func (r *Lexer) Uint32Str() uint32 { - s, b := r.unsafeString() + s, b := r.unsafeString(false) if !r.Ok() { return 0 } @@ -890,7 +952,7 @@ func (r *Lexer) Uint32Str() uint32 { } func (r *Lexer) Uint64Str() uint64 { - s, b := r.unsafeString() + s, b := r.unsafeString(false) if !r.Ok() { return 0 } @@ -915,7 +977,7 @@ func (r *Lexer) UintptrStr() uintptr { } func (r *Lexer) Int8Str() int8 { - s, b := r.unsafeString() + s, b := r.unsafeString(false) if !r.Ok() { return 0 } @@ -932,7 +994,7 @@ func (r *Lexer) Int8Str() int8 { } func (r *Lexer) Int16Str() int16 { - s, b := r.unsafeString() + s, b := r.unsafeString(false) if !r.Ok() { return 0 } @@ -949,7 +1011,7 @@ func (r *Lexer) Int16Str() int16 { } func (r *Lexer) Int32Str() int32 { - s, b := r.unsafeString() + s, b := r.unsafeString(false) if !r.Ok() { return 0 } @@ -966,7 +1028,7 @@ func (r *Lexer) Int32Str() int32 { } func (r *Lexer) Int64Str() int64 { - s, b := r.unsafeString() + s, b := r.unsafeString(false) if !r.Ok() { return 0 } @@ -1004,7 +1066,7 @@ func (r *Lexer) Float32() float32 { } func (r *Lexer) Float32Str() float32 { - s, b := r.unsafeString() + s, b := r.unsafeString(false) if !r.Ok() { return 0 } @@ -1037,7 +1099,7 @@ func (r *Lexer) Float64() float64 { } func (r *Lexer) Float64Str() float64 { - s, b := r.unsafeString() + s, b := r.unsafeString(false) if !r.Ok() { return 0 } diff --git a/vendor/github.com/mailru/easyjson/jwriter/writer.go b/vendor/github.com/mailru/easyjson/jwriter/writer.go index eb8547ccc2..2c5b20105b 100644 --- a/vendor/github.com/mailru/easyjson/jwriter/writer.go +++ b/vendor/github.com/mailru/easyjson/jwriter/writer.go @@ -297,11 +297,9 @@ func (w *Writer) String(s string) { p := 0 // last non-escape symbol - var escapeTable [128]bool + escapeTable := &htmlEscapeTable if w.NoEscapeHTML { - escapeTable = htmlNoEscapeTable - } else { - escapeTable = htmlEscapeTable + escapeTable = &htmlNoEscapeTable } for i := 0; i < len(s); { diff --git a/vendor/github.com/swaggo/swag/.gitignore b/vendor/github.com/swaggo/swag/.gitignore index bea8db6819..865a6f357f 100644 --- a/vendor/github.com/swaggo/swag/.gitignore +++ b/vendor/github.com/swaggo/swag/.gitignore @@ -1,5 +1,9 @@ dist testdata/simple*/docs +testdata/quotes/docs +testdata/quotes/quotes.so +testdata/delims/docs +testdata/delims/delims.so example/basic/docs/* example/celler/docs/* cover.out @@ -16,5 +20,5 @@ cover.out # Etc .DS_Store -swag -swag.exe +/swag +/swag.exe diff --git a/vendor/github.com/swaggo/swag/.goreleaser.yml b/vendor/github.com/swaggo/swag/.goreleaser.yml index d0b576ab1d..a431d5db6c 100644 --- a/vendor/github.com/swaggo/swag/.goreleaser.yml +++ b/vendor/github.com/swaggo/swag/.goreleaser.yml @@ -1,12 +1,23 @@ build: main: cmd/swag/main.go -archive: - replacements: - darwin: Darwin - linux: Linux - windows: Windows - 386: i386 - amd64: x86_64 + goos: + - linux + - darwin + goarch: + - amd64 + - arm64 + - 386 + env: + - CGO_ENABLED=0 + +archives: + - id: foo + name_template: >- + {{ .ProjectName }}_ + {{- .Version }}_ + {{- if eq .Os "linux"}}Linux{{ else if eq .Os "darwin"}}Darwin{{ else }}{{ .Os }}{{ end }}_ + {{- if eq .Arch "386" }}i386{{ else if eq .Arch "amd64" }}x86_64{{ else }}{{ .Arch }}{{ end }} + checksum: name_template: 'checksums.txt' snapshot: diff --git a/vendor/github.com/swaggo/swag/.travis.yml b/vendor/github.com/swaggo/swag/.travis.yml deleted file mode 100644 index 36a51fd720..0000000000 --- a/vendor/github.com/swaggo/swag/.travis.yml +++ /dev/null @@ -1,18 +0,0 @@ -language: go - -go: - - 1.13.x - - 1.14.x - -install: - - make deps - -script: - - make fmt-check - - make lint - - make vet - - make build - - make test - -after_success: - - bash <(curl -s https://codecov.io/bash) diff --git a/vendor/github.com/swaggo/swag/Dockerfile b/vendor/github.com/swaggo/swag/Dockerfile index 0a5d9b8db9..410fe31efc 100644 --- a/vendor/github.com/swaggo/swag/Dockerfile +++ b/vendor/github.com/swaggo/swag/Dockerfile @@ -1,7 +1,7 @@ # Dockerfile References: https://docs.docker.com/engine/reference/builder/ # Start from the latest golang base image -FROM golang:1.14-alpine as builder +FROM golang:1.20-alpine as builder # Set the Current Working Directory inside the container WORKDIR /app @@ -22,8 +22,9 @@ RUN CGO_ENABLED=0 GOOS=linux go build -v -a -installsuffix cgo -o swag cmd/swag/ ######## Start a new stage from scratch ####### FROM scratch -WORKDIR /root/ +WORKDIR /code/ # Copy the Pre-built binary file from the previous stage -COPY --from=builder /app/swag . +COPY --from=builder /app/swag /bin/swag +ENTRYPOINT ["/bin/swag"] diff --git a/vendor/github.com/swaggo/swag/Makefile b/vendor/github.com/swaggo/swag/Makefile index f78299612d..0d8175da78 100644 --- a/vendor/github.com/swaggo/swag/Makefile +++ b/vendor/github.com/swaggo/swag/Makefile @@ -6,6 +6,7 @@ GOBUILD:=$(GOCMD) build GOINSTALL:=$(GOCMD) install GOCLEAN:=$(GOCMD) clean GOTEST:=$(GOCMD) test +GOMODTIDY:=$(GOCMD) mod tidy GOGET:=$(GOCMD) get GOLIST:=$(GOCMD) list GOVET:=$(GOCMD) vet @@ -13,7 +14,7 @@ GOPATH:=$(shell $(GOCMD) env GOPATH) u := $(if $(update),-u) BINARY_NAME:=swag -PACKAGES:=$(shell $(GOLIST) github.com/swaggo/swag github.com/swaggo/swag/cmd/swag github.com/swaggo/swag/gen) +PACKAGES:=$(shell $(GOLIST) github.com/swaggo/swag github.com/swaggo/swag/cmd/swag github.com/swaggo/swag/gen github.com/swaggo/swag/format) GOFILES:=$(shell find . -name "*.go" -type f) export GO111MODULE := on @@ -54,20 +55,12 @@ clean: .PHONY: deps deps: - $(GOGET) github.com/swaggo/cli - $(GOGET) github.com/ghodss/yaml - $(GOGET) github.com/gin-gonic/gin - $(GOGET) github.com/KyleBanks/depth - $(GOGET) github.com/go-openapi/jsonreference - $(GOGET) github.com/go-openapi/spec - $(GOGET) github.com/stretchr/testify/assert - $(GOGET) github.com/alecthomas/template - $(GOGET) golang.org/x/tools/go/loader + $(GOMODTIDY) .PHONY: devel-deps -devel-deps: +devel-deps: GO111MODULE=off $(GOGET) -v -u \ - golang.org/x/lint/golint + golang.org/x/lint/golint .PHONY: lint lint: devel-deps @@ -93,4 +86,4 @@ fmt-check: .PHONY: view-covered view-covered: $(GOTEST) -coverprofile=cover.out $(TARGET) - $(GOCMD) tool cover -html=cover.out \ No newline at end of file + $(GOCMD) tool cover -html=cover.out diff --git a/vendor/github.com/swaggo/swag/README.md b/vendor/github.com/swaggo/swag/README.md index 5edbd5d5aa..054e36ec6c 100644 --- a/vendor/github.com/swaggo/swag/README.md +++ b/vendor/github.com/swaggo/swag/README.md @@ -1,15 +1,15 @@ # swag -🌍 *[English](README.md) ∙ [简体中文](README_zh-CN.md)* +🌍 *[English](README.md) ∙ [简体中文](README_zh-CN.md) ∙ [Português](README_pt.md)* -[![Travis Status](https://img.shields.io/travis/swaggo/swag/master.svg)](https://travis-ci.org/swaggo/swag) +[![Build Status](https://github.com/swaggo/swag/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/features/actions) [![Coverage Status](https://img.shields.io/codecov/c/github/swaggo/swag/master.svg)](https://codecov.io/gh/swaggo/swag) [![Go Report Card](https://goreportcard.com/badge/github.com/swaggo/swag)](https://goreportcard.com/report/github.com/swaggo/swag) [![codebeat badge](https://codebeat.co/badges/71e2f5e5-9e6b-405d-baf9-7cc8b5037330)](https://codebeat.co/projects/github-com-swaggo-swag-master) [![Go Doc](https://godoc.org/github.com/swaggo/swagg?status.svg)](https://godoc.org/github.com/swaggo/swag) -[![Backers on Open Collective](https://opencollective.com/swag/backers/badge.svg)](#backers) +[![Backers on Open Collective](https://opencollective.com/swag/backers/badge.svg)](#backers) [![Sponsors on Open Collective](https://opencollective.com/swag/sponsors/badge.svg)](#sponsors) [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fswaggo%2Fswag.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Fswaggo%2Fswag?ref=badge_shield) [![Release](https://img.shields.io/github/release/swaggo/swag.svg?style=flat-square)](https://github.com/swaggo/swag/releases) @@ -20,6 +20,7 @@ Swag converts Go annotations to Swagger Documentation 2.0. We've created a varie - [Getting started](#getting-started) - [Supported Web Frameworks](#supported-web-frameworks) - [How to use it with Gin](#how-to-use-it-with-gin) + - [The swag formatter](#the-swag-formatter) - [Implementation Status](#implementation-status) - [Declarative Comments Format](#declarative-comments-format) - [General API Info](#general-api-info) @@ -28,44 +29,64 @@ Swag converts Go annotations to Swagger Documentation 2.0. We've created a varie - [Examples](#examples) - [Descriptions over multiple lines](#descriptions-over-multiple-lines) - [User defined structure with an array type](#user-defined-structure-with-an-array-type) + - [Function scoped struct declaration](#function-scoped-struct-declaration) - [Model composition in response](#model-composition-in-response) - - [Add a headers in response](#add-a-headers-in-response) + - [Add a headers in response](#add-a-headers-in-response) - [Use multiple path params](#use-multiple-path-params) - [Example value of struct](#example-value-of-struct) + - [SchemaExample of body](#schemaexample-of-body) - [Description of struct](#description-of-struct) - [Use swaggertype tag to supported custom type](#use-swaggertype-tag-to-supported-custom-type) + - [Use global overrides to support a custom type](#use-global-overrides-to-support-a-custom-type) - [Use swaggerignore tag to exclude a field](#use-swaggerignore-tag-to-exclude-a-field) - [Add extension info to struct field](#add-extension-info-to-struct-field) - [Rename model to display](#rename-model-to-display) - - [How to using security annotations](#how-to-using-security-annotations) + - [How to use security annotations](#how-to-use-security-annotations) + - [Add a description for enum items](#add-a-description-for-enum-items) + - [Generate only specific docs file types](#generate-only-specific-docs-file-types) + - [How to use Go generic types](#how-to-use-generics) - [About the Project](#about-the-project) ## Getting started 1. Add comments to your API source code, See [Declarative Comments Format](#declarative-comments-format). -2. Download swag by using: +2. Install swag by using: ```sh -$ go get -u github.com/swaggo/swag/cmd/swag +go install github.com/swaggo/swag/cmd/swag@latest +``` +To build from source you need [Go](https://golang.org/dl/) (1.18 or newer). + +Alternatively you can run the docker image: +```sh +docker run --rm -v $(pwd):/code ghcr.io/swaggo/swag:latest ``` -To build from source you need [Go](https://golang.org/dl/) (1.9 or newer). Or download a pre-compiled binary from the [release page](https://github.com/swaggo/swag/releases). 3. Run `swag init` in the project's root folder which contains the `main.go` file. This will parse your comments and generate the required files (`docs` folder and `docs/docs.go`). ```sh -$ swag init +swag init ``` Make sure to import the generated `docs/docs.go` so that your specific configuration gets `init`'ed. If your General API annotations do not live in `main.go`, you can let swag know with `-g` flag. + ```go + import _ "example-module-name/docs" + ``` ```sh swag init -g http/api.go ``` +4. (optional) Use `swag fmt` format the SWAG comment. (Please upgrade to the latest version) + + ```sh + swag fmt + ``` + ## swag cli ```sh -$ swag init -h +swag init -h NAME: swag init - Create docs.go @@ -73,14 +94,45 @@ USAGE: swag init [command options] [arguments...] OPTIONS: - --generalInfo value, -g value Go file path in which 'swagger general API Info' is written (default: "main.go") - --dir value, -d value Directory you want to parse (default: "./") - --exclude value Exclude directoies and files, comma separated - --propertyStrategy value, -p value Property Naming Strategy like snakecase,camelcase,pascalcase (default: "camelcase") - --output value, -o value Output directory for all the generated files(swagger.json, swagger.yaml and doc.go) (default: "./docs") - --parseVendor Parse go files in 'vendor' folder, disabled by default - --parseDependency Parse go files in outside dependency folder, disabled by default - --parseInternal Parse go files in internal packages, disabled by default + --quiet, -q Make the logger quiet. (default: false) + --generalInfo value, -g value Go file path in which 'swagger general API Info' is written (default: "main.go") + --dir value, -d value Directories you want to parse,comma separated and general-info file must be in the first one (default: "./") + --exclude value Exclude directories and files when searching, comma separated + --propertyStrategy value, -p value Property Naming Strategy like snakecase,camelcase,pascalcase (default: "camelcase") + --output value, -o value Output directory for all the generated files(swagger.json, swagger.yaml and docs.go) (default: "./docs") + --outputTypes value, --ot value Output types of generated files (docs.go, swagger.json, swagger.yaml) like go,json,yaml (default: "go,json,yaml") + --parseVendor Parse go files in 'vendor' folder, disabled by default (default: false) + --parseDependency, --pd Parse go files inside dependency folder, disabled by default (default: false) + --markdownFiles value, --md value Parse folder containing markdown files to use as description, disabled by default + --codeExampleFiles value, --cef value Parse folder containing code example files to use for the x-codeSamples extension, disabled by default + --parseInternal Parse go files in internal packages, disabled by default (default: false) + --generatedTime Generate timestamp at the top of docs.go, disabled by default (default: false) + --parseDepth value Dependency parse depth (default: 100) + --requiredByDefault Set validation required for all fields by default (default: false) + --instanceName value This parameter can be used to name different swagger document instances. It is optional. + --overridesFile value File to read global type overrides from. (default: ".swaggo") + --parseGoList Parse dependency via 'go list' (default: true) + --tags value, -t value A comma-separated list of tags to filter the APIs for which the documentation is generated.Special case if the tag is prefixed with the '!' character then the APIs with that tag will be excluded + --templateDelims value, --td value Provide custom delimeters for Go template generation. The format is leftDelim,rightDelim. For example: "[[,]]" + --collectionFormat value, --cf value Set default collection format (default: "csv") + --state value Initial state for the state machine (default: ""), @HostState in root file, @State in other files + --help, -h show help (default: false) +``` + +```bash +swag fmt -h +NAME: + swag fmt - format swag comments + +USAGE: + swag fmt [command options] [arguments...] + +OPTIONS: + --dir value, -d value Directories you want to parse,comma separated and general-info file must be in the first one (default: "./") + --exclude value Exclude directories and files when searching, comma separated + --generalInfo value, -g value Go file path in which 'swagger general API Info' is written (default: "main.go") + --help, -h show help (default: false) + ``` ## Supported Web Frameworks @@ -89,13 +141,18 @@ OPTIONS: - [echo](http://github.com/swaggo/echo-swagger) - [buffalo](https://github.com/swaggo/buffalo-swagger) - [net/http](https://github.com/swaggo/http-swagger) +- [gorilla/mux](https://github.com/swaggo/http-swagger) +- [go-chi/chi](https://github.com/swaggo/http-swagger) - [flamingo](https://github.com/i-love-flamingo/swagger) -- [fiber](https://github.com/arsmn/fiber-swagger) +- [fiber](https://github.com/gofiber/swagger) +- [atreugo](https://github.com/Nerzal/atreugo-swagger) +- [hertz](https://github.com/hertz-contrib/swagger) ## How to use it with Gin Find the example source code [here](https://github.com/swaggo/swag/tree/master/example/celler). +Finish the steps in [Getting started](#getting-started) 1. After using `swag init` to generate Swagger 2.0 docs, import the following packages: ```go import "github.com/swaggo/gin-swagger" // gin-swagger middleware @@ -105,51 +162,25 @@ import "github.com/swaggo/files" // swagger embed files 2. Add [General API](#general-api-info) annotations in `main.go` code: ```go -// @title Swagger Example API -// @version 1.0 -// @description This is a sample server celler server. -// @termsOfService http://swagger.io/terms/ - -// @contact.name API Support -// @contact.url http://www.swagger.io/support -// @contact.email support@swagger.io - -// @license.name Apache 2.0 -// @license.url http://www.apache.org/licenses/LICENSE-2.0.html - -// @host localhost:8080 -// @BasePath /api/v1 -// @query.collection.format multi +// @title Swagger Example API +// @version 1.0 +// @description This is a sample server celler server. +// @termsOfService http://swagger.io/terms/ -// @securityDefinitions.basic BasicAuth +// @contact.name API Support +// @contact.url http://www.swagger.io/support +// @contact.email support@swagger.io -// @securityDefinitions.apikey ApiKeyAuth -// @in header -// @name Authorization +// @license.name Apache 2.0 +// @license.url http://www.apache.org/licenses/LICENSE-2.0.html -// @securitydefinitions.oauth2.application OAuth2Application -// @tokenUrl https://example.com/oauth/token -// @scope.write Grants write access -// @scope.admin Grants read and write access to administrative information +// @host localhost:8080 +// @BasePath /api/v1 -// @securitydefinitions.oauth2.implicit OAuth2Implicit -// @authorizationurl https://example.com/oauth/authorize -// @scope.write Grants write access -// @scope.admin Grants read and write access to administrative information - -// @securitydefinitions.oauth2.password OAuth2Password -// @tokenUrl https://example.com/oauth/token -// @scope.read Grants read access -// @scope.write Grants write access -// @scope.admin Grants read and write access to administrative information - -// @securitydefinitions.oauth2.accessCode OAuth2AccessCode -// @tokenUrl https://example.com/oauth/token -// @authorizationurl https://example.com/oauth/authorize -// @scope.admin Grants read and write access to administrative information - -// @x-extension-openapi {"example": "value on a json format"} +// @securityDefinitions.basic BasicAuth +// @externalDocs.description OpenAPI +// @externalDocs.url https://swagger.io/resources/open-api/ func main() { r := gin.Default() @@ -174,7 +205,7 @@ func main() { //... ``` -Additionally some general API info can be set dynamically. The generated code package `docs` exports `SwaggerInfo` variable which we can use to set the title, description, version, host and base path programatically. Example using Gin: +Additionally some general API info can be set dynamically. The generated code package `docs` exports `SwaggerInfo` variable which we can use to set the title, description, version, host and base path programmatically. Example using Gin: ```go package main @@ -183,29 +214,26 @@ import ( "github.com/gin-gonic/gin" "github.com/swaggo/files" "github.com/swaggo/gin-swagger" - + "./docs" // docs is generated by Swag CLI, you have to import it. ) -// @contact.name API Support -// @contact.url http://www.swagger.io/support -// @contact.email support@swagger.io - -// @license.name Apache 2.0 -// @license.url http://www.apache.org/licenses/LICENSE-2.0.html - -// @termsOfService http://swagger.io/terms/ +// @contact.name API Support +// @contact.url http://www.swagger.io/support +// @contact.email support@swagger.io +// @license.name Apache 2.0 +// @license.url http://www.apache.org/licenses/LICENSE-2.0.html func main() { - // programatically set swagger info + // programmatically set swagger info docs.SwaggerInfo.Title = "Swagger Example API" docs.SwaggerInfo.Description = "This is a sample server Petstore server." docs.SwaggerInfo.Version = "1.0" docs.SwaggerInfo.Host = "petstore.swagger.io" docs.SwaggerInfo.BasePath = "/v2" docs.SwaggerInfo.Schemes = []string{"http", "https"} - + r := gin.New() // use ginSwagger middleware to serve the API docs @@ -221,76 +249,111 @@ func main() { package controller import ( - "fmt" - "net/http" - "strconv" + "fmt" + "net/http" + "strconv" - "github.com/gin-gonic/gin" - "github.com/swaggo/swag/example/celler/httputil" - "github.com/swaggo/swag/example/celler/model" + "github.com/gin-gonic/gin" + "github.com/swaggo/swag/example/celler/httputil" + "github.com/swaggo/swag/example/celler/model" ) // ShowAccount godoc -// @Summary Show a account -// @Description get string by ID -// @ID get-string-by-int -// @Accept json -// @Produce json -// @Param id path int true "Account ID" -// @Success 200 {object} model.Account -// @Header 200 {string} Token "qwerty" -// @Failure 400 {object} httputil.HTTPError -// @Failure 404 {object} httputil.HTTPError -// @Failure 500 {object} httputil.HTTPError -// @Router /accounts/{id} [get] +// @Summary Show an account +// @Description get string by ID +// @Tags accounts +// @Accept json +// @Produce json +// @Param id path int true "Account ID" +// @Success 200 {object} model.Account +// @Failure 400 {object} httputil.HTTPError +// @Failure 404 {object} httputil.HTTPError +// @Failure 500 {object} httputil.HTTPError +// @Router /accounts/{id} [get] func (c *Controller) ShowAccount(ctx *gin.Context) { - id := ctx.Param("id") - aid, err := strconv.Atoi(id) - if err != nil { - httputil.NewError(ctx, http.StatusBadRequest, err) - return - } - account, err := model.AccountOne(aid) - if err != nil { - httputil.NewError(ctx, http.StatusNotFound, err) - return - } - ctx.JSON(http.StatusOK, account) + id := ctx.Param("id") + aid, err := strconv.Atoi(id) + if err != nil { + httputil.NewError(ctx, http.StatusBadRequest, err) + return + } + account, err := model.AccountOne(aid) + if err != nil { + httputil.NewError(ctx, http.StatusNotFound, err) + return + } + ctx.JSON(http.StatusOK, account) } // ListAccounts godoc -// @Summary List accounts -// @Description get accounts -// @Accept json -// @Produce json -// @Param q query string false "name search by q" -// @Success 200 {array} model.Account -// @Header 200 {string} Token "qwerty" -// @Failure 400 {object} httputil.HTTPError -// @Failure 404 {object} httputil.HTTPError -// @Failure 500 {object} httputil.HTTPError -// @Router /accounts [get] +// @Summary List accounts +// @Description get accounts +// @Tags accounts +// @Accept json +// @Produce json +// @Param q query string false "name search by q" Format(email) +// @Success 200 {array} model.Account +// @Failure 400 {object} httputil.HTTPError +// @Failure 404 {object} httputil.HTTPError +// @Failure 500 {object} httputil.HTTPError +// @Router /accounts [get] func (c *Controller) ListAccounts(ctx *gin.Context) { - q := ctx.Request.URL.Query().Get("q") - accounts, err := model.AccountsAll(q) - if err != nil { - httputil.NewError(ctx, http.StatusNotFound, err) - return - } - ctx.JSON(http.StatusOK, accounts) + q := ctx.Request.URL.Query().Get("q") + accounts, err := model.AccountsAll(q) + if err != nil { + httputil.NewError(ctx, http.StatusNotFound, err) + return + } + ctx.JSON(http.StatusOK, accounts) } - //... ``` ```console -$ swag init +swag init ``` 4. Run your app, and browse to http://localhost:8080/swagger/index.html. You will see Swagger 2.0 Api documents as shown below: ![swagger_index.html](https://raw.githubusercontent.com/swaggo/swag/master/assets/swagger-image.png) +## The swag formatter + +The Swag Comments can be automatically formatted, just like 'go fmt'. +Find the result of formatting [here](https://github.com/swaggo/swag/tree/master/example/celler). + +Usage: +```shell +swag fmt +``` + +Exclude folder: +```shell +swag fmt -d ./ --exclude ./internal +``` + +When using `swag fmt`, you need to ensure that you have a doc comment for the function to ensure correct formatting. +This is due to `swag fmt` indenting swag comments with tabs, which is only allowed *after* a standard doc comment. + +For example, use + +```go +// ListAccounts lists all existing accounts +// +// @Summary List accounts +// @Description get accounts +// @Tags accounts +// @Accept json +// @Produce json +// @Param q query string false "name search by q" Format(email) +// @Success 200 {array} model.Account +// @Failure 400 {object} httputil.HTTPError +// @Failure 404 {object} httputil.HTTPError +// @Failure 500 {object} httputil.HTTPError +// @Router /accounts [get] +func (c *Controller) ListAccounts(ctx *gin.Context) { +``` + ## Implementation Status [Swagger 2.0 document](https://swagger.io/docs/specification/2-0/basic-structure/) @@ -335,8 +398,12 @@ $ swag init | license.url | A URL to the license used for the API. MUST be in the format of a URL. | // @license.url http://www.apache.org/licenses/LICENSE-2.0.html | | host | The host (name or ip) serving the API. | // @host localhost:8080 | | BasePath | The base path on which the API is served. | // @BasePath /api/v1 | +| accept | A list of MIME types the APIs can consume. Note that Accept only affects operations with a request body, such as POST, PUT and PATCH. Value MUST be as described under [Mime Types](#mime-types). | // @accept json | +| produce | A list of MIME types the APIs can produce. Value MUST be as described under [Mime Types](#mime-types). | // @produce json | | query.collection.format | The default collection(array) param format in query,enums:csv,multi,pipes,tsv,ssv. If not set, csv is the default.| // @query.collection.format multi | schemes | The transfer protocol for the operation that separated by spaces. | // @schemes http https | +| externalDocs.description | Description of the external document. | // @externalDocs.description OpenAPI | +| externalDocs.url | URL of the external document. | // @externalDocs.url https://swagger.io/resources/open-api/ | | x-name | The extension key, must be start by x- and take only json value | // @x-example-key {"key": "value"} | ### Using markdown descriptions @@ -358,22 +425,26 @@ When a short string in your documentation is insufficient, or you need images, c [celler/controller](https://github.com/swaggo/swag/tree/master/example/celler/controller) -| annotation | description | -|-------------|----------------------------------------------------------------------------------------------------------------------------| -| description | A verbose explanation of the operation behavior. | -| description.markdown | A short description of the application. The description will be read from a file named like endpointname.md| // @description.file endpoint.description.markdown | -| id | A unique string used to identify the operation. Must be unique among all API operations. | -| tags | A list of tags to each API operation that separated by commas. | -| summary | A short summary of what the operation does. | -| accept | A list of MIME types the APIs can consume. Value MUST be as described under [Mime Types](#mime-types). | -| produce | A list of MIME types the APIs can produce. Value MUST be as described under [Mime Types](#mime-types). | -| param | Parameters that separated by spaces. `param name`,`param type`,`data type`,`is mandatory?`,`comment` `attribute(optional)` | -| security | [Security](#security) to each API operation. | -| success | Success response that separated by spaces. `return code`,`{param type}`,`data type`,`comment` | -| failure | Failure response that separated by spaces. `return code`,`{param type}`,`data type`,`comment` | -| header | Header in response that separated by spaces. `return code`,`{param type}`,`data type`,`comment` | -| router | Path definition that separated by spaces. `path`,`[httpMethod]` | -| x-name | The extension key, must be start by x- and take only json value. | +| annotation | description | +|----------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| description | A verbose explanation of the operation behavior. | +| description.markdown | A short description of the application. The description will be read from a file. E.g. `@description.markdown details` will load `details.md` | // @description.file endpoint.description.markdown | +| id | A unique string used to identify the operation. Must be unique among all API operations. | +| tags | A list of tags to each API operation that separated by commas. | +| summary | A short summary of what the operation does. | +| accept | A list of MIME types the APIs can consume. Note that Accept only affects operations with a request body, such as POST, PUT and PATCH. Value MUST be as described under [Mime Types](#mime-types). | +| produce | A list of MIME types the APIs can produce. Value MUST be as described under [Mime Types](#mime-types). | +| param | Parameters that separated by spaces. `param name`,`param type`,`data type`,`is mandatory?`,`comment` `attribute(optional)` | +| security | [Security](#security) to each API operation. | +| success | Success response that separated by spaces. `return code or default`,`{param type}`,`data type`,`comment` | +| failure | Failure response that separated by spaces. `return code or default`,`{param type}`,`data type`,`comment` | +| response | As same as `success` and `failure` | +| header | Header in response that separated by spaces. `return code`,`{param type}`,`data type`,`comment` | +| router | Path definition that separated by spaces. `path`,`[httpMethod]` | +| deprecatedrouter | As same as router, but deprecated. | +| x-name | The extension key, must be start by x- and take only json value. | +| x-codeSample | Optional Markdown usage. take `file` as parameter. This will then search for a file named like the summary in the given folder. | +| deprecated | Mark endpoint as deprecated. | @@ -413,45 +484,48 @@ Besides that, `swag` also accepts aliases for some MIME Types as follows: - integer (int, uint, uint32, uint64) - number (float32) - boolean (bool) +- file (param data type when uploading) - user defined struct ## Security | annotation | description | parameters | example | |------------|-------------|------------|---------| | securitydefinitions.basic | [Basic](https://swagger.io/docs/specification/2-0/authentication/basic-authentication/) auth. | | // @securityDefinitions.basic BasicAuth | -| securitydefinitions.apikey | [API key](https://swagger.io/docs/specification/2-0/authentication/api-keys/) auth. | in, name | // @securityDefinitions.apikey ApiKeyAuth | -| securitydefinitions.oauth2.application | [OAuth2 application](https://swagger.io/docs/specification/authentication/oauth2/) auth. | tokenUrl, scope | // @securitydefinitions.oauth2.application OAuth2Application | -| securitydefinitions.oauth2.implicit | [OAuth2 implicit](https://swagger.io/docs/specification/authentication/oauth2/) auth. | authorizationUrl, scope | // @securitydefinitions.oauth2.implicit OAuth2Implicit | -| securitydefinitions.oauth2.password | [OAuth2 password](https://swagger.io/docs/specification/authentication/oauth2/) auth. | tokenUrl, scope | // @securitydefinitions.oauth2.password OAuth2Password | -| securitydefinitions.oauth2.accessCode | [OAuth2 access code](https://swagger.io/docs/specification/authentication/oauth2/) auth. | tokenUrl, authorizationUrl, scope | // @securitydefinitions.oauth2.accessCode OAuth2AccessCode | - - -| parameters annotation | example | -|-----------------------|----------------------------------------------------------| -| in | // @in header | -| name | // @name Authorization | -| tokenUrl | // @tokenUrl https://example.com/oauth/token | -| authorizationurl | // @authorizationurl https://example.com/oauth/authorize | -| scope.hoge | // @scope.write Grants write access | - +| securitydefinitions.apikey | [API key](https://swagger.io/docs/specification/2-0/authentication/api-keys/) auth. | in, name, description | // @securityDefinitions.apikey ApiKeyAuth | +| securitydefinitions.oauth2.application | [OAuth2 application](https://swagger.io/docs/specification/authentication/oauth2/) auth. | tokenUrl, scope, description | // @securitydefinitions.oauth2.application OAuth2Application | +| securitydefinitions.oauth2.implicit | [OAuth2 implicit](https://swagger.io/docs/specification/authentication/oauth2/) auth. | authorizationUrl, scope, description | // @securitydefinitions.oauth2.implicit OAuth2Implicit | +| securitydefinitions.oauth2.password | [OAuth2 password](https://swagger.io/docs/specification/authentication/oauth2/) auth. | tokenUrl, scope, description | // @securitydefinitions.oauth2.password OAuth2Password | +| securitydefinitions.oauth2.accessCode | [OAuth2 access code](https://swagger.io/docs/specification/authentication/oauth2/) auth. | tokenUrl, authorizationUrl, scope, description | // @securitydefinitions.oauth2.accessCode OAuth2AccessCode | + + +| parameters annotation | example | +|---------------------------------|-------------------------------------------------------------------------| +| in | // @in header | +| name | // @name Authorization | +| tokenUrl | // @tokenUrl https://example.com/oauth/token | +| authorizationurl | // @authorizationurl https://example.com/oauth/authorize | +| scope.hoge | // @scope.write Grants write access | +| description | // @description OAuth protects our entity endpoints | ## Attribute ```go -// @Param enumstring query string false "string enums" Enums(A, B, C) -// @Param enumint query int false "int enums" Enums(1, 2, 3) -// @Param enumnumber query number false "int enums" Enums(1.1, 1.2, 1.3) -// @Param string query string false "string valid" minlength(5) maxlength(10) -// @Param int query int false "int valid" mininum(1) maxinum(10) -// @Param default query string false "string default" default(A) -// @Param collection query []string false "string collection" collectionFormat(multi) +// @Param enumstring query string false "string enums" Enums(A, B, C) +// @Param enumint query int false "int enums" Enums(1, 2, 3) +// @Param enumnumber query number false "int enums" Enums(1.1, 1.2, 1.3) +// @Param string query string false "string valid" minlength(5) maxlength(10) +// @Param int query int false "int valid" minimum(1) maximum(10) +// @Param default query string false "string default" default(A) +// @Param example query string false "string example" example(string) +// @Param collection query []string false "string collection" collectionFormat(multi) +// @Param extensions query []string false "string collection" extensions(x-example=test,x-nullable) ``` It also works for the struct fields: ```go type Foo struct { - Bar string `minLength:"4" maxLength:"16"` + Bar string `minLength:"4" maxLength:"16" example:"random string"` Baz int `minimum:"10" maximum:"20" default:"15"` Qux []string `enums:"foo,bar,baz"` } @@ -461,21 +535,23 @@ type Foo struct { Field Name | Type | Description ---|:---:|--- -validate | `string` | Determines the validation for the parameter. Possible values are: `required`. +validate | `string` | Determines the validation for the parameter. Possible values are: `required,optional`. default | * | Declares the value of the parameter that the server will use if none is provided, for example a "count" to control the number of results per page might default to 100 if not supplied by the client in the request. (Note: "default" has no meaning for required parameters.) See https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-6.2. Unlike JSON Schema this value MUST conform to the defined [`type`](#parameterType) for this parameter. maximum | `number` | See https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-5.1.2. minimum | `number` | See https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-5.1.3. +multipleOf | `number` | See https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-5.1.1. maxLength | `integer` | See https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-5.2.1. minLength | `integer` | See https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-5.2.2. enums | [\*] | See https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-5.5.1. format | `string` | The extending format for the previously mentioned [`type`](#parameterType). See [Data Type Formats](https://swagger.io/specification/v2/#dataTypeFormat) for further details. collectionFormat | `string` |Determines the format of the array if type array is used. Possible values are: Default value is `csv`. +example | * | Declares the example for the parameter value +extensions | `string` | Add extension to parameters. ### Future Field Name | Type | Description ---|:---:|--- -multipleOf | `number` | See https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-5.1.1. pattern | `string` | See https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-5.2.3. maxItems | `integer` | See https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-5.3.2. minItems | `integer` | See https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-5.3.3. @@ -485,7 +561,7 @@ Field Name | Type | Description ### Descriptions over multiple lines -You can add descriptions spanning multiple lines in either the general api description or routes definitions like so: +You can add descriptions spanning multiple lines in either the general api description or routes definitions like so: ```go // @description This is the first line @@ -508,6 +584,30 @@ type Account struct { } ``` + +### Function scoped struct declaration + +You can declare your request response structs inside a function body. +You must have to follow the naming convention `.. `. + +```go +package main + +// @Param request body main.MyHandler.request true "query params" +// @Success 200 {object} main.MyHandler.response +// @Router /test [post] +func MyHandler() { + type request struct { + RequestField string + } + + type response struct { + ResponseField string + } +} +``` + + ### Model composition in response ```go // JSONResult's data field will be overridden by the specific type proto.Order @@ -534,7 +634,7 @@ type Order struct { //in `proto` package @success 200 {object} jsonresult.JSONResult{data=[]string} "desc" ``` -- overriding multiple fields. field will be added if not exists +- overriding multiple fields. field will be added if not exists ```go @success 200 {object} jsonresult.JSONResult{data1=string,data2=[]string,data3=proto.Order,data4=[]proto.Order} "desc" ``` @@ -548,21 +648,35 @@ type DeepObject struct { //in `proto` package ### Add a headers in response ```go -// @Success 200 {string} string "ok" -// @Header 200 {string} Location "/entity/1" -// @Header 200 {string} Token "qwerty" +// @Success 200 {string} string "ok" +// @failure 400 {string} string "error" +// @response default {string} string "other error" +// @Header 200 {string} Location "/entity/1" +// @Header 200,400,default {string} Token "token" +// @Header all {string} Token2 "token2" ``` ### Use multiple path params ```go /// ... -// @Param group_id path int true "Group ID" +// @Param group_id path int true "Group ID" // @Param account_id path int true "Account ID" // ... // @Router /examples/groups/{group_id}/accounts/{account_id} [get] ``` +### Add multiple paths + +```go +/// ... +// @Param group_id path int true "Group ID" +// @Param user_id path int true "User ID" +// ... +// @Router /examples/groups/{group_id}/user/{user_id}/address [put] +// @Router /examples/user/{user_id}/address [put] +``` + ### Example value of struct ```go @@ -573,9 +687,18 @@ type Account struct { } ``` +### SchemaExample of body + +```go +// @Param email body string true "message/rfc822" SchemaExample(Subject: Testmail\r\n\r\nBody Message\r\n) +``` + ### Description of struct ```go +// Account model info +// @Description User account information +// @Description with user id and username type Account struct { // ID this is userid ID int `json:"id"` @@ -583,6 +706,27 @@ type Account struct { } ``` +[#708](https://github.com/swaggo/swag/issues/708) The parser handles only struct comments starting with `@Description` attribute. +But it writes all struct field comments as is. + +So, generated swagger doc as follows: +```json +"Account": { + "type":"object", + "description": "User account information with user id and username" + "properties": { + "id": { + "type": "integer", + "description": "ID this is userid" + }, + "name": { + "type":"string", + "description": "This is Name" + } + } +} +``` + ### Use swaggertype tag to supported custom type [#201](https://github.com/swaggo/swag/issues/201#issuecomment-475479409) @@ -647,6 +791,40 @@ generated swagger doc as follows: ``` +### Use global overrides to support a custom type + +If you are using generated files, the [`swaggertype`](#use-swaggertype-tag-to-supported-custom-type) or `swaggerignore` tags may not be possible. + +By passing a mapping to swag with `--overridesFile` you can tell swag to use one type in place of another wherever it appears. By default, if a `.swaggo` file is present in the current directory it will be used. + +Go code: +```go +type MyStruct struct { + ID sql.NullInt64 `json:"id"` + Name sql.NullString `json:"name"` +} +``` + +`.swaggo`: +``` +// Replace all NullInt64 with int +replace database/sql.NullInt64 int + +// Don't include any fields of type database/sql.NullString in the swagger docs +skip database/sql.NullString +``` + +Possible directives are comments (beginning with `//`), `replace path/to/a.type path/to/b.type`, and `skip path/to/a.type`. + +(Note that the full paths to any named types must be provided to prevent problems when multiple packages define a type with the same name) + +Rendered: +```go +"types.MyStruct": { + "id": "integer" +} +``` + ### Use swaggerignore tag to exclude a field @@ -662,7 +840,7 @@ type Account struct { ```go type Account struct { - ID string `json:"id" extensions:"x-nullable,x-abc=def"` // extensions fields must start with "x-" + ID string `json:"id" extensions:"x-nullable,x-abc=def,!x-omitempty"` // extensions fields must start with "x-" } ``` @@ -675,7 +853,8 @@ generate swagger doc as follows: "id": { "type": "string", "x-nullable": true, - "x-abc": "def" + "x-abc": "def", + "x-omitempty": false } } } @@ -688,7 +867,7 @@ type Resp struct { }//@name Response ``` -### How to using security annotations +### How to use security annotations General API info. @@ -714,6 +893,59 @@ Make it AND condition // @Security OAuth2Application[write, admin] ``` +Make it OR condition + +```go +// @Security ApiKeyAuth || firebase +// @Security OAuth2Application[write, admin] || APIKeyAuth +``` + + +### Add a description for enum items + +```go +type Example struct { + // Sort order: + // * asc - Ascending, from A to Z. + // * desc - Descending, from Z to A. + Order string `enums:"asc,desc"` +} +``` + +### Generate only specific docs file types + +By default `swag` command generates Swagger specification in three different files/file types: +- docs.go +- swagger.json +- swagger.yaml + +If you would like to limit a set of file types which should be generated you can use `--outputTypes` (short `-ot`) flag. Default value is `go,json,yaml` - output types separated with comma. To limit output only to `go` and `yaml` files, you would write `go,yaml`. With complete command that would be `swag init --outputTypes go,yaml`. + +### How to use Generics + +```go +// @Success 200 {object} web.GenericNestedResponse[types.Post] +// @Success 204 {object} web.GenericNestedResponse[types.Post, Types.AnotherOne] +// @Success 201 {object} web.GenericNestedResponse[web.GenericInnerType[types.Post]] +func GetPosts(w http.ResponseWriter, r *http.Request) { + _ = web.GenericNestedResponse[types.Post]{} +} +``` +See [this file](https://github.com/swaggo/swag/blob/master/testdata/generics_nested/api/api.go) for more details +and other examples. + +### Change the default Go Template action delimiters +[#980](https://github.com/swaggo/swag/issues/980) +[#1177](https://github.com/swaggo/swag/issues/1177) + +If your swagger annotations or struct fields contain "{{" or "}}", the template generation will most likely fail, as these are the default delimiters for [go templates](https://pkg.go.dev/text/template#Template.Delims). + +To make the generation work properly, you can change the default delimiters with `-td`. For example: +```console +swag init -g http/api.go -td "[[,]]" +``` +The new delimiter is a string with the format "``,``". + ## About the Project This project was inspired by [yvasiyarov/swagger](https://github.com/yvasiyarov/swagger) but we simplified the usage and added support a variety of [web frameworks](#supported-web-frameworks). Gopher image source is [tenntenn/gopher-stickers](https://github.com/tenntenn/gopher-stickers). It has licenses [creative commons licensing](http://creativecommons.org/licenses/by/3.0/deed.en). ## Contributors diff --git a/vendor/github.com/swaggo/swag/README_pt.md b/vendor/github.com/swaggo/swag/README_pt.md new file mode 100644 index 0000000000..7f95066bba --- /dev/null +++ b/vendor/github.com/swaggo/swag/README_pt.md @@ -0,0 +1,968 @@ +# swag + +🌍 *[English](README.md) ∙ [简体中文](README_zh-CN.md) ∙ [Português](README_pt.md)* + + + +[![Build Status](https://github.com/swaggo/swag/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/features/actions) +[![Coverage Status](https://img.shields.io/codecov/c/github/swaggo/swag/master.svg)](https://codecov.io/gh/swaggo/swag) +[![Go Report Card](https://goreportcard.com/badge/github.com/swaggo/swag)](https://goreportcard.com/report/github.com/swaggo/swag) +[![codebeat badge](https://codebeat.co/badges/71e2f5e5-9e6b-405d-baf9-7cc8b5037330)](https://codebeat.co/projects/github-com-swaggo-swag-master) +[![Go Doc](https://godoc.org/github.com/swaggo/swagg?status.svg)](https://godoc.org/github.com/swaggo/swag) +[![Backers on Open Collective](https://opencollective.com/swag/backers/badge.svg)](#backers) +[![Sponsors on Open Collective](https://opencollective.com/swag/sponsors/badge.svg)](#sponsors) [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fswaggo%2Fswag.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Fswaggo%2Fswag?ref=badge_shield) +[![Release](https://img.shields.io/github/release/swaggo/swag.svg?style=flat-square)](https://github.com/swaggo/swag/releases) + +Swag converte anotações Go para Documentação Swagger 2.0. Criámos uma variedade de plugins para populares [Go web frameworks](#supported-web-frameworks). Isto permite uma integração rápida com um projecto Go existente (utilizando a Swagger UI). + +## Conteúdo +- [Começando](#começando) + - [Estruturas Web Suportadas](#estruturas-web-suportadas) + - [Como utilizá-lo com Gin](#como-como-ser-como-gin) + - [O formatador de swag](#a-formatação-de-swag) + - [Estado de Implementação](#implementação-estado) + - [Formato dos comentários declarativos](#formato-dos-comentarios-declarativos) + - [Informações Gerais API](#informações-gerais-api) + - [Operação API](#api-operacao) + - [Segurança](#seguranca) + - [Exemplos](#exemplos) + - [Descrições em múltiplas linhas](#descricoes-sobre-múltiplas-linhas) + - [Estrutura definida pelo utilizador com um tipo de matriz](#-estrutura-definida-pelo-utilizador-com-um-um-tipo) + - [Declaração de estruturação de funções](#function-scoped-struct-declaration) + - [Composição do modelo em resposta](#model-composição-em-resposta) + - [Adicionar um cabeçalho em resposta](#add-a-headers-in-response) + - [Utilizar parâmetros de caminhos múltiplos](#use-multiple-path-params) + - [Exemplo de valor de estrutura](#exemplo-do-valor-de-estrutura) + - [Schema Exemplo do corpo](#schemaexample-of-body) + - [Descrição da estrutura](#descrição-da-estrutura) + - [Usar etiqueta do tipo swaggertype para suportar o tipo personalizado](#use-swaggertype-tag-to-supported-custom-type) + - [Utilizar anulações globais para suportar um tipo personalizado](#use-global-overrides-to-support-a-custom-type) + - [Use swaggerignore tag para excluir um campo](#use-swaggerignore-tag-to-excluir-um-campo) + - [Adicionar informações de extensão ao campo de estruturação](#add-extension-info-to-struct-field) + - [Renomear modelo a expor](#renome-modelo-a-exibir) + - [Como utilizar as anotações de segurança](#como-utilizar-as-anotações-de-segurança) + - [Adicionar uma descrição para enumerar artigos](#add-a-description-for-enum-items) + - [Gerar apenas tipos de ficheiros de documentos específicos](#generate-only-specific-docs-file-file-types) + - [Como usar tipos genéricos](#como-usar-tipos-genéricos) +- [Sobre o projecto](#sobre-o-projecto) + +## Começando + +1. Adicione comentários ao código-fonte da API, consulte [Formato dos comentários declarativos](#declarative-comments-format). + +2. Descarregue o swag utilizando: +```sh +go install github.com/swaggo/swag/cmd/swag@latest +``` +Para construir a partir da fonte é necessário [Go](https://golang.org/dl/) (1.18 ou mais recente). + +Ou descarregar um binário pré-compilado a partir da [página de lançamento](https://github.com/swaggo/swag/releases). + +3. Executar `swag init` na pasta raiz do projecto que contém o ficheiro `main.go`. Isto irá analisar os seus comentários e gerar os ficheiros necessários (pasta `docs` e `docs/docs.go`). +```sh +swag init +``` + +Certifique-se de importar os `docs/docs.go` gerados para que a sua configuração específica fique "init" ed. Se as suas anotações API gerais não viverem em `main.go`, pode avisar a swag com a bandeira `-g`. +```sh +swag init -g http/api.go +``` + +4. (opcional) Utilizar o formato `swag fmt` no comentário SWAG. (Por favor, actualizar para a versão mais recente) + +```sh +swag fmt +``` + +## swag cli + +```sh +swag init -h +NOME: + swag init - Criar docs.go + +UTILIZAÇÃO: + swag init [opções de comando] [argumentos...] + +OPÇÕES: + --quiet, -q Fazer o logger ficar quiet (por padrão: falso) + --generalInfo valor, -g valor Go caminho do ficheiro em que 'swagger general API Info' está escrito (por padrão: "main.go") + --dir valor, -d valor Os directórios que deseja analisar, separados por vírgulas e de informação geral devem estar no primeiro (por padrão: "./") + --exclude valor Excluir directórios e ficheiros ao pesquisar, separados por vírgulas + -propertyStrategy da estratégia, -p valor da propriedadeEstratégia de nomeação de propriedades como snakecase,camelcase,pascalcase (por padrão: "camelcase") + --output de saída, -o valor directório de saída para todos os ficheiros gerados(swagger.json, swagger.yaml e docs.go) (por padrão: "./docs") + --outputTypes valor de saídaTypes, -- valor de saída Tipos de ficheiros gerados (docs.go, swagger.json, swagger.yaml) como go,json,yaml (por padrão: "go,json,yaml") + --parseVendor ParseVendor Parse go files na pasta 'vendor', desactivado por padrão (padrão: falso) + --parseInternal Parse go ficheiros em pacotes internos, desactivados por padrão (padrão: falso) + --generatedTime Gerar timestamp no topo dos docs.go, desactivado por padrão (padrão: falso) + --parteDepth value Dependência profundidade parse (por padrão: 100) + --templateDelims value, --td value fornecem delimitadores personalizados para a geração de modelos Go. O formato é leftDelim,rightDelim. Por exemplo: "[[,]]" + ... + + --help, -h mostrar ajuda (por padrão: falso) +``` + +```bash +swag fmt -h +NOME: + swag fmt - formato swag comentários + +UTILIZAÇÃO: + swag fmt [opções de comando] [argumentos...] + +OPÇÕES: + --dir valor, -d valor Os directórios que pretende analisar, separados por vírgulas e de informação geral devem estar no primeiro (por padrão: "./") + --excluir valor Excluir directórios e ficheiros ao pesquisar, separados por vírgulas + --generalInfo value, -g value Go file path in which 'swagger general API Info' is written (por padrão: "main.go") + --ajuda, -h mostrar ajuda (por padrão: falso) + +``` + +## Estruturas Web Suportadas + +- [gin](http://github.com/swaggo/gin-swagger) +- [echo](http://github.com/swaggo/echo-swagger) +- [buffalo](https://github.com/swaggo/buffalo-swagger) +- [net/http](https://github.com/swaggo/http-swagger) +- [gorilla/mux](https://github.com/swaggo/http-swagger) +- [go-chi/chi](https://github.com/swaggo/http-swagger) +- [flamingo](https://github.com/i-love-flamingo/swagger) +- [fiber](https://github.com/gofiber/swagger) +- [atreugo](https://github.com/Nerzal/atreugo-swagger) +- [hertz](https://github.com/hertz-contrib/swagger) + +## Como utilizá-lo com Gin + +Encontrar o código fonte de exemplo [aqui](https://github.com/swaggo/swag/tree/master/example/celler). + +1. Depois de utilizar `swag init` para gerar os documentos Swagger 2.0, importar os seguintes pacotes: +```go +import "github.com/swaggo/gin-swagger" // gin-swagger middleware +import "github.com/swaggo/files" // swagger embed files +``` + +2. Adicionar [Informações Gerais API](#general-api-info) anotações em código `main.go`: + + +```go +// @title Swagger Example API +// @version 1.0 +// @description This is a sample server celler server. +// @termsOfService http://swagger.io/terms/ + +// @contact.name API Support +// @contact.url http://www.swagger.io/support +// @contact.email support@swagger.io + +// @license.name Apache 2.0 +// @license.url http://www.apache.org/licenses/LICENSE-2.0.html + +// @host localhost:8080 +// @BasePath /api/v1 + +// @securityDefinitions.basic BasicAuth + +// @externalDocs.description OpenAPI +// @externalDocs.url https://swagger.io/resources/open-api/ +func main() { + r := gin.Default() + + c := controller.NewController() + + v1 := r.Group("/api/v1") + { + accounts := v1.Group("/accounts") + { + accounts.GET(":id", c.ShowAccount) + accounts.GET("", c.ListAccounts) + accounts.POST("", c.AddAccount) + accounts.DELETE(":id", c.DeleteAccount) + accounts.PATCH(":id", c.UpdateAccount) + accounts.POST(":id/images", c.UploadAccountImage) + } + //... + } + r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) + r.Run(":8080") +} +//... +``` + +Além disso, algumas informações API gerais podem ser definidas de forma dinâmica. O pacote de código gerado `docs` exporta a variável `SwaggerInfo` que podemos utilizar para definir programticamente o título, descrição, versão, hospedeiro e caminho base. Exemplo utilizando Gin: + +```go +package main + +import ( + "github.com/gin-gonic/gin" + "github.com/swaggo/files" + "github.com/swaggo/gin-swagger" + + "./docs" // docs is generated by Swag CLI, you have to import it. +) + +// @contact.name API Support +// @contact.url http://www.swagger.io/support +// @contact.email support@swagger.io + +// @license.name Apache 2.0 +// @license.url http://www.apache.org/licenses/LICENSE-2.0.html +func main() { + + // programmatically set swagger info + docs.SwaggerInfo.Title = "Swagger Example API" + docs.SwaggerInfo.Description = "This is a sample server Petstore server." + docs.SwaggerInfo.Version = "1.0" + docs.SwaggerInfo.Host = "petstore.swagger.io" + docs.SwaggerInfo.BasePath = "/v2" + docs.SwaggerInfo.Schemes = []string{"http", "https"} + + r := gin.New() + + // use ginSwagger middleware to serve the API docs + r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) + + r.Run() +} +``` + +3. Adicionar [Operação API](#api-operacao) anotações em código `controller` + +```go +package controller + +import ( + "fmt" + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + "github.com/swaggo/swag/example/celler/httputil" + "github.com/swaggo/swag/example/celler/model" +) + +// ShowAccount godoc +// @Summary Show an account +// @Description get string by ID +// @Tags accounts +// @Accept json +// @Produce json +// @Param id path int true "Account ID" +// @Success 200 {object} model.Account +// @Failure 400 {object} httputil.HTTPError +// @Failure 404 {object} httputil.HTTPError +// @Failure 500 {object} httputil.HTTPError +// @Router /accounts/{id} [get] +func (c *Controller) ShowAccount(ctx *gin.Context) { + id := ctx.Param("id") + aid, err := strconv.Atoi(id) + if err != nil { + httputil.NewError(ctx, http.StatusBadRequest, err) + return + } + account, err := model.AccountOne(aid) + if err != nil { + httputil.NewError(ctx, http.StatusNotFound, err) + return + } + ctx.JSON(http.StatusOK, account) +} + +// ListAccounts godoc +// @Summary List accounts +// @Description get accounts +// @Tags accounts +// @Accept json +// @Produce json +// @Param q query string false "name search by q" Format(email) +// @Success 200 {array} model.Account +// @Failure 400 {object} httputil.HTTPError +// @Failure 404 {object} httputil.HTTPError +// @Failure 500 {object} httputil.HTTPError +// @Router /accounts [get] +func (c *Controller) ListAccounts(ctx *gin.Context) { + q := ctx.Request.URL.Query().Get("q") + accounts, err := model.AccountsAll(q) + if err != nil { + httputil.NewError(ctx, http.StatusNotFound, err) + return + } + ctx.JSON(http.StatusOK, accounts) +} +//... +``` + +```console +swag init +``` + +4. Execute a sua aplicação, e navegue para http://localhost:8080/swagger/index.html. Verá os documentos Swagger 2.0 Api, como mostrado abaixo: + +![swagger_index.html](https://raw.githubusercontent.com/swaggo/swag/master/assets/swagger-image.png) + +## O formatador de swag + +Os Swag Comments podem ser formatados automaticamente, assim como 'go fmt'. +Encontre o resultado da formatação [aqui](https://github.com/swaggo/swag/tree/master/example/celler). + +Usage: +```shell +swag fmt +``` + +Exclude folder: +```shell +swag fmt -d ./ --exclude ./internal +``` + +Ao utilizar `swag fmt`, é necessário assegurar-se de que tem um comentário doc para a função a fim de assegurar uma formatação correcta. +Isto deve-se ao `swag fmt` que traça comentários swag com separadores, o que só é permitido *após* um comentário doc padrão. + +Por exemplo, utilizar + +```go +// ListAccounts lists all existing accounts +// +// @Summary List accounts +// @Description get accounts +// @Tags accounts +// @Accept json +// @Produce json +// @Param q query string false "name search by q" Format(email) +// @Success 200 {array} model.Account +// @Failure 400 {object} httputil.HTTPError +// @Failure 404 {object} httputil.HTTPError +// @Failure 500 {object} httputil.HTTPError +// @Router /accounts [get] +func (c *Controller) ListAccounts(ctx *gin.Context) { +``` + +## Estado de Implementação + +[Documento Swagger 2.0](https://swagger.io/docs/specification/2-0/basic-structure/) + +- [x] Estrutura básica +- [x] Hospedeiro API e Caminho Base +- [x] Caminhos e operações +- [x] Descrição dos parâmetros +- [x] Descrever o corpo do pedido +- [x] Descrição das respostas +- [x] Tipos MIME +- [x] Autenticação + - [x] Autenticação básica + - [x] Chaves API +- [x] Acrescentar exemplos +- [x] Carregamento de ficheiros +- [x] Enums +- [x] Operações de Agrupamento com Etiquetas +- Extensões Swagger + +## Formato dos comentários declarativos + +## Informações Gerais API + +**Exemplo** +[celler/main.go](https://github.com/swaggo/swag/blob/master/example/celler/main.go) + +| anotação | descrição | exemplo | +|-------------|--------------------------------------------|---------------------------------| +| title | **Obrigatório.** O título da aplicação.| // @title Swagger Example API | +| version | **Obrigatório.** Fornece a versão da aplicação API.| // @version 1.0 | +| description | Uma breve descrição da candidatura. |// @descrição Este é um servidor servidor de celas de amostra. | +| tag.name | Nome de uma tag.| // @tag.name Este é o nome da tag | +| tag.description | Descrição da tag | // @tag.description Cool Description | +| tag.docs.url | Url da Documentação externa da tag | // @tag.docs.url https://example.com| +| tag.docs.description | Descrição da documentação externa da tag| // @tag.docs.description Melhor exemplo de documentação | +| TermsOfService | Os Termos de Serviço para o API.| // @termsOfService http://swagger.io/terms/ | +| contact.name | A informação de contacto para a API exposta.| // @contacto.name Suporte API | +| contact.url | O URL que aponta para as informações de contacto. DEVE estar no formato de um URL. | // @contact.url http://www.swagger.io/support| +| contact.email| O endereço de email da pessoa/organização de contacto. DEVE estar no formato de um endereço de correio electrónico.| // @contact.email support@swagger.io | +| license.name | **Obrigatório.** O nome da licença utilizada para a API.|// @licença.name Apache 2.0| +| license.url | Um URL para a licença utilizada para a API. DEVE estar no formato de um URL. | // @license.url http://www.apache.org/licenses/LICENSE-2.0.html | +| host | O anfitrião (nome ou ip) que serve o API. | // @host localhost:8080 | +| BasePath | O caminho de base sobre o qual o API é servido. | // @BasePath /api/v1 | +| accept | Uma lista de tipos de MIME que os APIs podem consumir. Note que accept só afecta operações com um organismo de pedido, tais como POST, PUT e PATCH. O valor DEVE ser o descrito em [Tipos de Mime](#mime-types). | // @accept json | +| produce | Uma lista de tipos de MIME que os APIs podem produce. O valor DEVE ser o descrito em [Tipos de Mime](#mime-types). | // @produce json | +| query.collection.format | O formato padrão de param de colecção(array) em query,enums:csv,multi,pipes,tsv,ssv. Se não definido, csv é o padrão.| // @query.collection.format multi +| schemes | O protocolo de transferência para a operação que separou por espaços. | // @schemes http https | +| externalDocs.description | Descrição do documento externo. | // @externalDocs.description OpenAPI | +| externalDocs.url | URL do documento externo. | // @externalDocs.url https://swagger.io/resources/open-api/ | +| x-name | A chave de extensão, deve ser iniciada por x- e tomar apenas o valor json | // @x-example-key {"chave": "valor"} | + +### Usando descrições de remarcação para baixo +Quando uma pequena sequência na sua documentação é insuficiente, ou precisa de imagens, exemplos de códigos e coisas do género, pode querer usar descrições de marcação. Para utilizar as descrições markdown, utilize as seguintes anotações. + +| anotação | descrição | exemplo | +|-------------|--------------------------------------------|---------------------------------| +| title | **Obrigatório.** O título da aplicação.| // @title Swagger Example API | +| version | **Obrigatório.** Fornece a versão da aplicação API.| // @versão 1.0 | +| description.markdown | Uma breve descrição da candidatura. Parsed a partir do ficheiro api.md. Esta é uma alternativa a @description |// @description.markdown Sem valor necessário, isto analisa a descrição do ficheiro api.md |. +| tag.name | Nome de uma tag.| // @tag.name Este é o nome da tag | +| tag.description.markdown | Descrição da tag esta é uma alternativa à tag.description. A descrição será lida a partir de um ficheiro nomeado como tagname.md | // @tag.description.markdown | + +## Operação API + +**Exemplo** +[celler/controller](https://github.com/swaggo/swag/tree/master/example/celler/controller) + +| anotação | descrição | +|-------------|----------------------------------------------------------------------------------------------------------------------------| +| descrição | Uma explicação verbosa do comportamento da operação. | +| description.markdown | Uma breve descrição da candidatura. A descrição será lida a partir de um ficheiro. Por exemplo, `@description.markdown details` irá carregar `details.md`| // @description.file endpoint.description.markdown | +| id | Um fio único utilizado para identificar a operação. Deve ser única entre todas as operações API. | +| tags | Uma lista de tags para cada operação API que separou por vírgulas. | +| summary | Um breve resumo do que a operação faz. | +| accept | Uma lista de tipos de MIME que os APIs podem consumir. Note que accept só afecta operações com um organismo de pedido, tais como POST, PUT e PATCH. O valor DEVE ser o descrito em [Tipos de Mime](#mime-types). | +| produce | Uma lista de tipos de MIME que os APIs podem produce. O valor DEVE ser o descrito em [Tipos de Mime](#mime-types). | +| param | Parâmetros que se separaram por espaços. `param name`,`param type`,`data type`,`is mandatory?`,`comment` `attribute(optional)` | +| security | [Segurança](#security) para cada operação API. | +| success | resposta de sucesso que separou por espaços. `return code or default`,`{param type}`,`data type`,`comment` |. +| failure | Resposta de falha que separou por espaços. `return code or default`,`{param type}`,`data type`,`comment` | +| response | Igual ao `sucesso` e `falha` | +| header | Cabeçalho em resposta que separou por espaços. `código de retorno`,`{tipo de parâmetro}`,`tipo de dados`,`comentário` |. +| router | Definição do caminho que separou por espaços. caminho",`path`,`[httpMethod]` |[httpMethod]` | +| x-name | A chave de extensão, deve ser iniciada por x- e tomar apenas o valor json. | +| x-codeSample | Optional Markdown use. tomar `file` como parâmetro. Isto irá então procurar um ficheiro nomeado como o resumo na pasta dada. | +| deprecated | Marcar o ponto final como depreciado. | + +## Mime Types + +`swag` aceita todos os tipos MIME que estão no formato correcto, ou seja, correspondem `*/*`. +Além disso, `swag` também aceita pseudónimos para alguns tipos de MIME, como se segue: + + +| Alias | MIME Type | +|-----------------------|-----------------------------------| +| json | application/json | +| xml | text/xml | +| plain | text/plain | +| html | text/html | +| mpfd | multipart/form-data | +| x-www-form-urlencoded | application/x-www-form-urlencoded | +| json-api | application/vnd.api+json | +| json-stream | application/x-json-stream | +| octet-stream | application/octet-stream | +| png | image/png | +| jpeg | image/jpeg | +| gif | image/gif | + + + +## Tipo de parâmetro + +- query +- path +- header +- body +- formData + +## Tipo de dados + +- string (string) +- integer (int, uint, uint32, uint64) +- number (float32) +- boolean (bool) +- file (param data type when uploading) +- user defined struct + +## Segurança +| anotação | descrição | parâmetros | exemplo | +|------------|-------------|------------|---------| +| securitydefinitions.basic | [Basic](https://swagger.io/docs/specification/2-0/authentication/basic-authentication/) auth. | | // @securityDefinitions.basicAuth | [Básico]() +| securitydefinitions.apikey | [chave API](https://swagger.io/docs/specification/2-0/authentication/api-keys/) auth. | in, name, description | // @securityDefinitions.apikey ApiKeyAuth | +| securitydefinitions.oauth2.application | [Aplicação OAuth2](https://swagger.io/docs/specification/authentication/oauth2/) auth. | tokenUrl, scope, description | // @securitydefinitions.oauth2.application OAuth2Application | +| securitydefinitions.oauth2.implicit | [OAuth2 implicit](https://swagger.io/docs/specification/authentication/oauth2/) auth. | authorizationUrl, scope, description | // @securitydefinitions.oauth2.implicit OAuth2Implicit | [OAuth2Implicit]() +| securitydefinitions.oauth2.password | [OAuth2 password](https://swagger.io/docs/specification/authentication/oauth2/) auth. | tokenUrl, scope, description | // @securitydefinitions.oauth2.password OAuth2Password | +| securitydefinitions.oauth2.accessCode | [código de acesso OAuth2](https://swagger.io/docs/specification/authentication/oauth2/) auth. | tokenUrl, authorizationUrl, scope, description | // @securitydefinitions.oauth2.accessCode OAuth2AccessCode | [código de acesso OAuth2.accessCode]() + + +| anotação de parâmetros | exemplo | +|---------------------------------|-------------------------------------------------------------------------| +| in | // @in header | +| name | // @name Authorization | +| tokenUrl | // @tokenUrl https://example.com/oauth/token | +| authorizationurl | // @authorizationurl https://example.com/oauth/authorize | +| scope.hoge | // @scope.write Grants write access | +| description | // @descrição OAuth protege os pontos finais da nossa entidade | + +## Atributo + +```go +// @Param enumstring query string false "string enums" Enums(A, B, C) +// @Param enumint query int false "int enums" Enums(1, 2, 3) +// @Param enumnumber query number false "int enums" Enums(1.1, 1.2, 1.3) +// @Param string query string false "string valid" minlength(5) maxlength(10) +// @Param int query int false "int valid" minimum(1) maximum(10) +// @Param default query string false "string default" default(A) +// @Param example query string false "string example" example(string) +// @Param collection query []string false "string collection" collectionFormat(multi) +// @Param extensions query []string false "string collection" extensions(x-example=test,x-nullable) +``` + +It also works for the struct fields: + +```go +type Foo struct { + Bar string `minLength:"4" maxLength:"16" example:"random string"` + Baz int `minimum:"10" maximum:"20" default:"15"` + Qux []string `enums:"foo,bar,baz"` +} +``` + +### Disponível + +Nome do campo | Tipo | Descrição +---|:---:|--- +validate | `string` | Determina a validação para o parâmetro. Os valores possíveis são: `required,optional`. +default | * | Declara o valor do parâmetro que o servidor utilizará se nenhum for fornecido, por exemplo, uma "contagem" para controlar o número de resultados por página poderá ser por defeito de 100 se não for fornecido pelo cliente no pedido. (Nota: "por defeito" não tem significado para os parâmetros requeridos). +See https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-6.2. Ao contrário do esquema JSON, este valor DEVE estar em conformidade com o definido [`type`](#parameterType) para este parâmetro. +maximum | `number` | Ver https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-5.1.2. +minimum | `number` | Ver https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-5.1.3. +multipleOf | `number` | Ver https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-5.1.1. +maxLength | `integer` | Ver https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-5.2.1. +minLength | `integer` | Ver https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-5.2.2. +enums | [\*] | Ver https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-5.5.1. +format | `string` | O formato de extensão para o anteriormente mencionado [`type`](#parameterType). Ver [Data Type Formats](https://swagger.io/specification/v2/#dataTypeFormat) para mais detalhes. +collectionFormat | `string` |Determina o formato da matriz se for utilizada uma matriz de tipos. Os valores possíveis são:
  • `csv` - valores separados por vírgulas `foo,bar`.
  • `ssv` - valores separados por espaço `foo bar`.
  • `tsv` - valores separados por tabulação `foo\tbar`.
  • `pipes` - valores separados por tubo foo|bar.
  • `multi` - corresponde a múltiplas instâncias de parâmetros em vez de múltiplos valores para uma única instância `foo=bar&foo=baz`. This is valid only for parameters [`in`](#parameterIn) "query" or "formData".
Default value is `csv`. +example | * | Declara o exemplo para o valor do parâmetro +extensions | `string` | Acrescentar extensão aos parâmetros. + +### Futuro + +Nome do campo | Tipo | Description +---|:---:|--- +pattern | `string` | See https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-5.2.3. +maxItems | `integer` | See https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-5.3.2. +minItems | `integer` | See https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-5.3.3. +uniqueItems | `boolean` | See https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-5.3.4. + +## Exemplos + + +### Descrições em múltiplas linhas + +É possível acrescentar descrições que abranjam várias linhas tanto na descrição geral da api como em definições de rotas como esta: + +```go +// @description This is the first line +// @description This is the second line +// @description And so forth. +``` + +### Estrutura definida pelo utilizador com um tipo de matriz + +```go +// @Success 200 {array} model.Account <-- This is a user defined struct. +``` + +```go +package model + +type Account struct { + ID int `json:"id" example:"1"` + Name string `json:"name" example:"account name"` +} +``` + + +### Declaração de estruturação de funções + +Pode declarar as estruturas de resposta do seu pedido dentro de um corpo funcional. +Deve ter de seguir a convenção de nomeação +`.. `. + +```go +package main + +// @Param request body main.MyHandler.request true "query params" +// @Success 200 {object} main.MyHandler.response +// @Router /test [post] +func MyHandler() { + type request struct { + RequestField string + } + + type response struct { + ResponseField string + } +} +``` + + +### Composição do modelo em resposta +```go +// JSONResult's data field will be overridden by the specific type proto.Order +@success 200 {object} jsonresult.JSONResult{data=proto.Order} "desc" +``` + +```go +type JSONResult struct { + Code int `json:"code" ` + Message string `json:"message"` + Data interface{} `json:"data"` +} + +type Order struct { //in `proto` package + Id uint `json:"id"` + Data interface{} `json:"data"` +} +``` + +- também suportam uma variedade de objectos e tipos primitivos como resposta aninhada +```go +@success 200 {object} jsonresult.JSONResult{data=[]proto.Order} "desc" +@success 200 {object} jsonresult.JSONResult{data=string} "desc" +@success 200 {object} jsonresult.JSONResult{data=[]string} "desc" +``` + +- campos múltiplos que se sobrepõem. campo será adicionado se não existir +```go +@success 200 {object} jsonresult.JSONResult{data1=string,data2=[]string,data3=proto.Order,data4=[]proto.Order} "desc" +``` +- overriding deep-level fields +```go +type DeepObject struct { //in `proto` package + ... +} +@success 200 {object} jsonresult.JSONResult{data1=proto.Order{data=proto.DeepObject},data2=[]proto.Order{data=[]proto.DeepObject}} "desc" +``` + +### Adicionar um cabeçalho em resposta + +```go +// @Success 200 {string} string "ok" +// @failure 400 {string} string "error" +// @response default {string} string "other error" +// @Header 200 {string} Location "/entity/1" +// @Header 200,400,default {string} Token "token" +// @Header all {string} Token2 "token2" +``` + + +### Utilizar parâmetros de caminhos múltiplos + +```go +/// ... +// @Param group_id path int true "Group ID" +// @Param account_id path int true "Account ID" +// ... +// @Router /examples/groups/{group_id}/accounts/{account_id} [get] +``` + +### Adicionar múltiplos caminhos + +```go +/// ... +// @Param group_id path int true "Group ID" +// @Param user_id path int true "User ID" +// ... +// @Router /examples/groups/{group_id}/user/{user_id}/address [put] +// @Router /examples/user/{user_id}/address [put] +``` + +### Exemplo de valor de estrutura + +```go +type Account struct { + ID int `json:"id" example:"1"` + Name string `json:"name" example:"account name"` + PhotoUrls []string `json:"photo_urls" example:"http://test/image/1.jpg,http://test/image/2.jpg"` +} +``` + +### Schema Exemplo do corpo + +```go +// @Param email body string true "message/rfc822" SchemaExample(Subject: Testmail\r\n\r\nBody Message\r\n) +``` + +### Descrição da estrutura + +```go +// Account model info +// @Description User account information +// @Description with user id and username +type Account struct { + // ID this is userid + ID int `json:"id"` + Name string `json:"name"` // This is Name +} +``` + +[#708](https://github.com/swaggo/swag/issues/708) O analisador trata apenas de comentários estruturais a partir de `@Description` attribute. + +Assim, gerou o doc. de swagger como se segue: +```json +"Account": { + "type":"object", + "description": "User account information with user id and username" + "properties": { + "id": { + "type": "integer", + "description": "ID this is userid" + }, + "name": { + "type":"string", + "description": "This is Name" + } + } +} +``` + +### Usar etiqueta do tipo swaggertype para suportar o tipo personalizado +[#201](https://github.com/swaggo/swag/issues/201#issuecomment-475479409) + +```go +type TimestampTime struct { + time.Time +} + +///implement encoding.JSON.Marshaler interface +func (t *TimestampTime) MarshalJSON() ([]byte, error) { + bin := make([]byte, 16) + bin = strconv.AppendInt(bin[:0], t.Time.Unix(), 10) + return bin, nil +} + +func (t *TimestampTime) UnmarshalJSON(bin []byte) error { + v, err := strconv.ParseInt(string(bin), 10, 64) + if err != nil { + return err + } + t.Time = time.Unix(v, 0) + return nil +} +/// + +type Account struct { + // Override primitive type by simply specifying it via `swaggertype` tag + ID sql.NullInt64 `json:"id" swaggertype:"integer"` + + // Override struct type to a primitive type 'integer' by specifying it via `swaggertype` tag + RegisterTime TimestampTime `json:"register_time" swaggertype:"primitive,integer"` + + // Array types can be overridden using "array," format + Coeffs []big.Float `json:"coeffs" swaggertype:"array,number"` +} +``` + +[#379](https://github.com/swaggo/swag/issues/379) +```go +type CerticateKeyPair struct { + Crt []byte `json:"crt" swaggertype:"string" format:"base64" example:"U3dhZ2dlciByb2Nrcw=="` + Key []byte `json:"key" swaggertype:"string" format:"base64" example:"U3dhZ2dlciByb2Nrcw=="` +} +``` +generated swagger doc as follows: +```go +"api.MyBinding": { + "type":"object", + "properties":{ + "crt":{ + "type":"string", + "format":"base64", + "example":"U3dhZ2dlciByb2Nrcw==" + }, + "key":{ + "type":"string", + "format":"base64", + "example":"U3dhZ2dlciByb2Nrcw==" + } + } +} + +``` + +### Utilizar anulações globais para suportar um tipo personalizado + +Se estiver a utilizar ficheiros gerados, as etiquetas [`swaggertype`](#use-swaggertype-tag-to-supported-custom-type) ou `swaggerignore` podem não ser possíveis. + +Ao passar um mapeamento para swag com `--overridesFile` pode dizer swag para utilizar um tipo no lugar de outro onde quer que apareça. Por defeito, se um ficheiro `.swaggo` estiver presente no directório actual, será utilizado. + +Go code: +```go +type MyStruct struct { + ID sql.NullInt64 `json:"id"` + Name sql.NullString `json:"name"` +} +``` + +`.swaggo`: +``` +// Substituir todos os NullInt64 por int +replace database/sql.NullInt64 int + +// Não inclua quaisquer campos do tipo base de database/sql. +NullString no swagger docs +skip database/sql.NullString +``` + +As directivas possíveis são comentários (começando por `//`), `replace path/to/a.type path/to/b.type`, e `skip path/to/a.type`. + +(Note que os caminhos completos para qualquer tipo nomeado devem ser fornecidos para evitar problemas quando vários pacotes definem um tipo com o mesmo nome) + +Entregue em: +```go +"types.MyStruct": { + "id": "integer" +} + +### Use swaggerignore tag para excluir um campo + +```go +type Account struct { + ID string `json:"id"` + Name string `json:"name"` + Ignored int `swaggerignore:"true"` +} +``` + + +### Adicionar informações de extensão ao campo de estruturação + +```go +type Account struct { + ID string `json:"id" extensions:"x-nullable,x-abc=def,!x-omitempty"` // extensions fields must start with "x-" +} +``` + +gerar doc. de swagger como se segue: + +```go +"Account": { + "type": "object", + "properties": { + "id": { + "type": "string", + "x-nullable": true, + "x-abc": "def", + "x-omitempty": false + } + } +} +``` + + +### Renomear modelo a expor + +```golang +type Resp struct { + Code int +}//@name Response +``` + +### Como utilizar as anotações de segurança + +Informações API gerais. + +```go +// @securityDefinitions.basic BasicAuth + +// @securitydefinitions.oauth2.application OAuth2Application +// @tokenUrl https://example.com/oauth/token +// @scope.write Grants write access +// @scope.admin Grants read and write access to administrative information +``` + +Cada operação API. + +```go +// @Security ApiKeyAuth +``` + +Faça-o AND condicione-o + +```go +// @Security ApiKeyAuth +// @Security OAuth2Application[write, admin] +``` + +Faça-o OR condição + +```go +// @Security ApiKeyAuth || firebase +// @Security OAuth2Application[write, admin] || APIKeyAuth +``` + + + +### Adicionar uma descrição para enumerar artigos + +```go +type Example struct { + // Sort order: + // * asc - Ascending, from A to Z. + // * desc - Descending, from Z to A. + Order string `enums:"asc,desc"` +} +``` + +### Gerar apenas tipos de ficheiros de documentos específicos + +Por defeito, o comando `swag` gera especificação Swagger em três tipos diferentes de ficheiros/arquivos: +- docs.go +- swagger.json +- swagger.yaml + +Se desejar limitar um conjunto de tipos de ficheiros que devem ser gerados pode utilizar a bandeira `--outputTypes` (short `-ot`). O valor por defeito é `go,json,yaml` - tipos de saída separados por vírgula. Para limitar a saída apenas a ficheiros `go` e `yaml`, escrever-se-ia `go,yaml'. Com comando completo que seria `swag init --outputTypes go,yaml`. + +### Como usar tipos genéricos + +```go +// @Success 200 {object} web.GenericNestedResponse[types.Post] +// @Success 204 {object} web.GenericNestedResponse[types.Post, Types.AnotherOne] +// @Success 201 {object} web.GenericNestedResponse[web.GenericInnerType[types.Post]] +func GetPosts(w http.ResponseWriter, r *http.Request) { + _ = web.GenericNestedResponse[types.Post]{} +} +``` +Para mais detalhes e outros exemplos, veja [esse arquivo](https://github.com/swaggo/swag/blob/master/testdata/generics_nested/api/api.go) + +### Alterar os delimitadores de acção padrão Go Template +[#980](https://github.com/swaggo/swag/issues/980) +[#1177](https://github.com/swaggo/swag/issues/1177) + +Se as suas anotações ou campos estruturantes contêm "{{" or "}}", a geração de modelos irá muito provavelmente falhar, uma vez que estes são os delimitadores por defeito para [go templates](https://pkg.go.dev/text/template#Template.Delims). + +Para que a geração funcione correctamente, pode alterar os delimitadores por defeito com `-td'. Por exemplo: +``console +swag init -g http/api.go -td "[[,]" +``` + +O novo delimitador é um fio com o formato "``,``". + +## Sobre o projecto +Este projecto foi inspirado por [yvasiyarov/swagger](https://github.com/yvasiyarov/swagger) mas simplificámos a utilização e acrescentámos apoio a uma variedade de [frameworks web](#estruturas-web-suportadas). A fonte de imagem Gopher é [tenntenn/gopher-stickers](https://github.com/tenntenn/gopher-stickers). Tem licenças [creative commons licensing](http://creativecommons.org/licenses/by/3.0/deed.en). + +## Contribuidores + +Este projecto existe graças a todas as pessoas que contribuem. [[Contribute](CONTRIBUTING.md)]. + + + +## Apoios + +Obrigado a todos os nossos apoiantes! 🙏 [[Become a backer](https://opencollective.com/swag#backer)] + + + + +## Patrocinadores + +Apoiar este projecto tornando-se um patrocinador. O seu logótipo aparecerá aqui com um link para o seu website. [[Become a sponsor](https://opencollective.com/swag#sponsor)] + + + + + + + + + + + + + + +## Licença +[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fswaggo%2Fswag.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2Fswaggo%2Fswag?ref=badge_large) diff --git a/vendor/github.com/swaggo/swag/README_zh-CN.md b/vendor/github.com/swaggo/swag/README_zh-CN.md index d14f49851a..87d600b522 100644 --- a/vendor/github.com/swaggo/swag/README_zh-CN.md +++ b/vendor/github.com/swaggo/swag/README_zh-CN.md @@ -20,6 +20,7 @@ Swag将Go的注释转换为Swagger2.0文档。我们为流行的 [Go Web Framewo - [快速开始](#快速开始) - [支持的Web框架](#支持的web框架) - [如何与Gin集成](#如何与gin集成) +- [格式化说明](#格式化说明) - [开发现状](#开发现状) - [声明式注释格式](#声明式注释格式) - [通用API信息](#通用api信息) @@ -46,10 +47,10 @@ Swag将Go的注释转换为Swagger2.0文档。我们为流行的 [Go Web Framewo 2. 使用如下命令下载swag: ```bash -go get -u github.com/swaggo/swag/cmd/swag +go install github.com/swaggo/swag/cmd/swag@latest ``` -从源码开始构建的话,需要有Go环境(1.9及以上版本)。 +从源码开始构建的话,需要有Go环境(1.18及以上版本)。 或者从github的release页面下载预编译好的二进制文件。 @@ -59,12 +60,18 @@ go get -u github.com/swaggo/swag/cmd/swag swag init ``` -确保导入了生成的`docs/docs.go`文件,这样特定的配置文件才会被初始化。如果通用API指数没有写在`main.go`中,可以使用`-g`标识符来告知swag。 +确保导入了生成的`docs/docs.go`文件,这样特定的配置文件才会被初始化。如果通用API注释没有写在`main.go`中,可以使用`-g`标识符来告知swag。 ```bash swag init -g http/api.go ``` +4. (可选) 使用`fmt`格式化 SWAG 注释。(请先升级到最新版本) + +```bash +swag fmt +``` + ## swag cli ```bash @@ -76,14 +83,35 @@ USAGE: swag init [command options] [arguments...] OPTIONS: - --generalInfo value, -g value API通用信息所在的go源文件路径,如果是相对路径则基于API解析目录 (默认: "main.go") - --dir value, -d value API解析目录 (默认: "./") - --propertyStrategy value, -p value 结构体字段命名规则,三种:snakecase,camelcase,pascalcase (默认: "camelcase") - --output value, -o value 文件(swagger.json, swagger.yaml and doc.go)输出目录 (默认: "./docs") - --parseVendor 是否解析vendor目录里的go源文件,默认不 - --parseDependency 是否解析依赖目录中的go源文件,默认不 - --markdownFiles value, --md value 指定API的描述信息所使用的markdown文件所在的目录 - --generatedTime 是否输出时间到输出文件docs.go的顶部,默认是 + --generalInfo value, -g value API通用信息所在的go源文件路径,如果是相对路径则基于API解析目录 (默认: "main.go") + --dir value, -d value API解析目录 (默认: "./") + --exclude value 解析扫描时排除的目录,多个目录可用逗号分隔(默认:空) + --propertyStrategy value, -p value 结构体字段命名规则,三种:snakecase,camelcase,pascalcase (默认: "camelcase") + --output value, -o value 文件(swagger.json, swagger.yaml and doc.go)输出目录 (默认: "./docs") + --parseVendor 是否解析vendor目录里的go源文件,默认不 + --parseDependency 是否解析依赖目录中的go源文件,默认不 + --markdownFiles value, --md value 指定API的描述信息所使用的markdown文件所在的目录 + --generatedTime 是否输出时间到输出文件docs.go的顶部,默认是 + --codeExampleFiles value, --cef value 解析包含用于 x-codeSamples 扩展的代码示例文件的文件夹,默认禁用 + --parseInternal 解析 internal 包中的go文件,默认禁用 + --parseDepth value 依赖解析深度 (默认: 100) + --instanceName value 设置文档实例名 (默认: "swagger") +``` + +```bash +swag fmt -h +NAME: + swag fmt - format swag comments + +USAGE: + swag fmt [command options] [arguments...] + +OPTIONS: + --dir value, -d value API解析目录 (默认: "./") + --exclude value 解析扫描时排除的目录,多个目录可用逗号分隔(默认:空) + --generalInfo value, -g value API通用信息所在的go源文件路径,如果是相对路径则基于API解析目录 (默认: "main.go") + --help, -h show help (default: false) + ``` ## 支持的Web框架 @@ -92,6 +120,12 @@ OPTIONS: - [echo](http://github.com/swaggo/echo-swagger) - [buffalo](https://github.com/swaggo/buffalo-swagger) - [net/http](https://github.com/swaggo/http-swagger) +- [gorilla/mux](https://github.com/swaggo/http-swagger) +- [go-chi/chi](https://github.com/swaggo/http-swagger) +- [flamingo](https://github.com/i-love-flamingo/swagger) +- [fiber](https://github.com/gofiber/swagger) +- [atreugo](https://github.com/Nerzal/atreugo-swagger) +- [hertz](https://github.com/hertz-contrib/swagger) ## 如何与Gin集成 @@ -106,52 +140,26 @@ import "github.com/swaggo/files" // swagger embed files 2. 在`main.go`源代码中添加通用的API注释: -```bash -// @title Swagger Example API -// @version 1.0 -// @description This is a sample server celler server. -// @termsOfService http://swagger.io/terms/ - -// @contact.name API Support -// @contact.url http://www.swagger.io/support -// @contact.email support@swagger.io +```go +// @title Swagger Example API +// @version 1.0 +// @description This is a sample server celler server. +// @termsOfService http://swagger.io/terms/ -// @license.name Apache 2.0 -// @license.url http://www.apache.org/licenses/LICENSE-2.0.html +// @contact.name API Support +// @contact.url http://www.swagger.io/support +// @contact.email support@swagger.io -// @host localhost:8080 -// @BasePath /api/v1 -// @query.collection.format multi +// @license.name Apache 2.0 +// @license.url http://www.apache.org/licenses/LICENSE-2.0.html -// @securityDefinitions.basic BasicAuth +// @host localhost:8080 +// @BasePath /api/v1 -// @securityDefinitions.apikey ApiKeyAuth -// @in header -// @name Authorization - -// @securitydefinitions.oauth2.application OAuth2Application -// @tokenUrl https://example.com/oauth/token -// @scope.write Grants write access -// @scope.admin Grants read and write access to administrative information - -// @securitydefinitions.oauth2.implicit OAuth2Implicit -// @authorizationurl https://example.com/oauth/authorize -// @scope.write Grants write access -// @scope.admin Grants read and write access to administrative information - -// @securitydefinitions.oauth2.password OAuth2Password -// @tokenUrl https://example.com/oauth/token -// @scope.read Grants read access -// @scope.write Grants write access -// @scope.admin Grants read and write access to administrative information - -// @securitydefinitions.oauth2.accessCode OAuth2AccessCode -// @tokenUrl https://example.com/oauth/token -// @authorizationurl https://example.com/oauth/authorize -// @scope.admin Grants read and write access to administrative information - -// @x-extension-openapi {"example": "value on a json format"} +// @securityDefinitions.basic BasicAuth +// @externalDocs.description OpenAPI +// @externalDocs.url https://swagger.io/resources/open-api/ func main() { r := gin.Default() @@ -189,15 +197,12 @@ import ( "./docs" // docs is generated by Swag CLI, you have to import it. ) -// @contact.name API Support -// @contact.url http://www.swagger.io/support -// @contact.email support@swagger.io - -// @license.name Apache 2.0 -// @license.url http://www.apache.org/licenses/LICENSE-2.0.html - -// @termsOfService http://swagger.io/terms/ +// @contact.name API Support +// @contact.url http://www.swagger.io/support +// @contact.email support@swagger.io +// @license.name Apache 2.0 +// @license.url http://www.apache.org/licenses/LICENSE-2.0.html func main() { // programatically set swagger info @@ -233,55 +238,53 @@ import ( ) // ShowAccount godoc -// @Summary Show a account -// @Description get string by ID -// @ID get-string-by-int -// @Accept json -// @Produce json -// @Param id path int true "Account ID" -// @Success 200 {object} model.Account -// @Header 200 {string} Token "qwerty" -// @Failure 400 {object} httputil.HTTPError -// @Failure 404 {object} httputil.HTTPError -// @Failure 500 {object} httputil.HTTPError -// @Router /accounts/{id} [get] +// @Summary Show an account +// @Description get string by ID +// @Tags accounts +// @Accept json +// @Produce json +// @Param id path int true "Account ID" +// @Success 200 {object} model.Account +// @Failure 400 {object} httputil.HTTPError +// @Failure 404 {object} httputil.HTTPError +// @Failure 500 {object} httputil.HTTPError +// @Router /accounts/{id} [get] func (c *Controller) ShowAccount(ctx *gin.Context) { - id := ctx.Param("id") - aid, err := strconv.Atoi(id) - if err != nil { - httputil.NewError(ctx, http.StatusBadRequest, err) - return - } - account, err := model.AccountOne(aid) - if err != nil { - httputil.NewError(ctx, http.StatusNotFound, err) - return - } - ctx.JSON(http.StatusOK, account) + id := ctx.Param("id") + aid, err := strconv.Atoi(id) + if err != nil { + httputil.NewError(ctx, http.StatusBadRequest, err) + return + } + account, err := model.AccountOne(aid) + if err != nil { + httputil.NewError(ctx, http.StatusNotFound, err) + return + } + ctx.JSON(http.StatusOK, account) } // ListAccounts godoc -// @Summary List accounts -// @Description get accounts -// @Accept json -// @Produce json -// @Param q query string false "name search by q" -// @Success 200 {array} model.Account -// @Header 200 {string} Token "qwerty" -// @Failure 400 {object} httputil.HTTPError -// @Failure 404 {object} httputil.HTTPError -// @Failure 500 {object} httputil.HTTPError -// @Router /accounts [get] +// @Summary List accounts +// @Description get accounts +// @Tags accounts +// @Accept json +// @Produce json +// @Param q query string false "name search by q" Format(email) +// @Success 200 {array} model.Account +// @Failure 400 {object} httputil.HTTPError +// @Failure 404 {object} httputil.HTTPError +// @Failure 500 {object} httputil.HTTPError +// @Router /accounts [get] func (c *Controller) ListAccounts(ctx *gin.Context) { - q := ctx.Request.URL.Query().Get("q") - accounts, err := model.AccountsAll(q) - if err != nil { - httputil.NewError(ctx, http.StatusNotFound, err) - return - } - ctx.JSON(http.StatusOK, accounts) + q := ctx.Request.URL.Query().Get("q") + accounts, err := model.AccountsAll(q) + if err != nil { + httputil.NewError(ctx, http.StatusNotFound, err) + return + } + ctx.JSON(http.StatusOK, accounts) } - //... ``` @@ -289,10 +292,25 @@ func (c *Controller) ListAccounts(ctx *gin.Context) { swag init ``` -4. 运行程序,然后在浏览器中访问 http://localhost:8080/swagger/index.html。将看到Swagger 2.0 Api文档,如下所示: +4. 运行程序,然后在浏览器中访问 http://localhost:8080/swagger/index.html 。将看到Swagger 2.0 Api文档,如下所示: ![swagger_index.html](https://raw.githubusercontent.com/swaggo/swag/master/assets/swagger-image.png) +## 格式化说明 + +可以针对Swag的注释自动格式化,就像`go fmt`。 +此处查看格式化结果 [here](https://github.com/swaggo/swag/tree/master/example/celler). + +示例: +```shell +swag fmt +``` + +排除目录(不扫描)示例: +```shell +swag fmt -d ./ --exclude ./internal +``` + ## 开发现状 [Swagger 2.0 文档](https://swagger.io/docs/specification/2-0/basic-structure/) @@ -336,8 +354,12 @@ swag init | license.url | 用于API的许可证的URL。 必须采用网址格式。 | // @license.url http://www.apache.org/licenses/LICENSE-2.0.html | | host | 运行API的主机(主机名或IP地址)。 | // @host localhost:8080 | | BasePath | 运行API的基本路径。 | // @BasePath /api/v1 | +| accept | API 可以使用的 MIME 类型列表。 请注意,Accept 仅影响具有请求正文的操作,例如 POST、PUT 和 PATCH。 值必须如“[Mime类型](#mime类型)”中所述。 | // @accept json | +| produce | API可以生成的MIME类型的列表。值必须如“[Mime类型](#mime类型)”中所述。 | // @produce json | | query.collection.format | 请求URI query里数组参数的默认格式:csv,multi,pipes,tsv,ssv。 如果未设置,则默认为csv。 | // @query.collection.format multi | | schemes | 用空格分隔的请求的传输协议。 | // @schemes http https | +| externalDocs.description | Description of the external document. | // @externalDocs.description OpenAPI | +| externalDocs.url | URL of the external document. | // @externalDocs.url https://swagger.io/resources/open-api/ | | x-name | 扩展的键必须以x-开头,并且只能使用json值 | // @x-example-key {"key": "value"} | ### 使用Markdown描述 @@ -356,26 +378,29 @@ swag init Example [celler/controller](https://github.com/swaggo/swag/tree/master/example/celler/controller) -| 注释 | 描述 | | -| -------------------- | ------------------------------------------------------------------------------------------------------- | -------------------------------------------------- | -| description | 操作行为的详细说明。 | -| description.markdown | 应用程序的简短描述。该描述将从名为`endpointname.md`的文件中读取。 | // @description.file endpoint.description.markdown | -| id | 用于标识操作的唯一字符串。在所有API操作中必须唯一。 | -| tags | 每个API操作的标签列表,以逗号分隔。 | +| 注释 | 描述 | +|----------------------|------------------------------------------------------------------------------------------------| +| description | 操作行为的详细说明。 | +| description.markdown | 应用程序的简短描述。该描述将从名为`endpointname.md`的文件中读取。 | +| id | 用于标识操作的唯一字符串。在所有API操作中必须唯一。 | +| tags | 每个API操作的标签列表,以逗号分隔。 | | summary | 该操作的简短摘要。 | -| accept | API可以使用的MIME类型的列表。值必须如“[Mime类型](#mime-types)”中所述。 | -| produce | API可以生成的MIME类型的列表。值必须如“[Mime类型](#mime-types)”中所述。 | +| accept | API 可以使用的 MIME 类型列表。 请注意,Accept 仅影响具有请求正文的操作,例如 POST、PUT 和 PATCH。 值必须如“[Mime类型](#mime类型)”中所述。 | +| produce | API可以生成的MIME类型的列表。值必须如“[Mime类型](#mime类型)”中所述。 | | param | 用空格分隔的参数。`param name`,`param type`,`data type`,`is mandatory?`,`comment` `attribute(optional)` | -| security | 每个API操作的[安全性](#security)。 | -| success | 以空格分隔的成功响应。`return code`,`{param type}`,`data type`,`comment` | -| failure | 以空格分隔的故障响应。`return code`,`{param type}`,`data type`,`comment` | -| header | 以空格分隔的头字段。 `return code`,`{param type}`,`data type`,`comment` | -| router | 以空格分隔的路径定义。 `path`,`[httpMethod]` | -| x-name | 扩展字段必须以`x-`开头,并且只能使用json值。 | +| security | 每个API操作的[安全性](#安全性)。 | +| success | 以空格分隔的成功响应。`return code`,`{param type}`,`data type`,`comment` | +| failure | 以空格分隔的故障响应。`return code`,`{param type}`,`data type`,`comment` | +| response | 与success、failure作用相同 | +| header | 以空格分隔的头字段。 `return code`,`{param type}`,`data type`,`comment` | +| router | 以空格分隔的路径定义。 `path`,`[httpMethod]` | +| deprecatedrouter | 与router相同,但是是deprecated的。 | +| x-name | 扩展字段必须以`x-`开头,并且只能使用json值。 | +| deprecated | 将当前API操作的所有路径设置为deprecated | ## Mime类型 -`swag` g接受所有格式正确的MIME类型, 即使匹配 `*/*`。除此之外,`swag`还接受某些MIME类型的别名,如下所示: +`swag` 接受所有格式正确的MIME类型, 即使匹配 `*/*`。除此之外,`swag`还接受某些MIME类型的别名,如下所示: | Alias | MIME Type | | --------------------- | --------------------------------- | @@ -430,13 +455,14 @@ Example [celler/controller](https://github.com/swaggo/swag/tree/master/example/c ## 属性 ```go -// @Param enumstring query string false "string enums" Enums(A, B, C) -// @Param enumint query int false "int enums" Enums(1, 2, 3) -// @Param enumnumber query number false "int enums" Enums(1.1, 1.2, 1.3) -// @Param string query string false "string valid" minlength(5) maxlength(10) -// @Param int query int false "int valid" mininum(1) maxinum(10) -// @Param default query string false "string default" default(A) -// @Param collection query []string false "string collection" collectionFormat(multi) +// @Param enumstring query string false "string enums" Enums(A, B, C) +// @Param enumint query int false "int enums" Enums(1, 2, 3) +// @Param enumnumber query number false "int enums" Enums(1.1, 1.2, 1.3) +// @Param string query string false "string valid" minlength(5) maxlength(10) +// @Param int query int false "int valid" minimum(1) maximum(10) +// @Param default query string false "string default" default(A) +// @Param collection query []string false "string collection" collectionFormat(multi) +// @Param extensions query []string false "string collection" extensions(x-example=test,x-nullable) ``` 也适用于结构体字段: @@ -535,17 +561,20 @@ type Order struct { //in `proto` package ### 在响应中增加头字段 ```go -// @Success 200 {string} string "ok" -// @Header 200 {string} Location "/entity/1" -// @Header 200 {string} Token "qwerty" +// @Success 200 {string} string "ok" +// @failure 400 {string} string "error" +// @response default {string} string "other error" +// @Header 200 {string} Location "/entity/1" +// @Header 200,400,default {string} Token "token" +// @Header all {string} Token2 "token2" ``` ### 使用多路径参数 ```go /// ... -// @Param group_id path int true "Group ID" -// @Param account_id path int true "Account ID" +// @Param group_id path int true "Group ID" +// @Param account_id path int true "Account ID" // ... // @Router /examples/groups/{group_id}/accounts/{account_id} [get] ``` @@ -652,7 +681,7 @@ type Account struct { ```go type Account struct { - ID string `json:"id" extensions:"x-nullable,x-abc=def"` // 扩展字段必须以"x-"开头 + ID string `json:"id" extensions:"x-nullable,x-abc=def,!x-omitempty"` // 扩展字段必须以"x-"开头 } ``` @@ -665,7 +694,8 @@ type Account struct { "id": { "type": "string", "x-nullable": true, - "x-abc": "def" + "x-abc": "def", + "x-omitempty": false } } } diff --git a/vendor/github.com/swaggo/swag/const.go b/vendor/github.com/swaggo/swag/const.go new file mode 100644 index 0000000000..83755103b7 --- /dev/null +++ b/vendor/github.com/swaggo/swag/const.go @@ -0,0 +1,567 @@ +package swag + +import ( + "go/ast" + "go/token" + "reflect" + "strconv" + "strings" + "unicode/utf8" +) + +// ConstVariable a model to record a const variable +type ConstVariable struct { + Name *ast.Ident + Type ast.Expr + Value interface{} + Comment *ast.CommentGroup + File *ast.File + Pkg *PackageDefinitions +} + +var escapedChars = map[uint8]uint8{ + 'n': '\n', + 'r': '\r', + 't': '\t', + 'v': '\v', + '\\': '\\', + '"': '"', +} + +// EvaluateEscapedChar parse escaped character +func EvaluateEscapedChar(text string) rune { + if len(text) == 1 { + return rune(text[0]) + } + + if len(text) == 2 && text[0] == '\\' { + return rune(escapedChars[text[1]]) + } + + if len(text) == 6 && text[0:2] == "\\u" { + n, err := strconv.ParseInt(text[2:], 16, 32) + if err == nil { + return rune(n) + } + } + + return 0 +} + +// EvaluateEscapedString parse escaped characters in string +func EvaluateEscapedString(text string) string { + if !strings.ContainsRune(text, '\\') { + return text + } + result := make([]byte, 0, len(text)) + for i := 0; i < len(text); i++ { + if text[i] == '\\' { + i++ + if text[i] == 'u' { + i++ + char, err := strconv.ParseInt(text[i:i+4], 16, 32) + if err == nil { + result = utf8.AppendRune(result, rune(char)) + } + i += 3 + } else if c, ok := escapedChars[text[i]]; ok { + result = append(result, c) + } + } else { + result = append(result, text[i]) + } + } + return string(result) +} + +// EvaluateDataConversion evaluate the type a explicit type conversion +func EvaluateDataConversion(x interface{}, typeName string) interface{} { + switch value := x.(type) { + case int: + switch typeName { + case "int": + return int(value) + case "byte": + return byte(value) + case "int8": + return int8(value) + case "int16": + return int16(value) + case "int32": + return int32(value) + case "int64": + return int64(value) + case "uint": + return uint(value) + case "uint8": + return uint8(value) + case "uint16": + return uint16(value) + case "uint32": + return uint32(value) + case "uint64": + return uint64(value) + case "rune": + return rune(value) + } + case uint: + switch typeName { + case "int": + return int(value) + case "byte": + return byte(value) + case "int8": + return int8(value) + case "int16": + return int16(value) + case "int32": + return int32(value) + case "int64": + return int64(value) + case "uint": + return uint(value) + case "uint8": + return uint8(value) + case "uint16": + return uint16(value) + case "uint32": + return uint32(value) + case "uint64": + return uint64(value) + case "rune": + return rune(value) + } + case int8: + switch typeName { + case "int": + return int(value) + case "byte": + return byte(value) + case "int8": + return int8(value) + case "int16": + return int16(value) + case "int32": + return int32(value) + case "int64": + return int64(value) + case "uint": + return uint(value) + case "uint8": + return uint8(value) + case "uint16": + return uint16(value) + case "uint32": + return uint32(value) + case "uint64": + return uint64(value) + case "rune": + return rune(value) + } + case uint8: + switch typeName { + case "int": + return int(value) + case "byte": + return byte(value) + case "int8": + return int8(value) + case "int16": + return int16(value) + case "int32": + return int32(value) + case "int64": + return int64(value) + case "uint": + return uint(value) + case "uint8": + return uint8(value) + case "uint16": + return uint16(value) + case "uint32": + return uint32(value) + case "uint64": + return uint64(value) + case "rune": + return rune(value) + } + case int16: + switch typeName { + case "int": + return int(value) + case "byte": + return byte(value) + case "int8": + return int8(value) + case "int16": + return int16(value) + case "int32": + return int32(value) + case "int64": + return int64(value) + case "uint": + return uint(value) + case "uint8": + return uint8(value) + case "uint16": + return uint16(value) + case "uint32": + return uint32(value) + case "uint64": + return uint64(value) + case "rune": + return rune(value) + } + case uint16: + switch typeName { + case "int": + return int(value) + case "byte": + return byte(value) + case "int8": + return int8(value) + case "int16": + return int16(value) + case "int32": + return int32(value) + case "int64": + return int64(value) + case "uint": + return uint(value) + case "uint8": + return uint8(value) + case "uint16": + return uint16(value) + case "uint32": + return uint32(value) + case "uint64": + return uint64(value) + case "rune": + return rune(value) + } + case int32: + switch typeName { + case "int": + return int(value) + case "byte": + return byte(value) + case "int8": + return int8(value) + case "int16": + return int16(value) + case "int32": + return int32(value) + case "int64": + return int64(value) + case "uint": + return uint(value) + case "uint8": + return uint8(value) + case "uint16": + return uint16(value) + case "uint32": + return uint32(value) + case "uint64": + return uint64(value) + case "rune": + return rune(value) + case "string": + return string(value) + } + case uint32: + switch typeName { + case "int": + return int(value) + case "byte": + return byte(value) + case "int8": + return int8(value) + case "int16": + return int16(value) + case "int32": + return int32(value) + case "int64": + return int64(value) + case "uint": + return uint(value) + case "uint8": + return uint8(value) + case "uint16": + return uint16(value) + case "uint32": + return uint32(value) + case "uint64": + return uint64(value) + case "rune": + return rune(value) + } + case int64: + switch typeName { + case "int": + return int(value) + case "byte": + return byte(value) + case "int8": + return int8(value) + case "int16": + return int16(value) + case "int32": + return int32(value) + case "int64": + return int64(value) + case "uint": + return uint(value) + case "uint8": + return uint8(value) + case "uint16": + return uint16(value) + case "uint32": + return uint32(value) + case "uint64": + return uint64(value) + case "rune": + return rune(value) + } + case uint64: + switch typeName { + case "int": + return int(value) + case "byte": + return byte(value) + case "int8": + return int8(value) + case "int16": + return int16(value) + case "int32": + return int32(value) + case "int64": + return int64(value) + case "uint": + return uint(value) + case "uint8": + return uint8(value) + case "uint16": + return uint16(value) + case "uint32": + return uint32(value) + case "uint64": + return uint64(value) + case "rune": + return rune(value) + } + case string: + switch typeName { + case "string": + return value + } + } + return nil +} + +// EvaluateUnary evaluate the type and value of a unary expression +func EvaluateUnary(x interface{}, operator token.Token, xtype ast.Expr) (interface{}, ast.Expr) { + switch operator { + case token.SUB: + switch value := x.(type) { + case int: + return -value, xtype + case int8: + return -value, xtype + case int16: + return -value, xtype + case int32: + return -value, xtype + case int64: + return -value, xtype + } + case token.XOR: + switch value := x.(type) { + case int: + return ^value, xtype + case int8: + return ^value, xtype + case int16: + return ^value, xtype + case int32: + return ^value, xtype + case int64: + return ^value, xtype + case uint: + return ^value, xtype + case uint8: + return ^value, xtype + case uint16: + return ^value, xtype + case uint32: + return ^value, xtype + case uint64: + return ^value, xtype + } + } + return nil, nil +} + +// EvaluateBinary evaluate the type and value of a binary expression +func EvaluateBinary(x, y interface{}, operator token.Token, xtype, ytype ast.Expr) (interface{}, ast.Expr) { + if operator == token.SHR || operator == token.SHL { + var rightOperand uint64 + yValue := reflect.ValueOf(y) + if yValue.CanUint() { + rightOperand = yValue.Uint() + } else if yValue.CanInt() { + rightOperand = uint64(yValue.Int()) + } + + switch operator { + case token.SHL: + switch xValue := x.(type) { + case int: + return xValue << rightOperand, xtype + case int8: + return xValue << rightOperand, xtype + case int16: + return xValue << rightOperand, xtype + case int32: + return xValue << rightOperand, xtype + case int64: + return xValue << rightOperand, xtype + case uint: + return xValue << rightOperand, xtype + case uint8: + return xValue << rightOperand, xtype + case uint16: + return xValue << rightOperand, xtype + case uint32: + return xValue << rightOperand, xtype + case uint64: + return xValue << rightOperand, xtype + } + case token.SHR: + switch xValue := x.(type) { + case int: + return xValue >> rightOperand, xtype + case int8: + return xValue >> rightOperand, xtype + case int16: + return xValue >> rightOperand, xtype + case int32: + return xValue >> rightOperand, xtype + case int64: + return xValue >> rightOperand, xtype + case uint: + return xValue >> rightOperand, xtype + case uint8: + return xValue >> rightOperand, xtype + case uint16: + return xValue >> rightOperand, xtype + case uint32: + return xValue >> rightOperand, xtype + case uint64: + return xValue >> rightOperand, xtype + } + } + return nil, nil + } + + evalType := xtype + if evalType == nil { + evalType = ytype + } + + xValue := reflect.ValueOf(x) + yValue := reflect.ValueOf(y) + if xValue.Kind() == reflect.String && yValue.Kind() == reflect.String { + return xValue.String() + yValue.String(), evalType + } + + var targetValue reflect.Value + if xValue.Kind() != reflect.Int { + targetValue = reflect.New(xValue.Type()).Elem() + } else { + targetValue = reflect.New(yValue.Type()).Elem() + } + + switch operator { + case token.ADD: + if xValue.CanInt() && yValue.CanInt() { + targetValue.SetInt(xValue.Int() + yValue.Int()) + } else if xValue.CanUint() && yValue.CanUint() { + targetValue.SetUint(xValue.Uint() + yValue.Uint()) + } else if xValue.CanInt() && yValue.CanUint() { + targetValue.SetUint(uint64(xValue.Int()) + yValue.Uint()) + } else if xValue.CanUint() && yValue.CanInt() { + targetValue.SetUint(xValue.Uint() + uint64(yValue.Int())) + } + case token.SUB: + if xValue.CanInt() && yValue.CanInt() { + targetValue.SetInt(xValue.Int() - yValue.Int()) + } else if xValue.CanUint() && yValue.CanUint() { + targetValue.SetUint(xValue.Uint() - yValue.Uint()) + } else if xValue.CanInt() && yValue.CanUint() { + targetValue.SetUint(uint64(xValue.Int()) - yValue.Uint()) + } else if xValue.CanUint() && yValue.CanInt() { + targetValue.SetUint(xValue.Uint() - uint64(yValue.Int())) + } + case token.MUL: + if xValue.CanInt() && yValue.CanInt() { + targetValue.SetInt(xValue.Int() * yValue.Int()) + } else if xValue.CanUint() && yValue.CanUint() { + targetValue.SetUint(xValue.Uint() * yValue.Uint()) + } else if xValue.CanInt() && yValue.CanUint() { + targetValue.SetUint(uint64(xValue.Int()) * yValue.Uint()) + } else if xValue.CanUint() && yValue.CanInt() { + targetValue.SetUint(xValue.Uint() * uint64(yValue.Int())) + } + case token.QUO: + if xValue.CanInt() && yValue.CanInt() { + targetValue.SetInt(xValue.Int() / yValue.Int()) + } else if xValue.CanUint() && yValue.CanUint() { + targetValue.SetUint(xValue.Uint() / yValue.Uint()) + } else if xValue.CanInt() && yValue.CanUint() { + targetValue.SetUint(uint64(xValue.Int()) / yValue.Uint()) + } else if xValue.CanUint() && yValue.CanInt() { + targetValue.SetUint(xValue.Uint() / uint64(yValue.Int())) + } + case token.REM: + if xValue.CanInt() && yValue.CanInt() { + targetValue.SetInt(xValue.Int() % yValue.Int()) + } else if xValue.CanUint() && yValue.CanUint() { + targetValue.SetUint(xValue.Uint() % yValue.Uint()) + } else if xValue.CanInt() && yValue.CanUint() { + targetValue.SetUint(uint64(xValue.Int()) % yValue.Uint()) + } else if xValue.CanUint() && yValue.CanInt() { + targetValue.SetUint(xValue.Uint() % uint64(yValue.Int())) + } + case token.AND: + if xValue.CanInt() && yValue.CanInt() { + targetValue.SetInt(xValue.Int() & yValue.Int()) + } else if xValue.CanUint() && yValue.CanUint() { + targetValue.SetUint(xValue.Uint() & yValue.Uint()) + } else if xValue.CanInt() && yValue.CanUint() { + targetValue.SetUint(uint64(xValue.Int()) & yValue.Uint()) + } else if xValue.CanUint() && yValue.CanInt() { + targetValue.SetUint(xValue.Uint() & uint64(yValue.Int())) + } + case token.OR: + if xValue.CanInt() && yValue.CanInt() { + targetValue.SetInt(xValue.Int() | yValue.Int()) + } else if xValue.CanUint() && yValue.CanUint() { + targetValue.SetUint(xValue.Uint() | yValue.Uint()) + } else if xValue.CanInt() && yValue.CanUint() { + targetValue.SetUint(uint64(xValue.Int()) | yValue.Uint()) + } else if xValue.CanUint() && yValue.CanInt() { + targetValue.SetUint(xValue.Uint() | uint64(yValue.Int())) + } + case token.XOR: + if xValue.CanInt() && yValue.CanInt() { + targetValue.SetInt(xValue.Int() ^ yValue.Int()) + } else if xValue.CanUint() && yValue.CanUint() { + targetValue.SetUint(xValue.Uint() ^ yValue.Uint()) + } else if xValue.CanInt() && yValue.CanUint() { + targetValue.SetUint(uint64(xValue.Int()) ^ yValue.Uint()) + } else if xValue.CanUint() && yValue.CanInt() { + targetValue.SetUint(xValue.Uint() ^ uint64(yValue.Int())) + } + } + return targetValue.Interface(), evalType +} diff --git a/vendor/github.com/swaggo/swag/debug.go b/vendor/github.com/swaggo/swag/debug.go deleted file mode 100644 index f28a8e653f..0000000000 --- a/vendor/github.com/swaggo/swag/debug.go +++ /dev/null @@ -1,30 +0,0 @@ -package swag - -import ( - "log" -) - -const ( - test = iota - release -) - -var swagMode = release - -func isRelease() bool { - return swagMode == release -} - -// Println calls Output to print to the standard logger when release mode. -func Println(v ...interface{}) { - if isRelease() { - log.Println(v...) - } -} - -// Printf calls Output to print to the standard logger when release mode. -func Printf(format string, v ...interface{}) { - if isRelease() { - log.Printf(format, v...) - } -} diff --git a/vendor/github.com/swaggo/swag/enums.go b/vendor/github.com/swaggo/swag/enums.go new file mode 100644 index 0000000000..38658f20ad --- /dev/null +++ b/vendor/github.com/swaggo/swag/enums.go @@ -0,0 +1,13 @@ +package swag + +const ( + enumVarNamesExtension = "x-enum-varnames" + enumCommentsExtension = "x-enum-comments" +) + +// EnumValue a model to record an enum consts variable +type EnumValue struct { + key string + Value interface{} + Comment string +} diff --git a/vendor/github.com/swaggo/swag/field_parser.go b/vendor/github.com/swaggo/swag/field_parser.go new file mode 100644 index 0000000000..9b24e7872a --- /dev/null +++ b/vendor/github.com/swaggo/swag/field_parser.go @@ -0,0 +1,680 @@ +package swag + +import ( + "fmt" + "go/ast" + "reflect" + "regexp" + "strconv" + "strings" + "sync" + "unicode" + + "github.com/go-openapi/spec" +) + +var _ FieldParser = &tagBaseFieldParser{p: nil, field: nil, tag: ""} + +const ( + requiredLabel = "required" + optionalLabel = "optional" + swaggerTypeTag = "swaggertype" + swaggerIgnoreTag = "swaggerignore" +) + +type tagBaseFieldParser struct { + p *Parser + field *ast.Field + tag reflect.StructTag +} + +func newTagBaseFieldParser(p *Parser, field *ast.Field) FieldParser { + fieldParser := tagBaseFieldParser{ + p: p, + field: field, + tag: "", + } + if fieldParser.field.Tag != nil { + fieldParser.tag = reflect.StructTag(strings.ReplaceAll(field.Tag.Value, "`", "")) + } + + return &fieldParser +} + +func (ps *tagBaseFieldParser) ShouldSkip() bool { + // Skip non-exported fields. + if ps.field.Names != nil && !ast.IsExported(ps.field.Names[0].Name) { + return true + } + + if ps.field.Tag == nil { + return false + } + + ignoreTag := ps.tag.Get(swaggerIgnoreTag) + if strings.EqualFold(ignoreTag, "true") { + return true + } + + // json:"tag,hoge" + name := strings.TrimSpace(strings.Split(ps.tag.Get(jsonTag), ",")[0]) + if name == "-" { + return true + } + + return false +} + +func (ps *tagBaseFieldParser) FieldName() (string, error) { + var name string + + if ps.field.Tag != nil { + // json:"tag,hoge" + name = strings.TrimSpace(strings.Split(ps.tag.Get(jsonTag), ",")[0]) + if name != "" { + return name, nil + } + + // use "form" tag over json tag + name = ps.FormName() + if name != "" { + return name, nil + } + } + + if ps.field.Names == nil { + return "", nil + } + + switch ps.p.PropNamingStrategy { + case SnakeCase: + return toSnakeCase(ps.field.Names[0].Name), nil + case PascalCase: + return ps.field.Names[0].Name, nil + default: + return toLowerCamelCase(ps.field.Names[0].Name), nil + } +} + +func (ps *tagBaseFieldParser) firstTagValue(tag string) string { + if ps.field.Tag != nil { + return strings.TrimRight(strings.TrimSpace(strings.Split(ps.tag.Get(tag), ",")[0]), "[]") + } + return "" +} + +func (ps *tagBaseFieldParser) FormName() string { + return ps.firstTagValue(formTag) +} + +func (ps *tagBaseFieldParser) HeaderName() string { + return ps.firstTagValue(headerTag) +} + +func (ps *tagBaseFieldParser) PathName() string { + return ps.firstTagValue(uriTag) +} + +func toSnakeCase(in string) string { + var ( + runes = []rune(in) + length = len(runes) + out []rune + ) + + for idx := 0; idx < length; idx++ { + if idx > 0 && unicode.IsUpper(runes[idx]) && + ((idx+1 < length && unicode.IsLower(runes[idx+1])) || unicode.IsLower(runes[idx-1])) { + out = append(out, '_') + } + + out = append(out, unicode.ToLower(runes[idx])) + } + + return string(out) +} + +func toLowerCamelCase(in string) string { + var flag bool + + out := make([]rune, len(in)) + + runes := []rune(in) + for i, curr := range runes { + if (i == 0 && unicode.IsUpper(curr)) || (flag && unicode.IsUpper(curr)) { + out[i] = unicode.ToLower(curr) + flag = true + + continue + } + + out[i] = curr + flag = false + } + + return string(out) +} + +func (ps *tagBaseFieldParser) CustomSchema() (*spec.Schema, error) { + if ps.field.Tag == nil { + return nil, nil + } + + typeTag := ps.tag.Get(swaggerTypeTag) + if typeTag != "" { + return BuildCustomSchema(strings.Split(typeTag, ",")) + } + + return nil, nil +} + +type structField struct { + schemaType string + arrayType string + formatType string + maximum *float64 + minimum *float64 + multipleOf *float64 + maxLength *int64 + minLength *int64 + maxItems *int64 + minItems *int64 + exampleValue interface{} + enums []interface{} + enumVarNames []interface{} + unique bool +} + +// splitNotWrapped slices s into all substrings separated by sep if sep is not +// wrapped by brackets and returns a slice of the substrings between those separators. +func splitNotWrapped(s string, sep rune) []string { + openCloseMap := map[rune]rune{ + '(': ')', + '[': ']', + '{': '}', + } + + var ( + result = make([]string, 0) + current = strings.Builder{} + openCount = 0 + openChar rune + ) + + for _, char := range s { + switch { + case openChar == 0 && openCloseMap[char] != 0: + openChar = char + + openCount++ + + current.WriteRune(char) + case char == openChar: + openCount++ + + current.WriteRune(char) + case openCount > 0 && char == openCloseMap[openChar]: + openCount-- + + current.WriteRune(char) + case openCount == 0 && char == sep: + result = append(result, current.String()) + + openChar = 0 + + current = strings.Builder{} + default: + current.WriteRune(char) + } + } + + if current.String() != "" { + result = append(result, current.String()) + } + + return result +} + +// ComplementSchema complement schema with field properties +func (ps *tagBaseFieldParser) ComplementSchema(schema *spec.Schema) error { + types := ps.p.GetSchemaTypePath(schema, 2) + if len(types) == 0 { + return fmt.Errorf("invalid type for field: %s", ps.field.Names[0]) + } + + if IsRefSchema(schema) { + var newSchema = spec.Schema{} + err := ps.complementSchema(&newSchema, types) + if err != nil { + return err + } + if !reflect.ValueOf(newSchema).IsZero() { + *schema = *(newSchema.WithAllOf(*schema)) + } + return nil + } + + return ps.complementSchema(schema, types) +} + +// complementSchema complement schema with field properties +func (ps *tagBaseFieldParser) complementSchema(schema *spec.Schema, types []string) error { + if ps.field.Tag == nil { + if ps.field.Doc != nil { + schema.Description = strings.TrimSpace(ps.field.Doc.Text()) + } + + if schema.Description == "" && ps.field.Comment != nil { + schema.Description = strings.TrimSpace(ps.field.Comment.Text()) + } + + return nil + } + + field := &structField{ + schemaType: types[0], + formatType: ps.tag.Get(formatTag), + } + + if len(types) > 1 && (types[0] == ARRAY || types[0] == OBJECT) { + field.arrayType = types[1] + } + + jsonTagValue := ps.tag.Get(jsonTag) + + bindingTagValue := ps.tag.Get(bindingTag) + if bindingTagValue != "" { + parseValidTags(bindingTagValue, field) + } + + validateTagValue := ps.tag.Get(validateTag) + if validateTagValue != "" { + parseValidTags(validateTagValue, field) + } + + enumsTagValue := ps.tag.Get(enumsTag) + if enumsTagValue != "" { + err := parseEnumTags(enumsTagValue, field) + if err != nil { + return err + } + } + + if IsNumericType(field.schemaType) || IsNumericType(field.arrayType) { + maximum, err := getFloatTag(ps.tag, maximumTag) + if err != nil { + return err + } + + if maximum != nil { + field.maximum = maximum + } + + minimum, err := getFloatTag(ps.tag, minimumTag) + if err != nil { + return err + } + + if minimum != nil { + field.minimum = minimum + } + + multipleOf, err := getFloatTag(ps.tag, multipleOfTag) + if err != nil { + return err + } + + if multipleOf != nil { + field.multipleOf = multipleOf + } + } + + if field.schemaType == STRING || field.arrayType == STRING { + maxLength, err := getIntTag(ps.tag, maxLengthTag) + if err != nil { + return err + } + + if maxLength != nil { + field.maxLength = maxLength + } + + minLength, err := getIntTag(ps.tag, minLengthTag) + if err != nil { + return err + } + + if minLength != nil { + field.minLength = minLength + } + } + + // json:"name,string" or json:",string" + exampleTagValue, ok := ps.tag.Lookup(exampleTag) + if ok { + field.exampleValue = exampleTagValue + + if !strings.Contains(jsonTagValue, ",string") { + example, err := defineTypeOfExample(field.schemaType, field.arrayType, exampleTagValue) + if err != nil { + return err + } + + field.exampleValue = example + } + } + + // perform this after setting everything else (min, max, etc...) + if strings.Contains(jsonTagValue, ",string") { + // @encoding/json: "It applies only to fields of string, floating point, integer, or boolean types." + defaultValues := map[string]string{ + // Zero Values as string + STRING: "", + INTEGER: "0", + BOOLEAN: "false", + NUMBER: "0", + } + + defaultValue, ok := defaultValues[field.schemaType] + if ok { + field.schemaType = STRING + *schema = *PrimitiveSchema(field.schemaType) + + if field.exampleValue == nil { + // if exampleValue is not defined by the user, + // we will force an example with a correct value + // (eg: int->"0", bool:"false") + field.exampleValue = defaultValue + } + } + } + + if ps.field.Doc != nil { + schema.Description = strings.TrimSpace(ps.field.Doc.Text()) + } + + if schema.Description == "" && ps.field.Comment != nil { + schema.Description = strings.TrimSpace(ps.field.Comment.Text()) + } + + schema.ReadOnly = ps.tag.Get(readOnlyTag) == "true" + + defaultTagValue := ps.tag.Get(defaultTag) + if defaultTagValue != "" { + value, err := defineType(field.schemaType, defaultTagValue) + if err != nil { + return err + } + + schema.Default = value + } + + schema.Example = field.exampleValue + + if field.schemaType != ARRAY { + schema.Format = field.formatType + } + + extensionsTagValue := ps.tag.Get(extensionsTag) + if extensionsTagValue != "" { + schema.Extensions = setExtensionParam(extensionsTagValue) + } + + varNamesTag := ps.tag.Get("x-enum-varnames") + if varNamesTag != "" { + varNames := strings.Split(varNamesTag, ",") + if len(varNames) != len(field.enums) { + return fmt.Errorf("invalid count of x-enum-varnames. expected %d, got %d", len(field.enums), len(varNames)) + } + + field.enumVarNames = nil + + for _, v := range varNames { + field.enumVarNames = append(field.enumVarNames, v) + } + + if field.schemaType == ARRAY { + // Add the var names in the items schema + if schema.Items.Schema.Extensions == nil { + schema.Items.Schema.Extensions = map[string]interface{}{} + } + schema.Items.Schema.Extensions[enumVarNamesExtension] = field.enumVarNames + } else { + // Add to top level schema + if schema.Extensions == nil { + schema.Extensions = map[string]interface{}{} + } + schema.Extensions[enumVarNamesExtension] = field.enumVarNames + } + } + + eleSchema := schema + + if field.schemaType == ARRAY { + // For Array only + schema.MaxItems = field.maxItems + schema.MinItems = field.minItems + schema.UniqueItems = field.unique + + eleSchema = schema.Items.Schema + eleSchema.Format = field.formatType + } + + eleSchema.Maximum = field.maximum + eleSchema.Minimum = field.minimum + eleSchema.MultipleOf = field.multipleOf + eleSchema.MaxLength = field.maxLength + eleSchema.MinLength = field.minLength + eleSchema.Enum = field.enums + + return nil +} + +func getFloatTag(structTag reflect.StructTag, tagName string) (*float64, error) { + strValue := structTag.Get(tagName) + if strValue == "" { + return nil, nil + } + + value, err := strconv.ParseFloat(strValue, 64) + if err != nil { + return nil, fmt.Errorf("can't parse numeric value of %q tag: %v", tagName, err) + } + + return &value, nil +} + +func getIntTag(structTag reflect.StructTag, tagName string) (*int64, error) { + strValue := structTag.Get(tagName) + if strValue == "" { + return nil, nil + } + + value, err := strconv.ParseInt(strValue, 10, 64) + if err != nil { + return nil, fmt.Errorf("can't parse numeric value of %q tag: %v", tagName, err) + } + + return &value, nil +} + +func (ps *tagBaseFieldParser) IsRequired() (bool, error) { + if ps.field.Tag == nil { + return false, nil + } + + bindingTag := ps.tag.Get(bindingTag) + if bindingTag != "" { + for _, val := range strings.Split(bindingTag, ",") { + switch val { + case requiredLabel: + return true, nil + case optionalLabel: + return false, nil + } + } + } + + validateTag := ps.tag.Get(validateTag) + if validateTag != "" { + for _, val := range strings.Split(validateTag, ",") { + switch val { + case requiredLabel: + return true, nil + case optionalLabel: + return false, nil + } + } + } + + return ps.p.RequiredByDefault, nil +} + +func parseValidTags(validTag string, sf *structField) { + // `validate:"required,max=10,min=1"` + // ps. required checked by IsRequired(). + for _, val := range strings.Split(validTag, ",") { + var ( + valValue string + keyVal = strings.Split(val, "=") + ) + + switch len(keyVal) { + case 1: + case 2: + valValue = strings.ReplaceAll(strings.ReplaceAll(keyVal[1], utf8HexComma, ","), utf8Pipe, "|") + default: + continue + } + + switch keyVal[0] { + case "max", "lte": + sf.setMax(valValue) + case "min", "gte": + sf.setMin(valValue) + case "oneof": + sf.setOneOf(valValue) + case "unique": + if sf.schemaType == ARRAY { + sf.unique = true + } + case "dive": + // ignore dive + return + default: + continue + } + } +} + +func parseEnumTags(enumTag string, field *structField) error { + enumType := field.schemaType + if field.schemaType == ARRAY { + enumType = field.arrayType + } + + field.enums = nil + + for _, e := range strings.Split(enumTag, ",") { + value, err := defineType(enumType, e) + if err != nil { + return err + } + + field.enums = append(field.enums, value) + } + + return nil +} + +func (sf *structField) setOneOf(valValue string) { + if len(sf.enums) != 0 { + return + } + + enumType := sf.schemaType + if sf.schemaType == ARRAY { + enumType = sf.arrayType + } + + valValues := parseOneOfParam2(valValue) + for i := range valValues { + value, err := defineType(enumType, valValues[i]) + if err != nil { + continue + } + + sf.enums = append(sf.enums, value) + } +} + +func (sf *structField) setMin(valValue string) { + value, err := strconv.ParseFloat(valValue, 64) + if err != nil { + return + } + + switch sf.schemaType { + case INTEGER, NUMBER: + sf.minimum = &value + case STRING: + intValue := int64(value) + sf.minLength = &intValue + case ARRAY: + intValue := int64(value) + sf.minItems = &intValue + } +} + +func (sf *structField) setMax(valValue string) { + value, err := strconv.ParseFloat(valValue, 64) + if err != nil { + return + } + + switch sf.schemaType { + case INTEGER, NUMBER: + sf.maximum = &value + case STRING: + intValue := int64(value) + sf.maxLength = &intValue + case ARRAY: + intValue := int64(value) + sf.maxItems = &intValue + } +} + +const ( + utf8HexComma = "0x2C" + utf8Pipe = "0x7C" +) + +// These code copy from +// https://github.com/go-playground/validator/blob/d4271985b44b735c6f76abc7a06532ee997f9476/baked_in.go#L207 +// ---. +var oneofValsCache = map[string][]string{} +var oneofValsCacheRWLock = sync.RWMutex{} +var splitParamsRegex = regexp.MustCompile(`'[^']*'|\S+`) + +func parseOneOfParam2(param string) []string { + oneofValsCacheRWLock.RLock() + values, ok := oneofValsCache[param] + oneofValsCacheRWLock.RUnlock() + + if !ok { + oneofValsCacheRWLock.Lock() + values = splitParamsRegex.FindAllString(param, -1) + + for i := 0; i < len(values); i++ { + values[i] = strings.ReplaceAll(values[i], "'", "") + } + + oneofValsCache[param] = values + + oneofValsCacheRWLock.Unlock() + } + + return values +} + +// ---. diff --git a/vendor/github.com/swaggo/swag/formatter.go b/vendor/github.com/swaggo/swag/formatter.go new file mode 100644 index 0000000000..511e3a8224 --- /dev/null +++ b/vendor/github.com/swaggo/swag/formatter.go @@ -0,0 +1,182 @@ +package swag + +import ( + "bytes" + "fmt" + "go/ast" + goparser "go/parser" + "go/token" + "log" + "os" + "regexp" + "sort" + "strings" + "text/tabwriter" +) + +// Check of @Param @Success @Failure @Response @Header +var specialTagForSplit = map[string]bool{ + paramAttr: true, + successAttr: true, + failureAttr: true, + responseAttr: true, + headerAttr: true, +} + +var skipChar = map[byte]byte{ + '"': '"', + '(': ')', + '{': '}', + '[': ']', +} + +// Formatter implements a formatter for Go source files. +type Formatter struct { + // debugging output goes here + debug Debugger +} + +// NewFormatter create a new formatter instance. +func NewFormatter() *Formatter { + formatter := &Formatter{ + debug: log.New(os.Stdout, "", log.LstdFlags), + } + return formatter +} + +// Format formats swag comments in contents. It uses fileName to report errors +// that happen during parsing of contents. +func (f *Formatter) Format(fileName string, contents []byte) ([]byte, error) { + fileSet := token.NewFileSet() + ast, err := goparser.ParseFile(fileSet, fileName, contents, goparser.ParseComments) + if err != nil { + return nil, err + } + + // Formatting changes are described as an edit list of byte range + // replacements. We make these content-level edits directly rather than + // changing the AST nodes and writing those out (via [go/printer] or + // [go/format]) so that we only change the formatting of Swag attribute + // comments. This won't touch the formatting of any other comments, or of + // functions, etc. + maxEdits := 0 + for _, comment := range ast.Comments { + maxEdits += len(comment.List) + } + edits := make(edits, 0, maxEdits) + + for _, comment := range ast.Comments { + formatFuncDoc(fileSet, comment.List, &edits) + } + + return edits.apply(contents), nil +} + +type edit struct { + begin int + end int + replacement []byte +} + +type edits []edit + +func (edits edits) apply(contents []byte) []byte { + // Apply the edits with the highest offset first, so that earlier edits + // don't affect the offsets of later edits. + sort.Slice(edits, func(i, j int) bool { + return edits[i].begin > edits[j].begin + }) + + for _, edit := range edits { + prefix := contents[:edit.begin] + suffix := contents[edit.end:] + contents = append(prefix, append(edit.replacement, suffix...)...) + } + + return contents +} + +// formatFuncDoc reformats the comment lines in commentList, and appends any +// changes to the edit list. +func formatFuncDoc(fileSet *token.FileSet, commentList []*ast.Comment, edits *edits) { + // Building the edit list to format a comment block is a two-step process. + // First, we iterate over each comment line looking for Swag attributes. In + // each one we find, we replace alignment whitespace with a tab character, + // then write the result into a tab writer. + + linesToComments := make(map[int]int, len(commentList)) + + buffer := &bytes.Buffer{} + w := tabwriter.NewWriter(buffer, 1, 4, 1, '\t', 0) + + for commentIndex, comment := range commentList { + text := comment.Text + if attr, body, found := swagComment(text); found { + formatted := "//\t" + attr + if body != "" { + formatted += "\t" + splitComment2(attr, body) + } + _, _ = fmt.Fprintln(w, formatted) + linesToComments[len(linesToComments)] = commentIndex + } + } + + // Once we've loaded all of the comment lines to be aligned into the tab + // writer, flushing it causes the aligned text to be written out to the + // backing buffer. + _ = w.Flush() + + // Now the second step: we iterate over the aligned comment lines that were + // written into the backing buffer, pair each one up to its original + // comment line, and use the combination to describe the edit that needs to + // be made to the original input. + formattedComments := bytes.Split(buffer.Bytes(), []byte("\n")) + for lineIndex, commentIndex := range linesToComments { + comment := commentList[commentIndex] + *edits = append(*edits, edit{ + begin: fileSet.Position(comment.Pos()).Offset, + end: fileSet.Position(comment.End()).Offset, + replacement: formattedComments[lineIndex], + }) + } +} + +func splitComment2(attr, body string) string { + if specialTagForSplit[strings.ToLower(attr)] { + for i := 0; i < len(body); i++ { + if skipEnd, ok := skipChar[body[i]]; ok { + skipStart, n := body[i], 1 + for i++; i < len(body); i++ { + if skipStart != skipEnd && body[i] == skipStart { + n++ + } else if body[i] == skipEnd { + n-- + if n == 0 { + break + } + } + } + } else if body[i] == ' ' || body[i] == '\t' { + j := i + for ; j < len(body) && (body[j] == ' ' || body[j] == '\t'); j++ { + } + body = replaceRange(body, i, j, "\t") + } + } + } + return body +} + +func replaceRange(s string, start, end int, new string) string { + return s[:start] + new + s[end:] +} + +var swagCommentLineExpression = regexp.MustCompile(`^\/\/\s+(@[\S.]+)\s*(.*)`) + +func swagComment(comment string) (string, string, bool) { + matches := swagCommentLineExpression.FindStringSubmatch(comment) + if matches == nil { + return "", "", false + } + return matches[1], matches[2], true +} diff --git a/vendor/github.com/swaggo/swag/gen/gen.go b/vendor/github.com/swaggo/swag/gen/gen.go index b80fd21f45..43cf73ed8b 100644 --- a/vendor/github.com/swaggo/swag/gen/gen.go +++ b/vendor/github.com/swaggo/swag/gen/gen.go @@ -1,6 +1,7 @@ package gen import ( + "bufio" "bytes" "encoding/json" "fmt" @@ -9,96 +10,259 @@ import ( "log" "os" "path" + "path/filepath" "strings" "text/template" "time" - "github.com/ghodss/yaml" "github.com/go-openapi/spec" "github.com/swaggo/swag" + "golang.org/x/text/cases" + "golang.org/x/text/language" + "sigs.k8s.io/yaml" ) +var open = os.Open + +// DefaultOverridesFile is the location swagger will look for type overrides. +const DefaultOverridesFile = ".swaggo" + +type genTypeWriter func(*Config, *spec.Swagger) error + // Gen presents a generate tool for swag. type Gen struct { - jsonIndent func(data interface{}) ([]byte, error) - jsonToYAML func(data []byte) ([]byte, error) + json func(data interface{}) ([]byte, error) + jsonIndent func(data interface{}) ([]byte, error) + jsonToYAML func(data []byte) ([]byte, error) + outputTypeMap map[string]genTypeWriter + debug Debugger +} + +// Debugger is the interface that wraps the basic Printf method. +type Debugger interface { + Printf(format string, v ...interface{}) } // New creates a new Gen. func New() *Gen { - return &Gen{ + gen := Gen{ + json: json.Marshal, jsonIndent: func(data interface{}) ([]byte, error) { return json.MarshalIndent(data, "", " ") }, jsonToYAML: yaml.JSONToYAML, + debug: log.New(os.Stdout, "", log.LstdFlags), + } + + gen.outputTypeMap = map[string]genTypeWriter{ + "go": gen.writeDocSwagger, + "json": gen.writeJSONSwagger, + "yaml": gen.writeYAMLSwagger, + "yml": gen.writeYAMLSwagger, } + + return &gen } // Config presents Gen configurations. type Config struct { - // SearchDir the swag would be parse + Debugger swag.Debugger + + // SearchDir the swag would parse,comma separated if multiple SearchDir string // excludes dirs and files in SearchDir,comma separated Excludes string + // outputs only specific extension + ParseExtension string + // OutputDir represents the output directory for all the generated files OutputDir string + // OutputTypes define types of files which should be generated + OutputTypes []string + // MainAPIFile the Go file path in which 'swagger general API Info' is written MainAPIFile string - // PropNamingStrategy represents property naming strategy like snakecase,camelcase,pascalcase + // PropNamingStrategy represents property naming strategy like snake case,camel case,pascal case PropNamingStrategy string + // MarkdownFilesDir used to find markdown files, which can be used for tag descriptions + MarkdownFilesDir string + + // CodeExampleFilesDir used to find code example files, which can be used for x-codeSamples + CodeExampleFilesDir string + + // InstanceName is used to get distinct names for different swagger documents in the + // same project. The default value is "swagger". + InstanceName string + + // ParseDepth dependency parse depth + ParseDepth int + // ParseVendor whether swag should be parse vendor folder ParseVendor bool - // ParseDependencies whether swag should be parse outside dependency folder - ParseDependency bool + // ParseDependencies whether swag should be parse outside dependency folder: 0 none, 1 models, 2 operations, 3 all + ParseDependency int // ParseInternal whether swag should parse internal packages ParseInternal bool - // MarkdownFilesDir used to find markdownfiles, which can be used for tag descriptions - MarkdownFilesDir string + // Strict whether swag should error or warn when it detects cases which are most likely user errors + Strict bool // GeneratedTime whether swag should generate the timestamp at the top of docs.go GeneratedTime bool + + // RequiredByDefault set validation required for all fields by default + RequiredByDefault bool + + // OverridesFile defines global type overrides. + OverridesFile string + + // ParseGoList whether swag use go list to parse dependency + ParseGoList bool + + // include only tags mentioned when searching, comma separated + Tags string + + // LeftTemplateDelim defines the left delimiter for the template generation + LeftTemplateDelim string + + // RightTemplateDelim defines the right delimiter for the template generation + RightTemplateDelim string + + // PackageName defines package name of generated `docs.go` + PackageName string + + // CollectionFormat set default collection format + CollectionFormat string + + // Parse only packages whose import path match the given prefix, comma separated + PackagePrefix string + + // State set host state + State string } -// Build builds swagger json file for given searchDir and mainAPIFile. Returns json +// Build builds swagger json file for given searchDir and mainAPIFile. Returns json. func (g *Gen) Build(config *Config) error { - if _, err := os.Stat(config.SearchDir); os.IsNotExist(err) { - return fmt.Errorf("dir: %s is not exist", config.SearchDir) + if config.Debugger != nil { + g.debug = config.Debugger + } + if config.InstanceName == "" { + config.InstanceName = swag.Name + } + + searchDirs := strings.Split(config.SearchDir, ",") + for _, searchDir := range searchDirs { + if _, err := os.Stat(searchDir); os.IsNotExist(err) { + return fmt.Errorf("dir: %s does not exist", searchDir) + } + } + + if config.LeftTemplateDelim == "" { + config.LeftTemplateDelim = "{{" } - log.Println("Generate swagger docs....") - p := swag.New(swag.SetMarkdownFileDirectory(config.MarkdownFilesDir), - swag.SetExcludedDirsAndFiles(config.Excludes)) + if config.RightTemplateDelim == "" { + config.RightTemplateDelim = "}}" + } + + var overrides map[string]string + + if config.OverridesFile != "" { + overridesFile, err := open(config.OverridesFile) + if err != nil { + // Don't bother reporting if the default file is missing; assume there are no overrides + if !(config.OverridesFile == DefaultOverridesFile && os.IsNotExist(err)) { + return fmt.Errorf("could not open overrides file: %w", err) + } + } else { + g.debug.Printf("Using overrides from %s", config.OverridesFile) + + overrides, err = parseOverrides(overridesFile) + if err != nil { + return err + } + } + } + + g.debug.Printf("Generate swagger docs....") + + p := swag.New( + swag.SetParseDependency(config.ParseDependency), + swag.SetMarkdownFileDirectory(config.MarkdownFilesDir), + swag.SetDebugger(config.Debugger), + swag.SetExcludedDirsAndFiles(config.Excludes), + swag.SetParseExtension(config.ParseExtension), + swag.SetCodeExamplesDirectory(config.CodeExampleFilesDir), + swag.SetStrict(config.Strict), + swag.SetOverrides(overrides), + swag.ParseUsingGoList(config.ParseGoList), + swag.SetTags(config.Tags), + swag.SetCollectionFormat(config.CollectionFormat), + swag.SetPackagePrefix(config.PackagePrefix), + ) + p.PropNamingStrategy = config.PropNamingStrategy p.ParseVendor = config.ParseVendor - p.ParseDependency = config.ParseDependency p.ParseInternal = config.ParseInternal + p.RequiredByDefault = config.RequiredByDefault + p.HostState = config.State - if err := p.ParseAPI(config.SearchDir, config.MainAPIFile); err != nil { + if err := p.ParseAPIMultiSearchDir(searchDirs, config.MainAPIFile, config.ParseDepth); err != nil { return err } + swagger := p.GetSwagger() - b, err := g.jsonIndent(swagger) - if err != nil { + if err := os.MkdirAll(config.OutputDir, os.ModePerm); err != nil { return err } - if err := os.MkdirAll(config.OutputDir, os.ModePerm); err != nil { + for _, outputType := range config.OutputTypes { + outputType = strings.ToLower(strings.TrimSpace(outputType)) + if typeWriter, ok := g.outputTypeMap[outputType]; ok { + if err := typeWriter(config, swagger); err != nil { + return err + } + } else { + log.Printf("output type '%s' not supported", outputType) + } + } + + return nil +} + +func (g *Gen) writeDocSwagger(config *Config, swagger *spec.Swagger) error { + var filename = "docs.go" + + if config.State != "" { + filename = config.State + "_" + filename + } + + if config.InstanceName != swag.Name { + filename = config.InstanceName + "_" + filename + } + + docFileName := path.Join(config.OutputDir, filename) + + absOutputDir, err := filepath.Abs(config.OutputDir) + if err != nil { return err } - packageName := path.Base(config.OutputDir) - docFileName := path.Join(config.OutputDir, "docs.go") - jsonFileName := path.Join(config.OutputDir, "swagger.json") - yamlFileName := path.Join(config.OutputDir, "swagger.yaml") + var packageName string + if len(config.PackageName) > 0 { + packageName = config.PackageName + } else { + packageName = filepath.Base(absOutputDir) + packageName = strings.ReplaceAll(packageName, "-", "_") + } docs, err := os.Create(docFileName) if err != nil { @@ -106,30 +270,74 @@ func (g *Gen) Build(config *Config) error { } defer docs.Close() - err = g.writeFile(b, jsonFileName) + // Write doc + err = g.writeGoDoc(packageName, docs, swagger, config) if err != nil { return err } - y, err := g.jsonToYAML(b) + g.debug.Printf("create docs.go at %+v", docFileName) + + return nil +} + +func (g *Gen) writeJSONSwagger(config *Config, swagger *spec.Swagger) error { + var filename = "swagger.json" + + if config.State != "" { + filename = config.State + "_" + filename + } + + if config.InstanceName != swag.Name { + filename = config.InstanceName + "_" + filename + } + + jsonFileName := path.Join(config.OutputDir, filename) + + b, err := g.jsonIndent(swagger) if err != nil { - return fmt.Errorf("cannot convert json to yaml error: %s", err) + return err } - err = g.writeFile(y, yamlFileName) + err = g.writeFile(b, jsonFileName) if err != nil { return err } - // Write doc - err = g.writeGoDoc(packageName, docs, swagger, config) + g.debug.Printf("create swagger.json at %+v", jsonFileName) + + return nil +} + +func (g *Gen) writeYAMLSwagger(config *Config, swagger *spec.Swagger) error { + var filename = "swagger.yaml" + + if config.State != "" { + filename = config.State + "_" + filename + } + + if config.InstanceName != swag.Name { + filename = config.InstanceName + "_" + filename + } + + yamlFileName := path.Join(config.OutputDir, filename) + + b, err := g.json(swagger) + if err != nil { + return err + } + + y, err := g.jsonToYAML(b) + if err != nil { + return fmt.Errorf("cannot covert json to yaml error: %s", err) + } + + err = g.writeFile(y, yamlFileName) if err != nil { return err } - log.Printf("create docs.go at %+v", docFileName) - log.Printf("create swagger.json at %+v", jsonFileName) - log.Printf("create swagger.yaml at %+v", yamlFileName) + g.debug.Printf("create swagger.yaml at %+v", yamlFileName) return nil } @@ -139,25 +347,73 @@ func (g *Gen) writeFile(b []byte, file string) error { if err != nil { return err } + defer f.Close() _, err = f.Write(b) + return err } func (g *Gen) formatSource(src []byte) []byte { code, err := format.Source(src) if err != nil { - code = src // Output the unformatted code anyway + code = src // Formatter failed, return original code. } + return code } +// Read and parse the overrides file. +func parseOverrides(r io.Reader) (map[string]string, error) { + overrides := make(map[string]string) + scanner := bufio.NewScanner(r) + + for scanner.Scan() { + line := scanner.Text() + + // Skip comments + if len(line) > 1 && line[0:2] == "//" { + continue + } + + parts := strings.Fields(line) + + switch len(parts) { + case 0: + // only whitespace + continue + case 2: + // either a skip or malformed + if parts[0] != "skip" { + return nil, fmt.Errorf("could not parse override: '%s'", line) + } + + overrides[parts[1]] = "" + case 3: + // either a replace or malformed + if parts[0] != "replace" { + return nil, fmt.Errorf("could not parse override: '%s'", line) + } + + overrides[parts[1]] = parts[2] + default: + return nil, fmt.Errorf("could not parse override: '%s'", line) + } + } + + if err := scanner.Err(); err != nil { + return nil, fmt.Errorf("error reading overrides file: %w", err) + } + + return overrides, nil +} + func (g *Gen) writeGoDoc(packageName string, output io.Writer, swagger *spec.Swagger, config *Config) error { generator, err := template.New("swagger_info").Funcs(template.FuncMap{ "printDoc": func(v string) string { // Add schemes - v = "{\n \"schemes\": {{ marshal .Schemes }}," + v[1:] + v = "{\n \"schemes\": " + config.LeftTemplateDelim + " marshal .Schemes " + config.RightTemplateDelim + "," + v[1:] // Sanitize backticks return strings.Replace(v, "`", "`+\"`\"+`", -1) }, @@ -176,16 +432,16 @@ func (g *Gen) writeGoDoc(packageName string, output io.Writer, swagger *spec.Swa Info: &spec.Info{ VendorExtensible: swagger.Info.VendorExtensible, InfoProps: spec.InfoProps{ - Description: "{{.Description}}", - Title: "{{.Title}}", + Description: config.LeftTemplateDelim + "escape .Description" + config.RightTemplateDelim, + Title: config.LeftTemplateDelim + ".Title" + config.RightTemplateDelim, TermsOfService: swagger.Info.TermsOfService, Contact: swagger.Info.Contact, License: swagger.Info.License, - Version: "{{.Version}}", + Version: config.LeftTemplateDelim + ".Version" + config.RightTemplateDelim, }, }, - Host: "{{.Host}}", - BasePath: "{{.BasePath}}", + Host: config.LeftTemplateDelim + ".Host" + config.RightTemplateDelim, + BasePath: config.LeftTemplateDelim + ".BasePath" + config.RightTemplateDelim, Paths: swagger.Paths, Definitions: swagger.Definitions, Parameters: swagger.Parameters, @@ -203,29 +459,43 @@ func (g *Gen) writeGoDoc(packageName string, output io.Writer, swagger *spec.Swa return err } + state := "" + if len(config.State) > 0 { + state = cases.Title(language.English).String(strings.ToLower(config.State)) + } + buffer := &bytes.Buffer{} + err = generator.Execute(buffer, struct { - Timestamp time.Time - GeneratedTime bool - Doc string - Host string - PackageName string - BasePath string - Schemes []string - Title string - Description string - Version string + Timestamp time.Time + Doc string + Host string + PackageName string + BasePath string + Title string + Description string + Version string + State string + InstanceName string + Schemes []string + GeneratedTime bool + LeftTemplateDelim string + RightTemplateDelim string }{ - Timestamp: time.Now(), - GeneratedTime: config.GeneratedTime, - Doc: string(buf), - Host: swagger.Host, - PackageName: packageName, - BasePath: swagger.BasePath, - Schemes: swagger.Schemes, - Title: swagger.Info.Title, - Description: swagger.Info.Description, - Version: swagger.Info.Version, + Timestamp: time.Now(), + GeneratedTime: config.GeneratedTime, + Doc: string(buf), + Host: swagger.Host, + PackageName: packageName, + BasePath: swagger.BasePath, + Schemes: swagger.Schemes, + Title: swagger.Info.Title, + Description: swagger.Info.Description, + Version: swagger.Info.Version, + State: state, + InstanceName: config.InstanceName, + LeftTemplateDelim: config.LeftTemplateDelim, + RightTemplateDelim: config.RightTemplateDelim, }) if err != nil { return err @@ -235,70 +505,32 @@ func (g *Gen) writeGoDoc(packageName string, output io.Writer, swagger *spec.Swa // write _, err = output.Write(code) + return err } -var packageTemplate = `// GENERATED BY THE COMMAND ABOVE; DO NOT EDIT -// This file was generated by swaggo/swag{{ if .GeneratedTime }} at -// {{ .Timestamp }}{{ end }} - +var packageTemplate = `// Package {{.PackageName}} Code generated by swaggo/swag{{ if .GeneratedTime }} at {{ .Timestamp }}{{ end }}. DO NOT EDIT package {{.PackageName}} -import ( - "bytes" - "encoding/json" - "strings" +import "github.com/swaggo/swag" - "github.com/alecthomas/template" - "github.com/swaggo/swag" -) - -var doc = ` + "`{{ printDoc .Doc}}`" + ` - -type swaggerInfo struct { - Version string - Host string - BasePath string - Schemes []string - Title string - Description string -} +const docTemplate{{ if ne .InstanceName "swagger" }}{{ .InstanceName }} {{- end }}{{ .State }} = ` + "`{{ printDoc .Doc}}`" + ` -// SwaggerInfo holds exported Swagger Info so clients can modify it -var SwaggerInfo = swaggerInfo{ +// Swagger{{ .State }}Info{{ if ne .InstanceName "swagger" }}{{ .InstanceName }} {{- end }} holds exported Swagger Info so clients can modify it +var Swagger{{ .State }}Info{{ if ne .InstanceName "swagger" }}{{ .InstanceName }} {{- end }} = &swag.Spec{ Version: {{ printf "%q" .Version}}, - Host: {{ printf "%q" .Host}}, + Host: {{ printf "%q" .Host}}, BasePath: {{ printf "%q" .BasePath}}, Schemes: []string{ {{ range $index, $schema := .Schemes}}{{if gt $index 0}},{{end}}{{printf "%q" $schema}}{{end}} }, Title: {{ printf "%q" .Title}}, Description: {{ printf "%q" .Description}}, -} - -type s struct{} - -func (s *s) ReadDoc() string { - sInfo := SwaggerInfo - sInfo.Description = strings.Replace(sInfo.Description, "\n", "\\n", -1) - - t, err := template.New("swagger_info").Funcs(template.FuncMap{ - "marshal": func(v interface{}) string { - a, _ := json.Marshal(v) - return string(a) - }, - }).Parse(doc) - if err != nil { - return doc - } - - var tpl bytes.Buffer - if err := t.Execute(&tpl, sInfo); err != nil { - return doc - } - - return tpl.String() + InfoInstanceName: {{ printf "%q" .InstanceName }}, + SwaggerTemplate: docTemplate{{ if ne .InstanceName "swagger" }}{{ .InstanceName }} {{- end }}{{ .State }}, + LeftDelim: {{ printf "%q" .LeftTemplateDelim}}, + RightDelim: {{ printf "%q" .RightTemplateDelim}}, } func init() { - swag.Register(swag.Name, &s{}) + swag.Register(Swagger{{ .State }}Info{{ if ne .InstanceName "swagger" }}{{ .InstanceName }} {{- end }}.InstanceName(), Swagger{{ .State }}Info{{ if ne .InstanceName "swagger" }}{{ .InstanceName }} {{- end }}) } ` diff --git a/vendor/github.com/swaggo/swag/generics.go b/vendor/github.com/swaggo/swag/generics.go new file mode 100644 index 0000000000..07344bbafb --- /dev/null +++ b/vendor/github.com/swaggo/swag/generics.go @@ -0,0 +1,432 @@ +//go:build go1.18 +// +build go1.18 + +package swag + +import ( + "errors" + "fmt" + "go/ast" + "strings" + "unicode" + + "github.com/go-openapi/spec" +) + +type genericTypeSpec struct { + TypeSpec *TypeSpecDef + Name string +} + +type formalParamType struct { + Name string + Type string +} + +func (t *genericTypeSpec) TypeName() string { + if t.TypeSpec != nil { + return t.TypeSpec.TypeName() + } + return t.Name +} + +func normalizeGenericTypeName(name string) string { + return strings.Replace(name, ".", "_", -1) +} + +func (pkgDefs *PackagesDefinitions) getTypeFromGenericParam(genericParam string, file *ast.File) (typeSpecDef *TypeSpecDef) { + if strings.HasPrefix(genericParam, "[]") { + typeSpecDef = pkgDefs.getTypeFromGenericParam(genericParam[2:], file) + if typeSpecDef == nil { + return nil + } + var expr ast.Expr + switch typeSpecDef.TypeSpec.Type.(type) { + case *ast.ArrayType, *ast.MapType: + expr = typeSpecDef.TypeSpec.Type + default: + name := typeSpecDef.TypeName() + expr = ast.NewIdent(name) + if _, ok := pkgDefs.uniqueDefinitions[name]; !ok { + pkgDefs.uniqueDefinitions[name] = typeSpecDef + } + } + return &TypeSpecDef{ + TypeSpec: &ast.TypeSpec{ + Name: ast.NewIdent(string(IgnoreNameOverridePrefix) + "array_" + typeSpecDef.TypeName()), + Type: &ast.ArrayType{ + Elt: expr, + }, + }, + Enums: typeSpecDef.Enums, + PkgPath: typeSpecDef.PkgPath, + ParentSpec: typeSpecDef.ParentSpec, + NotUnique: false, + } + } + + if strings.HasPrefix(genericParam, "map[") { + parts := strings.SplitN(genericParam[4:], "]", 2) + if len(parts) != 2 { + return nil + } + typeSpecDef = pkgDefs.getTypeFromGenericParam(parts[1], file) + if typeSpecDef == nil { + return nil + } + var expr ast.Expr + switch typeSpecDef.TypeSpec.Type.(type) { + case *ast.ArrayType, *ast.MapType: + expr = typeSpecDef.TypeSpec.Type + default: + name := typeSpecDef.TypeName() + expr = ast.NewIdent(name) + if _, ok := pkgDefs.uniqueDefinitions[name]; !ok { + pkgDefs.uniqueDefinitions[name] = typeSpecDef + } + } + return &TypeSpecDef{ + TypeSpec: &ast.TypeSpec{ + Name: ast.NewIdent(string(IgnoreNameOverridePrefix) + "map_" + parts[0] + "_" + typeSpecDef.TypeName()), + Type: &ast.MapType{ + Key: ast.NewIdent(parts[0]), //assume key is string or integer + Value: expr, + }, + }, + Enums: typeSpecDef.Enums, + PkgPath: typeSpecDef.PkgPath, + ParentSpec: typeSpecDef.ParentSpec, + NotUnique: false, + } + + } + if IsGolangPrimitiveType(genericParam) { + return &TypeSpecDef{ + TypeSpec: &ast.TypeSpec{ + Name: ast.NewIdent(genericParam), + Type: ast.NewIdent(genericParam), + }, + } + } + return pkgDefs.FindTypeSpec(genericParam, file) +} + +func (pkgDefs *PackagesDefinitions) parametrizeGenericType(file *ast.File, original *TypeSpecDef, fullGenericForm string) *TypeSpecDef { + if original == nil || original.TypeSpec.TypeParams == nil || len(original.TypeSpec.TypeParams.List) == 0 { + return original + } + + name, genericParams := splitGenericsTypeName(fullGenericForm) + if genericParams == nil { + return nil + } + + //generic[x,y any,z any] considered, TODO what if the type is not `any`, but a concrete one, such as `int32|int64` or an certain interface{} + var formals []formalParamType + for _, field := range original.TypeSpec.TypeParams.List { + for _, ident := range field.Names { + formal := formalParamType{Name: ident.Name} + if ident, ok := field.Type.(*ast.Ident); ok { + formal.Type = ident.Name + } + formals = append(formals, formal) + } + } + if len(genericParams) != len(formals) { + return nil + } + genericParamTypeDefs := map[string]*genericTypeSpec{} + + for i, genericParam := range genericParams { + var typeDef *TypeSpecDef + if !IsGolangPrimitiveType(genericParam) { + typeDef = pkgDefs.getTypeFromGenericParam(genericParam, file) + if typeDef != nil { + genericParam = typeDef.TypeName() + if _, ok := pkgDefs.uniqueDefinitions[genericParam]; !ok { + pkgDefs.uniqueDefinitions[genericParam] = typeDef + } + } + } + genericParamTypeDefs[formals[i].Name] = &genericTypeSpec{ + TypeSpec: typeDef, + Name: genericParam, + } + } + + name = fmt.Sprintf("%s%s-", string(IgnoreNameOverridePrefix), original.TypeName()) + var nameParts []string + for _, def := range formals { + if specDef, ok := genericParamTypeDefs[def.Name]; ok { + nameParts = append(nameParts, specDef.TypeName()) + } + } + + name += normalizeGenericTypeName(strings.Join(nameParts, "-")) + + if typeSpec, ok := pkgDefs.uniqueDefinitions[name]; ok { + return typeSpec + } + + parametrizedTypeSpec := &TypeSpecDef{ + File: original.File, + PkgPath: original.PkgPath, + TypeSpec: &ast.TypeSpec{ + Name: &ast.Ident{ + Name: name, + NamePos: original.TypeSpec.Name.NamePos, + Obj: original.TypeSpec.Name.Obj, + }, + Doc: original.TypeSpec.Doc, + Assign: original.TypeSpec.Assign, + }, + } + pkgDefs.uniqueDefinitions[name] = parametrizedTypeSpec + + parametrizedTypeSpec.TypeSpec.Type = pkgDefs.resolveGenericType(original.File, original.TypeSpec.Type, genericParamTypeDefs) + + return parametrizedTypeSpec +} + +// splitGenericsTypeName splits a generic struct name in his parts +func splitGenericsTypeName(fullGenericForm string) (string, []string) { + //remove all spaces character + fullGenericForm = strings.Map(func(r rune) rune { + if unicode.IsSpace(r) { + return -1 + } + return r + }, fullGenericForm) + + // split only at the first '[' and remove the last ']' + if fullGenericForm[len(fullGenericForm)-1] != ']' { + return "", nil + } + + genericParams := strings.SplitN(fullGenericForm[:len(fullGenericForm)-1], "[", 2) + if len(genericParams) == 1 { + return "", nil + } + + // generic type name + genericTypeName := genericParams[0] + + depth := 0 + genericParams = strings.FieldsFunc(genericParams[1], func(r rune) bool { + if r == '[' { + depth++ + } else if r == ']' { + depth-- + } else if r == ',' && depth == 0 { + return true + } + return false + }) + if depth != 0 { + return "", nil + } + + return genericTypeName, genericParams +} + +func (pkgDefs *PackagesDefinitions) getParametrizedType(genTypeSpec *genericTypeSpec) ast.Expr { + if genTypeSpec.TypeSpec != nil && strings.Contains(genTypeSpec.Name, ".") { + parts := strings.SplitN(genTypeSpec.Name, ".", 2) + return &ast.SelectorExpr{ + X: &ast.Ident{Name: parts[0]}, + Sel: &ast.Ident{Name: parts[1]}, + } + } + + //a primitive type name or a type name in current package + return &ast.Ident{Name: genTypeSpec.Name} +} + +func (pkgDefs *PackagesDefinitions) resolveGenericType(file *ast.File, expr ast.Expr, genericParamTypeDefs map[string]*genericTypeSpec) ast.Expr { + switch astExpr := expr.(type) { + case *ast.Ident: + if genTypeSpec, ok := genericParamTypeDefs[astExpr.Name]; ok { + return pkgDefs.getParametrizedType(genTypeSpec) + } + case *ast.ArrayType: + return &ast.ArrayType{ + Elt: pkgDefs.resolveGenericType(file, astExpr.Elt, genericParamTypeDefs), + Len: astExpr.Len, + Lbrack: astExpr.Lbrack, + } + case *ast.MapType: + return &ast.MapType{ + Map: astExpr.Map, + Key: pkgDefs.resolveGenericType(file, astExpr.Key, genericParamTypeDefs), + Value: pkgDefs.resolveGenericType(file, astExpr.Value, genericParamTypeDefs), + } + case *ast.StarExpr: + return &ast.StarExpr{ + Star: astExpr.Star, + X: pkgDefs.resolveGenericType(file, astExpr.X, genericParamTypeDefs), + } + case *ast.IndexExpr, *ast.IndexListExpr: + fullGenericName, _ := getGenericFieldType(file, expr, genericParamTypeDefs) + typeDef := pkgDefs.FindTypeSpec(fullGenericName, file) + if typeDef != nil { + return typeDef.TypeSpec.Name + } + case *ast.StructType: + newStructTypeDef := &ast.StructType{ + Struct: astExpr.Struct, + Incomplete: astExpr.Incomplete, + Fields: &ast.FieldList{ + Opening: astExpr.Fields.Opening, + Closing: astExpr.Fields.Closing, + }, + } + + for _, field := range astExpr.Fields.List { + newField := &ast.Field{ + Type: field.Type, + Doc: field.Doc, + Names: field.Names, + Tag: field.Tag, + Comment: field.Comment, + } + + newField.Type = pkgDefs.resolveGenericType(file, field.Type, genericParamTypeDefs) + + newStructTypeDef.Fields.List = append(newStructTypeDef.Fields.List, newField) + } + return newStructTypeDef + } + return expr +} + +func getExtendedGenericFieldType(file *ast.File, field ast.Expr, genericParamTypeDefs map[string]*genericTypeSpec) (string, error) { + switch fieldType := field.(type) { + case *ast.ArrayType: + fieldName, err := getExtendedGenericFieldType(file, fieldType.Elt, genericParamTypeDefs) + return "[]" + fieldName, err + case *ast.StarExpr: + return getExtendedGenericFieldType(file, fieldType.X, genericParamTypeDefs) + case *ast.Ident: + if genericParamTypeDefs != nil { + if typeSpec, ok := genericParamTypeDefs[fieldType.Name]; ok { + return typeSpec.Name, nil + } + } + if fieldType.Obj == nil { + return fieldType.Name, nil + } + + tSpec := &TypeSpecDef{ + File: file, + TypeSpec: fieldType.Obj.Decl.(*ast.TypeSpec), + PkgPath: file.Name.Name, + } + return tSpec.TypeName(), nil + default: + return getFieldType(file, field, genericParamTypeDefs) + } +} + +func getGenericFieldType(file *ast.File, field ast.Expr, genericParamTypeDefs map[string]*genericTypeSpec) (string, error) { + var fullName string + var baseName string + var err error + switch fieldType := field.(type) { + case *ast.IndexListExpr: + baseName, err = getGenericTypeName(file, fieldType.X) + if err != nil { + return "", err + } + fullName = baseName + "[" + + for _, index := range fieldType.Indices { + fieldName, err := getExtendedGenericFieldType(file, index, genericParamTypeDefs) + if err != nil { + return "", err + } + + fullName += fieldName + "," + } + + fullName = strings.TrimRight(fullName, ",") + "]" + case *ast.IndexExpr: + baseName, err = getGenericTypeName(file, fieldType.X) + if err != nil { + return "", err + } + + indexName, err := getExtendedGenericFieldType(file, fieldType.Index, genericParamTypeDefs) + if err != nil { + return "", err + } + + fullName = fmt.Sprintf("%s[%s]", baseName, indexName) + } + + if fullName == "" { + return "", fmt.Errorf("unknown field type %#v", field) + } + + var packageName string + if !strings.Contains(baseName, ".") { + if file.Name == nil { + return "", errors.New("file name is nil") + } + packageName, _ = getFieldType(file, file.Name, genericParamTypeDefs) + } + + return strings.TrimLeft(fmt.Sprintf("%s.%s", packageName, fullName), "."), nil +} + +func getGenericTypeName(file *ast.File, field ast.Expr) (string, error) { + switch fieldType := field.(type) { + case *ast.Ident: + if fieldType.Obj == nil { + return fieldType.Name, nil + } + + tSpec := &TypeSpecDef{ + File: file, + TypeSpec: fieldType.Obj.Decl.(*ast.TypeSpec), + PkgPath: file.Name.Name, + } + return tSpec.TypeName(), nil + case *ast.ArrayType: + tSpec := &TypeSpecDef{ + File: file, + TypeSpec: fieldType.Elt.(*ast.Ident).Obj.Decl.(*ast.TypeSpec), + PkgPath: file.Name.Name, + } + return tSpec.TypeName(), nil + case *ast.SelectorExpr: + return fmt.Sprintf("%s.%s", fieldType.X.(*ast.Ident).Name, fieldType.Sel.Name), nil + } + return "", fmt.Errorf("unknown type %#v", field) +} + +func (parser *Parser) parseGenericTypeExpr(file *ast.File, typeExpr ast.Expr) (*spec.Schema, error) { + switch expr := typeExpr.(type) { + // suppress debug messages for these types + case *ast.InterfaceType: + case *ast.StructType: + case *ast.Ident: + case *ast.StarExpr: + case *ast.SelectorExpr: + case *ast.ArrayType: + case *ast.MapType: + case *ast.FuncType: + case *ast.IndexExpr, *ast.IndexListExpr: + name, err := getExtendedGenericFieldType(file, expr, nil) + if err == nil { + if schema, err := parser.getTypeSchema(name, file, false); err == nil { + return schema, nil + } + } + + parser.debug.Printf("Type definition of type '%T' is not supported yet. Using 'object' instead. (%s)\n", typeExpr, err) + default: + parser.debug.Printf("Type definition of type '%T' is not supported yet. Using 'object' instead.\n", typeExpr) + } + + return PrimitiveSchema(OBJECT), nil +} diff --git a/vendor/github.com/swaggo/swag/golist.go b/vendor/github.com/swaggo/swag/golist.go new file mode 100644 index 0000000000..fa0b2cd9ee --- /dev/null +++ b/vendor/github.com/swaggo/swag/golist.go @@ -0,0 +1,78 @@ +package swag + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "go/build" + "os/exec" + "path/filepath" +) + +func listPackages(ctx context.Context, dir string, env []string, args ...string) (pkgs []*build.Package, finalErr error) { + cmd := exec.CommandContext(ctx, "go", append([]string{"list", "-json", "-e"}, args...)...) + cmd.Env = env + cmd.Dir = dir + + stdout, err := cmd.StdoutPipe() + if err != nil { + return nil, err + } + var stderrBuf bytes.Buffer + cmd.Stderr = &stderrBuf + defer func() { + if stderrBuf.Len() > 0 { + finalErr = fmt.Errorf("%v\n%s", finalErr, stderrBuf.Bytes()) + } + }() + + err = cmd.Start() + if err != nil { + return nil, err + } + dec := json.NewDecoder(stdout) + for dec.More() { + var pkg build.Package + err = dec.Decode(&pkg) + if err != nil { + return nil, err + } + pkgs = append(pkgs, &pkg) + } + err = cmd.Wait() + if err != nil { + return nil, err + } + return pkgs, nil +} + +func (parser *Parser) getAllGoFileInfoFromDepsByList(pkg *build.Package, parseFlag ParseFlag) error { + ignoreInternal := pkg.Goroot && !parser.ParseInternal + if ignoreInternal { // ignored internal + return nil + } + + if parser.skipPackageByPrefix(pkg.ImportPath) { + return nil // ignored by user-defined package path prefixes + } + + srcDir := pkg.Dir + var err error + for i := range pkg.GoFiles { + err = parser.parseFile(pkg.ImportPath, filepath.Join(srcDir, pkg.GoFiles[i]), nil, parseFlag) + if err != nil { + return err + } + } + + // parse .go source files that import "C" + for i := range pkg.CgoFiles { + err = parser.parseFile(pkg.ImportPath, filepath.Join(srcDir, pkg.CgoFiles[i]), nil, parseFlag) + if err != nil { + return err + } + } + + return nil +} diff --git a/vendor/github.com/swaggo/swag/operation.go b/vendor/github.com/swaggo/swag/operation.go index 4450bf697e..169510ffc1 100644 --- a/vendor/github.com/swaggo/swag/operation.go +++ b/vendor/github.com/swaggo/swag/operation.go @@ -8,24 +8,30 @@ import ( "go/token" "net/http" "os" + "path/filepath" "regexp" - "sort" "strconv" "strings" - "github.com/go-openapi/jsonreference" "github.com/go-openapi/spec" "golang.org/x/tools/go/loader" ) +// RouteProperties describes HTTP properties of a single router comment. +type RouteProperties struct { + HTTPMethod string + Path string + Deprecated bool +} + // Operation describes a single API operation on a path. // For more information: https://github.com/swaggo/swag#api-operation type Operation struct { - HTTPMethod string - Path string + parser *Parser + codeExampleFilesDir string spec.Operation - - parser *Parser + RouterProperties []RouteProperties + State string } var mimeTypeAliases = map[string]string{ @@ -46,75 +52,160 @@ var mimeTypeAliases = map[string]string{ var mimeTypePattern = regexp.MustCompile("^[^/]+/[^/]+$") // NewOperation creates a new Operation with default properties. -// map[int]Response -func NewOperation() *Operation { - return &Operation{ - HTTPMethod: "get", +// map[int]Response. +func NewOperation(parser *Parser, options ...func(*Operation)) *Operation { + if parser == nil { + parser = New() + } + + result := &Operation{ + parser: parser, + RouterProperties: []RouteProperties{}, Operation: spec.Operation{ - OperationProps: spec.OperationProps{}, + OperationProps: spec.OperationProps{ + ID: "", + Description: "", + Summary: "", + Security: nil, + ExternalDocs: nil, + Deprecated: false, + Tags: []string{}, + Consumes: []string{}, + Produces: []string{}, + Schemes: []string{}, + Parameters: []spec.Parameter{}, + Responses: &spec.Responses{ + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{}, + }, + ResponsesProps: spec.ResponsesProps{ + Default: nil, + StatusCodeResponses: make(map[int]spec.Response), + }, + }, + }, + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{}, + }, }, + codeExampleFilesDir: "", + } + + for _, option := range options { + option(result) + } + + return result +} + +// SetCodeExampleFilesDirectory sets the directory to search for codeExamples. +func SetCodeExampleFilesDirectory(directoryPath string) func(*Operation) { + return func(o *Operation) { + o.codeExampleFilesDir = directoryPath } } // ParseComment parses comment for given comment string and returns error if error occurs. func (operation *Operation) ParseComment(comment string, astFile *ast.File) error { - commentLine := strings.TrimSpace(strings.TrimLeft(comment, "//")) + commentLine := strings.TrimSpace(strings.TrimLeft(comment, "/")) if len(commentLine) == 0 { return nil } - attribute := strings.Fields(commentLine)[0] - lineRemainder := strings.TrimSpace(commentLine[len(attribute):]) - lowerAttribute := strings.ToLower(attribute) - var err error + fields := FieldsByAnySpace(commentLine, 2) + attribute := fields[0] + lowerAttribute := strings.ToLower(attribute) + var lineRemainder string + if len(fields) > 1 { + lineRemainder = fields[1] + } switch lowerAttribute { - case "@description": + case stateAttr: + operation.ParseStateComment(lineRemainder) + case descriptionAttr: operation.ParseDescriptionComment(lineRemainder) - case "@description.markdown": + case descriptionMarkdownAttr: commentInfo, err := getMarkdownForTag(lineRemainder, operation.parser.markdownFileDir) if err != nil { return err } + operation.ParseDescriptionComment(string(commentInfo)) - case "@summary": + case summaryAttr: operation.Summary = lineRemainder - case "@id": + case idAttr: operation.ID = lineRemainder - case "@tags": + case tagsAttr: operation.ParseTagsComment(lineRemainder) - case "@accept": - err = operation.ParseAcceptComment(lineRemainder) - case "@produce": - err = operation.ParseProduceComment(lineRemainder) - case "@param": - err = operation.ParseParamComment(lineRemainder, astFile) - case "@success", "@failure": - err = operation.ParseResponseComment(lineRemainder, astFile) - case "@header": - err = operation.ParseResponseHeaderComment(lineRemainder, astFile) - case "@router": - err = operation.ParseRouterComment(lineRemainder) - case "@security": - err = operation.ParseSecurityComment(lineRemainder) - case "@deprecated": + case acceptAttr: + return operation.ParseAcceptComment(lineRemainder) + case produceAttr: + return operation.ParseProduceComment(lineRemainder) + case paramAttr: + return operation.ParseParamComment(lineRemainder, astFile) + case successAttr, failureAttr, responseAttr: + return operation.ParseResponseComment(lineRemainder, astFile) + case headerAttr: + return operation.ParseResponseHeaderComment(lineRemainder, astFile) + case routerAttr: + return operation.ParseRouterComment(lineRemainder, false) + case deprecatedRouterAttr: + return operation.ParseRouterComment(lineRemainder, true) + case securityAttr: + return operation.ParseSecurityComment(lineRemainder) + case deprecatedAttr: operation.Deprecate() + case xCodeSamplesAttr: + return operation.ParseCodeSample(attribute, commentLine, lineRemainder) default: - err = operation.ParseMetadata(attribute, lowerAttribute, lineRemainder) + return operation.ParseMetadata(attribute, lowerAttribute, lineRemainder) } - return err + return nil } -// ParseDescriptionComment godoc +// ParseCodeSample parse code sample. +func (operation *Operation) ParseCodeSample(attribute, _, lineRemainder string) error { + if lineRemainder == "file" { + data, err := getCodeExampleForSummary(operation.Summary, operation.codeExampleFilesDir) + if err != nil { + return err + } + + var valueJSON interface{} + + err = json.Unmarshal(data, &valueJSON) + if err != nil { + return fmt.Errorf("annotation %s need a valid json value", attribute) + } + + // don't use the method provided by spec lib, because it will call toLower() on attribute names, which is wrongly + operation.Extensions[attribute[1:]] = valueJSON + + return nil + } + + // Fallback into existing logic + return operation.ParseMetadata(attribute, strings.ToLower(attribute), lineRemainder) +} + +// ParseStateComment parse state comment. +func (operation *Operation) ParseStateComment(lineRemainder string) { + operation.State = lineRemainder +} + +// ParseDescriptionComment parse description comment. func (operation *Operation) ParseDescriptionComment(lineRemainder string) { if operation.Description == "" { operation.Description = lineRemainder + return } + operation.Description += "\n" + lineRemainder } -// ParseMetadata godoc +// ParseMetadata parse metadata. func (operation *Operation) ParseMetadata(attribute, lowerAttribute, lineRemainder string) error { // parsing specific meta data extensions if strings.HasPrefix(lowerAttribute, "@x-") { @@ -123,122 +214,141 @@ func (operation *Operation) ParseMetadata(attribute, lowerAttribute, lineRemaind } var valueJSON interface{} - if err := json.Unmarshal([]byte(lineRemainder), &valueJSON); err != nil { + + err := json.Unmarshal([]byte(lineRemainder), &valueJSON) + if err != nil { return fmt.Errorf("annotation %s need a valid json value", attribute) } - operation.Operation.AddExtension(attribute[1:], valueJSON) // Trim "@" at head + + // don't use the method provided by spec lib, because it will call toLower() on attribute names, which is wrongly + operation.Extensions[attribute[1:]] = valueJSON } + return nil } -var paramPattern = regexp.MustCompile(`(\S+)[\s]+([\w]+)[\s]+([\S.]+)[\s]+([\w]+)[\s]+"([^"]+)"`) +var paramPattern = regexp.MustCompile(`(\S+)\s+(\w+)\s+([\S. ]+?)\s+(\w+)\s+"([^"]+)"`) + +func findInSlice(arr []string, target string) bool { + for _, str := range arr { + if str == target { + return true + } + } + + return false +} // ParseParamComment parses params return []string of param properties // E.g. @Param queryText formData string true "The email for login" -// [param name] [paramType] [data type] [is mandatory?] [Comment] -// E.g. @Param some_id path int true "Some ID" +// +// [param name] [paramType] [data type] [is mandatory?] [Comment] +// +// E.g. @Param some_id path int true "Some ID". func (operation *Operation) ParseParamComment(commentLine string, astFile *ast.File) error { matches := paramPattern.FindStringSubmatch(commentLine) if len(matches) != 6 { return fmt.Errorf("missing required param comment parameters \"%s\"", commentLine) } + name := matches[1] paramType := matches[2] refType := TransToValidSchemeType(matches[3]) // Detect refType - objectType := "object" + objectType := OBJECT + if strings.HasPrefix(refType, "[]") { - objectType = "array" + objectType = ARRAY refType = strings.TrimPrefix(refType, "[]") refType = TransToValidSchemeType(refType) } else if IsPrimitiveType(refType) || paramType == "formData" && refType == "file" { - objectType = "primitive" + objectType = PRIMITIVE + } + + var enums []interface{} + if !IsPrimitiveType(refType) { + schema, _ := operation.parser.getTypeSchema(refType, astFile, false) + if schema != nil && len(schema.Type) == 1 && schema.Enum != nil { + if objectType == OBJECT { + objectType = PRIMITIVE + } + refType = TransToValidSchemeType(schema.Type[0]) + enums = schema.Enum + } } requiredText := strings.ToLower(matches[4]) - required := requiredText == "true" || requiredText == "required" + required := requiredText == "true" || requiredText == requiredLabel description := matches[5] - param := createParameter(paramType, description, name, refType, required) + param := createParameter(paramType, description, name, objectType, refType, required, enums, operation.parser.collectionFormatInQuery) switch paramType { - case "path", "header", "formData": - switch objectType { - case "array", "object": - return fmt.Errorf("%s is not supported type for %s", refType, paramType) - } - case "query": + case "path", "header", "query", "formData": switch objectType { - case "array": - if !IsPrimitiveType(refType) { + case ARRAY: + if !IsPrimitiveType(refType) && !(refType == "file" && paramType == "formData") { return fmt.Errorf("%s is not supported array type for %s", refType, paramType) } - param.SimpleSchema.Type = "array" - if operation.parser != nil { - param.CollectionFormat = TransToValidCollectionFormat(operation.parser.collectionFormatInQuery) - } - param.SimpleSchema.Items = &spec.Items{ - SimpleSchema: spec.SimpleSchema{ - Type: refType, - }, - } - case "object": - refType, typeSpec, err := operation.registerSchemaType(refType, astFile) - if err != nil { - return err - } - structType, ok := typeSpec.Type.(*ast.StructType) - if !ok { - return fmt.Errorf("%s is not supported type for %s", refType, paramType) - } - refSplit := strings.Split(refType, ".") - schema, err := operation.parser.parseStruct(refSplit[0], structType.Fields) + case PRIMITIVE: + break + case OBJECT: + schema, err := operation.parser.getTypeSchema(refType, astFile, false) if err != nil { return err } + if len(schema.Properties) == 0 { return nil } - find := func(arr []string, target string) bool { - for _, str := range arr { - if str == target { - return true + + items := schema.Properties.ToOrderedSchemaItems() + + for _, item := range items { + name, prop := item.Name, &item.Schema + if len(prop.Type) == 0 { + prop = operation.parser.getUnderlyingSchema(prop) + if len(prop.Type) == 0 { + continue } } - return false - } - orderedNames := make([]string, 0, len(schema.Properties)) - for k := range schema.Properties { - orderedNames = append(orderedNames, k) - } - sort.Strings(orderedNames) - for _, name := range orderedNames { - prop := schema.Properties[name] - if len(prop.Type) == 0 { - continue + + nameOverrideType := paramType + // query also uses formData tags + if paramType == "query" { + nameOverrideType = "formData" } - if prop.Type[0] == "array" && - prop.Items.Schema != nil && - len(prop.Items.Schema.Type) > 0 && - IsSimplePrimitiveType(prop.Items.Schema.Type[0]) { - param = createParameter(paramType, prop.Description, name, prop.Type[0], find(schema.Required, name)) - param.SimpleSchema.Type = prop.Type[0] - if operation.parser != nil && operation.parser.collectionFormatInQuery != "" && param.CollectionFormat == "" { - param.CollectionFormat = TransToValidCollectionFormat(operation.parser.collectionFormatInQuery) + // load overridden type specific name from extensions if exists + if nameVal, ok := item.Schema.Extensions[nameOverrideType]; ok { + name = nameVal.(string) + } + + switch { + case prop.Type[0] == ARRAY: + if prop.Items.Schema == nil { + continue + } + itemSchema := prop.Items.Schema + if len(itemSchema.Type) == 0 { + itemSchema = operation.parser.getUnderlyingSchema(prop.Items.Schema) } - param.SimpleSchema.Items = &spec.Items{ - SimpleSchema: spec.SimpleSchema{ - Type: prop.Items.Schema.Type[0], - }, + if len(itemSchema.Type) == 0 { + continue } - } else if IsSimplePrimitiveType(prop.Type[0]) { - param = createParameter(paramType, prop.Description, name, prop.Type[0], find(schema.Required, name)) - } else { - Println(fmt.Sprintf("skip field [%s] in %s is not supported type for %s", name, refType, paramType)) + if !IsSimplePrimitiveType(itemSchema.Type[0]) { + continue + } + param = createParameter(paramType, prop.Description, name, prop.Type[0], itemSchema.Type[0], findInSlice(schema.Required, item.Name), itemSchema.Enum, operation.parser.collectionFormatInQuery) + + case IsSimplePrimitiveType(prop.Type[0]): + param = createParameter(paramType, prop.Description, name, PRIMITIVE, prop.Type[0], findInSlice(schema.Required, item.Name), nil, operation.parser.collectionFormatInQuery) + default: + operation.parser.debug.Printf("skip field [%s] in %s is not supported type for %s", name, refType, paramType) continue } + param.Nullable = prop.Nullable param.Format = prop.Format param.Default = prop.Default @@ -258,187 +368,173 @@ func (operation *Operation) ParseParamComment(commentLine string, astFile *ast.F param.CommonValidations.Enum = prop.Enum operation.Operation.Parameters = append(operation.Operation.Parameters, param) } + return nil } case "body": - switch objectType { - case "primitive": - param.Schema.Type = spec.StringOrArray{refType} - case "array": - refType = "[]" + refType - fallthrough - case "object": - schema, err := operation.parseObjectSchema(refType, astFile) + if objectType == PRIMITIVE { + param.Schema = PrimitiveSchema(refType) + } else { + schema, err := operation.parseAPIObjectSchema(commentLine, objectType, refType, astFile) if err != nil { return err } + param.Schema = schema } default: return fmt.Errorf("%s is not supported paramType", paramType) } - if err := operation.parseAndExtractionParamAttribute(commentLine, objectType, refType, ¶m); err != nil { + err := operation.parseParamAttribute(commentLine, objectType, refType, paramType, ¶m) + + if err != nil { return err } + operation.Operation.Parameters = append(operation.Operation.Parameters, param) + return nil } -func (operation *Operation) registerSchemaType(schemaType string, astFile *ast.File) (string, *ast.TypeSpec, error) { - if !strings.ContainsRune(schemaType, '.') { - if astFile == nil { - return schemaType, nil, fmt.Errorf("no package name for type %s", schemaType) - } - schemaType = fullTypeName(astFile.Name.String(), schemaType) - } - refSplit := strings.Split(schemaType, ".") - pkgName := refSplit[0] - typeName := refSplit[1] - if typeSpec, ok := operation.parser.TypeDefinitions[pkgName][typeName]; ok { - operation.parser.registerTypes[schemaType] = typeSpec - return schemaType, typeSpec, nil - } - var typeSpec *ast.TypeSpec - if astFile == nil { - return schemaType, nil, fmt.Errorf("can not register schema type: %q reason: astFile == nil", schemaType) - } - for _, imp := range astFile.Imports { - if imp.Name != nil && imp.Name.Name == pkgName { // the import had an alias that matched - break - } - impPath := strings.Replace(imp.Path.Value, `"`, ``, -1) - if strings.HasSuffix(impPath, "/"+pkgName) { - var err error - typeSpec, err = findTypeDef(impPath, typeName) - if err != nil { - return schemaType, nil, fmt.Errorf("can not find type def: %q error: %s", schemaType, err) - } - break - } - } - - if typeSpec == nil { - return schemaType, nil, fmt.Errorf("can not find schema type: %q", schemaType) - } - - if _, ok := operation.parser.TypeDefinitions[pkgName]; !ok { - operation.parser.TypeDefinitions[pkgName] = make(map[string]*ast.TypeSpec) - } - - operation.parser.TypeDefinitions[pkgName][typeName] = typeSpec - operation.parser.registerTypes[schemaType] = typeSpec - return schemaType, typeSpec, nil -} +const ( + formTag = "form" + jsonTag = "json" + uriTag = "uri" + headerTag = "header" + bindingTag = "binding" + defaultTag = "default" + enumsTag = "enums" + exampleTag = "example" + schemaExampleTag = "schemaExample" + formatTag = "format" + validateTag = "validate" + minimumTag = "minimum" + maximumTag = "maximum" + minLengthTag = "minLength" + maxLengthTag = "maxLength" + multipleOfTag = "multipleOf" + readOnlyTag = "readonly" + extensionsTag = "extensions" + collectionFormatTag = "collectionFormat" +) var regexAttributes = map[string]*regexp.Regexp{ // for Enums(A, B) - "enums": regexp.MustCompile(`(?i)\s+enums\(.*\)`), - // for Minimum(0) - "maxinum": regexp.MustCompile(`(?i)\s+maxinum\(.*\)`), - // for Maximum(0) - "mininum": regexp.MustCompile(`(?i)\s+mininum\(.*\)`), - // for Maximum(0) - "default": regexp.MustCompile(`(?i)\s+default\(.*\)`), + enumsTag: regexp.MustCompile(`(?i)\s+enums\(.*\)`), + // for maximum(0) + maximumTag: regexp.MustCompile(`(?i)\s+maxinum|maximum\(.*\)`), + // for minimum(0) + minimumTag: regexp.MustCompile(`(?i)\s+mininum|minimum\(.*\)`), + // for default(0) + defaultTag: regexp.MustCompile(`(?i)\s+default\(.*\)`), // for minlength(0) - "minlength": regexp.MustCompile(`(?i)\s+minlength\(.*\)`), + minLengthTag: regexp.MustCompile(`(?i)\s+minlength\(.*\)`), // for maxlength(0) - "maxlength": regexp.MustCompile(`(?i)\s+maxlength\(.*\)`), + maxLengthTag: regexp.MustCompile(`(?i)\s+maxlength\(.*\)`), // for format(email) - "format": regexp.MustCompile(`(?i)\s+format\(.*\)`), + formatTag: regexp.MustCompile(`(?i)\s+format\(.*\)`), + // for extensions(x-example=test) + extensionsTag: regexp.MustCompile(`(?i)\s+extensions\(.*\)`), // for collectionFormat(csv) - "collectionFormat": regexp.MustCompile(`(?i)\s+collectionFormat\(.*\)`), + collectionFormatTag: regexp.MustCompile(`(?i)\s+collectionFormat\(.*\)`), + // example(0) + exampleTag: regexp.MustCompile(`(?i)\s+example\(.*\)`), + // schemaExample(0) + schemaExampleTag: regexp.MustCompile(`(?i)\s+schemaExample\(.*\)`), } -func (operation *Operation) parseAndExtractionParamAttribute(commentLine, objectType, schemaType string, param *spec.Parameter) error { +func (operation *Operation) parseParamAttribute(comment, objectType, schemaType, paramType string, param *spec.Parameter) error { schemaType = TransToValidSchemeType(schemaType) + for attrKey, re := range regexAttributes { - attr, err := findAttr(re, commentLine) + attr, err := findAttr(re, comment) if err != nil { continue } + switch attrKey { - case "enums": - err := setEnumParam(attr, schemaType, param) - if err != nil { - return err - } - case "maxinum": - n, err := setNumberParam(attrKey, schemaType, attr, commentLine) - if err != nil { - return err - } - param.Maximum = &n - case "mininum": - n, err := setNumberParam(attrKey, schemaType, attr, commentLine) - if err != nil { - return err - } - param.Minimum = &n - case "default": - value, err := defineType(schemaType, attr) - if err != nil { - return nil - } - param.Default = value - case "maxlength": - n, err := setStringParam(attrKey, schemaType, attr, commentLine) - if err != nil { - return err - } - param.MaxLength = &n - case "minlength": - n, err := setStringParam(attrKey, schemaType, attr, commentLine) - if err != nil { - return err - } - param.MinLength = &n - case "format": + case enumsTag: + err = setEnumParam(param, attr, objectType, schemaType, paramType) + case minimumTag, maximumTag: + err = setNumberParam(param, attrKey, schemaType, attr, comment) + case defaultTag: + err = setDefault(param, schemaType, attr) + case minLengthTag, maxLengthTag: + err = setStringParam(param, attrKey, schemaType, attr, comment) + case formatTag: param.Format = attr - case "collectionFormat": - n, err := setCollectionFormatParam(attrKey, objectType, attr, commentLine) - if err != nil { - return err - } - param.CollectionFormat = n + case exampleTag: + err = setExample(param, schemaType, attr) + case schemaExampleTag: + err = setSchemaExample(param, schemaType, attr) + case extensionsTag: + param.Extensions = setExtensionParam(attr) + case collectionFormatTag: + err = setCollectionFormatParam(param, attrKey, objectType, attr, comment) + } + + if err != nil { + return err } } + return nil } func findAttr(re *regexp.Regexp, commentLine string) (string, error) { attr := re.FindString(commentLine) - l := strings.Index(attr, "(") - r := strings.Index(attr, ")") + + l, r := strings.Index(attr, "("), strings.Index(attr, ")") if l == -1 || r == -1 { return "", fmt.Errorf("can not find regex=%s, comment=%s", re.String(), commentLine) } + return strings.TrimSpace(attr[l+1 : r]), nil } -func setStringParam(name, schemaType, attr, commentLine string) (int64, error) { - if schemaType != "string" { - return 0, fmt.Errorf("%s is attribute to set to a number. comment=%s got=%s", name, commentLine, schemaType) +func setStringParam(param *spec.Parameter, name, schemaType, attr, commentLine string) error { + if schemaType != STRING { + return fmt.Errorf("%s is attribute to set to a number. comment=%s got=%s", name, commentLine, schemaType) } + n, err := strconv.ParseInt(attr, 10, 64) if err != nil { - return 0, fmt.Errorf("%s is allow only a number got=%s", name, attr) + return fmt.Errorf("%s is allow only a number got=%s", name, attr) } - return n, nil -} -func setNumberParam(name, schemaType, attr, commentLine string) (float64, error) { - if schemaType != "integer" && schemaType != "number" { - return 0, fmt.Errorf("%s is attribute to set to a number. comment=%s got=%s", name, commentLine, schemaType) + switch name { + case minLengthTag: + param.MinLength = &n + case maxLengthTag: + param.MaxLength = &n } - n, err := strconv.ParseFloat(attr, 64) - if err != nil { - return 0, fmt.Errorf("maximum is allow only a number. comment=%s got=%s", commentLine, attr) + + return nil +} + +func setNumberParam(param *spec.Parameter, name, schemaType, attr, commentLine string) error { + switch schemaType { + case INTEGER, NUMBER: + n, err := strconv.ParseFloat(attr, 64) + if err != nil { + return fmt.Errorf("maximum is allow only a number. comment=%s got=%s", commentLine, attr) + } + + switch name { + case minimumTag: + param.Minimum = &n + case maximumTag: + param.Maximum = &n + } + + return nil + default: + return fmt.Errorf("%s is attribute to set to a number. comment=%s got=%s", name, commentLine, schemaType) } - return n, nil } -func setEnumParam(attr, schemaType string, param *spec.Parameter) error { +func setEnumParam(param *spec.Parameter, attr, objectType, schemaType, paramType string) error { for _, e := range strings.Split(attr, ",") { e = strings.TrimSpace(e) @@ -446,51 +542,131 @@ func setEnumParam(attr, schemaType string, param *spec.Parameter) error { if err != nil { return err } - param.Enum = append(param.Enum, value) + + switch objectType { + case ARRAY: + param.Items.Enum = append(param.Items.Enum, value) + default: + switch paramType { + case "body": + param.Schema.Enum = append(param.Schema.Enum, value) + default: + param.Enum = append(param.Enum, value) + } + } + } + + return nil +} + +func setExtensionParam(attr string) spec.Extensions { + extensions := spec.Extensions{} + + for _, val := range splitNotWrapped(attr, ',') { + parts := strings.SplitN(val, "=", 2) + if len(parts) == 2 { + extensions.Add(parts[0], parts[1]) + + continue + } + + if len(parts[0]) > 0 && string(parts[0][0]) == "!" { + extensions.Add(parts[0][1:], false) + + continue + } + + extensions.Add(parts[0], true) + } + + return extensions +} + +func setCollectionFormatParam(param *spec.Parameter, name, schemaType, attr, commentLine string) error { + if schemaType == ARRAY { + param.CollectionFormat = TransToValidCollectionFormat(attr) + + return nil + } + + return fmt.Errorf("%s is attribute to set to an array. comment=%s got=%s", name, commentLine, schemaType) +} + +func setDefault(param *spec.Parameter, schemaType string, value string) error { + val, err := defineType(schemaType, value) + if err != nil { + return nil // Don't set a default value if it's not valid + } + + param.Default = val + + return nil +} + +func setSchemaExample(param *spec.Parameter, schemaType string, value string) error { + val, err := defineType(schemaType, value) + if err != nil { + return nil // Don't set a example value if it's not valid + } + // skip schema + if param.Schema == nil { + return nil } + + switch v := val.(type) { + case string: + // replaces \r \n \t in example string values. + param.Schema.Example = strings.NewReplacer(`\r`, "\r", `\n`, "\n", `\t`, "\t").Replace(v) + default: + param.Schema.Example = val + } + return nil } -func setCollectionFormatParam(name, schemaType, attr, commentLine string) (string, error) { - if schemaType != "array" { - return "", fmt.Errorf("%s is attribute to set to an array. comment=%s got=%s", name, commentLine, schemaType) +func setExample(param *spec.Parameter, schemaType string, value string) error { + val, err := defineType(schemaType, value) + if err != nil { + return nil // Don't set a example value if it's not valid } - return TransToValidCollectionFormat(attr), nil + + param.Example = val + + return nil } -// defineType enum value define the type (object and array unsupported) -func defineType(schemaType string, value string) (interface{}, error) { +// defineType enum value define the type (object and array unsupported). +func defineType(schemaType string, value string) (v interface{}, err error) { schemaType = TransToValidSchemeType(schemaType) + switch schemaType { - case "string": + case STRING: return value, nil - case "number": - v, err := strconv.ParseFloat(value, 64) + case NUMBER: + v, err = strconv.ParseFloat(value, 64) if err != nil { return nil, fmt.Errorf("enum value %s can't convert to %s err: %s", value, schemaType, err) } - return v, nil - case "integer": - v, err := strconv.Atoi(value) + case INTEGER: + v, err = strconv.Atoi(value) if err != nil { return nil, fmt.Errorf("enum value %s can't convert to %s err: %s", value, schemaType, err) } - return v, nil - case "boolean": - v, err := strconv.ParseBool(value) + case BOOLEAN: + v, err = strconv.ParseBool(value) if err != nil { return nil, fmt.Errorf("enum value %s can't convert to %s err: %s", value, schemaType, err) } - return v, nil default: - return nil, fmt.Errorf("%s is unsupported type in enum value", schemaType) + return nil, fmt.Errorf("%s is unsupported type in enum value %s", schemaType, value) } + + return v, nil } // ParseTagsComment parses comment for given `tag` comment string. func (operation *Operation) ParseTagsComment(commentLine string) { - tags := strings.Split(commentLine, ",") - for _, tag := range tags { + for _, tag := range strings.Split(commentLine, ",") { operation.Tags = append(operation.Tags, strings.TrimSpace(tag)) } } @@ -507,70 +683,92 @@ func (operation *Operation) ParseProduceComment(commentLine string) error { // parseMimeTypeList parses a list of MIME Types for a comment like // `produce` (`Content-Type:` response header) or -// `accept` (`Accept:` request header) +// `accept` (`Accept:` request header). func parseMimeTypeList(mimeTypeList string, typeList *[]string, format string) error { - mimeTypes := strings.Split(mimeTypeList, ",") - for _, typeName := range mimeTypes { + for _, typeName := range strings.Split(mimeTypeList, ",") { if mimeTypePattern.MatchString(typeName) { *typeList = append(*typeList, typeName) + continue } - if aliasMimeType, ok := mimeTypeAliases[typeName]; ok { - *typeList = append(*typeList, aliasMimeType) - continue + + aliasMimeType, ok := mimeTypeAliases[typeName] + if !ok { + return fmt.Errorf(format, typeName) } - return fmt.Errorf(format, typeName) + + *typeList = append(*typeList, aliasMimeType) } + return nil } -var routerPattern = regexp.MustCompile(`^(/[\w\.\/\-{}\+:]*)[[:blank:]]+\[(\w+)]`) +var routerPattern = regexp.MustCompile(`^(/[\w./\-{}+:$]*)[[:blank:]]+\[(\w+)]`) -// ParseRouterComment parses comment for gived `router` comment string. -func (operation *Operation) ParseRouterComment(commentLine string) error { - var matches []string - - if matches = routerPattern.FindStringSubmatch(commentLine); len(matches) != 3 { +// ParseRouterComment parses comment for given `router` comment string. +func (operation *Operation) ParseRouterComment(commentLine string, deprecated bool) error { + matches := routerPattern.FindStringSubmatch(commentLine) + if len(matches) != 3 { return fmt.Errorf("can not parse router comment \"%s\"", commentLine) } - path := matches[1] - httpMethod := matches[2] - operation.Path = path - operation.HTTPMethod = strings.ToUpper(httpMethod) + signature := RouteProperties{ + Path: matches[1], + HTTPMethod: strings.ToUpper(matches[2]), + Deprecated: deprecated, + } + + if _, ok := allMethod[signature.HTTPMethod]; !ok { + return fmt.Errorf("invalid method: %s", signature.HTTPMethod) + } + + operation.RouterProperties = append(operation.RouterProperties, signature) return nil } -// ParseSecurityComment parses comment for gived `security` comment string. +// ParseSecurityComment parses comment for given `security` comment string. func (operation *Operation) ParseSecurityComment(commentLine string) error { - securitySource := commentLine[strings.Index(commentLine, "@Security")+1:] - l := strings.Index(securitySource, "[") - r := strings.Index(securitySource, "]") - // exists scope - if !(l == -1 && r == -1) { - scopes := securitySource[l+1 : r] - s := []string{} - for _, scope := range strings.Split(scopes, ",") { - scope = strings.TrimSpace(scope) - s = append(s, scope) - } - securityKey := securitySource[0:l] - securityMap := map[string][]string{} - securityMap[securityKey] = append(securityMap[securityKey], s...) - operation.Security = append(operation.Security, securityMap) - } else { - securityKey := strings.TrimSpace(securitySource) - securityMap := map[string][]string{} - securityMap[securityKey] = []string{} - operation.Security = append(operation.Security, securityMap) + if len(commentLine) == 0 { + operation.Security = []map[string][]string{} + return nil } + + var ( + securityMap = make(map[string][]string) + securitySource = commentLine[strings.Index(commentLine, "@Security")+1:] + ) + + for _, securityOption := range strings.Split(securitySource, "||") { + securityOption = strings.TrimSpace(securityOption) + + left, right := strings.Index(securityOption, "["), strings.Index(securityOption, "]") + + if !(left == -1 && right == -1) { + scopes := securityOption[left+1 : right] + + var options []string + + for _, scope := range strings.Split(scopes, ",") { + options = append(options, strings.TrimSpace(scope)) + } + + securityKey := securityOption[0:left] + securityMap[securityKey] = append(securityMap[securityKey], options...) + } else { + securityKey := strings.TrimSpace(securityOption) + securityMap[securityKey] = []string{} + } + } + + operation.Security = append(operation.Security, securityMap) + return nil } // findTypeDef attempts to find the *ast.TypeSpec for a specific type given the -// type's name and the package's import path -// TODO: improve finding external pkg +// type's name and the package's import path. +// TODO: improve finding external pkg. func findTypeDef(importPath, typeName string) (*ast.TypeSpec, error) { cwd, err := os.Getwd() if err != nil { @@ -606,12 +804,13 @@ func findTypeDef(importPath, typeName string) (*ast.TypeSpec, error) { } // TODO: possibly cache pkgInfo since it's an expensive operation - for i := range pkgInfo.Files { for _, astDeclaration := range pkgInfo.Files[i].Decls { - if generalDeclaration, ok := astDeclaration.(*ast.GenDecl); ok && generalDeclaration.Tok == token.TYPE { + generalDeclaration, ok := astDeclaration.(*ast.GenDecl) + if ok && generalDeclaration.Tok == token.TYPE { for _, astSpec := range generalDeclaration.Specs { - if typeSpec, ok := astSpec.(*ast.TypeSpec); ok { + typeSpec, ok := astSpec.(*ast.TypeSpec) + if ok { if typeSpec.Name.String() == typeName { return typeSpec, nil } @@ -620,320 +819,427 @@ func findTypeDef(importPath, typeName string) (*ast.TypeSpec, error) { } } } + return nil, fmt.Errorf("type spec not found") } -var responsePattern = regexp.MustCompile(`([\d]+)[\s]+([\w\{\}]+)[\s]+([\w\-\.\/\{\}=,\[\]]+)[^"]*(.*)?`) +var responsePattern = regexp.MustCompile(`^([\w,]+)\s+([\w{}]+)\s+([\w\-.\\{}=,\[\s\]]+)\s*(".*)?`) -//RepsonseType{data1=Type1,data2=Type2} -var combinedPattern = regexp.MustCompile(`^([\w\-\.\/\[\]]+)\{(.*)\}$`) +// ResponseType{data1=Type1,data2=Type2}. +var combinedPattern = regexp.MustCompile(`^([\w\-./\[\]]+){(.*)}$`) func (operation *Operation) parseObjectSchema(refType string, astFile *ast.File) (*spec.Schema, error) { + return parseObjectSchema(operation.parser, refType, astFile) +} + +func parseObjectSchema(parser *Parser, refType string, astFile *ast.File) (*spec.Schema, error) { switch { - case refType == "interface{}": - return &spec.Schema{SchemaProps: spec.SchemaProps{Type: []string{"object"}}}, nil + case refType == NIL: + return nil, nil + case refType == INTERFACE: + return PrimitiveSchema(OBJECT), nil + case refType == ANY: + return PrimitiveSchema(OBJECT), nil case IsGolangPrimitiveType(refType): refType = TransToValidSchemeType(refType) - return &spec.Schema{SchemaProps: spec.SchemaProps{Type: []string{refType}}}, nil + + return PrimitiveSchema(refType), nil case IsPrimitiveType(refType): - return &spec.Schema{SchemaProps: spec.SchemaProps{Type: []string{refType}}}, nil + return PrimitiveSchema(refType), nil case strings.HasPrefix(refType, "[]"): - schema, err := operation.parseObjectSchema(refType[2:], astFile) + schema, err := parseObjectSchema(parser, refType[2:], astFile) if err != nil { return nil, err } - return &spec.Schema{SchemaProps: spec.SchemaProps{ - Type: []string{"array"}, - Items: &spec.SchemaOrArray{Schema: schema}}, - }, nil + + return spec.ArrayProperty(schema), nil case strings.HasPrefix(refType, "map["): - //ignore key type + // ignore key type idx := strings.Index(refType, "]") if idx < 0 { return nil, fmt.Errorf("invalid type: %s", refType) } + refType = refType[idx+1:] - var valueSchema spec.SchemaOrBool - if refType == "interface{}" { - valueSchema.Allows = true - } else { - schema, err := operation.parseObjectSchema(refType, astFile) - if err != nil { - return &spec.Schema{}, err - } - valueSchema.Schema = schema + if refType == INTERFACE || refType == ANY { + return spec.MapProperty(nil), nil } - return &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - AdditionalProperties: &valueSchema, - }, - }, nil + + schema, err := parseObjectSchema(parser, refType, astFile) + if err != nil { + return nil, err + } + + return spec.MapProperty(schema), nil case strings.Contains(refType, "{"): - return operation.parseResponseCombinedObjectSchema(refType, astFile) + return parseCombinedObjectSchema(parser, refType, astFile) default: - if operation.parser != nil { // checking refType has existing in 'TypeDefinitions' - refNewType, typeSpec, err := operation.registerSchemaType(refType, astFile) + if parser != nil { // checking refType has existing in 'TypeDefinitions' + schema, err := parser.getTypeSchema(refType, astFile, true) if err != nil { return nil, err } - refType = TypeDocName(refNewType, typeSpec) + + return schema, nil } - return &spec.Schema{SchemaProps: spec.SchemaProps{Ref: spec.Ref{ - Ref: jsonreference.MustCreateRef("#/definitions/" + refType), - }}}, nil + + return RefSchema(refType), nil } } -func (operation *Operation) parseResponseCombinedObjectSchema(refType string, astFile *ast.File) (*spec.Schema, error) { +func parseFields(s string) []string { + nestLevel := 0 + + return strings.FieldsFunc(s, func(char rune) bool { + if char == '{' { + nestLevel++ + + return false + } else if char == '}' { + nestLevel-- + + return false + } + + return char == ',' && nestLevel == 0 + }) +} + +func parseCombinedObjectSchema(parser *Parser, refType string, astFile *ast.File) (*spec.Schema, error) { matches := combinedPattern.FindStringSubmatch(refType) if len(matches) != 3 { return nil, fmt.Errorf("invalid type: %s", refType) } - refType = matches[1] - schema, err := operation.parseObjectSchema(refType, astFile) + + schema, err := parseObjectSchema(parser, matches[1], astFile) if err != nil { return nil, err } - parseFields := func(s string) []string { - n := 0 - return strings.FieldsFunc(s, func(r rune) bool { - if r == '{' { - n++ - return false - } else if r == '}' { - n-- - return false - } - return r == ',' && n == 0 - }) - } + fields, props := parseFields(matches[2]), map[string]spec.Schema{} - fields := parseFields(matches[2]) - props := map[string]spec.Schema{} for _, field := range fields { - if matches := strings.SplitN(field, "=", 2); len(matches) == 2 { - if strings.HasPrefix(matches[1], "[]") { - itemSchema, err := operation.parseObjectSchema(matches[1][2:], astFile) - if err != nil { - return nil, err - } - props[matches[0]] = spec.Schema{SchemaProps: spec.SchemaProps{ - Type: []string{"array"}, - Items: &spec.SchemaOrArray{Schema: itemSchema}}, - } - } else { - schema, err := operation.parseObjectSchema(matches[1], astFile) - if err != nil { - return nil, err - } - props[matches[0]] = *schema + keyVal := strings.SplitN(field, "=", 2) + if len(keyVal) == 2 { + schema, err := parseObjectSchema(parser, keyVal[1], astFile) + if err != nil { + return nil, err } + + if schema == nil { + schema = PrimitiveSchema(OBJECT) + } + + props[keyVal[0]] = *schema } } if len(props) == 0 { return schema, nil } - return &spec.Schema{ + + if schema.Ref.GetURL() == nil && len(schema.Type) > 0 && schema.Type[0] == OBJECT && len(schema.Properties) == 0 && schema.AdditionalProperties == nil { + schema.Properties = props + return schema, nil + } + + return spec.ComposedSchema(*schema, spec.Schema{ SchemaProps: spec.SchemaProps{ - AllOf: []spec.Schema{ - *schema, - { - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: props, - }, - }, - }, + Type: []string{OBJECT}, + Properties: props, }, - }, nil + }), nil } -func (operation *Operation) parseResponseSchema(schemaType, refType string, astFile *ast.File) (*spec.Schema, error) { +func (operation *Operation) parseAPIObjectSchema(commentLine, schemaType, refType string, astFile *ast.File) (*spec.Schema, error) { + if strings.HasSuffix(refType, ",") && strings.Contains(refType, "[") { + // regexp may have broken generic syntax. find closing bracket and add it back + allMatchesLenOffset := strings.Index(commentLine, refType) + len(refType) + lostPartEndIdx := strings.Index(commentLine[allMatchesLenOffset:], "]") + if lostPartEndIdx >= 0 { + refType += commentLine[allMatchesLenOffset : allMatchesLenOffset+lostPartEndIdx+1] + } + } + switch schemaType { - case "object": + case OBJECT: if !strings.HasPrefix(refType, "[]") { return operation.parseObjectSchema(refType, astFile) } + refType = refType[2:] + fallthrough - case "array": + case ARRAY: schema, err := operation.parseObjectSchema(refType, astFile) if err != nil { return nil, err } - return &spec.Schema{SchemaProps: spec.SchemaProps{ - Type: []string{"array"}, - Items: &spec.SchemaOrArray{Schema: schema}}, - }, nil + + return spec.ArrayProperty(schema), nil default: - return &spec.Schema{SchemaProps: spec.SchemaProps{Type: []string{schemaType}}}, nil + return PrimitiveSchema(schemaType), nil } } // ParseResponseComment parses comment for given `response` comment string. func (operation *Operation) ParseResponseComment(commentLine string, astFile *ast.File) error { - var matches []string - - if matches = responsePattern.FindStringSubmatch(commentLine); len(matches) != 5 { + matches := responsePattern.FindStringSubmatch(commentLine) + if len(matches) != 5 { err := operation.ParseEmptyResponseComment(commentLine) if err != nil { return operation.ParseEmptyResponseOnly(commentLine) } + return err } - code, _ := strconv.Atoi(matches[1]) + description := strings.Trim(matches[4], "\"") - responseDescription := strings.Trim(matches[4], "\"") - if responseDescription == "" { - responseDescription = http.StatusText(code) - } - - schemaType := strings.Trim(matches[2], "{}") - refType := matches[3] - schema, err := operation.parseResponseSchema(schemaType, refType, astFile) + schema, err := operation.parseAPIObjectSchema(commentLine, strings.Trim(matches[2], "{}"), strings.TrimSpace(matches[3]), astFile) if err != nil { return err } - if operation.Responses == nil { - operation.Responses = &spec.Responses{ - ResponsesProps: spec.ResponsesProps{ - StatusCodeResponses: make(map[int]spec.Response), - }, + for _, codeStr := range strings.Split(matches[1], ",") { + if strings.EqualFold(codeStr, defaultTag) { + operation.DefaultResponse().WithSchema(schema).WithDescription(description) + + continue } - } - operation.Responses.StatusCodeResponses[code] = spec.Response{ - ResponseProps: spec.ResponseProps{Schema: schema, Description: responseDescription}, + code, err := strconv.Atoi(codeStr) + if err != nil { + return fmt.Errorf("can not parse response comment \"%s\"", commentLine) + } + + resp := spec.NewResponse().WithSchema(schema).WithDescription(description) + if description == "" { + resp.WithDescription(http.StatusText(code)) + } + + operation.AddResponse(code, resp) } + return nil } -// ParseResponseHeaderComment parses comment for gived `response header` comment string. -func (operation *Operation) ParseResponseHeaderComment(commentLine string, astFile *ast.File) error { - var matches []string +func newHeaderSpec(schemaType, description string) spec.Header { + return spec.Header{ + SimpleSchema: spec.SimpleSchema{ + Type: schemaType, + }, + HeaderProps: spec.HeaderProps{ + Description: description, + }, + VendorExtensible: spec.VendorExtensible{ + Extensions: nil, + }, + CommonValidations: spec.CommonValidations{ + Maximum: nil, + ExclusiveMaximum: false, + Minimum: nil, + ExclusiveMinimum: false, + MaxLength: nil, + MinLength: nil, + Pattern: "", + MaxItems: nil, + MinItems: nil, + UniqueItems: false, + MultipleOf: nil, + Enum: nil, + }, + } +} - if matches = responsePattern.FindStringSubmatch(commentLine); len(matches) != 5 { +// ParseResponseHeaderComment parses comment for given `response header` comment string. +func (operation *Operation) ParseResponseHeaderComment(commentLine string, _ *ast.File) error { + matches := responsePattern.FindStringSubmatch(commentLine) + if len(matches) != 5 { return fmt.Errorf("can not parse response comment \"%s\"", commentLine) } - response := spec.Response{} - - code, _ := strconv.Atoi(matches[1]) + header := newHeaderSpec(strings.Trim(matches[2], "{}"), strings.Trim(matches[4], "\"")) - responseDescription := strings.Trim(matches[4], "\"") - if responseDescription == "" { - responseDescription = http.StatusText(code) - } - response.Description = responseDescription + headerKey := strings.TrimSpace(matches[3]) - schemaType := strings.Trim(matches[2], "{}") - refType := matches[3] + if strings.EqualFold(matches[1], "all") { + if operation.Responses.Default != nil { + operation.Responses.Default.Headers[headerKey] = header + } - if operation.Responses == nil { - operation.Responses = &spec.Responses{ - ResponsesProps: spec.ResponsesProps{ - StatusCodeResponses: make(map[int]spec.Response), - }, + if operation.Responses.StatusCodeResponses != nil { + for code, response := range operation.Responses.StatusCodeResponses { + response.Headers[headerKey] = header + operation.Responses.StatusCodeResponses[code] = response + } } + + return nil } - response, responseExist := operation.Responses.StatusCodeResponses[code] - if responseExist { - header := spec.Header{} - header.Description = responseDescription - header.Type = schemaType + for _, codeStr := range strings.Split(matches[1], ",") { + if strings.EqualFold(codeStr, defaultTag) { + if operation.Responses.Default != nil { + operation.Responses.Default.Headers[headerKey] = header + } - if response.Headers == nil { - response.Headers = make(map[string]spec.Header) + continue } - response.Headers[refType] = header - operation.Responses.StatusCodeResponses[code] = response + code, err := strconv.Atoi(codeStr) + if err != nil { + return fmt.Errorf("can not parse response comment \"%s\"", commentLine) + } + + if operation.Responses.StatusCodeResponses != nil { + response, responseExist := operation.Responses.StatusCodeResponses[code] + if responseExist { + response.Headers[headerKey] = header + + operation.Responses.StatusCodeResponses[code] = response + } + } } return nil } -var emptyResponsePattern = regexp.MustCompile(`([\d]+)[\s]+"(.*)"`) +var emptyResponsePattern = regexp.MustCompile(`([\w,]+)\s+"(.*)"`) -// ParseEmptyResponseComment parse only comment out status code and description,eg: @Success 200 "it's ok" +// ParseEmptyResponseComment parse only comment out status code and description,eg: @Success 200 "it's ok". func (operation *Operation) ParseEmptyResponseComment(commentLine string) error { - var matches []string - - if matches = emptyResponsePattern.FindStringSubmatch(commentLine); len(matches) != 3 { + matches := emptyResponsePattern.FindStringSubmatch(commentLine) + if len(matches) != 3 { return fmt.Errorf("can not parse response comment \"%s\"", commentLine) } - response := spec.Response{} + description := strings.Trim(matches[2], "\"") - code, _ := strconv.Atoi(matches[1]) + for _, codeStr := range strings.Split(matches[1], ",") { + if strings.EqualFold(codeStr, defaultTag) { + operation.DefaultResponse().WithDescription(description) - response.Description = strings.Trim(matches[2], "") + continue + } - if operation.Responses == nil { - operation.Responses = &spec.Responses{ - ResponsesProps: spec.ResponsesProps{ - StatusCodeResponses: make(map[int]spec.Response), - }, + code, err := strconv.Atoi(codeStr) + if err != nil { + return fmt.Errorf("can not parse response comment \"%s\"", commentLine) } - } - operation.Responses.StatusCodeResponses[code] = response + operation.AddResponse(code, spec.NewResponse().WithDescription(description)) + } return nil } -//ParseEmptyResponseOnly parse only comment out status code ,eg: @Success 200 +// ParseEmptyResponseOnly parse only comment out status code ,eg: @Success 200. func (operation *Operation) ParseEmptyResponseOnly(commentLine string) error { - response := spec.Response{} + for _, codeStr := range strings.Split(commentLine, ",") { + if strings.EqualFold(codeStr, defaultTag) { + _ = operation.DefaultResponse() - code, err := strconv.Atoi(commentLine) - if err != nil { - return fmt.Errorf("can not parse response comment \"%s\"", commentLine) + continue + } + + code, err := strconv.Atoi(codeStr) + if err != nil { + return fmt.Errorf("can not parse response comment \"%s\"", commentLine) + } + + operation.AddResponse(code, spec.NewResponse().WithDescription(http.StatusText(code))) } - if operation.Responses == nil { - operation.Responses = &spec.Responses{ - ResponsesProps: spec.ResponsesProps{ - StatusCodeResponses: make(map[int]spec.Response), + + return nil +} + +// DefaultResponse return the default response member pointer. +func (operation *Operation) DefaultResponse() *spec.Response { + if operation.Responses.Default == nil { + operation.Responses.Default = &spec.Response{ + ResponseProps: spec.ResponseProps{ + Description: "", + Headers: make(map[string]spec.Header), }, } } - operation.Responses.StatusCodeResponses[code] = response + return operation.Responses.Default +} + +// AddResponse add a response for a code. +func (operation *Operation) AddResponse(code int, response *spec.Response) { + if response.Headers == nil { + response.Headers = make(map[string]spec.Header) + } - return nil + operation.Responses.StatusCodeResponses[code] = *response } -// createParameter returns swagger spec.Parameter for gived paramType, description, paramName, schemaType, required -func createParameter(paramType, description, paramName, schemaType string, required bool) spec.Parameter { +// createParameter returns swagger spec.Parameter for given paramType, description, paramName, schemaType, required. +func createParameter(paramType, description, paramName, objectType, schemaType string, required bool, enums []interface{}, collectionFormat string) spec.Parameter { // //five possible parameter types. query, path, body, header, form - paramProps := spec.ParamProps{ - Name: paramName, - Description: description, - Required: required, - In: paramType, + result := spec.Parameter{ + ParamProps: spec.ParamProps{ + Name: paramName, + Description: description, + Required: required, + In: paramType, + }, } + if paramType == "body" { - paramProps.Schema = &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{schemaType}, + return result + } + + switch objectType { + case ARRAY: + result.Type = objectType + result.CollectionFormat = collectionFormat + result.Items = &spec.Items{ + CommonValidations: spec.CommonValidations{ + Enum: enums, + }, + SimpleSchema: spec.SimpleSchema{ + Type: schemaType, }, } - parameter := spec.Parameter{ - ParamProps: paramProps, - } - return parameter + case PRIMITIVE, OBJECT: + result.Type = schemaType + result.Enum = enums } - parameter := spec.Parameter{ - ParamProps: paramProps, - SimpleSchema: spec.SimpleSchema{ - Type: schemaType, - }, + return result +} + +func getCodeExampleForSummary(summaryName string, dirPath string) ([]byte, error) { + dirEntries, err := os.ReadDir(dirPath) + if err != nil { + return nil, err } - return parameter + + for _, entry := range dirEntries { + if entry.IsDir() { + continue + } + + fileName := entry.Name() + + if !strings.Contains(fileName, ".json") { + continue + } + + if strings.Contains(fileName, summaryName) { + fullPath := filepath.Join(dirPath, fileName) + + commentInfo, err := os.ReadFile(fullPath) + if err != nil { + return nil, fmt.Errorf("Failed to read code example file %s error: %s ", fullPath, err) + } + + return commentInfo, nil + } + } + + return nil, fmt.Errorf("unable to find code example file for tag %s in the given directory", summaryName) } diff --git a/vendor/github.com/swaggo/swag/package.go b/vendor/github.com/swaggo/swag/package.go new file mode 100644 index 0000000000..6c3129e52e --- /dev/null +++ b/vendor/github.com/swaggo/swag/package.go @@ -0,0 +1,187 @@ +package swag + +import ( + "go/ast" + "go/token" + "reflect" + "strconv" + "strings" +) + +// PackageDefinitions files and definition in a package. +type PackageDefinitions struct { + // files in this package, map key is file's relative path starting package path + Files map[string]*ast.File + + // definitions in this package, map key is typeName + TypeDefinitions map[string]*TypeSpecDef + + // const variables in this package, map key is the name + ConstTable map[string]*ConstVariable + + // const variables in order in this package + OrderedConst []*ConstVariable + + // package name + Name string + + // package path + Path string +} + +// ConstVariableGlobalEvaluator an interface used to evaluate enums across packages +type ConstVariableGlobalEvaluator interface { + EvaluateConstValue(pkg *PackageDefinitions, cv *ConstVariable, recursiveStack map[string]struct{}) (interface{}, ast.Expr) + EvaluateConstValueByName(file *ast.File, pkgPath, constVariableName string, recursiveStack map[string]struct{}) (interface{}, ast.Expr) + FindTypeSpec(typeName string, file *ast.File) *TypeSpecDef +} + +// NewPackageDefinitions new a PackageDefinitions object +func NewPackageDefinitions(name, pkgPath string) *PackageDefinitions { + return &PackageDefinitions{ + Name: name, + Path: pkgPath, + Files: make(map[string]*ast.File), + TypeDefinitions: make(map[string]*TypeSpecDef), + ConstTable: make(map[string]*ConstVariable), + } +} + +// AddFile add a file +func (pkg *PackageDefinitions) AddFile(pkgPath string, file *ast.File) *PackageDefinitions { + pkg.Files[pkgPath] = file + return pkg +} + +// AddTypeSpec add a type spec. +func (pkg *PackageDefinitions) AddTypeSpec(name string, typeSpec *TypeSpecDef) *PackageDefinitions { + pkg.TypeDefinitions[name] = typeSpec + return pkg +} + +// AddConst add a const variable. +func (pkg *PackageDefinitions) AddConst(astFile *ast.File, valueSpec *ast.ValueSpec) *PackageDefinitions { + for i := 0; i < len(valueSpec.Names) && i < len(valueSpec.Values); i++ { + variable := &ConstVariable{ + Name: valueSpec.Names[i], + Type: valueSpec.Type, + Value: valueSpec.Values[i], + Comment: valueSpec.Comment, + File: astFile, + } + pkg.ConstTable[valueSpec.Names[i].Name] = variable + pkg.OrderedConst = append(pkg.OrderedConst, variable) + } + return pkg +} + +func (pkg *PackageDefinitions) evaluateConstValue(file *ast.File, iota int, expr ast.Expr, globalEvaluator ConstVariableGlobalEvaluator, recursiveStack map[string]struct{}) (interface{}, ast.Expr) { + switch valueExpr := expr.(type) { + case *ast.Ident: + if valueExpr.Name == "iota" { + return iota, nil + } + if pkg.ConstTable != nil { + if cv, ok := pkg.ConstTable[valueExpr.Name]; ok { + return globalEvaluator.EvaluateConstValue(pkg, cv, recursiveStack) + } + } + case *ast.SelectorExpr: + pkgIdent, ok := valueExpr.X.(*ast.Ident) + if !ok { + return nil, nil + } + return globalEvaluator.EvaluateConstValueByName(file, pkgIdent.Name, valueExpr.Sel.Name, recursiveStack) + case *ast.BasicLit: + switch valueExpr.Kind { + case token.INT: + // handle underscored number, such as 1_000_000 + if strings.ContainsRune(valueExpr.Value, '_') { + valueExpr.Value = strings.Replace(valueExpr.Value, "_", "", -1) + } + if len(valueExpr.Value) >= 2 && valueExpr.Value[0] == '0' { + var start, base = 2, 8 + switch valueExpr.Value[1] { + case 'x', 'X': + //hex + base = 16 + case 'b', 'B': + //binary + base = 2 + default: + //octet + start = 1 + } + if x, err := strconv.ParseInt(valueExpr.Value[start:], base, 64); err == nil { + return int(x), nil + } else if x, err := strconv.ParseUint(valueExpr.Value[start:], base, 64); err == nil { + return x, nil + } else { + panic(err) + } + } + + //a basic literal integer is int type in default, or must have an explicit converting type in front + if x, err := strconv.ParseInt(valueExpr.Value, 10, 64); err == nil { + return int(x), nil + } else if x, err := strconv.ParseUint(valueExpr.Value, 10, 64); err == nil { + return x, nil + } else { + panic(err) + } + case token.STRING: + if valueExpr.Value[0] == '`' { + return valueExpr.Value[1 : len(valueExpr.Value)-1], nil + } + return EvaluateEscapedString(valueExpr.Value[1 : len(valueExpr.Value)-1]), nil + case token.CHAR: + return EvaluateEscapedChar(valueExpr.Value[1 : len(valueExpr.Value)-1]), nil + } + case *ast.UnaryExpr: + x, evalType := pkg.evaluateConstValue(file, iota, valueExpr.X, globalEvaluator, recursiveStack) + if x == nil { + return x, evalType + } + return EvaluateUnary(x, valueExpr.Op, evalType) + case *ast.BinaryExpr: + x, evalTypex := pkg.evaluateConstValue(file, iota, valueExpr.X, globalEvaluator, recursiveStack) + y, evalTypey := pkg.evaluateConstValue(file, iota, valueExpr.Y, globalEvaluator, recursiveStack) + if x == nil || y == nil { + return nil, nil + } + return EvaluateBinary(x, y, valueExpr.Op, evalTypex, evalTypey) + case *ast.ParenExpr: + return pkg.evaluateConstValue(file, iota, valueExpr.X, globalEvaluator, recursiveStack) + case *ast.CallExpr: + //data conversion + if len(valueExpr.Args) != 1 { + return nil, nil + } + arg := valueExpr.Args[0] + if ident, ok := valueExpr.Fun.(*ast.Ident); ok { + name := ident.Name + if name == "uintptr" { + name = "uint" + } + value, _ := pkg.evaluateConstValue(file, iota, arg, globalEvaluator, recursiveStack) + if IsGolangPrimitiveType(name) { + value = EvaluateDataConversion(value, name) + return value, nil + } else if name == "len" { + return reflect.ValueOf(value).Len(), nil + } + typeDef := globalEvaluator.FindTypeSpec(name, file) + if typeDef == nil { + return nil, nil + } + return value, valueExpr.Fun + } else if selector, ok := valueExpr.Fun.(*ast.SelectorExpr); ok { + typeDef := globalEvaluator.FindTypeSpec(fullTypeName(selector.X.(*ast.Ident).Name, selector.Sel.Name), file) + if typeDef == nil { + return nil, nil + } + return arg, typeDef.TypeSpec.Type + } + } + return nil, nil +} diff --git a/vendor/github.com/swaggo/swag/packages.go b/vendor/github.com/swaggo/swag/packages.go new file mode 100644 index 0000000000..69a1b05298 --- /dev/null +++ b/vendor/github.com/swaggo/swag/packages.go @@ -0,0 +1,574 @@ +package swag + +import ( + "fmt" + "go/ast" + goparser "go/parser" + "go/token" + "os" + "path/filepath" + "runtime" + "sort" + "strings" + + "golang.org/x/tools/go/loader" +) + +// PackagesDefinitions map[package import path]*PackageDefinitions. +type PackagesDefinitions struct { + files map[*ast.File]*AstFileInfo + packages map[string]*PackageDefinitions + uniqueDefinitions map[string]*TypeSpecDef + parseDependency ParseFlag + debug Debugger +} + +// NewPackagesDefinitions create object PackagesDefinitions. +func NewPackagesDefinitions() *PackagesDefinitions { + return &PackagesDefinitions{ + files: make(map[*ast.File]*AstFileInfo), + packages: make(map[string]*PackageDefinitions), + uniqueDefinitions: make(map[string]*TypeSpecDef), + } +} + +// ParseFile parse a source file. +func (pkgDefs *PackagesDefinitions) ParseFile(packageDir, path string, src interface{}, flag ParseFlag) error { + // positions are relative to FileSet + fileSet := token.NewFileSet() + astFile, err := goparser.ParseFile(fileSet, path, src, goparser.ParseComments) + if err != nil { + return fmt.Errorf("failed to parse file %s, error:%+v", path, err) + } + return pkgDefs.collectAstFile(fileSet, packageDir, path, astFile, flag) +} + +// collectAstFile collect ast.file. +func (pkgDefs *PackagesDefinitions) collectAstFile(fileSet *token.FileSet, packageDir, path string, astFile *ast.File, flag ParseFlag) error { + if pkgDefs.files == nil { + pkgDefs.files = make(map[*ast.File]*AstFileInfo) + } + + if pkgDefs.packages == nil { + pkgDefs.packages = make(map[string]*PackageDefinitions) + } + + // return without storing the file if we lack a packageDir + if packageDir == "" { + return nil + } + + path, err := filepath.Abs(path) + if err != nil { + return err + } + + dependency, ok := pkgDefs.packages[packageDir] + if ok { + // return without storing the file if it already exists + _, exists := dependency.Files[path] + if exists { + return nil + } + + dependency.Files[path] = astFile + } else { + pkgDefs.packages[packageDir] = NewPackageDefinitions(astFile.Name.Name, packageDir).AddFile(path, astFile) + } + + pkgDefs.files[astFile] = &AstFileInfo{ + FileSet: fileSet, + File: astFile, + Path: path, + PackagePath: packageDir, + ParseFlag: flag, + } + + return nil +} + +// RangeFiles for range the collection of ast.File in alphabetic order. +func (pkgDefs *PackagesDefinitions) RangeFiles(handle func(info *AstFileInfo) error) error { + sortedFiles := make([]*AstFileInfo, 0, len(pkgDefs.files)) + for _, info := range pkgDefs.files { + // ignore package path prefix with 'vendor' or $GOROOT, + // because the router info of api will not be included these files. + if strings.HasPrefix(info.PackagePath, "vendor") || strings.HasPrefix(info.Path, runtime.GOROOT()) { + continue + } + sortedFiles = append(sortedFiles, info) + } + + sort.Slice(sortedFiles, func(i, j int) bool { + return strings.Compare(sortedFiles[i].Path, sortedFiles[j].Path) < 0 + }) + + for _, info := range sortedFiles { + err := handle(info) + if err != nil { + return err + } + } + + return nil +} + +// ParseTypes parse types +// @Return parsed definitions. +func (pkgDefs *PackagesDefinitions) ParseTypes() (map[*TypeSpecDef]*Schema, error) { + parsedSchemas := make(map[*TypeSpecDef]*Schema) + for astFile, info := range pkgDefs.files { + pkgDefs.parseTypesFromFile(astFile, info.PackagePath, parsedSchemas) + pkgDefs.parseFunctionScopedTypesFromFile(astFile, info.PackagePath, parsedSchemas) + } + pkgDefs.removeAllNotUniqueTypes() + pkgDefs.evaluateAllConstVariables() + pkgDefs.collectConstEnums(parsedSchemas) + return parsedSchemas, nil +} + +func (pkgDefs *PackagesDefinitions) parseTypesFromFile(astFile *ast.File, packagePath string, parsedSchemas map[*TypeSpecDef]*Schema) { + for _, astDeclaration := range astFile.Decls { + generalDeclaration, ok := astDeclaration.(*ast.GenDecl) + if !ok { + continue + } + if generalDeclaration.Tok == token.TYPE { + for _, astSpec := range generalDeclaration.Specs { + if typeSpec, ok := astSpec.(*ast.TypeSpec); ok { + typeSpecDef := &TypeSpecDef{ + PkgPath: packagePath, + File: astFile, + TypeSpec: typeSpec, + } + + if idt, ok := typeSpec.Type.(*ast.Ident); ok && IsGolangPrimitiveType(idt.Name) && parsedSchemas != nil { + parsedSchemas[typeSpecDef] = &Schema{ + PkgPath: typeSpecDef.PkgPath, + Name: astFile.Name.Name, + Schema: PrimitiveSchema(TransToValidSchemeType(idt.Name)), + } + } + + if pkgDefs.uniqueDefinitions == nil { + pkgDefs.uniqueDefinitions = make(map[string]*TypeSpecDef) + } + + fullName := typeSpecDef.TypeName() + + anotherTypeDef, ok := pkgDefs.uniqueDefinitions[fullName] + if ok { + if anotherTypeDef == nil { + typeSpecDef.NotUnique = true + fullName = typeSpecDef.TypeName() + pkgDefs.uniqueDefinitions[fullName] = typeSpecDef + } else if typeSpecDef.PkgPath != anotherTypeDef.PkgPath { + pkgDefs.uniqueDefinitions[fullName] = nil + anotherTypeDef.NotUnique = true + pkgDefs.uniqueDefinitions[anotherTypeDef.TypeName()] = anotherTypeDef + typeSpecDef.NotUnique = true + fullName = typeSpecDef.TypeName() + pkgDefs.uniqueDefinitions[fullName] = typeSpecDef + } + } else { + pkgDefs.uniqueDefinitions[fullName] = typeSpecDef + } + + if pkgDefs.packages[typeSpecDef.PkgPath] == nil { + pkgDefs.packages[typeSpecDef.PkgPath] = NewPackageDefinitions(astFile.Name.Name, typeSpecDef.PkgPath).AddTypeSpec(typeSpecDef.Name(), typeSpecDef) + } else if _, ok = pkgDefs.packages[typeSpecDef.PkgPath].TypeDefinitions[typeSpecDef.Name()]; !ok { + pkgDefs.packages[typeSpecDef.PkgPath].AddTypeSpec(typeSpecDef.Name(), typeSpecDef) + } + } + } + } else if generalDeclaration.Tok == token.CONST { + // collect consts + pkgDefs.collectConstVariables(astFile, packagePath, generalDeclaration) + } + } +} + +func (pkgDefs *PackagesDefinitions) parseFunctionScopedTypesFromFile(astFile *ast.File, packagePath string, parsedSchemas map[*TypeSpecDef]*Schema) { + for _, astDeclaration := range astFile.Decls { + funcDeclaration, ok := astDeclaration.(*ast.FuncDecl) + if ok && funcDeclaration.Body != nil { + for _, stmt := range funcDeclaration.Body.List { + if declStmt, ok := (stmt).(*ast.DeclStmt); ok { + if genDecl, ok := (declStmt.Decl).(*ast.GenDecl); ok && genDecl.Tok == token.TYPE { + for _, astSpec := range genDecl.Specs { + if typeSpec, ok := astSpec.(*ast.TypeSpec); ok { + typeSpecDef := &TypeSpecDef{ + PkgPath: packagePath, + File: astFile, + TypeSpec: typeSpec, + ParentSpec: astDeclaration, + } + + if idt, ok := typeSpec.Type.(*ast.Ident); ok && IsGolangPrimitiveType(idt.Name) && parsedSchemas != nil { + parsedSchemas[typeSpecDef] = &Schema{ + PkgPath: typeSpecDef.PkgPath, + Name: astFile.Name.Name, + Schema: PrimitiveSchema(TransToValidSchemeType(idt.Name)), + } + } + + if pkgDefs.uniqueDefinitions == nil { + pkgDefs.uniqueDefinitions = make(map[string]*TypeSpecDef) + } + + fullName := typeSpecDef.TypeName() + + anotherTypeDef, ok := pkgDefs.uniqueDefinitions[fullName] + if ok { + if anotherTypeDef == nil { + typeSpecDef.NotUnique = true + fullName = typeSpecDef.TypeName() + pkgDefs.uniqueDefinitions[fullName] = typeSpecDef + } else if typeSpecDef.PkgPath != anotherTypeDef.PkgPath { + pkgDefs.uniqueDefinitions[fullName] = nil + anotherTypeDef.NotUnique = true + pkgDefs.uniqueDefinitions[anotherTypeDef.TypeName()] = anotherTypeDef + typeSpecDef.NotUnique = true + fullName = typeSpecDef.TypeName() + pkgDefs.uniqueDefinitions[fullName] = typeSpecDef + } + } else { + pkgDefs.uniqueDefinitions[fullName] = typeSpecDef + } + + if pkgDefs.packages[typeSpecDef.PkgPath] == nil { + pkgDefs.packages[typeSpecDef.PkgPath] = NewPackageDefinitions(astFile.Name.Name, typeSpecDef.PkgPath).AddTypeSpec(fullName, typeSpecDef) + } else if _, ok = pkgDefs.packages[typeSpecDef.PkgPath].TypeDefinitions[fullName]; !ok { + pkgDefs.packages[typeSpecDef.PkgPath].AddTypeSpec(fullName, typeSpecDef) + } + } + } + + } + } + } + } + } +} + +func (pkgDefs *PackagesDefinitions) collectConstVariables(astFile *ast.File, packagePath string, generalDeclaration *ast.GenDecl) { + pkg, ok := pkgDefs.packages[packagePath] + if !ok { + pkg = NewPackageDefinitions(astFile.Name.Name, packagePath) + pkgDefs.packages[packagePath] = pkg + } + + var lastValueSpec *ast.ValueSpec + for _, astSpec := range generalDeclaration.Specs { + valueSpec, ok := astSpec.(*ast.ValueSpec) + if !ok { + continue + } + if len(valueSpec.Names) == 1 && len(valueSpec.Values) == 1 { + lastValueSpec = valueSpec + } else if len(valueSpec.Names) == 1 && len(valueSpec.Values) == 0 && valueSpec.Type == nil && lastValueSpec != nil { + valueSpec.Type = lastValueSpec.Type + valueSpec.Values = lastValueSpec.Values + } + pkg.AddConst(astFile, valueSpec) + } +} + +func (pkgDefs *PackagesDefinitions) evaluateAllConstVariables() { + for _, pkg := range pkgDefs.packages { + for _, constVar := range pkg.OrderedConst { + pkgDefs.EvaluateConstValue(pkg, constVar, nil) + } + } +} + +// EvaluateConstValue evaluate a const variable. +func (pkgDefs *PackagesDefinitions) EvaluateConstValue(pkg *PackageDefinitions, cv *ConstVariable, recursiveStack map[string]struct{}) (interface{}, ast.Expr) { + if expr, ok := cv.Value.(ast.Expr); ok { + defer func() { + if err := recover(); err != nil { + if fi, ok := pkgDefs.files[cv.File]; ok { + pos := fi.FileSet.Position(cv.Name.NamePos) + pkgDefs.debug.Printf("warning: failed to evaluate const %s at %s:%d:%d, %v", cv.Name.Name, fi.Path, pos.Line, pos.Column, err) + } + } + }() + if recursiveStack == nil { + recursiveStack = make(map[string]struct{}) + } + fullConstName := fullTypeName(pkg.Path, cv.Name.Name) + if _, ok = recursiveStack[fullConstName]; ok { + return nil, nil + } + recursiveStack[fullConstName] = struct{}{} + + value, evalType := pkg.evaluateConstValue(cv.File, cv.Name.Obj.Data.(int), expr, pkgDefs, recursiveStack) + if cv.Type == nil && evalType != nil { + cv.Type = evalType + } + if value != nil { + cv.Value = value + } + return value, cv.Type + } + return cv.Value, cv.Type +} + +// EvaluateConstValueByName evaluate a const variable by name. +func (pkgDefs *PackagesDefinitions) EvaluateConstValueByName(file *ast.File, pkgName, constVariableName string, recursiveStack map[string]struct{}) (interface{}, ast.Expr) { + matchedPkgPaths, externalPkgPaths := pkgDefs.findPackagePathFromImports(pkgName, file) + for _, pkgPath := range matchedPkgPaths { + if pkg, ok := pkgDefs.packages[pkgPath]; ok { + if cv, ok := pkg.ConstTable[constVariableName]; ok { + return pkgDefs.EvaluateConstValue(pkg, cv, recursiveStack) + } + } + } + if pkgDefs.parseDependency > 0 { + for _, pkgPath := range externalPkgPaths { + if err := pkgDefs.loadExternalPackage(pkgPath); err == nil { + if pkg, ok := pkgDefs.packages[pkgPath]; ok { + if cv, ok := pkg.ConstTable[constVariableName]; ok { + return pkgDefs.EvaluateConstValue(pkg, cv, recursiveStack) + } + } + } + } + } + return nil, nil +} + +func (pkgDefs *PackagesDefinitions) collectConstEnums(parsedSchemas map[*TypeSpecDef]*Schema) { + for _, pkg := range pkgDefs.packages { + for _, constVar := range pkg.OrderedConst { + if constVar.Type == nil { + continue + } + ident, ok := constVar.Type.(*ast.Ident) + if !ok || IsGolangPrimitiveType(ident.Name) { + continue + } + typeDef, ok := pkg.TypeDefinitions[ident.Name] + if !ok { + continue + } + + //delete it from parsed schemas, and will parse it again + if _, ok = parsedSchemas[typeDef]; ok { + delete(parsedSchemas, typeDef) + } + + if typeDef.Enums == nil { + typeDef.Enums = make([]EnumValue, 0) + } + + name := constVar.Name.Name + if _, ok = constVar.Value.(ast.Expr); ok { + continue + } + + enumValue := EnumValue{ + key: name, + Value: constVar.Value, + } + if constVar.Comment != nil && len(constVar.Comment.List) > 0 { + enumValue.Comment = constVar.Comment.List[0].Text + enumValue.Comment = strings.TrimPrefix(enumValue.Comment, "//") + enumValue.Comment = strings.TrimPrefix(enumValue.Comment, "/*") + enumValue.Comment = strings.TrimSuffix(enumValue.Comment, "*/") + enumValue.Comment = strings.TrimSpace(enumValue.Comment) + } + typeDef.Enums = append(typeDef.Enums, enumValue) + } + } +} + +func (pkgDefs *PackagesDefinitions) removeAllNotUniqueTypes() { + for key, ud := range pkgDefs.uniqueDefinitions { + if ud == nil { + delete(pkgDefs.uniqueDefinitions, key) + } + } +} + +func (pkgDefs *PackagesDefinitions) findTypeSpec(pkgPath string, typeName string) *TypeSpecDef { + if pkgDefs.packages == nil { + return nil + } + + pd, found := pkgDefs.packages[pkgPath] + if found { + typeSpec, ok := pd.TypeDefinitions[typeName] + if ok { + return typeSpec + } + } + + return nil +} + +func (pkgDefs *PackagesDefinitions) loadExternalPackage(importPath string) error { + cwd, err := os.Getwd() + if err != nil { + return err + } + + conf := loader.Config{ + ParserMode: goparser.ParseComments, + Cwd: cwd, + } + + conf.Import(importPath) + + loaderProgram, err := conf.Load() + if err != nil { + return err + } + + for _, info := range loaderProgram.AllPackages { + pkgPath := strings.TrimPrefix(info.Pkg.Path(), "vendor/") + for _, astFile := range info.Files { + pkgDefs.parseTypesFromFile(astFile, pkgPath, nil) + } + } + + return nil +} + +// findPackagePathFromImports finds out the package path of a package via ranging imports of an ast.File +// @pkg the name of the target package +// @file current ast.File in which to search imports +// @return the package paths of a package of @pkg. +func (pkgDefs *PackagesDefinitions) findPackagePathFromImports(pkg string, file *ast.File) (matchedPkgPaths, externalPkgPaths []string) { + if file == nil { + return + } + + if strings.ContainsRune(pkg, '.') { + pkg = strings.Split(pkg, ".")[0] + } + + matchLastPathPart := func(pkgPath string) bool { + paths := strings.Split(pkgPath, "/") + return paths[len(paths)-1] == pkg + } + + // prior to match named package + for _, imp := range file.Imports { + path := strings.Trim(imp.Path.Value, `"`) + if imp.Name != nil { + if imp.Name.Name == pkg { + // if name match, break loop and return + _, ok := pkgDefs.packages[path] + if ok { + matchedPkgPaths = []string{path} + externalPkgPaths = nil + } else { + externalPkgPaths = []string{path} + matchedPkgPaths = nil + } + break + } else if imp.Name.Name == "_" && len(pkg) > 0 { + //for unused types + pd, ok := pkgDefs.packages[path] + if ok { + if pd.Name == pkg { + matchedPkgPaths = append(matchedPkgPaths, path) + } + } else if matchLastPathPart(path) { + externalPkgPaths = append(externalPkgPaths, path) + } + } else if imp.Name.Name == "." && len(pkg) == 0 { + _, ok := pkgDefs.packages[path] + if ok { + matchedPkgPaths = append(matchedPkgPaths, path) + } else if len(pkg) == 0 || matchLastPathPart(path) { + externalPkgPaths = append(externalPkgPaths, path) + } + } + } else if pkgDefs.packages != nil && len(pkg) > 0 { + pd, ok := pkgDefs.packages[path] + if ok { + if pd.Name == pkg { + matchedPkgPaths = append(matchedPkgPaths, path) + } + } else if matchLastPathPart(path) { + externalPkgPaths = append(externalPkgPaths, path) + } + } + } + + if len(pkg) == 0 || file.Name.Name == pkg { + matchedPkgPaths = append(matchedPkgPaths, pkgDefs.files[file].PackagePath) + } + + return +} + +func (pkgDefs *PackagesDefinitions) findTypeSpecFromPackagePaths(matchedPkgPaths, externalPkgPaths []string, name string) (typeDef *TypeSpecDef) { + if pkgDefs.parseDependency > 0 { + for _, pkgPath := range externalPkgPaths { + if err := pkgDefs.loadExternalPackage(pkgPath); err == nil { + typeDef = pkgDefs.findTypeSpec(pkgPath, name) + if typeDef != nil { + return typeDef + } + } + } + } + + for _, pkgPath := range matchedPkgPaths { + typeDef = pkgDefs.findTypeSpec(pkgPath, name) + if typeDef != nil { + return typeDef + } + } + + return typeDef +} + +// FindTypeSpec finds out TypeSpecDef of a type by typeName +// @typeName the name of the target type, if it starts with a package name, find its own package path from imports on top of @file +// @file the ast.file in which @typeName is used +// @pkgPath the package path of @file. +func (pkgDefs *PackagesDefinitions) FindTypeSpec(typeName string, file *ast.File) *TypeSpecDef { + if IsGolangPrimitiveType(typeName) { + return nil + } + + if file == nil { // for test + return pkgDefs.uniqueDefinitions[typeName] + } + + parts := strings.Split(strings.Split(typeName, "[")[0], ".") + if len(parts) > 1 { + pkgPaths, externalPkgPaths := pkgDefs.findPackagePathFromImports(parts[0], file) + if len(externalPkgPaths) == 0 || pkgDefs.parseDependency == ParseNone { + typeDef, ok := pkgDefs.uniqueDefinitions[typeName] + if ok { + return typeDef + } + } + typeDef := pkgDefs.findTypeSpecFromPackagePaths(pkgPaths, externalPkgPaths, parts[1]) + return pkgDefs.parametrizeGenericType(file, typeDef, typeName) + } + + typeDef, ok := pkgDefs.uniqueDefinitions[fullTypeName(file.Name.Name, typeName)] + if ok { + return typeDef + } + + //in case that comment //@name renamed the type with a name without a dot + typeDef, ok = pkgDefs.uniqueDefinitions[typeName] + if ok { + return typeDef + } + + name := parts[0] + typeDef, ok = pkgDefs.uniqueDefinitions[fullTypeName(file.Name.Name, name)] + if !ok { + pkgPaths, externalPkgPaths := pkgDefs.findPackagePathFromImports("", file) + typeDef = pkgDefs.findTypeSpecFromPackagePaths(pkgPaths, externalPkgPaths, name) + } + return pkgDefs.parametrizeGenericType(file, typeDef, typeName) +} diff --git a/vendor/github.com/swaggo/swag/parser.go b/vendor/github.com/swaggo/swag/parser.go index b4871f4331..604f8278e9 100644 --- a/vendor/github.com/swaggo/swag/parser.go +++ b/vendor/github.com/swaggo/swag/parser.go @@ -1,13 +1,15 @@ package swag import ( + "context" "encoding/json" + "errors" "fmt" "go/ast" "go/build" goparser "go/parser" "go/token" - "io/ioutil" + "log" "net/http" "os" "os/exec" @@ -16,10 +18,8 @@ import ( "sort" "strconv" "strings" - "unicode" "github.com/KyleBanks/depth" - "github.com/go-openapi/jsonreference" "github.com/go-openapi/spec" ) @@ -32,49 +32,173 @@ const ( // SnakeCase indicates using SnakeCase strategy for struct field. SnakeCase = "snakecase" + + idAttr = "@id" + acceptAttr = "@accept" + produceAttr = "@produce" + paramAttr = "@param" + successAttr = "@success" + failureAttr = "@failure" + responseAttr = "@response" + headerAttr = "@header" + tagsAttr = "@tags" + routerAttr = "@router" + deprecatedRouterAttr = "@deprecatedrouter" + summaryAttr = "@summary" + deprecatedAttr = "@deprecated" + securityAttr = "@security" + titleAttr = "@title" + conNameAttr = "@contact.name" + conURLAttr = "@contact.url" + conEmailAttr = "@contact.email" + licNameAttr = "@license.name" + licURLAttr = "@license.url" + versionAttr = "@version" + descriptionAttr = "@description" + descriptionMarkdownAttr = "@description.markdown" + secBasicAttr = "@securitydefinitions.basic" + secAPIKeyAttr = "@securitydefinitions.apikey" + secApplicationAttr = "@securitydefinitions.oauth2.application" + secImplicitAttr = "@securitydefinitions.oauth2.implicit" + secPasswordAttr = "@securitydefinitions.oauth2.password" + secAccessCodeAttr = "@securitydefinitions.oauth2.accesscode" + tosAttr = "@termsofservice" + extDocsDescAttr = "@externaldocs.description" + extDocsURLAttr = "@externaldocs.url" + xCodeSamplesAttr = "@x-codesamples" + scopeAttrPrefix = "@scope." + stateAttr = "@state" +) + +// ParseFlag determine what to parse +type ParseFlag int + +const ( + // ParseNone parse nothing + ParseNone ParseFlag = 0x00 + // ParseModels parse models + ParseModels = 0x01 + // ParseOperations parse operations + ParseOperations = 0x02 + // ParseAll parse operations and models + ParseAll = ParseOperations | ParseModels ) +var ( + // ErrRecursiveParseStruct recursively parsing struct. + ErrRecursiveParseStruct = errors.New("recursively parsing struct") + + // ErrFuncTypeField field type is func. + ErrFuncTypeField = errors.New("field type is func") + + // ErrFailedConvertPrimitiveType Failed to convert for swag to interpretable type. + ErrFailedConvertPrimitiveType = errors.New("swag property: failed convert primitive type") + + // ErrSkippedField .swaggo specifies field should be skipped. + ErrSkippedField = errors.New("field is skipped by global overrides") +) + +var allMethod = map[string]struct{}{ + http.MethodGet: {}, + http.MethodPut: {}, + http.MethodPost: {}, + http.MethodDelete: {}, + http.MethodOptions: {}, + http.MethodHead: {}, + http.MethodPatch: {}, +} + // Parser implements a parser for Go source files. type Parser struct { // swagger represents the root document object for the API specification swagger *spec.Swagger - // files is a map that stores map[real_go_file_path][astFile] - files map[string]*ast.File + // packages store entities of APIs, definitions, file, package path etc. and their relations + packages *PackagesDefinitions - // TypeDefinitions is a map that stores [package name][type name][*ast.TypeSpec] - TypeDefinitions map[string]map[string]*ast.TypeSpec + // parsedSchemas store schemas which have been parsed from ast.TypeSpec + parsedSchemas map[*TypeSpecDef]*Schema - // ImportAliases is map that stores [import name][import package name][*ast.ImportSpec] - ImportAliases map[string]map[string]*ast.ImportSpec - - // CustomPrimitiveTypes is a map that stores custom primitive types to actual golang types [type name][string] - CustomPrimitiveTypes map[string]string - - // registerTypes is a map that stores [refTypeName][*ast.TypeSpec] - registerTypes map[string]*ast.TypeSpec + // outputSchemas store schemas which will be export to swagger + outputSchemas map[*TypeSpecDef]*Schema + // PropNamingStrategy naming strategy PropNamingStrategy string + // ParseVendor parse vendor folder ParseVendor bool - // ParseDependencies whether swag should be parse outside dependency folder - ParseDependency bool + // ParseDependencies whether swag should be parse outside dependency folder: 0 none, 1 models, 2 operations, 3 all + ParseDependency ParseFlag // ParseInternal whether swag should parse internal packages ParseInternal bool + // Strict whether swag should error or warn when it detects cases which are most likely user errors + Strict bool + + // RequiredByDefault set validation required for all fields by default + RequiredByDefault bool + // structStack stores full names of the structures that were already parsed or are being parsed now - structStack []string + structStack []*TypeSpecDef // markdownFileDir holds the path to the folder, where markdown files are stored markdownFileDir string + // codeExampleFilesDir holds path to the folder, where code example files are stored + codeExampleFilesDir string + // collectionFormatInQuery set the default collectionFormat otherwise then 'csv' for array in query params collectionFormatInQuery string // excludes excludes dirs and files in SearchDir - excludes map[string]bool + excludes map[string]struct{} + + // packagePrefix is a list of package path prefixes, packages that do not + // match any one of them will be excluded when searching. + packagePrefix []string + + // tells parser to include only specific extension + parseExtension string + + // debugging output goes here + debug Debugger + + // fieldParserFactory create FieldParser + fieldParserFactory FieldParserFactory + + // Overrides allows global replacements of types. A blank replacement will be skipped. + Overrides map[string]string + + // parseGoList whether swag use go list to parse dependency + parseGoList bool + + // tags to filter the APIs after + tags map[string]struct{} + + // HostState is the state of the host + HostState string +} + +// FieldParserFactory create FieldParser. +type FieldParserFactory func(ps *Parser, field *ast.Field) FieldParser + +// FieldParser parse struct field. +type FieldParser interface { + ShouldSkip() bool + FieldName() (string, error) + FormName() string + HeaderName() string + PathName() string + CustomSchema() (*spec.Schema, error) + ComplementSchema(schema *spec.Schema) error + IsRequired() (bool, error) +} + +// Debugger is the interface that wraps the basic Printf method. +type Debugger interface { + Printf(format string, v ...interface{}) } // New creates a new Parser with default properties. @@ -85,102 +209,262 @@ func New(options ...func(*Parser)) *Parser { Info: &spec.Info{ InfoProps: spec.InfoProps{ Contact: &spec.ContactInfo{}, - License: &spec.License{}, + License: nil, + }, + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{}, }, }, Paths: &spec.Paths{ Paths: make(map[string]spec.PathItem), + VendorExtensible: spec.VendorExtensible{ + Extensions: nil, + }, }, - Definitions: make(map[string]spec.Schema), + Definitions: make(map[string]spec.Schema), + SecurityDefinitions: make(map[string]*spec.SecurityScheme), + }, + VendorExtensible: spec.VendorExtensible{ + Extensions: nil, }, }, - files: make(map[string]*ast.File), - TypeDefinitions: make(map[string]map[string]*ast.TypeSpec), - ImportAliases: make(map[string]map[string]*ast.ImportSpec), - CustomPrimitiveTypes: make(map[string]string), - registerTypes: make(map[string]*ast.TypeSpec), - excludes: make(map[string]bool), + packages: NewPackagesDefinitions(), + debug: log.New(os.Stdout, "", log.LstdFlags), + parsedSchemas: make(map[*TypeSpecDef]*Schema), + outputSchemas: make(map[*TypeSpecDef]*Schema), + excludes: make(map[string]struct{}), + tags: make(map[string]struct{}), + fieldParserFactory: newTagBaseFieldParser, + Overrides: make(map[string]string), } for _, option := range options { option(parser) } + parser.packages.debug = parser.debug + return parser } -// SetMarkdownFileDirectory sets the directory to search for markdownfiles +// SetParseDependency sets whether to parse the dependent packages. +func SetParseDependency(parseDependency int) func(*Parser) { + return func(p *Parser) { + p.ParseDependency = ParseFlag(parseDependency) + if p.packages != nil { + p.packages.parseDependency = p.ParseDependency + } + } +} + +// SetMarkdownFileDirectory sets the directory to search for markdown files. func SetMarkdownFileDirectory(directoryPath string) func(*Parser) { return func(p *Parser) { p.markdownFileDir = directoryPath } } -// SetExcludedDirsAndFiles sets directories and files to be excluded when searching +// SetCodeExamplesDirectory sets the directory to search for code example files. +func SetCodeExamplesDirectory(directoryPath string) func(*Parser) { + return func(p *Parser) { + p.codeExampleFilesDir = directoryPath + } +} + +// SetExcludedDirsAndFiles sets directories and files to be excluded when searching. func SetExcludedDirsAndFiles(excludes string) func(*Parser) { return func(p *Parser) { for _, f := range strings.Split(excludes, ",") { f = strings.TrimSpace(f) if f != "" { f = filepath.Clean(f) - p.excludes[f] = true + p.excludes[f] = struct{}{} } } } } -// ParseAPI parses general api info for given searchDir and mainAPIFile -func (parser *Parser) ParseAPI(searchDir string, mainAPIFile string) error { - Printf("Generate general API Info, search dir:%s", searchDir) +// SetPackagePrefix sets a list of package path prefixes from a comma-separated +// string, packages that do not match any one of them will be excluded when +// searching. +func SetPackagePrefix(packagePrefix string) func(*Parser) { + return func(p *Parser) { + for _, f := range strings.Split(packagePrefix, ",") { + f = strings.TrimSpace(f) + if f != "" { + p.packagePrefix = append(p.packagePrefix, f) + } + } + } +} - if err := parser.getAllGoFileInfo(searchDir); err != nil { - return err +// SetTags sets the tags to be included +func SetTags(include string) func(*Parser) { + return func(p *Parser) { + for _, f := range strings.Split(include, ",") { + f = strings.TrimSpace(f) + if f != "" { + p.tags[f] = struct{}{} + } + } } +} - var t depth.Tree - t.ResolveInternal = true +// SetParseExtension parses only those operations which match given extension +func SetParseExtension(parseExtension string) func(*Parser) { + return func(p *Parser) { + p.parseExtension = parseExtension + } +} - absMainAPIFilePath, err := filepath.Abs(filepath.Join(searchDir, mainAPIFile)) - if err != nil { - return err +// SetStrict sets whether swag should error or warn when it detects cases which are most likely user errors. +func SetStrict(strict bool) func(*Parser) { + return func(p *Parser) { + p.Strict = strict + } +} + +// SetDebugger allows the use of user-defined implementations. +func SetDebugger(logger Debugger) func(parser *Parser) { + return func(p *Parser) { + if logger != nil { + p.debug = logger + } + } +} + +// SetFieldParserFactory allows the use of user-defined implementations. +func SetFieldParserFactory(factory FieldParserFactory) func(parser *Parser) { + return func(p *Parser) { + p.fieldParserFactory = factory + } +} + +// SetOverrides allows the use of user-defined global type overrides. +func SetOverrides(overrides map[string]string) func(parser *Parser) { + return func(p *Parser) { + for k, v := range overrides { + p.Overrides[k] = v + } + } +} + +// SetCollectionFormat set default collection format +func SetCollectionFormat(collectionFormat string) func(*Parser) { + return func(p *Parser) { + p.collectionFormatInQuery = collectionFormat } +} - if parser.ParseDependency { - pkgName, err := getPkgName(filepath.Dir(absMainAPIFilePath)) +// ParseUsingGoList sets whether swag use go list to parse dependency +func ParseUsingGoList(enabled bool) func(parser *Parser) { + return func(p *Parser) { + p.parseGoList = enabled + } +} + +// ParseAPI parses general api info for given searchDir and mainAPIFile. +func (parser *Parser) ParseAPI(searchDir string, mainAPIFile string, parseDepth int) error { + return parser.ParseAPIMultiSearchDir([]string{searchDir}, mainAPIFile, parseDepth) +} + +// skipPackageByPrefix returns true the given pkgpath does not match +// any user-defined package path prefixes. +func (parser *Parser) skipPackageByPrefix(pkgpath string) bool { + if len(parser.packagePrefix) == 0 { + return false + } + for _, prefix := range parser.packagePrefix { + if strings.HasPrefix(pkgpath, prefix) { + return false + } + } + return true +} + +// ParseAPIMultiSearchDir is like ParseAPI but for multiple search dirs. +func (parser *Parser) ParseAPIMultiSearchDir(searchDirs []string, mainAPIFile string, parseDepth int) error { + for _, searchDir := range searchDirs { + parser.debug.Printf("Generate general API Info, search dir:%s", searchDir) + + packageDir, err := getPkgName(searchDir) if err != nil { - return err + parser.debug.Printf("warning: failed to get package name in dir: %s, error: %s", searchDir, err.Error()) } - if err := t.Resolve(pkgName); err != nil { - return fmt.Errorf("pkg %s cannot find all dependencies, %s", pkgName, err) + + err = parser.getAllGoFileInfo(packageDir, searchDir) + if err != nil { + return err } - for i := 0; i < len(t.Root.Deps); i++ { - if err := parser.getAllGoFileInfoFromDeps(&t.Root.Deps[i]); err != nil { + } + + absMainAPIFilePath, err := filepath.Abs(filepath.Join(searchDirs[0], mainAPIFile)) + if err != nil { + return err + } + + // Use 'go list' command instead of depth.Resolve() + if parser.ParseDependency > 0 { + if parser.parseGoList { + pkgs, err := listPackages(context.Background(), filepath.Dir(absMainAPIFilePath), nil, "-deps") + if err != nil { + return fmt.Errorf("pkg %s cannot find all dependencies, %s", filepath.Dir(absMainAPIFilePath), err) + } + + length := len(pkgs) + for i := 0; i < length; i++ { + err := parser.getAllGoFileInfoFromDepsByList(pkgs[i], parser.ParseDependency) + if err != nil { + return err + } + } + } else { + var t depth.Tree + t.ResolveInternal = true + t.MaxDepth = parseDepth + + pkgName, err := getPkgName(filepath.Dir(absMainAPIFilePath)) + if err != nil { return err } + + err = t.Resolve(pkgName) + if err != nil { + return fmt.Errorf("pkg %s cannot find all dependencies, %s", pkgName, err) + } + for i := 0; i < len(t.Root.Deps); i++ { + err := parser.getAllGoFileInfoFromDeps(&t.Root.Deps[i], parser.ParseDependency) + if err != nil { + return err + } + } } } - if err := parser.ParseGeneralAPIInfo(absMainAPIFilePath); err != nil { + err = parser.ParseGeneralAPIInfo(absMainAPIFilePath) + if err != nil { return err } - for _, astFile := range parser.files { - parser.ParseType(astFile) + parser.parsedSchemas, err = parser.packages.ParseTypes() + if err != nil { + return err } - for fileName, astFile := range parser.files { - if err := parser.ParseRouterAPIInfo(fileName, astFile); err != nil { - return err - } + err = parser.packages.RangeFiles(parser.ParseRouterAPIInfo) + if err != nil { + return err } - return parser.parseDefinitions() + return parser.checkOperationIDUniqueness() } func getPkgName(searchDir string) (string, error) { cmd := exec.Command("go", "list", "-f={{.ImportPath}}") cmd.Dir = searchDir + var stdout, stderr strings.Builder + cmd.Stdout = &stdout cmd.Stderr = &stderr @@ -193,253 +477,421 @@ func getPkgName(searchDir string) (string, error) { if outStr[0] == '_' { // will shown like _/{GOPATH}/src/{YOUR_PACKAGE} when NOT enable GO MODULE. outStr = strings.TrimPrefix(outStr, "_"+build.Default.GOPATH+"/src/") } + f := strings.Split(outStr, "\n") + outStr = f[0] return outStr, nil } -// ParseGeneralAPIInfo parses general api info for given mainAPIFile path +// ParseGeneralAPIInfo parses general api info for given mainAPIFile path. func (parser *Parser) ParseGeneralAPIInfo(mainAPIFile string) error { - fileSet := token.NewFileSet() - fileTree, err := goparser.ParseFile(fileSet, mainAPIFile, nil, goparser.ParseComments) + fileTree, err := goparser.ParseFile(token.NewFileSet(), mainAPIFile, nil, goparser.ParseComments) if err != nil { return fmt.Errorf("cannot parse source files %s: %s", mainAPIFile, err) } parser.swagger.Swagger = "2.0" - securityMap := map[string]*spec.SecurityScheme{} for _, comment := range fileTree.Comments { - if !isGeneralAPIComment(comment) { + comments := strings.Split(comment.Text(), "\n") + if !isGeneralAPIComment(comments) { continue } - comments := strings.Split(comment.Text(), "\n") - previousAttribute := "" - // parsing classic meta data model - for i, commentLine := range comments { - attribute := strings.ToLower(strings.Split(commentLine, " ")[0]) - value := strings.TrimSpace(commentLine[len(attribute):]) - multilineBlock := false + + err = parseGeneralAPIInfo(parser, comments) + if err != nil { + return err + } + } + + return nil +} + +func parseGeneralAPIInfo(parser *Parser, comments []string) error { + previousAttribute := "" + + // parsing classic meta data model + for line := 0; line < len(comments); line++ { + commentLine := comments[line] + commentLine = strings.TrimSpace(commentLine) + if len(commentLine) == 0 { + continue + } + fields := FieldsByAnySpace(commentLine, 2) + + attribute := fields[0] + var value string + if len(fields) > 1 { + value = fields[1] + } + + switch attr := strings.ToLower(attribute); attr { + case versionAttr, titleAttr, tosAttr, licNameAttr, licURLAttr, conNameAttr, conURLAttr, conEmailAttr: + setSwaggerInfo(parser.swagger, attr, value) + case descriptionAttr: if previousAttribute == attribute { - multilineBlock = true + parser.swagger.Info.Description += "\n" + value + + continue } - switch attribute { - case "@version": - parser.swagger.Info.Version = value - case "@title": - parser.swagger.Info.Title = value - case "@description": - if multilineBlock { - parser.swagger.Info.Description += "\n" + value - continue - } - parser.swagger.Info.Description = value - case "@description.markdown": - commentInfo, err := getMarkdownForTag("api", parser.markdownFileDir) - if err != nil { - return err - } - parser.swagger.Info.Description = string(commentInfo) - case "@termsofservice": - parser.swagger.Info.TermsOfService = value - case "@contact.name": - parser.swagger.Info.Contact.Name = value - case "@contact.email": - parser.swagger.Info.Contact.Email = value - case "@contact.url": - parser.swagger.Info.Contact.URL = value - case "@license.name": - parser.swagger.Info.License.Name = value - case "@license.url": - parser.swagger.Info.License.URL = value - case "@host": - parser.swagger.Host = value - case "@basepath": - parser.swagger.BasePath = value - case "@schemes": - parser.swagger.Schemes = getSchemes(commentLine) - case "@tag.name": - parser.swagger.Tags = append(parser.swagger.Tags, spec.Tag{ - TagProps: spec.TagProps{ - Name: value, - }, - }) - case "@tag.description": - tag := parser.swagger.Tags[len(parser.swagger.Tags)-1] - tag.TagProps.Description = value - replaceLastTag(parser.swagger.Tags, tag) - case "@tag.description.markdown": - tag := parser.swagger.Tags[len(parser.swagger.Tags)-1] - commentInfo, err := getMarkdownForTag(tag.TagProps.Name, parser.markdownFileDir) - if err != nil { - return err - } - tag.TagProps.Description = string(commentInfo) - replaceLastTag(parser.swagger.Tags, tag) - case "@tag.docs.url": - tag := parser.swagger.Tags[len(parser.swagger.Tags)-1] - tag.TagProps.ExternalDocs = &spec.ExternalDocumentation{ - URL: value, - } - replaceLastTag(parser.swagger.Tags, tag) - case "@tag.docs.description": - tag := parser.swagger.Tags[len(parser.swagger.Tags)-1] - if tag.TagProps.ExternalDocs == nil { - return fmt.Errorf("%s needs to come after a @tags.docs.url", attribute) - } - tag.TagProps.ExternalDocs.Description = value - replaceLastTag(parser.swagger.Tags, tag) - case "@securitydefinitions.basic": - securityMap[value] = spec.BasicAuth() - case "@securitydefinitions.apikey": - attrMap, _, err := extractSecurityAttribute(attribute, []string{"@in", "@name"}, comments[i+1:]) - if err != nil { - return err - } - securityMap[value] = spec.APIKeyAuth(attrMap["@name"], attrMap["@in"]) - case "@securitydefinitions.oauth2.application": - attrMap, scopes, err := extractSecurityAttribute(attribute, []string{"@tokenurl"}, comments[i+1:]) - if err != nil { - return err + + setSwaggerInfo(parser.swagger, attr, value) + case descriptionMarkdownAttr: + commentInfo, err := getMarkdownForTag("api", parser.markdownFileDir) + if err != nil { + return err + } + + setSwaggerInfo(parser.swagger, descriptionAttr, string(commentInfo)) + + case "@host": + parser.swagger.Host = value + case "@hoststate": + fields = FieldsByAnySpace(commentLine, 3) + if len(fields) != 3 { + return fmt.Errorf("%s needs 3 arguments", attribute) + } + if parser.HostState == fields[1] { + parser.swagger.Host = fields[2] + } + case "@basepath": + parser.swagger.BasePath = value + + case acceptAttr: + err := parser.ParseAcceptComment(value) + if err != nil { + return err + } + case produceAttr: + err := parser.ParseProduceComment(value) + if err != nil { + return err + } + case "@schemes": + parser.swagger.Schemes = strings.Split(value, " ") + case "@tag.name": + parser.swagger.Tags = append(parser.swagger.Tags, spec.Tag{ + TagProps: spec.TagProps{ + Name: value, + }, + }) + case "@tag.description": + tag := parser.swagger.Tags[len(parser.swagger.Tags)-1] + tag.TagProps.Description = value + replaceLastTag(parser.swagger.Tags, tag) + case "@tag.description.markdown": + tag := parser.swagger.Tags[len(parser.swagger.Tags)-1] + + commentInfo, err := getMarkdownForTag(tag.TagProps.Name, parser.markdownFileDir) + if err != nil { + return err + } + + tag.TagProps.Description = string(commentInfo) + replaceLastTag(parser.swagger.Tags, tag) + case "@tag.docs.url": + tag := parser.swagger.Tags[len(parser.swagger.Tags)-1] + tag.TagProps.ExternalDocs = &spec.ExternalDocumentation{ + URL: value, + Description: "", + } + + replaceLastTag(parser.swagger.Tags, tag) + case "@tag.docs.description": + tag := parser.swagger.Tags[len(parser.swagger.Tags)-1] + if tag.TagProps.ExternalDocs == nil { + return fmt.Errorf("%s needs to come after a @tags.docs.url", attribute) + } + + tag.TagProps.ExternalDocs.Description = value + replaceLastTag(parser.swagger.Tags, tag) + + case secBasicAttr, secAPIKeyAttr, secApplicationAttr, secImplicitAttr, secPasswordAttr, secAccessCodeAttr: + scheme, err := parseSecAttributes(attribute, comments, &line) + if err != nil { + return err + } + + parser.swagger.SecurityDefinitions[value] = scheme + + case securityAttr: + parser.swagger.Security = append(parser.swagger.Security, parseSecurity(value)) + + case "@query.collection.format": + parser.collectionFormatInQuery = TransToValidCollectionFormat(value) + + case extDocsDescAttr, extDocsURLAttr: + if parser.swagger.ExternalDocs == nil { + parser.swagger.ExternalDocs = new(spec.ExternalDocumentation) + } + switch attr { + case extDocsDescAttr: + parser.swagger.ExternalDocs.Description = value + case extDocsURLAttr: + parser.swagger.ExternalDocs.URL = value + } + + default: + if strings.HasPrefix(attribute, "@x-") { + extensionName := attribute[1:] + + extExistsInSecurityDef := false + // for each security definition + for _, v := range parser.swagger.SecurityDefinitions { + // check if extension exists + _, extExistsInSecurityDef = v.VendorExtensible.Extensions.GetString(extensionName) + // if it exists in at least one, then we stop iterating + if extExistsInSecurityDef { + break + } } - securityMap[value] = securitySchemeOAuth2Application(attrMap["@tokenurl"], scopes) - case "@securitydefinitions.oauth2.implicit": - attrMap, scopes, err := extractSecurityAttribute(attribute, []string{"@authorizationurl"}, comments[i+1:]) - if err != nil { - return err + + // if it is present on security def, don't add it again + if extExistsInSecurityDef { + break } - securityMap[value] = securitySchemeOAuth2Implicit(attrMap["@authorizationurl"], scopes) - case "@securitydefinitions.oauth2.password": - attrMap, scopes, err := extractSecurityAttribute(attribute, []string{"@tokenurl"}, comments[i+1:]) - if err != nil { - return err + + if len(value) == 0 { + return fmt.Errorf("annotation %s need a value", attribute) } - securityMap[value] = securitySchemeOAuth2Password(attrMap["@tokenurl"], scopes) - case "@securitydefinitions.oauth2.accesscode": - attrMap, scopes, err := extractSecurityAttribute(attribute, []string{"@tokenurl", "@authorizationurl"}, comments[i+1:]) + + var valueJSON interface{} + err := json.Unmarshal([]byte(value), &valueJSON) if err != nil { - return err + return fmt.Errorf("annotation %s need a valid json value", attribute) } - securityMap[value] = securitySchemeOAuth2AccessToken(attrMap["@authorizationurl"], attrMap["@tokenurl"], scopes) - case "@query.collection.format": - parser.collectionFormatInQuery = value - default: - prefixExtension := "@x-" - if len(attribute) > 5 { // Prefix extension + 1 char + 1 space + 1 char - if attribute[:len(prefixExtension)] == prefixExtension { - var valueJSON interface{} - split := strings.SplitAfter(commentLine, attribute+" ") - if len(split) < 2 { - return fmt.Errorf("annotation %s need a value", attribute) - } - extensionName := "x-" + strings.SplitAfter(attribute, prefixExtension)[1] - if err := json.Unmarshal([]byte(split[1]), &valueJSON); err != nil { - return fmt.Errorf("annotation %s need a valid json value", attribute) - } - parser.swagger.AddExtension(extensionName, valueJSON) + + if strings.Contains(extensionName, "logo") { + parser.swagger.Info.Extensions.Add(extensionName, valueJSON) + } else { + if parser.swagger.Extensions == nil { + parser.swagger.Extensions = make(map[string]interface{}) } + + parser.swagger.Extensions[attribute[1:]] = valueJSON } } - previousAttribute = attribute } - } - if len(securityMap) > 0 { - parser.swagger.SecurityDefinitions = securityMap + previousAttribute = attribute } return nil } -func isGeneralAPIComment(comment *ast.CommentGroup) bool { - for _, commentLine := range strings.Split(comment.Text(), "\n") { - attribute := strings.ToLower(strings.Split(commentLine, " ")[0]) - switch attribute { - // The @summary, @router, @success,@failure annotation belongs to Operation - case "@summary", "@router", "@success", "@failure": - return false - } +func setSwaggerInfo(swagger *spec.Swagger, attribute, value string) { + switch attribute { + case versionAttr: + swagger.Info.Version = value + case titleAttr: + swagger.Info.Title = value + case tosAttr: + swagger.Info.TermsOfService = value + case descriptionAttr: + swagger.Info.Description = value + case conNameAttr: + swagger.Info.Contact.Name = value + case conEmailAttr: + swagger.Info.Contact.Email = value + case conURLAttr: + swagger.Info.Contact.URL = value + case licNameAttr: + swagger.Info.License = initIfEmpty(swagger.Info.License) + swagger.Info.License.Name = value + case licURLAttr: + swagger.Info.License = initIfEmpty(swagger.Info.License) + swagger.Info.License.URL = value } - return true } -func extractSecurityAttribute(context string, search []string, lines []string) (map[string]string, map[string]string, error) { - attrMap := map[string]string{} - scopes := map[string]string{} - for _, v := range lines { - securityAttr := strings.ToLower(strings.Split(v, " ")[0]) +func parseSecAttributes(context string, lines []string, index *int) (*spec.SecurityScheme, error) { + const ( + in = "@in" + name = "@name" + descriptionAttr = "@description" + tokenURL = "@tokenurl" + authorizationURL = "@authorizationurl" + ) + + var search []string + + attribute := strings.ToLower(FieldsByAnySpace(lines[*index], 2)[0]) + switch attribute { + case secBasicAttr: + return spec.BasicAuth(), nil + case secAPIKeyAttr: + search = []string{in, name} + case secApplicationAttr, secPasswordAttr: + search = []string{tokenURL} + case secImplicitAttr: + search = []string{authorizationURL} + case secAccessCodeAttr: + search = []string{tokenURL, authorizationURL} + } + + // For the first line we get the attributes in the context parameter, so we skip to the next one + *index++ + + attrMap, scopes := make(map[string]string), make(map[string]string) + extensions, description := make(map[string]interface{}), "" + +loopline: + for ; *index < len(lines); *index++ { + v := strings.TrimSpace(lines[*index]) + if len(v) == 0 { + continue + } + + fields := FieldsByAnySpace(v, 2) + securityAttr := strings.ToLower(fields[0]) + var value string + if len(fields) > 1 { + value = fields[1] + } + for _, findterm := range search { if securityAttr == findterm { - attrMap[securityAttr] = strings.TrimSpace(v[len(securityAttr):]) - continue + attrMap[securityAttr] = value + continue loopline } } - isExists, err := isExistsScope(securityAttr) - if err != nil { - return nil, nil, err + + if isExists, err := isExistsScope(securityAttr); err != nil { + return nil, err + } else if isExists { + scopes[securityAttr[len(scopeAttrPrefix):]] = value + continue } - if isExists { - scopScheme, err := getScopeScheme(securityAttr) - if err != nil { - return nil, nil, err - } - scopes[scopScheme] = v[len(securityAttr):] + + if strings.HasPrefix(securityAttr, "@x-") { + // Add the custom attribute without the @ + extensions[securityAttr[1:]] = value + continue } + + // Not mandatory field + if securityAttr == descriptionAttr { + description = value + } + // next securityDefinitions if strings.Index(securityAttr, "@securitydefinitions.") == 0 { + // Go back to the previous line and break + *index-- + break } } + if len(attrMap) != len(search) { - return nil, nil, fmt.Errorf("%s is %v required", context, search) + return nil, fmt.Errorf("%s is %v required", context, search) } - return attrMap, scopes, nil -} -func securitySchemeOAuth2Application(tokenurl string, scopes map[string]string) *spec.SecurityScheme { - securityScheme := spec.OAuth2Application(tokenurl) - for scope, description := range scopes { - securityScheme.AddScope(scope, description) + var scheme *spec.SecurityScheme + + switch attribute { + case secAPIKeyAttr: + scheme = spec.APIKeyAuth(attrMap[name], attrMap[in]) + case secApplicationAttr: + scheme = spec.OAuth2Application(attrMap[tokenURL]) + case secImplicitAttr: + scheme = spec.OAuth2Implicit(attrMap[authorizationURL]) + case secPasswordAttr: + scheme = spec.OAuth2Password(attrMap[tokenURL]) + case secAccessCodeAttr: + scheme = spec.OAuth2AccessToken(attrMap[authorizationURL], attrMap[tokenURL]) } - return securityScheme + + scheme.Description = description + + for extKey, extValue := range extensions { + scheme.AddExtension(extKey, extValue) + } + + for scope, scopeDescription := range scopes { + scheme.AddScope(scope, scopeDescription) + } + + return scheme, nil } -func securitySchemeOAuth2Implicit(authorizationurl string, scopes map[string]string) *spec.SecurityScheme { - securityScheme := spec.OAuth2Implicit(authorizationurl) - for scope, description := range scopes { - securityScheme.AddScope(scope, description) +func parseSecurity(commentLine string) map[string][]string { + securityMap := make(map[string][]string) + + for _, securityOption := range strings.Split(commentLine, "||") { + securityOption = strings.TrimSpace(securityOption) + + left, right := strings.Index(securityOption, "["), strings.Index(securityOption, "]") + + if !(left == -1 && right == -1) { + scopes := securityOption[left+1 : right] + + var options []string + + for _, scope := range strings.Split(scopes, ",") { + options = append(options, strings.TrimSpace(scope)) + } + + securityKey := securityOption[0:left] + securityMap[securityKey] = append(securityMap[securityKey], options...) + } else { + securityKey := strings.TrimSpace(securityOption) + securityMap[securityKey] = []string{} + } } - return securityScheme + + return securityMap } -func securitySchemeOAuth2Password(tokenurl string, scopes map[string]string) *spec.SecurityScheme { - securityScheme := spec.OAuth2Password(tokenurl) - for scope, description := range scopes { - securityScheme.AddScope(scope, description) +func initIfEmpty(license *spec.License) *spec.License { + if license == nil { + return new(spec.License) } - return securityScheme + + return license +} + +// ParseAcceptComment parses comment for given `accept` comment string. +func (parser *Parser) ParseAcceptComment(commentLine string) error { + return parseMimeTypeList(commentLine, &parser.swagger.Consumes, "%v accept type can't be accepted") } -func securitySchemeOAuth2AccessToken(authorizationurl, tokenurl string, scopes map[string]string) *spec.SecurityScheme { - securityScheme := spec.OAuth2AccessToken(authorizationurl, tokenurl) - for scope, description := range scopes { - securityScheme.AddScope(scope, description) +// ParseProduceComment parses comment for given `produce` comment string. +func (parser *Parser) ParseProduceComment(commentLine string) error { + return parseMimeTypeList(commentLine, &parser.swagger.Produces, "%v produce type can't be accepted") +} + +func isGeneralAPIComment(comments []string) bool { + for _, commentLine := range comments { + commentLine = strings.TrimSpace(commentLine) + if len(commentLine) == 0 { + continue + } + attribute := strings.ToLower(FieldsByAnySpace(commentLine, 2)[0]) + switch attribute { + // The @summary, @router, @success, @failure annotation belongs to Operation + case summaryAttr, routerAttr, successAttr, failureAttr, responseAttr: + return false + } } - return securityScheme + + return true } func getMarkdownForTag(tagName string, dirPath string) ([]byte, error) { - filesInfos, err := ioutil.ReadDir(dirPath) + dirEntries, err := os.ReadDir(dirPath) if err != nil { return nil, err } - for _, fileInfo := range filesInfos { - if fileInfo.IsDir() { + for _, entry := range dirEntries { + if entry.IsDir() { continue } - fileName := fileInfo.Name() + + fileName := entry.Name() if !strings.Contains(fileName, ".md") { continue @@ -447,1038 +899,747 @@ func getMarkdownForTag(tagName string, dirPath string) ([]byte, error) { if strings.Contains(fileName, tagName) { fullPath := filepath.Join(dirPath, fileName) - commentInfo, err := ioutil.ReadFile(fullPath) + + commentInfo, err := os.ReadFile(fullPath) if err != nil { return nil, fmt.Errorf("Failed to read markdown file %s error: %s ", fullPath, err) } + return commentInfo, nil } } - return nil, fmt.Errorf("Unable to find markdown file for tag %s in the given directory", tagName) -} -func getScopeScheme(scope string) (string, error) { - scopeValue := scope[strings.Index(scope, "@scope."):] - if scopeValue == "" { - return "", fmt.Errorf("@scope is empty") - } - return scope[len("@scope."):], nil + return nil, fmt.Errorf("Unable to find markdown file for tag %s in the given directory", tagName) } func isExistsScope(scope string) (bool, error) { s := strings.Fields(scope) for _, v := range s { - if strings.Contains(v, "@scope.") { + if strings.HasPrefix(v, scopeAttrPrefix) { if strings.Contains(v, ",") { return false, fmt.Errorf("@scope can't use comma(,) get=" + v) } } } - return strings.Contains(scope, "@scope."), nil -} -// getSchemes parses swagger schemes for given commentLine -func getSchemes(commentLine string) []string { - attribute := strings.ToLower(strings.Split(commentLine, " ")[0]) - return strings.Split(strings.TrimSpace(commentLine[len(attribute):]), " ") + return strings.HasPrefix(scope, scopeAttrPrefix), nil } -// ParseRouterAPIInfo parses router api info for given astFile -func (parser *Parser) ParseRouterAPIInfo(fileName string, astFile *ast.File) error { - for _, astDescription := range astFile.Decls { - switch astDeclaration := astDescription.(type) { - case *ast.FuncDecl: - if astDeclaration.Doc != nil && astDeclaration.Doc.List != nil { - operation := NewOperation() //for per 'function' comment, create a new 'Operation' object - operation.parser = parser - for _, comment := range astDeclaration.Doc.List { - if err := operation.ParseComment(comment.Text, astFile); err != nil { - return fmt.Errorf("ParseComment error in file %s :%+v", fileName, err) - } - } - var pathItem spec.PathItem - var ok bool +func getTagsFromComment(comment string) (tags []string) { + commentLine := strings.TrimSpace(strings.TrimLeft(comment, "/")) + if len(commentLine) == 0 { + return nil + } - if pathItem, ok = parser.swagger.Paths.Paths[operation.Path]; !ok { - pathItem = spec.PathItem{} - } - switch strings.ToUpper(operation.HTTPMethod) { - case http.MethodGet: - pathItem.Get = &operation.Operation - case http.MethodPost: - pathItem.Post = &operation.Operation - case http.MethodDelete: - pathItem.Delete = &operation.Operation - case http.MethodPut: - pathItem.Put = &operation.Operation - case http.MethodPatch: - pathItem.Patch = &operation.Operation - case http.MethodHead: - pathItem.Head = &operation.Operation - case http.MethodOptions: - pathItem.Options = &operation.Operation - } + attribute := strings.Fields(commentLine)[0] + lineRemainder, lowerAttribute := strings.TrimSpace(commentLine[len(attribute):]), strings.ToLower(attribute) - parser.swagger.Paths.Paths[operation.Path] = pathItem - } + if lowerAttribute == tagsAttr { + for _, tag := range strings.Split(lineRemainder, ",") { + tags = append(tags, strings.TrimSpace(tag)) } } + return - return nil } -// ParseType parses type info for given astFile. -func (parser *Parser) ParseType(astFile *ast.File) { - if _, ok := parser.TypeDefinitions[astFile.Name.String()]; !ok { - parser.TypeDefinitions[astFile.Name.String()] = make(map[string]*ast.TypeSpec) - } - - for _, astDeclaration := range astFile.Decls { - if generalDeclaration, ok := astDeclaration.(*ast.GenDecl); ok && generalDeclaration.Tok == token.TYPE { - for _, astSpec := range generalDeclaration.Specs { - if typeSpec, ok := astSpec.(*ast.TypeSpec); ok { - typeName := fmt.Sprintf("%v", typeSpec.Type) - // check if its a custom primitive type - if IsGolangPrimitiveType(typeName) { - var typeSpecFullName = fmt.Sprintf("%s.%s", astFile.Name.String(), typeSpec.Name.String()) - parser.CustomPrimitiveTypes[typeSpecFullName] = TransToValidSchemeType(typeName) - } else { - parser.TypeDefinitions[astFile.Name.String()][typeSpec.Name.String()] = typeSpec - } +func (parser *Parser) matchTags(comments []*ast.Comment) (match bool) { + if len(parser.tags) == 0 { + return true + } - } + match = false + for _, comment := range comments { + for _, tag := range getTagsFromComment(comment.Text) { + if _, has := parser.tags["!"+tag]; has { + return false + } + if _, has := parser.tags[tag]; has { + match = true // keep iterating as it may contain a tag that is excluded } } } - for _, importSpec := range astFile.Imports { - if importSpec.Name == nil { - continue + if !match { + // If all tags are negation then we should return true + for key := range parser.tags { + if key[0] != '!' { + return false + } } + } + return true +} - alias := importSpec.Name.Name +func matchExtension(extensionToMatch string, comments []*ast.Comment) (match bool) { + if len(extensionToMatch) != 0 { + for _, comment := range comments { + commentLine := strings.TrimSpace(strings.TrimLeft(comment.Text, "/")) + fields := FieldsByAnySpace(commentLine, 2) + if len(fields) > 0 { + lowerAttribute := strings.ToLower(fields[0]) - if _, ok := parser.ImportAliases[alias]; !ok { - parser.ImportAliases[alias] = make(map[string]*ast.ImportSpec) + if lowerAttribute == fmt.Sprintf("@x-%s", strings.ToLower(extensionToMatch)) { + return true + } + } } - - importParts := strings.Split(strings.Trim(importSpec.Path.Value, "\""), "/") - importPkgName := importParts[len(importParts)-1] - - parser.ImportAliases[alias][importPkgName] = importSpec + return false } + return true } -func (parser *Parser) isInStructStack(refTypeName string) bool { - for _, structName := range parser.structStack { - if refTypeName == structName { - return true +// ParseRouterAPIInfo parses router api info for given astFile. +func (parser *Parser) ParseRouterAPIInfo(fileInfo *AstFileInfo) error { +DeclsLoop: + for _, astDescription := range fileInfo.File.Decls { + if (fileInfo.ParseFlag & ParseOperations) == ParseNone { + continue + } + astDeclaration, ok := astDescription.(*ast.FuncDecl) + if ok && astDeclaration.Doc != nil && astDeclaration.Doc.List != nil { + if parser.matchTags(astDeclaration.Doc.List) && + matchExtension(parser.parseExtension, astDeclaration.Doc.List) { + // for per 'function' comment, create a new 'Operation' object + operation := NewOperation(parser, SetCodeExampleFilesDirectory(parser.codeExampleFilesDir)) + for _, comment := range astDeclaration.Doc.List { + err := operation.ParseComment(comment.Text, fileInfo.File) + if err != nil { + return fmt.Errorf("ParseComment error in file %s :%+v", fileInfo.Path, err) + } + if operation.State != "" && operation.State != parser.HostState { + continue DeclsLoop + } + } + err := processRouterOperation(parser, operation) + if err != nil { + return err + } + } } } - return false + + return nil } -// parseDefinitions parses Swagger Api definitions. -func (parser *Parser) parseDefinitions() error { - // sort the typeNames so that parsing definitions is deterministic - typeNames := make([]string, 0, len(parser.registerTypes)) - for refTypeName := range parser.registerTypes { - typeNames = append(typeNames, refTypeName) - } - sort.Strings(typeNames) - - for _, refTypeName := range typeNames { - typeSpec := parser.registerTypes[refTypeName] - ss := strings.Split(refTypeName, ".") - pkgName := ss[0] - parser.structStack = nil - if err := parser.ParseDefinition(pkgName, typeSpec.Name.Name, typeSpec); err != nil { - return err - } +func refRouteMethodOp(item *spec.PathItem, method string) (op **spec.Operation) { + switch method { + case http.MethodGet: + op = &item.Get + case http.MethodPost: + op = &item.Post + case http.MethodDelete: + op = &item.Delete + case http.MethodPut: + op = &item.Put + case http.MethodPatch: + op = &item.Patch + case http.MethodHead: + op = &item.Head + case http.MethodOptions: + op = &item.Options } - return nil + + return } -// ParseDefinition parses given type spec that corresponds to the type under -// given name and package, and populates swagger schema definitions registry -// with a schema for the given type -func (parser *Parser) ParseDefinition(pkgName, typeName string, typeSpec *ast.TypeSpec) error { - refTypeName := TypeDocName(pkgName, typeSpec) +func processRouterOperation(parser *Parser, operation *Operation) error { + for _, routeProperties := range operation.RouterProperties { + var ( + pathItem spec.PathItem + ok bool + ) - if typeSpec == nil { - Println("Skipping '" + refTypeName + "', pkg '" + pkgName + "' not found, try add flag --parseDependency or --parseVendor.") - return nil - } + pathItem, ok = parser.swagger.Paths.Paths[routeProperties.Path] + if !ok { + pathItem = spec.PathItem{} + } - if _, isParsed := parser.swagger.Definitions[refTypeName]; isParsed { - Println("Skipping '" + refTypeName + "', already parsed.") - return nil - } + op := refRouteMethodOp(&pathItem, routeProperties.HTTPMethod) - if parser.isInStructStack(refTypeName) { - Println("Skipping '" + refTypeName + "', recursion detected.") - return nil - } - parser.structStack = append(parser.structStack, refTypeName) + // check if we already have an operation for this path and method + if *op != nil { + err := fmt.Errorf("route %s %s is declared multiple times", routeProperties.HTTPMethod, routeProperties.Path) + if parser.Strict { + return err + } - Println("Generating " + refTypeName) + parser.debug.Printf("warning: %s\n", err) + } - schema, err := parser.parseTypeExpr(pkgName, typeName, typeSpec.Type) - if err != nil { - return err + if len(operation.RouterProperties) > 1 { + newOp := *operation + var validParams []spec.Parameter + for _, param := range newOp.Operation.OperationProps.Parameters { + if param.In == "path" && !strings.Contains(routeProperties.Path, param.Name) { + // This path param is not actually contained in the path, skip adding it to the final params + continue + } + validParams = append(validParams, param) + } + newOp.Operation.OperationProps.Parameters = validParams + *op = &newOp.Operation + } else { + *op = &operation.Operation + } + + if routeProperties.Deprecated { + (*op).Deprecated = routeProperties.Deprecated + } + + parser.swagger.Paths.Paths[routeProperties.Path] = pathItem } - parser.swagger.Definitions[refTypeName] = *schema + return nil } -func (parser *Parser) collectRequiredFields(pkgName string, properties map[string]spec.Schema, extraRequired []string) (requiredFields []string) { - // created sorted list of properties keys so when we iterate over them it's deterministic - ks := make([]string, 0, len(properties)) - for k := range properties { - ks = append(ks, k) +func convertFromSpecificToPrimitive(typeName string) (string, error) { + name := typeName + if strings.ContainsRune(name, '.') { + name = strings.Split(name, ".")[1] } - sort.Strings(ks) - requiredFields = make([]string, 0) + switch strings.ToUpper(name) { + case "TIME", "OBJECTID", "UUID": + return STRING, nil + case "DECIMAL": + return NUMBER, nil + } - // iterate over keys list instead of map to avoid the random shuffle of the order that go does for maps - for _, k := range ks { - prop := properties[k] + return typeName, ErrFailedConvertPrimitiveType +} - // todo find the pkgName of the property type - tname := prop.SchemaProps.Type[0] - if _, ok := parser.TypeDefinitions[pkgName][tname]; ok { - tspec := parser.TypeDefinitions[pkgName][tname] - parser.ParseDefinition(pkgName, tname, tspec) - } - requiredFields = append(requiredFields, prop.SchemaProps.Required...) - properties[k] = prop +func (parser *Parser) getTypeSchema(typeName string, file *ast.File, ref bool) (*spec.Schema, error) { + if override, ok := parser.Overrides[typeName]; ok { + parser.debug.Printf("Override detected for %s: using %s instead", typeName, override) + return parseObjectSchema(parser, override, file) } - if extraRequired != nil { - requiredFields = append(requiredFields, extraRequired...) + if IsInterfaceLike(typeName) { + return &spec.Schema{}, nil + } + if IsGolangPrimitiveType(typeName) { + return PrimitiveSchema(TransToValidSchemeType(typeName)), nil } - sort.Strings(requiredFields) - - return -} + schemaType, err := convertFromSpecificToPrimitive(typeName) + if err == nil { + return PrimitiveSchema(schemaType), nil + } -func fullTypeName(pkgName, typeName string) string { - if pkgName != "" { - return pkgName + "." + typeName + typeSpecDef := parser.packages.FindTypeSpec(typeName, file) + if typeSpecDef == nil { + return nil, fmt.Errorf("cannot find type definition: %s", typeName) } - return typeName -} -// parseTypeExpr parses given type expression that corresponds to the type under -// given name and package, and returns swagger schema for it. -func (parser *Parser) parseTypeExpr(pkgName, typeName string, typeExpr ast.Expr) (*spec.Schema, error) { + if override, ok := parser.Overrides[typeSpecDef.FullPath()]; ok { + if override == "" { + parser.debug.Printf("Override detected for %s: ignoring", typeSpecDef.FullPath()) - switch expr := typeExpr.(type) { - // type Foo struct {...} - case *ast.StructType: - if typedef, ok := parser.TypeDefinitions[pkgName][typeName]; ok { - refTypeName := TypeDocName(pkgName, typedef) - if schema, isParsed := parser.swagger.Definitions[refTypeName]; isParsed { - return &schema, nil - } + return nil, ErrSkippedField } - return parser.parseStruct(pkgName, expr.Fields) + parser.debug.Printf("Override detected for %s: using %s instead", typeSpecDef.FullPath(), override) - // type Foo Baz - case *ast.Ident: - if IsGolangPrimitiveType(expr.Name) { - return &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: spec.StringOrArray{TransToValidSchemeType(expr.Name)}, - }, - }, nil - } - refTypeName := fullTypeName(pkgName, expr.Name) - if typedef, ok := parser.TypeDefinitions[pkgName][expr.Name]; ok { - refTypeName = TypeDocName(pkgName, typedef) - if _, isParsed := parser.swagger.Definitions[refTypeName]; !isParsed { - parser.ParseDefinition(pkgName, expr.Name, typedef) - } + separator := strings.LastIndex(override, ".") + if separator == -1 { + // treat as a swaggertype tag + parts := strings.Split(override, ",") + + return BuildCustomSchema(parts) } - return &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Ref: spec.Ref{ - Ref: jsonreference.MustCreateRef("#/definitions/" + refTypeName), - }, - }, - }, nil + typeSpecDef = parser.packages.findTypeSpec(override[0:separator], override[separator+1:]) + } - // type Foo *Baz - case *ast.StarExpr: - return parser.parseTypeExpr(pkgName, typeName, expr.X) + schema, ok := parser.parsedSchemas[typeSpecDef] + if !ok { + var err error - // type Foo []Baz - case *ast.ArrayType: - itemSchema, err := parser.parseTypeExpr(pkgName, "", expr.Elt) + schema, err = parser.ParseDefinition(typeSpecDef) if err != nil { - return &spec.Schema{}, err + if err == ErrRecursiveParseStruct && ref { + return parser.getRefTypeSchema(typeSpecDef, schema), nil + } + return nil, fmt.Errorf("%s: %w", typeName, err) } - return &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"array"}, - Items: &spec.SchemaOrArray{ - Schema: itemSchema, - }, - }, - }, nil + } - // type Foo pkg.Bar - case *ast.SelectorExpr: - if xIdent, ok := expr.X.(*ast.Ident); ok { - return parser.parseTypeExpr(xIdent.Name, expr.Sel.Name, expr.Sel) + if ref { + if IsComplexSchema(schema.Schema) { + return parser.getRefTypeSchema(typeSpecDef, schema), nil } - - // type Foo map[string]Bar - case *ast.MapType: - var valueSchema spec.SchemaOrBool - if _, ok := expr.Value.(*ast.InterfaceType); ok { - valueSchema.Allows = true - } else { - schema, err := parser.parseTypeExpr(pkgName, "", expr.Value) - if err != nil { - return &spec.Schema{}, err - } - valueSchema.Schema = schema - } - return &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - AdditionalProperties: &valueSchema, - }, - }, nil - // ... - default: - Printf("Type definition of type '%T' is not supported yet. Using 'object' instead.\n", typeExpr) + // if it is a simple schema, just return a copy + newSchema := *schema.Schema + return &newSchema, nil } - return &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - }, - }, nil + return schema.Schema, nil } -func (parser *Parser) parseStruct(pkgName string, fields *ast.FieldList) (*spec.Schema, error) { +func (parser *Parser) getRefTypeSchema(typeSpecDef *TypeSpecDef, schema *Schema) *spec.Schema { + _, ok := parser.outputSchemas[typeSpecDef] + if !ok { + parser.swagger.Definitions[schema.Name] = spec.Schema{} - extraRequired := make([]string, 0) - properties := make(map[string]spec.Schema) - for _, field := range fields.List { - fieldProps, requiredFromAnon, err := parser.parseStructField(pkgName, field) - if err != nil { - return &spec.Schema{}, err + if schema.Schema != nil { + parser.swagger.Definitions[schema.Name] = *schema.Schema } - extraRequired = append(extraRequired, requiredFromAnon...) - for k, v := range fieldProps { - properties[k] = v - } - } - - // collect requireds from our properties and anonymous fields - required := parser.collectRequiredFields(pkgName, properties, extraRequired) - // unset required from properties because we've collected them - for k, prop := range properties { - prop.SchemaProps.Required = make([]string, 0) - properties[k] = prop + parser.outputSchemas[typeSpecDef] = schema } - return &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: properties, - Required: required, - }}, nil -} + refSchema := RefSchema(schema.Name) -type structField struct { - name string - desc string - schemaType string - arrayType string - formatType string - isRequired bool - readOnly bool - crossPkg string - exampleValue interface{} - maximum *float64 - minimum *float64 - maxLength *int64 - minLength *int64 - enums []interface{} - defaultValue interface{} - extensions map[string]interface{} + return refSchema } -func (sf *structField) toStandardSchema() *spec.Schema { - required := make([]string, 0) - if sf.isRequired { - required = append(required, sf.name) - } - return &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{sf.schemaType}, - Description: sf.desc, - Format: sf.formatType, - Required: required, - Maximum: sf.maximum, - Minimum: sf.minimum, - MaxLength: sf.maxLength, - MinLength: sf.minLength, - Enum: sf.enums, - Default: sf.defaultValue, - }, - SwaggerSchemaProps: spec.SwaggerSchemaProps{ - Example: sf.exampleValue, - ReadOnly: sf.readOnly, - }, - VendorExtensible: spec.VendorExtensible{ - Extensions: sf.extensions, - }, +func (parser *Parser) isInStructStack(typeSpecDef *TypeSpecDef) bool { + for _, specDef := range parser.structStack { + if typeSpecDef == specDef { + return true + } } -} -func (parser *Parser) parseStructField(pkgName string, field *ast.Field) (map[string]spec.Schema, []string, error) { - properties := map[string]spec.Schema{} + return false +} - if field.Names == nil { - fullTypeName, err := getFieldType(field.Type) - if err != nil { - return properties, []string{}, nil - } +// ParseDefinition parses given type spec that corresponds to the type under +// given name and package, and populates swagger schema definitions registry +// with a schema for the given type +func (parser *Parser) ParseDefinition(typeSpecDef *TypeSpecDef) (*Schema, error) { + typeName := typeSpecDef.TypeName() + schema, found := parser.parsedSchemas[typeSpecDef] + if found { + parser.debug.Printf("Skipping '%s', already parsed.", typeName) - typeName := fullTypeName + return schema, nil + } - if splits := strings.Split(fullTypeName, "."); len(splits) > 1 { - pkgName = splits[0] - typeName = splits[1] - } + if parser.isInStructStack(typeSpecDef) { + parser.debug.Printf("Skipping '%s', recursion detected.", typeName) - typeSpec := parser.TypeDefinitions[pkgName][typeName] - if typeSpec == nil { - // Check if the pkg name is an alias and try to define type spec using real package name - if aliases, ok := parser.ImportAliases[pkgName]; ok { - for alias := range aliases { - typeSpec = parser.TypeDefinitions[alias][typeName] - if typeSpec != nil { - break - } - } - } - } - if typeSpec != nil { - schema, err := parser.parseTypeExpr(pkgName, typeName, typeSpec.Type) - if err != nil { - return properties, []string{}, err - } - schemaType := "unknown" - if len(schema.SchemaProps.Type) > 0 { - schemaType = schema.SchemaProps.Type[0] - } + return &Schema{ + Name: typeName, + PkgPath: typeSpecDef.PkgPath, + Schema: PrimitiveSchema(OBJECT), + }, + ErrRecursiveParseStruct + } - switch schemaType { - case "object": - for k, v := range schema.SchemaProps.Properties { - properties[k] = v - } - case "array": - properties[typeName] = *schema - default: - Printf("Can't extract properties from a schema of type '%s'", schemaType) - } - return properties, schema.SchemaProps.Required, nil - } + parser.structStack = append(parser.structStack, typeSpecDef) - return properties, nil, nil - } + parser.debug.Printf("Generating %s", typeName) - structField, err := parser.parseField(pkgName, field) + definition, err := parser.parseTypeExpr(typeSpecDef.File, typeSpecDef.TypeSpec.Type, false) if err != nil { - return properties, nil, err - } - if structField.name == "" { - return properties, nil, nil + parser.debug.Printf("Error parsing type definition '%s': %s", typeName, err) + return nil, err } - // TODO: find package of schemaType and/or arrayType - if structField.crossPkg != "" { - pkgName = structField.crossPkg + if definition.Description == "" { + fillDefinitionDescription(definition, typeSpecDef.File, typeSpecDef) } - fillObject := func(src, dest interface{}) error { - bin, err := json.Marshal(src) - if err != nil { - return err + if len(typeSpecDef.Enums) > 0 { + var varnames []string + var enumComments = make(map[string]string) + for _, value := range typeSpecDef.Enums { + definition.Enum = append(definition.Enum, value.Value) + varnames = append(varnames, value.key) + if len(value.Comment) > 0 { + enumComments[value.key] = value.Comment + } } - return json.Unmarshal(bin, dest) - } - - //for spec.Schema have implemented json.Marshaler, here in another way to convert - fillSchema := func(src, dest *spec.Schema) error { - err = fillObject(&src.SchemaProps, &dest.SchemaProps) - if err != nil { - return err + if definition.Extensions == nil { + definition.Extensions = make(spec.Extensions) } - err = fillObject(&src.SwaggerSchemaProps, &dest.SwaggerSchemaProps) - if err != nil { - return err + definition.Extensions[enumVarNamesExtension] = varnames + if len(enumComments) > 0 { + definition.Extensions[enumCommentsExtension] = enumComments } - return fillObject(&src.VendorExtensible, &dest.VendorExtensible) } - if typeSpec, ok := parser.TypeDefinitions[pkgName][structField.schemaType]; ok { // user type field - // write definition if not yet present - err = parser.ParseDefinition(pkgName, structField.schemaType, typeSpec) - if err != nil { - return properties, nil, err - } - required := make([]string, 0) - if structField.isRequired { - required = append(required, structField.name) - } - properties[structField.name] = spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, // to avoid swagger validation error - Description: structField.desc, - Required: required, - Ref: spec.Ref{ - Ref: jsonreference.MustCreateRef("#/definitions/" + TypeDocName(pkgName, typeSpec)), - }, - }, - SwaggerSchemaProps: spec.SwaggerSchemaProps{ - ReadOnly: structField.readOnly, - }, - } - } else if structField.schemaType == "array" { // array field type - // if defined -- ref it - if typeSpec, ok := parser.TypeDefinitions[pkgName][structField.arrayType]; ok { // user type in array - parser.ParseDefinition(pkgName, structField.arrayType, - parser.TypeDefinitions[pkgName][structField.arrayType]) - required := make([]string, 0) - if structField.isRequired { - required = append(required, structField.name) - } - properties[structField.name] = spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{structField.schemaType}, - Description: structField.desc, - Required: required, - Items: &spec.SchemaOrArray{ - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Ref: spec.Ref{ - Ref: jsonreference.MustCreateRef("#/definitions/" + TypeDocName(pkgName, typeSpec)), - }, - }, - }, - }, - }, - SwaggerSchemaProps: spec.SwaggerSchemaProps{ - ReadOnly: structField.readOnly, - }, - } - } else if structField.arrayType == "object" { - // Anonymous struct - if astTypeArray, ok := field.Type.(*ast.ArrayType); ok { // if array - props := make(map[string]spec.Schema) - if expr, ok := astTypeArray.Elt.(*ast.StructType); ok { - for _, field := range expr.Fields.List { - var fieldProps map[string]spec.Schema - fieldProps, _, err = parser.parseStructField(pkgName, field) - if err != nil { - return properties, nil, err - } - for k, v := range fieldProps { - props[k] = v - } - } - properties[structField.name] = spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{structField.schemaType}, - Description: structField.desc, - Items: &spec.SchemaOrArray{ - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: props, - }, - }, - }, - }, - SwaggerSchemaProps: spec.SwaggerSchemaProps{ - ReadOnly: structField.readOnly, - }, - } - } else { - schema, _ := parser.parseTypeExpr(pkgName, "", astTypeArray.Elt) - properties[structField.name] = spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{structField.schemaType}, - Description: structField.desc, - Items: &spec.SchemaOrArray{ - Schema: schema, - }, - }, - SwaggerSchemaProps: spec.SwaggerSchemaProps{ - ReadOnly: structField.readOnly, - }, - } - } - } - } else if structField.arrayType == "array" { - if astTypeArray, ok := field.Type.(*ast.ArrayType); ok { - schema, _ := parser.parseTypeExpr(pkgName, "", astTypeArray.Elt) - properties[structField.name] = spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{structField.schemaType}, - Description: structField.desc, - Items: &spec.SchemaOrArray{ - Schema: schema, - }, - }, - SwaggerSchemaProps: spec.SwaggerSchemaProps{ - ReadOnly: structField.readOnly, - }, - } - } - } else { - // standard type in array - required := make([]string, 0) - if structField.isRequired { - required = append(required, structField.name) - } + sch := Schema{ + Name: typeName, + PkgPath: typeSpecDef.PkgPath, + Schema: definition, + } + parser.parsedSchemas[typeSpecDef] = &sch + + // update an empty schema as a result of recursion + s2, found := parser.outputSchemas[typeSpecDef] + if found { + parser.swagger.Definitions[s2.Name] = *definition + } - properties[structField.name] = spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{structField.schemaType}, - Description: structField.desc, - Format: structField.formatType, - Required: required, - Items: &spec.SchemaOrArray{ - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{structField.arrayType}, - Maximum: structField.maximum, - Minimum: structField.minimum, - MaxLength: structField.maxLength, - MinLength: structField.minLength, - Enum: structField.enums, - Default: structField.defaultValue, - }, - }, - }, - }, - SwaggerSchemaProps: spec.SwaggerSchemaProps{ - Example: structField.exampleValue, - ReadOnly: structField.readOnly, - }, + return &sch, nil +} + +func fullTypeName(parts ...string) string { + return strings.Join(parts, ".") +} + +// fillDefinitionDescription additionally fills fields in definition (spec.Schema) +// TODO: If .go file contains many types, it may work for a long time +func fillDefinitionDescription(definition *spec.Schema, file *ast.File, typeSpecDef *TypeSpecDef) { + if file == nil { + return + } + for _, astDeclaration := range file.Decls { + generalDeclaration, ok := astDeclaration.(*ast.GenDecl) + if !ok || generalDeclaration.Tok != token.TYPE { + continue + } + + for _, astSpec := range generalDeclaration.Specs { + typeSpec, ok := astSpec.(*ast.TypeSpec) + if !ok || typeSpec != typeSpecDef.TypeSpec { + continue } + + definition.Description = + extractDeclarationDescription(typeSpec.Doc, typeSpec.Comment, generalDeclaration.Doc) } - } else if astTypeMap, ok := field.Type.(*ast.MapType); ok { // if map - stdSchema := structField.toStandardSchema() - mapValueSchema, err := parser.parseTypeExpr(pkgName, "", astTypeMap) - if err != nil { - return properties, nil, err - } - stdSchema.Type = mapValueSchema.Type - stdSchema.AdditionalProperties = mapValueSchema.AdditionalProperties - properties[structField.name] = *stdSchema - } else { - stdSchema := structField.toStandardSchema() - properties[structField.name] = *stdSchema - - if nestStar, ok := field.Type.(*ast.StarExpr); ok { - if !IsGolangPrimitiveType(structField.schemaType) { - schema, err := parser.parseTypeExpr(pkgName, structField.schemaType, nestStar.X) - if err != nil { - return properties, nil, err - } + } +} - if len(schema.SchemaProps.Type) > 0 { - err = fillSchema(schema, stdSchema) - if err != nil { - return properties, nil, err - } - properties[structField.name] = *stdSchema - return properties, nil, nil - } +// extractDeclarationDescription gets first description +// from attribute descriptionAttr in commentGroups (ast.CommentGroup) +func extractDeclarationDescription(commentGroups ...*ast.CommentGroup) string { + var description string + + for _, commentGroup := range commentGroups { + if commentGroup == nil { + continue + } + + isHandlingDescription := false + + for _, comment := range commentGroup.List { + commentText := strings.TrimSpace(strings.TrimLeft(comment.Text, "/")) + if len(commentText) == 0 { + continue } - } else if nestStruct, ok := field.Type.(*ast.StructType); ok { - props := map[string]spec.Schema{} - nestRequired := make([]string, 0) - for _, v := range nestStruct.Fields.List { - p, _, err := parser.parseStructField(pkgName, v) - if err != nil { - return properties, nil, err - } - for k, v := range p { - if v.SchemaProps.Type[0] != "object" { - nestRequired = append(nestRequired, v.SchemaProps.Required...) - v.SchemaProps.Required = make([]string, 0) - } - props[k] = v + attribute := FieldsByAnySpace(commentText, 2)[0] + + if strings.ToLower(attribute) != descriptionAttr { + if !isHandlingDescription { + continue } + + break } - stdSchema.Properties = props - stdSchema.Required = nestRequired - properties[structField.name] = *stdSchema + + isHandlingDescription = true + description += " " + strings.TrimSpace(commentText[len(attribute):]) } } - return properties, nil, nil + + return strings.TrimLeft(description, " ") } -func getFieldType(field interface{}) (string, error) { +// parseTypeExpr parses given type expression that corresponds to the type under +// given name and package, and returns swagger schema for it. +func (parser *Parser) parseTypeExpr(file *ast.File, typeExpr ast.Expr, ref bool) (*spec.Schema, error) { + switch expr := typeExpr.(type) { + // type Foo interface{} + case *ast.InterfaceType: + return &spec.Schema{}, nil + + // type Foo struct {...} + case *ast.StructType: + return parser.parseStruct(file, expr.Fields) - switch ftype := field.(type) { + // type Foo Baz case *ast.Ident: - return ftype.Name, nil + return parser.getTypeSchema(expr.Name, file, ref) + // type Foo *Baz + case *ast.StarExpr: + return parser.parseTypeExpr(file, expr.X, ref) + + // type Foo pkg.Bar case *ast.SelectorExpr: - packageName, err := getFieldType(ftype.X) + if xIdent, ok := expr.X.(*ast.Ident); ok { + return parser.getTypeSchema(fullTypeName(xIdent.Name, expr.Sel.Name), file, ref) + } + // type Foo []Baz + case *ast.ArrayType: + itemSchema, err := parser.parseTypeExpr(file, expr.Elt, true) if err != nil { - return "", err + return nil, err } - return fmt.Sprintf("%s.%s", packageName, ftype.Sel.Name), nil - case *ast.StarExpr: - fullName, err := getFieldType(ftype.X) + return spec.ArrayProperty(itemSchema), nil + // type Foo map[string]Bar + case *ast.MapType: + if _, ok := expr.Value.(*ast.InterfaceType); ok { + return spec.MapProperty(nil), nil + } + schema, err := parser.parseTypeExpr(file, expr.Value, true) if err != nil { - return "", err + return nil, err } - return fullName, nil + return spec.MapProperty(schema), nil + + case *ast.FuncType: + return nil, ErrFuncTypeField + // ... } - return "", fmt.Errorf("unknown field type %#v", field) + + return parser.parseGenericTypeExpr(file, typeExpr) } -func (parser *Parser) parseField(pkgName string, field *ast.Field) (*structField, error) { - prop, err := getPropertyName(pkgName, field.Type, parser) - if err != nil { - return nil, err - } +func (parser *Parser) parseStruct(file *ast.File, fields *ast.FieldList) (*spec.Schema, error) { + required, properties := make([]string, 0), make(map[string]spec.Schema) + + for _, field := range fields.List { + fieldProps, requiredFromAnon, err := parser.parseStructField(file, field) + if err != nil { + if errors.Is(err, ErrFuncTypeField) || errors.Is(err, ErrSkippedField) { + continue + } - if len(prop.ArrayType) == 0 { - if err := CheckSchemaType(prop.SchemaType); err != nil { return nil, err } - } else { - if err := CheckSchemaType("array"); err != nil { - return nil, err + + if len(fieldProps) == 0 { + continue } - } - // Skip func fields. - if prop.SchemaType == "func" { - return &structField{name: ""}, nil - } + required = append(required, requiredFromAnon...) - // Skip non-exported fields. - if !ast.IsExported(field.Names[0].Name) { - return &structField{name: ""}, nil + for k, v := range fieldProps { + properties[k] = v + } } - structField := &structField{ - name: field.Names[0].Name, - schemaType: prop.SchemaType, - arrayType: prop.ArrayType, - crossPkg: prop.CrossPkg, - } + sort.Strings(required) - switch parser.PropNamingStrategy { - case SnakeCase: - structField.name = toSnakeCase(structField.name) - case PascalCase: - //use struct field name - case CamelCase: - structField.name = toLowerCamelCase(structField.name) - default: - structField.name = toLowerCamelCase(structField.name) - } + return &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{OBJECT}, + Properties: properties, + Required: required, + }, + }, nil +} - if field.Doc != nil { - structField.desc = strings.TrimSpace(field.Doc.Text()) - } - if structField.desc == "" && field.Comment != nil { - structField.desc = strings.TrimSpace(field.Comment.Text()) +func (parser *Parser) parseStructField(file *ast.File, field *ast.Field) (map[string]spec.Schema, []string, error) { + if field.Tag != nil { + skip, ok := reflect.StructTag(strings.ReplaceAll(field.Tag.Value, "`", "")).Lookup("swaggerignore") + if ok && strings.EqualFold(skip, "true") { + return nil, nil, nil + } } - if field.Tag == nil { - return structField, nil + ps := parser.fieldParserFactory(parser, field) + + if ps.ShouldSkip() { + return nil, nil, nil } - // `json:"tag"` -> json:"tag" - structTag := reflect.StructTag(strings.Replace(field.Tag.Value, "`", "", -1)) - if ignoreTag := structTag.Get("swaggerignore"); ignoreTag == "true" { - structField.name = "" - return structField, nil + fieldName, err := ps.FieldName() + if err != nil { + return nil, nil, err } - jsonTag := structTag.Get("json") - hasStringTag := false - // json:"tag,hoge" - if strings.Contains(jsonTag, ",") { - // json:"name,string" or json:",string" - if strings.Contains(jsonTag, ",string") { - hasStringTag = true + if fieldName == "" { + typeName, err := getFieldType(file, field.Type, nil) + if err != nil { + return nil, nil, fmt.Errorf("%s: %w", fieldName, err) } - // json:",hoge" - if strings.HasPrefix(jsonTag, ",") { - jsonTag = "" - } else { - jsonTag = strings.SplitN(jsonTag, ",", 2)[0] - } - } - if jsonTag == "-" { - structField.name = "" - return structField, nil - } else if jsonTag != "" { - structField.name = jsonTag - } - - if typeTag := structTag.Get("swaggertype"); typeTag != "" { - parts := strings.Split(typeTag, ",") - if 0 < len(parts) && len(parts) <= 2 { - newSchemaType := parts[0] - newArrayType := structField.arrayType - if len(parts) >= 2 { - if newSchemaType == "array" { - newArrayType = parts[1] - if err := CheckSchemaType(newArrayType); err != nil { - return nil, err - } - } else if newSchemaType == "primitive" { - newSchemaType = parts[1] - newArrayType = parts[1] - } - } + schema, err := parser.getTypeSchema(typeName, file, false) + if err != nil { + return nil, nil, fmt.Errorf("%s: %w", fieldName, err) + } - if err := CheckSchemaType(newSchemaType); err != nil { - return nil, err + if len(schema.Type) > 0 && schema.Type[0] == OBJECT { + if len(schema.Properties) == 0 { + return nil, nil, nil } - structField.schemaType = newSchemaType - structField.arrayType = newArrayType - } - } - if exampleTag := structTag.Get("example"); exampleTag != "" { - if hasStringTag { - // then the example must be in string format - structField.exampleValue = exampleTag - } else { - example, err := defineTypeOfExample(structField.schemaType, structField.arrayType, exampleTag) - if err != nil { - return nil, err - } - structField.exampleValue = example - } - } - if formatTag := structTag.Get("format"); formatTag != "" { - structField.formatType = formatTag - } - if bindingTag := structTag.Get("binding"); bindingTag != "" { - for _, val := range strings.Split(bindingTag, ",") { - if val == "required" { - structField.isRequired = true - break + properties := map[string]spec.Schema{} + for k, v := range schema.Properties { + properties[k] = v } - } - } - if validateTag := structTag.Get("validate"); validateTag != "" { - for _, val := range strings.Split(validateTag, ",") { - if val == "required" { - structField.isRequired = true - break - } - } - } - if extensionsTag := structTag.Get("extensions"); extensionsTag != "" { - structField.extensions = map[string]interface{}{} - for _, val := range strings.Split(extensionsTag, ",") { - parts := strings.SplitN(val, "=", 2) - if len(parts) == 2 { - structField.extensions[parts[0]] = parts[1] - } else { - structField.extensions[parts[0]] = true - } - } - } - if enumsTag := structTag.Get("enums"); enumsTag != "" { - enumType := structField.schemaType - if structField.schemaType == "array" { - enumType = structField.arrayType - } - for _, e := range strings.Split(enumsTag, ",") { - value, err := defineType(enumType, e) - if err != nil { - return nil, err - } - structField.enums = append(structField.enums, value) + return properties, schema.SchemaProps.Required, nil } + // for alias type of non-struct types ,such as array,map, etc. ignore field tag. + return map[string]spec.Schema{typeName: *schema}, nil, nil + } - if defaultTag := structTag.Get("default"); defaultTag != "" { - value, err := defineType(structField.schemaType, defaultTag) - if err != nil { - return nil, err - } - structField.defaultValue = value + + schema, err := ps.CustomSchema() + if err != nil { + return nil, nil, fmt.Errorf("%s: %w", fieldName, err) } - if IsNumericType(structField.schemaType) || IsNumericType(structField.arrayType) { - maximum, err := getFloatTag(structTag, "maximum") - if err != nil { - return nil, err + if schema == nil { + typeName, err := getFieldType(file, field.Type, nil) + if err == nil { + // named type + schema, err = parser.getTypeSchema(typeName, file, true) + } else { + // unnamed type + schema, err = parser.parseTypeExpr(file, field.Type, false) } - structField.maximum = maximum - minimum, err := getFloatTag(structTag, "minimum") if err != nil { - return nil, err + return nil, nil, fmt.Errorf("%s: %w", fieldName, err) } - structField.minimum = minimum } - if structField.schemaType == "string" || structField.arrayType == "string" { - maxLength, err := getIntTag(structTag, "maxLength") - if err != nil { - return nil, err - } - structField.maxLength = maxLength - minLength, err := getIntTag(structTag, "minLength") - if err != nil { - return nil, err - } - structField.minLength = minLength - } - if readOnly := structTag.Get("readonly"); readOnly != "" { - structField.readOnly = readOnly == "true" + err = ps.ComplementSchema(schema) + if err != nil { + return nil, nil, fmt.Errorf("%s: %w", fieldName, err) } - // perform this after setting everything else (min, max, etc...) - if hasStringTag { + var tagRequired []string - // @encoding/json: "It applies only to fields of string, floating point, integer, or boolean types." - defaultValues := map[string]string{ - // Zero Values as string - "string": "", - "integer": "0", - "boolean": "false", - "number": "0", - } + required, err := ps.IsRequired() + if err != nil { + return nil, nil, fmt.Errorf("%s: %w", fieldName, err) + } - if defaultValue, ok := defaultValues[structField.schemaType]; ok { - structField.schemaType = "string" + if required { + tagRequired = append(tagRequired, fieldName) + } - if structField.exampleValue == nil { - // if exampleValue is not defined by the user, - // we will force an example with a correct value - // (eg: int->"0", bool:"false") - structField.exampleValue = defaultValue - } - } + if schema.Extensions == nil { + schema.Extensions = make(spec.Extensions) + } + if formName := ps.FormName(); len(formName) > 0 { + schema.Extensions["formData"] = formName + } + if headerName := ps.HeaderName(); len(headerName) > 0 { + schema.Extensions["header"] = headerName + } + if pathName := ps.PathName(); len(pathName) > 0 { + schema.Extensions["path"] = pathName } - return structField, nil + return map[string]spec.Schema{fieldName: *schema}, tagRequired, nil } -func replaceLastTag(slice []spec.Tag, element spec.Tag) { - slice = slice[:len(slice)-1] - slice = append(slice, element) +func getFieldType(file *ast.File, field ast.Expr, genericParamTypeDefs map[string]*genericTypeSpec) (string, error) { + switch fieldType := field.(type) { + case *ast.Ident: + return fieldType.Name, nil + case *ast.SelectorExpr: + packageName, err := getFieldType(file, fieldType.X, genericParamTypeDefs) + if err != nil { + return "", err + } + + return fullTypeName(packageName, fieldType.Sel.Name), nil + case *ast.StarExpr: + fullName, err := getFieldType(file, fieldType.X, genericParamTypeDefs) + if err != nil { + return "", err + } + + return fullName, nil + default: + return getGenericFieldType(file, field, genericParamTypeDefs) + } } -func getFloatTag(structTag reflect.StructTag, tagName string) (*float64, error) { - strValue := structTag.Get(tagName) - if strValue == "" { - return nil, nil +func (parser *Parser) getUnderlyingSchema(schema *spec.Schema) *spec.Schema { + if schema == nil { + return nil } - value, err := strconv.ParseFloat(strValue, 64) - if err != nil { - return nil, fmt.Errorf("can't parse numeric value of %q tag: %v", tagName, err) + if url := schema.Ref.GetURL(); url != nil { + if pos := strings.LastIndexByte(url.Fragment, '/'); pos >= 0 { + name := url.Fragment[pos+1:] + if schema, ok := parser.swagger.Definitions[name]; ok { + return &schema + } + } } - return &value, nil + if len(schema.AllOf) > 0 { + merged := &spec.Schema{} + MergeSchema(merged, schema) + for _, s := range schema.AllOf { + MergeSchema(merged, parser.getUnderlyingSchema(&s)) + } + return merged + } + return nil } -func getIntTag(structTag reflect.StructTag, tagName string) (*int64, error) { - strValue := structTag.Get(tagName) - if strValue == "" { - return nil, nil +// GetSchemaTypePath get path of schema type. +func (parser *Parser) GetSchemaTypePath(schema *spec.Schema, depth int) []string { + if schema == nil || depth == 0 { + return nil } - value, err := strconv.ParseInt(strValue, 10, 64) - if err != nil { - return nil, fmt.Errorf("can't parse numeric value of %q tag: %v", tagName, err) + if underlying := parser.getUnderlyingSchema(schema); underlying != nil { + return parser.GetSchemaTypePath(underlying, depth) } - return &value, nil -} + if len(schema.Type) > 0 { + switch schema.Type[0] { + case ARRAY: + depth-- -func toSnakeCase(in string) string { - runes := []rune(in) - length := len(runes) + s := []string{schema.Type[0]} - var out []rune - for i := 0; i < length; i++ { - if i > 0 && unicode.IsUpper(runes[i]) && ((i+1 < length && unicode.IsLower(runes[i+1])) || unicode.IsLower(runes[i-1])) { - out = append(out, '_') - } - out = append(out, unicode.ToLower(runes[i])) - } - return string(out) -} + return append(s, parser.GetSchemaTypePath(schema.Items.Schema, depth)...) + case OBJECT: + if schema.AdditionalProperties != nil && schema.AdditionalProperties.Schema != nil { + // for map + depth-- -func toLowerCamelCase(in string) string { - runes := []rune(in) + s := []string{schema.Type[0]} - var out []rune - flag := false - for i, curr := range runes { - if (i == 0 && unicode.IsUpper(curr)) || (flag && unicode.IsUpper(curr)) { - out = append(out, unicode.ToLower(curr)) - flag = true - } else { - out = append(out, curr) - flag = false + return append(s, parser.GetSchemaTypePath(schema.AdditionalProperties.Schema, depth)...) + } } + + return []string{schema.Type[0]} } - return string(out) + return []string{ANY} } -// defineTypeOfExample example value define the type (object and array unsupported) +func replaceLastTag(slice []spec.Tag, element spec.Tag) { + slice = append(slice[:len(slice)-1], element) +} + +// defineTypeOfExample example value define the type (object and array unsupported). func defineTypeOfExample(schemaType, arrayType, exampleValue string) (interface{}, error) { switch schemaType { - case "string": + case STRING: return exampleValue, nil - case "number": + case NUMBER: v, err := strconv.ParseFloat(exampleValue, 64) if err != nil { return nil, fmt.Errorf("example value %s can't convert to %s err: %s", exampleValue, schemaType, err) } + return v, nil - case "integer": + case INTEGER: v, err := strconv.Atoi(exampleValue) if err != nil { return nil, fmt.Errorf("example value %s can't convert to %s err: %s", exampleValue, schemaType, err) } + return v, nil - case "boolean": + case BOOLEAN: v, err := strconv.ParseBool(exampleValue) if err != nil { return nil, fmt.Errorf("example value %s can't convert to %s err: %s", exampleValue, schemaType, err) } + return v, nil - case "array": + case ARRAY: values := strings.Split(exampleValue, ",") result := make([]interface{}, 0) for _, value := range values { @@ -1486,31 +1647,85 @@ func defineTypeOfExample(schemaType, arrayType, exampleValue string) (interface{ if err != nil { return nil, err } + result = append(result, v) } + + return result, nil + case OBJECT: + if arrayType == "" { + return nil, fmt.Errorf("%s is unsupported type in example value `%s`", schemaType, exampleValue) + } + + values := strings.Split(exampleValue, ",") + + result := map[string]interface{}{} + + for _, value := range values { + mapData := strings.SplitN(value, ":", 2) + + if len(mapData) == 2 { + v, err := defineTypeOfExample(arrayType, "", mapData[1]) + if err != nil { + return nil, err + } + + result[mapData[0]] = v + + continue + } + + return nil, fmt.Errorf("example value %s should format: key:value", exampleValue) + } + return result, nil - default: - return nil, fmt.Errorf("%s is unsupported type in example value", schemaType) } + + return nil, fmt.Errorf("%s is unsupported type in example value %s", schemaType, exampleValue) } // GetAllGoFileInfo gets all Go source files information for given searchDir. -func (parser *Parser) getAllGoFileInfo(searchDir string) error { - return filepath.Walk(searchDir, parser.visit) +func (parser *Parser) getAllGoFileInfo(packageDir, searchDir string) error { + if parser.skipPackageByPrefix(packageDir) { + return nil // ignored by user-defined package path prefixes + } + return filepath.Walk(searchDir, func(path string, f os.FileInfo, _ error) error { + err := parser.Skip(path, f) + if err != nil { + return err + } + + if f.IsDir() { + return nil + } + + relPath, err := filepath.Rel(searchDir, path) + if err != nil { + return err + } + + return parser.parseFile(filepath.ToSlash(filepath.Dir(filepath.Clean(filepath.Join(packageDir, relPath)))), path, nil, ParseAll) + }) } -func (parser *Parser) getAllGoFileInfoFromDeps(pkg *depth.Pkg) error { +func (parser *Parser) getAllGoFileInfoFromDeps(pkg *depth.Pkg, parseFlag ParseFlag) error { ignoreInternal := pkg.Internal && !parser.ParseInternal if ignoreInternal || !pkg.Resolved { // ignored internal and not resolved dependencies return nil } + if pkg.Raw != nil && parser.skipPackageByPrefix(pkg.Raw.ImportPath) { + return nil // ignored by user-defined package path prefixes + } + // Skip cgo if pkg.Raw == nil && pkg.Name == "C" { return nil } + srcDir := pkg.Raw.Dir - files, err := ioutil.ReadDir(srcDir) // only parsing files in the dir(don't contains sub dir files) + + files, err := os.ReadDir(srcDir) // only parsing files in the dir(don't contain sub dir files) if err != nil { return err } @@ -1521,13 +1736,13 @@ func (parser *Parser) getAllGoFileInfoFromDeps(pkg *depth.Pkg) error { } path := filepath.Join(srcDir, f.Name()) - if err := parser.parseFile(path); err != nil { + if err := parser.parseFile(pkg.Name, path, nil, parseFlag); err != nil { return err } } for i := 0; i < len(pkg.Deps); i++ { - if err := parser.getAllGoFileInfoFromDeps(&pkg.Deps[i]); err != nil { + if err := parser.getAllGoFileInfoFromDeps(&pkg.Deps[i], parseFlag); err != nil { return err } } @@ -1535,46 +1750,86 @@ func (parser *Parser) getAllGoFileInfoFromDeps(pkg *depth.Pkg) error { return nil } -func (parser *Parser) visit(path string, f os.FileInfo, err error) error { - if err := parser.Skip(path, f); err != nil { - return err +func (parser *Parser) parseFile(packageDir, path string, src interface{}, flag ParseFlag) error { + if strings.HasSuffix(strings.ToLower(path), "_test.go") || filepath.Ext(path) != ".go" { + return nil } - return parser.parseFile(path) + + return parser.packages.ParseFile(packageDir, path, src, flag) } -func (parser *Parser) parseFile(path string) error { - if ext := filepath.Ext(path); ext == ".go" { - fset := token.NewFileSet() // positions are relative to fset - astFile, err := goparser.ParseFile(fset, path, nil, goparser.ParseComments) - if err != nil { - return fmt.Errorf("ParseFile error:%+v", err) +func (parser *Parser) checkOperationIDUniqueness() error { + // operationsIds contains all operationId annotations to check it's unique + operationsIds := make(map[string]string) + + for path, item := range parser.swagger.Paths.Paths { + var method, id string + + for method = range allMethod { + op := refRouteMethodOp(&item, method) + if *op != nil { + id = (**op).ID + + break + } + } + + if id == "" { + continue + } + + current := fmt.Sprintf("%s %s", method, path) + + previous, ok := operationsIds[id] + if ok { + return fmt.Errorf( + "duplicated @id annotation '%s' found in '%s', previously declared in: '%s'", + id, current, previous) } - parser.files[path] = astFile + operationsIds[id] = current } + return nil } -// Skip returns filepath.SkipDir error if match vendor and hidden folder +// Skip returns filepath.SkipDir error if match vendor and hidden folder. func (parser *Parser) Skip(path string, f os.FileInfo) error { - if f.IsDir() { - if !parser.ParseVendor && f.Name() == "vendor" || //ignore "vendor" - f.Name() == "docs" || //exclude docs - len(f.Name()) > 1 && f.Name()[0] == '.' { // exclude all hidden folder - return filepath.SkipDir - } + return walkWith(parser.excludes, parser.ParseVendor)(path, f) +} - if parser.excludes != nil { - if _, ok := parser.excludes[path]; ok { +func walkWith(excludes map[string]struct{}, parseVendor bool) func(path string, fileInfo os.FileInfo) error { + return func(path string, f os.FileInfo) error { + if f.IsDir() { + if !parseVendor && f.Name() == "vendor" || // ignore "vendor" + f.Name() == "docs" || // exclude docs + len(f.Name()) > 1 && f.Name()[0] == '.' && f.Name() != ".." { // exclude all hidden folder return filepath.SkipDir } + + if excludes != nil { + if _, ok := excludes[path]; ok { + return filepath.SkipDir + } + } } - } - return nil + return nil + } } // GetSwagger returns *spec.Swagger which is the root document object for the API specification. func (parser *Parser) GetSwagger() *spec.Swagger { return parser.swagger } + +// addTestType just for tests. +func (parser *Parser) addTestType(typename string) { + typeDef := &TypeSpecDef{} + parser.packages.uniqueDefinitions[typename] = typeDef + parser.parsedSchemas[typeDef] = &Schema{ + PkgPath: "", + Name: typename, + Schema: PrimitiveSchema(OBJECT), + } +} diff --git a/vendor/github.com/swaggo/swag/property.go b/vendor/github.com/swaggo/swag/property.go deleted file mode 100644 index 6d45632737..0000000000 --- a/vendor/github.com/swaggo/swag/property.go +++ /dev/null @@ -1,139 +0,0 @@ -package swag - -import ( - "errors" - "fmt" - "go/ast" - "strings" -) - -// ErrFailedConvertPrimitiveType Failed to convert for swag to interpretable type -var ErrFailedConvertPrimitiveType = errors.New("swag property: failed convert primitive type") - -type propertyName struct { - SchemaType string - ArrayType string - CrossPkg string -} - -type propertyNewFunc func(schemeType string, crossPkg string) propertyName - -func newArrayProperty(schemeType string, crossPkg string) propertyName { - return propertyName{ - SchemaType: "array", - ArrayType: schemeType, - CrossPkg: crossPkg, - } -} - -func newProperty(schemeType string, crossPkg string) propertyName { - return propertyName{ - SchemaType: schemeType, - ArrayType: "string", - CrossPkg: crossPkg, - } -} - -func convertFromSpecificToPrimitive(typeName string) (string, error) { - typeName = strings.ToUpper(typeName) - switch typeName { - case "TIME", "OBJECTID", "UUID": - return "string", nil - case "DECIMAL": - return "number", nil - } - return "", ErrFailedConvertPrimitiveType -} - -func parseFieldSelectorExpr(astTypeSelectorExpr *ast.SelectorExpr, parser *Parser, propertyNewFunc propertyNewFunc) propertyName { - if primitiveType, err := convertFromSpecificToPrimitive(astTypeSelectorExpr.Sel.Name); err == nil { - return propertyNewFunc(primitiveType, "") - } - - if pkgName, ok := astTypeSelectorExpr.X.(*ast.Ident); ok { - if typeDefinitions, ok := parser.TypeDefinitions[pkgName.Name][astTypeSelectorExpr.Sel.Name]; ok { - if expr, ok := typeDefinitions.Type.(*ast.SelectorExpr); ok { - if primitiveType, err := convertFromSpecificToPrimitive(expr.Sel.Name); err == nil { - return propertyNewFunc(primitiveType, "") - } - } - parser.ParseDefinition(pkgName.Name, astTypeSelectorExpr.Sel.Name, typeDefinitions) - return propertyNewFunc(astTypeSelectorExpr.Sel.Name, pkgName.Name) - } - if aliasedNames, ok := parser.ImportAliases[pkgName.Name]; ok { - for aliasedName := range aliasedNames { - if typeDefinitions, ok := parser.TypeDefinitions[aliasedName][astTypeSelectorExpr.Sel.Name]; ok { - if expr, ok := typeDefinitions.Type.(*ast.SelectorExpr); ok { - if primitiveType, err := convertFromSpecificToPrimitive(expr.Sel.Name); err == nil { - return propertyNewFunc(primitiveType, "") - } - } - parser.ParseDefinition(aliasedName, astTypeSelectorExpr.Sel.Name, typeDefinitions) - return propertyNewFunc(astTypeSelectorExpr.Sel.Name, aliasedName) - } - } - } - name := fmt.Sprintf("%s.%v", pkgName, astTypeSelectorExpr.Sel.Name) - if actualPrimitiveType, isCustomType := parser.CustomPrimitiveTypes[name]; isCustomType { - return propertyName{SchemaType: actualPrimitiveType, ArrayType: actualPrimitiveType} - } - } - return propertyName{SchemaType: "string", ArrayType: "string"} -} - -// getPropertyName returns the string value for the given field if it exists -// allowedValues: array, boolean, integer, null, number, object, string -func getPropertyName(pkgName string, expr ast.Expr, parser *Parser) (propertyName, error) { - switch tp := expr.(type) { - case *ast.SelectorExpr: - return parseFieldSelectorExpr(tp, parser, newProperty), nil - case *ast.StarExpr: - return getPropertyName(pkgName, tp.X, parser) - case *ast.ArrayType: - return getArrayPropertyName(pkgName, tp.Elt, parser), nil - case *ast.MapType, *ast.StructType, *ast.InterfaceType: - return propertyName{SchemaType: "object", ArrayType: "object"}, nil - case *ast.FuncType: - return propertyName{SchemaType: "func", ArrayType: ""}, nil - case *ast.Ident: - name := tp.Name - // check if it is a custom type - if actualPrimitiveType, isCustomType := parser.CustomPrimitiveTypes[fullTypeName(pkgName, name)]; isCustomType { - return propertyName{SchemaType: actualPrimitiveType, ArrayType: actualPrimitiveType}, nil - } - - name = TransToValidSchemeType(name) - return propertyName{SchemaType: name, ArrayType: name}, nil - default: - return propertyName{}, errors.New("not supported" + fmt.Sprint(expr)) - } -} - -func getArrayPropertyName(pkgName string, astTypeArrayElt ast.Expr, parser *Parser) propertyName { - switch elt := astTypeArrayElt.(type) { - case *ast.StructType, *ast.MapType, *ast.InterfaceType: - return propertyName{SchemaType: "array", ArrayType: "object"} - case *ast.ArrayType: - return propertyName{SchemaType: "array", ArrayType: "array"} - case *ast.StarExpr: - return getArrayPropertyName(pkgName, elt.X, parser) - case *ast.SelectorExpr: - return parseFieldSelectorExpr(elt, parser, newArrayProperty) - case *ast.Ident: - name := elt.Name - if actualPrimitiveType, isCustomType := parser.CustomPrimitiveTypes[fullTypeName(pkgName, name)]; isCustomType { - name = actualPrimitiveType - } else { - name = TransToValidSchemeType(elt.Name) - } - return propertyName{SchemaType: "array", ArrayType: name} - default: - name := fmt.Sprintf("%s", astTypeArrayElt) - if actualPrimitiveType, isCustomType := parser.CustomPrimitiveTypes[fullTypeName(pkgName, name)]; isCustomType { - name = actualPrimitiveType - } else { - name = TransToValidSchemeType(name) - } - return propertyName{SchemaType: "array", ArrayType: name} - } -} diff --git a/vendor/github.com/swaggo/swag/schema.go b/vendor/github.com/swaggo/swag/schema.go index 539340fae9..b3a5b38c16 100644 --- a/vendor/github.com/swaggo/swag/schema.go +++ b/vendor/github.com/swaggo/swag/schema.go @@ -1,65 +1,101 @@ package swag import ( + "errors" "fmt" - "go/ast" - "strings" + "github.com/go-openapi/spec" ) -// CheckSchemaType checks if typeName is not a name of primitive type +const ( + // ARRAY represent a array value. + ARRAY = "array" + // OBJECT represent a object value. + OBJECT = "object" + // PRIMITIVE represent a primitive value. + PRIMITIVE = "primitive" + // BOOLEAN represent a boolean value. + BOOLEAN = "boolean" + // INTEGER represent a integer value. + INTEGER = "integer" + // NUMBER represent a number value. + NUMBER = "number" + // STRING represent a string value. + STRING = "string" + // FUNC represent a function value. + FUNC = "func" + // ERROR represent a error value. + ERROR = "error" + // INTERFACE represent a interface value. + INTERFACE = "interface{}" + // ANY represent a any value. + ANY = "any" + // NIL represent a empty value. + NIL = "nil" + + // IgnoreNameOverridePrefix Prepend to model to avoid renaming based on comment. + IgnoreNameOverridePrefix = '$' +) + +// CheckSchemaType checks if typeName is not a name of primitive type. func CheckSchemaType(typeName string) error { if !IsPrimitiveType(typeName) { return fmt.Errorf("%s is not basic types", typeName) } + return nil } -// IsSimplePrimitiveType determine whether the type name is a simple primitive type +// IsSimplePrimitiveType determine whether the type name is a simple primitive type. func IsSimplePrimitiveType(typeName string) bool { switch typeName { - case "string", "number", "integer", "boolean": + case STRING, NUMBER, INTEGER, BOOLEAN: return true - default: - return false } + + return false } -// IsPrimitiveType determine whether the type name is a primitive type +// IsPrimitiveType determine whether the type name is a primitive type. func IsPrimitiveType(typeName string) bool { switch typeName { - case "string", "number", "integer", "boolean", "array", "object", "func": + case STRING, NUMBER, INTEGER, BOOLEAN, ARRAY, OBJECT, FUNC: return true - default: - return false } + + return false +} + +// IsInterfaceLike determines whether the swagger type name is an go named interface type like error type. +func IsInterfaceLike(typeName string) bool { + return typeName == ERROR || typeName == ANY } -// IsNumericType determines whether the swagger type name is a numeric type +// IsNumericType determines whether the swagger type name is a numeric type. func IsNumericType(typeName string) bool { - return typeName == "integer" || typeName == "number" + return typeName == INTEGER || typeName == NUMBER } // TransToValidSchemeType indicates type will transfer golang basic type to swagger supported type. func TransToValidSchemeType(typeName string) string { switch typeName { case "uint", "int", "uint8", "int8", "uint16", "int16", "byte": - return "integer" + return INTEGER case "uint32", "int32", "rune": - return "integer" + return INTEGER case "uint64", "int64": - return "integer" + return INTEGER case "float32", "float64": - return "number" + return NUMBER case "bool": - return "boolean" + return BOOLEAN case "string": - return "string" - default: - return typeName // to support user defined types + return STRING } + + return typeName } -// IsGolangPrimitiveType determine whether the type name is a golang primitive type +// IsGolangPrimitiveType determine whether the type name is a golang primitive type. func IsGolangPrimitiveType(typeName string) bool { switch typeName { case "uint", @@ -79,39 +115,179 @@ func IsGolangPrimitiveType(typeName string) bool { "bool", "string": return true - default: - return false } + + return false } -// TransToValidCollectionFormat determine valid collection format +// TransToValidCollectionFormat determine valid collection format. func TransToValidCollectionFormat(format string) string { switch format { case "csv", "multi", "pipes", "tsv", "ssv": return format - default: - return "" } + + return "" +} + +func ignoreNameOverride(name string) bool { + return len(name) != 0 && name[0] == IgnoreNameOverridePrefix +} + +// IsComplexSchema whether a schema is complex and should be a ref schema +func IsComplexSchema(schema *spec.Schema) bool { + // a enum type should be complex + if len(schema.Enum) > 0 { + return true + } + + // a deep array type is complex, how to determine deep? here more than 2 ,for example: [][]object,[][][]int + if len(schema.Type) > 2 { + return true + } + + //Object included, such as Object or []Object + for _, st := range schema.Type { + if st == OBJECT { + return true + } + } + return false +} + +// IsRefSchema whether a schema is a reference schema. +func IsRefSchema(schema *spec.Schema) bool { + return schema.Ref.Ref.GetURL() != nil +} + +// RefSchema build a reference schema. +func RefSchema(refType string) *spec.Schema { + return spec.RefSchema("#/definitions/" + refType) } -// TypeDocName get alias from comment '// @name ', otherwise the original type name to display in doc -func TypeDocName(pkgName string, spec *ast.TypeSpec) string { - if spec != nil { - if spec.Comment != nil { - for _, comment := range spec.Comment.List { - text := strings.TrimSpace(comment.Text) - text = strings.TrimLeft(text, "//") - text = strings.TrimSpace(text) - texts := strings.Split(text, " ") - if len(texts) > 1 && strings.ToLower(texts[0]) == "@name" { - return texts[1] - } - } +// PrimitiveSchema build a primitive schema. +func PrimitiveSchema(refType string) *spec.Schema { + return &spec.Schema{SchemaProps: spec.SchemaProps{Type: []string{refType}}} +} + +// BuildCustomSchema build custom schema specified by tag swaggertype. +func BuildCustomSchema(types []string) (*spec.Schema, error) { + if len(types) == 0 { + return nil, nil + } + + switch types[0] { + case PRIMITIVE: + if len(types) == 1 { + return nil, errors.New("need primitive type after primitive") } - if spec.Name != nil { - return fullTypeName(strings.Split(pkgName, ".")[0], spec.Name.Name) + + return BuildCustomSchema(types[1:]) + case ARRAY: + if len(types) == 1 { + return nil, errors.New("need array item type after array") + } + + schema, err := BuildCustomSchema(types[1:]) + if err != nil { + return nil, err + } + + return spec.ArrayProperty(schema), nil + case OBJECT: + if len(types) == 1 { + return PrimitiveSchema(types[0]), nil + } + + schema, err := BuildCustomSchema(types[1:]) + if err != nil { + return nil, err + } + + return spec.MapProperty(schema), nil + default: + err := CheckSchemaType(types[0]) + if err != nil { + return nil, err } + + return PrimitiveSchema(types[0]), nil } +} - return pkgName +// MergeSchema merge schemas +func MergeSchema(dst *spec.Schema, src *spec.Schema) *spec.Schema { + if len(src.Type) > 0 { + dst.Type = src.Type + } + if len(src.Properties) > 0 { + dst.Properties = src.Properties + } + if src.Items != nil { + dst.Items = src.Items + } + if src.AdditionalProperties != nil { + dst.AdditionalProperties = src.AdditionalProperties + } + if len(src.Description) > 0 { + dst.Description = src.Description + } + if src.Nullable { + dst.Nullable = src.Nullable + } + if len(src.Format) > 0 { + dst.Format = src.Format + } + if src.Default != nil { + dst.Default = src.Default + } + if src.Example != nil { + dst.Example = src.Example + } + if len(src.Extensions) > 0 { + dst.Extensions = src.Extensions + } + if src.Maximum != nil { + dst.Maximum = src.Maximum + } + if src.Minimum != nil { + dst.Minimum = src.Minimum + } + if src.ExclusiveMaximum { + dst.ExclusiveMaximum = src.ExclusiveMaximum + } + if src.ExclusiveMinimum { + dst.ExclusiveMinimum = src.ExclusiveMinimum + } + if src.MaxLength != nil { + dst.MaxLength = src.MaxLength + } + if src.MinLength != nil { + dst.MinLength = src.MinLength + } + if len(src.Pattern) > 0 { + dst.Pattern = src.Pattern + } + if src.MaxItems != nil { + dst.MaxItems = src.MaxItems + } + if src.MinItems != nil { + dst.MinItems = src.MinItems + } + if src.UniqueItems { + dst.UniqueItems = src.UniqueItems + } + if src.MultipleOf != nil { + dst.MultipleOf = src.MultipleOf + } + if len(src.Enum) > 0 { + dst.Enum = src.Enum + } + if len(src.Extensions) > 0 { + dst.Extensions = src.Extensions + } + if len(src.ExtraProps) > 0 { + dst.ExtraProps = src.ExtraProps + } + return dst } diff --git a/vendor/github.com/swaggo/swag/spec.go b/vendor/github.com/swaggo/swag/spec.go new file mode 100644 index 0000000000..c18a365b0c --- /dev/null +++ b/vendor/github.com/swaggo/swag/spec.go @@ -0,0 +1,64 @@ +package swag + +import ( + "bytes" + "encoding/json" + "strings" + "text/template" +) + +// Spec holds exported Swagger Info so clients can modify it. +type Spec struct { + Version string + Host string + BasePath string + Schemes []string + Title string + Description string + InfoInstanceName string + SwaggerTemplate string + LeftDelim string + RightDelim string +} + +// ReadDoc parses SwaggerTemplate into swagger document. +func (i *Spec) ReadDoc() string { + i.Description = strings.ReplaceAll(i.Description, "\n", "\\n") + + tpl := template.New("swagger_info").Funcs(template.FuncMap{ + "marshal": func(v interface{}) string { + a, _ := json.Marshal(v) + + return string(a) + }, + "escape": func(v interface{}) string { + // escape tabs + var str = strings.ReplaceAll(v.(string), "\t", "\\t") + // replace " with \", and if that results in \\", replace that with \\\" + str = strings.ReplaceAll(str, "\"", "\\\"") + + return strings.ReplaceAll(str, "\\\\\"", "\\\\\\\"") + }, + }) + + if i.LeftDelim != "" && i.RightDelim != "" { + tpl = tpl.Delims(i.LeftDelim, i.RightDelim) + } + + parsed, err := tpl.Parse(i.SwaggerTemplate) + if err != nil { + return i.SwaggerTemplate + } + + var doc bytes.Buffer + if err = parsed.Execute(&doc, i); err != nil { + return i.SwaggerTemplate + } + + return doc.String() +} + +// InstanceName returns Spec instance name. +func (i *Spec) InstanceName() string { + return i.InfoInstanceName +} diff --git a/vendor/github.com/swaggo/swag/swagger.go b/vendor/github.com/swaggo/swag/swagger.go index 38c5ebfc60..74c162c284 100644 --- a/vendor/github.com/swaggo/swag/swagger.go +++ b/vendor/github.com/swaggo/swag/swagger.go @@ -2,6 +2,7 @@ package swag import ( "errors" + "fmt" "sync" ) @@ -10,10 +11,10 @@ const Name = "swagger" var ( swaggerMu sync.RWMutex - swag Swagger + swags map[string]Swagger ) -// Swagger is a interface to read swagger document. +// Swagger is an interface to read swagger document. type Swagger interface { ReadDoc() string } @@ -22,20 +23,50 @@ type Swagger interface { func Register(name string, swagger Swagger) { swaggerMu.Lock() defer swaggerMu.Unlock() + if swagger == nil { panic("swagger is nil") } - if swag != nil { + if swags == nil { + swags = make(map[string]Swagger) + } + + if _, ok := swags[name]; ok { panic("Register called twice for swag: " + name) } - swag = swagger + + swags[name] = swagger +} + +// GetSwagger returns the swagger instance for given name. +// If not found, returns nil. +func GetSwagger(name string) Swagger { + swaggerMu.RLock() + defer swaggerMu.RUnlock() + + return swags[name] } -// ReadDoc reads swagger document. -func ReadDoc() (string, error) { - if swag != nil { - return swag.ReadDoc(), nil +// ReadDoc reads swagger document. An optional name parameter can be passed to read a specific document. +// The default name is "swagger". +func ReadDoc(optionalName ...string) (string, error) { + swaggerMu.RLock() + defer swaggerMu.RUnlock() + + if swags == nil { + return "", errors.New("no swag has yet been registered") + } + + name := Name + if len(optionalName) != 0 && optionalName[0] != "" { + name = optionalName[0] } - return "", errors.New("not yet registered swag") + + swag, ok := swags[name] + if !ok { + return "", fmt.Errorf("no swag named \"%s\" was registered", name) + } + + return swag.ReadDoc(), nil } diff --git a/vendor/github.com/swaggo/swag/types.go b/vendor/github.com/swaggo/swag/types.go new file mode 100644 index 0000000000..0076a6b40e --- /dev/null +++ b/vendor/github.com/swaggo/swag/types.go @@ -0,0 +1,105 @@ +package swag + +import ( + "go/ast" + "go/token" + "regexp" + "strings" + + "github.com/go-openapi/spec" +) + +// Schema parsed schema. +type Schema struct { + *spec.Schema // + PkgPath string // package import path used to rename Name of a definition int case of conflict + Name string // Name in definitions +} + +// TypeSpecDef the whole information of a typeSpec. +type TypeSpecDef struct { + // ast file where TypeSpec is + File *ast.File + + // the TypeSpec of this type definition + TypeSpec *ast.TypeSpec + + Enums []EnumValue + + // path of package starting from under ${GOPATH}/src or from module path in go.mod + PkgPath string + ParentSpec ast.Decl + + NotUnique bool +} + +// Name the name of the typeSpec. +func (t *TypeSpecDef) Name() string { + if t.TypeSpec != nil && t.TypeSpec.Name != nil { + return t.TypeSpec.Name.Name + } + + return "" +} + +// TypeName the type name of the typeSpec. +func (t *TypeSpecDef) TypeName() string { + if ignoreNameOverride(t.TypeSpec.Name.Name) { + return t.TypeSpec.Name.Name[1:] + } else if t.TypeSpec.Comment != nil { + // get alias from comment '// @name ' + const regexCaseInsensitive = "(?i)" + reTypeName, err := regexp.Compile(regexCaseInsensitive + `^@name\s+(\S+)`) + if err != nil { + panic(err) + } + for _, comment := range t.TypeSpec.Comment.List { + trimmedComment := strings.TrimSpace(strings.TrimLeft(comment.Text, "/")) + texts := reTypeName.FindStringSubmatch(trimmedComment) + if len(texts) > 1 { + return texts[1] + } + } + } + + var names []string + if t.NotUnique { + pkgPath := strings.Map(func(r rune) rune { + if r == '\\' || r == '/' || r == '.' { + return '_' + } + return r + }, t.PkgPath) + names = append(names, pkgPath) + } else if t.File != nil { + names = append(names, t.File.Name.Name) + } + if parentFun, ok := (t.ParentSpec).(*ast.FuncDecl); ok && parentFun != nil { + names = append(names, parentFun.Name.Name) + } + names = append(names, t.TypeSpec.Name.Name) + return fullTypeName(names...) +} + +// FullPath return the full path of the typeSpec. +func (t *TypeSpecDef) FullPath() string { + return t.PkgPath + "." + t.Name() +} + +// AstFileInfo information of an ast.File. +type AstFileInfo struct { + //FileSet the FileSet object which is used to parse this go source file + FileSet *token.FileSet + + // File ast.File + File *ast.File + + // Path the path of the ast.File + Path string + + // PackagePath package import path of the ast.File + PackagePath string + + // ParseFlag determine what to parse + ParseFlag ParseFlag +} diff --git a/vendor/github.com/swaggo/swag/utils.go b/vendor/github.com/swaggo/swag/utils.go new file mode 100644 index 0000000000..df31ff2e16 --- /dev/null +++ b/vendor/github.com/swaggo/swag/utils.go @@ -0,0 +1,55 @@ +package swag + +import "unicode" + +// FieldsFunc split a string s by a func splitter into max n parts +func FieldsFunc(s string, f func(rune2 rune) bool, n int) []string { + // A span is used to record a slice of s of the form s[start:end]. + // The start index is inclusive and the end index is exclusive. + type span struct { + start int + end int + } + spans := make([]span, 0, 32) + + // Find the field start and end indices. + // Doing this in a separate pass (rather than slicing the string s + // and collecting the result substrings right away) is significantly + // more efficient, possibly due to cache effects. + start := -1 // valid span start if >= 0 + for end, rune := range s { + if f(rune) { + if start >= 0 { + spans = append(spans, span{start, end}) + // Set start to a negative value. + // Note: using -1 here consistently and reproducibly + // slows down this code by a several percent on amd64. + start = ^start + } + } else { + if start < 0 { + start = end + if n > 0 && len(spans)+1 >= n { + break + } + } + } + } + + // Last field might end at EOF. + if start >= 0 { + spans = append(spans, span{start, len(s)}) + } + + // Create strings from recorded field indices. + a := make([]string, len(spans)) + for i, span := range spans { + a[i] = s[span.start:span.end] + } + return a +} + +// FieldsByAnySpace split a string s by any space character into max n parts +func FieldsByAnySpace(s string, n int) []string { + return FieldsFunc(s, unicode.IsSpace, n) +} diff --git a/vendor/github.com/swaggo/swag/version.go b/vendor/github.com/swaggo/swag/version.go index d322f013e8..ff2810e61d 100644 --- a/vendor/github.com/swaggo/swag/version.go +++ b/vendor/github.com/swaggo/swag/version.go @@ -1,4 +1,4 @@ package swag -// Version of swag -const Version = "v1.6.7" +// Version of swag. +const Version = "v1.16.3" diff --git a/vendor/golang.org/x/crypto/chacha20/chacha_arm64.go b/vendor/golang.org/x/crypto/chacha20/chacha_arm64.go index 94c71ac1ac..5dfacbb983 100644 --- a/vendor/golang.org/x/crypto/chacha20/chacha_arm64.go +++ b/vendor/golang.org/x/crypto/chacha20/chacha_arm64.go @@ -2,8 +2,8 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build go1.11 && gc && !purego -// +build go1.11,gc,!purego +//go:build gc && !purego +// +build gc,!purego package chacha20 diff --git a/vendor/golang.org/x/crypto/chacha20/chacha_arm64.s b/vendor/golang.org/x/crypto/chacha20/chacha_arm64.s index 63cae9e6f0..f1f66230d1 100644 --- a/vendor/golang.org/x/crypto/chacha20/chacha_arm64.s +++ b/vendor/golang.org/x/crypto/chacha20/chacha_arm64.s @@ -2,8 +2,8 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build go1.11 && gc && !purego -// +build go1.11,gc,!purego +//go:build gc && !purego +// +build gc,!purego #include "textflag.h" diff --git a/vendor/golang.org/x/crypto/chacha20/chacha_noasm.go b/vendor/golang.org/x/crypto/chacha20/chacha_noasm.go index 025b49897e..02ff3d05e9 100644 --- a/vendor/golang.org/x/crypto/chacha20/chacha_noasm.go +++ b/vendor/golang.org/x/crypto/chacha20/chacha_noasm.go @@ -2,8 +2,8 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build (!arm64 && !s390x && !ppc64le) || (arm64 && !go1.11) || !gc || purego -// +build !arm64,!s390x,!ppc64le arm64,!go1.11 !gc purego +//go:build (!arm64 && !s390x && !ppc64le) || !gc || purego +// +build !arm64,!s390x,!ppc64le !gc purego package chacha20 diff --git a/vendor/golang.org/x/crypto/ed25519/ed25519.go b/vendor/golang.org/x/crypto/ed25519/ed25519.go deleted file mode 100644 index a7828345fc..0000000000 --- a/vendor/golang.org/x/crypto/ed25519/ed25519.go +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// Package ed25519 implements the Ed25519 signature algorithm. See -// https://ed25519.cr.yp.to/. -// -// These functions are also compatible with the “Ed25519” function defined in -// RFC 8032. However, unlike RFC 8032's formulation, this package's private key -// representation includes a public key suffix to make multiple signing -// operations with the same key more efficient. This package refers to the RFC -// 8032 private key as the “seed”. -// -// Beginning with Go 1.13, the functionality of this package was moved to the -// standard library as crypto/ed25519. This package only acts as a compatibility -// wrapper. -package ed25519 - -import ( - "crypto/ed25519" - "io" -) - -const ( - // PublicKeySize is the size, in bytes, of public keys as used in this package. - PublicKeySize = 32 - // PrivateKeySize is the size, in bytes, of private keys as used in this package. - PrivateKeySize = 64 - // SignatureSize is the size, in bytes, of signatures generated and verified by this package. - SignatureSize = 64 - // SeedSize is the size, in bytes, of private key seeds. These are the private key representations used by RFC 8032. - SeedSize = 32 -) - -// PublicKey is the type of Ed25519 public keys. -// -// This type is an alias for crypto/ed25519's PublicKey type. -// See the crypto/ed25519 package for the methods on this type. -type PublicKey = ed25519.PublicKey - -// PrivateKey is the type of Ed25519 private keys. It implements crypto.Signer. -// -// This type is an alias for crypto/ed25519's PrivateKey type. -// See the crypto/ed25519 package for the methods on this type. -type PrivateKey = ed25519.PrivateKey - -// GenerateKey generates a public/private key pair using entropy from rand. -// If rand is nil, crypto/rand.Reader will be used. -func GenerateKey(rand io.Reader) (PublicKey, PrivateKey, error) { - return ed25519.GenerateKey(rand) -} - -// NewKeyFromSeed calculates a private key from a seed. It will panic if -// len(seed) is not SeedSize. This function is provided for interoperability -// with RFC 8032. RFC 8032's private keys correspond to seeds in this -// package. -func NewKeyFromSeed(seed []byte) PrivateKey { - return ed25519.NewKeyFromSeed(seed) -} - -// Sign signs the message with privateKey and returns a signature. It will -// panic if len(privateKey) is not PrivateKeySize. -func Sign(privateKey PrivateKey, message []byte) []byte { - return ed25519.Sign(privateKey, message) -} - -// Verify reports whether sig is a valid signature of message by publicKey. It -// will panic if len(publicKey) is not PublicKeySize. -func Verify(publicKey PublicKey, message, sig []byte) bool { - return ed25519.Verify(publicKey, message, sig) -} diff --git a/vendor/golang.org/x/crypto/sha3/sha3.go b/vendor/golang.org/x/crypto/sha3/sha3.go index fa182beb40..4884d172a4 100644 --- a/vendor/golang.org/x/crypto/sha3/sha3.go +++ b/vendor/golang.org/x/crypto/sha3/sha3.go @@ -121,11 +121,11 @@ func (d *state) padAndPermute(dsbyte byte) { copyOut(d, d.buf) } -// Write absorbs more data into the hash's state. It produces an error -// if more data is written to the ShakeHash after writing +// Write absorbs more data into the hash's state. It panics if any +// output has already been read. func (d *state) Write(p []byte) (written int, err error) { if d.state != spongeAbsorbing { - panic("sha3: write to sponge after read") + panic("sha3: Write after Read") } if d.buf == nil { d.buf = d.storage.asBytes()[:0] @@ -182,12 +182,16 @@ func (d *state) Read(out []byte) (n int, err error) { } // Sum applies padding to the hash state and then squeezes out the desired -// number of output bytes. +// number of output bytes. It panics if any output has already been read. func (d *state) Sum(in []byte) []byte { + if d.state != spongeAbsorbing { + panic("sha3: Sum after Read") + } + // Make a copy of the original hash so that caller can keep writing // and summing. dup := d.clone() - hash := make([]byte, dup.outputLen) + hash := make([]byte, dup.outputLen, 64) // explicit cap to allow stack allocation dup.Read(hash) return append(in, hash...) } diff --git a/vendor/golang.org/x/crypto/sha3/sha3_s390x.go b/vendor/golang.org/x/crypto/sha3/sha3_s390x.go index 63a3edb4ce..ec26f147ff 100644 --- a/vendor/golang.org/x/crypto/sha3/sha3_s390x.go +++ b/vendor/golang.org/x/crypto/sha3/sha3_s390x.go @@ -49,7 +49,7 @@ type asmState struct { buf []byte // care must be taken to ensure cap(buf) is a multiple of rate rate int // equivalent to block size storage [3072]byte // underlying storage for buf - outputLen int // output length if fixed, 0 if not + outputLen int // output length for full security function code // KIMD/KLMD function code state spongeDirection // whether the sponge is absorbing or squeezing } @@ -72,8 +72,10 @@ func newAsmState(function code) *asmState { s.outputLen = 64 case shake_128: s.rate = 168 + s.outputLen = 32 case shake_256: s.rate = 136 + s.outputLen = 64 default: panic("sha3: unrecognized function code") } @@ -108,7 +110,7 @@ func (s *asmState) resetBuf() { // It never returns an error. func (s *asmState) Write(b []byte) (int, error) { if s.state != spongeAbsorbing { - panic("sha3: write to sponge after read") + panic("sha3: Write after Read") } length := len(b) for len(b) > 0 { @@ -192,8 +194,8 @@ func (s *asmState) Read(out []byte) (n int, err error) { // Sum appends the current hash to b and returns the resulting slice. // It does not change the underlying hash state. func (s *asmState) Sum(b []byte) []byte { - if s.outputLen == 0 { - panic("sha3: cannot call Sum on SHAKE functions") + if s.state != spongeAbsorbing { + panic("sha3: Sum after Read") } // Copy the state to preserve the original. diff --git a/vendor/golang.org/x/crypto/sha3/shake.go b/vendor/golang.org/x/crypto/sha3/shake.go index d7be2954ab..bb69984027 100644 --- a/vendor/golang.org/x/crypto/sha3/shake.go +++ b/vendor/golang.org/x/crypto/sha3/shake.go @@ -17,26 +17,25 @@ package sha3 import ( "encoding/binary" + "hash" "io" ) -// ShakeHash defines the interface to hash functions that -// support arbitrary-length output. +// ShakeHash defines the interface to hash functions that support +// arbitrary-length output. When used as a plain [hash.Hash], it +// produces minimum-length outputs that provide full-strength generic +// security. type ShakeHash interface { - // Write absorbs more data into the hash's state. It panics if input is - // written to it after output has been read from it. - io.Writer + hash.Hash // Read reads more output from the hash; reading affects the hash's // state. (ShakeHash.Read is thus very different from Hash.Sum) - // It never returns an error. + // It never returns an error, but subsequent calls to Write or Sum + // will panic. io.Reader // Clone returns a copy of the ShakeHash in its current state. Clone() ShakeHash - - // Reset resets the ShakeHash to its initial state. - Reset() } // cSHAKE specific context @@ -81,8 +80,8 @@ func leftEncode(value uint64) []byte { return b[i-1:] } -func newCShake(N, S []byte, rate int, dsbyte byte) ShakeHash { - c := cshakeState{state: &state{rate: rate, dsbyte: dsbyte}} +func newCShake(N, S []byte, rate, outputLen int, dsbyte byte) ShakeHash { + c := cshakeState{state: &state{rate: rate, outputLen: outputLen, dsbyte: dsbyte}} // leftEncode returns max 9 bytes c.initBlock = make([]byte, 0, 9*2+len(N)+len(S)) @@ -119,7 +118,7 @@ func NewShake128() ShakeHash { if h := newShake128Asm(); h != nil { return h } - return &state{rate: rate128, dsbyte: dsbyteShake} + return &state{rate: rate128, outputLen: 32, dsbyte: dsbyteShake} } // NewShake256 creates a new SHAKE256 variable-output-length ShakeHash. @@ -129,7 +128,7 @@ func NewShake256() ShakeHash { if h := newShake256Asm(); h != nil { return h } - return &state{rate: rate256, dsbyte: dsbyteShake} + return &state{rate: rate256, outputLen: 64, dsbyte: dsbyteShake} } // NewCShake128 creates a new instance of cSHAKE128 variable-output-length ShakeHash, @@ -142,7 +141,7 @@ func NewCShake128(N, S []byte) ShakeHash { if len(N) == 0 && len(S) == 0 { return NewShake128() } - return newCShake(N, S, rate128, dsbyteCShake) + return newCShake(N, S, rate128, 32, dsbyteCShake) } // NewCShake256 creates a new instance of cSHAKE256 variable-output-length ShakeHash, @@ -155,7 +154,7 @@ func NewCShake256(N, S []byte) ShakeHash { if len(N) == 0 && len(S) == 0 { return NewShake256() } - return newCShake(N, S, rate256, dsbyteCShake) + return newCShake(N, S, rate256, 64, dsbyteCShake) } // ShakeSum128 writes an arbitrary-length digest of data into hash. diff --git a/vendor/golang.org/x/crypto/ssh/agent/client.go b/vendor/golang.org/x/crypto/ssh/agent/client.go index c3e112a939..9f09aae7dd 100644 --- a/vendor/golang.org/x/crypto/ssh/agent/client.go +++ b/vendor/golang.org/x/crypto/ssh/agent/client.go @@ -16,6 +16,7 @@ import ( "bytes" "crypto/dsa" "crypto/ecdsa" + "crypto/ed25519" "crypto/elliptic" "crypto/rsa" "encoding/base64" @@ -26,7 +27,6 @@ import ( "math/big" "sync" - "golang.org/x/crypto/ed25519" "golang.org/x/crypto/ssh" ) diff --git a/vendor/golang.org/x/crypto/ssh/agent/server.go b/vendor/golang.org/x/crypto/ssh/agent/server.go index 9a769de03d..dd2e0a3e71 100644 --- a/vendor/golang.org/x/crypto/ssh/agent/server.go +++ b/vendor/golang.org/x/crypto/ssh/agent/server.go @@ -7,6 +7,7 @@ package agent import ( "crypto/dsa" "crypto/ecdsa" + "crypto/ed25519" "crypto/elliptic" "crypto/rsa" "encoding/binary" @@ -16,7 +17,6 @@ import ( "log" "math/big" - "golang.org/x/crypto/ed25519" "golang.org/x/crypto/ssh" ) diff --git a/vendor/golang.org/x/crypto/ssh/certs.go b/vendor/golang.org/x/crypto/ssh/certs.go index fc04d03e19..27d0e14aa9 100644 --- a/vendor/golang.org/x/crypto/ssh/certs.go +++ b/vendor/golang.org/x/crypto/ssh/certs.go @@ -16,8 +16,9 @@ import ( // Certificate algorithm names from [PROTOCOL.certkeys]. These values can appear // in Certificate.Type, PublicKey.Type, and ClientConfig.HostKeyAlgorithms. -// Unlike key algorithm names, these are not passed to AlgorithmSigner and don't -// appear in the Signature.Format field. +// Unlike key algorithm names, these are not passed to AlgorithmSigner nor +// returned by MultiAlgorithmSigner and don't appear in the Signature.Format +// field. const ( CertAlgoRSAv01 = "ssh-rsa-cert-v01@openssh.com" CertAlgoDSAv01 = "ssh-dss-cert-v01@openssh.com" @@ -255,10 +256,17 @@ func NewCertSigner(cert *Certificate, signer Signer) (Signer, error) { return nil, errors.New("ssh: signer and cert have different public key") } - if algorithmSigner, ok := signer.(AlgorithmSigner); ok { + switch s := signer.(type) { + case MultiAlgorithmSigner: + return &multiAlgorithmSigner{ + AlgorithmSigner: &algorithmOpenSSHCertSigner{ + &openSSHCertSigner{cert, signer}, s}, + supportedAlgorithms: s.Algorithms(), + }, nil + case AlgorithmSigner: return &algorithmOpenSSHCertSigner{ - &openSSHCertSigner{cert, signer}, algorithmSigner}, nil - } else { + &openSSHCertSigner{cert, signer}, s}, nil + default: return &openSSHCertSigner{cert, signer}, nil } } @@ -432,7 +440,9 @@ func (c *CertChecker) CheckCert(principal string, cert *Certificate) error { } // SignCert signs the certificate with an authority, setting the Nonce, -// SignatureKey, and Signature fields. +// SignatureKey, and Signature fields. If the authority implements the +// MultiAlgorithmSigner interface the first algorithm in the list is used. This +// is useful if you want to sign with a specific algorithm. func (c *Certificate) SignCert(rand io.Reader, authority Signer) error { c.Nonce = make([]byte, 32) if _, err := io.ReadFull(rand, c.Nonce); err != nil { @@ -440,8 +450,20 @@ func (c *Certificate) SignCert(rand io.Reader, authority Signer) error { } c.SignatureKey = authority.PublicKey() - // Default to KeyAlgoRSASHA512 for ssh-rsa signers. - if v, ok := authority.(AlgorithmSigner); ok && v.PublicKey().Type() == KeyAlgoRSA { + if v, ok := authority.(MultiAlgorithmSigner); ok { + if len(v.Algorithms()) == 0 { + return errors.New("the provided authority has no signature algorithm") + } + // Use the first algorithm in the list. + sig, err := v.SignWithAlgorithm(rand, c.bytesForSigning(), v.Algorithms()[0]) + if err != nil { + return err + } + c.Signature = sig + return nil + } else if v, ok := authority.(AlgorithmSigner); ok && v.PublicKey().Type() == KeyAlgoRSA { + // Default to KeyAlgoRSASHA512 for ssh-rsa signers. + // TODO: consider using KeyAlgoRSASHA256 as default. sig, err := v.SignWithAlgorithm(rand, c.bytesForSigning(), KeyAlgoRSASHA512) if err != nil { return err diff --git a/vendor/golang.org/x/crypto/ssh/client_auth.go b/vendor/golang.org/x/crypto/ssh/client_auth.go index 409b5ea1d4..5c3bc25723 100644 --- a/vendor/golang.org/x/crypto/ssh/client_auth.go +++ b/vendor/golang.org/x/crypto/ssh/client_auth.go @@ -71,7 +71,9 @@ func (c *connection) clientAuthenticate(config *ClientConfig) error { for auth := AuthMethod(new(noneAuth)); auth != nil; { ok, methods, err := auth.auth(sessionID, config.User, c.transport, config.Rand, extensions) if err != nil { - return err + // We return the error later if there is no other method left to + // try. + ok = authFailure } if ok == authSuccess { // success @@ -101,6 +103,12 @@ func (c *connection) clientAuthenticate(config *ClientConfig) error { } } } + + if auth == nil && err != nil { + // We have an error and there are no other authentication methods to + // try, so we return it. + return err + } } return fmt.Errorf("ssh: unable to authenticate, attempted methods %v, no supported methods remain", tried) } @@ -217,21 +225,45 @@ func (cb publicKeyCallback) method() string { return "publickey" } -func pickSignatureAlgorithm(signer Signer, extensions map[string][]byte) (as AlgorithmSigner, algo string) { +func pickSignatureAlgorithm(signer Signer, extensions map[string][]byte) (MultiAlgorithmSigner, string, error) { + var as MultiAlgorithmSigner keyFormat := signer.PublicKey().Type() - // Like in sendKexInit, if the public key implements AlgorithmSigner we - // assume it supports all algorithms, otherwise only the key format one. - as, ok := signer.(AlgorithmSigner) - if !ok { - return algorithmSignerWrapper{signer}, keyFormat + // If the signer implements MultiAlgorithmSigner we use the algorithms it + // support, if it implements AlgorithmSigner we assume it supports all + // algorithms, otherwise only the key format one. + switch s := signer.(type) { + case MultiAlgorithmSigner: + as = s + case AlgorithmSigner: + as = &multiAlgorithmSigner{ + AlgorithmSigner: s, + supportedAlgorithms: algorithmsForKeyFormat(underlyingAlgo(keyFormat)), + } + default: + as = &multiAlgorithmSigner{ + AlgorithmSigner: algorithmSignerWrapper{signer}, + supportedAlgorithms: []string{underlyingAlgo(keyFormat)}, + } + } + + getFallbackAlgo := func() (string, error) { + // Fallback to use if there is no "server-sig-algs" extension or a + // common algorithm cannot be found. We use the public key format if the + // MultiAlgorithmSigner supports it, otherwise we return an error. + if !contains(as.Algorithms(), underlyingAlgo(keyFormat)) { + return "", fmt.Errorf("ssh: no common public key signature algorithm, server only supports %q for key type %q, signer only supports %v", + underlyingAlgo(keyFormat), keyFormat, as.Algorithms()) + } + return keyFormat, nil } extPayload, ok := extensions["server-sig-algs"] if !ok { - // If there is no "server-sig-algs" extension, fall back to the key - // format algorithm. - return as, keyFormat + // If there is no "server-sig-algs" extension use the fallback + // algorithm. + algo, err := getFallbackAlgo() + return as, algo, err } // The server-sig-algs extension only carries underlying signature @@ -245,15 +277,22 @@ func pickSignatureAlgorithm(signer Signer, extensions map[string][]byte) (as Alg } } - keyAlgos := algorithmsForKeyFormat(keyFormat) + // Filter algorithms based on those supported by MultiAlgorithmSigner. + var keyAlgos []string + for _, algo := range algorithmsForKeyFormat(keyFormat) { + if contains(as.Algorithms(), underlyingAlgo(algo)) { + keyAlgos = append(keyAlgos, algo) + } + } + algo, err := findCommon("public key signature algorithm", keyAlgos, serverAlgos) if err != nil { - // If there is no overlap, try the key anyway with the key format - // algorithm, to support servers that fail to list all supported - // algorithms. - return as, keyFormat + // If there is no overlap, return the fallback algorithm to support + // servers that fail to list all supported algorithms. + algo, err := getFallbackAlgo() + return as, algo, err } - return as, algo + return as, algo, nil } func (cb publicKeyCallback) auth(session []byte, user string, c packetConn, rand io.Reader, extensions map[string][]byte) (authResult, []string, error) { @@ -267,10 +306,17 @@ func (cb publicKeyCallback) auth(session []byte, user string, c packetConn, rand return authFailure, nil, err } var methods []string + var errSigAlgo error for _, signer := range signers { pub := signer.PublicKey() - as, algo := pickSignatureAlgorithm(signer, extensions) - + as, algo, err := pickSignatureAlgorithm(signer, extensions) + if err != nil && errSigAlgo == nil { + // If we cannot negotiate a signature algorithm store the first + // error so we can return it to provide a more meaningful message if + // no other signers work. + errSigAlgo = err + continue + } ok, err := validateKey(pub, algo, user, c) if err != nil { return authFailure, nil, err @@ -317,22 +363,12 @@ func (cb publicKeyCallback) auth(session []byte, user string, c packetConn, rand // contain the "publickey" method, do not attempt to authenticate with any // other keys. According to RFC 4252 Section 7, the latter can occur when // additional authentication methods are required. - if success == authSuccess || !containsMethod(methods, cb.method()) { + if success == authSuccess || !contains(methods, cb.method()) { return success, methods, err } } - return authFailure, methods, nil -} - -func containsMethod(methods []string, method string) bool { - for _, m := range methods { - if m == method { - return true - } - } - - return false + return authFailure, methods, errSigAlgo } // validateKey validates the key provided is acceptable to the server. diff --git a/vendor/golang.org/x/crypto/ssh/doc.go b/vendor/golang.org/x/crypto/ssh/doc.go index f6bff60dc7..edbe63340d 100644 --- a/vendor/golang.org/x/crypto/ssh/doc.go +++ b/vendor/golang.org/x/crypto/ssh/doc.go @@ -13,6 +13,7 @@ others. References: + [PROTOCOL]: https://cvsweb.openbsd.org/cgi-bin/cvsweb/src/usr.bin/ssh/PROTOCOL?rev=HEAD [PROTOCOL.certkeys]: http://cvsweb.openbsd.org/cgi-bin/cvsweb/src/usr.bin/ssh/PROTOCOL.certkeys?rev=HEAD [SSH-PARAMETERS]: http://www.iana.org/assignments/ssh-parameters/ssh-parameters.xml#ssh-parameters-1 diff --git a/vendor/golang.org/x/crypto/ssh/handshake.go b/vendor/golang.org/x/crypto/ssh/handshake.go index 07a1843e0a..70a7369ff9 100644 --- a/vendor/golang.org/x/crypto/ssh/handshake.go +++ b/vendor/golang.org/x/crypto/ssh/handshake.go @@ -461,19 +461,24 @@ func (t *handshakeTransport) sendKexInit() error { isServer := len(t.hostKeys) > 0 if isServer { for _, k := range t.hostKeys { - // If k is an AlgorithmSigner, presume it supports all signature algorithms - // associated with the key format. (Ideally AlgorithmSigner would have a - // method to advertise supported algorithms, but it doesn't. This means that - // adding support for a new algorithm is a breaking change, as we will - // immediately negotiate it even if existing implementations don't support - // it. If that ever happens, we'll have to figure something out.) - // If k is not an AlgorithmSigner, we can only assume it only supports the - // algorithms that matches the key format. (This means that Sign can't pick - // a different default.) + // If k is a MultiAlgorithmSigner, we restrict the signature + // algorithms. If k is a AlgorithmSigner, presume it supports all + // signature algorithms associated with the key format. If k is not + // an AlgorithmSigner, we can only assume it only supports the + // algorithms that matches the key format. (This means that Sign + // can't pick a different default). keyFormat := k.PublicKey().Type() - if _, ok := k.(AlgorithmSigner); ok { + + switch s := k.(type) { + case MultiAlgorithmSigner: + for _, algo := range algorithmsForKeyFormat(keyFormat) { + if contains(s.Algorithms(), underlyingAlgo(algo)) { + msg.ServerHostKeyAlgos = append(msg.ServerHostKeyAlgos, algo) + } + } + case AlgorithmSigner: msg.ServerHostKeyAlgos = append(msg.ServerHostKeyAlgos, algorithmsForKeyFormat(keyFormat)...) - } else { + default: msg.ServerHostKeyAlgos = append(msg.ServerHostKeyAlgos, keyFormat) } } @@ -642,16 +647,20 @@ func (t *handshakeTransport) enterKeyExchange(otherInitPacket []byte) error { // On the server side, after the first SSH_MSG_NEWKEYS, send a SSH_MSG_EXT_INFO // message with the server-sig-algs extension if the client supports it. See - // RFC 8308, Sections 2.4 and 3.1. + // RFC 8308, Sections 2.4 and 3.1, and [PROTOCOL], Section 1.9. if !isClient && firstKeyExchange && contains(clientInit.KexAlgos, "ext-info-c") { extInfo := &extInfoMsg{ - NumExtensions: 1, - Payload: make([]byte, 0, 4+15+4+len(supportedPubKeyAuthAlgosList)), + NumExtensions: 2, + Payload: make([]byte, 0, 4+15+4+len(supportedPubKeyAuthAlgosList)+4+16+4+1), } extInfo.Payload = appendInt(extInfo.Payload, len("server-sig-algs")) extInfo.Payload = append(extInfo.Payload, "server-sig-algs"...) extInfo.Payload = appendInt(extInfo.Payload, len(supportedPubKeyAuthAlgosList)) extInfo.Payload = append(extInfo.Payload, supportedPubKeyAuthAlgosList...) + extInfo.Payload = appendInt(extInfo.Payload, len("ping@openssh.com")) + extInfo.Payload = append(extInfo.Payload, "ping@openssh.com"...) + extInfo.Payload = appendInt(extInfo.Payload, 1) + extInfo.Payload = append(extInfo.Payload, "0"...) if err := t.conn.writePacket(Marshal(extInfo)); err != nil { return err } @@ -685,9 +694,16 @@ func (a algorithmSignerWrapper) SignWithAlgorithm(rand io.Reader, data []byte, a func pickHostKey(hostKeys []Signer, algo string) AlgorithmSigner { for _, k := range hostKeys { + if s, ok := k.(MultiAlgorithmSigner); ok { + if !contains(s.Algorithms(), underlyingAlgo(algo)) { + continue + } + } + if algo == k.PublicKey().Type() { return algorithmSignerWrapper{k} } + k, ok := k.(AlgorithmSigner) if !ok { continue diff --git a/vendor/golang.org/x/crypto/ssh/keys.go b/vendor/golang.org/x/crypto/ssh/keys.go index dac8ee7244..ef1bad731b 100644 --- a/vendor/golang.org/x/crypto/ssh/keys.go +++ b/vendor/golang.org/x/crypto/ssh/keys.go @@ -11,13 +11,16 @@ import ( "crypto/cipher" "crypto/dsa" "crypto/ecdsa" + "crypto/ed25519" "crypto/elliptic" "crypto/md5" + "crypto/rand" "crypto/rsa" "crypto/sha256" "crypto/x509" "encoding/asn1" "encoding/base64" + "encoding/binary" "encoding/hex" "encoding/pem" "errors" @@ -26,7 +29,6 @@ import ( "math/big" "strings" - "golang.org/x/crypto/ed25519" "golang.org/x/crypto/ssh/internal/bcrypt_pbkdf" ) @@ -295,6 +297,18 @@ func MarshalAuthorizedKey(key PublicKey) []byte { return b.Bytes() } +// MarshalPrivateKey returns a PEM block with the private key serialized in the +// OpenSSH format. +func MarshalPrivateKey(key crypto.PrivateKey, comment string) (*pem.Block, error) { + return marshalOpenSSHPrivateKey(key, comment, unencryptedOpenSSHMarshaler) +} + +// MarshalPrivateKeyWithPassphrase returns a PEM block holding the encrypted +// private key serialized in the OpenSSH format. +func MarshalPrivateKeyWithPassphrase(key crypto.PrivateKey, comment string, passphrase []byte) (*pem.Block, error) { + return marshalOpenSSHPrivateKey(key, comment, passphraseProtectedOpenSSHMarshaler(passphrase)) +} + // PublicKey represents a public key using an unspecified algorithm. // // Some PublicKeys provided by this package also implement CryptoPublicKey. @@ -321,7 +335,7 @@ type CryptoPublicKey interface { // A Signer can create signatures that verify against a public key. // -// Some Signers provided by this package also implement AlgorithmSigner. +// Some Signers provided by this package also implement MultiAlgorithmSigner. type Signer interface { // PublicKey returns the associated PublicKey. PublicKey() PublicKey @@ -336,9 +350,9 @@ type Signer interface { // An AlgorithmSigner is a Signer that also supports specifying an algorithm to // use for signing. // -// An AlgorithmSigner can't advertise the algorithms it supports, so it should -// be prepared to be invoked with every algorithm supported by the public key -// format. +// An AlgorithmSigner can't advertise the algorithms it supports, unless it also +// implements MultiAlgorithmSigner, so it should be prepared to be invoked with +// every algorithm supported by the public key format. type AlgorithmSigner interface { Signer @@ -349,6 +363,75 @@ type AlgorithmSigner interface { SignWithAlgorithm(rand io.Reader, data []byte, algorithm string) (*Signature, error) } +// MultiAlgorithmSigner is an AlgorithmSigner that also reports the algorithms +// supported by that signer. +type MultiAlgorithmSigner interface { + AlgorithmSigner + + // Algorithms returns the available algorithms in preference order. The list + // must not be empty, and it must not include certificate types. + Algorithms() []string +} + +// NewSignerWithAlgorithms returns a signer restricted to the specified +// algorithms. The algorithms must be set in preference order. The list must not +// be empty, and it must not include certificate types. An error is returned if +// the specified algorithms are incompatible with the public key type. +func NewSignerWithAlgorithms(signer AlgorithmSigner, algorithms []string) (MultiAlgorithmSigner, error) { + if len(algorithms) == 0 { + return nil, errors.New("ssh: please specify at least one valid signing algorithm") + } + var signerAlgos []string + supportedAlgos := algorithmsForKeyFormat(underlyingAlgo(signer.PublicKey().Type())) + if s, ok := signer.(*multiAlgorithmSigner); ok { + signerAlgos = s.Algorithms() + } else { + signerAlgos = supportedAlgos + } + + for _, algo := range algorithms { + if !contains(supportedAlgos, algo) { + return nil, fmt.Errorf("ssh: algorithm %q is not supported for key type %q", + algo, signer.PublicKey().Type()) + } + if !contains(signerAlgos, algo) { + return nil, fmt.Errorf("ssh: algorithm %q is restricted for the provided signer", algo) + } + } + return &multiAlgorithmSigner{ + AlgorithmSigner: signer, + supportedAlgorithms: algorithms, + }, nil +} + +type multiAlgorithmSigner struct { + AlgorithmSigner + supportedAlgorithms []string +} + +func (s *multiAlgorithmSigner) Algorithms() []string { + return s.supportedAlgorithms +} + +func (s *multiAlgorithmSigner) isAlgorithmSupported(algorithm string) bool { + if algorithm == "" { + algorithm = underlyingAlgo(s.PublicKey().Type()) + } + for _, algo := range s.supportedAlgorithms { + if algorithm == algo { + return true + } + } + return false +} + +func (s *multiAlgorithmSigner) SignWithAlgorithm(rand io.Reader, data []byte, algorithm string) (*Signature, error) { + if !s.isAlgorithmSupported(algorithm) { + return nil, fmt.Errorf("ssh: algorithm %q is not supported: %v", algorithm, s.supportedAlgorithms) + } + return s.AlgorithmSigner.SignWithAlgorithm(rand, data, algorithm) +} + type rsaPublicKey rsa.PublicKey func (r *rsaPublicKey) Type() string { @@ -512,6 +595,10 @@ func (k *dsaPrivateKey) Sign(rand io.Reader, data []byte) (*Signature, error) { return k.SignWithAlgorithm(rand, data, k.PublicKey().Type()) } +func (k *dsaPrivateKey) Algorithms() []string { + return []string{k.PublicKey().Type()} +} + func (k *dsaPrivateKey) SignWithAlgorithm(rand io.Reader, data []byte, algorithm string) (*Signature, error) { if algorithm != "" && algorithm != k.PublicKey().Type() { return nil, fmt.Errorf("ssh: unsupported signature algorithm %s", algorithm) @@ -961,13 +1048,16 @@ func (s *wrappedSigner) Sign(rand io.Reader, data []byte) (*Signature, error) { return s.SignWithAlgorithm(rand, data, s.pubKey.Type()) } +func (s *wrappedSigner) Algorithms() []string { + return algorithmsForKeyFormat(s.pubKey.Type()) +} + func (s *wrappedSigner) SignWithAlgorithm(rand io.Reader, data []byte, algorithm string) (*Signature, error) { if algorithm == "" { algorithm = s.pubKey.Type() } - supportedAlgos := algorithmsForKeyFormat(s.pubKey.Type()) - if !contains(supportedAlgos, algorithm) { + if !contains(s.Algorithms(), algorithm) { return nil, fmt.Errorf("ssh: unsupported signature algorithm %q for key format %q", algorithm, s.pubKey.Type()) } @@ -1241,28 +1331,106 @@ func passphraseProtectedOpenSSHKey(passphrase []byte) openSSHDecryptFunc { } } +func unencryptedOpenSSHMarshaler(privKeyBlock []byte) ([]byte, string, string, string, error) { + key := generateOpenSSHPadding(privKeyBlock, 8) + return key, "none", "none", "", nil +} + +func passphraseProtectedOpenSSHMarshaler(passphrase []byte) openSSHEncryptFunc { + return func(privKeyBlock []byte) ([]byte, string, string, string, error) { + salt := make([]byte, 16) + if _, err := rand.Read(salt); err != nil { + return nil, "", "", "", err + } + + opts := struct { + Salt []byte + Rounds uint32 + }{salt, 16} + + // Derive key to encrypt the private key block. + k, err := bcrypt_pbkdf.Key(passphrase, salt, int(opts.Rounds), 32+aes.BlockSize) + if err != nil { + return nil, "", "", "", err + } + + // Add padding matching the block size of AES. + keyBlock := generateOpenSSHPadding(privKeyBlock, aes.BlockSize) + + // Encrypt the private key using the derived secret. + + dst := make([]byte, len(keyBlock)) + key, iv := k[:32], k[32:] + block, err := aes.NewCipher(key) + if err != nil { + return nil, "", "", "", err + } + + stream := cipher.NewCTR(block, iv) + stream.XORKeyStream(dst, keyBlock) + + return dst, "aes256-ctr", "bcrypt", string(Marshal(opts)), nil + } +} + +const privateKeyAuthMagic = "openssh-key-v1\x00" + type openSSHDecryptFunc func(CipherName, KdfName, KdfOpts string, PrivKeyBlock []byte) ([]byte, error) +type openSSHEncryptFunc func(PrivKeyBlock []byte) (ProtectedKeyBlock []byte, cipherName, kdfName, kdfOptions string, err error) + +type openSSHEncryptedPrivateKey struct { + CipherName string + KdfName string + KdfOpts string + NumKeys uint32 + PubKey []byte + PrivKeyBlock []byte +} + +type openSSHPrivateKey struct { + Check1 uint32 + Check2 uint32 + Keytype string + Rest []byte `ssh:"rest"` +} + +type openSSHRSAPrivateKey struct { + N *big.Int + E *big.Int + D *big.Int + Iqmp *big.Int + P *big.Int + Q *big.Int + Comment string + Pad []byte `ssh:"rest"` +} + +type openSSHEd25519PrivateKey struct { + Pub []byte + Priv []byte + Comment string + Pad []byte `ssh:"rest"` +} + +type openSSHECDSAPrivateKey struct { + Curve string + Pub []byte + D *big.Int + Comment string + Pad []byte `ssh:"rest"` +} // parseOpenSSHPrivateKey parses an OpenSSH private key, using the decrypt // function to unwrap the encrypted portion. unencryptedOpenSSHKey can be used // as the decrypt function to parse an unencrypted private key. See // https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.key. func parseOpenSSHPrivateKey(key []byte, decrypt openSSHDecryptFunc) (crypto.PrivateKey, error) { - const magic = "openssh-key-v1\x00" - if len(key) < len(magic) || string(key[:len(magic)]) != magic { + if len(key) < len(privateKeyAuthMagic) || string(key[:len(privateKeyAuthMagic)]) != privateKeyAuthMagic { return nil, errors.New("ssh: invalid openssh private key format") } - remaining := key[len(magic):] - - var w struct { - CipherName string - KdfName string - KdfOpts string - NumKeys uint32 - PubKey []byte - PrivKeyBlock []byte - } + remaining := key[len(privateKeyAuthMagic):] + var w openSSHEncryptedPrivateKey if err := Unmarshal(remaining, &w); err != nil { return nil, err } @@ -1284,13 +1452,7 @@ func parseOpenSSHPrivateKey(key []byte, decrypt openSSHDecryptFunc) (crypto.Priv return nil, err } - pk1 := struct { - Check1 uint32 - Check2 uint32 - Keytype string - Rest []byte `ssh:"rest"` - }{} - + var pk1 openSSHPrivateKey if err := Unmarshal(privKeyBlock, &pk1); err != nil || pk1.Check1 != pk1.Check2 { if w.CipherName != "none" { return nil, x509.IncorrectPasswordError @@ -1300,18 +1462,7 @@ func parseOpenSSHPrivateKey(key []byte, decrypt openSSHDecryptFunc) (crypto.Priv switch pk1.Keytype { case KeyAlgoRSA: - // https://github.com/openssh/openssh-portable/blob/master/sshkey.c#L2760-L2773 - key := struct { - N *big.Int - E *big.Int - D *big.Int - Iqmp *big.Int - P *big.Int - Q *big.Int - Comment string - Pad []byte `ssh:"rest"` - }{} - + var key openSSHRSAPrivateKey if err := Unmarshal(pk1.Rest, &key); err != nil { return nil, err } @@ -1337,13 +1488,7 @@ func parseOpenSSHPrivateKey(key []byte, decrypt openSSHDecryptFunc) (crypto.Priv return pk, nil case KeyAlgoED25519: - key := struct { - Pub []byte - Priv []byte - Comment string - Pad []byte `ssh:"rest"` - }{} - + var key openSSHEd25519PrivateKey if err := Unmarshal(pk1.Rest, &key); err != nil { return nil, err } @@ -1360,14 +1505,7 @@ func parseOpenSSHPrivateKey(key []byte, decrypt openSSHDecryptFunc) (crypto.Priv copy(pk, key.Priv) return &pk, nil case KeyAlgoECDSA256, KeyAlgoECDSA384, KeyAlgoECDSA521: - key := struct { - Curve string - Pub []byte - D *big.Int - Comment string - Pad []byte `ssh:"rest"` - }{} - + var key openSSHECDSAPrivateKey if err := Unmarshal(pk1.Rest, &key); err != nil { return nil, err } @@ -1415,6 +1553,131 @@ func parseOpenSSHPrivateKey(key []byte, decrypt openSSHDecryptFunc) (crypto.Priv } } +func marshalOpenSSHPrivateKey(key crypto.PrivateKey, comment string, encrypt openSSHEncryptFunc) (*pem.Block, error) { + var w openSSHEncryptedPrivateKey + var pk1 openSSHPrivateKey + + // Random check bytes. + var check uint32 + if err := binary.Read(rand.Reader, binary.BigEndian, &check); err != nil { + return nil, err + } + + pk1.Check1 = check + pk1.Check2 = check + w.NumKeys = 1 + + // Use a []byte directly on ed25519 keys. + if k, ok := key.(*ed25519.PrivateKey); ok { + key = *k + } + + switch k := key.(type) { + case *rsa.PrivateKey: + E := new(big.Int).SetInt64(int64(k.PublicKey.E)) + // Marshal public key: + // E and N are in reversed order in the public and private key. + pubKey := struct { + KeyType string + E *big.Int + N *big.Int + }{ + KeyAlgoRSA, + E, k.PublicKey.N, + } + w.PubKey = Marshal(pubKey) + + // Marshal private key. + key := openSSHRSAPrivateKey{ + N: k.PublicKey.N, + E: E, + D: k.D, + Iqmp: k.Precomputed.Qinv, + P: k.Primes[0], + Q: k.Primes[1], + Comment: comment, + } + pk1.Keytype = KeyAlgoRSA + pk1.Rest = Marshal(key) + case ed25519.PrivateKey: + pub := make([]byte, ed25519.PublicKeySize) + priv := make([]byte, ed25519.PrivateKeySize) + copy(pub, k[32:]) + copy(priv, k) + + // Marshal public key. + pubKey := struct { + KeyType string + Pub []byte + }{ + KeyAlgoED25519, pub, + } + w.PubKey = Marshal(pubKey) + + // Marshal private key. + key := openSSHEd25519PrivateKey{ + Pub: pub, + Priv: priv, + Comment: comment, + } + pk1.Keytype = KeyAlgoED25519 + pk1.Rest = Marshal(key) + case *ecdsa.PrivateKey: + var curve, keyType string + switch name := k.Curve.Params().Name; name { + case "P-256": + curve = "nistp256" + keyType = KeyAlgoECDSA256 + case "P-384": + curve = "nistp384" + keyType = KeyAlgoECDSA384 + case "P-521": + curve = "nistp521" + keyType = KeyAlgoECDSA521 + default: + return nil, errors.New("ssh: unhandled elliptic curve " + name) + } + + pub := elliptic.Marshal(k.Curve, k.PublicKey.X, k.PublicKey.Y) + + // Marshal public key. + pubKey := struct { + KeyType string + Curve string + Pub []byte + }{ + keyType, curve, pub, + } + w.PubKey = Marshal(pubKey) + + // Marshal private key. + key := openSSHECDSAPrivateKey{ + Curve: curve, + Pub: pub, + D: k.D, + Comment: comment, + } + pk1.Keytype = keyType + pk1.Rest = Marshal(key) + default: + return nil, fmt.Errorf("ssh: unsupported key type %T", k) + } + + var err error + // Add padding and encrypt the key if necessary. + w.PrivKeyBlock, w.CipherName, w.KdfName, w.KdfOpts, err = encrypt(Marshal(pk1)) + if err != nil { + return nil, err + } + + b := Marshal(w) + block := &pem.Block{ + Type: "OPENSSH PRIVATE KEY", + Bytes: append([]byte(privateKeyAuthMagic), b...), + } + return block, nil +} + func checkOpenSSHKeyPadding(pad []byte) error { for i, b := range pad { if int(b) != i+1 { @@ -1424,6 +1687,13 @@ func checkOpenSSHKeyPadding(pad []byte) error { return nil } +func generateOpenSSHPadding(block []byte, blockSize int) []byte { + for i, l := 0, len(block); (l+i)%blockSize != 0; i++ { + block = append(block, byte(i+1)) + } + return block +} + // FingerprintLegacyMD5 returns the user presentation of the key's // fingerprint as described by RFC 4716 section 4. func FingerprintLegacyMD5(pubKey PublicKey) string { diff --git a/vendor/golang.org/x/crypto/ssh/messages.go b/vendor/golang.org/x/crypto/ssh/messages.go index 922032d952..b55f860564 100644 --- a/vendor/golang.org/x/crypto/ssh/messages.go +++ b/vendor/golang.org/x/crypto/ssh/messages.go @@ -349,6 +349,20 @@ type userAuthGSSAPIError struct { LanguageTag string } +// Transport layer OpenSSH extension. See [PROTOCOL], section 1.9 +const msgPing = 192 + +type pingMsg struct { + Data string `sshtype:"192"` +} + +// Transport layer OpenSSH extension. See [PROTOCOL], section 1.9 +const msgPong = 193 + +type pongMsg struct { + Data string `sshtype:"193"` +} + // typeTags returns the possible type bytes for the given reflect.Type, which // should be a struct. The possible values are separated by a '|' character. func typeTags(structType reflect.Type) (tags []byte) { diff --git a/vendor/golang.org/x/crypto/ssh/mux.go b/vendor/golang.org/x/crypto/ssh/mux.go index 9654c01869..d2d24c635d 100644 --- a/vendor/golang.org/x/crypto/ssh/mux.go +++ b/vendor/golang.org/x/crypto/ssh/mux.go @@ -231,6 +231,12 @@ func (m *mux) onePacket() error { return m.handleChannelOpen(packet) case msgGlobalRequest, msgRequestSuccess, msgRequestFailure: return m.handleGlobalPacket(packet) + case msgPing: + var msg pingMsg + if err := Unmarshal(packet, &msg); err != nil { + return fmt.Errorf("failed to unmarshal ping@openssh.com message: %w", err) + } + return m.sendMessage(pongMsg(msg)) } // assume a channel packet. diff --git a/vendor/golang.org/x/crypto/ssh/server.go b/vendor/golang.org/x/crypto/ssh/server.go index b21322affa..727c71b9c7 100644 --- a/vendor/golang.org/x/crypto/ssh/server.go +++ b/vendor/golang.org/x/crypto/ssh/server.go @@ -576,7 +576,16 @@ userAuthLoop: if !ok || len(payload) > 0 { return nil, parseError(msgUserAuthRequest) } - + // Ensure the declared public key algo is compatible with the + // decoded one. This check will ensure we don't accept e.g. + // ssh-rsa-cert-v01@openssh.com algorithm with ssh-rsa public + // key type. The algorithm and public key type must be + // consistent: both must be certificate algorithms, or neither. + if !contains(algorithmsForKeyFormat(pubKey.Type()), algo) { + authErr = fmt.Errorf("ssh: public key type %q not compatible with selected algorithm %q", + pubKey.Type(), algo) + break + } // Ensure the public key algo and signature algo // are supported. Compare the private key // algorithm name that corresponds to algo with diff --git a/vendor/golang.org/x/net/http2/server.go b/vendor/golang.org/x/net/http2/server.go index 6d5e008874..02c88b6b3e 100644 --- a/vendor/golang.org/x/net/http2/server.go +++ b/vendor/golang.org/x/net/http2/server.go @@ -581,9 +581,11 @@ type serverConn struct { advMaxStreams uint32 // our SETTINGS_MAX_CONCURRENT_STREAMS advertised the client curClientStreams uint32 // number of open streams initiated by the client curPushedStreams uint32 // number of open streams initiated by server push + curHandlers uint32 // number of running handler goroutines maxClientStreamID uint32 // max ever seen from client (odd), or 0 if there have been no client requests maxPushPromiseID uint32 // ID of the last push promise (even), or 0 if there have been no pushes streams map[uint32]*stream + unstartedHandlers []unstartedHandler initialStreamSendWindowSize int32 maxFrameSize int32 peerMaxHeaderListSize uint32 // zero means unknown (default) @@ -981,6 +983,8 @@ func (sc *serverConn) serve() { return case gracefulShutdownMsg: sc.startGracefulShutdownInternal() + case handlerDoneMsg: + sc.handlerDone() default: panic("unknown timer") } @@ -1020,6 +1024,7 @@ var ( idleTimerMsg = new(serverMessage) shutdownTimerMsg = new(serverMessage) gracefulShutdownMsg = new(serverMessage) + handlerDoneMsg = new(serverMessage) ) func (sc *serverConn) onSettingsTimer() { sc.sendServeMsg(settingsTimerMsg) } @@ -1892,9 +1897,11 @@ func (st *stream) copyTrailersToHandlerRequest() { // onReadTimeout is run on its own goroutine (from time.AfterFunc) // when the stream's ReadTimeout has fired. func (st *stream) onReadTimeout() { - // Wrap the ErrDeadlineExceeded to avoid callers depending on us - // returning the bare error. - st.body.CloseWithError(fmt.Errorf("%w", os.ErrDeadlineExceeded)) + if st.body != nil { + // Wrap the ErrDeadlineExceeded to avoid callers depending on us + // returning the bare error. + st.body.CloseWithError(fmt.Errorf("%w", os.ErrDeadlineExceeded)) + } } // onWriteTimeout is run on its own goroutine (from time.AfterFunc) @@ -2012,13 +2019,10 @@ func (sc *serverConn) processHeaders(f *MetaHeadersFrame) error { // (in Go 1.8), though. That's a more sane option anyway. if sc.hs.ReadTimeout != 0 { sc.conn.SetReadDeadline(time.Time{}) - if st.body != nil { - st.readDeadline = time.AfterFunc(sc.hs.ReadTimeout, st.onReadTimeout) - } + st.readDeadline = time.AfterFunc(sc.hs.ReadTimeout, st.onReadTimeout) } - go sc.runHandler(rw, req, handler) - return nil + return sc.scheduleHandler(id, rw, req, handler) } func (sc *serverConn) upgradeRequest(req *http.Request) { @@ -2038,6 +2042,10 @@ func (sc *serverConn) upgradeRequest(req *http.Request) { sc.conn.SetReadDeadline(time.Time{}) } + // This is the first request on the connection, + // so start the handler directly rather than going + // through scheduleHandler. + sc.curHandlers++ go sc.runHandler(rw, req, sc.handler.ServeHTTP) } @@ -2278,8 +2286,62 @@ func (sc *serverConn) newResponseWriter(st *stream, req *http.Request) *response return &responseWriter{rws: rws} } +type unstartedHandler struct { + streamID uint32 + rw *responseWriter + req *http.Request + handler func(http.ResponseWriter, *http.Request) +} + +// scheduleHandler starts a handler goroutine, +// or schedules one to start as soon as an existing handler finishes. +func (sc *serverConn) scheduleHandler(streamID uint32, rw *responseWriter, req *http.Request, handler func(http.ResponseWriter, *http.Request)) error { + sc.serveG.check() + maxHandlers := sc.advMaxStreams + if sc.curHandlers < maxHandlers { + sc.curHandlers++ + go sc.runHandler(rw, req, handler) + return nil + } + if len(sc.unstartedHandlers) > int(4*sc.advMaxStreams) { + return sc.countError("too_many_early_resets", ConnectionError(ErrCodeEnhanceYourCalm)) + } + sc.unstartedHandlers = append(sc.unstartedHandlers, unstartedHandler{ + streamID: streamID, + rw: rw, + req: req, + handler: handler, + }) + return nil +} + +func (sc *serverConn) handlerDone() { + sc.serveG.check() + sc.curHandlers-- + i := 0 + maxHandlers := sc.advMaxStreams + for ; i < len(sc.unstartedHandlers); i++ { + u := sc.unstartedHandlers[i] + if sc.streams[u.streamID] == nil { + // This stream was reset before its goroutine had a chance to start. + continue + } + if sc.curHandlers >= maxHandlers { + break + } + sc.curHandlers++ + go sc.runHandler(u.rw, u.req, u.handler) + sc.unstartedHandlers[i] = unstartedHandler{} // don't retain references + } + sc.unstartedHandlers = sc.unstartedHandlers[i:] + if len(sc.unstartedHandlers) == 0 { + sc.unstartedHandlers = nil + } +} + // Run on its own goroutine. func (sc *serverConn) runHandler(rw *responseWriter, req *http.Request, handler func(http.ResponseWriter, *http.Request)) { + defer sc.sendServeMsg(handlerDoneMsg) didPanic := true defer func() { rw.rws.stream.cancelCtx() diff --git a/vendor/modules.txt b/vendor/modules.txt index 5066fbc413..ae5fad4862 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -49,7 +49,7 @@ github.com/StackExchange/wmi # github.com/acomagu/bufpipe v1.0.4 ## explicit; go 1.12 github.com/acomagu/bufpipe -# github.com/actiontech/dms v0.0.0-20240426094735-693e797b6293 +# github.com/actiontech/dms v0.0.0-20240830025931-20d313b918e8 ## explicit; go 1.19 github.com/actiontech/dms/pkg/dms-common/api/accesstoken github.com/actiontech/dms/pkg/dms-common/api/base/v1 @@ -207,9 +207,6 @@ github.com/gabriel-vasile/mimetype github.com/gabriel-vasile/mimetype/internal/charset github.com/gabriel-vasile/mimetype/internal/json github.com/gabriel-vasile/mimetype/internal/magic -# github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32 -## explicit -github.com/ghodss/yaml # github.com/github/gh-ost v1.1.3-0.20210727153850-e484824bbd68 ## explicit; go 1.16 github.com/github/gh-ost/go/base @@ -295,20 +292,20 @@ github.com/go-ole/go-ole/oleutil # github.com/go-openapi/errors v0.20.3 ## explicit; go 1.14 github.com/go-openapi/errors -# github.com/go-openapi/jsonpointer v0.19.3 +# github.com/go-openapi/jsonpointer v0.19.5 ## explicit; go 1.13 github.com/go-openapi/jsonpointer -# github.com/go-openapi/jsonreference v0.19.4 +# github.com/go-openapi/jsonreference v0.19.6 ## explicit; go 1.13 github.com/go-openapi/jsonreference -# github.com/go-openapi/spec v0.19.8 +# github.com/go-openapi/spec v0.20.4 ## explicit; go 1.13 github.com/go-openapi/spec # github.com/go-openapi/strfmt v0.21.7 ## explicit; go 1.19 github.com/go-openapi/strfmt -# github.com/go-openapi/swag v0.19.9 -## explicit; go 1.13 +# github.com/go-openapi/swag v0.19.15 +## explicit; go 1.11 github.com/go-openapi/swag # github.com/go-playground/locales v0.14.1 ## explicit; go 1.17 @@ -438,6 +435,9 @@ github.com/jinzhu/now ## explicit; go 1.10 github.com/jmoiron/sqlx github.com/jmoiron/sqlx/reflectx +# github.com/josharian/intern v1.0.0 +## explicit; go 1.5 +github.com/josharian/intern # github.com/json-iterator/go v1.1.12 ## explicit; go 1.12 github.com/json-iterator/go @@ -498,7 +498,7 @@ github.com/larksuite/oapi-sdk-go/v3/service/wiki/v2 # github.com/leodido/go-urn v1.2.4 ## explicit; go 1.16 github.com/leodido/go-urn -# github.com/mailru/easyjson v0.7.1 +# github.com/mailru/easyjson v0.7.7 ## explicit; go 1.12 github.com/mailru/easyjson/buffer github.com/mailru/easyjson/jlexer @@ -643,8 +643,8 @@ github.com/spf13/pflag # github.com/stretchr/testify v1.8.4 ## explicit; go 1.20 github.com/stretchr/testify/assert -# github.com/swaggo/swag v1.6.7 -## explicit; go 1.13 +# github.com/swaggo/swag v1.16.3 +## explicit; go 1.18 github.com/swaggo/swag github.com/swaggo/swag/gen # github.com/tjfoc/gmsm v1.4.1 @@ -692,7 +692,7 @@ go.uber.org/zap/internal/bufferpool go.uber.org/zap/internal/color go.uber.org/zap/internal/exit go.uber.org/zap/zapcore -# golang.org/x/crypto v0.13.0 +# golang.org/x/crypto v0.14.0 ## explicit; go 1.17 golang.org/x/crypto/acme golang.org/x/crypto/acme/autocert @@ -703,7 +703,6 @@ golang.org/x/crypto/cast5 golang.org/x/crypto/chacha20 golang.org/x/crypto/curve25519 golang.org/x/crypto/curve25519/internal/field -golang.org/x/crypto/ed25519 golang.org/x/crypto/hkdf golang.org/x/crypto/internal/alias golang.org/x/crypto/internal/poly1305 @@ -721,7 +720,7 @@ golang.org/x/exp/slices # golang.org/x/mod v0.12.0 ## explicit; go 1.17 golang.org/x/mod/semver -# golang.org/x/net v0.15.0 +# golang.org/x/net v0.17.0 ## explicit; go 1.17 golang.org/x/net/context golang.org/x/net/html @@ -900,6 +899,9 @@ gorm.io/gorm/logger gorm.io/gorm/migrator gorm.io/gorm/schema gorm.io/gorm/utils +# sigs.k8s.io/yaml v1.3.0 +## explicit; go 1.12 +sigs.k8s.io/yaml # vitess.io/vitess v0.12.0 ## explicit; go 1.17 vitess.io/vitess/go/bytes2 diff --git a/vendor/github.com/ghodss/yaml/.gitignore b/vendor/sigs.k8s.io/yaml/.gitignore similarity index 88% rename from vendor/github.com/ghodss/yaml/.gitignore rename to vendor/sigs.k8s.io/yaml/.gitignore index e256a31e00..2dc92904ef 100644 --- a/vendor/github.com/ghodss/yaml/.gitignore +++ b/vendor/sigs.k8s.io/yaml/.gitignore @@ -6,6 +6,10 @@ .project .settings/** +# Idea files +.idea/** +.idea/ + # Emacs save files *~ diff --git a/vendor/sigs.k8s.io/yaml/.travis.yml b/vendor/sigs.k8s.io/yaml/.travis.yml new file mode 100644 index 0000000000..54ed8f9cb9 --- /dev/null +++ b/vendor/sigs.k8s.io/yaml/.travis.yml @@ -0,0 +1,12 @@ +language: go +arch: arm64 +dist: focal +go: 1.15.x +script: + - diff -u <(echo -n) <(gofmt -d *.go) + - diff -u <(echo -n) <(golint $(go list -e ./...) | grep -v YAMLToJSON) + - GO111MODULE=on go vet . + - GO111MODULE=on go test -v -race ./... + - git diff --exit-code +install: + - GO111MODULE=off go get golang.org/x/lint/golint diff --git a/vendor/sigs.k8s.io/yaml/CONTRIBUTING.md b/vendor/sigs.k8s.io/yaml/CONTRIBUTING.md new file mode 100644 index 0000000000..de47115137 --- /dev/null +++ b/vendor/sigs.k8s.io/yaml/CONTRIBUTING.md @@ -0,0 +1,31 @@ +# Contributing Guidelines + +Welcome to Kubernetes. We are excited about the prospect of you joining our [community](https://github.com/kubernetes/community)! The Kubernetes community abides by the CNCF [code of conduct](code-of-conduct.md). Here is an excerpt: + +_As contributors and maintainers of this project, and in the interest of fostering an open and welcoming community, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities._ + +## Getting Started + +We have full documentation on how to get started contributing here: + + + +- [Contributor License Agreement](https://git.k8s.io/community/CLA.md) Kubernetes projects require that you sign a Contributor License Agreement (CLA) before we can accept your pull requests +- [Kubernetes Contributor Guide](http://git.k8s.io/community/contributors/guide) - Main contributor documentation, or you can just jump directly to the [contributing section](http://git.k8s.io/community/contributors/guide#contributing) +- [Contributor Cheat Sheet](https://git.k8s.io/community/contributors/guide/contributor-cheatsheet.md) - Common resources for existing developers + +## Mentorship + +- [Mentoring Initiatives](https://git.k8s.io/community/mentoring) - We have a diverse set of mentorship programs available that are always looking for volunteers! + + diff --git a/vendor/github.com/ghodss/yaml/LICENSE b/vendor/sigs.k8s.io/yaml/LICENSE similarity index 100% rename from vendor/github.com/ghodss/yaml/LICENSE rename to vendor/sigs.k8s.io/yaml/LICENSE diff --git a/vendor/sigs.k8s.io/yaml/OWNERS b/vendor/sigs.k8s.io/yaml/OWNERS new file mode 100644 index 0000000000..325b40b076 --- /dev/null +++ b/vendor/sigs.k8s.io/yaml/OWNERS @@ -0,0 +1,27 @@ +# See the OWNERS docs at https://go.k8s.io/owners + +approvers: +- dims +- lavalamp +- smarterclayton +- deads2k +- sttts +- liggitt +- caesarxuchao +reviewers: +- dims +- thockin +- lavalamp +- smarterclayton +- wojtek-t +- deads2k +- derekwaynecarr +- caesarxuchao +- mikedanese +- liggitt +- gmarek +- sttts +- ncdc +- tallclair +labels: +- sig/api-machinery diff --git a/vendor/github.com/ghodss/yaml/README.md b/vendor/sigs.k8s.io/yaml/README.md similarity index 86% rename from vendor/github.com/ghodss/yaml/README.md rename to vendor/sigs.k8s.io/yaml/README.md index 0200f75b4d..e81cc426be 100644 --- a/vendor/github.com/ghodss/yaml/README.md +++ b/vendor/sigs.k8s.io/yaml/README.md @@ -1,12 +1,14 @@ # YAML marshaling and unmarshaling support for Go -[![Build Status](https://travis-ci.org/ghodss/yaml.svg)](https://travis-ci.org/ghodss/yaml) +[![Build Status](https://travis-ci.org/kubernetes-sigs/yaml.svg)](https://travis-ci.org/kubernetes-sigs/yaml) + +kubernetes-sigs/yaml is a permanent fork of [ghodss/yaml](https://github.com/ghodss/yaml). ## Introduction A wrapper around [go-yaml](https://github.com/go-yaml/yaml) designed to enable a better way of handling YAML when marshaling to and from structs. -In short, this library first converts YAML to JSON using go-yaml and then uses `json.Marshal` and `json.Unmarshal` to convert to or from the struct. This means that it effectively reuses the JSON struct tags as well as the custom JSON methods `MarshalJSON` and `UnmarshalJSON` unlike go-yaml. For a detailed overview of the rationale behind this method, [see this blog post](http://ghodss.com/2014/the-right-way-to-handle-yaml-in-golang/). +In short, this library first converts YAML to JSON using go-yaml and then uses `json.Marshal` and `json.Unmarshal` to convert to or from the struct. This means that it effectively reuses the JSON struct tags as well as the custom JSON methods `MarshalJSON` and `UnmarshalJSON` unlike go-yaml. For a detailed overview of the rationale behind this method, [see this blog post](http://web.archive.org/web/20190603050330/http://ghodss.com/2014/the-right-way-to-handle-yaml-in-golang/). ## Compatibility @@ -32,13 +34,13 @@ GOOD: To install, run: ``` -$ go get github.com/ghodss/yaml +$ go get sigs.k8s.io/yaml ``` And import using: ``` -import "github.com/ghodss/yaml" +import "sigs.k8s.io/yaml" ``` Usage is very similar to the JSON library: @@ -49,7 +51,7 @@ package main import ( "fmt" - "github.com/ghodss/yaml" + "sigs.k8s.io/yaml" ) type Person struct { @@ -93,7 +95,7 @@ package main import ( "fmt" - "github.com/ghodss/yaml" + "sigs.k8s.io/yaml" ) func main() { @@ -105,8 +107,8 @@ func main() { } fmt.Println(string(y)) /* Output: - name: John age: 30 + name: John */ j2, err := yaml.YAMLToJSON(y) if err != nil { diff --git a/vendor/sigs.k8s.io/yaml/RELEASE.md b/vendor/sigs.k8s.io/yaml/RELEASE.md new file mode 100644 index 0000000000..6b642464e5 --- /dev/null +++ b/vendor/sigs.k8s.io/yaml/RELEASE.md @@ -0,0 +1,9 @@ +# Release Process + +The `yaml` Project is released on an as-needed basis. The process is as follows: + +1. An issue is proposing a new release with a changelog since the last release +1. All [OWNERS](OWNERS) must LGTM this release +1. An OWNER runs `git tag -s $VERSION` and inserts the changelog and pushes the tag with `git push $VERSION` +1. The release issue is closed +1. An announcement email is sent to `kubernetes-dev@googlegroups.com` with the subject `[ANNOUNCE] kubernetes-template-project $VERSION is released` diff --git a/vendor/sigs.k8s.io/yaml/SECURITY_CONTACTS b/vendor/sigs.k8s.io/yaml/SECURITY_CONTACTS new file mode 100644 index 0000000000..0648a8ebff --- /dev/null +++ b/vendor/sigs.k8s.io/yaml/SECURITY_CONTACTS @@ -0,0 +1,17 @@ +# Defined below are the security contacts for this repo. +# +# They are the contact point for the Product Security Team to reach out +# to for triaging and handling of incoming issues. +# +# The below names agree to abide by the +# [Embargo Policy](https://github.com/kubernetes/sig-release/blob/master/security-release-process-documentation/security-release-process.md#embargo-policy) +# and will be removed and replaced if they violate that agreement. +# +# DO NOT REPORT SECURITY VULNERABILITIES DIRECTLY TO THESE NAMES, FOLLOW THE +# INSTRUCTIONS AT https://kubernetes.io/security/ + +cjcullen +jessfraz +liggitt +philips +tallclair diff --git a/vendor/sigs.k8s.io/yaml/code-of-conduct.md b/vendor/sigs.k8s.io/yaml/code-of-conduct.md new file mode 100644 index 0000000000..0d15c00cf3 --- /dev/null +++ b/vendor/sigs.k8s.io/yaml/code-of-conduct.md @@ -0,0 +1,3 @@ +# Kubernetes Community Code of Conduct + +Please refer to our [Kubernetes Community Code of Conduct](https://git.k8s.io/community/code-of-conduct.md) diff --git a/vendor/github.com/ghodss/yaml/fields.go b/vendor/sigs.k8s.io/yaml/fields.go similarity index 99% rename from vendor/github.com/ghodss/yaml/fields.go rename to vendor/sigs.k8s.io/yaml/fields.go index 5860074026..235b7f2cf6 100644 --- a/vendor/github.com/ghodss/yaml/fields.go +++ b/vendor/sigs.k8s.io/yaml/fields.go @@ -1,6 +1,7 @@ // Copyright 2013 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. + package yaml import ( diff --git a/vendor/github.com/ghodss/yaml/yaml.go b/vendor/sigs.k8s.io/yaml/yaml.go similarity index 78% rename from vendor/github.com/ghodss/yaml/yaml.go rename to vendor/sigs.k8s.io/yaml/yaml.go index dfd264d6c5..efbc535d41 100644 --- a/vendor/github.com/ghodss/yaml/yaml.go +++ b/vendor/sigs.k8s.io/yaml/yaml.go @@ -1,14 +1,4 @@ -// Package yaml provides a wrapper around go-yaml designed to enable a better -// way of handling YAML when marshaling to and from structs. -// -// In short, this package first converts YAML to JSON using go-yaml and then -// uses json.Marshal and json.Unmarshal to convert to or from the struct. This -// means that it effectively reuses the JSON struct tags as well as the custom -// JSON methods MarshalJSON and UnmarshalJSON unlike go-yaml. -// -// See also http://ghodss.com/2014/the-right-way-to-handle-yaml-in-golang -// -package yaml // import "github.com/ghodss/yaml" +package yaml import ( "bytes" @@ -21,7 +11,7 @@ import ( "gopkg.in/yaml.v2" ) -// Marshals the object into JSON then converts JSON to YAML and returns the +// Marshal marshals the object into JSON then converts JSON to YAML and returns the // YAML. func Marshal(o interface{}) ([]byte, error) { j, err := json.Marshal(o) @@ -43,19 +33,24 @@ type JSONOpt func(*json.Decoder) *json.Decoder // Unmarshal converts YAML to JSON then uses JSON to unmarshal into an object, // optionally configuring the behavior of the JSON unmarshal. func Unmarshal(y []byte, o interface{}, opts ...JSONOpt) error { - return unmarshal(yaml.Unmarshal, y, o, opts) + return yamlUnmarshal(y, o, false, opts...) } -// UnmarshalStrict is like Unmarshal except that any mapping keys that are -// duplicates will result in an error. -// To also be strict about unknown fields, add the DisallowUnknownFields option. +// UnmarshalStrict strictly converts YAML to JSON then uses JSON to unmarshal +// into an object, optionally configuring the behavior of the JSON unmarshal. func UnmarshalStrict(y []byte, o interface{}, opts ...JSONOpt) error { - return unmarshal(yaml.UnmarshalStrict, y, o, opts) + return yamlUnmarshal(y, o, true, append(opts, DisallowUnknownFields)...) } -func unmarshal(f func(in []byte, out interface{}) (err error), y []byte, o interface{}, opts []JSONOpt) error { +// yamlUnmarshal unmarshals the given YAML byte stream into the given interface, +// optionally performing the unmarshalling strictly +func yamlUnmarshal(y []byte, o interface{}, strict bool, opts ...JSONOpt) error { vo := reflect.ValueOf(o) - j, err := yamlToJSON(y, &vo, f) + unmarshalFn := yaml.Unmarshal + if strict { + unmarshalFn = yaml.UnmarshalStrict + } + j, err := yamlToJSON(y, &vo, unmarshalFn) if err != nil { return fmt.Errorf("error converting YAML to JSON: %v", err) } @@ -83,7 +78,7 @@ func jsonUnmarshal(r io.Reader, o interface{}, opts ...JSONOpt) error { return nil } -// Convert JSON to YAML. +// JSONToYAML Converts JSON to YAML. func JSONToYAML(j []byte) ([]byte, error) { // Convert the JSON to an object. var jsonObj interface{} @@ -134,7 +129,7 @@ func yamlToJSON(y []byte, jsonTarget *reflect.Value, yamlUnmarshal func([]byte, // YAML objects are not completely compatible with JSON objects (e.g. you // can have non-string keys in YAML). So, convert the YAML-compatible object // to a JSON-compatible object, failing with an error if irrecoverable - // incompatibilities happen along the way. + // incompatibilties happen along the way. jsonObj, err := convertToJSONableObject(yamlObj, jsonTarget) if err != nil { return nil, err @@ -321,6 +316,65 @@ func convertToJSONableObject(yamlObj interface{}, jsonTarget *reflect.Value) (in } return yamlObj, nil } +} + +// JSONObjectToYAMLObject converts an in-memory JSON object into a YAML in-memory MapSlice, +// without going through a byte representation. A nil or empty map[string]interface{} input is +// converted to an empty map, i.e. yaml.MapSlice(nil). +// +// interface{} slices stay interface{} slices. map[string]interface{} becomes yaml.MapSlice. +// +// int64 and float64 are down casted following the logic of github.com/go-yaml/yaml: +// - float64s are down-casted as far as possible without data-loss to int, int64, uint64. +// - int64s are down-casted to int if possible without data-loss. +// +// Big int/int64/uint64 do not lose precision as in the json-yaml roundtripping case. +// +// string, bool and any other types are unchanged. +func JSONObjectToYAMLObject(j map[string]interface{}) yaml.MapSlice { + if len(j) == 0 { + return nil + } + ret := make(yaml.MapSlice, 0, len(j)) + for k, v := range j { + ret = append(ret, yaml.MapItem{Key: k, Value: jsonToYAMLValue(v)}) + } + return ret +} - return nil, nil +func jsonToYAMLValue(j interface{}) interface{} { + switch j := j.(type) { + case map[string]interface{}: + if j == nil { + return interface{}(nil) + } + return JSONObjectToYAMLObject(j) + case []interface{}: + if j == nil { + return interface{}(nil) + } + ret := make([]interface{}, len(j)) + for i := range j { + ret[i] = jsonToYAMLValue(j[i]) + } + return ret + case float64: + // replicate the logic in https://github.com/go-yaml/yaml/blob/51d6538a90f86fe93ac480b35f37b2be17fef232/resolve.go#L151 + if i64 := int64(j); j == float64(i64) { + if i := int(i64); i64 == int64(i) { + return i + } + return i64 + } + if ui64 := uint64(j); j == float64(ui64) { + return ui64 + } + return j + case int64: + if i := int(j); j == int64(i) { + return i + } + return j + } + return j } diff --git a/vendor/github.com/ghodss/yaml/yaml_go110.go b/vendor/sigs.k8s.io/yaml/yaml_go110.go similarity index 100% rename from vendor/github.com/ghodss/yaml/yaml_go110.go rename to vendor/sigs.k8s.io/yaml/yaml_go110.go