Skip to content

Commit

Permalink
feat(generators/model): add service API endpoint fitting to a scaffol…
Browse files Browse the repository at this point in the history
…ded model
  • Loading branch information
cueedee committed Nov 14, 2016
1 parent a04056c commit 299a206
Show file tree
Hide file tree
Showing 4 changed files with 241 additions and 8 deletions.
204 changes: 200 additions & 4 deletions generators/model/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ var generators = require( 'yeoman-generator' )
, yosay = require( 'yosay' )
, youtil = require( './../../lib/youtil.js' )
, chalk = require( 'chalk' )
, glob = require( 'glob' )
, _ = require( 'lodash' )
;

Expand Down Expand Up @@ -53,6 +54,22 @@ var ModelGenerator = generators.Base.extend(
}
);

this.option(
'api'
, {
type: String
, desc: 'The name of the API this model should connect to.'
}
);

this.option(
'service'
, {
type: String
, desc: 'The service API endpoint URL this model should connect to (relative to the API\'s base).'
}
);

this.option(
'singleton'
, {
Expand All @@ -66,9 +83,40 @@ var ModelGenerator = generators.Base.extend(
{
this._assertBatApp();

// Find available APIs:
//
var apis = this.apis
= {}
, base = this.destinationPath( 'src/apis' )
;

glob.sync( '**/*.coffee', { cwd: base } ).forEach(

function ( path )
{
var pathAbs = base + '/' + path;
var match = this.fs.read( pathAbs ).match( /@class\s+(\S+)/ );

if ( !( match ) ) { return; }

var className = match[ 1 ]
, name = decapitalize( className.replace( /Api$/, '' ))
;

apis[ name ] =
{
className: className
, pathAbs: pathAbs
, path: path
}
;

}.bind( this )
);

// Container for template expansion data.
//
this.templateData = {};
this.templateData = {};
}

, prompting:
Expand Down Expand Up @@ -98,6 +146,45 @@ var ModelGenerator = generators.Base.extend(
, validate: youtil.isNonBlank
, filter: youtil.sentencify
}
, {
type: 'list'
, name: 'api'
, message: 'Should this model connect to an API?'
, choices: [ '- none -' ].concat( _.keys( this.apis ))
, default: youtil.definedToString( this.options.api )
, validate: function ( value )
{
return value in this.apis;
}.bind( this )
, filter: function ( value ) {
return this.apis[ value ];
}.bind( this )
, when: !( _.isEmpty( this.apis ))
}
, {
type: 'input'
, name: 'service'
, message: (
'To which service API endpoint should this model connect?'
+ chalk.gray ( ' - please enter a URL relative to the API\'s base.' )
)
, default: function ( answers )
{
return (
youtil.definedToString( this.options.service )
|| answers.modelName
|| this.templateData.modelName
);
}.bind( this )
, filter: function ( value )
{
return value.replace( /^\/+/, '' );
}
, when: function ( answers )
{
return answers.api || this.templateData.api;
}.bind( this )
}
, {
type: 'confirm'
, name: 'singleton'
Expand Down Expand Up @@ -126,8 +213,8 @@ var ModelGenerator = generators.Base.extend(

, configuring: function ()
{
var data = this.templateData
, modelName = data.modelName
var data = this.templateData
, modelName = data.modelName
;

_.extend(
Expand All @@ -150,13 +237,122 @@ var ModelGenerator = generators.Base.extend(
var data = this.templateData
, templates =
{
'model.coffee': [ 'src/models/' + data.fileBase + '.coffee' ]
'model.coffee': [ 'src/models/' + data.fileBase + '.coffee' ]
}
;

this._templatesProcess( templates );
}
}

, install: {

updateApi: function () {

var data = this.templateData
, api = data.api
, modelName = data.modelName
;

if ( !( api )) { return; }

//
// Insert the expanded fragment template into the api collection definition.
// Look for a place to insert, preferably at an alfanumerically ordered position.
// Do nothing if an service API endpoint defintion for this model seems to exist already.
//

var fs = this.fs
, collection = fs.read( api.pathAbs )
, matcherDec = /^([ \t]*).*?\bnew\s+ApiServicesCollection\(\s*?(^[ \t]*)?\[[ \t]*(\n)?/m
// 1------1 2-------2 3--3
, match = collection.match( matcherDec )
;

if ( !( match ) )
{
this.log(
'It appears that "' + api.pathAbs + '" does not contains an `ApiServicesCollection`\n'
+ 'Leaving it untouched.'
);

return;
}

var level = ' '
, indent = (( match[ 2 ] != null ) ? match[ 2 ] : ( match[ 1 ] + level ))
, insertAt = match.index + match[ 0 ].length
, padPre = match[ 3 ] ? '' : '\n'
, padPost = match[ 3 ] ? '' : indent
, matcherDef = /^(([ \t]*)([ \t]+))###\*[\s\S]*?^\1###[\s\S]*?^\1id:\s*'([^\]]*?)'[^\]]*?^\2(?:,[ \t]*(\n)?|(?=(\])))/mg
// ^12======23======31 ^\1 ^\1 4-------4 ^\2 5--5 6--6
;

// Start looking for definitions directly after API declaration opening.
//
matcherDef.lastIndex = insertAt;

// Find a place to insert
//
while ( (( match = matcherDef.exec( collection ) )) )
{
level = match[ 3 ];

if ( modelName > match[ 4 ] )
{
// Possibly insert after this match.
//
indent = match[ 2 ];
insertAt = match.index + match[ 0 ].length;
padPre = match[ 5 ] ? '' : match[ 6 ] ? ',\n' : '\n';
padPost = match[ 5 ] ? '' : indent;
continue;
}

if ( modelName < match[ 4 ] )
{
// Insert before this match.
//
insertAt = match.index;
padPre = '';
padPost = '';
break;
}

this.log(
'It appears that "' + api.pathAbs + '" already contains a service API endpoint definition for "' + data.className + '".\n'
+ 'Leaving it untouched.'
);

return;
}

// Avoid conflict warning.
//
this.conflicter.force = true;

// Expand fragment template and read it back.
//
var fragmentPath = 'src/apis/api-service-literal-fragment.coffee'
, fragmentDst = this.destinationPath( fragmentPath )
;

this._templatesProcess( [ [ fragmentPath ] ] );

var fragment = fs.read( fragmentDst );

fs.write(
api.pathAbs
, collection.slice( 0, insertAt )
+ padPre
+ fragment.replace( /^ /mg, level ).replace( /^(?=.*?\S)/mg, indent )
+ padPost
+ collection.slice( insertAt )
);

fs.delete( fragmentDst );
}
}
}
);

Expand Down
30 changes: 26 additions & 4 deletions generators/model/templates/model.coffee
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
( ( factory ) ->
if typeof exports is 'object'
module.exports = factory(
require( '<%- backbone.modulePath %>' )
require( '<%- backbone.modulePath %>' )<% if ( api ) { %>

require( './../apis/<%- api.path %>' )<% } %>
)
else if typeof define is 'function' and define.amd
define( [
'<%- backbone.modulePath %>'
'<%- backbone.modulePath %>'<% if ( api ) { %>

'./../apis/<%- api.path %>'<% } %>
], factory )
return
)((
<%- backbone.className %>
<%- backbone.className %><% if ( api ) { %>

api<% } %>
) ->

###*
Expand Down Expand Up @@ -63,7 +69,23 @@
# @final
###

defaults: {}<% if ( singleton ) { %>
defaults: {}<% if ( api ) { %>


###*
# Service API endpoint; defined in the {{#crossLink '<%- api.className %>/<%- modelName %>:attribute'}}<%- api.className %>{{/crossLink}}.
#<% if ( singleton ) { %>
# @property url<% } else { %>
# @property urlRoot<% } %>
# @type ApiServiceModel
# @static
# @final
#
# @default '<<%- api.className %>.url>/<%- service %>'
###
<% if ( singleton ) { %>
url: api.get( '<%- modelName %>' )<% } else { %>
urlRoot: api.get( '<%- modelName %>' )<% } %><% } %><% if ( singleton ) { %>


## Export singleton.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
###*
# Service API endpoint for accessing <% if ( singleton ) { %>the <% } %>{{#crossLink '<%- className %>'}}<%- modelName %> resource<% if ( ! singleton ) { %>s<% } %>{{/crossLink}}.
#
# @attribute <%- modelName %>
# @type ApiServiceModel
# @final
#
# @default '<<%- api.className %>.url>/<%- service %>'
###

id: '<%- modelName %>'
urlPath: '<%- service %>'

,
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"dependencies": {
"chalk": "^1.1.3",
"coffee-script": "^1.11.0",
"glob": "^7.0.5",
"language-tags": "^1.0.5",
"lodash": "^4.14.1",
"mkdirp": "^0.5.1",
Expand Down

0 comments on commit 299a206

Please sign in to comment.