Lab42::OpenMap
OpenMap = OpenStruct with a rich Map API
N.B. All these code examples are verified with the speculate_about gem
Context Quick Starting Guide
Given an OpenMap
require "lab42/open_map/include" # aliases Lab42::OpenMap as OpenMap
let(:pet) { OpenMap.new }
Then we can see that it can be empty
expect( pet ).to be_empty
And that we can add fields like to a hash
pet[:name] = "Furfur"
expect( pet[:name] ).to eq("Furfur")
However we are not a hash.
Example: Only Symbol Keys please
expect{ pet["name"] = nil }.to raise_error(ArgumentError, %{"name" is not a symbol})
And that holds for construction too
expect{ OpenMap.new("verbose" => false) }
.to raise_error(ArgumentError, %{the following keys are not symbols: ["verbose"]})
But many Hash
methods are applicable to OpenMap
Given that
let (:dog) {OpenMap.new(name: "Rantanplan", breed: "You are kidding?")}
Then we can access it like a hash
expect( dog.keys ).to eq(%i[name breed])
expect( dog.values ).to eq(["Rantanplan", "You are kidding?"])
expect( dog.size ).to eq(2)
expect( dog.map.to_a ).to eq([[:name, "Rantanplan"], [:breed, "You are kidding?"]])
And we can use slice and get a nice counterpart without
expect( dog.slice(:name) ).to eq(name: "Rantanplan")
expect( dog.slice(:name, :breed) ).to eq(name: "Rantanplan", breed: "You are kidding?")
expect( dog.without(:breed) ).to eq(name: "Rantanplan")
expect( dog.without(:name, :breed) ).to eq({})
Example: each_pair
expect( dog.each_pair.to_a ).to eq([[:name, "Rantanplan"],[ :breed, "You are kidding?"]])
each_pair
is important for the following to work
Given we have OpenStruct
require "ostruct"
Then we can create one from an OpenMap
struct_dog = OpenStruct.new(dog)
expect( struct_dog.name ).to eq("Rantanplan")
And last, but certainly not least: Named Access
expect(dog.name).to eq("Rantanplan")
dog.breed = "still unknown"
expect( dog.values ).to eq(["Rantanplan", "still unknown"])
Context All About Named Access
Given the same dog again
let (:dog) {OpenMap.new(name: "Rantanplan", breed: "You are kidding?")}
Then we cannot access a nonexistant field by name
expect{ dog.age }.to raise_error(NoMethodError, %r{\Aundefined method `age' for})
And we cannot create a new one either
expect{ dog.age = 10 }.to raise_error(NoMethodError, %r{\Aundefined method `age' for})
But we still can create new fields with []=
or update
dog.update( age: 10 )
expect( dog.age ).to eq(10)
And of course the update
method preserves our symbol keys only property
expect{ dog.update("verbose" => true) }
.to raise_error(ArgumentError, %{the following keys are not symbols: ["verbose"]})
Context Methods that create new OpenMap
objects
Given a cat now, cannot risk losing half of the pet loving community ;)
let(:garfield) {OpenMap.new(name: "Garfield", yob: 1976, creator: "Jim Davis")} # Yes under a differnt name, but still
let!(:nermal) { garfield.merge(name: "Nermal", yob: 1979)}
Then exactly the following can be certified
expect( nermal ).to be_kind_of(OpenMap)
expect( garfield.values ).to eq(["Garfield", 1976, "Jim Davis"])
expect( nermal.values ).to eq(["Nermal", 1979, "Jim Davis"])
We also have a counterpart to without
, called sans
And with sans
we get this
partial_garfield = garfield.sans(:yob, :creator)
expect( partial_garfield.to_h ).to eq(name: "Garfield")
expect( garfield.values ).to eq(["Garfield", 1976, "Jim Davis"])
Context Hash like Protocol
We have already seen that []
, slice
, size
and friends act like on hashes, let us document
the other Hashlike methods here
Given a nice little OpenMap
let(:my_map) { OpenMap.new(street: "Champs Elysée", city: "Paris", country: "France") }
fetch
Then we can fetch existing values
expect( my_map.fetch(:city) ).to eq("Paris")
And we have to be a little bit more careful with non existing values
expect( my_map.fetch(:zip, 75008) ).to eq(75008)
expect( my_map.fetch(:number) { 42 } ).to eq(42)
expect{ my_map.fetch(:continent) }.to raise_error(KeyError, "key not found: :continent" )
Pattern Matching with deconstruct_keys
And we can pattern match
my_map in {city: city, street: street}
expect( [street, city] ).to eq(["Champs Elysée", "Paris"])
expect{ my_map in {city: "Bordeaux"} }.to raise_error(NoMatchingPatternError)
entries
And with not much to say about
expect( my_map.entries ).to eq([[:street, "Champs Elysée"], [:city, "Paris"], [:country, "France"]])
LICENSE
Copyright 2020 Robert Dober [email protected]
Apache-2.0 c.f LICENSE