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.
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
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 # => 2Method 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"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 "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 # => 1merge 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)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"store = Flexor.new({ a: 1, b: 2, c: 3 })
store.delete(:b) # => 2
store.to_h # => { a: 1, c: 3 }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 # => Hashto_h recursively converts back to plain hashes. Round-trips are lossless:
original = { users: [{ name: "Bob" }], meta: { version: 1 } }
Flexor.new(original).to_h == original # => trueAutovivified-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" }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"
endArray patterns via deconstruct:
point = Flexor.new({ x: 3, y: 4 })
case point
in [Integer => x, Integer => y]
puts "Point(#{x}, #{y})"
endstore = 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 }a = Flexor.new({ x: 1 })
b = Flexor.new({ x: 1 })
a == b # => true
a == { x: 1 } # => true
Flexor.new.nil? # => truestore = Flexor.new({ locked: true })
store.freeze
store.locked # => true
store.new_key = 1 # => FrozenErrordup and clone create shallow copies:
original = Flexor.new({ a: 1 })
copy = original.dup
copy.b = 2
original.key?(:b) # => falseAfter 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.
The gem is available as open source under the terms of the MIT License.