This is another installment to a short series started about 10 days ago. If you remember, I am building a code generator that can produce 100% of the code for a business application targeting the MEAN (for Mongo, Express, Node.js and Angular) stack.
What happened since the last update
Since the previous post, a lot of progress has been made. Much of the code for a domain model class is being generated (including state machine support), as you can see below (bear in mind there are still quite a few gaps – but I still have 3 days!):
var EventEmitter = require('events').EventEmitter;
/**
* An issue describes a problem report, a feature request or just a work item for a project. Issues are reported by and
* assigned to users, and go through a lifecycle from the time they are opened until they are resolved and eventually
* closed.
*/
var issueSchema = new Schema({
summary : String,
issueId : Number,
issueKey : String,
reportedOn : Date,
severity : Severity,
status : Status,
resolution : Resolution,
resolvedOn : Date,
votes : Number,
commentCount : Number,
waitingFor : String,
mine : Boolean,
free : Boolean,
description : String
});
/*************************** ACTIONS ***************************/
/**
* Report a new issue.
*/
issueSchema.statics.reportIssue = function (project, summary, description, severity) {
newIssue = new Issue();
newIssue.summary = summary;
newIssue.description = description;
newIssue.severity = severity;
newIssue.reporter = User.current;
newIssue.project = project;
newIssue.userNotifier.issueReported(newIssue.issueKey, summary, description, newIssue.reporter.email);
};
/**
* Release the issue so another committer can work on it.
*/
issueSchema.methods.release = function () {
this.assignee = null;
};
/**
* Assign an issue to a user.
*/
issueSchema.methods.assign = function (newAssignee) {
this.assignee = newAssignee;
};
/**
* Suspend work on this issue.
*/
issueSchema.methods.suspend = function () {};
/**
* Start/resume work on this issue.
*/
issueSchema.methods.start = function () {};
/**
* Resolve the issue.
*/
issueSchema.methods.resolve = function (resolution) {
this.resolvedOn = new Date();
this.resolution = resolution;
};
/**
* Reopen the issue.
*/
issueSchema.methods.reopen = function (reason) {
this.resolvedOn = null;
this.resolution = null;
if (reason.notEquals("")) {
this.comment(reason);
}
};
/**
* Add a comment to the issue
*/
issueSchema.methods.comment = function (text) {
this.addComment(text, null);
};
issueSchema.methods.addWatcher = function (userToAdd) {
this.issuesWatched = userToAdd;
};
issueSchema.methods.vote = function () {
this.voted = User.current;
};
issueSchema.methods.withdrawVote = function () {
delete this.voted;
};
/**
* Take over an issue currently available.
*/
issueSchema.methods.assignToMe = function () {
this.assignee = User.current;
};
/**
* Take over an issue currently assigned to another user (not in progress).
*/
issueSchema.methods.steal = function () {
this.assignee = User.current;
};
/**
* Close the issue marking it as verified.
*/
issueSchema.methods.verify = function () {
};
/*************************** QUERIES ***************************/
issueSchema.statics.bySeverity = function (toMatch) {
return this.model('Issue').find().where('severity').eq(toMatch).exec();
};
issueSchema.statics.byStatus = function (toMatch) {
return Issue.filterByStatus(this.model('Issue').find(), toMatch).exec();
};
/*************************** DERIVED PROPERTIES ****************/
issueSchema.methods.getIssueKey = function () {
return this.project.token + "-" + this.issueId;
};
issueSchema.methods.getVotes = function () {
return ;
};
issueSchema.methods.getCommentCount = function () {
return ;
};
issueSchema.methods.getWaitingFor = function () {
return "" + + " day(s)";
};
issueSchema.methods.getMine = function () {
return User.current == this.assignee;
};
issueSchema.methods.getFree = function () {
return this.assignee == null;
};
/*************************** PRIVATE OPS ***********************/
issueSchema.methods.referenceDate = function () {
if (this.resolvedOn == null) {
return new Date();
} else {
return this.resolvedOn;
}
};
issueSchema.statics.filterByStatus = function (issues, toMatch) {
return issues.where('status').eq(toMatch);
};
issueSchema.methods.addComment = function (text, inReplyTo) {
comment = new Comment();
comment.user = User.current;
comment.on = new Date();
comment.commented = text;
comment.inReplyTo = inReplyTo;
this.issue = comment;
this.userNotifier.commentAdded(this.issueKey, comment.user.email, this.reporter.email, text);
};
/*************************** STATE MACHINE ********************/
Issue.emitter.on('resolve', function () {
if (this.status == 'Open') {
this.status = 'Resolved';
return;
}
if (this.status == 'Assigned') {
this.status = 'Resolved';
return;
}
});
Issue.emitter.on('assignToMe', function () {
if (this.status == 'Open') {
this.status = 'Assigned';
return;
}
});
Issue.emitter.on('assign', function () {
if (this.status == 'Open') {
this.status = 'Assigned';
return;
}
});
Issue.emitter.on('suspend', function () {
if (this.status == 'InProgress') {
this.status = 'Assigned';
return;
}
});
Issue.emitter.on('release', function () {
if (this.status == 'Assigned') {
this.status = 'Open';
return;
}
});
Issue.emitter.on('steal', function () {
if (this.status == 'Assigned') {
this.status = 'Assigned';
return;
}
});
Issue.emitter.on('start', function () {
if (this.status == 'Assigned') {
this.status = 'InProgress';
return;
}
});
Issue.emitter.on('verify', function () {
if (this.status == 'Resolved') {
this.status = 'Verified';
return;
}
});
Issue.emitter.on('reopen', function () {
if (this.status == 'Verified') {
this.status = 'Open';
return;
}
});
var Issue = mongoose.model('Issue', issueSchema);
Issue.emitter = new EventEmitter();
This is just for one of the entity classes defined in the ShipIt example application, here is an excerpt of the original model that covers the Issue entity:
(*
An issue describes a problem report, a feature request or just a work item for a project.
Issues are reported by and assigned to users, and go through a lifecycle from the time
they are opened until they are resolved and eventually closed.
*)
class Issue
attribute summary : String;
derived id attribute issueId : Integer;
derived attribute issueKey : String := {
self.project.token + "-" + self.issueId
};
attribute labels : Label[*];
attribute project : Project;
port userNotifier : UserNotifier;
readonly attribute reportedOn : Date := { Date#today() };
readonly attribute reporter : User;
readonly attribute assignee : User[0, 1];
attribute severity : Severity := Major;
attribute status : Status;
readonly attribute resolution : Resolution[0, 1];
readonly attribute resolvedOn : Date[0, 1];
readonly attribute comments : Comment[*];
attribute watchers : User[*];
derived attribute votes : Integer := {
self<-VotedIssues->voters.size()
};
derived attribute commentCount : Integer := { self.comments.size() };
derived attribute waitingFor : String := {
"" + self.reportedOn.differenceInDays(self.referenceDate()) + " day(s)"
};
private derived attribute mine : Boolean := {
User#current == self.assignee
};
private derived attribute free : Boolean := { self.assignee == null };
private query referenceDate() : Date;
begin
if (self.resolvedOn == null) then
return Date#today()
else
return self.resolvedOn;
end;
attribute description : Memo;
(* Report a new issue. *)
static operation reportIssue(project : Project, summary : String, description : Memo,
severity : Severity := Normal)
precondition Must_be_logged_in { User#provisioned };
begin
var newIssue;
newIssue := new Issue;
newIssue.summary := summary;
newIssue.description := description;
newIssue.severity := severity;
newIssue.reporter := User#current;
newIssue.project := project;
send IssueReported(
issueKey := newIssue.issueKey,
summary := summary,
description := description,
userEmail := newIssue.reporter.email) to newIssue.userNotifier;
end;
static query bySeverity(toMatch : Severity) : Issue[*];
begin
return Issue extent.select((i : Issue) : Boolean {
i.severity == toMatch
});
end;
private static query filterByStatus(issues : Issue[*], toMatch : Status) : Issue[*];
begin
return issues.select((issue : Issue) : Boolean {
issue.status == toMatch
});
end;
static query byStatus(toMatch : Status) : Issue[*];
begin
return Issue#filterByStatus(Issue extent, toMatch);
end;
(* Release the issue so another committer can work on it. *)
operation release()
precondition { self.mine };
begin
self.assignee := null;
end;
(* Assign an issue to a user. *)
operation assign(newAssignee : User)
precondition { self.mine or self.free };
begin
self.assignee := newAssignee;
end;
(* Suspend work on this issue. *)
operation suspend()
precondition { self.mine };
(* Start/resume work on this issue. *)
operation start()
precondition { self.mine };
(* Resolve the issue. *)
operation resolve(resolution : Resolution := Fixed)
precondition { self.mine or self.free };
begin
self.resolvedOn := Date#today();
self.resolution := resolution;
end;
(* Reopen the issue. *)
operation reopen(reason : Memo);
begin
self.resolvedOn := null;
self.resolution := null;
if (reason != "") then
self.comment(reason);
end;
(* Add a comment to the issue *)
operation comment(text : Memo);
begin
self.addComment(text, null);
end;
private operation addComment(text : Memo, inReplyTo : Comment);
begin
var comment;
comment := new Comment;
comment.user := User#current;
comment.\on := Date#today();
comment.commented := text;
comment.inReplyTo := inReplyTo;
link IssueComments(issue := self, comments := comment);
send CommentAdded(
issueKey := self.issueKey,
author := comment.user.email,
userEmail := self.reporter.email,
comment := text) to self.userNotifier;
end;
operation addWatcher(userToAdd : User);
begin
link WatchedIssues(issuesWatched := self, watchers := userToAdd);
end;
operation vote()
precondition { not (User#current == null) }
precondition { ! self.mine }
precondition {
! self<-VotedIssues->voters.includes(User#current)
};
begin
link VotedIssues(voted := self, voters := User#current);
end;
operation withdrawVote()
precondition { not (User#current == null) }
precondition {
self<-VotedIssues->voters.includes(User#current)
};
begin
unlink VotedIssues(voted := self, voters := User#current);
end;
(* Take over an issue currently available. *)
operation assignToMe()
precondition { User#current.committer }
precondition { not self.mine };
begin
self.assignee := User#current;
end;
(* Take over an issue currently assigned to another user (not in progress). *)
operation steal()
precondition { User#current.committer }
precondition { not (self.mine) };
begin
self.assignee := User#current;
end;
(* Close the issue marking it as verified. *)
operation verify();
begin
end;
statemachine Status
initial state Open
transition on call(resolve) to Resolved;
transition on call(assignToMe) to Assigned;
transition on call(assign) to Assigned;
end;
state InProgress
transition on call(suspend) to Assigned;
end;
state Assigned
transition on call(release) to Open;
transition on call(resolve) to Resolved;
transition on call(steal) to Assigned;
transition on call(start) to InProgress;
end;
state Resolved
transition on call(verify) to Verified;
end;
state Verified
transition on call(reopen) to Open;
end;
end;
end;
More examples here.
Xtend is still making the difference
What an awesome language for writing code generators Xtend is. See, for example, how simple is the code to implement state machine support in Javascript, including guard conditions on transitions, and automatically triggering of on-entry and on-exit behavior:
def generateStateMachine(StateMachine stateMachine, Class entity) {
val stateAttribute = entity.findStateProperties.head
if (stateAttribute == null) {
return ''
}
val triggersPerEvent = stateMachine.findTriggersPerEvent
triggersPerEvent.entrySet.map[generateEventHandler(entity, stateAttribute, it.key, it.value)].join('\n')
}
def generateEventHandler(Class entity, Property stateAttribute, Event event, List triggers) {
val modelName = entity.name;
'''
«modelName».emitter.on('«event.generateName»', function () {
«IF (triggers.exists[(it.eContainer as Transition).guard != null])»
var guard;
«ENDIF»
«triggers.map[generateHandlerForTrigger(entity, stateAttribute, it)].join('\n')»
});
'''
}
def generateHandlerForTrigger(Class entity, Property stateAttribute, Trigger trigger) {
val transition = trigger.eContainer as Transition
val originalState = transition.source
val targetState = transition.target
'''
«transition.generateComment»if (this.«stateAttribute.name» == '«originalState.name»') {
«IF (transition.guard != null)»
guard = «generatePredicate(transition.guard)»;
if (guard()) {
«generateStateTransition(stateAttribute, originalState, targetState)»
}
«ELSE»
«generateStateTransition(stateAttribute, originalState, targetState)»
«ENDIF»
}'''
}
def generateStateTransition(Property stateAttribute, Vertex originalState, Vertex newState) {
'''
«IF (originalState instanceof State)»
«IF (originalState.exit != null)»
(function() «generateActivity(originalState.exit as Activity)»)();
«ENDIF»
«ENDIF»
this.«stateAttribute.name» = '«newState.name»';
«IF (newState instanceof State)»
«IF (newState.entry != null)»
(function() «generateActivity(newState.entry as Activity)»)();
«ENDIF»
«ENDIF»
return;
'''
}
def generateName(Event e) {
switch (e) {
CallEvent : e.operation.name
SignalEvent : e.signal.name
TimeEvent : '_time'
AnyReceiveEvent : '_any'
default : unsupportedElement(e)
}
}
Next steps
Sorry if I am being a bit too terse. Will be glad to reply to any questions or comments, but time is running out fast and there is lots to be done still. For the next update, I hope to have Express route generation for the REST API and hopefully will have started on test suite generation. Let’s see how that goes!