Skip to content

Commit

Permalink
feat(schema): add enums function to declare a shared enum in swagger
Browse files Browse the repository at this point in the history
Closes: #2237
  • Loading branch information
Romakita committed Mar 4, 2023
1 parent ea30f5f commit 2deccc0
Show file tree
Hide file tree
Showing 10 changed files with 203 additions and 36 deletions.
60 changes: 51 additions & 9 deletions docs/docs/model.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ json type or when you use a mixed TypeScript types.
</Tab>
</Tabs>

## Nullable <Badge text="6.25.0+"/>
## Nullable

The @@Nullable@@ decorator is used allow a null value on a field while preserving the original Typescript type.

Expand Down Expand Up @@ -326,6 +326,48 @@ class MyController {
}
```

### Set label to an enum <Badge text="7.17.0+"/>

With OpenSpec 3 it's now possible to create shared enum for many models in `components.schemas` instead of having its inlined values in
each model.

Ts.ED introduce a new function `enums()` to declare the enum schema as follows:

```ts
import {enums} from "@tsed/schema";

enum ProductTypes {
ALL = "ALL",
ASSETS = "ASSETS",
FOOD = "FOOD"
}

enums(ProductTypes).label("ProductTypes");

// in models
class Product {
@Property()
title: string;

@Enum(ProductTypes)
type: ProductTypes;
}

// in controller

import {Enum} from "@tsed/schema";
import {QueryParams, Controller} from "@tsed/common";

@Controller("/products")
class ProductsController {
@Get("/:type")
@Returns(200, Array).Of(Product)
async get(@PathParams("type") @Enum(ProductTypes) type: ProductTypes): Promise<Product> {
return [new Product()];
}
}
```

## Constant values

The @@Const@@ decorator is used to restrict a value to a single value. For example, if you only support shipping to the
Expand Down Expand Up @@ -504,7 +546,7 @@ Circular reference can be resolved by using arrow with a @@Property@@ and @@Coll

<<< @/docs/snippets/model/circular-references.ts

## Custom Keys <Badge text="6.17.0+"/>
## Custom Keys

Ts.ED introduces the @@Keyword@@ decorator to declare a new custom validator for Ajv. Combined with the @@CustomKey@@
decorator to add keywords to a property of your class, you can use more complex scenarios than what basic JsonSchema
Expand Down Expand Up @@ -860,7 +902,7 @@ class MyModel {

Now `prop4` will have a `ChildModel` generated along to groups configuration.

## RequiredGroups <Badge text="6.34.0+"/>
## RequiredGroups

As @@Groups@@ decorator, @@RequiredGroups@@ allow you to define when a field is `required` depending on the given groups strategy.

Expand All @@ -886,7 +928,7 @@ class MyModel {
}
```

## AllowedGroups <Badge text="v6.126.0+"/>
## AllowedGroups

This feature let your API consumer to define which field he wants to consume. The server will filter automatically fields based on the @@Groups@@
strategy.
Expand Down Expand Up @@ -1015,7 +1057,7 @@ Expected json:
</Tab>
</Tabs>

## Partial <Badge text="6.58.0+"/>
## Partial

Partial allow you to create a Partial model on an endpoint:

Expand All @@ -1036,7 +1078,7 @@ class MyController {

## Advanced validation

### BeforeDeserialize <Badge text="6.39.0+"/>
### BeforeDeserialize

If you want to validate or manipulate data before the model has been deserialized you can use the @@BeforeDeserialize@@ decorator.

Expand Down Expand Up @@ -1070,7 +1112,7 @@ export class Animal {
}
```

### AfterDeserialize <Badge text="6.39.0+"/>
### AfterDeserialize

If you want to validate or manipulate data after the model has been deserialized you can use the @@AfterDeserialize@@ decorator.

Expand Down Expand Up @@ -1412,7 +1454,7 @@ The used features are the following:
</Tab>
</Tabs>

## Deep object on query <Badge text="6.64.2+"/>
## Deep object on query

With OpenAPI 3, it's possible to describe and use a [deepObject](https://swagger.io/docs/specification/serialization/#query) `style` as Query params.
It means, a consumer can call your endpoint with the following url:
Expand Down Expand Up @@ -1575,7 +1617,7 @@ You can declare schema by using the @@JsonSchemaObject@@ interface:

<<< @/docs/snippets/model/raw-schema-controller.ts

### Using functions <Badge text="6.14.0+"/>
### Using functions

It's also possible to write a valid JsonSchema by using the functional approach (Joi like):

Expand Down
2 changes: 1 addition & 1 deletion packages/di/src/services/InjectorService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ export class InjectorService extends Container {
* @param locals
* @param options
*/
getMany<Type = any>(type: string, locals?: LocalsContainer, options?: Partial<InvokeOptions<Type>>): Type[] {
getMany<Type = any>(type: any, locals?: LocalsContainer, options?: Partial<InvokeOptions<Type>>): Type[] {
return this.getProviders(type).map((provider) => this.invoke(provider.token, locals, options)!);
}

Expand Down
7 changes: 7 additions & 0 deletions packages/specs/schema/src/components/anyMapper.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {JsonLazyRef} from "../domain/JsonLazyRef";
import {JsonSchema} from "../domain/JsonSchema";
import {JsonSchemaOptions} from "../interfaces/JsonSchemaOptions";
import {execMapper, registerJsonSchemaMapper} from "../registries/JsonSchemaMapperContainer";
import {mapGenericsOptions} from "../utils/generics";
Expand All @@ -13,6 +14,12 @@ export function anyMapper(input: any, options: JsonSchemaOptions = {}): any {
return execMapper("lazyRef", input, options);
}

if (input instanceof JsonSchema && input.get("enum") instanceof JsonSchema) {
const enumSchema: JsonSchema = input.get("enum");

return toRef(enumSchema, enumSchema.toJSON(options), options);
}

if ("toJSON" in input) {
const schema = input.toJSON(mapGenericsOptions(options));

Expand Down
1 change: 1 addition & 0 deletions packages/specs/schema/src/components/objectMapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export function objectMapper(input: any, options: JsonSchemaOptions) {
...options,
groups: input?.$forwardGroups || value?.$forwardGroups ? options.groups : undefined
};

// remove groups to avoid bad schema generation over children models
obj[key] = execMapper("item", value, opts);
obj[key] = mapNullableType(obj[key], value, opts);
Expand Down
125 changes: 109 additions & 16 deletions packages/specs/schema/src/decorators/common/enum.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import {enums} from "../../utils/from";
import {getJsonSchema} from "../../utils/getJsonSchema";
import {Enum} from "./enum";

Expand Down Expand Up @@ -57,12 +58,12 @@ describe("@Enum", () => {
});

describe("when is a typescript enum (string)", () => {
enum SomeEnum {
ENUM_1 = "enum1",
ENUM_2 = "enum2"
}

it("should declare prop", () => {
enum SomeEnum {
ENUM_1 = "enum1",
ENUM_2 = "enum2"
}

// WHEN
class Model {
@Enum(SomeEnum)
Expand All @@ -82,12 +83,12 @@ describe("@Enum", () => {
});

describe("when is a typescript enum (index)", () => {
enum SomeEnum {
ENUM_1,
ENUM_2
}

it("should declare prop", () => {
enum SomeEnum {
ENUM_1,
ENUM_2
}

// WHEN
class Model {
@Enum(SomeEnum)
Expand All @@ -107,13 +108,105 @@ describe("@Enum", () => {
});

describe("when is a typescript enum (mixed type)", () => {
enum SomeEnum {
ENUM_1,
ENUM_2 = "test",
ENUM_3 = "test2"
}

it("should declare prop", () => {
enum SomeEnum {
ENUM_1,
ENUM_2 = "test",
ENUM_3 = "test2"
}

// WHEN
class Model {
@Enum(SomeEnum)
num: SomeEnum;
}

expect(getJsonSchema(Model)).toEqual({
properties: {
num: {
enum: [0, "test", "test2"],
type: ["number", "string"]
}
},
type: "object"
});
});
});
describe("when is a typescript enum with a label (set enum schema)", () => {
it("should declare prop with a shared enum in definitions", () => {
enum SomeEnum {
ENUM_1,
ENUM_2 = "test",
ENUM_3 = "test2"
}

const enumValues = enums(SomeEnum).label("SomeEnum");

// WHEN
class Model {
@Enum(enumValues)
num: SomeEnum;
}

expect(getJsonSchema(Model)).toEqual({
definitions: {
SomeEnum: {
enum: [0, "test", "test2"],
type: ["number", "string"]
}
},
properties: {
num: {
$ref: "#/definitions/SomeEnum"
}
},
type: "object"
});
});
});
describe("when is a typescript enum with a label (set enum)", () => {
it("should declare prop with a shared enum in definitions", () => {
enum SomeEnum {
ENUM_1,
ENUM_2 = "test",
ENUM_3 = "test2"
}

enums(SomeEnum).label("SomeEnum");

// WHEN
class Model {
@Enum(SomeEnum)
num: SomeEnum;
}

expect(getJsonSchema(Model)).toEqual({
definitions: {
SomeEnum: {
enum: [0, "test", "test2"],
type: ["number", "string"]
}
},
properties: {
num: {
$ref: "#/definitions/SomeEnum"
}
},
type: "object"
});
});
});

describe("when is a typescript enum schema without label (set enum)", () => {
it("should inline enum", () => {
enum SomeEnum {
ENUM_1,
ENUM_2 = "test",
ENUM_3 = "test2"
}

enums(SomeEnum);

// WHEN
class Model {
@Enum(SomeEnum)
Expand Down
31 changes: 22 additions & 9 deletions packages/specs/schema/src/domain/JsonSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
import type {JSONSchema6, JSONSchema6Definition, JSONSchema6Type, JSONSchema6TypeName, JSONSchema6Version} from "json-schema";
import {IgnoreCallback} from "../interfaces/IgnoreCallback";
import {JsonSchemaOptions} from "../interfaces/JsonSchemaOptions";
import {enumsRegistry} from "../registries/enumRegistries";
import {execMapper} from "../registries/JsonSchemaMapperContainer";
import {NestedGenerics} from "../utils/generics";
import {getComputedType} from "../utils/getComputedType";
Expand Down Expand Up @@ -178,6 +179,10 @@ export class JsonSchema extends Map<string, any> implements NestedGenerics {
return this.get("writeOnly");
}

get hasDiscriminator() {
return !!this.#discriminator;
}

static from(obj: Partial<JsonSchemaObject> = {}) {
return new JsonSchema(obj);
}
Expand Down Expand Up @@ -291,10 +296,6 @@ export class JsonSchema extends Map<string, any> implements NestedGenerics {
return this;
}

get hasDiscriminator() {
return !!this.#discriminator;
}

discriminator() {
this.isDiscriminator = true;
return (this.#discriminator =
Expand Down Expand Up @@ -592,10 +593,23 @@ export class JsonSchema extends Map<string, any> implements NestedGenerics {
* @see https://tools.ietf.org/html/draft-wright-json-schema-validation-01#section-6.23
*/
enum(...enumValues: any[]): this;
enum(enumValue: any | any[], ...enumValues: any[]): this {
const {values, types} = serializeEnumValues([enumValue, enumValues].flat());
enum(enumSchema: JsonSchema): this;
enum(enumValue: any | any[] | JsonSchema, ...enumValues: any[]): this {
if (enumsRegistry.has(enumValue)) {
return this.enum(enumsRegistry.get(enumValue));
}

super.set("enum", values).any(...types);
if (enumValue instanceof JsonSchema) {
if (enumValue.getName()) {
super.set("enum", enumValue);
} else {
super.set("enum", enumValue.get("enum")).any(...enumValue.getJsonType());
}
} else {
const {values, types} = serializeEnumValues([enumValue, enumValues].flat());

super.set("enum", values).any(...types);
}

return this;
}
Expand Down Expand Up @@ -857,7 +871,6 @@ export class JsonSchema extends Map<string, any> implements NestedGenerics {
})
);
} else {
// TODO when OS3 will the only minimal supported version, we'll can remove this code
if (types.length) {
types = uniq(types).map(getJsonType);

Expand Down Expand Up @@ -1004,7 +1017,7 @@ export class JsonSchema extends Map<string, any> implements NestedGenerics {
* Get the symbolic name of the entity
*/
getName() {
return this.get("name") || (this.#target ? nameOf(classOf(this.getComputedType())) : "");
return this.get("name") || (isClass(this.#target) ? nameOf(classOf(this.getComputedType())) : "");
}

clone() {
Expand Down
Loading

0 comments on commit 2deccc0

Please sign in to comment.