feature_envy 0.1.0 → 0.2.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: 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