Flexor
A Hash-like data store that does what you tell it to do.
Flexor gives you autovivifying nested access, nil-safe chaining, and seamless conversion between hashes and method-style access. Built for spikes, prototyping, and anywhere you need a flexible data container without upfront schema design.
Installation
Install the gem and add to the application's Gemfile by executing:
$ bundle add flexor
If bundler is not being used to manage dependencies, install the gem by executing:
$ gem install flexor
Usage
Construction
From a hash:
store = Flexor.new({ user: { name: "Alice", address: { city: "NYC" } } })
store.user.name # => "Alice"
store.user.address.city # => "NYC"
From JSON:
store = Flexor.from_json('{"api": {"version": 2}}')
store.api.version # => 2
Accessing Properties
Method access and bracket access are interchangeable:
store = Flexor.new({ name: "Alice" })
store.name # => "Alice"
store[:name] # => "Alice"
Nested chaining works to any depth:
store = Flexor.new
store.config.database.host = "localhost"
store.config.database.port = 5432
store.config.database.host # => "localhost"
Safe Chaining
Accessing an unset property returns a nil-like Flexor instead of raising. You can chain as deep as you want without guard clauses:
store = Flexor.new
store.anything.deeply.nested.nil? # => true
store.missing.nil? # => true
store.missing.to_s # => ""
"Hello #{store.ghost}" # => "Hello "
Assignment Vivifies
When you assign a hash or array of hashes, Flexor auto-converts them so chaining continues to work:
store = Flexor.new
store.config = { db: { host: "localhost" } }
store.config.db.host # => "localhost"
store[:config].class # => Flexor
store.items = [{ id: 1 }, { id: 2 }]
store.items.first.id # => 1
Deep Merge
merge returns a new Flexor; merge! mutates in place. Both deep merge nested structures:
defaults = Flexor.new({ db: { host: "localhost", port: 5432 }, log: "info" })
overrides = { db: { port: 3306, name: "mydb" }, log: "debug" }
config = defaults.merge(overrides)
config.db.host # => "localhost" (preserved from defaults)
config.db.port # => 3306 (overridden)
config.db.name # => "mydb" (added)
config.log # => "debug" (overridden)
Serialization
to_json converts to JSON via to_h:
store = Flexor.new({ user: { name: "Alice" }, tags: ["admin"] })
store.to_json # => '{"user":{"name":"Alice"},"tags":["admin"]}'
# Round-trips with from_json
Flexor.from_json(store.to_json).user.name # => "Alice"
Deleting Keys
store = Flexor.new({ a: 1, b: 2, c: 3 })
store.delete(:b) # => 2
store.to_h # => { a: 1, c: 3 }
Raw Storage
If you need to store a raw Hash without conversion, use set_raw:
store = Flexor.new
store.set_raw(:headers, { "Content-Type" => "application/json" })
store[:headers].class # => Hash
Converting Back
to_h recursively converts back to plain hashes. Round-trips are lossless:
original = { users: [{ name: "Bob" }], meta: { version: 1 } }
Flexor.new(original).to_h == original # => true
Autovivified-but-never-written paths don't appear in to_h:
store = Flexor.new({ real: "data" })
store.phantom.deep.chain # read-only access
store.to_h # => { real: "data" }
Pattern Matching
Hash patterns via deconstruct_keys:
config = Flexor.new({ db: { host: "pg", port: 5432 }, cache: "redis" })
case config
in { db: { host: String => host }, cache: "redis" }
puts "db host=#{host}, cache=redis"
end
Array patterns via deconstruct:
point = Flexor.new({ x: 3, y: 4 })
case point
in [Integer => x, Integer => y]
puts "Point(#{x}, #{y})"
end
Hash-like Methods
store = Flexor.new({ a: 1, b: 2, c: 3 })
store.keys # => [:a, :b, :c]
store.values # => [1, 2, 3]
store.size # => 3
store.empty? # => false
store.key?(:a) # => true
store.each { |k, v| puts "#{k}: #{v}" }
store.map { |k, v| [k, v * 10] }
store.select { |_k, v| v > 1 }
Equality
a = Flexor.new({ x: 1 })
b = Flexor.new({ x: 1 })
a == b # => true
a == { x: 1 } # => true
Flexor.new.nil? # => true
Freezing
store = Flexor.new({ locked: true })
store.freeze
store.locked # => true
store.new_key = 1 # => FrozenError
Copying
dup and clone create shallow copies:
original = Flexor.new({ a: 1 })
copy = original.dup
copy.b = 2
original.key?(:b) # => false
Development
After checking out the repo, run bin/setup to install dependencies. Then, run rake spec to run the tests. Run rake to run both tests and rubocop.
License
The gem is available as open source under the terms of the MIT License.