@platforma-sdk/workflow-tengo 1.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/dist/tengo/lib/assets.lib.tengo +64 -0
- package/dist/tengo/lib/constants.lib.tengo +75 -0
- package/dist/tengo/lib/exec.internal.lib.tengo +211 -0
- package/dist/tengo/lib/exec.lib.tengo +516 -0
- package/dist/tengo/lib/exec.package.lib.tengo +296 -0
- package/dist/tengo/lib/exec.runcmd.lib.tengo +275 -0
- package/dist/tengo/lib/exec.runenv.lib.tengo +221 -0
- package/dist/tengo/lib/exec.stream.lib.tengo +64 -0
- package/dist/tengo/lib/file.lib.tengo +112 -0
- package/dist/tengo/lib/ll.lib.tengo +233 -0
- package/dist/tengo/lib/maps.lib.tengo +102 -0
- package/dist/tengo/lib/objects.lib.tengo +22 -0
- package/dist/tengo/lib/oop.lib.tengo +29 -0
- package/dist/tengo/lib/path.lib.tengo +165 -0
- package/dist/tengo/lib/pframes.constants.lib.tengo +53 -0
- package/dist/tengo/lib/pframes.export-pcolumn.lib.tengo +47 -0
- package/dist/tengo/lib/pframes.lib.tengo +84 -0
- package/dist/tengo/lib/pframes.ll.lib.tengo +86 -0
- package/dist/tengo/lib/pframes.util.lib.tengo +244 -0
- package/dist/tengo/lib/pframes.xsv.lib.tengo +178 -0
- package/dist/tengo/lib/render.lib.tengo +100 -0
- package/dist/tengo/lib/sets.lib.tengo +93 -0
- package/dist/tengo/lib/slices.lib.tengo +202 -0
- package/dist/tengo/lib/smart.lib.tengo +1005 -0
- package/dist/tengo/lib/tpl.lib.tengo +535 -0
- package/dist/tengo/lib/tpl.state-await.lib.tengo +255 -0
- package/dist/tengo/lib/tpl.util.lib.tengo +19 -0
- package/dist/tengo/lib/validation.lib.tengo +428 -0
- package/dist/tengo/lib/workdir.lib.tengo +300 -0
- package/dist/tengo/lib/workdir.proc.lib.tengo +158 -0
- package/dist/tengo/lib/workdir.save.lib.tengo +158 -0
- package/dist/tengo/lib/workflow.bobject.lib.tengo +51 -0
- package/dist/tengo/lib/workflow.bquery.lib.tengo +208 -0
- package/dist/tengo/lib/workflow.constants.lib.tengo +8 -0
- package/dist/tengo/lib/workflow.context.lib.tengo +56 -0
- package/dist/tengo/lib/workflow.lib.tengo +298 -0
- package/dist/tengo/tpl/exec.create-python-venv.plj.gz +0 -0
- package/dist/tengo/tpl/exec.exec.plj.gz +0 -0
- package/dist/tengo/tpl/pframes.aggregate.plj.gz +0 -0
- package/dist/tengo/tpl/pframes.export-multiple-pcolumns.plj.gz +0 -0
- package/dist/tengo/tpl/pframes.export-pframe.plj.gz +0 -0
- package/dist/tengo/tpl/pframes.export-single-pcolumn.plj.gz +0 -0
- package/dist/tengo/tpl/pframes.import-dir.plj.gz +0 -0
- package/dist/tengo/tpl/pframes.import-xsv-map.plj.gz +0 -0
- package/dist/tengo/tpl/pframes.map-pframe.plj.gz +0 -0
- package/dist/tengo/tpl/workdir.save.plj.gz +0 -0
- package/dist/tengo/tpl/workflow.build-ctx.plj.gz +0 -0
- package/index.d.ts +4 -0
- package/index.js +13 -0
- package/package.json +25 -0
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Library implementing state machine to await certain resource states in
|
|
3
|
+
* ephemeral templates.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
tx := import("tx")
|
|
7
|
+
ll := import("@platforma-sdk/workflow-tengo:ll")
|
|
8
|
+
sets := import("@platforma-sdk/workflow-tengo:sets")
|
|
9
|
+
text := import("text")
|
|
10
|
+
|
|
11
|
+
createContext := func() {
|
|
12
|
+
return {
|
|
13
|
+
activeSubscriptions: {},
|
|
14
|
+
resolvedStates: {}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
awaitOneState := func(field, path, state) {
|
|
19
|
+
if field.isErrorSet() && !field.isValueSet() {
|
|
20
|
+
// Field is in pure error state (just error and no referenced resource).
|
|
21
|
+
// This is a terminal state, so we stop any traversal here, and return value
|
|
22
|
+
// the same as we would return if we achieved the desired state. It is
|
|
23
|
+
// template's body responsibility to correctly handle the error state.
|
|
24
|
+
return { type: "Ready" }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if len(path) > 0 {
|
|
28
|
+
step := path[0]
|
|
29
|
+
rPath := path[1:]
|
|
30
|
+
|
|
31
|
+
if !field.isValueSet() {
|
|
32
|
+
return {
|
|
33
|
+
type: "Subscribe",
|
|
34
|
+
subscriptions: [{
|
|
35
|
+
resource: field.id.ResourceID,
|
|
36
|
+
event: "InputSet"
|
|
37
|
+
}]
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
resource := field.getValue()
|
|
42
|
+
|
|
43
|
+
if is_string(step) {
|
|
44
|
+
nextField := resource.inputs()[step]
|
|
45
|
+
if nextField == undefined {
|
|
46
|
+
if resource.info().InputsLocked {
|
|
47
|
+
ll.panic("field %v not found and inputs locked", step)
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
type: "Subscribe",
|
|
51
|
+
subscriptions: [{
|
|
52
|
+
resource: resource.id,
|
|
53
|
+
event: "FieldCreated"
|
|
54
|
+
},{
|
|
55
|
+
resource: resource.id,
|
|
56
|
+
event: "InputsLocked"
|
|
57
|
+
}]
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return awaitOneState(nextField, rPath, state)
|
|
61
|
+
} else if is_map(step) {
|
|
62
|
+
if is_undefined(step.wildcard) == is_undefined(step.match) {
|
|
63
|
+
ll.panic("Unknown step structure: %v", step)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
matcher := undefined
|
|
67
|
+
|
|
68
|
+
if !is_undefined(step.wildcard) {
|
|
69
|
+
if step.wildcard != "*" {
|
|
70
|
+
ll.panic("Complex wildcards are not supported, use { match: \"{regexp}\" }: %v", step.wildcard)
|
|
71
|
+
}
|
|
72
|
+
matcher = func(field) {
|
|
73
|
+
return true
|
|
74
|
+
}
|
|
75
|
+
} else {
|
|
76
|
+
regex := text.re_compile(step.match)
|
|
77
|
+
if is_error(regex) {
|
|
78
|
+
ll.panic("Error parsing regex %v: %v", step.match, regex)
|
|
79
|
+
}
|
|
80
|
+
matcher = func(field) {
|
|
81
|
+
return regex.match(field)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if !resource.info().InputsLocked {
|
|
86
|
+
return {
|
|
87
|
+
type: "Subscribe",
|
|
88
|
+
subscriptions: [{
|
|
89
|
+
resource: resource.id,
|
|
90
|
+
event: "InputsLocked"
|
|
91
|
+
}]
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
subscriptions := []
|
|
96
|
+
for nextFieldName, nextField in resource.inputs() {
|
|
97
|
+
if !matcher(nextFieldName) {
|
|
98
|
+
continue
|
|
99
|
+
}
|
|
100
|
+
res := awaitOneState(nextField, rPath, state)
|
|
101
|
+
if res.type == "Ready" {
|
|
102
|
+
continue
|
|
103
|
+
} else if res.type == "Subscribe" {
|
|
104
|
+
subscriptions = append(subscriptions, res.subscriptions...)
|
|
105
|
+
} else {
|
|
106
|
+
ll.panic("Unknown result type: %v", res.type)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if len(subscriptions) == 0 {
|
|
110
|
+
return { type: "Ready" }
|
|
111
|
+
} else {
|
|
112
|
+
return {
|
|
113
|
+
type: "Subscribe",
|
|
114
|
+
subscriptions: subscriptions
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
} else {
|
|
118
|
+
ll.panic("Malformed state await request path: %v", step)
|
|
119
|
+
}
|
|
120
|
+
} else {
|
|
121
|
+
if state == "Exists" {
|
|
122
|
+
return { type: "Ready" }
|
|
123
|
+
} else {
|
|
124
|
+
if !field.isSet() {
|
|
125
|
+
return {
|
|
126
|
+
type: "Subscribe",
|
|
127
|
+
subscriptions: [{
|
|
128
|
+
resource: field.id.ResourceID,
|
|
129
|
+
event: "InputSet"
|
|
130
|
+
}]
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
resource := field.getValue()
|
|
134
|
+
if state == "Set" {
|
|
135
|
+
return { type: "Ready" }
|
|
136
|
+
} else if state == "InputsLocked" {
|
|
137
|
+
if resource.info().InputsLocked {
|
|
138
|
+
return { type: "Ready" }
|
|
139
|
+
} else {
|
|
140
|
+
return {
|
|
141
|
+
type: "Subscribe",
|
|
142
|
+
subscriptions: [{
|
|
143
|
+
resource: resource.id,
|
|
144
|
+
event: "InputsLocked"
|
|
145
|
+
}]
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
} else if state == "AllInputsSet" {
|
|
149
|
+
if resource.info().AllInputsSet {
|
|
150
|
+
return { type: "Ready" }
|
|
151
|
+
} else {
|
|
152
|
+
return {
|
|
153
|
+
type: "Subscribe",
|
|
154
|
+
subscriptions: [{
|
|
155
|
+
resource: resource.id,
|
|
156
|
+
event: "AllInputsSet"
|
|
157
|
+
}]
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
} else if state == "ResourceReady" {
|
|
161
|
+
if resource.info().ResourceReady {
|
|
162
|
+
return { type: "Ready" }
|
|
163
|
+
} else {
|
|
164
|
+
return {
|
|
165
|
+
type: "Subscribe",
|
|
166
|
+
subscriptions: [{
|
|
167
|
+
resource: resource.id,
|
|
168
|
+
event: "ResourceReady"
|
|
169
|
+
}]
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
} else {
|
|
173
|
+
ll.panic("Unknown await state: %v", state)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Returns true if all requested states are reached */
|
|
180
|
+
await := func(rootField, ctxStorageResource, requests) {
|
|
181
|
+
pssKey := "await:" + string(rootField.id.ResourceID) + ":" + rootField.id.Name
|
|
182
|
+
prevSubscriptions := ctxStorageResource.kvGetAsJson(pssKey)
|
|
183
|
+
|
|
184
|
+
if !is_undefined(prevSubscriptions) && len(prevSubscriptions) == 0 {
|
|
185
|
+
// initialization was finished before, in a previous template execution
|
|
186
|
+
ll.panic("Unexpected state, desired state was reached in the previous call")
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if is_undefined(prevSubscriptions) {
|
|
190
|
+
// initializing previous subscriptions set
|
|
191
|
+
prevSubscriptions = {}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
subscriptions := {}
|
|
195
|
+
|
|
196
|
+
addSubscription := func(subscription) {
|
|
197
|
+
key := string(subscription.resource) + ":" + subscription.event
|
|
198
|
+
if !is_undefined(subscriptions[key]) {
|
|
199
|
+
return false
|
|
200
|
+
} else {
|
|
201
|
+
subscriptions[key] = subscription
|
|
202
|
+
return true
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
for _, request in requests {
|
|
207
|
+
if !is_map(request) || !is_array(request.path) || !is_string(request.state) {
|
|
208
|
+
ll.panic("Malformed state await request: %v", request)
|
|
209
|
+
}
|
|
210
|
+
res := awaitOneState(rootField, request.path, request.state)
|
|
211
|
+
if res.type == "Ready" {
|
|
212
|
+
continue
|
|
213
|
+
} else if res.type == "Subscribe" {
|
|
214
|
+
for subscription in res.subscriptions {
|
|
215
|
+
addSubscription(subscription)
|
|
216
|
+
}
|
|
217
|
+
} else {
|
|
218
|
+
ll.panic("Unknown result type %v", res.type)
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// creating new subscriptions
|
|
223
|
+
for key, sub in subscriptions {
|
|
224
|
+
if !is_undefined(prevSubscriptions[key]) {
|
|
225
|
+
// don't create subscription, if it was created in the past
|
|
226
|
+
continue
|
|
227
|
+
}
|
|
228
|
+
e := {}
|
|
229
|
+
e[sub.event] = true
|
|
230
|
+
tx.subscribeTo(sub.resource, pssKey + ":" + key, e)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// unsubscribing from subscriptions we don't need anymore
|
|
234
|
+
for key, _ in prevSubscriptions {
|
|
235
|
+
if !is_undefined(subscriptions[key]) {
|
|
236
|
+
// don't unsubscribe from subscriptions, we still need
|
|
237
|
+
continue
|
|
238
|
+
}
|
|
239
|
+
tx.unsubscribeFrom(pssKey + ":" + key)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// creating and saving new context; savong only keys
|
|
243
|
+
newPrevSubscriptions := {}
|
|
244
|
+
for key, _ in subscriptions {
|
|
245
|
+
newPrevSubscriptions[key] = true
|
|
246
|
+
}
|
|
247
|
+
// saving the context
|
|
248
|
+
ctxStorageResource.kvSetJson(pssKey, newPrevSubscriptions)
|
|
249
|
+
|
|
250
|
+
return len(subscriptions) == 0
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export ll.toStrict({
|
|
254
|
+
await: await
|
|
255
|
+
})
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/* Templates/renderers utils */
|
|
2
|
+
|
|
3
|
+
ll := import("@platforma-sdk/workflow-tengo:ll")
|
|
4
|
+
|
|
5
|
+
// template field name in renderer resource
|
|
6
|
+
TEMPLATE_FIELD := "template"
|
|
7
|
+
// field name of a resource map with the inputs in renderer resource
|
|
8
|
+
INPUTS_MAP_FIELD := "inputs"
|
|
9
|
+
// prefix of output fields template results in renderer resource
|
|
10
|
+
TEMPLATE_OUTPUT_PREFIX := "outputs/"
|
|
11
|
+
// input field containing a map with resources to prepare for the body
|
|
12
|
+
PREPARE_MAP_FIELD_NAME := "__prepare__"
|
|
13
|
+
|
|
14
|
+
export ll.toStrict({
|
|
15
|
+
TEMPLATE_FIELD : TEMPLATE_FIELD,
|
|
16
|
+
INPUTS_MAP_FIELD : INPUTS_MAP_FIELD,
|
|
17
|
+
TEMPLATE_OUTPUT_PREFIX : TEMPLATE_OUTPUT_PREFIX,
|
|
18
|
+
PREPARE_MAP_FIELD_NAME : PREPARE_MAP_FIELD_NAME
|
|
19
|
+
})
|
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
ll := import("@platforma-sdk/workflow-tengo:ll")
|
|
2
|
+
sets := import("@platforma-sdk/workflow-tengo:sets")
|
|
3
|
+
fmt := import("fmt")
|
|
4
|
+
text := import("text")
|
|
5
|
+
maps := import("@platforma-sdk/workflow-tengo:maps")
|
|
6
|
+
enum := import("enum")
|
|
7
|
+
smart := import("@platforma-sdk/workflow-tengo:smart")
|
|
8
|
+
|
|
9
|
+
json := import("json")
|
|
10
|
+
|
|
11
|
+
//
|
|
12
|
+
// Tools for implementing your own checks:
|
|
13
|
+
//
|
|
14
|
+
|
|
15
|
+
_formatKeyPath := func(current, format, key) {
|
|
16
|
+
if current == "" {
|
|
17
|
+
if !is_string(key) {
|
|
18
|
+
return fmt.sprintf("[%d]", key)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return key
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return fmt.sprintf(format, current, key)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Validation success result.
|
|
29
|
+
*/
|
|
30
|
+
success := { result: true }
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Validation failure result with a message.
|
|
34
|
+
*
|
|
35
|
+
* @param keyPath: string - the current stack state.
|
|
36
|
+
* @param msg: string - the failure message.
|
|
37
|
+
* @param args: ...any - additional arguments for the message.
|
|
38
|
+
*/
|
|
39
|
+
fail := func(keyPath, msg, ...args) {
|
|
40
|
+
return {
|
|
41
|
+
result: false,
|
|
42
|
+
message: keyPath + ": " + fmt.sprintf(msg, args...)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
isSuccess := func(r) {
|
|
47
|
+
return r.result == true
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Returns true if the validation failed.
|
|
52
|
+
*
|
|
53
|
+
* @param r: either fail or success.
|
|
54
|
+
*/
|
|
55
|
+
isFail := func(r) {
|
|
56
|
+
return r.result == false
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Checks a condition and returns success or failure.
|
|
61
|
+
*
|
|
62
|
+
* @param condition: bool - the condition to check.
|
|
63
|
+
* @param stack: any - the current stack state.
|
|
64
|
+
* @param msg: string - the failure message if the condition is false.
|
|
65
|
+
* @param args: variadic any - additional arguments for the message.
|
|
66
|
+
*/
|
|
67
|
+
check := func(condition, keyPath, msg, ...args) {
|
|
68
|
+
if condition {
|
|
69
|
+
return success
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return fail(keyPath, msg, args...)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
//
|
|
76
|
+
// Validation:
|
|
77
|
+
//
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Validates JSON types based on tags when the schema is a string.
|
|
81
|
+
*
|
|
82
|
+
* @param js: any - the JSON to validate.
|
|
83
|
+
* @param tags: { key: string, tags: []tag } - the tags to validate against.
|
|
84
|
+
* @param stack: ...any - the current stack state.
|
|
85
|
+
*/
|
|
86
|
+
_validateTypes := func(js, tags, keyPath, ...stack) {
|
|
87
|
+
k := tags.key
|
|
88
|
+
|
|
89
|
+
if k == "any" {
|
|
90
|
+
return success
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if k == "alphanumeric" && ((is_string(js) && text.re_match("^[[:alnum:]]*$", js)) || is_int(js)) {
|
|
94
|
+
return success
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if k == "string" && is_string(js) {
|
|
98
|
+
regex := tags.tags["regex"]
|
|
99
|
+
if !is_undefined(regex) && !text.re_match(regex, js) {
|
|
100
|
+
return fail(keyPath, "value %q does not conform regex %q", js, regex)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return success
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if k == "char" && is_char(js) {
|
|
107
|
+
return success
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if k == "bytes" && is_bytes(js) {
|
|
111
|
+
return success
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if k == "null" && is_undefined(js) {
|
|
115
|
+
return success
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if k == "number" && (is_int(js) || is_float(js)) {
|
|
119
|
+
return success
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if k == "bool" && is_bool(js) {
|
|
123
|
+
return success
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return fail(keyPath, "js %q is not one of a type: %s", js, tags.key)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Parses JSON tags from a string rule.
|
|
131
|
+
*
|
|
132
|
+
* @param rule: string - a key with tags, e.g. "any,type=string,regex=abc.*"
|
|
133
|
+
* @return parsedRule: { key: string, tags: map } - the key and tags parsed from the rule.
|
|
134
|
+
*/
|
|
135
|
+
_parseTags := func(rule) {
|
|
136
|
+
tagsKV := text.split(rule, ",")
|
|
137
|
+
key := tagsKV[0]
|
|
138
|
+
tagsKV = tagsKV[1:]
|
|
139
|
+
|
|
140
|
+
tags := {}
|
|
141
|
+
for _, kv in tagsKV {
|
|
142
|
+
kAndV := text.split(kv, "=")
|
|
143
|
+
k := kAndV[0]
|
|
144
|
+
if len(kAndV) == 1 {
|
|
145
|
+
tags[k] = true
|
|
146
|
+
} else {
|
|
147
|
+
tags[k] = kAndV[1]
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return ll.toStrict({
|
|
152
|
+
key: key,
|
|
153
|
+
tags: tags
|
|
154
|
+
})
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Validates JSON against a schema.
|
|
159
|
+
*
|
|
160
|
+
* @param js: any - the JSON to validate.
|
|
161
|
+
* @param schema: any - the schema to validate against.
|
|
162
|
+
* @param stack: ...any - the current stack state.
|
|
163
|
+
* @return either fail or success
|
|
164
|
+
*/
|
|
165
|
+
_validateJson := func(js, schema, keyPath, ...stack) {
|
|
166
|
+
if is_undefined(schema) {
|
|
167
|
+
if len(stack) == 0 {
|
|
168
|
+
return fail(keyPath, "key is not defined by schema, but exists in the JSON")
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return fail(keyPath, "key is not defined by schema, but exists in the JSON. Validated item: %#v", stack[0])
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// if the schema is a string, then
|
|
175
|
+
// we check the predefined type.
|
|
176
|
+
if is_string(schema) {
|
|
177
|
+
if schema == "any" {
|
|
178
|
+
return success
|
|
179
|
+
}
|
|
180
|
+
tags := _parseTags(schema)
|
|
181
|
+
|
|
182
|
+
return _validateTypes(js, tags, keyPath, stack...)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// if the schema is a function, call it
|
|
186
|
+
// with the element and the path of how we
|
|
187
|
+
// got this element, i.e. recursion history.
|
|
188
|
+
if is_callable(schema) {
|
|
189
|
+
return schema(js, keyPath, stack)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// if the schema is an array with "or" at the beginning,
|
|
193
|
+
// we should check if the element is succeed with
|
|
194
|
+
// any of the following elements.
|
|
195
|
+
if is_array(schema) && len(schema) > 0 && schema[0] == "or" {
|
|
196
|
+
for i, elem in schema[1:] {
|
|
197
|
+
r := _validateJson(js, elem, keyPath, stack...)
|
|
198
|
+
if isSuccess(r) {
|
|
199
|
+
return r
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return fail(keyPath, "js %q does not fit any of the following rules: %q", js, schema)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// if the schema is an array, then check that every
|
|
207
|
+
// element of the json has the schema that is the first element
|
|
208
|
+
// of the schema.
|
|
209
|
+
if is_array(schema) {
|
|
210
|
+
if !is_array(js) {
|
|
211
|
+
return fail(keyPath, "js %q must be array", js)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
schema = schema[0]
|
|
215
|
+
for i, elem in js {
|
|
216
|
+
r := _validateJson(elem, schema, _formatKeyPath(keyPath, "%s[%d]", i), js, stack...)
|
|
217
|
+
if isFail(r) {
|
|
218
|
+
return r
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return success
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// only map left, since schema can't be undefined
|
|
226
|
+
// if the schema is a map, check that every key
|
|
227
|
+
// in json-object is in the schema and vice-versa,
|
|
228
|
+
// unless there are tags "any" or "omitempty".
|
|
229
|
+
if !ll.isMap(js) {
|
|
230
|
+
return fail(keyPath, "js %#v must be map", js)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// schema.key -> tags
|
|
234
|
+
schemaTags := {}
|
|
235
|
+
// schema.key -> rule
|
|
236
|
+
schemaRules := {}
|
|
237
|
+
|
|
238
|
+
for k, v in schema {
|
|
239
|
+
p := _parseTags(k)
|
|
240
|
+
schemaTags[p.key] = p
|
|
241
|
+
schemaRules[p.key] = v
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// tags that should be applied to all elements
|
|
245
|
+
any := schemaTags["any"]
|
|
246
|
+
|
|
247
|
+
options := schemaTags["__options__"]
|
|
248
|
+
if is_undefined(options) {
|
|
249
|
+
options = {}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
for _key, elem in js {
|
|
253
|
+
if any {
|
|
254
|
+
r := _validateJson(elem, schemaRules["any"], _formatKeyPath(keyPath, "%s.%s", _key), js, stack...)
|
|
255
|
+
if isFail(r) {
|
|
256
|
+
return r
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if !is_undefined(any.tags["type"]) {
|
|
260
|
+
r := _validateJson(_key, any.tags["type"], keyPath, stack...)
|
|
261
|
+
if isFail(r) {
|
|
262
|
+
return r
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
continue
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// tags for the key
|
|
270
|
+
key := schemaTags[_key]
|
|
271
|
+
|
|
272
|
+
if is_undefined(key) {
|
|
273
|
+
if options.tags["closed"] == true {
|
|
274
|
+
return fail(keyPath, "only keys %v from the schema must be set, found %q", maps.getKeys(schema), _key)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
continue
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if key.tags["type"] {
|
|
281
|
+
r := _validateJson(_key, key.tags["type"], keyPath, stack...)
|
|
282
|
+
if isFail(r) {
|
|
283
|
+
return r
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if key.tags["omitempty"] && is_undefined(elem) {
|
|
288
|
+
continue
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
r := _validateJson(elem, schemaRules[_key], _formatKeyPath(keyPath, "%s.%s", _key), js, stack...)
|
|
292
|
+
if isFail(r) {
|
|
293
|
+
return r
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return success
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Checks if the given JSON conforms to the given schema.
|
|
302
|
+
*
|
|
303
|
+
* @param js: any - the JSON to check.
|
|
304
|
+
* @param schema: any - the schema to check against.
|
|
305
|
+
* @return either fail or success
|
|
306
|
+
*/
|
|
307
|
+
checkJson := func(js, schema) {
|
|
308
|
+
return _validateJson(js, schema, "")
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Checks if the given JSON is valid according to the given schema.
|
|
313
|
+
*
|
|
314
|
+
* @param js: any - the JSON to check.
|
|
315
|
+
* @param schema: any - the schema to check against.
|
|
316
|
+
* @return result: bool - the result of the validation (true/false).
|
|
317
|
+
*/
|
|
318
|
+
isValid := func(js, schema) {
|
|
319
|
+
return _validateJson(js, schema, "").result
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Asserts that a given JSON conforms to a given schema.
|
|
324
|
+
*
|
|
325
|
+
* @param js: any - the JSON to assert.
|
|
326
|
+
* @param schema: any - the schema to assert against.
|
|
327
|
+
* @param failMsg: string - optional parameter with custom message to be printed on validation error
|
|
328
|
+
*/
|
|
329
|
+
assertJsonSchema := func(js, schema, ...failMsg) {
|
|
330
|
+
r := _validateJson(js, schema, "")
|
|
331
|
+
|
|
332
|
+
msg := "JSON validation error"
|
|
333
|
+
if len(failMsg) > 0 {
|
|
334
|
+
msg = failMsg[0]
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if isFail(r) {
|
|
338
|
+
ll.panic("%s: %s", msg, r.message)
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
//
|
|
343
|
+
// Domain-specific schemas and checks:
|
|
344
|
+
//
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Checks if the given element is a resource.
|
|
348
|
+
*
|
|
349
|
+
* @param options: object - resource validation options
|
|
350
|
+
*/
|
|
351
|
+
resource := func(...args) {
|
|
352
|
+
|
|
353
|
+
ops := {}
|
|
354
|
+
if len(args) > 0 {
|
|
355
|
+
ops = args[0]
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
assertJsonSchema(ops, {
|
|
359
|
+
`type,omitempty`: {
|
|
360
|
+
Name: `string`,
|
|
361
|
+
Version: `string`
|
|
362
|
+
}
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
checker := func(elem, keyPath, stack) {
|
|
366
|
+
c := undefined
|
|
367
|
+
|
|
368
|
+
c = check(smart.isResource(elem), keyPath, "the element " + elem + " must be a resource")
|
|
369
|
+
if isFail(c) {
|
|
370
|
+
return c
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if !is_undefined(ops.type) {
|
|
374
|
+
c = check(elem.info().Type.Name == ops.type.Name && elem.info().Type.Version == ops.type.Version, keyPath,
|
|
375
|
+
"the element " + elem + " must have the following type: ", ops.type, " found: ", elem.info().Type)
|
|
376
|
+
if isFail(c) {
|
|
377
|
+
return c
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return c
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return checker
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Checks if the given element is a resource with specified type.
|
|
389
|
+
*
|
|
390
|
+
* @param rt: ResourceType - desired resource type
|
|
391
|
+
*/
|
|
392
|
+
resourceType := func(rt) {
|
|
393
|
+
return func(elem, keyPath, stack) {
|
|
394
|
+
return check(smart.isResource(elem) && elem.info().Type.Name == rt.Name, keyPath, "the element " + elem + " must be a resource")
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Checks if the given element is a reference.
|
|
400
|
+
*
|
|
401
|
+
* @param elem: any - the element to check.
|
|
402
|
+
* @param stack: any - the current stack state.
|
|
403
|
+
*/
|
|
404
|
+
reference := func(elem, keyPath, stack) {
|
|
405
|
+
return check(smart.isReference(elem), keyPath, "the element " + elem + " must be a resource or a field")
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Schema for reference
|
|
410
|
+
*/
|
|
411
|
+
refSchema := {
|
|
412
|
+
blockId: `string`,
|
|
413
|
+
name: `string`
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
export ll.toStrict({
|
|
417
|
+
success: success,
|
|
418
|
+
fail: fail,
|
|
419
|
+
isSuccess: isSuccess,
|
|
420
|
+
isFail: isFail,
|
|
421
|
+
checkJson: checkJson,
|
|
422
|
+
isValid: isValid,
|
|
423
|
+
assertJsonSchema: assertJsonSchema,
|
|
424
|
+
resource: resource,
|
|
425
|
+
reference: reference,
|
|
426
|
+
resourceType: resourceType,
|
|
427
|
+
ref: refSchema
|
|
428
|
+
})
|