terrain 0.0.1 → 0.0.2
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/.gitignore +1 -0
- data/.travis.yml +6 -0
- data/Gemfile +1 -0
- data/README.md +92 -9
- data/Rakefile +5 -0
- data/lib/terrain/config.rb +9 -0
- data/lib/terrain/errors.rb +23 -11
- data/lib/terrain/page.rb +63 -0
- data/lib/terrain/resource.rb +32 -1
- data/lib/terrain.rb +13 -0
- data/spec/spec_helper.rb +1 -1
- data/spec/terrain/errors_spec.rb +22 -2
- data/spec/terrain/page_spec.rb +101 -0
- data/spec/terrain/resource_spec.rb +100 -2
- data/terrain.gemspec +1 -1
- metadata +8 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 35b20c85da62fa29911c7f66fd6c3152dd138d21
|
4
|
+
data.tar.gz: 16e76c904d30e761b3667f0ae0c135fad8b7529c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 42432a6d37702385300c453a526620951a797acfaeaece1f17b81546fb9b041921fb1ae5f452bd37892d5bccb2d6ec71a0037a811c250896f9e28558705f9fe2
|
7
|
+
data.tar.gz: 89b74868b62563bec9d303e926632559ee79a4d65d5759020d650b6cb9d35ca70e085f05d4d186017f94d457d9dad9956797bede36615216277ba59b3f7f8251
|
data/.gitignore
CHANGED
data/.travis.yml
ADDED
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,11 +1,8 @@
|
|
1
1
|
# Terrain
|
2
2
|
|
3
|
-
|
3
|
+
[](https://travis-ci.org/scttnlsn/terrain)
|
4
4
|
|
5
|
-
|
6
|
-
* basic CRUD
|
7
|
-
* serialization via
|
8
|
-
* authorization via [Pundit](https://github.com/elabs/pundit)
|
5
|
+
Opinionated toolkit for building CRUD APIs with Rails
|
9
6
|
|
10
7
|
## Install
|
11
8
|
|
@@ -17,6 +14,18 @@ gem 'terrain'
|
|
17
14
|
|
18
15
|
## Usage
|
19
16
|
|
17
|
+
* [Error handling](#error-handling)
|
18
|
+
* [Resources](#resources)
|
19
|
+
* [Authorization](#authorization)
|
20
|
+
* [Serialization](#serialization)
|
21
|
+
* [Querying](#querying)
|
22
|
+
* [Filtering](#filtering)
|
23
|
+
* [Ordering](#ordering)
|
24
|
+
* [Pagination](#pagination)
|
25
|
+
* [Relationships](#relationships)
|
26
|
+
* [CRUD operations](#crud-operations)
|
27
|
+
* [Config](#config)
|
28
|
+
|
20
29
|
### Error handling
|
21
30
|
|
22
31
|
```ruby
|
@@ -39,7 +48,8 @@ JSON responses are of the form:
|
|
39
48
|
{
|
40
49
|
"error": {
|
41
50
|
"key": "type_of_error",
|
42
|
-
"message": "Localized error message"
|
51
|
+
"message": "Localized error message",
|
52
|
+
"details": "Optional details"
|
43
53
|
}
|
44
54
|
}
|
45
55
|
```
|
@@ -55,7 +65,7 @@ class ExampleController < ApplicationController
|
|
55
65
|
private
|
56
66
|
|
57
67
|
def my_error
|
58
|
-
error_response(:type_of_error, 500)
|
68
|
+
error_response(:type_of_error, 500, { some: :details })
|
59
69
|
end
|
60
70
|
end
|
61
71
|
```
|
@@ -84,11 +94,75 @@ via [ActiveModelSerializers](https://github.com/rails-api/active_model_serialize
|
|
84
94
|
|
85
95
|
#### Querying
|
86
96
|
|
87
|
-
|
97
|
+
Records of a given resource are queried by requesting the `index` action.
|
98
|
+
|
99
|
+
##### Filtering
|
100
|
+
|
101
|
+
Queries are scoped to the results returned from the `resource_scope` method. By default this returns all records, however, you can override it to further filter the results (i.e. based on query params, nested route params, etc.):
|
102
|
+
|
103
|
+
```ruby
|
104
|
+
class ExampleController < ApplicationController
|
105
|
+
include Terrain::Resource
|
106
|
+
|
107
|
+
resource Example, permit: [:foo, :bar, :baz]
|
108
|
+
|
109
|
+
private
|
110
|
+
|
111
|
+
def resource_scope
|
112
|
+
scope = super
|
113
|
+
scope = scope.where(foo: params[:foo]) if params[:foo].present?
|
114
|
+
scope
|
115
|
+
end
|
116
|
+
end
|
117
|
+
```
|
118
|
+
|
119
|
+
##### Ordering
|
120
|
+
|
121
|
+
You can pass an `order` param to reorder the response records. Specify a comma-separated list of fields and prefix the field with a `-` for descending order:
|
122
|
+
|
123
|
+
```ruby
|
124
|
+
# corresponds to Example.order('foo', 'bar desc')
|
125
|
+
get :index, order: 'foo,-bar'
|
126
|
+
```
|
127
|
+
|
128
|
+
##### Pagination
|
129
|
+
|
130
|
+
To request a range of records, specify the range in an HTTP header:
|
131
|
+
|
132
|
+
```ruby
|
133
|
+
# Request the first 10 records
|
134
|
+
get :index, {}, { 'Range' => '0-9' }
|
135
|
+
```
|
136
|
+
|
137
|
+
All responses include a `Content-Range` header that specifies the exact range returned as well as a total count of records. i.e.
|
138
|
+
|
139
|
+
```
|
140
|
+
Content-Range: 0-9/100
|
141
|
+
```
|
142
|
+
|
143
|
+
You can also pass open ended ranges such as `10-` (i.e. skip the first 10 records).
|
144
|
+
|
145
|
+
##### Relationships
|
146
|
+
|
147
|
+
No model relationships are serialized in the response by default. To specify the set of relationships to be embedded in the response, pass a comma-separated list of relationships in the `include` param.
|
148
|
+
|
149
|
+
As an example, suppose we're querying for posts which each have many tags and belong to an author. We could embed those relationships with the following `include` param:
|
150
|
+
|
151
|
+
```ruby
|
152
|
+
get :index, include: 'author,tags'
|
153
|
+
```
|
154
|
+
|
155
|
+
Suppose now that the author also has a profile relationship. We could include the author, author profile and tags by passing:
|
156
|
+
|
157
|
+
```ruby
|
158
|
+
get :index, include: 'author.profile,tags'
|
159
|
+
```
|
160
|
+
|
161
|
+
Included relationships are automatically preloaded via the ActiveRecord `includes` method. The `include` param is also supported in `show` actions.
|
88
162
|
|
89
163
|
#### CRUD operations
|
90
164
|
|
91
|
-
You may need an action to perform additional steps beyond simple persistence. There are
|
165
|
+
You may need an action to perform additional steps beyond simple persistence. There are methods for performing CRUD operations that can be overridden (shown below with their default implementation):
|
92
166
|
|
93
167
|
```ruby
|
94
168
|
class ExampleController < ApplicationController
|
@@ -112,3 +186,12 @@ class ExampleController < ApplicationController
|
|
112
186
|
end
|
113
187
|
end
|
114
188
|
```
|
189
|
+
|
190
|
+
### Config
|
191
|
+
|
192
|
+
```ruby
|
193
|
+
Terrain.configure do |config|
|
194
|
+
# Maximum number of records returned
|
195
|
+
config.max_records = Float::INFINITY
|
196
|
+
end
|
197
|
+
```
|
data/Rakefile
ADDED
data/lib/terrain/errors.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'terrain/page'
|
2
|
+
|
1
3
|
module Terrain
|
2
4
|
module Errors
|
3
5
|
extend ActiveSupport::Concern
|
@@ -9,8 +11,25 @@ module Terrain
|
|
9
11
|
rescue_from 'ActionController::RoutingError', with: :route_not_found
|
10
12
|
rescue_from 'ActiveRecord::RecordInvalid', with: :record_invalid
|
11
13
|
|
14
|
+
rescue_from Terrain::Page::RangeError, with: :range_error
|
15
|
+
|
12
16
|
private
|
13
17
|
|
18
|
+
def error_response(key = :server_error, status = 500, details = nil)
|
19
|
+
result = {
|
20
|
+
error: {
|
21
|
+
key: key,
|
22
|
+
message: I18n.t("terrain.errors.#{key}", request: request)
|
23
|
+
}
|
24
|
+
}
|
25
|
+
|
26
|
+
if details.present?
|
27
|
+
result[:error][:details] = details
|
28
|
+
end
|
29
|
+
|
30
|
+
render json: result, status: status
|
31
|
+
end
|
32
|
+
|
14
33
|
def association_not_found
|
15
34
|
error_response(:association_not_found, 400)
|
16
35
|
end
|
@@ -31,19 +50,12 @@ module Terrain
|
|
31
50
|
error_response(:route_not_found, 404)
|
32
51
|
end
|
33
52
|
|
34
|
-
def record_invalid
|
35
|
-
error_response(:record_invalid, 422)
|
53
|
+
def record_invalid(error)
|
54
|
+
error_response(:record_invalid, 422, error.record.errors.to_hash)
|
36
55
|
end
|
37
56
|
|
38
|
-
def
|
39
|
-
|
40
|
-
error: {
|
41
|
-
key: key,
|
42
|
-
message: I18n.t("terrain.errors.#{key}", request: request)
|
43
|
-
}
|
44
|
-
}
|
45
|
-
|
46
|
-
render json: result, status: status
|
57
|
+
def range_error
|
58
|
+
error_response(:range_error, 416)
|
47
59
|
end
|
48
60
|
end
|
49
61
|
end
|
data/lib/terrain/page.rb
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
module Terrain
|
2
|
+
class Page
|
3
|
+
class RangeError < StandardError; end
|
4
|
+
|
5
|
+
RANGE_REGEX = /^(?<from>[0-9]*)-(?<to>[0-9]*)$/
|
6
|
+
|
7
|
+
attr_reader :scope, :range
|
8
|
+
|
9
|
+
def initialize(scope, range = nil)
|
10
|
+
@scope = scope
|
11
|
+
@range = range
|
12
|
+
end
|
13
|
+
|
14
|
+
def bounds
|
15
|
+
@bounds ||= begin
|
16
|
+
if range.present?
|
17
|
+
if match
|
18
|
+
raise RangeError if from > to
|
19
|
+
[from, to]
|
20
|
+
else
|
21
|
+
raise RangeError
|
22
|
+
end
|
23
|
+
else
|
24
|
+
[0, count - 1]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def count
|
30
|
+
@count ||= scope.count
|
31
|
+
end
|
32
|
+
|
33
|
+
def records
|
34
|
+
from, to = bounds
|
35
|
+
limit = [to - from + 1, Terrain.config.max_records].min
|
36
|
+
@records ||= scope.offset(from).limit(limit)
|
37
|
+
end
|
38
|
+
|
39
|
+
def content_range
|
40
|
+
if count > 0
|
41
|
+
from, to = bounds
|
42
|
+
to = [to, from + records.count - 1].min
|
43
|
+
"#{from}-#{to}/#{count}"
|
44
|
+
else
|
45
|
+
'*/0'
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def match
|
52
|
+
range.match(RANGE_REGEX)
|
53
|
+
end
|
54
|
+
|
55
|
+
def from
|
56
|
+
match && match[:from].present? ? match[:from].to_i : 0
|
57
|
+
end
|
58
|
+
|
59
|
+
def to
|
60
|
+
match && match[:to].present? ? match[:to].to_i : (count - 1)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
data/lib/terrain/resource.rb
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
require 'active_model_serializers'
|
2
2
|
require 'pundit'
|
3
3
|
|
4
|
+
require 'terrain/page'
|
5
|
+
|
4
6
|
module Terrain
|
5
7
|
module Resource
|
6
8
|
extend ActiveSupport::Concern
|
@@ -19,7 +21,13 @@ module Terrain
|
|
19
21
|
|
20
22
|
module Actions
|
21
23
|
def index
|
22
|
-
|
24
|
+
scope = order(resource_scope)
|
25
|
+
|
26
|
+
range = request.headers['Range']
|
27
|
+
page = Terrain::Page.new(scope, range)
|
28
|
+
|
29
|
+
headers['Content-Range'] = page.content_range
|
30
|
+
render json: page.records, include: (params[:include] || [])
|
23
31
|
end
|
24
32
|
|
25
33
|
def create
|
@@ -88,6 +96,29 @@ module Terrain
|
|
88
96
|
end
|
89
97
|
end
|
90
98
|
|
99
|
+
def order(scope)
|
100
|
+
if params[:order].present?
|
101
|
+
order = params[:order].gsub(/ /, '').split(',').map do |field|
|
102
|
+
direction = 'asc'
|
103
|
+
|
104
|
+
if field[0] == '-'
|
105
|
+
direction = 'desc'
|
106
|
+
field = field[1..-1]
|
107
|
+
end
|
108
|
+
|
109
|
+
"#{field} #{direction}"
|
110
|
+
end
|
111
|
+
|
112
|
+
scope = scope.order(order)
|
113
|
+
end
|
114
|
+
|
115
|
+
scope
|
116
|
+
end
|
117
|
+
|
118
|
+
def resource_scope
|
119
|
+
preloaded_resource.all
|
120
|
+
end
|
121
|
+
|
91
122
|
def load_record
|
92
123
|
preloaded_resource.find(params[:id])
|
93
124
|
end
|
data/lib/terrain.rb
CHANGED
data/spec/spec_helper.rb
CHANGED
@@ -43,7 +43,7 @@ module Helpers
|
|
43
43
|
def serialize(value, options = {})
|
44
44
|
options[:include] ||= []
|
45
45
|
if value.respond_to?(:each)
|
46
|
-
|
46
|
+
value.map { |item| serialize(item, options) }
|
47
47
|
else
|
48
48
|
ActiveModelSerializers::SerializableResource.new(value, options).as_json.symbolize_keys
|
49
49
|
end
|
data/spec/terrain/errors_spec.rb
CHANGED
@@ -59,16 +59,36 @@ describe 'Terrain::Errors', type: :controller do
|
|
59
59
|
it { expect_json('error.key', 'route_not_found') }
|
60
60
|
end
|
61
61
|
|
62
|
-
context '
|
62
|
+
context 'range error' do
|
63
|
+
controller do
|
64
|
+
def index
|
65
|
+
raise Terrain::Page::RangeError
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
it { expect(response.status).to eq 416 }
|
70
|
+
it { expect_json_types(error: :object) }
|
71
|
+
it { expect_json_types('error.message', :string) }
|
72
|
+
it { expect_json('error.key', 'range_error') }
|
73
|
+
end
|
74
|
+
|
75
|
+
context 'record invalid' do
|
76
|
+
let(:record) { Example.new }
|
77
|
+
|
63
78
|
controller do
|
64
79
|
def index
|
65
|
-
|
80
|
+
record = Example.new
|
81
|
+
record.valid?
|
82
|
+
raise ActiveRecord::RecordInvalid, record
|
66
83
|
end
|
67
84
|
end
|
68
85
|
|
86
|
+
before { record.valid? }
|
87
|
+
|
69
88
|
it { expect(response.status).to eq 422 }
|
70
89
|
it { expect_json_types(error: :object) }
|
71
90
|
it { expect_json_types('error.message', :string) }
|
72
91
|
it { expect_json('error.key', 'record_invalid') }
|
92
|
+
it { expect_json('error.details', record.errors.to_hash) }
|
73
93
|
end
|
74
94
|
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Terrain::Page do
|
4
|
+
let!(:records) { create_list(:example, 10) }
|
5
|
+
let(:scope) { Example.all }
|
6
|
+
let(:empty_scope) { Example.where('1 = 0') }
|
7
|
+
let(:range) { '0-4' }
|
8
|
+
let(:page) { described_class.new(scope, range) }
|
9
|
+
|
10
|
+
describe '#bounds' do
|
11
|
+
subject { page.bounds }
|
12
|
+
|
13
|
+
it { is_expected.to eq [0, 4] }
|
14
|
+
|
15
|
+
context 'with empty range' do
|
16
|
+
let(:range) { '5-5' }
|
17
|
+
|
18
|
+
it { is_expected.to eq [5, 5] }
|
19
|
+
end
|
20
|
+
|
21
|
+
context 'with no upper bound' do
|
22
|
+
let(:range) { '5-' }
|
23
|
+
|
24
|
+
it { is_expected.to eq [5, 9] }
|
25
|
+
end
|
26
|
+
|
27
|
+
context 'with no lower bound' do
|
28
|
+
let(:range) { '-5' }
|
29
|
+
|
30
|
+
it { is_expected.to eq [0, 5] }
|
31
|
+
end
|
32
|
+
|
33
|
+
context 'with invalid range' do
|
34
|
+
let(:range) { '3-2' }
|
35
|
+
|
36
|
+
it { expect { subject }.to raise_error described_class::RangeError }
|
37
|
+
end
|
38
|
+
|
39
|
+
context 'with invalid upper bound' do
|
40
|
+
let(:range) { '5-x' }
|
41
|
+
|
42
|
+
it { expect { subject }.to raise_error described_class::RangeError }
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
describe '#count' do
|
47
|
+
subject { page.count }
|
48
|
+
|
49
|
+
it { is_expected.to eq 10 }
|
50
|
+
|
51
|
+
context 'with no records' do
|
52
|
+
let(:scope) { empty_scope }
|
53
|
+
|
54
|
+
it { is_expected.to eq 0 }
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
describe '#records' do
|
59
|
+
subject { page.records }
|
60
|
+
|
61
|
+
it { is_expected.to eq records[0..4] }
|
62
|
+
|
63
|
+
context 'with fewer records than requested range' do
|
64
|
+
let(:range) { '5-14' }
|
65
|
+
|
66
|
+
it { is_expected.to eq records[5..9] }
|
67
|
+
end
|
68
|
+
|
69
|
+
context 'with max records configured' do
|
70
|
+
before { Terrain.config.max_records = 3 }
|
71
|
+
after { Terrain.config.max_records = nil }
|
72
|
+
|
73
|
+
it { is_expected.to eq records[0..2] }
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
describe '#content_range' do
|
78
|
+
subject { page.content_range }
|
79
|
+
|
80
|
+
it { is_expected.to eq '0-4/10' }
|
81
|
+
|
82
|
+
context 'with fewer records than requested range' do
|
83
|
+
let(:range) { '5-14' }
|
84
|
+
|
85
|
+
it { is_expected.to eq '5-9/10' }
|
86
|
+
end
|
87
|
+
|
88
|
+
context 'with max records configured' do
|
89
|
+
before { Terrain.config.max_records = 3 }
|
90
|
+
after { Terrain.config.max_records = nil }
|
91
|
+
|
92
|
+
it { is_expected.to eq '0-2/10' }
|
93
|
+
end
|
94
|
+
|
95
|
+
context 'with no records' do
|
96
|
+
let(:scope) { empty_scope }
|
97
|
+
|
98
|
+
it { is_expected.to eq '*/0' }
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -8,7 +8,7 @@ describe 'Terrain::Resource', type: :controller do
|
|
8
8
|
end
|
9
9
|
|
10
10
|
describe '#index' do
|
11
|
-
let!(:
|
11
|
+
let!(:records) { create_list(:example, 10) }
|
12
12
|
|
13
13
|
it 'responds with 200 status' do
|
14
14
|
get :index
|
@@ -17,7 +17,105 @@ describe 'Terrain::Resource', type: :controller do
|
|
17
17
|
|
18
18
|
it 'responds with serialized records' do
|
19
19
|
get :index
|
20
|
-
expect(response.body).to eq serialize(
|
20
|
+
expect(response.body).to eq serialize(records).to_json
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'responds with Content-Range header' do
|
24
|
+
get :index
|
25
|
+
expect(response.headers['Content-Range']).to eq Terrain::Page.new(Example.all).content_range
|
26
|
+
end
|
27
|
+
|
28
|
+
context 'filtered' do
|
29
|
+
let(:record) { records.first }
|
30
|
+
|
31
|
+
before do
|
32
|
+
record.foo = 'test'
|
33
|
+
record.save!
|
34
|
+
end
|
35
|
+
|
36
|
+
controller do
|
37
|
+
resource Example
|
38
|
+
|
39
|
+
def resource_scope
|
40
|
+
super.where(foo: params[:foo])
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'responds with filtered records' do
|
45
|
+
get :index, foo: 'test'
|
46
|
+
expect(response.body).to eq serialize([record]).to_json
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
context 'ordered' do
|
51
|
+
let(:params) { ActionController::Parameters.new(order: 'foo') }
|
52
|
+
|
53
|
+
it 'responds with ordered records' do
|
54
|
+
get :index, params
|
55
|
+
expect(response.body).to eq serialize(Example.order('foo asc')).to_json
|
56
|
+
end
|
57
|
+
|
58
|
+
context 'descending' do
|
59
|
+
let(:params) { ActionController::Parameters.new(order: '-foo') }
|
60
|
+
|
61
|
+
it 'responds with ordered records' do
|
62
|
+
get :index, params
|
63
|
+
expect(response.body).to eq serialize(Example.order('foo desc')).to_json
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
context 'multiple sort columns' do
|
68
|
+
let(:params) { ActionController::Parameters.new(order: ' - foo , bar ') }
|
69
|
+
|
70
|
+
it 'responds with ordered records' do
|
71
|
+
get :index, params
|
72
|
+
expect(response.body).to eq serialize(Example.order('foo desc', 'bar asc')).to_json
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
context 'paged' do
|
78
|
+
before { request.headers['Range'] = '0-4' }
|
79
|
+
|
80
|
+
it 'responds with requested records' do
|
81
|
+
get :index
|
82
|
+
expect(response.body).to eq serialize(records[0..4]).to_json
|
83
|
+
end
|
84
|
+
|
85
|
+
it 'responds with Content-Range header' do
|
86
|
+
get :index
|
87
|
+
expect(response.headers['Content-Range']).to eq Terrain::Page.new(Example.all, '0-4').content_range
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
context 'with relations' do
|
92
|
+
before do
|
93
|
+
records.each do |record|
|
94
|
+
create_list(:widget, 3, example: record)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
it 'does not include relations in serialized record' do
|
99
|
+
get :index
|
100
|
+
expect(response.body).to eq serialize(records, include: []).to_json
|
101
|
+
end
|
102
|
+
|
103
|
+
context 'with valid include' do
|
104
|
+
let(:params) { ActionController::Parameters.new(include: 'widgets') }
|
105
|
+
|
106
|
+
it 'includes relations in serialized record' do
|
107
|
+
get :index, params
|
108
|
+
expect(response.body).to eq serialize(records, include: ['widgets']).to_json
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
context 'with invalid include' do
|
113
|
+
let(:params) { ActionController::Parameters.new(include: 'widgets,wrong') }
|
114
|
+
|
115
|
+
it 'raises error' do
|
116
|
+
expect { get :index, params }.to raise_error ActiveRecord::AssociationNotFoundError
|
117
|
+
end
|
118
|
+
end
|
21
119
|
end
|
22
120
|
end
|
23
121
|
|
data/terrain.gemspec
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: terrain
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Scott Nelson
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-05-
|
11
|
+
date: 2016-05-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -60,10 +60,14 @@ extra_rdoc_files: []
|
|
60
60
|
files:
|
61
61
|
- ".gitignore"
|
62
62
|
- ".rspec"
|
63
|
+
- ".travis.yml"
|
63
64
|
- Gemfile
|
64
65
|
- README.md
|
66
|
+
- Rakefile
|
65
67
|
- lib/terrain.rb
|
68
|
+
- lib/terrain/config.rb
|
66
69
|
- lib/terrain/errors.rb
|
70
|
+
- lib/terrain/page.rb
|
67
71
|
- lib/terrain/resource.rb
|
68
72
|
- spec/spec_helper.rb
|
69
73
|
- spec/support/example.rb
|
@@ -73,6 +77,7 @@ files:
|
|
73
77
|
- spec/support/widget.rb
|
74
78
|
- spec/support/widget_factory.rb
|
75
79
|
- spec/terrain/errors_spec.rb
|
80
|
+
- spec/terrain/page_spec.rb
|
76
81
|
- spec/terrain/resource_spec.rb
|
77
82
|
- terrain.gemspec
|
78
83
|
homepage: https://github.com/scttnlsn/terrain
|
@@ -108,4 +113,5 @@ test_files:
|
|
108
113
|
- spec/support/widget.rb
|
109
114
|
- spec/support/widget_factory.rb
|
110
115
|
- spec/terrain/errors_spec.rb
|
116
|
+
- spec/terrain/page_spec.rb
|
111
117
|
- spec/terrain/resource_spec.rb
|