docs: function zValidateInfo() {
  
          // Info describing a bound entity's property's validation
          // 'scope' is the scope of the binding
          //      'scope.$eval(text)' evaluates 'text' in the context of that scope
          //      'scope.entityAspect' returns an EntityAspect if the scope is an Entity
          //      while this is an Ng concept, it could be modeled in other frameworks
          //
          // 'modelPath' is the entity property's data binding specification
          // by default the property from which validation information should be obtained.
          //
          // 'validationPath' is an alternative specification of the entity property
          // from which validation information should be obtained.
          function Info(scope, modelPath, validationPath) {
  
              // need some path info from either of these attrs or it's pointless
              if (!modelPath && !validationPath) { return; }
  
              this.scope = scope;
  
              setEntityAndPropertyPaths(this, modelPath, validationPath);
  
              this.getEntityAspect = this.entityPath ?
                      getEntityAspectFromEntityPath(this) :
                      getEntityAspect(this);
  
              this.getValErrs = createGetValErrs(this);
              this.isRequired = undefined; // don't know initially
          }
  
          Info.prototype = {
              constructor: Info,
              getIsRequired: getIsRequired,
              getType: getType
          };
  
          return {
              create: create,
          };
  
          /*** zValidateInfo implementation ***/
  
          // Create info about the data bound entity property
          function create(scope, modelPath, validationPath) {
              return new Info(scope, modelPath, validationPath);
          }
  
          // Create the 'getValErrs' function that will be watched
          function createGetValErrs(info) {
              return function () {
                  var aspect = info.getEntityAspect();
                  if (aspect) {
                      var errs = aspect.getValidationErrors(info.propertyPath);
                      if (errs.length) {
                          return errs
                              // concatenate all errors into a single string
                              .map(function (e) { return e.errorMessage; })
                              .join('; ');
                      }
                      return '';
                  }
                  // No data bound entity yet.
                  // Return something other than a string so that
                  // watch calls `valErrsChanged` when an entity is bound
                  return null;
              };
          }
  
          function getEntityAspect(info) {
              return function () {
                  return info.scope.entityAspect;
              }
          }
  
          function getEntityAspectFromEntityPath(info) {
              return function () {
                  try {
                      var entity = info.scope.$eval(info.entityPath);
                      if (!entity.entityAspect) {
                          return info.scope.$eval(info.entityPath)['complexAspect'].getEntityAspect();
                      }
                      return info.scope.$eval(info.entityPath)['entityAspect'];
                  }                
                  catch (_) { return undefined; }
              }
          }
  
          // determine if bound property is required.
          function getIsRequired() {
              var info = this;
              if (info.isRequired !== undefined) { return info.isRequired; }
  
              // We don't know if it is required yet.
              // Once bound to the entity we can determine whether the data property is required
              // Note: Not bound until *second* call to the directive's link function
              //       which is why you MUST call 'getIsRequired'
              //       inside 'valErrsChanged' rather than in the link function
              var entityType = info.getType();
              if (entityType) { // the bound entity is known
                  var requiredProperties =
                      getRequiredPropertiesForEntityType(entityType);
  
                  return info.isRequired = !!requiredProperties[info.propertyPath];
              }
              return undefined; // don't know yet
          }
  
          function getType() {
              var aspect = this.getEntityAspect();
              return aspect ? aspect.entity.entityType : null;
          }
  
          /*
          * getRequiredPropertiesForEntityType
          * Returns a hash of property names of properties that are required.
          * Creates that hash lazily and adds it to the
          * entityType's metadata for easier access by this directive
          */
          function getRequiredPropertiesForEntityType(type) {
              if (type.custom && type.custom.required) {
                  return type.custom.required;
              }
  
              // Don't yet know the required properties for this type
              // Find out now
              if (!type.custom) {
                  type.custom = {};
              }
              var required = {};
              type.custom.required = required;
              var props = type.getProperties();
              var childProps = getComplexChildProperties(props);
              props = props.concat(childProps);            
              props.forEach(function (prop) {
                  var vals = prop.validators;
                  for (var i = vals.length; i--;) {
                      var val = vals[i];
                      // Todo: add the 'isRequired' property to breeze.Validator.required validator
                      if (val.context.isRequired || val.name === 'required') {
                          required[prop.name] = true;
                          break;
                      }
                  }
              });
              return required;
          }
  
          function getComplexChildProperties(props) {
              var children = [];
              props.forEach(function (prop) {
                  if (prop.isComplexProperty) {
                      children = children.concat(prop.dataType.getProperties());
                  }
              });
              return children;
          }
   
          function setEntityAndPropertyPaths(info, modelPath, validationPath) {
  
              // examples:
              //   'productId'               // property only
              //   'vm.order.delivery'       // entity path and property
              //   'vm.order["delivery"]'    // entity path and indexed property
              if (modelPath) {
                  parsePath(modelPath);
              }
              // validationPath can override either entity or property path;
              // examples:
              //   'productId'               // property only
              //   'vm.order.delivery'       // entity path and property
              //   'vm.order["delivery"]'    // entity path and indexed property
              //
              // optional ','  syntax as {entity, property} path separator
              // so can separate entity path from a complex property path
              // examples:
              //   'vm.order,address.street' // entity w/ complex prop
              //   'vm.order,address[street]' // entity w/ complex indexed prop
              if (validationPath) {
                  // Look for ',' syntax
                  var paths = validationPath.split(',');
                  var pPath = paths.pop(); // after ','
                  var ePath = paths.pop(); // before ','
                  if (ePath) { info.entityPath = ePath.trim(); }
  
                  if (info.entityPath) {
                      info.propertyPath = pPath;
                  } else {
                      // Didn't use ',' syntax and didn't specify entityPath in model.
                      // Therefore entire path spec must be in pPath; parse it.
                      parsePath(pPath);
                  }
              }
  
              function parsePath(path) {
                  if (path[path.length - 1] === ']') {
                      parseIndexedPaths(path);
                  } else {
                      parseDottedPath(path);
                  }
              }
  
              function parseDottedPath(path) {
                  // ex: 'vm.order.delivery'
                  // propertyPath should be 'delivery'
                  // entityPath should be 'vm.order'
                  paths = path.split('.');
                  info.propertyPath = paths.pop(); // property is after last '.'
                  info.entityPath = paths.join('.'); // path to entity is before last '.'
              }
  
              // extract paths from strings using square-bracket notation, e.g. 'vm.order[delivery]'
              function parseIndexedPaths(path) {
                  var opensb = path.lastIndexOf('[');
                  info.entityPath = path.substring(0, opensb);  // path to entity is before last [
                  var propertyPath = path.substring(opensb + 1, path.length - 1); // property is between [ ]
                  // eval it, in case it's an angular expression
                  try { var evalPath = info.scope.$eval(propertyPath); }
                  catch (_) { }
                  info.propertyPath = evalPath ? evalPath : propertyPath;
              }
          }
      }