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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6a3f9a5c639bfa422a1441b9399fe91bb858f072787b6ff882f13dbbb706d1e1
4
- data.tar.gz: 50a6aec6958948a931bbfd6bfbd55208adb241dca6c2d79cf3bc7329c79cebb4
3
+ metadata.gz: 2ce8d324bd08bb913b2bb6b1c3f62b1e2eeed43a0fb34ecb123f1fcf1f9fcb1a
4
+ data.tar.gz: 45aa053034a7bb22fa3c3ca4a17a7d767a3b11c1fb8828a7535708cc0f0cf390
5
5
  SHA512:
6
- metadata.gz: 77d117adcb1376fc62219aad4d05798b5075ba511f4e36bf8377e963c868231333c979d3842315def62d88f1543bb0a3ef1f9ef03644152441b5d17081d62372
7
- data.tar.gz: d4573e52bed1ce97874e1f4b11aef7b3cee8f697d5e0b35066545e888ca59ccf011b85d6f2ec34ada12a0ccf758425b92dcfb1fcb87723166c027fc8102b8c02
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) Core Runtime: Minimal HTTP App
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":"Internal Server 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
- ## 5.1 create full project (recommended)
136
-
137
- ```bash
138
- bundle exec serrano new my_app
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
- bundle exec serrano new my_app --db=sqlite
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
- bundle exec serrano new my_app --minimal
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
- bundle exec serrano new my_app --minimal --db=postgres
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
- bundle exec serrano generate resource Article title:string content:text
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
- bundle exec serrano generate controller Comments
198
- bundle exec serrano generate service Reports::Monthly
199
- bundle exec serrano generate repository Comment
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":"Internal Server 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 `Internal Server Error`
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
- bundle exec serrano new APP_NAME [--minimal] [--db=sqlite|postgres|mysql]
114
- bundle exec serrano generate resource Article title:string content:text
115
- bundle exec serrano generate controller Name
116
- bundle exec serrano generate service Namespace::Name
117
- bundle exec serrano generate repository Name
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
- - route code is suggested in terminal output; automatic `config.ru` route editing is handled by generator logic when app file exists
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
 
@@ -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)
@@ -1,9 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "./concerns/validatable"
4
-
5
3
  class <%= resource[:singular_camel] %>
6
- include Validatable
4
+ include Serrano::Validation
7
5
 
8
6
  <% fields.each do |field| -%>
9
7
  <% rules = [] -%>
@@ -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: '{"error":"Not Found"}'
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Serrano
4
- VERSION = "0.1.2"
4
+ VERSION = "0.1.3"
5
5
  end
data/lib/serrano.rb CHANGED
@@ -6,6 +6,7 @@ require_relative 'serrano/router'
6
6
  require_relative 'serrano/request'
7
7
  require_relative 'serrano/response'
8
8
  require_relative 'serrano/dispatcher'
9
+ require_relative 'serrano/validation'
9
10
 
10
11
  module Serrano
11
12
  end
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.2
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