# ObjectInspector [![Gem Version](https://badge.fury.io/rb/object_inspector.svg)](https://badge.fury.io/rb/object_inspector) [![Build Status](https://travis-ci.org/pdobb/object_inspector.svg?branch=master)](https://travis-ci.org/pdobb/object_inspector) [![Test Coverage](https://api.codeclimate.com/v1/badges/34e821263d9e0c33d536/test_coverage)](https://codeclimate.com/github/pdobb/object_inspector/test_coverage) [![Maintainability](https://api.codeclimate.com/v1/badges/34e821263d9e0c33d536/maintainability)](https://codeclimate.com/github/pdobb/object_inspector/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](#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](#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 # => "" ``` 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 # => "" ``` ## 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 # => "" ``` 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 # => "" ``` ## 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](http://api.rubyonrails.org/classes/ActiveSupport/StringInquirer.html). 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 `` like this: ```ruby my_object.inspect(scope: ) ``` 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. - `` -- 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) # => "" my_object.inspect(scope: :verbose) # => "" my_object.inspect(scope: %i[self complex verbose]) # => "" my_object.inspect(scope: :all) # => "" my_object.inspect # => "" ObjectInspector.configuration.default_scope = :complex my_object.inspect # => "" ObjectInspector.configuration.default_scope = %i[self complex verbose] my_object.inspect # => "" ObjectInspector.configuration.default_scope = :all my_object.inspect # => "" ``` ## 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 # => "" ``` 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 # => "" ``` ## 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 # => "" ``` ## Clearing Output for Specified Inspect Method Pass `nil` to any inspect method type to not display it: ```ruby class MyObject include ObjectInspector::InspectorsHelper def inspect_identification; "My Object" end def inspect_info; "INFO" end def inspect_flags; "FLAG1" end def inspect_issues; "ISSUE1" end end MyObject.new.inspect # => "" MyObject.new.inspect(info: nil, flags: nil, issues: nil) # => "" MyObject.new.inspect(identification: nil, info: nil, flags: nil, issues: nil) # => "" ``` ## Custom Formatters A custom inspect formatter can be defined by implementing the interface defined by [ObjectInspector::BaseFormatter](https://github.com/pdobb/object_inspector/blob/master/lib/object_inspector/formatters/base_formatter.rb). Then, either override the ObjectInspector::Configuration#formatter_class value (see [Configuration](#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](https://github.com/pdobb/object_identifier) 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 # => "" ``` ## 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](https://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](https://opensource.org/licenses/MIT). [ObjectInspector::TemplatingFormatter]: https://github.com/pdobb/object_inspector/blob/master/lib/object_inspector/formatters/templating_formatter.rb [ObjectInspector::CombiningFormatter]: https://github.com/pdobb/object_inspector/blob/master/lib/object_inspector/formatters/combining_formatter.rb [ObjectInspector Benchmarking Scripts]: https://github.com/pdobb/object_inspector/blob/master/scripts/benchmarking/object_inspector.rb [Formatters Benchmarking Scripts]: https://github.com/pdobb/object_inspector/blob/master/scripts/benchmarking/formatters.rb