jsii-pacmak 1.63.2 → 1.65.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -36,6 +36,15 @@ const version_1 = require("../lib/version");
36
36
  type: 'boolean',
37
37
  desc: 'generate code only (instead of building and packaging)',
38
38
  default: false,
39
+ })
40
+ .option('runtime-type-checking', {
41
+ type: 'boolean',
42
+ desc: [
43
+ 'generate runtime type checking code where compile-time type checking is not possible.',
44
+ 'Disabling this will generate less code, but will produce less helpful error messages when',
45
+ 'developers pass invalid values to the generated bindings.',
46
+ ].join(' '),
47
+ default: true,
39
48
  })
40
49
  .option('fingerprint', {
41
50
  type: 'boolean',
@@ -154,6 +163,7 @@ const version_1 = require("../lib/version");
154
163
  recurse: argv.recurse,
155
164
  rosettaUnknownSnippets,
156
165
  rosettaTablet: argv['rosetta-tablet'],
166
+ runtimeTypeChecking: argv['runtime-type-checking'],
157
167
  targets: argv.targets?.map((target) => target),
158
168
  updateNpmIgnoreFiles: argv.npmignore,
159
169
  validateAssemblies: argv['validate-assemblies'],
package/lib/builder.d.ts CHANGED
@@ -8,34 +8,39 @@ export interface BuildOptions {
8
8
  * Whether to fingerprint the produced artifacts.
9
9
  * @default true
10
10
  */
11
- fingerprint?: boolean;
11
+ readonly fingerprint?: boolean;
12
12
  /**
13
13
  * Whether artifacts should be re-build even if their fingerprints look up-to-date.
14
14
  * @default false
15
15
  */
16
- force?: boolean;
16
+ readonly force?: boolean;
17
17
  /**
18
18
  * Arguments provided by the user (how they are used is target-dependent)
19
19
  */
20
- arguments: {
20
+ readonly arguments: {
21
21
  readonly [name: string]: any;
22
22
  };
23
23
  /**
24
24
  * Only generate code, don't build
25
25
  */
26
- codeOnly?: boolean;
26
+ readonly codeOnly?: boolean;
27
27
  /**
28
28
  * Whether or not to clean
29
29
  */
30
- clean?: boolean;
30
+ readonly clean?: boolean;
31
31
  /**
32
32
  * Whether to add an additional subdirectory for the target language
33
33
  */
34
- languageSubdirectory?: boolean;
34
+ readonly languageSubdirectory?: boolean;
35
35
  /**
36
36
  * The Rosetta instance to load examples from
37
37
  */
38
- rosetta: Rosetta;
38
+ readonly rosetta: Rosetta;
39
+ /**
40
+ * Whether to generate runtime type checking code in places where compile-time
41
+ * type checking is not possible.
42
+ */
43
+ readonly runtimeTypeChecking: boolean;
39
44
  }
40
45
  /**
41
46
  * Interface for classes that can build language targets
package/lib/builder.js CHANGED
@@ -63,13 +63,14 @@ class IndependentPackageBuilder {
63
63
  }
64
64
  makeTarget(module, options) {
65
65
  return new this.targetConstructor({
66
- targetName: this.targetName,
67
- packageDir: module.moduleDirectory,
66
+ arguments: options.arguments,
68
67
  assembly: module.assembly,
69
68
  fingerprint: options.fingerprint,
70
69
  force: options.force,
71
- arguments: options.arguments,
70
+ packageDir: module.moduleDirectory,
72
71
  rosetta: options.rosetta,
72
+ runtimeTypeChecking: options.runtimeTypeChecking,
73
+ targetName: this.targetName,
73
74
  });
74
75
  }
75
76
  finalOutputDir(module, options) {
@@ -20,6 +20,11 @@ export interface GeneratorOptions {
20
20
  * If this property is set, the generator will add "Base" to abstract class names
21
21
  */
22
22
  addBasePostfixToAbstractClassNames?: boolean;
23
+ /**
24
+ * If this property is set, the generator will add runtime type checking code in places
25
+ * where compile-time type checking is not possible.
26
+ */
27
+ runtimeTypeChecking: boolean;
23
28
  }
24
29
  export interface IGenerator {
25
30
  /**
@@ -72,7 +77,8 @@ export declare abstract class Generator implements IGenerator {
72
77
  private _assembly?;
73
78
  protected _reflectAssembly?: reflect.Assembly;
74
79
  private fingerprint?;
75
- constructor(options?: GeneratorOptions);
80
+ constructor(options: GeneratorOptions);
81
+ protected get runtimeTypeChecking(): boolean;
76
82
  protected get assembly(): spec.Assembly;
77
83
  get reflectAssembly(): reflect.Assembly;
78
84
  get metadata(): {
package/lib/generator.js CHANGED
@@ -13,11 +13,14 @@ const version_1 = require("./version");
13
13
  * Given a jsii module, it will invoke "events" to emit various elements.
14
14
  */
15
15
  class Generator {
16
- constructor(options = {}) {
16
+ constructor(options) {
17
17
  this.options = options;
18
18
  this.excludeTypes = new Array();
19
19
  this.code = new codemaker_1.CodeMaker();
20
20
  }
21
+ get runtimeTypeChecking() {
22
+ return this.options.runtimeTypeChecking;
23
+ }
21
24
  get assembly() {
22
25
  if (!this._assembly) {
23
26
  throw new Error('No assembly has been loaded! The #load() method must be called first!');
package/lib/index.d.ts CHANGED
@@ -6,7 +6,7 @@ export { configure as configureLogging } from './logging';
6
6
  /**
7
7
  * Generates code in the desired targets.
8
8
  */
9
- export declare function pacmak({ argv, clean, codeOnly, fingerprint, force, forceSubdirectory, forceTarget, inputDirectories, outputDirectory, parallel, recurse, rosettaTablet, targets, timers, rosettaUnknownSnippets, updateNpmIgnoreFiles, validateAssemblies, }: PacmakOptions): Promise<void>;
9
+ export declare function pacmak({ argv, clean, codeOnly, fingerprint, force, forceSubdirectory, forceTarget, inputDirectories, outputDirectory, parallel, recurse, rosettaTablet, rosettaUnknownSnippets, runtimeTypeChecking, targets, timers, updateNpmIgnoreFiles, validateAssemblies, }: PacmakOptions): Promise<void>;
10
10
  /**
11
11
  * Options provided to the `pacmak` function.
12
12
  */
@@ -93,6 +93,12 @@ export interface PacmakOptions {
93
93
  * @default undefined
94
94
  */
95
95
  readonly rosettaTablet?: string;
96
+ /**
97
+ * Whether to inject runtime type checks in places where compile-time type checking is not performed.
98
+ *
99
+ * @default true
100
+ */
101
+ readonly runtimeTypeChecking?: boolean;
96
102
  /**
97
103
  * The list of targets for which code should be generated. Unless `forceTarget` is `true`, a given target will only
98
104
  * be generated for assemblies that have configured it.
package/lib/index.js CHANGED
@@ -16,7 +16,7 @@ Object.defineProperty(exports, "configureLogging", { enumerable: true, get: func
16
16
  /**
17
17
  * Generates code in the desired targets.
18
18
  */
19
- async function pacmak({ argv = {}, clean = true, codeOnly = false, fingerprint = true, force = false, forceSubdirectory = true, forceTarget = false, inputDirectories, outputDirectory, parallel = true, recurse = false, rosettaTablet, targets = Object.values(targets_1.TargetName), timers = new timer_1.Timers(), rosettaUnknownSnippets = undefined, updateNpmIgnoreFiles = false, validateAssemblies = false, }) {
19
+ async function pacmak({ argv = {}, clean = true, codeOnly = false, fingerprint = true, force = false, forceSubdirectory = true, forceTarget = false, inputDirectories, outputDirectory, parallel = true, recurse = false, rosettaTablet, rosettaUnknownSnippets = undefined, runtimeTypeChecking = true, targets = Object.values(targets_1.TargetName), timers = new timer_1.Timers(), updateNpmIgnoreFiles = false, validateAssemblies = false, }) {
20
20
  const rosetta = new jsii_rosetta_1.Rosetta({
21
21
  unknownSnippets: rosettaUnknownSnippets,
22
22
  prefixDisclaimer: true,
@@ -74,6 +74,7 @@ async function pacmak({ argv = {}, clean = true, codeOnly = false, fingerprint =
74
74
  force,
75
75
  perLanguageDirectory,
76
76
  rosetta,
77
+ runtimeTypeChecking,
77
78
  }))
78
79
  .then(() => logging.info(`${targetSet.targetType} finished`), (err) => {
79
80
  logging.warn(`${targetSet.targetType} failed`);
@@ -95,20 +96,21 @@ async function pacmak({ argv = {}, clean = true, codeOnly = false, fingerprint =
95
96
  exports.pacmak = pacmak;
96
97
  //#endregion
97
98
  //#region Building
98
- async function buildTargetsForLanguage(targetLanguage, modules, { argv, clean, codeOnly, fingerprint, force, perLanguageDirectory, rosetta, }) {
99
+ async function buildTargetsForLanguage(targetLanguage, modules, { argv, clean, codeOnly, fingerprint, force, perLanguageDirectory, rosetta, runtimeTypeChecking, }) {
99
100
  // ``argv.target`` is guaranteed valid by ``yargs`` through the ``choices`` directive.
100
101
  const factory = targets_1.ALL_BUILDERS[targetLanguage];
101
102
  if (!factory) {
102
103
  throw new Error(`Unsupported target: '${targetLanguage}'`);
103
104
  }
104
105
  return factory(modules, {
106
+ arguments: argv,
105
107
  clean: clean,
106
108
  codeOnly: codeOnly,
107
- rosetta,
108
- force: force,
109
109
  fingerprint: fingerprint,
110
- arguments: argv,
110
+ force: force,
111
111
  languageSubdirectory: perLanguageDirectory,
112
+ rosetta,
113
+ runtimeTypeChecking,
112
114
  }).buildModules();
113
115
  }
114
116
  function sliceTargets(modulesSorted, requestedTargets, force) {
package/lib/target.d.ts CHANGED
@@ -12,6 +12,7 @@ export declare abstract class Target {
12
12
  protected readonly targetName: string;
13
13
  protected readonly assembly: reflect.Assembly;
14
14
  protected readonly rosetta: Rosetta;
15
+ protected readonly runtimeTypeChecking: boolean;
15
16
  protected abstract readonly generator: IGenerator;
16
17
  constructor(options: TargetOptions);
17
18
  /**
@@ -125,6 +126,8 @@ export interface TargetOptions {
125
126
  assembly: reflect.Assembly;
126
127
  /** The Rosetta instance */
127
128
  rosetta: Rosetta;
129
+ /** Whether to generate runtime type-checking code */
130
+ runtimeTypeChecking: boolean;
128
131
  /**
129
132
  * Whether to fingerprint the produced artifacts.
130
133
  * @default true
package/lib/target.js CHANGED
@@ -8,12 +8,13 @@ const dependency_graph_1 = require("./dependency-graph");
8
8
  const logging = require("./logging");
9
9
  class Target {
10
10
  constructor(options) {
11
- this.packageDir = options.packageDir;
11
+ this.arguments = options.arguments;
12
12
  this.assembly = options.assembly;
13
- this.rosetta = options.rosetta;
14
13
  this.fingerprint = options.fingerprint ?? true;
15
14
  this.force = options.force ?? false;
16
- this.arguments = options.arguments;
15
+ this.packageDir = options.packageDir;
16
+ this.rosetta = options.rosetta;
17
+ this.runtimeTypeChecking = options.runtimeTypeChecking;
17
18
  this.targetName = options.targetName;
18
19
  }
19
20
  /**
@@ -7,13 +7,16 @@ import { Generator, Legalese } from '../../generator';
7
7
  */
8
8
  export declare class DotNetGenerator extends Generator {
9
9
  private readonly assembliesCurrentlyBeingCompiled;
10
+ private readonly nameutils;
10
11
  private readonly rosetta;
11
12
  private firstMemberWritten;
12
13
  private typeresolver;
13
- private readonly nameutils;
14
14
  private dotnetRuntimeGenerator;
15
15
  private dotnetDocGenerator;
16
- constructor(assembliesCurrentlyBeingCompiled: string[], rosetta: Rosetta);
16
+ constructor(assembliesCurrentlyBeingCompiled: string[], options: {
17
+ readonly rosetta: Rosetta;
18
+ readonly runtimeTypeChecking: boolean;
19
+ });
17
20
  load(packageRoot: string, assembly: reflect.Assembly): Promise<void>;
18
21
  /**
19
22
  * Runs the generator (in-memory).
@@ -55,6 +58,13 @@ export declare class DotNetGenerator extends Generator {
55
58
  protected onEnumMember(enm: spec.EnumType, member: spec.EnumMember): void;
56
59
  private namespaceFor;
57
60
  private emitMethod;
61
+ /**
62
+ * Emits type checks for values passed for type union parameters.
63
+ *
64
+ * @param parameters the list of parameters received by the function.
65
+ * @param noMangle use parameter names as-is (useful for setters, for example) instead of mangling them.
66
+ */
67
+ private emitUnionParameterValdation;
58
68
  /**
59
69
  * Founds out if a member (property or method) is already defined in one of the base classes
60
70
  *
@@ -3,9 +3,13 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.DotNetGenerator = void 0;
4
4
  const spec = require("@jsii/spec");
5
5
  const clone = require("clone");
6
+ const crypto_1 = require("crypto");
6
7
  const fs = require("fs-extra");
8
+ const http = require("http");
9
+ const https = require("https");
7
10
  const path = require("path");
8
11
  const generator_1 = require("../../generator");
12
+ const logging_1 = require("../../logging");
9
13
  const dotnetdocgenerator_1 = require("./dotnetdocgenerator");
10
14
  const dotnetruntimegenerator_1 = require("./dotnetruntimegenerator");
11
15
  const dotnettyperesolver_1 = require("./dotnettyperesolver");
@@ -15,18 +19,18 @@ const nameutils_1 = require("./nameutils");
15
19
  * CODE GENERATOR V2
16
20
  */
17
21
  class DotNetGenerator extends generator_1.Generator {
18
- constructor(assembliesCurrentlyBeingCompiled, rosetta) {
19
- super();
22
+ constructor(assembliesCurrentlyBeingCompiled, options) {
23
+ super(options);
20
24
  this.assembliesCurrentlyBeingCompiled = assembliesCurrentlyBeingCompiled;
21
- this.rosetta = rosetta;
25
+ this.nameutils = new nameutils_1.DotNetNameUtils();
22
26
  // Flags that tracks if we have already wrote the first member of the class
23
27
  this.firstMemberWritten = false;
24
- this.nameutils = new nameutils_1.DotNetNameUtils();
25
28
  // Override the openBlock to get a correct C# looking code block with the curly brace after the line
26
29
  this.code.openBlock = function (text) {
27
30
  this.line(text);
28
31
  this.open('{');
29
32
  };
33
+ this.rosetta = options.rosetta;
30
34
  }
31
35
  async load(packageRoot, assembly) {
32
36
  await super.load(packageRoot, assembly);
@@ -48,7 +52,6 @@ class DotNetGenerator extends generator_1.Generator {
48
52
  const tarballFileName = path.basename(tarball);
49
53
  const filegen = new filegenerator_1.FileGenerator(this.assembly, tarballFileName, this.code);
50
54
  filegen.generateAssemblyInfoFile();
51
- filegen.generateProjectFile(this.typeresolver.namespaceDependencies);
52
55
  // Calling super.save() dumps the tarball in the format name@version.jsii.tgz.
53
56
  // This is not in sync with the Old .NET generator where the name is scope-name-version.tgz.
54
57
  // Hence we are saving the files ourselves here:
@@ -59,6 +62,17 @@ class DotNetGenerator extends generator_1.Generator {
59
62
  }
60
63
  await fs.mkdirp(path.join(outdir, packageId));
61
64
  await fs.copyFile(tarball, path.join(outdir, packageId, tarballFileName));
65
+ // Attempt to download the package icon from the configured URL so we can use the non-deprecated PackageIcon
66
+ // attribute. If this fails or is opted out (via $JSII_PACMAK_DOTNET_NO_DOWNLOAD_ICON being set), then only the
67
+ // deprecated PackageIconUrl will be emitted.
68
+ const iconFile = this.assembly.targets?.dotnet?.iconUrl != null &&
69
+ !process.env.JSII_PACMAK_DOTNET_NO_DOWNLOAD_ICON
70
+ ? await tryDownloadResource(this.assembly.targets.dotnet.iconUrl, path.join(outdir, packageId)).catch((err) => {
71
+ (0, logging_1.debug)(`[dotnet] Unable to download package icon, will only use deprecated PackageIconUrl attribute: ${err.cause}`);
72
+ return Promise.resolve(undefined);
73
+ })
74
+ : undefined;
75
+ filegen.generateProjectFile(this.typeresolver.namespaceDependencies, iconFile);
62
76
  // Create an anchor file for the current model
63
77
  this.generateDependencyAnchorFile();
64
78
  if (license) {
@@ -251,10 +265,18 @@ class DotNetGenerator extends generator_1.Generator {
251
265
  // Create the constructors:
252
266
  // Abstract classes have protected constructors.
253
267
  const visibility = cls.abstract ? 'protected' : 'public';
268
+ this.code.openBlock(`${visibility} ${className}(${parametersDefinition}): base(_MakeDeputyProps(${parametersBase}))`);
269
+ this.code.closeBlock();
270
+ this.code.line();
271
+ // This private method is injected so we can validate arguments before deferring to the base constructor, where
272
+ // the instance will be created in the kernel (where it'd fail on a sub-optimal error instead)...
273
+ this.code.line('[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]');
274
+ this.code.openBlock(`private static DeputyProps _MakeDeputyProps(${parametersDefinition})`);
275
+ this.emitUnionParameterValdation(initializer.parameters);
254
276
  const args = parametersBase.length > 0
255
277
  ? `new object?[]{${parametersBase}}`
256
278
  : `System.Array.Empty<object?>()`;
257
- this.code.openBlock(`${visibility} ${className}(${parametersDefinition}): base(new DeputyProps(${args}))`);
279
+ this.code.line(`return new DeputyProps(${args});`);
258
280
  this.code.closeBlock();
259
281
  this.code.line();
260
282
  }
@@ -410,10 +432,125 @@ class DotNetGenerator extends generator_1.Generator {
410
432
  }
411
433
  else {
412
434
  this.code.openBlock(`${access} ${staticKeyWord}${overrideKeyWord}${virtualKeyWord}${signature}`);
435
+ this.emitUnionParameterValdation(method.parameters);
413
436
  this.code.line(this.dotnetRuntimeGenerator.createInvokeMethodIdentifier(method, cls));
414
437
  this.code.closeBlock();
415
438
  }
416
439
  }
440
+ /**
441
+ * Emits type checks for values passed for type union parameters.
442
+ *
443
+ * @param parameters the list of parameters received by the function.
444
+ * @param noMangle use parameter names as-is (useful for setters, for example) instead of mangling them.
445
+ */
446
+ emitUnionParameterValdation(parameters, { noMangle = false } = {}) {
447
+ if (!this.runtimeTypeChecking) {
448
+ // We were configured not to emit those, so bail out now.
449
+ return;
450
+ }
451
+ const unionParameters = parameters?.filter(({ type }) => containsUnionType(type));
452
+ if (unionParameters == null || unionParameters.length === 0) {
453
+ return;
454
+ }
455
+ this.code.openBlock('if (Amazon.JSII.Runtime.Configuration.RuntimeTypeChecking)');
456
+ for (const param of unionParameters) {
457
+ const name = noMangle
458
+ ? param.name
459
+ : this.nameutils.convertParameterName(param.name);
460
+ if (param.optional) {
461
+ this.code.openBlock(`if (${name} != null)`);
462
+ }
463
+ validate.call(this, name, noMangle ? name : `argument {nameof(${name})}`, param.type, noMangle ? name : `{nameof(${name})}`);
464
+ if (param.optional) {
465
+ this.code.closeBlock();
466
+ }
467
+ }
468
+ this.code.closeBlock();
469
+ function validate(value, descr, type, parameterName) {
470
+ if (spec.isUnionTypeReference(type)) {
471
+ validateTypeUnion.call(this, value, descr, type, parameterName);
472
+ }
473
+ else {
474
+ const collectionType = type;
475
+ if (collectionType.collection.kind === spec.CollectionKind.Array) {
476
+ validateArray.call(this, value, descr, collectionType.collection.elementtype, parameterName);
477
+ }
478
+ else if (collectionType.collection.kind === spec.CollectionKind.Map) {
479
+ validateMap.call(this, value, descr, collectionType.collection.elementtype, parameterName);
480
+ }
481
+ else {
482
+ throw new Error(`Unhandled collection kind: ${spec.describeTypeReference(type)}`);
483
+ }
484
+ }
485
+ }
486
+ function validateArray(value, descr, elementType, parameterName) {
487
+ const varName = `__idx_${(0, crypto_1.createHash)('sha256')
488
+ .update(descr)
489
+ .digest('hex')
490
+ .slice(0, 6)}`;
491
+ this.code.openBlock(`for (int ${varName} = 0 ; ${varName} < ${value}.Length ; ${varName}++)`);
492
+ validate.call(this, `${value}[${varName}]`, `${descr}[{${varName}}]`, elementType, parameterName);
493
+ this.code.closeBlock();
494
+ }
495
+ function validateMap(value, descr, elementType, parameterName) {
496
+ const varName = `__item_${(0, crypto_1.createHash)('sha256')
497
+ .update(descr)
498
+ .digest('hex')
499
+ .slice(0, 6)}`;
500
+ this.code.openBlock(`foreach (var ${varName} in ${value})`);
501
+ validate.call(this, `${varName}.Value`, `${descr}[\\"{${varName}.Key}\\"]`, elementType, parameterName);
502
+ this.code.closeBlock();
503
+ }
504
+ function validateTypeUnion(value, descr, type, parameterName) {
505
+ this.code.indent('if (');
506
+ let emitAnd = false;
507
+ const typeRefs = type.union.types;
508
+ for (const typeRef of typeRefs) {
509
+ const prefix = emitAnd ? '&& ' : '';
510
+ const dotNetType = this.typeresolver.toDotNetType(typeRef);
511
+ // In the case of double, we test for all standard numeric types of .NET (these implicitly convert).
512
+ const test = dotNetType === 'double'
513
+ ? [
514
+ 'byte',
515
+ 'decimal',
516
+ 'double',
517
+ 'float',
518
+ 'int',
519
+ 'long',
520
+ 'sbyte',
521
+ 'short',
522
+ 'uint',
523
+ 'ulong',
524
+ 'ushort',
525
+ ]
526
+ .map((numeric) => `${value} is ${numeric}`)
527
+ .join(' || ')
528
+ : `${value} is ${dotNetType}`;
529
+ this.code.line(`${prefix}!(${test})`);
530
+ emitAnd = true;
531
+ }
532
+ if (typeRefs.some((ref) => spec.isNamedTypeReference(ref) &&
533
+ spec.isInterfaceType(this.findType(ref.fqn)))) {
534
+ // AnonymousObject will convert to any interface type, even if unsafely. It is the opaque
535
+ // type returned when a non-intrinsically typed value is passed through an any or union
536
+ // return point. We basically cannot type-check that at runtime in a complete way.
537
+ this.code.line(`&& !(${value} is Amazon.JSII.Runtime.Deputy.AnonymousObject)`);
538
+ }
539
+ this.code.unindent(')');
540
+ this.code.openBlock('');
541
+ const placeholders = typeRefs
542
+ .map((typeRef) => {
543
+ const typeName = this.typeresolver.toDotNetTypeName(typeRef);
544
+ if (typeName.startsWith('"') && typeName.endsWith('"')) {
545
+ return typeName.slice(1, -1);
546
+ }
547
+ return `{${typeName}}`;
548
+ })
549
+ .join(', ');
550
+ this.code.line(`throw new System.ArgumentException($"Expected ${descr} to be one of: ${placeholders}; received {${value}.GetType().FullName}", $"${parameterName}");`);
551
+ this.code.closeBlock();
552
+ }
553
+ }
417
554
  /**
418
555
  * Founds out if a member (property or method) is already defined in one of the base classes
419
556
  *
@@ -546,7 +683,8 @@ class DotNetGenerator extends generator_1.Generator {
546
683
  : // An abstract class could extend a concrete class... We must walk up the inheritance tree in this case...
547
684
  this.proxyMustUseNewModifier(base)));
548
685
  }
549
- return (type.interfaces?.find((fqn) => this.findType(fqn).assembly === type.assembly) != null);
686
+ return (type.interfaces != null &&
687
+ type.interfaces.some((fqn) => this.findType(fqn).assembly === type.assembly));
550
688
  }
551
689
  /**
552
690
  * Emits an Interface Datatype class
@@ -670,6 +808,17 @@ class DotNetGenerator extends generator_1.Generator {
670
808
  const access = this.renderAccessLevel(prop);
671
809
  const staticKeyWord = prop.static ? 'static ' : '';
672
810
  const propName = this.nameutils.convertPropertyName(prop.name);
811
+ const propTypeFQN = this.typeresolver.toDotNetType(prop.type);
812
+ const isOptional = prop.optional ? '?' : '';
813
+ // We need to use a backing field so we can perform type checking if the property type is a union, and this is a struct.
814
+ const backingFieldName = spec.isInterfaceType(cls) && datatype && containsUnionType(prop.type)
815
+ ? // We down-case the first letter, private fields are conventionally named with a _ prefix, and a camelCase name.
816
+ `_${propName.replace(/[A-Z]/, (c) => c.toLowerCase())}`
817
+ : undefined;
818
+ if (backingFieldName != null) {
819
+ this.code.line(`private ${propTypeFQN}${isOptional} ${backingFieldName};`);
820
+ this.code.line();
821
+ }
673
822
  this.dotnetDocGenerator.emitDocs(prop, {
674
823
  api: 'member',
675
824
  fqn: definingType.fqn,
@@ -701,12 +850,13 @@ class DotNetGenerator extends generator_1.Generator {
701
850
  isVirtualKeyWord = 'virtual ';
702
851
  }
703
852
  }
704
- const propTypeFQN = this.typeresolver.toDotNetType(prop.type);
705
- const isOptional = prop.optional ? '?' : '';
706
853
  const statement = `${access} ${isAbstractKeyword}${isVirtualKeyWord}${staticKeyWord}${isOverrideKeyWord}${propTypeFQN}${isOptional} ${propName}`;
707
854
  this.code.openBlock(statement);
708
855
  // Emit getters
709
- if (datatype || prop.const || prop.abstract) {
856
+ if (backingFieldName != null) {
857
+ this.code.line(`get => ${backingFieldName};`);
858
+ }
859
+ else if (datatype || prop.const || prop.abstract) {
710
860
  this.code.line('get;');
711
861
  }
712
862
  else {
@@ -720,16 +870,34 @@ class DotNetGenerator extends generator_1.Generator {
720
870
  }
721
871
  }
722
872
  // Emit setters
723
- if (datatype || (!prop.immutable && prop.abstract)) {
873
+ if (backingFieldName) {
874
+ this.code.openBlock('set');
875
+ this.emitUnionParameterValdation([
876
+ {
877
+ name: 'value',
878
+ type: prop.type,
879
+ optional: prop.optional,
880
+ },
881
+ ], { noMangle: true });
882
+ this.code.line(`${backingFieldName} = value;`);
883
+ this.code.closeBlock();
884
+ }
885
+ else if (datatype || (!prop.immutable && prop.abstract)) {
724
886
  this.code.line('set;');
725
887
  }
726
888
  else {
727
889
  if (!prop.immutable) {
728
- if (prop.static) {
729
- this.code.line(`set => SetStaticProperty(typeof(${className}), value);`);
890
+ const setCode = prop.static
891
+ ? `SetStaticProperty(typeof(${className}), value);`
892
+ : 'SetInstanceProperty(value);';
893
+ if (containsUnionType(prop.type)) {
894
+ this.code.openBlock('set');
895
+ this.emitUnionParameterValdation([{ name: 'value', optional: prop.optional, type: prop.type }], { noMangle: true });
896
+ this.code.line(setCode);
897
+ this.code.closeBlock();
730
898
  }
731
899
  else {
732
- this.code.line('set => SetInstanceProperty(value);');
900
+ this.code.line(`set => ${setCode}`);
733
901
  }
734
902
  }
735
903
  }
@@ -872,4 +1040,100 @@ class DotNetGenerator extends generator_1.Generator {
872
1040
  }
873
1041
  }
874
1042
  exports.DotNetGenerator = DotNetGenerator;
1043
+ async function tryDownloadResource(urlText, into) {
1044
+ const url = new URL(urlText);
1045
+ let request;
1046
+ switch (url.protocol) {
1047
+ case 'http:':
1048
+ request = http.get;
1049
+ break;
1050
+ case 'https:':
1051
+ request = https.get;
1052
+ break;
1053
+ default:
1054
+ // Unhandled protocol... ignoring
1055
+ (0, logging_1.debug)(`Unsupported URL protocol for resource download: ${url.protocol} (full URL: ${urlText})`);
1056
+ return undefined;
1057
+ }
1058
+ return new Promise((ok, ko) => request(url, (res) => {
1059
+ switch (res.statusCode) {
1060
+ case 200:
1061
+ let fileName = path.basename(url.pathname);
1062
+ // Ensure there is a content-appropriate extension on the result...
1063
+ switch (res.headers['content-type']) {
1064
+ case 'image/gif':
1065
+ if (!fileName.endsWith('.gif')) {
1066
+ fileName = `${fileName}.gif`;
1067
+ }
1068
+ break;
1069
+ case 'image/jpeg':
1070
+ if (!fileName.endsWith('.jpg')) {
1071
+ fileName = `${fileName}.jpg`;
1072
+ }
1073
+ break;
1074
+ case 'image/png':
1075
+ if (!fileName.endsWith('.png')) {
1076
+ fileName = `${fileName}.png`;
1077
+ }
1078
+ break;
1079
+ default:
1080
+ // Nothing to do...
1081
+ }
1082
+ const filePath = path.join('resources', fileName);
1083
+ try {
1084
+ fs.mkdirpSync(path.join(into, 'resources'));
1085
+ }
1086
+ catch (err) {
1087
+ return ko(err);
1088
+ }
1089
+ try {
1090
+ const fd = fs.openSync(path.join(into, filePath), fs.constants.O_CREAT |
1091
+ fs.constants.O_TRUNC |
1092
+ fs.constants.O_WRONLY);
1093
+ res
1094
+ .once('error', (cause) => {
1095
+ try {
1096
+ fs.closeSync(fd);
1097
+ }
1098
+ catch {
1099
+ // IGNORE
1100
+ }
1101
+ ko(cause);
1102
+ })
1103
+ .on('data', (chunk) => {
1104
+ const buff = Buffer.from(chunk);
1105
+ let offset = 0;
1106
+ while (offset < buff.length) {
1107
+ try {
1108
+ offset += fs.writeSync(fd, buff, offset);
1109
+ }
1110
+ catch (err) {
1111
+ return ko(err);
1112
+ }
1113
+ }
1114
+ })
1115
+ .once('close', () => {
1116
+ try {
1117
+ fs.closeSync(fd);
1118
+ ok(filePath);
1119
+ }
1120
+ catch (err) {
1121
+ ko(err);
1122
+ }
1123
+ });
1124
+ }
1125
+ catch (err) {
1126
+ return ko(err);
1127
+ }
1128
+ break;
1129
+ default:
1130
+ ko(new Error(`GET ${urlText} -- HTTP ${res.statusCode ?? 0} (${res.statusMessage ?? 'Unknown Error'})`));
1131
+ }
1132
+ }).once('error', ko));
1133
+ }
1134
+ function containsUnionType(typeRef) {
1135
+ return (spec.isUnionTypeReference(typeRef) ||
1136
+ (spec.isCollectionTypeReference(typeRef) &&
1137
+ containsUnionType(typeRef.collection.elementtype)));
1138
+ }
875
1139
  //# sourceMappingURL=dotnetgenerator.js.map