1 // ==========================================================================
  2 // Project:   SproutCore - JavaScript Application Framework
  3 // Copyright: ©2006-2011 Strobe Inc. and contributors.
  4 //            Portions ©2008-2011 Apple Inc. All rights reserved.
  5 // License:   Licensed under MIT license (see license.js)
  6 // ==========================================================================
  7 
  8 sc_require('models/record');
  9 
 10 /** @class
 11 
 12   A RecordAttribute describes a single attribute on a record.  It is used to
 13   generate computed properties on records that can automatically convert data
 14   types and verify data.
 15 
 16   When defining an attribute on an SC.Record, you can configure it this way:
 17 
 18       title: SC.Record.attr(String, {
 19         defaultValue: 'Untitled',
 20         isRequired: YES|NO
 21       })
 22 
 23   In addition to having predefined transform types, there is also a way to
 24   set a computed relationship on an attribute. A typical example of this would
 25   be if you have record with a parentGuid attribute, but are not able to
 26   determine which record type to map to before looking at the guid (or any
 27   other attributes). To set up such a computed property, you can attach a
 28   function in the attribute definition of the SC.Record subclass:
 29 
 30       relatedToComputed: SC.Record.toOne(function() {
 31         return (this.readAttribute('relatedToComputed').indexOf("foo")==0) ? MyApp.Foo : MyApp.Bar;
 32       })
 33 
 34   Notice that we are not using .get() to avoid another transform which would
 35   trigger an infinite loop.
 36 
 37   You usually will not work with RecordAttribute objects directly, though you
 38   may extend the class in any way that you like to create a custom attribute.
 39 
 40   A number of default RecordAttribute types are defined on the SC.Record.
 41 
 42   @extends SC.Object
 43   @see SC.Record
 44   @see SC.ManyAttribute
 45   @see SC.SingleAttribute
 46   @since SproutCore 1.0
 47 */
 48 SC.RecordAttribute = SC.Object.extend(
 49   /** @scope SC.RecordAttribute.prototype */ {
 50   /**
 51     Walk like a duck.
 52 
 53     @type Boolean
 54     @default YES
 55   */
 56   isRecordAttribute: YES,
 57 
 58   /**
 59     The default value.  If attribute is `null` or `undefined`, this default
 60     value will be substituted instead.  Note that `defaultValue`s are not
 61     converted, so the value should be in the output type expected by the
 62     attribute.
 63 
 64     If you use a `defaultValue` function, the arguments given to it are the
 65     record instance and the key.
 66 
 67     @type Object|function
 68     @default null
 69   */
 70   defaultValue: null,
 71 
 72   /**
 73     The attribute type.  Must be either an object class or a property path
 74     naming a class.  The built in handler allows all native types to pass
 75     through, converts records to ids and dates to UTF strings.
 76 
 77     If you use the `attr()` helper method to create a RecordAttribute instance,
 78     it will set this property to the first parameter you pass.
 79 
 80     @type Object|String
 81     @default String
 82   */
 83   type: String,
 84 
 85   /**
 86     The underlying attribute key name this attribute should manage.  If this
 87     property is left empty, then the key will be whatever property name this
 88     attribute assigned to on the record.  If you need to provide some kind
 89     of alternate mapping, this provides you a way to override it.
 90 
 91     @type String
 92     @default null
 93   */
 94   key: null,
 95 
 96   /**
 97     If `YES`, then the attribute is required and will fail validation unless
 98     the property is set to a non-null or undefined value.
 99 
100     @type Boolean
101     @default NO
102   */
103   isRequired: NO,
104 
105   /**
106     If `NO` then attempts to edit the attribute will be ignored.
107 
108     @type Boolean
109     @default YES
110   */
111   isEditable: YES,
112 
113   /**
114     If set when using the Date format, expect the ISO8601 date format.
115     This is the default.
116 
117     @type Boolean
118     @default YES
119   */
120   useIsoDate: YES,
121 
122   /**
123     Can only be used for toOne or toMany relationship attributes. If YES,
124     this flag will ensure that any related objects will also be marked
125     dirty when this record dirtied.
126 
127     Useful when you might have multiple related objects that you want to
128     consider in an 'aggregated' state. For instance, by changing a child
129     object (image) you might also want to automatically mark the parent
130     (album) dirty as well.
131 
132     @type Boolean
133     @default NO
134   */
135   aggregate: NO,
136 
137 
138   /**
139     Can only be used for toOne or toMany relationship attributes. If YES,
140     this flag will lazily create the related record that was pushed in
141     from the data source (via pushRetrieve) if the related record does
142     not exist yet.
143 
144     Useful when you have a record used as a join table. Assumptions then
145     can be made that the record exists at all times (even if it doesn't).
146     For instance, if you have a contact that is a member of groups,
147     a group will be created automatically when a contact pushes a new
148     group.
149 
150     Note that you will have to take care of destroying the created record
151     once all relationships are removed from it.
152 
153     @type Boolean
154     @default NO
155    */
156   lazilyInstantiate: NO,
157 
158   // ..........................................................
159   // HELPER PROPERTIES
160   //
161 
162   /**
163     Returns the type, resolved to a class.  If the type property is a regular
164     class, returns the type unchanged.  Otherwise attempts to lookup the
165     type as a property path.
166 
167     @property
168     @type Object
169     @default String
170   */
171   typeClass: function() {
172     var ret = this.get('type');
173     if (SC.typeOf(ret) === SC.T_STRING) ret = SC.requiredObjectForPropertyPath(ret);
174     return ret ;
175   }.property('type').cacheable(),
176 
177   /**
178     Finds the transform handler. Attempts to find a transform that you
179     registered using registerTransform for this attribute's type, otherwise
180     defaults to using the default transform for String.
181 
182     @property
183     @type Transform
184   */
185   transform: function() {
186     var klass      = this.get('typeClass') || String,
187         transforms = SC.RecordAttribute.transforms,
188         ret ;
189 
190     // walk up class hierarchy looking for a transform handler
191     while(klass && !(ret = transforms[SC.guidFor(klass)])) {
192       // check if super has create property to detect SC.Object's
193       if(klass.superclass.hasOwnProperty('create')) klass = klass.superclass ;
194       // otherwise return the function transform handler
195       else klass = SC.T_FUNCTION ;
196     }
197 
198     return ret ;
199   }.property('typeClass').cacheable(),
200 
201   // ..........................................................
202   // LOW-LEVEL METHODS
203   //
204 
205   /**
206     Converts the passed value into the core attribute value.  This will apply
207     any format transforms.  You can install standard transforms by adding to
208     the `SC.RecordAttribute.transforms` hash.  See
209     SC.RecordAttribute.registerTransform() for more.
210 
211     @param {SC.Record} record The record instance
212     @param {String} key The key used to access this attribute on the record
213     @param {Object} value The property value before being transformed
214     @returns {Object} The transformed value
215   */
216   toType: function(record, key, value) {
217     var transform = this.get('transform'),
218         type      = this.get('typeClass'),
219         children;
220 
221     if (transform && transform.to) {
222       value = transform.to(value, this, type, record, key) ;
223 
224       // if the transform needs to do something when its children change, we need to set up an observer for it
225       if(!SC.none(value) && (children = transform.observesChildren)) {
226         var i, len = children.length,
227         // store the record, transform, and key so the observer knows where it was called from
228         context = {
229           record: record,
230           key: key
231         };
232 
233         for(i = 0; i < len; i++) value.addObserver(children[i], this, this._SCRA_childObserver, context);
234       }
235     }
236 
237     return value ;
238   },
239 
240   /**
241     @private
242 
243     Shared observer used by any attribute whose transform creates a seperate
244     object that needs to write back to the datahash when it changes. For
245     example, when enumerable content changes on a `SC.Set` attribute, it
246     writes back automatically instead of forcing you to call `.set` manually.
247 
248     This functionality can be used by setting an array named
249     observesChildren on your transform containing the names of keys to
250     observe. When one of them triggers it will call childDidChange on your
251     transform with the same arguments as to and from.
252 
253     @param {Object} obj The transformed value that is being observed
254     @param {String} key The key used to access this attribute on the record
255     @param {Object} prev Previous value (not used)
256     @param {Object} context Hash of extra context information
257   */
258   _SCRA_childObserver: function(obj, key, prev, context) {
259     // write the new value back to the record
260     this.call(context.record, context.key, obj);
261 
262     // mark the attribute as dirty
263     context.record.notifyPropertyChange(context.key);
264   },
265 
266   /**
267     Converts the passed value from the core attribute value.  This will apply
268     any format transforms.  You can install standard transforms by adding to
269     the `SC.RecordAttribute.transforms` hash.  See
270     `SC.RecordAttribute.registerTransform()` for more.
271 
272     @param {SC.Record} record The record instance
273     @param {String} key The key used to access this attribute on the record
274     @param {Object} value The transformed value
275     @returns {Object} The value converted back to attribute format
276   */
277   fromType: function(record, key, value) {
278     var transform = this.get('transform'),
279         type      = this.get('typeClass');
280 
281     if (transform && transform.from) {
282       value = transform.from(value, this, type, record, key);
283     }
284     return value;
285   },
286 
287   /**
288     The core handler. Called when `get()` is called on the
289     parent record, since `SC.RecordAttribute` uses `isProperty` to masquerade
290     as a computed property. Get expects a property be a function, thus we
291     need to implement call.
292 
293     @param {SC.Record} record The record instance
294     @param {String} key The key used to access this attribute on the record
295     @param {Object} value The property value if called as a setter
296     @returns {Object} property value
297   */
298   call: function(record, key, value) {
299     var attrKey = this.get('key') || key, nvalue;
300 
301     if ((value !== undefined) && this.get('isEditable')) {
302       // careful: don't overwrite value here.  we want the return value to
303       // cache.
304       nvalue = this.fromType(record, key, value) ; // convert to attribute.
305       record.writeAttribute(attrKey, nvalue);
306     }
307 
308     value = record.readAttribute(attrKey);
309     if (SC.none(value) && (value = this.get('defaultValue'))) {
310        if (typeof value === SC.T_FUNCTION) {
311         value = value(record, key, this);
312       }
313     }
314 
315     value = this.toType(record, key, value);
316 
317     return value ;
318   },
319 
320   /**
321     Apply needs to implemented for sc_super to work.
322 
323     @see SC.RecordAttribute#call
324   */
325   apply: function(target, args) {
326     return this.call.apply(target, args);
327   },
328 
329   // ..........................................................
330   // INTERNAL SUPPORT
331   //
332 
333   /** @private - Make this look like a property so that `get()` will call it. */
334   isProperty: YES,
335 
336   /** @private - Make this look cacheable */
337   isCacheable: YES,
338 
339   /** @private - needed for KVO `property()` support */
340   dependentKeys: [],
341 
342   /** @private */
343   init: function() {
344     sc_super();
345     // setup some internal properties needed for KVO - faking 'cacheable'
346     this.cacheKey = "__cache__recattr__" + SC.guidFor(this) ;
347     this.lastSetValueKey = "__lastValue__recattr__" + SC.guidFor(this) ;
348   }
349 }) ;
350 
351 // ..........................................................
352 // CLASS METHODS
353 //
354 
355 SC.RecordAttribute.mixin(
356   /** @scope SC.RecordAttribute.prototype */{
357   /**
358     The default method used to create a record attribute instance.  Unlike
359     `create()`, takes an `attributeType` as the first parameter which will be
360     set on the attribute itself.  You can pass a string naming a class or a
361     class itself.
362 
363     @static
364     @param {Object|String} attributeType the assumed attribute type
365     @param {Hash} opts optional additional config options
366     @returns {SC.RecordAttribute} new instance
367   */
368   attr: function(attributeType, opts) {
369     if (!opts) opts = {} ;
370     if (!opts.type) opts.type = attributeType || String ;
371     return this.create(opts);
372   },
373 
374   /** @private
375     Hash of registered transforms by class guid.
376   */
377   transforms: {},
378 
379   /**
380     Call to register a transform handler for a specific type of object.  The
381     object you pass can be of any type as long as it responds to the following
382     methods
383 
384      - `to(value, attr, klass, record, key)` converts the passed value
385        (which will be of the class expected by the attribute) into the
386        underlying attribute value
387      - `from(value, attr, klass, record, key)` converts the underlying
388        attribute value into a value of the class
389 
390     You can also provide an array of keys to observer on the return value.
391     When any of these change, your from method will be called to write the
392     changed object back to the record. For example:
393 
394         {
395           to: function(value, attr, type, record, key) {
396             if(value) return value.toSet();
397             else return SC.Set.create();
398           },
399 
400           from: function(value, attr, type, record, key) {
401             return value.toArray();
402           },
403 
404           observesChildren: ['[]']
405         }
406 
407     @static
408     @param {Object} klass the type of object you convert
409     @param {Object} transform the transform object
410     @returns {SC.RecordAttribute} receiver
411   */
412   registerTransform: function(klass, transform) {
413     SC.RecordAttribute.transforms[SC.guidFor(klass)] = transform;
414   }
415 });
416 
417 // ..........................................................
418 // STANDARD ATTRIBUTE TRANSFORMS
419 //
420 
421 // Object, String, Number just pass through.
422 
423 /** @private - generic converter for Boolean records */
424 SC.RecordAttribute.registerTransform(Boolean, {
425   /** @private - convert an arbitrary object value to a boolean */
426   to: function(obj) {
427     return SC.none(obj) ? null : !!obj;
428   }
429 });
430 
431 /** @private - generic converter for Numbers */
432 SC.RecordAttribute.registerTransform(Number, {
433   /** @private - convert an arbitrary object value to a Number */
434   to: function(obj) {
435     return SC.none(obj) ? null : Number(obj) ;
436   }
437 });
438 
439 /** @private - generic converter for Strings */
440 SC.RecordAttribute.registerTransform(String, {
441   /** @private -
442     convert an arbitrary object value to a String
443     allow null through as that will be checked separately
444   */
445   to: function(obj) {
446     if (!(typeof obj === SC.T_STRING) && !SC.none(obj) && obj.toString) {
447       obj = obj.toString();
448     }
449     return obj;
450   }
451 });
452 
453 /** @private - generic converter for Array */
454 SC.RecordAttribute.registerTransform(Array, {
455   /** @private - check if obj is an array
456   */
457   to: function(obj) {
458     if (!SC.isArray(obj) && !SC.none(obj)) {
459       obj = [];
460     }
461     return obj;
462   },
463 
464   observesChildren: ['[]']
465 });
466 
467 /** @private - generic converter for Object */
468 SC.RecordAttribute.registerTransform(Object, {
469   /** @private - check if obj is an object */
470   to: function(obj) {
471     if (!(typeof obj === 'object') && !SC.none(obj)) {
472       obj = {};
473     }
474     return obj;
475   }
476 });
477 
478 /** @private - generic converter for SC.Record-type records */
479 SC.RecordAttribute.registerTransform(SC.Record, {
480 
481   /** @private - convert a record id to a record instance */
482   to: function(id, attr, recordType, parentRecord) {
483     var store = parentRecord.get('store');
484     if (SC.none(id) || (id==="")) return null;
485     else return store.find(recordType, id);
486   },
487 
488   /** @private - convert a record instance to a record id */
489   from: function(record) { return record ? record.get('id') : null; }
490 });
491 
492 /** @private - generic converter for transforming computed record attributes */
493 SC.RecordAttribute.registerTransform(SC.T_FUNCTION, {
494 
495   /** @private - convert a record id to a record instance */
496   to: function(id, attr, recordType, parentRecord) {
497     recordType = recordType.apply(parentRecord);
498     var store = parentRecord.get('store');
499     return store.find(recordType, id);
500   },
501 
502   /** @private - convert a record instance to a record id */
503   from: function(record) { return record.get('id'); }
504 });
505 
506 /** @private - generic converter for Date records */
507 SC.RecordAttribute.registerTransform(Date, {
508 
509   /** @private - convert a string to a Date */
510   to: function(str, attr) {
511 
512     // If a null or undefined value is passed, don't
513     // do any normalization.
514     if (SC.none(str)) { return str; }
515 
516     var ret ;
517     str = str.toString() || '';
518 
519     if (attr.get('useIsoDate')) {
520       var regexp = "([0-9]{4})(-([0-9]{2})(-([0-9]{2})" +
521              "(T([0-9]{2}):([0-9]{2})(:([0-9]{2})(\\.([0-9]+))?)?" +
522              "(Z|(([-+])([0-9]{2}):([0-9]{2})))?)?)?)?",
523           d      = str.match(new RegExp(regexp)),
524           offset = 0,
525           date   = new Date(d[1], 0, 1),
526           time ;
527 
528       if (d[3]) { date.setMonth(d[3] - 1); }
529       if (d[5]) { date.setDate(d[5]); }
530       if (d[7]) { date.setHours(d[7]); }
531       if (d[8]) { date.setMinutes(d[8]); }
532       if (d[10]) { date.setSeconds(d[10]); }
533       if (d[12]) { date.setMilliseconds(Number("0." + d[12]) * 1000); }
534       if (d[14]) {
535          offset = (Number(d[16]) * 60) + Number(d[17]);
536          offset *= ((d[15] === '-') ? 1 : -1);
537       }
538 
539       offset -= date.getTimezoneOffset();
540       time = (Number(date) + (offset * 60 * 1000));
541 
542       ret = new Date();
543       ret.setTime(Number(time));
544     } else ret = new Date(Date.parse(str));
545     return ret ;
546   },
547 
548   _dates: {},
549 
550   /** @private - pad with leading zeroes */
551   _zeropad: function(num) {
552     return ((num<0) ? '-' : '') + ((num<10) ? '0' : '') + Math.abs(num);
553   },
554 
555   /** @private - convert a date to a string */
556   from: function(date) {
557 
558     if (SC.none(date)) { return null; }
559 
560     var ret = this._dates[date.getTime()];
561     if (ret) return ret ;
562 
563     // figure timezone
564     var zp = this._zeropad,
565         tz = 0-date.getTimezoneOffset()/60;
566 
567     tz = (tz === 0) ? 'Z' : '%@:00'.fmt(zp(tz));
568 
569     this._dates[date.getTime()] = ret = "%@-%@-%@T%@:%@:%@%@".fmt(
570       zp(date.getFullYear()),
571       zp(date.getMonth()+1),
572       zp(date.getDate()),
573       zp(date.getHours()),
574       zp(date.getMinutes()),
575       zp(date.getSeconds()),
576       tz) ;
577 
578     return ret ;
579   }
580 });
581 
582 if (SC.DateTime && !SC.RecordAttribute.transforms[SC.guidFor(SC.DateTime)]) {
583 
584   /**
585     Registers a transform to allow `SC.DateTime` to be used as a record
586     attribute, ie `SC.Record.attr(SC.DateTime);`
587   */
588 
589   SC.RecordAttribute.registerTransform(SC.DateTime, {
590 
591     /** @private
592       Convert a String to a DateTime
593     */
594     to: function(str, attr) {
595       if (SC.none(str) || SC.instanceOf(str, SC.DateTime)) return str;
596       if(attr.get('useUnixTime')) {
597         if(SC.typeOf(str) === SC.T_STRING) { str = parseInt(str); }
598         if(isNaN(str) || SC.typeOf(str) !== SC.T_NUMBER) { str = 0; }
599         return SC.DateTime.create({ milliseconds: str*1000, timezone: 0 });
600       }
601       if (SC.instanceOf(str, Date)) return SC.DateTime.create(str.getTime());
602       var format = attr.get('format');
603       return SC.DateTime.parse(str, format ? format : SC.DateTime.recordFormat);
604     },
605 
606     /** @private
607       Convert a DateTime to a String
608     */
609     from: function(dt, attr) {
610       if (SC.none(dt)) return dt;
611       if (attr.get('useUnixTime')) {
612         return dt.get('milliseconds')/1000;
613       }
614       var format = attr.get('format');
615       return dt.toFormattedString(format ? format : SC.DateTime.recordFormat);
616     }
617   });
618 
619 }
620 
621 /**
622   Parses a coreset represented as an array.
623  */
624 SC.RecordAttribute.registerTransform(SC.Set, {
625   to: function(value, attr, type, record, key) {
626     return SC.Set.create(value);
627   },
628 
629   from: function(value, attr, type, record, key) {
630     return value.toArray();
631   },
632 
633   observesChildren: ['[]']
634 });
635