diff --git a/restlike/generateProto.sh b/restlike/generateProto.sh new file mode 100755 index 0000000000000000000000000000000000000000..67b6d293f6f4e6a68eff4162a42acb242129cd18 --- /dev/null +++ b/restlike/generateProto.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +protoc --go_out=paths=source_relative:. restlike/restLikeMessages.proto diff --git a/restlike/message.go b/restlike/message.go deleted file mode 100644 index cabcbe6bd28c117c93cca220f9edb195e1810934..0000000000000000000000000000000000000000 --- a/restlike/message.go +++ /dev/null @@ -1,52 +0,0 @@ -//////////////////////////////////////////////////////////////////////////////// -// Copyright © 2022 Privategrity Corporation / -// / -// All rights reserved. / -//////////////////////////////////////////////////////////////////////////////// - -package restlike - -import ( - "github.com/pkg/errors" -) - -// Message are used for sending to and receiving from a RestServer -type Message interface { - Content() Data - Headers() Param - Method() Method - URI() URI - Error() error -} - -// message implements the Message interface using JSON -type message struct { - content Data - headers Param - method Method - uri URI - err string -} - -func (m message) Content() Data { - return m.content -} - -func (m message) Headers() Param { - return m.headers -} - -func (m message) Method() Method { - return m.method -} - -func (m message) URI() URI { - return m.uri -} - -func (m message) Error() error { - if len(m.err) == 0 { - return nil - } - return errors.New(m.err) -} diff --git a/restlike/receiver.go b/restlike/receiver.go index a3a80ef86b30549e4046a305a033acb86677f512..a32de80e5a772846f23a1420ec0e3e88eb4d3139 100644 --- a/restlike/receiver.go +++ b/restlike/receiver.go @@ -7,32 +7,46 @@ package restlike import ( - "encoding/json" jww "github.com/spf13/jwalterweatherman" + "gitlab.com/elixxir/client/cmix" "gitlab.com/elixxir/client/cmix/identity/receptionID" "gitlab.com/elixxir/client/cmix/rounds" "gitlab.com/elixxir/client/single" + "google.golang.org/protobuf/proto" + "time" ) // processor is the reception handler for a RestServer type singleReceiver struct { - endpoints Endpoints + endpoints *Endpoints } // Callback is the handler for single-use message reception for a RestServer func (s *singleReceiver) Callback(req *single.Request, receptionId receptionID.EphemeralIdentity, rounds []rounds.Round) { // Unmarshal the payload - newMessage := &message{} - err := json.Unmarshal(req.GetPayload(), newMessage) + newMessage := &Message{} + err := proto.Unmarshal(req.GetPayload(), newMessage) if err != nil { jww.ERROR.Printf("Unable to unmarshal restlike message: %+v", err) return } // Send the payload to the proper Callback - if cb, err := s.endpoints.Get(newMessage.URI(), newMessage.Method()); err == nil { + if cb, err := s.endpoints.Get(URI(newMessage.GetUri()), Method(newMessage.GetMethod())); err == nil { cb(newMessage) } else { - jww.ERROR.Printf("Unable to call restlike endpoint: %+v", err) + // If no callback, send an error response + responseMessage := &Message{Error: err.Error()} + payload, err := proto.Marshal(responseMessage) + if err != nil { + jww.ERROR.Printf("Unable to marshal restlike response message: %+v", err) + return + } + // Send the response + // TODO: Parameterize params and timeout + _, err = req.Respond(payload, cmix.GetDefaultCMIXParams(), 30*time.Second) + if err != nil { + jww.ERROR.Printf("Unable to send restlike response message: %+v", err) + } } } diff --git a/restlike/request.go b/restlike/request.go index 211348375cd6f229f7575c6f59b910250f314cb7..8991b003b8f2154308aeeebb0d4ea30a08aa4e9c 100644 --- a/restlike/request.go +++ b/restlike/request.go @@ -7,59 +7,48 @@ package restlike import ( - "encoding/json" jww "github.com/spf13/jwalterweatherman" "gitlab.com/elixxir/client/catalog" "gitlab.com/elixxir/client/single" "gitlab.com/elixxir/crypto/contact" "gitlab.com/elixxir/crypto/cyclic" "gitlab.com/xx_network/crypto/csprng" + "google.golang.org/protobuf/proto" ) -// Request allows for making REST-like requests to a RestServer -type Request interface { - // Request provides several Method of sending Data to the given URI - // and blocks until the Message is returned - Request(method Method, recipient contact.Contact, path URI, content Data, param Param) (Message, error) - - // AsyncRequest provides several Method of sending Data to the given URI - // and will return the Message to the given Callback when received - AsyncRequest(method Method, recipient contact.Contact, path URI, content Data, param Param, cb Callback) error -} - -// SingleRequest implements the Request interface using single-use messages +// SingleRequest allows for making REST-like requests to a RestServer using single-use messages // Can be used as stateful or declared inline without state type SingleRequest struct { - RequestParam single.RequestParams - Net single.Cmix - Rng csprng.Source - E2eGrp *cyclic.Group + Net single.Cmix + Rng csprng.Source + E2eGrp *cyclic.Group } // Request provides several Method of sending Data to the given URI // and blocks until the Message is returned -func (s *SingleRequest) Request(method Method, recipient contact.Contact, path URI, content Data, param Param) (Message, error) { +func (s *SingleRequest) Request(method Method, recipient contact.Contact, path URI, + content Data, headers *Headers, singleParams single.RequestParams) (*Message, error) { // Build the Message - newMessage := &message{ - content: content, - headers: param, - method: method, - uri: path, + newMessage := &Message{ + Content: content, + Headers: headers, + Method: uint32(method), + Uri: string(path), } - msg, err := json.Marshal(newMessage) + msg, err := proto.Marshal(newMessage) if err != nil { return nil, err } // Build callback for the single-use response - signalChannel := make(chan Message, 1) - cb := func(msg Message) { + signalChannel := make(chan *Message, 1) + cb := func(msg *Message) { signalChannel <- msg } // Transmit the Message _, _, err = single.TransmitRequest(recipient, catalog.RestLike, msg, - &singleResponse{responseCallback: cb}, s.RequestParam, s.Net, s.Rng, s.E2eGrp) + &singleResponse{responseCallback: cb}, singleParams, s.Net, s.Rng, s.E2eGrp) if err != nil { return nil, err } @@ -74,21 +63,22 @@ func (s *SingleRequest) Request(method Method, recipient contact.Contact, path U // AsyncRequest provides several Method of sending Data to the given URI // and will return the Message to the given Callback when received -func (s *SingleRequest) AsyncRequest(method Method, recipient contact.Contact, path URI, content Data, param Param, cb Callback) error { +func (s *SingleRequest) AsyncRequest(method Method, recipient contact.Contact, path URI, + content Data, headers *Headers, cb Callback, singleParams single.RequestParams) error { // Build the Message - newMessage := &message{ - content: content, - headers: param, - method: method, - uri: path, + newMessage := &Message{ + Content: content, + Headers: headers, + Method: uint32(method), + Uri: string(path), } - msg, err := json.Marshal(newMessage) + msg, err := proto.Marshal(newMessage) if err != nil { return err } // Transmit the Message _, _, err = single.TransmitRequest(recipient, catalog.RestLike, msg, - &singleResponse{responseCallback: cb}, s.RequestParam, s.Net, s.Rng, s.E2eGrp) + &singleResponse{responseCallback: cb}, singleParams, s.Net, s.Rng, s.E2eGrp) return err } diff --git a/restlike/response.go b/restlike/response.go index d8f4486c3b60d40bd58111260ed010ece34fa207..a81030ddde6d486af472101ad86c23cfc3950b07 100644 --- a/restlike/response.go +++ b/restlike/response.go @@ -7,10 +7,9 @@ package restlike import ( - "encoding/json" - jww "github.com/spf13/jwalterweatherman" "gitlab.com/elixxir/client/cmix/identity/receptionID" "gitlab.com/elixxir/client/cmix/rounds" + "google.golang.org/protobuf/proto" ) // processor is the response handler for a Request @@ -20,19 +19,19 @@ type singleResponse struct { // Callback is the handler for single-use message responses for a Request func (s *singleResponse) Callback(payload []byte, receptionID receptionID.EphemeralIdentity, rounds []rounds.Round, err error) { - newMessage := &message{} + newMessage := &Message{} // Handle response errors if err != nil { - newMessage.err = err.Error() + newMessage.Error = err.Error() s.responseCallback(newMessage) + return } // Unmarshal the payload - err = json.Unmarshal(payload, newMessage) + err = proto.Unmarshal(payload, newMessage) if err != nil { - jww.ERROR.Printf("Unable to unmarshal restlike message: %+v", err) - return + newMessage.Error = err.Error() } // Send the response payload to the responseCallback diff --git a/restlike/restLikeMessages.pb.go b/restlike/restLikeMessages.pb.go new file mode 100644 index 0000000000000000000000000000000000000000..c6666a5d08dd3c0a086a100a19ca0a74f630af79 --- /dev/null +++ b/restlike/restLikeMessages.pb.go @@ -0,0 +1,270 @@ +/////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +/////////////////////////////////////////////////////////////////////////////// + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.27.1 +// protoc v3.19.1 +// source: restlike/restLikeMessages.proto + +package restlike + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// Message are used for sending to and receiving from a RestServer +type Message struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Content []byte `protobuf:"bytes,1,opt,name=content,proto3" json:"content,omitempty"` + Headers *Headers `protobuf:"bytes,2,opt,name=headers,proto3" json:"headers,omitempty"` + Method uint32 `protobuf:"varint,3,opt,name=method,proto3" json:"method,omitempty"` + Uri string `protobuf:"bytes,4,opt,name=uri,proto3" json:"uri,omitempty"` + Error string `protobuf:"bytes,5,opt,name=error,proto3" json:"error,omitempty"` +} + +func (x *Message) Reset() { + *x = Message{} + if protoimpl.UnsafeEnabled { + mi := &file_restlike_restLikeMessages_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Message) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Message) ProtoMessage() {} + +func (x *Message) ProtoReflect() protoreflect.Message { + mi := &file_restlike_restLikeMessages_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Message.ProtoReflect.Descriptor instead. +func (*Message) Descriptor() ([]byte, []int) { + return file_restlike_restLikeMessages_proto_rawDescGZIP(), []int{0} +} + +func (x *Message) GetContent() []byte { + if x != nil { + return x.Content + } + return nil +} + +func (x *Message) GetHeaders() *Headers { + if x != nil { + return x.Headers + } + return nil +} + +func (x *Message) GetMethod() uint32 { + if x != nil { + return x.Method + } + return 0 +} + +func (x *Message) GetUri() string { + if x != nil { + return x.Uri + } + return "" +} + +func (x *Message) GetError() string { + if x != nil { + return x.Error + } + return "" +} + +// Headers allows different configurations for each Request +// that will be specified in the Request header +type Headers struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Headers allows for custom headers to be included with a Request + Headers []byte `protobuf:"bytes,1,opt,name=headers,proto3" json:"headers,omitempty"` + // Version allows for endpoints to be backwards-compatible + // and handle different formats of the same Request + Version uint32 `protobuf:"varint,2,opt,name=version,proto3" json:"version,omitempty"` +} + +func (x *Headers) Reset() { + *x = Headers{} + if protoimpl.UnsafeEnabled { + mi := &file_restlike_restLikeMessages_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Headers) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Headers) ProtoMessage() {} + +func (x *Headers) ProtoReflect() protoreflect.Message { + mi := &file_restlike_restLikeMessages_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Headers.ProtoReflect.Descriptor instead. +func (*Headers) Descriptor() ([]byte, []int) { + return file_restlike_restLikeMessages_proto_rawDescGZIP(), []int{1} +} + +func (x *Headers) GetHeaders() []byte { + if x != nil { + return x.Headers + } + return nil +} + +func (x *Headers) GetVersion() uint32 { + if x != nil { + return x.Version + } + return 0 +} + +var File_restlike_restLikeMessages_proto protoreflect.FileDescriptor + +var file_restlike_restLikeMessages_proto_rawDesc = []byte{ + 0x0a, 0x1f, 0x72, 0x65, 0x73, 0x74, 0x6c, 0x69, 0x6b, 0x65, 0x2f, 0x72, 0x65, 0x73, 0x74, 0x4c, + 0x69, 0x6b, 0x65, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x12, 0x10, 0x72, 0x65, 0x73, 0x74, 0x4c, 0x69, 0x6b, 0x65, 0x4d, 0x65, 0x73, 0x73, 0x61, + 0x67, 0x65, 0x73, 0x22, 0x98, 0x01, 0x0a, 0x07, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, + 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, + 0x52, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x12, 0x33, 0x0a, 0x07, 0x68, 0x65, 0x61, + 0x64, 0x65, 0x72, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x72, 0x65, 0x73, + 0x74, 0x4c, 0x69, 0x6b, 0x65, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x48, 0x65, + 0x61, 0x64, 0x65, 0x72, 0x73, 0x52, 0x07, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x12, 0x16, + 0x0a, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x06, + 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x69, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x69, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, + 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x3d, + 0x0a, 0x07, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x68, 0x65, 0x61, + 0x64, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x68, 0x65, 0x61, 0x64, + 0x65, 0x72, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x42, 0x24, 0x5a, + 0x22, 0x67, 0x69, 0x74, 0x6c, 0x61, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x65, 0x6c, 0x69, 0x78, + 0x78, 0x69, 0x72, 0x2f, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x2f, 0x72, 0x65, 0x73, 0x74, 0x6c, + 0x69, 0x6b, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_restlike_restLikeMessages_proto_rawDescOnce sync.Once + file_restlike_restLikeMessages_proto_rawDescData = file_restlike_restLikeMessages_proto_rawDesc +) + +func file_restlike_restLikeMessages_proto_rawDescGZIP() []byte { + file_restlike_restLikeMessages_proto_rawDescOnce.Do(func() { + file_restlike_restLikeMessages_proto_rawDescData = protoimpl.X.CompressGZIP(file_restlike_restLikeMessages_proto_rawDescData) + }) + return file_restlike_restLikeMessages_proto_rawDescData +} + +var file_restlike_restLikeMessages_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_restlike_restLikeMessages_proto_goTypes = []interface{}{ + (*Message)(nil), // 0: restLikeMessages.Message + (*Headers)(nil), // 1: restLikeMessages.Headers +} +var file_restlike_restLikeMessages_proto_depIdxs = []int32{ + 1, // 0: restLikeMessages.Message.headers:type_name -> restLikeMessages.Headers + 1, // [1:1] is the sub-list for method output_type + 1, // [1:1] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_restlike_restLikeMessages_proto_init() } +func file_restlike_restLikeMessages_proto_init() { + if File_restlike_restLikeMessages_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_restlike_restLikeMessages_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Message); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_restlike_restLikeMessages_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Headers); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_restlike_restLikeMessages_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_restlike_restLikeMessages_proto_goTypes, + DependencyIndexes: file_restlike_restLikeMessages_proto_depIdxs, + MessageInfos: file_restlike_restLikeMessages_proto_msgTypes, + }.Build() + File_restlike_restLikeMessages_proto = out.File + file_restlike_restLikeMessages_proto_rawDesc = nil + file_restlike_restLikeMessages_proto_goTypes = nil + file_restlike_restLikeMessages_proto_depIdxs = nil +} diff --git a/restlike/restLikeMessages.proto b/restlike/restLikeMessages.proto new file mode 100644 index 0000000000000000000000000000000000000000..00156d5443904383deb89f64c391e0009741dc78 --- /dev/null +++ b/restlike/restLikeMessages.proto @@ -0,0 +1,30 @@ +/////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +/////////////////////////////////////////////////////////////////////////////// + +syntax = "proto3"; +package restLikeMessages; +option go_package = "gitlab.com/elixxir/client/restlike"; + +// Message are used for sending to and receiving from a RestServer +message Message { + bytes content = 1; + Headers headers = 2; + uint32 method = 3; + string uri = 4; + string error = 5; +} + +// Headers allows different configurations for each Request +// that will be specified in the Request header +message Headers { + // Headers allows for custom headers to be included with a Request + bytes headers = 1; + + // Version allows for endpoints to be backwards-compatible + // and handle different formats of the same Request + uint32 version = 2; +} \ No newline at end of file diff --git a/restlike/restServer.go b/restlike/restServer.go index 450c7ebfb635c7d20c06e832461c11afc0e04c29..dccb13ad3da83850bdd98be9b568dfab5c933102 100644 --- a/restlike/restServer.go +++ b/restlike/restServer.go @@ -31,7 +31,7 @@ type RestServer interface { type singleServer struct { receptionId *id.ID listener single.Listener - endpoints Endpoints + endpoints *Endpoints } // NewSingleServer builds a RestServer with single-use and @@ -39,7 +39,7 @@ type singleServer struct { func NewSingleServer(receptionId *id.ID, privKey *cyclic.Int, net single.ListenCmix, e2eGrp *cyclic.Group) RestServer { newServer := &singleServer{ receptionId: receptionId, - endpoints: make(map[URI]map[Method]Callback), + endpoints: &Endpoints{endpoints: make(map[URI]map[Method]Callback)}, } newServer.listener = single.Listen(catalog.RestLike, receptionId, privKey, net, e2eGrp, &singleReceiver{newServer.endpoints}) @@ -61,7 +61,7 @@ func (r *singleServer) UnregisterEndpoint(path URI, method Method) error { // Close the internal RestServer endpoints and external services func (r *singleServer) Close() { // Clear all internal endpoints - r.endpoints = make(map[URI]map[Method]Callback) + r.endpoints = nil // Destroy external services r.listener.Stop() } diff --git a/restlike/types.go b/restlike/types.go index 99c1791b2d595f5c8d7f67f1b3c5ea8055995b6d..199142892e81e98262fbea4500ed37c2a17eea14 100644 --- a/restlike/types.go +++ b/restlike/types.go @@ -6,32 +6,24 @@ package restlike -import "github.com/pkg/errors" +import ( + "github.com/pkg/errors" + "sync" +) // URI defines the destination endpoint of a Request type URI string // Data provides a generic structure for data sent with a Request or received in a Message // NOTE: The way this is encoded is up to the implementation. For example, protobuf or JSON -type Data string +type Data []byte // Method defines the possible Request types -type Method uint8 +type Method uint32 // Callback provides the ability to make asynchronous Request // in order to get the Message later without blocking -type Callback func(Message) - -// Param allows different configurations for each Request -// that will be specified in the Request header -type Param struct { - // Version allows for endpoints to be backwards-compatible - // and handle different formats of the same Request - Version uint - - // Headers allows for custom headers to be included with a Request - Headers Data -} +type Callback func(*Message) const ( // Undefined default value @@ -67,42 +59,54 @@ func (m Method) String() string { } // Endpoints represents a map of internal endpoints for a RestServer -type Endpoints map[URI]map[Method]Callback +type Endpoints struct { + endpoints map[URI]map[Method]Callback + sync.RWMutex +} // Add a new Endpoint // Returns an error if Endpoint already exists -func (e Endpoints) Add(path URI, method Method, cb Callback) error { - if _, ok := e[path]; !ok { - e[path] = make(map[Method]Callback) +func (e *Endpoints) Add(path URI, method Method, cb Callback) error { + e.Lock() + defer e.Unlock() + + if _, ok := e.endpoints[path]; !ok { + e.endpoints[path] = make(map[Method]Callback) } - if _, ok := e[path][method]; ok { + if _, ok := e.endpoints[path][method]; ok { return errors.Errorf("unable to RegisterEndpoint: %s/%s already exists", path, method) } - e[path][method] = cb + e.endpoints[path][method] = cb return nil } // Get an Endpoint // Returns an error if Endpoint does not exist -func (e Endpoints) Get(path URI, method Method) (Callback, error) { - if _, ok := e[path]; !ok { +func (e *Endpoints) Get(path URI, method Method) (Callback, error) { + e.RLock() + defer e.RUnlock() + + if _, ok := e.endpoints[path]; !ok { return nil, errors.Errorf("unable to locate endpoint: %s", path) } - if _, innerOk := e[path][method]; !innerOk { + if _, innerOk := e.endpoints[path][method]; !innerOk { return nil, errors.Errorf("unable to locate endpoint: %s/%s", path, method) } - return e[path][method], nil + return e.endpoints[path][method], nil } // Remove an Endpoint // Returns an error if Endpoint does not exist -func (e Endpoints) Remove(path URI, method Method) error { +func (e *Endpoints) Remove(path URI, method Method) error { + e.Lock() + defer e.Unlock() + if _, err := e.Get(path, method); err != nil { return errors.Errorf("unable to UnregisterEndpoint: %s", err.Error()) } - delete(e[path], method) - if len(e[path]) == 0 { - delete(e, path) + delete(e.endpoints[path], method) + if len(e.endpoints[path]) == 0 { + delete(e.endpoints, path) } return nil }