backframe 0.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.idea/vcs.xml +6 -0
- data/Gemfile +14 -0
- data/README.md +6 -0
- data/Rakefile +25 -0
- data/backframe.gemspec +22 -0
- data/lib/backframe/api/adapter.rb +49 -0
- data/lib/backframe/api/errors.rb +46 -0
- data/lib/backframe/api/headers.rb +9 -0
- data/lib/backframe/api/page.rb +177 -0
- data/lib/backframe/api.rb +35 -0
- data/lib/backframe/mime.rb +11 -0
- data/lib/backframe/resource/actions.rb +99 -0
- data/lib/backframe/resource.rb +90 -0
- data/lib/backframe/version.rb +3 -0
- data/lib/backframe.rb +10 -0
- data/spec/backframe/api_spec.rb +225 -0
- data/spec/backframe/resource_spec.rb +178 -0
- data/spec/spec_helper.rb +61 -0
- data/spec/support/example.rb +20 -0
- data/spec/support/example_factory.rb +9 -0
- data/spec/support/example_serializer.rb +3 -0
- data/spec/support/schema.rb +8 -0
- metadata +115 -0
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
data/.idea/vcs.xml
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
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,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
|
data/lib/backframe.rb
ADDED
@@ -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
|
data/spec/spec_helper.rb
ADDED
@@ -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
|
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
|