pragma 2.0.0 → 2.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +12 -0
- data/CHANGELOG.md +25 -5
- data/README.md +364 -10
- data/lib/pragma.rb +7 -0
- data/lib/pragma/operation/create.rb +3 -3
- data/lib/pragma/operation/destroy.rb +10 -9
- data/lib/pragma/operation/filter/base.rb +20 -0
- data/lib/pragma/operation/filter/equals.rb +13 -0
- data/lib/pragma/operation/filter/ilike.rb +13 -0
- data/lib/pragma/operation/filter/like.rb +13 -0
- data/lib/pragma/operation/index.rb +5 -5
- data/lib/pragma/operation/macro/classes.rb +6 -1
- data/lib/pragma/operation/macro/decorator.rb +32 -11
- data/lib/pragma/operation/macro/filtering.rb +47 -0
- data/lib/pragma/operation/macro/model.rb +4 -1
- data/lib/pragma/operation/macro/ordering.rb +84 -0
- data/lib/pragma/operation/macro/pagination.rb +7 -3
- data/lib/pragma/operation/macro/policy.rb +3 -3
- data/lib/pragma/operation/show.rb +2 -2
- data/lib/pragma/operation/update.rb +4 -4
- data/lib/pragma/version.rb +1 -1
- data/pragma.gemspec +4 -4
- metadata +18 -12
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 423424f1618e9c1e91217573586e6e10b70b5a06
|
4
|
+
data.tar.gz: 77cfe2fd44b3f5df6f22563345b9ae61d48413e4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cea1554dba5bfdbb80680ea52d044843b7ab23ff28c04ccee072e750ae96b70cb2822963da3224b607fc06f9caa801e15ee8024037a2567dcb479fc7175c870b
|
7
|
+
data.tar.gz: f6af85934562c1538f576aba2e9a74dbfa2f2b60340a8e2a991a3cedfefd5edafd6eb44e23e7e80425861b4aa4cfe14d6212fb92bb7c84ac9f96f435259b2b21
|
data/.rubocop.yml
CHANGED
@@ -88,3 +88,15 @@ Metrics/ClassLength:
|
|
88
88
|
|
89
89
|
Metrics/BlockLength:
|
90
90
|
Enabled: false
|
91
|
+
|
92
|
+
Style/Documentation:
|
93
|
+
Enabled: false
|
94
|
+
|
95
|
+
Naming/MethodName:
|
96
|
+
Enabled: false
|
97
|
+
|
98
|
+
Naming/AccessorMethodName:
|
99
|
+
Enabled: false
|
100
|
+
|
101
|
+
RSpec/MessageSpies:
|
102
|
+
EnforcedStyle: receive
|
data/CHANGELOG.md
CHANGED
@@ -1,8 +1,28 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
-
|
3
|
+
All notable changes to this project will be documented in this file.
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
5
|
+
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
6
|
+
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
7
|
+
|
8
|
+
## [Unreleased]
|
9
|
+
|
10
|
+
## [2.1.0]
|
11
|
+
|
12
|
+
### Added
|
13
|
+
|
14
|
+
- Implemented `expand.limit` and `expand.enabled`
|
15
|
+
- Implemented the `Ordering` macro
|
16
|
+
- Implemented the `Filtering` macro
|
17
|
+
|
18
|
+
### Fixed
|
19
|
+
|
20
|
+
- Fixed automatic lookup of nested model classes
|
21
|
+
|
22
|
+
## [2.0.0]
|
23
|
+
|
24
|
+
First Pragma 2 release.
|
25
|
+
|
26
|
+
[Unreleased]: https://github.com/pragmarb/pragma/compare/v2.1.0...HEAD
|
27
|
+
[2.1.0]: https://github.com/pragmarb/pragma/compare/v2.0.0...v2.1.0
|
28
|
+
[2.0.0]: https://github.com/pragmarb/pragma/compare/v1.2.6...v2.0.0
|
data/README.md
CHANGED
@@ -1,11 +1,12 @@
|
|
1
1
|
# Pragma
|
2
2
|
|
3
|
-
[![Build Status](https://
|
4
|
-
[![Dependency Status](https://
|
5
|
-
[![
|
6
|
-
[![
|
3
|
+
[![Build Status](https://travis-ci.org/pragmarb/pragma.svg?branch=master)](https://travis-ci.org/pragmarb/pragma)
|
4
|
+
[![Dependency Status](https://gemnasium.com/badges/github.com/pragmarb/pragma.svg)](https://gemnasium.com/github.com/pragmarb/pragma)
|
5
|
+
[![Coverage Status](https://coveralls.io/repos/github/pragmarb/pragma/badge.svg?branch=master)](https://coveralls.io/github/pragmarb/pragma?branch=master)
|
6
|
+
[![Maintainability](https://api.codeclimate.com/v1/badges/e51e8d7489eb72ab97ba/maintainability)](https://codeclimate.com/github/pragmarb/pragma/maintainability)
|
7
7
|
|
8
|
-
Welcome to Pragma,
|
8
|
+
Welcome to Pragma, an expressive, opinionated ecosystem for building beautiful RESTful APIs with
|
9
|
+
Ruby.
|
9
10
|
|
10
11
|
You can think of this as a meta-gem that pulls in the following pieces:
|
11
12
|
|
@@ -99,8 +100,10 @@ This gem works best if you follow the recommended structure for organizing resou
|
|
99
100
|
│ ├── destroy.rb
|
100
101
|
│ ├── index.rb
|
101
102
|
│ └── update.rb
|
103
|
+
└── decorator
|
104
|
+
| ├── collection.rb
|
105
|
+
| └── instance.rb
|
102
106
|
└── policy.rb
|
103
|
-
└── decorator.rb
|
104
107
|
```
|
105
108
|
|
106
109
|
Your modules and classes would, of course, follow the same structure: `API::V1::Article::Policy` and
|
@@ -128,10 +131,10 @@ module API
|
|
128
131
|
module Operation
|
129
132
|
class Create < Pragma::Operation::Create
|
130
133
|
# This assumes that you have the following:
|
131
|
-
#
|
132
|
-
#
|
133
|
-
#
|
134
|
-
#
|
134
|
+
# 1) an Article model
|
135
|
+
# 2) a Policy (responding to #create?)
|
136
|
+
# 3) a Create contract
|
137
|
+
# 4) an Instance decorator
|
135
138
|
end
|
136
139
|
end
|
137
140
|
end
|
@@ -139,6 +142,357 @@ module API
|
|
139
142
|
end
|
140
143
|
```
|
141
144
|
|
145
|
+
## Macros
|
146
|
+
|
147
|
+
The FF are implemented through their own set of macros, which take care of stuff like authorizing,
|
148
|
+
paginating, filtering etc.
|
149
|
+
|
150
|
+
If you want, you can use these macros in your own operations.
|
151
|
+
|
152
|
+
### Classes
|
153
|
+
|
154
|
+
**Used in:** Index, Show, Create, Update, Destroy
|
155
|
+
|
156
|
+
The `Classes` macro is responsible of tying together all the Pragma components: put it into an
|
157
|
+
operation and it will determine the class names of the related policy, model, decorators and
|
158
|
+
contract. You can override any of these classes when defining the operation or at runtime if you
|
159
|
+
wish.
|
160
|
+
|
161
|
+
Example usage:
|
162
|
+
|
163
|
+
```ruby
|
164
|
+
module API
|
165
|
+
module V1
|
166
|
+
module Article
|
167
|
+
module Operation
|
168
|
+
class Create < Pragma::Operation::Base
|
169
|
+
# Let the macro figure out class names.
|
170
|
+
step Pragma::Operation::Macro::Classes()
|
171
|
+
step :execute!
|
172
|
+
|
173
|
+
# But override the contract.
|
174
|
+
self['contract.default.class'] = Contract::CustomCreate
|
175
|
+
|
176
|
+
def execute!(options)
|
177
|
+
# `options` contains the following:
|
178
|
+
#
|
179
|
+
# `model.class`
|
180
|
+
# `policy.default.class`
|
181
|
+
# `policy.default.scope.class`
|
182
|
+
# `decorator.instance.class`
|
183
|
+
# `decorator.collection.class`
|
184
|
+
# `contract.default.class`
|
185
|
+
#
|
186
|
+
# These will be `nil` if the expected classes do not exist.
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
```
|
194
|
+
|
195
|
+
### Model
|
196
|
+
|
197
|
+
**Used in:** Index, Show, Create, Update, Destroy
|
198
|
+
|
199
|
+
The `Model` macro provides support for performing different operations with models. It can either
|
200
|
+
build a new instance of the model, if you are creating a new record, for instance, or it can find
|
201
|
+
an existing record by ID.
|
202
|
+
|
203
|
+
Example of building a new record:
|
204
|
+
|
205
|
+
```ruby
|
206
|
+
module API
|
207
|
+
module V1
|
208
|
+
module Article
|
209
|
+
module Operation
|
210
|
+
class Create < Pragma::Operation::Base
|
211
|
+
# This step can be done by Classes if you want.
|
212
|
+
self['model.class'] = ::Article
|
213
|
+
|
214
|
+
step Pragma::Operation::Macro::Model(:build)
|
215
|
+
step :save!
|
216
|
+
|
217
|
+
def save!(options)
|
218
|
+
# Here you'd usually validate and assign parameters before saving.
|
219
|
+
|
220
|
+
# ...
|
221
|
+
|
222
|
+
options['model'].save!
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|
227
|
+
end
|
228
|
+
end
|
229
|
+
```
|
230
|
+
|
231
|
+
As we mentioned, `Model` can also be used to find a record by ID:
|
232
|
+
|
233
|
+
```ruby
|
234
|
+
module API
|
235
|
+
module V1
|
236
|
+
module Article
|
237
|
+
module Operation
|
238
|
+
class Show < Pragma::Operation::Base
|
239
|
+
# This step can be done by Classes if you want.
|
240
|
+
self['model.class'] = ::Article
|
241
|
+
|
242
|
+
step Pragma::Operation::Macro::Model(:find_by), fail_fast: true
|
243
|
+
step :respond!
|
244
|
+
|
245
|
+
def respond!(options)
|
246
|
+
options['result.response'] = Response::Ok.new(
|
247
|
+
entity: options['model']
|
248
|
+
)
|
249
|
+
end
|
250
|
+
end
|
251
|
+
end
|
252
|
+
end
|
253
|
+
end
|
254
|
+
end
|
255
|
+
```
|
256
|
+
|
257
|
+
In the example above, if the record is not found, the macro will respond with `404 Not Found` and a
|
258
|
+
descriptive error message for you. If you want to override the error handling logic, you can remove
|
259
|
+
the `fail_fast` option and instead implement your own `failure` step.
|
260
|
+
|
261
|
+
### Policy
|
262
|
+
|
263
|
+
**Used in:** Index, Show, Create, Update, Destroy
|
264
|
+
|
265
|
+
The `Policy` macro ensures that the current user can perform an operation on a given record.
|
266
|
+
|
267
|
+
Here's a usage example:
|
268
|
+
|
269
|
+
```ruby
|
270
|
+
module API
|
271
|
+
module V1
|
272
|
+
module Article
|
273
|
+
module Operation
|
274
|
+
class Show < Pragma::Operation::Base
|
275
|
+
# This step can be done by Classes if you want.
|
276
|
+
self['policy.default.class'] = Policy
|
277
|
+
|
278
|
+
step :model!
|
279
|
+
step Pragma::Operation::Macro::Policy(), fail_fast: true
|
280
|
+
step :respond!
|
281
|
+
|
282
|
+
def model!(params:, **)
|
283
|
+
options['model'] = ::Article.find(params[:id])
|
284
|
+
end
|
285
|
+
end
|
286
|
+
end
|
287
|
+
end
|
288
|
+
end
|
289
|
+
end
|
290
|
+
```
|
291
|
+
|
292
|
+
If the user is not authorized to perform the operation (i.e. if the policy's `#show?` method returns
|
293
|
+
`false`), the macro will respond with `403 Forbidden` and a descriptive error message. If you want
|
294
|
+
to override the error handling logic, you can remove the `fail_fast` option and instead implement
|
295
|
+
your own `failure` step.
|
296
|
+
|
297
|
+
### Filtering
|
298
|
+
|
299
|
+
**Used in:** Index
|
300
|
+
|
301
|
+
The `Filtering` macro provides a simple interface to define basic filters for your API. You simply
|
302
|
+
include the macro and configure which filters you want to expose to the users.
|
303
|
+
|
304
|
+
```ruby
|
305
|
+
module API
|
306
|
+
module V1
|
307
|
+
module Article
|
308
|
+
module Operation
|
309
|
+
class Index < Pragma::Operation::Base
|
310
|
+
step :model!
|
311
|
+
step Pragma::Operation::Macro::Filtering()
|
312
|
+
step :respond!
|
313
|
+
|
314
|
+
self['filtering.filters'] = [
|
315
|
+
Pragma::Operation::Filter::Equals.new(param: :by_category, column: :category_id),
|
316
|
+
Pragma::Operation::Filter::Ilike.new(param: :by_title, column: :title)
|
317
|
+
]
|
318
|
+
|
319
|
+
def model!(params:, **)
|
320
|
+
options['model'] = ::Article.all
|
321
|
+
end
|
322
|
+
end
|
323
|
+
end
|
324
|
+
end
|
325
|
+
end
|
326
|
+
end
|
327
|
+
```
|
328
|
+
|
329
|
+
With the example above, you are exposing the `by_category` filter and the `by_title` filters. The
|
330
|
+
following filters are available for ActiveRecord currently:
|
331
|
+
|
332
|
+
- `Equals`: performs an equality (`=`) comparison.
|
333
|
+
- `Like`: performs a `LIKE` comparison.
|
334
|
+
- `Ilike`: performs an `ILIKE` comparison.
|
335
|
+
|
336
|
+
Support for more clauses as well as more ORMs will come soon.
|
337
|
+
|
338
|
+
### Ordering
|
339
|
+
|
340
|
+
**Used in:** Index
|
341
|
+
|
342
|
+
As the name suggests, the `Ordering` macro allows you to easily implement default and user-defined
|
343
|
+
ordering.
|
344
|
+
|
345
|
+
Here's an example:
|
346
|
+
|
347
|
+
```ruby
|
348
|
+
module API
|
349
|
+
module V1
|
350
|
+
module Article
|
351
|
+
module Operation
|
352
|
+
class Index < Pragma::Operation::Base
|
353
|
+
# This step can be done by Classes if you want.
|
354
|
+
self['model.class'] = ::Article
|
355
|
+
|
356
|
+
self['ordering.default_column'] = :published_at
|
357
|
+
self['ordering.default_direction'] = :desc
|
358
|
+
self['ordering.columns'] = %i[title published_at updated_at]
|
359
|
+
|
360
|
+
step :model!
|
361
|
+
|
362
|
+
# This will override `model` with the ordered relation.
|
363
|
+
step Pragma::Operation::Macro::Ordering(), fail_fast: true
|
364
|
+
|
365
|
+
step :respond!
|
366
|
+
|
367
|
+
def model!(options)
|
368
|
+
options['model'] = options['model.class'].all
|
369
|
+
end
|
370
|
+
|
371
|
+
def respond!(options)
|
372
|
+
options['result.response'] = Response::Ok.new(
|
373
|
+
entity: options['model']
|
374
|
+
)
|
375
|
+
end
|
376
|
+
end
|
377
|
+
end
|
378
|
+
end
|
379
|
+
end
|
380
|
+
end
|
381
|
+
```
|
382
|
+
|
383
|
+
If the user provides an invalid order column or direction, the macro will respond with `422 Unprocessable Entity`
|
384
|
+
and a descriptive error message. If you wish to implement your own error handling logic, you can
|
385
|
+
remove the `fail_fast` option and implement your own `failure` step.
|
386
|
+
|
387
|
+
The macro accepts the following options, which can be defined on the operation or at runtime:
|
388
|
+
|
389
|
+
- `ordering.columns`: an array of columns the user can order by.
|
390
|
+
- `ordering.default_column`: the default column to order by (default: `created_at`).
|
391
|
+
- `ordering.default_direction`: the default direction to order by (default: `desc`).
|
392
|
+
- `ordering.column_param`: the name of the parameter which will contain the order column.
|
393
|
+
- `ordering.direction_param`: the name of the parameter which will contain the order direction.
|
394
|
+
|
395
|
+
### Pagination
|
396
|
+
|
397
|
+
**Used in:** Index
|
398
|
+
|
399
|
+
The `Pagination` macro is responsible for paginating collections of records through
|
400
|
+
[will_paginate](https://github.com/mislav/will_paginate). It also allows your users to set the
|
401
|
+
number of records per page.
|
402
|
+
|
403
|
+
```ruby
|
404
|
+
module API
|
405
|
+
module V1
|
406
|
+
module Article
|
407
|
+
module Operation
|
408
|
+
class Index < Pragma::Operation::Base
|
409
|
+
# This step can be done by Classes if you want.
|
410
|
+
self['model.class'] = ::Article
|
411
|
+
|
412
|
+
step :model!
|
413
|
+
|
414
|
+
# This will override `model` with the paginated relation.
|
415
|
+
step Pragma::Operation::Macro::Pagination(), fail_fast: true
|
416
|
+
|
417
|
+
step :respond!
|
418
|
+
|
419
|
+
def model!(options)
|
420
|
+
options['model'] = options['model.class'].all
|
421
|
+
end
|
422
|
+
|
423
|
+
def respond!(options)
|
424
|
+
options['result.response'] = Response::Ok.new(
|
425
|
+
entity: options['model']
|
426
|
+
)
|
427
|
+
end
|
428
|
+
end
|
429
|
+
end
|
430
|
+
end
|
431
|
+
end
|
432
|
+
end
|
433
|
+
```
|
434
|
+
|
435
|
+
In the example above, if the page or per-page number fail validation, the macro will respond with
|
436
|
+
`422 Unprocessable Entity` and a descriptive error message. If you wish to implement your own error
|
437
|
+
handling logic, you can remove the `fail_fast` option and implement your own `failure` step.
|
438
|
+
|
439
|
+
The macro accepts the following options, which can be defined on the operation or at runtime:
|
440
|
+
|
441
|
+
- `pagination.page_param`: the parameter that will contain the page number.
|
442
|
+
- `pagination.per_page_param`: the parameter that will contain the number of items to include in each page.
|
443
|
+
- `pagination.default_per_page`: the default number of items per page.
|
444
|
+
- `pagination.max_per_page`: the max number of items per page.
|
445
|
+
|
446
|
+
This macro is best used in conjunction with the [Collection](https://github.com/pragmarb/pragma-decorator#collection)
|
447
|
+
and [Pagination](https://github.com/pragmarb/pragma-decorator#pagination) modules of
|
448
|
+
[Pragma::Decorator](https://github.com/pragmarb/pragma-decorator), which will expose all the
|
449
|
+
pagination metadata.
|
450
|
+
|
451
|
+
### Decorator
|
452
|
+
|
453
|
+
**Used in:** Index, Show, Create, Update
|
454
|
+
|
455
|
+
The `Decorator` macro uses one of your decorators to decorate the model. If you are using
|
456
|
+
[expansion](https://github.com/pragmarb/pragma-decorator#associations), it will also make sure that
|
457
|
+
the expansion parameter is valid.
|
458
|
+
|
459
|
+
Example usage:
|
460
|
+
|
461
|
+
```ruby
|
462
|
+
module API
|
463
|
+
module V1
|
464
|
+
module Article
|
465
|
+
module Operation
|
466
|
+
class Show < Pragma::Operation::Base
|
467
|
+
# This step can be done by Classes if you want.
|
468
|
+
self['decorator.instance.class'] = Decorator::Instance
|
469
|
+
|
470
|
+
step :model!
|
471
|
+
step Pragma::Operation::Macro::Decorator(), fail_fast: true
|
472
|
+
step :respond!
|
473
|
+
|
474
|
+
def model!(params:, **)
|
475
|
+
options['model'] = ::Article.find(params[:id])
|
476
|
+
end
|
477
|
+
|
478
|
+
def respond!(options)
|
479
|
+
# Pragma does this for you in the default operations.
|
480
|
+
options['result.response'] = Response::Ok.new(
|
481
|
+
entity: options['result.decorator.instance']
|
482
|
+
)
|
483
|
+
end
|
484
|
+
end
|
485
|
+
end
|
486
|
+
end
|
487
|
+
end
|
488
|
+
end
|
489
|
+
```
|
490
|
+
|
491
|
+
The macro accepts the following options, which can be defined on the operation or at runtime:
|
492
|
+
|
493
|
+
- `expand.enabled`: whether associations can be expanded.
|
494
|
+
- `expand.limit`: how many associations can be expanded at once.
|
495
|
+
|
142
496
|
## Contributing
|
143
497
|
|
144
498
|
Bug reports and pull requests are welcome on GitHub at https://github.com/pragmarb/pragma.
|
data/lib/pragma.rb
CHANGED
@@ -12,8 +12,15 @@ require 'pragma/version'
|
|
12
12
|
|
13
13
|
require 'pragma/decorator/error'
|
14
14
|
|
15
|
+
require 'pragma/operation/filter/base'
|
16
|
+
require 'pragma/operation/filter/equals'
|
17
|
+
require 'pragma/operation/filter/like'
|
18
|
+
require 'pragma/operation/filter/ilike'
|
19
|
+
|
15
20
|
require 'pragma/operation/macro/classes'
|
16
21
|
require 'pragma/operation/macro/decorator'
|
22
|
+
require 'pragma/operation/macro/filtering'
|
23
|
+
require 'pragma/operation/macro/ordering'
|
17
24
|
require 'pragma/operation/macro/pagination'
|
18
25
|
require 'pragma/operation/macro/policy'
|
19
26
|
require 'pragma/operation/macro/model'
|
@@ -8,10 +8,10 @@ module Pragma
|
|
8
8
|
class Create < Pragma::Operation::Base
|
9
9
|
step Macro::Classes()
|
10
10
|
step Macro::Model()
|
11
|
-
step Macro::Policy()
|
11
|
+
step Macro::Policy()
|
12
12
|
step Macro::Contract::Build()
|
13
|
-
step Macro::Contract::Validate()
|
14
|
-
step Macro::Contract::Persist()
|
13
|
+
step Macro::Contract::Validate()
|
14
|
+
step Macro::Contract::Persist()
|
15
15
|
step Macro::Decorator()
|
16
16
|
step :respond!
|
17
17
|
|
@@ -7,20 +7,21 @@ module Pragma
|
|
7
7
|
# @author Alessandro Desantis
|
8
8
|
class Destroy < Pragma::Operation::Base
|
9
9
|
step Macro::Classes()
|
10
|
-
step Macro::Model(:find_by)
|
11
|
-
step Macro::Policy()
|
10
|
+
step Macro::Model(:find_by)
|
11
|
+
step Macro::Policy()
|
12
12
|
step :destroy!
|
13
|
-
failure :handle_invalid_model!, fail_fast: true
|
14
13
|
step :respond!
|
15
14
|
|
16
15
|
def destroy!(_options, model:, **)
|
17
|
-
model.destroy
|
18
|
-
|
16
|
+
unless model.destroy
|
17
|
+
options['result.response'] = Response::UnprocessableEntity.new(
|
18
|
+
errors: model.errors
|
19
|
+
).decorate_with(Decorator::Error)
|
20
|
+
|
21
|
+
return false
|
22
|
+
end
|
19
23
|
|
20
|
-
|
21
|
-
options['result.response'] = Response::UnprocessableEntity.new(
|
22
|
-
errors: model.errors
|
23
|
-
).decorate_with(Decorator::Error)
|
24
|
+
true
|
24
25
|
end
|
25
26
|
|
26
27
|
def respond!(options)
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Pragma
|
4
|
+
module Operation
|
5
|
+
module Filter
|
6
|
+
class Base
|
7
|
+
attr_reader :param, :column
|
8
|
+
|
9
|
+
def initialize(param:, column:)
|
10
|
+
@param = param.to_sym
|
11
|
+
@column = column.to_sym
|
12
|
+
end
|
13
|
+
|
14
|
+
def apply(*)
|
15
|
+
fail NotImplementedError
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -1,7 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'trailblazer/dsl'
|
4
|
-
|
5
3
|
module Pragma
|
6
4
|
module Operation
|
7
5
|
# Finds all records of the requested resource, authorizes them, paginates them and decorates
|
@@ -12,8 +10,10 @@ module Pragma
|
|
12
10
|
step Macro::Classes()
|
13
11
|
step :retrieve!
|
14
12
|
step :scope!
|
15
|
-
step Macro::
|
16
|
-
step Macro::
|
13
|
+
step Macro::Filtering()
|
14
|
+
step Macro::Ordering()
|
15
|
+
step Macro::Pagination()
|
16
|
+
step Macro::Decorator(name: :collection)
|
17
17
|
step :respond!
|
18
18
|
|
19
19
|
def retrieve!(options)
|
@@ -24,7 +24,7 @@ module Pragma
|
|
24
24
|
options['model'] = options['policy.default.scope.class'].new(current_user, model).resolve
|
25
25
|
end
|
26
26
|
|
27
|
-
def respond!(options,
|
27
|
+
def respond!(options, **)
|
28
28
|
options['result.response'] = Response::Ok.new(
|
29
29
|
entity: options['result.decorator.collection']
|
30
30
|
)
|
@@ -48,9 +48,14 @@ module Pragma
|
|
48
48
|
input.class.name.split('::')[0..-3]
|
49
49
|
end
|
50
50
|
|
51
|
+
def root_namespace(input, options)
|
52
|
+
resource_namespace = resource_namespace(input, options)
|
53
|
+
resource_namespace[0..((resource_namespace.index('API') || 1) - 1)]
|
54
|
+
end
|
55
|
+
|
51
56
|
def expected_model_class(input, options)
|
52
57
|
[
|
53
|
-
|
58
|
+
root_namespace(input, options).join('::'),
|
54
59
|
resource_namespace(input, options).last
|
55
60
|
].join('::')
|
56
61
|
end
|
@@ -11,10 +11,9 @@ module Pragma
|
|
11
11
|
module Decorator
|
12
12
|
class << self
|
13
13
|
def for(_input, name, options)
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
end
|
14
|
+
set_defaults(options)
|
15
|
+
|
16
|
+
return false unless validate_params(options)
|
18
17
|
|
19
18
|
options["result.decorator.#{name}"] = options["decorator.#{name}.class"].new(
|
20
19
|
options['model']
|
@@ -25,20 +24,42 @@ module Pragma
|
|
25
24
|
|
26
25
|
private
|
27
26
|
|
27
|
+
def set_defaults(options)
|
28
|
+
hash_options = options.to_hash
|
29
|
+
|
30
|
+
{
|
31
|
+
'expand.enabled' => true
|
32
|
+
}.each_pair do |key, value|
|
33
|
+
options[key] = value unless hash_options.key?(key.to_sym)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
28
37
|
def validate_params(options)
|
29
38
|
options['contract.expand'] = Dry::Validation.Schema do
|
30
|
-
optional(:expand)
|
39
|
+
optional(:expand) do
|
40
|
+
if options['expand.enabled']
|
41
|
+
array? do
|
42
|
+
each(:str?) &
|
43
|
+
# This is the ugliest, only way I found to define a dynamic validation tree.
|
44
|
+
(options['expand.limit'] ? max_size?(options['expand.limit']) : array?)
|
45
|
+
end
|
46
|
+
else
|
47
|
+
none? | empty?
|
48
|
+
end
|
49
|
+
end
|
31
50
|
end
|
32
51
|
|
33
52
|
options['result.contract.expand'] = options['contract.expand'].call(options['params'])
|
34
53
|
|
35
|
-
options['result.contract.expand'].errors.
|
36
|
-
|
54
|
+
if options['result.contract.expand'].errors.any?
|
55
|
+
options['result.response'] = Response::UnprocessableEntity.new(
|
56
|
+
errors: options['result.contract.expand'].errors
|
57
|
+
).decorate_with(Pragma::Decorator::Error)
|
37
58
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
59
|
+
return false
|
60
|
+
end
|
61
|
+
|
62
|
+
true
|
42
63
|
end
|
43
64
|
|
44
65
|
def validate_expansion(options, name)
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Pragma
|
4
|
+
module Operation
|
5
|
+
module Macro
|
6
|
+
def self.Filtering
|
7
|
+
step = ->(input, options) { Filtering.for(input, options) }
|
8
|
+
[step, name: 'filtering']
|
9
|
+
end
|
10
|
+
|
11
|
+
module Filtering
|
12
|
+
class << self
|
13
|
+
def for(_input, options)
|
14
|
+
set_defaults(options)
|
15
|
+
|
16
|
+
options['model'] = apply_filtering(options)
|
17
|
+
|
18
|
+
true
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def set_defaults(options)
|
24
|
+
{
|
25
|
+
'filtering.filters' => []
|
26
|
+
}.each_pair do |key, value|
|
27
|
+
options[key] = value unless options[key]
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def apply_filtering(options)
|
32
|
+
relation = options['model']
|
33
|
+
|
34
|
+
options['filtering.filters'].each do |filter|
|
35
|
+
value = options['params'][filter.param]
|
36
|
+
next unless value.present?
|
37
|
+
|
38
|
+
relation = filter.apply(relation: options['model'], value: value)
|
39
|
+
end
|
40
|
+
|
41
|
+
relation
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Pragma
|
4
|
+
module Operation
|
5
|
+
module Macro
|
6
|
+
def self.Ordering
|
7
|
+
step = ->(input, options) { Ordering.for(input, options) }
|
8
|
+
[step, name: 'ordering']
|
9
|
+
end
|
10
|
+
|
11
|
+
module Ordering
|
12
|
+
class << self
|
13
|
+
def for(_input, options)
|
14
|
+
set_defaults(options)
|
15
|
+
|
16
|
+
unless validate_params(options)
|
17
|
+
handle_invalid_contract(options)
|
18
|
+
return false
|
19
|
+
end
|
20
|
+
|
21
|
+
order_column = order_column(options)
|
22
|
+
|
23
|
+
if order_column
|
24
|
+
options['model'] = options['model'].order(order_column => order_direction(options))
|
25
|
+
end
|
26
|
+
|
27
|
+
true
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def set_defaults(options)
|
33
|
+
default_column = if options['model.class']&.method_defined?(:created_at)
|
34
|
+
:created_at
|
35
|
+
end
|
36
|
+
|
37
|
+
{
|
38
|
+
'ordering.columns' => [default_column].compact,
|
39
|
+
'ordering.default_column' => default_column,
|
40
|
+
'ordering.default_direction' => :desc,
|
41
|
+
'ordering.column_param' => :order_property,
|
42
|
+
'ordering.direction_param' => :order_direction
|
43
|
+
}.each_pair do |key, value|
|
44
|
+
options[key] = value unless options[key]
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def validate_params(options)
|
49
|
+
options['contract.ordering'] = Dry::Validation.Schema do
|
50
|
+
optional(options['ordering.column_param']).filled do
|
51
|
+
str? & included_in?(options['ordering.columns'].map(&:to_s))
|
52
|
+
end
|
53
|
+
optional(options['ordering.direction_param']).filled do
|
54
|
+
str? & included_in?(%w[asc desc ASC DESC])
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
options['result.contract.ordering'] = options['contract.ordering'].call(
|
59
|
+
options['params']
|
60
|
+
)
|
61
|
+
|
62
|
+
options['result.contract.ordering'].errors.empty?
|
63
|
+
end
|
64
|
+
|
65
|
+
def handle_invalid_contract(options)
|
66
|
+
options['result.response'] = Response::UnprocessableEntity.new(
|
67
|
+
errors: options['result.contract.ordering'].errors
|
68
|
+
).decorate_with(Pragma::Decorator::Error)
|
69
|
+
end
|
70
|
+
|
71
|
+
def order_column(options)
|
72
|
+
params = options['params']
|
73
|
+
params[options['ordering.column_param']] || options['ordering.default_column']
|
74
|
+
end
|
75
|
+
|
76
|
+
def order_direction(options)
|
77
|
+
params = options['params']
|
78
|
+
params[options['ordering.direction_param']] || options['ordering.default_direction']
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -34,7 +34,7 @@ module Pragma
|
|
34
34
|
'pagination.default_per_page' => 30,
|
35
35
|
'pagination.max_per_page' => 100
|
36
36
|
}.each_pair do |key, value|
|
37
|
-
options[key]
|
37
|
+
options[key] = value unless options[key]
|
38
38
|
end
|
39
39
|
end
|
40
40
|
|
@@ -53,10 +53,14 @@ module Pragma
|
|
53
53
|
def validate_params(options)
|
54
54
|
options['contract.pagination'] = Dry::Validation.Schema do
|
55
55
|
optional(options['pagination.page_param']).filled { int? & gteq?(1) }
|
56
|
-
optional(options['pagination.per_page_param']).filled
|
56
|
+
optional(options['pagination.per_page_param']).filled do
|
57
|
+
int? & (gteq?(1) & lteq?(options['pagination.max_per_page']))
|
58
|
+
end
|
57
59
|
end
|
58
60
|
|
59
|
-
options['result.contract.pagination'] = options['contract.pagination'].call(
|
61
|
+
options['result.contract.pagination'] = options['contract.pagination'].call(
|
62
|
+
options['params']
|
63
|
+
)
|
60
64
|
|
61
65
|
options['result.contract.pagination'].errors.empty?
|
62
66
|
end
|
@@ -1,7 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'trailblazer/operation/pundit'
|
4
|
-
|
5
3
|
module Pragma
|
6
4
|
module Operation
|
7
5
|
module Macro
|
@@ -31,7 +29,9 @@ module Pragma
|
|
31
29
|
private
|
32
30
|
|
33
31
|
def handle_unauthorized!(options)
|
34
|
-
options['result.response'] = Pragma::Operation::Response::Forbidden.new.decorate_with(
|
32
|
+
options['result.response'] = Pragma::Operation::Response::Forbidden.new.decorate_with(
|
33
|
+
Pragma::Decorator::Error
|
34
|
+
)
|
35
35
|
end
|
36
36
|
end
|
37
37
|
end
|
@@ -7,8 +7,8 @@ module Pragma
|
|
7
7
|
# @author Alessandro Desantis
|
8
8
|
class Show < Pragma::Operation::Base
|
9
9
|
step Macro::Classes()
|
10
|
-
step Macro::Model(:find_by)
|
11
|
-
step Macro::Policy()
|
10
|
+
step Macro::Model(:find_by)
|
11
|
+
step Macro::Policy()
|
12
12
|
step Macro::Decorator()
|
13
13
|
step :respond!
|
14
14
|
|
@@ -7,11 +7,11 @@ module Pragma
|
|
7
7
|
# @author Alessandro Desantis
|
8
8
|
class Update < Pragma::Operation::Base
|
9
9
|
step Macro::Classes()
|
10
|
-
step Macro::Model(:find_by)
|
11
|
-
step Macro::Policy()
|
10
|
+
step Macro::Model(:find_by)
|
11
|
+
step Macro::Policy()
|
12
12
|
step Macro::Contract::Build()
|
13
|
-
step Macro::Contract::Validate()
|
14
|
-
step Macro::Contract::Persist()
|
13
|
+
step Macro::Contract::Validate()
|
14
|
+
step Macro::Contract::Persist()
|
15
15
|
step Macro::Decorator()
|
16
16
|
step :respond!
|
17
17
|
|
data/lib/pragma/version.rb
CHANGED
data/pragma.gemspec
CHANGED
@@ -21,17 +21,17 @@ Gem::Specification.new do |spec|
|
|
21
21
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
22
22
|
spec.require_paths = ['lib']
|
23
23
|
|
24
|
-
spec.add_dependency 'trailblazer', '~> 2.0'
|
25
|
-
spec.add_dependency 'pragma-operation', '~> 2.0'
|
26
|
-
spec.add_dependency 'pragma-policy', '~> 2.0'
|
27
24
|
spec.add_dependency 'pragma-contract', '~> 2.0'
|
28
25
|
spec.add_dependency 'pragma-decorator', '~> 2.0'
|
26
|
+
spec.add_dependency 'pragma-operation', '~> 2.0'
|
27
|
+
spec.add_dependency 'pragma-policy', '~> 2.0'
|
28
|
+
spec.add_dependency 'trailblazer', '~> 2.0'
|
29
29
|
spec.add_dependency 'will_paginate', '~> 3.1'
|
30
30
|
|
31
31
|
spec.add_development_dependency 'bundler'
|
32
|
+
spec.add_development_dependency 'coveralls'
|
32
33
|
spec.add_development_dependency 'rake'
|
33
34
|
spec.add_development_dependency 'rspec'
|
34
35
|
spec.add_development_dependency 'rubocop'
|
35
36
|
spec.add_development_dependency 'rubocop-rspec'
|
36
|
-
spec.add_development_dependency 'coveralls'
|
37
37
|
end
|
metadata
CHANGED
@@ -1,17 +1,17 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: pragma
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Alessandro Desantis
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2018-01-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
|
-
name:
|
14
|
+
name: pragma-contract
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
@@ -25,7 +25,7 @@ dependencies:
|
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '2.0'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
|
-
name: pragma-
|
28
|
+
name: pragma-decorator
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
31
|
- - "~>"
|
@@ -39,7 +39,7 @@ dependencies:
|
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '2.0'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
|
-
name: pragma-
|
42
|
+
name: pragma-operation
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
45
|
- - "~>"
|
@@ -53,7 +53,7 @@ dependencies:
|
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '2.0'
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
|
-
name: pragma-
|
56
|
+
name: pragma-policy
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
58
58
|
requirements:
|
59
59
|
- - "~>"
|
@@ -67,7 +67,7 @@ dependencies:
|
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: '2.0'
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
|
-
name:
|
70
|
+
name: trailblazer
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
72
72
|
requirements:
|
73
73
|
- - "~>"
|
@@ -109,7 +109,7 @@ dependencies:
|
|
109
109
|
- !ruby/object:Gem::Version
|
110
110
|
version: '0'
|
111
111
|
- !ruby/object:Gem::Dependency
|
112
|
-
name:
|
112
|
+
name: coveralls
|
113
113
|
requirement: !ruby/object:Gem::Requirement
|
114
114
|
requirements:
|
115
115
|
- - ">="
|
@@ -123,7 +123,7 @@ dependencies:
|
|
123
123
|
- !ruby/object:Gem::Version
|
124
124
|
version: '0'
|
125
125
|
- !ruby/object:Gem::Dependency
|
126
|
-
name:
|
126
|
+
name: rake
|
127
127
|
requirement: !ruby/object:Gem::Requirement
|
128
128
|
requirements:
|
129
129
|
- - ">="
|
@@ -137,7 +137,7 @@ dependencies:
|
|
137
137
|
- !ruby/object:Gem::Version
|
138
138
|
version: '0'
|
139
139
|
- !ruby/object:Gem::Dependency
|
140
|
-
name:
|
140
|
+
name: rspec
|
141
141
|
requirement: !ruby/object:Gem::Requirement
|
142
142
|
requirements:
|
143
143
|
- - ">="
|
@@ -151,7 +151,7 @@ dependencies:
|
|
151
151
|
- !ruby/object:Gem::Version
|
152
152
|
version: '0'
|
153
153
|
- !ruby/object:Gem::Dependency
|
154
|
-
name: rubocop
|
154
|
+
name: rubocop
|
155
155
|
requirement: !ruby/object:Gem::Requirement
|
156
156
|
requirements:
|
157
157
|
- - ">="
|
@@ -165,7 +165,7 @@ dependencies:
|
|
165
165
|
- !ruby/object:Gem::Version
|
166
166
|
version: '0'
|
167
167
|
- !ruby/object:Gem::Dependency
|
168
|
-
name:
|
168
|
+
name: rubocop-rspec
|
169
169
|
requirement: !ruby/object:Gem::Requirement
|
170
170
|
requirements:
|
171
171
|
- - ">="
|
@@ -200,13 +200,19 @@ files:
|
|
200
200
|
- lib/pragma/decorator/error.rb
|
201
201
|
- lib/pragma/operation/create.rb
|
202
202
|
- lib/pragma/operation/destroy.rb
|
203
|
+
- lib/pragma/operation/filter/base.rb
|
204
|
+
- lib/pragma/operation/filter/equals.rb
|
205
|
+
- lib/pragma/operation/filter/ilike.rb
|
206
|
+
- lib/pragma/operation/filter/like.rb
|
203
207
|
- lib/pragma/operation/index.rb
|
204
208
|
- lib/pragma/operation/macro/classes.rb
|
205
209
|
- lib/pragma/operation/macro/contract/build.rb
|
206
210
|
- lib/pragma/operation/macro/contract/persist.rb
|
207
211
|
- lib/pragma/operation/macro/contract/validate.rb
|
208
212
|
- lib/pragma/operation/macro/decorator.rb
|
213
|
+
- lib/pragma/operation/macro/filtering.rb
|
209
214
|
- lib/pragma/operation/macro/model.rb
|
215
|
+
- lib/pragma/operation/macro/ordering.rb
|
210
216
|
- lib/pragma/operation/macro/pagination.rb
|
211
217
|
- lib/pragma/operation/macro/policy.rb
|
212
218
|
- lib/pragma/operation/show.rb
|