A resource — a named thing that can be allocated to a session.
People and rooms are the same kind of thing here; only their
attributes differ. A room has seats and video_conferencing, a
person has skills and access needs, and both sit somewhere in the
Agenda.Place tree.
Two fields carry more weight than they appear to:
requires— attributes this resource demands of whatever it is allocated alongside.step_free_access: trueon a person is not a fact about their availability; it is a constraint they impose on the room. Modelling it here means accessibility cannot be forgotten at the call site.concurrency— how many sessions may hold this resource at once. This is notseats. A 200-seat lecture hall isseats: 200, concurrency: 1; a bank of twenty identical lockers isseats: 1, concurrency: 20. Conflating the two is what lets a hall accept two simultaneous lectures.limits— how much it may be claimed over a period, as[day: 1, week: 5]. Concurrency asks how many claims may overlap at an instant; a limit asks how much falls inside a stretch of calendar, however far apart. A nurse who may work one shift a day and five a week isconcurrency: 1, limits: [day: 1, week: 5]— the concurrency stops two at once, the limits make it a contract.A limit may measure time instead of claims, and may set a floor as well as a ceiling —
limits: [day: ~o"PT7H36M", week: [at_least: ~o"PT38H"]]. SeeAgenda.Limit, which also explains why only ceilings constrain the search.
new/2 builds one. attribute/2 reads an attribute, cutype/1 and
resource_type/1 its calendar user type, within?/2 and
separation/2 place it in the place tree, and fetch/2 and
fetch_all/2 find resources in a pool by name.
Calendar user type
Two conventional attributes say what a resource is to a calendar, so that an iCalendar adapter maps it mechanically instead of guessing from its name or its other attributes:
cutype:— RFC 5545'sCUTYPE, for anything that can accept an invitation as anATTENDEE::individual,:group,:resourceor:room.resource_type:— RFC 9073'sRESOURCE-TYPE, for aVRESOURCE::room,:projector,:remote_conference_audioor:remote_conference_video.
Both are ordinary attributes, so a session can ask for them like any
other (needs(:kit, resource_type: :projector)), and a value outside
these lists is kept as given, as iCalendar allows its own extension
names. cutype/1 and resource_type/1 read them.
An Agenda resource is never RFC 5545's RESOURCES property. That
is free text on an event: it cannot be booked and cannot be checked
for conflicts, and the likeness of the names is the reason to say so.
Summary
Types
What a resource is as a calendar user: RFC 5545's CUTYPE, in
lower case. :unknown is RFC 5545's UNKNOWN, for a resource that
does not say.
What kind of equipment or space a resource is: RFC 9073's
RESOURCE-TYPE, in lower case with underscores.
A named, allocatable thing.
Functions
The value of name on resource, or nil when the resource does
not carry that attribute.
What resource is as a calendar user, iCalendar's CUTYPE.
The resource in pool with this name.
Every resource in pool named by names, in the order given.
Build a resource.
What kind of equipment or space resource is, RFC 9073's
RESOURCE-TYPE.
How far apart two resources are in the place tree, in levels.
true when resource sits inside place, at any depth.
Types
@type cutype() :: :individual | :group | :resource | :room | :unknown
What a resource is as a calendar user: RFC 5545's CUTYPE, in
lower case. :unknown is RFC 5545's UNKNOWN, for a resource that
does not say.
@type resource_type() ::
:room | :projector | :remote_conference_audio | :remote_conference_video
What kind of equipment or space a resource is: RFC 9073's
RESOURCE-TYPE, in lower case with underscores.
@type t() :: %Agenda.Resource{ attributes: %{optional(atom()) => term()}, avoids: term(), buffer_after: Tempo.Duration.t() | nil, buffer_before: Tempo.Duration.t() | nil, concurrency: pos_integer(), limits: [Agenda.Limit.t()], name: String.t(), open: Tempo.t() | Tempo.t() | nil, prefers: term(), requires: %{optional(atom()) => term()}, within: Agenda.Place.t() | nil }
A named, allocatable thing.
Functions
The value of name on resource, or nil when the resource does
not carry that attribute.
Arguments
resourceis at/0.nameis the attribute name.
Returns
- the attribute value, or
nil.
Examples
iex> boardroom = Agenda.Resource.new("Boardroom", seats: 8)
iex> {Agenda.Resource.attribute(boardroom, :seats),
...> Agenda.Resource.attribute(boardroom, :projector)}
{8, nil}
What resource is as a calendar user, iCalendar's CUTYPE.
Read from the cutype: attribute. A resource that does not carry
one is :unknown, RFC 5545's own word for it: Agenda does not guess
that a resource with seats is a room.
Arguments
resourceis at/0.
Returns
the
cutype:attribute, acutype/0or another value as it was given, or:unknownwhen the resource has none.
Examples
iex> boardroom = Agenda.Resource.new("Boardroom", cutype: :room, seats: 8)
iex> Agenda.Resource.cutype(boardroom)
:room
iex> Agenda.Resource.cutype(Agenda.Resource.new("Annexe", seats: 4))
:unknown
The resource in pool with this name.
A resource's name is its identity: the arranger, the ledger and the
fixpoint bridge all decide "is this the same resource?" by comparing
names. Looking one up is therefore an ordinary thing to want, and
doing it by hand — Enum.find/2, or a map built at the call site —
is how a mistyped name becomes a silently missing resource instead
of an error.
Arguments
poolis a list oft/0.nameis the resource name to find.
Returns
{:ok, resource}, or{:error, {:unknown_resource, name}}.
Examples
iex> pool = [Agenda.Resource.new("Boardroom"), Agenda.Resource.new("Annexe")]
iex> {:ok, resource} = Agenda.Resource.fetch(pool, "Annexe")
iex> resource.name
"Annexe"
iex> pool = [Agenda.Resource.new("Boardroom")]
iex> Agenda.Resource.fetch(pool, "Baordroom")
{:error, {:unknown_resource, "Baordroom"}}
Every resource in pool named by names, in the order given.
All or nothing: an unknown name fails the call rather than returning a shorter list. A roster that quietly loses a member is the failure this exists to prevent — the session still runs, without the person it named. Every unknown name is reported, not just the first.
Arguments
poolis a list oft/0.namesis a list of resource names.
Returns
{:ok, resources}in the order the names were given, or{:error, {:unknown_resources, names}}listing every name that is not in the pool, in the order given.
Examples
iex> pool = [Agenda.Resource.new("Ann"), Agenda.Resource.new("Bo")]
iex> {:ok, resources} = Agenda.Resource.fetch_all(pool, ["Bo", "Ann"])
iex> Enum.map(resources, & &1.name)
["Bo", "Ann"]
iex> pool = [Agenda.Resource.new("Ann")]
iex> Agenda.Resource.fetch_all(pool, ["Ann", "Bo", "Cee"])
{:error, {:unknown_resources, ["Bo", "Cee"]}}
Build a resource.
Any option that is not reserved becomes an attribute, so attributes
read as themselves at the call site rather than being nested inside
an attributes: keyword.
Arguments
nameis the resource's name.optionsis a keyword list of options.
Options
:withinis the enclosingAgenda.Place.t/0. The default isnil.:requiresis a keyword list of attributes this resource demands of resources allocated alongside it. The default is[].:concurrencyis how many sessions may hold this resource simultaneously. The default is1.:limitsbudgets the resource over a period, as[day: 1, week: 5]— at most one shift a day and five a week. Recognised periods are:day,:weekand:month. A value may be a count, a duration (~o"PT7H36M"), or a keyword list carrying:at_mostand:at_least. This is not concurrency: concurrency is how many claims may overlap at one instant, a limit is how much falls inside a stretch of calendar however far apart. Parsed on construction, so a malformed limit raises here rather than being silently unenforced. The default is none. SeeAgenda.Limit.:avoidsis when the resource would rather not be used — a Tempo value, an ISO 8601 string, or a recurrence. Unlike:openthis is a wish, not a rule: it makes a placement worse rather than invalid, and only counts when the programme declares the:resource_wishespreference. The default is none.:prefersis the mirror — when the resource would rather be used. A placement outside it is a violation. The default is none.:openis when the resource is available at all — seeAgenda.open/2, which validates it. The default isnil, meaning the resource is never open.:buffer_beforeis turnaround needed before each claim — set-up, travel in, a room being unlocked. ATempo.Duration.t/0; the default is none.:buffer_afteris turnaround needed after each claim — cleaning, resetting the room, a machine cooling down. ATempo.Duration.t/0; the default is none.every other option is taken as an attribute.
Returns
- a
t/0.
Examples
iex> boardroom = Agenda.Resource.new("Boardroom", seats: 8, video_conferencing: true)
iex> boardroom.attributes
%{seats: 8, video_conferencing: true}
iex> alice = Agenda.Resource.new("Alice", requires: [step_free_access: true])
iex> alice.requires
%{step_free_access: true}
@spec resource_type(t()) :: resource_type() | term()
What kind of equipment or space resource is, RFC 9073's
RESOURCE-TYPE.
Read from the resource_type: attribute.
Arguments
resourceis at/0.
Returns
the
resource_type:attribute, aresource_type/0or another value as it was given, ornilwhen the resource has none.
Examples
iex> projector = Agenda.Resource.new("Projector 2", cutype: :resource, resource_type: :projector)
iex> Agenda.Resource.resource_type(projector)
:projector
iex> Agenda.Resource.resource_type(Agenda.Resource.new("Alice", cutype: :individual))
nil
@spec separation(t(), t()) :: Agenda.Place.separation()
How far apart two resources are in the place tree, in levels.
Delegates to Agenda.Place.separation/2; a resource with no
place is :disjoint from everything, itself included, because
nothing can be said about the journey.
Arguments
aandbare each at/0.
Returns
0, a positive integer, or:disjoint— seeAgenda.Place.separation/2.
Examples
iex> sydney = Agenda.Place.new("Sydney Convention Centre")
iex> level_2 = Agenda.Place.new("Level 2", within: sydney)
iex> level_3 = Agenda.Place.new("Level 3", within: sydney)
iex> boardroom = Agenda.Resource.new("Boardroom", within: level_2)
iex> annexe = Agenda.Resource.new("Annexe", within: level_3)
iex> Agenda.Resource.separation(boardroom, annexe)
1
@spec within?(t(), Agenda.Place.t()) :: boolean()
true when resource sits inside place, at any depth.
A resource with no place is inside nothing.
Arguments
resourceis at/0.placeis aAgenda.Place.t/0.
Returns
trueorfalse.
Examples
iex> sydney = Agenda.Place.new("Sydney Convention Centre")
iex> level_2 = Agenda.Place.new("Level 2", within: sydney)
iex> boardroom = Agenda.Resource.new("Boardroom", within: level_2)
iex> Agenda.Resource.within?(boardroom, sydney)
true
iex> sydney = Agenda.Place.new("Sydney Convention Centre")
iex> nowhere = Agenda.Resource.new("Nowhere")
iex> Agenda.Resource.within?(nowhere, sydney)
false