Compile-time verified PubSub.
Usually, a broadcast and its handler are two string literals in two files, and nothing
connects them. When you rename or delete an event, dead handlers and unhandled messages
stay behind, and nothing reports them. VerifiedPubSub makes a manifest the one place
that declares each topic and event. A change that breaks a call site is then a compile
error.
The manifest
defmodule MyApp.Topics do
use VerifiedPubSub.Manifest, pubsub: MyApp.PubSub
topic :pets, ["owners", :owner_id, "pets"] do
message :adopted do
field :id, :string
field :name, :string
end
message :rehomed do
field :id, :string
end
end
end:pets is the name that call sites use. The list is the wire topic, so a change to its
segments does not change a call site. The segments are joined with :. A string is a
literal and an atom is a param. The params come from the segments, so you do not declare
them a second time.
Broadcasting
defmodule MyApp.Pets do
use VerifiedPubSub, manifest: MyApp.Topics
def create(attrs) do
# ...
broadcast!(:pets, %{owner_id: attrs.owner_id}, :adopted, payload)
end
enduse VerifiedPubSub, manifest: ... imports VerifiedPubSub.Api. The topic and the
event are normal arguments, but these are macros, so they must be literal atoms.
Because of this, a typo is a compile error. Params can be built at runtime.
A topic with no params takes no params argument, as in
broadcast!(:system, :alert, payload) and subscribe(:system). An explicit %{} also
works. When a topic takes params and the call site omits them, the compile error names
them.
A topic is always followed immediately by its params. The params only fill in the topic
segments, so {topic, params} is the address and {event, payload} is the message.
Phoenix.PubSub.broadcast/3 uses the same order: first the address, then the message.
To skip the sender, use broadcast_from!/5. It works like
Phoenix.PubSub.broadcast_from/4, and from comes first, as it does there:
broadcast_from!(self(), :pets, %{owner_id: id}, :adopted, payload)Subscribing
defmodule MyApp.Worker do
use GenServer
use VerifiedPubSub.Subscriber, manifest: MyApp.Topics
def init(owner_id) do
:ok = subscribe(:pets, %{owner_id: owner_id})
{:ok, owner_id}
end
handle_message :pets, :adopted, payload, state do
{:noreply, state}
end
ignore_message :pets, :rehomed
endA subscriber must handle or explicitly ignore each event that a subscribed topic
declares. Subscribers often need only some of the events of a topic, so
ignore_message/2 exists. Without it, the coverage check would be too strict to use.
To match on topic params, put them where broadcast!/4 takes them: right after the
topic. The leading arguments are the same in both. A broadcast builds them, and a
handler matches them:
broadcast! :pets, %{owner_id: id}, :adopted, payload
handle_message :pets, %{owner_id: owner}, :adopted, payload, stateThe params argument is optional, and a pattern can name a subset of the params. A
pattern can also match the whole %VerifiedPubSub.Message{} in the payload position, to
get the other data that the message carries.
What is and is not checked
Each of these is a compile error:
a subscriber that does not handle or ignore each event of a topic that it subscribes to
a subscriber that handles an event that the manifest does not declare, or names a topic that the manifest does not declare
duplicate topics, duplicate events on one topic, and topic segments that are not valid
two topics whose segments can match the same wire topic
a broadcast to an unknown topic, with an unknown event, or with an event of a different topic
a literal params map with missing or unexpected keys
The compiler cannot check a params map built at runtime. Instead, Map.fetch!/2 raises
KeyError for a missing key.
The library also checks payload shapes. It checks a literal payload map at compile
time. It checks a payload built at runtime on every broadcast, in every environment.
Both broadcast/4 and broadcast!/4 raise VerifiedPubSub.PayloadError. See "Payload
shapes" below.
Not checked:
- Topic param values. The coverage check counts each
{topic, event}pair. If every clause for an event matches only some param values, the event still counts as covered. A message with a different value then raisesFunctionClauseError. When clauses match on param values, end with a clause that matches any value.
Payload shapes
Each field declares a key that the payload must carry:
message :adopted do
field :id, :string
field :pet, MyApp.Pet
field :tags, {:list, :string}
field :note, :string, required: false
endA type is one of :string, :integer, :float, :boolean, :atom, :map, :list,
:any, a {:list, type} tuple, or a struct module. An unknown type is a compile error.
The payload is a map of exactly the declared fields. The library rejects undeclared
keys, so the manifest stays an accurate description of the data on the wire. For the
same reason, a struct cannot be the payload: it carries __struct__ and all of its own
keys. Put the struct in a field instead:
broadcast!(:pets, %{owner_id: id}, :adopted, %{pet: pet})required: false lets the key be absent, or present as nil.
Topic segments
A topic is a list of segments joined with :, so each param always fills a whole
segment. A literal segment cannot be empty or contain :. Each call checks the param
values. A value must not be empty and must not contain :, or the call raises
VerifiedPubSub.TopicError. The manifest also makes sure that no two topics match the
same wire topic. Together, these checks make sure that a wire topic built from params is
always the topic that its call site names. See VerifiedPubSub.Topic.
Transport
Broadcasts and subscriptions go through Phoenix.PubSub, so :pubsub names a
Phoenix.PubSub that your supervision tree starts:
children = [{Phoenix.PubSub, name: MyApp.PubSub}]This library has no adapter layer, on purpose. Phoenix.PubSub already has an adapter
behaviour, and you configure PG2, Redis, and other transports there. A wrapper here
would repeat an extension point that the layer below already has. You would then
configure the transport in two places.
Summary
Functions
Imports the atom-first macros in VerifiedPubSub.Api, bound to manifest.
Functions
Imports the atom-first macros in VerifiedPubSub.Api, bound to manifest.