├── .gitignore ├── .travis.yml ├── LICENSE ├── MAINTAINERS ├── README.md ├── go.mod ├── go.sum ├── handlers.go ├── handlers_test.go ├── jsonrpc2.go ├── middleware.go ├── parser ├── helpers.go ├── helpers_test.go ├── import.go ├── parser.go ├── parser_test.go └── struct.go ├── server.go ├── server_test.go ├── smd └── model.go ├── testdata ├── arith.go ├── arithsrv │ └── main.go ├── catalogue.go ├── model │ └── model.go ├── objects │ └── objects.go ├── phonebook.go ├── printer.go ├── subservice │ ├── subarithservice.go │ ├── subarithservice_zenrpc.go │ └── types.go └── testdata_zenrpc.go └── zenrpc ├── main.go └── template.go /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.x 5 | - master 6 | 7 | script: 8 | - go test -race -coverprofile=coverage.txt -covermode=atomic 9 | 10 | after_success: 11 | - bash <(curl -s https://codecov.io/bash) 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright © 2017, SEMRush CY Ltd. (written by Sergey Bykov, Vladimir Tereschenko, Andrei Simonov). 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /MAINTAINERS: -------------------------------------------------------------------------------- 1 | Sergey Bykov 2 | Vladimir Tereschenko 3 | Andrei Simonov 4 | Mikhail Eremin -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zenrpc: JSON-RPC 2.0 Server Implementation with SMD support 2 | 3 | [![Go Report Card](https://goreportcard.com/badge/github.com/semrush/zenrpc)](https://goreportcard.com/report/github.com/semrush/zenrpc) [![Build Status](https://travis-ci.org/semrush/zenrpc.svg?branch=master)](https://travis-ci.org/semrush/zenrpc) [![codecov](https://codecov.io/gh/semrush/zenrpc/branch/master/graph/badge.svg)](https://codecov.io/gh/semrush/zenrpc) [![GoDoc](https://godoc.org/github.com/semrush/zenrpc?status.svg)](https://godoc.org/github.com/semrush/zenrpc) 4 | 5 | `zenrpc` is a JSON-RPC 2.0 server library with [Service Mapping Description](https://dojotoolkit.org/reference-guide/1.8/dojox/rpc/smd.html) support. 6 | It's built on top of `go generate` instead of reflection. 7 | 8 | # How to Use 9 | 10 | ```Service is struct with RPC methods, service represents RPC namespace.``` 11 | 12 | 1. Install zenrpc generator `go get github.com/semrush/zenrpc/v2/zenrpc` 13 | 1. Import `github.com/semrush/zenrpc/v2` into our code with rpc service. 14 | 1. Add trailing comment `//zenrpc` to your service or embed `zenrpc.Service` into your service struct. 15 | 1. Write your funcs almost as usual. 16 | 1. Do not forget run `go generate` or `zenrpc` for magic 17 | 18 | ### Accepted Method Signatures 19 | 20 | func(Service) Method([args]) (, ) 21 | func(Service) Method([args]) 22 | func(Service) Method([args]) 23 | func(Service) Method([args]) 24 | 25 | - Value could be a pointer 26 | - Error is error or *zenrpc.Error 27 | 28 | ## Example 29 | ```go 30 | package main 31 | 32 | import ( 33 | "flag" 34 | "context" 35 | "errors" 36 | "math" 37 | "log" 38 | "net/http" 39 | "os" 40 | 41 | "github.com/semrush/zenrpc/v2" 42 | "github.com/semrush/zenrpc/v2/testdata" 43 | ) 44 | 45 | type ArithService struct{ zenrpc.Service } 46 | 47 | // Sum sums two digits and returns error with error code as result and IP from context. 48 | func (as ArithService) Sum(ctx context.Context, a, b int) (bool, *zenrpc.Error) { 49 | r, _ := zenrpc.RequestFromContext(ctx) 50 | 51 | return true, zenrpc.NewStringError(a+b, r.Host) 52 | } 53 | 54 | // Multiply multiples two digits and returns result. 55 | func (as ArithService) Multiply(a, b int) int { 56 | return a * b 57 | } 58 | 59 | type Quotient struct { 60 | Quo, Rem int 61 | } 62 | 63 | func (as ArithService) Divide(a, b int) (quo *Quotient, err error) { 64 | if b == 0 { 65 | return nil, errors.New("divide by zero") 66 | } else if b == 1 { 67 | return nil, zenrpc.NewError(401, errors.New("we do not serve 1")) 68 | } 69 | 70 | return &Quotient{ 71 | Quo: a / b, 72 | Rem: a % b, 73 | }, nil 74 | } 75 | 76 | // Pow returns x**y, the base-x exponential of y. If Exp is not set then default value is 2. 77 | //zenrpc:exp=2 78 | func (as ArithService) Pow(base float64, exp float64) float64 { 79 | return math.Pow(base, exp) 80 | } 81 | 82 | //go:generate zenrpc 83 | 84 | func main() { 85 | addr := flag.String("addr", "localhost:9999", "listen address") 86 | flag.Parse() 87 | 88 | rpc := zenrpc.NewServer(zenrpc.Options{ExposeSMD: true}) 89 | rpc.Register("arith", testdata.ArithService{}) 90 | rpc.Register("", testdata.ArithService{}) // public 91 | rpc.Use(zenrpc.Logger(log.New(os.Stderr, "", log.LstdFlags))) 92 | 93 | http.Handle("/", rpc) 94 | 95 | log.Printf("starting arithsrv on %s", *addr) 96 | log.Fatal(http.ListenAndServe(*addr, nil)) 97 | } 98 | 99 | ``` 100 | 101 | 102 | ## Magic comments 103 | 104 | All comments are optional. 105 | 106 | Method comments 107 | //zenrpc:[=][whitespaces] 108 | //zenrpc:[whitespaces] 109 | //zenrpc:return[whitespaces] 110 | 111 | Struct comments 112 | type MyService struct {} //zenrpc 113 | 114 | ## Need to browse your api and do some test api calls? 115 | We recommend to use [SMDBox](https://github.com/semrush/smdbox). It is Swagger-like JSON RPC API browser, compatible with smd scheme, generated by zenrpc. 116 | 117 | # JSON-RPC 2.0 Supported Features 118 | 119 | * [x] Requests 120 | * [x] Single requests 121 | * [x] Batch requests 122 | * [x] Notifications 123 | * [x] Parameters 124 | * [x] Named 125 | * [x] Position 126 | * [x] Default values 127 | * [x] SMD Schema 128 | * [x] Input 129 | * [x] Output 130 | * [x] Codes 131 | * [ ] Scopes for OAuth 132 | 133 | # Server Library Features 134 | 135 | * [x] go generate 136 | * [ ] Transports 137 | * [x] HTTP 138 | * [x] WebSocket 139 | * [ ] RabbitMQ 140 | * [x] Server middleware 141 | * [x] Basic support 142 | * [x] Metrics 143 | * [x] Logging 144 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/semrush/zenrpc/v2 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/gorilla/websocket v1.4.2 7 | github.com/prometheus/client_golang v1.7.1 8 | github.com/smartystreets/goconvey v1.6.4 9 | github.com/thoas/go-funk v0.6.0 10 | golang.org/x/tools v0.0.0-20200729173947-1c30660f9f89 11 | ) 12 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 2 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 3 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 4 | github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 5 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 6 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 7 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 8 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 9 | github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= 10 | github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 11 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 13 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 14 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 15 | github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 16 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 17 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 18 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 19 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 20 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 21 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 22 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 23 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 24 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 25 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 26 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 27 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 28 | github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= 29 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 30 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 31 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 32 | github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= 33 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 34 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 35 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= 36 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 37 | github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= 38 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 39 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 40 | github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 41 | github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= 42 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 43 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 44 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 45 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 46 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 47 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 48 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 49 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 50 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 51 | github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= 52 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 53 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 54 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 55 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 56 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 57 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 58 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 59 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 60 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 61 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 62 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 63 | github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= 64 | github.com/prometheus/client_golang v1.7.1 h1:NTGy1Ja9pByO+xAeH/qiWnLrKtr3hJPNjaVUwnjpdpA= 65 | github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= 66 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 67 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 68 | github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= 69 | github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 70 | github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 71 | github.com/prometheus/common v0.10.0 h1:RyRA7RzGXQZiW+tGMr7sxa85G1z0yOpM1qq5c8lNawc= 72 | github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= 73 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 74 | github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 75 | github.com/prometheus/procfs v0.1.3 h1:F0+tqvhOksq22sc6iCHF5WGlWjdwj92p0udFh1VFBS8= 76 | github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= 77 | github.com/semrush/zenrpc v1.1.1 h1:McE4BFoXP95NnDU+tQHhfzVpmODS4p55JKXxHR64nx4= 78 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 79 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 80 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= 81 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 82 | github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= 83 | github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 84 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 85 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 86 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 87 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 88 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 89 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 90 | github.com/thoas/go-funk v0.6.0 h1:ryxN0pa9FnI7YHgODdLIZ4T6paCZJt8od6N9oRztMxM= 91 | github.com/thoas/go-funk v0.6.0/go.mod h1:+IWnUfUmFO1+WVYQWQtIJHeRRdaIyyYglZN7xzUPe4Q= 92 | github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 93 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 94 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 95 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 96 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 97 | golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= 98 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 99 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 100 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 101 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 102 | golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 103 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 104 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 105 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 106 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 107 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 108 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 109 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 110 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 111 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 112 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 113 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 114 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 115 | golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 116 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 117 | golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1 h1:ogLJMz+qpzav7lGMh10LMvAkM/fAoGlaiiHYiFYdm80= 118 | golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 119 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 120 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 121 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 122 | golang.org/x/tools v0.0.0-20200729173947-1c30660f9f89 h1:Vr0/urnX/81QM48v4snko5AmpNh2rTiepXMSvBRZ4Xg= 123 | golang.org/x/tools v0.0.0-20200729173947-1c30660f9f89/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 124 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 125 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 126 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 127 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 128 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 129 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 130 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 131 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 132 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 133 | google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= 134 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 135 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 136 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 137 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 138 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 139 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 140 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 141 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 142 | gopkg.in/yaml.v2 v2.2.5 h1:ymVxjfMaHvXD8RqPRmzHHsB3VvucivSkIAvJFDI5O3c= 143 | gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 144 | -------------------------------------------------------------------------------- /handlers.go: -------------------------------------------------------------------------------- 1 | package zenrpc 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "net/http" 7 | "strings" 8 | "time" 9 | 10 | "github.com/gorilla/websocket" 11 | ) 12 | 13 | type Printer interface { 14 | Printf(string, ...interface{}) 15 | } 16 | 17 | // ServeHTTP process JSON-RPC 2.0 requests via HTTP. 18 | // http://www.simple-is-better.org/json-rpc/transport_http.html 19 | func (s Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { 20 | // check for CORS GET & POST requests 21 | if s.options.AllowCORS { 22 | w.Header().Set("Access-Control-Allow-Origin", "*") 23 | } 24 | 25 | // check for smd parameter and server settings and write schema if all conditions met, 26 | if _, ok := r.URL.Query()["smd"]; ok && s.options.ExposeSMD && r.Method == http.MethodGet { 27 | b, _ := json.Marshal(s.SMD()) 28 | w.Write(b) 29 | return 30 | } 31 | 32 | // check for CORS OPTIONS pre-requests for POST https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS 33 | if s.options.AllowCORS && r.Method == http.MethodOptions { 34 | w.Header().Set("Allow", "OPTIONS, GET, POST") 35 | w.Header().Set("Access-Control-Allow-Methods", "OPTIONS, GET, POST") 36 | w.Header().Set("Access-Control-Allow-Headers", "X-PINGOTHER, Content-Type") 37 | w.Header().Set("Access-Control-Max-Age", "86400") 38 | w.WriteHeader(http.StatusOK) 39 | return 40 | } 41 | 42 | // check for content-type and POST method. 43 | if !s.options.DisableTransportChecks { 44 | if !strings.HasPrefix(r.Header.Get("Content-Type"), contentTypeJSON) { 45 | w.WriteHeader(http.StatusUnsupportedMediaType) 46 | return 47 | } else if r.Method == http.MethodGet { 48 | w.WriteHeader(http.StatusMethodNotAllowed) 49 | return 50 | } else if r.Method != http.MethodPost { 51 | // skip rpc calls 52 | return 53 | } 54 | } 55 | 56 | // ok, method is POST and content-type is application/json, process body 57 | b, err := ioutil.ReadAll(r.Body) 58 | var data interface{} 59 | 60 | if err != nil { 61 | s.printf("read request body failed with err=%v", err) 62 | data = NewResponseError(nil, ParseError, "", nil) 63 | } else { 64 | data = s.process(newRequestContext(r.Context(), r), b) 65 | } 66 | 67 | // if responses is empty -> all requests are notifications -> exit immediately 68 | if data == nil { 69 | return 70 | } 71 | 72 | // set headers 73 | w.Header().Set("Content-Type", contentTypeJSON) 74 | 75 | // marshals data and write it to client. 76 | if resp, err := json.Marshal(data); err != nil { 77 | s.printf("marshal json response failed with err=%v", err) 78 | w.WriteHeader(http.StatusInternalServerError) 79 | } else if _, err := w.Write(resp); err != nil { 80 | s.printf("write response failed with err=%v", err) 81 | w.WriteHeader(http.StatusInternalServerError) 82 | } 83 | 84 | return 85 | } 86 | 87 | // ServeWS processes JSON-RPC 2.0 requests via Gorilla WebSocket. 88 | // https://github.com/gorilla/websocket/blob/master/examples/echo/ 89 | func (s Server) ServeWS(w http.ResponseWriter, r *http.Request) { 90 | c, err := s.options.Upgrader.Upgrade(w, r, nil) 91 | if err != nil { 92 | s.printf("upgrade connection failed with err=%v", err) 93 | return 94 | } 95 | defer c.Close() 96 | 97 | for { 98 | mt, message, err := c.ReadMessage() 99 | 100 | // normal closure 101 | if websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway) { 102 | break 103 | } 104 | // abnormal closure 105 | if err != nil { 106 | s.printf("read message failed with err=%v", err) 107 | break 108 | } 109 | 110 | data, err := s.Do(newRequestContext(r.Context(), r), message) 111 | if err != nil { 112 | s.printf("marshal json response failed with err=%v", err) 113 | c.WriteControl(websocket.CloseInternalServerErr, nil, time.Time{}) 114 | break 115 | } 116 | 117 | if err = c.WriteMessage(mt, data); err != nil { 118 | s.printf("write response failed with err=%v", err) 119 | c.WriteControl(websocket.CloseInternalServerErr, nil, time.Time{}) 120 | break 121 | } 122 | } 123 | } 124 | 125 | // SMDBoxHandler is a handler for SMDBox web app. 126 | func SMDBoxHandler(w http.ResponseWriter, r *http.Request) { 127 | w.Write([]byte(` 128 | 129 | 130 | 131 | 132 | SMD Box 133 | 134 | 135 | 136 |
137 | 138 | 139 | `)) 140 | } 141 | -------------------------------------------------------------------------------- /handlers_test.go: -------------------------------------------------------------------------------- 1 | package zenrpc_test 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "io/ioutil" 8 | "log" 9 | "net/http" 10 | "net/http/httptest" 11 | "net/url" 12 | "strings" 13 | "testing" 14 | 15 | "github.com/gorilla/websocket" 16 | "github.com/semrush/zenrpc/v2" 17 | "github.com/semrush/zenrpc/v2/testdata" 18 | ) 19 | 20 | func TestServer_ServeHTTPWithHeaders(t *testing.T) { 21 | ts := httptest.NewServer(http.HandlerFunc(rpc.ServeHTTP)) 22 | defer ts.Close() 23 | 24 | var tc = []struct { 25 | h string 26 | s int 27 | }{ 28 | { 29 | h: "application/json", 30 | s: 200, 31 | }, 32 | { 33 | h: "application/json; charset=utf-8", 34 | s: 200, 35 | }, 36 | { 37 | h: "application/text; charset=utf-8", 38 | s: 415, 39 | }, 40 | } 41 | 42 | for _, c := range tc { 43 | res, err := http.Post(ts.URL, c.h, bytes.NewBufferString(`{"jsonrpc": "2.0", "method": "arith.pi", "id": 2 }`)) 44 | if err != nil { 45 | log.Fatal(err) 46 | } 47 | 48 | if res.StatusCode != c.s { 49 | t.Errorf("Input: %s\n got %d expected %d", c.h, res.StatusCode, c.s) 50 | } 51 | } 52 | } 53 | 54 | func TestServer_ServeHTTP(t *testing.T) { 55 | ts := httptest.NewServer(http.HandlerFunc(rpc.ServeHTTP)) 56 | defer ts.Close() 57 | 58 | var tc = []struct { 59 | in, out string 60 | }{ 61 | { 62 | in: `{"jsonrpc": "2.0", "method": "arith.divide", "params": { "a": 1, "b": 24 }, "id": 1 }`, 63 | out: `{"jsonrpc":"2.0","id":1,"result":{"Quo":0,"rem":1}}`}, 64 | { 65 | in: `{"jsonrpc": "2.0", "method": "arith.divide", "params": [ 1, 24 ], "id": 1 }`, 66 | out: `{"jsonrpc":"2.0","id":1,"result":{"Quo":0,"rem":1}}`}, 67 | { 68 | in: `{"jsonrpc": "2.0", "method": "arith.divide", "params": { "a": 1, "b": 0 }, "id": 1 }`, 69 | out: `{"jsonrpc":"2.0","id":1,"error":{"code":-32603,"message":"divide by zero"}}`}, 70 | { 71 | in: `{"jsonrpc": "2.0", "method": "Arith.Divide", "params": { "a": 1, "b": 1 }, "id": "1" }`, 72 | out: `{"jsonrpc":"2.0","id":"1","error":{"code":401,"message":"we do not serve 1"}}`}, 73 | { 74 | in: `{"jsonrpc": "2.0", "method": "arith.multiply", "params": { "a": 3, "b": 2 }, "id": 0 }`, 75 | out: `{"jsonrpc":"2.0","id":0,"result":6}`}, 76 | { 77 | in: `{"jsonrpc": "2.0", "method": "multiply", "params": { "a": 4, "b": 2 }, "id": 0 }`, 78 | out: `{"jsonrpc":"2.0","id":0,"result":8}`}, 79 | { 80 | in: `{"jsonrpc": "2.0", "method": "arith.pow", "params": { "base": 3, "exp": 3 }, "id": 0 }`, 81 | out: `{"jsonrpc":"2.0","id":0,"result":27}`}, 82 | { 83 | in: `{"jsonrpc": "2.0", "method": "arith.sum", "params": { "a": 3, "b": 3 }, "id": 1 }`, 84 | out: `{"jsonrpc":"2.0","id":1,"error":{"code":6,"message":"` + ts.Listener.Addr().String() + `"}}`}, 85 | { 86 | in: `{"jsonrpc": "2.0", "method": "arith.pow", "params": { "base": 3 }, "id": 0 }`, 87 | out: `{"jsonrpc":"2.0","id":0,"result":9}`}, 88 | { 89 | in: `{"jsonrpc": "2.0", "method": "arith.pow", "params": [ 3 ], "id": 0 }`, 90 | out: `{"jsonrpc":"2.0","id":0,"result":9}`}, 91 | { 92 | in: `{"jsonrpc": "2.0", "method": "arith.pow", "params": [ 3, 3 ], "id": 0 }`, 93 | out: `{"jsonrpc":"2.0","id":0,"result":27}`}, 94 | { 95 | in: `{"jsonrpc": "2.0", "method": "arith.pi", "id": 0 }`, 96 | out: `{"jsonrpc":"2.0","id":0,"result":3.141592653589793}`}, 97 | { 98 | in: `{"jsonrpc": "2.0", "method": "arith.checkerror", "id": 0, "params": [ false ] }`, 99 | out: `{"jsonrpc":"2.0","id":0,"result":null}`}, 100 | { 101 | in: `{"jsonrpc": "2.0", "method": "arith.checkerror", "id": 0, "params": [ true ] }`, 102 | out: `{"jsonrpc":"2.0","id":0,"error":{"code":-32603,"message":"test"}}`}, 103 | { 104 | in: `{"jsonrpc": "2.0", "method": "arith.checkzenrpcerror", "id": 0, "params": [ false ] }`, 105 | out: `{"jsonrpc":"2.0","id":0,"result":null}`}, 106 | { 107 | in: `{"jsonrpc": "2.0", "method": "arith.checkzenrpcerror", "id": 0, "params": [ true ] }`, 108 | out: `{"jsonrpc":"2.0","id":0,"error":{"code":500,"message":"test"}}`}, 109 | } 110 | 111 | for _, c := range tc { 112 | res, err := http.Post(ts.URL, "application/json", bytes.NewBufferString(c.in)) 113 | if err != nil { 114 | log.Fatal(err) 115 | } 116 | 117 | resp, err := ioutil.ReadAll(res.Body) 118 | res.Body.Close() 119 | if err != nil { 120 | log.Fatal(err) 121 | } 122 | 123 | if string(resp) != c.out { 124 | t.Errorf("Input: %s\n got %s expected %s", c.in, resp, c.out) 125 | } 126 | } 127 | } 128 | 129 | func TestServer_ServeHTTPNotifications(t *testing.T) { 130 | ts := httptest.NewServer(http.HandlerFunc(rpc.ServeHTTP)) 131 | defer ts.Close() 132 | 133 | var tc = []struct { 134 | in, out string 135 | }{ 136 | { 137 | in: `{"jsonrpc": "2.0", "method": "arith.divide", "params": { "a": 1, "b": 24 }}`, 138 | out: ``}, 139 | { 140 | // should be empty even with error 141 | in: `{"jsonrpc": "2.0", "method": "arith.divide", "params": { "a": 1, "b": 0 }}`, 142 | out: ``}, 143 | { 144 | // but parse errors should be displayed 145 | in: `{"jsonrpc": "1.0", "method": "Arith.Divide", "params": { "a": 1, "b": 1 }`, 146 | out: `{"jsonrpc":"2.0","id":null,"error":{"code":-32700,"message":"Parse error"}}`}, 147 | { 148 | // in batch requests notifications should not be listed in response 149 | in: `[{"jsonrpc": "2.0", "method": "arith.multiply", "params": { "a": 3, "b": 2 }, "id": 0 }, 150 | {"jsonrpc": "2.0", "method": "arith.pow", "params": { "base": 2, "exp": 2 } }]`, 151 | out: `[{"jsonrpc":"2.0","id":0,"result":6}]`}, 152 | { 153 | // order doesn't matter 154 | in: `[{"jsonrpc": "2.0", "method": "arith.multiply", "params": { "a": 3, "b": 2 } }, 155 | {"jsonrpc": "2.0", "method": "arith.pow", "params": { "base": 2, "exp": 2 }, "id": 0 }]`, 156 | out: `[{"jsonrpc":"2.0","id":0,"result":4}]`}, 157 | { 158 | // all notifications 159 | in: `[{"jsonrpc": "2.0", "method": "arith.multiply", "params": { "a": 3, "b": 2 } }, 160 | {"jsonrpc": "2.0", "method": "arith.pow", "params": { "base": 2, "exp": 2 }}]`, 161 | out: ``}, 162 | } 163 | 164 | for _, c := range tc { 165 | res, err := http.Post(ts.URL, "application/json", bytes.NewBufferString(c.in)) 166 | if err != nil { 167 | log.Fatal(err) 168 | } 169 | 170 | resp, err := ioutil.ReadAll(res.Body) 171 | res.Body.Close() 172 | if err != nil { 173 | log.Fatal(err) 174 | } 175 | 176 | if string(resp) != c.out { 177 | t.Errorf("Input: %s\n got %s expected %s", c.in, resp, c.out) 178 | } 179 | } 180 | } 181 | 182 | func TestServer_ServeHTTPBatch(t *testing.T) { 183 | ts := httptest.NewServer(http.HandlerFunc(rpc.ServeHTTP)) 184 | defer ts.Close() 185 | 186 | var tc = []struct { 187 | in string 188 | out []string 189 | }{ 190 | { 191 | // batch requests should process asynchronously, any order in responses accepted 192 | in: `[{"jsonrpc": "2.0", "method": "arith.multiply", "params": { "a": 3, "b": 2 }, "id": 0 }, 193 | {"jsonrpc": "2.0", "method": "arith.multiply", "params": { "a": 3, "b": 3 }, "id": 1 }, 194 | {"jsonrpc": "2.0", "method": "arith.pow", "params": { "a": 2, "b": 3 } }, 195 | {"jsonrpc": "2.0", "method": "arith.pow", "params": { "base": 2, "exp": 2 }, "id": 2 }]`, 196 | out: []string{ 197 | `{"jsonrpc":"2.0","id":1,"result":9}`, 198 | `{"jsonrpc":"2.0","id":0,"result":6}`, 199 | `{"jsonrpc":"2.0","id":2,"result":4}`}}, 200 | { 201 | // one of the requests errored 202 | in: `[{"jsonrpc": "2.0", "method": "arith.multiply1", "params": { "a": 3, "b": 2 }, "id": 0 }, 203 | {"jsonrpc": "2.0", "method": "arith.multiply", "params": { "a": 3, "b": 3 }, "id": 1 }, 204 | {"jsonrpc": "2.0", "method": "arith.pow", "params": { "a": 2, "b": 3 } }, 205 | {"jsonrpc": "2.0", "method": "arith.pow", "params": { "base": 2, "exp": 2 }, "id": 2 }]`, 206 | out: []string{ 207 | `{"jsonrpc":"2.0","id":1,"result":9}`, 208 | `{"jsonrpc":"2.0","id":0,"error":{"code":-32601,"message":"Method not found"}}`, 209 | `{"jsonrpc":"2.0","id":2,"result":4}`}}, 210 | { 211 | // to much batch requests 212 | in: `[{"jsonrpc": "2.0", "method": "arith.multiply1", "params": { "a": 3, "b": 2 }, "id": 0 }, 213 | {"jsonrpc": "2.0", "method": "arith.multiply", "params": { "a": 3, "b": 3 }, "id": 1 }, 214 | {"jsonrpc": "2.0", "method": "arith.pow", "params": { "a": 2, "b": 3 } }, 215 | {"jsonrpc": "2.0", "method": "arith.pow", "params": { "a": 2, "b": 3 } }, 216 | {"jsonrpc": "2.0", "method": "arith.pow", "params": { "a": 2, "b": 3 } }, 217 | {"jsonrpc": "2.0", "method": "arith.pow", "params": { "base": 2, "exp": 2 }, "id": 2 }]`, 218 | out: []string{ 219 | `{"jsonrpc":"2.0","id":null,"error":{"code":-32600,"message":"Invalid Request","data":"max requests length in batch exceeded"}}`}}, 220 | } 221 | 222 | for _, c := range tc { 223 | res, err := http.Post(ts.URL, "application/json", bytes.NewBufferString(c.in)) 224 | if err != nil { 225 | log.Fatal(err) 226 | } 227 | 228 | resp, err := ioutil.ReadAll(res.Body) 229 | res.Body.Close() 230 | if err != nil { 231 | log.Fatal(err) 232 | } 233 | 234 | // checking if count of responses is correct 235 | if cnt := strings.Count(string(resp), `"jsonrpc":"2.0"`); len(c.out) != cnt { 236 | t.Errorf("Input: %s\n got %d in batch expected %d", c.in, cnt, len(c.out)) 237 | } 238 | 239 | // checking every response variant to be in response 240 | for _, check := range c.out { 241 | if !strings.Contains(string(resp), check) { 242 | t.Errorf("Input: %s\n not found %s in batch %s", c.in, check, resp) 243 | } 244 | } 245 | } 246 | } 247 | 248 | func TestServer_ServeHTTPWithErrors(t *testing.T) { 249 | rpcHiddenErrorField := zenrpc.NewServer(zenrpc.Options{AllowCORS: true, HideErrorDataField: true}) 250 | rpcHiddenErrorField.Register("arith", &testdata.ArithService{}) 251 | 252 | ts := httptest.NewServer(http.HandlerFunc(rpc.ServeHTTP)) 253 | defer ts.Close() 254 | 255 | tsHid := httptest.NewServer(http.HandlerFunc(rpcHiddenErrorField.ServeHTTP)) 256 | defer tsHid.Close() 257 | 258 | var tc = []struct { 259 | url string 260 | in, out string 261 | }{ 262 | { 263 | url: ts.URL, 264 | in: `{"jsonrpc": "2.0", "method": "multiple1", "id": 1 }`, 265 | out: `{"jsonrpc":"2.0","id":1,"error":{"code":-32601,"message":"Method not found"}}`}, 266 | { 267 | url: ts.URL, 268 | in: `{"jsonrpc": "2.0", "method": "test.multiple1", "id": 1 }`, 269 | out: `{"jsonrpc":"2.0","id":1,"error":{"code":-32601,"message":"Method not found"}}`}, 270 | { 271 | url: ts.URL, 272 | in: `{"jsonrpc": "2.0", "method": "foobar, "params": "bar", "baz]`, 273 | out: `{"jsonrpc":"2.0","id":null,"error":{"code":-32700,"message":"Parse error"}}`}, 274 | { 275 | url: ts.URL, 276 | in: `{"jsonrpc": "2.0", "params": { "a": 1, "b": 0 }, "id": 1 }`, 277 | out: `{"jsonrpc":"2.0","id":1,"error":{"code":-32600,"message":"Invalid Request"}}`}, 278 | { 279 | url: ts.URL, 280 | in: `{"jsonrpc": "2.0", "method": 1, "params": "bar"}`, 281 | out: `{"jsonrpc":"2.0","id":null,"error":{"code":-32700,"message":"Parse error"}}`, 282 | // in spec: {"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": null} 283 | }, 284 | { 285 | url: ts.URL, 286 | in: `{"jsonrpc": "2.0", "method": "arith.pow", "params": { "base": "3" }, "id": 0 }`, 287 | out: `{"jsonrpc":"2.0","id":0,"error":{"code":-32602,"message":"Invalid params","data":"json: cannot unmarshal string into Go struct field .base of type float64"}}`, 288 | }, 289 | { 290 | url: tsHid.URL, 291 | in: `{"jsonrpc": "2.0", "method": "arith.pow", "params": { "base": "3" }, "id": 0 }`, 292 | out: `{"jsonrpc":"2.0","id":0,"error":{"code":-32602,"message":"Invalid params"}}`, 293 | }, 294 | } 295 | 296 | for _, c := range tc { 297 | res, err := http.Post(c.url, "application/json", bytes.NewBufferString(c.in)) 298 | if err != nil { 299 | log.Fatal(err) 300 | } 301 | 302 | resp, err := ioutil.ReadAll(res.Body) 303 | res.Body.Close() 304 | if err != nil { 305 | log.Fatal(err) 306 | } 307 | 308 | if string(resp) != c.out { 309 | t.Errorf("Input: %s\n got %s expected %s", c.in, resp, c.out) 310 | } 311 | } 312 | } 313 | 314 | func TestServer_Extensions(t *testing.T) { 315 | middleware := func(h zenrpc.InvokeFunc) zenrpc.InvokeFunc { 316 | return func(ctx context.Context, method string, params json.RawMessage) zenrpc.Response { 317 | r := h(ctx, method, params) 318 | 319 | // ignore multiply method 320 | if method != testdata.RPC.ArithService.Multiply { 321 | r.Extensions = map[string]interface{}{"debug": "true"} 322 | } 323 | 324 | return r 325 | } 326 | } 327 | 328 | server := zenrpc.NewServer(zenrpc.Options{AllowCORS: true, HideErrorDataField: true}) 329 | server.Register("arith", &testdata.ArithService{}) 330 | server.Use(middleware) 331 | 332 | ts := httptest.NewServer(http.HandlerFunc(server.ServeHTTP)) 333 | defer ts.Close() 334 | 335 | var tc = []struct { 336 | url string 337 | in, out string 338 | }{ 339 | { 340 | url: ts.URL, 341 | in: `{"jsonrpc": "2.0", "method": "multiple1", "id": 1 }`, 342 | out: `{"jsonrpc":"2.0","id":1,"error":{"code":-32601,"message":"Method not found"}}`}, 343 | { 344 | url: ts.URL, 345 | in: `{"jsonrpc": "2.0", "method": "arith.divide", "params": { "a": 1, "b": 24 }}`, 346 | out: ``}, 347 | { 348 | url: ts.URL, 349 | in: `{"jsonrpc": "2.0", "method": "arith.divide", "params": { "a": 1, "b": 24 }, "id": 1 }`, 350 | out: `{"jsonrpc":"2.0","id":1,"result":{"Quo":0,"rem":1},"extensions":{"debug":"true"}}`}, 351 | { 352 | url: ts.URL, 353 | in: `{"jsonrpc": "2.0", "method": "arith.multiply", "params": { "a": 1, "b": 24 }, "id": 1 }`, 354 | out: `{"jsonrpc":"2.0","id":1,"result":24}`}, 355 | { 356 | url: ts.URL, 357 | in: `{"jsonrpc": "2.0", "method": "arith.checkerror", "params": [ true ], "id": 1 }`, 358 | out: `{"jsonrpc":"2.0","id":1,"error":{"code":-32603,"message":"test"},"extensions":{"debug":"true"}}`}, 359 | } 360 | 361 | for _, c := range tc { 362 | res, err := http.Post(c.url, "application/json", bytes.NewBufferString(c.in)) 363 | if err != nil { 364 | log.Fatal(err) 365 | } 366 | 367 | resp, err := ioutil.ReadAll(res.Body) 368 | res.Body.Close() 369 | if err != nil { 370 | log.Fatal(err) 371 | } 372 | 373 | if string(resp) != c.out { 374 | t.Errorf("Input: %s\n got %s expected %s", c.in, resp, c.out) 375 | } 376 | } 377 | } 378 | 379 | func TestServer_ServeWS(t *testing.T) { 380 | ts := httptest.NewServer(http.HandlerFunc(rpc.ServeWS)) 381 | defer ts.Close() 382 | 383 | u, _ := url.Parse(ts.URL) 384 | u.Scheme = "ws" 385 | 386 | ws, _, err := websocket.DefaultDialer.Dial(u.String(), nil) 387 | if err != nil { 388 | log.Fatal(err) 389 | } 390 | defer ws.Close() 391 | 392 | var tc = []struct { 393 | in, out string 394 | }{ 395 | { 396 | in: `{"jsonrpc": "2.0", "method": "arith.divide", "params": { "a": 1, "b": 24 }, "id": 1 }`, 397 | out: `{"jsonrpc":"2.0","id":1,"result":{"Quo":0,"rem":1}}`}, 398 | { 399 | in: `{"jsonrpc": "2.0", "method": "arith.divide", "params": { "a": 1, "b": 0 }, "id": 1 }`, 400 | out: `{"jsonrpc":"2.0","id":1,"error":{"code":-32603,"message":"divide by zero"}}`}, 401 | { 402 | in: `{"jsonrpc": "2.0", "method": "Arith.Divide", "params": { "a": 1, "b": 1 }, "id": "1" }`, 403 | out: `{"jsonrpc":"2.0","id":"1","error":{"code":401,"message":"we do not serve 1"}}`}, 404 | { 405 | in: `{"jsonrpc": "2.0", "method": "arith.multiply", "params": { "a": 3, "b": 2 }, "id": 0 }`, 406 | out: `{"jsonrpc":"2.0","id":0,"result":6}`}, 407 | { 408 | in: `{"jsonrpc": "2.0", "method": "multiply", "params": { "a": 4, "b": 2 }, "id": 0 }`, 409 | out: `{"jsonrpc":"2.0","id":0,"result":8}`}, 410 | { 411 | in: `{"jsonrpc": "2.0", "method": "arith.pow", "params": { "base": 3, "exp": 3 }, "id": 0 }`, 412 | out: `{"jsonrpc":"2.0","id":0,"result":27}`}, 413 | { 414 | in: `{"jsonrpc": "2.0", "method": "arith.sum", "params": { "a": 3, "b": 3 }, "id": 1 }`, 415 | out: `{"jsonrpc":"2.0","id":1,"error":{"code":6,"message":"` + ts.Listener.Addr().String() + `"}}`}, 416 | { 417 | in: `{"jsonrpc": "2.0", "method": "arith.pow", "params": { "base": 3 }, "id": 0 }`, 418 | out: `{"jsonrpc":"2.0","id":0,"result":9}`}, 419 | } 420 | 421 | for _, c := range tc { 422 | if err := ws.WriteMessage(websocket.TextMessage, []byte(c.in)); err != nil { 423 | log.Fatal(err) 424 | return 425 | } 426 | 427 | _, resp, err := ws.ReadMessage() 428 | if err != nil { 429 | log.Fatal(err) 430 | return 431 | } 432 | 433 | if string(resp) != c.out { 434 | t.Errorf("Input: %s\n got %s expected %s", c.in, resp, c.out) 435 | } 436 | } 437 | 438 | if err := ws.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")); err != nil { 439 | log.Fatal(err) 440 | return 441 | } 442 | } 443 | -------------------------------------------------------------------------------- /jsonrpc2.go: -------------------------------------------------------------------------------- 1 | package zenrpc 2 | 3 | import ( 4 | "encoding/json" 5 | ) 6 | 7 | const ( 8 | // ParseError is error code defined by JSON-RPC 2.0 spec. 9 | // Invalid JSON was received by the server. 10 | // An error occurred on the server while parsing the JSON text. 11 | ParseError = -32700 12 | 13 | // InvalidRequest is error code defined by JSON-RPC 2.0 spec. 14 | // The JSON sent is not as valid Request object. 15 | InvalidRequest = -32600 16 | 17 | // MethodNotFound is error code defined by JSON-RPC 2.0 spec. 18 | // The method does not exist / is not available. 19 | MethodNotFound = -32601 20 | 21 | // InvalidParams is error code defined by JSON-RPC 2.0 spec. 22 | // Invalid method parameter(s). 23 | InvalidParams = -32602 24 | 25 | // InternalError is error code defined by JSON-RPC 2.0 spec. 26 | // Internal JSON-RPC error. 27 | InternalError = -32603 28 | 29 | // ServerError is error code defined by JSON-RPC 2.0 spec. 30 | // Reserved for implementation-defined server-errors. 31 | ServerError = -32000 32 | 33 | // Version is only supported JSON-RPC Version. 34 | Version = "2.0" 35 | ) 36 | 37 | var errorMessages = map[int]string{ 38 | ParseError: "Parse error", 39 | InvalidRequest: "Invalid Request", 40 | MethodNotFound: "Method not found", 41 | InvalidParams: "Invalid params", 42 | InternalError: "Internal error", 43 | ServerError: "Server error", 44 | } 45 | 46 | // ErrorMsg returns error as text for default JSON-RPC errors. 47 | func ErrorMsg(code int) string { 48 | return errorMessages[code] 49 | } 50 | 51 | // Request is a json structure for json-rpc request to server. See: 52 | // http://www.jsonrpc.org/specification#request_object 53 | //easyjson:json 54 | type Request struct { 55 | // A String specifying the version of the JSON-RPC protocol. MUST be exactly "2.0". 56 | Version string `json:"jsonrpc"` 57 | 58 | // An identifier established by the Client that MUST contain as String, Number, or NULL value if included. 59 | // If it is not included it is assumed to be as notification. 60 | // The value SHOULD normally not be Null [1] and Numbers SHOULD NOT contain fractional parts. 61 | ID *json.RawMessage `json:"id"` 62 | 63 | // A String containing the name of the method to be invoked. 64 | // Method names that begin with the word rpc followed by as period character (U+002E or ASCII 46) 65 | // are reserved for rpc-internal methods and extensions and MUST NOT be used for anything else. 66 | Method string `json:"method"` 67 | 68 | // A Structured value that holds the parameter values to be used during the invocation of the method. 69 | // This member MAY be omitted. 70 | Params json.RawMessage `json:"params"` 71 | 72 | // Namespace holds namespace. Not in spec, for internal needs. 73 | Namespace string `json:"-"` 74 | } 75 | 76 | // Response is json structure for json-rpc response from server. See: 77 | // http://www.jsonrpc.org/specification#response_object 78 | //easyjson:json 79 | type Response struct { 80 | // A String specifying the version of the JSON-RPC protocol. MUST be exactly "2.0". 81 | Version string `json:"jsonrpc"` 82 | 83 | // This member is REQUIRED. 84 | // It MUST be the same as the value of the id member in the Request Object. 85 | // If there was an error in detecting the id in the Request object (e.g. Parse error/Invalid Request), it MUST be Null. 86 | ID *json.RawMessage `json:"id"` 87 | 88 | // This member is REQUIRED on success. 89 | // This member MUST NOT exist if there was an error invoking the method. 90 | // The value of this member is determined by the method invoked on the Server. 91 | Result *json.RawMessage `json:"result,omitempty"` 92 | 93 | // This member is REQUIRED on error. 94 | // This member MUST NOT exist if there was no error triggered during invocation. 95 | // The value for this member MUST be an Object as defined in section 5.1. 96 | Error *Error `json:"error,omitempty"` 97 | 98 | // Extensions is additional field for extending standard response. It could be useful for tracing, method execution, etc... 99 | Extensions map[string]interface{} `json:"extensions,omitempty"` 100 | } 101 | 102 | // JSON is temporary method that silences error during json marshalling. 103 | func (r Response) JSON() []byte { 104 | // TODO process error 105 | b, _ := json.Marshal(r) 106 | return b 107 | } 108 | 109 | // Error object used in response if function call errored. See: 110 | // http://www.jsonrpc.org/specification#error_object 111 | //easyjson:json 112 | type Error struct { 113 | // A Number that indicates the error type that occurred. 114 | // This MUST be an integer. 115 | Code int `json:"code"` 116 | 117 | // A String providing as short description of the error. 118 | // The message SHOULD be limited to as concise single sentence. 119 | Message string `json:"message"` 120 | 121 | // A Primitive or Structured value that contains additional information about the error. 122 | // This may be omitted. 123 | // The value of this member is defined by the Server (e.g. detailed error information, nested errors etc.). 124 | Data interface{} `json:"data,omitempty"` 125 | 126 | // Err is inner error. 127 | Err error `json:"-"` 128 | } 129 | 130 | // NewStringError makes a JSON-RPC with given code and message. 131 | func NewStringError(code int, message string) *Error { 132 | return &Error{Code: code, Message: message} 133 | } 134 | 135 | // NewError makes a JSON-RPC error with given code and standard error. 136 | func NewError(code int, err error) *Error { 137 | e := &Error{Code: code, Err: err} 138 | e.Message = e.Error() 139 | return e 140 | } 141 | 142 | // Error returns first filled value from Err, Message or default text for JSON-RPC error. 143 | func (e Error) Error() string { 144 | if e.Err != nil { 145 | return e.Err.Error() 146 | } 147 | 148 | if e.Message != "" { 149 | return e.Message 150 | } 151 | 152 | return ErrorMsg(e.Code) 153 | } 154 | 155 | // NewResponseError returns new Response with Error object. 156 | func NewResponseError(id *json.RawMessage, code int, message string, data interface{}) Response { 157 | if message == "" { 158 | message = ErrorMsg(code) 159 | } 160 | 161 | return Response{ 162 | Version: Version, 163 | ID: id, 164 | Error: &Error{ 165 | Code: code, 166 | Message: message, 167 | Data: data, 168 | }, 169 | } 170 | } 171 | 172 | // Set sets result and error if needed. 173 | func (r *Response) Set(v interface{}, er ...error) { 174 | r.Version = Version 175 | var err error 176 | 177 | if e, ok := v.(error); ok && e != nil { 178 | er = []error{e} 179 | v = nil 180 | } 181 | // check for nil *zenrpc.Error 182 | // TODO(sergeyfast): add ability to return other error types 183 | if len(er) > 0 && er[0] != nil { 184 | err = er[0] 185 | if e, ok := err.(*Error); ok && e == nil { 186 | err = nil 187 | } 188 | } 189 | 190 | // set first error if occurred 191 | if err != nil { 192 | if e, ok := err.(*Error); ok { 193 | r.Error = e 194 | } else { 195 | r.Error = NewError(InternalError, err) 196 | } 197 | 198 | return 199 | } 200 | 201 | // set result or error on marshal 202 | if res, err := json.Marshal(v); err != nil { 203 | r.Error = NewError(ServerError, err) 204 | } else { 205 | rm := json.RawMessage(res) 206 | r.Result = &rm 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /middleware.go: -------------------------------------------------------------------------------- 1 | package zenrpc 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "github.com/prometheus/client_golang/prometheus" 7 | "log" 8 | "strconv" 9 | "time" 10 | ) 11 | 12 | // Logger is middleware for JSON-RPC 2.0 Server. 13 | // It's just an example for middleware, will be refactored later. 14 | func Logger(l *log.Logger) MiddlewareFunc { 15 | return func(h InvokeFunc) InvokeFunc { 16 | return func(ctx context.Context, method string, params json.RawMessage) Response { 17 | start, ip := time.Now(), "" 18 | if req, ok := RequestFromContext(ctx); ok && req != nil { 19 | ip = req.RemoteAddr 20 | } 21 | 22 | r := h(ctx, method, params) 23 | l.Printf("ip=%s method=%s.%s duration=%v params=%s err=%s", ip, NamespaceFromContext(ctx), method, time.Since(start), params, r.Error) 24 | 25 | return r 26 | } 27 | } 28 | } 29 | 30 | // Metrics is a middleware for logging duration of RPC requests via Prometheus. Default AppName is zenrpc. 31 | // It exposes two metrics: appName_rpc_error_requests_count and appName_rpc_responses_duration_seconds. 32 | func Metrics(appName string) MiddlewareFunc { 33 | if appName == "" { 34 | appName = "zenrpc" 35 | } 36 | 37 | rpcErrors := prometheus.NewCounterVec(prometheus.CounterOpts{ 38 | Namespace: appName, 39 | Subsystem: "rpc", 40 | Name: "error_requests_count", 41 | Help: "Error requests count by method and error code.", 42 | }, []string{"method", "code"}) 43 | 44 | rpcDurations := prometheus.NewSummaryVec(prometheus.SummaryOpts{ 45 | Namespace: appName, 46 | Subsystem: "rpc", 47 | Name: "responses_duration_seconds", 48 | Help: "Response time by method and error code.", 49 | }, []string{"method", "code"}) 50 | 51 | prometheus.MustRegister(rpcErrors, rpcDurations) 52 | 53 | return func(h InvokeFunc) InvokeFunc { 54 | return func(ctx context.Context, method string, params json.RawMessage) Response { 55 | start, code := time.Now(), "" 56 | r := h(ctx, method, params) 57 | 58 | // log metrics 59 | if n := NamespaceFromContext(ctx); n != "" { 60 | method = n + "." + method 61 | } 62 | 63 | if r.Error != nil { 64 | code = strconv.Itoa(r.Error.Code) 65 | rpcErrors.WithLabelValues(method, code).Inc() 66 | } 67 | 68 | rpcDurations.WithLabelValues(method, code).Observe(time.Since(start).Seconds()) 69 | 70 | return r 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /parser/helpers.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "fmt" 5 | "github.com/thoas/go-funk" 6 | "go/ast" 7 | "golang.org/x/tools/go/packages" 8 | "path" 9 | "strings" 10 | ) 11 | 12 | type PackageFiles struct { 13 | PackagePath string 14 | PackageName string 15 | 16 | AstFiles []*ast.File 17 | } 18 | 19 | func filterFile(filepath string) bool { 20 | if !strings.HasSuffix(filepath, goFileSuffix) || 21 | strings.HasSuffix(filepath, GenerateFileSuffix) || strings.HasSuffix(filepath, testFileSuffix) { 22 | return false 23 | } 24 | return true 25 | } 26 | 27 | func getDependenciesFilenames(dir string) ([]string, error) { 28 | goFiles := []string{} 29 | pkgs, err := loadPackage(dir) 30 | if err != nil { 31 | return nil, err 32 | } 33 | for _, pack := range pkgs { 34 | goFiles = append(goFiles, goFilesFromPackage(pack)...) 35 | for _, childPack := range pack.Imports { 36 | goFiles = append(goFiles, goFilesFromPackage(childPack)...) 37 | } 38 | } 39 | return funk.UniqString(goFiles), nil 40 | } 41 | 42 | func GetDependenciesAstFiles(filename string) ([]PackageFiles, error) { 43 | pkgs, err := loadPackageWithSyntax(path.Dir(filename)) 44 | if err != nil { 45 | return nil, err 46 | } 47 | pfs := []PackageFiles{} 48 | done := map[string]bool{} 49 | for _, pkg := range pkgs { 50 | if _, ok := done[pkg.PkgPath]; ok { 51 | continue 52 | } 53 | 54 | pfs = append(pfs, PackageFiles{ 55 | PackagePath: pkg.PkgPath, 56 | PackageName: pkg.Name, 57 | AstFiles: pkg.Syntax, 58 | }) 59 | 60 | done[pkg.PkgPath] = true 61 | 62 | for _, childPack := range pkg.Imports { 63 | if _, ok := done[childPack.PkgPath]; ok { 64 | continue 65 | } 66 | 67 | pfs = append(pfs, PackageFiles{ 68 | PackagePath: childPack.PkgPath, 69 | PackageName: childPack.Name, 70 | AstFiles: childPack.Syntax, 71 | }) 72 | 73 | done[childPack.PkgPath] = true 74 | } 75 | } 76 | return pfs, nil 77 | } 78 | 79 | func goFilesFromPackage(pkg *packages.Package) []string { 80 | files := []string{} 81 | files = append(files, pkg.GoFiles...) 82 | return funk.FilterString(files, filterFile) 83 | } 84 | 85 | func EntryPointPackageName(filename string) (string, string, error) { 86 | pkgs, err := loadPackage(path.Dir(filename)) 87 | if err != nil { 88 | return "", "", err 89 | } 90 | for _, pack := range pkgs { 91 | return pack.Name, pack.PkgPath, nil 92 | } 93 | return "", "", fmt.Errorf("package not found for entry point") 94 | } 95 | 96 | func loadPackage(path string) ([]*packages.Package, error) { 97 | return packages.Load(&packages.Config{ 98 | Mode: packages.NeedImports | packages.NeedFiles | packages.NeedName, 99 | }, path) 100 | } 101 | 102 | func loadPackageWithSyntax(path string) ([]*packages.Package, error) { 103 | return packages.Load(&packages.Config{ 104 | Mode: packages.NeedImports | 105 | packages.NeedFiles | 106 | packages.NeedName | 107 | packages.NeedSyntax, 108 | }, path) 109 | } 110 | -------------------------------------------------------------------------------- /parser/helpers_test.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | . "github.com/smartystreets/goconvey/convey" 5 | "testing" 6 | ) 7 | 8 | func TestLoadPackage(t *testing.T) { 9 | Convey("Should load package with syntax and imports", t, func() { 10 | _, err := loadPackage("../testdata/subservice/subarithservice.go") 11 | So(err, ShouldBeNil) 12 | }) 13 | } 14 | -------------------------------------------------------------------------------- /parser/import.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "go/ast" 5 | "strings" 6 | ) 7 | 8 | func importNameOrAliasAndPath(i *ast.ImportSpec) (name, path string) { 9 | path = i.Path.Value[1 : len(i.Path.Value)-1] // remove quotes "" 10 | if i.Name != nil { 11 | name = i.Name.Name 12 | } else { 13 | name = path[strings.LastIndex(path, "/")+1:] 14 | } 15 | 16 | return 17 | } 18 | 19 | func uniqueImports(in []*ast.ImportSpec) (out []*ast.ImportSpec) { 20 | set := make(map[string]struct{}) 21 | for _, i := range in { 22 | key := i.Path.Value 23 | if i.Name != nil { 24 | key += "|" + i.Name.Name 25 | } 26 | 27 | if _, ok := set[key]; !ok { 28 | out = append(out, i) 29 | set[key] = struct{}{} 30 | } 31 | } 32 | 33 | return 34 | } 35 | 36 | // filterImports filter imports by namespace in structs 37 | func filterImports(in []*ast.ImportSpec, names map[string]struct{}) (out []*ast.ImportSpec) { 38 | for _, i := range in { 39 | name, _ := importNameOrAliasAndPath(i) 40 | if _, ok := names[name]; ok { 41 | out = append(out, i) 42 | } 43 | } 44 | 45 | return 46 | } 47 | -------------------------------------------------------------------------------- /parser/parser.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "fmt" 5 | "go/ast" 6 | "go/token" 7 | "path/filepath" 8 | "regexp" 9 | "strconv" 10 | "strings" 11 | "unicode" 12 | "unicode/utf8" 13 | ) 14 | 15 | const ( 16 | GenerateFileSuffix = "_zenrpc.go" 17 | 18 | zenrpcComment = "//zenrpc" 19 | zenrpcService = "zenrpc.Service" 20 | contextTypeName = "context.Context" 21 | errorTypeName = "zenrpc.Error" 22 | testFileSuffix = "_test.go" 23 | goFileSuffix = ".go" 24 | zenrpcMagicPrefix = "//zenrpc:" 25 | ) 26 | 27 | var errorCommentRegexp = regexp.MustCompile("^(-?\\d+)\\s*(.*)$") 28 | var returnCommentRegexp = regexp.MustCompile("return\\s*(.*)") 29 | var argumentCommentRegexp = regexp.MustCompile("([^=( ]+)\\s*(\\(\\s*([^ )]+)\\s*\\))?(\\s*=\\s*((`([^`]+)`)|([^ ]+)))?\\s*(.*)") 30 | 31 | // PackageInfo represents struct info for XXX_zenrpc.go file generation 32 | type PackageInfo struct { 33 | EntryPoint string 34 | Dir string 35 | PackageName string 36 | PackagePath string 37 | 38 | Services []*Service 39 | 40 | Scopes map[string][]*ast.Scope // key - import name, value - array of scopes from each package file 41 | Structs map[string]*Struct 42 | Imports []*ast.ImportSpec 43 | 44 | PackageNamesAndAliasesUsedInServices map[string]struct{} // set of structs names from arguments for printing imports 45 | ImportsIncludedToGeneratedCode []*ast.ImportSpec 46 | } 47 | 48 | type Service struct { 49 | GenDecl *ast.GenDecl 50 | Name string 51 | Methods []*Method 52 | Description string 53 | } 54 | 55 | type Method struct { 56 | FuncDecl *ast.FuncType 57 | Name string 58 | LowerCaseName string 59 | HasContext bool 60 | Args []Arg 61 | DefaultValues map[string]DefaultValue 62 | Returns []Return 63 | SMDReturn *SMDReturn // return for generate smd schema; pointer for nil check 64 | Description string 65 | 66 | Errors []SMDError // errors for documentation in SMD 67 | } 68 | 69 | type DefaultValue struct { 70 | Name string 71 | CapitalName string 72 | Type string // without star 73 | Comment string // original comment 74 | Value string 75 | } 76 | 77 | type Arg struct { 78 | Name string 79 | Type string 80 | CapitalName string 81 | JsonName string 82 | HasStar bool 83 | HasDefaultValue bool 84 | Description string // from magic comment 85 | SMDType SMDType 86 | } 87 | 88 | type Return struct { 89 | Name string 90 | Type string 91 | } 92 | 93 | type SMDReturn struct { 94 | Name string 95 | HasStar bool 96 | Description string 97 | SMDType SMDType 98 | } 99 | 100 | type Struct struct { 101 | Name string // key in map, Ref in arguments and returns 102 | Namespace string 103 | Type string 104 | StructType *ast.StructType 105 | Properties []Property // array because order is important 106 | } 107 | 108 | type Property struct { 109 | Name string 110 | Description string 111 | SMDType SMDType 112 | } 113 | 114 | // SMDType is a type representation for SMD generation 115 | type SMDType struct { 116 | Type string 117 | ItemsType string // for array 118 | Ref string // for object and also if array item is object 119 | } 120 | 121 | type SMDError struct { 122 | Code int 123 | Description string 124 | } 125 | 126 | func NewPackageInfo(filename string) (*PackageInfo, error) { 127 | dir, err := filepath.Abs(filepath.Dir(filename)) 128 | if err != nil { 129 | return nil, err 130 | } 131 | 132 | packageName, packagePath, err := EntryPointPackageName(filename) 133 | if err != nil { 134 | return nil, err 135 | } 136 | 137 | return &PackageInfo{ 138 | EntryPoint: filename, 139 | Dir: dir, 140 | PackageName: packageName, 141 | PackagePath: packagePath, 142 | Services: []*Service{}, 143 | 144 | Scopes: make(map[string][]*ast.Scope), 145 | Structs: make(map[string]*Struct), 146 | Imports: []*ast.ImportSpec{}, 147 | 148 | PackageNamesAndAliasesUsedInServices: make(map[string]struct{}), 149 | ImportsIncludedToGeneratedCode: []*ast.ImportSpec{}, 150 | }, nil 151 | } 152 | 153 | // ParseFiles parse all files associated with package from original file 154 | func (pi *PackageInfo) Parse(filename string) error { 155 | pfs, err := GetDependenciesAstFiles(filename) 156 | if err != nil { 157 | return err 158 | } 159 | 160 | for _, pkg := range pfs { 161 | for _, astFile := range pkg.AstFiles { 162 | if pkg.PackagePath == pi.PackagePath { 163 | // get structs for zenrpc only for root package 164 | pi.collectServices(astFile) 165 | } 166 | // collect scopes 167 | pi.collectScopes(astFile) 168 | // get imports 169 | pi.collectImports(astFile) 170 | } 171 | } 172 | 173 | // second loop: parse methods. It runs in separate loop because we need all services to be collected for this parsing 174 | for _, pkg := range pfs { 175 | for _, f := range pkg.AstFiles { 176 | if err := pi.parseMethods(f); err != nil { 177 | return err 178 | } 179 | } 180 | } 181 | 182 | // collect imports for generated code - only include imports that are explicitly imported in service code (all imports with definitions are more) 183 | pi.collectImportsForGeneratedCode() 184 | 185 | pi.parseStructs() 186 | 187 | return nil 188 | } 189 | 190 | func (pi *PackageInfo) collectScopes(astFile *ast.File) { 191 | if pi.PackageName != astFile.Name.Name { 192 | pi.Scopes[astFile.Name.Name] = append(pi.Scopes[astFile.Name.Name], astFile.Scope) // collect other package scopes 193 | } else { 194 | pi.Scopes["."] = append(pi.Scopes["."], astFile.Scope) // collect current package scopes 195 | } 196 | } 197 | 198 | func (pi *PackageInfo) collectImports(astFile *ast.File) { 199 | pi.Imports = append(pi.Imports, astFile.Imports...) // collect imports 200 | } 201 | 202 | func (pi *PackageInfo) collectImportsForGeneratedCode() { 203 | // collect scopes from imported packages 204 | pi.ImportsIncludedToGeneratedCode = filterImports(uniqueImports(pi.Imports), pi.PackageNamesAndAliasesUsedInServices) 205 | } 206 | 207 | func (pi *PackageInfo) collectServices(f *ast.File) { 208 | for _, decl := range f.Decls { 209 | gdecl, ok := decl.(*ast.GenDecl) 210 | if !ok || gdecl.Tok != token.TYPE { 211 | continue 212 | } 213 | 214 | for _, spec := range gdecl.Specs { 215 | spec, ok := spec.(*ast.TypeSpec) 216 | if !ok { 217 | continue 218 | } 219 | 220 | if !ast.IsExported(spec.Name.Name) { 221 | continue 222 | } 223 | 224 | structType, ok := spec.Type.(*ast.StructType) 225 | if !ok { 226 | continue 227 | } 228 | 229 | // check that struct is our zenrpc struct 230 | if hasZenrpcComment(spec) || hasZenrpcService(structType) { 231 | pi.Services = append(pi.Services, &Service{ 232 | GenDecl: gdecl, 233 | Name: spec.Name.Name, 234 | Methods: []*Method{}, 235 | Description: parseCommentGroup(spec.Doc), 236 | }) 237 | } 238 | } 239 | } 240 | } 241 | 242 | func (pi *PackageInfo) parseMethods(f *ast.File) error { 243 | for _, decl := range f.Decls { 244 | fdecl, ok := decl.(*ast.FuncDecl) 245 | if !ok || fdecl.Recv == nil { 246 | continue 247 | } 248 | 249 | m := Method{ 250 | FuncDecl: fdecl.Type, 251 | Name: fdecl.Name.Name, 252 | LowerCaseName: strings.ToLower(fdecl.Name.Name), 253 | Args: []Arg{}, 254 | DefaultValues: make(map[string]DefaultValue), 255 | Returns: []Return{}, 256 | Description: parseCommentGroup(fdecl.Doc), 257 | Errors: []SMDError{}, 258 | } 259 | 260 | serviceNames := m.linkWithServices(pi, fdecl) 261 | 262 | // services not found 263 | if len(serviceNames) == 0 { 264 | continue 265 | } 266 | 267 | if err := m.parseArguments(pi, fdecl, serviceNames); err != nil { 268 | return err 269 | } 270 | 271 | if err := m.parseReturns(pi, fdecl, serviceNames); err != nil { 272 | return err 273 | } 274 | 275 | // parse default values 276 | m.parseComments(fdecl.Doc, pi) 277 | } 278 | 279 | return nil 280 | } 281 | 282 | func (pi PackageInfo) String() string { 283 | result := fmt.Sprintf("Generated services for package %s:\n", pi.PackageName) 284 | for _, s := range pi.Services { 285 | result += fmt.Sprintf("- %s\n", s.Name) 286 | for _, m := range s.Methods { 287 | result += fmt.Sprintf(" • %s", m.Name) 288 | 289 | // args 290 | result += "(" 291 | for i, a := range m.Args { 292 | if i != 0 { 293 | result += ", " 294 | } 295 | 296 | result += fmt.Sprintf("%s %s", a.Name, a.Type) 297 | } 298 | result += ") " 299 | 300 | // no return args 301 | if len(m.Returns) == 0 { 302 | result += "\n" 303 | continue 304 | } 305 | 306 | // only one return arg without name 307 | if len(m.Returns) == 1 && len(m.Returns[0].Name) == 0 { 308 | result += m.Returns[0].Type + "\n" 309 | continue 310 | } 311 | 312 | // return 313 | result += "(" 314 | for i, a := range m.Returns { 315 | if i != 0 { 316 | result += fmt.Sprintf(", ") 317 | } 318 | 319 | if len(a.Name) == 0 { 320 | result += a.Type 321 | } else { 322 | result += fmt.Sprintf("%s %s", a.Name, a.Type) 323 | } 324 | } 325 | result += ")\n" 326 | } 327 | } 328 | 329 | return result 330 | } 331 | 332 | func (pi PackageInfo) OutputFilename() string { 333 | return filepath.Join(pi.Dir, pi.PackageName+GenerateFileSuffix) 334 | } 335 | 336 | // HasErrorVariable define adding err variable to generated Invoke function 337 | func (s Service) HasErrorVariable() bool { 338 | for _, m := range s.Methods { 339 | if len(m.Args) > 0 { 340 | return true 341 | } 342 | } 343 | return false 344 | } 345 | 346 | // linkWithServices add method for services 347 | func (m *Method) linkWithServices(pi *PackageInfo, fdecl *ast.FuncDecl) (names []string) { 348 | for _, field := range fdecl.Recv.List { 349 | // field can be pointer or not 350 | var ident *ast.Ident 351 | if starExpr, ok := field.Type.(*ast.StarExpr); ok { 352 | if ident, ok = starExpr.X.(*ast.Ident); !ok { 353 | continue 354 | } 355 | } else if ident, ok = field.Type.(*ast.Ident); !ok { 356 | continue 357 | } 358 | 359 | if !ast.IsExported(fdecl.Name.Name) { 360 | continue 361 | } 362 | 363 | // find service in our service list 364 | // method can be in several services 365 | for _, s := range pi.Services { 366 | if s.Name == ident.Name { 367 | names = append(names, s.Name) 368 | s.Methods = append(s.Methods, m) 369 | break 370 | } 371 | } 372 | } 373 | 374 | return 375 | } 376 | 377 | func (m *Method) parseArguments(pi *PackageInfo, fdecl *ast.FuncDecl, serviceNames []string) error { 378 | if fdecl.Type.Params == nil || fdecl.Type.Params.List == nil { 379 | return nil 380 | } 381 | 382 | for _, field := range fdecl.Type.Params.List { 383 | if field.Names == nil { 384 | continue 385 | } 386 | 387 | // parse type 388 | typeName := parseType(field.Type) 389 | if typeName == "" { 390 | // get argument names 391 | fields := []string{} 392 | for _, name := range field.Names { 393 | fields = append(fields, name.Name) 394 | } 395 | 396 | // get Service.Method list 397 | methods := []string{} 398 | for _, s := range serviceNames { 399 | methods = append(methods, s+"."+m.Name) 400 | } 401 | return fmt.Errorf("Can't parse type of argument %s in %s", strings.Join(fields, ", "), strings.Join(methods, ", ")) 402 | } 403 | 404 | if typeName == contextTypeName { 405 | m.HasContext = true 406 | continue // not add context to arg list 407 | } 408 | 409 | hasStar := hasStar(typeName) // check for pointer 410 | smdType, itemType := parseSMDType(field.Type) 411 | 412 | // find and save struct 413 | s := parseStruct(field.Type) 414 | var ref string 415 | if s != nil { 416 | ref = s.Name 417 | 418 | // collect namespaces (imports) 419 | if s.Namespace != "" { 420 | if _, ok := pi.PackageNamesAndAliasesUsedInServices[s.Namespace]; !ok { 421 | pi.PackageNamesAndAliasesUsedInServices[s.Namespace] = struct{}{} 422 | } 423 | } 424 | 425 | if currentS, ok := pi.Structs[s.Name]; !ok || (currentS.StructType == nil && s.StructType != nil) { 426 | pi.Structs[s.Name] = s 427 | } 428 | } 429 | 430 | // parse names 431 | for _, name := range field.Names { 432 | m.Args = append(m.Args, Arg{ 433 | Name: name.Name, 434 | Type: typeName, 435 | CapitalName: strings.Title(name.Name), 436 | JsonName: lowerFirst(name.Name), 437 | HasStar: hasStar, 438 | SMDType: SMDType{ 439 | Type: smdType, 440 | ItemsType: itemType, 441 | Ref: ref, 442 | }, 443 | }) 444 | } 445 | } 446 | 447 | return nil 448 | } 449 | 450 | func (m *Method) parseReturns(pi *PackageInfo, fdecl *ast.FuncDecl, serviceNames []string) error { 451 | if fdecl.Type.Results == nil || fdecl.Type.Results.List == nil { 452 | return nil 453 | } 454 | 455 | // get Service.Method list 456 | methods := func() string { 457 | methods := []string{} 458 | for _, s := range serviceNames { 459 | methods = append(methods, s+"."+m.Name) 460 | } 461 | return strings.Join(methods, ", ") 462 | } 463 | 464 | hasError := false 465 | for _, field := range fdecl.Type.Results.List { 466 | if len(field.Names) > 1 { 467 | return fmt.Errorf("%s contain more than one return arguments with same type", methods()) 468 | } 469 | 470 | // parse type 471 | typeName := parseType(field.Type) 472 | if typeName == "" { 473 | return fmt.Errorf("Can't parse type of return value in %s on position %d", methods(), len(m.Returns)+1) 474 | } 475 | 476 | var fieldName string 477 | // get names if exist 478 | if field.Names != nil { 479 | fieldName = field.Names[0].Name 480 | } 481 | 482 | m.Returns = append(m.Returns, Return{ 483 | Type: typeName, 484 | Name: fieldName, 485 | }) 486 | 487 | if typeName == "error" || typeName == errorTypeName || typeName == "*"+errorTypeName { 488 | if hasError { 489 | return fmt.Errorf("%s contain more than one error return arguments", methods()) 490 | } 491 | hasError = true 492 | continue 493 | } 494 | 495 | if m.SMDReturn != nil { 496 | return fmt.Errorf("%s contain more than one variable return argument", methods()) 497 | } 498 | 499 | hasStar := hasStar(typeName) // check for pointer 500 | smdType, itemType := parseSMDType(field.Type) 501 | 502 | // find and save struct 503 | s := parseStruct(field.Type) 504 | var ref string 505 | if s != nil { 506 | ref = s.Name 507 | 508 | if currentS, ok := pi.Structs[s.Name]; !ok || (currentS.StructType == nil && s.StructType != nil) { 509 | pi.Structs[s.Name] = s 510 | } 511 | } 512 | 513 | m.SMDReturn = &SMDReturn{ 514 | Name: fieldName, 515 | HasStar: hasStar, 516 | SMDType: SMDType{ 517 | Type: smdType, 518 | ItemsType: itemType, 519 | Ref: ref, 520 | }, 521 | } 522 | } 523 | 524 | return nil 525 | } 526 | 527 | // parseComments parse method comments and 528 | // fill default values, description for params and user errors map 529 | func (m *Method) parseComments(doc *ast.CommentGroup, pi *PackageInfo) { 530 | if doc == nil { 531 | return 532 | } 533 | 534 | for _, comment := range doc.List { 535 | if !strings.HasPrefix(comment.Text, zenrpcMagicPrefix) { 536 | continue 537 | } 538 | 539 | line := strings.TrimPrefix(strings.TrimSpace(comment.Text), zenrpcMagicPrefix) 540 | switch parseCommentType(line) { 541 | case "argument": 542 | name, alias, hasDefault, defaultValue, description := parseArgumentComment(line) 543 | for i, a := range m.Args { 544 | if a.Name == name { 545 | m.Args[i].Description = description 546 | 547 | if hasDefault { 548 | m.DefaultValues[name] = DefaultValue{ 549 | Name: name, 550 | CapitalName: a.CapitalName, 551 | Type: strings.TrimPrefix(a.Type, "*"), // remove star 552 | Comment: comment.Text, 553 | Value: defaultValue, 554 | } 555 | 556 | m.Args[i].HasDefaultValue = true 557 | } 558 | 559 | if alias != "" { 560 | m.Args[i].JsonName = alias 561 | } 562 | } 563 | } 564 | case "return": 565 | m.SMDReturn.Description = parseReturnComment(line) 566 | case "error": 567 | code, description := parseErrorComment(line) 568 | m.Errors = append(m.Errors, SMDError{code, description}) 569 | } 570 | } 571 | } 572 | 573 | func parseCommentType(line string) string { 574 | if strings.HasPrefix(line, "return") { 575 | return "return" 576 | } 577 | 578 | if errorCommentRegexp.MatchString(line) { 579 | return "error" 580 | } 581 | 582 | return "argument" 583 | } 584 | 585 | func parseReturnComment(line string) string { 586 | matches := returnCommentRegexp.FindStringSubmatch(line) 587 | if len(matches) < 2 { 588 | return "" 589 | } 590 | 591 | return matches[1] 592 | } 593 | 594 | func parseErrorComment(line string) (int, string) { 595 | matches := errorCommentRegexp.FindStringSubmatch(line) 596 | if len(matches) < 3 { 597 | // should not be here 598 | return 0, "" 599 | } 600 | 601 | code, err := strconv.Atoi(matches[1]) 602 | if err != nil { 603 | return 0, "" 604 | } 605 | 606 | return code, matches[2] 607 | } 608 | 609 | func parseArgumentComment(line string) (name, alias string, hasDefault bool, defaultValue, description string) { 610 | matches := argumentCommentRegexp.FindStringSubmatch(line) 611 | 612 | if len(matches) < 10 { 613 | return 614 | } 615 | 616 | // name index = 1 617 | name = matches[1] 618 | // alias index = 3 619 | alias = matches[3] 620 | // has default index = 4 621 | hasDefault = matches[4] != "" 622 | // default index = 5 623 | defaultValue = matches[5] 624 | // default quoted index = 7 can override non quoted string 625 | if matches[7] != "" { 626 | defaultValue = matches[7] 627 | } 628 | // description index = 9 629 | description = strings.TrimSpace(matches[9]) 630 | 631 | return 632 | } 633 | 634 | func parseCommentGroup(doc *ast.CommentGroup) string { 635 | if doc == nil { 636 | return "" 637 | } 638 | 639 | result := "" 640 | for _, comment := range doc.List { 641 | if strings.HasPrefix(comment.Text, zenrpcMagicPrefix) { 642 | continue 643 | } 644 | 645 | if len(result) > 0 { 646 | result += "\n" 647 | } 648 | result += strings.TrimSpace(strings.TrimPrefix(comment.Text, "//")) 649 | } 650 | 651 | return result 652 | } 653 | 654 | func parseType(expr ast.Expr) string { 655 | switch v := expr.(type) { 656 | case *ast.StarExpr: 657 | return "*" + parseType(v.X) 658 | case *ast.SelectorExpr: 659 | return parseType(v.X) + "." + v.Sel.Name 660 | case *ast.ArrayType: 661 | return "[" + parseType(v.Len) + "]" + parseType(v.Elt) 662 | case *ast.MapType: 663 | return "map[" + parseType(v.Key) + "]" + parseType(v.Value) 664 | case *ast.InterfaceType: 665 | return "interface{}" 666 | case *ast.Ident: 667 | return v.Name 668 | case *ast.BasicLit: 669 | // for array size 670 | return v.Value 671 | default: 672 | return "" 673 | } 674 | } 675 | 676 | // Returned value will be used as smd.{Value} variable from smd package 677 | func parseSMDType(expr ast.Expr) (string, string) { 678 | switch v := expr.(type) { 679 | case *ast.StarExpr: 680 | return parseSMDType(v.X) 681 | case *ast.SelectorExpr, *ast.MapType, *ast.InterfaceType: 682 | return "Object", "" 683 | case *ast.ArrayType: 684 | mainType, itemType := parseSMDType(v.Elt) 685 | if itemType != "" { 686 | return "Array", itemType 687 | } 688 | 689 | return "Array", mainType 690 | case *ast.Ident: 691 | switch v.Name { 692 | case "bool": 693 | return "Boolean", "" 694 | case "string": 695 | return "String", "" 696 | case "int", "int8", "int16", "int32", "int64", "uint", "uint8", "uint16", "uint32", "uint64", "uintptr", "byte", "rune": 697 | return "Integer", "" 698 | case "float32", "float64", "complex64", "complex128": 699 | return "Float", "" 700 | default: 701 | return "Object", "" // *ast.Ident contain type name, if type not basic then it struct or alias 702 | } 703 | default: 704 | return "Object", "" // default complex type is object 705 | } 706 | } 707 | 708 | // parseStruct find struct in type for display in SMD 709 | func parseStruct(expr ast.Expr) *Struct { 710 | switch v := expr.(type) { 711 | case *ast.StarExpr: 712 | return parseStruct(v.X) 713 | case *ast.SelectorExpr: 714 | namespace := v.X.(*ast.Ident).Name 715 | return &Struct{ 716 | Name: namespace + "." + v.Sel.Name, 717 | Namespace: namespace, 718 | Type: v.Sel.Name, 719 | } 720 | case *ast.ArrayType: 721 | // will get last type 722 | return parseStruct(v.Elt) 723 | case *ast.MapType: 724 | // will get last type 725 | return parseStruct(v.Value) 726 | case *ast.Ident: 727 | switch v.Name { 728 | case "bool", "string", 729 | "int", "int8", "int16", "int32", "int64", "uint", "uint8", "uint16", "uint32", "uint64", "uintptr", "byte", "rune", 730 | "float32", "float64", "complex64", "complex128": 731 | return nil 732 | } 733 | 734 | s := &Struct{ 735 | Name: v.Name, 736 | Namespace: ".", 737 | Type: v.Name, 738 | } 739 | 740 | if v.Obj != nil && v.Obj.Decl != nil { 741 | if ts, ok := v.Obj.Decl.(*ast.TypeSpec); ok { 742 | if st, ok := ts.Type.(*ast.StructType); ok { 743 | s.StructType = st 744 | } 745 | } 746 | } 747 | 748 | return s 749 | default: 750 | return nil 751 | } 752 | } 753 | 754 | func hasZenrpcComment(spec *ast.TypeSpec) bool { 755 | if spec.Comment != nil && len(spec.Comment.List) > 0 && spec.Comment.List[0].Text == zenrpcComment { 756 | return true 757 | } 758 | 759 | return false 760 | } 761 | 762 | func hasZenrpcService(structType *ast.StructType) bool { 763 | if structType.Fields.List == nil { 764 | return false 765 | } 766 | 767 | for _, field := range structType.Fields.List { 768 | selectorExpr, ok := field.Type.(*ast.SelectorExpr) 769 | if !ok { 770 | continue 771 | } 772 | 773 | x, ok := selectorExpr.X.(*ast.Ident) 774 | if ok && selectorExpr.Sel != nil && x.Name+"."+selectorExpr.Sel.Name == zenrpcService { 775 | return true 776 | } 777 | } 778 | 779 | return false 780 | } 781 | 782 | func lowerFirst(s string) string { 783 | if s == "" { 784 | return "" 785 | } 786 | r, n := utf8.DecodeRuneInString(s) 787 | return string(unicode.ToLower(r)) + s[n:] 788 | } 789 | 790 | func hasStar(s string) bool { 791 | if s[:1] == "*" { 792 | return true 793 | } 794 | 795 | return false 796 | } 797 | -------------------------------------------------------------------------------- /parser/parser_test.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import "testing" 4 | 5 | func Test_parseArgumentComment(t *testing.T) { 6 | tests := []struct { 7 | test string 8 | line string 9 | wantName string 10 | wantAlias string 11 | wantHasDefault bool 12 | wantDefaultValue string 13 | wantDescription string 14 | }{ 15 | { 16 | test: "should parse only name", 17 | line: "var", 18 | wantName: "var", 19 | }, 20 | { 21 | test: "should parse only name with spaces", 22 | line: " var ", 23 | wantName: "var", 24 | }, 25 | { 26 | test: "should parse name with alias", 27 | line: "var(alias)", 28 | wantName: "var", 29 | wantAlias: "alias", 30 | }, 31 | { 32 | test: "should parse name with alias with spaces", 33 | line: "var ( alias )", 34 | wantName: "var", 35 | wantAlias: "alias", 36 | }, 37 | { 38 | test: "should parse name with alias and default", 39 | line: "var(alias)=default", 40 | wantName: "var", 41 | wantAlias: "alias", 42 | wantHasDefault: true, 43 | wantDefaultValue: "default", 44 | }, 45 | { 46 | test: "should parse name with alias and default with spaces", 47 | line: "var(alias) = default", 48 | wantName: "var", 49 | wantAlias: "alias", 50 | wantHasDefault: true, 51 | wantDefaultValue: "default", 52 | }, 53 | { 54 | test: "should parse name with alias and quoted default", 55 | line: "var(alias)=`default`", 56 | wantName: "var", 57 | wantAlias: "alias", 58 | wantHasDefault: true, 59 | wantDefaultValue: "default", 60 | }, 61 | { 62 | test: "should parse name with alias and quoted default with spaces", 63 | line: "var(alias)= `defa ult ` ", 64 | wantName: "var", 65 | wantAlias: "alias", 66 | wantHasDefault: true, 67 | wantDefaultValue: "defa ult ", 68 | }, 69 | { 70 | test: "should parse name with alias, quoted default with spaces and description", 71 | line: "var(alias)= `defa ult ` description ", 72 | wantName: "var", 73 | wantAlias: "alias", 74 | wantHasDefault: true, 75 | wantDefaultValue: "defa ult ", 76 | wantDescription: "description", 77 | }, 78 | { 79 | test: "should parse name and description", 80 | line: "var description", 81 | wantName: "var", 82 | wantHasDefault: false, 83 | wantDescription: "description", 84 | }, 85 | { 86 | test: "should parse name and default", 87 | line: "var=default", 88 | wantName: "var", 89 | wantHasDefault: true, 90 | wantDefaultValue: "default", 91 | }, 92 | { 93 | test: "should parse name and default and description", 94 | line: "var=default", 95 | wantName: "var", 96 | wantHasDefault: true, 97 | wantDefaultValue: "default", 98 | }, 99 | { 100 | test: "should parse name and quoted default", 101 | line: "var=`default`", 102 | wantName: "var", 103 | wantHasDefault: true, 104 | wantDefaultValue: "default", 105 | }, 106 | { 107 | test: "should parse name and quoted default and description", 108 | line: "var=`default` description", 109 | wantName: "var", 110 | wantHasDefault: true, 111 | wantDefaultValue: "default", 112 | wantDescription: "description", 113 | }, 114 | { 115 | test: "should parse name and alias and description", 116 | line: "var(alias) description", 117 | wantName: "var", 118 | wantAlias: "alias", 119 | wantDescription: "description", 120 | }, 121 | } 122 | 123 | for _, tt := range tests { 124 | t.Run(tt.test, func(t *testing.T) { 125 | gotName, gotAlias, gotHasDefault, gotDefaultValue, gotDescription := parseArgumentComment(tt.line) 126 | if gotName != tt.wantName { 127 | t.Errorf("parseArgumentComment() gotName = %v, want %v", gotName, tt.wantName) 128 | } 129 | if gotAlias != tt.wantAlias { 130 | t.Errorf("parseArgumentComment() gotAlias = %v, want %v", gotAlias, tt.wantAlias) 131 | } 132 | if gotHasDefault != tt.wantHasDefault { 133 | t.Errorf("parseArgumentComment() gotHasDefault = %v, want %v", gotHasDefault, tt.wantHasDefault) 134 | } 135 | if gotDefaultValue != tt.wantDefaultValue { 136 | t.Errorf("parseArgumentComment() gotDefaultValue = %v, want %v", gotDefaultValue, tt.wantDefaultValue) 137 | } 138 | if gotDescription != tt.wantDescription { 139 | t.Errorf("parseArgumentComment() gotDefaultValue = %v, want %v", gotDefaultValue, tt.wantDefaultValue) 140 | } 141 | }) 142 | } 143 | } 144 | 145 | func Test_parseCommentType(t *testing.T) { 146 | tests := []struct { 147 | test string 148 | line string 149 | want string 150 | }{ 151 | { 152 | test: "should detect return", 153 | line: "return result", 154 | want: "return", 155 | }, 156 | { 157 | test: "should detect return without description", 158 | line: "return", 159 | want: "return", 160 | }, 161 | { 162 | test: "should detect error", 163 | line: "0 description", 164 | want: "error", 165 | }, 166 | { 167 | test: "should detect error with negative code", 168 | line: "-100 description", 169 | want: "error", 170 | }, 171 | { 172 | test: "should detect error without description", 173 | line: "-100", 174 | want: "error", 175 | }, 176 | { 177 | test: "should detect argument", 178 | line: "var(alias)", 179 | want: "argument", 180 | }, 181 | { 182 | test: "should detect argument with numbers", 183 | line: "var100=100 description", 184 | want: "argument", 185 | }, 186 | } 187 | for _, tt := range tests { 188 | t.Run(tt.test, func(t *testing.T) { 189 | if got := parseCommentType(tt.line); got != tt.want { 190 | t.Errorf("parseCommentType() = %v, want %v", got, tt.want) 191 | } 192 | }) 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /parser/struct.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "go/ast" 5 | "reflect" 6 | "strings" 7 | ) 8 | 9 | func (pi *PackageInfo) parseStructs() { 10 | for _, s := range pi.Structs { 11 | s.parse(pi) 12 | } 13 | } 14 | 15 | func (s *Struct) findTypeSpec(pi *PackageInfo) bool { 16 | if s.StructType != nil { 17 | return true 18 | } 19 | 20 | for _, f := range pi.Scopes[s.Namespace] { 21 | if obj, ok := f.Objects[s.Type]; ok && obj.Decl != nil { 22 | if ts, ok := obj.Decl.(*ast.TypeSpec); ok { 23 | if st, ok := ts.Type.(*ast.StructType); ok { 24 | s.StructType = st 25 | return true 26 | } 27 | } 28 | } 29 | } 30 | 31 | return false 32 | } 33 | 34 | func (s *Struct) parse(pi *PackageInfo) error { 35 | if !s.findTypeSpec(pi) || s.Properties != nil { 36 | // can't find struct implementation 37 | // or struct already parsed 38 | return nil 39 | } 40 | 41 | s.Properties = []Property{} 42 | for _, field := range s.StructType.Fields.List { 43 | tag := parseJsonTag(field.Tag) 44 | 45 | // do not parse tags that ignored in json 46 | if tag == "-" { 47 | continue 48 | } 49 | 50 | // parse embedded struct 51 | if field.Names == nil { 52 | if embeddedS := parseStruct(field.Type); embeddedS != nil { 53 | // set right namespace for struct from another package 54 | if embeddedS.Namespace == "." && s.Namespace != "." { 55 | embeddedS.Namespace = s.Namespace 56 | embeddedS.Name = s.Namespace + "." + embeddedS.Type 57 | } 58 | 59 | if currentS, ok := pi.Structs[embeddedS.Name]; !ok || (currentS.StructType == nil && embeddedS.StructType != nil) { 60 | pi.Structs[embeddedS.Name] = embeddedS 61 | } 62 | 63 | if err := embeddedS.parse(pi); err != nil { 64 | return err 65 | } 66 | 67 | if embeddedS.Properties != nil && len(embeddedS.Properties) > 0 { 68 | s.Properties = append(s.Properties, embeddedS.Properties...) 69 | } 70 | } 71 | 72 | continue 73 | } 74 | 75 | smdType, itemType := parseSMDType(field.Type) 76 | 77 | var ref string 78 | // parse field with struct type 79 | if internalS := parseStruct(field.Type); internalS != nil { 80 | // set right namespace for struct from another package 81 | if internalS.Namespace == "." && s.Namespace != "." { 82 | internalS.Namespace = s.Namespace 83 | internalS.Name = s.Namespace + "." + internalS.Type 84 | } 85 | 86 | ref = internalS.Name 87 | if currentS, ok := pi.Structs[internalS.Name]; !ok || (currentS.StructType == nil && internalS.StructType != nil) { 88 | pi.Structs[internalS.Name] = internalS 89 | } 90 | 91 | // avoid self-linked infinite recursion 92 | if internalS.Name != s.Name { 93 | if err := internalS.parse(pi); err != nil { 94 | return err 95 | } 96 | } 97 | } 98 | 99 | // parse inline struct 100 | if inlineStructType, ok := field.Type.(*ast.StructType); ok { 101 | // call struct by first property name 102 | inlineS := &Struct{ 103 | Name: s.Name + "_" + field.Names[0].Name, 104 | Namespace: s.Namespace, 105 | Type: s.Type + "_" + field.Names[0].Name, 106 | StructType: inlineStructType, 107 | } 108 | 109 | pi.Structs[inlineS.Name] = inlineS 110 | ref = inlineS.Name 111 | if err := inlineS.parse(pi); err != nil { 112 | return err 113 | } 114 | } 115 | 116 | // description 117 | description := parseCommentGroup(field.Doc) 118 | comment := parseCommentGroup(field.Comment) 119 | if description != "" && comment != "" { 120 | description += "\n" 121 | } 122 | description += comment 123 | 124 | // parse names 125 | for i, name := range field.Names { 126 | if !ast.IsExported(name.Name) { 127 | continue 128 | } 129 | 130 | p := Property{ 131 | Name: name.Name, 132 | Description: description, 133 | SMDType: SMDType{ 134 | Type: smdType, 135 | ItemsType: itemType, 136 | Ref: ref, 137 | }, 138 | } 139 | 140 | if i == 0 { 141 | // tag only for first name 142 | if tag == "-" { 143 | continue 144 | } else if tag != "" { 145 | p.Name = tag 146 | } 147 | } 148 | 149 | s.Properties = append(s.Properties, p) 150 | } 151 | } 152 | 153 | return nil 154 | } 155 | 156 | func parseJsonTag(bl *ast.BasicLit) string { 157 | if bl == nil { 158 | return "" 159 | } 160 | 161 | tags := bl.Value[1 : len(bl.Value)-1] // remove quotes `` 162 | tag := strings.Split(reflect.StructTag(tags).Get("json"), ",")[0] 163 | 164 | return tag 165 | } 166 | 167 | // Definitions returns list of structs used inside smdType 168 | func Definitions(smdType SMDType, structs map[string]*Struct) []*Struct { 169 | if smdType.Ref == "" { 170 | return nil 171 | } 172 | 173 | names := definitions(smdType, structs) 174 | // todo what about arrays? 175 | if smdType.Type == "Array" /*|| smdType.Type == "Object" */ { 176 | // add object to definitions if type array 177 | names = append([]string{smdType.Ref}, names...) 178 | } 179 | 180 | result := []*Struct{} 181 | unique := map[string]struct{}{} // structs in result must be unique 182 | for _, name := range names { 183 | if s, ok := structs[name]; ok { 184 | if _, ok := unique[name]; !ok { 185 | result = append(result, s) 186 | unique[name] = struct{}{} 187 | } 188 | } 189 | } 190 | 191 | return result 192 | } 193 | 194 | // definitions returns list of struct names used inside smdType 195 | func definitions(smdType SMDType, structs map[string]*Struct) []string { 196 | result := []string{} 197 | if s, ok := structs[smdType.Ref]; ok { 198 | for _, p := range s.Properties { 199 | if p.SMDType.Ref != "" { 200 | result = append(result, p.SMDType.Ref) 201 | 202 | // avoid self-linked infinite recursion 203 | if smdType.Ref != p.SMDType.Ref { 204 | result = append(result, definitions(p.SMDType, structs)...) 205 | } 206 | } 207 | } 208 | } 209 | 210 | return result 211 | } 212 | 213 | func uniqueStructsNamespaces(structs map[string]*Struct) (set map[string]struct{}) { 214 | set = make(map[string]struct{}) 215 | for _, s := range structs { 216 | if s.Namespace != "" { 217 | if _, ok := set[s.Namespace]; !ok { 218 | set[s.Namespace] = struct{}{} 219 | } 220 | } 221 | } 222 | 223 | return 224 | } 225 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package zenrpc 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "fmt" 8 | "net/http" 9 | "strings" 10 | "sync" 11 | "unicode" 12 | 13 | "github.com/gorilla/websocket" 14 | "github.com/semrush/zenrpc/v2/smd" 15 | ) 16 | 17 | type contextKey string 18 | 19 | const ( 20 | // defaultBatchMaxLen is default value of BatchMaxLen option in rpc Server options. 21 | defaultBatchMaxLen = 10 22 | 23 | // defaultTargetURL is default value for SMD target url. 24 | defaultTargetURL = "/" 25 | 26 | // context key for http.Request object. 27 | requestKey contextKey = "request" 28 | 29 | // context key for namespace. 30 | namespaceKey contextKey = "namespace" 31 | 32 | // context key for ID. 33 | IDKey contextKey = "id" 34 | 35 | // contentTypeJSON is default content type for HTTP transport. 36 | contentTypeJSON = "application/json" 37 | ) 38 | 39 | // MiddlewareFunc is a function for executing as middleware. 40 | type MiddlewareFunc func(InvokeFunc) InvokeFunc 41 | 42 | // InvokeFunc is a function for processing single JSON-RPC 2.0 Request after validation and parsing. 43 | type InvokeFunc func(context.Context, string, json.RawMessage) Response 44 | 45 | // Invoker implements service handler. 46 | type Invoker interface { 47 | Invoke(ctx context.Context, method string, params json.RawMessage) Response 48 | SMD() smd.ServiceInfo 49 | } 50 | 51 | // Service is as struct for discovering JSON-RPC 2.0 services for zenrpc generator cmd. 52 | type Service struct{} 53 | 54 | // Options is options for JSON-RPC 2.0 Server. 55 | type Options struct { 56 | // BatchMaxLen sets maximum quantity of requests in single batch. 57 | BatchMaxLen int 58 | 59 | // TargetURL is RPC endpoint. 60 | TargetURL string 61 | 62 | // ExposeSMD exposes SMD schema with ?smd GET parameter. 63 | ExposeSMD bool 64 | 65 | // DisableTransportChecks disables Content-Type and methods checks. Use only for development mode. 66 | DisableTransportChecks bool 67 | 68 | // AllowCORS adds header Access-Control-Allow-Origin with *. 69 | AllowCORS bool 70 | 71 | // Upgrader sets options for gorilla websocket. If nil, default options will be used 72 | Upgrader *websocket.Upgrader 73 | 74 | // HideErrorDataField removes data field from response error 75 | HideErrorDataField bool 76 | } 77 | 78 | // Server is JSON-RPC 2.0 Server. 79 | type Server struct { 80 | services map[string]Invoker 81 | options Options 82 | middleware []MiddlewareFunc 83 | logger Printer 84 | } 85 | 86 | // NewServer returns new JSON-RPC 2.0 Server. 87 | func NewServer(opts Options) Server { 88 | // For safety reasons we do not allowing to much requests in batch 89 | if opts.BatchMaxLen == 0 { 90 | opts.BatchMaxLen = defaultBatchMaxLen 91 | } 92 | 93 | if opts.TargetURL == "" { 94 | opts.TargetURL = defaultTargetURL 95 | } 96 | 97 | if opts.Upgrader == nil { 98 | opts.Upgrader = &websocket.Upgrader{ 99 | CheckOrigin: func(r *http.Request) bool { return opts.AllowCORS }, 100 | } 101 | } 102 | 103 | return Server{ 104 | services: make(map[string]Invoker), 105 | options: opts, 106 | } 107 | } 108 | 109 | // Use registers middleware. 110 | func (s *Server) Use(m ...MiddlewareFunc) { 111 | s.middleware = append(s.middleware, m...) 112 | } 113 | 114 | // Register registers new service for given namespace. For public namespace use empty string. 115 | func (s *Server) Register(namespace string, service Invoker) { 116 | s.services[strings.ToLower(namespace)] = service 117 | } 118 | 119 | // RegisterAll registers all services listed in map. 120 | func (s *Server) RegisterAll(services map[string]Invoker) { 121 | for ns, srv := range services { 122 | s.Register(ns, srv) 123 | } 124 | } 125 | 126 | // SetLogger sets logger for debug 127 | func (s *Server) SetLogger(printer Printer) { 128 | s.logger = printer 129 | } 130 | 131 | // process process JSON-RPC 2.0 message, invokes correct method for namespace and returns JSON-RPC 2.0 Response. 132 | func (s *Server) process(ctx context.Context, message json.RawMessage) interface{} { 133 | var requests []Request 134 | // parsing batch requests 135 | batch := IsArray(message) 136 | 137 | // making not batch request looks like batch to simplify further code 138 | if !batch { 139 | message = append(append([]byte{'['}, message...), ']') 140 | } 141 | 142 | // unmarshal request(s) 143 | if err := json.Unmarshal(message, &requests); err != nil { 144 | return NewResponseError(nil, ParseError, "", nil) 145 | } 146 | 147 | // if there no requests to process 148 | if len(requests) == 0 { 149 | return NewResponseError(nil, InvalidRequest, "", nil) 150 | } else if len(requests) > s.options.BatchMaxLen { 151 | return NewResponseError(nil, InvalidRequest, "", "max requests length in batch exceeded") 152 | } 153 | 154 | // process single request: if request single and not notification - just run it and return result 155 | if !batch && requests[0].ID != nil { 156 | return s.processRequest(ctx, requests[0]) 157 | } 158 | 159 | // process batch requests 160 | if res := s.processBatch(ctx, requests); len(res) > 0 { 161 | return res 162 | } 163 | 164 | return nil 165 | } 166 | 167 | // processBatch process batch requests with context. 168 | func (s Server) processBatch(ctx context.Context, requests []Request) []Response { 169 | reqLen := len(requests) 170 | 171 | // running requests in batch asynchronously 172 | respChan := make(chan Response, reqLen) 173 | 174 | var wg sync.WaitGroup 175 | wg.Add(reqLen) 176 | 177 | for _, req := range requests { 178 | // running request in goroutine 179 | go func(req Request) { 180 | if req.ID == nil { 181 | // ignoring response if request is notification 182 | wg.Done() 183 | s.processRequest(ctx, req) 184 | } else { 185 | respChan <- s.processRequest(ctx, req) 186 | wg.Done() 187 | } 188 | }(req) 189 | } 190 | 191 | // waiting to complete 192 | wg.Wait() 193 | close(respChan) 194 | 195 | // collecting responses 196 | responses := make([]Response, 0, reqLen) 197 | for r := range respChan { 198 | responses = append(responses, r) 199 | } 200 | 201 | // no responses -> all requests are notifications 202 | if len(responses) == 0 { 203 | return nil 204 | } 205 | return responses 206 | } 207 | 208 | // processRequest processes a single request in service invoker. 209 | func (s Server) processRequest(ctx context.Context, req Request) Response { 210 | // checks for json-rpc version and method 211 | if req.Version != Version || req.Method == "" { 212 | return NewResponseError(req.ID, InvalidRequest, "", nil) 213 | } 214 | 215 | // convert method to lower and find namespace 216 | lowerM := strings.ToLower(req.Method) 217 | sp := strings.SplitN(lowerM, ".", 2) 218 | namespace, method := "", lowerM 219 | if len(sp) == 2 { 220 | namespace, method = sp[0], sp[1] 221 | } 222 | 223 | if _, ok := s.services[namespace]; !ok { 224 | return NewResponseError(req.ID, MethodNotFound, "", nil) 225 | } 226 | 227 | // set namespace to context 228 | ctx = newNamespaceContext(ctx, namespace) 229 | 230 | // set id to context 231 | ctx = newIDContext(ctx, req.ID) 232 | 233 | // set middleware to func 234 | f := InvokeFunc(s.services[namespace].Invoke) 235 | for i := len(s.middleware) - 1; i >= 0; i-- { 236 | f = s.middleware[i](f) 237 | } 238 | 239 | // invoke func with middleware 240 | resp := f(ctx, method, req.Params) 241 | resp.ID = req.ID 242 | 243 | if s.options.HideErrorDataField && resp.Error != nil { 244 | resp.Error.Data = nil 245 | } 246 | 247 | return resp 248 | } 249 | 250 | // Do process JSON-RPC 2.0 request, invokes correct method for namespace and returns JSON-RPC 2.0 Response or marshaller error. 251 | func (s Server) Do(ctx context.Context, req []byte) ([]byte, error) { 252 | return json.Marshal(s.process(ctx, req)) 253 | } 254 | 255 | func (s Server) printf(format string, v ...interface{}) { 256 | if s.logger != nil { 257 | s.logger.Printf(format, v...) 258 | } 259 | } 260 | 261 | // SMD returns Service Mapping Description object with all registered methods. 262 | func (s Server) SMD() smd.Schema { 263 | sch := smd.Schema{ 264 | Transport: "POST", 265 | Envelope: "JSON-RPC-2.0", 266 | SMDVersion: "2.0", 267 | ContentType: contentTypeJSON, 268 | Target: s.options.TargetURL, 269 | Services: make(map[string]smd.Service), 270 | } 271 | 272 | for n, v := range s.services { 273 | info, namespace := v.SMD(), "" 274 | if n != "" { 275 | namespace = n + "." 276 | } 277 | 278 | for m, d := range info.Methods { 279 | method := namespace + m 280 | sch.Services[method] = d 281 | sch.Description += info.Description // TODO formatting 282 | } 283 | } 284 | 285 | return sch 286 | } 287 | 288 | // IsArray checks json message if it array or object. 289 | func IsArray(message json.RawMessage) bool { 290 | for _, b := range message { 291 | if unicode.IsSpace(rune(b)) { 292 | continue 293 | } 294 | 295 | if b == '[' { 296 | return true 297 | } 298 | break 299 | } 300 | 301 | return false 302 | } 303 | 304 | // ConvertToObject converts json array into object using key by index from keys array. 305 | func ConvertToObject(keys []string, params json.RawMessage) (json.RawMessage, error) { 306 | paramCount := len(keys) 307 | 308 | var rawParams []json.RawMessage 309 | if err := json.Unmarshal(params, &rawParams); err != nil { 310 | return nil, err 311 | } 312 | 313 | rawParamCount := len(rawParams) 314 | if paramCount < rawParamCount { 315 | return nil, fmt.Errorf("invalid params number, expected %d, got %d", paramCount, len(rawParams)) 316 | } 317 | 318 | buf := bytes.Buffer{} 319 | if _, err := buf.WriteString(`{`); err != nil { 320 | return nil, err 321 | } 322 | 323 | for i, p := range rawParams { 324 | // Writing key 325 | if _, err := buf.WriteString(`"` + keys[i] + `":`); err != nil { 326 | return nil, err 327 | } 328 | 329 | // Writing value 330 | if _, err := buf.Write(p); err != nil { 331 | return nil, err 332 | } 333 | 334 | // Writing trailing comma if not last argument 335 | if i != rawParamCount-1 { 336 | if _, err := buf.WriteString(`,`); err != nil { 337 | return nil, err 338 | } 339 | } 340 | 341 | } 342 | if _, err := buf.WriteString(`}`); err != nil { 343 | return nil, err 344 | } 345 | 346 | return buf.Bytes(), nil 347 | } 348 | 349 | // newRequestContext creates new context with http.Request. 350 | func newRequestContext(ctx context.Context, req *http.Request) context.Context { 351 | return context.WithValue(ctx, requestKey, req) 352 | } 353 | 354 | // RequestFromContext returns http.Request from context. 355 | func RequestFromContext(ctx context.Context) (*http.Request, bool) { 356 | r, ok := ctx.Value(requestKey).(*http.Request) 357 | return r, ok 358 | } 359 | 360 | // newNamespaceContext creates new context with current method namespace. 361 | func newNamespaceContext(ctx context.Context, namespace string) context.Context { 362 | return context.WithValue(ctx, namespaceKey, namespace) 363 | } 364 | 365 | // NamespaceFromContext returns method's namespace from context. 366 | func NamespaceFromContext(ctx context.Context) string { 367 | if r, ok := ctx.Value(namespaceKey).(string); ok { 368 | return r 369 | } 370 | 371 | return "" 372 | } 373 | 374 | // newIDContext creates new context with current request ID. 375 | func newIDContext(ctx context.Context, ID *json.RawMessage) context.Context { 376 | return context.WithValue(ctx, IDKey, ID) 377 | } 378 | 379 | // IDFromContext returns request ID from context. 380 | func IDFromContext(ctx context.Context) *json.RawMessage { 381 | if r, ok := ctx.Value(IDKey).(*json.RawMessage); ok { 382 | return r 383 | } 384 | 385 | return nil 386 | } 387 | -------------------------------------------------------------------------------- /server_test.go: -------------------------------------------------------------------------------- 1 | package zenrpc_test 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "testing" 7 | 8 | "github.com/semrush/zenrpc/v2" 9 | "github.com/semrush/zenrpc/v2/testdata" 10 | ) 11 | 12 | var rpc = zenrpc.NewServer(zenrpc.Options{BatchMaxLen: 5, AllowCORS: true}) 13 | 14 | func init() { 15 | rpc.Register("arith", &testdata.ArithService{}) 16 | rpc.Register("", &testdata.ArithService{}) 17 | //rpc.Use(zenrpc.Logger(log.New(os.Stderr, "", log.LstdFlags))) 18 | } 19 | 20 | func TestServer_SMD(t *testing.T) { 21 | r := rpc.SMD() 22 | if b, err := json.Marshal(r); err != nil { 23 | t.Fatal(err) 24 | } else if !bytes.Contains(b, []byte("default")) { 25 | t.Error(string(b)) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /smd/model.go: -------------------------------------------------------------------------------- 1 | package smd 2 | 3 | import "encoding/json" 4 | 5 | const ( 6 | String = "string" 7 | Integer = "integer" 8 | Array = "array" 9 | Boolean = "boolean" 10 | Float = "number" 11 | Object = "object" 12 | ) 13 | 14 | // Schema is struct for http://dojotoolkit.org/reference-guide/1.10/dojox/rpc/smd.html 15 | // This struct doesn't implement complete specification. 16 | type Schema struct { 17 | // Transport property defines the transport mechanism to be used to deliver service calls to servers. 18 | Transport string `json:"transport,omitempty"` 19 | 20 | // Envelope defines how a service message string is created from the provided parameters. 21 | Envelope string `json:"envelope,omitempty"` 22 | 23 | // ContentType is content type of the content returned by a service. Any valid MIME type is acceptable. 24 | // This property defaults to application/json. 25 | ContentType string `json:"contentType,omitempty"` 26 | 27 | // SMDVersion is a string that indicates the version level of the SMD being used. 28 | // This specification is at version level "2.0". This property SHOULD be included. 29 | SMDVersion string `json:"SMDVersion,omitempty"` 30 | // This should indicate what URL (or IP address in the case of TCP/IP transport) to use for the method call requests. 31 | // A URL may be an absolute URL or a relative URL 32 | Target string `json:"target,omitempty"` 33 | 34 | // Description of the service. This property SHOULD be included. 35 | Description string `json:"description,omitempty"` 36 | 37 | // Services should be an Object value where each property in the Object represents one of the available services. 38 | // The property name represents the name of the service, and the value is the service description. 39 | // This property MUST be included. 40 | Services map[string]Service `json:"services"` 41 | } 42 | 43 | // Service is a web endpoint that can perform an action and/or return 44 | // specific information in response to a defined network request. 45 | type Service struct { 46 | Description string `json:"description"` 47 | 48 | // Parameters for the service calls. A parameters value MUST be an Array. 49 | // Each value in the parameters Array should describe a parameter and follow the JSON Schema property definition. 50 | // Each of parameters that are defined at the root level are inherited by each of service definition's parameters. 51 | Parameters []JSONSchema `json:"parameters"` 52 | 53 | // Returns indicates the expected type of value returned from the method call. 54 | // This value of this property should follow JSON Schema type definition. 55 | Returns JSONSchema `json:"returns"` 56 | 57 | // Errors describes error codes from JSON-RPC 2.0 Specification 58 | Errors map[int]string `json:"errors,omitempty"` 59 | } 60 | 61 | type JSONSchema struct { 62 | // Name of the parameter. If names are not provided for all the parameters, 63 | // this indicates positional/ordered parameter calls MUST be used. 64 | // If names are provided in the parameters this indicates that named parameters SHOULD be issued by 65 | // the client making the service call, and the server MUST support named parameters, 66 | // but positional parameters MAY be issued by the client and servers SHOULD support positional parameters. 67 | Name string `json:"name,omitempty"` 68 | Type string `json:"type,omitempty"` 69 | Optional bool `json:"optional,omitempty"` 70 | Default *json.RawMessage `json:"default,omitempty"` 71 | Description string `json:"description,omitempty"` 72 | Properties map[string]Property `json:"properties,omitempty"` 73 | Definitions map[string]Definition `json:"definitions,omitempty"` 74 | Items map[string]string `json:"items,omitempty"` 75 | } 76 | 77 | type Property struct { 78 | Type string `json:"type,omitempty"` 79 | Description string `json:"description,omitempty"` 80 | Items map[string]string `json:"items,omitempty"` 81 | Definitions map[string]Definition `json:"definitions,omitempty"` 82 | Ref string `json:"$ref,omitempty"` 83 | } 84 | 85 | type Definition struct { 86 | Type string `json:"type,omitempty"` 87 | Properties map[string]Property `json:"properties,omitempty"` 88 | } 89 | 90 | type ServiceInfo struct { 91 | Description string 92 | Methods map[string]Service 93 | } 94 | 95 | // RawMessageString returns string as *json.RawMessage. 96 | func RawMessageString(m string) *json.RawMessage { 97 | r := json.RawMessage(m) 98 | return &r 99 | } 100 | -------------------------------------------------------------------------------- /testdata/arith.go: -------------------------------------------------------------------------------- 1 | package testdata 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "github.com/semrush/zenrpc/v2" 7 | "github.com/semrush/zenrpc/v2/testdata/model" 8 | "math" 9 | ) 10 | 11 | type ArithService struct{ zenrpc.Service } 12 | 13 | type Point struct { 14 | X, Y int // coordinate 15 | Z int `json:"-"` 16 | } 17 | 18 | // Sum sums two digits and returns error with error code as result and IP from context. 19 | func (as ArithService) Sum(ctx context.Context, a, b int) (bool, *zenrpc.Error) { 20 | r, _ := zenrpc.RequestFromContext(ctx) 21 | 22 | return true, zenrpc.NewStringError(a+b, r.Host) 23 | } 24 | 25 | func (as ArithService) Positive() (bool, *zenrpc.Error) { 26 | return true, nil 27 | } 28 | 29 | func (ArithService) DoSomething() { 30 | // some optimistic operations 31 | } 32 | 33 | func (ArithService) GetPoints() []model.Point { 34 | return []model.Point{} 35 | } 36 | 37 | func (ArithService) DoSomethingWithPoint(p model.Point) model.Point { 38 | // some optimistic operations 39 | return p 40 | } 41 | 42 | // Multiply multiples two digits and returns result. 43 | func (as ArithService) Multiply(a, b int) int { 44 | return a * b 45 | } 46 | 47 | // CheckError throws error is isErr true. 48 | //zenrpc:500 test error 49 | func (ArithService) CheckError(isErr bool) error { 50 | if isErr { 51 | return errors.New("test") 52 | } 53 | 54 | return nil 55 | } 56 | 57 | // CheckError throws zenrpc error is isErr true. 58 | //zenrpc:500 test error 59 | func (ArithService) CheckZenRPCError(isErr bool) *zenrpc.Error { 60 | if isErr { 61 | return zenrpc.NewStringError(500, "test") 62 | } 63 | 64 | return nil 65 | } 66 | 67 | // Quotient docs 68 | type Quotient struct { 69 | // Quo docs 70 | Quo int 71 | 72 | // Rem docs 73 | Rem int `json:"rem"` 74 | } 75 | 76 | // Divide divides two numbers. 77 | //zenrpc:a the a 78 | //zenrpc:b the b 79 | //zenrpc:quo result is Quotient, should be named var 80 | //zenrpc:401 we do not serve 1 81 | //zenrpc:-32603 divide by zero 82 | func (as *ArithService) Divide(a, b int) (quo *Quotient, err error) { 83 | if b == 0 { 84 | return nil, errors.New("divide by zero") 85 | } else if b == 1 { 86 | return nil, zenrpc.NewError(401, errors.New("we do not serve 1")) 87 | } 88 | 89 | return &Quotient{ 90 | Quo: a / b, 91 | Rem: a % b, 92 | }, nil 93 | } 94 | 95 | // Pow returns x**y, the base-x exponential of y. If Exp is not set then default value is 2. 96 | //zenrpc:exp=2 exponent could be empty 97 | func (as *ArithService) Pow(base float64, exp *float64) float64 { 98 | return math.Pow(base, *exp) 99 | } 100 | 101 | // PI returns math.Pi. 102 | func (ArithService) Pi() float64 { 103 | return math.Pi 104 | } 105 | 106 | // SumArray returns sum all items from array 107 | //zenrpc:array=[]float64{1,2,4} 108 | func (as *ArithService) SumArray(array *[]float64) float64 { 109 | var sum float64 110 | 111 | for _, i := range *array { 112 | sum += i 113 | } 114 | return sum 115 | } 116 | 117 | //go:generate zenrpc 118 | -------------------------------------------------------------------------------- /testdata/arithsrv/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "log" 6 | "net/http" 7 | "os" 8 | 9 | "github.com/prometheus/client_golang/prometheus/promhttp" 10 | "github.com/semrush/zenrpc/v2" 11 | "github.com/semrush/zenrpc/v2/testdata" 12 | ) 13 | 14 | func main() { 15 | addr := flag.String("addr", "localhost:9999", "listen address") 16 | flag.Parse() 17 | 18 | const phonebook = "phonebook" 19 | 20 | rpc := zenrpc.NewServer(zenrpc.Options{ 21 | ExposeSMD: true, 22 | AllowCORS: true, 23 | DisableTransportChecks: true, 24 | }) 25 | rpc.Register(phonebook, testdata.PhoneBook{DB: testdata.People}) 26 | rpc.Register("arith", testdata.ArithService{}) 27 | rpc.Register("printer", testdata.PrintService{}) 28 | rpc.Register("", testdata.ArithService{}) // public 29 | 30 | rpc.Use(zenrpc.Logger(log.New(os.Stderr, "", log.LstdFlags))) 31 | rpc.Use(zenrpc.Metrics(""), testdata.SerialPeopleAccess(phonebook)) 32 | 33 | rpc.SetLogger(log.New(os.Stderr, "A", log.LstdFlags)) 34 | 35 | http.Handle("/", rpc) 36 | http.HandleFunc("/ws", rpc.ServeWS) 37 | http.Handle("/metrics", promhttp.Handler()) 38 | http.HandleFunc("/doc", zenrpc.SMDBoxHandler) 39 | 40 | log.Printf("starting arithsrv on %s", *addr) 41 | log.Fatal(http.ListenAndServe(*addr, nil)) 42 | } 43 | -------------------------------------------------------------------------------- /testdata/catalogue.go: -------------------------------------------------------------------------------- 1 | package testdata 2 | 3 | import ( 4 | "github.com/semrush/zenrpc/v2" 5 | ) 6 | 7 | type Group struct { 8 | Id int `json:"id"` 9 | Title string `json:"title"` 10 | Nodes []Group `json:"nodes"` 11 | Groups []Group `json:"group"` 12 | ChildOpt *Group `json:"child"` 13 | Sub SubGroup `json:"sub"` 14 | } 15 | 16 | type SubGroup struct { 17 | Id int `json:"id"` 18 | Title string `json:"title"` 19 | //Nodes []Group `json:"nodes"` TODO still causes infinite recursion 20 | } 21 | 22 | type Campaign struct { 23 | Id int `json:"id"` 24 | Groups []Group `json:"group"` 25 | } 26 | 27 | type CatalogueService struct{ zenrpc.Service } 28 | 29 | func (s CatalogueService) First(groups []Group) (bool, error) { 30 | return true, nil 31 | } 32 | 33 | func (s CatalogueService) Second(campaigns []Campaign) (bool, error) { 34 | return true, nil 35 | } 36 | 37 | func (s CatalogueService) Third() (Campaign, error) { 38 | return Campaign{}, nil 39 | } 40 | 41 | //go:generate zenrpc 42 | -------------------------------------------------------------------------------- /testdata/model/model.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "github.com/semrush/zenrpc/v2/testdata/objects" 4 | 5 | type Point struct { 6 | objects.AbstractObject // embeded object 7 | X, Y int // coordinate 8 | Z int `json:"-"` 9 | ConnectedObject objects.AbstractObject 10 | } 11 | -------------------------------------------------------------------------------- /testdata/objects/objects.go: -------------------------------------------------------------------------------- 1 | package objects 2 | 3 | type AbstractObject struct { 4 | Name string 5 | SomeField string 6 | Measure float64 7 | } 8 | -------------------------------------------------------------------------------- /testdata/phonebook.go: -------------------------------------------------------------------------------- 1 | package testdata 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "github.com/semrush/zenrpc/v2" 8 | "sync" 9 | ) 10 | 11 | // SerialPeopleAccess is middleware for seiral access to PhoneBook methods 12 | func SerialPeopleAccess(pbNamespace string) zenrpc.MiddlewareFunc { 13 | var lock sync.RWMutex 14 | return func(h zenrpc.InvokeFunc) zenrpc.InvokeFunc { 15 | return func(ctx context.Context, method string, params json.RawMessage) zenrpc.Response { 16 | if zenrpc.NamespaceFromContext(ctx) == pbNamespace { 17 | switch method { 18 | case RPC.PhoneBook.Get, RPC.PhoneBook.ById: 19 | lock.RLock() 20 | defer lock.RUnlock() 21 | case RPC.PhoneBook.Delete, RPC.PhoneBook.Save, RPC.PhoneBook.Remove: 22 | lock.Lock() 23 | defer lock.Unlock() 24 | } 25 | } 26 | 27 | return h(ctx, method, params) 28 | } 29 | } 30 | } 31 | 32 | // People is sample db. 33 | var People = map[uint64]*Person{ 34 | 1: { 35 | ID: 1, 36 | FirstName: "John", 37 | LastName: "Doe", 38 | Phone: "+1-800-142-31-22", 39 | Mobile: []string{"m1", "m2", "m3"}, 40 | Deleted: false, 41 | Addresses: []Address{ 42 | {Street: "Main street", City: "Main City"}, 43 | {Street: "Default street", City: "Default City"}, 44 | }, 45 | }, 46 | 2: { 47 | ID: 2, 48 | FirstName: "Ivan", 49 | LastName: "Ivanov", 50 | Phone: "+7-900-131-53-94", 51 | Deleted: true, 52 | AltAddress: &Address{Street: "Main street", City: "Main City"}, 53 | }, 54 | } 55 | 56 | // Person in base model for phone book 57 | type Person struct { 58 | // ID is Unique Identifier for person 59 | ID uint64 60 | FirstName, LastName string 61 | // Phone is main phone 62 | Phone string 63 | WorkPhone *string 64 | Mobile []string 65 | 66 | // Deleted is flag for 67 | Deleted bool 68 | 69 | // Addresses Could be nil or len() == 0. 70 | Addresses []Address 71 | AltAddress *Address `json:"address"` 72 | } 73 | 74 | type PersonSearch struct { 75 | // ByName is filter for searching person by first name or last name. 76 | ByName *string 77 | ByType *string 78 | ByPhone string 79 | ByAddress *Address 80 | } 81 | 82 | type Address struct { 83 | Street string 84 | City string 85 | } 86 | 87 | type PhoneBook struct { 88 | DB map[uint64]*Person 89 | id uint64 90 | } //zenrpc 91 | 92 | // Get returns all people from DB. 93 | //zenrpc:page=0 current page 94 | //zenrpc:count=50 page size 95 | func (pb PhoneBook) Get(search PersonSearch, page, count *int) (res []*Person) { 96 | for _, p := range pb.DB { 97 | res = append(res, p) 98 | } 99 | 100 | return 101 | } 102 | 103 | // ValidateSearch returns given search as result. 104 | //zenrpc:search search object 105 | func (pb PhoneBook) ValidateSearch(search *PersonSearch) *PersonSearch { 106 | return search 107 | } 108 | 109 | // ById returns Person from DB. 110 | //zenrpc:id person id 111 | //zenrpc:404 person was not found 112 | func (pb PhoneBook) ById(id uint64) (*Person, *zenrpc.Error) { 113 | if p, ok := pb.DB[id]; ok { 114 | return p, nil 115 | } 116 | 117 | return nil, zenrpc.NewStringError(404, "person was not found") 118 | } 119 | 120 | // Delete marks person as deleted. 121 | //zenrpc:id person id 122 | //zenrpc:success operation result 123 | func (pb PhoneBook) Delete(id uint64) (success bool, error error) { 124 | if p, ok := pb.DB[id]; ok { 125 | p.Deleted = true 126 | return true, nil 127 | } 128 | 129 | return false, errors.New("person was not found") 130 | } 131 | 132 | // Removes deletes person from DB. 133 | //zenrpc:id person id 134 | //zenrpc:return operation result 135 | func (pb PhoneBook) Remove(id uint64) (success bool, error error) { 136 | if _, ok := pb.DB[id]; ok { 137 | delete(pb.DB, id) 138 | return true, nil 139 | } 140 | 141 | return false, errors.New("person was not found") 142 | } 143 | 144 | // Save saves person to DB. 145 | //zenrpc:replace=false update person if exist 146 | //zenrpc:400 invalid request 147 | //zenrpc:401 use replace=true 148 | func (pb *PhoneBook) Save(p Person, replace *bool) (id uint64, err *zenrpc.Error) { 149 | // validate 150 | if p.FirstName == "" || p.LastName == "" { 151 | return 0, zenrpc.NewStringError(400, "first name or last name is empty") 152 | } 153 | 154 | _, ok := pb.DB[p.ID] 155 | if ok && *replace == false { 156 | return 0, zenrpc.NewStringError(401, "") 157 | } 158 | 159 | pb.id++ 160 | p.ID = pb.id 161 | pb.DB[p.ID] = &p 162 | 163 | return pb.id, nil 164 | } 165 | 166 | // Prints message 167 | //zenrpc:str(type)=`"hello world"` 168 | func (pb *PhoneBook) Echo(str string) string { 169 | return str 170 | } 171 | -------------------------------------------------------------------------------- /testdata/printer.go: -------------------------------------------------------------------------------- 1 | package testdata 2 | 3 | import "github.com/semrush/zenrpc/v2" 4 | 5 | type PrintService struct{ zenrpc.Service } 6 | 7 | //zenrpc:s="test" 8 | func (PrintService) PrintRequiredDefault(s string) string { 9 | return s 10 | } 11 | 12 | //zenrpc:s="test" 13 | func (PrintService) PrintOptionalWithDefault(s *string) string { 14 | // if client passes nil to this method it will be replaced with default value 15 | return *s 16 | } 17 | 18 | func (PrintService) PrintRequired(s string) string { 19 | return s 20 | } 21 | 22 | func (PrintService) PrintOptional(s *string) string { 23 | if s == nil { 24 | return "string is empty" 25 | } 26 | 27 | return *s 28 | } 29 | -------------------------------------------------------------------------------- /testdata/subservice/subarithservice.go: -------------------------------------------------------------------------------- 1 | package subarithservice 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "github.com/semrush/zenrpc/v2" 7 | "github.com/semrush/zenrpc/v2/testdata/model" 8 | "math" 9 | ) 10 | 11 | type SubArithService struct{} //zenrpc 12 | 13 | // Sum sums two digits and returns error with error code as result and IP from context. 14 | func (as SubArithService) Sum(ctx context.Context, a, b int) (bool, *zenrpc.Error) { 15 | r, _ := zenrpc.RequestFromContext(ctx) 16 | 17 | return true, zenrpc.NewStringError(a+b, r.Host) 18 | } 19 | 20 | func (as SubArithService) Positive() (bool, *zenrpc.Error) { 21 | return true, nil 22 | } 23 | 24 | func (SubArithService) ReturnPointFromSamePackage(p Point) Point { 25 | // some optimistic operations 26 | return Point{} 27 | } 28 | 29 | func (SubArithService) GetPoints() []model.Point { 30 | return []model.Point{} 31 | } 32 | 33 | func (SubArithService) GetPointsFromSamePackage() []Point { 34 | return []Point{} 35 | } 36 | 37 | func (SubArithService) DoSomethingWithPoint(p model.Point) model.Point { 38 | // some optimistic operations 39 | return p 40 | } 41 | 42 | // Multiply multiples two digits and returns result. 43 | func (as SubArithService) Multiply(a, b int) int { 44 | return a * b 45 | } 46 | 47 | // CheckError throws error is isErr true. 48 | //zenrpc:500 test error 49 | func (SubArithService) CheckError(isErr bool) error { 50 | if isErr { 51 | return errors.New("test") 52 | } 53 | 54 | return nil 55 | } 56 | 57 | // CheckError throws zenrpc error is isErr true. 58 | //zenrpc:500 test error 59 | func (SubArithService) CheckZenRPCError(isErr bool) *zenrpc.Error { 60 | if isErr { 61 | return zenrpc.NewStringError(500, "test") 62 | } 63 | 64 | return nil 65 | } 66 | 67 | // Quotient docs 68 | type Quotient struct { 69 | // Quo docs 70 | Quo int 71 | 72 | // Rem docs 73 | Rem int `json:"rem"` 74 | } 75 | 76 | // Divide divides two numbers. 77 | //zenrpc:a the a 78 | //zenrpc:b the b 79 | //zenrpc:quo result is Quotient, should be named var 80 | //zenrpc:401 we do not serve 1 81 | //zenrpc:-32603 divide by zero 82 | func (as *SubArithService) Divide(a, b int) (quo *Quotient, err error) { 83 | if b == 0 { 84 | return nil, errors.New("divide by zero") 85 | } else if b == 1 { 86 | return nil, zenrpc.NewError(401, errors.New("we do not serve 1")) 87 | } 88 | 89 | return &Quotient{ 90 | Quo: a / b, 91 | Rem: a % b, 92 | }, nil 93 | } 94 | 95 | // Pow returns x**y, the base-x exponential of y. If Exp is not set then default value is 2. 96 | //zenrpc:exp=2 exponent could be empty 97 | func (as *SubArithService) Pow(base float64, exp *float64) float64 { 98 | return math.Pow(base, *exp) 99 | } 100 | 101 | // PI returns math.Pi. 102 | func (SubArithService) Pi() float64 { 103 | return math.Pi 104 | } 105 | 106 | // SumArray returns sum all items from array 107 | //zenrpc:array=[]float64{1,2,4} 108 | func (as *SubArithService) SumArray(array *[]float64) float64 { 109 | var sum float64 110 | 111 | for _, i := range *array { 112 | sum += i 113 | } 114 | return sum 115 | } 116 | 117 | //go:generate zenrpc 118 | -------------------------------------------------------------------------------- /testdata/subservice/subarithservice_zenrpc.go: -------------------------------------------------------------------------------- 1 | // Code generated by zenrpc; DO NOT EDIT. 2 | 3 | package subarithservice 4 | 5 | import ( 6 | "context" 7 | "encoding/json" 8 | 9 | "github.com/semrush/zenrpc/v2" 10 | "github.com/semrush/zenrpc/v2/smd" 11 | 12 | "github.com/semrush/zenrpc/v2/testdata/model" 13 | ) 14 | 15 | var RPC = struct { 16 | SubArithService struct{ Sum, Positive, ReturnPointFromSamePackage, GetPoints, GetPointsFromSamePackage, DoSomethingWithPoint, Multiply, CheckError, CheckZenRPCError, Divide, Pow, Pi, SumArray string } 17 | }{ 18 | SubArithService: struct{ Sum, Positive, ReturnPointFromSamePackage, GetPoints, GetPointsFromSamePackage, DoSomethingWithPoint, Multiply, CheckError, CheckZenRPCError, Divide, Pow, Pi, SumArray string }{ 19 | Sum: "sum", 20 | Positive: "positive", 21 | ReturnPointFromSamePackage: "returnpointfromsamepackage", 22 | GetPoints: "getpoints", 23 | GetPointsFromSamePackage: "getpointsfromsamepackage", 24 | DoSomethingWithPoint: "dosomethingwithpoint", 25 | Multiply: "multiply", 26 | CheckError: "checkerror", 27 | CheckZenRPCError: "checkzenrpcerror", 28 | Divide: "divide", 29 | Pow: "pow", 30 | Pi: "pi", 31 | SumArray: "sumarray", 32 | }, 33 | } 34 | 35 | func (SubArithService) SMD() smd.ServiceInfo { 36 | return smd.ServiceInfo{ 37 | Description: ``, 38 | Methods: map[string]smd.Service{ 39 | "Sum": { 40 | Description: `Sum sums two digits and returns error with error code as result and IP from context.`, 41 | Parameters: []smd.JSONSchema{ 42 | { 43 | Name: "a", 44 | Optional: false, 45 | Description: ``, 46 | Type: smd.Integer, 47 | }, 48 | { 49 | Name: "b", 50 | Optional: false, 51 | Description: ``, 52 | Type: smd.Integer, 53 | }, 54 | }, 55 | Returns: smd.JSONSchema{ 56 | Description: ``, 57 | Optional: false, 58 | Type: smd.Boolean, 59 | }, 60 | }, 61 | "Positive": { 62 | Description: ``, 63 | Parameters: []smd.JSONSchema{}, 64 | Returns: smd.JSONSchema{ 65 | Description: ``, 66 | Optional: false, 67 | Type: smd.Boolean, 68 | }, 69 | }, 70 | "ReturnPointFromSamePackage": { 71 | Description: ``, 72 | Parameters: []smd.JSONSchema{ 73 | { 74 | Name: "p", 75 | Optional: false, 76 | Description: ``, 77 | Type: smd.Object, 78 | Properties: map[string]smd.Property{ 79 | "Name": { 80 | Description: ``, 81 | Type: smd.String, 82 | }, 83 | "SomeField": { 84 | Description: ``, 85 | Type: smd.String, 86 | }, 87 | "Measure": { 88 | Description: ``, 89 | Type: smd.Float, 90 | }, 91 | "A": { 92 | Description: `coordinate`, 93 | Type: smd.Integer, 94 | }, 95 | "B": { 96 | Description: `coordinate`, 97 | Type: smd.Integer, 98 | }, 99 | "when": { 100 | Description: `when it happened`, 101 | Ref: "#/definitions/time.Time", 102 | Type: smd.Object, 103 | }, 104 | }, 105 | Definitions: map[string]smd.Definition{ 106 | "time.Time": { 107 | Type: "object", 108 | Properties: map[string]smd.Property{}, 109 | }, 110 | }, 111 | }, 112 | }, 113 | Returns: smd.JSONSchema{ 114 | Description: ``, 115 | Optional: false, 116 | Type: smd.Object, 117 | Properties: map[string]smd.Property{ 118 | "Name": { 119 | Description: ``, 120 | Type: smd.String, 121 | }, 122 | "SomeField": { 123 | Description: ``, 124 | Type: smd.String, 125 | }, 126 | "Measure": { 127 | Description: ``, 128 | Type: smd.Float, 129 | }, 130 | "A": { 131 | Description: `coordinate`, 132 | Type: smd.Integer, 133 | }, 134 | "B": { 135 | Description: `coordinate`, 136 | Type: smd.Integer, 137 | }, 138 | "when": { 139 | Description: `when it happened`, 140 | Ref: "#/definitions/time.Time", 141 | Type: smd.Object, 142 | }, 143 | }, 144 | Definitions: map[string]smd.Definition{ 145 | "time.Time": { 146 | Type: "object", 147 | Properties: map[string]smd.Property{}, 148 | }, 149 | }, 150 | }, 151 | }, 152 | "GetPoints": { 153 | Description: ``, 154 | Parameters: []smd.JSONSchema{}, 155 | Returns: smd.JSONSchema{ 156 | Description: ``, 157 | Optional: false, 158 | Type: smd.Array, 159 | Items: map[string]string{ 160 | "$ref": "#/definitions/model.Point", 161 | }, 162 | Definitions: map[string]smd.Definition{ 163 | "model.Point": { 164 | Type: "object", 165 | Properties: map[string]smd.Property{ 166 | "Name": { 167 | Description: ``, 168 | Type: smd.String, 169 | }, 170 | "SomeField": { 171 | Description: ``, 172 | Type: smd.String, 173 | }, 174 | "Measure": { 175 | Description: ``, 176 | Type: smd.Float, 177 | }, 178 | "X": { 179 | Description: `coordinate`, 180 | Type: smd.Integer, 181 | }, 182 | "Y": { 183 | Description: `coordinate`, 184 | Type: smd.Integer, 185 | }, 186 | "ConnectedObject": { 187 | Description: ``, 188 | Ref: "#/definitions/objects.AbstractObject", 189 | Type: smd.Object, 190 | }, 191 | }, 192 | }, 193 | "objects.AbstractObject": { 194 | Type: "object", 195 | Properties: map[string]smd.Property{ 196 | "Name": { 197 | Description: ``, 198 | Type: smd.String, 199 | }, 200 | "SomeField": { 201 | Description: ``, 202 | Type: smd.String, 203 | }, 204 | "Measure": { 205 | Description: ``, 206 | Type: smd.Float, 207 | }, 208 | }, 209 | }, 210 | }, 211 | }, 212 | }, 213 | "GetPointsFromSamePackage": { 214 | Description: ``, 215 | Parameters: []smd.JSONSchema{}, 216 | Returns: smd.JSONSchema{ 217 | Description: ``, 218 | Optional: false, 219 | Type: smd.Array, 220 | Items: map[string]string{ 221 | "$ref": "#/definitions/Point", 222 | }, 223 | Definitions: map[string]smd.Definition{ 224 | "Point": { 225 | Type: "object", 226 | Properties: map[string]smd.Property{ 227 | "Name": { 228 | Description: ``, 229 | Type: smd.String, 230 | }, 231 | "SomeField": { 232 | Description: ``, 233 | Type: smd.String, 234 | }, 235 | "Measure": { 236 | Description: ``, 237 | Type: smd.Float, 238 | }, 239 | "A": { 240 | Description: `coordinate`, 241 | Type: smd.Integer, 242 | }, 243 | "B": { 244 | Description: `coordinate`, 245 | Type: smd.Integer, 246 | }, 247 | "when": { 248 | Description: `when it happened`, 249 | Ref: "#/definitions/time.Time", 250 | Type: smd.Object, 251 | }, 252 | }, 253 | }, 254 | "time.Time": { 255 | Type: "object", 256 | Properties: map[string]smd.Property{}, 257 | }, 258 | }, 259 | }, 260 | }, 261 | "DoSomethingWithPoint": { 262 | Description: ``, 263 | Parameters: []smd.JSONSchema{ 264 | { 265 | Name: "p", 266 | Optional: false, 267 | Description: ``, 268 | Type: smd.Object, 269 | Properties: map[string]smd.Property{ 270 | "Name": { 271 | Description: ``, 272 | Type: smd.String, 273 | }, 274 | "SomeField": { 275 | Description: ``, 276 | Type: smd.String, 277 | }, 278 | "Measure": { 279 | Description: ``, 280 | Type: smd.Float, 281 | }, 282 | "X": { 283 | Description: `coordinate`, 284 | Type: smd.Integer, 285 | }, 286 | "Y": { 287 | Description: `coordinate`, 288 | Type: smd.Integer, 289 | }, 290 | "ConnectedObject": { 291 | Description: ``, 292 | Ref: "#/definitions/objects.AbstractObject", 293 | Type: smd.Object, 294 | }, 295 | }, 296 | Definitions: map[string]smd.Definition{ 297 | "objects.AbstractObject": { 298 | Type: "object", 299 | Properties: map[string]smd.Property{ 300 | "Name": { 301 | Description: ``, 302 | Type: smd.String, 303 | }, 304 | "SomeField": { 305 | Description: ``, 306 | Type: smd.String, 307 | }, 308 | "Measure": { 309 | Description: ``, 310 | Type: smd.Float, 311 | }, 312 | }, 313 | }, 314 | }, 315 | }, 316 | }, 317 | Returns: smd.JSONSchema{ 318 | Description: ``, 319 | Optional: false, 320 | Type: smd.Object, 321 | Properties: map[string]smd.Property{ 322 | "Name": { 323 | Description: ``, 324 | Type: smd.String, 325 | }, 326 | "SomeField": { 327 | Description: ``, 328 | Type: smd.String, 329 | }, 330 | "Measure": { 331 | Description: ``, 332 | Type: smd.Float, 333 | }, 334 | "X": { 335 | Description: `coordinate`, 336 | Type: smd.Integer, 337 | }, 338 | "Y": { 339 | Description: `coordinate`, 340 | Type: smd.Integer, 341 | }, 342 | "ConnectedObject": { 343 | Description: ``, 344 | Ref: "#/definitions/objects.AbstractObject", 345 | Type: smd.Object, 346 | }, 347 | }, 348 | Definitions: map[string]smd.Definition{ 349 | "objects.AbstractObject": { 350 | Type: "object", 351 | Properties: map[string]smd.Property{ 352 | "Name": { 353 | Description: ``, 354 | Type: smd.String, 355 | }, 356 | "SomeField": { 357 | Description: ``, 358 | Type: smd.String, 359 | }, 360 | "Measure": { 361 | Description: ``, 362 | Type: smd.Float, 363 | }, 364 | }, 365 | }, 366 | }, 367 | }, 368 | }, 369 | "Multiply": { 370 | Description: `Multiply multiples two digits and returns result.`, 371 | Parameters: []smd.JSONSchema{ 372 | { 373 | Name: "a", 374 | Optional: false, 375 | Description: ``, 376 | Type: smd.Integer, 377 | }, 378 | { 379 | Name: "b", 380 | Optional: false, 381 | Description: ``, 382 | Type: smd.Integer, 383 | }, 384 | }, 385 | Returns: smd.JSONSchema{ 386 | Description: ``, 387 | Optional: false, 388 | Type: smd.Integer, 389 | }, 390 | }, 391 | "CheckError": { 392 | Description: `CheckError throws error is isErr true.`, 393 | Parameters: []smd.JSONSchema{ 394 | { 395 | Name: "isErr", 396 | Optional: false, 397 | Description: ``, 398 | Type: smd.Boolean, 399 | }, 400 | }, 401 | Errors: map[int]string{ 402 | 500: "test error", 403 | }, 404 | }, 405 | "CheckZenRPCError": { 406 | Description: `CheckError throws zenrpc error is isErr true.`, 407 | Parameters: []smd.JSONSchema{ 408 | { 409 | Name: "isErr", 410 | Optional: false, 411 | Description: ``, 412 | Type: smd.Boolean, 413 | }, 414 | }, 415 | Errors: map[int]string{ 416 | 500: "test error", 417 | }, 418 | }, 419 | "Divide": { 420 | Description: `Divide divides two numbers.`, 421 | Parameters: []smd.JSONSchema{ 422 | { 423 | Name: "a", 424 | Optional: false, 425 | Description: `the a`, 426 | Type: smd.Integer, 427 | }, 428 | { 429 | Name: "b", 430 | Optional: false, 431 | Description: `the b`, 432 | Type: smd.Integer, 433 | }, 434 | }, 435 | Returns: smd.JSONSchema{ 436 | Description: ``, 437 | Optional: true, 438 | Type: smd.Object, 439 | Properties: map[string]smd.Property{ 440 | "Quo": { 441 | Description: `Quo docs`, 442 | Type: smd.Integer, 443 | }, 444 | "rem": { 445 | Description: `Rem docs`, 446 | Type: smd.Integer, 447 | }, 448 | }, 449 | }, 450 | Errors: map[int]string{ 451 | 401: "we do not serve 1", 452 | -32603: "divide by zero", 453 | }, 454 | }, 455 | "Pow": { 456 | Description: `Pow returns x**y, the base-x exponential of y. If Exp is not set then default value is 2.`, 457 | Parameters: []smd.JSONSchema{ 458 | { 459 | Name: "base", 460 | Optional: false, 461 | Description: ``, 462 | Type: smd.Float, 463 | }, 464 | { 465 | Name: "exp", 466 | Optional: true, 467 | Description: `exponent could be empty`, 468 | Type: smd.Float, 469 | }, 470 | }, 471 | Returns: smd.JSONSchema{ 472 | Description: ``, 473 | Optional: false, 474 | Type: smd.Float, 475 | }, 476 | }, 477 | "Pi": { 478 | Description: `PI returns math.Pi.`, 479 | Parameters: []smd.JSONSchema{}, 480 | Returns: smd.JSONSchema{ 481 | Description: ``, 482 | Optional: false, 483 | Type: smd.Float, 484 | }, 485 | }, 486 | "SumArray": { 487 | Description: `SumArray returns sum all items from array`, 488 | Parameters: []smd.JSONSchema{ 489 | { 490 | Name: "array", 491 | Optional: true, 492 | Description: ``, 493 | Type: smd.Array, 494 | Items: map[string]string{ 495 | "type": smd.Float, 496 | }, 497 | }, 498 | }, 499 | Returns: smd.JSONSchema{ 500 | Description: ``, 501 | Optional: false, 502 | Type: smd.Float, 503 | }, 504 | }, 505 | }, 506 | } 507 | } 508 | 509 | // Invoke is as generated code from zenrpc cmd 510 | func (s SubArithService) Invoke(ctx context.Context, method string, params json.RawMessage) zenrpc.Response { 511 | resp := zenrpc.Response{} 512 | var err error 513 | 514 | switch method { 515 | case RPC.SubArithService.Sum: 516 | var args = struct { 517 | A int `json:"a"` 518 | B int `json:"b"` 519 | }{} 520 | 521 | if zenrpc.IsArray(params) { 522 | if params, err = zenrpc.ConvertToObject([]string{"a", "b"}, params); err != nil { 523 | return zenrpc.NewResponseError(nil, zenrpc.InvalidParams, "", err.Error()) 524 | } 525 | } 526 | 527 | if len(params) > 0 { 528 | if err := json.Unmarshal(params, &args); err != nil { 529 | return zenrpc.NewResponseError(nil, zenrpc.InvalidParams, "", err.Error()) 530 | } 531 | } 532 | 533 | resp.Set(s.Sum(ctx, args.A, args.B)) 534 | 535 | case RPC.SubArithService.Positive: 536 | resp.Set(s.Positive()) 537 | 538 | case RPC.SubArithService.ReturnPointFromSamePackage: 539 | var args = struct { 540 | P Point `json:"p"` 541 | }{} 542 | 543 | if zenrpc.IsArray(params) { 544 | if params, err = zenrpc.ConvertToObject([]string{"p"}, params); err != nil { 545 | return zenrpc.NewResponseError(nil, zenrpc.InvalidParams, "", err.Error()) 546 | } 547 | } 548 | 549 | if len(params) > 0 { 550 | if err := json.Unmarshal(params, &args); err != nil { 551 | return zenrpc.NewResponseError(nil, zenrpc.InvalidParams, "", err.Error()) 552 | } 553 | } 554 | 555 | resp.Set(s.ReturnPointFromSamePackage(args.P)) 556 | 557 | case RPC.SubArithService.GetPoints: 558 | resp.Set(s.GetPoints()) 559 | 560 | case RPC.SubArithService.GetPointsFromSamePackage: 561 | resp.Set(s.GetPointsFromSamePackage()) 562 | 563 | case RPC.SubArithService.DoSomethingWithPoint: 564 | var args = struct { 565 | P model.Point `json:"p"` 566 | }{} 567 | 568 | if zenrpc.IsArray(params) { 569 | if params, err = zenrpc.ConvertToObject([]string{"p"}, params); err != nil { 570 | return zenrpc.NewResponseError(nil, zenrpc.InvalidParams, "", err.Error()) 571 | } 572 | } 573 | 574 | if len(params) > 0 { 575 | if err := json.Unmarshal(params, &args); err != nil { 576 | return zenrpc.NewResponseError(nil, zenrpc.InvalidParams, "", err.Error()) 577 | } 578 | } 579 | 580 | resp.Set(s.DoSomethingWithPoint(args.P)) 581 | 582 | case RPC.SubArithService.Multiply: 583 | var args = struct { 584 | A int `json:"a"` 585 | B int `json:"b"` 586 | }{} 587 | 588 | if zenrpc.IsArray(params) { 589 | if params, err = zenrpc.ConvertToObject([]string{"a", "b"}, params); err != nil { 590 | return zenrpc.NewResponseError(nil, zenrpc.InvalidParams, "", err.Error()) 591 | } 592 | } 593 | 594 | if len(params) > 0 { 595 | if err := json.Unmarshal(params, &args); err != nil { 596 | return zenrpc.NewResponseError(nil, zenrpc.InvalidParams, "", err.Error()) 597 | } 598 | } 599 | 600 | resp.Set(s.Multiply(args.A, args.B)) 601 | 602 | case RPC.SubArithService.CheckError: 603 | var args = struct { 604 | IsErr bool `json:"isErr"` 605 | }{} 606 | 607 | if zenrpc.IsArray(params) { 608 | if params, err = zenrpc.ConvertToObject([]string{"isErr"}, params); err != nil { 609 | return zenrpc.NewResponseError(nil, zenrpc.InvalidParams, "", err.Error()) 610 | } 611 | } 612 | 613 | if len(params) > 0 { 614 | if err := json.Unmarshal(params, &args); err != nil { 615 | return zenrpc.NewResponseError(nil, zenrpc.InvalidParams, "", err.Error()) 616 | } 617 | } 618 | 619 | resp.Set(s.CheckError(args.IsErr)) 620 | 621 | case RPC.SubArithService.CheckZenRPCError: 622 | var args = struct { 623 | IsErr bool `json:"isErr"` 624 | }{} 625 | 626 | if zenrpc.IsArray(params) { 627 | if params, err = zenrpc.ConvertToObject([]string{"isErr"}, params); err != nil { 628 | return zenrpc.NewResponseError(nil, zenrpc.InvalidParams, "", err.Error()) 629 | } 630 | } 631 | 632 | if len(params) > 0 { 633 | if err := json.Unmarshal(params, &args); err != nil { 634 | return zenrpc.NewResponseError(nil, zenrpc.InvalidParams, "", err.Error()) 635 | } 636 | } 637 | 638 | resp.Set(s.CheckZenRPCError(args.IsErr)) 639 | 640 | case RPC.SubArithService.Divide: 641 | var args = struct { 642 | A int `json:"a"` 643 | B int `json:"b"` 644 | }{} 645 | 646 | if zenrpc.IsArray(params) { 647 | if params, err = zenrpc.ConvertToObject([]string{"a", "b"}, params); err != nil { 648 | return zenrpc.NewResponseError(nil, zenrpc.InvalidParams, "", err.Error()) 649 | } 650 | } 651 | 652 | if len(params) > 0 { 653 | if err := json.Unmarshal(params, &args); err != nil { 654 | return zenrpc.NewResponseError(nil, zenrpc.InvalidParams, "", err.Error()) 655 | } 656 | } 657 | 658 | resp.Set(s.Divide(args.A, args.B)) 659 | 660 | case RPC.SubArithService.Pow: 661 | var args = struct { 662 | Base float64 `json:"base"` 663 | Exp *float64 `json:"exp"` 664 | }{} 665 | 666 | if zenrpc.IsArray(params) { 667 | if params, err = zenrpc.ConvertToObject([]string{"base", "exp"}, params); err != nil { 668 | return zenrpc.NewResponseError(nil, zenrpc.InvalidParams, "", err.Error()) 669 | } 670 | } 671 | 672 | if len(params) > 0 { 673 | if err := json.Unmarshal(params, &args); err != nil { 674 | return zenrpc.NewResponseError(nil, zenrpc.InvalidParams, "", err.Error()) 675 | } 676 | } 677 | 678 | //zenrpc:exp=2 exponent could be empty 679 | if args.Exp == nil { 680 | var v float64 = 2 681 | args.Exp = &v 682 | } 683 | 684 | resp.Set(s.Pow(args.Base, args.Exp)) 685 | 686 | case RPC.SubArithService.Pi: 687 | resp.Set(s.Pi()) 688 | 689 | case RPC.SubArithService.SumArray: 690 | var args = struct { 691 | Array *[]float64 `json:"array"` 692 | }{} 693 | 694 | if zenrpc.IsArray(params) { 695 | if params, err = zenrpc.ConvertToObject([]string{"array"}, params); err != nil { 696 | return zenrpc.NewResponseError(nil, zenrpc.InvalidParams, "", err.Error()) 697 | } 698 | } 699 | 700 | if len(params) > 0 { 701 | if err := json.Unmarshal(params, &args); err != nil { 702 | return zenrpc.NewResponseError(nil, zenrpc.InvalidParams, "", err.Error()) 703 | } 704 | } 705 | 706 | //zenrpc:array=[]float64{1,2,4} 707 | if args.Array == nil { 708 | var v []float64 = []float64{1, 2, 4} 709 | args.Array = &v 710 | } 711 | 712 | resp.Set(s.SumArray(args.Array)) 713 | 714 | default: 715 | resp = zenrpc.NewResponseError(nil, zenrpc.MethodNotFound, "", nil) 716 | } 717 | 718 | return resp 719 | } 720 | -------------------------------------------------------------------------------- /testdata/subservice/types.go: -------------------------------------------------------------------------------- 1 | package subarithservice 2 | 3 | import ( 4 | "github.com/semrush/zenrpc/v2/testdata/objects" 5 | "time" 6 | ) 7 | 8 | type Point struct { 9 | objects.AbstractObject 10 | A, B int // coordinate 11 | C int `json:"-"` 12 | When *time.Time `json:"when"` // when it happened 13 | } 14 | -------------------------------------------------------------------------------- /testdata/testdata_zenrpc.go: -------------------------------------------------------------------------------- 1 | // Code generated by zenrpc; DO NOT EDIT. 2 | 3 | package testdata 4 | 5 | import ( 6 | "context" 7 | "encoding/json" 8 | 9 | "github.com/semrush/zenrpc/v2" 10 | "github.com/semrush/zenrpc/v2/smd" 11 | 12 | "github.com/semrush/zenrpc/v2/testdata/model" 13 | ) 14 | 15 | var RPC = struct { 16 | ArithService struct{ Sum, Positive, DoSomething, GetPoints, DoSomethingWithPoint, Multiply, CheckError, CheckZenRPCError, Divide, Pow, Pi, SumArray string } 17 | CatalogueService struct{ First, Second, Third string } 18 | PhoneBook struct{ Get, ValidateSearch, ById, Delete, Remove, Save, Echo string } 19 | PrintService struct{ PrintRequiredDefault, PrintOptionalWithDefault, PrintRequired, PrintOptional string } 20 | }{ 21 | ArithService: struct{ Sum, Positive, DoSomething, GetPoints, DoSomethingWithPoint, Multiply, CheckError, CheckZenRPCError, Divide, Pow, Pi, SumArray string }{ 22 | Sum: "sum", 23 | Positive: "positive", 24 | DoSomething: "dosomething", 25 | GetPoints: "getpoints", 26 | DoSomethingWithPoint: "dosomethingwithpoint", 27 | Multiply: "multiply", 28 | CheckError: "checkerror", 29 | CheckZenRPCError: "checkzenrpcerror", 30 | Divide: "divide", 31 | Pow: "pow", 32 | Pi: "pi", 33 | SumArray: "sumarray", 34 | }, 35 | CatalogueService: struct{ First, Second, Third string }{ 36 | First: "first", 37 | Second: "second", 38 | Third: "third", 39 | }, 40 | PhoneBook: struct{ Get, ValidateSearch, ById, Delete, Remove, Save, Echo string }{ 41 | Get: "get", 42 | ValidateSearch: "validatesearch", 43 | ById: "byid", 44 | Delete: "delete", 45 | Remove: "remove", 46 | Save: "save", 47 | Echo: "echo", 48 | }, 49 | PrintService: struct{ PrintRequiredDefault, PrintOptionalWithDefault, PrintRequired, PrintOptional string }{ 50 | PrintRequiredDefault: "printrequireddefault", 51 | PrintOptionalWithDefault: "printoptionalwithdefault", 52 | PrintRequired: "printrequired", 53 | PrintOptional: "printoptional", 54 | }, 55 | } 56 | 57 | func (ArithService) SMD() smd.ServiceInfo { 58 | return smd.ServiceInfo{ 59 | Description: ``, 60 | Methods: map[string]smd.Service{ 61 | "Sum": { 62 | Description: `Sum sums two digits and returns error with error code as result and IP from context.`, 63 | Parameters: []smd.JSONSchema{ 64 | { 65 | Name: "a", 66 | Optional: false, 67 | Description: ``, 68 | Type: smd.Integer, 69 | }, 70 | { 71 | Name: "b", 72 | Optional: false, 73 | Description: ``, 74 | Type: smd.Integer, 75 | }, 76 | }, 77 | Returns: smd.JSONSchema{ 78 | Description: ``, 79 | Optional: false, 80 | Type: smd.Boolean, 81 | }, 82 | }, 83 | "Positive": { 84 | Description: ``, 85 | Parameters: []smd.JSONSchema{}, 86 | Returns: smd.JSONSchema{ 87 | Description: ``, 88 | Optional: false, 89 | Type: smd.Boolean, 90 | }, 91 | }, 92 | "DoSomething": { 93 | Description: ``, 94 | Parameters: []smd.JSONSchema{}, 95 | }, 96 | "GetPoints": { 97 | Description: ``, 98 | Parameters: []smd.JSONSchema{}, 99 | Returns: smd.JSONSchema{ 100 | Description: ``, 101 | Optional: false, 102 | Type: smd.Array, 103 | Items: map[string]string{ 104 | "$ref": "#/definitions/model.Point", 105 | }, 106 | Definitions: map[string]smd.Definition{ 107 | "model.Point": { 108 | Type: "object", 109 | Properties: map[string]smd.Property{ 110 | "X": { 111 | Description: `coordinate`, 112 | Type: smd.Integer, 113 | }, 114 | "Y": { 115 | Description: `coordinate`, 116 | Type: smd.Integer, 117 | }, 118 | "ConnectedObject": { 119 | Description: ``, 120 | Ref: "#/definitions/objects.AbstractObject", 121 | Type: smd.Object, 122 | }, 123 | }, 124 | }, 125 | "objects.AbstractObject": { 126 | Type: "object", 127 | Properties: map[string]smd.Property{}, 128 | }, 129 | }, 130 | }, 131 | }, 132 | "DoSomethingWithPoint": { 133 | Description: ``, 134 | Parameters: []smd.JSONSchema{ 135 | { 136 | Name: "p", 137 | Optional: false, 138 | Description: ``, 139 | Type: smd.Object, 140 | Properties: map[string]smd.Property{ 141 | "X": { 142 | Description: `coordinate`, 143 | Type: smd.Integer, 144 | }, 145 | "Y": { 146 | Description: `coordinate`, 147 | Type: smd.Integer, 148 | }, 149 | "ConnectedObject": { 150 | Description: ``, 151 | Ref: "#/definitions/objects.AbstractObject", 152 | Type: smd.Object, 153 | }, 154 | }, 155 | Definitions: map[string]smd.Definition{ 156 | "objects.AbstractObject": { 157 | Type: "object", 158 | Properties: map[string]smd.Property{}, 159 | }, 160 | }, 161 | }, 162 | }, 163 | Returns: smd.JSONSchema{ 164 | Description: ``, 165 | Optional: false, 166 | Type: smd.Object, 167 | Properties: map[string]smd.Property{ 168 | "X": { 169 | Description: `coordinate`, 170 | Type: smd.Integer, 171 | }, 172 | "Y": { 173 | Description: `coordinate`, 174 | Type: smd.Integer, 175 | }, 176 | "ConnectedObject": { 177 | Description: ``, 178 | Ref: "#/definitions/objects.AbstractObject", 179 | Type: smd.Object, 180 | }, 181 | }, 182 | Definitions: map[string]smd.Definition{ 183 | "objects.AbstractObject": { 184 | Type: "object", 185 | Properties: map[string]smd.Property{}, 186 | }, 187 | }, 188 | }, 189 | }, 190 | "Multiply": { 191 | Description: `Multiply multiples two digits and returns result.`, 192 | Parameters: []smd.JSONSchema{ 193 | { 194 | Name: "a", 195 | Optional: false, 196 | Description: ``, 197 | Type: smd.Integer, 198 | }, 199 | { 200 | Name: "b", 201 | Optional: false, 202 | Description: ``, 203 | Type: smd.Integer, 204 | }, 205 | }, 206 | Returns: smd.JSONSchema{ 207 | Description: ``, 208 | Optional: false, 209 | Type: smd.Integer, 210 | }, 211 | }, 212 | "CheckError": { 213 | Description: `CheckError throws error is isErr true.`, 214 | Parameters: []smd.JSONSchema{ 215 | { 216 | Name: "isErr", 217 | Optional: false, 218 | Description: ``, 219 | Type: smd.Boolean, 220 | }, 221 | }, 222 | Errors: map[int]string{ 223 | 500: "test error", 224 | }, 225 | }, 226 | "CheckZenRPCError": { 227 | Description: `CheckError throws zenrpc error is isErr true.`, 228 | Parameters: []smd.JSONSchema{ 229 | { 230 | Name: "isErr", 231 | Optional: false, 232 | Description: ``, 233 | Type: smd.Boolean, 234 | }, 235 | }, 236 | Errors: map[int]string{ 237 | 500: "test error", 238 | }, 239 | }, 240 | "Divide": { 241 | Description: `Divide divides two numbers.`, 242 | Parameters: []smd.JSONSchema{ 243 | { 244 | Name: "a", 245 | Optional: false, 246 | Description: ``, 247 | Type: smd.Integer, 248 | }, 249 | { 250 | Name: "b", 251 | Optional: false, 252 | Description: `the b`, 253 | Type: smd.Integer, 254 | }, 255 | }, 256 | Returns: smd.JSONSchema{ 257 | Description: ``, 258 | Optional: true, 259 | Type: smd.Object, 260 | Properties: map[string]smd.Property{ 261 | "Quo": { 262 | Description: `Quo docs`, 263 | Type: smd.Integer, 264 | }, 265 | "rem": { 266 | Description: `Rem docs`, 267 | Type: smd.Integer, 268 | }, 269 | }, 270 | }, 271 | Errors: map[int]string{ 272 | 401: "we do not serve 1", 273 | -32603: "divide by zero", 274 | }, 275 | }, 276 | "Pow": { 277 | Description: `Pow returns x**y, the base-x exponential of y. If Exp is not set then default value is 2.`, 278 | Parameters: []smd.JSONSchema{ 279 | { 280 | Name: "base", 281 | Optional: false, 282 | Description: ``, 283 | Type: smd.Float, 284 | }, 285 | { 286 | Name: "exp", 287 | Optional: true, 288 | Description: `exponent could be empty`, 289 | Type: smd.Float, 290 | }, 291 | }, 292 | Returns: smd.JSONSchema{ 293 | Description: ``, 294 | Optional: false, 295 | Type: smd.Float, 296 | }, 297 | }, 298 | "Pi": { 299 | Description: `PI returns math.Pi.`, 300 | Parameters: []smd.JSONSchema{}, 301 | Returns: smd.JSONSchema{ 302 | Description: ``, 303 | Optional: false, 304 | Type: smd.Float, 305 | }, 306 | }, 307 | "SumArray": { 308 | Description: `SumArray returns sum all items from array`, 309 | Parameters: []smd.JSONSchema{ 310 | { 311 | Name: "array", 312 | Optional: true, 313 | Description: ``, 314 | Type: smd.Array, 315 | Items: map[string]string{ 316 | "type": smd.Float, 317 | }, 318 | }, 319 | }, 320 | Returns: smd.JSONSchema{ 321 | Description: ``, 322 | Optional: false, 323 | Type: smd.Float, 324 | }, 325 | }, 326 | }, 327 | } 328 | } 329 | 330 | // Invoke is as generated code from zenrpc cmd 331 | func (s ArithService) Invoke(ctx context.Context, method string, params json.RawMessage) zenrpc.Response { 332 | resp := zenrpc.Response{} 333 | var err error 334 | 335 | switch method { 336 | case RPC.ArithService.Sum: 337 | var args = struct { 338 | A int `json:"a"` 339 | B int `json:"b"` 340 | }{} 341 | 342 | if zenrpc.IsArray(params) { 343 | if params, err = zenrpc.ConvertToObject([]string{"a", "b"}, params); err != nil { 344 | return zenrpc.NewResponseError(nil, zenrpc.InvalidParams, "", err.Error()) 345 | } 346 | } 347 | 348 | if len(params) > 0 { 349 | if err := json.Unmarshal(params, &args); err != nil { 350 | return zenrpc.NewResponseError(nil, zenrpc.InvalidParams, "", err.Error()) 351 | } 352 | } 353 | 354 | resp.Set(s.Sum(ctx, args.A, args.B)) 355 | 356 | case RPC.ArithService.Positive: 357 | resp.Set(s.Positive()) 358 | 359 | case RPC.ArithService.DoSomething: 360 | s.DoSomething() 361 | 362 | case RPC.ArithService.GetPoints: 363 | resp.Set(s.GetPoints()) 364 | 365 | case RPC.ArithService.DoSomethingWithPoint: 366 | var args = struct { 367 | P model.Point `json:"p"` 368 | }{} 369 | 370 | if zenrpc.IsArray(params) { 371 | if params, err = zenrpc.ConvertToObject([]string{"p"}, params); err != nil { 372 | return zenrpc.NewResponseError(nil, zenrpc.InvalidParams, "", err.Error()) 373 | } 374 | } 375 | 376 | if len(params) > 0 { 377 | if err := json.Unmarshal(params, &args); err != nil { 378 | return zenrpc.NewResponseError(nil, zenrpc.InvalidParams, "", err.Error()) 379 | } 380 | } 381 | 382 | resp.Set(s.DoSomethingWithPoint(args.P)) 383 | 384 | case RPC.ArithService.Multiply: 385 | var args = struct { 386 | A int `json:"a"` 387 | B int `json:"b"` 388 | }{} 389 | 390 | if zenrpc.IsArray(params) { 391 | if params, err = zenrpc.ConvertToObject([]string{"a", "b"}, params); err != nil { 392 | return zenrpc.NewResponseError(nil, zenrpc.InvalidParams, "", err.Error()) 393 | } 394 | } 395 | 396 | if len(params) > 0 { 397 | if err := json.Unmarshal(params, &args); err != nil { 398 | return zenrpc.NewResponseError(nil, zenrpc.InvalidParams, "", err.Error()) 399 | } 400 | } 401 | 402 | resp.Set(s.Multiply(args.A, args.B)) 403 | 404 | case RPC.ArithService.CheckError: 405 | var args = struct { 406 | IsErr bool `json:"isErr"` 407 | }{} 408 | 409 | if zenrpc.IsArray(params) { 410 | if params, err = zenrpc.ConvertToObject([]string{"isErr"}, params); err != nil { 411 | return zenrpc.NewResponseError(nil, zenrpc.InvalidParams, "", err.Error()) 412 | } 413 | } 414 | 415 | if len(params) > 0 { 416 | if err := json.Unmarshal(params, &args); err != nil { 417 | return zenrpc.NewResponseError(nil, zenrpc.InvalidParams, "", err.Error()) 418 | } 419 | } 420 | 421 | resp.Set(s.CheckError(args.IsErr)) 422 | 423 | case RPC.ArithService.CheckZenRPCError: 424 | var args = struct { 425 | IsErr bool `json:"isErr"` 426 | }{} 427 | 428 | if zenrpc.IsArray(params) { 429 | if params, err = zenrpc.ConvertToObject([]string{"isErr"}, params); err != nil { 430 | return zenrpc.NewResponseError(nil, zenrpc.InvalidParams, "", err.Error()) 431 | } 432 | } 433 | 434 | if len(params) > 0 { 435 | if err := json.Unmarshal(params, &args); err != nil { 436 | return zenrpc.NewResponseError(nil, zenrpc.InvalidParams, "", err.Error()) 437 | } 438 | } 439 | 440 | resp.Set(s.CheckZenRPCError(args.IsErr)) 441 | 442 | case RPC.ArithService.Divide: 443 | var args = struct { 444 | A int `json:"a"` 445 | B int `json:"b"` 446 | }{} 447 | 448 | if zenrpc.IsArray(params) { 449 | if params, err = zenrpc.ConvertToObject([]string{"a", "b"}, params); err != nil { 450 | return zenrpc.NewResponseError(nil, zenrpc.InvalidParams, "", err.Error()) 451 | } 452 | } 453 | 454 | if len(params) > 0 { 455 | if err := json.Unmarshal(params, &args); err != nil { 456 | return zenrpc.NewResponseError(nil, zenrpc.InvalidParams, "", err.Error()) 457 | } 458 | } 459 | 460 | resp.Set(s.Divide(args.A, args.B)) 461 | 462 | case RPC.ArithService.Pow: 463 | var args = struct { 464 | Base float64 `json:"base"` 465 | Exp *float64 `json:"exp"` 466 | }{} 467 | 468 | if zenrpc.IsArray(params) { 469 | if params, err = zenrpc.ConvertToObject([]string{"base", "exp"}, params); err != nil { 470 | return zenrpc.NewResponseError(nil, zenrpc.InvalidParams, "", err.Error()) 471 | } 472 | } 473 | 474 | if len(params) > 0 { 475 | if err := json.Unmarshal(params, &args); err != nil { 476 | return zenrpc.NewResponseError(nil, zenrpc.InvalidParams, "", err.Error()) 477 | } 478 | } 479 | 480 | //zenrpc:exp=2 exponent could be empty 481 | if args.Exp == nil { 482 | var v float64 = 2 483 | args.Exp = &v 484 | } 485 | 486 | resp.Set(s.Pow(args.Base, args.Exp)) 487 | 488 | case RPC.ArithService.Pi: 489 | resp.Set(s.Pi()) 490 | 491 | case RPC.ArithService.SumArray: 492 | var args = struct { 493 | Array *[]float64 `json:"array"` 494 | }{} 495 | 496 | if zenrpc.IsArray(params) { 497 | if params, err = zenrpc.ConvertToObject([]string{"array"}, params); err != nil { 498 | return zenrpc.NewResponseError(nil, zenrpc.InvalidParams, "", err.Error()) 499 | } 500 | } 501 | 502 | if len(params) > 0 { 503 | if err := json.Unmarshal(params, &args); err != nil { 504 | return zenrpc.NewResponseError(nil, zenrpc.InvalidParams, "", err.Error()) 505 | } 506 | } 507 | 508 | //zenrpc:array=[]float64{1,2,4} 509 | if args.Array == nil { 510 | var v []float64 = []float64{1, 2, 4} 511 | args.Array = &v 512 | } 513 | 514 | resp.Set(s.SumArray(args.Array)) 515 | 516 | default: 517 | resp = zenrpc.NewResponseError(nil, zenrpc.MethodNotFound, "", nil) 518 | } 519 | 520 | return resp 521 | } 522 | 523 | func (CatalogueService) SMD() smd.ServiceInfo { 524 | return smd.ServiceInfo{ 525 | Description: ``, 526 | Methods: map[string]smd.Service{ 527 | "First": { 528 | Description: ``, 529 | Parameters: []smd.JSONSchema{ 530 | { 531 | Name: "groups", 532 | Optional: false, 533 | Description: ``, 534 | Type: smd.Array, 535 | Items: map[string]string{ 536 | "$ref": "#/definitions/Group", 537 | }, 538 | Definitions: map[string]smd.Definition{ 539 | "Group": { 540 | Type: "object", 541 | Properties: map[string]smd.Property{ 542 | "id": { 543 | Description: ``, 544 | Type: smd.Integer, 545 | }, 546 | "title": { 547 | Description: ``, 548 | Type: smd.String, 549 | }, 550 | "nodes": { 551 | Description: ``, 552 | Type: smd.Array, 553 | Items: map[string]string{ 554 | "$ref": "#/definitions/Group", 555 | }, 556 | }, 557 | "group": { 558 | Description: ``, 559 | Type: smd.Array, 560 | Items: map[string]string{ 561 | "$ref": "#/definitions/Group", 562 | }, 563 | }, 564 | "child": { 565 | Description: ``, 566 | Ref: "#/definitions/Group", 567 | Type: smd.Object, 568 | }, 569 | "sub": { 570 | Description: ``, 571 | Ref: "#/definitions/SubGroup", 572 | Type: smd.Object, 573 | }, 574 | }, 575 | }, 576 | "SubGroup": { 577 | Type: "object", 578 | Properties: map[string]smd.Property{ 579 | "id": { 580 | Description: ``, 581 | Type: smd.Integer, 582 | }, 583 | "title": { 584 | Description: ``, 585 | Type: smd.String, 586 | }, 587 | }, 588 | }, 589 | }, 590 | }, 591 | }, 592 | Returns: smd.JSONSchema{ 593 | Description: ``, 594 | Optional: false, 595 | Type: smd.Boolean, 596 | }, 597 | }, 598 | "Second": { 599 | Description: ``, 600 | Parameters: []smd.JSONSchema{ 601 | { 602 | Name: "campaigns", 603 | Optional: false, 604 | Description: ``, 605 | Type: smd.Array, 606 | Items: map[string]string{ 607 | "$ref": "#/definitions/Campaign", 608 | }, 609 | Definitions: map[string]smd.Definition{ 610 | "Campaign": { 611 | Type: "object", 612 | Properties: map[string]smd.Property{ 613 | "id": { 614 | Description: ``, 615 | Type: smd.Integer, 616 | }, 617 | "group": { 618 | Description: ``, 619 | Type: smd.Array, 620 | Items: map[string]string{ 621 | "$ref": "#/definitions/Group", 622 | }, 623 | }, 624 | }, 625 | }, 626 | "Group": { 627 | Type: "object", 628 | Properties: map[string]smd.Property{ 629 | "id": { 630 | Description: ``, 631 | Type: smd.Integer, 632 | }, 633 | "title": { 634 | Description: ``, 635 | Type: smd.String, 636 | }, 637 | "nodes": { 638 | Description: ``, 639 | Type: smd.Array, 640 | Items: map[string]string{ 641 | "$ref": "#/definitions/Group", 642 | }, 643 | }, 644 | "group": { 645 | Description: ``, 646 | Type: smd.Array, 647 | Items: map[string]string{ 648 | "$ref": "#/definitions/Group", 649 | }, 650 | }, 651 | "child": { 652 | Description: ``, 653 | Ref: "#/definitions/Group", 654 | Type: smd.Object, 655 | }, 656 | "sub": { 657 | Description: ``, 658 | Ref: "#/definitions/SubGroup", 659 | Type: smd.Object, 660 | }, 661 | }, 662 | }, 663 | "SubGroup": { 664 | Type: "object", 665 | Properties: map[string]smd.Property{ 666 | "id": { 667 | Description: ``, 668 | Type: smd.Integer, 669 | }, 670 | "title": { 671 | Description: ``, 672 | Type: smd.String, 673 | }, 674 | }, 675 | }, 676 | }, 677 | }, 678 | }, 679 | Returns: smd.JSONSchema{ 680 | Description: ``, 681 | Optional: false, 682 | Type: smd.Boolean, 683 | }, 684 | }, 685 | "Third": { 686 | Description: ``, 687 | Parameters: []smd.JSONSchema{}, 688 | Returns: smd.JSONSchema{ 689 | Description: ``, 690 | Optional: false, 691 | Type: smd.Object, 692 | Properties: map[string]smd.Property{ 693 | "id": { 694 | Description: ``, 695 | Type: smd.Integer, 696 | }, 697 | "group": { 698 | Description: ``, 699 | Type: smd.Array, 700 | Items: map[string]string{ 701 | "$ref": "#/definitions/Group", 702 | }, 703 | }, 704 | }, 705 | Definitions: map[string]smd.Definition{ 706 | "Group": { 707 | Type: "object", 708 | Properties: map[string]smd.Property{ 709 | "id": { 710 | Description: ``, 711 | Type: smd.Integer, 712 | }, 713 | "title": { 714 | Description: ``, 715 | Type: smd.String, 716 | }, 717 | "nodes": { 718 | Description: ``, 719 | Type: smd.Array, 720 | Items: map[string]string{ 721 | "$ref": "#/definitions/Group", 722 | }, 723 | }, 724 | "group": { 725 | Description: ``, 726 | Type: smd.Array, 727 | Items: map[string]string{ 728 | "$ref": "#/definitions/Group", 729 | }, 730 | }, 731 | "child": { 732 | Description: ``, 733 | Ref: "#/definitions/Group", 734 | Type: smd.Object, 735 | }, 736 | "sub": { 737 | Description: ``, 738 | Ref: "#/definitions/SubGroup", 739 | Type: smd.Object, 740 | }, 741 | }, 742 | }, 743 | "SubGroup": { 744 | Type: "object", 745 | Properties: map[string]smd.Property{ 746 | "id": { 747 | Description: ``, 748 | Type: smd.Integer, 749 | }, 750 | "title": { 751 | Description: ``, 752 | Type: smd.String, 753 | }, 754 | }, 755 | }, 756 | }, 757 | }, 758 | }, 759 | }, 760 | } 761 | } 762 | 763 | // Invoke is as generated code from zenrpc cmd 764 | func (s CatalogueService) Invoke(ctx context.Context, method string, params json.RawMessage) zenrpc.Response { 765 | resp := zenrpc.Response{} 766 | var err error 767 | 768 | switch method { 769 | case RPC.CatalogueService.First: 770 | var args = struct { 771 | Groups []Group `json:"groups"` 772 | }{} 773 | 774 | if zenrpc.IsArray(params) { 775 | if params, err = zenrpc.ConvertToObject([]string{"groups"}, params); err != nil { 776 | return zenrpc.NewResponseError(nil, zenrpc.InvalidParams, "", err.Error()) 777 | } 778 | } 779 | 780 | if len(params) > 0 { 781 | if err := json.Unmarshal(params, &args); err != nil { 782 | return zenrpc.NewResponseError(nil, zenrpc.InvalidParams, "", err.Error()) 783 | } 784 | } 785 | 786 | resp.Set(s.First(args.Groups)) 787 | 788 | case RPC.CatalogueService.Second: 789 | var args = struct { 790 | Campaigns []Campaign `json:"campaigns"` 791 | }{} 792 | 793 | if zenrpc.IsArray(params) { 794 | if params, err = zenrpc.ConvertToObject([]string{"campaigns"}, params); err != nil { 795 | return zenrpc.NewResponseError(nil, zenrpc.InvalidParams, "", err.Error()) 796 | } 797 | } 798 | 799 | if len(params) > 0 { 800 | if err := json.Unmarshal(params, &args); err != nil { 801 | return zenrpc.NewResponseError(nil, zenrpc.InvalidParams, "", err.Error()) 802 | } 803 | } 804 | 805 | resp.Set(s.Second(args.Campaigns)) 806 | 807 | case RPC.CatalogueService.Third: 808 | resp.Set(s.Third()) 809 | 810 | default: 811 | resp = zenrpc.NewResponseError(nil, zenrpc.MethodNotFound, "", nil) 812 | } 813 | 814 | return resp 815 | } 816 | 817 | func (PhoneBook) SMD() smd.ServiceInfo { 818 | return smd.ServiceInfo{ 819 | Description: ``, 820 | Methods: map[string]smd.Service{ 821 | "Get": { 822 | Description: `Get returns all people from DB.`, 823 | Parameters: []smd.JSONSchema{ 824 | { 825 | Name: "search", 826 | Optional: false, 827 | Description: ``, 828 | Type: smd.Object, 829 | Properties: map[string]smd.Property{ 830 | "ByName": { 831 | Description: `ByName is filter for searching person by first name or last name.`, 832 | Type: smd.String, 833 | }, 834 | "ByType": { 835 | Description: ``, 836 | Type: smd.String, 837 | }, 838 | "ByPhone": { 839 | Description: ``, 840 | Type: smd.String, 841 | }, 842 | "ByAddress": { 843 | Description: ``, 844 | Ref: "#/definitions/Address", 845 | Type: smd.Object, 846 | }, 847 | }, 848 | Definitions: map[string]smd.Definition{ 849 | "Address": { 850 | Type: "object", 851 | Properties: map[string]smd.Property{ 852 | "Street": { 853 | Description: ``, 854 | Type: smd.String, 855 | }, 856 | "City": { 857 | Description: ``, 858 | Type: smd.String, 859 | }, 860 | }, 861 | }, 862 | }, 863 | }, 864 | { 865 | Name: "page", 866 | Optional: true, 867 | Description: `current page`, 868 | Type: smd.Integer, 869 | }, 870 | { 871 | Name: "count", 872 | Optional: true, 873 | Description: `page size`, 874 | Type: smd.Integer, 875 | }, 876 | }, 877 | Returns: smd.JSONSchema{ 878 | Description: ``, 879 | Optional: false, 880 | Type: smd.Array, 881 | Items: map[string]string{ 882 | "$ref": "#/definitions/Person", 883 | }, 884 | Definitions: map[string]smd.Definition{ 885 | "Person": { 886 | Type: "object", 887 | Properties: map[string]smd.Property{ 888 | "ID": { 889 | Description: `ID is Unique Identifier for person`, 890 | Type: smd.Integer, 891 | }, 892 | "FirstName": { 893 | Description: ``, 894 | Type: smd.String, 895 | }, 896 | "LastName": { 897 | Description: ``, 898 | Type: smd.String, 899 | }, 900 | "Phone": { 901 | Description: `Phone is main phone`, 902 | Type: smd.String, 903 | }, 904 | "WorkPhone": { 905 | Description: ``, 906 | Type: smd.String, 907 | }, 908 | "Mobile": { 909 | Description: ``, 910 | Type: smd.Array, 911 | Items: map[string]string{ 912 | "type": smd.String, 913 | }, 914 | }, 915 | "Deleted": { 916 | Description: `Deleted is flag for`, 917 | Type: smd.Boolean, 918 | }, 919 | "Addresses": { 920 | Description: `Addresses Could be nil or len() == 0.`, 921 | Type: smd.Array, 922 | Items: map[string]string{ 923 | "$ref": "#/definitions/Address", 924 | }, 925 | }, 926 | "address": { 927 | Description: ``, 928 | Ref: "#/definitions/Address", 929 | Type: smd.Object, 930 | }, 931 | }, 932 | }, 933 | "Address": { 934 | Type: "object", 935 | Properties: map[string]smd.Property{ 936 | "Street": { 937 | Description: ``, 938 | Type: smd.String, 939 | }, 940 | "City": { 941 | Description: ``, 942 | Type: smd.String, 943 | }, 944 | }, 945 | }, 946 | }, 947 | }, 948 | }, 949 | "ValidateSearch": { 950 | Description: `ValidateSearch returns given search as result.`, 951 | Parameters: []smd.JSONSchema{ 952 | { 953 | Name: "search", 954 | Optional: true, 955 | Description: `search object`, 956 | Type: smd.Object, 957 | Properties: map[string]smd.Property{ 958 | "ByName": { 959 | Description: `ByName is filter for searching person by first name or last name.`, 960 | Type: smd.String, 961 | }, 962 | "ByType": { 963 | Description: ``, 964 | Type: smd.String, 965 | }, 966 | "ByPhone": { 967 | Description: ``, 968 | Type: smd.String, 969 | }, 970 | "ByAddress": { 971 | Description: ``, 972 | Ref: "#/definitions/Address", 973 | Type: smd.Object, 974 | }, 975 | }, 976 | Definitions: map[string]smd.Definition{ 977 | "Address": { 978 | Type: "object", 979 | Properties: map[string]smd.Property{ 980 | "Street": { 981 | Description: ``, 982 | Type: smd.String, 983 | }, 984 | "City": { 985 | Description: ``, 986 | Type: smd.String, 987 | }, 988 | }, 989 | }, 990 | }, 991 | }, 992 | }, 993 | Returns: smd.JSONSchema{ 994 | Description: ``, 995 | Optional: true, 996 | Type: smd.Object, 997 | Properties: map[string]smd.Property{ 998 | "ByName": { 999 | Description: `ByName is filter for searching person by first name or last name.`, 1000 | Type: smd.String, 1001 | }, 1002 | "ByType": { 1003 | Description: ``, 1004 | Type: smd.String, 1005 | }, 1006 | "ByPhone": { 1007 | Description: ``, 1008 | Type: smd.String, 1009 | }, 1010 | "ByAddress": { 1011 | Description: ``, 1012 | Ref: "#/definitions/Address", 1013 | Type: smd.Object, 1014 | }, 1015 | }, 1016 | Definitions: map[string]smd.Definition{ 1017 | "Address": { 1018 | Type: "object", 1019 | Properties: map[string]smd.Property{ 1020 | "Street": { 1021 | Description: ``, 1022 | Type: smd.String, 1023 | }, 1024 | "City": { 1025 | Description: ``, 1026 | Type: smd.String, 1027 | }, 1028 | }, 1029 | }, 1030 | }, 1031 | }, 1032 | }, 1033 | "ById": { 1034 | Description: `ById returns Person from DB.`, 1035 | Parameters: []smd.JSONSchema{ 1036 | { 1037 | Name: "id", 1038 | Optional: false, 1039 | Description: `person id`, 1040 | Type: smd.Integer, 1041 | }, 1042 | }, 1043 | Returns: smd.JSONSchema{ 1044 | Description: ``, 1045 | Optional: true, 1046 | Type: smd.Object, 1047 | Properties: map[string]smd.Property{ 1048 | "ID": { 1049 | Description: `ID is Unique Identifier for person`, 1050 | Type: smd.Integer, 1051 | }, 1052 | "FirstName": { 1053 | Description: ``, 1054 | Type: smd.String, 1055 | }, 1056 | "LastName": { 1057 | Description: ``, 1058 | Type: smd.String, 1059 | }, 1060 | "Phone": { 1061 | Description: `Phone is main phone`, 1062 | Type: smd.String, 1063 | }, 1064 | "WorkPhone": { 1065 | Description: ``, 1066 | Type: smd.String, 1067 | }, 1068 | "Mobile": { 1069 | Description: ``, 1070 | Type: smd.Array, 1071 | Items: map[string]string{ 1072 | "type": smd.String, 1073 | }, 1074 | }, 1075 | "Deleted": { 1076 | Description: `Deleted is flag for`, 1077 | Type: smd.Boolean, 1078 | }, 1079 | "Addresses": { 1080 | Description: `Addresses Could be nil or len() == 0.`, 1081 | Type: smd.Array, 1082 | Items: map[string]string{ 1083 | "$ref": "#/definitions/Address", 1084 | }, 1085 | }, 1086 | "address": { 1087 | Description: ``, 1088 | Ref: "#/definitions/Address", 1089 | Type: smd.Object, 1090 | }, 1091 | }, 1092 | Definitions: map[string]smd.Definition{ 1093 | "Address": { 1094 | Type: "object", 1095 | Properties: map[string]smd.Property{ 1096 | "Street": { 1097 | Description: ``, 1098 | Type: smd.String, 1099 | }, 1100 | "City": { 1101 | Description: ``, 1102 | Type: smd.String, 1103 | }, 1104 | }, 1105 | }, 1106 | }, 1107 | }, 1108 | Errors: map[int]string{ 1109 | 404: "person was not found", 1110 | }, 1111 | }, 1112 | "Delete": { 1113 | Description: `Delete marks person as deleted.`, 1114 | Parameters: []smd.JSONSchema{ 1115 | { 1116 | Name: "id", 1117 | Optional: false, 1118 | Description: `person id`, 1119 | Type: smd.Integer, 1120 | }, 1121 | }, 1122 | Returns: smd.JSONSchema{ 1123 | Description: ``, 1124 | Optional: false, 1125 | Type: smd.Boolean, 1126 | }, 1127 | }, 1128 | "Remove": { 1129 | Description: `Removes deletes person from DB.`, 1130 | Parameters: []smd.JSONSchema{ 1131 | { 1132 | Name: "id", 1133 | Optional: false, 1134 | Description: `person id`, 1135 | Type: smd.Integer, 1136 | }, 1137 | }, 1138 | Returns: smd.JSONSchema{ 1139 | Description: `operation result`, 1140 | Optional: false, 1141 | Type: smd.Boolean, 1142 | }, 1143 | }, 1144 | "Save": { 1145 | Description: `Save saves person to DB.`, 1146 | Parameters: []smd.JSONSchema{ 1147 | { 1148 | Name: "p", 1149 | Optional: false, 1150 | Description: ``, 1151 | Type: smd.Object, 1152 | Properties: map[string]smd.Property{ 1153 | "ID": { 1154 | Description: `ID is Unique Identifier for person`, 1155 | Type: smd.Integer, 1156 | }, 1157 | "FirstName": { 1158 | Description: ``, 1159 | Type: smd.String, 1160 | }, 1161 | "LastName": { 1162 | Description: ``, 1163 | Type: smd.String, 1164 | }, 1165 | "Phone": { 1166 | Description: `Phone is main phone`, 1167 | Type: smd.String, 1168 | }, 1169 | "WorkPhone": { 1170 | Description: ``, 1171 | Type: smd.String, 1172 | }, 1173 | "Mobile": { 1174 | Description: ``, 1175 | Type: smd.Array, 1176 | Items: map[string]string{ 1177 | "type": smd.String, 1178 | }, 1179 | }, 1180 | "Deleted": { 1181 | Description: `Deleted is flag for`, 1182 | Type: smd.Boolean, 1183 | }, 1184 | "Addresses": { 1185 | Description: `Addresses Could be nil or len() == 0.`, 1186 | Type: smd.Array, 1187 | Items: map[string]string{ 1188 | "$ref": "#/definitions/Address", 1189 | }, 1190 | }, 1191 | "address": { 1192 | Description: ``, 1193 | Ref: "#/definitions/Address", 1194 | Type: smd.Object, 1195 | }, 1196 | }, 1197 | Definitions: map[string]smd.Definition{ 1198 | "Address": { 1199 | Type: "object", 1200 | Properties: map[string]smd.Property{ 1201 | "Street": { 1202 | Description: ``, 1203 | Type: smd.String, 1204 | }, 1205 | "City": { 1206 | Description: ``, 1207 | Type: smd.String, 1208 | }, 1209 | }, 1210 | }, 1211 | }, 1212 | }, 1213 | { 1214 | Name: "replace", 1215 | Optional: true, 1216 | Description: `update person if exist`, 1217 | Type: smd.Boolean, 1218 | }, 1219 | }, 1220 | Returns: smd.JSONSchema{ 1221 | Description: ``, 1222 | Optional: false, 1223 | Type: smd.Integer, 1224 | }, 1225 | Errors: map[int]string{ 1226 | 400: "invalid request", 1227 | 401: "use replace=true", 1228 | }, 1229 | }, 1230 | "Echo": { 1231 | Description: `Prints message`, 1232 | Parameters: []smd.JSONSchema{ 1233 | { 1234 | Name: "str", 1235 | Optional: true, 1236 | Description: ``, 1237 | Type: smd.String, 1238 | }, 1239 | }, 1240 | Returns: smd.JSONSchema{ 1241 | Description: ``, 1242 | Optional: false, 1243 | Type: smd.String, 1244 | }, 1245 | }, 1246 | }, 1247 | } 1248 | } 1249 | 1250 | // Invoke is as generated code from zenrpc cmd 1251 | func (s PhoneBook) Invoke(ctx context.Context, method string, params json.RawMessage) zenrpc.Response { 1252 | resp := zenrpc.Response{} 1253 | var err error 1254 | 1255 | switch method { 1256 | case RPC.PhoneBook.Get: 1257 | var args = struct { 1258 | Search PersonSearch `json:"search"` 1259 | Page *int `json:"page"` 1260 | Count *int `json:"count"` 1261 | }{} 1262 | 1263 | if zenrpc.IsArray(params) { 1264 | if params, err = zenrpc.ConvertToObject([]string{"search", "page", "count"}, params); err != nil { 1265 | return zenrpc.NewResponseError(nil, zenrpc.InvalidParams, "", err.Error()) 1266 | } 1267 | } 1268 | 1269 | if len(params) > 0 { 1270 | if err := json.Unmarshal(params, &args); err != nil { 1271 | return zenrpc.NewResponseError(nil, zenrpc.InvalidParams, "", err.Error()) 1272 | } 1273 | } 1274 | 1275 | //zenrpc:count=50 page size 1276 | if args.Count == nil { 1277 | var v int = 50 1278 | args.Count = &v 1279 | } 1280 | 1281 | //zenrpc:page=0 current page 1282 | if args.Page == nil { 1283 | var v int = 0 1284 | args.Page = &v 1285 | } 1286 | 1287 | resp.Set(s.Get(args.Search, args.Page, args.Count)) 1288 | 1289 | case RPC.PhoneBook.ValidateSearch: 1290 | var args = struct { 1291 | Search *PersonSearch `json:"search"` 1292 | }{} 1293 | 1294 | if zenrpc.IsArray(params) { 1295 | if params, err = zenrpc.ConvertToObject([]string{"search"}, params); err != nil { 1296 | return zenrpc.NewResponseError(nil, zenrpc.InvalidParams, "", err.Error()) 1297 | } 1298 | } 1299 | 1300 | if len(params) > 0 { 1301 | if err := json.Unmarshal(params, &args); err != nil { 1302 | return zenrpc.NewResponseError(nil, zenrpc.InvalidParams, "", err.Error()) 1303 | } 1304 | } 1305 | 1306 | resp.Set(s.ValidateSearch(args.Search)) 1307 | 1308 | case RPC.PhoneBook.ById: 1309 | var args = struct { 1310 | Id uint64 `json:"id"` 1311 | }{} 1312 | 1313 | if zenrpc.IsArray(params) { 1314 | if params, err = zenrpc.ConvertToObject([]string{"id"}, params); err != nil { 1315 | return zenrpc.NewResponseError(nil, zenrpc.InvalidParams, "", err.Error()) 1316 | } 1317 | } 1318 | 1319 | if len(params) > 0 { 1320 | if err := json.Unmarshal(params, &args); err != nil { 1321 | return zenrpc.NewResponseError(nil, zenrpc.InvalidParams, "", err.Error()) 1322 | } 1323 | } 1324 | 1325 | resp.Set(s.ById(args.Id)) 1326 | 1327 | case RPC.PhoneBook.Delete: 1328 | var args = struct { 1329 | Id uint64 `json:"id"` 1330 | }{} 1331 | 1332 | if zenrpc.IsArray(params) { 1333 | if params, err = zenrpc.ConvertToObject([]string{"id"}, params); err != nil { 1334 | return zenrpc.NewResponseError(nil, zenrpc.InvalidParams, "", err.Error()) 1335 | } 1336 | } 1337 | 1338 | if len(params) > 0 { 1339 | if err := json.Unmarshal(params, &args); err != nil { 1340 | return zenrpc.NewResponseError(nil, zenrpc.InvalidParams, "", err.Error()) 1341 | } 1342 | } 1343 | 1344 | resp.Set(s.Delete(args.Id)) 1345 | 1346 | case RPC.PhoneBook.Remove: 1347 | var args = struct { 1348 | Id uint64 `json:"id"` 1349 | }{} 1350 | 1351 | if zenrpc.IsArray(params) { 1352 | if params, err = zenrpc.ConvertToObject([]string{"id"}, params); err != nil { 1353 | return zenrpc.NewResponseError(nil, zenrpc.InvalidParams, "", err.Error()) 1354 | } 1355 | } 1356 | 1357 | if len(params) > 0 { 1358 | if err := json.Unmarshal(params, &args); err != nil { 1359 | return zenrpc.NewResponseError(nil, zenrpc.InvalidParams, "", err.Error()) 1360 | } 1361 | } 1362 | 1363 | resp.Set(s.Remove(args.Id)) 1364 | 1365 | case RPC.PhoneBook.Save: 1366 | var args = struct { 1367 | P Person `json:"p"` 1368 | Replace *bool `json:"replace"` 1369 | }{} 1370 | 1371 | if zenrpc.IsArray(params) { 1372 | if params, err = zenrpc.ConvertToObject([]string{"p", "replace"}, params); err != nil { 1373 | return zenrpc.NewResponseError(nil, zenrpc.InvalidParams, "", err.Error()) 1374 | } 1375 | } 1376 | 1377 | if len(params) > 0 { 1378 | if err := json.Unmarshal(params, &args); err != nil { 1379 | return zenrpc.NewResponseError(nil, zenrpc.InvalidParams, "", err.Error()) 1380 | } 1381 | } 1382 | 1383 | //zenrpc:replace=false update person if exist 1384 | if args.Replace == nil { 1385 | var v bool = false 1386 | args.Replace = &v 1387 | } 1388 | 1389 | resp.Set(s.Save(args.P, args.Replace)) 1390 | 1391 | case RPC.PhoneBook.Echo: 1392 | var args = struct { 1393 | Str *string `json:"type"` 1394 | }{} 1395 | 1396 | if zenrpc.IsArray(params) { 1397 | if params, err = zenrpc.ConvertToObject([]string{"type"}, params); err != nil { 1398 | return zenrpc.NewResponseError(nil, zenrpc.InvalidParams, "", err.Error()) 1399 | } 1400 | } 1401 | 1402 | if len(params) > 0 { 1403 | if err := json.Unmarshal(params, &args); err != nil { 1404 | return zenrpc.NewResponseError(nil, zenrpc.InvalidParams, "", err.Error()) 1405 | } 1406 | } 1407 | 1408 | //zenrpc:str(type)=`"hello world"` 1409 | if args.Str == nil { 1410 | var v string = "hello world" 1411 | args.Str = &v 1412 | } 1413 | 1414 | resp.Set(s.Echo(*args.Str)) 1415 | 1416 | default: 1417 | resp = zenrpc.NewResponseError(nil, zenrpc.MethodNotFound, "", nil) 1418 | } 1419 | 1420 | return resp 1421 | } 1422 | 1423 | func (PrintService) SMD() smd.ServiceInfo { 1424 | return smd.ServiceInfo{ 1425 | Description: ``, 1426 | Methods: map[string]smd.Service{ 1427 | "PrintRequiredDefault": { 1428 | Description: ``, 1429 | Parameters: []smd.JSONSchema{ 1430 | { 1431 | Name: "s", 1432 | Optional: true, 1433 | Description: ``, 1434 | Type: smd.String, 1435 | }, 1436 | }, 1437 | Returns: smd.JSONSchema{ 1438 | Description: ``, 1439 | Optional: false, 1440 | Type: smd.String, 1441 | }, 1442 | }, 1443 | "PrintOptionalWithDefault": { 1444 | Description: ``, 1445 | Parameters: []smd.JSONSchema{ 1446 | { 1447 | Name: "s", 1448 | Optional: true, 1449 | Description: ``, 1450 | Type: smd.String, 1451 | }, 1452 | }, 1453 | Returns: smd.JSONSchema{ 1454 | Description: ``, 1455 | Optional: false, 1456 | Type: smd.String, 1457 | }, 1458 | }, 1459 | "PrintRequired": { 1460 | Description: ``, 1461 | Parameters: []smd.JSONSchema{ 1462 | { 1463 | Name: "s", 1464 | Optional: false, 1465 | Description: ``, 1466 | Type: smd.String, 1467 | }, 1468 | }, 1469 | Returns: smd.JSONSchema{ 1470 | Description: ``, 1471 | Optional: false, 1472 | Type: smd.String, 1473 | }, 1474 | }, 1475 | "PrintOptional": { 1476 | Description: ``, 1477 | Parameters: []smd.JSONSchema{ 1478 | { 1479 | Name: "s", 1480 | Optional: true, 1481 | Description: ``, 1482 | Type: smd.String, 1483 | }, 1484 | }, 1485 | Returns: smd.JSONSchema{ 1486 | Description: ``, 1487 | Optional: false, 1488 | Type: smd.String, 1489 | }, 1490 | }, 1491 | }, 1492 | } 1493 | } 1494 | 1495 | // Invoke is as generated code from zenrpc cmd 1496 | func (s PrintService) Invoke(ctx context.Context, method string, params json.RawMessage) zenrpc.Response { 1497 | resp := zenrpc.Response{} 1498 | var err error 1499 | 1500 | switch method { 1501 | case RPC.PrintService.PrintRequiredDefault: 1502 | var args = struct { 1503 | S *string `json:"s"` 1504 | }{} 1505 | 1506 | if zenrpc.IsArray(params) { 1507 | if params, err = zenrpc.ConvertToObject([]string{"s"}, params); err != nil { 1508 | return zenrpc.NewResponseError(nil, zenrpc.InvalidParams, "", err.Error()) 1509 | } 1510 | } 1511 | 1512 | if len(params) > 0 { 1513 | if err := json.Unmarshal(params, &args); err != nil { 1514 | return zenrpc.NewResponseError(nil, zenrpc.InvalidParams, "", err.Error()) 1515 | } 1516 | } 1517 | 1518 | //zenrpc:s="test" 1519 | if args.S == nil { 1520 | var v string = "test" 1521 | args.S = &v 1522 | } 1523 | 1524 | resp.Set(s.PrintRequiredDefault(*args.S)) 1525 | 1526 | case RPC.PrintService.PrintOptionalWithDefault: 1527 | var args = struct { 1528 | S *string `json:"s"` 1529 | }{} 1530 | 1531 | if zenrpc.IsArray(params) { 1532 | if params, err = zenrpc.ConvertToObject([]string{"s"}, params); err != nil { 1533 | return zenrpc.NewResponseError(nil, zenrpc.InvalidParams, "", err.Error()) 1534 | } 1535 | } 1536 | 1537 | if len(params) > 0 { 1538 | if err := json.Unmarshal(params, &args); err != nil { 1539 | return zenrpc.NewResponseError(nil, zenrpc.InvalidParams, "", err.Error()) 1540 | } 1541 | } 1542 | 1543 | //zenrpc:s="test" 1544 | if args.S == nil { 1545 | var v string = "test" 1546 | args.S = &v 1547 | } 1548 | 1549 | resp.Set(s.PrintOptionalWithDefault(args.S)) 1550 | 1551 | case RPC.PrintService.PrintRequired: 1552 | var args = struct { 1553 | S string `json:"s"` 1554 | }{} 1555 | 1556 | if zenrpc.IsArray(params) { 1557 | if params, err = zenrpc.ConvertToObject([]string{"s"}, params); err != nil { 1558 | return zenrpc.NewResponseError(nil, zenrpc.InvalidParams, "", err.Error()) 1559 | } 1560 | } 1561 | 1562 | if len(params) > 0 { 1563 | if err := json.Unmarshal(params, &args); err != nil { 1564 | return zenrpc.NewResponseError(nil, zenrpc.InvalidParams, "", err.Error()) 1565 | } 1566 | } 1567 | 1568 | resp.Set(s.PrintRequired(args.S)) 1569 | 1570 | case RPC.PrintService.PrintOptional: 1571 | var args = struct { 1572 | S *string `json:"s"` 1573 | }{} 1574 | 1575 | if zenrpc.IsArray(params) { 1576 | if params, err = zenrpc.ConvertToObject([]string{"s"}, params); err != nil { 1577 | return zenrpc.NewResponseError(nil, zenrpc.InvalidParams, "", err.Error()) 1578 | } 1579 | } 1580 | 1581 | if len(params) > 0 { 1582 | if err := json.Unmarshal(params, &args); err != nil { 1583 | return zenrpc.NewResponseError(nil, zenrpc.InvalidParams, "", err.Error()) 1584 | } 1585 | } 1586 | 1587 | resp.Set(s.PrintOptional(args.S)) 1588 | 1589 | default: 1590 | resp = zenrpc.NewResponseError(nil, zenrpc.MethodNotFound, "", nil) 1591 | } 1592 | 1593 | return resp 1594 | } 1595 | -------------------------------------------------------------------------------- /zenrpc/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "github.com/semrush/zenrpc/v2/parser" 7 | "go/format" 8 | "os" 9 | "time" 10 | ) 11 | 12 | const ( 13 | version = "2.1.1" 14 | 15 | openIssueURL = "https://github.com/semrush/zenrpc/issues/new" 16 | githubURL = "https://github.com/semrush/zenrpc" 17 | ) 18 | 19 | func main() { 20 | start := time.Now() 21 | fmt.Printf("Generator version: %s\n", version) 22 | 23 | var filename string 24 | if len(os.Args) > 1 { 25 | filename = os.Args[len(os.Args)-1] 26 | } else { 27 | filename = os.Getenv("GOFILE") 28 | } 29 | 30 | if len(filename) == 0 { 31 | fmt.Fprintln(os.Stderr, "File path is empty") 32 | os.Exit(1) 33 | } 34 | 35 | fmt.Printf("Entrypoint: %s\n", filename) 36 | 37 | // create package info 38 | pi, err := parser.NewPackageInfo(filename) 39 | if err != nil { 40 | printError(err) 41 | os.Exit(1) 42 | } 43 | 44 | outputFileName := pi.OutputFilename() 45 | 46 | // remove output file if it already exists 47 | if _, err := os.Stat(outputFileName); err == nil { 48 | if err := os.Remove(outputFileName); err != nil { 49 | printError(err) 50 | os.Exit(1) 51 | } 52 | } 53 | 54 | if err := pi.Parse(filename); err != nil { 55 | printError(err) 56 | os.Exit(1) 57 | } 58 | 59 | if len(pi.Services) == 0 { 60 | fmt.Fprintln(os.Stderr, "Services not found") 61 | os.Exit(1) 62 | } 63 | 64 | if err := generateFile(outputFileName, pi); err != nil { 65 | printError(err) 66 | os.Exit(1) 67 | } 68 | 69 | fmt.Printf("Generated: %s\n", outputFileName) 70 | fmt.Printf("Duration: %dms\n", int64(time.Since(start)/time.Millisecond)) 71 | fmt.Println() 72 | fmt.Print(pi) 73 | fmt.Println() 74 | } 75 | 76 | func printError(err error) { 77 | // print error to stderr 78 | fmt.Fprintf(os.Stderr, "Error: %s\n", err) 79 | 80 | // print contact information to stdout 81 | fmt.Println("\nYou may help us and create issue:") 82 | fmt.Printf("\t%s\n", openIssueURL) 83 | fmt.Println("For more information, see:") 84 | fmt.Printf("\t%s\n\n", githubURL) 85 | } 86 | 87 | func generateFile(outputFileName string, pi *parser.PackageInfo) error { 88 | file, err := os.Create(outputFileName) 89 | if err != nil { 90 | return err 91 | } 92 | defer file.Close() 93 | 94 | output := new(bytes.Buffer) 95 | if err := serviceTemplate.Execute(output, pi); err != nil { 96 | return err 97 | } 98 | 99 | source, err := format.Source(output.Bytes()) 100 | if err != nil { 101 | return err 102 | } 103 | 104 | if _, err = file.Write(source); err != nil { 105 | return err 106 | } 107 | 108 | return nil 109 | } 110 | -------------------------------------------------------------------------------- /zenrpc/template.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/semrush/zenrpc/v2/parser" 5 | "text/template" 6 | ) 7 | 8 | var ( 9 | serviceTemplate = template.Must(template.New("service"). 10 | Funcs(template.FuncMap{"definitions": parser.Definitions}). 11 | Parse(` 12 | {{define "smdType" -}} 13 | Type: smd.{{.Type}}, 14 | {{- if eq .Type "Array" }} 15 | Items: map[string]string{ 16 | {{- if and (eq .ItemsType "Object") .Ref }} 17 | "$ref": "#/definitions/{{.Ref}}", 18 | {{else}} 19 | "type": smd.{{.ItemsType}}, 20 | {{end}} 21 | }, 22 | {{- end}} 23 | {{- end}} 24 | 25 | {{define "properties" -}} 26 | Properties: map[string]smd.Property{ 27 | {{range $i, $e := . -}} 28 | "{{.Name}}": { 29 | Description: ` + "`{{.Description}}`" + `, 30 | {{- if and (eq .SMDType.Type "Object") .SMDType.Ref }} 31 | Ref: "#/definitions/{{.SMDType.Ref}}", 32 | {{- end}} 33 | {{template "smdType" .SMDType}} 34 | }, 35 | {{ end }} 36 | }, 37 | {{- end}} 38 | 39 | {{define "definitions" -}} 40 | {{if .}} 41 | Definitions: map[string]smd.Definition{ 42 | {{- range .}} 43 | "{{ .Name }}": { 44 | Type: "object", 45 | {{ template "properties" .Properties}} 46 | }, 47 | {{- end }} 48 | }, 49 | {{ end }} 50 | {{- end}} 51 | 52 | 53 | // Code generated by zenrpc; DO NOT EDIT. 54 | 55 | package {{.PackageName}} 56 | 57 | import ( 58 | "encoding/json" 59 | "context" 60 | 61 | "github.com/semrush/zenrpc/v2" 62 | "github.com/semrush/zenrpc/v2/smd" 63 | 64 | {{ range .ImportsIncludedToGeneratedCode}} 65 | {{if .Name}}{{.Name.Name}} {{end}}{{.Path.Value}} 66 | {{- end }} 67 | ) 68 | 69 | var RPC = struct { 70 | {{ range .Services}} 71 | {{.Name}} struct { {{range $i, $e := .Methods }}{{if $i}}, {{end}}{{.Name}}{{ end }} string } 72 | {{- end }} 73 | }{ 74 | {{- range .Services}} 75 | {{.Name}}: struct { {{range $i, $e := .Methods }} {{if $i}}, {{end}}{{.Name}}{{ end }} string }{ 76 | {{- range .Methods }} 77 | {{.Name}}: "{{.LowerCaseName}}", 78 | {{- end }} 79 | }, 80 | {{- end }} 81 | } 82 | 83 | {{ range $s := .Services}} 84 | 85 | func ({{.Name}}) SMD() smd.ServiceInfo { 86 | return smd.ServiceInfo{ 87 | Description: ` + "`{{.Description}}`" + `, 88 | Methods: map[string]smd.Service{ 89 | {{- range .Methods }} 90 | "{{.Name}}": { 91 | Description: ` + "`{{.Description}}`" + `, 92 | Parameters: []smd.JSONSchema{ 93 | {{- range .Args }} 94 | { 95 | Name: "{{.Name}}", 96 | Optional: {{or .HasStar .HasDefaultValue}}, 97 | Description: ` + "`{{.Description}}`" + `, 98 | {{template "smdType" .SMDType}} 99 | {{- if and (eq .SMDType.Type "Object") (ne .SMDType.Ref "")}} 100 | {{ template "properties" (index $.Structs .SMDType.Ref).Properties}} 101 | {{- end}} 102 | {{- template "definitions" definitions .SMDType $.Structs }} 103 | }, 104 | {{- end }} 105 | }, 106 | {{- if .SMDReturn}} 107 | Returns: smd.JSONSchema{ 108 | Description: ` + "`{{.SMDReturn.Description}}`" + `, 109 | Optional: {{.SMDReturn.HasStar}}, 110 | {{template "smdType" .SMDReturn.SMDType }} 111 | {{- if and (eq .SMDReturn.SMDType.Type "Object") (ne .SMDReturn.SMDType.Ref "")}} 112 | {{ template "properties" (index $.Structs .SMDReturn.SMDType.Ref).Properties}} 113 | {{- end}} 114 | {{- template "definitions" definitions .SMDReturn.SMDType $.Structs }} 115 | }, 116 | {{- end}} 117 | {{- if .Errors}} 118 | Errors: map[int]string{ 119 | {{- range .Errors }} 120 | {{.Code}}: "{{.Description}}", 121 | {{- end }} 122 | }, 123 | {{- end}} 124 | }, 125 | {{- end }} 126 | }, 127 | } 128 | } 129 | 130 | // Invoke is as generated code from zenrpc cmd 131 | func (s {{.Name}}) Invoke(ctx context.Context, method string, params json.RawMessage) zenrpc.Response { 132 | resp := zenrpc.Response{} 133 | {{ if .HasErrorVariable }}var err error{{ end }} 134 | 135 | switch method { 136 | {{- range .Methods }} 137 | case RPC.{{$s.Name}}.{{.Name}}: {{ if .Args }} 138 | var args = struct { 139 | {{ range .Args }} 140 | {{.CapitalName}} {{if and (not .HasStar) .HasDefaultValue}}*{{end}}{{.Type}} ` + "`json:\"{{.JsonName}}\"`" + ` 141 | {{- end }} 142 | }{} 143 | 144 | if zenrpc.IsArray(params) { 145 | if params, err = zenrpc.ConvertToObject([]string{ 146 | {{- range .Args }}"{{.JsonName}}",{{ end -}} 147 | }, params); err != nil { 148 | return zenrpc.NewResponseError(nil, zenrpc.InvalidParams, "", err.Error()) 149 | } 150 | } 151 | 152 | if len(params) > 0 { 153 | if err := json.Unmarshal(params, &args); err != nil { 154 | return zenrpc.NewResponseError(nil, zenrpc.InvalidParams, "", err.Error()) 155 | } 156 | } 157 | 158 | {{ range .DefaultValues }} 159 | {{.Comment}} 160 | if args.{{.CapitalName}} == nil { 161 | var v {{.Type}} = {{.Value}} 162 | args.{{.CapitalName}} = &v 163 | } 164 | {{ end }} 165 | 166 | {{ end }} {{if .Returns}} 167 | resp.Set(s.{{.Name}}({{if .HasContext}}ctx, {{end}} {{ range .Args }}{{if and (not .HasStar) .HasDefaultValue}}*{{end}}args.{{.CapitalName}}, {{ end }})) 168 | {{else}} 169 | s.{{.Name}}({{if .HasContext}}ctx, {{end}} {{ range .Args }}{{if and (not .HasStar) .HasDefaultValue}}*{{end}}args.{{.CapitalName}}, {{ end }}) 170 | {{end}} 171 | {{- end }} 172 | default: 173 | resp = zenrpc.NewResponseError(nil, zenrpc.MethodNotFound, "", nil) 174 | } 175 | 176 | return resp 177 | } 178 | {{- end }} 179 | `)) 180 | ) 181 | --------------------------------------------------------------------------------