-
Notifications
You must be signed in to change notification settings - Fork 1
/
index.js
272 lines (243 loc) · 10.4 KB
/
index.js
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
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
/**
* @license
* ©2015-2016 Luxembourg Institute of Science and Technology All Rights Reserved
* JavaScript Modelling Framework (JSMF)
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
* @author J.S. Sottet
* @author N. Biri
* @author A. Vagner
*/
'use strict'
const _ = require ('lodash')
/**
* Crawl crawls la whole JSMF model from a given entry point
*
* @param {object} searchParameters - The definition of how the model is crawled, the following object properties are inspected:
* @param {Function} [searchParameters.predicate=_.constant(true)] - A predicate (a function that takes an object as parameter) that must be fullfilled by an object to be part of the result. If undefined, all the objects are accepted
* @param {Number} [searchParameters.depth=-1] - the number of references to be followed befor we stop crawling, if we don't want to limit crawling, use -1.
* @param {Function} [searchParameters.followIf=_.constant(true)] - A function that take an object and a reference as parameters, if the function is evaluate to true, we follow this reference, otherwise, we stop crawling this branch. If undefined, all the references are followed.
* @param {boolean} [searchParameters.stopOnfirst=false] - Set if we continue to crawl the model when the expected predicate is found.
* @param {boolean} [searchParameters.includeRoot=true] - include the entrypoint in the result.
* @param {object} entrypoint - The entrypoint object to crawl the model.
* @returns {List} The elements that fullfill the searchParameters
* See unit tests for examples.
*/
function crawl(searchParameters, entrypoint) {
const predicate = searchParameters['predicate'] || _.constant(true)
let depth = searchParameters['depth']
if (depth === undefined) { depth = -1 }
const propertyFilter = searchParameters['followIf'] || _.constant(true)
let includeRoot = searchParameters['includeRoot']
if (includeRoot === undefined) { includeRoot = true }
let stopOnFirst = searchParameters['stopOnFirst']
if (stopOnFirst === undefined) { stopOnFirst = false }
const startingNodes = includeRoot
? [crawlEntry(entrypoint, depth)]
: _.map(nodeChildren(propertyFilter, entrypoint), x => crawlEntry(x, nextDepth(depth)))
return _crawl(predicate, propertyFilter, stopOnFirst, startingNodes).result
}
function nextDepth(depth) {
return depth > 0 ? depth - 1 : depth
}
function _crawl(predicate, propertyFilter, stopOnFirst, entrypoints) {
const ctx = {visited: new Set(), result: []}
while (!(_.isEmpty(entrypoints))) {
const current = entrypoints.pop()
const entrypoint = current.elem
const depth = current.depth
let children = []
let found = false
if (entrypoint !== undefined && !ctx.visited.has(entrypoint)) {
ctx.visited.add(entrypoint)
found = predicate(entrypoint)
if (found) {
ctx.result.push(entrypoint)
if (stopOnFirst) { return ctx }
}
if (depth !== 0) {
children = nodeChildren(propertyFilter, entrypoint)
}
const newDepth = nextDepth(depth)
children = _.map(children, x => crawlEntry(x, newDepth))
}
entrypoints = entrypoints.concat(children)
}
return ctx
}
function nodeChildren(filter, entrypoint) {
const refs = entrypoint.conformsTo().getAllReferences()
return _(refs).map((v, ref) => filter(entrypoint, ref) ? entrypoint[ref] : [])
.flatten()
.value()
}
function crawlEntry(elem, depth) {
return {elem, depth}
}
/**
* Get all the modelingelements from a model that belongs to a class (according to their inheritance chain)
* @param {Class} cls - The class we are looking for
* @param {Model} model - The inspected model.
* @param {boolean} [strict=false] - If strict is false, seek for instances of the clas or of any of its subclass. Otherwise, seek only exact instances of the class.
*/
function allInstancesFromModel (cls, model, strict) {
const me = _.get(model, ['referenceModel', 'modellingElements'])
if (_.isEmpty(me)) {
const os = _(model.modellingElements).values().flatten().value()
if (!strict) {
return _.filter(os,
x => _(x.conformsTo().getInheritanceChain())
.map('__name')
.includes(cls.__name))
} else {
return _.filter(os, x=> x.conformsTo().__name === cls.__name)
}
} else if (!strict) {
const clss = _(me).values().flatten().value()
return _(clss).filter( x => (x.getInheritanceChain !== undefined)
&& _.includes(x.getInheritanceChain(), cls))
.map('__name')
.map(x => model.modellingElements[x] || [])
.flatten()
.value()
} else {
return me[cls.__name]
}
}
/**
* Get all the modelingelements from a model that satisfies a predicate
* @param {Function} predicate - A predicate (a function that takes an object as parameter) that must be fullfilled by an object to be part of the result.
* @param {Model} model - The inspected model.
* @returns {List} The elements of the model that verifies the predicate function
*/
function filterModelElements (predicate, model) {
return _(model.modellingElements).values()
.flatten()
.filter(x => predicate(x))
.value()
}
/**
* Get the elements down a given path from a given entrypoint of a model.
* @param {object} searchParameters - The parameters of the search. The following properties are inspected:
* @param {List} path - The path to follow. A path is a list of reference names that must be followed. The last element can be an attribute name. If no value is given, the default value is the empty list
* @param {Function} [searchParameters.predicate=_.constant(true)] - A predicate (a function that takes an object as parameter) that must be fullfilled by an object to be part of the result. If undefined, all the objects are accepted
* @param {boolean} [searchParameters.targetOnly=true] - if true, we return only the objects at the end of the path otherwise, we also take objects we pass through during the search. Default value is 'true'.
* @param {Symbol} - [searchParameters.searchMethod=DFS_All] - The searchMethod used to crawl the model (either {DFS_All}, {DFS_First}, {BFS_All}, or {BFS_First}).
* @returns {List} The elements that fullfill the searchParameters
*/
function follow(searchParameters, entrypoint) {
const path = searchParameters['path'] || []
path.reverse()
const predicate = searchParameters['predicate'] || _.constant(true)
let targetOnly = searchParameters['targetOnly']
if (targetOnly === undefined) { targetOnly = true }
const method = searchParameters['searchMethod'] || DFS_All
const entrypoints = [followEntry(entrypoint, path)]
return _follow(predicate, method, targetOnly, entrypoints)
}
function followEntry(elem, path) {
return {elem, path}
}
function _follow(predicate, method, targetOnly, entrypoints) {
const acc = []
while (!(_.isEmpty(entrypoints))) {
const current = entrypoints.pop()
const entrypoint = current.elem
const path = current.path
if ((!targetOnly || _.isEmpty(path)) && predicate(entrypoint)) {
acc.push(entrypoint)
if (stopOnFirst(method)) { return acc }
}
if (!(_.isEmpty(path))) {
const pathElement = path.pop()
if (_.isString(pathElement)) {
let values = entrypoint[pathElement]
if (values === undefined) {
throw new Error(`Unsuppported method ${pathElement} for object ${entrypoint}`)
}
values = _.map(values, x => followEntry(x, path))
entrypoints = isDFS(method)
? entrypoints.concat(values)
: values.concat(entrypoints)
} else if (pathElement instanceof Function) {
if (pathElement(entrypoint)) {
entrypoints.push(followEntry(entrypoint, path))
}
} else {
throw new Error(`invalid path element ${pathElement}`)
}
}
}
return acc
}
/*********************
* Predicate helpers *
*********************/
/**
* hasClass returns a function that checks if a given object belongs to the given JSMF Class.
* @param {Class} cls - The expected Class.
*/
function hasClass(cls) {
return (x => _.includes(x.conformsTo().getInheritanceChain(), cls))
}
/**************************
* PropertyFilter helpers *
**************************/
/**
* A helper for the construction of the propertyFilter for @{allInstancesFromObject} and @{getObjectsFromObject}.
* for a given clas, follows only the references provided in the corresponding map entry.
*/
function referenceMap(x) {
return ((e, ref) => {
const hierarchy = e.conformsTo().getInheritanceChain()
return _.some(hierarchy, c => _.includes(x[c.__name], ref))
})
}
/******************
* Search Methods *
******************/
/**
* - DFS_All: Deep First Search, get all the elements that match the predicate.
*/
const DFS_All = Symbol('DFS_All')
/**
* BFS_All: Breadth First Search, get all the elements that match the predicate.
*/
const BFS_All = Symbol('BFS_All')
/**
* DFS_First: Deep First Search, get the first element that matches the predicate.
*/
const DFS_First = Symbol('DFS_First')
/**
* BFS_First: Breadth First Search, get the first element that matches the predicate.
*/
const BFS_First = Symbol('BFS_First')
function isDFS(m) {
return _.includes([DFS_All, DFS_First], m)
}
function stopOnFirst(m) {
return _.includes([DFS_First, BFS_First], m)
}
module.exports = {
crawl,
follow,
allInstancesFromModel,
filterModelElements,
DFS_All,
DFS_First,
BFS_All,
BFS_First,
hasClass,
referenceMap
}