rest_framework 0.1.0 → 0.1.1
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/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
|