pragma 2.0.0 → 2.1.0

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
  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