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