@01-edu/shared 1.0.2 → 1.0.3

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.
@@ -0,0 +1,236 @@
1
+ import { childTypes, objectTypes } from './toolbox.js'
2
+ import { attributes, relationAttributes } from './attrs.js'
3
+
4
+ const normalize = str =>
5
+ str
6
+ .toLowerCase()
7
+ .replaceAll(/[^a-z0-9]+/g, ' ')
8
+ .trim()
9
+ .replaceAll(' ', '-')
10
+
11
+ const assertDef = def => {
12
+ const {
13
+ type,
14
+ name,
15
+ attrs,
16
+ children,
17
+ childrenAttrs,
18
+ refId,
19
+ referencePath,
20
+ ...rest
21
+ } = def
22
+ const [extra] = Object.keys(rest)
23
+ if (extra) {
24
+ throw Error(
25
+ `Unexpected property ${extra}, did you mean to define an attribute ?`,
26
+ )
27
+ }
28
+ if (!objectTypes.has(type)) throw Error(`Invalid type property`)
29
+ if (!name || typeof name !== 'string') throw Error(`Invalid name property`)
30
+ if (attrs && typeof attrs !== 'object') throw Error(`Invalid attrs property`)
31
+ if (refId && typeof refId !== 'number') throw Error(`Invalid refId property`)
32
+ if (childrenAttrs) throw Error(`childrenAttrs is no longer supported`)
33
+
34
+ for (const [key, value] of Object.entries(attrs)) {
35
+ if (relationAttributes[key]) {
36
+ throw Error(
37
+ `Attr ${key} should be defined in the relation with its parent, not on the object itself.`,
38
+ )
39
+ }
40
+
41
+ const matches = attributes[key]
42
+ if (!matches) throw Error(`Undefined attr ${key}`)
43
+ const attrDefs = matches[type]
44
+ if (!attrDefs) {
45
+ const types = Object.keys(matches).join(', ')
46
+ throw Error(`${type} does not match any of ${types} for attr ${key}`)
47
+ }
48
+
49
+ if (attrDefs.deprecated) {
50
+ throw Error(`attr ${key} deprecated: ${attrDefs.deprecated}`)
51
+ }
52
+ }
53
+ }
54
+
55
+ // TODO: maybe check for circularity here
56
+ // I skipped this check for simplicity and performance
57
+ // because since we already have check that we have a correct parent / child type
58
+ // structure, a circular relation should not be possible
59
+ const buildTree = ({ name, type, attrs, children, referencePath }, parent) => {
60
+ const object = { name, type, attrs, children: {}, referencePath, parent }
61
+ if (!children) return object
62
+ let prev
63
+ for (const [key, { ref, ...rest }] of Object.entries(children)) {
64
+ const child = buildTree(ref, object)
65
+ child.attrs = { ...child.attrs, ...rest }
66
+ prev && (prev.next = child)
67
+ child.prev = prev
68
+ object.children[key] = child
69
+ }
70
+ return object
71
+ }
72
+
73
+ const assertRelation = (parent, key, relation) => {
74
+ if (!/^[a-z0-9-]*$/.test(key)) {
75
+ throw Error(
76
+ `Invalid key for child ${key} Kebab-case key suggestion: ${normalize(key)}`,
77
+ )
78
+ }
79
+
80
+ const child = relation.ref
81
+ const { type } = child
82
+ const allowedTypes = childTypes[parent.type]
83
+ if (!allowedTypes.includes(type)) {
84
+ throw Error(`Type ${type} ${key} is not a possible child`)
85
+ }
86
+
87
+ // Should never happen
88
+ if (parent.referencePath === child.referencePath) {
89
+ throw Error(`Self reference in child ${key}`)
90
+ }
91
+
92
+ for (const [key, value] of Object.entries(relation)) {
93
+ if (key === 'ref') continue
94
+ const matches = attributes[key]
95
+ if (matches?.[type]?.restrictive) {
96
+ throw Error(
97
+ `Attr ${key} should be defined on the object itself, not in the relation with its parent.`,
98
+ )
99
+ }
100
+ const relMatches = relationAttributes[key]
101
+ if (!matches && !relMatches) throw Error(`Undefined attr ${key}`)
102
+ const attrDefs = (relMatches?.[parent.type] || matches)[type]
103
+ if (!attrDefs) {
104
+ const types = Object.keys(matches).join(', ')
105
+ throw Error(`${type} does not match any of ${types} for attr ${key}`)
106
+ }
107
+
108
+ if (attrDefs.deprecated) {
109
+ throw Error(`attr ${key} deprecated: ${attrDefs.deprecated}`)
110
+ }
111
+ }
112
+ }
113
+
114
+ const checkAttrs = object => {
115
+ for (const [key, value] of Object.entries(object.attrs)) {
116
+ try {
117
+ const attrDefs =
118
+ relationAttributes[key]?.[object.parent?.type]?.[object.type] ||
119
+ attributes[key][object.type]
120
+
121
+ if (value?.type === 'function') {
122
+ const fn = attrDefs.functionsByName?.[value.name]
123
+ if (!fn) throw Error(`function ${value.name} not found for attr ${key}`)
124
+ } else if (!attrDefs.check(value, object)) {
125
+ throw Error(`wrong type for attr ${key}`)
126
+ }
127
+ } catch (err) {
128
+ err.parentRef = object.parent?.referencePath
129
+ err.childRef = object.referencePath
130
+ err.value = value
131
+ err.key = key
132
+ throw err
133
+ }
134
+ }
135
+
136
+ for (const child of Object.values(object.children)) {
137
+ checkAttrs(child)
138
+ }
139
+ }
140
+
141
+ // TODO: replace by Set.intersection once available in node
142
+ const intersection = (a, b) => {
143
+ const result = new Set()
144
+ for (const element of a) {
145
+ b.has(element) && result.add(element)
146
+ }
147
+ return result
148
+ }
149
+
150
+ const generatePairs = arr => {
151
+ const result = []
152
+ let i = -1
153
+ // Only go up to the second to last element
154
+ while (++i < arr.length - 1) {
155
+ let j = i
156
+ // Start from the next element to avoid duplicates
157
+ while (++j < arr.length) {
158
+ result.push([arr[i], arr[j]])
159
+ }
160
+ }
161
+ return result
162
+ }
163
+
164
+ const isExam = def => def.type === 'exam'
165
+ const assertExams = definitions => {
166
+ const groups = {}
167
+ for (const exam of definitions.filter(isExam)) {
168
+ let prevExamGroup = 0
169
+ for (const [key, exercise] of Object.entries(exam.children)) {
170
+ try {
171
+ const { group } = exercise
172
+ if (!group) throw Error('missing exam group')
173
+
174
+ const groupDiff = group - prevExamGroup
175
+ prevExamGroup = group
176
+ if (groupDiff > 1) throw Error('exam must not have gaps in groups')
177
+ if (groupDiff < 0) {
178
+ throw Error('exercise must be sorted by group from lower to greater')
179
+ }
180
+
181
+ // exam exercise group should stay consistent across all exams
182
+ const prevGroup = groups[key]
183
+ if (prevGroup == null) {
184
+ groups[key] = group
185
+ } else if (prevGroup !== group) {
186
+ throw Error('Exercise used in different exam groups')
187
+ }
188
+ } catch (err) {
189
+ err.exam = exam.referencePath
190
+ err.exercise = exercise.ref.referencePath
191
+ throw err
192
+ }
193
+ }
194
+ }
195
+ }
196
+
197
+ export const checkAndBuildDefinitions = async readDef => {
198
+ const cache = {}
199
+ const getDefs = async ([key, relation]) => {
200
+ const def = cache[key] || (cache[key] = await readDef(key))
201
+ try {
202
+ assertDef(def)
203
+ } catch (err) {
204
+ err.type = def.type
205
+ err.referencePath = def.referencePath
206
+ throw err
207
+ }
208
+ relation && (relation.ref = def)
209
+ const relations = Object.entries(def.children || {}).map(getDefs)
210
+ return [def, ...(await Promise.all(relations)).flat()]
211
+ }
212
+
213
+ const definitions = await getDefs([])
214
+
215
+ // Assert all relations looks ok, possible child / parent, allowed attributes
216
+ for (const def of definitions) {
217
+ if (!def.children) continue
218
+ const allowedTypes = childTypes[def.type]
219
+ try {
220
+ for (const entry of Object.entries(def.children)) {
221
+ assertRelation(def, entry[0], entry[1])
222
+ }
223
+ } catch (err) {
224
+ err.type = def.type
225
+ err.referencePath = def.referencePath
226
+ throw err
227
+ }
228
+ }
229
+
230
+ // Check all attributes in context
231
+ const root = buildTree(definitions[0])
232
+ checkAttrs(root)
233
+ assertExams(definitions)
234
+
235
+ return { definitions, root }
236
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@01-edu/shared",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "type": "module",
5
5
  "description": "",
6
6
  "scripts": {
@@ -9,10 +9,9 @@
9
9
  "files": [
10
10
  "./bin/check-definitions.js",
11
11
  "./event-utils.js",
12
- "./event-utils.js",
13
12
  "./lodash.deburr.js",
14
13
  "./onboarding.js",
15
- "./onboarding.js",
14
+ "./definitions-checker.js",
16
15
  "./path.js",
17
16
  "./programming-languages.js",
18
17
  "./score.js",