Last active
September 10, 2021 17:31
-
-
Save nire0510/6478b5134893399879c0 to your computer and use it in GitHub Desktop.
Ember.js Model Generator - Reverse engineer your JSON objects into Ember Data models
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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 <Code>!</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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(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); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment