-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathEggs.coffee
702 lines (608 loc) · 24.6 KB
/
Eggs.coffee
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
# Eggs 0.0.1
# Utilities
# ---------
unless _.isPlainObject
_.mixin isPlainObject: (obj) ->
(obj instanceof Object) and not (obj instanceof Array) and (typeof obj isnt 'array')
# Environment
# -----------
Eggs = @Eggs = {}
# Eggs.Model
# ----------
# An `Eggs.Model` lets you manage data and synchronize it with the server.
# To specify a new model, the class should be extended using either coffee
# script's `extend` or `Eggs.Model.extend`. Methods that may be extended are:
# - `initialize` will be called when a new isntance of the model is created;
# - `validate` will be called with an object of attributes that should be
# validated. Returns an error (usually a string) or nothing if the
# validation was successful.
#
# Once extended, the new model class can be instantiated to represent an entry.
# The constructor accepts the initial *attributes* for the new model instance
# and *options*. Initial attributes will be passed through the validation
# process if any. Available options are:
# - `shouldValidate` defaults to `true` and indicates if the model isntance
# should be validated.
#
# Instance overridable methods:
# - `idAttribute` is the name of the attribute used to compute `id` and
# `url` properties values.
#
# Instance methods:
# - `attributes` returns a Bacon.Property of valid attributes in the model.
# Validation errors are sent through.
# - `set` modify attributes. Attributes will be validated if needed and the
# actual change will happen only if the set attribute is valid. `set`
# accepts different inputs:
# - *object, options*: set attributes by adding or modifying the current
# ones;
# - *name string, value*: will set or add the single attribute with the
# given value if possible;
# - *names array, options*: apply options derived actions to attributes in
# the array.
# Options are:
# - `reset`: defualt to false, if true will remove all attributes before
# setting the new ones. If false, setted attributes will be merged with
# existing ones;
# - `unset`: default to false, if true will remove the specified attributes
# instead of setting them;
# - `save`: will send a request to the server to save the model based on
# id and url;
# - `waitSave`: before pushing an updated attribute object, waits for the
# server to respond successfully to the save operation.
# Returns `attributes`.
#
# Example Usage:
# class MyModel extends Eggs.Model
# defaults: { myField: 'myFieldDefaultValue' }
# validate: (attributes) -> "too short!" if attributes.myField.length < 3
# myModel = new MyModel({ myOtherField: 2 })
# myModel.attributes().onValue (value) -> console.log(value)
Eggs.Model = class Model
# The default attribute name for linking the model with the database.
# MongoDB and CouchDB users may want to set this to `"_id"`
idAttribute: 'id'
# Initialize could be used by subclasses to add their own model initialization
initialize: ->
# `defaults` can be defined as an object that contains default attributes
# that will be used when creating a new model.
# `validate` can be defined to be a function receiving attributes and
# returning a falsy value if the attributes are valid. If the function
# returns something it will be treated as a validation error value.
# Parse is used to convert a server response into the object to be
# set as attributes for the model instance.
# It's a plain function returning an object that will be passed to
# `set` from the default `fetch` implementation.
parse: (response) ->
response
# A function that prepare the JSON to be sent to the server for saving.
# This method will also be used by the default `toJSON` implementation.
prepareJSON: (attributes) ->
attributes
# A function that prepares the URL to be used to save or fetch this model to
# the server. The default implementation uses `urlRoot` or the collection's
# url and the model's id to craft an URL of the form: urlRoot/id.
prepareURL: (attributes) ->
base = _.result(@, 'urlRoot') or @collection?.url
throw new Error("Expecting `urlRoot` to be defined") unless base?
if base.charAt(base.length - 1) is '/'
base = base.substring(0, base.length - 1)
id = attributes[@idAttribute]
if id then "#{base}/#{encodeURIComponent(id)}" else base
# The model constructor will generate `attributes` method. It will also
# assing a client id `cid` to the model.
constructor: (attrs, options) ->
options = _.defaults {}, options,
shouldValidate: yes
collection: null
attributesAreValid = not options.shouldValidate
# Generate a unique client id that wil be used by collections for
# unsaved models
@cid = _.uniqueId('c')
# The collection that this model is in.
@collection = options.collection
# The bus and relative Property that will send validated attributes
# and validation errors.
attributes = null
attributesBus = new Bacon.Bus
attributesProperty = attributesBus.toProperty()
# Subscribe on an empty function to activate the property so that it will
# always be current.
attributesProperty.onValue ->
# The main accessor to model attributes.
@attributes = ->
return attributesProperty
# Setting model's attributes
@set = (obj, opts) ->
# Accepts only a plain object if there are no options
unless arguments.length > 1 or _.isPlainObject(obj)
throw new Error("Invalid parameter for `set` method: #{obj}")
obj ?= {}
opts ?= {}
# Unset will remove the given options
if opts.unset
# For unset one can specity an attribute name, a plain object or an
# array of names. Homogenize to the latest
if _.isString(obj) then obj = [obj]
else if _.isPlainObject(obj) then obj = _.keys(obj)
# Getting the new attributes
newAttributes = _.omit(attributes, obj)
# Push only if something have actually been unset
if _.difference(_.keys(attributes), _.keys(newAttributes))
attributes = newAttributes
attributesBus.push(_.clone(attributes))
# Early exit
return attributesProperty
# One can specify a single attribute to set, generate a plain object for
# that and get the third parameter as options
if _.isString(obj)
o = {}
o[obj] = opts
obj = o
opts = arguments[2] ? {}
# Parse if needed
obj = @parse(obj) if opts.parse
# If there is no need to reset, mix new and current attributes
if opts.reset
obj = _.clone(obj)
else
obj = _.extend({}, attributes, obj)
# Validate only if there are changes
opts.save = yes if opts.waitSave
if (objEqualAttributes = _.isEqual(obj, attributes)) and not opts.save
return attributesProperty
# Push an error if validation fails
if options.shouldValidate and error = @validate?(obj)
attributesBus.error(error)
# Initialize attribute property with an empty object if there are
# sill no valid attributes
attributesBus.push({}) unless attributesAreValid
return attributesProperty
# From now on attributes will be considered valid (they will not be
# udpated on validation errors)
if opts.save
save = Bacon.fromPromise($.ajax
type: if obj[@idAttribute]? then 'PUT' else 'POST'
dataType: 'json'
processData: false
url: @prepareURL(obj)
data: obj).map((response) =>
attributesAreValid = yes
if opts.reset
attributes = _.extend(obj, @parse(response))
else
attributes = _.extend(attributes, obj, @parse(response)))
if opts.waitSave
# Activate the save by plugging it into the attribute bus
attributesBus.plug(save)
# Early exit, attributes will be updated by the plugged stream
return attributesBus.toProperty()
else
# Activate the save by consuming the stream
save.onValue (attr) ->
attributesBus.push(_.clone(attr))
Bacon.noMore
# In case of normal set or saving without wait, update the attributes
# imemdiatly
unless objEqualAttributes
attributesAreValid = yes
attributes = obj
attributesBus.push(_.clone(attributes))
attributesProperty
# Will indicate if the current set of attributes is valid.
@valid = ->
@_valid or= attributesBus.map(yes).toProperty(attributesAreValid).skipDuplicates()
# Initialize the model attributes
@set(_.defaults({}, attrs, _.result(@, 'defaults')))
# Custom initialization
@initialize.apply(@, arguments)
# Initiates an AJAX request to fetch the model's data form the server.
# Returns a Bacon.Property that will send the updated attributes once
# they have been set to the model.
fetch: ->
fetch = @url()
.take(1)
.flatMapLatest((url) ->
Bacon.fromPromise $.ajax
type: 'GET'
dataType: 'json'
url: url)
.flatMapLatest((result) =>
@set(result, { parse: yes }))
.toProperty()
# Activate the fetch reaction
fetch.onValue -> Bacon.noMore
fetch
# Initiates an AJAX request that sends the model's attributes to the server.
# Returns a Bacon.Property derived from the AJAX request promise.
save: (args...) ->
if _.isString(args[0])
args[2] = _.extend({}, args[2], { save: yes })
else
args[1] = _.extend({}, args[1], { save: yes })
@set(args...)
# Destroy the model instance on the server if it was present.
# Returns a Bacon.EvnetStream that will push a single value returned from
# the server or `null` if no server activity was initiated.
# TODO remove from collection
destroy: ->
Bacon.combineAsArray(@url(), @id())
.take(1)
.flatMap((info) ->
[url, id] = info
if id?
Bacon.fromPromise $.ajax
type: 'DELETE'
dataType: 'json'
processData: false
url: url
else
Bacon.once(null))
# Unset the given attributes in the model. The parameter can either be a string
# with the name of the attribute to unset, or an array of names.
unset: (attrNames) ->
attrNames = [attrNames] unless _.isArray(attrNames)
@set(attrNames, { unset: true })
# Returns a Bacon.Property pushing the id of the model or null if the model
# is new. This method uses `idAttribute` to determine which attribute is the id.
id: ->
@_id or= @attributes().map (attr) =>
attr[@idAttribute]
# Returns a Bacon.Property that updates with the URL for synching the model.
# This methos uses `urlRoot` to compute the URL.
url: ->
@_url or= @attributes().map (attr) =>
@prepareURL(attr)
# Returns a Bacon.Property with the JSON reppresentation of the model's
# attributes. By deafult, this function just returns `attributes()`.
toJSON: ->
@attributes().map (attr) =>
@prepareJSON(attr)
# Eggs.Collection
# ---------------
# An `Eggs.Collection` groups together multiple model instances.
#
# - `models(options)` returns a Bacon.Property that sends the models
# contained in the collection. Options is an object that can contain:
# - `get`: array or single model or id. This will make `models` return
# an array containing only models with those ids.
# - `valid`: default to **false** will make the property only push valid
# models.
# - `sorted`: default to **false** will make the property push sorted
# models. If true, the collection's comparator will be used. The
# comparator can be specified to the collection extension, as an
# instance construction option or directly specified as `sorted`. The
# comparator can be a *string* indicating which model's attribute
# to use for natural sorting the results or a *function(a, b)* receiving
# two models attributes and returning the ordering between the two.
# - `modelsAttributes(options)` returns a Bacon.Property that sends the
# collection models attributes. It accepts options of models getters and:
# - `from`: by default to 'sortedModels', a string indicating from which
# models getter to collect attributes; values are `'models'`,
# `'validModels'` and `'sortedModels'`.
# - `pluck`: a model attribute name to retrieve instead of the complete
# model attributes; if an array of names is specified, only the given
# attributes will be picked.
# - `add` modify the collection's content. If a model already exists it will
# be skipped unless `merge` or `update` is specified; in that case models
# will be merged or updated with new attributes *after* the collection
# update. It accepts the following parameters:
# - *models, options*: adds or remove models depending on options. *Models*
# can be an array or single Model instance or collection of attributes.
# Options are:
# - `reset`: deafult to **false**, indicates if the model should be
# emptied before adding the new content;
# - `merge`: default to **false**, indicates if added models with the same
# idAttribute to existing models should be merged;
# - `update`: default to **false**, is similar to `merge` but instead of
# merging models having the same idAttribute it will substitute them;
# - `at`: specify the index at which start to insert new attributes;
# - `parse`: default to **false**, will use `parse` on added models before
# actually add them;
# - `save`: will send a request to the server to save each model;
# - `waitSave`: before pushing an updated models array, waits for the
# server to respond successfully to the save operations.
# Returns `models`.
# - `remove` modify the collection's content by removing models. It accepts
# an array of models or ids. Returns `models`.
Eggs.Collection = class Collection
# The class of model elements contained in this collection. By default this
# member is set to `Eggs.Model`.
modelClass: Model
# Called when constructing a new collection. By default this method does
# nothing.
initialize: ->
# `idAttribute` can be defined as a string indicating the model attribute
# name that the collection should use as id. If not specified, the
# modelClass.idAttribute will be used.
# `comparator` can be defined as a string indicating an attribute to be
# used for sorting or a function receiving a couple of models to compare.
# The function should return 0 if the models are equal -1 if the first is
# before the second and 1 otherwise. It will be used by `sortedModels`
# property.
# `url` is the server URL to use to fetch collections. This URL will also be
# used by collection's model when saving. Unlike models `url` this is a plain
# string instead of a Property.
# Parse is used to convert a server response into the array of attributes to
# be set as models for the collection instance.
# It's a plain function returning an object that will be passed to
# `add` from the default `fetch` implementation.
parse: (response) ->
response
# The constructor will generate the `models` method to access and modify
# the Collection's elements.
constructor: (cModels, cOptions = {}) ->
@modelClass = cOptions.modelClass if cOptions.modelClass?
@comparator = cOptions.comparator if cOptions.comparator?
# The Bus used to push updated models and the relative Property.
modelsBus = new Bacon.Bus
modelsProperty = modelsBus.toProperty()
modelsArray = []
modelsByCId = {}
# Activate modelsProperty
modelsProperty.onValue ->
# Utility function that will prepare a Model or an attributes object
# to be added to this Collection
prepareModel = (attrs, opts = {}) =>
if attrs instanceof Model
attrs.collection = @ unless attrs.collection?
return attrs
opts.collection = @
new @modelClass(attrs, opts)
# The main accessor to collection's models
@models = (args...) ->
# options may be a shortcut for options.get, in that case there may be
# other options as a second parameter
options = {}
if args.length
if args[0] instanceof Model or _.isNumber(args[0]) or _.isString(args[0]) or _.isArray(args[0])
options = { get: args[0] }
args = args.slice(1)
options = _.extend(options, args...)
models = modelsProperty
if options.get?
# Make sure that options.get is an array and make a copy of it to avoid
# external changes
idsAndModels = if _.isArray(options.get) then options.get.slice() else [options.get]
# Retrieve all the requested models
models = Bacon.combineTemplate((if i instanceof Model then i.id() else i) for i in idsAndModels).flatMapLatest((idsOnly) ->
modelsProperty.flatMapLatest((ms) ->
Bacon.combineAsArray(m.id() for m in ms).map((modelIds) ->
results = []
for id, idIndex in idsOnly
indexInModels = -1
indexInModels = modelIds.indexOf(id) if id?
indexInModels = ms.indexOf(idsAndModels[idIndex]) if indexInModels < 0
if indexInModels >= 0
results.push(ms[indexInModels])
results))).toProperty()
# Get valid models if needed
if options.valid
models = models.flatMapLatest((ms) ->
Bacon.combineAsArray(m.valid() for m in ms)
.map((validArray) ->
result = []
for v, i in validArray
result.push(ms[i]) if v
result))
.toProperty()
# Get sorted models if needed
if options.sorted or options.comparator?
comparator = options.sorted unless _.isBoolean(options.sorted)
comparator = options.comparator if options.comparator?
comparator ?= @comparator
throw new Error("Invalid comparator for sorted models: #{comparator}") unless comparator?
unless _.isFunction(comparator)
comparatorFunction = (a, b) =>
if a[0]?[comparator] < b[0]?[comparator] then -1
else if a[0]?[comparator] > b[0]?[comparator] then 1
else 0
else
comparatorFunction = (a, b) => comparator(a[0], b[0])
models = models.flatMapLatest((ms) ->
Bacon.combineAsArray(m.attributes() for m in ms)
.map((mattrs) ->
([attrs, ms[i]] for attrs, i in mattrs)
.sort(comparatorFunction)
.map((am) -> am[1])))
.toProperty()
# Return the builded models property
models
# The models Property is decorated with a `collection` attribute
@models.collection = @
# Method to add models to the collection content
@add = (models, options) ->
models = if _.isArray(models) then models.slice() else [models]
options or= {}
add = []
if options.reset
delete m.collection for m in modelsArray when m.collection is @
modelsArray = []
modelsByCId = {}
# Parse models if needed
models = @parse(models) if options.parse
# Prapare models to make sure to have Model instances
for model in models
model = prepareModel(model, options)
unless modelsByCId[model.cid]?
add.push(model)
modelsByCId[model.cid] = model
if modelsArray.length
if add.length
# Prepare to add new models in a non empty collection
at = options.at ? modelsArray.length
options.update = no if options.merge
@idAttribute = modelsArray[0].idAttribute unless @idAttribute?
# Update will contain objects with `model` and `set` that should
# be set after updating the collection
updateModels = []
Bacon.combineAsArray(m.attributes() for m in modelsArray)
.take(1).flatMapLatest((modelsAttributes) =>
# Get all the ids of the models currently in the collection; this
# will have the same index as models in modelsArray
modelsIds = _.pluck(modelsAttributes, @idAttribute)
# Get add array models attributes
Bacon.combineAsArray(m.attributes() for m in add)
.take(1).flatMapLatest((addAttributes) =>
# cleanAdd will have all the models actually to add after updates
# of existing ones
cleanAdd = []
for addAttrs, addIndex in addAttributes
# Get the index of the model in modelsArray of an already
# present model
if (addId = addAttrs[@idAttribute]) and (modelIndex = _.indexOf(modelsIds, addId)) >= 0
# With update or merge option, will set the existing model
# after the collection update
updateModels.push({ model: modelsArray[modelIndex], set: addAttrs }) if options.update?
continue
# If no conflicts, add to cleanAdd
cleanAdd.push(add[addIndex])
# We can finally add to the modelsArray and push the update
if cleanAdd.length
# TODO handle `save` option and `waitSave`
modelsArray[at..at-1] = cleanAdd
modelsBus.push(modelsArray)
# Will return the models property already updated with the change
modelsProperty))
# Activate the operation
.onValue -> Bacon.noMore
# Update single models
for u in updateModels
u.model.set(u.set, { reset: options.update })
# Just return modelsProperty as the previous reaction will be
# already executed at this point
return modelsProperty
else
# Adding models to a currently empty collection
modelsArray = add
if add.length or options.reset
modelsBus.push(modelsArray)
modelsProperty
# Method to remove models from the collection
@remove = (idsAndModels) ->
@models(idsAndModels).take(1).onValue((models) =>
models = [models] unless _.isArray(models)
for m in models
delete m.collection if m.collection is @
delete modelsByCId[m.cid]
modelsArray = modelsArray.filter((v) -> models.indexOf(v) < 0)
modelsBus.push(modelsArray))
modelsProperty
# Initialize models with constructor options
@add(cModels ? [], cOptions)
@initialize.apply(@, arguments)
# Sends a model array only containing valid models
validModels: (args...) ->
args.push({ valid: yes })
@models(args...)
# Sends an ordered models array if `comparator` is specified
sortedModels: (args...) ->
if args.length
if _.isString(args[0]) or _.isFunction(args[0])
args = [{ valid: yes, sorted: args[0] }].concat(args.slice(1))
else
args.push({ valid: yes, sorted: yes })
else
args = [{ valid: yes, sorted: yes }]
@models(args...)
# Remove all collection's models and substitute them with those specified.
reset: (models, options) ->
@add(models, _.extend({}, options, { reset: true }))
# Initiates an AJAX request to fetch the colleciton's content form the server
# Returns a Bacon.Property that will send updated content once received.
fetch: (options) ->
options or= {}
url = options.url ? @url
throw new Error("Invalid URL for Collection #{@}") unless url?
fetch = Bacon.fromPromise($.ajax
type: 'GET'
dataType: 'json'
url: url)
.flatMap((result) =>
@add(result, _.extend({ reset: yes }, options, { parse: yes })))
.toProperty()
fetch.onValue -> Bacon.noMore
fetch
# Bacon extensions
# ----------------
# Get a field from an object
Bacon.Observable.prototype.get = (field) ->
@map (obj) -> obj[field]
# Pluck a filed from an array of objects
Bacon.Observable.prototype.pluck = (field) ->
@filter(_.isArray).map((obj) -> _.pluck(obj, field))
# Pick the given fields from an object
Bacon.Observable.prototype.pick = (fields...) ->
@filter(_.isObject).map (obj) -> _.pick(obj, fields...)
# Sends array of keys derived from an object
Bacon.Observable.prototype.keys = ->
@filter(_.isObject).map(_.keys)
# Collection specific utility to extract attributes from a Models array.
# Returns a Bacon.Property.
Bacon.Observable.prototype.attributes = ->
@flatMapLatest((models) ->
Bacon.combineAsArray(m.attributes() for m in models when m instanceof Model))
.toProperty()
# UNTESTED WORK FROM THIS POINT
# -----------------------------
Eggs.model = (extension) ->
parent = Model
if extension and _.has(extension, 'constructor')
child = extension.constructor
else
child = () -> parent.apply(@, arguments)
class Surrogate
constructor: () -> @constructor = child
Surrogate.prototype = parent.prototype
child.prototype = new Surrogate
_.extend(child.prototype, extension) if extension
child.__super__ = parent.prototype
child
routeStripper = /^[#\/]|\s+$/g
Eggs.currentLocation = (() ->
location = window?.location
hasPushState = location?.history?.pushState
hasHashChange = 'onhashchange' of window
getHash = () ->
match = location.href.match /#(.*)$/
if match then match[1] else ''
getFragment = (fragment) ->
unless fragment
if hasPushState or not hasHashChange
fragment = location.pathname
else
fragment = getHash()
fragment.replace(routeStripper, '')
if hasPushState
windowLocationStream = Bacon.fromEventTarget(window, 'popstate')
else if hasHashChange
windowLocationStream = Bacon.fromEventTarget(window, 'hashchange')
else
windowLocationStream = Bacon.interval(100)
windowLocationStream
.map(() ->
getFragment())
.skipDuplicates()
.toProperty(getFragment())
)()
# Routing
optionalParam = /\((.*?)\)/g
namedParam = /(\(\?)?:\w+/g
splatParam = /\*\w+/g
escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g
Eggs.route = (route) ->
# Get the RegExp for the route
unless _.isRegExp(route)
route = route
.replace(escapeRegExp, '\\$&')
.replace(optionalParam, '(?:$1)?')
.replace(namedParam, (match, optional) ->
if optional then match else '([^\/]+)')
.replace(splatParam, '(.*?)')
route = new RegExp('^' + route + '$')
Eggs
.currentLocation
.filter((location) ->
route.test(location))
.map((location) ->
route.exec(location).slice(1))