introspective_grape 0.4.1 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/lint.yml +23 -0
- data/.github/workflows/security.yml +32 -0
- data/.github/workflows/test.yml +18 -0
- data/.rubocop.yml +84 -1173
- data/.ruby-version +1 -1
- data/CHANGELOG.md +30 -0
- data/Gemfile +3 -5
- data/README.md +94 -63
- data/Rakefile +1 -6
- data/introspective_grape.gemspec +63 -53
- data/lib/introspective_grape/api.rb +166 -137
- data/lib/introspective_grape/camel_snake.rb +5 -3
- data/lib/introspective_grape/create_helpers.rb +3 -5
- data/lib/introspective_grape/doc.rb +19 -5
- data/lib/introspective_grape/filters.rb +98 -83
- data/lib/introspective_grape/formatter/camel_json.rb +2 -3
- data/lib/introspective_grape/helpers.rb +55 -48
- data/lib/introspective_grape/snake_params.rb +1 -2
- data/lib/introspective_grape/traversal.rb +56 -54
- data/lib/introspective_grape/validators.rb +23 -23
- data/lib/introspective_grape/version.rb +3 -1
- data/spec/dummy/.ruby-version +1 -0
- data/spec/dummy/Gemfile +5 -4
- data/spec/dummy/app/api/api_helpers.rb +1 -1
- data/spec/dummy/app/api/dummy/company_api.rb +10 -1
- data/spec/dummy/app/api/dummy/project_api.rb +1 -0
- data/spec/dummy/app/api/dummy/sessions.rb +1 -1
- data/spec/dummy/app/api/dummy_api.rb +8 -2
- data/spec/dummy/app/assets/config/manifest.js +4 -0
- data/spec/dummy/app/models/user.rb +1 -1
- data/spec/dummy/config/database.yml +1 -1
- data/spec/rails_helper.rb +1 -1
- data/spec/requests/company_api_spec.rb +9 -0
- metadata +153 -45
- data/.coveralls.yml +0 -2
- data/.travis.yml +0 -40
- data/bin/rails +0 -12
- data/gemfiles/Gemfile.rails.5.0.0 +0 -14
- data/gemfiles/Gemfile.rails.5.0.1 +0 -14
- data/gemfiles/Gemfile.rails.5.1.0 +0 -14
- data/gemfiles/Gemfile.rails.5.2.0 +0 -14
- data/gemfiles/Gemfile.rails.master +0 -14
@@ -1,113 +1,128 @@
|
|
1
|
-
module IntrospectiveGrape
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
#
|
6
|
-
|
7
|
-
def default_sort(*args)
|
8
|
-
@default_sort ||= args
|
9
|
-
end
|
10
|
-
|
11
|
-
def custom_filter(*args)
|
12
|
-
custom_filters( *args )
|
13
|
-
end
|
1
|
+
module IntrospectiveGrape
|
2
|
+
module Filters
|
3
|
+
# Allow filters on all whitelisted model attributes (from api_params) and declare
|
4
|
+
# customer filters for the index in a method.
|
14
5
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
@custom_filters
|
19
|
-
end
|
6
|
+
def default_sort(*args)
|
7
|
+
@default_sort ||= args
|
8
|
+
end
|
20
9
|
|
21
|
-
|
22
|
-
|
23
|
-
|
10
|
+
def custom_filter(*args)
|
11
|
+
custom_filters( *args )
|
12
|
+
end
|
24
13
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
14
|
+
def custom_filters(*args)
|
15
|
+
@custom_filters ||= {}
|
16
|
+
@custom_filters = Hash[*args].merge(@custom_filters) if args.present?
|
17
|
+
@custom_filters
|
18
|
+
end
|
30
19
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
}.map { |field|
|
35
|
-
(klass.param_type(model,field) == DateTime ? ["#{field}_start", "#{field}_end"] : field.to_s)
|
36
|
-
}.flatten
|
37
|
-
end
|
20
|
+
def filter_on(*args)
|
21
|
+
filters( *args )
|
22
|
+
end
|
38
23
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
else
|
44
|
-
false
|
24
|
+
def filters(*args)
|
25
|
+
@filters ||= []
|
26
|
+
@filters += args if args.present?
|
27
|
+
@filters
|
45
28
|
end
|
46
|
-
end
|
47
29
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
30
|
+
def simple_filters(klass, model, api_params)
|
31
|
+
@simple_filters ||= api_params.select {|p| p.is_a? Symbol }.select {|field|
|
32
|
+
filters.include?(:all) || filters.include?(field)
|
33
|
+
}.map {|field|
|
34
|
+
(klass.param_type(model, field) == DateTime ? ["#{field}_start", "#{field}_end"] : field.to_s)
|
35
|
+
}.flatten
|
53
36
|
end
|
54
|
-
end
|
55
37
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
if timestamp_filter(klass,model,field)
|
61
|
-
terminal = field.ends_with?("_start") ? "initial" : "terminal"
|
62
|
-
dsl.optional field, type: klass.param_type(model,field), description: "Constrain #{field} by #{terminal} date."
|
63
|
-
elsif identifier_filter(klass,model,field)
|
64
|
-
dsl.optional field, type: Array[Integer], coerce_with: ->(val) { val.split(',') }, description: "Filter by a comma separated list of integers."
|
38
|
+
def timestamp_filter(klass, model, field)
|
39
|
+
filter = field.sub(/_(end|start)\z/, '')
|
40
|
+
if field =~ /_(end|start)\z/ && klass.param_type(model, filter) == DateTime
|
41
|
+
filter
|
65
42
|
else
|
66
|
-
|
43
|
+
false
|
67
44
|
end
|
68
45
|
end
|
69
46
|
|
70
|
-
|
71
|
-
|
72
|
-
|
47
|
+
def declare_filter_params(dsl, klass, model, api_params)
|
48
|
+
# Declare optional parameters for filtering parameters, create two parameters per
|
49
|
+
# timestamp, a Start and an End, to apply a date range.
|
50
|
+
simple_filters(klass, model, api_params).each do |field|
|
51
|
+
declare_simple_filter(dsl, klass, model, field)
|
52
|
+
end
|
73
53
|
|
74
|
-
|
75
|
-
|
54
|
+
custom_filters.each do |filter, details|
|
55
|
+
dsl.optional filter, details
|
56
|
+
end
|
76
57
|
|
58
|
+
dsl.optional :filter, type: String, description: filter_doc if special_filter_enabled?(filters)
|
59
|
+
end
|
77
60
|
|
78
|
-
|
79
|
-
|
61
|
+
def declare_simple_filter(dsl, klass, model, field)
|
62
|
+
if timestamp_filter(klass, model, field)
|
63
|
+
dsl.optional field, type: klass.param_type(model, field), description: "Constrain #{field} by #{humanize_date_range(field)} date."
|
64
|
+
elsif identifier_filter?(model, field)
|
65
|
+
dsl.optional field, type: Array[String], coerce_with: ->(val) { val.split(',') }, description: 'Filter by a comma separated list of unique identifiers.'
|
66
|
+
else
|
67
|
+
dsl.optional field, type: klass.param_type(model, field), description: "Filter on #{field} by value."
|
68
|
+
end
|
69
|
+
end
|
80
70
|
|
81
|
-
|
82
|
-
|
83
|
-
records.where("#{timestamp_filter(klass,model,field)} #{op} ?", Time.zone.parse(params[field]))
|
84
|
-
elsif model.respond_to?("#{field}=")
|
85
|
-
records.send("#{field}=", params[field])
|
86
|
-
else
|
87
|
-
records.where(field => params[field])
|
71
|
+
def humanize_date_range(field)
|
72
|
+
field.ends_with?('_start') ? 'initial' : 'terminal'
|
88
73
|
end
|
89
|
-
end
|
90
74
|
|
91
|
-
|
92
|
-
|
75
|
+
def identifier_filter?(model, field)
|
76
|
+
true if field.ends_with?('id') && %i(integer uuid).include?(model.columns_hash[field]&.type)
|
77
|
+
end
|
93
78
|
|
94
|
-
|
95
|
-
|
79
|
+
def special_filter_enabled?(filters)
|
80
|
+
filters.include?(:all) || filters.include?(:filter)
|
96
81
|
end
|
97
82
|
|
98
|
-
|
99
|
-
|
83
|
+
def filter_doc
|
84
|
+
<<-STR
|
85
|
+
JSON of conditions for query. If you're familiar with ActiveRecord's query conventions you can build more complex filters, i.e. against included child associations, e.g.: {\"<association_name>_<parent>\":{\"field\":\"value\"}}
|
86
|
+
STR
|
100
87
|
end
|
101
88
|
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
89
|
+
def apply_simple_filter(klass, model, params, records, field)
|
90
|
+
return records if params[field].blank?
|
91
|
+
|
92
|
+
if timestamp_filter(klass, model, field)
|
93
|
+
op = field.ends_with?('_start') ? '>=' : '<='
|
94
|
+
records.where("#{timestamp_filter(klass, model, field)} #{op} ?", Time.zone.parse(params[field]))
|
95
|
+
elsif model.respond_to?("#{field}=")
|
96
|
+
records.send("#{field}=", params[field])
|
97
|
+
else
|
98
|
+
records.where(field => params[field])
|
106
99
|
end
|
107
100
|
end
|
108
101
|
|
109
|
-
|
102
|
+
def apply_filter_params(klass, model, api_params, params, records)
|
103
|
+
records = records.order(default_sort) if default_sort.present?
|
110
104
|
|
111
|
-
|
105
|
+
simple_filters(klass, model, api_params).each do |field|
|
106
|
+
records = apply_simple_filter(klass, model, params, records, field)
|
107
|
+
end
|
108
|
+
|
109
|
+
klass.custom_filters.each do |filter, _details|
|
110
|
+
records = records.send(filter, params[filter])
|
111
|
+
end
|
112
|
+
|
113
|
+
records = apply_filters(records, params[:filter])
|
114
|
+
records.where( JSON.parse(params[:query]) ) if params[:query].present?
|
115
|
+
records
|
116
|
+
end
|
117
|
+
|
118
|
+
def apply_filters(records, filters)
|
119
|
+
if filters.present?
|
120
|
+
filters = JSON.parse( filters.delete('\\') )
|
121
|
+
filters.each do |key, value|
|
122
|
+
records = records.where(key => value) if value.present?
|
123
|
+
end
|
124
|
+
end
|
125
|
+
records
|
126
|
+
end
|
112
127
|
end
|
113
128
|
end
|
@@ -9,9 +9,8 @@ module IntrospectiveGrape
|
|
9
9
|
# We only need to parse(object.to_json) like this if it isn't already
|
10
10
|
# a native hash (or array of them), i.e. we have to parse Grape::Entities
|
11
11
|
# and other formatter facades:
|
12
|
-
|
13
|
-
|
14
|
-
end
|
12
|
+
has_hash = (object.is_a?(Array) && object.first.is_a?(Hash)) || object.is_a?(Hash)
|
13
|
+
object = JSON.parse(object.to_json) if object.respond_to?(:to_json) && !has_hash
|
15
14
|
CamelSnakeKeys.camel_keys(object)
|
16
15
|
end
|
17
16
|
|
@@ -1,63 +1,70 @@
|
|
1
|
-
module IntrospectiveGrape
|
2
|
-
|
3
|
-
|
4
|
-
# IntrospectiveGrape::API.authentication_method=
|
5
|
-
@authentication_method = method
|
6
|
-
end
|
1
|
+
module IntrospectiveGrape
|
2
|
+
module Helpers
|
3
|
+
API_ACTIONS = %i(index show create update destroy).freeze
|
7
4
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
@authentication_method
|
12
|
-
elsif context.respond_to?('authenticate!')
|
13
|
-
'authenticate!'
|
14
|
-
elsif context.respond_to?('authorize!')
|
15
|
-
'authorize!'
|
5
|
+
def authentication_method=(method)
|
6
|
+
# IntrospectiveGrape::API.authentication_method=
|
7
|
+
@authentication_method = method
|
16
8
|
end
|
17
|
-
end
|
18
|
-
|
19
|
-
def paginate(args={})
|
20
|
-
@pagination = args
|
21
|
-
end
|
22
9
|
|
23
|
-
|
24
|
-
|
25
|
-
|
10
|
+
def authentication_method(context)
|
11
|
+
# Default to "authenticate!" or as grape docs once suggested, "authorize!"
|
12
|
+
if @authentication_method
|
13
|
+
@authentication_method
|
14
|
+
elsif context.respond_to?('authenticate!')
|
15
|
+
'authenticate!'
|
16
|
+
elsif context.respond_to?('authorize!')
|
17
|
+
'authorize!'
|
18
|
+
end
|
19
|
+
end
|
26
20
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
args = API_ACTIONS if args.include?(:all)
|
31
|
-
args = [] if args.include?(:none)
|
21
|
+
def paginate(args={})
|
22
|
+
@pagination = args
|
23
|
+
end
|
32
24
|
|
33
|
-
|
34
|
-
|
25
|
+
def pagination
|
26
|
+
@pagination
|
27
|
+
end
|
35
28
|
|
36
|
-
|
37
|
-
|
29
|
+
def exclude_actions(model, *args)
|
30
|
+
args = all_or_none(args)
|
31
|
+
@exclude_actions ||= {}
|
32
|
+
@exclude_actions[model.name] ||= []
|
38
33
|
|
39
|
-
|
40
|
-
|
41
|
-
@exclude_actions[model.name] = API_ACTIONS-exclude_actions(model, args)
|
42
|
-
end
|
34
|
+
undefined_actions = args - API_ACTIONS
|
35
|
+
raise "#{model.name} defines invalid actions: #{undefined_actions}" if undefined_actions.present?
|
43
36
|
|
37
|
+
@exclude_actions[model.name] = args.present? ? args : @exclude_actions[model.name]
|
38
|
+
end
|
44
39
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
40
|
+
def all_or_none(args=[])
|
41
|
+
args.flatten!&.compact!
|
42
|
+
args = API_ACTIONS if args.include?(:all)
|
43
|
+
args = [] if args.include?(:none)
|
44
|
+
args
|
45
|
+
end
|
49
46
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
47
|
+
def include_actions(model, *args)
|
48
|
+
@exclude_actions ||= {}
|
49
|
+
@exclude_actions[model.name] ||= []
|
50
|
+
@exclude_actions[model.name] = API_ACTIONS - exclude_actions(model, args)
|
51
|
+
end
|
54
52
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
53
|
+
def default_includes(model, *args)
|
54
|
+
@default_includes ||= {}
|
55
|
+
@default_includes[model.name] = args.present? ? args.flatten : @default_includes[model.name] || []
|
56
|
+
end
|
59
57
|
|
58
|
+
def whitelist(whitelist=nil)
|
59
|
+
return @whitelist unless whitelist
|
60
60
|
|
61
|
-
|
61
|
+
@whitelist = whitelist
|
62
|
+
end
|
62
63
|
|
64
|
+
def skip_presence_validations(fields=nil)
|
65
|
+
return @skip_presence_fields || [] unless fields
|
63
66
|
|
67
|
+
@skip_presence_fields = [fields].flatten
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -1,12 +1,11 @@
|
|
1
1
|
module IntrospectiveGrape
|
2
2
|
module SnakeParams
|
3
|
-
|
4
3
|
def snake_params_before_validation
|
5
4
|
before_validation do
|
6
5
|
# We have to snake case the Rack params then re-assign @params to the
|
7
6
|
# request.params, because of the I-think-very-goofy-and-inexplicable
|
8
7
|
# way Grape interacts with both independently of each other
|
9
|
-
(CamelSnakeKeys.snake_keys(params)||{}).each do |k,v|
|
8
|
+
(CamelSnakeKeys.snake_keys(params) || {}).each do |k, v|
|
10
9
|
request.delete_param(k.camelize(:lower))
|
11
10
|
request.update_param(k, v)
|
12
11
|
end
|
@@ -1,54 +1,56 @@
|
|
1
|
-
module IntrospectiveGrape
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
leaves
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
end
|
1
|
+
module IntrospectiveGrape
|
2
|
+
module Traversal
|
3
|
+
# For deeply nested endpoints we want to present the record being affected, these
|
4
|
+
# methods traverse down from the parent instance to the child model associations
|
5
|
+
# of the deeply nested route.
|
6
|
+
|
7
|
+
def find_leaves(routes, record, params)
|
8
|
+
# Traverse down our route and find the leaf's siblings from its parent, e.g.
|
9
|
+
# project/#/teams/#/team_users ~> project.find.teams.find.team_users
|
10
|
+
# (the traversal of the intermediate nodes occurs in find_leaf())
|
11
|
+
return record if routes.size < 2 # the leaf is the root
|
12
|
+
|
13
|
+
record = find_leaf(routes, record, params) || return
|
14
|
+
|
15
|
+
assoc = routes.last
|
16
|
+
if assoc.many?
|
17
|
+
leaves = record.send( assoc.reflection.name ).includes( default_includes(assoc.model) )
|
18
|
+
verify_records_found(leaves, routes)
|
19
|
+
leaves
|
20
|
+
else
|
21
|
+
# has_one associations don't return a CollectionProxy and so don't support
|
22
|
+
# eager loading.
|
23
|
+
record.send( assoc.reflection.name )
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def verify_records_found(leaves, routes)
|
28
|
+
return if (leaves.map(&:class) - [routes.last.model]).empty?
|
29
|
+
|
30
|
+
raise ActiveRecord::RecordNotFound.new("Records contain the wrong models, they should all be #{routes.last.model.name}, found #{records.map(&:class).map(&:name).join(',')}")
|
31
|
+
end
|
32
|
+
|
33
|
+
def find_leaf(routes, record, params)
|
34
|
+
return record unless routes.size > 1
|
35
|
+
|
36
|
+
# For deeply nested routes we need to search from the root of the API to the leaf
|
37
|
+
# of its nested associations in order to guarantee the validity of the relationship,
|
38
|
+
# the authorization on the parent model, and the sanity of passed parameters.
|
39
|
+
routes[1..-1].each do |r|
|
40
|
+
if record && params[r.key]
|
41
|
+
ref = r.reflection
|
42
|
+
record = record.send(ref.name).where( id: params[r.key] ).first if ref
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
verify_record_found(routes, params, record)
|
47
|
+
record
|
48
|
+
end
|
49
|
+
|
50
|
+
def verify_record_found(routes, params, record)
|
51
|
+
return unless params[routes.last.key] && record.class != routes.last.model
|
52
|
+
|
53
|
+
raise ActiveRecord::RecordNotFound.new("No #{routes.last.model.name} with ID '#{params[routes.last.key]}'")
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -1,34 +1,34 @@
|
|
1
1
|
require 'grape/validations'
|
2
|
-
module Grape
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
2
|
+
module Grape
|
3
|
+
module Validators
|
4
|
+
class Json < Grape::Validations::Base
|
5
|
+
def validate_param!(field, params)
|
6
|
+
begin
|
7
|
+
JSON.parse( params[field] )
|
8
|
+
rescue StandardError
|
9
|
+
raise Grape::Exceptions::Validation, params: [@scope.full_name(field)], message: 'must be valid JSON!'
|
10
|
+
end
|
10
11
|
end
|
11
12
|
end
|
12
|
-
end
|
13
13
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
14
|
+
class JsonArray < Grape::Validations::Base
|
15
|
+
def validate_param!(field, params)
|
16
|
+
begin
|
17
|
+
raise unless JSON.parse( params[field] ).is_a? Array
|
18
|
+
rescue StandardError
|
19
|
+
raise Grape::Exceptions::Validation, params: [@scope.full_name(field)], message: 'must be a valid JSON array!'
|
20
|
+
end
|
20
21
|
end
|
21
22
|
end
|
22
|
-
end
|
23
23
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
24
|
+
class JsonHash < Grape::Validations::Base
|
25
|
+
def validate_param!(field, params)
|
26
|
+
begin
|
27
|
+
raise unless JSON.parse( params[field] ).is_a? Hash
|
28
|
+
rescue StandardError
|
29
|
+
raise Grape::Exceptions::Validation, params: [@scope.full_name(field)], message: 'must be a valid JSON hash!'
|
30
|
+
end
|
30
31
|
end
|
31
32
|
end
|
32
33
|
end
|
33
|
-
|
34
34
|
end
|
@@ -0,0 +1 @@
|
|
1
|
+
2.6.0
|
data/spec/dummy/Gemfile
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
source 'https://rubygems.org'
|
2
|
-
gem 'rails'
|
2
|
+
gem 'rails', '5.2.6'
|
3
3
|
|
4
|
-
|
4
|
+
gem 'byebug'
|
5
5
|
gem 'camel_snake_keys'
|
6
6
|
|
7
7
|
gem 'devise'
|
@@ -12,10 +12,11 @@ gem 'grape'
|
|
12
12
|
gem 'grape-entity'
|
13
13
|
gem 'grape-kaminari'
|
14
14
|
gem 'grape-swagger'
|
15
|
-
gem '
|
15
|
+
gem 'grape-swagger-entity'
|
16
|
+
gem 'introspective_grape', path: '~/Dropbox/contrib/introspective_grape'
|
16
17
|
|
17
18
|
gem 'paperclip'
|
18
19
|
gem 'pundit'
|
19
20
|
|
20
21
|
gem 'rack-cors'
|
21
|
-
gem 'sqlite3'
|
22
|
+
gem 'sqlite3'
|
@@ -2,7 +2,7 @@ module ApiHelpers
|
|
2
2
|
def current_user
|
3
3
|
params[:api_key].present? && @user = User.find_by_authentication_token(params[:api_key])
|
4
4
|
# for testing in situ
|
5
|
-
|
5
|
+
@user = User.find_or_create_by(email: 'test@test.com', superuser: true, authentication_token: '1234567890', first_name: "First", last_name: "Last")
|
6
6
|
end
|
7
7
|
|
8
8
|
def authenticate!
|
@@ -5,7 +5,7 @@ class Dummy::CompanyAPI < IntrospectiveGrape::API
|
|
5
5
|
|
6
6
|
desc "Test default values in an extra endpoint"
|
7
7
|
params do
|
8
|
-
optional :boolean_default, type:
|
8
|
+
optional :boolean_default, type: Boolean, default: false
|
9
9
|
optional :string_default, type: String, default: "foo"
|
10
10
|
optional :integer_default, type: Integer, default: 123
|
11
11
|
end
|
@@ -14,6 +14,15 @@ class Dummy::CompanyAPI < IntrospectiveGrape::API
|
|
14
14
|
present params
|
15
15
|
end
|
16
16
|
|
17
|
+
desc "Test kaminari pagination in a custom index"
|
18
|
+
params do
|
19
|
+
use :pagination
|
20
|
+
end
|
21
|
+
get '/paginated/list' do
|
22
|
+
authorize Company.new, :index?
|
23
|
+
companies = Company.all
|
24
|
+
present paginate(companies), using: CompanyEntity
|
25
|
+
end
|
17
26
|
end
|
18
27
|
|
19
28
|
class CompanyEntity < Grape::Entity
|
@@ -1,9 +1,14 @@
|
|
1
|
-
|
1
|
+
require 'byebug'
|
2
|
+
require 'grape-kaminari'
|
3
|
+
class DummyAPI < Grape::API #::Instance
|
4
|
+
include Grape::Kaminari
|
5
|
+
|
2
6
|
version 'v1', using: :path
|
3
7
|
format :json
|
4
8
|
formatter :json, IntrospectiveGrape::Formatter::CamelJson
|
5
9
|
default_format :json
|
6
10
|
|
11
|
+
|
7
12
|
include ErrorHandlers
|
8
13
|
helpers PermissionsHelper
|
9
14
|
helpers ApiHelpers
|
@@ -32,7 +37,8 @@ class DummyAPI < Grape::API
|
|
32
37
|
|
33
38
|
# Mount every api endpoint under app/api/dummy/.
|
34
39
|
Dir.glob(Rails.root+"app"+"api"+'dummy'+'*.rb').each do |f|
|
35
|
-
api = "Dummy::#{File.basename(f, '.rb').camelize.sub(/Api$/,'API')}"
|
40
|
+
api = "Dummy::#{File.basename(f, '.rb').camelize.sub(/Api$/,'API')}"
|
41
|
+
api = api.constantize
|
36
42
|
mount api if api.respond_to? :endpoints
|
37
43
|
end
|
38
44
|
|