UNPKG

9.26 kBtext/coffeescriptView Raw
1{
2 defineModule, log, object, upperCamelCase, lowerCamelCase, each
3 isPlainObject
4 isPlainArray
5 isFunction
6 isNumber
7 isBoolean
8 cloneStructure
9 isString
10 mergeInto
11 concatInto
12 formattedInspect
13 merge
14} = require 'art-standard-lib'
15
16###
17Todo:
18 validatedDeclarable / validatedExtendableProperty
19 Which use Art.Validation
20
21TODO:
22 When we switch to ES6, we should make the
23 class API look identical to the current instance API.
24
25 That means declarable API looks like this:
26 @extendableProperty foo: {}
27
28 # extend:
29 @foo: hi: 123
30
31 The differnce is we add a ":".
32
33 The benefit is it's a normal getter/setter pair:
34
35 @foo = hi: 123
36
37 log @foo
38
39 The one diference is the "setter" is really an
40 "extender"
41
42
43###
44
45defineModule module, -> (superClass) -> class ExtendablePropertyMixin extends superClass
46
47 ###
48 IN
49 object: any object
50 property: string, property name
51 init:
52 (object) -> returning initial value for object
53 OR
54 initial value is computed by:
55 cloneStructure object[property] || init
56
57 EFFECT:
58 if object.hasOwnProperty property, return its current value
59 otherwise, initialize and return it with init()
60 ###
61 @getOwnProperty: getOwnProperty = (object, internalName, init) ->
62 if object.hasOwnProperty internalName
63 object[internalName]
64 else object[internalName] = init object, internalName
65
66 optimizedInitFunction = (internalName, init) ->
67 switch
68 when isFunction init then init
69 when isString(init) || isNumber(init) || isBoolean init
70 (object) -> object[internalName] ? init
71
72 else
73 (object) -> cloneStructure object[internalName] ? init
74
75 ###
76 objectPropertyExtender
77
78 IN: @ is set to the property-value to extend
79
80 API 1:
81 IN: map
82 EFFECT: mergeInto propValue, map
83
84 API 2:
85 IN: key, value
86 EFFECT: propValue[key] = valuee
87
88 OUT: ignore
89 ###
90 @objectPropertyExtender: objectPropertyExtender = (toExtend, mapOrKey, value) ->
91 return toExtend if mapOrKey == undefined || mapOrKey == null
92 if isString mapOrKey
93 toExtend[mapOrKey] = value
94 else if isPlainObject mapOrKey
95 mergeInto toExtend, mapOrKey
96 else
97 log {mapOrKey, value, type: mapOrKey?.constructor}
98 throw new Error "first value argument must be a plain object or string: #{formattedInspect {key:mapOrKey, value}}"
99 toExtend
100
101 ###
102 arrayPropertyExtender
103
104 IN: valueToExtend, value
105 value:
106 array: concatInto propValue, array
107 non-array: propValue.push value
108
109 NOTE: if you want to concat an array-as-a-value to the end of propValue, do this:
110 arrayPropertyExtender.call propValue, [arrayAsValue]
111
112 OUT: ignore
113 ###
114 @arrayPropertyExtender: arrayPropertyExtender = (toExtend, arrayOrValue) ->
115 if isPlainArray arrayOrValue
116 concatInto toExtend, arrayOrValue
117 else
118 toExtend.push arrayOrValue
119 toExtend
120
121 ###
122 Extendable Properties
123
124 EXAMPLE:
125 class Foo extends BaseClass
126 @extendableProperty foo: {}
127
128 Extendable properties work like inheritance:
129
130 When any subclass or instance extends an extendable property, they
131 inherit a cloneStructure of the property from up the inheritance tree, and then
132 add their own extensions without effecting the parent copy.
133
134 With Object property types, this can just be a parallel prototype chain.
135 (It isn't currently: if you modify a parent after extending it to a child,
136 the child won't get updates.)
137
138 BUT, you can also have array or other types of extend-properties, which
139 JavaScript doesn't have any built-in mechanisms for inheriting.
140
141 BASIC API:
142 @extendableProperty: (map, options) -> ...
143
144 IN:
145 map: name: defaultValue
146 options:
147 declarable: true/false
148 if true, slightly alters the created functions:
149 for: @extendableProperty foo: ...
150 generates:
151 @foo
152
153 extend:
154 DEFAULTS:
155 switch defaultValue
156 when is Object then objectPropertyExtender
157 when is Array then arrayPropetyExtender
158 else defaultExtender
159
160 (extendable, extendWithValues...) -> newExtendedOwnPropertyValue
161 IN:
162 extendable: the current, extended value, already cloned, so direct mutation is OK
163 extendWithValues: 1 or more values passed into the extend funtion by the client.
164 Ex: for an array, this is either a single value or an array
165 Ex: for an object, this is either a single object or two args: key, value
166 OUT: new property value to set own-property to
167 EFFECT:
168 Can be pure functional and just return the new, extended data.
169 OR
170 Can modify extendable directly, since it is an object/array/atomic value unique to the current class/instance.
171 If modifying extendable directly, be sure to return extendable.
172 Regardless, the returned value becomes the new extendable prop's value.
173
174
175
176 EFFECT: for each {foo: defaultValue} in map, extendableProperty:
177 WARNING:
178 !!! Don't modify the object returned by a getter !!!
179
180 Getters only return the current, most-extended property value. It may not be extended to the
181 current subclass or instance! Instead, call @extendFoo() if you wish to manually modify
182 the extended property.
183
184 declarable:
185 getters:
186 @getFoo:
187 getFoo:
188
189 extenders:
190 @foo:
191 foo:
192
193 non-declarable:
194
195 getters:
196 @getFoo:
197 @getter foo:
198
199 extenders:
200 @foo:
201 @extendFoo:
202 extendFoo:
203
204 IN:
205 0-args: nothing happens beyond the standard EFFECT
206 1+args: passed to the "extend" function
207
208 EFFECT: creates a extension (cloneStructure) of the property for the currnet class, subclass or instance
209 OUT: the current, extendedPropValue
210
211 API 1: IN: 0 args
212 NO ADDITIONAL EFFECT - just returns the extended property
213 API 2: IN: 1 or more args
214 In addition to extending and returning the extended property:
215 calls: propExtender extendedPropValue, args...
216
217 NOTE: gthe prototype getters call the class getter for extension purposes.
218 The result is each instance won't get its own version of the property.
219 E.G. Interitance is done at the Class level, not the Instance level.
220
221 ###
222 defaultExtender = (toExtend, v) ->
223 throw new Error "not expecting undefined" if v == undefined
224 v
225
226 noOptions = {}
227
228 @extendableProperty: (map, options = noOptions) ->
229 if isFunction oldExtender = options
230 log.error "DEPRICATED customPropertyExtender not supported, use extend: option "
231 options = extend: (extendable, args...) ->
232 oldExtender.apply extendable, args
233 {extend, declarable, noSetter} = options
234 each map, (defaultValue, name) =>
235
236 name = lowerCamelCase name
237 ucProp = upperCamelCase name
238 internalName = @propInternalName name
239 getterName = "get#{ucProp}"
240 extenderName = "extend#{ucProp}"
241
242 propertyExtender = extend ?
243 if isPlainObject defaultValue then objectPropertyExtender
244 else if isPlainArray defaultValue then arrayPropertyExtender
245 else
246 throw new Error "defaultValue must not be undefined" unless defaultValue != undefined
247 defaultExtender
248
249 ###########################
250 # Class Methods
251 ###########################
252 @[getterName] = -> @prototype[internalName] ? defaultValue
253
254 # extend prototype (class)
255 # IN: value (must match defaultValue's type - an object or an array)
256 # EFFECT: property has been extended for the class-object this was called on (not affecting any parent class)
257 # OUT: the extendable property's current value
258 @[name] = @[extenderName] = (value) ->
259 extendablePropValue = getOwnProperty @prototype, internalName, optimizedInitFunction internalName, defaultValue
260 if arguments.length > 0 && value != undefined
261 @prototype[internalName] = propertyExtender extendablePropValue, arguments...
262 extendablePropValue
263
264 ###########################
265 # Instance Methods
266 ###########################
267 instanceGetter = -> @[internalName] ? defaultValue
268 # extend this (instance)
269 # IN: value (must match defaultValue's type - an object or an array)
270 # EFFECT: property has been extended for the current instance-object this was called on (not affecting it's class or any parent-class)
271 # OUT: the extendable property's current value
272 instanceExtender = @prototype[extenderName] = (value) ->
273 extendablePropValue = getOwnProperty @, internalName, optimizedInitFunction internalName, defaultValue
274 if arguments.length > 0 && value != undefined
275 @[internalName] = propertyExtender extendablePropValue, arguments...
276 extendablePropValue
277
278 if declarable
279 @prototype[getterName] = instanceGetter
280 @prototype[name] = instanceExtender
281 else
282 @addSetter name, instanceExtender unless noSetter
283 @addGetter name, instanceGetter
284
285 @declarable: (map, options) ->
286 @extendableProperty map, merge options, declarable: true
\No newline at end of file