@sap/cds-compiler 3.4.4 → 3.5.2

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