magic-presenter 0.3.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 17e4d44c0c2d9a9e09e7e099a4067b9e6aafe27e1b389cf01808ed0a77d0b9ae
4
- data.tar.gz: 7d492486c2f4edb2407af461794e1f0c7e53c7f6b93e65595a618369fb845250
3
+ metadata.gz: ea088d296d049e126830600bfcc99c5265fbecb41e9d5db51c0d27c02c73e6c8
4
+ data.tar.gz: e5932ed2865e3f534d181ee18c9eccf26aefb250b48e5564cd3d56cf8b55040d
5
5
  SHA512:
6
- metadata.gz: fb2609c3dbba1cec025bca22aaa196c718b32084b2954b8184f9b5ce896e7a9289c0b7eb0660469edaf3571d27d99026f6e588f58004e395cee07a75820e38d7
7
- data.tar.gz: 1a87908713cb7dbcb029687dd1d4b9006432e8ba9703ef1dbe813088060c471ada70f8d7d1f5de4cdc9210ba602bd0457b1bf061f6fd3d9a25c5a420003198e0
6
+ metadata.gz: 3cd7a2241b4eae01cc861951d165cff4c52a563ee517c0f05f9e90fab79ac6c53f81a0ca561fb09a1fdaf88d9d39ef435953dcc87a3642c3a632735401d0a0b1
7
+ data.tar.gz: b4fb57b18de8d450e2297722ab69b09761fb9c5bb89d3b93dfb418f5f064771c5736f9423a617ddab404fb0193608c2c331f415c6a2bd44daee85321dd635bec
data/README.md CHANGED
@@ -12,7 +12,9 @@
12
12
 
13
13
  A bit of history: this gem was inspired by digging deeper into [Draper](https://github.com/drapergem/draper) with an eye on a refactoring.
14
14
 
15
- Based on [Magic Decorator](https://github.com/Alexander-Senko/magic-decorator), it implements a presenter logic.
15
+ Based on [Magic Decorator](
16
+ https://github.com/Alexander-Senko/magic-decorator
17
+ ), it implements a presenter logic.
16
18
 
17
19
  ## Installation
18
20
 
@@ -68,11 +70,41 @@ See the help for more info:
68
70
 
69
71
  $ bin/rails generate presenter --help
70
72
 
73
+ ### View helpers
74
+
75
+ A presenter can use any helpers via `#helpers` (aliased as `#h`) both in class and instance methods:
76
+
77
+ ```ruby
78
+ class PersonPresenter < Magic::Presenter::Base
79
+ def self.links
80
+ [ h.link_to('All', model_class) ]
81
+ end
82
+
83
+ def link(...)
84
+ helpers.link_to(name, self, ...)
85
+ end
86
+ end
87
+ ```
88
+
89
+ A view context must be set to enable helpers.
90
+ It’s done automagically [wherever possible](#view-context).
91
+ However, one can set it explicitly anywhere:
92
+
93
+ ```ruby
94
+ Magic::Presenter.with view_context: ApplicationController.new.view_context do
95
+ # put the code that uses helpers within presenters here
96
+ end
97
+ ```
98
+
99
+ > [!NOTE]
100
+ > A valid `request` may be needed for URL helpers to get host info.
101
+
71
102
  ## 🧙 Magic
72
103
 
73
- It’s based on [Magic Decorator](
74
- https://github.com/Alexander-Senko/magic-decorator#magic
75
- ), so get familiar with that one as well.
104
+ > [!IMPORTANT]
105
+ > It’s based on [Magic Decorator](
106
+ > https://github.com/Alexander-Senko/magic-decorator#magic
107
+ > ), so get familiar with that one as well.
76
108
 
77
109
  ### Presentable scope
78
110
 
@@ -86,7 +118,7 @@ Presenters provide automatic class inference for any model based on its class na
86
118
  ).
87
119
 
88
120
  For example, `MyNamespace::MyModel.new.decorate` looks for `MyNamespace::MyPresenter` first.
89
- When missing, it further looks for decorators for its ancestor classes, up to `ObjectPresenter`.
121
+ When missing, it further looks for presenters for its ancestor classes, up to `ObjectPresenter`.
90
122
 
91
123
  #### Mapping rules
92
124
 
@@ -103,12 +135,87 @@ When in doubt, one can use `Magic::Presenter.name_for`:
103
135
  Magic::Presenter.name_for Person # => "PersonPresenter"
104
136
  ```
105
137
 
106
- ### In views
138
+ #### Preloading
139
+
140
+ > [!NOTE]
141
+ > Magic Lookup doesn’t try to autoload any classes, it searches among already loaded ones instead.
142
+ > Thus, presenters should be preloaded to be visible via [lookups](#presenter-class-inference).
143
+
144
+ This is done automatically in both _test_ and _production_ environments by Rails.
145
+ All the application’s presenters and models are eagerly loaded before normal and reverse lookups by Magic Presenter as well.
146
+ So, normally one shouldn’t worry about that.
107
147
 
108
148
  > [!IMPORTANT]
149
+ > When developing a Rails engine that defines its own presenters, one should take care of the preloading themselves.
150
+
151
+ That could be done in an initializer with a helper method provided:
152
+
153
+ ```ruby
154
+ Rails.application.config.to_prepare do
155
+ Magic.eager_load :presenters, engine: MyLib::Engine
156
+ end
157
+ ```
158
+
159
+ ### Class methods delegation
160
+
161
+ Missing class methods of a presenter are delegated to a matching model class if the latter can be inferred unambiguously.
162
+ `Magic::Lookup::Error` is raised otherwise.
163
+
164
+ ### In views
165
+
166
+ > [!NOTE]
109
167
  > Every object passed to views is decorated automagically.
110
168
  > This involves both implicit instance variables and `locals` passed explicitly.
111
169
 
170
+ ### Helpers
171
+
172
+ One can call helpers directly without explicit `helper` or `h`:
173
+
174
+ ```ruby
175
+ class PersonPresenter < Magic::Presenter::Base
176
+ def self.links
177
+ [ link_to('All', model_class) ]
178
+ end
179
+
180
+ def link(...) = link_to(name, self, ...)
181
+ end
182
+ ```
183
+
184
+ #### View context
185
+
186
+ View context is set automagically to enable helpers:
187
+ - in views,
188
+ - in controller actions,
189
+ - in mailer actions.
190
+
191
+ ## Generators
192
+
193
+ > [!NOTE]
194
+ > The built-in `helper` generator is overridden with `presenter` one to generate presenters instead of helpers.
195
+
196
+ ## Testing presenters
197
+
198
+ Magic Presenter supports RSpec and Test::Unit.
199
+ The appropriate tests are generated alongside a presenter.
200
+
201
+ Testing presenters is much like [testing Rails helpers](
202
+ https://guides.rubyonrails.org/testing.html#testing-helpers
203
+ ).
204
+ Since the test class inherits from `ActionView::TestCase`, Rails’ helper methods such as `link_to`, `localize` and many others are available in tests.
205
+
206
+ As any presenter is a decorator, see also [how to test decorators](
207
+ https://github.com/Alexander-Senko/magic-decorator#testing-decorators
208
+ ).
209
+
210
+ ### RSpec
211
+
212
+ Presenter specs are expected to live in `spec/presenters`.
213
+ If a different path is used, `type: :presenter` metadata should be set explicitly.
214
+
215
+ ### Test::Unit
216
+
217
+ Tests related to the presenters are located under the `test/presenters` directory and inherit from `Magic::Presenter::TestCase`.
218
+
112
219
  ## Development
113
220
 
114
221
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ presenter_context = proc do
4
+ around_action :set_presenter_context
5
+
6
+ private
7
+
8
+ def set_presenter_context(&)
9
+ Magic::Presenter.with view_context:, &
10
+ end
11
+ end
12
+
13
+ ActiveSupport.on_load :action_controller, &presenter_context
14
+ ActiveSupport.on_load :action_mailer, &presenter_context
@@ -1,19 +1,41 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- ActiveSupport.on_load :action_view do
3
+ ActiveSupport.on_load :action_view do # rubocop:disable Metrics/BlockLength
4
4
  concerning :DecoratedAssignments, prepend: true do
5
- def assign(assignments, ...)
6
- assignments
7
- .transform_values! &:decorated
5
+ def assign assignments
6
+ decorate assignments
8
7
 
9
8
  super
10
9
  end
11
10
 
12
11
  def _run(method, template, locals, ...)
13
- locals
14
- .transform_values! &:decorated
12
+ decorate locals
15
13
 
16
14
  super
17
15
  end
16
+
17
+ private
18
+
19
+ def decorate objects
20
+ objects
21
+ .transform_values!(&:decorated)
22
+ end
23
+ end
24
+
25
+ concerning :PresenterContext, prepend: true do
26
+ def in_rendering_context(...)
27
+ Magic::Presenter.with view_context: self do
28
+ super
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def decorate(...)
35
+ super
36
+ .each_value
37
+ .grep(Magic::Presenter::Base)
38
+ .each { _1.view_context = self }
39
+ end
18
40
  end
19
41
  end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ if defined? Rails::Generators
4
+ # HACK: eliminates the following Thor warning:
5
+ #
6
+ # Deprecation warning: Expected boolean default value for '--helper'; got :presenter (string).
7
+ Thor::Option.prepend Module.new {
8
+ def validate_default_type!
9
+ return if @name == 'helper'
10
+
11
+ super
12
+ end
13
+ }
14
+
15
+ Magic.each_engine do |engine|
16
+ engine.config.generators do
17
+ _1.helper = :presenter
18
+ end
19
+ end
20
+ end
@@ -1,7 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- RSpec.configure do |config|
4
- config.include concern(:PresenterExampleGroup) {
5
- included { metadata[:type] = :presenter }
6
- }, file_path: %r'spec/presenters', type: :presenter
7
- end if defined? RSpec
3
+ if defined? RSpec::Core
4
+ RSpec.configure do |config|
5
+ if defined? RSpec::Rails
6
+ require 'rspec/rails/example/presenter_example_group'
7
+
8
+ config.include RSpec::Rails::PresenterExampleGroup, type: :presenter
9
+ end
10
+
11
+ # Tag all groups and examples in the spec/presenters directory with
12
+ # type: :presenter
13
+ config.define_derived_metadata file_path: %r'/spec/presenters/' do |metadata|
14
+ metadata[:type] ||= :presenter
15
+ end
16
+ end
17
+ end
@@ -3,7 +3,7 @@
3
3
  module Magic
4
4
  module Presenter
5
5
  module Generator # :nodoc:
6
- require 'generators/presenter/presenter_generator'
6
+ require 'generators/rails/presenter/presenter_generator'
7
7
 
8
8
  private
9
9
 
@@ -18,7 +18,7 @@ module Magic
18
18
  root / 'presenters' / path
19
19
  end
20
20
 
21
- def presenter_path(*) = file_path(*, root: PresenterGenerator.target_root)
21
+ def presenter_path(*) = file_path(*, root: Rails::PresenterGenerator.target_root)
22
22
  end
23
23
  end
24
24
  end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rails
4
+ class PresenterGenerator < Generators::NamedBase # :nodoc:
5
+ include Magic::Presenter::Generator
6
+ include Generators::ResourceHelpers
7
+
8
+ source_root File.expand_path('templates', __dir__)
9
+
10
+ check_class_collision suffix: 'Presenter'
11
+
12
+ class_option :parent,
13
+ type: :string,
14
+ default: 'ApplicationPresenter',
15
+ desc: 'The parent class for the generated presenter'
16
+
17
+ cattr_reader :target_root, default: Pathname('app')
18
+
19
+ def create_presenter_file
20
+ template 'presenter.rb', "#{file_path}.rb"
21
+ end
22
+
23
+ hook_for :test_framework do |generator|
24
+ invoke generator, [ name ]
25
+ end
26
+
27
+ private
28
+
29
+ def parent_class_name = options[:parent].classify
30
+ end
31
+ end
@@ -1,9 +1,6 @@
1
1
  require "test_helper"
2
2
 
3
3
  <% module_namespacing do -%>
4
- class <%= class_name %>Test < ActiveSupport::TestCase
5
- # test "the truth" do
6
- # assert true
7
- # end
4
+ class <%= class_name %>Test < Magic::Presenter::TestCase
8
5
  end
9
6
  <% end -%>
@@ -18,6 +18,7 @@ module Magic
18
18
  # up to `ObjectPresenter`.
19
19
  class Base < Decorator::Base
20
20
  include GlobalID if defined? ::GlobalID
21
+ prepend Helpers if defined? ::ActionView
21
22
 
22
23
  class << self
23
24
  def name_for object_class
@@ -39,6 +40,8 @@ module Magic
39
40
  } for #{self}"
40
41
  end
41
42
 
43
+ delegate_missing_to :model_class
44
+
42
45
  def descendants
43
46
  Magic.eager_load :presenters
44
47
 
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Magic
4
+ module Presenter
5
+ module Helpers # :nodoc:
6
+ extend ActiveSupport::Concern
7
+
8
+ # The Magic::Presenter::Helpers::MissingContext exception is
9
+ # raised when no view context to run helpers in has been set.
10
+ class MissingContext < RuntimeError
11
+ def message
12
+ <<~TEXT
13
+ missing view context
14
+ You should set Magic::Presenter.view_context first
15
+ TEXT
16
+ end
17
+ end
18
+
19
+ prepended do
20
+ class_attribute :view_context
21
+ end
22
+
23
+ class_methods do
24
+ include Helpers
25
+
26
+ alias __raise__ raise
27
+ end
28
+
29
+ private
30
+
31
+ def helpers
32
+ view_context or
33
+ __raise__ MissingContext
34
+ end
35
+
36
+ alias_method :h, :helpers
37
+
38
+ def method_missing(method, ...)
39
+ super
40
+ rescue NoMethodError
41
+ __raise__ unless view_context
42
+ __raise__ unless helpers.respond_to? method
43
+
44
+ helpers.send(method, ...)
45
+ end
46
+
47
+ def respond_to_missing?(method, ...)
48
+ super or
49
+ (helpers if view_context).respond_to?(method, ...)
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'action_view/test_case'
4
+
5
+ module Magic
6
+ module Presenter
7
+ # = Magic Presenter test case
8
+ #
9
+ class TestCase < ActiveSupport::TestCase
10
+ module Behavior # :nodoc:
11
+ extend ActiveSupport::Concern
12
+
13
+ include ActionView::TestCase::Behavior
14
+
15
+ included do
16
+ Magic.each_engine { include _1.routes.url_helpers }
17
+ end
18
+ end
19
+
20
+ include Behavior
21
+ end
22
+ end
23
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Magic
4
4
  module Presenter
5
- VERSION = '0.3.0'
5
+ VERSION = '1.0.0'
6
6
  end
7
7
  end
@@ -9,25 +9,34 @@ module Magic # :nodoc:
9
9
  # Presentation layer for Rails models
10
10
  module Presenter
11
11
  autoload :Base, 'magic/presenter/base'
12
+ autoload :Helpers, 'magic/presenter/helpers'
12
13
  autoload :GlobalID, 'magic/presenter/global_id'
14
+ autoload :TestCase, 'magic/presenter/test_case'
13
15
  autoload :Generator, 'generators/magic/presenter/generator'
14
16
 
15
- module_function
16
-
17
- def for(...) = Base.for(...)
18
- def name_for(...) = Base.name_for(...)
17
+ singleton_class.delegate *%i[
18
+ for name_for
19
+ view_context view_context=
20
+ ], to: Base
19
21
  end
20
22
 
21
- module_function
23
+ module_function # TODO: extract to Magic Support
22
24
 
23
- def eager_load *scopes
25
+ def eager_load *scopes, engine: Rails.application
24
26
  return if Rails.application.config.eager_load
25
27
 
26
28
  scopes
27
29
  .map(&:to_s)
28
30
  .map(&:pluralize)
29
- .map { Rails.root / 'app' / _1 }
31
+ .map { engine.root / 'app' / _1 }
30
32
  .select(&:exist?)
31
33
  .each { Rails.autoloaders.main.eager_load_dir _1 }
32
34
  end
35
+
36
+ def each_engine(&)
37
+ Rails.application
38
+ .then { [ _1, *_1.railties ] }
39
+ .grep(Rails::Engine)
40
+ .each(&)
41
+ end
33
42
  end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rspec/rails'
4
+
5
+ module RSpec
6
+ module Rails
7
+ # @api public
8
+ # Container module for presenter specs.
9
+ module PresenterExampleGroup
10
+ extend ActiveSupport::Concern
11
+ include HelperExampleGroup
12
+ include Magic::Presenter::TestCase::Behavior
13
+
14
+ included do
15
+ around { Magic::Presenter.with view_context: self, &_1 }
16
+ end
17
+ end
18
+ end
19
+ end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: magic-presenter
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alexander Senko
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2024-10-27 00:00:00.000000000 Z
10
+ date: 2024-11-23 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: rails
@@ -35,14 +35,28 @@ dependencies:
35
35
  requirements:
36
36
  - - "~>"
37
37
  - !ruby/object:Gem::Version
38
- version: '0.3'
38
+ version: '1.0'
39
39
  type: :runtime
40
40
  prerelease: false
41
41
  version_requirements: !ruby/object:Gem::Requirement
42
42
  requirements:
43
43
  - - "~>"
44
44
  - !ruby/object:Gem::Version
45
- version: '0.3'
45
+ version: '1.0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: magic-lookup
48
+ requirement: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: '0'
53
+ type: :runtime
54
+ prerelease: false
55
+ version_requirements: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: '0'
46
60
  description: Based on Magic Decorator, it’s meant to replace Draper.
47
61
  email:
48
62
  - Alexander.Senko@gmail.com
@@ -54,16 +68,18 @@ files:
54
68
  - README.md
55
69
  - Rakefile
56
70
  - app/models/concerns/magic/presentable.rb
71
+ - config/initializers/action_controller.rb
57
72
  - config/initializers/action_view.rb
73
+ - config/initializers/generators.rb
58
74
  - config/initializers/presentable.rb
59
75
  - config/initializers/rspec.rb
60
76
  - lib/generators/magic/presenter/generator.rb
61
77
  - lib/generators/magic/presenter/install/USAGE
62
78
  - lib/generators/magic/presenter/install/install_generator.rb
63
79
  - lib/generators/magic/presenter/install/templates/application_presenter.rb.tt
64
- - lib/generators/presenter/USAGE
65
- - lib/generators/presenter/presenter_generator.rb
66
- - lib/generators/presenter/templates/presenter.rb.tt
80
+ - lib/generators/rails/presenter/USAGE
81
+ - lib/generators/rails/presenter/presenter_generator.rb
82
+ - lib/generators/rails/presenter/templates/presenter.rb.tt
67
83
  - lib/generators/rspec/presenter/presenter_generator.rb
68
84
  - lib/generators/rspec/presenter/templates/presenter_spec.rb.tt
69
85
  - lib/generators/test_unit/presenter/presenter_generator.rb
@@ -73,7 +89,10 @@ files:
73
89
  - lib/magic/presenter/base.rb
74
90
  - lib/magic/presenter/engine.rb
75
91
  - lib/magic/presenter/global_id.rb
92
+ - lib/magic/presenter/helpers.rb
93
+ - lib/magic/presenter/test_case.rb
76
94
  - lib/magic/presenter/version.rb
95
+ - lib/rspec/rails/example/presenter_example_group.rb
77
96
  - lib/tasks/magic/presenter_tasks.rake
78
97
  homepage: https://github.com/Alexander-Senko/magic-presenter
79
98
  licenses:
@@ -81,7 +100,7 @@ licenses:
81
100
  metadata:
82
101
  homepage_uri: https://github.com/Alexander-Senko/magic-presenter
83
102
  source_code_uri: https://github.com/Alexander-Senko/magic-presenter
84
- changelog_uri: https://github.com/Alexander-Senko/magic-presenter/blob/v0.3.0/CHANGELOG.md
103
+ changelog_uri: https://github.com/Alexander-Senko/magic-presenter/blob/v1.0.0/CHANGELOG.md
85
104
  rdoc_options: []
86
105
  require_paths:
87
106
  - lib
@@ -1,26 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class PresenterGenerator < Rails::Generators::NamedBase # :nodoc:
4
- include Magic::Presenter::Generator
5
-
6
- source_root File.expand_path('templates', __dir__)
7
-
8
- check_class_collision suffix: 'Presenter'
9
-
10
- class_option :parent,
11
- type: :string,
12
- default: 'ApplicationPresenter',
13
- desc: 'The parent class for the generated presenter'
14
-
15
- cattr_reader :target_root, default: Pathname('app')
16
-
17
- def create_presenter_file
18
- template 'presenter.rb', "#{file_path}.rb"
19
- end
20
-
21
- hook_for :test_framework
22
-
23
- private
24
-
25
- def parent_class_name = options[:parent].classify
26
- end