magic-presenter 0.4.0 → 1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8ca84b456b21332f6eeba63a5667ed1c8cf7ac97d59141192f138d1698c504bf
4
- data.tar.gz: c380ea14f2ddd44150ae71095063e895486d33077521fe1067fca81d4c4b36b0
3
+ metadata.gz: 85a48a4efc45256dda38fcb77db1d477118712dde86576c03978c5e653188f3a
4
+ data.tar.gz: 542fbc0836dcce1d87b845b5d84a405d45941c139e6ed41e4114dca002b29409
5
5
  SHA512:
6
- metadata.gz: 728dbf1b11115069bb58bfb47c81258f358d22c3570c35dc4a5e7e4d9429a2fa5a6dc3237d0ef0e6a0442d88ca7e9b405e481bd5ea2da90c618800593c8f5155
7
- data.tar.gz: f014751ea9cf0470fd1387f25f63b56291a63a434abd177ca0e1aaab27ba1d9a4da4e590354b729fcd12a40fa19f125866daedfba4bd904f37e8d48b3c281ea6
6
+ metadata.gz: efd78f68cd2eb528c2208e220422945fc75ec8c464d71e9b2a92c2fd65d49a01943e6b2d78962f02cadafb3edb3b664b414b7ef4d0e08937b9e30569c0449c1a
7
+ data.tar.gz: 06f5013f93b259fbcc962a8a6caf4d6a8126b96ad5ca075d388dd184db410f90df8d335b03563a8eb5ae9168f175adc84b75c8256eb0094cf8200d30b899a3b5
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
 
@@ -85,7 +87,7 @@ end
85
87
  ```
86
88
 
87
89
  A view context must be set to enable helpers.
88
- It’s done automagically [wherever possible](#helpers).
90
+ It’s done automagically [wherever possible](#view-context).
89
91
  However, one can set it explicitly anywhere:
90
92
 
91
93
  ```ruby
@@ -99,9 +101,10 @@ end
99
101
 
100
102
  ## 🧙 Magic
101
103
 
102
- It’s based on [Magic Decorator](
103
- https://github.com/Alexander-Senko/magic-decorator#magic
104
- ), 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.
105
108
 
106
109
  ### Presentable scope
107
110
 
@@ -115,7 +118,7 @@ Presenters provide automatic class inference for any model based on its class na
115
118
  ).
116
119
 
117
120
  For example, `MyNamespace::MyModel.new.decorate` looks for `MyNamespace::MyPresenter` first.
118
- 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`.
119
122
 
120
123
  #### Mapping rules
121
124
 
@@ -132,19 +135,87 @@ When in doubt, one can use `Magic::Presenter.name_for`:
132
135
  Magic::Presenter.name_for Person # => "PersonPresenter"
133
136
  ```
134
137
 
135
- ### 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.
136
147
 
137
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]
138
167
  > Every object passed to views is decorated automagically.
139
168
  > This involves both implicit instance variables and `locals` passed explicitly.
140
169
 
141
170
  ### Helpers
142
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
+
143
186
  View context is set automagically to enable helpers:
144
187
  - in views,
145
188
  - in controller actions,
146
189
  - in mailer actions.
147
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
+
148
219
  ## Development
149
220
 
150
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.
@@ -2,7 +2,7 @@
2
2
 
3
3
  ActiveSupport.on_load :action_view do # rubocop:disable Metrics/BlockLength
4
4
  concerning :DecoratedAssignments, prepend: true do
5
- def assign(assignments, ...)
5
+ def assign assignments
6
6
  decorate assignments
7
7
 
8
8
  super
@@ -24,7 +24,7 @@ ActiveSupport.on_load :action_view do # rubocop:disable Metrics/BlockLength
24
24
 
25
25
  concerning :PresenterContext, prepend: true do
26
26
  def in_rendering_context(...)
27
- Magic::Presenter::Base.with view_context: self do
27
+ Magic::Presenter.with view_context: self do
28
28
  super
29
29
  end
30
30
  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 -%>
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'delegate'
4
-
5
3
  module Magic
6
4
  module Presenter
7
5
  # = Magic Presenter
@@ -31,7 +29,14 @@ module Magic
31
29
 
32
30
  def model_class
33
31
  Presentable.classes
34
- .select { Presenter.for(_1) == self }
32
+ .select { self.for(_1) == self }
33
+ .optional do |classes|
34
+ next unless classes.many?
35
+
36
+ classes
37
+ .select { name_for(_1) == name }
38
+ .optional { classes if _1.empty? } # lookup failed — return original
39
+ end
35
40
  .sole
36
41
  rescue Enumerable::SoleItemExpectedError => error
37
42
  raise Lookup::Error, "#{error.message
@@ -40,6 +45,8 @@ module Magic
40
45
  } for #{self}"
41
46
  end
42
47
 
48
+ delegate_missing_to :model_class
49
+
43
50
  def descendants
44
51
  Magic.eager_load :presenters
45
52
 
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'magic/core_ext/kernel/optional'
4
+
3
5
  module Magic
4
6
  module Presenter
5
7
  class Engine < ::Rails::Engine # :nodoc:
@@ -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.4.0'
5
+ VERSION = '1.1.0'
6
6
  end
7
7
  end
@@ -11,6 +11,7 @@ module Magic # :nodoc:
11
11
  autoload :Base, 'magic/presenter/base'
12
12
  autoload :Helpers, 'magic/presenter/helpers'
13
13
  autoload :GlobalID, 'magic/presenter/global_id'
14
+ autoload :TestCase, 'magic/presenter/test_case'
14
15
  autoload :Generator, 'generators/magic/presenter/generator'
15
16
 
16
17
  singleton_class.delegate *%i[
@@ -19,16 +20,23 @@ module Magic # :nodoc:
19
20
  ], to: Base
20
21
  end
21
22
 
22
- module_function
23
+ module_function # TODO: extract to Magic Support
23
24
 
24
- def eager_load *scopes
25
+ def eager_load *scopes, engine: Rails.application
25
26
  return if Rails.application.config.eager_load
26
27
 
27
28
  scopes
28
29
  .map(&:to_s)
29
30
  .map(&:pluralize)
30
- .map { Rails.root / 'app' / _1 }
31
+ .map { engine.root / 'app' / _1 }
31
32
  .select(&:exist?)
32
33
  .each { Rails.autoloaders.main.eager_load_dir _1 }
33
34
  end
35
+
36
+ def each_engine(&)
37
+ Rails.application
38
+ .then { [ _1, *_1.railties ] }
39
+ .grep(Rails::Engine)
40
+ .each(&)
41
+ end
34
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.4.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alexander Senko
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2024-10-29 00:00:00.000000000 Z
10
+ date: 2025-05-20 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: rails
@@ -35,14 +35,42 @@ 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'
60
+ - !ruby/object:Gem::Dependency
61
+ name: magic-support
62
+ requirement: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: '0'
67
+ type: :runtime
68
+ prerelease: false
69
+ version_requirements: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: '0'
46
74
  description: Based on Magic Decorator, it’s meant to replace Draper.
47
75
  email:
48
76
  - Alexander.Senko@gmail.com
@@ -56,15 +84,16 @@ files:
56
84
  - app/models/concerns/magic/presentable.rb
57
85
  - config/initializers/action_controller.rb
58
86
  - config/initializers/action_view.rb
87
+ - config/initializers/generators.rb
59
88
  - config/initializers/presentable.rb
60
89
  - config/initializers/rspec.rb
61
90
  - lib/generators/magic/presenter/generator.rb
62
91
  - lib/generators/magic/presenter/install/USAGE
63
92
  - lib/generators/magic/presenter/install/install_generator.rb
64
93
  - lib/generators/magic/presenter/install/templates/application_presenter.rb.tt
65
- - lib/generators/presenter/USAGE
66
- - lib/generators/presenter/presenter_generator.rb
67
- - lib/generators/presenter/templates/presenter.rb.tt
94
+ - lib/generators/rails/presenter/USAGE
95
+ - lib/generators/rails/presenter/presenter_generator.rb
96
+ - lib/generators/rails/presenter/templates/presenter.rb.tt
68
97
  - lib/generators/rspec/presenter/presenter_generator.rb
69
98
  - lib/generators/rspec/presenter/templates/presenter_spec.rb.tt
70
99
  - lib/generators/test_unit/presenter/presenter_generator.rb
@@ -75,7 +104,9 @@ files:
75
104
  - lib/magic/presenter/engine.rb
76
105
  - lib/magic/presenter/global_id.rb
77
106
  - lib/magic/presenter/helpers.rb
107
+ - lib/magic/presenter/test_case.rb
78
108
  - lib/magic/presenter/version.rb
109
+ - lib/rspec/rails/example/presenter_example_group.rb
79
110
  - lib/tasks/magic/presenter_tasks.rake
80
111
  homepage: https://github.com/Alexander-Senko/magic-presenter
81
112
  licenses:
@@ -83,7 +114,7 @@ licenses:
83
114
  metadata:
84
115
  homepage_uri: https://github.com/Alexander-Senko/magic-presenter
85
116
  source_code_uri: https://github.com/Alexander-Senko/magic-presenter
86
- changelog_uri: https://github.com/Alexander-Senko/magic-presenter/blob/v0.4.0/CHANGELOG.md
117
+ changelog_uri: https://github.com/Alexander-Senko/magic-presenter/blob/v1.1.0/CHANGELOG.md
87
118
  rdoc_options: []
88
119
  require_paths:
89
120
  - lib
@@ -98,7 +129,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
98
129
  - !ruby/object:Gem::Version
99
130
  version: '0'
100
131
  requirements: []
101
- rubygems_version: 3.6.0.dev
132
+ rubygems_version: 3.6.5
102
133
  specification_version: 4
103
134
  summary: Presentation layer for Rails models
104
135
  test_files: []
@@ -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