@kurotako/gen-angular 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -99,6 +99,18 @@ function zodSymbol(zod, namespace, entity, role) {
99
99
  function zodModule(zod, namespace, entity) {
100
100
  return zodEntity(zod, namespace, entity).module;
101
101
  }
102
+ function zodRefType(zod, namespace, ref) {
103
+ const key = entityKey(namespace, ref);
104
+ const entry = zod.entities[key];
105
+ if (entry === void 0) {
106
+ throw new MissingZodSymbolError(key, "type");
107
+ }
108
+ const id = entry.symbols.type;
109
+ if (id === void 0) {
110
+ throw new MissingZodSymbolError(key, "type");
111
+ }
112
+ return { typeName: id, module: entry.module };
113
+ }
102
114
  function zodExtra(zod) {
103
115
  return zod.extra;
104
116
  }
@@ -225,6 +237,36 @@ function importStmt(spec, names, typeOnly) {
225
237
 
226
238
  // src/render/controls.ts
227
239
  import { resolveEnum } from "@kurotako/ir";
240
+
241
+ // src/render/unions.ts
242
+ import { flattenUnion } from "@kurotako/ir";
243
+ function discriminatedUnion(field, source) {
244
+ const type = field.type;
245
+ if (type.kind !== "union" || type.discriminator?.mapping === void 0) {
246
+ return void 0;
247
+ }
248
+ if (field.list || field.nullable) {
249
+ return void 0;
250
+ }
251
+ const variants = [];
252
+ for (const [value, ref] of Object.entries(type.discriminator.mapping)) {
253
+ if (source.entities[ref] === void 0) {
254
+ return void 0;
255
+ }
256
+ variants.push({ value, entity: ref });
257
+ }
258
+ return variants.length >= 2 ? { discriminator: type.discriminator.propertyName, variants } : void 0;
259
+ }
260
+ function refNames(type, into = /* @__PURE__ */ new Set()) {
261
+ if (type.kind === "ref") {
262
+ into.add(type.ref);
263
+ } else if (type.kind === "union") {
264
+ for (const variant of type.variants) {
265
+ refNames(variant, into);
266
+ }
267
+ }
268
+ return into;
269
+ }
228
270
  var SCALAR_BASE = {
229
271
  string: "string",
230
272
  uuid: "string",
@@ -238,18 +280,71 @@ var SCALAR_BASE = {
238
280
  datetime: "Date",
239
281
  json: "unknown"
240
282
  };
241
- function baseType(field, zodEnumTypeName) {
283
+ function variantType(type, refTypeName, enumTypeName, cyclicRefs) {
284
+ switch (type.kind) {
285
+ case "scalar":
286
+ return SCALAR_BASE[type.scalar] ?? "unknown";
287
+ case "enum":
288
+ return enumTypeName(type.ref);
289
+ case "ref":
290
+ return refTypeName(type.ref);
291
+ case "unknown":
292
+ return "unknown";
293
+ case "union":
294
+ return unionType(type, refTypeName, enumTypeName, cyclicRefs).text;
295
+ }
296
+ }
297
+ function unionType(type, refTypeName, enumTypeName, cyclicRefs = /* @__PURE__ */ new Set()) {
298
+ const variants = flattenUnion(type);
299
+ if (variants.length === 0) {
300
+ return { text: "unknown", recursive: false };
301
+ }
302
+ const recursive = [...refNames(type)].some((ref) => cyclicRefs.has(ref));
303
+ if (recursive) {
304
+ return { text: "unknown", recursive: true };
305
+ }
306
+ const text = variants.map((variant) => {
307
+ const inner = variantType(variant, refTypeName, enumTypeName, cyclicRefs);
308
+ return variant.kind === "union" ? `(${inner})` : inner;
309
+ }).join(" | ");
310
+ return { text, recursive: false };
311
+ }
312
+
313
+ // src/render/controls.ts
314
+ var SCALAR_BASE2 = {
315
+ string: "string",
316
+ uuid: "string",
317
+ decimal: "string",
318
+ bytes: "string",
319
+ int: "number",
320
+ float: "number",
321
+ bigint: "bigint",
322
+ boolean: "boolean",
323
+ date: "Date",
324
+ datetime: "Date",
325
+ json: "unknown"
326
+ };
327
+ function baseType(field, resolvers) {
242
328
  switch (field.type.kind) {
243
329
  case "scalar":
244
- return SCALAR_BASE[field.type.scalar];
330
+ return SCALAR_BASE2[field.type.scalar];
245
331
  case "enum":
246
- return zodEnumTypeName(field.type.ref);
332
+ return resolvers.enumTypeName(field.type.ref);
247
333
  case "unknown":
248
334
  return "unknown";
335
+ case "ref":
336
+ return resolvers.refTypeName(field.type.ref);
337
+ case "union":
338
+ return unionType(
339
+ field.type,
340
+ resolvers.refTypeName,
341
+ resolvers.enumTypeName,
342
+ resolvers.cyclicRefs
343
+ ).text;
249
344
  }
250
345
  }
251
- function controlType(field, zodEnumTypeName) {
252
- let t = baseType(field, zodEnumTypeName);
346
+ function controlType(field, resolvers) {
347
+ let t = baseType(field, resolvers);
253
348
  if (field.list) {
254
349
  t = `${t}[]`;
255
350
  }
@@ -305,12 +400,18 @@ function controlExpr(field, typeArg, sourceExpr) {
305
400
  if (field.nullable) {
306
401
  return `new FormControl<${typeArg}>(${sourceExpr})`;
307
402
  }
403
+ if (field.type.kind === "union") {
404
+ return `new FormControl<${typeArg}>((${sourceExpr}) as ${typeArg}) /* union: validated by zodValidator(schema) */`;
405
+ }
406
+ if (field.type.kind === "ref") {
407
+ return `new FormControl<${typeArg}>((${sourceExpr}) as ${typeArg})`;
408
+ }
308
409
  return `new FormControl(${sourceExpr}, { nonNullable: true })`;
309
410
  }
310
- function fieldControlEntry(field, zodEnumTypeName) {
411
+ function fieldControlEntry(field, resolvers) {
311
412
  return {
312
413
  name: field.name,
313
- fullType: `FormControl<${controlType(field, zodEnumTypeName)}>`
414
+ fullType: `FormControl<${controlType(field, resolvers)}>`
314
415
  };
315
416
  }
316
417
  function controlsInterface(interfaceName, entries) {
@@ -365,10 +466,42 @@ function variantFields(entity, variant) {
365
466
  }
366
467
 
367
468
  // src/render/reactive.ts
469
+ function discriminatedControlEntry(union, fieldName, variant, family) {
470
+ const lit = union.variants.map((v2) => JSON.stringify(v2.value)).join(" | ");
471
+ const subs = union.variants.map(
472
+ (v2) => `${JSON.stringify(v2.value)}: FormGroup<${controlsTypeName(v2.entity, variant, family)}>`
473
+ ).join("; ");
474
+ return {
475
+ name: fieldName,
476
+ fullType: `FormGroup<{ ${JSON.stringify(union.discriminator)}: FormControl<${lit}>; ${subs} }>`
477
+ };
478
+ }
479
+ function discriminatedFactoryExpr(union, fieldName, variant, deep, zod, namespace, injectedFactories) {
480
+ const lit = union.variants.map((v2) => JSON.stringify(v2.value)).join(" | ");
481
+ const first = JSON.stringify(union.variants[0]?.value ?? "");
482
+ const method = factoryMethod(variant);
483
+ const subLines = union.variants.map((v2) => {
484
+ const param = injectedFactories.get(v2.entity) ?? `${lowerFirst2(v2.entity)}FormFactory`;
485
+ if (variant === "Create") {
486
+ return ` ${JSON.stringify(v2.value)}: this.${param}.${method}(),`;
487
+ }
488
+ const role = deep ? "updateDeepType" : "updateType";
489
+ const dto = zodSymbol(zod, namespace, v2.entity, role);
490
+ return ` ${JSON.stringify(v2.value)}: this.${param}.${method}(value.${fieldName} as unknown as ${dto}),`;
491
+ }).join("\n");
492
+ return `(() => {
493
+ const g = new FormGroup({
494
+ ${JSON.stringify(union.discriminator)}: new FormControl<${lit}>(${first}, { nonNullable: true }),
495
+ ${subLines}
496
+ });
497
+ switchDiscriminatedGroup(g, ${JSON.stringify(union.discriminator)});
498
+ return g;
499
+ })()`;
500
+ }
368
501
  function lowerFirst2(s) {
369
502
  return s.length === 0 ? s : s.charAt(0).toLowerCase() + s.slice(1);
370
503
  }
371
- function reactiveEntity(entity, namespace, source, options, zod, imports, logger) {
504
+ function reactiveEntity(entity, namespace, source, options, zod, imports, cyclicRefs, logger) {
372
505
  const deep = options.relations === "deep";
373
506
  const enumZero = enumZeroFromSource(source, entity);
374
507
  imports.value("@angular/core", "Injectable");
@@ -381,6 +514,7 @@ function reactiveEntity(entity, namespace, source, options, zod, imports, logger
381
514
  };
382
515
  const blocks = [];
383
516
  const injectedFactories = /* @__PURE__ */ new Map();
517
+ const warnedUnionFields = /* @__PURE__ */ new Set();
384
518
  for (const variant of ["Create", "Update"]) {
385
519
  const family = deep ? "Deep" : "";
386
520
  const schemaRole = deep ? variant === "Create" ? "createDeepSchema" : "updateDeepSchema" : variant === "Create" ? "createSchema" : "updateSchema";
@@ -393,8 +527,61 @@ function reactiveEntity(entity, namespace, source, options, zod, imports, logger
393
527
  imports.value(`${namespace}/angular/zod-forms.runtime`, "zodValidator");
394
528
  const interfaceName = controlsTypeName(entity.name, variant, family);
395
529
  const formType = formTypeName(entity.name, variant, family);
530
+ const resolvers = {
531
+ enumTypeName: zodEnumTypeName,
532
+ refTypeName: (ref) => {
533
+ const r = zodRefType(zod, namespace, ref);
534
+ imports.type(r.module, r.typeName);
535
+ return r.typeName;
536
+ },
537
+ cyclicRefs
538
+ };
396
539
  const fieldEntries = variantFields(entity, variant).map(
397
- (field) => fieldControlEntry(field, zodEnumTypeName)
540
+ (field) => {
541
+ const union = field.type.kind === "union" ? discriminatedUnion(field, source) : void 0;
542
+ if (union !== void 0) {
543
+ imports.value(
544
+ `${namespace}/angular/zod-forms.runtime`,
545
+ "switchDiscriminatedGroup"
546
+ );
547
+ for (const v2 of union.variants) {
548
+ const targetModule = `${namespace}/angular/${v2.entity}.form`;
549
+ imports.type(
550
+ targetModule,
551
+ controlsTypeName(v2.entity, variant, family)
552
+ );
553
+ if (!injectedFactories.has(v2.entity)) {
554
+ injectedFactories.set(
555
+ v2.entity,
556
+ `${lowerFirst2(v2.entity)}FormFactory`
557
+ );
558
+ imports.value(targetModule, factoryName(v2.entity));
559
+ }
560
+ if (variant === "Update") {
561
+ const role = deep ? "updateDeepType" : "updateType";
562
+ imports.type(
563
+ zodModule(zod, namespace, v2.entity),
564
+ zodSymbol(zod, namespace, v2.entity, role)
565
+ );
566
+ }
567
+ }
568
+ return discriminatedControlEntry(union, field.name, variant, family);
569
+ }
570
+ if (field.type.kind === "union" && !warnedUnionFields.has(field.name)) {
571
+ warnedUnionFields.add(field.name);
572
+ const recursive = unionType(
573
+ field.type,
574
+ resolvers.refTypeName,
575
+ resolvers.enumTypeName,
576
+ cyclicRefs
577
+ ).recursive;
578
+ const cause = recursive ? "a recursive ref branch (chains into a cycle)" : "no discriminator mapping, an alias target, or a list/nullable union";
579
+ logger?.warn(
580
+ `gen-angular: union field '${entity.name}.${field.name}' has no usable discriminated sub-form (${cause}); emitting a free FormControl${recursive ? "<unknown>" : ""} validated by zodValidator`
581
+ );
582
+ }
583
+ return fieldControlEntry(field, resolvers);
584
+ }
398
585
  );
399
586
  const relations = deep ? deepRelations(entity, variant, namespace, logger) : [];
400
587
  const manyRelations = relations.filter((r) => r.many);
@@ -441,15 +628,17 @@ function reactiveEntity(entity, namespace, source, options, zod, imports, logger
441
628
  renderFactoryClass(
442
629
  entity,
443
630
  namespace,
631
+ source,
444
632
  options,
445
633
  zod,
446
634
  injectedFactories,
447
- enumZero
635
+ enumZero,
636
+ cyclicRefs
448
637
  )
449
638
  );
450
639
  return blocks.join("\n\n");
451
640
  }
452
- function renderFactoryClass(entity, namespace, options, zod, injectedFactories, enumZero) {
641
+ function renderFactoryClass(entity, namespace, source, options, zod, injectedFactories, enumZero, cyclicRefs) {
453
642
  const deep = options.relations === "deep";
454
643
  const className = factoryName(entity.name);
455
644
  const ctorParams = [...injectedFactories.entries()].map(
@@ -464,11 +653,13 @@ function renderFactoryClass(entity, namespace, options, zod, injectedFactories,
464
653
  renderFactoryMethod(
465
654
  entity,
466
655
  namespace,
656
+ source,
467
657
  variant,
468
658
  options,
469
659
  zod,
470
660
  injectedFactories,
471
- enumZero
661
+ enumZero,
662
+ cyclicRefs
472
663
  )
473
664
  );
474
665
  }
@@ -506,7 +697,7 @@ export class ${className} {
506
697
  ${body}
507
698
  }`;
508
699
  }
509
- function renderFactoryMethod(entity, namespace, variant, options, zod, injectedFactories, enumZero) {
700
+ function renderFactoryMethod(entity, namespace, source, variant, options, zod, injectedFactories, enumZero, cyclicRefs) {
510
701
  const deep = options.relations === "deep";
511
702
  const family = deep ? "Deep" : "";
512
703
  const typeRole = deep && variant === "Create" ? "createDeepType" : deep && variant === "Update" ? "updateDeepType" : variant === "Create" ? "createType" : "updateType";
@@ -516,13 +707,29 @@ function renderFactoryMethod(entity, namespace, variant, options, zod, injectedF
516
707
  const interfaceName = controlsTypeName(entity.name, variant, family);
517
708
  const formType = formTypeName(entity.name, variant, family);
518
709
  const methodName = factoryMethod(variant);
519
- const zodEnumTypeName = (ref) => ref;
710
+ const resolvers = {
711
+ enumTypeName: (ref) => ref,
712
+ refTypeName: (ref) => zodRefType(zod, namespace, ref).typeName,
713
+ cyclicRefs
714
+ };
520
715
  const fields = variantFields(entity, variant);
521
716
  const lines = fields.map((field) => {
522
- const typeArg = controlType(field, zodEnumTypeName);
717
+ const union = field.type.kind === "union" ? discriminatedUnion(field, source) : void 0;
718
+ if (union !== void 0) {
719
+ return ` ${field.name}: ${discriminatedFactoryExpr(
720
+ union,
721
+ field.name,
722
+ variant,
723
+ deep,
724
+ zod,
725
+ namespace,
726
+ injectedFactories
727
+ )},`;
728
+ }
729
+ const typeArg = controlType(field, resolvers);
523
730
  const accessor = variant === "Create" ? `init?.${field.name}` : `value.${field.name}`;
524
- const source = `${accessor} ?? ${initExpr(field, enumZero)}`;
525
- return ` ${field.name}: ${controlExpr(field, typeArg, source)},`;
731
+ const seed = `${accessor} ?? ${initExpr(field, enumZero)}`;
732
+ return ` ${field.name}: ${controlExpr(field, typeArg, seed)},`;
526
733
  });
527
734
  const relations = deep ? deepRelations(entity, variant, namespace, void 0) : [];
528
735
  const relationLines = relations.map((r) => {
@@ -583,9 +790,12 @@ function signalEntity(entity, namespace, source, options, zod, imports, logger)
583
790
  imports.value(module, schemaId);
584
791
  imports.value(`${namespace}/angular/zod-forms.runtime`, "zodTreeValidate");
585
792
  const fields = variantFields(entity, variant);
586
- const fieldLines = fields.map(
587
- (field) => ` ${field.name}: init?.${field.name} ?? ${initExpr(field, enumZero)},`
588
- );
793
+ const fieldLines = fields.map((field) => {
794
+ if (field.type.kind === "ref" || field.type.kind === "union") {
795
+ return ` ${field.name}: (init?.${field.name} ?? undefined) as ${typeId}[${JSON.stringify(field.name)}],`;
796
+ }
797
+ return ` ${field.name}: init?.${field.name} ?? ${initExpr(field, enumZero)},`;
798
+ });
589
799
  const relations = deep ? deepRelations(entity, variant, namespace, logger) : [];
590
800
  const relationLines = relations.map((rel) => {
591
801
  if (rel.many) {
@@ -625,12 +835,21 @@ ${modelBody}
625
835
  }
626
836
 
627
837
  // src/emit/entity.ts
628
- function emitEntity(entity, namespace, source, options, zod, logger) {
838
+ function emitEntity(entity, namespace, source, options, zod, cyclicRefs, logger) {
629
839
  const imports = new ImportsRecorder();
630
840
  const blocks = [];
631
841
  if (options.forms.includes("reactive")) {
632
842
  blocks.push(
633
- reactiveEntity(entity, namespace, source, options, zod, imports, logger)
843
+ reactiveEntity(
844
+ entity,
845
+ namespace,
846
+ source,
847
+ options,
848
+ zod,
849
+ imports,
850
+ cyclicRefs,
851
+ logger
852
+ )
634
853
  );
635
854
  }
636
855
  if (options.forms.includes("signal")) {
@@ -724,6 +943,53 @@ function collectControls(control: AbstractControl): AbstractControl[] {
724
943
  }
725
944
  return out;
726
945
  }`;
946
+ var SWITCH_DISCRIMINATED_GROUP = `/**
947
+ * Wire a discriminated-union sub-FormGroup: enable only the sub-group named by
948
+ * the discriminator control's value (disable the rest), keep it in sync on
949
+ * valueChanges, and rewrite the group's getRawValue() to the flat
950
+ * active-variant shape a z.discriminatedUnion schema can parse.
951
+ */
952
+ export function switchDiscriminatedGroup(
953
+ group: AbstractControl,
954
+ discriminator: string,
955
+ ): void {
956
+ const controls = (group as unknown as {
957
+ controls: Record<string, AbstractControl>;
958
+ }).controls;
959
+ const selector = controls[discriminator];
960
+ if (selector === undefined) {
961
+ return;
962
+ }
963
+ const variantKeys = Object.keys(controls).filter(
964
+ (key) => key !== discriminator,
965
+ );
966
+
967
+ const apply = (active: unknown): void => {
968
+ for (const key of variantKeys) {
969
+ const sub = controls[key];
970
+ if (sub === undefined) {
971
+ continue;
972
+ }
973
+ if (key === active) {
974
+ sub.enable({ emitEvent: false });
975
+ } else {
976
+ sub.disable({ emitEvent: false });
977
+ }
978
+ }
979
+ };
980
+
981
+ apply(selector.value);
982
+ selector.valueChanges.subscribe(apply);
983
+
984
+ (group as { getRawValue: () => unknown }).getRawValue = () => {
985
+ const active = controls[String(selector.value)];
986
+ const variant =
987
+ active === undefined || active === selector
988
+ ? {}
989
+ : (active.getRawValue() as Record<string, unknown>);
990
+ return { ...variant, [discriminator]: selector.value };
991
+ };
992
+ }`;
727
993
  var ZOD_TREE_VALIDATE = `export function zodTreeValidate<T>(
728
994
  path: SchemaPath<T>,
729
995
  schema: ZodType<T>,
@@ -773,7 +1039,7 @@ function emitRuntime(_source, options) {
773
1039
  imports.push("import type { ZodType } from 'zod';");
774
1040
  const blocks = [];
775
1041
  if (reactive) {
776
- blocks.push(ZOD_VALIDATOR);
1042
+ blocks.push(ZOD_VALIDATOR, SWITCH_DISCRIMINATED_GROUP);
777
1043
  }
778
1044
  if (signal) {
779
1045
  blocks.push(ZOD_TREE_VALIDATE);
@@ -810,6 +1076,13 @@ var angularGenerator = defineGenerator({
810
1076
  for (const [namespace, source] of Object.entries(ctx.ir.sources)) {
811
1077
  const prefix = `${namespace}/angular`;
812
1078
  const entities = Object.values(source.entities);
1079
+ const cyclicRefs = /* @__PURE__ */ new Set();
1080
+ for (const key of ctx.cycles) {
1081
+ const dot = key.indexOf(".");
1082
+ if (dot > 0 && key.slice(0, dot) === namespace) {
1083
+ cyclicRefs.add(key.slice(dot + 1));
1084
+ }
1085
+ }
813
1086
  if (entities.length > 0 && options.forms.length > 0) {
814
1087
  files.push({
815
1088
  path: `${prefix}/zod-forms.runtime.ts`,
@@ -825,6 +1098,7 @@ var angularGenerator = defineGenerator({
825
1098
  source,
826
1099
  options,
827
1100
  zod,
1101
+ cyclicRefs,
828
1102
  ctx.logger
829
1103
  )
830
1104
  });