magic-presenter 0.1.0 → 0.3.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: c2d9f852b05ac3df9f02d0a6f43256155d36f0aa54c9b94887017c6695040000
4
- data.tar.gz: 07f21527902b18f58d4ffc88655b3e37317a331fa8db2f37addaa421478a523b
3
+ metadata.gz: 17e4d44c0c2d9a9e09e7e099a4067b9e6aafe27e1b389cf01808ed0a77d0b9ae
4
+ data.tar.gz: 7d492486c2f4edb2407af461794e1f0c7e53c7f6b93e65595a618369fb845250
5
5
  SHA512:
6
- metadata.gz: 3d230e25c5b5a5bfaef1e941df5388cdcc71315287fbf71177fa7ebe1ca13ff4fa16a2e6a07e358bbed35213053262e6900b390cdc9572f5e33ea5ae59161626
7
- data.tar.gz: 36e3545b509b62c1c68f22d156adec1ef46ddf34ff0efeb9875aedf13bac1856a6387006211ad9c93a9dc118aa55f98a6a0db09133172d8b6453ed6f81e968e1
6
+ metadata.gz: fb2609c3dbba1cec025bca22aaa196c718b32084b2954b8184f9b5ce896e7a9289c0b7eb0660469edaf3571d27d99026f6e588f58004e395cee07a75820e38d7
7
+ data.tar.gz: 1a87908713cb7dbcb029687dd1d4b9006432e8ba9703ef1dbe813088060c471ada70f8d7d1f5de4cdc9210ba602bd0457b1bf061f6fd3d9a25c5a420003198e0
data/README.md CHANGED
@@ -1,5 +1,8 @@
1
1
  # 🧙 Magic Presenter
2
2
 
3
+ ![GitHub Actions Workflow Status](
4
+ https://img.shields.io/github/actions/workflow/status/Alexander-Senko/magic-presenter/ci.yml
5
+ )
3
6
  ![Code Climate maintainability](
4
7
  https://img.shields.io/codeclimate/maintainability-percentage/Alexander-Senko/magic-presenter
5
8
  )
@@ -15,11 +18,15 @@ Based on [Magic Decorator](https://github.com/Alexander-Senko/magic-decorator),
15
18
 
16
19
  Install the gem and add to the application's Gemfile by executing:
17
20
 
18
- $ bundle add magic-presenter
21
+ $ bundle add magic-presenter
19
22
 
20
23
  If bundler is not being used to manage dependencies, install the gem by executing:
21
24
 
22
- $ gem install magic-presenter
25
+ $ gem install magic-presenter
26
+
27
+ After all the gems are `bundle`d run the installer in the project directory to generate the necessary files:
28
+
29
+ $ bin/rails generate magic:presenter:install
23
30
 
24
31
  ## Usage
25
32
 
@@ -51,7 +58,17 @@ When no presenter is found,
51
58
  - `#decorate!` raises `Magic::Lookup::Error`,
52
59
  - `#decorated` returns the original object.
53
60
 
54
- ## Magic
61
+ ### Generators
62
+
63
+ A generator can be used to generate a presenter:
64
+
65
+ $ bin/rails generate presenter Person
66
+
67
+ See the help for more info:
68
+
69
+ $ bin/rails generate presenter --help
70
+
71
+ ## 🧙 Magic
55
72
 
56
73
  It’s based on [Magic Decorator](
57
74
  https://github.com/Alexander-Senko/magic-decorator#magic
@@ -59,7 +76,7 @@ It’s based on [Magic Decorator](
59
76
 
60
77
  ### Presentable scope
61
78
 
62
- `Magic::Presentable` is mixed into `ActiveModel::Model` by default.
79
+ `Magic::Presentable` is mixed into `ActiveModel::API` by default.
63
80
  It means that any model, be it `ActiveRecord::Base`, `Mongoid::Document` or whatever else, is _magically presentable_.
64
81
 
65
82
  ### Presenter class inference
@@ -68,9 +85,30 @@ Presenters provide automatic class inference for any model based on its class na
68
85
  https://github.com/Alexander-Senko/magic-lookup
69
86
  ).
70
87
 
71
- For example, `MyNamespace::MyModel.new.decorate` looks for `MyNamespace::MyModelPresenter` first.
88
+ For example, `MyNamespace::MyModel.new.decorate` looks for `MyNamespace::MyPresenter` first.
72
89
  When missing, it further looks for decorators for its ancestor classes, up to `ObjectPresenter`.
73
90
 
91
+ #### Mapping rules
92
+
93
+ - `MyObject` → `MyObjectPresenter`
94
+ - `MyModel` → `MyPresenter`
95
+ - `MyRecord` → `MyPresenter`
96
+
97
+ > [!TIP]
98
+ > That’s why `ApplicationPresenter` presents `ApplicationRecord` alongside all its descendants automagically with no extra code.
99
+
100
+ When in doubt, one can use `Magic::Presenter.name_for`:
101
+
102
+ ```ruby
103
+ Magic::Presenter.name_for Person # => "PersonPresenter"
104
+ ```
105
+
106
+ ### In views
107
+
108
+ > [!IMPORTANT]
109
+ > Every object passed to views is decorated automagically.
110
+ > This involves both implicit instance variables and `locals` passed explicitly.
111
+
74
112
  ## Development
75
113
 
76
114
  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.
@@ -7,8 +7,16 @@ module Magic
7
7
  module Presentable
8
8
  include Decoratable
9
9
 
10
+ class << self
11
+ def classes
12
+ Magic.eager_load :models
13
+
14
+ super
15
+ end
16
+ end
17
+
10
18
  private
11
19
 
12
- def decorator = Presenter.for self.class
20
+ def decorator_base = Presenter
13
21
  end
14
22
  end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ ActiveSupport.on_load :action_view do
4
+ concerning :DecoratedAssignments, prepend: true do
5
+ def assign(assignments, ...)
6
+ assignments
7
+ .transform_values! &:decorated
8
+
9
+ super
10
+ end
11
+
12
+ def _run(method, template, locals, ...)
13
+ locals
14
+ .transform_values! &:decorated
15
+
16
+ super
17
+ end
18
+ end
19
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- ActiveSupport.on_load :active_model do
4
- include Magic::Presentable
3
+ Rails.application.config.to_prepare do
4
+ ActiveModel::API.include Magic::Presentable
5
5
  end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
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
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Magic
4
+ module Presenter
5
+ module Generator # :nodoc:
6
+ require 'generators/presenter/presenter_generator'
7
+
8
+ private
9
+
10
+ def file_name name = super()
11
+ name
12
+ .camelize
13
+ .then { Magic::Presenter.name_for _1 }
14
+ .underscore
15
+ end
16
+
17
+ def file_path path = super(), root: target_root
18
+ root / 'presenters' / path
19
+ end
20
+
21
+ def presenter_path(*) = file_path(*, root: PresenterGenerator.target_root)
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,10 @@
1
+ Description:
2
+ Generates application files used by Magic Presenter:
3
+ ApplicationPresenter: a base class for other presenters to
4
+ inherit from.
5
+
6
+ Example:
7
+ bin/rails generate magic:presenter:install
8
+
9
+ This will create:
10
+ ApplicationPresenter: app/presenters/application_presenter.rb
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Magic
4
+ module Presenter
5
+ class InstallGenerator < Rails::Generators::Base # :nodoc:
6
+ include Generator
7
+
8
+ source_root File.expand_path('templates', __dir__)
9
+
10
+ def create_files
11
+ template 'application_presenter.rb', "#{presenter_path namespaced_file_name 'ApplicationRecord'}.rb"
12
+ end
13
+
14
+ private
15
+
16
+ def namespaced_file_name(*)
17
+ File.join *(namespaced_path if namespaced?), file_name(*)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,6 @@
1
+ <% module_namespacing do -%>
2
+ class ApplicationPresenter < Magic::Presenter::Base
3
+ # Define presentation-specific methods here.
4
+ # They are shared by all the descendant presenters.
5
+ end
6
+ <% end -%>
@@ -0,0 +1,28 @@
1
+ Description:
2
+ Generates a new presenter for a model. Pass the model name, either
3
+ CamelCased or under_scored, as an argument.
4
+
5
+ This generator invokes your configured test framework, which
6
+ defaults to TestUnit. It supports both TestUnit and RSpec.
7
+
8
+ If a `--parent` option is given, it’s used as a superclass of the
9
+ created presenter.
10
+
11
+ Example:
12
+ bin/rails generate presenter account
13
+
14
+ This will create:
15
+ Presenter: app/presenters/account_presenter.rb
16
+ for TestUnit:
17
+ Test: test/presenters/account_presenter_test.rb
18
+ for RSpec:
19
+ Spec: spec/presenters/account_presenter_spec.rb
20
+
21
+ bin/rails generate presenter admin/account
22
+
23
+ This will create:
24
+ Presenter: app/presenters/admin/account_presenter.rb
25
+ for TestUnit:
26
+ Test: test/presenters/admin/account_presenter_test.rb
27
+ for RSpec:
28
+ Spec: spec/presenters/admin/account_presenter_spec.rb
@@ -0,0 +1,26 @@
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
@@ -0,0 +1,5 @@
1
+ <% module_namespacing do -%>
2
+ class <%= class_name %> < <%= parent_class_name %>
3
+ # Define presentation-specific methods here.
4
+ end
5
+ <% end -%>
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'generators/rspec'
4
+ require 'generators/magic/presenter/generator'
5
+
6
+ module Rspec # :nodoc:
7
+ module Generators # :nodoc:
8
+ class PresenterGenerator < Base # :nodoc:
9
+ include Magic::Presenter::Generator
10
+
11
+ source_root File.expand_path('templates', __dir__)
12
+
13
+ cattr_reader :target_root, default: Pathname('spec')
14
+
15
+ def create_test_file
16
+ template 'presenter_spec.rb', "#{file_path}_spec.rb"
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,7 @@
1
+ require 'rails_helper'
2
+
3
+ <% module_namespacing do -%>
4
+ RSpec.describe <%= class_name %> do
5
+ pending "add some examples to (or delete) #{__FILE__}"
6
+ end
7
+ <% end -%>
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators/test_unit'
4
+ require 'generators/magic/presenter/generator'
5
+
6
+ module TestUnit # :nodoc:
7
+ module Generators # :nodoc:
8
+ class PresenterGenerator < Base # :nodoc:
9
+ include Magic::Presenter::Generator
10
+
11
+ source_root File.expand_path('templates', __dir__)
12
+
13
+ check_class_collision suffix: 'PresenterTest'
14
+
15
+ cattr_reader :target_root, default: Pathname('test')
16
+
17
+ def create_test_file
18
+ template 'presenter_test.rb', "#{file_path}_test.rb"
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,9 @@
1
+ require "test_helper"
2
+
3
+ <% module_namespacing do -%>
4
+ class <%= class_name %>Test < ActiveSupport::TestCase
5
+ # test "the truth" do
6
+ # assert true
7
+ # end
8
+ end
9
+ <% end -%>
@@ -13,12 +13,37 @@ module Magic
13
13
  #
14
14
  # Presenters provide automatic class inference for any model based
15
15
  # on its class name powered by Magic Lookup.
16
- # For example, `MyModel.new` looks for `MyModelPresenter` first.
16
+ # For example, `MyModel.new` looks for `MyPresenter` first.
17
17
  # If not found, it looks for presenters of its ancestor classes,
18
18
  # up to `ObjectPresenter`.
19
19
  class Base < Decorator::Base
20
+ include GlobalID if defined? ::GlobalID
21
+
20
22
  class << self
21
- def name_for(object_class) = "#{object_class}Presenter"
23
+ def name_for object_class
24
+ object_class
25
+ .to_s
26
+ .delete_suffix('Model')
27
+ .delete_suffix('Record')
28
+ .then { "#{_1}Presenter" }
29
+ end
30
+
31
+ def model_class
32
+ Presentable.classes
33
+ .select { Presenter.for(_1) == self }
34
+ .sole
35
+ rescue Enumerable::SoleItemExpectedError => error
36
+ raise Lookup::Error, "#{error.message
37
+ .sub('items', 'model classes')
38
+ .sub('item', 'model class')
39
+ } for #{self}"
40
+ end
41
+
42
+ def descendants
43
+ Magic.eager_load :presenters
44
+
45
+ super
46
+ end
22
47
  end
23
48
  end
24
49
  end
@@ -4,6 +4,10 @@ module Magic
4
4
  module Presenter
5
5
  class Engine < ::Rails::Engine # :nodoc:
6
6
  isolate_namespace Magic::Presenter
7
+
8
+ config.generators do
9
+ _1.test_framework = :rspec
10
+ end
7
11
  end
8
12
  end
9
13
  end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Magic
4
+ module Presenter
5
+ # = Active Job integration
6
+ #
7
+ # Active Job [accepts Active Record objects](
8
+ # http://edgeguides.rubyonrails.org/active_job_basics.html#globalid
9
+ # ).
10
+ # An object passed to a background job must implement [Global ID](
11
+ # https://github.com/rails/globalid
12
+ # ).
13
+ #
14
+ # This module implements Global ID for decorated models by defining
15
+ # `.find` method that uses the one from a model class and decorates
16
+ # the result.
17
+ # This means one can pass decorated objects to background jobs and
18
+ # get them decorated when deserialized.
19
+ module GlobalID
20
+ extend ActiveSupport::Concern
21
+
22
+ included do
23
+ include ::GlobalID::Identification
24
+ end
25
+
26
+ class_methods do
27
+ def find(...)
28
+ model_class
29
+ .find(...)
30
+ .decorated
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Magic
4
4
  module Presenter
5
- VERSION = '0.1.0'
5
+ VERSION = '0.3.0'
6
6
  end
7
7
  end
@@ -5,16 +5,29 @@ require 'magic/decorator'
5
5
  require_relative 'presenter/version'
6
6
  require_relative 'presenter/engine'
7
7
 
8
- module Magic
9
- autoload :Presentable, 'magic/presentable'
10
-
8
+ module Magic # :nodoc:
11
9
  # Presentation layer for Rails models
12
10
  module Presenter
13
- autoload :Base, 'magic/presenter/base'
11
+ autoload :Base, 'magic/presenter/base'
12
+ autoload :GlobalID, 'magic/presenter/global_id'
13
+ autoload :Generator, 'generators/magic/presenter/generator'
14
14
 
15
15
  module_function
16
16
 
17
17
  def for(...) = Base.for(...)
18
18
  def name_for(...) = Base.name_for(...)
19
19
  end
20
+
21
+ module_function
22
+
23
+ def eager_load *scopes
24
+ return if Rails.application.config.eager_load
25
+
26
+ scopes
27
+ .map(&:to_s)
28
+ .map(&:pluralize)
29
+ .map { Rails.root / 'app' / _1 }
30
+ .select(&:exist?)
31
+ .each { Rails.autoloaders.main.eager_load_dir _1 }
32
+ end
20
33
  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.1.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alexander Senko
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2024-10-19 00:00:00.000000000 Z
10
+ date: 2024-10-27 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: rails
@@ -33,16 +33,16 @@ dependencies:
33
33
  name: magic-decorator
34
34
  requirement: !ruby/object:Gem::Requirement
35
35
  requirements:
36
- - - ">="
36
+ - - "~>"
37
37
  - !ruby/object:Gem::Version
38
- version: '0'
38
+ version: '0.3'
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: '0.3'
46
46
  description: Based on Magic Decorator, it’s meant to replace Draper.
47
47
  email:
48
48
  - Alexander.Senko@gmail.com
@@ -53,12 +53,26 @@ files:
53
53
  - MIT-LICENSE
54
54
  - README.md
55
55
  - Rakefile
56
+ - app/models/concerns/magic/presentable.rb
57
+ - config/initializers/action_view.rb
56
58
  - config/initializers/presentable.rb
57
- - lib/magic/presentable.rb
59
+ - config/initializers/rspec.rb
60
+ - lib/generators/magic/presenter/generator.rb
61
+ - lib/generators/magic/presenter/install/USAGE
62
+ - lib/generators/magic/presenter/install/install_generator.rb
63
+ - 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
67
+ - lib/generators/rspec/presenter/presenter_generator.rb
68
+ - lib/generators/rspec/presenter/templates/presenter_spec.rb.tt
69
+ - lib/generators/test_unit/presenter/presenter_generator.rb
70
+ - lib/generators/test_unit/presenter/templates/presenter_test.rb.tt
58
71
  - lib/magic/presenter.rb
59
72
  - lib/magic/presenter/authors.rb
60
73
  - lib/magic/presenter/base.rb
61
74
  - lib/magic/presenter/engine.rb
75
+ - lib/magic/presenter/global_id.rb
62
76
  - lib/magic/presenter/version.rb
63
77
  - lib/tasks/magic/presenter_tasks.rake
64
78
  homepage: https://github.com/Alexander-Senko/magic-presenter
@@ -67,7 +81,7 @@ licenses:
67
81
  metadata:
68
82
  homepage_uri: https://github.com/Alexander-Senko/magic-presenter
69
83
  source_code_uri: https://github.com/Alexander-Senko/magic-presenter
70
- changelog_uri: https://github.com/Alexander-Senko/magic-presenter/blob/v0.1.0/CHANGELOG.md
84
+ changelog_uri: https://github.com/Alexander-Senko/magic-presenter/blob/v0.3.0/CHANGELOG.md
71
85
  rdoc_options: []
72
86
  require_paths:
73
87
  - lib