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 | ###
|
17 | Todo:
|
18 | validatedDeclarable / validatedExtendableProperty
|
19 | Which use Art.Validation
|
20 |
|
21 | TODO:
|
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 |
|
45 | defineModule 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 |