Agenda.Requirement (Agenda v0.1.0)

Copy Markdown View Source

A requirement — what a session demands of the resources allocated to it.

A requirement is named, so that an arrangement can say which role a resource was chosen for, and comes in two shapes:

  • by attribute — "a room seating at least eight with video conferencing"; any resource satisfying the predicates will do.

  • by roster — "Alice, Bob and Carol"; these named resources specifically.

Both shapes are the same struct, so a session can hold a list of requirements without the caller sorting them into kinds.

Summary

Types

t()

What a session demands, under a role name.

Functions

The resources from candidates that satisfy requirement.

true when resource satisfies every attribute of requirement.

Fold the requirements induced by resources into requirement.

Combine two requirements on the same role into one.

Build a requirement from attribute predicates.

Build a requirement naming specific resources.

Every way resource fails requirement, as readable phrases.

Types

t()

@type t() :: %Agenda.Requirement{
  attributes: %{optional(atom()) => Agenda.Predicate.t()},
  name: atom(),
  roster: [Agenda.Resource.t()]
}

What a session demands, under a role name.

Functions

eligible(requirement, candidates)

@spec eligible(t(), [Agenda.Resource.t()]) :: [Agenda.Resource.t()]

The resources from candidates that satisfy requirement.

Arguments

Returns

  • the eligible resources, in the order given.

Examples

iex> import Agenda.Predicate
iex> big = Agenda.Resource.new("Boardroom", seats: 8)
iex> small = Agenda.Resource.new("Meeting room 2", seats: 4)
iex> requirement = Agenda.Requirement.new(:room, seats: at_least(8))
iex> Agenda.Requirement.eligible(requirement, [big, small]) |> Enum.map(& &1.name)
["Boardroom"]

eligible?(requirement, resource)

@spec eligible?(t(), Agenda.Resource.t()) :: boolean()

true when resource satisfies every attribute of requirement.

Arguments

Returns

  • true or false.

Examples

iex> import Agenda.Predicate
iex> boardroom = Agenda.Resource.new("Boardroom", seats: 8)
iex> requirement = Agenda.Requirement.new(:room, seats: at_least(8))
iex> Agenda.Requirement.eligible?(requirement, boardroom)
true

induce(requirement, resources)

@spec induce(t(), [Agenda.Resource.t()]) :: t()

Fold the requirements induced by resources into requirement.

This is what makes requires: on a person bind the room: allocating Alice, who requires step-free access, tightens the room requirement so that an inaccessible room stops being eligible.

Arguments

  • requirement is the t/0 to tighten.

  • resources is a list of Agenda.Resource.t/0 whose requires should be folded in.

Returns

  • a t/0 carrying the additional predicates.

Examples

iex> alice = Agenda.Resource.new("Alice", requires: [step_free_access: true])
iex> room = Agenda.Requirement.new(:room, seats: 8)
iex> tightened = Agenda.Requirement.induce(room, [alice])
iex> Map.keys(tightened.attributes) |> Enum.sort()
[:seats, :step_free_access]

merge(requirement, addition)

@spec merge(t(), t()) :: t()

Combine two requirements on the same role into one.

Naming a role twice is one demand stated in two calls, not two demands: roster(:speaker, [alice]) followed by roster(:speaker, [bob]) wants both people on stage. The alternative — keeping both requirements — cannot be honoured, because an arrangement fills each role once, so the second would quietly displace the first.

Attributes combine the way induce/2 combines them, with the later predicate winning where both name the same attribute. Rosters are concatenated, and a resource named twice is named once.

Arguments

  • requirement is the t/0 stated first.

  • addition is the t/0 stated after it, under the same name.

Returns

  • one t/0 demanding everything both demanded.

Examples

iex> alice = Agenda.Resource.new("Alice")
iex> bob = Agenda.Resource.new("Bob")
iex> first = Agenda.Requirement.roster(:speaker, [alice])
iex> second = Agenda.Requirement.roster(:speaker, [bob])
iex> Agenda.Requirement.merge(first, second).roster |> Enum.map(& &1.name)
["Alice", "Bob"]

iex> import Agenda.Predicate
iex> seating = Agenda.Requirement.new(:room, seats: at_least(250))
iex> wired = Agenda.Requirement.new(:room, projector: true)
iex> Agenda.Requirement.merge(seating, wired).attributes |> Map.keys() |> Enum.sort()
[:projector, :seats]

new(name, predicates \\ [])

@spec new(
  atom(),
  keyword()
) :: t()

Build a requirement from attribute predicates.

Bare values are coerced to Agenda.Predicate.exactly/1, so video_conferencing: true and seats: at_least(8) may be mixed freely.

Arguments

  • name is the role name, such as :room.

  • predicates is a keyword list of attribute predicates.

Returns

Examples

iex> import Agenda.Predicate
iex> requirement = Agenda.Requirement.new(:room, seats: at_least(8))
iex> requirement.attributes[:seats].op
:at_least

roster(name, resources)

@spec roster(atom(), [Agenda.Resource.t()]) :: t()

Build a requirement naming specific resources.

Arguments

  • name is the role name, such as :attendees.

  • resources is the list of required Agenda.Resource.t/0.

Returns

Examples

iex> alice = Agenda.Resource.new("Alice")
iex> requirement = Agenda.Requirement.roster(:attendees, [alice])
iex> Enum.map(requirement.roster, & &1.name)
["Alice"]

unmet(requirement, resource)

@spec unmet(t(), Agenda.Resource.t()) :: [String.t()]

Every way resource fails requirement, as readable phrases.

An empty list means the resource is eligible — so eligible?/2 is unmet/2 returning nothing, and the explanation is never computed separately from the decision.

Arguments

Returns

  • a list of phrases, one per unsatisfied attribute, empty when the resource qualifies.

Examples

iex> import Agenda.Predicate
iex> small = Agenda.Resource.new("Meeting room 2", seats: 4)
iex> requirement = Agenda.Requirement.new(:room, seats: at_least(8))
iex> Agenda.Requirement.unmet(requirement, small)
["seats is 4 — needs at least 8"]