<commit_before>/** @babel */
import path from 'path'

import {Emitter} from 'atom'

import GitShellOutStrategy from '../git-shell-out-strategy'
import FilePatch from './file-patch'
import Hunk from './hunk'
import HunkLine from './hunk-line'
import MergeConflict from './merge-conflict'
import {readFile} from '../helpers'

const MERGE_MARKER_REGEX = /^(>|<){7} \S+$/m

export class AbortMergeError extends Error {
  constructor (code, path) {
    super()
    this.message = `${code}: ${path}.`
    this.code = code
    this.path = path
    this.stack = new Error().stack
  }
}

export class CommitError extends Error {
  constructor (code) {
    super()
    this.message = `Commit error: ${code}.`
    this.code = code
    this.stack = new Error().stack
  }
}

export default class Repository {
  static async open (workingDirectory, gitStrategy) {
    gitStrategy = gitStrategy || new GitShellOutStrategy(workingDirectory.getPath())
    if (await gitStrategy.isGitRepository(workingDirectory)) {
      return new Repository(workingDirectory, gitStrategy)
    } else {
      return null
    }
  }

  constructor (workingDirectory, gitStrategy) {
    this.workingDirectory = workingDirectory
    this.emitter = new Emitter()
    this.stagedFilePatchesByPath = new Map()
    this.stagedFilePatchesSinceParentCommitByPath = new Map()
    this.unstagedFilePatchesByPath = new Map()
    this.mergeConflictsByPath = new Map()
    this.git = gitStrategy
  }

  destroy () {
    this.emitter.dispose()
  }

  onDidUpdate (callback) {
    return this.emitter.on('did-update', callback)
  }

  didUpdate () {
    this.emitter.emit('did-update')
  }

  getWorkingDirectory () {
    return this.workingDirectory
  }

  getWorkingDirectoryPath () {
    return this.getWorkingDirectory().getRealPathSync()
  }

  getGitDirectoryPath () {
    return path.join(this.getWorkingDirectoryPath(), '.git')
  }

  async refresh () {
    if (global.PRINT_GIT_TIMES) console.time('refresh')
    this.stagedChangesPromise = this.fetchStagedChanges()
    this.stagedChangesSinceParentCommitPromise = this.fetchStagedChangesSinceParentCommit()
    this.unstagedChangesPromise = this.fetchUnstagedChanges()
    this.mergeConflictsPromise = this.fetchMergeConflicts()
    await Promise.all([
      this.stagedChangesPromise,
      this.stagedChangesSinceParentCommitPromise,
      this.unstagedChangesPromise,
      this.mergeConflictsPromise
    ])
    if (global.PRINT_GIT_TIMES) console.timeEnd('refresh')
    this.didUpdate()
  }

  getUnstagedChanges () {
    if (!this.unstagedChangesPromise) {
      this.unstagedChangesPromise = this.fetchUnstagedChanges()
    }
    return this.unstagedChangesPromise
  }

  getStagedChanges () {
    if (!this.stagedChangesPromise) {
      this.stagedChangesPromise = this.fetchStagedChanges()
    }
    return this.stagedChangesPromise
  }

  getStagedChangesSinceParentCommit () {
    if (!this.stagedChangesSinceParentCommitPromise) {
      this.stagedChangesSinceParentCommitPromise = this.fetchStagedChangesSinceParentCommit()
    }
    return this.stagedChangesSinceParentCommitPromise
  }

  getMergeConflicts () {
    if (!this.mergeConflictsPromise) {
      this.mergeConflictsPromise = this.fetchMergeConflicts()
    }
    return this.mergeConflictsPromise
  }

  async fetchUnstagedChanges () {
    const rawDiffs = await this.git.diff()
    return this.updateFilePatches(this.unstagedFilePatchesByPath, rawDiffs)
  }

  async fetchStagedChanges () {
    const rawDiffs = await this.git.diff({staged: true})
    return this.updateFilePatches(this.stagedFilePatchesByPath, rawDiffs)
  }

  async fetchStagedChangesSinceParentCommit () {
    try {
      const rawDiffs = await this.git.diff({staged: true, baseCommit: 'HEAD~'})
      return this.updateFilePatches(this.stagedFilePatchesSinceParentCommitByPath, rawDiffs)
    } catch (e) {
      if (e.message.includes(`ambiguous argument 'HEAD~'`)) {
        return []
      } else {
        throw e
      }
    }
  }

  updateFilePatches (patchesByPath, rawDiffs) {
    const filePatches = this.buildFilePatchesFromRawDiffs(rawDiffs)

    const validFilePatches = new Set()
    for (let newPatch of filePatches) {
      const path = newPatch.getPath()
      const existingPatch = patchesByPath.get(path)
      if (existingPatch == null) {
        patchesByPath.set(path, newPatch)
      } else {
        existingPatch.update(newPatch)
      }
      validFilePatches.add(path)
    }

    for (let [path, patch] of patchesByPath) {
      if (!validFilePatches.has(path)) {
        patchesByPath.delete(path)
        patch.destroy()
      }
    }

    return Array.from(patchesByPath.values()).sort((a, b) => a.getPath().localeCompare(b.getPath()))
  }

  buildFilePatchesFromRawDiffs (rawDiffs) {
    const statusMap = {
      '+': 'added',
      '-': 'deleted',
      ' ': 'unchanged'
    }
    return rawDiffs.map(patch => {
      const hunks = patch.hunks.map(hunk => {
        let oldLineNumber = hunk.oldStartLine
        let newLineNumber = hunk.newStartLine
        const hunkLines = hunk.lines.map(line => {
          let status = statusMap[line[0]]
          const text = line.slice(1)
          let hunkLine
          if (status === 'unchanged') {
            hunkLine = new HunkLine(text, status, oldLineNumber, newLineNumber)
            oldLineNumber++
            newLineNumber++
          } else if (status === 'added') {
            hunkLine = new HunkLine(text, status, -1, newLineNumber)
            newLineNumber++
          } else if (status === 'deleted') {
            hunkLine = new HunkLine(text, status, oldLineNumber, -1)
            oldLineNumber++
          } else if (status === undefined) {
            hunkLine = new HunkLine('\\' + text, status, -1, newLineNumber - 1)
          }
          return hunkLine
        })
        return new Hunk(hunk.oldStartLine, hunk.newStartLine, hunk.oldLineCount, hunk.newLineCount, hunkLines)
      })
      return new FilePatch(patch.oldPath, patch.newPath, patch.status, hunks)
    })
  }

  async fetchMergeConflicts () {
    const statusesByPath = await this.git.getMergeConflictFileStatus()
    const validConflicts = new Set()
    for (let path in statusesByPath) {
      const statuses = statusesByPath[path]
      const existingConflict = this.mergeConflictsByPath.get(path)
      if (existingConflict == null) {
        this.mergeConflictsByPath.set(path, new MergeConflict(path, statuses.ours, statuses.theirs, statuses.file))
      } else {
        existingConflict.updateFileStatus(statuses.file)
      }
      validConflicts.add(path)
    }

    for (let [path, conflict] of this.mergeConflictsByPath) {
      if (!validConflicts.has(path)) {
        this.mergeConflictsByPath.delete(path)
        conflict.destroy()
      }
    }

    return Array.from(this.mergeConflictsByPath.values())
      .sort((a, b) => a.getPath().localeCompare(b.getPath()))
  }

  async stageFiles (paths) {
    await this.git.stageFiles(paths)
    await this.refresh()
  }

  async unstageFiles (paths) {
    await this.git.unstageFiles(paths)
    await this.refresh()
  }

  async stageFilesFromParentCommit (paths) {
    await this.git.unstageFiles(paths, 'HEAD~')
    await this.refresh()
  }

  async applyPatchToIndex (filePatch) {
    const patchStr = filePatch.getHeaderString() + filePatch.toString()
    await this.git.applyPatchToIndex(patchStr)
    await this.refresh()
  }

  async pathHasMergeMarkers (relativePath) {
    try {
      const contents = await readFile(path.join(this.getWorkingDirectoryPath(), relativePath), 'utf8')
      return MERGE_MARKER_REGEX.test(contents)
    } catch (e) {
      if (e.code === 'ENOENT') return false
      else throw e
    }
  }

  getMergeHead () {
    return this.git.getMergeHead()
  }

  async getMergeMessage () {
    try {
      const contents = await readFile(path.join(this.getWorkingDirectoryPath(), '.git', 'MERGE_MSG'), 'utf8')
      return contents
    } catch (e) {
      return null
    }
  }

  async abortMerge () {
    await this.git.abortMerge()
    return this.refresh()
  }

  isMerging () {
    return this.git.isMerging()
  }

  async commit (message, options) {
    await this.git.commit(this.formatCommitmessage(message), options)
    this.stagedChangesPromise = null
    this.stagedChangesSinceParentCommitPromise = null
    this.didUpdate()
  }

  getLastCommit () {
    return this.git.getHeadCommit()
  }

  formatCommitmessage (message) {
    // strip out comments
    message = message.replace(/^#.*$/mg, '').trim()

    // hard wrap message (except for first line) at 72 characters
    let results = []
    lines = message.split('\n').map((line, index) => {
      if (line.length <= 72 || index === 0) {
        results.push(line)
      } else {
        const matches = line.match(/.{1,72}(\s|$)|\S+?(\s|$)/g)
          .map(line => {
            return line.endsWith('\n') ? line.substr(0, line.length - 1) : line
          })
        results = results.concat(matches)
      }
    })

    return results.join('\n')
  }

  readFileFromIndex (path) {
    return this.git.readFileFromIndex(path)
  }

  push (branchName) {
    return this.git.push(branchName)
  }

  fetch (branchName) {
    return this.git.fetch(branchName)
  }

  async pull (branchName) {
    await this.fetch(branchName)
    const remote = await this.git.getRemote(branchName)
    return this.git.merge(`${remote}/${branchName}`)
  }

  getAheadCount (branchName) {
    return this.git.getAheadCount(branchName)
  }

  getBehindCount (branchName) {
    return this.git.getBehindCount(branchName)
  }

  getRemote (branchName) {
    return this.git.getRemote(branchName)
  }

  getCurrentBranch () {
    return this.git.getCurrentBranch()
  }

  getBranches () {
    return this.git.getBranches()
  }

  async checkout (branchName, options) {
    await this.git.checkout(branchName, options)
    await this.refresh()
  }
}
<commit_msg>:art: method name<commit_after>/** @babel */
import path from 'path'

import {Emitter} from 'atom'

import GitShellOutStrategy from '../git-shell-out-strategy'
import FilePatch from './file-patch'
import Hunk from './hunk'
import HunkLine from './hunk-line'
import MergeConflict from './merge-conflict'
import {readFile} from '../helpers'

const MERGE_MARKER_REGEX = /^(>|<){7} \S+$/m

export class AbortMergeError extends Error {
  constructor (code, path) {
    super()
    this.message = `${code}: ${path}.`
    this.code = code
    this.path = path
    this.stack = new Error().stack
  }
}

export class CommitError extends Error {
  constructor (code) {
    super()
    this.message = `Commit error: ${code}.`
    this.code = code
    this.stack = new Error().stack
  }
}

export default class Repository {
  static async open (workingDirectory, gitStrategy) {
    gitStrategy = gitStrategy || new GitShellOutStrategy(workingDirectory.getPath())
    if (await gitStrategy.isGitRepository(workingDirectory)) {
      return new Repository(workingDirectory, gitStrategy)
    } else {
      return null
    }
  }

  constructor (workingDirectory, gitStrategy) {
    this.workingDirectory = workingDirectory
    this.emitter = new Emitter()
    this.stagedFilePatchesByPath = new Map()
    this.stagedFilePatchesSinceParentCommitByPath = new Map()
    this.unstagedFilePatchesByPath = new Map()
    this.mergeConflictsByPath = new Map()
    this.git = gitStrategy
  }

  destroy () {
    this.emitter.dispose()
  }

  onDidUpdate (callback) {
    return this.emitter.on('did-update', callback)
  }

  didUpdate () {
    this.emitter.emit('did-update')
  }

  getWorkingDirectory () {
    return this.workingDirectory
  }

  getWorkingDirectoryPath () {
    return this.getWorkingDirectory().getRealPathSync()
  }

  getGitDirectoryPath () {
    return path.join(this.getWorkingDirectoryPath(), '.git')
  }

  async refresh () {
    if (global.PRINT_GIT_TIMES) console.time('refresh')
    this.stagedChangesPromise = this.fetchStagedChanges()
    this.stagedChangesSinceParentCommitPromise = this.fetchStagedChangesSinceParentCommit()
    this.unstagedChangesPromise = this.fetchUnstagedChanges()
    this.mergeConflictsPromise = this.fetchMergeConflicts()
    await Promise.all([
      this.stagedChangesPromise,
      this.stagedChangesSinceParentCommitPromise,
      this.unstagedChangesPromise,
      this.mergeConflictsPromise
    ])
    if (global.PRINT_GIT_TIMES) console.timeEnd('refresh')
    this.didUpdate()
  }

  getUnstagedChanges () {
    if (!this.unstagedChangesPromise) {
      this.unstagedChangesPromise = this.fetchUnstagedChanges()
    }
    return this.unstagedChangesPromise
  }

  getStagedChanges () {
    if (!this.stagedChangesPromise) {
      this.stagedChangesPromise = this.fetchStagedChanges()
    }
    return this.stagedChangesPromise
  }

  getStagedChangesSinceParentCommit () {
    if (!this.stagedChangesSinceParentCommitPromise) {
      this.stagedChangesSinceParentCommitPromise = this.fetchStagedChangesSinceParentCommit()
    }
    return this.stagedChangesSinceParentCommitPromise
  }

  getMergeConflicts () {
    if (!this.mergeConflictsPromise) {
      this.mergeConflictsPromise = this.fetchMergeConflicts()
    }
    return this.mergeConflictsPromise
  }

  async fetchUnstagedChanges () {
    const rawDiffs = await this.git.diff()
    return this.updateFilePatches(this.unstagedFilePatchesByPath, rawDiffs)
  }

  async fetchStagedChanges () {
    const rawDiffs = await this.git.diff({staged: true})
    return this.updateFilePatches(this.stagedFilePatchesByPath, rawDiffs)
  }

  async fetchStagedChangesSinceParentCommit () {
    try {
      const rawDiffs = await this.git.diff({staged: true, baseCommit: 'HEAD~'})
      return this.updateFilePatches(this.stagedFilePatchesSinceParentCommitByPath, rawDiffs)
    } catch (e) {
      if (e.message.includes(`ambiguous argument 'HEAD~'`)) {
        return []
      } else {
        throw e
      }
    }
  }

  updateFilePatches (patchesByPath, rawDiffs) {
    const filePatches = this.buildFilePatchesFromRawDiffs(rawDiffs)

    const validFilePatches = new Set()
    for (let newPatch of filePatches) {
      const path = newPatch.getPath()
      const existingPatch = patchesByPath.get(path)
      if (existingPatch == null) {
        patchesByPath.set(path, newPatch)
      } else {
        existingPatch.update(newPatch)
      }
      validFilePatches.add(path)
    }

    for (let [path, patch] of patchesByPath) {
      if (!validFilePatches.has(path)) {
        patchesByPath.delete(path)
        patch.destroy()
      }
    }

    return Array.from(patchesByPath.values()).sort((a, b) => a.getPath().localeCompare(b.getPath()))
  }

  buildFilePatchesFromRawDiffs (rawDiffs) {
    const statusMap = {
      '+': 'added',
      '-': 'deleted',
      ' ': 'unchanged'
    }
    return rawDiffs.map(patch => {
      const hunks = patch.hunks.map(hunk => {
        let oldLineNumber = hunk.oldStartLine
        let newLineNumber = hunk.newStartLine
        const hunkLines = hunk.lines.map(line => {
          let status = statusMap[line[0]]
          const text = line.slice(1)
          let hunkLine
          if (status === 'unchanged') {
            hunkLine = new HunkLine(text, status, oldLineNumber, newLineNumber)
            oldLineNumber++
            newLineNumber++
          } else if (status === 'added') {
            hunkLine = new HunkLine(text, status, -1, newLineNumber)
            newLineNumber++
          } else if (status === 'deleted') {
            hunkLine = new HunkLine(text, status, oldLineNumber, -1)
            oldLineNumber++
          } else if (status === undefined) {
            hunkLine = new HunkLine('\\' + text, status, -1, newLineNumber - 1)
          }
          return hunkLine
        })
        return new Hunk(hunk.oldStartLine, hunk.newStartLine, hunk.oldLineCount, hunk.newLineCount, hunkLines)
      })
      return new FilePatch(patch.oldPath, patch.newPath, patch.status, hunks)
    })
  }

  async fetchMergeConflicts () {
    const statusesByPath = await this.git.getMergeConflictFileStatus()
    const validConflicts = new Set()
    for (let path in statusesByPath) {
      const statuses = statusesByPath[path]
      const existingConflict = this.mergeConflictsByPath.get(path)
      if (existingConflict == null) {
        this.mergeConflictsByPath.set(path, new MergeConflict(path, statuses.ours, statuses.theirs, statuses.file))
      } else {
        existingConflict.updateFileStatus(statuses.file)
      }
      validConflicts.add(path)
    }

    for (let [path, conflict] of this.mergeConflictsByPath) {
      if (!validConflicts.has(path)) {
        this.mergeConflictsByPath.delete(path)
        conflict.destroy()
      }
    }

    return Array.from(this.mergeConflictsByPath.values())
      .sort((a, b) => a.getPath().localeCompare(b.getPath()))
  }

  async stageFiles (paths) {
    await this.git.stageFiles(paths)
    await this.refresh()
  }

  async unstageFiles (paths) {
    await this.git.unstageFiles(paths)
    await this.refresh()
  }

  async stageFilesFromParentCommit (paths) {
    await this.git.unstageFiles(paths, 'HEAD~')
    await this.refresh()
  }

  async applyPatchToIndex (filePatch) {
    const patchStr = filePatch.getHeaderString() + filePatch.toString()
    await this.git.applyPatchToIndex(patchStr)
    await this.refresh()
  }

  async pathHasMergeMarkers (relativePath) {
    try {
      const contents = await readFile(path.join(this.getWorkingDirectoryPath(), relativePath), 'utf8')
      return MERGE_MARKER_REGEX.test(contents)
    } catch (e) {
      if (e.code === 'ENOENT') return false
      else throw e
    }
  }

  getMergeHead () {
    return this.git.getMergeHead()
  }

  async getMergeMessage () {
    try {
      const contents = await readFile(path.join(this.getWorkingDirectoryPath(), '.git', 'MERGE_MSG'), 'utf8')
      return contents
    } catch (e) {
      return null
    }
  }

  async abortMerge () {
    await this.git.abortMerge()
    return this.refresh()
  }

  isMerging () {
    return this.git.isMerging()
  }

  async commit (message, options) {
    await this.git.commit(this.formatCommitMessage(message), options)
    this.stagedChangesPromise = null
    this.stagedChangesSinceParentCommitPromise = null
    this.didUpdate()
  }

  getLastCommit () {
    return this.git.getHeadCommit()
  }

  formatCommitMessage (message) {
    // strip out comments
    message = message.replace(/^#.*$/mg, '').trim()

    // hard wrap message (except for first line) at 72 characters
    let results = []
    lines = message.split('\n').map((line, index) => {
      if (line.length <= 72 || index === 0) {
        results.push(line)
      } else {
        const matches = line.match(/.{1,72}(\s|$)|\S+?(\s|$)/g)
          .map(line => {
            return line.endsWith('\n') ? line.substr(0, line.length - 1) : line
          })
        results = results.concat(matches)
      }
    })

    return results.join('\n')
  }

  readFileFromIndex (path) {
    return this.git.readFileFromIndex(path)
  }

  push (branchName) {
    return this.git.push(branchName)
  }

  fetch (branchName) {
    return this.git.fetch(branchName)
  }

  async pull (branchName) {
    await this.fetch(branchName)
    const remote = await this.git.getRemote(branchName)
    return this.git.merge(`${remote}/${branchName}`)
  }

  getAheadCount (branchName) {
    return this.git.getAheadCount(branchName)
  }

  getBehindCount (branchName) {
    return this.git.getBehindCount(branchName)
  }

  getRemote (branchName) {
    return this.git.getRemote(branchName)
  }

  getCurrentBranch () {
    return this.git.getCurrentBranch()
  }

  getBranches () {
    return this.git.getBranches()
  }

  async checkout (branchName, options) {
    await this.git.checkout(branchName, options)
    await this.refresh()
  }
}
<|endoftext|>


<commit_before>import yubikiri from 'yubikiri';
import {Emitter} from 'event-kit';

import RelayNetworkLayerManager from '../relay-network-layer-manager';
import Author, {nullAuthor} from './author';
import {UNAUTHENTICATED} from '../shared/keytar-strategy';
import ModelObserver from './model-observer';

// This is a guess about what a reasonable value is. Can adjust if performance is poor.
const MAX_COMMITS = 5000;

export const source = {
  PENDING: Symbol('pending'),
  GITLOG: Symbol('git log'),
  GITHUBAPI: Symbol('github API'),
};

class GraphQLCache {
  // One hour
  static MAX_AGE_MS = 3.6e6

  constructor() {
    this.bySlug = new Map();
  }

  get(remote) {
    const slug = remote.getSlug();
    const {ts, data} = this.bySlug.get(slug) || {
      ts: -Infinity,
      data: {},
    };

    if (Date.now() - ts > this.constructor.MAX_AGE_MS) {
      this.bySlug.delete(slug);
      return null;
    }
    return data;
  }

  set(remote, data) {
    this.bySlug.set(remote.getSlug(), {ts: Date.now(), data});
  }
}

export default class UserStore {
  constructor({repository, login}) {
    this.emitter = new Emitter();

    // TODO: [ku 3/2018] Consider using Dexie (indexDB wrapper) like Desktop and persist users across sessions
    this.allUsers = new Map();
    this.users = [];
    this.committer = nullAuthor;

    this.last = {
      source: source.PENDING,
      repository: null,
    };
    this.cache = new GraphQLCache();

    this.repositoryObserver = new ModelObserver({
      fetchData: r => yubikiri({
        committer: r.getCommitter(),
        authors: r.getAuthors({max: MAX_COMMITS}),
        remotes: r.getRemotes(),
      }),
      didUpdate: () => this.loadUsers(),
    });
    this.repositoryObserver.setActiveModel(repository);

    this.loginObserver = new ModelObserver({
      didUpdate: () => this.loadUsers(),
    });
    this.loginObserver.setActiveModel(login);
  }

  async loadUsers() {
    const data = this.repositoryObserver.getActiveModelData();

    if (!data) {
      return;
    }

    this.setCommitter(data.committer);
    const githubRemotes = data.remotes.filter(remote => remote.isGithubRepo());

    if (githubRemotes.length === 0) {
      this.addUsers(data.authors, source.GITLOG);
    } else {
      await this.loadUsersFromGraphQL(githubRemotes);
    }
  }

  loadUsersFromGraphQL(remotes) {
    return Promise.all(
      remotes.map(remote => this.loadMentionableUsers(remote)),
    );
  }

  async loadMentionableUsers(remote) {
    const cached = this.cache.get(remote);
    if (cached !== null) {
      this.addUsers(cached, source.GITHUBAPI);
      return;
    }

    const loginModel = this.loginObserver.getActiveModel();
    if (!loginModel) {
      return;
    }

    const token = await loginModel.getToken('https://api.github.com');
    if (token === UNAUTHENTICATED) {
      return;
    }

    const fetchQuery = RelayNetworkLayerManager.getFetchQuery('https://api.github.com/graphql', token);

    let hasMore = true;
    let cursor = null;
    const remoteUsers = [];

    while (hasMore) {
      const response = await fetchQuery({
        name: 'GetMentionableUsers',
        text: `
          query GetMentionableUsers($owner: String!, $name: String!, $first: Int!, $after: String) {
            repository(owner: $owner, name: $name) {
              mentionableUsers(first: $first, after: $after) {
                nodes {
                  login
                  email
                  name
                }
                pageInfo {
                  hasNextPage
                  endCursor
                }
              }
            }
          }
        `,
      }, {
        owner: remote.getOwner(),
        name: remote.getRepo(),
        first: 100,
        after: cursor,
      });

      const connection = response.data.repository.mentionableUsers;
      const authors = connection.nodes.map(node => {
        if (node.email === '') {
          node.email = `${node.login}@users.noreply.github.com`;
        }

        return new Author(node.email, node.name, node.login);
      });
      this.addUsers(authors, source.GITHUBAPI);
      remoteUsers.push(...authors);

      cursor = connection.pageInfo.endCursor;
      hasMore = connection.pageInfo.hasNextPage;
    }

    this.cache.set(remote, remoteUsers);
  }

  addUsers(users, nextSource) {
    if (
      nextSource !== this.last.source ||
      this.repositoryObserver.getActiveModel() !== this.last.repository
    ) {
      this.allUsers.clear();
    }

    let changed = false;
    for (const author of users) {
      if (!this.allUsers.has(author.getEmail())) {
        changed = true;
      }
      this.allUsers.set(author.getEmail(), author);
    }

    if (changed) {
      this.finalize();
    }
    this.last.source = nextSource;
    this.last.repository = this.repositoryObserver.getActiveModel();
  }

  finalize() {
    // TODO: [ku 3/2018] consider sorting based on most recent authors or commit frequency
    const users = [];
    for (const author of this.allUsers.values()) {
      if (author.matches(this.committer)) { continue; }
      if (author.isNoReply()) { continue; }

      users.push(author);
    }
    users.sort(Author.compare);
    this.users = users;
    this.didUpdate();
  }

  setRepository(repository) {
    this.repositoryObserver.setActiveModel(repository);
  }

  setLoginModel(login) {
    this.loginObserver.setActiveModel(login);
  }

  setCommitter(committer) {
    const changed = !this.committer.matches(committer);
    this.committer = committer;
    if (changed) {
      this.finalize();
    }
  }

  didUpdate() {
    this.emitter.emit('did-update', this.getUsers());
  }

  onDidUpdate(callback) {
    return this.emitter.on('did-update', callback);
  }

  getUsers() {
    return this.users;
  }
}
<commit_msg>Handle an INSUFFICIENT result in UserStore<commit_after>import yubikiri from 'yubikiri';
import {Emitter} from 'event-kit';

import RelayNetworkLayerManager from '../relay-network-layer-manager';
import Author, {nullAuthor} from './author';
import {UNAUTHENTICATED, INSUFFICIENT} from '../shared/keytar-strategy';
import ModelObserver from './model-observer';

// This is a guess about what a reasonable value is. Can adjust if performance is poor.
const MAX_COMMITS = 5000;

export const source = {
  PENDING: Symbol('pending'),
  GITLOG: Symbol('git log'),
  GITHUBAPI: Symbol('github API'),
};

class GraphQLCache {
  // One hour
  static MAX_AGE_MS = 3.6e6

  constructor() {
    this.bySlug = new Map();
  }

  get(remote) {
    const slug = remote.getSlug();
    const {ts, data} = this.bySlug.get(slug) || {
      ts: -Infinity,
      data: {},
    };

    if (Date.now() - ts > this.constructor.MAX_AGE_MS) {
      this.bySlug.delete(slug);
      return null;
    }
    return data;
  }

  set(remote, data) {
    this.bySlug.set(remote.getSlug(), {ts: Date.now(), data});
  }
}

export default class UserStore {
  constructor({repository, login}) {
    this.emitter = new Emitter();

    // TODO: [ku 3/2018] Consider using Dexie (indexDB wrapper) like Desktop and persist users across sessions
    this.allUsers = new Map();
    this.users = [];
    this.committer = nullAuthor;

    this.last = {
      source: source.PENDING,
      repository: null,
    };
    this.cache = new GraphQLCache();

    this.repositoryObserver = new ModelObserver({
      fetchData: r => yubikiri({
        committer: r.getCommitter(),
        authors: r.getAuthors({max: MAX_COMMITS}),
        remotes: r.getRemotes(),
      }),
      didUpdate: () => this.loadUsers(),
    });
    this.repositoryObserver.setActiveModel(repository);

    this.loginObserver = new ModelObserver({
      didUpdate: () => this.loadUsers(),
    });
    this.loginObserver.setActiveModel(login);
  }

  async loadUsers() {
    const data = this.repositoryObserver.getActiveModelData();

    if (!data) {
      return;
    }

    this.setCommitter(data.committer);
    const githubRemotes = data.remotes.filter(remote => remote.isGithubRepo());

    if (githubRemotes.length === 0) {
      this.addUsers(data.authors, source.GITLOG);
    } else {
      await this.loadUsersFromGraphQL(githubRemotes);
    }
  }

  loadUsersFromGraphQL(remotes) {
    return Promise.all(
      remotes.map(remote => this.loadMentionableUsers(remote)),
    );
  }

  async loadMentionableUsers(remote) {
    const cached = this.cache.get(remote);
    if (cached !== null) {
      this.addUsers(cached, source.GITHUBAPI);
      return;
    }

    const loginModel = this.loginObserver.getActiveModel();
    if (!loginModel) {
      return;
    }

    const token = await loginModel.getToken('https://api.github.com');
    if (token === UNAUTHENTICATED || token === INSUFFICIENT) {
      return;
    }

    const fetchQuery = RelayNetworkLayerManager.getFetchQuery('https://api.github.com/graphql', token);

    let hasMore = true;
    let cursor = null;
    const remoteUsers = [];

    while (hasMore) {
      const response = await fetchQuery({
        name: 'GetMentionableUsers',
        text: `
          query GetMentionableUsers($owner: String!, $name: String!, $first: Int!, $after: String) {
            repository(owner: $owner, name: $name) {
              mentionableUsers(first: $first, after: $after) {
                nodes {
                  login
                  email
                  name
                }
                pageInfo {
                  hasNextPage
                  endCursor
                }
              }
            }
          }
        `,
      }, {
        owner: remote.getOwner(),
        name: remote.getRepo(),
        first: 100,
        after: cursor,
      });

      const connection = response.data.repository.mentionableUsers;
      const authors = connection.nodes.map(node => {
        if (node.email === '') {
          node.email = `${node.login}@users.noreply.github.com`;
        }

        return new Author(node.email, node.name, node.login);
      });
      this.addUsers(authors, source.GITHUBAPI);
      remoteUsers.push(...authors);

      cursor = connection.pageInfo.endCursor;
      hasMore = connection.pageInfo.hasNextPage;
    }

    this.cache.set(remote, remoteUsers);
  }

  addUsers(users, nextSource) {
    if (
      nextSource !== this.last.source ||
      this.repositoryObserver.getActiveModel() !== this.last.repository
    ) {
      this.allUsers.clear();
    }

    let changed = false;
    for (const author of users) {
      if (!this.allUsers.has(author.getEmail())) {
        changed = true;
      }
      this.allUsers.set(author.getEmail(), author);
    }

    if (changed) {
      this.finalize();
    }
    this.last.source = nextSource;
    this.last.repository = this.repositoryObserver.getActiveModel();
  }

  finalize() {
    // TODO: [ku 3/2018] consider sorting based on most recent authors or commit frequency
    const users = [];
    for (const author of this.allUsers.values()) {
      if (author.matches(this.committer)) { continue; }
      if (author.isNoReply()) { continue; }

      users.push(author);
    }
    users.sort(Author.compare);
    this.users = users;
    this.didUpdate();
  }

  setRepository(repository) {
    this.repositoryObserver.setActiveModel(repository);
  }

  setLoginModel(login) {
    this.loginObserver.setActiveModel(login);
  }

  setCommitter(committer) {
    const changed = !this.committer.matches(committer);
    this.committer = committer;
    if (changed) {
      this.finalize();
    }
  }

  didUpdate() {
    this.emitter.emit('did-update', this.getUsers());
  }

  onDidUpdate(callback) {
    return this.emitter.on('did-update', callback);
  }

  getUsers() {
    return this.users;
  }
}
<|endoftext|>


<commit_before>var Promise = require('bluebird');
var ConcurrencyError = require('tapeworm').ConcurrencyError;
var DuplicateCommitError = require('tapeworm').DuplicateCommitError;
var _ = require('lodash');

var CONCURRENCY_EXCEPTION_CODE = 11000;

var MdbPartition = function (mongodb, partitionId) {
  this._mongodb = mongodb;
  this._partitionId = partitionId;
  this._collection = null;
}

MdbPartition.prototype.open = function () {
  var self = this;
  this._commits = this._mongodb.collection('tw_' + this._partitionId + "_commits");
  this._snapshots = this._mongodb.collection('tw_' + this._partitionId + "_snapshots");
  return new Promise(function (resolve, reject) {
    self._snapshots.ensureIndex({ id: 1 }, { unique: true });
    self._commits.ensureIndex({ id: 1 }, { unique: true }, function (err, indexName) {
      if (err) {
        reject(err);
      } else {
        self._commits.ensureIndex({ streamId: 1, commitSequence: 1 }, { unique: true }, function (err, indexName) {
          if (err) {
            reject(err);
          } else {
            self._commits.ensureIndex({ createDateTime: 1 }, { unique: false }, function (err, indexName) {
              if (err) {
                reject(err);
              } else {
                resolve(self);
              }
            });
          }
        });
      }
    });
  });
};

MdbPartition.prototype.storeSnapshot = function (streamId, snapshot, version, callback) {
  var self = this;
  return new Promise(function (resolve, reject) {
    var toStore = { _id: streamId, id: streamId, version: version, snapshot: snapshot };
    self._snapshots.save(toStore, function (err, res) {
      if (err) {
        reject(err);
      } else {
        resolve(toStore);
      }
    });
  }).nodeify(callback);
};

MdbPartition.prototype.loadSnapshot = function (streamId, callback) {
  var self = this;
  return new Promise(function (resolve, reject) {
    self._snapshots.find({ id: streamId }).toArray(function (err, res) {
      if (err) {
        reject(err);
      } else {
        resolve(res[0]);
      }
    });
  }).nodeify(callback);
}

MdbPartition.prototype.removeSnapshot = function (streamId, callback) {
    var self = this;
    return new Promise(function (resolve, reject) {
      var toStore = { _id: streamId, id: streamId, version: -1 };
      self._snapshots.save(toStore, function (err, res) {
        if (err) {
          reject(err);
        } else {
          resolve(toStore);
        }
      });
    }).nodeify(callback);
  };

MdbPartition.prototype.append = function (commit, callback) {
  var self = this;
  commit.isDispatched = false;
  commit.createDateTime = new Date();
  return new Promise(function (resolve, reject) {
    self._commits.insert(commit, function (err, result) {
      if (err) {
        if (err.code === CONCURRENCY_EXCEPTION_CODE) {
          //console.log(JSON.stringify(err));
          if (err.message.indexOf('commitSequence') > 0) {
            reject(new ConcurrencyError('Concurrency error on stream ' + commit.streamId));
          } else {
            reject(new DuplicateCommitError('Duplicate commit on stream ' + commit.streamId));
          }
        } else {
          reject(err);
        }
      } else {
        resolve(commit);
      }
    });
  }).nodeify(callback);
}

MdbPartition.prototype.markAsDispatched = function (commit, callback) {
  throw Error('not implemented');
};

MdbPartition.prototype.getUndispatched = function (callback) {
  throw Error('not implemented');
};

MdbPartition.prototype.queryAll = function (callback) {
  //return this._promisify(this._commits.slice(), callback);
  var self = this;
  return new Promise(function (resolve, reject) {
    //TODO: sort in insert order...
    self._commits.find({}).sort({ createDateTime: 1 }).toArray(function (err, commits) {
      if (err) {
        reject(err);
      } else {
        resolve(commits);
      }
    });
  }).nodeify(callback);
};

MdbPartition.prototype.queryStream = function (streamId, fromEventSequence, callback) {
  var self = this;
  // if from event sequence is 0 / undefined - use fallback..
  if (_.isNil(fromEventSequence) || fromEventSequence === 0) {
    return self.queryStreamFallback(streamId, fromEventSequence, callback);
  }
  if (typeof fromEventSequence === 'function') {
    callback = fromEventSequence;
    fromEventSequence = 0;
  }
  return new Promise(function (resolve, reject) {
    // use limit 1 and only include events from requested version - if we are unable to find the correct event, use fallback...
    self._commits.find({ streamId: streamId, 'events.version': { $gte: fromEventSequence } }).limit(1).sort({ commitSequence: -1 }).toArray(function (err, commits) {
      if (err) {
        reject(err);
      } else {
        // assume this will happen most of the times
        if (commits.length === 0) {
          resolve([]);
          return;
        }

        var events = _.get(commits, '[0].events', []);
        // check if our requested event version is out of reach for the last commit - need to use fallback then..
        if (events[0].version > fromEventSequence) {
          // problem... use fallback.
          return self.queryStreamFallback(streamId, fromEventSequence).then(function (res) {
            resolve(res);
          })
        }

        // find every event that has version equal or larger than fromEventSequence
        var foundEvents = [];
        for (var i = 0; i < events.length; i++) {
          if (events[i].version >= fromEventSequence) {
            foundEvents.push(events[i]);
          }
        }
        var commit = commits[0];
        commit.events = foundEvents;
        resolve([commit]);
      }
    });
  }).nodeify(callback);
}

MdbPartition.prototype.queryStreamFallback = function (streamId, fromEventSequence, callback) {
  if (typeof fromEventSequence === 'function') {
    callback = fromEventSequence;
    fromEventSequence = 0;
  }
  var self = this;
  return new Promise(function (resolve, reject) {
    //TODO: sort in insert order...
    self._commits.find({ streamId: streamId }).sort({ commitSequence: 1 }).toArray(function (err, commits) {
      if (err) {
        reject(err);
      } else {
        var result = commits
        if (fromEventSequence > 0) {
          var startCommitId = 0;
          var foundEvents = 0;
          for (var i = 0; i < result.length; i++) {
            foundEvents += result[i].events.length;
            startCommitId++;
            if (foundEvents >= fromEventSequence) {
              break;
            }
          }
          var tooMany = foundEvents - fromEventSequence;

          result = result.slice(startCommitId - (tooMany > 0 ? 1 : 0));
          if (tooMany > 0) {
            result[0].events = result[0].events.slice(result[0].events.length - tooMany);
          }
        }
        resolve(result);
      }
    });
  }).nodeify(callback);
};

module.exports = MdbPartition;
<commit_msg>fix order of argument checks<commit_after>var Promise = require('bluebird');
var ConcurrencyError = require('tapeworm').ConcurrencyError;
var DuplicateCommitError = require('tapeworm').DuplicateCommitError;
var _ = require('lodash');

var CONCURRENCY_EXCEPTION_CODE = 11000;

var MdbPartition = function (mongodb, partitionId) {
  this._mongodb = mongodb;
  this._partitionId = partitionId;
  this._collection = null;
}

MdbPartition.prototype.open = function () {
  var self = this;
  this._commits = this._mongodb.collection('tw_' + this._partitionId + "_commits");
  this._snapshots = this._mongodb.collection('tw_' + this._partitionId + "_snapshots");
  return new Promise(function (resolve, reject) {
    self._snapshots.ensureIndex({ id: 1 }, { unique: true });
    self._commits.ensureIndex({ id: 1 }, { unique: true }, function (err, indexName) {
      if (err) {
        reject(err);
      } else {
        self._commits.ensureIndex({ streamId: 1, commitSequence: 1 }, { unique: true }, function (err, indexName) {
          if (err) {
            reject(err);
          } else {
            self._commits.ensureIndex({ createDateTime: 1 }, { unique: false }, function (err, indexName) {
              if (err) {
                reject(err);
              } else {
                resolve(self);
              }
            });
          }
        });
      }
    });
  });
};

MdbPartition.prototype.storeSnapshot = function (streamId, snapshot, version, callback) {
  var self = this;
  return new Promise(function (resolve, reject) {
    var toStore = { _id: streamId, id: streamId, version: version, snapshot: snapshot };
    self._snapshots.save(toStore, function (err, res) {
      if (err) {
        reject(err);
      } else {
        resolve(toStore);
      }
    });
  }).nodeify(callback);
};

MdbPartition.prototype.loadSnapshot = function (streamId, callback) {
  var self = this;
  return new Promise(function (resolve, reject) {
    self._snapshots.find({ id: streamId }).toArray(function (err, res) {
      if (err) {
        reject(err);
      } else {
        resolve(res[0]);
      }
    });
  }).nodeify(callback);
}

MdbPartition.prototype.removeSnapshot = function (streamId, callback) {
    var self = this;
    return new Promise(function (resolve, reject) {
      var toStore = { _id: streamId, id: streamId, version: -1 };
      self._snapshots.save(toStore, function (err, res) {
        if (err) {
          reject(err);
        } else {
          resolve(toStore);
        }
      });
    }).nodeify(callback);
  };

MdbPartition.prototype.append = function (commit, callback) {
  var self = this;
  commit.isDispatched = false;
  commit.createDateTime = new Date();
  return new Promise(function (resolve, reject) {
    self._commits.insert(commit, function (err, result) {
      if (err) {
        if (err.code === CONCURRENCY_EXCEPTION_CODE) {
          //console.log(JSON.stringify(err));
          if (err.message.indexOf('commitSequence') > 0) {
            reject(new ConcurrencyError('Concurrency error on stream ' + commit.streamId));
          } else {
            reject(new DuplicateCommitError('Duplicate commit on stream ' + commit.streamId));
          }
        } else {
          reject(err);
        }
      } else {
        resolve(commit);
      }
    });
  }).nodeify(callback);
}

MdbPartition.prototype.markAsDispatched = function (commit, callback) {
  throw Error('not implemented');
};

MdbPartition.prototype.getUndispatched = function (callback) {
  throw Error('not implemented');
};

MdbPartition.prototype.queryAll = function (callback) {
  //return this._promisify(this._commits.slice(), callback);
  var self = this;
  return new Promise(function (resolve, reject) {
    //TODO: sort in insert order...
    self._commits.find({}).sort({ createDateTime: 1 }).toArray(function (err, commits) {
      if (err) {
        reject(err);
      } else {
        resolve(commits);
      }
    });
  }).nodeify(callback);
};

MdbPartition.prototype.queryStream = function (streamId, fromEventSequence, callback) {
  var self = this;
  if (typeof fromEventSequence === 'function') {
    callback = fromEventSequence;
    fromEventSequence = 0;
  }
  // if from event sequence is 0 / undefined - use fallback..
  if (_.isNil(fromEventSequence) || fromEventSequence === 0) {
    return self.queryStreamFallback(streamId, fromEventSequence, callback);
  }
  return new Promise(function (resolve, reject) {
    // use limit 1 and only include events from requested version - if we are unable to find the correct event, use fallback...
    self._commits.find({ streamId: streamId, 'events.version': { $gte: fromEventSequence } }).limit(1).sort({ commitSequence: -1 }).toArray(function (err, commits) {
      if (err) {
        reject(err);
      } else {
        // assume this will happen most of the times
        if (commits.length === 0) {
          resolve([]);
          return;
        }

        var events = _.get(commits, '[0].events', []);
        // check if our requested event version is out of reach for the last commit - need to use fallback then..
        if (events[0].version > fromEventSequence) {
          // problem... use fallback.
          return self.queryStreamFallback(streamId, fromEventSequence).then(function (res) {
            resolve(res);
          })
        }

        // find every event that has version equal or larger than fromEventSequence
        var foundEvents = [];
        for (var i = 0; i < events.length; i++) {
          if (events[i].version >= fromEventSequence) {
            foundEvents.push(events[i]);
          }
        }
        var commit = commits[0];
        commit.events = foundEvents;
        resolve([commit]);
      }
    });
  }).nodeify(callback);
}

MdbPartition.prototype.queryStreamFallback = function (streamId, fromEventSequence, callback) {
  if (typeof fromEventSequence === 'function') {
    callback = fromEventSequence;
    fromEventSequence = 0;
  }
  var self = this;
  return new Promise(function (resolve, reject) {
    //TODO: sort in insert order...
    self._commits.find({ streamId: streamId }).sort({ commitSequence: 1 }).toArray(function (err, commits) {
      if (err) {
        reject(err);
      } else {
        var result = commits
        if (fromEventSequence > 0) {
          var startCommitId = 0;
          var foundEvents = 0;
          for (var i = 0; i < result.length; i++) {
            foundEvents += result[i].events.length;
            startCommitId++;
            if (foundEvents >= fromEventSequence) {
              break;
            }
          }
          var tooMany = foundEvents - fromEventSequence;

          result = result.slice(startCommitId - (tooMany > 0 ? 1 : 0));
          if (tooMany > 0) {
            result[0].events = result[0].events.slice(result[0].events.length - tooMany);
          }
        }
        resolve(result);
      }
    });
  }).nodeify(callback);
};

module.exports = MdbPartition;
<|endoftext|>


<commit_before>/*jslint node: true */
'use strict';
var logger = require('../utils/logger');
var serverURL = process.env.SERVER + (process.env.PORT || 80);
var app = require('../app');
var request = require('superagent');
var response = require('../lib/response');
var testData = require('./data');
var constants = require('../lib/constants');
var chai = require('chai');
var expect = chai.expect;
chai.use(require('chai-json-schema'));

var server = app.listen(process.env.PORT || 80, function () {
    logger.debug('Express server listening on port ' + server.address().port);
});

/**
 * Tests for the web service
 */
describe('The Pageload Timer Web Service: ', function () {

    it("Should Return 4 Valid Timing Records Given Sample Request", function (done) {
        this.timeout(10000);
        request.post(serverURL)
            .type('application/json')
            .send(testData.sampleRequest)
            .end(function (err, res) {
                expect(res.status).to.equal(200);
                expect(res.ok).to.equal(true);
                //TODO expect response Content-Type = application/json;
                expect(response.isValid(JSON.stringify(res.body))).to.be.true;
                expect(res.body.response).to.have.length(4);
                var req = JSON.parse(testData.sampleRequest);
                //TODO change to "oneOf" after chai-json-schema is updated
                //TODO or change to forEach
                //expect(res.body.response[0].url).to.contain(req.payload);
                expect(req.payload).to.contain(res.body.response[0].url);
                expect(req.payload).to.contain(res.body.response[1].url);
                expect(req.payload).to.contain(res.body.response[2].url);
                expect(req.payload).to.contain(res.body.response[3].url);
                done();
            });
    });

    it("Should Return Single Response Given Single Existing URL", function (done) {
        var validUrl = 'http://www.google.com/';
        request.post(serverURL)
            .type('application/json')
            .send({payload: [validUrl]})
            .end(function (err, res) {
                expect(res.ok).to.equal(true);
                expect(res.status).to.equal(200);
                expect(res.body.response).to.have.length(1);
                expect(res.body.response[0]).to.have.property('url');
                expect(res.body.response[0]).to.have.property('latency_ms');
                expect(res.body.response[0].url).to.be.validUrl;
                done();
            });
    });

    it("Should Return 404 Not Found Given Well-Formed Non-Existing URL", function (done) {
        request.post(serverURL)
            .type('application/json')
            .send({payload: ['http://highly-unlikely-71863916278.co.nz']})
            .end(function (err, res) {
                expect(JSON.stringify(res.body)).to.contain(constants.notFoundErrorMsg);
                expect(res.status).to.equal(404);
                expect(res.ok).to.equal(false);
                done();
            });
    });

    it("Should Return 404 Not Found Given Invalid TLD", function (done) {
        request.post(serverURL)
            .type('application/json')
            .send({payload: ['http://www.test.invalidTLD/']})
            .end(function (err, res) {
                expect(JSON.stringify(res.body)).to.contain(constants.notFoundErrorMsg);
                expect(res.status).to.equal(404);
                expect(res.ok).to.equal(false);
                done();
            });
    });

    it("Should Return 400 Parse Error Given Invalid Schema", function (done) {
        request.post(serverURL)
            .type('application/json')
            .send({payloads: [], name: "invalid"})
            .end(function (err, res) {
                expect(JSON.stringify(res.body)).to.equal(testData.expectedJSONParseErrorMsg);
                expect(res.status).to.equal(400);
                expect(res.ok).to.equal(false);
                done();
            });
    });

    it("Should Return 400 Parse Error Given Invalid MIME Type", function (done) {
        request.post(serverURL)
            .type('text/plain')
            .send(testData.sampleRequest)
            .end(function (err, res) {
                expect(res.ok).to.equal(false);
                expect(JSON.stringify(res.body)).to.equal(testData.expectedJSONParseErrorMsg);
                expect(res.status).to.equal(400);
                done();
            });
    });

    it("Should Return 400 Parse Error Given Non-JSON Request", function (done) {
        request.post(serverURL)
            .type('application/json')
            .send("invalid text")
            .end(function (err, res) {
                expect(res.ok).to.equal(false);
                expect(JSON.stringify(res.body)).to.equal(testData.expectedJSONParseErrorMsg);
                expect(res.status).to.equal(400);
                done();
            });
    });

    it("Should Return 404 Not Found Given Invalid POST Path", function (done) {
        request.post(serverURL + "/invalidPath").end(function (err, res) {
            expect(res.ok).to.equal(false);
            expect(res.status).to.equal(404);
            expect(res.body.error).to.equal(constants.routeNotFoundErrorMsg);
            done();
        });
    });

    it("Should Return 400 Parse Error Given Missing Request", function (done) {
        request.post(serverURL).end(function (err, res) {
            expect(res.ok).to.equal(false);
            expect(JSON.stringify(res.body)).to.equal(testData.expectedJSONParseErrorMsg);
            expect(res.status).to.equal(400);
            done();
        });
    });

    it("Should Return 400 Parse Error Given Null Request", function (done) {
        request.post(serverURL)
            .type('application/json')
            .send(null)
            .end(function (err, res) {
                expect(res.ok).to.equal(false);
                expect(JSON.stringify(res.body)).to.equal(testData.expectedJSONParseErrorMsg);
                expect(res.status).to.equal(400);
                done();
            });
    });

    it("Should Return 400 Parse Error Given Empty JSON Request", function (done) {
        request.post(serverURL)
            .type('application/json')
            .send({})
            .end(function (err, res) {
                expect(res.ok).to.equal(false);
                expect(JSON.stringify(res.body)).to.equal(testData.expectedJSONParseErrorMsg);
                expect(res.status).to.equal(400);
                done();
            });
    });

    it("Should Return 400 Parse Error Given Empty String Request", function (done) {
        request.post(serverURL)
            .type('application/json')
            .send('')
            .end(function (err, res) {
                expect(res.ok).to.equal(false);
                expect(JSON.stringify(res.body)).to.equal(testData.expectedJSONParseErrorMsg);
                expect(res.status).to.equal(400);
                done();
            });
    });

    it("Should Return 400 Parse Error Given Empty Payload", function (done) {
        request.post(serverURL)
            .type('application/json')
            .send(testData.emptyRequest)
            .end(function (err, res) {
                expect(res.ok).to.equal(false);
                expect(JSON.stringify(res.body)).to.equal(testData.expectedJSONParseErrorMsg);
                expect(res.status).to.equal(400);
                done();
            });
    });

    after(function () {
        server.close();
        logger.debug('Express server is now closed.');
    });
});

/**
 * Last-ditch Error Handler (main is in app.js)
 */
process.on('uncaughtException', function (err) {
    logger.error('Uncaught Exception (app.test.js):', err);
    // let it die because it will be restarted
});<commit_msg>Change serverURL<commit_after>/*jslint node: true */
'use strict';
var logger = require('../utils/logger');
var serverURL = 'http://localhost:' + (process.env.PORT || 80);
var app = require('../app');
var request = require('superagent');
var response = require('../lib/response');
var testData = require('./data');
var constants = require('../lib/constants');
var chai = require('chai');
var expect = chai.expect;
chai.use(require('chai-json-schema'));

var server = app.listen(process.env.PORT || 80, function () {
    logger.debug('Express server listening on port ' + server.address().port);
});

/**
 * Tests for the web service
 */
describe('The Pageload Timer Web Service: ', function () {

    it("Should Return 4 Valid Timing Records Given Sample Request", function (done) {
        this.timeout(10000);
        request.post(serverURL)
            .type('application/json')
            .send(testData.sampleRequest)
            .end(function (err, res) {
                expect(res.status).to.equal(200);
                expect(res.ok).to.equal(true);
                //TODO expect response Content-Type = application/json;
                expect(response.isValid(JSON.stringify(res.body))).to.be.true;
                expect(res.body.response).to.have.length(4);
                var req = JSON.parse(testData.sampleRequest);
                //TODO change to "oneOf" after chai-json-schema is updated
                //TODO or change to forEach
                //expect(res.body.response[0].url).to.contain(req.payload);
                expect(req.payload).to.contain(res.body.response[0].url);
                expect(req.payload).to.contain(res.body.response[1].url);
                expect(req.payload).to.contain(res.body.response[2].url);
                expect(req.payload).to.contain(res.body.response[3].url);
                done();
            });
    });

    it("Should Return Single Response Given Single Existing URL", function (done) {
        var validUrl = 'http://www.google.com/';
        request.post(serverURL)
            .type('application/json')
            .send({payload: [validUrl]})
            .end(function (err, res) {
                expect(res.ok).to.equal(true);
                expect(res.status).to.equal(200);
                expect(res.body.response).to.have.length(1);
                expect(res.body.response[0]).to.have.property('url');
                expect(res.body.response[0]).to.have.property('latency_ms');
                expect(res.body.response[0].url).to.be.validUrl;
                done();
            });
    });

    it("Should Return 404 Not Found Given Well-Formed Non-Existing URL", function (done) {
        request.post(serverURL)
            .type('application/json')
            .send({payload: ['http://highly-unlikely-71863916278.co.nz']})
            .end(function (err, res) {
                expect(JSON.stringify(res.body)).to.contain(constants.notFoundErrorMsg);
                expect(res.status).to.equal(404);
                expect(res.ok).to.equal(false);
                done();
            });
    });

    it("Should Return 404 Not Found Given Invalid TLD", function (done) {
        request.post(serverURL)
            .type('application/json')
            .send({payload: ['http://www.test.invalidTLD/']})
            .end(function (err, res) {
                expect(JSON.stringify(res.body)).to.contain(constants.notFoundErrorMsg);
                expect(res.status).to.equal(404);
                expect(res.ok).to.equal(false);
                done();
            });
    });

    it("Should Return 400 Parse Error Given Invalid Schema", function (done) {
        request.post(serverURL)
            .type('application/json')
            .send({payloads: [], name: "invalid"})
            .end(function (err, res) {
                expect(JSON.stringify(res.body)).to.equal(testData.expectedJSONParseErrorMsg);
                expect(res.status).to.equal(400);
                expect(res.ok).to.equal(false);
                done();
            });
    });

    it("Should Return 400 Parse Error Given Invalid MIME Type", function (done) {
        request.post(serverURL)
            .type('text/plain')
            .send(testData.sampleRequest)
            .end(function (err, res) {
                expect(res.ok).to.equal(false);
                expect(JSON.stringify(res.body)).to.equal(testData.expectedJSONParseErrorMsg);
                expect(res.status).to.equal(400);
                done();
            });
    });

    it("Should Return 400 Parse Error Given Non-JSON Request", function (done) {
        request.post(serverURL)
            .type('application/json')
            .send("invalid text")
            .end(function (err, res) {
                expect(res.ok).to.equal(false);
                expect(JSON.stringify(res.body)).to.equal(testData.expectedJSONParseErrorMsg);
                expect(res.status).to.equal(400);
                done();
            });
    });

    it("Should Return 404 Not Found Given Invalid POST Path", function (done) {
        request.post(serverURL + "/invalidPath").end(function (err, res) {
            expect(res.ok).to.equal(false);
            expect(res.status).to.equal(404);
            expect(res.body.error).to.equal(constants.routeNotFoundErrorMsg);
            done();
        });
    });

    it("Should Return 400 Parse Error Given Missing Request", function (done) {
        request.post(serverURL).end(function (err, res) {
            expect(res.ok).to.equal(false);
            expect(JSON.stringify(res.body)).to.equal(testData.expectedJSONParseErrorMsg);
            expect(res.status).to.equal(400);
            done();
        });
    });

    it("Should Return 400 Parse Error Given Null Request", function (done) {
        request.post(serverURL)
            .type('application/json')
            .send(null)
            .end(function (err, res) {
                expect(res.ok).to.equal(false);
                expect(JSON.stringify(res.body)).to.equal(testData.expectedJSONParseErrorMsg);
                expect(res.status).to.equal(400);
                done();
            });
    });

    it("Should Return 400 Parse Error Given Empty JSON Request", function (done) {
        request.post(serverURL)
            .type('application/json')
            .send({})
            .end(function (err, res) {
                expect(res.ok).to.equal(false);
                expect(JSON.stringify(res.body)).to.equal(testData.expectedJSONParseErrorMsg);
                expect(res.status).to.equal(400);
                done();
            });
    });

    it("Should Return 400 Parse Error Given Empty String Request", function (done) {
        request.post(serverURL)
            .type('application/json')
            .send('')
            .end(function (err, res) {
                expect(res.ok).to.equal(false);
                expect(JSON.stringify(res.body)).to.equal(testData.expectedJSONParseErrorMsg);
                expect(res.status).to.equal(400);
                done();
            });
    });

    it("Should Return 400 Parse Error Given Empty Payload", function (done) {
        request.post(serverURL)
            .type('application/json')
            .send(testData.emptyRequest)
            .end(function (err, res) {
                expect(res.ok).to.equal(false);
                expect(JSON.stringify(res.body)).to.equal(testData.expectedJSONParseErrorMsg);
                expect(res.status).to.equal(400);
                done();
            });
    });

    after(function () {
        server.close();
        logger.debug('Express server is now closed.');
    });
});

/**
 * Last-ditch Error Handler (main is in app.js)
 */
process.on('uncaughtException', function (err) {
    logger.error('Uncaught Exception (app.test.js):', err);
    // let it die because it will be restarted
});
<|endoftext|>


<commit_before>/**
 * Copyright 2014 GetHuman LLC
 * Author: Jeff Whelpley
 * Date: 10/21/14
 *
 *
 */
var _ = require('lodash');
var Q = require('q');

/**
 * Go through all resources and add the API endpoints to the Hapi
 * server that is passed in
 * @param opts
 */
function addApiRoutes(opts) {
    var resources   = this.pancakes.cook('resources', null);
    var apiPrefix   = opts.apiPrefix;
    var server      = opts.server;
    var me          = this;
    var cors = {
        origin:         opts.hosts || [],
        headers:        ['Accept', 'Accept-Version', 'Content-Type', 'Api-Version', 'X-Requested-With', 'Authorization'],
        credentials:    true
    };

    _.each(resources, function (resource) {
        _.each(resource.api, function (methodInfo, httpMethod) {
            _.each(methodInfo, function (operation, path) {

                // if path includes {_id} convert to {_id?} so optional
                // this will allow both /users/someid and /users to work
                // we want this because pancakes handles param validation
                // only do this if path without {_id} is not already defined
                var lenWithoutId = path.length - 6;
                var pathWithoutId = path.substring(0, lenWithoutId);
                if (path.substring(lenWithoutId) === '/{_id}' && !methodInfo[pathWithoutId]) {
                    path = pathWithoutId + '/{_id?}';
                }

                // if the operation is a string, then it is the name of the handler method
                if (_.isString(operation)) {
                    server.route({
                        method:             httpMethod,
                        path:               apiPrefix + path,
                        config: {
                            cors:           cors,
                            handler:        me.getApiRouteHandler(resource, operation)
                        }
                    });
                }

                // else if object with type stream, need to use a slightly different call to server.route
                else if (_.isObject(operation) && operation.type === 'stream' && operation.operation) {
                    server.route({
                        method:             httpMethod,
                        path:               apiPrefix + path,
                        config: {
                            payload: {
                                maxBytes:   209715200,
                                output:     'stream',
                                parse:      true
                            },
                            cors:           cors,
                            handler:        me.getApiRouteHandler(resource, operation.operation)
                        }
                    });
                }

                // else error
                else {
                    throw new Error('Invalid type of operation for ' + resource.name + ' ' + path);
                }
            });
        });
    });

    server.on('response', function (request) {
        // careful because we don't want to screw up all/any requests...
        if ( request.response && request.response.statusCode && request.response.statusCode > 399 && request.method
                && request.method.toUpperCase && request.url && request.url.path ) {
            var lapse = -1;
            if ( request.info && request.info.responded && request.info.received ) {
                lapse = request.info.responded - request.info.received;
            }
            var msg = request.method.toUpperCase() + ' ' + request.url.path + ' -> '
                + request.response.statusCode + ' ' + '(' + lapse + 'ms)';
            if ( requests.info && request.info.referrer && request.info.referrer.length ) {
                msg += ' ref: ' + request.info.referrer;
            }
            console.log(msg);
        }
    });
}

/**
 * Get a route handler given a set of parameters
 * @param resource
 * @param operation
 * @returns {Function}
 */
function  getApiRouteHandler(resource, operation) {
    var me = this;

    return function (request, reply) {
        var req = {
            caller:     request.caller,
            resource:   resource,
            method:     operation
        };

        // add query and params to the request
        _.extend(req, request.query, request.params);

        if (request.payload && !_.isEmpty(request.payload)) {
            req.data = request.payload;
        }

        return Q.fcall(function () {
            return me.pancakes.apiRouteHandler.processApiCall(resource, operation, req);
        })
            .then(function (res) {
                reply(res.data);
            })
            .catch(function (err) {
                reply(err);
            });
    };
}

module.exports = {
    addApiRoutes: addApiRoutes,
    getApiRouteHandler: getApiRouteHandler
};<commit_msg>typo causing errors for lots of requests<commit_after>/**
 * Copyright 2014 GetHuman LLC
 * Author: Jeff Whelpley
 * Date: 10/21/14
 *
 *
 */
var _ = require('lodash');
var Q = require('q');

/**
 * Go through all resources and add the API endpoints to the Hapi
 * server that is passed in
 * @param opts
 */
function addApiRoutes(opts) {
    var resources   = this.pancakes.cook('resources', null);
    var apiPrefix   = opts.apiPrefix;
    var server      = opts.server;
    var me          = this;
    var cors = {
        origin:         opts.hosts || [],
        headers:        ['Accept', 'Accept-Version', 'Content-Type', 'Api-Version', 'X-Requested-With', 'Authorization'],
        credentials:    true
    };

    _.each(resources, function (resource) {
        _.each(resource.api, function (methodInfo, httpMethod) {
            _.each(methodInfo, function (operation, path) {

                // if path includes {_id} convert to {_id?} so optional
                // this will allow both /users/someid and /users to work
                // we want this because pancakes handles param validation
                // only do this if path without {_id} is not already defined
                var lenWithoutId = path.length - 6;
                var pathWithoutId = path.substring(0, lenWithoutId);
                if (path.substring(lenWithoutId) === '/{_id}' && !methodInfo[pathWithoutId]) {
                    path = pathWithoutId + '/{_id?}';
                }

                // if the operation is a string, then it is the name of the handler method
                if (_.isString(operation)) {
                    server.route({
                        method:             httpMethod,
                        path:               apiPrefix + path,
                        config: {
                            cors:           cors,
                            handler:        me.getApiRouteHandler(resource, operation)
                        }
                    });
                }

                // else if object with type stream, need to use a slightly different call to server.route
                else if (_.isObject(operation) && operation.type === 'stream' && operation.operation) {
                    server.route({
                        method:             httpMethod,
                        path:               apiPrefix + path,
                        config: {
                            payload: {
                                maxBytes:   209715200,
                                output:     'stream',
                                parse:      true
                            },
                            cors:           cors,
                            handler:        me.getApiRouteHandler(resource, operation.operation)
                        }
                    });
                }

                // else error
                else {
                    throw new Error('Invalid type of operation for ' + resource.name + ' ' + path);
                }
            });
        });
    });

    server.on('response', function (request) {
        // careful because we don't want to screw up all/any requests...
        if ( request.response && request.response.statusCode && request.response.statusCode > 399 && request.method
                && request.method.toUpperCase && request.url && request.url.path ) {
            var lapse = -1;
            if ( request.info && request.info.responded && request.info.received ) {
                lapse = request.info.responded - request.info.received;
            }
            var msg = request.method.toUpperCase() + ' ' + request.url.path + ' -> '
                + request.response.statusCode + ' ' + '(' + lapse + 'ms)';
            if ( request.info && request.info.referrer && request.info.referrer.length ) {
                msg += ' ref: ' + request.info.referrer;
            }
            console.log(msg);
        }
    });
}

/**
 * Get a route handler given a set of parameters
 * @param resource
 * @param operation
 * @returns {Function}
 */
function  getApiRouteHandler(resource, operation) {
    var me = this;

    return function (request, reply) {
        var req = {
            caller:     request.caller,
            resource:   resource,
            method:     operation
        };

        // add query and params to the request
        _.extend(req, request.query, request.params);

        if (request.payload && !_.isEmpty(request.payload)) {
            req.data = request.payload;
        }

        return Q.fcall(function () {
            return me.pancakes.apiRouteHandler.processApiCall(resource, operation, req);
        })
            .then(function (res) {
                reply(res.data);
            })
            .catch(function (err) {
                reply(err);
            });
    };
}

module.exports = {
    addApiRoutes: addApiRoutes,
    getApiRouteHandler: getApiRouteHandler
};<|endoftext|>


<commit_before>const assert = require('assert')
const Selector = require('./selector')

const primitives = {
    types: [ Boolean, Number, String, BigInt, Array ],
    conditions: [
        (when) => {
            const conditions = [ (b) => typeof b == 'boolean' ]
            if (typeof when[0] == 'boolean') {
                const value = when.shift()
                conditions.push((b) => !! b == value)
            }
            conditions.push.apply(conditions, _functions(when))
            assert(when.length == 0)
            return conditions
        },
        (when) => {
            const conditions = [ (n) => typeof n == 'number' ]
            if (typeof when[0] == 'number') {
                const value = when.shift()
                conditions.push((n) => n == value)
            } else if (
                Array.isArray(when[0]) &&
                when[0].map((n) => typeof n == 'number').length == 2
            ) {
                const range = when.shift()
                conditions.push((n) => range[0] <= n && n <= range[1])
            }
            conditions.push.apply(conditions, _functions(when))
            assert(when.length == 0)
            return conditions
        },
        (when) => {
            const conditions = [ (s) => typeof s == 'string' ]
            if (typeof when[0] == 'string') {
                const value = when.shift()
                conditions.push((s) => s.toString() == value)
            } else if (when[0] instanceof RegExp) {
                const regex = when.shift()
                conditions.push((s) => regex.test(s.toString()))
                assert(when.length == 0)
            }
            conditions.push.apply(conditions, _functions(when))
            assert(when.length == 0)
            return conditions
        },
        (when) => {
            const conditions =  [ (n) => typeof n == 'bigint' ]
            if (typeof when[0] == 'bigint') {
                const value = when.shift()
                conditions.push((n) => n == value)
            } else if (
                Array.isArray(when[0]) &&
                when[0].map((n) => typeof n == 'bigint').length == 2
            ) {
                const range = when.shift()
                conditions.push((n) => range[0] <= n && n <= range[1])
            }
            conditions.push.apply(conditions, _functions(when))
            assert(when.length == 0)
            return conditions
        },
        (when) => {
            const conditions =  [ (a) => Array.isArray(a) ]
            if (Array.isArray(when[0])) {
                const value = when.shift()
                conditions.push((a) =>  a.filter((v, i) => value[i] == v).length == value.length)
            }
            conditions.push.apply(conditions, _functions(when))
            assert(when.length == 0)
            return conditions
        }
    ]
}

function _only (when) {
    if (typeof when[0] == 'boolean') {
        return when.shift()
    }
    return true
}

function _dive (when) {
    if (typeof when[0] == 'number') {
        const dive = when.shift()
        return [ dive, dive ]
    }
    if (
        Array.isArray(when[0]) &&
        when[0].filter((n) => typeof n == 'number').length == 2
    ) {
        return when.shift()
    }
    return [ 0, Infinity ]
}

function _type (when) {
    if (
        typeof when[0] == 'function' &&
        when[0].prototype != null &&
        typeof when[0].prototype == 'object'
    ) {
        return when.shift()
    }
    return Error
}

function _string (when, type) {
    if (typeof when[0] == 'string') {
        const string = when.shift()
        if (type.prototype instanceof Error || type === Error) {
            return [ (e) => e.message == string ]
        }
        return [ (o) => o.toString() == string ]
    } else if (when[0] instanceof RegExp) {
        const regex = when.shift()
        if (type.prototype instanceof Error || type === Error) {
            return [ (e) => regex.test(e.message) ]
        }
        return [ (o) => regex.test(o.toString()) ]
    }
    return []
}

function _properties (when) {
    if (typeof when[0] == 'object' && !Array.isArray(when[0])) {
        const properties = when.shift()
        return [ (o) => {
            for (const property in properties) {
                if (properties[property] instanceof RegExp) {
                    if (!properties[property].test(o[property])) {
                        return false
                    }
                } else {
                    if (properties[property] != o[property]) {
                        return false
                    }
                }
            }
            return true
        } ]
    }
    return []
}

function _functions (when) {
    const functions = []
    for (;;) {
        if (
            typeof when[0] == 'function' &&
            (
                when[0].prototype == null ||
                typeof when[0].prototype != 'object'
            )
        ) {
            functions.push(when.shift())
        } else {
            return functions
        }
    }
}

function _boolean (when) {
    const booleans = []
    for (;;) {
        if (
            typeof when[0] == 'boolean'
        ) {
            const value = when.shift()
            booleans.push(() => value)
        } else {
            return booleans
        }
    }
}

function _callback (vargs) {
    if (typeof vargs[0] == 'function') {
        return vargs.shift()
    }
    if (vargs.length != 0 && !Array.isArray(vargs[0])) {
        const result = vargs.shift()
        return () => result
    }
    return () => {}
}

module.exports = function (error, ...vargs) {
    const cases = []
    if (vargs.length == 1) {
        if (
            Array.isArray(vargs[0]) &&
            Array.isArray(vargs[0][0]) &&
            (
                vargs[0][0].length != 2
                ||
                vargs[0][0].filter((n) => typeof n == 'number').length != 2
            )
        ) {
            vargs = vargs[0]
        }
    }
    while (vargs.length) {
        const parts = [{ dive: [ 0, Infinity ] }]
        const when = vargs.shift()
        const only = vargs.length != 0 && Array.isArray(vargs[0]) && vargs[0].length == 0
            ? ! vargs.shift()
            : true
        while (when.length) {
            const conditions = []
            parts[parts.length - 1].dive = _dive(when)
            const type = _type(when)
            const index = primitives.types.indexOf(type)
            if (~index) {
                conditions.push.apply(conditions, primitives.conditions[index](when))
            } else {
                conditions.push((e) => e instanceof type)
                conditions.push.apply(conditions, _string(when, type))
                conditions.push.apply(conditions, _properties(when))
                conditions.push.apply(conditions, _functions(when))
                conditions.push.apply(conditions, _boolean(when))
            }
            parts.push({ dive: [ 0, 1 ], conditions: conditions })
        }
        const callback = _callback(vargs)
        const path = []
        for (let i = 1, I = parts.length; i < I; i++) {
            const { dive, conditions } = parts[i]
            path.push((e) => {
                for (const condition of conditions) {
                    if (!condition(e)) {
                        return null
                    }
                }
                return dive
            })
        }
        const dive = parts[0].dive
        cases.push({
            test: (error) => {
                const selector = new Selector(error)
                const errors = []
                const found = selector.prune(path, dive)
                if (found.length != 0) {
                    if (only) {
                       return found.length == 1 ? found.shift() : null
                    }
                    return found
                }
                return null
            },
            callback: callback
        })
    }
    for (var i = 0, I = cases.length; i < I; i++) {
        const found = cases[i].test(error)
        if (found != null) {
            return cases[i].callback(found)
        }
    }
    throw error
}
<commit_msg>Remove dead code.<commit_after>const assert = require('assert')
const Selector = require('./selector')

const primitives = {
    types: [ Boolean, Number, String, BigInt, Array ],
    conditions: [
        (when) => {
            const conditions = [ (b) => typeof b == 'boolean' ]
            if (typeof when[0] == 'boolean') {
                const value = when.shift()
                conditions.push((b) => !! b == value)
            }
            conditions.push.apply(conditions, _functions(when))
            assert(when.length == 0)
            return conditions
        },
        (when) => {
            const conditions = [ (n) => typeof n == 'number' ]
            if (typeof when[0] == 'number') {
                const value = when.shift()
                conditions.push((n) => n == value)
            } else if (
                Array.isArray(when[0]) &&
                when[0].map((n) => typeof n == 'number').length == 2
            ) {
                const range = when.shift()
                conditions.push((n) => range[0] <= n && n <= range[1])
            }
            conditions.push.apply(conditions, _functions(when))
            assert(when.length == 0)
            return conditions
        },
        (when) => {
            const conditions = [ (s) => typeof s == 'string' ]
            if (typeof when[0] == 'string') {
                const value = when.shift()
                conditions.push((s) => s.toString() == value)
            } else if (when[0] instanceof RegExp) {
                const regex = when.shift()
                conditions.push((s) => regex.test(s.toString()))
                assert(when.length == 0)
            }
            conditions.push.apply(conditions, _functions(when))
            assert(when.length == 0)
            return conditions
        },
        (when) => {
            const conditions =  [ (n) => typeof n == 'bigint' ]
            if (typeof when[0] == 'bigint') {
                const value = when.shift()
                conditions.push((n) => n == value)
            } else if (
                Array.isArray(when[0]) &&
                when[0].map((n) => typeof n == 'bigint').length == 2
            ) {
                const range = when.shift()
                conditions.push((n) => range[0] <= n && n <= range[1])
            }
            conditions.push.apply(conditions, _functions(when))
            assert(when.length == 0)
            return conditions
        },
        (when) => {
            const conditions =  [ (a) => Array.isArray(a) ]
            if (Array.isArray(when[0])) {
                const value = when.shift()
                conditions.push((a) =>  a.filter((v, i) => value[i] == v).length == value.length)
            }
            conditions.push.apply(conditions, _functions(when))
            assert(when.length == 0)
            return conditions
        }
    ]
}

function _dive (when) {
    if (typeof when[0] == 'number') {
        const dive = when.shift()
        return [ dive, dive ]
    }
    if (
        Array.isArray(when[0]) &&
        when[0].filter((n) => typeof n == 'number').length == 2
    ) {
        return when.shift()
    }
    return [ 0, Infinity ]
}

function _type (when) {
    if (
        typeof when[0] == 'function' &&
        when[0].prototype != null &&
        typeof when[0].prototype == 'object'
    ) {
        return when.shift()
    }
    return Error
}

function _string (when, type) {
    if (typeof when[0] == 'string') {
        const string = when.shift()
        if (type.prototype instanceof Error || type === Error) {
            return [ (e) => e.message == string ]
        }
        return [ (o) => o.toString() == string ]
    } else if (when[0] instanceof RegExp) {
        const regex = when.shift()
        if (type.prototype instanceof Error || type === Error) {
            return [ (e) => regex.test(e.message) ]
        }
        return [ (o) => regex.test(o.toString()) ]
    }
    return []
}

function _properties (when) {
    if (typeof when[0] == 'object' && !Array.isArray(when[0])) {
        const properties = when.shift()
        return [ (o) => {
            for (const property in properties) {
                if (properties[property] instanceof RegExp) {
                    if (!properties[property].test(o[property])) {
                        return false
                    }
                } else {
                    if (properties[property] != o[property]) {
                        return false
                    }
                }
            }
            return true
        } ]
    }
    return []
}

function _functions (when) {
    const functions = []
    for (;;) {
        if (
            typeof when[0] == 'function' &&
            (
                when[0].prototype == null ||
                typeof when[0].prototype != 'object'
            )
        ) {
            functions.push(when.shift())
        } else {
            return functions
        }
    }
}

function _boolean (when) {
    const booleans = []
    for (;;) {
        if (
            typeof when[0] == 'boolean'
        ) {
            const value = when.shift()
            booleans.push(() => value)
        } else {
            return booleans
        }
    }
}

function _callback (vargs) {
    if (typeof vargs[0] == 'function') {
        return vargs.shift()
    }
    if (vargs.length != 0 && !Array.isArray(vargs[0])) {
        const result = vargs.shift()
        return () => result
    }
    return () => {}
}

module.exports = function (error, ...vargs) {
    const cases = []
    if (vargs.length == 1) {
        if (
            Array.isArray(vargs[0]) &&
            Array.isArray(vargs[0][0]) &&
            (
                vargs[0][0].length != 2
                ||
                vargs[0][0].filter((n) => typeof n == 'number').length != 2
            )
        ) {
            vargs = vargs[0]
        }
    }
    while (vargs.length) {
        const parts = [{ dive: [ 0, Infinity ] }]
        const when = vargs.shift()
        const only = vargs.length != 0 && Array.isArray(vargs[0]) && vargs[0].length == 0
            ? ! vargs.shift()
            : true
        while (when.length) {
            const conditions = []
            parts[parts.length - 1].dive = _dive(when)
            const type = _type(when)
            const index = primitives.types.indexOf(type)
            if (~index) {
                conditions.push.apply(conditions, primitives.conditions[index](when))
            } else {
                conditions.push((e) => e instanceof type)
                conditions.push.apply(conditions, _string(when, type))
                conditions.push.apply(conditions, _properties(when))
                conditions.push.apply(conditions, _functions(when))
                conditions.push.apply(conditions, _boolean(when))
            }
            parts.push({ dive: [ 0, 1 ], conditions: conditions })
        }
        const callback = _callback(vargs)
        const path = []
        for (let i = 1, I = parts.length; i < I; i++) {
            const { dive, conditions } = parts[i]
            path.push((e) => {
                for (const condition of conditions) {
                    if (!condition(e)) {
                        return null
                    }
                }
                return dive
            })
        }
        const dive = parts[0].dive
        cases.push({
            test: (error) => {
                const selector = new Selector(error)
                const errors = []
                const found = selector.prune(path, dive)
                if (found.length != 0) {
                    if (only) {
                       return found.length == 1 ? found.shift() : null
                    }
                    return found
                }
                return null
            },
            callback: callback
        })
    }
    for (var i = 0, I = cases.length; i < I; i++) {
        const found = cases[i].test(error)
        if (found != null) {
            return cases[i].callback(found)
        }
    }
    throw error
}
<|endoftext|>