rasti-web 2.1.0 → 2.1.2
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 +4 -4
- data/.github/workflows/ci.yml +26 -0
- data/README.md +673 -19
- data/lib/rasti/web/request.rb +1 -1
- data/lib/rasti/web/template.rb +1 -1
- data/lib/rasti/web/version.rb +1 -1
- data/rasti-web.gemspec +2 -2
- metadata +11 -11
- data/.travis.yml +0 -20
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1541343e4a1c27d8f1679115b89a127f4821f586d33d42e6b2f8233b888f31c5
|
|
4
|
+
data.tar.gz: 212b77d8585d2838f6c04c88398630ea8262285d951e3d491e7761d09242bc90
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 144031ae6422daf22ced72d427f131f234ff5fa4ee7fbfa61d09082ec2bab7ded8bc06911a33046e19b4277acad99753e63a3a18b666785f3c6f4d73b4e02925
|
|
7
|
+
data.tar.gz: 84f51ba02b4f959a0cad86d11986ee55bc977650fb3641df87a5d86b14aebbc36a63def74a801867af89eb604ac4e49f32a671f671a85b0b3f140cd9324c08ef
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [ '**' ]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [ '**' ]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
|
|
12
|
+
name: Tests
|
|
13
|
+
runs-on: ubuntu-20.04
|
|
14
|
+
strategy:
|
|
15
|
+
matrix:
|
|
16
|
+
ruby-version: ['2.3', '2.4', '2.5', '2.6', '2.7', '3.0', '3.1', '3.2', 'jruby-9.2.9.0']
|
|
17
|
+
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@v3
|
|
20
|
+
- name: Set up Ruby
|
|
21
|
+
uses: ruby/setup-ruby@v1
|
|
22
|
+
with:
|
|
23
|
+
ruby-version: ${{ matrix.ruby-version }}
|
|
24
|
+
bundler-cache: true
|
|
25
|
+
- name: Run tests
|
|
26
|
+
run: bundle exec rake
|
data/README.md
CHANGED
|
@@ -1,11 +1,26 @@
|
|
|
1
1
|
# Rasti::Web
|
|
2
2
|
|
|
3
3
|
[](https://rubygems.org/gems/rasti-web)
|
|
4
|
-
[](https://coveralls.io/
|
|
6
|
-
[](https://codeclimate.com/github/gabynaiman/rasti-web)
|
|
4
|
+
[](https://github.com/gabynaiman/rasti-web/actions/workflows/ci.yml)
|
|
5
|
+
[](https://coveralls.io/github/gabynaiman/rasti-web?branch=master)
|
|
7
6
|
|
|
8
|
-
Web blocks
|
|
7
|
+
**Rasti::Web** is a lightweight, modular web framework built on top of [Rack](https://github.com/rack/rack). It provides essential building blocks for creating robust web applications without imposing too much structure.
|
|
8
|
+
|
|
9
|
+
## Key Features
|
|
10
|
+
|
|
11
|
+
- **Simplicity**: Minimalist and easy-to-understand API
|
|
12
|
+
- **Flexibility**: Direct access to Rack primitives for full control over the request/response cycle
|
|
13
|
+
- **Powerful rendering**: Support for multiple formats (HTML, JSON, JavaScript, CSS, files, etc.)
|
|
14
|
+
- **Advanced routing**: Static, parameterized, optional, and wildcard routes
|
|
15
|
+
- **Template system**: ERB integration for views, layouts, and partials
|
|
16
|
+
- **Error handling**: Hooks system and rescue_from for exception management
|
|
17
|
+
|
|
18
|
+
## Use Cases
|
|
19
|
+
|
|
20
|
+
- Building REST APIs
|
|
21
|
+
- Full web applications
|
|
22
|
+
- Microservices
|
|
23
|
+
- Applications requiring fine-grained control over the request/response cycle
|
|
9
24
|
|
|
10
25
|
## Installation
|
|
11
26
|
|
|
@@ -23,36 +38,206 @@ Or install it yourself as:
|
|
|
23
38
|
|
|
24
39
|
## Usage
|
|
25
40
|
|
|
41
|
+
### Application
|
|
42
|
+
|
|
43
|
+
#### Basic definition
|
|
44
|
+
|
|
45
|
+
```ruby
|
|
46
|
+
class WebApp < Rasti::Web::Application
|
|
47
|
+
|
|
48
|
+
get '/' do |request, response, render|
|
|
49
|
+
render.html 'Welcome'
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
end
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
#### Middleware
|
|
56
|
+
|
|
57
|
+
```ruby
|
|
58
|
+
class WebApp < Rasti::Web::Application
|
|
59
|
+
|
|
60
|
+
use Rack::Session::Cookie, secret: 'my_secret'
|
|
61
|
+
use SomeCustomMiddleware
|
|
62
|
+
|
|
63
|
+
get '/private' do |request, response, render|
|
|
64
|
+
render.html 'Private content'
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
end
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
#### Mounting sub-applications
|
|
71
|
+
|
|
72
|
+
```ruby
|
|
73
|
+
class ApiApp < Rasti::Web::Application
|
|
74
|
+
get '/resource/:id' do |request, response, render|
|
|
75
|
+
render.json id: request.params['id'].to_i
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
class WebApp < Rasti::Web::Application
|
|
80
|
+
map '/api', ApiApp
|
|
81
|
+
|
|
82
|
+
get '/' do |request, response, render|
|
|
83
|
+
render.html 'Home'
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
#### Custom not_found handler
|
|
89
|
+
|
|
90
|
+
```ruby
|
|
91
|
+
class WebApp < Rasti::Web::Application
|
|
92
|
+
|
|
93
|
+
get '/' do |request, response, render|
|
|
94
|
+
render.html 'Home'
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
not_found do |request, response, render|
|
|
98
|
+
render.status 404, 'Page not found'
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
end
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
#### Listing routes
|
|
105
|
+
|
|
106
|
+
```ruby
|
|
107
|
+
WebApp.all_routes
|
|
108
|
+
# => {'GET' => ['/'], 'POST' => ['/users']}
|
|
109
|
+
```
|
|
110
|
+
|
|
26
111
|
### Routing
|
|
27
112
|
|
|
113
|
+
#### HTTP Verbs
|
|
114
|
+
|
|
115
|
+
Rasti::Web supports all standard HTTP verbs:
|
|
116
|
+
|
|
28
117
|
```ruby
|
|
29
|
-
# app.rb
|
|
30
118
|
class WebApp < Rasti::Web::Application
|
|
31
119
|
|
|
32
|
-
|
|
120
|
+
get '/resources' do |request, response, render|
|
|
121
|
+
render.json resources: Resource.all
|
|
122
|
+
end
|
|
33
123
|
|
|
34
|
-
|
|
35
|
-
|
|
124
|
+
post '/resources' do |request, response, render|
|
|
125
|
+
resource = Resource.create(request.params)
|
|
126
|
+
render.json resource, 201
|
|
36
127
|
end
|
|
37
128
|
|
|
38
|
-
put '/
|
|
129
|
+
put '/resources/:id' do |request, response, render|
|
|
130
|
+
resource = Resource.update(request.params[:id], request.params)
|
|
131
|
+
render.json resource
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
patch '/resources/:id' do |request, response, render|
|
|
135
|
+
resource = Resource.patch(request.params[:id], request.params)
|
|
136
|
+
render.json resource
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
delete '/resources/:id' do |request, response, render|
|
|
140
|
+
Resource.delete(request.params[:id])
|
|
141
|
+
render.status 204
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
head '/resources/:id' do |request, response, render|
|
|
145
|
+
render.status Resource.exists?(request.params[:id]) ? 200 : 404
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
options '/resources' do |request, response, render|
|
|
149
|
+
render.status 200, 'Allow' => 'GET, POST, OPTIONS'
|
|
150
|
+
end
|
|
39
151
|
|
|
40
152
|
end
|
|
153
|
+
```
|
|
41
154
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
155
|
+
#### Static routes
|
|
156
|
+
|
|
157
|
+
```ruby
|
|
158
|
+
get '/about' do |request, response, render|
|
|
159
|
+
render.html 'About page'
|
|
160
|
+
end
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
#### Parameterized routes
|
|
164
|
+
|
|
165
|
+
```ruby
|
|
166
|
+
get '/users/:id' do |request, response, render|
|
|
167
|
+
user = User.find(request.params[:id])
|
|
168
|
+
render.json user
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
get '/posts/:post_id/comments/:id' do |request, response, render|
|
|
172
|
+
comment = Comment.find(request.params[:id], post_id: request.params[:post_id])
|
|
173
|
+
render.json comment
|
|
174
|
+
end
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
#### Optional parameters
|
|
178
|
+
|
|
179
|
+
```ruby
|
|
180
|
+
# Matches: /resource, /resource/123, /resource/123/edit
|
|
181
|
+
get '/:resource(/:id(/:action))' do |request, response, render|
|
|
182
|
+
render.json(
|
|
183
|
+
resource: request.params[:resource],
|
|
184
|
+
id: request.params[:id],
|
|
185
|
+
action: request.params[:action]
|
|
186
|
+
)
|
|
187
|
+
end
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
#### Wildcard routes
|
|
191
|
+
|
|
192
|
+
```ruby
|
|
193
|
+
# Wildcard at the beginning: /*/files/download
|
|
194
|
+
get '/*/files/download' do |request, response, render|
|
|
195
|
+
path = request.params[:wildcard] # "users/documents"
|
|
196
|
+
render.file File.join(path, 'file.zip')
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Wildcard in the middle: /files/*/download
|
|
200
|
+
get '/files/*/download' do |request, response, render|
|
|
201
|
+
path = request.params[:wildcard]
|
|
202
|
+
render.file File.join('files', path, 'download.zip')
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Wildcard at the end: /files/*
|
|
206
|
+
get '/files/*' do |request, response, render|
|
|
207
|
+
path = request.params[:wildcard]
|
|
208
|
+
render.file File.join('files', path)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Wildcard with parameters: /files/*/download/:id
|
|
212
|
+
get '/files/*/download/:id' do |request, response, render|
|
|
213
|
+
render.json(
|
|
214
|
+
path: request.params[:wildcard],
|
|
215
|
+
id: request.params[:id]
|
|
216
|
+
)
|
|
217
|
+
end
|
|
45
218
|
```
|
|
46
219
|
|
|
47
220
|
### Controllers
|
|
48
221
|
|
|
222
|
+
#### Basic controller
|
|
223
|
+
|
|
49
224
|
```ruby
|
|
50
225
|
class UsersController < Rasti::Web::Controller
|
|
51
226
|
|
|
227
|
+
def index
|
|
228
|
+
users = User.all
|
|
229
|
+
render.view 'users/list', users: users
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def show
|
|
233
|
+
user = User.find(params[:id])
|
|
234
|
+
render.view 'users/show', user: user
|
|
235
|
+
end
|
|
236
|
+
|
|
52
237
|
def update
|
|
53
238
|
user = User.find(params[:id])
|
|
54
239
|
if user.update_attributes(params[:user])
|
|
55
|
-
render.view 'users/
|
|
240
|
+
render.view 'users/show', user: user
|
|
56
241
|
else
|
|
57
242
|
render.view 'users/edit', user: user
|
|
58
243
|
end
|
|
@@ -61,36 +246,505 @@ class UsersController < Rasti::Web::Controller
|
|
|
61
246
|
end
|
|
62
247
|
```
|
|
63
248
|
|
|
64
|
-
|
|
249
|
+
#### Using controllers in routes
|
|
250
|
+
|
|
251
|
+
```ruby
|
|
252
|
+
class WebApp < Rasti::Web::Application
|
|
253
|
+
|
|
254
|
+
get '/users', UsersController >> :index
|
|
255
|
+
get '/users/:id', UsersController >> :show
|
|
256
|
+
put '/users/:id', UsersController >> :update
|
|
257
|
+
|
|
258
|
+
end
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
#### Hooks
|
|
262
|
+
|
|
263
|
+
##### Before hooks
|
|
65
264
|
|
|
66
265
|
```ruby
|
|
67
266
|
class UsersController < Rasti::Web::Controller
|
|
68
267
|
|
|
268
|
+
# Runs before all actions
|
|
69
269
|
before_action do |action_name|
|
|
270
|
+
authenticate_user!
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# Runs before a specific action
|
|
274
|
+
before_action :update do
|
|
275
|
+
verify_permissions!
|
|
70
276
|
end
|
|
71
277
|
|
|
72
|
-
|
|
278
|
+
def update
|
|
279
|
+
# action code
|
|
73
280
|
end
|
|
74
281
|
|
|
282
|
+
end
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
##### After hooks
|
|
286
|
+
|
|
287
|
+
```ruby
|
|
288
|
+
class UsersController < Rasti::Web::Controller
|
|
289
|
+
|
|
290
|
+
# Runs after all actions
|
|
75
291
|
after_action do |action_name|
|
|
292
|
+
log_action(action_name)
|
|
76
293
|
end
|
|
77
294
|
|
|
78
|
-
|
|
295
|
+
# Runs after a specific action
|
|
296
|
+
after_action :create do
|
|
297
|
+
send_notification
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def create
|
|
301
|
+
# action code
|
|
79
302
|
end
|
|
80
303
|
|
|
81
304
|
end
|
|
82
305
|
```
|
|
83
306
|
|
|
84
|
-
|
|
307
|
+
#### Error handling
|
|
85
308
|
|
|
86
309
|
```ruby
|
|
87
310
|
class UsersController < Rasti::Web::Controller
|
|
88
311
|
|
|
89
|
-
|
|
90
|
-
|
|
312
|
+
# Catch a specific exception
|
|
313
|
+
rescue_from UserNotFound do |ex|
|
|
314
|
+
render.status 404, ex.message
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# Catch by class hierarchy (catches IOError and its subclasses like EOFError)
|
|
318
|
+
rescue_from IOError do |ex|
|
|
319
|
+
render.status 500, ex.message
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
# Multiple rescue_from
|
|
323
|
+
rescue_from ValidationError do |ex|
|
|
324
|
+
render.json({errors: ex.errors}, 422)
|
|
91
325
|
end
|
|
92
326
|
|
|
327
|
+
rescue_from AuthenticationError do |ex|
|
|
328
|
+
render.status 401, 'Unauthorized'
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def show
|
|
332
|
+
user = User.find(params[:id]) # may raise UserNotFound
|
|
333
|
+
render.json user
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
end
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
### Endpoints
|
|
340
|
+
|
|
341
|
+
Endpoints can be blocks or controller actions:
|
|
342
|
+
|
|
343
|
+
```ruby
|
|
344
|
+
# Endpoint as a block
|
|
345
|
+
endpoint = Rasti::Web::Endpoint.new do |request, response, render|
|
|
346
|
+
render.text 'Hello world'
|
|
93
347
|
end
|
|
348
|
+
|
|
349
|
+
# Endpoint from a controller
|
|
350
|
+
endpoint = UsersController.action :index
|
|
351
|
+
|
|
352
|
+
# Endpoints are Rack-compatible
|
|
353
|
+
status, headers, response = endpoint.call(env)
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
### Request
|
|
357
|
+
|
|
358
|
+
#### Accessing parameters
|
|
359
|
+
|
|
360
|
+
```ruby
|
|
361
|
+
get '/search' do |request, response, render|
|
|
362
|
+
# Route parameters
|
|
363
|
+
user_id = request.params[:user_id]
|
|
364
|
+
|
|
365
|
+
# Query string parameters (?q=ruby&page=1)
|
|
366
|
+
query = request.params['q']
|
|
367
|
+
page = request.params[:page]
|
|
368
|
+
|
|
369
|
+
# Parameters are accessible with strings or symbols
|
|
370
|
+
render.json query: query, page: page
|
|
371
|
+
end
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
#### Form parameters
|
|
375
|
+
|
|
376
|
+
```ruby
|
|
377
|
+
post '/users' do |request, response, render|
|
|
378
|
+
# Form parameters (POST/PUT)
|
|
379
|
+
name = request.params[:name]
|
|
380
|
+
email = request.params['email']
|
|
381
|
+
|
|
382
|
+
user = User.create(name: name, email: email)
|
|
383
|
+
render.json user
|
|
384
|
+
end
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
#### JSON body parameters
|
|
388
|
+
|
|
389
|
+
```ruby
|
|
390
|
+
post '/api/users' do |request, response, render|
|
|
391
|
+
# Content-Type: application/json automatically parsed
|
|
392
|
+
if request.json?
|
|
393
|
+
user = User.create(request.params)
|
|
394
|
+
render.json user, 201
|
|
395
|
+
else
|
|
396
|
+
render.status 400, 'Expected JSON content type'
|
|
397
|
+
end
|
|
398
|
+
end
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
### Rendering
|
|
402
|
+
|
|
403
|
+
The `render` object provides multiple methods for different response formats:
|
|
404
|
+
|
|
405
|
+
#### Status codes
|
|
406
|
+
|
|
407
|
+
```ruby
|
|
408
|
+
# Status code only
|
|
409
|
+
render.status 404
|
|
410
|
+
|
|
411
|
+
# Status code and body
|
|
412
|
+
render.status 500, 'Internal server error'
|
|
413
|
+
|
|
414
|
+
# Status code and headers
|
|
415
|
+
render.status 201, 'Content-Type' => 'application/json'
|
|
416
|
+
|
|
417
|
+
# Status code, body and headers
|
|
418
|
+
render.status 403, 'Forbidden', 'Content-Type' => 'text/html'
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
#### Text
|
|
422
|
+
|
|
423
|
+
```ruby
|
|
424
|
+
# Plain text
|
|
425
|
+
render.text 'Hello world'
|
|
426
|
+
|
|
427
|
+
# With status code
|
|
428
|
+
render.text 'Not found', 404
|
|
429
|
+
|
|
430
|
+
# With headers
|
|
431
|
+
render.text 'Encoded text', 'Content-Encoding' => 'gzip'
|
|
432
|
+
|
|
433
|
+
# With status code and headers
|
|
434
|
+
render.text 'Error', 500, 'Content-Encoding' => 'gzip'
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
#### HTML
|
|
438
|
+
|
|
439
|
+
```ruby
|
|
440
|
+
# Basic HTML
|
|
441
|
+
render.html '<h1>Welcome</h1>'
|
|
442
|
+
|
|
443
|
+
# With status code
|
|
444
|
+
render.html '<h1>Error</h1>', 500
|
|
445
|
+
|
|
446
|
+
# With headers
|
|
447
|
+
render.html '<p>Content</p>', 'Content-Encoding' => 'gzip'
|
|
448
|
+
|
|
449
|
+
# With status code and headers
|
|
450
|
+
render.html '<h1>Not found</h1>', 404, 'Custom-Header' => 'value'
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
#### JSON
|
|
454
|
+
|
|
455
|
+
```ruby
|
|
456
|
+
# Object serialized to JSON
|
|
457
|
+
render.json id: 123, name: 'John'
|
|
458
|
+
|
|
459
|
+
# Direct JSON string
|
|
460
|
+
render.json '{"x":1,"y":2}'
|
|
461
|
+
|
|
462
|
+
# With status code
|
|
463
|
+
render.json {error: 'Invalid'}, 422
|
|
464
|
+
|
|
465
|
+
# With headers
|
|
466
|
+
render.json {data: []}, 'Content-Encoding' => 'gzip'
|
|
467
|
+
|
|
468
|
+
# With status code and headers
|
|
469
|
+
render.json {error: 'Invalid'}, 422, 'X-Error-Code' => 'VAL001'
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
#### JavaScript
|
|
473
|
+
|
|
474
|
+
```ruby
|
|
475
|
+
# JavaScript code
|
|
476
|
+
render.js 'alert("hello");'
|
|
477
|
+
|
|
478
|
+
# With status code
|
|
479
|
+
render.js 'console.log("loaded");', 206
|
|
480
|
+
|
|
481
|
+
# With headers
|
|
482
|
+
render.js 'alert("hello");', 'Content-Encoding' => 'gzip'
|
|
483
|
+
|
|
484
|
+
# With status code and headers
|
|
485
|
+
render.js 'alert("hello");', 206, 'Cache-Control' => 'no-cache'
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
#### CSS
|
|
489
|
+
|
|
490
|
+
```ruby
|
|
491
|
+
# CSS code
|
|
492
|
+
render.css 'body{margin:0}'
|
|
493
|
+
|
|
494
|
+
# With status code
|
|
495
|
+
render.css 'body{margin:0}', 206
|
|
496
|
+
|
|
497
|
+
# With headers
|
|
498
|
+
render.css 'body{margin:0}', 'Content-Encoding' => 'gzip'
|
|
499
|
+
|
|
500
|
+
# With status code and headers
|
|
501
|
+
render.css 'body{margin:0}', 206, 'Cache-Control' => 'public'
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
#### File downloads
|
|
505
|
+
|
|
506
|
+
```ruby
|
|
507
|
+
# File download
|
|
508
|
+
render.file '/path/to/file.zip'
|
|
509
|
+
# Content-Type and Content-Disposition are set automatically
|
|
510
|
+
|
|
511
|
+
# With status code
|
|
512
|
+
render.file '/path/to/file.pdf', 206
|
|
513
|
+
|
|
514
|
+
# With custom headers
|
|
515
|
+
render.file '/path/to/file.zip', 'Content-Disposition' => 'attachment; filename=custom.zip'
|
|
516
|
+
|
|
517
|
+
# With status code and headers
|
|
518
|
+
render.file '/path/to/file.zip', 206, 'Content-Disposition' => 'attachment; filename=custom.zip'
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
#### Data with headers
|
|
522
|
+
|
|
523
|
+
```ruby
|
|
524
|
+
# Data without Content-Type
|
|
525
|
+
render.data 'Raw content'
|
|
526
|
+
|
|
527
|
+
# With status code
|
|
528
|
+
render.data 'Content', 206
|
|
529
|
+
|
|
530
|
+
# With headers (useful with Rasti::Web::Headers.for_file)
|
|
531
|
+
render.data file_content, Rasti::Web::Headers.for_file('document.txt')
|
|
532
|
+
|
|
533
|
+
# With status code and headers
|
|
534
|
+
render.data content, 206, Rasti::Web::Headers.for_file('file.txt')
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
### Templates
|
|
538
|
+
|
|
539
|
+
Rasti::Web uses ERB for templates. By default, it looks for templates in `views/`.
|
|
540
|
+
|
|
541
|
+
#### Configuring views directory
|
|
542
|
+
|
|
543
|
+
```ruby
|
|
544
|
+
# Configure template directory (default: 'views')
|
|
545
|
+
Rasti::Web::Template.template_path = '/path/to/templates'
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
#### Partials
|
|
549
|
+
|
|
550
|
+
Partials are templates without layout:
|
|
551
|
+
|
|
552
|
+
```ruby
|
|
553
|
+
# views/users/_user_info.erb
|
|
554
|
+
<h1><%= title %></h1>
|
|
555
|
+
<div><%= text %></div>
|
|
556
|
+
|
|
557
|
+
# In your endpoint/controller:
|
|
558
|
+
render.partial 'users/user_info', title: 'Welcome', text: 'Hello world'
|
|
559
|
+
# => <h1>Welcome</h1><div>Hello world</div>
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
#### Layouts
|
|
563
|
+
|
|
564
|
+
```ruby
|
|
565
|
+
# views/layout.erb
|
|
566
|
+
<html><body><%= yield %></body></html>
|
|
567
|
+
|
|
568
|
+
# Layout with content
|
|
569
|
+
render.layout { 'Page content' }
|
|
570
|
+
# => <html><body>Page content</body></html>
|
|
571
|
+
|
|
572
|
+
# Empty layout
|
|
573
|
+
render.layout
|
|
574
|
+
# => <html><body></body></html>
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
#### Custom layouts
|
|
578
|
+
|
|
579
|
+
```ruby
|
|
580
|
+
# views/custom_layout.erb
|
|
581
|
+
<html><body class="custom"><%= yield %></body></html>
|
|
582
|
+
|
|
583
|
+
# Use custom layout
|
|
584
|
+
render.layout('custom_layout') { 'Page content' }
|
|
585
|
+
# => <html><body class="custom">Page content</body></html>
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
#### Views
|
|
589
|
+
|
|
590
|
+
Views combine a template with a layout:
|
|
591
|
+
|
|
592
|
+
```ruby
|
|
593
|
+
# views/users/profile.erb
|
|
594
|
+
<h1><%= title %></h1>
|
|
595
|
+
<div><%= text %></div>
|
|
596
|
+
|
|
597
|
+
# views/layout.erb
|
|
598
|
+
<html><body><%= yield %></body></html>
|
|
599
|
+
|
|
600
|
+
# With default layout (layout.erb)
|
|
601
|
+
render.view 'users/profile', title: 'Welcome', text: 'Hello world'
|
|
602
|
+
# => <html><body><h1>Welcome</h1><div>Hello world</div></body></html>
|
|
603
|
+
|
|
604
|
+
# With custom layout
|
|
605
|
+
render.view 'users/profile', {title: 'Welcome', text: 'Hello'}, 'custom_layout'
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
#### Context methods and local variables
|
|
609
|
+
|
|
610
|
+
```ruby
|
|
611
|
+
# Module with context methods
|
|
612
|
+
module ViewHelpers
|
|
613
|
+
def format_date(date)
|
|
614
|
+
date.strftime('%Y-%m-%d')
|
|
615
|
+
end
|
|
616
|
+
end
|
|
617
|
+
|
|
618
|
+
# views/posts/show.erb using context method
|
|
619
|
+
<h1><%= format_date(post.created_at) %></h1>
|
|
620
|
+
|
|
621
|
+
# Render with context
|
|
622
|
+
class PostContext
|
|
623
|
+
include ViewHelpers
|
|
624
|
+
end
|
|
625
|
+
|
|
626
|
+
render.partial 'posts/show', PostContext.new, post: post
|
|
627
|
+
|
|
628
|
+
# Or use local variables directly
|
|
629
|
+
render.partial 'posts/show', title: 'My Post', text: 'Content'
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
## Advanced Usage
|
|
633
|
+
|
|
634
|
+
### Complete example
|
|
635
|
+
|
|
636
|
+
```ruby
|
|
637
|
+
# app.rb
|
|
638
|
+
class AuthController < Rasti::Web::Controller
|
|
639
|
+
|
|
640
|
+
def login
|
|
641
|
+
user = User.authenticate(params[:email], params[:password])
|
|
642
|
+
if user
|
|
643
|
+
session[:user_id] = user.id
|
|
644
|
+
render.json user
|
|
645
|
+
else
|
|
646
|
+
render.status 401, 'Invalid credentials'
|
|
647
|
+
end
|
|
648
|
+
rescue AuthenticationError => ex
|
|
649
|
+
render.status 401, ex.message
|
|
650
|
+
end
|
|
651
|
+
|
|
652
|
+
end
|
|
653
|
+
|
|
654
|
+
class UsersController < Rasti::Web::Controller
|
|
655
|
+
|
|
656
|
+
before_action do |action_name|
|
|
657
|
+
authenticate_user!
|
|
658
|
+
end
|
|
659
|
+
|
|
660
|
+
rescue_from UserNotFound do |ex|
|
|
661
|
+
render.status 404, ex.message
|
|
662
|
+
end
|
|
663
|
+
|
|
664
|
+
def index
|
|
665
|
+
users = User.all
|
|
666
|
+
render.view 'users/index', users: users
|
|
667
|
+
end
|
|
668
|
+
|
|
669
|
+
def show
|
|
670
|
+
user = User.find(params[:id])
|
|
671
|
+
render.view 'users/show', user: user
|
|
672
|
+
end
|
|
673
|
+
|
|
674
|
+
private
|
|
675
|
+
|
|
676
|
+
def authenticate_user!
|
|
677
|
+
unless session[:user_id]
|
|
678
|
+
render.status 401, 'Unauthorized'
|
|
679
|
+
end
|
|
680
|
+
end
|
|
681
|
+
|
|
682
|
+
end
|
|
683
|
+
|
|
684
|
+
class WebApp < Rasti::Web::Application
|
|
685
|
+
|
|
686
|
+
use Rack::Session::Cookie, secret: ENV['SESSION_SECRET']
|
|
687
|
+
|
|
688
|
+
# Public routes
|
|
689
|
+
post '/login', AuthController >> :login
|
|
690
|
+
|
|
691
|
+
# Protected routes
|
|
692
|
+
get '/users', UsersController >> :index
|
|
693
|
+
get '/users/:id', UsersController >> :show
|
|
694
|
+
|
|
695
|
+
# API mount
|
|
696
|
+
map '/api', ApiApp
|
|
697
|
+
|
|
698
|
+
# Custom not found
|
|
699
|
+
not_found do |request, response, render|
|
|
700
|
+
render.view '404', path: request.path_info
|
|
701
|
+
end
|
|
702
|
+
|
|
703
|
+
end
|
|
704
|
+
|
|
705
|
+
# config.ru
|
|
706
|
+
require_relative 'app'
|
|
707
|
+
run WebApp
|
|
708
|
+
```
|
|
709
|
+
|
|
710
|
+
## Running Tests
|
|
711
|
+
|
|
712
|
+
Run all tests:
|
|
713
|
+
|
|
714
|
+
```bash
|
|
715
|
+
rake spec
|
|
716
|
+
# or simply
|
|
717
|
+
rake
|
|
718
|
+
```
|
|
719
|
+
|
|
720
|
+
Run tests from a specific directory:
|
|
721
|
+
|
|
722
|
+
```bash
|
|
723
|
+
DIR=spec/web rake spec
|
|
724
|
+
```
|
|
725
|
+
|
|
726
|
+
Run a specific test file:
|
|
727
|
+
|
|
728
|
+
```bash
|
|
729
|
+
TEST=spec/endpoint_spec.rb rake spec
|
|
730
|
+
```
|
|
731
|
+
|
|
732
|
+
Run tests from a specific line in a file:
|
|
733
|
+
|
|
734
|
+
```bash
|
|
735
|
+
TEST=spec/endpoint_spec.rb:45 rake spec
|
|
736
|
+
```
|
|
737
|
+
|
|
738
|
+
Run tests matching a name pattern:
|
|
739
|
+
|
|
740
|
+
```bash
|
|
741
|
+
NAME=render rake spec
|
|
742
|
+
```
|
|
743
|
+
|
|
744
|
+
You can combine options:
|
|
745
|
+
|
|
746
|
+
```bash
|
|
747
|
+
DIR=spec/web NAME=controller rake spec
|
|
94
748
|
```
|
|
95
749
|
|
|
96
750
|
## Contributing
|
data/lib/rasti/web/request.rb
CHANGED
|
@@ -7,7 +7,7 @@ module Rasti
|
|
|
7
7
|
hash.update self.GET
|
|
8
8
|
hash.update self.POST
|
|
9
9
|
hash.update env[ROUTE_PARAMS] if env.key? ROUTE_PARAMS
|
|
10
|
-
hash.update JSON.parse(body_text) if json? && body_text
|
|
10
|
+
hash.update JSON.parse(body_text) if json? && body_text && !body_text.empty?
|
|
11
11
|
end
|
|
12
12
|
end
|
|
13
13
|
|
data/lib/rasti/web/template.rb
CHANGED
|
@@ -4,7 +4,7 @@ module Rasti
|
|
|
4
4
|
|
|
5
5
|
def self.render(template, context=nil, locals={}, &block)
|
|
6
6
|
files = Web.template_engines.map { |e| File.join Web.views_path, "#{template}.#{e}" }
|
|
7
|
-
template_file = files.detect { |f| File.
|
|
7
|
+
template_file = files.detect { |f| File.exist? f }
|
|
8
8
|
|
|
9
9
|
raise "Missing template #{template} [#{files.join(', ')}]" unless template_file
|
|
10
10
|
|
data/lib/rasti/web/version.rb
CHANGED
data/rasti-web.gemspec
CHANGED
|
@@ -26,12 +26,12 @@ Gem::Specification.new do |spec|
|
|
|
26
26
|
spec.add_dependency 'content-type', '~> 0.0'
|
|
27
27
|
spec.add_dependency 'class_ancestry_sort', '~> 0.1'
|
|
28
28
|
|
|
29
|
-
spec.add_development_dependency 'rake', '~>
|
|
29
|
+
spec.add_development_dependency 'rake', '~> 13.0'
|
|
30
30
|
spec.add_development_dependency 'minitest', '~> 5.0', '< 5.11'
|
|
31
31
|
spec.add_development_dependency 'minitest-colorin', '~> 0.1'
|
|
32
32
|
spec.add_development_dependency 'minitest-line', '~> 0.6'
|
|
33
33
|
spec.add_development_dependency 'simplecov', '~> 0.12'
|
|
34
34
|
spec.add_development_dependency 'coveralls', '~> 0.8'
|
|
35
|
-
spec.add_development_dependency 'pry-nav', '~> 0
|
|
35
|
+
spec.add_development_dependency 'pry-nav', '~> 1.0'
|
|
36
36
|
spec.add_development_dependency 'rack-test', '~> 0.6'
|
|
37
37
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rasti-web
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.1.
|
|
4
|
+
version: 2.1.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Gabriel Naiman
|
|
8
|
-
autorequire:
|
|
8
|
+
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2026-01-30 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rack
|
|
@@ -114,14 +114,14 @@ dependencies:
|
|
|
114
114
|
requirements:
|
|
115
115
|
- - "~>"
|
|
116
116
|
- !ruby/object:Gem::Version
|
|
117
|
-
version: '
|
|
117
|
+
version: '13.0'
|
|
118
118
|
type: :development
|
|
119
119
|
prerelease: false
|
|
120
120
|
version_requirements: !ruby/object:Gem::Requirement
|
|
121
121
|
requirements:
|
|
122
122
|
- - "~>"
|
|
123
123
|
- !ruby/object:Gem::Version
|
|
124
|
-
version: '
|
|
124
|
+
version: '13.0'
|
|
125
125
|
- !ruby/object:Gem::Dependency
|
|
126
126
|
name: minitest
|
|
127
127
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -204,14 +204,14 @@ dependencies:
|
|
|
204
204
|
requirements:
|
|
205
205
|
- - "~>"
|
|
206
206
|
- !ruby/object:Gem::Version
|
|
207
|
-
version: '0
|
|
207
|
+
version: '1.0'
|
|
208
208
|
type: :development
|
|
209
209
|
prerelease: false
|
|
210
210
|
version_requirements: !ruby/object:Gem::Requirement
|
|
211
211
|
requirements:
|
|
212
212
|
- - "~>"
|
|
213
213
|
- !ruby/object:Gem::Version
|
|
214
|
-
version: '0
|
|
214
|
+
version: '1.0'
|
|
215
215
|
- !ruby/object:Gem::Dependency
|
|
216
216
|
name: rack-test
|
|
217
217
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -234,10 +234,10 @@ extensions: []
|
|
|
234
234
|
extra_rdoc_files: []
|
|
235
235
|
files:
|
|
236
236
|
- ".coveralls.yml"
|
|
237
|
+
- ".github/workflows/ci.yml"
|
|
237
238
|
- ".gitignore"
|
|
238
239
|
- ".ruby-gemset"
|
|
239
240
|
- ".ruby-version"
|
|
240
|
-
- ".travis.yml"
|
|
241
241
|
- Gemfile
|
|
242
242
|
- LICENSE.txt
|
|
243
243
|
- README.md
|
|
@@ -277,7 +277,7 @@ homepage: https://github.com/gabynaiman/rasti-web
|
|
|
277
277
|
licenses:
|
|
278
278
|
- MIT
|
|
279
279
|
metadata: {}
|
|
280
|
-
post_install_message:
|
|
280
|
+
post_install_message:
|
|
281
281
|
rdoc_options: []
|
|
282
282
|
require_paths:
|
|
283
283
|
- lib
|
|
@@ -292,8 +292,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
292
292
|
- !ruby/object:Gem::Version
|
|
293
293
|
version: '0'
|
|
294
294
|
requirements: []
|
|
295
|
-
rubygems_version: 3.0.
|
|
296
|
-
signing_key:
|
|
295
|
+
rubygems_version: 3.0.9
|
|
296
|
+
signing_key:
|
|
297
297
|
specification_version: 4
|
|
298
298
|
summary: Web blocks to build robust applications
|
|
299
299
|
test_files:
|
data/.travis.yml
DELETED