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

@@ -2,7 +2,7 @@
## 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
@@ -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. |
| `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. |
| `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
@@ -40,13 +40,23 @@ org.tinystruct/
│ ├── HttpServer.java ← built-in HTTP server
│ ├── EventDispatcher.java ← event bus
│ └── Settings.java ← reads application.properties
├── data/component/Builder.java ← JSON serialization (use instead of Gson/Jackson)
└── http/ ← Request, Response, Constants
├── data/
│ ├── 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
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
@@ -55,6 +65,7 @@ By default, the framework assumes a view template is required. If `templateRequi
@Override
public void init() {
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:**
```bash
bin/dispatcher hello
bin/dispatcher greet/James
bin/dispatcher echo --words "Hello" --import com.example.HelloApp
```
### Configuration Access

View File

@@ -2,34 +2,59 @@
## 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
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
### Serialization
### Serialize a Single Object
```java
import org.tinystruct.data.component.Builder;
// Create and populate
Builder response = new Builder();
response.put("status", "success");
response.put("count", 42);
response.put("data", someList);
return response; // {"status":"success","count":42,...}
return response.toString(); // {"status":"success","count":42}
```
### Parsing
### Serialize a List using Builders
```java
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();
parsed.parse(jsonString);
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
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
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
@@ -22,6 +26,8 @@ When a request is dispatched, the `ActionRegistry` automatically injects depende
| `HTTP_DELETE` | HTTP DELETE 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
### Basic Action Declaration
@@ -29,29 +35,30 @@ When a request is dispatched, the `ActionRegistry` automatically injects depende
@Action(
value = "path/subpath", // required: URI segment or CLI command
description = "What it does", // shown in --help output
mode = Mode.HTTP_POST, // default: Mode.DEFAULT (both CLI + HTTP)
options = {}, // CLI option flags
example = "curl -X POST http://localhost:8080/path/subpath/42"
mode = Mode.DEFAULT, // default: Mode.DEFAULT
example = "bin/dispatcher path/subpath/42"
)
public String myAction(int id) { ... }
```
### Parameterized Paths (Regex Generation)
### Parameterized Paths
```java
@Action("user/{id}")
public String getUser(int id) { ... }
// → pattern: ^/?user/(-?\d+)$
@Action("search")
public String search(String query) { ... }
// → pattern: ^/?search/([^/]+)$
// → CLI: bin/dispatcher user/42
// → HTTP: /?q=user/42
```
### Request and Response Injection
### Dependency Injection
`ActionRegistry` automatically injects `Request` and/or `Response` from `Context` if they are parameters:
```java
@Action(value = "upload", mode = Mode.HTTP_POST)
public String upload(Request<?, ?> req, Response<?, ?> res) throws ApplicationException {
// req.getParameter("file"), res.setHeader(...), etc.
// Access raw request/response if needed
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
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
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
@@ -23,52 +36,62 @@ public String echo() {
}
```
### Session Management (Web Mode)
### Session Management
```java
@Action(value = "login", mode = Mode.HTTP_POST)
public String login(Request request) {
public String login(Request<?, ?> request) {
request.getSession().setAttribute("userId", "42");
return "Logged in";
}
```
@Action("profile")
public String profile(Request request) {
Object userId = request.getSession().getAttribute("userId");
if (userId == null) return "Not logged in";
return "User: " + userId;
### Server-Sent Events (SSE)
```java
@Action("sse/connect")
public String connect() {
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
Register handlers in `init()` for asynchronous task execution.
```java
// 1. Define an event
public class OrderCreatedEvent implements org.tinystruct.system.Event<Order> {
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()));
EventDispatcher.getInstance().registerHandler(MyEvent.class, event -> {
CompletableFuture.runAsync(() -> doHeavyWork(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
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
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
### Unit Testing an Application
### Unit Test
```java
import org.junit.jupiter.api.*;
import org.tinystruct.application.ActionRegistry;
import org.tinystruct.system.Settings;
class MyAppTest {
private MyApp app;
@BeforeEach
void setUp() {
app = new MyApp();
Settings config = new Settings();
app.setConfiguration(config);
app.init(); // triggers @Action annotation processing
app.setConfiguration(new Settings());
app.init(); // triggers @Action annotation processing and registers all actions
}
@Test
void testHello() throws Exception {
// Direct invocation via the application object
Object result = app.invoke("hello");
Assertions.assertEquals("Hello, tinystruct!", result);
Assertions.assertEquals("Hello!", result);
}
@Test
void testGreet() throws Exception {
// Invocation with arguments
Object result = app.invoke("greet", new Object[]{"James"});
Assertions.assertEquals("Hello, James!", result);
}
}
```
### Testing via ActionRegistry
If you need to test the routing logic itself, use the `ActionRegistry` singleton to verify path matching:
### ActionRegistry Match Testing
```java
@Test
void testRouting() {
ActionRegistry registry = ActionRegistry.getInstance();
// Verify a path matches an action
Action action = registry.getAction("greet/James");
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
```