rails-surrender 0.1.0
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 +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: []
|