Skip to content
Merged
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
47 changes: 32 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -53,8 +55,10 @@ import (
"html/template"
"log/slog"
"net/http"
"sync"

"github.com/linkdata/jaws"
"github.com/linkdata/jaws/ui"
)

const indexhtml = `
Expand Down Expand Up @@ -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())
}
```
Expand Down Expand Up @@ -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
Expand All @@ -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**.

Expand All @@ -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)
Expand All @@ -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()
Expand All @@ -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`.
Expand All @@ -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,
Expand All @@ -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
Expand Down
File renamed without changes.
10 changes: 5 additions & 5 deletions jaws/auth.go → core/auth.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package jaws
package core

type Auth interface {
Data() map[string]any // returns authenticated user data, or nil
Expand All @@ -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 }
4 changes: 2 additions & 2 deletions jaws/auth_test.go → core/auth_test.go
Original file line number Diff line number Diff line change
@@ -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()
}
Expand Down
2 changes: 1 addition & 1 deletion jaws/bind.go → core/bind.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package jaws
package core

import (
"sync"
Expand Down
2 changes: 1 addition & 1 deletion jaws/bind_test.go → core/bind_test.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package jaws
package core

import (
"io"
Expand Down
2 changes: 1 addition & 1 deletion jaws/binder.go → core/binder.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package jaws
package core

// BindSetHook is a function that replaces JawsSetLocked for a Binder.
//
Expand Down
2 changes: 1 addition & 1 deletion jaws/binding.go → core/binding.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package jaws
package core

import (
"fmt"
Expand Down
2 changes: 1 addition & 1 deletion jaws/bindinghook.go → core/bindinghook.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package jaws
package core

import (
"fmt"
Expand Down
81 changes: 81 additions & 0 deletions core/broadcast_test.go
Original file line number Diff line number Diff line change
@@ -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("<b>x</b>"))
if msg := nextBroadcast(t, jw); msg.What != what.Inner || msg.Data != "<b>x</b>" {
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", "<i>a</i>")
if msg := nextBroadcast(t, jw); msg.What != what.Insert || msg.Data != "0\n<i>a</i>" {
t.Fatalf("unexpected insert msg %#v", msg)
}
jw.Replace("t", "0", "<i>b</i>")
if msg := nextBroadcast(t, jw); msg.What != what.Replace || msg.Data != "0\n<i>b</i>" {
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", "<em>c</em>")
if msg := nextBroadcast(t, jw); msg.What != what.Append || msg.Data != "<em>c</em>" {
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)
}
}
2 changes: 1 addition & 1 deletion jaws/clickhandler.go → core/clickhandler.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package jaws
package core

import "github.com/linkdata/jaws/what"

Expand Down
13 changes: 7 additions & 6 deletions jaws/clickhandler_test.go → core/clickhandler_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package jaws
package core

import (
"html/template"
"testing"

"github.com/linkdata/jaws/what"
Expand All @@ -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()

Expand All @@ -32,12 +33,12 @@ func Test_clickHandlerWapper_JawsEvent(t *testing.T) {
}

want := `<div id="Jid.1">inner</div>`
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()
Expand All @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion jaws/container.go → core/container.go
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
4 changes: 4 additions & 0 deletions core/dateformat.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package core

// ISO8601 is the date format used by date input widgets (YYYY-MM-DD).
const ISO8601 = "2006-01-02"
4 changes: 2 additions & 2 deletions jaws/defaultcookiename.go → core/defaultcookiename.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package jaws
package core

import (
"os"
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package jaws
package core

import (
"path"
Expand Down
11 changes: 9 additions & 2 deletions jaws/element.go → core/element.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package jaws
package core

import (
"bytes"
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down
Loading