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.
- checksums.yaml +7 -0
- data/.gitignore +5 -2
- data/.rspec +2 -0
- data/.travis.yml +18 -0
- data/.yardopts +1 -0
- data/CHANGELOG.md +31 -0
- data/Gemfile +22 -2
- data/Guardfile +26 -0
- data/README.md +210 -52
- data/Rakefile +77 -1
- data/gemfiles/3.0.gemfile +2 -0
- data/gemfiles/3.1.gemfile +2 -0
- data/gemfiles/3.2.gemfile +2 -0
- data/gemfiles/4.0.gemfile +2 -0
- data/gemfiles/4.1.gemfile +2 -0
- data/lib/generators/controller_override.rb +15 -0
- data/lib/generators/mini_test/presenter_generator.rb +20 -0
- data/lib/generators/mini_test/templates/presenter_spec.rb +4 -0
- data/lib/generators/mini_test/templates/presenter_test.rb +4 -0
- data/lib/generators/rails/presenter_generator.rb +36 -0
- data/lib/generators/rails/templates/presenter.rb +19 -0
- data/lib/generators/rspec/presenter_generator.rb +9 -0
- data/lib/generators/rspec/templates/presenter_spec.rb +4 -0
- data/lib/generators/test_unit/presenter_generator.rb +9 -0
- data/lib/generators/test_unit/templates/presenter_test.rb +4 -0
- data/lib/strong_presenter/associable.rb +78 -0
- data/lib/strong_presenter/collection_presenter.rb +90 -0
- data/lib/strong_presenter/controller_additions.rb +50 -0
- data/lib/strong_presenter/delegation.rb +18 -0
- data/lib/strong_presenter/factory.rb +74 -0
- data/lib/strong_presenter/helper_proxy.rb +29 -11
- data/lib/strong_presenter/inferrer.rb +54 -0
- data/lib/strong_presenter/permissible.rb +73 -0
- data/lib/strong_presenter/permissions.rb +138 -0
- data/lib/strong_presenter/presenter.rb +191 -0
- data/lib/strong_presenter/presenter_association.rb +29 -0
- data/lib/strong_presenter/presenter_helper_constructor.rb +60 -0
- data/lib/strong_presenter/railtie.rb +27 -3
- data/lib/strong_presenter/tasks/test.rake +22 -0
- data/lib/strong_presenter/test/devise_helper.rb +30 -0
- data/lib/strong_presenter/test/minitest_integration.rb +6 -0
- data/lib/strong_presenter/test/rspec_integration.rb +16 -0
- data/lib/strong_presenter/test_case.rb +53 -0
- data/lib/strong_presenter/version.rb +1 -1
- data/lib/strong_presenter/view_context/build_strategy.rb +48 -0
- data/lib/strong_presenter/view_context.rb +84 -0
- data/lib/strong_presenter/view_helpers.rb +39 -0
- data/lib/strong_presenter.rb +64 -2
- data/spec/dummy/.rspec +2 -0
- data/spec/dummy/Rakefile +7 -0
- data/spec/dummy/app/controllers/application_controller.rb +4 -0
- data/spec/dummy/app/controllers/localized_urls.rb +5 -0
- data/spec/dummy/app/controllers/posts_controller.rb +25 -0
- data/spec/dummy/app/helpers/application_helper.rb +5 -0
- data/spec/dummy/app/mailers/application_mailer.rb +3 -0
- data/spec/dummy/app/mailers/post_mailer.rb +19 -0
- data/spec/dummy/app/models/admin.rb +5 -0
- data/spec/dummy/app/models/post.rb +3 -0
- data/spec/dummy/app/models/user.rb +5 -0
- data/spec/dummy/app/presenters/post_presenter.rb +69 -0
- data/spec/dummy/app/presenters/special_post_presenter.rb +5 -0
- data/spec/dummy/app/presenters/special_posts_presenter.rb +5 -0
- data/spec/dummy/app/views/layouts/application.html.erb +11 -0
- data/spec/dummy/app/views/post_mailer/presented_email.html.erb +1 -0
- data/spec/dummy/app/views/posts/_post.html.erb +50 -0
- data/spec/dummy/app/views/posts/show.html.erb +1 -0
- data/spec/dummy/bin/rails +4 -0
- data/spec/dummy/config/application.rb +70 -0
- data/spec/dummy/config/boot.rb +5 -0
- data/spec/dummy/config/database.yml +25 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +33 -0
- data/spec/dummy/config/environments/production.rb +57 -0
- data/spec/dummy/config/environments/test.rb +31 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/inflections.rb +15 -0
- data/spec/dummy/config/initializers/mime_types.rb +5 -0
- data/spec/dummy/config/initializers/secret_token.rb +8 -0
- data/spec/dummy/config/initializers/session_store.rb +8 -0
- data/spec/dummy/config/locales/en.yml +5 -0
- data/spec/dummy/config/routes.rb +9 -0
- data/spec/dummy/config.ru +4 -0
- data/spec/dummy/db/migrate/20121019115657_create_posts.rb +8 -0
- data/spec/dummy/db/schema.rb +21 -0
- data/spec/dummy/db/seeds.rb +2 -0
- data/spec/dummy/fast_spec/post_presenter_spec.rb +37 -0
- data/spec/dummy/lib/tasks/test.rake +16 -0
- data/spec/dummy/log/.gitkeep +0 -0
- data/spec/dummy/public/404.html +26 -0
- data/spec/dummy/public/422.html +26 -0
- data/spec/dummy/public/500.html +25 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/dummy/script/rails +6 -0
- data/spec/dummy/spec/fast_spec_helper.rb +13 -0
- data/spec/dummy/spec/mailers/post_mailer_spec.rb +33 -0
- data/spec/dummy/spec/models/post_spec.rb +4 -0
- data/spec/dummy/spec/presenters/active_model_serializers_spec.rb +11 -0
- data/spec/dummy/spec/presenters/devise_spec.rb +64 -0
- data/spec/dummy/spec/presenters/helpers_spec.rb +21 -0
- data/spec/dummy/spec/presenters/post_presenter_spec.rb +66 -0
- data/spec/dummy/spec/presenters/spec_type_spec.rb +7 -0
- data/spec/dummy/spec/presenters/special_post_presenter_spec.rb +11 -0
- data/spec/dummy/spec/presenters/view_context_spec.rb +22 -0
- data/spec/dummy/spec/spec_helper.rb +19 -0
- data/spec/dummy/test/minitest_helper.rb +2 -0
- data/spec/dummy/test/presenters/minitest/devise_test.rb +64 -0
- data/spec/dummy/test/presenters/minitest/helpers_test.rb +21 -0
- data/spec/dummy/test/presenters/minitest/spec_type_test.rb +52 -0
- data/spec/dummy/test/presenters/minitest/view_context_test.rb +24 -0
- data/spec/dummy/test/presenters/test_unit/devise_test.rb +64 -0
- data/spec/dummy/test/presenters/test_unit/helpers_test.rb +21 -0
- data/spec/dummy/test/presenters/test_unit/view_context_test.rb +24 -0
- data/spec/dummy/test/test_helper.rb +13 -0
- data/spec/generators/presenters/presenter_generator_spec.rb +131 -0
- data/spec/generators/simplecov_spec.rb +5 -0
- data/spec/integration/integration_spec.rb +81 -0
- data/spec/integration/simplecov_spec.rb +4 -0
- data/spec/spec_helper.rb +47 -0
- data/spec/strong_presenter/associable_spec.rb +122 -0
- data/spec/strong_presenter/collection_presenter_spec.rb +34 -0
- data/spec/strong_presenter/delegation_spec.rb +20 -0
- data/spec/strong_presenter/permissible_spec.rb +24 -0
- data/spec/strong_presenter/permissions_spec.rb +188 -0
- data/spec/strong_presenter/presenter_spec.rb +43 -0
- data/spec/strong_presenter/simplecov_spec.rb +4 -0
- data/spec/support/dummy_app.rb +85 -0
- data/spec/support/matchers/have_text.rb +50 -0
- data/spec/support/models.rb +14 -0
- data/spec/support/schema.rb +12 -0
- data/strong_presenter.gemspec +15 -0
- metadata +392 -13
- 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,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,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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
13
|
-
|
|
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
|
+
|