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

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,217 @@
1
+ const { SourceFile, Buffer } = require('../file')
2
+
3
+ /**
4
+ * Inline declarations of types can come in different flavours.
5
+ * The compiler can therefore be adjusted to print out one or the other
6
+ * by plugging different implementations of this abstract class into
7
+ * their resolution mechanism.
8
+ */
9
+ class InlineDeclarationResolver {
10
+
11
+ /**
12
+ * @param {string} name
13
+ * @param {import('../compile').TypeResolveInfo} type
14
+ * @param {import('../file').Buffer} buffer
15
+ * @param {string} statementEnd
16
+ * @private
17
+ * @abstract
18
+ */
19
+ printInlineType(name, type, buffer, statementEnd) { /* abstract */ }
20
+
21
+ /**
22
+ * Attempts to resolve a type that could reference another type.
23
+ * @param {any} val
24
+ * @param {import('../compile').TypeResolveInfo} into @see Visitor.resolveType
25
+ * @param {SourceFile} file temporary file to resolve dummy types into.
26
+ * @public
27
+ */
28
+ resolveInlineDeclaration(items, into, relativeTo) {
29
+ const dummy = new SourceFile(relativeTo.path.asDirectory())
30
+ dummy.classes.currentIndet = relativeTo.classes.currentIndent
31
+ dummy.classes.add('{')
32
+ dummy.classes.indent()
33
+
34
+ into.structuredType = {}
35
+ for (const [subname, subelement] of Object.entries(items ?? {})) {
36
+ // in inline definitions, we sometimes have to resolve first
37
+ // FIXME: does this tie in with how we sometimes end up with resolved typed in resolveType()?
38
+ const se = (typeof subelement === 'string')
39
+ ? this.visitor._resolveTypeName(subelement)
40
+ : subelement
41
+ into.structuredType[subname] = this.visitor.visitElement(subname, se, dummy)
42
+ }
43
+ dummy.classes.outdent()
44
+ dummy.classes.add('}')
45
+ // FIXME: pass as param
46
+ //dummy.classes.add(element.constructor.name === 'array' ? '}[]' : '}')
47
+ into.imports = (into.imports ?? []).concat(...Object.values(dummy.imports))
48
+ into.isInlineDeclaration = true
49
+ //into.structuredType = dummy
50
+ Object.defineProperty(into, 'type', {
51
+ get: () => dummy.classes.join('\n')
52
+ })
53
+ }
54
+
55
+ /**
56
+ * Visits a single element in an entity.
57
+ * @param {string} name name of the element
58
+ * @param {import('../compile').CSN} element CSN data belonging to the the element.
59
+ * @param {SourceFile} file the namespace file the surrounding entity is being printed into.
60
+ * @param {Buffer} [buffer] buffer to add the definition to. If no buffer is passed, the passed file's class buffer is used instead.
61
+ * @public
62
+ */
63
+ visitElement(name, element, file, buffer = file.classes) {
64
+ this.depth++
65
+ for (const d of this.visitor._docify(element.doc)) {
66
+ buffer.add(d)
67
+ }
68
+ const type = this.visitor.resolveAndRequire(element, file)
69
+ this.depth--
70
+ if (this.depth === 0) {
71
+ this.printInlineType(name, type, buffer)
72
+ }
73
+ return type
74
+ }
75
+
76
+ /**
77
+ * Separator between value V and type T: `v : T`.
78
+ * Depending on the visitor's setting, this is may be `?:` for optional
79
+ * properties or `:` for required properties.
80
+ * @returns {'?'|':'}
81
+ */
82
+ getPropertyTypeSeparator() {
83
+ return this.visitor.options.propertiesOptional ? '?:' : ':'
84
+ }
85
+
86
+ /** @param {import('../compile').Visitor} visitor */
87
+ constructor(visitor) {
88
+ this.visitor = visitor
89
+ // type resolution might recurse. This indicator is used to determine
90
+ // when the type has been fully resolve (depth === 0) and should be printed
91
+ this.depth = 0
92
+ }
93
+
94
+ /**
95
+ * Produces a string representation of how to produce a [Typescript type lookup](https://www.typescriptlang.org/docs/handbook/2/indexed-access-types.html#handbook-content)
96
+ * under the current configuration.
97
+ * @example
98
+ * ```ts
99
+ * type T = {
100
+ * a: {
101
+ * b: number
102
+ * }
103
+ * }
104
+ *
105
+ * T['a']['b'] // number
106
+ * ```
107
+ * but especially with inline declarations, the access will differ between flattened and nested representations.
108
+ * @param {string[]} members a list of members, in the above example it would be `['a', 'b']`
109
+ * @returns {string} type access string snippet. In the above sample, we would return `"['a']['b']"`
110
+ * @public
111
+ * @abstract
112
+ */
113
+ getTypeLookup(members) { /* abstract */ }
114
+ }
115
+
116
+ /**
117
+ * Resolves inline declarations in a flat fashion.
118
+ * @example
119
+ * ```cds
120
+ * // cds
121
+ * entity E {
122
+ * x: { a: Integer; b: String; }
123
+ * }
124
+ * ```
125
+ * ↓
126
+ * ```ts
127
+ * // ts
128
+ * class E {
129
+ * x_a: number;
130
+ * x_b: string;
131
+ * }
132
+ * ```
133
+ */
134
+ class FlatInlineDeclarationResolver extends InlineDeclarationResolver {
135
+ constructor(visitor) { super(visitor) }
136
+
137
+ prefix(p) {
138
+ return p ? `${p}_` : ''
139
+ }
140
+
141
+ flatten(prefix, type) {
142
+ return type.typeInfo.structuredType
143
+ ? Object.entries(type.typeInfo.structuredType).map(([k,v]) => this.flatten(`${this.prefix(prefix)}${k}`, v))
144
+ : [`${prefix} ${this.getPropertyTypeSeparator()} ${type.typeName}`]
145
+ }
146
+
147
+ printInlineType(name, type, buffer) {
148
+ for(const prop of this.flatten(name, type).flat()) {
149
+ buffer.add(prop)
150
+ }
151
+ }
152
+
153
+ getTypeLookup(members) {
154
+ return `['${members.join('_')}']`
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Resolves inline declarations to a structured type.
160
+ * @example
161
+ * ```cds
162
+ * // cds
163
+ * entity E {
164
+ * x: { a: Integer; b: String; }
165
+ * }
166
+ * ```
167
+ * ↓
168
+ * ```ts
169
+ * // ts
170
+ * class E {
171
+ * x: { a: number; b: string; }
172
+ * }
173
+ * ```
174
+ */
175
+ class StructuredInlineDeclarationResolver extends InlineDeclarationResolver {
176
+ constructor(visitor) {
177
+ super(visitor)
178
+ this.printDepth = 0
179
+ }
180
+
181
+ flatten(name, type, buffer, statementEnd = ';') {
182
+ // in addition to the regular depth during resolution, we may have another depth while printing
183
+ // nested types on which the line ending depends
184
+ this.printDepth++
185
+ const lineEnding = this.printDepth > 1 ? ',' : statementEnd
186
+ if (type.typeInfo.structuredType) {
187
+ const prefix = name ? `${name} ${this.getPropertyTypeSeparator()}`: ''
188
+ buffer.add(`${prefix} {`)
189
+ buffer.indent()
190
+ for (const [n, t] of Object.entries(type.typeInfo.structuredType)) {
191
+ this.flatten(n, t, buffer)
192
+ }
193
+ buffer.outdent()
194
+ buffer.add(`}${lineEnding}`)
195
+ } else {
196
+ buffer.add(`${name} ${this.getPropertyTypeSeparator()} ${type.typeName}${lineEnding}`)
197
+ }
198
+ this.printDepth--
199
+ return buffer
200
+ }
201
+
202
+ printInlineType(name, type, buffer, statementEnd) {
203
+ // FIXME: indent not quite right
204
+ const sub = new Buffer()
205
+ sub.currentIndent = buffer.currentIndent
206
+ buffer.add(this.flatten(name, type, sub, statementEnd).join())
207
+ }
208
+
209
+ getTypeLookup(members) {
210
+ return members.map(m => `['${m}']`).join('')
211
+ }
212
+ }
213
+
214
+ module.exports = {
215
+ FlatInlineDeclarationResolver,
216
+ StructuredInlineDeclarationResolver
217
+ }
package/lib/file.d.ts ADDED
@@ -0,0 +1,208 @@
1
+ type KVs = [string, string][]
2
+ type Namespace = {[key: string]: Buffer}
3
+ type ActionParams = [string, string][]
4
+ type AsDirectoryParams = {relative: string | undefined, local: boolean, posix: boolean}
5
+
6
+ /**
7
+ * String buffer to conveniently append strings to.
8
+ */
9
+ export class Buffer {
10
+ public parts: string[];
11
+ private indentation: string;
12
+ private currentIndent: string;
13
+
14
+ constructor(indentation: string);
15
+
16
+ /**
17
+ * Indents by the predefined spacing.
18
+ */
19
+ indent(): void;
20
+
21
+ /**
22
+ * Removes one level of indentation.
23
+ */
24
+ outdent(): void;
25
+
26
+ /**
27
+ * Concats all elements in the buffer into a single string.
28
+ * @param glue string to intersperse all buffer contents with
29
+ * @returns string spilled buffer contents.
30
+ */
31
+ join(glue: string): string;
32
+
33
+ /**
34
+ * Clears the buffer.
35
+ */
36
+ clear(): void;
37
+
38
+ /**
39
+ * Adds an element to the buffer with the current indentation level.
40
+ * @param part
41
+ */
42
+ add(part: string): void;
43
+ }
44
+
45
+ /**
46
+ * Convenience class to handle path qualifiers.
47
+ */
48
+ export class Path {
49
+ private parts: string[];
50
+
51
+ /**
52
+ *
53
+ * @param parts parts of the path. 'a.b.c' -> ['a', 'b', 'c']
54
+ * @param kind FIXME: currently unused
55
+ */
56
+ constructor(parts: string[], kind: string);
57
+
58
+ /**
59
+ * @returns the path to the parent directory. 'a.b.c'.getParent() -> 'a.b'
60
+ */
61
+ getParent(): Path;
62
+
63
+ /**
64
+ * Transfoms the Path into a directory path.
65
+ * @param relative if defined, the path is constructed relative to this directory
66
+ * @param local if set to true, './' is prefixed to the directory
67
+ * @param posix if set to true, all slashes will be forward slashes on every OS. Useful for require/ import
68
+ * @returns directory 'a.b.c'.asDirectory() -> 'a/b/c' (or a\b\c when on Windows without passing posix = true)
69
+ */
70
+ asDirectory(params: AsDirectoryParams): string;
71
+
72
+ /**
73
+ * Transforms the Path into a namespace qualifier.
74
+ * @returns namespace qualifier 'a.b.c'.asNamespace() -> 'a.b.c'
75
+ */
76
+ asNamespace(): string;
77
+
78
+ /**
79
+ * Transforms the Path into an identifier that can be used as variable name.
80
+ * @returns identifier 'a.b.c'.asIdentifier() -> '_a_b_c', ''.asIdentifier() -> '_'
81
+ */
82
+ asIdentifier(): string;
83
+
84
+ /**
85
+ * @returns true, iff the Path refers to the current working directory, aka './'
86
+ */
87
+ isCwd(relative: string | undefined): boolean;
88
+ }
89
+
90
+ /**
91
+ * Source file containing several buffers.
92
+ */
93
+ export class SourceFile {
94
+ public readonly path: Path;
95
+ private imports: {}
96
+ private types: Buffer;
97
+ private classes: Buffer;
98
+ private enums: Buffer;
99
+ private actions: Buffer;
100
+ private namespaces: Namespace;
101
+ private classNames: {}
102
+ private inflections: [string, string][]
103
+
104
+ constructor(path: string);
105
+
106
+ /**
107
+ * Adds a pair of singular and plural inflection.
108
+ * These are later used to generate the singular -> plural
109
+ * aliases in the index.js file.
110
+ * @param singular singular type without namespace.
111
+ * @param plural plural type without namespace
112
+ * @param original original entity name without namespace.
113
+ * In many cases this will be the same as plural.
114
+ */
115
+ addInflection(singular: string, plural: string, original: string);
116
+
117
+ /**
118
+ * Adds an action definition in form of a arrow function to the file.
119
+ * @param name name of the action
120
+ * @param params list of parameters, passed as [name, type] pairs
121
+ * @param returns the return type of the action
122
+ */
123
+ addAction(name: string, params: ActionParams, returns: string);
124
+
125
+ /**
126
+ * Adds an enum to this file.
127
+ * @param fq fully qualified name of the enum
128
+ * @param name local name of the enum
129
+ * @param kvs list of key-value pairs
130
+ */
131
+ addEnum(fq: string, clean: string, kvs: KVs);
132
+
133
+ /**
134
+ * Adds an arbitrary piece of code that is added
135
+ * right after the imports.
136
+ * @param code the preamble code.
137
+ */
138
+ addPreamble(code: string);
139
+
140
+ /**
141
+ * Adds a type alias to this file.
142
+ * @param fq fully qualified name of the enum
143
+ * @param name local name of the enum
144
+ * @param rhs the right hand side of the assignment
145
+ */
146
+ addType(fq: string, clean: string, rhs: string);
147
+
148
+ /**
149
+ * Adds a class to this file.
150
+ * This differs from writing to the classes buffer,
151
+ * as it is just a cache to collect all classes that
152
+ * are supposed to be present in this file.
153
+ * @param clean cleaned name of the class
154
+ * @param fq fully qualified name, including the namespace
155
+ */
156
+ addClass(clean: string, fq: string): void;
157
+
158
+ /**
159
+ * Retrieves or creates and retrieves a sub namespace
160
+ * with a given name.
161
+ * @param name of the sub namespace.
162
+ * @returns the sub namespace.
163
+ */
164
+ getSubNamespace(name: string): Namespace;
165
+
166
+ /**
167
+ * Adds an import if it does not exist yet.
168
+ * @param imp qualifier for the namespace to import.
169
+ */
170
+ addImport(imp: Path): void;
171
+
172
+ /**
173
+ * Writes all imports to a buffer, relative to the current file.
174
+ * Creates a new buffer on each call, as concatenating import strings directly
175
+ * upon discovering them would complicate filtering out duplicate entries.
176
+ * @returns all imports written to a buffer.
177
+ */
178
+ getImports(): Buffer;
179
+
180
+ /**
181
+ * Creates one string from the buffers representing the type definitions.
182
+ * @returns complete file contents.
183
+ */
184
+ toTypeDefs(): string;
185
+
186
+ /**
187
+ * Concats the classnames to an export dictionary
188
+ * to create the accompanying JS file for the typings.
189
+ * @returns a string containing the module.exports for the JS file.
190
+ */
191
+ toJSExports(): string;
192
+ }
193
+
194
+ /**
195
+ * Base definitions used throughout the typing process,
196
+ * such as Associations and Compositions.
197
+ */
198
+ declare const baseDefinitions: SourceFile;
199
+
200
+ /**
201
+ * Writes the files to disk. For each source, a index.d.ts holding the type definitions
202
+ * and a index.js holding implementation stubs is generated at the appropriate directory.
203
+ * Missing directories are created automatically and asynchronously.
204
+ * @param root root directory to prefix all directories with
205
+ * @param sources source files to write to disk
206
+ * @returns Promise that resolves to a list of all directory paths pointing to generated files.
207
+ */
208
+ export function writeout(root: string, sources: SourceFile[]): Promise<string[]>;