rails-surrender 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/rails/surrender/config/initializers/active_record_associations_patch.rb +17 -0
- data/lib/rails/surrender/config/initializers/active_record_preloader_patch.rb +20 -0
- data/lib/rails/surrender/config/locales/en.yml +12 -0
- data/lib/rails/surrender/controller_additions.rb +109 -0
- data/lib/rails/surrender/default_ability.rb +12 -0
- data/lib/rails/surrender/exceptions.rb +9 -0
- data/lib/rails/surrender/helpers/filter_builder.rb +50 -0
- data/lib/rails/surrender/helpers/pagination_builder.rb +25 -0
- data/lib/rails/surrender/helpers/query_param_parser.rb +98 -0
- data/lib/rails/surrender/helpers/sort_builder.rb +56 -0
- data/lib/rails/surrender/model_additions.rb +59 -0
- data/lib/rails/surrender/model_filter_scopes.rb +56 -0
- data/lib/rails/surrender/railtie.rb +22 -0
- data/lib/rails/surrender/render/configuration/inclusion_mapper_logic.rb +69 -0
- data/lib/rails/surrender/render/configuration/instance_logic.rb +88 -0
- data/lib/rails/surrender/render/configuration.rb +93 -0
- data/lib/rails/surrender/render/count.rb +21 -0
- data/lib/rails/surrender/render/ids.rb +21 -0
- data/lib/rails/surrender/render/resource/collection.rb +26 -0
- data/lib/rails/surrender/render/resource/inclusion_mapper.rb +48 -0
- data/lib/rails/surrender/render/resource/instance.rb +75 -0
- data/lib/rails/surrender/render/resource.rb +54 -0
- data/lib/rails/surrender/response.rb +18 -0
- data/lib/rails/surrender/version.rb +7 -0
- data/lib/rails/surrender.rb +31 -0
- data/test/surrender_test.rb +9 -0
- data/test/test_helper.rb +21 -0
- metadata +98 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 06b60d3e2d458b2bc4d683990a6542e73291f3a3b97be987a0bd069732a885d8
|
4
|
+
data.tar.gz: 56a20555312a69afbd2f8e27dfa5f52d1e76adc8033dd1b0fd994dd20e0d2f27
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 1bf619637f8644ba084faf242a70b8f71a44e210b2f52477988b651bca5598eb7aa860b3d203b749039d6da2483e5e84bc6ee277a84ef52350a17e0de83efe58
|
7
|
+
data.tar.gz: 12eef2b6c7587faedb212037ea786d4a056650be3faf3583b9e73625e1cb5b58a8d11de069c1248604e3bac320bffd123b4237ec059cf9ef68e4d4e777a3b05f
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
#
|
3
|
+
# ActiveRecord::Associations.class_eval do
|
4
|
+
# # patching to prevent AssociationNotFoundError.
|
5
|
+
# # if there is no association just return nil instead
|
6
|
+
# def association(name) # :nodoc:
|
7
|
+
# super
|
8
|
+
# rescue AssociationNotFoundError
|
9
|
+
# return nil
|
10
|
+
# end
|
11
|
+
# end
|
12
|
+
#
|
13
|
+
# ActiveRecord::Relation.class_eval do
|
14
|
+
# def eager_loading?
|
15
|
+
# false
|
16
|
+
# end
|
17
|
+
# end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
#
|
3
|
+
# ActiveRecord::Associations::Preloader.class_eval do
|
4
|
+
# # patching to ignore nil response from association method (also patched)
|
5
|
+
#
|
6
|
+
# # if there is no association just return nil instead
|
7
|
+
# def grouped_records(association, records)
|
8
|
+
# h = {}
|
9
|
+
# records.each do |record|
|
10
|
+
# next unless record
|
11
|
+
#
|
12
|
+
# assoc = record.association(association)
|
13
|
+
# next if assoc.nil?
|
14
|
+
#
|
15
|
+
# klasses = h[assoc.reflection] ||= {}
|
16
|
+
# (klasses[assoc.klass] ||= []) << record
|
17
|
+
# end
|
18
|
+
# h
|
19
|
+
# end
|
20
|
+
# end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
---
|
2
|
+
en:
|
3
|
+
surrender:
|
4
|
+
error:
|
5
|
+
query_string:
|
6
|
+
incorrect_format: "The %{param} parameter was improperly formatted."
|
7
|
+
filter:
|
8
|
+
not_available: "%{param} is not a valid filter parameter."
|
9
|
+
include:
|
10
|
+
not_available: "%{param} is not a valid include parameter."
|
11
|
+
sort:
|
12
|
+
invalid_column: "%{param} is not a valid sort parameter."
|
@@ -0,0 +1,109 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'helpers/query_param_parser'
|
4
|
+
require_relative 'helpers/filter_builder'
|
5
|
+
require_relative 'helpers/pagination_builder'
|
6
|
+
require_relative 'helpers/sort_builder'
|
7
|
+
|
8
|
+
module Rails
|
9
|
+
module Surrender
|
10
|
+
# Additions to the Rails ActionController to allow surrender's rendering.
|
11
|
+
module ControllerAdditions
|
12
|
+
def initialize(*args)
|
13
|
+
@will_paginate = true
|
14
|
+
super
|
15
|
+
end
|
16
|
+
|
17
|
+
def surrender(resource, status: 200, reload: true, include: [], exclude: [])
|
18
|
+
resource = filter resource if parsed_query_params.filter?
|
19
|
+
|
20
|
+
if parsed_query_params.sort?
|
21
|
+
resource = sort resource
|
22
|
+
response.headers['X-Sort'] = parsed_query_params.sort.request
|
23
|
+
end
|
24
|
+
|
25
|
+
if paginate?(resource)
|
26
|
+
resource = paginate resource
|
27
|
+
response.headers['X-Pagination'] = pagination_headers(resource)
|
28
|
+
end
|
29
|
+
|
30
|
+
surrender_response = if parsed_query_params.count?
|
31
|
+
Render::Count.new(resource).parse
|
32
|
+
elsif parsed_query_params.ids?
|
33
|
+
Render::Ids.new(resource).parse
|
34
|
+
else
|
35
|
+
config = Render::Configuration.new(
|
36
|
+
resource_class: resource_class(resource),
|
37
|
+
reload_resource: reload,
|
38
|
+
user_exclude: parsed_query_params.exclude,
|
39
|
+
user_include: parsed_query_params.include,
|
40
|
+
ctrl_exclude: exclude,
|
41
|
+
ctrl_include: include
|
42
|
+
)
|
43
|
+
|
44
|
+
Render::Resource.new(resource: resource,
|
45
|
+
ability: ability,
|
46
|
+
config: config).parse
|
47
|
+
end
|
48
|
+
|
49
|
+
# Allows the calling method to decorate the response data before returning the result
|
50
|
+
surrender_response.data = yield surrender_response.data if block_given?
|
51
|
+
|
52
|
+
render(json: surrender_response.json_data, status: status)
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def ability
|
58
|
+
return current_ability if respond_to?(:current_ability)
|
59
|
+
|
60
|
+
DefaultAbility.new
|
61
|
+
end
|
62
|
+
|
63
|
+
def resource_class(resource)
|
64
|
+
resource.try(:klass) || resource.class
|
65
|
+
end
|
66
|
+
|
67
|
+
def skip_pagination
|
68
|
+
@will_paginate = false
|
69
|
+
end
|
70
|
+
|
71
|
+
def filter(resource)
|
72
|
+
FilterBuilder.new(resource: resource, filter: parsed_query_params.filter).build!
|
73
|
+
end
|
74
|
+
|
75
|
+
def paginate?(resource)
|
76
|
+
pagination(resource).paginatable? && @will_paginate
|
77
|
+
end
|
78
|
+
|
79
|
+
def paginate(resource)
|
80
|
+
pagination(resource).build!
|
81
|
+
end
|
82
|
+
|
83
|
+
def pagination(resource)
|
84
|
+
PaginationBuilder.new(resource: resource, pagination: parsed_query_params.pagination)
|
85
|
+
end
|
86
|
+
|
87
|
+
def pagination_headers(resource)
|
88
|
+
{
|
89
|
+
total: resource.total_count,
|
90
|
+
page_total: resource.count,
|
91
|
+
page: resource.current_page,
|
92
|
+
previous_page: resource.prev_page,
|
93
|
+
next_page: resource.next_page,
|
94
|
+
last_page: resource.total_pages,
|
95
|
+
per_page: parsed_query_params.pagination.per,
|
96
|
+
offset: resource.offset_value
|
97
|
+
}.to_json
|
98
|
+
end
|
99
|
+
|
100
|
+
def sort(resource)
|
101
|
+
SortBuilder.new(resource: resource, sort: parsed_query_params.sort).build!
|
102
|
+
end
|
103
|
+
|
104
|
+
def parsed_query_params
|
105
|
+
@parsed_query_params ||= QueryParamParser.new(request.query_parameters.symbolize_keys)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rails
|
4
|
+
module Surrender
|
5
|
+
# apply filtering directives to the given resource, based on the given filter controls
|
6
|
+
class FilterBuilder
|
7
|
+
attr_reader :resource, :filter
|
8
|
+
|
9
|
+
def initialize(resource:, filter:)
|
10
|
+
@resource = resource
|
11
|
+
@filter = filter
|
12
|
+
end
|
13
|
+
|
14
|
+
def build!
|
15
|
+
return resource unless resource.is_a?(ActiveRecord::Relation)
|
16
|
+
|
17
|
+
filter.each do |term|
|
18
|
+
scope, value = term.first
|
19
|
+
|
20
|
+
send_filter_for(scope, value)
|
21
|
+
end
|
22
|
+
|
23
|
+
resource
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def send_filter_for(scope, value)
|
29
|
+
if resource.respond_to?(filter_method(scope))
|
30
|
+
# filter exists on model?
|
31
|
+
@resource = @resource.send(filter_method(scope), value)
|
32
|
+
elsif resource.respond_to?(filter_method_id(scope))
|
33
|
+
# resolved it by appending _id?
|
34
|
+
@resource = @resource.send(filter_method_id(scope), value)
|
35
|
+
else
|
36
|
+
raise Error, I18n.t('surrender.error.query_string.filter.not_available', param: scope)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# prepend filter_by so that only filter_by scope methods are reachable.
|
41
|
+
def filter_method(scope)
|
42
|
+
"filter_by_#{scope}".gsub('.', '_').to_sym
|
43
|
+
end
|
44
|
+
|
45
|
+
def filter_method_id(scope)
|
46
|
+
"#{filter_method(scope)}_id".to_sym
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rails
|
4
|
+
module Surrender
|
5
|
+
# apply pagination directives to the given resource, based on the given pagination controls
|
6
|
+
class PaginationBuilder
|
7
|
+
attr_reader :resource, :pagination
|
8
|
+
|
9
|
+
def initialize(resource:, pagination:)
|
10
|
+
@resource = resource
|
11
|
+
@pagination = pagination
|
12
|
+
end
|
13
|
+
|
14
|
+
def build!
|
15
|
+
return resource unless paginatable?
|
16
|
+
|
17
|
+
resource.page(pagination.page).per(pagination.per)
|
18
|
+
end
|
19
|
+
|
20
|
+
def paginatable?
|
21
|
+
resource.respond_to?(:page)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rails
|
4
|
+
module Surrender
|
5
|
+
# parse the requests query_params for surrender's controls and validate the formatting
|
6
|
+
class QueryParamParser
|
7
|
+
attr_reader :query_params
|
8
|
+
|
9
|
+
COUNT_PARAM = :count
|
10
|
+
EXCLUDE_PARAM = :exclude
|
11
|
+
FILTER_PARAM = :filter
|
12
|
+
IDS_PARAM = :ids
|
13
|
+
INCLUDE_PARAM = :include
|
14
|
+
SORT_PARAM = :sort
|
15
|
+
PAGE_PARAM = :page
|
16
|
+
PER_PARAM = :per
|
17
|
+
|
18
|
+
PER_PAGE_DEFAULT = 50
|
19
|
+
PAGE_DEFAULT = 1
|
20
|
+
|
21
|
+
Sort = Struct.new(:request, :direction, :association, :attribute, :scope_method, keyword_init: true)
|
22
|
+
Pagination = Struct.new(:page, :per, keyword_init: true)
|
23
|
+
|
24
|
+
def initialize(query_params)
|
25
|
+
@query_params = query_params
|
26
|
+
end
|
27
|
+
|
28
|
+
def include
|
29
|
+
@include ||= parse_yml(query_params[INCLUDE_PARAM], :include)
|
30
|
+
end
|
31
|
+
|
32
|
+
def exclude
|
33
|
+
@exclude ||= parse_yml(query_params[EXCLUDE_PARAM], :exclude)
|
34
|
+
end
|
35
|
+
|
36
|
+
def sort?
|
37
|
+
query_params.key? SORT_PARAM
|
38
|
+
end
|
39
|
+
|
40
|
+
def sort
|
41
|
+
@sort ||= begin
|
42
|
+
sort = String.new(query_params[SORT_PARAM] || '')
|
43
|
+
|
44
|
+
direction_flag = ['+', '-'].include?(sort[0, 1]) ? sort.slice!(0) : '+'
|
45
|
+
direction = direction_flag == '-' ? 'DESC' : 'ASC'
|
46
|
+
|
47
|
+
scope_method = "sort_by_#{sort}".gsub('.', '_').to_sym
|
48
|
+
association, attribute = sort_attributes(sort)
|
49
|
+
|
50
|
+
Sort.new(request: query_params[SORT_PARAM], direction: direction, attribute: attribute,
|
51
|
+
association: association, scope_method: scope_method)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def filter?
|
56
|
+
filter.present?
|
57
|
+
end
|
58
|
+
|
59
|
+
def filter
|
60
|
+
@filter ||= parse_yml(query_params[FILTER_PARAM], :filter)
|
61
|
+
end
|
62
|
+
|
63
|
+
def ids?
|
64
|
+
query_params.key?(IDS_PARAM)
|
65
|
+
end
|
66
|
+
|
67
|
+
def count?
|
68
|
+
query_params.key?(COUNT_PARAM)
|
69
|
+
end
|
70
|
+
|
71
|
+
def paginate?
|
72
|
+
query_params.key? PAGE_PARAM
|
73
|
+
end
|
74
|
+
|
75
|
+
def pagination
|
76
|
+
@pagination ||= Pagination.new(
|
77
|
+
page: query_params[PAGE_PARAM]&.to_i || PAGE_DEFAULT,
|
78
|
+
per: query_params[PER_PARAM]&.to_i || PER_PAGE_DEFAULT
|
79
|
+
)
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
def sort_attributes(sort)
|
85
|
+
return sort.split('.') if sort.include? '.'
|
86
|
+
|
87
|
+
['', sort]
|
88
|
+
end
|
89
|
+
|
90
|
+
def parse_yml(query_string, action)
|
91
|
+
query_string ||= '' # empty string in case nil is passed
|
92
|
+
Psych.safe_load("[#{query_string.gsub(/(,|:)/, '\1 ')}]")
|
93
|
+
rescue StandardError
|
94
|
+
raise Error, I18n.t('surrender.error.query_string.incorrect_format', param: action)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rails
|
4
|
+
module Surrender
|
5
|
+
# apply sort directives to the given resource, based on the given sort controls
|
6
|
+
class SortBuilder
|
7
|
+
attr_reader :resource, :sort
|
8
|
+
|
9
|
+
def initialize(resource:, sort:)
|
10
|
+
@resource = resource
|
11
|
+
@sort = sort
|
12
|
+
end
|
13
|
+
|
14
|
+
def build!
|
15
|
+
return resource unless resource.is_a? ActiveRecord::Relation
|
16
|
+
|
17
|
+
return resource_attribute_order if resource_has_attribute?
|
18
|
+
|
19
|
+
return scope_method_order if resource_has_scope_method?
|
20
|
+
|
21
|
+
return association_attribute_order if resource_has_association_with_attribute?
|
22
|
+
|
23
|
+
raise Error, I18n.t('surrender.error.query_string.sort.invalid_column', param: sort.request)
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def resource_has_attribute?
|
29
|
+
resource.klass.attribute_names.include?(sort.attribute)
|
30
|
+
end
|
31
|
+
|
32
|
+
def resource_attribute_order
|
33
|
+
resource.order(sort.attribute => sort.direction)
|
34
|
+
end
|
35
|
+
|
36
|
+
def resource_has_scope_method?
|
37
|
+
resource.respond_to?(sort.scope_method)
|
38
|
+
end
|
39
|
+
|
40
|
+
def scope_method_order
|
41
|
+
resource.send(sort.scope_method, sort.direction)
|
42
|
+
end
|
43
|
+
|
44
|
+
def resource_has_association_with_attribute?
|
45
|
+
resource.reflections.keys.include?(sort.association) &&
|
46
|
+
resource.reflect_on_association(sort.association).klass.attribute_names.include?(sort.attribute)
|
47
|
+
end
|
48
|
+
|
49
|
+
def association_attribute_order
|
50
|
+
table_name = resource.reflect_on_association(sort.association).klass.table_name
|
51
|
+
resource.joins(sort.association.to_sym)
|
52
|
+
.order("#{table_name}.#{sort.attribute} #{sort.direction}")
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rails
|
4
|
+
module Surrender
|
5
|
+
# Aad methods to the model class to describe how the model renders.
|
6
|
+
module ModelAdditions
|
7
|
+
def self.included(base)
|
8
|
+
attr_accessor :surrender_attributes
|
9
|
+
attr_accessor :surrender_expands
|
10
|
+
attr_accessor :surrender_available_attributes
|
11
|
+
attr_accessor :surrender_available_expands
|
12
|
+
|
13
|
+
base.extend(ClassMethods)
|
14
|
+
end
|
15
|
+
|
16
|
+
module ClassMethods
|
17
|
+
def surrenders(*args)
|
18
|
+
directives = args.extract_options!
|
19
|
+
|
20
|
+
# Run through the various lists of attributes and assign them to the rendering context
|
21
|
+
# If the superclass has attributes then consume those as well.
|
22
|
+
%w[attributes expands available_attributes available_expands].each do |directive|
|
23
|
+
surrender_directive = "surrender_#{directive}"
|
24
|
+
list = []
|
25
|
+
if superclass.instance_variable_defined?("@#{surrender_directive}")
|
26
|
+
list << superclass.instance_variable_get("@#{surrender_directive}")
|
27
|
+
end
|
28
|
+
list << directives[directive.to_sym] if directives.key?(directive.to_sym)
|
29
|
+
instance_variable_set("@#{surrender_directive}", list.flatten.uniq)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def surrender_attributes
|
34
|
+
@surrender_attributes ||= %i[id created_at]
|
35
|
+
end
|
36
|
+
|
37
|
+
def surrender_available_attributes
|
38
|
+
@surrender_available_attributes ||= []
|
39
|
+
end
|
40
|
+
|
41
|
+
def surrender_expands
|
42
|
+
@surrender_expands ||= []
|
43
|
+
end
|
44
|
+
|
45
|
+
def surrender_available_expands
|
46
|
+
@surrender_available_expands ||= []
|
47
|
+
end
|
48
|
+
|
49
|
+
def surrender_callable_attributes
|
50
|
+
@surrender_callable_attributes ||= (surrender_attributes + surrender_available_attributes).flatten
|
51
|
+
end
|
52
|
+
|
53
|
+
def surrender_callable_expands
|
54
|
+
@surrender_callable_expands ||= (surrender_expands + surrender_available_expands).flatten
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Add filter_by_date_(to/from/before/after) methods for all *_at columns
|
4
|
+
module Rails
|
5
|
+
module Surrender
|
6
|
+
# apply filter scopes to the model
|
7
|
+
module ModelFilterScopes
|
8
|
+
def self.included(base)
|
9
|
+
base.extend(ClassMethods)
|
10
|
+
end
|
11
|
+
|
12
|
+
module ClassMethods
|
13
|
+
def inherited(child)
|
14
|
+
super
|
15
|
+
|
16
|
+
# Bad things happen if you ask table_exists? to ApplicationRecord while it's loading!
|
17
|
+
# TODO: Add configuration in case ApplicationRecord is _not_ the primary abstract class
|
18
|
+
return if child.name == 'ApplicationRecord'
|
19
|
+
|
20
|
+
return unless child.table_exists?
|
21
|
+
|
22
|
+
child.instance_eval do
|
23
|
+
# scope to filter by every column name
|
24
|
+
apply_surrender_column_name_scopes(child)
|
25
|
+
|
26
|
+
# scope to filter by date or time column names
|
27
|
+
apply_surrender_column_datetime_scopes(child)
|
28
|
+
rescue StandardError
|
29
|
+
# TODO: why are tests failing here!!!
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def apply_surrender_column_name_scopes(child)
|
34
|
+
child.column_names.each do |column|
|
35
|
+
scope "filter_by_#{column}".to_sym, ->(val) { where({ column.to_sym => val }) }
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def apply_surrender_column_datetime_scopes(child)
|
40
|
+
with_surrender_datetime_columns(child) do |column|
|
41
|
+
base = column.split(/_at$/).first
|
42
|
+
|
43
|
+
scope "filter_by_#{base}_to".to_sym, ->(time) { where("#{child.table_name}.#{column} <= ?", time) }
|
44
|
+
scope "filter_by_#{base}_from".to_sym, ->(time) { where("#{child.table_name}.#{column} >= ?", time) }
|
45
|
+
scope "filter_by_#{base}_before".to_sym, ->(time) { where("#{child.table_name}.#{column} < ?", time) }
|
46
|
+
scope "filter_by_#{base}_after".to_sym, ->(time) { where("#{child.table_name}.#{column} > ?", time) }
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def with_surrender_datetime_columns(child, &block)
|
51
|
+
child.columns.select { |c| c.type.in? %i[date datetime] }.map(&:name).each(&block)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rails
|
4
|
+
module Surrender
|
5
|
+
class Railtie < ::Rails::Railtie
|
6
|
+
config.after_initialize do
|
7
|
+
ActionController::Base.class_eval do
|
8
|
+
include Rails::Surrender::ControllerAdditions
|
9
|
+
end
|
10
|
+
|
11
|
+
ActionController::API.class_eval do
|
12
|
+
include Rails::Surrender::ControllerAdditions
|
13
|
+
end
|
14
|
+
|
15
|
+
ActiveRecord::Base.class_eval do
|
16
|
+
include Rails::Surrender::ModelAdditions
|
17
|
+
include Rails::Surrender::ModelFilterScopes
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rails
|
4
|
+
module Surrender
|
5
|
+
module Render
|
6
|
+
# Container for config structure when rendering or generating the inclusion object.
|
7
|
+
class Configuration
|
8
|
+
module InclusionMapperLogic
|
9
|
+
def expanding_elements
|
10
|
+
list = resource_class_surrender_attributes_that_expand +
|
11
|
+
resource_class_surrender_expands +
|
12
|
+
resource_class_subclass_surrender_attributes_that_expand +
|
13
|
+
resource_class_subclass_surrender_expands +
|
14
|
+
user_included_joins_required +
|
15
|
+
ctrl_included_joins_required
|
16
|
+
.flatten.uniq
|
17
|
+
list
|
18
|
+
.map { |e| element_from(e) }
|
19
|
+
.reject do |element|
|
20
|
+
element.klass.in?(history) ||
|
21
|
+
element.name.in?(local_user_excludes) ||
|
22
|
+
(element.name.in?(local_ctrl_excludes) && !element.name.in?(local_user_includes))
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def resource_class_surrender_attributes_that_expand
|
29
|
+
resource_class.surrender_attributes
|
30
|
+
.select { |attr| attr.match /_ids$/ }
|
31
|
+
.map { |attr| attr.to_s.sub('_ids', '').pluralize }
|
32
|
+
.select { |include| attribute_type(include).in? %i[expand associate] }
|
33
|
+
end
|
34
|
+
|
35
|
+
def resource_class_surrender_expands
|
36
|
+
resource_class.surrender_expands
|
37
|
+
end
|
38
|
+
|
39
|
+
def resource_class_subclass_surrender_attributes_that_expand
|
40
|
+
resource_class.subclasses.map do |subclass|
|
41
|
+
subclass.surrender_attributes
|
42
|
+
.select { |attr| attr.match /_ids$/ }
|
43
|
+
.map { |attr| attr.to_s.sub('_ids', '').pluralize }
|
44
|
+
.select { |include| attribute_type(include, resource_class: sc).in? %i[expand associate] }
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def resource_class_subclass_surrender_expands
|
49
|
+
resource_class.subclasses.map(&:surrender_expands)
|
50
|
+
end
|
51
|
+
|
52
|
+
def user_included_joins_required
|
53
|
+
top_level_keys_from(user_include).select { |include| attribute_type(include).in? %i[expand associate] }
|
54
|
+
end
|
55
|
+
|
56
|
+
def ctrl_included_joins_required
|
57
|
+
top_level_keys_from(ctrl_include).select { |include| attribute_type(include).in? %i[expand associate] }
|
58
|
+
end
|
59
|
+
|
60
|
+
def element_from(item)
|
61
|
+
item_name = resource_class.reflections.key?(item.to_s) ? item.to_s : item.to_s.sub('_ids', '').pluralize
|
62
|
+
item_klass = resource_class.reflections[item_name].klass
|
63
|
+
Element.new name: item_name, klass: item_klass, continue: (item.to_s == item_name)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rails
|
4
|
+
module Surrender
|
5
|
+
module Render
|
6
|
+
# Container for config structure when rendering or generating the inclusion object.
|
7
|
+
class Configuration
|
8
|
+
module InstanceLogic
|
9
|
+
def nested_user_includes
|
10
|
+
next_level_asks_from(user_include)
|
11
|
+
end
|
12
|
+
|
13
|
+
def nested_ctrl_includes
|
14
|
+
next_level_asks_from(ctrl_include)
|
15
|
+
end
|
16
|
+
|
17
|
+
def nested_user_excludes
|
18
|
+
next_level_asks_from(user_exclude)
|
19
|
+
end
|
20
|
+
|
21
|
+
def nested_ctrl_excludes
|
22
|
+
next_level_asks_from(ctrl_exclude)
|
23
|
+
end
|
24
|
+
|
25
|
+
def locally_included_attributes
|
26
|
+
[].push(resource_class.surrender_attributes)
|
27
|
+
.push(ctrl_included_attributes)
|
28
|
+
.push(user_included_attributes_to_render)
|
29
|
+
.flatten.uniq
|
30
|
+
.reject { |attr| exclude_locally?(attr) }
|
31
|
+
end
|
32
|
+
|
33
|
+
def locally_included_expands
|
34
|
+
[].push(user_included_local_expansions_to_render)
|
35
|
+
.push(ctrl_included_expansions)
|
36
|
+
.push(resource_class.surrender_expands)
|
37
|
+
.flatten.uniq
|
38
|
+
.each_with_object({}) { |key, result| result[key.to_sym] = [] }
|
39
|
+
.deep_merge(nested_user_includes)
|
40
|
+
.deep_merge(nested_ctrl_includes)
|
41
|
+
end
|
42
|
+
|
43
|
+
def exclude_locally?(key)
|
44
|
+
local_excludes.include?(key) && !local_user_includes.include?(key)
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def local_excludes
|
50
|
+
local_ctrl_excludes.dup.push(local_user_excludes).flatten.uniq
|
51
|
+
end
|
52
|
+
|
53
|
+
def user_included_attributes
|
54
|
+
top_level_keys_from(user_include).select { |include| attribute_type(include).in? %i[include associate] }
|
55
|
+
end
|
56
|
+
|
57
|
+
def user_included_attributes_to_render
|
58
|
+
attrs = user_included_attributes
|
59
|
+
unavailable_attrs = (attrs - resource_class.surrender_callable_attributes)
|
60
|
+
return attrs if unavailable_attrs.empty?
|
61
|
+
|
62
|
+
raise Error, I18n.t('surrender.error.query_string.include.not_available', param: unavailable_attrs)
|
63
|
+
end
|
64
|
+
|
65
|
+
def user_included_expansions
|
66
|
+
local_user_includes.select { |i| attribute_type(i) == :expand }
|
67
|
+
end
|
68
|
+
|
69
|
+
def user_included_local_expansions_to_render
|
70
|
+
expansions = user_included_expansions
|
71
|
+
unavailable_expansions = (expansions - resource_class.surrender_callable_expands)
|
72
|
+
return expansions if unavailable_expansions.empty?
|
73
|
+
|
74
|
+
raise Error, I18n.t('surrender.error.query_string.include.not_available', param: unavailable_expansions)
|
75
|
+
end
|
76
|
+
|
77
|
+
def ctrl_included_attributes
|
78
|
+
top_level_keys_from(ctrl_include).select { |include| attribute_type(include).in? %i[include associate] }
|
79
|
+
end
|
80
|
+
|
81
|
+
def ctrl_included_expansions
|
82
|
+
local_ctrl_includes.select { |i| attribute_type(i) == :expand }
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'configuration/inclusion_mapper_logic'
|
4
|
+
require_relative 'configuration/instance_logic'
|
5
|
+
|
6
|
+
module Rails
|
7
|
+
module Surrender
|
8
|
+
module Render
|
9
|
+
# Container for config structure when rendering or generating the inclusion object.
|
10
|
+
class Configuration
|
11
|
+
include InclusionMapperLogic
|
12
|
+
include InstanceLogic
|
13
|
+
|
14
|
+
attr_accessor :resource_class,
|
15
|
+
:reload_resource,
|
16
|
+
:user_exclude,
|
17
|
+
:user_include,
|
18
|
+
:ctrl_exclude,
|
19
|
+
:ctrl_include,
|
20
|
+
:history
|
21
|
+
|
22
|
+
alias reload_resource? reload_resource
|
23
|
+
|
24
|
+
Element = Struct.new(:name, :klass, :continue, keyword_init: true)
|
25
|
+
|
26
|
+
def initialize(
|
27
|
+
resource_class: nil,
|
28
|
+
reload_resource: false,
|
29
|
+
user_exclude: [],
|
30
|
+
user_include: [],
|
31
|
+
ctrl_exclude: [],
|
32
|
+
ctrl_include: [],
|
33
|
+
history: []
|
34
|
+
)
|
35
|
+
@resource_class = resource_class
|
36
|
+
@reload_resource = reload_resource
|
37
|
+
@user_exclude = user_exclude.compact
|
38
|
+
@user_include = user_include
|
39
|
+
@ctrl_exclude = ctrl_exclude.compact
|
40
|
+
@ctrl_include = ctrl_include
|
41
|
+
@history = history
|
42
|
+
|
43
|
+
validate_user_includes!
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def validate_user_includes!
|
49
|
+
return if invalid_local_user_includes.empty?
|
50
|
+
|
51
|
+
raise Error, I18n.t('surrender.error.query_string.include.not_available', param: invalid_local_user_includes)
|
52
|
+
end
|
53
|
+
|
54
|
+
def invalid_local_user_includes
|
55
|
+
local_user_includes.select { |include| attribute_type(include) == :none }
|
56
|
+
end
|
57
|
+
|
58
|
+
def local_ctrl_excludes
|
59
|
+
top_level_keys_from(ctrl_exclude)
|
60
|
+
end
|
61
|
+
|
62
|
+
def local_ctrl_includes
|
63
|
+
top_level_keys_from(ctrl_include)
|
64
|
+
end
|
65
|
+
|
66
|
+
def local_user_excludes
|
67
|
+
top_level_keys_from(user_exclude)
|
68
|
+
end
|
69
|
+
|
70
|
+
def local_user_includes
|
71
|
+
top_level_keys_from(user_include)
|
72
|
+
end
|
73
|
+
|
74
|
+
def attribute_type(attr, klass: resource_class)
|
75
|
+
return :expand if klass.reflections.keys.include?(attr.to_s)
|
76
|
+
return :associate if klass.reflections.keys.include?(attr.to_s.sub('_ids', '').pluralize)
|
77
|
+
return :include if klass.attribute_names.include?(attr.to_s)
|
78
|
+
return :include if klass.instance_methods.include?(attr)
|
79
|
+
|
80
|
+
:none
|
81
|
+
end
|
82
|
+
|
83
|
+
def top_level_keys_from(list)
|
84
|
+
list.map { |x| x.is_a?(Hash) ? x.keys : x }.flatten.map(&:to_sym).uniq
|
85
|
+
end
|
86
|
+
|
87
|
+
def next_level_asks_from(list)
|
88
|
+
list.select { |x| x.is_a? Hash }.reduce({}, :merge).symbolize_keys
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rails
|
4
|
+
module Surrender
|
5
|
+
module Render
|
6
|
+
# Rendering a count of resources.
|
7
|
+
class Count
|
8
|
+
attr_reader :resource
|
9
|
+
|
10
|
+
def initialize(resource)
|
11
|
+
@resource = resource
|
12
|
+
end
|
13
|
+
|
14
|
+
def parse
|
15
|
+
count = resource.respond_to?(:count) ? resource.count : 1
|
16
|
+
Response.new(data: { count: count })
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rails
|
4
|
+
module Surrender
|
5
|
+
module Render
|
6
|
+
# Rendering the IDs of the requested resources.
|
7
|
+
class Ids
|
8
|
+
attr_reader :resource
|
9
|
+
|
10
|
+
def initialize(resource)
|
11
|
+
@resource = resource
|
12
|
+
end
|
13
|
+
|
14
|
+
def parse
|
15
|
+
ids = resource.respond_to?(:ids) ? resource.ids : [resource.id]
|
16
|
+
Response.new(data: { ids: ids })
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rails
|
4
|
+
module Surrender
|
5
|
+
module Render
|
6
|
+
class Resource
|
7
|
+
# Renders a collection resource
|
8
|
+
class Collection
|
9
|
+
attr_reader :resource, :config, :ability
|
10
|
+
|
11
|
+
def initialize(resource:, config:, ability:)
|
12
|
+
@resource = resource
|
13
|
+
@config = config
|
14
|
+
@ability = ability
|
15
|
+
end
|
16
|
+
|
17
|
+
def render
|
18
|
+
return nil if resource.nil?
|
19
|
+
|
20
|
+
resource.map { |data| Instance.new(resource: data, config: config, ability: ability).render }
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rails
|
4
|
+
module Surrender
|
5
|
+
module Render
|
6
|
+
class Resource
|
7
|
+
# Builds a complete map of the resources needed to fulfill the request, for supplying to ActiveRecord.includes
|
8
|
+
class InclusionMapper
|
9
|
+
attr_reader :resource_class, :config
|
10
|
+
|
11
|
+
Element = Struct.new(:name, :klass, keyword_init: true)
|
12
|
+
# InclusionMapper is designed to recursively crawl through the model rendering structure and build a hash
|
13
|
+
# that ActiveRecord can use to eager load ALL of the data we're going to render, to prevent N+1 queries
|
14
|
+
def initialize(resource_class:, config:)
|
15
|
+
@resource_class = resource_class
|
16
|
+
@config = config
|
17
|
+
end
|
18
|
+
|
19
|
+
def parse
|
20
|
+
config.history.push resource_class
|
21
|
+
includes = []
|
22
|
+
|
23
|
+
config.expanding_elements.each do |element|
|
24
|
+
item_config = Configuration.new(
|
25
|
+
resource_class: element.klass,
|
26
|
+
user_include: config.nested_user_includes[element.name] || [],
|
27
|
+
ctrl_include: config.nested_ctrl_includes[element.name] || [],
|
28
|
+
user_exclude: config.nested_user_excludes[element.name] || [],
|
29
|
+
ctrl_exclude: config.nested_ctrl_excludes[element.name] || [],
|
30
|
+
history: config.history.dup.push(element.klass)
|
31
|
+
)
|
32
|
+
|
33
|
+
nested = if element.continue
|
34
|
+
InclusionMapper.new(resource_class: element.klass, config: item_config).parse
|
35
|
+
else
|
36
|
+
[]
|
37
|
+
end
|
38
|
+
|
39
|
+
includes << (nested.size.zero? ? element.name : { element.name => nested })
|
40
|
+
end
|
41
|
+
|
42
|
+
includes.sort_by { |x| x.is_a?(Symbol) ? 0 : 1 }
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rails
|
4
|
+
module Surrender
|
5
|
+
module Render
|
6
|
+
class Resource
|
7
|
+
# Renders an instance resource
|
8
|
+
class Instance
|
9
|
+
attr_reader :resource, :config, :ability
|
10
|
+
|
11
|
+
def initialize(resource:, config:, ability:)
|
12
|
+
@resource = resource
|
13
|
+
@config = config
|
14
|
+
@ability = ability
|
15
|
+
end
|
16
|
+
|
17
|
+
def render
|
18
|
+
config.history.push history_class
|
19
|
+
|
20
|
+
result = {}
|
21
|
+
config.locally_included_attributes.each { |attr| result[attr.to_sym] = resource.send(attr) }
|
22
|
+
|
23
|
+
config.locally_included_expands.each_key do |key|
|
24
|
+
next if config.exclude_locally?(key)
|
25
|
+
|
26
|
+
nested_resource_class = nested_class_for(resource, key)
|
27
|
+
next if config.history.include? nested_resource_class
|
28
|
+
|
29
|
+
nested_config = nested_config_for(nested_resource_class, key)
|
30
|
+
|
31
|
+
if resource.class.reflections[key.to_s].try(:collection?)
|
32
|
+
collection = resource.send(key.to_sym).select { |i| ability.can? :read, i }
|
33
|
+
result[key] = Collection.new(resource: collection, config: nested_config, ability: ability).render
|
34
|
+
else
|
35
|
+
instance = resource.send(key)
|
36
|
+
next if config.history.include? instance.class
|
37
|
+
|
38
|
+
if ability.can?(:read, instance)
|
39
|
+
result[key] = Instance.new(resource: instance, config: nested_config, ability: ability).render
|
40
|
+
elsif instance.nil?
|
41
|
+
result[key] = nil # represent an associated element as null if it's missing
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
result
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def nested_class_for(resource, key)
|
51
|
+
resource.class.reflections[key.to_s].klass
|
52
|
+
rescue NoMethodError
|
53
|
+
resource.send(key).class
|
54
|
+
end
|
55
|
+
|
56
|
+
def nested_config_for(nested_resource_class, key)
|
57
|
+
Configuration.new(
|
58
|
+
resource_class: nested_resource_class,
|
59
|
+
user_include: config.nested_user_includes[key] || [],
|
60
|
+
ctrl_include: config.nested_ctrl_includes[key] || [],
|
61
|
+
user_exclude: config.nested_user_excludes[key] || [],
|
62
|
+
ctrl_exclude: config.nested_ctrl_excludes[key] || [],
|
63
|
+
history: config.history
|
64
|
+
)
|
65
|
+
end
|
66
|
+
|
67
|
+
# get to the root subclass for sti models and store that as history
|
68
|
+
def history_class
|
69
|
+
resource.class.superclass until resource.class.superclass.in? [ActiveRecord::Base, ApplicationRecord]
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'resource/inclusion_mapper'
|
4
|
+
require_relative 'resource/collection'
|
5
|
+
require_relative 'resource/instance'
|
6
|
+
|
7
|
+
module Rails
|
8
|
+
module Surrender
|
9
|
+
module Render
|
10
|
+
# Rendering a resource, and it's various nested components, according to the given config params.
|
11
|
+
class Resource
|
12
|
+
attr_reader :resource, :ability, :config
|
13
|
+
|
14
|
+
def initialize(resource:, ability:, config:)
|
15
|
+
@resource = resource
|
16
|
+
@ability = ability
|
17
|
+
@config = config
|
18
|
+
end
|
19
|
+
|
20
|
+
def parse
|
21
|
+
data = case resource
|
22
|
+
when nil? then {}
|
23
|
+
when Hash || Array then resource
|
24
|
+
when ActiveRecord::Relation then collection_data
|
25
|
+
else instance_data
|
26
|
+
end
|
27
|
+
Response.new(data: data)
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def collection_data
|
33
|
+
includes = InclusionMapper.new(resource_class: resource.klass, config: config).parse
|
34
|
+
data = @resource.includes(includes)
|
35
|
+
Collection.new(resource: data, config: config, ability: ability).render
|
36
|
+
end
|
37
|
+
|
38
|
+
def instance_data
|
39
|
+
# Reloading the instance here allows us to take advantage of the eager loading
|
40
|
+
# capabilities of ActiveRecord with our 'includes' hash to prevent N+1 queries.
|
41
|
+
# This can save a TON of response time when the data sets begin to get large.
|
42
|
+
data = if config.reload_resource?
|
43
|
+
includes = InclusionMapper.new(resource_class: resource.class, config: config).parse
|
44
|
+
@resource = resource.class.includes(includes).find_by_id(resource.id)
|
45
|
+
else
|
46
|
+
resource
|
47
|
+
end
|
48
|
+
|
49
|
+
Instance.new(resource: data, config: config, ability: ability).render
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rails
|
4
|
+
module Surrender
|
5
|
+
# Generate a Response object from the given data
|
6
|
+
class Response
|
7
|
+
attr_accessor :data
|
8
|
+
|
9
|
+
def initialize(data:)
|
10
|
+
@data = data
|
11
|
+
end
|
12
|
+
|
13
|
+
def json_data
|
14
|
+
::Oj.dump(data, mode: :compat)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rails
|
4
|
+
# Base namespace for the entire project
|
5
|
+
module Surrender
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
require 'kaminari'
|
10
|
+
require 'oj'
|
11
|
+
|
12
|
+
require 'rails/surrender/default_ability'
|
13
|
+
require 'rails/surrender/controller_additions'
|
14
|
+
require 'rails/surrender/exceptions'
|
15
|
+
require 'rails/surrender/model_additions'
|
16
|
+
require 'rails/surrender/model_filter_scopes'
|
17
|
+
require 'rails/surrender/railtie'
|
18
|
+
require 'rails/surrender/render/ids'
|
19
|
+
require 'rails/surrender/render/configuration'
|
20
|
+
require 'rails/surrender/render/count'
|
21
|
+
require 'rails/surrender/render/resource'
|
22
|
+
require 'rails/surrender/response'
|
23
|
+
require 'rails/surrender/version'
|
24
|
+
|
25
|
+
# Load Surrender specific error messages
|
26
|
+
locale_path = Dir.glob("#{__dir__}/surrender/config/locales/*.{rb,yml}")
|
27
|
+
I18n.load_path += locale_path unless I18n.load_path.include?(locale_path)
|
28
|
+
|
29
|
+
# ActiveRecord Patches required for proper functionality
|
30
|
+
require 'rails/surrender/config/initializers/active_record_associations_patch'
|
31
|
+
require 'rails/surrender/config/initializers/active_record_preloader_patch'
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Configure Rails Environment
|
4
|
+
ENV['RAILS_ENV'] = 'test'
|
5
|
+
|
6
|
+
require File.expand_path('../test/dummy/config/environment.rb', __dir__)
|
7
|
+
ActiveRecord::Migrator.migrations_paths = [File.expand_path('../test/dummy/db/migrate', __dir__)]
|
8
|
+
require 'rails/test_help'
|
9
|
+
|
10
|
+
# Filter out Minitest backtrace while allowing backtrace from other libraries
|
11
|
+
# to be shown.
|
12
|
+
Minitest.backtrace_filter = Minitest::BacktraceFilter.new
|
13
|
+
|
14
|
+
# Load support files
|
15
|
+
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
|
16
|
+
|
17
|
+
# Load fixtures from the engine
|
18
|
+
if ActiveSupport::TestCase.respond_to?(:fixture_path=)
|
19
|
+
ActiveSupport::TestCase.fixture_path = File.expand_path('fixtures', __dir__)
|
20
|
+
ActiveSupport::TestCase.fixtures :all
|
21
|
+
end
|
metadata
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rails-surrender
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Shawn McBride
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2022-01-02 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: kaminari
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - '='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 1.2.1
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - '='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 1.2.1
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: oj
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - '='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 3.13.10
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - '='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 3.13.10
|
41
|
+
description:
|
42
|
+
email: smmcbride@gmail.com
|
43
|
+
executables: []
|
44
|
+
extensions: []
|
45
|
+
extra_rdoc_files: []
|
46
|
+
files:
|
47
|
+
- lib/rails/surrender.rb
|
48
|
+
- lib/rails/surrender/config/initializers/active_record_associations_patch.rb
|
49
|
+
- lib/rails/surrender/config/initializers/active_record_preloader_patch.rb
|
50
|
+
- lib/rails/surrender/config/locales/en.yml
|
51
|
+
- lib/rails/surrender/controller_additions.rb
|
52
|
+
- lib/rails/surrender/default_ability.rb
|
53
|
+
- lib/rails/surrender/exceptions.rb
|
54
|
+
- lib/rails/surrender/helpers/filter_builder.rb
|
55
|
+
- lib/rails/surrender/helpers/pagination_builder.rb
|
56
|
+
- lib/rails/surrender/helpers/query_param_parser.rb
|
57
|
+
- lib/rails/surrender/helpers/sort_builder.rb
|
58
|
+
- lib/rails/surrender/model_additions.rb
|
59
|
+
- lib/rails/surrender/model_filter_scopes.rb
|
60
|
+
- lib/rails/surrender/railtie.rb
|
61
|
+
- lib/rails/surrender/render/configuration.rb
|
62
|
+
- lib/rails/surrender/render/configuration/inclusion_mapper_logic.rb
|
63
|
+
- lib/rails/surrender/render/configuration/instance_logic.rb
|
64
|
+
- lib/rails/surrender/render/count.rb
|
65
|
+
- lib/rails/surrender/render/ids.rb
|
66
|
+
- lib/rails/surrender/render/resource.rb
|
67
|
+
- lib/rails/surrender/render/resource/collection.rb
|
68
|
+
- lib/rails/surrender/render/resource/inclusion_mapper.rb
|
69
|
+
- lib/rails/surrender/render/resource/instance.rb
|
70
|
+
- lib/rails/surrender/response.rb
|
71
|
+
- lib/rails/surrender/version.rb
|
72
|
+
- test/surrender_test.rb
|
73
|
+
- test/test_helper.rb
|
74
|
+
homepage: https://github.com/smmcbride/rails-surrender
|
75
|
+
licenses:
|
76
|
+
- GPL-3.0
|
77
|
+
metadata:
|
78
|
+
rubygems_mfa_required: 'true'
|
79
|
+
post_install_message:
|
80
|
+
rdoc_options: []
|
81
|
+
require_paths:
|
82
|
+
- lib
|
83
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
84
|
+
requirements:
|
85
|
+
- - "~>"
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: '3.0'
|
88
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
89
|
+
requirements:
|
90
|
+
- - ">="
|
91
|
+
- !ruby/object:Gem::Version
|
92
|
+
version: '0'
|
93
|
+
requirements: []
|
94
|
+
rubygems_version: 3.2.15
|
95
|
+
signing_key:
|
96
|
+
specification_version: 4
|
97
|
+
summary: JSON rendering for Rails API
|
98
|
+
test_files: []
|