Skip to content

Pass through required properties to allOf/anyOf/oneOf children #342

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Aug 15, 2022

Conversation

hedgepigdaniel
Copy link
Contributor

@hedgepigdaniel hedgepigdaniel commented Feb 15, 2022

Including #336, but:

  • extended to also work for anyOf/oneOf.
  • modified to avoid a bug where parent fields were incorrectly marked as required

@@ -15947,13 +15947,13 @@ export class Api<SecurityDataType extends unknown> extends HttpClient<SecurityDa
| { status?: "queued" | "in_progress"; [key: string]: any }
| ({ status?: "completed"; [key: string]: any } & { status?: "queued" | "in_progress"; [key: string]: any })
) & {
name: string;
name?: string;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is correct

head_sha: string;
details_url?: string;
external_id?: string;
status?: "queued" | "in_progress" | "completed";
started_at?: string;
conclusion?: "success" | "failure" | "neutral" | "cancelled" | "skipped" | "timed_out" | "action_required";
conclusion: "success" | "failure" | "neutral" | "cancelled" | "skipped" | "timed_out" | "action_required";
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This unfortunately is incorrect. This is the abbridged/relevant schema:

"schema": {
  "type": "object",
  "properties": {
    "name": {
      "type": "string",
      "description": "The name of the check. For example, \"code-coverage\"."
    },
    "status": {
      "type": "string",
      "description": "The current status. Can be one of `queued`, `in_progress`, or `completed`.",
      "enum": ["queued", "in_progress", "completed"]
    },
    "conclusion": {
      "type": "string",
      "description": "**Required if you provide `completed_at` or a `status` of `completed`**. The final conclusion of the check. Can be one of `success`, `failure`, `neutral`, `cancelled`, `skipped`, `timed_out`, or `action_required`.  \n**Note:** Providing `conclusion` will automatically set the `status` parameter to `completed`. Only GitHub can change a check run conclusion to `stale`.",
      "enum": [
        "success",
        "failure",
        "neutral",
        "cancelled",
        "skipped",
        "timed_out",
        "action_required"
      ]
    }
  },
  "anyOf": [
    {
      "properties": {
        "status": {
          "enum": ["completed"]
        }
      },
      "required": ["conclusion"],
      "additionalProperties": true
    },
    {
      "properties": {
        "status": {
          "enum": ["queued", "in_progress"]
        }
      },
      "additionalProperties": true
    }
  ]
},

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As in, conclusion is required, but only if status is completed. But the generated type indicates that it is required in all cases, which is incorrect.

src/schema.js Outdated
@@ -181,13 +181,19 @@ const filterContents = (contents, types) => _.filter(contents, (type) => !_.incl
const complexSchemaParsers = {
[SCHEMA_TYPES.COMPLEX_ONE_OF]: (schema) => {
// T1 | T2
const combined = _.map(schema.oneOf, complexTypeGetter);
const combined = _.map(
schema.oneOf.map((s) => _.merge({ required: schema.required }, s)),
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This logic isn't quite right, because it assumes that schema.required makes sense in the parent schema. But like the example in "full-swagger-scheme` shows, this isn't always the case.

Fixing this properly would require the logic in complexSchemaParsers to be lifted up a bit (i.e. if there is an anyOf/oneOf/allOf, then the entire type of the parent schema needs to be mapped, not just the fields in the anyOf.

But maybe a good first step is just to filter schema.required down to those fields that are directly defined in the nested schema. That way it would at least tighten the type for simple cases, without introducing this bug.

@hedgepigdaniel
Copy link
Contributor Author

The last commit fixes the bug I mentioned with required referencing fields defined on the parent.

@@ -178,16 +178,57 @@ const getObjectTypeContent = (schema) => {
const complexTypeGetter = (schema) => getInlineParseContent(schema);
const filterContents = (contents, types) => _.filter(contents, (type) => !_.includes(types, type));

const makeAddRequiredToChildSchema = (parentSchema) => (childSchema) => {
let required = childSchema.required || [];

Choose a reason for hiding this comment

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

LTS for Node 12 is ending in April 2022 so depending on the level of support swagger-typescript-api provides to Node you could use null coalescing (i.e. ??)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, I tried to avoid that because personally (unfortunately) I still use this with Node.js 12. IMO the best way to address this is to add a build step, but that's out of scope for this PR.

@@ -178,16 +178,57 @@ const getObjectTypeContent = (schema) => {
const complexTypeGetter = (schema) => getInlineParseContent(schema);
const filterContents = (contents, types) => _.filter(contents, (type) => !_.includes(types, type));

const makeAddRequiredToChildSchema = (parentSchema) => (childSchema) => {

Choose a reason for hiding this comment

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

Imo it would be simpler to read if you did:

({ required = [], properties = [], ...other }) => 

Instead of

(childSchema) => 

As it keeps default logic/values all in one place

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Could be. I personally prefer creating a new variable instead of reassigning function parameters (destructured or not). But happy to change depending on what the maintainers prefer.

// Identify properties that are required in the child schema, but
// defined only in the parent schema (TODO: this only works one level deep)
const parentPropertiesRequiredByChild = required.filter(
(key) => !_.keys(childSchema.properties).includes(key) && _.keys(parentSchema.properties).includes(key),

Choose a reason for hiding this comment

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

I believe childSchema.properties should be just properties

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It could be, but I think childSchema.properties makes more sense here, since it's referring to the properties that are in the child schema, not the properties variable that will be modifiied and returned (which includes properties that are not in the child schema).

(key) =>
!required.includes(key) && (_.keys(properties).includes(key) || _.keys(parentSchema.properties).includes(key)),
),
);

Choose a reason for hiding this comment

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

Goes back to my point about Node support but: You could use a Set instead of !required.includes(key). I think it'd be slightly clearer in terms of what you're trying to achieve (I assume avoiding duplicates)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, honestly I was just trying to stick to the convention, the surrounding code uses lodash data structures so I used them too 🤷 - happy to change if the maintainers prefer.

Choose a reason for hiding this comment

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

Yep fair enough. Leave it up to the author :)

required = required.concat(
(parentSchema.required || []).filter(
(key) =>
!required.includes(key) && (_.keys(properties).includes(key) || _.keys(parentSchema.properties).includes(key)),

Choose a reason for hiding this comment

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

_.keys(properties).includes(key) || _.keys(parentSchema.properties).includes(key)

Is it necessary to filter out keys that do not exist in the properties? I wouldn't have thought it would cause any issues if they were added

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not sure, but given that logically required is a set, the most correct thing seems to be to avoid duplicates.

// Add such properties to the child so that they can be overriden and made required
properties = {
...properties,
...parentPropertiesRequiredByChild.reduce(

Choose a reason for hiding this comment

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

Why only fields required by the child?

To my knowledge allOf should apply ALL fields, regardless of whether they're optional/required. Are optional fields being handled elsewhere in this code base and if so shouldn't this code live in the same spot as that logic?

Choose a reason for hiding this comment

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

Maybe more importantly: Rather than overriding the original properties I believe the childSchema's properties and the parent schema's properties should be merged together so that both the child and parent's type conditions are validated against. As per: https://json-schema.org/understanding-json-schema/reference/combining.html#allof

the given data must be valid against all of the given subschemas

Maybe it's just out of scope for this PR?

Copy link
Contributor Author

@hedgepigdaniel hedgepigdaniel Feb 22, 2022

Choose a reason for hiding this comment

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

As per: https://json-schema.org/understanding-json-schema/reference/combining.html#allof

the given data must be valid against all of the given subschemas

I think that's referring to all of the subschemas, not the combination of parent/children. Although yes, my understanding is that it must be valid against parent and child.

Why only fields required by the child?
the childSchema's properties and the parent schema's properties should be merged together

These fields aren't being added to check that their shape is correct. The generated types for oneOf/anyOf already inherit from the parent schema type at the typescript level - so effectively they are already merged.

The reason to add them here (duplicating the definitions from the parent) is so that they can be marked as required in typescript. Hence, this only includes fields that are defined in the parent, but made required in the child.

Another option might be to do it in typescript, e.g.

type ChildSchema = ParentSchema & {
  child_field: string;
  parent_field_made_optional_in_the_child: Exclude<undefined, ParentSchema['parent_field_made_optional_in_the_child']>
};

This would handle fields from grandparent schemas, and nparent schemas - but it requires the parent schema to have a name, which currently it doesn't.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Are optional fields being handled elsewhere in this code base and if so shouldn't this code live in the same spot as that logic?

I'm not sure what you mean - open to suggestions if you have them.

>;
}
| ({ description: string } & {
files: Record<
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@PatrickShaw notice that files has exactly the same shape as the one on (new) line 10665. The entire definition is duplicated here - not because the shape is different, just so that it can be made a compulsory property. The type from the parent schema starts on line 10663 - and it has no name, so we can't refer to it here.

@js2me
Copy link
Member

js2me commented Aug 15, 2022

Hello @hedgepigdaniel !
Thanks for your PR!

@js2me js2me added the merge label Aug 15, 2022
@js2me js2me merged commit 5a09a7a into acacode:next Aug 15, 2022
js2me added a commit that referenced this pull request Aug 15, 2022
* Feature: allow passing "patch" option to "swagger2openapi" (#283)

Co-authored-by: cassel <[email protected]>

* Loads prettier config from user's project (#286)

* fix: problem with missing HttpResponse type with --to-js option

* fix: missing extractRequestBody field in type

* fix(reject): fix reject and exit with code 1 if error (#295)

* fix(reject): fix reject and exit with code 1 if error

* feat: add a parameter to sort types and types properties (#299)

Co-authored-by: rcatoio <[email protected]>

* feat: add an option to disable throwing an error when request.ok is not true (#297)

Co-authored-by: rcatoio <[email protected]>
Co-authored-by: Sergey S. Volkov <[email protected]>

* fix: fix missing `title` of object type (#303)

* chore: refresh auto generated code; chore: add axios to dev deps

* bump: up to 9.3.0

* bump: up to 9.3.1; fix: axios

* chore: fix typo (#376)

* 304 Support readOnly properties (#365)

* allow mutually exclusive input options (#327)

Co-authored-by: Sergey S. Volkov <[email protected]>

* Update fetch-http-client.eta (#329)

* BREAKING_CHANGE: add --extract-response-body option; bump: version to 9.4.0

* feat: added authorization token to headers
Co-authored-by: MarcinFilipek <[email protected]>

* feat: --extract-response-error option

* chore: refresh code gen tests

* fix: change COMPLEX_NOT_OF to COMPLEX_NOT (#356)

* Fix request format and allow for files in request - With code review changes (#350)

* fix pick format
allow for file array in form data requests

* fixed code review suggestions

Co-authored-by: Daniele De Matteo <[email protected]>
Co-authored-by: Sergey S. Volkov <[email protected]>

* Pass through required properties to allOf/anyOf/oneOf children (#342)

* Pass through required properties to allOf children

* extend fix to anyOf and oneOf

* fix: account for children making parent fields required

Co-authored-by: Anders Cassidy <[email protected]>
Co-authored-by: Daniel Playfair Cal <[email protected]>
Co-authored-by: Sergey S. Volkov <[email protected]>

* Add option to change main Api class name (#320)

* Update api.eta

* Update index.d.ts

* Update index.js

* Update index.js

* Update config.js

Co-authored-by: Sergey S. Volkov <[email protected]>

* fix: missing stringifyFormItem method; bump: up version to 10.0.0

* docs: update docs, changelog, fix: problem with complex types (makeAddRequiredToChildSchema)

Co-authored-by: Xavier Cassel <[email protected]>
Co-authored-by: cassel <[email protected]>
Co-authored-by: Adrian Wieprzkowicz <[email protected]>
Co-authored-by: EvgenBabenko <[email protected]>
Co-authored-by: RoCat <[email protected]>
Co-authored-by: rcatoio <[email protected]>
Co-authored-by: 卡色 <[email protected]>
Co-authored-by: 江麻妞 <[email protected]>
Co-authored-by: Kasper Moskwiak <[email protected]>
Co-authored-by: Ben Watkins <[email protected]>
Co-authored-by: bonukai <[email protected]>
Co-authored-by: baggoedw <[email protected]>
Co-authored-by: Marcus Dunn <[email protected]>
Co-authored-by: Daniele De Matteo <[email protected]>
Co-authored-by: Daniel Playfair Cal <[email protected]>
Co-authored-by: Anders Cassidy <[email protected]>
Co-authored-by: Daniel Playfair Cal <[email protected]>
@js2me
Copy link
Member

js2me commented Aug 17, 2022

@all-contributors please add @hedgepigdaniel for code bugs and add @PatrickShaw for review

@allcontributors
Copy link
Contributor

@js2me

I could not determine your intention.

Basic usage: @all-contributors please add @Someone for code, doc and infra

For other usages see the documentation

@js2me
Copy link
Member

js2me commented Aug 17, 2022

@all-contributors please add @hedgepigdaniel for code bugs

@allcontributors
Copy link
Contributor

@js2me

I've put up a pull request to add @hedgepigdaniel! 🎉

@js2me
Copy link
Member

js2me commented Aug 17, 2022

@all-contributors please add @PatrickShaw for review

@allcontributors
Copy link
Contributor

@js2me

I've put up a pull request to add @PatrickShaw! 🎉

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants