feature_envy 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: ebacd4b2d2a8d5292de874248a212a72b02a7bc13b5279b08f020a88c8973e71
4
- data.tar.gz: bca18e8eab4ef3d67efa38b65a09f4ede7291873a95a266748bbfd79e2b9ab45
3
+ metadata.gz: a14b68770c0137db65d3846d607afa7c355f994d3f5716dc8f9f97b092c63cc4
4
+ data.tar.gz: f8c7eca5be96c55987540c741f19d2161e8d758ce3a22de98e7fc5603ca3d0ba
5
5
  SHA512:
6
- metadata.gz: 534c102e65795b30fd4cdd42ef10b34b062776642f145ebddffd3865fc39e81657c870e645e782362f39c45b1d6afb2e565caefb2932da69fcd84d71121a5548
7
- data.tar.gz: 0be465e644e71f4b51f14acb6e6e496c43618e5fbe8e13cc2a288291789074bcbc5d297589b4cb4b3d36eff742eb5a5351b3db81298a7637e22f348e78afb53b
6
+ metadata.gz: 2c2f7efc87d84526283e4558102ef8355455efa9597fbf83d9874a1adc177cbaddbd8167c87a50836778b2312d94dda1328c85eae06d91cb395d0eb4c6cc6228
7
+ data.tar.gz: 0c338372e47dba9ee7e4177f619a6de77cc8515c0daaea0976634a315564b5bfe34250a51ada1d55c126902c819d10ee8704cdf0d358fd50921860a905917ad7
data/README.md CHANGED
@@ -5,6 +5,13 @@ 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
+ - Inspect (inspired by Elixir's `Kernel.inspect/2`)
14
+
8
15
  ## Installation
9
16
 
10
17
  You can install the gem by running `gem install feature_envy` or adding it to
@@ -18,9 +25,75 @@ Don't forget to run `bundle install` afterwards.
18
25
 
19
26
  ## Usage
20
27
 
21
- Features are independent from each other and can be enabled one-by-one when
22
- needed. Please refer to individual feature documentation to understand how to
23
- enabled them.
28
+ Below are example snippets for a quick start with the project. Please refer to
29
+ individual feature documentation for details. Features are designed to be
30
+ independent and should be enabled one-by-one.
31
+
32
+ ### Final Classes
33
+
34
+ ```ruby
35
+ module Models
36
+ # Enable the feature in a given module via the using directive.
37
+ using FeatureEnvy::FinalClass
38
+
39
+ class Admin < User
40
+ # Call final! inside the definition of a class you want to mark final.
41
+ final!
42
+ end
43
+ end
44
+ ```
45
+
46
+ ### Lazy Accessors
47
+
48
+ ```ruby
49
+ class User
50
+ # Enable the feature in a given class via the using directive. Alternatively,
51
+ # you can enable it in a higher-level module, so that all classes defined in
52
+ # support lazy accessors.
53
+ using FeatureEnvy::LazyAccessor
54
+
55
+ # These are some attributes that will be used by the lazy accessor.
56
+ attr_accessor :first_name, :last_name
57
+
58
+ # full_name is computed in a thread-safe fashion, and is lazy, i.e. it's
59
+ # computed on first access and then cached.
60
+ lazy(:full_name) { "#{first_name}" "#{last_name}" }
61
+ end
62
+ ```
63
+
64
+ ### Object Literals
65
+
66
+ ```ruby
67
+ # Object literals are inspired by JavaScript and enable inline object definition
68
+ # that mixes both attributes and methods. Consider the example below:
69
+ app = object do
70
+ @database = create_database_connection
71
+ @router = create_router
72
+
73
+ def start
74
+ @database.connect
75
+ @router.start
76
+ end
77
+ end
78
+
79
+ # app has @database and @router as attributes and responds to #start.
80
+ app.start
81
+ ```
82
+
83
+ ### Inspect
84
+
85
+ ```ruby
86
+ # Elixir-style inspect for debugging during development and testing. First,
87
+ # make #inspect! available on all objects.
88
+ class BasicObject
89
+ include FeatureEnvy::Inspect
90
+ end
91
+
92
+ # Second, configure how objects are inspected and where the results are sent.
93
+ # In this case, we just call the regular #inspect and send results to stderr.
94
+ FeatureEnvy::Inspect.inspector = FeatureEnvy::Inspect::InspectInspector
95
+ FeatureEnvy::Inspect.output = $stderr
96
+ ```
24
97
 
25
98
  ## Author
26
99
 
@@ -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,10 @@
1
+ module FeatureEnvy
2
+ module FinalMethod
3
+ refine Module do
4
+ def final *method_names
5
+ method_names.each do |method_name|
6
+ end
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,224 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FeatureEnvy
4
+ # Inspect method.
5
+ #
6
+ # ### Definition
7
+ #
8
+ # The inspect method is a helper method, inspired by `inspect/2` in Elixir,
9
+ # aimed at making print debugging easier with minimal disruption to
10
+ # surrounding code.
11
+ #
12
+ # ### Applications
13
+ #
14
+ # Quick inspection of intermediate values during development; **not intended
15
+ # for use in production**.
16
+ #
17
+ # ### Usage
18
+ #
19
+ # 1. Proceed with the remaining steps **only in non-production** environments.
20
+ # 2. Set an inspector by calling {FeatureEnvy::Inspect.inspector=}.
21
+ # This is a method taking the object being inspected at as the argument and
22
+ # returning its string representation. You can use the built-in
23
+ # {InspectInspector} as the starting point.
24
+ # 3. Set an output by calling {FeatureEnvy::Inspect.output=}. This is an object
25
+ # implementing `#puts` that will be called with the inspector result, so
26
+ # IO objects like `$stdout` and `$stderr` can be used.
27
+ # 4. Call {inspect} on objects you want to inspect at.
28
+ #
29
+ # A custom inspector and output can be provided. The examples below have
30
+ # templates that can be used as starting points in development. There's also
31
+ # an example showing how to enable the module in a Rails app.
32
+ #
33
+ # ### Discussion
34
+ #
35
+ # Elixir makes it easy to print objects of interest, including intermediate
36
+ # ones, by passing them through `Kernel.inspect/2`. This function prints the
37
+ # object's representation and returns the object itself, so that it can be
38
+ # called on intermediate results without disrupting the remaining
39
+ # instructions.
40
+ #
41
+ # For example, the instruction below instantiates `Language`, prints its
42
+ # representation, and then passes that language to `Language.create!/1`:
43
+ #
44
+ # ```elixir
45
+ # %Language{name: "Ruby"} |>
46
+ # inspect() |>
47
+ # Language.create!()
48
+ # ```
49
+ #
50
+ # {FeatureEnvy::Inspect} is a Ruby counterpart of Elixir's `inspect/2`.
51
+ # `#inspect!` was chosen since `#inspect` is already defined by Ruby and the
52
+ # `!` suffix indicates a "dangerous" method.
53
+ #
54
+ # The syntax `object.inspect!` was chosen over `inspect!(object)` as it's
55
+ # easier to insert in the middle of complicated method calls by requiring less
56
+ # modifications to the surrounding code. The difference is best illustrated
57
+ # in the following example:
58
+ #
59
+ # ```ruby
60
+ # # If we start with ...
61
+ # User.create!(user_attributes(request))
62
+ #
63
+ # # ... then it's easier to do to this ...
64
+ # User.create!(user_attributes(request).inspect)
65
+ #
66
+ # # ... than this:
67
+ # User.create!(inspect(user_attributes(request)))
68
+ # ```
69
+ #
70
+ # ### Implementation Notes
71
+ #
72
+ # 1. Refinement-based activation would require the developer to add
73
+ # `using FeatureEnvy::Inspect` before calling `inspect!`, which would be
74
+ # extremely inconvenient. Since the feature is intended for non-production
75
+ # use only monkey-patching is the only way to activate it.
76
+ #
77
+ # @example Enabling Inspect in a Rails app
78
+ # # Inspect should be activated only in non-production environments.
79
+ # unless Rails.env.production?
80
+ # # To make the method available on all objects, BasicObject must be
81
+ # # reopened and patched.
82
+ # class BasicObject
83
+ # include FeatureEnvy::Inspect
84
+ # end
85
+ #
86
+ # # Setting a inspector is required. Below, we're using a built-in inspector
87
+ # # that calls #inspect on the object being inspected at.
88
+ # FeatureEnvy::Inspect.inspector = FeatureEnvy::Inspect::InspectInspector
89
+ #
90
+ # # Results should be printed to stderr.
91
+ # FeatureEnvy::Inspect.output = $stderr
92
+ # end
93
+ #
94
+ # @example Inspector and output class templates
95
+ # class CustomInspector
96
+ # def call(object)
97
+ # # object is the object on which inspect! was called. The method should
98
+ # # return the string that should be passed to the output.
99
+ # end
100
+ # end
101
+ #
102
+ # class CustomOutput
103
+ # def puts(string)
104
+ # # string is the return value of #call sent to FeatureEnvy::Inspect.inspector.
105
+ # # The output object is responsible for showing this string to the
106
+ # # developer.
107
+ # end
108
+ # end
109
+ #
110
+ # @example Sending output to a logger
111
+ # # Assuming logger is an instance of the built-in Logger class, an adapter
112
+ # # is needed to make it output inspection results.
113
+ # FeatureEnvy::Inspect.output = FeatureEnvy::Inspect::LoggerAdapter.new logger
114
+ module Inspect
115
+ # A base class for errors related to the inspect method.
116
+ class Error < FeatureEnvy::Error; end
117
+
118
+ # An error raised when {#inspect!} is called but no inspector has been set.
119
+ class NoInspectorError < Error
120
+ # @!visibility private
121
+ def initialize
122
+ super(<<~ERROR)
123
+ No inspector has been set. Ensure that FeatureEnvy::Inspect.inspector is set
124
+ to an object responding to #call(object) somewhere early during
125
+ initialization.
126
+ ERROR
127
+ end
128
+ end
129
+
130
+ # An error raised when {#inspect!} is called but no output has been set.
131
+ class NoOutputError < Error
132
+ # @!visibility private
133
+ def initialize
134
+ super(<<~ERROR)
135
+ No output has been set. Ensure that FeatureEnvy::Inspect.output is set
136
+ to an object responding to #puts(string) somewhere early during
137
+ initialization.
138
+ ERROR
139
+ end
140
+ end
141
+
142
+ # Inspect the object and return it.
143
+ #
144
+ # The method inspects the object by:
145
+ #
146
+ # 1. Passing the object to `#inspect` defined on {.inspector}, producing a
147
+ # string representation of the object.
148
+ # 2. Passing that string to `#puts` defined on {.output}.
149
+ #
150
+ # @return [self] The object on which {#inspect!} was called.
151
+ def inspect!
152
+ if FeatureEnvy::Inspect.inspector.nil?
153
+ raise NoInspectorError.new
154
+ end
155
+ if FeatureEnvy::Inspect.output.nil?
156
+ raise NoOutputError.new
157
+ end
158
+
159
+ result = FeatureEnvy::Inspect.inspector.call self
160
+ FeatureEnvy::Inspect.output.puts result
161
+
162
+ self
163
+ end
164
+
165
+ class << self
166
+ # The inspector converting objects to string representations.
167
+ #
168
+ # The inspector **must** respond to `#call` with the object being
169
+ # inspected as the only argument, and **must** return a string, that will
170
+ # then be sent to {FeatureEnvy::Inspect.output}.
171
+ #
172
+ # @return [#call] The inspector currently in use.
173
+ attr_accessor :inspector
174
+
175
+ # The output object sending inspection results to the developer.
176
+ #
177
+ # The output object **must** respond to `#puts` with the string to print
178
+ # as its only argument. This implies all IO objects can be used, as well
179
+ # as custom classes implementing that interface.
180
+ #
181
+ # @return [#puts] The output object currently in use.
182
+ #
183
+ # @see FeatureEnvy::Inspect::LoggerAdapter
184
+ attr_accessor :output
185
+ end
186
+
187
+ # An inspect-based inspector.
188
+ #
189
+ # This is an inspector that calls `#inspect` on objects being inspected.
190
+ InspectInspector = ->(object) { object.inspect }
191
+
192
+ # An adapter class enabling the user of loggers for output.
193
+ #
194
+ # {FeatureEnvy::Inspect.output} must respond to `#puts`, which precludes
195
+ # loggers. This adapter can be used to make loggers usable as outputs by
196
+ # logging at the desired level.
197
+ #
198
+ # @example
199
+ # # Given a logger, it can be used as inspection output by setting:
200
+ # FeatureEnvy::Inspect.output = FeatureEnvy::Inspect::LoggerAdapter.new logger
201
+ #
202
+ # @example Changing the log level
203
+ # FeatureEnvy::Inspect.output =
204
+ # FeatureEnvy::Inspect::LoggerAdapter.new logger,
205
+ # level: Logger::INFO
206
+ class LoggerAdapter
207
+ # Initializes a new adapter for the specified logger.
208
+ #
209
+ # @param logger [Logger] logger to use for output
210
+ # @param level [Logger::DEBUG | Logger::INFO | Logger::WARN | Logger::ERROR | Logger::FATAL | Logger::UNKNOWN]
211
+ # level at which inspection results should be logged.
212
+ def initialize logger, level: Logger::DEBUG
213
+ @logger = logger
214
+ @level = level
215
+ end
216
+
217
+ # @api private
218
+ def puts string
219
+ @logger.add @level, string
220
+ nil
221
+ end
222
+ end
223
+ end
224
+ end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module FeatureEnvy
4
- # @private
4
+ # @!visibility private
5
5
  module Internal
6
6
  # Returns all subclasses of the given class.
7
7
  def self.subclasses parent_class
@@ -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,5 @@
1
+ module FeatureEnvy
2
+ module Missing
3
+ # Missing parameters
4
+ end
5
+ end
@@ -0,0 +1,11 @@
1
+ # irb(main):002:0> method(:foo).parameters
2
+ # => [[:req, :a], [:req, :b], [:opt, :c], [:keyreq, :d], [:block, :block]]
3
+
4
+ module FeatureEnvy
5
+ module NameDispatch
6
+ refine Module do
7
+ def name_dispatch *method_names
8
+ end
9
+ end
10
+ end
11
+ 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
@@ -0,0 +1,102 @@
1
+ # TODO:
2
+ # - handle blocks
3
+ # - add pre- and post-condition checking
4
+ # - add Kernel#parameter for injection
5
+ # - add Operation::Pipeline class to represent the whole pipeline
6
+ # - add inline parameter transformation
7
+ # - configurable exception handling
8
+ module FeatureEnvy
9
+ class Operation
10
+ module PipelineOperator
11
+ refine BasicObject do
12
+ def >>(callable) = callable.call(self)
13
+ end
14
+ end
15
+
16
+ class Pipeline
17
+ def initialize first, second
18
+ first_operations = first.is_a?(Pipeline) ? first.operations : [first]
19
+ second_operations = second.is_a?(Pipeline) ? second.operations : [second]
20
+
21
+ @operations = first_operations + second_operations
22
+ end
23
+
24
+ def call value
25
+ @operations.each do |operation|
26
+ value = operation.call value
27
+ end
28
+
29
+ value
30
+ end
31
+
32
+ def inspect
33
+ @operations.map(&:inspect).join " >> "
34
+ end
35
+
36
+ protected
37
+
38
+ attr_reader :operations
39
+ end
40
+
41
+ Placeholder = Object.new.freeze
42
+
43
+ class << self
44
+ private :new
45
+
46
+ def define(&block)
47
+ klass = Class.new(Operation)
48
+ klass.define_singleton_method(:perform, &block)
49
+ klass
50
+ end
51
+
52
+ def call(*args, **kwargs)
53
+ if args.include?(Placeholder) || kwargs.value?(Placeholder)
54
+ new(*args, **kwargs)
55
+ else
56
+ perform(*args, **kwargs)
57
+ end
58
+ end
59
+
60
+ def [](...) = self.call(...)
61
+ end
62
+
63
+ def initialize *args, **kwargs
64
+ @args = args
65
+ @kwargs = kwargs
66
+ end
67
+
68
+ def call value
69
+ args = @args.map { |arg| arg.equal?(Placeholder) ? value : arg }
70
+ kwargs = @kwargs.transform_values { |arg| arg.equal?(Placeholder) ? value : arg }
71
+
72
+ self.class.perform *args, **kwargs
73
+ end
74
+
75
+ def [](...) = call(...)
76
+
77
+ def inspect
78
+ args = []
79
+ @args.each do |arg|
80
+ args << inspect_arg(arg)
81
+ end
82
+ kwargs_part = @kwargs.each do |key, arg|
83
+ args << "#{key}: #{inspect_arg(arg)}"
84
+ end
85
+ "#{self.class.name}[#{args.join(", ")}]"
86
+ end
87
+
88
+ def >>(rest)
89
+ Pipeline.new(self, rest)
90
+ end
91
+
92
+ private
93
+
94
+ def inspect_arg(arg)
95
+ if arg.equal?(Placeholder)
96
+ "__"
97
+ else
98
+ arg.inspect
99
+ end
100
+ end
101
+ end
102
+ 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.3.0"
6
6
  end
data/lib/feature_envy.rb CHANGED
@@ -7,10 +7,20 @@ require_relative "feature_envy/version"
7
7
  # Features are independent from each other and are implemented in separate
8
8
  # submodules. Refer to module documentation for details on how each feature can
9
9
  # be enabled and used.
10
+ #
11
+ # The following features are available:
12
+ #
13
+ # - {FeatureEnvy::FinalClass} - final classes.
14
+ # - {FeatureEnvy::LazyAccessor} - lazy accessors.
15
+ # - {FeatureEnvy::ObjectLiteral} - object literals.
16
+ # - {FeatureEnvy::Inspect} - Elixir-style inspect.
10
17
  module FeatureEnvy
11
18
  # A base class for all errors raised by Feature Envy.
12
19
  class Error < StandardError; end
13
20
 
14
- autoload :Internal, "feature_envy/internal"
15
- autoload :FinalClass, "feature_envy/final_class"
21
+ autoload :FinalClass, "feature_envy/final_class"
22
+ autoload :Internal, "feature_envy/internal"
23
+ autoload :LazyAccessor, "feature_envy/lazy_accessor"
24
+ autoload :ObjectLiteral, "feature_envy/object_literal"
25
+ autoload :Inspect, "feature_envy/inspect"
16
26
  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.3.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-06-02 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
@@ -103,18 +103,23 @@ files:
103
103
  - README.md
104
104
  - lib/feature_envy.rb
105
105
  - lib/feature_envy/final_class.rb
106
+ - lib/feature_envy/final_method.rb
107
+ - lib/feature_envy/inspect.rb
106
108
  - lib/feature_envy/internal.rb
109
+ - lib/feature_envy/lazy_accessor.rb
110
+ - lib/feature_envy/missing.rb
111
+ - lib/feature_envy/name_dispatch.rb
112
+ - lib/feature_envy/object_literal.rb
113
+ - lib/feature_envy/operation.rb
107
114
  - 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
115
  homepage: https://github.com/gregnavis/feature_envy
113
116
  licenses:
114
117
  - MIT
115
118
  metadata:
116
119
  homepage_uri: https://github.com/gregnavis/feature_envy
117
120
  source_code_uri: https://github.com/gregnavis/feature_envy
121
+ bug_tracker_uri: https://github.com/gregnavis/feature_envy/issues
122
+ documentation_uri: https://rubydoc.info/gems/feature_envy
118
123
  rubygems_mfa_required: 'true'
119
124
  post_install_message:
120
125
  rdoc_options: []
@@ -124,7 +129,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
124
129
  requirements:
125
130
  - - ">="
126
131
  - !ruby/object:Gem::Version
127
- version: 2.5.0
132
+ version: 3.1.0
128
133
  required_rubygems_version: !ruby/object:Gem::Requirement
129
134
  requirements:
130
135
  - - ">="
@@ -135,8 +140,4 @@ rubygems_version: 3.4.6
135
140
  signing_key:
136
141
  specification_version: 4
137
142
  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
143
+ 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