diff --git a/README.md b/README.md
index 38d9a232..62bf936b 100644
--- a/README.md
+++ b/README.md
@@ -39,6 +39,8 @@ go get github.com/linkdata/jaws
After the dependency is added, your Go module will be able to import
and use JaWS as demonstrated below.
+For widget authoring guidance see `ui/README.md`.
+
## Quick start
The following minimal program renders a single range input whose value
@@ -53,8 +55,10 @@ import (
"html/template"
"log/slog"
"net/http"
+ "sync"
"github.com/linkdata/jaws"
+ "github.com/linkdata/jaws/ui"
)
const indexhtml = `
@@ -82,7 +86,7 @@ func main() {
var mu sync.Mutex
var f float64
- http.DefaultServeMux.Handle("/", jw.Handler("index", jaws.Bind(&mu, &f)))
+ http.DefaultServeMux.Handle("/", ui.Handler(jw, "index", jaws.Bind(&mu, &f)))
slog.Error(http.ListenAndServe("localhost:8080", nil).Error())
}
```
@@ -148,15 +152,19 @@ endpoints to be registered in whichever router you choose to use. All of
the endpoints start with "/jaws/", and `Jaws.ServeHTTP()` will handle all
of them.
-* `/jaws/jaws.*.js`
+* `/jaws/\.jaws\.[0-9a-z]+\.css`
+
+ Serves the built-in JaWS stylesheet.
+
+ The response should be cached indefinitely.
+
+* `/jaws/\.jaws\.[0-9a-z]+\.js`
- The exact URL is the value of `jaws.JavascriptPath`. It must return
- the client-side Javascript, the uncompressed contents of which can be had with
- `jaws.JavascriptText`, or a gzipped version with `jaws.JavascriptGZip`.
+ Serves the built-in JaWS client-side JavaScript.
The response should be cached indefinitely.
-* `/jaws/[0-9a-z]+`
+* `/jaws/[0-9a-z]+` (and `/jaws/[0-9a-z]+/noscript`)
The WebSocket endpoint. The trailing string must be decoded using
`jaws.JawsKeyValue()` and then the matching JaWS Request retrieved
@@ -172,7 +180,7 @@ of them.
come online. This is done in order to not spam the WebSocket endpoint with
connection requests, and browsers are better at handling XHR requests failing.
- If you don't have a JaWS object, or if it's completion channel is closed (see
+ If you don't have a JaWS object, or if its completion channel is closed (see
`Jaws.Done()`), return **503 Service Unavailable**. If you're ready to serve
requests, return **204 No Content**.
@@ -181,7 +189,10 @@ of them.
Handling the routes with the standard library's `http.DefaultServeMux`:
```go
-jw := jaws.New()
+jw, err := jaws.New()
+if err != nil {
+ panic(err)
+}
defer jw.Close()
go jw.Serve()
http.DefaultServeMux.Handle("/jaws/", jw)
@@ -190,7 +201,10 @@ http.DefaultServeMux.Handle("/jaws/", jw)
Handling the routes with [Echo](https://echo.labstack.com/):
```go
-jw := jaws.New()
+jw, err := jaws.New()
+if err != nil {
+ panic(err)
+}
defer jw.Close()
go jw.Serve()
router := echo.New()
@@ -208,7 +222,7 @@ be made into one using `jaws.MakeHTMLGetter()`.
In order of precedence, this can be:
* `jaws.HTMLGetter`: `JawsGetHTML(*Element) template.HTML` to be used as-is.
* `jaws.Getter[string]`: `JawsGet(*Element) string` that will be escaped using `html.EscapeString`.
-* `jaws.AnyGetter`: `JawsGetAny(*Element) any` that will be rendered using `fmt.Sprint()` and escaped using `html.EscapeString`.
+* `jaws.Formatter`: `Format("%v") string` that will be escaped using `html.EscapeString`.
* `fmt.Stringer`: `String() string` that will be escaped using `html.EscapeString`.
* a static `template.HTML` or `string` to be used as-is with no HTML escaping.
* everything else is rendered using `fmt.Sprint()` and escaped using `html.EscapeString`.
@@ -229,9 +243,12 @@ getters and on-success handlers.
### Session handling
JaWS has non-persistent session handling integrated. Sessions won't
-be persisted across restarts and must have an expiry time. A new
-session is created with `EnsureSession()` and sending it's `Cookie()`
-to the client browser.
+be persisted across restarts and must have an expiry time.
+
+Use one of these patterns:
+
+* Wrap page handlers with `Jaws.Session(handler)` to ensure a session exists.
+* Call `Jaws.NewSession(w, r)` explicitly to create and attach a fresh session cookie.
When subsequent Requests are created with `NewRequest()`, if the
HTTP request has the cookie set and comes from the correct IP,
@@ -251,11 +268,11 @@ default is `jaws`.
### A note on the Context
-The Request object embeds a context.Context inside it's struct,
+The Request object embeds a context.Context inside its struct,
contrary to recommended Go practice.
The reason is that there is no unbroken call chain from the time the Request
-object is created when the initial HTTP request comes in and when it's
+object is created when the initial HTTP request comes in and when it is
requested during the Javascript WebSocket HTTP request.
### Security of the WebSocket callback
diff --git a/jaws/LICENSE b/core/LICENSE
similarity index 100%
rename from jaws/LICENSE
rename to core/LICENSE
diff --git a/jaws/auth.go b/core/auth.go
similarity index 60%
rename from jaws/auth.go
rename to core/auth.go
index 886c03fd..1400e599 100644
--- a/jaws/auth.go
+++ b/core/auth.go
@@ -1,4 +1,4 @@
-package jaws
+package core
type Auth interface {
Data() map[string]any // returns authenticated user data, or nil
@@ -8,8 +8,8 @@ type Auth interface {
type MakeAuthFn func(*Request) Auth
-type defaultAuth struct{}
+type DefaultAuth struct{}
-func (defaultAuth) Data() map[string]any { return nil }
-func (defaultAuth) Email() string { return "" }
-func (defaultAuth) IsAdmin() bool { return true }
+func (DefaultAuth) Data() map[string]any { return nil }
+func (DefaultAuth) Email() string { return "" }
+func (DefaultAuth) IsAdmin() bool { return true }
diff --git a/jaws/auth_test.go b/core/auth_test.go
similarity index 84%
rename from jaws/auth_test.go
rename to core/auth_test.go
index 5b9a7438..081eadf0 100644
--- a/jaws/auth_test.go
+++ b/core/auth_test.go
@@ -1,11 +1,11 @@
-package jaws
+package core
import (
"testing"
)
func Test_defaultAuth(t *testing.T) {
- a := defaultAuth{}
+ a := DefaultAuth{}
if a.Data() != nil {
t.Fatal()
}
diff --git a/jaws/bind.go b/core/bind.go
similarity index 96%
rename from jaws/bind.go
rename to core/bind.go
index 5b3764bb..af4f200a 100644
--- a/jaws/bind.go
+++ b/core/bind.go
@@ -1,4 +1,4 @@
-package jaws
+package core
import (
"sync"
diff --git a/jaws/bind_test.go b/core/bind_test.go
similarity index 99%
rename from jaws/bind_test.go
rename to core/bind_test.go
index e3a0edbd..829183c8 100644
--- a/jaws/bind_test.go
+++ b/core/bind_test.go
@@ -1,4 +1,4 @@
-package jaws
+package core
import (
"io"
diff --git a/jaws/binder.go b/core/binder.go
similarity index 99%
rename from jaws/binder.go
rename to core/binder.go
index a95fa12c..588191fa 100644
--- a/jaws/binder.go
+++ b/core/binder.go
@@ -1,4 +1,4 @@
-package jaws
+package core
// BindSetHook is a function that replaces JawsSetLocked for a Binder.
//
diff --git a/jaws/binding.go b/core/binding.go
similarity index 99%
rename from jaws/binding.go
rename to core/binding.go
index 49b93f86..b73c6dd3 100644
--- a/jaws/binding.go
+++ b/core/binding.go
@@ -1,4 +1,4 @@
-package jaws
+package core
import (
"fmt"
diff --git a/jaws/bindinghook.go b/core/bindinghook.go
similarity index 99%
rename from jaws/bindinghook.go
rename to core/bindinghook.go
index 3aa4a3db..1956c2f3 100644
--- a/jaws/bindinghook.go
+++ b/core/bindinghook.go
@@ -1,4 +1,4 @@
-package jaws
+package core
import (
"fmt"
diff --git a/core/broadcast_test.go b/core/broadcast_test.go
new file mode 100644
index 00000000..ee979191
--- /dev/null
+++ b/core/broadcast_test.go
@@ -0,0 +1,81 @@
+package core
+
+import (
+ "html/template"
+ "testing"
+
+ "github.com/linkdata/jaws/what"
+)
+
+func TestCoverage_GenerateHeadAndConvenienceBroadcasts(t *testing.T) {
+ jw, err := New()
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer jw.Close()
+
+ if err := jw.GenerateHeadHTML("%zz"); err == nil {
+ t.Fatal("expected url parse error")
+ }
+ if err := jw.GenerateHeadHTML("/favicon.ico", "/app.js"); err != nil {
+ t.Fatal(err)
+ }
+
+ jw.Reload()
+ if msg := nextBroadcast(t, jw); msg.What != what.Reload {
+ t.Fatalf("unexpected reload msg %#v", msg)
+ }
+ jw.Redirect("/next")
+ if msg := nextBroadcast(t, jw); msg.What != what.Redirect || msg.Data != "/next" {
+ t.Fatalf("unexpected redirect msg %#v", msg)
+ }
+ jw.Alert("info", "hello")
+ if msg := nextBroadcast(t, jw); msg.What != what.Alert || msg.Data != "info\nhello" {
+ t.Fatalf("unexpected alert msg %#v", msg)
+ }
+
+ jw.SetInner("t", template.HTML("x"))
+ if msg := nextBroadcast(t, jw); msg.What != what.Inner || msg.Data != "x" {
+ t.Fatalf("unexpected set inner msg %#v", msg)
+ }
+ jw.SetAttr("t", "k", "v")
+ if msg := nextBroadcast(t, jw); msg.What != what.SAttr || msg.Data != "k\nv" {
+ t.Fatalf("unexpected set attr msg %#v", msg)
+ }
+ jw.RemoveAttr("t", "k")
+ if msg := nextBroadcast(t, jw); msg.What != what.RAttr || msg.Data != "k" {
+ t.Fatalf("unexpected remove attr msg %#v", msg)
+ }
+ jw.SetClass("t", "c")
+ if msg := nextBroadcast(t, jw); msg.What != what.SClass || msg.Data != "c" {
+ t.Fatalf("unexpected set class msg %#v", msg)
+ }
+ jw.RemoveClass("t", "c")
+ if msg := nextBroadcast(t, jw); msg.What != what.RClass || msg.Data != "c" {
+ t.Fatalf("unexpected remove class msg %#v", msg)
+ }
+ jw.SetValue("t", "v")
+ if msg := nextBroadcast(t, jw); msg.What != what.Value || msg.Data != "v" {
+ t.Fatalf("unexpected set value msg %#v", msg)
+ }
+ jw.Insert("t", "0", "a")
+ if msg := nextBroadcast(t, jw); msg.What != what.Insert || msg.Data != "0\na" {
+ t.Fatalf("unexpected insert msg %#v", msg)
+ }
+ jw.Replace("t", "0", "b")
+ if msg := nextBroadcast(t, jw); msg.What != what.Replace || msg.Data != "0\nb" {
+ t.Fatalf("unexpected replace msg %#v", msg)
+ }
+ jw.Delete("t")
+ if msg := nextBroadcast(t, jw); msg.What != what.Delete {
+ t.Fatalf("unexpected delete msg %#v", msg)
+ }
+ jw.Append("t", "c")
+ if msg := nextBroadcast(t, jw); msg.What != what.Append || msg.Data != "c" {
+ t.Fatalf("unexpected append msg %#v", msg)
+ }
+ jw.JsCall("t", "fn", `{"a":1}`)
+ if msg := nextBroadcast(t, jw); msg.What != what.Call || msg.Data != `fn={"a":1}` {
+ t.Fatalf("unexpected jscall msg %#v", msg)
+ }
+}
diff --git a/jaws/clickhandler.go b/core/clickhandler.go
similarity index 97%
rename from jaws/clickhandler.go
rename to core/clickhandler.go
index 9d17f21f..f0e3c81c 100644
--- a/jaws/clickhandler.go
+++ b/core/clickhandler.go
@@ -1,4 +1,4 @@
-package jaws
+package core
import "github.com/linkdata/jaws/what"
diff --git a/jaws/clickhandler_test.go b/core/clickhandler_test.go
similarity index 75%
rename from jaws/clickhandler_test.go
rename to core/clickhandler_test.go
index 268efea8..d79c04cb 100644
--- a/jaws/clickhandler_test.go
+++ b/core/clickhandler_test.go
@@ -1,6 +1,7 @@
-package jaws
+package core
import (
+ "html/template"
"testing"
"github.com/linkdata/jaws/what"
@@ -22,7 +23,7 @@ var _ ClickHandler = (*testJawsClick)(nil)
func Test_clickHandlerWapper_JawsEvent(t *testing.T) {
th := newTestHelper(t)
- nextJid = 0
+ NextJid = 0
rq := newTestRequest(t)
defer rq.Close()
@@ -32,12 +33,12 @@ func Test_clickHandlerWapper_JawsEvent(t *testing.T) {
}
want := `
inner
`
- rq.Div("inner", tjc)
+ rq.UI(testDivWidget{inner: template.HTML("inner")}, tjc)
if got := rq.BodyString(); got != want {
- t.Errorf("Request.Div() = %q, want %q", got, want)
+ t.Errorf("Request.UI(NewDiv()) = %q, want %q", got, want)
}
- rq.InCh <- wsMsg{Data: "text", Jid: 1, What: what.Input}
+ rq.InCh <- WsMsg{Data: "text", Jid: 1, What: what.Input}
select {
case <-th.C:
th.Timeout()
@@ -46,7 +47,7 @@ func Test_clickHandlerWapper_JawsEvent(t *testing.T) {
default:
}
- rq.InCh <- wsMsg{Data: "adam", Jid: 1, What: what.Click}
+ rq.InCh <- WsMsg{Data: "adam", Jid: 1, What: what.Click}
select {
case <-th.C:
th.Timeout()
diff --git a/jaws/container.go b/core/container.go
similarity index 93%
rename from jaws/container.go
rename to core/container.go
index 1a75da7d..b4218b31 100644
--- a/jaws/container.go
+++ b/core/container.go
@@ -1,4 +1,4 @@
-package jaws
+package core
type Container interface {
// JawsContains must return a slice of hashable UI objects. The slice contents must not be modified after returning it.
diff --git a/core/dateformat.go b/core/dateformat.go
new file mode 100644
index 00000000..958eef86
--- /dev/null
+++ b/core/dateformat.go
@@ -0,0 +1,4 @@
+package core
+
+// ISO8601 is the date format used by date input widgets (YYYY-MM-DD).
+const ISO8601 = "2006-01-02"
diff --git a/jaws/defaultcookiename.go b/core/defaultcookiename.go
similarity index 92%
rename from jaws/defaultcookiename.go
rename to core/defaultcookiename.go
index e30c5fca..f086960f 100644
--- a/jaws/defaultcookiename.go
+++ b/core/defaultcookiename.go
@@ -1,4 +1,4 @@
-package jaws
+package core
import (
"os"
@@ -22,7 +22,7 @@ func makeCookieName(exename string) (cookie string) {
var b []byte
for _, ch := range exename {
if ('a' <= ch && ch <= 'z') || ('A' <= ch && ch <= 'Z') || ('0' <= ch && ch <= '9') {
- b = append(b, byte(ch))
+ b = append(b, byte(ch)) //#nosec G115
}
}
if len(b) > 0 {
diff --git a/jaws/defaultcookiename_test.go b/core/defaultcookiename_test.go
similarity index 97%
rename from jaws/defaultcookiename_test.go
rename to core/defaultcookiename_test.go
index 584be928..f1babadc 100644
--- a/jaws/defaultcookiename_test.go
+++ b/core/defaultcookiename_test.go
@@ -1,4 +1,4 @@
-package jaws
+package core
import (
"path"
diff --git a/jaws/element.go b/core/element.go
similarity index 97%
rename from jaws/element.go
rename to core/element.go
index 63b3518c..9df20ab9 100644
--- a/jaws/element.go
+++ b/core/element.go
@@ -1,4 +1,4 @@
-package jaws
+package core
import (
"bytes"
@@ -26,6 +26,13 @@ func (e *Element) String() string {
return fmt.Sprintf("Element{%T, id=%q, Tags: %v}", e.Ui(), e.Jid(), e.Request.TagsOf(e))
}
+// AddHandler adds the given handlers to the Element.
+func (e *Element) AddHandlers(h ...EventHandler) {
+ if !e.deleted {
+ e.handlers = append(e.handlers, h...)
+ }
+}
+
// Tag adds the given tags to the Element.
func (e *Element) Tag(tags ...any) {
if !e.deleted {
@@ -102,7 +109,7 @@ func (e *Element) JawsUpdate() {
func (e *Element) queue(wht what.What, data string) {
if !e.deleted {
- e.Request.queue(wsMsg{
+ e.Request.queue(WsMsg{
Data: data,
Jid: e.jid,
What: wht,
diff --git a/jaws/element_test.go b/core/element_test.go
similarity index 97%
rename from jaws/element_test.go
rename to core/element_test.go
index 9cfb5eb3..699163ea 100644
--- a/jaws/element_test.go
+++ b/core/element_test.go
@@ -1,4 +1,4 @@
-package jaws
+package core
import (
"fmt"
@@ -112,7 +112,7 @@ func TestElement_Queued(t *testing.T) {
e.Order([]jid.Jid{1, 2})
replaceHTML := template.HTML(fmt.Sprintf("", e.Jid().String()))
e.Replace(replaceHTML)
- th.Equal(rq.wsQueue, []wsMsg{
+ th.Equal(rq.wsQueue, []WsMsg{
{
Data: "hidden\n",
Jid: e.jid,
@@ -168,7 +168,7 @@ func TestElement_Queued(t *testing.T) {
}
pendingRq := rq.Jaws.NewRequest(httptest.NewRequest(http.MethodGet, "/", nil))
- RequestWriter{pendingRq, httptest.NewRecorder()}.UI(tss)
+ testRequestWriter{rq: pendingRq, Writer: httptest.NewRecorder()}.UI(tss)
rq.UI(tss)
rq.Jaws.Dirty(tss)
diff --git a/jaws/errillegaltagtype.go b/core/errillegaltagtype.go
similarity index 96%
rename from jaws/errillegaltagtype.go
rename to core/errillegaltagtype.go
index 476d6130..e9399c24 100644
--- a/jaws/errillegaltagtype.go
+++ b/core/errillegaltagtype.go
@@ -1,4 +1,4 @@
-package jaws
+package core
import "fmt"
diff --git a/jaws/errnotcomparable.go b/core/errnotcomparable.go
similarity index 97%
rename from jaws/errnotcomparable.go
rename to core/errnotcomparable.go
index 3f9ac126..9f772ef0 100644
--- a/jaws/errnotcomparable.go
+++ b/core/errnotcomparable.go
@@ -1,4 +1,4 @@
-package jaws
+package core
import (
"reflect"
diff --git a/jaws/errnowebsocketrequest.go b/core/errnowebsocketrequest.go
similarity index 97%
rename from jaws/errnowebsocketrequest.go
rename to core/errnowebsocketrequest.go
index fbf88cb6..b5ee3f2f 100644
--- a/jaws/errnowebsocketrequest.go
+++ b/core/errnowebsocketrequest.go
@@ -1,4 +1,4 @@
-package jaws
+package core
import (
"fmt"
diff --git a/jaws/errpendingcancelled.go b/core/errpendingcancelled.go
similarity index 98%
rename from jaws/errpendingcancelled.go
rename to core/errpendingcancelled.go
index 133d3e8c..124537e1 100644
--- a/jaws/errpendingcancelled.go
+++ b/core/errpendingcancelled.go
@@ -1,4 +1,4 @@
-package jaws
+package core
import (
"fmt"
diff --git a/jaws/errvalueunchanged.go b/core/errvalueunchanged.go
similarity index 94%
rename from jaws/errvalueunchanged.go
rename to core/errvalueunchanged.go
index e8d81955..8321dd2b 100644
--- a/jaws/errvalueunchanged.go
+++ b/core/errvalueunchanged.go
@@ -1,4 +1,4 @@
-package jaws
+package core
import "errors"
diff --git a/jaws/eventhandler.go b/core/eventhandler.go
similarity index 94%
rename from jaws/eventhandler.go
rename to core/eventhandler.go
index 4f46d816..a704423f 100644
--- a/jaws/eventhandler.go
+++ b/core/eventhandler.go
@@ -1,4 +1,4 @@
-package jaws
+package core
import (
"github.com/linkdata/jaws/what"
@@ -44,7 +44,7 @@ func callEventHandler(obj any, e *Element, wht what.What, val string) (err error
return ErrEventUnhandled
}
-func callEventHandlers(ui any, e *Element, wht what.What, val string) (err error) {
+func CallEventHandlers(ui any, e *Element, wht what.What, val string) (err error) {
if err = callEventHandler(ui, e, wht, val); err == ErrEventUnhandled {
for _, h := range e.handlers {
if err = callEventHandler(h, e, wht, val); err != ErrEventUnhandled {
diff --git a/core/extra_test.go b/core/extra_test.go
new file mode 100644
index 00000000..858e2ed5
--- /dev/null
+++ b/core/extra_test.go
@@ -0,0 +1,195 @@
+package core
+
+import (
+ "io"
+ "net/http/httptest"
+ "strings"
+ "testing"
+
+ "github.com/linkdata/deadlock"
+ "github.com/linkdata/jaws/what"
+)
+
+type testStringTag struct{}
+
+func (testStringTag) String() string { return "str" }
+
+type testApplyGetterAll struct {
+ initErr error
+}
+
+func (a testApplyGetterAll) JawsGetTag(*Request) any { return Tag("tg") }
+func (a testApplyGetterAll) JawsClick(*Element, string) error {
+ return ErrEventUnhandled
+}
+func (a testApplyGetterAll) JawsEvent(*Element, what.What, string) error {
+ return ErrEventUnhandled
+}
+func (a testApplyGetterAll) JawsInit(*Element) error {
+ return a.initErr
+}
+
+func TestCoverage_getterSetterFactories(t *testing.T) {
+ g := MakeGetter[string]("x")
+ if g.JawsGet(nil) != "x" {
+ t.Fatal("unexpected getter value")
+ }
+ if tag := g.(TagGetter).JawsGetTag(nil); tag != nil {
+ t.Fatal("expected nil tag")
+ }
+ // makeGetter Getter[T] passthrough branch.
+ g2 := MakeGetter[string](Getter[string](getterStatic[string]{v: "y"}))
+ if g2.JawsGet(nil) != "y" {
+ t.Fatal("unexpected passthrough getter value")
+ }
+
+ s := MakeSetter[string]("x")
+ if s.JawsGet(nil) != "x" {
+ t.Fatal("unexpected setter getter value")
+ }
+ if err := s.JawsSet(nil, "x"); err != ErrValueNotSettable {
+ t.Fatalf("unexpected err: %v", err)
+ }
+ // makeSetter Setter[T] passthrough branch.
+ s2 := MakeSetter[string](Setter[string](setterStatic[string]{v: "z"}))
+ if s2.JawsGet(nil) != "z" {
+ t.Fatal("unexpected passthrough setter value")
+ }
+}
+
+func TestCoverage_miscBranches(t *testing.T) {
+ NextJid = 0
+ rq := newTestRequest(t)
+ defer rq.Close()
+
+ // TagString pointer branch + fmt.Stringer branch
+ _ = TagString(&testStringTag{})
+ _ = TagString(testStringTag{})
+
+ // Register UI render
+ reg := testRegisterUI{Updater: &testUi{}}
+ elem := rq.NewElement(reg)
+ if err := elem.JawsRender(nil, nil); err != nil {
+ t.Fatal(err)
+ }
+
+ // RequestWriter.Initial
+ if rq.Request.Initial() == nil {
+ t.Fatal("expected initial request")
+ }
+ if rq.Initial() == nil {
+ t.Fatal("expected initial request from writer")
+ }
+
+ // DeleteElement exported wrapper
+ e2 := rq.NewElement(&testUi{})
+ id2 := e2.Jid()
+ rq.DeleteElement(e2)
+ if rq.GetElementByJid(id2) != nil {
+ t.Fatal("expected deleted element")
+ }
+
+}
+
+func TestCoverage_namedBoolOptionAndContains(t *testing.T) {
+ NextJid = 0
+ rq := newTestRequest(t)
+ defer rq.Close()
+
+ nba := NewNamedBoolArray().Add("1", "one")
+ nba.Set("1", true)
+ contents := nba.JawsContains(nil)
+ if len(contents) != 1 {
+ t.Fatalf("want 1 content got %d", len(contents))
+ }
+ elem := rq.NewElement(contents[0])
+ var sb strings.Builder
+ if err := elem.JawsRender(&sb, []any{"hidden"}); err != nil {
+ t.Fatal(err)
+ }
+ if !strings.Contains(sb.String(), "selected") {
+ t.Fatal("expected selected option rendering")
+ }
+ contents[0].JawsUpdate(elem)
+ nba.Set("1", false)
+ contents[0].JawsUpdate(elem)
+}
+
+func TestCoverage_elementBranches(t *testing.T) {
+ NextJid = 0
+ jw, err := New()
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer jw.Close()
+ rq := jw.NewRequest(httptest.NewRequest("GET", "/", nil))
+
+ tu := &testUi{renderFn: func(*Element, io.Writer, []any) error { return nil }}
+ elem := rq.NewElement(tu)
+
+ // renderDebug n/a branch when request lock is held.
+ rq.mu.Lock()
+ var sb strings.Builder
+ elem.renderDebug(&sb)
+ rq.mu.Unlock()
+ // renderDebug tag join branch when lock is available and multiple tags exist.
+ elem.Tag(Tag("a"), Tag("b"))
+ sb.Reset()
+ elem.renderDebug(&sb)
+ if !strings.Contains(sb.String(), ", ") {
+ t.Fatal("expected comma-separated tags in debug output")
+ }
+ // JawsRender debug path.
+ rq.Jaws.Debug = true
+ sb.Reset()
+ if err := elem.JawsRender(&sb, nil); err != nil {
+ t.Fatal(err)
+ }
+ rq.Jaws.Debug = false
+
+ // deleted branch in Element.JawsRender/JawsUpdate
+ rq.deleteElement(elem)
+ if err := elem.JawsRender(&sb, nil); err != nil {
+ t.Fatal(err)
+ }
+ elem.JawsUpdate()
+}
+
+func TestCoverage_applyGetterBranchesAndDebugNewElement(t *testing.T) {
+ NextJid = 0
+ rq := newTestRequest(t)
+ defer rq.Close()
+ elem := rq.NewElement(&testUi{})
+
+ // nil getter
+ if tag, err := elem.ApplyGetter(nil); tag != nil || err != nil {
+ t.Fatalf("unexpected %v %v", tag, err)
+ }
+
+ // getter with tag/click/event/init handler
+ ag := testApplyGetterAll{}
+ if tag, err := elem.ApplyGetter(ag); tag != Tag("tg") || err != nil {
+ t.Fatalf("unexpected %v %v", tag, err)
+ }
+ agErr := testApplyGetterAll{initErr: ErrNotComparable}
+ if _, err := elem.ApplyGetter(agErr); err != ErrNotComparable {
+ t.Fatalf("expected init err, got %v", err)
+ }
+
+ // This branch only exists in deadlock debug builds.
+ if deadlock.Debug {
+ defer func() {
+ if recover() == nil {
+ t.Fatal("expected panic for non-comparable UI in debug mode")
+ }
+ }()
+ rq.NewElement(testUnhashableUI{m: map[string]int{"x": 1}})
+ }
+}
+
+type testUnhashableUI struct {
+ m map[string]int
+}
+
+func (testUnhashableUI) JawsRender(*Element, io.Writer, []any) error { return nil }
+func (testUnhashableUI) JawsUpdate(*Element) {}
diff --git a/jaws/getter.go b/core/getter.go
similarity index 91%
rename from jaws/getter.go
rename to core/getter.go
index d61c4db3..c30db190 100644
--- a/jaws/getter.go
+++ b/core/getter.go
@@ -1,4 +1,4 @@
-package jaws
+package core
import (
"errors"
@@ -31,7 +31,7 @@ func makeStaticGetter[T comparable](v T) Getter[T] {
return getterStatic[T]{v}
}
-func makeGetter[T comparable](v any) Getter[T] {
+func MakeGetter[T comparable](v any) Getter[T] {
switch v := v.(type) {
case Getter[T]:
return v
diff --git a/jaws/getter_test.go b/core/getter_test.go
similarity index 77%
rename from jaws/getter_test.go
rename to core/getter_test.go
index 61adcadc..ddcb27f7 100644
--- a/jaws/getter_test.go
+++ b/core/getter_test.go
@@ -1,4 +1,4 @@
-package jaws
+package core
import "testing"
@@ -8,12 +8,12 @@ func Test_makeGetter_panic(t *testing.T) {
t.Fail()
}
}()
- setter2 := makeGetter[string](123)
+ setter2 := MakeGetter[string](123)
t.Error(setter2)
}
func Test_makeGetter(t *testing.T) {
- setter := makeGetter[string]("foo")
+ setter := MakeGetter[string]("foo")
if err := setter.(Setter[string]).JawsSet(nil, "bar"); err != ErrValueNotSettable {
t.Error(err)
}
diff --git a/core/helpers_test.go b/core/helpers_test.go
new file mode 100644
index 00000000..9893adfd
--- /dev/null
+++ b/core/helpers_test.go
@@ -0,0 +1,24 @@
+package core
+
+import (
+ "io"
+ "testing"
+ "time"
+)
+
+func nextBroadcast(t *testing.T, jw *Jaws) Message {
+ t.Helper()
+ select {
+ case msg := <-jw.bcastCh:
+ return msg
+ case <-time.After(time.Second):
+ t.Fatal("timeout waiting for broadcast")
+ return Message{}
+ }
+}
+
+type errReader struct{}
+
+func (errReader) Read([]byte) (int, error) {
+ return 0, io.EOF
+}
diff --git a/jaws/htmlgetter.go b/core/htmlgetter.go
similarity index 93%
rename from jaws/htmlgetter.go
rename to core/htmlgetter.go
index 5ea71324..d8657fd6 100644
--- a/jaws/htmlgetter.go
+++ b/core/htmlgetter.go
@@ -1,4 +1,4 @@
-package jaws
+package core
import (
"html/template"
diff --git a/jaws/htmlgetterfunc.go b/core/htmlgetterfunc.go
similarity index 97%
rename from jaws/htmlgetterfunc.go
rename to core/htmlgetterfunc.go
index 629e355e..4e4d10e3 100644
--- a/jaws/htmlgetterfunc.go
+++ b/core/htmlgetterfunc.go
@@ -1,4 +1,4 @@
-package jaws
+package core
import "html/template"
diff --git a/jaws/htmlgetterfunc_test.go b/core/htmlgetterfunc_test.go
similarity index 96%
rename from jaws/htmlgetterfunc_test.go
rename to core/htmlgetterfunc_test.go
index 9bcb58b3..25751bec 100644
--- a/jaws/htmlgetterfunc_test.go
+++ b/core/htmlgetterfunc_test.go
@@ -1,4 +1,4 @@
-package jaws
+package core
import (
"html/template"
diff --git a/core/ids_test.go b/core/ids_test.go
new file mode 100644
index 00000000..fe7e7339
--- /dev/null
+++ b/core/ids_test.go
@@ -0,0 +1,99 @@
+package core
+
+import (
+ "bufio"
+ "bytes"
+ "html/template"
+ "net/http"
+ "net/http/httptest"
+ "strings"
+ "testing"
+)
+
+func TestCoverage_IDAndLookupHelpers(t *testing.T) {
+ NextJid = 0
+ if a, b := NextID(), NextID(); b <= a {
+ t.Fatalf("expected increasing ids, got %d then %d", a, b)
+ }
+ if got := string(AppendID([]byte("x"))); !strings.HasPrefix(got, "x") || len(got) <= 1 {
+ t.Fatalf("unexpected append id result %q", got)
+ }
+ if got := MakeID(); !strings.HasPrefix(got, "jaws.") {
+ t.Fatalf("unexpected id %q", got)
+ }
+
+ jw, err := New()
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer jw.Close()
+
+ tmpl := template.Must(template.New("it").Parse(`ok`))
+ jw.AddTemplateLookuper(tmpl)
+ if got := jw.LookupTemplate("it"); got == nil {
+ t.Fatal("expected found template")
+ }
+ if got := jw.LookupTemplate("missing"); got != nil {
+ t.Fatal("expected missing template")
+ }
+ jw.RemoveTemplateLookuper(nil)
+ jw.RemoveTemplateLookuper(tmpl)
+
+ hr := httptest.NewRequest(http.MethodGet, "/", nil)
+ rq := jw.NewRequest(hr)
+ if rq == nil {
+ t.Fatal("expected request")
+ }
+ if got := jw.RequestCount(); got != 1 {
+ t.Fatalf("expected one request, got %d", got)
+ }
+ jw.recycle(rq)
+ if got := jw.RequestCount(); got != 0 {
+ t.Fatalf("expected zero requests, got %d", got)
+ }
+}
+
+func TestCoverage_CookieParseAndIP(t *testing.T) {
+ h := http.Header{}
+ h.Add("Cookie", `a=1; jaws=`+JawsKeyString(11)+`; x=2`)
+ h.Add("Cookie", `jaws="`+JawsKeyString(12)+`"`)
+ h.Add("Cookie", `jaws=not-a-key`)
+
+ ids := getCookieSessionsIds(h, "jaws")
+ if len(ids) != 2 || ids[0] != 11 || ids[1] != 12 {
+ t.Fatalf("unexpected cookie ids %#v", ids)
+ }
+
+ if got := parseIP("127.0.0.1:1234"); !got.IsValid() {
+ t.Fatalf("expected parsed host:port ip, got %v", got)
+ }
+ if got := parseIP("::1"); !got.IsValid() {
+ t.Fatalf("expected parsed direct ip, got %v", got)
+ }
+ if got := parseIP(""); got.IsValid() {
+ t.Fatalf("expected invalid ip for empty remote addr, got %v", got)
+ }
+}
+
+func TestCoverage_NonZeroRandomAndPanic(t *testing.T) {
+ jw, err := New()
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer jw.Close()
+
+ // First random value is zero, second is one.
+ zeroThenOne := append(make([]byte, 8), []byte{1, 0, 0, 0, 0, 0, 0, 0}...)
+ jw.kg = bufio.NewReader(bytes.NewReader(zeroThenOne))
+ if got := jw.nonZeroRandomLocked(); got != 1 {
+ t.Fatalf("unexpected non-zero random value %d", got)
+ }
+
+ defer func() {
+ if recover() == nil {
+ t.Fatal("expected panic on random source read error")
+ }
+ }()
+ jw.kg = bufio.NewReader(errReader{})
+ _ = jw.nonZeroRandomLocked()
+}
diff --git a/jaws/inithandler.go b/core/inithandler.go
similarity index 96%
rename from jaws/inithandler.go
rename to core/inithandler.go
index 079c4ca3..df13b720 100644
--- a/jaws/inithandler.go
+++ b/core/inithandler.go
@@ -1,4 +1,4 @@
-package jaws
+package core
// InitHandler allows initializing UI getters and setters before their use.
//
diff --git a/jaws/jaws.css b/core/jaws.css
similarity index 100%
rename from jaws/jaws.css
rename to core/jaws.css
diff --git a/jaws/jaws.go b/core/jaws.go
similarity index 99%
rename from jaws/jaws.go
rename to core/jaws.go
index 7fd38f0a..6e833010 100644
--- a/jaws/jaws.go
+++ b/core/jaws.go
@@ -1,10 +1,10 @@
-// package jaws provides a mechanism to create dynamic
+// package core provides a mechanism to create dynamic
// webpages using Javascript and WebSockets.
//
// It integrates well with Go's html/template package,
// but can be used without it. It can be used with any
// router that supports the standard ServeHTTP interface.
-package jaws
+package core
import (
"bufio"
@@ -207,7 +207,7 @@ func (jw *Jaws) MustLog(err error) {
// NextID returns a uint64 unique within lifetime of the program.
func NextID() int64 {
- return atomic.AddInt64((*int64)(&nextJid), 1)
+ return atomic.AddInt64((*int64)(&NextJid), 1)
}
// AppendID appends the result of NextID() in text form to the given slice.
diff --git a/jaws/jaws.js b/core/jaws.js
similarity index 100%
rename from jaws/jaws.js
rename to core/jaws.js
diff --git a/jaws/jawsevent_test.go b/core/jawsevent_test.go
similarity index 89%
rename from jaws/jawsevent_test.go
rename to core/jawsevent_test.go
index 4af7a00d..49a53f3a 100644
--- a/jaws/jawsevent_test.go
+++ b/core/jawsevent_test.go
@@ -1,4 +1,4 @@
-package jaws
+package core
import (
"fmt"
@@ -57,7 +57,7 @@ var _ UI = (*testJawsEvent)(nil)
func Test_JawsEvent_ClickUnhandled(t *testing.T) {
th := newTestHelper(t)
- nextJid = 0
+ NextJid = 0
rq := newTestRequest(t)
defer rq.Close()
@@ -68,7 +68,7 @@ func Test_JawsEvent_ClickUnhandled(t *testing.T) {
id := rq.Register(zomgItem, je, "attr1", []string{"attr2"}, template.HTMLAttr("attr3"), []template.HTMLAttr{"attr4"})
je.clickerr = ErrEventUnhandled
- rq.InCh <- wsMsg{Data: "name", Jid: id, What: what.Click}
+ rq.InCh <- WsMsg{Data: "name", Jid: id, What: what.Click}
select {
case <-th.C:
th.Timeout()
@@ -81,7 +81,7 @@ func Test_JawsEvent_ClickUnhandled(t *testing.T) {
func Test_JawsEvent_AllUnhandled(t *testing.T) {
th := newTestHelper(t)
- nextJid = 0
+ NextJid = 0
rq := newTestRequest(t)
defer rq.Close()
@@ -93,7 +93,7 @@ func Test_JawsEvent_AllUnhandled(t *testing.T) {
je.clickerr = ErrEventUnhandled
je.eventerr = ErrEventUnhandled
- rq.InCh <- wsMsg{Data: "name", Jid: id, What: what.Click}
+ rq.InCh <- WsMsg{Data: "name", Jid: id, What: what.Click}
select {
case <-th.C:
th.Timeout()
@@ -125,21 +125,21 @@ func (t *testJawsEventHandler) JawsEvent(e *Element, wht what.What, val string)
func Test_JawsEvent_ExtraHandler(t *testing.T) {
th := newTestHelper(t)
- nextJid = 0
+ NextJid = 0
rq := newTestRequest(t)
defer rq.Close()
msgCh := make(chan string, 1)
defer close(msgCh)
- je := NewUiDiv(&testJawsEventHandler{msgCh: msgCh})
+ je := &testJawsEventHandler{msgCh: msgCh}
var sb strings.Builder
- elem := rq.NewElement(je)
- th.NoErr(je.JawsRender(elem, &sb, nil))
+ elem := rq.NewElement(testDivWidget{inner: "tjEH"})
+ th.NoErr(elem.JawsRender(&sb, []any{je}))
th.Equal(sb.String(), "tjEH
")
- rq.InCh <- wsMsg{Data: "name", Jid: 1, What: what.Click}
+ rq.InCh <- WsMsg{Data: "name", Jid: 1, What: what.Click}
select {
case <-th.C:
th.Timeout()
diff --git a/jaws/js.go b/core/js.go
similarity index 99%
rename from jaws/js.go
rename to core/js.go
index 55836adb..fc2d251c 100644
--- a/jaws/js.go
+++ b/core/js.go
@@ -1,4 +1,4 @@
-package jaws
+package core
import (
_ "embed"
diff --git a/jaws/js_test.go b/core/js_test.go
similarity index 99%
rename from jaws/js_test.go
rename to core/js_test.go
index d71771a2..f7236d14 100644
--- a/jaws/js_test.go
+++ b/core/js_test.go
@@ -1,4 +1,4 @@
-package jaws
+package core
import (
_ "embed"
diff --git a/jaws/logger.go b/core/logger.go
similarity index 92%
rename from jaws/logger.go
rename to core/logger.go
index 36f5e4f5..af09c2c8 100644
--- a/jaws/logger.go
+++ b/core/logger.go
@@ -1,4 +1,4 @@
-package jaws
+package core
// Logger matches the log/slog.Logger interface.
type Logger interface {
diff --git a/jaws/makehtmlgetter.go b/core/makehtmlgetter.go
similarity index 82%
rename from jaws/makehtmlgetter.go
rename to core/makehtmlgetter.go
index 0a1bc70e..f7d08d35 100644
--- a/jaws/makehtmlgetter.go
+++ b/core/makehtmlgetter.go
@@ -1,4 +1,4 @@
-package jaws
+package core
import (
"fmt"
@@ -40,9 +40,9 @@ func (g htmlGetterString) JawsGetTag(rq *Request) any {
//
// Depending on the type of v, we return:
//
-// - jaws.HTMLGetter: `JawsGetHTML(e *Element) template.HTML` to be used as-is.
-// - jaws.Getter[string]: `JawsGet(elem *Element) string` that will be escaped using `html.EscapeString`.
-// - jaws.AnyGetter: `JawsGetAny(elem *Element) any` that will be rendered using `fmt.Sprint()` and escaped using `html.EscapeString`.
+// - HTMLGetter: `JawsGetHTML(e *Element) template.HTML` to be used as-is.
+// - Getter[string]: `JawsGet(elem *Element) string` that will be escaped using `html.EscapeString`.
+// - Formatter: `Format("%v") string` that will be escaped using `html.EscapeString`.
// - fmt.Stringer: `String() string` that will be escaped using `html.EscapeString`.
// - a static `template.HTML` or `string` to be used as-is with no HTML escaping.
// - everything else is rendered using `fmt.Sprint()` and escaped using `html.EscapeString`.
diff --git a/jaws/makehtmlgetter_test.go b/core/makehtmlgetter_test.go
similarity index 95%
rename from jaws/makehtmlgetter_test.go
rename to core/makehtmlgetter_test.go
index 71dcdc55..5a6b9b24 100644
--- a/jaws/makehtmlgetter_test.go
+++ b/core/makehtmlgetter_test.go
@@ -1,4 +1,4 @@
-package jaws
+package core
import (
"html"
@@ -8,6 +8,12 @@ import (
"testing"
)
+type testStringer struct{}
+
+func (testStringer) String() string {
+ return ""
+}
+
/*type testAnySetter struct {
Value any
}
diff --git a/jaws/message.go b/core/message.go
similarity index 97%
rename from jaws/message.go
rename to core/message.go
index 265ba1c1..9b7ac7e4 100644
--- a/jaws/message.go
+++ b/core/message.go
@@ -1,4 +1,4 @@
-package jaws
+package core
import (
"fmt"
diff --git a/jaws/message_test.go b/core/message_test.go
similarity index 96%
rename from jaws/message_test.go
rename to core/message_test.go
index d4a52a96..fc0da049 100644
--- a/jaws/message_test.go
+++ b/core/message_test.go
@@ -1,4 +1,4 @@
-package jaws
+package core
import (
"testing"
diff --git a/jaws/namedbool.go b/core/namedbool.go
similarity index 99%
rename from jaws/namedbool.go
rename to core/namedbool.go
index 6bfffb0b..ffdbdc6d 100644
--- a/jaws/namedbool.go
+++ b/core/namedbool.go
@@ -1,4 +1,4 @@
-package jaws
+package core
import (
"fmt"
diff --git a/jaws/namedbool_test.go b/core/namedbool_test.go
similarity index 90%
rename from jaws/namedbool_test.go
rename to core/namedbool_test.go
index 51907684..05df225a 100644
--- a/jaws/namedbool_test.go
+++ b/core/namedbool_test.go
@@ -1,4 +1,4 @@
-package jaws
+package core
import (
"html/template"
@@ -13,7 +13,7 @@ func TestNamedBool(t *testing.T) {
nb := nba.data[0]
rq := newTestRequest(t)
- e := rq.NewElement(NewUiCheckbox(nb))
+ e := rq.NewElement(&testUi{})
defer rq.Close()
is.Equal(nba, nb.Array())
diff --git a/jaws/namedboolarray.go b/core/namedboolarray.go
similarity index 98%
rename from jaws/namedboolarray.go
rename to core/namedboolarray.go
index 899dfef3..1817ccb1 100644
--- a/jaws/namedboolarray.go
+++ b/core/namedboolarray.go
@@ -1,4 +1,4 @@
-package jaws
+package core
import (
"html/template"
@@ -40,7 +40,7 @@ func (nba *NamedBoolArray) WriteLocked(fn func(nbl []*NamedBool) []*NamedBool) {
func (nba *NamedBoolArray) JawsContains(e *Element) (contents []UI) {
nba.mu.RLock()
for _, nb := range nba.data {
- contents = append(contents, UiOption{nb})
+ contents = append(contents, namedBoolOption{nb})
}
nba.mu.RUnlock()
return
diff --git a/jaws/namedboolarray_test.go b/core/namedboolarray_test.go
similarity index 97%
rename from jaws/namedboolarray_test.go
rename to core/namedboolarray_test.go
index 6db85b95..5e81a83e 100644
--- a/jaws/namedboolarray_test.go
+++ b/core/namedboolarray_test.go
@@ -1,4 +1,4 @@
-package jaws
+package core
import (
"html/template"
@@ -79,7 +79,7 @@ func Test_NamedBoolArray(t *testing.T) {
is.Equal(nba.IsChecked("2"), true)
rq := newTestRequest(t)
- e := rq.NewElement(NewUiSelect(nba))
+ e := rq.NewElement(&testUi{})
defer rq.Close()
is.Equal(nba.JawsGet(e), "2")
diff --git a/jaws/uioption.go b/core/namedbooloption.go
similarity index 56%
rename from jaws/uioption.go
rename to core/namedbooloption.go
index aa4748f1..d85e59ed 100644
--- a/jaws/uioption.go
+++ b/core/namedbooloption.go
@@ -1,4 +1,4 @@
-package jaws
+package core
import (
"html"
@@ -6,9 +6,13 @@ import (
"io"
)
-type UiOption struct{ *NamedBool }
+// namedBoolOption is an internal UI wrapper used by NamedBoolArray.JawsContains.
+// It intentionally stays unexported; public option widgets live in package ui.
+type namedBoolOption struct {
+ *NamedBool
+}
-func (ui UiOption) JawsRender(e *Element, w io.Writer, params []any) error {
+func (ui namedBoolOption) JawsRender(e *Element, w io.Writer, params []any) error {
e.Tag(ui.NamedBool)
attrs := e.ApplyParams(params)
valattr := template.HTMLAttr(`value="` + html.EscapeString(ui.Name()) + `"`) // #nosec G203
@@ -19,7 +23,7 @@ func (ui UiOption) JawsRender(e *Element, w io.Writer, params []any) error {
return WriteHTMLInner(w, e.Jid(), "option", "", ui.JawsGetHTML(e), attrs...)
}
-func (ui UiOption) JawsUpdate(e *Element) {
+func (ui namedBoolOption) JawsUpdate(e *Element) {
if ui.Checked() {
e.SetAttr("selected", "")
} else {
diff --git a/jaws/parseparams.go b/core/parseparams.go
similarity index 98%
rename from jaws/parseparams.go
rename to core/parseparams.go
index 6cd55d0f..7627c7c6 100644
--- a/jaws/parseparams.go
+++ b/core/parseparams.go
@@ -1,4 +1,4 @@
-package jaws
+package core
import "html/template"
diff --git a/jaws/renderer.go b/core/renderer.go
similarity index 95%
rename from jaws/renderer.go
rename to core/renderer.go
index 22a242b6..9d157920 100644
--- a/jaws/renderer.go
+++ b/core/renderer.go
@@ -1,4 +1,4 @@
-package jaws
+package core
import "io"
diff --git a/jaws/request.go b/core/request.go
similarity index 93%
rename from jaws/request.go
rename to core/request.go
index fc94b5af..09adb172 100644
--- a/jaws/request.go
+++ b/core/request.go
@@ -1,4 +1,4 @@
-package jaws
+package core
import (
"context"
@@ -34,7 +34,7 @@ type Request struct {
Jaws *Jaws // (read-only) the JaWS instance the Request belongs to
JawsKey uint64 // (read-only) a random number used in the WebSocket URI to identify this Request
remoteIP netip.Addr // (read-only) remote IP, or nil
- rendering atomic.Bool // set to true by RequestWriter.Write()
+ Rendering atomic.Bool // set to true by RequestWriter.Write()
running atomic.Bool // if ServeHTTP() is running
claimed atomic.Bool // if UseRequest() has been called for it
mu deadlock.RWMutex // protects following
@@ -49,7 +49,7 @@ type Request struct {
elems []*Element // our Elements
tagMap map[any][]*Element // maps tags to Elements
muQueue deadlock.Mutex // protects wsQueue
- wsQueue []wsMsg // queued messages to send
+ wsQueue []WsMsg // queued messages to send
}
type eventFnCall struct {
@@ -257,7 +257,7 @@ func (rq *Request) SetContext(fn func(oldctx context.Context) (newctx context.Co
func (rq *Request) maintenance(now time.Time, requestTimeout time.Duration) bool {
if !rq.running.Load() {
- if rq.rendering.Swap(false) {
+ if rq.Rendering.Swap(false) {
rq.mu.Lock()
rq.lastWrite = now
rq.mu.Unlock()
@@ -369,11 +369,12 @@ func (rq *Request) wantMessage(msg *Message) (yes bool) {
return
}
-var nextJid Jid
+// NextJid is the next Jid that should be used. Used when testing. Do not modify it outside of tests.
+var NextJid Jid
func (rq *Request) newElementLocked(ui UI) (elem *Element) {
elem = &Element{
- jid: Jid(atomic.AddInt64((*int64)(&nextJid), 1)),
+ jid: Jid(atomic.AddInt64((*int64)(&NextJid), 1)),
ui: ui,
Request: rq,
}
@@ -435,7 +436,7 @@ func (rq *Request) appendDirtyTags(tags []any) {
}
// Tag adds the given tags to the given Element.
-func (rq *Request) tagExpanded(elem *Element, expandedtags []any) {
+func (rq *Request) TagExpanded(elem *Element, expandedtags []any) {
rq.mu.Lock()
defer rq.mu.Unlock()
for _, tag := range expandedtags {
@@ -448,7 +449,7 @@ func (rq *Request) tagExpanded(elem *Element, expandedtags []any) {
// Tag adds the given tags to the given Element.
func (rq *Request) Tag(elem *Element, tags ...any) {
if elem != nil && len(tags) > 0 && elem.Request == rq {
- rq.tagExpanded(elem, MustTagExpand(elem.Request, tags))
+ rq.TagExpanded(elem, MustTagExpand(elem.Request, tags))
}
}
@@ -472,7 +473,7 @@ func (rq *Request) GetElements(tagitem any) (elems []*Element) {
}
// process is the main message processing loop. Will unsubscribe broadcastMsgCh and close outboundMsgCh on exit.
-func (rq *Request) process(broadcastMsgCh chan Message, incomingMsgCh <-chan wsMsg, outboundMsgCh chan<- wsMsg) {
+func (rq *Request) process(broadcastMsgCh chan Message, incomingMsgCh <-chan WsMsg, outboundMsgCh chan<- WsMsg) {
jawsDoneCh := rq.Jaws.Done()
httpDoneCh := rq.httpDoneCh
eventDoneCh := make(chan struct{})
@@ -504,7 +505,7 @@ func (rq *Request) process(broadcastMsgCh chan Message, incomingMsgCh <-chan wsM
for {
var tagmsg Message
- var wsmsg wsMsg
+ var wsmsg WsMsg
var ok bool
rq.sendQueue(outboundMsgCh)
@@ -551,7 +552,7 @@ func (rq *Request) process(broadcastMsgCh chan Message, incomingMsgCh <-chan wsM
case *Request:
case string:
// target is a regular HTML ID
- rq.queue(wsMsg{
+ rq.queue(WsMsg{
Data: v + "\t" + strconv.Quote(tagmsg.Data),
What: tagmsg.What,
Jid: -1,
@@ -562,7 +563,7 @@ func (rq *Request) process(broadcastMsgCh chan Message, incomingMsgCh <-chan wsM
switch tagmsg.What {
case what.Reload, what.Redirect, what.Order, what.Alert:
- rq.queue(wsMsg{
+ rq.queue(WsMsg{
Jid: 0,
Data: tagmsg.Data,
What: tagmsg.What,
@@ -571,7 +572,7 @@ func (rq *Request) process(broadcastMsgCh chan Message, incomingMsgCh <-chan wsM
for _, elem := range todo {
switch tagmsg.What {
case what.Delete:
- rq.queue(wsMsg{
+ rq.queue(WsMsg{
Jid: elem.Jid(),
What: what.Delete,
})
@@ -588,7 +589,7 @@ func (rq *Request) process(broadcastMsgCh chan Message, incomingMsgCh <-chan wsM
// an error to be sent out as an alert message.
// primary usecase is tests.
if err := rq.Jaws.Log(rq.callAllEventHandlers(elem.Jid(), tagmsg.What, tagmsg.Data)); err != nil {
- rq.queue(wsMsg{
+ rq.queue(WsMsg{
Data: tagmsg.Data,
Jid: elem.Jid(),
What: what.Alert,
@@ -597,7 +598,7 @@ func (rq *Request) process(broadcastMsgCh chan Message, incomingMsgCh <-chan wsM
case what.Update:
elem.JawsUpdate()
default:
- rq.queue(wsMsg{
+ rq.queue(WsMsg{
Data: tagmsg.Data,
Jid: elem.Jid(),
What: tagmsg.What,
@@ -618,7 +619,7 @@ func (rq *Request) handleRemove(data string) {
}
}
-func (rq *Request) queue(msg wsMsg) {
+func (rq *Request) queue(msg WsMsg) {
rq.muQueue.Lock()
rq.wsQueue = append(rq.wsQueue, msg)
rq.muQueue.Unlock()
@@ -650,7 +651,7 @@ func (rq *Request) callAllEventHandlers(id Jid, wht what.What, val string) (err
rq.mu.RUnlock()
for _, e := range elems {
- if err = callEventHandlers(e.Ui(), e, wht, val); err != ErrEventUnhandled {
+ if err = CallEventHandlers(e.Ui(), e, wht, val); err != ErrEventUnhandled {
return
}
}
@@ -669,7 +670,7 @@ func (rq *Request) queueEvent(eventCallCh chan eventFnCall, call eventFnCall) {
}
}
-func (rq *Request) getSendMsgs() (toSend []wsMsg) {
+func (rq *Request) getSendMsgs() (toSend []WsMsg) {
rq.mu.RLock()
defer rq.mu.RUnlock()
@@ -695,11 +696,11 @@ func (rq *Request) getSendMsgs() (toSend []wsMsg) {
rq.wsQueue = rq.wsQueue[:0]
}
- slices.SortStableFunc(toSend, func(a, b wsMsg) int { return int(a.Jid - b.Jid) })
+ slices.SortStableFunc(toSend, func(a, b WsMsg) int { return int(a.Jid - b.Jid) })
return
}
-func (rq *Request) sendQueue(outboundMsgCh chan<- wsMsg) {
+func (rq *Request) sendQueue(outboundMsgCh chan<- WsMsg) {
for _, msg := range rq.getSendMsgs() {
select {
case <-rq.Context().Done():
@@ -742,6 +743,15 @@ func (rq *Request) deleteElement(e *Element) {
rq.deleteElementLocked(e)
}
+// DeleteElement removes elem from the Request element registry.
+//
+// This is primarily intended for UI implementations that manage dynamic child
+// element sets and need to drop stale elements after issuing a corresponding
+// DOM remove operation.
+func (rq *Request) DeleteElement(elem *Element) {
+ rq.deleteElement(elem)
+}
+
func (rq *Request) makeUpdateList() (todo []*Element) {
rq.mu.Lock()
seen := map[*Element]struct{}{}
@@ -761,11 +771,11 @@ func (rq *Request) makeUpdateList() (todo []*Element) {
}
// eventCaller calls event functions
-func (rq *Request) eventCaller(eventCallCh <-chan eventFnCall, outboundMsgCh chan<- wsMsg, eventDoneCh chan<- struct{}) {
+func (rq *Request) eventCaller(eventCallCh <-chan eventFnCall, outboundMsgCh chan<- WsMsg, eventDoneCh chan<- struct{}) {
defer close(eventDoneCh)
for call := range eventCallCh {
if err := rq.callAllEventHandlers(call.jid, call.wht, call.data); err != nil {
- var m wsMsg
+ var m WsMsg
m.FillAlert(err)
select {
case outboundMsgCh <- m:
@@ -788,11 +798,6 @@ func (rq *Request) onConnect() (err error) {
return
}
-// Writer returns a RequestWriter with this Request and the given Writer.
-func (rq *Request) Writer(w io.Writer) RequestWriter {
- return RequestWriter{rq: rq, Writer: w}
-}
-
func (rq *Request) validateWebSocketOrigin(r *http.Request) (err error) {
err = ErrWebsocketOriginMissing
if origin := r.Header.Get("Origin"); origin != "" {
diff --git a/core/request_lifecycle_test.go b/core/request_lifecycle_test.go
new file mode 100644
index 00000000..5e1ab25d
--- /dev/null
+++ b/core/request_lifecycle_test.go
@@ -0,0 +1,176 @@
+package core
+
+import (
+ "context"
+ "errors"
+ "net/http/httptest"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/linkdata/jaws/what"
+)
+
+func TestCoverage_PendingSubscribeMaintenanceAndParse(t *testing.T) {
+ jw, err := New()
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer jw.Close()
+
+ hr := httptest.NewRequest("GET", "/", nil)
+ rq := jw.NewRequest(hr)
+ if got := jw.Pending(); got != 1 {
+ t.Fatalf("expected one pending request, got %d", got)
+ }
+ if claimed := jw.UseRequest(rq.JawsKey, hr); claimed != rq {
+ t.Fatal("expected request claim")
+ }
+ if got := jw.Pending(); got != 0 {
+ t.Fatalf("expected zero pending requests, got %d", got)
+ }
+
+ msgCh := jw.subscribe(rq, 1)
+ if msgCh == nil {
+ t.Fatal("expected non-nil subscription channel")
+ }
+ if sub := <-jw.subCh; sub.msgCh != msgCh {
+ t.Fatal("unexpected subscription")
+ }
+ jw.unsubscribe(msgCh)
+ if got := <-jw.unsubCh; got != msgCh {
+ t.Fatal("unexpected unsubscribe channel")
+ }
+
+ // Request timeout path.
+ rq.mu.Lock()
+ rq.lastWrite = time.Now().Add(-time.Hour)
+ rq.mu.Unlock()
+ jw.maintenance(time.Second)
+ if got := jw.RequestCount(); got != 0 {
+ t.Fatalf("expected request recycled, got %d", got)
+ }
+
+ // Dead session cleanup path.
+ sess := jw.newSession(nil, hr)
+ sess.mu.Lock()
+ sess.deadline = time.Now().Add(-time.Second)
+ sess.mu.Unlock()
+ jw.maintenance(time.Second)
+ if got := jw.SessionCount(); got != 0 {
+ t.Fatalf("expected dead session cleanup, got %d", got)
+ }
+
+ // done-channel branch in subscribe and unsubscribe.
+ jw.subCh <- subscription{} // fill channel so send case is not selectable
+ jw.unsubCh <- make(chan Message)
+ jw.Close()
+ if ch := jw.subscribe(nil, 1); ch != nil {
+ t.Fatalf("expected nil subscription after close, got %v", ch)
+ }
+ jw.unsubscribe(nil)
+}
+
+func TestCoverage_RequestMaintenanceClaimAndErrors(t *testing.T) {
+ jw, err := New()
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer jw.Close()
+
+ hr := httptest.NewRequest("GET", "/", nil)
+ rq := jw.NewRequest(hr)
+ if err := rq.claim(hr); err != nil {
+ t.Fatal(err)
+ }
+ if err := rq.claim(hr); !errors.Is(err, ErrRequestAlreadyClaimed) {
+ t.Fatalf("expected ErrRequestAlreadyClaimed, got %v", err)
+ }
+
+ hrA := httptest.NewRequest("GET", "/", nil)
+ hrA.RemoteAddr = "1.2.3.4:1234"
+ rqA := jw.NewRequest(hrA)
+ hrB := httptest.NewRequest("GET", "/", nil)
+ hrB.RemoteAddr = "2.2.2.2:4321"
+ if err := rqA.claim(hrB); err == nil {
+ t.Fatal("expected ip mismatch error")
+ }
+
+ now := time.Now()
+ rqM := jw.NewRequest(httptest.NewRequest("GET", "/", nil))
+ rqM.lastWrite = now.Add(-time.Hour)
+ if !rqM.maintenance(now, time.Second) {
+ t.Fatal("expected maintenance timeout")
+ }
+ rqR := jw.NewRequest(httptest.NewRequest("GET", "/", nil))
+ nowR := time.Now()
+ rqR.Rendering.Store(true)
+ if rqR.maintenance(nowR, time.Hour) {
+ t.Fatal("expected maintenance continue")
+ }
+ rqR.mu.RLock()
+ lastWrite := rqR.lastWrite
+ rqR.mu.RUnlock()
+ if lastWrite != nowR {
+ t.Fatalf("expected lastWrite updated to now, got %v want %v", lastWrite, nowR)
+ }
+ rqC := jw.NewRequest(httptest.NewRequest("GET", "/", nil))
+ rqC.cancel(errors.New("cancelled"))
+ if !rqC.maintenance(time.Now(), time.Hour) {
+ t.Fatal("expected maintenance cancellation")
+ }
+ rqOK := jw.NewRequest(httptest.NewRequest("GET", "/", nil))
+ rqOK.lastWrite = time.Now()
+ if rqOK.maintenance(time.Now(), time.Hour) {
+ t.Fatal("expected maintenance keepalive")
+ }
+
+ errNoWS := newErrNoWebSocketRequest(rqOK)
+ if !errors.Is(errNoWS, ErrNoWebSocketRequest) {
+ t.Fatalf("expected no-websocket error type, got %v", errNoWS)
+ }
+ if got := errNoWS.Error(); !strings.Contains(got, "no WebSocket request received from") {
+ t.Fatalf("unexpected error text %q", got)
+ }
+
+ maybePanic(nil)
+ defer func() {
+ if recover() == nil {
+ t.Fatal("expected panic from maybePanic")
+ }
+ }()
+ maybePanic(errors.New("boom"))
+}
+
+func TestCoverage_RequestProcessHTTPDoneAndBroadcastDone(t *testing.T) {
+ jw, err := New()
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer jw.Close()
+
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+ hr := httptest.NewRequest("GET", "/", nil).WithContext(ctx)
+ rq := jw.NewRequest(hr)
+ if err := rq.claim(hr); err != nil {
+ t.Fatal(err)
+ }
+ bcastCh := make(chan Message)
+ inCh := make(chan WsMsg)
+ outCh := make(chan WsMsg, 1)
+ done := make(chan struct{})
+ go func() {
+ rq.process(bcastCh, inCh, outCh)
+ close(done)
+ }()
+ cancel()
+ select {
+ case <-done:
+ case <-time.After(time.Second):
+ t.Fatal("timeout waiting for process exit on httpDone")
+ }
+
+ jw.Close()
+ jw.Broadcast(Message{What: what.Update})
+}
diff --git a/jaws/request_test.go b/core/request_test.go
similarity index 96%
rename from jaws/request_test.go
rename to core/request_test.go
index 3d1c9169..c293243b 100644
--- a/jaws/request_test.go
+++ b/core/request_test.go
@@ -1,4 +1,4 @@
-package jaws
+package core
import (
"bytes"
@@ -19,10 +19,10 @@ import (
const testTimeout = time.Second * 3
-func fillWsCh(ch chan wsMsg) {
+func fillWsCh(ch chan WsMsg) {
for {
select {
- case ch <- wsMsg{}:
+ case ch <- WsMsg{}:
default:
return
}
@@ -60,7 +60,7 @@ func TestRequest_HeadHTML(t *testing.T) {
func TestRequestWriter_TailHTML(t *testing.T) {
th := newTestHelper(t)
- nextJid = 0
+ NextJid = 0
jw, _ := New()
defer jw.Close()
rq := jw.NewRequest(nil)
@@ -107,7 +107,7 @@ func TestRequest_SendArrivesOk(t *testing.T) {
elem := rq.GetElementByJid(jid)
is.True(elem != nil)
if elem != nil {
- is.Equal(msg, wsMsg{Jid: elem.jid, Data: "bar", What: what.Inner})
+ is.Equal(msg, WsMsg{Jid: elem.jid, Data: "bar", What: what.Inner})
}
}
}
@@ -205,7 +205,7 @@ func TestRequest_Trigger(t *testing.T) {
case <-th.C:
th.Timeout()
case msg := <-rq.OutCh:
- th.Equal(msg.Format(), (&wsMsg{
+ th.Equal(msg.Format(), (&WsMsg{
Data: "danger\nomg",
Jid: jid.Jid(0),
What: what.Alert,
@@ -312,7 +312,7 @@ func TestRequest_EventFnQueueOverflowPanicsWithNoLogger(t *testing.T) {
return
case <-th.C:
th.Timeout()
- case rq.InCh <- wsMsg{Jid: jid, What: what.Input}:
+ case rq.InCh <- WsMsg{Jid: jid, What: what.Input}:
}
}
}
@@ -380,7 +380,7 @@ func TestRequest_IgnoresIncomingMsgsDuringShutdown(t *testing.T) {
rq.Jaws.Broadcast(Message{Dest: rq})
}
select {
- case rq.InCh <- wsMsg{}:
+ case rq.InCh <- WsMsg{}:
case <-rq.DoneCh:
th.Fatal()
case <-th.C:
@@ -474,7 +474,7 @@ func TestRequest_DeleteByTag(t *testing.T) {
th := newTestHelper(t)
tj := newTestJaws()
defer tj.Close()
- nextJid = 0
+ NextJid = 0
rq1 := tj.newRequest(nil)
ui1 := &testUi{}
e11 := rq1.NewElement(ui1)
@@ -604,8 +604,8 @@ func TestRequest_Dirty(t *testing.T) {
tss1 := &testUi{s: "foo1"}
tss2 := &testUi{s: "foo2"}
- rq.UI(NewUiText(tss1))
- rq.UI(NewUiText(tss2))
+ rq.UI(newTestTextInputWidget(tss1))
+ rq.UI(newTestTextInputWidget(tss2))
th.Equal(tss1.getCalled, int32(1))
th.Equal(tss2.getCalled, int32(1))
th.True(strings.Contains(string(rq.BodyString()), "foo1"))
@@ -625,7 +625,7 @@ func TestRequest_Dirty(t *testing.T) {
func TestRequest_UpdatePanicLogs(t *testing.T) {
th := newTestHelper(t)
- nextJid = 0
+ NextJid = 0
rq := newTestRequest(t)
defer rq.Close()
var log bytes.Buffer
@@ -649,17 +649,17 @@ func TestRequest_UpdatePanicLogs(t *testing.T) {
func TestRequest_IncomingRemove(t *testing.T) {
th := newTestHelper(t)
- nextJid = 0
+ NextJid = 0
rq := newTestRequest(t)
defer rq.Close()
tss := newTestSetter("")
- rq.UI(NewUiText(tss))
+ rq.UI(newTestTextInputWidget(tss))
select {
case <-th.C:
th.Timeout()
- case rq.InCh <- wsMsg{What: what.Remove, Data: "Jid.1"}:
+ case rq.InCh <- WsMsg{What: what.Remove, Data: "Jid.1"}:
}
elem := rq.GetElementByJid(1)
@@ -676,7 +676,7 @@ func TestRequest_IncomingRemove(t *testing.T) {
func TestRequest_IncomingClick(t *testing.T) {
th := newTestHelper(t)
- nextJid = 0
+ NextJid = 0
rq := newTestRequest(t)
defer rq.Close()
@@ -690,13 +690,13 @@ func TestRequest_IncomingClick(t *testing.T) {
testSetter: newTestSetter(""),
}
- rq.Div("1", tjc1)
- rq.Div("2", tjc2)
+ rq.UI(testDivWidget{inner: "1"}, tjc1)
+ rq.UI(testDivWidget{inner: "2"}, tjc2)
select {
case <-th.C:
th.Timeout()
- case rq.InCh <- wsMsg{What: what.Click, Data: "name\tJid.1\tJid.2"}:
+ case rq.InCh <- WsMsg{What: what.Click, Data: "name\tJid.1\tJid.2"}:
}
select {
diff --git a/jaws/rwlocker.go b/core/rwlocker.go
similarity index 93%
rename from jaws/rwlocker.go
rename to core/rwlocker.go
index be68809e..470a70ab 100644
--- a/jaws/rwlocker.go
+++ b/core/rwlocker.go
@@ -1,4 +1,4 @@
-package jaws
+package core
import "sync"
diff --git a/jaws/selecthandler.go b/core/selecthandler.go
similarity index 82%
rename from jaws/selecthandler.go
rename to core/selecthandler.go
index 6e72def7..174d84b0 100644
--- a/jaws/selecthandler.go
+++ b/core/selecthandler.go
@@ -1,4 +1,4 @@
-package jaws
+package core
type SelectHandler interface {
Container
diff --git a/core/serve_ws_test.go b/core/serve_ws_test.go
new file mode 100644
index 00000000..89657624
--- /dev/null
+++ b/core/serve_ws_test.go
@@ -0,0 +1,98 @@
+package core
+
+import (
+ "net/http/httptest"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/linkdata/jaws/jid"
+ "github.com/linkdata/jaws/what"
+)
+
+func TestCoverage_WsMsgAppendNegativeJidAndServeWithTimeoutBounds(t *testing.T) {
+ msg := WsMsg{Jid: jid.Jid(-1), What: what.Update, Data: "raw\tdata"}
+ if got := string(msg.Append(nil)); got != "Update\traw\tdata\n" {
+ t.Fatalf("unexpected ws append result %q", got)
+ }
+ msg = WsMsg{Jid: 1, What: what.Call, Data: `fn={"a":1}`}
+ if got := string(msg.Append(nil)); !strings.Contains(got, `fn={"a":1}`) || strings.Contains(got, `"fn={"`) {
+ t.Fatalf("unexpected ws append quoted call payload %q", got)
+ }
+
+ // Min interval clamp path.
+ jwMin, err := New()
+ if err != nil {
+ t.Fatal(err)
+ }
+ doneMin := make(chan struct{})
+ go func() {
+ jwMin.ServeWithTimeout(time.Nanosecond)
+ close(doneMin)
+ }()
+ jwMin.Close()
+ select {
+ case <-doneMin:
+ case <-time.After(time.Second):
+ t.Fatal("timeout waiting for ServeWithTimeout(min)")
+ }
+
+ // Max interval clamp path.
+ jwMax, err := New()
+ if err != nil {
+ t.Fatal(err)
+ }
+ doneMax := make(chan struct{})
+ go func() {
+ jwMax.ServeWithTimeout(10 * time.Second)
+ close(doneMax)
+ }()
+ jwMax.Close()
+ select {
+ case <-doneMax:
+ case <-time.After(time.Second):
+ t.Fatal("timeout waiting for ServeWithTimeout(max)")
+ }
+}
+
+func TestCoverage_ServeWithTimeoutFullSubscriberChannel(t *testing.T) {
+ jw, err := New()
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer jw.Close()
+ rq := jw.NewRequest(httptest.NewRequest("GET", "/", nil))
+ msgCh := make(chan Message) // unbuffered: always full when nobody receives
+ done := make(chan struct{})
+ go func() {
+ jw.ServeWithTimeout(50 * time.Millisecond)
+ close(done)
+ }()
+ jw.subCh <- subscription{msgCh: msgCh, rq: rq}
+ // Ensure ServeWithTimeout has consumed the subscription before broadcast.
+ for i := 0; i <= cap(jw.subCh); i++ {
+ jw.subCh <- subscription{}
+ }
+ jw.bcastCh <- Message{What: what.Alert, Data: "x"}
+
+ waitUntil := time.Now().Add(time.Second)
+ closed := false
+ for !closed && time.Now().Before(waitUntil) {
+ select {
+ case _, ok := <-msgCh:
+ closed = !ok
+ default:
+ time.Sleep(time.Millisecond)
+ }
+ }
+ if !closed {
+ t.Fatal("expected subscriber channel to be closed when full")
+ }
+
+ jw.Close()
+ select {
+ case <-done:
+ case <-time.After(time.Second):
+ t.Fatal("timeout waiting for ServeWithTimeout exit")
+ }
+}
diff --git a/jaws/servehttp.go b/core/servehttp.go
similarity index 98%
rename from jaws/servehttp.go
rename to core/servehttp.go
index ec221c78..12808eb7 100644
--- a/jaws/servehttp.go
+++ b/core/servehttp.go
@@ -1,4 +1,4 @@
-package jaws
+package core
import (
"net/http"
diff --git a/jaws/servehttp_test.go b/core/servehttp_test.go
similarity index 99%
rename from jaws/servehttp_test.go
rename to core/servehttp_test.go
index e7aac1b6..b589d3b2 100644
--- a/jaws/servehttp_test.go
+++ b/core/servehttp_test.go
@@ -1,4 +1,4 @@
-package jaws
+package core
import (
"compress/gzip"
diff --git a/jaws/session.go b/core/session.go
similarity index 99%
rename from jaws/session.go
rename to core/session.go
index 99d30ede..9e025563 100644
--- a/jaws/session.go
+++ b/core/session.go
@@ -1,4 +1,4 @@
-package jaws
+package core
import (
"net/http"
diff --git a/jaws/session_test.go b/core/session_test.go
similarity index 97%
rename from jaws/session_test.go
rename to core/session_test.go
index 18191029..24b42b6e 100644
--- a/jaws/session_test.go
+++ b/core/session_test.go
@@ -1,4 +1,4 @@
-package jaws
+package core
import (
"context"
@@ -282,7 +282,7 @@ func TestSession_Delete(t *testing.T) {
}
byebyeItem := &testUi{}
- RequestWriter{ts.rq, httptest.NewRecorder()}.Register(byebyeItem, func(e *Element, evt what.What, val string) error {
+ testRequestWriter{rq: ts.rq, Writer: httptest.NewRecorder()}.Register(byebyeItem, func(e *Element, evt what.What, val string) error {
sess2 := ts.jw.GetSession(e.Request.Initial())
if x := sess2; x != ts.sess {
t.Error(x)
@@ -319,7 +319,7 @@ func TestSession_Delete(t *testing.T) {
}
defer conn.Close(websocket.StatusNormalClosure, "")
- msg := wsMsg{Jid: jidForTag(ts.rq, byebyeItem), What: what.Input}
+ msg := WsMsg{Jid: jidForTag(ts.rq, byebyeItem), What: what.Input}
ctx, cancel := context.WithCancel(ts.ctx)
defer cancel()
diff --git a/jaws/sessioner.go b/core/sessioner.go
similarity index 96%
rename from jaws/sessioner.go
rename to core/sessioner.go
index 09b9d065..5eb4bf7c 100644
--- a/jaws/sessioner.go
+++ b/core/sessioner.go
@@ -1,4 +1,4 @@
-package jaws
+package core
import "net/http"
diff --git a/jaws/sessioner_test.go b/core/sessioner_test.go
similarity index 95%
rename from jaws/sessioner_test.go
rename to core/sessioner_test.go
index 67217181..bbf872c0 100644
--- a/jaws/sessioner_test.go
+++ b/core/sessioner_test.go
@@ -1,4 +1,4 @@
-package jaws
+package core
import (
"bytes"
@@ -7,7 +7,7 @@ import (
)
func TestJaws_Session(t *testing.T) {
- nextJid = 0
+ NextJid = 0
rq := newTestRequest(t)
defer rq.Close()
diff --git a/jaws/setter.go b/core/setter.go
similarity index 93%
rename from jaws/setter.go
rename to core/setter.go
index 947f3e75..04bfe665 100644
--- a/jaws/setter.go
+++ b/core/setter.go
@@ -1,4 +1,4 @@
-package jaws
+package core
import (
"fmt"
@@ -38,7 +38,7 @@ func (s setterStatic[T]) JawsGetTag(*Request) any {
return nil
}
-func makeSetter[T comparable](v any) Setter[T] {
+func MakeSetter[T comparable](v any) Setter[T] {
switch v := v.(type) {
case Setter[T]:
return v
diff --git a/jaws/setter_test.go b/core/setter_test.go
similarity index 87%
rename from jaws/setter_test.go
rename to core/setter_test.go
index 181a09a9..6002cbd2 100644
--- a/jaws/setter_test.go
+++ b/core/setter_test.go
@@ -1,4 +1,4 @@
-package jaws
+package core
import (
"testing"
@@ -14,7 +14,7 @@ func (testGetterString) JawsGet(*Element) string {
func Test_makeSetter(t *testing.T) {
tsg := testGetterString{}
- setter1 := makeSetter[string](tsg)
+ setter1 := MakeSetter[string](tsg)
if err := setter1.JawsSet(nil, "foo"); err != ErrValueNotSettable {
t.Error(err)
}
@@ -25,7 +25,7 @@ func Test_makeSetter(t *testing.T) {
t.Error(tag)
}
- setter2 := makeSetter[string]("quux")
+ setter2 := MakeSetter[string]("quux")
if err := setter2.JawsSet(nil, "foo"); err != ErrValueNotSettable {
t.Error(err)
}
@@ -43,6 +43,6 @@ func Test_makeSetter_panic(t *testing.T) {
t.Fail()
}
}()
- setter2 := makeSetter[string](123)
+ setter2 := MakeSetter[string](123)
t.Error(setter2)
}
diff --git a/jaws/setterfloat64.go b/core/setterfloat64.go
similarity index 97%
rename from jaws/setterfloat64.go
rename to core/setterfloat64.go
index 498e6b65..33330b96 100644
--- a/jaws/setterfloat64.go
+++ b/core/setterfloat64.go
@@ -1,4 +1,4 @@
-package jaws
+package core
import (
"fmt"
@@ -81,7 +81,7 @@ func makeSetterFloat64for[T numeric](s *Setter[float64], v any) bool {
return false
}
-func makeSetterFloat64(v any) (s Setter[float64]) {
+func MakeSetterFloat64(v any) (s Setter[float64]) {
switch v := v.(type) {
case Setter[float64]:
return v
diff --git a/jaws/setterfloat64_test.go b/core/setterfloat64_test.go
similarity index 91%
rename from jaws/setterfloat64_test.go
rename to core/setterfloat64_test.go
index eabf00f5..0d20c776 100644
--- a/jaws/setterfloat64_test.go
+++ b/core/setterfloat64_test.go
@@ -1,4 +1,4 @@
-package jaws
+package core
import (
"reflect"
@@ -53,7 +53,7 @@ func Test_makeSetterFloat64types(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- if gotS := makeSetterFloat64(tt.v); !reflect.DeepEqual(gotS, tt.wantS) {
+ if gotS := MakeSetterFloat64(tt.v); !reflect.DeepEqual(gotS, tt.wantS) {
t.Errorf("makeSetterFloat64() = %#v, want %#v", gotS, tt.wantS)
}
})
@@ -62,7 +62,7 @@ func Test_makeSetterFloat64types(t *testing.T) {
func Test_makeSetterFloat64_int(t *testing.T) {
tsint := newTestSetter(int(0))
- gotS := makeSetterFloat64(tsint)
+ gotS := MakeSetterFloat64(tsint)
err := gotS.JawsSet(nil, 1)
if err != nil {
t.Fatal(err)
@@ -78,7 +78,7 @@ func Test_makeSetterFloat64_int(t *testing.T) {
func Test_makeSetterFloat64ReadOnly_int(t *testing.T) {
tgint := testGetter[int]{1}
- gotS := makeSetterFloat64(tgint)
+ gotS := MakeSetterFloat64(tgint)
err := gotS.JawsSet(nil, 2)
if err == nil {
t.Fatal("expected error")
@@ -94,7 +94,7 @@ func Test_makeSetterFloat64ReadOnly_int(t *testing.T) {
func Test_makeSetterFloat64Static_int(t *testing.T) {
v := 1
- gotS := makeSetterFloat64(v)
+ gotS := MakeSetterFloat64(v)
err := gotS.JawsSet(nil, 2)
if err == nil {
t.Fatal("expected error")
@@ -115,6 +115,6 @@ func Test_makeSetterFloat64_panic(t *testing.T) {
}
}()
- _ = makeSetterFloat64("x")
+ _ = MakeSetterFloat64("x")
t.Fatal("expected panic")
}
diff --git a/jaws/setup.go b/core/setup.go
similarity index 99%
rename from jaws/setup.go
rename to core/setup.go
index 21ec4d0e..86ad7259 100644
--- a/jaws/setup.go
+++ b/core/setup.go
@@ -1,4 +1,4 @@
-package jaws
+package core
import (
"errors"
diff --git a/jaws/setup_test.go b/core/setup_test.go
similarity index 99%
rename from jaws/setup_test.go
rename to core/setup_test.go
index 844bb474..6bb7108c 100644
--- a/jaws/setup_test.go
+++ b/core/setup_test.go
@@ -1,4 +1,4 @@
-package jaws
+package core
import (
"fmt"
diff --git a/jaws/stringgetterfunc.go b/core/stringgetterfunc.go
similarity index 97%
rename from jaws/stringgetterfunc.go
rename to core/stringgetterfunc.go
index 86ef5039..11ef123d 100644
--- a/jaws/stringgetterfunc.go
+++ b/core/stringgetterfunc.go
@@ -1,4 +1,4 @@
-package jaws
+package core
type stringGetterFunc struct {
fn func(*Element) string
diff --git a/jaws/stringgetterfunc_test.go b/core/stringgetterfunc_test.go
similarity index 96%
rename from jaws/stringgetterfunc_test.go
rename to core/stringgetterfunc_test.go
index 3df2e039..56bad309 100644
--- a/jaws/stringgetterfunc_test.go
+++ b/core/stringgetterfunc_test.go
@@ -1,4 +1,4 @@
-package jaws
+package core
import (
"reflect"
diff --git a/jaws/subscription.go b/core/subscription.go
similarity index 83%
rename from jaws/subscription.go
rename to core/subscription.go
index 6cfcef67..6811305b 100644
--- a/jaws/subscription.go
+++ b/core/subscription.go
@@ -1,4 +1,4 @@
-package jaws
+package core
type subscription struct {
msgCh chan Message
diff --git a/jaws/tag.go b/core/tag.go
similarity index 99%
rename from jaws/tag.go
rename to core/tag.go
index bd946fbb..b9a91eb1 100644
--- a/jaws/tag.go
+++ b/core/tag.go
@@ -1,4 +1,4 @@
-package jaws
+package core
import (
"fmt"
diff --git a/jaws/tag_test.go b/core/tag_test.go
similarity index 99%
rename from jaws/tag_test.go
rename to core/tag_test.go
index 4ae40e15..192f765d 100644
--- a/jaws/tag_test.go
+++ b/core/tag_test.go
@@ -1,4 +1,4 @@
-package jaws
+package core
import (
"errors"
diff --git a/jaws/taggetter.go b/core/taggetter.go
similarity index 87%
rename from jaws/taggetter.go
rename to core/taggetter.go
index 68a75630..accc0b07 100644
--- a/jaws/taggetter.go
+++ b/core/taggetter.go
@@ -1,4 +1,4 @@
-package jaws
+package core
type TagGetter interface {
JawsGetTag(rq *Request) any // Note that the Request may be nil
diff --git a/jaws/templatelookuper.go b/core/templatelookuper.go
similarity index 92%
rename from jaws/templatelookuper.go
rename to core/templatelookuper.go
index fd4ae144..e48f2b54 100644
--- a/jaws/templatelookuper.go
+++ b/core/templatelookuper.go
@@ -1,4 +1,4 @@
-package jaws
+package core
import "html/template"
diff --git a/jaws/test.sh b/core/test.sh
similarity index 100%
rename from jaws/test.sh
rename to core/test.sh
diff --git a/jaws/testhelper_test.go b/core/testhelper_test.go
similarity index 99%
rename from jaws/testhelper_test.go
rename to core/testhelper_test.go
index 3b1746b0..21e695d3 100644
--- a/jaws/testhelper_test.go
+++ b/core/testhelper_test.go
@@ -1,4 +1,4 @@
-package jaws
+package core
import (
"bytes"
diff --git a/jaws/testjaws_test.go b/core/testjaws_test.go
similarity index 83%
rename from jaws/testjaws_test.go
rename to core/testjaws_test.go
index 46f4d634..e546d3ee 100644
--- a/jaws/testjaws_test.go
+++ b/core/testjaws_test.go
@@ -1,4 +1,4 @@
-package jaws
+package core
import (
"bytes"
@@ -20,12 +20,10 @@ func newTestJaws() (tj *testJaws) {
if err != nil {
panic(err)
}
- tj = &testJaws{
- Jaws: jw,
- }
+ tj = &testJaws{Jaws: jw}
tj.Jaws.Logger = slog.New(slog.NewTextHandler(&tj.log, nil))
tj.Jaws.MakeAuth = func(r *Request) Auth {
- return defaultAuth{}
+ return DefaultAuth{}
}
tj.testtmpl = template.Must(template.New("testtemplate").Parse(`{{with $.Dot}}{{.}}
{{end}}`))
tj.AddTemplateLookuper(tj.testtmpl)
@@ -39,11 +37,11 @@ func (tj *testJaws) newRequest(hr *http.Request) (tr *TestRequest) {
return NewTestRequest(tj.Jaws, hr)
}
-func newTestRequest(t *testing.T) (tr *TestRequest) {
+func newTestRequest(t *testing.T) (tr *testRequest) {
tj := newTestJaws()
if t != nil {
t.Helper()
t.Cleanup(tj.Close)
}
- return NewTestRequest(tj.Jaws, nil)
+ return newWrappedTestRequest(tj.Jaws, nil)
}
diff --git a/core/testrequest_test.go b/core/testrequest_test.go
new file mode 100644
index 00000000..6c5b30a3
--- /dev/null
+++ b/core/testrequest_test.go
@@ -0,0 +1,34 @@
+package core
+
+import "net/http"
+
+type testRequest struct {
+ *TestRequest
+ rw testRequestWriter
+}
+
+func newWrappedTestRequest(jw *Jaws, hr *http.Request) *testRequest {
+ tr := NewTestRequest(jw, hr)
+ if tr == nil {
+ return nil
+ }
+ return &testRequest{
+ TestRequest: tr,
+ rw: testRequestWriter{
+ rq: tr.Request,
+ Writer: tr.ResponseRecorder,
+ },
+ }
+}
+
+func (tr *testRequest) UI(ui UI, params ...any) error { return tr.rw.UI(ui, params...) }
+func (tr *testRequest) Initial() *http.Request { return tr.rw.Initial() }
+func (tr *testRequest) HeadHTML() error { return tr.rw.HeadHTML() }
+func (tr *testRequest) TailHTML() error { return tr.rw.TailHTML() }
+func (tr *testRequest) Session() *Session { return tr.rw.Session() }
+func (tr *testRequest) Get(key string) (val any) { return tr.rw.Get(key) }
+func (tr *testRequest) Set(key string, val any) { tr.rw.Set(key, val) }
+func (tr *testRequest) Register(u Updater, p ...any) Jid { return tr.rw.Register(u, p...) }
+func (tr *testRequest) Template(name string, dot any, params ...any) error {
+ return tr.rw.Template(name, dot, params...)
+}
diff --git a/core/testrequestwriter_test.go b/core/testrequestwriter_test.go
new file mode 100644
index 00000000..324c4ae4
--- /dev/null
+++ b/core/testrequestwriter_test.go
@@ -0,0 +1,202 @@
+package core
+
+import (
+ "bytes"
+ "fmt"
+ "html/template"
+ "io"
+ "net/http"
+ "strings"
+ "text/template/parse"
+
+ "github.com/linkdata/deadlock"
+ "github.com/linkdata/jaws/what"
+)
+
+type testRequestWriter struct {
+ rq *Request
+ io.Writer
+}
+
+type testRegisterUI struct{ Updater }
+
+func (testRegisterUI) JawsRender(*Element, io.Writer, []any) error { return nil }
+func (ui testRegisterUI) JawsUpdate(e *Element) { ui.Updater.JawsUpdate(e) }
+
+func (rw testRequestWriter) UI(ui UI, params ...any) error {
+ return rw.rq.NewElement(ui).JawsRender(rw, params)
+}
+
+func (rw testRequestWriter) Write(p []byte) (n int, err error) {
+ rw.rq.Rendering.Store(true)
+ return rw.Writer.Write(p)
+}
+
+func (rw testRequestWriter) Request() *Request {
+ return rw.rq
+}
+
+func (rw testRequestWriter) Initial() *http.Request {
+ return rw.rq.Initial()
+}
+
+func (rw testRequestWriter) HeadHTML() error {
+ return rw.rq.HeadHTML(rw)
+}
+
+func (rw testRequestWriter) TailHTML() error {
+ return rw.rq.TailHTML(rw)
+}
+
+func (rw testRequestWriter) Session() *Session {
+ return rw.rq.Session()
+}
+
+func (rw testRequestWriter) Get(key string) (val any) {
+ return rw.rq.Get(key)
+}
+
+func (rw testRequestWriter) Set(key string, val any) {
+ rw.rq.Set(key, val)
+}
+
+func (rw testRequestWriter) Register(updater Updater, params ...any) Jid {
+ elem := rw.rq.NewElement(testRegisterUI{Updater: updater})
+ elem.Tag(updater)
+ elem.ApplyParams(params)
+ updater.JawsUpdate(elem)
+ return elem.Jid()
+}
+
+func (rq *Request) Writer(w io.Writer) testRequestWriter {
+ return testRequestWriter{rq: rq, Writer: w}
+}
+
+type testHandler struct {
+ *Jaws
+ Template testTemplateUI
+}
+
+func (h testHandler) ServeHTTP(wr http.ResponseWriter, r *http.Request) {
+ _ = h.Log(h.NewRequest(r).NewElement(h.Template).JawsRender(wr, nil))
+}
+
+func (jw *Jaws) Handler(name string, dot any) http.Handler {
+ return testHandler{Jaws: jw, Template: testTemplateUI{Name: name, Dot: dot}}
+}
+
+type testWith struct {
+ *Element
+ testRequestWriter
+ Dot any
+ Attrs template.HTMLAttr
+ Auth Auth
+}
+
+type testTemplateUI struct {
+ Name string
+ Dot any
+}
+
+func (t testTemplateUI) String() string {
+ return fmt.Sprintf("{%q, %s}", t.Name, TagString(t.Dot))
+}
+
+func findJidOrJsOrHTMLNode(node parse.Node) (found bool) {
+ switch node := node.(type) {
+ case *parse.TextNode:
+ if node != nil {
+ found = found || bytes.Contains(node.Text, []byte("