Skip to content

Commit

Permalink
Merge 55461b7 into 2d590fb
Browse files Browse the repository at this point in the history
  • Loading branch information
jirenius committed Nov 15, 2019
2 parents 2d590fb + 55461b7 commit a821ccc
Show file tree
Hide file tree
Showing 20 changed files with 683 additions and 257 deletions.
44 changes: 44 additions & 0 deletions docs/res-protocol-v1.2-update.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# RES Protocol v1.2 Update

## Changes from v1.1



## Deprecation

The update deprecates the pre-defined *new call requests* and replaces it with the [Resource response](res-service-protocol.md#response).


Any useThe specification has been updated to have the changed properties contained in a `values` field:

**v1.1 successful response payload to new call request:**
```json
{
"result": { "rid": "example.foo.42" }
}
```

**v1.2 successful response payload to new call request:**
```json
{
"rid": "example.foo.42"
}
```

## Reason

The ability to return a resource reference as part of a call or auth is useful for situations such as n Model change events, as defined in RES-service specification V1.0, gave no room to include meta data in the event. This is a design flaw which prevents the specification to adapt to requests such as version numbering of resources.

## Impact

The version upgrade affects both the RES-service protocol and the RES-client protocol.

* A client supporting Any client should add a deprecated warning on the use of the predefined `new

Resgate can detect service legacy (v1.1) behavior and handle it, but will log a *Deprecated* warning with a link to this page.

## Migration

Any service that handled *new call requests* should be updated to follow v1.2 specification.

This can be done in partial steps, one service at a time, as Resgate detects legacy behavior for each *new call response*.
37 changes: 23 additions & 14 deletions docs/res-protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
* [Models](#models)
* [Collections](#collections)
* [Values](#values)
* [Resource references](#resource-references)
* [Messaging system](#messaging-system)
* [Services](#services)
* [Gateways](#gateways)
Expand Down Expand Up @@ -48,11 +49,11 @@ May be omitted. If omitted, then the question mark separator MUST also be omitte

**Examples**

* `userService.users` - A collection representing a list of users
* `userService.user.42` - A model representing a user
* `userService.user.42.roles` - A collection of roles held by a user
* `messageService.messages?start=0&limit=25` - A collection representing the first 25 messages of a list
* `userService.users?q=Jane` - A collection of users with the name Jane
* `example.users` - A collection representing a list of users
* `example.user.42` - A model representing a user
* `example.user.42.roles` - A collection of roles held by a user
* `chat.messages?start=0&limit=25` - A collection representing the first 25 messages of a list
* `example.users?q=Jane` - A collection of users with the name Jane

## Models

Expand All @@ -63,7 +64,7 @@ A model is an unordered set of named properties and [values](#values) represente
{
"id": 42,
"name": "Jane Doe",
"roles": { "rid": "userService.user.42.roles" }
"roles": { "rid": "example.user.42.roles" }
}
```

Expand All @@ -78,19 +79,27 @@ A collection is an ordered list of [values](#values) represented by a JSON array

## Values

A value is either a *primitive* or a *resource reference*.
Primitives are either a JSON `string`, `number`, `true`, `false`, or `null` value.
Resource references are JSON objects with the following parameter:
A value is either a *primitive* or a [resource reference](#resource-references).
A primitive is either a JSON `string`, `number`, `true`, `false`, or `null` value.

### Example
```javascript
"foo" // string
42 // number
true // boolean true
false // boolean false
null // null
{ "rid": "example.user.42" } // resource reference
```

## Resource references

A resource reference is a JSON objects with the following parameter:

**rid**
Resource ID of the referenced resource.
MUST be a valid [resource ID](#resource-ids).

### Example
```json
{ "rid": "userService.user.42" }
```

## Messaging system

The messaging system handles the communication between [services](#services) and [gateways](#gateways). It MUST provide the following functionality:
Expand Down
16 changes: 12 additions & 4 deletions docs/res-service-protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,17 +63,23 @@ The content of the payload depends on the subject type.


## Response
When a request is received by a service, it should send a response as a JSON object with following members:
When a request is received by a service, it should send a response as a JSON object with one of the following members:

**result**
Is REQUIRED on success.
Will be ignored on error.
Is REQUIRED on success if **resource** is not set.
SHOULD be ignored if **error** or **resource** is set.
The value is determined by the request subject.

**resource**
MUST be omitted if the request type is not `call` or `auth`.
Is REQUIRED on success if **result** is not set.
SHOULD be ignored if **error** is set.
The value MUST be a valid [resource reference](res-protocol.md#resource-references).

**error**
Is REQUIRED on error.
MUST be omitted on success.
The value MUST be an error object as defined in the [Error object](#error-object) section.
The value MUST be an [error object](#error-object).

## Error object

Expand Down Expand Up @@ -316,6 +322,8 @@ MUST NOT be sent on [collections](res-protocol.md#collections).

## New call request

*DEPRECATED: Use [resource response](#response) instead.*

**Subject**
`call.<resourceName>.new`

Expand Down
29 changes: 11 additions & 18 deletions server/apiHandler.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,24 +86,17 @@ func (s *Service) apiHandler(w http.ResponseWriter, r *http.Request) {
}

s.temporaryConn(w, r, func(c *wsConn, cb func([]byte, error)) {
switch action {
case "new":
c.NewHTTPResource(rid, s.cfg.APIPath, params, func(href string, err error) {
if err == nil {
w.Header().Set("Location", href)
w.WriteHeader(http.StatusCreated)
}
c.CallHTTPResource(rid, s.cfg.APIPath, action, params, func(r json.RawMessage, href string, err error) {
if err != nil {
cb(nil, err)
})
default:
c.CallResource(rid, action, params, func(r json.RawMessage, err error) {
if err != nil {
cb(nil, err)
return
}
} else if href != "" {
w.Header().Set("Location", href)
w.WriteHeader(http.StatusOK)
cb(nil, nil)
} else {
cb(s.enc.EncodePOST(r))
})
}
}
})
})

default:
Expand All @@ -118,7 +111,7 @@ func notFoundHandler(w http.ResponseWriter, r *http.Request, enc APIEncoder) {
}

func (s *Service) temporaryConn(w http.ResponseWriter, r *http.Request, cb func(*wsConn, func([]byte, error))) {
c := s.newWSConn(nil, r)
c := s.newWSConn(nil, r, latestProtocol)
if c == nil {
httpError(w, reserr.ErrServiceUnavailable, s.enc)
return
Expand All @@ -144,7 +137,7 @@ func (s *Service) temporaryConn(w http.ResponseWriter, r *http.Request, cb func(
}
c.Enqueue(func() {
if s.cfg.HeaderAuth != nil {
c.AuthResource(s.cfg.headerAuthRID, s.cfg.headerAuthAction, nil, func(_ json.RawMessage, err error) {
c.AuthResource(s.cfg.headerAuthRID, s.cfg.headerAuthAction, nil, func(_ interface{}, err error) {
cb(c, rs)
})
} else {
Expand Down
56 changes: 34 additions & 22 deletions server/codec/codec.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,9 @@ type Request struct {
// Response represents a RES-service response
// https://github.com/resgateio/resgate/blob/master/docs/res-service-protocol.md#response
type Response struct {
Result json.RawMessage `json:"result"`
Error *reserr.Error `json:"error"`
Result json.RawMessage `json:"result"`
Resource *Resource `json:"resource"`
Error *reserr.Error `json:"error"`
}

// AccessResponse represents the response of a RES-service access request
Expand Down Expand Up @@ -81,12 +82,12 @@ type AuthRequest struct {
// NewResponse represents the response of a RES-service new call request
// https://github.com/resgateio/resgate/blob/master/docs/res-service-protocol.md#new-call-request
type NewResponse struct {
Result *NewResult `json:"result"`
Result *Resource `json:"result"`
Error *reserr.Error `json:"error"`
}

// NewResult represents the response result of a RES-service new call request
type NewResult struct {
// Resource represents the resource response of a RES-service call or auth request
type Resource struct {
RID string `json:"rid"`
}

Expand Down Expand Up @@ -413,7 +414,7 @@ func DecodeEventQueryResponse(payload []byte) (*EventQueryResult, error) {
}

// IsLegacyChangeEvent returns true if the model change event is detected as v1.0 legacy
// Remove after 2020-03-31
// [DEPRECATED:deprecatedModelChangeEvent]
func IsLegacyChangeEvent(data json.RawMessage) bool {
var r map[string]json.RawMessage
err := json.Unmarshal(data, &r)
Expand Down Expand Up @@ -527,45 +528,56 @@ func DecodeAccessResponse(payload []byte) (*AccessResult, *reserr.Error) {
}

// DecodeCallResponse decodes a JSON encoded RES-service call response
func DecodeCallResponse(payload []byte) (json.RawMessage, error) {
func DecodeCallResponse(payload []byte) (json.RawMessage, string, error) {
var r Response
err := json.Unmarshal(payload, &r)
if err != nil {
return nil, reserr.RESError(err)
return nil, "", reserr.RESError(err)
}

if r.Error != nil {
return nil, r.Error
return nil, "", r.Error
}

if r.Resource != nil {
rid := r.Resource.RID
if !IsValidRID(rid, true) {
return nil, "", errInvalidResponse
}
return nil, rid, nil
}

if r.Result == nil {
return nil, errMissingResult
return nil, "", errMissingResult
}

return r.Result, nil
return r.Result, "", nil
}

// DecodeNewResponse decodes a JSON encoded RES-service new call response
func DecodeNewResponse(payload []byte) (string, error) {
var r NewResponse
err := json.Unmarshal(payload, &r)
// TryDecodeLegacyNewResult tries to detect legacy v1.1.1 behavior.
// Returns empty string and nil error when the result is not detected as legacy.
// [DEPRECATED:deprecatedNewCallRequest]
func TryDecodeLegacyNewResult(result json.RawMessage) (string, error) {
var r map[string]interface{}
err := json.Unmarshal(result, &r)
if err != nil {
return "", reserr.RESError(err)
return "", nil
}

if r.Error != nil {
return "", r.Error
if len(r) != 1 {
return "", nil
}

if r.Result == nil {
return "", errMissingResult
rid, ok := r["rid"].(string)
if !ok {
return "", nil
}

if !IsValidRID(r.Result.RID, true) {
if !IsValidRID(rid, true) {
return "", errInvalidResponse
}

return r.Result.RID, nil
return rid, nil
}

// DecodeConnTokenEvent decodes a JSON encoded RES-service connection token event
Expand Down
3 changes: 3 additions & 0 deletions server/rescache/deprecated.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ type featureType int
// deprecated feature types
const (
deprecatedModelChangeEvent featureType = 1 << iota
deprecatedNewCallRequest
)

// deprecated logs a deprecated error for each unique service name and feature
Expand All @@ -33,6 +34,8 @@ func (c *Cache) deprecated(rid string, typ featureType) {
switch typ {
case deprecatedModelChangeEvent:
msg = "model change event v1.0 detected\n Legacy support will be removed after 2020-03-31. For more information:\n https://github.com/resgateio/resgate/blob/master/docs/res-protocol-v1.1-update.md"
case deprecatedNewCallRequest:
msg = "new call request v1.1 detected\n Legacy support will be removed after 2021-11-30. For more information:\n https://github.com/resgateio/resgate/blob/master/docs/res-protocol-v1.2-update.md"
default:
c.Errorf("Invalid deprecation feature type: %d", typ)
return
Expand Down
33 changes: 17 additions & 16 deletions server/rescache/rescache.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,40 +140,41 @@ func (c *Cache) Access(sub Subscriber, token interface{}, callback func(access *
}

// Call sends a method call request
func (c *Cache) Call(req codec.Requester, rname, query, action string, token, params interface{}, callback func(result json.RawMessage, err error)) {
func (c *Cache) Call(req codec.Requester, rname, query, action string, token, params interface{}, callback func(result json.RawMessage, rid string, err error)) {
payload := codec.CreateRequest(params, req, query, token)
subj := "call." + rname + "." + action
c.sendRequest(rname, subj, payload, func(data []byte, err error) {
if err != nil {
callback(nil, err)
callback(nil, "", err)
return
}

callback(codec.DecodeCallResponse(data))
})
}

// CallNew sends a call request with the new method, expecting a response with an RID
func (c *Cache) CallNew(req codec.Requester, rname, query string, token, params interface{}, callback func(newRID string, err error)) {
payload := codec.CreateRequest(params, req, query, token)
subj := "call." + rname + ".new"
c.sendRequest(rname, subj, payload, func(data []byte, err error) {
if err != nil {
callback("", err)
// [DEPRECATED:deprecatedNewCallRequest]
if action == "new" {
result, rid, err := codec.DecodeCallResponse(data)
if err == nil && rid == "" {
rid, err = codec.TryDecodeLegacyNewResult(result)
if err != nil || rid != "" {
c.deprecated(rname, deprecatedNewCallRequest)
callback(nil, rid, err)
return
}
}
callback(result, rid, err)
return
}

callback(codec.DecodeNewResponse(data))
callback(codec.DecodeCallResponse(data))
})
}

// Auth sends an auth method call
func (c *Cache) Auth(req codec.AuthRequester, rname, query, action string, token, params interface{}, callback func(result json.RawMessage, err error)) {
func (c *Cache) Auth(req codec.AuthRequester, rname, query, action string, token, params interface{}, callback func(result json.RawMessage, rid string, err error)) {
payload := codec.CreateAuthRequest(params, req, query, token)
subj := "auth." + rname + "." + action
c.sendRequest(rname, subj, payload, func(data []byte, err error) {
if err != nil {
callback(nil, err)
callback(nil, "", err)
return
}

Expand Down

0 comments on commit a821ccc

Please sign in to comment.