terrain 0.0.1

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: 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
@@ -0,0 +1,10 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
4
+
5
+ gem 'airborne'
6
+ gem 'factory_girl'
7
+ gem 'faker'
8
+ gem 'rails'
9
+ gem 'rspec-rails'
10
+ gem 'sqlite3'
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
@@ -0,0 +1,4 @@
1
+ require 'active_support'
2
+
3
+ require 'terrain/errors'
4
+ require 'terrain/resource'
@@ -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,5 @@
1
+ class Example < ActiveRecord::Base
2
+ has_many :widgets
3
+
4
+ validates :foo, presence: true
5
+ end
@@ -0,0 +1,7 @@
1
+ FactoryGirl.define do
2
+ factory :example do
3
+ foo { Faker::Lorem.word }
4
+ bar { Faker::Lorem.word }
5
+ baz { Faker::Lorem.word }
6
+ end
7
+ end
@@ -0,0 +1,4 @@
1
+ class ExampleSerializer < ActiveModel::Serializer
2
+ attributes :foo, :bar, :baz
3
+ has_many :widgets
4
+ end
@@ -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,3 @@
1
+ class Widget < ActiveRecord::Base
2
+ belongs_to :example
3
+ end
@@ -0,0 +1,5 @@
1
+ FactoryGirl.define do
2
+ factory :widget do
3
+ example
4
+ end
5
+ 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