magic-support 0.3.2 → 0.4.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: 3b45a52aa66c0c22ad94c811078bb9cfa23259b47bf100bf1801764ceb11a5fd
4
- data.tar.gz: adc4e53bf22e41cc8347e9cf7304c4659f054d622ad25c0312d7f17fed836200
3
+ metadata.gz: 300e982507adbfef7f0443c3a28e10eef71663ad657111414bcc378fbefa671e
4
+ data.tar.gz: 6d880303471f7fd11c0adb570bc243e3924cc84930fd06b449a7c85b6d820e77
5
5
  SHA512:
6
- metadata.gz: 02aaa8dced956eca69517aba746fd53dd4e2c77c69944a0504aaf66b13e59a80e0f48b1ca56d4a9b11c3e532375acaab5ecd6312b4a09699db4523765a102df3
7
- data.tar.gz: d49b593c146296b518730b6c8d74d2238ab890f0a68db9bc132c9c8623099dc7a47d9c7bce3d98d1dc076b4f5e0a1a79625e0cbedf7dfa0fef1af992fc1c2b10
6
+ metadata.gz: 3ab3d1dbcf81b9d22a4ea50e24cf0ce11cf5d23231711d7baf8ee5cc556963d544a41152a6273fcea8980814100f0b3c9c5ea5ef68f1accd9add6daaa1cf002b
7
+ data.tar.gz: a83ab7bd0a8c1397e35dee965efcb155c2788a1fb13c11a4f2c58eac67167e39968c6544406cfc7242785b68cc0474b36acef9f559385e2005bb40ef570fb7b0
data/CHANGELOG.md CHANGED
@@ -1,3 +1,27 @@
1
+ ## [0.4.0] — 2026-10-09
2
+
3
+ ### Changed
4
+
5
+ - Moved to Ruby 3.4+.
6
+
7
+ #### `Kernel`
8
+
9
+ - Replaced `#optional` with `#then_try`.
10
+
11
+ ### Added
12
+
13
+ #### `Kernel`
14
+
15
+ - Conditional `Kernel#then` via `if:`/`unless:` parameters.
16
+ When a condition isn’t met it returns self the same way `#then_try` does.
17
+ - Optional arguments for `Kernel#then_try`
18
+ to call `#try` under the hood.
19
+ Thus, one can replace, e.g., `.then_try { it.try :decorate }` with a shortcut: `.then_try :decorate`.
20
+ - `Kernel#then_try` without a block
21
+ to return an enumerator suppressing `NoMethodError`s.
22
+ Thus, one can call, e.g., `.then_try.detect &:odd?` for any `Numeric`, not just `Integer`.
23
+
24
+
1
25
  ## [0.3.2] — 2026-05-05
2
26
 
3
27
  Works with Ruby 4+.
data/README.md CHANGED
@@ -3,11 +3,15 @@
3
3
  ![GitHub Actions Workflow Status](
4
4
  https://img.shields.io/github/actions/workflow/status/Alexander-Senko/magic-support/ci.yml
5
5
  )
6
- ![Code Climate maintainability](
7
- https://img.shields.io/codeclimate/maintainability-percentage/Alexander-Senko/magic-support
6
+ [![Maintainability](
7
+ https://qlty.sh/gh/Alexander-Senko/projects/magic-support/maintainability.svg
8
+ )](
9
+ https://qlty.sh/gh/Alexander-Senko/projects/magic-support
8
10
  )
9
- ![Code Climate coverage](
10
- https://img.shields.io/codeclimate/coverage/Alexander-Senko/magic-support
11
+ [![Code Coverage](
12
+ https://qlty.sh/gh/Alexander-Senko/projects/magic-support/coverage.svg
13
+ )](
14
+ https://qlty.sh/gh/Alexander-Senko/projects/magic-support
11
15
  )
12
16
 
13
17
  Magic Support is a collection of utility classes and standard library extensions
@@ -71,28 +75,66 @@ require 'magic/core_ext'
71
75
 
72
76
  ### Extensions to all objects
73
77
 
74
- #### `#optional`
78
+ #### `#then`
79
+
80
+ Yields self to the block and returns the block’s result, just like the original `then`, if the condition permits it.
81
+ Returns self otherwise, just like `then_try`.
82
+
83
+ ```ruby
84
+ @people = Person
85
+ .then(if: new?) { it.where created_at: 1.hour.ago... }
86
+ .then(if: gdpr?, &:anonymize)
87
+ ```
88
+
89
+ Conditions may be callables, in which case they are called with self.
90
+
91
+ ```ruby
92
+ data
93
+ .then(if: :compressed?) { decompress it }
94
+ .then(unless: :parsed?) { parse it }
95
+ .then(if: -> { it.size > LIMIT }) { shrink it }
96
+ ```
97
+
98
+ With no condition keywords, it behaves like the standard `then`.
99
+
100
+ Defined in `core_ext/kernel/then`.
101
+
102
+ #### `#then_try`
75
103
 
76
104
  Yields self to the block and returns the result of the block if it’s truthy, and self otherwise.
77
105
 
78
106
  ```ruby
79
107
  rand(6) # returns 0–5
80
- .optional { it + 1 if one_based? } # returns 1–6 if 1-based, or
108
+ .then_try { it + 1 if one_based? } # returns 1–6 if 1-based, or
81
109
  # the original 0–5 otherwise
82
110
  ```
83
111
 
84
- It can be considered as a conditional `#then`.
85
-
86
- Good usage for `#optional` is value piping in method chains with conditional processing:
112
+ Good usage for `#then_try` is value piping in method chains with conditional processing:
87
113
 
88
114
  ```ruby
89
115
  @people = Person
90
- .optional { it.where created_at: (1.hour.ago...) if new? }
91
- .optional { anonymize it if gdpr? }
92
- .optional { try :decorate }
116
+ .then_try { it.where created_at: (1.hour.ago...) if new? }
117
+ .then_try { anonymize it if gdpr? }
118
+ .then_try(:decorate)
119
+ ```
120
+
121
+ When called without a block, the method returns an Enumerator, which can be used, for example, for `NoMethodError`-proof conditional circuit-breaking:
122
+
123
+ ```ruby
124
+ 101.then_try.detect &:odd? # => 101
125
+ 102.then_try.detect &:odd? # => nil
126
+ 1.3.then_try.detect &:odd? # => nil
127
+ ```
128
+
129
+ Traditional `#then` behaves like this:
130
+
131
+ ```ruby
132
+ 101.then.detect &:odd? # => 101
133
+ 102.then.detect &:odd? # => nil
134
+ 1.3.then.detect &:odd? # raises NoMethodError
93
135
  ```
94
136
 
95
- Defined in `core_ext/kernel/optional`.
137
+ Defined in `core_ext/kernel/then`.
96
138
 
97
139
  ## Gems
98
140
 
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'utils'
4
+
5
+ require 'active_support/core_ext/object/try'
6
+
7
+ Magic::CoreExt.kernel do
8
+ # Yields self to the block and returns the block’s result, just like
9
+ # the original +then+, if the condition permits it.
10
+ # Returns self otherwise, just like +then_try+.
11
+ #
12
+ # rand(6) # returns 0–5
13
+ # .then(&:succ, if: one_based?) # returns 1–6 if 1-based, or
14
+ # # the original 0–5 otherwise
15
+ #
16
+ # @people = Person
17
+ # .then(if: new?) { it.where created_at: 1.hour.ago... }
18
+ # .then(if: gdpr?, &:anonymize)
19
+ #
20
+ # Conditions may be callables, in which case they are called with self.
21
+ #
22
+ # data
23
+ # .then(if: :compressed?) { decompress it }
24
+ # .then(unless: :parsed?) { parse it }
25
+ # .then(if: -> { it.size > LIMIT }) { shrink it }
26
+ #
27
+ # With no condition keywords, it behaves like the standard +then+.
28
+ def then **conditions, &block # rubocop:disable Metrics
29
+ return super unless conditions.any?
30
+
31
+ return Enumerator.new do |yielder|
32
+ self.then **conditions do
33
+ yielder << it
34
+ end
35
+ end unless block
36
+
37
+ if (keywords = conditions.keys - %i[ if unless ]).any?
38
+ require 'active_support/core_ext/enumerable'
39
+
40
+ raise ArgumentError, "unknown keyword#{'s' if keywords.many?}: #{keywords.map(&:inspect) * ', '}"
41
+ end
42
+
43
+ conditions.transform_values! do |condition|
44
+ if condition.respond_to? :to_proc
45
+ condition.to_proc[self]
46
+ else
47
+ condition
48
+ end
49
+ end
50
+
51
+ if conditions.fetch(:if, true) and not conditions.fetch(:unless, false)
52
+ super &block
53
+ else
54
+ self
55
+ end
56
+ end
57
+
58
+ # Yields self to the block and returns the result of the block if it’s
59
+ # truthy, and self otherwise.
60
+ #
61
+ # rand(6) # returns 0–5
62
+ # .then_try { it + 1 if one_based? } # returns 1–6 if 1-based, or
63
+ # # the original 0–5 otherwise
64
+ #
65
+ # It can be considered as a conditional +then+.
66
+ #
67
+ # Good usage for +then_try+ is value piping in method chains with
68
+ # conditional processing:
69
+ #
70
+ # @people = Person
71
+ # .then_try { it.where created_at: (1.hour.ago...) if new? }
72
+ # .then_try { anonymize it if gdpr? }
73
+ # .then_try(:decorate)
74
+ #
75
+ # See the proposal: https://bugs.ruby-lang.org/issues/21435
76
+ #
77
+ # When called without a block, the method returns an Enumerator, which
78
+ # can be used, for example, for +NoMethodError+-proof conditional
79
+ # circuit-breaking:
80
+ #
81
+ # 101.then.detect &:odd? # => 101
82
+ # 102.then.detect &:odd? # => nil
83
+ # 1.3.then.detect &:odd? # raises NoMethodError
84
+ #
85
+ # 101.then_try.detect &:odd? # => 101
86
+ # 102.then_try.detect &:odd? # => nil
87
+ # 1.3.then_try.detect &:odd? # => nil
88
+ def then_try(*args, **, &)
89
+ tap do
90
+ result = if args.any?
91
+ try(*args, **, &)
92
+ else
93
+ # Enumerator mode
94
+ return Enumerator.new do |yielder|
95
+ yielder << self
96
+ rescue NoMethodError => error
97
+ raise unless
98
+ self == error.receiver
99
+ end unless block_given?
100
+
101
+ yield self
102
+ end or next # bypass
103
+
104
+ break result # `#then` mode
105
+ end
106
+ end
107
+ end
@@ -16,7 +16,7 @@ module Magic # :nodoc:
16
16
 
17
17
  def kernel(&)
18
18
  Module.new(&).tap do |kernel|
19
- Kernel::MODULES.each { _1.include kernel }
19
+ Kernel::MODULES.each { it.prepend kernel }
20
20
  end
21
21
  end
22
22
  end
data/lib/magic/loader.rb CHANGED
@@ -10,7 +10,7 @@ module Magic # :nodoc:
10
10
  Pathname(scope)
11
11
  .sub_ext('')
12
12
  .glob('*.rb')
13
- .each { require _1 }
13
+ .each { require it }
14
14
  end
15
15
  end
16
16
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Magic
4
4
  module Support
5
- VERSION = '0.3.2'
5
+ VERSION = '0.4.0'
6
6
  end
7
7
  end
data/lib/rspec/method.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'active_support/concern'
4
- require 'magic/core_ext/kernel/optional'
4
+ require 'magic/core_ext/kernel/then'
5
5
 
6
6
  module RSpec # :nodoc:
7
7
  module Method
@@ -11,7 +11,7 @@ module RSpec # :nodoc:
11
11
  }
12
12
  .map { |name, pattern| "(?<#{name}>#{pattern})" }
13
13
  .join
14
- .then { /(?:^|\s)#{_1}(?:$|\s)?/ } # may be surrounded by some text
14
+ .then { /(?:^|\s)#{it}(?:$|\s)?/ } # may be surrounded by some text
15
15
 
16
16
  # @api public
17
17
  # Container module for method specs.
@@ -43,6 +43,7 @@ module RSpec # :nodoc:
43
43
  description.match(REFERENCE) in { scope:, name: } or
44
44
  raise ArgumentError, "'#{description}' doesn't look like a method reference"
45
45
 
46
+ adjust_description if description.start_with? scope
46
47
  backup_subject
47
48
 
48
49
  let :receiver do
@@ -62,7 +63,7 @@ module RSpec # :nodoc:
62
63
  def backup_subject
63
64
  subject_method = instance_method :subject
64
65
 
65
- while subject_method.owner.then { not _1.respond_to? :metadata or _1.metadata[:method] }
66
+ while subject_method.owner.then { not it.respond_to? :metadata or it.metadata[:method] }
66
67
  subject_method = subject_method.super_method or break
67
68
  end
68
69
 
@@ -72,6 +73,11 @@ module RSpec # :nodoc:
72
73
  let(:__subject__) { method(:subject).super_method[] }
73
74
  end
74
75
  end
76
+
77
+ def adjust_description
78
+ metadata[:full_description]
79
+ .sub!(/#{described_class}\s+#{description}/, "#{described_class}#{description}")
80
+ end
75
81
  end
76
82
 
77
83
  def subject
@@ -95,7 +101,7 @@ module RSpec # :nodoc:
95
101
  shared_context :delegated do |to:, with: nil|
96
102
  before do
97
103
  allow(to).to receive(method_name)
98
- .optional { _1.with *with if with }
104
+ .then(if: with) { it.with *with }
99
105
  .and_call_original
100
106
  end
101
107
 
@@ -115,6 +121,6 @@ module RSpec # :nodoc:
115
121
  end
116
122
 
117
123
  configure do
118
- _1.backtrace_exclusion_patterns << %r'/lib/rspec/method'
124
+ it.backtrace_exclusion_patterns << %r'/lib/rspec/method'
119
125
  end
120
126
  end
@@ -2,6 +2,13 @@ module Magic
2
2
  module CoreExt
3
3
  module Kernel
4
4
  MODULES: Array[Module]
5
+
6
+ def then_try: [U] { (self) -> U } -> (U | self)
7
+ | (interned method_name, *untyped, **untyped) ?{ (*untyped, **untyped) -> untyped } -> (untyped | self)
8
+ | -> Enumerator[self, untyped]
9
+
10
+ def then: [U] (?if: bool, ?unless: bool) { (self) -> U } -> (U | self)
11
+ | (?if: bool, ?unless: bool) -> Enumerator[self, untyped]
5
12
  end
6
13
  end
7
14
  end
metadata CHANGED
@@ -1,82 +1,84 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: magic-support
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.2
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
- - Alexander Senko
7
+ - Alexander Senko
8
8
  bindir: bin
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
- - !ruby/object:Gem::Dependency
13
- name: activesupport
14
- requirement: !ruby/object:Gem::Requirement
15
- requirements:
16
- - - ">="
17
- - !ruby/object:Gem::Version
18
- version: '0'
19
- type: :runtime
20
- prerelease: false
21
- version_requirements: !ruby/object:Gem::Requirement
22
- requirements:
23
- - - ">="
24
- - !ruby/object:Gem::Version
25
- version: '0'
12
+ - !ruby/object:Gem::Dependency
13
+ name: activesupport
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ -
17
+ - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: "0"
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ -
25
+ - ">="
26
+ - !ruby/object:Gem::Version
27
+ version: "0"
26
28
  description: Utility classes and Ruby extensions beyond Active Support.
27
29
  email:
28
- - Alexander.Senko@gmail.com
30
+ - Alexander.Senko@gmail.com
29
31
  executables: []
30
32
  extensions: []
31
33
  extra_rdoc_files: []
32
34
  files:
33
- - ".rspec"
34
- - ".rubocop.yml"
35
- - CHANGELOG.md
36
- - CODE_OF_CONDUCT.md
37
- - LICENSE.txt
38
- - README.md
39
- - Rakefile
40
- - lib/magic/core_ext.rb
41
- - lib/magic/core_ext/kernel.rb
42
- - lib/magic/core_ext/kernel/optional.rb
43
- - lib/magic/core_ext/kernel/utils.rb
44
- - lib/magic/loader.rb
45
- - lib/magic/support.rb
46
- - lib/magic/support/authors.rb
47
- - lib/magic/support/version.rb
48
- - lib/rspec/method.rb
49
- - lib/rspec/method/its.rb
50
- - lib/rubygems/author.rb
51
- - sig/lib/magic/support/author.rbs
52
- - sig/magic/core_ext.rbs
53
- - sig/magic/core_ext/kernel.rbs
54
- - sig/magic/loader.rbs
55
- - sig/magic/support.rbs
56
- - sig/rspec/method.rbs
57
- - sig/rspec/method/its.rbs
58
- - sig/rubygems/author.rbs
59
- homepage: https://github.com/Alexander-Senko/magic-support
35
+ - CHANGELOG.md
36
+ - CODE_OF_CONDUCT.md
37
+ - LICENSE.txt
38
+ - README.md
39
+ - Rakefile
40
+ - lib/magic/core_ext.rb
41
+ - lib/magic/core_ext/kernel.rb
42
+ - lib/magic/core_ext/kernel/then.rb
43
+ - lib/magic/core_ext/kernel/utils.rb
44
+ - lib/magic/loader.rb
45
+ - lib/magic/support.rb
46
+ - lib/magic/support/authors.rb
47
+ - lib/magic/support/version.rb
48
+ - lib/rspec/method.rb
49
+ - lib/rspec/method/its.rb
50
+ - lib/rubygems/author.rb
51
+ - sig/lib/magic/support/author.rbs
52
+ - sig/magic/core_ext.rbs
53
+ - sig/magic/core_ext/kernel.rbs
54
+ - sig/magic/loader.rbs
55
+ - sig/magic/support.rbs
56
+ - sig/rspec/method.rbs
57
+ - sig/rspec/method/its.rbs
58
+ - sig/rubygems/author.rbs
59
+ homepage: "https://github.com/Alexander-Senko/magic-support"
60
60
  licenses:
61
- - MIT
61
+ - MIT
62
62
  metadata:
63
- source_code_uri: https://github.com/Alexander-Senko/magic-support
64
- changelog_uri: https://github.com/Alexander-Senko/magic-support/blob/v0.3.2/CHANGELOG.md
63
+ source_code_uri: "https://github.com/Alexander-Senko/magic-support"
64
+ changelog_uri: "https://github.com/Alexander-Senko/magic-support/blob/v0.4.0/CHANGELOG.md"
65
65
  rdoc_options: []
66
66
  require_paths:
67
- - lib
67
+ - lib
68
68
  required_ruby_version: !ruby/object:Gem::Requirement
69
69
  requirements:
70
- - - ">="
71
- - !ruby/object:Gem::Version
72
- version: '3.2'
70
+ -
71
+ - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: "3.4"
73
74
  required_rubygems_version: !ruby/object:Gem::Requirement
74
75
  requirements:
75
- - - ">="
76
- - !ruby/object:Gem::Version
77
- version: '0'
76
+ -
77
+ - ">="
78
+ - !ruby/object:Gem::Version
79
+ version: "0"
78
80
  requirements: []
79
- rubygems_version: 3.7.0.dev
81
+ rubygems_version: 4.1.0.dev
80
82
  specification_version: 4
81
83
  summary: Active Support extended
82
84
  test_files: []
data/.rspec DELETED
@@ -1,3 +0,0 @@
1
- --format documentation
2
- --color
3
- --require spec_helper
data/.rubocop.yml DELETED
@@ -1,7 +0,0 @@
1
- inherit_from:
2
- - https://github.com/Alexander-Senko/Alexander-Senko/raw/refs/heads/main/.rubocop.yml
3
- - https://github.com/Alexander-Senko/Alexander-Senko/raw/refs/heads/main/.rubocop-rspec.yml
4
-
5
- plugins:
6
- - rubocop-rspec
7
- - rubocop-rake
@@ -1,31 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'utils'
4
-
5
- Magic::CoreExt.kernel do
6
- # Yields self to the block and returns the result of the block if it’s
7
- # truthy, and self otherwise.
8
- #
9
- # rand(6) # returns 0–5
10
- # .optional { it + 1 if one_based? } # returns 1–6 if 1-based, or
11
- # # the original 0–5 otherwise
12
- #
13
- # It can be considered as a conditional +then+.
14
- #
15
- # Good usage for +optional+ is value piping in method chains with
16
- # conditional processing:
17
- #
18
- # @people = Person
19
- # .optional { it.where created_at: (1.hour.ago...) if new? }
20
- # .optional { anonymize it if gdpr? }
21
- # .optional { try :decorate }
22
- #
23
- def optional
24
- tap do
25
- result = yield(self) or
26
- next
27
-
28
- break result
29
- end
30
- end
31
- end