@cap-js/cds-typer 0.2.5-beta.1 → 0.3.0

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.
@@ -1,4 +1,5 @@
1
1
  const { SourceFile, Buffer } = require('../file')
2
+ const { docify } = require('./wrappers')
2
3
 
3
4
  /**
4
5
  * Inline declarations of types can come in different flavours.
@@ -10,24 +11,24 @@ class InlineDeclarationResolver {
10
11
 
11
12
  /**
12
13
  * @param {string} name
13
- * @param {import('../compile').TypeResolveInfo} type
14
+ * @param {import('./resolver').TypeResolveInfo} type
14
15
  * @param {import('../file').Buffer} buffer
15
16
  * @param {string} statementEnd
16
- * @private
17
+ * @protected
17
18
  * @abstract
18
19
  */
19
20
  printInlineType(name, type, buffer, statementEnd) { /* abstract */ }
20
21
 
21
22
  /**
22
23
  * Attempts to resolve a type that could reference another type.
23
- * @param {any} val
24
- * @param {import('../compile').TypeResolveInfo} into @see Visitor.resolveType
24
+ * @param {any} items
25
+ * @param {import('./resolver').TypeResolveInfo} into @see Visitor.resolveType
25
26
  * @param {SourceFile} file temporary file to resolve dummy types into.
26
27
  * @public
27
28
  */
28
29
  resolveInlineDeclaration(items, into, relativeTo) {
29
30
  const dummy = new SourceFile(relativeTo.path.asDirectory())
30
- dummy.classes.currentIndet = relativeTo.classes.currentIndent
31
+ dummy.classes.currentIndent = relativeTo.classes.currentIndent
31
32
  dummy.classes.add('{')
32
33
  dummy.classes.indent()
33
34
 
@@ -36,7 +37,7 @@ class InlineDeclarationResolver {
36
37
  // in inline definitions, we sometimes have to resolve first
37
38
  // FIXME: does this tie in with how we sometimes end up with resolved typed in resolveType()?
38
39
  const se = (typeof subelement === 'string')
39
- ? this.visitor._resolveTypeName(subelement)
40
+ ? this.visitor.resolver.resolveTypeName(subelement)
40
41
  : subelement
41
42
  into.structuredType[subname] = this.visitor.visitElement(subname, se, dummy)
42
43
  }
@@ -55,17 +56,17 @@ class InlineDeclarationResolver {
55
56
  /**
56
57
  * Visits a single element in an entity.
57
58
  * @param {string} name name of the element
58
- * @param {import('../compile').CSN} element CSN data belonging to the the element.
59
+ * @param {import('./resolver').CSN} element CSN data belonging to the the element.
59
60
  * @param {SourceFile} file the namespace file the surrounding entity is being printed into.
60
61
  * @param {Buffer} [buffer] buffer to add the definition to. If no buffer is passed, the passed file's class buffer is used instead.
61
62
  * @public
62
63
  */
63
64
  visitElement(name, element, file, buffer = file.classes) {
64
65
  this.depth++
65
- for (const d of this.visitor._docify(element.doc)) {
66
+ for (const d of docify(element.doc)) {
66
67
  buffer.add(d)
67
68
  }
68
- const type = this.visitor.resolveAndRequire(element, file)
69
+ const type = this.visitor.resolver.resolveAndRequire(element, file)
69
70
  this.depth--
70
71
  if (this.depth === 0) {
71
72
  this.printInlineType(name, type, buffer)
@@ -77,13 +78,13 @@ class InlineDeclarationResolver {
77
78
  * Separator between value V and type T: `v : T`.
78
79
  * Depending on the visitor's setting, this is may be `?:` for optional
79
80
  * properties or `:` for required properties.
80
- * @returns {'?'|':'}
81
+ * @returns {'?:'|':'}
81
82
  */
82
83
  getPropertyTypeSeparator() {
83
84
  return this.visitor.options.propertiesOptional ? '?:' : ':'
84
85
  }
85
86
 
86
- /** @param {import('../compile').Visitor} visitor */
87
+ /** @param {import('../visitor').Visitor} visitor */
87
88
  constructor(visitor) {
88
89
  this.visitor = visitor
89
90
  // type resolution might recurse. This indicator is used to determine
@@ -110,7 +111,7 @@ class InlineDeclarationResolver {
110
111
  * @public
111
112
  * @abstract
112
113
  */
113
- getTypeLookup(members) { /* abstract */ }
114
+ getTypeLookup(members) { /* abstract */ return '' }
114
115
  }
115
116
 
116
117
  /**
@@ -0,0 +1,471 @@
1
+ 'use strict'
2
+
3
+ const util = require('../util')
4
+ const { Buffer, SourceFile, Path, Library, baseDefinitions } = require("../file")
5
+ const { deepRequire, createToManyAssociation, createToOneAssociation, createArrayOf, createCompositionOfMany, createCompositionOfOne } = require('./wrappers')
6
+ const { Visitor } = require("../visitor")
7
+ const { StructuredInlineDeclarationResolver } = require("./inline")
8
+
9
+ /** @typedef {{ cardinality?: { max?: '*' | number } }} EntityCSN */
10
+ /** @typedef {{ definitions?: Object<string, EntityCSN> }} CSN */
11
+
12
+ /**
13
+ * When nested inline types require additional imports. E.g.:
14
+ * ```cds
15
+ * // mymodel.cds
16
+ * Foo {
17
+ * bar: {
18
+ * baz: a.b.c.Baz // need to require a.b.c in mymodel.cds!
19
+ * }
20
+ * }
21
+ * ```
22
+ * @typedef {{
23
+ * isBuiltin: boolean,
24
+ * isInlineDeclaration: boolean,
25
+ * isForeignKeyReference: boolean,
26
+ * isArray: boolean,
27
+ * type: string,
28
+ * path?: Path,
29
+ * csn?: CSN,
30
+ * imports: Path[]
31
+ * inner: TypeResolveInfo
32
+ * }} TypeResolveInfo
33
+ */
34
+
35
+ /**
36
+ * Builtin types defined by CDS.
37
+ */
38
+ const Builtins = {
39
+ UUID: 'string',
40
+ String: 'string',
41
+ Binary: 'string',
42
+ LargeString: 'string',
43
+ LargeBinary: 'string',
44
+ Integer: 'number',
45
+ UInt8: 'number',
46
+ Int16: 'number',
47
+ Int32: 'number',
48
+ Int64: 'number',
49
+ Integer64: 'number',
50
+ Decimal: 'number',
51
+ DecimalFloat: 'number',
52
+ Float: 'number',
53
+ Double: 'number',
54
+ Boolean: 'boolean',
55
+ Date: 'Date',
56
+ DateTime: 'Date',
57
+ Time: 'Date',
58
+ Timestamp: 'Date',
59
+ //
60
+ Composition: 'Array',
61
+ Association: 'Array'
62
+ }
63
+
64
+ class Resolver {
65
+ get csn() { return this.visitor.csn }
66
+
67
+ /** @param {Visitor} visitor */
68
+ constructor(visitor) {
69
+ /** @type {Visitor} */
70
+ this.visitor = visitor
71
+
72
+ /** @type {Library[]} */
73
+ this.libraries = [new Library(require.resolve('../../library/cds.hana.ts'))]
74
+ }
75
+
76
+ /**
77
+ * Returns all libraries that have been referenced at least once.
78
+ * @returns {Library[]}
79
+ */
80
+ getUsedLibraries() {
81
+ return this.libraries.filter(l => l.referenced)
82
+ }
83
+
84
+ /**
85
+ * Conveniently combines resolveNamespace and trimNamespace
86
+ * to end up with both the resolved Path of the namespace,
87
+ * and the clean name of the class.
88
+ * @param {string} fq the fully qualified name of an entity.
89
+ * @returns {[Path, string]} a tuple, [0] holding the path to the namespace, [1] holding the clean name of the entity.
90
+ */
91
+ untangle(fq) {
92
+ const ns = this.resolveNamespace(fq.split('.'))
93
+ const name = this.trimNamespace(fq)
94
+ return [new Path(ns.split('.')), name]
95
+ }
96
+
97
+ /**
98
+ * Convenience method to shave off the namespace of a fully qualified path.
99
+ * More specifically, only the parts (reading from right to left) that are of
100
+ * kind "entity" are retained.
101
+ * a.b.c.Foo -> Foo
102
+ * Bar -> Bar
103
+ * sap.cap.Book.text -> Book.text (assuming Book and text are both of kind "entity")
104
+ * @param {string} p path
105
+ * @returns {string} the entity name without leading namespace.
106
+ */
107
+ trimNamespace(p) {
108
+ // start on right side, go up while we have an entity at hand
109
+ // we cant start on left side, as that clashes with undefined entities like "sap"
110
+ const parts = p.split('.')
111
+ if (parts.length <= 1) {
112
+ return p
113
+ }
114
+
115
+ let qualifier = parts.join('.')
116
+ while (
117
+ this.csn.definitions[qualifier] &&
118
+ ['entity', 'type', 'aspect'].includes(this.csn.definitions[qualifier].kind)
119
+ ) {
120
+ parts.pop()
121
+ qualifier = parts.join('.')
122
+ }
123
+
124
+ return qualifier ? p.substring(qualifier.length + 1) : p
125
+ }
126
+
127
+ /**
128
+ * Generates singular and plural inflection for the passed type.
129
+ * Several cases are covered here:
130
+ * - explicit annotation by the user in the CSN
131
+ * - implicitly derived inflection based on simple grammar rules
132
+ * - collisions between singular and plural name (resolved by appending a '_' suffix)
133
+ * - inline type definitions, which don't really have a linguistic plural,
134
+ * but need to expressed as array type to be consumable by the likes of Composition.of.many<T>
135
+ * @param {import('./resolver').TypeResolveInfo} typeInfo information about the type gathered so far.
136
+ * @param {string} [namespace] namespace the type occurs in. If passed, will be shaved off from the name
137
+ * @returns {Inflection}
138
+ */
139
+ inflect(typeInfo, namespace) {
140
+ let typeName
141
+ let singular
142
+ let plural
143
+
144
+ if (typeInfo.isInlineDeclaration) {
145
+ // if we detected an inline declaration, we take a quick detour via an InlineDeclarationResolver
146
+ // to rectify the typeName (which would be just '{' elsewise).
147
+ // The correct typename in string form is required in stringifyLambda(...)
148
+ // Note that whenever the typeName is relevant, it is assumed to be in structured form
149
+ // (i.e. a struct), so we always use a StructuredInlineDeclarationResolver here, regardless of
150
+ // what is configured for nested declarations in the visitor.
151
+ // FIXME: in most other places where we have an inline declaration, we actually don't need the typeName.
152
+ // If stringifyLambda(...) is the only place where we need this, we should have stringifyLambda call this
153
+ // piece of code instead to reduce overhead.
154
+ const into = new Buffer()
155
+ new StructuredInlineDeclarationResolver(this.visitor).printInlineType(undefined, { typeInfo }, into, '')
156
+ typeName = into.join(' ')
157
+ singular = typeName
158
+ plural = createArrayOf(typeName) //`Array<${typeName}>`
159
+ } else {
160
+ // TODO: make sure the resolution still works. Currently, we only cut off the namespace!
161
+ singular = util.singular4(typeInfo.csn)
162
+ plural = util.getPluralAnnotation(typeInfo.csn) ? util.plural4(typeInfo.csn) : typeInfo.plainName
163
+
164
+ // don't slice off namespace if it isn't part of the inflected name.
165
+ // This happens when the user adds an annotation and singular4 therefore
166
+ // already returns an identifier without namespace
167
+ if (namespace && singular.startsWith(namespace)) {
168
+ // TODO: not totally sure why plural doesn't have to be sliced
169
+ singular = singular.slice(namespace.length + 1)
170
+ }
171
+
172
+ if (singular === plural) {
173
+ // same as when creating the entity
174
+ plural += '_'
175
+ }
176
+ }
177
+ if (!singular || !plural) {
178
+ this.logger.error(`Singular ('${singular}') or plural ('${plural}') for '${typeName}' is empty.`)
179
+ }
180
+
181
+ return { typeName, singular, plural }
182
+ }
183
+
184
+ /**
185
+ * Convenient API to consume resolveType.
186
+ * Internally calls resolveType, determines how it has to be imported,
187
+ * used, etc. relative to file and just returns the name under
188
+ * which it will finally be known within file.
189
+ *
190
+ * For example:
191
+ * model1.cds contains entity Foo
192
+ * model2.cds references Foo
193
+ *
194
+ * calling resolveAndRequire({... Foo}, model2.d.ts) would then:
195
+ * 1. add an import of model1 to model2 with proper path resolution and alias, e.g. "import * as m1 from './model1'"
196
+ * 2. resolve any singular/ plural issues and association/ composition around it
197
+ * 3. return a properly prefixed name to use within model2.d.ts, e.g. "m1.Foo"
198
+ *
199
+ * @param {CSN} element the CSN element to resolve the type for.
200
+ * @param {SourceFile} file source file for context.
201
+ * @returns {{typeName: string, typeInfo: TypeResolveInfo & { inflection: Inflection } }} info about the resolved type
202
+ */
203
+ resolveAndRequire(element, file) {
204
+ const typeInfo = this.resolveType(element, file)
205
+ const cardinality = this.getMaxCardinality(element)
206
+
207
+ let typeName = typeInfo.plainName ?? typeInfo.type
208
+
209
+ // only applies to builtin types, because the association/ composition _themselves_ are the (builtin) types we are checking, not their generic parameter!
210
+ if (typeInfo.isBuiltin === true) {
211
+ const [toOne, toMany] =
212
+ {
213
+ Association: [createToOneAssociation, createToManyAssociation],
214
+ Composition: [createCompositionOfOne, createCompositionOfMany],
215
+ }[element.constructor.name] ?? []
216
+
217
+ if (toOne && toMany) {
218
+ const target = typeof element.target === 'string' ? { type: element.target } : element.target
219
+ const { singular, plural } = this.resolveAndRequire(target, file).typeInfo.inflection
220
+ typeName =
221
+ cardinality > 1 ? toMany(plural) : toOne(this.visitor.isSelfReference(element.target) ? 'this' : singular)
222
+ file.addImport(baseDefinitions.path)
223
+ }
224
+ } else {
225
+ // TODO: this could go into resolve type
226
+ // resolve and maybe generate an import.
227
+ // Inline declarations don't have a corresponding path, etc., so skip those.
228
+ if (typeInfo.isInlineDeclaration === false) {
229
+ const namespace = this.resolveNamespace(typeInfo.path.parts)
230
+ const parent = new Path(namespace.split('.')) //t.path.getParent()
231
+ typeInfo.inflection = this.inflect(typeInfo, namespace)
232
+
233
+ if (!parent.isCwd(file.path.asDirectory())) {
234
+ file.addImport(parent)
235
+ // prepend namespace
236
+ typeName = `${parent.asIdentifier()}.${typeName}`
237
+ typeInfo.inflection.singular = `${parent.asIdentifier()}.${typeInfo.inflection.singular}`
238
+ typeInfo.inflection.plural = `${parent.asIdentifier()}.${typeInfo.inflection.plural}`
239
+ }
240
+
241
+ if (element.type.ref?.length > 1) {
242
+ const [entity, ...members] = element.type.ref
243
+ const lookup = this.visitor.inlineDeclarationResolver.getTypeLookup(members)
244
+ typeName = deepRequire(typeInfo.inflection.singular, lookup)
245
+ file.addImport(baseDefinitions.path)
246
+ }
247
+ }
248
+ // FIXME NOW: inline declarations, aka structs go here!
249
+
250
+ for (const imp of typeInfo.imports ?? []) {
251
+ if (!imp.isCwd(file.path.asDirectory())) {
252
+ file.addImport(imp)
253
+ }
254
+ }
255
+ }
256
+
257
+ if (typeInfo.isInlineDeclaration === true) {
258
+ typeInfo.inflection = this.inflect(typeInfo)
259
+ }
260
+
261
+ if (typeInfo.isArray === true) {
262
+ typeName = createArrayOf(typeName)
263
+ }
264
+
265
+ // FIXME: typeName could probably just become part of typeInfo
266
+ return { typeName, typeInfo }
267
+ }
268
+
269
+ /**
270
+ * Attempts to retrieve the max cardinality of a CSN for an entity.
271
+ * @param {EntityCSN} element csn of entity to retrieve cardinality for
272
+ * @returns {number} max cardinality of the element.
273
+ * If no cardinality is attached to the element, cardinality is 1.
274
+ * If it is set to '*', result is Infinity.
275
+ */
276
+ getMaxCardinality(element) {
277
+ const cardinality = element?.cardinality?.max ?? 1
278
+ return cardinality === '*' ? Infinity : parseInt(cardinality)
279
+ }
280
+
281
+ /**
282
+ * Resolves the fully qualified name of an entity to its parent entity.
283
+ * resolveParent(a.b.c.D) -> CSN {a.b.c}
284
+ * @param {string} name fully qualified name of the entity to resolve the parent of.
285
+ * @returns {CSN} the resolved parent CSN.
286
+ */
287
+ resolveParent(name) {
288
+ return this.csn.definitions[name.split('.').slice(0, -1).join('.')]
289
+ }
290
+
291
+ /**
292
+ * Resolves a fully qualified identifier to a namespace.
293
+ * In an identifier 'a.b.c.D.E', the namespace is the part of the identifier
294
+ * read from left to right which does not contain a kind 'context' or 'service'.
295
+ * That is, if in the above example 'D' is a context and 'E' is a service,
296
+ * the resulting namespace is 'a.b.c'.
297
+ * @param {string[]} pathParts the distinct parts of the namespace, i.e. ['a','b','c','D','E']
298
+ * @returns {string} the namespace's name, i.e. 'a.b.c'.
299
+ */
300
+ resolveNamespace(pathParts) {
301
+ let result
302
+ while (result === undefined) {
303
+ const path = pathParts.join('.')
304
+ const def = this.csn.definitions[path]
305
+ if (!def) {
306
+ result = path
307
+ } else if (['context', 'service'].includes(def.kind)) {
308
+ result = path
309
+ } else {
310
+ pathParts = pathParts.slice(0, -1)
311
+ }
312
+ }
313
+ return result
314
+ }
315
+
316
+ /**
317
+ * Resolves an element's type to either a builtin or a user defined type.
318
+ * Enriched with additional information for improved printout (see return type).
319
+ * @param {CSN} element the CSN element to resolve the type for.
320
+ * @param {SourceFile} file source file for context.
321
+ * @returns {TypeResolveInfo} description of the resolved type
322
+ */
323
+ resolveType(element, file) {
324
+ // while resolving inline declarations, it can happen that we land here
325
+ // with an already resolved type. In that case, just return the type we have.
326
+ if (element && Object.hasOwn(element, 'isBuiltin')) {
327
+ return element
328
+ }
329
+
330
+ const result = {
331
+ isBuiltin: false, // will be rectified in the corresponding handlers, if needed
332
+ isInlineDeclaration: false,
333
+ isForeignKeyReference: false,
334
+ isArray: false,
335
+ }
336
+
337
+ // FIXME: switch case
338
+ if (element?.type === undefined) {
339
+ // "fallback" type "empty object". May be overriden via #resolveInlineDeclarationType
340
+ // later on with an inline declaration
341
+ result.type = '{}'
342
+ result.isInlineDeclaration = true
343
+ } else {
344
+ this.resolvePotentialReferenceType(element.type, result, file)
345
+ }
346
+
347
+ // objects and arrays
348
+ if (element?.items) {
349
+ // FIXME: builtin = true? arrays are kinda builtin
350
+ result.isArray = true
351
+ this.resolveType(element.items, file)
352
+ //delete element.items
353
+ } else if (element?.elements && !element?.type) {
354
+ // explicitly skip named type definitions, which have elements too, but should not be considered inline declarations
355
+ this.#resolveInlineDeclarationType(element.elements, result, file)
356
+ }
357
+
358
+ if (result.isBuiltin === false && result.isInlineDeclaration === false && !result.plainName) {
359
+ this.logger.warning(
360
+ `Plain name is empty for ${element?.type ?? '<empty>'}. This will probably cause issues.`
361
+ )
362
+ }
363
+ return result
364
+ }
365
+
366
+ /**
367
+ * Resolves an inline declaration of a type.
368
+ * We can encounter declarations like:
369
+ *
370
+ * record : array of {
371
+ * column : String;
372
+ * data : String;
373
+ * }
374
+ *
375
+ * These have to be resolved to a new type.
376
+ *
377
+ * @param {any[]} items the properties of the inline declaration.
378
+ * @param {TypeResolveInfo} into @see resolveType()
379
+ * @param {SourceFile} relativeTo the sourcefile in which we have found the reference to the type.
380
+ * This is important to correctly detect when a field in the inline declaration is referencing
381
+ * types from the CWD. In that case, we will not add an import for that type and not add a namespace-prefix.
382
+ */
383
+ #resolveInlineDeclarationType(items, into, relativeTo) {
384
+ return this.visitor.inlineDeclarationResolver.resolveInlineDeclaration(items, into, relativeTo)
385
+ }
386
+
387
+ /**
388
+ * Attempts to resolve a type that could reference another type.
389
+ * @param {?} val
390
+ * @param {TypeResolveInfo} into see resolveType()
391
+ * @param {SourceFile} file only needed as we may call #resolveInlineDeclarationType from here. Will be expelled at some point.
392
+ */
393
+ resolvePotentialReferenceType(val, into, file) {
394
+ // FIXME: get rid of file parameter! it is only used to pass to #resolveInlineDeclarationType
395
+ if (val.elements) {
396
+ this.#resolveInlineDeclarationType(val, into, file) // FIXME INDENT!
397
+ } else if (val.constructor === Object && 'ref' in val) {
398
+ this.#resolveTypeName(val.ref[0], into)
399
+ into.isForeignKeyReference = true
400
+ } else {
401
+ // val is string
402
+ this.#resolveTypeName(val, into)
403
+ }
404
+ }
405
+
406
+ /**
407
+ * Attempts to resolve a string to a type.
408
+ * String is supposed to refer to either a builtin type
409
+ * or any type defined in CSN.
410
+ * @param {string} t fully qualified type, like cds.String, or a.b.c.d.Foo
411
+ * @param {TypeResolveInfo} into optional dictionary to fill by reference, see resolveType()
412
+ * @returns @see resolveType
413
+ */
414
+ #resolveTypeName(t, into) {
415
+ const result = into || {}
416
+ const path = t.split('.')
417
+ if (path.length === 2 && path[0] === 'cds') {
418
+ // builtin type
419
+ const resolvedBuiltin = Builtins[path[1]]
420
+ if (!resolvedBuiltin) {
421
+ throw new Error(`Can not resolve apparent builtin type '${t}' to any CDS type.`)
422
+ }
423
+ result.type = resolvedBuiltin
424
+ result.isBuiltin = true
425
+ } else if (t in this.csn.definitions) {
426
+ // user-defined type
427
+ result.type = this.trimNamespace(util.singular4(this.csn.definitions[t])) //(path[path.length - 1])
428
+ result.isBuiltin = false
429
+ result.path = new Path(path) // FIXME: relative to current file
430
+ result.csn = this.csn.definitions[t]
431
+ result.plainName = this.trimNamespace(t)
432
+ } else {
433
+ // type offered by some library
434
+ const lib = this.libraries.find((lib) => lib.offers(t))
435
+ if (lib) {
436
+ // only use the last name of the (fully qualified) type name in this case.
437
+ // We can not use trimNamespace, as that actually does a semantic lookup within the CSN.
438
+ // But entities that are found in libraries are not part of that CSN and have therefore be
439
+ // separated from their namespace in a more barbarian way.
440
+ // Luckily, this is not an issue, as libraries are supposed to be flat. So we can assume the
441
+ // last portion of the type to refer to the entity.
442
+ // We use this plain name as type name because consider:
443
+ //
444
+ // ```cds
445
+ // entity Book { title: hana.VARCHAR }
446
+ // ```
447
+ //
448
+ // ```ts
449
+ // import * as _cds_hana from '../../cds/hana'
450
+ // class Book { title: _cds_hana.cds.hana.VARCHAR } // <- how it would be without discarding the namespace
451
+ // class Book { title: _cds_hana.VARCHAR } // <- how we want it to look
452
+ // ```
453
+ const plain = t.split('.').at(-1)
454
+ lib.referenced = true
455
+ result.type = plain
456
+ result.isBuiltin = false
457
+ result.path = lib.path
458
+ result.csn = { name: t }
459
+ result.plainName = plain
460
+ } else {
461
+ throw new Error(`Can not resolve '${t}' to any builtin, library-, or user defined type.`)
462
+ }
463
+ }
464
+
465
+ return result
466
+ }
467
+ }
468
+
469
+ module.exports = {
470
+ Resolver
471
+ }
@@ -0,0 +1,66 @@
1
+ 'use strict'
2
+
3
+ const base = require('../file').baseDefinitions.path.asIdentifier()
4
+
5
+ /**
6
+ * Wraps type into association to scalar.
7
+ * @param {string} t the singular type name.
8
+ * @returns {string}
9
+ */
10
+ const createToOneAssociation = t => `${base}.Association.to<${t}>`
11
+
12
+ /**
13
+ * Wraps type into association to vector.
14
+ * @param {string} t the singular type name.
15
+ * @returns {string}
16
+ */
17
+ const createToManyAssociation = t => `${base}.Association.to.many<${t}>`
18
+
19
+ /**
20
+ * Wraps type into composition of scalar.
21
+ * @param {string} t the singular type name.
22
+ * @returns {string}
23
+ */
24
+ const createCompositionOfOne = t => `${base}.Composition.of<${t}>`
25
+
26
+ /**
27
+ * Wraps type into composition of vector.
28
+ * @param {string} t the singular type name.
29
+ * @returns {string}
30
+ */
31
+ const createCompositionOfMany = t => `${base}.Composition.of.many<${t}>`
32
+
33
+ /**
34
+ * Wraps type into an array.
35
+ * @param {string} t the singular type name.
36
+ * @returns {string}
37
+ */
38
+ const createArrayOf = t => `Array<${t}>`
39
+
40
+ /**
41
+ * Wraps type into a deep require (removes all posibilities of undefined recursively).
42
+ * @param {string} t the singular type name.
43
+ * @param {string?} lookup a property lookup of the required type (`['Foo']`)
44
+ * @returns {string}
45
+ */
46
+ const deepRequire = (t, lookup = '') => `${base}.DeepRequired<${t}>${lookup}`
47
+
48
+ /**
49
+ * Puts a passed string in docstring format.
50
+ * @param {string} doc raw string to docify. May contain linebreaks.
51
+ * @returns {string[]} an array of lines wrapped in doc format. The result is not
52
+ * concatenated to be properly indented by `buffer.add(...)`.
53
+ */
54
+ const docify = doc => doc
55
+ ? ['/**'].concat(doc.split('\n').map((line) => `* ${line}`)).concat(['*/'])
56
+ : []
57
+
58
+ module.exports = {
59
+ createArrayOf,
60
+ createToOneAssociation,
61
+ createToManyAssociation,
62
+ createCompositionOfOne,
63
+ createCompositionOfMany,
64
+ deepRequire,
65
+ docify
66
+ }
package/lib/file.js CHANGED
@@ -136,6 +136,19 @@ class SourceFile extends File {
136
136
  this.inflections.push([singular, plural, original])
137
137
  }
138
138
 
139
+ /**
140
+ * Adds a function definition in form of a arrow function to the file.
141
+ * @param {string} name name of the function
142
+ * @param {{relative: string | undefined, local: boolean, posix: boolean}} params list of parameters, passed as [name, type] pairs
143
+ * @param returns the return type of the function
144
+ */
145
+ addFunction(name, params, returns) {
146
+ // FIXME: use different buffers for buffers and actions, or at least rename buffer to the more general category "functions"?
147
+ this.actions.buffer.add("// function")
148
+ this.actions.buffer.add(`export declare const ${SourceFile.stringifyLambda(name, params, returns)};`)
149
+ this.actions.names.push(name)
150
+ }
151
+
139
152
  /**
140
153
  * Adds an action definition in form of a arrow function to the file.
141
154
  * @param {string} name name of the action
@@ -428,6 +441,7 @@ class Path {
428
441
  * @type {SourceFile}
429
442
  */
430
443
  const baseDefinitions = new SourceFile('_')
444
+ // FIXME: this should be a library someday
431
445
  baseDefinitions.addPreamble(`
432
446
  export namespace Association {
433
447
  export type to <T> = T;
package/lib/logging.js CHANGED
@@ -16,15 +16,16 @@ class Logger {
16
16
  for (let i = 0; i < lvls.length - 1; i++) {
17
17
  // -1 to ignore NONE
18
18
  const level = lvls[i]
19
- this[level.toLowerCase()] = function (message) {
20
- this._log(level, message)
21
- }.bind(this)
19
+ this[level.toLowerCase()] = function (message) { this._log(level, message) }.bind(this)
22
20
  }
23
21
  }
24
22
 
23
+ // only temporarily to disable those warnings...
24
+ //warning(s) {}; error(s) {}; info(s) {}; debug(s) {};
25
+
25
26
  /**
26
27
  * Add all log levels starting at level.
27
- * @param {number} level level to start from.
28
+ * @param {number} baseLevel level to start from.
28
29
  */
29
30
  addFrom(baseLevel) {
30
31
  const vals = Object.values(Levels)