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 +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