1
0
mirror of https://github.com/MontFerret/ferret.git synced 2025-01-16 03:21:03 +02:00

Added HTTP methods (#452)

* Added HTTP methods

* Added unit tests

* Fixed linting issue

* Replaced localhost with 127.0.0.1

* Added random port generator

* Disabled http tests

* Disabled HTTP GET test
This commit is contained in:
Tim Voronov 2020-02-21 11:06:45 -05:00 committed by GitHub
parent c49045ef56
commit 67dc8703c7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 668 additions and 8 deletions

View File

@ -1,4 +1,4 @@
// Code generated from antlr/FqlLexer.g4 by ANTLR 4.7.2. DO NOT EDIT.
// Code generated from antlr/FqlLexer.g4 by ANTLR 4.8. DO NOT EDIT.
package fql

View File

@ -1,4 +1,4 @@
// Code generated from antlr/FqlParser.g4 by ANTLR 4.7.2. DO NOT EDIT.
// Code generated from antlr/FqlParser.g4 by ANTLR 4.8. DO NOT EDIT.
package fql // FqlParser
import (

View File

@ -1,4 +1,4 @@
// Code generated from antlr/FqlParser.g4 by ANTLR 4.7.2. DO NOT EDIT.
// Code generated from antlr/FqlParser.g4 by ANTLR 4.8. DO NOT EDIT.
package fql // FqlParser
import "github.com/antlr/antlr4/runtime/Go/antlr"

View File

@ -1,4 +1,4 @@
// Code generated from antlr/FqlParser.g4 by ANTLR 4.7.2. DO NOT EDIT.
// Code generated from antlr/FqlParser.g4 by ANTLR 4.8. DO NOT EDIT.
package fql // FqlParser
import "github.com/antlr/antlr4/runtime/Go/antlr"

View File

@ -1,4 +1,4 @@
// Code generated from antlr/FqlParser.g4 by ANTLR 4.7.2. DO NOT EDIT.
// Code generated from antlr/FqlParser.g4 by ANTLR 4.8. DO NOT EDIT.
package fql // FqlParser
import "github.com/antlr/antlr4/runtime/Go/antlr"

View File

@ -1,4 +1,4 @@
// Code generated from antlr/FqlParser.g4 by ANTLR 4.7.2. DO NOT EDIT.
// Code generated from antlr/FqlParser.g4 by ANTLR 4.8. DO NOT EDIT.
package fql // FqlParser
import "github.com/antlr/antlr4/runtime/Go/antlr"

View File

@ -3,16 +3,20 @@ package io
import (
"github.com/MontFerret/ferret/pkg/runtime/core"
"github.com/MontFerret/ferret/pkg/stdlib/io/fs"
"github.com/MontFerret/ferret/pkg/stdlib/io/net"
)
// RegisterLib register `IO` namespace functions.
func RegisterLib(ns core.Namespace) error {
io := ns.Namespace("IO")
err := fs.RegisterLib(io)
if err != nil {
if err := fs.RegisterLib(io); err != nil {
return core.Error(err, "register `FS`")
}
if err := net.RegisterLib(io); err != nil {
return core.Error(err, "register `NET`")
}
return nil
}

View File

@ -0,0 +1,14 @@
package http
import (
"context"
h "net/http"
"github.com/MontFerret/ferret/pkg/runtime/core"
)
// DELETE makes a DELETE request to the specified URL.
// @params url or (String) - path to file to write into.
func DELETE(ctx context.Context, args ...core.Value) (core.Value, error) {
return execMethod(ctx, h.MethodDelete, args)
}

View File

@ -0,0 +1,109 @@
package http_test
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"math/rand"
h "net/http"
"testing"
"github.com/pkg/errors"
. "github.com/smartystreets/goconvey/convey"
"github.com/MontFerret/ferret/pkg/runtime/values"
"github.com/MontFerret/ferret/pkg/runtime/values/types"
"github.com/MontFerret/ferret/pkg/stdlib/io/net/http"
)
func randPort() string {
min := 8000
max := 8999
return fmt.Sprintf(":%d", rand.Intn(max-min)+min)
}
func TestDELETE(t *testing.T) {
SkipConvey("Should successfully make request", t, func() {
type User struct {
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
}
port := randPort()
server := &h.Server{
Addr: port,
Handler: h.HandlerFunc(func(w h.ResponseWriter, r *h.Request) {
var err error
defer func() {
if err != nil {
w.Write([]byte(err.Error()))
} else {
w.Write([]byte("OK"))
}
}()
if r.Method != "DELETE" {
err = errors.Errorf("Expected method to be DELETE, but got %s", r.Method)
return
}
data, err := ioutil.ReadAll(r.Body)
if err != nil {
return
}
user := User{}
err = json.Unmarshal(data, &user)
if err != nil {
return
}
if user.FirstName != "Rob" {
err = errors.Errorf("Expected FirstName to be Rob, but got %s", user.FirstName)
return
}
if user.LastName != "Pike" {
err = errors.Errorf("Expected LastName to be Pike, but got %s", user.LastName)
return
}
}),
}
ctx, cancel := context.WithCancel(context.Background())
go func() {
server.ListenAndServe()
}()
defer func() {
cancel()
server.Shutdown(ctx)
}()
b, err := json.Marshal(User{
FirstName: "Rob",
LastName: "Pike",
})
So(err, ShouldBeNil)
out, err := http.DELETE(ctx, values.NewObjectWith(
values.NewObjectProperty("url", values.NewString("http://127.0.0.1"+port)),
values.NewObjectProperty("body", values.NewBinary(b)),
))
So(err, ShouldBeNil)
So(out.Type().ID(), ShouldEqual, types.Binary.ID())
So(out.String(), ShouldEqual, "OK")
})
}

View File

@ -0,0 +1,35 @@
package http
import (
"context"
h "net/http"
"github.com/MontFerret/ferret/pkg/runtime/core"
"github.com/MontFerret/ferret/pkg/runtime/values"
"github.com/MontFerret/ferret/pkg/runtime/values/types"
)
// GET makes a GET request to the specified URL.
// @params url or (String) - path to file to write into.
func GET(ctx context.Context, args ...core.Value) (core.Value, error) {
if err := core.ValidateArgs(args, 1, 1); err != nil {
return values.None, err
}
arg := args[0]
if err := core.ValidateType(arg, types.String, types.Object); err != nil {
return values.None, err
}
if arg.Type() == types.String {
return makeRequest(ctx, Params{
Method: "GET",
URL: values.ToString(arg),
Headers: nil,
Body: nil,
})
}
return execMethod(ctx, h.MethodGet, args)
}

View File

@ -0,0 +1,112 @@
package http_test
import (
"context"
"github.com/MontFerret/ferret/pkg/runtime/values"
"github.com/MontFerret/ferret/pkg/runtime/values/types"
"github.com/pkg/errors"
h "net/http"
"testing"
. "github.com/smartystreets/goconvey/convey"
"github.com/MontFerret/ferret/pkg/stdlib/io/net/http"
)
func TestGET(t *testing.T) {
SkipConvey("Should successfully make request", t, func() {
port := randPort()
server := &h.Server{
Addr: port,
Handler: h.HandlerFunc(func(w h.ResponseWriter, r *h.Request) {
if r.Method == "GET" {
w.Write([]byte("OK"))
} else {
w.Write([]byte("Expected method to be GET"))
}
}),
}
ctx, cancel := context.WithCancel(context.Background())
go func() {
server.ListenAndServe()
}()
defer func() {
cancel()
server.Shutdown(ctx)
}()
out, err := http.GET(ctx, values.NewString("http://localhost"+port))
So(err, ShouldBeNil)
So(out.Type().ID(), ShouldEqual, types.Binary.ID())
So(out.String(), ShouldEqual, "OK")
})
SkipConvey("Should add headers to a request", t, func() {
port := randPort()
server := &h.Server{
Addr: port,
Handler: h.HandlerFunc(func(w h.ResponseWriter, r *h.Request) {
var err error
defer func() {
if err != nil {
w.Write([]byte(err.Error()))
} else {
w.Write([]byte("OK"))
}
}()
if r.Method != "GET" {
err = errors.Errorf("Expected method to be GET, but got %s", r.Method)
return
}
token := r.Header.Get("X-Token")
if token != "Ferret" {
err = errors.Errorf("Expected X-Token header to equal to Ferret, but got %s", token)
return
}
from := r.Header.Get("X-From")
if from != "localhost" {
err = errors.Errorf("Expected X-From header to equal to localhost, but got %s", from)
return
}
}),
}
ctx, cancel := context.WithCancel(context.Background())
go func() {
server.ListenAndServe()
}()
defer func() {
cancel()
server.Shutdown(ctx)
}()
out, err := http.GET(ctx, values.NewObjectWith(
values.NewObjectProperty("url", values.NewString("http://127.0.0.1"+port)),
values.NewObjectProperty("headers", values.NewObjectWith(
values.NewObjectProperty("X-Token", values.NewString("Ferret")),
values.NewObjectProperty("X-From", values.NewString("localhost")),
)),
))
So(err, ShouldBeNil)
So(out.Type().ID(), ShouldEqual, types.Binary.ID())
So(out.String(), ShouldEqual, "OK")
})
}

View File

@ -0,0 +1,17 @@
package http
import "github.com/MontFerret/ferret/pkg/runtime/core"
// RegisterLib register `HTTP` namespace functions.
func RegisterLib(ns core.Namespace) error {
return ns.
Namespace("HTTP").
RegisterFunctions(
core.NewFunctionsFromMap(map[string]core.Function{
"GET": GET,
"POST": POST,
"PUT": PUT,
"DELETE": DELETE,
"DO": REQUEST,
}))
}

View File

@ -0,0 +1,14 @@
package http
import (
"context"
h "net/http"
"github.com/MontFerret/ferret/pkg/runtime/core"
)
// POST makes a POST request to the specified URL.
// @params url or (String) - path to file to write into.
func POST(ctx context.Context, args ...core.Value) (core.Value, error) {
return execMethod(ctx, h.MethodPost, args)
}

View File

@ -0,0 +1,101 @@
package http_test
import (
"context"
"encoding/json"
"io/ioutil"
h "net/http"
"testing"
"github.com/pkg/errors"
. "github.com/smartystreets/goconvey/convey"
"github.com/MontFerret/ferret/pkg/runtime/values"
"github.com/MontFerret/ferret/pkg/runtime/values/types"
"github.com/MontFerret/ferret/pkg/stdlib/io/net/http"
)
func TestPOST(t *testing.T) {
SkipConvey("Should successfully make request", t, func() {
type User struct {
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
}
port := randPort()
server := &h.Server{
Addr: port,
Handler: h.HandlerFunc(func(w h.ResponseWriter, r *h.Request) {
var err error
defer func() {
if err != nil {
w.Write([]byte(err.Error()))
} else {
w.Write([]byte("OK"))
}
}()
if r.Method != "POST" {
err = errors.Errorf("Expected method to be POST, but got %s", r.Method)
return
}
data, err := ioutil.ReadAll(r.Body)
if err != nil {
return
}
user := User{}
err = json.Unmarshal(data, &user)
if err != nil {
return
}
if user.FirstName != "Rob" {
err = errors.Errorf("Expected FirstName to be Rob, but got %s", user.FirstName)
return
}
if user.LastName != "Pike" {
err = errors.Errorf("Expected LastName to be Pike, but got %s", user.LastName)
return
}
}),
}
ctx, cancel := context.WithCancel(context.Background())
go func() {
server.ListenAndServe()
}()
defer func() {
cancel()
server.Shutdown(ctx)
}()
b, err := json.Marshal(User{
FirstName: "Rob",
LastName: "Pike",
})
So(err, ShouldBeNil)
out, err := http.POST(ctx, values.NewObjectWith(
values.NewObjectProperty("url", values.NewString("http://127.0.0.1"+port)),
values.NewObjectProperty("body", values.NewBinary(b)),
))
So(err, ShouldBeNil)
So(out.Type().ID(), ShouldEqual, types.Binary.ID())
So(out.String(), ShouldEqual, "OK")
})
}

View File

@ -0,0 +1,14 @@
package http
import (
"context"
h "net/http"
"github.com/MontFerret/ferret/pkg/runtime/core"
)
// PUT makes a PUT request to the specified URL.
// @params url or (String) - path to file to write into.
func PUT(ctx context.Context, args ...core.Value) (core.Value, error) {
return execMethod(ctx, h.MethodPut, args)
}

View File

@ -0,0 +1,101 @@
package http_test
import (
"context"
"encoding/json"
"io/ioutil"
h "net/http"
"testing"
"github.com/pkg/errors"
. "github.com/smartystreets/goconvey/convey"
"github.com/MontFerret/ferret/pkg/runtime/values"
"github.com/MontFerret/ferret/pkg/runtime/values/types"
"github.com/MontFerret/ferret/pkg/stdlib/io/net/http"
)
func TestPUT(t *testing.T) {
SkipConvey("Should successfully make request", t, func() {
type User struct {
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
}
port := randPort()
server := &h.Server{
Addr: port,
Handler: h.HandlerFunc(func(w h.ResponseWriter, r *h.Request) {
var err error
defer func() {
if err != nil {
w.Write([]byte(err.Error()))
} else {
w.Write([]byte("OK"))
}
}()
if r.Method != "PUT" {
err = errors.Errorf("Expected method to be PUT, but got %s", r.Method)
return
}
data, err := ioutil.ReadAll(r.Body)
if err != nil {
return
}
user := User{}
err = json.Unmarshal(data, &user)
if err != nil {
return
}
if user.FirstName != "Rob" {
err = errors.Errorf("Expected FirstName to be Rob, but got %s", user.FirstName)
return
}
if user.LastName != "Pike" {
err = errors.Errorf("Expected LastName to be Pike, but got %s", user.LastName)
return
}
}),
}
ctx, cancel := context.WithCancel(context.Background())
go func() {
server.ListenAndServe()
}()
defer func() {
cancel()
server.Shutdown(ctx)
}()
b, err := json.Marshal(User{
FirstName: "Rob",
LastName: "Pike",
})
So(err, ShouldBeNil)
out, err := http.PUT(ctx, values.NewObjectWith(
values.NewObjectProperty("url", values.NewString("http://127.0.0.1"+port)),
values.NewObjectProperty("body", values.NewBinary(b)),
))
So(err, ShouldBeNil)
So(out.Type().ID(), ShouldEqual, types.Binary.ID())
So(out.String(), ShouldEqual, "OK")
})
}

View File

@ -0,0 +1,122 @@
package http
import (
"bytes"
"context"
"io/ioutil"
h "net/http"
"github.com/MontFerret/ferret/pkg/runtime/core"
"github.com/MontFerret/ferret/pkg/runtime/values"
"github.com/MontFerret/ferret/pkg/runtime/values/types"
)
type Params struct {
Method values.String
URL values.String
Headers *values.Object
Body values.Binary
}
func REQUEST(ctx context.Context, args ...core.Value) (core.Value, error) {
return execMethod(ctx, "", args)
}
func execMethod(ctx context.Context, method values.String, args []core.Value) (core.Value, error) {
if err := core.ValidateArgs(args, 1, 1); err != nil {
return values.None, err
}
arg := args[0]
if err := core.ValidateType(arg, types.Object); err != nil {
return values.None, err
}
p, err := newParamsFrom(arg.(*values.Object))
if err != nil {
return values.None, err
}
if method != "" {
p.Method = method
}
return makeRequest(ctx, p)
}
func makeRequest(ctx context.Context, params Params) (core.Value, error) {
client := h.Client{}
req, err := h.NewRequest(params.Method.String(), params.URL.String(), bytes.NewBuffer(params.Body))
if err != nil {
return values.None, err
}
req.Header = h.Header{}
if params.Headers != nil {
params.Headers.ForEach(func(value core.Value, key string) bool {
req.Header.Set(key, value.String())
return true
})
}
resp, err := client.Do(req.WithContext(ctx))
if err != nil {
return values.None, err
}
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
return values.None, err
}
defer resp.Body.Close()
return values.NewBinary(data), nil
}
func newParamsFrom(obj *values.Object) (Params, error) {
p := Params{}
method, exists := obj.Get("method")
if exists {
p.Method = values.ToString(method)
}
url, exists := obj.Get("url")
if !exists {
return Params{}, core.Error(core.ErrMissedArgument, ".url")
}
p.URL = values.NewString(url.String())
headers, exists := obj.Get("headers")
if exists {
if err := core.ValidateType(headers, types.Object); err != nil {
return Params{}, core.Error(err, ".headers")
}
p.Headers = headers.(*values.Object)
}
body, exists := obj.Get("body")
if exists {
if err := core.ValidateType(body, types.Binary); err != nil {
return Params{}, core.Error(err, ".body")
}
p.Body = body.(values.Binary)
}
return p, nil
}

17
pkg/stdlib/io/net/lib.go Normal file
View File

@ -0,0 +1,17 @@
package net
import (
"github.com/MontFerret/ferret/pkg/runtime/core"
"github.com/MontFerret/ferret/pkg/stdlib/io/net/http"
)
// RegisterLib register `NET` namespace functions.
func RegisterLib(ns core.Namespace) error {
io := ns.Namespace("NET")
if err := http.RegisterLib(io); err != nil {
return core.Error(err, "register `HTTP`")
}
return nil
}