├── .gitignore ├── .travis.yml ├── Gopkg.lock ├── Gopkg.toml ├── Makefile ├── README.md ├── acceptance_test.go ├── api ├── api.go ├── api.pb.go └── api.proto ├── cmd ├── grpc-example │ ├── Dockerfile │ ├── README.md │ └── main.go └── testclient │ └── main.go ├── config ├── grpc-examle-service.yaml └── grpc-example-deployment.yaml └── pkg ├── coinmarketcap ├── ticker.go └── ticker_test.go ├── server └── server.go └── types └── c_currency.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.db 2 | .idea/ 3 | docker-tmp/ 4 | vendor/ 5 | build/ 6 | tmp/ 7 | cmd/grpc-example/grpc-example 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | services: 3 | - docker 4 | language: go 5 | go: 6 | - "1.10" 7 | env: 8 | global: 9 | - CGO_ENABLED=0 10 | matrix: 11 | - SERVICE=grpc-example 12 | install: 13 | - curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh 14 | - dep ensure 15 | script: 16 | - go test -cover ./cmd/... ./pkg/... 17 | - go vet $(go list ./... | grep -v vendor) 18 | - cd cmd/${SERVICE} && go build 19 | - docker image build -t ${DOCKER_USERNAME}/${SERVICE}:${TRAVIS_BRANCH} . 20 | after_success: 21 | - if [ -n "${TRAVIS_TAG}" ] ; then 22 | docker login -u="${DOCKER_USERNAME}" -p="${DOCKER_PASSWORD}"; 23 | docker push yuribuerov/$SERVICE:$TRAVIS_BRANCH; 24 | fi 25 | -------------------------------------------------------------------------------- /Gopkg.lock: -------------------------------------------------------------------------------- 1 | # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. 2 | 3 | 4 | [[projects]] 5 | name = "github.com/davecgh/go-spew" 6 | packages = ["spew"] 7 | revision = "346938d642f2ec3594ed81d874461961cd0faa76" 8 | version = "v1.1.0" 9 | 10 | [[projects]] 11 | name = "github.com/go-kit/kit" 12 | packages = ["log"] 13 | revision = "v0.4.0-77-ga9ca672" 14 | 15 | [[projects]] 16 | name = "github.com/go-logfmt/logfmt" 17 | packages = ["."] 18 | revision = "390ab7935ee28ec6b286364bba9b4dd6410cb3d5" 19 | version = "v0.3.0" 20 | 21 | [[projects]] 22 | name = "github.com/go-stack/stack" 23 | packages = ["."] 24 | revision = "7a2f19628aabfe68f0766b59e74d6315f8347d22" 25 | version = "v1.5.3" 26 | 27 | [[projects]] 28 | name = "github.com/golang/protobuf" 29 | packages = [ 30 | "proto", 31 | "ptypes", 32 | "ptypes/any", 33 | "ptypes/duration", 34 | "ptypes/timestamp" 35 | ] 36 | revision = "e325f44" 37 | 38 | [[projects]] 39 | name = "github.com/grpc-ecosystem/go-grpc-middleware" 40 | packages = [ 41 | ".", 42 | "recovery" 43 | ] 44 | revision = "d0c54e68681ec7999ac17864470f3bee6521ba2b" 45 | 46 | [[projects]] 47 | branch = "master" 48 | name = "github.com/kr/logfmt" 49 | packages = ["."] 50 | revision = "b84e30acd515aadc4b783ad4ff83aff3299bdfe0" 51 | 52 | [[projects]] 53 | name = "github.com/pmezard/go-difflib" 54 | packages = ["difflib"] 55 | revision = "792786c7400a136282c1664665ae0a8db921c6c2" 56 | version = "v1.0.0" 57 | 58 | [[projects]] 59 | name = "github.com/stretchr/testify" 60 | packages = [ 61 | "assert", 62 | "require" 63 | ] 64 | revision = "v1.1.4-66-gf6abca5" 65 | 66 | [[projects]] 67 | branch = "master" 68 | name = "golang.org/x/net" 69 | packages = [ 70 | "context", 71 | "http2", 72 | "http2/hpack", 73 | "idna", 74 | "internal/timeseries", 75 | "lex/httplex", 76 | "trace" 77 | ] 78 | revision = "6078986fec03a1dcc236c34816c71b0e05018fda" 79 | 80 | [[projects]] 81 | name = "golang.org/x/text" 82 | packages = [ 83 | "collate", 84 | "collate/build", 85 | "internal/colltab", 86 | "internal/gen", 87 | "internal/tag", 88 | "internal/triegen", 89 | "internal/ucd", 90 | "language", 91 | "secure/bidirule", 92 | "transform", 93 | "unicode/bidi", 94 | "unicode/cldr", 95 | "unicode/norm", 96 | "unicode/rangetable" 97 | ] 98 | revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0" 99 | version = "v0.3.0" 100 | 101 | [[projects]] 102 | name = "google.golang.org/genproto" 103 | packages = ["googleapis/rpc/status"] 104 | revision = "aa2eb68" 105 | 106 | [[projects]] 107 | name = "google.golang.org/grpc" 108 | packages = [ 109 | ".", 110 | "balancer", 111 | "balancer/base", 112 | "balancer/roundrobin", 113 | "codes", 114 | "connectivity", 115 | "credentials", 116 | "encoding", 117 | "encoding/proto", 118 | "grpclb/grpc_lb_v1/messages", 119 | "grpclog", 120 | "internal", 121 | "keepalive", 122 | "metadata", 123 | "naming", 124 | "peer", 125 | "resolver", 126 | "resolver/dns", 127 | "resolver/passthrough", 128 | "stats", 129 | "status", 130 | "tap", 131 | "transport" 132 | ] 133 | revision = "8e4536a86ab602859c20df5ebfd0bd4228d08655" 134 | version = "v1.10.0" 135 | 136 | [solve-meta] 137 | analyzer-name = "dep" 138 | analyzer-version = 1 139 | inputs-digest = "6f666c11d6f4b97b49bbb97a620b0e0665e4d55dc0b1a6cd40fe80c1d5009b52" 140 | solver-name = "gps-cdcl" 141 | solver-version = 1 142 | -------------------------------------------------------------------------------- /Gopkg.toml: -------------------------------------------------------------------------------- 1 | # Gopkg.toml example 2 | # 3 | # Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md 4 | # for detailed Gopkg.toml documentation. 5 | # 6 | # required = ["github.com/user/thing/cmd/thing"] 7 | # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] 8 | # 9 | # [[constraint]] 10 | # name = "github.com/user/project" 11 | # version = "1.0.0" 12 | # 13 | # [[constraint]] 14 | # name = "github.com/user/project2" 15 | # branch = "dev" 16 | # source = "github.com/myfork/project2" 17 | # 18 | # [[override]] 19 | # name = "github.com/x/y" 20 | # version = "2.4.0" 21 | # 22 | # [prune] 23 | # non-go = false 24 | # go-tests = true 25 | # unused-packages = true 26 | 27 | 28 | [[constraint]] 29 | branch = "master" 30 | name = "golang.org/x/net" 31 | 32 | [[constraint]] 33 | name = "google.golang.org/grpc" 34 | version = "1.10.0" 35 | 36 | [prune] 37 | go-tests = true 38 | unused-packages = true 39 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | default: build 2 | 3 | .PHONY: build 4 | build: 5 | GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o "cmd/grpc-example/grpc-example" cmd/grpc-example/main.go 6 | test: 7 | @go test -cover ./cmd/... ./pkg/... -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # grpc-example 2 | 1. Install `dep` tool for dependency management: 3 | ```sh 4 | brew install dep 5 | ``` 6 | ```sh 7 | dep ensure 8 | ``` 9 | # Run Tests 10 | ```bash 11 | make test 12 | ``` 13 | 14 | # Run App 15 | ```bash 16 | make 17 | docker build -t grpc-example -f ./cmd/grpc-example/Dockerfile cmd/grpc-example 18 | docker run -it --rm --read-only \ 19 | --volume $(pwd)/docker-tmp:/tmp \ 20 | --publish 50051:50051 \ 21 | grpc-example 22 | ``` 23 | * Connection test 24 | ```bash 25 | curl -L https://127.0.0.1:50051 -XGET 26 | ``` 27 | 28 | * Client example 29 | ```bash 30 | go run ./cmd/testclient/main.go -port 50051 -limit 10 31 | ``` 32 | -------------------------------------------------------------------------------- /acceptance_test.go: -------------------------------------------------------------------------------- 1 | package grpc_example_test 2 | 3 | import ( 4 | "net" 5 | "os" 6 | "testing" 7 | 8 | "github.com/YuriBuerov/grpc-example/api" 9 | "github.com/YuriBuerov/grpc-example/pkg/server" 10 | "github.com/go-kit/kit/log" 11 | "github.com/stretchr/testify/require" 12 | "golang.org/x/net/context" 13 | "google.golang.org/grpc" 14 | ) 15 | 16 | func TestGetCCurrencies(t *testing.T) { 17 | addr, cancel := tServer(t) 18 | defer cancel() 19 | c, conn := tClient(t, addr) 20 | defer conn.Close() 21 | 22 | req := &api.GetCCurrenciesRequest{Limit: 3} 23 | resp, err := c.GetCCurrencies(context.Background(), req) 24 | 25 | require.Nil(t, err) 26 | require.NotNil(t, resp) 27 | } 28 | 29 | func tServer(t *testing.T) (string, func()) { 30 | logger := log.With(log.NewJSONLogger(os.Stdout), "caller", log.DefaultCaller) 31 | 32 | s, err := server.NewGRPCServer(logger) 33 | require.Nil(t, err) 34 | l, err := net.Listen("tcp", "localhost:0") 35 | require.Nil(t, err) 36 | 37 | go s.Serve(l) 38 | 39 | ctx, cancel := context.WithCancel(context.Background()) 40 | go func() { 41 | <-ctx.Done() 42 | s.GracefulStop() 43 | }() 44 | 45 | return l.Addr().String(), cancel 46 | } 47 | 48 | func tClient(t *testing.T, addr string) (api.ApiClient, *grpc.ClientConn) { 49 | c, err := grpc.Dial(addr, grpc.WithInsecure()) 50 | require.Nil(t, err) 51 | 52 | return api.NewApiClient(c), c 53 | } 54 | -------------------------------------------------------------------------------- /api/api.go: -------------------------------------------------------------------------------- 1 | // 2 | //go:generate protoc -I. -I$GOPATH/src -I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis --gofast_out=plugins=grpc:. api.proto 3 | package api 4 | -------------------------------------------------------------------------------- /api/api.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-gogo. DO NOT EDIT. 2 | // source: api.proto 3 | 4 | /* 5 | Package api is a generated protocol buffer package. 6 | 7 | It is generated from these files: 8 | api.proto 9 | 10 | It has these top-level messages: 11 | GetCCurrenciesRequest 12 | GetCCurrenciesResponse 13 | */ 14 | package api 15 | 16 | import proto "github.com/golang/protobuf/proto" 17 | import fmt "fmt" 18 | import math "math" 19 | 20 | import ( 21 | context "golang.org/x/net/context" 22 | grpc "google.golang.org/grpc" 23 | ) 24 | 25 | import io "io" 26 | 27 | // Reference imports to suppress errors if they are not otherwise used. 28 | var _ = proto.Marshal 29 | var _ = fmt.Errorf 30 | var _ = math.Inf 31 | 32 | // This is a compile-time assertion to ensure that this generated file 33 | // is compatible with the proto package it is being compiled against. 34 | // A compilation error at this line likely means your copy of the 35 | // proto package needs to be updated. 36 | const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package 37 | 38 | type GetCCurrenciesRequest struct { 39 | Limit uint32 `protobuf:"varint,1,opt,name=limit,proto3" json:"limit,omitempty"` 40 | } 41 | 42 | func (m *GetCCurrenciesRequest) Reset() { *m = GetCCurrenciesRequest{} } 43 | func (m *GetCCurrenciesRequest) String() string { return proto.CompactTextString(m) } 44 | func (*GetCCurrenciesRequest) ProtoMessage() {} 45 | func (*GetCCurrenciesRequest) Descriptor() ([]byte, []int) { return fileDescriptorApi, []int{0} } 46 | 47 | func (m *GetCCurrenciesRequest) GetLimit() uint32 { 48 | if m != nil { 49 | return m.Limit 50 | } 51 | return 0 52 | } 53 | 54 | type GetCCurrenciesResponse struct { 55 | Currencies []*GetCCurrenciesResponse_CCurrency `protobuf:"bytes,1,rep,name=currencies" json:"currencies,omitempty"` 56 | } 57 | 58 | func (m *GetCCurrenciesResponse) Reset() { *m = GetCCurrenciesResponse{} } 59 | func (m *GetCCurrenciesResponse) String() string { return proto.CompactTextString(m) } 60 | func (*GetCCurrenciesResponse) ProtoMessage() {} 61 | func (*GetCCurrenciesResponse) Descriptor() ([]byte, []int) { return fileDescriptorApi, []int{1} } 62 | 63 | func (m *GetCCurrenciesResponse) GetCurrencies() []*GetCCurrenciesResponse_CCurrency { 64 | if m != nil { 65 | return m.Currencies 66 | } 67 | return nil 68 | } 69 | 70 | type GetCCurrenciesResponse_CCurrency struct { 71 | Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` 72 | Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` 73 | Symbol string `protobuf:"bytes,3,opt,name=symbol,proto3" json:"symbol,omitempty"` 74 | Rank uint32 `protobuf:"varint,4,opt,name=rank,proto3" json:"rank,omitempty"` 75 | PriceUSD float64 `protobuf:"fixed64,5,opt,name=priceUSD,proto3" json:"priceUSD,omitempty"` 76 | DailyChange float64 `protobuf:"fixed64,6,opt,name=dailyChange,proto3" json:"dailyChange,omitempty"` 77 | } 78 | 79 | func (m *GetCCurrenciesResponse_CCurrency) Reset() { *m = GetCCurrenciesResponse_CCurrency{} } 80 | func (m *GetCCurrenciesResponse_CCurrency) String() string { return proto.CompactTextString(m) } 81 | func (*GetCCurrenciesResponse_CCurrency) ProtoMessage() {} 82 | func (*GetCCurrenciesResponse_CCurrency) Descriptor() ([]byte, []int) { 83 | return fileDescriptorApi, []int{1, 0} 84 | } 85 | 86 | func (m *GetCCurrenciesResponse_CCurrency) GetId() string { 87 | if m != nil { 88 | return m.Id 89 | } 90 | return "" 91 | } 92 | 93 | func (m *GetCCurrenciesResponse_CCurrency) GetName() string { 94 | if m != nil { 95 | return m.Name 96 | } 97 | return "" 98 | } 99 | 100 | func (m *GetCCurrenciesResponse_CCurrency) GetSymbol() string { 101 | if m != nil { 102 | return m.Symbol 103 | } 104 | return "" 105 | } 106 | 107 | func (m *GetCCurrenciesResponse_CCurrency) GetRank() uint32 { 108 | if m != nil { 109 | return m.Rank 110 | } 111 | return 0 112 | } 113 | 114 | func (m *GetCCurrenciesResponse_CCurrency) GetPriceUSD() float64 { 115 | if m != nil { 116 | return m.PriceUSD 117 | } 118 | return 0 119 | } 120 | 121 | func (m *GetCCurrenciesResponse_CCurrency) GetDailyChange() float64 { 122 | if m != nil { 123 | return m.DailyChange 124 | } 125 | return 0 126 | } 127 | 128 | func init() { 129 | proto.RegisterType((*GetCCurrenciesRequest)(nil), "api.GetCCurrenciesRequest") 130 | proto.RegisterType((*GetCCurrenciesResponse)(nil), "api.GetCCurrenciesResponse") 131 | proto.RegisterType((*GetCCurrenciesResponse_CCurrency)(nil), "api.GetCCurrenciesResponse.CCurrency") 132 | } 133 | 134 | // Reference imports to suppress errors if they are not otherwise used. 135 | var _ context.Context 136 | var _ grpc.ClientConn 137 | 138 | // This is a compile-time assertion to ensure that this generated file 139 | // is compatible with the grpc package it is being compiled against. 140 | const _ = grpc.SupportPackageIsVersion4 141 | 142 | // Client API for Api service 143 | 144 | type ApiClient interface { 145 | GetCCurrencies(ctx context.Context, in *GetCCurrenciesRequest, opts ...grpc.CallOption) (*GetCCurrenciesResponse, error) 146 | } 147 | 148 | type apiClient struct { 149 | cc *grpc.ClientConn 150 | } 151 | 152 | func NewApiClient(cc *grpc.ClientConn) ApiClient { 153 | return &apiClient{cc} 154 | } 155 | 156 | func (c *apiClient) GetCCurrencies(ctx context.Context, in *GetCCurrenciesRequest, opts ...grpc.CallOption) (*GetCCurrenciesResponse, error) { 157 | out := new(GetCCurrenciesResponse) 158 | err := grpc.Invoke(ctx, "/api.Api/GetCCurrencies", in, out, c.cc, opts...) 159 | if err != nil { 160 | return nil, err 161 | } 162 | return out, nil 163 | } 164 | 165 | // Server API for Api service 166 | 167 | type ApiServer interface { 168 | GetCCurrencies(context.Context, *GetCCurrenciesRequest) (*GetCCurrenciesResponse, error) 169 | } 170 | 171 | func RegisterApiServer(s *grpc.Server, srv ApiServer) { 172 | s.RegisterService(&_Api_serviceDesc, srv) 173 | } 174 | 175 | func _Api_GetCCurrencies_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 176 | in := new(GetCCurrenciesRequest) 177 | if err := dec(in); err != nil { 178 | return nil, err 179 | } 180 | if interceptor == nil { 181 | return srv.(ApiServer).GetCCurrencies(ctx, in) 182 | } 183 | info := &grpc.UnaryServerInfo{ 184 | Server: srv, 185 | FullMethod: "/api.Api/GetCCurrencies", 186 | } 187 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 188 | return srv.(ApiServer).GetCCurrencies(ctx, req.(*GetCCurrenciesRequest)) 189 | } 190 | return interceptor(ctx, in, info, handler) 191 | } 192 | 193 | var _Api_serviceDesc = grpc.ServiceDesc{ 194 | ServiceName: "api.Api", 195 | HandlerType: (*ApiServer)(nil), 196 | Methods: []grpc.MethodDesc{ 197 | { 198 | MethodName: "GetCCurrencies", 199 | Handler: _Api_GetCCurrencies_Handler, 200 | }, 201 | }, 202 | Streams: []grpc.StreamDesc{}, 203 | Metadata: "api.proto", 204 | } 205 | 206 | func (m *GetCCurrenciesRequest) Marshal() (dAtA []byte, err error) { 207 | size := m.Size() 208 | dAtA = make([]byte, size) 209 | n, err := m.MarshalTo(dAtA) 210 | if err != nil { 211 | return nil, err 212 | } 213 | return dAtA[:n], nil 214 | } 215 | 216 | func (m *GetCCurrenciesRequest) MarshalTo(dAtA []byte) (int, error) { 217 | var i int 218 | _ = i 219 | var l int 220 | _ = l 221 | if m.Limit != 0 { 222 | dAtA[i] = 0x8 223 | i++ 224 | i = encodeVarintApi(dAtA, i, uint64(m.Limit)) 225 | } 226 | return i, nil 227 | } 228 | 229 | func (m *GetCCurrenciesResponse) Marshal() (dAtA []byte, err error) { 230 | size := m.Size() 231 | dAtA = make([]byte, size) 232 | n, err := m.MarshalTo(dAtA) 233 | if err != nil { 234 | return nil, err 235 | } 236 | return dAtA[:n], nil 237 | } 238 | 239 | func (m *GetCCurrenciesResponse) MarshalTo(dAtA []byte) (int, error) { 240 | var i int 241 | _ = i 242 | var l int 243 | _ = l 244 | if len(m.Currencies) > 0 { 245 | for _, msg := range m.Currencies { 246 | dAtA[i] = 0xa 247 | i++ 248 | i = encodeVarintApi(dAtA, i, uint64(msg.Size())) 249 | n, err := msg.MarshalTo(dAtA[i:]) 250 | if err != nil { 251 | return 0, err 252 | } 253 | i += n 254 | } 255 | } 256 | return i, nil 257 | } 258 | 259 | func (m *GetCCurrenciesResponse_CCurrency) Marshal() (dAtA []byte, err error) { 260 | size := m.Size() 261 | dAtA = make([]byte, size) 262 | n, err := m.MarshalTo(dAtA) 263 | if err != nil { 264 | return nil, err 265 | } 266 | return dAtA[:n], nil 267 | } 268 | 269 | func (m *GetCCurrenciesResponse_CCurrency) MarshalTo(dAtA []byte) (int, error) { 270 | var i int 271 | _ = i 272 | var l int 273 | _ = l 274 | if len(m.Id) > 0 { 275 | dAtA[i] = 0xa 276 | i++ 277 | i = encodeVarintApi(dAtA, i, uint64(len(m.Id))) 278 | i += copy(dAtA[i:], m.Id) 279 | } 280 | if len(m.Name) > 0 { 281 | dAtA[i] = 0x12 282 | i++ 283 | i = encodeVarintApi(dAtA, i, uint64(len(m.Name))) 284 | i += copy(dAtA[i:], m.Name) 285 | } 286 | if len(m.Symbol) > 0 { 287 | dAtA[i] = 0x1a 288 | i++ 289 | i = encodeVarintApi(dAtA, i, uint64(len(m.Symbol))) 290 | i += copy(dAtA[i:], m.Symbol) 291 | } 292 | if m.Rank != 0 { 293 | dAtA[i] = 0x20 294 | i++ 295 | i = encodeVarintApi(dAtA, i, uint64(m.Rank)) 296 | } 297 | if m.PriceUSD != 0 { 298 | dAtA[i] = 0x29 299 | i++ 300 | i = encodeFixed64Api(dAtA, i, uint64(math.Float64bits(float64(m.PriceUSD)))) 301 | } 302 | if m.DailyChange != 0 { 303 | dAtA[i] = 0x31 304 | i++ 305 | i = encodeFixed64Api(dAtA, i, uint64(math.Float64bits(float64(m.DailyChange)))) 306 | } 307 | return i, nil 308 | } 309 | 310 | func encodeFixed64Api(dAtA []byte, offset int, v uint64) int { 311 | dAtA[offset] = uint8(v) 312 | dAtA[offset+1] = uint8(v >> 8) 313 | dAtA[offset+2] = uint8(v >> 16) 314 | dAtA[offset+3] = uint8(v >> 24) 315 | dAtA[offset+4] = uint8(v >> 32) 316 | dAtA[offset+5] = uint8(v >> 40) 317 | dAtA[offset+6] = uint8(v >> 48) 318 | dAtA[offset+7] = uint8(v >> 56) 319 | return offset + 8 320 | } 321 | func encodeFixed32Api(dAtA []byte, offset int, v uint32) int { 322 | dAtA[offset] = uint8(v) 323 | dAtA[offset+1] = uint8(v >> 8) 324 | dAtA[offset+2] = uint8(v >> 16) 325 | dAtA[offset+3] = uint8(v >> 24) 326 | return offset + 4 327 | } 328 | func encodeVarintApi(dAtA []byte, offset int, v uint64) int { 329 | for v >= 1<<7 { 330 | dAtA[offset] = uint8(v&0x7f | 0x80) 331 | v >>= 7 332 | offset++ 333 | } 334 | dAtA[offset] = uint8(v) 335 | return offset + 1 336 | } 337 | func (m *GetCCurrenciesRequest) Size() (n int) { 338 | var l int 339 | _ = l 340 | if m.Limit != 0 { 341 | n += 1 + sovApi(uint64(m.Limit)) 342 | } 343 | return n 344 | } 345 | 346 | func (m *GetCCurrenciesResponse) Size() (n int) { 347 | var l int 348 | _ = l 349 | if len(m.Currencies) > 0 { 350 | for _, e := range m.Currencies { 351 | l = e.Size() 352 | n += 1 + l + sovApi(uint64(l)) 353 | } 354 | } 355 | return n 356 | } 357 | 358 | func (m *GetCCurrenciesResponse_CCurrency) Size() (n int) { 359 | var l int 360 | _ = l 361 | l = len(m.Id) 362 | if l > 0 { 363 | n += 1 + l + sovApi(uint64(l)) 364 | } 365 | l = len(m.Name) 366 | if l > 0 { 367 | n += 1 + l + sovApi(uint64(l)) 368 | } 369 | l = len(m.Symbol) 370 | if l > 0 { 371 | n += 1 + l + sovApi(uint64(l)) 372 | } 373 | if m.Rank != 0 { 374 | n += 1 + sovApi(uint64(m.Rank)) 375 | } 376 | if m.PriceUSD != 0 { 377 | n += 9 378 | } 379 | if m.DailyChange != 0 { 380 | n += 9 381 | } 382 | return n 383 | } 384 | 385 | func sovApi(x uint64) (n int) { 386 | for { 387 | n++ 388 | x >>= 7 389 | if x == 0 { 390 | break 391 | } 392 | } 393 | return n 394 | } 395 | func sozApi(x uint64) (n int) { 396 | return sovApi(uint64((x << 1) ^ uint64((int64(x) >> 63)))) 397 | } 398 | func (m *GetCCurrenciesRequest) Unmarshal(dAtA []byte) error { 399 | l := len(dAtA) 400 | iNdEx := 0 401 | for iNdEx < l { 402 | preIndex := iNdEx 403 | var wire uint64 404 | for shift := uint(0); ; shift += 7 { 405 | if shift >= 64 { 406 | return ErrIntOverflowApi 407 | } 408 | if iNdEx >= l { 409 | return io.ErrUnexpectedEOF 410 | } 411 | b := dAtA[iNdEx] 412 | iNdEx++ 413 | wire |= (uint64(b) & 0x7F) << shift 414 | if b < 0x80 { 415 | break 416 | } 417 | } 418 | fieldNum := int32(wire >> 3) 419 | wireType := int(wire & 0x7) 420 | if wireType == 4 { 421 | return fmt.Errorf("proto: GetCCurrenciesRequest: wiretype end group for non-group") 422 | } 423 | if fieldNum <= 0 { 424 | return fmt.Errorf("proto: GetCCurrenciesRequest: illegal tag %d (wire type %d)", fieldNum, wire) 425 | } 426 | switch fieldNum { 427 | case 1: 428 | if wireType != 0 { 429 | return fmt.Errorf("proto: wrong wireType = %d for field Limit", wireType) 430 | } 431 | m.Limit = 0 432 | for shift := uint(0); ; shift += 7 { 433 | if shift >= 64 { 434 | return ErrIntOverflowApi 435 | } 436 | if iNdEx >= l { 437 | return io.ErrUnexpectedEOF 438 | } 439 | b := dAtA[iNdEx] 440 | iNdEx++ 441 | m.Limit |= (uint32(b) & 0x7F) << shift 442 | if b < 0x80 { 443 | break 444 | } 445 | } 446 | default: 447 | iNdEx = preIndex 448 | skippy, err := skipApi(dAtA[iNdEx:]) 449 | if err != nil { 450 | return err 451 | } 452 | if skippy < 0 { 453 | return ErrInvalidLengthApi 454 | } 455 | if (iNdEx + skippy) > l { 456 | return io.ErrUnexpectedEOF 457 | } 458 | iNdEx += skippy 459 | } 460 | } 461 | 462 | if iNdEx > l { 463 | return io.ErrUnexpectedEOF 464 | } 465 | return nil 466 | } 467 | func (m *GetCCurrenciesResponse) Unmarshal(dAtA []byte) error { 468 | l := len(dAtA) 469 | iNdEx := 0 470 | for iNdEx < l { 471 | preIndex := iNdEx 472 | var wire uint64 473 | for shift := uint(0); ; shift += 7 { 474 | if shift >= 64 { 475 | return ErrIntOverflowApi 476 | } 477 | if iNdEx >= l { 478 | return io.ErrUnexpectedEOF 479 | } 480 | b := dAtA[iNdEx] 481 | iNdEx++ 482 | wire |= (uint64(b) & 0x7F) << shift 483 | if b < 0x80 { 484 | break 485 | } 486 | } 487 | fieldNum := int32(wire >> 3) 488 | wireType := int(wire & 0x7) 489 | if wireType == 4 { 490 | return fmt.Errorf("proto: GetCCurrenciesResponse: wiretype end group for non-group") 491 | } 492 | if fieldNum <= 0 { 493 | return fmt.Errorf("proto: GetCCurrenciesResponse: illegal tag %d (wire type %d)", fieldNum, wire) 494 | } 495 | switch fieldNum { 496 | case 1: 497 | if wireType != 2 { 498 | return fmt.Errorf("proto: wrong wireType = %d for field Currencies", wireType) 499 | } 500 | var msglen int 501 | for shift := uint(0); ; shift += 7 { 502 | if shift >= 64 { 503 | return ErrIntOverflowApi 504 | } 505 | if iNdEx >= l { 506 | return io.ErrUnexpectedEOF 507 | } 508 | b := dAtA[iNdEx] 509 | iNdEx++ 510 | msglen |= (int(b) & 0x7F) << shift 511 | if b < 0x80 { 512 | break 513 | } 514 | } 515 | if msglen < 0 { 516 | return ErrInvalidLengthApi 517 | } 518 | postIndex := iNdEx + msglen 519 | if postIndex > l { 520 | return io.ErrUnexpectedEOF 521 | } 522 | m.Currencies = append(m.Currencies, &GetCCurrenciesResponse_CCurrency{}) 523 | if err := m.Currencies[len(m.Currencies)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { 524 | return err 525 | } 526 | iNdEx = postIndex 527 | default: 528 | iNdEx = preIndex 529 | skippy, err := skipApi(dAtA[iNdEx:]) 530 | if err != nil { 531 | return err 532 | } 533 | if skippy < 0 { 534 | return ErrInvalidLengthApi 535 | } 536 | if (iNdEx + skippy) > l { 537 | return io.ErrUnexpectedEOF 538 | } 539 | iNdEx += skippy 540 | } 541 | } 542 | 543 | if iNdEx > l { 544 | return io.ErrUnexpectedEOF 545 | } 546 | return nil 547 | } 548 | func (m *GetCCurrenciesResponse_CCurrency) Unmarshal(dAtA []byte) error { 549 | l := len(dAtA) 550 | iNdEx := 0 551 | for iNdEx < l { 552 | preIndex := iNdEx 553 | var wire uint64 554 | for shift := uint(0); ; shift += 7 { 555 | if shift >= 64 { 556 | return ErrIntOverflowApi 557 | } 558 | if iNdEx >= l { 559 | return io.ErrUnexpectedEOF 560 | } 561 | b := dAtA[iNdEx] 562 | iNdEx++ 563 | wire |= (uint64(b) & 0x7F) << shift 564 | if b < 0x80 { 565 | break 566 | } 567 | } 568 | fieldNum := int32(wire >> 3) 569 | wireType := int(wire & 0x7) 570 | if wireType == 4 { 571 | return fmt.Errorf("proto: CCurrency: wiretype end group for non-group") 572 | } 573 | if fieldNum <= 0 { 574 | return fmt.Errorf("proto: CCurrency: illegal tag %d (wire type %d)", fieldNum, wire) 575 | } 576 | switch fieldNum { 577 | case 1: 578 | if wireType != 2 { 579 | return fmt.Errorf("proto: wrong wireType = %d for field Id", wireType) 580 | } 581 | var stringLen uint64 582 | for shift := uint(0); ; shift += 7 { 583 | if shift >= 64 { 584 | return ErrIntOverflowApi 585 | } 586 | if iNdEx >= l { 587 | return io.ErrUnexpectedEOF 588 | } 589 | b := dAtA[iNdEx] 590 | iNdEx++ 591 | stringLen |= (uint64(b) & 0x7F) << shift 592 | if b < 0x80 { 593 | break 594 | } 595 | } 596 | intStringLen := int(stringLen) 597 | if intStringLen < 0 { 598 | return ErrInvalidLengthApi 599 | } 600 | postIndex := iNdEx + intStringLen 601 | if postIndex > l { 602 | return io.ErrUnexpectedEOF 603 | } 604 | m.Id = string(dAtA[iNdEx:postIndex]) 605 | iNdEx = postIndex 606 | case 2: 607 | if wireType != 2 { 608 | return fmt.Errorf("proto: wrong wireType = %d for field Name", wireType) 609 | } 610 | var stringLen uint64 611 | for shift := uint(0); ; shift += 7 { 612 | if shift >= 64 { 613 | return ErrIntOverflowApi 614 | } 615 | if iNdEx >= l { 616 | return io.ErrUnexpectedEOF 617 | } 618 | b := dAtA[iNdEx] 619 | iNdEx++ 620 | stringLen |= (uint64(b) & 0x7F) << shift 621 | if b < 0x80 { 622 | break 623 | } 624 | } 625 | intStringLen := int(stringLen) 626 | if intStringLen < 0 { 627 | return ErrInvalidLengthApi 628 | } 629 | postIndex := iNdEx + intStringLen 630 | if postIndex > l { 631 | return io.ErrUnexpectedEOF 632 | } 633 | m.Name = string(dAtA[iNdEx:postIndex]) 634 | iNdEx = postIndex 635 | case 3: 636 | if wireType != 2 { 637 | return fmt.Errorf("proto: wrong wireType = %d for field Symbol", wireType) 638 | } 639 | var stringLen uint64 640 | for shift := uint(0); ; shift += 7 { 641 | if shift >= 64 { 642 | return ErrIntOverflowApi 643 | } 644 | if iNdEx >= l { 645 | return io.ErrUnexpectedEOF 646 | } 647 | b := dAtA[iNdEx] 648 | iNdEx++ 649 | stringLen |= (uint64(b) & 0x7F) << shift 650 | if b < 0x80 { 651 | break 652 | } 653 | } 654 | intStringLen := int(stringLen) 655 | if intStringLen < 0 { 656 | return ErrInvalidLengthApi 657 | } 658 | postIndex := iNdEx + intStringLen 659 | if postIndex > l { 660 | return io.ErrUnexpectedEOF 661 | } 662 | m.Symbol = string(dAtA[iNdEx:postIndex]) 663 | iNdEx = postIndex 664 | case 4: 665 | if wireType != 0 { 666 | return fmt.Errorf("proto: wrong wireType = %d for field Rank", wireType) 667 | } 668 | m.Rank = 0 669 | for shift := uint(0); ; shift += 7 { 670 | if shift >= 64 { 671 | return ErrIntOverflowApi 672 | } 673 | if iNdEx >= l { 674 | return io.ErrUnexpectedEOF 675 | } 676 | b := dAtA[iNdEx] 677 | iNdEx++ 678 | m.Rank |= (uint32(b) & 0x7F) << shift 679 | if b < 0x80 { 680 | break 681 | } 682 | } 683 | case 5: 684 | if wireType != 1 { 685 | return fmt.Errorf("proto: wrong wireType = %d for field PriceUSD", wireType) 686 | } 687 | var v uint64 688 | if (iNdEx + 8) > l { 689 | return io.ErrUnexpectedEOF 690 | } 691 | iNdEx += 8 692 | v = uint64(dAtA[iNdEx-8]) 693 | v |= uint64(dAtA[iNdEx-7]) << 8 694 | v |= uint64(dAtA[iNdEx-6]) << 16 695 | v |= uint64(dAtA[iNdEx-5]) << 24 696 | v |= uint64(dAtA[iNdEx-4]) << 32 697 | v |= uint64(dAtA[iNdEx-3]) << 40 698 | v |= uint64(dAtA[iNdEx-2]) << 48 699 | v |= uint64(dAtA[iNdEx-1]) << 56 700 | m.PriceUSD = float64(math.Float64frombits(v)) 701 | case 6: 702 | if wireType != 1 { 703 | return fmt.Errorf("proto: wrong wireType = %d for field DailyChange", wireType) 704 | } 705 | var v uint64 706 | if (iNdEx + 8) > l { 707 | return io.ErrUnexpectedEOF 708 | } 709 | iNdEx += 8 710 | v = uint64(dAtA[iNdEx-8]) 711 | v |= uint64(dAtA[iNdEx-7]) << 8 712 | v |= uint64(dAtA[iNdEx-6]) << 16 713 | v |= uint64(dAtA[iNdEx-5]) << 24 714 | v |= uint64(dAtA[iNdEx-4]) << 32 715 | v |= uint64(dAtA[iNdEx-3]) << 40 716 | v |= uint64(dAtA[iNdEx-2]) << 48 717 | v |= uint64(dAtA[iNdEx-1]) << 56 718 | m.DailyChange = float64(math.Float64frombits(v)) 719 | default: 720 | iNdEx = preIndex 721 | skippy, err := skipApi(dAtA[iNdEx:]) 722 | if err != nil { 723 | return err 724 | } 725 | if skippy < 0 { 726 | return ErrInvalidLengthApi 727 | } 728 | if (iNdEx + skippy) > l { 729 | return io.ErrUnexpectedEOF 730 | } 731 | iNdEx += skippy 732 | } 733 | } 734 | 735 | if iNdEx > l { 736 | return io.ErrUnexpectedEOF 737 | } 738 | return nil 739 | } 740 | func skipApi(dAtA []byte) (n int, err error) { 741 | l := len(dAtA) 742 | iNdEx := 0 743 | for iNdEx < l { 744 | var wire uint64 745 | for shift := uint(0); ; shift += 7 { 746 | if shift >= 64 { 747 | return 0, ErrIntOverflowApi 748 | } 749 | if iNdEx >= l { 750 | return 0, io.ErrUnexpectedEOF 751 | } 752 | b := dAtA[iNdEx] 753 | iNdEx++ 754 | wire |= (uint64(b) & 0x7F) << shift 755 | if b < 0x80 { 756 | break 757 | } 758 | } 759 | wireType := int(wire & 0x7) 760 | switch wireType { 761 | case 0: 762 | for shift := uint(0); ; shift += 7 { 763 | if shift >= 64 { 764 | return 0, ErrIntOverflowApi 765 | } 766 | if iNdEx >= l { 767 | return 0, io.ErrUnexpectedEOF 768 | } 769 | iNdEx++ 770 | if dAtA[iNdEx-1] < 0x80 { 771 | break 772 | } 773 | } 774 | return iNdEx, nil 775 | case 1: 776 | iNdEx += 8 777 | return iNdEx, nil 778 | case 2: 779 | var length int 780 | for shift := uint(0); ; shift += 7 { 781 | if shift >= 64 { 782 | return 0, ErrIntOverflowApi 783 | } 784 | if iNdEx >= l { 785 | return 0, io.ErrUnexpectedEOF 786 | } 787 | b := dAtA[iNdEx] 788 | iNdEx++ 789 | length |= (int(b) & 0x7F) << shift 790 | if b < 0x80 { 791 | break 792 | } 793 | } 794 | iNdEx += length 795 | if length < 0 { 796 | return 0, ErrInvalidLengthApi 797 | } 798 | return iNdEx, nil 799 | case 3: 800 | for { 801 | var innerWire uint64 802 | var start int = iNdEx 803 | for shift := uint(0); ; shift += 7 { 804 | if shift >= 64 { 805 | return 0, ErrIntOverflowApi 806 | } 807 | if iNdEx >= l { 808 | return 0, io.ErrUnexpectedEOF 809 | } 810 | b := dAtA[iNdEx] 811 | iNdEx++ 812 | innerWire |= (uint64(b) & 0x7F) << shift 813 | if b < 0x80 { 814 | break 815 | } 816 | } 817 | innerWireType := int(innerWire & 0x7) 818 | if innerWireType == 4 { 819 | break 820 | } 821 | next, err := skipApi(dAtA[start:]) 822 | if err != nil { 823 | return 0, err 824 | } 825 | iNdEx = start + next 826 | } 827 | return iNdEx, nil 828 | case 4: 829 | return iNdEx, nil 830 | case 5: 831 | iNdEx += 4 832 | return iNdEx, nil 833 | default: 834 | return 0, fmt.Errorf("proto: illegal wireType %d", wireType) 835 | } 836 | } 837 | panic("unreachable") 838 | } 839 | 840 | var ( 841 | ErrInvalidLengthApi = fmt.Errorf("proto: negative length found during unmarshaling") 842 | ErrIntOverflowApi = fmt.Errorf("proto: integer overflow") 843 | ) 844 | 845 | func init() { proto.RegisterFile("api.proto", fileDescriptorApi) } 846 | 847 | var fileDescriptorApi = []byte{ 848 | // 276 bytes of a gzipped FileDescriptorProto 849 | 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xe2, 0x4c, 0x2c, 0xc8, 0xd4, 850 | 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x62, 0x4e, 0x2c, 0xc8, 0x54, 0xd2, 0xe5, 0x12, 0x75, 0x4f, 851 | 0x2d, 0x71, 0x76, 0x2e, 0x2d, 0x2a, 0x4a, 0xcd, 0x4b, 0xce, 0x4c, 0x2d, 0x0e, 0x4a, 0x2d, 0x2c, 852 | 0x4d, 0x2d, 0x2e, 0x11, 0x12, 0xe1, 0x62, 0xcd, 0xc9, 0xcc, 0xcd, 0x2c, 0x91, 0x60, 0x54, 0x60, 853 | 0xd4, 0xe0, 0x0d, 0x82, 0x70, 0x94, 0x7e, 0x33, 0x72, 0x89, 0xa1, 0xab, 0x2f, 0x2e, 0xc8, 0xcf, 854 | 0x2b, 0x4e, 0x15, 0x72, 0xe5, 0xe2, 0x4a, 0x86, 0x8b, 0x4a, 0x30, 0x2a, 0x30, 0x6b, 0x70, 0x1b, 855 | 0xa9, 0xea, 0x81, 0xac, 0xc3, 0xae, 0x41, 0x0f, 0x26, 0x56, 0x19, 0x84, 0xa4, 0x51, 0x6a, 0x26, 856 | 0x23, 0x17, 0x27, 0x5c, 0x46, 0x88, 0x8f, 0x8b, 0x29, 0x33, 0x05, 0xec, 0x04, 0xce, 0x20, 0xa6, 857 | 0xcc, 0x14, 0x21, 0x21, 0x2e, 0x96, 0xbc, 0xc4, 0xdc, 0x54, 0x09, 0x26, 0xb0, 0x08, 0x98, 0x2d, 858 | 0x24, 0xc6, 0xc5, 0x56, 0x5c, 0x99, 0x9b, 0x94, 0x9f, 0x23, 0xc1, 0x0c, 0x16, 0x85, 0xf2, 0x40, 859 | 0x6a, 0x8b, 0x12, 0xf3, 0xb2, 0x25, 0x58, 0xc0, 0x1e, 0x00, 0xb3, 0x85, 0xa4, 0xb8, 0x38, 0x0a, 860 | 0x8a, 0x32, 0x93, 0x53, 0x43, 0x83, 0x5d, 0x24, 0x58, 0x15, 0x18, 0x35, 0x18, 0x83, 0xe0, 0x7c, 861 | 0x21, 0x05, 0x2e, 0xee, 0x94, 0xc4, 0xcc, 0x9c, 0x4a, 0xe7, 0x8c, 0xc4, 0xbc, 0xf4, 0x54, 0x09, 862 | 0x36, 0xb0, 0x34, 0xb2, 0x90, 0x51, 0x00, 0x17, 0xb3, 0x63, 0x41, 0xa6, 0x90, 0x27, 0x17, 0x1f, 863 | 0xaa, 0x97, 0x84, 0xa4, 0xb0, 0xfa, 0x13, 0x1c, 0x90, 0x52, 0xd2, 0x78, 0xc2, 0xc0, 0x49, 0xe0, 864 | 0xc4, 0x23, 0x39, 0xc6, 0x0b, 0x8f, 0xe4, 0x18, 0x1f, 0x3c, 0x92, 0x63, 0x9c, 0xf1, 0x58, 0x8e, 865 | 0x21, 0x89, 0x0d, 0x1c, 0x39, 0xc6, 0x80, 0x00, 0x00, 0x00, 0xff, 0xff, 0x7d, 0x67, 0x1c, 0x23, 866 | 0xa9, 0x01, 0x00, 0x00, 867 | } 868 | -------------------------------------------------------------------------------- /api/api.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package api; 4 | 5 | service Api { 6 | rpc GetCCurrencies (GetCCurrenciesRequest) returns (GetCCurrenciesResponse); 7 | } 8 | 9 | message GetCCurrenciesRequest { 10 | uint32 limit = 1; 11 | } 12 | 13 | message GetCCurrenciesResponse { 14 | message CCurrency { 15 | string id = 1; 16 | string name = 2; 17 | string symbol = 3; 18 | uint32 rank = 4; 19 | double priceUSD = 5; // just to keep it simple 20 | double dailyChange = 6; 21 | } 22 | repeated CCurrency currencies = 1; 23 | } -------------------------------------------------------------------------------- /cmd/grpc-example/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.5 2 | MAINTAINER Yuri Buerov 3 | 4 | RUN apk -U add ca-certificates 5 | 6 | COPY grpc-example / 7 | 8 | ENTRYPOINT ["/grpc-example"] 9 | CMD [""] 10 | -------------------------------------------------------------------------------- /cmd/grpc-example/README.md: -------------------------------------------------------------------------------- 1 | ```bash 2 | docker build -t grpc-example -f Dockerfile . 3 | docker run -it --rm --read-only \ 4 | --volume $(pwd)/docker-tmp:/tmp \ 5 | --publish 50051:50051 \ 6 | grpc-example 7 | ``` -------------------------------------------------------------------------------- /cmd/grpc-example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net" 5 | "os" 6 | 7 | "github.com/YuriBuerov/grpc-example/pkg/server" 8 | "github.com/go-kit/kit/log" 9 | "golang.org/x/net/context" 10 | "google.golang.org/grpc" 11 | "os/signal" 12 | "syscall" 13 | ) 14 | 15 | // We don't have config in this example, so port defined as constant 16 | const ( 17 | port = ":50051" 18 | ) 19 | 20 | func main() { 21 | // Initialize logger 22 | logger := log.NewJSONLogger(log.NewSyncWriter(os.Stdout)) 23 | logger = log.With(logger, "ts", log.DefaultTimestampUTC, "caller", log.DefaultCaller, "version", "1.0") 24 | 25 | // Recover from panic 26 | defer func() { 27 | if err := recover(); err != nil { 28 | logger.Log("error", "recover from panic", "cause", err) 29 | os.Exit(1) 30 | } 31 | }() 32 | 33 | // Initialize GRPC server 34 | ctx, cancel := context.WithCancel(context.Background()) 35 | defer cancel() 36 | grpcServer, err := server.NewGRPCServer(logger) 37 | if err != nil { 38 | logger.Log("error", "failed to init grpc server", "cause", err) 39 | os.Exit(1) 40 | } 41 | defer grpcServer.GracefulStop() 42 | catchStopSignal(ctx.Done(), cancel, grpcServer) 43 | 44 | lis, err := net.Listen("tcp", port) 45 | if err != nil { 46 | logger.Log("error", "failed to listen", "cause", err) 47 | os.Exit(1) 48 | } 49 | 50 | logger.Log("event", "GRPC server started", "addr", port) 51 | if err := grpcServer.Serve(lis); err != nil { 52 | logger.Log("error", "GRPC server", "cause", err) 53 | os.Exit(1) 54 | } 55 | } 56 | 57 | func catchStopSignal(done <-chan struct{}, cancel context.CancelFunc, server *grpc.Server) { 58 | var stop = make(chan os.Signal, 1) 59 | signal.Notify(stop, syscall.SIGTERM) 60 | 61 | go func() { 62 | select { 63 | case <-stop: 64 | cancel() 65 | case <-done: 66 | } 67 | server.GracefulStop() 68 | }() 69 | } 70 | -------------------------------------------------------------------------------- /cmd/testclient/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "log" 6 | "os" 7 | "fmt" 8 | 9 | "github.com/YuriBuerov/grpc-example/api" 10 | "golang.org/x/net/context" 11 | "google.golang.org/grpc" 12 | ) 13 | 14 | // Minimalistic example of GRPC client 15 | func main() { 16 | port := flag.String("port", "50051", "server port") 17 | limit := flag.Int("limit", 10, "limit") 18 | flag.Parse() 19 | if port == nil { 20 | log.Fatal("port is required") 21 | os.Exit(3) 22 | } 23 | 24 | conn, err := grpc.Dial(fmt.Sprintf("localhost:%s", *port), grpc.WithInsecure()) 25 | if err != nil { 26 | log.Fatal("failed to estabilish grpc connection") 27 | os.Exit(1) 28 | } 29 | 30 | client := api.NewApiClient(conn) 31 | defer conn.Close() 32 | 33 | req := &api.GetCCurrenciesRequest{Limit: uint32(*limit)} 34 | resp, err := client.GetCCurrencies(context.Background(), req) 35 | if err != nil { 36 | log.Fatal(err) 37 | os.Exit(1) 38 | } 39 | 40 | log.Printf("Response---> %v\n", resp.Currencies) 41 | } 42 | -------------------------------------------------------------------------------- /config/grpc-examle-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: grpc-example 5 | spec: 6 | selector: 7 | grpc-example/app: grpc 8 | grpc-example/tier: api 9 | ports: 10 | - port: 50051 11 | targetPort: 50051 12 | name: grpc -------------------------------------------------------------------------------- /config/grpc-example-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1beta1 2 | kind: Deployment 3 | metadata: 4 | name: grpc-example 5 | spec: 6 | replicas: 2 7 | template: 8 | metadata: 9 | labels: 10 | grpc-example/app: grpc 11 | grpc-example/tier: api 12 | spec: 13 | containers: 14 | - name: api 15 | image: yuribuerov/grpc-example:v1.0 16 | ports: 17 | - containerPort: 50051 18 | name: grpc 19 | imagePullSecrets: 20 | - name: grpc-example 21 | -------------------------------------------------------------------------------- /pkg/coinmarketcap/ticker.go: -------------------------------------------------------------------------------- 1 | package coinmarketcap 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | 8 | "github.com/YuriBuerov/grpc-example/api" 9 | "github.com/YuriBuerov/grpc-example/pkg/types" 10 | "github.com/go-kit/kit/log" 11 | "golang.org/x/net/context" 12 | "google.golang.org/grpc/codes" 13 | "google.golang.org/grpc/status" 14 | ) 15 | 16 | const baseURL = "https://api.coinmarketcap.com/v1/ticker/" 17 | 18 | // CTicker handler 19 | type CTicker struct { 20 | logger log.Logger 21 | client *http.Client 22 | baseURL string 23 | } 24 | 25 | // NewCTicker handler initializer 26 | func NewCTicker(logger log.Logger, c *http.Client) *CTicker { 27 | return &CTicker{ 28 | logger: logger, 29 | client: c, 30 | baseURL: baseURL, 31 | } 32 | } 33 | 34 | // GetCCurrencies handle func, which CTicker handler have to implement. (take a look on api.proto and api.pb.go) 35 | func (t *CTicker) GetCCurrencies(ctx context.Context, in *api.GetCCurrenciesRequest) (*api.GetCCurrenciesResponse, error) { 36 | // Just simple functionality to show GPRC handler example 37 | var resp api.GetCCurrenciesResponse 38 | 39 | url := fmt.Sprintf("%s?limit=%d", t.baseURL, in.Limit) 40 | r, err := t.client.Get(url) 41 | if err != nil { 42 | t.logger.Log("error", err) 43 | return nil, status.Errorf(codes.Internal, "coinmarketcap api call error: %s", err.Error()) 44 | } 45 | defer r.Body.Close() 46 | 47 | var cCurrencies types.CCurrencies 48 | if err := json.NewDecoder(r.Body).Decode(&cCurrencies); err != nil { 49 | t.logger.Log("error", err) 50 | return nil, status.Errorf(codes.Internal, "coinmarketcap api decode resp error: %s", err.Error()) 51 | } 52 | 53 | resp.Currencies = make([]*api.GetCCurrenciesResponse_CCurrency, len(cCurrencies)) 54 | for i, c := range cCurrencies { 55 | resp.Currencies[i] = types.ToProtoCCurrency(&c) 56 | } 57 | 58 | return &resp, nil 59 | } 60 | -------------------------------------------------------------------------------- /pkg/coinmarketcap/ticker_test.go: -------------------------------------------------------------------------------- 1 | package coinmarketcap_test 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "os" 7 | "testing" 8 | 9 | "github.com/YuriBuerov/grpc-example/api" 10 | "github.com/YuriBuerov/grpc-example/pkg/coinmarketcap" 11 | "github.com/go-kit/kit/log" 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | func TestGetCCurrencies(t *testing.T) { 16 | cTicker := coinmarketcap.NewCTicker(log.NewLogfmtLogger(os.Stdout), http.DefaultClient) 17 | 18 | res, err := cTicker.GetCCurrencies(context.Background(), &api.GetCCurrenciesRequest{Limit: 3}) 19 | 20 | check := func(currencies []*api.GetCCurrenciesResponse_CCurrency) bool { 21 | for _, c := range currencies { 22 | if c.Symbol == "BTC" { 23 | return true 24 | } 25 | } 26 | return false 27 | } 28 | 29 | require.NoError(t, err) 30 | require.True(t, check(res.Currencies)) 31 | } 32 | -------------------------------------------------------------------------------- /pkg/server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "net/http" 5 | "runtime/debug" 6 | 7 | "github.com/YuriBuerov/grpc-example/api" 8 | "github.com/YuriBuerov/grpc-example/pkg/coinmarketcap" 9 | "github.com/go-kit/kit/log" 10 | "github.com/grpc-ecosystem/go-grpc-middleware" 11 | "github.com/grpc-ecosystem/go-grpc-middleware/recovery" 12 | "google.golang.org/grpc" 13 | "google.golang.org/grpc/codes" 14 | "google.golang.org/grpc/status" 15 | ) 16 | 17 | // Here all handlers have to be stored 18 | // Each handler have to implement related handle func, take a look on api.proto and api.pb.go 19 | type server struct { 20 | *coinmarketcap.CTicker 21 | } 22 | 23 | // NewGRPCServer GRPC server initializer 24 | func NewGRPCServer(logger log.Logger) (*grpc.Server, error) { 25 | // Use middleware to handle recover 26 | opts := []grpc_recovery.Option{ 27 | grpc_recovery.WithRecoveryHandler(func(p interface{}) error { 28 | logger.Log("error", "recovering from panic", "cause", p, "trace", string(debug.Stack())) 29 | return status.Errorf(codes.Internal, "%s", p) 30 | }), 31 | } 32 | 33 | // Initialize handler 34 | cTicker := coinmarketcap.NewCTicker(logger, http.DefaultClient) 35 | 36 | // Initialize GRPC server and pass middleware opts 37 | s := grpc.NewServer( 38 | grpc_middleware.WithUnaryServerChain( 39 | grpc_recovery.UnaryServerInterceptor(opts...), 40 | ), 41 | ) 42 | 43 | api.RegisterApiServer(s, &server{ 44 | CTicker: cTicker, 45 | }) 46 | 47 | return s, nil 48 | } 49 | -------------------------------------------------------------------------------- /pkg/types/c_currency.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "github.com/YuriBuerov/grpc-example/api" 5 | ) 6 | 7 | // CCurrency object represent crypto currency from coinmarketcap API response 8 | type CCurrency struct { 9 | ID string `json:"id"` 10 | Name string `json:"name"` 11 | Symbol string `json:"symbol"` 12 | Rank int `json:"rank,string"` 13 | PriceUSD float64 `json:"price_usd,string"` 14 | PriceBTC float64 `json:"price_btc,string"` 15 | PercentChange24H float64 `json:"percent_change_24h,string"` 16 | } 17 | 18 | // CCurrencies set of CCurrency objects 19 | type CCurrencies []CCurrency 20 | 21 | // ToProtoCCurrency convert CCurrency to proto currency entry, take a look on api.proto 22 | func ToProtoCCurrency(c *CCurrency) *api.GetCCurrenciesResponse_CCurrency { 23 | return &api.GetCCurrenciesResponse_CCurrency{ 24 | Id: c.ID, 25 | Name: c.Name, 26 | Symbol: c.Symbol, 27 | Rank: uint32(c.Rank), 28 | PriceUSD: c.PriceUSD, 29 | DailyChange: c.PercentChange24H, 30 | } 31 | } 32 | --------------------------------------------------------------------------------