Inquisitive

Predicate methods for those curious about their datastructures.

Synopsis

Inquisitive provides String, Array, and Hash subclasses with dynamic predicate methods that allow you to interrogate the most common Ruby datastructures in a readable, friendly fashion. It's the inevitable evolution of ActiveSupport's StringInquirer.

It also allows you to elegantly interrogate your ENV hash through the Inquisitive::Environment module.

Inquisitive will try to use ActiveSupport's HashWithIndifferentAccess, but if that cannot be found it will bootstrap itself with a minimal, well-tested version extracted from ActiveSupport 4.0.

Status

Version Quality Dependencies

:thumbsup: Continuous Integration Test Coverage
Master Build Status Coverage Status
Development Build Status Coverage Status

Installation

To add to your project:

$ echo "gem 'inquisitive'" >> Gemfile
$ bundle install

Otherwise:

$ gem install inquisitive

Usage

Helpers

You can coerce any object to a supported Inquisitive equivalent with the Inquisitive coercion helpers:

Inquisitive.coerce('foo').class
#=> Inquisitive::String
Inquisitive.coerce(1).class
#=> Integer

Inquisitive['foo'].class
#=> Inquisitive::String
Inquisitive[1].class
#=> Integer

Inquisitive.coerce!('foo').class
#=> Inquisitive::String
Inquisitive.coerce!(1).class
#=> NameError

You can check if any object appears to be present with the Inquisitive presence helper:

Inquisitive.present? 'foo'
#=> true
Inquisitive.present? i[foo]
#=> true
Inquisitive.present? {foo: :bar}
#=> true
Inquisitive.present? 0
#=> true
Inquisitive.present? true
#=> true

Inquisitive.present? ''
#=> false
Inquisitive.present? Array.new
#=> false
Inquisitive.present? Hash.new
#=> false
Inquisitive.present? false
#=> false
Inquisitive.present? nil
#=> false
Inquisitive.present? Inquisitive::NilClass.new
#=> false

Finally, you can check if any object is explicitly an Inquisitive object with the Inquisitive object helper:

nil_object = nil
Inquisitive.object? nil_object
#=> false
Inquisitive.object? Inquisitive[nil_object]
#=> true
Inquisitive.object? Inquisitive::NilClass.new nil_object
#=> true

string = 'foo'
Inquisitive.object? string
#=> false
Inquisitive.object? Inquisitive[string]
#=> true
Inquisitive.object? Inquisitive::String.new string
#=> true

array = i[foo]
Inquisitive.object? array
#=> false
Inquisitive.object? Inquisitive[array]
#=> true
Inquisitive.object? Inquisitive::Array.new array
#=> true

hash = {foo: :bar}
Inquisitive.object? hash
#=> false
Inquisitive.object? Inquisitive[hash]
#=> true
Inquisitive.object? Inquisitive::Hash.new hash
#=> true

String

Inquisitive::String tests equality:

environment = Inquisitive::String.new 'development'
#=> "development"
environment.development?
#=> true
environment.not.development?
#=> false

Array

Inquisitive::Array tests inclusion:

supported_databases = Inquisitive::Array.new %w[mysql postgres sqlite]
#=> ["mysql", "postgres", "sqlite"]
supported_databases.postgres?
#=> true
supported_databases.sql_server?
#=> false
supported_databases.exclude.sql_server?
#=> true

Hash

Inquisitive::Hash provides struct-like access to its values, wrapped in other inquisitive objects:

stubbed = Inquisitive::Hash.new(
  authentication: true,
  in: 'development',
  services: %w[database api],
  api: {protocol: 'https', subdomains: %w[app web db]},
  ignorable: { junk: [ "" ] }
)
#=> {"authentication"=>true,
#=>  "in"=>"development",
#=>  "services"=>["database", "api"],
#=>  "api"=>{"protocol"=>"https", "subdomains"=>["app", "web", "db"]},
#=>  "ignorable"=>{"junk"=>[""]}}

stubbed.authentication?
#=> true
stubbed.registration?
#=> false
stubbed.services?
#=> true
stubbed.api?
#=> true
stubbed.ignorable?
#=> false

stubbed.in.development?
#=> true
stubbed.in.production?
#=> false

stubbed.services.database?
#=> true
stubbed.services.sidekiq?
#=> false

stubbed.api.protocol?
#=> true
stubbed.api.protocol.http?
#=> false
stubbed.api.domains.web?
#=> true

Inquisitive::Hash also allows negation with the no method:

config = Inquisitive::Hash.new(database: 'postgres')
#=> {"database"=>"postgres"}

config.database?
#=> true
config.no.database?
#=> false
config.api?
#=> false
config.no.api?
#=> true

Empty keys and nil values become instances of Inquisitive::NilClass, which is a black-hole null object that respects the Inquisitive interface, allowing you to inquire on non-existant nested datastructures as if there was one there, negated methods included:

stubbed = Inquisitive::Hash.new
#=> {}

# We can query it as if we assumed we had:
#=> {"authentication"=>true,
#=>  "in"=>"development",
#=>  "services"=>["database", "api"],
#=>  "api"=>{"protocol"=>"https", "subdomains"=>["app", "web", "db"]}}

stubbed.authentication?
#=> false
stubbed.registration?
#=> false
stubbed.services?
#=> false
stubbed.api?
#=> false
stubbed.ignorable?
#=> false
stubbed.no.ignorable?
#=> true

stubbed.in.development?
#=> false
stubbed.in.production?
#=> false
stubbed.in.not.production?
#=> true

stubbed.services.database?
#=> false
stubbed.services.sidekiq?
#=> false
stubbed.services.exclude.sidekiq?
#=> true

stubbed.api.protocol?
#=> false
stubbed.api.no.protocol?
#=> true
stubbed.api.protocol.http?
#=> false
stubbed.api.domains.web?
#=> false

This custom Inquisitive::NilClass comes with a few caveats, read the section below to understand them.

NilClass

Inquisitive::NilClass is a black-hole null object that respects the Inquisitive interface, allowing you to inquire on non-existant nested datastructures as if there was one there, negated methods included:

nillish = Inquisitive::NilClass.new
#=> nil

nillish.nil?
#=> true
nillish.present?
#=> false


nillish.access
#=> nil
nillish.not.access
#=> true
nillish.exclude.access
#=> true
nillish.no.access
#=> true

Be warned: since Ruby doesn't allow subclassing NilClass and provides no boolean-coercion mechanism, Inquisitive::NilClass will appear truthy. I recommend using built-in predicates (stubbed.authentication? && ...), presence predicates with ActiveSupport (stubbed.authentication.present? && ...), Inquisitive's presence utility (Inquisitive.present?(stubbed.authentication) && ...) or nil predicates (stubbed.authentication.nil? || ...) in boolean chains. Also note that for Inquisitive::Hash access, stubbed.fetch(:authentication, ...) behaves as expected.

Inquisitive Environment

Inquisitive::Environment can be used in your modules and classes to more easily interrogate ENV variables with inquisitive objects:

Strings

ENV['ENVIRONMENT'] = "development"
class MyApp
  extend Inquisitive::Environment
  inquires_about 'ENVIRONMENT'
end

MyApp.environment
#=> "development"
MyApp.environment.development?
#=> true
MyApp.environment.production?
#=> false

Arrays

Arrays are recognized when environment variables contain commas:

ENV['SUPPORTED_DATABASES'] = "mysql,postgres,sqlite"
class MyApp
  extend Inquisitive::Environment
  inquires_about 'SUPPORTED_DATABASES'
end

MyApp.supported_databases
#=> ["mysql", "postgres", "sqlite"]
MyApp.supported_databases.sqlite?
#=> true
MyApp.supported_databases.sql_server?
#=> false

Hashes

Hashes are recognized when environment variables names contain double underscores:

ENV['STUB__AUTHENTICATION'] = 'true'
ENV['STUB__IN'] = "development"
ENV['STUB__SERVICES'] = "database,api"
ENV['STUB__API__PROTOCOL'] = "https"
ENV['STUB__API__SUBDOMAINS'] = "app,web,db"
class MyApp
  extend Inquisitive::Environment
  inquires_about 'STUB'
end

MyApp.stub.authentication?
#=> true
MyApp.stub.registration?
#=> false
MyApp.stub.in.development?
#=> true
MyApp.stub.in.production?
#=> false
MyApp.stub.services.exclude.sidekiq?
#=> true
MyApp.stub.services.sidekiq?
#=> false
MyApp.stub.api.protocol.http?
#=> false
MyApp.stub.api.subdomains.web?
#=> true

Naming

You can name your environment inquirers with :with:

ENV['ENVIRONMENT'] = "development"
class MyApp
  extend Inquisitive::Environment
  inquires_about 'ENVIRONMENT', with: :env
end

MyApp.env
#=> "development"
MyApp.env.development?
#=> true

MyApp.env.production?
#=> false

Presence

Environment inquirers can have explicit presence checks, circumventing a common pitfall when reasoning about environment variables. Borrowing from the example above:

ENV['STUB__AUTHENTICATION'] = 'false'
class MyApp
  extend Inquisitive::Environment
  inquires_about 'STUB'
end

MyApp.stub.authentication
#=> "false"
MyApp.stub.authentication?
#=> true
MyApp.stub.authentication.true?
#=> false

It's common to use the presence of environment variables as runtime booleans. This is frequently done by setting the environment variable to the string "true" when you want it to be true, and not at all otherwise. As demonstrated, this pattern can lead to ambiguity when the string is other values.

By default such variables will be parsed as an Inquisitive::String, so predicate methods will return true whatever their contents, as long as they exist. You can bind the predicate method tighter to an explicit value if you prefer:

ENV['STUB_AUTHENTICATION'] = 'false'
ENV['STUB_REGISTRATION'] = 'true'
class MyApp
  extend Inquisitive::Environment
  inquires_about 'STUB_AUTHENTICATION', present_if: 'true'
  inquires_about 'STUB_REGISTRATION', present_if: 'true'
end

MyApp.stub_authentication
#=> "false"
MyApp.stub_authentication?
#=> false

MyApp.stub_registration
#=> "true"
MyApp.stub_registration?
#=> true

This only works on top-level inquirers, so there's no way to get our nested MyApp.stubbed.authentication? to behave as expected. Prefer MyApp.stubbed.authentication.true? instead.

The present_if check uses === under the covers for maximum expressiveness, so you can also use it to match against regexs, classes, and other constructs.

Truthy Booleans

Inquisitive::Environment.truthy contains a regex useful for reading booleans from environment variables.

ENV['NO'] = 'no'
ENV['YES'] = 'yes'
ENV['TRUTHY'] = 'TrUe'
ENV['FALSEY'] = 'FaLsE'
ENV['BOOLEAN'] = '1'
ENV['BOOLENOPE'] = '0'
class MyCli
  extend Inquisitive::Environment
  inquires_about 'NO', present_if: truthy
  inquires_about 'YES', present_if: truthy
  inquires_about 'TRUTHY', present_if: truthy
  inquires_about 'FALSEY', present_if: truthy
  inquires_about 'BOOLEAN', present_if: truthy
  inquires_about 'BOOLENOPE', present_if: truthy
end

MyApp.no?
#=> false
MyApp.yes?
#=> true

MyApp.truthy?
#=> true
MyApp.falsey?
#=> false

MyApp.boolean?
#=> true
MyApp.boolenope?

Inquiry mode

Environment inquirers have three configurable modes, defaulting to :static.

class MyApp
  extend Inquisitive::Environment
  inquires_about 'STUB', mode: i[dynamic lazy static].sample
end
  • Dynamic

    Environment inquiries parse ENV on every invocation.

    Use if you're manipulating the environment in between invocations, so Inquisitive can pick up on new values, detect changes between string or array notation, and discover new keys for hash notation.

  • Lazy

    Environment inquiries check ENV on their first invocation, and re-use the response in future invocations.

    Use if you're loading the module with environment inquiry methods before you've finished preparing your environment.

  • Static

    Environment inquiries use the contents of ENV at the moment inquires_about was invoked.

    Use if your application is well-behaved and doesn't go mucking around with the environment at runtim.

Contributing

  1. Fork it
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create new Pull Request