Update/Add comprehensive tinystruct patterns reference documentation (#1895)

* feat: update tinystruct-patterns skill with comprehensive expert knowledge

* Update skills/tinystruct-patterns/SKILL.md

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Update skills/tinystruct-patterns/SKILL.md

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Update skills/tinystruct-patterns/references/database.md

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* Update testing.md

* Update database.md

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
This commit is contained in:
James M. ZHOU
2026-05-15 09:18:19 +08:00
committed by GitHub
parent 7d15a2282b
commit d1710bd2e7
7 changed files with 384 additions and 132 deletions

View File

@@ -1,28 +1,38 @@
--- ---
name: tinystruct-patterns name: tinystruct-patterns
description: Use when developing application modules or microservices with the tinystruct Java framework. Covers routing, context management, JSON handling with Builder, and CLI/HTTP dual-mode patterns. description: Expert guidance for developing with the tinystruct Java framework. Use when working on the tinystruct codebase or any project built on tinystruct — including creating Application classes, @Action-mapped routes, unit tests, ActionRegistry, HTTP/CLI dual-mode handling, the built-in HTTP server, the event system, JSON with Builder/Builders, database persistence with AbstractData, POJO generation, Server-Sent Events (SSE), file uploads, and outbound HTTP networking.
origin: ECC origin: ECC
--- ---
# tinystruct Development Patterns # tinystruct Development Patterns
Architecture and implementation patterns for building modules with the **tinystruct** Java framework a lightweight system where CLI and HTTP are equal citizens. Architecture and implementation patterns for building modules with the **tinystruct** Java framework a lightweight, high-performance framework that treats CLI and HTTP as equal citizens, requiring no `main()` method and minimal configuration.
## When to Use ## Core Principle
**CLI and HTTP are equal citizens.** Every method annotated with `@Action` should ideally be runnable from both a terminal and a web browser without modification. This "dual-mode" capability is the core design philosophy of tinystruct.
## When to Activate
### When to Use
- Creating new `Application` modules by extending `AbstractApplication`. - Creating new `Application` modules by extending `AbstractApplication`.
- Defining routes and command-line actions using `@Action`. - Defining routes and command-line actions using `@Action`.
- Handling per-request state via `Context`. - Handling per-request state via `Context`.
- Performing JSON serialization using the native `Builder` component. - Performing JSON serialization using the native `Builder` and `Builders` components.
- Working with database persistence via `AbstractData` POJOs.
- Generating POJOs from database tables using the `generate` command.
- Implementing Server-Sent Events (SSE) for real-time push.
- Handling file uploads via multipart data.
- Making outbound HTTP requests with `URLRequest` and `HTTPHandler`.
- Configuring database connections or system settings in `application.properties`. - Configuring database connections or system settings in `application.properties`.
- Generating or re-generating the standard `bin/dispatcher` entry point via `ApplicationManager.init()`.
- Debugging routing conflicts (Actions) or CLI argument parsing. - Debugging routing conflicts (Actions) or CLI argument parsing.
## How It Works ## How It Works
The tinystruct framework treats any method annotated with `@Action` as a routable endpoint for both terminal and web environments. Applications are created by extending `AbstractApplication`, which provides core lifecycle hooks like `init()` and access to the request `Context`. The tinystruct framework treats any method annotated with `@Action` as a routable endpoint for both terminal and web environments. Applications are created by extending `AbstractApplication`, which provides core lifecycle hooks like `init()` and access to the request `Context`.
Routing is handled by the `ActionRegistry`, which automatically maps path segments to method arguments and injects dependencies. For data-only services, the native `Builder` component should be used for JSON serialization to maintain a zero-dependency footprint. The framework also includes a utility in `ApplicationManager` to bootstrap the project's execution environment by generating the `bin/dispatcher` script. Routing is handled by the `ActionRegistry`, which automatically maps path segments to method arguments and injects dependencies. For data-only services, the native `Builder` and `Builders` components should be used for JSON serialization to maintain a zero-dependency footprint. The database layer uses `AbstractData` POJOs paired with XML mapping files for CRUD operations without external ORM libraries.
## Examples ## Examples
@@ -40,38 +50,77 @@ public class MyService extends AbstractApplication {
public String greet() { public String greet() {
return "Hello from tinystruct!"; return "Hello from tinystruct!";
} }
}
```
### Parameterized Routing (getUser) // Path parameter: GET /?q=greet/James OR bin/dispatcher greet/James
```java @Action("greet")
// Handles /api/user/123 (Web) or "bin/dispatcher api/user/123" (CLI) public String greet(String name) {
@Action("api/user/(\\d+)") return "Hello, " + name + "!";
public String getUser(int userId) { }
return "User ID: " + userId;
} }
``` ```
### HTTP Mode Disambiguation (login) ### HTTP Mode Disambiguation (login)
```java ```java
@Action(value = "login", mode = Mode.HTTP_POST) @Action(value = "login", mode = Mode.HTTP_POST)
public boolean doLogin() { public String doLogin(Request<?, ?> request) throws ApplicationException {
// Process login logic request.getSession().setAttribute("userId", "42");
return true; return "Logged in";
} }
``` ```
### Native JSON Data Handling (getData) ### Native JSON Data Handling (Builder + Builders)
```java ```java
import org.tinystruct.data.component.Builder;
import org.tinystruct.data.component.Builders;
@Action("api/data") @Action("api/data")
public Builder getData() throws ApplicationException { public String getData() throws ApplicationException {
Builder builder = new Builder(); Builders dataList = new Builders();
builder.put("status", "success"); Builder item = new Builder();
Builder nested = new Builder(); item.put("id", 1);
nested.put("id", 1); item.put("name", "James");
nested.put("name", "James"); dataList.add(item);
builder.put("data", nested);
return builder; Builder response = new Builder();
response.put("status", "success");
response.put("data", dataList);
return response.toString(); // {"status":"success","data":[{"id":1,"name":"James"}]}
}
```
### SSE (Server-Sent Events)
```java
import org.tinystruct.http.SSEPushManager;
@Action("sse/connect")
public String connect() {
return "{\"type\":\"connect\",\"message\":\"Connected to SSE\"}";
}
// Push to a specific client
String sessionId = getContext().getId();
Builder msg = new Builder();
msg.put("text", "Hello, user!");
SSEPushManager.getInstance().push(sessionId, msg);
// Broadcast to all
// Broadcast to all
SSEPushManager.getInstance().broadcast(msg);
```
### File Upload
```java
import org.tinystruct.data.FileEntity;
@Action(value = "upload", mode = Mode.HTTP_POST)
public String upload(Request<?, ?> request) throws ApplicationException {
List<FileEntity> files = request.getAttachments();
if (files != null) {
for (FileEntity file : files) {
System.out.println("Uploaded: " + file.getFilename());
}
}
return "Upload OK";
} }
``` ```
@@ -83,35 +132,48 @@ Settings are managed in `src/main/resources/application.properties`.
# Database # Database
driver=org.h2.Driver driver=org.h2.Driver
database.url=jdbc:h2:~/mydb database.url=jdbc:h2:~/mydb
database.user=sa
database.password=
# App specific # Server
my.service.endpoint=https://api.example.com default.home.page=hello
server.port=8080
# Locale
default.language=en_US
# Session (Redis for clustered environments)
# default.session.repository=org.tinystruct.http.RedisSessionRepository
# redis.host=127.0.0.1
# redis.port=6379
``` ```
## Testing Patterns Access config values in your application:
Use JUnit 5 to test actions by verifying they are registered in the `ActionRegistry`.
```java ```java
@Test String port = this.getConfiguration("server.port");
void testActionRegistration() {
Application app = new MyService();
app.init();
ActionRegistry registry = ActionRegistry.getInstance();
assertNotNull(registry.get("greet"));
}
``` ```
## Red Flags & Anti-patterns ## Red Flags & Anti-patterns
| Symptom | Correct Pattern | | Symptom | Correct Pattern |
|---|---| |---|---|
| Importing `com.google.gson` or `com.fasterxml.jackson` | Use `org.tinystruct.data.component.Builder`. | | Importing `com.google.gson` or `com.fasterxml.jackson` | Use `org.tinystruct.data.component.Builder` / `Builders`. |
| `FileNotFoundException` for `.view` files | Call `setTemplateRequired(false)` in `init()` for API-only apps. | | Using `List<Builder>` for JSON arrays | Use `Builders` to avoid generic type erasure issues. |
| `ApplicationRuntimeException: template not found` | Call `setTemplateRequired(false)` in `init()` for API-only apps. |
| Annotating `private` methods with `@Action` | Actions must be `public` to be registered by the framework. | | Annotating `private` methods with `@Action` | Actions must be `public` to be registered by the framework. |
| Hardcoding `main(String[] args)` in apps | Use `bin/dispatcher` as the entry point for all modules. | | Hardcoding `main(String[] args)` in apps | Use `bin/dispatcher` as the entry point for all modules. |
| Manual `ActionRegistry` registration | Prefer the `@Action` annotation for automatic discovery. | | Manual `ActionRegistry` registration | Prefer the `@Action` annotation for automatic discovery. |
| Action not found at runtime | Ensure class is imported via `--import` or listed in `application.properties`. |
| CLI arg not visible | Pass with `--key value`; access via `getContext().getAttribute("--key")`. |
| Two methods same path, wrong one fires | Set explicit `mode` (e.g., `HTTP_GET` vs `HTTP_POST`) to disambiguate. |
## Best Practices
1. **Granular Applications**: Break logic into smaller, focused applications rather than one monolithic class.
2. **Setup in `init()`**: Leverage `init()` for setup (config, DB) rather than the constructor. Do NOT call `setAction()` — use `@Action` annotation.
3. **Mode Awareness**: Use the `Mode` parameter in `@Action` to restrict sensitive operations to `CLI` only or specific HTTP methods.
4. **Context over Params**: For optional CLI flags, use `getContext().getAttribute("--flag")` rather than adding parameters to the method signature.
5. **Asynchronous Events**: For heavy tasks triggered by events, use `CompletableFuture.runAsync()` inside the event handler.
## Technical Reference ## Technical Reference
@@ -119,13 +181,23 @@ Detailed guides are available in the `references/` directory:
- [Architecture & Config](references/architecture.md) — Abstractions, Package Map, Properties - [Architecture & Config](references/architecture.md) — Abstractions, Package Map, Properties
- [Routing & @Action](references/routing.md) — Annotation details, Modes, Parameters - [Routing & @Action](references/routing.md) — Annotation details, Modes, Parameters
- [Data Handling](references/data-handling.md) — Using the native `Builder` for JSON - [Data Handling](references/data-handling.md) — Builder, Builders, JSON serialization & parsing
- [System & Usage](references/system-usage.md) — Context, Sessions, Events, CLI usage - [Database Persistence](references/database.md) — AbstractData POJOs, CRUD, mapping XML, POJO generation
- [Testing Patterns](references/testing.md) — JUnit 5 integration and ActionRegistry testing - [System & Usage](references/system-usage.md) — Context, Sessions, SSE, File Uploads, Events, Networking
- [Testing Patterns](references/testing.md) — JUnit 5 unit and HTTP integration testing
## Reference Source Files (Internal) ## Reference Source Files (Internal)
- `src/main/java/org/tinystruct/AbstractApplication.java` — Core base class - `src/main/java/org/tinystruct/AbstractApplication.java` — Core base class with lifecycle hooks
- `src/main/java/org/tinystruct/system/annotation/Action.java` — Annotation & Modes - `src/main/java/org/tinystruct/system/annotation/Action.java` — Annotation & Modes
- `src/main/java/org/tinystruct/application/ActionRegistry.java` — Routing Engine - `src/main/java/org/tinystruct/application/ActionRegistry.java` — Routing Engine
- `src/main/java/org/tinystruct/data/component/Builder.java` — JSON/Data Serializer - `src/main/java/org/tinystruct/data/component/Builder.java` — JSON object serializer
- `src/main/java/org/tinystruct/data/component/Builders.java` — JSON array serializer
- `src/main/java/org/tinystruct/data/component/AbstractData.java` — Base POJO class with CRUD
- `src/main/java/org/tinystruct/data/Mapping.java` — Mapping XML parser
- `src/main/java/org/tinystruct/data/tools/MySQLGenerator.java` — POJO generator reference
- `src/main/java/org/tinystruct/data/component/FieldType.java` — SQL-to-Java type mappings
- `src/main/java/org/tinystruct/data/component/Condition.java` — Fluent SQL query builder
- `src/main/java/org/tinystruct/http/SSEPushManager.java` — SSE connection management
- `src/test/java/org/tinystruct/application/ActionRegistryTest.java` — Registry test examples
- `src/test/java/org/tinystruct/system/HttpServerHttpModeTest.java` — HTTP integration test patterns

View File

@@ -2,7 +2,7 @@
## When to Use ## When to Use
Choose **tinystruct** when you need a lightweight, high-performance Java framework that treats CLI and HTTP as equal citizens. It is ideal for building microservices, command-line utilities, and data-driven applications where a small footprint and zero-dependency JSON handling are required. Use it when you want to write logic once and expose it via both a terminal and a web server without modification. Choose **tinystruct** when you need a lightweight, high-performance Java framework that treats CLI and HTTP as equal citizens. Ideal for microservices, CLI utilities, and data-driven applications with a small footprint and zero-dependency JSON handling.
## How It Works ## How It Works
@@ -20,7 +20,7 @@ The framework operates on a singleton `ActionRegistry` that maps URL patterns (o
| `Action` | Wraps a `MethodHandle` + regex pattern + priority + `Mode` for dispatch. | | `Action` | Wraps a `MethodHandle` + regex pattern + priority + `Mode` for dispatch. |
| `Context` | Per-request state store. Access via `getContext()`. Holds CLI args and HTTP request/response. | | `Context` | Per-request state store. Access via `getContext()`. Holds CLI args and HTTP request/response. |
| `Dispatcher` | CLI entry point (`bin/dispatcher`). Reads `--import` to load applications. | | `Dispatcher` | CLI entry point (`bin/dispatcher`). Reads `--import` to load applications. |
| `HttpServer` | Built-in Netty-based HTTP server. Start with `bin/dispatcher start --import org.tinystruct.system.HttpServer`. | | `HttpServer` | Built-in HTTP server. Start with `bin/dispatcher start --import org.tinystruct.system.HttpServer`. |
### Package Map ### Package Map
@@ -40,13 +40,23 @@ org.tinystruct/
│ ├── HttpServer.java ← built-in HTTP server │ ├── HttpServer.java ← built-in HTTP server
│ ├── EventDispatcher.java ← event bus │ ├── EventDispatcher.java ← event bus
│ └── Settings.java ← reads application.properties │ └── Settings.java ← reads application.properties
├── data/component/Builder.java ← JSON serialization (use instead of Gson/Jackson) ├── data/
└── http/ ← Request, Response, Constants │ ├── component/Builder.java ← JSON object (use instead of Gson/Jackson)
│ ├── component/Builders.java ← JSON array
│ ├── component/AbstractData.java ← base POJO for DB persistence
│ ├── component/Condition.java ← fluent SQL query builder
│ ├── component/FieldType.java ← SQL-to-Java type mappings
│ ├── Mapping.java ← reads .map.xml metadata
│ ├── DatabaseOperator.java ← low-level JDBC wrapper
│ └── FileEntity.java ← file upload representation
├── http/ ← Request, Response, Constants
│ └── SSEPushManager.java ← Server-Sent Events management
└── net/ ← URLRequest, HTTPHandler (outbound HTTP)
``` ```
### Template Behavior and Dispatch Flow ### Template Behavior and Dispatch Flow
By default, the framework assumes a view template is required. If `templateRequired` is `true`, `toString()` looks for a `.view` file in `src/main/resources/themes/<ClassName>.view`. Use `getContext()` to manage state and `setVariable("name", value)` to pass data to templates, which use `[%name%]` for interpolation. By default, the framework assumes a view template is required. If `templateRequired` is `true`, `toString()` looks for a `.view` file in `src/main/resources/themes/<ClassName>.view`. Use `setVariable("name", value)` to pass data to templates, which use `{%name%}` for interpolation.
## Examples ## Examples
@@ -55,6 +65,7 @@ By default, the framework assumes a view template is required. If `templateRequi
@Override @Override
public void init() { public void init() {
this.setTemplateRequired(false); // Skip .view template lookup for data-only apps this.setTemplateRequired(false); // Skip .view template lookup for data-only apps
// Do NOT call setAction() here — use @Action annotation instead
} }
``` ```
@@ -68,6 +79,8 @@ public String hello() {
**Execution via Dispatcher:** **Execution via Dispatcher:**
```bash ```bash
bin/dispatcher hello bin/dispatcher hello
bin/dispatcher greet/James
bin/dispatcher echo --words "Hello" --import com.example.HelloApp
``` ```
### Configuration Access ### Configuration Access

View File

@@ -2,34 +2,59 @@
## When to Use ## When to Use
Prefer `org.tinystruct.data.component.Builder` in scenarios where you need a lightweight, high-performance JSON solution with **zero external dependencies**. It is specifically designed to keep your tinystruct applications lean and fast, making it the ideal choice for microservices and CLI tools where including heavy libraries like Jackson or Gson would be overkill. Prefer `org.tinystruct.data.component.Builder` and `Builders` for lightweight, zero-dependency JSON. Use `Builder` for JSON objects (`{}`), `Builders` for JSON arrays (`[]`). **Always use `Builders` instead of `List<Builder>`** to avoid generic type erasure issues.
## How It Works ## How It Works
The `Builder` class provides a simple key-value interface for both creating and reading JSON structures. It integrates directly with `AbstractApplication` result handling; when an action method returns a `Builder` object, the framework automatically serializes it to the response stream. This prevents the need for manual string conversion and ensures consistent data formatting across your application modules. `Builder` provides a key-value interface for creating and reading JSON objects. `Builders` provides an indexed list for JSON arrays. Both integrate directly with `AbstractApplication` result handling.
### Why Builder/Builders?
- **Zero External Dependencies** — lean and fast
- **Native Integration** — works with framework result handling
- **Type Safety** — `Builders` serializes properly to `[]`; `List<Builder>` can cause casting issues
## Examples ## Examples
### Serialization ### Serialize a Single Object
```java ```java
import org.tinystruct.data.component.Builder; import org.tinystruct.data.component.Builder;
// Create and populate
Builder response = new Builder(); Builder response = new Builder();
response.put("status", "success"); response.put("status", "success");
response.put("count", 42); response.put("count", 42);
response.put("data", someList); return response.toString(); // {"status":"success","count":42}
return response; // {"status":"success","count":42,...}
``` ```
### Parsing ### Serialize a List using Builders
```java ```java
import org.tinystruct.data.component.Builder; import org.tinystruct.data.component.Builder;
import org.tinystruct.data.component.Builders;
// Parse a JSON string Builders dataList = new Builders();
for (MyModel item : myCollection) {
Builder b = new Builder();
b.put("id", item.getId());
b.put("name", item.getName());
dataList.add(b);
}
Builder response = new Builder();
response.put("data", dataList);
return response.toString(); // {"data":[{"id":1,"name":"X"}]}
```
### Parse a JSON Object
```java
Builder parsed = new Builder(); Builder parsed = new Builder();
parsed.parse(jsonString); parsed.parse(jsonString);
String status = parsed.get("status").toString(); String status = parsed.get("status").toString();
``` ```
### Parse a JSON Array
```java
Builders parsedArray = new Builders();
parsedArray.parse(jsonArrayString);
for (int i = 0; i < parsedArray.size(); i++) {
Builder item = parsedArray.get(i);
System.out.println(item.get("name"));
}
```

View File

@@ -0,0 +1,99 @@
# tinystruct Database Persistence
## When to Use
Use the built-in ORM-like data layer for database operations. It provides a lightweight alternative to JPA/Hibernate using POJOs extending `AbstractData` and XML mapping files.
## How It Works
### Architecture
Each table is represented by:
1. **Java POJO**: Extends `AbstractData`, provides getters/setters and `setData(Row)`.
2. **Mapping XML**: `ClassName.map.xml` in resources, binding Java fields to DB columns.
#### Key Base Class: `AbstractData`
Provides CRUD methods:
- `append()` / `appendAndGetId()`
- `update()`
- `delete()`
- `findAll()` / `findOneById()` / `findOneByKey(key, value)`
- `findWith(where, params)`
- `find(SQL, params)`
### POJO Generation (CLI)
Introspect a live database table to produce the POJO and mapping file.
#### Configuration
`application.properties`:
```properties
driver=com.mysql.cj.jdbc.Driver
database.url=jdbc:mysql://localhost:3306/mydb
database.user=root
database.password=secret
```
#### Command
```bash
# Interactive mode
bin/dispatcher generate
# Specify table
bin/dispatcher generate --tables users
```
## Examples
### CRUD Operations
```java
// CREATE
User user = new User();
user.setUsername("james");
user.append();
// READ
User user = new User();
user.setId(42);
user.findOneById();
// UPDATE
user.setEmail("new@example.com");
user.update();
// DELETE
user.delete();
```
### Querying with Conditions
```java
User user = new User();
Table results = user.findWith("username LIKE ?", new Object[]{"%jam%"});
// Fluent Condition Builder
Condition condition = new Condition();
condition.setRequestFields("id,username");
Table filtered = user.find(
condition.select("`users`").and("email LIKE ?").orderBy("id DESC"),
new Object[]{"%@example.com"}
);
```
### Mapping XML Structure
`User.map.xml`:
```xml
<mapping>
<class name="User" table="users">
<id name="Id" column="id" increment="true" generate="false" length="11" type="int"/>
<property name="username" column="username" length="50" type="varchar"/>
<property name="email" column="email" length="100" type="varchar"/>
</class>
</mapping>
```
## Important Rules
1. **File Placement**: The mapping XML **must** mirror the POJO's package path under `src/main/resources/`.
2. **Naming**: Table names are singularized for class names (`users``User`). Underscored columns become camelCase fields (`created_at``createdAt`).
3. **Setters**: Use `setFieldAsXxx` methods (e.g., `setFieldAsString`) in setters to sync state with the internal field map.
4. **Id Field**: The primary key field in Java is always named `Id` (inherited from `AbstractData`).

View File

@@ -2,13 +2,17 @@
## When to Use ## When to Use
Use the `@Action` annotation in your applications to define routes for both CLI commands and HTTP endpoints. It is appropriate whenever you need to map logic to a specific path, handle parameterized requests (e.g., retrieving a resource by ID), or restrict execution to specific HTTP methods (GET, POST, etc.) while maintaining a consistent command structure across environments. Use the `@Action` annotation in your applications to define routes for both CLI commands and HTTP endpoints. It is appropriate whenever you need to map logic to a specific path, handle parameterized requests, or restrict execution to specific HTTP methods while maintaining a consistent command structure across environments.
## How It Works ## How It Works
The `ActionRegistry` parses `@Action` annotations to build a routing table. For parameterized methods, the framework automatically maps Java parameter types (int, String, etc.) to corresponding regex segments to generate an internal matching pattern. For instance, `getUser(int id)` generates a regex targeting digits, while `search(String query)` targets generic path segments. The `ActionRegistry` parses `@Action` annotations to build a routing table. For parameterized methods, the framework automatically maps Java parameter types to corresponding regex segments.
When a request is dispatched, the `ActionRegistry` automatically injects dependencies like `Request` and `Response` into the action method if they are specified as parameters, drawing them directly from the current request's `Context`. Execution is further filtered by the `Mode` value, allowing a single path to invoke different logic depending on whether the trigger was a terminal command or a specific type of HTTP request. ### Regex Generation Rules
- `getUser(int id)` → pattern: `^/?user/(-?\d+)$`
- `search(String query)` → pattern: `^/?search/([^/]+)$`
Supported parameter types: `String`, `int/Integer`, `long/Long`, `float/Float`, `double/Double`, `boolean/Boolean`, `char/Character`, `short/Short`, `byte/Byte`, `Date` (parsed as `yyyy-MM-dd HH:mm:ss`).
### Mode Values ### Mode Values
@@ -22,6 +26,8 @@ When a request is dispatched, the `ActionRegistry` automatically injects depende
| `HTTP_DELETE` | HTTP DELETE only | | `HTTP_DELETE` | HTTP DELETE only |
| `HTTP_PATCH` | HTTP PATCH only | | `HTTP_PATCH` | HTTP PATCH only |
> **Note:** You can map HTTP method names to `Mode` using `Action.Mode.fromName(String methodName)`. Unknown or null values return `Mode.DEFAULT`.
## Examples ## Examples
### Basic Action Declaration ### Basic Action Declaration
@@ -29,29 +35,30 @@ When a request is dispatched, the `ActionRegistry` automatically injects depende
@Action( @Action(
value = "path/subpath", // required: URI segment or CLI command value = "path/subpath", // required: URI segment or CLI command
description = "What it does", // shown in --help output description = "What it does", // shown in --help output
mode = Mode.HTTP_POST, // default: Mode.DEFAULT (both CLI + HTTP) mode = Mode.DEFAULT, // default: Mode.DEFAULT
options = {}, // CLI option flags example = "bin/dispatcher path/subpath/42"
example = "curl -X POST http://localhost:8080/path/subpath/42"
) )
public String myAction(int id) { ... } public String myAction(int id) { ... }
``` ```
### Parameterized Paths (Regex Generation) ### Parameterized Paths
```java ```java
@Action("user/{id}") @Action("user/{id}")
public String getUser(int id) { ... } public String getUser(int id) { ... }
// → pattern: ^/?user/(-?\d+)$ // → CLI: bin/dispatcher user/42
// → HTTP: /?q=user/42
@Action("search")
public String search(String query) { ... }
// → pattern: ^/?search/([^/]+)$
``` ```
### Request and Response Injection ### Dependency Injection
`ActionRegistry` automatically injects `Request` and/or `Response` from `Context` if they are parameters:
```java ```java
@Action(value = "upload", mode = Mode.HTTP_POST) @Action(value = "upload", mode = Mode.HTTP_POST)
public String upload(Request<?, ?> req, Response<?, ?> res) throws ApplicationException { public String upload(Request<?, ?> req, Response<?, ?> res) throws ApplicationException {
// req.getParameter("file"), res.setHeader(...), etc. // Access raw request/response if needed
return "ok"; return "ok";
} }
``` ```
### Path Matching Priority
If two methods share the same path, the framework uses the first match in the `ActionRegistry`. Use explicit `Mode` values to disambiguate (e.g., separating a GET for a form and a POST for submission).

View File

@@ -2,13 +2,26 @@
## When to Use ## When to Use
Use the system and usage patterns described here when you need to handle stateful interactions across CLI and HTTP modes, manage user sessions in web applications, or implement loosely coupled communication between application modules using an event-driven architecture. Use these patterns to handle request state, manage web sessions, implement Server-Sent Events (SSE), handle file uploads, or perform outbound HTTP networking.
## How It Works ## How It Works
The framework's `Context` serves as the primary data store for request-specific state. In CLI mode, flags passed as `--key value` are automatically parsed and stored in the `Context` with the `--` prefix, allowing action methods to retrieve command parameters easily. For web applications, the system provides standard session management via the `Request` object, enabling the storage of user data across multiple HTTP requests. ### Context and CLI Arguments
`Context` is the primary data store for request-specific state. CLI flags passed as `--key value` are stored in `Context` as `"--key"`.
The internal `EventDispatcher` facilitates an asynchronous event bus. By defining custom `Event` classes and registering handlers (typically within an application's `init()` method), you can trigger background tasks—such as sending emails or logging audit trails—without blocking the main execution path. ### Session Management
Pluggable architecture. Default is `MemorySessionRepository`. Configure Redis in `application.properties`:
```properties
default.session.repository=org.tinystruct.http.RedisSessionRepository
redis.host=127.0.0.1
redis.port=6379
```
### Server-Sent Events (SSE)
Built-in support for real-time push. The `HttpServer` automatically handles the SSE lifecycle when it detects the `Accept: text/event-stream` header. Connections are tracked by session ID in `SSEPushManager`.
### Outbound Networking
Use `URLRequest` and `HTTPHandler` for making HTTP requests to external services.
## Examples ## Examples
@@ -23,52 +36,62 @@ public String echo() {
} }
``` ```
### Session Management (Web Mode) ### Session Management
```java ```java
@Action(value = "login", mode = Mode.HTTP_POST) @Action(value = "login", mode = Mode.HTTP_POST)
public String login(Request request) { public String login(Request<?, ?> request) {
request.getSession().setAttribute("userId", "42"); request.getSession().setAttribute("userId", "42");
return "Logged in"; return "Logged in";
} }
```
@Action("profile") ### Server-Sent Events (SSE)
public String profile(Request request) { ```java
Object userId = request.getSession().getAttribute("userId"); @Action("sse/connect")
if (userId == null) return "Not logged in"; public String connect() {
return "User: " + userId; return "{\"type\":\"connect\",\"message\":\"Connected\"}";
}
// In another method or event handler:
String sessionId = getContext().getId();
SSEPushManager.getInstance().push(sessionId, new Builder().put("msg", "hello"));
```
### File Uploads
```java
import org.tinystruct.data.FileEntity;
@Action(value = "upload", mode = Mode.HTTP_POST)
public String upload(Request<?, ?> request) throws ApplicationException {
List<FileEntity> files = request.getAttachments();
if (files != null) {
for (FileEntity file : files) {
// file.getFilename(), file.getContent()
}
}
return "Uploaded";
}
```
### Outbound HTTP
```java
import org.tinystruct.net.URLRequest;
import org.tinystruct.net.handlers.HTTPHandler;
URLRequest request = new URLRequest(new URL("https://api.example.com"));
request.setMethod("POST").setBody("{\"data\":\"val\"}");
HTTPHandler handler = new HTTPHandler();
var response = handler.handleRequest(request);
if (response.getStatusCode() == 200) {
String body = response.getBody();
} }
``` ```
### Event System ### Event System
Register handlers in `init()` for asynchronous task execution.
```java ```java
// 1. Define an event EventDispatcher.getInstance().registerHandler(MyEvent.class, event -> {
public class OrderCreatedEvent implements org.tinystruct.system.Event<Order> { CompletableFuture.runAsync(() -> doHeavyWork(event.getPayload()));
private final Order order;
public OrderCreatedEvent(Order order) { this.order = order; }
@Override public String getName() { return "order_created"; }
@Override public Order getPayload() { return order; }
}
// 2. Register a handler
EventDispatcher.getInstance().registerHandler(OrderCreatedEvent.class, event -> {
CompletableFuture.runAsync(() -> sendConfirmationEmail(event.getPayload()));
}); });
// 3. Dispatch
EventDispatcher.getInstance().dispatch(new OrderCreatedEvent(newOrder));
```
### Running the Application
```bash
# CLI mode
bin/dispatcher hello
bin/dispatcher echo --words "Hello" --import com.example.HelloApp
# HTTP server (listens on :8080 by default)
bin/dispatcher start --import org.tinystruct.system.HttpServer
# Database utilities
bin/dispatcher generate --table users
bin/dispatcher sql-query "SELECT * FROM users"
``` ```

View File

@@ -2,58 +2,71 @@
## When to Use ## When to Use
Use the testing patterns described here when writing units tests for your tinystruct applications with **JUnit 5**. These patterns are essential for verifying that your `@Action` methods return the correct results and that your routing logic is properly registered within the singleton `ActionRegistry`. Use these patterns when writing unit tests for your applications with **JUnit 5**. Essential for verifying action logic, routing registration, and HTTP mode behavior.
## How It Works ## How It Works
Testing tinystruct applications requires a specific setup to ensure framework-level features like annotation processing and configuration management are active. By creating a new instance of your application and passing it a `Settings` object in the `setUp()` method, you trigger the `init()` lifecycle. This ensures all `@Action` methods are discovered and registered. ### Unit Testing Applications
ActionRegistry is a singleton. To test an application:
1. Instantiate the application.
2. Provide a `Settings` object (triggers `init()` and annotation processing).
3. Use `app.invoke(path, args)` to test logic directly.
Because the `ActionRegistry` is a singleton, it is critical to maintain isolation between tests by properly initializing your application state before each test execution, preventing side effects from leaking across the test suite. ### HTTP Integration Testing
For tests involving the built-in HTTP server:
1. Start `HttpServer` in a background thread.
2. Use `ApplicationManager.call("start", context, Action.Mode.CLI)` to boot.
3. Wait for the port to be open using a `Socket`.
4. Use `URLRequest` and `HTTPHandler` to perform actual requests.
## Examples ## Examples
### Unit Testing an Application ### Unit Test
```java ```java
import org.junit.jupiter.api.*; import org.junit.jupiter.api.*;
import org.tinystruct.application.ActionRegistry;
import org.tinystruct.system.Settings; import org.tinystruct.system.Settings;
class MyAppTest { class MyAppTest {
private MyApp app; private MyApp app;
@BeforeEach @BeforeEach
void setUp() { void setUp() {
app = new MyApp(); app = new MyApp();
Settings config = new Settings(); app.setConfiguration(new Settings());
app.setConfiguration(config); app.init(); // triggers @Action annotation processing and registers all actions
app.init(); // triggers @Action annotation processing
} }
@Test
void testHello() throws Exception { void testHello() throws Exception {
// Direct invocation via the application object
Object result = app.invoke("hello"); Object result = app.invoke("hello");
Assertions.assertEquals("Hello, tinystruct!", result); Assertions.assertEquals("Hello!", result);
} }
@Test @Test
void testGreet() throws Exception { void testGreet() throws Exception {
// Invocation with arguments
Object result = app.invoke("greet", new Object[]{"James"}); Object result = app.invoke("greet", new Object[]{"James"});
Assertions.assertEquals("Hello, James!", result); Assertions.assertEquals("Hello, James!", result);
} }
} }
``` ```
### Testing via ActionRegistry ### ActionRegistry Match Testing
If you need to test the routing logic itself, use the `ActionRegistry` singleton to verify path matching:
```java ```java
@Test @Test
void testRouting() { void testRouting() {
ActionRegistry registry = ActionRegistry.getInstance(); ActionRegistry registry = ActionRegistry.getInstance();
// Verify a path matches an action
Action action = registry.getAction("greet/James"); Action action = registry.getAction("greet/James");
Assertions.assertNotNull(action); Assertions.assertNotNull(action);
} }
``` ```
Reference: `src/test/java/org/tinystruct/application/ActionRegistryTest.java`
### HTTP Integration Pattern
Reference: `src/test/java/org/tinystruct/system/HttpServerHttpModeTest.java`
```java
// Pattern:
// 1. Start server in thread
// 2. Poll for port availability
// 3. Send HTTP request via HTTPHandler
// 4. Assert response body/status
```