magic-decorator 0.1.0.1 → 0.3.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: 3b2496bbc27395a1c313aaa44e0c993037437a485595de2ef59f269835238409
4
- data.tar.gz: 6cace80885c21da63511ac98e4a231ed03ffc67ac9567af4d192ad411b0b7149
3
+ metadata.gz: 673fe3ecf73066b97e1076dfb9a41e7fb31732396e9c39a5f97561d1ec2e616f
4
+ data.tar.gz: 44c33204523d4022f8b2b1c1eb28dd51f5a3499d5001cdfae97fff59568d89d1
5
5
  SHA512:
6
- metadata.gz: 51300c4ae7274695352d0c4eab403f378646c3afb966eee38119000080a525b6637e838a57fce9897484f36de6917ebad630f94e25806399ea72e683789616f4
7
- data.tar.gz: 81c9ee0f416defadebe38efb6e944a643b54f4d08d5aadf04062093141f229bc0a1aa26fa5ff293f219873c7e6927aae7cac163d2c66ba520214d9159a77ccc5
6
+ metadata.gz: 309f3a70d8ce2ae726375e59e152a77d044b7ffe52d8f7064e4baefe964aecc90c4a835dbf3e5474a56e0c7de08e0cb0e975e3e65bc3872a2cc2a18f2a401885
7
+ data.tar.gz: 595d311692d8db65bb29e0adc44222a48abea98b7b869a62d6b823cb436801be1317cddfc9c4caacf9a6514ec8cc0a1182a1b26c005f9efe2c52fa0d48cca2fc
data/CHANGELOG.md CHANGED
@@ -1,10 +1,44 @@
1
+ ## [0.3.0] — 2024-10-27
2
+
3
+ ### Added
4
+
5
+ - Improved extendability: one may override `Magic::Decoratable#decorator_base` to be used for lookups.
6
+ - `Magic::Decoratable.classes` for all the decoratables.
7
+
8
+ ### Fixed
9
+
10
+ - Failures on double decoration attempts.
11
+
12
+
13
+ ## [0.2.0] — 2024-10-17
14
+
15
+ ### Changed
16
+
17
+ - For almost any method called on a decorated object, both its result and `yield`ed arguments get decorated.
18
+ Some methods aren’t meant to be decorated though:
19
+ - `deconstruct` & `deconstruct_keys` for _pattern matching_,
20
+ - _converting_ methods: those starting with `to_`,
21
+ - _system_ methods: those starting with `_`.
22
+
23
+ ### Added
24
+
25
+ - `Magic::Decorator::Base.undecorated` to exclude methods from being decorated automagically.
26
+
27
+ #### Default decorators
28
+
29
+ - `EnumerableDecorator` to decorate `Enumerable`s.
30
+ - enables _splat_ operator: `*decorated` ,
31
+ - enables _double-splat_ operator: `**decorated`,
32
+ - enumerating methods yield decorated items.
33
+
34
+
1
35
  ## [0.1.0] — 2024-10-13
2
36
 
3
37
  ### Added
4
38
 
5
39
  - `Magic::Decorator::Base` — a basic decorator class.
6
40
  - `Magic::Decoratable` to be included in decoratable classes.
7
- - `#decorate`,
8
- - `#decorate!`,
9
- - `#decorated`,
10
- - `#decorated?`.
41
+ - `#decorate`,
42
+ - `#decorate!`,
43
+ - `#decorated`,
44
+ - `#decorated?`.
data/README.md CHANGED
@@ -1,5 +1,8 @@
1
1
  # 🪄 Magic Decorator
2
2
 
3
+ ![GitHub Actions Workflow Status](
4
+ https://img.shields.io/github/actions/workflow/status/Alexander-Senko/magic-decorator/ci.yml
5
+ )
3
6
  ![Code Climate maintainability](
4
7
  https://img.shields.io/codeclimate/maintainability-percentage/Alexander-Senko/magic-decorator
5
8
  )
@@ -47,20 +50,83 @@ person.name # => "John Smith"
47
50
  This module adds three methods to decorate an object.
48
51
  Decorator class is being inferred automatically.
49
52
  When no decorator is found,
50
- * `#decorate` returns `nil`,
51
- * `#decorate!` raises `Magic::Lookup::Error`,
52
- * `#decorated` returns the original object.
53
+ - `#decorate` returns `nil`,
54
+ - `#decorate!` raises `Magic::Lookup::Error`,
55
+ - `#decorated` returns the original object.
53
56
 
54
57
  One can test for the object is actually decorated with `#decorated?`.
55
58
 
56
59
  ```ruby
57
- 'with no decorator for String'.decorated.decorated? # => false
58
- ['with a decorator for Array'].decorated.decorated? # => true
60
+ 'with no decorator for String'.decorated
61
+ .decorated? # => false
62
+ ['with a decorator for Array'].decorated
63
+ .decorated? # => true
64
+ ```
65
+
66
+ ### Extending decorator logic
67
+
68
+ When extending `Magic::Decoratable`, one may override `#decorator_base` to be used for lookup.
69
+
70
+ ```ruby
71
+ class Special::Decorator < Magic::Decorator::Base
72
+ def self.name_for object_class
73
+ "Special::#{object_class}Decorator"
74
+ end
75
+ end
76
+
77
+ module Special::Decoratable
78
+ include Magic::Decoratable
79
+
80
+ private
81
+
82
+ def decorator_base = Special::Decorator
83
+ end
84
+
85
+ class Special::Model
86
+ include Special::Decoratable
87
+ end
88
+
89
+ Special::Model.new.decorate # looks for Special::Decorator descendants
59
90
  ```
60
91
 
61
- #### Magic
92
+ ## 🪄 Magic
62
93
 
63
- `Decoratable` is mixed into `Object` by default. That means that effectively any object is `Decoratable`.
94
+ ### Decoratable scope
95
+
96
+ `Magic::Decoratable` is mixed into `Object` by default. It means that effectively any object is _magically decoratable_.
97
+
98
+ One can use `Magic::Decoratable.classes` to see all the decoratable classes.
99
+
100
+ ### Decoration expansion
101
+
102
+ For almost any method called on a decorated object, both its result and `yield`ed arguments get decorated.
103
+
104
+ ```ruby
105
+ 'with no decorator for String'.decorated.chars
106
+ .decorated? # => false
107
+ ['with a decorator for Array'].decorated.map(&:chars).first.grep(/\S/).group_by(&:upcase).transform_values(&:size).sort_by(&:last).reverse.first(5).map(&:first)
108
+ .decorated? # => true
109
+ ```
110
+
111
+ #### Undecorated methods
112
+
113
+ Some methods aren’t meant to be decorated though:
114
+
115
+ - `deconstruct` & `deconstruct_keys` for _pattern matching_,
116
+ - _converting_ methods: those starting with `to_`,
117
+ - _system_ methods: those starting with `_`.
118
+
119
+ #### `undecorated` modifier
120
+
121
+ `Magic::Decorator::Base.undecorated` can be used to exclude methods from being decorated automagically.
122
+
123
+ ```ruby
124
+ class MyDecorator < Magic::Decorator::Base
125
+ undecorated %i[to_s inspect]
126
+ undecorated :raw_method
127
+ undecorated :m1, :m2
128
+ end
129
+ ```
64
130
 
65
131
  ### Decorator class inference
66
132
 
@@ -72,6 +138,33 @@ powered by [Magic Lookup](
72
138
  For example, `MyNamespace::MyModel.new.decorate` looks for `MyNamespace::MyModelDecorator` first.
73
139
  When missing, it further looks for decorators for its ancestor classes, up to `ObjectDecorator`.
74
140
 
141
+ ### Default decorators
142
+
143
+ #### `EnumerableDecorator`
144
+
145
+ It automagically decorates all its decoratable items.
146
+
147
+ ```ruby
148
+ [1, [2], { 3 => 4 }, '5'].decorated
149
+ .map &:decorated? # => [false, true, true, false]
150
+
151
+ { 1 => 2, [3] => [4] }.decorated.keys
152
+ .map &:decorated? # => [false, true]
153
+ { 1 => 2, [3] => [4] }.decorated.values
154
+ .map &:decorated? # => [false, true]
155
+
156
+ { 1 => 2, [3] => [4] }.decorated[1]
157
+ .decorated? # => false
158
+ { 1 => 2, [3] => [4] }.decorated[[3]]
159
+ .decorated? # => true
160
+ ```
161
+
162
+ ##### Side effects for decorated collections
163
+
164
+ - enables _splat_ operator: `*decorated` ,
165
+ - enables _double-splat_ operator: `**decorated`,
166
+ - enumerating methods yield decorated items.
167
+
75
168
  ## Development
76
169
 
77
170
  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,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ class EnumerableDecorator < Magic::Decorator::Base
4
+ def to_a(...) = super.map &:decorated
5
+ def to_ary(...) = super.map &:decorated
6
+
7
+ def to_h(...) = super.to_h { |*h| h.map! &:decorated }
8
+ def to_hash(...) = super.to_h { |*h| h.map! &:decorated }
9
+ end
@@ -1,14 +1,28 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'active_support/concern'
4
+
3
5
  module Magic
4
6
  module Decoratable
7
+ extend ActiveSupport::Concern
8
+
9
+ class_methods do
10
+ def classes
11
+ ObjectSpace.each_object(Class)
12
+ .select { _1 < self }
13
+ end
14
+ end
15
+
16
+ extend ClassMethods
17
+
5
18
  def decorate = decorator&.new self
6
- def decorate! = decorate || raise(Lookup::Error.for self, Decorator)
19
+ def decorate! = decorate || raise(Lookup::Error.for self, decorator_base)
7
20
  def decorated = decorate || self
8
21
  def decorated? = false
9
22
 
10
23
  private
11
24
 
12
- def decorator = Decorator.for self.class
25
+ def decorator = decorator_base.for self.class
26
+ def decorator_base = Decorator
13
27
  end
14
28
  end
@@ -5,13 +5,56 @@ require 'delegate'
5
5
  module Magic
6
6
  module Decorator
7
7
  class Base < SimpleDelegator
8
- extend Lookup
8
+ extend Lookup
9
+ include Decoratable
9
10
 
10
11
  class << self
11
12
  def name_for(object_class) = "#{object_class}Decorator"
13
+
14
+ private
15
+
16
+ def undecorated method, *methods
17
+ return [ method, *methods ].map { undecorated _1 } if
18
+ methods.any?
19
+ return undecorated *method if
20
+ method.is_a? Array
21
+ raise TypeError, "#{method} is not a symbol nor a string" unless
22
+ method in Symbol | String
23
+
24
+ class_eval <<~RUBY, __FILE__, __LINE__ + 1
25
+ def #{method}(...) = __getobj__.#{method}(...)
26
+ RUBY
27
+ end
12
28
  end
13
29
 
14
30
  def decorated? = true
31
+
32
+ undecorated %i[
33
+ deconstruct
34
+ deconstruct_keys
35
+ ]
36
+
37
+ def method_missing(method, ...)
38
+ return super if method.start_with? 'to_' # converter
39
+ return super if method.start_with? '_' # system
40
+
41
+ if block_given?
42
+ super { |*args| yield *args.map(&:decorated) }
43
+ else
44
+ super
45
+ end.decorated
46
+ rescue NoMethodError => error
47
+ __raise__ error.class.new(
48
+ error.message.sub(self.class.name, __getobj__.class.name),
49
+ error.name,
50
+ error.args,
51
+ # error.private_call?, # FIXME: not implemented in TruffleRuby
52
+
53
+ receiver: __getobj__
54
+ ).tap {
55
+ _1.set_backtrace error.backtrace[2..] # FIXME: use `backtrace_locations` with Ruby 3.4+
56
+ }
57
+ end
15
58
  end
16
59
  end
17
60
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Magic
4
4
  module Decorator
5
- VERSION = '0.1.0.1'
5
+ VERSION = '0.3.0'
6
6
  end
7
7
  end
@@ -18,3 +18,5 @@ module Magic
18
18
  def name_for(...) = Base.name_for(...)
19
19
  end
20
20
  end
21
+
22
+ require 'enumerable_decorator'
@@ -0,0 +1,9 @@
1
+ class EnumerableDecorator[unchecked out Element]
2
+ include _Each[Element, void]
3
+ include Enumerable[Element]
4
+
5
+ def to_ary: () -> ::Array[Element]
6
+
7
+ def to_hash: () -> ::Hash[untyped, untyped]
8
+ | [T, U] () { (Element) -> [T, U] } -> ::Hash[T, U]
9
+ end
@@ -1,13 +1,16 @@
1
1
  module Magic
2
2
  module Decoratable
3
- def decorate: -> Decorator?
4
- def decorate!: -> Decorator
5
- def decorated: -> (Decorator | self)
3
+ def self.classes: () -> Array[Class]
6
4
 
7
- def decorated?: -> bool
5
+ def decorate: () -> Decorator?
6
+ def decorate!: () -> Decorator
7
+ def decorated: () -> (Decorator | self)
8
+
9
+ def decorated?: () -> bool
8
10
 
9
11
  private
10
12
 
11
- def decorator: -> Class?
13
+ def decorator: () -> Class?
14
+ def decorator_base: () -> Module
12
15
  end
13
16
  end
@@ -4,6 +4,12 @@ module Magic
4
4
  extend Lookup
5
5
 
6
6
  def decorated?: -> bool
7
+
8
+ private
9
+
10
+ def self.undecorated: (interned) -> Symbol
11
+ | (interned, interned, *interned) -> Array[Symbol]
12
+ | (Array[interned]) -> Array[Symbol]
7
13
  end
8
14
  end
9
15
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: magic-decorator
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0.1
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-13 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: magic-lookup
@@ -23,6 +23,20 @@ dependencies:
23
23
  - - ">="
24
24
  - !ruby/object:Gem::Version
25
25
  version: '0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: activesupport
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
26
40
  description: 'SimpleDelegator on steroids: automatic delegation, decorator class inference,
27
41
  etc.'
28
42
  email:
@@ -38,11 +52,13 @@ files:
38
52
  - LICENSE.txt
39
53
  - README.md
40
54
  - Rakefile
55
+ - lib/enumerable_decorator.rb
41
56
  - lib/magic/decoratable.rb
42
57
  - lib/magic/decorator.rb
43
58
  - lib/magic/decorator/authors.rb
44
59
  - lib/magic/decorator/base.rb
45
60
  - lib/magic/decorator/version.rb
61
+ - sig/enumerable_decorator.rbs
46
62
  - sig/gem.rbs
47
63
  - sig/magic/decoratable.rbs
48
64
  - sig/magic/decorator.rbs
@@ -53,7 +69,7 @@ licenses:
53
69
  metadata:
54
70
  homepage_uri: https://github.com/Alexander-Senko/magic-decorator
55
71
  source_code_uri: https://github.com/Alexander-Senko/magic-decorator
56
- changelog_uri: https://github.com/Alexander-Senko/magic-decorator/blob/v0.1.0.1/CHANGELOG.md
72
+ changelog_uri: https://github.com/Alexander-Senko/magic-decorator/blob/v0.3.0/CHANGELOG.md
57
73
  rdoc_options: []
58
74
  require_paths:
59
75
  - lib