pragma 1.2.6 → 2.0.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 +4 -4
- data/.rubocop.yml +11 -8
- data/Gemfile +5 -0
- data/README.md +65 -10
- data/lib/pragma.rb +12 -1
- data/lib/pragma/decorator/error.rb +11 -0
- data/lib/pragma/operation/create.rb +14 -23
- data/lib/pragma/operation/destroy.rb +17 -15
- data/lib/pragma/operation/index.rb +19 -101
- data/lib/pragma/operation/macro/classes.rb +96 -0
- data/lib/pragma/operation/macro/contract/build.rb +27 -0
- data/lib/pragma/operation/macro/contract/persist.rb +27 -0
- data/lib/pragma/operation/macro/contract/validate.rb +27 -0
- data/lib/pragma/operation/macro/decorator.rb +61 -0
- data/lib/pragma/operation/macro/model.rb +27 -0
- data/lib/pragma/operation/macro/pagination.rb +92 -0
- data/lib/pragma/operation/macro/policy.rb +40 -0
- data/lib/pragma/operation/show.rb +8 -15
- data/lib/pragma/operation/update.rb +12 -23
- data/lib/pragma/version.rb +2 -1
- data/pragma.gemspec +7 -5
- metadata +34 -14
- data/doc/01-sensible-defaults.md +0 -50
- data/doc/02-crud-operations.md +0 -109
- data/lib/pragma/operation/defaults.rb +0 -83
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 27c896a2d1034183edbcae9079c7f435057e62da
|
4
|
+
data.tar.gz: bc5a709485d117f346b7d23880ab155e8d66392b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b754320e5db9566992162c53ab8f1db93f5f073021745a5975ac7ea3ddd4c2334ebcaf39fb69447191c8dc2e2efd7574b685fa0703b222655303f2b00d4ebe79
|
7
|
+
data.tar.gz: 6067ff68d0fbaa81cc5de835109650bb4b7d4811a0795f0a76fc7b5dc6cbca8b042cbd54ebb9ea489dcec737e7b0df755d02b7609b2e3ab610e77312df537d66
|
data/.rubocop.yml
CHANGED
@@ -24,26 +24,26 @@ Style/BlockDelimiters:
|
|
24
24
|
Exclude:
|
25
25
|
- 'spec/**/*'
|
26
26
|
|
27
|
-
|
27
|
+
Layout/AlignParameters:
|
28
28
|
EnforcedStyle: with_fixed_indentation
|
29
29
|
|
30
|
-
|
30
|
+
Layout/ClosingParenthesisIndentation:
|
31
31
|
Enabled: false
|
32
32
|
|
33
33
|
Metrics/LineLength:
|
34
34
|
Max: 100
|
35
35
|
AllowURI: true
|
36
36
|
|
37
|
-
|
37
|
+
Layout/FirstParameterIndentation:
|
38
38
|
Enabled: false
|
39
39
|
|
40
|
-
|
40
|
+
Layout/MultilineMethodCallIndentation:
|
41
41
|
EnforcedStyle: indented
|
42
42
|
|
43
|
-
|
43
|
+
Layout/IndentArray:
|
44
44
|
EnforcedStyle: consistent
|
45
45
|
|
46
|
-
|
46
|
+
Layout/IndentHash:
|
47
47
|
EnforcedStyle: consistent
|
48
48
|
|
49
49
|
Style/SignalException:
|
@@ -53,7 +53,7 @@ Style/BracesAroundHashParameters:
|
|
53
53
|
EnforcedStyle: context_dependent
|
54
54
|
|
55
55
|
Lint/EndAlignment:
|
56
|
-
|
56
|
+
EnforcedStyleAlignWith: variable
|
57
57
|
AutoCorrect: true
|
58
58
|
|
59
59
|
Style/AndOr:
|
@@ -68,7 +68,7 @@ RSpec/NamedSubject:
|
|
68
68
|
RSpec/ExampleLength:
|
69
69
|
Enabled: false
|
70
70
|
|
71
|
-
|
71
|
+
Layout/MultilineMethodCallBraceLayout:
|
72
72
|
Enabled: false
|
73
73
|
|
74
74
|
Metrics/MethodLength:
|
@@ -85,3 +85,6 @@ Metrics/CyclomaticComplexity:
|
|
85
85
|
|
86
86
|
Metrics/ClassLength:
|
87
87
|
Enabled: false
|
88
|
+
|
89
|
+
Metrics/BlockLength:
|
90
|
+
Enabled: false
|
data/Gemfile
CHANGED
@@ -4,3 +4,8 @@ source 'https://rubygems.org'
|
|
4
4
|
gemspec
|
5
5
|
|
6
6
|
gem 'pry'
|
7
|
+
|
8
|
+
gem 'pragma-policy', github: 'pragmarb/pragma-policy'
|
9
|
+
gem 'pragma-operation', github: 'pragmarb/pragma-operation'
|
10
|
+
gem 'pragma-contract', github: 'pragmarb/pragma-contract'
|
11
|
+
gem 'pragma-decorator', github: 'pragmarb/pragma-decorator'
|
data/README.md
CHANGED
@@ -24,25 +24,25 @@ Looking for a Rails integration? Check out [pragma-rails](https://github.com/pra
|
|
24
24
|
Pragma was created with a very specific goal in mind: to make the development of JSON APIs a matter
|
25
25
|
of hours, not days. In other words, Pragma is for JSON APIs what Rails is for web applications.
|
26
26
|
|
27
|
-
|
27
|
+
Here are the ground rules:
|
28
28
|
|
29
29
|
1. **Pragma is opinionated.** With Pragma, you don't get to make a lot of choices and that's
|
30
30
|
_exactly_ why people are using it: they want to focus on the business logic of their API rather
|
31
31
|
than the useless details. We understand this approach will not work in some cases and that's
|
32
32
|
alright. If you need more personalization, only use a subset of Pragma (see item 2) or something
|
33
33
|
else.
|
34
|
-
2. **Pragma is modular.** Pragma is built as a set of gems (currently
|
34
|
+
2. **Pragma is modular.** Pragma is built as a set of gems (currently 6), plus some standalone
|
35
35
|
tools. You can pick one or more modules and use them in your application as you see fit. Even
|
36
36
|
though they are completely independent from each other, they nicely integrate and work best when
|
37
37
|
used together, creating an ecosystem that will dramatically speed up your design and development
|
38
38
|
process.
|
39
|
-
3. **Pragma is
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
39
|
+
3. **Pragma is designed to be Rails-free.** Just as what happens with Trailblazer, our Rails
|
40
|
+
integration is decoupled from the rest of the ecosystem and all of the gems can be used without
|
41
|
+
Rails. This is just a byproduct of the project's design: Pragma is built with pure Ruby.
|
42
|
+
[pragma-rails](https://github.com/pragmarb/pragma-rails) is the only available framework
|
43
|
+
integration at the moment, but more will come!
|
44
44
|
|
45
|
-
|
45
|
+
### Why not Trailblazer?
|
46
46
|
|
47
47
|
[Trailblazer](https://github.com/trailblazer/trailblazer) and all of its companion projects are
|
48
48
|
awesome. They are so awesome that Pragma is built on top of them: even though we're not using
|
@@ -50,7 +50,8 @@ the Trailblazer gem itself yet, many of the Pragma gems are simply extensions of
|
|
50
50
|
counterparts:
|
51
51
|
|
52
52
|
- decorators are [ROAR representers](https://github.com/apotonick/roar);
|
53
|
-
- contracts are [Reform forms](https://github.com/apotonick/reform)
|
53
|
+
- contracts are [Reform forms](https://github.com/apotonick/reform);
|
54
|
+
- operations are [Trailblazer operations](https://github.com/trailblazer/trailblazer-operation).
|
54
55
|
|
55
56
|
Trailblazer and Pragma have different (but similar) places in the Ruby world: Trailblazer is an
|
56
57
|
architecture for building all kinds of web applications in an intelligent, rational way, while
|
@@ -82,7 +83,61 @@ $ gem install pragma
|
|
82
83
|
|
83
84
|
## Usage
|
84
85
|
|
85
|
-
|
86
|
+
### Project Structure
|
87
|
+
|
88
|
+
This gem works best if you follow the recommended structure for organizing resources:
|
89
|
+
|
90
|
+
```
|
91
|
+
└── api
|
92
|
+
└── v1
|
93
|
+
└── article
|
94
|
+
├── contract
|
95
|
+
│ ├── create.rb
|
96
|
+
│ └── update.rb
|
97
|
+
├── operation
|
98
|
+
│ ├── create.rb
|
99
|
+
│ ├── destroy.rb
|
100
|
+
│ ├── index.rb
|
101
|
+
│ └── update.rb
|
102
|
+
└── policy.rb
|
103
|
+
└── decorator.rb
|
104
|
+
```
|
105
|
+
|
106
|
+
Your modules and classes would, of course, follow the same structure: `API::V1::Article::Policy` and
|
107
|
+
so on and so forth.
|
108
|
+
|
109
|
+
If you adhere to this structure, the gem will be able to locate all of your classes without any
|
110
|
+
explicit configuration. This will save you a lot of time and is highly recommended.
|
111
|
+
|
112
|
+
### Fantastic Five
|
113
|
+
|
114
|
+
Pragma comes with five built-in operations, often referred to as Fantastic Five (or "FF" for
|
115
|
+
brevity). They are, of course, Index, Show, Create, Update and Destroy.
|
116
|
+
|
117
|
+
These operations leverage the full power of the integrated Pragma ecosystem and require all four
|
118
|
+
components to be properly installed and configured in your application. You may reconfigure them
|
119
|
+
to skip some of the steps, but it is highly recommended to use them as they come.
|
120
|
+
|
121
|
+
You can find these operations under [lib/pragma/operation](https://github.com/pragmarb/pragma/tree/master/lib/pragma/operation).
|
122
|
+
To use them, simply create your own operations and inherit from ours. For instance:
|
123
|
+
|
124
|
+
```ruby
|
125
|
+
module API
|
126
|
+
module V1
|
127
|
+
module Article
|
128
|
+
module Operation
|
129
|
+
class Create < Pragma::Operation::Create
|
130
|
+
# 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
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
```
|
86
141
|
|
87
142
|
## Contributing
|
88
143
|
|
data/lib/pragma.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
require 'pragma/operation'
|
3
4
|
require 'pragma/policy'
|
4
5
|
require 'pragma/contract'
|
@@ -9,7 +10,17 @@ require 'will_paginate/array'
|
|
9
10
|
|
10
11
|
require 'pragma/version'
|
11
12
|
|
12
|
-
require 'pragma/
|
13
|
+
require 'pragma/decorator/error'
|
14
|
+
|
15
|
+
require 'pragma/operation/macro/classes'
|
16
|
+
require 'pragma/operation/macro/decorator'
|
17
|
+
require 'pragma/operation/macro/pagination'
|
18
|
+
require 'pragma/operation/macro/policy'
|
19
|
+
require 'pragma/operation/macro/model'
|
20
|
+
require 'pragma/operation/macro/contract/build'
|
21
|
+
require 'pragma/operation/macro/contract/validate'
|
22
|
+
require 'pragma/operation/macro/contract/persist'
|
23
|
+
|
13
24
|
require 'pragma/operation/index'
|
14
25
|
require 'pragma/operation/show'
|
15
26
|
require 'pragma/operation/create'
|
@@ -1,33 +1,24 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
module Pragma
|
3
4
|
module Operation
|
4
|
-
#
|
5
|
-
# responds with the decorated record.
|
5
|
+
# Creates a new record and responds with the decorated record.
|
6
6
|
#
|
7
7
|
# @author Alessandro Desantis
|
8
8
|
class Create < Pragma::Operation::Base
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
context.contract.save
|
19
|
-
context.record.save!
|
20
|
-
|
21
|
-
respond_with status: :created, resource: decorate(context.record)
|
22
|
-
end
|
23
|
-
|
24
|
-
protected
|
9
|
+
step Macro::Classes()
|
10
|
+
step Macro::Model()
|
11
|
+
step Macro::Policy(), fail_fast: true
|
12
|
+
step Macro::Contract::Build()
|
13
|
+
step Macro::Contract::Validate(), fail_fast: true
|
14
|
+
step Macro::Contract::Persist(), fail_fast: true
|
15
|
+
step Macro::Decorator()
|
16
|
+
step :respond!
|
25
17
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
self.class.model_klass.new
|
18
|
+
def respond!(options)
|
19
|
+
options['result.response'] = Response::Created.new(
|
20
|
+
entity: options['result.decorator.instance']
|
21
|
+
)
|
31
22
|
end
|
32
23
|
end
|
33
24
|
end
|
@@ -1,28 +1,30 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
module Pragma
|
3
4
|
module Operation
|
4
|
-
# Finds
|
5
|
+
# Finds an existing record, destroys it and responds 204 No Content.
|
5
6
|
#
|
6
7
|
# @author Alessandro Desantis
|
7
8
|
class Destroy < Pragma::Operation::Base
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
9
|
+
step Macro::Classes()
|
10
|
+
step Macro::Model(:find_by), fail_fast: true
|
11
|
+
step Macro::Policy(), fail_fast: true
|
12
|
+
step :destroy!
|
13
|
+
failure :handle_invalid_model!, fail_fast: true
|
14
|
+
step :respond!
|
13
15
|
|
14
|
-
|
15
|
-
|
16
|
-
head :no_content
|
16
|
+
def destroy!(_options, model:, **)
|
17
|
+
model.destroy
|
17
18
|
end
|
18
19
|
|
19
|
-
|
20
|
+
def handle_invalid_model!(options, model:, **)
|
21
|
+
options['result.response'] = Response::UnprocessableEntity.new(
|
22
|
+
errors: model.errors
|
23
|
+
).decorate_with(Decorator::Error)
|
24
|
+
end
|
20
25
|
|
21
|
-
|
22
|
-
|
23
|
-
# @return [Object]
|
24
|
-
def find_record
|
25
|
-
self.class.model_klass.find(params[:id])
|
26
|
+
def respond!(options)
|
27
|
+
options['result.response'] = Response::NoContent.new
|
26
28
|
end
|
27
29
|
end
|
28
30
|
end
|
@@ -1,115 +1,33 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'trailblazer/dsl'
|
4
|
+
|
2
5
|
module Pragma
|
3
6
|
module Operation
|
4
|
-
# Finds all records of the requested resource, authorizes them, paginates them and
|
5
|
-
#
|
7
|
+
# Finds all records of the requested resource, authorizes them, paginates them and decorates
|
8
|
+
# them.
|
6
9
|
#
|
7
10
|
# @author Alessandro Desantis
|
8
11
|
class Index < Pragma::Operation::Base
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
context.records = context.records.paginate(page: page, per_page: per_page)
|
16
|
-
rescue RangeError => e
|
17
|
-
respond_with!(
|
18
|
-
status: :bad_request,
|
19
|
-
resource: {
|
20
|
-
error_type: :invalid_page,
|
21
|
-
error_message: e.message
|
22
|
-
}
|
23
|
-
)
|
24
|
-
end
|
25
|
-
|
26
|
-
respond_with(
|
27
|
-
resource: decorate(context.records),
|
28
|
-
status: :ok,
|
29
|
-
headers: {
|
30
|
-
'Page' => context.records.current_page.to_i,
|
31
|
-
'Per-Page' => context.records.per_page,
|
32
|
-
'Total' => context.records.total_entries
|
33
|
-
},
|
34
|
-
links: {
|
35
|
-
first: build_page_url(1),
|
36
|
-
last: build_page_url(context.records.total_pages),
|
37
|
-
next: (build_page_url(context.records.next_page) if context.records.next_page),
|
38
|
-
prev: (build_page_url(context.records.previous_page) if context.records.previous_page)
|
39
|
-
}
|
40
|
-
)
|
41
|
-
end
|
42
|
-
|
43
|
-
protected
|
44
|
-
|
45
|
-
# Finds all the records. By default, calls +.all+ on the model class, which is inferred from
|
46
|
-
# the operation's namespace (e.g. +API::V1::Post::Operation::Index+ will retrieve all records
|
47
|
-
# of the +Post+ model).
|
48
|
-
#
|
49
|
-
# @return [Enumerable]
|
50
|
-
def find_records
|
51
|
-
self.class.model_klass.all
|
52
|
-
end
|
12
|
+
step Macro::Classes()
|
13
|
+
step :retrieve!
|
14
|
+
step :scope!
|
15
|
+
step Macro::Pagination(), fail_fast: true
|
16
|
+
step Macro::Decorator(name: :collection), fail_fast: true
|
17
|
+
step :respond!
|
53
18
|
|
54
|
-
|
55
|
-
|
56
|
-
# @return [Symbol]
|
57
|
-
def page_param
|
58
|
-
:page
|
19
|
+
def retrieve!(options)
|
20
|
+
options['model'] = options['model.class'].all
|
59
21
|
end
|
60
22
|
|
61
|
-
|
62
|
-
|
63
|
-
# @return [Fixnum]
|
64
|
-
#
|
65
|
-
# @see #page_param
|
66
|
-
def page
|
67
|
-
return 1 if !params[page_param] || params[page_param].empty?
|
68
|
-
params[page_param].to_i
|
23
|
+
def scope!(options, current_user:, model:, **)
|
24
|
+
options['model'] = options['policy.default.scope.class'].new(current_user, model).resolve
|
69
25
|
end
|
70
26
|
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
:per_page
|
76
|
-
end
|
77
|
-
|
78
|
-
# Returns the default number of records per page.
|
79
|
-
#
|
80
|
-
# @return [Fixnum]
|
81
|
-
def default_per_page
|
82
|
-
30
|
83
|
-
end
|
84
|
-
|
85
|
-
# Returns the maximum number of records per page.
|
86
|
-
#
|
87
|
-
# @return [Fixnum]
|
88
|
-
def max_per_page
|
89
|
-
100
|
90
|
-
end
|
91
|
-
|
92
|
-
# Returns the number of records to include per page. By default, this is the +per_page+
|
93
|
-
# parameter, up to a maximum of {#max_per_page} records, or {#default_per_page} if the
|
94
|
-
# parameter is not present.
|
95
|
-
#
|
96
|
-
# @return [Fixnum]
|
97
|
-
#
|
98
|
-
# @see #default_per_page
|
99
|
-
# @see #max_per_page
|
100
|
-
# @see #per_page_param
|
101
|
-
def per_page
|
102
|
-
return default_per_page if !params[per_page_param] || params[per_page_param].empty?
|
103
|
-
params[per_page_param].to_i > max_per_page ? max_per_page : params[per_page_param].to_i
|
104
|
-
end
|
105
|
-
|
106
|
-
# Builds the URL to a specific page in the collection.
|
107
|
-
#
|
108
|
-
# @param page [Fixnum] a page number
|
109
|
-
#
|
110
|
-
# @return [String]
|
111
|
-
def build_page_url(_page)
|
112
|
-
nil
|
27
|
+
def respond!(options, model:, **)
|
28
|
+
options['result.response'] = Response::Ok.new(
|
29
|
+
entity: options['result.decorator.collection']
|
30
|
+
)
|
113
31
|
end
|
114
32
|
end
|
115
33
|
end
|