introspective_grape 0.4.1 → 0.5.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
|