|
| 1 | +--- |
| 2 | +name: tinystruct-dev |
| 3 | +description: Expert guidance for developing with the tinystruct Java framework. Use this skill whenever working on the tinystruct codebase or any project built on tinystruct — including creating new Application classes, adding @Action-mapped routes, writing unit tests, working with ActionRegistry, setting up HTTP/CLI dual-mode handling, configuring the built-in HTTP server, using the event system, handling JSON with Builder, or debugging routing and context issues. Trigger this skill for any task involving tinystruct patterns, framework internals, or developer conventions. |
| 4 | +--- |
| 5 | + |
| 6 | +# tinystruct Framework Developer Skill |
| 7 | + |
| 8 | +This skill captures the architecture, conventions, and patterns of the **tinystruct** Java framework — a lightweight, high-performance framework that treats CLI and HTTP as equal citizens, requiring no `main()` method and minimal configuration. |
| 9 | + |
| 10 | +Project root: `%HOME%\IdeaProjects\tinystruct` |
| 11 | +*(Note: If this directory does not exist, clone the framework repository from `https://github.com/tinystruct/tinystruct.git` to create it)* |
| 12 | + |
| 13 | +--- |
| 14 | + |
| 15 | +## Core Architecture |
| 16 | + |
| 17 | +### Key Abstractions |
| 18 | + |
| 19 | +| Class/Interface | Role | |
| 20 | +|---|---| |
| 21 | +| `AbstractApplication` | Base class for all tinystruct applications. Extend this. | |
| 22 | +| `@Action` annotation | Maps a method to a URI path (web) or command name (CLI). The single routing primitive. | |
| 23 | +| `ActionRegistry` | Singleton that maps URL patterns to `Action` objects via regex. Never instantiate directly. | |
| 24 | +| `Action` | Wraps a `MethodHandle` + regex pattern + priority + `Mode` for dispatch. | |
| 25 | +| `Context` | Per-request state store. Access via `getContext()`. Holds CLI args and HTTP request/response. | |
| 26 | +| `Dispatcher` | CLI entry point (`bin/dispatcher`). Reads `--import` to load applications. | |
| 27 | +| `HttpServer` | Built-in Netty-based HTTP server. Start with `bin/dispatcher start --import org.tinystruct.system.HttpServer`. | |
| 28 | + |
| 29 | +### Package Map |
| 30 | + |
| 31 | +``` |
| 32 | +org.tinystruct/ |
| 33 | +├── AbstractApplication.java → extend this |
| 34 | +├── Application.java → interface |
| 35 | +├── ApplicationException.java → checked exception |
| 36 | +├── ApplicationRuntimeException.java → unchecked exception |
| 37 | +├── application/ |
| 38 | +│ ├── Action.java → runtime action wrapper |
| 39 | +│ ├── ActionRegistry.java → singleton route registry |
| 40 | +│ └── Context.java → request context |
| 41 | +├── system/ |
| 42 | +│ ├── annotation/Action.java → @Action annotation + Mode enum |
| 43 | +│ ├── Dispatcher.java → CLI dispatcher |
| 44 | +│ ├── HttpServer.java → built-in HTTP server |
| 45 | +│ ├── EventDispatcher.java → event bus |
| 46 | +│ └── Settings.java → reads application.properties |
| 47 | +├── data/component/Builder.java → JSON serialization (use instead of Gson/Jackson) |
| 48 | +└── http/ → Request, Response, Constants |
| 49 | +``` |
| 50 | + |
| 51 | +--- |
| 52 | + |
| 53 | +## Creating an Application |
| 54 | + |
| 55 | +Every module is an `Application`. Extend `AbstractApplication`: |
| 56 | + |
| 57 | +```java |
| 58 | +package com.example; |
| 59 | + |
| 60 | +import org.tinystruct.AbstractApplication; |
| 61 | +import org.tinystruct.ApplicationException; |
| 62 | +import org.tinystruct.system.annotation.Action; |
| 63 | +import org.tinystruct.system.annotation.Action.Mode; |
| 64 | + |
| 65 | +public class HelloApp extends AbstractApplication { |
| 66 | + |
| 67 | + @Override |
| 68 | + public void init() { |
| 69 | + // One-time setup: set config, register resources. |
| 70 | + // Do NOT register actions here — use @Action annotation instead. |
| 71 | + this.setTemplateRequired(false); // skip .view template lookup if returning data directly |
| 72 | + } |
| 73 | + |
| 74 | + @Override |
| 75 | + public String version() { |
| 76 | + return "1.0.0"; |
| 77 | + } |
| 78 | + |
| 79 | + // Handles: bin/dispatcher hello AND GET /?q=hello |
| 80 | + @Action("hello") |
| 81 | + public String hello() { |
| 82 | + return "Hello, tinystruct!"; |
| 83 | + } |
| 84 | + |
| 85 | + // Path parameter: GET /?q=greet/James OR bin/dispatcher greet/James |
| 86 | + @Action("greet") |
| 87 | + public String greet(String name) { |
| 88 | + return "Hello, " + name + "!"; |
| 89 | + } |
| 90 | + |
| 91 | + // HTTP-only POST handler |
| 92 | + @Action(value = "submit", mode = Mode.HTTP_POST) |
| 93 | + public String submit() throws ApplicationException { |
| 94 | + // Access raw request if needed |
| 95 | + return "Submitted"; |
| 96 | + } |
| 97 | +} |
| 98 | +``` |
| 99 | + |
| 100 | +### `init()` Rules |
| 101 | +- Called once when the application is loaded (via `setConfiguration()`). |
| 102 | +- Use it for: setting up DB connections, configuring resource paths, calling `setTemplateRequired(false)`. |
| 103 | +- **Do not** call `setAction()` here — use `@Action` annotation, which is processed automatically by `AnnotationProcessor`. |
| 104 | + |
| 105 | +--- |
| 106 | + |
| 107 | +## @Action Annotation Reference |
| 108 | + |
| 109 | +```java |
| 110 | +@Action( |
| 111 | + value = "path/subpath", // required: URI segment or CLI command |
| 112 | + description = "What it does", // shown in --help output |
| 113 | + mode = Mode.HTTP_POST, // default: Mode.DEFAULT (both CLI + HTTP) |
| 114 | + arguments = { // optional: parameter metadata for CLI help |
| 115 | + @Argument(key = "--id", description = "The item ID") |
| 116 | + }, |
| 117 | + options = {}, // CLI option flags |
| 118 | + example = "bin/dispatcher path/subpath --id 42" |
| 119 | +) |
| 120 | +public String myAction(int id) { ... } |
| 121 | +``` |
| 122 | + |
| 123 | +### Mode Values |
| 124 | +| Mode | When it triggers | |
| 125 | +|---|---| |
| 126 | +| `DEFAULT` | Both CLI and HTTP (GET, POST, etc.) | |
| 127 | +| `CLI` | CLI dispatcher only | |
| 128 | +| `HTTP_GET` | HTTP GET only | |
| 129 | +| `HTTP_POST` | HTTP POST only | |
| 130 | +| `HTTP_PUT` | HTTP PUT only | |
| 131 | +| `HTTP_DELETE` | HTTP DELETE only | |
| 132 | +| `HTTP_PATCH` | HTTP PATCH only | |
| 133 | + |
| 134 | +### Path Parameters |
| 135 | +tinystruct automatically builds a regex from the method signature: |
| 136 | + |
| 137 | +```java |
| 138 | +@Action("user/{id}") |
| 139 | +public String getUser(int id) { ... } |
| 140 | +// → pattern: ^/?user/(-?\d+)$ |
| 141 | + |
| 142 | +@Action("search") |
| 143 | +public String search(String query) { ... } |
| 144 | +// → pattern: ^/?search/([^/]+)$ |
| 145 | +// → CLI: bin/dispatcher search/hello |
| 146 | +// → HTTP: /?q=search/hello |
| 147 | +``` |
| 148 | + |
| 149 | +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`). |
| 150 | + |
| 151 | +### Accessing Request/Response |
| 152 | + |
| 153 | +Include `Request` and/or `Response` as parameters — ActionRegistry automatically injects them from `Context`: |
| 154 | + |
| 155 | +```java |
| 156 | +@Action(value = "upload", mode = Mode.HTTP_POST) |
| 157 | +public String upload(Request<?, ?> req, Response<?, ?> res) throws ApplicationException { |
| 158 | + // req.getParameter("file"), res.setHeader(...), etc. |
| 159 | + return "ok"; |
| 160 | +} |
| 161 | +``` |
| 162 | + |
| 163 | +--- |
| 164 | + |
| 165 | +## Context and CLI Arguments |
| 166 | + |
| 167 | +```java |
| 168 | +@Action("echo") |
| 169 | +public String echo() { |
| 170 | + // CLI: bin/dispatcher echo --words "Hello World" |
| 171 | + Object words = getContext().getAttribute("--words"); |
| 172 | + if (words != null) return words.toString(); |
| 173 | + return "No words provided"; |
| 174 | +} |
| 175 | +``` |
| 176 | + |
| 177 | +CLI flags passed as `--key value` are stored in `Context` as `"--key"` → value. |
| 178 | + |
| 179 | +--- |
| 180 | + |
| 181 | +## JSON Handling (use `Builder`, not Gson/Jackson) |
| 182 | + |
| 183 | +```java |
| 184 | +import org.tinystruct.data.component.Builder; |
| 185 | + |
| 186 | +// Serialize |
| 187 | +Builder response = new Builder(); |
| 188 | +response.put("status", "success"); |
| 189 | +response.put("count", 42); |
| 190 | +response.put("data", someList); |
| 191 | +return response.toString(); // {"status":"success","count":42,...} |
| 192 | + |
| 193 | +// Parse |
| 194 | +Builder parsed = new Builder(); |
| 195 | +parsed.parse(jsonString); |
| 196 | +String status = parsed.get("status").toString(); |
| 197 | +``` |
| 198 | + |
| 199 | +--- |
| 200 | + |
| 201 | +## Session Management (Web Mode) |
| 202 | + |
| 203 | +```java |
| 204 | +@Action(value = "login", mode = Mode.HTTP_POST) |
| 205 | +public String login() { |
| 206 | + getContext().getSession().setAttribute("userId", "42"); |
| 207 | + return "Logged in"; |
| 208 | +} |
| 209 | + |
| 210 | +@Action("profile") |
| 211 | +public String profile() { |
| 212 | + Object userId = getContext().getSession().getAttribute("userId"); |
| 213 | + if (userId == null) return "Not logged in"; |
| 214 | + return "User: " + userId; |
| 215 | +} |
| 216 | +``` |
| 217 | + |
| 218 | +--- |
| 219 | + |
| 220 | +## Event System |
| 221 | + |
| 222 | +```java |
| 223 | +// 1. Define an event |
| 224 | +public class OrderCreatedEvent implements org.tinystruct.system.Event<Order> { |
| 225 | + private final Order order; |
| 226 | + public OrderCreatedEvent(Order order) { this.order = order; } |
| 227 | + |
| 228 | + @Override public String getName() { return "order_created"; } |
| 229 | + @Override public Order getPayload() { return order; } |
| 230 | +} |
| 231 | + |
| 232 | +// 2. Register a handler (typically in init()) |
| 233 | +EventDispatcher.getInstance().registerHandler(OrderCreatedEvent.class, event -> { |
| 234 | + CompletableFuture.runAsync(() -> sendConfirmationEmail(event.getPayload())); |
| 235 | +}); |
| 236 | + |
| 237 | +// 3. Dispatch |
| 238 | +EventDispatcher.getInstance().dispatch(new OrderCreatedEvent(newOrder)); |
| 239 | +``` |
| 240 | + |
| 241 | +--- |
| 242 | + |
| 243 | +## Templates |
| 244 | + |
| 245 | +If `templateRequired` is `true` (the default), `toString()` looks for a `.view` file: |
| 246 | +- Location: `src/main/resources/themes/<ClassName>.view` (on classpath) |
| 247 | +- Variables are interpolated using `[%variableName%]` |
| 248 | + |
| 249 | +```java |
| 250 | +// In your action method: |
| 251 | +setVariable("username", "James"); |
| 252 | +setVariable("count", String.valueOf(42)); |
| 253 | +// The template file uses: [%username%] and [%count%] |
| 254 | +``` |
| 255 | + |
| 256 | +To skip templates and return data directly (e.g., for APIs): |
| 257 | +```java |
| 258 | +@Override |
| 259 | +public void init() { |
| 260 | + this.setTemplateRequired(false); |
| 261 | +} |
| 262 | +``` |
| 263 | + |
| 264 | +--- |
| 265 | + |
| 266 | +## Configuration (`application.properties`) |
| 267 | + |
| 268 | +Located at `src/main/resources/application.properties`: |
| 269 | + |
| 270 | +```properties |
| 271 | +# Database |
| 272 | +driver=org.h2.Driver |
| 273 | +database.url=jdbc:h2:~/mydb |
| 274 | +database.user=sa |
| 275 | +database.password= |
| 276 | + |
| 277 | +# Server |
| 278 | +default.home.page=hello # default action for /?q= (root URL) |
| 279 | +server.port=8080 |
| 280 | + |
| 281 | +# Locale |
| 282 | +default.language=en_US |
| 283 | +``` |
| 284 | + |
| 285 | +Access config values in your application: |
| 286 | +```java |
| 287 | +String port = this.getConfiguration("server.port"); |
| 288 | +``` |
| 289 | + |
| 290 | +--- |
| 291 | + |
| 292 | +## Running the Application |
| 293 | + |
| 294 | +```bash |
| 295 | +# CLI mode |
| 296 | +bin/dispatcher hello |
| 297 | +bin/dispatcher greet/James |
| 298 | +bin/dispatcher echo --words "Hello" --import com.example.HelloApp |
| 299 | + |
| 300 | +# HTTP server (listens on :8080 by default) |
| 301 | +bin/dispatcher start --import org.tinystruct.system.HttpServer |
| 302 | +# Then: http://localhost:8080/?q=hello |
| 303 | + |
| 304 | +# Generate POJO from DB table |
| 305 | +bin/dispatcher generate --table users |
| 306 | + |
| 307 | +# Run SQL |
| 308 | +bin/dispatcher sql-query "SELECT * FROM users" --import org.tinystruct.system.Dispatcher |
| 309 | +``` |
| 310 | + |
| 311 | +--- |
| 312 | + |
| 313 | +## Testing Patterns |
| 314 | + |
| 315 | +Use JUnit 5. ActionRegistry is a singleton — reset or use fresh state carefully in tests. |
| 316 | + |
| 317 | +```java |
| 318 | +import org.junit.jupiter.api.*; |
| 319 | +import org.tinystruct.application.ActionRegistry; |
| 320 | + |
| 321 | +class MyAppTest { |
| 322 | + |
| 323 | + private MyApp app; |
| 324 | + |
| 325 | + @BeforeEach |
| 326 | + void setUp() { |
| 327 | + app = new MyApp(); |
| 328 | + // Set a minimal configuration to trigger init() and annotation processing |
| 329 | + Settings config = new Settings(); |
| 330 | + app.setConfiguration(config); |
| 331 | + } |
| 332 | + |
| 333 | + @Test |
| 334 | + void testHello() throws Exception { |
| 335 | + Object result = app.invoke("hello"); |
| 336 | + Assertions.assertEquals("Hello, tinystruct!", result); |
| 337 | + } |
| 338 | + |
| 339 | + @Test |
| 340 | + void testGreet() throws Exception { |
| 341 | + Object result = app.invoke("greet", new Object[]{"James"}); |
| 342 | + Assertions.assertEquals("Hello, James!", result); |
| 343 | + } |
| 344 | +} |
| 345 | +``` |
| 346 | + |
| 347 | +For `ActionRegistry` unit tests, follow the pattern in: |
| 348 | +`src/test/java/org/tinystruct/application/ActionRegistryTest.java` |
| 349 | + |
| 350 | +--- |
| 351 | + |
| 352 | +## Common Pitfalls |
| 353 | + |
| 354 | +| Problem | Fix | |
| 355 | +|---|---| |
| 356 | +| `ApplicationRuntimeException: template not found` | Call `setTemplateRequired(false)` in `init()` if you return data directly | |
| 357 | +| Action not found at runtime | Make sure the class is imported via `--import` or listed in `application.properties` | |
| 358 | +| Method not registered | Ensure `@Action` annotation is on a `public` method — private/protected methods are ignored | |
| 359 | +| CLI arg not visible | Pass with `--key value` syntax; access via `getContext().getAttribute("--key")` | |
| 360 | +| JSON using Gson/Jackson | Use `org.tinystruct.data.component.Builder` instead — it's the framework-native JSON library | |
| 361 | +| Two methods same path, wrong one fires | Set explicit `mode` (e.g., `HTTP_GET` vs `HTTP_POST`) to disambiguate | |
| 362 | + |
| 363 | +--- |
| 364 | + |
| 365 | +## Reference Files |
| 366 | + |
| 367 | +- `DEVELOPER_GUIDE.md` — full developer guide with examples |
| 368 | +- `README.md` — quick start and architecture diagram |
| 369 | +- `src/main/java/org/tinystruct/AbstractApplication.java` — complete base class |
| 370 | +- `src/main/java/org/tinystruct/system/annotation/Action.java` — annotation definition + `Mode` enum |
| 371 | +- `src/main/java/org/tinystruct/application/ActionRegistry.java` — routing engine |
| 372 | +- `src/test/java/org/tinystruct/application/ActionRegistryTest.java` — registry test examples |
0 commit comments