nire0510
3/13/2016 - 6:35 AM

Ember.js Model Generator - Reverse engineer your JSON objects into Ember Data models

Ember.js Model Generator - Reverse engineer your JSON objects into Ember Data models

(function (Ember, undefined) {
  'use strict';
  
  /* *** APPLICATION *** */
  
  // initalize Ember application:
  let App = Ember.Application.create();

  /* *** ROUTER *** */
  
  // initialize Ember router:
  App.Router.map(function() {
    this.route('models');
  });
  
  // set location mode to none:
  App.Router.reopen({
    location: 'none'
  });
  
  /*  ***ROUTES *** */
  
  App.IndexRoute = Ember.Route.extend({
    /* SERVICES */
    data: Ember.inject.service(),
    
    /* HOOKS */
    model() {
      return {
        links: this.get('data').getLinks(1),
        specs: this.get('data').getSpecs()
      }
    },
    
    /* ACTIONS */
    actions: {
      didTransition() {
        // fix textarea height:
        Ember.run.scheduleOnce('afterRender', this, function () {
          $('#json-object').trigger('autoresize');
          Ember.$('select').material_select();
        })
      }
    }
  });
  
  App.ModelsRoute = Ember.Route.extend({
    /* SERVICES */
    data: Ember.inject.service(),
    generator: Ember.inject.service(),
    
    /* HOOKS */
    model() {
      return {
        links: this.get('data').getLinks(2),
        json: {
          jsonObject: this.controllerFor('index').get('jsonObject'),
          jsonSpecification: this.controllerFor('index').get('jsonSpecification'),
          rootModelName: this.controllerFor('index').get('rootModelName')
        },
        models: this.get('generator').generateModels(
          this.controllerFor('index').get('jsonObject'),
          this.controllerFor('index').get('jsonSpecification'),
          this.controllerFor('index').get('rootModelName').toLowerCase(),
          this.controllerFor('index').get('useFragments')
        )
      }
    },
    
    /* ACTIONS */
    actions: {
      didTransition() {
        // Prettify javascript:
        Ember.run.scheduleOnce('afterRender', function () {
          Ember.$('pre code').each(function(i, block) {
            hljs.highlightBlock(block);
          });
        })
      }
    }
  })
  
  /* *** CONTROLLERS *** */
  
  App.ApplicationController = Ember.Controller.extend({
    actions: {
      showCode() {
        top.$('#preview .block-menu span:first').click().click();
        event.target.hidden = true;
      }
    }
  });
  
  App.IndexController = Ember.Controller.extend({
    /* PROPERTIES*/
    jsonString: '',
    jsonObject: null,
    jsonValid: false,
    jsonSpecification: '',
    rootModelName: '',
    useFragments: false,
    jsonStringChanged: Ember.observer('jsonString', function () {
      // parse JSON object:
      this.set('jsonString', this.get('jsonString').replace('\n', '').replace(/\s{2,}/, ' '));
      Ember.run.debounce(this, this.get('actions').validateJSON, 2000);
    }),
    jsonSpecificationChanged: Ember.observer('jsonSpecification', function() {
      // guess JSON specification if its valid:
      Ember.run.next(() => {
        Ember.$('select').material_select();
      });
    }),
    buttonEnabled: Ember.computed('jsonValid', 'jsonSpecification', 'rootModelName', function () {
      return this.get('jsonValid') && 
        this.get('jsonSpecification') !== '' &&
        (this.get('jsonSpecification') !== 'freestyle' ||
          (this.get('jsonSpecification') === 'freestyle' && this.get('rootModelName') !== ''));
    }),
    buttonNotEnabled: Ember.computed.not('buttonEnabled'),
    
    /* ACTIONS */
    actions: {
      didTransition() {
        // fix textarea height:
        Ember.run.scheduleOnce('afterRender', this, function () {
          $('#json-object').trigger('autoresize');
          Ember.$('select').material_select();
        })
      },
      
      /**
       * Clears form and fixes textarea height
       */
      resetForm() {
        this.setProperties({
          jsonString: '',
          jsonObject: null,
          jsonValid: false,
          jsonSpecification: '',
          rootModelName: '',
          useFragments: false
        });
        Ember.run.next(function () {
          $('#json-object').trigger('autoresize');
        });
      },
      
      /**
       * Toggels Ember Data Fragment flag
       */
      toggleFragments() {
        this.toggleProperty('useFragments');
      },
      
      /**
       * Validates JSON structure and if is valid - tries to find JSON specification
       * @param {string} strJSON JSON object content
       */
      validateJSON() {
        try {
          this.set('jsonObject', JSON.parse(this.get('jsonString')));
          this.set('jsonValid', true);
          this.send('guessSpecification');
        } catch (e) {
          Materialize.toast('JSON object is not valid', 4000);
          this.set('jsonValid', false);
        }
      },
      
      /**
       * Guesses given JSON object specification
       */
      guessSpecification() {
        // is it JSON API?
        if (this.get('jsonObject').hasOwnProperty('data') &&
          Array.isArray(this.get('jsonObject').data) &&
          this.get('jsonObject').data[0].hasOwnProperty('type')) {
          
          this.set('jsonSpecification', 'json');
          Materialize.toast(`Looks like a JSON API spec, isn't it?`, 6000);
        }
        else if (Object.keys(this.get('jsonObject')).every(key => typeof this.get('jsonObject')[key] === 'object')) {
          this.set('jsonSpecification', 'rest');
          Materialize.toast(`Wait a minute... It's REST API spec, right???`, 6000);
        }
      }
    }
  });
  
  App.ModelsController = Ember.Controller.extend({
    /* SERVICES */
    generator: Ember.inject.service()
  });
  
  /* *** COMPONENTS *** */
  
  App.NavBarComponent = Ember.Component.extend({});
  
  /* ***SERVICES *** */
  
  App.DataService = Ember.Service.extend({
    // list of links:
    links: [
      {
        route: 'index',
        text: 'CONFIGURATION'
      },
      {
        route: 'models',
        text: 'MODELS'
      }
    ],
    /**
     * Gets links
     * @param {number} intItems Number of links to return
     * @returns {object[]}
     */
    getLinks(intItems) {
      return this.get('links').slice(0, intItems);
    },
    
    /**
     * Gets JSON specifications list
     * @returns {object[]}
     */
    getSpecs() {
      return [
        {
          key: 'json',
          value: 'JSON API'
        },
        {
          key: 'rest',
          value: 'REST API'
        },
        {
          key: 'freestyle',
          value: 'Freestyle'
        }
      ];
    }
  });
  
  App.GeneratorService = Ember.Service.extend({
    /* PROPERTIES */
    models: [],
    
    inflector: Ember.computed(function () {
      return new Ember.Inflector(Ember.Inflector.defaultRules);
    }),
    
    /* ACTIONS */
    /**
     * Generates models out of JSON object
     * @params {object} objJSON Input JSON object
     * @params {string} strJSONSpecification Specification in use in JSON object
     * @params {string} strRootModelName Root model name in case og freestyle JSON
     * @params {boolean} blnUseFragments Indicates whether Ember-data-fragments are in use
     * @returns {object[]}
     */
    generateModels(objJSON, strJSONSpecification, strRootModelName, blnUseFragments) {
      let objJSONInput;
      
      // reset data:
      this.set('models', []);
      
      switch (strJSONSpecification) {
        case 'json':
          objJSON = this.get('convertJSONtoREST').call(this, objJSON);
          // we continue to REST here:
        case 'rest':
          for (var strKey in objJSON) {
            objJSONInput = Array.isArray(objJSON[strKey]) ? objJSON[strKey][0] : objJSON[strKey];
            this.get('revealModel').call(this, this.get('formatModelName').call(this, strKey), objJSONInput, null, null, blnUseFragments);
          }
          break;
        default:  // freestyle
          this.get('revealModel').call(this, strRootModelName, objJSON, null, null, blnUseFragments);
          break;
      }
      
      // reverse models order:
      return this.get('models').reverse();
    },
    
    /**
     * Checks if there is already model which parsed with the same name
     * @param {string} strModelName Model name
     * @returns {boolean}
     */
    hasModel(strModelName) {
      return this.get('models').some(function (model) {
        return model.name === strModelName;
      });
    },
    
    /**
     * Reveals model structure
     * @param {string} strModelName Current model name
     * @param {object} objJSON JSON object to parse
     * @param {string} strParentModel Parent model name (used only on nested models)
     * @param {string} strRelationType Relation type between model and parent (used only on nested models)
     * @param {boolean} blnUseFragments Indicates whether Ember-data-fragments are in use
     */
    revealModel(strModelName, objJSON, strParentModel, strRelationType, blnUseFragments) {
      let modelDefinition = [];
      
      if (this.hasModel(strModelName.dasherize()) === true) {
        return;
      }
      
      modelDefinition.push(`/* app/models/${strModelName.dasherize()}.js */`);
      modelDefinition.push(`import DS from 'ember-data';`);
      if (blnUseFragments === true) {
        modelDefinition.push(`import MF from 'model-fragments';`);
      }
      modelDefinition.push(``);
      modelDefinition.push(`export default ${blnUseFragments && strParentModel ? 'MF.Fragment' : 'DS.Model'}.extend({`);
      for (var strKey in objJSON) {
        switch(typeof objJSON[strKey]) {
          case 'boolean':
          case 'date':
          case 'number':
          case 'string':
            modelDefinition.push(`  ${strKey}: DS.attr('${typeof objJSON[strKey]}'),`);
            break;
          default:
            // sub-model - assuming one to many:
            if (Array.isArray(objJSON[strKey])) {
              if (objJSON[strKey].every(item => typeof item !== 'object')) {
                if (blnUseFragments) {
                  modelDefinition.push(`  ${strKey}: MF.array('${this.get('formatModelName').call(this, strKey)}'),`);
                }
                else {
                  modelDefinition.push(`  ${strKey}: DS.hasMany('UNKNOWN'),`);
                }
              }
              else {
                this.revealModel(this.get('formatModelName').call(this, strKey), objJSON[strKey][0], strModelName, '1:*', blnUseFragments);
                modelDefinition.push(`  ${strKey}: ${blnUseFragments ? 'MF.fragmentArray' : 'DS.hasMany'}('${this.get('formatModelName').call(this, strKey)}'),`);
              }
            }
            // sub-model - assuming one to one
            else if (objJSON[strKey] && typeof objJSON[strKey] === 'object') {
              this.revealModel(this.get('formatModelName').call(this, strKey), objJSON[strKey], strModelName, '1:1', blnUseFragments);
              modelDefinition.push(`  ${strKey}: ${blnUseFragments ? 'MF.fragment' : 'DS.belongsTo'}('${this.get('formatModelName').call(this, strKey)}'),`);
            }
            else {
              modelDefinition.push(`  ${strKey}: DS.attr('UNKNOWN'),`);
            }
            break;
        }
      }
      // Add relation to parent, if any:
      switch (strRelationType) {
        case '1:1':
        case '1:*':
          modelDefinition.push(`  ${strParentModel}: DS.belongsTo('${strParentModel}'),`);
          break;
        case '*:1':
        case '*:*':
          modelDefinition.push(`  ${strParentModel}: DS.hasMany('${strParentModel}'),`);
          break;
      }
      // remove last comma:
      modelDefinition[modelDefinition.length - 1] = modelDefinition[modelDefinition.length - 1].substr(0, modelDefinition[modelDefinition.length - 1].length - 1);
      // close model:
      modelDefinition.push('});');
      
      // add model to collection:
      this.get('models').push({
        name: strModelName,
        definition: modelDefinition
      });
    },
    
    /**
     * Converts JSON API specification JSON object to REST
     * @param {object} objJSON JSON object to convert
     * @returns {object}
     */
    convertJSONtoREST(objJSON) {
      let objJSONOutput = {};
      
      if (this.guessSpecification(objJSON) === 'json') {
        // get main model:
        objJSONOutput[objJSON.data[0].type] = objJSON.data[0].attributes;
        // objJSONOutput[objJSON.data.type].id = objJSON.data.id;
        // get included models:
        if (objJSON.hasOwnProperty('included')) {
          objJSON.included.forEach(function (objJSONIncluded) {
            objJSONOutput[objJSONIncluded.type] = objJSONIncluded.attributes;
            // objJSONOutput[objJSONIncluded.type].id = objJSON.id;
          });
        }
      }
      
      return objJSONOutput;
    },
    
    /**
     * Singularizes & dasherizes model name
     * @param {string} strModelName Model name
     * @returns {string}
     */
    formatModelName(strModelName) {
      strModelName = this.get('inflector').singularize(strModelName);
      return strModelName.dasherize();
    },
    
    /**
     * Guesses given JSON object specification
     */
    guessSpecification(objJSON) {
      // is it JSON API?
      if (objJSON.hasOwnProperty('data') &&
        Array.isArray(objJSON.data) &&
        objJSON.data[0].hasOwnProperty('type')) {
        
        return 'json';
      }
      else if (Object.keys(objJSON).every(key => typeof objJSON[key] === 'object')) {
        return 'rest';
      }
    }
  }),
  
  /* HELPERS */
  
  App.EqHelper = Ember.Helper.extend({
    compute(params/*, hash*/) {
      return params[0] === params[1];
    }
  });
})(Ember);
<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="keywords" content="ember.js, ember-data, model, materializecss">
  <meta name="description" content="Reverse engineer your JSON objects into Ember Data models">
  <meta name="author" content="Nir Elbaz">
  <title>Ember Models Generator</title>
  <!--Import Google Icon Font-->
  <link href="http://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
  <!-- Compiled and minified CSS -->
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/0.97.5/css/materialize.min.css">
  <!-- Code Highlight -->
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.2.0/styles/github.min.css">
  <!--Let browser know website is optimized for mobile-->
  <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
  <meta name="feditor:preset" content="preview"/>
</head>
<body>
  
  <!-- Application template -->
  <script type="text/x-handlebars">
    <main>
      {{outlet}}
    </main>
    <div class="show-code deep-orange darken-4 white-text" {{action "showCode"}}>Show Me The &lt;Code&gt;!</div>
  </script>

  <!-- Index (configuration) route template -->
  <script type="text/x-handlebars" data-template-name="index">
    {{nav-bar links=model.links}}
    
    <div class="container">
      <div class="row">
        <div class="col s12">
          <div class="card">
            <div class="card-content">
              <span class="card-title">Settings</span>
              <p>
                Generating and modifying Ember Data models can be a real bummer sometimes, especially when your database scheme is not fully baked yet.<br/>
                <strong>EMBER MODEL GENERATOR</strong> can ease this task, by reverse engineering JSON objects produced by your APIs.<br><br>
                <p>Fill in the form below and click on the orange button below to generate the models:</p>
              </p>
              <form>
                <div class="row">
                  <div class="input-field col s12">
                    {{textarea id="json-object" value=jsonString class="materialize-textarea active" placeholder="Paste here the JSON object out of which you wish to extract model(s)" required=true}}
                    <label class="active" for="json-object">JSON Object</label>
                  </div>
                </div>
                <div class="row">
                  <div class="input-field col s12 l6">
                    <select id="json-specification" name="json-specification" value=jsonSpecification onchange={{action (mut jsonSpecification) value="target.value"}}>
                      <option value="" selected={{eq "" jsonSpecification}}>Choose the format of your JSON object</option>
                      {{#each model.specs as |spec|}}
                      <option value={{spec.key}} selected={{eq spec.key jsonSpecification}}>{{spec.value}}</option>
                      {{/each}}
                    </select>
                    <label>JSON Specification</label>
                  </div>
                  <div class="input-field col s12 l6 {{unless (eq jsonSpecification "freestyle") "hide"}}">
                    {{input value=rootModelName placeholder="Type here the name of the root model in a singular form" id="root-model" type="text" class="validate"}}
                    <label class="active" for="root-model">Root Model Name</label>
                  </div>
                </div>
                <div class="row">
                  <div class="col s12">
                    <dl>
                      <dt>JSON Specification</dt>
                      <dd>Read more and see sample on the official <a href="http://jsonapi.org/">JSON API website</a></dd>
                      <dt>REST Specification</dt>
                      <dd>A hash which every key represents a different model in its singular form</dd>
                      <dt>Freestyle</dt>
                      <dd>JSON object itself is a model. Specifying the root model name is required.</dd>
                    </dl>
                  </div>
                </div>
                <div class="row">
                  <div class="col s12">
                    {{input type="checkbox" name="fragments" checked=useFragments}}
                    <label for="fragments" {{action "toggleFragments"}}>I use <a href="https://github.com/lytics/ember-data-model-fragments" target="_blank">Ember Data Model Fragments</a> in my project</label>
                  </div>
                </div>
              </form>
            </div>
            <div class="card-action">
              <a class="waves-effect waves-teal btn-flat" {{action "resetForm"}}>Reset Form</a>
              {{#link-to "models" class="waves-effect waves-light btn deep-orange lighten-1" classNameBindings="buttonEnabled::disabled" disabled=buttonNotEnabled}}
              <i class="material-icons left">thumb_up</i>Do your magic
              {{/link-to}}
            </div>
          </div>
        </div>
      </div>
    </div>
  </script>
  
  <!-- Models (output) route template -->
  <script type="text/x-handlebars" data-template-name="models">
    {{nav-bar links=model.links}}
    
    <div class="container">
      <div class="row">
        {{#each model.models as |m|}}
        <div class="col s12 l6">
          <div class="card model">
            <div class="card-content">
              <span class="card-title"><i class="material-icons">reorder</i> {{m.name}} Model</span>
<pre><code class="javascript">
{{#each m.definition as |line|}}
{{line}}
{{/each}}</code></pre>
            </div>
          </div>
        </div>
        {{/each}}
      </div>
    </div>
  </script>

  <!-- Navbar component template -->
  <script type="text/x-handlebars" data-template-name="components/nav-bar">
    <div class="navbar-fixed">
      <nav>
        <div class="nav-wrapper deep-orange darken-2">
          <a class="brand-logo right">Ember Model Generator</a>
          <div class="col s12">
            {{#each links as |link|}}
            {{#link-to link.route class="breadcrumb"}}{{link.text}}{{/link-to}}
            {{/each}}
          </div>
        </div>
      </nav>
    </div>
  </script>
  
  <!-- Import jQuery before materialize.js -->
  <script type="text/javascript" src="https://code.jquery.com/jquery-2.1.1.min.js"></script>
  <!-- Import my beloved, Ember.js -->
  <script src="https://cdnjs.cloudflare.com/ajax/libs/ember.js/2.4.1/ember.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/ember-data.js/2.4.0/ember-data.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/ember.js/2.4.1/ember-template-compiler.js"></script>
  <!-- Compiled and minified materialize JavaScript -->
  <script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/0.97.5/js/materialize.min.js"></script>
  <!-- Syntax highlight -->
  <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.2.0/highlight.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.2.0/languages/javascript.min.js"></script>
</body>
</html>
body {
  background-color: #faf4f1;
}

form {
  margin-top: 1rem;
}

dt {
  color: #999;
  font-weight: bold;
}

.nav-wrapper {
  padding-left: 1rem;
  padding-right: 1rem;
}

.container {
  margin-top: 2rem
}

.brand-logo {
  font-weight: bold;
  text-transform: uppercase;
}

.show-code {
  border-top: 4px solid #ffab91;
  border-bottom: 4px solid #ffab91;
  cursor: pointer;
  font-size: 1.5rem;
  padding: 0.2rem;
  text-align: center;
  transform: rotateZ(45deg);
  position: fixed;
  top: 130px;
  right: -60px;
  width: 300px;
}

@media all and (max-width: 650px) {
  .show-code {
    top: 110px;
  }
}

.card-title {
  text-transform: capitalize;
  font-weight: bold;
}

.card-title .material-icons {
  vertical-align: sub;
}

.card-action {
  text-align: right;
}

/* label color */
.input-field label {
  /* color: #000; */
}
/* label focus color */
.input-field input[type=text]:focus + label,
textarea.materialize-textarea:focus:not([readonly])+label,
.dropdown-content li>span {
  color: #ff8a65;
}
/* label underline focus color */
.input-field input[type=text]:focus {
  border-bottom: 1px solid #ff8a65;
  box-shadow: 0 1px 0 0 #ff8a65;
}
/* valid color */
.input-field input[type=text].valid {
  border-bottom: 1px solid #ff8a65;
  box-shadow: 0 1px 0 0 #ff8a65;
}
/* invalid color */
.input-field input[type=text].invalid,
textarea.materialize-textarea:focus:not([readonly]) {
  border-bottom: 1px solid #f44336;
  box-shadow: 0 1px 0 0 #f44336;
}
/* icon prefix focus color */
.input-field .prefix.active {
  color: #000;
}