Skip to content

Commit 2fe5158

Browse files
authored
Merge pull request #3247 from hey-api/fix/ts-dsl-object-prop-remove
fix: object ts-dsl improvements, ability to remove
2 parents 361d5dc + 7be1561 commit 2fe5158

File tree

8 files changed

+157
-54
lines changed

8 files changed

+157
-54
lines changed

.changeset/big-streets-behave.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@hey-api/openapi-ts': patch
3+
---
4+
5+
**ts-dsl**: allow removing object properties by passing `null`

.changeset/ready-dots-beg.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@hey-api/openapi-ts': patch
3+
---
4+
5+
**ts-dsl**: override object properties when called multiple times with the same name

dev/openapi-ts.config.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -475,7 +475,7 @@ export default defineConfig(() => {
475475
// definitions: 'z{{name}}',
476476
exportFromIndex: true,
477477
// metadata: true,
478-
// name: 'valibot',
478+
name: 'valibot',
479479
// requests: {
480480
// case: 'PascalCase',
481481
// name: '{{name}}Data',
@@ -524,6 +524,7 @@ export default defineConfig(() => {
524524
const additional = ctx.nodes.additionalProperties(ctx);
525525
if (additional === undefined) {
526526
const shape = ctx.nodes.shape(ctx);
527+
shape.prop('body', $(v).attr('never').call());
527528
ctx.nodes.base = () => $(v).attr('looseObject').call(shape);
528529
}
529530
},

packages/openapi-ts/src/ts-dsl/expr/object.ts

Lines changed: 61 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ const Mixed = AsMixin(
2121
export class ObjectTsDsl extends Mixed {
2222
readonly '~dsl' = 'ObjectTsDsl';
2323

24-
protected _props: Array<ObjectPropTsDsl> = [];
24+
protected _props = new Map<string, ObjectPropTsDsl>();
25+
protected _spreadCounter = 0;
2526

2627
constructor(...props: Array<ObjectPropTsDsl>) {
2728
super();
@@ -30,63 +31,101 @@ export class ObjectTsDsl extends Mixed {
3031

3132
override analyze(ctx: AnalysisContext): void {
3233
super.analyze(ctx);
33-
for (const prop of this._props) {
34+
for (const prop of this._props.values()) {
3435
ctx.analyze(prop);
3536
}
3637
}
3738

38-
/** Adds a computed property (e.g. `{ [expr]: value }`). */
39-
computed(name: string, expr: ExprFn): this {
40-
this._props.push(
41-
new ObjectPropTsDsl({ kind: 'computed', name }).value(expr),
42-
);
39+
/** Returns composite key for the property. */
40+
private _propKey(prop: ObjectPropTsDsl): string {
41+
if (prop.kind === 'spread') {
42+
return `spread:${this._spreadCounter++}`;
43+
}
44+
return `${prop.kind}:${prop.propName}`;
45+
}
46+
47+
/** Adds a computed property (e.g. `{ [expr]: value }`), or removes if null. */
48+
computed(name: string, expr: ExprFn | null): this {
49+
if (expr === null) {
50+
this._props.delete(`computed:${name}`);
51+
} else {
52+
this._props.set(
53+
`computed:${name}`,
54+
new ObjectPropTsDsl({ kind: 'computed', name }).value(expr),
55+
);
56+
}
4357
return this;
4458
}
4559

46-
/** Adds a getter property (e.g. `{ get foo() { ... } }`). */
47-
getter(name: string, stmt: StmtFn): this {
48-
this._props.push(new ObjectPropTsDsl({ kind: 'getter', name }).value(stmt));
60+
/** Adds a getter property (e.g. `{ get foo() { ... } }`), or removes if null. */
61+
getter(name: string, stmt: StmtFn | null): this {
62+
if (stmt === null) {
63+
this._props.delete(`getter:${name}`);
64+
} else {
65+
this._props.set(
66+
`getter:${name}`,
67+
new ObjectPropTsDsl({ kind: 'getter', name }).value(stmt),
68+
);
69+
}
4970
return this;
5071
}
5172

5273
/** Returns true if object has at least one property or spread. */
5374
hasProps(): boolean {
54-
return this._props.length > 0;
75+
return this._props.size > 0;
5576
}
5677

5778
/** Returns true if object has no properties or spreads. */
5879
get isEmpty(): boolean {
59-
return this._props.length === 0;
80+
return this._props.size === 0;
6081
}
6182

62-
/** Adds a property assignment. */
63-
prop(name: string, expr: ExprFn): this {
64-
this._props.push(new ObjectPropTsDsl({ kind: 'prop', name }).value(expr));
83+
/** Adds a property assignment, or removes if null. */
84+
prop(name: string, expr: ExprFn | null): this {
85+
if (expr === null) {
86+
this._props.delete(`prop:${name}`);
87+
} else {
88+
this._props.set(
89+
`prop:${name}`,
90+
new ObjectPropTsDsl({ kind: 'prop', name }).value(expr),
91+
);
92+
}
6593
return this;
6694
}
6795

6896
/** Adds multiple properties. */
6997
props(...props: ReadonlyArray<ObjectPropTsDsl>): this {
70-
this._props.push(...props);
98+
for (const prop of props) {
99+
this._props.set(this._propKey(prop), prop);
100+
}
71101
return this;
72102
}
73103

74-
/** Adds a setter property (e.g. `{ set foo(v) { ... } }`). */
75-
setter(name: string, stmt: StmtFn): this {
76-
this._props.push(new ObjectPropTsDsl({ kind: 'setter', name }).value(stmt));
104+
/** Adds a setter property (e.g. `{ set foo(v) { ... } }`), or removes if null. */
105+
setter(name: string, stmt: StmtFn | null): this {
106+
if (stmt === null) {
107+
this._props.delete(`setter:${name}`);
108+
} else {
109+
this._props.set(
110+
`setter:${name}`,
111+
new ObjectPropTsDsl({ kind: 'setter', name }).value(stmt),
112+
);
113+
}
77114
return this;
78115
}
79116

80117
/** Adds a spread property (e.g. `{ ...options }`). */
81118
spread(expr: ExprFn): this {
82-
this._props.push(new ObjectPropTsDsl({ kind: 'spread' }).value(expr));
119+
const key = `spread:${this._spreadCounter++}`;
120+
this._props.set(key, new ObjectPropTsDsl({ kind: 'spread' }).value(expr));
83121
return this;
84122
}
85123

86124
override toAst() {
125+
const props = [...this._props.values()];
87126
const node = ts.factory.createObjectLiteralExpression(
88-
this.$node(this._props),
89-
this.$multiline(this._props.length),
127+
this.$node(props),
128+
this.$multiline(props.length),
90129
);
91130
return this.$hint(node);
92131
}

packages/openapi-ts/src/ts-dsl/expr/prop.ts

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,13 @@ import { IdTsDsl } from './id';
1212

1313
type Expr = NodeName | MaybeTsDsl<ts.Expression>;
1414
type Stmt = NodeName | MaybeTsDsl<ts.Statement>;
15-
type Kind = 'computed' | 'getter' | 'prop' | 'setter' | 'spread';
15+
16+
export type ObjectPropKind =
17+
| 'computed'
18+
| 'getter'
19+
| 'prop'
20+
| 'setter'
21+
| 'spread';
1622

1723
type Meta =
1824
| { kind: 'computed'; name: string }
@@ -27,19 +33,26 @@ export class ObjectPropTsDsl extends Mixed {
2733
readonly '~dsl' = 'ObjectPropTsDsl';
2834

2935
protected _value?: Ref<Expr | Stmt>;
30-
protected meta: Meta;
36+
protected _meta: Meta;
3137

3238
constructor(meta: Meta) {
3339
super();
34-
this.meta = meta;
40+
this._meta = meta;
41+
}
42+
43+
get kind(): ObjectPropKind {
44+
return this._meta.kind;
45+
}
46+
47+
get propName(): string | undefined {
48+
return this._meta.name;
3549
}
3650

3751
override analyze(ctx: AnalysisContext): void {
3852
super.analyze(ctx);
3953
ctx.analyze(this._value);
4054
}
4155

42-
/** Returns true when all required builder calls are present. */
4356
get isValid(): boolean {
4457
return this.missingRequiredCalls().length === 0;
4558
}
@@ -56,7 +69,7 @@ export class ObjectPropTsDsl extends Mixed {
5669
override toAst() {
5770
this.$validate();
5871
const node = this.$node(this._value);
59-
if (this.meta.kind === 'spread') {
72+
if (this._meta.kind === 'spread') {
6073
if (ts.isStatement(node)) {
6174
throw new Error(
6275
'Invalid spread: object spread must be an expression, not a statement.',
@@ -65,19 +78,19 @@ export class ObjectPropTsDsl extends Mixed {
6578
const result = ts.factory.createSpreadAssignment(node);
6679
return this.$docs(result);
6780
}
68-
if (this.meta.kind === 'getter') {
69-
const getter = new GetterTsDsl(this.meta.name).do(node);
81+
if (this._meta.kind === 'getter') {
82+
const getter = new GetterTsDsl(this._meta.name).do(node);
7083
const result = this.$node(getter);
7184
return this.$docs(result);
7285
}
73-
if (this.meta.kind === 'setter') {
74-
const setter = new SetterTsDsl(this.meta.name).do(node);
86+
if (this._meta.kind === 'setter') {
87+
const setter = new SetterTsDsl(this._meta.name).do(node);
7588
const result = this.$node(setter);
7689
return this.$docs(result);
7790
}
78-
if (ts.isIdentifier(node) && node.text === this.meta.name) {
91+
if (ts.isIdentifier(node) && node.text === this._meta.name) {
7992
const result = ts.factory.createShorthandPropertyAssignment(
80-
this.meta.name,
93+
this._meta.name,
8194
);
8295
return this.$docs(result);
8396
}
@@ -87,24 +100,24 @@ export class ObjectPropTsDsl extends Mixed {
87100
);
88101
}
89102
const result = ts.factory.createPropertyAssignment(
90-
this.meta.kind === 'computed'
103+
this._meta.kind === 'computed'
91104
? ts.factory.createComputedPropertyName(
92-
this.$node(new IdTsDsl(this.meta.name)),
105+
this.$node(new IdTsDsl(this._meta.name)),
93106
)
94-
: this.$node(safePropName(this.meta.name)),
107+
: this.$node(safePropName(this._meta.name)),
95108
node,
96109
);
97110
return this.$docs(result);
98111
}
99112

100113
$validate(): asserts this is this & {
101114
_value: Expr | Stmt;
102-
kind: Kind;
115+
kind: ObjectPropKind;
103116
} {
104117
const missing = this.missingRequiredCalls();
105118
if (missing.length === 0) return;
106119
throw new Error(
107-
`Object property${this.meta.name ? ` "${this.meta.name}"` : ''} missing ${missing.join(' and ')}`,
120+
`Object property${this._meta.name ? ` "${this._meta.name}"` : ''} missing ${missing.join(' and ')}`,
108121
);
109122
}
110123

packages/openapi-ts/src/ts-dsl/type/idx-sig.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { DocMixin } from '../mixins/doc';
1111
import { ReadonlyMixin } from '../mixins/modifiers';
1212

1313
export type TypeIdxSigType = string | MaybeTsDsl<ts.TypeNode>;
14+
export type TypeIdxSigKind = 'idxSig';
1415

1516
const Mixed = DocMixin(ReadonlyMixin(TsDsl<ts.IndexSignatureDeclaration>));
1617

@@ -27,6 +28,16 @@ export class TypeIdxSigTsDsl extends Mixed {
2728
fn?.(this);
2829
}
2930

31+
/** Element kind. */
32+
get kind(): TypeIdxSigKind {
33+
return 'idxSig';
34+
}
35+
36+
/** Index signature parameter name. */
37+
get propName(): string {
38+
return this.name.toString();
39+
}
40+
3041
override analyze(ctx: AnalysisContext): void {
3142
super.analyze(ctx);
3243
ctx.analyze(this._key);

packages/openapi-ts/src/ts-dsl/type/object.ts

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,40 +11,58 @@ export class TypeObjectTsDsl extends Mixed {
1111
readonly '~dsl' = 'TypeObjectTsDsl';
1212
override scope: NodeScope = 'type';
1313

14-
protected props: Array<TypePropTsDsl | TypeIdxSigTsDsl> = [];
14+
protected _props = new Map<string, TypePropTsDsl | TypeIdxSigTsDsl>();
1515

1616
override analyze(ctx: AnalysisContext): void {
1717
super.analyze(ctx);
18-
for (const prop of this.props) {
18+
for (const prop of this._props.values()) {
1919
ctx.analyze(prop);
2020
}
2121
}
2222

23-
/** Returns true if object has at least one property or spread. */
23+
/** Returns true if object has at least one property or index signature. */
2424
hasProps(): boolean {
25-
return this.props.length > 0;
25+
return this._props.size > 0;
2626
}
2727

28-
/** Adds an index signature to the object type. */
29-
idxSig(name: string, fn: (i: TypeIdxSigTsDsl) => void): this {
30-
const idx = new TypeIdxSigTsDsl(name, fn);
31-
this.props.push(idx);
28+
/** Adds an index signature to the object type, or removes if fn is null. */
29+
idxSig(name: string, fn: ((i: TypeIdxSigTsDsl) => void) | null): this {
30+
const key = `idxSig:${name}`;
31+
if (fn === null) {
32+
this._props.delete(key);
33+
} else {
34+
this._props.set(key, new TypeIdxSigTsDsl(name, fn));
35+
}
3236
return this;
3337
}
3438

35-
/** Returns true if object has no properties or spreads. */
39+
/** Returns true if object has no properties or index signatures. */
3640
get isEmpty(): boolean {
37-
return !this.props.length;
41+
return this._props.size === 0;
42+
}
43+
44+
/** Adds a property signature, or removes if fn is null. */
45+
prop(name: string, fn: ((p: TypePropTsDsl) => void) | null): this {
46+
const key = `prop:${name}`;
47+
if (fn === null) {
48+
this._props.delete(key);
49+
} else {
50+
this._props.set(key, new TypePropTsDsl(name, fn));
51+
}
52+
return this;
3853
}
3954

40-
/** Adds a property signature (returns property builder). */
41-
prop(name: string, fn: (p: TypePropTsDsl) => void): this {
42-
const prop = new TypePropTsDsl(name, fn);
43-
this.props.push(prop);
55+
/** Adds multiple properties/index signatures. */
56+
props(...members: ReadonlyArray<TypePropTsDsl | TypeIdxSigTsDsl>): this {
57+
for (const member of members) {
58+
this._props.set(`${member.kind}:${member.propName}`, member);
59+
}
4460
return this;
4561
}
4662

4763
override toAst() {
48-
return ts.factory.createTypeLiteralNode(this.$node(this.props));
64+
return ts.factory.createTypeLiteralNode(
65+
this.$node([...this._props.values()]),
66+
);
4967
}
5068
}

packages/openapi-ts/src/ts-dsl/type/prop.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { TokenTsDsl } from '../token';
1616
import { safePropName } from '../utils/name';
1717

1818
export type TypePropType = NodeName | MaybeTsDsl<ts.TypeNode>;
19+
export type TypePropKind = 'prop';
1920

2021
const Mixed = DocMixin(OptionalMixin(ReadonlyMixin(TsDsl<ts.TypeElement>)));
2122

@@ -31,6 +32,16 @@ export class TypePropTsDsl extends Mixed {
3132
fn(this);
3233
}
3334

35+
/** Element kind. */
36+
get kind(): TypePropKind {
37+
return 'prop';
38+
}
39+
40+
/** Property name. */
41+
get propName(): string {
42+
return this.name.toString();
43+
}
44+
3445
override analyze(ctx: AnalysisContext): void {
3546
super.analyze(ctx);
3647
ctx.analyze(this._type);

0 commit comments

Comments
 (0)