pragma-operation 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 7fcd5701aaa0aa49b20295bd71d6ddc588497703
4
+ data.tar.gz: 93ba0c1bc4babd8702c1cf60157ed2ea9d9ec0fd
5
+ SHA512:
6
+ metadata.gz: 0ef5a8d2a21ec06508545284dd72d51fe9a4a964b3003bb188def99b642a4c0f06213f308fbb69c4e0460f2eab33f36171e79b3d46d574829d0bfdb48315e937
7
+ data.tar.gz: 02c4231ed617768e829dc551c3e7d40e1da8aca12808d2f6c7813ffdf124a8216662bc847f83fd3b642211b30ec86c0e09671864c49aa35f5c25010787dd81bc
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ /spec/examples.txt
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,87 @@
1
+ require: rubocop-rspec
2
+
3
+ AllCops:
4
+ TargetRubyVersion: 2.3
5
+ Include:
6
+ - '**/Gemfile'
7
+ - '**/Rakefile'
8
+ Exclude:
9
+ - 'bin/*'
10
+ - 'db/**/*'
11
+ - 'vendor/bundle/**/*'
12
+ - 'spec/spec_helper.rb'
13
+ - 'spec/rails_helper.rb'
14
+ - 'spec/support/**/*'
15
+ - 'config/**/*'
16
+ - '**/Rakefile'
17
+ - '**/Gemfile'
18
+
19
+ RSpec/DescribeClass:
20
+ Exclude:
21
+ - 'spec/requests/**/*'
22
+
23
+ Style/BlockDelimiters:
24
+ Exclude:
25
+ - 'spec/**/*'
26
+
27
+ Style/AlignParameters:
28
+ EnforcedStyle: with_fixed_indentation
29
+
30
+ Style/ClosingParenthesisIndentation:
31
+ Enabled: false
32
+
33
+ Metrics/LineLength:
34
+ Max: 100
35
+ AllowURI: true
36
+
37
+ Style/FirstParameterIndentation:
38
+ Enabled: false
39
+
40
+ Style/MultilineMethodCallIndentation:
41
+ EnforcedStyle: indented
42
+
43
+ Style/IndentArray:
44
+ EnforcedStyle: consistent
45
+
46
+ Style/IndentHash:
47
+ EnforcedStyle: consistent
48
+
49
+ Style/SignalException:
50
+ EnforcedStyle: semantic
51
+
52
+ Style/BracesAroundHashParameters:
53
+ EnforcedStyle: context_dependent
54
+
55
+ Lint/EndAlignment:
56
+ AlignWith: variable
57
+ AutoCorrect: true
58
+
59
+ Style/AndOr:
60
+ EnforcedStyle: conditionals
61
+
62
+ Style/MultilineBlockChain:
63
+ Enabled: false
64
+
65
+ RSpec/NamedSubject:
66
+ Enabled: false
67
+
68
+ RSpec/ExampleLength:
69
+ Enabled: false
70
+
71
+ Style/MultilineMethodCallBraceLayout:
72
+ Enabled: false
73
+
74
+ Metrics/MethodLength:
75
+ Enabled: false
76
+
77
+ Metrics/AbcSize:
78
+ Enabled: false
79
+
80
+ Metrics/PerceivedComplexity:
81
+ Enabled: false
82
+
83
+ Metrics/CyclomaticComplexity:
84
+ Enabled: false
85
+
86
+ Metrics/ClassLength:
87
+ Enabled: false
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.3.1
5
+ before_install: gem install bundler -v 1.13.3
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in pragma-operation.gemspec
4
+ gemspec
5
+
6
+ gem 'pragma-policy', github: 'pragmarb/pragma-policy'
7
+ gem 'pragma-contract', github: 'pragmarb/pragma-contract'
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 Alessandro Desantis
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,43 @@
1
+ # Pragma::Operation
2
+
3
+ [![Build Status](https://img.shields.io/travis/pragmarb/pragma-operation.svg?maxAge=3600&style=flat-square)](https://travis-ci.org/pragmarb/pragma-operation)
4
+ [![Dependency Status](https://img.shields.io/gemnasium/pragmarb/pragma-operation.svg?maxAge=3600&style=flat-square)](https://gemnasium.com/github.com/pragmarb/pragma-operation)
5
+ [![Code Climate](https://img.shields.io/codeclimate/github/pragmarb/pragma-operation.svg?maxAge=3600&style=flat-square)](https://codeclimate.com/github/pragmarb/pragma-operation)
6
+ [![Coveralls](https://img.shields.io/coveralls/pragmarb/pragma-operation.svg?maxAge=3600&style=flat-square)](https://coveralls.io/github/pragmarb/pragma-operation)
7
+
8
+ Operations encapsulate the business logic of your JSON API.
9
+
10
+ They are built on top of the awesome [Interactor](https://github.com/collectiveidea/interactor) gem.
11
+
12
+ ## Installation
13
+
14
+ Add this line to your application's Gemfile:
15
+
16
+ ```ruby
17
+ gem 'pragma-operation'
18
+ ```
19
+
20
+ And then execute:
21
+
22
+ ```console
23
+ $ bundle
24
+ ```
25
+
26
+ Or install it yourself as:
27
+
28
+ ```console
29
+ $ gem install pragma-operation
30
+ ```
31
+
32
+ ## Usage
33
+
34
+ All documentation is in the [doc](https://github.com/pragmarb/pragma-operation/tree/master/doc)
35
+ folder.
36
+
37
+ ## Contributing
38
+
39
+ Bug reports and pull requests are welcome on GitHub at https://github.com/pragmarb/pragma-operation.
40
+
41
+ ## License
42
+
43
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "pragma/operation"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,145 @@
1
+ # Basic usage
2
+
3
+ Here's the simplest operation you can write:
4
+
5
+ ```ruby
6
+ module API
7
+ module V1
8
+ module Ping
9
+ module Operation
10
+ class Create < Pragma::Operation::Base
11
+ def call
12
+ respond_with status: :ok, resource: { pong: params[:pong] }
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ ```
20
+
21
+ Here's how you use it:
22
+
23
+ ```ruby
24
+ result = API::V1::Ping::Operation::Create.call(params: { pong: 'HELLO' })
25
+
26
+ result.status # => :ok
27
+ result.resource # => { pong: 'HELLO' }
28
+ ```
29
+
30
+ As you can see, an operation takes parameters as input and responds with:
31
+
32
+ - an HTTP status code;
33
+ - (optional) a resource (i.e. an object implementing `#to_json`).
34
+
35
+ If you don't want to return a resource, you can use the `#head` shortcut:
36
+
37
+ ```ruby
38
+ module API
39
+ module V1
40
+ module Ping
41
+ module Operation
42
+ class Create < Pragma::Operation::Base
43
+ def call
44
+ head :no_content
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ ```
52
+
53
+ Since Pragma::Operation is built on top of [Interactor](https://github.com/collectiveidea/interactor),
54
+ you should consult its documentation for the basic usage of operations; the rest of this section
55
+ only covers the features provided specifically by Pragma::Operation.
56
+
57
+ ## Handling errors
58
+
59
+ You can use the `#success?` and `#failure?` method to check whether an operation was successful. An
60
+ operation is considered successful when it returns a 2xx or 3xx status code:
61
+
62
+ ```ruby
63
+ module API
64
+ module V1
65
+ module Ping
66
+ module Operation
67
+ class Create < Pragma::Operation::Base
68
+ def call
69
+ if params[:pong].blank?
70
+ return respond_with(
71
+ status: :unprocessable_entity,
72
+ resource: {
73
+ error_type: :missing_pong,
74
+ error_message: "You must provide a 'pong' parameter."
75
+ }
76
+ )
77
+ end
78
+
79
+ respond_with status: :ok, resource: { pong: params[:pong] }
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
86
+ ```
87
+
88
+ Once more, here's an example usage of the above operation:
89
+
90
+ ```ruby
91
+ result1 = API::V1::Ping::Operation::Create.call(params: { pong: '' })
92
+ result1.success? # => false
93
+
94
+ result2 = API::V1::Ping::Operation::Create.call(params: { pong: 'HELLO' })
95
+ result2.success? # => true
96
+ ```
97
+
98
+ ## Halting the execution
99
+
100
+ Both `#respond_with` and `#head` provide bang counterparts that halt the execution of the operation.
101
+ They are useful, for instance, in before callbacks.
102
+
103
+ The above operation can be rewritten like this:
104
+
105
+ ```ruby
106
+ module API
107
+ module V1
108
+ module Ping
109
+ module Operation
110
+ class Create < Pragma::Operation::Base
111
+ before :validate_params
112
+
113
+ def call
114
+ respond_with status: :ok, resource: { pong: params[:pong] }
115
+ end
116
+
117
+ private
118
+
119
+ def validate_params
120
+ if params[:pong].blank?
121
+ respond_with!(
122
+ status: :unprocessable_entity,
123
+ resource: {
124
+ error_type: :missing_pong,
125
+ error_message: "You must provide a 'pong' parameter."
126
+ }
127
+ )
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
135
+ ```
136
+
137
+ The result is identical:
138
+
139
+ ```ruby
140
+ result1 = API::V1::Ping::Operation::Create.call(params: { pong: '' })
141
+ result1.success? # => false
142
+
143
+ result2 = API::V1::Ping::Operation::Create.call(params: { pong: 'HELLO' })
144
+ result2.success? # => true
145
+ ```
@@ -0,0 +1,92 @@
1
+ # Contracts
2
+
3
+ Operations integrate with [Pragma::Contract](https://github.com/pragmarb/pragma-contract). You can
4
+ specify the contract to use with `#contract` and get access to `#validate` and `#validate!` in your
5
+ operations:
6
+
7
+ ```ruby
8
+ module API
9
+ module V1
10
+ module Post
11
+ module Operation
12
+ class Create < Pragma::Operation::Base
13
+ contract API::V1::Post::Contract::Create
14
+
15
+ def call
16
+ post = Post.new
17
+
18
+ validate! contract
19
+ contract.save
20
+
21
+ respond_with status: :created, resource: post
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ ```
29
+
30
+ If the contract passes validation, then all is good. If not, an error is raised:
31
+
32
+ ```ruby
33
+ result1 = API::V1::Post::Operation::Create.call(params: {
34
+ title: 'My First Post',
35
+ body: 'Hello everyone, this is my first post!'
36
+ })
37
+
38
+ result1.status # => :created
39
+ result1.resource
40
+ # => {
41
+ # 'title' => 'My First Post'
42
+ # 'body' => 'Hello everyone, this is my first post!'
43
+ # }
44
+
45
+ result2 = API::V1::Post::Operation::Create.call(params: {
46
+ title: 'My First Post'
47
+ })
48
+
49
+ result2.status # => :forbidden
50
+ result2.resource
51
+ # => {
52
+ # 'error_type' => 'unprocessable_entity',
53
+ # 'error_message' => 'The contract for this operation was not respected.',
54
+ # 'meta' => {
55
+ # 'errors' => {
56
+ # 'body' => ["can't be blank"]
57
+ # }
58
+ # }
59
+ # }
60
+ ```
61
+
62
+ You can also use the non-bang method `#validate` to implement your own logic:
63
+
64
+ ```ruby
65
+ module API
66
+ module V1
67
+ module Post
68
+ module Operation
69
+ class Create < Pragma::Operation::Base
70
+ contract API::V1::Post::Contract::Create
71
+
72
+ def call
73
+ post = Post.new
74
+ contract = build_contract(post)
75
+
76
+ unless validate(contract)
77
+ respond_with!(
78
+ status: :unprocessable_entity,
79
+ resource: nil
80
+ )
81
+ end
82
+
83
+ contract.save
84
+
85
+ respond_with status: :created, resource: post
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
92
+ ```
@@ -0,0 +1,94 @@
1
+ # Policies
2
+
3
+ Operations integrate with [Pragma::Policy](https://github.com/pragmarb/pragma-policy). All you have
4
+ to do is specify the policy class with `#policy`. This will give you access to `#authorize` and
5
+ `#authorize!`:
6
+
7
+ ```ruby
8
+ module API
9
+ module V1
10
+ module Post
11
+ module Operation
12
+ class Create < Pragma::Operation::Base
13
+ policy API::V1::Post::Policy
14
+
15
+ def call
16
+ post = Post.new(params)
17
+ authorize! post
18
+
19
+ post.save!
20
+
21
+ respond_with status: :created, resource: post
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ ```
29
+
30
+ Of course, you will now have to pass the user performing the operation in addition to the usual
31
+ parameters:
32
+
33
+ ```ruby
34
+ result1 = API::V1::Post::Operation::Create.call(
35
+ params: {
36
+ title: 'My First Post',
37
+ body: 'Hello everyone, this is my first post!'
38
+ },
39
+ current_user: authorized_user
40
+ )
41
+
42
+ result1.status # => :created
43
+ result1.resource
44
+ # => {
45
+ # 'title' => 'My First Post'
46
+ # 'body' => 'Hello everyone, this is my first post!'
47
+ # }
48
+
49
+ result2 = API::V1::Post::Operation::Create.call(
50
+ params: {
51
+ title: 'My First Post',
52
+ body: 'Hello everyone, this is my first post!'
53
+ },
54
+ current_user: unauthorized_user
55
+ )
56
+
57
+ result2.status # => :forbidden
58
+ result2.resource
59
+ # => {
60
+ # 'error_type' => 'forbidden',
61
+ # 'error_message' => 'You are not authorized to perform this operation.'
62
+ # }
63
+ ```
64
+
65
+ If you want to customize how you handle authorization, you can use the non-bang method `#authorize`:
66
+
67
+ ```ruby
68
+ module API
69
+ module V1
70
+ module Post
71
+ module Operation
72
+ class Create < Pragma::Operation::Base
73
+ policy API::V1::Post::Policy
74
+
75
+ def call
76
+ post = Post.new(params)
77
+
78
+ unless authorize(post)
79
+ respond_with!(
80
+ status: :forbidden,
81
+ resource: nil # if you don't need error info
82
+ )
83
+ end
84
+
85
+ post.save!
86
+
87
+ respond_with status: :created, resource: post
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
94
+ ```
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+ require 'interactor'
3
+
4
+ require 'pragma/operation/version'
5
+ require 'pragma/operation/base'
6
+ require 'pragma/operation/authorization'
7
+ require 'pragma/operation/validation'
8
+
9
+ module Pragma
10
+ # Operations provide business logic encapsulation for your JSON API.
11
+ #
12
+ # @author Alessandro Desantis
13
+ module Operation
14
+ end
15
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+ module Pragma
3
+ module Operation
4
+ module Authorization
5
+ def self.included(base)
6
+ base.extend ClassMethods
7
+ base.include InstanceMethods
8
+ end
9
+
10
+ module ClassMethods
11
+ # Sets the contract to use for validating this operation.
12
+ #
13
+ # @param klass [Class] a subclass of +Pragma::Contract::Base+
14
+ def contract(klass)
15
+ @contract = klass
16
+ end
17
+
18
+ # Builds the contract for the given resource, using the previous defined contract class.
19
+ #
20
+ # @param resource [Object]
21
+ #
22
+ # @return [Pragma::Contract::Base]
23
+ #
24
+ # @see #contract
25
+ def build_contract(resource)
26
+ @contract.new(resource)
27
+ end
28
+ end
29
+
30
+ module InstanceMethods
31
+ # Builds the policy for the current user and the given resource, using the previously
32
+ # defined policy class.
33
+ #
34
+ # @param resource [Object]
35
+ #
36
+ # @return [Pragma::Policy::Base]
37
+ #
38
+ # @see .policy
39
+ # @see .build_policy
40
+ def build_policy(resource)
41
+ self.class.build_policy(user: current_user, resource: resource)
42
+ end
43
+
44
+ # Authorizes this operation on the provided resource or policy.
45
+ #
46
+ # @param authorizable [Pragma::Policy::Base|Object] resource or policy
47
+ #
48
+ # @return [Boolean] whether the operation is authorized
49
+ def authorize(authorizable)
50
+ policy = if defined?(Pragma::Policy::Base) && authorizable.is_a?(Pragma::Policy::Base)
51
+ authorizable
52
+ else
53
+ build_policy(authorizable)
54
+ end
55
+
56
+ policy.send("#{self.class.operation_name}?")
57
+ end
58
+
59
+ # Authorizes this operation on the provided resource or policy. If the user is not
60
+ # authorized to perform the operation, responds with 403 Forbidden and an error body and
61
+ # halts the execution.
62
+ #
63
+ # @param authorizable [Pragma::Policy::Base|Object] resource or policy
64
+ def authorize!(authorizable)
65
+ return if authorize(authorizable)
66
+
67
+ respond_with!(
68
+ status: :forbidden,
69
+ resource: {
70
+ error_type: :forbidden,
71
+ error_message: 'You are not authorized to perform this operation.'
72
+ }
73
+ )
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,237 @@
1
+ # frozen_string_literal: true
2
+ module Pragma
3
+ module Operation
4
+ # This is the base class all your operations should extend.
5
+ #
6
+ # @author Alessandro Desantis
7
+ #
8
+ # @abstract Subclass and override {#call} to implement an operation.
9
+ class Base
10
+ include Interactor
11
+
12
+ STATUSES = {
13
+ 200 => :ok,
14
+ 201 => :created,
15
+ 202 => :accepted,
16
+ 203 => :non_authoritative_information,
17
+ 204 => :no_content,
18
+ 205 => :reset_content,
19
+ 206 => :partial_content,
20
+ 207 => :multi_status,
21
+ 208 => :already_reported,
22
+ 300 => :multiple_choices,
23
+ 301 => :moved_permanently,
24
+ 302 => :found,
25
+ 303 => :see_other,
26
+ 304 => :not_modified,
27
+ 305 => :use_proxy,
28
+ 307 => :temporary_redirect,
29
+ 400 => :bad_request,
30
+ 401 => :unauthorized,
31
+ 402 => :payment_required,
32
+ 403 => :forbidden,
33
+ 404 => :not_found,
34
+ 405 => :method_not_allowed,
35
+ 406 => :not_acceptable,
36
+ 407 => :proxy_authentication_required,
37
+ 408 => :request_timeout,
38
+ 409 => :conflict,
39
+ 410 => :gone,
40
+ 411 => :length_required,
41
+ 412 => :precondition_failed,
42
+ 413 => :request_entity_too_large,
43
+ 414 => :request_uri_too_large,
44
+ 415 => :unsupported_media_type,
45
+ 416 => :request_range_not_satisfiable,
46
+ 417 => :expectation_failed,
47
+ 418 => :im_a_teapot,
48
+ 422 => :unprocessable_entity,
49
+ 423 => :locked,
50
+ 424 => :failed_dependency,
51
+ 425 => :unordered_collection,
52
+ 426 => :upgrade_required,
53
+ 428 => :precondition_required,
54
+ 429 => :too_many_requests,
55
+ 431 => :request_header_fields_too_large,
56
+ 449 => :retry_with,
57
+ 500 => :internal_server_error,
58
+ 501 => :not_implemented,
59
+ 502 => :bad_gateway,
60
+ 503 => :service_unavailable,
61
+ 504 => :gateway_timeout,
62
+ 505 => :http_version_not_supported,
63
+ 506 => :variant_also_negotiates,
64
+ 507 => :insufficient_storage,
65
+ 509 => :bandwidth_limit_exceeded,
66
+ 510 => :not_extended,
67
+ 511 => :network_authentication_required
68
+ }.freeze
69
+
70
+ class << self
71
+ def inherited(child)
72
+ child.class_eval do
73
+ include Authorization
74
+ include Validation
75
+
76
+ before :setup_context
77
+ around :handle_halt
78
+ after :mark_result, :consolidate_status, :validate_status, :set_default_status
79
+ end
80
+ end
81
+
82
+ # Returns the name of this operation.
83
+ #
84
+ # For instance, if the operation is called +API::V1::Post::Operation::Create+, returns
85
+ # +create+.
86
+ #
87
+ # @return [Symbol]
88
+ def operation_name
89
+ name.split('::').last
90
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
91
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
92
+ .tr('-', '_')
93
+ .downcase
94
+ .to_sym
95
+ end
96
+ end
97
+
98
+ # Runs the operation.
99
+ def call
100
+ fail NotImplementedError
101
+ end
102
+
103
+ protected
104
+
105
+ # Returns the params this operation is being run with.
106
+ #
107
+ # This is just a shortcut for +context.params+.
108
+ #
109
+ # @return [Hash]
110
+ def params
111
+ context.params
112
+ end
113
+
114
+ # Sets the status and resource to respond with.
115
+ #
116
+ # You can achieve the same result by setting +context.status+ and +context.resource+ wherever
117
+ # you want in {#call}.
118
+ #
119
+ # Note that calling this method doesn't halt the execution of the operation and that this
120
+ # method can be called multiple times, overriding the previous context.
121
+ #
122
+ # @param status [Integer|Symbol] an HTTP status code
123
+ # @param resource [Object] an object responding to +#to_json+
124
+ def respond_with(status:, resource:)
125
+ context.status = status
126
+ context.resource = resource
127
+ end
128
+
129
+ # Same as {#respond_with}, but also halts the execution of the operation.
130
+ #
131
+ # @param status [Integer|Symbol] an HTTP status code
132
+ # @param resource [Object] an object responding to +#to_json+
133
+ #
134
+ # @see #respond_with
135
+ def respond_with!(status:, resource:)
136
+ respond_with status: status, resource: resource
137
+ fail Halt
138
+ end
139
+
140
+ # Sets the status to respond with.
141
+ #
142
+ # You can achieve the same result by setting +context.status+ wherever you want in {#call}.
143
+ #
144
+ # Note that calling this method doesn't halt the execution of the operation and that this
145
+ # method can be called multiple times, overriding the previous context.
146
+ #
147
+ # @param status [Integer|Symbol] an HTTP status code
148
+ def head(status)
149
+ context.status = status
150
+ end
151
+
152
+ # Same as {#head}, but also halts the execution of the operation.
153
+ #
154
+ # @param status [Integer|Symbol] an HTTP status code
155
+ #
156
+ # @see #head
157
+ def head!(status)
158
+ head status
159
+ fail Halt
160
+ end
161
+
162
+ # Returns the current user.
163
+ #
164
+ # This is just a shortcut for +context.current_user+.
165
+ #
166
+ # @return [Object]
167
+ def current_user
168
+ context.current_user
169
+ end
170
+
171
+ private
172
+
173
+ def with_hooks
174
+ # This overrides the default behavior, which is not to run after hooks if an exception is
175
+ # raised either in +#call+ or one of the before hooks. See:
176
+ # https://github.com/collectiveidea/interactor/blob/master/lib/interactor/hooks.rb#L210)
177
+ run_around_hooks do
178
+ begin
179
+ run_before_hooks
180
+ yield
181
+ ensure
182
+ run_after_hooks
183
+ end
184
+ end
185
+ end
186
+
187
+ def setup_context
188
+ context.params ||= {}
189
+ end
190
+
191
+ def handle_halt(interactor)
192
+ interactor.call
193
+ rescue Halt # rubocop:disable Lint/HandleExceptions
194
+ end
195
+
196
+ def set_default_status
197
+ return if context.status
198
+ context.status = context.resource ? :ok : :no_content
199
+ end
200
+
201
+ def validate_status
202
+ if context.status.is_a?(Integer)
203
+ fail InvalidStatusError, context.status unless STATUSES.key?(context.status)
204
+ else
205
+ fail InvalidStatusError, context.status unless STATUSES.invert.key?(context.status.to_sym)
206
+ end
207
+ end
208
+
209
+ def consolidate_status
210
+ context.status = if context.status.is_a?(Integer)
211
+ STATUSES[context.status]
212
+ else
213
+ context.status.to_sym
214
+ end
215
+ end
216
+
217
+ def mark_result
218
+ return if /\A(2|3)\d{2}\z/ =~ STATUSES.invert[context.status].to_s
219
+ context.fail!
220
+ end
221
+ end
222
+
223
+ Halt = Class.new(StandardError)
224
+
225
+ # This error is raised when an invalid status is set for an operation.
226
+ #
227
+ # @author Alessandro Desantis
228
+ class InvalidStatusError < StandardError
229
+ # Initializes the error.
230
+ #
231
+ # @param [Integer|Symbol] an invalid HTTP status code
232
+ def initialize(status)
233
+ super "'#{status}' is not a valid HTTP status code."
234
+ end
235
+ end
236
+ end
237
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+ module Pragma
3
+ module Operation
4
+ module Validation
5
+ def self.included(base)
6
+ base.extend ClassMethods
7
+ base.include InstanceMethods
8
+ end
9
+
10
+ module ClassMethods
11
+ # Sets the policy to use for authorizing this operation.
12
+ #
13
+ # @param klass [Class] a subclass of +Pragma::Policy::Base+
14
+ def policy(klass)
15
+ @policy = klass
16
+ end
17
+
18
+ # Builds the policy for the given user and resource, using the previous defined policy
19
+ # class.
20
+ #
21
+ # @param user [Object]
22
+ # @param resource [Object]
23
+ #
24
+ # @return [Pragma::Policy::Base]
25
+ #
26
+ # @see #policy
27
+ def build_policy(user:, resource:)
28
+ @policy.new(user: user, resource: resource)
29
+ end
30
+ end
31
+
32
+ module InstanceMethods
33
+ # Builds the contract for the given resource, using the previously defined contract class.
34
+ #
35
+ # This is just an instance-level alias of {.build_contract}. You should use this from inside
36
+ # the operation.
37
+ #
38
+ # @param resource [Object]
39
+ #
40
+ # @return [Pragma::Contract::Base]
41
+ #
42
+ # @see .contract
43
+ # @see .build_contract
44
+ def build_contract(resource)
45
+ self.class.build_contract(resource)
46
+ end
47
+
48
+ # Validates this operation on the provided contract or resource.
49
+ #
50
+ # @param validatable [Object|Pragma::Contract::Base] contract or resource
51
+ #
52
+ # @return [Boolean] whether the operation is valid
53
+ def validate(validatable)
54
+ contract = if defined?(Pragma::Contract::Base) && validatable.is_a?(Pragma::Contract::Base)
55
+ validatable
56
+ else
57
+ build_contract(validatable)
58
+ end
59
+
60
+ contract.validate(params)
61
+ end
62
+
63
+ # Validates this operation on the provided contract or resource. If the operation is not
64
+ # valid, responds with 422 Unprocessable Entity and an error body and halts the execution.
65
+ #
66
+ # @param validatable [Object|Pragma::Contract::Base] contract or resource
67
+ def validate!(validatable)
68
+ contract = if defined?(Pragma::Contract::Base) && validatable.is_a?(Pragma::Contract::Base)
69
+ validatable
70
+ else
71
+ build_contract(validatable)
72
+ end
73
+
74
+ return if validate(contract)
75
+
76
+ respond_with!(
77
+ status: :unprocessable_entity,
78
+ resource: {
79
+ error_type: :contract_not_respected,
80
+ error_message: 'The contract for this operation was not respected.',
81
+ meta: {
82
+ errors: contract.errors.messages
83
+ }
84
+ }
85
+ )
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+ module Pragma
3
+ module Operation
4
+ VERSION = '0.1.0'
5
+ end
6
+ end
@@ -0,0 +1,31 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'pragma/operation/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "pragma-operation"
8
+ spec.version = Pragma::Operation::VERSION
9
+ spec.authors = ["Alessandro Desantis"]
10
+ spec.email = ["desa.alessandro@gmail.com"]
11
+
12
+ spec.summary = 'Business logic encapsulation for your JSON API.'
13
+ spec.homepage = "https://github.com/pragmarb/pragma-operation"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
17
+ f.match(%r{^(test|spec|features)/})
18
+ end
19
+ spec.bindir = "exe"
20
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
21
+ spec.require_paths = ["lib"]
22
+
23
+ spec.add_dependency 'interactor', '~> 3.1.0'
24
+
25
+ spec.add_development_dependency "bundler"
26
+ spec.add_development_dependency "rake"
27
+ spec.add_development_dependency "rspec"
28
+ spec.add_development_dependency "rubocop"
29
+ spec.add_development_dependency "rubocop-rspec"
30
+ spec.add_development_dependency "coveralls"
31
+ end
metadata ADDED
@@ -0,0 +1,161 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pragma-operation
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Alessandro Desantis
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2016-12-26 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: interactor
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 3.1.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 3.1.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rubocop
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rubocop-rspec
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: coveralls
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ description:
112
+ email:
113
+ - desa.alessandro@gmail.com
114
+ executables: []
115
+ extensions: []
116
+ extra_rdoc_files: []
117
+ files:
118
+ - ".gitignore"
119
+ - ".rspec"
120
+ - ".rubocop.yml"
121
+ - ".travis.yml"
122
+ - Gemfile
123
+ - LICENSE.txt
124
+ - README.md
125
+ - Rakefile
126
+ - bin/console
127
+ - bin/setup
128
+ - doc/01-basic-usage.md
129
+ - doc/02-contracts.md
130
+ - doc/03-policies.md
131
+ - lib/pragma/operation.rb
132
+ - lib/pragma/operation/authorization.rb
133
+ - lib/pragma/operation/base.rb
134
+ - lib/pragma/operation/validation.rb
135
+ - lib/pragma/operation/version.rb
136
+ - pragma-operation.gemspec
137
+ homepage: https://github.com/pragmarb/pragma-operation
138
+ licenses:
139
+ - MIT
140
+ metadata: {}
141
+ post_install_message:
142
+ rdoc_options: []
143
+ require_paths:
144
+ - lib
145
+ required_ruby_version: !ruby/object:Gem::Requirement
146
+ requirements:
147
+ - - ">="
148
+ - !ruby/object:Gem::Version
149
+ version: '0'
150
+ required_rubygems_version: !ruby/object:Gem::Requirement
151
+ requirements:
152
+ - - ">="
153
+ - !ruby/object:Gem::Version
154
+ version: '0'
155
+ requirements: []
156
+ rubyforge_project:
157
+ rubygems_version: 2.5.2
158
+ signing_key:
159
+ specification_version: 4
160
+ summary: Business logic encapsulation for your JSON API.
161
+ test_files: []