toolup-forgetoolup-forge

API reference

API reference

Public surface of ToolUp.Scheduling.

ToolUp.Scheduling.Core

Identity types

type ResourceId = ResourceId of string
type BookingId = BookingId of string
type SeriesId = SeriesId of string

Resource

type Resource = {
    ResourceId: ResourceId
    Name: string
    Description: string option
    AvailabilityWindows: AvailabilityWindow list
    BlockedTimes: BlockedTime list
    SlotDurationMinutes: int
    BufferBetweenSlotsMinutes: int
}

and AvailabilityWindow = {
    Days: DayOfWeek list
    StartTime: TimeSpan
    EndTime: TimeSpan
    Timezone: string                 // IANA tz name
}

and BlockedTime = {
    Start: DateTime
    End: DateTime
    Reason: string
}

Slot

type Slot = {
    ResourceId: ResourceId
    Start: DateTime
    End: DateTime
    Status: SlotStatus
}

and SlotStatus =
    | Free
    | Booked of BookingId
    | Blocked of reason: string

Booking

type Booking = {
    BookingId: BookingId
    ResourceId: ResourceId
    Start: DateTime
    End: DateTime
    BookedBy: string
    BookedAt: DateTime
    Notes: string option
    Status: BookingStatus
    SeriesId: SeriesId option
}

and BookingStatus =
    | Confirmed
    | Cancelled of cancelledAt: DateTime

RecurrenceRule

type RecurrenceRule = {
    Frequency: Frequency
    Interval: int
    ByDayOfWeek: DayOfWeek list
    ByDayOfMonth: int list
    Count: int option
    Until: DateTime option
}

and Frequency = Daily | Weekly | Monthly | Yearly

BookingError

type BookingError =
    | OutsideAvailability
    | SlotOccupied
    | ResourceNotFound
    | Forbidden
    | InvalidTimeRange of reason: string
    | StorageError of reason: string

ISchedulingApi

type ISchedulingApi = {
    ListResources: unit -> Async<Resource list>
    GetResource: ResourceId -> Async<Resource option>
    SaveResource: Resource -> Async<Result<Resource, BookingError>>
    DeleteResource: ResourceId -> Async<Result<unit, BookingError>>

    ListSlots: ListSlotsRequest -> Async<Slot list>

    Book: BookingRequest -> Async<Result<Booking, BookingError>>
    BookSeries: BookSeriesRequest -> Async<Result<Booking list, BookingError * DateTime list>>
    Cancel: BookingId -> Async<Result<unit, BookingError>>
    GetBooking: BookingId -> Async<Booking option>
    ListBookings: ListBookingsRequest -> Async<Booking list>

    ExportICalendar: ResourceId -> Async<string>
}

and ListSlotsRequest = {
    ResourceId: ResourceId
    Start: DateTime
    End: DateTime
}

and BookingRequest = {
    ResourceId: ResourceId
    Start: DateTime
    End: DateTime
    Notes: string option
}

and BookSeriesRequest = {
    ResourceId: ResourceId
    DurationMinutes: int
    Recurrence: RecurrenceRule
    StartDate: DateTime
    StartTime: TimeSpan
    Notes: string option
}

and ListBookingsRequest = {
    ResourceId: ResourceId option
    Status: BookingStatus option
    Start: DateTime option
    End: DateTime option
    LimitTo: int option
}

RecurrenceExpander

module RecurrenceExpander =
    val expand: rule: RecurrenceRule -> startDate: DateTime -> DateTime list
    val validate: rule: RecurrenceRule -> Result<unit, RecurrenceError>

and RecurrenceError =
    | UnterminatedRecurrence         // neither Count nor Until set
    | ConflictingTermination         // both Count and Until set inconsistently
    | InvalidInterval of int          // Interval <= 0

iCalendar

module iCalendar =
    val toICalString: events: ICalEvent list -> string
    val fromBooking: booking: Booking -> resource: Resource -> ICalEvent

and ICalEvent = {
    Uid: string
    DtStart: DateTime               // UTC
    DtEnd: DateTime                 // UTC
    Summary: string
    Description: string option
    Created: DateTime
}

toICalString produces RFC 5545–compliant content. fromBooking is the standard mapping.

ToolUp.Scheduling.Server

IBookingScheduler

type IBookingScheduler =
    abstract ListResources: scopeId: string -> Async<Resource list>
    abstract GetResource: scopeId: string -> resourceId: ResourceId -> Async<Resource option>
    abstract SaveResource: scopeId: string -> resource: Resource -> Async<Result<Resource, BookingError>>
    abstract DeleteResource: scopeId: string -> resourceId: ResourceId -> Async<Result<unit, BookingError>>

    abstract ListSlots: scopeId: string -> request: ListSlotsRequest -> Async<Slot list>

    abstract Book: scopeId: string -> request: BookingRequest -> bookedBy: string -> Async<Result<Booking, BookingError>>
    abstract BookSeries: scopeId: string -> request: BookSeriesRequest -> bookedBy: string -> Async<Result<Booking list, BookingError * DateTime list>>
    abstract Cancel: scopeId: string -> bookingId: BookingId -> cancelledBy: string -> Async<Result<unit, BookingError>>
    abstract GetBooking: scopeId: string -> bookingId: BookingId -> Async<Booking option>
    abstract ListBookings: scopeId: string -> request: ListBookingsRequest -> Async<Booking list>

    abstract ExportICalendar: scopeId: string -> resourceId: ResourceId -> Async<string>

Default impl: BookingScheduler over IEntityStore. Per-ResourceId SemaphoreSlim for concurrency.

BookingConflictDetector

module BookingConflictDetector =
    val findConflicts:
        existing: Booking list ->
        candidate: BookingRequest ->
        Booking list

Pure function for conflict detection. Used internally by Book; exposed for client-side preview.

SchedulingServerApp

type SchedulingServerApp = {
    Server: ServerApp
    Resources: Resource list
}

module SchedulingServerApp =
    val fromServerApp: ServerApp -> SchedulingServerApp
    val withResource: Resource -> SchedulingServerApp -> SchedulingServerApp
    val run: SchedulingServerApp -> int

withResource registers a compose-time resource (persisted at startup to the platform scope; per-team resources persist via SaveResource at runtime).

ToolUp.Scheduling.Client

The client surface is small — no built-in calendar UI. The shipped pieces:

SchedulingClient.proxy

val proxy: ISchedulingApi

Fable.Remoting proxy. Use in Elmish commands:

Cmd.OfAsync.either (fun () -> SchedulingClient.proxy.Book request) () onSuccess onFailure

RecurrenceFormFields (Feliz components)

RecurrenceFormFields.render
    {| Rule: RecurrenceRule
       OnChange: RecurrenceRule -> unit |}

Renders the form inputs for editing a RecurrenceRule (frequency picker, day-of-week multiselect, count / until selector). Drop into any module that needs recurrence editing.

Events emitted to IEventStore

Under _platform.audit:

  • ResourceCreated
  • ResourceUpdated
  • ResourceDeleted
  • BookingCreated
  • BookingCancelled
  • BookingSeriesCreated (covers all bookings in a series; carries SeriesId)

HTTP endpoints

Auto-injected by SchedulingServerApp.run:

  • POST /api/ISchedulingApi/* — every method on the interface.
  • Optional: GET /api/scheduling/{resourceId}/calendar.ics — for direct iCalendar subscription (set up manually by your module; the SDK doesn't auto-inject this URL).

Configuration knobs

  • ServerConfig.EntityStore = EnabledEntityStore — required.
  • SchedulingServerApp.Resources (the compose-time resource list) — propagated to the storage layer at startup.

Conformance test pack

ToolUp.Scheduling.Tests ships:

  • IBookingSchedulerContract — N tests covering CRUD on resources, slot derivation, booking with concurrency, conflict detection, series booking atomicity.
  • RecurrenceExpanderTests — covers Daily / Weekly / Monthly / Yearly + Count + Until terminations + edge cases (DST transitions, leap year, year boundary).

External impls bind into the contract pack:

[<Tests>]
let tests =
    testList "MyBookingScheduler conformance" [
        yield! IBookingSchedulerContract.tests
            (fun () -> MyBookingScheduler.create() :> IBookingScheduler)
    ]