Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions __mocks__/api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,21 @@ definitions:
properties:
unlimited:
type: boolean
OneOfPropertyTest:
type: object
description: test if we can use `oneOf` as an object property
properties:
fields:
oneOf:
- type: object
properties:
foo:
type: string
- type: object
properties:
bar:
type: string
- $ref: "#/definitions/Person"
AdditionalPropsTest:
type: object
additionalProperties:
Expand Down
15 changes: 15 additions & 0 deletions __mocks__/openapi_v3/api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,21 @@ components:
properties:
unlimited:
type: boolean
OneOfPropertyTest:
type: object
description: test if we can use `oneOf` as an object property
properties:
fields:
oneOf:
- type: object
properties:
foo:
type: string
- type: object
properties:
bar:
type: string
- $ref: "#/components/schemas/Person"
AllOfWithOneElementTest:
description: test if we can use allOf with just one element inside
allOf:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,78 @@ export type OneOfTest = t.TypeOf<typeof OneOfTest>;
"
`;

exports[`Openapi V2 |> gen-api-models should generate an object with a union property when that property has oneOf: oneof-property-test 1`] = `
"/**
* Do not edit this file it is auto-generated by io-utils / gen-api-models.
* See https://github.com/pagopa/io-utils
*/
/* eslint-disable */

import * as t from \\"io-ts\\";
import { Person } from \\"./Person\\";

/**
* test if we can use \`oneOf\` as an object property
*/

// required attributes
const OneOfPropertyTestFields1R = t.interface({});

// optional attributes
const OneOfPropertyTestFields1O = t.partial({
foo: t.string
});

export const OneOfPropertyTestFields1 = t.intersection(
[OneOfPropertyTestFields1R, OneOfPropertyTestFields1O],
\\"OneOfPropertyTestFields1\\"
);

export type OneOfPropertyTestFields1 = t.TypeOf<
typeof OneOfPropertyTestFields1
>;

// required attributes
const OneOfPropertyTestFields2R = t.interface({});

// optional attributes
const OneOfPropertyTestFields2O = t.partial({
bar: t.string
});

export const OneOfPropertyTestFields2 = t.intersection(
[OneOfPropertyTestFields2R, OneOfPropertyTestFields2O],
\\"OneOfPropertyTestFields2\\"
);

export type OneOfPropertyTestFields2 = t.TypeOf<
typeof OneOfPropertyTestFields2
>;

export const OneOfPropertyTestFields = t.union(
[OneOfPropertyTestFields1, OneOfPropertyTestFields2, Person],
\\"OneOfPropertyTestFields\\"
);

export type OneOfPropertyTestFields = t.TypeOf<typeof OneOfPropertyTestFields>;

// required attributes
const OneOfPropertyTestR = t.interface({});

// optional attributes
const OneOfPropertyTestO = t.partial({
fields: OneOfPropertyTestFields
});

export const OneOfPropertyTest = t.intersection(
[OneOfPropertyTestR, OneOfPropertyTestO],
\\"OneOfPropertyTest\\"
);

export type OneOfPropertyTest = t.TypeOf<typeof OneOfPropertyTest>;
"
`;

exports[`Openapi V2 |> gen-api-models should generate decoder definitions for (get, /test-auth-bearer) 1`] = `
"
/****************************************************************
Expand Down Expand Up @@ -2196,6 +2268,78 @@ export type OneOfTest = t.TypeOf<typeof OneOfTest>;
"
`;

exports[`Openapi V3 |> gen-api-models should generate an object with a union property when that property has oneOf: oneof-property-test 1`] = `
"/**
* Do not edit this file it is auto-generated by io-utils / gen-api-models.
* See https://github.com/pagopa/io-utils
*/
/* eslint-disable */

import * as t from \\"io-ts\\";
import { Person } from \\"./Person\\";

/**
* test if we can use \`oneOf\` as an object property
*/

// required attributes
const OneOfPropertyTestFields1R = t.interface({});

// optional attributes
const OneOfPropertyTestFields1O = t.partial({
foo: t.string
});

export const OneOfPropertyTestFields1 = t.intersection(
[OneOfPropertyTestFields1R, OneOfPropertyTestFields1O],
\\"OneOfPropertyTestFields1\\"
);

export type OneOfPropertyTestFields1 = t.TypeOf<
typeof OneOfPropertyTestFields1
>;

// required attributes
const OneOfPropertyTestFields2R = t.interface({});

// optional attributes
const OneOfPropertyTestFields2O = t.partial({
bar: t.string
});

export const OneOfPropertyTestFields2 = t.intersection(
[OneOfPropertyTestFields2R, OneOfPropertyTestFields2O],
\\"OneOfPropertyTestFields2\\"
);

export type OneOfPropertyTestFields2 = t.TypeOf<
typeof OneOfPropertyTestFields2
>;

export const OneOfPropertyTestFields = t.union(
[OneOfPropertyTestFields1, OneOfPropertyTestFields2, Person],
\\"OneOfPropertyTestFields\\"
);

export type OneOfPropertyTestFields = t.TypeOf<typeof OneOfPropertyTestFields>;

// required attributes
const OneOfPropertyTestR = t.interface({});

// optional attributes
const OneOfPropertyTestO = t.partial({
fields: OneOfPropertyTestFields
});

export const OneOfPropertyTest = t.intersection(
[OneOfPropertyTestR, OneOfPropertyTestO],
\\"OneOfPropertyTest\\"
);

export type OneOfPropertyTest = t.TypeOf<typeof OneOfPropertyTest>;
"
`;

exports[`Openapi V3 |> gen-api-models should generate decoder definitions for (get, /test-auth-bearer) 1`] = `
"
/****************************************************************
Expand Down
17 changes: 17 additions & 0 deletions src/commands/gen-api-models/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,23 @@ describe.each`
expect(code).toMatchSnapshot("oneof-test");
});

it("should generate an object with a union property when that property has oneOf", async () => {
const definitonName = "OneOfPropertyTest";
const definition = getDefinitionOrFail(spec, definitonName);

const code = await renderDefinitionCode(
definitonName,
getParser(spec).parseDefinition(
// @ts-ignore
definition
),
false
);

expect(code).toContain("t.union");
expect(code).toMatchSnapshot("oneof-property-test");
});

Comment on lines +353 to +369

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added tests in 4c70dee

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Amazing, thanks! In addition to that, could you please include some scenarios where we expect things to go wrong? For example:

  const variant5 = {
    fields: [{ name: 42 }]
  };

it("should generate a type union from allOf when x-one-of is used", async () => {
if (version === 2) {
const definitonName = "AllOfOneOfTest";
Expand Down
72 changes: 47 additions & 25 deletions templates/macros.njk
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,15 @@
{{ defineConst(definition.default, definitionName, typedef, false, inline) }}
{% endmacro %}

{##
# defines a oneOf property
#}
{% macro defineOneOfProperty(parentPropName, propName, definition, inline = false) -%}
{{- 'import * as t from "io-ts";' | addImport -}}
{% set definitionName %}{{ parentPropName }}{{ propName | capitalizeFirst }}{% endset %}
{{ definitionName }}
Comment thread
giovanniberti marked this conversation as resolved.
Outdated
{% endmacro %}

{##
# defines an object property of some prop.type
#}
Expand All @@ -290,6 +299,8 @@
{{ defineString(propName, prop, true) }}
{% elif prop.type == "boolean" %}
{{ defineBoolean(propName, prop, true) }}
{% elif prop.oneOf %}
{{ defineOneOfProperty(parentPropName, propName, prop.oneOf, true)}}
{% else %}
// TODO: generate model for definition "{{ propName }}: {{ prop.type }}"
{% endif %}
Expand Down Expand Up @@ -323,6 +334,38 @@
{% endfor %}
{% endmacro -%}

{% macro defineOneOf(definitionName, prop, strictInterfaces, camelCasedPropNames) -%}
{{- 'import * as t from "io-ts";' | addImport -}}

{% for schema in prop -%}
{% if schema.type == "object" %}
{% set name %}{{ definitionName }}{{ loop.index }}{% endset %}
{{ defineObject(name, schema, strictInterfaces, camelCasedPropNames) }}
{% elif schema.$ref %}
{%- set realPropName = schema.$ref | splitBy("/") | last -%}
{{ importLocalProp(realPropName) }}
{% elif schema.type == "array" %}
{% set name %}{{ definitionName }}{{ loop.index }}{% endset %}
{{ defineArray(name, schema) }}
{% endif %}
Comment thread
giovanniberti marked this conversation as resolved.
{% endfor %}

export const {{ definitionName }} =
t.union([
{% for schema in prop -%}
{% if schema.type == "object" or schema.type == "array" %}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is covered by the snapshot (specifically the generated constants named OneOfPropertyTestFieldsN like here)

Would you suggest a separate test case?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I see from generated code is:

export const OneOfPropertyTestFields4 = t.readonlyArray(
  t.object,
  "array of object"
);

Inline array object definitions are not supported, unfortunately. :(

{{ definitionName }}{{ loop.index }},
{% elif schema.$ref %}
{{ schema.$ref | splitBy("/") | last }},
{% endif %}
{% endfor %}
],
"{{ definitionName }}"
);

export type {{ definitionName }} = t.TypeOf<typeof {{ definitionName }}>;
{% endmacro -%}

{##
# define object properties recursively,
# supports additionaProperties, allOf and oneOf.
Expand All @@ -341,6 +384,9 @@
{% if prop.type == "object" %}
{% set composedPropName %}{{ definitionName }}{{ propName | capitalizeFirst }}{% endset %}
{{ defineObject(composedPropName, prop, strictInterfaces, camelCasedPropNames) }}
{% elif prop.oneOf %}
{% set composedPropName %}{{ definitionName }}{{ propName | capitalizeFirst }}{% endset %}
{{ defineOneOf(composedPropName, prop.oneOf, strictInterfaces, camelCasedPropNames) }}
{% endif %}
{% endfor %}

Expand Down Expand Up @@ -418,31 +464,7 @@
{% elif definition.oneOf %}
{% set oneOfProps = definition.oneOf if definition.oneOf else definition.allOf %}

{{- 'import * as t from "io-ts";' | addImport -}}

{% for schema in oneOfProps -%}
{% if schema.type == "object" %}
{{ defineObject(definitionName + loop.index, schema, strictInterfaces, camelCasedPropNames) }}
{% elif schema.$ref %}
{%- set realPropName = schema.$ref | splitBy("/") | last -%}
{{ importLocalProp(realPropName) }}
{% endif %}
{% endfor %}

export const {{ definitionName }} =
t.union([
{% for schema in oneOfProps -%}
{% if schema.type == "object" %}
{{ definitionName + loop.index }},
{% elif schema.$ref %}
{{ schema.$ref | splitBy("/") | last }},
{% endif %}
{% endfor %}
],
"{{ definitionName }}"
);

export type {{ definitionName }} = t.TypeOf<typeof {{ definitionName }}>;
{{ defineOneOf(definitionName, oneOfProps, strictInterfaces, camelCasedPropNames) }}

{% elif definition.type == "number" %}

Expand Down