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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d7675c689bf69e95854627b7dc5e28affa5a2fed1bcb46f8255a80217c6abdea
4
- data.tar.gz: 17a050ab57c4af67a4c967756b4704220e8511940498cb5f66c9989b6ecb2225
3
+ metadata.gz: a14b68770c0137db65d3846d607afa7c355f994d3f5716dc8f9f97b092c63cc4
4
+ data.tar.gz: f8c7eca5be96c55987540c741f19d2161e8d758ce3a22de98e7fc5603ca3d0ba
5
5
  SHA512:
6
- metadata.gz: 073e92360b8fbcaa1b6ce67a68f37adfdc4282c69bc15be4cc1e4119a99671958c175597309169a9e122008371a70814dd3e0836250185bad95374a241b6dbd5
7
- data.tar.gz: 2f3e21f91d1be99f5e00251568c87cb62131e8666af9c2a2d9685d42832d20cefb36a1f8ba87705c8b4f0a4fc3ddad10177c6caf473dcd6001156e2113f07804
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
- Features are independent from each other and can be enabled one-by-one when
28
- needed. Please refer to individual feature documentation to understand how to
29
- enabled them.
28
+ Below are example snippets for a quick start with the project. Please refer to
29
+ individual feature documentation for details. Features are designed to be
30
+ independent and should be enabled one-by-one.
31
+
32
+ ### Final Classes
33
+
34
+ ```ruby
35
+ module Models
36
+ # Enable the feature in a given module via the using directive.
37
+ using FeatureEnvy::FinalClass
38
+
39
+ class Admin < User
40
+ # Call final! inside the definition of a class you want to mark final.
41
+ final!
42
+ end
43
+ end
44
+ ```
45
+
46
+ ### Lazy Accessors
47
+
48
+ ```ruby
49
+ class User
50
+ # Enable the feature in a given class via the using directive. Alternatively,
51
+ # you can enable it in a higher-level module, so that all classes defined in
52
+ # support lazy accessors.
53
+ using FeatureEnvy::LazyAccessor
54
+
55
+ # These are some attributes that will be used by the lazy accessor.
56
+ attr_accessor :first_name, :last_name
57
+
58
+ # full_name is computed in a thread-safe fashion, and is lazy, i.e. it's
59
+ # computed on first access and then cached.
60
+ lazy(:full_name) { "#{first_name}" "#{last_name}" }
61
+ end
62
+ ```
63
+
64
+ ### Object Literals
65
+
66
+ ```ruby
67
+ # Object literals are inspired by JavaScript and enable inline object definition
68
+ # that mixes both attributes and methods. Consider the example below:
69
+ app = object do
70
+ @database = create_database_connection
71
+ @router = create_router
72
+
73
+ def start
74
+ @database.connect
75
+ @router.start
76
+ end
77
+ end
78
+
79
+ # app has @database and @router as attributes and responds to #start.
80
+ app.start
81
+ ```
82
+
83
+ ### Inspect
84
+
85
+ ```ruby
86
+ # Elixir-style inspect for debugging during development and testing. First,
87
+ # make #inspect! available on all objects.
88
+ class BasicObject
89
+ include FeatureEnvy::Inspect
90
+ end
91
+
92
+ # Second, configure how objects are inspected and where the results are sent.
93
+ # In this case, we just call the regular #inspect and send results to stderr.
94
+ FeatureEnvy::Inspect.inspector = FeatureEnvy::Inspect::InspectInspector
95
+ FeatureEnvy::Inspect.output = $stderr
96
+ ```
30
97
 
31
98
  ## Author
32
99
 
@@ -0,0 +1,10 @@
1
+ module FeatureEnvy
2
+ module FinalMethod
3
+ refine Module do
4
+ def final *method_names
5
+ method_names.each do |method_name|
6
+ end
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,224 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FeatureEnvy
4
+ # Inspect method.
5
+ #
6
+ # ### Definition
7
+ #
8
+ # The inspect method is a helper method, inspired by `inspect/2` in Elixir,
9
+ # aimed at making print debugging easier with minimal disruption to
10
+ # surrounding code.
11
+ #
12
+ # ### Applications
13
+ #
14
+ # Quick inspection of intermediate values during development; **not intended
15
+ # for use in production**.
16
+ #
17
+ # ### Usage
18
+ #
19
+ # 1. Proceed with the remaining steps **only in non-production** environments.
20
+ # 2. Set an inspector by calling {FeatureEnvy::Inspect.inspector=}.
21
+ # This is a method taking the object being inspected at as the argument and
22
+ # returning its string representation. You can use the built-in
23
+ # {InspectInspector} as the starting point.
24
+ # 3. Set an output by calling {FeatureEnvy::Inspect.output=}. This is an object
25
+ # implementing `#puts` that will be called with the inspector result, so
26
+ # IO objects like `$stdout` and `$stderr` can be used.
27
+ # 4. Call {inspect} on objects you want to inspect at.
28
+ #
29
+ # A custom inspector and output can be provided. The examples below have
30
+ # templates that can be used as starting points in development. There's also
31
+ # an example showing how to enable the module in a Rails app.
32
+ #
33
+ # ### Discussion
34
+ #
35
+ # Elixir makes it easy to print objects of interest, including intermediate
36
+ # ones, by passing them through `Kernel.inspect/2`. This function prints the
37
+ # object's representation and returns the object itself, so that it can be
38
+ # called on intermediate results without disrupting the remaining
39
+ # instructions.
40
+ #
41
+ # For example, the instruction below instantiates `Language`, prints its
42
+ # representation, and then passes that language to `Language.create!/1`:
43
+ #
44
+ # ```elixir
45
+ # %Language{name: "Ruby"} |>
46
+ # inspect() |>
47
+ # Language.create!()
48
+ # ```
49
+ #
50
+ # {FeatureEnvy::Inspect} is a Ruby counterpart of Elixir's `inspect/2`.
51
+ # `#inspect!` was chosen since `#inspect` is already defined by Ruby and the
52
+ # `!` suffix indicates a "dangerous" method.
53
+ #
54
+ # The syntax `object.inspect!` was chosen over `inspect!(object)` as it's
55
+ # easier to insert in the middle of complicated method calls by requiring less
56
+ # modifications to the surrounding code. The difference is best illustrated
57
+ # in the following example:
58
+ #
59
+ # ```ruby
60
+ # # If we start with ...
61
+ # User.create!(user_attributes(request))
62
+ #
63
+ # # ... then it's easier to do to this ...
64
+ # User.create!(user_attributes(request).inspect)
65
+ #
66
+ # # ... than this:
67
+ # User.create!(inspect(user_attributes(request)))
68
+ # ```
69
+ #
70
+ # ### Implementation Notes
71
+ #
72
+ # 1. Refinement-based activation would require the developer to add
73
+ # `using FeatureEnvy::Inspect` before calling `inspect!`, which would be
74
+ # extremely inconvenient. Since the feature is intended for non-production
75
+ # use only monkey-patching is the only way to activate it.
76
+ #
77
+ # @example Enabling Inspect in a Rails app
78
+ # # Inspect should be activated only in non-production environments.
79
+ # unless Rails.env.production?
80
+ # # To make the method available on all objects, BasicObject must be
81
+ # # reopened and patched.
82
+ # class BasicObject
83
+ # include FeatureEnvy::Inspect
84
+ # end
85
+ #
86
+ # # Setting a inspector is required. Below, we're using a built-in inspector
87
+ # # that calls #inspect on the object being inspected at.
88
+ # FeatureEnvy::Inspect.inspector = FeatureEnvy::Inspect::InspectInspector
89
+ #
90
+ # # Results should be printed to stderr.
91
+ # FeatureEnvy::Inspect.output = $stderr
92
+ # end
93
+ #
94
+ # @example Inspector and output class templates
95
+ # class CustomInspector
96
+ # def call(object)
97
+ # # object is the object on which inspect! was called. The method should
98
+ # # return the string that should be passed to the output.
99
+ # end
100
+ # end
101
+ #
102
+ # class CustomOutput
103
+ # def puts(string)
104
+ # # string is the return value of #call sent to FeatureEnvy::Inspect.inspector.
105
+ # # The output object is responsible for showing this string to the
106
+ # # developer.
107
+ # end
108
+ # end
109
+ #
110
+ # @example Sending output to a logger
111
+ # # Assuming logger is an instance of the built-in Logger class, an adapter
112
+ # # is needed to make it output inspection results.
113
+ # FeatureEnvy::Inspect.output = FeatureEnvy::Inspect::LoggerAdapter.new logger
114
+ module Inspect
115
+ # A base class for errors related to the inspect method.
116
+ class Error < FeatureEnvy::Error; end
117
+
118
+ # An error raised when {#inspect!} is called but no inspector has been set.
119
+ class NoInspectorError < Error
120
+ # @!visibility private
121
+ def initialize
122
+ super(<<~ERROR)
123
+ No inspector has been set. Ensure that FeatureEnvy::Inspect.inspector is set
124
+ to an object responding to #call(object) somewhere early during
125
+ initialization.
126
+ ERROR
127
+ end
128
+ end
129
+
130
+ # An error raised when {#inspect!} is called but no output has been set.
131
+ class NoOutputError < Error
132
+ # @!visibility private
133
+ def initialize
134
+ super(<<~ERROR)
135
+ No output has been set. Ensure that FeatureEnvy::Inspect.output is set
136
+ to an object responding to #puts(string) somewhere early during
137
+ initialization.
138
+ ERROR
139
+ end
140
+ end
141
+
142
+ # Inspect the object and return it.
143
+ #
144
+ # The method inspects the object by:
145
+ #
146
+ # 1. Passing the object to `#inspect` defined on {.inspector}, producing a
147
+ # string representation of the object.
148
+ # 2. Passing that string to `#puts` defined on {.output}.
149
+ #
150
+ # @return [self] The object on which {#inspect!} was called.
151
+ def inspect!
152
+ if FeatureEnvy::Inspect.inspector.nil?
153
+ raise NoInspectorError.new
154
+ end
155
+ if FeatureEnvy::Inspect.output.nil?
156
+ raise NoOutputError.new
157
+ end
158
+
159
+ result = FeatureEnvy::Inspect.inspector.call self
160
+ FeatureEnvy::Inspect.output.puts result
161
+
162
+ self
163
+ end
164
+
165
+ class << self
166
+ # The inspector converting objects to string representations.
167
+ #
168
+ # The inspector **must** respond to `#call` with the object being
169
+ # inspected as the only argument, and **must** return a string, that will
170
+ # then be sent to {FeatureEnvy::Inspect.output}.
171
+ #
172
+ # @return [#call] The inspector currently in use.
173
+ attr_accessor :inspector
174
+
175
+ # The output object sending inspection results to the developer.
176
+ #
177
+ # The output object **must** respond to `#puts` with the string to print
178
+ # as its only argument. This implies all IO objects can be used, as well
179
+ # as custom classes implementing that interface.
180
+ #
181
+ # @return [#puts] The output object currently in use.
182
+ #
183
+ # @see FeatureEnvy::Inspect::LoggerAdapter
184
+ attr_accessor :output
185
+ end
186
+
187
+ # An inspect-based inspector.
188
+ #
189
+ # This is an inspector that calls `#inspect` on objects being inspected.
190
+ InspectInspector = ->(object) { object.inspect }
191
+
192
+ # An adapter class enabling the user of loggers for output.
193
+ #
194
+ # {FeatureEnvy::Inspect.output} must respond to `#puts`, which precludes
195
+ # loggers. This adapter can be used to make loggers usable as outputs by
196
+ # logging at the desired level.
197
+ #
198
+ # @example
199
+ # # Given a logger, it can be used as inspection output by setting:
200
+ # FeatureEnvy::Inspect.output = FeatureEnvy::Inspect::LoggerAdapter.new logger
201
+ #
202
+ # @example Changing the log level
203
+ # FeatureEnvy::Inspect.output =
204
+ # FeatureEnvy::Inspect::LoggerAdapter.new logger,
205
+ # level: Logger::INFO
206
+ class LoggerAdapter
207
+ # Initializes a new adapter for the specified logger.
208
+ #
209
+ # @param logger [Logger] logger to use for output
210
+ # @param level [Logger::DEBUG | Logger::INFO | Logger::WARN | Logger::ERROR | Logger::FATAL | Logger::UNKNOWN]
211
+ # level at which inspection results should be logged.
212
+ def initialize logger, level: Logger::DEBUG
213
+ @logger = logger
214
+ @level = level
215
+ end
216
+
217
+ # @api private
218
+ def puts string
219
+ @logger.add @level, string
220
+ nil
221
+ end
222
+ end
223
+ end
224
+ end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module FeatureEnvy
4
- # @private
4
+ # @!visibility private
5
5
  module Internal
6
6
  # Returns all subclasses of the given class.
7
7
  def self.subclasses parent_class
@@ -0,0 +1,5 @@
1
+ module FeatureEnvy
2
+ module Missing
3
+ # Missing parameters
4
+ end
5
+ end
@@ -0,0 +1,11 @@
1
+ # irb(main):002:0> method(:foo).parameters
2
+ # => [[:req, :a], [:req, :b], [:opt, :c], [:keyreq, :d], [:block, :block]]
3
+
4
+ module FeatureEnvy
5
+ module NameDispatch
6
+ refine Module do
7
+ def name_dispatch *method_names
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,102 @@
1
+ # TODO:
2
+ # - handle blocks
3
+ # - add pre- and post-condition checking
4
+ # - add Kernel#parameter for injection
5
+ # - add Operation::Pipeline class to represent the whole pipeline
6
+ # - add inline parameter transformation
7
+ # - configurable exception handling
8
+ module FeatureEnvy
9
+ class Operation
10
+ module PipelineOperator
11
+ refine BasicObject do
12
+ def >>(callable) = callable.call(self)
13
+ end
14
+ end
15
+
16
+ class Pipeline
17
+ def initialize first, second
18
+ first_operations = first.is_a?(Pipeline) ? first.operations : [first]
19
+ second_operations = second.is_a?(Pipeline) ? second.operations : [second]
20
+
21
+ @operations = first_operations + second_operations
22
+ end
23
+
24
+ def call value
25
+ @operations.each do |operation|
26
+ value = operation.call value
27
+ end
28
+
29
+ value
30
+ end
31
+
32
+ def inspect
33
+ @operations.map(&:inspect).join " >> "
34
+ end
35
+
36
+ protected
37
+
38
+ attr_reader :operations
39
+ end
40
+
41
+ Placeholder = Object.new.freeze
42
+
43
+ class << self
44
+ private :new
45
+
46
+ def define(&block)
47
+ klass = Class.new(Operation)
48
+ klass.define_singleton_method(:perform, &block)
49
+ klass
50
+ end
51
+
52
+ def call(*args, **kwargs)
53
+ if args.include?(Placeholder) || kwargs.value?(Placeholder)
54
+ new(*args, **kwargs)
55
+ else
56
+ perform(*args, **kwargs)
57
+ end
58
+ end
59
+
60
+ def [](...) = self.call(...)
61
+ end
62
+
63
+ def initialize *args, **kwargs
64
+ @args = args
65
+ @kwargs = kwargs
66
+ end
67
+
68
+ def call value
69
+ args = @args.map { |arg| arg.equal?(Placeholder) ? value : arg }
70
+ kwargs = @kwargs.transform_values { |arg| arg.equal?(Placeholder) ? value : arg }
71
+
72
+ self.class.perform *args, **kwargs
73
+ end
74
+
75
+ def [](...) = call(...)
76
+
77
+ def inspect
78
+ args = []
79
+ @args.each do |arg|
80
+ args << inspect_arg(arg)
81
+ end
82
+ kwargs_part = @kwargs.each do |key, arg|
83
+ args << "#{key}: #{inspect_arg(arg)}"
84
+ end
85
+ "#{self.class.name}[#{args.join(", ")}]"
86
+ end
87
+
88
+ def >>(rest)
89
+ Pipeline.new(self, rest)
90
+ end
91
+
92
+ private
93
+
94
+ def inspect_arg(arg)
95
+ if arg.equal?(Placeholder)
96
+ "__"
97
+ else
98
+ arg.inspect
99
+ end
100
+ end
101
+ end
102
+ end
@@ -2,5 +2,5 @@
2
2
 
3
3
  module FeatureEnvy
4
4
  # The current version number.
5
- VERSION = "0.2.0"
5
+ VERSION = "0.3.0"
6
6
  end
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.2.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-04-05 00:00:00.000000000 Z
11
+ date: 2023-06-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -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: