adamgibbons/ics

View on GitHub
src/schema/index.js

Summary

Maintainability
A
0 mins
Test Coverage
import * as yup from 'yup'

// yup url validation blocks localhost, so use a more flexible regex instead
// taken from https://github.com/jquense/yup/issues/224#issuecomment-417172609
// This does mean that the url validation error is
// "url must match the following: ...." as opposed to "url must be a valid URL"
const urlRegex = /^(?:([a-z0-9+.-]+):\/\/)(?:\S+(?::\S*)?@)?(?:(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*\.?)(?::\d{2,5})?(?:[/?#]\S*)?$/

const dateTimeSchema = ({ required }) => yup.lazy((value) => {
  if (typeof value === 'number') {
    return yup.number().integer().min(0)
  }
  if (typeof value === 'string') {
    return yup.string().required()
  }
  if (!required && typeof value === 'undefined') {
    return yup.mixed().oneOf([undefined])
  }

  return yup.array().required().min(3).max(7).of(yup.lazy((item, options) => {
    const itemIndex = options.parent.indexOf(options.value)
    return [
      yup.number().integer(),
      yup.number().integer().min(1).max(12),
      yup.number().integer().min(1).max(31),
      yup.number().integer().min(0).max(23),
      yup.number().integer().min(0).max(60),
      yup.number().integer().min(0).max(60)
    ][itemIndex]
  }))
  }
)

const durationSchema = yup.object().shape({
  before: yup.boolean(),//option to set before alaram
  weeks: yup.number(),
  days: yup.number(),
  hours: yup.number(),
  minutes: yup.number(),
  seconds: yup.number()
}).noUnknown()

const contactSchema = yup.object().shape({
  name: yup.string(),
  email: yup.string().email(),
  rsvp: yup.boolean(),
  dir: yup.string().matches(urlRegex),
  partstat: yup.string(),
  role: yup.string(),
  cutype: yup.string(),
  xNumGuests: yup.number()
}).noUnknown()

const organizerSchema = yup.object().shape({
  name: yup.string(),
  email: yup.string().email(),
  dir: yup.string(),
  sentBy: yup.string()
}).noUnknown()

const alarmSchema = yup.object().shape({
  action: yup.string().matches(/^(audio|display|email)$/).required(),
  trigger: yup.mixed().required(),
  description: yup.string(),
  duration: durationSchema,
  repeat: yup.number(),
  attach: yup.string(),
  attachType: yup.string(),
  summary: yup.string(),
  attendee: contactSchema,
  'x-prop': yup.mixed(),
  'iana-prop': yup.mixed()
}).noUnknown()

const headerShape = {
  productId: yup.string(),
  method: yup.string(),
  calName: yup.string()
}

const headerSchema = yup.object().shape(headerShape).noUnknown()

const eventShape = {
  summary: yup.string(),
  timestamp: dateTimeSchema({ required: false }),
  title: yup.string(),
  uid: yup.string(),
  sequence: yup.number().integer().max(2_147_483_647),
  start: dateTimeSchema({ required: true }),
  duration: durationSchema,
  startType: yup.string().matches(/^(utc|local)$/),
  startInputType: yup.string().matches(/^(utc|local)$/),
  startOutputType: yup.string().matches(/^(utc|local)$/),
  end: dateTimeSchema({ required: false }),
  endInputType: yup.string().matches(/^(utc|local)$/),
  endOutputType: yup.string().matches(/^(utc|local)$/),
  description: yup.string(),
  url: yup.string().matches(urlRegex),
  geo: yup.object().shape({lat: yup.number(), lon: yup.number()}),
  location: yup.string(),
  status: yup.string().matches(/^(TENTATIVE|CANCELLED|CONFIRMED)$/i),
  categories: yup.array().of(yup.string()),
  organizer: organizerSchema,
  attendees: yup.array().of(contactSchema),
  alarms: yup.array().of(alarmSchema),
  recurrenceRule: yup.string(),
  busyStatus: yup.string().matches(/^(TENTATIVE|FREE|BUSY|OOF)$/i),
  transp: yup.string().matches(/^(TRANSPARENT|OPAQUE)$/i),
  classification: yup.string(),
  created: dateTimeSchema({ required: false }),
  lastModified: dateTimeSchema({ required: false }),
  exclusionDates: yup.array().of(dateTimeSchema({ required: true })),
  htmlContent: yup.string()
}

const headerAndEventSchema = yup.object().shape({ ...headerShape, ...eventShape }).test('xor', `object should have end or duration (but not both)`, val => {
  const hasEnd = !!val.end
  const hasDuration = !!val.duration
  return ((hasEnd && !hasDuration) || (!hasEnd && hasDuration) || (!hasEnd && !hasDuration))
}).noUnknown()


export function validateHeader (candidate) {
  try {
    const value = headerSchema.validateSync(candidate, {abortEarly: false, strict: true})
    return {error: null, value}
  } catch (error) {
    return {error: Object.assign({}, error), value: undefined}
  }
}

export function validateHeaderAndEvent (candidate) {
  try {
    const value = headerAndEventSchema.validateSync(candidate, {abortEarly: false, strict: true})
    return {error: null, value}
  } catch (error) {
    return {error: Object.assign({}, error), value: undefined}
  }
}