strong_presenter 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
+