backframe 0.0.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: 6893ca50993e2054bfa00298833e7d1d95762c6b
4
+ data.tar.gz: fcaf54876f10f59cc5e9a8c4306caccfc8e9b5ff
5
+ SHA512:
6
+ metadata.gz: 1b3d72462843957ff8c6c5b8380f7e29d0b7133d99e79bc737527f56e9dbe985d42818c3dfd13fb2304a3bf028da1085ad7ecfe2dcab9fb33259b5e60bcd6e09
7
+ data.tar.gz: 68ebc292ef2ea169459348d7c31955976bcdd8432afb30b066ee7d88da46ca9270cb037bfa816ad4997ce7287b52e096772230ee61357bbcb5b2dd752e541743
data/.gitignore ADDED
@@ -0,0 +1,10 @@
1
+ .project
2
+ .DS_Store
3
+ .bundle
4
+ coverage
5
+ rdoc
6
+ pkg
7
+ *.gem
8
+ example.rb
9
+ .idea/
10
+ Gemfile.lock
data/.idea/vcs.xml ADDED
@@ -0,0 +1,6 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="VcsDirectoryMappings">
4
+ <mapping directory="$PROJECT_DIR$" vcs="Git" />
5
+ </component>
6
+ </project>
data/Gemfile ADDED
@@ -0,0 +1,14 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
4
+
5
+ group :development, :test do
6
+ gem 'active_model_serializers', '0.10.0.rc4'
7
+ gem 'factory_girl'
8
+ gem 'faker'
9
+ gem 'kaminari'
10
+ gem 'rails'
11
+ gem 'rake'
12
+ gem 'rspec-rails'
13
+ gem 'sqlite3'
14
+ end
data/README.md ADDED
@@ -0,0 +1,6 @@
1
+ # Backframe
2
+
3
+ ## Development
4
+
5
+ bundle install
6
+ bundle exec rake spec
data/Rakefile ADDED
@@ -0,0 +1,25 @@
1
+ $LOAD_PATH.unshift File.expand_path("../lib", __FILE__)
2
+ require 'rspec/core/rake_task'
3
+ require 'bundler/version'
4
+ require './lib/backframe'
5
+
6
+ RSpec::Core::RakeTask.new(:spec) do |t|
7
+ t.rspec_opts = ['--color']
8
+ end
9
+
10
+ task default: :spec
11
+
12
+ desc "Build the gem"
13
+ task :build do
14
+ system "gem build backframe.gemspec"
15
+ end
16
+
17
+ desc "install the gem"
18
+ task :install do
19
+ system "gem install backframe-#{Backframe::VERSION}.gem"
20
+ end
21
+
22
+ desc "Build and release the gem"
23
+ task :release => :build do
24
+ system "gem push backframe-#{Backframe::VERSION}.gem"
25
+ end
data/backframe.gemspec ADDED
@@ -0,0 +1,22 @@
1
+ lib = File.expand_path('../lib', __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+
4
+ require 'backframe/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = 'backframe'
8
+ gem.email = 'hello@thinktopography.com'
9
+ gem.description = 'Rails bindings for Reframe'
10
+ gem.version = Backframe::VERSION
11
+ gem.summary = 'Backframe'
12
+ gem.authors = ['Greg Kops', 'Scott Nelson']
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.add_runtime_dependency 'activesupport', '~> 4.0'
20
+ gem.add_runtime_dependency 'active_model_serializers', '>= 0.10.0.rc4'
21
+ gem.add_runtime_dependency 'kaminari', '~> 0.16'
22
+ end
@@ -0,0 +1,49 @@
1
+ module Backframe
2
+ module API
3
+ class Adapter < ActiveModel::Serializer::Adapter::Attributes
4
+ attr_reader :fields, :links
5
+
6
+ def initialize(serializer, options = {})
7
+ super
8
+ @fields = options[:fields]
9
+ @links = options[:links]
10
+ end
11
+
12
+ def serializable_hash(options = nil)
13
+ if paginated?
14
+ with_pagination_metadata(super)
15
+ else
16
+ select_fields(super)
17
+ end
18
+ end
19
+
20
+ def paginated?
21
+ serializer.respond_to?(:paginated?) && serializer.paginated?
22
+ end
23
+
24
+ private
25
+
26
+ def with_pagination_metadata(records)
27
+ {
28
+ records: records.map(&method(:select_fields)),
29
+ total_records: paginated.total_count,
30
+ total_pages: paginated.total_pages,
31
+ current_page: paginated.current_page,
32
+ links: links
33
+ }
34
+ end
35
+
36
+ def paginated
37
+ serializer.object
38
+ end
39
+
40
+ def select_fields(object)
41
+ if fields.present?
42
+ object.select { |key, val| fields.include?(key) }
43
+ else
44
+ object
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,46 @@
1
+ module Backframe
2
+ module API
3
+ module Errors
4
+ def unauthenticated_request
5
+ error_response(:unauthenticated, 401)
6
+ end
7
+
8
+ def unauthorized_request
9
+ error_response(:unauthorized, 403)
10
+ end
11
+
12
+ def resource_not_found
13
+ error_response(:resource_not_found, 404)
14
+ end
15
+
16
+ def route_not_found
17
+ error_response(:route_not_found, 404)
18
+ end
19
+
20
+ private
21
+
22
+ def error_response(code, status = 500)
23
+ # TODO: Create generator for translations
24
+
25
+ result = {
26
+ error: {
27
+ message: I18n.t("backframe.api.#{code}", method: request.method),
28
+ status: status
29
+ }
30
+ }
31
+
32
+ render json: result, status: status
33
+ end
34
+
35
+ def resource_error_response(resource, status = 500)
36
+ result = {
37
+ message: I18n.t('backframe.api.resource_error', request: request),
38
+ errors: resource.errors,
39
+ status: status
40
+ }
41
+
42
+ render json: result, status: status
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,9 @@
1
+ module Backframe
2
+ module API
3
+ module Headers
4
+ def set_expiration_header
5
+ headers['Last-Modified'] = Time.now.httpdate
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,177 @@
1
+ module Backframe
2
+ module API
3
+ module Page
4
+ DEFAULT_PAGE = 1
5
+ DEFAULT_PER_PAGE = 100
6
+
7
+ def page(collection, serializer = nil)
8
+ classname = collection.base_class.name+'Serializer'
9
+ serializer ||= classname.constantize
10
+ args = params.except(*request.path_parameters.keys)
11
+ filters = args.except([:sort,:page,:per_page,:fields,:exclude_ids])
12
+ model = (collection.respond_to?(:klass)) ? collection.klass.name.constantize : collection
13
+ collection = model.filter(collection, filters) if collection.respond_to?(:filter)
14
+
15
+ if args.key?(:sort)
16
+ args[:sort].split(',').each do |sort|
17
+ key = (sort[0] == '-') ? sort[1..-1] : sort
18
+ order = (sort[0] == '-') ? 'desc' : 'asc'
19
+ collection = model.sort(collection, key, order)
20
+ end
21
+ else
22
+ collection = model.sort(collection)
23
+ end
24
+
25
+ if args.key?(:exclude_ids)
26
+ ids = args[:exclude_ids].split(',')
27
+ collection = collection.where('id NOT IN (?)', ids)
28
+ end
29
+
30
+ if args.key?(:all)
31
+ args[:page] = 1
32
+ args[:per_page] = 10000
33
+ end
34
+
35
+ args[:page] ||= DEFAULT_PAGE
36
+ args[:per_page] ||= DEFAULT_PER_PAGE
37
+
38
+
39
+ collection = collection.page(args[:page]).per(args[:per_page])
40
+ fields = (args.key?(:fields)) ? args[:fields].split(',').map(&:to_sym) : serializer._attributes
41
+
42
+ respond_to do |format|
43
+ format.json {
44
+ render json: collection,
45
+ content_type: 'application/json',
46
+ adapter: Backframe::API::Adapter,
47
+ fields: fields,
48
+ links: pagination_links(collection, args[:per_page], args[:page])
49
+ }
50
+ format.csv {
51
+ content_type = (args.key?(:plain)) ? 'text/plain' : 'text/csv'
52
+ render :text => collection_to_csv(collection, serializer, fields, ","), :content_type => content_type, :status => 200
53
+ }
54
+ format.tsv {
55
+ content_type = (args.key?(:plain)) ? 'text/plain' : 'text/tab-separated-values'
56
+ render :text => collection_to_csv(collection, serializer, fields, "\t"), :content_type => content_type, :status => 200
57
+ }
58
+ format.xls {
59
+ content_type = (args.key?(:plain)) ? 'text/xml' : 'application/xls'
60
+ render :text => collection_to_xls(collection, serializer, fields), :content_type => content_type, :status => 200
61
+ }
62
+ format.xlsx {
63
+ content_type = (args.key?(:plain)) ? 'text/xml' : 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
64
+ render :text => collection_to_xls(collection, serializer, fields), :content_type => content_type, :status => 200
65
+ }
66
+ end
67
+ end
68
+
69
+ def collection_to_csv(collection, serializer, fields, separator)
70
+ rows = []
71
+ line = []
72
+ headers = []
73
+ collection.each do |record|
74
+ headers = []
75
+ line = []
76
+ serialized = OpenStruct.new(ActiveModel::SerializableResource.new(record).as_json)
77
+ fields.each do |fullkey|
78
+ value = serialized
79
+ fullkey.to_s.split(".").each do |key|
80
+ if value.respond_to?(key)
81
+ value = value.send(key)
82
+ if value.is_a?(Time)
83
+ value = value.strftime("%F %T")
84
+ elsif value.is_a?(Date)
85
+ value = value.strftime("%F")
86
+ elsif value.is_a?(Hash)
87
+ value = OpenStruct.new(value)
88
+ else
89
+ value = value.to_s
90
+ end
91
+ else
92
+ value = nil
93
+ end
94
+ end
95
+ if !value.is_a?(OpenStruct)
96
+ headers << fullkey
97
+ line << value
98
+ end
99
+ end
100
+ rows << line.join(separator)
101
+ end
102
+ rows.unshift(headers.join(separator))
103
+ rows.join("\n")
104
+ end
105
+
106
+ def collection_to_xls(collection, serializer, fields)
107
+ rows = []
108
+ rows << '<?xml version="1.0"?>'
109
+ rows << '<Workbook xmlns="urn:schemas-microsoft-com:office:spreadsheet" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:x="urn:schemas-microsoft-com:office:excel" xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet" xmlns:html="http://www.w3.org/TR/REC-html40">'
110
+ rows << '<Worksheet ss:Name="Sheet1">'
111
+ rows << '<Table>'
112
+ line = []
113
+ fields.each do |key|
114
+ line << '<Cell><Data ss:Type="String">'+key+'</Data></Cell>'
115
+ end
116
+ rows << "<Row>"+line.join+"</Row>"
117
+ collection.all.each do |record|
118
+ line = []
119
+ serialized = serializer.new(record)
120
+ fields.each do |fullkey|
121
+ value = serialized
122
+ fullkey.to_s.split(".").each do |key|
123
+ if value.respond_to?(key)
124
+ value = value.send(key)
125
+ if value.is_a?(Hash)
126
+ value = OpenStruct.new(value)
127
+ end
128
+ else
129
+ value = nil
130
+ end
131
+ end
132
+ if value.is_a?(Numeric)
133
+ line << '<Cell><Data ss:Type="Number">'+value+'</Data></Cell>'
134
+ elsif value.is_a?(Time)
135
+ line << '<Cell><Data ss:Type="String">'+value.strftime("%F %T")+'</Data></Cell>'
136
+ elsif value.is_a?(Date)
137
+ line << '<Cell><Data ss:Type="String">'+value.strftime("%F")+'</Data></Cell>'
138
+ elsif value.is_a?(String)
139
+ line << '<Cell><Data ss:Type="String">'+value+'</Data></Cell>'
140
+ else
141
+ line << '<Cell><Data ss:Type="String"></Data></Cell>'
142
+ end
143
+ end
144
+ rows << "<Row>"+line.join+"</Row>"
145
+ end
146
+ rows << '</Table>'
147
+ rows << '</Worksheet>'
148
+ rows << '</Workbook>'
149
+ rows.join("\n")
150
+ end
151
+
152
+ def pagination_links(collection, per_page, page)
153
+ return {} if collection.total_count.zero?
154
+ links = {}
155
+ links[:self] = pagination_link(per_page, page)
156
+ if collection.next_page.present?
157
+ links[:next] = pagination_link(per_page, collection.next_page)
158
+ end
159
+ if page.to_i < collection.total_pages
160
+ links[:last] = pagination_link(per_page, collection.total_pages)
161
+ end
162
+ if page.to_i > 1
163
+ links[:first] = pagination_link(per_page, 1)
164
+ end
165
+ if collection.prev_page.present?
166
+ links[:prev] = pagination_link(per_page, collection.prev_page)
167
+ end
168
+ links
169
+ end
170
+
171
+ def pagination_link(per_page, page)
172
+ args = (per_page.to_i != DEFAULT_PER_PAGE) ? "per_page="+per_page.to_s+"&page="+page.to_s : "page="+page.to_s
173
+ base_api_url+request.path+"?"+args
174
+ end
175
+ end
176
+ end
177
+ end
@@ -0,0 +1,35 @@
1
+ require 'active_support'
2
+ require 'active_support/inflector'
3
+ require 'active_model_serializers'
4
+
5
+ require 'backframe/api/adapter'
6
+ require 'backframe/api/errors'
7
+ require 'backframe/api/headers'
8
+ require 'backframe/api/page'
9
+
10
+ module Backframe
11
+ module API
12
+ extend ActiveSupport::Concern
13
+
14
+ class_methods do
15
+ def acts_as_api
16
+ include Errors
17
+ include Headers
18
+ include Page
19
+
20
+ before_action :set_expiration_header
21
+
22
+ rescue_from Exceptions::Unauthenticated, :with => :unauthenticated_request
23
+ rescue_from Exceptions::Unauthorized, :with => :unauthorized_request
24
+ rescue_from 'ActiveRecord::RecordNotFound', :with => :resource_not_found
25
+ rescue_from 'ActionController::RoutingError', :with => :route_not_found
26
+ end
27
+ end
28
+
29
+ included do
30
+ def base_api_url
31
+ raise 'must be overridden'
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,11 @@
1
+ module Backframe
2
+ module Mime
3
+ extend self
4
+
5
+ def register_types
6
+ ::Mime::Type.register('text/tab-separated-values', :tsv)
7
+ ::Mime::Type.register('application/vnd.ms-excel', :xls)
8
+ ::Mime::Type.register('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', :xlsx)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,99 @@
1
+ module Backframe
2
+ module Resource
3
+ module Actions
4
+ module Index
5
+ def index
6
+ page(resource.includes(self.class.resource_opts[:include]), nil)
7
+ end
8
+ end
9
+
10
+ module Create
11
+ def create
12
+ @item = resource.new(allowed_params)
13
+ if @item.save
14
+ if @item.respond_to?(:activities)
15
+ Activity.create!(subject: current_user, text: 'created {object1}', object1: @item)
16
+ end
17
+
18
+ render json: @item, status: 201, adapter: Backframe::API::Adapter
19
+ else
20
+ resource_error_response(@item, 422)
21
+ end
22
+ end
23
+ end
24
+
25
+ module Show
26
+ def show
27
+ render json: @item, adapter: Backframe::API::Adapter
28
+ end
29
+ end
30
+
31
+ module Edit
32
+ def edit
33
+ json = {}
34
+ self.class.resource_opts[:allowed].each do |attribute|
35
+ if(attribute.is_a?(Hash))
36
+ attribute.each do |key,val|
37
+ json[key] = @item.send(key)
38
+ end
39
+ else
40
+ json[attribute] = @item.send(attribute)
41
+ end
42
+ end
43
+ render json: json
44
+ end
45
+ end
46
+
47
+ module Update
48
+ def update
49
+ if @item.update_attributes(allowed_params)
50
+ if @item.respond_to?(:activities)
51
+ Activity.create!(subject: current_user, text: 'updated {object1}', object1: @item)
52
+ end
53
+
54
+ render json: @item, adapter: Backframe::API::Adapter
55
+ else
56
+ resource_error_response(@item, 422)
57
+ end
58
+ end
59
+ end
60
+
61
+ module UpdateAll
62
+ def update_all
63
+ records = []
64
+
65
+ ActiveRecord::Base.transaction do
66
+ records = update_all_params.map(&method(:update_record))
67
+ end
68
+
69
+ render json: records, adapter: Backframe::API::Adapter
70
+ end
71
+
72
+ private
73
+
74
+ def update_record(attrs)
75
+ attrs = attrs.with_indifferent_access
76
+ record = resource.find(attrs[:id])
77
+ record.attributes = attrs
78
+ record.save!
79
+ record
80
+ end
81
+
82
+ def update_all_params
83
+ permitted = self.class.resource_opts[:allowed] << :id
84
+ params.permit(records: permitted)[:records] || []
85
+ end
86
+ end
87
+
88
+ module Destroy
89
+ def destroy
90
+ if @item.destroy
91
+ render nothing: true, status: 204
92
+ else
93
+ resource_error_response(@item, 422)
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,90 @@
1
+ require 'active_support'
2
+
3
+ require 'backframe/resource/actions'
4
+
5
+ module Backframe
6
+ module Resource
7
+ extend ActiveSupport::Concern
8
+
9
+ class_methods do
10
+ def acts_as_resource(resource, *args)
11
+ arguments = (args.present?) ? args[0] : {}
12
+
13
+ @resource = resource
14
+ @resource_opts = arguments
15
+
16
+ include Helpers
17
+
18
+ member_methods = [:show,:edit,:update,:destroy]
19
+ if arguments.key?(:has_many)
20
+ member_methods.concat(arguments[:has_many])
21
+ end
22
+
23
+ if arguments.key?(:only)
24
+ member_methods = (member_methods.to_set & arguments[:only].to_set).to_a
25
+ end
26
+
27
+ if arguments.key?(:except)
28
+ member_methods = (member_methods.to_set - arguments[:except].to_set).to_a
29
+ end
30
+
31
+ before_action :load_item, only: member_methods
32
+
33
+ include Actions::Index if include_method?(arguments, :index)
34
+ include Actions::Create if include_method?(arguments, :create)
35
+ include Actions::Show if include_method?(arguments, :show)
36
+ include Actions::Edit if include_method?(arguments, :edit)
37
+ include Actions::Update if include_method?(arguments, :update)
38
+ include Actions::UpdateAll if include_method?(arguments, :update)
39
+ include Actions::Destroy if include_method?(arguments, :destroy)
40
+
41
+ if arguments.key?(:has_many)
42
+ arguments[:has_many].each do |association|
43
+ class_eval <<-EOV
44
+ def #{association}
45
+ page(@item.#{association})
46
+ end
47
+ EOV
48
+ end
49
+ end
50
+ end
51
+
52
+ def resource; @resource; end
53
+ def resource_opts; @resource_opts; end
54
+
55
+ private
56
+
57
+ def include_method?(arguments, method)
58
+ (arguments[:only].present? && arguments[:only].include?(method)) ||
59
+ (arguments[:except].present? && !arguments[:except].include?(method)) ||
60
+ (arguments[:only].nil? && arguments[:except].nil?)
61
+ end
62
+ end
63
+ end
64
+
65
+ module Helpers
66
+ def resource
67
+ self.class.resource.constantize
68
+ rescue NameError
69
+ instance_eval self.class.resource
70
+ end
71
+
72
+ def load_item
73
+ @item = resource.find(params[:id])
74
+ end
75
+
76
+ def allowed_params
77
+ allowed = params.permit(self.class.resource_opts[:allowed])
78
+ self.class.resource_opts[:allowed].each do |attribute|
79
+ if attribute.is_a?(Hash)
80
+ attribute.each do |key,value|
81
+ if value.is_a?(Array) && !allowed.key?(key)
82
+ allowed[key] = []
83
+ end
84
+ end
85
+ end
86
+ end
87
+ allowed
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,3 @@
1
+ module Backframe
2
+ VERSION = '0.0.0'
3
+ end
data/lib/backframe.rb ADDED
@@ -0,0 +1,10 @@
1
+ require 'backframe/api'
2
+ require 'backframe/mime'
3
+ require 'backframe/resource'
4
+
5
+ module Backframe
6
+ module Exceptions
7
+ class Unauthenticated < StandardError; end
8
+ class Unauthorized < StandardError; end
9
+ end
10
+ end
@@ -0,0 +1,225 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'Backframe::API', type: :controller do
4
+ controller do
5
+ include Backframe::API
6
+ acts_as_api
7
+
8
+ def base_api_url
9
+ '/example'
10
+ end
11
+ end
12
+
13
+ describe 'error handlng' do
14
+ before { get :index }
15
+
16
+ context 'resource not found' do
17
+ controller do
18
+ def index
19
+ raise ActiveRecord::RecordNotFound
20
+ end
21
+ end
22
+
23
+ it 'responds with 404 status' do
24
+ expect(response.status).to eq 404
25
+ end
26
+
27
+ it 'responds with JSON body' do
28
+ expect(json_response[:error][:status]).to eq 404
29
+ expect(json_response[:error][:message]).to be
30
+ end
31
+ end
32
+
33
+ context 'route not found' do
34
+ controller do
35
+ def index
36
+ raise ActionController::RoutingError, ''
37
+ end
38
+ end
39
+
40
+ it 'responds with 404 status' do
41
+ expect(response.status).to eq 404
42
+ end
43
+
44
+ it 'responds with JSON body' do
45
+ expect(json_response[:error][:status]).to eq 404
46
+ expect(json_response[:error][:message]).to be
47
+ end
48
+ end
49
+
50
+ context 'unauthenticated' do
51
+ controller do
52
+ def index
53
+ raise Backframe::Exceptions::Unauthenticated
54
+ end
55
+ end
56
+
57
+ it 'responds with 401 status' do
58
+ expect(response.status).to eq 401
59
+ end
60
+
61
+ it 'responds with JSON body' do
62
+ expect(json_response[:error][:status]).to eq 401
63
+ expect(json_response[:error][:message]).to be
64
+ end
65
+ end
66
+
67
+ context 'unauthorized' do
68
+ controller do
69
+ def index
70
+ raise Backframe::Exceptions::Unauthorized
71
+ end
72
+ end
73
+
74
+ it 'responds with 403 status' do
75
+ expect(response.status).to eq 403
76
+ end
77
+
78
+ it 'responds with JSON body' do
79
+ expect(json_response[:error][:status]).to eq 403
80
+ expect(json_response[:error][:message]).to be
81
+ end
82
+ end
83
+ end
84
+
85
+ describe 'headers' do
86
+ controller do
87
+ def index
88
+ render nothing: true
89
+ end
90
+ end
91
+
92
+ it 'responds with Last-Modified header' do
93
+ get :index
94
+
95
+ # FIXME: Use timecop here
96
+ expect(response.headers['Last-Modified']).to eq Time.now.httpdate
97
+ end
98
+ end
99
+
100
+ describe '#page' do
101
+ let!(:records) { create_list(:example, 10) }
102
+
103
+ controller do
104
+ def index
105
+ page(Example.all, nil)
106
+ end
107
+ end
108
+
109
+ it 'responds with 200 status' do
110
+ get :index, format: :json
111
+ expect(response.status).to eq 200
112
+ end
113
+
114
+ it 'responds with records' do
115
+ get :index, format: :json
116
+ expect(json_response[:records].length).to eq 10
117
+ end
118
+
119
+ it 'responds with links' do
120
+ get :index, format: :json
121
+ expect(json_response[:links]).to be
122
+ expect(json_response[:links][:self]).to eq '/example/anonymous.json?page=1'
123
+ end
124
+
125
+ context 'with pagination params' do
126
+ before { get :index, page: 2, per_page: 3, format: :json }
127
+
128
+ it 'serializes one page of records' do
129
+ expect(json_response[:records].length).to eq 3
130
+
131
+ serialized = serialize(Example.sort(Example)[3]).stringify_keys
132
+ expect(json_response[:records].first).to eq serialized
133
+ end
134
+
135
+ it 'includes pagination metadata' do
136
+ expect(json_response[:total_records]).to eq 10
137
+ expect(json_response[:total_pages]).to eq 4
138
+ expect(json_response[:current_page]).to eq 2
139
+ end
140
+ end
141
+
142
+ context 'without pagination params' do
143
+ before { get :index, format: :json }
144
+
145
+ it 'serializes all records' do
146
+ expect(json_response[:records].length).to eq 10
147
+
148
+ serialized = serialize(Example.sort(Example).first).stringify_keys
149
+ expect(json_response[:records].first).to eq serialized
150
+ end
151
+
152
+ it 'includes pagination metadata' do
153
+ expect(json_response[:total_records]).to eq 10
154
+ expect(json_response[:total_pages]).to eq 1
155
+ expect(json_response[:current_page]).to eq 1
156
+ end
157
+ end
158
+
159
+ context 'with sort params' do
160
+ controller do
161
+ def index
162
+ page(Example.all)
163
+ end
164
+ end
165
+
166
+ context 'with single param' do
167
+ before do
168
+ create_list(:example, 10)
169
+ get :index, sort: 'a', format: :json
170
+ end
171
+
172
+ it 'responds with records sorted ascending' do
173
+ values = json_response[:records].map { |record| record[:a] }
174
+ expect(values).to eq values.sort
175
+ end
176
+ end
177
+
178
+ context 'with negated param' do
179
+ before do
180
+ create_list(:example, 10)
181
+ get :index, sort: '-a', format: :json
182
+ end
183
+
184
+ it 'responds with records sorted descending' do
185
+ values = json_response[:records].map { |record| record[:a] }
186
+ expect(values).to eq values.sort.reverse
187
+ end
188
+ end
189
+
190
+ context 'with multiple params' do
191
+ before do
192
+ create_list(:example, 10, a: 'a')
193
+ create_list(:example, 10)
194
+ get :index, sort: 'a,b', format: :json
195
+ end
196
+
197
+ it 'responds with records sorted by both fields' do
198
+ values = json_response[:records].map { |record| [record[:a], record[:b]] }
199
+ expect(values).to eq values.sort
200
+ end
201
+ end
202
+ end
203
+
204
+ context 'with field params' do
205
+ let!(:records) { create_list(:example, 10) }
206
+
207
+ before { get :index, fields: 'a,b', format: :json }
208
+
209
+ it 'only responds with given fields' do
210
+ expect(json_response[:records].first).to eq serialize(Example.sort(Example).first, fields: [:a, :b]).stringify_keys
211
+ end
212
+ end
213
+
214
+ context 'with exclude ids params' do
215
+ let!(:records) { create_list(:example, 10) }
216
+ let(:exclude_ids) { records.take(5).map(&:id).join(',') }
217
+
218
+ before { get :index, exclude_ids: exclude_ids, format: :json }
219
+
220
+ it 'responds with unexcluded records' do
221
+ expect(json_response[:records].length).to eq 5
222
+ end
223
+ end
224
+ end
225
+ end
@@ -0,0 +1,178 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'Backframe::Resource', type: :controller do
4
+ controller do
5
+ include Backframe::API
6
+ include Backframe::Resource
7
+
8
+ acts_as_api
9
+ acts_as_resource 'Example', allowed: [:a, :b, :c]
10
+
11
+ def base_api_url
12
+ '/example'
13
+ end
14
+ end
15
+
16
+ describe '#index' do
17
+ it 'responds with 200 status' do
18
+ get :index, format: :json
19
+ expect(response.status).to eq 200
20
+ end
21
+ end
22
+
23
+ describe '#create' do
24
+ let(:params) { attributes_for(:example).merge(format: :json) }
25
+
26
+ it 'responds with 201 status' do
27
+ post :create, params
28
+ expect(response.status).to eq 201
29
+ end
30
+
31
+ it 'creates record' do
32
+ expect { post :create, params }.to change { Example.count }.by 1
33
+ end
34
+
35
+ it 'responds with record' do
36
+ post :create, params
37
+ expect(json_response).to eq serialize(Example.last).stringify_keys
38
+ end
39
+
40
+ context 'when save fails' do
41
+ before do
42
+ allow_any_instance_of(Example).to receive(:save) { false }
43
+ end
44
+
45
+ it 'responds with 422 status' do
46
+ post :create, params
47
+ expect(response.status).to eq 422
48
+ end
49
+
50
+ it 'does not create record' do
51
+ expect { post :create, params }.to change { Example.count }.by 0
52
+ end
53
+
54
+ it 'responds with errors' do
55
+ post :create, params
56
+ expect(json_response[:errors]).to be
57
+ expect(json_response[:message]).to be
58
+ expect(json_response[:status]).to eq 422
59
+ end
60
+ end
61
+ end
62
+
63
+ describe '#show' do
64
+ let!(:record) { create(:example) }
65
+
66
+ it 'responds with 200 status' do
67
+ get :show, id: record.id
68
+ expect(response.status).to eq 200
69
+ end
70
+
71
+ it 'responds with record' do
72
+ get :show, id: record.id
73
+ expect(json_response).to eq serialize(record).stringify_keys
74
+ end
75
+ end
76
+
77
+ describe '#edit' do
78
+ let!(:record) { create(:example) }
79
+
80
+ it 'responds with 200 status' do
81
+ get :edit, id: record.id
82
+ expect(response.status).to eq 200
83
+ end
84
+
85
+ it 'responds with record' do
86
+ get :edit, id: record.id
87
+ expect(json_response).to eq serialize(record).stringify_keys
88
+ end
89
+ end
90
+
91
+ describe '#update' do
92
+ let!(:record) { create(:example) }
93
+ let(:params) { attributes_for(:example).merge(id: record.id) }
94
+
95
+ it 'responds with 200 status' do
96
+ patch :update, params
97
+ expect(response.status).to eq 200
98
+ end
99
+
100
+ it 'udpates record' do
101
+ patch :update, params
102
+ expect(json_response[:a]).to eq params[:a]
103
+ end
104
+
105
+ it 'responds with updated record' do
106
+ patch :update, params
107
+ expect(json_response).to eq serialize(record.reload).stringify_keys
108
+ end
109
+
110
+ context 'when save fails' do
111
+ before do
112
+ allow_any_instance_of(Example).to receive(:save) { false }
113
+ patch :update, params
114
+ end
115
+
116
+ it 'responds with 422 status' do
117
+ expect(response.status).to eq 422
118
+ end
119
+
120
+ it 'does not update record' do
121
+ expect(record.a).to eq record.reload.a
122
+ end
123
+
124
+ it 'responds with errors' do
125
+ expect(json_response[:errors]).to be
126
+ expect(json_response[:message]).to be
127
+ expect(json_response[:status]).to eq 422
128
+ end
129
+ end
130
+ end
131
+
132
+ describe '#update_all' do
133
+ let!(:records) { create_list(:example, 3) }
134
+ let(:attrs) { records.map { |record| { id: record.id, a: 'a' } } }
135
+ let(:params) { ActionController::Parameters.new(records: attrs, format: :json) }
136
+
137
+ before do
138
+ routes.draw { post :update_all, to: 'anonymous#update_all' }
139
+ post :update_all, params
140
+ end
141
+
142
+ it 'responds with 200 status' do
143
+ expect(response.status).to eq 200
144
+ end
145
+
146
+ it 'updates all records' do
147
+ records.each { |record| expect(record.reload.a).to eq 'a' }
148
+ end
149
+ end
150
+
151
+ describe '#destroy' do
152
+ let!(:record) { create(:example) }
153
+
154
+ it 'responds with 204 status' do
155
+ delete :destroy, id: record.id
156
+ expect(response.status).to eq 204
157
+ end
158
+
159
+ it 'deletes record' do
160
+ expect { delete :destroy, id: record.id }.to change { Example.count }.by(-1)
161
+ end
162
+
163
+ context 'when delete fails' do
164
+ before do
165
+ allow_any_instance_of(Example).to receive(:destroy) { false }
166
+ end
167
+
168
+ it 'responds with 422 status' do
169
+ delete :destroy, id: record.id
170
+ expect(response.status).to eq 422
171
+ end
172
+
173
+ it 'does not delete record' do
174
+ expect { delete :destroy, id: record.id }.to change { Example.count }.by 0
175
+ end
176
+ end
177
+ end
178
+ end
@@ -0,0 +1,61 @@
1
+ ENV['RAILS_ENV'] = 'test'
2
+
3
+ require 'backframe'
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 'factory_girl'
11
+ require 'kaminari'
12
+ require 'rspec/rails'
13
+
14
+ LOGGER = Logger.new('/dev/null')
15
+
16
+ Rails.logger = LOGGER
17
+ ActiveModelSerializers.logger = LOGGER
18
+ ActiveRecord::Base.logger = LOGGER
19
+
20
+ DATABASE = {
21
+ adapter: 'sqlite3',
22
+ database: ':memory:'
23
+ }
24
+
25
+ ActiveRecord::Migration.verbose = false
26
+ ActiveRecord::Base.establish_connection(DATABASE)
27
+
28
+ module Backframe
29
+ class Application < ::Rails::Application
30
+ def self.find_root(from)
31
+ Dir.pwd
32
+ end
33
+
34
+ config.eager_load = false
35
+ config.secret_key_base = 'secret'
36
+ end
37
+ end
38
+
39
+ Backframe::Application.initialize!
40
+ Backframe::Mime.register_types
41
+
42
+ module Helpers
43
+ def json_response
44
+ @json_response ||= JSON.parse(response.body).with_indifferent_access
45
+ end
46
+
47
+ def serialize(value, opts = {})
48
+ opts[:adapter] ||= Backframe::API::Adapter
49
+ ActiveModel::SerializableResource.new(value, opts).as_json
50
+ end
51
+ end
52
+
53
+ RSpec.configure do |config|
54
+ config.include Helpers
55
+ config.include Rails.application.routes.url_helpers
56
+ config.include FactoryGirl::Syntax::Methods
57
+
58
+ config.use_transactional_fixtures = true
59
+ end
60
+
61
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
@@ -0,0 +1,20 @@
1
+ class Example < ActiveRecord::Base
2
+ def self.sort(relation, key = nil, order = nil)
3
+ self._sort(relation, key, order, 'created_at', 'desc', [self])
4
+ end
5
+
6
+ def self._sort(relation, key, order, default_key = 'created_at', default_order = 'desc', included = nil)
7
+ sortfields = {}
8
+ included ||= [self]
9
+ included.each do |model|
10
+ model.columns.each do |column|
11
+ sortkey = (model == self) ? column.name : "#{model.table_name.singularize}.#{column.name}"
12
+ sortfields[sortkey] = "\"#{model.table_name}\".\"#{column.name}\""
13
+ end
14
+ end
15
+
16
+ key = (key.present? && sortfields.has_key?(key)) ? key : default_key
17
+ order = (order.present?) ? order : default_order
18
+ relation.order("#{sortfields[key]} #{order}")
19
+ end
20
+ end
@@ -0,0 +1,9 @@
1
+ require 'faker'
2
+
3
+ FactoryGirl.define do
4
+ factory :example do
5
+ a { Faker::Lorem.word }
6
+ b { Faker::Lorem.word }
7
+ c { Faker::Lorem.word }
8
+ end
9
+ end
@@ -0,0 +1,3 @@
1
+ class ExampleSerializer < ActiveModel::Serializer
2
+ attributes :a, :b, :c
3
+ end
@@ -0,0 +1,8 @@
1
+ ActiveRecord::Schema.define(version: 1) do
2
+ create_table 'examples' do |t|
3
+ t.string 'a'
4
+ t.string 'b'
5
+ t.string 'c'
6
+ t.timestamps null: false
7
+ end
8
+ end
metadata ADDED
@@ -0,0 +1,115 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: backframe
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Greg Kops
8
+ - Scott Nelson
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2016-04-28 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: activesupport
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - "~>"
19
+ - !ruby/object:Gem::Version
20
+ version: '4.0'
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - "~>"
26
+ - !ruby/object:Gem::Version
27
+ version: '4.0'
28
+ - !ruby/object:Gem::Dependency
29
+ name: active_model_serializers
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - ">="
33
+ - !ruby/object:Gem::Version
34
+ version: 0.10.0.rc4
35
+ type: :runtime
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: 0.10.0.rc4
42
+ - !ruby/object:Gem::Dependency
43
+ name: kaminari
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - "~>"
47
+ - !ruby/object:Gem::Version
48
+ version: '0.16'
49
+ type: :runtime
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - "~>"
54
+ - !ruby/object:Gem::Version
55
+ version: '0.16'
56
+ description: Rails bindings for Reframe
57
+ email: hello@thinktopography.com
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - ".gitignore"
63
+ - ".idea/vcs.xml"
64
+ - Gemfile
65
+ - README.md
66
+ - Rakefile
67
+ - backframe.gemspec
68
+ - lib/backframe.rb
69
+ - lib/backframe/api.rb
70
+ - lib/backframe/api/adapter.rb
71
+ - lib/backframe/api/errors.rb
72
+ - lib/backframe/api/headers.rb
73
+ - lib/backframe/api/page.rb
74
+ - lib/backframe/mime.rb
75
+ - lib/backframe/resource.rb
76
+ - lib/backframe/resource/actions.rb
77
+ - lib/backframe/version.rb
78
+ - spec/backframe/api_spec.rb
79
+ - spec/backframe/resource_spec.rb
80
+ - spec/spec_helper.rb
81
+ - spec/support/example.rb
82
+ - spec/support/example_factory.rb
83
+ - spec/support/example_serializer.rb
84
+ - spec/support/schema.rb
85
+ homepage:
86
+ licenses: []
87
+ metadata: {}
88
+ post_install_message:
89
+ rdoc_options: []
90
+ require_paths:
91
+ - lib
92
+ required_ruby_version: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ required_rubygems_version: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - ">="
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ requirements: []
103
+ rubyforge_project:
104
+ rubygems_version: 2.4.6
105
+ signing_key:
106
+ specification_version: 4
107
+ summary: Backframe
108
+ test_files:
109
+ - spec/backframe/api_spec.rb
110
+ - spec/backframe/resource_spec.rb
111
+ - spec/spec_helper.rb
112
+ - spec/support/example.rb
113
+ - spec/support/example_factory.rb
114
+ - spec/support/example_serializer.rb
115
+ - spec/support/schema.rb