terrain 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +1 -0
- data/.rspec +1 -0
- data/Gemfile +10 -0
- data/README.md +114 -0
- data/lib/terrain/errors.rb +50 -0
- data/lib/terrain/resource.rb +109 -0
- data/lib/terrain.rb +4 -0
- data/spec/spec_helper.rb +68 -0
- data/spec/support/example.rb +5 -0
- data/spec/support/example_factory.rb +7 -0
- data/spec/support/example_serializer.rb +4 -0
- data/spec/support/schema.rb +13 -0
- data/spec/support/widget.rb +3 -0
- data/spec/support/widget_factory.rb +5 -0
- data/spec/terrain/errors_spec.rb +74 -0
- data/spec/terrain/resource_spec.rb +200 -0
- data/terrain.gemspec +24 -0
- metadata +111 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: d37822d50c6e00a4b0cfb6720ef147eef776554c
|
4
|
+
data.tar.gz: fa979a9c65f39f0f36101126c8c4ffab1ee583ea
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: b783ca61aaf45f49ffe57c99a110dccdaba51bdf562edaf678c1eb3069d84ebd4ad12cac36a71d400afa071d7c26f4e491153bcc2beccb11e0ca8ea2ee616b45
|
7
|
+
data.tar.gz: 559f5adc616c79de489abe36483534c5b9c437d3939f6f45145e4543e4ff8bcc9408ffc7d3c0751a37187988aa917ec0a9d9bdd45bf778b7653c8a798d0f207f
|
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
Gemfile.lock
|
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,114 @@
|
|
1
|
+
# Terrain
|
2
|
+
|
3
|
+
Opinionated toolkit for building CRUD APIs with Rails
|
4
|
+
|
5
|
+
* error handling
|
6
|
+
* basic CRUD
|
7
|
+
* serialization via
|
8
|
+
* authorization via [Pundit](https://github.com/elabs/pundit)
|
9
|
+
|
10
|
+
## Install
|
11
|
+
|
12
|
+
Add Terrain to your Gemfile:
|
13
|
+
|
14
|
+
```ruby
|
15
|
+
gem 'terrain'
|
16
|
+
```
|
17
|
+
|
18
|
+
## Usage
|
19
|
+
|
20
|
+
### Error handling
|
21
|
+
|
22
|
+
```ruby
|
23
|
+
class ExampleController < ApplicationController
|
24
|
+
include Terrain::Errors
|
25
|
+
end
|
26
|
+
```
|
27
|
+
|
28
|
+
Rescues the following errors:
|
29
|
+
|
30
|
+
* `ActiveRecord::AssociationNotFoundError` (400)
|
31
|
+
* `Pundit::NotAuthorizedError` (403)
|
32
|
+
* `ActiveRecord::RecordNotFound` (404)
|
33
|
+
* `ActionController::RoutingError` (404)
|
34
|
+
* `ActiveRecord::RecordInvalid` (422)
|
35
|
+
|
36
|
+
JSON responses are of the form:
|
37
|
+
|
38
|
+
```json
|
39
|
+
{
|
40
|
+
"error": {
|
41
|
+
"key": "type_of_error",
|
42
|
+
"message": "Localized error message"
|
43
|
+
}
|
44
|
+
}
|
45
|
+
```
|
46
|
+
|
47
|
+
To rescue a custom error with a similar response:
|
48
|
+
|
49
|
+
```ruby
|
50
|
+
class ExampleController < ApplicationController
|
51
|
+
include Terrain::Errors
|
52
|
+
|
53
|
+
rescue_from MyError, with: :my_error
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def my_error
|
58
|
+
error_response(:type_of_error, 500)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
```
|
62
|
+
|
63
|
+
### Resources
|
64
|
+
|
65
|
+
Suppose you have an `Example` model with `foo`, `bar`, and `baz` columns.
|
66
|
+
|
67
|
+
```ruby
|
68
|
+
class ExampleController < ApplicationController
|
69
|
+
include Terrain::Resource
|
70
|
+
|
71
|
+
resource Example, permit: [:foo, :bar, :baz]
|
72
|
+
end
|
73
|
+
```
|
74
|
+
|
75
|
+
This sets up the typical resourceful Rails controller actions. Note that **you'll still need to setup corresponding routes**.
|
76
|
+
|
77
|
+
#### Authorization
|
78
|
+
|
79
|
+
Authorization is handled by [Pundit](https://github.com/elabs/pundit). If the policy class for a given resource exists, each controller action calls the policy before proceeding with the operation. Authorization expects a `current_user` controller method to exist (otherwise `nil` is used as the `pundit_user`).
|
80
|
+
|
81
|
+
#### Serialization
|
82
|
+
|
83
|
+
via [ActiveModelSerializers](https://github.com/rails-api/active_model_serializers)
|
84
|
+
|
85
|
+
#### Querying
|
86
|
+
|
87
|
+
* `include` - This corresponds to the `ActiveModelSerializers` include option and embeds the given relationships in the response. Relationships are also preloaded according to the given string. If omitted then no relationships will be included or embedded in the response.
|
88
|
+
|
89
|
+
#### CRUD operations
|
90
|
+
|
91
|
+
You may need an action to perform additional steps beyond simple persistence. There are hooks for each CRUD operation (shown below with their default implementation):
|
92
|
+
|
93
|
+
```ruby
|
94
|
+
class ExampleController < ApplicationController
|
95
|
+
include Terrain::Resource
|
96
|
+
|
97
|
+
resource Example, permit: [:foo, :bar, :baz]
|
98
|
+
|
99
|
+
private
|
100
|
+
|
101
|
+
def create_record
|
102
|
+
resource.create!(permitted_params)
|
103
|
+
end
|
104
|
+
|
105
|
+
def update_record(record)
|
106
|
+
record.update_attributes!(permitted_params)
|
107
|
+
record
|
108
|
+
end
|
109
|
+
|
110
|
+
def destroy_record(record)
|
111
|
+
record.delete
|
112
|
+
end
|
113
|
+
end
|
114
|
+
```
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Terrain
|
2
|
+
module Errors
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
rescue_from 'ActiveRecord::AssociationNotFoundError', with: :association_not_found
|
7
|
+
rescue_from 'Pundit::NotAuthorizedError', with: :unauthorized
|
8
|
+
rescue_from 'ActiveRecord::RecordNotFound', with: :record_not_found
|
9
|
+
rescue_from 'ActionController::RoutingError', with: :route_not_found
|
10
|
+
rescue_from 'ActiveRecord::RecordInvalid', with: :record_invalid
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def association_not_found
|
15
|
+
error_response(:association_not_found, 400)
|
16
|
+
end
|
17
|
+
|
18
|
+
def unauthenticated
|
19
|
+
error_response(:unauthenticated, 401)
|
20
|
+
end
|
21
|
+
|
22
|
+
def unauthorized
|
23
|
+
error_response(:unauthorized, 403)
|
24
|
+
end
|
25
|
+
|
26
|
+
def record_not_found
|
27
|
+
error_response(:record_not_found, 404)
|
28
|
+
end
|
29
|
+
|
30
|
+
def route_not_found
|
31
|
+
error_response(:route_not_found, 404)
|
32
|
+
end
|
33
|
+
|
34
|
+
def record_invalid
|
35
|
+
error_response(:record_invalid, 422)
|
36
|
+
end
|
37
|
+
|
38
|
+
def error_response(key = :server_error, status = 500)
|
39
|
+
result = {
|
40
|
+
error: {
|
41
|
+
key: key,
|
42
|
+
message: I18n.t("terrain.errors.#{key}", request: request)
|
43
|
+
}
|
44
|
+
}
|
45
|
+
|
46
|
+
render json: result, status: status
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
require 'active_model_serializers'
|
2
|
+
require 'pundit'
|
3
|
+
|
4
|
+
module Terrain
|
5
|
+
module Resource
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
class_methods do
|
9
|
+
attr_accessor :permit
|
10
|
+
|
11
|
+
def resource(resource, options = {})
|
12
|
+
include Pundit
|
13
|
+
include Actions
|
14
|
+
|
15
|
+
@resource = resource
|
16
|
+
self.permit = options[:permit] || []
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
module Actions
|
21
|
+
def index
|
22
|
+
render json: preloaded_resource.all
|
23
|
+
end
|
24
|
+
|
25
|
+
def create
|
26
|
+
record = resource.new(permitted_params)
|
27
|
+
authorize_record(record)
|
28
|
+
|
29
|
+
render json: create_record, include: [], status: 201
|
30
|
+
end
|
31
|
+
|
32
|
+
def show
|
33
|
+
record = load_record
|
34
|
+
authorize_record(record)
|
35
|
+
|
36
|
+
render json: record, include: params[:include] || []
|
37
|
+
end
|
38
|
+
|
39
|
+
def update
|
40
|
+
record = load_record
|
41
|
+
authorize_record(record)
|
42
|
+
|
43
|
+
render json: update_record(record), include: []
|
44
|
+
end
|
45
|
+
|
46
|
+
def destroy
|
47
|
+
record = load_record
|
48
|
+
authorize_record(record)
|
49
|
+
destroy_record(record)
|
50
|
+
|
51
|
+
render nothing: true, status: 204
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def resource
|
57
|
+
self.class.instance_variable_get('@resource')
|
58
|
+
end
|
59
|
+
|
60
|
+
def includes_hash
|
61
|
+
if params[:include].present?
|
62
|
+
ActiveModel::Serializer::IncludeTree::Parsing.include_string_to_hash(params[:include])
|
63
|
+
else
|
64
|
+
{}
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def preloaded_resource
|
69
|
+
resource.includes(includes_hash)
|
70
|
+
end
|
71
|
+
|
72
|
+
def permitted_params
|
73
|
+
params.permit(self.class.permit)
|
74
|
+
end
|
75
|
+
|
76
|
+
def pundit_user
|
77
|
+
if defined?(current_user)
|
78
|
+
current_user
|
79
|
+
else
|
80
|
+
nil
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def authorize_record(record)
|
85
|
+
finder = Pundit::PolicyFinder.new(record)
|
86
|
+
if finder.policy
|
87
|
+
authorize(record)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def load_record
|
92
|
+
preloaded_resource.find(params[:id])
|
93
|
+
end
|
94
|
+
|
95
|
+
def create_record
|
96
|
+
resource.create!(permitted_params)
|
97
|
+
end
|
98
|
+
|
99
|
+
def update_record(record)
|
100
|
+
record.update_attributes!(permitted_params)
|
101
|
+
record
|
102
|
+
end
|
103
|
+
|
104
|
+
def destroy_record(record)
|
105
|
+
record.delete
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
data/lib/terrain.rb
ADDED
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
ENV['RAILS_ENV'] = 'test'
|
2
|
+
|
3
|
+
require 'terrain'
|
4
|
+
|
5
|
+
require 'rails'
|
6
|
+
require 'action_controller/railtie'
|
7
|
+
require 'active_model_serializers'
|
8
|
+
require 'active_model_serializers/railtie'
|
9
|
+
require 'active_record'
|
10
|
+
require 'airborne'
|
11
|
+
require 'factory_girl'
|
12
|
+
require 'faker'
|
13
|
+
require 'rspec/rails'
|
14
|
+
|
15
|
+
LOGGER = Logger.new('/dev/null')
|
16
|
+
|
17
|
+
Rails.logger = LOGGER
|
18
|
+
ActiveModelSerializers.logger = LOGGER
|
19
|
+
ActiveRecord::Base.logger = LOGGER
|
20
|
+
|
21
|
+
DATABASE = {
|
22
|
+
adapter: 'sqlite3',
|
23
|
+
database: ':memory:'
|
24
|
+
}
|
25
|
+
|
26
|
+
ActiveRecord::Migration.verbose = false
|
27
|
+
ActiveRecord::Base.establish_connection(DATABASE)
|
28
|
+
|
29
|
+
module Terrain
|
30
|
+
class Application < ::Rails::Application
|
31
|
+
def self.find_root(from)
|
32
|
+
Dir.pwd
|
33
|
+
end
|
34
|
+
|
35
|
+
config.eager_load = false
|
36
|
+
config.secret_key_base = 'secret'
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
Terrain::Application.initialize!
|
41
|
+
|
42
|
+
module Helpers
|
43
|
+
def serialize(value, options = {})
|
44
|
+
options[:include] ||= []
|
45
|
+
if value.respond_to?(:each)
|
46
|
+
ActiveModel::Serializer::CollectionSerializer.new(value, options).as_json
|
47
|
+
else
|
48
|
+
ActiveModelSerializers::SerializableResource.new(value, options).as_json.symbolize_keys
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def policy_double(methods)
|
53
|
+
Class.new(Struct.new(:user, :record)) do
|
54
|
+
methods.each do |name, value|
|
55
|
+
define_method(name) { value }
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
RSpec.configure do |config|
|
62
|
+
config.include Helpers
|
63
|
+
config.include FactoryGirl::Syntax::Methods
|
64
|
+
|
65
|
+
config.use_transactional_fixtures = true
|
66
|
+
end
|
67
|
+
|
68
|
+
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
|
@@ -0,0 +1,13 @@
|
|
1
|
+
ActiveRecord::Schema.define(version: 1) do
|
2
|
+
create_table :examples do |t|
|
3
|
+
t.string :foo, null: false
|
4
|
+
t.string :bar
|
5
|
+
t.string :baz
|
6
|
+
t.timestamps null: false
|
7
|
+
end
|
8
|
+
|
9
|
+
create_table :widgets do |t|
|
10
|
+
t.integer :example_id, null: false
|
11
|
+
t.timestamps null: false
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'Terrain::Errors', type: :controller do
|
4
|
+
controller do
|
5
|
+
include Terrain::Errors
|
6
|
+
end
|
7
|
+
|
8
|
+
before { get :index }
|
9
|
+
|
10
|
+
context 'association not found' do
|
11
|
+
controller do
|
12
|
+
def index
|
13
|
+
raise ActiveRecord::AssociationNotFoundError.new(nil, nil)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
it { expect(response.status).to eq 400 }
|
18
|
+
it { expect_json_types(error: :object) }
|
19
|
+
it { expect_json_types('error.message', :string) }
|
20
|
+
it { expect_json('error.key', 'association_not_found') }
|
21
|
+
end
|
22
|
+
|
23
|
+
context 'unauthorized' do
|
24
|
+
controller do
|
25
|
+
def index
|
26
|
+
raise Pundit::NotAuthorizedError
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
it { expect(response.status).to eq 403 }
|
31
|
+
it { expect_json_types(error: :object) }
|
32
|
+
it { expect_json_types('error.message', :string) }
|
33
|
+
it { expect_json('error.key', 'unauthorized') }
|
34
|
+
end
|
35
|
+
|
36
|
+
context 'record not found' do
|
37
|
+
controller do
|
38
|
+
def index
|
39
|
+
raise ActiveRecord::RecordNotFound
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
it { expect(response.status).to eq 404 }
|
44
|
+
it { expect_json_types(error: :object) }
|
45
|
+
it { expect_json_types('error.message', :string) }
|
46
|
+
it { expect_json('error.key', 'record_not_found') }
|
47
|
+
end
|
48
|
+
|
49
|
+
context 'route not found' do
|
50
|
+
controller do
|
51
|
+
def index
|
52
|
+
raise ActionController::RoutingError, ''
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
it { expect(response.status).to eq 404 }
|
57
|
+
it { expect_json_types(error: :object) }
|
58
|
+
it { expect_json_types('error.message', :string) }
|
59
|
+
it { expect_json('error.key', 'route_not_found') }
|
60
|
+
end
|
61
|
+
|
62
|
+
context 'route not found' do
|
63
|
+
controller do
|
64
|
+
def index
|
65
|
+
raise ActiveRecord::RecordInvalid, Example.new
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
it { expect(response.status).to eq 422 }
|
70
|
+
it { expect_json_types(error: :object) }
|
71
|
+
it { expect_json_types('error.message', :string) }
|
72
|
+
it { expect_json('error.key', 'record_invalid') }
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,200 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'Terrain::Resource', type: :controller do
|
4
|
+
controller do
|
5
|
+
include Terrain::Resource
|
6
|
+
|
7
|
+
resource Example, permit: [:foo, :bar, :baz]
|
8
|
+
end
|
9
|
+
|
10
|
+
describe '#index' do
|
11
|
+
let!(:examples) { create_list(:example, 10) }
|
12
|
+
|
13
|
+
it 'responds with 200 status' do
|
14
|
+
get :index
|
15
|
+
expect(response.status).to eq 200
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'responds with serialized records' do
|
19
|
+
get :index
|
20
|
+
expect(response.body).to eq serialize(Example.all).to_json
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
describe '#create' do
|
25
|
+
let(:params) { ActionController::Parameters.new(attributes_for(:example)) }
|
26
|
+
|
27
|
+
it 'responds with 201 status' do
|
28
|
+
post :create, params
|
29
|
+
expect(response.status).to eq 201
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'creates record' do
|
33
|
+
expect { post :create, params }.to change { Example.count }.by 1
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'responds with serialized record' do
|
37
|
+
post :create, params
|
38
|
+
expect(response.body).to eq serialize(Example.last).to_json
|
39
|
+
end
|
40
|
+
|
41
|
+
context 'invalid params' do
|
42
|
+
let(:params) { ActionController::Parameters.new(attributes_for(:example, foo: nil)) }
|
43
|
+
|
44
|
+
it 'raises error' do
|
45
|
+
expect { post :create, params }.to raise_error ActiveRecord::RecordInvalid
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
context 'with failing policy' do
|
50
|
+
before do
|
51
|
+
allow_any_instance_of(Example).to receive(:policy_class) { policy_double(create?: false) }
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'raises error' do
|
55
|
+
expect { post :create, params }.to raise_error Pundit::NotAuthorizedError
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
describe '#show' do
|
61
|
+
let!(:record) { create(:example) }
|
62
|
+
let(:params) { ActionController::Parameters.new(id: record.id) }
|
63
|
+
|
64
|
+
it 'responds with 200 status' do
|
65
|
+
get :show, params
|
66
|
+
expect(response.status).to eq 200
|
67
|
+
end
|
68
|
+
|
69
|
+
it 'responds with serialized record' do
|
70
|
+
get :show, params
|
71
|
+
expect(response.body).to eq serialize(record).to_json
|
72
|
+
end
|
73
|
+
|
74
|
+
context 'invalid ID' do
|
75
|
+
let(:params) { ActionController::Parameters.new(id: 999) }
|
76
|
+
|
77
|
+
it 'raises error' do
|
78
|
+
expect { get :show, params }.to raise_error ActiveRecord::RecordNotFound
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
context 'with failing policy' do
|
83
|
+
before do
|
84
|
+
allow_any_instance_of(Example).to receive(:policy_class) { policy_double(show?: false) }
|
85
|
+
end
|
86
|
+
|
87
|
+
it 'raises error' do
|
88
|
+
expect { get :show, params }.to raise_error Pundit::NotAuthorizedError
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
context 'with relations' do
|
93
|
+
let!(:widgets) { create_list(:widget, 3, example: record) }
|
94
|
+
|
95
|
+
it 'does not include relations in serialized record' do
|
96
|
+
get :show, params
|
97
|
+
expect(response.body).to eq serialize(record, include: []).to_json
|
98
|
+
end
|
99
|
+
|
100
|
+
context 'with valid include' do
|
101
|
+
let(:params) { ActionController::Parameters.new(id: record.id, include: 'widgets') }
|
102
|
+
|
103
|
+
it 'includes relations in serialized record' do
|
104
|
+
get :show, params
|
105
|
+
expect(response.body).to eq serialize(record, include: ['widgets']).to_json
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
context 'with invalid include' do
|
110
|
+
let(:params) { ActionController::Parameters.new(id: record.id, include: 'widgets,wrong') }
|
111
|
+
|
112
|
+
it 'raises error' do
|
113
|
+
expect { get :show, params }.to raise_error ActiveRecord::AssociationNotFoundError
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
describe '#update' do
|
120
|
+
let!(:record) { create(:example) }
|
121
|
+
let(:attrs) { attributes_for(:example) }
|
122
|
+
let(:params) { ActionController::Parameters.new(attrs.merge(id: record.id)) }
|
123
|
+
|
124
|
+
it 'responds with 200 status' do
|
125
|
+
patch :update, params
|
126
|
+
expect(response.status).to eq 200
|
127
|
+
end
|
128
|
+
|
129
|
+
it 'updates record' do
|
130
|
+
patch :update, params
|
131
|
+
record.reload
|
132
|
+
expect(record.foo).to eq params[:foo]
|
133
|
+
expect(record.bar).to eq params[:bar]
|
134
|
+
expect(record.baz).to eq params[:baz]
|
135
|
+
end
|
136
|
+
|
137
|
+
it 'responds with serialized record' do
|
138
|
+
patch :update, params
|
139
|
+
expect(response.body).to eq serialize(record.reload).to_json
|
140
|
+
end
|
141
|
+
|
142
|
+
context 'invalid params' do
|
143
|
+
let(:attrs) { attributes_for(:example, foo: nil) }
|
144
|
+
|
145
|
+
it 'raises error' do
|
146
|
+
expect { patch :update, params }.to raise_error ActiveRecord::RecordInvalid
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
context 'invalid ID' do
|
151
|
+
let(:params) { ActionController::Parameters.new(id: 999) }
|
152
|
+
|
153
|
+
it 'raises error' do
|
154
|
+
expect { patch :update, params }.to raise_error ActiveRecord::RecordNotFound
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
context 'with failing policy' do
|
159
|
+
before do
|
160
|
+
allow_any_instance_of(Example).to receive(:policy_class) { policy_double(update?: false) }
|
161
|
+
end
|
162
|
+
|
163
|
+
it 'raises error' do
|
164
|
+
expect { patch :update, params }.to raise_error Pundit::NotAuthorizedError
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
describe '#destroy' do
|
170
|
+
let!(:record) { create(:example) }
|
171
|
+
let(:params) { ActionController::Parameters.new(id: record.id) }
|
172
|
+
|
173
|
+
it 'responds with 204 status' do
|
174
|
+
delete :destroy, params
|
175
|
+
expect(response.status).to eq 204
|
176
|
+
end
|
177
|
+
|
178
|
+
it 'deletes record' do
|
179
|
+
expect { delete :destroy, params }.to change { Example.count }.by -1
|
180
|
+
end
|
181
|
+
|
182
|
+
context 'invalid ID' do
|
183
|
+
let(:params) { ActionController::Parameters.new(id: 999) }
|
184
|
+
|
185
|
+
it 'raises error' do
|
186
|
+
expect { patch :update, params }.to raise_error ActiveRecord::RecordNotFound
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
context 'with failing policy' do
|
191
|
+
before do
|
192
|
+
allow_any_instance_of(Example).to receive(:policy_class) { policy_double(destroy?: false) }
|
193
|
+
end
|
194
|
+
|
195
|
+
it 'raises error' do
|
196
|
+
expect { delete :destroy, params }.to raise_error Pundit::NotAuthorizedError
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
data/terrain.gemspec
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
lib = File.expand_path('../lib', __FILE__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.name = 'terrain'
|
6
|
+
gem.version = '0.0.1'
|
7
|
+
gem.summary = 'Terrain'
|
8
|
+
gem.description = ''
|
9
|
+
gem.license = 'MIT'
|
10
|
+
gem.authors = ['Scott Nelson']
|
11
|
+
gem.email = 'scott@scottnelson.co'
|
12
|
+
gem.homepage = 'https://github.com/scttnlsn/terrain'
|
13
|
+
|
14
|
+
gem.files = `git ls-files`.split($/)
|
15
|
+
gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) }
|
16
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
17
|
+
gem.require_paths = ['lib']
|
18
|
+
|
19
|
+
gem.required_ruby_version = '>= 2.0.0'
|
20
|
+
|
21
|
+
gem.add_dependency 'activesupport', '>= 4.0'
|
22
|
+
gem.add_dependency 'pundit', '1.1.0'
|
23
|
+
gem.add_dependency 'active_model_serializers', '>= 0.10.0.rc5'
|
24
|
+
end
|
metadata
ADDED
@@ -0,0 +1,111 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: terrain
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Scott Nelson
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-05-04 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activesupport
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '4.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '4.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: pundit
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - '='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 1.1.0
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - '='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 1.1.0
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: active_model_serializers
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 0.10.0.rc5
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 0.10.0.rc5
|
55
|
+
description: ''
|
56
|
+
email: scott@scottnelson.co
|
57
|
+
executables: []
|
58
|
+
extensions: []
|
59
|
+
extra_rdoc_files: []
|
60
|
+
files:
|
61
|
+
- ".gitignore"
|
62
|
+
- ".rspec"
|
63
|
+
- Gemfile
|
64
|
+
- README.md
|
65
|
+
- lib/terrain.rb
|
66
|
+
- lib/terrain/errors.rb
|
67
|
+
- lib/terrain/resource.rb
|
68
|
+
- spec/spec_helper.rb
|
69
|
+
- spec/support/example.rb
|
70
|
+
- spec/support/example_factory.rb
|
71
|
+
- spec/support/example_serializer.rb
|
72
|
+
- spec/support/schema.rb
|
73
|
+
- spec/support/widget.rb
|
74
|
+
- spec/support/widget_factory.rb
|
75
|
+
- spec/terrain/errors_spec.rb
|
76
|
+
- spec/terrain/resource_spec.rb
|
77
|
+
- terrain.gemspec
|
78
|
+
homepage: https://github.com/scttnlsn/terrain
|
79
|
+
licenses:
|
80
|
+
- MIT
|
81
|
+
metadata: {}
|
82
|
+
post_install_message:
|
83
|
+
rdoc_options: []
|
84
|
+
require_paths:
|
85
|
+
- lib
|
86
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
87
|
+
requirements:
|
88
|
+
- - ">="
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
version: 2.0.0
|
91
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - ">="
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '0'
|
96
|
+
requirements: []
|
97
|
+
rubyforge_project:
|
98
|
+
rubygems_version: 2.4.5.1
|
99
|
+
signing_key:
|
100
|
+
specification_version: 4
|
101
|
+
summary: Terrain
|
102
|
+
test_files:
|
103
|
+
- spec/spec_helper.rb
|
104
|
+
- spec/support/example.rb
|
105
|
+
- spec/support/example_factory.rb
|
106
|
+
- spec/support/example_serializer.rb
|
107
|
+
- spec/support/schema.rb
|
108
|
+
- spec/support/widget.rb
|
109
|
+
- spec/support/widget_factory.rb
|
110
|
+
- spec/terrain/errors_spec.rb
|
111
|
+
- spec/terrain/resource_spec.rb
|