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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@

CODEJOB_PR=https://github.com/tinywasm/crudp/pull/8
20 changes: 10 additions & 10 deletions crudp.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,19 @@ package crudp

import (
"reflect"

"github.com/tinywasm/binary"
)

type actionHandler struct {
name string
index uint8
handler any
dataType reflect.Type
Create func(data ...any) any
Read func(data ...any) any
Update func(data ...any) any
Delete func(data ...any) any
ValidateData func(action byte, data ...any) error
Create func(payload any) (any, error)
Read func(id string) (any, error)
List func() (any, error)
Update func(payload any) (any, error)
Delete func(id string) error
ValidateData func(action byte, payload any) error
AllowedRoles func(action byte) []byte
}

Expand All @@ -38,11 +37,12 @@ type CrudP struct {
// noOpAccessCheck is a default no-op access validation
func noOpAccessCheck(actionHandler, byte, ...any) error { return nil }

// New creates a new CrudP instance with binary codec by default
// New creates a new CrudP instance. No codec is configured by default.
// Use SetCodecs() to provide serialization functions before execution.
func New() *CrudP {
cp := &CrudP{
encode: binary.Encode,
decode: binary.Decode,
encode: nil,
decode: nil,
log: func(...any) {}, // No-op logger by default
accessCheck: noOpAccessCheck, // No-op by default
}
Expand Down
15 changes: 9 additions & 6 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ flowchart TD
| Decision | Choice | Rationale |
|----------|--------|-----------|
| Serialization | `Encode`/`Decode` funcs | Support JSON (tinywasm/json) or Binary (tinywasm/binary) formats |
| Handler signature | `func(data ...any) any` | Simplifies API and reduces binary size |
| Handler signature | Explicit typed signatures | Improves safety and clarity |
| Packet structure | Internal to `crudp` | Unified protocol definition and execution |
| Batching | Delegated to `tinywasm/broker` | Keep CRUDP core simple and focused on execution |
| Message types | `0-4` (uint8) | Normal, Info, Error, Warning, Success from `tinywasm/fmt` |
Expand All @@ -66,14 +66,17 @@ crudp

```go
// CRUD interfaces - return any (result or error)
type Creator interface { Create(data ...any) any }
type Reader interface { Read(data ...any) any }
type Updater interface { Update(data ...any) any }
type Deleter interface { Delete(data ...any) any }
type Creator interface { Create(payload any) (any, error) }
type Reader interface {
Read(id string) (any, error)
List() (any, error)
}
type Updater interface { Update(payload any) (any, error) }
type Deleter interface { Delete(id string) error }

// Optional interfaces
type NamedHandler interface { HandlerName() string }
type Validator interface { Validate(action byte, data ...any) error }
type Validator interface { Validate(action byte, payload any) error }
```

## Implementation Status
Expand Down
2 changes: 1 addition & 1 deletion docs/PLAN.md → docs/CHECK_PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
> and `List()`. Add proper `error` return to all methods. The `db *orm.DB` is captured
> in the handler's constructor — never passed as a method parameter.
>
> **Status:** Pending execution
> **Status:** Executed

---

Expand Down
54 changes: 26 additions & 28 deletions docs/FILE_UPLOAD.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,36 +30,34 @@ type Handler struct{}

func (h *Handler) HandlerName() string { return "files" }

func (h *Handler) Create(data ...any) any {
for _, item := range data {
switch v := item.(type) {
case *http.Request:
// 1. Handle Multipart Upload (Server-side only logic)
file, header, err := v.FormFile("file")
if err != nil {
return err
}
defer file.Close()

// 2. Save file
path := "/uploads/" + header.Filename
dst, _ := os.Create(path)
io.Copy(dst, file)

// 3. Return the reference
return &FileReference{
ID: "unique-id",
Path: path,
Name: header.Filename,
}

case *FileReference:
// 4. Handle JSON Metadata (Batch or Client-side)
// Save metadata to database...
return v
func (h *Handler) Create(payload any) (any, error) {
switch v := payload.(type) {
case *http.Request:
// 1. Handle Multipart Upload (Server-side only logic)
file, header, err := v.FormFile("file")
if err != nil {
return nil, err
}
defer file.Close()

// 2. Save file
path := "/uploads/" + header.Filename
dst, _ := os.Create(path)
io.Copy(dst, file)

// 3. Return the reference
return &FileReference{
ID: "unique-id",
Path: path,
Name: header.Filename,
}, nil

case *FileReference:
// 4. Handle JSON Metadata (Batch or Client-side)
// Save metadata to database...
return v, nil
}
return nil
return nil, nil
}
```

Expand Down
44 changes: 40 additions & 4 deletions docs/HANDLER_REGISTER.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ For a complete step-by-step example, see the [Integration Guide](./INTEGRATION_G

Entities implement one or more of the CRUD interfaces defined in [`interfaces.go`](../interfaces.go):

- `Creator`: `Create(data ...any) any`
- `Reader`: `Read(data ...any) any`
- `Updater`: `Update(data ...any) any`
- `Deleter`: `Delete(data ...any) any`
- `Creator`: `Create(payload any) (any, error)`
- `Reader`: `Read(id string) (any, error)` and `List() (any, error)`
- `Updater`: `Update(payload any) (any, error)`
- `Deleter`: `Delete(id string) error`

**Key Points:**
- **Return types**: Returning an `error` allows CRUDP to automatically populate error messages in the response.
Expand All @@ -36,3 +36,39 @@ Use `RegisterHandlers` to register Entity instances. The order in the slice dete
```go
err := cp.RegisterHandlers(&User{}, &Product{})
```

## Handler Wrapper Pattern (Best Practice)

For handlers that need external dependencies (like a database connection) without using global state, use a wrapper struct that captures dependencies in its constructor. The entity model struct itself remains a pure data type.

```go
// The entity model struct (User) stays pure — no CRUDP methods on it.
// A separate handler wrapper captures db in its constructor.

type userCRUD struct{ db *orm.DB }

func (h *userCRUD) HandlerName() string { return "users" }
func (h *userCRUD) AllowedRoles(action byte) []byte { return []byte{'a'} }
func (h *userCRUD) ValidateData(action byte, _ any) error { return nil }

func (h *userCRUD) Create(payload any) (any, error) {
u := payload.(User)
return createUser(h.db, u.Email, u.Name, u.Phone)
}
func (h *userCRUD) Read(id string) (any, error) { return getUser(h.db, nil, id) }
func (h *userCRUD) List() (any, error) { return listUsers(h.db) }
func (h *userCRUD) Update(payload any) (any, error) {
u := payload.(User)
return u, updateUser(h.db, u.ID, u.Name, u.Phone)
}
func (h *userCRUD) Delete(id string) error { return deleteUser(h.db, id) }

// Registration in the consuming app:
// cp.RegisterHandlers(&userCRUD{db: db})
```

Key properties of this pattern:
- No global `store` — `db` is explicit in the constructor.
- Model struct (`User`) remains a pure data type — no behavior attached.
- Each entity gets a dedicated `*CRUD` type — follows SRP.
- Type assertion (`payload.(User)`) happens once inside the handler — all external code is clean.
15 changes: 7 additions & 8 deletions docs/HTTP_ROUTES_AND_MIDDLEWARE.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,17 +53,16 @@ http.ListenAndServe(":8080", handler)
Since handlers receive the `*http.Request`, you can handle any HTTP-specific logic (like file uploads or webhooks) directly inside your CRUD methods.

```go
func (h *UserHandler) Create(data ...any) any {
for _, item := range data {
if r, ok := item.(*http.Request); ok {
// Check headers, handle multipart, etc.
if r.Header.Get("X-Custom-Webhook") != "" {
return h.handleWebhook(r)
}
func (h *UserHandler) Create(payload any) (any, error) {
if r, ok := payload.(*http.Request); ok {
// Check headers, handle multipart, etc.
if r.Header.Get("X-Custom-Webhook") != "" {
return h.handleWebhook(r)
}
}

// Default JSON processing...
return nil
return nil, nil
}
```

Expand Down
11 changes: 5 additions & 6 deletions docs/INITIAL_VISION.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ Handlers return `any` which can be:
- An `error` (detected automatically by the server)

```go
func (h *Handler) Create(data ...any) any {
func (h *Handler) Create(payload any) (any, error) {
// Return result or error
}
```
Expand All @@ -43,12 +43,11 @@ Server-side handlers receive `*http.Request` in the `data` slice, enabling:
- Any HTTP-specific logic without custom routes

```go
func (h *Handler) Create(data ...any) any {
for _, item := range data {
if r, ok := item.(*http.Request); ok {
// Access headers, parse multipart, etc.
}
func (h *Handler) Create(payload any) (any, error) {
if r, ok := payload.(*http.Request); ok {
// Access headers, parse multipart, etc.
}
return nil, nil
}
```

Expand Down
8 changes: 4 additions & 4 deletions docs/INTEGRATION_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ type User struct {
func (u *User) HandlerName() string { return "users" }

// Mandatory: All CRUD entities must implement DataValidator
func (u *User) ValidateData(action byte, data ...any) error {
func (u *User) ValidateData(action byte, payload any) error {
return nil // Implement logic here
}

Expand Down Expand Up @@ -76,7 +76,7 @@ var users = []*User{
{ID: 2, Name: "Bob", Email: "bob@example.com"},
}

func (h *Handler) Create(data ...any) any {
func (h *Handler) Create(payload any) (any, error) {
for _, item := range data {
switch v := item.(type) {
case *context.Context:
Expand All @@ -91,7 +91,7 @@ func (h *Handler) Create(data ...any) any {
return nil
}

func (h *Handler) Read(data ...any) any {
func (h *Handler) Read(id string) (any, error) {
for _, item := range data {
if path, ok := item.(string); ok {
if path == "" { return users } // All users
Expand Down Expand Up @@ -119,7 +119,7 @@ import (
. "github.com/tinywasm/fmt"
)

func (h *Handler) Read(data ...any) any {
func (h *Handler) Read(id string) (any, error) {
for _, item := range data {
switch v := item.(type) {
case *User:
Expand Down
21 changes: 9 additions & 12 deletions docs/WEBHOOKS.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,24 +23,21 @@ type WebhookEvent struct {

func (w *WebhookEvent) HandlerName() string { return "webhooks" }

func (w *WebhookEvent) ValidateData(action byte, data ...any) error { return nil }
func (w *WebhookEvent) ValidateData(action byte, payload any) error { return nil }

// Access control (see [ACCESS_CONTROL.md](./ACCESS_CONTROL.md))
func (w *WebhookEvent) AllowedRoles(action byte) []byte { return []byte{'*'} } // Webhooks from any authenticated source

func (w *WebhookEvent) Create(data ...any) any {
var provider string
var r *http.Request

for _, item := range data {
switch v := item.(type) {
case string:
provider = v // e.g., "stripe" from /webhooks/stripe
case *http.Request:
r = v
}
func (w *WebhookEvent) Create(payload any) (any, error) {
// In this example we assume the payload is a struct holding the required context
// or we fetch the *http.Request directly if it's passed as the payload
r, ok := payload.(*http.Request)
if !ok {
return nil, errors.New("expected http.Request payload")
}

provider := r.URL.Path // Simplify getting provider from path or query params

// 1. Read raw body for signature verification
body, _ := io.ReadAll(r.Body)

Expand Down
12 changes: 6 additions & 6 deletions docs/img/badges.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
31 changes: 31 additions & 0 deletions docs_grep.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
docs/ACCESS_CONTROL.md:40:cp.SetUserRoles(func(data ...any) []byte {
docs/HANDLER_REGISTER.md:13:- `Creator`: `Create(data ...any) any`
docs/HANDLER_REGISTER.md:14:- `Reader`: `Read(data ...any) any`
docs/HANDLER_REGISTER.md:15:- `Updater`: `Update(data ...any) any`
docs/HANDLER_REGISTER.md:16:- `Deleter`: `Delete(data ...any) any`
docs/INTEGRATION_GUIDE.md:43:func (u *User) ValidateData(action byte, data ...any) error {
docs/INTEGRATION_GUIDE.md:79:func (h *Handler) Create(data ...any) any {
docs/INTEGRATION_GUIDE.md:94:func (h *Handler) Read(data ...any) any {
docs/INTEGRATION_GUIDE.md:122:func (h *Handler) Read(data ...any) any {
docs/INTEGRATION_GUIDE.md:197: cp.SetUserRoles(func(data ...any) []byte {
docs/INITIAL_VISION.md:33:func (h *Handler) Create(data ...any) any {
docs/INITIAL_VISION.md:46:func (h *Handler) Create(data ...any) any {
docs/WEBHOOKS.md:26:func (w *WebhookEvent) ValidateData(action byte, data ...any) error { return nil }
docs/WEBHOOKS.md:31:func (w *WebhookEvent) Create(data ...any) any {
docs/PLAN.md:4:> Remove the semantic ambiguity in `Read(data ...any) any` by splitting into `Read(id string)`
docs/PLAN.md:26:type Creator interface { Create(data ...any) any }
docs/PLAN.md:27:type Reader interface { Read(data ...any) any }
docs/PLAN.md:28:type Updater interface { Update(data ...any) any }
docs/PLAN.md:29:type Deleter interface { Delete(data ...any) any }
docs/PLAN.md:99:- `Read(id string) (any, error)` — explicit id, no more ambiguous `data ...any`
docs/PLAN.md:150:Update `CallHandler` to use the new typed methods. The incoming `data ...any` is now
docs/PLAN.md:155:func (cp *CrudP) CallHandler(handlerID uint8, action byte, data ...any) (any, error) {
docs/FILE_UPLOAD.md:33:func (h *Handler) Create(data ...any) any {
docs/HTTP_ROUTES_AND_MIDDLEWARE.md:20:Handlers receive the following injected values in the `data ...any` slice:
docs/HTTP_ROUTES_AND_MIDDLEWARE.md:56:func (h *UserHandler) Create(data ...any) any {
docs/ARCHITECTURE.md:49:| Handler signature | `func(data ...any) any` | Simplifies API and reduces binary size |
docs/ARCHITECTURE.md:69:type Creator interface { Create(data ...any) any }
docs/ARCHITECTURE.md:70:type Reader interface { Read(data ...any) any }
docs/ARCHITECTURE.md:71:type Updater interface { Update(data ...any) any }
docs/ARCHITECTURE.md:72:type Deleter interface { Delete(data ...any) any }
docs/ARCHITECTURE.md:76:type Validator interface { Validate(action byte, data ...any) error }
Loading