Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Closes #20 - Implement an HTTP package
The main request function is implemented in Go with convenience functions written in Nitrogen.
- Loading branch information
Showing
6 changed files
with
319 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
# http.ni | ||
|
||
Functions for encoding and decoding http. | ||
|
||
To use: `import 'stdlib/http'` | ||
|
||
## get(url: string): Response | ||
|
||
`get` makes an HTTP GET request to the given URL. | ||
|
||
## post(url: string[, data: T, options: HTTPOptions]): Response | ||
|
||
`post` makes an HTTP POST request to the given URL. If data is not a string, | ||
it will be JSON encoded and the header `Content-Type` will be set to "json/application". | ||
|
||
## getJSON(url: string): T | ||
|
||
Calls `get` with the given URL and returns the output of `json.decode` on the | ||
returned body. | ||
|
||
## req(method: string, url: string[, data: string, options: HTTPOptions]): Response | ||
|
||
`req` is a low-level command to the native HTTP implementation. `req` can be used | ||
to send requests that aren't possible with the other convenience functions such | ||
as other methods like DELETE or PUT. | ||
|
||
## canonicalHeaderKey(s: string): string | ||
|
||
`canonicalHeaderKey` returns the canonical format of the header key s. The | ||
canonicalization converts the first letter and any letter following a hyphen to | ||
upper case; the rest are converted to lowercase. For example, the canonical key | ||
for "accept-encoding" is "Accept-Encoding". If s contains a space or invalid header | ||
field bytes, it is returned without modifications.* | ||
|
||
|
||
\* Description for `canonicalHeaderKey` taken from https://golang.org/pkg/net/http/#CanonicalHeaderKey. | ||
|
||
## Response: map | ||
|
||
Response is a map with the following structure: | ||
|
||
``` | ||
{ | ||
"body": string | ||
"headers": map | ||
} | ||
``` | ||
|
||
### Fields | ||
|
||
#### body | ||
|
||
The body of the returned request. No processing is done once received. | ||
|
||
#### headers | ||
|
||
`headers` is a map of string keys to string values containing the HTTP headers | ||
sent or received. When received, the map keys will be in canonical header format. | ||
|
||
## HTTPOptions: map | ||
|
||
HTTPOptions is a map with the following structure: | ||
|
||
``` | ||
{ | ||
"headers": map | ||
} | ||
``` | ||
|
||
### Fields | ||
|
||
#### headers | ||
|
||
`headers` is a map of string keys to string values containing the HTTP headers | ||
sent or received. When received, the map keys will be in canonical header format. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
import "stdlib/native/http" | ||
import "stdlib/json" | ||
|
||
use http.do | ||
|
||
const exports = { | ||
"req": do, | ||
"canonicalHeaderKey": http.canonicalHeaderKey, | ||
} | ||
|
||
func getJSON(url) { | ||
const resp = get(url) | ||
return json.decode(resp.body) | ||
} | ||
exports.getJSON = getJSON | ||
|
||
func get(url) { | ||
let options | ||
if len(arguments) >= 1: options = arguments[0] | ||
return do("GET", url, "", options) | ||
} | ||
exports.get = get | ||
|
||
func post(url) { | ||
let data | ||
let options = {} | ||
|
||
if len(arguments) >= 1: data = arguments[0] | ||
if len(arguments) >= 2: options = arguments[1] | ||
|
||
if !isNull(data) and !isString(data) { | ||
data = json.encode(data) | ||
options["Content-Type"] = "application/json" | ||
} | ||
|
||
return do("POST", url, data, options) | ||
} | ||
exports.post = post | ||
|
||
return exports |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
package string | ||
|
||
import ( | ||
"io/ioutil" | ||
"net/http" | ||
"strings" | ||
|
||
"github.com/nitrogen-lang/nitrogen/src/moduleutils" | ||
"github.com/nitrogen-lang/nitrogen/src/object" | ||
"github.com/nitrogen-lang/nitrogen/src/vm" | ||
) | ||
|
||
var ( | ||
moduleName = "stdlib/native/http" | ||
client = &http.Client{} | ||
) | ||
|
||
func init() { | ||
vm.RegisterModule(moduleName, &object.Module{ | ||
Name: moduleName, | ||
Methods: map[string]object.BuiltinFunction{ | ||
"do": do, | ||
"canonicalHeaderKey": canonicalHeaderKey, | ||
}, | ||
Vars: map[string]object.Object{}, | ||
}) | ||
} | ||
|
||
func do(interpreter object.Interpreter, env *object.Environment, args ...object.Object) object.Object { | ||
if ac := moduleutils.CheckMinArgs("http.do", 2, args...); ac != nil { | ||
return ac | ||
} | ||
|
||
// Argument 1 - HTTP method | ||
methodObj, ok := args[0].(*object.String) | ||
if !ok { | ||
return object.NewException("http.do expected first argument to be a string, got %s", args[0].Type().String()) | ||
} | ||
method := strings.ToUpper(methodObj.String()) | ||
if method == "" { | ||
method = "GET" | ||
} | ||
|
||
// Argument 2 - URL | ||
urlObj, ok := args[1].(*object.String) | ||
if !ok { | ||
return object.NewException("http.do expected second argument to be a string, got %s", args[1].Type().String()) | ||
} | ||
|
||
url := strings.TrimSpace(urlObj.String()) | ||
if url == "" { | ||
return object.NewException("http.do expected a non-empty string") | ||
} | ||
|
||
// Argument 3 - Data payload | ||
data := "" | ||
if len(args) >= 3 && args[2] != object.NullConst { | ||
dataObj, ok := args[2].(*object.String) | ||
if !ok { | ||
return object.NewException("http.do expected third argument to be a string, got %s", args[2].Type().String()) | ||
} | ||
data = dataObj.String() | ||
} | ||
|
||
// Argument 4 - Request options | ||
var optionsObj *object.Hash | ||
if len(args) >= 4 && args[3] != object.NullConst { | ||
dataObj, ok := args[3].(*object.Hash) | ||
if !ok { | ||
return object.NewException("http.post expected fourth argument to be a map, got %s", args[3].Type().String()) | ||
} | ||
optionsObj = dataObj | ||
} | ||
|
||
req, err := http.NewRequest(method, url, strings.NewReader(data)) | ||
if err != nil { | ||
return object.NewException("error making HTTP request: %s", err.Error()) | ||
} | ||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded") | ||
|
||
if optionsObj != nil { | ||
headers := optionsObj.LookupKey("headers") | ||
headersMap, ok := headers.(*object.Hash) | ||
if !ok { | ||
return object.NewException("headers option must be map") | ||
} | ||
|
||
for _, pair := range headersMap.Pairs { | ||
if pair.Key.Type() != object.StringObj { | ||
continue | ||
} | ||
if pair.Value.Type() != object.StringObj { | ||
continue | ||
} | ||
req.Header.Set(pair.Key.(*object.String).String(), pair.Value.(*object.String).String()) | ||
} | ||
} | ||
|
||
resp, err := client.Do(req) | ||
|
||
if err != nil { | ||
return object.NewException("error making HTTP request: %s", err.Error()) | ||
} | ||
defer resp.Body.Close() | ||
|
||
return buildReturnValue(resp) | ||
} | ||
|
||
func buildReturnValue(resp *http.Response) object.Object { | ||
body, _ := ioutil.ReadAll(resp.Body) | ||
|
||
headers := make(map[string]string, len(resp.Header)) | ||
|
||
for name, values := range resp.Header { | ||
headers[name] = values[0] | ||
} | ||
|
||
hash := &object.Hash{ | ||
Pairs: make(map[object.HashKey]object.HashPair, 2), | ||
} | ||
|
||
hash.SetKey("body", object.MakeStringObj(string(body))) | ||
hash.SetKey("headers", object.StringMapToHash(headers)) | ||
|
||
return hash | ||
} | ||
|
||
func canonicalHeaderKey(interpreter object.Interpreter, env *object.Environment, args ...object.Object) object.Object { | ||
if ac := moduleutils.CheckMinArgs("http.canonicalHeaderKey", 1, args...); ac != nil { | ||
return ac | ||
} | ||
|
||
headerKey, ok := args[0].(*object.String) | ||
if !ok { | ||
return object.NewException("http.canonicalHeaderKey expected a string argument, got %s", args[0].Type().String()) | ||
} | ||
|
||
return object.MakeStringObj(http.CanonicalHeaderKey(headerKey.String())) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
import "stdlib/http" | ||
import "stdlib/json" | ||
import "stdlib/test" | ||
import "stdlib/collections" | ||
|
||
use collections.contains | ||
|
||
test.run("HTTP GET request", func(assert) { | ||
const resp = http.get("https://jsonplaceholder.typicode.com/posts/1") | ||
|
||
assert.isTrue(contains(resp, "body")) | ||
assert.isTrue(contains(resp, "headers")) | ||
assert.isTrue(isString(resp.body)) | ||
}) | ||
|
||
test.run("HTTP GET JSON request", func(assert) { | ||
const resp = http.getJSON("https://jsonplaceholder.typicode.com/posts/1") | ||
|
||
assert.isTrue(isMap(resp)) | ||
assert.isTrue(contains(resp, "id")) | ||
assert.isTrue(contains(resp, "title")) | ||
assert.isTrue(contains(resp, "body")) | ||
assert.isTrue(contains(resp, "userId")) | ||
}) | ||
|
||
test.run("HTTP POST request", func(assert) { | ||
const data = json.encode({ | ||
"title": 'foo', | ||
"body": 'bar', | ||
"userId": 1, | ||
}) | ||
|
||
const resp = http.post("https://jsonplaceholder.typicode.com/posts", data, { | ||
"headers": { | ||
"Content-Type": "application/json", | ||
}, | ||
}) | ||
|
||
assert.isTrue(contains(resp, "body")) | ||
assert.isTrue(contains(resp, "headers")) | ||
assert.isTrue(isString(resp.body)) | ||
|
||
const respData = json.decode(resp.body) | ||
assert.isTrue(isMap(respData)) | ||
assert.isTrue(contains(respData, "id")) | ||
assert.isTrue(contains(respData, "title")) | ||
assert.isTrue(contains(respData, "body")) | ||
assert.isTrue(contains(respData, "userId")) | ||
}) |