@sap/cds-compiler 3.4.2 → 3.5.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.
Files changed (143) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/README.md +1 -0
  3. package/bin/cds_update_identifiers.js +5 -5
  4. package/bin/cdsc.js +15 -16
  5. package/bin/cdshi.js +19 -6
  6. package/doc/CHANGELOG_ARCHIVE.md +2 -2
  7. package/doc/CHANGELOG_BETA.md +9 -1
  8. package/doc/CHANGELOG_DEPRECATED.md +2 -0
  9. package/lib/api/main.js +61 -59
  10. package/lib/api/options.js +4 -2
  11. package/lib/api/validate.js +2 -2
  12. package/lib/base/cleanSymbols.js +2 -3
  13. package/lib/base/dictionaries.js +6 -6
  14. package/lib/base/error.js +2 -2
  15. package/lib/base/keywords.js +6 -6
  16. package/lib/base/location.js +11 -12
  17. package/lib/base/message-registry.js +177 -58
  18. package/lib/base/messages.js +252 -180
  19. package/lib/base/model.js +14 -11
  20. package/lib/base/node-helpers.js +9 -10
  21. package/lib/base/optionProcessorHelper.js +138 -129
  22. package/lib/checks/.eslintrc.json +2 -0
  23. package/lib/checks/actionsFunctions.js +5 -5
  24. package/lib/checks/annotationsOData.js +4 -4
  25. package/lib/checks/arrayOfs.js +1 -1
  26. package/lib/checks/cdsPersistence.js +1 -1
  27. package/lib/checks/checkForTypes.js +3 -3
  28. package/lib/checks/defaultValues.js +3 -3
  29. package/lib/checks/elements.js +7 -7
  30. package/lib/checks/emptyOrOnlyVirtual.js +2 -2
  31. package/lib/checks/foreignKeys.js +1 -1
  32. package/lib/checks/invalidTarget.js +4 -4
  33. package/lib/checks/managedInType.js +1 -1
  34. package/lib/checks/managedWithoutKeys.js +1 -1
  35. package/lib/checks/nonexpandableStructured.js +5 -3
  36. package/lib/checks/nullableKeys.js +1 -1
  37. package/lib/checks/onConditions.js +5 -6
  38. package/lib/checks/parameters.js +1 -1
  39. package/lib/checks/queryNoDbArtifacts.js +2 -2
  40. package/lib/checks/selectItems.js +4 -4
  41. package/lib/checks/sql-snippets.js +4 -4
  42. package/lib/checks/types.js +7 -7
  43. package/lib/checks/utils.js +4 -4
  44. package/lib/checks/validator.js +16 -13
  45. package/lib/compiler/.eslintrc.json +4 -1
  46. package/lib/compiler/assert-consistency.js +8 -7
  47. package/lib/compiler/builtins.js +14 -14
  48. package/lib/compiler/checks.js +123 -48
  49. package/lib/compiler/define.js +12 -13
  50. package/lib/compiler/extend.js +266 -60
  51. package/lib/compiler/finalize-parse-cdl.js +10 -5
  52. package/lib/compiler/index.js +17 -14
  53. package/lib/compiler/populate.js +14 -6
  54. package/lib/compiler/propagator.js +2 -0
  55. package/lib/compiler/resolve.js +2 -15
  56. package/lib/compiler/shared.js +27 -16
  57. package/lib/compiler/tweak-assocs.js +5 -6
  58. package/lib/compiler/utils.js +20 -0
  59. package/lib/edm/annotations/genericTranslation.js +604 -358
  60. package/lib/edm/annotations/preprocessAnnotations.js +39 -35
  61. package/lib/edm/csn2edm.js +275 -222
  62. package/lib/edm/edm.js +17 -3
  63. package/lib/edm/edmAnnoPreprocessor.js +6 -6
  64. package/lib/edm/edmInboundChecks.js +2 -2
  65. package/lib/edm/edmPreprocessor.js +107 -77
  66. package/lib/edm/edmUtils.js +44 -5
  67. package/lib/gen/Dictionary.json +210 -8
  68. package/lib/gen/language.checksum +1 -1
  69. package/lib/gen/language.interp +67 -63
  70. package/lib/gen/language.tokens +81 -81
  71. package/lib/gen/languageLexer.interp +4 -10
  72. package/lib/gen/languageLexer.js +854 -869
  73. package/lib/gen/languageLexer.tokens +79 -81
  74. package/lib/gen/languageParser.js +14309 -13832
  75. package/lib/inspect/inspectModelStatistics.js +2 -2
  76. package/lib/inspect/inspectPropagation.js +6 -6
  77. package/lib/inspect/inspectUtils.js +2 -2
  78. package/lib/json/from-csn.js +102 -55
  79. package/lib/json/to-csn.js +119 -198
  80. package/lib/language/antlrParser.js +5 -2
  81. package/lib/language/docCommentParser.js +6 -6
  82. package/lib/language/errorStrategy.js +43 -23
  83. package/lib/language/genericAntlrParser.js +113 -133
  84. package/lib/language/language.g4 +1550 -1506
  85. package/lib/language/multiLineStringParser.js +3 -3
  86. package/lib/language/textUtils.js +2 -2
  87. package/lib/main.js +3 -3
  88. package/lib/model/csnRefs.js +5 -0
  89. package/lib/model/csnUtils.js +130 -122
  90. package/lib/model/revealInternalProperties.js +1 -1
  91. package/lib/model/sortViews.js +4 -6
  92. package/lib/modelCompare/compare.js +2 -2
  93. package/lib/modelCompare/utils/.eslintrc.json +22 -0
  94. package/lib/modelCompare/utils/filter.js +100 -0
  95. package/lib/optionProcessor.js +5 -0
  96. package/lib/render/.eslintrc.json +1 -0
  97. package/lib/render/DuplicateChecker.js +1 -1
  98. package/lib/render/manageConstraints.js +12 -12
  99. package/lib/render/toCdl.js +311 -276
  100. package/lib/render/toHdbcds.js +97 -94
  101. package/lib/render/toRename.js +5 -5
  102. package/lib/render/toSql.js +127 -223
  103. package/lib/render/utils/common.js +141 -108
  104. package/lib/render/utils/delta.js +227 -0
  105. package/lib/render/utils/sql.js +22 -6
  106. package/lib/render/utils/stringEscapes.js +3 -3
  107. package/lib/transform/db/.eslintrc.json +2 -0
  108. package/lib/transform/db/applyTransformations.js +3 -3
  109. package/lib/transform/db/assertUnique.js +13 -12
  110. package/lib/transform/db/associations.js +5 -5
  111. package/lib/transform/db/cdsPersistence.js +10 -8
  112. package/lib/transform/db/constraints.js +14 -14
  113. package/lib/transform/db/expansion.js +20 -22
  114. package/lib/transform/db/flattening.js +24 -42
  115. package/lib/transform/db/groupByOrderBy.js +3 -3
  116. package/lib/transform/db/temporal.js +6 -6
  117. package/lib/transform/db/transformExists.js +23 -23
  118. package/lib/transform/db/views.js +16 -16
  119. package/lib/transform/draft/.eslintrc.json +1 -35
  120. package/lib/transform/draft/db.js +10 -10
  121. package/lib/transform/draft/odata.js +2 -2
  122. package/lib/transform/forOdataNew.js +8 -29
  123. package/lib/transform/forRelationalDB.js +16 -6
  124. package/lib/transform/localized.js +11 -10
  125. package/lib/transform/odata/toFinalBaseType.js +41 -27
  126. package/lib/transform/odata/typesExposure.js +113 -47
  127. package/lib/transform/parseExpr.js +209 -106
  128. package/lib/transform/transformUtilsNew.js +17 -10
  129. package/lib/transform/translateAssocsToJoins.js +24 -19
  130. package/lib/transform/universalCsn/coreComputed.js +10 -10
  131. package/lib/transform/universalCsn/universalCsnEnricher.js +26 -26
  132. package/lib/transform/universalCsn/utils.js +3 -3
  133. package/lib/utils/file.js +5 -5
  134. package/lib/utils/moduleResolve.js +13 -13
  135. package/lib/utils/objectUtils.js +6 -6
  136. package/lib/utils/term.js +5 -2
  137. package/lib/utils/timetrace.js +51 -24
  138. package/package.json +5 -8
  139. package/share/messages/check-proper-type-of.md +1 -1
  140. package/share/messages/message-explanations.json +1 -1
  141. package/share/messages/redirected-to-complex.md +4 -4
  142. package/share/messages/{syntax-expecting-integer.md → syntax-expecting-unsigned-int.md} +7 -4
  143. package/lib/modelCompare/filter.js +0 -83
@@ -1,10 +1,11 @@
1
1
  'use strict';
2
2
  const { isEdmPropertyRendered, isBuiltinType } = require('../../model/csnUtils');
3
3
  const edmUtils = require('../edmUtils.js');
4
- const preprocessAnnotations = require('./preprocessAnnotations.js');
5
4
  const oDataDictionary = require('../../gen/Dictionary.json');
5
+ const preprocessAnnotations = require('./preprocessAnnotations.js');
6
6
  const { forEachDefinition } = require('../../model/csnUtils');
7
- const { forEach } = require("../../utils/objectUtils");
7
+ const { forEach } = require('../../utils/objectUtils');
8
+ const { isBetaEnabled } = require('../../base/model.js');
8
9
 
9
10
  /*
10
11
  OASIS: https://github.com/oasis-tcs/odata-vocabularies/tree/master/vocabularies
@@ -15,19 +16,21 @@ const { forEach } = require("../../utils/objectUtils");
15
16
  JSON (published)
16
17
  Measures (published)
17
18
  Repeatability (published)
18
- Temporal (not published, not yet finalized)
19
+ Temporal (published)
19
20
  Validation (published)
20
21
 
21
22
  SAP: https://github.com/SAP/odata-vocabularies/tree/master/vocabularies
22
23
  Analytics (published)
23
24
  CodeList (published)
24
- Common (pubished)
25
+ Common (published)
25
26
  Communication (published)
26
27
  DataIntegration (published)
27
28
  Graph (published, experimental)
28
29
  Hierarchy (published, experimental)
29
30
  HTML5 (published, experimental)
30
31
  ODM (published, experimental)
32
+ Offline (experimental)
33
+ PDF (published)
31
34
  PersonalData (published)
32
35
  Session (published)
33
36
  UI (published)
@@ -109,6 +112,16 @@ const vocabularyDefinitions = {
109
112
  'inc': { Alias: 'ODM', Namespace: 'com.sap.vocabularies.ODM.v1' },
110
113
  'int': { filename: 'ODM.xml' }
111
114
  },
115
+ 'Offline': {
116
+ 'ref': { Uri: 'https://sap.github.io/odata-vocabularies/vocabularies/Offline.xml' },
117
+ 'inc': { Alias: 'Offline', Namespace: 'com.sap.vocabularies.Offline.v1' },
118
+ 'int': { filename: 'Offline.xml' }
119
+ },
120
+ 'PDF': {
121
+ 'ref': { Uri: 'https://sap.github.io/odata-vocabularies/vocabularies/PDF.xml' },
122
+ 'inc': { Alias: 'PDF', Namespace: 'com.sap.vocabularies.PDF.v1' },
123
+ 'int': { filename: 'PDF.xml' }
124
+ },
112
125
  'PersonalData': {
113
126
  'ref': { Uri: 'https://sap.github.io/odata-vocabularies/vocabularies/PersonalData.xml' },
114
127
  'inc': { Alias: 'PersonalData', Namespace: 'com.sap.vocabularies.PersonalData.v1' },
@@ -138,7 +151,6 @@ const vocabularyDefinitions = {
138
151
 
139
152
  const knownVocabularies = Object.keys(vocabularyDefinitions);
140
153
 
141
-
142
154
  /**************************************************************************************************
143
155
  * csn2annotationEdm
144
156
  *
@@ -146,8 +158,8 @@ const knownVocabularies = Object.keys(vocabularyDefinitions);
146
158
  * v - array with two boolean entries, first is for v2, second is for v4
147
159
  * dictReplacement: for test purposes, replaces the standard oDataDictionary
148
160
  */
149
- function csn2annotationEdm(csn, serviceName, Edm = undefined, options=undefined, messageFunctions=undefined) {
150
-
161
+ function csn2annotationEdm(reqDefs, csnVocabularies, serviceName, Edm = undefined, options=undefined, messageFunctions=undefined) {
162
+
151
163
  if(!Edm)
152
164
  throw new Error('Please debug me: csn2annotationsEdm must be invoked with Edm');
153
165
  if(!options)
@@ -157,13 +169,30 @@ function csn2annotationEdm(csn, serviceName, Edm = undefined, options=undefined,
157
169
  // global variable where we store all the generated annotations
158
170
  const g_annosArray = [];
159
171
 
160
- const { info, warning, error } = messageFunctions;
172
+ const { message } = messageFunctions;
173
+
174
+ const [ adhocDictionary, allKnownVocabularies ] = createAdhocDictionary();
161
175
 
162
- // Static dynamic expression dictionary, loaded with Edm creators
176
+ /*
177
+ allKnownVocabularies is the union of knownVocabularies & adhoc $mySchemaName annotation definition
178
+ this allows to identify namespace prefixes of arbitrary length.
179
+
180
+ If in the future, the full Oasis vocabulary names shall be made usable for assignments
181
+ eg. @com.sap.vocabularies.Common.v1.Label, all that needs to be done is to x-link the
182
+ vocabulary dictionary whith the full ns as key:
183
+ vocabularyDefinitions['@com.sap.vocabularies.Common.v1'] = vocabularyDefinitions['Common']
184
+ */
185
+ allKnownVocabularies.push(...knownVocabularies);
186
+ allKnownVocabularies.sort((a,b) => b.length-a.length);
187
+ const whatsMyTermNamespace = function(anno) {
188
+ return allKnownVocabularies.reduce((rc, ns) => !rc && anno && anno.startsWith('@' + ns + '.') ? ns : rc, undefined);
189
+ }
190
+
191
+ // Static dynamic expression dictionary, loaded with Edm creators
163
192
  const [ dynamicExpressions, dynamicExpressionNames ] = initEdmJson();
164
193
 
165
194
  // annotation preprocessing
166
- preprocessAnnotations.preprocessAnnotations(csn, serviceName, options);
195
+ preprocessAnnotations.preprocessAnnotations(reqDefs, serviceName, options);
167
196
 
168
197
  // we take note of which vocabularies are actually used in a service in order to avoid
169
198
  // producing useless references; reset everything to "unused"
@@ -171,32 +200,41 @@ function csn2annotationEdm(csn, serviceName, Edm = undefined, options=undefined,
171
200
  vocabularyDefinitions[n].used = false;
172
201
  });
173
202
 
203
+ // These vocabularies are always added for the runtimes
204
+ vocabularyDefinitions['Common'].used = true;
205
+ vocabularyDefinitions['Core'].used = true;
206
+
174
207
  // provide functions for dictionary lookup
175
208
  // use closure to avoid making "dict" and "experimental" global variables
176
- let { getDictTerm, getDictType } = function(){
177
- let dict = options.dictReplacement || oDataDictionary; // tests can set different dictionary via options
178
- let experimental = {}; // take note of all experimental annos that have been used
179
- let deprecated = {}; // take note of all deprecated annos that have been used
209
+ const { getDictTerm, getDictType } = function(){
210
+ const dict = options.dictReplacement || oDataDictionary; // tests can set different dictionary via options
211
+ const experimental = {}; // take note of all experimental annos that have been used
212
+ const deprecated = {}; // take note of all deprecated annos that have been used
180
213
 
181
214
  return {
182
215
  // called to look-up a term in the dictionary
183
216
  // in addition: - note usage of the respective vocabulary
184
217
  // - issue a warning if the term is flagged as "experimental"
185
- getDictTerm: function(termName, context) {
186
- const dictTerm = dict.terms[termName]
218
+ getDictTerm: function(termName, msg) {
219
+ const dictTerm = (dict.terms[termName] ||
220
+ adhocDictionary.terms[serviceName + '.' + termName] ||
221
+ adhocDictionary.terms[termName]);
187
222
  // register vocabulary usage if possible
188
223
  const vocName = termName.slice(0, termName.indexOf('.'));
189
224
  if(vocabularyDefinitions[vocName])
190
225
  vocabularyDefinitions[vocName].used = true;
191
-
226
+ else if(dictTerm?.$myServiceRoot &&
227
+ adhocDictionary.xrefs[dictTerm?.$myServiceRoot])
228
+ adhocDictionary.xrefs[dictTerm.$myServiceRoot].used = true;
192
229
  if (dictTerm) {
193
- // issue warning for usage of experimental Terms, but only once per Term
230
+ // issue message for usage of experimental Terms, but only once per Term
194
231
  if (dictTerm['$experimental'] && !experimental[termName]) {
195
- message(warning, context, 'Term "' + termName + '" is experimental and can be changed or removed at any time, do not use productively!');
232
+ message('odata-anno-dict', msg.location, { anno: msg.anno(), '#': 'experimental' });
196
233
  experimental[termName] = true;
197
234
  }
198
235
  if (dictTerm['$deprecated'] && !deprecated[termName]) {
199
- message(info, context, 'Term "' + termName + '" is deprecated. ' + dictTerm['$deprecationText']);
236
+ message('odata-anno-def', msg.location,
237
+ { anno: msg.anno(), depr: dictTerm['$deprecationText'], '#': 'deprecated' });
200
238
  deprecated[termName] = true;
201
239
  }
202
240
  }
@@ -205,10 +243,14 @@ function csn2annotationEdm(csn, serviceName, Edm = undefined, options=undefined,
205
243
  // called to look-up a type in the dictionary
206
244
  // in addition, note usage of the respective vocabulary
207
245
  getDictType: function (typeName) {
208
- let dictType = dict.types[typeName];
246
+ let dictType = (dict.types[typeName] ||
247
+ adhocDictionary.types[serviceName + '.' + typeName] ||
248
+ adhocDictionary.types[typeName]);
209
249
  if (dictType) {
210
250
  // register usage of vocabulary
211
- vocabularyDefinitions[typeName.slice(0, typeName.indexOf('.'))].used = true;
251
+ const voc = vocabularyDefinitions[typeName.slice(0, typeName.indexOf('.'))];
252
+ if(voc)
253
+ voc.used = true;
212
254
  }
213
255
  return dictType;
214
256
  }
@@ -222,18 +264,20 @@ function csn2annotationEdm(csn, serviceName, Edm = undefined, options=undefined,
222
264
  // Note: only works for single service
223
265
  // Note: we assume that all objects ly flat in the service, i.e. objName always
224
266
  // looks like <service name, can contain dots>.<id>
225
- forEachDefinition(csn, (object, objName) => {
267
+ forEachDefinition(reqDefs, (object, objName) => {
226
268
  if (objName === serviceName || objName.startsWith(serviceName + '.')) {
269
+
270
+ const location = [ 'definitions', objName ];
227
271
  if (object.kind === 'action' || object.kind === 'function') {
228
- handleAction(objName, object, null);
272
+ handleAction(objName, object, null, location);
229
273
  }
230
274
  else { // service, entity, anything else?
231
275
  // handle the annotations directly tied to the object
232
- handleAnnotations(objName, object);
276
+ handleAnnotations(objName, object, location);
233
277
  // handle the annotations of the object's elements
234
- handleElements(objName, object);
278
+ handleElements(objName, object, location);
235
279
  // handle the annotations of the object's actions
236
- handleBoundActions(objName, object);
280
+ handleBoundActions(objName, object, location);
237
281
  }
238
282
  }
239
283
  });
@@ -241,7 +285,9 @@ function csn2annotationEdm(csn, serviceName, Edm = undefined, options=undefined,
241
285
  // filter out empty <Annotations...> elements
242
286
  // add references for the used vocabularies
243
287
  return {
244
- annos: g_annosArray, usedVocabularies: Object.values(vocabularyDefinitions).filter(v => v.used)
288
+ annos: g_annosArray,
289
+ usedVocabularies: Object.values(vocabularyDefinitions).filter(v => v.used),
290
+ xrefs: Object.values(adhocDictionary.xrefs).filter(v => v.used).map(v => v.$myServiceRoot)
245
291
  };
246
292
 
247
293
  //-------------------------------------------------------------------------------------------------
@@ -254,21 +300,6 @@ function csn2annotationEdm(csn, serviceName, Edm = undefined, options=undefined,
254
300
  return v && v[0];
255
301
  }
256
302
 
257
- // this function is called in the translation code to issue an info/warning/error message
258
- // messages are reported via the severity function
259
- // context contains "semantic location"
260
- function message(severity, context, message) {
261
- let fullMessage = 'In annotation translation: ' + message;
262
- if (context) {
263
- let loc = 'target: ' + context.target + ', annotation: ' + context.term;
264
- if (context.stack.length > 0) {
265
- loc += context.stack.join('');
266
- }
267
- fullMessage += ', ' + loc;
268
- }
269
- severity(null, null, `${fullMessage}`);
270
- }
271
-
272
303
  /*
273
304
  Mapping annotated thing in cds/csn => annotated thing in edmx:
274
305
 
@@ -312,37 +343,17 @@ function csn2annotationEdm(csn, serviceName, Edm = undefined, options=undefined,
312
343
  // handle the annotations of the elements of an object
313
344
  // in: objname : name of the object
314
345
  // object : the object itself
315
- function handleElements(objname, object) {
346
+ function handleElements(objname, object, location) {
316
347
  if (!object.elements) return;
317
348
  Object.entries(object.elements).forEach(([elemName, element]) => {
318
349
  // determine the name of the target in the resulting edm
319
350
  // for non-assoc element, this simply is "<objectName>/<elementName>"
320
- let edmTargetName = objname + '/' + elemName;
321
- handleAnnotations(edmTargetName, element);
322
-
323
- // handle sub elements
324
- if (element.elements) {
325
- handleNestedElements(objname, elemName, element.elements);
326
- }
327
- });
328
- }
329
-
330
- // handling annotations at nested elements is not yet supported
331
- // => issue a warning, but only if there actually are annotations
332
- function handleNestedElements(objname, baseElemName, elementsObj) {
333
- if(!elementsObj) return;
334
- Object.entries(elementsObj).forEach(([elemName, element]) => {
335
- if (Object.keys(element).filter( x => x[0] === '@' ).filter(filterKnownVocabularies).length > 0) {
336
- message(warning, null, `annotations at nested elements are not yet supported, object ${objname}, element ${baseElemName}.${elemName}`);
337
- }
338
-
339
- if (element.elements) {
340
- handleNestedElements(objname, baseElemName + '.' + elemName, element.elements);
341
- }
351
+ const edmTargetName = objname + '/' + elemName;
352
+ const eLocation = [ ...location, 'elements', elemName ];
353
+ handleAnnotations(edmTargetName, element, eLocation);
342
354
  });
343
355
  }
344
356
 
345
-
346
357
  // Annotations for actions and functions (and their parameters)
347
358
  // v2, unbound: Target = <service>.EntityContainer/<action/function>
348
359
  // v2, bound: Target = <service>.EntityContainer/<entity>_<action/function>
@@ -354,17 +365,17 @@ function csn2annotationEdm(csn, serviceName, Edm = undefined, options=undefined,
354
365
  // handle the annotations of cObject's (an entity) bound actions/functions and their parameters
355
366
  // in: cObjectname : qualified name of the object that holds the actions
356
367
  // cObject : the object itself
357
- function handleBoundActions(cObjectname, cObject) {
368
+ function handleBoundActions(cObjectname, cObject, location) {
358
369
  if(!cObject.actions) return;
359
370
  // get service name: remove last part of the object name
360
371
  // only works if all objects ly flat in the service
361
- let nameParts = cObjectname.split('.')
362
- let entityName = nameParts.pop();
363
- let serviceName = nameParts.join('.');
372
+ const nameParts = cObjectname.split('.')
373
+ const entityName = nameParts.pop();
374
+ const serviceName = nameParts.join('.');
364
375
 
365
376
  Object.entries(cObject.actions).forEach(([n, action]) => {
366
- let actionName = serviceName + '.' + (isV2() ? entityName + '_' : '') + n;
367
- handleAction(actionName, action, cObjectname);
377
+ const actionName = serviceName + '.' + (isV2() ? entityName + '_' : '') + n;
378
+ handleAction(actionName, action, cObjectname, [ ...location, 'actions', n ]);
368
379
  });
369
380
  }
370
381
 
@@ -373,63 +384,63 @@ function csn2annotationEdm(csn, serviceName, Edm = undefined, options=undefined,
373
384
  // in: cActionName : qualified name of the action
374
385
  // cAction : the action object
375
386
  // entityNameIfBound : qualified name of entity if bound action/function
376
- function handleAction(cActionName, cAction, entityNameIfBound) {
387
+ function handleAction(cActionName, cAction, entityNameIfBound, location) {
377
388
  let actionName = cActionName;
378
389
  if (isV2()) { // Replace up to last dot with <serviceName>.EntityContainer
379
390
  const lastDotIndex = actionName.lastIndexOf('.');
380
391
  if (lastDotIndex > -1)
381
- actionName = serviceName + '.EntityContainer/' + actionName.substr(lastDotIndex + 1);
392
+ actionName = serviceName + '.EntityContainer/' + actionName.substring(lastDotIndex + 1);
382
393
  }
383
394
  else { // add parameter type list
384
- actionName += relParList(cAction, entityNameIfBound);
395
+ actionName += relParList();
385
396
  }
386
397
 
387
- handleAnnotations(actionName, cAction);
398
+ handleAnnotations(actionName, cAction, location);
399
+
388
400
  if(cAction.params) {
389
401
  Object.entries(cAction.params).forEach(([n, p]) => {
390
- let edmTargetName = actionName + '/' + n;
391
- handleAnnotations(edmTargetName, p);
402
+ const edmTargetName = actionName + '/' + n;
403
+ handleAnnotations(edmTargetName, p, [ ...location, 'params', n ]);
392
404
  });
393
405
  }
394
- }
395
406
 
396
- function relParList(action, bindingParam) {
407
+ function relParList() {
397
408
  // we rely on the order of params in the csn being the correct one
398
- let params = [];
399
- if (bindingParam) {
400
- params.push(action['@cds.odata.bindingparameter.collection'] ? 'Collection(' + bindingParam + ')' : bindingParam);
401
- }
402
- if (action.kind === 'function') {
403
- if(action.params) {
404
- action.params && Object.values(action.params).forEach(p => {
405
- let isArrayType = !p.type && p.items && p.items.type;
406
- params.push(isArrayType ? 'Collection(' + mapType(p.items) + ')' : mapType(p));
407
- });
409
+ const params = [];
410
+ if (entityNameIfBound) {
411
+ params.push(cAction['@cds.odata.bindingparameter.collection'] ? 'Collection(' + entityNameIfBound + ')' : entityNameIfBound);
412
+ }
413
+ if (cAction.kind === 'function') {
414
+ if(cAction.params) {
415
+ cAction.params && Object.values(cAction.params).forEach(p => {
416
+ const isArrayType = !p.type && p.items && p.items.type;
417
+ params.push(isArrayType ? 'Collection(' + mapType(p.items) + ')' : mapType(p));
418
+ });
419
+ }
408
420
  }
409
- }
410
- return '(' + params.join(',') + ')';
421
+ return '(' + params.join(',') + ')';
411
422
 
412
- function mapType(p) {
413
- if(isBuiltinType(p.type))
414
- return edmUtils.mapCdsToEdmType(p, messageFunctions, false /*is only called for v4*/)
415
- else if(options.whatsMySchemaName) {
416
- const schemaName = options.whatsMySchemaName(p.type);
423
+ function mapType(p) {
424
+ if(isBuiltinType(p.type))
425
+ return edmUtils.mapCdsToEdmType(p, messageFunctions, false /*is only called for v4*/)
426
+ else if(options.whatsMySchemaName) {
427
+ const schemaName = options.whatsMySchemaName(p.type);
417
428
  // strip the service namespace of from a parameter type
418
- if(schemaName && schemaName !== options.serviceName)
419
- return p.type.replace(options.serviceName + '.', '');
429
+ if(schemaName && schemaName !== options.serviceName)
430
+ return p.type.replace(options.serviceName + '.', '');
431
+ }
432
+ return p.type;
420
433
  }
421
- return p.type;
422
434
  }
423
- }
424
-
425
435
 
436
+ }
426
437
 
427
438
 
428
439
  // handle all the annotations for a given cds thing, here called carrier
429
440
  // edmTargetName : string, name of the target in edm
430
441
  // carrier: object, the annotated cds thing, contains all the annotations
431
442
  // as properties with names starting with @
432
- function handleAnnotations(edmTargetName, carrier) {
443
+ function handleAnnotations(edmTargetName, carrier, location) {
433
444
  // collect the names of the carrier's annotation properties
434
445
  // keep only those annotations that - start with a known vocabulary name
435
446
  // - have a value other than null
@@ -447,22 +458,20 @@ function csn2annotationEdm(csn, serviceName, Edm = undefined, options=undefined,
447
458
  // Filter unknown toplevel annotations
448
459
  // Final filtering of all annotations is done in handleTerm
449
460
 
450
- let annoNames = Object.keys(carrier).filter( x => x[0] === '@' );
451
- const nullWhitelist = [ '@Core.OperationAvailable' ];
452
- let knownAnnos = annoNames.filter(filterKnownVocabularies).filter(x => carrier[x] !== null || nullWhitelist.includes(x));
461
+ let knownAnnos = filterKnownAnnotations();
453
462
  if (knownAnnos.length === 0) return;
454
463
 
455
464
  if(rewriteInnerAnnotations()) {
456
- annoNames = Object.keys(carrier).filter( x => x[0] === '@' );
457
- knownAnnos = annoNames.filter(filterKnownVocabularies).filter(x => carrier[x] !== null || nullWhitelist.includes(x));
465
+ knownAnnos = filterKnownAnnotations();
458
466
  if (knownAnnos.length === 0) return;
459
467
  }
468
+
460
469
  const prefixTree = createPrefixTree();
461
470
 
462
471
  // usually, for a given carrier there is one target
463
472
  // for some carriers (service, entity), there can be an alternative target (usually the EntitySet)
464
473
  // alternativeEdmTargetName: name of alternative target
465
- // which one to choose depends on the "AppliesTo" info of the single annotations, so we have
474
+ // which one to choose depends on the "AppliesTo" message of the single annotations, so we have
466
475
  // to defer this decision; this is why we here construct a function that can make the decision
467
476
  // later when looking at single annotations
468
477
 
@@ -480,7 +489,7 @@ function csn2annotationEdm(csn, serviceName, Edm = undefined, options=undefined,
480
489
  const alternativeAnnotations = [];
481
490
 
482
491
  // now create annotation objects for all the annotations of carrier
483
- handleAnno2(addAnnotation, edmTargetName /*used for messages*/, prefixTree);
492
+ handleAnno2(addAnnotation, prefixTree, location);
484
493
 
485
494
  // Produce Edm.Annotations and attach collected Edm.Annotation(s) to the
486
495
  // envelope (or directly to the Schema)
@@ -498,6 +507,44 @@ function csn2annotationEdm(csn, serviceName, Edm = undefined, options=undefined,
498
507
  g_annosArray.push(annotations);
499
508
  }
500
509
 
510
+ function filterKnownAnnotations() {
511
+ const annoNames = Object.keys(carrier).filter( x => x[0] === '@' );
512
+ const nullWhitelist = [ '@Core.OperationAvailable' ];
513
+ const knownAnnos = annoNames.filter(whatsMyTermNamespace).filter(x => carrier[x] !== null || nullWhitelist.includes(x));
514
+ if(isBetaEnabled(options, 'odataTerms')) {
515
+ // Extend knownAnnos with the in-service term definitions
516
+ const adhoc = [];
517
+ annoNames.forEach(an => {
518
+ const paths = an.slice(1).split('.');
519
+ const hasNSPrefix = paths[0] === serviceName;
520
+ if(!hasNSPrefix) {
521
+ paths.splice(0, 0, serviceName);
522
+ }
523
+ const fqName = '@' + paths.join('.');
524
+ const i = paths[1].indexOf('#');
525
+ const termNameWithoutQualifiers = i > 0 ? paths[1].substring(0, i) : paths[1];
526
+ const def = reqDefs.definitions[paths[0] + '.' + termNameWithoutQualifiers];
527
+ // if there is a term definition inside the service and the
528
+ // annotation value is != null, then add the annotation to the list
529
+ // of known annotations
530
+ if(def?.kind === 'annotation' && carrier[an] !== null) {
531
+ // Subsequent annotation handler code expects that first path segment
532
+ // is the Vocabulary namespace. The ad-hoc namespace is the service
533
+ // name itself.
534
+ // For service S an annotation assignment could be addressed
535
+ // relative or absolute to the service @S.foo or @foo
536
+ if(!hasNSPrefix) {
537
+ carrier[fqName] = carrier[an];
538
+ delete carrier[an];
539
+ }
540
+ adhoc.push(fqName);
541
+ }
542
+ });
543
+ knownAnnos.push(...adhoc);
544
+ }
545
+ return knownAnnos;
546
+ }
547
+
501
548
  // construct a function that is used to add an <Annotation ...> to the
502
549
  // respective collector array
503
550
  // this function is specific to the actual carrier, following the mapping rules given above
@@ -522,7 +569,7 @@ function csn2annotationEdm(csn, serviceName, Edm = undefined, options=undefined,
522
569
  }
523
570
  // Another crazy hack due to this crazy function:
524
571
  // If carrier is a managed association (has keys) and rc is false (annotation was not applicable)
525
- // return true to NOT trigger 'unapplicable' info message
572
+ // return true to NOT trigger 'unapplicable' message message
526
573
  if(rc === false && carrier.target && carrier.keys && appliesTo.includes('Property'))
527
574
  rc = true;
528
575
  return rc;
@@ -558,7 +605,7 @@ function csn2annotationEdm(csn, serviceName, Edm = undefined, options=undefined,
558
605
  alternativeEdmTargetName = carrier.$entitySetName || edmTargetName
559
606
  const lastDotIndex = alternativeEdmTargetName.lastIndexOf('.');
560
607
  if (lastDotIndex > -1)
561
- alternativeEdmTargetName = serviceName + '.EntityContainer/' + alternativeEdmTargetName.substr(lastDotIndex + 1);
608
+ alternativeEdmTargetName = serviceName + '.EntityContainer/' + alternativeEdmTargetName.substring(lastDotIndex + 1);
562
609
  hasAlternativeCarrier = carrier.$hasEntitySet;
563
610
  }
564
611
  else if(carrier.kind === 'type') {
@@ -630,7 +677,7 @@ function csn2annotationEdm(csn, serviceName, Edm = undefined, options=undefined,
630
677
 
631
678
  function rewriteInnerAnnotations() {
632
679
  let rc = false;
633
- for (let a of knownAnnos) {
680
+ for (const a of knownAnnos) {
634
681
  const [ prefix, innerAnnotation ] = a.split('.@');
635
682
  /*
636
683
  New inner annotation (de-)structuring of the core compiler to make
@@ -684,7 +731,7 @@ function csn2annotationEdm(csn, serviceName, Edm = undefined, options=undefined,
684
731
  // see example at definition of function mergePathStepsIntoPrefixTree
685
732
  const prefixTree = {};
686
733
 
687
- for (let a of knownAnnos) {
734
+ for (const a of knownAnnos) {
688
735
  // remove leading @ and split at "."
689
736
  // stop splitting at ".@" (used for nested annotations)
690
737
  // Inline JSON EDM allows to add annotations to record members
@@ -692,7 +739,9 @@ function csn2annotationEdm(csn, serviceName, Edm = undefined, options=undefined,
692
739
  // The splitter should leave such annotations alone, handleEdmJson
693
740
  // takes care of assigning these annotations to the record members
694
741
  const [ prefix, innerAnnotation ] = a.split('.@');
695
- const steps = prefix.slice(1).split('.');
742
+ const ns = whatsMyTermNamespace(prefix);
743
+ const steps = prefix.replace('@' + ns + '.', '').split('.');
744
+ steps.splice(0,0, ns);
696
745
  let i = steps.lastIndexOf('$edmJson');
697
746
  if(i > -1) {
698
747
  i = steps.findIndex(s => s.includes('@'), i+1);
@@ -738,7 +787,7 @@ function csn2annotationEdm(csn, serviceName, Edm = undefined, options=undefined,
738
787
  // q2 : ... } } }
739
788
  function mergePathStepsIntoPrefixTree(tree, pathSteps, index, carrier) {
740
789
  // TODO check nesting level > 3
741
- let name = pathSteps[index];
790
+ const name = pathSteps[index];
742
791
  if (index+1 < pathSteps.length ) {
743
792
  if (!tree[name]) {
744
793
  tree[name] = {};
@@ -756,27 +805,35 @@ function csn2annotationEdm(csn, serviceName, Edm = undefined, options=undefined,
756
805
  // handle all the annotations for a given carrier
757
806
  // addAnnotationFunc: a function that adds the <Annotation ...> tags created here into the
758
807
  // correct parent tag (see handleAnnotations())
759
- // edmTargetName: name of the edmx target, only used for messages
760
808
  // prefixTree: the annotations
761
- function handleAnno2(addAnnotationFunc, edmTargetName, prefixTree) {
809
+ function handleAnno2(addAnnotationFunc, prefixTree, location) {
762
810
  // first level names of prefix tree are the vocabulary names
763
811
  // second level names are the term names
764
812
  // create an annotation tag <Annotation ...> for each term
765
- for (let voc of Object.keys(prefixTree)) {
766
- for (let term of Object.keys(prefixTree[voc])) {
767
- let fullTermName = voc + '.' + term;
813
+ for (const voc of Object.keys(prefixTree)) {
814
+ for (const term of Object.keys(prefixTree[voc])) {
815
+ const fullTermName = voc + '.' + term;
816
+
817
+ // msg is "semantic" location message used for messages
818
+ const msg = {
819
+ fullTermName,
820
+ stack: [],
821
+ location: [ ...location, '@' + fullTermName ],
822
+ };
823
+ msg.anno = () => {
824
+ return msg.fullTermName + msg.stack.join('');
825
+ }
768
826
 
769
- // context is "semantic" location info used for messages
770
- let context = { target: edmTargetName, term: fullTermName, stack: [] };
771
827
  // anno is the full <Annotation Term=...>
772
- let anno = handleTerm(fullTermName, prefixTree[voc][term], context);
828
+ const anno = handleTerm(fullTermName, prefixTree[voc][term], msg);
773
829
  if(anno !== undefined) {
774
- // addAnnotationFunc needs AppliesTo info from dictionary to decide where to put the anno
775
- fullTermName = fullTermName.replace(/#(\w+)$/g, ''); // remove qualifier
776
- let dictTerm = getDictTerm(fullTermName, context); // message for unknown term was already issued in handleTerm
830
+ // addAnnotationFunc needs AppliesTo message from dictionary to decide where to put the anno
831
+ const termName = fullTermName.replace(/#(\w+)$/g, ''); // remove qualifier
832
+ const dictTerm = getDictTerm(termName, msg); // message for unknown term was already issued in handleTerm
777
833
  if(!addAnnotationFunc(anno, dictTerm && dictTerm.AppliesTo)) {
778
834
  if(dictTerm && dictTerm.AppliesTo) {
779
- message(info, context, `Term "${ fullTermName }" is not applied (AppliesTo="${ dictTerm.AppliesTo.join(' ') }")`);
835
+ message('odata-anno-def', location,
836
+ { anno: termName, rawvalues: dictTerm.AppliesTo, '#': 'notapplied' });
780
837
  }
781
838
  }
782
839
  }
@@ -788,9 +845,9 @@ function csn2annotationEdm(csn, serviceName, Edm = undefined, options=undefined,
788
845
  // annoValue : the annotation value from the csn
789
846
  // if the csn contains flattened out elements of a structured annotation,
790
847
  // they are regrouped here
791
- // context : for messages
848
+ // msg : for messages
792
849
  // return : object that represents the annotation in the result edmx
793
- function handleTerm(termName, annoValue, context) {
850
+ function handleTerm(termName, annoValue, msg) {
794
851
  /**
795
852
  * create the <Annotation ...> tag
796
853
  * @type {object}
@@ -798,52 +855,40 @@ function csn2annotationEdm(csn, serviceName, Edm = undefined, options=undefined,
798
855
  let newAnno = undefined;
799
856
  const omissions = { 'Aggregation.default':1 };
800
857
  const nullList = { 'Core.OperationAvailable':1 };
801
- const voc = termName.slice(0, termName.indexOf('.'));
802
- if(vocabularyDefinitions[voc] && annoValue !== null && !omissions[termName]|| nullList[termName]) {
803
- newAnno = new Edm.Annotation(v, termName);
804
-
858
+ if(annoValue !== null && !omissions[termName]|| nullList[termName]) {
805
859
  // termName may contain a qualifier: @UI.FieldGroup#shippingStatus
806
860
  // -> remove qualifier from termName and set Qualifier attribute in newAnno
807
- let p = termName.split('#');
808
- let termNameWithoutQualifiers = p[0];
809
- if (p.length > 1) {
810
- checkOdataTerm(p[0]);
811
- if (!edmUtils.isODataSimpleIdentifier(p[1])) {
812
- message(error, context,
813
- `OData annotation qualifier "${ p[1] }" must start with a letter or underscore, followed by at most 127 letters, underscores or digits`);
861
+ const i = termName.indexOf('#');
862
+ const termNameWithoutQualifiers = i > 0 ? termName.substring(0, i) : termName;
863
+ const qualifier = i >= 0 ? termName.substring(i+1) : undefined;
864
+
865
+ termNameWithoutQualifiers.split('.').forEach((id) => {
866
+ if(!edmUtils.isODataSimpleIdentifier(id))
867
+ message('odata-spec-violation-id', msg.location, { id });
868
+ })
869
+ newAnno = new Edm.Annotation(v, termNameWithoutQualifiers);
870
+ if (qualifier?.length) {
871
+ if (!edmUtils.isODataSimpleIdentifier(qualifier)) {
872
+ message('odata-spec-violation-id', msg.location,
873
+ { id: qualifier, '#': 'qualifier' });
814
874
  }
815
875
  newAnno.setEdmAttribute('Term', termNameWithoutQualifiers);
816
- newAnno.setEdmAttribute('Qualifier', p[1]);
876
+ newAnno.setEdmAttribute('Qualifier', qualifier);
817
877
  }
818
- if (p.length>2) {
819
- message(warning, context, `multiple qualifiers (${ p[1] },${ p[2] }${ p.length > 3 ? ',...' : '' })`);
820
- }
821
-
822
878
  // get the type of the term from the dictionary
823
879
  let termTypeName = null;
824
- let dictTerm = getDictTerm(termNameWithoutQualifiers, context);
880
+ const dictTerm = getDictTerm(termNameWithoutQualifiers, msg);
825
881
  if (dictTerm) {
826
882
  termTypeName = dictTerm.Type;
827
883
  }
828
884
  else {
829
- message(info, context, `Unknown term “${ termNameWithoutQualifiers }”`);
885
+ message('odata-anno-def', msg.location,{ anno: termNameWithoutQualifiers });
830
886
  }
831
887
 
832
888
  // handle the annotation value and put the result into the <Annotation ...> tag just created above
833
- handleValue(annoValue, newAnno, termNameWithoutQualifiers, termTypeName, context);
889
+ handleValue(annoValue, newAnno, termNameWithoutQualifiers, termTypeName, msg);
834
890
  }
835
891
  return newAnno;
836
-
837
- function checkOdataTerm(ns) {
838
- const simpleIdentifiers = ns.split('.');
839
- simpleIdentifiers.forEach((identifier) => {
840
- if(!edmUtils.isODataSimpleIdentifier(identifier)){
841
- message(error, context,
842
- `OData annotation term "${ identifier }" must consist of one or more dot separated simple identifiers (each starting with a letter or underscore, followed by at most 127 letters)`);
843
- }
844
- })
845
- }
846
-
847
892
  }
848
893
 
849
894
 
@@ -852,7 +897,7 @@ function csn2annotationEdm(csn, serviceName, Edm = undefined, options=undefined,
852
897
  // oTarget: the result object (o: odata)
853
898
  // oTermName: current term
854
899
  // dTypeName: expected type of cAnnoValue according to dictionary, may be null (d: dictionary)
855
- function handleValue(cAnnoValue, oTarget, oTermName, dTypeName, context) {
900
+ function handleValue(cAnnoValue, oTarget, oTermName, dTypeName, msg) {
856
901
  // this function basically only figures out what kind of annotation value we have
857
902
  // (can be: array, expression, enum, pseudo-record, record, simple value),
858
903
  // then calls a more specific function to deal with it and puts
@@ -863,65 +908,86 @@ function csn2annotationEdm(csn, serviceName, Edm = undefined, options=undefined,
863
908
  if (isEnumType(dTypeName))
864
909
  {
865
910
  // if we find an array although we expect an enum, this may be a "flag enum"
866
- checkMultiEnumValue(cAnnoValue, dTypeName, context);
867
- oTarget.setJSON({ 'EnumMember': generateMultiEnumValue(cAnnoValue, dTypeName, false), 'EnumMember@odata.type' : '#'+dTypeName });
868
- oTarget.setXml( { 'EnumMember': generateMultiEnumValue(cAnnoValue, dTypeName, true) });
911
+ checkMultiEnumValue(cAnnoValue);
912
+ oTarget.setJSON({ 'EnumMember': generateMultiEnumValue(cAnnoValue, false), 'EnumMember@odata.type' : '#'+dTypeName });
913
+ oTarget.setXml( { 'EnumMember': generateMultiEnumValue(cAnnoValue, true) });
869
914
  }
870
915
  else
871
916
  {
872
- oTarget.append(generateCollection(cAnnoValue, oTermName, dTypeName, context));
917
+ oTarget.append(generateCollection(cAnnoValue, oTermName, dTypeName, msg));
873
918
  }
874
919
  }
875
920
  else if (cAnnoValue && typeof cAnnoValue === 'object') {
876
- if (Object.keys(cAnnoValue).length === 0) {
877
- message(warning, context, 'empty record');
878
- }
879
- else if ('=' in cAnnoValue) {
921
+ // an empty record is rendered as <Record/>
922
+ if ('=' in cAnnoValue) {
880
923
  // expression
881
- let res = handleExpression(cAnnoValue['='], dTypeName, context);
924
+ const res = handleExpression(cAnnoValue['='], dTypeName);
882
925
  oTarget.setXml( { [res.name] : res.value });
883
926
  oTarget.setJSON( { [res.name] : res.value });
884
927
  }
885
928
  else if (cAnnoValue['#'] !== undefined) {
929
+ const enumSymbol = cAnnoValue['#'];
886
930
  // enum
887
931
  if (dTypeName) {
888
- checkEnumValue(cAnnoValue['#'], dTypeName, context);
889
- oTarget.setXml( { 'EnumMember': dTypeName + '/' + cAnnoValue['#'] });
932
+ const typeDef = getDictType(stripCollection(dTypeName));
933
+ if(typeDef && typeDef.$Allowed && !typeDef.Members) {
934
+ const allowedValue = typeDef.$Allowed.Symbols[enumSymbol];
935
+ if(!allowedValue) {
936
+ message('odata-anno-value', msg.location,
937
+ { anno: msg.anno(),
938
+ type: dTypeName,
939
+ value: `"#${enumSymbol}"`,
940
+ rawvalues: Object.keys(typeDef.$Allowed.Symbols).map(m => `"#${m}"`),
941
+ '#': 'enum'
942
+ });
943
+ }
944
+ else {
945
+ oTarget.setXml( { [typeDef.UnderlyingType?.replace('Edm.', '') || 'String']: allowedValue.Value || enumSymbol });
946
+ }
947
+ }
948
+ else if(checkEnumValue(enumSymbol))
949
+ oTarget.setXml( { 'EnumMember': dTypeName + '/' + enumSymbol });
950
+ else
951
+ oTarget.setXml( { 'String': enumSymbol });
890
952
  }
891
953
  else {
892
- oTarget.setXml( { 'EnumMember': oTermName + 'Type/' + cAnnoValue['#'] });
954
+ oTarget.setXml( { 'EnumMember': oTermName + 'Type/' + enumSymbol });
893
955
  }
894
- oTarget.setJSON({ 'Edm.String': cAnnoValue['#'] });
956
+ oTarget.setJSON({ 'Edm.String': enumSymbol });
895
957
  }
896
958
  else if (cAnnoValue['$value'] !== undefined) {
897
959
  // "pseudo-structure" used for annotating scalar annotations
898
- handleValue(cAnnoValue['$value'], oTarget, oTermName, dTypeName, context);
960
+ handleValue(cAnnoValue['$value'], oTarget, oTermName, dTypeName, msg);
899
961
 
900
- let k = Object.keys(cAnnoValue).filter( x => x.charAt(0) === '@');
962
+ const k = Object.keys(cAnnoValue).filter( x => x[0] === '@');
901
963
  if (!k || k.length === 0) {
902
- message(warning, context, 'pseudo-struct without nested annotation');
964
+ message('odata-anno-value', msg.location,
965
+ { anno: msg.anno(), str: 'nested', '#': 'nested' });
903
966
  }
904
- for (let nestedAnnoName of k) {
905
- let nestedAnno = handleTerm(nestedAnnoName.slice(1), cAnnoValue[nestedAnnoName], context);
967
+ for (const nestedAnnoName of k) {
968
+ const nestedAnno = handleTerm(nestedAnnoName.slice(1), cAnnoValue[nestedAnnoName], msg);
906
969
  oTarget.append(nestedAnno);
907
970
  }
908
971
  }
909
972
  else if (cAnnoValue['$edmJson']) {
910
973
  // "pseudo-structure" used for embedding a piece of JSON that represents "OData CSDL, JSON Representation"
911
- oTarget.append(handleEdmJson(cAnnoValue['$edmJson'], context));
974
+ oTarget.append(handleEdmJson(cAnnoValue['$edmJson'], msg));
912
975
  }
913
976
  else if ( Object.keys(cAnnoValue).filter( x => x[0] !== '@' ).length === 0) {
914
977
  // object consists only of properties starting with "@", no $value
915
- message(warning, context, 'nested annotations without corresponding base annotation');
978
+ message('odata-anno-value', msg.location,
979
+ { anno: msg.anno(), str: 'base', '#': 'nested'} );
916
980
  }
917
981
  else {
918
982
  // regular record
919
- oTarget.append(generateRecord(cAnnoValue, oTermName, dTypeName, context));
983
+ oTarget.append(generateRecord(cAnnoValue, oTermName, dTypeName, msg));
920
984
  }
921
985
  }
922
986
  else {
923
- let res = handleSimpleValue(cAnnoValue, dTypeName, context);
924
- if(oTermName === 'Core.OperationAvailable' && dTypeName === 'Edm.Boolean' && cAnnoValue === null) {
987
+ const res = handleSimpleValue(cAnnoValue, dTypeName, msg);
988
+ if(((oTermName === 'Core.OperationAvailable' && dTypeName === 'Edm.Boolean') ||
989
+ (oTermName === 'Validation.AllowedValues' && dTypeName === 'Edm.PrimitiveType'))
990
+ && cAnnoValue === null) {
925
991
  oTarget.append(new Edm.ValueThing(v, 'Null'));
926
992
  oTarget._ignoreChildren = true;
927
993
  }
@@ -930,81 +996,95 @@ function csn2annotationEdm(csn, serviceName, Edm = undefined, options=undefined,
930
996
  }
931
997
  oTarget.setJSON( { [res.jsonName] : res.value });
932
998
  }
933
- }
934
-
935
999
 
936
- // found an enum value ("#"), check whether this fits
937
- // the expected type "dTypeName"
938
- function checkEnumValue(enumValue, dTypeName, context) {
939
- let expectedType = getDictType(dTypeName);
940
- if (!expectedType && !isPrimitiveType(dTypeName)) {
941
- message(warning, context, `internal error: dictionary inconsistency: type '${ dTypeName }' not found`);
942
- }
943
- else if (isComplexType(dTypeName)) {
944
- message(warning, context, `found enum value, but expected complex type ${ dTypeName }`);
945
- }
946
- else if (isPrimitiveType(dTypeName) || expectedType['$kind'] !== 'EnumType') {
947
- message(warning, context, `found enum value, but expected non-enum type ${ dTypeName }`);
948
- }
949
- else if (!expectedType['Members'].includes(enumValue)) {
950
- message(warning, context, `enumeration type ${ dTypeName } has no value ${ enumValue }`);
1000
+ // found an enum value ("#"), check whether this fits
1001
+ // the expected type "dTypeName"
1002
+ function checkEnumValue(value) {
1003
+ let rc = true;
1004
+ const expectedType = getDictType(dTypeName);
1005
+ if (!expectedType && !isPrimitiveType(dTypeName)) {
1006
+ message('odata-anno-dict', msg.location,
1007
+ { anno: msg.anno(), type: dTypeName });
1008
+ }
1009
+ else if (isComplexType(dTypeName) || isPrimitiveType(dTypeName) || expectedType['$kind'] !== 'EnumType') {
1010
+ message('odata-anno-value', msg.location,
1011
+ { anno: msg.anno(),
1012
+ type: dTypeName,
1013
+ value: `"#${value}"` });
1014
+ rc = false;
1015
+ }
1016
+ else if (!expectedType.Members.includes(value)) {
1017
+ message('odata-anno-value', msg.location,
1018
+ { anno: msg.anno(),
1019
+ type: dTypeName,
1020
+ value: `"#${value}"`,
1021
+ rawvalues: expectedType.Members.map(m => `"#${m}"`),
1022
+ '#': 'enum'
1023
+ });
1024
+ }
1025
+ return rc;
951
1026
  }
952
- }
953
1027
 
954
- // cAnnoValue: array
955
- // dTypeName: expected type, already identified as enum type
956
- // array is expected to contain enum values
957
- function checkMultiEnumValue(cAnnoValue, dTypeName, context) {
1028
+ // cAnnoValue: array
1029
+ // dTypeName: expected type, already identified as enum type
1030
+ // array is expected to contain enum values
1031
+ function checkMultiEnumValue(cAnnoValue) {
958
1032
  // we know that dTypeName is not null
959
- let type = getDictType(dTypeName);
960
- if (!type || type['IsFlags'] !== 'true') {
961
- message(warning, context, `enum type '${ dTypeName }' doesn't allow multiple values`);
1033
+ const type = getDictType(dTypeName);
1034
+ if (!type || type['IsFlags'] !== 'true') {
1035
+ message('odata-anno-value', msg.location,
1036
+ { anno: msg.anno(),
1037
+ str: 'collection',
1038
+ type: dTypeName,
1039
+ '#': 'struct' });
1040
+ }
1041
+
1042
+ let index = 0;
1043
+ for (let value of cAnnoValue) {
1044
+ msg.stack.push('[' + index + ']');
1045
+ index++;
1046
+ if (value['#']) {
1047
+ checkEnumValue(value['#']);
1048
+ }
1049
+ else {
1050
+ message('odata-anno-value', msg.location,
1051
+ { anno: msg.anno(),
1052
+ type: dTypeName,
1053
+ value: value['='] || value,
1054
+ rawvalues: type.Members.map(m => `"#${m}"`),
1055
+ '#': 'enum'
1056
+ });
1057
+ }
1058
+ msg.stack.pop();
1059
+ }
962
1060
  }
963
1061
 
964
- let index = 0;
965
- for (const e of cAnnoValue) {
966
- context.stack.push('[' + index + ']');
967
- index++;
968
- if (e['#']) {
969
- checkEnumValue(e['#'], dTypeName, context);
970
- }
971
- else {
972
- // TODO improve message: but found ...
973
- message(warning, context, 'expected an enum value');
974
- }
975
- context.stack.pop();
1062
+ function generateMultiEnumValue(cAnnoValue, forXml) {
1063
+ // remove all invalid entries (warnining message has already been issued)
1064
+ // replace short enum name by the full name
1065
+ // concatenate all the enums to a string, separated by spaces
1066
+ return cAnnoValue.filter( x => x['#'] != undefined ).map( x => (forXml ? dTypeName + '/' : '') + x['#'] ).join(forXml ? ' ' : ',');
976
1067
  }
977
1068
  }
978
1069
 
979
- function generateMultiEnumValue(cAnnoValue, dTypeName, forXml)
980
- {
981
- // remove all invalid entries (warnining message has already been issued)
982
- // replace short enum name by the full name
983
- // concatenate all the enums to a string, separated by spaces
984
- return cAnnoValue.filter( x => x['#'] != undefined ).map( x => (forXml ? dTypeName + '/' : '') + x['#'] ).join(forXml ? ' ' : ',');
985
- }
986
1070
 
987
1071
 
988
1072
  // found an expression value ("=") "expr"
989
1073
  // expected type is dTypeName
990
1074
  // note: expr can also be provided if an enum/complex type/collection is expected
991
- function handleExpression(expr, dTypeName, context) {
1075
+ function handleExpression(value, dTypeName) {
992
1076
  let typeName = 'Path';
993
1077
  if( ['Edm.AnnotationPath', 'Edm.ModelElementPath', 'Edm.NavigationPropertyPath', 'Edm.PropertyPath', 'Edm.Path' ].includes(dTypeName) )
994
1078
  typeName = dTypeName.split('.')[1];
995
1079
 
996
- let val = expr;
997
- if (!expr) {
998
- message(warning, context, 'empty expression value');
999
- }
1000
- else {
1080
+ if(value) {
1001
1081
  // replace all occurrences of '.' by '/' up to first '@'
1002
- val = expr.split('@').map((o,i) => (i === 0 ? o.replace(/\./g, '/') : o)).join('@');
1082
+ value = value.split('@').map((o,i) => (i === 0 ? o.replace(/\./g, '/') : o)).join('@');
1003
1083
  }
1004
1084
 
1005
1085
  return {
1006
1086
  name : typeName,
1007
- value : val
1087
+ value
1008
1088
  }
1009
1089
  }
1010
1090
 
@@ -1016,119 +1096,147 @@ function csn2annotationEdm(csn, serviceName, Edm = undefined, options=undefined,
1016
1096
  // floating point type except Edm.Decimal -> Float
1017
1097
  // Edm.Decimal -> Decimal
1018
1098
  // integer tpye -> Int
1019
- function handleSimpleValue(val, dTypeName, context) {
1099
+ function handleSimpleValue(value, dTypeName, msg) {
1020
1100
  // these types must be represented as "String" values in XML:
1021
1101
  const castToXmlString = [ 'Edm.PrimitiveType', 'Edm.Stream', 'Edm.Untyped' ];
1022
1102
  // caller already made sure that val is neither object nor array
1023
- dTypeName = resolveType(dTypeName);
1024
1103
 
1025
- if (isEnumType(dTypeName)) {
1026
- const type = getDictType(dTypeName);
1027
- const expected = type.Members.map(m => `"#${m}"`).join(', ');
1028
- message(warning, context, `found non-enum value "${val}", expected ${expected} for ${dTypeName}`);
1104
+ // check if type has allowed values
1105
+ const typeDef = getDictType(dTypeName);
1106
+ const Allowed = typeDef?.$Allowed;
1107
+
1108
+ let resolvedType = resolveTypeDefinition(dTypeName);
1109
+
1110
+ if (isEnumType(resolvedType)) {
1111
+ const type = getDictType(resolvedType);
1112
+ const expected = type.Members.map(m => `"#${m}"`);
1113
+ message('odata-anno-value', msg.location,
1114
+ { anno: msg.anno(),
1115
+ value,
1116
+ rawvalues: expected,
1117
+ type: resolvedType,
1118
+ '#': 'enum' });
1029
1119
  }
1030
1120
 
1031
1121
  let typeName = 'String';
1032
-
1033
- if (typeof val === 'string') {
1034
- if (dTypeName === 'Edm.Boolean') {
1122
+ if(Allowed && !Allowed.Values[value])
1123
+ message('odata-anno-value', msg.location,
1124
+ { anno: msg.anno(),
1125
+ value,
1126
+ rawvalues: Object.keys(Allowed.Values),
1127
+ type: resolvedType,
1128
+ '#': 'enum' });
1129
+
1130
+ if (typeof value === 'string') {
1131
+ if (resolvedType === 'Edm.Boolean') {
1035
1132
  typeName = 'Bool';
1036
- if (val !== 'true' && val !== 'false') {
1037
- message(warning, context, `found String, but expected type ${ dTypeName }`);
1133
+ if (value !== 'true' && value !== 'false') {
1134
+ message('odata-anno-value', msg.location,
1135
+ { anno: msg.anno(), value, type: resolvedType });
1038
1136
  }
1039
1137
  }
1040
- else if (dTypeName === 'Edm.Decimal') {
1138
+ else if (resolvedType === 'Edm.Decimal') {
1041
1139
  typeName = 'Decimal';
1042
- if (isNaN(Number(val)) || isNaN(parseFloat(val))) {
1043
- message(warning, context, `found non-numeric string, but expected type ${ dTypeName }`);
1140
+ if (isNaN(Number(value)) || isNaN(parseFloat(value))) {
1141
+ message('odata-anno-value', msg.location,
1142
+ { anno: msg.anno(), value, type: resolvedType });
1044
1143
  }
1045
1144
  }
1046
- else if (dTypeName === 'Edm.Double' || dTypeName === 'Edm.Single') {
1145
+ else if (resolvedType === 'Edm.Double' || resolvedType === 'Edm.Single') {
1047
1146
  typeName = 'Float';
1048
- if (isNaN(Number(val)) || isNaN(parseFloat(val))) {
1049
- message(warning, context, `found non-numeric string, but expected type ${ dTypeName }`);
1147
+ if (isNaN(Number(value)) || isNaN(parseFloat(value))) {
1148
+ message('odata-anno-value', msg.location,
1149
+ { anno: msg.anno(), value, type: resolvedType });
1050
1150
  }
1051
1151
  }
1052
- else if (isComplexType(dTypeName)) {
1053
- message(warning, context, `found String, but expected complex type ${ dTypeName }`);
1152
+ else if (isComplexType(resolvedType)) {
1153
+ message('odata-anno-value', msg.location,
1154
+ { anno: msg.anno(), value, type: resolvedType });
1054
1155
  }
1055
- else if (isEnumType(dTypeName)) {
1056
- message(warning, context, `found String, but expected enum type ${ dTypeName }`);
1156
+ else if (isEnumType(resolvedType)) {
1157
+ message('odata-anno-value', msg.location,
1158
+ { anno: msg.anno(), value, type: resolvedType });
1057
1159
  typeName = 'EnumMember';
1058
1160
  }
1059
- else if (dTypeName && dTypeName.startsWith('Edm.') && !castToXmlString.includes(dTypeName)) {
1161
+ else if (resolvedType && resolvedType.startsWith('Edm.') && !castToXmlString.includes(resolvedType)) {
1060
1162
  // this covers also all paths
1061
- typeName = dTypeName.substring(4);
1163
+ typeName = resolvedType.substring(4);
1062
1164
  }
1063
1165
  else {
1064
- if(dTypeName == undefined || castToXmlString.some(t => t === dTypeName))
1065
- dTypeName = 'Edm.String';
1166
+ if(resolvedType == undefined || castToXmlString.some(t => t === resolvedType))
1167
+ resolvedType = 'Edm.String';
1066
1168
  // TODO
1067
- //message(warning, context, "type is not yet handled: found String, expected type: " + dTypeName);
1169
+ //message(message, msg, "type is not yet handled: found String, expected type: " + dTypeName);
1068
1170
  }
1069
1171
  }
1070
- else if (typeof val === 'boolean') {
1071
- if(dTypeName == undefined || dTypeName === 'Edm.Boolean' || dTypeName === 'Edm.PrimitiveType') {
1172
+ else if (typeof value === 'boolean') {
1173
+ if(resolvedType == undefined || resolvedType === 'Edm.Boolean' || resolvedType === 'Edm.PrimitiveType') {
1072
1174
  typeName = 'Bool';
1073
- dTypeName = 'Edm.Boolean';
1175
+ resolvedType = 'Edm.Boolean';
1074
1176
  }
1075
- if (dTypeName === 'Edm.Boolean') {
1076
- val = val ? 'true' : 'false';
1177
+ if (resolvedType === 'Edm.Boolean') {
1178
+ value = value ? 'true' : 'false';
1077
1179
  }
1078
- else if (dTypeName === 'Edm.String') {
1180
+ else if (resolvedType === 'Edm.String') {
1079
1181
  typeName = 'String';
1080
1182
  }
1081
1183
  else {
1082
- message(warning, context, `found Boolean, but expected type ${ dTypeName }`);
1184
+ message('odata-anno-value', msg.location,
1185
+ { anno: msg.anno(), value, type: resolvedType });
1083
1186
  }
1084
1187
  }
1085
- else if (typeof val === 'number') {
1086
- if (isComplexType(dTypeName)) {
1087
- message(warning, context, `found number, but expected complex type ${ dTypeName }`);
1188
+ else if (typeof value === 'number') {
1189
+ if (isComplexType(resolvedType)) {
1190
+ message('odata-anno-value', msg.location,
1191
+ { anno: msg.anno(), value, type: resolvedType });
1088
1192
  }
1089
- else if (dTypeName === 'Edm.String') {
1193
+ else if (resolvedType === 'Edm.String') {
1090
1194
  typeName = 'String';
1091
1195
  }
1092
- else if (dTypeName === 'Edm.PropertyPath') {
1093
- message(warning, context, `found number, but expected type ${ dTypeName }`);
1196
+ else if (resolvedType === 'Edm.PropertyPath') {
1197
+ message('odata-anno-value', msg.location,
1198
+ { anno: msg.anno(), value, type: resolvedType });
1094
1199
  }
1095
- else if (dTypeName === 'Edm.Boolean') {
1096
- message(warning, context, `found number, but expected type ${ dTypeName }`);
1200
+ else if (resolvedType === 'Edm.Boolean') {
1201
+ message('odata-anno-value', msg.location,
1202
+ { anno: msg.anno(), value, type: resolvedType });
1097
1203
  }
1098
- else if (dTypeName === 'Edm.Decimal') {
1204
+ else if (resolvedType === 'Edm.Decimal') {
1099
1205
  typeName = 'Decimal';
1100
1206
  }
1101
- else if (dTypeName === 'Edm.Double') {
1207
+ else if (resolvedType === 'Edm.Double') {
1102
1208
  typeName = 'Float';
1103
1209
  }
1104
1210
  else {
1105
1211
  //typeName = Number.isInteger(val) ? 'Int' : 'Float';
1106
- if(Number.isInteger(val)) {
1212
+ if(Number.isInteger(value)) {
1107
1213
  typeName = 'Int';
1108
- if(dTypeName == undefined || dTypeName === 'Edm.PrimitiveType' || !dTypeName.startsWith('Edm.'))
1109
- dTypeName = 'Edm.Int64';
1214
+ if(resolvedType == undefined || resolvedType === 'Edm.PrimitiveType' || !resolvedType.startsWith('Edm.'))
1215
+ resolvedType = 'Edm.Int64';
1110
1216
  }
1111
1217
  else {
1112
1218
  typeName = 'Float';
1113
- if(dTypeName == undefined || dTypeName === 'Edm.PrimitiveType'|| !dTypeName.startsWith('Edm.'))
1114
- dTypeName = 'Edm.Double';
1219
+ if(resolvedType == undefined || resolvedType === 'Edm.PrimitiveType'|| !resolvedType.startsWith('Edm.'))
1220
+ resolvedType = 'Edm.Double';
1115
1221
  }
1116
1222
  }
1117
1223
  }
1118
- else if (val === null && dTypeName == null && typeName === 'String') {
1119
- dTypeName = 'Edm.String';
1120
- }
1121
- else {
1122
- message(warning, context, `expected simple value, but found value '${ val }' with type '${ typeof val }'`);
1123
- }
1224
+ else if (value === null)
1225
+ if((resolvedType == null || resolvedType == 'Edm.PrimitiveType') && typeName === 'String') {
1226
+ resolvedType = 'Edm.String';
1227
+ }
1228
+ else {
1229
+ message('odata-anno-value', msg.location,
1230
+ { anno: msg.anno(), value, type: resolvedType });
1231
+ }
1124
1232
 
1125
- if( ['Edm.AnnotationPath', 'Edm.ModelElementPath', 'Edm.NavigationPropertyPath', 'Edm.PropertyPath', 'Edm.Path' ].includes(dTypeName) )
1126
- dTypeName = dTypeName.split('.')[1];
1233
+ if( ['Edm.AnnotationPath', 'Edm.ModelElementPath', 'Edm.NavigationPropertyPath', 'Edm.PropertyPath', 'Edm.Path' ].includes(resolvedType) )
1234
+ resolvedType = resolvedType.split('.')[1];
1127
1235
 
1128
1236
  return {
1129
1237
  name : typeName,
1130
- jsonName: dTypeName,
1131
- value : val
1238
+ jsonName: resolvedType,
1239
+ value : value
1132
1240
  };
1133
1241
  }
1134
1242
 
@@ -1137,16 +1245,18 @@ function csn2annotationEdm(csn, serviceName, Edm = undefined, options=undefined,
1137
1245
  // dTypeName : name of the expected record type according to vocabulary, may be null
1138
1246
  //
1139
1247
  // can be called for a record directly below a term, or at a deeper level
1140
- function generateRecord(obj, termName, dTypeName, context) {
1248
+ function generateRecord(obj, termName, dTypeName, msg) {
1141
1249
  /** @type {object} */
1142
- let newRecord = new Edm.Record(v);
1250
+ const newRecord = new Edm.Record(v);
1143
1251
 
1144
1252
  // first determine what is the actual type to be used for the record
1145
1253
  if (dTypeName && !isComplexType(dTypeName)) {
1146
1254
  if (!getDictType(dTypeName) && !isPrimitiveType(dTypeName) && !isCollection(dTypeName))
1147
- message(warning, context, `internal error: dictionary inconsistency: type '${ dTypeName }' not found`);
1255
+ message('odata-anno-dict', msg.location,
1256
+ { anno: msg.anno(), type: dTypeName, '#': 'std' });
1148
1257
  else
1149
- message(warning, context, `found complex type, but expected type '${ dTypeName }'`);
1258
+ message('odata-anno-value', msg.location,
1259
+ { anno: msg.anno(), str: 'structured', type: dTypeName, '#': 'struct' });
1150
1260
  return newRecord;
1151
1261
  }
1152
1262
 
@@ -1155,26 +1265,32 @@ function csn2annotationEdm(csn, serviceName, Edm = undefined, options=undefined,
1155
1265
  actualTypeName = obj['$Type'];
1156
1266
  if (!getDictType(actualTypeName)) {
1157
1267
  // this type doesn't exist
1158
- message(warning, context, `explicitly specified type '${ actualTypeName }' not found in vocabulary`);
1159
- // explicitly mentioned type, render in XML and JSON
1160
- newRecord.setEdmAttribute('Type', actualTypeName);
1161
- }
1162
- else if (dTypeName && !isDerivedFrom(actualTypeName, dTypeName)) {
1163
- // this type doesn't fit the expected one
1164
- message(warning, context, `explicitly specified type '${ actualTypeName
1165
- }' is not derived from expected type '${ dTypeName }'`);
1166
- actualTypeName = dTypeName;
1268
+ message('odata-anno-type', msg.location,
1269
+ { anno: msg.anno(), type: actualTypeName, '#': 'unknown' });
1167
1270
  // explicitly mentioned type, render in XML and JSON
1168
1271
  newRecord.setEdmAttribute('Type', actualTypeName);
1169
1272
  }
1170
1273
  else if (isAbstractType(actualTypeName)) {
1171
1274
  // this type is abstract
1172
- message(warning, context, `explicitly specified type '${ actualTypeName }' is abstract, specify a concrete type`);
1275
+ message('odata-anno-type', msg.location,
1276
+ { anno: msg.anno(), type: actualTypeName, '#': 'abstract' });
1173
1277
  if(dTypeName)
1174
1278
  actualTypeName = dTypeName;
1175
1279
  // set to definition name and render in XML and JSON
1176
1280
  newRecord.setEdmAttribute('Type', actualTypeName);
1177
1281
  }
1282
+ else if (dTypeName && !isDerivedFrom(actualTypeName, dTypeName)) {
1283
+ // this type doesn't fit the expected one
1284
+ message('odata-anno-type', msg.location,
1285
+ { anno: msg.anno(),
1286
+ type: actualTypeName,
1287
+ name: dTypeName,
1288
+ code: '$Type',
1289
+ '#': 'derived' });
1290
+ actualTypeName = dTypeName;
1291
+ // explicitly mentioned type, render in XML and JSON
1292
+ newRecord.setEdmAttribute('Type', actualTypeName);
1293
+ }
1178
1294
  else {
1179
1295
  // ok
1180
1296
  // Dictionary Type, render in XML only for backward compatibility
@@ -1191,7 +1307,8 @@ function csn2annotationEdm(csn, serviceName, Edm = undefined, options=undefined,
1191
1307
  actualTypeName = dTypeName;
1192
1308
  }
1193
1309
  if (isAbstractType(actualTypeName))
1194
- message(warning, context, `type '${ dTypeName }' is abstract, use '$Type' to specify a concrete type`);
1310
+ message('odata-anno-type', msg.location,
1311
+ { anno: msg.anno(), type: dTypeName, '#': 'abstract' });
1195
1312
 
1196
1313
  // Dictionary Type, render in XML only for backward compatibility
1197
1314
  newRecord.setXml( { Type: actualTypeName });
@@ -1201,37 +1318,38 @@ function csn2annotationEdm(csn, serviceName, Edm = undefined, options=undefined,
1201
1318
  }
1202
1319
 
1203
1320
  // now the type is clear, so look ath the value
1204
- let dictProperties = getAllProperties(actualTypeName);
1321
+ const dictProperties = getAllProperties(actualTypeName);
1205
1322
 
1206
1323
  // loop over elements
1207
- for (let i of Object.keys(obj)) {
1208
- context.stack.push('.' + i);
1324
+ for (const name of Object.keys(obj)) {
1325
+ msg.stack.push('.' + name);
1209
1326
 
1210
- if (i === '$Type') {
1327
+ if (name === '$Type') {
1211
1328
  // ignore, this is an "artificial" property used to indicate the type
1212
1329
  }
1213
- else if (i.charAt(0) === '@') {
1330
+ else if (name[0] === '@') {
1214
1331
  // not a regular property, but a nested annotation
1215
- let newAnno = handleTerm(i.substring(1, i.length), obj[i], context);
1332
+ const newAnno = handleTerm(name.substring(1, name.length), obj[name], msg);
1216
1333
  newRecord.append(newAnno);
1217
1334
  }
1218
1335
  else {
1219
1336
  // regular property
1220
1337
  let dictPropertyTypeName = null;
1221
1338
  if (dictProperties) {
1222
- dictPropertyTypeName = dictProperties[i];
1223
- if (!dictPropertyTypeName && !getDictType(actualTypeName).OpenType){
1224
- message(warning, context, `record type '${ actualTypeName }' doesn't have a property '${ i }'`);
1339
+ dictPropertyTypeName = dictProperties[name];
1340
+ if (!dictPropertyTypeName && !getDictType(actualTypeName).OpenType) {
1341
+ message('odata-anno-type', msg.location,
1342
+ { name, anno: termName, type: dTypeName });
1225
1343
  }
1226
1344
  }
1227
1345
 
1228
- let newPropertyValue = new Edm.PropertyValue(v, i);
1346
+ let newPropertyValue = new Edm.PropertyValue(v, name);
1229
1347
  // property value can be anything, so delegate handling to handleValue
1230
- handleValue(obj[i], newPropertyValue, termName, dictPropertyTypeName, context);
1348
+ handleValue(obj[name], newPropertyValue, termName, dictPropertyTypeName, msg);
1231
1349
  newRecord.append(newPropertyValue);
1232
1350
  }
1233
1351
 
1234
- context.stack.pop();
1352
+ msg.stack.pop();
1235
1353
  }
1236
1354
 
1237
1355
  return newRecord;
@@ -1240,56 +1358,59 @@ function csn2annotationEdm(csn, serviceName, Edm = undefined, options=undefined,
1240
1358
 
1241
1359
  // annoValue is an array
1242
1360
  // dTypeName : Collection(...) according to dictionary
1243
- function generateCollection(annoValue, termName, dTypeName, context) {
1244
- let newCollection = new Edm.Collection(v);
1361
+ function generateCollection(annoValue, termName, dTypeName, msg) {
1362
+ const newCollection = new Edm.Collection(v);
1245
1363
 
1246
1364
  let innerTypeName = null;
1247
1365
  if (dTypeName) {
1248
- var match = dTypeName.match(/^Collection\((.+)\)/);
1366
+ const match = dTypeName.match(/^Collection\((.+)\)/);
1249
1367
  if (match) {
1250
1368
  innerTypeName = match[1];
1251
1369
  }
1252
1370
  else {
1253
- message(warning, context, `found collection value, but expected non-collection type ${ dTypeName }`);
1371
+ message('odata-anno-value', msg.location,
1372
+ { anno: msg.anno(), str: 'collection', type: dTypeName, '#': 'struct' });
1254
1373
  }
1255
1374
  }
1256
1375
 
1257
1376
  let index = 0;
1258
1377
  for (const value of annoValue) {
1259
- context.stack.push('[' + index + ']');
1378
+ msg.stack.push('[' + index + ']');
1260
1379
  index++
1261
1380
 
1262
1381
  // for dealing with the single array entries we unfortunately cannot call handleValue(),
1263
1382
  // as the values inside an array are represented differently from the values
1264
1383
  // in a record or term
1265
1384
  if (Array.isArray(value)) {
1266
- message(warning, context, 'nested collections are not supported');
1385
+ message('odata-anno-value', msg.location,
1386
+ { anno: msg.anno(), '#': 'nestedcollection' });
1267
1387
  }
1268
1388
  else if (value && typeof value === 'object') {
1269
1389
  if (value['=']) {
1270
- let res = handleExpression(value['='], innerTypeName, context);
1271
- let newPropertyPath = new Edm.ValueThing(v, res.name, res.value );
1390
+ const res = handleExpression(value['='], innerTypeName);
1391
+ const newPropertyPath = new Edm.ValueThing(v, res.name, res.value );
1272
1392
  newPropertyPath.setJSON( { [res.name] : res.value } );
1273
1393
  newCollection.append(newPropertyPath);
1274
1394
  }
1275
1395
  else if (value['#']) {
1276
- message(warning, context, 'enum inside collection is not yet supported');
1396
+ message('odata-anno-value', msg.location,
1397
+ { anno: msg.anno(), '#': 'enumincollection' });
1277
1398
  }
1278
1399
  else if(value['$edmJson']) {
1279
- newCollection.append(handleEdmJson(value['$edmJson'], context));
1400
+ newCollection.append(handleEdmJson(value['$edmJson'], msg));
1280
1401
  }
1281
1402
  else {
1282
- newCollection.append(generateRecord(value, termName, innerTypeName, context));
1403
+ newCollection.append(generateRecord(value, termName, innerTypeName, msg));
1283
1404
  }
1284
1405
  }
1285
1406
  else {
1286
- let res = handleSimpleValue(value, innerTypeName, context);
1287
- let newThing = (value === null) ?new Edm.ValueThing(v, 'Null') : new Edm.ValueThing(v, res.name, value );
1407
+ const res = handleSimpleValue(value, innerTypeName, msg);
1408
+ const newThing = (value === null) ?new Edm.ValueThing(v, 'Null') : new Edm.ValueThing(v, res.name, value );
1288
1409
  newThing.setJSON( { [res.jsonName] : res.value });
1289
1410
  newCollection.append(newThing);
1290
1411
  }
1291
1412
 
1292
- context.stack.pop();
1413
+ msg.stack.pop();
1293
1414
  }
1294
1415
 
1295
1416
  return newCollection;
@@ -1303,7 +1424,7 @@ function csn2annotationEdm(csn, serviceName, Edm = undefined, options=undefined,
1303
1424
  // See example in test/odataAnnotations/smallTests/edmJson_noReverse_ok
1304
1425
  // and test3/ODataBackends/DynExpr
1305
1426
 
1306
- function handleEdmJson(obj, context, exprDef=undefined) {
1427
+ function handleEdmJson(obj, msg, exprDef=undefined) {
1307
1428
 
1308
1429
  let edmNode = undefined;
1309
1430
  if(obj === undefined || obj === null)
@@ -1312,7 +1433,8 @@ function csn2annotationEdm(csn, serviceName, Edm = undefined, options=undefined,
1312
1433
  const dynExprs = edmUtils.intersect(dynamicExpressionNames, Object.keys(obj));
1313
1434
 
1314
1435
  if(dynExprs.length > 1) {
1315
- message(warning, context, `EDM JSON code contains more than one dynamic expression: ${ dynExprs }`);
1436
+ message('odata-anno-value', msg.location,
1437
+ { anno: msg.anno(), rawvalues: dynExprs, '#': 'multexpr' });
1316
1438
  return edmNode;
1317
1439
  }
1318
1440
 
@@ -1328,7 +1450,7 @@ function csn2annotationEdm(csn, serviceName, Edm = undefined, options=undefined,
1328
1450
  if(Array.isArray(obj)) {
1329
1451
  // EDM JSON doesn't mention annotations on collections
1330
1452
  edmNode = new Edm.Collection(v);
1331
- obj.forEach(o => edmNode.append(handleEdmJson(o, context)));
1453
+ obj.forEach(o => edmNode.append(handleEdmJson(o, msg)));
1332
1454
  }
1333
1455
  else if(typeof obj === 'object') {
1334
1456
  edmNode = new Edm.Record(v);
@@ -1342,11 +1464,11 @@ function csn2annotationEdm(csn, serviceName, Edm = undefined, options=undefined,
1342
1464
  let child = undefined;
1343
1465
  const [ head, tail ] = k.split('@');
1344
1466
  if(tail) {
1345
- child = handleTerm(tail, val, context);
1467
+ child = handleTerm(tail, val, msg);
1346
1468
  }
1347
1469
  else {
1348
1470
  child = new Edm.PropertyValue(v, head);
1349
- child.append(handleEdmJson(val, context));
1471
+ child.append(handleEdmJson(val, msg));
1350
1472
  }
1351
1473
  if(child) {
1352
1474
  if(tail && head.length) {
@@ -1385,7 +1507,7 @@ function csn2annotationEdm(csn, serviceName, Edm = undefined, options=undefined,
1385
1507
  forEach(obj, (name, val) => {
1386
1508
  if(exprDef) {
1387
1509
  if(exprDef.anno && name[0] === '@') {
1388
- edmNode.append(handleTerm(name.slice(1), val, context));
1510
+ edmNode.append(handleTerm(name.slice(1), val, msg));
1389
1511
  }
1390
1512
  else if (exprDef.attr && exprDef.attr.includes(name)) {
1391
1513
  if (name[0] === '$') {
@@ -1400,11 +1522,11 @@ function csn2annotationEdm(csn, serviceName, Edm = undefined, options=undefined,
1400
1522
  else if(exprDef.children) {
1401
1523
  if (Array.isArray(val)) {
1402
1524
  val.forEach(a => {
1403
- edmNode.append(handleEdmJson(a, context, exprDef));
1525
+ edmNode.append(handleEdmJson(a, msg, exprDef));
1404
1526
  });
1405
1527
  }
1406
1528
  else {
1407
- edmNode.append(handleEdmJson(val, context, exprDef));
1529
+ edmNode.append(handleEdmJson(val, msg, exprDef));
1408
1530
  }
1409
1531
  }
1410
1532
  }
@@ -1432,6 +1554,130 @@ function csn2annotationEdm(csn, serviceName, Edm = undefined, options=undefined,
1432
1554
  }
1433
1555
  }
1434
1556
 
1557
+ /*
1558
+ translate vocabulary definitions into an adhoc dictionary
1559
+ with the same structure as the gobal jsonDictionary that
1560
+ contains all official term and type definitions.
1561
+
1562
+ Return the dictionary and an array of schemas to which
1563
+ the vocabulary definitions belong
1564
+ */
1565
+ function createAdhocDictionary() {
1566
+ const allKnownVocabularies = [];
1567
+ const dict = { terms: {}, types: {}, xrefs: {} };
1568
+
1569
+ if(!isBetaEnabled(options, 'odataTerms'))
1570
+ return [ dict, allKnownVocabularies ];
1571
+
1572
+ for(let termName in csnVocabularies) {
1573
+ let dictDef = oDataDictionary.terms[termName];
1574
+ if(dictDef) {
1575
+ message('odata-anno-dict', ['vocabularies', termName],
1576
+ { anno: termName, '#': 'redefinition' } );
1577
+ }
1578
+ else if(!dictDef) {
1579
+ const annoDef = csnVocabularies[termName];
1580
+ if(annoDef?.$mySchemaName) {
1581
+ if(!allKnownVocabularies.includes[annoDef.$mySchemaName])
1582
+ allKnownVocabularies.push(annoDef.$mySchemaName);
1583
+ const myServiceRoot = options.whatsMyServiceRootName(annoDef.$mySchemaName);
1584
+ if(!dict.xrefs[myServiceRoot])
1585
+ dict.xrefs[myServiceRoot] = { $myServiceRoot: myServiceRoot, used: false };
1586
+ const edmType = new Edm.TypeBase(options.v, {}, annoDef);
1587
+ dictDef = edmType._edmAttributes;
1588
+ dictDef['$myServiceRoot'] = myServiceRoot;
1589
+ let val = annoDef['@odata.term.AppliesTo'];
1590
+ if(val != null)
1591
+ dictDef.AppliesTo = Array.isArray(val) ? val.map(v=>v['='] || v) : [val['='] || val];
1592
+ val = annoDef['@odata.term.Experimental'];
1593
+ if(val != null)
1594
+ dictDef['$experimental'] = !!val;
1595
+ val = annoDef['@odata.term.Deprecated'];
1596
+ if(val != null) {
1597
+ dictDef['$deprecated'] = !!val;
1598
+ if(typeof val === 'string')
1599
+ dictDef['$deprecationText'] = val;
1600
+ }
1601
+ dict.terms[termName] = dictDef;
1602
+
1603
+ if((annoDef.items?.enum || annoDef.enum) && isBuiltinType(annoDef.items?.type || annoDef.type)) {
1604
+ const enumType = createTypeDefWithAllowedValues(annoDef, annoDef, dictDef.Type, ['vocabularies', termName ]);
1605
+ const tName = termName + '_$$$EnumType$$$$';
1606
+ dict.types[tName] = enumType;
1607
+ dictDef.Type = tName;
1608
+ }
1609
+ else
1610
+ addTypesToDictionary(annoDef);
1611
+ }
1612
+ }
1613
+ }
1614
+ return [ dict, allKnownVocabularies ];
1615
+
1616
+ function addTypesToDictionary(node) {
1617
+ const typeName = node.items?.type || node.type;
1618
+ // for type reuse in x-ref mode, the definition has already been
1619
+ // replaced by a reference object in edmPreprocessor.
1620
+ // Fall back to original type (the one of the other service).
1621
+ const typeDef = reqDefs.definitions[typeName] || reqDefs.definitions[typeName.replace(serviceName + '.', '')];
1622
+ if(typeDef) {
1623
+ let dictDef = { };
1624
+ const elements = typeDef.items?.elements || typeDef.elements;
1625
+ if(elements) {
1626
+ // complex type
1627
+ dictDef['$kind'] = 'ComplexType';
1628
+ dictDef.Properties = new Object(null);
1629
+
1630
+ for(let en in elements) {
1631
+ const elt = elements[en];
1632
+ if(isEdmPropertyRendered(elt, options)) {
1633
+ const edmType = new Edm.TypeBase(options.v, {}, elt);
1634
+ dictDef.Properties[en] = edmType._edmAttributes[edmType._typeName];
1635
+ addTypesToDictionary(elt);
1636
+ }
1637
+ }
1638
+ }
1639
+ else {
1640
+ // type definition
1641
+ const edmType = new Edm.TypeBase(options.v, {}, typeDef);
1642
+ dictDef = createTypeDefWithAllowedValues(node, typeDef, edmType._edmAttributes[edmType._typeName], ['definitions', typeName ]);
1643
+ if(!typeDef.enum)
1644
+ delete dictDef.$Allowed;
1645
+ }
1646
+ dict.types[typeName] = dictDef;
1647
+ }
1648
+ }
1649
+
1650
+ function createTypeDefWithAllowedValues(node, typeDef, UnderlyingType, path) {
1651
+ const dictTypeDef = { $kind: 'TypeDefinition', UnderlyingType, $Allowed: { Values: {}, Symbols: {} } };
1652
+ // create an artificial type that holds the $Allowed enum symbols and values
1653
+ if(node.items && typeDef.enum || typeDef.items?.enum)
1654
+ message('odata-anno-dict-enum', [ 'vocabularies', node.name ],
1655
+ { name: node.name,
1656
+ type: typeDef.name,
1657
+ '#': node.name === typeDef.name ? 'std' : 'type'
1658
+ });
1659
+ const enumDic = (typeDef.items?.enum || typeDef.enum);
1660
+ const baseType = (typeDef.items || typeDef).type;
1661
+ if(baseType !== 'cds.String' && Object.values(enumDic).some(v => !v.val)) {
1662
+ message('odata-anno-dict-enum', path,
1663
+ { name: node.name, type: baseType, '#': 'value' });
1664
+ }
1665
+ else {
1666
+ for(const symbol in enumDic) {
1667
+ const valDic = { '#SymbolicName': symbol };
1668
+ const enumDef = enumDic[symbol];
1669
+ // <Null/> values can't be rendered
1670
+ if(enumDef.val === undefined)
1671
+ valDic.Value = symbol;
1672
+ else if(valDic.val !== null)
1673
+ valDic.Value = enumDef.val;
1674
+ dictTypeDef.$Allowed.Symbols[symbol] = dictTypeDef.$Allowed.Values[symbol] = valDic;
1675
+ }
1676
+ }
1677
+ return dictTypeDef;
1678
+ }
1679
+ }
1680
+
1435
1681
  function initEdmJson() {
1436
1682
  // Static dynamic expression dictionary, loaded with Edm creators
1437
1683
  const dynamicExpressions = {
@@ -1495,25 +1741,25 @@ function csn2annotationEdm(csn, serviceName, Edm = undefined, options=undefined,
1495
1741
  //-------------------------------------------------------------------------------------------------
1496
1742
  //-------------------------------------------------------------------------------------------------
1497
1743
 
1498
- // filter function, assumed to be used for array of string
1499
- // accepts those strings that start with a known vocabulary name
1500
- function filterKnownVocabularies(name) {
1501
- var match = name.match(/^(@)(\w+)/);
1502
- if (match == null) return false;
1503
- return knownVocabularies.includes(match[2]); // second match group
1504
- }
1505
-
1506
- // resolve "derived types"
1744
+ // resolve "derived types"
1507
1745
  // -> if dTypeName is a TypeDefinition, replace by
1508
1746
  // underlying type
1509
- function resolveType(dTypeName) {
1510
- let type = getDictType(dTypeName);
1747
+ function resolveTypeDefinition(dTypeName) {
1748
+ const type = getDictType(dTypeName);
1511
1749
  if (type && type.UnderlyingType && type['$kind'] === 'TypeDefinition') {
1512
1750
  return type.UnderlyingType;
1513
1751
  }
1514
1752
  return dTypeName;
1515
1753
  }
1516
1754
 
1755
+ function stripCollection(typeName) {
1756
+ const match = typeName.match(/^Collection\((.+)\)/);
1757
+ if (match) {
1758
+ typeName = match[1];
1759
+ }
1760
+ return typeName;
1761
+ }
1762
+
1517
1763
  function isPrimitiveType(typeName) {
1518
1764
  return typeName.split('.')[0] === 'Edm';
1519
1765
  }
@@ -1523,17 +1769,17 @@ function csn2annotationEdm(csn, serviceName, Edm = undefined, options=undefined,
1523
1769
  }
1524
1770
 
1525
1771
  function isEnumType(dTypeName) {
1526
- let type = getDictType(dTypeName);
1772
+ const type = getDictType(dTypeName);
1527
1773
  return type && type['$kind'] === 'EnumType';
1528
1774
  }
1529
1775
 
1530
1776
  function isComplexType(dTypeName) {
1531
- let type = getDictType(dTypeName);
1777
+ const type = getDictType(dTypeName);
1532
1778
  return dTypeName === 'Edm.ComplexType' || type && type['$kind'] === 'ComplexType';
1533
1779
  }
1534
1780
 
1535
1781
  function isAbstractType(dTypeName) {
1536
- let type = getDictType(dTypeName);
1782
+ const type = getDictType(dTypeName);
1537
1783
  return type && type['Abstract'] === 'true';
1538
1784
  }
1539
1785