Skip to content
Snippets Groups Projects
Commit 9cdfdcd9 authored by Jake Taylor's avatar Jake Taylor
Browse files

added restlike package

parent b053970e
No related branches found
No related tags found
3 merge requests!510Release,!214Project/restlike,!207WIP: Client Restructure
......@@ -23,4 +23,6 @@ const (
Group = "group"
EndFT = "endFT"
GroupRq = "groupRq"
RestLike = "restLike"
)
////////////////////////////////////////////////////////////////////////////////
// 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)
}
////////////////////////////////////////////////////////////////////////////////
// Copyright © 2022 Privategrity Corporation /
// /
// All rights reserved. /
////////////////////////////////////////////////////////////////////////////////
package restlike
import (
"encoding/json"
jww "github.com/spf13/jwalterweatherman"
"gitlab.com/elixxir/client/cmix/identity/receptionID"
"gitlab.com/elixxir/client/cmix/rounds"
"gitlab.com/elixxir/client/single"
)
// processor is the reception handler for a RestServer
type singleReceiver struct {
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)
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 {
cb(newMessage)
} else {
jww.ERROR.Printf("Unable to call restlike endpoint: %+v", err)
}
}
////////////////////////////////////////////////////////////////////////////////
// Copyright © 2022 Privategrity Corporation /
// /
// All rights reserved. /
////////////////////////////////////////////////////////////////////////////////
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"
)
// 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
// 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
//func TransmitRequest(recipient contact.Contact, tag string, payload []byte,
// callback Response, param RequestParams, net Cmix, rng csprng.Source,
// e2eGrp *cyclic.Group) ([]id.Round, receptionID.EphemeralIdentity, error)
}
// 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) {
// Build the Message
newMessage := &message{
content: content,
headers: param,
method: method,
uri: path,
}
msg, err := json.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 <- msg
}
// Transmit the Message
_, _, err = single.TransmitRequest(recipient, catalog.RestLike, msg,
&singleResponse{responseCallback: cb}, s.RequestParam, s.Net, s.Rng, s.E2eGrp)
if err != nil {
return nil, err
}
// Block waiting for single-use response
jww.DEBUG.Printf("Restlike waiting for single-use response from %s...", recipient.ID.String())
newResponse := <-signalChannel
jww.DEBUG.Printf("Restlike single-use response received from %s", recipient.ID.String())
return newResponse, nil
}
// 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 {
// Build the Message
newMessage := &message{
content: content,
headers: param,
method: method,
uri: path,
}
msg, err := json.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)
return err
}
////////////////////////////////////////////////////////////////////////////////
// Copyright © 2022 Privategrity Corporation /
// /
// All rights reserved. /
////////////////////////////////////////////////////////////////////////////////
package restlike
import (
"encoding/json"
jww "github.com/spf13/jwalterweatherman"
"gitlab.com/elixxir/client/cmix/identity/receptionID"
"gitlab.com/elixxir/client/cmix/rounds"
)
// processor is the response handler for a Request
type singleResponse struct {
responseCallback Callback
}
// 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{}
// Handle response errors
if err != nil {
newMessage.err = err.Error()
s.responseCallback(newMessage)
}
// Unmarshal the payload
err = json.Unmarshal(payload, newMessage)
if err != nil {
jww.ERROR.Printf("Unable to unmarshal restlike message: %+v", err)
return
}
// Send the response payload to the responseCallback
s.responseCallback(newMessage)
}
////////////////////////////////////////////////////////////////////////////////
// Copyright © 2022 Privategrity Corporation /
// /
// All rights reserved. /
////////////////////////////////////////////////////////////////////////////////
package restlike
import (
"gitlab.com/elixxir/client/catalog"
"gitlab.com/elixxir/client/single"
"gitlab.com/elixxir/crypto/cyclic"
"gitlab.com/xx_network/primitives/id"
)
// RestServer allows for clients to make REST-like requests this client
type RestServer interface {
// RegisterEndpoint allows the association of a Callback with
// a specific URI and a variety of different REST Method
RegisterEndpoint(path URI, method Method, cb Callback) error
// UnregisterEndpoint removes the Callback associated with
// a specific URI and REST Method
UnregisterEndpoint(path URI, method Method) error
// Close the internal RestServer endpoints and external services
Close()
}
// singleServer implements the RestServer interface using single-use
type singleServer struct {
receptionId *id.ID
listener single.Listener
endpoints Endpoints
}
// NewSingleServer builds a RestServer with single-use and
// the provided arguments, then registers necessary external services
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),
}
newServer.listener = single.Listen(catalog.RestLike, receptionId, privKey,
net, e2eGrp, &singleReceiver{newServer.endpoints})
return newServer
}
// RegisterEndpoint allows the association of a Callback with
// a specific URI and a variety of different REST Method
func (r *singleServer) RegisterEndpoint(path URI, method Method, cb Callback) error {
return r.endpoints.Add(path, method, cb)
}
// UnregisterEndpoint removes the Callback associated with
// a specific URI and REST Method
func (r *singleServer) UnregisterEndpoint(path URI, method Method) error {
return r.endpoints.Remove(path, method)
}
// Close the internal RestServer endpoints and external services
func (r *singleServer) Close() {
// Clear all internal endpoints
r.endpoints = make(map[URI]map[Method]Callback)
// Destroy external services
r.listener.Stop()
}
////////////////////////////////////////////////////////////////////////////////
// Copyright © 2022 Privategrity Corporation /
// /
// All rights reserved. /
////////////////////////////////////////////////////////////////////////////////
package restlike
import "github.com/pkg/errors"
// 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
// Method defines the possible Request types
type Method uint8
// 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
}
const (
// Undefined default value
Undefined Method = iota
// Get retrieve an existing resource.
Get
// Post creates a new resource.
Post
// Put updates an existing resource.
Put
// Patch partially updates an existing resource.
Patch
// Delete a resource.
Delete
)
// methodStrings is a map of Method values back to their constant names for printing
var methodStrings = map[Method]string{
Undefined: "undefined",
Get: "get",
Post: "post",
Put: "put",
Patch: "patch",
Delete: "delete",
}
// String returns the Method as a human-readable name.
func (m Method) String() string {
if methodStr, ok := methodStrings[m]; ok {
return methodStr
}
return methodStrings[Undefined]
}
// Endpoints represents a map of internal endpoints for a RestServer
type Endpoints map[URI]map[Method]Callback
// 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)
}
if _, ok := e[path][method]; ok {
return errors.Errorf("unable to RegisterEndpoint: %s/%s already exists", path, method)
}
e[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 {
return nil, errors.Errorf("unable to locate endpoint: %s", path)
}
if _, innerOk := e[path][method]; !innerOk {
return nil, errors.Errorf("unable to locate endpoint: %s/%s", path, method)
}
return e[path][method], nil
}
// Remove an Endpoint
// Returns an error if Endpoint does not exist
func (e Endpoints) Remove(path URI, method Method) error {
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)
}
return nil
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment