joosy 0.1.0.RC1 → 0.1.0.RC2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. data/Gemfile +1 -0
  2. data/Gemfile.lock +8 -1
  3. data/MIT-LICENSE +2 -2
  4. data/README.md +89 -0
  5. data/app/assets/javascripts/joosy/core/application.js.coffee +25 -5
  6. data/app/assets/javascripts/joosy/core/form.js.coffee +212 -22
  7. data/app/assets/javascripts/joosy/core/helpers.js.coffee +11 -1
  8. data/app/assets/javascripts/joosy/core/joosy.js.coffee +22 -17
  9. data/app/assets/javascripts/joosy/core/layout.js.coffee +17 -7
  10. data/app/assets/javascripts/joosy/core/modules/container.js.coffee +19 -15
  11. data/app/assets/javascripts/joosy/core/modules/events.js.coffee +10 -9
  12. data/app/assets/javascripts/joosy/core/modules/filters.js.coffee +16 -12
  13. data/app/assets/javascripts/joosy/core/modules/log.js.coffee +8 -5
  14. data/app/assets/javascripts/joosy/core/modules/module.js.coffee +31 -21
  15. data/app/assets/javascripts/joosy/core/modules/renderer.js.coffee +114 -51
  16. data/app/assets/javascripts/joosy/core/modules/time_manager.js.coffee +2 -2
  17. data/app/assets/javascripts/joosy/core/modules/widgets_manager.js.coffee +10 -10
  18. data/app/assets/javascripts/joosy/core/page.js.coffee +31 -21
  19. data/app/assets/javascripts/joosy/core/preloader.js.coffee +3 -3
  20. data/app/assets/javascripts/joosy/core/resource/collection.js.coffee +137 -0
  21. data/app/assets/javascripts/joosy/core/resource/generic.js.coffee +178 -13
  22. data/app/assets/javascripts/joosy/core/resource/rest.js.coffee +167 -44
  23. data/app/assets/javascripts/joosy/core/resource/rest_collection.js.coffee +100 -32
  24. data/app/assets/javascripts/joosy/core/router.js.coffee +23 -25
  25. data/app/assets/javascripts/joosy/core/templaters/rails_jst.js.coffee +19 -3
  26. data/app/assets/javascripts/joosy/core/widget.js.coffee +7 -9
  27. data/app/assets/javascripts/joosy/preloaders/caching.js.coffee +117 -57
  28. data/app/assets/javascripts/joosy/preloaders/inline.js.coffee +23 -24
  29. data/app/helpers/joosy/sprockets_helper.rb +1 -1
  30. data/lib/joosy/forms.rb +2 -12
  31. data/lib/joosy/rails/version.rb +1 -1
  32. data/lib/rails/generators/joosy/templates/app/pages/template.js.coffee +1 -1
  33. data/lib/rails/generators/joosy/templates/app/resources/template.js.coffee +1 -1
  34. data/spec/javascripts/joosy/core/form_spec.js.coffee +55 -12
  35. data/spec/javascripts/joosy/core/layout_spec.js.coffee +1 -1
  36. data/spec/javascripts/joosy/core/modules/container_spec.js.coffee +0 -1
  37. data/spec/javascripts/joosy/core/modules/module_spec.js.coffee +1 -1
  38. data/spec/javascripts/joosy/core/modules/renderer_spec.js.coffee +39 -3
  39. data/spec/javascripts/joosy/core/modules/time_manager_spec.js.coffee +1 -1
  40. data/spec/javascripts/joosy/core/page_spec.js.coffee +4 -1
  41. data/spec/javascripts/joosy/core/resource/collection_spec.js.coffee +84 -0
  42. data/spec/javascripts/joosy/core/resource/generic_spec.js.coffee +86 -3
  43. data/spec/javascripts/joosy/core/resource/rest_collection_spec.js.coffee +15 -22
  44. data/spec/javascripts/joosy/core/resource/rest_spec.js.coffee +27 -4
  45. data/spec/javascripts/joosy/core/widget_spec.js.coffee +3 -14
  46. metadata +21 -19
  47. data/README.rdoc +0 -3
@@ -1,5 +1,5 @@
1
1
  # Preloader stub
2
- @Preloader = Object.extended
2
+ @Preloader =
3
3
  load: (libraries, options) ->
4
- @.merge options
5
- @complete?.call window
4
+ @[key] = val for key, val of options
5
+ @complete?.call window
@@ -0,0 +1,137 @@
1
+ #
2
+ # Basic collection of Resources
3
+ # Turns JSON array into array of Resources and manages them
4
+ #
5
+ # Generally you should not use Collection directly. It will be
6
+ # automatically created by things like Joosy.Resource.Generic#map
7
+ # or Joosy.Resource.REST#find.
8
+ #
9
+ # Example:
10
+ # class R extends Joosy.Resource.Generic
11
+ # @entity 'r'
12
+ #
13
+ # collection = new Joosy.Resource.Collection(R)
14
+ #
15
+ # collection.reset [{foo: 'bar'}, {foo: 'baz'}]
16
+ # collection.each (resource) ->
17
+ # resource('foo')
18
+ #
19
+ class Joosy.Resource.Collection extends Joosy.Module
20
+ @include Joosy.Modules.Events
21
+
22
+ #
23
+ # Allows to modify data before it gets stored
24
+ #
25
+ # @param [Function] action `(Object) -> Object` to call
26
+ #
27
+ @beforeLoad: (action) -> @::__beforeLoad = action
28
+
29
+ #
30
+ # Modelized data storage
31
+ #
32
+ data: []
33
+
34
+ #
35
+ # @param [Class] model Resource class this collection will handle
36
+ #
37
+ constructor: (@model) ->
38
+
39
+ #
40
+ # Clears the storage and attempts to import given JSON
41
+ #
42
+ # @param [Object] entities Entities to import
43
+ # @param [Boolean] notify Indicates whether to trigger 'changed' event
44
+ #
45
+ reset: (entities, notify=true) ->
46
+ if @__beforeLoad?
47
+ entities = @__beforeLoad entities
48
+
49
+ @data = @modelize entities
50
+
51
+ if notify
52
+ @trigger 'changed'
53
+ this
54
+
55
+ #
56
+ # Turns Objects array into array of Resources
57
+ #
58
+ # @param [Array] collection Array of Objects
59
+ #
60
+ modelize: (collection) ->
61
+ root = @model::__entityName.pluralize()
62
+
63
+ if collection not instanceof Array
64
+ collection = collection?[root.camelize(false)]
65
+
66
+ if collection not instanceof Array
67
+ throw new Error "Can not read incoming JSON"
68
+
69
+ collection.map (x) =>
70
+ @model.create x
71
+
72
+ #
73
+ # Calls callback for each Resource inside Collection
74
+ #
75
+ # @param [Function] callback
76
+ #
77
+ each: (callback) ->
78
+ @data.each callback
79
+
80
+ #
81
+ # Gets first resource matching description (see Sugar.js Array#find)
82
+ #
83
+ # @param [Function] description Callback matcher
84
+ #
85
+ find: (description) ->
86
+ @data.find description
87
+
88
+ #
89
+ # Gets resource by id
90
+ #
91
+ # @param [Integer] id Id to find
92
+ #
93
+ findById: (id) ->
94
+ @data.find (x) -> x('id').toString() == id.toString()
95
+
96
+ #
97
+ # Gets resource by its index inside collection
98
+ #
99
+ # @param [Integer] i Index
100
+ #
101
+ at: (i) ->
102
+ @data[i]
103
+
104
+ #
105
+ # Removes resource from collection by its index or by === comparison
106
+ #
107
+ # @param [Integer] target Index
108
+ # @param [Resource] target Resource by itself
109
+ # @param [Boolean] notify Indicates whether to trigger 'changed' event
110
+ #
111
+ remove: (target, notify=true) ->
112
+ if Object.isNumber target
113
+ index = target
114
+ else
115
+ index = @data.indexOf target
116
+ if index >= 0
117
+ result = @data.splice(index, 1)[0]
118
+ if notify
119
+ @trigger 'changed'
120
+ result
121
+
122
+ #
123
+ # Adds resource to collection to given index or to the end
124
+ #
125
+ # @param [Resource] element Resource to add
126
+ # @param [Integer] index Index to add to. If omited will be pushed to the end
127
+ # @param [Boolean] notify Indicates whether to trigger 'changed' event
128
+ #
129
+ add: (element, index=false, notify=true) ->
130
+ if index
131
+ @data.splice index, 0, element
132
+ else
133
+ @data.push element
134
+
135
+ if notify
136
+ @trigger 'changed'
137
+ element
@@ -1,12 +1,129 @@
1
+ #
2
+ # Basic data wrapper with triggering
3
+ #
4
+ # Example:
5
+ # class R extends Joosy.Resource.Generic
6
+ # @entity 'r'
7
+ #
8
+ # @beforeLoad (data) ->
9
+ # data.real = true
10
+ #
11
+ # r = R.create {r: {foo: {bar: 'baz'}}}
12
+ #
13
+ # r('foo') # {baz: 'baz'}
14
+ # r('real') # true
15
+ # r('foo.bar') # baz
16
+ # r('foo.bar', 'fluffy') # triggers 'changed'
17
+ #
1
18
  class Joosy.Resource.Generic extends Joosy.Module
2
19
  @include Joosy.Modules.Log
3
20
  @include Joosy.Modules.Events
4
21
 
22
+ #
23
+ # Sets the data source description (which is NOT required)
24
+ # This has no use in Generic but is required in any descendant
25
+ #
26
+ # @param [mixed] Source can be any type including lambda
27
+ # If lambda is given resource will not expect direct .create() calls
28
+ # You'll have to prepare descendant with .at() first
29
+ #
30
+ # Example:
31
+ # Class Y extends Joosy.Resource.Generic
32
+ # @source 'fluffies'
33
+ # class R extends Joosy.Resource.Generic
34
+ # @source -> (path) "/"+path
35
+ #
36
+ # r = Y.create{} # will work as expected
37
+ # r = R.create {} # will raise exception
38
+ # r = R.at('foo/bar').create {} # will work as expected
39
+ #
40
+ @source: (source) -> @__source = source
41
+
42
+ #
43
+ # Required to do some magic like skipping the root node
44
+ #
45
+ # @param [String] name Singular name of resource
46
+ #
47
+ @entity: (name) -> @::__entityName = name
48
+
49
+ #
50
+ # Sets the collection of current Resource
51
+ #
52
+ # @param [Object] klass Class to assign as collection
53
+ #
54
+ @collection: (klass) -> @::__collection = -> klass
55
+
56
+ #
57
+ # Default value of resource collection
58
+ # Will try to seek for EntityNamesCollection
59
+ # Will fallback to Joosy.Resource.Collection
60
+ #
61
+ __collection: ->
62
+ named = @__entityName.camelize().pluralize() + 'Collection'
63
+ if window[named] then window[named] else Joosy.Resource.Collection
64
+
65
+ #
66
+ # Allows to modify data before it gets stored
67
+ #
68
+ # @param [Function] action `(Object) -> Object` to call
69
+ #
5
70
  @beforeLoad: (action) -> @::__beforeLoad = action
6
71
 
7
- @create: ->
72
+ #
73
+ # Dynamically creates collection of inline resources
74
+ # Inline resource share the instance with direct data and therefore can be used
75
+ # to better handle inline changes
76
+ #
77
+ # Example:
78
+ # class Zombie extends Joosy.Resource.Generic
79
+ # @entity 'a'
80
+ # class Puppy extends Joosy.Resource.Generic
81
+ # @entity 'b'
82
+ # @maps 'zombies'
83
+ #
84
+ # p = Puppy.create {zombies: [{foo: 'bar'}]}
85
+ #
86
+ # p('zombies') # Direct access: [{foo: 'bar'}]
87
+ # p.zombies # Wrapped Collection of Zombie instances
88
+ # p.zombies.at(0)('foo') # bar
89
+ #
90
+ # @param [String] name Pluralized name of property to define
91
+ # @param [Class] klass Resource class to instantiate
92
+ #
93
+ @map: (name, klass=false) ->
94
+ unless klass
95
+ klass = window[name.singularize().camelize()]
96
+
97
+ if !klass
98
+ throw new Error "#{Joosy.Module.__className @}> class can not be detected for '#{name}' mapping"
99
+
100
+ @beforeLoad (data) ->
101
+ @[name] = new (klass::__collection()) klass
102
+ if Object.isArray data[name]
103
+ @[name].reset data[name]
104
+ data
105
+
106
+ #
107
+ # Creates the descnendant of current resource with proper source
108
+ # Should be used together with lambda source (see @source for example)
109
+ #
110
+ @at: ->
111
+ if !Object.isFunction @__source
112
+ throw new Error "#{Joosy.Module.__className @}> should be created directly (without `at')"
113
+
114
+ class clone extends this
115
+ clone.__source = @__source arguments...
116
+ clone
117
+
118
+ #
119
+ # Wraps instance of resource inside shim-function allowing to track
120
+ # data changes. See class example
121
+ #
122
+ # @return [Joosy.Resource.Generic]
123
+ #
124
+ @create: ->
8
125
  shim = ->
9
- shim.__call.apply(shim, arguments)
126
+ shim.__call.apply shim, arguments
10
127
 
11
128
  if shim.__proto__
12
129
  shim.__proto__ = @prototype
@@ -16,22 +133,46 @@ class Joosy.Resource.Generic extends Joosy.Module
16
133
 
17
134
  shim.constructor = @
18
135
 
19
- @apply(shim, arguments)
136
+ @apply shim, arguments
20
137
 
21
138
  shim
22
139
 
140
+ #
141
+ # Should NOT be called directly, use .create() instead
142
+ #
143
+ # @param [Object] data Data to store
144
+ #
23
145
  constructor: (data) ->
24
- @__fillData(data)
25
-
146
+ @__fillData data
147
+
148
+ #
149
+ # Getter for wrapped data
150
+ #
151
+ # @param [String] path Attribute name to get. Can contain dots to get inline Objects values
152
+ # @return [mixed]
153
+ #
26
154
  get: (path) ->
27
- target = @__callTarget(path)
155
+ target = @__callTarget path
28
156
  target[0][target[1]]
29
157
 
158
+ #
159
+ # Setter for wrapped data, triggers 'changed'
160
+ #
161
+ # @param [String] path Attribute name to set. Can contain dots to get inline Objects values
162
+ # @param [mixed] value Value to set
163
+ #
30
164
  set: (path, value) ->
31
- target = @__callTarget(path)
165
+ target = @__callTarget path
32
166
  target[0][target[1]] = value
33
167
  @trigger 'changed'
168
+ null
34
169
 
170
+ #
171
+ # Locates the actual instance of dotted path from get/set
172
+ #
173
+ # @param [String] path Path to the attribute ('foo.bar')
174
+ # @return [Array] Instance of object containing last step of path and keyword for required field
175
+ #
35
176
  __callTarget: (path) ->
36
177
  if path.has(/\./) && !@e[path]?
37
178
  path = path.split '.'
@@ -46,16 +187,40 @@ class Joosy.Resource.Generic extends Joosy.Module
46
187
  else
47
188
  [@e, path]
48
189
 
190
+ #
191
+ # Wrapper for .create() magic
192
+ #
49
193
  __call: (path, value) ->
50
- if value
194
+ if arguments.length > 1
51
195
  @set path, value
52
196
  else
53
197
  @get path
54
198
 
55
- __fillData: (data) ->
56
- @e = @__prepareData(data)
199
+ #
200
+ # Defines how exactly prepared data should be saved
201
+ #
202
+ # @param [Object] data Raw data to store
203
+ #
204
+ __fillData: (data, notify=true) ->
205
+ @e = @__prepareData data
206
+
207
+ if notify
208
+ @trigger 'changed'
209
+
210
+ null
57
211
 
58
- __prepareData: (data) ->
59
- data = Object.extended(data)
60
- data = @__beforeLoad(data) if @__beforeLoad?
212
+ #
213
+ # Prepares raw data: cuts the root node if it exists, runs before filters
214
+ #
215
+ # @param [Object] data Raw data to prepare
216
+ # @return [Object]
217
+ #
218
+ __prepareData: (data) ->
219
+ if Object.isObject(data) && Object.keys(data).length == 1 && @__entityName
220
+ name = @__entityName.camelize(false)
221
+ data = data[name] if data[name]
222
+
223
+ if @__beforeLoad?
224
+ data = @__beforeLoad data
225
+
61
226
  data
@@ -1,76 +1,199 @@
1
1
  #= require ./rest_collection
2
2
 
3
+ #
4
+ # Resource with the HTTP REST as the backend
5
+ #
6
+ # Example:
7
+ # class Rocket extends Joosy.Resource.REST
8
+ # @entity 'rocket'
9
+ #
10
+ # r = Rocket.find {speed: 'fast'} # queries /rockets/?speed=fast to get RESTCollection
11
+ # r = Rocket.find 1 # queries /rockets/1 to get Rocket instance
12
+ #
13
+ # class Engine extends Joosy.Resource.REST
14
+ # @entity 'engine'
15
+ # @source ->
16
+ # (rocket) "/rockets/#{rocket 'id'}/engines"
17
+ #
18
+ # e = Engine.at(r).find {oil: true} # queries /rockets/1/engies?oil=true
19
+ #
3
20
  class Joosy.Resource.REST extends Joosy.Resource.Generic
4
21
 
22
+ #
23
+ # Default primary key field 'id'
24
+ #
5
25
  __primaryKey: 'id'
6
-
7
- @entity: (name) -> @::__entityName = name
8
- @source: (source) -> @::__source = source
26
+
27
+
28
+ #
29
+ # Default value of resource collection
30
+ # Will try to seek for EntityNamesCollection
31
+ # Will fallback to Joosy.Resource.RESTCollection
32
+ #
33
+ __collection: ->
34
+ named = @__entityName.camelize().pluralize() + 'Collection'
35
+ if window[named] then window[named] else Joosy.Resource.RESTCollection
36
+
37
+ #
38
+ # Sets the field containing primary key
39
+ #
40
+ # It has no direct use inside the REST resource itself and can be omited
41
+ # That's said: REST resource can work without primary at all. It's here
42
+ # just to improve end-users experience for cases when primary exists.
43
+ #
44
+ # @param [String] primary Name of the field
45
+ #
9
46
  @primary: (primary) -> @::__primaryKey = primary
10
47
 
11
- constructor: (description) ->
12
- if @constructor.__isId(description)
48
+ #
49
+ # Should NOT be called directly, use .create() instead
50
+ #
51
+ # @param [Integer|String|Object] description ID of entity or full data to store
52
+ #
53
+ constructor: (description={}) ->
54
+ if @constructor.__isId description
13
55
  @id = description
14
56
  else
15
57
  super description
16
58
  @id = @e[@__primaryKey]
17
59
 
18
- @entityName: ->
19
- unless @::hasOwnProperty '__entityName'
20
- throw new Error "Joosy.Resource.REST does not have entity name"
21
-
22
- @::__entityName
23
-
24
- # Returns single entity if int/string given
60
+ #
61
+ # Queries for REST data and creates resources instances
62
+ #
63
+ # Returns single entity if integer or string given
25
64
  # Returns collection if no value or Object (with parameters) given
26
- @find: (description, callback, options) ->
27
- if @__isId(description)
28
- resource = new @(description)
29
- resource.fetch callback, options
65
+ #
66
+ # If first parameter is a Function it's considered as a result callback
67
+ # In this case parameters will be considered equal to {}
68
+ #
69
+ # Example:
70
+ # class Rocket extends Joosy.Resource.REST
71
+ # @entity 'rocket'
72
+ #
73
+ # Rocket.find 1
74
+ # Rocket.find {type: 'nuclear'}, (data) -> data
75
+ # Rocket.find (data) -> data
76
+ # Rocket.find 1,
77
+ # success: (data) -> data)
78
+ # cache: true
79
+ #
80
+ # @param [Integer|String|Object] description ID of entity or full data to store
81
+ # @param [Function|Object] options AJAX options.
82
+ # Will be considered as a success callback if function given
83
+ #
84
+ # @return [Joosy.Resource.REST|Joosy.Resource.RESTCollection]
85
+ #
86
+ @find: (description, options) ->
87
+ if Object.isFunction options
88
+ options = {success: options}
89
+
90
+ if @__isId description
91
+ resource = @create description
92
+ resource.fetch options
30
93
  resource
31
94
  else
32
- if Object.isFunction(description) && !callback
33
- callback = description
95
+ if !options? && Object.isFunction description
96
+ options = {success: description}
34
97
  description = undefined
35
- resources = new Joosy.Resource.RESTCollection(@, description)
36
- resources.fetch callback, options
98
+ resources = new (@::__collection()) this, description
99
+ resources.fetch options
37
100
  resources
38
101
 
39
- fetch: (callback, options) ->
40
- @constructor.__ajax 'get', @constructor.__buildSource(extension: @id), options, (e) =>
41
- @__fillData(e)
42
- callback?(this)
102
+ #
103
+ # Queries the resource url and reloads the data from server
104
+ #
105
+ # @param [Function|Object] options AJAX options.
106
+ # Will be considered as a success callback if function given
107
+ # @return [Joosy.Resource.REST]
108
+ #
109
+ fetch: (options) ->
110
+ if Object.isFunction options
111
+ callback = options
112
+ else
113
+ callback = options?.success
114
+ delete options?.success
43
115
 
116
+ @constructor.__ajax 'get', @constructor.__buildSource(extension: @id), options, (e) =>
117
+ @__fillData e, false
118
+ callback? this
119
+ @trigger 'changed'
44
120
  this
45
121
 
46
122
  save: ->
47
123
 
48
- destroy: (callback, options) ->
124
+ #
125
+ # Destroys the resource by DELETE query
126
+ #
127
+ # @param [Function|Object] options AJAX options.
128
+ # Will be considered as a success callback if function given
129
+ # @return [Joosy.Resource.REST]
130
+ #
131
+ destroy: (options) ->
132
+ if Object.isFunction options
133
+ callback = options
134
+ else
135
+ callback = options?.success
136
+ delete options?.success
137
+
49
138
  @constructor.__ajax 'delete', @constructor.__buildSource(extension: @id), options, (e) =>
50
- callback?(this)
51
-
139
+ callback? this
52
140
  this
53
-
54
- @__isId: (something) -> Object.isNumber(something) || Object.isString(something)
55
-
141
+
142
+ #
143
+ # Requests the REST member URL with POST or any method given in options.type
144
+ #
145
+ # @param [String] ending Member url (like 'foo' or 'foo/bar')
146
+ # @param [Function|Object] options AJAX options.
147
+ # Will be considered as a success callback if function given
148
+ #
149
+ request: (ending, options) ->
150
+ if Object.isFunction options
151
+ callback = options
152
+ else
153
+ callback = options?.success
154
+ delete options?.success
155
+
156
+ if options.method || options.type
157
+ type = options.method || options.type
158
+ else
159
+ type = 'post'
160
+
161
+ @constructor.__ajax type, @constructor.__buildSource(extension: "#{@id}/#{ending}"), options, callback
162
+
163
+ #
164
+ # Checks if given description can be considered as ID
165
+ #
166
+ # @param [Integer|String|Object] something Value to test
167
+ # @return [Boolean]
168
+ #
169
+ @__isId: (something) ->
170
+ Object.isNumber(something) || Object.isString(something)
171
+
172
+ #
173
+ # jQuery AJAX wrapper
174
+ #
175
+ # @param [String] method HTTP Method (GET/POST/PUT/DELETE)
176
+ # @param [String] url URL to query
177
+ # @param [Object] options AJAX options to pass with request
178
+ # @param [Function] callback XHR callback
179
+ #
56
180
  @__ajax: (method, url, options={}, callback) ->
57
- $.ajax url, Object.extended(
181
+ $.ajax url, Joosy.Module.merge options,
58
182
  type: method
59
183
  success: callback
60
184
  cache: false
61
185
  dataType: 'json'
62
- ).merge options
63
186
 
187
+ #
188
+ # Builds URL for current resource location
189
+ #
190
+ # @param [Object] options Handling options
191
+ # extension: string to add to resource base url
192
+ # params: GET-params to add to resulting url
193
+ #
64
194
  @__buildSource: (options={}) ->
65
- unless @::hasOwnProperty '__source'
66
- @::__source = "/" + @entityName().pluralize()
67
-
68
- source = Joosy.buildUrl("#{@::__source}/#{options.extension || ''}", options.params)
69
-
70
- __fillData: (data) ->
71
- data = @__prepareData data
72
-
73
- if Object.isObject(data) && data[@constructor.entityName()] && data.keys().length == 1
74
- @e = Object.extended data[@constructor.entityName()]
75
- else
76
- @e = data
195
+ unless @hasOwnProperty '__source'
196
+ @__source = "/" + @::__entityName.pluralize()
197
+
198
+ source = if Object.isFunction(@__source) then @__source() else @__source
199
+ source = Joosy.buildUrl "#{source}/#{options.extension || ''}", options.params