backframe 0.0.0

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 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