-
Notifications
You must be signed in to change notification settings - Fork 39
/
ObjectTemplate.coffee
206 lines (158 loc) · 6.55 KB
/
ObjectTemplate.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
# handle CommonJS/Node.js or browser
sysmo = require?('sysmo') || window?.Sysmo
TemplateConfig = require?('./TemplateConfig') || window?.json2json.TemplateConfig
# class definition
class ObjectTemplate
constructor: (config, parent) ->
@config = new TemplateConfig config
@parent = parent
transform: (data) =>
node = @nodeToProcess data
return null unless node?
# process properties
switch sysmo.type node
when 'Array' then @processArray node
when 'Object' then @processMap node
else null #node
# assume each array element is a map
processArray: (node) =>
# convert array to hash if config.arrayToMap is true
context = if @config.arrayToMap then {} else []
for element, index in node #when @config.processable node, element, index
# convert the index to a key if converting array to map
# @updateContext handles the context type automatically
key = if @config.arrayToMap then @chooseKey(element) else index
# don't call @processMap because it can lead to double nesting if @config.nestTemplate is true
value = @createMapStructure(element)
# because we don't call @processMap we have to manually ensure values are arrays
if @config.arrayToMap and @config.ensureArray and !context[key]?
value = [value]
else
key = index
# we are calling both @createMapStructure and @processMap because we need to
# create the map structure for the choosen keys in `choose` as well as map it
# to an array
if @config.config.choose
value = @processMap(@createMapStructure(element))
else
value = @processMap(element)
@updateContext context, element, value, key
context
processMap: (node) =>
# convert hash to array if config.mapToArray is true
if @config.mapToArray
context = []
# Simple iteration and context updation
for key, value of node
@updateContext context, node, value, key
return context
if @config.ensureArray then return @processArray [node]
context = @createMapStructure node
if @config.nestTemplate and (nested_key = @chooseKey(node))
nested_context = {}
nested_context[nested_key] = context;
context = nested_context
context
createMapStructure: (node) =>
context = {}
return @chooseValue(node, context) unless @config.nestTemplate
# loop through properties to pick up any key/values that should be nested
for key, value of node when @config.processable node, value, key
# call @getNode() to register the use of the property on that node
nested = @getNode(node, key)
value = if @config.mapToArray then nested else @chooseValue nested
@updateContext context, nested, value, key
context
chooseKey: (node) =>
result = @config.getKey node
switch result.name
when 'value' then result.value
when 'path' then @getNode node, result.value
else null
chooseValue: (node, context = {}) =>
result = @config.getValue node
switch result.name
when 'value' then result.value
when 'path' then @getNode node, result.value
when 'template' then @processTemplate node, context, result.value
else null
processTemplate: (node, context, template = {}) =>
# loop through properties in template
for key, value of template
# process mapping instructions
switch sysmo.type value
# string should be the path to a property on the current node
when 'String' then filter = (node, path) => @getNode(node, path)
# array gets multiple property values
when 'Array' then filter = (node, paths) => @getNode(node, path) for path in paths
# function is a custom filter for the node
when 'Function' then filter = (node, value) => value.call(@, node, key)
when 'Object' then filter = (node, config) => new @constructor(config, @).transform node
else filter = (node, value) -> value
value = filter(node, value)
@updateContext context, node, value, key
@processRemaining context, node
context
processRemaining: (context, node) =>
# loop through properties to pick up any key/values that should be chosen.
# skip if node property already used, the property was specified by the template, or it should not be choose.
for key, value of node when !@pathAccessed(node, key) and key not in context and @config.processable node, value, key
@updateContext context, node, value, key
context
updateContext: (context, node, value, key) =>
# format key and value
formatted = @config.applyFormatting node, value, key
if sysmo.isArray(formatted)
@aggregateValue context, item.key, item.value for item in formatted
else if formatted?
@aggregateValue context, formatted.key, formatted.value
aggregateValue: (context, key, value) =>
return context unless value? or [email protected]
# if context is an array, just add the value
if sysmo.isArray(context)
if @config.config.flatArray && sysmo.isArray(value)
context.push.apply(context, value)
else
context.push(value)
return context
existing = context[key]
return context if @config.aggregate context, key, value, existing
if !existing?
context[key] = value
else if !sysmo.isArray(existing)
context[key] = [existing, value]
else
context[key].push value
context
nodeToProcess: (node) =>
@getNode node, @config.getPath()
getNode: (node, path) =>
return null unless path
return node if path is '.'
@paths node, path
sysmo.getDeepValue node, path, true
pathAccessed: (node, path) =>
key = path.split('.')[0]
@paths(node).indexOf(key) isnt -1
# track the first property in a path for each node through object tree
paths: (node, path) =>
path = path.split('.')[0] if path
@pathNodes or= @parent and @parent.pathNodes or []
@pathCache or= @parent and @parent.pathCache or []
index = @pathNodes.indexOf node
return (if index isnt -1 then @pathCache[index] else []) unless path
if index is -1
paths = []
@pathNodes.push node
@pathCache.push paths
else
paths = @pathCache[index]
paths.push(path) if path and paths.indexOf(path) == -1
paths
# register module (CommonJS/Node.js) or handle browser
if module?
module.exports = ObjectTemplate
else
window.json2json or= {}
window.json2json.ObjectTemplate = ObjectTemplate