Skip to content
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

Enhance support for default values #170

Open
wants to merge 22 commits into
base: main
Choose a base branch
from

Conversation

aaronsky
Copy link

@aaronsky aaronsky commented Nov 23, 2022

Fixes #169.

Breaking changes:

  • Booleans are no longer treated as special amongst other properties in the spec. Therefore, if a boolean property is not declared as a required property, it may now render as an optional boolean.

Other changes:

  • Properties with default values have their default values assigned in initializers and decoders, if their type is a specifically-supported builtin (previously this only worked for booleans).
  • Properties with default values that match an enum case will use the enum case as default rather than nil.
  • Required properties with enum types that contain only one case now automatically use the only case as a default value.

@aaronsky aaronsky marked this pull request as ready for review November 23, 2022 19:38
Copy link
Member

@liamnichols liamnichols left a comment

Choose a reason for hiding this comment

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

Wow big thank you for putting this Pull Request up so quickly! I've left an inline comment about the approach we are taking for the initialiser as I think there are a couple of other things we need to consider.

I also want to talk more about using the default value in the decodable implementation because with my understanding of a JSON schema/Open API spec, I don't think that the default keyword was defined for this.

The OpenAPI Docs don't say much about default, but they do say it's aligned with JSON Schema's definition, which states the following:

The default keyword specifies a default value. This value is not used to fill in missing values during the validation process. Non-validation tools such as documentation generators or form generators may use this value to give hints to users about how to use a value. However, default is typically used to express that if a value is missing, then the value is semantically the same as if the value was present with the default value. The value of default should validate against the schema in which it resides, but that isn’t required.

I can see the default value useful as a hit in the generated initialisers, but manipulating the decoded response to fill gaps with it feels like it might be a step too far since nothing suggests that nullable and default are related to each other (thinking outside of Swift).

That being said, I don't know everything so I could be wrong.. Have you compared how some of the other generators in existence use default? I might be swayed if there are other good examples but at the moment I'm leaning towards suggesting that we only apply the default to the initialisers. Let me know what you think, but thanks again for the work here!

Sources/CreateAPI/Generator/Generator+Schemas.swift Outdated Show resolved Hide resolved
Sources/CreateAPI/Generator/Generator+Schemas.swift Outdated Show resolved Hide resolved
@aaronsky
Copy link
Author

I can see the default value useful as a hit in the generated initialisers, but manipulating the decoded response to fill gaps with it feels like it might be a step too far since nothing suggests that nullable and default are related to each other (thinking outside of Swift).

That being said, I don't know everything so I could be wrong.. Have you compared how some of the other generators in existence use default? I might be swayed if there are other good examples but at the moment I'm leaning towards suggesting that we only apply the default to the initialisers.

I can compare output to some of the other big generators. I generally agree that defaults should not be influencing the decision to generate decoder-inits. I'm also leaning towards minimizing the generation of decoder-inits as much as possible, but that would require us to have a little more insight into whether or not synthesis is possible.

@aaronsky
Copy link
Author

I've put up a couple new solutions to the feedback you've given me. Let me know what you think of this once you've reviewed, given your time and energy.

Copy link
Member

@liamnichols liamnichols left a comment

Choose a reason for hiding this comment

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

@aaronsky sorry it took me so long to get back to this! Just to confirm from your last message, my understanding was that we aren't going to provide a default value when decoding via init(from:) and instead would only use the default values in the generated memberwise initialiser?

public var allowAutoMerge: Bool
public var allowAutoMerge: Bool?
Copy link
Member

Choose a reason for hiding this comment

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

Are these changes right? I'm not sure what has caused them but we don't seem t have modified the schema have we?

Copy link
Author

Choose a reason for hiding this comment

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

My understanding of the OpenAPI spec, limited as it may be, is that it is technically more accurate because these properties are not required in their respective definitions.

Copy link
Member

Choose a reason for hiding this comment

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

Oh I see, this property should always have been optional because it is not included in the required array on the schema.

I guess the prior behaviour treated it as non-optional because it had a default value,. If that was the case, I agree that it was likely incorrect so am in favour of fixing it 👍

Do you think it would be possible to pull this bug fix out into its own Pull Request? I'm just being a bit cautious as its quite hard to follow amongst the other changes going on 🙇 It's also something that will likely cause a breaking change to people already using CreateAPI so should be called out as an independent fix in the release notes 🙏

Copy link
Author

Choose a reason for hiding this comment

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

I'm trying to unwind my changes since it's been a few months since I did most of this work, in order to give a definitive answer on this. It might be possible to split this out, but it's not immediately clear.

@aaronsky
Copy link
Author

@aaronsky sorry it took me so long to get back to this! Just to confirm from your last message, my understanding was that we aren't going to provide a default value when decoding via init(from:) and instead would only use the default values in the generated memberwise initialiser?

I believe that's what we agreed on.

@aaronsky aaronsky force-pushed the aaronsky/enhanced-default-values branch from 5ea0288 to 89cdcea Compare February 15, 2023 01:15
@liamnichols
Copy link
Member

@aaronsky sorry it took me so long to get back to this! Just to confirm from your last message, my understanding was that we aren't going to provide a default value when decoding via init(from:) and instead would only use the default values in the generated memberwise initialiser?

I believe that's what we agreed on.

Ah ok then, I ask because I still see the nil coalescing to the default value in the snapshots, like this:

https://github.com/CreateAPI/CreateAPI/pull/170/files#diff-e17d0dd37de44a675de95aef390c1a217a05ce891f7bc9902e7b284b75ec5ed0R92

We shouldn't be doing that, right?

@aaronsky
Copy link
Author

Actually that one is deliberate, because that is the default defined in the github.yml spec

@liamnichols
Copy link
Member

I think that still goes against how we want to interpret default? We want to keep it to be provided as a hint for the memberwise initialiser but we don't really want to start making customisations to the decoder behaviour.

The property is still optional, so if the decoding container doesn't decode the value (either because it was omitted or because it was null?) then we should pass that on still rather than falling back to the default.

With the exception of the separate bug fix, it's important that the only change in generator output is the addition of a default value on the memberwise initialiser rather than behavioural changes to how decoding works. Maybe we could look into seeing if the decoder defaults becomes an opt-in change that we might want to support later on, but I don't think that it should happen by default or in this PR.

Does that sound ok? Thanks for sticking with me on this one as I also try to figure out how we want this to work 😅

@aaronsky
Copy link
Author

@liamnichols I see what you mean now. I've pushed up a revision to (hopefully) address this.

Copy link
Member

@liamnichols liamnichols left a comment

Choose a reason for hiding this comment

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

I looked at the diff a bit closer and now I understand what is going on a bit more, but I am now also more confused 😄

CreateAPI was already using it's 'default' value in the decoder implementation but I didn't notice this because it only supported boolean defaults from the schema itself.

You can see this here:

self.isComplete = try values.decodeIfPresent(Bool.self, forKey: "complete") ?? false

To be honest, this is a really confusing and unexpected behaviour (at least it is to me). If property is nullable/not required, we're still treating it as non-optional by overriding the Codable implementation to fallback to the default value.

It's kind of neat, but it's kind of also confusing and I am still not sure which direction we should take 😄

To me, this feels quite non-standard for a code generator to make that kind of decision. Would this in fact encourage poor schema design? Why is a property nullable in the first place if there is a default value?

Sorry to open a can of worms on this PR given that its existing behaviour, but I guess it's more important as we consider opening it up to more than just booleans.

Comment on lines -424 to +427
let isOptional = property.isOptional && property.defaultValue == nil
output += "\(access)\(isReadonly ? "let" : "var") \(property.name): \(property.type)\(isOptional ? "?" : "")"
output += "\(access)\(isReadonly ? "let" : "var") \(property.name): \(property.type)\(property.isOptional ? "?" : "")"
Copy link
Member

Choose a reason for hiding this comment

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

Ahh so this is what fixes the bug in described in https://github.com/CreateAPI/CreateAPI/pull/170/files#r1047362572

Copy link
Member

Choose a reason for hiding this comment

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

Ahh but it does this because the generated decoder implementation was already using the defaultValue as a fallback 🙈

let defaultValue = (property.isOptional && property.defaultValue != nil) ? " ?? \(property.defaultValue!)" : ""
return "self.\(property.name.accessor) = try values.\(decode)(\(property.type).self, forKey: \(key))\(defaultValue)"

Which is what we said that we should stop doing.

I guess this didn't show up much (at all?) before because it was limited to only booleans. Hmmm, now I'm confused as to what we should do because includeDefaultValues is true by default.

@aaronsky aaronsky force-pushed the aaronsky/enhanced-default-values branch from 8ecd1dd to 26c4ca3 Compare March 19, 2024 11:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Non-optional enum properties with only variant should set a default value
2 participants