strong_presenter 0.0.1 → 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.
Files changed (132) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +5 -2
  3. data/.rspec +2 -0
  4. data/.travis.yml +18 -0
  5. data/.yardopts +1 -0
  6. data/CHANGELOG.md +31 -0
  7. data/Gemfile +22 -2
  8. data/Guardfile +26 -0
  9. data/README.md +210 -52
  10. data/Rakefile +77 -1
  11. data/gemfiles/3.0.gemfile +2 -0
  12. data/gemfiles/3.1.gemfile +2 -0
  13. data/gemfiles/3.2.gemfile +2 -0
  14. data/gemfiles/4.0.gemfile +2 -0
  15. data/gemfiles/4.1.gemfile +2 -0
  16. data/lib/generators/controller_override.rb +15 -0
  17. data/lib/generators/mini_test/presenter_generator.rb +20 -0
  18. data/lib/generators/mini_test/templates/presenter_spec.rb +4 -0
  19. data/lib/generators/mini_test/templates/presenter_test.rb +4 -0
  20. data/lib/generators/rails/presenter_generator.rb +36 -0
  21. data/lib/generators/rails/templates/presenter.rb +19 -0
  22. data/lib/generators/rspec/presenter_generator.rb +9 -0
  23. data/lib/generators/rspec/templates/presenter_spec.rb +4 -0
  24. data/lib/generators/test_unit/presenter_generator.rb +9 -0
  25. data/lib/generators/test_unit/templates/presenter_test.rb +4 -0
  26. data/lib/strong_presenter/associable.rb +78 -0
  27. data/lib/strong_presenter/collection_presenter.rb +90 -0
  28. data/lib/strong_presenter/controller_additions.rb +50 -0
  29. data/lib/strong_presenter/delegation.rb +18 -0
  30. data/lib/strong_presenter/factory.rb +74 -0
  31. data/lib/strong_presenter/helper_proxy.rb +29 -11
  32. data/lib/strong_presenter/inferrer.rb +54 -0
  33. data/lib/strong_presenter/permissible.rb +73 -0
  34. data/lib/strong_presenter/permissions.rb +138 -0
  35. data/lib/strong_presenter/presenter.rb +191 -0
  36. data/lib/strong_presenter/presenter_association.rb +29 -0
  37. data/lib/strong_presenter/presenter_helper_constructor.rb +60 -0
  38. data/lib/strong_presenter/railtie.rb +27 -3
  39. data/lib/strong_presenter/tasks/test.rake +22 -0
  40. data/lib/strong_presenter/test/devise_helper.rb +30 -0
  41. data/lib/strong_presenter/test/minitest_integration.rb +6 -0
  42. data/lib/strong_presenter/test/rspec_integration.rb +16 -0
  43. data/lib/strong_presenter/test_case.rb +53 -0
  44. data/lib/strong_presenter/version.rb +1 -1
  45. data/lib/strong_presenter/view_context/build_strategy.rb +48 -0
  46. data/lib/strong_presenter/view_context.rb +84 -0
  47. data/lib/strong_presenter/view_helpers.rb +39 -0
  48. data/lib/strong_presenter.rb +64 -2
  49. data/spec/dummy/.rspec +2 -0
  50. data/spec/dummy/Rakefile +7 -0
  51. data/spec/dummy/app/controllers/application_controller.rb +4 -0
  52. data/spec/dummy/app/controllers/localized_urls.rb +5 -0
  53. data/spec/dummy/app/controllers/posts_controller.rb +25 -0
  54. data/spec/dummy/app/helpers/application_helper.rb +5 -0
  55. data/spec/dummy/app/mailers/application_mailer.rb +3 -0
  56. data/spec/dummy/app/mailers/post_mailer.rb +19 -0
  57. data/spec/dummy/app/models/admin.rb +5 -0
  58. data/spec/dummy/app/models/post.rb +3 -0
  59. data/spec/dummy/app/models/user.rb +5 -0
  60. data/spec/dummy/app/presenters/post_presenter.rb +69 -0
  61. data/spec/dummy/app/presenters/special_post_presenter.rb +5 -0
  62. data/spec/dummy/app/presenters/special_posts_presenter.rb +5 -0
  63. data/spec/dummy/app/views/layouts/application.html.erb +11 -0
  64. data/spec/dummy/app/views/post_mailer/presented_email.html.erb +1 -0
  65. data/spec/dummy/app/views/posts/_post.html.erb +50 -0
  66. data/spec/dummy/app/views/posts/show.html.erb +1 -0
  67. data/spec/dummy/bin/rails +4 -0
  68. data/spec/dummy/config/application.rb +70 -0
  69. data/spec/dummy/config/boot.rb +5 -0
  70. data/spec/dummy/config/database.yml +25 -0
  71. data/spec/dummy/config/environment.rb +5 -0
  72. data/spec/dummy/config/environments/development.rb +33 -0
  73. data/spec/dummy/config/environments/production.rb +57 -0
  74. data/spec/dummy/config/environments/test.rb +31 -0
  75. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  76. data/spec/dummy/config/initializers/inflections.rb +15 -0
  77. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  78. data/spec/dummy/config/initializers/secret_token.rb +8 -0
  79. data/spec/dummy/config/initializers/session_store.rb +8 -0
  80. data/spec/dummy/config/locales/en.yml +5 -0
  81. data/spec/dummy/config/routes.rb +9 -0
  82. data/spec/dummy/config.ru +4 -0
  83. data/spec/dummy/db/migrate/20121019115657_create_posts.rb +8 -0
  84. data/spec/dummy/db/schema.rb +21 -0
  85. data/spec/dummy/db/seeds.rb +2 -0
  86. data/spec/dummy/fast_spec/post_presenter_spec.rb +37 -0
  87. data/spec/dummy/lib/tasks/test.rake +16 -0
  88. data/spec/dummy/log/.gitkeep +0 -0
  89. data/spec/dummy/public/404.html +26 -0
  90. data/spec/dummy/public/422.html +26 -0
  91. data/spec/dummy/public/500.html +25 -0
  92. data/spec/dummy/public/favicon.ico +0 -0
  93. data/spec/dummy/script/rails +6 -0
  94. data/spec/dummy/spec/fast_spec_helper.rb +13 -0
  95. data/spec/dummy/spec/mailers/post_mailer_spec.rb +33 -0
  96. data/spec/dummy/spec/models/post_spec.rb +4 -0
  97. data/spec/dummy/spec/presenters/active_model_serializers_spec.rb +11 -0
  98. data/spec/dummy/spec/presenters/devise_spec.rb +64 -0
  99. data/spec/dummy/spec/presenters/helpers_spec.rb +21 -0
  100. data/spec/dummy/spec/presenters/post_presenter_spec.rb +66 -0
  101. data/spec/dummy/spec/presenters/spec_type_spec.rb +7 -0
  102. data/spec/dummy/spec/presenters/special_post_presenter_spec.rb +11 -0
  103. data/spec/dummy/spec/presenters/view_context_spec.rb +22 -0
  104. data/spec/dummy/spec/spec_helper.rb +19 -0
  105. data/spec/dummy/test/minitest_helper.rb +2 -0
  106. data/spec/dummy/test/presenters/minitest/devise_test.rb +64 -0
  107. data/spec/dummy/test/presenters/minitest/helpers_test.rb +21 -0
  108. data/spec/dummy/test/presenters/minitest/spec_type_test.rb +52 -0
  109. data/spec/dummy/test/presenters/minitest/view_context_test.rb +24 -0
  110. data/spec/dummy/test/presenters/test_unit/devise_test.rb +64 -0
  111. data/spec/dummy/test/presenters/test_unit/helpers_test.rb +21 -0
  112. data/spec/dummy/test/presenters/test_unit/view_context_test.rb +24 -0
  113. data/spec/dummy/test/test_helper.rb +13 -0
  114. data/spec/generators/presenters/presenter_generator_spec.rb +131 -0
  115. data/spec/generators/simplecov_spec.rb +5 -0
  116. data/spec/integration/integration_spec.rb +81 -0
  117. data/spec/integration/simplecov_spec.rb +4 -0
  118. data/spec/spec_helper.rb +47 -0
  119. data/spec/strong_presenter/associable_spec.rb +122 -0
  120. data/spec/strong_presenter/collection_presenter_spec.rb +34 -0
  121. data/spec/strong_presenter/delegation_spec.rb +20 -0
  122. data/spec/strong_presenter/permissible_spec.rb +24 -0
  123. data/spec/strong_presenter/permissions_spec.rb +188 -0
  124. data/spec/strong_presenter/presenter_spec.rb +43 -0
  125. data/spec/strong_presenter/simplecov_spec.rb +4 -0
  126. data/spec/support/dummy_app.rb +85 -0
  127. data/spec/support/matchers/have_text.rb +50 -0
  128. data/spec/support/models.rb +14 -0
  129. data/spec/support/schema.rb +12 -0
  130. data/strong_presenter.gemspec +15 -0
  131. metadata +392 -13
  132. data/lib/strong_presenter/base.rb +0 -217
@@ -0,0 +1,36 @@
1
+ module Rails
2
+ module Generators
3
+ class PresenterGenerator < NamedBase
4
+ source_root File.expand_path("../templates", __FILE__)
5
+ check_class_collision suffix: "Presenter"
6
+
7
+ class_option :parent, type: :string, desc: "The parent class for the generated presenter"
8
+
9
+ def create_presenter_file
10
+ template 'presenter.rb', File.join('app/presenters', class_path, "#{file_name}_presenter.rb")
11
+ end
12
+
13
+ hook_for :test_framework
14
+
15
+ private
16
+
17
+ def parent_class_name
18
+ options.fetch("parent") do
19
+ begin
20
+ require 'application_presenter'
21
+ ApplicationPresenter
22
+ rescue LoadError
23
+ "StrongPresenter::Presenter"
24
+ end
25
+ end
26
+ end
27
+
28
+ # Rails 3.0.X compatibility, stolen from https://github.com/jnunemaker/mongomapper/pull/385/files#L1R32
29
+ unless methods.include?(:module_namespacing)
30
+ def module_namespacing
31
+ yield if block_given?
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,19 @@
1
+ <%- module_namespacing do -%>
2
+ <%- if parent_class_name.present? -%>
3
+ class <%= class_name %>Presenter < <%= parent_class_name %>
4
+ <%- else -%>
5
+ class <%= class_name %>
6
+ <%- end -%>
7
+ delegate_all
8
+
9
+ # Define presentation-specific methods here. Helpers are accessed through
10
+ # `helpers` (aka `h`). You can override attributes, for example:
11
+ #
12
+ # def created_at
13
+ # helpers.content_tag :span, class: 'time' do
14
+ # object.created_at.strftime("%a %m/%d/%y")
15
+ # end
16
+ # end
17
+
18
+ end
19
+ <% end -%>
@@ -0,0 +1,9 @@
1
+ module Rspec
2
+ class PresenterGenerator < ::Rails::Generators::NamedBase
3
+ source_root File.expand_path('../templates', __FILE__)
4
+
5
+ def create_spec_file
6
+ template 'presenter_spec.rb', File.join('spec/presenters', class_path, "#{singular_name}_presenter_spec.rb")
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,4 @@
1
+ require 'spec_helper'
2
+
3
+ describe <%= class_name %>Presenter do
4
+ end
@@ -0,0 +1,9 @@
1
+ module TestUnit
2
+ class PresenterGenerator < ::Rails::Generators::NamedBase
3
+ source_root File.expand_path('../templates', __FILE__)
4
+
5
+ def create_test_file
6
+ template 'presenter_test.rb', File.join('test/presenters', class_path, "#{singular_name}_presenter_test.rb")
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,4 @@
1
+ require 'test_helper'
2
+
3
+ class <%= class_name %>PresenterTest < StrongPresenter::TestCase
4
+ end
@@ -0,0 +1,78 @@
1
+ module StrongPresenter
2
+
3
+ # Methods for defining presenter associations
4
+ module Associable
5
+ extend ActiveSupport::Concern
6
+
7
+ module ClassMethods
8
+ # Automatically wraps multiple associations.
9
+ # @param [Symbol] association
10
+ # name of the association to wrap.
11
+ # @option options [Class] :with
12
+ # the presenter to apply to the association.
13
+ # @option options [Symbol] :scope
14
+ # a scope to apply when fetching the association.
15
+ # @yield
16
+ # block executed when association presenter is initialized, in
17
+ # the context of the parent presenter instance (instance_exec-ed)
18
+ # @yieldparam [Presenter] the association presenter
19
+ # @return [void]
20
+ def presents_association(association, options = {})
21
+ options.assert_valid_keys(:with, :scope)
22
+ options[:with] = Associable.object_association_class(object_class, association) unless options.has_key? :with
23
+ presenter_associations[association] ||= StrongPresenter::PresenterAssociation.new(association, options) do |presenter|
24
+ presenter.link_permissions self, association
25
+ yield presenter if block_given?
26
+ end
27
+ define_method(association) do
28
+ self.class.send(:presenter_associations)[association].wrap(self)
29
+ end
30
+ end
31
+
32
+ # @overload presents_associations(*associations, options = {})
33
+ # Automatically wraps multiple associations.
34
+ # @param [Symbols*] associations
35
+ # names of the associations to wrap.
36
+ # @option options [Class] :with
37
+ # the presenter to apply to the association.
38
+ # @option options [Symbol] :scope
39
+ # a scope to apply when fetching the association.
40
+ # @yield
41
+ # block executed when association presenter is initialized, in
42
+ # the context of the parent presenter instance (instance_exec-ed)
43
+ # @yieldparam [Presenter] the association presenter
44
+ # @return [void]
45
+ def presents_associations(*associations)
46
+ options = associations.extract_options!
47
+ options.assert_valid_keys(:with, :scope)
48
+ associations.each { |association| presents_association(association, options) {|presenter| yield if block_given?} }
49
+ end
50
+
51
+ private
52
+ def presenter_associations
53
+ @presenter_associations ||= {}
54
+ end
55
+ end
56
+
57
+ protected
58
+
59
+ # infer association class if possible
60
+ def self.object_association_class(object_class, association)
61
+ if self.descendant_of(object_class, "ActiveRecord::Reflection")
62
+ association_class = object_class.reflect_on_association(association).klass
63
+ else
64
+ return nil
65
+ end
66
+ "#{association_class}Presenter".constantize
67
+ rescue NameError
68
+ nil
69
+ end
70
+
71
+ def self.descendant_of(object_class, klass)
72
+ object_class.ancestors.include? klass.constantize
73
+ rescue NameError
74
+ false
75
+ end
76
+ end
77
+ end
78
+
@@ -0,0 +1,90 @@
1
+ module StrongPresenter
2
+ class CollectionPresenter
3
+ include Enumerable
4
+ include StrongPresenter::ViewHelpers
5
+ include StrongPresenter::Permissible
6
+ include StrongPresenter::Delegation
7
+
8
+ array_methods = Array.instance_methods - Object.instance_methods
9
+ delegate :==, :as_json, *array_methods, to: :collection
10
+
11
+ # @param [Enumerable] object
12
+ # collection to present
13
+ def initialize(object, options = {})
14
+ options.assert_valid_keys(:with)
15
+ @object = object
16
+ @presenter_class = options[:with]
17
+ end
18
+
19
+ def to_s
20
+ "#<#{self.class.name} of #{presenter_class || "inferred presenters"} for #{object.inspect}>"
21
+ end
22
+
23
+ protected
24
+ # @return [Class] the presenter class used to present each item, as set by
25
+ # {#initialize}.
26
+ def presenter_class
27
+ @presenter_class = @presenter_class.nil? ? self.class.send(:presenter_class) : @presenter_class
28
+ end
29
+
30
+ def item_presenter(item)
31
+ return presenter_class if presenter_class
32
+ StrongPresenter::Presenter.inferred_presenter(item)
33
+ end
34
+
35
+ # @return the collection being presented.
36
+ attr_reader :object
37
+
38
+ # Wraps the given item.
39
+ def wrap_item(item)
40
+ item_presenter(item).new(item).tap do |presenter|
41
+ presenter.link_permissions self # item's permitted_attributes is linked to collection
42
+ end
43
+ end
44
+
45
+ # @return [Array] the items being presented
46
+ def collection
47
+ @collection ||= object.map{|item| wrap_item(item)}
48
+ end
49
+
50
+ class << self
51
+ # Sets the presenter used to wrap models in the collection
52
+ def presents_with presenter
53
+ @presenter_class = presenter
54
+ self
55
+ end
56
+
57
+ protected
58
+ def set_item_presenter_collection
59
+ collection = self
60
+ presenter_class.instance_exec do
61
+ unless nil?
62
+ if !const_defined? :Collection
63
+ const_set :Collection, collection
64
+ elsif self::Collection.name.demodulize == "Collection"
65
+ remove_const :Collection
66
+ const_set :Collection, collection
67
+ end
68
+ end
69
+ end
70
+ rescue NameError => error
71
+ end
72
+
73
+ private
74
+ def inherited(subclass)
75
+ subclass.set_item_presenter_collection
76
+ super
77
+ end
78
+
79
+ def inferred_presenter_class
80
+ presenter_class = Inferrer.new(name).chomp("Presenter").inferred_class { |name| "#{name.singularize}Presenter" }
81
+ presenter_class == self ? nil : presenter_class
82
+ end
83
+
84
+ def presenter_class
85
+ @presenter_class ||= inferred_presenter_class
86
+ end
87
+
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,50 @@
1
+ module StrongPresenter
2
+ module ControllerAdditions
3
+ extend ActiveSupport::Concern
4
+
5
+ module ClassMethods
6
+ # @overload presents(*variables, options = {})
7
+ # Defines a helper method to access instance variables wrapped in presenters.
8
+ #
9
+ # @example
10
+ # # app/controllers/articles_controller.rb
11
+ # class ArticlesController < ApplicationController
12
+ # presents :article
13
+ # presents :comments, with: ArticleCommentsPresenter, only: :show
14
+ # presents :comments, with: CommentsPresenter, only: :index { |presenter| presenter.permit(:author, :text) }
15
+ #
16
+ # def show
17
+ # @article = Article.find(params[:id])
18
+ # end
19
+ # end
20
+ #
21
+ # # app/views/articles/show.html.erb
22
+ # <%= article.presented_title %>
23
+ #
24
+ # @param [Symbols*] variables
25
+ # names of the instance variables to present (without the `@`).
26
+ # @param [Hash] options
27
+ # @option options [Presenter, CollectionPresenter] :with (nil)
28
+ # presenter class to use. If nil, it is inferred from the instance
29
+ # variable.
30
+ # @option options [Symbols*] :only (nil)
31
+ # apply presenter only on these controller actions.
32
+ # @option options [Symbols*] :except (nil)
33
+ # don't apply presenter on these controller actions.
34
+ # @yield [Presenter] code to execute when presenter is initialized
35
+ def presents(*variables, &block)
36
+ options = variables.extract_options!
37
+ options.assert_valid_keys(:with, :only, :except)
38
+
39
+ constructor = StrongPresenter::PresenterHelperConstructor.new(self, block, options)
40
+
41
+ variables.each do |variable|
42
+ constructor.call(variable)
43
+ helper_method variable
44
+ end
45
+ end
46
+
47
+ end
48
+ end
49
+ end
50
+
@@ -0,0 +1,18 @@
1
+ module StrongPresenter
2
+ module Delegation
3
+ extend ActiveSupport::Concern
4
+
5
+ module ClassMethods
6
+ # @overload delegate(*methods, options = {})
7
+ # Overrides {http://api.rubyonrails.org/classes/Module.html#method-i-delegate Module.delegate}
8
+ # to make `:object` the default delegation target.
9
+ #
10
+ # @return [void]
11
+ def delegate(*methods)
12
+ options = methods.extract_options!
13
+ super *methods, options.reverse_merge(to: :object)
14
+ end
15
+ end
16
+ end
17
+ end
18
+
@@ -0,0 +1,74 @@
1
+ module StrongPresenter
2
+ class Factory
3
+ # Creates a presenter factory.
4
+ #
5
+ # @option options [Presenter, CollectionPresenter] :with (nil)
6
+ # presenter class to use. If nil, it is inferred from the object
7
+ # passed to {#wrap}.
8
+ def initialize(options = {})
9
+ options.assert_valid_keys(:with)
10
+ @presenter_class = options.delete(:with)
11
+ end
12
+
13
+ # Wraps an object with a presenter, inferring whether to create a singular or collection
14
+ # presenter from the type of object passed.
15
+ #
16
+ # @param [Object] object
17
+ # object to present.
18
+ # @return [Presenter, CollectionPresenter] the presenter.
19
+ def wrap(object)
20
+ return nil if object.nil?
21
+ Worker.new(presenter_class, object).call { |presenter| yield presenter if block_given? }
22
+ end
23
+
24
+ private
25
+
26
+ attr_reader :presenter_class
27
+
28
+ # @private
29
+ class Worker
30
+ def initialize(presenter_class, object)
31
+ @presenter_class = presenter_class
32
+ @object = object
33
+ @presenter_class = presenter_class::Collection if collection? && !presenter_class.nil? && (presenter_class < StrongPresenter::Presenter)
34
+ end
35
+
36
+ def call
37
+ presenter.new(object) { |presenter| yield presenter if block_given? }
38
+ end
39
+
40
+ def presenter
41
+ return presenter_class if presenter_class
42
+ return object_presenter if object_presenter
43
+ return StrongPresenter::CollectionPresenter if collection?
44
+ raise StrongPresenter::UninferrablePresenterError.new(object.class)
45
+ end
46
+
47
+ private
48
+
49
+ attr_reader :presenter_class, :object
50
+
51
+ def object_presenter
52
+ @object_presenter = @object_presenter.nil? ? get_object_presenter : @object_presenter
53
+ end
54
+
55
+ def get_object_presenter
56
+ StrongPresenter::Presenter.inferred_presenter(object)
57
+ rescue NameError => error
58
+ false
59
+ end
60
+
61
+ def collection?
62
+ object.is_a?(Enumerable) or is_a?("ActiveRecord::Associations::CollectionProxy") # or any other wrappers
63
+ end
64
+
65
+ # Checks if object is an instance of klass, false in case klass does not exist.
66
+ def is_a? klass
67
+ object.is_a? klass.constantize
68
+ rescue NameError
69
+ false
70
+ end
71
+ end
72
+ end
73
+ end
74
+
@@ -1,16 +1,34 @@
1
1
  module StrongPresenter
2
- class HelperProxy < ActionView::Base
3
- include Rails.application.routes.url_helpers
4
-
5
- def method_missing method, *args, &block
6
- if ApplicationController.helpers.respond_to? method
7
- ApplicationController.helpers.public_send method, *args, &block
8
- else
9
- super
10
- end
2
+ # Provides access to helper methods - both Rails built-in helpers, and those
3
+ # defined in your application.
4
+
5
+ # Copied from Draper::HelperProxy
6
+ class HelperProxy
7
+
8
+ # @overload initialize(view_context)
9
+ def initialize(view_context)
10
+ @view_context = view_context
11
11
  end
12
- def respond_to? method, include_all = false
13
- ApplicationController.helpers.respond_to?(method, include_all) or super
12
+
13
+ # Sends helper methods to the view context.
14
+ def method_missing(method, *args, &block)
15
+ self.class.define_proxy method
16
+ send(method, *args, &block)
17
+ end
18
+
19
+ delegate :capture, to: :view_context
20
+
21
+ protected
22
+
23
+ attr_reader :view_context
24
+
25
+ private
26
+
27
+ def self.define_proxy(name)
28
+ define_method name do |*args, &block|
29
+ view_context.send(name, *args, &block)
30
+ end
14
31
  end
32
+
15
33
  end
16
34
  end
@@ -0,0 +1,54 @@
1
+ module StrongPresenter
2
+ # @private
3
+ # Helper class for inferring class names
4
+ class Inferrer
5
+ attr_accessor :input, :suffix
6
+
7
+ # Constructs inferer object
8
+ # @param [String] input
9
+ # Input name as base to infer from
10
+ def initialize(input)
11
+ self.input = input
12
+ end
13
+
14
+ # Sets input suffix
15
+ # @param [String] suffix
16
+ # Suffix which must be present in input to chomp/remove
17
+ # @return [self] chainable
18
+ def chomp(suffix)
19
+ self.suffix = suffix
20
+ self
21
+ end
22
+
23
+ # Extracts name by removing suffix
24
+ # @return [String]
25
+ def extract_name
26
+ raise UnextractableError if input.nil? || input.demodulize !~ /.+#{suffix}$/
27
+ input.chomp(suffix)
28
+ end
29
+
30
+ # Retrieve inferred class if it exists. If not, nil is returned.
31
+ # @yield (optional) for further transforming name
32
+ # @yieldparam [String] name after suffix removed
33
+ # @yieldreturn [String] name after transformation
34
+ # @return [Class] inferred class
35
+ def inferred_class
36
+ name = extract_name
37
+ name = yield name if block_given?
38
+ name.constantize
39
+ rescue NameError => error
40
+ raise unless Inferrer.missing_name?(error, name)
41
+ nil
42
+ end
43
+
44
+ protected
45
+ # Detect if error is due to inferred class not existing, or some other error
46
+ def self.missing_name?(error, name)
47
+ return true if error.is_a? UnextractableError
48
+ missing_name = error.missing_name
49
+ length = [missing_name.length, name.length].min
50
+ missing_name[-length..-1] == name[-length..-1]
51
+ end
52
+ class UnextractableError < NameError; end
53
+ end
54
+ end
@@ -0,0 +1,73 @@
1
+ module StrongPresenter
2
+ module Permissible
3
+ extend ActiveSupport::Concern
4
+
5
+ module ClassMethods
6
+ # Permits all presenter attributes for presents, present & filter methods.
7
+ def permit!
8
+ define_method(:permitted_attributes){ @permitted_attributes ||= StrongPresenter::Permissions.new.permit_all! }
9
+ end
10
+ end
11
+
12
+ # Permits given attributes. May be invoked multiple times.
13
+ #
14
+ # @example Each argument represents a single attribute:
15
+ # ArticlePresenter.new(@article).permit(:heading, :article)
16
+ #
17
+ # @example Attribute paths can be specified using symbol arrays. If an author name is normally accessed using @article.author.name:
18
+ # ArticlePresenter.new(@article).permit([:author, :name])
19
+ #
20
+ # @param [[Symbols*]*] attribute_paths
21
+ # the attributes to permit. An array of symbols represents an attribute path.
22
+ # @return [self]
23
+ def permit *attribute_paths
24
+ permitted_attributes.permit permissions_prefix, *attribute_paths
25
+ self
26
+ end
27
+
28
+ # Permits all presenter attributes for presents, present & filter methods.
29
+ def permit!
30
+ permitted_attributes.permit_all!
31
+ self
32
+ end
33
+
34
+ # Selects the attributes given which have been permitted - an array of attributes. Attributes are
35
+ # symbols, and attribute paths are arrays of symbols.
36
+ # @param [Array<Symbol>*] attribute_paths
37
+ # the attribute paths to check. The attribute paths may also have arguments.
38
+ # @return [Array<Array<Symbol>, Symbol>] attribute (paths)
39
+ def filter *attribute_paths
40
+ select_permitted(*attribute_paths).map{ |attribute| attribute.first if attribute.size == 1 } # un-pack symbol if array with single symbol
41
+ end
42
+
43
+ protected
44
+ def permitted_attributes
45
+ @permitted_attributes ||= StrongPresenter::Permissions.new
46
+ end
47
+
48
+ # Selects the attributes given which have been permitted - an array of attributes
49
+ # Each returned attribute paths will be an array, even if it consists of only 1 symbol
50
+ # @param [Array<Symbols>*] attribute_paths
51
+ # the attribute paths to check. The attribute paths may also have arguments.
52
+ # @return [Array<Array<Symbol>>] attribute (paths)
53
+ def select_permitted *attribute_paths
54
+ permitted_attributes.select_permitted permissions_prefix, *attribute_paths
55
+ end
56
+
57
+ # Links presenter to permissions group of given presenter.
58
+ # @param [Presenter] parent_presenter
59
+ # @param [Array<Symbol>] relative_path
60
+ # The prefix prepended before every permission check relative to parent presenter.
61
+ def link_permissions parent_presenter, relative_path = []
62
+ self.permissions_prefix = parent_presenter.send(:permissions_prefix) + Array(relative_path)
63
+ @permitted_attributes = parent_presenter.send(:permitted_attributes).merge @permitted_attributes, permissions_prefix
64
+ end
65
+
66
+ private
67
+ attr_writer :permissions_prefix
68
+ def permissions_prefix
69
+ @permissions_prefix ||= []
70
+ end
71
+ end
72
+ end
73
+