Gem Version

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