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 +4 -4
- data/README.md +78 -7
- data/config/initializers/action_view.rb +2 -2
- data/config/initializers/generators.rb +20 -0
- data/config/initializers/rspec.rb +15 -5
- data/lib/generators/magic/presenter/generator.rb +2 -2
- data/lib/generators/rails/presenter/presenter_generator.rb +31 -0
- data/lib/generators/test_unit/presenter/templates/presenter_test.rb.tt +1 -4
- data/lib/magic/presenter/base.rb +10 -3
- data/lib/magic/presenter/engine.rb +2 -0
- data/lib/magic/presenter/test_case.rb +23 -0
- data/lib/magic/presenter/version.rb +1 -1
- data/lib/magic/presenter.rb +11 -3
- data/lib/rspec/rails/example/presenter_example_group.rb +19 -0
- metadata +40 -9
- data/lib/generators/presenter/presenter_generator.rb +0 -26
- /data/lib/generators/{presenter → rails/presenter}/USAGE +0 -0
- /data/lib/generators/{presenter → rails/presenter}/templates/presenter.rb.tt +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 85a48a4efc45256dda38fcb77db1d477118712dde86576c03978c5e653188f3a
|
4
|
+
data.tar.gz: 542fbc0836dcce1d87b845b5d84a405d45941c139e6ed41e4114dca002b29409
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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](
|
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](#
|
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
|
-
|
103
|
-
|
104
|
-
|
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
|
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
|
-
|
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
|
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
|
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
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
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
|
data/lib/magic/presenter/base.rb
CHANGED
@@ -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 {
|
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
|
|
@@ -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
|
data/lib/magic/presenter.rb
CHANGED
@@ -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 {
|
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:
|
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:
|
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
|
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
|
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/
|
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.
|
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
|
File without changes
|
File without changes
|