forked from joelvh/json2json
-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathObjectTemplate.coffee
187 lines (143 loc) · 5.96 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
# handle CommonJS/Node.js or browser
sysmo = require?('sysmo') || window?.Sysmo
TemplateConfig = require?('./TemplateConfig') || window?.json2json.TemplateConfig
Q = require 'q'
# class definition
class ObjectTemplate
constructor: (config, parent) ->
@config = new TemplateConfig config
@parent = parent
transform: (data) =>
node = @nodeToProcess data
return Q(null) unless node?
# process properties
switch sysmo.type node
when 'Array' then @processArray node
when 'Object' then @processMap node
else Q(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 []
([e,i] for e, i in node).reduce (chain, ei) =>
[element, index] = ei
chain.then () =>
# 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
@createMapStructure(element).then (value) =>
@updateContext context, element, value, key
, Q(context)
processMap: (node) =>
@createMapStructure(node).then (context) =>
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
([k,v] for k, v of node).reduce (chain, kv) =>
[key, value] = kv
chain.then () =>
return context if not @config.processable node, value, key
# call @getNode() to register the use of the property on that node
nested = @getNode(node, key)
@chooseValue(nested).then (value) =>
@updateContext context, nested, value, key
, Q(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 Q(result.value)
when 'path' then Q(@getNode node, result.value)
when 'template' then @processTemplate node, context, result.value
else Q(null)
processTemplate: (node, context, template = {}) =>
chain = ([k,v] for k, v of template).reduce (chain, kv) =>
[key, value] = kv
chain.then () =>
# 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
# TODO: allow async for this custom function as well
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
, Q(context)
chain.then () =>
@processRemaining context, node
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.
([k,v] for k, v of node).reduce (chain, kv) =>
[key, value] = kv
chain.then () =>
return context if @pathAccessed(node, key) or key in context or !@config.processable node, value, key
@updateContext context, node, value, key
, Q(context)
updateContext: (context, node, value, key) =>
# format key and value
@config.applyFormatting(node, value, key).then (formatted) =>
@aggregateValue context, formatted.key, formatted.value
aggregateValue: (context, key, value) =>
return context unless value?
# if context is an array, just add the value
if sysmo.isArray(context)
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