From f0405e2879fcaf305c431816c5fe2ad215e38bd3 Mon Sep 17 00:00:00 2001 From: Julio Date: Wed, 30 Aug 2017 20:04:39 -0700 Subject: [PATCH 01/17] added and implemented HTTP errors HTTPError is for errors returned from http calls HTTPResponseError is only returned in get() and post() on non 200 responses. The general Net::HTTP.NewRequest() method will not use this --- vm/error.go | 5 +++++ vm/http.go | 18 +++++++----------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/vm/error.go b/vm/error.go index 8749fd7eb..4f43e6736 100644 --- a/vm/error.go +++ b/vm/error.go @@ -19,6 +19,11 @@ const ( UnsupportedMethodError = "UnsupportedMethodError" // ConstantAlreadyInitializedError means user re-declares twice ConstantAlreadyInitializedError = "ConstantAlreadyInitializedError" + //HTTPError is for general errors returned from http functions + HTTPError = "HTTP Error" + //HTTPResponseError is for non 200 responses in general contexts + //ex Net::HTTP.post() + HTTPResponseError = "HTTP Response Error" ) func (vm *VM) initErrorObject(errorType, format string, args ...interface{}) *Error { diff --git a/vm/http.go b/vm/http.go index c90b2fb23..89c88b9c9 100644 --- a/vm/http.go +++ b/vm/http.go @@ -71,12 +71,12 @@ func builtinHTTPClassMethods() []*BuiltInMethodObject { } resp, err := http.Get(uri.String()) - if err != nil { - return t.vm.initErrorObject(InternalError, err.Error()) + return t.vm.initErrorObject(HTTPError, err.Error()) } + if resp.StatusCode != http.StatusOK { - return t.vm.initErrorObject(InternalError, resp.Status) + return t.vm.initErrorObject(HTTPResponseError, resp.Status) } content, err := ioutil.ReadAll(resp.Body) @@ -98,22 +98,18 @@ func builtinHTTPClassMethods() []*BuiltInMethodObject { return t.vm.initErrorObject(ArgumentError, "Expect 3 arguments. got=%v", strconv.Itoa(len(args))) } - uri, err := url.Parse(args[0].(*StringObject).value) - if err != nil { - return t.vm.initErrorObject(ArgumentError, err.Error()) - } + host := args[0].(*StringObject).value contentType := args[1].(*StringObject).value body := args[2].(*StringObject).value - resp, err := http.Post(uri.String(), contentType, strings.NewReader(body)) - + resp, err := http.Post(host, contentType, strings.NewReader(body)) if err != nil { - return t.vm.initErrorObject(InternalError, err.Error()) + return t.vm.initErrorObject(HTTPError, err.Error()) } if resp.StatusCode != http.StatusOK { - return t.vm.initErrorObject(InternalError, resp.Status) + return t.vm.initErrorObject(HTTPResponseError, resp.Status) } content, err := ioutil.ReadAll(resp.Body) From 27252813e056e4365b64354ed6dab9a365ed9cb9 Mon Sep 17 00:00:00 2001 From: Julio Date: Wed, 6 Sep 2017 23:44:08 -0700 Subject: [PATCH 02/17] added Net::HTTP::Client built in class for sending fully configurable requests. It ha not been tested, this commit is for readability purposes --- samples/http-client.gb | 3 ++ vm/http.go | 44 ++++++++++++++- vm/http_client.go | 118 +++++++++++++++++++++++++++++++++++++++++ vm/http_client_test.go | 87 ++++++++++++++++++++++++++++++ 4 files changed, 251 insertions(+), 1 deletion(-) create mode 100644 samples/http-client.gb create mode 100644 vm/http_client.go create mode 100644 vm/http_client_test.go diff --git a/samples/http-client.gb b/samples/http-client.gb new file mode 100644 index 000000000..96a27ba5f --- /dev/null +++ b/samples/http-client.gb @@ -0,0 +1,3 @@ + + +puts("hello") \ No newline at end of file diff --git a/vm/http.go b/vm/http.go index 89c88b9c9..75393aeb9 100644 --- a/vm/http.go +++ b/vm/http.go @@ -12,6 +12,7 @@ import ( var ( httpRequestClass *RClass httpResponseClass *RClass + httpClientClass *RClass ) func initHTTPClass(vm *VM) { @@ -20,12 +21,14 @@ func initHTTPClass(vm *VM) { http.setBuiltInMethods(builtinHTTPClassMethods(), true) initRequestClass(vm, http) initResponseClass(vm, http) + initClientClass(vm, http) net.setClassConstant(http) // Use Goby code to extend request and response classes. vm.execGobyLib("net/http/response.gb") vm.execGobyLib("net/http/request.gb") + vm.execGobyLib("net/http/client.gb") } func initRequestClass(vm *VM, hc *RClass) *RClass { @@ -44,12 +47,22 @@ func initResponseClass(vm *VM, hc *RClass) *RClass { hc.setClassConstant(responseClass) builtinHTTPResponseInstanceMethods := []*BuiltInMethodObject{} - responseClass.setBuiltInMethods(builtinHTTPResponseInstanceMethods, false) + responseClass.setBuiltInMethods(builtinHTTPResponseInstanceMethods, true) httpResponseClass = responseClass return responseClass } +func initClientClass(vm *VM, hc *RClass) *RClass { + clientClass := vm.initializeClass("Client", false) + hc.setClassConstant(clientClass) + + clientClass.setBuiltInMethods(builtinHTTPClientClassMethods(), false) + + httpClientClass = clientClass + return clientClass +} + func builtinHTTPClassMethods() []*BuiltInMethodObject { return []*BuiltInMethodObject{ { @@ -119,6 +132,35 @@ func builtinHTTPClassMethods() []*BuiltInMethodObject { return t.vm.initErrorObject(InternalError, err.Error()) } + return t.vm.initStringObject(string(content)) + } + }, + }, { + // Sends a POST request to the target with type header and body. Returns the HTTP response as a string. + Name: "head", + Fn: func(receiver Object) builtinMethodBody { + return func(t *thread, args []Object, blockFrame *callFrame) Object { + if len(args) != 1 { + return t.vm.initErrorObject(ArgumentError, "Expect 1 arguments. got=%v", strconv.Itoa(len(args))) + } + + host := args[0].(*StringObject).value + + resp, err := http.Head(host) + if err != nil { + return t.vm.initErrorObject(HTTPError, err.Error()) + } + if resp.StatusCode != http.StatusOK { + return t.vm.initErrorObject(HTTPResponseError, resp.Status) + } + + content, err := ioutil.ReadAll(resp.Body) + resp.Body.Close() + + if err != nil { + return t.vm.initErrorObject(InternalError, err.Error()) + } + return t.vm.initStringObject(string(content)) } }, diff --git a/vm/http_client.go b/vm/http_client.go new file mode 100644 index 000000000..2e428c946 --- /dev/null +++ b/vm/http_client.go @@ -0,0 +1,118 @@ +package vm + +import ( + "strconv" + "net/http" + "errors" + "strings" + "fmt" + "io/ioutil" +) + +func builtinHTTPClientClassMethods() []*BuiltInMethodObject { + goClient := http.DefaultClient + + return []*BuiltInMethodObject{ + { + // Sends a GET request to the target and returns the HTTP response as a string. + Name: "send", + Fn: func(receiver Object) builtinMethodBody { + return func(t *thread, args []Object, blockFrame *callFrame) Object { + + if len(args) != 0 { + return t.vm.initErrorObject(ArgumentError, "Expect 0 arguments. got=%v", strconv.Itoa(len(args))) + + } + + req := httpRequestClass.initializeInstance() + + result := t.builtInMethodYield(blockFrame, req) + + if err, ok := result.Target.(*Error); ok { + fmt.Printf("Error: %s", err.Message) + return err //a Error object + } + + + goReq, err := requestGobyToGo(req) + if err != nil { + return t.vm.initErrorObject(ArgumentError, "Could not parse request object %v", req) + } + + resp, err := goClient.Do(goReq) + if err != nil { + return t.vm.initErrorObject(HTTPResponseError, "Could not get response: %s", err.Error()) + } + + gobyResp, err := responseGoToGoby(t, resp) + if err != nil { + return t.vm.initErrorObject(HTTPError, "Could not read response: %s", err.Error()) + } + + return gobyResp + } + }, + }, + } +} + +func requestGobyToGo(gobyReq *RObject) (*http.Request, error) { + //:method, :protocol, :body, :content_length, :transfer_encoding, :host, :path, :url, :params + uObj, ok := gobyReq.instanceVariableGet("url") +Er: + if !ok { + return nil, errors.New("could not parse paramehters") + } + + u := uObj.(*StringObject).value + + methodObj, ok := gobyReq.instanceVariableGet("method") + if !ok { + goto Er + } + + method := methodObj.(*StringObject).value + + var body string + if method == "GET" || method== "HEAD" { + bodyObj, ok := gobyReq.instanceVariableGet("body") + if !ok { + goto Er + } + + body = bodyObj.(*StringObject).value + } + + return http.NewRequest(method, u, strings.NewReader(body)) + +} + +func responseGoToGoby(t *thread, goResp *http.Response) (*RObject, error) { + gobyResp := httpResponseClass.initializeInstance() + + //attr_accessor :body, :status, :status_code, :protocol, :transfer_encoding, :http_version, :request_http_version, :request +//attr_reader :headers + + body, err := ioutil.ReadAll(goResp.Body) + if err != nil { + return nil, err + } + + gobyResp.instanceVariableSet("body", t.vm.initStringObject(string(body))) + gobyResp.instanceVariableSet("status_code", t.vm.initObjectFromGoType(goResp.StatusCode)) + gobyResp.instanceVariableSet("status", t.vm.initObjectFromGoType(goResp.Status)) + gobyResp.instanceVariableSet("protocol", t.vm.initObjectFromGoType(goResp.Proto)) + gobyResp.instanceVariableSet("transfer_encoding", t.vm.initObjectFromGoType(goResp.TransferEncoding)) + + + underHeaders := map[string]Object{} + + for k, v := range goResp.Header { + underHeaders[k] = t.vm.initObjectFromGoType(v) + } + + gobyResp.instanceVariableSet("headers", t.vm.initHashObject(underHeaders)) + + + return gobyResp, nil +} \ No newline at end of file diff --git a/vm/http_client_test.go b/vm/http_client_test.go new file mode 100644 index 000000000..ae3e1af12 --- /dev/null +++ b/vm/http_client_test.go @@ -0,0 +1,87 @@ +package vm + +import ( + "testing" + //"net/http/httptest" + //"net/http" + "fmt" + "io/ioutil" + "net/http" +) + +func TestHTTPClientObject(t *testing.T) { + + //blocking channel + c := make(chan bool, 1) + + //server to test off of + go startTestServer(c) + + tests := []struct { + input string + expected interface{} + }{ + //test get request + {` + require "net/http" + + c = Net::HTTP::Client.new + + c.send do |req| + req. + `, "GET Hello World"}, + {` + require "net/http" + + Net::HTTP.post("http://127.0.0.1:3000/index", "text/plain", "Hi Again") + `, "POST Hi Again"}, + } + + //block until server is ready + <-c + + for i, tt := range tests { + v := initTestVM() + evaluated := v.testEval(t, tt.input, getFilename()) + checkExpected(t, i, evaluated, tt.expected) + v.checkCFP(t, i, 0) + v.checkSP(t, i, 1) + } +} + +func TestHTTPClientObjectFail(t *testing.T) { + //blocking channel + c := make(chan bool, 1) + + //server to test off of + go startTestServer(c) + + testsFail := []errorTestCase{ + {` + require "net/http" + + Net::HTTP::Client.new("http://127.0.0.1:3000/error") + `, "InternalError: 404 Not Found", 4}, + {` + require "net/http" + + Net::HTTP.post("http://127.0.0.1:3000/error", "text/plain", "Let me down") + `, "InternalError: 404 Not Found", 4}, + {` + require "net/http" + + Net::HTTP.post("http://127.0.0.1:3001", "text/plain", "Let me down") + `, "InternalError: Post http://127.0.0.1:3001: dial tcp 127.0.0.1:3001: getsockopt: connection refused", 4}, + } + + //block until server is ready + <-c + + for i, tt := range testsFail { + v := initTestVM() + evaluated := v.testEval(t, tt.input, getFilename()) + checkError(t, i, evaluated, tt.expected, getFilename(), tt.errorLine) + v.checkCFP(t, i, 1) + v.checkSP(t, i, 1) + } +} From 0348240ebe6c4b5b5d3875cbbe4a4e33ae607b57 Mon Sep 17 00:00:00 2001 From: Julio Date: Wed, 30 Aug 2017 20:04:39 -0700 Subject: [PATCH 03/17] added and implemented HTTP errors HTTPError is for errors returned from http calls HTTPResponseError is only returned in get() and post() on non 200 responses. The general Net::HTTP.NewRequest() method will not use this --- vm/http.go | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/vm/http.go b/vm/http.go index 0fa70e495..409162318 100644 --- a/vm/http.go +++ b/vm/http.go @@ -72,10 +72,10 @@ func builtinHTTPClassMethods() []*BuiltInMethodObject { } resp, err := http.Get(uri.String()) - if err != nil { return t.vm.initErrorObject(errors.InternalError, err.Error()) } + if resp.StatusCode != http.StatusOK { return t.vm.initErrorObject(errors.InternalError, resp.Status) } @@ -99,17 +99,14 @@ func builtinHTTPClassMethods() []*BuiltInMethodObject { return t.vm.initErrorObject(errors.ArgumentError, "Expect 3 arguments. got=%v", strconv.Itoa(len(args))) } - uri, err := url.Parse(args[0].(*StringObject).value) - if err != nil { - return t.vm.initErrorObject(errors.ArgumentError, err.Error()) - } + + host := args[0].(*StringObject).value contentType := args[1].(*StringObject).value body := args[2].(*StringObject).value - resp, err := http.Post(uri.String(), contentType, strings.NewReader(body)) - + resp, err := http.Post(host, contentType, strings.NewReader(body)) if err != nil { return t.vm.initErrorObject(errors.InternalError, err.Error()) } From d7eea9056b008c6d4c30c6f6cc81694e3fd04ab7 Mon Sep 17 00:00:00 2001 From: Julio Date: Wed, 6 Sep 2017 23:44:08 -0700 Subject: [PATCH 04/17] added Net::HTTP::Client built in class for sending fully configurable requests. It ha not been tested, this commit is for readability purposes --- samples/http-client.gb | 3 ++ vm/http.go | 44 ++++++++++++++- vm/http_client.go | 118 +++++++++++++++++++++++++++++++++++++++++ vm/http_client_test.go | 87 ++++++++++++++++++++++++++++++ 4 files changed, 251 insertions(+), 1 deletion(-) create mode 100644 samples/http-client.gb create mode 100644 vm/http_client.go create mode 100644 vm/http_client_test.go diff --git a/samples/http-client.gb b/samples/http-client.gb new file mode 100644 index 000000000..96a27ba5f --- /dev/null +++ b/samples/http-client.gb @@ -0,0 +1,3 @@ + + +puts("hello") \ No newline at end of file diff --git a/vm/http.go b/vm/http.go index 409162318..a623834e9 100644 --- a/vm/http.go +++ b/vm/http.go @@ -13,6 +13,7 @@ import ( var ( httpRequestClass *RClass httpResponseClass *RClass + httpClientClass *RClass ) func initHTTPClass(vm *VM) { @@ -21,12 +22,14 @@ func initHTTPClass(vm *VM) { http.setBuiltInMethods(builtinHTTPClassMethods(), true) initRequestClass(vm, http) initResponseClass(vm, http) + initClientClass(vm, http) net.setClassConstant(http) // Use Goby code to extend request and response classes. vm.execGobyLib("net/http/response.gb") vm.execGobyLib("net/http/request.gb") + vm.execGobyLib("net/http/client.gb") } func initRequestClass(vm *VM, hc *RClass) *RClass { @@ -45,12 +48,22 @@ func initResponseClass(vm *VM, hc *RClass) *RClass { hc.setClassConstant(responseClass) builtinHTTPResponseInstanceMethods := []*BuiltInMethodObject{} - responseClass.setBuiltInMethods(builtinHTTPResponseInstanceMethods, false) + responseClass.setBuiltInMethods(builtinHTTPResponseInstanceMethods, true) httpResponseClass = responseClass return responseClass } +func initClientClass(vm *VM, hc *RClass) *RClass { + clientClass := vm.initializeClass("Client", false) + hc.setClassConstant(clientClass) + + clientClass.setBuiltInMethods(builtinHTTPClientClassMethods(), false) + + httpClientClass = clientClass + return clientClass +} + func builtinHTTPClassMethods() []*BuiltInMethodObject { return []*BuiltInMethodObject{ { @@ -121,6 +134,35 @@ func builtinHTTPClassMethods() []*BuiltInMethodObject { return t.vm.initErrorObject(errors.InternalError, err.Error()) } + return t.vm.initStringObject(string(content)) + } + }, + }, { + // Sends a POST request to the target with type header and body. Returns the HTTP response as a string. + Name: "head", + Fn: func(receiver Object) builtinMethodBody { + return func(t *thread, args []Object, blockFrame *callFrame) Object { + if len(args) != 1 { + return t.vm.initErrorObject(ArgumentError, "Expect 1 arguments. got=%v", strconv.Itoa(len(args))) + } + + host := args[0].(*StringObject).value + + resp, err := http.Head(host) + if err != nil { + return t.vm.initErrorObject(HTTPError, err.Error()) + } + if resp.StatusCode != http.StatusOK { + return t.vm.initErrorObject(HTTPResponseError, resp.Status) + } + + content, err := ioutil.ReadAll(resp.Body) + resp.Body.Close() + + if err != nil { + return t.vm.initErrorObject(InternalError, err.Error()) + } + return t.vm.initStringObject(string(content)) } }, diff --git a/vm/http_client.go b/vm/http_client.go new file mode 100644 index 000000000..2e428c946 --- /dev/null +++ b/vm/http_client.go @@ -0,0 +1,118 @@ +package vm + +import ( + "strconv" + "net/http" + "errors" + "strings" + "fmt" + "io/ioutil" +) + +func builtinHTTPClientClassMethods() []*BuiltInMethodObject { + goClient := http.DefaultClient + + return []*BuiltInMethodObject{ + { + // Sends a GET request to the target and returns the HTTP response as a string. + Name: "send", + Fn: func(receiver Object) builtinMethodBody { + return func(t *thread, args []Object, blockFrame *callFrame) Object { + + if len(args) != 0 { + return t.vm.initErrorObject(ArgumentError, "Expect 0 arguments. got=%v", strconv.Itoa(len(args))) + + } + + req := httpRequestClass.initializeInstance() + + result := t.builtInMethodYield(blockFrame, req) + + if err, ok := result.Target.(*Error); ok { + fmt.Printf("Error: %s", err.Message) + return err //a Error object + } + + + goReq, err := requestGobyToGo(req) + if err != nil { + return t.vm.initErrorObject(ArgumentError, "Could not parse request object %v", req) + } + + resp, err := goClient.Do(goReq) + if err != nil { + return t.vm.initErrorObject(HTTPResponseError, "Could not get response: %s", err.Error()) + } + + gobyResp, err := responseGoToGoby(t, resp) + if err != nil { + return t.vm.initErrorObject(HTTPError, "Could not read response: %s", err.Error()) + } + + return gobyResp + } + }, + }, + } +} + +func requestGobyToGo(gobyReq *RObject) (*http.Request, error) { + //:method, :protocol, :body, :content_length, :transfer_encoding, :host, :path, :url, :params + uObj, ok := gobyReq.instanceVariableGet("url") +Er: + if !ok { + return nil, errors.New("could not parse paramehters") + } + + u := uObj.(*StringObject).value + + methodObj, ok := gobyReq.instanceVariableGet("method") + if !ok { + goto Er + } + + method := methodObj.(*StringObject).value + + var body string + if method == "GET" || method== "HEAD" { + bodyObj, ok := gobyReq.instanceVariableGet("body") + if !ok { + goto Er + } + + body = bodyObj.(*StringObject).value + } + + return http.NewRequest(method, u, strings.NewReader(body)) + +} + +func responseGoToGoby(t *thread, goResp *http.Response) (*RObject, error) { + gobyResp := httpResponseClass.initializeInstance() + + //attr_accessor :body, :status, :status_code, :protocol, :transfer_encoding, :http_version, :request_http_version, :request +//attr_reader :headers + + body, err := ioutil.ReadAll(goResp.Body) + if err != nil { + return nil, err + } + + gobyResp.instanceVariableSet("body", t.vm.initStringObject(string(body))) + gobyResp.instanceVariableSet("status_code", t.vm.initObjectFromGoType(goResp.StatusCode)) + gobyResp.instanceVariableSet("status", t.vm.initObjectFromGoType(goResp.Status)) + gobyResp.instanceVariableSet("protocol", t.vm.initObjectFromGoType(goResp.Proto)) + gobyResp.instanceVariableSet("transfer_encoding", t.vm.initObjectFromGoType(goResp.TransferEncoding)) + + + underHeaders := map[string]Object{} + + for k, v := range goResp.Header { + underHeaders[k] = t.vm.initObjectFromGoType(v) + } + + gobyResp.instanceVariableSet("headers", t.vm.initHashObject(underHeaders)) + + + return gobyResp, nil +} \ No newline at end of file diff --git a/vm/http_client_test.go b/vm/http_client_test.go new file mode 100644 index 000000000..ae3e1af12 --- /dev/null +++ b/vm/http_client_test.go @@ -0,0 +1,87 @@ +package vm + +import ( + "testing" + //"net/http/httptest" + //"net/http" + "fmt" + "io/ioutil" + "net/http" +) + +func TestHTTPClientObject(t *testing.T) { + + //blocking channel + c := make(chan bool, 1) + + //server to test off of + go startTestServer(c) + + tests := []struct { + input string + expected interface{} + }{ + //test get request + {` + require "net/http" + + c = Net::HTTP::Client.new + + c.send do |req| + req. + `, "GET Hello World"}, + {` + require "net/http" + + Net::HTTP.post("http://127.0.0.1:3000/index", "text/plain", "Hi Again") + `, "POST Hi Again"}, + } + + //block until server is ready + <-c + + for i, tt := range tests { + v := initTestVM() + evaluated := v.testEval(t, tt.input, getFilename()) + checkExpected(t, i, evaluated, tt.expected) + v.checkCFP(t, i, 0) + v.checkSP(t, i, 1) + } +} + +func TestHTTPClientObjectFail(t *testing.T) { + //blocking channel + c := make(chan bool, 1) + + //server to test off of + go startTestServer(c) + + testsFail := []errorTestCase{ + {` + require "net/http" + + Net::HTTP::Client.new("http://127.0.0.1:3000/error") + `, "InternalError: 404 Not Found", 4}, + {` + require "net/http" + + Net::HTTP.post("http://127.0.0.1:3000/error", "text/plain", "Let me down") + `, "InternalError: 404 Not Found", 4}, + {` + require "net/http" + + Net::HTTP.post("http://127.0.0.1:3001", "text/plain", "Let me down") + `, "InternalError: Post http://127.0.0.1:3001: dial tcp 127.0.0.1:3001: getsockopt: connection refused", 4}, + } + + //block until server is ready + <-c + + for i, tt := range testsFail { + v := initTestVM() + evaluated := v.testEval(t, tt.input, getFilename()) + checkError(t, i, evaluated, tt.expected, getFilename(), tt.errorLine) + v.checkCFP(t, i, 1) + v.checkSP(t, i, 1) + } +} From 4964448a9a764718774b5429f83509a58da8e6ea Mon Sep 17 00:00:00 2001 From: Julio Date: Thu, 7 Sep 2017 12:42:10 -0700 Subject: [PATCH 05/17] created positive tests and fixed the client class to pass the tests --- vm/http.go | 1 - vm/http_client.go | 36 ++++++++++++++++++------------------ vm/http_client_test.go | 30 ++++++++++++++++++------------ 3 files changed, 36 insertions(+), 31 deletions(-) diff --git a/vm/http.go b/vm/http.go index bedbc76f6..3e65b9011 100644 --- a/vm/http.go +++ b/vm/http.go @@ -29,7 +29,6 @@ func initHTTPClass(vm *VM) { // Use Goby code to extend request and response classes. vm.execGobyLib("net/http/response.gb") vm.execGobyLib("net/http/request.gb") - vm.execGobyLib("net/http/client.gb") } func initRequestClass(vm *VM, hc *RClass) *RClass { diff --git a/vm/http_client.go b/vm/http_client.go index 2e428c946..18ff134e2 100644 --- a/vm/http_client.go +++ b/vm/http_client.go @@ -7,6 +7,7 @@ import ( "strings" "fmt" "io/ioutil" + gerrors "github.com/goby-lang/goby/vm/errors" ) func builtinHTTPClientClassMethods() []*BuiltInMethodObject { @@ -20,7 +21,7 @@ func builtinHTTPClientClassMethods() []*BuiltInMethodObject { return func(t *thread, args []Object, blockFrame *callFrame) Object { if len(args) != 0 { - return t.vm.initErrorObject(ArgumentError, "Expect 0 arguments. got=%v", strconv.Itoa(len(args))) + return t.vm.initErrorObject(gerrors.ArgumentError, "Expect 0 arguments. got=%v", strconv.Itoa(len(args))) } @@ -36,17 +37,17 @@ func builtinHTTPClientClassMethods() []*BuiltInMethodObject { goReq, err := requestGobyToGo(req) if err != nil { - return t.vm.initErrorObject(ArgumentError, "Could not parse request object %v", req) + return t.vm.initErrorObject(gerrors.ArgumentError, "Could not parse request object %s", err.Error()) } resp, err := goClient.Do(goReq) if err != nil { - return t.vm.initErrorObject(HTTPResponseError, "Could not get response: %s", err.Error()) + return t.vm.initErrorObject(gerrors.InternalError, "Could not get response: %s", err.Error()) } gobyResp, err := responseGoToGoby(t, resp) if err != nil { - return t.vm.initErrorObject(HTTPError, "Could not read response: %s", err.Error()) + return t.vm.initErrorObject(gerrors.InternalError, "Could not read response: %s", err.Error()) } return gobyResp @@ -58,26 +59,25 @@ func builtinHTTPClientClassMethods() []*BuiltInMethodObject { func requestGobyToGo(gobyReq *RObject) (*http.Request, error) { //:method, :protocol, :body, :content_length, :transfer_encoding, :host, :path, :url, :params - uObj, ok := gobyReq.instanceVariableGet("url") -Er: + uObj, ok := gobyReq.instanceVariableGet("@url") if !ok { - return nil, errors.New("could not parse paramehters") + return nil, errors.New("could not get url") } u := uObj.(*StringObject).value - methodObj, ok := gobyReq.instanceVariableGet("method") + methodObj, ok := gobyReq.instanceVariableGet("@method") if !ok { - goto Er + return nil, errors.New("could not get method") } method := methodObj.(*StringObject).value var body string - if method == "GET" || method== "HEAD" { - bodyObj, ok := gobyReq.instanceVariableGet("body") + if !(method == "GET" || method== "HEAD") { + bodyObj, ok := gobyReq.instanceVariableGet("@body") if !ok { - goto Er + return nil, errors.New("could not get body") } body = bodyObj.(*StringObject).value @@ -98,11 +98,11 @@ func responseGoToGoby(t *thread, goResp *http.Response) (*RObject, error) { return nil, err } - gobyResp.instanceVariableSet("body", t.vm.initStringObject(string(body))) - gobyResp.instanceVariableSet("status_code", t.vm.initObjectFromGoType(goResp.StatusCode)) - gobyResp.instanceVariableSet("status", t.vm.initObjectFromGoType(goResp.Status)) - gobyResp.instanceVariableSet("protocol", t.vm.initObjectFromGoType(goResp.Proto)) - gobyResp.instanceVariableSet("transfer_encoding", t.vm.initObjectFromGoType(goResp.TransferEncoding)) + gobyResp.instanceVariableSet("@body", t.vm.initStringObject(string(body))) + gobyResp.instanceVariableSet("@status_code", t.vm.initObjectFromGoType(goResp.StatusCode)) + gobyResp.instanceVariableSet("@status", t.vm.initObjectFromGoType(goResp.Status)) + gobyResp.instanceVariableSet("@protocol", t.vm.initObjectFromGoType(goResp.Proto)) + gobyResp.instanceVariableSet("@transfer_encoding", t.vm.initObjectFromGoType(goResp.TransferEncoding)) underHeaders := map[string]Object{} @@ -111,7 +111,7 @@ func responseGoToGoby(t *thread, goResp *http.Response) (*RObject, error) { underHeaders[k] = t.vm.initObjectFromGoType(v) } - gobyResp.instanceVariableSet("headers", t.vm.initHashObject(underHeaders)) + gobyResp.instanceVariableSet("@headers", t.vm.initHashObject(underHeaders)) return gobyResp, nil diff --git a/vm/http_client_test.go b/vm/http_client_test.go index ae3e1af12..a7d7c9a38 100644 --- a/vm/http_client_test.go +++ b/vm/http_client_test.go @@ -1,13 +1,6 @@ package vm -import ( - "testing" - //"net/http/httptest" - //"net/http" - "fmt" - "io/ioutil" - "net/http" -) +import "testing" func TestHTTPClientObject(t *testing.T) { @@ -27,13 +20,25 @@ func TestHTTPClientObject(t *testing.T) { c = Net::HTTP::Client.new - c.send do |req| - req. + res = c.send do |req| + req.url = "http://127.0.0.1:3000/index" + req.method = "GET" + end + + res.body `, "GET Hello World"}, {` require "net/http" - Net::HTTP.post("http://127.0.0.1:3000/index", "text/plain", "Hi Again") + c = Net::HTTP::Client.new + + res = c.send do |req| + req.url = "http://127.0.0.1:3000/index" + req.method = "POST" + req.body = "Hi Again" + end + + res.body `, "POST Hi Again"}, } @@ -48,7 +53,7 @@ func TestHTTPClientObject(t *testing.T) { v.checkSP(t, i, 1) } } - +/* func TestHTTPClientObjectFail(t *testing.T) { //blocking channel c := make(chan bool, 1) @@ -85,3 +90,4 @@ func TestHTTPClientObjectFail(t *testing.T) { v.checkSP(t, i, 1) } } +*/ \ No newline at end of file From 5ce146eb24f3e0298c30451e84f4a6b649ac9953 Mon Sep 17 00:00:00 2001 From: Julio Date: Thu, 7 Sep 2017 12:45:31 -0700 Subject: [PATCH 06/17] added a sample usage of the http client --- samples/http-client.gb | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/samples/http-client.gb b/samples/http-client.gb index 96a27ba5f..bca2ce2d0 100644 --- a/samples/http-client.gb +++ b/samples/http-client.gb @@ -1,3 +1,10 @@ +require "net/http" +c = Net::HTTP::Client.new -puts("hello") \ No newline at end of file +res = c.send do |req| + req.url = "https://google.com" + req.method = "GET" +end + +puts res.body \ No newline at end of file From 69c1789b8dcd319a809df1809e81dce53b6c4f4a Mon Sep 17 00:00:00 2001 From: Julio Date: Thu, 7 Sep 2017 15:19:02 -0700 Subject: [PATCH 07/17] removed references to HTTPError - was causing panic added error tests and is passing --- vm/http_client.go | 7 ++++--- vm/http_client_test.go | 39 +++++++++++++++++++++------------------ 2 files changed, 25 insertions(+), 21 deletions(-) diff --git a/vm/http_client.go b/vm/http_client.go index 18ff134e2..ef528088b 100644 --- a/vm/http_client.go +++ b/vm/http_client.go @@ -37,17 +37,18 @@ func builtinHTTPClientClassMethods() []*BuiltInMethodObject { goReq, err := requestGobyToGo(req) if err != nil { - return t.vm.initErrorObject(gerrors.ArgumentError, "Could not parse request object %s", err.Error()) + return t.vm.initErrorObject(gerrors.ArgumentError, "Request object incomplete object %s", err) } resp, err := goClient.Do(goReq) if err != nil { - return t.vm.initErrorObject(gerrors.InternalError, "Could not get response: %s", err.Error()) + fmt.Println("do error: ", err) + return t.vm.initErrorObject(gerrors.InternalError, "Could not get response: %s", err) } gobyResp, err := responseGoToGoby(t, resp) if err != nil { - return t.vm.initErrorObject(gerrors.InternalError, "Could not read response: %s", err.Error()) + return t.vm.initErrorObject(gerrors.InternalError, "Could not read response: %s", err) } return gobyResp diff --git a/vm/http_client_test.go b/vm/http_client_test.go index a7d7c9a38..523c51e09 100644 --- a/vm/http_client_test.go +++ b/vm/http_client_test.go @@ -40,6 +40,18 @@ func TestHTTPClientObject(t *testing.T) { res.body `, "POST Hi Again"}, + {` + require "net/http" + + c = Net::HTTP::Client.new + + res = c.send do |req| + req.url = "http://127.0.0.1:3000/error" + req.method = "GET" + end + + res.status_code + `, 404}, } //block until server is ready @@ -53,34 +65,26 @@ func TestHTTPClientObject(t *testing.T) { v.checkSP(t, i, 1) } } -/* -func TestHTTPClientObjectFail(t *testing.T) { - //blocking channel - c := make(chan bool, 1) - //server to test off of - go startTestServer(c) +func TestHTTPClientObjectFail(t *testing.T) { testsFail := []errorTestCase{ {` require "net/http" - Net::HTTP::Client.new("http://127.0.0.1:3000/error") - `, "InternalError: 404 Not Found", 4}, - {` - require "net/http" + c = Net::HTTP::Client.new - Net::HTTP.post("http://127.0.0.1:3000/error", "text/plain", "Let me down") - `, "InternalError: 404 Not Found", 4}, - {` - require "net/http" + res = c.send do |req| + req.url = "http://127.0.0.1:3001" + req.method = "GET" + end - Net::HTTP.post("http://127.0.0.1:3001", "text/plain", "Let me down") - `, "InternalError: Post http://127.0.0.1:3001: dial tcp 127.0.0.1:3001: getsockopt: connection refused", 4}, + res + `, "InternalError: Could not get response: Get http://127.0.0.1:3001: dial tcp 127.0.0.1:3001: getsockopt: connection refused", 6}, } //block until server is ready - <-c + //<-c for i, tt := range testsFail { v := initTestVM() @@ -90,4 +94,3 @@ func TestHTTPClientObjectFail(t *testing.T) { v.checkSP(t, i, 1) } } -*/ \ No newline at end of file From 0af568cab94ab911b7c7a6bc50025be425ce8544 Mon Sep 17 00:00:00 2001 From: Julio Date: Fri, 8 Sep 2017 22:07:22 -0700 Subject: [PATCH 08/17] fixed confusion with class methods Custom errors with fmt.Errorf go fmt on the changed files --- vm/http.go | 43 +++++++++++++++++++++++++++++++++---------- vm/http_client.go | 21 +++++++++------------ 2 files changed, 42 insertions(+), 22 deletions(-) diff --git a/vm/http.go b/vm/http.go index 3e65b9011..f557658d1 100644 --- a/vm/http.go +++ b/vm/http.go @@ -13,7 +13,7 @@ import ( var ( httpRequestClass *RClass httpResponseClass *RClass - httpClientClass *RClass + httpClientClass *RClass ) func initHTTPClass(vm *VM) { @@ -47,7 +47,7 @@ func initResponseClass(vm *VM, hc *RClass) *RClass { hc.setClassConstant(responseClass) builtinHTTPResponseInstanceMethods := []*BuiltInMethodObject{} - responseClass.setBuiltInMethods(builtinHTTPResponseInstanceMethods, true) + responseClass.setBuiltInMethods(builtinHTTPResponseInstanceMethods, false) httpResponseClass = responseClass return responseClass @@ -70,14 +70,22 @@ func builtinHTTPClassMethods() []*BuiltInMethodObject { Name: "get", Fn: func(receiver Object) builtinMethodBody { return func(t *thread, args []Object, blockFrame *callFrame) Object { + arg0, ok := args[0].(*StringObject) + if !ok { + return t.vm.initErrorObject(errors.ArgumentError, "Argument 0 must be a string") + } - uri, err := url.Parse(args[0].(*StringObject).value) + uri, err := url.Parse(arg0.value) if len(args) > 1 { var arr []string for _, v := range args[1:] { - arr = append(arr, v.(*StringObject).value) + argn, ok := v.(*StringObject) + if !ok { + return t.vm.initErrorObject(errors.ArgumentError, "Splat arguments must be a string") + } + arr = append(arr, argn.value) } uri.Path = path.Join(arr...) @@ -111,11 +119,23 @@ func builtinHTTPClassMethods() []*BuiltInMethodObject { return t.vm.initErrorObject(errors.ArgumentError, "Expect 3 arguments. got=%v", strconv.Itoa(len(args))) } - host := args[0].(*StringObject).value + arg0, ok := args[0].(*StringObject) + if !ok { + return t.vm.initErrorObject(errors.ArgumentError, "Argument 0 must be a string") + } + host := arg0.value - contentType := args[1].(*StringObject).value + arg1, ok := args[1].(*StringObject) + if !ok { + return t.vm.initErrorObject(errors.ArgumentError, "Argument 1 must be a string") + } + contentType := arg1.value - body := args[2].(*StringObject).value + arg2, ok := args[2].(*StringObject) + if !ok { + return t.vm.initErrorObject(errors.ArgumentError, "Argument 2 must be a string") + } + body := arg2.value resp, err := http.Post(host, contentType, strings.NewReader(body)) if err != nil { @@ -144,14 +164,17 @@ func builtinHTTPClassMethods() []*BuiltInMethodObject { return t.vm.initErrorObject(errors.ArgumentError, "Expect 1 arguments. got=%v", strconv.Itoa(len(args))) } - host := args[0].(*StringObject).value + host, ok := args[0].(*StringObject) + if !ok { + return t.vm.initErrorObject(errors.ArgumentError, "Argument 0 must be a string") + } - _, err := http.Head(host) + _, err := http.Head(host.value) if err != nil { return t.vm.initErrorObject(errors.InternalError, err.Error()) } - //TODO: make return value a map + //TODO: make return value a map of headers return t.vm.initStringObject("") } }, diff --git a/vm/http_client.go b/vm/http_client.go index ef528088b..0a087907a 100644 --- a/vm/http_client.go +++ b/vm/http_client.go @@ -1,13 +1,13 @@ package vm import ( - "strconv" - "net/http" "errors" - "strings" "fmt" - "io/ioutil" gerrors "github.com/goby-lang/goby/vm/errors" + "io/ioutil" + "net/http" + "strconv" + "strings" ) func builtinHTTPClientClassMethods() []*BuiltInMethodObject { @@ -34,7 +34,6 @@ func builtinHTTPClientClassMethods() []*BuiltInMethodObject { return err //a Error object } - goReq, err := requestGobyToGo(req) if err != nil { return t.vm.initErrorObject(gerrors.ArgumentError, "Request object incomplete object %s", err) @@ -69,16 +68,16 @@ func requestGobyToGo(gobyReq *RObject) (*http.Request, error) { methodObj, ok := gobyReq.instanceVariableGet("@method") if !ok { - return nil, errors.New("could not get method") + return nil, fmt.Errorf("could not get method") } method := methodObj.(*StringObject).value var body string - if !(method == "GET" || method== "HEAD") { + if !(method == "GET" || method == "HEAD") { bodyObj, ok := gobyReq.instanceVariableGet("@body") if !ok { - return nil, errors.New("could not get body") + return nil, fmt.Errorf("could not get body") } body = bodyObj.(*StringObject).value @@ -92,7 +91,7 @@ func responseGoToGoby(t *thread, goResp *http.Response) (*RObject, error) { gobyResp := httpResponseClass.initializeInstance() //attr_accessor :body, :status, :status_code, :protocol, :transfer_encoding, :http_version, :request_http_version, :request -//attr_reader :headers + //attr_reader :headers body, err := ioutil.ReadAll(goResp.Body) if err != nil { @@ -105,7 +104,6 @@ func responseGoToGoby(t *thread, goResp *http.Response) (*RObject, error) { gobyResp.instanceVariableSet("@protocol", t.vm.initObjectFromGoType(goResp.Proto)) gobyResp.instanceVariableSet("@transfer_encoding", t.vm.initObjectFromGoType(goResp.TransferEncoding)) - underHeaders := map[string]Object{} for k, v := range goResp.Header { @@ -114,6 +112,5 @@ func responseGoToGoby(t *thread, goResp *http.Response) (*RObject, error) { gobyResp.instanceVariableSet("@headers", t.vm.initHashObject(underHeaders)) - return gobyResp, nil -} \ No newline at end of file +} From f33317c14542b0acfa4d0a64958e2ef3768d832f Mon Sep 17 00:00:00 2001 From: Julio Date: Fri, 8 Sep 2017 22:46:01 -0700 Subject: [PATCH 09/17] refactoring in http_client.go file --- vm/http.go | 2 +- vm/http_client.go | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/vm/http.go b/vm/http.go index d4f7edef7..80312b48b 100644 --- a/vm/http.go +++ b/vm/http.go @@ -181,7 +181,7 @@ func initClientClass(vm *VM, hc *RClass) *RClass { clientClass := vm.initializeClass("Client", false) hc.setClassConstant(clientClass) - clientClass.setBuiltinMethods(builtinHTTPClientClassMethods(), false) + clientClass.setBuiltinMethods(builtinHTTPClientInstanceMethods(), false) httpClientClass = clientClass return clientClass diff --git a/vm/http_client.go b/vm/http_client.go index 0a087907a..056a87590 100644 --- a/vm/http_client.go +++ b/vm/http_client.go @@ -10,10 +10,11 @@ import ( "strings" ) -func builtinHTTPClientClassMethods() []*BuiltInMethodObject { +// Instance methods -------------------------------------------------------- +func builtinHTTPClientInstanceMethods() []*BuiltinMethodObject { goClient := http.DefaultClient - return []*BuiltInMethodObject{ + return []*BuiltinMethodObject{ { // Sends a GET request to the target and returns the HTTP response as a string. Name: "send", @@ -27,7 +28,7 @@ func builtinHTTPClientClassMethods() []*BuiltInMethodObject { req := httpRequestClass.initializeInstance() - result := t.builtInMethodYield(blockFrame, req) + result := t.builtinMethodYield(blockFrame, req) if err, ok := result.Target.(*Error); ok { fmt.Printf("Error: %s", err.Message) @@ -87,6 +88,8 @@ func requestGobyToGo(gobyReq *RObject) (*http.Request, error) { } +// Other helper functions ----------------------------------------------- + func responseGoToGoby(t *thread, goResp *http.Response) (*RObject, error) { gobyResp := httpResponseClass.initializeInstance() From f67a2697406b663345e901417f3827367de8f1db Mon Sep 17 00:00:00 2001 From: Julio Date: Fri, 8 Sep 2017 22:52:53 -0700 Subject: [PATCH 10/17] re-added client initialization in http initialization. Was accidentally removed during merge --- vm/http.go | 1 + 1 file changed, 1 insertion(+) diff --git a/vm/http.go b/vm/http.go index 80312b48b..90f4772bd 100644 --- a/vm/http.go +++ b/vm/http.go @@ -147,6 +147,7 @@ func initHTTPClass(vm *VM) { http.setBuiltinMethods(builtinHTTPClassMethods(), true) initRequestClass(vm, http) initResponseClass(vm, http) + initClientClass(vm, http) net.setClassConstant(http) From fd7e1606b789f20160cfe3f2045c4d78d444f04e Mon Sep 17 00:00:00 2001 From: Julio Date: Sat, 9 Sep 2017 17:03:57 -0700 Subject: [PATCH 11/17] Changed error messages to be more descriptive and coherent Chaneged fail tests to reflect new error messages http.head() now returns a map of headers --- vm/error.go | 2 +- vm/errors/error.go | 2 ++ vm/http.go | 44 +++++++++++++++++++++++------------------- vm/http_client.go | 14 ++++++-------- vm/http_client_test.go | 2 +- vm/http_test.go | 6 +++--- 6 files changed, 37 insertions(+), 33 deletions(-) diff --git a/vm/error.go b/vm/error.go index 1218670a9..0057b4f77 100644 --- a/vm/error.go +++ b/vm/error.go @@ -51,7 +51,7 @@ func (vm *VM) initErrorObject(errorType, format string, args ...interface{}) *Er } func (vm *VM) initErrorClasses() { - errTypes := []string{errors.InternalError, errors.ArgumentError, errors.NameError, errors.TypeError, errors.UndefinedMethodError, errors.UnsupportedMethodError, errors.ConstantAlreadyInitializedError} + errTypes := []string{errors.InternalError, errors.ArgumentError, errors.NameError, errors.TypeError, errors.UndefinedMethodError, errors.UnsupportedMethodError, errors.ConstantAlreadyInitializedError, errors.HTTPError} for _, errType := range errTypes { c := vm.initializeClass(errType, false) diff --git a/vm/errors/error.go b/vm/errors/error.go index 53980ad3e..ef8cc5838 100644 --- a/vm/errors/error.go +++ b/vm/errors/error.go @@ -15,6 +15,8 @@ const ( UnsupportedMethodError = "UnsupportedMethodError" // ConstantAlreadyInitializedError means user re-declares twice ConstantAlreadyInitializedError = "ConstantAlreadyInitializedError" + // HTTPError is returned when when a request fails to return a proper response + HTTPError = "HTTPError" ) /* diff --git a/vm/http.go b/vm/http.go index 90f4772bd..e5c398b3c 100644 --- a/vm/http.go +++ b/vm/http.go @@ -27,7 +27,7 @@ func builtinHTTPClassMethods() []*BuiltinMethodObject { return func(t *thread, args []Object, blockFrame *callFrame) Object { arg0, ok := args[0].(*StringObject) if !ok { - return t.vm.initErrorObject(errors.ArgumentError, "Argument 0 must be a string") + return t.vm.initErrorObject(errors.ArgumentError, "Expect argument 0 to be string, got: %s", args[0].Class().Name) } uri, err := url.Parse(arg0.value) @@ -35,10 +35,10 @@ func builtinHTTPClassMethods() []*BuiltinMethodObject { if len(args) > 1 { var arr []string - for _, v := range args[1:] { + for i, v := range args[1:] { argn, ok := v.(*StringObject) if !ok { - return t.vm.initErrorObject(errors.ArgumentError, "Splat arguments must be a string") + return t.vm.initErrorObject(errors.ArgumentError, "Splat arguments must be a string, got: %s for argument %d", v.Class().Name, i) } arr = append(arr, argn.value) } @@ -48,12 +48,10 @@ func builtinHTTPClassMethods() []*BuiltinMethodObject { resp, err := http.Get(uri.String()) if err != nil { - return t.vm.initErrorObject(errors.InternalError, err.Error()) + return t.vm.initErrorObject(errors.HTTPError, "Could not complete request, %s", err) } - if resp.StatusCode != http.StatusOK { - return t.vm.initErrorObject(errors.InternalError, resp.Status) - } + return t.vm.initErrorObject(errors.HTTPError, "Non-200 response, %s (%d)", resp.Status, resp.StatusCode) } content, err := ioutil.ReadAll(resp.Body) resp.Body.Close() @@ -71,34 +69,33 @@ func builtinHTTPClassMethods() []*BuiltinMethodObject { Fn: func(receiver Object) builtinMethodBody { return func(t *thread, args []Object, blockFrame *callFrame) Object { if len(args) != 3 { - return t.vm.initErrorObject(errors.ArgumentError, "Expect 3 arguments. got=%v", strconv.Itoa(len(args))) + return t.vm.initErrorObject(errors.ArgumentError, errors.WrongNumberOfArgumentFormat,3, strconv.Itoa(len(args))) } arg0, ok := args[0].(*StringObject) if !ok { - return t.vm.initErrorObject(errors.ArgumentError, "Argument 0 must be a string") + return t.vm.initErrorObject(errors.ArgumentError, "Expect argument 0 to be string, got: %s", args[0].Class().Name) } host := arg0.value arg1, ok := args[1].(*StringObject) if !ok { - return t.vm.initErrorObject(errors.ArgumentError, "Argument 1 must be a string") + return t.vm.initErrorObject(errors.ArgumentError, "Expect argument 1 to be string, got: %s", args[0].Class().Name) } contentType := arg1.value arg2, ok := args[2].(*StringObject) if !ok { - return t.vm.initErrorObject(errors.ArgumentError, "Argument 2 must be a string") + return t.vm.initErrorObject(errors.ArgumentError, "Expect argument 0 to be string, got: %s", args[0].Class().Name) } body := arg2.value resp, err := http.Post(host, contentType, strings.NewReader(body)) if err != nil { - return t.vm.initErrorObject(errors.InternalError, err.Error()) + return t.vm.initErrorObject(errors.HTTPError, "Could not complete request, %s", err) } if resp.StatusCode != http.StatusOK { - return t.vm.initErrorObject(errors.InternalError, resp.Status) - } + return t.vm.initErrorObject(errors.HTTPError, "Non-200 response, %s (%d)", resp.Status, resp.StatusCode) } content, err := ioutil.ReadAll(resp.Body) resp.Body.Close() @@ -116,21 +113,28 @@ func builtinHTTPClassMethods() []*BuiltinMethodObject { Fn: func(receiver Object) builtinMethodBody { return func(t *thread, args []Object, blockFrame *callFrame) Object { if len(args) != 1 { - return t.vm.initErrorObject(errors.ArgumentError, "Expect 1 arguments. got=%v", strconv.Itoa(len(args))) + return t.vm.initErrorObject(errors.ArgumentError, "Expect 1 argument. got=%v", strconv.Itoa(len(args))) } host, ok := args[0].(*StringObject) if !ok { - return t.vm.initErrorObject(errors.ArgumentError, "Argument 0 must be a string") + return t.vm.initErrorObject(errors.ArgumentError, "Expect argument 0 to be string, got: %s", args[0].Class().Name) } - _, err := http.Head(host.value) + resp, err := http.Head(host.value) if err != nil { - return t.vm.initErrorObject(errors.InternalError, err.Error()) + return t.vm.initErrorObject(errors.HTTPError, "Could not complete request, %s", err) + } + if resp.StatusCode != http.StatusOK { + return t.vm.initErrorObject(errors.HTTPError, "Non-200 response, %s (%d)", resp.Status, resp.StatusCode) } + + ret := t.vm.initHashObject(map[string]Object{}) + + for k, v := range resp.Header { + ret.Pairs[k] = t.vm.initStringObject(strings.Join(v, " ")) } - //TODO: make return value a map of headers - return t.vm.initStringObject("") + return ret } }, }, diff --git a/vm/http_client.go b/vm/http_client.go index 056a87590..1743e7f78 100644 --- a/vm/http_client.go +++ b/vm/http_client.go @@ -1,9 +1,8 @@ package vm import ( - "errors" "fmt" - gerrors "github.com/goby-lang/goby/vm/errors" + "github.com/goby-lang/goby/vm/errors" "io/ioutil" "net/http" "strconv" @@ -22,7 +21,7 @@ func builtinHTTPClientInstanceMethods() []*BuiltinMethodObject { return func(t *thread, args []Object, blockFrame *callFrame) Object { if len(args) != 0 { - return t.vm.initErrorObject(gerrors.ArgumentError, "Expect 0 arguments. got=%v", strconv.Itoa(len(args))) + return t.vm.initErrorObject(errors.ArgumentError, "Expect 0 arguments. got=%v", strconv.Itoa(len(args))) } @@ -37,18 +36,17 @@ func builtinHTTPClientInstanceMethods() []*BuiltinMethodObject { goReq, err := requestGobyToGo(req) if err != nil { - return t.vm.initErrorObject(gerrors.ArgumentError, "Request object incomplete object %s", err) + return t.vm.initErrorObject(errors.ArgumentError, "Request object incomplete, %s", err) } resp, err := goClient.Do(goReq) if err != nil { - fmt.Println("do error: ", err) - return t.vm.initErrorObject(gerrors.InternalError, "Could not get response: %s", err) + return t.vm.initErrorObject(errors.HTTPError, "Could not complete request, %s", err) } gobyResp, err := responseGoToGoby(t, resp) if err != nil { - return t.vm.initErrorObject(gerrors.InternalError, "Could not read response: %s", err) + return t.vm.initErrorObject(errors.InternalError, "Could not read response: %s", err) } return gobyResp @@ -62,7 +60,7 @@ func requestGobyToGo(gobyReq *RObject) (*http.Request, error) { //:method, :protocol, :body, :content_length, :transfer_encoding, :host, :path, :url, :params uObj, ok := gobyReq.instanceVariableGet("@url") if !ok { - return nil, errors.New("could not get url") + return nil, fmt.Errorf("could not get url") } u := uObj.(*StringObject).value diff --git a/vm/http_client_test.go b/vm/http_client_test.go index 523c51e09..61bc24b52 100644 --- a/vm/http_client_test.go +++ b/vm/http_client_test.go @@ -80,7 +80,7 @@ func TestHTTPClientObjectFail(t *testing.T) { end res - `, "InternalError: Could not get response: Get http://127.0.0.1:3001: dial tcp 127.0.0.1:3001: getsockopt: connection refused", 6}, + `, "HTTPError: Could not complete request, Get http://127.0.0.1:3001: dial tcp 127.0.0.1:3001: getsockopt: connection refused", 6}, } //block until server is ready diff --git a/vm/http_test.go b/vm/http_test.go index 6cdab8ac0..6ab55e1c3 100644 --- a/vm/http_test.go +++ b/vm/http_test.go @@ -91,17 +91,17 @@ func TestHTTPObjectFail(t *testing.T) { require "net/http" Net::HTTP.get("http://127.0.0.1:3000/error") - `, "InternalError: 404 Not Found", 4}, + `, "HTTPError: Non-200 response, 404 Not Found (404)", 4}, {` require "net/http" Net::HTTP.post("http://127.0.0.1:3000/error", "text/plain", "Let me down") - `, "InternalError: 404 Not Found", 4}, + `, "HTTPError: Non-200 response, 404 Not Found (404)", 4}, {` require "net/http" Net::HTTP.post("http://127.0.0.1:3001", "text/plain", "Let me down") - `, "InternalError: Post http://127.0.0.1:3001: dial tcp 127.0.0.1:3001: getsockopt: connection refused", 4}, + `, "HTTPError: Could not complete request, Post http://127.0.0.1:3001: dial tcp 127.0.0.1:3001: getsockopt: connection refused", 4}, } //block until server is ready From 86db44efd1d6ec8ea30040032b24f3ae26ebc497 Mon Sep 17 00:00:00 2001 From: Julio Date: Sat, 9 Sep 2017 17:06:00 -0700 Subject: [PATCH 12/17] go fmt --- vm/http.go | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/vm/http.go b/vm/http.go index e5c398b3c..a41b67db6 100644 --- a/vm/http.go +++ b/vm/http.go @@ -51,7 +51,8 @@ func builtinHTTPClassMethods() []*BuiltinMethodObject { return t.vm.initErrorObject(errors.HTTPError, "Could not complete request, %s", err) } if resp.StatusCode != http.StatusOK { - return t.vm.initErrorObject(errors.HTTPError, "Non-200 response, %s (%d)", resp.Status, resp.StatusCode) } + return t.vm.initErrorObject(errors.HTTPError, "Non-200 response, %s (%d)", resp.Status, resp.StatusCode) + } content, err := ioutil.ReadAll(resp.Body) resp.Body.Close() @@ -69,7 +70,7 @@ func builtinHTTPClassMethods() []*BuiltinMethodObject { Fn: func(receiver Object) builtinMethodBody { return func(t *thread, args []Object, blockFrame *callFrame) Object { if len(args) != 3 { - return t.vm.initErrorObject(errors.ArgumentError, errors.WrongNumberOfArgumentFormat,3, strconv.Itoa(len(args))) + return t.vm.initErrorObject(errors.ArgumentError, errors.WrongNumberOfArgumentFormat, 3, strconv.Itoa(len(args))) } arg0, ok := args[0].(*StringObject) @@ -86,7 +87,7 @@ func builtinHTTPClassMethods() []*BuiltinMethodObject { arg2, ok := args[2].(*StringObject) if !ok { - return t.vm.initErrorObject(errors.ArgumentError, "Expect argument 0 to be string, got: %s", args[0].Class().Name) + return t.vm.initErrorObject(errors.ArgumentError, "Expect argument 0 to be string, got: %s", args[0].Class().Name) } body := arg2.value @@ -95,7 +96,8 @@ func builtinHTTPClassMethods() []*BuiltinMethodObject { return t.vm.initErrorObject(errors.HTTPError, "Could not complete request, %s", err) } if resp.StatusCode != http.StatusOK { - return t.vm.initErrorObject(errors.HTTPError, "Non-200 response, %s (%d)", resp.Status, resp.StatusCode) } + return t.vm.initErrorObject(errors.HTTPError, "Non-200 response, %s (%d)", resp.Status, resp.StatusCode) + } content, err := ioutil.ReadAll(resp.Body) resp.Body.Close() @@ -118,7 +120,7 @@ func builtinHTTPClassMethods() []*BuiltinMethodObject { host, ok := args[0].(*StringObject) if !ok { - return t.vm.initErrorObject(errors.ArgumentError, "Expect argument 0 to be string, got: %s", args[0].Class().Name) + return t.vm.initErrorObject(errors.ArgumentError, "Expect argument 0 to be string, got: %s", args[0].Class().Name) } resp, err := http.Head(host.value) @@ -126,7 +128,8 @@ func builtinHTTPClassMethods() []*BuiltinMethodObject { return t.vm.initErrorObject(errors.HTTPError, "Could not complete request, %s", err) } if resp.StatusCode != http.StatusOK { - return t.vm.initErrorObject(errors.HTTPError, "Non-200 response, %s (%d)", resp.Status, resp.StatusCode) } + return t.vm.initErrorObject(errors.HTTPError, "Non-200 response, %s (%d)", resp.Status, resp.StatusCode) + } ret := t.vm.initHashObject(map[string]Object{}) From aea02d332bcbec3c7baba20460e1f8823fac6810 Mon Sep 17 00:00:00 2001 From: Julio Date: Sat, 9 Sep 2017 19:05:34 -0700 Subject: [PATCH 13/17] refactored client class to prevent collision with builtin 'send' method restructure client implementation for more ruby-like experience and less verbosity --- vm/http.go | 21 +++++++ vm/http_client.go | 128 +++++++++++++++++++++++++++++++++++------ vm/http_client_test.go | 36 ++++-------- 3 files changed, 143 insertions(+), 42 deletions(-) diff --git a/vm/http.go b/vm/http.go index a41b67db6..303023dae 100644 --- a/vm/http.go +++ b/vm/http.go @@ -140,6 +140,27 @@ func builtinHTTPClassMethods() []*BuiltinMethodObject { return ret } }, + }, { + // Sends a GET request to the target and returns the HTTP response as a string. + Name: "start", + Fn: func(receiver Object) builtinMethodBody { + return func(t *thread, args []Object, blockFrame *callFrame) Object { + + if len(args) != 0 { + return t.vm.initErrorObject(errors.ArgumentError, "Expect 0 arguments. got=%v", strconv.Itoa(len(args))) + } + + gobyClient := httpClientClass.initializeInstance() + + result := t.builtinMethodYield(blockFrame, gobyClient) + + if err, ok := result.Target.(*Error); ok { + return err //an Error object + } + + return result.Target + } + }, }, } } diff --git a/vm/http_client.go b/vm/http_client.go index 1743e7f78..f332cba8e 100644 --- a/vm/http_client.go +++ b/vm/http_client.go @@ -2,51 +2,147 @@ package vm import ( "fmt" - "github.com/goby-lang/goby/vm/errors" "io/ioutil" "net/http" - "strconv" "strings" + + "github.com/goby-lang/goby/vm/classes" + "github.com/goby-lang/goby/vm/errors" ) // Instance methods -------------------------------------------------------- func builtinHTTPClientInstanceMethods() []*BuiltinMethodObject { + //TODO: cookie jar goClient := http.DefaultClient return []*BuiltinMethodObject{ { // Sends a GET request to the target and returns the HTTP response as a string. - Name: "send", + Name: "get", Fn: func(receiver Object) builtinMethodBody { return func(t *thread, args []Object, blockFrame *callFrame) Object { + if len(args) != 1 { + return t.vm.initErrorObject(errors.ArgumentError, errors.WrongNumberOfArgumentFormat, 1, len(args)) + } - if len(args) != 0 { - return t.vm.initErrorObject(errors.ArgumentError, "Expect 0 arguments. got=%v", strconv.Itoa(len(args))) + u, ok := args[0].(*StringObject) + if !ok { + return t.vm.initErrorObject(errors.TypeError, errors.WrongArgumentTypeFormat, classes.StringClass, u.Class().Name) + } + resp, err := goClient.Get(u.value) + if err != nil { + return t.vm.initErrorObject(errors.HTTPError, "Could not complete request, %s", err) } - req := httpRequestClass.initializeInstance() + gobyResp, err := responseGoToGoby(t, resp) + if err != nil { + return t.vm.initErrorObject(errors.InternalError, err.Error()) + } - result := t.builtinMethodYield(blockFrame, req) + return gobyResp + } + }, + }, { + // Sends a GET request to the target and returns the HTTP response as a string. + Name: "post", + Fn: func(receiver Object) builtinMethodBody { + return func(t *thread, args []Object, blockFrame *callFrame) Object { + if len(args) != 3 { + return t.vm.initErrorObject(errors.ArgumentError, errors.WrongNumberOfArgumentFormat, 3, len(args)) + } + + u, ok := args[0].(*StringObject) + if !ok { + return t.vm.initErrorObject(errors.TypeError, errors.WrongArgumentTypeFormat, classes.StringClass, u.Class().Name) + } + + contentType, ok := args[1].(*StringObject) + if !ok { + return t.vm.initErrorObject(errors.TypeError, errors.WrongArgumentTypeFormat, classes.StringClass, u.Class().Name) + } - if err, ok := result.Target.(*Error); ok { - fmt.Printf("Error: %s", err.Message) - return err //a Error object + body, ok := args[2].(*StringObject) + if !ok { + return t.vm.initErrorObject(errors.TypeError, errors.WrongArgumentTypeFormat, classes.StringClass, u.Class().Name) } - goReq, err := requestGobyToGo(req) + bodyR := strings.NewReader(body.value) + + resp, err := goClient.Post(u.value, contentType.value, bodyR) if err != nil { - return t.vm.initErrorObject(errors.ArgumentError, "Request object incomplete, %s", err) + return t.vm.initErrorObject(errors.HTTPError, "Could not complete request, %s", err) + } + + gobyResp, err := responseGoToGoby(t, resp) + if err != nil { + return t.vm.initErrorObject(errors.InternalError, err.Error()) + } + + return gobyResp + } + }, + }, { + // Sends a GET request to the target and returns the HTTP response as a string. + Name: "head", + Fn: func(receiver Object) builtinMethodBody { + return func(t *thread, args []Object, blockFrame *callFrame) Object { + if len(args) != 1 { + return t.vm.initErrorObject(errors.ArgumentError, errors.WrongNumberOfArgumentFormat, 1, len(args)) } - resp, err := goClient.Do(goReq) + u, ok := args[0].(*StringObject) + if !ok { + return t.vm.initErrorObject(errors.TypeError, errors.WrongArgumentTypeFormat, classes.StringClass, u.Class().Name) + } + + resp, err := goClient.Head(u.value) if err != nil { return t.vm.initErrorObject(errors.HTTPError, "Could not complete request, %s", err) } gobyResp, err := responseGoToGoby(t, resp) if err != nil { - return t.vm.initErrorObject(errors.InternalError, "Could not read response: %s", err) + return t.vm.initErrorObject(errors.InternalError, err.Error()) + } + + return gobyResp + } + }, + }, { + // Sends a GET request to the target and returns the HTTP response as a string. + Name: "request", + Fn: func(receiver Object) builtinMethodBody { + return func(t *thread, args []Object, blockFrame *callFrame) Object { + return httpRequestClass.initializeInstance() + } + }, + }, { + Name: "exec", + Fn: func(receiver Object) builtinMethodBody { + return func(t *thread, args []Object, blockFrame *callFrame) Object { + if len(args) != 1 { + return t.vm.initErrorObject(errors.ArgumentError, errors.WrongNumberOfArgumentFormat, 1, len(args)) + } + + if args[0].Class().Name != httpRequestClass.Name { + return t.vm.initErrorObject(errors.TypeError, errors.WrongArgumentTypeFormat, "HTTP Response", args[0].Class().Name) + } + + goReq, err := requestGobyToGo(args[0]) + if err != nil { + return t.vm.initErrorObject(errors.ArgumentError, err.Error()) + } + + goResp, err := goClient.Do(goReq) + if err != nil { + return t.vm.initErrorObject(errors.HTTPError, "Could not complete request, %s", err) + } + + gobyResp, err := responseGoToGoby(t, goResp) + + if err != nil { + return t.vm.initErrorObject(errors.InternalError, err.Error()) } return gobyResp @@ -56,7 +152,7 @@ func builtinHTTPClientInstanceMethods() []*BuiltinMethodObject { } } -func requestGobyToGo(gobyReq *RObject) (*http.Request, error) { +func requestGobyToGo(gobyReq Object) (*http.Request, error) { //:method, :protocol, :body, :content_length, :transfer_encoding, :host, :path, :url, :params uObj, ok := gobyReq.instanceVariableGet("@url") if !ok { @@ -88,7 +184,7 @@ func requestGobyToGo(gobyReq *RObject) (*http.Request, error) { // Other helper functions ----------------------------------------------- -func responseGoToGoby(t *thread, goResp *http.Response) (*RObject, error) { +func responseGoToGoby(t *thread, goResp *http.Response) (Object, error) { gobyResp := httpResponseClass.initializeInstance() //attr_accessor :body, :status, :status_code, :protocol, :transfer_encoding, :http_version, :request_http_version, :request diff --git a/vm/http_client_test.go b/vm/http_client_test.go index 61bc24b52..beee1c70f 100644 --- a/vm/http_client_test.go +++ b/vm/http_client_test.go @@ -18,11 +18,8 @@ func TestHTTPClientObject(t *testing.T) { {` require "net/http" - c = Net::HTTP::Client.new - - res = c.send do |req| - req.url = "http://127.0.0.1:3000/index" - req.method = "GET" + res = Net::HTTP.start do |client| + res = client.get("http://127.0.0.1:3000/index") end res.body @@ -30,12 +27,8 @@ func TestHTTPClientObject(t *testing.T) { {` require "net/http" - c = Net::HTTP::Client.new - - res = c.send do |req| - req.url = "http://127.0.0.1:3000/index" - req.method = "POST" - req.body = "Hi Again" + res = Net::HTTP.start do |client| + client.post("http://127.0.0.1:3000/index", "text/plain", "Hi Again") end res.body @@ -43,11 +36,8 @@ func TestHTTPClientObject(t *testing.T) { {` require "net/http" - c = Net::HTTP::Client.new - - res = c.send do |req| - req.url = "http://127.0.0.1:3000/error" - req.method = "GET" + res = Net::HTTP.start do |client| + client.get("http://127.0.0.1:3000/error") end res.status_code @@ -72,25 +62,19 @@ func TestHTTPClientObjectFail(t *testing.T) { {` require "net/http" - c = Net::HTTP::Client.new - - res = c.send do |req| - req.url = "http://127.0.0.1:3001" - req.method = "GET" + res = Net::HTTP.start do |client| + client.get("http://127.0.0.1:3001") end res - `, "HTTPError: Could not complete request, Get http://127.0.0.1:3001: dial tcp 127.0.0.1:3001: getsockopt: connection refused", 6}, + `, "HTTPError: Could not complete request, Get http://127.0.0.1:3001: dial tcp 127.0.0.1:3001: getsockopt: connection refused", 5}, } - //block until server is ready - //<-c - for i, tt := range testsFail { v := initTestVM() evaluated := v.testEval(t, tt.input, getFilename()) checkError(t, i, evaluated, tt.expected, getFilename(), tt.errorLine) - v.checkCFP(t, i, 1) + v.checkCFP(t, i, 3) v.checkSP(t, i, 1) } } From ec6053d4a91efdc86ee787d1a1c10f53a75d05cf Mon Sep 17 00:00:00 2001 From: Julio Date: Sun, 10 Sep 2017 20:21:41 -0700 Subject: [PATCH 14/17] documentation and code organization changes for Net::HTTP and Net::HTTP::Client classes --- vm/http.go | 18 ++++-------------- vm/http_client.go | 28 ++++++++++++++++++++++------ 2 files changed, 26 insertions(+), 20 deletions(-) diff --git a/vm/http.go b/vm/http.go index 303023dae..45784261e 100644 --- a/vm/http.go +++ b/vm/http.go @@ -21,7 +21,7 @@ var ( func builtinHTTPClassMethods() []*BuiltinMethodObject { return []*BuiltinMethodObject{ { - // Sends a GET request to the target and returns the HTTP response as a string. + // Sends a GET request to the target and returns the HTTP response as a string. Will error on non-200 responses, for more control over http requests look at the `start` method. Name: "get", Fn: func(receiver Object) builtinMethodBody { return func(t *thread, args []Object, blockFrame *callFrame) Object { @@ -65,7 +65,7 @@ func builtinHTTPClassMethods() []*BuiltinMethodObject { } }, }, { - // Sends a POST request to the target with type header and body. Returns the HTTP response as a string. + // Sends a POST request to the target with type header and body. Returns the HTTP response as a string. Will error on non-200 responses, for more control over http requests look at the `start` method. Name: "post", Fn: func(receiver Object) builtinMethodBody { return func(t *thread, args []Object, blockFrame *callFrame) Object { @@ -110,7 +110,7 @@ func builtinHTTPClassMethods() []*BuiltinMethodObject { } }, }, { - // Sends a HEAD request to the target with type header and body. Returns the HTTP response as a string. + // Sends a HEAD request to the target with type header and body. Returns the HTTP response as a string. Will error on non-200 responses, for more control over http requests look at the `start` method. Name: "head", Fn: func(receiver Object) builtinMethodBody { return func(t *thread, args []Object, blockFrame *callFrame) Object { @@ -141,7 +141,7 @@ func builtinHTTPClassMethods() []*BuiltinMethodObject { } }, }, { - // Sends a GET request to the target and returns the HTTP response as a string. + // Starts an HTTP client. This method requires a block which takes a Net::HTTP::Client object. The return value of this method is the last evaluated value of the provided block. Name: "start", Fn: func(receiver Object) builtinMethodBody { return func(t *thread, args []Object, blockFrame *callFrame) Object { @@ -205,13 +205,3 @@ func initResponseClass(vm *VM, hc *RClass) *RClass { httpResponseClass = responseClass return responseClass } - -func initClientClass(vm *VM, hc *RClass) *RClass { - clientClass := vm.initializeClass("Client", false) - hc.setClassConstant(clientClass) - - clientClass.setBuiltinMethods(builtinHTTPClientInstanceMethods(), false) - - httpClientClass = clientClass - return clientClass -} diff --git a/vm/http_client.go b/vm/http_client.go index f332cba8e..04493f182 100644 --- a/vm/http_client.go +++ b/vm/http_client.go @@ -11,13 +11,14 @@ import ( ) // Instance methods -------------------------------------------------------- + func builtinHTTPClientInstanceMethods() []*BuiltinMethodObject { //TODO: cookie jar goClient := http.DefaultClient return []*BuiltinMethodObject{ { - // Sends a GET request to the target and returns the HTTP response as a string. + // Sends a GET request to the target and returns a `Net::HTTP::Response` object. Name: "get", Fn: func(receiver Object) builtinMethodBody { return func(t *thread, args []Object, blockFrame *callFrame) Object { @@ -44,7 +45,7 @@ func builtinHTTPClientInstanceMethods() []*BuiltinMethodObject { } }, }, { - // Sends a GET request to the target and returns the HTTP response as a string. + // Sends a POST request to the target and returns a `Net::HTTP::Response` object. Name: "post", Fn: func(receiver Object) builtinMethodBody { return func(t *thread, args []Object, blockFrame *callFrame) Object { @@ -83,7 +84,7 @@ func builtinHTTPClientInstanceMethods() []*BuiltinMethodObject { } }, }, { - // Sends a GET request to the target and returns the HTTP response as a string. + // Sends a HEAD request to the target and returns a `Net::HTTP::Response` object. Name: "head", Fn: func(receiver Object) builtinMethodBody { return func(t *thread, args []Object, blockFrame *callFrame) Object { @@ -110,7 +111,7 @@ func builtinHTTPClientInstanceMethods() []*BuiltinMethodObject { } }, }, { - // Sends a GET request to the target and returns the HTTP response as a string. + // Returns a blank `Net::HTTP::Request` object to be sent with the`exec` method Name: "request", Fn: func(receiver Object) builtinMethodBody { return func(t *thread, args []Object, blockFrame *callFrame) Object { @@ -118,6 +119,7 @@ func builtinHTTPClientInstanceMethods() []*BuiltinMethodObject { } }, }, { + // Sends a passed `Net::HTTP::Request` object and returns a `Net::HTTP::Response` object Name: "exec", Fn: func(receiver Object) builtinMethodBody { return func(t *thread, args []Object, blockFrame *callFrame) Object { @@ -152,6 +154,22 @@ func builtinHTTPClientInstanceMethods() []*BuiltinMethodObject { } } +// Internal functions =================================================== + +// Functions for initialization ----------------------------------------- + +func initClientClass(vm *VM, hc *RClass) *RClass { + clientClass := vm.initializeClass("Client", false) + hc.setClassConstant(clientClass) + + clientClass.setBuiltinMethods(builtinHTTPClientInstanceMethods(), false) + + httpClientClass = clientClass + return clientClass +} + +// Other helper functions ----------------------------------------------- + func requestGobyToGo(gobyReq Object) (*http.Request, error) { //:method, :protocol, :body, :content_length, :transfer_encoding, :host, :path, :url, :params uObj, ok := gobyReq.instanceVariableGet("@url") @@ -182,8 +200,6 @@ func requestGobyToGo(gobyReq Object) (*http.Request, error) { } -// Other helper functions ----------------------------------------------- - func responseGoToGoby(t *thread, goResp *http.Response) (Object, error) { gobyResp := httpResponseClass.initializeInstance() From 4e5a149dabb024a9b25484051f10a0b43ad5d6ae Mon Sep 17 00:00:00 2001 From: Julio Date: Sun, 17 Sep 2017 11:56:06 -0700 Subject: [PATCH 15/17] merge from master splat args for HTTP#head tests for http class todo item for HTTP#Client added test for HTTP#Client --- vm/http.go | 28 +++++++++++++++------ vm/http_client.go | 2 +- vm/http_client_test.go | 9 +++++++ vm/http_test.go | 57 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 87 insertions(+), 9 deletions(-) diff --git a/vm/http.go b/vm/http.go index 45784261e..9f7048cf0 100644 --- a/vm/http.go +++ b/vm/http.go @@ -70,7 +70,7 @@ func builtinHTTPClassMethods() []*BuiltinMethodObject { Fn: func(receiver Object) builtinMethodBody { return func(t *thread, args []Object, blockFrame *callFrame) Object { if len(args) != 3 { - return t.vm.initErrorObject(errors.ArgumentError, errors.WrongNumberOfArgumentFormat, 3, strconv.Itoa(len(args))) + return t.vm.initErrorObject(errors.ArgumentError, errors.WrongNumberOfArgumentFormat, 3, len(args)) } arg0, ok := args[0].(*StringObject) @@ -87,7 +87,7 @@ func builtinHTTPClassMethods() []*BuiltinMethodObject { arg2, ok := args[2].(*StringObject) if !ok { - return t.vm.initErrorObject(errors.ArgumentError, "Expect argument 0 to be string, got: %s", args[0].Class().Name) + return t.vm.initErrorObject(errors.ArgumentError, "Expect argument 2 to be string, got: %s", args[0].Class().Name) } body := arg2.value @@ -114,16 +114,28 @@ func builtinHTTPClassMethods() []*BuiltinMethodObject { Name: "head", Fn: func(receiver Object) builtinMethodBody { return func(t *thread, args []Object, blockFrame *callFrame) Object { - if len(args) != 1 { - return t.vm.initErrorObject(errors.ArgumentError, "Expect 1 argument. got=%v", strconv.Itoa(len(args))) - } - - host, ok := args[0].(*StringObject) + arg0, ok := args[0].(*StringObject) if !ok { return t.vm.initErrorObject(errors.ArgumentError, "Expect argument 0 to be string, got: %s", args[0].Class().Name) } - resp, err := http.Head(host.value) + uri, err := url.Parse(arg0.value) + + if len(args) > 1 { + var arr []string + + for i, v := range args[1:] { + argn, ok := v.(*StringObject) + if !ok { + return t.vm.initErrorObject(errors.ArgumentError, "Splat arguments must be a string, got: %s for argument %d", v.Class().Name, i) + } + arr = append(arr, argn.value) + } + + uri.Path = path.Join(arr...) + } + + resp, err := http.Head(uri.String()) if err != nil { return t.vm.initErrorObject(errors.HTTPError, "Could not complete request, %s", err) } diff --git a/vm/http_client.go b/vm/http_client.go index 04493f182..0a903efbb 100644 --- a/vm/http_client.go +++ b/vm/http_client.go @@ -13,7 +13,7 @@ import ( // Instance methods -------------------------------------------------------- func builtinHTTPClientInstanceMethods() []*BuiltinMethodObject { - //TODO: cookie jar + //TODO: cookie jar and mutable client goClient := http.DefaultClient return []*BuiltinMethodObject{ diff --git a/vm/http_client_test.go b/vm/http_client_test.go index beee1c70f..e49b09bfc 100644 --- a/vm/http_client_test.go +++ b/vm/http_client_test.go @@ -66,6 +66,15 @@ func TestHTTPClientObjectFail(t *testing.T) { client.get("http://127.0.0.1:3001") end + res + `, "HTTPError: Could not complete request, Get http://127.0.0.1:3001: dial tcp 127.0.0.1:3001: getsockopt: connection refused", 5}, + {` + require "net/http" + + res = Net::HTTP.start do |client| + client.get("http://127.0.0.1:3001") + end + res `, "HTTPError: Could not complete request, Get http://127.0.0.1:3001: dial tcp 127.0.0.1:3001: getsockopt: connection refused", 5}, } diff --git a/vm/http_test.go b/vm/http_test.go index 6ab55e1c3..1b78832b8 100644 --- a/vm/http_test.go +++ b/vm/http_test.go @@ -65,6 +65,12 @@ func TestHTTPObject(t *testing.T) { Net::HTTP.post("http://127.0.0.1:3000/index", "text/plain", "Hi Again") `, "POST Hi Again"}, + {` + require "net/http" + + res = Net::HTTP.head("http://127.0.0.1:3000/index") + res.status + `, "OK"}, } //block until server is ready @@ -87,6 +93,7 @@ func TestHTTPObjectFail(t *testing.T) { go startTestServer(c) testsFail := []errorTestCase{ + //HTTPErrors for get() {` require "net/http" @@ -95,6 +102,23 @@ func TestHTTPObjectFail(t *testing.T) { {` require "net/http" + Net::HTTP.get("http://127.0.0.1:3001") + `, "HTTPError: Could not complete request, Get http://127.0.0.1:3001: dial tcp 127.0.0.1:3001: getsockopt: connection refused", 4}, + //Argument errors for get() + {` + require "net/http" + + Net::HTTP.get(42) + `, "ArgumentError: Expect argument 0 to be string, got: Integer", 4}, + {` + require "net/http" + + Net::HTTP.get("http://127.0.0.1:3000/error", 40, 2) + `, "ArgumentError: Splat arguments must be a string, got: Integer for argument 0", 4}, + //HTTPErrors for post() + {` + require "net/http" + Net::HTTP.post("http://127.0.0.1:3000/error", "text/plain", "Let me down") `, "HTTPError: Non-200 response, 404 Not Found (404)", 4}, {` @@ -102,6 +126,39 @@ func TestHTTPObjectFail(t *testing.T) { Net::HTTP.post("http://127.0.0.1:3001", "text/plain", "Let me down") `, "HTTPError: Could not complete request, Post http://127.0.0.1:3001: dial tcp 127.0.0.1:3001: getsockopt: connection refused", 4}, + //Argument errors for post() + {` + require "net/http" + + Net::HTTP.post("http://127.0.0.1:3001", "text/plain", "Let me down", "again") + `, "ArgumentError: Expect 3 arguments. got: 4", 4}, + {` + require "net/http" + + Net::HTTP.post(42, "text/plain", "Let me down") + `, "ArgumentError: Expect argument 0 to be string, got: Integer", 4}, + //HTTPErrors for head() + {` + require "net/http" + + Net::HTTP.head("http://127.0.0.1:3000/error") + `, "HTTPError: Non-200 response, 404 Not Found (404)", 4}, + {` + require "net/http" + + Net::HTTP.head("http://127.0.0.1:3001") + `, "HTTPError: Could not complete request, Head http://127.0.0.1:3001: dial tcp 127.0.0.1:3001: getsockopt: connection refused", 4}, + //Argument errors for head() + {` + require "net/http" + + Net::HTTP.head(42) + `, "ArgumentError: Expect argument 0 to be string, got: Integer", 4}, + {` + require "net/http" + + Net::HTTP.head("http://127.0.0.1:3000/error", 40, 2) + `, "ArgumentError: Splat arguments must be a string, got: Integer for argument 0", 4}, } //block until server is ready From a538567fd2a3a04094d17e26204292bbd7dfdeaf Mon Sep 17 00:00:00 2001 From: Julio Date: Sun, 17 Sep 2017 12:10:54 -0700 Subject: [PATCH 16/17] added tests for HTTP::Client --- vm/http_client_test.go | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/vm/http_client_test.go b/vm/http_client_test.go index e49b09bfc..3ef508688 100644 --- a/vm/http_client_test.go +++ b/vm/http_client_test.go @@ -36,6 +36,28 @@ func TestHTTPClientObject(t *testing.T) { {` require "net/http" + res = Net::HTTP.start do |client| + r = client.request() + r.url = "http://127.0.0.1:3000/index" + r.method = "POST" + r.body = "Another way of doing it" + client.exec(r) + end + + res.body + `, "POST Another way of doing it"}, + {` + require "net/http" + + res = Net::HTTP.start do |client| + client.head("http://127.0.0.1:3000/index") + end + + res.status_code + `, 200}, + {` + require "net/http" + res = Net::HTTP.start do |client| client.get("http://127.0.0.1:3000/error") end From 9d74ed63c1fbf8024349e46c24ff744dd88868b9 Mon Sep 17 00:00:00 2001 From: Julio Date: Sun, 17 Sep 2017 16:54:59 -0700 Subject: [PATCH 17/17] improper tests and documentation of HTTP#head --- vm/http.go | 2 +- vm/http_test.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/vm/http.go b/vm/http.go index 9f7048cf0..2809820a4 100644 --- a/vm/http.go +++ b/vm/http.go @@ -110,7 +110,7 @@ func builtinHTTPClassMethods() []*BuiltinMethodObject { } }, }, { - // Sends a HEAD request to the target with type header and body. Returns the HTTP response as a string. Will error on non-200 responses, for more control over http requests look at the `start` method. + // Sends a HEAD request to the target with type header and body. Returns the HTTP headers as a map[string]string. Will error on non-200 responses, for more control over http requests look at the `start` method. Name: "head", Fn: func(receiver Object) builtinMethodBody { return func(t *thread, args []Object, blockFrame *callFrame) Object { diff --git a/vm/http_test.go b/vm/http_test.go index 1b78832b8..46938e8d8 100644 --- a/vm/http_test.go +++ b/vm/http_test.go @@ -69,8 +69,8 @@ func TestHTTPObject(t *testing.T) { require "net/http" res = Net::HTTP.head("http://127.0.0.1:3000/index") - res.status - `, "OK"}, + res["Content-Length"] + `, "15"}, } //block until server is ready