wire
Dependency injection in the spirit of google/wire: named providers, interface bindings, cleanups and a graph check for missing dependencies and cycles.
import wire
wire wires the layers of a program together: a repository, a service that uses it, a handler that uses the service. You register one provider per component, name what each one needs, and ask for the top of the graph. Each component is built once, and Close runs the cleanups in the reverse of the order they were built.
Salam has no runtime type information, so providers are identified by a name you choose, usually the type name. A provider is a function that takes the container and returns the component; it reads its own dependencies with Get. The second string of Provide lists those dependencies so that Build can check the whole graph before anything runs.
Providers and the graph check
Provide(c, name, needs, witness, factory) registers a provider. needs is a comma separated list of provider names, empty when there are none. witness is any value of the component's type; it only tells the compiler which type the factory returns. Build returns an empty string when the graph is sound and otherwise a message that names the problem: a missing dependency, a duplicate provider or a dependency cycle with the full path.
import wire
struct Repo:
pub rows: int = 0
end
struct Service:
pub repo: Repo = Repo {}
pub func Count(): int:
ret this.repo.rows
end
end
struct Handler:
pub svc: Service = Service {}
pub func Serve(): str:
ret "rows=" + this.svc.Count()
end
end
func main:
c := wire.New()
wire.Provide(c, "Repo", "", Repo {}, (_c: wire.Container): ret Repo { rows = 3 } end)
wire.Provide(c, "Service", "Repo", Service {}, (cx: wire.Container):
mut r := Repo {}
_ok := wire.Get(cx, "Repo", r)
ret Service { repo = r }
end)
wire.Provide(c, "Handler", "Service", Handler {}, (cx: wire.Container):
mut s := Service {}
_ok := wire.Get(cx, "Service", s)
ret Handler { svc = s }
end)
err := wire.Build(c)
if err != "":
println err
ret 1
end
mut h := Handler {}
_ok := wire.Get(c, "Handler", h)
println h.Serve()
wire.Close(c)
endGet(c, name, out) builds the component if it does not exist yet and copies it into out. It returns false and sets Error when the name is unknown or a cycle is hit at run time.
Interfaces, values and cleanups
Bind(c, "Store", "MemStore") makes the name Store resolve to the provider MemStore, which is how a service asks for an interface and gets the implementation chosen at the wiring site. Value registers a ready made value such as a configuration struct. OnClose attaches a cleanup to a provider; Close runs the cleanups of the components that were actually built, last built first, and frees the container.
Built lists the names in the order they were built and Count the number of registered names.
Reading the errors
Build only looks at the registered names and their needs lists, it never runs a factory. A sound graph gives an empty string; the other three messages are missing dependency: Service needs Repo, duplicate provider: Repo and dependency cycle: A -> B -> C -> A. Call it once at startup and stop there if the text is not empty. Get with a name nobody registered returns false and leaves missing provider: Nope in Error.
import wire
func report(label: str, c: wire.Container):
msg := wire.Build(c)
println label + ":", msg == "" ? "sound" : msg
wire.Close(c)
end
func main:
ok := wire.New()
wire.Provide(ok, "Repo", "", 0, (_c: wire.Container): ret 1 end)
report("fine", ok)
missing := wire.New()
wire.Provide(missing, "Service", "Repo", 0, (_c: wire.Container): ret 1 end)
report("missing", missing)
twice := wire.New()
wire.Provide(twice, "Repo", "", 0, (_c: wire.Container): ret 1 end)
wire.Provide(twice, "Repo", "", 0, (_c: wire.Container): ret 2 end)
report("duplicate", twice)
loop := wire.New()
wire.Provide(loop, "A", "B", 0, (_c: wire.Container): ret 1 end)
wire.Provide(loop, "B", "C", 0, (_c: wire.Container): ret 2 end)
wire.Provide(loop, "C", "A", 0, (_c: wire.Container): ret 3 end)
report("cycle", loop)
empty := wire.New()
mut n := 0
println "get:", wire.Get(empty, "Nope", n), wire.Error(empty)
wire.Close(empty)
endChoosing an implementation at the wiring site
Put the choice in one function that builds the container. Here Value stores the configuration, two providers make a Store in different ways, and Bind points the name Store at one of them depending on the environment. Code that asks for Store does not change. Only the components that something actually asked for are built: Built lists 1 name in the test container and 2 in the production one, out of 4 registered.
import wire
struct Config:
pub env: str = ""
end
struct Store:
pub kind: str = ""
end
func build(env: str): wire.Container:
c := wire.New()
wire.Value(c, "Config", Config { env = env })
wire.Provide(c, "Mem", "", Store {}, (_c: wire.Container): ret Store { kind = "memory" } end)
wire.Provide(c, "Disk", "Config", Store {}, (cx: wire.Container):
mut cfg := Config {}
_ok := wire.Get(cx, "Config", cfg)
ret Store { kind = "disk for " + cfg.env }
end)
if env == "test":
wire.Bind(c, "Store", "Mem")
else:
wire.Bind(c, "Store", "Disk")
end
ret c
end
func main:
for_env := ["test", "prod"]
each env in for_env:
c := build(env)
println env + ":", wire.Build(c) == "" ? "sound" : "broken"
mut s := Store {}
_ok := wire.Get(c, "Store", s)
println " store is", s.kind
built := wire.Built(c)
println " built", built.len(), "of", wire.Count(c)
built.free()
wire.Close(c)
end
endBuild once, close in reverse
Every provider is a singleton: asking twice for Cache runs its factory once and hands out copies of the same value. The cleanups registered with OnClose run from the last built component to the first, so the cache is closed before the database it holds. A second Close does nothing.
import wire
mut made := 0
struct Db:
pub id: int = 0
end
struct Cache:
pub db: Db = Db {}
end
func main:
c := wire.New()
wire.Provide(c, "Db", "", Db {}, (_c: wire.Container):
made += 1
ret Db { id = 7 }
end)
wire.Provide(c, "Cache", "Db", Cache {}, (cx: wire.Container):
mut d := Db {}
_ok := wire.Get(cx, "Db", d)
ret Cache { db = d }
end)
wire.OnClose(c, "Db", Db {}, (d: Db): println "closing db", d.id end)
wire.OnClose(c, "Cache", Cache {}, (k: Cache): println "closing cache over", k.db.id end)
before := wire.Built(c)
println "before", before.len()
before.free()
mut a := Cache {}
mut b := Cache {}
_x := wire.Get(c, "Cache", a)
_y := wire.Get(c, "Cache", b)
println "factory runs", made
built := wire.Built(c)
println "order", built.get(0), built.get(1)
built.free()
wire.Close(c)
wire.Close(c)
println "done"
endTypes
Container
struct Container| Name | Type | Description |
|---|---|---|
st | State* | The shared state of the container. |
Functions
New
func New(): Containerimport wire
func main:
c := wire.New()
wire.Provide(c, "Base", "", 0, (_c: wire.Container): ret 10 end)
wire.Provide(c, "Double", "Base", 0, (cx: wire.Container):
mut b := 0
_ok := wire.Get(cx, "Base", b)
ret b * 2
end)
mut d := 0
if wire.Get(c, "Double", d):
println d
end
wire.Close(c)
endProvide
func Provide<T>(c: Container, name: str, needs: str, _witness: T, factory: func(Container): T)needs is a comma separated list of provider names, the witness is any value of the component type and only fixes T, and factory builds the component.The example for this item is shown with the ones above: wire-Provide.salam
Value
func Value<T>(c: Container, name: str, v: T)import wire
struct Config:
pub port: int = 0
end
func main:
c := wire.New()
wire.Value(c, "Config", Config { port = 8080 })
mut cfg := Config {}
if wire.Get(c, "Config", cfg):
println cfg.port
end
wire.Close(c)
endBind
func Bind(c: Container, iface: str, concrete: str)iface resolve to the provider concrete.import wire
interface Greeter:
func Hello(): str
end
struct English:
pub func Hello(): str:
ret "hello"
end
end
func main:
c := wire.New()
wire.Provide(c, "English", "", English {}, (_c: wire.Container): ret English {} end)
wire.Bind(c, "Greeter", "English")
mut g := English {}
if wire.Get(c, "Greeter", g):
println g.Hello()
end
wire.Close(c)
endOnClose
func OnClose<T>(c: Container, name: str, _witness: T, f: func(T))Close calls it if the component was built.import wire
struct Conn:
pub id: int = 0
end
func main:
c := wire.New()
wire.Provide(c, "Conn", "", Conn {}, (_c: wire.Container): ret Conn { id = 7 } end)
wire.OnClose(c, "Conn", Conn {}, (v: Conn): println "closing", v.id end)
mut x := Conn {}
_ok := wire.Get(c, "Conn", x)
wire.Close(c)
endBuild
func Build(c: Container): strimport wire
func main:
c := wire.New()
wire.Provide(c, "Service", "Repo", 0, (_c: wire.Container): ret 1 end)
println wire.Build(c)
wire.Provide(c, "Repo", "Service", 0, (_c: wire.Container): ret 2 end)
println wire.Build(c)
wire.Close(c)
endGet
func Get<T>(c: Container, name: str, out &: T): boolout. Returns false and sets Error when the name is unknown or a cycle is reached.The example for this item is shown with the ones above: wire-Provide.salam
Error
func Error(c: Container): strCount
func Count(c: Container): intBuilt
func Built(c: Container): Vector<str>Close
func Close(c: Container)The example for this item is shown with the ones above: wire-OnClose.salam