Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions internal/code-gen/scripting/configuration/fetch_configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ package configuration

import "github.com/gost-dom/browser/internal/code-gen/packagenames"

// configureFetchSpecs registers the code-generation customizations for the Fetch
// API types (Request, Response, Body, Headers, and the global fetch operation),
// marking unsupported members as not-implemented and flagging the members that
// have hand-written custom implementations.
func configureFetchSpecs(specs *WebAPIConfig) {
req := specs.Type("Request")
// req.OverrideWrappedType = &GoType{Package: packagenames.Fetch, Name: "Request", Pointer: true}
Expand All @@ -16,14 +20,19 @@ func configureFetchSpecs(specs *WebAPIConfig) {
res.OverrideWrappedType = &GoType{Package: packagenames.Fetch, Name: "Response", Pointer: true}
res.SkipConstructor = true
res.MarkMembersAsNotImplemented(
"type", "clone", "url", "redirected", "ok", "statusText",
"type", "clone", "url", "redirected",
)
res.Method("ok").SetCustomImplementation()
res.Method("statusText").SetCustomImplementation()

body := specs.Type("Body")
body.MarkMembersAsNotImplemented(
"arrayBuffer", "blob", "bytes", "formData", "text", "bodyUsed",
"blob", "formData", "bodyUsed",
)
body.Method("json").SetCustomImplementation()
body.Method("text").SetCustomImplementation()
body.Method("arrayBuffer").SetCustomImplementation()
body.Method("bytes").SetCustomImplementation()

headers := specs.Type("Headers")
headers.MarkMembersAsNotImplemented("getSetCookie")
Expand Down
21 changes: 21 additions & 0 deletions internal/fetch/fetch.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"io"
"net/http"
"strings"
"sync/atomic"
"time"

Expand Down Expand Up @@ -152,6 +153,26 @@ type Response struct {
httpResponse *http.Response
}

// Ok reports whether the response status is in the range 200-299, matching the
// [Response.ok] getter in the Fetch API.
//
// [Response.ok]: https://developer.mozilla.org/en-US/docs/Web/API/Response/ok
func (r Response) Ok() bool { return r.Status >= 200 && r.Status <= 299 }

// StatusText returns the HTTP status reason phrase, e.g. "OK" for 200. When the
// originating [http.Response] carries an explicit reason phrase that value is
// used; otherwise it falls back to the standard text for the status code.
//
// [Response.statusText]: https://developer.mozilla.org/en-US/docs/Web/API/Response/statusText
func (r Response) StatusText() string {
if r.httpResponse != nil {
if _, text, ok := strings.Cut(r.httpResponse.Status, " "); ok && text != "" {
return text
}
}
return http.StatusText(r.Status)
}

type ReadableStream struct {
Reader io.Reader
}
Expand Down
29 changes: 29 additions & 0 deletions internal/fetch/response_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package fetch_test

import (
"testing"

"github.com/gost-dom/browser/internal/fetch"
"github.com/stretchr/testify/assert"
)

// TestResponseOk verifies that Response.Ok reports true only for status codes
// in the 200-299 range.
func TestResponseOk(t *testing.T) {
for _, tc := range []struct {
status int
want bool
}{{199, false}, {200, true}, {204, true}, {299, true}, {300, false}, {404, false}, {500, false}} {
assert.Equalf(t, tc.want, (fetch.Response{Status: tc.status}).Ok(),
"Ok() for status %d", tc.status)
}
}

// TestResponseStatusText verifies that Response.StatusText falls back to the
// standard reason phrase when there is no originating http.Response.
func TestResponseStatusText(t *testing.T) {
// With no originating http.Response, StatusText falls back to the standard
// reason phrase for the status code.
assert.Equal(t, "OK", (fetch.Response{Status: 200}).StatusText())
assert.Equal(t, "Not Found", (fetch.Response{Status: 404}).StatusText())
}
47 changes: 45 additions & 2 deletions scripting/internal/fetch/body.go
Original file line number Diff line number Diff line change
@@ -1,25 +1,68 @@
package fetch

import (
"fmt"

"github.com/gost-dom/browser/internal/fetch"
"github.com/gost-dom/browser/internal/promise"
"github.com/gost-dom/browser/internal/streams"
"github.com/gost-dom/browser/scripting/internal/codec"
js "github.com/gost-dom/browser/scripting/internal/js"
)

func Body_json[T any](cbCtx js.CallbackContext[T]) (res js.Value[T], err error) {
// encodeBodyPromise is the shared implementation behind the Body consumer
// methods (json, text, bytes, arrayBuffer): it reads the entire [fetch.Body] of
// the current instance and resolves a Promise to the contents encoded by encoder.
func encodeBodyPromise[T any](
cbCtx js.CallbackContext[T],
encoder codec.Encoder[T, []byte],
) (js.Value[T], error) {
instance, err := js.As[fetch.Body](cbCtx.Instance())
if err != nil {
return nil, err
}
return codec.EncodePromise(cbCtx, promise.ReadAll(instance), EncodeJSONBytes)
return codec.EncodePromise(cbCtx, promise.ReadAll(instance), encoder)
}

func Body_json[T any](cbCtx js.CallbackContext[T]) (res js.Value[T], err error) {
return encodeBodyPromise(cbCtx, EncodeJSONBytes)
}

func EncodeJSONBytes[T any](scope js.Scope[T], b []byte) (js.Value[T], error) {
return scope.JSONParse(string(b))
}

func Body_text[T any](cbCtx js.CallbackContext[T]) (res js.Value[T], err error) {
return encodeBodyPromise(cbCtx, encodeBytesAsString)
}

func encodeBytesAsString[T any](scope js.Scope[T], b []byte) (js.Value[T], error) {
return scope.NewString(string(b)), nil
}

func Body_bytes[T any](cbCtx js.CallbackContext[T]) (res js.Value[T], err error) {
return encodeBodyPromise(cbCtx, encodeBytesAsUint8Array)
}

func encodeBytesAsUint8Array[T any](scope js.Scope[T], b []byte) (js.Value[T], error) {
return scope.NewUint8Array(b), nil
}

func Body_arrayBuffer[T any](cbCtx js.CallbackContext[T]) (res js.Value[T], err error) {
return encodeBodyPromise(cbCtx, encodeBytesAsArrayBuffer)
}

func encodeBytesAsArrayBuffer[T any](scope js.Scope[T], b []byte) (js.Value[T], error) {
arr := scope.NewUint8Array(b)
obj, ok := arr.AsObject()
if !ok {
// A freshly created Uint8Array is always an object; if that ever fails
// we must not return the Uint8Array, as callers expect an ArrayBuffer.
return nil, fmt.Errorf("gost-dom/fetch: arrayBuffer: Uint8Array is not an object")
}
return obj.Get("buffer")
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

func encodeReadableStream[T any](
cbCtx js.CallbackContext[T],
body streams.ReadableStream,
Expand Down
12 changes: 0 additions & 12 deletions scripting/internal/fetch/body_generated.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,26 +19,14 @@ func InitializeBody[T any](jsClass js.Class[T]) {
jsClass.CreateAttribute("bodyUsed", Body_bodyUsed, nil)
}

func Body_arrayBuffer[T any](cbCtx js.CallbackContext[T]) (res js.Value[T], err error) {
return codec.EncodeCallbackErrorf(cbCtx, "Body.Body_arrayBuffer: Not implemented. Create an issue: https://github.com/gost-dom/browser/issues")
}

func Body_blob[T any](cbCtx js.CallbackContext[T]) (res js.Value[T], err error) {
return codec.EncodeCallbackErrorf(cbCtx, "Body.Body_blob: Not implemented. Create an issue: https://github.com/gost-dom/browser/issues")
}

func Body_bytes[T any](cbCtx js.CallbackContext[T]) (res js.Value[T], err error) {
return codec.EncodeCallbackErrorf(cbCtx, "Body.Body_bytes: Not implemented. Create an issue: https://github.com/gost-dom/browser/issues")
}

func Body_formData[T any](cbCtx js.CallbackContext[T]) (res js.Value[T], err error) {
return codec.EncodeCallbackErrorf(cbCtx, "Body.Body_formData: Not implemented. Create an issue: https://github.com/gost-dom/browser/issues")
}

func Body_text[T any](cbCtx js.CallbackContext[T]) (res js.Value[T], err error) {
return codec.EncodeCallbackErrorf(cbCtx, "Body.Body_text: Not implemented. Create an issue: https://github.com/gost-dom/browser/issues")
}

func Body_body[T any](cbCtx js.CallbackContext[T]) (res js.Value[T], err error) {
instance, err := js.As[fetch.Body](cbCtx.Instance())
if err != nil {
Expand Down
23 changes: 23 additions & 0 deletions scripting/internal/fetch/response.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,32 @@ package fetch
import (
"fmt"

"github.com/gost-dom/browser/internal/fetch"
"github.com/gost-dom/browser/scripting/internal/codec"
"github.com/gost-dom/browser/scripting/internal/js"
)

// ResponseConstructor implements the Response constructor. gost-dom does not
// support constructing Response objects from script, so it always returns an
// error.
func ResponseConstructor[T any](cbCtx js.CallbackContext[T]) (js.Value[T], error) {
return nil, fmt.Errorf("gost-dom/fetch: Response constructor not implemented")
}

// Response_ok implements the Response.ok getter (status in the range 200-299).
func Response_ok[T any](cbCtx js.CallbackContext[T]) (js.Value[T], error) {
instance, err := js.As[*fetch.Response](cbCtx.Instance())
if err != nil {
return nil, err
}
return codec.EncodeBoolean(cbCtx, instance.Ok())
}

// Response_statusText implements the Response.statusText getter.
func Response_statusText[T any](cbCtx js.CallbackContext[T]) (js.Value[T], error) {
instance, err := js.As[*fetch.Response](cbCtx.Instance())
if err != nil {
return nil, err
}
return codec.EncodeString(cbCtx, instance.StatusText())
}
8 changes: 0 additions & 8 deletions scripting/internal/fetch/response_generated.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,6 @@ func Response_status[T any](cbCtx js.CallbackContext[T]) (res js.Value[T], err e
return codec.EncodeInt(cbCtx, result)
}

func Response_ok[T any](cbCtx js.CallbackContext[T]) (res js.Value[T], err error) {
return codec.EncodeCallbackErrorf(cbCtx, "Response.Response_ok: Not implemented. Create an issue: https://github.com/gost-dom/browser/issues")
}

func Response_statusText[T any](cbCtx js.CallbackContext[T]) (res js.Value[T], err error) {
return codec.EncodeCallbackErrorf(cbCtx, "Response.Response_statusText: Not implemented. Create an issue: https://github.com/gost-dom/browser/issues")
}

func Response_headers[T any](cbCtx js.CallbackContext[T]) (res js.Value[T], err error) {
instance, err := js.As[*fetch.Response](cbCtx.Instance())
if err != nil {
Expand Down
51 changes: 51 additions & 0 deletions scripting/internal/scripttests/fetch_suite.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ func testFetch(t *testing.T, e html.ScriptEngine) {
t.Run("404 for not found resource", func(t *testing.T) { testNotFound(t, e) })
t.Run("ReadableStream body", func(t *testing.T) { testReadableStream(t, e) })
t.Run("Headers", func(t *testing.T) { testHeaders(t, e) })
t.Run("Response surface", func(t *testing.T) { testResponseSurface(t, e) })
t.Run("Request", func(t *testing.T) { testRequest(t, e) })
t.Run("Request-based Programmable delays", func(t *testing.T) { testProgrammableDelays(t, e) })
t.Run("Simple Programmable delays", func(t *testing.T) { testSimpleProgrammableDelays(t, e) })
Expand Down Expand Up @@ -466,6 +467,56 @@ func testRequest(t *testing.T, e html.ScriptEngine) {
})
}

// testResponseSurface exercises the Response.ok/status/statusText getters and
// the Body.text/json/bytes/arrayBuffer consumers against both a 200 and a 404
// response.
func testResponseSurface(t *testing.T, e html.ScriptEngine) {
ctx, cancel := context.WithTimeout(t.Context(), defaultTimeout)
defer cancel()

g := gomega.NewWithT(t)
handler := gosttest.HttpHandlerMap{
"/index.html": gosttest.StaticHTML(`<body>dummy</body>`),
"/data.json": gosttest.StaticJSON(`{"foo":"bar"}`),
}
win := openWindow(t, e, handler, "https://example.com/index.html")
win.MustRun(`
globalThis.res = {};
(async () => {
const ok = await fetch("/data.json");
res.ok = ok.ok;
res.status = ok.status;
res.statusText = ok.statusText;
res.text = await (await fetch("/data.json")).text();
res.json = JSON.stringify(await (await fetch("/data.json")).json());
const buf = await (await fetch("/data.json")).arrayBuffer();
res.arrayBufferIsArrayBuffer = buf instanceof ArrayBuffer;
res.arrayBuffer = buf.byteLength;
const bytes = await (await fetch("/data.json")).bytes();
res.bytesIsUint8Array = bytes instanceof Uint8Array;
res.bytes = bytes.length;
const notFound = await fetch("/does-not-exist");
res.notOk = notFound.ok;
res.notStatus = notFound.status;
res.notStatusText = notFound.statusText;
})();
`)
assert.NoError(t, win.Clock().ProcessEvents(ctx))

g.Expect(win.Eval("res.ok")).To(BeTrue(), "Response.ok for 200")
g.Expect(win.Eval("res.status")).To(BeEquivalentTo(200), "Response.status")
g.Expect(win.Eval("res.statusText")).To(Equal("OK"), "Response.statusText")
g.Expect(win.Eval("res.text")).To(Equal(`{"foo":"bar"}`), "Body.text()")
g.Expect(win.Eval("res.json")).To(Equal(`{"foo":"bar"}`), "Body.json()")
g.Expect(win.Eval("res.arrayBufferIsArrayBuffer")).To(BeTrue(), "Body.arrayBuffer() returns an ArrayBuffer")
g.Expect(win.Eval("res.arrayBuffer")).To(BeEquivalentTo(13), "Body.arrayBuffer() byteLength")
g.Expect(win.Eval("res.bytesIsUint8Array")).To(BeTrue(), "Body.bytes() returns a Uint8Array")
g.Expect(win.Eval("res.bytes")).To(BeEquivalentTo(13), "Body.bytes() length")
g.Expect(win.Eval("res.notOk")).To(BeFalse(), "Response.ok for 404")
g.Expect(win.Eval("res.notStatus")).To(BeEquivalentTo(404), "Response.status for 404")
Comment thread
coderabbitai[bot] marked this conversation as resolved.
g.Expect(win.Eval("res.notStatusText")).To(Equal("Not Found"), "Response.statusText for 404")
}

func testHeaders(t *testing.T, e html.ScriptEngine) {
t.Run("Throws on invalid value", func(t *testing.T) {
win := initWindow(t, e, nil)
Expand Down
Loading