aris 1.4.2 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +251 -0
  3. data/README.md +18 -0
  4. data/docs/ADAPTERS.md +478 -0
  5. data/docs/ARCHITECTURE.md +222 -0
  6. data/docs/CONTENT.md +967 -0
  7. data/docs/PERFORMANCE.md +492 -0
  8. data/docs/PLUGIN_DEVELOPMENT.md +688 -0
  9. data/docs/USAGE.md +4998 -0
  10. data/docs/plugins/API_KEY_AUTH.md +232 -0
  11. data/docs/plugins/BASIC_AUTH.md +582 -0
  12. data/docs/plugins/BEARER_AUTH.md +394 -0
  13. data/docs/plugins/CACHE.md +369 -0
  14. data/docs/plugins/COMPRESSION.md +216 -0
  15. data/docs/plugins/COOKIES.md +30 -0
  16. data/docs/plugins/CORS.md +283 -0
  17. data/docs/plugins/CSRF.md +751 -0
  18. data/docs/plugins/ETAG.md +308 -0
  19. data/docs/plugins/FORM_PARSER.md +193 -0
  20. data/docs/plugins/HEALTH_CHECK.md +469 -0
  21. data/docs/plugins/JSON.md +291 -0
  22. data/docs/plugins/MULTIPART.md +427 -0
  23. data/docs/plugins/RATE_LIMITER.md +368 -0
  24. data/docs/plugins/REQUEST_ID.md +369 -0
  25. data/docs/plugins/REQUEST_LOGGER.md +151 -0
  26. data/docs/plugins/SECURITY.md +193 -0
  27. data/docs/plugins/SESSION.md +98 -0
  28. data/lib/aris/adapters/rack/adapter.rb +17 -2
  29. data/lib/aris/adapters/rack/request.rb +29 -11
  30. data/lib/aris/plugins/basic_auth.rb +3 -1
  31. data/lib/aris/plugins/cookies.rb +4 -32
  32. data/lib/aris/plugins/cors.rb +8 -1
  33. data/lib/aris/plugins/csrf.rb +63 -22
  34. data/lib/aris/plugins/flash.rb +3 -1
  35. data/lib/aris/plugins/form_parser.rb +52 -31
  36. data/lib/aris/plugins/multipart.rb +22 -2
  37. data/lib/aris/plugins/request_logger.rb +8 -1
  38. data/lib/aris/plugins/security_headers.rb +8 -1
  39. data/lib/aris/plugins/session.rb +150 -99
  40. data/lib/aris/response_helpers.rb +41 -0
  41. data/lib/aris/version.rb +2 -2
  42. metadata +31 -3
data/docs/ADAPTERS.md ADDED
@@ -0,0 +1,478 @@
1
+ # Adapter Architecture
2
+
3
+ Aris Router uses a **PipelineRunner** abstraction that makes it server-agnostic. The router core handles route matching, while adapters translate between server-specific I/O and the universal `Request`/`Response` interface.
4
+
5
+ ## Architecture Overview
6
+
7
+ ```
8
+ ┌─────────────────────────────────────────────────────────────┐
9
+ │ Aris::Router (Core) │
10
+ │ Domain/Path/Method Matching │
11
+ └─────────────────────────────────────────────────────────────┘
12
+
13
+ ┌─────────────────────────────────────────────────────────────┐
14
+ │ Aris::PipelineRunner │
15
+ │ Executes Plugins → Handler → Response │
16
+ └─────────────────────────────────────────────────────────────┘
17
+
18
+ ┌──────────────────┐ ┌──────────────────┐ ┌────────────────┐
19
+ │ Rack Adapter │ │ Mock Adapter │ │ Your Adapter │
20
+ │ (Puma, Falcon) │ │ (Testing) │ │ (Agoo, etc) │
21
+ └──────────────────┘ └──────────────────┘ └────────────────┘
22
+ ```
23
+
24
+ ## Key Concepts
25
+
26
+ ### 1. PipelineRunner (Server-Agnostic Core)
27
+
28
+ Located in `lib/aris/pipeline_runner.rb`, this module executes the plugin pipeline and handler. It works with ANY adapter that provides a compatible Request/Response.
29
+
30
+ **Signature:**
31
+ ```ruby
32
+ PipelineRunner.call(request: request, route: route, response: response)
33
+ # Returns: Response object, Array, Hash, or String
34
+ ```
35
+
36
+ ### 2. Adapter Responsibilities
37
+
38
+ An adapter must:
39
+ 1. Convert server-specific input → Aris Request
40
+ 2. Set thread-local domain context
41
+ 3. Match route via `Router.match`
42
+ 4. Call `PipelineRunner.call`
43
+ 5. Format PipelineRunner result → server-specific output
44
+ 6. Clean up thread-local context
45
+
46
+ ### 3. Request Interface Contract
47
+
48
+ Any Request class MUST implement these methods:
49
+
50
+ ```ruby
51
+ class YourAdapter::Request
52
+ # Required: HTTP method
53
+ def method
54
+ # Return: String (e.g., "GET", "POST")
55
+ end
56
+
57
+ def request_method
58
+ # Alias for method (some plugins use this)
59
+ end
60
+
61
+ # Required: Path
62
+ def path
63
+ # Return: String (e.g., "/users/123")
64
+ end
65
+
66
+ def path_info
67
+ # Alias for path
68
+ end
69
+
70
+ # Required: Domain/Host
71
+ def domain
72
+ # Return: String (e.g., "example.com")
73
+ end
74
+
75
+ def host
76
+ # Alias for domain
77
+ end
78
+
79
+ # Required: Query string
80
+ def query
81
+ # Return: String (e.g., "page=1&limit=10")
82
+ end
83
+
84
+ # Required: Headers
85
+ def headers
86
+ # Return: Hash with header names as keys
87
+ # Example: { 'HTTP_AUTHORIZATION' => 'Bearer token' }
88
+ end
89
+
90
+ # Required: Body
91
+ def body
92
+ # Return: String (raw request body)
93
+ end
94
+
95
+ # Required: Query parameters
96
+ def params
97
+ # Return: Hash of parsed query string
98
+ # Example: { 'page' => '1', 'limit' => '10' }
99
+ end
100
+
101
+ # Optional but recommended: Struct-like access
102
+ def [](key)
103
+ # Return value for :method, :domain, :path, etc.
104
+ end
105
+
106
+ # Required: Plugin data attachment
107
+ attr_accessor :json_body # For JSON parser plugin
108
+ # Plugins may use instance variables like @current_user
109
+ end
110
+ ```
111
+
112
+ ### 4. Response Interface Contract
113
+
114
+ Any Response class MUST have these attributes:
115
+
116
+ ```ruby
117
+ class YourAdapter::Response
118
+ attr_accessor :status # Integer (e.g., 200, 404)
119
+ attr_accessor :headers # Hash (e.g., {'content-type' => 'application/json'})
120
+ attr_accessor :body # Array of strings (e.g., ['Hello'])
121
+
122
+ def initialize
123
+ @status = 200
124
+ @headers = {'content-type' => 'text/html'}
125
+ @body = []
126
+ end
127
+ end
128
+ ```
129
+
130
+ ## Building a Custom Adapter
131
+
132
+ ### Example: Agoo Adapter
133
+
134
+ Here's how to create an adapter for Agoo (high-performance Ruby server):
135
+
136
+ **File Structure:**
137
+ ```
138
+ lib/aris/adapters/agoo/
139
+ adapter.rb
140
+ request.rb
141
+ response.rb
142
+ ```
143
+
144
+ **1. Request Implementation (`agoo/request.rb`):**
145
+
146
+ ```ruby
147
+ module Aris
148
+ module Adapters
149
+ module Agoo
150
+ class Request
151
+ attr_reader :agoo_request
152
+ attr_accessor :json_body
153
+
154
+ def initialize(agoo_request)
155
+ @agoo_request = agoo_request
156
+ end
157
+
158
+ def method
159
+ @agoo_request.request_method
160
+ end
161
+
162
+ alias_method :request_method, :method
163
+
164
+ def path
165
+ @agoo_request.path_info
166
+ end
167
+
168
+ alias_method :path_info, :path
169
+
170
+ def domain
171
+ @agoo_request.headers['Host'] || 'localhost'
172
+ end
173
+
174
+ alias_method :host, :domain
175
+
176
+ def query
177
+ @agoo_request.query_string || ''
178
+ end
179
+
180
+ def headers
181
+ # Convert Agoo headers to standard format
182
+ @headers ||= @agoo_request.headers.transform_keys do |key|
183
+ "HTTP_#{key.upcase.gsub('-', '_')}"
184
+ end
185
+ end
186
+
187
+ def body
188
+ @body ||= @agoo_request.body
189
+ end
190
+
191
+ def params
192
+ @params ||= begin
193
+ return {} if query.empty?
194
+ query.split('&').each_with_object({}) do |pair, hash|
195
+ key, value = pair.split('=')
196
+ hash[key] = value if key
197
+ end
198
+ end
199
+ end
200
+
201
+ def [](key)
202
+ case key
203
+ when :method then method
204
+ when :domain then domain
205
+ when :path then path
206
+ when :host then host
207
+ else nil
208
+ end
209
+ end
210
+ end
211
+ end
212
+ end
213
+ end
214
+ ```
215
+
216
+ **2. Response Implementation (`agoo/response.rb`):**
217
+
218
+ ```ruby
219
+ module Aris
220
+ module Adapters
221
+ module Agoo
222
+ class Response
223
+ attr_accessor :status, :headers, :body
224
+
225
+ def initialize
226
+ @status = 200
227
+ @headers = {'content-type' => 'text/html'}
228
+ @body = []
229
+ end
230
+ end
231
+ end
232
+ end
233
+ end
234
+ ```
235
+
236
+ **3. Adapter Implementation (`agoo/adapter.rb`):**
237
+
238
+ ```ruby
239
+ require_relative '../../core'
240
+ require_relative '../../pipeline_runner'
241
+ require_relative 'request'
242
+ require_relative 'response'
243
+ require 'json'
244
+
245
+ module Aris
246
+ module Adapters
247
+ module Agoo
248
+ class Adapter
249
+ def call(agoo_request)
250
+ request = Request.new(agoo_request)
251
+ request_domain = request.host
252
+ Thread.current[:aris_current_domain] = request_domain
253
+
254
+ begin
255
+ route = Aris::Router.match(
256
+ domain: request_domain,
257
+ method: request.request_method.downcase.to_sym,
258
+ path: request.path_info
259
+ )
260
+
261
+ unless route
262
+ return format_agoo_response(Aris.not_found(request))
263
+ end
264
+
265
+ response = Response.new
266
+
267
+ # Core execution via PipelineRunner
268
+ result = PipelineRunner.call(
269
+ request: request,
270
+ route: route,
271
+ response: response
272
+ )
273
+
274
+ format_agoo_response(result, response)
275
+
276
+ rescue Aris::Router::RouteNotFoundError
277
+ return format_agoo_response(Aris.not_found(request))
278
+ rescue Exception => e
279
+ return format_agoo_response(Aris.error(request, e))
280
+ ensure
281
+ Thread.current[:aris_current_domain] = nil
282
+ end
283
+ end
284
+
285
+ private
286
+
287
+ def format_agoo_response(result, response = nil)
288
+ case result
289
+ when Response
290
+ # Return Agoo-compatible response
291
+ [result.status, result.headers, result.body]
292
+ when Array
293
+ # Already in [status, headers, body] format
294
+ result
295
+ when Hash
296
+ headers = response ? response.headers.merge({'content-type' => 'application/json'}) : {'content-type' => 'application/json'}
297
+ [200, headers, [result.to_json]]
298
+ else
299
+ headers = response ? response.headers.merge({'content-type' => 'text/plain'}) : {'content-type' => 'text/plain'}
300
+ [200, headers, [result.to_s]]
301
+ end
302
+ end
303
+ end
304
+ end
305
+ end
306
+ end
307
+ ```
308
+
309
+ **4. Usage:**
310
+
311
+ ```ruby
312
+ require 'agoo'
313
+ require 'aris'
314
+ require 'aris/adapters/agoo/adapter'
315
+
316
+ # Define routes
317
+ Aris.routes({
318
+ "example.com": {
319
+ "/hello": {
320
+ get: { to: ->(req, params) { "Hello from Agoo!" } }
321
+ }
322
+ }
323
+ })
324
+
325
+ # Start Agoo server
326
+ Agoo::Server.init(6464, 'root')
327
+
328
+ handler = Aris::Adapters::Agoo::Adapter.new
329
+ Agoo::Server.handle(:GET, '/hello', handler)
330
+
331
+ Agoo::Server.start
332
+ ```
333
+
334
+ ## Plugin Compatibility
335
+
336
+ **The beauty of this architecture:** Plugins work with ANY adapter without modification!
337
+
338
+ ```ruby
339
+ # This plugin works with Rack, Agoo, Mock, or any future adapter
340
+ bearer_auth = Aris::Plugins::BearerAuth.build(token: 'secret')
341
+
342
+ Aris.routes({
343
+ "api.example.com": {
344
+ use: [bearer_auth], # Works everywhere!
345
+ "/data": {
346
+ get: { to: DataHandler }
347
+ }
348
+ }
349
+ })
350
+ ```
351
+
352
+ ### Why Plugins Are Adapter-Agnostic
353
+
354
+ Plugins only use the **interface contract** methods:
355
+ - `request.method` - Works in any adapter
356
+ - `request.headers` - Works in any adapter
357
+ - `request.body` - Works in any adapter
358
+ - `response.status = 401` - Works in any adapter
359
+
360
+ Example from `BearerAuth` plugin:
361
+ ```ruby
362
+ def call(request, response)
363
+ auth_header = request.headers['HTTP_AUTHORIZATION'] # ← Interface method
364
+
365
+ if auth_header.nil?
366
+ response.status = 401 # ← Interface method
367
+ return response
368
+ end
369
+
370
+ # ... validation logic
371
+ nil # Continue pipeline
372
+ end
373
+ ```
374
+
375
+ ## Testing Your Adapter
376
+
377
+ ### 1. Basic Functionality Test
378
+
379
+ ```ruby
380
+ class YourAdapterTest < Minitest::Test
381
+ def test_basic_request
382
+ Aris.routes({
383
+ "test.com": {
384
+ "/hello": { get: { to: ->(req, params) { "Hi!" } } }
385
+ }
386
+ })
387
+
388
+ adapter = YourAdapter::Adapter.new
389
+ result = adapter.call(your_request_format)
390
+
391
+ assert_equal 200, result[:status]
392
+ end
393
+ end
394
+ ```
395
+
396
+ ### 2. Plugin Compatibility Test
397
+
398
+ ```ruby
399
+ def test_plugin_works
400
+ json_plugin = Aris::Plugins::Json
401
+
402
+ Aris.routes({
403
+ "test.com": {
404
+ use: [json_plugin],
405
+ "/api": { post: { to: ->(req, params) { req.json_body } } }
406
+ }
407
+ })
408
+
409
+ adapter = YourAdapter::Adapter.new
410
+ result = adapter.call(
411
+ method: 'POST',
412
+ body: '{"test": true}',
413
+ headers: { 'content-type' => 'application/json' }
414
+ )
415
+
416
+ # Plugin should have parsed JSON
417
+ assert_includes result[:body].first, 'test'
418
+ end
419
+ ```
420
+
421
+ ## Existing Adapters
422
+
423
+ ### Rack Adapter (Production)
424
+ - **Location:** `lib/aris/adapters/rack/`
425
+ - **Servers:** Puma, Falcon, Unicorn, Passenger, WEBrick
426
+ - **Usage:** `Aris::Adapters::Rack::Adapter.new`
427
+
428
+ ### Mock Adapter (Testing)
429
+ - **Location:** `lib/aris/adapters/mock/`
430
+ - **Purpose:** Unit testing without server overhead
431
+ - **Usage:** `Aris::Adapters::Mock::Adapter.new`
432
+
433
+ ## Performance Considerations
434
+
435
+ 1. **Request Object Caching:** Cache parsed headers, params, body to avoid re-parsing
436
+ 2. **Thread-Local Cleanup:** ALWAYS clean up `Thread.current[:aris_current_domain]` in ensure block
437
+ 3. **Minimal Allocations:** Reuse objects where possible in hot paths
438
+ 4. **Lazy Parsing:** Don't parse body/params until accessed
439
+
440
+ ## Adapter Checklist
441
+
442
+ When building a new adapter, verify:
443
+
444
+ - [ ] Request implements all required methods
445
+ - [ ] Response implements all required attributes
446
+ - [ ] Thread-local domain context is set/cleaned up
447
+ - [ ] Error handling (404/500) works
448
+ - [ ] Path parameters work (`/users/:id`)
449
+ - [ ] Query parameters work (`?page=1`)
450
+ - [ ] Request body is accessible
451
+ - [ ] Headers are accessible (with HTTP_ prefix convention)
452
+ - [ ] Plugins work (test with BearerAuth, Json, CORS)
453
+ - [ ] Response formatting handles all types (Response, Array, Hash, String)
454
+
455
+ ## Future Adapter Ideas
456
+
457
+ - **Iodine:** Native C extension server
458
+ - **Falcon:** Async fiber-based server
459
+ - **Thin:** EventMachine-based server
460
+ - **Direct CGI:** For traditional CGI environments
461
+ - **Lambda/Serverless:** AWS Lambda, Google Cloud Functions
462
+
463
+ ## Questions?
464
+
465
+ The adapter pattern is simple but powerful. If you're building a custom adapter and hit issues, check:
466
+ 1. Does your Request implement ALL interface methods?
467
+ 2. Is thread-local context cleaned up?
468
+ 3. Can you run the Mock adapter tests with your adapter?
469
+
470
+ Happy adapting! 🚀
471
+ ```
472
+
473
+ **Run tests to confirm docs are accurate:**
474
+ ```bash
475
+ ruby test/run_all_tests.rb
476
+ ```
477
+
478
+ ✅ Once that passes, want me to create a quick `ARCHITECTURE.md` showing the overall system design?
@@ -0,0 +1,222 @@
1
+ **Create: `docs/ARCHITECTURE.md`**
2
+
3
+ ```markdown
4
+ # Architecture
5
+
6
+ ## 30-Second Overview
7
+
8
+ ```
9
+ Router matches route → PipelineRunner executes plugins + handler → Adapter formats output
10
+ ```
11
+
12
+ **Key idea:** Router core knows nothing about servers. Adapters translate between servers and the universal Request/Response interface.
13
+
14
+ ## The Stack
15
+
16
+ ```
17
+ ┌─────────────────────────────────────────┐
18
+ │ Adapter (Rack, Agoo, Mock, etc.) │ ← Server-specific I/O
19
+ ├─────────────────────────────────────────┤
20
+ │ PipelineRunner │ ← Executes plugins → handler
21
+ ├─────────────────────────────────────────┤
22
+ │ Router (Trie-based matching) │ ← domain/path/method → route
23
+ └─────────────────────────────────────────┘
24
+ ```
25
+
26
+ ## Core Components
27
+
28
+ ### Router (`lib/aris/core.rb`)
29
+ - Trie-based route matching
30
+ - Compiles at startup (zero runtime parsing)
31
+ - Handles: domains, path params, wildcards, constraints
32
+ - Resolves plugin symbols → classes at compile time
33
+
34
+ ### PipelineRunner (`lib/aris/pipeline_runner.rb`)
35
+ - Server-agnostic execution
36
+ - Runs plugin chain, calls handler
37
+ - Returns result (Response/Array/Hash/String)
38
+
39
+ ### Adapters (`lib/aris/adapters/*`)
40
+ - Translate server input → Request
41
+ - Call PipelineRunner
42
+ - Format result → server output
43
+ - Manage thread-local context
44
+
45
+ ### Plugins (`lib/aris/plugins/*`)
46
+ - Contract: `call(request, response) → nil or response`
47
+ - Return `nil` = continue, return `response` = halt
48
+ - Work with ANY adapter (zero coupling)
49
+
50
+ ## Request Flow
51
+
52
+ ```
53
+ 1. HTTP request arrives
54
+ 2. Adapter creates Request object
55
+ 3. Router.match(domain, method, path) → route
56
+ 4. PipelineRunner.call(request, route, response)
57
+ ├─ Execute each plugin in route[:use]
58
+ ├─ If plugin returns response → halt, return it
59
+ └─ Execute handler with params
60
+ 5. Adapter formats result for server
61
+ ```
62
+
63
+ ## Design Decisions
64
+
65
+ **Hash-based config everywhere**
66
+ ```ruby
67
+ plugin.build(token: 'secret') # NOT plugin.build { |c| c.token = 'secret' }
68
+ ```
69
+
70
+ **No magic symbols/tags**
71
+ ```ruby
72
+ use: [:csrf, bearer_auth] # :csrf from registry, bearer_auth is instance
73
+ ```
74
+
75
+ **Compile-time resolution**
76
+ ```ruby
77
+ Router.define(config) # Symbols resolve to classes here, not per-request
78
+ ```
79
+
80
+ **Thread-local domain**
81
+ ```ruby
82
+ Thread.current[:aris_current_domain] # Enables path helpers without passing domain
83
+ ```
84
+
85
+ **Request mutation via ivars**
86
+ ```ruby
87
+ request.instance_variable_set(:@current_user, user) # Plugins attach data
88
+ handler.instance_variable_get(:@current_user) # Handlers read it
89
+ ```
90
+
91
+ ## Plugin Contract
92
+
93
+ ```ruby
94
+ class MyPlugin
95
+ def self.call(request, response)
96
+ # Read: request.method, request.path, request.headers, etc.
97
+ # Write: response.status, response.headers, response.body
98
+
99
+ return response if should_halt # Stop pipeline
100
+ nil # Continue
101
+ end
102
+
103
+ def self.build(**config)
104
+ new(**config) # Config via keyword args
105
+ end
106
+ end
107
+ ```
108
+
109
+ ## Adapter Contract
110
+
111
+ **Request must implement:**
112
+ - `method`, `path`, `domain`, `query`, `headers`, `body`, `params`
113
+ - Aliases: `request_method`, `path_info`, `host`
114
+ - Mutable: `json_body` accessor (for JSON plugin)
115
+
116
+ **Response must have:**
117
+ - `status`, `headers`, `body` (all read/write)
118
+
119
+ **Adapter must:**
120
+ 1. Create Request from server input
121
+ 2. Set `Thread.current[:aris_current_domain]`
122
+ 3. Call `Router.match` → get route
123
+ 4. Call `PipelineRunner.call(request, route, response)`
124
+ 5. Format result for server
125
+ 6. Clean up thread-local in `ensure` block
126
+
127
+ ## File Structure
128
+
129
+ ```
130
+ lib/aris/
131
+ core.rb # Router + trie matching
132
+ pipeline_runner.rb # Plugin/handler execution
133
+ plugins.rb # Plugin registry
134
+ adapters/
135
+ rack/
136
+ adapter.rb # Rack server integration
137
+ request.rb # Rack env → Request
138
+ response.rb # Response object
139
+ mock/ # Testing adapter (no server)
140
+ ...
141
+ plugins/
142
+ csrf.rb # CSRF protection
143
+ json.rb # JSON parser
144
+ bearer_auth.rb # Auth plugins
145
+ ...
146
+ ```
147
+
148
+ ## Performance Notes
149
+
150
+ - Trie lookup: O(path segments)
151
+ - Plugin resolution: compile-time (zero runtime cost)
152
+ - No regex on hot path (only in constraints)
153
+ - Request caching: parse headers/params once
154
+ - Thread-local overhead: ~1-2 allocations/request
155
+
156
+ ## Testing Strategy
157
+
158
+ - **Rack tests** (90%): Full integration via Rack adapter
159
+ - **Mock adapter** (10%): Proves abstraction works
160
+ - **Contract tests**: Verify Request/Response interface
161
+
162
+ All plugins tested once via Rack. Adapter tests are minimal.
163
+
164
+ ## Why This Design?
165
+
166
+ **Problem:** Ruby servers (Rack, Agoo, Iodine) have different APIs
167
+ **Solution:** Adapter pattern + PipelineRunner abstraction
168
+ **Result:** Write plugins once, work everywhere
169
+
170
+ **Tradeoff:** Slight abstraction overhead vs. massive flexibility gain
171
+
172
+ ## Next Steps
173
+
174
+ - **Using Rack?** Use `Aris::Adapters::Rack::Adapter` (works now)
175
+ - **Other server?** Build adapter (see `docs/ADAPTERS.md`)
176
+ - **New plugin?** Just implement `call(request, response)` contract
177
+
178
+ That's it. Simple, fast, composable. 🚀
179
+ ```
180
+
181
+ **Create: `docs/README.md`** (index for docs)
182
+
183
+ ```markdown
184
+ # Documentation
185
+
186
+ ## Quick Links
187
+
188
+ - **[Architecture](ARCHITECTURE.md)** - How Aris Router works (5 min read)
189
+ - **[Adapters](ADAPTERS.md)** - Building custom server adapters (10 min read)
190
+
191
+ ## Philosophy
192
+
193
+ - Zero magic
194
+ - Hash-based config
195
+ - Plugin composition
196
+ - Server agnostic
197
+ - Performance first
198
+
199
+ ## Getting Started
200
+
201
+ ```ruby
202
+ require 'aris'
203
+
204
+ Aris.routes({
205
+ "example.com": {
206
+ use: [:json, :csrf], # Plugin chain
207
+ "/api/users/:id": {
208
+ get: { to: UsersHandler, as: :user }
209
+ }
210
+ }
211
+ })
212
+
213
+ app = Aris::Adapters::Rack::Adapter.new
214
+ ```
215
+
216
+ Done.
217
+ ```
218
+
219
+ Run tests to confirm nothing broke:
220
+ ```bash
221
+ ruby test/run_all_tests.rb
222
+ ```