ObjectInspector

Gem Version Build Status Test Coverage Maintainability

ObjectInspector takes Object#inspect to the next level. Specify any combination of identification attributes, flags, issues, info, and/or a name along with an optional, self-definable scope option to represents objects. Great for the console, logging, etc.

Because object inspection code should be uniform, easy to build, and its output should be easy to read!

If you’d like to just jump into an example: Full Example.

Installation

Add this line to your application’s Gemfile:

ruby gem "object_inspector"

And then execute:

$ bundle

Or install it yourself:

$ gem install object_inspector

Compatibility

Tested MRI Ruby Versions: * 2.3 * 2.4 * 2.5 * 2.6 * edge

ObjectInspector has no other dependencies.

Configuration

Global/default values for ObjectInspector can be configured via the ObjectInspector::Configuration object.

Note: In a Rails app, the following would go in e.g. config/initializers/object_inspector.rb

ruby # Default values are shown. ObjectInspector.configure do |config| config.formatter_class = ObjectInspector::TemplatingFormatter config.inspect_method_prefix = "inspect" config.default_scope = ObjectInspector::Scope.new(:self) config.wild_card_scope = "all" config.out_of_scope_placeholder = "*" config.presenter_inspect_flags = " ⇨ " config.name_separator = " - " config.flags_separator = " / " config.issues_separator = " | " config.info_separator = " | " end

Usage

Given, an object of any type, call ObjectInspector::Inspector.inspect.

```ruby class MyObject def inspect ObjectInspector::Inspector.inspect(self) end end

MyObject.new.inspect # => “" ```

See also Helper Usage for an even simpler usage option.

Output Customization

Use the identification, flags, info, and/or name options to customize inspect output.

```ruby class MyObject def inspect ObjectInspector::Inspector.inspect( self, identification: “My Object”, flags: “FLAG1 / FLAG2”, info: “INFO”, name: “NAME”) end end

MyObject.new.inspect # => “<My Object(FLAG1 / FLAG2) INFO :: NAME>” ```

Or, define inspect_identification, inspect_flags, inspect_info, and/or inspect_name (or display_name) as either public or private methods on Object.

```ruby class MyObject def inspect ObjectInspector::Inspector.inspect(self) end

private

def inspect_identification; “My Object” end def inspect_flags; “FLAG1 / FLAG2” end def inspect_issues; “ISSUE1 | ISSUE2” end def inspect_info; “INFO” end def inspect_name; “NAME” end # Or: def display_name; “NAME” end end

MyObject.new.inspect # => “<My Object(FLAG1 / FLAG2) !!ISSUE1 | ISSUE2!! INFO :: NAME>” ```

Helper Usage

To save some typing, include ObjectInspector::InspectHelper into an object and ObjectInspector::Inspector.inspect will be called on self automatically.

```ruby class MyObject include ObjectInspector::InspectorsHelper end

MyObject.new.inspect # => “" ```

To access the ObjectInspector::Inspector’s options via the helper, call into super.

```ruby class MyObject include ObjectInspector::InspectorsHelper

def inspect super(identification: “My Object”, flags: “FLAG1”, issues: “ISSUE1 | ISSUE2”, info: “INFO”, name: “NAME”) end end

MyObject.new.inspect # => “<My Object(FLAG1) !!ISSUE1 | ISSUE2!! INFO :: NAME>” ```

Or, define inspect_identification, inspect_flags, inspect_info, and/or inspect_name (or display_name) in Object.

```ruby class MyObject include ObjectInspector::InspectorsHelper

private

def inspect_identification; “My Object” end def inspect_flags; “FLAG1 / FLAG2” end def inspect_issues; “ISSUE1 | ISSUE2” end def inspect_info; “INFO” end def inspect_name; “NAME” end # Or: def display_name; “NAME” end end

MyObject.new.inspect # => “<My Object(FLAG1) !!ISSUE1 | ISSUE2!! INFO :: NAME>” ```

Scopes

Use the scope option to define the scope of the inspect_* methods. The supplied value will be wrapped by the ObjectInspector::Scope helper object. The default value is ObjectInspector::Scope.new(:self).

Scope Names

ObjectInspector::Scope acts like ActiveSupport::StringInquirer. This is a prettier way to test for a given type of “scope” within objects.

The ObjectInspector::Scope objects in these examples are the same as specifying <scope_name> like this:

ruby my_object.inspect(scope: <scope_name>)

Options: - :self (Default) – Is meant to confine object interrogation to self (don’t interrogate neighboring objects). - :all – Is meant to match on all scopes, regardless of their name. - <custom> – Anything else that makes sense for the object to key on.

ruby scope = ObjectInspector::Scope.new scope.self? # => true scope.verbose? # => false scope.complex? # => false

Multiple Scope Names

It is also possible to pass in multiple scope names to match on.

ruby scope = ObjectInspector::Scope.new(%i[verbose complex]) scope.self? # => false scope.verbose? # => true scope.complex? # => true

The “Wild Card” Scope

Finally, :all is a “wild card” scope name, and will match on all scope names.

ruby scope = ObjectInspector::Scope.new(:all) scope.self? # => true scope.verbose? # => true scope.complex? # => true

Scope blocks

Passing a block to a scope predicate falls back to the out-of-scope placeholder (* by default) if the scope does not match.

ruby scope = ObjectInspector::Scope.new(:verbose) scope.verbose? { "MATCH" } # => "MATCH" scope.complex? { "MATCH" } # => "*"

Scope Joiners

ObjectInspector::Scope also offers helper methods for uniformly joining inspect elements: - join_name – Joins name parts with ` - ` by default - join_flags – Joins flags with ` / ` by default - join_info – Joins info items with ` | ` by default

ruby scope = ObjectInspector::Scope.new(:verbose) scope.join_name([1, 2, 3]) # => "1 - 2 - 3" scope.join_name([1, 2, 3, nil]) # => "1 - 2 - 3" scope.join_flags([1, 2, 3]) # => "1 / 2 / 3" scope.join_flags([1, 2, 3, nil]) # => "1 / 2 / 3" scope.join_info([1, 2, 3]) # => "1 | 2 | 3" scope.join_info([1, 2, 3, nil]) # => "1 | 2 | 3"

Full Example

```ruby class MyObject include ObjectInspector::InspectorsHelper

attr_reader :name, :a2

def initialize(name, a2 = 2) @name = name @a2 = a2 end

def associated_object1 OpenStruct.new(flags: “AO1_FLAG1”) end

def associated_object2 OpenStruct.new(flags: “AO2_FLAG1”) end

# Or def inspect_name def display_name(scope:) name end

private

def inspect_identification identify(:a2) end

def inspect_flags(scope:) flags = [“DEFAULT_FLAG”]

flags <<
  scope.verbose? {
    [
      associated_object1.flags,
      associated_object2.flags,
    ]
  }

scope.join_flags(flags)   end

def inspect_issues “!!WARNING!!” end

def inspect_info(scope:) info = [“Default Info”] info « “Complex Info” if scope.complex? info « scope.verbose? { “Verbose Info” }

scope.join_info(info)   end end

my_object = MyObject.new(“Name”)

my_object.inspect(scope: :complex) # => “<MyObjecta2:2 !!!!WARNING!!!! Default Info | Complex Info | * :: Name>”

my_object.inspect(scope: :verbose) # => “<MyObjecta2:2 !!!!WARNING!!!! Default Info | Verbose Info :: Name>”

my_object.inspect(scope: %i[self complex verbose]) # => “<MyObjecta2:2 !!!!WARNING!!!! Default Info | Complex Info | Verbose Info :: Name>”

my_object.inspect(scope: :all) # => “<MyObjecta2:2 !!!!WARNING!!!! Default Info | Complex Info | Verbose Info :: Name>”

my_object.inspect # => “<MyObjecta2:2 !!!!WARNING!!!! Default Info | * :: Name>”

ObjectInspector.configuration.default_scope = :complex my_object.inspect # => “<MyObjecta2:2 !!!!WARNING!!!! Default Info | Complex Info | * :: Name>”

ObjectInspector.configuration.default_scope = %i[self complex verbose] my_object.inspect # => “<MyObjecta2:2 !!!!WARNING!!!! Default Info | Complex Info | Verbose Info :: Name>”

ObjectInspector.configuration.default_scope = :all my_object.inspect # => “<MyObjecta2:2 !!!!WARNING!!!! Default Info | Complex Info | Verbose Info :: Name>” ```

Wrapped Objects

If the Object being inspected wraps another object – i.e. defines #to_model and #to_model returns an object other than self – the inspect output will re-inspect the wrapped object. The wrapper points to the wrapped object with an arrow (⇨).

```ruby class MyWrapperObject include ObjectInspector::InspectorsHelper

def to_model @to_model ||= MyWrappedObject.new end

private

def inspect_flags; “WRAPPER_FLAG1” end end

class MyWrappedObject include ObjectInspector::InspectorsHelper

private

def inspect_flags; “FLAG1 / FLAG2” end def inspect_info; “INFO” end end

MyWrapperObject.new.inspect # => “<MyWrapperObject(WRAPPER_FLAG1)> ⇨ <MyWrappedObject(FLAG1 / FLAG2) INFO>” ```

This feature is recursive.

Wrapped Delegators

If the Object being inspected is wrapped by an object that delegates all unknown methods to the wrapped object, then inspect flags will be doubled up. To get around this, redefine the inspect method in the Wrapper object e.g. like:

```ruby class MyDelegatingWrapperObject include ObjectInspector::InspectorsHelper

def initialize(my_object) @my_object = my_object end

def inspect(**kargs) super(identification: self.class.name, name: nil, flags: nil, info: nil, issues: nil, **kargs) end

def to_model @my_object end

private

def method_missing(method_symbol, *args) @my_object.send(method_symbol, *args) end

def respond_to_missing?(args) @my_object.respond_to?(args) || super end end

class MyWrappedObject include ObjectInspector::InspectorsHelper

def display_name “WRAPPED_OBJECT_NAME” end

private

def inspect_flags; “FLAG1” end def inspect_info; “INFO” end def inspect_issues; “ISSUE1” end def inspect_name; “NAME” end end

MyDelegatingWrapperObject.new(MyWrappedObject.new).inspect # => “ ⇨ <MyWrappedObject(FLAG1) !!ISSUE1!! INFO :: NAME>" ```

On-the-fly Inspect Methods

When passed as an option (as opposed to being called via an Object-defined method) symbols will be called/evaluated on Object on the fly.

```ruby class MyObject include ObjectInspector::InspectorsHelper

def my_method1; “Result1” end def my_method2; “Result2” end

def inspect_info; :my_method2 end end

MyObject.new.inspect(info: “my_method1”) # => “" MyObject.new.inspect(info: :my_method2) # => "" MyObject.new.inspect # => "" ```

Custom Formatters

A custom inspect formatter can be defined by implementing the interface defined by ObjectInspector::BaseFormatter. Then, either override the ObjectInspector::Configuration#formatter_class value (see Configuration) or just pass your custom class name into ObjectInspector::Inspector.new.

```ruby class MyCustomFormatter < ObjectInspector::BaseFormatter def call “[#identification Flags: #flags – Info: #info – Name: #name]” end end

class MyObject include ObjectInspector::InspectorsHelper

def inspect super(formatter: MyCustomFormatter, identification: “IDENTIFICATION”, flags: “FLAG1 / FLAG2”, info: “INFO”, name: “NAME”) end end

MyObject.new.inspect # => “[IDENTIFICATION Flags: FLAG1 / FLAG2 – Info: INFO – Name: NAME]” ```

See examples: - ObjectInspector::TemplatingFormatter - ObjectInspector::CombiningFormatter

Supporting Gems

ObjectInspector works great with the ObjectIdentifier gem.

```ruby class MyObject include ObjectInspector::InspectorsHelper

def my_method1 1 end

def my_method2 2 end

private

def inspect_identification identify(:my_method1, :my_method2) end

def inspect_flags; “FLAG1 / FLAG2” end def inspect_issues; “ISSUE1 | ISSUE2” end def inspect_info; “INFO” end def inspect_name; “NAME” end end

MyObject.new.inspect # => “<MyObjectmy_method1:1, my_method2:2 !!ISSUE1 | ISSUE2!! INFO :: NAME>” ```

Performance

Benchmarking ObjectInspector

ObjectInspetor is ~4x slower than Ruby’s default inspect.

Performance of ObjectInspect can be tested by playing the ObjectInspector Benchmarking Scripts in the pry console for this gem.

ruby play scripts/benchmarking/object_inspector.rb # Comparison: # Ruby: 30382.2 i/s # ObjectInspector::Inspector: 7712.2 i/s - 3.94x slower

Benchmarking Formatters

ObjectInspector::TemplatingFormatter – which is the default Formatter – outperforms ObjectInspector::CombiningFormatter by about 30% on average.

Performance of Formatters can be tested by playing the Formatters Benchmarking Scripts in the pry console for this gem.

ruby play scripts/benchmarking/formatters.rb # == Averaged ============================================================= # ... # # Comparison: # ObjectInspector::TemplatingFormatter: 45725.3 i/s # ObjectInspector::CombiningFormatter: 34973.9 i/s - 1.31x slower # # == Done

Benchmarking Custom Formatters

Custom Formatters may be similarly gauged for comparison by adding them to the custom_formatter_klasses array before playing the script.

```ruby custom_formatter_klasses = [MyCustomFormatter]

play scripts/benchmarking/formatters.rb # == Averaged ============================================================= # … # # Comparison: # MyCustomFormatter: 52001.2 i/s # ObjectInspector::TemplatingFormatter: 49854.2 i/s - same-ish: difference falls within error # ObjectInspector::CombiningFormatter: 38963.5 i/s - 1.33x slower # # == Done ```

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake test to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and tags, and push the .gem file to rubygems.org.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/pdobb/object_inspector.

License

The gem is available as open source under the terms of the MIT License.