rest_framework 0.1.0 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +11 -6
- data/lib/rest_framework.rb +1 -0
- data/lib/rest_framework/VERSION_STAMP +1 -1
- data/lib/rest_framework/controller_mixins.rb +4 -0
- data/lib/rest_framework/controller_mixins/base.rb +148 -162
- data/lib/rest_framework/controller_mixins/models.rb +210 -181
- data/lib/rest_framework/filters.rb +53 -50
- data/lib/rest_framework/paginators.rb +76 -65
- data/lib/rest_framework/routers.rb +4 -4
- data/lib/rest_framework/serializers.rb +90 -92
- metadata +1 -1
@@ -1,65 +1,68 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
end
|
1
|
+
class RESTFramework::BaseFilter
|
2
|
+
def initialize(controller:)
|
3
|
+
@controller = controller
|
4
|
+
end
|
6
5
|
|
7
|
-
|
8
|
-
|
9
|
-
end
|
6
|
+
def get_filtered_data(data)
|
7
|
+
raise NotImplementedError
|
10
8
|
end
|
9
|
+
end
|
11
10
|
|
12
|
-
# A simple filtering backend that supports filtering a recordset based on fields defined on the
|
13
|
-
# controller class.
|
14
|
-
class ModelFilter < BaseFilter
|
15
|
-
# Filter params for keys allowed by the current action's filterset_fields/fields config.
|
16
|
-
def _get_filter_params
|
17
|
-
fields = @controller.class.filterset_fields || @controller.send(:get_fields)
|
18
|
-
return @controller.request.query_parameters.select { |p|
|
19
|
-
fields.include?(p.to_sym) || fields.include?(p.to_s)
|
20
|
-
}.to_hash.symbolize_keys
|
21
|
-
end
|
22
11
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
12
|
+
# A simple filtering backend that supports filtering a recordset based on fields defined on the
|
13
|
+
# controller class.
|
14
|
+
class RESTFramework::ModelFilter < RESTFramework::BaseFilter
|
15
|
+
# Filter params for keys allowed by the current action's filterset_fields/fields config.
|
16
|
+
def _get_filter_params
|
17
|
+
fields = @controller.class.filterset_fields || @controller.send(:get_fields)
|
18
|
+
return @controller.request.query_parameters.select { |p|
|
19
|
+
fields.include?(p.to_sym) || fields.include?(p.to_s)
|
20
|
+
}.to_hash.symbolize_keys
|
21
|
+
end
|
28
22
|
|
29
|
-
|
23
|
+
# Filter data according to the request query parameters.
|
24
|
+
def get_filtered_data(data)
|
25
|
+
filter_params = self._get_filter_params
|
26
|
+
unless filter_params.blank?
|
27
|
+
return data.where(**filter_params)
|
30
28
|
end
|
29
|
+
|
30
|
+
return data
|
31
31
|
end
|
32
|
+
end
|
32
33
|
|
33
|
-
# A filter backend which handles ordering of the recordset.
|
34
|
-
class ModelOrderingFilter < BaseFilter
|
35
|
-
# Convert ordering string to an ordering configuration.
|
36
|
-
def _get_ordering
|
37
|
-
return nil unless @controller.class.ordering_query_param
|
38
34
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
else
|
45
|
-
[field.to_sym, :asc]
|
46
|
-
end
|
47
|
-
}.to_h
|
48
|
-
end
|
35
|
+
# A filter backend which handles ordering of the recordset.
|
36
|
+
class RESTFramework::ModelOrderingFilter < RESTFramework::BaseFilter
|
37
|
+
# Convert ordering string to an ordering configuration.
|
38
|
+
def _get_ordering
|
39
|
+
return nil unless @controller.class.ordering_query_param
|
49
40
|
|
50
|
-
|
41
|
+
order_string = @controller.params[@controller.class.ordering_query_param]
|
42
|
+
unless order_string.blank?
|
43
|
+
return order_string.split(',').map { |field|
|
44
|
+
if field[0] == '-'
|
45
|
+
[field[1..-1].to_sym, :desc]
|
46
|
+
else
|
47
|
+
[field.to_sym, :asc]
|
48
|
+
end
|
49
|
+
}.to_h
|
51
50
|
end
|
52
51
|
|
53
|
-
|
54
|
-
ordering = self._get_ordering
|
55
|
-
if ordering && !ordering.empty?
|
56
|
-
return data.order(_get_ordering)
|
57
|
-
end
|
58
|
-
return data
|
59
|
-
end
|
52
|
+
return nil
|
60
53
|
end
|
61
54
|
|
62
|
-
#
|
63
|
-
|
64
|
-
|
55
|
+
# Order data according to the request query parameters.
|
56
|
+
def get_filtered_data(data)
|
57
|
+
ordering = self._get_ordering
|
58
|
+
if ordering && !ordering.empty?
|
59
|
+
return data.order(_get_ordering)
|
60
|
+
end
|
61
|
+
return data
|
62
|
+
end
|
65
63
|
end
|
64
|
+
|
65
|
+
|
66
|
+
# TODO: implement searching within fields rather than exact match filtering (ModelFilter)
|
67
|
+
# class RESTFramework::ModelSearchFilter < RESTFramework::BaseFilter
|
68
|
+
# end
|
@@ -1,84 +1,95 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
class PageNumberPaginator
|
7
|
-
def initialize(data:, controller:, **kwargs)
|
8
|
-
@data = data
|
9
|
-
@controller = controller
|
10
|
-
@count = data.count
|
11
|
-
@page_size = self._page_size
|
12
|
-
|
13
|
-
@total_pages = @count / @page_size
|
14
|
-
@total_pages += 1 if (@count % @page_size != 0)
|
15
|
-
end
|
1
|
+
class RESTFramework::BasePaginator
|
2
|
+
def initialize(data:, controller:, **kwargs)
|
3
|
+
@data = data
|
4
|
+
@controller = controller
|
5
|
+
end
|
16
6
|
|
17
|
-
|
18
|
-
|
7
|
+
# Get the page and return it so the caller can serialize it.
|
8
|
+
def get_page
|
9
|
+
raise NotImplementedError
|
10
|
+
end
|
19
11
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
end
|
26
|
-
end
|
12
|
+
# Wrap the serialized page with appropriate metadata.
|
13
|
+
def get_paginated_response(serialized_page)
|
14
|
+
raise NotImplementedError
|
15
|
+
end
|
16
|
+
end
|
27
17
|
|
28
|
-
# Otherwise, get from config.
|
29
|
-
if !page_size && @controller.class.page_size
|
30
|
-
page_size = @controller.class.page_size
|
31
|
-
end
|
32
18
|
|
33
|
-
|
34
|
-
|
19
|
+
# A simple paginator based on page numbers.
|
20
|
+
#
|
21
|
+
# Example: http://example.com/api/users/?page=3&page_size=50
|
22
|
+
class RESTFramework::PageNumberPaginator < RESTFramework::BasePaginator
|
23
|
+
def initialize(**kwargs)
|
24
|
+
super
|
25
|
+
@count = @data.count
|
26
|
+
@page_size = self._page_size
|
27
|
+
|
28
|
+
@total_pages = @count / @page_size
|
29
|
+
@total_pages += 1 if (@count % @page_size != 0)
|
30
|
+
end
|
31
|
+
|
32
|
+
def _page_size
|
33
|
+
page_size = nil
|
35
34
|
|
36
|
-
|
37
|
-
|
38
|
-
|
35
|
+
# Get from context, if allowed.
|
36
|
+
if @controller.class.page_size_query_param
|
37
|
+
if page_size = @controller.params[@controller.class.page_size_query_param].presence
|
38
|
+
page_size = page_size.to_i
|
39
39
|
end
|
40
|
+
end
|
40
41
|
|
41
|
-
|
42
|
-
|
42
|
+
# Otherwise, get from config.
|
43
|
+
if !page_size && @controller.class.page_size
|
44
|
+
page_size = @controller.class.page_size
|
43
45
|
end
|
44
46
|
|
45
|
-
|
46
|
-
|
47
|
+
# Ensure we don't exceed the max page size.
|
48
|
+
if @controller.class.max_page_size && page_size > @controller.class.max_page_size
|
49
|
+
page_size = @controller.class.max_page_size
|
47
50
|
end
|
48
51
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
52
|
+
# Ensure we return at least 1.
|
53
|
+
return page_size.zero? ? 1 : page_size
|
54
|
+
end
|
55
|
+
|
56
|
+
def _page_query_param
|
57
|
+
return @controller.class.page_query_param&.to_sym
|
58
|
+
end
|
59
|
+
|
60
|
+
# Get the page and return it so the caller can serialize it.
|
61
|
+
def get_page(page_number=nil)
|
62
|
+
# If page number isn't provided, infer from the params or use 1 as a fallback value.
|
63
|
+
if !page_number
|
64
|
+
page_number = @controller&.params&.[](self._page_query_param)
|
65
|
+
if page_number.blank?
|
66
|
+
page_number = 1
|
67
|
+
else
|
68
|
+
page_number = page_number.to_i
|
69
|
+
if page_number.zero?
|
54
70
|
page_number = 1
|
55
|
-
else
|
56
|
-
page_number = page_number.to_i
|
57
|
-
if page_number.zero?
|
58
|
-
page_number = 1
|
59
|
-
end
|
60
71
|
end
|
61
72
|
end
|
62
|
-
@page_number = page_number
|
63
|
-
|
64
|
-
# Get the data page and return it so the caller can serialize the data in the proper format.
|
65
|
-
page_index = @page_number - 1
|
66
|
-
return @data.limit(@page_size).offset(page_index * @page_size)
|
67
73
|
end
|
74
|
+
@page_number = page_number
|
68
75
|
|
69
|
-
#
|
70
|
-
|
71
|
-
|
72
|
-
count: @count,
|
73
|
-
page: @page_number,
|
74
|
-
total_pages: @total_pages,
|
75
|
-
results: serialized_page,
|
76
|
-
}
|
77
|
-
end
|
76
|
+
# Get the data page and return it so the caller can serialize the data in the proper format.
|
77
|
+
page_index = @page_number - 1
|
78
|
+
return @data.limit(@page_size).offset(page_index * @page_size)
|
78
79
|
end
|
79
80
|
|
80
|
-
# TODO:
|
81
|
-
|
82
|
-
|
83
|
-
|
81
|
+
# Wrap the serialized page with appropriate metadata. TODO: include links.
|
82
|
+
def get_paginated_response(serialized_page)
|
83
|
+
return {
|
84
|
+
count: @count,
|
85
|
+
page: @page_number,
|
86
|
+
total_pages: @total_pages,
|
87
|
+
results: serialized_page,
|
88
|
+
}
|
89
|
+
end
|
84
90
|
end
|
91
|
+
|
92
|
+
|
93
|
+
# TODO: implement this
|
94
|
+
# class RESTFramework::CountOffsetPaginator
|
95
|
+
# end
|
@@ -2,7 +2,7 @@ require 'action_dispatch/routing/mapper'
|
|
2
2
|
|
3
3
|
module ActionDispatch::Routing
|
4
4
|
class Mapper
|
5
|
-
#
|
5
|
+
# Internal helper to take extra_actions hash and convert to a consistent format.
|
6
6
|
protected def _parse_extra_actions(extra_actions)
|
7
7
|
return (extra_actions || {}).map do |k,v|
|
8
8
|
kwargs = {}
|
@@ -37,7 +37,7 @@ module ActionDispatch::Routing
|
|
37
37
|
end
|
38
38
|
end
|
39
39
|
|
40
|
-
#
|
40
|
+
# Internal interface to get the controller class from the name and current scope.
|
41
41
|
protected def _get_controller_class(name, pluralize: true, fallback_reverse_pluralization: true)
|
42
42
|
# get class name
|
43
43
|
name = name.to_s.camelize # camelize to leave plural names plural
|
@@ -71,7 +71,7 @@ module ActionDispatch::Routing
|
|
71
71
|
return controller
|
72
72
|
end
|
73
73
|
|
74
|
-
#
|
74
|
+
# Internal core implementation of the `rest_resource(s)` router, both singular and plural.
|
75
75
|
# @param default_singular [Boolean] the default plurality of the resource if the plurality is
|
76
76
|
# not otherwise defined by the controller
|
77
77
|
# @param name [Symbol] the resource name, from which path and controller are deduced by default
|
@@ -166,7 +166,7 @@ module ActionDispatch::Routing
|
|
166
166
|
end
|
167
167
|
|
168
168
|
# Route a controller's `#root` to '/' in the current scope/namespace, along with other actions.
|
169
|
-
# @param
|
169
|
+
# @param name [Symbol] the snake_case name of the controller
|
170
170
|
def rest_root(name=nil, **kwargs, &block)
|
171
171
|
# By default, use RootController#root.
|
172
172
|
root_action = kwargs.delete(:action) || :root
|
@@ -1,116 +1,114 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
attr_reader :errors
|
1
|
+
class RESTFramework::BaseSerializer
|
2
|
+
attr_reader :errors
|
4
3
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
end
|
4
|
+
def initialize(object: nil, controller: nil, **kwargs)
|
5
|
+
@object = object
|
6
|
+
@controller = controller
|
9
7
|
end
|
8
|
+
end
|
10
9
|
|
11
|
-
# This serializer uses `.as_json` to serialize objects. Despite the name, `.as_json` is an
|
12
|
-
# `ActiveModel` method which converts objects to Ruby primitives (with the top-level being either
|
13
|
-
# an array or a hash).
|
14
|
-
class NativeModelSerializer < BaseSerializer
|
15
|
-
class_attribute :config
|
16
|
-
class_attribute :singular_config
|
17
|
-
class_attribute :plural_config
|
18
|
-
class_attribute :action_config
|
19
|
-
|
20
|
-
def initialize(many: nil, model: nil, **kwargs)
|
21
|
-
super(**kwargs)
|
22
|
-
|
23
|
-
if many.nil?
|
24
|
-
@many = @object.respond_to?(:count) ? @object.count : nil
|
25
|
-
else
|
26
|
-
@many = many
|
27
|
-
end
|
28
|
-
|
29
|
-
@model = model || (@controller ? @controller.send(:get_model) : nil)
|
30
|
-
end
|
31
10
|
|
32
|
-
|
33
|
-
|
34
|
-
|
11
|
+
# This serializer uses `.as_json` to serialize objects. Despite the name, `.as_json` is an
|
12
|
+
# `ActiveModel` method which converts objects to Ruby primitives (with the top-level being either
|
13
|
+
# an array or a hash).
|
14
|
+
class RESTFramework::NativeModelSerializer < RESTFramework::BaseSerializer
|
15
|
+
class_attribute :config
|
16
|
+
class_attribute :singular_config
|
17
|
+
class_attribute :plural_config
|
18
|
+
class_attribute :action_config
|
19
|
+
|
20
|
+
def initialize(many: nil, model: nil, **kwargs)
|
21
|
+
super(**kwargs)
|
22
|
+
|
23
|
+
if many.nil?
|
24
|
+
@many = @object.respond_to?(:count) ? @object.count : nil
|
25
|
+
else
|
26
|
+
@many = many
|
35
27
|
end
|
36
28
|
|
37
|
-
|
38
|
-
|
39
|
-
action = self.get_action
|
29
|
+
@model = model || (@controller ? @controller.send(:get_model) : nil)
|
30
|
+
end
|
40
31
|
|
41
|
-
|
42
|
-
|
43
|
-
|
32
|
+
# Get controller action, if possible.
|
33
|
+
def get_action
|
34
|
+
return @controller&.action_name&.to_sym
|
35
|
+
end
|
44
36
|
|
45
|
-
|
46
|
-
|
37
|
+
# Get a locally defined configuration, if one is defined.
|
38
|
+
def get_local_serializer_config
|
39
|
+
action = self.get_action
|
47
40
|
|
48
|
-
|
49
|
-
|
50
|
-
|
41
|
+
if action && self.action_config
|
42
|
+
# Index action should use :list serializer config if :index is not provided.
|
43
|
+
action = :list if action == :index && !self.action_config.key?(:index)
|
51
44
|
|
52
|
-
|
53
|
-
return self.config
|
45
|
+
return self.action_config[action] if self.action_config[action]
|
54
46
|
end
|
55
47
|
|
56
|
-
#
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
fields = @controller.try(:get_fields) if @controller
|
70
|
-
unless fields.blank?
|
71
|
-
columns, methods = fields.partition { |f| f.to_s.in?(@model.column_names) }
|
72
|
-
return {only: columns, methods: methods}
|
73
|
-
end
|
74
|
-
|
75
|
-
return {}
|
48
|
+
# No action_config, so try singular/plural config.
|
49
|
+
return self.plural_config if @many && self.plural_config
|
50
|
+
return self.singular_config if !@many && self.singular_config
|
51
|
+
|
52
|
+
# Lastly, try returning the default config.
|
53
|
+
return self.config
|
54
|
+
end
|
55
|
+
|
56
|
+
# Get a configuration passable to `as_json` for the object.
|
57
|
+
def get_serializer_config
|
58
|
+
# Return a locally defined serializer config if one is defined.
|
59
|
+
if local_config = self.get_local_serializer_config
|
60
|
+
return local_config
|
76
61
|
end
|
77
62
|
|
78
|
-
#
|
79
|
-
|
80
|
-
|
81
|
-
@many = @object.respond_to?(:each) if @many.nil?
|
82
|
-
return @object.as_json(self.get_serializer_config)
|
83
|
-
end
|
84
|
-
return nil
|
63
|
+
# Return a serializer config if one is defined.
|
64
|
+
if serializer_config = @controller.send(:get_native_serializer_config)
|
65
|
+
return serializer_config
|
85
66
|
end
|
86
67
|
|
87
|
-
#
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
return @_nested_config[key]
|
68
|
+
# If the config wasn't determined, build a serializer config from model fields.
|
69
|
+
fields = @controller.try(:get_fields) if @controller
|
70
|
+
unless fields.blank?
|
71
|
+
columns, methods = fields.partition { |f| f.to_s.in?(@model.column_names) }
|
72
|
+
return {only: columns, methods: methods}
|
93
73
|
end
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
74
|
+
|
75
|
+
return {}
|
76
|
+
end
|
77
|
+
|
78
|
+
# Convert the object (record or recordset) to Ruby primitives.
|
79
|
+
def serialize
|
80
|
+
if @object
|
81
|
+
@many = @object.respond_to?(:each) if @many.nil?
|
82
|
+
return @object.as_json(self.get_serializer_config)
|
99
83
|
end
|
84
|
+
return nil
|
85
|
+
end
|
100
86
|
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
end
|
106
|
-
return @_nested_config[key]
|
87
|
+
# Allow a serializer instance to be used as a hash directly in a nested serializer config.
|
88
|
+
def [](key)
|
89
|
+
unless instance_variable_defined?(:@_nested_config)
|
90
|
+
@_nested_config = self.get_serializer_config
|
107
91
|
end
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
92
|
+
return @_nested_config[key]
|
93
|
+
end
|
94
|
+
def []=(key, value)
|
95
|
+
unless instance_variable_defined?(:@_nested_config)
|
96
|
+
@_nested_config = self.get_serializer_config
|
113
97
|
end
|
98
|
+
return @_nested_config[key] = value
|
114
99
|
end
|
115
100
|
|
101
|
+
# Allow a serializer class to be used as a hash directly in a nested serializer config.
|
102
|
+
def self.[](key)
|
103
|
+
unless instance_variable_defined?(:@_nested_config)
|
104
|
+
@_nested_config = self.new.get_serializer_config
|
105
|
+
end
|
106
|
+
return @_nested_config[key]
|
107
|
+
end
|
108
|
+
def self.[]=(key, value)
|
109
|
+
unless instance_variable_defined?(:@_nested_config)
|
110
|
+
@_nested_config = self.new.get_serializer_config
|
111
|
+
end
|
112
|
+
return @_nested_config[key] = value
|
113
|
+
end
|
116
114
|
end
|