aris 1.3.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.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +342 -0
- data/lib/aris/adapters/base.rb +25 -0
- data/lib/aris/adapters/joys_integration.rb +94 -0
- data/lib/aris/adapters/mock/adapter.rb +141 -0
- data/lib/aris/adapters/mock/request.rb +81 -0
- data/lib/aris/adapters/mock/response.rb +17 -0
- data/lib/aris/adapters/rack/adapter.rb +117 -0
- data/lib/aris/adapters/rack/request.rb +66 -0
- data/lib/aris/adapters/rack/response.rb +16 -0
- data/lib/aris/core.rb +931 -0
- data/lib/aris/discovery.rb +312 -0
- data/lib/aris/locale_injector.rb +39 -0
- data/lib/aris/pipeline_runner.rb +100 -0
- data/lib/aris/plugins/api_key_auth.rb +61 -0
- data/lib/aris/plugins/basic_auth.rb +68 -0
- data/lib/aris/plugins/bearer_auth.rb +64 -0
- data/lib/aris/plugins/cache.rb +120 -0
- data/lib/aris/plugins/compression.rb +96 -0
- data/lib/aris/plugins/cookies.rb +46 -0
- data/lib/aris/plugins/cors.rb +81 -0
- data/lib/aris/plugins/csrf.rb +48 -0
- data/lib/aris/plugins/etag.rb +90 -0
- data/lib/aris/plugins/flash.rb +124 -0
- data/lib/aris/plugins/form_parser.rb +46 -0
- data/lib/aris/plugins/health_check.rb +62 -0
- data/lib/aris/plugins/json.rb +32 -0
- data/lib/aris/plugins/multipart.rb +160 -0
- data/lib/aris/plugins/rate_limiter.rb +60 -0
- data/lib/aris/plugins/request_id.rb +38 -0
- data/lib/aris/plugins/request_logger.rb +43 -0
- data/lib/aris/plugins/security_headers.rb +99 -0
- data/lib/aris/plugins/session.rb +175 -0
- data/lib/aris/plugins.rb +23 -0
- data/lib/aris/response_helpers.rb +156 -0
- data/lib/aris/route_helpers.rb +141 -0
- data/lib/aris/utils/redirects.rb +44 -0
- data/lib/aris/utils/sitemap.rb +84 -0
- data/lib/aris/version.rb +3 -0
- data/lib/aris.rb +35 -0
- metadata +151 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 939f838547c98d96452cf200c2b5ae4d9cbdfaf17f6811415f2f341e7cf96a32
|
|
4
|
+
data.tar.gz: c7c25cd371a1413068d0b490b7d92fa29809c077fde4c04f75a8619427de600f
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 4f4b0181b8df3cf3f7b421cbfef4bf9082324e833f31c688d638c41808845b81842448243da9e91279d0781f690edb7b76fde2c6859d060df58c4e3bf906e9fb
|
|
7
|
+
data.tar.gz: 756840ba2f35be79777e3aeeb01be6a4ca560119d85d5e5ba11a725cc7f87ab8e2afa73a0fbd22c5ac97f4dcccfde31bf1c1ff621cddeea62c4864c3df15bdc9
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Steven Garcia
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
<img width="1696" height="704" alt="aris" src="https://github.com/user-attachments/assets/3f0a67d1-c4c0-4357-b0e7-8d8ad62563ba" />
|
|
2
|
+
|
|
3
|
+
# Aris
|
|
4
|
+
|
|
5
|
+
**A fast, framework-agnostic router for Ruby.**
|
|
6
|
+
|
|
7
|
+
Aris treats routes as plain data structures instead of code. This design choice makes routing predictable, testable, and roughly 3× faster than comparable frameworks. It works anywhere—Rack apps, CLI tools, background jobs, or custom servers.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
gem 'aris'
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Quick Start
|
|
16
|
+
|
|
17
|
+
Routes are just hashes. Define them once at boot, and Aris compiles them into an optimized lookup structure.
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
Aris.routes({
|
|
21
|
+
"api.example.com": {
|
|
22
|
+
"/users/:id": {
|
|
23
|
+
get: { to: UserHandler, as: :user }
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Match incoming requests:
|
|
30
|
+
|
|
31
|
+
```ruby
|
|
32
|
+
result = Aris::Router.match(
|
|
33
|
+
domain: "api.example.com",
|
|
34
|
+
method: :get,
|
|
35
|
+
path: "/users/123"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
result[:handler] # => UserHandler
|
|
39
|
+
result[:params] # => { id: "123" }
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Generate paths from named routes:
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
Aris.path("api.example.com", :user, id: 123)
|
|
46
|
+
# => "/users/123"
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
That's the core. Everything else builds on these three concepts: define, match, generate.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Core Features
|
|
54
|
+
|
|
55
|
+
### Multiple HTTP Methods
|
|
56
|
+
|
|
57
|
+
```ruby
|
|
58
|
+
Aris.routes({
|
|
59
|
+
"example.com": {
|
|
60
|
+
"/posts/:id": {
|
|
61
|
+
get: { to: PostShowHandler },
|
|
62
|
+
put: { to: PostUpdateHandler },
|
|
63
|
+
delete: { to: PostDeleteHandler }
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Wildcards for Catch-All Routes
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
Aris.routes({
|
|
73
|
+
"example.com": {
|
|
74
|
+
"/files/*path": { get: { to: FileHandler } }
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
result = Aris::Router.match(
|
|
79
|
+
domain: "example.com",
|
|
80
|
+
method: :get,
|
|
81
|
+
path: "/files/docs/2024/report.pdf"
|
|
82
|
+
)
|
|
83
|
+
result[:params] # => { path: "docs/2024/report.pdf" }
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Parameter Constraints
|
|
87
|
+
|
|
88
|
+
Validate parameters at the routing level to fail fast on invalid input.
|
|
89
|
+
|
|
90
|
+
```ruby
|
|
91
|
+
Aris.routes({
|
|
92
|
+
"example.com": {
|
|
93
|
+
"/users/:id": {
|
|
94
|
+
get: {
|
|
95
|
+
to: UserHandler,
|
|
96
|
+
constraints: { id: /\A\d{1,8}\z/ }
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
})
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Multi-Domain Routing
|
|
104
|
+
|
|
105
|
+
Define different routing trees per domain, with wildcard fallback support.
|
|
106
|
+
|
|
107
|
+
```ruby
|
|
108
|
+
Aris.routes({
|
|
109
|
+
"example.com": {
|
|
110
|
+
"/": { get: { to: HomeHandler } }
|
|
111
|
+
},
|
|
112
|
+
"admin.example.com": {
|
|
113
|
+
"/": { get: { to: AdminDashboardHandler } }
|
|
114
|
+
},
|
|
115
|
+
"*": {
|
|
116
|
+
"/health": { get: { to: HealthHandler } }
|
|
117
|
+
}
|
|
118
|
+
})
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Composable Plugins
|
|
122
|
+
|
|
123
|
+
Plugins execute between routing and handler dispatch. They're just callables that can inspect, modify, or halt the request.
|
|
124
|
+
|
|
125
|
+
```ruby
|
|
126
|
+
class Auth
|
|
127
|
+
def self.call(request, response)
|
|
128
|
+
unless authorized?(request)
|
|
129
|
+
response.status = 401
|
|
130
|
+
response.body = ['Unauthorized']
|
|
131
|
+
return response # Halts processing
|
|
132
|
+
end
|
|
133
|
+
nil # Continue to next plugin or handler
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
Aris.routes({
|
|
138
|
+
"api.example.com": {
|
|
139
|
+
use: [CorsHeaders, Auth], # Applied to all routes
|
|
140
|
+
"/users": { get: { to: UsersHandler } }
|
|
141
|
+
}
|
|
142
|
+
})
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Plugins inherit down the routing tree and can be overridden at any level.
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
### File-Based Route Discovery
|
|
150
|
+
|
|
151
|
+
Define routes by creating files instead of writing hash definitions. The directory structure maps directly to your routes.
|
|
152
|
+
|
|
153
|
+
```ruby
|
|
154
|
+
# Directory structure:
|
|
155
|
+
# app/routes/
|
|
156
|
+
# example.com/
|
|
157
|
+
# users/
|
|
158
|
+
# get.rb # GET /users
|
|
159
|
+
# _id/
|
|
160
|
+
# get.rb # GET /users/:id
|
|
161
|
+
# _/
|
|
162
|
+
# health/
|
|
163
|
+
# get.rb # GET /health on any domain
|
|
164
|
+
|
|
165
|
+
# app/routes/example.com/users/_id/get.rb
|
|
166
|
+
class Handler
|
|
167
|
+
def self.call(request, params)
|
|
168
|
+
{ id: params[:id], name: "User #{params[:id]}" }
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Boot configuration
|
|
173
|
+
Aris.discover_and_define('app/routes')
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Convention: `domain/path/segments/_param/method.rb`. Use `_` for wildcard domains and parameter names. Each file defines a `Handler` class with a `.call` method.
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## Use Cases
|
|
182
|
+
|
|
183
|
+
### Rack Applications
|
|
184
|
+
|
|
185
|
+
```ruby
|
|
186
|
+
# config.ru
|
|
187
|
+
Aris.routes({
|
|
188
|
+
"example.com": {
|
|
189
|
+
"/": { get: { to: HomeHandler } }
|
|
190
|
+
}
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
run Aris::Adapters::RackApp.new
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### CLI Tools
|
|
197
|
+
|
|
198
|
+
```ruby
|
|
199
|
+
# Route commands to handlers
|
|
200
|
+
result = Aris::Router.match(
|
|
201
|
+
domain: "cli.internal",
|
|
202
|
+
method: :get,
|
|
203
|
+
path: "/users/#{ARGV[0]}/report"
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
result[:handler].call(result[:params]) if result
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### Background Jobs
|
|
210
|
+
|
|
211
|
+
```ruby
|
|
212
|
+
class WebhookRouter
|
|
213
|
+
def self.route(event_type, payload)
|
|
214
|
+
result = Aris::Router.match(
|
|
215
|
+
domain: "webhooks.internal",
|
|
216
|
+
method: :post,
|
|
217
|
+
path: "/events/#{event_type}"
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
result[:handler].call(payload) if result
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### Custom Servers
|
|
226
|
+
|
|
227
|
+
```ruby
|
|
228
|
+
class ServerAdapter
|
|
229
|
+
def handle(native_request)
|
|
230
|
+
result = Aris::Router.match(
|
|
231
|
+
domain: native_request.host,
|
|
232
|
+
method: native_request.method,
|
|
233
|
+
path: native_request.path
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
return [404, {}, ['Not Found']] unless result
|
|
237
|
+
|
|
238
|
+
result[:handler].call(native_request, result[:params])
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
## Error Handling
|
|
246
|
+
|
|
247
|
+
Define custom handlers for 404s and 500s once, and they'll be used everywhere.
|
|
248
|
+
|
|
249
|
+
```ruby
|
|
250
|
+
Aris.default(
|
|
251
|
+
not_found: ->(req, params) {
|
|
252
|
+
[404, {}, ['{"error": "Not found"}']]
|
|
253
|
+
},
|
|
254
|
+
error: ->(req, exception) {
|
|
255
|
+
ErrorTracker.log(exception)
|
|
256
|
+
[500, {}, ['{"error": "Internal error"}']]
|
|
257
|
+
}
|
|
258
|
+
)
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
Trigger these handlers from your code:
|
|
262
|
+
|
|
263
|
+
```ruby
|
|
264
|
+
class UserHandler
|
|
265
|
+
def self.call(request, params)
|
|
266
|
+
user = User.find(params[:id])
|
|
267
|
+
return Aris.not_found(request) unless user
|
|
268
|
+
|
|
269
|
+
user.to_json
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## Performance
|
|
279
|
+
|
|
280
|
+
Aris compiles routes into a Trie structure at boot time, enabling constant-time lookups regardless of route count. Benchmarks against Roda show 2.5-3.8× faster routing across all scenarios.
|
|
281
|
+
|
|
282
|
+
```
|
|
283
|
+
Root path: 3.1× faster (570ns vs 1.75μs)
|
|
284
|
+
Single parameter: 2.8× faster (998ns vs 2.78μs)
|
|
285
|
+
Two parameters: 3.0× faster (1.31μs vs 3.89μs)
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
Route matching is O(k) where k is path depth, not route count. Adding 1,000 routes has zero impact on lookup speed—only compilation time increases (3ms for 1,000 routes).
|
|
289
|
+
|
|
290
|
+
**Why is it fast?**
|
|
291
|
+
|
|
292
|
+
Routes are data structures, not code. Matching a request is a tree traversal with no method dispatch, no block evaluation, and no runtime metaprogramming. The implementation caches aggressively and minimizes object allocation in the hot path.
|
|
293
|
+
|
|
294
|
+
---
|
|
295
|
+
|
|
296
|
+
## Documentation
|
|
297
|
+
|
|
298
|
+
**[Full Usage Guide](docs/USAGE.md)** - Complete API reference with detailed examples
|
|
299
|
+
**[Adapters Guide](docs/ADAPTERS.md)** - Build on top of aris agnostic interface
|
|
300
|
+
**[Architecture](docs/ARCHITECTURE.md)** - Learn all about the design decisions behind Aris
|
|
301
|
+
**[Plugin Development](docs/PLUGIN_DEVELOPMENT.md)** - How to build custom middleware
|
|
302
|
+
**[Performance Details](docs/PERFORMANCE.md)** - Benchmarks, profiling, and optimization
|
|
303
|
+
|
|
304
|
+
---
|
|
305
|
+
|
|
306
|
+
## FAQ
|
|
307
|
+
|
|
308
|
+
**Can I use this with Rails or Sinatra?**
|
|
309
|
+
Yes, but it's better suited for new projects or specific use cases like API-only apps, CLI routing, or background job dispatch. For existing apps, Aris works well alongside framework routing rather than replacing it.
|
|
310
|
+
|
|
311
|
+
**How does this compare to Rack middleware?**
|
|
312
|
+
Aris's plugins run after route matching, so they have access to route parameters and metadata. Rack middleware runs before routing and only sees raw HTTP data. Use both together—Rack for HTTP concerns, Aris plugins for application logic.
|
|
313
|
+
|
|
314
|
+
**What if I need to add routes at runtime?**
|
|
315
|
+
Calling `Aris.routes` performs a full recompilation (milliseconds for thousands of routes) and isn't thread-safe. Most apps define routes once at boot. For truly dynamic routing, use parameterized routes with dynamic handler logic instead.
|
|
316
|
+
|
|
317
|
+
**Does it support regex patterns in routes?**
|
|
318
|
+
No, because that would break the Trie optimization. Use wildcard parameters (`*path`) for globbing and constraints for validation. This keeps routing fast while still enforcing requirements.
|
|
319
|
+
|
|
320
|
+
**How do I handle API versioning?**
|
|
321
|
+
Use domain-based (`v1.api.example.com`) or path-based (`/api/v1/users`) versioning. Both work identically—domain-based gives cleaner separation, path-based keeps everything on one domain.
|
|
322
|
+
|
|
323
|
+
---
|
|
324
|
+
|
|
325
|
+
## Contributing
|
|
326
|
+
|
|
327
|
+
Pull requests welcome. The codebase prioritizes simplicity and performance—every feature should justify its complexity and impact on routing speed.
|
|
328
|
+
|
|
329
|
+
Run tests: `ruby test/run_all_tests.rb`
|
|
330
|
+
Run benchmarks: `ruby test/bench/vs_roda.rb`
|
|
331
|
+
|
|
332
|
+
---
|
|
333
|
+
|
|
334
|
+
## License
|
|
335
|
+
|
|
336
|
+
MIT
|
|
337
|
+
|
|
338
|
+
---
|
|
339
|
+
|
|
340
|
+
## Acknowledgments
|
|
341
|
+
|
|
342
|
+
Inspired by the pragmatic design of Roda, the performance focus of Aaron Patterson's work, and the elegance of Rack. Thanks to the Ruby community for constantly pushing what's possible.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# lib/aris/adapters/base.rb
|
|
2
|
+
module Aris
|
|
3
|
+
module Adapters
|
|
4
|
+
class Base
|
|
5
|
+
def handle_sitemap(request)
|
|
6
|
+
return nil unless defined?(Aris::Utils::Sitemap) && request.path == '/sitemap.xml'
|
|
7
|
+
|
|
8
|
+
xml = Aris::Utils::Sitemap.generate(
|
|
9
|
+
base_url: "#{request.scheme}://#{request.host}",
|
|
10
|
+
domain: request.host
|
|
11
|
+
)
|
|
12
|
+
[200, {'content-type' => 'application/xml'}, [xml]]
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def handle_redirect(request)
|
|
16
|
+
return nil unless defined?(Aris::Utils::Redirects)
|
|
17
|
+
|
|
18
|
+
redirect = Aris::Utils::Redirects.find(request.path)
|
|
19
|
+
return nil unless redirect
|
|
20
|
+
|
|
21
|
+
[redirect[:status], {'Location' => redirect[:to]}, []]
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
require_relative 'base'
|
|
2
|
+
|
|
3
|
+
class JoysPlugin
|
|
4
|
+
def initialize(app)
|
|
5
|
+
@app = app
|
|
6
|
+
end
|
|
7
|
+
def call(env)
|
|
8
|
+
request = Rack::Request.new(env)
|
|
9
|
+
Thread.current[:joys_request] = request
|
|
10
|
+
Thread.current[:joys_params] = {}
|
|
11
|
+
|
|
12
|
+
begin
|
|
13
|
+
@app.call(env)
|
|
14
|
+
ensure
|
|
15
|
+
# Comprehensive cleanup
|
|
16
|
+
Thread.current[:joys_request] = nil
|
|
17
|
+
Thread.current[:joys_params] = nil
|
|
18
|
+
Thread.current[:joys_locals] = nil
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
module Joys
|
|
24
|
+
module Adapters
|
|
25
|
+
class Aris < ::Aris::Adapters::Base
|
|
26
|
+
def integrate!
|
|
27
|
+
inject_helpers!
|
|
28
|
+
[::Aris::Adapters::Mock::Response, ::Aris::Adapters::Rack::Response].each do |klass|
|
|
29
|
+
klass.include(Renderer)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def inject_helpers!
|
|
34
|
+
Joys::Render::Helpers.module_eval do
|
|
35
|
+
def request; Thread.current[:joys_request]; end
|
|
36
|
+
def params; Thread.current[:joys_params]; end
|
|
37
|
+
def _(content); raw(content); end
|
|
38
|
+
def path(route_name, **params)
|
|
39
|
+
::Aris.path(route_name, **params) # Call on ::Aris module, not the adapter
|
|
40
|
+
end
|
|
41
|
+
def make(name, *args, **kwargs, &block)
|
|
42
|
+
Joys.define :comp, name, *args, **kwargs, &block
|
|
43
|
+
end
|
|
44
|
+
def load_css_file(*names)
|
|
45
|
+
names.each do |name|
|
|
46
|
+
file_path = File.join(Joys::Config.css_parts, "#{name}.css")
|
|
47
|
+
if File.exist?(file_path)
|
|
48
|
+
raw File.read(file_path)
|
|
49
|
+
else
|
|
50
|
+
raise "CSS file not found: #{file_path}"
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
nil
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
module Renderer
|
|
60
|
+
def render(path, **locals)
|
|
61
|
+
Thread.current[:joys_params] = locals[:params] || {}
|
|
62
|
+
file = File.join(Joys::Config.pages, "#{path}.rb")
|
|
63
|
+
raise "Template not found: #{file}" unless File.exist?(file)
|
|
64
|
+
|
|
65
|
+
renderer = Object.new
|
|
66
|
+
renderer.extend(Joys::Render::Helpers)
|
|
67
|
+
renderer.extend(Joys::Tags)
|
|
68
|
+
|
|
69
|
+
# Set the page name for style compilation
|
|
70
|
+
page_name = "page_#{path.gsub('/', '_')}"
|
|
71
|
+
|
|
72
|
+
# Initialize ALL Joys state on the renderer instance
|
|
73
|
+
renderer.instance_variable_set(:@bf, String.new(capacity: 16384))
|
|
74
|
+
renderer.instance_variable_set(:@slots, {})
|
|
75
|
+
renderer.instance_variable_set(:@styles, [])
|
|
76
|
+
renderer.instance_variable_set(:@style_base_css, [])
|
|
77
|
+
renderer.instance_variable_set(:@style_media_queries, {})
|
|
78
|
+
renderer.instance_variable_set(:@used_components, Set.new)
|
|
79
|
+
renderer.instance_variable_set(:@current_page, page_name) # ADD THIS!
|
|
80
|
+
|
|
81
|
+
# Inject locals as methods
|
|
82
|
+
locals.each { |k, v| renderer.define_singleton_method(k) { v } }
|
|
83
|
+
|
|
84
|
+
# Evaluate the page file
|
|
85
|
+
renderer.instance_eval(File.read(file), file)
|
|
86
|
+
|
|
87
|
+
# Set the HTML on the Aris response
|
|
88
|
+
self.html(renderer.instance_variable_get(:@bf))
|
|
89
|
+
self
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# lib/aris/adapters/mock/adapter.rb
|
|
2
|
+
require_relative '../../core'
|
|
3
|
+
require_relative '../../pipeline_runner'
|
|
4
|
+
require_relative 'request'
|
|
5
|
+
require_relative 'response'
|
|
6
|
+
require 'json'
|
|
7
|
+
|
|
8
|
+
module Aris
|
|
9
|
+
module Adapters
|
|
10
|
+
module Mock
|
|
11
|
+
class Adapter
|
|
12
|
+
def call(method:, path:, domain:, headers: {}, body: '', query: '')
|
|
13
|
+
# Parse cookies from headers before creating request
|
|
14
|
+
cookies = parse_cookies(headers)
|
|
15
|
+
|
|
16
|
+
request = Request.new(
|
|
17
|
+
method: method,
|
|
18
|
+
path: path,
|
|
19
|
+
domain: domain,
|
|
20
|
+
headers: headers,
|
|
21
|
+
body: body,
|
|
22
|
+
query: query,
|
|
23
|
+
cookies: cookies # Add cookies
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
request_domain = request.host
|
|
27
|
+
Thread.current[:aris_current_domain] = request_domain
|
|
28
|
+
|
|
29
|
+
begin
|
|
30
|
+
redirect_response, normalized_path = Aris.handle_trailing_slash(path)
|
|
31
|
+
if redirect_response
|
|
32
|
+
return {
|
|
33
|
+
status: redirect_response[0],
|
|
34
|
+
headers: redirect_response[1],
|
|
35
|
+
body: redirect_response[2]
|
|
36
|
+
}
|
|
37
|
+
end
|
|
38
|
+
domain_config = Aris::Router.domain_config(domain)
|
|
39
|
+
if domain_config && domain_config[:locales] && domain_config[:locales].any? && domain_config[:root_locale_redirect] != false
|
|
40
|
+
if path == '/' || path.empty?
|
|
41
|
+
default_locale = domain_config[:default_locale] || domain_config[:locales].first
|
|
42
|
+
return { status: 302, headers: {'Location' => "/#{default_locale}/"}, body: [] }
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
if defined?(Aris::Utils::Sitemap) && path == '/sitemap.xml'
|
|
47
|
+
xml = Aris::Utils::Sitemap.generate(base_url: "https://#{domain}", domain: domain)
|
|
48
|
+
return { status: 200, headers: {'content-type' => 'application/xml'}, body: [xml] }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Check redirects
|
|
52
|
+
if defined?(Aris::Utils::Redirects)
|
|
53
|
+
redirect = Aris::Utils::Redirects.find(path)
|
|
54
|
+
if redirect
|
|
55
|
+
return { status: redirect[:status], headers: {'Location' => redirect[:to]}, body: [] }
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
route = Aris::Router.match(
|
|
60
|
+
domain: request_domain,
|
|
61
|
+
method: request.request_method.downcase.to_sym,
|
|
62
|
+
path: request.path_info
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
unless route
|
|
66
|
+
return format_response(Aris.not_found(request))
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Inject locale methods into request if locale is present
|
|
70
|
+
if route[:locale]
|
|
71
|
+
request.define_singleton_method(:locale) { route[:locale] }
|
|
72
|
+
if domain_config
|
|
73
|
+
request.define_singleton_method(:available_locales) { domain_config[:locales] }
|
|
74
|
+
request.define_singleton_method(:default_locale) { domain_config[:default_locale] }
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
response = Response.new
|
|
79
|
+
request.instance_variable_set(:@response, response)
|
|
80
|
+
request.define_singleton_method(:response) { @response }
|
|
81
|
+
# Execute plugins and handler via PipelineRunner
|
|
82
|
+
result = PipelineRunner.call(request: request, route: route, response: response)
|
|
83
|
+
|
|
84
|
+
# Format the result
|
|
85
|
+
format_response(result, response)
|
|
86
|
+
|
|
87
|
+
rescue Aris::Router::RouteNotFoundError
|
|
88
|
+
return format_response(Aris.not_found(request))
|
|
89
|
+
rescue Exception => e
|
|
90
|
+
return format_response(Aris.error(request, e))
|
|
91
|
+
ensure
|
|
92
|
+
Thread.current[:aris_current_domain] = nil
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def subdomain
|
|
97
|
+
@subdomain || extract_subdomain_from_domain
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
private
|
|
101
|
+
|
|
102
|
+
def extract_subdomain_from_domain
|
|
103
|
+
return nil unless @env[:subdomain] || @env['SUBDOMAIN']
|
|
104
|
+
|
|
105
|
+
@env[:subdomain] || @env['SUBDOMAIN']
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def parse_cookies(headers)
|
|
109
|
+
return {} unless headers && headers['Cookie']
|
|
110
|
+
|
|
111
|
+
cookies = {}
|
|
112
|
+
headers['Cookie'].split(';').each do |cookie|
|
|
113
|
+
name, value = cookie.strip.split('=', 2)
|
|
114
|
+
cookies[name] = value if name && value
|
|
115
|
+
end
|
|
116
|
+
cookies
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def format_response(result, response = nil)
|
|
120
|
+
case result
|
|
121
|
+
when Response
|
|
122
|
+
{ status: result.status, headers: result.headers, body: result.body }
|
|
123
|
+
when Array
|
|
124
|
+
# Don't unwrap - keep body as-is
|
|
125
|
+
{ status: result[0], headers: result[1], body: result[2] }
|
|
126
|
+
when Hash
|
|
127
|
+
headers = response ? response.headers.merge({'content-type' => 'application/json'}) : {'content-type' => 'application/json'}
|
|
128
|
+
{ status: 200, headers: headers, body: [result.to_json] }
|
|
129
|
+
else
|
|
130
|
+
if result.respond_to?(:status) && result.respond_to?(:headers) && result.respond_to?(:body)
|
|
131
|
+
{ status: result.status, headers: result.headers, body: result.body }
|
|
132
|
+
else
|
|
133
|
+
headers = response ? response.headers.merge({'content-type' => 'text/plain'}) : {'content-type' => 'text/plain'}
|
|
134
|
+
{ status: 200, headers: headers, body: [result.to_s] }
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|