Step 1's store was a bare map, so nothing stopped a
caller from passing any map, or storing keys we
never meant to allow. This step gives KV its own
type: a struct with validation at the boundary.
A struct is a map with a fixed set of fields and a
name. @enforce_keys makes construction without
those fields a compile-time error.
1
defmoduleKVdo@enforce_keys[:data,:max_keys]defstruct[:data,:max_keys]@typekey::String.t()@typet::%__MODULE__{data:%{key()=>term()},max_keys:pos_integer()}@specnew(pos_integer())::t()defnew(max_keys\\100)whenis_integer(max_keys)andmax_keys>0do%KV{data:%{},max_keys:max_keys}end# `put/3` validates, then writes. Each check returns# `:ok` or `{:error, reason}`; `with` runs them in# order and stops at the first one that doesn't match.@specput(t(),key(),term())::{:ok,t()}|{:error,atom()}defput(%KV{}=kv,key,value)dowith:ok<-check_key(key),:ok<-check_room(kv,key)do{:ok,%{kv|data:Map.put(kv.data,key,value)}}endend@specfetch(t(),key())::{:ok,term()}|{:error,:not_found}deffetch(%KV{data:data},key)docaseMap.fetch(data,key)do{:ok,value}->{:ok,value}:error->{:error,:not_found}endenddefpcheck_key(key)whenis_binary(key)andbyte_size(key)in1..64,do::okdefpcheck_key(_),do:{:error,:invalid_key}defpcheck_room(%KV{data:data,max_keys:max},key)doifmap_size(data)<maxorMap.has_key?(data,key),do::ok,else:{:error,:full}endend
The happy path chains with with too. Each step
hands the new store to the next.