API Docs for: v2.11.0
Show:

File: packages/ember-application/lib/system/resolver.js

/**
@module ember
@submodule ember-application
*/

import { dictionary } from 'ember-utils';
import { assert, info, get } from 'ember-metal';
import {
  String as StringUtils,
  Object as EmberObject,
  Namespace
} from 'ember-runtime';
import validateType from '../utils/validate-type';
import { getTemplate } from 'ember-glimmer';

export const Resolver = EmberObject.extend({
  /*
    This will be set to the Application instance when it is
    created.

    @property namespace
  */
  namespace: null,
  normalize:         null, // required
  resolve:           null, // required
  parseName:         null, // required
  lookupDescription: null, // required
  makeToString:      null, // required
  resolveOther:      null, // required
  _logLookup:        null  // required
});

/**
  The DefaultResolver defines the default lookup rules to resolve
  container lookups before consulting the container for registered
  items:

  * templates are looked up on `Ember.TEMPLATES`
  * other names are looked up on the application after converting
    the name. For example, `controller:post` looks up
    `App.PostController` by default.
  * there are some nuances (see examples below)

  ### How Resolving Works

  The container calls this object's `resolve` method with the
  `fullName` argument.

  It first parses the fullName into an object using `parseName`.

  Then it checks for the presence of a type-specific instance
  method of the form `resolve[Type]` and calls it if it exists.
  For example if it was resolving 'template:post', it would call
  the `resolveTemplate` method.

  Its last resort is to call the `resolveOther` method.

  The methods of this object are designed to be easy to override
  in a subclass. For example, you could enhance how a template
  is resolved like so:

  ```javascript
  App = Ember.Application.create({
    Resolver: Ember.DefaultResolver.extend({
      resolveTemplate: function(parsedName) {
        let resolvedTemplate = this._super(parsedName);
        if (resolvedTemplate) { return resolvedTemplate; }
        return Ember.TEMPLATES['not_found'];
      }
    })
  });
  ```

  Some examples of how names are resolved:

  ```
  'template:post'           //=> Ember.TEMPLATES['post']
  'template:posts/byline'   //=> Ember.TEMPLATES['posts/byline']
  'template:posts.byline'   //=> Ember.TEMPLATES['posts/byline']
  'template:blogPost'       //=> Ember.TEMPLATES['blogPost']
                            //   OR
                            //   Ember.TEMPLATES['blog_post']
  'controller:post'         //=> App.PostController
  'controller:posts.index'  //=> App.PostsIndexController
  'controller:blog/post'    //=> Blog.PostController
  'controller:basic'        //=> Ember.Controller
  'route:post'              //=> App.PostRoute
  'route:posts.index'       //=> App.PostsIndexRoute
  'route:blog/post'         //=> Blog.PostRoute
  'route:basic'             //=> Ember.Route
  'view:post'               //=> App.PostView
  'view:posts.index'        //=> App.PostsIndexView
  'view:blog/post'          //=> Blog.PostView
  'view:basic'              //=> Ember.View
  'foo:post'                //=> App.PostFoo
  'model:post'              //=> App.Post
  ```

  @class DefaultResolver
  @namespace Ember
  @extends Ember.Object
  @public
*/

export default EmberObject.extend({
  /**
    This will be set to the Application instance when it is
    created.

    @property namespace
    @public
  */
  namespace: null,

  init() {
    this._parseNameCache = dictionary(null);
  },
  normalize(fullName) {
    var [
      type,
      name
    ] = fullName.split(':', 2);

    assert(
      'Tried to normalize a container name without a colon (:) in it. ' +
      'You probably tried to lookup a name that did not contain a type, ' +
      'a colon, and a name. A proper lookup name would be `view:post`.',
      fullName.split(':').length === 2
    );

    if (type !== 'template') {
      var result = name;

      if (result.indexOf('.') > -1) {
        result = result.replace(/\.(.)/g, function(m) {
          return m.charAt(1).toUpperCase();
        });
      }

      if (name.indexOf('_') > -1) {
        result = result.replace(/_(.)/g, function(m) {
          return m.charAt(1).toUpperCase();
        });
      }

      if (name.indexOf('-') > -1) {
        result = result.replace(/-(.)/g, function(m) {
          return m.charAt(1).toUpperCase();
        });
      }

      return type + ':' + result;
    } else {
      return fullName;
    }
  },


  /**
    This method is called via the container's resolver method.
    It parses the provided `fullName` and then looks up and
    returns the appropriate template or class.

    @method resolve
    @param {String} fullName the lookup string
    @return {Object} the resolved factory
    @public
  */
  resolve(fullName) {
    var parsedName = this.parseName(fullName);
    var resolveMethodName = parsedName.resolveMethodName;
    var resolved;

    if (this[resolveMethodName]) {
      resolved = this[resolveMethodName](parsedName);
    }

    resolved = resolved || this.resolveOther(parsedName);

    if (parsedName.root && parsedName.root.LOG_RESOLVER) {
      this._logLookup(resolved, parsedName);
    }

    if (resolved) {
      validateType(resolved, parsedName);
    }

    return resolved;
  },

  /**
    Convert the string name of the form 'type:name' to
    a Javascript object with the parsed aspects of the name
    broken out.

    @param {String} fullName the lookup string
    @method parseName
    @protected
  */

  parseName(fullName) {
    return this._parseNameCache[fullName] || (
      (this._parseNameCache[fullName] = this._parseName(fullName))
    );
  },

  _parseName(fullName) {
    let [
      type,
      fullNameWithoutType
    ] = fullName.split(':');

    var name = fullNameWithoutType;
    var namespace = get(this, 'namespace');
    var root = namespace;
    let lastSlashIndex = name.lastIndexOf('/');
    let dirname = lastSlashIndex !== -1 ? name.slice(0, lastSlashIndex) : null;

    if (type !== 'template' && lastSlashIndex !== -1) {
      let parts = name.split('/');
      name = parts[parts.length - 1];
      let namespaceName = StringUtils.capitalize(parts.slice(0, -1).join('.'));
      root = Namespace.byName(namespaceName);

      assert(
        'You are looking for a ' + name + ' ' + type + ' in the ' +
        namespaceName + ' namespace, but the namespace could not be found',
        root
      );
    }

    let resolveMethodName = fullNameWithoutType === 'main' ? 'Main' : StringUtils.classify(type);

    if (!(name && type)) {
      throw new TypeError('Invalid fullName: `' + fullName + '`, must be of the form `type:name` ');
    }

    return {
      fullName,
      type,
      fullNameWithoutType,
      dirname,
      name,
      root,
      resolveMethodName: 'resolve' + resolveMethodName
    };
  },

  /**
    Returns a human-readable description for a fullName. Used by the
    Application namespace in assertions to describe the
    precise name of the class that Ember is looking for, rather than
    container keys.

    @param {String} fullName the lookup string
    @method lookupDescription
    @protected
  */
  lookupDescription(fullName) {
    let parsedName = this.parseName(fullName);
    let description;

    if (parsedName.type === 'template') {
      return 'template at ' + parsedName.fullNameWithoutType.replace(/\./g, '/');
    }

    description = parsedName.root + '.' + StringUtils.classify(parsedName.name).replace(/\./g, '');

    if (parsedName.type !== 'model') {
      description += StringUtils.classify(parsedName.type);
    }

    return description;
  },

  makeToString(factory, fullName) {
    return factory.toString();
  },

  /**
    Given a parseName object (output from `parseName`), apply
    the conventions expected by `Ember.Router`

    @param {Object} parsedName a parseName object with the parsed
      fullName lookup string
    @method useRouterNaming
    @protected
  */
  useRouterNaming(parsedName) {
    parsedName.name = parsedName.name.replace(/\./g, '_');
    if (parsedName.name === 'basic') {
      parsedName.name = '';
    }
  },
  /**
    Look up the template in Ember.TEMPLATES

    @param {Object} parsedName a parseName object with the parsed
      fullName lookup string
    @method resolveTemplate
    @protected
  */
  resolveTemplate(parsedName) {
    let templateName = parsedName.fullNameWithoutType.replace(/\./g, '/');

    return getTemplate(templateName) || getTemplate(StringUtils.decamelize(templateName));
  },

  /**
    Lookup the view using `resolveOther`

    @param {Object} parsedName a parseName object with the parsed
      fullName lookup string
    @method resolveView
    @protected
  */
  resolveView(parsedName) {
    this.useRouterNaming(parsedName);
    return this.resolveOther(parsedName);
  },

  /**
    Lookup the controller using `resolveOther`

    @param {Object} parsedName a parseName object with the parsed
      fullName lookup string
    @method resolveController
    @protected
  */
  resolveController(parsedName) {
    this.useRouterNaming(parsedName);
    return this.resolveOther(parsedName);
  },
  /**
    Lookup the route using `resolveOther`

    @param {Object} parsedName a parseName object with the parsed
      fullName lookup string
    @method resolveRoute
    @protected
  */
  resolveRoute(parsedName) {
    this.useRouterNaming(parsedName);
    return this.resolveOther(parsedName);
  },

  /**
    Lookup the model on the Application namespace

    @param {Object} parsedName a parseName object with the parsed
      fullName lookup string
    @method resolveModel
    @protected
  */
  resolveModel(parsedName) {
    let className = StringUtils.classify(parsedName.name);
    let factory = get(parsedName.root, className);

    return factory;
  },
  /**
    Look up the specified object (from parsedName) on the appropriate
    namespace (usually on the Application)

    @param {Object} parsedName a parseName object with the parsed
      fullName lookup string
    @method resolveHelper
    @protected
  */
  resolveHelper(parsedName) {
    return this.resolveOther(parsedName);
  },
  /**
    Look up the specified object (from parsedName) on the appropriate
    namespace (usually on the Application)

    @param {Object} parsedName a parseName object with the parsed
      fullName lookup string
    @method resolveOther
    @protected
  */
  resolveOther(parsedName) {
    let className = StringUtils.classify(parsedName.name) + StringUtils.classify(parsedName.type);
    let factory = get(parsedName.root, className);
    return factory;
  },

  resolveMain(parsedName) {
    let className = StringUtils.classify(parsedName.type);
    return get(parsedName.root, className);
  },

  /**
   @method _logLookup
   @param {Boolean} found
   @param {Object} parsedName
   @private
  */
  _logLookup(found, parsedName) {
    let symbol, padding;

    if (found) {
      symbol = '[✓]';
    } else {
      symbol = '[ ]';
    }

    if (parsedName.fullName.length > 60) {
      padding = '.';
    } else {
      padding = new Array(60 - parsedName.fullName.length).join('.');
    }

    info(symbol, parsedName.fullName, padding, this.lookupDescription(parsedName.fullName));
  },

  /**
   Used to iterate all items of a given type.

   @method knownForType
   @param {String} type the type to search for
   @private
   */
  knownForType(type) {
    let namespace = get(this, 'namespace');
    let suffix = StringUtils.classify(type);
    let typeRegexp = new RegExp(`${suffix}$`);

    let known = dictionary(null);
    let knownKeys = Object.keys(namespace);
    for (let index = 0; index < knownKeys.length; index++) {
      let name = knownKeys[index];

      if (typeRegexp.test(name)) {
        let containerName = this.translateToContainerFullname(type, name);

        known[containerName] = true;
      }
    }

    return known;
  },

  /**
   Converts provided name from the backing namespace into a container lookup name.

   Examples:

   App.FooBarHelper -> helper:foo-bar
   App.THelper -> helper:t

   @method translateToContainerFullname
   @param {String} type
   @param {String} name
   @private
   */

  translateToContainerFullname(type, name) {
    let suffix = StringUtils.classify(type);
    let namePrefix = name.slice(0, suffix.length * -1);
    let dasherizedName = StringUtils.dasherize(namePrefix);

    return `${type}:${dasherizedName}`;
  }
});