---
title: "Body encoding and decoding"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Body encoding and decoding}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r, include = FALSE}
knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>", 
  results = 'markup'
)
```

# Encoding

Let's consider an example. We develop an application which calculates `factorial` of a number:

```{r}
library(RestRserve)
backend = BackendRserve$new()
application = Application$new()
application$add_get(path = "/factorial", function(.req, .res) {
  x = .req$get_param_query("x")
  x = as.integer(x)
  .res$set_body(factorial(x))
})
```

Here is how request will be processed:
```{r}
request = Request$new(
  path = "/factorial", 
  method = "GET", 
  parameters_query = list(x = 10)
)
response = application$process_request(request)
response
```

Let's take a closer look to the `response` object and its `body` property:
```{r}
str(response$body)
```

As we can see it is a numeric value. HTTP response body however can't be an arbitrary R object. It should be something that external systems can understand - either `character` vector or `raw` vector. Fortunately `application` helps to avoid writing boilerplate code to encode the body. Based on the `content_type` property it can find `encode` function which will be used to transform `body` into a http body.
```{r}
response$content_type
```
```{r}
response$encode
```

Two immediate questions can arise:

1. Why `content_type` is equal to `text/plain`? 
    - This is because we can specify default `content_type` in `Application` constructor. It is `text/plain` by default, which means all the responses by default will have `text/plain` content type.
1. How does application know how to encode `text/plain`? Can it encode any arbitrary content type?
    - Application by default is initialized with pre-defined ?EncodeDecodeMiddleware middleware. The logic on how to encode and decode request and response body is controlled by its `ContentHandlers` property. Out of the box it supports two content types - `text/plain` and `application/json`. 

For instance `app1` and `app2` are equal:
```{r}
encode_decode_middleware = EncodeDecodeMiddleware$new()
app1  = Application$new(middleware = list())
app1$append_middleware(encode_decode_middleware)

app2 = Application$new()
```

Here is example on how you can get the actual function used for `application/json` encoding:
```{r}

FUN = encode_decode_middleware$ContentHandlers$get_encode('application/json')
FUN
```
We can manually override application default content-type:
```{r}
application$add_get(path = "/factorial-json", function(.req, .res) {
  x = as.integer(.req$get_param_query("x"))
  result = factorial(x)
  .res$set_body(list(result = result))
  .res$set_content_type("application/json")
})
```

```{r}
request = Request$new(
  path = "/factorial-json", 
  method = "GET", 
  parameters_query = list(x = 10)
)
response = application$process_request(request)
```

```{r}
response$body
```

And here is a little bit more complex example where we store a binary object in the body. We will use R's native serialization, but one can use `protobuf`, `messagepack`, etc.

```{r}
application$add_get(path = "/factorial-rds", function(.req, .res) {
  x = as.integer(.req$get_param_query("x"))
  result = factorial(x)
  body_rds = serialize(list(result = result), connection = NULL)
  .res$set_body(body_rds)
  .res$set_content_type("application/x-rds")
})

```
However function above won't work correctly. Out of the box `ContentHndlers` doesn't know anything about `application/x-rds`:
```{r}
request = Request$new(
  path = "/factorial-rds", 
  method = "GET", 
  parameters_query = list(x = 10)
)
response = application$process_request(request)
response$body
```
In order to resolve problem above we would need to either register `application/x-rds` content handler with `ContentHandlers$set_encode()` or manually specify `encode` function (`identity` in our case):

```{r}
application$add_get(path = "/factorial-rds2", function(.req, .res) {
  x = as.integer(.req$get_param_query("x"))
  result = factorial(x)
  body_rds = serialize(list(result = result), connection = NULL)
  .res$set_body(body_rds)
  .res$set_content_type("application/x-rds")
  .res$encode = identity
})
```
Now the answer is valid:
```{r}
request = Request$new(
  path = "/factorial-rds2", 
  method = "GET", 
  parameters_query = list(x = 10)
)
response = application$process_request(request)
unserialize(response$body)
```

# Decoding

RestRserve facilitates with parsing incoming request body as well. Consider a service which expects JSON POST requests:

```{r}
application = Application$new(content_type = "application/json")
application$add_post("/echo", function(.req, .res) {
  .res$set_body(.req$body)
})

request = Request$new(path = "/echo", method = "POST", body = '{"hello":"world"}', content_type = "application/json")
response = application$process_request(request)
response$body
```
The logic behind decoding is also controlled by ?EncodeDecodeMiddleware and its `ContentHandlers` property.

# Extending encoding and decoding

Here is an example which demonstrates on how to extend ?EncodeDecodeMiddleware to handle additional content types:

```{r}
encode_decode_middleware = EncodeDecodeMiddleware$new()

encode_decode_middleware$ContentHandlers$set_encode(
  "text/csv", 
  function(x) {
    con = rawConnection(raw(0), "w")
    on.exit(close(con))
    write.csv(x, con, row.names = FALSE)
    rawConnectionValue(con)
  }
)

encode_decode_middleware$ContentHandlers$set_decode(
  "text/csv", 
  function(x) {
    res = try({
      con = textConnection(rawToChar(x), open = "r")
      on.exit(close(con))
      read.csv(con)
    }, silent = TRUE)
    
    if (inherits(res, "try-error")) {
      raise(HTTPError$bad_request(body = attributes(res)$condition$message))
    }
    return(res)
  }
)
```

Extended middleware needs to be provided to the application constructor:

```{r}
data(iris)
app = Application$new(middleware = list(encode_decode_middleware))
```

Now let's test it:

```{r}
app$add_get("/iris", FUN = function(.req, .res) {
  .res$set_content_type("text/csv")
  .res$set_body(iris)
})

req = Request$new(path = "/iris", method = "GET")
res = app$process_request(req)

iris_out = read.csv(textConnection(rawToChar(res$body)))
head(iris_out)
```

```{r}
app$add_post("/in", FUN = function(.req, .res) {
  str(.req$body)
})
req = Request$new(path = "/in", method = "POST", body = res$body, content_type = "text/csv")
app$process_request(req)
```