Ver código fonte

forgot validation file

Adam Shaw 7 anos atrás
pai
commit
49a56fc6e9
1 arquivos alterados com 236 adições e 0 exclusões
  1. 236 0
      src/validation.ts

+ 236 - 0
src/validation.ts

@@ -0,0 +1,236 @@
+import { EventStore, getRelatedEvents, expandRecurring, getStoreRange } from './structs/event-store'
+import Calendar from './Calendar'
+import { DateSpan, parseOpenDateSpan, OpenDateSpanInput, isDateSpanPropsWithin, isDateSpanPropsEqual, OpenDateSpan } from './structs/date-span'
+import { EventInstance, EventDef, EventTuple } from './structs/event'
+import { EventSource, EventSourceHash } from './structs/event-source'
+import { rangeContainsRange, rangesIntersect } from './datelib/date-range'
+import { DateEnv } from './datelib/env'
+import { CalendarState } from './reducers/types'
+import EventApi from './api/EventApi'
+
+// TODO: rename to "criteria" ?
+export type ConstraintInput = 'businessHours' | string | OpenDateSpanInput
+export type Constraint = 'businessHours' | string | OpenDateSpan
+export type Overlap = boolean | ((stillEvent: EventApi, movingEvent: EventApi | null) => boolean)
+export type Allow = (span: DateSpan, movingEvent: EventApi | null) => boolean
+
+interface ValidationEntity {
+  dateSpan: DateSpan
+  event: EventTuple | null
+  constraint: Constraint | null // in addition to calendar's
+  overlap: boolean | null // in addition to calendar's. granular entities can't provide functions
+  allow: Allow | null // in addition to calendar's
+}
+
+export function isEventsValid(eventStore: EventStore, calendar: Calendar): boolean {
+  return isEntitiesValid(
+    eventStoreToEntities(eventStore, calendar.state.eventSources),
+    normalizeConstraint(calendar.opt('eventConstraint'), calendar.dateEnv),
+    calendar.opt('eventOverlap'),
+    calendar.opt('eventAllow'),
+    calendar
+  )
+}
+
+export function isSelectionValid(selection: DateSpan, calendar: Calendar): boolean {
+  return isEntitiesValid(
+    [ { dateSpan: selection, event: null, constraint: null, overlap: null, allow: null } ],
+    normalizeConstraint(calendar.opt('selectConstraint'), calendar.dateEnv),
+    calendar.opt('selectOverlap'),
+    calendar.opt('selectAllow'),
+    calendar
+  )
+}
+
+function isEntitiesValid(
+  entities: ValidationEntity[],
+  globalConstraint: Constraint | null,
+  globalOverlap: Overlap | null,
+  globalAllow: Allow | null,
+  calendar: Calendar
+): boolean {
+  let state = calendar.state
+
+  for (let entity of entities) {
+    if (
+      !isDateSpanWithinConstraint(entity.dateSpan, entity.constraint, calendar) ||
+      !isDateSpanWithinConstraint(entity.dateSpan, globalConstraint, calendar)
+    ) {
+      return false
+    }
+  }
+
+  let eventEntities = eventStoreToEntities(state.eventStore, state.eventSources)
+
+  for (let subjectEntity of entities) {
+    for (let eventEntity of eventEntities) {
+      if (dateSpansCollide(subjectEntity.dateSpan, eventEntity.dateSpan)) {
+        if (
+          subjectEntity.overlap === false ||
+          eventEntity.overlap === false ||
+          !isOverlapValid(eventEntity.event, subjectEntity.event, globalOverlap, calendar)
+        ) {
+          return false
+        }
+      }
+    }
+  }
+
+  for (let entity of entities) {
+    if (
+      !isDateSpanAllowed(entity.dateSpan, entity.event, entity.allow, calendar) ||
+      !isDateSpanAllowed(entity.dateSpan, entity.event, globalAllow, calendar)
+    ) {
+      return false
+    }
+  }
+
+  return true
+}
+
+function eventStoreToEntities(eventStore: EventStore, eventSources: EventSourceHash): ValidationEntity[] {
+  return mapEventInstances(
+    eventStore, eventSources,
+    function(eventInstance: EventInstance, eventDef: EventDef, eventSource: EventSource): ValidationEntity {
+      let constraint = eventDef.constraint as Constraint
+      let overlap = eventDef.overlap as boolean
+
+      if (constraint == null && eventSource) {
+        constraint = eventSource.constraint
+      }
+
+      if (overlap == null && eventSource) {
+        overlap = eventSource.overlap
+
+        if (overlap == null) {
+          overlap = true
+        }
+      }
+
+      return {
+        dateSpan: eventToDateSpan(eventDef, eventInstance),
+        event: { def: eventDef, instance: eventInstance },
+        constraint,
+        overlap,
+        allow: eventSource ? eventSource.allow : null
+      }
+    }
+  )
+}
+
+function mapEventInstances(
+  eventStore: EventStore,
+  eventSources: EventSourceHash,
+  callback: (instance: EventInstance, def: EventDef, source: EventSource | null) => any
+): any[] {
+  let { defs, instances } = eventStore
+  let res = []
+
+  for (let instanceId in instances) {
+    let instance = instances[instanceId]
+    let def = defs[instance.defId]
+    let source = eventSources[def.sourceId] || null
+
+    res.push(callback(instance, def, source))
+  }
+
+  return res
+}
+
+function isDateSpanWithinConstraint(subjectSpan: DateSpan, constraint: any, calendar: Calendar): boolean {
+  let constrainingSpans: DateSpan[] = constraintToSpans(constraint, subjectSpan, calendar)
+
+  for (let constrainingSpan of constrainingSpans) {
+    if (!dateSpanContainsOther(constrainingSpan, subjectSpan)) {
+      return false
+    }
+  }
+
+  return true
+}
+
+function constraintToSpans(constraint: Constraint, subjectSpan: DateSpan, calendar: Calendar): DateSpan[] {
+
+  if (constraint === 'businessHours') {
+    let store = getPeerBusinessHours(subjectSpan, calendar.state)
+    store = expandRecurring(store, getStoreRange(store), calendar)
+    return eventStoreToDateSpans(store)
+
+  } else if (typeof constraint === 'string') { // an ID
+    let store = getRelatedEvents(calendar.state.eventStore, constraint)
+    return eventStoreToDateSpans(store)
+
+  } else if (typeof constraint === 'object' && constraint) { // non-null object
+    let parsedSpan = parseOpenDateSpan(constraint, calendar.dateEnv)
+    if (parsedSpan) {
+      return [ parsedSpan ]
+    }
+  }
+
+  return []
+}
+
+function isOverlapValid(still: EventTuple, moving: EventTuple | null, overlap: Overlap | null, calendar: Calendar): boolean {
+  if (typeof overlap === 'boolean') {
+    return overlap
+  } else if (typeof overlap === 'function') {
+    return Boolean(
+      overlap(
+        new EventApi(calendar, still.def, still.instance),
+        moving ? new EventApi(calendar, moving.def, moving.instance) : null
+      )
+    )
+  }
+
+  return true
+}
+
+function isDateSpanAllowed(dateSpan: DateSpan, moving: EventTuple | null, allow: Allow | null, calendar: Calendar): boolean {
+  if (typeof allow === 'function') {
+    return Boolean(
+      allow(
+        dateSpan,
+        moving ? new EventApi(calendar, moving.def, moving.instance) : null
+      )
+    )
+  }
+
+  return true
+}
+
+function dateSpansCollide(span0: DateSpan, span1: DateSpan): boolean {
+  return rangesIntersect(span0.range, span1.range) && isDateSpanPropsEqual(span0, span1)
+}
+
+function dateSpanContainsOther(outerSpan: DateSpan, subjectSpan: DateSpan): boolean {
+  return rangeContainsRange(outerSpan.range, subjectSpan.range) && isDateSpanPropsWithin(subjectSpan, outerSpan)
+}
+
+function eventStoreToDateSpans(store: EventStore): DateSpan[] {
+  return mapEventInstances(store, {}, function(instance: EventInstance, def: EventDef) {
+    return eventToDateSpan(def, instance)
+  })
+}
+
+// TODO: plugin
+function eventToDateSpan(def: EventDef, instance: EventInstance): DateSpan {
+  return {
+    isAllDay: def.isAllDay,
+    range: instance.range
+  }
+}
+
+// TODO: plugin
+function getPeerBusinessHours(subjectSpan: DateSpan, state: CalendarState): EventStore {
+  return state.eventStore
+}
+
+export function normalizeConstraint(input: ConstraintInput, dateEnv: DateEnv): Constraint {
+  if (typeof input === 'object' && input) { // non-null object
+    return parseOpenDateSpan(input, dateEnv)
+  } else if (input != null) {
+    return String(input)
+  } else {
+    return null
+  }
+}