magic-presenter 0.3.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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