feature_envy 0.2.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 +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:
|