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 +4 -4
- data/README.md +76 -3
- data/lib/feature_envy/final_class.rb +24 -18
- data/lib/feature_envy/final_method.rb +10 -0
- data/lib/feature_envy/inspect.rb +224 -0
- data/lib/feature_envy/internal.rb +1 -1
- data/lib/feature_envy/lazy_accessor.rb +236 -0
- data/lib/feature_envy/missing.rb +5 -0
- data/lib/feature_envy/name_dispatch.rb +11 -0
- data/lib/feature_envy/object_literal.rb +80 -0
- data/lib/feature_envy/operation.rb +102 -0
- data/lib/feature_envy/version.rb +1 -1
- data/lib/feature_envy.rb +12 -2
- metadata +17 -16
- data/test/final_class_test.rb +0 -43
- data/test/internal_test.rb +0 -25
- data/test/support/assertions.rb +0 -11
- data/test/test_helper.rb +0 -12
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a14b68770c0137db65d3846d607afa7c355f994d3f5716dc8f9f97b092c63cc4
|
4
|
+
data.tar.gz: f8c7eca5be96c55987540c741f19d2161e8d758ce3a22de98e7fc5603ca3d0ba
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
22
|
-
|
23
|
-
enabled
|
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.
|
8
|
-
#
|
9
|
-
#
|
10
|
-
#
|
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
|
-
#
|
16
|
+
# ### Usage
|
13
17
|
#
|
14
|
-
# 1.
|
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
|
-
#
|
25
|
+
# ### Discussion
|
19
26
|
#
|
20
|
-
#
|
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
|
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,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
|
@@ -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
|
@@ -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
|
data/lib/feature_envy/version.rb
CHANGED
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 :
|
15
|
-
autoload :
|
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.
|
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-
|
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.
|
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.
|
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.
|
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.
|
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:
|
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: []
|
data/test/final_class_test.rb
DELETED
@@ -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
|
data/test/internal_test.rb
DELETED
@@ -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
|
data/test/support/assertions.rb
DELETED
data/test/test_helper.rb
DELETED