# `Agenda.Resource`
[🔗](https://github.com/elixir-tempo/agenda/blob/v0.3.0/lib/agenda/resource.ex#L1)

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: true` on 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 *not* `seats`. A 200-seat lecture hall is
  `seats: 200, concurrency: 1`; a bank of twenty identical lockers is
  `seats: 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 is `concurrency: 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"]]`. See `Agenda.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's `CUTYPE`, for anything that can accept
  an invitation as an `ATTENDEE`: `:individual`, `:group`,
  `:resource` or `:room`.

* **`resource_type:`** — RFC 9073's `RESOURCE-TYPE`, for a
  `VRESOURCE`: `:room`, `:projector`, `:remote_conference_audio` or
  `: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.

# `cutype`

```elixir
@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.

# `resource_type`

```elixir
@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.

# `t`

```elixir
@type t() :: %Agenda.Resource{
  attributes: %{optional(atom()) =&gt; 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()) =&gt; term()},
  within: Agenda.Place.t() | nil
}
```

A named, allocatable thing.

# `attribute`

```elixir
@spec attribute(t(), atom()) :: term()
```

The value of `name` on `resource`, or `nil` when the resource does
not carry that attribute.

### Arguments

* `resource` is a `t:t/0`.

* `name` is 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}

# `cutype`

```elixir
@spec cutype(t()) :: cutype() | term()
```

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

* `resource` is a `t:t/0`.

### Returns

* the `cutype:` attribute, a `t:cutype/0` or another value as it was
  given, or

* `:unknown` when 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

# `fetch`

```elixir
@spec fetch([t()], String.t()) ::
  {:ok, t()} | {:error, {:unknown_resource, String.t()}}
```

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

* `pool` is a list of `t:t/0`.

* `name` is 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"}}

# `fetch_all`

```elixir
@spec fetch_all([t()], [String.t()]) ::
  {:ok, [t()]} | {:error, {:unknown_resources, [String.t()]}}
```

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

* `pool` is a list of `t:t/0`.

* `names` is 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"]}}

# `new`

```elixir
@spec new(String.t(), keyword()) :: t()
```

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

* `name` is the resource's name.

* `options` is a keyword list of options.

### Options

* `:within` is the enclosing `t:Agenda.Place.t/0`. The
  default is `nil`.

* `:requires` is a keyword list of attributes this resource demands
  of resources allocated alongside it. The default is `[]`.

* `:concurrency` is how many sessions may hold this resource
  simultaneously. The default is `1`.

* `:limits` budgets the resource *over a period*, as
  `[day: 1, week: 5]` — at most one shift a day and five a week.
  Recognised periods are `:day`, `:week` and `:month`. A value may be
  a count, a duration (`~o"PT7H36M"`), or a keyword list carrying
  `:at_most` and `: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. See `Agenda.Limit`.

* `:avoids` is when the resource would *rather not* be used — a
  Tempo value, an ISO 8601 string, or a recurrence. Unlike `:open`
  this is a wish, not a rule: it makes a placement worse rather than
  invalid, and only counts when the programme declares the
  `:resource_wishes` preference. The default is none.

* `:prefers` is the mirror — when the resource would *rather* be
  used. A placement outside it is a violation. The default is none.

* `:open` is when the resource is available at all — see
  `Agenda.open/2`, which validates it. The default is `nil`,
  meaning the resource is never open.

* `:buffer_before` is turnaround needed before each claim — set-up,
  travel in, a room being unlocked. A `t:Tempo.Duration.t/0`; the
  default is none.

* `:buffer_after` is turnaround needed after each claim — cleaning,
  resetting the room, a machine cooling down. A
  `t:Tempo.Duration.t/0`; the default is none.

* every other option is taken as an attribute.

### Returns

* a `t: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}

# `resource_type`

```elixir
@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

* `resource` is a `t:t/0`.

### Returns

* the `resource_type:` attribute, a `t:resource_type/0` or another
  value as it was given, or

* `nil` when 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

# `separation`

```elixir
@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

* `a` and `b` are each a `t:t/0`.

### Returns

* `0`, a positive integer, or `:disjoint` — see
  `Agenda.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

# `within?`

```elixir
@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

* `resource` is a `t:t/0`.

* `place` is a `t:Agenda.Place.t/0`.

### Returns

* `true` or `false`.

### 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

---

*Consult [api-reference.md](api-reference.md) for complete listing*
