serrano-vk 0.1.2 → 0.1.3
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/EXAMPLES.md +76 -42
- data/README.md +44 -29
- data/lib/serrano/cli/generate.rb +0 -1
- data/lib/serrano/cli/templates/entity.rb.tt +1 -3
- data/lib/serrano/dispatcher.rb +9 -9
- data/lib/serrano/validation.rb +138 -0
- data/lib/serrano/version.rb +1 -1
- data/lib/serrano.rb +1 -0
- metadata +2 -2
- data/lib/serrano/cli/templates/entity_validatable.rb.tt +0 -136
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2ce8d324bd08bb913b2bb6b1c3f62b1e2eeed43a0fb34ecb123f1fcf1f9fcb1a
|
|
4
|
+
data.tar.gz: 45aa053034a7bb22fa3c3ca4a17a7d767a3b11c1fb8828a7535708cc0f0cf390
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4f8316342eee52daa26d5a1a69790f0349b1a775d03771823c35657ec7a5475e22d5a37258bf514f9e330046abd85720d5e842ca640f705357cd1935f758de6f
|
|
7
|
+
data.tar.gz: 01e32c0cdf34631cf0084710feacb99d0a7aad55ef817b5c4afae20ac4ac7ff37b292b7f57f237b4dc75e1d9c901d00a78efb6177a1aa366088b25b3bbeda106
|
data/EXAMPLES.md
CHANGED
|
@@ -1,8 +1,18 @@
|
|
|
1
|
-
# Serrano - Full Examples
|
|
2
|
-
|
|
3
|
-
This document is the usage cookbook for the framework and CLI.
|
|
4
|
-
|
|
5
|
-
##
|
|
1
|
+
# Serrano - Full Examples
|
|
2
|
+
|
|
3
|
+
This document is the usage cookbook for the framework and CLI.
|
|
4
|
+
|
|
5
|
+
## 0) Server setup note
|
|
6
|
+
|
|
7
|
+
Serrano only provides HTTP core + CLI scaffolding. It does not assume a fixed production server.
|
|
8
|
+
In your project, you can run with any Rack-compatible server you prefer (for local quickstart, `rackup` is used in examples).
|
|
9
|
+
|
|
10
|
+
If you want to use a different server (for example puma, falcon, unicorn), start the app by running that server with your `config.ru`.
|
|
11
|
+
|
|
12
|
+
- Add `server` gem to your project Gemfile if needed.
|
|
13
|
+
- Keep `config.ru` exposing `run app` as shown below.
|
|
14
|
+
|
|
15
|
+
## 1) Core Runtime: Minimal HTTP App
|
|
6
16
|
|
|
7
17
|
### 1.1 files
|
|
8
18
|
|
|
@@ -82,8 +92,8 @@ def crash(_request)
|
|
|
82
92
|
end
|
|
83
93
|
```
|
|
84
94
|
|
|
85
|
-
- status `500`
|
|
86
|
-
- body `{"error":"
|
|
95
|
+
- status `500`
|
|
96
|
+
- body `{"error":"RuntimeError: boom"}`
|
|
87
97
|
|
|
88
98
|
---
|
|
89
99
|
|
|
@@ -130,37 +140,52 @@ request['id']
|
|
|
130
140
|
|
|
131
141
|
---
|
|
132
142
|
|
|
133
|
-
## 5) CLI: bootstrap new projects
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
```bash
|
|
138
|
-
|
|
139
|
-
```
|
|
143
|
+
## 5) CLI: bootstrap new projects
|
|
144
|
+
|
|
145
|
+
Use:
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
serrano new my_app
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
`serrano new` creates the project structure only. It does not register any routes by itself.
|
|
152
|
+
For first endpoint (`/articles`) do:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
cd my_app
|
|
156
|
+
serrano generate resource Article title:string content:text
|
|
157
|
+
```
|
|
158
|
+
This command inserts the routes into `config.ru` automatically.
|
|
159
|
+
|
|
160
|
+
## 5.1 create full project (recommended)
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
serrano new my_app
|
|
164
|
+
```
|
|
140
165
|
|
|
141
166
|
Generated structure includes `app/`, `config.ru`, `Gemfile`, `config/db.rb`, `db/migrations/`.
|
|
142
167
|
|
|
143
168
|
## 5.2 create full project + sqlite db
|
|
144
169
|
|
|
145
|
-
```bash
|
|
146
|
-
|
|
147
|
-
```
|
|
170
|
+
```bash
|
|
171
|
+
serrano new my_app --db=sqlite
|
|
172
|
+
```
|
|
148
173
|
|
|
149
174
|
Adds sqlite dependency and DB config.
|
|
150
175
|
|
|
151
176
|
## 5.3 minimal project
|
|
152
177
|
|
|
153
|
-
```bash
|
|
154
|
-
|
|
155
|
-
```
|
|
178
|
+
```bash
|
|
179
|
+
serrano new my_app --minimal
|
|
180
|
+
```
|
|
156
181
|
|
|
157
182
|
Only boot files, no app/db scaffolding.
|
|
158
183
|
|
|
159
184
|
## 5.4 minimal + db
|
|
160
185
|
|
|
161
|
-
```bash
|
|
162
|
-
|
|
163
|
-
```
|
|
186
|
+
```bash
|
|
187
|
+
serrano new my_app --minimal --db=postgres
|
|
188
|
+
```
|
|
164
189
|
|
|
165
190
|
Orthogonal behavior: `--minimal` does not disable DB files.
|
|
166
191
|
|
|
@@ -178,9 +203,9 @@ bundle exec rackup
|
|
|
178
203
|
|
|
179
204
|
### 6.1 scaffold full resource
|
|
180
205
|
|
|
181
|
-
```bash
|
|
182
|
-
|
|
183
|
-
```
|
|
206
|
+
```bash
|
|
207
|
+
serrano generate resource Article title:string content:text
|
|
208
|
+
```
|
|
184
209
|
|
|
185
210
|
Creates:
|
|
186
211
|
|
|
@@ -193,11 +218,11 @@ Creates:
|
|
|
193
218
|
|
|
194
219
|
### 6.2 generate individual files
|
|
195
220
|
|
|
196
|
-
```bash
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
```
|
|
221
|
+
```bash
|
|
222
|
+
serrano generate controller Comments
|
|
223
|
+
serrano generate service Reports::Monthly
|
|
224
|
+
serrano generate repository Comment
|
|
225
|
+
```
|
|
201
226
|
|
|
202
227
|
---
|
|
203
228
|
|
|
@@ -211,11 +236,11 @@ This example assumes you are inside an app where `generate resource` already cre
|
|
|
211
236
|
bundle exec rackup
|
|
212
237
|
```
|
|
213
238
|
|
|
214
|
-
### 7.2 routes examples
|
|
215
|
-
|
|
216
|
-
```bash
|
|
217
|
-
curl -i http://localhost:9292/articles
|
|
218
|
-
curl -i http://localhost:9292/articles/1
|
|
239
|
+
### 7.2 routes examples
|
|
240
|
+
|
|
241
|
+
```bash
|
|
242
|
+
curl -i http://localhost:9292/articles
|
|
243
|
+
curl -i http://localhost:9292/articles/1
|
|
219
244
|
curl -i -X POST http://localhost:9292/articles \
|
|
220
245
|
-H "Content-Type: application/x-www-form-urlencoded" \
|
|
221
246
|
--data-raw "title=First&content=Hello"
|
|
@@ -230,8 +255,17 @@ Expected mapping:
|
|
|
230
255
|
- `GET /articles` -> `index`
|
|
231
256
|
- `GET /articles/:id` -> `show`
|
|
232
257
|
- `POST /articles` -> `create`
|
|
233
|
-
- `PUT /articles/:id` -> `update`
|
|
234
|
-
- `DELETE /articles/:id` -> `destroy`
|
|
258
|
+
- `PUT /articles/:id` -> `update`
|
|
259
|
+
- `DELETE /articles/:id` -> `destroy`
|
|
260
|
+
|
|
261
|
+
### 7.3 pass params
|
|
262
|
+
|
|
263
|
+
```bash
|
|
264
|
+
curl -i "http://localhost:9292/articles/1?source=cli"
|
|
265
|
+
curl -i -X POST "http://localhost:9292/articles" \
|
|
266
|
+
-H "Content-Type: application/x-www-form-urlencoded" \
|
|
267
|
+
--data "title=First&content=Hello"
|
|
268
|
+
```
|
|
235
269
|
|
|
236
270
|
---
|
|
237
271
|
|
|
@@ -259,10 +293,10 @@ def crash(_request)
|
|
|
259
293
|
end
|
|
260
294
|
```
|
|
261
295
|
|
|
262
|
-
Expect:
|
|
263
|
-
|
|
264
|
-
- `500`
|
|
265
|
-
- body `{"error":"
|
|
296
|
+
Expect:
|
|
297
|
+
|
|
298
|
+
- `500`
|
|
299
|
+
- body `{"error":"StandardError: boom"}`
|
|
266
300
|
|
|
267
301
|
---
|
|
268
302
|
|
data/README.md
CHANGED
|
@@ -24,21 +24,22 @@ If you want database or CRUD tooling, use the optional CLI and example scaffoldi
|
|
|
24
24
|
|
|
25
25
|
---
|
|
26
26
|
|
|
27
|
-
## Core Contracts
|
|
28
|
-
|
|
29
|
-
- Supported methods: `GET`, `POST`, `PUT`, `DELETE`
|
|
30
|
-
- Route DSL:
|
|
27
|
+
## Core Contracts
|
|
28
|
+
|
|
29
|
+
- Supported methods: `GET`, `POST`, `PUT`, `DELETE`
|
|
30
|
+
- Route DSL:
|
|
31
31
|
- `get(path, controller, action)`
|
|
32
32
|
- `post(path, controller, action)`
|
|
33
33
|
- `put(path, controller, action)`
|
|
34
34
|
- `delete(path, controller, action)`
|
|
35
|
-
- Controller return values:
|
|
36
|
-
- `Hash` => JSON response, status `200`
|
|
37
|
-
- `String` => `text/plain`, status `200`
|
|
38
|
-
- `Serrano::Response` => use as-is
|
|
39
|
-
- other values => `500` with
|
|
40
|
-
- Missing route => `404 Not Found`
|
|
41
|
-
- Missing method for existing path => `405 Method Not Allowed` with lowercase `allow` header and supported methods
|
|
35
|
+
- Controller return values:
|
|
36
|
+
- `Hash` => JSON response, status `200`
|
|
37
|
+
- `String` => `text/plain`, status `200`
|
|
38
|
+
- `Serrano::Response` => use as-is
|
|
39
|
+
- other values => `500` with JSON error
|
|
40
|
+
- Missing route => `404 Not Found`
|
|
41
|
+
- Missing method for existing path => `405 Method Not Allowed` with lowercase `allow` header and supported methods
|
|
42
|
+
- Controller exceptions => `500` JSON error response. Example: `{"error":"RuntimeError: boom"}`
|
|
42
43
|
|
|
43
44
|
See [EXAMPLES.md](./EXAMPLES.md) for full request/response demos.
|
|
44
45
|
|
|
@@ -95,11 +96,14 @@ Run:
|
|
|
95
96
|
bundle exec rackup
|
|
96
97
|
```
|
|
97
98
|
|
|
98
|
-
Quick check:
|
|
99
|
-
|
|
100
|
-
```bash
|
|
101
|
-
curl -i http://localhost:9292/users
|
|
102
|
-
```
|
|
99
|
+
Quick check:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
curl -i http://localhost:9292/users
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
If you used `serrano new`, routes are not present until you generate resources or define them yourself.
|
|
106
|
+
Without matching routes, responses are `404` (path-aware payload).
|
|
103
107
|
|
|
104
108
|
---
|
|
105
109
|
|
|
@@ -107,19 +111,30 @@ curl -i http://localhost:9292/users
|
|
|
107
111
|
|
|
108
112
|
The CLI is optional and depends on the core runtime.
|
|
109
113
|
|
|
110
|
-
### Available commands
|
|
111
|
-
|
|
112
|
-
```bash
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
- `new` creates project files (minimal mode and DB options are orthogonal)
|
|
121
|
-
- `generate resource` creates controller, services (`index`, `show`, `create`, `update`, `destroy`), repository, entity, migration
|
|
122
|
-
-
|
|
114
|
+
### Available commands
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
serrano new APP_NAME [--minimal] [--db=sqlite|postgres|mysql]
|
|
118
|
+
serrano generate resource Article title:string content:text
|
|
119
|
+
serrano generate controller Name
|
|
120
|
+
serrano generate service Namespace::Name
|
|
121
|
+
serrano generate repository Name
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
- `new` creates project files (minimal mode and DB options are orthogonal)
|
|
125
|
+
- `generate resource` creates controller, services (`index`, `show`, `create`, `update`, `destroy`), repository, entity, migration
|
|
126
|
+
- `generate resource` updates `config.ru` with required `require_relative` and route lines when possible.
|
|
127
|
+
|
|
128
|
+
### Parameters in requests
|
|
129
|
+
|
|
130
|
+
Using controller params:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
curl -i "http://localhost:9292/users/12?foo=abc"
|
|
134
|
+
curl -i -X POST "http://localhost:9292/articles" \
|
|
135
|
+
-H "Content-Type: application/x-www-form-urlencoded" \
|
|
136
|
+
--data "title=First&content=Hello"
|
|
137
|
+
```
|
|
123
138
|
|
|
124
139
|
For full command usage with sample outputs and full flows, read [EXAMPLES.md](./EXAMPLES.md).
|
|
125
140
|
|
data/lib/serrano/cli/generate.rb
CHANGED
|
@@ -17,7 +17,6 @@ module Serrano
|
|
|
17
17
|
)
|
|
18
18
|
|
|
19
19
|
write_template("controller.rb.tt", target_path("app/controllers/#{resource[:plural_snake]}_controller.rb"), context)
|
|
20
|
-
write_template("entity_validatable.rb.tt", target_path("app/entities/concerns/validatable.rb"), context)
|
|
21
20
|
write_template("service_index.rb.tt", target_path("app/services/#{resource[:plural_snake]}/index.rb"), context)
|
|
22
21
|
write_template("service_show.rb.tt", target_path("app/services/#{resource[:plural_snake]}/show.rb"), context)
|
|
23
22
|
write_template("service_create.rb.tt", target_path("app/services/#{resource[:plural_snake]}/create.rb"), context)
|
data/lib/serrano/dispatcher.rb
CHANGED
|
@@ -11,13 +11,13 @@ module Serrano
|
|
|
11
11
|
def call(env)
|
|
12
12
|
request = Request.new(env)
|
|
13
13
|
route = @router.resolve(request.method, request.path)
|
|
14
|
-
|
|
15
|
-
unless route
|
|
16
|
-
allowed_methods = @router.allowed_methods(request.path)
|
|
17
|
-
return method_not_allowed(allowed_methods) unless allowed_methods.empty?
|
|
18
|
-
|
|
19
|
-
return not_found
|
|
20
|
-
end
|
|
14
|
+
|
|
15
|
+
unless route
|
|
16
|
+
allowed_methods = @router.allowed_methods(request.path)
|
|
17
|
+
return method_not_allowed(allowed_methods) unless allowed_methods.empty?
|
|
18
|
+
|
|
19
|
+
return not_found(request.path)
|
|
20
|
+
end
|
|
21
21
|
|
|
22
22
|
controller = route[:controller].new
|
|
23
23
|
request.set_path_params(route[:path_params] || {})
|
|
@@ -44,11 +44,11 @@ module Serrano
|
|
|
44
44
|
).to_rack
|
|
45
45
|
end
|
|
46
46
|
|
|
47
|
-
def not_found
|
|
47
|
+
def not_found(path)
|
|
48
48
|
Response.new(
|
|
49
49
|
status: 404,
|
|
50
50
|
headers: { 'content-type' => 'application/json' },
|
|
51
|
-
body:
|
|
51
|
+
body: JSON.generate(error: 'Not Found', path: path)
|
|
52
52
|
).to_rack
|
|
53
53
|
end
|
|
54
54
|
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Serrano
|
|
4
|
+
module Validation
|
|
5
|
+
def self.included(base)
|
|
6
|
+
base.extend(ClassMethods)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
module ClassMethods
|
|
10
|
+
def validations
|
|
11
|
+
@validations ||= Hash.new { |hash, key| hash[key] = [] }
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def validates(field, rules = {})
|
|
15
|
+
validations[field.to_sym] << rules
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def validate(input)
|
|
19
|
+
attrs = symbolize_keys(input || {})
|
|
20
|
+
errors = []
|
|
21
|
+
|
|
22
|
+
validations.each do |field, field_rules|
|
|
23
|
+
value = attrs[field]
|
|
24
|
+
field_rules.each do |rules|
|
|
25
|
+
validate_presence(field, value, rules, errors)
|
|
26
|
+
validate_length(field, value, rules, errors)
|
|
27
|
+
validate_format(field, value, rules, errors)
|
|
28
|
+
validate_inclusion(field, value, rules, errors)
|
|
29
|
+
validate_exclusion(field, value, rules, errors)
|
|
30
|
+
validate_numericality(field, value, rules, errors)
|
|
31
|
+
validate_range(field, value, rules, errors)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
if errors.empty?
|
|
36
|
+
{ ok: true, attrs: attrs }
|
|
37
|
+
else
|
|
38
|
+
{ ok: false, errors: errors, attrs: attrs }
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def symbolize_keys(input)
|
|
45
|
+
input.each_with_object({}) { |(key, value), memo| memo[key.to_sym] = value }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def blank?(value)
|
|
49
|
+
value.nil? || value.to_s.strip.empty?
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def validate_presence(field, value, rules, errors)
|
|
53
|
+
return unless rules[:presence]
|
|
54
|
+
|
|
55
|
+
errors << "#{field} is required" if blank?(value)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def validate_length(field, value, rules, errors)
|
|
59
|
+
config = rules[:length]
|
|
60
|
+
return unless config
|
|
61
|
+
return if blank?(value)
|
|
62
|
+
|
|
63
|
+
str = value.to_s
|
|
64
|
+
if config[:min] && str.length < config[:min]
|
|
65
|
+
errors << "#{field} is too short (minimum is #{config[:min]})"
|
|
66
|
+
end
|
|
67
|
+
if config[:max] && str.length > config[:max]
|
|
68
|
+
errors << "#{field} is too long (maximum is #{config[:max]})"
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def validate_format(field, value, rules, errors)
|
|
73
|
+
pattern = rules[:format]
|
|
74
|
+
return unless pattern
|
|
75
|
+
return if blank?(value)
|
|
76
|
+
|
|
77
|
+
errors << "#{field} is invalid" unless pattern.match?(value.to_s)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def validate_inclusion(field, value, rules, errors)
|
|
81
|
+
allowed = rules[:inclusion]
|
|
82
|
+
return unless allowed
|
|
83
|
+
return if blank?(value)
|
|
84
|
+
|
|
85
|
+
errors << "#{field} is not included in the list" unless allowed.include?(value)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def validate_exclusion(field, value, rules, errors)
|
|
89
|
+
blocked = rules[:exclusion]
|
|
90
|
+
return unless blocked
|
|
91
|
+
return if blank?(value)
|
|
92
|
+
|
|
93
|
+
errors << "#{field} is reserved" if blocked.include?(value)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def validate_numericality(field, value, rules, errors)
|
|
97
|
+
config = rules[:numericality]
|
|
98
|
+
return unless config
|
|
99
|
+
return if blank?(value)
|
|
100
|
+
|
|
101
|
+
number = begin
|
|
102
|
+
Float(value)
|
|
103
|
+
rescue StandardError
|
|
104
|
+
nil
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
if number.nil?
|
|
108
|
+
errors << "#{field} is not a number"
|
|
109
|
+
return
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
if config.is_a?(Hash) && config[:only_integer] && number != number.to_i
|
|
113
|
+
errors << "#{field} must be an integer"
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def validate_range(field, value, rules, errors)
|
|
118
|
+
config = rules[:range]
|
|
119
|
+
return unless config
|
|
120
|
+
return if blank?(value)
|
|
121
|
+
|
|
122
|
+
number = begin
|
|
123
|
+
Float(value)
|
|
124
|
+
rescue StandardError
|
|
125
|
+
nil
|
|
126
|
+
end
|
|
127
|
+
return if number.nil?
|
|
128
|
+
|
|
129
|
+
if config[:min] && number < config[:min]
|
|
130
|
+
errors << "#{field} must be greater than or equal to #{config[:min]}"
|
|
131
|
+
end
|
|
132
|
+
if config[:max] && number > config[:max]
|
|
133
|
+
errors << "#{field} must be less than or equal to #{config[:max]}"
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
data/lib/serrano/version.rb
CHANGED
data/lib/serrano.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: serrano-vk
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jesus Martinez
|
|
@@ -692,7 +692,6 @@ files:
|
|
|
692
692
|
- lib/serrano/cli/generate.rb
|
|
693
693
|
- lib/serrano/cli/templates/controller.rb.tt
|
|
694
694
|
- lib/serrano/cli/templates/entity.rb.tt
|
|
695
|
-
- lib/serrano/cli/templates/entity_validatable.rb.tt
|
|
696
695
|
- lib/serrano/cli/templates/migration.rb.tt
|
|
697
696
|
- lib/serrano/cli/templates/new_default_config.ru.tt
|
|
698
697
|
- lib/serrano/cli/templates/new_default_db.rb.tt
|
|
@@ -713,6 +712,7 @@ files:
|
|
|
713
712
|
- lib/serrano/request.rb
|
|
714
713
|
- lib/serrano/response.rb
|
|
715
714
|
- lib/serrano/router.rb
|
|
715
|
+
- lib/serrano/validation.rb
|
|
716
716
|
- lib/serrano/version.rb
|
|
717
717
|
homepage: https://github.com/vurokrazia/serrano
|
|
718
718
|
licenses:
|
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Validatable
|
|
4
|
-
def self.included(base)
|
|
5
|
-
base.extend(ClassMethods)
|
|
6
|
-
end
|
|
7
|
-
|
|
8
|
-
module ClassMethods
|
|
9
|
-
def validations
|
|
10
|
-
@validations ||= Hash.new { |hash, key| hash[key] = [] }
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
def validates(field, rules = {})
|
|
14
|
-
validations[field.to_sym] << rules
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def validate(input)
|
|
18
|
-
attrs = symbolize_keys(input || {})
|
|
19
|
-
errors = []
|
|
20
|
-
|
|
21
|
-
validations.each do |field, field_rules|
|
|
22
|
-
value = attrs[field]
|
|
23
|
-
field_rules.each do |rules|
|
|
24
|
-
validate_presence(field, value, rules, errors)
|
|
25
|
-
validate_length(field, value, rules, errors)
|
|
26
|
-
validate_format(field, value, rules, errors)
|
|
27
|
-
validate_inclusion(field, value, rules, errors)
|
|
28
|
-
validate_exclusion(field, value, rules, errors)
|
|
29
|
-
validate_numericality(field, value, rules, errors)
|
|
30
|
-
validate_range(field, value, rules, errors)
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
if errors.empty?
|
|
35
|
-
{ ok: true, attrs: attrs }
|
|
36
|
-
else
|
|
37
|
-
{ ok: false, errors: errors, attrs: attrs }
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
private
|
|
42
|
-
|
|
43
|
-
def symbolize_keys(input)
|
|
44
|
-
input.each_with_object({}) { |(key, value), memo| memo[key.to_sym] = value }
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
def blank?(value)
|
|
48
|
-
value.nil? || value.to_s.strip.empty?
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def validate_presence(field, value, rules, errors)
|
|
52
|
-
return unless rules[:presence]
|
|
53
|
-
|
|
54
|
-
errors << "#{field} is required" if blank?(value)
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def validate_length(field, value, rules, errors)
|
|
58
|
-
config = rules[:length]
|
|
59
|
-
return unless config
|
|
60
|
-
return if blank?(value)
|
|
61
|
-
|
|
62
|
-
str = value.to_s
|
|
63
|
-
if config[:min] && str.length < config[:min]
|
|
64
|
-
errors << "#{field} is too short (minimum is #{config[:min]})"
|
|
65
|
-
end
|
|
66
|
-
if config[:max] && str.length > config[:max]
|
|
67
|
-
errors << "#{field} is too long (maximum is #{config[:max]})"
|
|
68
|
-
end
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def validate_format(field, value, rules, errors)
|
|
72
|
-
pattern = rules[:format]
|
|
73
|
-
return unless pattern
|
|
74
|
-
return if blank?(value)
|
|
75
|
-
|
|
76
|
-
errors << "#{field} is invalid" unless pattern.match?(value.to_s)
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
def validate_inclusion(field, value, rules, errors)
|
|
80
|
-
allowed = rules[:inclusion]
|
|
81
|
-
return unless allowed
|
|
82
|
-
return if blank?(value)
|
|
83
|
-
|
|
84
|
-
errors << "#{field} is not included in the list" unless allowed.include?(value)
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
def validate_exclusion(field, value, rules, errors)
|
|
88
|
-
blocked = rules[:exclusion]
|
|
89
|
-
return unless blocked
|
|
90
|
-
return if blank?(value)
|
|
91
|
-
|
|
92
|
-
errors << "#{field} is reserved" if blocked.include?(value)
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
def validate_numericality(field, value, rules, errors)
|
|
96
|
-
config = rules[:numericality]
|
|
97
|
-
return unless config
|
|
98
|
-
return if blank?(value)
|
|
99
|
-
|
|
100
|
-
number = begin
|
|
101
|
-
Float(value)
|
|
102
|
-
rescue StandardError
|
|
103
|
-
nil
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
if number.nil?
|
|
107
|
-
errors << "#{field} is not a number"
|
|
108
|
-
return
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
if config.is_a?(Hash) && config[:only_integer] && number != number.to_i
|
|
112
|
-
errors << "#{field} must be an integer"
|
|
113
|
-
end
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
def validate_range(field, value, rules, errors)
|
|
117
|
-
config = rules[:range]
|
|
118
|
-
return unless config
|
|
119
|
-
return if blank?(value)
|
|
120
|
-
|
|
121
|
-
number = begin
|
|
122
|
-
Float(value)
|
|
123
|
-
rescue StandardError
|
|
124
|
-
nil
|
|
125
|
-
end
|
|
126
|
-
return if number.nil?
|
|
127
|
-
|
|
128
|
-
if config[:min] && number < config[:min]
|
|
129
|
-
errors << "#{field} must be greater than or equal to #{config[:min]}"
|
|
130
|
-
end
|
|
131
|
-
if config[:max] && number > config[:max]
|
|
132
|
-
errors << "#{field} must be less than or equal to #{config[:max]}"
|
|
133
|
-
end
|
|
134
|
-
end
|
|
135
|
-
end
|
|
136
|
-
end
|