From d63381e584998657c31ae82ba69c1ac4598cd900 Mon Sep 17 00:00:00 2001 From: Vasiliy Tolstov Date: Tue, 1 Feb 2022 00:30:36 +0300 Subject: [PATCH] improve openapi support Signed-off-by: Vasiliy Tolstov --- go.mod | 5 +- go.sum | 132 +++++- openapiv3.go | 1031 ++++++++++++++++++++++++++++++++------------- openapiv3_util.go | 39 ++ util.go | 123 +++++- variables.go | 1 + 6 files changed, 1027 insertions(+), 304 deletions(-) create mode 100644 openapiv3_util.go diff --git a/go.mod b/go.mod index c3249a8..eb226de 100644 --- a/go.mod +++ b/go.mod @@ -4,10 +4,7 @@ go 1.16 require ( github.com/fatih/structtag v1.2.0 - go.unistack.org/micro-proto/v3 v3.1.1 + go.unistack.org/micro-proto/v3 v3.2.3 golang.org/x/tools v0.1.8 google.golang.org/protobuf v1.27.1 ) - -//replace go.unistack.org/micro/v3 => ../micro -//replace go.unistack.org/micro-proto => ../micro-proto diff --git a/go.sum b/go.sum index 2bf60f9..7320662 100644 --- a/go.sum +++ b/go.sum @@ -1,31 +1,125 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4= github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= +github.com/flowstack/go-jsonschema v0.1.1/go.mod h1:yL7fNggx1o8rm9RlgXv7hTBWxdBM0rVwpMwimd3F3N0= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/gnostic v0.6.6 h1:MVSM2r2j9aRUvYNym66JGW96Ddd5MN4sTi59yktb6yk= +github.com/google/gnostic v0.6.6/go.mod h1:Nm8234We1lq6iB9OmlgNv3nH91XLLVZHCDayfA3xq+E= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -go.unistack.org/micro-proto/v3 v3.1.1 h1:78qRmltwGek5kSQ9tNmDZ9TCRvZM7YDIOgzriKvabjA= -go.unistack.org/micro-proto/v3 v3.1.1/go.mod h1:DpRhYCBXlmSJ/AAXTmntvlh7kQkYU6eFvlmYAx4BQS8= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go.unistack.org/micro-proto/v3 v3.2.2 h1:fpng7nIv+yc0MWEVg+cqn594llwPIjt42VH+fDBJ5ZM= +go.unistack.org/micro-proto/v3 v3.2.2/go.mod h1:ZltVWNECD5yK+40+OCONzGw4OtmSdTpVi8/KFgo9dqM= +go.unistack.org/micro-proto/v3 v3.2.3 h1:vSRI6VoZrlv0pUdo69irHv6HbbnD+oZOGEUE/TS5XBQ= +go.unistack.org/micro-proto/v3 v3.2.3/go.mod h1:ZltVWNECD5yK+40+OCONzGw4OtmSdTpVi8/KFgo9dqM= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.5.1 h1:OJxoQ/rynoF0dcCdI7cLPktw/hR2cueqYfjm43oqK38= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/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-20211019181941-9d821ace8654 h1:id054HUawV2/6IGm2IV8KZQjqtwAOo2CYlOToYqa0d0= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.8 h1:P1HhGGuLW4aAclzjtmJdf0mJOjVUZUzOTqkAkWL+l6w= golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= @@ -34,6 +128,40 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20220107163113-42d7afdf6368/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/openapiv3.go b/openapiv3.go index 8deb3e0..f0dffb4 100644 --- a/openapiv3.go +++ b/openapiv3.go @@ -12,49 +12,60 @@ // See the License for the specific language governing permissions and // limitations under the License. // + package main import ( "fmt" "log" - "net/http" + "net/url" "regexp" "sort" "strings" - annotations "go.unistack.org/micro-proto/v3/api" + "go.unistack.org/micro-proto/v3/api" + // v2 "go.unistack.org/micro-proto/v3/openapiv2" v3 "go.unistack.org/micro-proto/v3/openapiv3" "google.golang.org/protobuf/compiler/protogen" - jsonpb "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/reflect/protoreflect" ) -type openapiv3Generator struct { - plugin *protogen.Plugin +const ( + protobufValueName = "AnyJSONValue" +) +// openapiv3Generator holds internal state needed to generate an OpenAPIv3 document for a transcoded Protocol Buffer service. +type openapiv3Generator struct { + circularDepth int + naming string + plugin *protogen.Plugin requiredSchemas []string // Names of schemas that need to be generated. generatedSchemas []string // Names of schemas that have already been generated. linterRulePattern *regexp.Regexp - namePattern *regexp.Regexp + pathPattern *regexp.Regexp + namedPathPattern *regexp.Regexp } +// openapiv3Generate creates a new generator for a protoc plugin invocation. func (g *Generator) openapiv3Generate(component string, plugin *protogen.Plugin) error { og := &openapiv3Generator{ - plugin: plugin, + circularDepth: 2, + plugin: plugin, + naming: "proto", + requiredSchemas: make([]string, 0), generatedSchemas: make([]string, 0), linterRulePattern: regexp.MustCompile(`\(-- .* --\)`), - namePattern: regexp.MustCompile("{(.*)=(.*)}"), + pathPattern: regexp.MustCompile("{([^=}]+)}"), + namedPathPattern: regexp.MustCompile("{(.+)=(.+)}"), } d := og.buildDocumentV3(plugin) - - bytes, err := (jsonpb.MarshalOptions{Indent: " "}).Marshal(d) + bytes, err := d.YAMLValue("Generated with protoc-gen-go-micro\n") if err != nil { - return fmt.Errorf("failed to marshal: %s", err.Error()) + return fmt.Errorf("failed to marshal yaml: %s", err.Error()) } - outputFile := og.plugin.NewGeneratedFile(g.openapiFile, "") if _, err := outputFile.Write(bytes); err != nil { return fmt.Errorf("failed to write: %s", err.Error()) @@ -65,13 +76,7 @@ func (g *Generator) openapiv3Generate(component string, plugin *protogen.Plugin) // buildDocumentV3 builds an OpenAPIv3 document for a plugin request. func (g *openapiv3Generator) buildDocumentV3(plugin *protogen.Plugin) *v3.Document { - d := &v3.Document{} - d.Openapi = "3.0.3" - d.Info = &v3.Info{ - Title: "", - Version: "0.0.1", - Description: "", - } + d := &v3.Document{Openapi: "3.0.3", Info: &v3.Info{Version: "0.0.1"}} for _, file := range plugin.Files { if !proto.HasExtension(file.Desc.Options(), v3.E_Openapiv3Swagger) { @@ -101,16 +106,105 @@ func (g *openapiv3Generator) buildDocumentV3(plugin *protogen.Plugin) *v3.Docume AdditionalProperties: []*v3.NamedSchemaOrReference{}, }, } + for _, file := range g.plugin.Files { - g.addPathsToDocumentV3(d, file) + if file.Generate { + g.addPathsToDocumentV3(d, file) + } } + + // If there is only 1 service, then use it's title for the document, + // if the document is missing it. + if len(d.Tags) == 1 { + if d.Info.Title == "" && d.Tags[0].Name != "" { + d.Info.Title = d.Tags[0].Name + " API" + } + if d.Info.Description == "" { + d.Info.Description = d.Tags[0].Description + } + d.Tags[0].Description = "" + } + for len(g.requiredSchemas) > 0 { count := len(g.requiredSchemas) for _, file := range g.plugin.Files { - g.addSchemasToDocumentV3(d, file) + g.addSchemasToDocumentV3(d, file.Messages) } g.requiredSchemas = g.requiredSchemas[count:len(g.requiredSchemas)] } + + allServers := []string{} + + // If paths methods has servers, but they're all the same, then move servers to path level + for _, path := range d.Paths.Path { + servers := []string{} + // Only 1 server will ever be set, per method, by the generator + + if path.Value.Get != nil && len(path.Value.Get.Servers) == 1 { + servers = appendUniuqe(servers, path.Value.Get.Servers[0].Url) + allServers = appendUniuqe(servers, path.Value.Get.Servers[0].Url) + } + if path.Value.Post != nil && len(path.Value.Post.Servers) == 1 { + servers = appendUniuqe(servers, path.Value.Post.Servers[0].Url) + allServers = appendUniuqe(servers, path.Value.Post.Servers[0].Url) + } + if path.Value.Put != nil && len(path.Value.Put.Servers) == 1 { + servers = appendUniuqe(servers, path.Value.Put.Servers[0].Url) + allServers = appendUniuqe(servers, path.Value.Put.Servers[0].Url) + } + if path.Value.Delete != nil && len(path.Value.Delete.Servers) == 1 { + servers = appendUniuqe(servers, path.Value.Delete.Servers[0].Url) + allServers = appendUniuqe(servers, path.Value.Delete.Servers[0].Url) + } + if path.Value.Patch != nil && len(path.Value.Patch.Servers) == 1 { + servers = appendUniuqe(servers, path.Value.Patch.Servers[0].Url) + allServers = appendUniuqe(servers, path.Value.Patch.Servers[0].Url) + } + + if len(servers) == 1 { + path.Value.Servers = []*v3.Server{{Url: servers[0]}} + + if path.Value.Get != nil { + path.Value.Get.Servers = nil + } + if path.Value.Post != nil { + path.Value.Post.Servers = nil + } + if path.Value.Put != nil { + path.Value.Put.Servers = nil + } + if path.Value.Delete != nil { + path.Value.Delete.Servers = nil + } + if path.Value.Patch != nil { + path.Value.Patch.Servers = nil + } + } + } + + // Set all servers on API level + if len(allServers) > 0 { + d.Servers = []*v3.Server{} + for _, server := range allServers { + d.Servers = append(d.Servers, &v3.Server{Url: server}) + } + } + + // If there is only 1 server, we can safely remove all path level servers + if len(allServers) == 1 { + for _, path := range d.Paths.Path { + path.Value.Servers = nil + } + } + + // Sort the tags. + { + pairs := d.Tags + sort.Slice(pairs, func(i, j int) bool { + return pairs[i].Name < pairs[j].Name + }) + d.Tags = pairs + } // Sort the paths. { pairs := d.Paths.Path @@ -131,9 +225,11 @@ func (g *openapiv3Generator) buildDocumentV3(plugin *protogen.Plugin) *v3.Docume } // filterCommentString removes line breaks and linter rules from comments. -func (g *openapiv3Generator) filterCommentString(c protogen.Comments) string { +func (g *openapiv3Generator) filterCommentString(c protogen.Comments, removeNewLines bool) string { comment := string(c) - comment = strings.Replace(comment, "\n", "", -1) + if removeNewLines { + comment = strings.Replace(comment, "\n", "", -1) + } comment = g.linterRulePattern.ReplaceAllString(comment, "") return strings.TrimSpace(comment) } @@ -141,70 +237,262 @@ func (g *openapiv3Generator) filterCommentString(c protogen.Comments) string { // addPathsToDocumentV3 adds paths from a specified file descriptor. func (g *openapiv3Generator) addPathsToDocumentV3(d *v3.Document, file *protogen.File) { for _, service := range file.Services { - comment := g.filterCommentString(service.Comments.Leading) - if d.Info.Title == "" { - d.Info.Title = service.GoName - } - if d.Info.Description == "" { - d.Info.Description = comment - } + annotationsCount := 0 + for _, method := range service.Methods { - comment := g.filterCommentString(method.Comments.Leading) + comment := g.filterCommentString(method.Comments.Leading, false) inputMessage := method.Input outputMessage := method.Output operationID := service.GoName + "_" + method.GoName - eopt := proto.GetExtension(method.Desc.Options(), v3.E_Openapiv3Operation) - if eopt != nil && eopt != v3.E_Openapiv3Operation.InterfaceOf(v3.E_Openapiv3Operation.Zero()) { - opt := eopt.(*v3.Operation) - if opt.OperationId != "" { + + /* + e2opt := proto.GetExtension(method.Desc.Options(), v2.E_Openapiv2Operation) + if e2opt != nil && e2opt != v2.E_Openapiv2Operation.InterfaceOf(v2.E_Openapiv2Operation.Zero()) { + if opt, ok := e2opt.(*v2.Operation); ok && opt.OperationId != "" { + operationID = opt.OperationId + } + } + */ + e3opt := proto.GetExtension(method.Desc.Options(), v3.E_Openapiv3Operation) + if e3opt != nil && e3opt != v3.E_Openapiv3Operation.InterfaceOf(v3.E_Openapiv3Operation.Zero()) { + if opt, ok := e3opt.(*v3.Operation); ok && opt.OperationId != "" { operationID = opt.OperationId } } - xt := annotations.E_Http - extension := proto.GetExtension(method.Desc.Options(), xt) + var path string var methodName string var body string - if extension != nil && extension != xt.InterfaceOf(xt.Zero()) { - rule := extension.(*annotations.HttpRule) + + extHTTP := proto.GetExtension(method.Desc.Options(), api.E_Http) + if extHTTP != nil && extHTTP != api.E_Http.InterfaceOf(api.E_Http.Zero()) { + annotationsCount++ + + rule := extHTTP.(*api.HttpRule) body = rule.Body switch pattern := rule.Pattern.(type) { - case *annotations.HttpRule_Get: + case *api.HttpRule_Get: path = pattern.Get methodName = "GET" - case *annotations.HttpRule_Post: + case *api.HttpRule_Post: path = pattern.Post methodName = "POST" - case *annotations.HttpRule_Put: + case *api.HttpRule_Put: path = pattern.Put methodName = "PUT" - case *annotations.HttpRule_Delete: + case *api.HttpRule_Delete: path = pattern.Delete methodName = "DELETE" - case *annotations.HttpRule_Patch: + case *api.HttpRule_Patch: path = pattern.Patch methodName = "PATCH" - case *annotations.HttpRule_Custom: + case *api.HttpRule_Custom: path = "custom-unsupported" default: path = "unknown-unsupported" } } + if methodName != "" { + defaultHost := proto.GetExtension(service.Desc.Options(), api.E_DefaultHost).(string) + op, path2 := g.buildOperationV3( - file, method, operationID, comment, path, body, inputMessage, outputMessage) + file, method, operationID, service.GoName, comment, defaultHost, path, body, inputMessage, outputMessage) g.addOperationV3(d, op, path2, methodName) } } + + if annotationsCount > 0 { + comment := g.filterCommentString(service.Comments.Leading, false) + d.Tags = append(d.Tags, &v3.Tag{Name: service.GoName, Description: comment}) + } } } +func (g *openapiv3Generator) formatMessageRef(name string) string { + if g.naming == "proto" { + return name + } + + if len(name) > 1 { + return strings.ToUpper(name[0:1]) + name[1:] + } + + if len(name) == 1 { + return strings.ToLower(name) + } + + return name +} + +func (g *openapiv3Generator) getMessageName(message protoreflect.MessageDescriptor) string { + prefix := "" + parent := message.Parent() + if message != nil { + if _, ok := parent.(protoreflect.MessageDescriptor); ok { + prefix = string(parent.Name()) + "_" + prefix + } + } + + return prefix + string(message.Name()) +} + +func (g *openapiv3Generator) formatMessageName(message *protogen.Message) string { + name := g.getMessageName(message.Desc) + + if g.naming == "proto" { + return name + } + + if len(name) > 0 { + return strings.ToUpper(name[0:1]) + name[1:] + } + + return name +} + +func (g *openapiv3Generator) formatFieldName(field *protogen.Field) string { + if g.naming == "proto" { + return string(field.Desc.Name()) + } + + return field.Desc.JSONName() +} + +func (g *openapiv3Generator) findField(name string, inMessage *protogen.Message) *protogen.Field { + for _, field := range inMessage.Fields { + if string(field.Desc.Name()) == name || string(field.Desc.JSONName()) == name { + return field + } + } + + return nil +} + +func (g *openapiv3Generator) findAndFormatFieldName(name string, inMessage *protogen.Message) string { + field := g.findField(name, inMessage) + if field != nil { + return g.formatFieldName(field) + } + + return name +} + +// Note that fields which are mapped to URL query parameters must have a primitive type +// or a repeated primitive type or a non-repeated message type. +// In the case of a repeated type, the parameter can be repeated in the URL as ...?param=A¶m=B. +// In the case of a message type, each field of the message is mapped to a separate parameter, +// such as ...?foo.a=A&foo.b=B&foo.c=C. +// +// maps, Struct and Empty can NOT be used +// messages can have any number of sub messages - including circular (e.g. sub.subsub.sub.subsub.id) + +// buildQueryParamsV3 extracts any valid query params, including sub and recursive messages +func (g *openapiv3Generator) buildQueryParamsV3(field *protogen.Field) []*v3.ParameterOrReference { + depths := map[string]int{} + return g._buildQueryParamsV3(field, depths) +} + +// depths are used to keep track of how many times a message's fields has been seen +func (g *openapiv3Generator) _buildQueryParamsV3(field *protogen.Field, depths map[string]int) []*v3.ParameterOrReference { + parameters := []*v3.ParameterOrReference{} + + queryFieldName := g.formatFieldName(field) + fieldDescription := g.filterCommentString(field.Comments.Leading, true) + + if field.Desc.IsMap() { + // Map types are not allowed in query parameteres + return parameters + } else if field.Desc.Kind() == protoreflect.MessageKind { + // Represent google.protobuf.Value as reference to the value of const protobufValueName. + if g.fullMessageTypeName(field.Desc.Message()) == ".google.protobuf.Value" { + fieldSchema := g.schemaOrReferenceForField(field.Desc) + parameters = append(parameters, + &v3.ParameterOrReference{ + Oneof: &v3.ParameterOrReference_Parameter{ + Parameter: &v3.Parameter{ + Name: queryFieldName, + In: "query", + Description: fieldDescription, + Required: false, + Schema: fieldSchema, + }, + }, + }) + return parameters + } else if field.Desc.IsList() { + // Only non-repeated message types are valid + return parameters + } + + // Represent field masks directly as strings (don't expand them). + if g.fullMessageTypeName(field.Desc.Message()) == ".google.protobuf.FieldMask" { + fieldSchema := g.schemaOrReferenceForField(field.Desc) + parameters = append(parameters, + &v3.ParameterOrReference{ + Oneof: &v3.ParameterOrReference_Parameter{ + Parameter: &v3.Parameter{ + Name: queryFieldName, + In: "query", + Description: fieldDescription, + Required: false, + Schema: fieldSchema, + }, + }, + }) + return parameters + } + + // Sub messages are allowed, even circular, as long as the final type is a primitive. + // Go through each of the sub message fields + for _, subField := range field.Message.Fields { + subFieldFullName := string(subField.Desc.FullName()) + seen, ok := depths[subFieldFullName] + if !ok { + depths[subFieldFullName] = 0 + } + + if seen < g.circularDepth { + depths[subFieldFullName]++ + subParams := g._buildQueryParamsV3(subField, depths) + for _, subParam := range subParams { + if param, ok := subParam.Oneof.(*v3.ParameterOrReference_Parameter); ok { + param.Parameter.Name = queryFieldName + "." + param.Parameter.Name + parameters = append(parameters, subParam) + } + } + } + } + + } else if field.Desc.Kind() != protoreflect.GroupKind { + // schemaOrReferenceForField also handles array types + fieldSchema := g.schemaOrReferenceForField(field.Desc) + + parameters = append(parameters, + &v3.ParameterOrReference{ + Oneof: &v3.ParameterOrReference_Parameter{ + Parameter: &v3.Parameter{ + Name: queryFieldName, + In: "query", + Description: fieldDescription, + Required: false, + Schema: fieldSchema, + }, + }, + }) + } + + return parameters +} + // buildOperationV3 constructs an operation for a set of values. func (g *openapiv3Generator) buildOperationV3( file *protogen.File, method *protogen.Method, operationID string, + tagName string, description string, + defaultHost string, path string, bodyField string, inputMessage *protogen.Message, @@ -217,6 +505,7 @@ func (g *openapiv3Generator) buildOperationV3( } // Initialize the list of operation parameters. parameters := []*v3.ParameterOrReference{} + // Build a list of header parameters. eopt := proto.GetExtension(method.Desc.Options(), v3.E_Openapiv3Operation) if eopt != nil && eopt != v3.E_Openapiv3Operation.InterfaceOf(v3.E_Openapiv3Operation.Zero()) { @@ -233,86 +522,128 @@ func (g *openapiv3Generator) buildOperationV3( sparameters[parameter.Name] = struct{}{} } - // Build a list of path parameters. - pathParameters := make([]string, 0) - if matches := g.namePattern.FindStringSubmatch(path); matches != nil { + if u, err := url.Parse(path); err == nil { + mp := u.Query() + path = u.Path + if mp != nil { + for _, field := range inputMessage.Fields { + fieldName := string(field.Desc.Name()) + if _, ok := mp[fieldName]; ok && fieldName != bodyField { + fieldParams := g.buildQueryParamsV3(field) + parameters = append(parameters, fieldParams...) + coveredParameters = append(coveredParameters, fieldName) + } + } + } + } + + // Find simple path parameters like {id} + if allMatches := g.pathPattern.FindAllStringSubmatch(path, -1); allMatches != nil { + for _, matches := range allMatches { + // Add the value to the list of covered parameters. + coveredParameters = append(coveredParameters, matches[1]) + pathParameter := g.findAndFormatFieldName(matches[1], inputMessage) + path = strings.Replace(path, matches[1], pathParameter, 1) + + // Add the path parameters to the operation parameters. + var fieldSchema *v3.SchemaOrReference + + var fieldDescription string + field := g.findField(pathParameter, inputMessage) + if field != nil { + fieldSchema = g.schemaOrReferenceForField(field.Desc) + fieldDescription = g.filterCommentString(field.Comments.Leading, true) + } else { + // If field dooes not exist, it is safe to set it to string, as it is ignored downstream + fieldSchema = &v3.SchemaOrReference{ + Oneof: &v3.SchemaOrReference_Schema{ + Schema: &v3.Schema{ + Type: "string", + }, + }, + } + } + + parameters = append(parameters, + &v3.ParameterOrReference{ + Oneof: &v3.ParameterOrReference_Parameter{ + Parameter: &v3.Parameter{ + Name: pathParameter, + In: "path", + Description: fieldDescription, + Required: true, + Schema: fieldSchema, + }, + }, + }) + } + } + + // Find named path parameters like {name=shelves/*} + if matches := g.namedPathPattern.FindStringSubmatch(path); matches != nil { + // Build a list of named path parameters. + namedPathParameters := make([]string, 0) + // Add the "name=" "name" value to the list of covered parameters. coveredParameters = append(coveredParameters, matches[1]) // Convert the path from the starred form to use named path parameters. starredPath := matches[2] parts := strings.Split(starredPath, "/") // The starred path is assumed to be in the form "things/*/otherthings/*". - // We want to convert it to "things/{thing}/otherthings/{otherthing}". + // We want to convert it to "things/{thingsId}/otherthings/{otherthingsId}". for i := 0; i < len(parts)-1; i += 2 { section := parts[i] - parameter := singular(section) - parts[i+1] = "{" + parameter + "}" - pathParameters = append(pathParameters, parameter) + namedPathParameter := g.findAndFormatFieldName(section, inputMessage) + namedPathParameter = singular(namedPathParameter) + parts[i+1] = "{" + namedPathParameter + "}" + namedPathParameters = append(namedPathParameters, namedPathParameter) } // Rewrite the path to use the path parameters. newPath := strings.Join(parts, "/") path = strings.Replace(path, matches[0], newPath, 1) - } - // Add the path parameters to the operation parameters. - for _, pathParameter := range pathParameters { - if _, ok := sparameters[pathParameter]; ok { - continue - } - parameters = append(parameters, - &v3.ParameterOrReference{ - Oneof: &v3.ParameterOrReference_Parameter{ - Parameter: &v3.Parameter{ - Name: pathParameter, - In: "path", - Required: true, - Description: "The " + pathParameter + " id.", - Schema: &v3.SchemaOrReference{ - Oneof: &v3.SchemaOrReference_Schema{ - Schema: &v3.Schema{ - Type: "string", - }, - }, - }, - }, - }, - }) - } - // Add any unhandled fields in the request message as query parameters. - if bodyField != "*" { - for _, field := range inputMessage.Fields { - fieldName := string(field.Desc.Name()) - if !contains(coveredParameters, fieldName) { - if _, ok := sparameters[fieldName]; ok { - continue - } - // Get the field description from the comments. - fieldDescription := g.filterCommentString(field.Comments.Leading) - parameters = append(parameters, - &v3.ParameterOrReference{ - Oneof: &v3.ParameterOrReference_Parameter{ - Parameter: &v3.Parameter{ - Name: fieldName, - In: "query", - Description: fieldDescription, - Required: false, - Schema: &v3.SchemaOrReference{ - Oneof: &v3.SchemaOrReference_Schema{ - Schema: &v3.Schema{ - Type: "string", - }, + // Add the named path parameters to the operation parameters. + for _, namedPathParameter := range namedPathParameters { + if _, ok := sparameters[namedPathParameter]; ok { + continue + } + + parameters = append(parameters, + &v3.ParameterOrReference{ + Oneof: &v3.ParameterOrReference_Parameter{ + Parameter: &v3.Parameter{ + Name: namedPathParameter, + In: "path", + Required: true, + Description: "The " + namedPathParameter + " id.", + Schema: &v3.SchemaOrReference{ + Oneof: &v3.SchemaOrReference_Schema{ + Schema: &v3.Schema{ + Type: "string", }, }, }, }, - }) + }, + }) + } + } + + // Add any unhandled fields in the request message as query parameters. + if bodyField != "*" { + for _, field := range inputMessage.Fields { + fieldName := string(field.Desc.Name()) + if !contains(coveredParameters, fieldName) && fieldName != bodyField { + fieldParams := g.buildQueryParamsV3(field) + parameters = append(parameters, fieldParams...) } } } + // Create the response. responses := &v3.Responses{ ResponseOrReference: []*v3.NamedResponseOrReference{ - &v3.NamedResponseOrReference{ + { Name: "200", Value: &v3.ResponseOrReference{ Oneof: &v3.ResponseOrReference_Response{ @@ -325,29 +656,51 @@ func (g *openapiv3Generator) buildOperationV3( }, }, } + // Create the operation. op := &v3.Operation{ - Summary: description, + Tags: []string{tagName}, + Description: description, OperationId: operationID, Parameters: parameters, Responses: responses, } + + if defaultHost != "" { + hostURL, err := url.Parse(defaultHost) + if err == nil { + hostURL.Scheme = "https" + op.Servers = append(op.Servers, &v3.Server{Url: hostURL.String()}) + } + } + // If a body field is specified, we need to pass a message as the request body. if bodyField != "" { - var bodyFieldScalarTypeName string - var bodyFieldMessageTypeName string + var requestSchema *v3.SchemaOrReference + if bodyField == "*" { // Pass the entire request message as the request body. - bodyFieldMessageTypeName = g.fullMessageTypeName(inputMessage) + typeName := g.fullMessageTypeName(inputMessage.Desc) + requestSchema = g.schemaOrReferenceForType(typeName) + } else { // If body refers to a message field, use that type. for _, field := range inputMessage.Fields { if string(field.Desc.Name()) == bodyField { switch field.Desc.Kind() { case protoreflect.StringKind: - bodyFieldScalarTypeName = "string" + requestSchema = &v3.SchemaOrReference{ + Oneof: &v3.SchemaOrReference_Schema{ + Schema: &v3.Schema{ + Type: "string", + }, + }, + } + case protoreflect.MessageKind: - bodyFieldMessageTypeName = g.fullMessageTypeName(field.Message) + typeName := g.fullMessageTypeName(field.Message.Desc) + requestSchema = g.schemaOrReferenceForType(typeName) + default: log.Printf("unsupported field type %+v", field.Desc) } @@ -355,32 +708,17 @@ func (g *openapiv3Generator) buildOperationV3( } } } - var requestSchema *v3.SchemaOrReference - if bodyFieldScalarTypeName != "" { - requestSchema = &v3.SchemaOrReference{ - Oneof: &v3.SchemaOrReference_Schema{ - Schema: &v3.Schema{ - Type: bodyFieldScalarTypeName, - }, - }, - } - } else if bodyFieldMessageTypeName != "" { - requestSchema = &v3.SchemaOrReference{ - Oneof: &v3.SchemaOrReference_Reference{ - Reference: &v3.Reference{ - XRef: g.schemaReferenceForTypeName(bodyFieldMessageTypeName), - }, - }, - } - } + + ctype := getMediaType(eopt) + op.RequestBody = &v3.RequestBodyOrReference{ Oneof: &v3.RequestBodyOrReference_RequestBody{ RequestBody: &v3.RequestBody{ Required: true, Content: &v3.MediaTypes{ AdditionalProperties: []*v3.NamedMediaType{ - &v3.NamedMediaType{ - Name: "application/json", + { + Name: ctype, Value: &v3.MediaType{ Schema: requestSchema, }, @@ -410,15 +748,15 @@ func (g *openapiv3Generator) addOperationV3(d *v3.Document, op *v3.Operation, pa } // Set the operation on the specified method. switch methodName { - case http.MethodGet: + case "GET": selectedPathItem.Value.Get = op - case http.MethodPost: + case "POST": selectedPathItem.Value.Post = op - case http.MethodPut: + case "PUT": selectedPathItem.Value.Put = op - case http.MethodDelete: + case "DELETE": selectedPathItem.Value.Delete = op - case http.MethodPatch: + case "PATCH": selectedPathItem.Value.Patch = op } } @@ -428,49 +766,33 @@ func (g *openapiv3Generator) schemaReferenceForTypeName(typeName string) string if !contains(g.requiredSchemas, typeName) { g.requiredSchemas = append(g.requiredSchemas, typeName) } + + if typeName == ".google.protobuf.Value" { + return "#/components/schemas/" + protobufValueName + } + parts := strings.Split(typeName, ".") lastPart := parts[len(parts)-1] - return "#/components/schemas/" + lastPart -} - -// itemsItemForTypeName is a helper constructor. -func (g *openapiv3Generator) itemsItemForTypeName(typeName string) *v3.ItemsItem { - return &v3.ItemsItem{SchemaOrReference: []*v3.SchemaOrReference{&v3.SchemaOrReference{ - Oneof: &v3.SchemaOrReference_Schema{ - Schema: &v3.Schema{ - Type: typeName, - }, - }, - }}} -} - -// itemsItemForReference is a helper constructor. -func (g *openapiv3Generator) itemsItemForReference(xref string) *v3.ItemsItem { - return &v3.ItemsItem{SchemaOrReference: []*v3.SchemaOrReference{&v3.SchemaOrReference{ - Oneof: &v3.SchemaOrReference_Reference{ - Reference: &v3.Reference{ - XRef: xref, - }, - }, - }}} + return "#/components/schemas/" + g.formatMessageRef(lastPart) } // fullMessageTypeName builds the full type name of a message. -func (g *openapiv3Generator) fullMessageTypeName(message *protogen.Message) string { - return "." + string(message.Desc.ParentFile().Package()) + "." + string(message.Desc.Name()) +func (g *openapiv3Generator) fullMessageTypeName(message protoreflect.MessageDescriptor) string { + name := g.getMessageName(message) + return "." + string(message.ParentFile().Package()) + "." + name } func (g *openapiv3Generator) responseContentForMessage(outputMessage *protogen.Message) *v3.MediaTypes { - typeName := g.fullMessageTypeName(outputMessage) + typeName := g.fullMessageTypeName(outputMessage.Desc) if typeName == ".google.protobuf.Empty" { return &v3.MediaTypes{} } - if typeName == ".google.api.HttpBody" || typeName == ".micro.codec.Frame" { + if typeName == ".google.api.HttpBody" || typeName == ".micro.codec.Frame" || typeName == ".micro.api.HttpBody" { return &v3.MediaTypes{ AdditionalProperties: []*v3.NamedMediaType{ - &v3.NamedMediaType{ + { Name: "application/octet-stream", Value: &v3.MediaType{}, }, @@ -480,184 +802,309 @@ func (g *openapiv3Generator) responseContentForMessage(outputMessage *protogen.M return &v3.MediaTypes{ AdditionalProperties: []*v3.NamedMediaType{ - &v3.NamedMediaType{ + { Name: "application/json", Value: &v3.MediaType{ - Schema: &v3.SchemaOrReference{ - Oneof: &v3.SchemaOrReference_Reference{ - Reference: &v3.Reference{ - XRef: g.schemaReferenceForTypeName(g.fullMessageTypeName(outputMessage)), - }, - }, - }, + Schema: g.schemaOrReferenceForType(typeName), }, }, }, } } +func (g *openapiv3Generator) schemaOrReferenceForType(typeName string) *v3.SchemaOrReference { + switch typeName { + + case ".google.protobuf.Timestamp": + // Timestamps are serialized as strings + return &v3.SchemaOrReference{ + Oneof: &v3.SchemaOrReference_Schema{ + Schema: &v3.Schema{Type: "string", Format: "RFC3339"}, + }, + } + + case ".google.type.Date": + // Dates are serialized as strings + return &v3.SchemaOrReference{ + Oneof: &v3.SchemaOrReference_Schema{ + Schema: &v3.Schema{Type: "string", Format: "date"}, + }, + } + + case ".google.type.DateTime": + // DateTimes are serialized as strings + return &v3.SchemaOrReference{ + Oneof: &v3.SchemaOrReference_Schema{ + Schema: &v3.Schema{Type: "string", Format: "date-time"}, + }, + } + + case ".google.protobuf.FieldMask": + // Field masks are serialized as strings + return &v3.SchemaOrReference{ + Oneof: &v3.SchemaOrReference_Schema{ + Schema: &v3.Schema{Type: "string", Format: "field-mask"}, + }, + } + + case ".google.protobuf.Struct": + // Struct is equivalent to a JSON object + return &v3.SchemaOrReference{ + Oneof: &v3.SchemaOrReference_Schema{ + Schema: &v3.Schema{Type: "object"}, + }, + } + + case ".google.protobuf.Empty": + // Empty is close to JSON undefined than null, so ignore this field + return nil //&v3.SchemaOrReference{Oneof: &v3.SchemaOrReference_Schema{Schema: &v3.Schema{Type: "null"}}} + + default: + ref := g.schemaReferenceForTypeName(typeName) + return &v3.SchemaOrReference{ + Oneof: &v3.SchemaOrReference_Reference{ + Reference: &v3.Reference{XRef: ref}, + }, + } + } +} + +func (g *openapiv3Generator) schemaOrReferenceForField(field protoreflect.FieldDescriptor) *v3.SchemaOrReference { + if field.IsMap() { + return &v3.SchemaOrReference{ + Oneof: &v3.SchemaOrReference_Schema{ + Schema: &v3.Schema{ + Type: "object", + AdditionalProperties: &v3.AdditionalPropertiesItem{ + Oneof: &v3.AdditionalPropertiesItem_SchemaOrReference{ + SchemaOrReference: g.schemaOrReferenceForField(field.MapValue()), + }, + }, + }, + }, + } + } + + var kindSchema *v3.SchemaOrReference + + kind := field.Kind() + + switch kind { + + case protoreflect.MessageKind: + typeName := g.fullMessageTypeName(field.Message()) + kindSchema = g.schemaOrReferenceForType(typeName) + if kindSchema == nil { + return nil + } + + case protoreflect.StringKind: + kindSchema = &v3.SchemaOrReference{ + Oneof: &v3.SchemaOrReference_Schema{ + Schema: &v3.Schema{Type: "string"}, + }, + } + + case protoreflect.Int32Kind, protoreflect.Sint32Kind, protoreflect.Uint32Kind, + protoreflect.Int64Kind, protoreflect.Sint64Kind, protoreflect.Uint64Kind, + protoreflect.Sfixed32Kind, protoreflect.Fixed32Kind, protoreflect.Sfixed64Kind, + protoreflect.Fixed64Kind: + kindSchema = &v3.SchemaOrReference{ + Oneof: &v3.SchemaOrReference_Schema{ + Schema: &v3.Schema{Type: "integer", Format: kind.String()}, + }, + } + + case protoreflect.EnumKind: + kindSchema = &v3.SchemaOrReference{ + Oneof: &v3.SchemaOrReference_Schema{ + Schema: &v3.Schema{Type: "integer", Format: "enum"}, + }, + } + + case protoreflect.BoolKind: + kindSchema = &v3.SchemaOrReference{ + Oneof: &v3.SchemaOrReference_Schema{ + Schema: &v3.Schema{Type: "boolean"}, + }, + } + + case protoreflect.FloatKind, protoreflect.DoubleKind: + kindSchema = &v3.SchemaOrReference{ + Oneof: &v3.SchemaOrReference_Schema{ + Schema: &v3.Schema{Type: "number", Format: kind.String()}, + }, + } + + case protoreflect.BytesKind: + kindSchema = &v3.SchemaOrReference{ + Oneof: &v3.SchemaOrReference_Schema{ + Schema: &v3.Schema{Type: "string", Format: "bytes"}, + }, + } + + default: + log.Printf("(TODO) Unsupported field type: %+v", g.fullMessageTypeName(field.Message())) + } + + if field.IsList() { + return &v3.SchemaOrReference{ + Oneof: &v3.SchemaOrReference_Schema{ + Schema: &v3.Schema{ + Type: "array", + Items: &v3.ItemsItem{SchemaOrReference: []*v3.SchemaOrReference{kindSchema}}, + }, + }, + } + } + + return kindSchema +} + // addSchemasToDocumentV3 adds info from one file descriptor. -func (g *openapiv3Generator) addSchemasToDocumentV3(d *v3.Document, file *protogen.File) { +func (g *openapiv3Generator) addSchemasToDocumentV3(d *v3.Document, messages []*protogen.Message) { // For each message, generate a definition. - for _, message := range file.Messages { - typeName := g.fullMessageTypeName(message) + for _, message := range messages { + if message.Messages != nil { + g.addSchemasToDocumentV3(d, message.Messages) + } + + typeName := g.fullMessageTypeName(message.Desc) + // Only generate this if we need it and haven't already generated it. if !contains(g.requiredSchemas, typeName) || contains(g.generatedSchemas, typeName) { continue } + g.generatedSchemas = append(g.generatedSchemas, typeName) + + // google.protobuf.Value is handled like a special value when doing transcoding. + // It's interpreted as a "catch all" JSON value, that can be anything. + if message.Desc != nil && message.Desc.FullName() == "google.protobuf.Value" { + // Add the schema to the components.schema list. + description := protobufValueName + ` is a "catch all" type that can hold any JSON value, except null as this is not allowed in OpenAPI` + + d.Components.Schemas.AdditionalProperties = append(d.Components.Schemas.AdditionalProperties, + &v3.NamedSchemaOrReference{ + Name: protobufValueName, + Value: &v3.SchemaOrReference{ + Oneof: &v3.SchemaOrReference_Schema{ + Schema: &v3.Schema{ + Description: description, + OneOf: []*v3.SchemaOrReference{ + // type is not allow to be null in OpenAPI + { + Oneof: &v3.SchemaOrReference_Schema{ + Schema: &v3.Schema{Type: "string"}, + }, + }, { + Oneof: &v3.SchemaOrReference_Schema{ + Schema: &v3.Schema{Type: "number"}, + }, + }, { + Oneof: &v3.SchemaOrReference_Schema{ + Schema: &v3.Schema{Type: "integer"}, + }, + }, { + Oneof: &v3.SchemaOrReference_Schema{ + Schema: &v3.Schema{Type: "boolean"}, + }, + }, { + Oneof: &v3.SchemaOrReference_Schema{ + Schema: &v3.Schema{Type: "object"}, + }, + }, { + Oneof: &v3.SchemaOrReference_Schema{ + Schema: &v3.Schema{ + Type: "array", + Items: &v3.ItemsItem{ + SchemaOrReference: []*v3.SchemaOrReference{{ + Oneof: &v3.SchemaOrReference_Reference{ + Reference: &v3.Reference{XRef: "#/components/schemas/" + protobufValueName}, + }, + }}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + ) + continue + } + // Get the message description from the comments. - messageDescription := g.filterCommentString(message.Comments.Leading) + messageDescription := g.filterCommentString(message.Comments.Leading, true) + // Build an array holding the fields of the message. definitionProperties := &v3.Properties{ AdditionalProperties: make([]*v3.NamedSchemaOrReference, 0), } + + var required []string for _, field := range message.Fields { - // Check the field annotations to see if this is a readonly field. + // Check the field annotations to see if this is a readonly or writeonly field. + inputOnly := false outputOnly := false - extension := proto.GetExtension(field.Desc.Options(), annotations.E_FieldBehavior) + extension := proto.GetExtension(field.Desc.Options(), api.E_FieldBehavior) if extension != nil { switch v := extension.(type) { - case []annotations.FieldBehavior: + case []api.FieldBehavior: for _, vv := range v { - if vv == annotations.FieldBehavior_OUTPUT_ONLY { + switch vv { + case api.FieldBehavior_OUTPUT_ONLY: outputOnly = true + case api.FieldBehavior_INPUT_ONLY: + inputOnly = true + case api.FieldBehavior_REQUIRED: + required = append(required, g.formatFieldName(field)) } } default: log.Printf("unsupported extension type %T", extension) } } - // Get the field description from the comments. - fieldDescription := g.filterCommentString(field.Comments.Leading) + // The field is either described by a reference or a schema. - XRef := "" - fieldSchema := &v3.Schema{ - Description: fieldDescription, + fieldSchema := g.schemaOrReferenceForField(field.Desc) + if fieldSchema == nil { + continue } - if outputOnly { - fieldSchema.ReadOnly = true - } - if field.Desc.IsList() { - fieldSchema.Type = "array" - switch field.Desc.Kind() { - case protoreflect.MessageKind: - fieldSchema.Items = g.itemsItemForReference( - g.schemaReferenceForTypeName( - g.fullMessageTypeName(field.Message))) - case protoreflect.StringKind: - fieldSchema.Items = g.itemsItemForTypeName("string") - case protoreflect.Int32Kind, - protoreflect.Sint32Kind, - protoreflect.Uint32Kind, - protoreflect.Int64Kind, - protoreflect.Sint64Kind, - protoreflect.Uint64Kind, - protoreflect.Sfixed32Kind, - protoreflect.Fixed32Kind, - protoreflect.Sfixed64Kind, - protoreflect.Fixed64Kind: - fieldSchema.Items = g.itemsItemForTypeName("integer") - case protoreflect.EnumKind: - fieldSchema.Items = g.itemsItemForTypeName("integer") - case protoreflect.BoolKind: - fieldSchema.Items = g.itemsItemForTypeName("boolean") - case protoreflect.FloatKind, protoreflect.DoubleKind: - fieldSchema.Items = g.itemsItemForTypeName("number") - case protoreflect.BytesKind: - fieldSchema.Items = g.itemsItemForTypeName("string") - default: - log.Printf("(TODO) Unsupported array type: %+v", g.fullMessageTypeName(field.Message)) + + if schema, ok := fieldSchema.Oneof.(*v3.SchemaOrReference_Schema); ok { + // Get the field description from the comments. + schema.Schema.Description = g.filterCommentString(field.Comments.Leading, true) + if outputOnly { + schema.Schema.ReadOnly = true } - } else if field.Desc.IsMap() && - field.Desc.MapKey().Kind() == protoreflect.StringKind && - field.Desc.MapValue().Kind() == protoreflect.StringKind { - fieldSchema.Type = "object" - } else { - k := field.Desc.Kind() - switch k { - case protoreflect.MessageKind: - typeName := g.fullMessageTypeName(field.Message) - switch typeName { - case ".google.protobuf.Timestamp": - // Timestamps are serialized as strings - fieldSchema.Type = "string" - fieldSchema.Format = "RFC3339" - case ".google.type.Date": - // Dates are serialized as strings - fieldSchema.Type = "string" - fieldSchema.Format = "date" - case ".google.type.DateTime": - // DateTimes are serialized as strings - fieldSchema.Type = "string" - fieldSchema.Format = "date-time" - default: - // The field is described by a reference. - XRef = g.schemaReferenceForTypeName(typeName) - } - case protoreflect.StringKind: - fieldSchema.Type = "string" - case protoreflect.Int32Kind, - protoreflect.Sint32Kind, - protoreflect.Uint32Kind, - protoreflect.Int64Kind, - protoreflect.Sint64Kind, - protoreflect.Uint64Kind, - protoreflect.Sfixed32Kind, - protoreflect.Fixed32Kind, - protoreflect.Sfixed64Kind, - protoreflect.Fixed64Kind: - fieldSchema.Type = "integer" - fieldSchema.Format = k.String() - case protoreflect.EnumKind: - fieldSchema.Type = "integer" - fieldSchema.Format = "enum" - case protoreflect.BoolKind: - fieldSchema.Type = "boolean" - case protoreflect.FloatKind, protoreflect.DoubleKind: - fieldSchema.Type = "number" - fieldSchema.Format = k.String() - case protoreflect.BytesKind: - fieldSchema.Type = "string" - fieldSchema.Format = "bytes" - default: - log.Printf("(TODO) Unsupported field type: %+v", g.fullMessageTypeName(field.Message)) - } - } - var value *v3.SchemaOrReference - if XRef != "" { - value = &v3.SchemaOrReference{ - Oneof: &v3.SchemaOrReference_Reference{ - Reference: &v3.Reference{ - XRef: XRef, - }, - }, - } - } else { - value = &v3.SchemaOrReference{ - Oneof: &v3.SchemaOrReference_Schema{ - Schema: fieldSchema, - }, + if inputOnly { + schema.Schema.WriteOnly = true } } + definitionProperties.AdditionalProperties = append( definitionProperties.AdditionalProperties, &v3.NamedSchemaOrReference{ - Name: string(field.Desc.Name()), - Value: value, + Name: g.formatFieldName(field), + Value: fieldSchema, }, ) } // Add the schema to the components.schema list. d.Components.Schemas.AdditionalProperties = append(d.Components.Schemas.AdditionalProperties, &v3.NamedSchemaOrReference{ - Name: string(message.Desc.Name()), + Name: g.formatMessageName(message), Value: &v3.SchemaOrReference{ Oneof: &v3.SchemaOrReference_Schema{ Schema: &v3.Schema{ + Type: "object", Description: messageDescription, Properties: definitionProperties, + Required: required, }, }, }, @@ -676,6 +1123,14 @@ func contains(s []string, e string) bool { return false } +// appendUniuqe appends a string, to a string slice, if the string is not already in the slice +func appendUniuqe(s []string, e string) []string { + if !contains(s, e) { + return append(s, e) + } + return s +} + // singular produces the singular form of a collection name. func singular(plural string) string { if strings.HasSuffix(plural, "ves") { diff --git a/openapiv3_util.go b/openapiv3_util.go new file mode 100644 index 0000000..86e8689 --- /dev/null +++ b/openapiv3_util.go @@ -0,0 +1,39 @@ +package main + +import v3 "go.unistack.org/micro-proto/v3/openapiv3" + +func getMediaType(eopt interface{}) string { + ctype := "application/json" + + if eopt == nil { + return ctype + } + + if eopt == v3.E_Openapiv3Operation.InterfaceOf(v3.E_Openapiv3Operation.Zero()) { + return ctype + } + + opt, ok := eopt.(*v3.Operation) + if !ok || opt.RequestBody == nil { + return ctype + } + + if opt.GetRequestBody() == nil { + return ctype + } + + if opt.GetRequestBody().GetRequestBody() == nil { + return ctype + } + + c := opt.GetRequestBody().GetRequestBody().GetContent() + if c == nil { + return ctype + } + + for _, prop := range c.GetAdditionalProperties() { + ctype = prop.Name + } + + return ctype +} diff --git a/util.go b/util.go index dbaa3ab..3dc1e63 100644 --- a/util.go +++ b/util.go @@ -55,6 +55,9 @@ func generateServiceClientMethods(gfile *protogen.GeneratedFile, service *protog opts := proto.GetExtension(method.Desc.Options(), v2.E_Openapiv2Operation) if opts != nil { r := opts.(*v2.Operation) + if r.Responses == nil { + goto labelMethod + } gfile.P("errmap := make(map[string]interface{}, ", len(r.Responses.ResponseCode), ")") for _, rsp := range r.Responses.ResponseCode { if schema := rsp.Value.GetJsonReference(); schema != nil { @@ -62,9 +65,51 @@ func generateServiceClientMethods(gfile *protogen.GeneratedFile, service *protog if strings.HasPrefix(ref, "."+string(service.Desc.ParentFile().Package())+".") { ref = strings.TrimPrefix(ref, "."+string(service.Desc.ParentFile().Package())+".") } - if ref == "micro.codec.Frame" || ref == ".micro.codec.Frame" { + if ref[0] == '.' { + ref = ref[1:] + } + switch ref { + case "micro.codec.Frame": gfile.P(`errmap["`, rsp.Name, `"] = &`, microCodecPackage.Ident("Frame"), "{}") - } else { + case "micro.errors.Error": + gfile.P(`errmap["`, rsp.Name, `"] = &`, microErrorsPackage.Ident("Error"), "{}") + default: + gfile.P(`errmap["`, rsp.Name, `"] = &`, ref, "{}") + } + } + } + } + gfile.P("opts = append(opts,") + gfile.P(microClientHttpPackage.Ident("ErrorMap"), "(errmap),") + gfile.P(")") + } + if proto.HasExtension(method.Desc.Options(), v3.E_Openapiv3Operation) { + opts := proto.GetExtension(method.Desc.Options(), v3.E_Openapiv3Operation) + if opts != nil { + r := opts.(*v3.Operation) + if r.Responses == nil { + goto labelMethod + } + resps := r.Responses.ResponseOrReference + if r.Responses.GetDefault() != nil { + resps = append(resps, &v3.NamedResponseOrReference{Name: "default", Value: r.Responses.GetDefault()}) + } + gfile.P("errmap := make(map[string]interface{}, ", len(resps), ")") + for _, rsp := range resps { + if schema := rsp.Value.GetReference(); schema != nil { + ref := schema.XRef + if strings.HasPrefix(ref, "."+string(service.Desc.ParentFile().Package())+".") { + ref = strings.TrimPrefix(ref, "."+string(service.Desc.ParentFile().Package())+".") + } + if ref[0] == '.' { + ref = ref[1:] + } + switch ref { + case "micro.codec.Frame": + gfile.P(`errmap["`, rsp.Name, `"] = &`, microCodecPackage.Ident("Frame"), "{}") + case "micro.errors.Error": + gfile.P(`errmap["`, rsp.Name, `"] = &`, microErrorsPackage.Ident("Error"), "{}") + default: gfile.P(`errmap["`, rsp.Name, `"] = &`, ref, "{}") } } @@ -75,6 +120,7 @@ func generateServiceClientMethods(gfile *protogen.GeneratedFile, service *protog gfile.P(")") } + labelMethod: if proto.HasExtension(method.Desc.Options(), api_options.E_Http) { gfile.P("opts = append(opts,") endpoints, _ := generateEndpoints(method) @@ -93,10 +139,36 @@ func generateServiceClientMethods(gfile *protogen.GeneratedFile, service *protog parameters := make(map[string]map[string]string) // Build a list of header parameters. - eopt := proto.GetExtension(method.Desc.Options(), v3.E_Openapiv3Operation) - if eopt != nil && eopt != v3.E_Openapiv3Operation.InterfaceOf(v3.E_Openapiv3Operation.Zero()) { - opt := eopt.(*v3.Operation) + e2opt := proto.GetExtension(method.Desc.Options(), v2.E_Openapiv2Operation) + if e2opt != nil && e2opt != v2.E_Openapiv2Operation.InterfaceOf(v2.E_Openapiv2Operation.Zero()) { + opt := e2opt.(*v2.Operation) for _, paramOrRef := range opt.Parameters { + parameter := paramOrRef.GetParameter() + // NonBodyParameter() + if parameter == nil { + continue + } + nonBodyParameter := parameter.GetNonBodyParameter() + if nonBodyParameter == nil { + continue + } + headerParameter := nonBodyParameter.GetHeaderParameterSubSchema() + if headerParameter.In != "header" && headerParameter.In != "cookie" { + continue + } + in, ok := parameters[headerParameter.In] + if !ok { + in = make(map[string]string) + parameters[headerParameter.In] = in + } + in[headerParameter.Name] = fmt.Sprintf("%v", headerParameter.Required) + } + } + e3opt := proto.GetExtension(method.Desc.Options(), v3.E_Openapiv3Operation) + if e3opt != nil && e3opt != v3.E_Openapiv3Operation.InterfaceOf(v3.E_Openapiv3Operation.Zero()) { + opt := e3opt.(*v3.Operation) + for _, paramOrRef := range opt.Parameters { + parameter := paramOrRef.GetParameter() if parameter == nil { continue @@ -165,7 +237,7 @@ func generateServiceClientMethods(gfile *protogen.GeneratedFile, service *protog } if method.Desc.IsStreamingClient() && !method.Desc.IsStreamingServer() { - gfile.P("func (s *", unexport(serviceName), "Client", method.GoName, ") RecvAndClose() (*", gfile.QualifiedGoIdent(method.Output.GoIdent), ", error) {") + gfile.P("func (s *", unexport(serviceName), "Client", method.GoName, ") CloseAndRecv() (*", gfile.QualifiedGoIdent(method.Output.GoIdent), ", error) {") gfile.P("msg := &", gfile.QualifiedGoIdent(method.Output.GoIdent), "{}") gfile.P("err := s.RecvMsg(msg)") gfile.P("if err == nil {") @@ -183,6 +255,10 @@ func generateServiceClientMethods(gfile *protogen.GeneratedFile, service *protog gfile.P("return s.stream.Close()") gfile.P("}") gfile.P() + gfile.P("func (s *", unexport(serviceName), "Client", method.GoName, ") CloseSend() error {") + gfile.P("return s.stream.CloseSend()") + gfile.P("}") + gfile.P() gfile.P("func (s *", unexport(serviceName), "Client", method.GoName, ") Context() ", contextPackage.Ident("Context"), " {") gfile.P("return s.stream.Context()") gfile.P("}") @@ -248,9 +324,34 @@ func generateServiceServerMethods(gfile *protogen.GeneratedFile, service *protog } else { parameters := make(map[string]map[string]string) // Build a list of header parameters. - eopt := proto.GetExtension(method.Desc.Options(), v3.E_Openapiv3Operation) - if eopt != nil && eopt != v3.E_Openapiv3Operation.InterfaceOf(v3.E_Openapiv3Operation.Zero()) { - opt := eopt.(*v3.Operation) + e2opt := proto.GetExtension(method.Desc.Options(), v2.E_Openapiv2Operation) + if e2opt != nil && e2opt != v2.E_Openapiv2Operation.InterfaceOf(v2.E_Openapiv2Operation.Zero()) { + opt := e2opt.(*v2.Operation) + for _, paramOrRef := range opt.Parameters { + parameter := paramOrRef.GetParameter() + // NonBodyParameter() + if parameter == nil { + continue + } + nonBodyParameter := parameter.GetNonBodyParameter() + if nonBodyParameter == nil { + continue + } + headerParameter := nonBodyParameter.GetHeaderParameterSubSchema() + if headerParameter.In != "header" && headerParameter.In != "cookie" { + continue + } + in, ok := parameters[headerParameter.In] + if !ok { + in = make(map[string]string) + parameters[headerParameter.In] = in + } + in[headerParameter.Name] = fmt.Sprintf("%v", headerParameter.Required) + } + } + e3opt := proto.GetExtension(method.Desc.Options(), v3.E_Openapiv3Operation) + if e3opt != nil && e3opt != v3.E_Openapiv3Operation.InterfaceOf(v3.E_Openapiv3Operation.Zero()) { + opt := e3opt.(*v3.Operation) for _, paramOrRef := range opt.Parameters { parameter := paramOrRef.GetParameter() if parameter == nil { @@ -478,7 +579,8 @@ func generateServiceClientStreamInterface(gfile *protogen.GeneratedFile, service gfile.P("SendMsg(msg interface{}) error") gfile.P("RecvMsg(msg interface{}) error") if method.Desc.IsStreamingClient() && !method.Desc.IsStreamingServer() { - gfile.P("RecvAndClose() (*", gfile.QualifiedGoIdent(method.Output.GoIdent), ", error)") + gfile.P("CloseAndRecv() (*", gfile.QualifiedGoIdent(method.Output.GoIdent), ", error)") + gfile.P("CloseSend() () error") } gfile.P("Close() error") if method.Desc.IsStreamingClient() { @@ -505,6 +607,7 @@ func generateServiceServerStreamInterface(gfile *protogen.GeneratedFile, service gfile.P("RecvMsg(msg interface{}) error") if method.Desc.IsStreamingClient() && !method.Desc.IsStreamingServer() { gfile.P("SendAndClose(msg *", gfile.QualifiedGoIdent(method.Output.GoIdent), ") error") + gfile.P("CloseSend() error") } gfile.P("Close() error") if method.Desc.IsStreamingClient() { diff --git a/variables.go b/variables.go index c7e7397..0cf425b 100644 --- a/variables.go +++ b/variables.go @@ -17,6 +17,7 @@ var ( microClientHttpPackage = protogen.GoImportPath("go.unistack.org/micro-client-http/v3") microServerHttpPackage = protogen.GoImportPath("go.unistack.org/micro-server-http/v3") microCodecPackage = protogen.GoImportPath("go.unistack.org/micro/v3/codec") + microErrorsPackage = protogen.GoImportPath("go.unistack.org/micro/v3/errors") timePackage = protogen.GoImportPath("time") deprecationComment = "// Deprecated: Do not use." versionComment = "v3.5.3" -- 2.45.2