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
File renamed without changes.
44 changes: 24 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ 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`.
For widget authoring guidance see `lib/ui/README.md`.

## Quick start

Expand All @@ -58,7 +58,8 @@ import (
"sync"

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

const indexhtml = `
Expand Down Expand Up @@ -86,7 +87,7 @@ func main() {
var mu sync.Mutex
var f float64

http.DefaultServeMux.Handle("GET /", ui.Handler(jw, "index", jaws.Bind(&mu, &f)))
http.DefaultServeMux.Handle("GET /", ui.Handler(jw, "index", bind.New(&mu, &f)))
slog.Error(http.ListenAndServe("localhost:8080", nil).Error())
}
```
Expand Down Expand Up @@ -180,7 +181,7 @@ loop (`Serve()` or `ServeWithTimeout()`):
`(*Jaws).SessionCount()`, `(*Jaws).Sessions()`, `(*Jaws).Log()`,
`(*Jaws).MustLog()`.
* Static/ping JaWS endpoints via `(*Jaws).ServeHTTP()`:
`/jaws/.ping`, `/jaws/.jaws.*.js`, `/jaws/.jaws.*.css`.
`/jaws/.ping`, `/jaws/.jaws.<hash>.js`, `/jaws/.jaws.<hash>.css`.

Broadcasting APIs are not safe before the processing loop starts. In particular,
`(*Jaws).Broadcast()` (and helpers that call it), `(*Session).Broadcast()`,
Expand All @@ -193,12 +194,15 @@ Use `(*Jaws).SecureHeadersMiddleware(next)` to wrap page handlers with a
security-header baseline and a `Content-Security-Policy` that matches the
resources currently configured for JaWS.

The baseline headers come from
[`github.com/linkdata/secureheaders`](https://github.com/linkdata/secureheaders).

The middleware snapshots `secureheaders.DefaultHeaders`, replaces
`Content-Security-Policy` with `jw.ContentSecurityPolicy()`, and does not trust
forwarded HTTPS headers.

```go
page := ui.Handler(jw, "index", jaws.Bind(&mu, &f))
page := ui.Handler(jw, "index", bind.New(&mu, &f))
http.DefaultServeMux.Handle("GET /", jw.SecureHeadersMiddleware(page))
```

Expand All @@ -209,13 +213,13 @@ 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\.[0-9a-z]+\.css`
* `/jaws/.jaws.<hash>.css`

Serves the built-in JaWS stylesheet.

The response should be cached indefinitely.

* `/jaws/\.jaws\.[0-9a-z]+\.js`
* `/jaws/.jaws.<hash>.js`

Serves the built-in JaWS client-side JavaScript.

Expand All @@ -224,7 +228,7 @@ of them.
* `/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
`assets.JawsKeyValue()` (`github.com/linkdata/jaws/lib/assets`) and then the matching JaWS Request retrieved
using the JaWS object's `UseRequest()` method.

If the Request is not found, return a **404 Not Found**, otherwise
Expand Down Expand Up @@ -273,27 +277,27 @@ router.GET("/jaws/*", func(c echo.Context) error {

### HTML rendering

HTML output elements (e.g. `jaws.RequestWriter.Div()`) require a `jaws.HTMLGetter` or something that can
be made into one using `jaws.MakeHTMLGetter()`.
HTML output elements (e.g. `ui.RequestWriter.Div()`) require a `bind.HTMLGetter` or something that can
be made into one using `bind.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.Formatter`: `Format("%v") string` that will be escaped using `html.EscapeString`.
* `bind.HTMLGetter`: `JawsGetHTML(*Element) template.HTML` to be used as-is.
* `bind.Getter[string]`: `JawsGet(*Element) string` that will be escaped using `html.EscapeString`.
* `bind.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`.

You can use `jaws.Bind().FormatHTML()`, `jaws.HTMLGetterFunc()` or `jaws.StringGetterFunc()` to build a custom renderer
You can use `bind.New(...).FormatHTML()`, `bind.HTMLGetterFunc()` or `bind.StringGetterFunc()` to build a custom renderer
for trivial rendering tasks, or define a custom type implementing `HTMLGetter`.

### Data binding

HTML input elements (e.g. `jaws.RequestWriter.Range()`) require bi-directional data flow between the server and the browser.
The first argument to these is usually a `Setter[T]` where `T` is one of `string`, `float64`, `bool` or `time.Time`. It can
also be a `Getter[T]`, in which case the HTML element should be made read-only.
HTML input elements (e.g. `ui.RequestWriter.Range()`) require bi-directional data flow between the server and the browser.
The first argument to these is usually a `bind.Setter[T]` where `T` is one of `string`, `float64`, `bool` or `time.Time`. It can
also be a `bind.Getter[T]`, in which case the HTML element should be made read-only.

Since all data access need to be protected with locks, you will usually use `jaws.Bind()` to create a `jaws.Binder[T]`
Since all data access need to be protected with locks, you will usually use `bind.New()` to create a `bind.Binder[T]`
that combines a (RW)Locker and a pointer to a value of type `T`. It also allows you to add chained setters,
getters and on-success handlers.

Expand Down Expand Up @@ -321,7 +325,7 @@ session from a new IP will fail.

No data is stored in the client browser except the randomly generated
session cookie. You can set the cookie name in `Jaws.CookieName`, the
default is `jaws`.
default is derived from the executable name and falls back to `jaws`.

### A note on the Context

Expand Down Expand Up @@ -363,7 +367,7 @@ We try to minimize dependencies outside of the standard library.

* Browse the [Go package documentation](https://pkg.go.dev/github.com/linkdata/jaws)
for an API-by-API overview.
* Inspect the [`example_test.go`](./example_test.go) file for executable
* Inspect the [`example_test.go`](./examples/example_test.go) file for executable
examples that can be run with `go test`.
* Explore the [demo application](https://github.com/linkdata/jawsdemo)
to see a more complete, heavily commented project structure.
82 changes: 82 additions & 0 deletions contracts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package jaws

import (
"html/template"
"io"

"github.com/linkdata/jaws/lib/what"
)

type Container interface {
// JawsContains must return a slice of hashable UI objects. The slice contents must not be modified after returning it.
JawsContains(e *Element) (contents []UI)
}

// InitHandler allows initializing UI getters and setters before their use.
//
// You can of course initialize them in the call from the template engine,
// but at that point you don't have access to the Element, Element.Context
// or Element.Session.
type InitHandler interface {
JawsInit(e *Element) (err error)
}

// Logger matches the log/slog.Logger interface.
type Logger interface {
Info(msg string, args ...any)
Warn(msg string, args ...any)
Error(msg string, args ...any)
}

type Renderer interface {
// JawsRender is called once per Element when rendering the initial webpage.
// Do not call this yourself unless it's from within another JawsRender implementation.
JawsRender(e *Element, w io.Writer, params []any) error
}

// TemplateLookuper resolves a name to a *template.Template.
type TemplateLookuper interface {
Lookup(name string) *template.Template
}

// UI defines the required methods on JaWS UI objects.
// In addition, all UI objects must be comparable so they can be used as map keys.
type UI interface {
Renderer
Updater
}

type Updater interface {
// JawsUpdate is called for an Element that has been marked dirty to update it's HTML.
// Do not call this yourself unless it's from within another JawsUpdate implementation.
JawsUpdate(e *Element)
}

type ClickHandler interface {
// JawsClick is called when an Element's HTML element or something within it
// is clicked in the browser.
//
// The name parameter is taken from the first 'name' HTML attribute or HTML
// 'button' textContent found when traversing the DOM. It may be empty.
JawsClick(e *Element, name string) (err error)
}

type clickHandlerWrapper struct{ ClickHandler }

func (chw clickHandlerWrapper) JawsEvent(*Element, what.What, string) error {
return ErrEventUnhandled
}

type Auth interface {
Data() map[string]any // returns authenticated user data, or nil
Email() string // returns authenticated user email, or an empty string
IsAdmin() bool // return true if admins are defined and current user is one, or if no admins are defined
}

type MakeAuthFn func(*Request) Auth

type DefaultAuth struct{}

func (DefaultAuth) Data() map[string]any { return nil }
func (DefaultAuth) Email() string { return "" }
func (DefaultAuth) IsAdmin() bool { return true }
22 changes: 18 additions & 4 deletions core/clickhandler_test.go → contracts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import (
"html/template"
"testing"

"github.com/linkdata/jaws/what"
"github.com/linkdata/jaws/lib/what"
"github.com/linkdata/jaws/lib/wire"
)

type testJawsClick struct {
Expand All @@ -13,7 +14,7 @@ type testJawsClick struct {
}

func (tjc *testJawsClick) JawsClick(e *Element, name string) (err error) {
if err = tjc.err; err == nil {
if err = tjc.Err(); err == nil {
tjc.clickCh <- name
}
return
Expand All @@ -38,7 +39,7 @@ func Test_clickHandlerWapper_JawsEvent(t *testing.T) {
t.Errorf("Request.UI(NewDiv()) = %q, want %q", got, want)
}

rq.InCh <- WsMsg{Data: "text", Jid: 1, What: what.Input}
rq.InCh <- wire.WsMsg{Data: "text", Jid: 1, What: what.Input}
select {
case <-th.C:
th.Timeout()
Expand All @@ -47,7 +48,7 @@ func Test_clickHandlerWapper_JawsEvent(t *testing.T) {
default:
}

rq.InCh <- WsMsg{Data: "adam", Jid: 1, What: what.Click}
rq.InCh <- wire.WsMsg{Data: "adam", Jid: 1, What: what.Click}
select {
case <-th.C:
th.Timeout()
Expand All @@ -57,3 +58,16 @@ func Test_clickHandlerWapper_JawsEvent(t *testing.T) {
}
}
}

func Test_defaultAuth(t *testing.T) {
a := DefaultAuth{}
if a.Data() != nil {
t.Fatal()
}
if a.Email() != "" {
t.Fatal()
}
if a.IsAdmin() != true {
t.Fatal()
}
}
15 changes: 0 additions & 15 deletions core/auth.go

This file was deleted.

18 changes: 0 additions & 18 deletions core/auth_test.go

This file was deleted.

18 changes: 0 additions & 18 deletions core/clickhandler.go

This file was deleted.

6 changes: 0 additions & 6 deletions core/container.go

This file was deleted.

24 changes: 0 additions & 24 deletions core/helpers_test.go

This file was deleted.

23 changes: 0 additions & 23 deletions core/htmlgetterfunc.go

This file was deleted.

Loading
Loading