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
| :thumbsup: | Continuous Integration | Test Coverage |
|---|---|---|
| Master | ||
| Development |
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
ENVon every invocation.Use if you're manipulating the environment in between invocations, so
Inquisitivecan pick up on new values, detect changes between string or array notation, and discover new keys for hash notation.Lazy
Environment inquiries check
ENVon 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
ENVat the momentinquires_aboutwas invoked.Use if your application is well-behaved and doesn't go mucking around with the environment at runtim.
Contributing
- Fork it
- Create your feature branch (
git checkout -b my-new-feature) - Commit your changes (
git commit -am 'Add some feature') - Push to the branch (
git push origin my-new-feature) - Create new Pull Request