feature_envy 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 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