-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathindex.coffee
190 lines (169 loc) · 7.54 KB
/
index.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
_= require('lodash')
# Takes 0 or more objects to extend/enhance a target object with their attributes.
# Lets you set which attributes to inherit, by passing an array for each object to inherit:
# - You can pass lets say 3 objects, and then 3 config arrays like this:
# bakeIn(obj1, obj2, obj3, ['attr1'], ['*'], ['!', 'attr2'], targetObj), or you can pass it like this:
# bakeIn(obj1, ['attr1'], obj2, ['*'], obj3, ['attr2'], targetObj), it doesn't matter if you mix them up,
# but the order and quantity of conf arrays must match the order and quantity of parentObjects.
bakeInModule =
bakeIn: (args...)->
receivingObj = args.pop()
receivingObj._super = {}
receivingObjAttrs = _.mapValues(receivingObj, (val)-> true) # Creates an obj, with the receivingObj keys, and a boolean
# Filter(separates) parentObjects/classes from configurations arrays
@_filterArgs(args)
@staticMethods = {}
for baseObj, i in @baseObjs
@_filter.set(@options[i])
for own key, attr of baseObj
unless @_filter.skip(key)
if _.isFunction(attr)
fn = attr
fn = if @useParentContext.hasOwnProperty(key) then fn.bind(baseObj) else fn
if receivingObjAttrs.hasOwnProperty(key)
if key is 'constructor'
@_setSuperConstructor(receivingObj, fn)
else
receivingObj._super[key] = fn
else
receivingObj[key] = receivingObj._super[key] = fn
else
# We check if the receiving object already has an attribute with that keyName
# if none is found or the attr is an array/obj we concat/merge it
if not receivingObjAttrs.hasOwnProperty(key)
receivingObj[key] = _.cloneDeep(attr)
else if _.isArray(attr)
receivingObj[key] = receivingObj[key].concat(attr)
else if _.isObject(attr) and key isnt '_super'
receivingObj[key] = _.merge(receivingObj[key], attr)
@_freezeAndHideAttr(receivingObj, '_super')
if receivingObj.hasOwnProperty('constructor')
receivingObj = @_makeConstructor(receivingObj)
return receivingObj
_setSuperConstructor: (target, constructor)->
target._super.constructor = (superArgs...)-> constructor.apply(superArgs.shift(), superArgs)
# Filters from the arguments the base objects/classes and the option filter arrays
_filterArgs: (args)->
@baseObjs = []
@options = []
@useParentContext = {}
_.each args, (arg)=>
if not _.isObject(arg)
throw new Error 'BakeIn only accepts objects/arrays/fns e.g (fn/{} parent objects/classes or an [] with options)'
else if @_isOptionArr(arg)
@options.push(@_makeOptionsObj(arg))
else if _.isFunction(arg)
# When a fn is passed, we assume is a constructor, so we copy the properties in its prototype,
# as well as any attribute that might be attached to the constructor itself(not usual, but lets be safe)
# to an empty object. Then we add a constructor property to this new obj, so inheriting from "classes", will
# always result in objects with constructors as one of their keys, unless is specifically excluded by the caller.
fn = arg
obj = _.merge({}, fn, fn::)
obj.constructor = fn
@baseObjs.push(obj)
else
# If it is a simple object we just add it to our array
@baseObjs.push(arg)
_isOptionArr: (arg)->
if _.isArray arg
isStringsArray = _.every( arg, (item)-> if _.isString item then true else false )
if isStringsArray
return true
else
throw new Error 'Array contains illegal types: The config [] should only contain strings i.e: (attr names or filter symbols (! or *) )'
else
return false
_makeOptionsObj: (attrNames)->
filterKey = attrNames[0]
switch filterKey
when '!'
if attrNames[1]?
attrNames.shift()
attrNames = @_filterParentContextFlag(attrNames, true) # We use this one here just to make sure somebody didn't call
return {'exclude': attrNames}
else
return {'excludeAll': true}
when '*'
return {'includeAll': true}
else
attrNames = @_filterParentContextFlag(attrNames)
return {'include': attrNames}
# Checks for ~ flag in each attribute name... e.g ['~methodName'], even though we check this for all
# attributes in the list, it will only work when including method names. It won't work with excludes,
# regular attributes
_filterParentContextFlag: (attrNames, warningOnMatch)->
newAttrNames = []
for attrName in attrNames
if attrName.charAt(0) is '~'
if warningOnMatch
console.warn 'The ~ should only be used when including methods, not excluding them'
attrName = attrName.replace('~', '')
newAttrNames.push(attrName)
@useParentContext[attrName] = true
else
newAttrNames.push(attrName)
return newAttrNames
_checkForBalance: (baseObjs, options)->
if options.length > 0 and baseObjs.length isnt options.length
throw new Error 'Invalid number of conf-options: If you provide a conf obj, you must provide one for each baseObj'
return true
# Helper obj to let us know if we should skip, based on
# the bakeIn filter provided and the current key
_filter:
set: (conf)->
if conf?
@mode = _.keys(conf)[0]
@attrFilters = conf[@mode]
# If an string was provided instead of an array (intentionally or unintentionally) we convert it to an array
if _.isString(@attrFilters)
@attrFilters = @attrFilters.split(',')
else
@mode = undefined
@attrFilters = undefined
skip: (key)->
# When a certain condition is met, will return true or false, so the caller can
# know if it should skip or not
switch @mode
when 'include'
# When there are no items left on the included list, we return true to always skip
if @attrFilters.length is 0
return true
keyIndex = _.indexOf(@attrFilters, key)
# If we find the key to be included we don't skip so we return false, and we remove it from the list
if keyIndex >= 0
_.pullAt(@attrFilters, keyIndex)
return false
else
return true
when 'exclude'
# When there are no items left on the excluded list, we return false to avoid skipping
if @attrFilters.length is 0
return false
keyIndex = _.indexOf(@attrFilters, key)
# If we find the key to be excluded we want to skip so we return true, and we remove it from the list
if keyIndex >= 0
_.pullAt(@attrFilters, keyIndex)
return true
else
return false
when 'includeAll'
# We never skip
return false
when 'excludeAll'
# We always skip - Useful to quickly disable inheritance in dev env
return true
else
# When no options provided is the same as include all, so we never skip
return false
_freezeAndHideAttr: (obj, attributeName)->
Object.defineProperty obj, attributeName, {enumerable: false}
Object.freeze obj[attributeName]
_makeConstructor: (obj)->
#console.log 'Making constructor'
fn = (args...)->
obj.constructor.apply(this, args)
fn.prototype = obj
#fn.prototype.constructor = fn # This creates a circular reference, should check soon
#console.log 'fn', fn.prototype
return fn
module.exports = bakeInModule.bakeIn.bind(bakeInModule)