rails-surrender 0.1.0 → 8.0.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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +103 -0
  3. data/LICENSE +674 -0
  4. data/README.md +240 -0
  5. data/lib/rails/surrender/config/locales/en.yml +3 -0
  6. data/lib/rails/surrender/controller_additions.rb +13 -73
  7. data/lib/rails/surrender/fields/asks.rb +50 -0
  8. data/lib/rails/surrender/fields/attribute.rb +37 -0
  9. data/lib/rails/surrender/fields/expand.rb +65 -0
  10. data/lib/rails/surrender/fields/field_tree.rb +71 -0
  11. data/lib/rails/surrender/fields/id_list.rb +38 -0
  12. data/lib/rails/surrender/fields/node.rb +86 -0
  13. data/lib/rails/surrender/menu/field.rb +15 -0
  14. data/lib/rails/surrender/menu/filter.rb +11 -0
  15. data/lib/rails/surrender/menu/menu.rb +55 -0
  16. data/lib/rails/surrender/model_additions.rb +47 -32
  17. data/lib/rails/surrender/params/list_param.rb +23 -0
  18. data/lib/rails/surrender/railtie.rb +23 -9
  19. data/lib/rails/surrender/render/resource.rb +11 -30
  20. data/lib/rails/surrender/selection/filtering.rb +144 -0
  21. data/lib/rails/surrender/selection/page_size.rb +33 -0
  22. data/lib/rails/surrender/selection/selected.rb +50 -0
  23. data/lib/rails/surrender/selection/selection.rb +70 -0
  24. data/lib/rails/surrender/selection/sorting.rb +61 -0
  25. data/lib/rails/surrender/version.rb +1 -1
  26. data/lib/rails/surrender.rb +13 -12
  27. metadata +97 -35
  28. data/lib/rails/surrender/config/initializers/active_record_associations_patch.rb +0 -17
  29. data/lib/rails/surrender/config/initializers/active_record_preloader_patch.rb +0 -20
  30. data/lib/rails/surrender/helpers/filter_builder.rb +0 -50
  31. data/lib/rails/surrender/helpers/pagination_builder.rb +0 -25
  32. data/lib/rails/surrender/helpers/query_param_parser.rb +0 -98
  33. data/lib/rails/surrender/helpers/sort_builder.rb +0 -56
  34. data/lib/rails/surrender/model_filter_scopes.rb +0 -56
  35. data/lib/rails/surrender/render/configuration/inclusion_mapper_logic.rb +0 -69
  36. data/lib/rails/surrender/render/configuration/instance_logic.rb +0 -88
  37. data/lib/rails/surrender/render/configuration.rb +0 -93
  38. data/lib/rails/surrender/render/count.rb +0 -21
  39. data/lib/rails/surrender/render/ids.rb +0 -21
  40. data/lib/rails/surrender/render/resource/collection.rb +0 -26
  41. data/lib/rails/surrender/render/resource/inclusion_mapper.rb +0 -48
  42. data/lib/rails/surrender/render/resource/instance.rb +0 -75
  43. data/test/surrender_test.rb +0 -9
  44. data/test/test_helper.rb +0 -21
@@ -1,56 +0,0 @@
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
@@ -1,69 +0,0 @@
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
@@ -1,88 +0,0 @@
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
@@ -1,93 +0,0 @@
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
@@ -1,21 +0,0 @@
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
@@ -1,21 +0,0 @@
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
@@ -1,26 +0,0 @@
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
@@ -1,48 +0,0 @@
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
@@ -1,75 +0,0 @@
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
@@ -1,9 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'test_helper'
4
-
5
- class SurrenderTest < ActiveSupport::TestCase
6
- test 'truth' do
7
- assert_kind_of Module, Surrender
8
- end
9
- end
data/test/test_helper.rb DELETED
@@ -1,21 +0,0 @@
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