foam-framework
Version:
MVC metaprogramming framework
464 lines (399 loc) • 12.6 kB
JavaScript
/**
* @license
* Copyright 2012 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var ObjectToJSON = {
__proto__: Visitor.create(),
visitFunction: function(o) {
return o.toString();
},
visitObject: function(o) {
this.push({
model_: (o.model_.package ? o.model_.package + '.' : '') + o.model_.name
});
this.__proto__.visitObject.call(this, o);
return this.pop();
},
visitProperty: function(o, prop) {
prop.propertyToJSON(this, this.top(), o);
},
visitMap: function(o) {
this.push({});
Visitor.visitMap.call(this, o);
return this.pop();
},
visitMapElement: function(key, value) { this.top()[key] = this.visit(value); },
visitArray: function(o) {
this.push([]);
this.__proto__.visitArray.call(this, o);
return this.pop();
},
visitArrayElement: function (arr, i) { this.top().push(this.visit(arr[i])); }
};
var JSONToObject = {
__proto__: ObjectToJSON.create(),
visitString: function(o) {
try {
return o.substr(0, 9) === 'function(' ?
eval('(' + o + ')') :
o ;
} catch (x) {
console.log(x, o);
return o;
}
},
visitObject: function(o) {
var model = X.lookup(o.model_);
if ( ! model ) throw new Error('Unknown Model: ' + o.model_);
var obj = model.create();
// o.forEach((function(value, key) {
// Workaround for crbug.com/258522
Object_forEach(o, (function(value, key) {
if ( key !== 'model_' ) obj[key] = this.visit(value);
}).bind(this));
return obj;
},
// Substitute in-place
visitArray: Visitor.visitArray,
visitArrayElement: function (arr, i) { arr[i] = this.visit(arr[i]); }
};
CLASS({
name: 'FilteredDAO_',
extends: 'foam.dao.ProxyDAO',
documentation: '<p>Internal use only.</p>',
properties: [
{
name: 'query',
required: true
}
],
methods: {
select: function(sink, options, opt_X) {
return this.delegate.select(sink, options ? {
__proto__: options,
query: options.query ?
AND(this.query, options.query) :
this.query
} : {query: this.query}, opt_X);
},
removeAll: function(sink, options, opt_X) {
return this.delegate.removeAll(sink, options ? {
__proto__: options,
query: options.query ?
AND(this.query, options.query) :
this.query
} : {query: this.query}, opt_X);
},
listen: function(sink, options) {
return this.SUPER(sink, options ? {
__proto__: options,
query: options.query ?
AND(this.query, options.query) :
this.query
} : {query: this.query});
},
toString: function() {
return this.delegate + '.where(' + this.query + ')';
}
}
});
CLASS({
name: 'OrderedDAO_',
extends: 'foam.dao.ProxyDAO',
documentation: function() {/*
<p>Internal use only.</p>
*/},
properties: [
{
name: 'comparator',
required: true
}
],
methods: {
select: function(sink, options, opt_X) {
if ( options ) {
if ( ! options.order )
options = { __proto__: options, order: this.comparator };
} else {
options = {order: this.comparator};
}
return this.delegate.select(sink, options, opt_X);
},
toString: function() {
return this.delegate + '.orderBy(' + this.comparator + ')';
}
}
});
CLASS({
name: 'LimitedDAO_',
extends: 'foam.dao.ProxyDAO',
documentation: function() {/*
<p>Internal use only.</p>
*/},
properties: [
{
name: 'count',
required: true
}
],
methods: {
select: function(sink, options, opt_X) {
if ( options ) {
if ( 'limit' in options ) {
options = {
__proto__: options,
limit: Math.min(this.count, options.limit)
};
} else {
options = { __proto__: options, limit: this.count };
}
}
else {
options = { limit: this.count };
}
return this.delegate.select(sink, options, opt_X);
},
toString: function() {
return this.delegate + '.limit(' + this.count + ')';
}
}
});
CLASS({
name: 'SkipDAO_',
extends: 'foam.dao.ProxyDAO',
documentation: function() {/*
<p>Internal use only.</p>
*/},
properties: [
{
name: 'skip',
required: true,
postSet: function() {
if ( this.skip !== Math.floor(this.skip) )
console.warn('skip() called with non-integer value: ' + this.skip);
}
}
],
methods: {
select: function(sink, options, opt_X) {
options = options ? { __proto__: options, skip: this.skip } : { skip: this.skip };
return this.delegate.select(sink, options, opt_X);
},
toString: function() {
return this.delegate + '.skip(' + this.skip + ')';
}
}
});
CLASS({
name: 'RelationshipDAO',
extends: 'FilteredDAO_',
documentation: 'Adapts a DAO based on a Relationship.',
properties: [
{
name: 'relatedProperty',
required: true
},
{
name: 'relativeID',
required: true
},
{
name: 'query',
lazyFactory: function() {
return AND(NEQ(this.relatedProperty, ''),
EQ(this.relatedProperty, this.relativeID));
}
},
],
methods: [
function put(obj, sink) {
obj[this.relatedProperty.name] = this.relativeID;
this.SUPER(obj, sink);
}
]
});
function atxn(afunc) {
return function(ret) {
if ( GLOBAL.__TXN__ ) {
afunc.apply(this, arguments);
} else {
GLOBAL.__TXN__ = {};
var a = argsToArray(arguments);
a[0] = function() {
GLOBAL.__TXN__ = undefined;
ret();
};
afunc.apply(this, a);
}
};
}
CLASS({
name: 'AbstractDAO',
documentation: function() {/*
The base for most DAO implementations, $$DOC{ref:'.'} provides basic facilities for
$$DOC{ref:'.where'}, $$DOC{ref:'.limit'}, $$DOC{ref:'.skip'}, and $$DOC{ref:'.orderBy'}
operations, and provides for notifications of updates through $$DOC{ref:'.listen'}.
*/},
properties: [
{
name: 'daoListeners_',
transient: true,
hidden: true,
factory: function() { return []; },
compareProperty: function() { return 0; },
}
],
methods: {
update: function(expr) { /* Applies a change to the DAO contents. */
return this.select(UPDATE(expr, this));
},
select: function(sink, options) {
/* Template method. Override to copy the contents of this DAO (filtered or ordered as
necessary) to sink. */
},
remove: function(query, sink) {
/* Template method. Override to remove matching items and put them into sink if supplied. */
},
pipe: function(sink, options) { /* A $$DOC{ref:'.select'} followed by $$DOC{ref:'.listen'}.
Dump our contents to sink, then send future changes there as well. */
sink = this.decorateSink_(sink, options, true);
var fc = this.createFlowControl_();
var self = this;
this.select({
put: function() {
sink.put && sink.put.apply(sink, arguments);
},
remove: function() {
sink.remove && sink.remove.apply(sink, arguments);
},
error: function() {
sink.error && sink.error.apply(sink, arguments);
},
eof: function() {
if ( fc.stopped ) {
sink.eof && sink.eof();
} else {
self.listen(sink, options);
}
}
}, options, fc);
},
decorateSink_: function(sink, options, isListener, disableLimit) {
if ( options ) {
if ( ! disableLimit ) {
if ( options.limit ) sink = limitedSink(options.limit, sink);
if ( options.skip ) sink = skipSink(options.skip, sink);
}
if ( options.order && ! isListener ) {
sink = orderedSink(options.order, sink);
}
if ( options.query ) {
sink = predicatedSink(
options.query.partialEval ?
options.query.partialEval() :
options.query,
sink) ;
}
}
return sink;
},
createFlowControl_: function() {
return {
stop: function() { this.stopped = true; },
error: function(e) { this.errorEvt = e; }
};
},
where: function(query) { /* Return a DAO that contains a filtered subset of this one. */
// only use X if we are an invalid instance without a this.Y
return (this.Y || X).lookup('FilteredDAO_').create({query: query, delegate: this});
},
limit: function(count) { /* Return a DAO that contains a count limited subset of this one. */
return (this.Y || X).lookup('LimitedDAO_').create({count:count, delegate:this});
},
skip: function(skip) { /* Return a DAO that contains a subset of this one, skipping initial items. */
return (this.Y || X).lookup('SkipDAO_').create({skip:skip, delegate:this});
},
orderBy: function() { /* Return a DAO that contains a subset of this one, ordered as specified. */
return (this.Y || X).lookup('OrderedDAO_').create({ comparator: arguments.length == 1 ? arguments[0] : argsToArray(arguments), delegate: this });
},
listen: function(sink, options) { /* Send future changes to sink. */
this.daoListeners_.push(this.decorateSink_(sink, options, true));
},
unlisten: function(sink) { /* Stop sending updates to the given sink. */
var ls = this.daoListeners_;
// if ( ! ls.length ) console.warn('Phantom DAO unlisten: ', this, sink);
for ( var i = 0; i < ls.length ; i++ ) {
if ( ls[i].$UID === sink.$UID ) {
ls.splice(i, 1);
return true;
}
}
if ( DEBUG ) console.warn('Phantom DAO unlisten: ', this, sink);
},
// Default removeAll: calls select() with the same options and
// calls remove() for all returned values.
removeAll: function(sink, options) { /* Default $$DOC{ref:'.removeAll'}: calls
$$DOC{ref:'.select'} with the same options and calls $$DOC{ref:'.remove'}
for all returned values. */
var self = this;
var future = afuture();
this.select({
put: function(obj) {
self.remove(obj, { remove: sink && sink.remove });
}
})(function() {
sink && sink.eof();
future.set();
});
return future.get;
},
/**
* Notify all listeners of update to DAO.
* @param fName the name of the method in the listeners to call.
* possible values: 'put', 'remove'
**/
notify_: function(fName, args) {
// console.log(this.name_, ' ***** notify ', fName, ' args: ', args, ' listeners: ', this.daoListeners_);
for( var i = 0 ; i < this.daoListeners_.length ; i++ ) {
var l = this.daoListeners_[i];
var fn = l[fName];
if ( fn ) {
// Create flow-control object
args[2] = {
stop: (function(fn, l) {
return function() { fn(l); };
})(this.unlisten.bind(this), l),
error: function(e) { /* Don't care. */ }
};
try {
fn.apply(l, args);
} catch(err) {
if ( err !== this.UNSUBSCRIBE_EXCEPTION ) {
console.error('Error delivering event (removing listener): ', fName, err);
if ( DEBUG ) console.error(err.stack); // TODO: make a NO_DEBUGGER flag for mobile debugger mode?
}
this.unlisten(l);
}
}
}
},
}
});
// Experimental, convert all functions into sinks
Function.prototype.put = function() { this.apply(this, arguments); };
Function.prototype.remove = function() { this.apply(this, arguments); };
Function.prototype.reset = function() { this.call(this); };
//Function.prototype.error = function() { this.apply(this, arguments); };
//Function.prototype.eof = function() { this.apply(this, arguments); };