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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 27c896a2d1034183edbcae9079c7f435057e62da
4
- data.tar.gz: bc5a709485d117f346b7d23880ab155e8d66392b
3
+ metadata.gz: 423424f1618e9c1e91217573586e6e10b70b5a06
4
+ data.tar.gz: 77cfe2fd44b3f5df6f22563345b9ae61d48413e4
5
5
  SHA512:
6
- metadata.gz: b754320e5db9566992162c53ab8f1db93f5f073021745a5975ac7ea3ddd4c2334ebcaf39fb69447191c8dc2e2efd7574b685fa0703b222655303f2b00d4ebe79
7
- data.tar.gz: 6067ff68d0fbaa81cc5de835109650bb4b7d4811a0795f0a76fc7b5dc6cbca8b042cbd54ebb9ea489dcec737e7b0df755d02b7609b2e3ab610e77312df537d66
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
- ## Unreleased
3
+ All notable changes to this project will be documented in this file.
4
4
 
5
- - Removals
6
- - Bugfixes
7
- - Enhancements
8
- - Deprecation
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://img.shields.io/travis/pragmarb/pragma.svg?maxAge=3600&style=flat-square)](https://travis-ci.org/pragmarb/pragma)
4
- [![Dependency Status](https://img.shields.io/gemnasium/pragmarb/pragma.svg?maxAge=3600&style=flat-square)](https://gemnasium.com/github.com/pragmarb/pragma)
5
- [![Code Climate](https://img.shields.io/codeclimate/github/pragmarb/pragma.svg?maxAge=3600&style=flat-square)](https://codeclimate.com/github/pragmarb/pragma)
6
- [![Coveralls](https://img.shields.io/coveralls/pragmarb/pragma.svg?maxAge=3600&style=flat-square)](https://coveralls.io/github/pragmarb/pragma)
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, a pragmatic (duh!), opinionated architecture for building JSON APIs with Ruby!
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
- # - a policy that responds to #create?
132
- # - a Create contract
133
- # - a decorator
134
- # - an Article model
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(), fail_fast: true
11
+ step Macro::Policy()
12
12
  step Macro::Contract::Build()
13
- step Macro::Contract::Validate(), fail_fast: true
14
- step Macro::Contract::Persist(), fail_fast: true
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), fail_fast: true
11
- step Macro::Policy(), fail_fast: true
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
- end
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
- def handle_invalid_model!(options, model:, **)
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
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pragma
4
+ module Operation
5
+ module Filter
6
+ class Equals < Base
7
+ def apply(relation:, value:)
8
+ relation.where(column => value)
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pragma
4
+ module Operation
5
+ module Filter
6
+ class Ilike < Base
7
+ def apply(relation:, value:)
8
+ relation.where("#{column} ILIKE ?", "%#{value}%")
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pragma
4
+ module Operation
5
+ module Filter
6
+ class Like < Base
7
+ def apply(relation:, value:)
8
+ relation.where("#{column} LIKE ?", "%#{value}%")
9
+ end
10
+ end
11
+ end
12
+ end
13
+ 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::Pagination(), fail_fast: true
16
- step Macro::Decorator(name: :collection), fail_fast: true
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, model:, **)
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
- nil,
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
- unless validate_params(options)
15
- handle_invalid_contract(options)
16
- return false
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).each(:str?)
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.empty?
36
- end
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
- def handle_invalid_contract(options)
39
- options['result.response'] = Response::UnprocessableEntity.new(
40
- errors: options['result.contract.expand'].errors
41
- ).decorate_with(Pragma::Decorator::Error)
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
@@ -20,7 +20,10 @@ module Pragma
20
20
  end
21
21
  end
22
22
 
23
- [step, name: 'model.build']
23
+ [step, name: "model.#{action || 'build'}"]
24
+ end
25
+
26
+ module Model
24
27
  end
25
28
  end
26
29
  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] ||= value
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 { int? & (gteq?(1) & lteq?(options['pagination.max_per_page'])) }
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(options['params'])
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(Pragma::Decorator::Error)
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), fail_fast: true
11
- step Macro::Policy(), fail_fast: true
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), fail_fast: true
11
- step Macro::Policy(), fail_fast: true
10
+ step Macro::Model(:find_by)
11
+ step Macro::Policy()
12
12
  step Macro::Contract::Build()
13
- step Macro::Contract::Validate(), fail_fast: true
14
- step Macro::Contract::Persist(), fail_fast: true
13
+ step Macro::Contract::Validate()
14
+ step Macro::Contract::Persist()
15
15
  step Macro::Decorator()
16
16
  step :respond!
17
17
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pragma
4
- VERSION = '2.0.0'
4
+ VERSION = '2.1.0'
5
5
  end
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.0.0
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: 2017-09-27 00:00:00.000000000 Z
11
+ date: 2018-01-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: trailblazer
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-operation
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-policy
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-contract
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: pragma-decorator
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: rake
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: rspec
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: rubocop
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-rspec
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: coveralls
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