feature_envy 0.1.0 → 0.2.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: ebacd4b2d2a8d5292de874248a212a72b02a7bc13b5279b08f020a88c8973e71
4
- data.tar.gz: bca18e8eab4ef3d67efa38b65a09f4ede7291873a95a266748bbfd79e2b9ab45
3
+ metadata.gz: d7675c689bf69e95854627b7dc5e28affa5a2fed1bcb46f8255a80217c6abdea
4
+ data.tar.gz: 17a050ab57c4af67a4c967756b4704220e8511940498cb5f66c9989b6ecb2225
5
5
  SHA512:
6
- metadata.gz: 534c102e65795b30fd4cdd42ef10b34b062776642f145ebddffd3865fc39e81657c870e645e782362f39c45b1d6afb2e565caefb2932da69fcd84d71121a5548
7
- data.tar.gz: 0be465e644e71f4b51f14acb6e6e496c43618e5fbe8e13cc2a288291789074bcbc5d297589b4cb4b3d36eff742eb5a5351b3db81298a7637e22f348e78afb53b
6
+ metadata.gz: 073e92360b8fbcaa1b6ce67a68f37adfdc4282c69bc15be4cc1e4119a99671958c175597309169a9e122008371a70814dd3e0836250185bad95374a241b6dbd5
7
+ data.tar.gz: 2f3e21f91d1be99f5e00251568c87cb62131e8666af9c2a2d9685d42832d20cefb36a1f8ba87705c8b4f0a4fc3ddad10177c6caf473dcd6001156e2113f07804
data/README.md CHANGED
@@ -5,6 +5,12 @@ Feature Envy enhances Ruby with features found in other programming languages.
5
5
  **WARNING**: This gem is still in development and a stable release hasn't been
6
6
  made yet. Bug reports and contributions are welcome!
7
7
 
8
+ Supported features:
9
+
10
+ - Final classes
11
+ - Thread-safe lazy accessors
12
+ - Object literals
13
+
8
14
  ## Installation
9
15
 
10
16
  You can install the gem by running `gem install feature_envy` or adding it to
@@ -3,21 +3,35 @@
3
3
  module FeatureEnvy
4
4
  # Final classes.
5
5
  #
6
+ # ### Definition
7
+ #
6
8
  # A final class is a class that cannot be inherited from. In other words, a
7
- # final class enforces the invariant that it has no subclasses. Existence of
8
- # subclasses if checked **at the moment a class is defined final** (to catch
9
- # cases where a class is reopened and made final after subclasses were
10
- # defined) and when.
9
+ # final class enforces the invariant that it has no subclasses.
10
+ #
11
+ # ### Applications
12
+ #
13
+ # Preventing subclassing of classes that weren't specifically designed for
14
+ # handling it.
11
15
  #
12
- # The module can be used in two ways:
16
+ # ### Usage
13
17
  #
14
- # 1. Using it as a refinement and calling +.final!+ in bodies of classes that
18
+ # 1. Enable the feature in a specific class via `extend Feature::FinalClass`.
19
+ # The class has been marked final and there's nothing else to do.
20
+ # Alternatively, ...
21
+ # 2. Enable the feature in a specific **scope** using a refinement via
22
+ # `using FeatureEnvy::FinalClass` and call `final!` in all classes that
15
23
  # should be marked final.
16
- # 2. Extending the class with {FeatureEnvy::FinalClass}.
17
24
  #
18
- # See the example below for details.
25
+ # ### Discussion
19
26
  #
20
- # @example Final classes with refinements
27
+ # A class in Ruby can be made final by raising an error in its `inherited`
28
+ # hook. This is what this module does. However, this is **not** enough to
29
+ # guarantee that no subclasses will be created. Due to Ruby's dynamic nature
30
+ # it'd be possible to define a class, subclass, and then reopen the class and
31
+ # mark it final. This edge **is** taken care of and would result in an
32
+ # exception.
33
+ #
34
+ # @example
21
35
  # module Models
22
36
  # # Use the refinement within the module, so that all classes defined
23
37
  # # within support the final! method.
@@ -28,14 +42,6 @@ module FeatureEnvy
28
42
  # final!
29
43
  # end
30
44
  # end
31
- #
32
- # @example Final classes without refinements
33
- # module Models
34
- # class User < Base
35
- # # Mark the User class final.
36
- # extend FeatureEnvy::FinalClass
37
- # end
38
- # end
39
45
  module FinalClass
40
46
  # An error representing a final class invariant violation.
41
47
  class Error < FeatureEnvy::Error
@@ -72,7 +78,7 @@ module FeatureEnvy
72
78
  subclasses = Internal.subclasses final_class
73
79
  return if subclasses.empty?
74
80
 
75
- raise Error.new(final_class: final_class, subclasses: subclasses)
81
+ raise Error.new(final_class:, subclasses:)
76
82
  end
77
83
 
78
84
  # Determines whether a given class is marked final.
@@ -0,0 +1,236 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FeatureEnvy
4
+ # Lazy accessors.
5
+ #
6
+ # ### Definition
7
+ #
8
+ # A lazy **attribute** is an attribute whose value is determined on first
9
+ # access. The same value is used on all subsequent access without running the
10
+ # code to determine its value again.
11
+ #
12
+ # Lazy attributes are impossible in Ruby (see the discussion below), but
13
+ # lazy **accessors** are, and are provided by this module.
14
+ #
15
+ # ### Applications
16
+ #
17
+ # Deferring expensive computations until needed and ensuring they're performed
18
+ # at most once.
19
+ #
20
+ # ### Usage
21
+ #
22
+ # 1. Enable the feature in a specific class via `extend FeatureEnvy::LazyAccessor` or ...
23
+ # 2. Enable the feature in a specific **scope** (given module and all modules
24
+ # and classes contained within) using a refinement via `using FeatureEnvy::LazyAccessor`.
25
+ # 3. Define one or more lazy attributes via `lazy(:name) { definition }`.
26
+ # 4. Do **NOT** read or write to the underlying attribute, e.g. `@name`;
27
+ # always use the accessor method.
28
+ # 5. Lazy accessors are **thread-safe**: the definition block will be called
29
+ # at most once; if two threads call the accessor for the first time one
30
+ # will win the race to run the block and the other one will wait and reuse
31
+ # the value produced by the first.
32
+ # 6. It's impossible to reopen a class and add new lazy accessors after **the
33
+ # any class using lazy accessors has been instantiated**. Doing so would
34
+ # either make the code thread-unsafe or require additional thread-safety
35
+ # measures, potentially reducing performance.
36
+ #
37
+ # ### Discussion
38
+ #
39
+ # Ruby attributes start with `@` and assume the default value of `nil` if not
40
+ # assigned explicitly. Real lazy attributes are therefore impossible to
41
+ # implement in Ruby. Fortunately **accessors** (i.e. methods used to obtain
42
+ # attribute values) are conceptually close to attributes and can be made lazy.
43
+ #
44
+ # A naive approach found in many Ruby code bases looks like this:
45
+ #
46
+ # ```ruby
47
+ # def highest_score_user
48
+ # @highest_score_user ||= find_highest_score_user
49
+ # end
50
+ # ```
51
+ #
52
+ # It's simple but suffers from a serious flaw: if `nil` is assigned to the
53
+ # attribute then subsequent access will result in another attempt to determine
54
+ # the attribute's value.
55
+ #
56
+ # The proper approach is much more verbose:
57
+ #
58
+ # ```ruby
59
+ # def highest_score_user
60
+ # # If the underlying attribute is defined then return it no matter its value.
61
+ # return @highest_score_user if defined?(@highest_score_user)
62
+ #
63
+ # @highest_score_user = find_highest_score_user
64
+ # end
65
+ # ```
66
+ #
67
+ # ### Implementation Notes
68
+ #
69
+ # 1. Defining a lazy accessor defines a method with that name. The
70
+ # corresponding attribute is **not** set before the accessor is called for
71
+ # the first time.
72
+ # 2. The first time a lazy accessor is added to a class a special module
73
+ # is included into it. It provides an `initialize` method that sets
74
+ # `@lazy_attributes_mutexes` - a hash of mutexes protecting each lazy
75
+ # accessor.
76
+ #
77
+ # @example
78
+ # class User
79
+ # # Enable the feature via refinements.
80
+ # using FeatureEnvy::LazyAccessor
81
+ #
82
+ # # Lazy accessors can return nil and have it cached and reused in
83
+ # # subsequent calls.
84
+ # lazy(:full_name) do
85
+ # "#{first_name} #{last_name}" if first_name && last_name
86
+ # end
87
+ #
88
+ # # Lazy accessors are regular methods, that follow a specific structure,
89
+ # # so they can call other methods, including other lazy accessors.
90
+ # lazy(:letter_ending) do
91
+ # if full_name
92
+ # "Sincerely,\n#{full_name}"
93
+ # else
94
+ # "Sincerely"
95
+ # end
96
+ # end
97
+ # end
98
+ module LazyAccessor
99
+ # A class representing an error related to lazy-accessors.
100
+ class Error < FeatureEnvy::Error; end
101
+
102
+ refine Class do
103
+ def lazy name, &definition
104
+ LazyAccessor.define self, name, &definition
105
+ end
106
+ end
107
+
108
+ # Defines a lazy accessor.
109
+ #
110
+ # The `definition` block will be called once when the accessor is used for
111
+ # the first time. Its value is returned and cached for subsequent accessor
112
+ # use.
113
+ #
114
+ # @param name [String|Symbol] accessor name.
115
+ # @return [Array<Symbol>] the array containing the accessor name as a
116
+ # symbol; this is motivated by the built-in behavior of `attr_reader` and
117
+ # other built-in accessor definition methods.
118
+ # @yieldreturn the value to store in the underlying attribute and return on
119
+ # subsequent accessor use.
120
+ def lazy name, &definition
121
+ LazyAccessor.define self, name, &definition
122
+ end
123
+
124
+ # A class for creating mutexes for classes that make use of lazy accessors.
125
+ #
126
+ # The class keeps a hash that maps modules and classes to arrays of lazy
127
+ # accessor names defined therein.
128
+ #
129
+ # @private
130
+ class MutexFactory
131
+ def initialize
132
+ @mutexes_by_class = Hash.new { |hash, klass| hash[klass] = [] }
133
+ end
134
+
135
+ # Register a new lazy attribute.
136
+ #
137
+ # @return [Symbol] The name of the mutex corresponding to the specified
138
+ # lazy accessor.
139
+ #
140
+ # @private
141
+ def register klass, lazy_accessor_name
142
+ ObjectSpace.each_object(klass) do # rubocop:disable Lint/UnreachableLoop
143
+ raise Error.new(<<~ERROR)
144
+ An instance of #{klass.name} has been already created, so it's no longer
145
+ possible to define a new lazy accessor, due to thread-safety reasons.
146
+ ERROR
147
+ end
148
+
149
+ mutex_name = :"@#{lazy_accessor_name}_mutex"
150
+ @mutexes_by_class[klass] << mutex_name
151
+ mutex_name
152
+ end
153
+
154
+ # Create mutexes for lazy accessor supported on a given instance.
155
+ #
156
+ # @private
157
+ def initialize_mutexes_for instance
158
+ current_class = instance.class
159
+ while current_class
160
+ @mutexes_by_class[current_class].each do |mutex_name|
161
+ instance.instance_variable_set mutex_name, Thread::Mutex.new
162
+ end
163
+ current_class = current_class.superclass
164
+ end
165
+
166
+ instance.class.included_modules.each do |mod|
167
+ @mutexes_by_class[mod].each do |mutex_name|
168
+ instance.instance_variable_set mutex_name, Thread::Mutex.new
169
+ end
170
+ end
171
+ end
172
+ end
173
+ private_constant :MutexFactory
174
+
175
+ @mutex_factory = MutexFactory.new
176
+
177
+ class << self
178
+ # A mutex factory used by the lazy accessors feature.
179
+ #
180
+ # @private
181
+ attr_reader :mutex_factory
182
+
183
+ # Defines a lazy accessor.
184
+ #
185
+ # Required to share the code between the extension and refinement.
186
+ #
187
+ # @private
188
+ def define klass, name, &definition
189
+ name = name.to_sym
190
+ variable_name = :"@#{name}"
191
+ mutex_name = LazyAccessor.mutex_factory.register klass, name
192
+
193
+ klass.class_eval do
194
+ # Include the lazy accessor initializer to ensure state related to
195
+ # lazy accessors is initialized properly. There's no need to include
196
+ # this module more than once.
197
+ #
198
+ # Question: is the inclusion check required? Brief testing indicates
199
+ # it's not.
200
+ if !include? Initialize
201
+ include Initialize
202
+ end
203
+
204
+ [
205
+ define_method(name) do
206
+ mutex = instance_variable_get(mutex_name)
207
+ if mutex # rubocop:disable Style/SafeNavigation
208
+ mutex.synchronize do
209
+ if instance_variable_defined?(mutex_name)
210
+ instance_variable_set variable_name, instance_eval(&definition)
211
+ remove_instance_variable mutex_name
212
+ end
213
+ end
214
+ end
215
+
216
+ instance_variable_get variable_name
217
+ end
218
+ ]
219
+ end
220
+ end
221
+ end
222
+
223
+ # A module included in all classes that use lazy accessors responsible for
224
+ # initializing a hash of mutexes that guarantee thread-safety on first call.
225
+ #
226
+ # @private
227
+ module Initialize
228
+ def initialize ...
229
+ super
230
+
231
+ LazyAccessor.mutex_factory.initialize_mutexes_for self
232
+ end
233
+ end
234
+ private_constant :Initialize
235
+ end
236
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FeatureEnvy
4
+ # Object literals.
5
+ #
6
+ # ### Definition
7
+ #
8
+ # An expression that results in creating of an object with a predefined set of
9
+ # attributes and methods, without having to define and instantiated a class.
10
+ #
11
+ # ### Applications
12
+ #
13
+ # Defining singleton objects, both state and methods, without having to define
14
+ # their class explicitly.
15
+ #
16
+ # ### Usage
17
+ #
18
+ # 1. Enable the feature in a specific class via `include FeatureEnvy::ObjectLiteral`
19
+ # or ...
20
+ # 2. Enable the feature in a specific scope via `using FeatureEnvy::ObjectLiteral`.
21
+ # 3. Create objects by calling `object { ... }`.
22
+ #
23
+ # ### Discussion
24
+ #
25
+ # Ruby does not offer literals for defining arbitrary objects. Fortunately,
26
+ # that gap is easy to fill with a helper method. The snippet below is
27
+ # literally how Feature Envy implements object literals:
28
+ #
29
+ # ```ruby
30
+ # def object &definition
31
+ # object = Object.new
32
+ # object.instance_eval &definition
33
+ # object
34
+ # end
35
+ # ```
36
+ #
37
+ # All attributes set and methods defined inside the block will be set on
38
+ # `object`.
39
+ #
40
+ # @example
41
+ # # Enable the feature in the current scope.
42
+ # using FeatureEnvy::ObjectLiteral
43
+ #
44
+ # # Assuming `database` and `router` are already defined.
45
+ # app = object do
46
+ # @database = database
47
+ # @router = router
48
+ #
49
+ # def start
50
+ # @database.connect
51
+ # @router.activate
52
+ # end
53
+ # end
54
+ #
55
+ # app.start
56
+ module ObjectLiteral
57
+ refine Kernel do
58
+ def object ...
59
+ ObjectLiteral.object(...)
60
+ end
61
+ end
62
+
63
+ # Defines an object literal.
64
+ #
65
+ # @yield The block is evaluated in the context of a newly created object.
66
+ # Instance attributes and methods can be defined within the block and will
67
+ # end up being set on the resulting object.
68
+ # @return [Object] The object defined by the block passed to the call.
69
+ def object ...
70
+ ObjectLiteral.object(...)
71
+ end
72
+
73
+ # @private
74
+ def self.object &definition
75
+ result = Object.new
76
+ result.instance_eval &definition
77
+ result
78
+ end
79
+ end
80
+ end
@@ -2,5 +2,5 @@
2
2
 
3
3
  module FeatureEnvy
4
4
  # The current version number.
5
- VERSION = "0.1.0"
5
+ VERSION = "0.2.0"
6
6
  end
data/lib/feature_envy.rb CHANGED
@@ -11,6 +11,8 @@ module FeatureEnvy
11
11
  # A base class for all errors raised by Feature Envy.
12
12
  class Error < StandardError; end
13
13
 
14
- autoload :Internal, "feature_envy/internal"
15
- autoload :FinalClass, "feature_envy/final_class"
14
+ autoload :FinalClass, "feature_envy/final_class"
15
+ autoload :Internal, "feature_envy/internal"
16
+ autoload :LazyAccessor, "feature_envy/lazy_accessor"
17
+ autoload :ObjectLiteral, "feature_envy/object_literal"
16
18
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: feature_envy
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Greg Navis
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-03-24 00:00:00.000000000 Z
11
+ date: 2023-04-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -44,28 +44,28 @@ dependencies:
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: 1.21.0
47
+ version: 1.49.0
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: 1.21.0
54
+ version: 1.49.0
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: rubocop-minitest
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
59
  - - "~>"
60
60
  - !ruby/object:Gem::Version
61
- version: 0.26.1
61
+ version: 0.29.0
62
62
  type: :development
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
- version: 0.26.1
68
+ version: 0.29.0
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: rubocop-rake
71
71
  requirement: !ruby/object:Gem::Requirement
@@ -104,17 +104,17 @@ files:
104
104
  - lib/feature_envy.rb
105
105
  - lib/feature_envy/final_class.rb
106
106
  - lib/feature_envy/internal.rb
107
+ - lib/feature_envy/lazy_accessor.rb
108
+ - lib/feature_envy/object_literal.rb
107
109
  - lib/feature_envy/version.rb
108
- - test/final_class_test.rb
109
- - test/internal_test.rb
110
- - test/support/assertions.rb
111
- - test/test_helper.rb
112
110
  homepage: https://github.com/gregnavis/feature_envy
113
111
  licenses:
114
112
  - MIT
115
113
  metadata:
116
114
  homepage_uri: https://github.com/gregnavis/feature_envy
117
115
  source_code_uri: https://github.com/gregnavis/feature_envy
116
+ bug_tracker_uri: https://github.com/gregnavis/feature_envy/issues
117
+ documentation_uri: https://rubydoc.info/gems/feature_envy
118
118
  rubygems_mfa_required: 'true'
119
119
  post_install_message:
120
120
  rdoc_options: []
@@ -124,7 +124,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
124
124
  requirements:
125
125
  - - ">="
126
126
  - !ruby/object:Gem::Version
127
- version: 2.5.0
127
+ version: 3.1.0
128
128
  required_rubygems_version: !ruby/object:Gem::Requirement
129
129
  requirements:
130
130
  - - ">="
@@ -135,8 +135,4 @@ rubygems_version: 3.4.6
135
135
  signing_key:
136
136
  specification_version: 4
137
137
  summary: Feature Envy enhances Ruby with features inspired by other programming languages
138
- test_files:
139
- - test/final_class_test.rb
140
- - test/internal_test.rb
141
- - test/support/assertions.rb
142
- - test/test_helper.rb
138
+ test_files: []
@@ -1,43 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "test_helper"
4
-
5
- class FinalClassTest < Minitest::Test
6
- using FeatureEnvy::FinalClass
7
-
8
- def test_non_refinement_final_class_cannot_be_inherited
9
- user = Class.new do
10
- extend FeatureEnvy::FinalClass
11
- end
12
-
13
- assert_raises FeatureEnvy::FinalClass::Error,
14
- "Subclassing a final class should have raised an exception" do
15
- Class.new user
16
- end
17
- assert user.final?,
18
- "A final class should have been reported as final but was not"
19
- end
20
-
21
- def test_refinement_final_class_cannot_be_inherited
22
- user = Class.new do
23
- final!
24
- end
25
-
26
- assert_raises FeatureEnvy::FinalClass::Error,
27
- "Subclassing a final class should have raised an exception" do
28
- Class.new user
29
- end
30
- assert user.final?,
31
- "A final class should have been reported as final but was not"
32
- end
33
-
34
- def test_error_when_superclass_made_final
35
- model = Class.new
36
- user = Class.new model # rubocop:disable Lint/UselessAssignment
37
-
38
- assert_raises FeatureEnvy::FinalClass::Error,
39
- "Making a superclass final should have raised an exception" do
40
- model.extend FeatureEnvy::FinalClass
41
- end
42
- end
43
- end
@@ -1,25 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "test_helper"
4
-
5
- class InternalTest < Minitest::Test
6
- class Model; end
7
- class User < Model; end
8
- class Project < Model; end
9
- class AdminUser < User; end
10
-
11
- def test_subclasses
12
- assert_equal [User, Project].sort_by(&:name),
13
- FeatureEnvy::Internal.subclasses(Model).sort_by(&:name),
14
- "All subclasses and no other descendants should have been returned"
15
- end
16
-
17
- def test_class_name
18
- assert_equal "InternalTest::Model",
19
- FeatureEnvy::Internal.class_name(Model)
20
- assert_equal "InternalTest::User",
21
- FeatureEnvy::Internal.class_name(User)
22
- assert_equal "anonymous class",
23
- FeatureEnvy::Internal.class_name(Class.new)
24
- end
25
- end
@@ -1,11 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Assertions
4
- def assert_mapping callable, mapping
5
- Hash(mapping).each do |input, output|
6
- arguments = input.is_a?(Array) ? input : [input]
7
-
8
- assert_equal output, callable.call(*arguments)
9
- end
10
- end
11
- end
data/test/test_helper.rb DELETED
@@ -1,12 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
4
- require "feature_envy"
5
-
6
- require "minitest/autorun"
7
-
8
- require_relative "support/assertions"
9
-
10
- class Minitest::Test
11
- include Assertions
12
- end