<commit_before>/* @flow */
import * as _ from 'underscore';
import { invertDryadicProperties } from './Properties';
import { mapProperties, className } from './utils';
import CommandNode from './CommandNode';
import type Dryad from './Dryad';
import type DryadPlayer from './DryadPlayer';

/**
 * Manages the tree structure, contexts for nodes,
 * utilities to walk the tree, collect commands and
 * call command middleware.
 */
export default class DryadTree {

  root: ?Dryad;
  tree: ?TreeNode;
  dryads: {[id:string]: Dryad};
  contexts: {[id:string]: Object};
  getClass: Function;
  rootContext: Object;
  nodeLookUp: Map<string,TreeNode>;

  /**
   * @param {Dryad} rootDryad
   * @param {Function} getClass - lookup function
   */
  constructor(rootDryad:?Dryad, getClass:Function, rootContext:Object={}) {
    this.root = rootDryad;
    this.dryads = {};
    this.contexts = {};
    this.getClass = getClass;
    this.rootContext = rootContext;
    // keep a flat lookup table
    this.nodeLookUp = new Map();
    // tree structure
    if (this.root) {
      this.tree = this._makeTree(this.root);
      this.walk((node:TreeNode) => this.nodeLookUp.set(node.id, node));
    }
  }

  /**
   * Depth first traversal of the Dryad tree
   *
   * The function is given arguments:
   *   node {id type children}
   *   Dryad
   *   context
   *   memo
   *
   * @param {Function} fn - called with (dryad, context, node)
   * @param {TreeNode} node - starting node, defaults to the root
   * @param {Object} memo - for usage during recursion
   */
  walk(fn:Function, node:?TreeNode, memo:Object={}) : Object {
    if (!node) {
      node = this.tree;
    }

    if (node) {
      memo = fn(node, this.dryads[node.id], this.contexts[node.id], memo);
      node.children.forEach((child:TreeNode) => {
        memo = this.walk(fn, child, memo);
      });
    }

    return memo;
  }

  /**
   * Collect a tree of command objects from each node for a given method.
   * eg. 'add' 'remove' 'prepareForAdd'
   *
   * @param {String} stateTransitionName
   * @param {TreeNode} node - default is the root
   * @returns {CommandNode}
   */
  collectCommands(stateTransitionName:string, node:TreeNode, player:DryadPlayer) : CommandNode {

    let dryad = this.dryads[node.id];
    let context = this.contexts[node.id];

    let commands:?Object;
    try {
      switch (stateTransitionName) {
        case 'prepareForAdd':
          commands = dryad.prepareForAdd(player);
          break;
        case 'add':
          commands = dryad.add(player);
          break;
        case 'remove':
          commands = dryad.remove(player);
          break;
        // case 'callCommands'
        default:
          throw new Error(`Unsupported command ${stateTransitionName}`);
      }

    } catch(error) {
      player.log.log(`Error during collectCommands "${stateTransitionName}" for node:\n`);
      player.log.log(node);
      throw error;
    }

    return new CommandNode(
      commands,
      context,
      dryad.properties,
      node.id,
      node.children.map(child => this.collectCommands(stateTransitionName, child, player))
    );
  }

  /**
   * Construct a command tree for a single commandObject to be executed
   * with a single node's context.
   *
   * This is for runtime execution of commands,
   * called from streams and async processes initiated during Dryad's .add()
   */
  makeCommandTree(nodeId:string, commands:Object) : Object {
    return new CommandNode(
      commands,
      this.contexts[nodeId],
      this.dryads[nodeId].properties,
      nodeId,
      []
    );
  }

  /**
   * For any properties that are functions, call them with context to get the 'value'.
   * These converted/resolved values are passed into the commands at prepareForAdd/add etc.
   */
  dryadProperties(nodeId:string) : Object {
    let dryad = this.dryads[nodeId];
    let context = this.contexts[nodeId];
    // invertDryadicProperties replaces Dryads in properties with these accessor functions
    return mapProperties(dryad.properties, (value) : any => {
      return _.isFunction(value) ? value(context) : value;
    });
  }

  /**
   * Update a Dryad's context.
   *
   * @param {String} dryadId
   * @param {Object} update - values to assign into context
   * @returns {Object} context
   */
  updateContext(dryadId:string, update:Object) : Object {
    return _.assign(this.contexts[dryadId], update);
  }

  getContext(dryadId:string) : Object {
    return this.contexts[dryadId];
  }

  /**
   * Get a representation of current state of the tree.
   * Contains add|remove|prepared and may hold errors.
   */
  getDebugState() : Object {
    const formatState = (s) => {
      if (!s) {
        return s;
      }

      if (s.error) {
        return `ERROR: ${s.error}`;
      }

      if (s.add) {
        return 'running';
      }

      if (s.remove) {
        return 'removed';
      }

      if (s.prepare) {
        return 'prepared';
      }

      return `Unknown: ${JSON.stringify(s)}`;
    };

    const dbug = (node) => {
      let r = {
        id: node.id,
        class: this.dryads[node.id].constructor.name,
        state: formatState(this.contexts[node.id].state)
        // circular references, cannot print
        // context: JSON.stringify(this.contexts[node.id], null, 2)
      };
      if (node.children.length) {
        r = _.assign(r, {children: node.children.map(dbug)});
      }
      return r;
    };

    if (this.tree) {
      return dbug(this.tree);
    } else {
      return {};
    }
  }
<commit_msg>fix: DryadTree getDebugState didn't report 'prepared'<commit_after>/* @flow */
import * as _ from 'underscore';
import { invertDryadicProperties } from './Properties';
import { mapProperties, className } from './utils';
import CommandNode from './CommandNode';
import type Dryad from './Dryad';
import type DryadPlayer from './DryadPlayer';

/**
 * Manages the tree structure, contexts for nodes,
 * utilities to walk the tree, collect commands and
 * call command middleware.
 */
export default class DryadTree {

  root: ?Dryad;
  tree: ?TreeNode;
  dryads: {[id:string]: Dryad};
  contexts: {[id:string]: Object};
  getClass: Function;
  rootContext: Object;
  nodeLookUp: Map<string,TreeNode>;

  /**
   * @param {Dryad} rootDryad
   * @param {Function} getClass - lookup function
   */
  constructor(rootDryad:?Dryad, getClass:Function, rootContext:Object={}) {
    this.root = rootDryad;
    this.dryads = {};
    this.contexts = {};
    this.getClass = getClass;
    this.rootContext = rootContext;
    // keep a flat lookup table
    this.nodeLookUp = new Map();
    // tree structure
    if (this.root) {
      this.tree = this._makeTree(this.root);
      this.walk((node:TreeNode) => this.nodeLookUp.set(node.id, node));
    }
  }

  /**
   * Depth first traversal of the Dryad tree
   *
   * The function is given arguments:
   *   node {id type children}
   *   Dryad
   *   context
   *   memo
   *
   * @param {Function} fn - called with (dryad, context, node)
   * @param {TreeNode} node - starting node, defaults to the root
   * @param {Object} memo - for usage during recursion
   */
  walk(fn:Function, node:?TreeNode, memo:Object={}) : Object {
    if (!node) {
      node = this.tree;
    }

    if (node) {
      memo = fn(node, this.dryads[node.id], this.contexts[node.id], memo);
      node.children.forEach((child:TreeNode) => {
        memo = this.walk(fn, child, memo);
      });
    }

    return memo;
  }

  /**
   * Collect a tree of command objects from each node for a given method.
   * eg. 'add' 'remove' 'prepareForAdd'
   *
   * @param {String} stateTransitionName
   * @param {TreeNode} node - default is the root
   * @returns {CommandNode}
   */
  collectCommands(stateTransitionName:string, node:TreeNode, player:DryadPlayer) : CommandNode {

    let dryad = this.dryads[node.id];
    let context = this.contexts[node.id];

    let commands:?Object;
    try {
      switch (stateTransitionName) {
        case 'prepareForAdd':
          commands = dryad.prepareForAdd(player);
          break;
        case 'add':
          commands = dryad.add(player);
          break;
        case 'remove':
          commands = dryad.remove(player);
          break;
        // case 'callCommands'
        default:
          throw new Error(`Unsupported command ${stateTransitionName}`);
      }

    } catch(error) {
      player.log.log(`Error during collectCommands "${stateTransitionName}" for node:\n`);
      player.log.log(node);
      throw error;
    }

    return new CommandNode(
      commands,
      context,
      dryad.properties,
      node.id,
      node.children.map(child => this.collectCommands(stateTransitionName, child, player))
    );
  }

  /**
   * Construct a command tree for a single commandObject to be executed
   * with a single node's context.
   *
   * This is for runtime execution of commands,
   * called from streams and async processes initiated during Dryad's .add()
   */
  makeCommandTree(nodeId:string, commands:Object) : Object {
    return new CommandNode(
      commands,
      this.contexts[nodeId],
      this.dryads[nodeId].properties,
      nodeId,
      []
    );
  }

  /**
   * For any properties that are functions, call them with context to get the 'value'.
   * These converted/resolved values are passed into the commands at prepareForAdd/add etc.
   */
  dryadProperties(nodeId:string) : Object {
    let dryad = this.dryads[nodeId];
    let context = this.contexts[nodeId];
    // invertDryadicProperties replaces Dryads in properties with these accessor functions
    return mapProperties(dryad.properties, (value) : any => {
      return _.isFunction(value) ? value(context) : value;
    });
  }

  /**
   * Update a Dryad's context.
   *
   * @param {String} dryadId
   * @param {Object} update - values to assign into context
   * @returns {Object} context
   */
  updateContext(dryadId:string, update:Object) : Object {
    return _.assign(this.contexts[dryadId], update);
  }

  getContext(dryadId:string) : Object {
    return this.contexts[dryadId];
  }

  /**
   * Get a representation of current state of the tree.
   * Contains add|remove|prepared and may hold errors.
   */
  getDebugState() : Object {
    const formatState = (s) => {
      if (!s) {
        return s;
      }

      if (s.error) {
        return `ERROR: ${s.error}`;
      }

      if (s.add) {
        return 'running';
      }

      if (s.remove) {
        return 'removed';
      }

      if (s.prepareForAdd) {
        return 'prepared';
      }

      return `Unknown: ${JSON.stringify(s)}`;
    };

    const dbug = (node) => {
      let context = this.contexts[node.id];
      let state = context.hasOwnProperty('state') ? context.state : undefined;
      let r = {
        id: node.id,
        class: this.dryads[node.id].constructor.name,
        state: formatState(state)
        // circular references, cannot print
        // context: JSON.stringify(this.contexts[node.id], null, 2)
      };
      if (node.children.length) {
        r = _.assign(r, {children: node.children.map(dbug)});
      }
      return r;
    };

    if (this.tree) {
      return dbug(this.tree);
    } else {
      return {};
    }
  }

{'fraction_non_alphanumeric': 0.12715785726997686, 'fraction_numerical': 0.0001779676098949991, 'mean_word_length': 2.5420737472423576, 'pattern_counts': {'":': 0, '<': 5, '<?xml version=': 0, '>': 17, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7173420', 'n_tokens_mistral': 3534, 'n_tokens_neox': 3462, 'n_words': 923}