feature_envy 0.2.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 +70 -3
- 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/missing.rb +5 -0
- data/lib/feature_envy/name_dispatch.rb +11 -0
- data/lib/feature_envy/operation.rb +102 -0
- data/lib/feature_envy/version.rb +1 -1
- data/lib/feature_envy.rb +8 -0
- metadata +7 -2
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
@@ -10,6 +10,7 @@ Supported features:
|
|
10
10
|
- Final classes
|
11
11
|
- Thread-safe lazy accessors
|
12
12
|
- Object literals
|
13
|
+
- Inspect (inspired by Elixir's `Kernel.inspect/2`)
|
13
14
|
|
14
15
|
## Installation
|
15
16
|
|
@@ -24,9 +25,75 @@ Don't forget to run `bundle install` afterwards.
|
|
24
25
|
|
25
26
|
## Usage
|
26
27
|
|
27
|
-
|
28
|
-
|
29
|
-
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
|
+
```
|
30
97
|
|
31
98
|
## Author
|
32
99
|
|
@@ -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,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,6 +7,13 @@ 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
|
@@ -15,4 +22,5 @@ module FeatureEnvy
|
|
15
22
|
autoload :Internal, "feature_envy/internal"
|
16
23
|
autoload :LazyAccessor, "feature_envy/lazy_accessor"
|
17
24
|
autoload :ObjectLiteral, "feature_envy/object_literal"
|
25
|
+
autoload :Inspect, "feature_envy/inspect"
|
18
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
|
@@ -103,9 +103,14 @@ 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
|
107
109
|
- lib/feature_envy/lazy_accessor.rb
|
110
|
+
- lib/feature_envy/missing.rb
|
111
|
+
- lib/feature_envy/name_dispatch.rb
|
108
112
|
- lib/feature_envy/object_literal.rb
|
113
|
+
- lib/feature_envy/operation.rb
|
109
114
|
- lib/feature_envy/version.rb
|
110
115
|
homepage: https://github.com/gregnavis/feature_envy
|
111
116
|
licenses:
|